@apoa/core 0.1.2 → 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 +3 -3
- 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 +3 -3
package/dist/index.js
CHANGED
|
@@ -136,12 +136,14 @@ function parseScope(scope) {
|
|
|
136
136
|
return scope.split(":");
|
|
137
137
|
}
|
|
138
138
|
function matchScope(pattern, requested) {
|
|
139
|
+
if (!pattern || !requested) return false;
|
|
139
140
|
if (pattern === "*") return true;
|
|
140
141
|
const patternParts = parseScope(pattern);
|
|
141
142
|
const requestedParts = parseScope(requested);
|
|
142
143
|
if (patternParts.length !== requestedParts.length) return false;
|
|
143
144
|
for (let i = 0; i < patternParts.length; i++) {
|
|
144
145
|
if (patternParts[i] === "*") continue;
|
|
146
|
+
if (!patternParts[i] || !requestedParts[i]) return false;
|
|
145
147
|
if (patternParts[i] !== requestedParts[i]) return false;
|
|
146
148
|
}
|
|
147
149
|
return true;
|
|
@@ -307,8 +309,8 @@ async function authorize(token, service, action, options) {
|
|
|
307
309
|
for (const rule of rules) {
|
|
308
310
|
if (rule.enforcement === "hard") {
|
|
309
311
|
const ruleKey = rule.id.startsWith("no-") ? rule.id.slice(3) : rule.id;
|
|
310
|
-
const
|
|
311
|
-
if (
|
|
312
|
+
const actionSegments = action.toLowerCase().split(":");
|
|
313
|
+
if (actionSegments.includes(ruleKey.toLowerCase())) {
|
|
312
314
|
return {
|
|
313
315
|
authorized: false,
|
|
314
316
|
reason: `hard rule '${rule.id}' violated`,
|
|
@@ -416,6 +418,13 @@ function validateDefinition(raw) {
|
|
|
416
418
|
}
|
|
417
419
|
if (!svc.scopes || !Array.isArray(svc.scopes) || svc.scopes.length === 0) {
|
|
418
420
|
errors.push(`services[${i}].scopes must be a non-empty array`);
|
|
421
|
+
} else {
|
|
422
|
+
for (let j = 0; j < svc.scopes.length; j++) {
|
|
423
|
+
const s = svc.scopes[j];
|
|
424
|
+
if (typeof s !== "string" || s.length === 0) {
|
|
425
|
+
errors.push(`services[${i}].scopes[${j}] must be a non-empty string`);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
419
428
|
}
|
|
420
429
|
validateServiceAccessMode(svc, i, errors, warnings);
|
|
421
430
|
}
|
|
@@ -466,6 +475,11 @@ function validateMetadata(metadata, errors) {
|
|
|
466
475
|
}
|
|
467
476
|
const record = metadata;
|
|
468
477
|
for (const key of keys) {
|
|
478
|
+
if (key.startsWith("_")) {
|
|
479
|
+
errors.push(
|
|
480
|
+
`metadata key '${key}' uses reserved prefix '_' (reserved for SDK internal use)`
|
|
481
|
+
);
|
|
482
|
+
}
|
|
469
483
|
const value = record[key];
|
|
470
484
|
if (value !== null && typeof value !== "string" && typeof value !== "number" && typeof value !== "boolean") {
|
|
471
485
|
errors.push(
|
|
@@ -542,9 +556,7 @@ function validateLegalFramework(legal, errors) {
|
|
|
542
556
|
}
|
|
543
557
|
|
|
544
558
|
// src/revocation/revoke.ts
|
|
545
|
-
var defaultStore = new MemoryRevocationStore();
|
|
546
559
|
async function revoke(tokenId, options, store) {
|
|
547
|
-
const s = store ?? defaultStore;
|
|
548
560
|
const record = {
|
|
549
561
|
tokenId,
|
|
550
562
|
revokedAt: /* @__PURE__ */ new Date(),
|
|
@@ -552,19 +564,18 @@ async function revoke(tokenId, options, store) {
|
|
|
552
564
|
reason: options.reason,
|
|
553
565
|
cascaded: []
|
|
554
566
|
};
|
|
555
|
-
await
|
|
567
|
+
await store.add(record);
|
|
556
568
|
return record;
|
|
557
569
|
}
|
|
558
570
|
async function isRevoked(tokenId, store) {
|
|
559
|
-
const
|
|
560
|
-
const record = await s.check(tokenId);
|
|
571
|
+
const record = await store.check(tokenId);
|
|
561
572
|
return record !== null;
|
|
562
573
|
}
|
|
563
574
|
|
|
564
575
|
// src/audit/log.ts
|
|
565
|
-
var
|
|
576
|
+
var defaultStore = new MemoryAuditStore();
|
|
566
577
|
async function logAction(tokenId, entry, store) {
|
|
567
|
-
const s = store ??
|
|
578
|
+
const s = store ?? defaultStore;
|
|
568
579
|
const fullEntry = {
|
|
569
580
|
...entry,
|
|
570
581
|
tokenId,
|
|
@@ -574,13 +585,13 @@ async function logAction(tokenId, entry, store) {
|
|
|
574
585
|
}
|
|
575
586
|
|
|
576
587
|
// src/audit/trail.ts
|
|
577
|
-
var
|
|
588
|
+
var defaultStore2 = new MemoryAuditStore();
|
|
578
589
|
async function getAuditTrail(tokenId, options, store) {
|
|
579
|
-
const s = store ??
|
|
590
|
+
const s = store ?? defaultStore2;
|
|
580
591
|
return s.query(tokenId, options);
|
|
581
592
|
}
|
|
582
593
|
async function getAuditTrailByService(service, options, store) {
|
|
583
|
-
const s = store ??
|
|
594
|
+
const s = store ?? defaultStore2;
|
|
584
595
|
return s.queryByService(service, options);
|
|
585
596
|
}
|
|
586
597
|
|
|
@@ -610,7 +621,7 @@ async function verifySignature(token, key) {
|
|
|
610
621
|
}
|
|
611
622
|
|
|
612
623
|
// src/token/create.ts
|
|
613
|
-
async function createToken(definition, options) {
|
|
624
|
+
async function createToken(definition, options, parentTokenId) {
|
|
614
625
|
if (definition.metadata) {
|
|
615
626
|
validateMetadata2(definition.metadata);
|
|
616
627
|
}
|
|
@@ -626,7 +637,10 @@ async function createToken(definition, options) {
|
|
|
626
637
|
exp: Math.floor(
|
|
627
638
|
(typeof definition.expires === "string" ? new Date(definition.expires) : definition.expires).getTime() / 1e3
|
|
628
639
|
),
|
|
629
|
-
definition:
|
|
640
|
+
definition: {
|
|
641
|
+
...serializeDefinition(definition),
|
|
642
|
+
...parentTokenId ? { parentToken: parentTokenId } : {}
|
|
643
|
+
}
|
|
630
644
|
};
|
|
631
645
|
if (definition.notBefore) {
|
|
632
646
|
payload.nbf = Math.floor(
|
|
@@ -647,6 +661,7 @@ async function createToken(definition, options) {
|
|
|
647
661
|
signature: raw.split(".")[2],
|
|
648
662
|
issuer,
|
|
649
663
|
audience,
|
|
664
|
+
parentToken: parentTokenId,
|
|
650
665
|
raw
|
|
651
666
|
};
|
|
652
667
|
return token;
|
|
@@ -734,13 +749,29 @@ async function validateToken(token, options) {
|
|
|
734
749
|
"No key provided: supply publicKey, keyResolver, or publicKeyResolver"
|
|
735
750
|
);
|
|
736
751
|
}
|
|
752
|
+
const permittedAlgorithms = options.algorithms ?? ["EdDSA", "ES256"];
|
|
753
|
+
let headerAlg;
|
|
754
|
+
try {
|
|
755
|
+
const header = decodeHeader(rawJwt);
|
|
756
|
+
headerAlg = typeof header.alg === "string" ? header.alg : void 0;
|
|
757
|
+
} catch {
|
|
758
|
+
}
|
|
759
|
+
if (headerAlg && !permittedAlgorithms.includes(headerAlg)) {
|
|
760
|
+
errors.push(
|
|
761
|
+
`Token alg '${headerAlg}' is not in the permitted list [${permittedAlgorithms.join(", ")}]`
|
|
762
|
+
);
|
|
763
|
+
}
|
|
737
764
|
let payload;
|
|
738
|
-
|
|
765
|
+
let signatureVerified = false;
|
|
766
|
+
if (publicKey && headerAlg && permittedAlgorithms.includes(headerAlg)) {
|
|
739
767
|
try {
|
|
740
768
|
payload = await verifySignature(rawJwt, publicKey);
|
|
769
|
+
signatureVerified = true;
|
|
741
770
|
} catch {
|
|
742
771
|
errors.push("Signature verification failed");
|
|
743
772
|
}
|
|
773
|
+
} else if (publicKey && !headerAlg) {
|
|
774
|
+
errors.push("Token header missing alg");
|
|
744
775
|
}
|
|
745
776
|
if (!payload) {
|
|
746
777
|
try {
|
|
@@ -756,10 +787,12 @@ async function validateToken(token, options) {
|
|
|
756
787
|
if (!payload) {
|
|
757
788
|
return { valid: false, errors: errors.length > 0 ? errors : ["Unable to decode token"], warnings };
|
|
758
789
|
}
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
790
|
+
if (signatureVerified) {
|
|
791
|
+
try {
|
|
792
|
+
parsedToken = payloadToToken(payload, rawJwt);
|
|
793
|
+
} catch {
|
|
794
|
+
errors.push("Token payload has invalid structure");
|
|
795
|
+
}
|
|
763
796
|
}
|
|
764
797
|
const clockSkew = options.clockSkew;
|
|
765
798
|
if (payload.exp !== void 0) {
|
|
@@ -823,9 +856,7 @@ function base64urlDecode(input) {
|
|
|
823
856
|
}
|
|
824
857
|
|
|
825
858
|
// src/revocation/cascade.ts
|
|
826
|
-
var defaultStore4 = new MemoryRevocationStore();
|
|
827
859
|
async function cascadeRevoke(parentTokenId, childTokenIds, options, store) {
|
|
828
|
-
const s = store ?? defaultStore4;
|
|
829
860
|
const revokedAt = /* @__PURE__ */ new Date();
|
|
830
861
|
for (const childId of childTokenIds) {
|
|
831
862
|
const childRecord = {
|
|
@@ -835,7 +866,7 @@ async function cascadeRevoke(parentTokenId, childTokenIds, options, store) {
|
|
|
835
866
|
reason: options.reason ? `Cascade: ${options.reason}` : `Cascade revocation from parent ${parentTokenId}`,
|
|
836
867
|
cascaded: []
|
|
837
868
|
};
|
|
838
|
-
await
|
|
869
|
+
await store.add(childRecord);
|
|
839
870
|
}
|
|
840
871
|
const parentRecord = {
|
|
841
872
|
tokenId: parentTokenId,
|
|
@@ -844,7 +875,7 @@ async function cascadeRevoke(parentTokenId, childTokenIds, options, store) {
|
|
|
844
875
|
reason: options.reason,
|
|
845
876
|
cascaded: childTokenIds
|
|
846
877
|
};
|
|
847
|
-
await
|
|
878
|
+
await store.add(parentRecord);
|
|
848
879
|
return parentRecord;
|
|
849
880
|
}
|
|
850
881
|
|
|
@@ -893,7 +924,12 @@ function verifyAttenuation(parent, child, currentDepth = 0) {
|
|
|
893
924
|
verifyConstraintsNotRelaxed(parentService, childService);
|
|
894
925
|
}
|
|
895
926
|
if (parentDef.rules && parentDef.rules.length > 0) {
|
|
896
|
-
verifyRulesNotRemoved(
|
|
927
|
+
verifyRulesNotRemoved(
|
|
928
|
+
parentDef.rules.map((r) => r.id),
|
|
929
|
+
child,
|
|
930
|
+
parentDef.services.flatMap((s) => s.scopes),
|
|
931
|
+
child.services.flatMap((s) => s.scopes)
|
|
932
|
+
);
|
|
897
933
|
}
|
|
898
934
|
}
|
|
899
935
|
function verifyScopeSubset(parent, child) {
|
|
@@ -915,26 +951,23 @@ function verifyConstraintsNotRelaxed(parent, child) {
|
|
|
915
951
|
for (const [key, parentValue] of Object.entries(parent.constraints)) {
|
|
916
952
|
if (parentValue === false) {
|
|
917
953
|
const childValue = child.constraints?.[key];
|
|
918
|
-
if (childValue ===
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
);
|
|
925
|
-
}
|
|
926
|
-
}
|
|
954
|
+
if (childValue === false) continue;
|
|
955
|
+
throw new AttenuationViolationError(
|
|
956
|
+
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)`,
|
|
957
|
+
parent.scopes,
|
|
958
|
+
child.scopes
|
|
959
|
+
);
|
|
927
960
|
}
|
|
928
961
|
}
|
|
929
962
|
}
|
|
930
|
-
function verifyRulesNotRemoved(parentRuleIds, child) {
|
|
963
|
+
function verifyRulesNotRemoved(parentRuleIds, child, parentScopes, childScopes) {
|
|
931
964
|
const childRuleIds = new Set(child.rules?.map((r) => r.id) ?? []);
|
|
932
965
|
for (const parentRuleId of parentRuleIds) {
|
|
933
966
|
if (!childRuleIds.has(parentRuleId)) {
|
|
934
967
|
throw new AttenuationViolationError(
|
|
935
968
|
`Child removes parent rule '${parentRuleId}'. Rules can only be added, not removed.`,
|
|
936
|
-
|
|
937
|
-
|
|
969
|
+
parentScopes,
|
|
970
|
+
childScopes
|
|
938
971
|
);
|
|
939
972
|
}
|
|
940
973
|
}
|
|
@@ -943,7 +976,6 @@ function verifyRulesNotRemoved(parentRuleIds, child) {
|
|
|
943
976
|
// src/delegation/chain.ts
|
|
944
977
|
async function delegate(parentToken, childDef, options) {
|
|
945
978
|
const currentDepth = countDepth(parentToken);
|
|
946
|
-
verifyAttenuation(parentToken, childDef, currentDepth);
|
|
947
979
|
const parentDef = parentToken.definition;
|
|
948
980
|
const parentRules = parentDef.rules ?? [];
|
|
949
981
|
const childExtraRules = (childDef.rules ?? []).filter(
|
|
@@ -955,12 +987,32 @@ async function delegate(parentToken, childDef, options) {
|
|
|
955
987
|
...childDef.metadata,
|
|
956
988
|
_delegationDepth: childDepth
|
|
957
989
|
};
|
|
990
|
+
const inheritedServices = childDef.services.map((childSvc) => {
|
|
991
|
+
const parentSvc = parentDef.services.find((s) => s.service === childSvc.service);
|
|
992
|
+
if (!parentSvc?.constraints) return childSvc;
|
|
993
|
+
const inherited = {};
|
|
994
|
+
for (const [key, value] of Object.entries(parentSvc.constraints)) {
|
|
995
|
+
if (value === false) {
|
|
996
|
+
inherited[key] = false;
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
if (Object.keys(inherited).length === 0) return childSvc;
|
|
1000
|
+
return {
|
|
1001
|
+
...childSvc,
|
|
1002
|
+
constraints: { ...inherited, ...childSvc.constraints }
|
|
1003
|
+
};
|
|
1004
|
+
});
|
|
1005
|
+
verifyAttenuation(
|
|
1006
|
+
parentToken,
|
|
1007
|
+
{ ...childDef, services: inheritedServices, rules: mergedRules },
|
|
1008
|
+
currentDepth
|
|
1009
|
+
);
|
|
958
1010
|
const fullDefinition = {
|
|
959
1011
|
principal: parentDef.principal,
|
|
960
1012
|
// Inherited — cannot be overridden
|
|
961
1013
|
agent: childDef.agent,
|
|
962
1014
|
agentProvider: parentDef.agentProvider,
|
|
963
|
-
services:
|
|
1015
|
+
services: inheritedServices,
|
|
964
1016
|
rules: mergedRules.length > 0 ? mergedRules : void 0,
|
|
965
1017
|
expires: childDef.expires ?? parentDef.expires,
|
|
966
1018
|
revocable: parentDef.revocable,
|
|
@@ -969,8 +1021,7 @@ async function delegate(parentToken, childDef, options) {
|
|
|
969
1021
|
metadata: childMetadata,
|
|
970
1022
|
legal: parentDef.legal
|
|
971
1023
|
};
|
|
972
|
-
const childToken = await createToken(fullDefinition, options);
|
|
973
|
-
childToken.parentToken = parentToken.jti;
|
|
1024
|
+
const childToken = await createToken(fullDefinition, options, parentToken.jti);
|
|
974
1025
|
return childToken;
|
|
975
1026
|
}
|
|
976
1027
|
function countDepth(token) {
|
|
@@ -1064,6 +1115,18 @@ function checkAttenuation(parent, child, index, errors) {
|
|
|
1064
1115
|
);
|
|
1065
1116
|
}
|
|
1066
1117
|
}
|
|
1118
|
+
if (parentService.constraints) {
|
|
1119
|
+
for (const [key, parentValue] of Object.entries(parentService.constraints)) {
|
|
1120
|
+
if (parentValue === false) {
|
|
1121
|
+
const childValue = childService.constraints?.[key];
|
|
1122
|
+
if (childValue !== false) {
|
|
1123
|
+
errors.push(
|
|
1124
|
+
`Chain link ${index}: constraint '${key}' on '${childService.service}' relaxed by child (parent: false, child: ${childValue === void 0 ? "undefined" : JSON.stringify(childValue)})`
|
|
1125
|
+
);
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1067
1130
|
}
|
|
1068
1131
|
const childExp = child.definition.expires instanceof Date ? child.definition.expires.getTime() : new Date(child.definition.expires).getTime();
|
|
1069
1132
|
const parentExp = parent.definition.expires instanceof Date ? parent.definition.expires.getTime() : new Date(parent.definition.expires).getTime();
|
|
@@ -1074,6 +1137,82 @@ function checkAttenuation(parent, child, index, errors) {
|
|
|
1074
1137
|
}
|
|
1075
1138
|
}
|
|
1076
1139
|
|
|
1140
|
+
// src/jwks/index.ts
|
|
1141
|
+
import * as jose3 from "jose";
|
|
1142
|
+
async function publicKeyToJWK(publicKey, options) {
|
|
1143
|
+
const exported = await jose3.exportJWK(publicKey);
|
|
1144
|
+
const alg = options.alg ?? defaultAlgorithm(exported);
|
|
1145
|
+
return {
|
|
1146
|
+
...exported,
|
|
1147
|
+
kid: options.kid,
|
|
1148
|
+
use: options.use ?? "sig",
|
|
1149
|
+
alg
|
|
1150
|
+
};
|
|
1151
|
+
}
|
|
1152
|
+
function buildJWKS(keys) {
|
|
1153
|
+
return { keys };
|
|
1154
|
+
}
|
|
1155
|
+
function createJWKSResolver(url, options = {}) {
|
|
1156
|
+
if (!options.fetch && !options.allowInsecure && !url.startsWith("https://")) {
|
|
1157
|
+
throw new Error(
|
|
1158
|
+
`JWKS URL must use https:// (got: ${JSON.stringify(url)}). Pass allowInsecure: true or a custom fetch for local development.`
|
|
1159
|
+
);
|
|
1160
|
+
}
|
|
1161
|
+
const cacheMaxAgeMs = options.cacheMaxAgeMs ?? 60 * 60 * 1e3;
|
|
1162
|
+
const cooldownMs = options.cooldownMs ?? 24 * 60 * 60 * 1e3;
|
|
1163
|
+
const fetchImpl = options.fetch ?? fetch;
|
|
1164
|
+
let cache = null;
|
|
1165
|
+
let inflight = null;
|
|
1166
|
+
async function fetchJWKS() {
|
|
1167
|
+
const response = await fetchImpl(url, {
|
|
1168
|
+
headers: { accept: "application/jwk-set+json, application/json" }
|
|
1169
|
+
});
|
|
1170
|
+
if (!response.ok) {
|
|
1171
|
+
throw new Error(`JWKS fetch failed: ${response.status} ${response.statusText}`);
|
|
1172
|
+
}
|
|
1173
|
+
const body = await response.json();
|
|
1174
|
+
if (!body || !Array.isArray(body.keys)) {
|
|
1175
|
+
throw new Error("JWKS response did not contain a `keys` array");
|
|
1176
|
+
}
|
|
1177
|
+
return body;
|
|
1178
|
+
}
|
|
1179
|
+
async function getJWKS() {
|
|
1180
|
+
const now = Date.now();
|
|
1181
|
+
if (cache && now - cache.fetchedAt < cacheMaxAgeMs) {
|
|
1182
|
+
return cache.jwks;
|
|
1183
|
+
}
|
|
1184
|
+
if (inflight) {
|
|
1185
|
+
return inflight;
|
|
1186
|
+
}
|
|
1187
|
+
inflight = fetchJWKS().then((jwks) => {
|
|
1188
|
+
cache = { fetchedAt: Date.now(), jwks };
|
|
1189
|
+
return jwks;
|
|
1190
|
+
}).catch((err) => {
|
|
1191
|
+
if (cache && now - cache.fetchedAt < cooldownMs) {
|
|
1192
|
+
return cache.jwks;
|
|
1193
|
+
}
|
|
1194
|
+
throw err;
|
|
1195
|
+
}).finally(() => {
|
|
1196
|
+
inflight = null;
|
|
1197
|
+
});
|
|
1198
|
+
return inflight;
|
|
1199
|
+
}
|
|
1200
|
+
return {
|
|
1201
|
+
async resolve(kid) {
|
|
1202
|
+
const jwks = await getJWKS();
|
|
1203
|
+
const match = jwks.keys.find((k) => k.kid === kid);
|
|
1204
|
+
if (!match) return null;
|
|
1205
|
+
const key = await jose3.importJWK(match, match.alg);
|
|
1206
|
+
return key;
|
|
1207
|
+
}
|
|
1208
|
+
};
|
|
1209
|
+
}
|
|
1210
|
+
function defaultAlgorithm(jwk) {
|
|
1211
|
+
if (jwk.kty === "OKP" && jwk.crv === "Ed25519") return "EdDSA";
|
|
1212
|
+
if (jwk.kty === "EC" && jwk.crv === "P-256") return "ES256";
|
|
1213
|
+
return jwk.alg ?? "EdDSA";
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1077
1216
|
// src/client.ts
|
|
1078
1217
|
function createClient(options) {
|
|
1079
1218
|
const revocationStore = options?.revocationStore ?? new MemoryRevocationStore();
|
|
@@ -1156,10 +1295,12 @@ export {
|
|
|
1156
1295
|
ScopeViolationError,
|
|
1157
1296
|
TokenExpiredError,
|
|
1158
1297
|
authorize,
|
|
1298
|
+
buildJWKS,
|
|
1159
1299
|
cascadeRevoke,
|
|
1160
1300
|
checkConstraint,
|
|
1161
1301
|
checkScope,
|
|
1162
1302
|
createClient,
|
|
1303
|
+
createJWKSResolver,
|
|
1163
1304
|
createToken,
|
|
1164
1305
|
decodeHeader,
|
|
1165
1306
|
delegate,
|
|
@@ -1173,6 +1314,7 @@ export {
|
|
|
1173
1314
|
matchScope,
|
|
1174
1315
|
parseDefinition,
|
|
1175
1316
|
parseScope,
|
|
1317
|
+
publicKeyToJWK,
|
|
1176
1318
|
revoke,
|
|
1177
1319
|
sign,
|
|
1178
1320
|
signToken,
|