@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.
- package/README.md +455 -4
- package/dist/chunk-2E25IPBA.js +155 -0
- package/dist/chunk-2E25IPBA.js.map +1 -0
- package/dist/{chunk-3V6OKRWF.js → chunk-CXGKOJRJ.js} +374 -90
- package/dist/chunk-CXGKOJRJ.js.map +1 -0
- package/dist/chunk-QQW63PGU.js +25 -0
- package/dist/chunk-QQW63PGU.js.map +1 -0
- package/dist/chunk-SEUD4TE4.js +61 -0
- package/dist/chunk-SEUD4TE4.js.map +1 -0
- package/dist/{client-CecDIu_N.d.cts → client-BSZASrMo.d.ts} +109 -136
- package/dist/{client-CecDIu_N.d.ts → client-BxUHm5hX.d.cts} +109 -136
- package/dist/errors-D61xljZE.d.cts +87 -0
- package/dist/errors-D61xljZE.d.ts +87 -0
- package/dist/index.cjs +440 -35
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +40 -90
- package/dist/index.d.ts +40 -90
- package/dist/index.js +64 -25
- package/dist/index.js.map +1 -1
- package/dist/management.cjs +4372 -0
- package/dist/management.cjs.map +1 -0
- package/dist/management.d.cts +10923 -0
- package/dist/management.d.ts +10923 -0
- package/dist/management.js +4335 -0
- package/dist/management.js.map +1 -0
- 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 +42 -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 +5 -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 +14 -2
- 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,
|
|
110
|
-
if (!
|
|
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
|
|
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:
|
|
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 (
|
|
744
|
-
params.set("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:
|
|
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
|
|
1130
|
-
const
|
|
1131
|
-
assertDeclaration(
|
|
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
|
|
1139
|
-
const
|
|
1140
|
-
assertDeclaration(
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
1234
|
-
|
|
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
|
|
1237
|
-
|
|
1238
|
-
|
|
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(
|
|
1628
|
+
function canonicalManifestJson(permissions2, roles2) {
|
|
1241
1629
|
const canonical = {
|
|
1242
|
-
permissions: [...
|
|
1243
|
-
|
|
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:
|
|
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
|