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", }), ), ), );});
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",
}),
),
),
);
});