@apoa/core 0.1.0 → 0.2.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 +92 -0
- package/dist/index.cjs +185 -40
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +89 -14
- package/dist/index.d.ts +89 -14
- package/dist/index.js +182 -40
- package/dist/index.js.map +1 -1
- package/package.json +19 -4
package/README.md
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# @apoa/core
|
|
2
|
+
|
|
3
|
+
Reference TypeScript SDK for the [Agentic Power of Attorney (APOA)](https://github.com/agenticpoa/apoa) standard -- authorization infrastructure for AI agents.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @apoa/core
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import { createToken, checkScope, generateKeyPair, createClient } from '@apoa/core';
|
|
15
|
+
|
|
16
|
+
// Generate keys and create a client
|
|
17
|
+
const keys = await generateKeyPair();
|
|
18
|
+
const client = createClient({ defaultSigningOptions: { privateKey: keys.privateKey } });
|
|
19
|
+
|
|
20
|
+
// Create a signed authorization token
|
|
21
|
+
const token = await client.createToken({
|
|
22
|
+
principal: { id: "did:apoa:you" },
|
|
23
|
+
agent: { id: "did:apoa:your-agent", name: "HomeBot Pro" },
|
|
24
|
+
services: [{
|
|
25
|
+
service: "nationwidemortgage.com",
|
|
26
|
+
scopes: ["rate_lock:read", "documents:read"],
|
|
27
|
+
constraints: { signing: false },
|
|
28
|
+
accessMode: "browser",
|
|
29
|
+
browserConfig: {
|
|
30
|
+
allowedUrls: ["https://portal.nationwidemortgage.com/*"],
|
|
31
|
+
credentialVaultRef: "1password://vault/mortgage-portal",
|
|
32
|
+
},
|
|
33
|
+
}],
|
|
34
|
+
rules: [{ id: "no-signing", description: "Never sign anything", enforcement: "hard" }],
|
|
35
|
+
expires: "2026-09-01",
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// Authorize actions
|
|
39
|
+
const result = await client.authorize(token, "nationwidemortgage.com", "rate_lock:read");
|
|
40
|
+
// { authorized: true, checks: { revoked: false, scopeAllowed: true, ... } }
|
|
41
|
+
|
|
42
|
+
const denied = await client.authorize(token, "nationwidemortgage.com", "documents:sign");
|
|
43
|
+
// { authorized: false, reason: "scope 'documents:sign' not in authorized scopes" }
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Features
|
|
47
|
+
|
|
48
|
+
- **Token lifecycle**: create, sign (Ed25519/ES256), validate, parse
|
|
49
|
+
- **Scope matching**: hierarchical patterns (`appointments:*` matches `appointments:read`)
|
|
50
|
+
- **Constraint enforcement**: boolean denial checks
|
|
51
|
+
- **Authorization**: revocation + scope + constraints + hard/soft rules in one call
|
|
52
|
+
- **Delegation chains**: capability attenuation (permissions only narrow, never expand)
|
|
53
|
+
- **Cascade revocation**: revoke parent, all children die instantly
|
|
54
|
+
- **Audit trail**: append-only action log per token
|
|
55
|
+
- **Browser mode**: credential vault injection config (the AI never sees passwords)
|
|
56
|
+
- **Comprehensive test suite** with cross-SDK fixture verification against the [Python SDK](https://pypi.org/project/apoa/)
|
|
57
|
+
|
|
58
|
+
## Two Usage Styles
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
// Style 1: Client instance (recommended for apps)
|
|
62
|
+
const client = createClient({
|
|
63
|
+
revocationStore: new MemoryRevocationStore(),
|
|
64
|
+
auditStore: new MemoryAuditStore(),
|
|
65
|
+
defaultSigningOptions: { privateKey: keys.privateKey },
|
|
66
|
+
});
|
|
67
|
+
await client.authorize(token, "service.com", "action:read");
|
|
68
|
+
|
|
69
|
+
// Style 2: Standalone imports (for scripts and tests)
|
|
70
|
+
import { checkScope, authorize, createToken } from '@apoa/core';
|
|
71
|
+
checkScope(token, "service.com", "action:read");
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Cross-SDK Compatibility
|
|
75
|
+
|
|
76
|
+
Tokens created by `@apoa/core` validate in the [Python SDK](https://pypi.org/project/apoa/) and vice versa. The camelCase JWT payload round-trips correctly across both SDKs.
|
|
77
|
+
|
|
78
|
+
## Ecosystem
|
|
79
|
+
|
|
80
|
+
- [`@apoa/mcp`](https://www.npmjs.com/package/@apoa/mcp) -- APOA authorization for MCP servers
|
|
81
|
+
- [`@apoa/a2a`](https://github.com/agenticpoa/apoa-a2a) -- APOA authorization for A2A agent-to-agent communication
|
|
82
|
+
- [`apoa`](https://pypi.org/project/apoa/) -- Python SDK
|
|
83
|
+
|
|
84
|
+
## Links
|
|
85
|
+
|
|
86
|
+
- [Spec](https://github.com/agenticpoa/apoa/blob/main/SPEC.md)
|
|
87
|
+
- [Source](https://github.com/agenticpoa/apoa/tree/main/sdks/typescript)
|
|
88
|
+
- [Examples](https://github.com/agenticpoa/apoa/tree/main/sdks/typescript/examples)
|
|
89
|
+
|
|
90
|
+
## License
|
|
91
|
+
|
|
92
|
+
Apache-2.0
|
package/dist/index.cjs
CHANGED
|
@@ -42,10 +42,12 @@ __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,
|
|
@@ -59,6 +61,7 @@ __export(index_exports, {
|
|
|
59
61
|
matchScope: () => matchScope,
|
|
60
62
|
parseDefinition: () => parseDefinition,
|
|
61
63
|
parseScope: () => parseScope,
|
|
64
|
+
publicKeyToJWK: () => publicKeyToJWK,
|
|
62
65
|
revoke: () => revoke,
|
|
63
66
|
sign: () => sign,
|
|
64
67
|
signToken: () => signToken,
|
|
@@ -208,12 +211,14 @@ function parseScope(scope) {
|
|
|
208
211
|
return scope.split(":");
|
|
209
212
|
}
|
|
210
213
|
function matchScope(pattern, requested) {
|
|
214
|
+
if (!pattern || !requested) return false;
|
|
211
215
|
if (pattern === "*") return true;
|
|
212
216
|
const patternParts = parseScope(pattern);
|
|
213
217
|
const requestedParts = parseScope(requested);
|
|
214
218
|
if (patternParts.length !== requestedParts.length) return false;
|
|
215
219
|
for (let i = 0; i < patternParts.length; i++) {
|
|
216
220
|
if (patternParts[i] === "*") continue;
|
|
221
|
+
if (!patternParts[i] || !requestedParts[i]) return false;
|
|
217
222
|
if (patternParts[i] !== requestedParts[i]) return false;
|
|
218
223
|
}
|
|
219
224
|
return true;
|
|
@@ -379,8 +384,8 @@ async function authorize(token, service, action, options) {
|
|
|
379
384
|
for (const rule of rules) {
|
|
380
385
|
if (rule.enforcement === "hard") {
|
|
381
386
|
const ruleKey = rule.id.startsWith("no-") ? rule.id.slice(3) : rule.id;
|
|
382
|
-
const
|
|
383
|
-
if (
|
|
387
|
+
const actionSegments = action.toLowerCase().split(":");
|
|
388
|
+
if (actionSegments.includes(ruleKey.toLowerCase())) {
|
|
384
389
|
return {
|
|
385
390
|
authorized: false,
|
|
386
391
|
reason: `hard rule '${rule.id}' violated`,
|
|
@@ -488,6 +493,13 @@ function validateDefinition(raw) {
|
|
|
488
493
|
}
|
|
489
494
|
if (!svc.scopes || !Array.isArray(svc.scopes) || svc.scopes.length === 0) {
|
|
490
495
|
errors.push(`services[${i}].scopes must be a non-empty array`);
|
|
496
|
+
} else {
|
|
497
|
+
for (let j = 0; j < svc.scopes.length; j++) {
|
|
498
|
+
const s = svc.scopes[j];
|
|
499
|
+
if (typeof s !== "string" || s.length === 0) {
|
|
500
|
+
errors.push(`services[${i}].scopes[${j}] must be a non-empty string`);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
491
503
|
}
|
|
492
504
|
validateServiceAccessMode(svc, i, errors, warnings);
|
|
493
505
|
}
|
|
@@ -538,6 +550,11 @@ function validateMetadata(metadata, errors) {
|
|
|
538
550
|
}
|
|
539
551
|
const record = metadata;
|
|
540
552
|
for (const key of keys) {
|
|
553
|
+
if (key.startsWith("_")) {
|
|
554
|
+
errors.push(
|
|
555
|
+
`metadata key '${key}' uses reserved prefix '_' (reserved for SDK internal use)`
|
|
556
|
+
);
|
|
557
|
+
}
|
|
541
558
|
const value = record[key];
|
|
542
559
|
if (value !== null && typeof value !== "string" && typeof value !== "number" && typeof value !== "boolean") {
|
|
543
560
|
errors.push(
|
|
@@ -614,9 +631,7 @@ function validateLegalFramework(legal, errors) {
|
|
|
614
631
|
}
|
|
615
632
|
|
|
616
633
|
// src/revocation/revoke.ts
|
|
617
|
-
var defaultStore = new MemoryRevocationStore();
|
|
618
634
|
async function revoke(tokenId, options, store) {
|
|
619
|
-
const s = store ?? defaultStore;
|
|
620
635
|
const record = {
|
|
621
636
|
tokenId,
|
|
622
637
|
revokedAt: /* @__PURE__ */ new Date(),
|
|
@@ -624,19 +639,18 @@ async function revoke(tokenId, options, store) {
|
|
|
624
639
|
reason: options.reason,
|
|
625
640
|
cascaded: []
|
|
626
641
|
};
|
|
627
|
-
await
|
|
642
|
+
await store.add(record);
|
|
628
643
|
return record;
|
|
629
644
|
}
|
|
630
645
|
async function isRevoked(tokenId, store) {
|
|
631
|
-
const
|
|
632
|
-
const record = await s.check(tokenId);
|
|
646
|
+
const record = await store.check(tokenId);
|
|
633
647
|
return record !== null;
|
|
634
648
|
}
|
|
635
649
|
|
|
636
650
|
// src/audit/log.ts
|
|
637
|
-
var
|
|
651
|
+
var defaultStore = new MemoryAuditStore();
|
|
638
652
|
async function logAction(tokenId, entry, store) {
|
|
639
|
-
const s = store ??
|
|
653
|
+
const s = store ?? defaultStore;
|
|
640
654
|
const fullEntry = {
|
|
641
655
|
...entry,
|
|
642
656
|
tokenId,
|
|
@@ -646,13 +660,13 @@ async function logAction(tokenId, entry, store) {
|
|
|
646
660
|
}
|
|
647
661
|
|
|
648
662
|
// src/audit/trail.ts
|
|
649
|
-
var
|
|
663
|
+
var defaultStore2 = new MemoryAuditStore();
|
|
650
664
|
async function getAuditTrail(tokenId, options, store) {
|
|
651
|
-
const s = store ??
|
|
665
|
+
const s = store ?? defaultStore2;
|
|
652
666
|
return s.query(tokenId, options);
|
|
653
667
|
}
|
|
654
668
|
async function getAuditTrailByService(service, options, store) {
|
|
655
|
-
const s = store ??
|
|
669
|
+
const s = store ?? defaultStore2;
|
|
656
670
|
return s.queryByService(service, options);
|
|
657
671
|
}
|
|
658
672
|
|
|
@@ -682,7 +696,7 @@ async function verifySignature(token, key) {
|
|
|
682
696
|
}
|
|
683
697
|
|
|
684
698
|
// src/token/create.ts
|
|
685
|
-
async function createToken(definition, options) {
|
|
699
|
+
async function createToken(definition, options, parentTokenId) {
|
|
686
700
|
if (definition.metadata) {
|
|
687
701
|
validateMetadata2(definition.metadata);
|
|
688
702
|
}
|
|
@@ -698,7 +712,10 @@ async function createToken(definition, options) {
|
|
|
698
712
|
exp: Math.floor(
|
|
699
713
|
(typeof definition.expires === "string" ? new Date(definition.expires) : definition.expires).getTime() / 1e3
|
|
700
714
|
),
|
|
701
|
-
definition:
|
|
715
|
+
definition: {
|
|
716
|
+
...serializeDefinition(definition),
|
|
717
|
+
...parentTokenId ? { parentToken: parentTokenId } : {}
|
|
718
|
+
}
|
|
702
719
|
};
|
|
703
720
|
if (definition.notBefore) {
|
|
704
721
|
payload.nbf = Math.floor(
|
|
@@ -719,6 +736,7 @@ async function createToken(definition, options) {
|
|
|
719
736
|
signature: raw.split(".")[2],
|
|
720
737
|
issuer,
|
|
721
738
|
audience,
|
|
739
|
+
parentToken: parentTokenId,
|
|
722
740
|
raw
|
|
723
741
|
};
|
|
724
742
|
return token;
|
|
@@ -806,13 +824,29 @@ async function validateToken(token, options) {
|
|
|
806
824
|
"No key provided: supply publicKey, keyResolver, or publicKeyResolver"
|
|
807
825
|
);
|
|
808
826
|
}
|
|
827
|
+
const permittedAlgorithms = options.algorithms ?? ["EdDSA", "ES256"];
|
|
828
|
+
let headerAlg;
|
|
829
|
+
try {
|
|
830
|
+
const header = decodeHeader(rawJwt);
|
|
831
|
+
headerAlg = typeof header.alg === "string" ? header.alg : void 0;
|
|
832
|
+
} catch {
|
|
833
|
+
}
|
|
834
|
+
if (headerAlg && !permittedAlgorithms.includes(headerAlg)) {
|
|
835
|
+
errors.push(
|
|
836
|
+
`Token alg '${headerAlg}' is not in the permitted list [${permittedAlgorithms.join(", ")}]`
|
|
837
|
+
);
|
|
838
|
+
}
|
|
809
839
|
let payload;
|
|
810
|
-
|
|
840
|
+
let signatureVerified = false;
|
|
841
|
+
if (publicKey && headerAlg && permittedAlgorithms.includes(headerAlg)) {
|
|
811
842
|
try {
|
|
812
843
|
payload = await verifySignature(rawJwt, publicKey);
|
|
844
|
+
signatureVerified = true;
|
|
813
845
|
} catch {
|
|
814
846
|
errors.push("Signature verification failed");
|
|
815
847
|
}
|
|
848
|
+
} else if (publicKey && !headerAlg) {
|
|
849
|
+
errors.push("Token header missing alg");
|
|
816
850
|
}
|
|
817
851
|
if (!payload) {
|
|
818
852
|
try {
|
|
@@ -828,10 +862,12 @@ async function validateToken(token, options) {
|
|
|
828
862
|
if (!payload) {
|
|
829
863
|
return { valid: false, errors: errors.length > 0 ? errors : ["Unable to decode token"], warnings };
|
|
830
864
|
}
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
865
|
+
if (signatureVerified) {
|
|
866
|
+
try {
|
|
867
|
+
parsedToken = payloadToToken(payload, rawJwt);
|
|
868
|
+
} catch {
|
|
869
|
+
errors.push("Token payload has invalid structure");
|
|
870
|
+
}
|
|
835
871
|
}
|
|
836
872
|
const clockSkew = options.clockSkew;
|
|
837
873
|
if (payload.exp !== void 0) {
|
|
@@ -895,9 +931,7 @@ function base64urlDecode(input) {
|
|
|
895
931
|
}
|
|
896
932
|
|
|
897
933
|
// src/revocation/cascade.ts
|
|
898
|
-
var defaultStore4 = new MemoryRevocationStore();
|
|
899
934
|
async function cascadeRevoke(parentTokenId, childTokenIds, options, store) {
|
|
900
|
-
const s = store ?? defaultStore4;
|
|
901
935
|
const revokedAt = /* @__PURE__ */ new Date();
|
|
902
936
|
for (const childId of childTokenIds) {
|
|
903
937
|
const childRecord = {
|
|
@@ -907,7 +941,7 @@ async function cascadeRevoke(parentTokenId, childTokenIds, options, store) {
|
|
|
907
941
|
reason: options.reason ? `Cascade: ${options.reason}` : `Cascade revocation from parent ${parentTokenId}`,
|
|
908
942
|
cascaded: []
|
|
909
943
|
};
|
|
910
|
-
await
|
|
944
|
+
await store.add(childRecord);
|
|
911
945
|
}
|
|
912
946
|
const parentRecord = {
|
|
913
947
|
tokenId: parentTokenId,
|
|
@@ -916,7 +950,7 @@ async function cascadeRevoke(parentTokenId, childTokenIds, options, store) {
|
|
|
916
950
|
reason: options.reason,
|
|
917
951
|
cascaded: childTokenIds
|
|
918
952
|
};
|
|
919
|
-
await
|
|
953
|
+
await store.add(parentRecord);
|
|
920
954
|
return parentRecord;
|
|
921
955
|
}
|
|
922
956
|
|
|
@@ -965,7 +999,12 @@ function verifyAttenuation(parent, child, currentDepth = 0) {
|
|
|
965
999
|
verifyConstraintsNotRelaxed(parentService, childService);
|
|
966
1000
|
}
|
|
967
1001
|
if (parentDef.rules && parentDef.rules.length > 0) {
|
|
968
|
-
verifyRulesNotRemoved(
|
|
1002
|
+
verifyRulesNotRemoved(
|
|
1003
|
+
parentDef.rules.map((r) => r.id),
|
|
1004
|
+
child,
|
|
1005
|
+
parentDef.services.flatMap((s) => s.scopes),
|
|
1006
|
+
child.services.flatMap((s) => s.scopes)
|
|
1007
|
+
);
|
|
969
1008
|
}
|
|
970
1009
|
}
|
|
971
1010
|
function verifyScopeSubset(parent, child) {
|
|
@@ -987,26 +1026,23 @@ function verifyConstraintsNotRelaxed(parent, child) {
|
|
|
987
1026
|
for (const [key, parentValue] of Object.entries(parent.constraints)) {
|
|
988
1027
|
if (parentValue === false) {
|
|
989
1028
|
const childValue = child.constraints?.[key];
|
|
990
|
-
if (childValue ===
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
);
|
|
997
|
-
}
|
|
998
|
-
}
|
|
1029
|
+
if (childValue === false) continue;
|
|
1030
|
+
throw new AttenuationViolationError(
|
|
1031
|
+
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)`,
|
|
1032
|
+
parent.scopes,
|
|
1033
|
+
child.scopes
|
|
1034
|
+
);
|
|
999
1035
|
}
|
|
1000
1036
|
}
|
|
1001
1037
|
}
|
|
1002
|
-
function verifyRulesNotRemoved(parentRuleIds, child) {
|
|
1038
|
+
function verifyRulesNotRemoved(parentRuleIds, child, parentScopes, childScopes) {
|
|
1003
1039
|
const childRuleIds = new Set(child.rules?.map((r) => r.id) ?? []);
|
|
1004
1040
|
for (const parentRuleId of parentRuleIds) {
|
|
1005
1041
|
if (!childRuleIds.has(parentRuleId)) {
|
|
1006
1042
|
throw new AttenuationViolationError(
|
|
1007
1043
|
`Child removes parent rule '${parentRuleId}'. Rules can only be added, not removed.`,
|
|
1008
|
-
|
|
1009
|
-
|
|
1044
|
+
parentScopes,
|
|
1045
|
+
childScopes
|
|
1010
1046
|
);
|
|
1011
1047
|
}
|
|
1012
1048
|
}
|
|
@@ -1015,7 +1051,6 @@ function verifyRulesNotRemoved(parentRuleIds, child) {
|
|
|
1015
1051
|
// src/delegation/chain.ts
|
|
1016
1052
|
async function delegate(parentToken, childDef, options) {
|
|
1017
1053
|
const currentDepth = countDepth(parentToken);
|
|
1018
|
-
verifyAttenuation(parentToken, childDef, currentDepth);
|
|
1019
1054
|
const parentDef = parentToken.definition;
|
|
1020
1055
|
const parentRules = parentDef.rules ?? [];
|
|
1021
1056
|
const childExtraRules = (childDef.rules ?? []).filter(
|
|
@@ -1027,12 +1062,32 @@ async function delegate(parentToken, childDef, options) {
|
|
|
1027
1062
|
...childDef.metadata,
|
|
1028
1063
|
_delegationDepth: childDepth
|
|
1029
1064
|
};
|
|
1065
|
+
const inheritedServices = childDef.services.map((childSvc) => {
|
|
1066
|
+
const parentSvc = parentDef.services.find((s) => s.service === childSvc.service);
|
|
1067
|
+
if (!parentSvc?.constraints) return childSvc;
|
|
1068
|
+
const inherited = {};
|
|
1069
|
+
for (const [key, value] of Object.entries(parentSvc.constraints)) {
|
|
1070
|
+
if (value === false) {
|
|
1071
|
+
inherited[key] = false;
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
if (Object.keys(inherited).length === 0) return childSvc;
|
|
1075
|
+
return {
|
|
1076
|
+
...childSvc,
|
|
1077
|
+
constraints: { ...inherited, ...childSvc.constraints }
|
|
1078
|
+
};
|
|
1079
|
+
});
|
|
1080
|
+
verifyAttenuation(
|
|
1081
|
+
parentToken,
|
|
1082
|
+
{ ...childDef, services: inheritedServices, rules: mergedRules },
|
|
1083
|
+
currentDepth
|
|
1084
|
+
);
|
|
1030
1085
|
const fullDefinition = {
|
|
1031
1086
|
principal: parentDef.principal,
|
|
1032
1087
|
// Inherited — cannot be overridden
|
|
1033
1088
|
agent: childDef.agent,
|
|
1034
1089
|
agentProvider: parentDef.agentProvider,
|
|
1035
|
-
services:
|
|
1090
|
+
services: inheritedServices,
|
|
1036
1091
|
rules: mergedRules.length > 0 ? mergedRules : void 0,
|
|
1037
1092
|
expires: childDef.expires ?? parentDef.expires,
|
|
1038
1093
|
revocable: parentDef.revocable,
|
|
@@ -1041,8 +1096,7 @@ async function delegate(parentToken, childDef, options) {
|
|
|
1041
1096
|
metadata: childMetadata,
|
|
1042
1097
|
legal: parentDef.legal
|
|
1043
1098
|
};
|
|
1044
|
-
const childToken = await createToken(fullDefinition, options);
|
|
1045
|
-
childToken.parentToken = parentToken.jti;
|
|
1099
|
+
const childToken = await createToken(fullDefinition, options, parentToken.jti);
|
|
1046
1100
|
return childToken;
|
|
1047
1101
|
}
|
|
1048
1102
|
function countDepth(token) {
|
|
@@ -1136,6 +1190,18 @@ function checkAttenuation(parent, child, index, errors) {
|
|
|
1136
1190
|
);
|
|
1137
1191
|
}
|
|
1138
1192
|
}
|
|
1193
|
+
if (parentService.constraints) {
|
|
1194
|
+
for (const [key, parentValue] of Object.entries(parentService.constraints)) {
|
|
1195
|
+
if (parentValue === false) {
|
|
1196
|
+
const childValue = childService.constraints?.[key];
|
|
1197
|
+
if (childValue !== false) {
|
|
1198
|
+
errors.push(
|
|
1199
|
+
`Chain link ${index}: constraint '${key}' on '${childService.service}' relaxed by child (parent: false, child: ${childValue === void 0 ? "undefined" : JSON.stringify(childValue)})`
|
|
1200
|
+
);
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1139
1205
|
}
|
|
1140
1206
|
const childExp = child.definition.expires instanceof Date ? child.definition.expires.getTime() : new Date(child.definition.expires).getTime();
|
|
1141
1207
|
const parentExp = parent.definition.expires instanceof Date ? parent.definition.expires.getTime() : new Date(parent.definition.expires).getTime();
|
|
@@ -1146,6 +1212,82 @@ function checkAttenuation(parent, child, index, errors) {
|
|
|
1146
1212
|
}
|
|
1147
1213
|
}
|
|
1148
1214
|
|
|
1215
|
+
// src/jwks/index.ts
|
|
1216
|
+
var jose3 = __toESM(require("jose"), 1);
|
|
1217
|
+
async function publicKeyToJWK(publicKey, options) {
|
|
1218
|
+
const exported = await jose3.exportJWK(publicKey);
|
|
1219
|
+
const alg = options.alg ?? defaultAlgorithm(exported);
|
|
1220
|
+
return {
|
|
1221
|
+
...exported,
|
|
1222
|
+
kid: options.kid,
|
|
1223
|
+
use: options.use ?? "sig",
|
|
1224
|
+
alg
|
|
1225
|
+
};
|
|
1226
|
+
}
|
|
1227
|
+
function buildJWKS(keys) {
|
|
1228
|
+
return { keys };
|
|
1229
|
+
}
|
|
1230
|
+
function createJWKSResolver(url, options = {}) {
|
|
1231
|
+
if (!options.fetch && !options.allowInsecure && !url.startsWith("https://")) {
|
|
1232
|
+
throw new Error(
|
|
1233
|
+
`JWKS URL must use https:// (got: ${JSON.stringify(url)}). Pass allowInsecure: true or a custom fetch for local development.`
|
|
1234
|
+
);
|
|
1235
|
+
}
|
|
1236
|
+
const cacheMaxAgeMs = options.cacheMaxAgeMs ?? 60 * 60 * 1e3;
|
|
1237
|
+
const cooldownMs = options.cooldownMs ?? 24 * 60 * 60 * 1e3;
|
|
1238
|
+
const fetchImpl = options.fetch ?? fetch;
|
|
1239
|
+
let cache = null;
|
|
1240
|
+
let inflight = null;
|
|
1241
|
+
async function fetchJWKS() {
|
|
1242
|
+
const response = await fetchImpl(url, {
|
|
1243
|
+
headers: { accept: "application/jwk-set+json, application/json" }
|
|
1244
|
+
});
|
|
1245
|
+
if (!response.ok) {
|
|
1246
|
+
throw new Error(`JWKS fetch failed: ${response.status} ${response.statusText}`);
|
|
1247
|
+
}
|
|
1248
|
+
const body = await response.json();
|
|
1249
|
+
if (!body || !Array.isArray(body.keys)) {
|
|
1250
|
+
throw new Error("JWKS response did not contain a `keys` array");
|
|
1251
|
+
}
|
|
1252
|
+
return body;
|
|
1253
|
+
}
|
|
1254
|
+
async function getJWKS() {
|
|
1255
|
+
const now = Date.now();
|
|
1256
|
+
if (cache && now - cache.fetchedAt < cacheMaxAgeMs) {
|
|
1257
|
+
return cache.jwks;
|
|
1258
|
+
}
|
|
1259
|
+
if (inflight) {
|
|
1260
|
+
return inflight;
|
|
1261
|
+
}
|
|
1262
|
+
inflight = fetchJWKS().then((jwks) => {
|
|
1263
|
+
cache = { fetchedAt: Date.now(), jwks };
|
|
1264
|
+
return jwks;
|
|
1265
|
+
}).catch((err) => {
|
|
1266
|
+
if (cache && now - cache.fetchedAt < cooldownMs) {
|
|
1267
|
+
return cache.jwks;
|
|
1268
|
+
}
|
|
1269
|
+
throw err;
|
|
1270
|
+
}).finally(() => {
|
|
1271
|
+
inflight = null;
|
|
1272
|
+
});
|
|
1273
|
+
return inflight;
|
|
1274
|
+
}
|
|
1275
|
+
return {
|
|
1276
|
+
async resolve(kid) {
|
|
1277
|
+
const jwks = await getJWKS();
|
|
1278
|
+
const match = jwks.keys.find((k) => k.kid === kid);
|
|
1279
|
+
if (!match) return null;
|
|
1280
|
+
const key = await jose3.importJWK(match, match.alg);
|
|
1281
|
+
return key;
|
|
1282
|
+
}
|
|
1283
|
+
};
|
|
1284
|
+
}
|
|
1285
|
+
function defaultAlgorithm(jwk) {
|
|
1286
|
+
if (jwk.kty === "OKP" && jwk.crv === "Ed25519") return "EdDSA";
|
|
1287
|
+
if (jwk.kty === "EC" && jwk.crv === "P-256") return "ES256";
|
|
1288
|
+
return jwk.alg ?? "EdDSA";
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1149
1291
|
// src/client.ts
|
|
1150
1292
|
function createClient(options) {
|
|
1151
1293
|
const revocationStore = options?.revocationStore ?? new MemoryRevocationStore();
|
|
@@ -1229,10 +1371,12 @@ function createClient(options) {
|
|
|
1229
1371
|
ScopeViolationError,
|
|
1230
1372
|
TokenExpiredError,
|
|
1231
1373
|
authorize,
|
|
1374
|
+
buildJWKS,
|
|
1232
1375
|
cascadeRevoke,
|
|
1233
1376
|
checkConstraint,
|
|
1234
1377
|
checkScope,
|
|
1235
1378
|
createClient,
|
|
1379
|
+
createJWKSResolver,
|
|
1236
1380
|
createToken,
|
|
1237
1381
|
decodeHeader,
|
|
1238
1382
|
delegate,
|
|
@@ -1246,6 +1390,7 @@ function createClient(options) {
|
|
|
1246
1390
|
matchScope,
|
|
1247
1391
|
parseDefinition,
|
|
1248
1392
|
parseScope,
|
|
1393
|
+
publicKeyToJWK,
|
|
1249
1394
|
revoke,
|
|
1250
1395
|
sign,
|
|
1251
1396
|
signToken,
|