@cboxdk/id-js 0.18.0 → 0.20.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.
Files changed (39) hide show
  1. package/CHANGELOG.md +185 -0
  2. package/README.md +121 -4
  3. package/RELEASE-CANDIDATE.md +11 -0
  4. package/dist/api-keys-BE3R3q_A.d.ts +105 -0
  5. package/dist/api-keys-C8-Q4eFA.d.cts +105 -0
  6. package/dist/chunk-EVZ6RB3K.js +98 -0
  7. package/dist/chunk-EVZ6RB3K.js.map +1 -0
  8. package/dist/{chunk-CXGKOJRJ.js → chunk-YGLMLIKL.js} +144 -9
  9. package/dist/chunk-YGLMLIKL.js.map +1 -0
  10. package/dist/{client-BxUHm5hX.d.cts → client-BT-jOm0p.d.cts} +126 -2
  11. package/dist/{client-BSZASrMo.d.ts → client-xlEJalu8.d.ts} +126 -2
  12. package/dist/decisions-C84Fzah4.d.ts +23 -0
  13. package/dist/decisions-DNd9UXwi.d.cts +23 -0
  14. package/dist/index.cjs +338 -28
  15. package/dist/index.cjs.map +1 -1
  16. package/dist/index.d.cts +84 -6
  17. package/dist/index.d.ts +84 -6
  18. package/dist/index.js +87 -15
  19. package/dist/index.js.map +1 -1
  20. package/dist/management.cjs +1115 -62
  21. package/dist/management.cjs.map +1 -1
  22. package/dist/management.d.cts +3618 -135
  23. package/dist/management.d.ts +3618 -135
  24. package/dist/management.js +1115 -63
  25. package/dist/management.js.map +1 -1
  26. package/dist/nextjs.cjs +136 -5
  27. package/dist/nextjs.cjs.map +1 -1
  28. package/dist/nextjs.d.cts +4 -3
  29. package/dist/nextjs.d.ts +4 -3
  30. package/dist/nextjs.js +1 -1
  31. package/dist/server.cjs +103 -9
  32. package/dist/server.cjs.map +1 -1
  33. package/dist/server.d.cts +3 -105
  34. package/dist/server.d.ts +3 -105
  35. package/dist/server.js +1 -0
  36. package/dist/{types-BYZldock.d.cts → types-CC1bz68x.d.cts} +30 -1
  37. package/dist/{types-BYZldock.d.ts → types-CC1bz68x.d.ts} +30 -1
  38. package/package.json +8 -3
  39. package/dist/chunk-CXGKOJRJ.js.map +0 -1
package/dist/index.cjs CHANGED
@@ -174,6 +174,7 @@ function assertSecureIssuer(issuer) {
174
174
  }
175
175
 
176
176
  // src/frontend.ts
