1import * as NodeCryptoLayer from "@effect/platform-node/NodeCrypto";
2import { describe, expect, it } from "@effect/vitest";
3import { PgDialect } from "drizzle-orm/pg-core";
4import * as Effect from "effect/Effect";
5import * as Layer from "effect/Layer";
6
7import * as RelayDb from "../db.ts";
8import { relayEnvironmentCredentials } from "../persistence/schema.ts";
9import * as EnvironmentCredentials from "./EnvironmentCredentials.ts";
10
11describe("EnvironmentCredentials", () => {
12 it.effect("reports the credential creation persistence stage and preserves its cause", () => {
13 const cause = new Error("database unavailable");
14 const fakeDb = {
15 insert: (table: unknown) => {
16 expect(table).toBe(relayEnvironmentCredentials);
17 return {
18 values: () => Effect.void,
19 };
20 },
21 update: (table: unknown) => {
22 expect(table).toBe(relayEnvironmentCredentials);
23 return {
24 set: () => ({
25 where: () => Effect.fail(cause),
26 }),
27 };
28 },
29 } as unknown as RelayDb.RelayDb["Service"];
30
31 return Effect.gen(function* () {
32 const credentials = yield* EnvironmentCredentials.EnvironmentCredentials;
33 const error = yield* Effect.flip(
34 credentials.create({
35 environmentId: "env_test",
36 environmentPublicKey: "sensitive-public-key-material",
37 }),
38 );
39
40 expect(error).toMatchObject({
41 _tag: "EnvironmentCredentialCreatePersistenceError",
42 stage: "revoke-previous-credentials",
43 environmentId: "env_test",
44 });
45 expect(error.credentialId).toMatch(/^[0-9a-f]{64}$/);
46 expect(error.cause).toBe(cause);
47 expect(error).not.toHaveProperty("environmentPublicKey");
48 }).pipe(
49 Effect.provide(
50 EnvironmentCredentials.layer.pipe(
51 Layer.provide(NodeCryptoLayer.layer),
52 Layer.provide(Layer.succeed(RelayDb.RelayDb, fakeDb)),
53 ),
54 ),
55 );
56 });
57
58 it.effect("does not retain credential tokens when lookup persistence fails", () => {
59 const cause = new Error("database unavailable");
60 const token = "t3env_sensitive-credential-token";
61 const whereConditions: Array<unknown> = [];
62 const fakeDb = {
63 select: () => ({
64 from: (table: unknown) => {
65 expect(table).toBe(relayEnvironmentCredentials);
66 return {
67 where: (condition: unknown) => ({
68 limit: () => {
69 whereConditions.push(condition);
70 return Effect.fail(cause);
71 },
72 }),
73 };
74 },
75 }),
76 } as unknown as RelayDb.RelayDb["Service"];
77
78 return Effect.gen(function* () {
79 const credentials = yield* EnvironmentCredentials.EnvironmentCredentials;
80 const error = yield* Effect.flip(credentials.authenticate(token));
81
82 expect(error).toMatchObject({
83 _tag: "EnvironmentCredentialAuthenticatePersistenceError",
84 stage: "lookup-credential",
85 });
86 expect(error.cause).toBe(cause);
87 expect(error).not.toHaveProperty("token");
88 expect(whereConditions).toHaveLength(1);
89
90 const query = new PgDialect().sqlToQuery(whereConditions[0] as never);
91 expect(query.sql).toContain("exists");
92 expect(query.sql).toContain('"relay_environment_links"."environment_id"');
93 expect(query.sql).toContain('"relay_environment_links"."environment_public_key"');
94 expect(query.sql).toContain('"relay_environment_links"."revoked_at" is null');
95 }).pipe(
96 Effect.provide(
97 EnvironmentCredentials.layer.pipe(
98 Layer.provide(NodeCryptoLayer.layer),
99 Layer.provide(Layer.succeed(RelayDb.RelayDb, fakeDb)),
100 ),
101 ),
102 );
103 });
104
105 it.effect(
106 "creates opaque credentials and revokes only older credentials for the same key",
107 () => {
108 const insertedValues: Array<{
109 readonly credentialId: string;
110 readonly environmentId: string;
111 readonly environmentPublicKey: string;
112 readonly credentialHash: string;
113 readonly revokedAt: null;
114 readonly createdAt: string;
115 readonly updatedAt: string;
116 }> = [];
117 const staleCredentialRevocations: Array<{
118 readonly values: Record<string, unknown>;
119 readonly condition: unknown;
120 }> = [];
121
122 const fakeDb = {
123 insert: (table: unknown) => {
124 expect(table).toBe(relayEnvironmentCredentials);
125 return {
126 values: (values: (typeof insertedValues)[number]) => {
127 insertedValues.push(values);
128 return Effect.void;
129 },
130 };
131 },
132 update: (table: unknown) => {
133 expect(table).toBe(relayEnvironmentCredentials);
134 return {
135 set: (values: Record<string, unknown>) => ({
136 where: (condition: unknown) => {
137 staleCredentialRevocations.push({ values, condition });
138 return Effect.void;
139 },
140 }),
141 };
142 },
143 } as unknown as RelayDb.RelayDb["Service"];
144
145 return Effect.gen(function* () {
146 const credentials = yield* EnvironmentCredentials.EnvironmentCredentials;
147 const token = yield* credentials.create({
148 environmentId: "env_test",
149 environmentPublicKey: "environment-public-key",
150 });
151 const [, credentialId, secret] = token.split("_");
152
153 expect(token).toMatch(/^t3env_[0-9a-f]{64}_[0-9a-f]{96}$/);
154 expect(credentialId).toHaveLength(64);
155 expect(secret).toHaveLength(96);
156 expect(insertedValues).toHaveLength(1);
157 expect(insertedValues[0]).toMatchObject({
158 credentialId,
159 environmentId: "env_test",
160 environmentPublicKey: "environment-public-key",
161 revokedAt: null,
162 });
163 expect(insertedValues[0]?.credentialHash).toMatch(/^[A-Za-z0-9_-]{43}$/);
164 expect(insertedValues[0]?.credentialHash).not.toContain(token);
165 expect(insertedValues[0]?.createdAt).toBe(insertedValues[0]?.updatedAt);
166 expect(staleCredentialRevocations).toHaveLength(1);
167 expect(staleCredentialRevocations[0]?.values.revokedAt).toEqual(
168 staleCredentialRevocations[0]?.values.updatedAt,
169 );
170
171 const query = new PgDialect().sqlToQuery(staleCredentialRevocations[0]?.condition as never);
172 expect(query.sql).toContain('"relay_environment_credentials"."environment_id" = $1');
173 expect(query.sql).toContain(
174 '"relay_environment_credentials"."environment_public_key" = $2',
175 );
176 expect(query.sql).toContain('"relay_environment_credentials"."credential_id" <> $3');
177 expect(query.sql).toContain('"relay_environment_credentials"."revoked_at" is null');
178 expect(query.params).toEqual(["env_test", "environment-public-key", credentialId]);
179 }).pipe(
180 Effect.provide(
181 EnvironmentCredentials.layer.pipe(
182 Layer.provide(NodeCryptoLayer.layer),
183 Layer.provide(Layer.succeed(RelayDb.RelayDb, fakeDb)),
184 ),
185 ),
186 );
187 },
188 );
189
190 it.effect("revokes active credentials for an environment public key", () => {
191 const updateValues: Array<Record<string, unknown>> = [];
192 const whereConditions: Array<unknown> = [];
193 const fakeDb = {
194 update: (table: unknown) => {
195 expect(table).toBe(relayEnvironmentCredentials);
196 return {
197 set: (values: Record<string, unknown>) => {
198 updateValues.push(values);
199 return {
200 where: (condition: unknown) => {
201 whereConditions.push(condition);
202 return {
203 returning: (selection: unknown) => {
204 expect(selection).toBeDefined();
205 return Effect.succeed([{ credentialId: "credential-1" }]);
206 },
207 };
208 },
209 };
210 },
211 };
212 },
213 } as unknown as RelayDb.RelayDb["Service"];
214
215 return Effect.gen(function* () {
216 const credentials = yield* EnvironmentCredentials.EnvironmentCredentials;
217 const revoked = yield* credentials.revokeForEnvironmentPublicKey({
218 environmentId: "env_test",
219 environmentPublicKey: "environment-public-key",
220 });
221
222 expect(revoked).toBe(true);
223 expect(updateValues).toHaveLength(1);
224 expect(updateValues[0]?.revokedAt).toEqual(updateValues[0]?.updatedAt);
225 expect(whereConditions).toHaveLength(1);
226
227 const query = new PgDialect().sqlToQuery(whereConditions[0] as never);
228 expect(query.sql).toContain('"relay_environment_credentials"."environment_id" = $1');
229 expect(query.sql).toContain('"relay_environment_credentials"."environment_public_key" = $2');
230 expect(query.sql).toContain('"relay_environment_credentials"."revoked_at" is null');
231 expect(query.sql).toContain("not exists");
232 expect(query.sql).toContain('"relay_environment_links"."environment_id" = $3');
233 expect(query.sql).toContain('"relay_environment_links"."environment_public_key" = $4');
234 expect(query.sql).toContain('"relay_environment_links"."revoked_at" is null');
235 expect(query.params).toEqual([
236 "env_test",
237 "environment-public-key",
238 "env_test",
239 "environment-public-key",
240 ]);
241 }).pipe(
242 Effect.provide(
243 EnvironmentCredentials.layer.pipe(
244 Layer.provide(NodeCryptoLayer.layer),
245 Layer.provide(Layer.succeed(RelayDb.RelayDb, fakeDb)),
246 ),
247 ),
248 );
249 });
250});