From 94fb85579d495bc6c20db9724c499aef9cc85e34 Mon Sep 17 00:00:00 2001 From: Brandon Corbett Date: Wed, 29 Jul 2026 21:10:34 -0400 Subject: [PATCH] refactor(core): export the remaining handlers and verifiers from the root The admin, session, internal-metrics, and system-config handlers, along with verifySignedAuthResponse and verifyRefreshCookie, were reachable only through a subpath import. Everything else came from the root, so which import an adapter needed depended on which handler it wanted. 27 names are now available from the package root as well. Purely additive: nothing is removed or renamed, the ./handlers/* subpaths keep working, and a test asserts a subpath import and a root import resolve to the same function rather than two copies. Rewrites the README's public API overview to match, grouped by what an adapter author is looking for, covering the response contract, proxy, delivery, and contract-value exports added earlier in this epic that it had never listed. Refs #72 --- .changeset/core-public-surface.md | 13 +++ packages/core/README.md | 56 ++++++++-- packages/core/src/index.ts | 6 ++ packages/core/tests/publicExports.test.js | 124 ++++++++++++++++++++-- 4 files changed, 187 insertions(+), 12 deletions(-) create mode 100644 .changeset/core-public-surface.md diff --git a/.changeset/core-public-surface.md b/.changeset/core-public-surface.md new file mode 100644 index 0000000..f7c08ea --- /dev/null +++ b/.changeset/core-public-surface.md @@ -0,0 +1,13 @@ +--- +"@seamless-auth/core": minor +--- + +Export the remaining handlers and verifiers from the package root. + +The admin, session, internal-metrics, and system-config handlers, along with `verifySignedAuthResponse` and `verifyRefreshCookie`, were reachable only through a subpath import. Everything else came from the root, so which import an adapter needed depended on which handler it wanted. 27 names are now available from `@seamless-auth/core` as well. + +Purely additive. Nothing is removed or renamed, the `./handlers/*` subpaths keep working, and a test asserts that a subpath import and a root import resolve to the same function rather than two copies. + +The README's public API overview is rewritten to match, grouped by what an adapter author is looking for, and now covers the response contract, proxy, delivery, and contract-value exports added earlier in this epic that it had never listed. + +Part of #72. diff --git a/packages/core/README.md b/packages/core/README.md index ce4b0aa..1326296 100644 --- a/packages/core/README.md +++ b/packages/core/README.md @@ -82,21 +82,65 @@ Core helpers enforce transitions between these states. ## Public API (Overview) -Key exports include: +Everything below is available from the package root. The `./handlers/*` subpaths +remain for direct imports. + +**Sessions and cookies** - `ensureCookies(...)` – validates and refreshes session cookies - `refreshAccessToken(...)` – rotates expired access sessions - `verifyCookieJwt(...)` – verifies signed cookie payloads -- `createServiceToken(...)` – creates short-lived M2M assertions +- `verifyRefreshCookie(...)` – verifies a refresh cookie, returning `null` on failure +- `verifySignedAuthResponse(...)` – verifies an auth API response signature against its JWKS - `getSeamlessUser(...)` – resolves the hydrated user, typed as `SeamlessUser | null` - `hasScopedRole(...)` – checks scoped role grants such as `admin:read` + +**Building an adapter** + +- `applyResult(result, adapter, opts)` – turns a handler result into a response +- `applyCookies(result, adapter, opts)` – cookie instructions only, for middleware +- `ResponseAdapter` – the three methods an adapter provides: `setCookie`, `clearCookie`, `send` +- `proxyRequest(...)` – forwards a request to the auth API and returns its status and body +- `checkProxyIdentity(...)` – checks a request carries the session a proxied route requires +- `buildQueryString(...)` / `buildUpstreamUrl(...)` – build an upstream URL +- `signSessionCookie(...)` / `resolveCookieSameSite(...)` – cookie format and policy +- `authFetch(...)` – calls the auth API with the adapter's headers and a tolerant `json()` + +**Auth flow handlers** + +`loginHandler`, `finishLoginHandler`, `registerHandler`, `finishRegisterHandler`, +`logoutHandler`, `meHandler`, `requestOtpHandler`, `verifyLoginOtpHandler`, +`verifyRegistrationOtpHandler`, `requestMagicLinkHandler`, `verifyMagicLinkHandler`, +`pollMagicLinkConfirmationHandler`, `switchOrganizationHandler`, +`listOAuthProvidersHandler`, `startOAuthLoginHandler`, `finishOAuthLoginHandler`. + +**Admin and operations handlers** + +User, session, auth-event, metrics, and system-config handlers, for example +`getUsersHandler`, `updateUserHandler`, `listSessionsHandler`, +`getAuthEventsHandler`, `getDashboardMetricsHandler`, and +`getAvailableRolesHandler`. + +**Message delivery** + +- `deliverAuthMessage(...)` – delivers an auth message through the configured transports +- `applyExternalDelivery(...)` – delivers the payload on a response body and strips it +- `stripDelivery(...)` – removes the delivery payload from a body + +**Auth API contract** + +- `SERVICE_TOKEN_ISSUER` / `SERVICE_TOKEN_AUDIENCE` – the fixed identity for M2M service tokens +- `AUTH_DELIVERY_MODE_HEADER` / `EXTERNAL_DELIVERY_MODE` / `EXTERNAL_DELIVERY_HEADERS` +- `DEV_JWKS_KID` – the fallback key id, which is a misconfiguration to deploy on +- `createServiceToken(...)` / `buildExternalDeliveryAuthorization(...)` – mint service tokens + +**Utilities** + - `assertSecretStrength(...)` / `assertSecrets(...)` – enforce the minimum secret length - `redactSensitiveText(...)` – masks tokens, bearer values, and secrets before logging -- `listOAuthProvidersHandler(...)` – retrieves public OAuth provider metadata -- `startOAuthLoginHandler(...)` – starts an OAuth authorization-code login -- `finishOAuthLoginHandler(...)` – finishes OAuth login and returns cookie instructions -These functions return **descriptive results**, not HTTP responses. +Handlers return **descriptive results**, not HTTP responses. `applyResult` is what +turns one into a response, and it is the only place that decides how. ### Secret strength diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index de7f7e9..d66a615 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -3,6 +3,8 @@ export * from "./authMessaging.js"; export * from "./deliverAuthMessage.js"; export * from "./ensureCookies.js"; export * from "./verifyCookieJwt.js"; +export * from "./verifyRefreshCookie.js"; +export * from "./verifySignedAuthResponse.js"; export * from "./refreshAccessToken.js"; export * from "./getSeamlessUser.js"; export * from "./createServiceToken.js"; @@ -34,3 +36,7 @@ export * from "./handlers/requestMagicLinkHandler.js"; export * from "./handlers/pollMagicLinkConfirmationHandler.js"; export * from "./handlers/switchOrganizationHandler.js"; export * from "./handlers/oauthHandlers.js"; +export * from "./handlers/admin.js"; +export * from "./handlers/sessions.js"; +export * from "./handlers/internalMetrics.js"; +export * from "./handlers/systemConfig.js"; diff --git a/packages/core/tests/publicExports.test.js b/packages/core/tests/publicExports.test.js index 97aa943..36696ed 100644 --- a/packages/core/tests/publicExports.test.js +++ b/packages/core/tests/publicExports.test.js @@ -1,49 +1,161 @@ // Named imports from the built dist, mirroring the "Public API (Overview)" README section. // A missing named export fails this file at module link time, before any assertion runs. import { + applyCookies, + applyExternalDelivery, + applyResult, assertSecretStrength, assertSecrets, + authFetch, + buildExternalDeliveryAuthorization, + buildQueryString, + buildUpstreamUrl, + checkProxyIdentity, createServiceToken, + deliverAuthMessage, ensureCookies, + finishLoginHandler, finishOAuthLoginHandler, + finishRegisterHandler, + getAuthEventsHandler, + getAvailableRolesHandler, + getDashboardMetricsHandler, getSeamlessUser, + getUsersHandler, hasScopedRole, listOAuthProvidersHandler, + listSessionsHandler, + loginHandler, + logoutHandler, + meHandler, + pollMagicLinkConfirmationHandler, + proxyRequest, redactSensitiveText, refreshAccessToken, + registerHandler, + requestMagicLinkHandler, + requestOtpHandler, + resolveCookieSameSite, + roleGrantsAccess, + signSessionCookie, startOAuthLoginHandler, + stripDelivery, + switchOrganizationHandler, + updateUserHandler, verifyCookieJwt, + verifyLoginOtpHandler, + verifyMagicLinkHandler, + verifyRefreshCookie, + verifyRegistrationOtpHandler, + verifySignedAuthResponse, + AUTH_DELIVERY_MODE_HEADER, + DEV_JWKS_KID, + EXTERNAL_DELIVERY_HEADERS, + EXTERNAL_DELIVERY_MODE, + SERVICE_TOKEN_AUDIENCE, + SERVICE_TOKEN_ISSUER, } from "../dist/index.js"; -const DOCUMENTED_EXPORTS = { +const DOCUMENTED_FUNCTIONS = { + applyCookies, + applyExternalDelivery, + applyResult, assertSecretStrength, assertSecrets, + authFetch, + buildExternalDeliveryAuthorization, + buildQueryString, + buildUpstreamUrl, + checkProxyIdentity, createServiceToken, + deliverAuthMessage, ensureCookies, + finishLoginHandler, finishOAuthLoginHandler, + finishRegisterHandler, + getAuthEventsHandler, + getAvailableRolesHandler, + getDashboardMetricsHandler, getSeamlessUser, + getUsersHandler, hasScopedRole, listOAuthProvidersHandler, + listSessionsHandler, + loginHandler, + logoutHandler, + meHandler, + pollMagicLinkConfirmationHandler, + proxyRequest, redactSensitiveText, refreshAccessToken, + registerHandler, + requestMagicLinkHandler, + requestOtpHandler, + resolveCookieSameSite, + roleGrantsAccess, + signSessionCookie, startOAuthLoginHandler, + stripDelivery, + switchOrganizationHandler, + updateUserHandler, verifyCookieJwt, + verifyLoginOtpHandler, + verifyMagicLinkHandler, + verifyRefreshCookie, + verifyRegistrationOtpHandler, + verifySignedAuthResponse, +}; + +const DOCUMENTED_CONSTANTS = { + AUTH_DELIVERY_MODE_HEADER, + DEV_JWKS_KID, + EXTERNAL_DELIVERY_MODE, + SERVICE_TOKEN_AUDIENCE, + SERVICE_TOKEN_ISSUER, }; describe("@seamless-auth/core public exports", () => { - it.each(Object.keys(DOCUMENTED_EXPORTS))( + it.each(Object.keys(DOCUMENTED_FUNCTIONS))( "exports %s as a named function", (name) => { - expect(typeof DOCUMENTED_EXPORTS[name]).toBe("function"); + expect(typeof DOCUMENTED_FUNCTIONS[name]).toBe("function"); + }, + ); + + it.each(Object.keys(DOCUMENTED_CONSTANTS))( + "exports %s as a named string constant", + (name) => { + expect(typeof DOCUMENTED_CONSTANTS[name]).toBe("string"); }, ); + it("exports the external delivery headers as an object", () => { + expect(EXTERNAL_DELIVERY_HEADERS).toEqual({ + [AUTH_DELIVERY_MODE_HEADER]: EXTERNAL_DELIVERY_MODE, + }); + }); + it("exposes every documented name on the module namespace", async () => { const namespace = await import("../dist/index.js"); - const missing = Object.keys(DOCUMENTED_EXPORTS).filter( - (name) => typeof namespace[name] !== "function", - ); + const missing = [ + ...Object.keys(DOCUMENTED_FUNCTIONS), + ...Object.keys(DOCUMENTED_CONSTANTS), + ].filter((name) => namespace[name] === undefined); expect(missing).toEqual([]); }); + + // The subpaths predate the root exports and adopters import from them, so they + // have to keep resolving to the same functions. + it.each([ + ["admin", "getUsersHandler"], + ["sessions", "listSessionsHandler"], + ["internalMetrics", "getDashboardMetricsHandler"], + ["systemConfig", "getAvailableRolesHandler"], + ])("keeps handlers/%s reachable by subpath", async (module, exported) => { + const namespace = await import(`../dist/handlers/${module}.js`); + const root = await import("../dist/index.js"); + + expect(namespace[exported]).toBe(root[exported]); + }); });