@cboxdk/id-js 0.14.0 → 0.17.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 +237 -2
- package/dist/chunk-ASOQN47M.js +81 -0
- package/dist/chunk-ASOQN47M.js.map +1 -0
- package/dist/{chunk-UPRO73GX.js → chunk-G3CD7X4U.js} +334 -71
- package/dist/chunk-G3CD7X4U.js.map +1 -0
- package/dist/chunk-WYTGLCMF.js +154 -0
- package/dist/chunk-WYTGLCMF.js.map +1 -0
- package/dist/{client-CecDIu_N.d.cts → client-CEXddiCR.d.cts} +81 -136
- package/dist/{client-CecDIu_N.d.ts → client-CdwdigzG.d.ts} +81 -136
- package/dist/index.cjs +419 -34
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +39 -4
- package/dist/index.d.ts +39 -4
- package/dist/index.js +64 -25
- package/dist/index.js.map +1 -1
- package/dist/nextjs.cjs +531 -24
- 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 +41 -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 +4 -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 +6 -1
- package/dist/chunk-UPRO73GX.js.map +0 -1
|
@@ -1,63 +1,6 @@
|
|
|
1
|
+
import { AuthenticationError, ConfigurationError, assertSecureIssuer, InvalidStateError, oauthError } from './chunk-ASOQN47M.js';
|
|
1
2
|
import { createRemoteJWKSet, jwtVerify } from 'jose';
|
|
2
3
|
|
|
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 ?? (body.trim() === "" ? `HTTP ${response.status}` : body.slice(0, 200));
|
|
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
4
|
// src/pkce.ts
|
|
62
5
|
function base64url(bytes) {
|
|
63
6
|
let binary = "";
|
|
@@ -77,6 +20,75 @@ async function challenge(verifier) {
|
|
|
77
20
|
return base64url(new Uint8Array(digest));
|
|
78
21
|
}
|
|
79
22
|
|
|
23
|
+
// src/claims.ts
|
|
24
|
+
var ORGANIZATION_ROLES = /* @__PURE__ */ new Set([
|
|
25
|
+
"owner",
|
|
26
|
+
"admin",
|
|
27
|
+
"developer",
|
|
28
|
+
"member",
|
|
29
|
+
"viewer"
|
|
30
|
+
]);
|
|
31
|
+
function organization(source) {
|
|
32
|
+
const claims = claimsOf(source);
|
|
33
|
+
const id = claims["org"];
|
|
34
|
+
if (typeof id !== "string" || id === "") {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
const name = claims["org_name"];
|
|
38
|
+
const role = claims["org_role"];
|
|
39
|
+
return {
|
|
40
|
+
id,
|
|
41
|
+
name: typeof name === "string" && name !== "" ? name : null,
|
|
42
|
+
role: typeof role === "string" && ORGANIZATION_ROLES.has(role) ? role : null
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function actor(source) {
|
|
46
|
+
return parseActor(claimsOf(source)["act"], 0);
|
|
47
|
+
}
|
|
48
|
+
function isSupportSession(source) {
|
|
49
|
+
return actor(source) !== null;
|
|
50
|
+
}
|
|
51
|
+
function sessionId(source) {
|
|
52
|
+
const sid = claimsOf(source)["sid"];
|
|
53
|
+
return typeof sid === "string" && sid !== "" ? sid : null;
|
|
54
|
+
}
|
|
55
|
+
function roles(source) {
|
|
56
|
+
return stringList(claimsOf(source)["roles"]);
|
|
57
|
+
}
|
|
58
|
+
function permissions(source) {
|
|
59
|
+
return stringList(claimsOf(source)["permissions"]);
|
|
60
|
+
}
|
|
61
|
+
function hasRole(source, role) {
|
|
62
|
+
return roles(source).includes(role);
|
|
63
|
+
}
|
|
64
|
+
function hasPermission(source, permission) {
|
|
65
|
+
return permissions(source).includes(permission);
|
|
66
|
+
}
|
|
67
|
+
var MAX_ACTOR_DEPTH = 8;
|
|
68
|
+
function parseActor(claim, depth) {
|
|
69
|
+
if (claim === void 0 || claim === null) {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
if (typeof claim !== "object" || Array.isArray(claim)) {
|
|
73
|
+
return { sub: null, actor: null };
|
|
74
|
+
}
|
|
75
|
+
const record = claim;
|
|
76
|
+
const sub = record["sub"];
|
|
77
|
+
return {
|
|
78
|
+
sub: typeof sub === "string" && sub !== "" ? sub : null,
|
|
79
|
+
actor: depth + 1 < MAX_ACTOR_DEPTH ? parseActor(record["act"], depth + 1) : null
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
function stringList(claim) {
|
|
83
|
+
return Array.isArray(claim) ? claim.filter((value) => typeof value === "string" && value !== "") : [];
|
|
84
|
+
}
|
|
85
|
+
function claimsOf(source) {
|
|
86
|
+
return isUser(source) ? source.claims : source;
|
|
87
|
+
}
|
|
88
|
+
function isUser(source) {
|
|
89
|
+
return typeof source["claims"] === "object" && source["claims"] !== null && typeof source["accessToken"] === "string";
|
|
90
|
+
}
|
|
91
|
+
|
|
80
92
|
// src/vault.ts
|
|
81
93
|
var VaultClient = class {
|
|
82
94
|
constructor(issuer, token, timeoutMs = 1e4) {
|
|
@@ -229,7 +241,7 @@ var Discovery = class {
|
|
|
229
241
|
if (this.cached && this.cached.expiresAt > this.now()) {
|
|
230
242
|
return this.cached.doc;
|
|
231
243
|
}
|
|
232
|
-
const url = `${this.issuer
|
|
244
|
+
const url = `${trimSlash(this.issuer)}/.well-known/openid-configuration`;
|
|
233
245
|
const controller = new AbortController();
|
|
234
246
|
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
235
247
|
let doc;
|
|
@@ -250,6 +262,11 @@ var Discovery = class {
|
|
|
250
262
|
if (typeof doc.issuer !== "string" || doc.issuer === "") {
|
|
251
263
|
throw new AuthenticationError("The discovery document was missing an issuer.");
|
|
252
264
|
}
|
|
265
|
+
if (trimSlash(doc.issuer) !== trimSlash(this.issuer)) {
|
|
266
|
+
throw new AuthenticationError(
|
|
267
|
+
`The discovery document is for a different issuer (${doc.issuer}).`
|
|
268
|
+
);
|
|
269
|
+
}
|
|
253
270
|
this.cached = { doc, expiresAt: this.now() + this.cacheTtlMs };
|
|
254
271
|
return doc;
|
|
255
272
|
}
|
|
@@ -267,6 +284,9 @@ var Discovery = class {
|
|
|
267
284
|
return typeof value === "string" && value !== "" ? value : null;
|
|
268
285
|
}
|
|
269
286
|
};
|
|
287
|
+
function trimSlash(value) {
|
|
288
|
+
return value.replace(/\/+$/, "");
|
|
289
|
+
}
|
|
270
290
|
|
|
271
291
|
// src/client.ts
|
|
272
292
|
var CboxIdClient = class {
|
|
@@ -278,9 +298,7 @@ var CboxIdClient = class {
|
|
|
278
298
|
if (!config.clientId) {
|
|
279
299
|
throw new ConfigurationError("Cbox ID config `clientId` is required.");
|
|
280
300
|
}
|
|
281
|
-
|
|
282
|
-
throw new ConfigurationError("Cbox ID config `redirectUri` is required.");
|
|
283
|
-
}
|
|
301
|
+
assertSecureIssuer(config.issuer);
|
|
284
302
|
this.discovery = new Discovery(
|
|
285
303
|
config.issuer,
|
|
286
304
|
config.timeoutMs ?? 1e4,
|
|
@@ -296,21 +314,29 @@ var CboxIdClient = class {
|
|
|
296
314
|
* to {@link authenticate} on the callback.
|
|
297
315
|
*/
|
|
298
316
|
async createAuthorizationRequest(options = {}) {
|
|
317
|
+
const prompt = promptValue(options.prompt);
|
|
318
|
+
assertOrganizationOptions(options, prompt);
|
|
299
319
|
const codeVerifier = createVerifier();
|
|
300
320
|
const state = options.state ?? randomToken(16);
|
|
301
321
|
const nonce = randomToken(16);
|
|
322
|
+
const redirectUri = options.redirectUri ?? this.config.redirectUri;
|
|
323
|
+
if (redirectUri === void 0 || redirectUri === "") {
|
|
324
|
+
throw new ConfigurationError(
|
|
325
|
+
"A `redirectUri` is required to start the browser sign-in flow. Set it in the config, or pass it to createAuthorizationRequest."
|
|
326
|
+
);
|
|
327
|
+
}
|
|
302
328
|
const params = new URLSearchParams({
|
|
303
329
|
response_type: "code",
|
|
304
330
|
client_id: this.config.clientId,
|
|
305
|
-
redirect_uri:
|
|
331
|
+
redirect_uri: redirectUri,
|
|
306
332
|
scope: (options.scopes ?? this.scopes()).join(" "),
|
|
307
333
|
state,
|
|
308
334
|
nonce,
|
|
309
335
|
code_challenge: await challenge(codeVerifier),
|
|
310
336
|
code_challenge_method: "S256"
|
|
311
337
|
});
|
|
312
|
-
if (
|
|
313
|
-
params.set("prompt",
|
|
338
|
+
if (prompt.length > 0) {
|
|
339
|
+
params.set("prompt", prompt.join(" "));
|
|
314
340
|
}
|
|
315
341
|
if (options.loginHint) {
|
|
316
342
|
params.set("login_hint", options.loginHint);
|
|
@@ -318,6 +344,12 @@ var CboxIdClient = class {
|
|
|
318
344
|
if (typeof options.maxAge === "number") {
|
|
319
345
|
params.set("max_age", String(options.maxAge));
|
|
320
346
|
}
|
|
347
|
+
if (options.organization !== void 0) {
|
|
348
|
+
params.set("organization", options.organization);
|
|
349
|
+
}
|
|
350
|
+
if (options.organizationHint !== void 0) {
|
|
351
|
+
params.set("organization_hint", options.organizationHint);
|
|
352
|
+
}
|
|
321
353
|
const endpoint = await this.discovery.endpoint("authorization_endpoint");
|
|
322
354
|
return {
|
|
323
355
|
url: `${endpoint}?${params.toString()}`,
|
|
@@ -326,9 +358,27 @@ var CboxIdClient = class {
|
|
|
326
358
|
nonce,
|
|
327
359
|
// Carried through so authenticate() can hold the instance to it. Only present
|
|
328
360
|
// when requested (exactOptionalPropertyTypes).
|
|
329
|
-
...typeof options.maxAge === "number" ? { maxAge: options.maxAge } : {}
|
|
361
|
+
...typeof options.maxAge === "number" ? { maxAge: options.maxAge } : {},
|
|
362
|
+
...options.organization !== void 0 ? { organization: options.organization } : {}
|
|
330
363
|
};
|
|
331
364
|
}
|
|
365
|
+
/**
|
|
366
|
+
* Switch the signed-in person to another organization: a new authorization bound to
|
|
367
|
+
* `organizationId`. Persist and redirect exactly as for
|
|
368
|
+
* {@link createAuthorizationRequest} — it is one, with `organization` set.
|
|
369
|
+
*
|
|
370
|
+
* Cbox ID already holds the person's session, so this is normally a redirect there and
|
|
371
|
+
* straight back with no sign-in form. The tokens that come back carry the new `org`,
|
|
372
|
+
* `org_role`, `roles` and `permissions`; replace your session with them rather than
|
|
373
|
+
* patching the old one, because every one of those can differ between organizations.
|
|
374
|
+
*
|
|
375
|
+
* A person who is not (or is no longer) an active member of that organization comes
|
|
376
|
+
* back with `error=access_denied`, which {@link authenticate} throws as an
|
|
377
|
+
* `AuthenticationError` with `error === 'access_denied'`.
|
|
378
|
+
*/
|
|
379
|
+
switchOrganization(organizationId, options = {}) {
|
|
380
|
+
return this.createAuthorizationRequest({ ...options, organization: organizationId });
|
|
381
|
+
}
|
|
332
382
|
/**
|
|
333
383
|
* Complete login on your callback route: verify state, exchange the code with the
|
|
334
384
|
* PKCE verifier, verify the id_token, and return the authenticated user.
|
|
@@ -343,7 +393,9 @@ var CboxIdClient = class {
|
|
|
343
393
|
}
|
|
344
394
|
if (params.error) {
|
|
345
395
|
throw new AuthenticationError(
|
|
346
|
-
`Cbox ID returned an error: ${params.error}${params.error_description ? ` (${params.error_description})` : ""}
|
|
396
|
+
`Cbox ID returned an error: ${params.error}${params.error_description ? ` (${params.error_description})` : ""}`,
|
|
397
|
+
params.error,
|
|
398
|
+
params.error_description ?? void 0
|
|
347
399
|
);
|
|
348
400
|
}
|
|
349
401
|
if (!params.code) {
|
|
@@ -365,6 +417,24 @@ var CboxIdClient = class {
|
|
|
365
417
|
"A max_age was requested but no id_token was returned, so the authentication age could not be verified."
|
|
366
418
|
);
|
|
367
419
|
}
|
|
420
|
+
const user = await this.identityFrom(tokens, verified);
|
|
421
|
+
if (stored.organization !== void 0 && user.organizationId !== stored.organization) {
|
|
422
|
+
throw new AuthenticationError(
|
|
423
|
+
`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.`
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
return user;
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* Turn a token response into the verified {@link CboxUser} every flow returns.
|
|
430
|
+
*
|
|
431
|
+
* Shared by the browser flow and the device flow rather than written twice: the checks
|
|
432
|
+
* in here — the UserInfo subject matching the id_token's, the merge that lets UserInfo
|
|
433
|
+
* enrich but never overwrite signed claims — are the ones that decide whether an
|
|
434
|
+
* identity is trustworthy. A second copy is a second place for one of them to be
|
|
435
|
+
* quietly missing, and the flow it was missing from would look exactly as correct.
|
|
436
|
+
*/
|
|
437
|
+
async identityFrom(tokens, verified) {
|
|
368
438
|
const profile = await this.userinfo(tokens.access_token);
|
|
369
439
|
const verifiedSub = verified["sub"];
|
|
370
440
|
const profileSub = profile["sub"];
|
|
@@ -382,6 +452,11 @@ var CboxIdClient = class {
|
|
|
382
452
|
email: typeof claims["email"] === "string" ? claims["email"] : null,
|
|
383
453
|
name: typeof claims["name"] === "string" ? claims["name"] : null,
|
|
384
454
|
organizationId: typeof claims["org"] === "string" ? claims["org"] : null,
|
|
455
|
+
organization: organization(claims),
|
|
456
|
+
roles: roles(claims),
|
|
457
|
+
permissions: permissions(claims),
|
|
458
|
+
actor: actor(claims),
|
|
459
|
+
sessionId: sessionId(claims),
|
|
385
460
|
// Only present when the instance emitted the claim (exactOptionalPropertyTypes).
|
|
386
461
|
...organizations ? { organizations } : {},
|
|
387
462
|
claims,
|
|
@@ -391,6 +466,118 @@ var CboxIdClient = class {
|
|
|
391
466
|
expiresIn: typeof tokens.expires_in === "number" ? tokens.expires_in : 0
|
|
392
467
|
};
|
|
393
468
|
}
|
|
469
|
+
/**
|
|
470
|
+
* Start the device authorization grant (RFC 8628) — the flow for a program with no
|
|
471
|
+
* browser of its own: a CLI, a CI job, a container, a TV.
|
|
472
|
+
*
|
|
473
|
+
* Print `userCode` and `verificationUri`, then call
|
|
474
|
+
* {@link CboxIdClient.pollDeviceToken}. If the machine has a desktop you may also open
|
|
475
|
+
* `verificationUriComplete`, which fills the code in — but print the code anyway: the
|
|
476
|
+
* machine running your program is often not the one the person is looking at.
|
|
477
|
+
*
|
|
478
|
+
* The scopes are bounded by what the app is REGISTERED for. A device request naming one
|
|
479
|
+
* outside that ceiling is refused with `invalid_scope` rather than quietly reduced,
|
|
480
|
+
* because no browser is in front of it to notice a smaller grant.
|
|
481
|
+
*
|
|
482
|
+
* @throws ConfigurationError when the instance advertises no device endpoint.
|
|
483
|
+
*/
|
|
484
|
+
async requestDeviceAuthorization(scopes) {
|
|
485
|
+
const endpoint = await this.discovery.optionalEndpoint("device_authorization_endpoint");
|
|
486
|
+
if (endpoint === null) {
|
|
487
|
+
throw new ConfigurationError(
|
|
488
|
+
"This instance does not advertise a device_authorization_endpoint, so it does not support CLI sign-in."
|
|
489
|
+
);
|
|
490
|
+
}
|
|
491
|
+
const body = new URLSearchParams({
|
|
492
|
+
client_id: this.config.clientId,
|
|
493
|
+
scope: (scopes ?? this.scopes()).join(" ")
|
|
494
|
+
});
|
|
495
|
+
if (this.config.clientSecret) {
|
|
496
|
+
body.set("client_secret", this.config.clientSecret);
|
|
497
|
+
}
|
|
498
|
+
const response = await this.post(endpoint, body);
|
|
499
|
+
if (!response.ok) {
|
|
500
|
+
throw await oauthError(response, "Device authorization request failed");
|
|
501
|
+
}
|
|
502
|
+
const payload = await response.json();
|
|
503
|
+
const deviceCode = payload["device_code"];
|
|
504
|
+
const userCode = payload["user_code"];
|
|
505
|
+
const verificationUri = payload["verification_uri"];
|
|
506
|
+
if (typeof deviceCode !== "string" || typeof userCode !== "string" || typeof verificationUri !== "string") {
|
|
507
|
+
throw new AuthenticationError("The device authorization response was incomplete.");
|
|
508
|
+
}
|
|
509
|
+
return {
|
|
510
|
+
deviceCode,
|
|
511
|
+
userCode,
|
|
512
|
+
verificationUri,
|
|
513
|
+
verificationUriComplete: typeof payload["verification_uri_complete"] === "string" ? payload["verification_uri_complete"] : null,
|
|
514
|
+
expiresIn: typeof payload["expires_in"] === "number" ? payload["expires_in"] : 600,
|
|
515
|
+
// RFC 8628 §3.2: absent means 5 seconds. Polling faster than the server allows is
|
|
516
|
+
// answered with `slow_down`, so the default is the one the server assumes too.
|
|
517
|
+
interval: typeof payload["interval"] === "number" ? payload["interval"] : 5
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
/**
|
|
521
|
+
* Poll until the person approves, and return them.
|
|
522
|
+
*
|
|
523
|
+
* Blocks. It honours the server's `interval`, backs off permanently by five seconds on
|
|
524
|
+
* `slow_down` (RFC 8628 §3.5), and stops on the three answers that are final: they
|
|
525
|
+
* declined, the code expired, or tokens arrived. Pass an `AbortSignal` to give up
|
|
526
|
+
* early — a CLI should let Ctrl-C work.
|
|
527
|
+
*
|
|
528
|
+
* @throws AuthenticationError when the person declines or the code expires.
|
|
529
|
+
*/
|
|
530
|
+
async pollDeviceToken(authorization, options = {}) {
|
|
531
|
+
const endpoint = await this.discovery.endpoint("token_endpoint");
|
|
532
|
+
const deadline = Date.now() + authorization.expiresIn * 1e3;
|
|
533
|
+
let intervalMs = authorization.interval * 1e3;
|
|
534
|
+
for (; ; ) {
|
|
535
|
+
if (options.signal?.aborted === true) {
|
|
536
|
+
throw new AuthenticationError("Device sign-in was cancelled.");
|
|
537
|
+
}
|
|
538
|
+
if (Date.now() > deadline) {
|
|
539
|
+
throw new AuthenticationError(
|
|
540
|
+
"The device code expired before it was approved. Start the sign-in again."
|
|
541
|
+
);
|
|
542
|
+
}
|
|
543
|
+
await sleep(intervalMs, options.signal);
|
|
544
|
+
const body = new URLSearchParams({
|
|
545
|
+
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
|
546
|
+
device_code: authorization.deviceCode,
|
|
547
|
+
client_id: this.config.clientId
|
|
548
|
+
});
|
|
549
|
+
if (this.config.clientSecret) {
|
|
550
|
+
body.set("client_secret", this.config.clientSecret);
|
|
551
|
+
}
|
|
552
|
+
const response = await this.post(endpoint, body);
|
|
553
|
+
if (response.ok) {
|
|
554
|
+
const tokens = await response.json();
|
|
555
|
+
if (!tokens.access_token) {
|
|
556
|
+
throw new AuthenticationError("The device token response carried no access token.");
|
|
557
|
+
}
|
|
558
|
+
const verified = tokens.id_token ? await this.verifyIdToken(tokens.id_token, "") : {};
|
|
559
|
+
return await this.identityFrom(tokens, verified);
|
|
560
|
+
}
|
|
561
|
+
const payload = await response.json().catch(() => ({}));
|
|
562
|
+
const error = typeof payload["error"] === "string" ? payload["error"] : "invalid_request";
|
|
563
|
+
if (error === "authorization_pending") {
|
|
564
|
+
continue;
|
|
565
|
+
}
|
|
566
|
+
if (error === "slow_down") {
|
|
567
|
+
intervalMs += 5e3;
|
|
568
|
+
continue;
|
|
569
|
+
}
|
|
570
|
+
if (error === "access_denied") {
|
|
571
|
+
throw new AuthenticationError("Sign-in was declined.");
|
|
572
|
+
}
|
|
573
|
+
if (error === "expired_token") {
|
|
574
|
+
throw new AuthenticationError(
|
|
575
|
+
"The device code expired before it was approved. Start the sign-in again."
|
|
576
|
+
);
|
|
577
|
+
}
|
|
578
|
+
throw await oauthError(response, "Device sign-in failed");
|
|
579
|
+
}
|
|
580
|
+
}
|
|
394
581
|
/**
|
|
395
582
|
* Exchange a refresh token for a fresh access token (OAuth 2.0 `refresh_token`
|
|
396
583
|
* grant). Cbox ID rotates refresh tokens and detects reuse, so ALWAYS persist the
|
|
@@ -416,6 +603,9 @@ var CboxIdClient = class {
|
|
|
416
603
|
if (!tokens.access_token) {
|
|
417
604
|
throw new AuthenticationError("The refresh response carried no access token.");
|
|
418
605
|
}
|
|
606
|
+
if (tokens.id_token) {
|
|
607
|
+
await this.verifyIdToken(tokens.id_token, "");
|
|
608
|
+
}
|
|
419
609
|
return {
|
|
420
610
|
accessToken: tokens.access_token,
|
|
421
611
|
// Keep the presented token when the server does not rotate (OAuth 2.0 §6);
|
|
@@ -435,6 +625,25 @@ var CboxIdClient = class {
|
|
|
435
625
|
const base = `${this.config.issuer.replace(/\/$/, "")}${this.accountPath()}`;
|
|
436
626
|
return returnTo ? `${base}?${new URLSearchParams({ return_to: returnTo }).toString()}` : base;
|
|
437
627
|
}
|
|
628
|
+
/**
|
|
629
|
+
* The hosted page where a person creates and revokes API keys for your API — Cbox ID's
|
|
630
|
+
* `/account/api-keys`, preselected to this app (or `clientId`). Keys made there are
|
|
631
|
+
* checked with `ApiKeyVerifier` from `@cboxdk/id-js/server`.
|
|
632
|
+
*
|
|
633
|
+
* `returnTo` becomes a link back to your app, honoured only for an origin the app
|
|
634
|
+
* registered. `organization` picks which of the person's organizations the keys act in;
|
|
635
|
+
* omitted, the page uses the one they are in.
|
|
636
|
+
*/
|
|
637
|
+
apiKeysUrl(options = {}) {
|
|
638
|
+
const params = new URLSearchParams({ client_id: options.clientId ?? this.config.clientId });
|
|
639
|
+
if (options.returnTo) {
|
|
640
|
+
params.set("return_to", options.returnTo);
|
|
641
|
+
}
|
|
642
|
+
if (options.organization) {
|
|
643
|
+
params.set("organization", options.organization);
|
|
644
|
+
}
|
|
645
|
+
return `${this.config.issuer.replace(/\/$/, "")}/account/api-keys?${params.toString()}`;
|
|
646
|
+
}
|
|
438
647
|
/**
|
|
439
648
|
* The RP-initiated logout URL, or null when the instance advertises none.
|
|
440
649
|
*
|
|
@@ -574,10 +783,16 @@ var CboxIdClient = class {
|
|
|
574
783
|
return new VaultClient(this.config.issuer, accessToken, this.config.timeoutMs ?? 1e4);
|
|
575
784
|
}
|
|
576
785
|
async exchange(code, verifier, redirectUri) {
|
|
786
|
+
const callback = redirectUri ?? this.config.redirectUri;
|
|
787
|
+
if (callback === void 0 || callback === "") {
|
|
788
|
+
throw new ConfigurationError(
|
|
789
|
+
"A `redirectUri` is required to exchange an authorization code. It must be the same one the authorization request used."
|
|
790
|
+
);
|
|
791
|
+
}
|
|
577
792
|
const body = new URLSearchParams({
|
|
578
793
|
grant_type: "authorization_code",
|
|
579
794
|
code,
|
|
580
|
-
redirect_uri:
|
|
795
|
+
redirect_uri: callback,
|
|
581
796
|
client_id: this.config.clientId,
|
|
582
797
|
code_verifier: verifier
|
|
583
798
|
});
|
|
@@ -661,6 +876,37 @@ var CboxIdClient = class {
|
|
|
661
876
|
}
|
|
662
877
|
}
|
|
663
878
|
};
|
|
879
|
+
function promptValue(prompt) {
|
|
880
|
+
if (prompt === void 0) {
|
|
881
|
+
return [];
|
|
882
|
+
}
|
|
883
|
+
const values = [...new Set(typeof prompt === "string" ? [prompt] : prompt)];
|
|
884
|
+
if (values.includes("none") && values.length > 1) {
|
|
885
|
+
throw new ConfigurationError("`prompt: 'none'` cannot be combined with another prompt value.");
|
|
886
|
+
}
|
|
887
|
+
return values;
|
|
888
|
+
}
|
|
889
|
+
function assertOrganizationOptions(options, prompt) {
|
|
890
|
+
if (options.organization === "") {
|
|
891
|
+
throw new ConfigurationError("`organization` is empty. Omit it to sign in without binding to an organization.");
|
|
892
|
+
}
|
|
893
|
+
if (options.organizationHint === "") {
|
|
894
|
+
throw new ConfigurationError("`organizationHint` is empty. Omit it when you have no organization to suggest.");
|
|
895
|
+
}
|
|
896
|
+
if (options.organization === void 0) {
|
|
897
|
+
return;
|
|
898
|
+
}
|
|
899
|
+
if (prompt.includes("select_organization")) {
|
|
900
|
+
throw new ConfigurationError(
|
|
901
|
+
"`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."
|
|
902
|
+
);
|
|
903
|
+
}
|
|
904
|
+
if (prompt.includes("create_organization")) {
|
|
905
|
+
throw new ConfigurationError(
|
|
906
|
+
"`organization` binds the sign-in to an existing organization and `prompt: 'create_organization'` creates a new one. Send one or the other."
|
|
907
|
+
);
|
|
908
|
+
}
|
|
909
|
+
}
|
|
664
910
|
function timingSafeEqualString(a, b) {
|
|
665
911
|
if (a.length !== b.length) {
|
|
666
912
|
return false;
|
|
@@ -689,7 +935,24 @@ function parseOrganizations(claim) {
|
|
|
689
935
|
}
|
|
690
936
|
return orgs;
|
|
691
937
|
}
|
|
938
|
+
function sleep(ms, signal) {
|
|
939
|
+
return new Promise((resolve, reject) => {
|
|
940
|
+
if (signal?.aborted === true) {
|
|
941
|
+
reject(new AuthenticationError("Device sign-in was cancelled."));
|
|
942
|
+
return;
|
|
943
|
+
}
|
|
944
|
+
const timer = setTimeout(() => {
|
|
945
|
+
signal?.removeEventListener("abort", onAbort);
|
|
946
|
+
resolve();
|
|
947
|
+
}, ms);
|
|
948
|
+
const onAbort = () => {
|
|
949
|
+
clearTimeout(timer);
|
|
950
|
+
reject(new AuthenticationError("Device sign-in was cancelled."));
|
|
951
|
+
};
|
|
952
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
953
|
+
});
|
|
954
|
+
}
|
|
692
955
|
|
|
693
|
-
export {
|
|
694
|
-
//# sourceMappingURL=chunk-
|
|
695
|
-
//# sourceMappingURL=chunk-
|
|
956
|
+
export { CboxIdClient, Discovery, VaultClient, actor, challenge, createVerifier, hasPermission, hasRole, isSupportSession, organization, permissions, randomToken, roles, sessionId, verifyWebhook };
|
|
957
|
+
//# sourceMappingURL=chunk-G3CD7X4U.js.map
|
|
958
|
+
//# sourceMappingURL=chunk-G3CD7X4U.js.map
|