@cboxdk/id-js 0.19.0 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +213 -0
- package/README.md +51 -3
- package/RELEASE-CANDIDATE.md +11 -0
- package/dist/agent.cjs +613 -0
- package/dist/agent.cjs.map +1 -0
- package/dist/agent.d.cts +400 -0
- package/dist/agent.d.ts +400 -0
- package/dist/agent.js +527 -0
- package/dist/agent.js.map +1 -0
- package/dist/api-keys-BE3R3q_A.d.ts +105 -0
- package/dist/api-keys-C8-Q4eFA.d.cts +105 -0
- package/dist/chunk-EVZ6RB3K.js +98 -0
- package/dist/chunk-EVZ6RB3K.js.map +1 -0
- package/dist/{chunk-YGLMLIKL.js → chunk-T5LCQOLS.js} +4 -22
- package/dist/chunk-T5LCQOLS.js.map +1 -0
- package/dist/chunk-VGQASHTI.js +22 -0
- package/dist/chunk-VGQASHTI.js.map +1 -0
- package/dist/decisions-C84Fzah4.d.ts +23 -0
- package/dist/decisions-DNd9UXwi.d.cts +23 -0
- package/dist/index.cjs +127 -14
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +27 -6
- package/dist/index.js.map +1 -1
- package/dist/management.cjs +352 -19
- package/dist/management.cjs.map +1 -1
- package/dist/management.d.cts +994 -42
- package/dist/management.d.ts +994 -42
- package/dist/management.js +352 -19
- package/dist/management.js.map +1 -1
- package/dist/nextjs.d.cts +1 -1
- package/dist/nextjs.d.ts +1 -1
- package/dist/nextjs.js +2 -1
- package/dist/nextjs.js.map +1 -1
- package/dist/server.cjs +103 -9
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts +3 -105
- package/dist/server.d.ts +3 -105
- package/dist/server.js +1 -0
- package/package.json +16 -3
- package/dist/chunk-YGLMLIKL.js.map +0 -1
package/dist/nextjs.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { NextResponse, NextRequest } from 'next/server';
|
|
2
|
-
import { ApiKeyVerification } from './
|
|
2
|
+
import { a as ApiKeyVerification } from './api-keys-C8-Q4eFA.cjs';
|
|
3
3
|
import { C as CboxIdClient } from './client-BT-jOm0p.cjs';
|
|
4
4
|
import { A as AuthorizationRequestOptions, C as CboxUser, a as CboxIdConfig } from './types-CC1bz68x.cjs';
|
|
5
5
|
import './errors-D61xljZE.cjs';
|
package/dist/nextjs.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { NextResponse, NextRequest } from 'next/server';
|
|
2
|
-
import { ApiKeyVerification } from './
|
|
2
|
+
import { a as ApiKeyVerification } from './api-keys-BE3R3q_A.js';
|
|
3
3
|
import { C as CboxIdClient } from './client-xlEJalu8.js';
|
|
4
4
|
import { A as AuthorizationRequestOptions, C as CboxUser, a as CboxIdConfig } from './types-CC1bz68x.js';
|
|
5
5
|
import './errors-D61xljZE.js';
|
package/dist/nextjs.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { CboxIdClient } from './chunk-
|
|
1
|
+
import { CboxIdClient } from './chunk-T5LCQOLS.js';
|
|
2
2
|
import { ApiKeyVerifier } from './chunk-2E25IPBA.js';
|
|
3
|
+
import './chunk-VGQASHTI.js';
|
|
3
4
|
import './chunk-QQW63PGU.js';
|
|
4
5
|
import { ConfigurationError } from './chunk-SEUD4TE4.js';
|
|
5
6
|
import { NextResponse } from 'next/server';
|
package/dist/nextjs.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/nextjs.ts"],"names":[],"mappings":"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","file":"nextjs.js","sourcesContent":["import { NextResponse, type NextRequest } from 'next/server';\nimport { ApiKeyVerifier, type ApiKeyVerification } from './api-keys.js';\nimport { CboxIdClient } from './client.js';\nimport { ConfigurationError } from './errors.js';\nimport type { AuthorizationRequest, AuthorizationRequestOptions, CboxIdConfig, CboxUser } from './types.js';\n\n/**\n * First-class Next.js (App Router) adapter for {@link CboxIdClient}. It wires the\n * framework-agnostic core to short-lived, httpOnly cookies so login is a couple of\n * route handlers:\n *\n * ```ts\n * // app/auth/[...cbox]/route.ts is up to you; a minimal wiring:\n * import { cboxId } from '@/lib/cbox';\n *\n * export async function GET(request: NextRequest) {\n * return cboxId.signIn();\n * }\n * ```\n *\n * The temporary state/verifier/nonce cookies are httpOnly, `SameSite=Lax`, and\n * expire in 10 minutes, so a stalled login cleans itself up.\n */\n\nconst COOKIE = {\n state: 'cbox_id_state',\n verifier: 'cbox_id_verifier',\n nonce: 'cbox_id_nonce',\n // The step-up requirement has to survive the redirect like the nonce does: a\n // `maxAge` the callback cannot see is a `maxAge` nothing verifies.\n maxAge: 'cbox_id_max_age',\n // Likewise the organization a switch bound to: the callback refuses tokens for another.\n organization: 'cbox_id_organization',\n} as const;\n\nconst TEMP_COOKIE_MAX_AGE = 600; // 10 minutes\n\nexport interface CboxIdNext {\n /** The underlying framework-agnostic client. */\n readonly client: CboxIdClient;\n /**\n * Redirect to Cbox ID's authorize endpoint, stashing PKCE/state/nonce in cookies.\n *\n * `maxAge` (seconds) demands a re-authentication no older than that — the OIDC\n * step-up you want before a payment or an admin grant. It round-trips in a cookie\n * and {@link CboxIdNext.callback} verifies the id_token's `auth_time` against it.\n */\n signIn(options?: SignInOptions): Promise<NextResponse>;\n /**\n * Redirect to a new sign-in bound to another organization — the handler behind an\n * organization switcher. See {@link CboxIdClient.switchOrganization}: a person who is\n * not an active member comes back with `error=access_denied`, and {@link callback}\n * throws an `AuthenticationError` whose `error` is `'access_denied'`.\n */\n switchOrganization(organizationId: string, options?: Omit<SignInOptions, 'organization' | 'organizationHint'>): Promise<NextResponse>;\n /** Complete login on your callback route; returns the authenticated user. */\n callback(request: NextRequest): Promise<CboxUser>;\n /** The hosted profile-page URL (`return_to` appended when given). */\n profileUrl(returnTo?: string): string;\n /** A redirect response to the hosted profile page. */\n profileRedirect(returnTo?: string): NextResponse;\n /** The hosted API-keys page for this app. See {@link CboxIdClient.apiKeysUrl}. */\n apiKeysUrl(options?: { clientId?: string; returnTo?: string; organization?: string }): string;\n /**\n * Verify a customer API key presented to your API. Needs `clientSecret`\n * (`CBOX_ID_CLIENT_SECRET`); see `ApiKeyVerifier` in `@cboxdk/id-js/server`, which\n * this uses with the adapter's configuration and no cache.\n */\n verifyApiKey(key: string): Promise<ApiKeyVerification>;\n /**\n * RP-initiated logout URL, or null when the instance advertises none. Pass the\n * user's `id_token` as `idTokenHint` when you kept it; `client_id` is sent for\n * you, and is what lets the OP honour `returnTo` at all.\n */\n signOutUrl(returnTo?: string, idTokenHint?: string): Promise<string | null>;\n}\n\n/**\n * What {@link CboxIdNext.signIn} accepts: every authorization option except the callback\n * and `state`, which the adapter owns because it stores them in cookies.\n */\nexport type SignInOptions = Omit<AuthorizationRequestOptions, 'redirectUri' | 'state'>;\n\n/**\n * Build a Next.js adapter. Pass a config, or omit it to read from the environment\n * (`CBOX_ID_ISSUER`, `CBOX_ID_CLIENT_ID`, `CBOX_ID_CLIENT_SECRET`,\n * `CBOX_ID_REDIRECT_URI`).\n */\nexport function createCboxId(config?: Partial<CboxIdConfig>): CboxIdNext {\n const resolved = resolveConfig(config);\n const client = new CboxIdClient(resolved);\n let apiKeys: ApiKeyVerifier | undefined;\n\n const tempCookieOptions = {\n httpOnly: true,\n secure: true,\n sameSite: 'lax' as const,\n path: '/',\n maxAge: TEMP_COOKIE_MAX_AGE,\n };\n\n // One place that stashes PKCE/state/nonce, so a switch cannot forget what a sign-in\n // stores — a missing verifier cookie fails the callback, a missing max-age cookie\n // silently skips the step-up check.\n const redirectTo = (request: AuthorizationRequest): NextResponse => {\n const response = NextResponse.redirect(request.url);\n response.cookies.set(COOKIE.state, request.state, tempCookieOptions);\n response.cookies.set(COOKIE.verifier, request.codeVerifier, tempCookieOptions);\n response.cookies.set(COOKIE.nonce, request.nonce, tempCookieOptions);\n if (typeof request.maxAge === 'number') {\n response.cookies.set(COOKIE.maxAge, String(request.maxAge), tempCookieOptions);\n }\n if (request.organization !== undefined) {\n response.cookies.set(COOKIE.organization, request.organization, tempCookieOptions);\n } else {\n // A plain sign-in after an abandoned switch must not inherit its binding.\n response.cookies.delete(COOKIE.organization);\n }\n return response;\n };\n\n return {\n client,\n\n async signIn(options = {}) {\n return redirectTo(await client.createAuthorizationRequest(options));\n },\n\n async switchOrganization(organizationId, options = {}) {\n return redirectTo(await client.switchOrganization(organizationId, options));\n },\n\n async callback(request) {\n const storedMaxAge = request.cookies.get(COOKIE.maxAge)?.value;\n const maxAge = storedMaxAge !== undefined ? Number(storedMaxAge) : undefined;\n const organization = request.cookies.get(COOKIE.organization)?.value;\n\n return client.authenticate({\n params: {\n code: request.nextUrl.searchParams.get('code'),\n state: request.nextUrl.searchParams.get('state'),\n error: request.nextUrl.searchParams.get('error'),\n error_description: request.nextUrl.searchParams.get('error_description'),\n },\n stored: {\n state: request.cookies.get(COOKIE.state)?.value ?? '',\n codeVerifier: request.cookies.get(COOKIE.verifier)?.value ?? '',\n nonce: request.cookies.get(COOKIE.nonce)?.value ?? '',\n ...(maxAge !== undefined && Number.isFinite(maxAge) ? { maxAge } : {}),\n ...(organization !== undefined && organization !== '' ? { organization } : {}),\n },\n });\n },\n\n profileUrl(returnTo) {\n return client.profileUrl(returnTo);\n },\n\n profileRedirect(returnTo) {\n return NextResponse.redirect(client.profileUrl(returnTo));\n },\n\n apiKeysUrl(options) {\n return client.apiKeysUrl(options);\n },\n\n verifyApiKey(key) {\n // Built on first use, so an app that never verifies a key does not need a secret.\n apiKeys ??= new ApiKeyVerifier({\n issuer: resolved.issuer,\n clientId: resolved.clientId,\n clientSecret: resolved.clientSecret ?? '',\n ...(resolved.timeoutMs ? { timeoutMs: resolved.timeoutMs } : {}),\n });\n\n return apiKeys.verifyApiKey(key);\n },\n\n signOutUrl(returnTo, idTokenHint) {\n return client.logoutUrl(returnTo, idTokenHint);\n },\n };\n}\n\nfunction resolveConfig(config?: Partial<CboxIdConfig>): CboxIdConfig {\n const issuer = config?.issuer ?? process.env['CBOX_ID_ISSUER'];\n const clientId = config?.clientId ?? process.env['CBOX_ID_CLIENT_ID'];\n const redirectUri = config?.redirectUri ?? process.env['CBOX_ID_REDIRECT_URI'];\n const clientSecret = config?.clientSecret ?? process.env['CBOX_ID_CLIENT_SECRET'];\n\n if (!issuer || !clientId || !redirectUri) {\n throw new ConfigurationError(\n 'Cbox ID needs issuer, clientId and redirectUri — pass them to createCboxId() or set CBOX_ID_ISSUER / CBOX_ID_CLIENT_ID / CBOX_ID_REDIRECT_URI.',\n );\n }\n\n return {\n issuer,\n clientId,\n redirectUri,\n ...(clientSecret ? { clientSecret } : {}),\n ...(config?.scopes ? { scopes: config.scopes } : {}),\n ...(config?.accountPath ? { accountPath: config.accountPath } : {}),\n ...(config?.timeoutMs ? { timeoutMs: config.timeoutMs } : {}),\n ...(config?.cacheTtlMs ? { cacheTtlMs: config.cacheTtlMs } : {}),\n };\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/nextjs.ts"],"names":[],"mappings":";;;;;;;AAwBA,IAAM,MAAA,GAAS;AAAA,EACb,KAAA,EAAO,eAAA;AAAA,EACP,QAAA,EAAU,kBAAA;AAAA,EACV,KAAA,EAAO,eAAA;AAAA;AAAA;AAAA,EAGP,MAAA,EAAQ,iBAAA;AAAA;AAAA,EAER,YAAA,EAAc;AAChB,CAAA;AAEA,IAAM,mBAAA,GAAsB,GAAA;AAqDrB,SAAS,aAAa,MAAA,EAA4C;AACvE,EAAA,MAAM,QAAA,GAAW,cAAc,MAAM,CAAA;AACrC,EAAA,MAAM,MAAA,GAAS,IAAI,YAAA,CAAa,QAAQ,CAAA;AACxC,EAAA,IAAI,OAAA;AAEJ,EAAA,MAAM,iBAAA,GAAoB;AAAA,IACxB,QAAA,EAAU,IAAA;AAAA,IACV,MAAA,EAAQ,IAAA;AAAA,IACR,QAAA,EAAU,KAAA;AAAA,IACV,IAAA,EAAM,GAAA;AAAA,IACN,MAAA,EAAQ;AAAA,GACV;AAKA,EAAA,MAAM,UAAA,GAAa,CAAC,OAAA,KAAgD;AAClE,IAAA,MAAM,QAAA,GAAW,YAAA,CAAa,QAAA,CAAS,OAAA,CAAQ,GAAG,CAAA;AAClD,IAAA,QAAA,CAAS,QAAQ,GAAA,CAAI,MAAA,CAAO,KAAA,EAAO,OAAA,CAAQ,OAAO,iBAAiB,CAAA;AACnE,IAAA,QAAA,CAAS,QAAQ,GAAA,CAAI,MAAA,CAAO,QAAA,EAAU,OAAA,CAAQ,cAAc,iBAAiB,CAAA;AAC7E,IAAA,QAAA,CAAS,QAAQ,GAAA,CAAI,MAAA,CAAO,KAAA,EAAO,OAAA,CAAQ,OAAO,iBAAiB,CAAA;AACnE,IAAA,IAAI,OAAO,OAAA,CAAQ,MAAA,KAAW,QAAA,EAAU;AACtC,MAAA,QAAA,CAAS,OAAA,CAAQ,IAAI,MAAA,CAAO,MAAA,EAAQ,OAAO,OAAA,CAAQ,MAAM,GAAG,iBAAiB,CAAA;AAAA,IAC/E;AACA,IAAA,IAAI,OAAA,CAAQ,iBAAiB,MAAA,EAAW;AACtC,MAAA,QAAA,CAAS,QAAQ,GAAA,CAAI,MAAA,CAAO,YAAA,EAAc,OAAA,CAAQ,cAAc,iBAAiB,CAAA;AAAA,IACnF,CAAA,MAAO;AAEL,MAAA,QAAA,CAAS,OAAA,CAAQ,MAAA,CAAO,MAAA,CAAO,YAAY,CAAA;AAAA,IAC7C;AACA,IAAA,OAAO,QAAA;AAAA,EACT,CAAA;AAEA,EAAA,OAAO;AAAA,IACL,MAAA;AAAA,IAEA,MAAM,MAAA,CAAO,OAAA,GAAU,EAAC,EAAG;AACzB,MAAA,OAAO,UAAA,CAAW,MAAM,MAAA,CAAO,0BAAA,CAA2B,OAAO,CAAC,CAAA;AAAA,IACpE,CAAA;AAAA,IAEA,MAAM,kBAAA,CAAmB,cAAA,EAAgB,OAAA,GAAU,EAAC,EAAG;AACrD,MAAA,OAAO,WAAW,MAAM,MAAA,CAAO,kBAAA,CAAmB,cAAA,EAAgB,OAAO,CAAC,CAAA;AAAA,IAC5E,CAAA;AAAA,IAEA,MAAM,SAAS,OAAA,EAAS;AACtB,MAAA,MAAM,eAAe,OAAA,CAAQ,OAAA,CAAQ,GAAA,CAAI,MAAA,CAAO,MAAM,CAAA,EAAG,KAAA;AACzD,MAAA,MAAM,MAAA,GAAS,YAAA,KAAiB,MAAA,GAAY,MAAA,CAAO,YAAY,CAAA,GAAI,MAAA;AACnE,MAAA,MAAM,eAAe,OAAA,CAAQ,OAAA,CAAQ,GAAA,CAAI,MAAA,CAAO,YAAY,CAAA,EAAG,KAAA;AAE/D,MAAA,OAAO,OAAO,YAAA,CAAa;AAAA,QACzB,MAAA,EAAQ;AAAA,UACN,IAAA,EAAM,OAAA,CAAQ,OAAA,CAAQ,YAAA,CAAa,IAAI,MAAM,CAAA;AAAA,UAC7C,KAAA,EAAO,OAAA,CAAQ,OAAA,CAAQ,YAAA,CAAa,IAAI,OAAO,CAAA;AAAA,UAC/C,KAAA,EAAO,OAAA,CAAQ,OAAA,CAAQ,YAAA,CAAa,IAAI,OAAO,CAAA;AAAA,UAC/C,iBAAA,EAAmB,OAAA,CAAQ,OAAA,CAAQ,YAAA,CAAa,IAAI,mBAAmB;AAAA,SACzE;AAAA,QACA,MAAA,EAAQ;AAAA,UACN,OAAO,OAAA,CAAQ,OAAA,CAAQ,IAAI,MAAA,CAAO,KAAK,GAAG,KAAA,IAAS,EAAA;AAAA,UACnD,cAAc,OAAA,CAAQ,OAAA,CAAQ,IAAI,MAAA,CAAO,QAAQ,GAAG,KAAA,IAAS,EAAA;AAAA,UAC7D,OAAO,OAAA,CAAQ,OAAA,CAAQ,IAAI,MAAA,CAAO,KAAK,GAAG,KAAA,IAAS,EAAA;AAAA,UACnD,GAAI,MAAA,KAAW,MAAA,IAAa,MAAA,CAAO,QAAA,CAAS,MAAM,CAAA,GAAI,EAAE,MAAA,EAAO,GAAI,EAAC;AAAA,UACpE,GAAI,iBAAiB,MAAA,IAAa,YAAA,KAAiB,KAAK,EAAE,YAAA,KAAiB;AAAC;AAC9E,OACD,CAAA;AAAA,IACH,CAAA;AAAA,IAEA,WAAW,QAAA,EAAU;AACnB,MAAA,OAAO,MAAA,CAAO,WAAW,QAAQ,CAAA;AAAA,IACnC,CAAA;AAAA,IAEA,gBAAgB,QAAA,EAAU;AACxB,MAAA,OAAO,YAAA,CAAa,QAAA,CAAS,MAAA,CAAO,UAAA,CAAW,QAAQ,CAAC,CAAA;AAAA,IAC1D,CAAA;AAAA,IAEA,WAAW,OAAA,EAAS;AAClB,MAAA,OAAO,MAAA,CAAO,WAAW,OAAO,CAAA;AAAA,IAClC,CAAA;AAAA,IAEA,aAAa,GAAA,EAAK;AAEhB,MAAA,OAAA,KAAY,IAAI,cAAA,CAAe;AAAA,QAC7B,QAAQ,QAAA,CAAS,MAAA;AAAA,QACjB,UAAU,QAAA,CAAS,QAAA;AAAA,QACnB,YAAA,EAAc,SAAS,YAAA,IAAgB,EAAA;AAAA,QACvC,GAAI,SAAS,SAAA,GAAY,EAAE,WAAW,QAAA,CAAS,SAAA,KAAc;AAAC,OAC/D,CAAA;AAED,MAAA,OAAO,OAAA,CAAQ,aAAa,GAAG,CAAA;AAAA,IACjC,CAAA;AAAA,IAEA,UAAA,CAAW,UAAU,WAAA,EAAa;AAChC,MAAA,OAAO,MAAA,CAAO,SAAA,CAAU,QAAA,EAAU,WAAW,CAAA;AAAA,IAC/C;AAAA,GACF;AACF;AAEA,SAAS,cAAc,MAAA,EAA8C;AACnE,EAAA,MAAM,MAAA,GAAS,MAAA,EAAQ,MAAA,IAAU,OAAA,CAAQ,IAAI,gBAAgB,CAAA;AAC7D,EAAA,MAAM,QAAA,GAAW,MAAA,EAAQ,QAAA,IAAY,OAAA,CAAQ,IAAI,mBAAmB,CAAA;AACpE,EAAA,MAAM,WAAA,GAAc,MAAA,EAAQ,WAAA,IAAe,OAAA,CAAQ,IAAI,sBAAsB,CAAA;AAC7E,EAAA,MAAM,YAAA,GAAe,MAAA,EAAQ,YAAA,IAAgB,OAAA,CAAQ,IAAI,uBAAuB,CAAA;AAEhF,EAAA,IAAI,CAAC,MAAA,IAAU,CAAC,QAAA,IAAY,CAAC,WAAA,EAAa;AACxC,IAAA,MAAM,IAAI,kBAAA;AAAA,MACR;AAAA,KACF;AAAA,EACF;AAEA,EAAA,OAAO;AAAA,IACL,MAAA;AAAA,IACA,QAAA;AAAA,IACA,WAAA;AAAA,IACA,GAAI,YAAA,GAAe,EAAE,YAAA,KAAiB,EAAC;AAAA,IACvC,GAAI,QAAQ,MAAA,GAAS,EAAE,QAAQ,MAAA,CAAO,MAAA,KAAW,EAAC;AAAA,IAClD,GAAI,QAAQ,WAAA,GAAc,EAAE,aAAa,MAAA,CAAO,WAAA,KAAgB,EAAC;AAAA,IACjE,GAAI,QAAQ,SAAA,GAAY,EAAE,WAAW,MAAA,CAAO,SAAA,KAAc,EAAC;AAAA,IAC3D,GAAI,QAAQ,UAAA,GAAa,EAAE,YAAY,MAAA,CAAO,UAAA,KAAe;AAAC,GAChE;AACF","file":"nextjs.js","sourcesContent":["import { NextResponse, type NextRequest } from 'next/server';\nimport { ApiKeyVerifier, type ApiKeyVerification } from './api-keys.js';\nimport { CboxIdClient } from './client.js';\nimport { ConfigurationError } from './errors.js';\nimport type { AuthorizationRequest, AuthorizationRequestOptions, CboxIdConfig, CboxUser } from './types.js';\n\n/**\n * First-class Next.js (App Router) adapter for {@link CboxIdClient}. It wires the\n * framework-agnostic core to short-lived, httpOnly cookies so login is a couple of\n * route handlers:\n *\n * ```ts\n * // app/auth/[...cbox]/route.ts is up to you; a minimal wiring:\n * import { cboxId } from '@/lib/cbox';\n *\n * export async function GET(request: NextRequest) {\n * return cboxId.signIn();\n * }\n * ```\n *\n * The temporary state/verifier/nonce cookies are httpOnly, `SameSite=Lax`, and\n * expire in 10 minutes, so a stalled login cleans itself up.\n */\n\nconst COOKIE = {\n state: 'cbox_id_state',\n verifier: 'cbox_id_verifier',\n nonce: 'cbox_id_nonce',\n // The step-up requirement has to survive the redirect like the nonce does: a\n // `maxAge` the callback cannot see is a `maxAge` nothing verifies.\n maxAge: 'cbox_id_max_age',\n // Likewise the organization a switch bound to: the callback refuses tokens for another.\n organization: 'cbox_id_organization',\n} as const;\n\nconst TEMP_COOKIE_MAX_AGE = 600; // 10 minutes\n\nexport interface CboxIdNext {\n /** The underlying framework-agnostic client. */\n readonly client: CboxIdClient;\n /**\n * Redirect to Cbox ID's authorize endpoint, stashing PKCE/state/nonce in cookies.\n *\n * `maxAge` (seconds) demands a re-authentication no older than that — the OIDC\n * step-up you want before a payment or an admin grant. It round-trips in a cookie\n * and {@link CboxIdNext.callback} verifies the id_token's `auth_time` against it.\n */\n signIn(options?: SignInOptions): Promise<NextResponse>;\n /**\n * Redirect to a new sign-in bound to another organization — the handler behind an\n * organization switcher. See {@link CboxIdClient.switchOrganization}: a person who is\n * not an active member comes back with `error=access_denied`, and {@link callback}\n * throws an `AuthenticationError` whose `error` is `'access_denied'`.\n */\n switchOrganization(organizationId: string, options?: Omit<SignInOptions, 'organization' | 'organizationHint'>): Promise<NextResponse>;\n /** Complete login on your callback route; returns the authenticated user. */\n callback(request: NextRequest): Promise<CboxUser>;\n /** The hosted profile-page URL (`return_to` appended when given). */\n profileUrl(returnTo?: string): string;\n /** A redirect response to the hosted profile page. */\n profileRedirect(returnTo?: string): NextResponse;\n /** The hosted API-keys page for this app. See {@link CboxIdClient.apiKeysUrl}. */\n apiKeysUrl(options?: { clientId?: string; returnTo?: string; organization?: string }): string;\n /**\n * Verify a customer API key presented to your API. Needs `clientSecret`\n * (`CBOX_ID_CLIENT_SECRET`); see `ApiKeyVerifier` in `@cboxdk/id-js/server`, which\n * this uses with the adapter's configuration and no cache.\n */\n verifyApiKey(key: string): Promise<ApiKeyVerification>;\n /**\n * RP-initiated logout URL, or null when the instance advertises none. Pass the\n * user's `id_token` as `idTokenHint` when you kept it; `client_id` is sent for\n * you, and is what lets the OP honour `returnTo` at all.\n */\n signOutUrl(returnTo?: string, idTokenHint?: string): Promise<string | null>;\n}\n\n/**\n * What {@link CboxIdNext.signIn} accepts: every authorization option except the callback\n * and `state`, which the adapter owns because it stores them in cookies.\n */\nexport type SignInOptions = Omit<AuthorizationRequestOptions, 'redirectUri' | 'state'>;\n\n/**\n * Build a Next.js adapter. Pass a config, or omit it to read from the environment\n * (`CBOX_ID_ISSUER`, `CBOX_ID_CLIENT_ID`, `CBOX_ID_CLIENT_SECRET`,\n * `CBOX_ID_REDIRECT_URI`).\n */\nexport function createCboxId(config?: Partial<CboxIdConfig>): CboxIdNext {\n const resolved = resolveConfig(config);\n const client = new CboxIdClient(resolved);\n let apiKeys: ApiKeyVerifier | undefined;\n\n const tempCookieOptions = {\n httpOnly: true,\n secure: true,\n sameSite: 'lax' as const,\n path: '/',\n maxAge: TEMP_COOKIE_MAX_AGE,\n };\n\n // One place that stashes PKCE/state/nonce, so a switch cannot forget what a sign-in\n // stores — a missing verifier cookie fails the callback, a missing max-age cookie\n // silently skips the step-up check.\n const redirectTo = (request: AuthorizationRequest): NextResponse => {\n const response = NextResponse.redirect(request.url);\n response.cookies.set(COOKIE.state, request.state, tempCookieOptions);\n response.cookies.set(COOKIE.verifier, request.codeVerifier, tempCookieOptions);\n response.cookies.set(COOKIE.nonce, request.nonce, tempCookieOptions);\n if (typeof request.maxAge === 'number') {\n response.cookies.set(COOKIE.maxAge, String(request.maxAge), tempCookieOptions);\n }\n if (request.organization !== undefined) {\n response.cookies.set(COOKIE.organization, request.organization, tempCookieOptions);\n } else {\n // A plain sign-in after an abandoned switch must not inherit its binding.\n response.cookies.delete(COOKIE.organization);\n }\n return response;\n };\n\n return {\n client,\n\n async signIn(options = {}) {\n return redirectTo(await client.createAuthorizationRequest(options));\n },\n\n async switchOrganization(organizationId, options = {}) {\n return redirectTo(await client.switchOrganization(organizationId, options));\n },\n\n async callback(request) {\n const storedMaxAge = request.cookies.get(COOKIE.maxAge)?.value;\n const maxAge = storedMaxAge !== undefined ? Number(storedMaxAge) : undefined;\n const organization = request.cookies.get(COOKIE.organization)?.value;\n\n return client.authenticate({\n params: {\n code: request.nextUrl.searchParams.get('code'),\n state: request.nextUrl.searchParams.get('state'),\n error: request.nextUrl.searchParams.get('error'),\n error_description: request.nextUrl.searchParams.get('error_description'),\n },\n stored: {\n state: request.cookies.get(COOKIE.state)?.value ?? '',\n codeVerifier: request.cookies.get(COOKIE.verifier)?.value ?? '',\n nonce: request.cookies.get(COOKIE.nonce)?.value ?? '',\n ...(maxAge !== undefined && Number.isFinite(maxAge) ? { maxAge } : {}),\n ...(organization !== undefined && organization !== '' ? { organization } : {}),\n },\n });\n },\n\n profileUrl(returnTo) {\n return client.profileUrl(returnTo);\n },\n\n profileRedirect(returnTo) {\n return NextResponse.redirect(client.profileUrl(returnTo));\n },\n\n apiKeysUrl(options) {\n return client.apiKeysUrl(options);\n },\n\n verifyApiKey(key) {\n // Built on first use, so an app that never verifies a key does not need a secret.\n apiKeys ??= new ApiKeyVerifier({\n issuer: resolved.issuer,\n clientId: resolved.clientId,\n clientSecret: resolved.clientSecret ?? '',\n ...(resolved.timeoutMs ? { timeoutMs: resolved.timeoutMs } : {}),\n });\n\n return apiKeys.verifyApiKey(key);\n },\n\n signOutUrl(returnTo, idTokenHint) {\n return client.logoutUrl(returnTo, idTokenHint);\n },\n };\n}\n\nfunction resolveConfig(config?: Partial<CboxIdConfig>): CboxIdConfig {\n const issuer = config?.issuer ?? process.env['CBOX_ID_ISSUER'];\n const clientId = config?.clientId ?? process.env['CBOX_ID_CLIENT_ID'];\n const redirectUri = config?.redirectUri ?? process.env['CBOX_ID_REDIRECT_URI'];\n const clientSecret = config?.clientSecret ?? process.env['CBOX_ID_CLIENT_SECRET'];\n\n if (!issuer || !clientId || !redirectUri) {\n throw new ConfigurationError(\n 'Cbox ID needs issuer, clientId and redirectUri — pass them to createCboxId() or set CBOX_ID_ISSUER / CBOX_ID_CLIENT_ID / CBOX_ID_REDIRECT_URI.',\n );\n }\n\n return {\n issuer,\n clientId,\n redirectUri,\n ...(clientSecret ? { clientSecret } : {}),\n ...(config?.scopes ? { scopes: config.scopes } : {}),\n ...(config?.accountPath ? { accountPath: config.accountPath } : {}),\n ...(config?.timeoutMs ? { timeoutMs: config.timeoutMs } : {}),\n ...(config?.cacheTtlMs ? { cacheTtlMs: config.cacheTtlMs } : {}),\n };\n}\n"]}
|
package/dist/server.cjs
CHANGED
|
@@ -158,16 +158,16 @@ var ApiKeyVerifier = class {
|
|
|
158
158
|
if (typeof body !== "object" || body === null) {
|
|
159
159
|
throw new AuthenticationError("API key verification returned a body that is not a JSON object.");
|
|
160
160
|
}
|
|
161
|
-
const
|
|
162
|
-
if (
|
|
161
|
+
const record2 = body;
|
|
162
|
+
if (record2["active"] !== true) {
|
|
163
163
|
return { active: false };
|
|
164
164
|
}
|
|
165
|
-
const keyId =
|
|
166
|
-
const sub =
|
|
167
|
-
const org =
|
|
168
|
-
const clientId =
|
|
169
|
-
const expiresAt =
|
|
170
|
-
const permissions =
|
|
165
|
+
const keyId = record2["key_id"];
|
|
166
|
+
const sub = record2["sub"];
|
|
167
|
+
const org = record2["org"];
|
|
168
|
+
const clientId = record2["client_id"];
|
|
169
|
+
const expiresAt = record2["expires_at"];
|
|
170
|
+
const permissions = record2["permissions"];
|
|
171
171
|
if (!nonEmpty(keyId) || !nonEmpty(sub) || !nonEmpty(org) || !nonEmpty(clientId) || !Array.isArray(permissions) || expiresAt !== null && expiresAt !== void 0 && typeof expiresAt !== "string") {
|
|
172
172
|
throw new AuthenticationError("API key verification returned an active answer without the promised fields.");
|
|
173
173
|
}
|
|
@@ -182,7 +182,7 @@ var ApiKeyVerifier = class {
|
|
|
182
182
|
return { active: false };
|
|
183
183
|
}
|
|
184
184
|
}
|
|
185
|
-
const role =
|
|
185
|
+
const role = record2["org_role"];
|
|
186
186
|
return {
|
|
187
187
|
active: true,
|
|
188
188
|
key_id: keyId,
|
|
@@ -214,7 +214,101 @@ async function digest(key) {
|
|
|
214
214
|
return Array.from(new Uint8Array(bytes), (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
215
215
|
}
|
|
216
216
|
|
|
217
|
+
// src/decisions.ts
|
|
218
|
+
var PermissionDecision = class {
|
|
219
|
+
constructor(subject, organization, clientId, organizationRole, organizationActive, permissions) {
|
|
220
|
+
this.subject = subject;
|
|
221
|
+
this.organization = organization;
|
|
222
|
+
this.clientId = clientId;
|
|
223
|
+
this.organizationRole = organizationRole;
|
|
224
|
+
this.organizationActive = organizationActive;
|
|
225
|
+
this.permissions = permissions;
|
|
226
|
+
}
|
|
227
|
+
subject;
|
|
228
|
+
organization;
|
|
229
|
+
clientId;
|
|
230
|
+
organizationRole;
|
|
231
|
+
organizationActive;
|
|
232
|
+
permissions;
|
|
233
|
+
allows(permission) {
|
|
234
|
+
return this.organizationActive && this.permissions[permission] === true;
|
|
235
|
+
}
|
|
236
|
+
allowed() {
|
|
237
|
+
const values = Object.values(this.permissions);
|
|
238
|
+
return this.organizationActive && values.length > 0 && values.every((value) => value === true);
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
async function checkPermissions(config, accessToken, subject, organization, permissions) {
|
|
242
|
+
assertSecureIssuer(config.issuer);
|
|
243
|
+
const issuer = new URL(config.issuer);
|
|
244
|
+
if (issuer.username || issuer.password || issuer.search || issuer.hash || !config.clientId || !subject || organization === "" || !accessToken || accessToken.length > 32768 || /[\x00-\x20\x7f]/.test(accessToken) || !permissions.length || permissions.length > 50 || permissions.some((permission) => !permission.trim() || new TextEncoder().encode(permission).length > 255)) {
|
|
245
|
+
throw new ConfigurationError("Permission decision request is not configured or valid.");
|
|
246
|
+
}
|
|
247
|
+
const requested = [...new Set(permissions)];
|
|
248
|
+
try {
|
|
249
|
+
const response = await fetch(`${config.issuer.replace(/\/$/, "")}/oauth/decisions`, {
|
|
250
|
+
method: "POST",
|
|
251
|
+
redirect: "error",
|
|
252
|
+
cache: "no-store",
|
|
253
|
+
signal: AbortSignal.timeout(Math.max(1, config.timeoutMs ?? 1e4)),
|
|
254
|
+
headers: { authorization: `Bearer ${accessToken}`, accept: "application/json", "content-type": "application/json" },
|
|
255
|
+
body: JSON.stringify({ subject, org: organization, permission: requested })
|
|
256
|
+
});
|
|
257
|
+
if (response.status !== 200) {
|
|
258
|
+
await response.body?.cancel();
|
|
259
|
+
throw new AuthenticationError("Permission decision could not be verified.", void 0, void 0, response.status);
|
|
260
|
+
}
|
|
261
|
+
const reader = response.body?.getReader();
|
|
262
|
+
if (!reader) throw new AuthenticationError("Permission decision response is invalid.");
|
|
263
|
+
let size = 0;
|
|
264
|
+
let raw = "";
|
|
265
|
+
const decoder = new TextDecoder();
|
|
266
|
+
try {
|
|
267
|
+
for (; ; ) {
|
|
268
|
+
const { value, done } = await reader.read();
|
|
269
|
+
if (done) break;
|
|
270
|
+
size += value.byteLength;
|
|
271
|
+
if (size > 65536) throw new AuthenticationError("Permission decision response exceeded its limit.");
|
|
272
|
+
raw += decoder.decode(value, { stream: true });
|
|
273
|
+
}
|
|
274
|
+
raw += decoder.decode();
|
|
275
|
+
} finally {
|
|
276
|
+
await reader.cancel();
|
|
277
|
+
reader.releaseLock();
|
|
278
|
+
}
|
|
279
|
+
const data = JSON.parse(raw);
|
|
280
|
+
if (!record(data) || !record(data.subject) || data.mode !== "rbac" || data.subject.type !== "user" || data.subject.id !== subject || data.organization !== organization || data.client_id !== config.clientId || typeof data.organization_active !== "boolean" || typeof data.allowed !== "boolean" || !(data.org_role === null || typeof data.org_role === "string" && ["owner", "admin", "developer", "member", "viewer"].includes(data.org_role)) || !Array.isArray(data.results) || data.results.length !== requested.length) {
|
|
281
|
+
throw new AuthenticationError("Permission decision response binding is invalid.");
|
|
282
|
+
}
|
|
283
|
+
const checks = /* @__PURE__ */ Object.create(null);
|
|
284
|
+
for (const result of data.results) {
|
|
285
|
+
if (!record(result) || typeof result.permission !== "string" || typeof result.allowed !== "boolean" || !requested.includes(result.permission) || Object.hasOwn(checks, result.permission)) {
|
|
286
|
+
throw new AuthenticationError("Permission decision result is invalid.");
|
|
287
|
+
}
|
|
288
|
+
checks[result.permission] = result.allowed;
|
|
289
|
+
}
|
|
290
|
+
const decision = new PermissionDecision(
|
|
291
|
+
subject,
|
|
292
|
+
organization,
|
|
293
|
+
config.clientId,
|
|
294
|
+
data.org_role,
|
|
295
|
+
data.organization_active,
|
|
296
|
+
Object.freeze(checks)
|
|
297
|
+
);
|
|
298
|
+
if (data.allowed !== decision.allowed()) throw new AuthenticationError("Permission decision result is inconsistent.");
|
|
299
|
+
return decision;
|
|
300
|
+
} catch (error) {
|
|
301
|
+
if (error instanceof AuthenticationError) throw error;
|
|
302
|
+
throw new AuthenticationError("Permission decision could not be verified.");
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
function record(value) {
|
|
306
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
307
|
+
}
|
|
308
|
+
|
|
217
309
|
exports.ApiKeyVerifier = ApiKeyVerifier;
|
|
218
310
|
exports.MAX_API_KEY_CACHE_TTL_MS = MAX_API_KEY_CACHE_TTL_MS;
|
|
311
|
+
exports.PermissionDecision = PermissionDecision;
|
|
312
|
+
exports.checkPermissions = checkPermissions;
|
|
219
313
|
//# sourceMappingURL=server.cjs.map
|
|
220
314
|
//# sourceMappingURL=server.cjs.map
|
package/dist/server.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/errors.ts","../src/issuer.ts","../src/api-keys.ts"],"names":[],"mappings":"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","file":"server.cjs","sourcesContent":["/**\n * Base class for every error this SDK throws, so callers can `catch (e) { if (e\n * instanceof CboxIdError) ... }` in one branch.\n */\nexport class CboxIdError extends Error {\n constructor(message: string) {\n super(message);\n this.name = new.target.name;\n }\n}\n\n/** The client is misconfigured (a required option is missing or empty). */\nexport class ConfigurationError extends CboxIdError {}\n\n/**\n * The login `state` did not match what was stored — the callback is forged or\n * stale. Treat as a fresh start, not an error to surface to the user.\n */\nexport class InvalidStateError extends CboxIdError {}\n\n/**\n * Login could not be completed: the provider returned an error, the callback was\n * missing a code, or a token / id_token failed verification.\n *\n * `error` carries the RFC 6749 §5.2 code the authorization server sent, when it sent one.\n * It used to be discarded at every back-channel boundary, leaving a single message string\n * for outcomes that need opposite responses: `invalid_grant` on a refresh means the\n * session is over and the person has to sign in again, while `temporarily_unavailable`\n * means try the same token in a moment. A caller reduced to matching on prose either\n * retries what can never succeed, or signs out somebody who did not need to be.\n */\nexport class AuthenticationError extends CboxIdError {\n constructor(\n message: string,\n /** RFC 6749 §5.2 error code, when the server sent a parseable one. */\n readonly error?: string,\n /** The server's `error_description`, verbatim. Not end-user copy. */\n readonly errorDescription?: string,\n /** HTTP status, for the cases where the body says nothing useful. */\n readonly status?: number,\n /**\n * Seconds to wait, off the `Retry-After` header — set only on a 429.\n *\n * The distinction it carries is the one a caller most needs and could least infer:\n * a 429 is the ONLY back-channel failure where the same request will succeed\n * unchanged if you simply wait. Every other one needs a different request, or a new\n * sign-in. Without this the SDK reported \"token refresh failed\" and a caller with a\n * retry loop hammered a limiter that was already telling it exactly how long to stop.\n */\n readonly retryAfter?: number,\n ) {\n super(message);\n }\n\n /** Whether waiting `retryAfter` seconds and repeating the same request is worth it. */\n get isRateLimited(): boolean {\n return this.status === 429;\n }\n}\n\n/**\n * Read an OAuth error out of a failed token-endpoint response.\n *\n * Best-effort by design: a 502 from a proxy is HTML, a captive portal is worse, and the\n * caller still needs an error rather than a parse exception. What it must never do is\n * invent a code — an absent or unparseable `error` stays undefined, so\n * `e.error === 'invalid_grant'` is true only because the server said so.\n */\nexport async function oauthError(response: Response, fallback: string): Promise<AuthenticationError> {\n const body = await response.text().catch(() => '');\n\n let code: string | undefined;\n let description: string | undefined;\n\n try {\n const parsed: unknown = JSON.parse(body);\n\n if (typeof parsed === 'object' && parsed !== null) {\n const { error, error_description: errorDescription } = parsed as Record<string, unknown>;\n code = typeof error === 'string' ? error : undefined;\n description = typeof errorDescription === 'string' ? errorDescription : undefined;\n }\n } catch {\n // Not JSON. The status and the truncated body below are all there is to report.\n }\n\n // THE BODY IS NEVER ECHOED. These calls carry `client_secret`, `code_verifier` and\n // refresh tokens in their REQUEST, and a debug proxy, a captive portal or an upstream\n // error page that reflects what it received puts those straight into a thrown message\n // — which production apps log. Only the server's own OAuth error code travels, and\n // failing that, the status alone.\n const detail = code ?? `HTTP ${response.status}`;\n\n // `Retry-After` is seconds here — Laravel's throttler writes the integer form. The\n // HTTP-date form is legal too and is deliberately NOT parsed: guessing at a clock\n // skew is worse than saying nothing, and `status === 429` still tells the caller to\n // back off.\n const retryAfterHeader = response.headers.get('retry-after');\n const retryAfter = retryAfterHeader !== null && /^\\d+$/.test(retryAfterHeader.trim())\n ? Number(retryAfterHeader.trim())\n : undefined;\n\n return new AuthenticationError(`${fallback}: ${detail}`, code, description, response.status, retryAfter);\n}\n\n/**\n * The Frontend API refused, or could not be reached.\n *\n * Carries a `code` because the four ways this goes wrong need four different responses\n * from the caller, and telling them apart from a message string is the kind of thing\n * that works until somebody rewords the message:\n *\n * - `origin_not_allowed` — the overwhelmingly likely cause of a refusal, and a\n * configuration problem the developer fixes once in the console.\n * - `rate_limited` — back off; `retryAfter` says for how long, when the server said.\n * - `unavailable` — the network, or a 5xx. Transient by assumption; already retried.\n * - `malformed` — a 200 whose body is not the document we expect, which means something\n * between the page and us is rewriting responses (a proxy, an extension, a captive\n * portal). Worth distinguishing, because retrying it never helps.\n */\nexport class FrontendApiError extends CboxIdError {\n constructor(\n message: string,\n readonly code: 'origin_not_allowed' | 'rate_limited' | 'unavailable' | 'malformed',\n readonly status?: number,\n readonly retryAfter?: number,\n ) {\n super(message);\n }\n}\n","import { ConfigurationError } from './errors.js';\n\n/**\n * An issuer must be HTTPS.\n *\n * Everything this SDK does with it carries a credential: the authorization code, the PKCE\n * verifier, the client secret, the refresh token. Over `http://` a network attacker reads\n * all of them — and, worse, replaces the discovery document and JWKS, at which point a\n * forged id_token verifies cleanly and the whole verification chain proves nothing.\n *\n * Loopback is allowed because a native app's own callback listener is loopback by\n * definition (RFC 8252) and a dev instance runs there.\n */\nexport function assertSecureIssuer(issuer: string): void {\n let url: URL;\n\n try {\n url = new URL(issuer);\n } catch {\n throw new ConfigurationError('Cbox ID config `issuer` is not a valid URL.');\n }\n\n if (url.protocol === 'https:') {\n return;\n }\n\n const loopback = url.hostname === 'localhost' || url.hostname === '127.0.0.1' || url.hostname === '[::1]' || url.hostname === '::1';\n\n if (url.protocol === 'http:' && loopback) {\n return;\n }\n\n throw new ConfigurationError(\n `Cbox ID config \\`issuer\\` must be https (got ${url.protocol}//${url.hostname}).`,\n );\n}\n","import type { OrganizationRole } from './claims.js';\nimport { AuthenticationError, ConfigurationError, oauthError } from './errors.js';\nimport { assertSecureIssuer } from './issuer.js';\n\n/**\n * A customer API key Cbox ID vouches for: live, bound to YOUR app, and held by an active\n * member of `org`.\n *\n * The field names are the endpoint's own, so the claim helpers read it like a token —\n * `hasPermission(answer, 'invoices:create')`, `organization(answer)?.role`. (A type\n * alias rather than an interface for that reason: only an alias is assignable to the\n * helpers' record type.)\n */\nexport type ActiveApiKey = {\n active: true;\n /** The key's id — log this, never the key. */\n key_id: string;\n /** The person who holds the key (their subject id). */\n sub: string;\n /** The organization the key acts in. */\n org: string;\n /** The holder's membership tier in `org`; null for a tier this SDK does not know. */\n org_role: OrganizationRole | null;\n /**\n * What the key may do, already re-capped by Cbox ID: the key's own list intersected\n * with what the holder holds for your app right now. A demoted holder's key loses the\n * permission on the next verification, not at the next reissue.\n */\n permissions: string[];\n /** Your app's client id — always, or the answer is refused. */\n client_id: string;\n /** ISO 8601 UTC expiry, or null for a key that does not expire. */\n expires_at: string | null;\n};\n\n/**\n * Every refusal: unknown, malformed, revoked, expired, another app's key, a holder who\n * left. Cbox ID never says which, so the endpoint cannot be used to probe keys — and\n * neither does this.\n */\nexport interface InactiveApiKey {\n active: false;\n}\n\n/** What {@link ApiKeyVerifier.verifyApiKey} returns. Narrow on `active`. */\nexport type ApiKeyVerification = ActiveApiKey | InactiveApiKey;\n\n/** Configuration for an {@link ApiKeyVerifier}. */\nexport interface ApiKeyVerifierConfig {\n /** Base URL (issuer) of the Cbox ID instance. */\n issuer: string;\n /** Your app's OAuth client id — the app the keys were issued for. */\n clientId: string;\n /** Your app's client secret. Verification is a confidential-client call. */\n clientSecret: string;\n /** Timeout (ms) for the verification request. Defaults to `10000`. */\n timeoutMs?: number;\n /**\n * Cache an ACTIVE answer for this long (ms), so a burst of requests with one key costs\n * one round-trip. Off (`0`) by default and at most {@link MAX_API_KEY_CACHE_TTL_MS}:\n * while an answer is cached, a revoked key keeps working, and a demoted holder keeps\n * the permissions they lost. Never cached past the key's `expires_at`. Refusals are\n * never cached, so a key issued a moment ago works on its first request.\n */\n cacheTtlMs?: number;\n}\n\n/** The longest {@link ApiKeyVerifierConfig.cacheTtlMs} allowed: one minute. */\nexport const MAX_API_KEY_CACHE_TTL_MS = 60_000;\n\n/** Enough for a busy API's working set; oldest entries go first beyond it. */\nconst MAX_CACHE_ENTRIES = 1_000;\n\nconst ORGANIZATION_ROLES: ReadonlySet<string> = new Set<OrganizationRole>([\n 'owner',\n 'admin',\n 'developer',\n 'member',\n 'viewer',\n]);\n\n/**\n * Verifies customer API keys presented to your API: `POST {issuer}/oauth/api-keys/verify`,\n * authenticated with your app's own client credentials.\n *\n * SERVER-SIDE ONLY. It holds your client secret, so it lives in the\n * `@cboxdk/id-js/server` entry and the Next.js adapter, never in the main entry that a\n * browser bundle pulls in — and it refuses to be constructed in a browser at all.\n *\n * ```ts\n * import { ApiKeyVerifier } from '@cboxdk/id-js/server';\n * import { hasPermission } from '@cboxdk/id-js';\n *\n * const keys = new ApiKeyVerifier({ issuer, clientId, clientSecret, cacheTtlMs: 10_000 });\n *\n * const answer = await keys.verifyApiKey(request.headers.get('x-api-key') ?? '');\n * if (!answer.active) return unauthorized();\n * if (!hasPermission(answer, 'invoices:create')) return forbidden();\n * ```\n */\nexport class ApiKeyVerifier {\n private readonly endpoint: string;\n private readonly timeoutMs: number;\n private readonly cacheTtlMs: number;\n private readonly cache = new Map<string, { answer: ActiveApiKey; until: number }>();\n\n constructor(private readonly config: ApiKeyVerifierConfig) {\n // A heuristic, and enough of one: a page has a `document`, and no server runtime\n // this SDK supports (Node, Deno, Bun, edge workers) defines one.\n if (typeof (globalThis as { document?: unknown }).document !== 'undefined') {\n throw new ConfigurationError(\n 'API key verification uses your client secret and must run on your server, never in a browser.',\n );\n }\n if (!config.issuer || !config.clientId || !config.clientSecret) {\n throw new ConfigurationError('Verifying API keys needs issuer, clientId and clientSecret.');\n }\n assertSecureIssuer(config.issuer);\n\n const cacheTtlMs = config.cacheTtlMs ?? 0;\n if (!Number.isFinite(cacheTtlMs) || cacheTtlMs < 0 || cacheTtlMs > MAX_API_KEY_CACHE_TTL_MS) {\n throw new ConfigurationError(\n `cacheTtlMs must be between 0 and ${MAX_API_KEY_CACHE_TTL_MS}: a cached answer keeps a revoked key working for that long.`,\n );\n }\n\n this.endpoint = `${config.issuer.replace(/\\/$/, '')}/oauth/api-keys/verify`;\n this.timeoutMs = config.timeoutMs ?? 10_000;\n this.cacheTtlMs = cacheTtlMs;\n }\n\n /**\n * Ask Cbox ID whether `key` is good for this app, and what it may do.\n *\n * Resolves `{ active: false }` for every kind of bad key, without a request for an empty\n * one. Throws `AuthenticationError` when the call itself fails (wrong client\n * credentials, an unreachable instance) or when the answer cannot be trusted — one that\n * names another app's `client_id`, or does not have the promised shape. Both mean \"do\n * not let this request in\"; the difference is that a throw is something to look at.\n */\n async verifyApiKey(key: string): Promise<ApiKeyVerification> {\n if (typeof key !== 'string' || key.trim() === '') {\n return { active: false };\n }\n\n const cacheKey = this.cacheTtlMs > 0 ? await digest(key) : null;\n if (cacheKey !== null) {\n const hit = this.cache.get(cacheKey);\n if (hit && hit.until > Date.now()) {\n return hit.answer;\n }\n this.cache.delete(cacheKey);\n }\n\n const answer = this.parse(await this.request(key));\n\n if (cacheKey !== null && answer.active) {\n this.remember(cacheKey, answer);\n }\n\n return answer;\n }\n\n private async request(key: string): Promise<unknown> {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), this.timeoutMs);\n\n let response: Response;\n try {\n response = await fetch(this.endpoint, {\n method: 'POST',\n headers: {\n authorization: `Basic ${btoa(`${this.config.clientId}:${this.config.clientSecret}`)}`,\n accept: 'application/json',\n 'content-type': 'application/x-www-form-urlencoded',\n },\n body: new URLSearchParams({ key }),\n signal: controller.signal,\n });\n } catch (error) {\n throw new AuthenticationError(`API key verification request failed: ${String(error)}`);\n } finally {\n clearTimeout(timer);\n }\n\n if (!response.ok) {\n throw await oauthError(response, 'API key verification failed');\n }\n\n return response.json().catch(() => null);\n }\n\n private parse(body: unknown): ApiKeyVerification {\n if (typeof body !== 'object' || body === null) {\n throw new AuthenticationError('API key verification returned a body that is not a JSON object.');\n }\n\n const record = body as Record<string, unknown>;\n\n // Only a literal `true` lets anybody in.\n if (record['active'] !== true) {\n return { active: false };\n }\n\n const keyId = record['key_id'];\n const sub = record['sub'];\n const org = record['org'];\n const clientId = record['client_id'];\n const expiresAt = record['expires_at'];\n const permissions = record['permissions'];\n\n if (\n !nonEmpty(keyId) ||\n !nonEmpty(sub) ||\n !nonEmpty(org) ||\n !nonEmpty(clientId) ||\n !Array.isArray(permissions) ||\n (expiresAt !== null && expiresAt !== undefined && typeof expiresAt !== 'string')\n ) {\n throw new AuthenticationError('API key verification returned an active answer without the promised fields.');\n }\n\n // THE BINDING, CHECKED AGAIN HERE. Cbox ID only ever finds a key bound to the calling\n // client, so this cannot fire against a correct instance — which is exactly when a\n // check that it holds is worth having: a proxy, a misrouted base URL, or a shared\n // secret between two apps would otherwise let one app's customers into another's API.\n if (clientId !== this.config.clientId) {\n throw new AuthenticationError(\n `API key verification answered for client ${clientId}, not this app (${this.config.clientId}); refusing it.`,\n );\n }\n\n // Past its expiry by our clock is expired, whatever the instance thought a moment ago.\n if (typeof expiresAt === 'string') {\n const expiry = Date.parse(expiresAt);\n if (Number.isNaN(expiry) || expiry <= Date.now()) {\n return { active: false };\n }\n }\n\n const role = record['org_role'];\n\n return {\n active: true,\n key_id: keyId,\n sub,\n org,\n org_role: typeof role === 'string' && ORGANIZATION_ROLES.has(role) ? (role as OrganizationRole) : null,\n permissions: permissions.filter((value): value is string => nonEmpty(value)),\n client_id: clientId,\n expires_at: typeof expiresAt === 'string' ? expiresAt : null,\n };\n }\n\n private remember(cacheKey: string, answer: ActiveApiKey): void {\n const expiry = answer.expires_at === null ? Number.POSITIVE_INFINITY : Date.parse(answer.expires_at);\n const until = Math.min(Date.now() + this.cacheTtlMs, expiry);\n\n if (this.cache.size >= MAX_CACHE_ENTRIES) {\n const oldest = this.cache.keys().next();\n if (!oldest.done) {\n this.cache.delete(oldest.value);\n }\n }\n\n this.cache.set(cacheKey, { answer, until });\n }\n}\n\nfunction nonEmpty(value: unknown): value is string {\n return typeof value === 'string' && value !== '';\n}\n\n/** The cache is keyed by a hash, so live keys are not sitting in memory as plaintext. */\nasync function digest(key: string): Promise<string> {\n const bytes = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(key));\n return Array.from(new Uint8Array(bytes), (byte) => byte.toString(16).padStart(2, '0')).join('');\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/errors.ts","../src/issuer.ts","../src/api-keys.ts","../src/decisions.ts"],"names":["record"],"mappings":"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","file":"server.cjs","sourcesContent":["/**\n * Base class for every error this SDK throws, so callers can `catch (e) { if (e\n * instanceof CboxIdError) ... }` in one branch.\n */\nexport class CboxIdError extends Error {\n constructor(message: string) {\n super(message);\n this.name = new.target.name;\n }\n}\n\n/** The client is misconfigured (a required option is missing or empty). */\nexport class ConfigurationError extends CboxIdError {}\n\n/**\n * The login `state` did not match what was stored — the callback is forged or\n * stale. Treat as a fresh start, not an error to surface to the user.\n */\nexport class InvalidStateError extends CboxIdError {}\n\n/**\n * Login could not be completed: the provider returned an error, the callback was\n * missing a code, or a token / id_token failed verification.\n *\n * `error` carries the RFC 6749 §5.2 code the authorization server sent, when it sent one.\n * It used to be discarded at every back-channel boundary, leaving a single message string\n * for outcomes that need opposite responses: `invalid_grant` on a refresh means the\n * session is over and the person has to sign in again, while `temporarily_unavailable`\n * means try the same token in a moment. A caller reduced to matching on prose either\n * retries what can never succeed, or signs out somebody who did not need to be.\n */\nexport class AuthenticationError extends CboxIdError {\n constructor(\n message: string,\n /** RFC 6749 §5.2 error code, when the server sent a parseable one. */\n readonly error?: string,\n /** The server's `error_description`, verbatim. Not end-user copy. */\n readonly errorDescription?: string,\n /** HTTP status, for the cases where the body says nothing useful. */\n readonly status?: number,\n /**\n * Seconds to wait, off the `Retry-After` header — set only on a 429.\n *\n * The distinction it carries is the one a caller most needs and could least infer:\n * a 429 is the ONLY back-channel failure where the same request will succeed\n * unchanged if you simply wait. Every other one needs a different request, or a new\n * sign-in. Without this the SDK reported \"token refresh failed\" and a caller with a\n * retry loop hammered a limiter that was already telling it exactly how long to stop.\n */\n readonly retryAfter?: number,\n ) {\n super(message);\n }\n\n /** Whether waiting `retryAfter` seconds and repeating the same request is worth it. */\n get isRateLimited(): boolean {\n return this.status === 429;\n }\n}\n\n/**\n * Read an OAuth error out of a failed token-endpoint response.\n *\n * Best-effort by design: a 502 from a proxy is HTML, a captive portal is worse, and the\n * caller still needs an error rather than a parse exception. What it must never do is\n * invent a code — an absent or unparseable `error` stays undefined, so\n * `e.error === 'invalid_grant'` is true only because the server said so.\n */\nexport async function oauthError(response: Response, fallback: string): Promise<AuthenticationError> {\n const body = await response.text().catch(() => '');\n\n let code: string | undefined;\n let description: string | undefined;\n\n try {\n const parsed: unknown = JSON.parse(body);\n\n if (typeof parsed === 'object' && parsed !== null) {\n const { error, error_description: errorDescription } = parsed as Record<string, unknown>;\n code = typeof error === 'string' ? error : undefined;\n description = typeof errorDescription === 'string' ? errorDescription : undefined;\n }\n } catch {\n // Not JSON. The status and the truncated body below are all there is to report.\n }\n\n // THE BODY IS NEVER ECHOED. These calls carry `client_secret`, `code_verifier` and\n // refresh tokens in their REQUEST, and a debug proxy, a captive portal or an upstream\n // error page that reflects what it received puts those straight into a thrown message\n // — which production apps log. Only the server's own OAuth error code travels, and\n // failing that, the status alone.\n const detail = code ?? `HTTP ${response.status}`;\n\n // `Retry-After` is seconds here — Laravel's throttler writes the integer form. The\n // HTTP-date form is legal too and is deliberately NOT parsed: guessing at a clock\n // skew is worse than saying nothing, and `status === 429` still tells the caller to\n // back off.\n const retryAfterHeader = response.headers.get('retry-after');\n const retryAfter = retryAfterHeader !== null && /^\\d+$/.test(retryAfterHeader.trim())\n ? Number(retryAfterHeader.trim())\n : undefined;\n\n return new AuthenticationError(`${fallback}: ${detail}`, code, description, response.status, retryAfter);\n}\n\n/**\n * The Frontend API refused, or could not be reached.\n *\n * Carries a `code` because the four ways this goes wrong need four different responses\n * from the caller, and telling them apart from a message string is the kind of thing\n * that works until somebody rewords the message:\n *\n * - `origin_not_allowed` — the overwhelmingly likely cause of a refusal, and a\n * configuration problem the developer fixes once in the console.\n * - `rate_limited` — back off; `retryAfter` says for how long, when the server said.\n * - `unavailable` — the network, or a 5xx. Transient by assumption; already retried.\n * - `malformed` — a 200 whose body is not the document we expect, which means something\n * between the page and us is rewriting responses (a proxy, an extension, a captive\n * portal). Worth distinguishing, because retrying it never helps.\n */\nexport class FrontendApiError extends CboxIdError {\n constructor(\n message: string,\n readonly code: 'origin_not_allowed' | 'rate_limited' | 'unavailable' | 'malformed',\n readonly status?: number,\n readonly retryAfter?: number,\n ) {\n super(message);\n }\n}\n","import { ConfigurationError } from './errors.js';\n\n/**\n * An issuer must be HTTPS.\n *\n * Everything this SDK does with it carries a credential: the authorization code, the PKCE\n * verifier, the client secret, the refresh token. Over `http://` a network attacker reads\n * all of them — and, worse, replaces the discovery document and JWKS, at which point a\n * forged id_token verifies cleanly and the whole verification chain proves nothing.\n *\n * Loopback is allowed because a native app's own callback listener is loopback by\n * definition (RFC 8252) and a dev instance runs there.\n */\nexport function assertSecureIssuer(issuer: string): void {\n let url: URL;\n\n try {\n url = new URL(issuer);\n } catch {\n throw new ConfigurationError('Cbox ID config `issuer` is not a valid URL.');\n }\n\n if (url.protocol === 'https:') {\n return;\n }\n\n const loopback = url.hostname === 'localhost' || url.hostname === '127.0.0.1' || url.hostname === '[::1]' || url.hostname === '::1';\n\n if (url.protocol === 'http:' && loopback) {\n return;\n }\n\n throw new ConfigurationError(\n `Cbox ID config \\`issuer\\` must be https (got ${url.protocol}//${url.hostname}).`,\n );\n}\n","import type { OrganizationRole } from './claims.js';\nimport { AuthenticationError, ConfigurationError, oauthError } from './errors.js';\nimport { assertSecureIssuer } from './issuer.js';\n\n/**\n * A customer API key Cbox ID vouches for: live, bound to YOUR app, and held by an active\n * member of `org`.\n *\n * The field names are the endpoint's own, so the claim helpers read it like a token —\n * `hasPermission(answer, 'invoices:create')`, `organization(answer)?.role`. (A type\n * alias rather than an interface for that reason: only an alias is assignable to the\n * helpers' record type.)\n */\nexport type ActiveApiKey = {\n active: true;\n /** The key's id — log this, never the key. */\n key_id: string;\n /** The person who holds the key (their subject id). */\n sub: string;\n /** The organization the key acts in. */\n org: string;\n /** The holder's membership tier in `org`; null for a tier this SDK does not know. */\n org_role: OrganizationRole | null;\n /**\n * What the key may do, already re-capped by Cbox ID: the key's own list intersected\n * with what the holder holds for your app right now. A demoted holder's key loses the\n * permission on the next verification, not at the next reissue.\n */\n permissions: string[];\n /** Your app's client id — always, or the answer is refused. */\n client_id: string;\n /** ISO 8601 UTC expiry, or null for a key that does not expire. */\n expires_at: string | null;\n};\n\n/**\n * Every refusal: unknown, malformed, revoked, expired, another app's key, a holder who\n * left. Cbox ID never says which, so the endpoint cannot be used to probe keys — and\n * neither does this.\n */\nexport interface InactiveApiKey {\n active: false;\n}\n\n/** What {@link ApiKeyVerifier.verifyApiKey} returns. Narrow on `active`. */\nexport type ApiKeyVerification = ActiveApiKey | InactiveApiKey;\n\n/** Configuration for an {@link ApiKeyVerifier}. */\nexport interface ApiKeyVerifierConfig {\n /** Base URL (issuer) of the Cbox ID instance. */\n issuer: string;\n /** Your app's OAuth client id — the app the keys were issued for. */\n clientId: string;\n /** Your app's client secret. Verification is a confidential-client call. */\n clientSecret: string;\n /** Timeout (ms) for the verification request. Defaults to `10000`. */\n timeoutMs?: number;\n /**\n * Cache an ACTIVE answer for this long (ms), so a burst of requests with one key costs\n * one round-trip. Off (`0`) by default and at most {@link MAX_API_KEY_CACHE_TTL_MS}:\n * while an answer is cached, a revoked key keeps working, and a demoted holder keeps\n * the permissions they lost. Never cached past the key's `expires_at`. Refusals are\n * never cached, so a key issued a moment ago works on its first request.\n */\n cacheTtlMs?: number;\n}\n\n/** The longest {@link ApiKeyVerifierConfig.cacheTtlMs} allowed: one minute. */\nexport const MAX_API_KEY_CACHE_TTL_MS = 60_000;\n\n/** Enough for a busy API's working set; oldest entries go first beyond it. */\nconst MAX_CACHE_ENTRIES = 1_000;\n\nconst ORGANIZATION_ROLES: ReadonlySet<string> = new Set<OrganizationRole>([\n 'owner',\n 'admin',\n 'developer',\n 'member',\n 'viewer',\n]);\n\n/**\n * Verifies customer API keys presented to your API: `POST {issuer}/oauth/api-keys/verify`,\n * authenticated with your app's own client credentials.\n *\n * SERVER-SIDE ONLY. It holds your client secret, so it lives in the\n * `@cboxdk/id-js/server` entry and the Next.js adapter, never in the main entry that a\n * browser bundle pulls in — and it refuses to be constructed in a browser at all.\n *\n * ```ts\n * import { ApiKeyVerifier } from '@cboxdk/id-js/server';\n * import { hasPermission } from '@cboxdk/id-js';\n *\n * const keys = new ApiKeyVerifier({ issuer, clientId, clientSecret, cacheTtlMs: 10_000 });\n *\n * const answer = await keys.verifyApiKey(request.headers.get('x-api-key') ?? '');\n * if (!answer.active) return unauthorized();\n * if (!hasPermission(answer, 'invoices:create')) return forbidden();\n * ```\n */\nexport class ApiKeyVerifier {\n private readonly endpoint: string;\n private readonly timeoutMs: number;\n private readonly cacheTtlMs: number;\n private readonly cache = new Map<string, { answer: ActiveApiKey; until: number }>();\n\n constructor(private readonly config: ApiKeyVerifierConfig) {\n // A heuristic, and enough of one: a page has a `document`, and no server runtime\n // this SDK supports (Node, Deno, Bun, edge workers) defines one.\n if (typeof (globalThis as { document?: unknown }).document !== 'undefined') {\n throw new ConfigurationError(\n 'API key verification uses your client secret and must run on your server, never in a browser.',\n );\n }\n if (!config.issuer || !config.clientId || !config.clientSecret) {\n throw new ConfigurationError('Verifying API keys needs issuer, clientId and clientSecret.');\n }\n assertSecureIssuer(config.issuer);\n\n const cacheTtlMs = config.cacheTtlMs ?? 0;\n if (!Number.isFinite(cacheTtlMs) || cacheTtlMs < 0 || cacheTtlMs > MAX_API_KEY_CACHE_TTL_MS) {\n throw new ConfigurationError(\n `cacheTtlMs must be between 0 and ${MAX_API_KEY_CACHE_TTL_MS}: a cached answer keeps a revoked key working for that long.`,\n );\n }\n\n this.endpoint = `${config.issuer.replace(/\\/$/, '')}/oauth/api-keys/verify`;\n this.timeoutMs = config.timeoutMs ?? 10_000;\n this.cacheTtlMs = cacheTtlMs;\n }\n\n /**\n * Ask Cbox ID whether `key` is good for this app, and what it may do.\n *\n * Resolves `{ active: false }` for every kind of bad key, without a request for an empty\n * one. Throws `AuthenticationError` when the call itself fails (wrong client\n * credentials, an unreachable instance) or when the answer cannot be trusted — one that\n * names another app's `client_id`, or does not have the promised shape. Both mean \"do\n * not let this request in\"; the difference is that a throw is something to look at.\n */\n async verifyApiKey(key: string): Promise<ApiKeyVerification> {\n if (typeof key !== 'string' || key.trim() === '') {\n return { active: false };\n }\n\n const cacheKey = this.cacheTtlMs > 0 ? await digest(key) : null;\n if (cacheKey !== null) {\n const hit = this.cache.get(cacheKey);\n if (hit && hit.until > Date.now()) {\n return hit.answer;\n }\n this.cache.delete(cacheKey);\n }\n\n const answer = this.parse(await this.request(key));\n\n if (cacheKey !== null && answer.active) {\n this.remember(cacheKey, answer);\n }\n\n return answer;\n }\n\n private async request(key: string): Promise<unknown> {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), this.timeoutMs);\n\n let response: Response;\n try {\n response = await fetch(this.endpoint, {\n method: 'POST',\n headers: {\n authorization: `Basic ${btoa(`${this.config.clientId}:${this.config.clientSecret}`)}`,\n accept: 'application/json',\n 'content-type': 'application/x-www-form-urlencoded',\n },\n body: new URLSearchParams({ key }),\n signal: controller.signal,\n });\n } catch (error) {\n throw new AuthenticationError(`API key verification request failed: ${String(error)}`);\n } finally {\n clearTimeout(timer);\n }\n\n if (!response.ok) {\n throw await oauthError(response, 'API key verification failed');\n }\n\n return response.json().catch(() => null);\n }\n\n private parse(body: unknown): ApiKeyVerification {\n if (typeof body !== 'object' || body === null) {\n throw new AuthenticationError('API key verification returned a body that is not a JSON object.');\n }\n\n const record = body as Record<string, unknown>;\n\n // Only a literal `true` lets anybody in.\n if (record['active'] !== true) {\n return { active: false };\n }\n\n const keyId = record['key_id'];\n const sub = record['sub'];\n const org = record['org'];\n const clientId = record['client_id'];\n const expiresAt = record['expires_at'];\n const permissions = record['permissions'];\n\n if (\n !nonEmpty(keyId) ||\n !nonEmpty(sub) ||\n !nonEmpty(org) ||\n !nonEmpty(clientId) ||\n !Array.isArray(permissions) ||\n (expiresAt !== null && expiresAt !== undefined && typeof expiresAt !== 'string')\n ) {\n throw new AuthenticationError('API key verification returned an active answer without the promised fields.');\n }\n\n // THE BINDING, CHECKED AGAIN HERE. Cbox ID only ever finds a key bound to the calling\n // client, so this cannot fire against a correct instance — which is exactly when a\n // check that it holds is worth having: a proxy, a misrouted base URL, or a shared\n // secret between two apps would otherwise let one app's customers into another's API.\n if (clientId !== this.config.clientId) {\n throw new AuthenticationError(\n `API key verification answered for client ${clientId}, not this app (${this.config.clientId}); refusing it.`,\n );\n }\n\n // Past its expiry by our clock is expired, whatever the instance thought a moment ago.\n if (typeof expiresAt === 'string') {\n const expiry = Date.parse(expiresAt);\n if (Number.isNaN(expiry) || expiry <= Date.now()) {\n return { active: false };\n }\n }\n\n const role = record['org_role'];\n\n return {\n active: true,\n key_id: keyId,\n sub,\n org,\n org_role: typeof role === 'string' && ORGANIZATION_ROLES.has(role) ? (role as OrganizationRole) : null,\n permissions: permissions.filter((value): value is string => nonEmpty(value)),\n client_id: clientId,\n expires_at: typeof expiresAt === 'string' ? expiresAt : null,\n };\n }\n\n private remember(cacheKey: string, answer: ActiveApiKey): void {\n const expiry = answer.expires_at === null ? Number.POSITIVE_INFINITY : Date.parse(answer.expires_at);\n const until = Math.min(Date.now() + this.cacheTtlMs, expiry);\n\n if (this.cache.size >= MAX_CACHE_ENTRIES) {\n const oldest = this.cache.keys().next();\n if (!oldest.done) {\n this.cache.delete(oldest.value);\n }\n }\n\n this.cache.set(cacheKey, { answer, until });\n }\n}\n\nfunction nonEmpty(value: unknown): value is string {\n return typeof value === 'string' && value !== '';\n}\n\n/** The cache is keyed by a hash, so live keys are not sitting in memory as plaintext. */\nasync function digest(key: string): Promise<string> {\n const bytes = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(key));\n return Array.from(new Uint8Array(bytes), (byte) => byte.toString(16).padStart(2, '0')).join('');\n}\n","import { AuthenticationError, ConfigurationError } from './errors.js';\nimport { assertSecureIssuer } from './issuer.js';\nimport type { OrganizationRole } from './claims.js';\n\nexport interface PermissionDecisionConfig {\n issuer: string;\n clientId: string;\n timeoutMs?: number;\n}\n\n/** A current server decision; shared membership tiers are not app grants. */\nexport class PermissionDecision {\n constructor(\n readonly subject: string,\n readonly organization: string | null,\n readonly clientId: string,\n readonly organizationRole: OrganizationRole | null,\n readonly organizationActive: boolean,\n readonly permissions: Readonly<Record<string, boolean>>,\n ) {}\n\n allows(permission: string): boolean {\n return this.organizationActive && this.permissions[permission] === true;\n }\n\n allowed(): boolean {\n const values = Object.values(this.permissions);\n return this.organizationActive && values.length > 0 && values.every(value => value === true);\n }\n}\n\n/** No positive cache: check each request and again when queued work starts. */\nexport async function checkPermissions(\n config: PermissionDecisionConfig,\n accessToken: string,\n subject: string,\n organization: string | null,\n permissions: readonly string[],\n): Promise<PermissionDecision> {\n assertSecureIssuer(config.issuer);\n const issuer = new URL(config.issuer);\n if (issuer.username || issuer.password || issuer.search || issuer.hash || !config.clientId\n || !subject || organization === '' || !accessToken || accessToken.length > 32768\n || /[\\x00-\\x20\\x7f]/.test(accessToken) || !permissions.length || permissions.length > 50\n || permissions.some(permission => !permission.trim() || new TextEncoder().encode(permission).length > 255)) {\n throw new ConfigurationError('Permission decision request is not configured or valid.');\n }\n const requested = [...new Set(permissions)];\n try {\n const response = await fetch(`${config.issuer.replace(/\\/$/, '')}/oauth/decisions`, {\n method: 'POST', redirect: 'error', cache: 'no-store',\n signal: AbortSignal.timeout(Math.max(1, config.timeoutMs ?? 10_000)),\n headers: { authorization: `Bearer ${accessToken}`, accept: 'application/json', 'content-type': 'application/json' },\n body: JSON.stringify({ subject, org: organization, permission: requested }),\n });\n if (response.status !== 200) {\n await response.body?.cancel();\n throw new AuthenticationError('Permission decision could not be verified.', undefined, undefined, response.status);\n }\n const reader = response.body?.getReader();\n if (!reader) throw new AuthenticationError('Permission decision response is invalid.');\n let size = 0;\n let raw = '';\n const decoder = new TextDecoder();\n try {\n for (;;) {\n const { value, done } = await reader.read();\n if (done) break;\n size += value.byteLength;\n if (size > 65536) throw new AuthenticationError('Permission decision response exceeded its limit.');\n raw += decoder.decode(value, { stream: true });\n }\n raw += decoder.decode();\n } finally {\n await reader.cancel();\n reader.releaseLock();\n }\n const data: unknown = JSON.parse(raw);\n if (!record(data) || !record(data.subject) || data.mode !== 'rbac'\n || data.subject.type !== 'user' || data.subject.id !== subject\n || data.organization !== organization || data.client_id !== config.clientId\n || typeof data.organization_active !== 'boolean' || typeof data.allowed !== 'boolean'\n || !(data.org_role === null || (typeof data.org_role === 'string'\n && ['owner', 'admin', 'developer', 'member', 'viewer'].includes(data.org_role)))\n || !Array.isArray(data.results) || data.results.length !== requested.length) {\n throw new AuthenticationError('Permission decision response binding is invalid.');\n }\n const checks: Record<string, boolean> = Object.create(null) as Record<string, boolean>;\n for (const result of data.results as unknown[]) {\n if (!record(result) || typeof result.permission !== 'string' || typeof result.allowed !== 'boolean'\n || !requested.includes(result.permission) || Object.hasOwn(checks, result.permission)) {\n throw new AuthenticationError('Permission decision result is invalid.');\n }\n checks[result.permission] = result.allowed;\n }\n const decision = new PermissionDecision(subject, organization, config.clientId,\n data.org_role as OrganizationRole | null, data.organization_active, Object.freeze(checks));\n if (data.allowed !== decision.allowed()) throw new AuthenticationError('Permission decision result is inconsistent.');\n return decision;\n } catch (error) {\n if (error instanceof AuthenticationError) throw error;\n throw new AuthenticationError('Permission decision could not be verified.');\n }\n}\n\nfunction record(value: unknown): value is Record<string, unknown> {\n return value !== null && typeof value === 'object' && !Array.isArray(value);\n}\n"]}
|
package/dist/server.d.cts
CHANGED
|
@@ -1,105 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
* A customer API key Cbox ID vouches for: live, bound to YOUR app, and held by an active
|
|
5
|
-
* member of `org`.
|
|
6
|
-
*
|
|
7
|
-
* The field names are the endpoint's own, so the claim helpers read it like a token —
|
|
8
|
-
* `hasPermission(answer, 'invoices:create')`, `organization(answer)?.role`. (A type
|
|
9
|
-
* alias rather than an interface for that reason: only an alias is assignable to the
|
|
10
|
-
* helpers' record type.)
|
|
11
|
-
*/
|
|
12
|
-
type ActiveApiKey = {
|
|
13
|
-
active: true;
|
|
14
|
-
/** The key's id — log this, never the key. */
|
|
15
|
-
key_id: string;
|
|
16
|
-
/** The person who holds the key (their subject id). */
|
|
17
|
-
sub: string;
|
|
18
|
-
/** The organization the key acts in. */
|
|
19
|
-
org: string;
|
|
20
|
-
/** The holder's membership tier in `org`; null for a tier this SDK does not know. */
|
|
21
|
-
org_role: OrganizationRole | null;
|
|
22
|
-
/**
|
|
23
|
-
* What the key may do, already re-capped by Cbox ID: the key's own list intersected
|
|
24
|
-
* with what the holder holds for your app right now. A demoted holder's key loses the
|
|
25
|
-
* permission on the next verification, not at the next reissue.
|
|
26
|
-
*/
|
|
27
|
-
permissions: string[];
|
|
28
|
-
/** Your app's client id — always, or the answer is refused. */
|
|
29
|
-
client_id: string;
|
|
30
|
-
/** ISO 8601 UTC expiry, or null for a key that does not expire. */
|
|
31
|
-
expires_at: string | null;
|
|
32
|
-
};
|
|
33
|
-
/**
|
|
34
|
-
* Every refusal: unknown, malformed, revoked, expired, another app's key, a holder who
|
|
35
|
-
* left. Cbox ID never says which, so the endpoint cannot be used to probe keys — and
|
|
36
|
-
* neither does this.
|
|
37
|
-
*/
|
|
38
|
-
interface InactiveApiKey {
|
|
39
|
-
active: false;
|
|
40
|
-
}
|
|
41
|
-
/** What {@link ApiKeyVerifier.verifyApiKey} returns. Narrow on `active`. */
|
|
42
|
-
type ApiKeyVerification = ActiveApiKey | InactiveApiKey;
|
|
43
|
-
/** Configuration for an {@link ApiKeyVerifier}. */
|
|
44
|
-
interface ApiKeyVerifierConfig {
|
|
45
|
-
/** Base URL (issuer) of the Cbox ID instance. */
|
|
46
|
-
issuer: string;
|
|
47
|
-
/** Your app's OAuth client id — the app the keys were issued for. */
|
|
48
|
-
clientId: string;
|
|
49
|
-
/** Your app's client secret. Verification is a confidential-client call. */
|
|
50
|
-
clientSecret: string;
|
|
51
|
-
/** Timeout (ms) for the verification request. Defaults to `10000`. */
|
|
52
|
-
timeoutMs?: number;
|
|
53
|
-
/**
|
|
54
|
-
* Cache an ACTIVE answer for this long (ms), so a burst of requests with one key costs
|
|
55
|
-
* one round-trip. Off (`0`) by default and at most {@link MAX_API_KEY_CACHE_TTL_MS}:
|
|
56
|
-
* while an answer is cached, a revoked key keeps working, and a demoted holder keeps
|
|
57
|
-
* the permissions they lost. Never cached past the key's `expires_at`. Refusals are
|
|
58
|
-
* never cached, so a key issued a moment ago works on its first request.
|
|
59
|
-
*/
|
|
60
|
-
cacheTtlMs?: number;
|
|
61
|
-
}
|
|
62
|
-
/** The longest {@link ApiKeyVerifierConfig.cacheTtlMs} allowed: one minute. */
|
|
63
|
-
declare const MAX_API_KEY_CACHE_TTL_MS = 60000;
|
|
64
|
-
/**
|
|
65
|
-
* Verifies customer API keys presented to your API: `POST {issuer}/oauth/api-keys/verify`,
|
|
66
|
-
* authenticated with your app's own client credentials.
|
|
67
|
-
*
|
|
68
|
-
* SERVER-SIDE ONLY. It holds your client secret, so it lives in the
|
|
69
|
-
* `@cboxdk/id-js/server` entry and the Next.js adapter, never in the main entry that a
|
|
70
|
-
* browser bundle pulls in — and it refuses to be constructed in a browser at all.
|
|
71
|
-
*
|
|
72
|
-
* ```ts
|
|
73
|
-
* import { ApiKeyVerifier } from '@cboxdk/id-js/server';
|
|
74
|
-
* import { hasPermission } from '@cboxdk/id-js';
|
|
75
|
-
*
|
|
76
|
-
* const keys = new ApiKeyVerifier({ issuer, clientId, clientSecret, cacheTtlMs: 10_000 });
|
|
77
|
-
*
|
|
78
|
-
* const answer = await keys.verifyApiKey(request.headers.get('x-api-key') ?? '');
|
|
79
|
-
* if (!answer.active) return unauthorized();
|
|
80
|
-
* if (!hasPermission(answer, 'invoices:create')) return forbidden();
|
|
81
|
-
* ```
|
|
82
|
-
*/
|
|
83
|
-
declare class ApiKeyVerifier {
|
|
84
|
-
private readonly config;
|
|
85
|
-
private readonly endpoint;
|
|
86
|
-
private readonly timeoutMs;
|
|
87
|
-
private readonly cacheTtlMs;
|
|
88
|
-
private readonly cache;
|
|
89
|
-
constructor(config: ApiKeyVerifierConfig);
|
|
90
|
-
/**
|
|
91
|
-
* Ask Cbox ID whether `key` is good for this app, and what it may do.
|
|
92
|
-
*
|
|
93
|
-
* Resolves `{ active: false }` for every kind of bad key, without a request for an empty
|
|
94
|
-
* one. Throws `AuthenticationError` when the call itself fails (wrong client
|
|
95
|
-
* credentials, an unreachable instance) or when the answer cannot be trusted — one that
|
|
96
|
-
* names another app's `client_id`, or does not have the promised shape. Both mean "do
|
|
97
|
-
* not let this request in"; the difference is that a throw is something to look at.
|
|
98
|
-
*/
|
|
99
|
-
verifyApiKey(key: string): Promise<ApiKeyVerification>;
|
|
100
|
-
private request;
|
|
101
|
-
private parse;
|
|
102
|
-
private remember;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
export { type ActiveApiKey, type ApiKeyVerification, ApiKeyVerifier, type ApiKeyVerifierConfig, type InactiveApiKey, MAX_API_KEY_CACHE_TTL_MS };
|
|
1
|
+
export { A as ActiveApiKey, a as ApiKeyVerification, b as ApiKeyVerifier, c as ApiKeyVerifierConfig, I as InactiveApiKey, M as MAX_API_KEY_CACHE_TTL_MS } from './api-keys-C8-Q4eFA.cjs';
|
|
2
|
+
export { P as PermissionDecision, a as PermissionDecisionConfig, c as checkPermissions } from './decisions-DNd9UXwi.cjs';
|
|
3
|
+
import './types-CC1bz68x.cjs';
|
package/dist/server.d.ts
CHANGED
|
@@ -1,105 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
* A customer API key Cbox ID vouches for: live, bound to YOUR app, and held by an active
|
|
5
|
-
* member of `org`.
|
|
6
|
-
*
|
|
7
|
-
* The field names are the endpoint's own, so the claim helpers read it like a token —
|
|
8
|
-
* `hasPermission(answer, 'invoices:create')`, `organization(answer)?.role`. (A type
|
|
9
|
-
* alias rather than an interface for that reason: only an alias is assignable to the
|
|
10
|
-
* helpers' record type.)
|
|
11
|
-
*/
|
|
12
|
-
type ActiveApiKey = {
|
|
13
|
-
active: true;
|
|
14
|
-
/** The key's id — log this, never the key. */
|
|
15
|
-
key_id: string;
|
|
16
|
-
/** The person who holds the key (their subject id). */
|
|
17
|
-
sub: string;
|
|
18
|
-
/** The organization the key acts in. */
|
|
19
|
-
org: string;
|
|
20
|
-
/** The holder's membership tier in `org`; null for a tier this SDK does not know. */
|
|
21
|
-
org_role: OrganizationRole | null;
|
|
22
|
-
/**
|
|
23
|
-
* What the key may do, already re-capped by Cbox ID: the key's own list intersected
|
|
24
|
-
* with what the holder holds for your app right now. A demoted holder's key loses the
|
|
25
|
-
* permission on the next verification, not at the next reissue.
|
|
26
|
-
*/
|
|
27
|
-
permissions: string[];
|
|
28
|
-
/** Your app's client id — always, or the answer is refused. */
|
|
29
|
-
client_id: string;
|
|
30
|
-
/** ISO 8601 UTC expiry, or null for a key that does not expire. */
|
|
31
|
-
expires_at: string | null;
|
|
32
|
-
};
|
|
33
|
-
/**
|
|
34
|
-
* Every refusal: unknown, malformed, revoked, expired, another app's key, a holder who
|
|
35
|
-
* left. Cbox ID never says which, so the endpoint cannot be used to probe keys — and
|
|
36
|
-
* neither does this.
|
|
37
|
-
*/
|
|
38
|
-
interface InactiveApiKey {
|
|
39
|
-
active: false;
|
|
40
|
-
}
|
|
41
|
-
/** What {@link ApiKeyVerifier.verifyApiKey} returns. Narrow on `active`. */
|
|
42
|
-
type ApiKeyVerification = ActiveApiKey | InactiveApiKey;
|
|
43
|
-
/** Configuration for an {@link ApiKeyVerifier}. */
|
|
44
|
-
interface ApiKeyVerifierConfig {
|
|
45
|
-
/** Base URL (issuer) of the Cbox ID instance. */
|
|
46
|
-
issuer: string;
|
|
47
|
-
/** Your app's OAuth client id — the app the keys were issued for. */
|
|
48
|
-
clientId: string;
|
|
49
|
-
/** Your app's client secret. Verification is a confidential-client call. */
|
|
50
|
-
clientSecret: string;
|
|
51
|
-
/** Timeout (ms) for the verification request. Defaults to `10000`. */
|
|
52
|
-
timeoutMs?: number;
|
|
53
|
-
/**
|
|
54
|
-
* Cache an ACTIVE answer for this long (ms), so a burst of requests with one key costs
|
|
55
|
-
* one round-trip. Off (`0`) by default and at most {@link MAX_API_KEY_CACHE_TTL_MS}:
|
|
56
|
-
* while an answer is cached, a revoked key keeps working, and a demoted holder keeps
|
|
57
|
-
* the permissions they lost. Never cached past the key's `expires_at`. Refusals are
|
|
58
|
-
* never cached, so a key issued a moment ago works on its first request.
|
|
59
|
-
*/
|
|
60
|
-
cacheTtlMs?: number;
|
|
61
|
-
}
|
|
62
|
-
/** The longest {@link ApiKeyVerifierConfig.cacheTtlMs} allowed: one minute. */
|
|
63
|
-
declare const MAX_API_KEY_CACHE_TTL_MS = 60000;
|
|
64
|
-
/**
|
|
65
|
-
* Verifies customer API keys presented to your API: `POST {issuer}/oauth/api-keys/verify`,
|
|
66
|
-
* authenticated with your app's own client credentials.
|
|
67
|
-
*
|
|
68
|
-
* SERVER-SIDE ONLY. It holds your client secret, so it lives in the
|
|
69
|
-
* `@cboxdk/id-js/server` entry and the Next.js adapter, never in the main entry that a
|
|
70
|
-
* browser bundle pulls in — and it refuses to be constructed in a browser at all.
|
|
71
|
-
*
|
|
72
|
-
* ```ts
|
|
73
|
-
* import { ApiKeyVerifier } from '@cboxdk/id-js/server';
|
|
74
|
-
* import { hasPermission } from '@cboxdk/id-js';
|
|
75
|
-
*
|
|
76
|
-
* const keys = new ApiKeyVerifier({ issuer, clientId, clientSecret, cacheTtlMs: 10_000 });
|
|
77
|
-
*
|
|
78
|
-
* const answer = await keys.verifyApiKey(request.headers.get('x-api-key') ?? '');
|
|
79
|
-
* if (!answer.active) return unauthorized();
|
|
80
|
-
* if (!hasPermission(answer, 'invoices:create')) return forbidden();
|
|
81
|
-
* ```
|
|
82
|
-
*/
|
|
83
|
-
declare class ApiKeyVerifier {
|
|
84
|
-
private readonly config;
|
|
85
|
-
private readonly endpoint;
|
|
86
|
-
private readonly timeoutMs;
|
|
87
|
-
private readonly cacheTtlMs;
|
|
88
|
-
private readonly cache;
|
|
89
|
-
constructor(config: ApiKeyVerifierConfig);
|
|
90
|
-
/**
|
|
91
|
-
* Ask Cbox ID whether `key` is good for this app, and what it may do.
|
|
92
|
-
*
|
|
93
|
-
* Resolves `{ active: false }` for every kind of bad key, without a request for an empty
|
|
94
|
-
* one. Throws `AuthenticationError` when the call itself fails (wrong client
|
|
95
|
-
* credentials, an unreachable instance) or when the answer cannot be trusted — one that
|
|
96
|
-
* names another app's `client_id`, or does not have the promised shape. Both mean "do
|
|
97
|
-
* not let this request in"; the difference is that a throw is something to look at.
|
|
98
|
-
*/
|
|
99
|
-
verifyApiKey(key: string): Promise<ApiKeyVerification>;
|
|
100
|
-
private request;
|
|
101
|
-
private parse;
|
|
102
|
-
private remember;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
export { type ActiveApiKey, type ApiKeyVerification, ApiKeyVerifier, type ApiKeyVerifierConfig, type InactiveApiKey, MAX_API_KEY_CACHE_TTL_MS };
|
|
1
|
+
export { A as ActiveApiKey, a as ApiKeyVerification, b as ApiKeyVerifier, c as ApiKeyVerifierConfig, I as InactiveApiKey, M as MAX_API_KEY_CACHE_TTL_MS } from './api-keys-BE3R3q_A.js';
|
|
2
|
+
export { P as PermissionDecision, a as PermissionDecisionConfig, c as checkPermissions } from './decisions-C84Fzah4.js';
|
|
3
|
+
import './types-CC1bz68x.js';
|
package/dist/server.js
CHANGED