@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.
Files changed (41) hide show
  1. package/README.md +455 -4
  2. package/dist/chunk-2E25IPBA.js +155 -0
  3. package/dist/chunk-2E25IPBA.js.map +1 -0
  4. package/dist/{chunk-3V6OKRWF.js → chunk-CXGKOJRJ.js} +374 -90
  5. package/dist/chunk-CXGKOJRJ.js.map +1 -0
  6. package/dist/chunk-QQW63PGU.js +25 -0
  7. package/dist/chunk-QQW63PGU.js.map +1 -0
  8. package/dist/chunk-SEUD4TE4.js +61 -0
  9. package/dist/chunk-SEUD4TE4.js.map +1 -0
  10. package/dist/{client-CecDIu_N.d.cts → client-BSZASrMo.d.ts} +109 -136
  11. package/dist/{client-CecDIu_N.d.ts → client-BxUHm5hX.d.cts} +109 -136
  12. package/dist/errors-D61xljZE.d.cts +87 -0
  13. package/dist/errors-D61xljZE.d.ts +87 -0
  14. package/dist/index.cjs +440 -35
  15. package/dist/index.cjs.map +1 -1
  16. package/dist/index.d.cts +40 -90
  17. package/dist/index.d.ts +40 -90
  18. package/dist/index.js +64 -25
  19. package/dist/index.js.map +1 -1
  20. package/dist/management.cjs +4372 -0
  21. package/dist/management.cjs.map +1 -0
  22. package/dist/management.d.cts +10923 -0
  23. package/dist/management.d.ts +10923 -0
  24. package/dist/management.js +4335 -0
  25. package/dist/management.js.map +1 -0
  26. package/dist/nextjs.cjs +517 -42
  27. package/dist/nextjs.cjs.map +1 -1
  28. package/dist/nextjs.d.cts +29 -8
  29. package/dist/nextjs.d.ts +29 -8
  30. package/dist/nextjs.js +42 -12
  31. package/dist/nextjs.js.map +1 -1
  32. package/dist/server.cjs +220 -0
  33. package/dist/server.cjs.map +1 -0
  34. package/dist/server.d.cts +105 -0
  35. package/dist/server.d.ts +105 -0
  36. package/dist/server.js +5 -0
  37. package/dist/server.js.map +1 -0
  38. package/dist/types-BYZldock.d.cts +316 -0
  39. package/dist/types-BYZldock.d.ts +316 -0
  40. package/package.json +14 -2
  41. package/dist/chunk-3V6OKRWF.js.map +0 -1
package/dist/index.cjs CHANGED
@@ -106,12 +106,12 @@ function createLegacyVerifier(options) {
106
106
  );
107
107
  };
108
108
  }
