import * as NodeServices from "@effect/platform-node/NodeServices"; import { AuthAdministrativeScopes } from "@t3tools/contracts"; import { expect, it } from "@effect/vitest"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; import * as ServerConfig from "../config.ts"; import { SqlitePersistenceMemory } from "../persistence/Layers/Sqlite.ts"; import * as PairingGrantStore from "./PairingGrantStore.ts"; import * as EnvironmentAuth from "./EnvironmentAuth.ts"; import { resolveSessionCookieName } from "./utils.ts"; import * as ServerSecretStore from "./ServerSecretStore.ts"; /** Pinned so dev-mode cookie tests can assert the port-scoped name. */ const TEST_SERVER_PORT = 13_773; const makeServerConfigLayer = (overrides?: Partial) => Layer.effect( ServerConfig.ServerConfig, Effect.gen(function* () { const config = yield* ServerConfig.ServerConfig; return { ...config, ...overrides, // Last, so the port cannot be overridden out from under // makeCookieRequest — which builds the cookie name from this constant. // An override that changed it would leave the server reading // t3_session_ while every request still sent t3_session_13773, // and the tests would fail for a reason unrelated to what they assert. port: TEST_SERVER_PORT, } satisfies ServerConfig.ServerConfig["Service"]; }), ).pipe(Layer.provide(ServerConfig.layerTest(process.cwd(), { prefix: "t3-auth-server-test-" }))); const makeEnvironmentAuthLayer = (overrides?: Partial) => EnvironmentAuth.layer.pipe( Layer.provide(SqlitePersistenceMemory), Layer.provide(ServerSecretStore.layer), Layer.provide(makeServerConfigLayer(overrides)), ); const makeCookieRequest = ( sessionToken: string, ): Parameters[0] => ({ cookies: { // Derived, not hardcoded: the name is port-scoped so concurrent servers // on one hostname don't share a cookie. [resolveSessionCookieName({ port: TEST_SERVER_PORT, devUrl: undefined })]: sessionToken, }, headers: {}, }) as unknown as Parameters< EnvironmentAuth.EnvironmentAuth["Service"]["authenticateHttpRequest"] >[0]; const requestMetadata = { deviceType: "desktop" as const, os: "macOS", browser: "Chrome", ipAddress: "192.168.1.23", }; it.layer(NodeServices.layer)("EnvironmentAuth.layer", (it) => { it.effect("classifies invalid bootstrap credential failures for the HTTP boundary", () => Effect.sync(() => { const error = EnvironmentAuth.toBootstrapExchangeError( new PairingGrantStore.UnknownBootstrapCredentialError({}), ); expect(error._tag).toBe("ServerAuthInvalidCredentialError"); }), ); it.effect("maps unexpected bootstrap failures to 500", () => Effect.sync(() => { const cause = new PairingGrantStore.BootstrapCredentialConsumeError({ cause: new Error("sqlite is unavailable"), }); const error = EnvironmentAuth.toBootstrapExchangeError(cause); expect(error._tag).toBe("ServerAuthBootstrapCredentialValidationError"); expect(error.message).toBe("Failed to validate bootstrap credential."); if (error._tag === "ServerAuthBootstrapCredentialValidationError") { expect(error.cause).toBe(cause); } }), ); it.effect("issues standard pairing credentials by default", () => Effect.gen(function* () { const serverAuth = yield* EnvironmentAuth.EnvironmentAuth; const pairingCredential = yield* serverAuth.issuePairingCredential(); const exchanged = yield* serverAuth.createBrowserSession( pairingCredential.credential, requestMetadata, ); const verified = yield* serverAuth.authenticateHttpRequest( makeCookieRequest(exchanged.sessionToken), ); expect(verified.sessionId.length).toBeGreaterThan(0); expect(verified.scopes).toEqual([ "orchestration:read", "orchestration:operate", "terminal:operate", "review:write", "relay:read", ]); expect(verified.subject).toBe("one-time-token"); }).pipe(Effect.provide(makeEnvironmentAuthLayer())), ); it.effect("does not exchange ordinary pairing grants for administrative access tokens", () => Effect.gen(function* () { const serverAuth = yield* EnvironmentAuth.EnvironmentAuth; const pairingCredential = yield* serverAuth.issuePairingCredential(); const error = yield* serverAuth .exchangeBootstrapCredentialForAccessToken( pairingCredential.credential, ["orchestration:read", "access:write"], requestMetadata, ) .pipe(Effect.flip); expect(error._tag).toBe("ServerAuthScopeNotGrantedError"); }).pipe(Effect.provide(makeEnvironmentAuthLayer())), ); it.effect("inherits a constrained pairing grant when token exchange omits scope", () => Effect.gen(function* () { const serverAuth = yield* EnvironmentAuth.EnvironmentAuth; const pairingCredential = yield* serverAuth.issuePairingCredential({ scopes: ["orchestration:read"], }); const token = yield* serverAuth.exchangeBootstrapCredentialForAccessToken( pairingCredential.credential, undefined, requestMetadata, ); expect(token.scope).toBe("orchestration:read"); }).pipe(Effect.provide(makeEnvironmentAuthLayer())), ); it.effect("keeps user-issued administrative pairing links manageable", () => Effect.gen(function* () { const serverAuth = yield* EnvironmentAuth.EnvironmentAuth; const pairingCredential = yield* serverAuth.issuePairingCredential({ scopes: AuthAdministrativeScopes, }); const listedPairingLinks = yield* serverAuth.listPairingLinks(); expect( listedPairingLinks.find((pairingLink) => pairingLink.id === pairingCredential.id)?.subject, ).toBe("one-time-token"); }).pipe(Effect.provide(makeEnvironmentAuthLayer())), ); it.effect("issues startup pairing URLs that bootstrap administrative sessions", () => Effect.gen(function* () { const serverAuth = yield* EnvironmentAuth.EnvironmentAuth; const pairingUrl = yield* serverAuth.issueStartupPairingUrl("http://127.0.0.1:3773"); const token = new URLSearchParams(new URL(pairingUrl).hash.slice(1)).get("token"); const listedPairingLinks = yield* serverAuth.listPairingLinks(); expect(token).toBeTruthy(); expect( listedPairingLinks.some( (pairingLink) => pairingLink.subject === "administrative-bootstrap", ), ).toBe(false); const exchanged = yield* serverAuth.createBrowserSession(token ?? "", requestMetadata); const verified = yield* serverAuth.authenticateHttpRequest( makeCookieRequest(exchanged.sessionToken), ); expect(verified.scopes).toEqual([ "orchestration:read", "orchestration:operate", "terminal:operate", "review:write", "relay:read", "access:read", "access:write", "relay:write", ]); expect(verified.subject).toBe("administrative-bootstrap"); }).pipe(Effect.provide(makeEnvironmentAuthLayer())), ); it.effect( "lists pairing links and revokes other sessions while keeping the administrative session", () => Effect.gen(function* () { const serverAuth = yield* EnvironmentAuth.EnvironmentAuth; const administrativeExchange = yield* serverAuth.createBrowserSession( "desktop-bootstrap-token", requestMetadata, ); const administrativeSession = yield* serverAuth.authenticateHttpRequest( makeCookieRequest(administrativeExchange.sessionToken), ); const pairingCredential = yield* serverAuth.issuePairingCredential({ label: "Julius iPhone", }); const listedPairingLinks = yield* serverAuth.listPairingLinks(); const clientExchange = yield* serverAuth.createBrowserSession( pairingCredential.credential, { ...requestMetadata, deviceType: "mobile", os: "iOS", browser: "Safari", ipAddress: "192.168.1.88", }, ); const clientSession = yield* serverAuth.authenticateHttpRequest( makeCookieRequest(clientExchange.sessionToken), ); const clientsBeforeRevoke = yield* serverAuth.listClientSessions( administrativeSession.sessionId, ); const revokedCount = yield* serverAuth.revokeOtherClientSessions( administrativeSession.sessionId, ); const clientsAfterRevoke = yield* serverAuth.listClientSessions( administrativeSession.sessionId, ); expect(listedPairingLinks.map((entry) => entry.id)).toContain(pairingCredential.id); expect(listedPairingLinks.find((entry) => entry.id === pairingCredential.id)?.label).toBe( "Julius iPhone", ); expect(clientsBeforeRevoke).toHaveLength(2); expect( clientsBeforeRevoke.find((entry) => entry.sessionId === administrativeSession.sessionId) ?.current, ).toBe(true); expect( clientsBeforeRevoke.find((entry) => entry.sessionId === clientSession.sessionId)?.current, ).toBe(false); expect( clientsBeforeRevoke.find((entry) => entry.sessionId === clientSession.sessionId)?.client .label, ).toBe("Julius iPhone"); expect( clientsBeforeRevoke.find((entry) => entry.sessionId === clientSession.sessionId)?.client .deviceType, ).toBe("mobile"); expect(revokedCount).toBe(1); expect(clientsAfterRevoke).toHaveLength(1); expect(clientsAfterRevoke[0]?.sessionId).toBe(administrativeSession.sessionId); }).pipe( Effect.provide( makeEnvironmentAuthLayer({ desktopBootstrapToken: "desktop-bootstrap-token", }), ), ), ); });