1import {
2 Credential,
3 validateCredential,
4 type CredentialScope,
5} from "@/Neon/Credential";
6import { Branch } from "@/Neon/Branch";
7import { Project } from "@/Neon/Project";
8import { providers } from "@/Neon/Providers";
9import * as Test from "@/Test/Alchemy";
10import * as SDK from "@distilled.cloud/neon";
11import { expect } from "alchemy-test";
12import * as Effect from "effect/Effect";
13import * as Redacted from "effect/Redacted";
14import * as Result from "effect/Result";
15
16const { test } = Test.make({ providers: providers() });
17
18test.provider(
19 "credential reveals without rotation and replaces scopes",
20 (stack) =>
21 Effect.gen(function* () {
22 yield* stack.destroy();
23 const deploy = (scopes: CredentialScope[]) =>
24 stack.deploy(
25 Effect.gen(function* () {
26 const project = yield* Project("CredentialProject", {
27 region: "aws-us-east-2",
28 });
29 const credential = yield* Credential("Reader", { project, scopes });
30 const named = yield* Credential("Named", {
31 project,
32 scopes,
33 name: "named-storage-credential",
34 });
35 return { project, credential, named };
36 }),
37 );
38 const first = yield* deploy(["storage:read"]);
39 const second = yield* deploy(["storage:read"]);
40 expect(second.credential.tokenId).toBe(first.credential.tokenId);
41 const scope = {
42 project_id: first.credential.projectId,
43 branch_id: first.credential.branchId,
44 };
45 const listed = yield* SDK.listCredentials(scope);
46 expect(
47 listed.credentials.filter(
48 (credential) => credential.token_id === first.credential.tokenId,
49 ),
50 ).toHaveLength(1);
51 const revealed = yield* SDK.revealCredential({
52 ...scope,
53 token_id: first.credential.tokenId,
54 });
55 expect(
56 Redacted.value(first.credential.apiToken) ===
57 (Redacted.isRedacted(revealed.api_token)
58 ? Redacted.value(revealed.api_token)
59 : revealed.api_token),
60 ).toBe(true);
61 expect(Redacted.isRedacted(second.credential.s3SecretAccessKey)).toBe(
62 true,
63 );
64 const changed = yield* deploy(["storage:write"]);
65 expect(changed.credential.tokenId).not.toBe(first.credential.tokenId);
66 expect(changed.named.tokenId).not.toBe(first.named.tokenId);
67 expect(changed.named.name).toBe(first.named.name);
68 expect(
69 (yield* SDK.listCredentials(scope)).credentials.some(
70 (credential) =>
71 credential.token_id === first.named.tokenId &&
72 !credential.revoked_at,
73 ),
74 ).toBe(false);
75 const old = yield* SDK.revealCredential({
76 ...scope,
77 token_id: first.credential.tokenId,
78 }).pipe(
79 Effect.as(false),
80 Effect.catchTag("NotFound", () => Effect.succeed(true)),
81 );
82 expect(old).toBe(true);
83 yield* stack.destroy();
84 yield* stack.destroy();
85 }),
86 {
87 tags: [
88 "provider:neon",
89 "provider:neon:credential",
90 "provider:neon:project",
91 "live",
92 ],
93 timeout: 120_000,
94 },
95);
96
97test.provider(
98 "credential override validates ancestry scopes and out-of-band revocation",
99 (stack) =>
100 Effect.gen(function* () {
101 yield* stack.destroy();
102 const program = (
103 scopes: CredentialScope[] = ["storage:read", "storage:write"],
104 ) =>
105 Effect.gen(function* () {
106 const project = yield* Project("LineageProject", {
107 region: "aws-us-east-2",
108 });
109 const left = yield* Branch("Left", { project });
110 const child = yield* Branch("Child", { project, parentBranch: left });
111 const sibling = yield* Branch("Sibling", { project });
112 const credential = yield* Credential("Writer", {
113 branch: left,
114 scopes,
115 });
116 return { project, left, child, sibling, credential };
117 });
118 const first = yield* stack.deploy(program());
119 yield* validateCredential(first.credential, first.child, "storage:read");
120 yield* validateCredential(first.credential, first.left, "storage:write");
121 const sibling = yield* validateCredential(
122 first.credential,
123 first.sibling,
124 "storage:read",
125 ).pipe(Effect.result);
126 expect(Result.isFailure(sibling)).toBe(true);
127 if (Result.isFailure(sibling))
128 expect(sibling.failure._tag).toBe("CredentialRecoveryError");
129 const scope = yield* validateCredential(
130 first.credential,
131 first.left,
132 "ai_gateway:invoke",
133 ).pipe(Effect.result);
134 expect(Result.isFailure(scope)).toBe(true);
135 yield* SDK.revokeCredential({
136 project_id: first.credential.projectId,
137 branch_id: first.credential.branchId,
138 token_id: first.credential.tokenId,
139 });
140 // Reordering equal scopes schedules reconciliation without changing permissions.
141 const recovered = yield* stack.deploy(
142 program(["storage:write", "storage:read"]),
143 );
144 expect(recovered.credential.tokenId).not.toBe(first.credential.tokenId);
145 expect(recovered.credential.name).toBe(first.credential.name);
146 yield* stack.destroy();
147 yield* stack.destroy();
148 }),
149 {
150 tags: [
151 "provider:neon",
152 "provider:neon:branch",
153 "provider:neon:credential",
154 "provider:neon:project",
155 "live",
156 ],
157 timeout: 120_000,
158 },
159);