267 lines · 10.0 KB · TypeScript
RawDownload
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<ServerConfig.ServerConfig["Service"]>) =>  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_<other> 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<ServerConfig.ServerConfig["Service"]>) =>  EnvironmentAuth.layer.pipe(    Layer.provide(SqlitePersistenceMemory),    Layer.provide(ServerSecretStore.layer),    Layer.provide(makeServerConfigLayer(overrides)),  ); const makeCookieRequest = (  sessionToken: string,): Parameters<EnvironmentAuth.EnvironmentAuth["Service"]["authenticateHttpRequest"]>[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",          }),        ),      ),  );});