@cboxdk/id-js 0.15.1 → 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/dist/index.cjs CHANGED
@@ -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) {
@@ -708,9 +777,6 @@ var CboxIdClient = class {
708
777
  if (!config.clientId) {
709
778
  throw new ConfigurationError("Cbox ID config `clientId` is required.");
710
779
  }
711
- if (!config.redirectUri) {
712
- throw new ConfigurationError("Cbox ID config `redirectUri` is required.");
713
- }
714
780
  assertSecureIssuer(config.issuer);
715
781
  this.discovery = new Discovery(
716
782
  config.issuer,
@@ -727,21 +793,29 @@ var CboxIdClient = class {
727
793
  * to {@link authenticate} on the callback.
728
794
  */
729
795
  async createAuthorizationRequest(options = {}) {
796
+ const prompt = promptValue(options.prompt);
797
+ assertOrganizationOptions(options, prompt);
730
798
  const codeVerifier = createVerifier();
731
799
  const state = options.state ?? randomToken(16);
732
800
  const nonce = randomToken(16);
801
+ const redirectUri = options.redirectUri ?? this.config.redirectUri;
802
+ if (redirectUri === void 0 || redirectUri === "") {
803
+ throw new ConfigurationError(
804
+ "A `redirectUri` is required to start the browser sign-in flow. Set it in the config, or pass it to createAuthorizationRequest."
805
+ );
806
+ }
733
807
  const params = new URLSearchParams({
734
808
  response_type: "code",
735
809
  client_id: this.config.clientId,
736
- redirect_uri: options.redirectUri ?? this.config.redirectUri,
810
+ redirect_uri: redirectUri,
737
811
  scope: (options.scopes ?? this.scopes()).join(" "),
738
812
  state,
739
813
  nonce,
740
814
  code_challenge: await challenge(codeVerifier),
741
815
  code_challenge_method: "S256"
742
816
  });
743
- if (options.prompt) {
744
- params.set("prompt", options.prompt);
817
+ if (prompt.length > 0) {
818
+ params.set("prompt", prompt.join(" "));
745
819
  }
746
820
  if (options.loginHint) {
747
821
  params.set("login_hint", options.loginHint);
@@ -749,6 +823,12 @@ var CboxIdClient = class {
749
823
  if (typeof options.maxAge === "number") {
750
824
  params.set("max_age", String(options.maxAge));
751
825
  }
826
+ if (options.organization !== void 0) {
827
+ params.set("organization", options.organization);
828
+ }
829
+ if (options.organizationHint !== void 0) {
830
+ params.set("organization_hint", options.organizationHint);
831
+ }
752
832
  const endpoint = await this.discovery.endpoint("authorization_endpoint");
753
833
  return {
754
834
  url: `${endpoint}?${params.toString()}`,
@@ -757,9 +837,27 @@ var CboxIdClient = class {
757
837
  nonce,
758
838
  // Carried through so authenticate() can hold the instance to it. Only present
759
839
  // when requested (exactOptionalPropertyTypes).
760
- ...typeof options.maxAge === "number" ? { maxAge: options.maxAge } : {}
840
+ ...typeof options.maxAge === "number" ? { maxAge: options.maxAge } : {},
841
+ ...options.organization !== void 0 ? { organization: options.organization } : {}
761
842
  };
762
843
  }
844
+ /**
845
+ * Switch the signed-in person to another organization: a new authorization bound to
846
+ * `organizationId`. Persist and redirect exactly as for
847
+ * {@link createAuthorizationRequest} — it is one, with `organization` set.
848
+ *
849
+ * Cbox ID already holds the person's session, so this is normally a redirect there and
850
+ * straight back with no sign-in form. The tokens that come back carry the new `org`,
851
+ * `org_role`, `roles` and `permissions`; replace your session with them rather than
852
+ * patching the old one, because every one of those can differ between organizations.
853
+ *
854
+ * A person who is not (or is no longer) an active member of that organization comes
855
+ * back with `error=access_denied`, which {@link authenticate} throws as an
856
+ * `AuthenticationError` with `error === 'access_denied'`.
857
+ */
858
+ switchOrganization(organizationId, options = {}) {
859
+ return this.createAuthorizationRequest({ ...options, organization: organizationId });
860
+ }
763
861
  /**
764
862
  * Complete login on your callback route: verify state, exchange the code with the
765
863
  * PKCE verifier, verify the id_token, and return the authenticated user.
@@ -774,7 +872,9 @@ var CboxIdClient = class {
774
872
  }
775
873
  if (params.error) {
776
874
  throw new AuthenticationError(
777
- `Cbox ID returned an error: ${params.error}${params.error_description ? ` (${params.error_description})` : ""}`
875
+ `Cbox ID returned an error: ${params.error}${params.error_description ? ` (${params.error_description})` : ""}`,
876
+ params.error,
877
+ params.error_description ?? void 0
778
878
  );
779
879
  }
780
880
  if (!params.code) {
@@ -796,6 +896,24 @@ var CboxIdClient = class {
796
896
  "A max_age was requested but no id_token was returned, so the authentication age could not be verified."
797
897
  );
798
898
  }
899
+ const user = await this.identityFrom(tokens, verified);
900
+ if (stored.organization !== void 0 && user.organizationId !== stored.organization) {
901
+ throw new AuthenticationError(
902
+ `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.`
903
+ );
904
+ }
905
+ return user;
906
+ }
907
+ /**
908
+ * Turn a token response into the verified {@link CboxUser} every flow returns.
909
+ *
910
+ * Shared by the browser flow and the device flow rather than written twice: the checks
911
+ * in here — the UserInfo subject matching the id_token's, the merge that lets UserInfo
912
+ * enrich but never overwrite signed claims — are the ones that decide whether an
913
+ * identity is trustworthy. A second copy is a second place for one of them to be
914
+ * quietly missing, and the flow it was missing from would look exactly as correct.
915
+ */
916
+ async identityFrom(tokens, verified) {
799
917
  const profile = await this.userinfo(tokens.access_token);
800
918
  const verifiedSub = verified["sub"];
801
919
  const profileSub = profile["sub"];
@@ -813,6 +931,11 @@ var CboxIdClient = class {
813
931
  email: typeof claims["email"] === "string" ? claims["email"] : null,
814
932
  name: typeof claims["name"] === "string" ? claims["name"] : null,
815
933
  organizationId: typeof claims["org"] === "string" ? claims["org"] : null,
934
+ organization: organization(claims),
935
+ roles: roles(claims),
936
+ permissions: permissions(claims),
937
+ actor: actor(claims),
938
+ sessionId: sessionId(claims),
816
939
  // Only present when the instance emitted the claim (exactOptionalPropertyTypes).
817
940
  ...organizations ? { organizations } : {},
818
941
  claims,
@@ -822,6 +945,118 @@ var CboxIdClient = class {
822
945
  expiresIn: typeof tokens.expires_in === "number" ? tokens.expires_in : 0
823
946
  };
824
947
  }
948
+ /**
949
+ * Start the device authorization grant (RFC 8628) — the flow for a program with no
950
+ * browser of its own: a CLI, a CI job, a container, a TV.
951
+ *
952
+ * Print `userCode` and `verificationUri`, then call
953
+ * {@link CboxIdClient.pollDeviceToken}. If the machine has a desktop you may also open
954
+ * `verificationUriComplete`, which fills the code in — but print the code anyway: the
955
+ * machine running your program is often not the one the person is looking at.
956
+ *
957
+ * The scopes are bounded by what the app is REGISTERED for. A device request naming one
958
+ * outside that ceiling is refused with `invalid_scope` rather than quietly reduced,
959
+ * because no browser is in front of it to notice a smaller grant.
960
+ *
961
+ * @throws ConfigurationError when the instance advertises no device endpoint.
962
+ */
963
+ async requestDeviceAuthorization(scopes) {
964
+ const endpoint = await this.discovery.optionalEndpoint("device_authorization_endpoint");
965
+ if (endpoint === null) {
966
+ throw new ConfigurationError(
967
+ "This instance does not advertise a device_authorization_endpoint, so it does not support CLI sign-in."
968
+ );
969
+ }
970
+ const body = new URLSearchParams({
971
+ client_id: this.config.clientId,
972
+ scope: (scopes ?? this.scopes()).join(" ")
973
+ });
974
+ if (this.config.clientSecret) {
975
+ body.set("client_secret", this.config.clientSecret);
976
+ }
977
+ const response = await this.post(endpoint, body);
978
+ if (!response.ok) {
979
+ throw await oauthError(response, "Device authorization request failed");
980
+ }
981
+ const payload = await response.json();
982
+ const deviceCode = payload["device_code"];
983
+ const userCode = payload["user_code"];
984
+ const verificationUri = payload["verification_uri"];
985
+ if (typeof deviceCode !== "string" || typeof userCode !== "string" || typeof verificationUri !== "string") {
986
+ throw new AuthenticationError("The device authorization response was incomplete.");
987
+ }
988
+ return {
989
+ deviceCode,
990
+ userCode,
991
+ verificationUri,
992
+ verificationUriComplete: typeof payload["verification_uri_complete"] === "string" ? payload["verification_uri_complete"] : null,
993
+ expiresIn: typeof payload["expires_in"] === "number" ? payload["expires_in"] : 600,
994
+ // RFC 8628 §3.2: absent means 5 seconds. Polling faster than the server allows is
995
+ // answered with `slow_down`, so the default is the one the server assumes too.
996
+ interval: typeof payload["interval"] === "number" ? payload["interval"] : 5
997
+ };
998
+ }
999
+ /**
1000
+ * Poll until the person approves, and return them.
1001
+ *
1002
+ * Blocks. It honours the server's `interval`, backs off permanently by five seconds on
1003
+ * `slow_down` (RFC 8628 §3.5), and stops on the three answers that are final: they
1004
+ * declined, the code expired, or tokens arrived. Pass an `AbortSignal` to give up
1005
+ * early — a CLI should let Ctrl-C work.
1006
+ *
1007
+ * @throws AuthenticationError when the person declines or the code expires.
1008
+ */
1009
+ async pollDeviceToken(authorization, options = {}) {
1010
+ const endpoint = await this.discovery.endpoint("token_endpoint");
1011
+ const deadline = Date.now() + authorization.expiresIn * 1e3;
1012
+ let intervalMs = authorization.interval * 1e3;
1013
+ for (; ; ) {
1014
+ if (options.signal?.aborted === true) {
1015
+ throw new AuthenticationError("Device sign-in was cancelled.");
1016
+ }
1017
+ if (Date.now() > deadline) {
1018
+ throw new AuthenticationError(
1019
+ "The device code expired before it was approved. Start the sign-in again."
1020
+ );
1021
+ }
1022
+ await sleep(intervalMs, options.signal);
1023
+ const body = new URLSearchParams({
1024
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code",
1025
+ device_code: authorization.deviceCode,
1026
+ client_id: this.config.clientId
1027
+ });
1028
+ if (this.config.clientSecret) {
1029
+ body.set("client_secret", this.config.clientSecret);
1030
+ }
1031
+ const response = await this.post(endpoint, body);
1032
+ if (response.ok) {
1033
+ const tokens = await response.json();
1034
+ if (!tokens.access_token) {
1035
+ throw new AuthenticationError("The device token response carried no access token.");
1036
+ }
1037
+ const verified = tokens.id_token ? await this.verifyIdToken(tokens.id_token, "") : {};
1038
+ return await this.identityFrom(tokens, verified);
1039
+ }
1040
+ const payload = await response.json().catch(() => ({}));
1041
+ const error = typeof payload["error"] === "string" ? payload["error"] : "invalid_request";
1042
+ if (error === "authorization_pending") {
1043
+ continue;
1044
+ }
1045
+ if (error === "slow_down") {
1046
+ intervalMs += 5e3;
1047
+ continue;
1048
+ }
1049
+ if (error === "access_denied") {
1050
+ throw new AuthenticationError("Sign-in was declined.");
1051
+ }
1052
+ if (error === "expired_token") {
1053
+ throw new AuthenticationError(
1054
+ "The device code expired before it was approved. Start the sign-in again."
1055
+ );
1056
+ }
1057
+ throw await oauthError(response, "Device sign-in failed");
1058
+ }
1059
+ }
825
1060
  /**
826
1061
  * Exchange a refresh token for a fresh access token (OAuth 2.0 `refresh_token`
827
1062
  * grant). Cbox ID rotates refresh tokens and detects reuse, so ALWAYS persist the
@@ -869,6 +1104,25 @@ var CboxIdClient = class {
869
1104
  const base = `${this.config.issuer.replace(/\/$/, "")}${this.accountPath()}`;
870
1105
  return returnTo ? `${base}?${new URLSearchParams({ return_to: returnTo }).toString()}` : base;
871
1106
  }
1107
+ /**
1108
+ * The hosted page where a person creates and revokes API keys for your API — Cbox ID's
1109
+ * `/account/api-keys`, preselected to this app (or `clientId`). Keys made there are
1110
+ * checked with `ApiKeyVerifier` from `@cboxdk/id-js/server`.
1111
+ *
1112
+ * `returnTo` becomes a link back to your app, honoured only for an origin the app
1113
+ * registered. `organization` picks which of the person's organizations the keys act in;
1114
+ * omitted, the page uses the one they are in.
1115
+ */
1116
+ apiKeysUrl(options = {}) {
1117
+ const params = new URLSearchParams({ client_id: options.clientId ?? this.config.clientId });
1118
+ if (options.returnTo) {
1119
+ params.set("return_to", options.returnTo);
1120
+ }
1121
+ if (options.organization) {
1122
+ params.set("organization", options.organization);
1123
+ }
1124
+ return `${this.config.issuer.replace(/\/$/, "")}/account/api-keys?${params.toString()}`;
1125
+ }
872
1126
  /**
873
1127
  * The RP-initiated logout URL, or null when the instance advertises none.
874
1128
  *
@@ -1008,10 +1262,16 @@ var CboxIdClient = class {
1008
1262
  return new VaultClient(this.config.issuer, accessToken, this.config.timeoutMs ?? 1e4);
1009
1263
  }
1010
1264
  async exchange(code, verifier, redirectUri) {
1265
+ const callback = redirectUri ?? this.config.redirectUri;
1266
+ if (callback === void 0 || callback === "") {
1267
+ throw new ConfigurationError(
1268
+ "A `redirectUri` is required to exchange an authorization code. It must be the same one the authorization request used."
1269
+ );
1270
+ }
1011
1271
  const body = new URLSearchParams({
1012
1272
  grant_type: "authorization_code",
1013
1273
  code,
1014
- redirect_uri: redirectUri ?? this.config.redirectUri,
1274
+ redirect_uri: callback,
1015
1275
  client_id: this.config.clientId,
1016
1276
  code_verifier: verifier
1017
1277
  });
@@ -1095,6 +1355,37 @@ var CboxIdClient = class {
1095
1355
  }
1096
1356
  }
1097
1357
  };
1358
+ function promptValue(prompt) {
1359
+ if (prompt === void 0) {
1360
+ return [];
1361
+ }
1362
+ const values = [...new Set(typeof prompt === "string" ? [prompt] : prompt)];
1363
+ if (values.includes("none") && values.length > 1) {
1364
+ throw new ConfigurationError("`prompt: 'none'` cannot be combined with another prompt value.");
1365
+ }
1366
+ return values;
1367
+ }
1368
+ function assertOrganizationOptions(options, prompt) {
1369
+ if (options.organization === "") {
1370
+ throw new ConfigurationError("`organization` is empty. Omit it to sign in without binding to an organization.");
1371
+ }
1372
+ if (options.organizationHint === "") {
1373
+ throw new ConfigurationError("`organizationHint` is empty. Omit it when you have no organization to suggest.");
1374
+ }
1375
+ if (options.organization === void 0) {
1376
+ return;
1377
+ }
1378
+ if (prompt.includes("select_organization")) {
1379
+ throw new ConfigurationError(
1380
+ "`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."
1381
+ );
1382
+ }
1383
+ if (prompt.includes("create_organization")) {
1384
+ throw new ConfigurationError(
1385
+ "`organization` binds the sign-in to an existing organization and `prompt: 'create_organization'` creates a new one. Send one or the other."
1386
+ );
1387
+ }
1388
+ }
1098
1389
  function timingSafeEqualString(a, b) {
1099
1390
  if (a.length !== b.length) {
1100
1391
  return false;
@@ -1123,25 +1414,44 @@ function parseOrganizations(claim) {
1123
1414
  }
1124
1415
  return orgs;
1125
1416
  }
1417
+ function sleep(ms, signal) {
1418
+ return new Promise((resolve, reject) => {
1419
+ if (signal?.aborted === true) {
1420
+ reject(new AuthenticationError("Device sign-in was cancelled."));
1421
+ return;
1422
+ }
1423
+ const timer = setTimeout(() => {
1424
+ signal?.removeEventListener("abort", onAbort);
1425
+ resolve();
1426
+ }, ms);
1427
+ const onAbort = () => {
1428
+ clearTimeout(timer);
1429
+ reject(new AuthenticationError("Device sign-in was cancelled."));
1430
+ };
1431
+ signal?.addEventListener("abort", onAbort, { once: true });
1432
+ });
1433
+ }
1126
1434
 
1127
1435
  // src/authz.ts
1128
1436
  function defineAuthz(declaration) {
1129
- const permissions = declaration.permissions ?? [];
1130
- const roles = declaration.roles ?? [];
1131
- assertDeclaration(permissions, roles);
1437
+ const permissions2 = declaration.permissions ?? [];
1438
+ const roles2 = declaration.roles ?? [];
1439
+ assertDeclaration(permissions2, roles2);
1132
1440
  if (declaration.legacyLogin) {
1133
1441
  assertLegacyLogin(declaration.legacyLogin);
1134
1442
  }
1135
- return { ...declaration, permissions, roles };
1443
+ return { ...declaration, permissions: permissions2, roles: roles2 };
1136
1444
  }
1137
1445
  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 };
1446
+ const permissions2 = declaration.permissions ?? [];
1447
+ const roles2 = declaration.roles ?? [];
1448
+ assertDeclaration(permissions2, roles2);
1449
+ const version = (await sha256Hex(canonicalManifestJson(permissions2, roles2))).slice(0, 16);
1450
+ const manifest = {
1451
+ version,
1452
+ permissions: [...permissions2].sort((a, b) => byteCompare(a.key, b.key)).map(wirePermission),
1453
+ roles: [...roles2].sort((a, b) => byteCompare(a.key, b.key)).map(wireRole)
1454
+ };
1145
1455
  if (declaration.legacyLogin) {
1146
1456
  assertLegacyLogin(declaration.legacyLogin);
1147
1457
  manifest.legacy_login = declaration.legacyLogin;
@@ -1198,28 +1508,33 @@ async function mintToken(config, discovery, timeoutMs) {
1198
1508
  }
1199
1509
  return json2.access_token;
1200
1510
  }
1201
- function assertDeclaration(permissions, roles) {
1511
+ var KEY_PATTERN = /^[a-z][a-z0-9_-]*(?:[.:][a-z0-9_-]+)*$/;
1512
+ function assertDeclaration(permissions2, roles2) {
1202
1513
  const permissionKeys = /* @__PURE__ */ new Set();
1203
- for (const permission of permissions) {
1514
+ for (const permission of permissions2) {
1204
1515
  if (!permission.key) {
1205
1516
  throw new ConfigurationError("Every permission needs a non-empty `key`.");
1206
1517
  }
1518
+ assertKey(permission.key, "Permission");
1207
1519
  if (permissionKeys.has(permission.key)) {
1208
1520
  throw new ConfigurationError(`Permission "${permission.key}" is declared more than once.`);
1209
1521
  }
1522
+ assertFlag(permission.tenantAssignable, `Permission "${permission.key}"`);
1210
1523
  permissionKeys.add(permission.key);
1211
1524
  }
1212
1525
  const roleKeys = /* @__PURE__ */ new Set();
1213
- for (const role of roles) {
1526
+ for (const role of roles2) {
1214
1527
  if (!role.key) {
1215
1528
  throw new ConfigurationError("Every role needs a non-empty `key`.");
1216
1529
  }
1530
+ assertKey(role.key, "Role");
1217
1531
  if (!role.name) {
1218
1532
  throw new ConfigurationError(`Role "${role.key}" needs a non-empty \`name\`.`);
1219
1533
  }
1220
1534
  if (roleKeys.has(role.key)) {
1221
1535
  throw new ConfigurationError(`Role "${role.key}" is declared more than once.`);
1222
1536
  }
1537
+ assertFlag(role.tenantAssignable, `Role "${role.key}"`);
1223
1538
  roleKeys.add(role.key);
1224
1539
  for (const reference of role.permissions) {
1225
1540
  if (!permissionKeys.has(reference)) {
@@ -1230,25 +1545,54 @@ function assertDeclaration(permissions, roles) {
1230
1545
  }
1231
1546
  }
1232
1547
  }
1233
- function canonicalPermission(permission) {
1234
- return permission.description === void 0 ? { key: permission.key } : { key: permission.key, description: permission.description };
1548
+ function assertKey(key, what) {
1549
+ if (!KEY_PATTERN.test(key)) {
1550
+ throw new ConfigurationError(
1551
+ `${what} key "${key}" is not a lowercase \`feature:action\` slug (e.g. \`invoices:create\`).`
1552
+ );
1553
+ }
1235
1554
  }
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 };
1555
+ function assertFlag(value, owner) {
1556
+ if (value !== void 0 && typeof value !== "boolean") {
1557
+ throw new ConfigurationError(`${owner} \`tenantAssignable\` must be true or false.`);
1558
+ }
1559
+ }
1560
+ function wirePermission(permission) {
1561
+ return {
1562
+ key: permission.key,
1563
+ ...permission.description === void 0 ? {} : { description: permission.description },
1564
+ ...permission.tenantAssignable === true ? { tenant_assignable: true } : {}
1565
+ };
1239
1566
  }
1240
- function canonicalManifestJson(permissions, roles) {
1567
+ function wireRole(role) {
1568
+ return {
1569
+ key: role.key,
1570
+ name: role.name,
1571
+ ...role.description === void 0 ? {} : { description: role.description },
1572
+ permissions: uniqueSorted(role.permissions),
1573
+ ...role.tenantAssignable === false ? { tenant_assignable: false } : {}
1574
+ };
1575
+ }
1576
+ function canonicalManifestJson(permissions2, roles2) {
1241
1577
  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) => ({
1578
+ permissions: [...permissions2].sort((a, b) => byteCompare(a.key, b.key)).map((p) => ({
1579
+ key: p.key,
1580
+ description: emptyToNull(p.description),
1581
+ ...p.tenantAssignable === true ? { tenant_assignable: true } : {}
1582
+ })),
1583
+ roles: [...roles2].sort((a, b) => byteCompare(a.key, b.key)).map((r) => ({
1244
1584
  key: r.key,
1245
1585
  name: r.name,
1246
1586
  description: emptyToNull(r.description),
1247
- permissions: [...r.permissions].sort(byteCompare)
1587
+ permissions: uniqueSorted(r.permissions),
1588
+ ...r.tenantAssignable === false ? { tenant_assignable: false } : {}
1248
1589
  }))
1249
1590
  };
1250
1591
  return escapeLikePhp(JSON.stringify(canonical));
1251
1592
  }
1593
+ function uniqueSorted(values) {
1594
+ return [...new Set(values)].sort(byteCompare);
1595
+ }
1252
1596
  function emptyToNull(value) {
1253
1597
  return value === void 0 || value === "" ? null : value;
1254
1598
  }
@@ -1300,13 +1644,21 @@ exports.ConfigurationError = ConfigurationError;
1300
1644
  exports.FrontendApiError = FrontendApiError;
1301
1645
  exports.InvalidStateError = InvalidStateError;
1302
1646
  exports.VaultClient = VaultClient;
1647
+ exports.actor = actor;
1303
1648
  exports.buildManifest = buildManifest;
1304
1649
  exports.challenge = challenge;
1305
1650
  exports.createLegacyVerifier = createLegacyVerifier;
1306
1651
  exports.createVerifier = createVerifier;
1307
1652
  exports.defineAuthz = defineAuthz;
1653
+ exports.hasPermission = hasPermission;
1654
+ exports.hasRole = hasRole;
1655
+ exports.isSupportSession = isSupportSession;
1656
+ exports.organization = organization;
1657
+ exports.permissions = permissions;
1308
1658
  exports.publishManifest = publishManifest;
1309
1659
  exports.randomToken = randomToken;
1660
+ exports.roles = roles;
1661
+ exports.sessionId = sessionId;
1310
1662
  exports.verifyWebhook = verifyWebhook;
1311
1663
  //# sourceMappingURL=index.cjs.map
1312
1664
  //# sourceMappingURL=index.cjs.map