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]); + }); });