@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/README.md +237 -2
- package/dist/chunk-ASOQN47M.js +81 -0
- package/dist/chunk-ASOQN47M.js.map +1 -0
- package/dist/{chunk-3V6OKRWF.js → chunk-G3CD7X4U.js} +321 -90
- package/dist/chunk-G3CD7X4U.js.map +1 -0
- package/dist/chunk-WYTGLCMF.js +154 -0
- package/dist/chunk-WYTGLCMF.js.map +1 -0
- package/dist/{client-CecDIu_N.d.cts → client-CEXddiCR.d.cts} +81 -136
- package/dist/{client-CecDIu_N.d.ts → client-CdwdigzG.d.ts} +81 -136
- package/dist/index.cjs +384 -32
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +39 -4
- package/dist/index.d.ts +39 -4
- package/dist/index.js +63 -25
- package/dist/index.js.map +1 -1
- package/dist/nextjs.cjs +517 -42
- package/dist/nextjs.cjs.map +1 -1
- package/dist/nextjs.d.cts +29 -8
- package/dist/nextjs.d.ts +29 -8
- package/dist/nextjs.js +41 -12
- package/dist/nextjs.js.map +1 -1
- package/dist/server.cjs +220 -0
- package/dist/server.cjs.map +1 -0
- package/dist/server.d.cts +105 -0
- package/dist/server.d.ts +105 -0
- package/dist/server.js +4 -0
- package/dist/server.js.map +1 -0
- package/dist/types-BYZldock.d.cts +316 -0
- package/dist/types-BYZldock.d.ts +316 -0
- package/package.json +6 -1
- package/dist/chunk-3V6OKRWF.js.map +0 -1
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:
|
|
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 (
|
|
744
|
-
params.set("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:
|
|
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
|
|
1130
|
-
const
|
|
1131
|
-
assertDeclaration(
|
|
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
|
|
1139
|
-
const
|
|
1140
|
-
assertDeclaration(
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
1234
|
-
|
|
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
|
|
1237
|
-
|
|
1238
|
-
|
|
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
|
|
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: [...
|
|
1243
|
-
|
|
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:
|
|
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
|