1import { EnvironmentId } from "@t3tools/contracts";
2import { describe, expect, it } from "@effect/vitest";
3import * as Schema from "effect/Schema";
4import * as Effect from "effect/Effect";
5import * as Option from "effect/Option";
6
7import * as TokenStore from "../authorization/tokenStore.ts";
8import {
9 BearerConnectionCredential,
10 BearerConnectionProfile,
11 BearerConnectionRegistration,
12 RelayConnectionRegistration,
13 SshConnectionProfile,
14 SshConnectionRegistration,
15} from "../connection/catalog.ts";
16import {
17 BearerConnectionTarget,
18 ConnectionTransientError,
19 PrimaryConnectionTarget,
20 RelayConnectionTarget,
21 SshConnectionTarget,
22} from "../connection/model.ts";
23import {
24 GitHubRoutingPermissions,
25 makeGitHubRoutingPermissions,
26} from "../connection/githubRoutingPermissions.ts";
27import {
28 ConnectionCatalogDocument,
29 EMPTY_CONNECTION_CATALOG_DOCUMENT,
30 catalogRoutes,
31 setRoutesInCatalog,
32 putRemoteDpopTokenInCatalog,
33 registerConnectionInCatalog,
34 removeConnectionFromCatalog,
35 setConnectionEnabledInCatalog,
36} from "./storageDocument.ts";
37
38const decodeConnectionCatalogDocument = Schema.decodeUnknownEffect(ConnectionCatalogDocument);
39
40const ENVIRONMENT_ID = EnvironmentId.make("environment-1");
41const decodeCatalogDocument = Schema.decodeUnknownSync(ConnectionCatalogDocument);
42
43const RELAY_TARGET = new RelayConnectionTarget({
44 environmentId: ENVIRONMENT_ID,
45 label: "Remote",
46});
47const BEARER_TARGET = new BearerConnectionTarget({
48 environmentId: ENVIRONMENT_ID,
49 label: "Remote",
50 connectionId: "bearer-1",
51});
52const BEARER_PROFILE = new BearerConnectionProfile({
53 connectionId: BEARER_TARGET.connectionId,
54 environmentId: ENVIRONMENT_ID,
55 label: BEARER_TARGET.label,
56 httpBaseUrl: "https://remote.example.test",
57 wsBaseUrl: "wss://remote.example.test",
58});
59const BEARER_CREDENTIAL = new BearerConnectionCredential({
60 token: "bearer-token",
61});
62const REMOTE_TOKEN = new TokenStore.RemoteDpopAccessToken({
63 environmentId: ENVIRONMENT_ID,
64 label: "Remote",
65 endpoint: {
66 httpBaseUrl: "https://remote.example.test",
67 wsBaseUrl: "wss://remote.example.test",
68 providerKind: "cloudflare_tunnel",
69 },
70 accessToken: "dpop-token",
71 expiresAtEpochMs: 1_000_000,
72 dpopThumbprint: "thumbprint",
73});
74
75describe("ConnectionCatalogDocument", () => {
76 it.effect("persists explicit GitHub trust and forgets it when a connection is removed", () =>
77 Effect.gen(function* () {
78 let document = EMPTY_CONNECTION_CATALOG_DOCUMENT;
79 const entry = { target: BEARER_TARGET, profile: Option.some(BEARER_PROFILE), enabled: true };
80 const storage = {
81 read: Effect.sync(() => document.githubRoutingPermissions ?? []),
82 write: (githubRoutingPermissions: NonNullable<typeof document.githubRoutingPermissions>) =>
83 Effect.gen(function* () {
84 document = yield* decodeConnectionCatalogDocument({
85 ...document,
86 githubRoutingPermissions,
87 }).pipe(Effect.orDie);
88 }),
89 };
90 const permissions = yield* makeGitHubRoutingPermissions(storage);
91 expect(yield* permissions.get(entry)).toBe("off");
92 yield* permissions.set(entry, "read");
93 const restarted = yield* makeGitHubRoutingPermissions(storage);
94 expect(yield* restarted.get(entry)).toBe("read");
95
96 const changedEndpoint = {
97 ...entry,
98 profile: Option.some(
99 new BearerConnectionProfile({
100 ...BEARER_PROFILE,
101 httpBaseUrl: "https://different.example.test",
102 }),
103 ),
104 };
105 expect(yield* restarted.get(changedEndpoint)).toBe("off");
106 expect(
107 yield* restarted.get({
108 ...entry,
109 profile: Option.some(
110 new BearerConnectionProfile({
111 ...BEARER_PROFILE,
112 wsBaseUrl: "wss://different.example.test",
113 }),
114 ),
115 }),
116 ).toBe("off");
117 expect(
118 yield* restarted.get({ target: RELAY_TARGET, profile: Option.none(), enabled: true }),
119 ).toBe("off");
120 yield* restarted.set(entry, "read-write");
121 expect(yield* restarted.get(entry)).toBe("read-write");
122 yield* restarted.forget(ENVIRONMENT_ID);
123 expect(yield* restarted.get(entry)).toBe("off");
124 const afterRemoval = yield* makeGitHubRoutingPermissions(storage);
125 expect(yield* afterRemoval.get(entry)).toBe("off");
126
127 yield* permissions.set(entry, "read");
128 document = removeConnectionFromCatalog(document, ENVIRONMENT_ID);
129 const afterCatalogRemoval = yield* makeGitHubRoutingPermissions(storage);
130 expect(yield* afterCatalogRemoval.get(entry)).toBe("off");
131 }),
132 );
133
134 it.effect("requires explicit trust for primary, relay, and SSH connections independently", () =>
135 Effect.gen(function* () {
136 const permissions = yield* makeGitHubRoutingPermissions({
137 read: Effect.succeed([]),
138 write: () => Effect.void,
139 });
140 const entries = [
141 {
142 target: new PrimaryConnectionTarget({
143 environmentId: ENVIRONMENT_ID,
144 label: "Local",
145 httpBaseUrl: "http://localhost:3000",
146 wsBaseUrl: "ws://localhost:3000",
147 }),
148 profile: Option.none(),
149 enabled: true,
150 },
151 { target: RELAY_TARGET, profile: Option.none(), enabled: true },
152 {
153 enabled: true,
154 target: new SshConnectionTarget({
155 environmentId: ENVIRONMENT_ID,
156 label: "SSH",
157 connectionId: "ssh-1",
158 }),
159 profile: Option.some(
160 new SshConnectionProfile({
161 environmentId: ENVIRONMENT_ID,
162 label: "SSH",
163 connectionId: "ssh-1",
164 target: { alias: "work", hostname: "work.example.test", username: "maria", port: 22 },
165 }),
166 ),
167 },
168 ];
169 for (const entry of entries) {
170 expect(yield* permissions.get(entry)).toBe("off");
171 yield* permissions.set(entry, "read");
172 expect(yield* permissions.get(entry)).toBe("read");
173 yield* permissions.set(entry, "off");
174 expect(yield* permissions.get(entry)).toBe("off");
175 }
176 }),
177 );
178
179 it.effect("does not enable GitHub routing when permission persistence fails", () =>
180 Effect.gen(function* () {
181 const entry = { target: BEARER_TARGET, profile: Option.some(BEARER_PROFILE), enabled: true };
182 expect(yield* (yield* GitHubRoutingPermissions).get(entry)).toBe("off");
183 const permissions = yield* makeGitHubRoutingPermissions({
184 read: Effect.succeed([]),
185 write: () =>
186 Effect.fail(
187 new ConnectionTransientError({
188 reason: "remote-unavailable",
189 detail: "storage unavailable",
190 }),
191 ),
192 });
193 yield* permissions.set(entry, "read-write").pipe(Effect.flip);
194 expect(yield* permissions.get(entry)).toBe("off");
195 }),
196 );
197
198 it.each([
199 { name: "legacy", accountId: undefined },
200 { name: "account-bound", accountId: "account-1" },
201 ])("round-trips a catalog containing a $name DPoP token", ({ accountId }) => {
202 const token = new TokenStore.RemoteDpopAccessToken({
203 ...REMOTE_TOKEN,
204 ...(accountId === undefined ? {} : { accountId }),
205 });
206 const document = {
207 ...EMPTY_CONNECTION_CATALOG_DOCUMENT,
208 targets: [RELAY_TARGET],
209 remoteDpopTokens: [token],
210 };
211 const schema = Schema.fromJsonString(ConnectionCatalogDocument);
212 const restored = Schema.decodeSync(schema)(Schema.encodeSync(schema)(document));
213
214 expect(restored).toEqual(document);
215 expect(restored.remoteDpopTokens[0]?.accountId).toBe(accountId);
216 });
217
218 it("registers a bearer connection as one catalog mutation", () => {
219 const document = registerConnectionInCatalog(
220 EMPTY_CONNECTION_CATALOG_DOCUMENT,
221 new BearerConnectionRegistration({
222 target: BEARER_TARGET,
223 profile: BEARER_PROFILE,
224 credential: BEARER_CREDENTIAL,
225 }),
226 );
227
228 expect(document.targets).toEqual([BEARER_TARGET]);
229 expect(document.profiles).toEqual([BEARER_PROFILE]);
230 expect(document.credentials).toEqual([
231 {
232 connectionId: BEARER_TARGET.connectionId,
233 credential: BEARER_CREDENTIAL,
234 },
235 ]);
236 });
237
238 it("replaces obsolete connection metadata without discarding a reusable DPoP token", () => {
239 const bearer = registerConnectionInCatalog(
240 {
241 ...EMPTY_CONNECTION_CATALOG_DOCUMENT,
242 remoteDpopTokens: [REMOTE_TOKEN],
243 },
244 new BearerConnectionRegistration({
245 target: BEARER_TARGET,
246 profile: BEARER_PROFILE,
247 credential: BEARER_CREDENTIAL,
248 }),
249 );
250 const relayTarget = new RelayConnectionTarget({
251 environmentId: ENVIRONMENT_ID,
252 label: "Remote",
253 });
254 const relay = registerConnectionInCatalog(
255 bearer,
256 new RelayConnectionRegistration({ target: relayTarget }),
257 );
258
259 expect(relay.targets).toEqual([relayTarget]);
260 expect(relay.profiles).toEqual([]);
261 expect(relay.credentials).toEqual([]);
262 expect(relay.remoteDpopTokens).toEqual([REMOTE_TOKEN]);
263 });
264
265 it("removes every catalog record owned by an explicit disconnect", () => {
266 const registered = registerConnectionInCatalog(
267 {
268 ...EMPTY_CONNECTION_CATALOG_DOCUMENT,
269 remoteDpopTokens: [REMOTE_TOKEN],
270 },
271 new BearerConnectionRegistration({
272 target: BEARER_TARGET,
273 profile: BEARER_PROFILE,
274 credential: BEARER_CREDENTIAL,
275 }),
276 );
277
278 expect(removeConnectionFromCatalog(registered, ENVIRONMENT_ID)).toEqual(
279 EMPTY_CONNECTION_CATALOG_DOCUMENT,
280 );
281 });
282
283 it("stores a DPoP token for a registered relay environment", () => {
284 const registered = registerConnectionInCatalog(
285 EMPTY_CONNECTION_CATALOG_DOCUMENT,
286 new RelayConnectionRegistration({ target: RELAY_TARGET }),
287 );
288
289 expect(putRemoteDpopTokenInCatalog(registered, REMOTE_TOKEN)).toEqual({
290 ...registered,
291 remoteDpopTokens: [REMOTE_TOKEN],
292 });
293 });
294
295 it("ignores a late DPoP token after the environment is removed", () => {
296 const registered = registerConnectionInCatalog(
297 EMPTY_CONNECTION_CATALOG_DOCUMENT,
298 new RelayConnectionRegistration({ target: RELAY_TARGET }),
299 );
300 const removed = removeConnectionFromCatalog(registered, ENVIRONMENT_ID);
301
302 expect(putRemoteDpopTokenInCatalog(removed, REMOTE_TOKEN)).toEqual(
303 EMPTY_CONNECTION_CATALOG_DOCUMENT,
304 );
305 });
306
307 it("removes a DPoP token stored before the environment is removed", () => {
308 const registered = registerConnectionInCatalog(
309 EMPTY_CONNECTION_CATALOG_DOCUMENT,
310 new RelayConnectionRegistration({ target: RELAY_TARGET }),
311 );
312 const refreshed = putRemoteDpopTokenInCatalog(registered, REMOTE_TOKEN);
313
314 expect(removeConnectionFromCatalog(refreshed, ENVIRONMENT_ID)).toEqual(
315 EMPTY_CONNECTION_CATALOG_DOCUMENT,
316 );
317 });
318
319 it("does not store DPoP tokens for a different environment or connection kind", () => {
320 const bearer = registerConnectionInCatalog(
321 EMPTY_CONNECTION_CATALOG_DOCUMENT,
322 new BearerConnectionRegistration({
323 target: BEARER_TARGET,
324 profile: BEARER_PROFILE,
325 credential: BEARER_CREDENTIAL,
326 }),
327 );
328 const otherRelay = registerConnectionInCatalog(
329 EMPTY_CONNECTION_CATALOG_DOCUMENT,
330 new RelayConnectionRegistration({
331 target: new RelayConnectionTarget({
332 environmentId: EnvironmentId.make("environment-2"),
333 label: "Other relay",
334 }),
335 }),
336 );
337
338 expect(putRemoteDpopTokenInCatalog(bearer, REMOTE_TOKEN)).toBe(bearer);
339 expect(putRemoteDpopTokenInCatalog(otherRelay, REMOTE_TOKEN)).toBe(otherRelay);
340 });
341
342 it("decodes a document written before the disabled list existed", () => {
343 const decoded = decodeCatalogDocument({
344 schemaVersion: 1,
345 targets: [],
346 profiles: [],
347 credentials: [],
348 remoteDpopTokens: [],
349 });
350
351 expect(decoded.disabledEnvironmentIds).toEqual([]);
352 });
353
354 it("switches a saved environment off and back on without touching its records", () => {
355 const registered = registerConnectionInCatalog(
356 EMPTY_CONNECTION_CATALOG_DOCUMENT,
357 new BearerConnectionRegistration({
358 target: BEARER_TARGET,
359 profile: BEARER_PROFILE,
360 credential: BEARER_CREDENTIAL,
361 }),
362 );
363
364 const disabled = setConnectionEnabledInCatalog(registered, ENVIRONMENT_ID, false);
365 expect(disabled.disabledEnvironmentIds).toEqual([ENVIRONMENT_ID]);
366 expect(disabled.targets).toEqual(registered.targets);
367 expect(disabled.credentials).toEqual(registered.credentials);
368 // Idempotent: switching off twice stores the id once.
369 expect(
370 setConnectionEnabledInCatalog(disabled, ENVIRONMENT_ID, false).disabledEnvironmentIds,
371 ).toEqual([ENVIRONMENT_ID]);
372
373 expect(
374 setConnectionEnabledInCatalog(disabled, ENVIRONMENT_ID, true).disabledEnvironmentIds,
375 ).toEqual([]);
376 // Re-registering (editing label or URL) keeps the flag.
377 expect(
378 registerConnectionInCatalog(
379 disabled,
380 new BearerConnectionRegistration({
381 target: BEARER_TARGET,
382 profile: BEARER_PROFILE,
383 credential: BEARER_CREDENTIAL,
384 }),
385 ).disabledEnvironmentIds,
386 ).toEqual([ENVIRONMENT_ID]);
387 expect(removeConnectionFromCatalog(disabled, ENVIRONMENT_ID).disabledEnvironmentIds).toEqual(
388 [],
389 );
390 });
391
392 it("persists the normalized SSH profile beside its target", () => {
393 const target = new SshConnectionTarget({
394 environmentId: ENVIRONMENT_ID,
395 label: "SSH",
396 connectionId: "ssh-1",
397 });
398 const profile = new SshConnectionProfile({
399 connectionId: target.connectionId,
400 environmentId: target.environmentId,
401 label: target.label,
402 target: {
403 alias: "devbox",
404 hostname: "devbox.example.test",
405 username: "developer",
406 port: 22,
407 },
408 });
409 const document = registerConnectionInCatalog(
410 EMPTY_CONNECTION_CATALOG_DOCUMENT,
411 new SshConnectionRegistration({ target, profile }),
412 );
413
414 expect(document.targets).toEqual([target]);
415 expect(document.profiles).toEqual([profile]);
416 expect(document.credentials).toEqual([]);
417 });
418
419 it("keeps every route of an environment and drops only a removed route's records", () => {
420 const withBearer = registerConnectionInCatalog(
421 { ...EMPTY_CONNECTION_CATALOG_DOCUMENT, remoteDpopTokens: [REMOTE_TOKEN] },
422 new BearerConnectionRegistration({
423 target: BEARER_TARGET,
424 profile: BEARER_PROFILE,
425 credential: BEARER_CREDENTIAL,
426 }),
427 );
428 const withRelay = registerConnectionInCatalog(
429 withBearer,
430 new RelayConnectionRegistration({ target: RELAY_TARGET }),
431 [BEARER_TARGET, RELAY_TARGET],
432 );
433 expect(catalogRoutes(withRelay, ENVIRONMENT_ID)).toEqual([BEARER_TARGET, RELAY_TARGET]);
434 expect(withRelay.credentials).toHaveLength(1);
435
436 // Dropping the bearer route forgets its credential; T3 Connect keeps its token.
437 const relayOnly = setRoutesInCatalog(withRelay, ENVIRONMENT_ID, [RELAY_TARGET]);
438 expect(relayOnly.targets).toEqual([RELAY_TARGET]);
439 expect(relayOnly.profiles).toEqual([]);
440 expect(relayOnly.credentials).toEqual([]);
441 expect(relayOnly.remoteDpopTokens).toEqual([REMOTE_TOKEN]);
442
443 // Dropping T3 Connect forgets its token; the bearer route keeps its records.
444 const bearerOnly = setRoutesInCatalog(withRelay, ENVIRONMENT_ID, [BEARER_TARGET]);
445 expect(bearerOnly.credentials).toHaveLength(1);
446 expect(bearerOnly.remoteDpopTokens).toEqual([]);
447 });
448
449 it("keeps an environment's position in the catalog when its routes change", () => {
450 const other = new RelayConnectionTarget({
451 environmentId: EnvironmentId.make("environment-2"),
452 label: "Other",
453 });
454 const document = {
455 ...EMPTY_CONNECTION_CATALOG_DOCUMENT,
456 targets: [RELAY_TARGET, other],
457 };
458 expect(
459 setRoutesInCatalog(document, ENVIRONMENT_ID, [BEARER_TARGET, RELAY_TARGET]).targets,
460 ).toEqual([BEARER_TARGET, RELAY_TARGET, other]);
461 });
462});