@cboxdk/id-js 0.15.1 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +455 -4
- package/dist/chunk-2E25IPBA.js +155 -0
- package/dist/chunk-2E25IPBA.js.map +1 -0
- package/dist/{chunk-3V6OKRWF.js → chunk-CXGKOJRJ.js} +374 -90
- package/dist/chunk-CXGKOJRJ.js.map +1 -0
- package/dist/chunk-QQW63PGU.js +25 -0
- package/dist/chunk-QQW63PGU.js.map +1 -0
- package/dist/chunk-SEUD4TE4.js +61 -0
- package/dist/chunk-SEUD4TE4.js.map +1 -0
- package/dist/{client-CecDIu_N.d.cts → client-BSZASrMo.d.ts} +109 -136
- package/dist/{client-CecDIu_N.d.ts → client-BxUHm5hX.d.cts} +109 -136
- package/dist/errors-D61xljZE.d.cts +87 -0
- package/dist/errors-D61xljZE.d.ts +87 -0
- package/dist/index.cjs +440 -35
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +40 -90
- package/dist/index.d.ts +40 -90
- package/dist/index.js +64 -25
- package/dist/index.js.map +1 -1
- package/dist/management.cjs +4372 -0
- package/dist/management.cjs.map +1 -0
- package/dist/management.d.cts +10923 -0
- package/dist/management.d.ts +10923 -0
- package/dist/management.js +4335 -0
- package/dist/management.js.map +1 -0
- package/dist/nextjs.cjs +517 -42
- package/dist/nextjs.cjs.map +1 -1
- package/dist/nextjs.d.cts +29 -8
- package/dist/nextjs.d.ts +29 -8
- package/dist/nextjs.js +42 -12
- package/dist/nextjs.js.map +1 -1
- package/dist/server.cjs +220 -0
- package/dist/server.cjs.map +1 -0
- package/dist/server.d.cts +105 -0
- package/dist/server.d.ts +105 -0
- package/dist/server.js +5 -0
- package/dist/server.js.map +1 -0
- package/dist/types-BYZldock.d.cts +316 -0
- package/dist/types-BYZldock.d.ts +316 -0
- package/package.json +14 -2
- package/dist/chunk-3V6OKRWF.js.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/api-keys.ts"],"names":[],"mappings":"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","file":"chunk-2E25IPBA.js","sourcesContent":["import type { OrganizationRole } from './claims.js';\nimport { AuthenticationError, ConfigurationError, oauthError } from './errors.js';\nimport { assertSecureIssuer } from './issuer.js';\n\n/**\n * A customer API key Cbox ID vouches for: live, bound to YOUR app, and held by an active\n * member of `org`.\n *\n * The field names are the endpoint's own, so the claim helpers read it like a token —\n * `hasPermission(answer, 'invoices:create')`, `organization(answer)?.role`. (A type\n * alias rather than an interface for that reason: only an alias is assignable to the\n * helpers' record type.)\n */\nexport type ActiveApiKey = {\n active: true;\n /** The key's id — log this, never the key. */\n key_id: string;\n /** The person who holds the key (their subject id). */\n sub: string;\n /** The organization the key acts in. */\n org: string;\n /** The holder's membership tier in `org`; null for a tier this SDK does not know. */\n org_role: OrganizationRole | null;\n /**\n * What the key may do, already re-capped by Cbox ID: the key's own list intersected\n * with what the holder holds for your app right now. A demoted holder's key loses the\n * permission on the next verification, not at the next reissue.\n */\n permissions: string[];\n /** Your app's client id — always, or the answer is refused. */\n client_id: string;\n /** ISO 8601 UTC expiry, or null for a key that does not expire. */\n expires_at: string | null;\n};\n\n/**\n * Every refusal: unknown, malformed, revoked, expired, another app's key, a holder who\n * left. Cbox ID never says which, so the endpoint cannot be used to probe keys — and\n * neither does this.\n */\nexport interface InactiveApiKey {\n active: false;\n}\n\n/** What {@link ApiKeyVerifier.verifyApiKey} returns. Narrow on `active`. */\nexport type ApiKeyVerification = ActiveApiKey | InactiveApiKey;\n\n/** Configuration for an {@link ApiKeyVerifier}. */\nexport interface ApiKeyVerifierConfig {\n /** Base URL (issuer) of the Cbox ID instance. */\n issuer: string;\n /** Your app's OAuth client id — the app the keys were issued for. */\n clientId: string;\n /** Your app's client secret. Verification is a confidential-client call. */\n clientSecret: string;\n /** Timeout (ms) for the verification request. Defaults to `10000`. */\n timeoutMs?: number;\n /**\n * Cache an ACTIVE answer for this long (ms), so a burst of requests with one key costs\n * one round-trip. Off (`0`) by default and at most {@link MAX_API_KEY_CACHE_TTL_MS}:\n * while an answer is cached, a revoked key keeps working, and a demoted holder keeps\n * the permissions they lost. Never cached past the key's `expires_at`. Refusals are\n * never cached, so a key issued a moment ago works on its first request.\n */\n cacheTtlMs?: number;\n}\n\n/** The longest {@link ApiKeyVerifierConfig.cacheTtlMs} allowed: one minute. */\nexport const MAX_API_KEY_CACHE_TTL_MS = 60_000;\n\n/** Enough for a busy API's working set; oldest entries go first beyond it. */\nconst MAX_CACHE_ENTRIES = 1_000;\n\nconst ORGANIZATION_ROLES: ReadonlySet<string> = new Set<OrganizationRole>([\n 'owner',\n 'admin',\n 'developer',\n 'member',\n 'viewer',\n]);\n\n/**\n * Verifies customer API keys presented to your API: `POST {issuer}/oauth/api-keys/verify`,\n * authenticated with your app's own client credentials.\n *\n * SERVER-SIDE ONLY. It holds your client secret, so it lives in the\n * `@cboxdk/id-js/server` entry and the Next.js adapter, never in the main entry that a\n * browser bundle pulls in — and it refuses to be constructed in a browser at all.\n *\n * ```ts\n * import { ApiKeyVerifier } from '@cboxdk/id-js/server';\n * import { hasPermission } from '@cboxdk/id-js';\n *\n * const keys = new ApiKeyVerifier({ issuer, clientId, clientSecret, cacheTtlMs: 10_000 });\n *\n * const answer = await keys.verifyApiKey(request.headers.get('x-api-key') ?? '');\n * if (!answer.active) return unauthorized();\n * if (!hasPermission(answer, 'invoices:create')) return forbidden();\n * ```\n */\nexport class ApiKeyVerifier {\n private readonly endpoint: string;\n private readonly timeoutMs: number;\n private readonly cacheTtlMs: number;\n private readonly cache = new Map<string, { answer: ActiveApiKey; until: number }>();\n\n constructor(private readonly config: ApiKeyVerifierConfig) {\n // A heuristic, and enough of one: a page has a `document`, and no server runtime\n // this SDK supports (Node, Deno, Bun, edge workers) defines one.\n if (typeof (globalThis as { document?: unknown }).document !== 'undefined') {\n throw new ConfigurationError(\n 'API key verification uses your client secret and must run on your server, never in a browser.',\n );\n }\n if (!config.issuer || !config.clientId || !config.clientSecret) {\n throw new ConfigurationError('Verifying API keys needs issuer, clientId and clientSecret.');\n }\n assertSecureIssuer(config.issuer);\n\n const cacheTtlMs = config.cacheTtlMs ?? 0;\n if (!Number.isFinite(cacheTtlMs) || cacheTtlMs < 0 || cacheTtlMs > MAX_API_KEY_CACHE_TTL_MS) {\n throw new ConfigurationError(\n `cacheTtlMs must be between 0 and ${MAX_API_KEY_CACHE_TTL_MS}: a cached answer keeps a revoked key working for that long.`,\n );\n }\n\n this.endpoint = `${config.issuer.replace(/\\/$/, '')}/oauth/api-keys/verify`;\n this.timeoutMs = config.timeoutMs ?? 10_000;\n this.cacheTtlMs = cacheTtlMs;\n }\n\n /**\n * Ask Cbox ID whether `key` is good for this app, and what it may do.\n *\n * Resolves `{ active: false }` for every kind of bad key, without a request for an empty\n * one. Throws `AuthenticationError` when the call itself fails (wrong client\n * credentials, an unreachable instance) or when the answer cannot be trusted — one that\n * names another app's `client_id`, or does not have the promised shape. Both mean \"do\n * not let this request in\"; the difference is that a throw is something to look at.\n */\n async verifyApiKey(key: string): Promise<ApiKeyVerification> {\n if (typeof key !== 'string' || key.trim() === '') {\n return { active: false };\n }\n\n const cacheKey = this.cacheTtlMs > 0 ? await digest(key) : null;\n if (cacheKey !== null) {\n const hit = this.cache.get(cacheKey);\n if (hit && hit.until > Date.now()) {\n return hit.answer;\n }\n this.cache.delete(cacheKey);\n }\n\n const answer = this.parse(await this.request(key));\n\n if (cacheKey !== null && answer.active) {\n this.remember(cacheKey, answer);\n }\n\n return answer;\n }\n\n private async request(key: string): Promise<unknown> {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), this.timeoutMs);\n\n let response: Response;\n try {\n response = await fetch(this.endpoint, {\n method: 'POST',\n headers: {\n authorization: `Basic ${btoa(`${this.config.clientId}:${this.config.clientSecret}`)}`,\n accept: 'application/json',\n 'content-type': 'application/x-www-form-urlencoded',\n },\n body: new URLSearchParams({ key }),\n signal: controller.signal,\n });\n } catch (error) {\n throw new AuthenticationError(`API key verification request failed: ${String(error)}`);\n } finally {\n clearTimeout(timer);\n }\n\n if (!response.ok) {\n throw await oauthError(response, 'API key verification failed');\n }\n\n return response.json().catch(() => null);\n }\n\n private parse(body: unknown): ApiKeyVerification {\n if (typeof body !== 'object' || body === null) {\n throw new AuthenticationError('API key verification returned a body that is not a JSON object.');\n }\n\n const record = body as Record<string, unknown>;\n\n // Only a literal `true` lets anybody in.\n if (record['active'] !== true) {\n return { active: false };\n }\n\n const keyId = record['key_id'];\n const sub = record['sub'];\n const org = record['org'];\n const clientId = record['client_id'];\n const expiresAt = record['expires_at'];\n const permissions = record['permissions'];\n\n if (\n !nonEmpty(keyId) ||\n !nonEmpty(sub) ||\n !nonEmpty(org) ||\n !nonEmpty(clientId) ||\n !Array.isArray(permissions) ||\n (expiresAt !== null && expiresAt !== undefined && typeof expiresAt !== 'string')\n ) {\n throw new AuthenticationError('API key verification returned an active answer without the promised fields.');\n }\n\n // THE BINDING, CHECKED AGAIN HERE. Cbox ID only ever finds a key bound to the calling\n // client, so this cannot fire against a correct instance — which is exactly when a\n // check that it holds is worth having: a proxy, a misrouted base URL, or a shared\n // secret between two apps would otherwise let one app's customers into another's API.\n if (clientId !== this.config.clientId) {\n throw new AuthenticationError(\n `API key verification answered for client ${clientId}, not this app (${this.config.clientId}); refusing it.`,\n );\n }\n\n // Past its expiry by our clock is expired, whatever the instance thought a moment ago.\n if (typeof expiresAt === 'string') {\n const expiry = Date.parse(expiresAt);\n if (Number.isNaN(expiry) || expiry <= Date.now()) {\n return { active: false };\n }\n }\n\n const role = record['org_role'];\n\n return {\n active: true,\n key_id: keyId,\n sub,\n org,\n org_role: typeof role === 'string' && ORGANIZATION_ROLES.has(role) ? (role as OrganizationRole) : null,\n permissions: permissions.filter((value): value is string => nonEmpty(value)),\n client_id: clientId,\n expires_at: typeof expiresAt === 'string' ? expiresAt : null,\n };\n }\n\n private remember(cacheKey: string, answer: ActiveApiKey): void {\n const expiry = answer.expires_at === null ? Number.POSITIVE_INFINITY : Date.parse(answer.expires_at);\n const until = Math.min(Date.now() + this.cacheTtlMs, expiry);\n\n if (this.cache.size >= MAX_CACHE_ENTRIES) {\n const oldest = this.cache.keys().next();\n if (!oldest.done) {\n this.cache.delete(oldest.value);\n }\n }\n\n this.cache.set(cacheKey, { answer, until });\n }\n}\n\nfunction nonEmpty(value: unknown): value is string {\n return typeof value === 'string' && value !== '';\n}\n\n/** The cache is keyed by a hash, so live keys are not sitting in memory as plaintext. */\nasync function digest(key: string): Promise<string> {\n const bytes = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(key));\n return Array.from(new Uint8Array(bytes), (byte) => byte.toString(16).padStart(2, '0')).join('');\n}\n"]}
|
|
@@ -1,63 +1,7 @@
|
|
|
1
|
+
import { assertSecureIssuer } from './chunk-QQW63PGU.js';
|
|
2
|
+
import { AuthenticationError, ConfigurationError, InvalidStateError, oauthError } from './chunk-SEUD4TE4.js';
|
|
1
3
|
import { createRemoteJWKSet, jwtVerify } from 'jose';
|
|
2
4
|
|
|
3
|
-
// src/errors.ts
|
|
4
|
-
var CboxIdError = class extends Error {
|
|
5
|
-
constructor(message) {
|
|
6
|
-
super(message);
|
|
7
|
-
this.name = new.target.name;
|
|
8
|
-
}
|
|
9
|
-
};
|
|
10
|
-
var ConfigurationError = class extends CboxIdError {
|
|
11
|
-
};
|
|
12
|
-
var InvalidStateError = class extends CboxIdError {
|
|
13
|
-
};
|
|
14
|
-
var AuthenticationError = class extends CboxIdError {
|
|
15
|
-
constructor(message, error, errorDescription, status, retryAfter) {
|
|
16
|
-
super(message);
|
|
17
|
-
this.error = error;
|
|
18
|
-
this.errorDescription = errorDescription;
|
|
19
|
-
this.status = status;
|
|
20
|
-
this.retryAfter = retryAfter;
|
|
21
|
-
}
|
|
22
|
-
error;
|
|
23
|
-
errorDescription;
|
|
24
|
-
status;
|
|
25
|
-
retryAfter;
|
|
26
|
-
/** Whether waiting `retryAfter` seconds and repeating the same request is worth it. */
|
|
27
|
-
get isRateLimited() {
|
|
28
|
-
return this.status === 429;
|
|
29
|
-
}
|
|
30
|
-
};
|
|
31
|
-
async function oauthError(response, fallback) {
|
|
32
|
-
const body = await response.text().catch(() => "");
|
|
33
|
-
let code;
|
|
34
|
-
let description;
|
|
35
|
-
try {
|
|
36
|
-
const parsed = JSON.parse(body);
|
|
37
|
-
if (typeof parsed === "object" && parsed !== null) {
|
|
38
|
-
const { error, error_description: errorDescription } = parsed;
|
|
39
|
-
code = typeof error === "string" ? error : void 0;
|
|
40
|
-
description = typeof errorDescription === "string" ? errorDescription : void 0;
|
|
41
|
-
}
|
|
42
|
-
} catch {
|
|
43
|
-
}
|
|
44
|
-
const detail = code ?? `HTTP ${response.status}`;
|
|
45
|
-
const retryAfterHeader = response.headers.get("retry-after");
|
|
46
|
-
const retryAfter = retryAfterHeader !== null && /^\d+$/.test(retryAfterHeader.trim()) ? Number(retryAfterHeader.trim()) : void 0;
|
|
47
|
-
return new AuthenticationError(`${fallback}: ${detail}`, code, description, response.status, retryAfter);
|
|
48
|
-
}
|
|
49
|
-
var FrontendApiError = class extends CboxIdError {
|
|
50
|
-
constructor(message, code, status, retryAfter) {
|
|
51
|
-
super(message);
|
|
52
|
-
this.code = code;
|
|
53
|
-
this.status = status;
|
|
54
|
-
this.retryAfter = retryAfter;
|
|
55
|
-
}
|
|
56
|
-
code;
|
|
57
|
-
status;
|
|
58
|
-
retryAfter;
|
|
59
|
-
};
|
|
60
|
-
|
|
61
5
|
// src/pkce.ts
|
|
62
6
|
function base64url(bytes) {
|
|
63
7
|
let binary = "";
|
|
@@ -77,6 +21,75 @@ async function challenge(verifier) {
|
|
|
77
21
|
return base64url(new Uint8Array(digest));
|
|
78
22
|
}
|
|
79
23
|
|
|
24
|
+
// src/claims.ts
|
|
25
|
+
var ORGANIZATION_ROLES = /* @__PURE__ */ new Set([
|
|
26
|
+
"owner",
|
|
27
|
+
"admin",
|
|
28
|
+
"developer",
|
|
29
|
+
"member",
|
|
30
|
+
"viewer"
|
|
31
|
+
]);
|
|
32
|
+
function organization(source) {
|
|
33
|
+
const claims = claimsOf(source);
|
|
34
|
+
const id = claims["org"];
|
|
35
|
+
if (typeof id !== "string" || id === "") {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
const name = claims["org_name"];
|
|
39
|
+
const role = claims["org_role"];
|
|
40
|
+
return {
|
|
41
|
+
id,
|
|
42
|
+
name: typeof name === "string" && name !== "" ? name : null,
|
|
43
|
+
role: typeof role === "string" && ORGANIZATION_ROLES.has(role) ? role : null
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function actor(source) {
|
|
47
|
+
return parseActor(claimsOf(source)["act"], 0);
|
|
48
|
+
}
|
|
49
|
+
function isSupportSession(source) {
|
|
50
|
+
return actor(source) !== null;
|
|
51
|
+
}
|
|
52
|
+
function sessionId(source) {
|
|
53
|
+
const sid = claimsOf(source)["sid"];
|
|
54
|
+
return typeof sid === "string" && sid !== "" ? sid : null;
|
|
55
|
+
}
|
|
56
|
+
function roles(source) {
|
|
57
|
+
return stringList(claimsOf(source)["roles"]);
|
|
58
|
+
}
|
|
59
|
+
function permissions(source) {
|
|
60
|
+
return stringList(claimsOf(source)["permissions"]);
|
|
61
|
+
}
|
|
62
|
+
function hasRole(source, role) {
|
|
63
|
+
return roles(source).includes(role);
|
|
64
|
+
}
|
|
65
|
+
function hasPermission(source, permission) {
|
|
66
|
+
return permissions(source).includes(permission);
|
|
67
|
+
}
|
|
68
|
+
var MAX_ACTOR_DEPTH = 8;
|
|
69
|
+
function parseActor(claim, depth) {
|
|
70
|
+
if (claim === void 0 || claim === null) {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
if (typeof claim !== "object" || Array.isArray(claim)) {
|
|
74
|
+
return { sub: null, actor: null };
|
|
75
|
+
}
|
|
76
|
+
const record = claim;
|
|
77
|
+
const sub = record["sub"];
|
|
78
|
+
return {
|
|
79
|
+
sub: typeof sub === "string" && sub !== "" ? sub : null,
|
|
80
|
+
actor: depth + 1 < MAX_ACTOR_DEPTH ? parseActor(record["act"], depth + 1) : null
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
function stringList(claim) {
|
|
84
|
+
return Array.isArray(claim) ? claim.filter((value) => typeof value === "string" && value !== "") : [];
|
|
85
|
+
}
|
|
86
|
+
function claimsOf(source) {
|
|
87
|
+
return isUser(source) ? source.claims : source;
|
|
88
|
+
}
|
|
89
|
+
function isUser(source) {
|
|
90
|
+
return typeof source["claims"] === "object" && source["claims"] !== null && typeof source["accessToken"] === "string";
|
|
91
|
+
}
|
|
92
|
+
|
|
80
93
|
// src/vault.ts
|
|
81
94
|
var VaultClient = class {
|
|
82
95
|
constructor(issuer, token, timeoutMs = 1e4) {
|
|
@@ -211,6 +224,58 @@ async function verifyWebhook(options) {
|
|
|
211
224
|
);
|
|
212
225
|
return timingSafeEqual(expected, signature);
|
|
213
226
|
}
|
|
227
|
+
function header(headers, name) {
|
|
228
|
+
if (typeof Headers !== "undefined" && headers instanceof Headers) {
|
|
229
|
+
return headers.get(name) ?? void 0;
|
|
230
|
+
}
|
|
231
|
+
const record = headers;
|
|
232
|
+
const key = Object.keys(record).find((k) => k.toLowerCase() === name);
|
|
233
|
+
const value = key === void 0 ? void 0 : record[key];
|
|
234
|
+
return Array.isArray(value) ? value.join(" ") : value;
|
|
235
|
+
}
|
|
236
|
+
function base64ToBytes(value) {
|
|
237
|
+
try {
|
|
238
|
+
const binary = atob(value);
|
|
239
|
+
return Uint8Array.from(binary, (c) => c.charCodeAt(0));
|
|
240
|
+
} catch {
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
function toBase64(bytes) {
|
|
245
|
+
let binary = "";
|
|
246
|
+
for (const byte of new Uint8Array(bytes)) {
|
|
247
|
+
binary += String.fromCharCode(byte);
|
|
248
|
+
}
|
|
249
|
+
return btoa(binary);
|
|
250
|
+
}
|
|
251
|
+
async function verifyStandardWebhook(options) {
|
|
252
|
+
const { payload, headers, secret } = options;
|
|
253
|
+
const toleranceSeconds = options.toleranceSeconds ?? 300;
|
|
254
|
+
const now = options.now ?? Math.floor(Date.now() / 1e3);
|
|
255
|
+
const id = header(headers, "webhook-id");
|
|
256
|
+
const timestamp = header(headers, "webhook-timestamp");
|
|
257
|
+
const signatures = header(headers, "webhook-signature");
|
|
258
|
+
if (!id || !timestamp || !signatures || !/^\d+$/.test(timestamp)) {
|
|
259
|
+
return false;
|
|
260
|
+
}
|
|
261
|
+
if (Math.abs(now - Number(timestamp)) > toleranceSeconds) {
|
|
262
|
+
return false;
|
|
263
|
+
}
|
|
264
|
+
const keyBytes = secret.startsWith("whsec_") ? base64ToBytes(secret.slice("whsec_".length)) : new TextEncoder().encode(secret);
|
|
265
|
+
if (keyBytes === null || keyBytes.length === 0) {
|
|
266
|
+
return false;
|
|
267
|
+
}
|
|
268
|
+
const key = await crypto.subtle.importKey("raw", keyBytes, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
|
|
269
|
+
const expected = toBase64(await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(`${id}.${timestamp}.${payload}`)));
|
|
270
|
+
let valid = false;
|
|
271
|
+
for (const candidate of signatures.split(" ")) {
|
|
272
|
+
const [version, signature] = candidate.split(",", 2);
|
|
273
|
+
if (version === "v1" && signature !== void 0 && timingSafeEqual(expected, signature)) {
|
|
274
|
+
valid = true;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return valid;
|
|
278
|
+
}
|
|
214
279
|
|
|
215
280
|
// src/discovery.ts
|
|
216
281
|
var Discovery = class {
|
|
@@ -276,26 +341,6 @@ function trimSlash(value) {
|
|
|
276
341
|
return value.replace(/\/+$/, "");
|
|
277
342
|
}
|
|
278
343
|
|
|
279
|
-
// src/issuer.ts
|
|
280
|
-
function assertSecureIssuer(issuer) {
|
|
281
|
-
let url;
|
|
282
|
-
try {
|
|
283
|
-
url = new URL(issuer);
|
|
284
|
-
} catch {
|
|
285
|
-
throw new ConfigurationError("Cbox ID config `issuer` is not a valid URL.");
|
|
286
|
-
}
|
|
287
|
-
if (url.protocol === "https:") {
|
|
288
|
-
return;
|
|
289
|
-
}
|
|
290
|
-
const loopback = url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "[::1]" || url.hostname === "::1";
|
|
291
|
-
if (url.protocol === "http:" && loopback) {
|
|
292
|
-
return;
|
|
293
|
-
}
|
|
294
|
-
throw new ConfigurationError(
|
|
295
|
-
`Cbox ID config \`issuer\` must be https (got ${url.protocol}//${url.hostname}).`
|
|
296
|
-
);
|
|
297
|
-
}
|
|
298
|
-
|
|
299
344
|
// src/client.ts
|
|
300
345
|
var CboxIdClient = class {
|
|
301
346
|
constructor(config) {
|
|
@@ -306,9 +351,6 @@ var CboxIdClient = class {
|
|
|
306
351
|
if (!config.clientId) {
|
|
307
352
|
throw new ConfigurationError("Cbox ID config `clientId` is required.");
|
|
308
353
|
}
|
|
309
|
-
if (!config.redirectUri) {
|
|
310
|
-
throw new ConfigurationError("Cbox ID config `redirectUri` is required.");
|
|
311
|
-
}
|
|
312
354
|
assertSecureIssuer(config.issuer);
|
|
313
355
|
this.discovery = new Discovery(
|
|
314
356
|
config.issuer,
|
|
@@ -325,21 +367,29 @@ var CboxIdClient = class {
|
|
|
325
367
|
* to {@link authenticate} on the callback.
|
|
326
368
|
*/
|
|
327
369
|
async createAuthorizationRequest(options = {}) {
|
|
370
|
+
const prompt = promptValue(options.prompt);
|
|
371
|
+
assertOrganizationOptions(options, prompt);
|
|
328
372
|
const codeVerifier = createVerifier();
|
|
329
373
|
const state = options.state ?? randomToken(16);
|
|
330
374
|
const nonce = randomToken(16);
|
|
375
|
+
const redirectUri = options.redirectUri ?? this.config.redirectUri;
|
|
376
|
+
if (redirectUri === void 0 || redirectUri === "") {
|
|
377
|
+
throw new ConfigurationError(
|
|
378
|
+
"A `redirectUri` is required to start the browser sign-in flow. Set it in the config, or pass it to createAuthorizationRequest."
|
|
379
|
+
);
|
|
380
|
+
}
|
|
331
381
|
const params = new URLSearchParams({
|
|
332
382
|
response_type: "code",
|
|
333
383
|
client_id: this.config.clientId,
|
|
334
|
-
redirect_uri:
|
|
384
|
+
redirect_uri: redirectUri,
|
|
335
385
|
scope: (options.scopes ?? this.scopes()).join(" "),
|
|
336
386
|
state,
|
|
337
387
|
nonce,
|
|
338
388
|
code_challenge: await challenge(codeVerifier),
|
|
339
389
|
code_challenge_method: "S256"
|
|
340
390
|
});
|
|
341
|
-
if (
|
|
342
|
-
params.set("prompt",
|
|
391
|
+
if (prompt.length > 0) {
|
|
392
|
+
params.set("prompt", prompt.join(" "));
|
|
343
393
|
}
|
|
344
394
|
if (options.loginHint) {
|
|
345
395
|
params.set("login_hint", options.loginHint);
|
|
@@ -347,6 +397,12 @@ var CboxIdClient = class {
|
|
|
347
397
|
if (typeof options.maxAge === "number") {
|
|
348
398
|
params.set("max_age", String(options.maxAge));
|
|
349
399
|
}
|
|
400
|
+
if (options.organization !== void 0) {
|
|
401
|
+
params.set("organization", options.organization);
|
|
402
|
+
}
|
|
403
|
+
if (options.organizationHint !== void 0) {
|
|
404
|
+
params.set("organization_hint", options.organizationHint);
|
|
405
|
+
}
|
|
350
406
|
const endpoint = await this.discovery.endpoint("authorization_endpoint");
|
|
351
407
|
return {
|
|
352
408
|
url: `${endpoint}?${params.toString()}`,
|
|
@@ -355,9 +411,27 @@ var CboxIdClient = class {
|
|
|
355
411
|
nonce,
|
|
356
412
|
// Carried through so authenticate() can hold the instance to it. Only present
|
|
357
413
|
// when requested (exactOptionalPropertyTypes).
|
|
358
|
-
...typeof options.maxAge === "number" ? { maxAge: options.maxAge } : {}
|
|
414
|
+
...typeof options.maxAge === "number" ? { maxAge: options.maxAge } : {},
|
|
415
|
+
...options.organization !== void 0 ? { organization: options.organization } : {}
|
|
359
416
|
};
|
|
360
417
|
}
|
|
418
|
+
/**
|
|
419
|
+
* Switch the signed-in person to another organization: a new authorization bound to
|
|
420
|
+
* `organizationId`. Persist and redirect exactly as for
|
|
421
|
+
* {@link createAuthorizationRequest} — it is one, with `organization` set.
|
|
422
|
+
*
|
|
423
|
+
* Cbox ID already holds the person's session, so this is normally a redirect there and
|
|
424
|
+
* straight back with no sign-in form. The tokens that come back carry the new `org`,
|
|
425
|
+
* `org_role`, `roles` and `permissions`; replace your session with them rather than
|
|
426
|
+
* patching the old one, because every one of those can differ between organizations.
|
|
427
|
+
*
|
|
428
|
+
* A person who is not (or is no longer) an active member of that organization comes
|
|
429
|
+
* back with `error=access_denied`, which {@link authenticate} throws as an
|
|
430
|
+
* `AuthenticationError` with `error === 'access_denied'`.
|
|
431
|
+
*/
|
|
432
|
+
switchOrganization(organizationId, options = {}) {
|
|
433
|
+
return this.createAuthorizationRequest({ ...options, organization: organizationId });
|
|
434
|
+
}
|
|
361
435
|
/**
|
|
362
436
|
* Complete login on your callback route: verify state, exchange the code with the
|
|
363
437
|
* PKCE verifier, verify the id_token, and return the authenticated user.
|
|
@@ -372,7 +446,9 @@ var CboxIdClient = class {
|
|
|
372
446
|
}
|
|
373
447
|
if (params.error) {
|
|
374
448
|
throw new AuthenticationError(
|
|
375
|
-
`Cbox ID returned an error: ${params.error}${params.error_description ? ` (${params.error_description})` : ""}
|
|
449
|
+
`Cbox ID returned an error: ${params.error}${params.error_description ? ` (${params.error_description})` : ""}`,
|
|
450
|
+
params.error,
|
|
451
|
+
params.error_description ?? void 0
|
|
376
452
|
);
|
|
377
453
|
}
|
|
378
454
|
if (!params.code) {
|
|
@@ -394,6 +470,24 @@ var CboxIdClient = class {
|
|
|
394
470
|
"A max_age was requested but no id_token was returned, so the authentication age could not be verified."
|
|
395
471
|
);
|
|
396
472
|
}
|
|
473
|
+
const user = await this.identityFrom(tokens, verified);
|
|
474
|
+
if (stored.organization !== void 0 && user.organizationId !== stored.organization) {
|
|
475
|
+
throw new AuthenticationError(
|
|
476
|
+
`The sign-in was bound to organization ${stored.organization}, but the tokens are for ${user.organizationId ?? "no organization"}. The instance may not support organization selection.`
|
|
477
|
+
);
|
|
478
|
+
}
|
|
479
|
+
return user;
|
|
480
|
+
}
|
|
481
|
+
/**
|
|
482
|
+
* Turn a token response into the verified {@link CboxUser} every flow returns.
|
|
483
|
+
*
|
|
484
|
+
* Shared by the browser flow and the device flow rather than written twice: the checks
|
|
485
|
+
* in here — the UserInfo subject matching the id_token's, the merge that lets UserInfo
|
|
486
|
+
* enrich but never overwrite signed claims — are the ones that decide whether an
|
|
487
|
+
* identity is trustworthy. A second copy is a second place for one of them to be
|
|
488
|
+
* quietly missing, and the flow it was missing from would look exactly as correct.
|
|
489
|
+
*/
|
|
490
|
+
async identityFrom(tokens, verified) {
|
|
397
491
|
const profile = await this.userinfo(tokens.access_token);
|
|
398
492
|
const verifiedSub = verified["sub"];
|
|
399
493
|
const profileSub = profile["sub"];
|
|
@@ -411,6 +505,11 @@ var CboxIdClient = class {
|
|
|
411
505
|
email: typeof claims["email"] === "string" ? claims["email"] : null,
|
|
412
506
|
name: typeof claims["name"] === "string" ? claims["name"] : null,
|
|
413
507
|
organizationId: typeof claims["org"] === "string" ? claims["org"] : null,
|
|
508
|
+
organization: organization(claims),
|
|
509
|
+
roles: roles(claims),
|
|
510
|
+
permissions: permissions(claims),
|
|
511
|
+
actor: actor(claims),
|
|
512
|
+
sessionId: sessionId(claims),
|
|
414
513
|
// Only present when the instance emitted the claim (exactOptionalPropertyTypes).
|
|
415
514
|
...organizations ? { organizations } : {},
|
|
416
515
|
claims,
|
|
@@ -420,6 +519,118 @@ var CboxIdClient = class {
|
|
|
420
519
|
expiresIn: typeof tokens.expires_in === "number" ? tokens.expires_in : 0
|
|
421
520
|
};
|
|
422
521
|
}
|
|
522
|
+
/**
|
|
523
|
+
* Start the device authorization grant (RFC 8628) — the flow for a program with no
|
|
524
|
+
* browser of its own: a CLI, a CI job, a container, a TV.
|
|
525
|
+
*
|
|
526
|
+
* Print `userCode` and `verificationUri`, then call
|
|
527
|
+
* {@link CboxIdClient.pollDeviceToken}. If the machine has a desktop you may also open
|
|
528
|
+
* `verificationUriComplete`, which fills the code in — but print the code anyway: the
|
|
529
|
+
* machine running your program is often not the one the person is looking at.
|
|
530
|
+
*
|
|
531
|
+
* The scopes are bounded by what the app is REGISTERED for. A device request naming one
|
|
532
|
+
* outside that ceiling is refused with `invalid_scope` rather than quietly reduced,
|
|
533
|
+
* because no browser is in front of it to notice a smaller grant.
|
|
534
|
+
*
|
|
535
|
+
* @throws ConfigurationError when the instance advertises no device endpoint.
|
|
536
|
+
*/
|
|
537
|
+
async requestDeviceAuthorization(scopes) {
|
|
538
|
+
const endpoint = await this.discovery.optionalEndpoint("device_authorization_endpoint");
|
|
539
|
+
if (endpoint === null) {
|
|
540
|
+
throw new ConfigurationError(
|
|
541
|
+
"This instance does not advertise a device_authorization_endpoint, so it does not support CLI sign-in."
|
|
542
|
+
);
|
|
543
|
+
}
|
|
544
|
+
const body = new URLSearchParams({
|
|
545
|
+
client_id: this.config.clientId,
|
|
546
|
+
scope: (scopes ?? this.scopes()).join(" ")
|
|
547
|
+
});
|
|
548
|
+
if (this.config.clientSecret) {
|
|
549
|
+
body.set("client_secret", this.config.clientSecret);
|
|
550
|
+
}
|
|
551
|
+
const response = await this.post(endpoint, body);
|
|
552
|
+
if (!response.ok) {
|
|
553
|
+
throw await oauthError(response, "Device authorization request failed");
|
|
554
|
+
}
|
|
555
|
+
const payload = await response.json();
|
|
556
|
+
const deviceCode = payload["device_code"];
|
|
557
|
+
const userCode = payload["user_code"];
|
|
558
|
+
const verificationUri = payload["verification_uri"];
|
|
559
|
+
if (typeof deviceCode !== "string" || typeof userCode !== "string" || typeof verificationUri !== "string") {
|
|
560
|
+
throw new AuthenticationError("The device authorization response was incomplete.");
|
|
561
|
+
}
|
|
562
|
+
return {
|
|
563
|
+
deviceCode,
|
|
564
|
+
userCode,
|
|
565
|
+
verificationUri,
|
|
566
|
+
verificationUriComplete: typeof payload["verification_uri_complete"] === "string" ? payload["verification_uri_complete"] : null,
|
|
567
|
+
expiresIn: typeof payload["expires_in"] === "number" ? payload["expires_in"] : 600,
|
|
568
|
+
// RFC 8628 §3.2: absent means 5 seconds. Polling faster than the server allows is
|
|
569
|
+
// answered with `slow_down`, so the default is the one the server assumes too.
|
|
570
|
+
interval: typeof payload["interval"] === "number" ? payload["interval"] : 5
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
/**
|
|
574
|
+
* Poll until the person approves, and return them.
|
|
575
|
+
*
|
|
576
|
+
* Blocks. It honours the server's `interval`, backs off permanently by five seconds on
|
|
577
|
+
* `slow_down` (RFC 8628 §3.5), and stops on the three answers that are final: they
|
|
578
|
+
* declined, the code expired, or tokens arrived. Pass an `AbortSignal` to give up
|
|
579
|
+
* early — a CLI should let Ctrl-C work.
|
|
580
|
+
*
|
|
581
|
+
* @throws AuthenticationError when the person declines or the code expires.
|
|
582
|
+
*/
|
|
583
|
+
async pollDeviceToken(authorization, options = {}) {
|
|
584
|
+
const endpoint = await this.discovery.endpoint("token_endpoint");
|
|
585
|
+
const deadline = Date.now() + authorization.expiresIn * 1e3;
|
|
586
|
+
let intervalMs = authorization.interval * 1e3;
|
|
587
|
+
for (; ; ) {
|
|
588
|
+
if (options.signal?.aborted === true) {
|
|
589
|
+
throw new AuthenticationError("Device sign-in was cancelled.");
|
|
590
|
+
}
|
|
591
|
+
if (Date.now() > deadline) {
|
|
592
|
+
throw new AuthenticationError(
|
|
593
|
+
"The device code expired before it was approved. Start the sign-in again."
|
|
594
|
+
);
|
|
595
|
+
}
|
|
596
|
+
await sleep(intervalMs, options.signal);
|
|
597
|
+
const body = new URLSearchParams({
|
|
598
|
+
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
|
599
|
+
device_code: authorization.deviceCode,
|
|
600
|
+
client_id: this.config.clientId
|
|
601
|
+
});
|
|
602
|
+
if (this.config.clientSecret) {
|
|
603
|
+
body.set("client_secret", this.config.clientSecret);
|
|
604
|
+
}
|
|
605
|
+
const response = await this.post(endpoint, body);
|
|
606
|
+
if (response.ok) {
|
|
607
|
+
const tokens = await response.json();
|
|
608
|
+
if (!tokens.access_token) {
|
|
609
|
+
throw new AuthenticationError("The device token response carried no access token.");
|
|
610
|
+
}
|
|
611
|
+
const verified = tokens.id_token ? await this.verifyIdToken(tokens.id_token, "") : {};
|
|
612
|
+
return await this.identityFrom(tokens, verified);
|
|
613
|
+
}
|
|
614
|
+
const payload = await response.json().catch(() => ({}));
|
|
615
|
+
const error = typeof payload["error"] === "string" ? payload["error"] : "invalid_request";
|
|
616
|
+
if (error === "authorization_pending") {
|
|
617
|
+
continue;
|
|
618
|
+
}
|
|
619
|
+
if (error === "slow_down") {
|
|
620
|
+
intervalMs += 5e3;
|
|
621
|
+
continue;
|
|
622
|
+
}
|
|
623
|
+
if (error === "access_denied") {
|
|
624
|
+
throw new AuthenticationError("Sign-in was declined.");
|
|
625
|
+
}
|
|
626
|
+
if (error === "expired_token") {
|
|
627
|
+
throw new AuthenticationError(
|
|
628
|
+
"The device code expired before it was approved. Start the sign-in again."
|
|
629
|
+
);
|
|
630
|
+
}
|
|
631
|
+
throw await oauthError(response, "Device sign-in failed");
|
|
632
|
+
}
|
|
633
|
+
}
|
|
423
634
|
/**
|
|
424
635
|
* Exchange a refresh token for a fresh access token (OAuth 2.0 `refresh_token`
|
|
425
636
|
* grant). Cbox ID rotates refresh tokens and detects reuse, so ALWAYS persist the
|
|
@@ -467,6 +678,25 @@ var CboxIdClient = class {
|
|
|
467
678
|
const base = `${this.config.issuer.replace(/\/$/, "")}${this.accountPath()}`;
|
|
468
679
|
return returnTo ? `${base}?${new URLSearchParams({ return_to: returnTo }).toString()}` : base;
|
|
469
680
|
}
|
|
681
|
+
/**
|
|
682
|
+
* The hosted page where a person creates and revokes API keys for your API — Cbox ID's
|
|
683
|
+
* `/account/api-keys`, preselected to this app (or `clientId`). Keys made there are
|
|
684
|
+
* checked with `ApiKeyVerifier` from `@cboxdk/id-js/server`.
|
|
685
|
+
*
|
|
686
|
+
* `returnTo` becomes a link back to your app, honoured only for an origin the app
|
|
687
|
+
* registered. `organization` picks which of the person's organizations the keys act in;
|
|
688
|
+
* omitted, the page uses the one they are in.
|
|
689
|
+
*/
|
|
690
|
+
apiKeysUrl(options = {}) {
|
|
691
|
+
const params = new URLSearchParams({ client_id: options.clientId ?? this.config.clientId });
|
|
692
|
+
if (options.returnTo) {
|
|
693
|
+
params.set("return_to", options.returnTo);
|
|
694
|
+
}
|
|
695
|
+
if (options.organization) {
|
|
696
|
+
params.set("organization", options.organization);
|
|
697
|
+
}
|
|
698
|
+
return `${this.config.issuer.replace(/\/$/, "")}/account/api-keys?${params.toString()}`;
|
|
699
|
+
}
|
|
470
700
|
/**
|
|
471
701
|
* The RP-initiated logout URL, or null when the instance advertises none.
|
|
472
702
|
*
|
|
@@ -606,10 +836,16 @@ var CboxIdClient = class {
|
|
|
606
836
|
return new VaultClient(this.config.issuer, accessToken, this.config.timeoutMs ?? 1e4);
|
|
607
837
|
}
|
|
608
838
|
async exchange(code, verifier, redirectUri) {
|
|
839
|
+
const callback = redirectUri ?? this.config.redirectUri;
|
|
840
|
+
if (callback === void 0 || callback === "") {
|
|
841
|
+
throw new ConfigurationError(
|
|
842
|
+
"A `redirectUri` is required to exchange an authorization code. It must be the same one the authorization request used."
|
|
843
|
+
);
|
|
844
|
+
}
|
|
609
845
|
const body = new URLSearchParams({
|
|
610
846
|
grant_type: "authorization_code",
|
|
611
847
|
code,
|
|
612
|
-
redirect_uri:
|
|
848
|
+
redirect_uri: callback,
|
|
613
849
|
client_id: this.config.clientId,
|
|
614
850
|
code_verifier: verifier
|
|
615
851
|
});
|
|
@@ -693,6 +929,37 @@ var CboxIdClient = class {
|
|
|
693
929
|
}
|
|
694
930
|
}
|
|
695
931
|
};
|
|
932
|
+
function promptValue(prompt) {
|
|
933
|
+
if (prompt === void 0) {
|
|
934
|
+
return [];
|
|
935
|
+
}
|
|
936
|
+
const values = [...new Set(typeof prompt === "string" ? [prompt] : prompt)];
|
|
937
|
+
if (values.includes("none") && values.length > 1) {
|
|
938
|
+
throw new ConfigurationError("`prompt: 'none'` cannot be combined with another prompt value.");
|
|
939
|
+
}
|
|
940
|
+
return values;
|
|
941
|
+
}
|
|
942
|
+
function assertOrganizationOptions(options, prompt) {
|
|
943
|
+
if (options.organization === "") {
|
|
944
|
+
throw new ConfigurationError("`organization` is empty. Omit it to sign in without binding to an organization.");
|
|
945
|
+
}
|
|
946
|
+
if (options.organizationHint === "") {
|
|
947
|
+
throw new ConfigurationError("`organizationHint` is empty. Omit it when you have no organization to suggest.");
|
|
948
|
+
}
|
|
949
|
+
if (options.organization === void 0) {
|
|
950
|
+
return;
|
|
951
|
+
}
|
|
952
|
+
if (prompt.includes("select_organization")) {
|
|
953
|
+
throw new ConfigurationError(
|
|
954
|
+
"`organization` binds the sign-in to one organization, so `prompt: 'select_organization'` has nothing to choose. Use `organizationHint` to preselect an organization in the picker instead."
|
|
955
|
+
);
|
|
956
|
+
}
|
|
957
|
+
if (prompt.includes("create_organization")) {
|
|
958
|
+
throw new ConfigurationError(
|
|
959
|
+
"`organization` binds the sign-in to an existing organization and `prompt: 'create_organization'` creates a new one. Send one or the other."
|
|
960
|
+
);
|
|
961
|
+
}
|
|
962
|
+
}
|
|
696
963
|
function timingSafeEqualString(a, b) {
|
|
697
964
|
if (a.length !== b.length) {
|
|
698
965
|
return false;
|
|
@@ -721,7 +988,24 @@ function parseOrganizations(claim) {
|
|
|
721
988
|
}
|
|
722
989
|
return orgs;
|
|
723
990
|
}
|
|
991
|
+
function sleep(ms, signal) {
|
|
992
|
+
return new Promise((resolve, reject) => {
|
|
993
|
+
if (signal?.aborted === true) {
|
|
994
|
+
reject(new AuthenticationError("Device sign-in was cancelled."));
|
|
995
|
+
return;
|
|
996
|
+
}
|
|
997
|
+
const timer = setTimeout(() => {
|
|
998
|
+
signal?.removeEventListener("abort", onAbort);
|
|
999
|
+
resolve();
|
|
1000
|
+
}, ms);
|
|
1001
|
+
const onAbort = () => {
|
|
1002
|
+
clearTimeout(timer);
|
|
1003
|
+
reject(new AuthenticationError("Device sign-in was cancelled."));
|
|
1004
|
+
};
|
|
1005
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
1006
|
+
});
|
|
1007
|
+
}
|
|
724
1008
|
|
|
725
|
-
export {
|
|
726
|
-
//# sourceMappingURL=chunk-
|
|
727
|
-
//# sourceMappingURL=chunk-
|
|
1009
|
+
export { CboxIdClient, Discovery, VaultClient, actor, challenge, createVerifier, hasPermission, hasRole, isSupportSession, organization, permissions, randomToken, roles, sessionId, verifyStandardWebhook, verifyWebhook };
|
|
1010
|
+
//# sourceMappingURL=chunk-CXGKOJRJ.js.map
|
|
1011
|
+
//# sourceMappingURL=chunk-CXGKOJRJ.js.map
|