@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.
@@ -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.replace(/\/$/, "")}/.well-known/openid-configuration`;
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
- if (!config.redirectUri) {
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: options.redirectUri ?? this.config.redirectUri,
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 (options.prompt) {
313
- params.set("prompt", options.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: redirectUri ?? this.config.redirectUri,
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 { AuthenticationError, CboxIdClient, CboxIdError, ConfigurationError, Discovery, FrontendApiError, InvalidStateError, VaultClient, challenge, createVerifier, randomToken, verifyWebhook };
694
- //# sourceMappingURL=chunk-UPRO73GX.js.map
695
- //# sourceMappingURL=chunk-UPRO73GX.js.map
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