@cauthyhq/node 1.1.0 → 1.2.1
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 +37 -0
- package/README.md +79 -24
- package/dist/{cauthy-Wy-YDGrM.d.ts → cauthy-C7gZ-pbv.d.ts} +64 -2
- package/dist/{cauthy-Bn-i7cKD.d.mts → cauthy-CCSSj3Lg.d.mts} +64 -2
- package/dist/express.d.mts +2 -1
- package/dist/express.d.ts +2 -1
- package/dist/fastify.d.mts +2 -1
- package/dist/fastify.d.ts +2 -1
- package/dist/hono.d.mts +2 -1
- package/dist/hono.d.ts +2 -1
- package/dist/identity-BTJ8NB_w.d.mts +27 -0
- package/dist/identity-BTJ8NB_w.d.ts +27 -0
- package/dist/index.d.mts +21 -2
- package/dist/index.d.ts +21 -2
- package/dist/index.js +688 -67
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +683 -67
- package/dist/index.mjs.map +1 -1
- package/dist/keys.js.map +1 -1
- package/dist/keys.mjs.map +1 -1
- package/dist/mcp.js.map +1 -1
- package/dist/mcp.mjs.map +1 -1
- package/dist/next.d.mts +2 -1
- package/dist/next.d.ts +2 -1
- package/dist/testing.d.mts +36 -0
- package/dist/testing.d.ts +36 -0
- package/dist/testing.js +395 -0
- package/dist/testing.js.map +1 -1
- package/dist/testing.mjs +395 -0
- package/dist/testing.mjs.map +1 -1
- package/package.json +3 -1
package/dist/keys.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/keystores.ts","../src/keys.ts","../src/types.ts"],"sourcesContent":["// @cauthyhq/node/keys: where a contracted application keeps its own key.\n//\n// import { postgresKeyStore } from '@cauthyhq/node/keys'\n// keys: postgresKeyStore((text, params) => pool.query(text, params))\n//\n// The file store is for a developer's machine: production keeps its key in\n// its database, where every instance reads the same one.\n\nimport { mkdir, readFile, rename, writeFile } from 'node:fs/promises'\nimport { dirname } from 'node:path'\nimport type { KeyStore, StoredKey } from './keys'\n\nexport { memoryKeyStore, postgresKeyStore, pactKeysTableSql, type KeyStore, type PactKeys, type Query, type StoredKey } from './keys'\n\n/** Keys kept in a JSON file, readable only by its owner. For development. */\nexport function fileKeyStore(path: string): KeyStore {\n async function read(): Promise<StoredKey[]> {\n try {\n const parsed = JSON.parse(await readFile(path, 'utf8')) as unknown\n return Array.isArray(parsed) ? (parsed as StoredKey[]) : []\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === 'ENOENT') return []\n throw err\n }\n }\n // Written whole to a file beside it, then moved over it: a crash never\n // leaves half a file.\n let writing: Promise<void> = Promise.resolve()\n function write(change: (keys: StoredKey[]) => StoredKey[]): Promise<void> {\n writing = writing.then(async () => {\n const next = change(await read())\n await mkdir(dirname(path), { recursive: true })\n const tmp = `${path}.${process.pid}.tmp`\n await writeFile(tmp, JSON.stringify(next, null, 2) + '\\n', { mode: 0o600 })\n await rename(tmp, path)\n })\n return writing\n }\n return {\n list: read,\n add: k => write(keys => (keys.some(x => x.kid === k.kid) ? keys : [...keys, k])),\n addFirst: k => write(keys => (keys.length > 0 ? keys : [k])),\n update: k => write(keys => keys.map(x => (x.kid === k.kid ? k : x))),\n remove: kid => write(keys => keys.filter(x => x.kid !== kid)),\n }\n}\n","// The application's own key: made by the application, kept in its own\n// database, its public half published, its private half sealed at rest.\n//\n// A contracted application proves who it is to Cauthy with an assertion it\n// signs (private_key_jwt). Nobody hands it that key: the first time it needs\n// one it makes it, seals the private half with the key derived from its root\n// key (pact/seal/v1) and keeps it in a KeyStore — a table of its database, a\n// file while developing. Its public half is served at the jwks_url its\n// manifest names, where Cauthy reads it.\n//\n// Rotating: `rotate()` makes a new key, published at once. It signs once it\n// has been published longer than anyone caches the keys (SIGN_AFTER_MS):\n// Cauthy, meeting a key it does not know, reads the keys again, and a copy\n// cached on the way must already hold it. The one before it stays published\n// 24 hours, so an assertion signed a moment ago, or by an instance that has\n// not seen the new key yet, still verifies.\n//\n// The first key is made by whichever instance needs it first: the store keeps\n// it only while it holds none (addFirst), so instances starting together end\n// with one key. A store without addFirst may end with several; every one is\n// published, and every instance settles on the same one within seconds.\n\nimport { calculateJwkThumbprint, exportJWK, generateKeyPair, importJWK, type JWK, type KeyLike } from 'jose'\nimport { seal, unseal } from './seal'\nimport type { KeyRing } from './rootkey'\nimport { CauthyConfigError } from './types'\n\n/** A key as a KeyStore keeps it. Nothing in it is secret but sealed. */\nexport interface StoredKey {\n /** Its id, as published (the JWK thumbprint). */\n kid: string\n /** Its public half, as published. */\n publicJwk: JWK\n /** Its private half, sealed with the key derived from the root key. */\n sealed: string\n /** When it was made (ISO 8601, UTC). The newest signs. */\n createdAt: string\n}\n\n/** Where the application keeps its keys: a table, a file. */\nexport interface KeyStore {\n /** Every key kept. */\n list(): Promise<StoredKey[]>\n /** Keeps a new key. A kid already kept is left as it is. */\n add(key: StoredKey): Promise<void>\n /** Keeps the key only while the store holds none, in one step: instances starting together make one key. */\n addFirst?(key: StoredKey): Promise<void>\n /** Replaces a key's sealed private half (after the root key was rotated). */\n update(key: StoredKey): Promise<void>\n /** Forgets a key no longer published. */\n remove(kid: string): Promise<void>\n}\n\n/** How long a key stays published after a newer one appears. */\nexport const RETIRE_AFTER_MS = 24 * 60 * 60 * 1000\n/** How long a key that is no longer published is kept before it is forgotten. */\nconst FORGET_AFTER_MS = 7 * 24 * 60 * 60 * 1000\n/** How long the key that signs is remembered before the store is read again. */\nconst CURRENT_TTL_MS = 5 * 60 * 1000\n/** How long the published keys may be cached (the jwks_url's max-age). */\nexport const JWKS_MAX_AGE_S = 300\n/** How long a new key is published before it signs: longer than any cached copy of the keys. */\nexport const SIGN_AFTER_MS = (JWKS_MAX_AGE_S + 60) * 1000\n/** While every key is younger than that, the store is read again this often, so instances settle on one. */\nconst SETTLING_TTL_MS = 5 * 1000\n\n/** The application's keys, as auth.pact.keys gives them. */\nexport interface PactKeys {\n /** The key that signs now, made the first time there is none: the newest that has been published long enough. */\n current(): Promise<{ kid: string; privateKey: KeyLike | Uint8Array }>\n /** The public keys to publish: the one that signs, and the one before it for 24 hours after. */\n published(): Promise<{ keys: JWK[] }>\n /** Makes a new key, published at once and signing once SIGN_AFTER_MS has passed. Answers its id. */\n rotate(): Promise<string>\n /** Seals every key again with the current root key; answers how many changed. */\n reseal(): Promise<number>\n}\n\nconst newestFirst = (keys: StoredKey[]) => [...keys].sort((a, b) => b.createdAt.localeCompare(a.createdAt) || b.kid.localeCompare(a.kid))\n\nfunction published(key: StoredKey): JWK {\n const { kty, n, e } = key.publicJwk\n return { kty, n, e, kid: key.kid, alg: 'RS256', use: 'sig' }\n}\n\nexport function pactKeys(store: KeyStore, sealKeys: Promise<KeyRing>, now: () => number = Date.now): PactKeys {\n let remembered: { kid: string; privateKey: KeyLike | Uint8Array; until: number } | undefined\n let making: Promise<StoredKey> | undefined\n\n async function make(after?: StoredKey): Promise<StoredKey> {\n const { publicKey, privateKey } = await generateKeyPair('RS256', { modulusLength: 2048, extractable: true })\n const publicJwk = await exportJWK(publicKey)\n const kid = await calculateJwkThumbprint(publicJwk)\n // Always newer than the key it follows, however fast the rotation.\n const at = Math.max(now(), after ? Date.parse(after.createdAt) + 1 : 0)\n const key: StoredKey = { kid, publicJwk, sealed: await seal(await exportJWK(privateKey), (await sealKeys)[0]), createdAt: new Date(at).toISOString() }\n if (!after && store.addFirst) await store.addFirst(key)\n else await store.add(key)\n return key\n }\n\n const age = (key: StoredKey) => now() - Date.parse(key.createdAt)\n\n // The key that signs: the newest published long enough. When none is (the\n // first minutes of the first key, or a race of first keys), the oldest, read\n // again shortly so every instance lands on the same one.\n function signer(kept: StoredKey[]): { key: StoredKey; until: number } {\n const ripe = kept.find(k => age(k) >= SIGN_AFTER_MS)\n if (!ripe) return { key: kept[kept.length - 1], until: now() + SETTLING_TTL_MS }\n // A newer key ripens later: read again then, not after the usual while.\n const until = ripe === kept[0] ? now() + CURRENT_TTL_MS : Math.min(now() + CURRENT_TTL_MS, Date.parse(kept[0].createdAt) + SIGN_AFTER_MS)\n return { key: ripe, until }\n }\n\n async function open(key: StoredKey): Promise<{ jwk: JWK; fresh: boolean } | null> {\n const ring = await sealKeys\n for (let i = 0; i < ring.length; i++) {\n const jwk = await unseal<JWK>(key.sealed, ring[i])\n if (jwk !== null) return { jwk, fresh: i === 0 }\n }\n return null\n }\n\n // Every key kept, newest first; the first one is made when there is none.\n async function keptKeys(): Promise<StoredKey[]> {\n const kept = newestFirst(await store.list())\n if (kept.length > 0) return kept\n // Two callers at once make one key; two instances at once, one with\n // addFirst, and otherwise several that are all published.\n making ??= make().finally(() => { making = undefined })\n await making\n const again = newestFirst(await store.list())\n if (again.length === 0) throw new CauthyConfigError('the key store kept no key: check that it writes')\n return again\n }\n\n return {\n async current() {\n if (remembered && remembered.until > now()) return remembered\n const { key, until } = signer(await keptKeys())\n if (remembered?.kid === key.kid) {\n remembered.until = until\n return remembered\n }\n const opened = await open(key)\n if (opened === null) {\n throw new CauthyConfigError(`the key ${key.kid} cannot be opened with the root key (PACT_ROOT_KEY): ` +\n 'if the root key was rotated, set the previous one as PACT_ROOT_KEY_PREVIOUS')\n }\n remembered = { kid: key.kid, privateKey: await importJWK(opened.jwk, 'RS256'), until }\n return remembered\n },\n async published() {\n // Asked before anything was signed: the key is made now, so what is\n // published is the key that will sign.\n const kept = await keptKeys()\n const out: JWK[] = []\n for (let i = 0; i < kept.length; i++) {\n // The newest is published; an older one while its successor is young.\n if (i === 0 || now() - Date.parse(kept[i - 1].createdAt) < RETIRE_AFTER_MS) out.push(published(kept[i]))\n else if (now() - Date.parse(kept[i - 1].createdAt) > FORGET_AFTER_MS) await store.remove(kept[i].kid)\n }\n return { keys: out }\n },\n async rotate() {\n const key = await make(newestFirst(await store.list())[0])\n remembered = undefined\n return key.kid\n },\n async reseal() {\n let changed = 0\n const ring = await sealKeys\n for (const key of await store.list()) {\n const opened = await open(key)\n if (opened === null || opened.fresh) continue\n await store.update({ ...key, sealed: await seal(opened.jwk, ring[0]) })\n changed++\n }\n return changed\n },\n }\n}\n\n/** Keys kept in memory: for tests, and for trying contract mode out. Gone on restart. */\nexport function memoryKeyStore(): KeyStore {\n const keys = new Map<string, StoredKey>()\n return {\n async list() { return [...keys.values()] },\n async add(k) { if (!keys.has(k.kid)) keys.set(k.kid, k) },\n async addFirst(k) { if (keys.size === 0) keys.set(k.kid, k) },\n async update(k) { keys.set(k.kid, k) },\n async remove(kid) { keys.delete(kid) },\n }\n}\n\n/**\n * The table postgresKeyStore keeps the keys in, for the application's own\n * migrations (Drizzle, Prisma, plain SQL). The store never creates it.\n */\nexport function pactKeysTableSql(table = 'cauthy_pact_keys'): string {\n checkTable(table)\n return `CREATE TABLE IF NOT EXISTS ${table} (\n kid TEXT PRIMARY KEY,\n public_jwk JSONB NOT NULL,\n sealed TEXT NOT NULL,\n created_at TIMESTAMPTZ NOT NULL\n);`\n}\n\nfunction checkTable(table: string): void {\n if (!/^[a-z_][a-z0-9_]{0,62}$/.test(table)) throw new CauthyConfigError(`the key table's name must be a plain SQL identifier: ${table}`)\n}\n\n/**\n * Runs one SQL statement with $1, $2… parameters and answers its rows: what\n * the application's own database client does. With node-postgres,\n * `(text, params) => pool.query(text, params)`; with postgres.js,\n * `(text, params) => sql.unsafe(text, params)`.\n */\nexport type Query = (text: string, params: unknown[]) => Promise<{ rows: unknown[] } | unknown[]>\n\n/** Keys kept in a table of the application's Postgres database (see pactKeysTableSql). */\nexport function postgresKeyStore(query: Query, options: { table?: string } = {}): KeyStore {\n const table = options.table ?? 'cauthy_pact_keys'\n checkTable(table)\n const rows = async (text: string, params: unknown[]) => {\n const out = await query(text, params)\n return (Array.isArray(out) ? out : out.rows) as Record<string, unknown>[]\n }\n const at = (v: unknown) => (v instanceof Date ? v.toISOString() : new Date(String(v)).toISOString())\n return {\n async list() {\n return (await rows(`SELECT kid, public_jwk, sealed, created_at FROM ${table}`, [])).map(r => ({\n kid: String(r.kid),\n publicJwk: (typeof r.public_jwk === 'string' ? JSON.parse(r.public_jwk) : r.public_jwk) as JWK,\n sealed: String(r.sealed),\n createdAt: at(r.created_at),\n }))\n },\n async add(k) {\n await rows(`INSERT INTO ${table} (kid, public_jwk, sealed, created_at) VALUES ($1, $2, $3, $4) ON CONFLICT (kid) DO NOTHING`,\n [k.kid, JSON.stringify(k.publicJwk), k.sealed, k.createdAt])\n },\n async addFirst(k) {\n // One statement: a key another instance committed a moment ago is seen.\n await rows(`INSERT INTO ${table} (kid, public_jwk, sealed, created_at) SELECT $1, $2, $3, $4 WHERE NOT EXISTS (SELECT 1 FROM ${table}) ON CONFLICT (kid) DO NOTHING`,\n [k.kid, JSON.stringify(k.publicJwk), k.sealed, k.createdAt])\n },\n async update(k) {\n await rows(`UPDATE ${table} SET sealed = $2 WHERE kid = $1`, [k.kid, k.sealed])\n },\n async remove(kid) {\n await rows(`DELETE FROM ${table} WHERE kid = $1`, [kid])\n },\n }\n}\n","/** How an application is set up to sign its people in with Cauthy. */\nexport interface CauthyOptions {\n /** Cauthy's public address, e.g. `https://id.example.com`. Default: `CAUTHY_ISSUER`. */\n issuer?: string\n /** The application's OAuth client id. Default: `CAUTHY_CLIENT_ID`. */\n clientId?: string\n /** Its secret, when the client is confidential. Default: `CAUTHY_CLIENT_SECRET`. */\n clientSecret?: string\n /**\n * The key the session cookie is encrypted with: any string of 32 characters\n * or more, kept on the server. Default: `CAUTHY_SECRET`.\n */\n secret?: string\n /**\n * The system this application is, as its organizations are filed under in\n * Cauthy (`\"acme-crm\"`). When set, a session's organizations are that\n * system's. Default: `CAUTHY_SYSTEM`.\n */\n system?: string\n /**\n * The workspace's slug in Cauthy. Needed for the application's own sign-in\n * screens (`signIn.email`, `signUp.email`…), which sign people in through\n * the application's server instead of Cauthy's page. Default: `CAUTHY_TENANT`.\n */\n tenant?: string\n /** Where the routes are mounted. Default: `/api/auth`. */\n basePath?: string\n /**\n * What happens to a POST without `X-Cauthy-Request: 1` (every POST but\n * `/logout`). `required`, the default, answers `403 csrf_required`, as\n * protocol level strict asks. `warn` lets it through and says so once per\n * process: for the one deploy that moves an application whose browser still\n * runs cauthy-auth 0.42, which does not send the header. Remove it once the\n * browser runs cauthy-auth 0.43 or @cauthyhq/js.\n */\n requestHeader?: 'required' | 'warn'\n /**\n * The audience of the access tokens `auth.verifyToken` accepts: the\n * deployment's `JWT_AUDIENCE`, which Cauthy requires its operator to set and\n * has no default for. Default: `CAUTHY_AUDIENCE`.\n */\n audience?: string\n /**\n * The workspace's id (a lowercase UUID) for `auth.verifyToken`. By contract\n * it comes from the link's `tenant_id`; otherwise: this, or `CAUTHY_TENANT_ID`.\n */\n tenantId?: string\n /**\n * The application's own public origin (`https://app.example.com`): the\n * redirect URI, the cookies' `Secure` flag and which forms are its own come\n * from it. Default: `CAUTHY_BASE_URL`, else the origin of each request, with\n * the scheme a proxy names in `x-forwarded-proto`. Set it whenever a proxy\n * also changes the host or the port.\n */\n baseURL?: string\n /** Where a person lands after signing in, when no `return_to` was asked. Default: `/`. */\n afterSignIn?: string\n /** Where a person lands after signing out. Default: `/`. */\n afterSignOut?: string\n /** Name of the session cookie. Default: `cauthy_session`. */\n cookieName?: string\n /**\n * A key the application proves who it is with when it acts on its own\n * (`auth.system`), instead of its secret. Default: `CAUTHY_SYSTEM_KEY` and\n * `CAUTHY_SYSTEM_KEY_ID`.\n */\n systemKey?: import('./system').SystemKey\n /**\n * The application's own pages for what arrives by mail. A link Cauthy mails\n * opens the page named here, with a `token` to give back; with none, it\n * opens Cauthy's own page.\n *\n * signIn where `<SignIn />` is: a sign-in link opens it\n * resetPassword where `<ResetPassword />` is\n * verifyEmail where `<VerifyEmail />` is\n * acceptInvite where `<AcceptInvite />` is: an invitation opens it\n *\n * An invitation sent by `auth.system.members.add` or `auth.system.invite`\n * opens `acceptInvite` only when the application knows its public origin\n * (`baseURL`): that call has no request to read it from.\n */\n pages?: { signIn?: string; resetPassword?: string; verifyEmail?: string; acceptInvite?: string }\n /**\n * The address of the person a request comes from, passed on to Cauthy so\n * its limits on attempts count the person and not this server. Default: the\n * last address in `X-Forwarded-For`, which is the one the proxy in front of\n * the application wrote. With several proxies, or none, give your own.\n */\n clientIp?: (request: Request) => string | undefined\n /** `fetch` to reach Cauthy with. Default: the global one. */\n fetch?: typeof fetch\n /**\n * How long a call to Cauthy may take before it fails, in milliseconds.\n * `system`: the application's own calls (`auth.system`, and the proof its\n * sign-ins carry). Default 2000. `request`: the sign-in screens' calls and\n * the code, refresh and userinfo calls of a session. Default 5000. The\n * push-approval event stream has no deadline.\n */\n timeouts?: { system?: number; request?: number }\n /**\n * Run by contract: the issuer, the workspace, the clients and where the\n * application's keys are published all come from its link in the network's\n * context graph, and none of the CAUTHY_* variables may be set. See\n * \"Connect a system by contract\".\n *\n * createCauthy({ contract: { graph, system: 'acme-crm' }, keys: postgresKeyStore(query) })\n */\n contract?: import('./contract').ContractOptions\n /** Where the application keeps its own key, with a contract (@cauthyhq/node/keys). */\n keys?: import('./keys').KeyStore\n /**\n * The one secret of a contracted application: every key it needs is\n * derived from it. 32 random bytes or more. Default: `PACT_ROOT_KEY`.\n */\n rootKey?: string\n /** The root key before the current one, while rotating it. Default: `PACT_ROOT_KEY_PREVIOUS`. */\n rootKeyPrevious?: string\n /**\n * Where `auth.webhook` remembers the deliveries it took, so one sent twice\n * is refused. Default: this process's memory. Give a shared one (a table)\n * when the application runs on several instances.\n */\n webhookReplay?: import('./webhook').ReplayStore\n}\n\n/** The person signed in. Every moment is UTC. */\nexport interface SessionUser {\n id: string\n email: string\n name: string\n emailVerified: boolean\n /**\n * True when the account ran out of time to confirm its address: let it read\n * and refuse its writes.\n */\n restricted: boolean\n /** When an unconfirmed address stops being enough (ISO 8601, UTC), if there is a deadline. */\n emailVerifyBy?: string\n /** The language the person reads in (\"es\", \"en-US\"), when Cauthy knows it. */\n locale?: string\n /** A guest: a visitor's session before they sign up. No email; `account.upgrade` makes it an account, keeping `id`. */\n isAnonymous?: boolean\n}\n\n/** A value JSON can carry. What crosses from server to browser is typed as this, never `unknown`. */\nexport type JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue }\n\n/** An organization the person belongs to, with their role in it. */\nexport interface SessionOrganization {\n id: string\n system: string\n name: string\n kind: string\n externalId?: string\n role: string\n /** What the system keeps about the organization, as it filed it in Cauthy. */\n metadata: Record<string, JsonValue>\n /**\n * What the person may do here, as \"resource:action\" entries (\"*\" is any):\n * their role's permissions, what it inherits, and anything given to them\n * apart from it. Absent when the system keeps no catalog of roles in Cauthy.\n * Check with `can(organization, resource, action)`.\n */\n permissions?: string[]\n /** What the person is denied here, whatever else allows it. */\n denied?: string[]\n /**\n * Permissions that hold only when their condition does (an amount under a\n * limit, the person's own records…). They are in neither list above: `can`\n * answers no for them, and `auth.check` asks Cauthy. Present, maybe empty,\n * whenever `permissions` is.\n */\n conditional?: ConditionalPermission[]\n}\n\n/** A permission with a condition: Cauthy decides it per question (`auth.check`). */\nexport interface ConditionalPermission {\n resource: string\n action: string\n effect: 'allow' | 'deny'\n}\n\n/** What to ask Cauthy about: an action on a resource, with what the conditions read of it. */\nexport interface CheckQuestion {\n resource: string\n action: string\n /** What a condition reads as `resource.attrs`: `{ amount: 500 }`. */\n attributes?: Record<string, unknown>\n /** The record's id, as a condition reads it (`resource.id`). */\n resourceId?: string\n /** Whose the record is (`resource.owner_id`): `resource.owner_id == subject.id`. */\n ownerId?: string\n}\n\n/** An organization, by its id or as the session has it. */\nexport type OrganizationRef = string | { id: string }\n\n/** The rule that decided a check. */\nexport interface CheckRule {\n /** A permission of the person's role, or an exception given to the person. */\n source: 'role' | 'exception'\n /** The role, when `source` is `role`. */\n role?: string\n kind: string\n resource: string\n action: string\n effect: 'allow' | 'deny'\n condition?: string\n}\n\n/** Cauthy's answer to a check. */\nexport interface CheckResult {\n allowed: boolean\n /** Why, in words: \"allowed by role accountant: invoices:approve\". */\n reason: string\n /** The rule that decided; absent when none did (nothing allows it). */\n rule?: CheckRule\n}\n\n/** A signed-in person, as the server sees them. */\nexport interface Session {\n user: SessionUser\n /** The Cauthy tenant the account is of. */\n tenantId: string\n /** The roles the account holds in that tenant. */\n roles: string[]\n /**\n * Cauthy's access token for this person: send it as `Authorization: Bearer`\n * to another service that verifies Cauthy's tokens. Never give it to the browser.\n */\n accessToken: string\n /** When the access token expires and the session is checked with Cauthy again (ISO 8601, UTC). */\n expiresAt: string\n}\n\n/** What the browser is told of a session: who, and their organizations — never a token. */\nexport interface PublicSession {\n user: SessionUser\n tenantId: string\n roles: string[]\n organizations: SessionOrganization[]\n expiresAt: string\n}\n\n/** Why there is no session. */\nexport type SessionStatus =\n | 'authenticated'\n | 'unauthenticated' // nobody is signed in\n | 'expired' // signed in, but the session must be renewed first (see `Cauthy.resolve`)\n\nexport class CauthyConfigError extends Error {\n constructor(message: string) {\n super(`@cauthyhq/node: ${message}`)\n this.name = 'CauthyConfigError'\n }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/keystores.ts","../src/keys.ts","../src/types.ts"],"sourcesContent":["// @cauthyhq/node/keys: where a contracted application keeps its own key.\n//\n// import { postgresKeyStore } from '@cauthyhq/node/keys'\n// keys: postgresKeyStore((text, params) => pool.query(text, params))\n//\n// The file store is for a developer's machine: production keeps its key in\n// its database, where every instance reads the same one.\n\nimport { mkdir, readFile, rename, writeFile } from 'node:fs/promises'\nimport { dirname } from 'node:path'\nimport type { KeyStore, StoredKey } from './keys'\n\nexport { memoryKeyStore, postgresKeyStore, pactKeysTableSql, type KeyStore, type PactKeys, type Query, type StoredKey } from './keys'\n\n/** Keys kept in a JSON file, readable only by its owner. For development. */\nexport function fileKeyStore(path: string): KeyStore {\n async function read(): Promise<StoredKey[]> {\n try {\n const parsed = JSON.parse(await readFile(path, 'utf8')) as unknown\n return Array.isArray(parsed) ? (parsed as StoredKey[]) : []\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === 'ENOENT') return []\n throw err\n }\n }\n // Written whole to a file beside it, then moved over it: a crash never\n // leaves half a file.\n let writing: Promise<void> = Promise.resolve()\n function write(change: (keys: StoredKey[]) => StoredKey[]): Promise<void> {\n writing = writing.then(async () => {\n const next = change(await read())\n await mkdir(dirname(path), { recursive: true })\n const tmp = `${path}.${process.pid}.tmp`\n await writeFile(tmp, JSON.stringify(next, null, 2) + '\\n', { mode: 0o600 })\n await rename(tmp, path)\n })\n return writing\n }\n return {\n list: read,\n add: k => write(keys => (keys.some(x => x.kid === k.kid) ? keys : [...keys, k])),\n addFirst: k => write(keys => (keys.length > 0 ? keys : [k])),\n update: k => write(keys => keys.map(x => (x.kid === k.kid ? k : x))),\n remove: kid => write(keys => keys.filter(x => x.kid !== kid)),\n }\n}\n","// The application's own key: made by the application, kept in its own\n// database, its public half published, its private half sealed at rest.\n//\n// A contracted application proves who it is to Cauthy with an assertion it\n// signs (private_key_jwt). Nobody hands it that key: the first time it needs\n// one it makes it, seals the private half with the key derived from its root\n// key (pact/seal/v1) and keeps it in a KeyStore — a table of its database, a\n// file while developing. Its public half is served at the jwks_url its\n// manifest names, where Cauthy reads it.\n//\n// Rotating: `rotate()` makes a new key, published at once. It signs once it\n// has been published longer than anyone caches the keys (SIGN_AFTER_MS):\n// Cauthy, meeting a key it does not know, reads the keys again, and a copy\n// cached on the way must already hold it. The one before it stays published\n// 24 hours, so an assertion signed a moment ago, or by an instance that has\n// not seen the new key yet, still verifies.\n//\n// The first key is made by whichever instance needs it first: the store keeps\n// it only while it holds none (addFirst), so instances starting together end\n// with one key. A store without addFirst may end with several; every one is\n// published, and every instance settles on the same one within seconds.\n\nimport { calculateJwkThumbprint, exportJWK, generateKeyPair, importJWK, type JWK, type KeyLike } from 'jose'\nimport { seal, unseal } from './seal'\nimport type { KeyRing } from './rootkey'\nimport { CauthyConfigError } from './types'\n\n/** A key as a KeyStore keeps it. Nothing in it is secret but sealed. */\nexport interface StoredKey {\n /** Its id, as published (the JWK thumbprint). */\n kid: string\n /** Its public half, as published. */\n publicJwk: JWK\n /** Its private half, sealed with the key derived from the root key. */\n sealed: string\n /** When it was made (ISO 8601, UTC). The newest signs. */\n createdAt: string\n}\n\n/** Where the application keeps its keys: a table, a file. */\nexport interface KeyStore {\n /** Every key kept. */\n list(): Promise<StoredKey[]>\n /** Keeps a new key. A kid already kept is left as it is. */\n add(key: StoredKey): Promise<void>\n /** Keeps the key only while the store holds none, in one step: instances starting together make one key. */\n addFirst?(key: StoredKey): Promise<void>\n /** Replaces a key's sealed private half (after the root key was rotated). */\n update(key: StoredKey): Promise<void>\n /** Forgets a key no longer published. */\n remove(kid: string): Promise<void>\n}\n\n/** How long a key stays published after a newer one appears. */\nexport const RETIRE_AFTER_MS = 24 * 60 * 60 * 1000\n/** How long a key that is no longer published is kept before it is forgotten. */\nconst FORGET_AFTER_MS = 7 * 24 * 60 * 60 * 1000\n/** How long the key that signs is remembered before the store is read again. */\nconst CURRENT_TTL_MS = 5 * 60 * 1000\n/** How long the published keys may be cached (the jwks_url's max-age). */\nexport const JWKS_MAX_AGE_S = 300\n/** How long a new key is published before it signs: longer than any cached copy of the keys. */\nexport const SIGN_AFTER_MS = (JWKS_MAX_AGE_S + 60) * 1000\n/** While every key is younger than that, the store is read again this often, so instances settle on one. */\nconst SETTLING_TTL_MS = 5 * 1000\n\n/** The application's keys, as auth.pact.keys gives them. */\nexport interface PactKeys {\n /** The key that signs now, made the first time there is none: the newest that has been published long enough. */\n current(): Promise<{ kid: string; privateKey: KeyLike | Uint8Array }>\n /** The public keys to publish: the one that signs, and the one before it for 24 hours after. */\n published(): Promise<{ keys: JWK[] }>\n /** Makes a new key, published at once and signing once SIGN_AFTER_MS has passed. Answers its id. */\n rotate(): Promise<string>\n /** Seals every key again with the current root key; answers how many changed. */\n reseal(): Promise<number>\n}\n\nconst newestFirst = (keys: StoredKey[]) => [...keys].sort((a, b) => b.createdAt.localeCompare(a.createdAt) || b.kid.localeCompare(a.kid))\n\nfunction published(key: StoredKey): JWK {\n const { kty, n, e } = key.publicJwk\n return { kty, n, e, kid: key.kid, alg: 'RS256', use: 'sig' }\n}\n\nexport function pactKeys(store: KeyStore, sealKeys: Promise<KeyRing>, now: () => number = Date.now): PactKeys {\n let remembered: { kid: string; privateKey: KeyLike | Uint8Array; until: number } | undefined\n let making: Promise<StoredKey> | undefined\n\n async function make(after?: StoredKey): Promise<StoredKey> {\n const { publicKey, privateKey } = await generateKeyPair('RS256', { modulusLength: 2048, extractable: true })\n const publicJwk = await exportJWK(publicKey)\n const kid = await calculateJwkThumbprint(publicJwk)\n // Always newer than the key it follows, however fast the rotation.\n const at = Math.max(now(), after ? Date.parse(after.createdAt) + 1 : 0)\n const key: StoredKey = { kid, publicJwk, sealed: await seal(await exportJWK(privateKey), (await sealKeys)[0]), createdAt: new Date(at).toISOString() }\n if (!after && store.addFirst) await store.addFirst(key)\n else await store.add(key)\n return key\n }\n\n const age = (key: StoredKey) => now() - Date.parse(key.createdAt)\n\n // The key that signs: the newest published long enough. When none is (the\n // first minutes of the first key, or a race of first keys), the oldest, read\n // again shortly so every instance lands on the same one.\n function signer(kept: StoredKey[]): { key: StoredKey; until: number } {\n const ripe = kept.find(k => age(k) >= SIGN_AFTER_MS)\n if (!ripe) return { key: kept[kept.length - 1], until: now() + SETTLING_TTL_MS }\n // A newer key ripens later: read again then, not after the usual while.\n const until = ripe === kept[0] ? now() + CURRENT_TTL_MS : Math.min(now() + CURRENT_TTL_MS, Date.parse(kept[0].createdAt) + SIGN_AFTER_MS)\n return { key: ripe, until }\n }\n\n async function open(key: StoredKey): Promise<{ jwk: JWK; fresh: boolean } | null> {\n const ring = await sealKeys\n for (let i = 0; i < ring.length; i++) {\n const jwk = await unseal<JWK>(key.sealed, ring[i])\n if (jwk !== null) return { jwk, fresh: i === 0 }\n }\n return null\n }\n\n // Every key kept, newest first; the first one is made when there is none.\n async function keptKeys(): Promise<StoredKey[]> {\n const kept = newestFirst(await store.list())\n if (kept.length > 0) return kept\n // Two callers at once make one key; two instances at once, one with\n // addFirst, and otherwise several that are all published.\n making ??= make().finally(() => { making = undefined })\n await making\n const again = newestFirst(await store.list())\n if (again.length === 0) throw new CauthyConfigError('the key store kept no key: check that it writes')\n return again\n }\n\n return {\n async current() {\n if (remembered && remembered.until > now()) return remembered\n const { key, until } = signer(await keptKeys())\n if (remembered?.kid === key.kid) {\n remembered.until = until\n return remembered\n }\n const opened = await open(key)\n if (opened === null) {\n throw new CauthyConfigError(`the key ${key.kid} cannot be opened with the root key (PACT_ROOT_KEY): ` +\n 'if the root key was rotated, set the previous one as PACT_ROOT_KEY_PREVIOUS')\n }\n remembered = { kid: key.kid, privateKey: await importJWK(opened.jwk, 'RS256'), until }\n return remembered\n },\n async published() {\n // Asked before anything was signed: the key is made now, so what is\n // published is the key that will sign.\n const kept = await keptKeys()\n const out: JWK[] = []\n for (let i = 0; i < kept.length; i++) {\n // The newest is published; an older one while its successor is young.\n if (i === 0 || now() - Date.parse(kept[i - 1].createdAt) < RETIRE_AFTER_MS) out.push(published(kept[i]))\n else if (now() - Date.parse(kept[i - 1].createdAt) > FORGET_AFTER_MS) await store.remove(kept[i].kid)\n }\n return { keys: out }\n },\n async rotate() {\n const key = await make(newestFirst(await store.list())[0])\n remembered = undefined\n return key.kid\n },\n async reseal() {\n let changed = 0\n const ring = await sealKeys\n for (const key of await store.list()) {\n const opened = await open(key)\n if (opened === null || opened.fresh) continue\n await store.update({ ...key, sealed: await seal(opened.jwk, ring[0]) })\n changed++\n }\n return changed\n },\n }\n}\n\n/** Keys kept in memory: for tests, and for trying contract mode out. Gone on restart. */\nexport function memoryKeyStore(): KeyStore {\n const keys = new Map<string, StoredKey>()\n return {\n async list() { return [...keys.values()] },\n async add(k) { if (!keys.has(k.kid)) keys.set(k.kid, k) },\n async addFirst(k) { if (keys.size === 0) keys.set(k.kid, k) },\n async update(k) { keys.set(k.kid, k) },\n async remove(kid) { keys.delete(kid) },\n }\n}\n\n/**\n * The table postgresKeyStore keeps the keys in, for the application's own\n * migrations (Drizzle, Prisma, plain SQL). The store never creates it.\n */\nexport function pactKeysTableSql(table = 'cauthy_pact_keys'): string {\n checkTable(table)\n return `CREATE TABLE IF NOT EXISTS ${table} (\n kid TEXT PRIMARY KEY,\n public_jwk JSONB NOT NULL,\n sealed TEXT NOT NULL,\n created_at TIMESTAMPTZ NOT NULL\n);`\n}\n\nfunction checkTable(table: string): void {\n if (!/^[a-z_][a-z0-9_]{0,62}$/.test(table)) throw new CauthyConfigError(`the key table's name must be a plain SQL identifier: ${table}`)\n}\n\n/**\n * Runs one SQL statement with $1, $2… parameters and answers its rows: what\n * the application's own database client does. With node-postgres,\n * `(text, params) => pool.query(text, params)`; with postgres.js,\n * `(text, params) => sql.unsafe(text, params)`.\n */\nexport type Query = (text: string, params: unknown[]) => Promise<{ rows: unknown[] } | unknown[]>\n\n/** Keys kept in a table of the application's Postgres database (see pactKeysTableSql). */\nexport function postgresKeyStore(query: Query, options: { table?: string } = {}): KeyStore {\n const table = options.table ?? 'cauthy_pact_keys'\n checkTable(table)\n const rows = async (text: string, params: unknown[]) => {\n const out = await query(text, params)\n return (Array.isArray(out) ? out : out.rows) as Record<string, unknown>[]\n }\n const at = (v: unknown) => (v instanceof Date ? v.toISOString() : new Date(String(v)).toISOString())\n return {\n async list() {\n return (await rows(`SELECT kid, public_jwk, sealed, created_at FROM ${table}`, [])).map(r => ({\n kid: String(r.kid),\n publicJwk: (typeof r.public_jwk === 'string' ? JSON.parse(r.public_jwk) : r.public_jwk) as JWK,\n sealed: String(r.sealed),\n createdAt: at(r.created_at),\n }))\n },\n async add(k) {\n await rows(`INSERT INTO ${table} (kid, public_jwk, sealed, created_at) VALUES ($1, $2, $3, $4) ON CONFLICT (kid) DO NOTHING`,\n [k.kid, JSON.stringify(k.publicJwk), k.sealed, k.createdAt])\n },\n async addFirst(k) {\n // One statement: a key another instance committed a moment ago is seen.\n await rows(`INSERT INTO ${table} (kid, public_jwk, sealed, created_at) SELECT $1, $2, $3, $4 WHERE NOT EXISTS (SELECT 1 FROM ${table}) ON CONFLICT (kid) DO NOTHING`,\n [k.kid, JSON.stringify(k.publicJwk), k.sealed, k.createdAt])\n },\n async update(k) {\n await rows(`UPDATE ${table} SET sealed = $2 WHERE kid = $1`, [k.kid, k.sealed])\n },\n async remove(kid) {\n await rows(`DELETE FROM ${table} WHERE kid = $1`, [kid])\n },\n }\n}\n","/** How an application is set up to sign its people in with Cauthy. */\nexport interface CauthyOptions {\n /**\n * The application's CAUTHY_KEY, made in Cauthy's dashboard: Cauthy's\n * address, the application's client and its secret, in one value. Default:\n * `CAUTHY_KEY`. With it nothing else is read: the workspace, the system and\n * the application's addresses come from Cauthy (`GET /v1/connect/self`),\n * and passing `issuer`, `clientId`, `clientSecret`, `secret`, `system`,\n * `tenant`, `audience`, `tenantId`, `systemKey` or `rootKey` is refused.\n */\n key?: string\n /** The key being replaced, during a rotation: read, never written with. Default: `CAUTHY_KEY_PREVIOUS`. */\n keyPrevious?: string\n /** Cauthy's public address, e.g. `https://id.example.com`. Default: `CAUTHY_ISSUER`. */\n issuer?: string\n /** The application's OAuth client id. Default: `CAUTHY_CLIENT_ID`. */\n clientId?: string\n /** Its secret, when the client is confidential. Default: `CAUTHY_CLIENT_SECRET`. */\n clientSecret?: string\n /**\n * The key the session cookie is encrypted with: any string of 32 characters\n * or more, kept on the server. Default: `CAUTHY_SECRET`.\n */\n secret?: string\n /**\n * The system this application is, as its organizations are filed under in\n * Cauthy (`\"acme-crm\"`). When set, a session's organizations are that\n * system's. Default: `CAUTHY_SYSTEM`.\n */\n system?: string\n /**\n * The workspace's slug in Cauthy. Needed for the application's own sign-in\n * screens (`signIn.email`, `signUp.email`…), which sign people in through\n * the application's server instead of Cauthy's page. Default: `CAUTHY_TENANT`.\n */\n tenant?: string\n /** Where the routes are mounted. Default: `/api/auth`. */\n basePath?: string\n /**\n * What happens to a POST without `X-Cauthy-Request: 1` (every POST but\n * `/logout`). `required`, the default, answers `403 csrf_required`, as\n * protocol level strict asks. `warn` lets it through and says so once per\n * process: for the one deploy that moves an application whose browser still\n * runs cauthy-auth 0.42, which does not send the header. Remove it once the\n * browser runs cauthy-auth 0.43 or @cauthyhq/js.\n */\n requestHeader?: 'required' | 'warn'\n /**\n * The audience of the access tokens `auth.verifyToken` accepts: the\n * deployment's `JWT_AUDIENCE`, which Cauthy requires its operator to set and\n * has no default for. Default: `CAUTHY_AUDIENCE`.\n */\n audience?: string\n /**\n * The workspace's id (a lowercase UUID) for `auth.verifyToken`. By contract\n * it comes from the link's `tenant_id`; otherwise: this, or `CAUTHY_TENANT_ID`.\n */\n tenantId?: string\n /**\n * The application's own public origin (`https://app.example.com`): the\n * redirect URI, the cookies' `Secure` flag and which forms are its own come\n * from it. Default: `CAUTHY_BASE_URL`, else the origin of each request, with\n * the scheme a proxy names in `x-forwarded-proto`. Set it whenever a proxy\n * also changes the host or the port.\n */\n baseURL?: string\n /** Where a person lands after signing in, when no `return_to` was asked. Default: `/`. */\n afterSignIn?: string\n /** Where a person lands after signing out. Default: `/`. */\n afterSignOut?: string\n /** Name of the session cookie. Default: `cauthy_session`. */\n cookieName?: string\n /**\n * A key the application proves who it is with when it acts on its own\n * (`auth.system`), instead of its secret. Default: `CAUTHY_SYSTEM_KEY` and\n * `CAUTHY_SYSTEM_KEY_ID`.\n */\n systemKey?: import('./system').SystemKey\n /**\n * The application's own pages for what arrives by mail. A link Cauthy mails\n * opens the page named here, with a `token` to give back; with none, it\n * opens Cauthy's own page.\n *\n * signIn where `<SignIn />` is: a sign-in link opens it\n * resetPassword where `<ResetPassword />` is\n * verifyEmail where `<VerifyEmail />` is\n * acceptInvite where `<AcceptInvite />` is: an invitation opens it\n *\n * An invitation sent by `auth.system.members.add` or `auth.system.invite`\n * opens `acceptInvite` only when the application knows its public origin\n * (`baseURL`): that call has no request to read it from.\n */\n pages?: { signIn?: string; resetPassword?: string; verifyEmail?: string; acceptInvite?: string }\n /**\n * The address of the person a request comes from, passed on to Cauthy so\n * its limits on attempts count the person and not this server. Default: the\n * last address in `X-Forwarded-For`, which is the one the proxy in front of\n * the application wrote. With several proxies, or none, give your own.\n */\n clientIp?: (request: Request) => string | undefined\n /** `fetch` to reach Cauthy with. Default: the global one. */\n fetch?: typeof fetch\n /**\n * How long a call to Cauthy may take before it fails, in milliseconds.\n * `system`: the application's own calls (`auth.system`, and the proof its\n * sign-ins carry). Default 2000. `request`: the sign-in screens' calls and\n * the code, refresh and userinfo calls of a session. Default 5000. The\n * push-approval event stream has no deadline.\n */\n timeouts?: { system?: number; request?: number }\n /**\n * Run by contract: the issuer, the workspace, the clients and where the\n * application's keys are published all come from its link in the network's\n * context graph, and none of the CAUTHY_* variables may be set. See\n * \"Connect a system by contract\".\n *\n * createCauthy({ contract: { graph, system: 'acme-crm' }, keys: postgresKeyStore(query) })\n */\n contract?: import('./contract').ContractOptions\n /** Where the application keeps its own key, with a contract (@cauthyhq/node/keys). */\n keys?: import('./keys').KeyStore\n /**\n * The one secret of a contracted application: every key it needs is\n * derived from it. 32 random bytes or more. Default: `PACT_ROOT_KEY`.\n */\n rootKey?: string\n /** The root key before the current one, while rotating it. Default: `PACT_ROOT_KEY_PREVIOUS`. */\n rootKeyPrevious?: string\n /**\n * Where `auth.webhook` remembers the deliveries it took, so one sent twice\n * is refused. Default: this process's memory. Give a shared one (a table)\n * when the application runs on several instances.\n */\n webhookReplay?: import('./webhook').ReplayStore\n}\n\n/** The person signed in. Every moment is UTC. */\nexport interface SessionUser {\n id: string\n email: string\n name: string\n emailVerified: boolean\n /**\n * True when the account ran out of time to confirm its address: let it read\n * and refuse its writes.\n */\n restricted: boolean\n /** When an unconfirmed address stops being enough (ISO 8601, UTC), if there is a deadline. */\n emailVerifyBy?: string\n /** The language the person reads in (\"es\", \"en-US\"), when Cauthy knows it. */\n locale?: string\n /** A guest: a visitor's session before they sign up. No email; `account.upgrade` makes it an account, keeping `id`. */\n isAnonymous?: boolean\n}\n\n/** A value JSON can carry. What crosses from server to browser is typed as this, never `unknown`. */\nexport type JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue }\n\n/** An organization the person belongs to, with their role in it. */\nexport interface SessionOrganization {\n id: string\n system: string\n name: string\n kind: string\n externalId?: string\n role: string\n /** What the system keeps about the organization, as it filed it in Cauthy. */\n metadata: Record<string, JsonValue>\n /**\n * What the person may do here, as \"resource:action\" entries (\"*\" is any):\n * their role's permissions, what it inherits, and anything given to them\n * apart from it. Absent when the system keeps no catalog of roles in Cauthy.\n * Check with `can(organization, resource, action)`.\n */\n permissions?: string[]\n /** What the person is denied here, whatever else allows it. */\n denied?: string[]\n /**\n * Permissions that hold only when their condition does (an amount under a\n * limit, the person's own records…). They are in neither list above: `can`\n * answers no for them, and `auth.check` asks Cauthy. Present, maybe empty,\n * whenever `permissions` is.\n */\n conditional?: ConditionalPermission[]\n}\n\n/** A permission with a condition: Cauthy decides it per question (`auth.check`). */\nexport interface ConditionalPermission {\n resource: string\n action: string\n effect: 'allow' | 'deny'\n}\n\n/** What to ask Cauthy about: an action on a resource, with what the conditions read of it. */\nexport interface CheckQuestion {\n resource: string\n action: string\n /** What a condition reads as `resource.attrs`: `{ amount: 500 }`. */\n attributes?: Record<string, unknown>\n /** The record's id, as a condition reads it (`resource.id`). */\n resourceId?: string\n /** Whose the record is (`resource.owner_id`): `resource.owner_id == subject.id`. */\n ownerId?: string\n}\n\n/** An organization, by its id or as the session has it. */\nexport type OrganizationRef = string | { id: string }\n\n/** The rule that decided a check. */\nexport interface CheckRule {\n /** A permission of the person's role, or an exception given to the person. */\n source: 'role' | 'exception'\n /** The role, when `source` is `role`. */\n role?: string\n kind: string\n resource: string\n action: string\n effect: 'allow' | 'deny'\n condition?: string\n}\n\n/** Cauthy's answer to a check. */\nexport interface CheckResult {\n allowed: boolean\n /** Why, in words: \"allowed by role accountant: invoices:approve\". */\n reason: string\n /** The rule that decided; absent when none did (nothing allows it). */\n rule?: CheckRule\n}\n\n/** A signed-in person, as the server sees them. */\nexport interface Session {\n user: SessionUser\n /** The Cauthy tenant the account is of. */\n tenantId: string\n /** The roles the account holds in that tenant. */\n roles: string[]\n /**\n * Cauthy's access token for this person: send it as `Authorization: Bearer`\n * to another service that verifies Cauthy's tokens. Never give it to the browser.\n */\n accessToken: string\n /** When the access token expires and the session is checked with Cauthy again (ISO 8601, UTC). */\n expiresAt: string\n}\n\n/** What the browser is told of a session: who, and their organizations — never a token. */\nexport interface PublicSession {\n user: SessionUser\n tenantId: string\n roles: string[]\n organizations: SessionOrganization[]\n expiresAt: string\n}\n\n/** Why there is no session. */\nexport type SessionStatus =\n | 'authenticated'\n | 'unauthenticated' // nobody is signed in\n | 'expired' // signed in, but the session must be renewed first (see `Cauthy.resolve`)\n\nexport class CauthyConfigError extends Error {\n constructor(message: string) {\n super(`@cauthyhq/node: ${message}`)\n this.name = 'CauthyConfigError'\n }\n}\n\n/** Why a CAUTHY_KEY was refused (key/cauthy-key.md §3, §8, §9). */\nexport type CauthyKeyReason = 'key_format' | 'key_version' | 'key_checksum' | 'key_issuer' | 'key_client' | 'key_secret'\n | 'key_contract_issuer' | 'key_previous_mismatch'\n\n/** A refused CAUTHY_KEY. The message names the reason, never the key. */\nexport class CauthyKeyError extends CauthyConfigError {\n constructor(readonly reason: CauthyKeyReason, message: string) {\n super(`CAUTHY_KEY: ${message} (${reason})`)\n this.name = 'CauthyKeyError'\n }\n}\n"],"mappings":"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","names":[]}
|
package/dist/keys.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/keystores.ts","../src/keys.ts","../src/types.ts"],"sourcesContent":["// @cauthyhq/node/keys: where a contracted application keeps its own key.\n//\n// import { postgresKeyStore } from '@cauthyhq/node/keys'\n// keys: postgresKeyStore((text, params) => pool.query(text, params))\n//\n// The file store is for a developer's machine: production keeps its key in\n// its database, where every instance reads the same one.\n\nimport { mkdir, readFile, rename, writeFile } from 'node:fs/promises'\nimport { dirname } from 'node:path'\nimport type { KeyStore, StoredKey } from './keys'\n\nexport { memoryKeyStore, postgresKeyStore, pactKeysTableSql, type KeyStore, type PactKeys, type Query, type StoredKey } from './keys'\n\n/** Keys kept in a JSON file, readable only by its owner. For development. */\nexport function fileKeyStore(path: string): KeyStore {\n async function read(): Promise<StoredKey[]> {\n try {\n const parsed = JSON.parse(await readFile(path, 'utf8')) as unknown\n return Array.isArray(parsed) ? (parsed as StoredKey[]) : []\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === 'ENOENT') return []\n throw err\n }\n }\n // Written whole to a file beside it, then moved over it: a crash never\n // leaves half a file.\n let writing: Promise<void> = Promise.resolve()\n function write(change: (keys: StoredKey[]) => StoredKey[]): Promise<void> {\n writing = writing.then(async () => {\n const next = change(await read())\n await mkdir(dirname(path), { recursive: true })\n const tmp = `${path}.${process.pid}.tmp`\n await writeFile(tmp, JSON.stringify(next, null, 2) + '\\n', { mode: 0o600 })\n await rename(tmp, path)\n })\n return writing\n }\n return {\n list: read,\n add: k => write(keys => (keys.some(x => x.kid === k.kid) ? keys : [...keys, k])),\n addFirst: k => write(keys => (keys.length > 0 ? keys : [k])),\n update: k => write(keys => keys.map(x => (x.kid === k.kid ? k : x))),\n remove: kid => write(keys => keys.filter(x => x.kid !== kid)),\n }\n}\n","// The application's own key: made by the application, kept in its own\n// database, its public half published, its private half sealed at rest.\n//\n// A contracted application proves who it is to Cauthy with an assertion it\n// signs (private_key_jwt). Nobody hands it that key: the first time it needs\n// one it makes it, seals the private half with the key derived from its root\n// key (pact/seal/v1) and keeps it in a KeyStore — a table of its database, a\n// file while developing. Its public half is served at the jwks_url its\n// manifest names, where Cauthy reads it.\n//\n// Rotating: `rotate()` makes a new key, published at once. It signs once it\n// has been published longer than anyone caches the keys (SIGN_AFTER_MS):\n// Cauthy, meeting a key it does not know, reads the keys again, and a copy\n// cached on the way must already hold it. The one before it stays published\n// 24 hours, so an assertion signed a moment ago, or by an instance that has\n// not seen the new key yet, still verifies.\n//\n// The first key is made by whichever instance needs it first: the store keeps\n// it only while it holds none (addFirst), so instances starting together end\n// with one key. A store without addFirst may end with several; every one is\n// published, and every instance settles on the same one within seconds.\n\nimport { calculateJwkThumbprint, exportJWK, generateKeyPair, importJWK, type JWK, type KeyLike } from 'jose'\nimport { seal, unseal } from './seal'\nimport type { KeyRing } from './rootkey'\nimport { CauthyConfigError } from './types'\n\n/** A key as a KeyStore keeps it. Nothing in it is secret but sealed. */\nexport interface StoredKey {\n /** Its id, as published (the JWK thumbprint). */\n kid: string\n /** Its public half, as published. */\n publicJwk: JWK\n /** Its private half, sealed with the key derived from the root key. */\n sealed: string\n /** When it was made (ISO 8601, UTC). The newest signs. */\n createdAt: string\n}\n\n/** Where the application keeps its keys: a table, a file. */\nexport interface KeyStore {\n /** Every key kept. */\n list(): Promise<StoredKey[]>\n /** Keeps a new key. A kid already kept is left as it is. */\n add(key: StoredKey): Promise<void>\n /** Keeps the key only while the store holds none, in one step: instances starting together make one key. */\n addFirst?(key: StoredKey): Promise<void>\n /** Replaces a key's sealed private half (after the root key was rotated). */\n update(key: StoredKey): Promise<void>\n /** Forgets a key no longer published. */\n remove(kid: string): Promise<void>\n}\n\n/** How long a key stays published after a newer one appears. */\nexport const RETIRE_AFTER_MS = 24 * 60 * 60 * 1000\n/** How long a key that is no longer published is kept before it is forgotten. */\nconst FORGET_AFTER_MS = 7 * 24 * 60 * 60 * 1000\n/** How long the key that signs is remembered before the store is read again. */\nconst CURRENT_TTL_MS = 5 * 60 * 1000\n/** How long the published keys may be cached (the jwks_url's max-age). */\nexport const JWKS_MAX_AGE_S = 300\n/** How long a new key is published before it signs: longer than any cached copy of the keys. */\nexport const SIGN_AFTER_MS = (JWKS_MAX_AGE_S + 60) * 1000\n/** While every key is younger than that, the store is read again this often, so instances settle on one. */\nconst SETTLING_TTL_MS = 5 * 1000\n\n/** The application's keys, as auth.pact.keys gives them. */\nexport interface PactKeys {\n /** The key that signs now, made the first time there is none: the newest that has been published long enough. */\n current(): Promise<{ kid: string; privateKey: KeyLike | Uint8Array }>\n /** The public keys to publish: the one that signs, and the one before it for 24 hours after. */\n published(): Promise<{ keys: JWK[] }>\n /** Makes a new key, published at once and signing once SIGN_AFTER_MS has passed. Answers its id. */\n rotate(): Promise<string>\n /** Seals every key again with the current root key; answers how many changed. */\n reseal(): Promise<number>\n}\n\nconst newestFirst = (keys: StoredKey[]) => [...keys].sort((a, b) => b.createdAt.localeCompare(a.createdAt) || b.kid.localeCompare(a.kid))\n\nfunction published(key: StoredKey): JWK {\n const { kty, n, e } = key.publicJwk\n return { kty, n, e, kid: key.kid, alg: 'RS256', use: 'sig' }\n}\n\nexport function pactKeys(store: KeyStore, sealKeys: Promise<KeyRing>, now: () => number = Date.now): PactKeys {\n let remembered: { kid: string; privateKey: KeyLike | Uint8Array; until: number } | undefined\n let making: Promise<StoredKey> | undefined\n\n async function make(after?: StoredKey): Promise<StoredKey> {\n const { publicKey, privateKey } = await generateKeyPair('RS256', { modulusLength: 2048, extractable: true })\n const publicJwk = await exportJWK(publicKey)\n const kid = await calculateJwkThumbprint(publicJwk)\n // Always newer than the key it follows, however fast the rotation.\n const at = Math.max(now(), after ? Date.parse(after.createdAt) + 1 : 0)\n const key: StoredKey = { kid, publicJwk, sealed: await seal(await exportJWK(privateKey), (await sealKeys)[0]), createdAt: new Date(at).toISOString() }\n if (!after && store.addFirst) await store.addFirst(key)\n else await store.add(key)\n return key\n }\n\n const age = (key: StoredKey) => now() - Date.parse(key.createdAt)\n\n // The key that signs: the newest published long enough. When none is (the\n // first minutes of the first key, or a race of first keys), the oldest, read\n // again shortly so every instance lands on the same one.\n function signer(kept: StoredKey[]): { key: StoredKey; until: number } {\n const ripe = kept.find(k => age(k) >= SIGN_AFTER_MS)\n if (!ripe) return { key: kept[kept.length - 1], until: now() + SETTLING_TTL_MS }\n // A newer key ripens later: read again then, not after the usual while.\n const until = ripe === kept[0] ? now() + CURRENT_TTL_MS : Math.min(now() + CURRENT_TTL_MS, Date.parse(kept[0].createdAt) + SIGN_AFTER_MS)\n return { key: ripe, until }\n }\n\n async function open(key: StoredKey): Promise<{ jwk: JWK; fresh: boolean } | null> {\n const ring = await sealKeys\n for (let i = 0; i < ring.length; i++) {\n const jwk = await unseal<JWK>(key.sealed, ring[i])\n if (jwk !== null) return { jwk, fresh: i === 0 }\n }\n return null\n }\n\n // Every key kept, newest first; the first one is made when there is none.\n async function keptKeys(): Promise<StoredKey[]> {\n const kept = newestFirst(await store.list())\n if (kept.length > 0) return kept\n // Two callers at once make one key; two instances at once, one with\n // addFirst, and otherwise several that are all published.\n making ??= make().finally(() => { making = undefined })\n await making\n const again = newestFirst(await store.list())\n if (again.length === 0) throw new CauthyConfigError('the key store kept no key: check that it writes')\n return again\n }\n\n return {\n async current() {\n if (remembered && remembered.until > now()) return remembered\n const { key, until } = signer(await keptKeys())\n if (remembered?.kid === key.kid) {\n remembered.until = until\n return remembered\n }\n const opened = await open(key)\n if (opened === null) {\n throw new CauthyConfigError(`the key ${key.kid} cannot be opened with the root key (PACT_ROOT_KEY): ` +\n 'if the root key was rotated, set the previous one as PACT_ROOT_KEY_PREVIOUS')\n }\n remembered = { kid: key.kid, privateKey: await importJWK(opened.jwk, 'RS256'), until }\n return remembered\n },\n async published() {\n // Asked before anything was signed: the key is made now, so what is\n // published is the key that will sign.\n const kept = await keptKeys()\n const out: JWK[] = []\n for (let i = 0; i < kept.length; i++) {\n // The newest is published; an older one while its successor is young.\n if (i === 0 || now() - Date.parse(kept[i - 1].createdAt) < RETIRE_AFTER_MS) out.push(published(kept[i]))\n else if (now() - Date.parse(kept[i - 1].createdAt) > FORGET_AFTER_MS) await store.remove(kept[i].kid)\n }\n return { keys: out }\n },\n async rotate() {\n const key = await make(newestFirst(await store.list())[0])\n remembered = undefined\n return key.kid\n },\n async reseal() {\n let changed = 0\n const ring = await sealKeys\n for (const key of await store.list()) {\n const opened = await open(key)\n if (opened === null || opened.fresh) continue\n await store.update({ ...key, sealed: await seal(opened.jwk, ring[0]) })\n changed++\n }\n return changed\n },\n }\n}\n\n/** Keys kept in memory: for tests, and for trying contract mode out. Gone on restart. */\nexport function memoryKeyStore(): KeyStore {\n const keys = new Map<string, StoredKey>()\n return {\n async list() { return [...keys.values()] },\n async add(k) { if (!keys.has(k.kid)) keys.set(k.kid, k) },\n async addFirst(k) { if (keys.size === 0) keys.set(k.kid, k) },\n async update(k) { keys.set(k.kid, k) },\n async remove(kid) { keys.delete(kid) },\n }\n}\n\n/**\n * The table postgresKeyStore keeps the keys in, for the application's own\n * migrations (Drizzle, Prisma, plain SQL). The store never creates it.\n */\nexport function pactKeysTableSql(table = 'cauthy_pact_keys'): string {\n checkTable(table)\n return `CREATE TABLE IF NOT EXISTS ${table} (\n kid TEXT PRIMARY KEY,\n public_jwk JSONB NOT NULL,\n sealed TEXT NOT NULL,\n created_at TIMESTAMPTZ NOT NULL\n);`\n}\n\nfunction checkTable(table: string): void {\n if (!/^[a-z_][a-z0-9_]{0,62}$/.test(table)) throw new CauthyConfigError(`the key table's name must be a plain SQL identifier: ${table}`)\n}\n\n/**\n * Runs one SQL statement with $1, $2… parameters and answers its rows: what\n * the application's own database client does. With node-postgres,\n * `(text, params) => pool.query(text, params)`; with postgres.js,\n * `(text, params) => sql.unsafe(text, params)`.\n */\nexport type Query = (text: string, params: unknown[]) => Promise<{ rows: unknown[] } | unknown[]>\n\n/** Keys kept in a table of the application's Postgres database (see pactKeysTableSql). */\nexport function postgresKeyStore(query: Query, options: { table?: string } = {}): KeyStore {\n const table = options.table ?? 'cauthy_pact_keys'\n checkTable(table)\n const rows = async (text: string, params: unknown[]) => {\n const out = await query(text, params)\n return (Array.isArray(out) ? out : out.rows) as Record<string, unknown>[]\n }\n const at = (v: unknown) => (v instanceof Date ? v.toISOString() : new Date(String(v)).toISOString())\n return {\n async list() {\n return (await rows(`SELECT kid, public_jwk, sealed, created_at FROM ${table}`, [])).map(r => ({\n kid: String(r.kid),\n publicJwk: (typeof r.public_jwk === 'string' ? JSON.parse(r.public_jwk) : r.public_jwk) as JWK,\n sealed: String(r.sealed),\n createdAt: at(r.created_at),\n }))\n },\n async add(k) {\n await rows(`INSERT INTO ${table} (kid, public_jwk, sealed, created_at) VALUES ($1, $2, $3, $4) ON CONFLICT (kid) DO NOTHING`,\n [k.kid, JSON.stringify(k.publicJwk), k.sealed, k.createdAt])\n },\n async addFirst(k) {\n // One statement: a key another instance committed a moment ago is seen.\n await rows(`INSERT INTO ${table} (kid, public_jwk, sealed, created_at) SELECT $1, $2, $3, $4 WHERE NOT EXISTS (SELECT 1 FROM ${table}) ON CONFLICT (kid) DO NOTHING`,\n [k.kid, JSON.stringify(k.publicJwk), k.sealed, k.createdAt])\n },\n async update(k) {\n await rows(`UPDATE ${table} SET sealed = $2 WHERE kid = $1`, [k.kid, k.sealed])\n },\n async remove(kid) {\n await rows(`DELETE FROM ${table} WHERE kid = $1`, [kid])\n },\n }\n}\n","/** How an application is set up to sign its people in with Cauthy. */\nexport interface CauthyOptions {\n /** Cauthy's public address, e.g. `https://id.example.com`. Default: `CAUTHY_ISSUER`. */\n issuer?: string\n /** The application's OAuth client id. Default: `CAUTHY_CLIENT_ID`. */\n clientId?: string\n /** Its secret, when the client is confidential. Default: `CAUTHY_CLIENT_SECRET`. */\n clientSecret?: string\n /**\n * The key the session cookie is encrypted with: any string of 32 characters\n * or more, kept on the server. Default: `CAUTHY_SECRET`.\n */\n secret?: string\n /**\n * The system this application is, as its organizations are filed under in\n * Cauthy (`\"acme-crm\"`). When set, a session's organizations are that\n * system's. Default: `CAUTHY_SYSTEM`.\n */\n system?: string\n /**\n * The workspace's slug in Cauthy. Needed for the application's own sign-in\n * screens (`signIn.email`, `signUp.email`…), which sign people in through\n * the application's server instead of Cauthy's page. Default: `CAUTHY_TENANT`.\n */\n tenant?: string\n /** Where the routes are mounted. Default: `/api/auth`. */\n basePath?: string\n /**\n * What happens to a POST without `X-Cauthy-Request: 1` (every POST but\n * `/logout`). `required`, the default, answers `403 csrf_required`, as\n * protocol level strict asks. `warn` lets it through and says so once per\n * process: for the one deploy that moves an application whose browser still\n * runs cauthy-auth 0.42, which does not send the header. Remove it once the\n * browser runs cauthy-auth 0.43 or @cauthyhq/js.\n */\n requestHeader?: 'required' | 'warn'\n /**\n * The audience of the access tokens `auth.verifyToken` accepts: the\n * deployment's `JWT_AUDIENCE`, which Cauthy requires its operator to set and\n * has no default for. Default: `CAUTHY_AUDIENCE`.\n */\n audience?: string\n /**\n * The workspace's id (a lowercase UUID) for `auth.verifyToken`. By contract\n * it comes from the link's `tenant_id`; otherwise: this, or `CAUTHY_TENANT_ID`.\n */\n tenantId?: string\n /**\n * The application's own public origin (`https://app.example.com`): the\n * redirect URI, the cookies' `Secure` flag and which forms are its own come\n * from it. Default: `CAUTHY_BASE_URL`, else the origin of each request, with\n * the scheme a proxy names in `x-forwarded-proto`. Set it whenever a proxy\n * also changes the host or the port.\n */\n baseURL?: string\n /** Where a person lands after signing in, when no `return_to` was asked. Default: `/`. */\n afterSignIn?: string\n /** Where a person lands after signing out. Default: `/`. */\n afterSignOut?: string\n /** Name of the session cookie. Default: `cauthy_session`. */\n cookieName?: string\n /**\n * A key the application proves who it is with when it acts on its own\n * (`auth.system`), instead of its secret. Default: `CAUTHY_SYSTEM_KEY` and\n * `CAUTHY_SYSTEM_KEY_ID`.\n */\n systemKey?: import('./system').SystemKey\n /**\n * The application's own pages for what arrives by mail. A link Cauthy mails\n * opens the page named here, with a `token` to give back; with none, it\n * opens Cauthy's own page.\n *\n * signIn where `<SignIn />` is: a sign-in link opens it\n * resetPassword where `<ResetPassword />` is\n * verifyEmail where `<VerifyEmail />` is\n * acceptInvite where `<AcceptInvite />` is: an invitation opens it\n *\n * An invitation sent by `auth.system.members.add` or `auth.system.invite`\n * opens `acceptInvite` only when the application knows its public origin\n * (`baseURL`): that call has no request to read it from.\n */\n pages?: { signIn?: string; resetPassword?: string; verifyEmail?: string; acceptInvite?: string }\n /**\n * The address of the person a request comes from, passed on to Cauthy so\n * its limits on attempts count the person and not this server. Default: the\n * last address in `X-Forwarded-For`, which is the one the proxy in front of\n * the application wrote. With several proxies, or none, give your own.\n */\n clientIp?: (request: Request) => string | undefined\n /** `fetch` to reach Cauthy with. Default: the global one. */\n fetch?: typeof fetch\n /**\n * How long a call to Cauthy may take before it fails, in milliseconds.\n * `system`: the application's own calls (`auth.system`, and the proof its\n * sign-ins carry). Default 2000. `request`: the sign-in screens' calls and\n * the code, refresh and userinfo calls of a session. Default 5000. The\n * push-approval event stream has no deadline.\n */\n timeouts?: { system?: number; request?: number }\n /**\n * Run by contract: the issuer, the workspace, the clients and where the\n * application's keys are published all come from its link in the network's\n * context graph, and none of the CAUTHY_* variables may be set. See\n * \"Connect a system by contract\".\n *\n * createCauthy({ contract: { graph, system: 'acme-crm' }, keys: postgresKeyStore(query) })\n */\n contract?: import('./contract').ContractOptions\n /** Where the application keeps its own key, with a contract (@cauthyhq/node/keys). */\n keys?: import('./keys').KeyStore\n /**\n * The one secret of a contracted application: every key it needs is\n * derived from it. 32 random bytes or more. Default: `PACT_ROOT_KEY`.\n */\n rootKey?: string\n /** The root key before the current one, while rotating it. Default: `PACT_ROOT_KEY_PREVIOUS`. */\n rootKeyPrevious?: string\n /**\n * Where `auth.webhook` remembers the deliveries it took, so one sent twice\n * is refused. Default: this process's memory. Give a shared one (a table)\n * when the application runs on several instances.\n */\n webhookReplay?: import('./webhook').ReplayStore\n}\n\n/** The person signed in. Every moment is UTC. */\nexport interface SessionUser {\n id: string\n email: string\n name: string\n emailVerified: boolean\n /**\n * True when the account ran out of time to confirm its address: let it read\n * and refuse its writes.\n */\n restricted: boolean\n /** When an unconfirmed address stops being enough (ISO 8601, UTC), if there is a deadline. */\n emailVerifyBy?: string\n /** The language the person reads in (\"es\", \"en-US\"), when Cauthy knows it. */\n locale?: string\n /** A guest: a visitor's session before they sign up. No email; `account.upgrade` makes it an account, keeping `id`. */\n isAnonymous?: boolean\n}\n\n/** A value JSON can carry. What crosses from server to browser is typed as this, never `unknown`. */\nexport type JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue }\n\n/** An organization the person belongs to, with their role in it. */\nexport interface SessionOrganization {\n id: string\n system: string\n name: string\n kind: string\n externalId?: string\n role: string\n /** What the system keeps about the organization, as it filed it in Cauthy. */\n metadata: Record<string, JsonValue>\n /**\n * What the person may do here, as \"resource:action\" entries (\"*\" is any):\n * their role's permissions, what it inherits, and anything given to them\n * apart from it. Absent when the system keeps no catalog of roles in Cauthy.\n * Check with `can(organization, resource, action)`.\n */\n permissions?: string[]\n /** What the person is denied here, whatever else allows it. */\n denied?: string[]\n /**\n * Permissions that hold only when their condition does (an amount under a\n * limit, the person's own records…). They are in neither list above: `can`\n * answers no for them, and `auth.check` asks Cauthy. Present, maybe empty,\n * whenever `permissions` is.\n */\n conditional?: ConditionalPermission[]\n}\n\n/** A permission with a condition: Cauthy decides it per question (`auth.check`). */\nexport interface ConditionalPermission {\n resource: string\n action: string\n effect: 'allow' | 'deny'\n}\n\n/** What to ask Cauthy about: an action on a resource, with what the conditions read of it. */\nexport interface CheckQuestion {\n resource: string\n action: string\n /** What a condition reads as `resource.attrs`: `{ amount: 500 }`. */\n attributes?: Record<string, unknown>\n /** The record's id, as a condition reads it (`resource.id`). */\n resourceId?: string\n /** Whose the record is (`resource.owner_id`): `resource.owner_id == subject.id`. */\n ownerId?: string\n}\n\n/** An organization, by its id or as the session has it. */\nexport type OrganizationRef = string | { id: string }\n\n/** The rule that decided a check. */\nexport interface CheckRule {\n /** A permission of the person's role, or an exception given to the person. */\n source: 'role' | 'exception'\n /** The role, when `source` is `role`. */\n role?: string\n kind: string\n resource: string\n action: string\n effect: 'allow' | 'deny'\n condition?: string\n}\n\n/** Cauthy's answer to a check. */\nexport interface CheckResult {\n allowed: boolean\n /** Why, in words: \"allowed by role accountant: invoices:approve\". */\n reason: string\n /** The rule that decided; absent when none did (nothing allows it). */\n rule?: CheckRule\n}\n\n/** A signed-in person, as the server sees them. */\nexport interface Session {\n user: SessionUser\n /** The Cauthy tenant the account is of. */\n tenantId: string\n /** The roles the account holds in that tenant. */\n roles: string[]\n /**\n * Cauthy's access token for this person: send it as `Authorization: Bearer`\n * to another service that verifies Cauthy's tokens. Never give it to the browser.\n */\n accessToken: string\n /** When the access token expires and the session is checked with Cauthy again (ISO 8601, UTC). */\n expiresAt: string\n}\n\n/** What the browser is told of a session: who, and their organizations — never a token. */\nexport interface PublicSession {\n user: SessionUser\n tenantId: string\n roles: string[]\n organizations: SessionOrganization[]\n expiresAt: string\n}\n\n/** Why there is no session. */\nexport type SessionStatus =\n | 'authenticated'\n | 'unauthenticated' // nobody is signed in\n | 'expired' // signed in, but the session must be renewed first (see `Cauthy.resolve`)\n\nexport class CauthyConfigError extends Error {\n constructor(message: string) {\n super(`@cauthyhq/node: ${message}`)\n this.name = 'CauthyConfigError'\n }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/keystores.ts","../src/keys.ts","../src/types.ts"],"sourcesContent":["// @cauthyhq/node/keys: where a contracted application keeps its own key.\n//\n// import { postgresKeyStore } from '@cauthyhq/node/keys'\n// keys: postgresKeyStore((text, params) => pool.query(text, params))\n//\n// The file store is for a developer's machine: production keeps its key in\n// its database, where every instance reads the same one.\n\nimport { mkdir, readFile, rename, writeFile } from 'node:fs/promises'\nimport { dirname } from 'node:path'\nimport type { KeyStore, StoredKey } from './keys'\n\nexport { memoryKeyStore, postgresKeyStore, pactKeysTableSql, type KeyStore, type PactKeys, type Query, type StoredKey } from './keys'\n\n/** Keys kept in a JSON file, readable only by its owner. For development. */\nexport function fileKeyStore(path: string): KeyStore {\n async function read(): Promise<StoredKey[]> {\n try {\n const parsed = JSON.parse(await readFile(path, 'utf8')) as unknown\n return Array.isArray(parsed) ? (parsed as StoredKey[]) : []\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === 'ENOENT') return []\n throw err\n }\n }\n // Written whole to a file beside it, then moved over it: a crash never\n // leaves half a file.\n let writing: Promise<void> = Promise.resolve()\n function write(change: (keys: StoredKey[]) => StoredKey[]): Promise<void> {\n writing = writing.then(async () => {\n const next = change(await read())\n await mkdir(dirname(path), { recursive: true })\n const tmp = `${path}.${process.pid}.tmp`\n await writeFile(tmp, JSON.stringify(next, null, 2) + '\\n', { mode: 0o600 })\n await rename(tmp, path)\n })\n return writing\n }\n return {\n list: read,\n add: k => write(keys => (keys.some(x => x.kid === k.kid) ? keys : [...keys, k])),\n addFirst: k => write(keys => (keys.length > 0 ? keys : [k])),\n update: k => write(keys => keys.map(x => (x.kid === k.kid ? k : x))),\n remove: kid => write(keys => keys.filter(x => x.kid !== kid)),\n }\n}\n","// The application's own key: made by the application, kept in its own\n// database, its public half published, its private half sealed at rest.\n//\n// A contracted application proves who it is to Cauthy with an assertion it\n// signs (private_key_jwt). Nobody hands it that key: the first time it needs\n// one it makes it, seals the private half with the key derived from its root\n// key (pact/seal/v1) and keeps it in a KeyStore — a table of its database, a\n// file while developing. Its public half is served at the jwks_url its\n// manifest names, where Cauthy reads it.\n//\n// Rotating: `rotate()` makes a new key, published at once. It signs once it\n// has been published longer than anyone caches the keys (SIGN_AFTER_MS):\n// Cauthy, meeting a key it does not know, reads the keys again, and a copy\n// cached on the way must already hold it. The one before it stays published\n// 24 hours, so an assertion signed a moment ago, or by an instance that has\n// not seen the new key yet, still verifies.\n//\n// The first key is made by whichever instance needs it first: the store keeps\n// it only while it holds none (addFirst), so instances starting together end\n// with one key. A store without addFirst may end with several; every one is\n// published, and every instance settles on the same one within seconds.\n\nimport { calculateJwkThumbprint, exportJWK, generateKeyPair, importJWK, type JWK, type KeyLike } from 'jose'\nimport { seal, unseal } from './seal'\nimport type { KeyRing } from './rootkey'\nimport { CauthyConfigError } from './types'\n\n/** A key as a KeyStore keeps it. Nothing in it is secret but sealed. */\nexport interface StoredKey {\n /** Its id, as published (the JWK thumbprint). */\n kid: string\n /** Its public half, as published. */\n publicJwk: JWK\n /** Its private half, sealed with the key derived from the root key. */\n sealed: string\n /** When it was made (ISO 8601, UTC). The newest signs. */\n createdAt: string\n}\n\n/** Where the application keeps its keys: a table, a file. */\nexport interface KeyStore {\n /** Every key kept. */\n list(): Promise<StoredKey[]>\n /** Keeps a new key. A kid already kept is left as it is. */\n add(key: StoredKey): Promise<void>\n /** Keeps the key only while the store holds none, in one step: instances starting together make one key. */\n addFirst?(key: StoredKey): Promise<void>\n /** Replaces a key's sealed private half (after the root key was rotated). */\n update(key: StoredKey): Promise<void>\n /** Forgets a key no longer published. */\n remove(kid: string): Promise<void>\n}\n\n/** How long a key stays published after a newer one appears. */\nexport const RETIRE_AFTER_MS = 24 * 60 * 60 * 1000\n/** How long a key that is no longer published is kept before it is forgotten. */\nconst FORGET_AFTER_MS = 7 * 24 * 60 * 60 * 1000\n/** How long the key that signs is remembered before the store is read again. */\nconst CURRENT_TTL_MS = 5 * 60 * 1000\n/** How long the published keys may be cached (the jwks_url's max-age). */\nexport const JWKS_MAX_AGE_S = 300\n/** How long a new key is published before it signs: longer than any cached copy of the keys. */\nexport const SIGN_AFTER_MS = (JWKS_MAX_AGE_S + 60) * 1000\n/** While every key is younger than that, the store is read again this often, so instances settle on one. */\nconst SETTLING_TTL_MS = 5 * 1000\n\n/** The application's keys, as auth.pact.keys gives them. */\nexport interface PactKeys {\n /** The key that signs now, made the first time there is none: the newest that has been published long enough. */\n current(): Promise<{ kid: string; privateKey: KeyLike | Uint8Array }>\n /** The public keys to publish: the one that signs, and the one before it for 24 hours after. */\n published(): Promise<{ keys: JWK[] }>\n /** Makes a new key, published at once and signing once SIGN_AFTER_MS has passed. Answers its id. */\n rotate(): Promise<string>\n /** Seals every key again with the current root key; answers how many changed. */\n reseal(): Promise<number>\n}\n\nconst newestFirst = (keys: StoredKey[]) => [...keys].sort((a, b) => b.createdAt.localeCompare(a.createdAt) || b.kid.localeCompare(a.kid))\n\nfunction published(key: StoredKey): JWK {\n const { kty, n, e } = key.publicJwk\n return { kty, n, e, kid: key.kid, alg: 'RS256', use: 'sig' }\n}\n\nexport function pactKeys(store: KeyStore, sealKeys: Promise<KeyRing>, now: () => number = Date.now): PactKeys {\n let remembered: { kid: string; privateKey: KeyLike | Uint8Array; until: number } | undefined\n let making: Promise<StoredKey> | undefined\n\n async function make(after?: StoredKey): Promise<StoredKey> {\n const { publicKey, privateKey } = await generateKeyPair('RS256', { modulusLength: 2048, extractable: true })\n const publicJwk = await exportJWK(publicKey)\n const kid = await calculateJwkThumbprint(publicJwk)\n // Always newer than the key it follows, however fast the rotation.\n const at = Math.max(now(), after ? Date.parse(after.createdAt) + 1 : 0)\n const key: StoredKey = { kid, publicJwk, sealed: await seal(await exportJWK(privateKey), (await sealKeys)[0]), createdAt: new Date(at).toISOString() }\n if (!after && store.addFirst) await store.addFirst(key)\n else await store.add(key)\n return key\n }\n\n const age = (key: StoredKey) => now() - Date.parse(key.createdAt)\n\n // The key that signs: the newest published long enough. When none is (the\n // first minutes of the first key, or a race of first keys), the oldest, read\n // again shortly so every instance lands on the same one.\n function signer(kept: StoredKey[]): { key: StoredKey; until: number } {\n const ripe = kept.find(k => age(k) >= SIGN_AFTER_MS)\n if (!ripe) return { key: kept[kept.length - 1], until: now() + SETTLING_TTL_MS }\n // A newer key ripens later: read again then, not after the usual while.\n const until = ripe === kept[0] ? now() + CURRENT_TTL_MS : Math.min(now() + CURRENT_TTL_MS, Date.parse(kept[0].createdAt) + SIGN_AFTER_MS)\n return { key: ripe, until }\n }\n\n async function open(key: StoredKey): Promise<{ jwk: JWK; fresh: boolean } | null> {\n const ring = await sealKeys\n for (let i = 0; i < ring.length; i++) {\n const jwk = await unseal<JWK>(key.sealed, ring[i])\n if (jwk !== null) return { jwk, fresh: i === 0 }\n }\n return null\n }\n\n // Every key kept, newest first; the first one is made when there is none.\n async function keptKeys(): Promise<StoredKey[]> {\n const kept = newestFirst(await store.list())\n if (kept.length > 0) return kept\n // Two callers at once make one key; two instances at once, one with\n // addFirst, and otherwise several that are all published.\n making ??= make().finally(() => { making = undefined })\n await making\n const again = newestFirst(await store.list())\n if (again.length === 0) throw new CauthyConfigError('the key store kept no key: check that it writes')\n return again\n }\n\n return {\n async current() {\n if (remembered && remembered.until > now()) return remembered\n const { key, until } = signer(await keptKeys())\n if (remembered?.kid === key.kid) {\n remembered.until = until\n return remembered\n }\n const opened = await open(key)\n if (opened === null) {\n throw new CauthyConfigError(`the key ${key.kid} cannot be opened with the root key (PACT_ROOT_KEY): ` +\n 'if the root key was rotated, set the previous one as PACT_ROOT_KEY_PREVIOUS')\n }\n remembered = { kid: key.kid, privateKey: await importJWK(opened.jwk, 'RS256'), until }\n return remembered\n },\n async published() {\n // Asked before anything was signed: the key is made now, so what is\n // published is the key that will sign.\n const kept = await keptKeys()\n const out: JWK[] = []\n for (let i = 0; i < kept.length; i++) {\n // The newest is published; an older one while its successor is young.\n if (i === 0 || now() - Date.parse(kept[i - 1].createdAt) < RETIRE_AFTER_MS) out.push(published(kept[i]))\n else if (now() - Date.parse(kept[i - 1].createdAt) > FORGET_AFTER_MS) await store.remove(kept[i].kid)\n }\n return { keys: out }\n },\n async rotate() {\n const key = await make(newestFirst(await store.list())[0])\n remembered = undefined\n return key.kid\n },\n async reseal() {\n let changed = 0\n const ring = await sealKeys\n for (const key of await store.list()) {\n const opened = await open(key)\n if (opened === null || opened.fresh) continue\n await store.update({ ...key, sealed: await seal(opened.jwk, ring[0]) })\n changed++\n }\n return changed\n },\n }\n}\n\n/** Keys kept in memory: for tests, and for trying contract mode out. Gone on restart. */\nexport function memoryKeyStore(): KeyStore {\n const keys = new Map<string, StoredKey>()\n return {\n async list() { return [...keys.values()] },\n async add(k) { if (!keys.has(k.kid)) keys.set(k.kid, k) },\n async addFirst(k) { if (keys.size === 0) keys.set(k.kid, k) },\n async update(k) { keys.set(k.kid, k) },\n async remove(kid) { keys.delete(kid) },\n }\n}\n\n/**\n * The table postgresKeyStore keeps the keys in, for the application's own\n * migrations (Drizzle, Prisma, plain SQL). The store never creates it.\n */\nexport function pactKeysTableSql(table = 'cauthy_pact_keys'): string {\n checkTable(table)\n return `CREATE TABLE IF NOT EXISTS ${table} (\n kid TEXT PRIMARY KEY,\n public_jwk JSONB NOT NULL,\n sealed TEXT NOT NULL,\n created_at TIMESTAMPTZ NOT NULL\n);`\n}\n\nfunction checkTable(table: string): void {\n if (!/^[a-z_][a-z0-9_]{0,62}$/.test(table)) throw new CauthyConfigError(`the key table's name must be a plain SQL identifier: ${table}`)\n}\n\n/**\n * Runs one SQL statement with $1, $2… parameters and answers its rows: what\n * the application's own database client does. With node-postgres,\n * `(text, params) => pool.query(text, params)`; with postgres.js,\n * `(text, params) => sql.unsafe(text, params)`.\n */\nexport type Query = (text: string, params: unknown[]) => Promise<{ rows: unknown[] } | unknown[]>\n\n/** Keys kept in a table of the application's Postgres database (see pactKeysTableSql). */\nexport function postgresKeyStore(query: Query, options: { table?: string } = {}): KeyStore {\n const table = options.table ?? 'cauthy_pact_keys'\n checkTable(table)\n const rows = async (text: string, params: unknown[]) => {\n const out = await query(text, params)\n return (Array.isArray(out) ? out : out.rows) as Record<string, unknown>[]\n }\n const at = (v: unknown) => (v instanceof Date ? v.toISOString() : new Date(String(v)).toISOString())\n return {\n async list() {\n return (await rows(`SELECT kid, public_jwk, sealed, created_at FROM ${table}`, [])).map(r => ({\n kid: String(r.kid),\n publicJwk: (typeof r.public_jwk === 'string' ? JSON.parse(r.public_jwk) : r.public_jwk) as JWK,\n sealed: String(r.sealed),\n createdAt: at(r.created_at),\n }))\n },\n async add(k) {\n await rows(`INSERT INTO ${table} (kid, public_jwk, sealed, created_at) VALUES ($1, $2, $3, $4) ON CONFLICT (kid) DO NOTHING`,\n [k.kid, JSON.stringify(k.publicJwk), k.sealed, k.createdAt])\n },\n async addFirst(k) {\n // One statement: a key another instance committed a moment ago is seen.\n await rows(`INSERT INTO ${table} (kid, public_jwk, sealed, created_at) SELECT $1, $2, $3, $4 WHERE NOT EXISTS (SELECT 1 FROM ${table}) ON CONFLICT (kid) DO NOTHING`,\n [k.kid, JSON.stringify(k.publicJwk), k.sealed, k.createdAt])\n },\n async update(k) {\n await rows(`UPDATE ${table} SET sealed = $2 WHERE kid = $1`, [k.kid, k.sealed])\n },\n async remove(kid) {\n await rows(`DELETE FROM ${table} WHERE kid = $1`, [kid])\n },\n }\n}\n","/** How an application is set up to sign its people in with Cauthy. */\nexport interface CauthyOptions {\n /**\n * The application's CAUTHY_KEY, made in Cauthy's dashboard: Cauthy's\n * address, the application's client and its secret, in one value. Default:\n * `CAUTHY_KEY`. With it nothing else is read: the workspace, the system and\n * the application's addresses come from Cauthy (`GET /v1/connect/self`),\n * and passing `issuer`, `clientId`, `clientSecret`, `secret`, `system`,\n * `tenant`, `audience`, `tenantId`, `systemKey` or `rootKey` is refused.\n */\n key?: string\n /** The key being replaced, during a rotation: read, never written with. Default: `CAUTHY_KEY_PREVIOUS`. */\n keyPrevious?: string\n /** Cauthy's public address, e.g. `https://id.example.com`. Default: `CAUTHY_ISSUER`. */\n issuer?: string\n /** The application's OAuth client id. Default: `CAUTHY_CLIENT_ID`. */\n clientId?: string\n /** Its secret, when the client is confidential. Default: `CAUTHY_CLIENT_SECRET`. */\n clientSecret?: string\n /**\n * The key the session cookie is encrypted with: any string of 32 characters\n * or more, kept on the server. Default: `CAUTHY_SECRET`.\n */\n secret?: string\n /**\n * The system this application is, as its organizations are filed under in\n * Cauthy (`\"acme-crm\"`). When set, a session's organizations are that\n * system's. Default: `CAUTHY_SYSTEM`.\n */\n system?: string\n /**\n * The workspace's slug in Cauthy. Needed for the application's own sign-in\n * screens (`signIn.email`, `signUp.email`…), which sign people in through\n * the application's server instead of Cauthy's page. Default: `CAUTHY_TENANT`.\n */\n tenant?: string\n /** Where the routes are mounted. Default: `/api/auth`. */\n basePath?: string\n /**\n * What happens to a POST without `X-Cauthy-Request: 1` (every POST but\n * `/logout`). `required`, the default, answers `403 csrf_required`, as\n * protocol level strict asks. `warn` lets it through and says so once per\n * process: for the one deploy that moves an application whose browser still\n * runs cauthy-auth 0.42, which does not send the header. Remove it once the\n * browser runs cauthy-auth 0.43 or @cauthyhq/js.\n */\n requestHeader?: 'required' | 'warn'\n /**\n * The audience of the access tokens `auth.verifyToken` accepts: the\n * deployment's `JWT_AUDIENCE`, which Cauthy requires its operator to set and\n * has no default for. Default: `CAUTHY_AUDIENCE`.\n */\n audience?: string\n /**\n * The workspace's id (a lowercase UUID) for `auth.verifyToken`. By contract\n * it comes from the link's `tenant_id`; otherwise: this, or `CAUTHY_TENANT_ID`.\n */\n tenantId?: string\n /**\n * The application's own public origin (`https://app.example.com`): the\n * redirect URI, the cookies' `Secure` flag and which forms are its own come\n * from it. Default: `CAUTHY_BASE_URL`, else the origin of each request, with\n * the scheme a proxy names in `x-forwarded-proto`. Set it whenever a proxy\n * also changes the host or the port.\n */\n baseURL?: string\n /** Where a person lands after signing in, when no `return_to` was asked. Default: `/`. */\n afterSignIn?: string\n /** Where a person lands after signing out. Default: `/`. */\n afterSignOut?: string\n /** Name of the session cookie. Default: `cauthy_session`. */\n cookieName?: string\n /**\n * A key the application proves who it is with when it acts on its own\n * (`auth.system`), instead of its secret. Default: `CAUTHY_SYSTEM_KEY` and\n * `CAUTHY_SYSTEM_KEY_ID`.\n */\n systemKey?: import('./system').SystemKey\n /**\n * The application's own pages for what arrives by mail. A link Cauthy mails\n * opens the page named here, with a `token` to give back; with none, it\n * opens Cauthy's own page.\n *\n * signIn where `<SignIn />` is: a sign-in link opens it\n * resetPassword where `<ResetPassword />` is\n * verifyEmail where `<VerifyEmail />` is\n * acceptInvite where `<AcceptInvite />` is: an invitation opens it\n *\n * An invitation sent by `auth.system.members.add` or `auth.system.invite`\n * opens `acceptInvite` only when the application knows its public origin\n * (`baseURL`): that call has no request to read it from.\n */\n pages?: { signIn?: string; resetPassword?: string; verifyEmail?: string; acceptInvite?: string }\n /**\n * The address of the person a request comes from, passed on to Cauthy so\n * its limits on attempts count the person and not this server. Default: the\n * last address in `X-Forwarded-For`, which is the one the proxy in front of\n * the application wrote. With several proxies, or none, give your own.\n */\n clientIp?: (request: Request) => string | undefined\n /** `fetch` to reach Cauthy with. Default: the global one. */\n fetch?: typeof fetch\n /**\n * How long a call to Cauthy may take before it fails, in milliseconds.\n * `system`: the application's own calls (`auth.system`, and the proof its\n * sign-ins carry). Default 2000. `request`: the sign-in screens' calls and\n * the code, refresh and userinfo calls of a session. Default 5000. The\n * push-approval event stream has no deadline.\n */\n timeouts?: { system?: number; request?: number }\n /**\n * Run by contract: the issuer, the workspace, the clients and where the\n * application's keys are published all come from its link in the network's\n * context graph, and none of the CAUTHY_* variables may be set. See\n * \"Connect a system by contract\".\n *\n * createCauthy({ contract: { graph, system: 'acme-crm' }, keys: postgresKeyStore(query) })\n */\n contract?: import('./contract').ContractOptions\n /** Where the application keeps its own key, with a contract (@cauthyhq/node/keys). */\n keys?: import('./keys').KeyStore\n /**\n * The one secret of a contracted application: every key it needs is\n * derived from it. 32 random bytes or more. Default: `PACT_ROOT_KEY`.\n */\n rootKey?: string\n /** The root key before the current one, while rotating it. Default: `PACT_ROOT_KEY_PREVIOUS`. */\n rootKeyPrevious?: string\n /**\n * Where `auth.webhook` remembers the deliveries it took, so one sent twice\n * is refused. Default: this process's memory. Give a shared one (a table)\n * when the application runs on several instances.\n */\n webhookReplay?: import('./webhook').ReplayStore\n}\n\n/** The person signed in. Every moment is UTC. */\nexport interface SessionUser {\n id: string\n email: string\n name: string\n emailVerified: boolean\n /**\n * True when the account ran out of time to confirm its address: let it read\n * and refuse its writes.\n */\n restricted: boolean\n /** When an unconfirmed address stops being enough (ISO 8601, UTC), if there is a deadline. */\n emailVerifyBy?: string\n /** The language the person reads in (\"es\", \"en-US\"), when Cauthy knows it. */\n locale?: string\n /** A guest: a visitor's session before they sign up. No email; `account.upgrade` makes it an account, keeping `id`. */\n isAnonymous?: boolean\n}\n\n/** A value JSON can carry. What crosses from server to browser is typed as this, never `unknown`. */\nexport type JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue }\n\n/** An organization the person belongs to, with their role in it. */\nexport interface SessionOrganization {\n id: string\n system: string\n name: string\n kind: string\n externalId?: string\n role: string\n /** What the system keeps about the organization, as it filed it in Cauthy. */\n metadata: Record<string, JsonValue>\n /**\n * What the person may do here, as \"resource:action\" entries (\"*\" is any):\n * their role's permissions, what it inherits, and anything given to them\n * apart from it. Absent when the system keeps no catalog of roles in Cauthy.\n * Check with `can(organization, resource, action)`.\n */\n permissions?: string[]\n /** What the person is denied here, whatever else allows it. */\n denied?: string[]\n /**\n * Permissions that hold only when their condition does (an amount under a\n * limit, the person's own records…). They are in neither list above: `can`\n * answers no for them, and `auth.check` asks Cauthy. Present, maybe empty,\n * whenever `permissions` is.\n */\n conditional?: ConditionalPermission[]\n}\n\n/** A permission with a condition: Cauthy decides it per question (`auth.check`). */\nexport interface ConditionalPermission {\n resource: string\n action: string\n effect: 'allow' | 'deny'\n}\n\n/** What to ask Cauthy about: an action on a resource, with what the conditions read of it. */\nexport interface CheckQuestion {\n resource: string\n action: string\n /** What a condition reads as `resource.attrs`: `{ amount: 500 }`. */\n attributes?: Record<string, unknown>\n /** The record's id, as a condition reads it (`resource.id`). */\n resourceId?: string\n /** Whose the record is (`resource.owner_id`): `resource.owner_id == subject.id`. */\n ownerId?: string\n}\n\n/** An organization, by its id or as the session has it. */\nexport type OrganizationRef = string | { id: string }\n\n/** The rule that decided a check. */\nexport interface CheckRule {\n /** A permission of the person's role, or an exception given to the person. */\n source: 'role' | 'exception'\n /** The role, when `source` is `role`. */\n role?: string\n kind: string\n resource: string\n action: string\n effect: 'allow' | 'deny'\n condition?: string\n}\n\n/** Cauthy's answer to a check. */\nexport interface CheckResult {\n allowed: boolean\n /** Why, in words: \"allowed by role accountant: invoices:approve\". */\n reason: string\n /** The rule that decided; absent when none did (nothing allows it). */\n rule?: CheckRule\n}\n\n/** A signed-in person, as the server sees them. */\nexport interface Session {\n user: SessionUser\n /** The Cauthy tenant the account is of. */\n tenantId: string\n /** The roles the account holds in that tenant. */\n roles: string[]\n /**\n * Cauthy's access token for this person: send it as `Authorization: Bearer`\n * to another service that verifies Cauthy's tokens. Never give it to the browser.\n */\n accessToken: string\n /** When the access token expires and the session is checked with Cauthy again (ISO 8601, UTC). */\n expiresAt: string\n}\n\n/** What the browser is told of a session: who, and their organizations — never a token. */\nexport interface PublicSession {\n user: SessionUser\n tenantId: string\n roles: string[]\n organizations: SessionOrganization[]\n expiresAt: string\n}\n\n/** Why there is no session. */\nexport type SessionStatus =\n | 'authenticated'\n | 'unauthenticated' // nobody is signed in\n | 'expired' // signed in, but the session must be renewed first (see `Cauthy.resolve`)\n\nexport class CauthyConfigError extends Error {\n constructor(message: string) {\n super(`@cauthyhq/node: ${message}`)\n this.name = 'CauthyConfigError'\n }\n}\n\n/** Why a CAUTHY_KEY was refused (key/cauthy-key.md §3, §8, §9). */\nexport type CauthyKeyReason = 'key_format' | 'key_version' | 'key_checksum' | 'key_issuer' | 'key_client' | 'key_secret'\n | 'key_contract_issuer' | 'key_previous_mismatch'\n\n/** A refused CAUTHY_KEY. The message names the reason, never the key. */\nexport class CauthyKeyError extends CauthyConfigError {\n constructor(readonly reason: CauthyKeyReason, message: string) {\n super(`CAUTHY_KEY: ${message} (${reason})`)\n this.name = 'CauthyKeyError'\n }\n}\n"],"mappings":"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","names":[]}
|
package/dist/mcp.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/mcp.ts","../src/verify.ts","../src/url.ts"],"sourcesContent":["// An MCP server (or any API) that AI agents use as one of your people.\n//\n// import { mcpAuth } from '@cauthyhq/node/mcp'\n// const auth = mcpAuth({ resource: 'https://mcp.example.com/mcp', scopes: ['books:read'] })\n//\n// // Fetch-style (Hono, Next.js, Workers, Bun, Deno):\n// export default { fetch: auth.protect((request, agent) => handleMcp(request, agent)) }\n//\n// // Express or plain Node, with the MCP TypeScript SDK (its transport reads req.auth):\n// app.use(auth.middleware())\n//\n// How an agent gets in, as the MCP specification lays it out: it calls the\n// server, which answers 401 and points at its protected resource metadata\n// (RFC 9728); that names Cauthy as the authorization server; the agent\n// registers itself there, sends the person to sign in and allow it, and comes\n// back with a token issued for this server alone (its `aud`). This module is\n// the server's half: the metadata, the 401 that points at it, and checking\n// the token. The resource must be registered in Cauthy's dashboard (AI\n// agents), with the same address, for the workspace whose people use it.\n//\n// A server that calls another of the workspace's servers for the person\n// trades the agent's token for one for that server (token exchange, RFC 8693):\n//\n// const auth = mcpAuth({ resource, clientId, clientSecret }) // from the dashboard\n// const { accessToken } = await auth.exchange(agent, 'https://api.example.com', ['orders:read'])\n//\n// The workspace's admins must have delegated that call in the dashboard, and\n// the person allowed it when they connected the agent.\n\nimport { jwtVerify, type JWTVerifyGetKey } from 'jose'\nimport { keysOf } from './verify'\nimport { withoutTrailingSlashes } from './url'\n\n/** The person an agent acts for, and what they allowed it. */\nexport interface AgentIdentity {\n /** The person. */\n userId: string\n tenantId: string\n email?: string\n /** The person's roles in the workspace. */\n roles: string[]\n /** What the person allowed the agent to do here. */\n scopes: string[]\n /** The agent (its client_id at Cauthy). */\n clientId: string\n /** UTC, ISO 8601. */\n expiresAt: string\n /** The token as it arrived. */\n token: string\n /**\n * Set when the token came by token exchange: the server that called this\n * one for the person (the `act` claim's subject).\n */\n actor?: string\n}\n\nexport interface McpAuthOptions {\n /** This server's address, as registered in Cauthy. Default: `CAUTHY_MCP_RESOURCE`. */\n resource?: string\n /** Cauthy's address. Default: `CAUTHY_ISSUER`. */\n issuer?: string\n /** Scopes every call needs. Default: none. */\n scopes?: string[]\n /** The scopes this server understands, to tell agents (`scopes_supported`). Default: `scopes`. */\n scopesSupported?: string[]\n /** A name for the server, shown by some agents. */\n resourceName?: string\n /** Cauthy's keys. Default: fetched from the issuer and cached. */\n jwks?: JWTVerifyGetKey\n /** The moment to verify at. Default: now. */\n now?: () => Date\n /**\n * Ask Cauthy, besides checking the token here, whether the person still\n * allows the agent (RFC 7662). Then an agent is cut off the moment it is\n * disconnected, not when its token runs out, at the price of a call to\n * Cauthy per token every `cacheSeconds` (default 30). Default: off.\n */\n introspect?: boolean | { cacheSeconds?: number }\n /** The fetch to call Cauthy with. Default: the global one. */\n fetch?: typeof fetch\n /** How long a call to Cauthy (introspection, token exchange) may take, in milliseconds. Default: 5000. */\n timeoutMs?: number\n /**\n * This server's client_id and secret at Cauthy, for token exchange (AI\n * agents → the server → Server credentials). Defaults:\n * `CAUTHY_MCP_CLIENT_ID`, `CAUTHY_MCP_CLIENT_SECRET`.\n */\n clientId?: string\n clientSecret?: string\n}\n\n/** What a token exchange answers. */\nexport interface ExchangedToken {\n /** The token for the other server, as the person. Send it as `Authorization: Bearer`. */\n accessToken: string\n /** What it allows there. */\n scopes: string[]\n /** UTC, ISO 8601. Never later than the agent's own token. */\n expiresAt: string\n}\n\n/** A token exchange Cauthy refused, with the OAuth error it answered. */\nexport class CauthyExchangeError extends Error {\n constructor(\n readonly status: number,\n readonly code: string,\n readonly description: string,\n ) {\n super(`@cauthyhq/node: token exchange refused (${code}): ${description}`)\n this.name = 'CauthyExchangeError'\n }\n}\n\n/** A token refused: `status` is 401 (no token, or not a good one) or 403 (a scope missing). */\nexport class McpAuthError extends Error {\n constructor(\n readonly status: 401 | 403,\n readonly code: 'invalid_request' | 'invalid_token' | 'insufficient_scope',\n readonly description: string,\n ) {\n super(`@cauthyhq/node: ${description}`)\n this.name = 'McpAuthError'\n }\n}\n\n/** What the MCP TypeScript SDK reads from `req.auth` (its AuthInfo). */\nexport interface McpAuthInfo {\n token: string\n clientId: string\n scopes: string[]\n /** Seconds since the epoch. */\n expiresAt: number\n resource: URL\n extra: { userId: string; tenantId: string; email?: string; roles: string[] }\n}\n\nexport interface McpAuth {\n /** This server's address, in the form Cauthy compares it in. */\n resource: string\n /** Where the protected resource metadata is served. */\n metadataUrl: string\n /** The protected resource metadata (RFC 9728). */\n metadata(): Record<string, unknown>\n /** The metadata's paths: `/.well-known/oauth-protected-resource` plus this server's path. */\n metadataPaths: string[]\n /** Verifies the `Authorization: Bearer` of a request (or a token). Throws `McpAuthError`. */\n verify(from: string | Request | Headers, needs?: string[]): Promise<AgentIdentity>\n /** The answer to a refused token: 401 or 403, with the `WWW-Authenticate` that points an agent at Cauthy. */\n challenge(error?: McpAuthError): Response\n /** The metadata, when the request asks for it; `null` otherwise. */\n handleMetadata(request: Request): Response | null\n /** Wraps a fetch-style handler: serves the metadata, refuses what has no good token, passes the rest with who it is. */\n protect(handler: (request: Request, agent: AgentIdentity) => Response | Promise<Response>, needs?: string[]): (request: Request) => Promise<Response>\n /**\n * Connect-style middleware (Express, Node's http): serves the metadata,\n * refuses what has no good token, and sets `req.agent` and `req.auth` —\n * what the MCP TypeScript SDK's transport hands its tools as `authInfo`.\n */\n middleware(needs?: string[]): (req: NodeRequest, res: NodeResponse, next: (err?: unknown) => void) => void\n /**\n * Trades the agent's token this server received for one for another of the\n * workspace's servers, to call it for the person (RFC 8693). Needs this\n * server's `clientId` and `clientSecret`. Answers are kept until shortly\n * before they expire. Throws `CauthyExchangeError` when Cauthy refuses.\n */\n exchange(from: string | AgentIdentity, target: string, scopes?: string[]): Promise<ExchangedToken>\n}\n\n// The slice of Node's (and Express's) request and response this needs.\ninterface NodeRequest {\n method?: string\n url?: string\n originalUrl?: string\n headers: Record<string, string | string[] | undefined>\n auth?: McpAuthInfo\n agent?: AgentIdentity\n}\ninterface NodeResponse {\n statusCode: number\n setHeader(name: string, value: string): unknown\n end(body?: string): unknown\n}\n\nfunction env(name: string): string | undefined {\n return typeof process !== 'undefined' ? process.env?.[name] : undefined\n}\n\n/**\n * The address in the one form Cauthy keeps it: scheme and host in lower case,\n * no default port, no trailing slash, no query, no fragment.\n */\nexport function canonicalResource(raw: string): string {\n let u: URL\n try {\n u = new URL(raw.trim())\n } catch {\n throw new Error(`@cauthyhq/node: resource ${JSON.stringify(raw)} is not an absolute address`)\n }\n const local = u.hostname === 'localhost' || u.hostname === '127.0.0.1' || u.hostname === '[::1]'\n if (u.protocol !== 'https:' && !(u.protocol === 'http:' && local)) {\n throw new Error('@cauthyhq/node: resource must be an https address')\n }\n if (u.search !== '' || u.hash !== '') throw new Error('@cauthyhq/node: resource may not have a query or a fragment')\n return `${u.protocol}//${u.host}${withoutTrailingSlashes(u.pathname)}`\n}\n\nfunction bearer(from: string | Request | Headers): string {\n if (typeof from === 'string') return from.replace(/^Bearer\\s+/i, '').trim()\n const header = (from instanceof Headers ? from : from.headers).get('authorization') ?? ''\n return /^Bearer\\s+/i.test(header) ? header.replace(/^Bearer\\s+/i, '').trim() : ''\n}\n\nfunction isActor(v: unknown): v is { sub: string } {\n return typeof v === 'object' && v !== null && typeof (v as { sub?: unknown }).sub === 'string'\n}\n\nfunction quoted(v: string): string {\n return `\"${v.replace(/[\"\\\\]/g, '')}\"`\n}\n\n/** Protects an MCP server with Cauthy. */\nexport function mcpAuth(options: McpAuthOptions = {}): McpAuth {\n const rawResource = options.resource ?? env('CAUTHY_MCP_RESOURCE') ?? ''\n if (rawResource === '') throw new Error('@cauthyhq/node: resource (CAUTHY_MCP_RESOURCE) is not set')\n const resource = canonicalResource(rawResource)\n const issuer = withoutTrailingSlashes(options.issuer ?? env('CAUTHY_ISSUER') ?? '')\n if (issuer === '') throw new Error('@cauthyhq/node: issuer (CAUTHY_ISSUER) is not set')\n const timeoutMs = options.timeoutMs ?? 5000\n if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) throw new Error('@cauthyhq/node: timeoutMs must be a positive number of milliseconds')\n const required = options.scopes ?? []\n const url = new URL(resource)\n const path = url.pathname === '/' ? '' : url.pathname\n const metadataPaths = [...new Set([`/.well-known/oauth-protected-resource${path}`, '/.well-known/oauth-protected-resource'])]\n const metadataUrl = `${url.origin}${metadataPaths[0]}`\n\n const metadata = () => ({\n resource,\n authorization_servers: [issuer],\n bearer_methods_supported: ['header'],\n scopes_supported: options.scopesSupported ?? required,\n ...(options.resourceName ? { resource_name: options.resourceName } : {}),\n })\n\n const introspect = options.introspect ? { cacheMs: ((typeof options.introspect === 'object' ? options.introspect.cacheSeconds : undefined) ?? 30) * 1000 } : null\n const answers = new Map<string, { active: boolean; until: number }>()\n const stillAllowed = async (token: string, cacheMs: number): Promise<boolean> => {\n const now = Date.now()\n const kept = answers.get(token)\n if (kept && kept.until > now) return kept.active\n let active = false\n try {\n const res = await (options.fetch ?? fetch)(`${issuer}/v1/oauth/introspect`, {\n signal: AbortSignal.timeout(timeoutMs),\n method: 'POST',\n headers: { 'content-type': 'application/x-www-form-urlencoded', accept: 'application/json' },\n body: new URLSearchParams({ token }),\n })\n if (!res.ok) throw new Error(`introspection answered ${res.status}`)\n active = ((await res.json()) as { active?: unknown }).active === true\n } catch (err) {\n // Cauthy could not be asked: what it last said holds.\n if (kept) return kept.active\n throw new McpAuthError(401, 'invalid_token', `could not check the token with Cauthy: ${err instanceof Error ? err.message : 'unreachable'}`)\n }\n if (answers.size > 10_000) answers.clear()\n answers.set(token, { active, until: now + cacheMs })\n return active\n }\n\n const verify = async (from: string | Request | Headers, needs: string[] = []): Promise<AgentIdentity> => {\n const token = bearer(from)\n if (token === '') throw new McpAuthError(401, 'invalid_request', 'no access token')\n let claims: Record<string, unknown>\n try {\n const verified = await jwtVerify(token, options.jwks ?? keysOf(issuer), {\n algorithms: ['RS256'],\n issuer,\n audience: resource,\n requiredClaims: ['exp', 'sub'],\n currentDate: options.now?.(),\n })\n claims = verified.payload as Record<string, unknown>\n } catch (err) {\n throw new McpAuthError(401, 'invalid_token', err instanceof Error ? err.message : 'the token is not valid')\n }\n const clientId = typeof claims.client_id === 'string' ? claims.client_id : ''\n if (clientId === '') throw new McpAuthError(401, 'invalid_token', 'not a token issued to an agent')\n const scopes = typeof claims.scope === 'string' ? claims.scope.split(' ').filter(Boolean) : []\n if (introspect && !(await stillAllowed(token, introspect.cacheMs))) throw new McpAuthError(401, 'invalid_token', 'the token is no longer allowed: the agent was disconnected, or its access withdrawn')\n const missing = [...required, ...needs].filter((s) => !scopes.includes(s))\n if (missing.length > 0) throw new McpAuthError(403, 'insufficient_scope', `this needs ${missing.join(' ')}`)\n return {\n userId: String(claims.sub),\n tenantId: typeof claims.tid === 'string' ? claims.tid : '',\n ...(typeof claims.email === 'string' && claims.email !== '' ? { email: claims.email } : {}),\n roles: Array.isArray(claims.roles) ? claims.roles.filter((r): r is string => typeof r === 'string') : [],\n scopes,\n clientId,\n expiresAt: new Date((claims.exp as number) * 1000).toISOString(),\n token,\n ...(isActor(claims.act) ? { actor: claims.act.sub } : {}),\n }\n }\n\n const clientId = options.clientId ?? env('CAUTHY_MCP_CLIENT_ID') ?? ''\n const clientSecret = options.clientSecret ?? env('CAUTHY_MCP_CLIENT_SECRET') ?? ''\n const exchanged = new Map<string, ExchangedToken & { until: number }>()\n const exchange = async (from: string | AgentIdentity, target: string, scopes: string[] = []): Promise<ExchangedToken> => {\n if (clientId === '' || clientSecret === '') {\n throw new Error('@cauthyhq/node: token exchange needs clientId and clientSecret (CAUTHY_MCP_CLIENT_ID, CAUTHY_MCP_CLIENT_SECRET)')\n }\n const subject = typeof from === 'string' ? bearer(from) : from.token\n const to = canonicalResource(target)\n const key = `${subject}\\n${to}\\n${scopes.join(' ')}`\n const kept = exchanged.get(key)\n if (kept && kept.until > Date.now()) return { accessToken: kept.accessToken, scopes: kept.scopes, expiresAt: kept.expiresAt }\n let res: Response\n try {\n res = await (options.fetch ?? fetch)(`${issuer}/v1/oauth/token`, {\n signal: AbortSignal.timeout(timeoutMs),\n method: 'POST',\n headers: {\n 'content-type': 'application/x-www-form-urlencoded',\n accept: 'application/json',\n authorization: `Basic ${btoa(`${encodeURIComponent(clientId)}:${encodeURIComponent(clientSecret)}`)}`,\n },\n body: new URLSearchParams({\n grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange',\n subject_token: subject,\n subject_token_type: 'urn:ietf:params:oauth:token-type:access_token',\n resource: to,\n ...(scopes.length > 0 ? { scope: scopes.join(' ') } : {}),\n }),\n })\n } catch (err) {\n throw new CauthyExchangeError(503, 'temporarily_unavailable', `Cauthy could not be reached: ${err instanceof Error ? err.message : 'unreachable'}`)\n }\n const body = (await res.json().catch(() => ({}))) as Record<string, unknown>\n if (!res.ok || typeof body.access_token !== 'string') {\n throw new CauthyExchangeError(res.status, typeof body.error === 'string' ? body.error : 'server_error',\n typeof body.error_description === 'string' ? body.error_description : `Cauthy answered ${res.status}`)\n }\n const expiresIn = typeof body.expires_in === 'number' ? body.expires_in : 0\n const out: ExchangedToken = {\n accessToken: body.access_token,\n scopes: typeof body.scope === 'string' ? body.scope.split(' ').filter(Boolean) : [],\n expiresAt: new Date(Date.now() + expiresIn * 1000).toISOString(),\n }\n if (exchanged.size > 10_000) exchanged.clear()\n // Kept until 30 seconds before it runs out, so it is never sent stale.\n exchanged.set(key, { ...out, until: Date.now() + Math.max(0, expiresIn - 30) * 1000 })\n return out\n }\n\n const authenticate = (error?: McpAuthError): string => {\n const parts = [`resource_metadata=${quoted(metadataUrl)}`]\n if (error && error.code !== 'invalid_request') {\n parts.push(`error=${quoted(error.code)}`, `error_description=${quoted(error.description)}`)\n }\n // What to ask the person for: said when there is no token or it falls short.\n const scope = error?.code === 'invalid_token' ? [] : required\n if (scope.length > 0) parts.push(`scope=${quoted(scope.join(' '))}`)\n return `Bearer ${parts.join(', ')}`\n }\n\n const challenge = (error?: McpAuthError): Response => {\n const status = error?.status ?? 401\n return new Response(JSON.stringify({ error: error?.code ?? 'invalid_request', error_description: error?.description ?? 'an access token is required' }), {\n status,\n headers: { 'content-type': 'application/json', 'www-authenticate': authenticate(error) },\n })\n }\n\n const metadataResponse = () =>\n new Response(JSON.stringify(metadata()), {\n headers: { 'content-type': 'application/json', 'cache-control': 'public, max-age=3600', 'access-control-allow-origin': '*' },\n })\n\n const isMetadata = (pathname: string, method = 'GET') => method === 'GET' && metadataPaths.includes(withoutTrailingSlashes(pathname) || '/')\n\n const handleMetadata = (request: Request): Response | null =>\n isMetadata(new URL(request.url).pathname, request.method) ? metadataResponse() : null\n\n const toAuthInfo = (a: AgentIdentity): McpAuthInfo => ({\n token: a.token,\n clientId: a.clientId,\n scopes: a.scopes,\n expiresAt: Math.floor(new Date(a.expiresAt).getTime() / 1000),\n resource: new URL(resource),\n extra: { userId: a.userId, tenantId: a.tenantId, ...(a.email ? { email: a.email } : {}), roles: a.roles },\n })\n\n return {\n resource,\n metadataUrl,\n metadataPaths,\n metadata,\n verify,\n challenge,\n handleMetadata,\n protect(handler, needs) {\n return async (request) => {\n const meta = handleMetadata(request)\n if (meta) return meta\n let agent: AgentIdentity\n try {\n agent = await verify(request, needs)\n } catch (err) {\n if (err instanceof McpAuthError) return challenge(err)\n throw err\n }\n return handler(request, agent)\n }\n },\n exchange,\n middleware(needs) {\n return (req, res, next) => {\n const pathname = (req.originalUrl ?? req.url ?? '/').split('?')[0] ?? '/'\n if (isMetadata(pathname, req.method)) {\n res.statusCode = 200\n res.setHeader('content-type', 'application/json')\n res.setHeader('cache-control', 'public, max-age=3600')\n res.setHeader('access-control-allow-origin', '*')\n res.end(JSON.stringify(metadata()))\n return\n }\n const raw = req.headers.authorization\n const header = Array.isArray(raw) ? (raw[0] ?? '') : (raw ?? '')\n verify(/^Bearer\\s+/i.test(header) ? header : '', needs).then(\n (agent) => {\n req.agent = agent\n req.auth = toAuthInfo(agent)\n next()\n },\n (err: unknown) => {\n if (!(err instanceof McpAuthError)) return next(err)\n res.statusCode = err.status\n res.setHeader('content-type', 'application/json')\n res.setHeader('www-authenticate', authenticate(err))\n res.end(JSON.stringify({ error: err.code, error_description: err.description }))\n },\n )\n }\n },\n }\n}\n","// A backend that is handed a Cauthy access token: is it real, and whose is it?\n//\n// const who = await verifyToken(request) // or the token itself\n// who.userId, who.tenantId, who.roles\n//\n// Cauthy signs its tokens with a key it publishes (/.well-known/jwks.json), so\n// checking one needs no secret and no call to Cauthy per request: the keys are\n// fetched once and cached. Throws `CauthyTokenError` for anything else: answer\n// it 401, and read `reason`, one of the closed list of codes in the spec's\n// verification/rules.md.\n\nimport { compactVerify, createLocalJWKSet, type CompactVerifyGetKey, type JSONWebKeySet, type JWTVerifyGetKey } from 'jose'\nimport { parseStrict } from './json'\nimport { CauthyConfigError } from './types'\nimport { withoutTrailingSlashes } from './url'\n\n/** Who a verified access token belongs to. */\nexport interface TokenIdentity {\n /** The person. For a system's own token, the client it identifies as. */\n userId: string\n tenantId: string\n email?: string\n /** What the token says of the address, when it says (a boolean; absent otherwise). */\n emailVerified?: boolean\n roles: string[]\n /** Set when the token is a system's own and not a person's (only with `allowSystem`). */\n system?: string\n /** Set when the token is a guest's (only with `allowAnonymous`). */\n anonymous?: true\n /** The client a person's token was issued through (`azp`); absent when none was named. */\n authorizedParty?: string\n /**\n * The token's `client_id`: on a client's own token (only with\n * `allowClientTokens`), the client's public id; on an AI agent's, the agent's.\n */\n clientId?: string\n /** UTC, ISO 8601. */\n expiresAt: string\n /** The token as it arrived, to pass on. */\n token: string\n}\n\n/** Why a token was refused: the closed list of verification/rules.md, and `no_token` when none was given. */\nexport type TokenRefusal =\n | 'no_token' | 'too_large' | 'malformed' | 'alg_not_allowed' | 'unsupported_header' | 'missing_kid' | 'unknown_kid' | 'weak_key'\n | 'bad_signature' | 'missing_claim' | 'invalid_claim' | 'wrong_issuer' | 'wrong_audience' | 'expired' | 'not_yet_valid'\n | 'issued_in_future' | 'wrong_tenant' | 'system_token' | 'client_token' | 'anonymous_token' | 'wrong_client'\n\n/**\n * Cauthy's keys could not be fetched (or are not a key set): the token may be\n * good, and the application cannot tell. Answer 503, not 401. It is not a\n * `CauthyTokenError`: it says nothing about the token.\n */\nexport class CauthyUnavailableError extends Error {\n constructor(detail: string) {\n super(`@cauthyhq/node: Cauthy's keys are unavailable: ${detail}`)\n this.name = 'CauthyUnavailableError'\n }\n}\n\nexport class CauthyTokenError extends Error {\n constructor(readonly reason: TokenRefusal, readonly detail?: string) {\n super(`@cauthyhq/node: token refused: ${reason}${detail ? ` (${detail})` : ''}`)\n this.name = 'CauthyTokenError'\n }\n}\n\nexport interface VerifyTokenOptions {\n /** Cauthy's address. Default: `CAUTHY_ISSUER`. */\n issuer?: string\n /**\n * Further issuers a token may name, e.g. a workspace's custom domain. The\n * keys always come from `issuer`. Default: none.\n */\n issuers?: string[]\n /** The audience the tokens must carry. Required: this or `CAUTHY_AUDIENCE`. */\n audience?: string\n /** The workspace's id, a lowercase UUID. Required: this or `CAUTHY_TENANT_ID`. */\n tenantId?: string\n /** Cauthy's keys. Default: fetched from the issuer and cached (see `remoteKeys`). */\n jwks?: JWTVerifyGetKey\n /** The fetch to get the keys with. Default: the global one. */\n fetch?: typeof fetch\n /** The moment to verify at. Default: now. */\n now?: Date\n /** Clock tolerance in seconds, 0 to 60. Default: 30. */\n leeway?: number\n /**\n * Accept a system's own token (a `sys` claim) too: for an endpoint another\n * system calls. Default: false, so a system's token never stands for a\n * person (rule R13 of the spec's verification/rules.md).\n */\n allowSystem?: boolean\n /** Accept a guest's token (`anon: true`). Default: false (rule R14). */\n allowAnonymous?: boolean\n /**\n * Accept a client's own token (a `token_use` claim, from a\n * `client_credentials` grant) too: for a backend that serves machines as\n * well as people. Default: false, so a machine never passes as a person\n * (rule R13). The identity then carries `clientId`.\n */\n allowClientTokens?: boolean\n /**\n * The client this backend signs people in through. When set, a person's\n * token issued through another client (an `azp` naming another) is refused\n * with `wrong_client` (rule R15); a token with no `azp` passes. In contract\n * mode it is the link's sign-in client. Default: none, and `azp` is not checked.\n */\n clientId?: string\n}\n\nconst MAX_TOKEN_BYTES = 8192\nconst UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/\nconst NIL_UUID = '00000000-0000-0000-0000-000000000000'\nconst SEGMENT = /^[A-Za-z0-9_-]+$/\n\nconst MAX_KEYS_BYTES = 64 * 1024\nconst KEYS_TIMEOUT_MS = 5000\nconst KEYS_COOLDOWN_MS = 30_000\nconst KEYS_DEFAULT_TTL_S = 300\nconst KEYS_MIN_TTL_S = 60\nconst KEYS_MAX_TTL_S = 3600\n\n/**\n * Cauthy's published keys (K1 to K5 of verification/rules.md): fetched with a\n * GET that follows no redirect, within 5 seconds, from `<issuer>/.well-known/jwks.json`;\n * only a 200 of at most 64 KiB counts. Kept for the answer's max-age (at most\n * an hour and at least a minute, 5 minutes when it names none); a `kid` the set lacks fetches again\n * only if the last fetch is older than 30 seconds; one fetch at a time; a\n * failed fetch keeps the last good set, and with none it is\n * `CauthyUnavailableError`.\n */\nexport function remoteKeys(issuer: string, fetchKeys: typeof fetch = fetch): JWTVerifyGetKey {\n let set: JWTVerifyGetKey | undefined\n let startedAt = 0\n let expiresAt = 0\n let inflight: Promise<void> | undefined\n\n const load = async (): Promise<void> => {\n startedAt = Date.now()\n try {\n const res = await fetchKeys(`${issuer}/.well-known/jwks.json`, { redirect: 'manual', signal: AbortSignal.timeout(KEYS_TIMEOUT_MS), headers: { accept: 'application/json' } })\n if (res.status !== 200) throw new Error(`answered ${res.status}`)\n const bytes = await readCapped(res, MAX_KEYS_BYTES)\n const parsed = JSON.parse(new TextDecoder().decode(bytes)) as JSONWebKeySet\n if (!parsed || !Array.isArray(parsed.keys)) throw new Error('not a key set')\n // K6: two usable keys with one kid make the whole set untrustworthy.\n const seen = new Set<string>()\n for (const k of parsed.keys) {\n const usable = k?.kty === 'RSA' && (k.use === undefined || k.use === 'sig') && (k.alg === undefined || k.alg === 'RS256')\n if (!usable || typeof k.kid !== 'string') continue\n if (seen.has(k.kid)) throw new Error(`two usable keys share the kid ${JSON.stringify(k.kid)}`)\n seen.add(k.kid)\n }\n set = createLocalJWKSet(parsed)\n const maxAge = /max-age=(\\d+)/.exec(res.headers.get('cache-control') ?? '')\n // At least a minute: max-age=0 must not mean a fetch for every token.\n const ttl = Math.max(KEYS_MIN_TTL_S, Math.min(maxAge ? Number(maxAge[1]) : KEYS_DEFAULT_TTL_S, KEYS_MAX_TTL_S))\n expiresAt = Date.now() + ttl * 1000\n } catch (err) {\n // The last good set stands, and is not asked for again before the cooldown:\n // an outage must not cost a wait on every verification. With none, the\n // application cannot tell.\n if (set === undefined) throw new CauthyUnavailableError(err instanceof Error ? err.message : 'unreachable')\n expiresAt = Math.max(expiresAt, Date.now() + KEYS_COOLDOWN_MS)\n }\n }\n const refresh = (): Promise<void> => {\n inflight ??= load().finally(() => {\n inflight = undefined\n })\n return inflight\n }\n\n return async (header, token) => {\n if (set === undefined || Date.now() >= expiresAt) await refresh()\n try {\n return await set!(header, token)\n } catch (err) {\n const unknown = (err as { code?: unknown } | null)?.code === 'ERR_JWKS_NO_MATCHING_KEY'\n if (unknown && Date.now() - startedAt >= KEYS_COOLDOWN_MS) {\n await refresh()\n return set!(header, token)\n }\n throw err\n }\n }\n}\n\nasync function readCapped(res: Response, max: number): Promise<Uint8Array> {\n const reader = res.body?.getReader()\n if (!reader) return new Uint8Array()\n const chunks: Uint8Array[] = []\n let size = 0\n for (;;) {\n const { done, value } = await reader.read()\n if (done) break\n size += value.length\n if (size > max) {\n await reader.cancel()\n throw new Error(`over ${max} bytes`)\n }\n chunks.push(value)\n }\n const out = new Uint8Array(size)\n let at = 0\n for (const c of chunks) {\n out.set(c, at)\n at += c.length\n }\n return out\n}\n\nconst published = new Map<string, JWTVerifyGetKey>()\nconst withFetch = new WeakMap<typeof fetch, Map<string, JWTVerifyGetKey>>()\n\nfunction remoteKeysFor(issuer: string, fetchKeys: typeof fetch): JWTVerifyGetKey {\n let byIssuer = withFetch.get(fetchKeys)\n if (!byIssuer) withFetch.set(fetchKeys, (byIssuer = new Map()))\n let set = byIssuer.get(issuer)\n if (!set) byIssuer.set(issuer, (set = remoteKeys(issuer, fetchKeys)))\n return set\n}\n\n/** Cauthy's published keys, fetched once per issuer and kept (see `remoteKeys`). */\nexport function keysOf(issuer: string): JWTVerifyGetKey {\n let set = published.get(issuer)\n if (set === undefined) {\n set = remoteKeys(issuer)\n published.set(issuer, set)\n }\n return set\n}\n\nfunction env(name: string): string | undefined {\n return typeof process !== 'undefined' ? process.env?.[name] : undefined\n}\n\nfunction bearer(from: string | Request | Headers): string {\n // A raw token is taken as it is: surrounding whitespace is the caller's mistake (R2).\n if (typeof from === 'string') return from.replace(/^Bearer\\s+/i, '')\n const header = (from instanceof Headers ? from : from.headers).get('authorization') ?? ''\n return /^Bearer\\s+/i.test(header) ? header.replace(/^Bearer\\s+/i, '').trim() : ''\n}\n\nfunction issuerAddress(value: string, what: string): string {\n let url: URL\n try {\n url = new URL(value)\n } catch {\n throw new CauthyConfigError(`${what} must be an https address, e.g. https://id.example.com`)\n }\n // http is for a developer's own machine: localhost, 127.0.0.1, *.localhost and *.test.\n const local = url.hostname === 'localhost' || url.hostname === '127.0.0.1' || url.hostname.endsWith('.localhost') || url.hostname.endsWith('.test')\n if (url.protocol !== 'https:' && !(url.protocol === 'http:' && local)) throw new CauthyConfigError(`${what} must be https (http only for localhost, *.localhost and *.test)`)\n if (url.search !== '' || url.hash !== '') throw new CauthyConfigError(`${what} must have no query or fragment`)\n return withoutTrailingSlashes(value)\n}\n\nconst isObject = (v: unknown): v is Record<string, unknown> => typeof v === 'object' && v !== null && !Array.isArray(v)\n\nfunction decodeJson(segment: string): unknown {\n const padded = segment.replace(/-/g, '+').replace(/_/g, '/')\n return parseStrict(Buffer.from(padded, 'base64').toString('utf8'))\n}\n\n/**\n * A verifier for access tokens, configured once. Refuses to be made without\n * an issuer, an audience and a workspace: a verifier that checks none of\n * them would accept a token of any other application or workspace.\n */\nexport function createTokenVerifier(options: VerifyTokenOptions = {}): (from: string | Request | Headers) => Promise<TokenIdentity> {\n const issuerGiven = options.issuer ?? env('CAUTHY_ISSUER') ?? ''\n if (issuerGiven === '') throw new CauthyConfigError('issuer (CAUTHY_ISSUER) is not set')\n const issuer = issuerAddress(issuerGiven, 'issuer')\n const issuers = [issuer, ...(options.issuers ?? []).map((i) => issuerAddress(i, 'issuers'))]\n const audience = options.audience ?? env('CAUTHY_AUDIENCE') ?? ''\n if (audience === '') throw new CauthyConfigError('audience (CAUTHY_AUDIENCE) is not set: without it a token issued for another application would be accepted')\n const tenant = options.tenantId ?? env('CAUTHY_TENANT_ID') ?? ''\n if (tenant === '') throw new CauthyConfigError('tenantId (CAUTHY_TENANT_ID) is not set: without it a token of another workspace would be accepted')\n if (!UUID.test(tenant) || tenant === NIL_UUID) throw new CauthyConfigError('tenantId must be the workspace\\'s id, a lowercase UUID that is not the nil UUID')\n const leeway = options.leeway ?? 30\n if (!Number.isFinite(leeway) || leeway < 0 || leeway > 60) throw new CauthyConfigError('leeway must be 0 to 60 seconds')\n\n return async (from) => {\n const token = bearer(from)\n if (token === '') throw new CauthyTokenError('no_token')\n // R1\n if (Buffer.byteLength(token) > MAX_TOKEN_BYTES) throw new CauthyTokenError('too_large')\n // R2\n const parts = token.split('.')\n if (parts.length !== 3 || !parts.every((p) => SEGMENT.test(p) || p === '')) throw new CauthyTokenError('malformed', 'not three base64url segments')\n let header: unknown\n try {\n header = decodeJson(parts[0])\n } catch {\n throw new CauthyTokenError('malformed', 'the header is not JSON')\n }\n if (!isObject(header)) throw new CauthyTokenError('malformed', 'the header is not an object')\n // R3, R4, R5: the algorithm is the verifier's, never the token's.\n if (header.alg !== 'RS256') throw new CauthyTokenError('alg_not_allowed')\n if ('crit' in header) throw new CauthyTokenError('unsupported_header')\n // R4: typ is not required, but when present it is JWT or at+jwt, in any case.\n if ('typ' in header && !(typeof header.typ === 'string' && ['jwt', 'at+jwt'].includes(header.typ.toLowerCase()))) {\n throw new CauthyTokenError('unsupported_header', 'typ')\n }\n if (typeof header.kid !== 'string' || header.kid === '') throw new CauthyTokenError('missing_kid')\n\n // R5, R6\n let payloadBytes: Uint8Array\n try {\n const verified = await compactVerify(token, (options.jwks ?? (options.fetch ? remoteKeysFor(issuer, options.fetch) : keysOf(issuer))) as CompactVerifyGetKey, { algorithms: ['RS256'] })\n payloadBytes = verified.payload\n } catch (err) {\n // By code, not by class: the keys may come from another copy of jose.\n const code = (err as { code?: unknown } | null)?.code\n if (code === 'ERR_JWKS_NO_MATCHING_KEY') throw new CauthyTokenError('unknown_kid')\n if (code === 'ERR_JWS_SIGNATURE_VERIFICATION_FAILED') throw new CauthyTokenError('bad_signature')\n if (err instanceof TypeError && /2048/.test(err.message)) throw new CauthyTokenError('weak_key')\n if (code === 'ERR_JWS_INVALID') throw new CauthyTokenError('malformed', err instanceof Error ? err.message : undefined)\n if (err instanceof CauthyUnavailableError) throw err\n throw new CauthyTokenError('bad_signature', err instanceof Error ? err.message : undefined)\n }\n\n // R7\n let claims: unknown\n try {\n claims = parseStrict(new TextDecoder().decode(payloadBytes), ['exp', 'iat', 'nbf'])\n } catch {\n throw new CauthyTokenError('malformed', 'the payload is not JSON')\n }\n if (!isObject(claims)) throw new CauthyTokenError('malformed', 'the payload is not an object')\n for (const name of ['iss', 'sub', 'aud', 'exp', 'iat', 'tid']) {\n if (claims[name] === undefined) throw new CauthyTokenError('missing_claim', name)\n }\n // R8: no claim is null but roles.\n const bad = (name: string): never => {\n throw new CauthyTokenError('invalid_claim', name)\n }\n if (typeof claims.iss !== 'string') bad('iss')\n if (typeof claims.sub !== 'string' || !UUID.test(claims.sub)) bad('sub')\n if (typeof claims.tid !== 'string' || !UUID.test(claims.tid)) bad('tid')\n const aud = claims.aud\n if (!(typeof aud === 'string' || (Array.isArray(aud) && aud.length > 0 && aud.every((a) => typeof a === 'string')))) bad('aud')\n for (const name of ['exp', 'iat', 'nbf']) if (claims[name] !== undefined && !Number.isInteger(claims[name])) bad(name)\n if (claims.roles !== undefined && claims.roles !== null && !(Array.isArray(claims.roles) && claims.roles.every((r) => typeof r === 'string'))) bad('roles')\n if (claims.email !== undefined && typeof claims.email !== 'string') bad('email')\n for (const name of ['email_verified', 'anon']) if (claims[name] !== undefined && typeof claims[name] !== 'boolean') bad(name)\n for (const name of ['token_use', 'azp']) if (claims[name] !== undefined && typeof claims[name] !== 'string') bad(name)\n\n // R9, R10\n if (!issuers.includes(claims.iss as string)) throw new CauthyTokenError('wrong_issuer')\n if (!(Array.isArray(aud) ? aud.includes(audience) : aud === audience)) throw new CauthyTokenError('wrong_audience')\n // R11\n const now = (options.now ?? new Date()).getTime() / 1000\n if (now >= (claims.exp as number) + leeway) throw new CauthyTokenError('expired')\n if (claims.nbf !== undefined && now < (claims.nbf as number) - leeway) throw new CauthyTokenError('not_yet_valid')\n if (now < (claims.iat as number) - leeway) throw new CauthyTokenError('issued_in_future')\n // R12\n if (claims.tid !== tenant) throw new CauthyTokenError('wrong_tenant')\n // R13, R14\n // Its presence is the mark, whatever the value (null and false included).\n if ('sys' in claims && !options.allowSystem) throw new CauthyTokenError('system_token')\n // A client's own token stands for a machine: present is the mark, as for sys.\n if ('token_use' in claims && !options.allowClientTokens) throw new CauthyTokenError('client_token')\n if (claims.anon === true && !options.allowAnonymous) throw new CauthyTokenError('anonymous_token')\n // R15: a person's token issued through another client than the one named.\n if (options.clientId && typeof claims.azp === 'string' && claims.azp !== '' && claims.azp !== options.clientId) throw new CauthyTokenError('wrong_client')\n\n return {\n userId: claims.sub as string,\n tenantId: claims.tid as string,\n ...(typeof claims.email === 'string' && claims.email !== '' ? { email: claims.email } : {}),\n ...(typeof claims.email_verified === 'boolean' ? { emailVerified: claims.email_verified } : {}),\n roles: Array.isArray(claims.roles) ? (claims.roles as string[]) : [],\n ...(typeof claims.sys === 'string' && claims.sys !== '' ? { system: claims.sys } : {}),\n ...(claims.anon === true ? { anonymous: true as const } : {}),\n ...(typeof claims.azp === 'string' && claims.azp !== '' ? { authorizedParty: claims.azp } : {}),\n ...(typeof claims.client_id === 'string' && claims.client_id !== '' ? { clientId: claims.client_id } : {}),\n expiresAt: new Date((claims.exp as number) * 1000).toISOString(),\n token,\n }\n }\n}\n\n/**\n * Verifies an access token (or the `Authorization: Bearer` of a request) and\n * answers who it belongs to, by the rules of the spec's verification/rules.md.\n * Needs an issuer, an audience and a workspace (see `createTokenVerifier`,\n * which checks them once, at startup).\n */\nexport async function verifyToken(from: string | Request | Headers, options: VerifyTokenOptions = {}): Promise<TokenIdentity> {\n return createTokenVerifier(options)(from)\n}\n","/**\n * The address without its trailing slashes: `https://auth.example.com/` and\n * `https://auth.example.com` name the same issuer. A loop, not `/\\/+$/`,\n * which backtracks over a long run of slashes.\n */\nexport function withoutTrailingSlashes(s: string): string {\n let end = s.length\n while (end > 0 && s.charCodeAt(end - 1) === 47) end--\n return s.slice(0, end)\n}\n"],"mappings":"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","names":["import_jose","clientId"]}
|
|
1
|
+
{"version":3,"sources":["../src/mcp.ts","../src/verify.ts","../src/url.ts"],"sourcesContent":["// An MCP server (or any API) that AI agents use as one of your people.\n//\n// import { mcpAuth } from '@cauthyhq/node/mcp'\n// const auth = mcpAuth({ resource: 'https://mcp.example.com/mcp', scopes: ['books:read'] })\n//\n// // Fetch-style (Hono, Next.js, Workers, Bun, Deno):\n// export default { fetch: auth.protect((request, agent) => handleMcp(request, agent)) }\n//\n// // Express or plain Node, with the MCP TypeScript SDK (its transport reads req.auth):\n// app.use(auth.middleware())\n//\n// How an agent gets in, as the MCP specification lays it out: it calls the\n// server, which answers 401 and points at its protected resource metadata\n// (RFC 9728); that names Cauthy as the authorization server; the agent\n// registers itself there, sends the person to sign in and allow it, and comes\n// back with a token issued for this server alone (its `aud`). This module is\n// the server's half: the metadata, the 401 that points at it, and checking\n// the token. The resource must be registered in Cauthy's dashboard (AI\n// agents), with the same address, for the workspace whose people use it.\n//\n// A server that calls another of the workspace's servers for the person\n// trades the agent's token for one for that server (token exchange, RFC 8693):\n//\n// const auth = mcpAuth({ resource, clientId, clientSecret }) // from the dashboard\n// const { accessToken } = await auth.exchange(agent, 'https://api.example.com', ['orders:read'])\n//\n// The workspace's admins must have delegated that call in the dashboard, and\n// the person allowed it when they connected the agent.\n\nimport { jwtVerify, type JWTVerifyGetKey } from 'jose'\nimport { keysOf } from './verify'\nimport { withoutTrailingSlashes } from './url'\n\n/** The person an agent acts for, and what they allowed it. */\nexport interface AgentIdentity {\n /** The person. */\n userId: string\n tenantId: string\n email?: string\n /** The person's roles in the workspace. */\n roles: string[]\n /** What the person allowed the agent to do here. */\n scopes: string[]\n /** The agent (its client_id at Cauthy). */\n clientId: string\n /** UTC, ISO 8601. */\n expiresAt: string\n /** The token as it arrived. */\n token: string\n /**\n * Set when the token came by token exchange: the server that called this\n * one for the person (the `act` claim's subject).\n */\n actor?: string\n}\n\nexport interface McpAuthOptions {\n /** This server's address, as registered in Cauthy. Default: `CAUTHY_MCP_RESOURCE`. */\n resource?: string\n /** Cauthy's address. Default: `CAUTHY_ISSUER`. */\n issuer?: string\n /** Scopes every call needs. Default: none. */\n scopes?: string[]\n /** The scopes this server understands, to tell agents (`scopes_supported`). Default: `scopes`. */\n scopesSupported?: string[]\n /** A name for the server, shown by some agents. */\n resourceName?: string\n /** Cauthy's keys. Default: fetched from the issuer and cached. */\n jwks?: JWTVerifyGetKey\n /** The moment to verify at. Default: now. */\n now?: () => Date\n /**\n * Ask Cauthy, besides checking the token here, whether the person still\n * allows the agent (RFC 7662). Then an agent is cut off the moment it is\n * disconnected, not when its token runs out, at the price of a call to\n * Cauthy per token every `cacheSeconds` (default 30). Default: off.\n */\n introspect?: boolean | { cacheSeconds?: number }\n /** The fetch to call Cauthy with. Default: the global one. */\n fetch?: typeof fetch\n /** How long a call to Cauthy (introspection, token exchange) may take, in milliseconds. Default: 5000. */\n timeoutMs?: number\n /**\n * This server's client_id and secret at Cauthy, for token exchange (AI\n * agents → the server → Server credentials). Defaults:\n * `CAUTHY_MCP_CLIENT_ID`, `CAUTHY_MCP_CLIENT_SECRET`.\n */\n clientId?: string\n clientSecret?: string\n}\n\n/** What a token exchange answers. */\nexport interface ExchangedToken {\n /** The token for the other server, as the person. Send it as `Authorization: Bearer`. */\n accessToken: string\n /** What it allows there. */\n scopes: string[]\n /** UTC, ISO 8601. Never later than the agent's own token. */\n expiresAt: string\n}\n\n/** A token exchange Cauthy refused, with the OAuth error it answered. */\nexport class CauthyExchangeError extends Error {\n constructor(\n readonly status: number,\n readonly code: string,\n readonly description: string,\n ) {\n super(`@cauthyhq/node: token exchange refused (${code}): ${description}`)\n this.name = 'CauthyExchangeError'\n }\n}\n\n/** A token refused: `status` is 401 (no token, or not a good one) or 403 (a scope missing). */\nexport class McpAuthError extends Error {\n constructor(\n readonly status: 401 | 403,\n readonly code: 'invalid_request' | 'invalid_token' | 'insufficient_scope',\n readonly description: string,\n ) {\n super(`@cauthyhq/node: ${description}`)\n this.name = 'McpAuthError'\n }\n}\n\n/** What the MCP TypeScript SDK reads from `req.auth` (its AuthInfo). */\nexport interface McpAuthInfo {\n token: string\n clientId: string\n scopes: string[]\n /** Seconds since the epoch. */\n expiresAt: number\n resource: URL\n extra: { userId: string; tenantId: string; email?: string; roles: string[] }\n}\n\nexport interface McpAuth {\n /** This server's address, in the form Cauthy compares it in. */\n resource: string\n /** Where the protected resource metadata is served. */\n metadataUrl: string\n /** The protected resource metadata (RFC 9728). */\n metadata(): Record<string, unknown>\n /** The metadata's paths: `/.well-known/oauth-protected-resource` plus this server's path. */\n metadataPaths: string[]\n /** Verifies the `Authorization: Bearer` of a request (or a token). Throws `McpAuthError`. */\n verify(from: string | Request | Headers, needs?: string[]): Promise<AgentIdentity>\n /** The answer to a refused token: 401 or 403, with the `WWW-Authenticate` that points an agent at Cauthy. */\n challenge(error?: McpAuthError): Response\n /** The metadata, when the request asks for it; `null` otherwise. */\n handleMetadata(request: Request): Response | null\n /** Wraps a fetch-style handler: serves the metadata, refuses what has no good token, passes the rest with who it is. */\n protect(handler: (request: Request, agent: AgentIdentity) => Response | Promise<Response>, needs?: string[]): (request: Request) => Promise<Response>\n /**\n * Connect-style middleware (Express, Node's http): serves the metadata,\n * refuses what has no good token, and sets `req.agent` and `req.auth` —\n * what the MCP TypeScript SDK's transport hands its tools as `authInfo`.\n */\n middleware(needs?: string[]): (req: NodeRequest, res: NodeResponse, next: (err?: unknown) => void) => void\n /**\n * Trades the agent's token this server received for one for another of the\n * workspace's servers, to call it for the person (RFC 8693). Needs this\n * server's `clientId` and `clientSecret`. Answers are kept until shortly\n * before they expire. Throws `CauthyExchangeError` when Cauthy refuses.\n */\n exchange(from: string | AgentIdentity, target: string, scopes?: string[]): Promise<ExchangedToken>\n}\n\n// The slice of Node's (and Express's) request and response this needs.\ninterface NodeRequest {\n method?: string\n url?: string\n originalUrl?: string\n headers: Record<string, string | string[] | undefined>\n auth?: McpAuthInfo\n agent?: AgentIdentity\n}\ninterface NodeResponse {\n statusCode: number\n setHeader(name: string, value: string): unknown\n end(body?: string): unknown\n}\n\nfunction env(name: string): string | undefined {\n return typeof process !== 'undefined' ? process.env?.[name] : undefined\n}\n\n/**\n * The address in the one form Cauthy keeps it: scheme and host in lower case,\n * no default port, no trailing slash, no query, no fragment.\n */\nexport function canonicalResource(raw: string): string {\n let u: URL\n try {\n u = new URL(raw.trim())\n } catch {\n throw new Error(`@cauthyhq/node: resource ${JSON.stringify(raw)} is not an absolute address`)\n }\n const local = u.hostname === 'localhost' || u.hostname === '127.0.0.1' || u.hostname === '[::1]'\n if (u.protocol !== 'https:' && !(u.protocol === 'http:' && local)) {\n throw new Error('@cauthyhq/node: resource must be an https address')\n }\n if (u.search !== '' || u.hash !== '') throw new Error('@cauthyhq/node: resource may not have a query or a fragment')\n return `${u.protocol}//${u.host}${withoutTrailingSlashes(u.pathname)}`\n}\n\nfunction bearer(from: string | Request | Headers): string {\n if (typeof from === 'string') return from.replace(/^Bearer\\s+/i, '').trim()\n const header = (from instanceof Headers ? from : from.headers).get('authorization') ?? ''\n return /^Bearer\\s+/i.test(header) ? header.replace(/^Bearer\\s+/i, '').trim() : ''\n}\n\nfunction isActor(v: unknown): v is { sub: string } {\n return typeof v === 'object' && v !== null && typeof (v as { sub?: unknown }).sub === 'string'\n}\n\nfunction quoted(v: string): string {\n return `\"${v.replace(/[\"\\\\]/g, '')}\"`\n}\n\n/** Protects an MCP server with Cauthy. */\nexport function mcpAuth(options: McpAuthOptions = {}): McpAuth {\n const rawResource = options.resource ?? env('CAUTHY_MCP_RESOURCE') ?? ''\n if (rawResource === '') throw new Error('@cauthyhq/node: resource (CAUTHY_MCP_RESOURCE) is not set')\n const resource = canonicalResource(rawResource)\n const issuer = withoutTrailingSlashes(options.issuer ?? env('CAUTHY_ISSUER') ?? '')\n if (issuer === '') throw new Error('@cauthyhq/node: issuer (CAUTHY_ISSUER) is not set')\n const timeoutMs = options.timeoutMs ?? 5000\n if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) throw new Error('@cauthyhq/node: timeoutMs must be a positive number of milliseconds')\n const required = options.scopes ?? []\n const url = new URL(resource)\n const path = url.pathname === '/' ? '' : url.pathname\n const metadataPaths = [...new Set([`/.well-known/oauth-protected-resource${path}`, '/.well-known/oauth-protected-resource'])]\n const metadataUrl = `${url.origin}${metadataPaths[0]}`\n\n const metadata = () => ({\n resource,\n authorization_servers: [issuer],\n bearer_methods_supported: ['header'],\n scopes_supported: options.scopesSupported ?? required,\n ...(options.resourceName ? { resource_name: options.resourceName } : {}),\n })\n\n const introspect = options.introspect ? { cacheMs: ((typeof options.introspect === 'object' ? options.introspect.cacheSeconds : undefined) ?? 30) * 1000 } : null\n const answers = new Map<string, { active: boolean; until: number }>()\n const stillAllowed = async (token: string, cacheMs: number): Promise<boolean> => {\n const now = Date.now()\n const kept = answers.get(token)\n if (kept && kept.until > now) return kept.active\n let active = false\n try {\n const res = await (options.fetch ?? fetch)(`${issuer}/v1/oauth/introspect`, {\n signal: AbortSignal.timeout(timeoutMs),\n method: 'POST',\n headers: { 'content-type': 'application/x-www-form-urlencoded', accept: 'application/json' },\n body: new URLSearchParams({ token }),\n })\n if (!res.ok) throw new Error(`introspection answered ${res.status}`)\n active = ((await res.json()) as { active?: unknown }).active === true\n } catch (err) {\n // Cauthy could not be asked: what it last said holds.\n if (kept) return kept.active\n throw new McpAuthError(401, 'invalid_token', `could not check the token with Cauthy: ${err instanceof Error ? err.message : 'unreachable'}`)\n }\n if (answers.size > 10_000) answers.clear()\n answers.set(token, { active, until: now + cacheMs })\n return active\n }\n\n const verify = async (from: string | Request | Headers, needs: string[] = []): Promise<AgentIdentity> => {\n const token = bearer(from)\n if (token === '') throw new McpAuthError(401, 'invalid_request', 'no access token')\n let claims: Record<string, unknown>\n try {\n const verified = await jwtVerify(token, options.jwks ?? keysOf(issuer), {\n algorithms: ['RS256'],\n issuer,\n audience: resource,\n requiredClaims: ['exp', 'sub'],\n currentDate: options.now?.(),\n })\n claims = verified.payload as Record<string, unknown>\n } catch (err) {\n throw new McpAuthError(401, 'invalid_token', err instanceof Error ? err.message : 'the token is not valid')\n }\n const clientId = typeof claims.client_id === 'string' ? claims.client_id : ''\n if (clientId === '') throw new McpAuthError(401, 'invalid_token', 'not a token issued to an agent')\n const scopes = typeof claims.scope === 'string' ? claims.scope.split(' ').filter(Boolean) : []\n if (introspect && !(await stillAllowed(token, introspect.cacheMs))) throw new McpAuthError(401, 'invalid_token', 'the token is no longer allowed: the agent was disconnected, or its access withdrawn')\n const missing = [...required, ...needs].filter((s) => !scopes.includes(s))\n if (missing.length > 0) throw new McpAuthError(403, 'insufficient_scope', `this needs ${missing.join(' ')}`)\n return {\n userId: String(claims.sub),\n tenantId: typeof claims.tid === 'string' ? claims.tid : '',\n ...(typeof claims.email === 'string' && claims.email !== '' ? { email: claims.email } : {}),\n roles: Array.isArray(claims.roles) ? claims.roles.filter((r): r is string => typeof r === 'string') : [],\n scopes,\n clientId,\n expiresAt: new Date((claims.exp as number) * 1000).toISOString(),\n token,\n ...(isActor(claims.act) ? { actor: claims.act.sub } : {}),\n }\n }\n\n const clientId = options.clientId ?? env('CAUTHY_MCP_CLIENT_ID') ?? ''\n const clientSecret = options.clientSecret ?? env('CAUTHY_MCP_CLIENT_SECRET') ?? ''\n const exchanged = new Map<string, ExchangedToken & { until: number }>()\n const exchange = async (from: string | AgentIdentity, target: string, scopes: string[] = []): Promise<ExchangedToken> => {\n if (clientId === '' || clientSecret === '') {\n throw new Error('@cauthyhq/node: token exchange needs clientId and clientSecret (CAUTHY_MCP_CLIENT_ID, CAUTHY_MCP_CLIENT_SECRET)')\n }\n const subject = typeof from === 'string' ? bearer(from) : from.token\n const to = canonicalResource(target)\n const key = `${subject}\\n${to}\\n${scopes.join(' ')}`\n const kept = exchanged.get(key)\n if (kept && kept.until > Date.now()) return { accessToken: kept.accessToken, scopes: kept.scopes, expiresAt: kept.expiresAt }\n let res: Response\n try {\n res = await (options.fetch ?? fetch)(`${issuer}/v1/oauth/token`, {\n signal: AbortSignal.timeout(timeoutMs),\n method: 'POST',\n headers: {\n 'content-type': 'application/x-www-form-urlencoded',\n accept: 'application/json',\n authorization: `Basic ${btoa(`${encodeURIComponent(clientId)}:${encodeURIComponent(clientSecret)}`)}`,\n },\n body: new URLSearchParams({\n grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange',\n subject_token: subject,\n subject_token_type: 'urn:ietf:params:oauth:token-type:access_token',\n resource: to,\n ...(scopes.length > 0 ? { scope: scopes.join(' ') } : {}),\n }),\n })\n } catch (err) {\n throw new CauthyExchangeError(503, 'temporarily_unavailable', `Cauthy could not be reached: ${err instanceof Error ? err.message : 'unreachable'}`)\n }\n const body = (await res.json().catch(() => ({}))) as Record<string, unknown>\n if (!res.ok || typeof body.access_token !== 'string') {\n throw new CauthyExchangeError(res.status, typeof body.error === 'string' ? body.error : 'server_error',\n typeof body.error_description === 'string' ? body.error_description : `Cauthy answered ${res.status}`)\n }\n const expiresIn = typeof body.expires_in === 'number' ? body.expires_in : 0\n const out: ExchangedToken = {\n accessToken: body.access_token,\n scopes: typeof body.scope === 'string' ? body.scope.split(' ').filter(Boolean) : [],\n expiresAt: new Date(Date.now() + expiresIn * 1000).toISOString(),\n }\n if (exchanged.size > 10_000) exchanged.clear()\n // Kept until 30 seconds before it runs out, so it is never sent stale.\n exchanged.set(key, { ...out, until: Date.now() + Math.max(0, expiresIn - 30) * 1000 })\n return out\n }\n\n const authenticate = (error?: McpAuthError): string => {\n const parts = [`resource_metadata=${quoted(metadataUrl)}`]\n if (error && error.code !== 'invalid_request') {\n parts.push(`error=${quoted(error.code)}`, `error_description=${quoted(error.description)}`)\n }\n // What to ask the person for: said when there is no token or it falls short.\n const scope = error?.code === 'invalid_token' ? [] : required\n if (scope.length > 0) parts.push(`scope=${quoted(scope.join(' '))}`)\n return `Bearer ${parts.join(', ')}`\n }\n\n const challenge = (error?: McpAuthError): Response => {\n const status = error?.status ?? 401\n return new Response(JSON.stringify({ error: error?.code ?? 'invalid_request', error_description: error?.description ?? 'an access token is required' }), {\n status,\n headers: { 'content-type': 'application/json', 'www-authenticate': authenticate(error) },\n })\n }\n\n const metadataResponse = () =>\n new Response(JSON.stringify(metadata()), {\n headers: { 'content-type': 'application/json', 'cache-control': 'public, max-age=3600', 'access-control-allow-origin': '*' },\n })\n\n const isMetadata = (pathname: string, method = 'GET') => method === 'GET' && metadataPaths.includes(withoutTrailingSlashes(pathname) || '/')\n\n const handleMetadata = (request: Request): Response | null =>\n isMetadata(new URL(request.url).pathname, request.method) ? metadataResponse() : null\n\n const toAuthInfo = (a: AgentIdentity): McpAuthInfo => ({\n token: a.token,\n clientId: a.clientId,\n scopes: a.scopes,\n expiresAt: Math.floor(new Date(a.expiresAt).getTime() / 1000),\n resource: new URL(resource),\n extra: { userId: a.userId, tenantId: a.tenantId, ...(a.email ? { email: a.email } : {}), roles: a.roles },\n })\n\n return {\n resource,\n metadataUrl,\n metadataPaths,\n metadata,\n verify,\n challenge,\n handleMetadata,\n protect(handler, needs) {\n return async (request) => {\n const meta = handleMetadata(request)\n if (meta) return meta\n let agent: AgentIdentity\n try {\n agent = await verify(request, needs)\n } catch (err) {\n if (err instanceof McpAuthError) return challenge(err)\n throw err\n }\n return handler(request, agent)\n }\n },\n exchange,\n middleware(needs) {\n return (req, res, next) => {\n const pathname = (req.originalUrl ?? req.url ?? '/').split('?')[0] ?? '/'\n if (isMetadata(pathname, req.method)) {\n res.statusCode = 200\n res.setHeader('content-type', 'application/json')\n res.setHeader('cache-control', 'public, max-age=3600')\n res.setHeader('access-control-allow-origin', '*')\n res.end(JSON.stringify(metadata()))\n return\n }\n const raw = req.headers.authorization\n const header = Array.isArray(raw) ? (raw[0] ?? '') : (raw ?? '')\n verify(/^Bearer\\s+/i.test(header) ? header : '', needs).then(\n (agent) => {\n req.agent = agent\n req.auth = toAuthInfo(agent)\n next()\n },\n (err: unknown) => {\n if (!(err instanceof McpAuthError)) return next(err)\n res.statusCode = err.status\n res.setHeader('content-type', 'application/json')\n res.setHeader('www-authenticate', authenticate(err))\n res.end(JSON.stringify({ error: err.code, error_description: err.description }))\n },\n )\n }\n },\n }\n}\n","// A backend that is handed a Cauthy access token: is it real, and whose is it?\n//\n// const who = await verifyToken(request) // or the token itself\n// who.userId, who.tenantId, who.roles\n//\n// Cauthy signs its tokens with a key it publishes (/.well-known/jwks.json), so\n// checking one needs no secret and no call to Cauthy per request: the keys are\n// fetched once and cached. Throws `CauthyTokenError` for anything else: answer\n// it 401, and read `reason`, one of the closed list of codes in the spec's\n// verification/rules.md.\n\nimport { compactVerify, createLocalJWKSet, type CompactVerifyGetKey, type JSONWebKeySet, type JWTVerifyGetKey } from 'jose'\nimport { parseStrict } from './json'\nimport { CauthyConfigError } from './types'\nimport { deprecated } from './deprecate'\nimport { parseKey, refuseBesideKey } from './cauthykey'\nimport { deriveFromKey } from './keyderive'\nimport { createIdentity } from './identity'\nimport { createSystem } from './system'\nimport { deadline } from './deadline'\nimport { withoutTrailingSlashes } from './url'\n\n/** Who a verified access token belongs to. */\nexport interface TokenIdentity {\n /** The person. For a system's own token, the client it identifies as. */\n userId: string\n tenantId: string\n email?: string\n /** What the token says of the address, when it says (a boolean; absent otherwise). */\n emailVerified?: boolean\n roles: string[]\n /** Set when the token is a system's own and not a person's (only with `allowSystem`). */\n system?: string\n /** Set when the token is a guest's (only with `allowAnonymous`). */\n anonymous?: true\n /** The client a person's token was issued through (`azp`); absent when none was named. */\n authorizedParty?: string\n /**\n * The token's `client_id`: on a client's own token (only with\n * `allowClientTokens`), the client's public id; on an AI agent's, the agent's.\n */\n clientId?: string\n /** UTC, ISO 8601. */\n expiresAt: string\n /** The token as it arrived, to pass on. */\n token: string\n}\n\n/** Why a token was refused: the closed list of verification/rules.md, and `no_token` when none was given. */\nexport type TokenRefusal =\n | 'no_token' | 'too_large' | 'malformed' | 'alg_not_allowed' | 'unsupported_header' | 'missing_kid' | 'unknown_kid' | 'weak_key'\n | 'bad_signature' | 'missing_claim' | 'invalid_claim' | 'wrong_issuer' | 'wrong_audience' | 'expired' | 'not_yet_valid'\n | 'issued_in_future' | 'wrong_tenant' | 'system_token' | 'client_token' | 'anonymous_token' | 'wrong_client'\n\n/**\n * Cauthy's keys could not be fetched (or are not a key set): the token may be\n * good, and the application cannot tell. Answer 503, not 401. It is not a\n * `CauthyTokenError`: it says nothing about the token.\n */\nexport class CauthyUnavailableError extends Error {\n constructor(detail: string) {\n super(`@cauthyhq/node: Cauthy's keys are unavailable: ${detail}`)\n this.name = 'CauthyUnavailableError'\n }\n}\n\nexport class CauthyTokenError extends Error {\n constructor(readonly reason: TokenRefusal, readonly detail?: string) {\n super(`@cauthyhq/node: token refused: ${reason}${detail ? ` (${detail})` : ''}`)\n this.name = 'CauthyTokenError'\n }\n}\n\nexport interface VerifyTokenOptions {\n /** Cauthy's address. Default: `CAUTHY_ISSUER`. */\n issuer?: string\n /**\n * Further issuers a token may name, e.g. a workspace's custom domain. The\n * keys always come from `issuer`. Default: none.\n */\n issuers?: string[]\n /** The audience the tokens must carry. Required: this or `CAUTHY_AUDIENCE`. */\n audience?: string\n /** The workspace's id, a lowercase UUID. Required: this or `CAUTHY_TENANT_ID`. */\n tenantId?: string\n /** Cauthy's keys. Default: fetched from the issuer and cached (see `remoteKeys`). */\n jwks?: JWTVerifyGetKey\n /** The fetch to get the keys with. Default: the global one. */\n fetch?: typeof fetch\n /** The moment to verify at. Default: now. */\n now?: Date\n /** Clock tolerance in seconds, 0 to 60. Default: 30. */\n leeway?: number\n /**\n * Accept a system's own token (a `sys` claim) too: for an endpoint another\n * system calls. Default: false, so a system's token never stands for a\n * person (rule R13 of the spec's verification/rules.md).\n */\n allowSystem?: boolean\n /** Accept a guest's token (`anon: true`). Default: false (rule R14). */\n allowAnonymous?: boolean\n /**\n * Accept a client's own token (a `token_use` claim, from a\n * `client_credentials` grant) too: for a backend that serves machines as\n * well as people. Default: false, so a machine never passes as a person\n * (rule R13). The identity then carries `clientId`.\n */\n allowClientTokens?: boolean\n /**\n * The client this backend signs people in through. When set, a person's\n * token issued through another client (an `azp` naming another) is refused\n * with `wrong_client` (rule R15); a token with no `azp` passes. In contract\n * mode it is the link's sign-in client. Default: none, and `azp` is not checked.\n */\n clientId?: string\n}\n\nconst MAX_TOKEN_BYTES = 8192\nconst UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/\nconst NIL_UUID = '00000000-0000-0000-0000-000000000000'\nconst SEGMENT = /^[A-Za-z0-9_-]+$/\n\nconst MAX_KEYS_BYTES = 64 * 1024\nconst KEYS_TIMEOUT_MS = 5000\nconst KEYS_COOLDOWN_MS = 30_000\nconst KEYS_DEFAULT_TTL_S = 300\nconst KEYS_MIN_TTL_S = 60\nconst KEYS_MAX_TTL_S = 3600\n\n/**\n * Cauthy's published keys (K1 to K5 of verification/rules.md): fetched with a\n * GET that follows no redirect, within 5 seconds, from `<issuer>/.well-known/jwks.json`;\n * only a 200 of at most 64 KiB counts. Kept for the answer's max-age (at most\n * an hour and at least a minute, 5 minutes when it names none); a `kid` the set lacks fetches again\n * only if the last fetch is older than 30 seconds; one fetch at a time; a\n * failed fetch keeps the last good set, and with none it is\n * `CauthyUnavailableError`.\n */\nexport function remoteKeys(issuer: string, fetchKeys: typeof fetch = fetch): JWTVerifyGetKey {\n let set: JWTVerifyGetKey | undefined\n let startedAt = 0\n let expiresAt = 0\n let inflight: Promise<void> | undefined\n\n const load = async (): Promise<void> => {\n startedAt = Date.now()\n try {\n const res = await fetchKeys(`${issuer}/.well-known/jwks.json`, { redirect: 'manual', signal: AbortSignal.timeout(KEYS_TIMEOUT_MS), headers: { accept: 'application/json' } })\n if (res.status !== 200) throw new Error(`answered ${res.status}`)\n const bytes = await readCapped(res, MAX_KEYS_BYTES)\n const parsed = JSON.parse(new TextDecoder().decode(bytes)) as JSONWebKeySet\n if (!parsed || !Array.isArray(parsed.keys)) throw new Error('not a key set')\n // K6: two usable keys with one kid make the whole set untrustworthy.\n const seen = new Set<string>()\n for (const k of parsed.keys) {\n const usable = k?.kty === 'RSA' && (k.use === undefined || k.use === 'sig') && (k.alg === undefined || k.alg === 'RS256')\n if (!usable || typeof k.kid !== 'string') continue\n if (seen.has(k.kid)) throw new Error(`two usable keys share the kid ${JSON.stringify(k.kid)}`)\n seen.add(k.kid)\n }\n set = createLocalJWKSet(parsed)\n const maxAge = /max-age=(\\d+)/.exec(res.headers.get('cache-control') ?? '')\n // At least a minute: max-age=0 must not mean a fetch for every token.\n const ttl = Math.max(KEYS_MIN_TTL_S, Math.min(maxAge ? Number(maxAge[1]) : KEYS_DEFAULT_TTL_S, KEYS_MAX_TTL_S))\n expiresAt = Date.now() + ttl * 1000\n } catch (err) {\n // The last good set stands, and is not asked for again before the cooldown:\n // an outage must not cost a wait on every verification. With none, the\n // application cannot tell.\n if (set === undefined) throw new CauthyUnavailableError(err instanceof Error ? err.message : 'unreachable')\n expiresAt = Math.max(expiresAt, Date.now() + KEYS_COOLDOWN_MS)\n }\n }\n const refresh = (): Promise<void> => {\n inflight ??= load().finally(() => {\n inflight = undefined\n })\n return inflight\n }\n\n return async (header, token) => {\n if (set === undefined || Date.now() >= expiresAt) await refresh()\n try {\n return await set!(header, token)\n } catch (err) {\n const unknown = (err as { code?: unknown } | null)?.code === 'ERR_JWKS_NO_MATCHING_KEY'\n if (unknown && Date.now() - startedAt >= KEYS_COOLDOWN_MS) {\n await refresh()\n return set!(header, token)\n }\n throw err\n }\n }\n}\n\nasync function readCapped(res: Response, max: number): Promise<Uint8Array> {\n const reader = res.body?.getReader()\n if (!reader) return new Uint8Array()\n const chunks: Uint8Array[] = []\n let size = 0\n for (;;) {\n const { done, value } = await reader.read()\n if (done) break\n size += value.length\n if (size > max) {\n await reader.cancel()\n throw new Error(`over ${max} bytes`)\n }\n chunks.push(value)\n }\n const out = new Uint8Array(size)\n let at = 0\n for (const c of chunks) {\n out.set(c, at)\n at += c.length\n }\n return out\n}\n\nconst published = new Map<string, JWTVerifyGetKey>()\nconst withFetch = new WeakMap<typeof fetch, Map<string, JWTVerifyGetKey>>()\n\nfunction remoteKeysFor(issuer: string, fetchKeys: typeof fetch): JWTVerifyGetKey {\n let byIssuer = withFetch.get(fetchKeys)\n if (!byIssuer) withFetch.set(fetchKeys, (byIssuer = new Map()))\n let set = byIssuer.get(issuer)\n if (!set) byIssuer.set(issuer, (set = remoteKeys(issuer, fetchKeys)))\n return set\n}\n\n/** Cauthy's published keys, fetched once per issuer and kept (see `remoteKeys`). */\nexport function keysOf(issuer: string): JWTVerifyGetKey {\n let set = published.get(issuer)\n if (set === undefined) {\n set = remoteKeys(issuer)\n published.set(issuer, set)\n }\n return set\n}\n\nfunction env(name: string): string | undefined {\n return typeof process !== 'undefined' ? process.env?.[name] : undefined\n}\n\nfunction bearer(from: string | Request | Headers): string {\n // A raw token is taken as it is: surrounding whitespace is the caller's mistake (R2).\n if (typeof from === 'string') return from.replace(/^Bearer\\s+/i, '')\n const header = (from instanceof Headers ? from : from.headers).get('authorization') ?? ''\n return /^Bearer\\s+/i.test(header) ? header.replace(/^Bearer\\s+/i, '').trim() : ''\n}\n\nfunction issuerAddress(value: string, what: string): string {\n let url: URL\n try {\n url = new URL(value)\n } catch {\n throw new CauthyConfigError(`${what} must be an https address, e.g. https://id.example.com`)\n }\n // http is for a developer's own machine: localhost, 127.0.0.1, *.localhost and *.test.\n const local = url.hostname === 'localhost' || url.hostname === '127.0.0.1' || url.hostname.endsWith('.localhost') || url.hostname.endsWith('.test')\n if (url.protocol !== 'https:' && !(url.protocol === 'http:' && local)) throw new CauthyConfigError(`${what} must be https (http only for localhost, *.localhost and *.test)`)\n if (url.search !== '' || url.hash !== '') throw new CauthyConfigError(`${what} must have no query or fragment`)\n return withoutTrailingSlashes(value)\n}\n\nconst isObject = (v: unknown): v is Record<string, unknown> => typeof v === 'object' && v !== null && !Array.isArray(v)\n\nfunction decodeJson(segment: string): unknown {\n const padded = segment.replace(/-/g, '+').replace(/_/g, '/')\n return parseStrict(Buffer.from(padded, 'base64').toString('utf8'))\n}\n\n/**\n * A verifier for access tokens, configured once. Refuses to be made without\n * an issuer, an audience and a workspace: a verifier that checks none of\n * them would accept a token of any other application or workspace.\n */\nexport function createTokenVerifier(options: VerifyTokenOptions = {}): (from: string | Request | Headers) => Promise<TokenIdentity> {\n // Read from the environment, these three leave in 2.0: CAUTHY_KEY says them all.\n const fromEnv = (['CAUTHY_ISSUER', 'CAUTHY_AUDIENCE', 'CAUTHY_TENANT_ID'] as const)\n .filter((name, i) => [options.issuer, options.audience, options.tenantId][i] === undefined && (env(name) ?? '') !== '')\n if (fromEnv.length > 0) {\n deprecated('Reading CAUTHY_ISSUER, CAUTHY_AUDIENCE and CAUTHY_TENANT_ID to verify tokens', 'Set CAUTHY_KEY and use createVerifierFromEnv().')\n }\n const issuerGiven = options.issuer ?? env('CAUTHY_ISSUER') ?? ''\n if (issuerGiven === '') throw new CauthyConfigError('issuer (CAUTHY_ISSUER) is not set')\n const issuer = issuerAddress(issuerGiven, 'issuer')\n const issuers = [issuer, ...(options.issuers ?? []).map((i) => issuerAddress(i, 'issuers'))]\n const audience = options.audience ?? env('CAUTHY_AUDIENCE') ?? ''\n if (audience === '') throw new CauthyConfigError('audience (CAUTHY_AUDIENCE) is not set: without it a token issued for another application would be accepted')\n const tenant = options.tenantId ?? env('CAUTHY_TENANT_ID') ?? ''\n if (tenant === '') throw new CauthyConfigError('tenantId (CAUTHY_TENANT_ID) is not set: without it a token of another workspace would be accepted')\n if (!UUID.test(tenant) || tenant === NIL_UUID) throw new CauthyConfigError('tenantId must be the workspace\\'s id, a lowercase UUID that is not the nil UUID')\n const leeway = options.leeway ?? 30\n if (!Number.isFinite(leeway) || leeway < 0 || leeway > 60) throw new CauthyConfigError('leeway must be 0 to 60 seconds')\n\n return async (from) => {\n const token = bearer(from)\n if (token === '') throw new CauthyTokenError('no_token')\n // R1\n if (Buffer.byteLength(token) > MAX_TOKEN_BYTES) throw new CauthyTokenError('too_large')\n // R2\n const parts = token.split('.')\n if (parts.length !== 3 || !parts.every((p) => SEGMENT.test(p) || p === '')) throw new CauthyTokenError('malformed', 'not three base64url segments')\n let header: unknown\n try {\n header = decodeJson(parts[0])\n } catch {\n throw new CauthyTokenError('malformed', 'the header is not JSON')\n }\n if (!isObject(header)) throw new CauthyTokenError('malformed', 'the header is not an object')\n // R3, R4, R5: the algorithm is the verifier's, never the token's.\n if (header.alg !== 'RS256') throw new CauthyTokenError('alg_not_allowed')\n if ('crit' in header) throw new CauthyTokenError('unsupported_header')\n // R4: typ is not required, but when present it is JWT or at+jwt, in any case.\n if ('typ' in header && !(typeof header.typ === 'string' && ['jwt', 'at+jwt'].includes(header.typ.toLowerCase()))) {\n throw new CauthyTokenError('unsupported_header', 'typ')\n }\n if (typeof header.kid !== 'string' || header.kid === '') throw new CauthyTokenError('missing_kid')\n\n // R5, R6\n let payloadBytes: Uint8Array\n try {\n const verified = await compactVerify(token, (options.jwks ?? (options.fetch ? remoteKeysFor(issuer, options.fetch) : keysOf(issuer))) as CompactVerifyGetKey, { algorithms: ['RS256'] })\n payloadBytes = verified.payload\n } catch (err) {\n // By code, not by class: the keys may come from another copy of jose.\n const code = (err as { code?: unknown } | null)?.code\n if (code === 'ERR_JWKS_NO_MATCHING_KEY') throw new CauthyTokenError('unknown_kid')\n if (code === 'ERR_JWS_SIGNATURE_VERIFICATION_FAILED') throw new CauthyTokenError('bad_signature')\n if (err instanceof TypeError && /2048/.test(err.message)) throw new CauthyTokenError('weak_key')\n if (code === 'ERR_JWS_INVALID') throw new CauthyTokenError('malformed', err instanceof Error ? err.message : undefined)\n if (err instanceof CauthyUnavailableError) throw err\n throw new CauthyTokenError('bad_signature', err instanceof Error ? err.message : undefined)\n }\n\n // R7\n let claims: unknown\n try {\n claims = parseStrict(new TextDecoder().decode(payloadBytes), ['exp', 'iat', 'nbf'])\n } catch {\n throw new CauthyTokenError('malformed', 'the payload is not JSON')\n }\n if (!isObject(claims)) throw new CauthyTokenError('malformed', 'the payload is not an object')\n for (const name of ['iss', 'sub', 'aud', 'exp', 'iat', 'tid']) {\n if (claims[name] === undefined) throw new CauthyTokenError('missing_claim', name)\n }\n // R8: no claim is null but roles.\n const bad = (name: string): never => {\n throw new CauthyTokenError('invalid_claim', name)\n }\n if (typeof claims.iss !== 'string') bad('iss')\n if (typeof claims.sub !== 'string' || !UUID.test(claims.sub)) bad('sub')\n if (typeof claims.tid !== 'string' || !UUID.test(claims.tid)) bad('tid')\n const aud = claims.aud\n if (!(typeof aud === 'string' || (Array.isArray(aud) && aud.length > 0 && aud.every((a) => typeof a === 'string')))) bad('aud')\n for (const name of ['exp', 'iat', 'nbf']) if (claims[name] !== undefined && !Number.isInteger(claims[name])) bad(name)\n if (claims.roles !== undefined && claims.roles !== null && !(Array.isArray(claims.roles) && claims.roles.every((r) => typeof r === 'string'))) bad('roles')\n if (claims.email !== undefined && typeof claims.email !== 'string') bad('email')\n for (const name of ['email_verified', 'anon']) if (claims[name] !== undefined && typeof claims[name] !== 'boolean') bad(name)\n for (const name of ['token_use', 'azp']) if (claims[name] !== undefined && typeof claims[name] !== 'string') bad(name)\n\n // R9, R10\n if (!issuers.includes(claims.iss as string)) throw new CauthyTokenError('wrong_issuer')\n if (!(Array.isArray(aud) ? aud.includes(audience) : aud === audience)) throw new CauthyTokenError('wrong_audience')\n // R11\n const now = (options.now ?? new Date()).getTime() / 1000\n if (now >= (claims.exp as number) + leeway) throw new CauthyTokenError('expired')\n if (claims.nbf !== undefined && now < (claims.nbf as number) - leeway) throw new CauthyTokenError('not_yet_valid')\n if (now < (claims.iat as number) - leeway) throw new CauthyTokenError('issued_in_future')\n // R12\n if (claims.tid !== tenant) throw new CauthyTokenError('wrong_tenant')\n // R13, R14\n // Its presence is the mark, whatever the value (null and false included).\n if ('sys' in claims && !options.allowSystem) throw new CauthyTokenError('system_token')\n // A client's own token stands for a machine: present is the mark, as for sys.\n if ('token_use' in claims && !options.allowClientTokens) throw new CauthyTokenError('client_token')\n if (claims.anon === true && !options.allowAnonymous) throw new CauthyTokenError('anonymous_token')\n // R15: a person's token issued through another client than the one named.\n if (options.clientId && typeof claims.azp === 'string' && claims.azp !== '' && claims.azp !== options.clientId) throw new CauthyTokenError('wrong_client')\n\n return {\n userId: claims.sub as string,\n tenantId: claims.tid as string,\n ...(typeof claims.email === 'string' && claims.email !== '' ? { email: claims.email } : {}),\n ...(typeof claims.email_verified === 'boolean' ? { emailVerified: claims.email_verified } : {}),\n roles: Array.isArray(claims.roles) ? (claims.roles as string[]) : [],\n ...(typeof claims.sys === 'string' && claims.sys !== '' ? { system: claims.sys } : {}),\n ...(claims.anon === true ? { anonymous: true as const } : {}),\n ...(typeof claims.azp === 'string' && claims.azp !== '' ? { authorizedParty: claims.azp } : {}),\n ...(typeof claims.client_id === 'string' && claims.client_id !== '' ? { clientId: claims.client_id } : {}),\n expiresAt: new Date((claims.exp as number) * 1000).toISOString(),\n token,\n }\n }\n}\n\n/**\n * Verifies an access token (or the `Authorization: Bearer` of a request) and\n * answers who it belongs to, by the rules of the spec's verification/rules.md.\n * Needs an issuer, an audience and a workspace (see `createTokenVerifier`,\n * which checks them once, at startup).\n */\nexport async function verifyToken(from: string | Request | Headers, options: VerifyTokenOptions = {}): Promise<TokenIdentity> {\n return createTokenVerifier(options)(from)\n}\n\n/**\n * A verifier made from the environment. With `CAUTHY_KEY` the issuer, the\n * audience and the workspace are Cauthy's answer to `GET /v1/connect/self`\n * (asked with the key's assertion, once); nothing else is read, and any of\n * `issuer`, `audience`, `tenantId` given beside it is refused. Without it,\n * `CAUTHY_ISSUER`, `CAUTHY_AUDIENCE` and `CAUTHY_TENANT_ID`, deprecated.\n *\n * const verify = await createVerifierFromEnv()\n * const who = await verify(request)\n */\nexport async function createVerifierFromEnv(\n options: Omit<VerifyTokenOptions, 'issuers'> & { key?: string } = {},\n): Promise<(from: string | Request | Headers) => Promise<TokenIdentity>> {\n const text = options.key ?? env('CAUTHY_KEY')\n if (text === undefined || text === '') return createTokenVerifier(options)\n refuseBesideKey(typeof process !== 'undefined' ? process.env : undefined)\n for (const name of ['issuer', 'audience', 'tenantId'] as const) {\n if (options[name] !== undefined) throw new CauthyConfigError(`with CAUTHY_KEY, ${name} comes from CAUTHY_KEY: do not pass it`)\n }\n const key = parseKey(text)\n const system = createSystem({ issuer: key.issuer, clientId: key.clientId, signer: { clientId: key.clientId, signer: deriveFromKey(key).signer }, fetch: options.fetch })\n const id = await createIdentity({ issuer: key.issuer, token: () => system.token(), fetch: deadline(options.fetch, KEYS_TIMEOUT_MS) }).ready()\n const { key: _, ...rest } = options\n return createTokenVerifier({ ...rest, issuer: id.issuer, audience: id.audience, tenantId: id.tenant.id })\n}\n","/**\n * The address without its trailing slashes: `https://auth.example.com/` and\n * `https://auth.example.com` name the same issuer. A loop, not `/\\/+$/`,\n * which backtracks over a long run of slashes.\n */\nexport function withoutTrailingSlashes(s: string): string {\n let end = s.length\n while (end > 0 && s.charCodeAt(end - 1) === 47) end--\n return s.slice(0, end)\n}\n"],"mappings":"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","names":["import_jose","clientId"]}
|