@apoa/core 0.1.2 → 0.2.1
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 +3 -3
- package/dist/index.cjs +218 -40
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +107 -14
- package/dist/index.d.ts +107 -14
- package/dist/index.js +214 -40
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -53,7 +53,7 @@ const denied = await client.authorize(token, "nationwidemortgage.com", "document
|
|
|
53
53
|
- **Cascade revocation**: revoke parent, all children die instantly
|
|
54
54
|
- **Audit trail**: append-only action log per token
|
|
55
55
|
- **Browser mode**: credential vault injection config (the AI never sees passwords)
|
|
56
|
-
- **
|
|
56
|
+
- **Comprehensive test suite** with cross-SDK fixture verification against the [Python SDK](https://pypi.org/project/apoa/)
|
|
57
57
|
|
|
58
58
|
## Two Usage Styles
|
|
59
59
|
|
|
@@ -84,8 +84,8 @@ Tokens created by `@apoa/core` validate in the [Python SDK](https://pypi.org/pro
|
|
|
84
84
|
## Links
|
|
85
85
|
|
|
86
86
|
- [Spec](https://github.com/agenticpoa/apoa/blob/main/SPEC.md)
|
|
87
|
-
- [Source](https://github.com/agenticpoa/apoa/tree/main/
|
|
88
|
-
- [Examples](https://github.com/agenticpoa/apoa/tree/main/
|
|
87
|
+
- [Source](https://github.com/agenticpoa/apoa/tree/main/sdks/typescript)
|
|
88
|
+
- [Examples](https://github.com/agenticpoa/apoa/tree/main/sdks/typescript/examples)
|
|
89
89
|
|
|
90
90
|
## License
|
|
91
91
|
|
package/dist/index.cjs
CHANGED
|
@@ -42,16 +42,19 @@ __export(index_exports, {
|
|
|
42
42
|
ScopeViolationError: () => ScopeViolationError,
|
|
43
43
|
TokenExpiredError: () => TokenExpiredError,
|
|
44
44
|
authorize: () => authorize,
|
|
45
|
+
buildJWKS: () => buildJWKS,
|
|
45
46
|
cascadeRevoke: () => cascadeRevoke,
|
|
46
47
|
checkConstraint: () => checkConstraint,
|
|
47
48
|
checkScope: () => checkScope,
|
|
48
49
|
createClient: () => createClient,
|
|
50
|
+
createJWKSResolver: () => createJWKSResolver,
|
|
49
51
|
createToken: () => createToken,
|
|
50
52
|
decodeHeader: () => decodeHeader,
|
|
51
53
|
delegate: () => delegate,
|
|
52
54
|
generateKeyPair: () => generateKeyPair2,
|
|
53
55
|
getAuditTrail: () => getAuditTrail,
|
|
54
56
|
getAuditTrailByService: () => getAuditTrailByService,
|
|
57
|
+
getDelegationAncestorIds: () => getDelegationAncestorIds,
|
|
55
58
|
isBeforeNotBefore: () => isBeforeNotBefore,
|
|
56
59
|
isExpired: () => isExpired,
|
|
57
60
|
isRevoked: () => isRevoked,
|
|
@@ -59,6 +62,7 @@ __export(index_exports, {
|
|
|
59
62
|
matchScope: () => matchScope,
|
|
60
63
|
parseDefinition: () => parseDefinition,
|
|
61
64
|
parseScope: () => parseScope,
|
|
65
|
+
publicKeyToJWK: () => publicKeyToJWK,
|
|
62
66
|
revoke: () => revoke,
|
|
63
67
|
sign: () => sign,
|
|
64
68
|
signToken: () => signToken,
|
|
@@ -208,12 +212,14 @@ function parseScope(scope) {
|
|
|
208
212
|
return scope.split(":");
|
|
209
213
|
}
|
|
210
214
|
function matchScope(pattern, requested) {
|
|
215
|
+
if (!pattern || !requested) return false;
|
|
211
216
|
if (pattern === "*") return true;
|
|
212
217
|
const patternParts = parseScope(pattern);
|
|
213
218
|
const requestedParts = parseScope(requested);
|
|
214
219
|
if (patternParts.length !== requestedParts.length) return false;
|
|
215
220
|
for (let i = 0; i < patternParts.length; i++) {
|
|
216
221
|
if (patternParts[i] === "*") continue;
|
|
222
|
+
if (!patternParts[i] || !requestedParts[i]) return false;
|
|
217
223
|
if (patternParts[i] !== requestedParts[i]) return false;
|
|
218
224
|
}
|
|
219
225
|
return true;
|
|
@@ -379,8 +385,8 @@ async function authorize(token, service, action, options) {
|
|
|
379
385
|
for (const rule of rules) {
|
|
380
386
|
if (rule.enforcement === "hard") {
|
|
381
387
|
const ruleKey = rule.id.startsWith("no-") ? rule.id.slice(3) : rule.id;
|
|
382
|
-
const
|
|
383
|
-
if (
|
|
388
|
+
const actionSegments = action.toLowerCase().split(":");
|
|
389
|
+
if (actionSegments.includes(ruleKey.toLowerCase())) {
|
|
384
390
|
return {
|
|
385
391
|
authorized: false,
|
|
386
392
|
reason: `hard rule '${rule.id}' violated`,
|
|
@@ -488,6 +494,13 @@ function validateDefinition(raw) {
|
|
|
488
494
|
}
|
|
489
495
|
if (!svc.scopes || !Array.isArray(svc.scopes) || svc.scopes.length === 0) {
|
|
490
496
|
errors.push(`services[${i}].scopes must be a non-empty array`);
|
|
497
|
+
} else {
|
|
498
|
+
for (let j = 0; j < svc.scopes.length; j++) {
|
|
499
|
+
const s = svc.scopes[j];
|
|
500
|
+
if (typeof s !== "string" || s.length === 0) {
|
|
501
|
+
errors.push(`services[${i}].scopes[${j}] must be a non-empty string`);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
491
504
|
}
|
|
492
505
|
validateServiceAccessMode(svc, i, errors, warnings);
|
|
493
506
|
}
|
|
@@ -538,6 +551,11 @@ function validateMetadata(metadata, errors) {
|
|
|
538
551
|
}
|
|
539
552
|
const record = metadata;
|
|
540
553
|
for (const key of keys) {
|
|
554
|
+
if (key.startsWith("_")) {
|
|
555
|
+
errors.push(
|
|
556
|
+
`metadata key '${key}' uses reserved prefix '_' (reserved for SDK internal use)`
|
|
557
|
+
);
|
|
558
|
+
}
|
|
541
559
|
const value = record[key];
|
|
542
560
|
if (value !== null && typeof value !== "string" && typeof value !== "number" && typeof value !== "boolean") {
|
|
543
561
|
errors.push(
|
|
@@ -614,9 +632,7 @@ function validateLegalFramework(legal, errors) {
|
|
|
614
632
|
}
|
|
615
633
|
|
|
616
634
|
// src/revocation/revoke.ts
|
|
617
|
-
var defaultStore = new MemoryRevocationStore();
|
|
618
635
|
async function revoke(tokenId, options, store) {
|
|
619
|
-
const s = store ?? defaultStore;
|
|
620
636
|
const record = {
|
|
621
637
|
tokenId,
|
|
622
638
|
revokedAt: /* @__PURE__ */ new Date(),
|
|
@@ -624,19 +640,18 @@ async function revoke(tokenId, options, store) {
|
|
|
624
640
|
reason: options.reason,
|
|
625
641
|
cascaded: []
|
|
626
642
|
};
|
|
627
|
-
await
|
|
643
|
+
await store.add(record);
|
|
628
644
|
return record;
|
|
629
645
|
}
|
|
630
646
|
async function isRevoked(tokenId, store) {
|
|
631
|
-
const
|
|
632
|
-
const record = await s.check(tokenId);
|
|
647
|
+
const record = await store.check(tokenId);
|
|
633
648
|
return record !== null;
|
|
634
649
|
}
|
|
635
650
|
|
|
636
651
|
// src/audit/log.ts
|
|
637
|
-
var
|
|
652
|
+
var defaultStore = new MemoryAuditStore();
|
|
638
653
|
async function logAction(tokenId, entry, store) {
|
|
639
|
-
const s = store ??
|
|
654
|
+
const s = store ?? defaultStore;
|
|
640
655
|
const fullEntry = {
|
|
641
656
|
...entry,
|
|
642
657
|
tokenId,
|
|
@@ -646,13 +661,13 @@ async function logAction(tokenId, entry, store) {
|
|
|
646
661
|
}
|
|
647
662
|
|
|
648
663
|
// src/audit/trail.ts
|
|
649
|
-
var
|
|
664
|
+
var defaultStore2 = new MemoryAuditStore();
|
|
650
665
|
async function getAuditTrail(tokenId, options, store) {
|
|
651
|
-
const s = store ??
|
|
666
|
+
const s = store ?? defaultStore2;
|
|
652
667
|
return s.query(tokenId, options);
|
|
653
668
|
}
|
|
654
669
|
async function getAuditTrailByService(service, options, store) {
|
|
655
|
-
const s = store ??
|
|
670
|
+
const s = store ?? defaultStore2;
|
|
656
671
|
return s.queryByService(service, options);
|
|
657
672
|
}
|
|
658
673
|
|
|
@@ -682,7 +697,7 @@ async function verifySignature(token, key) {
|
|
|
682
697
|
}
|
|
683
698
|
|
|
684
699
|
// src/token/create.ts
|
|
685
|
-
async function createToken(definition, options) {
|
|
700
|
+
async function createToken(definition, options, parentTokenId) {
|
|
686
701
|
if (definition.metadata) {
|
|
687
702
|
validateMetadata2(definition.metadata);
|
|
688
703
|
}
|
|
@@ -698,7 +713,10 @@ async function createToken(definition, options) {
|
|
|
698
713
|
exp: Math.floor(
|
|
699
714
|
(typeof definition.expires === "string" ? new Date(definition.expires) : definition.expires).getTime() / 1e3
|
|
700
715
|
),
|
|
701
|
-
definition:
|
|
716
|
+
definition: {
|
|
717
|
+
...serializeDefinition(definition),
|
|
718
|
+
...parentTokenId ? { parentToken: parentTokenId } : {}
|
|
719
|
+
}
|
|
702
720
|
};
|
|
703
721
|
if (definition.notBefore) {
|
|
704
722
|
payload.nbf = Math.floor(
|
|
@@ -719,6 +737,7 @@ async function createToken(definition, options) {
|
|
|
719
737
|
signature: raw.split(".")[2],
|
|
720
738
|
issuer,
|
|
721
739
|
audience,
|
|
740
|
+
parentToken: parentTokenId,
|
|
722
741
|
raw
|
|
723
742
|
};
|
|
724
743
|
return token;
|
|
@@ -806,13 +825,29 @@ async function validateToken(token, options) {
|
|
|
806
825
|
"No key provided: supply publicKey, keyResolver, or publicKeyResolver"
|
|
807
826
|
);
|
|
808
827
|
}
|
|
828
|
+
const permittedAlgorithms = options.algorithms ?? ["EdDSA", "ES256"];
|
|
829
|
+
let headerAlg;
|
|
830
|
+
try {
|
|
831
|
+
const header = decodeHeader(rawJwt);
|
|
832
|
+
headerAlg = typeof header.alg === "string" ? header.alg : void 0;
|
|
833
|
+
} catch {
|
|
834
|
+
}
|
|
835
|
+
if (headerAlg && !permittedAlgorithms.includes(headerAlg)) {
|
|
836
|
+
errors.push(
|
|
837
|
+
`Token alg '${headerAlg}' is not in the permitted list [${permittedAlgorithms.join(", ")}]`
|
|
838
|
+
);
|
|
839
|
+
}
|
|
809
840
|
let payload;
|
|
810
|
-
|
|
841
|
+
let signatureVerified = false;
|
|
842
|
+
if (publicKey && headerAlg && permittedAlgorithms.includes(headerAlg)) {
|
|
811
843
|
try {
|
|
812
844
|
payload = await verifySignature(rawJwt, publicKey);
|
|
845
|
+
signatureVerified = true;
|
|
813
846
|
} catch {
|
|
814
847
|
errors.push("Signature verification failed");
|
|
815
848
|
}
|
|
849
|
+
} else if (publicKey && !headerAlg) {
|
|
850
|
+
errors.push("Token header missing alg");
|
|
816
851
|
}
|
|
817
852
|
if (!payload) {
|
|
818
853
|
try {
|
|
@@ -828,10 +863,12 @@ async function validateToken(token, options) {
|
|
|
828
863
|
if (!payload) {
|
|
829
864
|
return { valid: false, errors: errors.length > 0 ? errors : ["Unable to decode token"], warnings };
|
|
830
865
|
}
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
866
|
+
if (signatureVerified) {
|
|
867
|
+
try {
|
|
868
|
+
parsedToken = payloadToToken(payload, rawJwt);
|
|
869
|
+
} catch {
|
|
870
|
+
errors.push("Token payload has invalid structure");
|
|
871
|
+
}
|
|
835
872
|
}
|
|
836
873
|
const clockSkew = options.clockSkew;
|
|
837
874
|
if (payload.exp !== void 0) {
|
|
@@ -895,9 +932,7 @@ function base64urlDecode(input) {
|
|
|
895
932
|
}
|
|
896
933
|
|
|
897
934
|
// src/revocation/cascade.ts
|
|
898
|
-
var defaultStore4 = new MemoryRevocationStore();
|
|
899
935
|
async function cascadeRevoke(parentTokenId, childTokenIds, options, store) {
|
|
900
|
-
const s = store ?? defaultStore4;
|
|
901
936
|
const revokedAt = /* @__PURE__ */ new Date();
|
|
902
937
|
for (const childId of childTokenIds) {
|
|
903
938
|
const childRecord = {
|
|
@@ -907,7 +942,7 @@ async function cascadeRevoke(parentTokenId, childTokenIds, options, store) {
|
|
|
907
942
|
reason: options.reason ? `Cascade: ${options.reason}` : `Cascade revocation from parent ${parentTokenId}`,
|
|
908
943
|
cascaded: []
|
|
909
944
|
};
|
|
910
|
-
await
|
|
945
|
+
await store.add(childRecord);
|
|
911
946
|
}
|
|
912
947
|
const parentRecord = {
|
|
913
948
|
tokenId: parentTokenId,
|
|
@@ -916,7 +951,7 @@ async function cascadeRevoke(parentTokenId, childTokenIds, options, store) {
|
|
|
916
951
|
reason: options.reason,
|
|
917
952
|
cascaded: childTokenIds
|
|
918
953
|
};
|
|
919
|
-
await
|
|
954
|
+
await store.add(parentRecord);
|
|
920
955
|
return parentRecord;
|
|
921
956
|
}
|
|
922
957
|
|
|
@@ -965,7 +1000,12 @@ function verifyAttenuation(parent, child, currentDepth = 0) {
|
|
|
965
1000
|
verifyConstraintsNotRelaxed(parentService, childService);
|
|
966
1001
|
}
|
|
967
1002
|
if (parentDef.rules && parentDef.rules.length > 0) {
|
|
968
|
-
verifyRulesNotRemoved(
|
|
1003
|
+
verifyRulesNotRemoved(
|
|
1004
|
+
parentDef.rules.map((r) => r.id),
|
|
1005
|
+
child,
|
|
1006
|
+
parentDef.services.flatMap((s) => s.scopes),
|
|
1007
|
+
child.services.flatMap((s) => s.scopes)
|
|
1008
|
+
);
|
|
969
1009
|
}
|
|
970
1010
|
}
|
|
971
1011
|
function verifyScopeSubset(parent, child) {
|
|
@@ -987,26 +1027,23 @@ function verifyConstraintsNotRelaxed(parent, child) {
|
|
|
987
1027
|
for (const [key, parentValue] of Object.entries(parent.constraints)) {
|
|
988
1028
|
if (parentValue === false) {
|
|
989
1029
|
const childValue = child.constraints?.[key];
|
|
990
|
-
if (childValue ===
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
);
|
|
997
|
-
}
|
|
998
|
-
}
|
|
1030
|
+
if (childValue === false) continue;
|
|
1031
|
+
throw new AttenuationViolationError(
|
|
1032
|
+
childValue === true ? `Child relaxes constraint '${key}' on service '${child.service}' (parent: false, child: true)` : `Child omits constraint '${key}' on service '${child.service}' (parent: false, child: undefined)`,
|
|
1033
|
+
parent.scopes,
|
|
1034
|
+
child.scopes
|
|
1035
|
+
);
|
|
999
1036
|
}
|
|
1000
1037
|
}
|
|
1001
1038
|
}
|
|
1002
|
-
function verifyRulesNotRemoved(parentRuleIds, child) {
|
|
1039
|
+
function verifyRulesNotRemoved(parentRuleIds, child, parentScopes, childScopes) {
|
|
1003
1040
|
const childRuleIds = new Set(child.rules?.map((r) => r.id) ?? []);
|
|
1004
1041
|
for (const parentRuleId of parentRuleIds) {
|
|
1005
1042
|
if (!childRuleIds.has(parentRuleId)) {
|
|
1006
1043
|
throw new AttenuationViolationError(
|
|
1007
1044
|
`Child removes parent rule '${parentRuleId}'. Rules can only be added, not removed.`,
|
|
1008
|
-
|
|
1009
|
-
|
|
1045
|
+
parentScopes,
|
|
1046
|
+
childScopes
|
|
1010
1047
|
);
|
|
1011
1048
|
}
|
|
1012
1049
|
}
|
|
@@ -1015,7 +1052,6 @@ function verifyRulesNotRemoved(parentRuleIds, child) {
|
|
|
1015
1052
|
// src/delegation/chain.ts
|
|
1016
1053
|
async function delegate(parentToken, childDef, options) {
|
|
1017
1054
|
const currentDepth = countDepth(parentToken);
|
|
1018
|
-
verifyAttenuation(parentToken, childDef, currentDepth);
|
|
1019
1055
|
const parentDef = parentToken.definition;
|
|
1020
1056
|
const parentRules = parentDef.rules ?? [];
|
|
1021
1057
|
const childExtraRules = (childDef.rules ?? []).filter(
|
|
@@ -1027,12 +1063,32 @@ async function delegate(parentToken, childDef, options) {
|
|
|
1027
1063
|
...childDef.metadata,
|
|
1028
1064
|
_delegationDepth: childDepth
|
|
1029
1065
|
};
|
|
1066
|
+
const inheritedServices = childDef.services.map((childSvc) => {
|
|
1067
|
+
const parentSvc = parentDef.services.find((s) => s.service === childSvc.service);
|
|
1068
|
+
if (!parentSvc?.constraints) return childSvc;
|
|
1069
|
+
const inherited = {};
|
|
1070
|
+
for (const [key, value] of Object.entries(parentSvc.constraints)) {
|
|
1071
|
+
if (value === false) {
|
|
1072
|
+
inherited[key] = false;
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
if (Object.keys(inherited).length === 0) return childSvc;
|
|
1076
|
+
return {
|
|
1077
|
+
...childSvc,
|
|
1078
|
+
constraints: { ...inherited, ...childSvc.constraints }
|
|
1079
|
+
};
|
|
1080
|
+
});
|
|
1081
|
+
verifyAttenuation(
|
|
1082
|
+
parentToken,
|
|
1083
|
+
{ ...childDef, services: inheritedServices, rules: mergedRules },
|
|
1084
|
+
currentDepth
|
|
1085
|
+
);
|
|
1030
1086
|
const fullDefinition = {
|
|
1031
1087
|
principal: parentDef.principal,
|
|
1032
1088
|
// Inherited — cannot be overridden
|
|
1033
1089
|
agent: childDef.agent,
|
|
1034
1090
|
agentProvider: parentDef.agentProvider,
|
|
1035
|
-
services:
|
|
1091
|
+
services: inheritedServices,
|
|
1036
1092
|
rules: mergedRules.length > 0 ? mergedRules : void 0,
|
|
1037
1093
|
expires: childDef.expires ?? parentDef.expires,
|
|
1038
1094
|
revocable: parentDef.revocable,
|
|
@@ -1041,8 +1097,7 @@ async function delegate(parentToken, childDef, options) {
|
|
|
1041
1097
|
metadata: childMetadata,
|
|
1042
1098
|
legal: parentDef.legal
|
|
1043
1099
|
};
|
|
1044
|
-
const childToken = await createToken(fullDefinition, options);
|
|
1045
|
-
childToken.parentToken = parentToken.jti;
|
|
1100
|
+
const childToken = await createToken(fullDefinition, options, parentToken.jti);
|
|
1046
1101
|
return childToken;
|
|
1047
1102
|
}
|
|
1048
1103
|
function countDepth(token) {
|
|
@@ -1136,6 +1191,18 @@ function checkAttenuation(parent, child, index, errors) {
|
|
|
1136
1191
|
);
|
|
1137
1192
|
}
|
|
1138
1193
|
}
|
|
1194
|
+
if (parentService.constraints) {
|
|
1195
|
+
for (const [key, parentValue] of Object.entries(parentService.constraints)) {
|
|
1196
|
+
if (parentValue === false) {
|
|
1197
|
+
const childValue = childService.constraints?.[key];
|
|
1198
|
+
if (childValue !== false) {
|
|
1199
|
+
errors.push(
|
|
1200
|
+
`Chain link ${index}: constraint '${key}' on '${childService.service}' relaxed by child (parent: false, child: ${childValue === void 0 ? "undefined" : JSON.stringify(childValue)})`
|
|
1201
|
+
);
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1139
1206
|
}
|
|
1140
1207
|
const childExp = child.definition.expires instanceof Date ? child.definition.expires.getTime() : new Date(child.definition.expires).getTime();
|
|
1141
1208
|
const parentExp = parent.definition.expires instanceof Date ? parent.definition.expires.getTime() : new Date(parent.definition.expires).getTime();
|
|
@@ -1146,6 +1213,113 @@ function checkAttenuation(parent, child, index, errors) {
|
|
|
1146
1213
|
}
|
|
1147
1214
|
}
|
|
1148
1215
|
|
|
1216
|
+
// src/delegation/ancestors.ts
|
|
1217
|
+
function getDelegationAncestorIds(input) {
|
|
1218
|
+
const ids = [];
|
|
1219
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1220
|
+
const push = (value) => {
|
|
1221
|
+
if (typeof value !== "string" || value.length === 0 || seen.has(value)) {
|
|
1222
|
+
return;
|
|
1223
|
+
}
|
|
1224
|
+
seen.add(value);
|
|
1225
|
+
ids.push(value);
|
|
1226
|
+
};
|
|
1227
|
+
const token = input;
|
|
1228
|
+
const definition = hasDefinition(input) ? token.definition : input;
|
|
1229
|
+
push(token.parentToken);
|
|
1230
|
+
push(definition?.parentToken);
|
|
1231
|
+
const chain = definition?.delegationChain;
|
|
1232
|
+
if (Array.isArray(chain)) {
|
|
1233
|
+
for (const link of chain) {
|
|
1234
|
+
if (typeof link === "string") {
|
|
1235
|
+
push(link);
|
|
1236
|
+
} else if (link && typeof link === "object") {
|
|
1237
|
+
push(link.parentTokenId);
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
return ids;
|
|
1242
|
+
}
|
|
1243
|
+
function hasDefinition(input) {
|
|
1244
|
+
return Boolean(input && typeof input === "object" && "definition" in input);
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
// src/jwks/index.ts
|
|
1248
|
+
var jose3 = __toESM(require("jose"), 1);
|
|
1249
|
+
async function publicKeyToJWK(publicKey, options) {
|
|
1250
|
+
const exported = await jose3.exportJWK(publicKey);
|
|
1251
|
+
const alg = options.alg ?? defaultAlgorithm(exported);
|
|
1252
|
+
return {
|
|
1253
|
+
...exported,
|
|
1254
|
+
kid: options.kid,
|
|
1255
|
+
use: options.use ?? "sig",
|
|
1256
|
+
alg
|
|
1257
|
+
};
|
|
1258
|
+
}
|
|
1259
|
+
function buildJWKS(keys) {
|
|
1260
|
+
return { keys };
|
|
1261
|
+
}
|
|
1262
|
+
function createJWKSResolver(url, options = {}) {
|
|
1263
|
+
if (!options.fetch && !options.allowInsecure && !url.startsWith("https://")) {
|
|
1264
|
+
throw new Error(
|
|
1265
|
+
`JWKS URL must use https:// (got: ${JSON.stringify(url)}). Pass allowInsecure: true or a custom fetch for local development.`
|
|
1266
|
+
);
|
|
1267
|
+
}
|
|
1268
|
+
const cacheMaxAgeMs = options.cacheMaxAgeMs ?? 60 * 60 * 1e3;
|
|
1269
|
+
const cooldownMs = options.cooldownMs ?? 24 * 60 * 60 * 1e3;
|
|
1270
|
+
const fetchImpl = options.fetch ?? fetch;
|
|
1271
|
+
let cache = null;
|
|
1272
|
+
let inflight = null;
|
|
1273
|
+
async function fetchJWKS() {
|
|
1274
|
+
const response = await fetchImpl(url, {
|
|
1275
|
+
headers: { accept: "application/jwk-set+json, application/json" }
|
|
1276
|
+
});
|
|
1277
|
+
if (!response.ok) {
|
|
1278
|
+
throw new Error(`JWKS fetch failed: ${response.status} ${response.statusText}`);
|
|
1279
|
+
}
|
|
1280
|
+
const body = await response.json();
|
|
1281
|
+
if (!body || !Array.isArray(body.keys)) {
|
|
1282
|
+
throw new Error("JWKS response did not contain a `keys` array");
|
|
1283
|
+
}
|
|
1284
|
+
return body;
|
|
1285
|
+
}
|
|
1286
|
+
async function getJWKS() {
|
|
1287
|
+
const now = Date.now();
|
|
1288
|
+
if (cache && now - cache.fetchedAt < cacheMaxAgeMs) {
|
|
1289
|
+
return cache.jwks;
|
|
1290
|
+
}
|
|
1291
|
+
if (inflight) {
|
|
1292
|
+
return inflight;
|
|
1293
|
+
}
|
|
1294
|
+
inflight = fetchJWKS().then((jwks) => {
|
|
1295
|
+
cache = { fetchedAt: Date.now(), jwks };
|
|
1296
|
+
return jwks;
|
|
1297
|
+
}).catch((err) => {
|
|
1298
|
+
if (cache && now - cache.fetchedAt < cooldownMs) {
|
|
1299
|
+
return cache.jwks;
|
|
1300
|
+
}
|
|
1301
|
+
throw err;
|
|
1302
|
+
}).finally(() => {
|
|
1303
|
+
inflight = null;
|
|
1304
|
+
});
|
|
1305
|
+
return inflight;
|
|
1306
|
+
}
|
|
1307
|
+
return {
|
|
1308
|
+
async resolve(kid) {
|
|
1309
|
+
const jwks = await getJWKS();
|
|
1310
|
+
const match = jwks.keys.find((k) => k.kid === kid);
|
|
1311
|
+
if (!match) return null;
|
|
1312
|
+
const key = await jose3.importJWK(match, match.alg);
|
|
1313
|
+
return key;
|
|
1314
|
+
}
|
|
1315
|
+
};
|
|
1316
|
+
}
|
|
1317
|
+
function defaultAlgorithm(jwk) {
|
|
1318
|
+
if (jwk.kty === "OKP" && jwk.crv === "Ed25519") return "EdDSA";
|
|
1319
|
+
if (jwk.kty === "EC" && jwk.crv === "P-256") return "ES256";
|
|
1320
|
+
return jwk.alg ?? "EdDSA";
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1149
1323
|
// src/client.ts
|
|
1150
1324
|
function createClient(options) {
|
|
1151
1325
|
const revocationStore = options?.revocationStore ?? new MemoryRevocationStore();
|
|
@@ -1229,16 +1403,19 @@ function createClient(options) {
|
|
|
1229
1403
|
ScopeViolationError,
|
|
1230
1404
|
TokenExpiredError,
|
|
1231
1405
|
authorize,
|
|
1406
|
+
buildJWKS,
|
|
1232
1407
|
cascadeRevoke,
|
|
1233
1408
|
checkConstraint,
|
|
1234
1409
|
checkScope,
|
|
1235
1410
|
createClient,
|
|
1411
|
+
createJWKSResolver,
|
|
1236
1412
|
createToken,
|
|
1237
1413
|
decodeHeader,
|
|
1238
1414
|
delegate,
|
|
1239
1415
|
generateKeyPair,
|
|
1240
1416
|
getAuditTrail,
|
|
1241
1417
|
getAuditTrailByService,
|
|
1418
|
+
getDelegationAncestorIds,
|
|
1242
1419
|
isBeforeNotBefore,
|
|
1243
1420
|
isExpired,
|
|
1244
1421
|
isRevoked,
|
|
@@ -1246,6 +1423,7 @@ function createClient(options) {
|
|
|
1246
1423
|
matchScope,
|
|
1247
1424
|
parseDefinition,
|
|
1248
1425
|
parseScope,
|
|
1426
|
+
publicKeyToJWK,
|
|
1249
1427
|
revoke,
|
|
1250
1428
|
sign,
|
|
1251
1429
|
signToken,
|