109
- async function isSignedByCboxId(body, header, secret, toleranceSeconds) {
110
- if (!header) {
109
+ async function isSignedByCboxId(body, header2, secret, toleranceSeconds) {
110
+ if (!header2) {
111
111
  return false;
112
112
  }
113
113
  const parts = {};
114
- for (const piece of header.split(",")) {
114
+ for (const piece of header2.split(",")) {
115
115
  const [key2, value] = piece.split("=", 2);
116
116
  if (key2 && value) {
117
117
  parts[key2.trim()] = value.trim();
@@ -563,6 +563,75 @@ async function challenge(verifier) {
563
563
  return base64url(new Uint8Array(digest));
564
564
  }
565
565
 
566
+ // src/claims.ts
567
+ var ORGANIZATION_ROLES = /* @__PURE__ */ new Set([
568
+ "owner",
569
+ "admin",
570
+ "developer",
571
+ "member",
572
+ "viewer"
573
+ ]);
574
+ function organization(source) {
575
+ const claims = claimsOf(source);
576
+ const id = claims["org"];
577
+ if (typeof id !== "string" || id === "") {
578
+ return null;
579
+ }
580
+ const name = claims["org_name"];
581
+ const role = claims["org_role"];
582
+ return {
583
+ id,
584
+ name: typeof name === "string" && name !== "" ? name : null,
585
+ role: typeof role === "string" && ORGANIZATION_ROLES.has(role) ? role : null
586
+ };
587
+ }
588
+ function actor(source) {
589
+ return parseActor(claimsOf(source)["act"], 0);
590
+ }
591
+ function isSupportSession(source) {
592
+ return actor(source) !== null;
593
+ }
594
+ function sessionId(source) {
595
+ const sid = claimsOf(source)["sid"];
596
+ return typeof sid === "string" && sid !== "" ? sid : null;
597
+ }
598
+ function roles(source) {
599
+ return stringList(claimsOf(source)["roles"]);
600
+ }
601
+ function permissions(source) {
602
+ return stringList(claimsOf(source)["permissions"]);
603
+ }
604
+ function hasRole(source, role) {
605
+ return roles(source).includes(role);
606
+ }
607
+ function hasPermission(source, permission) {
608
+ return permissions(source).includes(permission);
609
+ }
610
+ var MAX_ACTOR_DEPTH = 8;
611
+ function parseActor(claim, depth) {
612
+ if (claim === void 0 || claim === null) {
613
+ return null;
614
+ }
615
+ if (typeof claim !== "object" || Array.isArray(claim)) {
616
+ return { sub: null, actor: null };
617
+ }
618
+ const record = claim;
619
+ const sub = record["sub"];
620
+ return {
621
+ sub: typeof sub === "string" && sub !== "" ? sub : null,
622
+ actor: depth + 1 < MAX_ACTOR_DEPTH ? parseActor(record["act"], depth + 1) : null
623
+ };
624
+ }
625
+ function stringList(claim) {
626
+ return Array.isArray(claim) ? claim.filter((value) => typeof value === "string" && value !== "") : [];
627
+ }
628
+ function claimsOf(source) {
629
+ return isUser(source) ? source.claims : source;
630
+ }
631
+ function isUser(source) {
632
+ return typeof source["claims"] === "object" && source["claims"] !== null && typeof source["accessToken"] === "string";
633
+ }
634
+
566
635
  // src/vault.ts
567
636
  var VaultClient = class {
568
637
  constructor(issuer, token, timeoutMs = 1e4) {
@@ -697,6 +766,58 @@ async function verifyWebhook(options) {
697
766
  );
698
767
  return timingSafeEqual2(expected, signature);
699
768
  }
769
+ function header(headers, name) {
770
+ if (typeof Headers !== "undefined" && headers instanceof Headers) {
771
+ return headers.get(name) ?? void 0;
772
+ }
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];
776
+ return Array.isArray(value) ? value.join(" ") : value;
777
+ }
778
+ function base64ToBytes(value) {
779
+ try {
780
+ const binary = atob(value);
781
+ return Uint8Array.from(binary, (c) => c.charCodeAt(0));
782
+ } catch {
783
+ return null;
784
+ }
785
+ }
786
+ function toBase64(bytes) {
787
+ let binary = "";
788
+ for (const byte of new Uint8Array(bytes)) {
789
+ binary += String.fromCharCode(byte);
790
+ }
791
+ return btoa(binary);
792
+ }
793
+ async function verifyStandardWebhook(options) {
794
+ const { payload, headers, secret } = options;
795
+ const toleranceSeconds = options.toleranceSeconds ?? 300;
796
+ const now = options.now ?? Math.floor(Date.now() / 1e3);
797
+ const id = header(headers, "webhook-id");
798
+ const timestamp = header(headers, "webhook-timestamp");
799
+ const signatures = header(headers, "webhook-signature");
800
+ if (!id || !timestamp || !signatures || !/^\d+$/.test(timestamp)) {
801
+ return false;
802
+ }
803
+ if (Math.abs(now - Number(timestamp)) > toleranceSeconds) {
804
+ return false;
805
+ }
806
+ const keyBytes = secret.startsWith("whsec_") ? base64ToBytes(secret.slice("whsec_".length)) : new TextEncoder().encode(secret);
807
+ if (keyBytes === null || keyBytes.length === 0) {
808
+ return false;
809
+ }
810
+ const key = await crypto.subtle.importKey("raw", keyBytes, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
811
+ const expected = toBase64(await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(`${id}.${timestamp}.${payload}`)));
812
+ let valid = false;
813
+ for (const candidate of signatures.split(" ")) {
814
+ const [version, signature] = candidate.split(",", 2);
815
+ if (version === "v1" && signature !== void 0 && timingSafeEqual2(expected, signature)) {
816
+ valid = true;
817
+ }
818
+ }
819
+ return valid;
820
+ }
700
821
 
701
822
  // src/client.ts
702
823
  var CboxIdClient = class {
@@ -708,9 +829,6 @@ var CboxIdClient = class {
708
829
  if (!config.clientId) {
709
830
  throw new ConfigurationError("Cbox ID config `clientId` is required.");
710
831
  }
711
- if (!config.redirectUri) {
712
- throw new ConfigurationError("Cbox ID config `redirectUri` is required.");
713
- }
714
832
  assertSecureIssuer(config.issuer);
715
833
  this.discovery = new Discovery(
716
834
  config.issuer,
@@ -727,21 +845,29 @@ var CboxIdClient = class {
727
845
  * to {@link authenticate} on the callback.
728
846
  */
729
847
  async createAuthorizationRequest(options = {}) {
848
+ const prompt = promptValue(options.prompt);
849
+ assertOrganizationOptions(options, prompt);
730
850
  const codeVerifier = createVerifier();
731
851
  const state = options.state ?? randomToken(16);
732
852
  const nonce = randomToken(16);
853
+ const redirectUri = options.redirectUri ?? this.config.redirectUri;
854
+ if (redirectUri === void 0 || redirectUri === "") {
855
+ throw new ConfigurationError(
856
+ "A `redirectUri` is required to start the browser sign-in flow. Set it in the config, or pass it to createAuthorizationRequest."
857
+ );
858
+ }
733
859
  const params = new URLSearchParams({
734
860
  response_type: "code",
735
861
  client_id: this.config.clientId,
736
- redirect_uri: options.redirectUri ?? this.config.redirectUri,
862
+ redirect_uri: redirectUri,
737
863
  scope: (options.scopes ?? this.scopes()).join(" "),
738
864
  state,
739
865
  nonce,
740
866
  code_challenge: await challenge(codeVerifier),
741
867
  code_challenge_method: "S256"
742
868
  });
743
- if (options.prompt) {
744
- params.set("prompt", options.prompt);
869
+ if (prompt.length > 0) {
870
+ params.set("prompt", prompt.join(" "));
745
871
  }
746
872
  if (options.loginHint) {
747
873
  params.set("login_hint", options.loginHint);
@@ -749,6 +875,12 @@ var CboxIdClient = class {
749
875
  if (typeof options.maxAge === "number") {
750
876
  params.set("max_age", String(options.maxAge));
751
877
  }
878
+ if (options.organization !== void 0) {
879
+ params.set("organization", options.organization);
880
+ }
881
+ if (options.organizationHint !== void 0) {
882
+ params.set("organization_hint", options.organizationHint);
883
+ }
752
884
  const endpoint = await this.discovery.endpoint("authorization_endpoint");
753
885
  return {
754
886
  url: `${endpoint}?${params.toString()}`,
@@ -757,9 +889,27 @@ var CboxIdClient = class {
757
889
  nonce,
758
890
  // Carried through so authenticate() can hold the instance to it. Only present
759
891
  // when requested (exactOptionalPropertyTypes).
760
- ...typeof options.maxAge === "number" ? { maxAge: options.maxAge } : {}
892
+ ...typeof options.maxAge === "number" ? { maxAge: options.maxAge } : {},
893
+ ...options.organization !== void 0 ? { organization: options.organization } : {}
761
894
  };
762
895
  }
896
+ /**
897
+ * Switch the signed-in person to another organization: a new authorization bound to
898
+ * `organizationId`. Persist and redirect exactly as for
899
+ * {@link createAuthorizationRequest} — it is one, with `organization` set.
900
+ *
901
+ * Cbox ID already holds the person's session, so this is normally a redirect there and
902
+ * straight back with no sign-in form. The tokens that come back carry the new `org`,
903
+ * `org_role`, `roles` and `permissions`; replace your session with them rather than
904
+ * patching the old one, because every one of those can differ between organizations.
905
+ *
906
+ * A person who is not (or is no longer) an active member of that organization comes
907
+ * back with `error=access_denied`, which {@link authenticate} throws as an
908
+ * `AuthenticationError` with `error === 'access_denied'`.
909
+ */
910
+ switchOrganization(organizationId, options = {}) {
911
+ return this.createAuthorizationRequest({ ...options, organization: organizationId });
912
+ }
763
913
  /**
764
914
  * Complete login on your callback route: verify state, exchange the code with the
765
915
  * PKCE verifier, verify the id_token, and return the authenticated user.
@@ -774,7 +924,9 @@ var CboxIdClient = class {
774
924
  }
775
925
  if (params.error) {
776
926
  throw new AuthenticationError(
777
- `Cbox ID returned an error: ${params.error}${params.error_description ? ` (${params.error_description})` : ""}`
927
+ `Cbox ID returned an error: ${params.error}${params.error_description ? ` (${params.error_description})` : ""}`,
928
+ params.error,
929
+ params.error_description ?? void 0
778
930
  );
779
931
  }
780
932
  if (!params.code) {
@@ -796,6 +948,24 @@ var CboxIdClient = class {
796
948
  "A max_age was requested but no id_token was returned, so the authentication age could not be verified."
797
949
  );
798
950
  }
951
+ const user = await this.identityFrom(tokens, verified);
952
+ if (stored.organization !== void 0 && user.organizationId !== stored.organization) {
953
+ throw new AuthenticationError(
954
+ `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.`
955
+ );
956
+ }
957
+ return user;
958
+ }
959
+ /**
960
+ * Turn a token response into the verified {@link CboxUser} every flow returns.
961
+ *
962
+ * Shared by the browser flow and the device flow rather than written twice: the checks
963
+ * in here — the UserInfo subject matching the id_token's, the merge that lets UserInfo
964
+ * enrich but never overwrite signed claims — are the ones that decide whether an
965
+ * identity is trustworthy. A second copy is a second place for one of them to be
966
+ * quietly missing, and the flow it was missing from would look exactly as correct.
967
+ */
968
+ async identityFrom(tokens, verified) {
799
969
  const profile = await this.userinfo(tokens.access_token);
800
970
  const verifiedSub = verified["sub"];
801
971
  const profileSub = profile["sub"];
@@ -813,6 +983,11 @@ var CboxIdClient = class {
813
983
  email: typeof claims["email"] === "string" ? claims["email"] : null,
814
984
  name: typeof claims["name"] === "string" ? claims["name"] : null,
815
985
  organizationId: typeof claims["org"] === "string" ? claims["org"] : null,
986
+ organization: organization(claims),
987
+ roles: roles(claims),
988
+ permissions: permissions(claims),
989
+ actor: actor(claims),
990
+ sessionId: sessionId(claims),
816
991
  // Only present when the instance emitted the claim (exactOptionalPropertyTypes).
817
992
  ...organizations ? { organizations } : {},
818
993
  claims,
@@ -822,6 +997,118 @@ var CboxIdClient = class {
822
997
  expiresIn: typeof tokens.expires_in === "number" ? tokens.expires_in : 0
823
998
  };
824
999
  }
1000
+ /**
1001
+ * Start the device authorization grant (RFC 8628) — the flow for a program with no
1002
+ * browser of its own: a CLI, a CI job, a container, a TV.
1003
+ *
1004
+ * Print `userCode` and `verificationUri`, then call
1005
+ * {@link CboxIdClient.pollDeviceToken}. If the machine has a desktop you may also open
1006
+ * `verificationUriComplete`, which fills the code in — but print the code anyway: the
1007
+ * machine running your program is often not the one the person is looking at.
1008
+ *
1009
+ * The scopes are bounded by what the app is REGISTERED for. A device request naming one
1010
+ * outside that ceiling is refused with `invalid_scope` rather than quietly reduced,
1011
+ * because no browser is in front of it to notice a smaller grant.
1012
+ *
1013
+ * @throws ConfigurationError when the instance advertises no device endpoint.
1014
+ */
1015
+ async requestDeviceAuthorization(scopes) {
1016
+ const endpoint = await this.discovery.optionalEndpoint("device_authorization_endpoint");
1017
+ if (endpoint === null) {
1018
+ throw new ConfigurationError(
1019
+ "This instance does not advertise a device_authorization_endpoint, so it does not support CLI sign-in."
1020
+ );
1021
+ }
1022
+ const body = new URLSearchParams({
1023
+ client_id: this.config.clientId,
1024
+ scope: (scopes ?? this.scopes()).join(" ")
1025
+ });
1026
+ if (this.config.clientSecret) {
1027
+ body.set("client_secret", this.config.clientSecret);
1028
+ }
1029
+ const response = await this.post(endpoint, body);
1030
+ if (!response.ok) {
1031
+ throw await oauthError(response, "Device authorization request failed");
1032
+ }
1033
+ const payload = await response.json();
1034
+ const deviceCode = payload["device_code"];
1035
+ const userCode = payload["user_code"];
1036
+ const verificationUri = payload["verification_uri"];
1037
+ if (typeof deviceCode !== "string" || typeof userCode !== "string" || typeof verificationUri !== "string") {
1038
+ throw new AuthenticationError("The device authorization response was incomplete.");
1039
+ }
1040
+ return {
1041
+ deviceCode,
1042
+ userCode,
1043
+ verificationUri,
1044
+ verificationUriComplete: typeof payload["verification_uri_complete"] === "string" ? payload["verification_uri_complete"] : null,
1045
+ expiresIn: typeof payload["expires_in"] === "number" ? payload["expires_in"] : 600,
1046
+ // RFC 8628 §3.2: absent means 5 seconds. Polling faster than the server allows is
1047
+ // answered with `slow_down`, so the default is the one the server assumes too.
1048
+ interval: typeof payload["interval"] === "number" ? payload["interval"] : 5
1049
+ };
1050
+ }
1051
+ /**
1052
+ * Poll until the person approves, and return them.
1053
+ *
1054
+ * Blocks. It honours the server's `interval`, backs off permanently by five seconds on
1055
+ * `slow_down` (RFC 8628 §3.5), and stops on the three answers that are final: they
1056
+ * declined, the code expired, or tokens arrived. Pass an `AbortSignal` to give up
1057
+ * early — a CLI should let Ctrl-C work.
1058
+ *
1059
+ * @throws AuthenticationError when the person declines or the code expires.
1060
+ */
1061
+ async pollDeviceToken(authorization, options = {}) {
1062
+ const endpoint = await this.discovery.endpoint("token_endpoint");
1063
+ const deadline = Date.now() + authorization.expiresIn * 1e3;
1064
+ let intervalMs = authorization.interval * 1e3;
1065
+ for (; ; ) {
1066
+ if (options.signal?.aborted === true) {
1067
+ throw new AuthenticationError("Device sign-in was cancelled.");
1068
+ }
1069
+ if (Date.now() > deadline) {
1070
+ throw new AuthenticationError(
1071
+ "The device code expired before it was approved. Start the sign-in again."
1072
+ );
1073
+ }
1074
+ await sleep(intervalMs, options.signal);
1075
+ const body = new URLSearchParams({
1076
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code",
1077
+ device_code: authorization.deviceCode,
1078
+ client_id: this.config.clientId
1079
+ });
1080
+ if (this.config.clientSecret) {
1081
+ body.set("client_secret", this.config.clientSecret);
1082
+ }
1083
+ const response = await this.post(endpoint, body);
1084
+ if (response.ok) {
1085
+ const tokens = await response.json();
1086
+ if (!tokens.access_token) {
1087
+ throw new AuthenticationError("The device token response carried no access token.");
1088
+ }
1089
+ const verified = tokens.id_token ? await this.verifyIdToken(tokens.id_token, "") : {};
1090
+ return await this.identityFrom(tokens, verified);
1091
+ }
1092
+ const payload = await response.json().catch(() => ({}));
1093
+ const error = typeof payload["error"] === "string" ? payload["error"] : "invalid_request";
1094
+ if (error === "authorization_pending") {
1095
+ continue;
1096
+ }
1097
+ if (error === "slow_down") {
1098
+ intervalMs += 5e3;
1099
+ continue;
1100
+ }
1101
+ if (error === "access_denied") {
1102
+ throw new AuthenticationError("Sign-in was declined.");
1103
+ }
1104
+ if (error === "expired_token") {
1105
+ throw new AuthenticationError(
1106
+ "The device code expired before it was approved. Start the sign-in again."
1107
+ );
1108
+ }
1109
+ throw await oauthError(response, "Device sign-in failed");
1110
+ }
1111
+ }
825
1112
  /**
826
1113
  * Exchange a refresh token for a fresh access token (OAuth 2.0 `refresh_token`
827
1114
  * grant). Cbox ID rotates refresh tokens and detects reuse, so ALWAYS persist the
@@ -869,6 +1156,25 @@ var CboxIdClient = class {
869
1156
  const base = `${this.config.issuer.replace(/\/$/, "")}${this.accountPath()}`;
870
1157
  return returnTo ? `${base}?${new URLSearchParams({ return_to: returnTo }).toString()}` : base;
871
1158
  }
1159
+ /**
1160
+ * The hosted page where a person creates and revokes API keys for your API — Cbox ID's
1161
+ * `/account/api-keys`, preselected to this app (or `clientId`). Keys made there are
1162
+ * checked with `ApiKeyVerifier` from `@cboxdk/id-js/server`.
1163
+ *
1164
+ * `returnTo` becomes a link back to your app, honoured only for an origin the app
1165
+ * registered. `organization` picks which of the person's organizations the keys act in;
1166
+ * omitted, the page uses the one they are in.
1167
+ */
1168
+ apiKeysUrl(options = {}) {
1169
+ const params = new URLSearchParams({ client_id: options.clientId ?? this.config.clientId });
1170
+ if (options.returnTo) {
1171
+ params.set("return_to", options.returnTo);
1172
+ }
1173
+ if (options.organization) {
1174
+ params.set("organization", options.organization);
1175
+ }
1176
+ return `${this.config.issuer.replace(/\/$/, "")}/account/api-keys?${params.toString()}`;
1177
+ }
872
1178
  /**
873
1179
  * The RP-initiated logout URL, or null when the instance advertises none.
874
1180
  *
@@ -1008,10 +1314,16 @@ var CboxIdClient = class {
1008
1314
  return new VaultClient(this.config.issuer, accessToken, this.config.timeoutMs ?? 1e4);
1009
1315
  }
1010
1316
  async exchange(code, verifier, redirectUri) {
1317
+ const callback = redirectUri ?? this.config.redirectUri;
1318
+ if (callback === void 0 || callback === "") {
1319
+ throw new ConfigurationError(
1320
+ "A `redirectUri` is required to exchange an authorization code. It must be the same one the authorization request used."
1321
+ );
1322
+ }
1011
1323
  const body = new URLSearchParams({
1012
1324
  grant_type: "authorization_code",
1013
1325
  code,
1014
- redirect_uri: redirectUri ?? this.config.redirectUri,
1326
+ redirect_uri: callback,
1015
1327
  client_id: this.config.clientId,
1016
1328
  code_verifier: verifier
1017
1329
  });
@@ -1095,6 +1407,37 @@ var CboxIdClient = class {
1095
1407
  }
1096
1408
  }
1097
1409
  };
1410
+ function promptValue(prompt) {
1411
+ if (prompt === void 0) {
1412
+ return [];
1413
+ }
1414
+ const values = [...new Set(typeof prompt === "string" ? [prompt] : prompt)];
1415
+ if (values.includes("none") && values.length > 1) {
1416
+ throw new ConfigurationError("`prompt: 'none'` cannot be combined with another prompt value.");
1417
+ }
1418
+ return values;
1419
+ }
1420
+ function assertOrganizationOptions(options, prompt) {
1421
+ if (options.organization === "") {
1422
+ throw new ConfigurationError("`organization` is empty. Omit it to sign in without binding to an organization.");
1423
+ }
1424
+ if (options.organizationHint === "") {
1425
+ throw new ConfigurationError("`organizationHint` is empty. Omit it when you have no organization to suggest.");
1426
+ }
1427
+ if (options.organization === void 0) {
1428
+ return;
1429
+ }
1430
+ if (prompt.includes("select_organization")) {
1431
+ throw new ConfigurationError(
1432
+ "`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."
1433
+ );
1434
+ }
1435
+ if (prompt.includes("create_organization")) {
1436
+ throw new ConfigurationError(
1437
+ "`organization` binds the sign-in to an existing organization and `prompt: 'create_organization'` creates a new one. Send one or the other."
1438
+ );
1439
+ }
1440
+ }
1098
1441
  function timingSafeEqualString(a, b) {
1099
1442
  if (a.length !== b.length) {
1100
1443
  return false;
@@ -1123,25 +1466,44 @@ function parseOrganizations(claim) {
1123
1466
  }
1124
1467
  return orgs;
1125
1468
  }
1469
+ function sleep(ms, signal) {
1470
+ return new Promise((resolve, reject) => {
1471
+ if (signal?.aborted === true) {
1472
+ reject(new AuthenticationError("Device sign-in was cancelled."));
1473
+ return;
1474
+ }
1475
+ const timer = setTimeout(() => {
1476
+ signal?.removeEventListener("abort", onAbort);
1477
+ resolve();
1478
+ }, ms);
1479
+ const onAbort = () => {
1480
+ clearTimeout(timer);
1481
+ reject(new AuthenticationError("Device sign-in was cancelled."));
1482
+ };
1483
+ signal?.addEventListener("abort", onAbort, { once: true });
1484
+ });
1485
+ }
1126
1486
 
1127
1487
  // src/authz.ts
1128
1488
  function defineAuthz(declaration) {
1129
- const permissions = declaration.permissions ?? [];
1130
- const roles = declaration.roles ?? [];
1131
- assertDeclaration(permissions, roles);
1489
+ const permissions2 = declaration.permissions ?? [];
1490
+ const roles2 = declaration.roles ?? [];
1491
+ assertDeclaration(permissions2, roles2);
1132
1492
  if (declaration.legacyLogin) {
1133
1493
  assertLegacyLogin(declaration.legacyLogin);
1134
1494
  }
1135
- return { ...declaration, permissions, roles };
1495
+ return { ...declaration, permissions: permissions2, roles: roles2 };
1136
1496
  }
1137
1497
  async function buildManifest(declaration) {
1138
- const permissions = (declaration.permissions ?? []).map(canonicalPermission);
1139
- const roles = (declaration.roles ?? []).map(canonicalRole);
1140
- assertDeclaration(permissions, roles);
1141
- permissions.sort((a, b) => byteCompare(a.key, b.key));
1142
- roles.sort((a, b) => byteCompare(a.key, b.key));
1143
- const version = (await sha256Hex(canonicalManifestJson(permissions, roles))).slice(0, 16);
1144
- const manifest = { version, permissions, roles };
1498
+ const permissions2 = declaration.permissions ?? [];
1499
+ const roles2 = declaration.roles ?? [];
1500
+ assertDeclaration(permissions2, roles2);
1501
+ const version = (await sha256Hex(canonicalManifestJson(permissions2, roles2))).slice(0, 16);
1502
+ const manifest = {
1503
+ version,
1504
+ permissions: [...permissions2].sort((a, b) => byteCompare(a.key, b.key)).map(wirePermission),
1505
+ roles: [...roles2].sort((a, b) => byteCompare(a.key, b.key)).map(wireRole)
1506
+ };
1145
1507
  if (declaration.legacyLogin) {
1146
1508
  assertLegacyLogin(declaration.legacyLogin);
1147
1509
  manifest.legacy_login = declaration.legacyLogin;
@@ -1198,28 +1560,33 @@ async function mintToken(config, discovery, timeoutMs) {
1198
1560
  }
1199
1561
  return json2.access_token;
1200
1562
  }
1201
- function assertDeclaration(permissions, roles) {
1563
+ var KEY_PATTERN = /^[a-z][a-z0-9_-]*(?:[.:][a-z0-9_-]+)*$/;
1564
+ function assertDeclaration(permissions2, roles2) {
1202
1565
  const permissionKeys = /* @__PURE__ */ new Set();
1203
- for (const permission of permissions) {
1566
+ for (const permission of permissions2) {
1204
1567
  if (!permission.key) {
1205
1568
  throw new ConfigurationError("Every permission needs a non-empty `key`.");
1206
1569
  }
1570
+ assertKey(permission.key, "Permission");
1207
1571
  if (permissionKeys.has(permission.key)) {
1208
1572
  throw new ConfigurationError(`Permission "${permission.key}" is declared more than once.`);
1209
1573
  }
1574
+ assertFlag(permission.tenantAssignable, `Permission "${permission.key}"`);
1210
1575
  permissionKeys.add(permission.key);
1211
1576
  }
1212
1577
  const roleKeys = /* @__PURE__ */ new Set();
1213
- for (const role of roles) {
1578
+ for (const role of roles2) {
1214
1579
  if (!role.key) {
1215
1580
  throw new ConfigurationError("Every role needs a non-empty `key`.");
1216
1581
  }
1582
+ assertKey(role.key, "Role");
1217
1583
  if (!role.name) {
1218
1584
  throw new ConfigurationError(`Role "${role.key}" needs a non-empty \`name\`.`);
1219
1585
  }
1220
1586
  if (roleKeys.has(role.key)) {
1221
1587
  throw new ConfigurationError(`Role "${role.key}" is declared more than once.`);
1222
1588
  }
1589
+ assertFlag(role.tenantAssignable, `Role "${role.key}"`);
1223
1590
  roleKeys.add(role.key);
1224
1591
  for (const reference of role.permissions) {
1225
1592
  if (!permissionKeys.has(reference)) {
@@ -1230,25 +1597,54 @@ function assertDeclaration(permissions, roles) {
1230
1597
  }
1231
1598
  }
1232
1599
  }
1233
- function canonicalPermission(permission) {
1234
- return permission.description === void 0 ? { key: permission.key } : { key: permission.key, description: permission.description };
1600
+ function assertKey(key, what) {
1601
+ if (!KEY_PATTERN.test(key)) {
1602
+ throw new ConfigurationError(
1603
+ `${what} key "${key}" is not a lowercase \`feature:action\` slug (e.g. \`invoices:create\`).`
1604
+ );
1605
+ }
1606
+ }
1607
+ function assertFlag(value, owner) {
1608
+ if (value !== void 0 && typeof value !== "boolean") {
1609
+ throw new ConfigurationError(`${owner} \`tenantAssignable\` must be true or false.`);
1610
+ }
1611
+ }
1612
+ function wirePermission(permission) {
1613
+ return {
1614
+ key: permission.key,
1615
+ ...permission.description === void 0 ? {} : { description: permission.description },
1616
+ ...permission.tenantAssignable === true ? { tenant_assignable: true } : {}
1617
+ };
1235
1618
  }
1236
- function canonicalRole(role) {
1237
- const permissions = [...role.permissions].sort(byteCompare);
1238
- return role.description === void 0 ? { key: role.key, name: role.name, permissions } : { key: role.key, name: role.name, description: role.description, permissions };
1619
+ function wireRole(role) {
1620
+ return {
1621
+ key: role.key,
1622
+ name: role.name,
1623
+ ...role.description === void 0 ? {} : { description: role.description },
1624
+ permissions: uniqueSorted(role.permissions),
1625
+ ...role.tenantAssignable === false ? { tenant_assignable: false } : {}
1626
+ };
1239
1627
  }
1240
- function canonicalManifestJson(permissions, roles) {
1628
+ function canonicalManifestJson(permissions2, roles2) {
1241
1629
  const canonical = {
1242
- permissions: [...permissions].sort((a, b) => byteCompare(a.key, b.key)).map((p) => ({ key: p.key, description: emptyToNull(p.description) })),
1243
- roles: [...roles].sort((a, b) => byteCompare(a.key, b.key)).map((r) => ({
1630
+ permissions: [...permissions2].sort((a, b) => byteCompare(a.key, b.key)).map((p) => ({
1631
+ key: p.key,
1632
+ description: emptyToNull(p.description),
1633
+ ...p.tenantAssignable === true ? { tenant_assignable: true } : {}
1634
+ })),
1635
+ roles: [...roles2].sort((a, b) => byteCompare(a.key, b.key)).map((r) => ({
1244
1636
  key: r.key,
1245
1637
  name: r.name,
1246
1638
  description: emptyToNull(r.description),
1247
- permissions: [...r.permissions].sort(byteCompare)
1639
+ permissions: uniqueSorted(r.permissions),
1640
+ ...r.tenantAssignable === false ? { tenant_assignable: false } : {}
1248
1641
  }))
1249
1642
  };
1250
1643
  return escapeLikePhp(JSON.stringify(canonical));
1251
1644
  }
1645
+ function uniqueSorted(values) {
1646
+ return [...new Set(values)].sort(byteCompare);
1647
+ }
1252
1648
  function emptyToNull(value) {
1253
1649
  return value === void 0 || value === "" ? null : value;
1254
1650
  }
@@ -1300,13 +1696,22 @@ exports.ConfigurationError = ConfigurationError;
1300
1696
  exports.FrontendApiError = FrontendApiError;
1301
1697
  exports.InvalidStateError = InvalidStateError;
1302
1698
  exports.VaultClient = VaultClient;
1699
+ exports.actor = actor;
1303
1700
  exports.buildManifest = buildManifest;
1304
1701
  exports.challenge = challenge;
1305
1702
  exports.createLegacyVerifier = createLegacyVerifier;
1306
1703
  exports.createVerifier = createVerifier;
1307
1704
  exports.defineAuthz = defineAuthz;
1705
+ exports.hasPermission = hasPermission;
1706
+ exports.hasRole = hasRole;
1707
+ exports.isSupportSession = isSupportSession;
1708
+ exports.organization = organization;
1709
+ exports.permissions = permissions;
1308
1710
  exports.publishManifest = publishManifest;
1309
1711
  exports.randomToken = randomToken;
1712
+ exports.roles = roles;
1713
+ exports.sessionId = sessionId;
1714
+ exports.verifyStandardWebhook = verifyStandardWebhook;
1310
1715
  exports.verifyWebhook = verifyWebhook;
1311
1716
  //# sourceMappingURL=index.cjs.map
1312
1717
  //# sourceMappingURL=index.cjs.map