1import * as Cloudflare from "@/Cloudflare";
2import * as Neon from "@/Neon";
3import * as Test from "@/Test/Alchemy";
4import * as SDK from "@distilled.cloud/neon";
5import { expect } from "alchemy-test";
6import * as Effect from "effect/Effect";
7import * as Layer from "effect/Layer";
8import * as Schedule from "effect/Schedule";
9import * as HttpClient from "effect/http/HttpClient";
10import BackendHttpWorker from "./fixtures/backend-http-worker.ts";
11import { backendBranch } from "./fixtures/backend-resources.ts";
12
13const { test } = Test.make({
14 providers: Layer.mergeAll(Neon.providers(), Cloudflare.providers()),
15});
16
17test.provider(
18 "Worker HTTP bindings own an AI-scoped credential and revoke it with the host",
19 (stack) =>
20 Effect.gen(function* () {
21 yield* stack.destroy();
22 const application = (includeWorker: boolean) =>
23 Effect.gen(function* () {
24 const branch = yield* backendBranch;
25 const worker = includeWorker ? yield* BackendHttpWorker : undefined;
26 return { branch, url: worker?.url };
27 });
28 const deployed = yield* stack.deploy(application(true));
29 const http = HttpClient.filterStatusOk(yield* HttpClient.HttpClient);
30 const response = yield* http.get(deployed.url!).pipe(
31 Effect.retry({
32 schedule: Schedule.exponential("200 millis"),
33 times: 7,
34 }),
35 );
36 expect(response.status).toBe(200);
37 expect(yield* response.json).toMatchObject({
38 hasToken: true,
39 sharedToken: true,
40 });
41 const request = {
42 project_id: deployed.branch.projectId,
43 branch_id: deployed.branch.branchId,
44 };
45 const credentials = (yield* SDK.listCredentials(
46 request,
47 )).credentials.filter(
48 (item) =>
49 item.principal_type === "user" &&
50 item.branch_id === deployed.branch.branchId &&
51 !item.revoked_at,
52 );
53 expect(credentials).toHaveLength(1);
54 expect(credentials[0].scopes).toContain("ai_gateway:invoke");
55 expect(credentials[0].scopes).not.toContain("storage:write");
56 yield* stack.deploy(application(false));
57 const remaining = (yield* SDK.listCredentials(
58 request,
59 )).credentials.filter(
60 (item) => item.token_id === credentials[0].token_id && !item.revoked_at,
61 );
62 expect(remaining).toHaveLength(0);
63 yield* stack.destroy();
64 }),
65 {
66 tags: [
67 "provider:cloudflare",
68 "provider:cloudflare:worker",
69 "provider:neon",
70 "provider:neon:aigateway",
71 "provider:neon:auth",
72 "provider:neon:branch",
73 "provider:neon:dataapi",
74 "provider:neon:project",
75 "live",
76 ],
77 timeout: 120_000,
78 },
79);