177
+ var SECOND_FACTOR_KINDS = /* @__PURE__ */ new Set(["totp", "sms", "recovery_code"]);
177
178
  var CboxIdFrontend = class {
178
179
  issuer;
179
180
  publishableKey;
@@ -188,7 +189,7 @@ var CboxIdFrontend = class {
188
189
  * fetches of the same immutable document is a waste the SDK should absorb rather than
189
190
  * ask every consumer to.
190
191
  */
191
- configPromise = null;
192
+ configPromises = /* @__PURE__ */ new Map();
192
193
  constructor(options) {
193
194
  if (!options.issuer) {
194
195
  throw new ConfigurationError("issuer is required.");
@@ -215,13 +216,20 @@ var CboxIdFrontend = class {
215
216
  * Fetched once per client instance. The document is small and changes when somebody
216
217
  * edits it in the console, so a long-lived page picks changes up on its next full load
217
218
  * rather than mid-session — which is the right trade for something that decides layout.
219
+ * Each `organization` is fetched and cached on its own.
218
220
  */
219
- async config() {
220
- this.configPromise ??= this.get("/frontend/v1/config", {}, isFrontendConfig).catch((error) => {
221
- this.configPromise = null;
222
- throw error;
223
- });
224
- return this.configPromise;
221
+ async config(options = {}) {
222
+ const organization2 = options.organization ?? "";
223
+ let pending = this.configPromises.get(organization2);
224
+ if (pending === void 0) {
225
+ const path = organization2 === "" ? "/frontend/v1/config" : `/frontend/v1/config?${new URLSearchParams({ organization: organization2 }).toString()}`;
226
+ pending = this.get(path, {}, isFrontendConfig).catch((error) => {
227
+ this.configPromises.delete(organization2);
228
+ throw error;
229
+ });
230
+ this.configPromises.set(organization2, pending);
231
+ }
232
+ return pending;
225
233
  }
226
234
  /**
227
235
  * Who is signed in, given an access token your app already holds.
@@ -283,8 +291,12 @@ var CboxIdFrontend = class {
283
291
  if (body.status === "ok" && typeof body.login_ticket === "string") {
284
292
  return { status: "ok", loginTicket: body.login_ticket, expiresIn: body.expires_in ?? 60 };
285
293
  }
286
- if ((body.status === "mfa_required" || body.status === "otp_required") && typeof body.mfa_token === "string") {
287
- return { status: body.status, mfaToken: body.mfa_token };
294
+ if (body.status === "mfa_required" && typeof body.mfa_token === "string") {
295
+ const factors = Array.isArray(body.factors) ? body.factors.filter((f) => typeof f === "string" && SECOND_FACTOR_KINDS.has(f)) : void 0;
296
+ return { status: "mfa_required", mfaToken: body.mfa_token, ...factors ? { factors } : {} };
297
+ }
298
+ if (body.status === "otp_required" && typeof body.mfa_token === "string") {
299
+ return { status: "otp_required", mfaToken: body.mfa_token };
288
300
  }
289
301
  if (body.status === "sso_required") {
290
302
  return { status: "sso_required" };
@@ -301,6 +313,11 @@ var CboxIdFrontend = class {
301
313
  *
302
314
  * A wrong code costs an attempt, not the sign-in: five are allowed before the token
303
315
  * dies and the person starts from the password again.
316
+ *
317
+ * `method` says which challenge the code answers: `'mfa'` for an authenticator or
318
+ * recovery code, `'otp'` for an emailed code (`otp_required`), `'sms'` for a code sent
319
+ * with {@link sendSmsCode}. A texted code is six digits like an authenticator's, so the
320
+ * server cannot tell them apart by shape — answering one as the other is refused.
304
321
  */
305
322
  async submitSecondFactor(mfaToken, code, method = "mfa") {
306
323
  const response = await this.post("/frontend/v1/sign-in/factor", { mfa_token: mfaToken, code, method });
@@ -312,6 +329,41 @@ var CboxIdFrontend = class {
312
329
  }
313
330
  return body.status === "ok" && typeof body.login_ticket === "string" ? { status: "ok", loginTicket: body.login_ticket, expiresIn: body.expires_in ?? 60 } : { status: "invalid" };
314
331
  }
332
+ /**
333
+ * Text the person a sign-in code, when `mfa_required` listed `'sms'` in `factors`.
334
+ * Then finish with `submitSecondFactor(mfaToken, code, 'sms')`.
335
+ *
336
+ * Only call it when the person asks ("Text me a code"), never on page load: each send
337
+ * spends one of the token's five attempts — that is what bounds how many texts one
338
+ * proved password can cause — and every text costs the customer money.
339
+ *
340
+ * ```ts
341
+ * if (result.status === 'mfa_required' && result.factors?.includes('sms')) {
342
+ * const sent = await frontend.sendSmsCode(result.mfaToken)
343
+ * if (sent.status === 'sent') showCodeField(`We texted ${sent.to}`)
344
+ * }
345
+ * ```
346
+ */
347
+ async sendSmsCode(mfaToken) {
348
+ const response = await this.post("/frontend/v1/sign-in/factor/sms", { mfa_token: mfaToken });
349
+ let body;
350
+ try {
351
+ body = await response.json();
352
+ } catch {
353
+ throw new FrontendApiError("Cbox ID returned a body that is not JSON.", "malformed", response.status);
354
+ }
355
+ if (body.status === "sent" && typeof body.to === "string") {
356
+ return { status: "sent", to: body.to, expiresIn: typeof body.expires_in === "number" ? body.expires_in : 300 };
357
+ }
358
+ if (body.status === "rate_limited" || response.status === 429) {
359
+ const retryAfter = typeof body.retry_after === "number" ? body.retry_after : Number(response.headers.get("Retry-After") ?? "");
360
+ return { status: "rate_limited", retryAfter: Number.isFinite(retryAfter) && retryAfter > 0 ? retryAfter : void 0 };
361
+ }
362
+ if (body.status === "unavailable") {
363
+ return { status: "unavailable" };
364
+ }
365
+ return { status: "invalid" };
366
+ }
315
367
  /**
316
368
  * The WebAuthn options for a passkey sign-in, and the handle that carries the challenge.
317
369
  *
@@ -607,6 +659,14 @@ function hasRole(source, role) {
607
659
  function hasPermission(source, permission) {
608
660
  return permissions(source).includes(permission);
609
661
  }
662
+ var FEATURE_FLAGS_SCOPE = "feature_flags";
663
+ function featureFlags(source) {
664
+ const claim = claimsOf(source)["feature_flags"];
665
+ return Array.isArray(claim) ? stringList(claim) : null;
666
+ }
667
+ function hasFeature(source, key) {
668
+ return featureFlags(source)?.includes(key) ?? false;
669
+ }
610
670
  var MAX_ACTOR_DEPTH = 8;
611
671
  function parseActor(claim, depth) {
612
672
  if (claim === void 0 || claim === null) {
@@ -615,11 +675,11 @@ function parseActor(claim, depth) {
615
675
  if (typeof claim !== "object" || Array.isArray(claim)) {
616
676
  return { sub: null, actor: null };
617
677
  }
618
- const record = claim;
619
- const sub = record["sub"];
678
+ const record2 = claim;
679
+ const sub = record2["sub"];
620
680
  return {
621
681
  sub: typeof sub === "string" && sub !== "" ? sub : null,
622
- actor: depth + 1 < MAX_ACTOR_DEPTH ? parseActor(record["act"], depth + 1) : null
682
+ actor: depth + 1 < MAX_ACTOR_DEPTH ? parseActor(record2["act"], depth + 1) : null
623
683
  };
624
684
  }
625
685
  function stringList(claim) {
@@ -633,6 +693,43 @@ function isUser(source) {
633
693
  }
634
694
 
635
695
  // src/vault.ts
696
+ function withConnectReturn(connectUrl, options = {}) {
697
+ const url = new URL(connectUrl);
698
+ if (options.clientId) url.searchParams.set("client_id", options.clientId);
699
+ if (options.returnTo) url.searchParams.set("return_to", options.returnTo);
700
+ return url.toString();
701
+ }
702
+ function pipeConnectUrl(issuer, provider, options = {}) {
703
+ return withConnectReturn(
704
+ `${issuer.replace(/\/$/, "")}/account/connected-services/${encodeURIComponent(provider)}/connect`,
705
+ options
706
+ );
707
+ }
708
+ var PipeLeaseError = class extends CboxIdError {
709
+ constructor(message, error, status, connectUrl = null, retryAfter = null) {
710
+ super(message);
711
+ this.error = error;
712
+ this.status = status;
713
+ this.connectUrl = connectUrl;
714
+ this.retryAfter = retryAfter;
715
+ }
716
+ error;
717
+ status;
718
+ connectUrl;
719
+ retryAfter;
720
+ /** `connectUrl` with your `client_id` and `return_to` on it, or null when there is none. */
721
+ connectUrlWith(options) {
722
+ return this.connectUrl === null ? null : withConnectReturn(this.connectUrl, options);
723
+ }
724
+ };
725
+ var PipeNotConnectedError = class extends PipeLeaseError {
726
+ };
727
+ var PipeReauthorizationRequiredError = class extends PipeLeaseError {
728
+ };
729
+ var PipeTemporarilyUnavailableError = class extends PipeLeaseError {
730
+ };
731
+ var PipeLeaseDeniedError = class extends PipeLeaseError {
732
+ };
636
733
  var VaultClient = class {
637
734
  constructor(issuer, token, timeoutMs = 1e4) {
638
735
  this.token = token;
@@ -688,11 +785,69 @@ var VaultClient = class {
688
785
  return { secretId: r.secret_id, provider: r.provider, secret: r.secret, expiresAt: r.expires_at };
689
786
  });
690
787
  }
788
+ /**
789
+ * Lease a fresh access token for a person's connected account at `provider` (Pipes,
790
+ * scope `vault.lease`). The token is refreshed first when it is about to expire, so
791
+ * you never handle a refresh token.
792
+ *
793
+ * With a client-credentials token, name the person in `userId`. With a token issued to
794
+ * your app FOR a person, leave it out — the lease is theirs.
795
+ *
796
+ * ```ts
797
+ * try {
798
+ * const { accessToken } = await vault.leasePipeToken('github', { userId, purpose: 'list-repos' })
799
+ * } catch (e) {
800
+ * if (e instanceof PipeNotConnectedError || e instanceof PipeReauthorizationRequiredError) {
801
+ * return redirect(e.connectUrlWith({ clientId, returnTo }))
802
+ * }
803
+ * throw e
804
+ * }
805
+ * ```
806
+ */
807
+ async leasePipeToken(provider, options) {
808
+ const path = `/pipes/${encodeURIComponent(provider)}/token`;
809
+ const response = await this.send("POST", path, {
810
+ purpose: options.purpose,
811
+ ...options.userId !== void 0 ? { user_id: options.userId } : {}
812
+ });
813
+ let body = {};
814
+ try {
815
+ const parsed = await response.json();
816
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
817
+ body = parsed;
818
+ }
819
+ } catch {
820
+ }
821
+ if (!response.ok) {
822
+ throw pipeLeaseError(response, body);
823
+ }
824
+ if (typeof body.access_token !== "string") {
825
+ throw new PipeLeaseError("The pipe lease answered without an access token.", null, response.status);
826
+ }
827
+ return {
828
+ accessToken: body.access_token,
829
+ tokenType: "Bearer",
830
+ provider: String(body.provider ?? provider),
831
+ userId: String(body.user_id ?? ""),
832
+ connectionId: String(body.connection_id ?? ""),
833
+ scopes: Array.isArray(body.scopes) ? body.scopes.filter((s) => typeof s === "string") : [],
834
+ expiresAt: typeof body.expires_at === "string" ? body.expires_at : null,
835
+ leaseExpiresAt: String(body.lease_expires_at ?? ""),
836
+ metadata: isStringRecord(body.metadata) ? body.metadata : {}
837
+ };
838
+ }
691
839
  async json(method, path, body) {
692
840
  const response = await this.request(method, path, body);
693
841
  return await response.json();
694
842
  }
695
843
  async request(method, path, body) {
844
+ const response = await this.send(method, path, body);
845
+ if (!response.ok) {
846
+ throw new AuthenticationError(`Vault ${method} ${path} failed with status ${response.status}.`);
847
+ }
848
+ return response;
849
+ }
850
+ async send(method, path, body) {
696
851
  const controller = new AbortController();
697
852
  const timer = setTimeout(() => controller.abort(), this.timeoutMs);
698
853
  try {
@@ -707,16 +862,35 @@ var VaultClient = class {
707
862
  if (body) {
708
863
  init.body = JSON.stringify(body);
709
864
  }
710
- const response = await fetch(`${this.base}${path}`, init);
711
- if (!response.ok) {
712
- throw new AuthenticationError(`Vault ${method} ${path} failed with status ${response.status}.`);
713
- }
714
- return response;
865
+ return await fetch(`${this.base}${path}`, init);
715
866
  } finally {
716
867
  clearTimeout(timer);
717
868
  }
718
869
  }
719
870
  };
871
+ function pipeLeaseError(response, body) {
872
+ const error = typeof body.error === "string" ? body.error : null;
873
+ const message = typeof body.message === "string" && body.message !== "" ? body.message : `The pipe lease failed with status ${response.status}.`;
874
+ const connectUrl = typeof body.connect_url === "string" && body.connect_url !== "" ? body.connect_url : null;
875
+ const header2 = response.headers.get("retry-after")?.trim() ?? "";
876
+ const retryAfter = /^\d+$/.test(header2) ? Number(header2) : null;
877
+ if (response.status === 404 && connectUrl !== null) {
878
+ return new PipeNotConnectedError(message, error, 404, connectUrl, retryAfter);
879
+ }
880
+ if (response.status === 409 && connectUrl !== null) {
881
+ return new PipeReauthorizationRequiredError(message, error, 409, connectUrl, retryAfter);
882
+ }
883
+ if (response.status === 503) {
884
+ return new PipeTemporarilyUnavailableError(message, error, 503, connectUrl, retryAfter);
885
+ }
886
+ if (response.status === 403 && error !== "insufficient_scope") {
887
+ return new PipeLeaseDeniedError(message, error, 403, connectUrl, retryAfter);
888
+ }
889
+ return new PipeLeaseError(message, error, response.status, connectUrl, retryAfter);
890
+ }
891
+ function isStringRecord(value) {
892
+ return typeof value === "object" && value !== null && !Array.isArray(value) && Object.values(value).every((v) => typeof v === "string");
893
+ }
720
894
 
721
895
  // src/webhook.ts
722
896
  function timingSafeEqual2(a, b) {
@@ -770,9 +944,9 @@ function header(headers, name) {
770
944
  if (typeof Headers !== "undefined" && headers instanceof Headers) {
771
945
  return headers.get(name) ?? void 0;
772
946
  }
773
- const record = headers;
774
- const key = Object.keys(record).find((k) => k.toLowerCase() === name);
775
- const value = key === void 0 ? void 0 : record[key];
947
+ const record2 = headers;
948
+ const key = Object.keys(record2).find((k) => k.toLowerCase() === name);
949
+ const value = key === void 0 ? void 0 : record2[key];
776
950
  return Array.isArray(value) ? value.join(" ") : value;
777
951
  }
778
952
  function base64ToBytes(value) {
@@ -986,6 +1160,7 @@ var CboxIdClient = class {
986
1160
  organization: organization(claims),
987
1161
  roles: roles(claims),
988
1162
  permissions: permissions(claims),
1163
+ featureFlags: featureFlags(claims),
989
1164
  actor: actor(claims),
990
1165
  sessionId: sessionId(claims),
991
1166
  // Only present when the instance emitted the claim (exactOptionalPropertyTypes).
@@ -1165,6 +1340,18 @@ var CboxIdClient = class {
1165
1340
  * registered. `organization` picks which of the person's organizations the keys act in;
1166
1341
  * omitted, the page uses the one they are in.
1167
1342
  */
1343
+ /**
1344
+ * The hosted page where the signed-in person connects their account at `provider`
1345
+ * (Pipes) — `/account/connected-services/{provider}/connect`, preselected to this app.
1346
+ * They come back to `returnTo` with `?provider=…&status=connected|cancelled|failed`,
1347
+ * honoured only on an origin the app registered.
1348
+ */
1349
+ pipeConnectUrl(provider, returnTo) {
1350
+ return pipeConnectUrl(this.config.issuer, provider, {
1351
+ clientId: this.config.clientId,
1352
+ ...returnTo !== void 0 ? { returnTo } : {}
1353
+ });
1354
+ }
1168
1355
  apiKeysUrl(options = {}) {
1169
1356
  const params = new URLSearchParams({ client_id: options.clientId ?? this.config.clientId });
1170
1357
  if (options.returnTo) {
@@ -1457,11 +1644,11 @@ function parseOrganizations(claim) {
1457
1644
  if (typeof entry !== "object" || entry === null) {
1458
1645
  continue;
1459
1646
  }
1460
- const record = entry;
1461
- const id = record["id"];
1462
- const name = record["name"];
1647
+ const record2 = entry;
1648
+ const id = record2["id"];
1649
+ const name = record2["name"];
1463
1650
  if (typeof id === "string" && id !== "" && typeof name === "string" && name !== "") {
1464
- orgs.push({ id, name, role: typeof record["role"] === "string" ? record["role"] : null });
1651
+ orgs.push({ id, name, role: typeof record2["role"] === "string" ? record2["role"] : null });
1465
1652
  }
1466
1653
  }
1467
1654
  return orgs;
@@ -1515,6 +1702,11 @@ async function publishManifest(config, declaration) {
1515
1702
  throw new ConfigurationError("Publishing a manifest needs issuer, clientId and clientSecret.");
1516
1703
  }
1517
1704
  const timeoutMs = config.timeoutMs ?? 1e4;
1705
+ assertSecureIssuer(config.issuer);
1706
+ const issuer = new URL(config.issuer);
1707
+ if (issuer.username || issuer.password || issuer.search || issuer.hash) {
1708
+ throw new ConfigurationError("The manifest issuer must not contain credentials, a query or a fragment.");
1709
+ }
1518
1710
  const manifest = await buildManifest(declaration);
1519
1711
  const discovery = new Discovery(config.issuer, timeoutMs, 36e5);
1520
1712
  const token = await mintToken(config, discovery, timeoutMs);
@@ -1522,6 +1714,7 @@ async function publishManifest(config, declaration) {
1522
1714
  `${config.issuer.replace(/\/$/, "")}/api/v1/apps/manifest`,
1523
1715
  {
1524
1716
  method: "POST",
1717
+ redirect: "error",
1525
1718
  headers: {
1526
1719
  authorization: `Bearer ${token}`,
1527
1720
  accept: "application/json",
@@ -1532,10 +1725,20 @@ async function publishManifest(config, declaration) {
1532
1725
  timeoutMs
1533
1726
  );
1534
1727
  if (!response.ok) {
1535
- throw new AuthenticationError(`Manifest push failed: HTTP ${response.status} ${await response.text()}`);
1728
+ throw new AuthenticationError(`Manifest push failed: HTTP ${response.status}`);
1729
+ }
1730
+ const json2 = await response.json().catch(() => {
1731
+ throw new AuthenticationError("Cbox ID returned an invalid manifest acknowledgement.");
1732
+ });
1733
+ const checksum = await sha256Hex(canonicalManifestJson(declaration.permissions ?? [], declaration.roles ?? []));
1734
+ if (json2 === null || typeof json2 !== "object" || Array.isArray(json2)) {
1735
+ throw new AuthenticationError("Cbox ID did not acknowledge the authorization manifest.");
1736
+ }
1737
+ const summary = json2;
1738
+ if (summary.client_id !== config.clientId || summary.version !== manifest.version || summary.checksum !== checksum || typeof summary.unchanged !== "boolean" || !Number.isInteger(summary.roles_declared) || !Number.isInteger(summary.permissions_declared) || (summary.roles_declared ?? -1) < 0 || (summary.permissions_declared ?? -1) < 0 || !summary.unchanged && (summary.roles_declared !== manifest.roles.length || summary.permissions_declared !== manifest.permissions.length)) {
1739
+ throw new AuthenticationError("Cbox ID did not acknowledge this app and its complete authorization manifest.");
1536
1740
  }
1537
- const json2 = await response.json();
1538
- return json2 !== null && typeof json2 === "object" ? json2 : {};
1741
+ return summary;
1539
1742
  }
1540
1743
  async function mintToken(config, discovery, timeoutMs) {
1541
1744
  const response = await fetchWithTimeout(
@@ -1543,6 +1746,7 @@ async function mintToken(config, discovery, timeoutMs) {
1543
1746
  {
1544
1747
  method: "POST",
1545
1748
  headers: { "content-type": "application/x-www-form-urlencoded" },
1749
+ redirect: "error",
1546
1750
  body: new URLSearchParams({
1547
1751
  grant_type: "client_credentials",
1548
1752
  client_id: config.clientId,
@@ -1553,7 +1757,7 @@ async function mintToken(config, discovery, timeoutMs) {
1553
1757
  timeoutMs
1554
1758
  );
1555
1759
  const json2 = await response.json().catch(() => ({}));
1556
- if (typeof json2.access_token !== "string" || json2.access_token === "") {
1760
+ if (!response.ok || typeof json2.access_token !== "string" || json2.access_token === "") {
1557
1761
  throw new AuthenticationError(
1558
1762
  "Could not obtain an apps.manifest access token \u2014 check the client credentials and that the client holds the apps.manifest scope."
1559
1763
  );
@@ -1675,6 +1879,8 @@ async function fetchWithTimeout(url, init, timeoutMs) {
1675
1879
  const timer = setTimeout(() => controller.abort(), timeoutMs);
1676
1880
  try {
1677
1881
  return await fetch(url, { ...init, signal: controller.signal });
1882
+ } catch {
1883
+ throw new AuthenticationError("Cbox ID manifest publication could not be verified.");
1678
1884
  } finally {
1679
1885
  clearTimeout(timer);
1680
1886
  }
@@ -1688,30 +1894,134 @@ function assertLegacyLogin(legacy) {
1688
1894
  }
1689
1895
  }
1690
1896
 
1897
+ // src/decisions.ts
1898
+ var PermissionDecision = class {
1899
+ constructor(subject, organization2, clientId, organizationRole, organizationActive, permissions2) {
1900
+ this.subject = subject;
1901
+ this.organization = organization2;
1902
+ this.clientId = clientId;
1903
+ this.organizationRole = organizationRole;
1904
+ this.organizationActive = organizationActive;
1905
+ this.permissions = permissions2;
1906
+ }
1907
+ subject;
1908
+ organization;
1909
+ clientId;
1910
+ organizationRole;
1911
+ organizationActive;
1912
+ permissions;
1913
+ allows(permission) {
1914
+ return this.organizationActive && this.permissions[permission] === true;
1915
+ }
1916
+ allowed() {
1917
+ const values = Object.values(this.permissions);
1918
+ return this.organizationActive && values.length > 0 && values.every((value) => value === true);
1919
+ }
1920
+ };
1921
+ async function checkPermissions(config, accessToken, subject, organization2, permissions2) {
1922
+ assertSecureIssuer(config.issuer);
1923
+ const issuer = new URL(config.issuer);
1924
+ if (issuer.username || issuer.password || issuer.search || issuer.hash || !config.clientId || !subject || organization2 === "" || !accessToken || accessToken.length > 32768 || /[\x00-\x20\x7f]/.test(accessToken) || !permissions2.length || permissions2.length > 50 || permissions2.some((permission) => !permission.trim() || new TextEncoder().encode(permission).length > 255)) {
1925
+ throw new ConfigurationError("Permission decision request is not configured or valid.");
1926
+ }
1927
+ const requested = [...new Set(permissions2)];
1928
+ try {
1929
+ const response = await fetch(`${config.issuer.replace(/\/$/, "")}/oauth/decisions`, {
1930
+ method: "POST",
1931
+ redirect: "error",
1932
+ cache: "no-store",
1933
+ signal: AbortSignal.timeout(Math.max(1, config.timeoutMs ?? 1e4)),
1934
+ headers: { authorization: `Bearer ${accessToken}`, accept: "application/json", "content-type": "application/json" },
1935
+ body: JSON.stringify({ subject, org: organization2, permission: requested })
1936
+ });
1937
+ if (response.status !== 200) {
1938
+ await response.body?.cancel();
1939
+ throw new AuthenticationError("Permission decision could not be verified.", void 0, void 0, response.status);
1940
+ }
1941
+ const reader = response.body?.getReader();
1942
+ if (!reader) throw new AuthenticationError("Permission decision response is invalid.");
1943
+ let size = 0;
1944
+ let raw = "";
1945
+ const decoder = new TextDecoder();
1946
+ try {
1947
+ for (; ; ) {
1948
+ const { value, done } = await reader.read();
1949
+ if (done) break;
1950
+ size += value.byteLength;
1951
+ if (size > 65536) throw new AuthenticationError("Permission decision response exceeded its limit.");
1952
+ raw += decoder.decode(value, { stream: true });
1953
+ }
1954
+ raw += decoder.decode();
1955
+ } finally {
1956
+ await reader.cancel();
1957
+ reader.releaseLock();
1958
+ }
1959
+ const data = JSON.parse(raw);
1960
+ if (!record(data) || !record(data.subject) || data.mode !== "rbac" || data.subject.type !== "user" || data.subject.id !== subject || data.organization !== organization2 || data.client_id !== config.clientId || typeof data.organization_active !== "boolean" || typeof data.allowed !== "boolean" || !(data.org_role === null || typeof data.org_role === "string" && ["owner", "admin", "developer", "member", "viewer"].includes(data.org_role)) || !Array.isArray(data.results) || data.results.length !== requested.length) {
1961
+ throw new AuthenticationError("Permission decision response binding is invalid.");
1962
+ }
1963
+ const checks = /* @__PURE__ */ Object.create(null);
1964
+ for (const result of data.results) {
1965
+ if (!record(result) || typeof result.permission !== "string" || typeof result.allowed !== "boolean" || !requested.includes(result.permission) || Object.hasOwn(checks, result.permission)) {
1966
+ throw new AuthenticationError("Permission decision result is invalid.");
1967
+ }
1968
+ checks[result.permission] = result.allowed;
1969
+ }
1970
+ const decision = new PermissionDecision(
1971
+ subject,
1972
+ organization2,
1973
+ config.clientId,
1974
+ data.org_role,
1975
+ data.organization_active,
1976
+ Object.freeze(checks)
1977
+ );
1978
+ if (data.allowed !== decision.allowed()) throw new AuthenticationError("Permission decision result is inconsistent.");
1979
+ return decision;
1980
+ } catch (error) {
1981
+ if (error instanceof AuthenticationError) throw error;
1982
+ throw new AuthenticationError("Permission decision could not be verified.");
1983
+ }
1984
+ }
1985
+ function record(value) {
1986
+ return value !== null && typeof value === "object" && !Array.isArray(value);
1987
+ }
1988
+
1691
1989
  exports.AuthenticationError = AuthenticationError;
1692
1990
  exports.CboxIdClient = CboxIdClient;
1693
1991
  exports.CboxIdError = CboxIdError;
1694
1992
  exports.CboxIdFrontend = CboxIdFrontend;
1695
1993
  exports.ConfigurationError = ConfigurationError;
1994
+ exports.FEATURE_FLAGS_SCOPE = FEATURE_FLAGS_SCOPE;
1696
1995
  exports.FrontendApiError = FrontendApiError;
1697
1996
  exports.InvalidStateError = InvalidStateError;
1997
+ exports.PermissionDecision = PermissionDecision;
1998
+ exports.PipeLeaseDeniedError = PipeLeaseDeniedError;
1999
+ exports.PipeLeaseError = PipeLeaseError;
2000
+ exports.PipeNotConnectedError = PipeNotConnectedError;
2001
+ exports.PipeReauthorizationRequiredError = PipeReauthorizationRequiredError;
2002
+ exports.PipeTemporarilyUnavailableError = PipeTemporarilyUnavailableError;
1698
2003
  exports.VaultClient = VaultClient;
1699
2004
  exports.actor = actor;
1700
2005
  exports.buildManifest = buildManifest;
1701
2006
  exports.challenge = challenge;
2007
+ exports.checkPermissions = checkPermissions;
1702
2008
  exports.createLegacyVerifier = createLegacyVerifier;
1703
2009
  exports.createVerifier = createVerifier;
1704
2010
  exports.defineAuthz = defineAuthz;
2011
+ exports.featureFlags = featureFlags;
2012
+ exports.hasFeature = hasFeature;
1705
2013
  exports.hasPermission = hasPermission;
1706
2014
  exports.hasRole = hasRole;
1707
2015
  exports.isSupportSession = isSupportSession;
1708
2016
  exports.organization = organization;
1709
2017
  exports.permissions = permissions;
2018
+ exports.pipeConnectUrl = pipeConnectUrl;
1710
2019
  exports.publishManifest = publishManifest;
1711
2020
  exports.randomToken = randomToken;
1712
2021
  exports.roles = roles;
1713
2022
  exports.sessionId = sessionId;
1714
2023
  exports.verifyStandardWebhook = verifyStandardWebhook;
1715
2024
  exports.verifyWebhook = verifyWebhook;
2025
+ exports.withConnectReturn = withConnectReturn;
1716
2026
  //# sourceMappingURL=index.cjs.map
1717
2027
  //# sourceMappingURL=index.cjs.map