1import { AuthAdministrativeScopes } from "@t3tools/contracts";
2import * as NodeServices from "@effect/platform-node/NodeServices";
3import { expect, it } from "@effect/vitest";
4import * as Effect from "effect/Effect";
5import * as Layer from "effect/Layer";
6
7import * as ServerConfig from "../config.ts";
8import * as ServerEnvironment from "../environment/ServerEnvironment.ts";
9import * as SqlitePersistence from "../persistence/Sqlite.ts";
10import * as EnvironmentAuth from "./EnvironmentAuth.ts";
11import * as ServerSecretStore from "./ServerSecretStore.ts";
12import * as SessionStore from "./SessionStore.ts";
13
14const layerServerConfig = (
15 overrides?: Partial<Pick<ServerConfig.ServerConfig["Service"], "desktopBootstrapToken">>,
16) =>
17 Layer.effect(
18 ServerConfig.ServerConfig,
19 Effect.gen(function* () {
20 const config = yield* ServerConfig.ServerConfig;
21 return {
22 ...config,
23 ...overrides,
24 } satisfies ServerConfig.ServerConfig["Service"];
25 }),
26 ).pipe(
27 Layer.provide(
28 ServerConfig.layerTest(process.cwd(), {
29 prefix: "t3-auth-control-plane-test-",
30 }),
31 ),
32 );
33
34const layerEnvironmentAuth = (
35 overrides?: Partial<Pick<ServerConfig.ServerConfig["Service"], "desktopBootstrapToken">>,
36) =>
37 EnvironmentAuth.layer.pipe(
38 Layer.provideMerge(ServerSecretStore.layer),
39 Layer.provideMerge(SqlitePersistence.layerMemory),
40 Layer.provide(ServerEnvironment.layerIdentity),
41 Layer.provide(layerServerConfig(overrides)),
42 );
43
44it.layer(NodeServices.layer)("EnvironmentAuth administrative operations", (it) => {
45 it.effect("creates, lists, and revokes client pairing links", () =>
46 Effect.gen(function* () {
47 const environmentAuth = yield* EnvironmentAuth.EnvironmentAuth;
48
49 const created = yield* environmentAuth.createPairingLink({
50 scopes: ["orchestration:read"],
51 subject: "one-time-token",
52 label: "CI phone",
53 });
54 const listedBeforeRevoke = yield* environmentAuth.listPairingLinks();
55 const revoked = yield* environmentAuth.revokePairingLink(created.id);
56 const listedAfterRevoke = yield* environmentAuth.listPairingLinks();
57
58 expect(created.scopes).toEqual(["orchestration:read"]);
59 expect(created.credential.length).toBeGreaterThan(0);
60 expect(listedBeforeRevoke).toHaveLength(1);
61 expect(listedBeforeRevoke[0]?.id).toBe(created.id);
62 expect(listedBeforeRevoke[0]?.label).toBe("CI phone");
63 expect(listedBeforeRevoke[0]).not.toHaveProperty("credential");
64 expect(revoked).toBe(true);
65 expect(listedAfterRevoke).toHaveLength(0);
66 }).pipe(Effect.provide(layerEnvironmentAuth())),
67 );
68
69 it.effect("issues bearer access token sessions without exposing raw tokens", () =>
70 Effect.gen(function* () {
71 const environmentAuth = yield* EnvironmentAuth.EnvironmentAuth;
72 const sessionCredentials = yield* SessionStore.SessionStore;
73
74 const issued = yield* environmentAuth.issueSession({
75 label: "deploy-bot",
76 });
77 const verified = yield* sessionCredentials.verify(issued.token);
78 const listedBeforeRevoke = yield* environmentAuth.listSessions();
79 const revoked = yield* environmentAuth.revokeSession(issued.sessionId);
80 const listedAfterRevoke = yield* environmentAuth.listSessions();
81
82 expect(issued.method).toBe("bearer-access-token");
83 expect(issued.scopes).toEqual(AuthAdministrativeScopes);
84 expect(issued.client.deviceType).toBe("bot");
85 expect(issued.client.label).toBe("deploy-bot");
86 expect(verified.sessionId).toBe(issued.sessionId);
87 expect(verified.scopes).toEqual(AuthAdministrativeScopes);
88 expect(verified.method).toBe("bearer-access-token");
89 expect(listedBeforeRevoke).toHaveLength(1);
90 expect(listedBeforeRevoke[0]?.sessionId).toBe(issued.sessionId);
91 expect("token" in (listedBeforeRevoke[0] ?? {})).toBe(false);
92 expect(revoked).toBe(true);
93 expect(listedAfterRevoke).toHaveLength(0);
94 }).pipe(Effect.provide(layerEnvironmentAuth())),
95 );
96
97 it.effect("surfaces lastConnectedAt through the listed session view", () =>
98 Effect.gen(function* () {
99 const environmentAuth = yield* EnvironmentAuth.EnvironmentAuth;
100 const sessionCredentials = yield* SessionStore.SessionStore;
101
102 const issued = yield* environmentAuth.issueSession({
103 label: "remote-ipad",
104 });
105 const beforeConnect = yield* environmentAuth.listSessions();
106 yield* sessionCredentials.markConnected(issued.sessionId);
107 const afterConnect = yield* environmentAuth.listSessions();
108
109 expect(beforeConnect[0]?.lastConnectedAt).toBeNull();
110 expect(afterConnect[0]?.lastConnectedAt).not.toBeNull();
111 }).pipe(Effect.provide(layerEnvironmentAuth())),
112 );
113});