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 * as ServerEnvironment from "../environment/ServerEnvironment.ts"; import { SqlitePersistenceMemory } from "../persistence/Layers/Sqlite.ts"; import * as PairingGrantStore from "./PairingGrantStore.ts"; import * as EnvironmentAuth from "./EnvironmentAuth.ts"; import * as ServerSecretStore from "./ServerSecretStore.ts"; import * as SessionStore from "./SessionStore.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, // Keep the test server deterministic even when the default test layer // changes its development port. 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(ServerEnvironment.identityLayer), Layer.provide(makeServerConfigLayer(overrides)), ); const makeCookieRequest = ( cookieName: string, sessionToken: string, ): Parameters[0] => ({ cookies: { [cookieName]: sessionToken, }, headers: {}, }) as unknown as Parameters< EnvironmentAuth.EnvironmentAuth["Service"]["authenticateHttpRequest"] >[0]; const makeBearerRequest = ( token: string, ): Parameters[0] => ({ cookies: {}, headers: { authorization: `Bearer ${token}`, }, }) 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 sessions = yield* SessionStore.SessionStore; const pairingCredential = yield* serverAuth.issuePairingCredential(); const exchanged = yield* serverAuth.createBrowserSession( pairingCredential.credential, requestMetadata, ); const verified = yield* serverAuth.authenticateHttpRequest( makeCookieRequest(sessions.cookieName, 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("prefers a bearer token over a stale legacy cookie", () => Effect.gen(function* () { const serverAuth = yield* EnvironmentAuth.EnvironmentAuth; const sessions = yield* SessionStore.SessionStore; const bearer = yield* serverAuth.issueSession(); const verified = yield* serverAuth.authenticateHttpRequest({ cookies: { [sessions.legacyCookieName ?? "t3_session"]: "stale" }, headers: { authorization: `Bearer ${bearer.token}` }, } as never); expect(verified.sessionId).toBe(bearer.sessionId); }).pipe(Effect.provide(makeEnvironmentAuthLayer({ mode: "web", host: "192.168.1.50" }))), ); 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("rotates desktop bearer sessions without accumulating authorized clients", () => Effect.gen(function* () { const serverAuth = yield* EnvironmentAuth.EnvironmentAuth; const sessions = yield* SessionStore.SessionStore; const browser = yield* serverAuth.createBrowserSession( "desktop-bootstrap-token", requestMetadata, ); const browserSession = yield* serverAuth.authenticateHttpRequest( makeCookieRequest(sessions.cookieName, browser.sessionToken), ); const staleSessions = yield* Effect.forEach([1, 2, 3], () => sessions.issue({ subject: "desktop-bootstrap", method: "bearer-access-token" }), ); const pairing = yield* serverAuth.issuePairingCredential(); const paired = yield* serverAuth.exchangeBootstrapCredentialForAccessToken( pairing.credential, undefined, { ...requestMetadata, label: "T3 Code Desktop" }, ); const first = yield* serverAuth.exchangeBootstrapCredentialForAccessToken( "desktop-bootstrap-token", undefined, requestMetadata, ); const firstSession = yield* serverAuth.authenticateHttpRequest( makeBearerRequest(first.access_token), ); const second = yield* serverAuth.exchangeBootstrapCredentialForAccessToken( "desktop-bootstrap-token", undefined, requestMetadata, ); const active = yield* serverAuth.listSessions(); const firstError = yield* serverAuth .authenticateHttpRequest(makeBearerRequest(first.access_token)) .pipe(Effect.flip); const secondSession = yield* serverAuth.authenticateHttpRequest( makeBearerRequest(second.access_token), ); expect(active).toHaveLength(3); expect(active.map((entry) => entry.sessionId)).toContain(browserSession.sessionId); expect(active.map((entry) => entry.sessionId)).toContain(secondSession.sessionId); expect(active.map((entry) => entry.sessionId)).not.toContain(firstSession.sessionId); expect(firstError._tag).toBe("ServerAuthInvalidCredentialError"); for (const stale of staleSessions) { const error = yield* sessions.verify(stale.token).pipe(Effect.flip); expect(error._tag).toBe("SessionTokenRevokedError"); } const pairedSession = yield* serverAuth.authenticateHttpRequest( makeBearerRequest(paired.access_token), ); expect(pairedSession.subject).toBe("one-time-token"); expect(active.map((entry) => entry.sessionId)).toContain(pairedSession.sessionId); }).pipe( Effect.provide( makeEnvironmentAuthLayer({ desktopBootstrapToken: "desktop-bootstrap-token", }), ), ), ); 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 sessions = yield* SessionStore.SessionStore; 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(sessions.cookieName, 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 sessions = yield* SessionStore.SessionStore; const administrativeExchange = yield* serverAuth.createBrowserSession( "desktop-bootstrap-token", requestMetadata, ); const administrativeSession = yield* serverAuth.authenticateHttpRequest( makeCookieRequest(sessions.cookieName, 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(sessions.cookieName, 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", }), ), ), ); });