life-as-api 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/LICENSE +21 -0
- package/README.md +377 -0
- package/dist/agent/finance-agent.d.ts +44 -0
- package/dist/agent/finance-agent.js +75 -0
- package/dist/agent/finance-agent.js.map +1 -0
- package/dist/agent/legal-agent.d.ts +44 -0
- package/dist/agent/legal-agent.js +76 -0
- package/dist/agent/legal-agent.js.map +1 -0
- package/dist/agent/medical-agent.d.ts +44 -0
- package/dist/agent/medical-agent.js +75 -0
- package/dist/agent/medical-agent.js.map +1 -0
- package/dist/agent/trusted-agent-registry.d.ts +53 -0
- package/dist/agent/trusted-agent-registry.js +57 -0
- package/dist/agent/trusted-agent-registry.js.map +1 -0
- package/dist/audit/audit-chain-verifier.d.ts +10 -0
- package/dist/audit/audit-chain-verifier.js +67 -0
- package/dist/audit/audit-chain-verifier.js.map +1 -0
- package/dist/audit/audit-log.d.ts +48 -0
- package/dist/audit/audit-log.js +74 -0
- package/dist/audit/audit-log.js.map +1 -0
- package/dist/audit/sqlite-audit-log.d.ts +29 -0
- package/dist/audit/sqlite-audit-log.js +142 -0
- package/dist/audit/sqlite-audit-log.js.map +1 -0
- package/dist/auth/access-context-issuer.d.ts +18 -0
- package/dist/auth/access-context-issuer.js +88 -0
- package/dist/auth/access-context-issuer.js.map +1 -0
- package/dist/auth/assert-authorized-for-subject.d.ts +2 -0
- package/dist/auth/assert-authorized-for-subject.js +20 -0
- package/dist/auth/assert-authorized-for-subject.js.map +1 -0
- package/dist/auth/backend-context-minting-boundary.d.ts +23 -0
- package/dist/auth/backend-context-minting-boundary.js +281 -0
- package/dist/auth/backend-context-minting-boundary.js.map +1 -0
- package/dist/auth/default-authorization-policy.d.ts +7 -0
- package/dist/auth/default-authorization-policy.js +64 -0
- package/dist/auth/default-authorization-policy.js.map +1 -0
- package/dist/auth/fake-auth-adapter.d.ts +7 -0
- package/dist/auth/fake-auth-adapter.js +27 -0
- package/dist/auth/fake-auth-adapter.js.map +1 -0
- package/dist/auth/http-client.d.ts +12 -0
- package/dist/auth/http-client.js +7 -0
- package/dist/auth/http-client.js.map +1 -0
- package/dist/auth/in-memory-grant-resolver.d.ts +21 -0
- package/dist/auth/in-memory-grant-resolver.js +73 -0
- package/dist/auth/in-memory-grant-resolver.js.map +1 -0
- package/dist/auth/in-memory-tenant-boundary.d.ts +16 -0
- package/dist/auth/in-memory-tenant-boundary.js +37 -0
- package/dist/auth/in-memory-tenant-boundary.js.map +1 -0
- package/dist/auth/jwks-key-ring-auth-adapter.d.ts +38 -0
- package/dist/auth/jwks-key-ring-auth-adapter.js +123 -0
- package/dist/auth/jwks-key-ring-auth-adapter.js.map +1 -0
- package/dist/auth/jwks-key-ring.d.ts +25 -0
- package/dist/auth/jwks-key-ring.js +111 -0
- package/dist/auth/jwks-key-ring.js.map +1 -0
- package/dist/auth/jwt-auth-adapter.d.ts +15 -0
- package/dist/auth/jwt-auth-adapter.js +59 -0
- package/dist/auth/jwt-auth-adapter.js.map +1 -0
- package/dist/auth/jwt-core.d.ts +32 -0
- package/dist/auth/jwt-core.js +226 -0
- package/dist/auth/jwt-core.js.map +1 -0
- package/dist/auth/jwt-key-ring-auth-adapter.d.ts +16 -0
- package/dist/auth/jwt-key-ring-auth-adapter.js +58 -0
- package/dist/auth/jwt-key-ring-auth-adapter.js.map +1 -0
- package/dist/auth/oidc-discovery.d.ts +15 -0
- package/dist/auth/oidc-discovery.js +46 -0
- package/dist/auth/oidc-discovery.js.map +1 -0
- package/dist/auth/production-scoped-life-api.d.ts +58 -0
- package/dist/auth/production-scoped-life-api.js +802 -0
- package/dist/auth/production-scoped-life-api.js.map +1 -0
- package/dist/auth/proposal-scope-inspector.d.ts +3 -0
- package/dist/auth/proposal-scope-inspector.js +42 -0
- package/dist/auth/proposal-scope-inspector.js.map +1 -0
- package/dist/auth/proposal-scoped-life-api.d.ts +21 -0
- package/dist/auth/proposal-scoped-life-api.js +168 -0
- package/dist/auth/proposal-scoped-life-api.js.map +1 -0
- package/dist/auth/rate-limited-context-minting-boundary.d.ts +27 -0
- package/dist/auth/rate-limited-context-minting-boundary.js +77 -0
- package/dist/auth/rate-limited-context-minting-boundary.js.map +1 -0
- package/dist/auth/revocation-aware-auth-adapter.d.ts +21 -0
- package/dist/auth/revocation-aware-auth-adapter.js +88 -0
- package/dist/auth/revocation-aware-auth-adapter.js.map +1 -0
- package/dist/auth/sqlite-grant-resolver.d.ts +8 -0
- package/dist/auth/sqlite-grant-resolver.js +78 -0
- package/dist/auth/sqlite-grant-resolver.js.map +1 -0
- package/dist/auth/sqlite-tenant-boundary.d.ts +7 -0
- package/dist/auth/sqlite-tenant-boundary.js +41 -0
- package/dist/auth/sqlite-tenant-boundary.js.map +1 -0
- package/dist/auth/static-jwt-key-ring.d.ts +13 -0
- package/dist/auth/static-jwt-key-ring.js +73 -0
- package/dist/auth/static-jwt-key-ring.js.map +1 -0
- package/dist/auth/subject-scoped-life-api.d.ts +14 -0
- package/dist/auth/subject-scoped-life-api.js +51 -0
- package/dist/auth/subject-scoped-life-api.js.map +1 -0
- package/dist/auth/test-context-minting-boundary.d.ts +13 -0
- package/dist/auth/test-context-minting-boundary.js +74 -0
- package/dist/auth/test-context-minting-boundary.js.map +1 -0
- package/dist/auth/types.d.ts +77 -0
- package/dist/auth/types.js +16 -0
- package/dist/auth/types.js.map +1 -0
- package/dist/compliance/subject-data-report.d.ts +43 -0
- package/dist/compliance/subject-data-report.js +155 -0
- package/dist/compliance/subject-data-report.js.map +1 -0
- package/dist/consent/consent-guard.d.ts +4 -0
- package/dist/consent/consent-guard.js +37 -0
- package/dist/consent/consent-guard.js.map +1 -0
- package/dist/consent/grant-registry.d.ts +7 -0
- package/dist/consent/grant-registry.js +13 -0
- package/dist/consent/grant-registry.js.map +1 -0
- package/dist/consent/intersect-selector.d.ts +3 -0
- package/dist/consent/intersect-selector.js +66 -0
- package/dist/consent/intersect-selector.js.map +1 -0
- package/dist/contract/diagnostics.d.ts +32 -0
- package/dist/contract/diagnostics.js +112 -0
- package/dist/contract/diagnostics.js.map +1 -0
- package/dist/contract/project-contracts.d.ts +5 -0
- package/dist/contract/project-contracts.js +159 -0
- package/dist/contract/project-contracts.js.map +1 -0
- package/dist/contract/types.d.ts +32 -0
- package/dist/contract/types.js +5 -0
- package/dist/contract/types.js.map +1 -0
- package/dist/crypto/aes-gcm.d.ts +11 -0
- package/dist/crypto/aes-gcm.js +78 -0
- package/dist/crypto/aes-gcm.js.map +1 -0
- package/dist/crypto/claim-serializer.d.ts +26 -0
- package/dist/crypto/claim-serializer.js +59 -0
- package/dist/crypto/claim-serializer.js.map +1 -0
- package/dist/crypto/sha256.d.ts +12 -0
- package/dist/crypto/sha256.js +40 -0
- package/dist/crypto/sha256.js.map +1 -0
- package/dist/crypto/static-key.d.ts +11 -0
- package/dist/crypto/static-key.js +60 -0
- package/dist/crypto/static-key.js.map +1 -0
- package/dist/crypto/types.d.ts +20 -0
- package/dist/crypto/types.js +8 -0
- package/dist/crypto/types.js.map +1 -0
- package/dist/datasource/trusted-data-source-registry.d.ts +51 -0
- package/dist/datasource/trusted-data-source-registry.js +57 -0
- package/dist/datasource/trusted-data-source-registry.js.map +1 -0
- package/dist/erasure/tombstone.d.ts +13 -0
- package/dist/erasure/tombstone.js +20 -0
- package/dist/erasure/tombstone.js.map +1 -0
- package/dist/evidence/evidence-backed-claim-creator.d.ts +48 -0
- package/dist/evidence/evidence-backed-claim-creator.js +107 -0
- package/dist/evidence/evidence-backed-claim-creator.js.map +1 -0
- package/dist/evidence/in-memory-raw-document-store.d.ts +26 -0
- package/dist/evidence/in-memory-raw-document-store.js +84 -0
- package/dist/evidence/in-memory-raw-document-store.js.map +1 -0
- package/dist/evidence/raw-document-store.d.ts +12 -0
- package/dist/evidence/raw-document-store.js +8 -0
- package/dist/evidence/raw-document-store.js.map +1 -0
- package/dist/evidence/source-evidence-validator.d.ts +4 -0
- package/dist/evidence/source-evidence-validator.js +14 -0
- package/dist/evidence/source-evidence-validator.js.map +1 -0
- package/dist/evidence/sqlite-raw-document-store.d.ts +19 -0
- package/dist/evidence/sqlite-raw-document-store.js +122 -0
- package/dist/evidence/sqlite-raw-document-store.js.map +1 -0
- package/dist/guardian/guardian-registry.d.ts +96 -0
- package/dist/guardian/guardian-registry.js +95 -0
- package/dist/guardian/guardian-registry.js.map +1 -0
- package/dist/index.d.ts +44 -0
- package/dist/index.js +72 -0
- package/dist/index.js.map +1 -0
- package/dist/observability/health.d.ts +12 -0
- package/dist/observability/health.js +16 -0
- package/dist/observability/health.js.map +1 -0
- package/dist/observability/logger.d.ts +21 -0
- package/dist/observability/logger.js +40 -0
- package/dist/observability/logger.js.map +1 -0
- package/dist/observability/metrics.d.ts +15 -0
- package/dist/observability/metrics.js +52 -0
- package/dist/observability/metrics.js.map +1 -0
- package/dist/persistence/backup.d.ts +24 -0
- package/dist/persistence/backup.js +251 -0
- package/dist/persistence/backup.js.map +1 -0
- package/dist/persistence/decryptability-scan.d.ts +10 -0
- package/dist/persistence/decryptability-scan.js +144 -0
- package/dist/persistence/decryptability-scan.js.map +1 -0
- package/dist/persistence/in-memory-unit-of-work.d.ts +11 -0
- package/dist/persistence/in-memory-unit-of-work.js +32 -0
- package/dist/persistence/in-memory-unit-of-work.js.map +1 -0
- package/dist/persistence/key-availability.d.ts +9 -0
- package/dist/persistence/key-availability.js +125 -0
- package/dist/persistence/key-availability.js.map +1 -0
- package/dist/persistence/sqlite/migrations.d.ts +9 -0
- package/dist/persistence/sqlite/migrations.js +535 -0
- package/dist/persistence/sqlite/migrations.js.map +1 -0
- package/dist/persistence/sqlite/schema.d.ts +10 -0
- package/dist/persistence/sqlite/schema.js +101 -0
- package/dist/persistence/sqlite/schema.js.map +1 -0
- package/dist/persistence/sqlite/sqlite-fact-store.d.ts +24 -0
- package/dist/persistence/sqlite/sqlite-fact-store.js +207 -0
- package/dist/persistence/sqlite/sqlite-fact-store.js.map +1 -0
- package/dist/persistence/sqlite/sqlite-source-registry.d.ts +17 -0
- package/dist/persistence/sqlite/sqlite-source-registry.js +132 -0
- package/dist/persistence/sqlite/sqlite-source-registry.js.map +1 -0
- package/dist/persistence/sqlite/sqlite-unit-of-work.d.ts +8 -0
- package/dist/persistence/sqlite/sqlite-unit-of-work.js +41 -0
- package/dist/persistence/sqlite/sqlite-unit-of-work.js.map +1 -0
- package/dist/persistence/sqlite/types.d.ts +24 -0
- package/dist/persistence/sqlite/types.js +28 -0
- package/dist/persistence/sqlite/types.js.map +1 -0
- package/dist/persistence/unit-of-work.d.ts +6 -0
- package/dist/persistence/unit-of-work.js +15 -0
- package/dist/persistence/unit-of-work.js.map +1 -0
- package/dist/proposal/accepted-claim-appender.d.ts +7 -0
- package/dist/proposal/accepted-claim-appender.js +16 -0
- package/dist/proposal/accepted-claim-appender.js.map +1 -0
- package/dist/proposal/in-memory-proposal-queue.d.ts +21 -0
- package/dist/proposal/in-memory-proposal-queue.js +218 -0
- package/dist/proposal/in-memory-proposal-queue.js.map +1 -0
- package/dist/proposal/policy-aware-proposal-queue.d.ts +23 -0
- package/dist/proposal/policy-aware-proposal-queue.js +162 -0
- package/dist/proposal/policy-aware-proposal-queue.js.map +1 -0
- package/dist/proposal/proposal-policy.d.ts +37 -0
- package/dist/proposal/proposal-policy.js +72 -0
- package/dist/proposal/proposal-policy.js.map +1 -0
- package/dist/proposal/proposal-queue.d.ts +13 -0
- package/dist/proposal/proposal-queue.js +15 -0
- package/dist/proposal/proposal-queue.js.map +1 -0
- package/dist/proposal/source-aware-claim-appender.d.ts +13 -0
- package/dist/proposal/source-aware-claim-appender.js +47 -0
- package/dist/proposal/source-aware-claim-appender.js.map +1 -0
- package/dist/proposal/sqlite-proposal-queue.d.ts +26 -0
- package/dist/proposal/sqlite-proposal-queue.js +282 -0
- package/dist/proposal/sqlite-proposal-queue.js.map +1 -0
- package/dist/resolution/resolve-claims.d.ts +4 -0
- package/dist/resolution/resolve-claims.js +101 -0
- package/dist/resolution/resolve-claims.js.map +1 -0
- package/dist/resolution/trust-profile.d.ts +12 -0
- package/dist/resolution/trust-profile.js +23 -0
- package/dist/resolution/trust-profile.js.map +1 -0
- package/dist/resolution/types.d.ts +15 -0
- package/dist/resolution/types.js +8 -0
- package/dist/resolution/types.js.map +1 -0
- package/dist/sdk/create-backend-integrated-life-api-for-test.d.ts +23 -0
- package/dist/sdk/create-backend-integrated-life-api-for-test.js +240 -0
- package/dist/sdk/create-backend-integrated-life-api-for-test.js.map +1 -0
- package/dist/sdk/create-life-api.d.ts +55 -0
- package/dist/sdk/create-life-api.js +251 -0
- package/dist/sdk/create-life-api.js.map +1 -0
- package/dist/sdk/create-sqlite-life-api.d.ts +33 -0
- package/dist/sdk/create-sqlite-life-api.js +654 -0
- package/dist/sdk/create-sqlite-life-api.js.map +1 -0
- package/dist/sdk/life-api.d.ts +27 -0
- package/dist/sdk/life-api.js +17 -0
- package/dist/sdk/life-api.js.map +1 -0
- package/dist/sdk/validation.d.ts +8 -0
- package/dist/sdk/validation.js +216 -0
- package/dist/sdk/validation.js.map +1 -0
- package/dist/source/in-memory-source-registry.d.ts +15 -0
- package/dist/source/in-memory-source-registry.js +104 -0
- package/dist/source/in-memory-source-registry.js.map +1 -0
- package/dist/source/source-aware-fact-store.d.ts +19 -0
- package/dist/source/source-aware-fact-store.js +40 -0
- package/dist/source/source-aware-fact-store.js.map +1 -0
- package/dist/source/source-kind-predicate-policy.d.ts +4 -0
- package/dist/source/source-kind-predicate-policy.js +62 -0
- package/dist/source/source-kind-predicate-policy.js.map +1 -0
- package/dist/source/source-registry.d.ts +11 -0
- package/dist/source/source-registry.js +12 -0
- package/dist/source/source-registry.js.map +1 -0
- package/dist/store/in-memory-fact-store.d.ts +25 -0
- package/dist/store/in-memory-fact-store.js +192 -0
- package/dist/store/in-memory-fact-store.js.map +1 -0
- package/dist/surface/context-query.d.ts +21 -0
- package/dist/surface/context-query.js +113 -0
- package/dist/surface/context-query.js.map +1 -0
- package/dist/surface/evidence-backed-context-query.d.ts +62 -0
- package/dist/surface/evidence-backed-context-query.js +240 -0
- package/dist/surface/evidence-backed-context-query.js.map +1 -0
- package/dist/types/access-context.d.ts +25 -0
- package/dist/types/access-context.js +6 -0
- package/dist/types/access-context.js.map +1 -0
- package/dist/types/claim.d.ts +48 -0
- package/dist/types/claim.js +5 -0
- package/dist/types/claim.js.map +1 -0
- package/dist/types/consent.d.ts +41 -0
- package/dist/types/consent.js +3 -0
- package/dist/types/consent.js.map +1 -0
- package/dist/types/crypto-boundary.d.ts +12 -0
- package/dist/types/crypto-boundary.js +15 -0
- package/dist/types/crypto-boundary.js.map +1 -0
- package/dist/types/evidence.d.ts +31 -0
- package/dist/types/evidence.js +8 -0
- package/dist/types/evidence.js.map +1 -0
- package/dist/types/fact-store.d.ts +24 -0
- package/dist/types/fact-store.js +12 -0
- package/dist/types/fact-store.js.map +1 -0
- package/dist/types/ids.d.ts +36 -0
- package/dist/types/ids.js +11 -0
- package/dist/types/ids.js.map +1 -0
- package/dist/types/proposal.d.ts +38 -0
- package/dist/types/proposal.js +7 -0
- package/dist/types/proposal.js.map +1 -0
- package/dist/types/source.d.ts +33 -0
- package/dist/types/source.js +6 -0
- package/dist/types/source.js.map +1 -0
- package/dist/types/trust.d.ts +24 -0
- package/dist/types/trust.js +5 -0
- package/dist/types/trust.js.map +1 -0
- package/dist/utilities/deep-freeze.d.ts +1 -0
- package/dist/utilities/deep-freeze.js +17 -0
- package/dist/utilities/deep-freeze.js.map +1 -0
- package/package.json +55 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Clock, GrantDescriptor, GrantResolver } from "./types.js";
|
|
2
|
+
/** Grant record for InMemoryGrantResolver. */
|
|
3
|
+
export interface GrantRecord {
|
|
4
|
+
readonly grantId: string;
|
|
5
|
+
readonly agentId: string;
|
|
6
|
+
readonly tenantId: string;
|
|
7
|
+
readonly notBefore: string;
|
|
8
|
+
readonly notAfter: string;
|
|
9
|
+
readonly status: "active" | "revoked";
|
|
10
|
+
readonly personId?: string;
|
|
11
|
+
}
|
|
12
|
+
/** @remarks TEST/DEV ONLY — not for production use. */
|
|
13
|
+
export declare class InMemoryGrantResolver implements GrantResolver {
|
|
14
|
+
private readonly _grants;
|
|
15
|
+
private readonly _clock;
|
|
16
|
+
private readonly _failWith;
|
|
17
|
+
constructor(grants: ReadonlyArray<GrantRecord>, clock: Clock, opts?: {
|
|
18
|
+
readonly failWith?: Error;
|
|
19
|
+
});
|
|
20
|
+
resolveActiveGrant(agentId: string, grantId: string, tenantId: string): Promise<GrantDescriptor | undefined>;
|
|
21
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// TEST/DEV ONLY — InMemoryGrantResolver (Production-02a).
|
|
2
|
+
// Do NOT export from src/index.ts. Do NOT use in production.
|
|
3
|
+
//
|
|
4
|
+
// AUTH_GRANT_ACTIVE_CHECKED_001: resolveActiveGrant fires before every AgentConsentContext mint.
|
|
5
|
+
// AUTH_GRANT_REVOKED_DENIED_001: revoked grants return undefined — never active.
|
|
6
|
+
// AUTH_GRANT_EXPIRED_DENIED_001: grants where clock.now() >= notAfter return undefined.
|
|
7
|
+
// AUTH_GRANT_TENANT_SCOPE_ENFORCED_001: grant tenantId must match requesting tenantId.
|
|
8
|
+
// AUTH_GRANT_AGENT_SCOPE_ENFORCED_001: grant agentId must match requesting agentId.
|
|
9
|
+
//
|
|
10
|
+
// All denial cases return undefined (no existence leak):
|
|
11
|
+
// missing, revoked, expired, not-yet-active, tenant mismatch, agent mismatch → undefined
|
|
12
|
+
//
|
|
13
|
+
// Storage-failure behavior: if constructed with failWith, resolveActiveGrant throws that error
|
|
14
|
+
// — storage errors must never be silently converted to undefined.
|
|
15
|
+
// A real backend storage failure is not the same as "no active grant."
|
|
16
|
+
import { asPersonId } from "../types/ids.js";
|
|
17
|
+
/** @remarks TEST/DEV ONLY — not for production use. */
|
|
18
|
+
export class InMemoryGrantResolver {
|
|
19
|
+
_grants;
|
|
20
|
+
_clock;
|
|
21
|
+
_failWith;
|
|
22
|
+
constructor(grants, clock, opts) {
|
|
23
|
+
const map = new Map();
|
|
24
|
+
for (const g of grants) {
|
|
25
|
+
map.set(g.grantId, g);
|
|
26
|
+
}
|
|
27
|
+
this._grants = map;
|
|
28
|
+
this._clock = clock;
|
|
29
|
+
this._failWith = opts?.failWith;
|
|
30
|
+
}
|
|
31
|
+
async resolveActiveGrant(agentId, grantId, tenantId) {
|
|
32
|
+
// Storage-failure simulation — must throw, not return undefined
|
|
33
|
+
if (this._failWith !== undefined) {
|
|
34
|
+
throw this._failWith;
|
|
35
|
+
}
|
|
36
|
+
const record = this._grants.get(grantId);
|
|
37
|
+
// Missing grant — return undefined (no existence leak)
|
|
38
|
+
if (record === undefined) {
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
// AUTH_GRANT_AGENT_SCOPE_ENFORCED_001: agentId must match
|
|
42
|
+
if (record.agentId !== agentId) {
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
// AUTH_GRANT_TENANT_SCOPE_ENFORCED_001: tenantId must match
|
|
46
|
+
if (record.tenantId !== tenantId) {
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
// AUTH_GRANT_REVOKED_DENIED_001: revoked grants are permanently denied
|
|
50
|
+
if (record.status === "revoked") {
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
const now = this._clock.now();
|
|
54
|
+
// Not-yet-active: before notBefore
|
|
55
|
+
if (now < record.notBefore) {
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
// AUTH_GRANT_EXPIRED_DENIED_001: expired at or after notAfter
|
|
59
|
+
if (now >= record.notAfter) {
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
// AUTH_GRANT_ACTIVE_CHECKED_001: all checks passed — return descriptor
|
|
63
|
+
return {
|
|
64
|
+
grantId: record.grantId,
|
|
65
|
+
agentId: record.agentId,
|
|
66
|
+
tenantId: record.tenantId,
|
|
67
|
+
notAfter: record.notAfter,
|
|
68
|
+
// exactOptionalPropertyTypes: omit the property entirely when absent.
|
|
69
|
+
...(record.personId !== undefined ? { personId: asPersonId(record.personId) } : {}),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
//# sourceMappingURL=in-memory-grant-resolver.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"in-memory-grant-resolver.js","sourceRoot":"","sources":["../../src/auth/in-memory-grant-resolver.ts"],"names":[],"mappings":"AAAA,0DAA0D;AAC1D,6DAA6D;AAC7D,EAAE;AACF,iGAAiG;AACjG,iFAAiF;AACjF,wFAAwF;AACxF,uFAAuF;AACvF,oFAAoF;AACpF,EAAE;AACF,yDAAyD;AACzD,2FAA2F;AAC3F,EAAE;AACF,+FAA+F;AAC/F,oEAAoE;AACpE,yEAAyE;AAGzE,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAa7C,uDAAuD;AACvD,MAAM,OAAO,qBAAqB;IACf,OAAO,CAAqC;IAC5C,MAAM,CAAW;IACjB,SAAS,CAAoB;IAE9C,YACE,MAAkC,EAClC,KAAa,EACb,IAAqC;QAErC,MAAM,GAAG,GAAG,IAAI,GAAG,EAAuB,CAAC;QAC3C,KAAK,MAAM,CAAC,IAAI,MAAM,EAAE,CAAC;YACvB,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC;QACxB,CAAC;QACD,IAAI,CAAC,OAAO,GAAK,GAAG,CAAC;QACrB,IAAI,CAAC,MAAM,GAAM,KAAK,CAAC;QACvB,IAAI,CAAC,SAAS,GAAG,IAAI,EAAE,QAAQ,CAAC;IAClC,CAAC;IAED,KAAK,CAAC,kBAAkB,CACtB,OAAgB,EAChB,OAAgB,EAChB,QAAgB;QAEhB,gEAAgE;QAChE,IAAI,IAAI,CAAC,SAAS,KAAK,SAAS,EAAE,CAAC;YACjC,MAAM,IAAI,CAAC,SAAS,CAAC;QACvB,CAAC;QAED,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QAEzC,uDAAuD;QACvD,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;YACzB,OAAO,SAAS,CAAC;QACnB,CAAC;QAED,0DAA0D;QAC1D,IAAI,MAAM,CAAC,OAAO,KAAK,OAAO,EAAE,CAAC;YAC/B,OAAO,SAAS,CAAC;QACnB,CAAC;QAED,4DAA4D;QAC5D,IAAI,MAAM,CAAC,QAAQ,KAAK,QAAQ,EAAE,CAAC;YACjC,OAAO,SAAS,CAAC;QACnB,CAAC;QAED,uEAAuE;QACvE,IAAI,MAAM,CAAC,MAAM,KAAK,SAAS,EAAE,CAAC;YAChC,OAAO,SAAS,CAAC;QACnB,CAAC;QAED,MAAM,GAAG,GAAG,IAAI,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC;QAE9B,mCAAmC;QACnC,IAAI,GAAG,GAAG,MAAM,CAAC,SAAS,EAAE,CAAC;YAC3B,OAAO,SAAS,CAAC;QACnB,CAAC;QAED,8DAA8D;QAC9D,IAAI,GAAG,IAAI,MAAM,CAAC,QAAQ,EAAE,CAAC;YAC3B,OAAO,SAAS,CAAC;QACnB,CAAC;QAED,uEAAuE;QACvE,OAAO;YACL,OAAO,EAAG,MAAM,CAAC,OAAO;YACxB,OAAO,EAAG,MAAM,CAAC,OAAO;YACxB,QAAQ,EAAE,MAAM,CAAC,QAAQ;YACzB,QAAQ,EAAE,MAAM,CAAC,QAAQ;YACzB,sEAAsE;YACtE,GAAG,CAAC,MAAM,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,UAAU,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SACpF,CAAC;IACJ,CAAC;CACF"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { TenantBoundary } from "./types.js";
|
|
2
|
+
/** Membership record for InMemoryTenantBoundary. */
|
|
3
|
+
export interface TenantMembership {
|
|
4
|
+
readonly tenantId: string;
|
|
5
|
+
readonly personId: string;
|
|
6
|
+
readonly status: "active" | "suspended";
|
|
7
|
+
}
|
|
8
|
+
/** @remarks TEST/DEV ONLY — not for production use. */
|
|
9
|
+
export declare class InMemoryTenantBoundary implements TenantBoundary {
|
|
10
|
+
private readonly _memberships;
|
|
11
|
+
private readonly _failWith;
|
|
12
|
+
constructor(memberships: ReadonlyArray<TenantMembership>, opts?: {
|
|
13
|
+
readonly failWith?: Error;
|
|
14
|
+
});
|
|
15
|
+
assertOwnership(tenantId: string, personId: string): Promise<void>;
|
|
16
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// TEST/DEV ONLY — InMemoryTenantBoundary (Production-02a).
|
|
2
|
+
// Do NOT export from src/index.ts. Do NOT use in production.
|
|
3
|
+
//
|
|
4
|
+
// AUTH_TENANT_OWNERSHIP_CHECKED_001: assertOwnership fires before every OwnerContext mint.
|
|
5
|
+
// AUTH_TENANT_OWNERSHIP_DENIED_001: thrown for missing, suspended, or tenant-mismatch.
|
|
6
|
+
// AUTH_TENANT_SUBJECT_NOT_FOUND_SAFE_001: missing and suspended produce identical error
|
|
7
|
+
// — caller cannot distinguish non-existence from suspension (no enumeration leak).
|
|
8
|
+
//
|
|
9
|
+
// Storage-failure behavior: if constructed with failWith, assertOwnership throws that error
|
|
10
|
+
// rather than returning or falling through — storage errors must not masquerade as denials.
|
|
11
|
+
/** @remarks TEST/DEV ONLY — not for production use. */
|
|
12
|
+
export class InMemoryTenantBoundary {
|
|
13
|
+
_memberships;
|
|
14
|
+
_failWith;
|
|
15
|
+
constructor(memberships, opts) {
|
|
16
|
+
const map = new Map();
|
|
17
|
+
for (const m of memberships) {
|
|
18
|
+
map.set(`${m.tenantId}:${m.personId}`, m);
|
|
19
|
+
}
|
|
20
|
+
this._memberships = map;
|
|
21
|
+
this._failWith = opts?.failWith;
|
|
22
|
+
}
|
|
23
|
+
async assertOwnership(tenantId, personId) {
|
|
24
|
+
// AUTH_TENANT_OWNERSHIP_CHECKED_001: always fires; never falls through on error
|
|
25
|
+
if (this._failWith !== undefined) {
|
|
26
|
+
throw this._failWith;
|
|
27
|
+
}
|
|
28
|
+
const record = this._memberships.get(`${tenantId}:${personId}`);
|
|
29
|
+
// AUTH_TENANT_SUBJECT_NOT_FOUND_SAFE_001:
|
|
30
|
+
// Missing and suspended produce the same code — no enumeration leak.
|
|
31
|
+
if (record === undefined || record.status !== "active") {
|
|
32
|
+
throw Object.assign(new Error("AUTH_TENANT_OWNERSHIP_DENIED_001: subject is not an active member of the tenant"), { code: "AUTH_TENANT_OWNERSHIP_DENIED_001" });
|
|
33
|
+
}
|
|
34
|
+
// AUTH_TENANT_OWNERSHIP_CHECKED_001: resolves void on success
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=in-memory-tenant-boundary.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"in-memory-tenant-boundary.js","sourceRoot":"","sources":["../../src/auth/in-memory-tenant-boundary.ts"],"names":[],"mappings":"AAAA,2DAA2D;AAC3D,6DAA6D;AAC7D,EAAE;AACF,2FAA2F;AAC3F,uFAAuF;AACvF,wFAAwF;AACxF,qFAAqF;AACrF,EAAE;AACF,4FAA4F;AAC5F,8FAA8F;AAW9F,uDAAuD;AACvD,MAAM,OAAO,sBAAsB;IAChB,YAAY,CAAwC;IACpD,SAAS,CAAuB;IAEjD,YACE,WAA4C,EAC5C,IAAoC;QAEpC,MAAM,GAAG,GAAG,IAAI,GAAG,EAA4B,CAAC;QAChD,KAAK,MAAM,CAAC,IAAI,WAAW,EAAE,CAAC;YAC5B,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,QAAQ,IAAI,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC,CAAC;QAC5C,CAAC;QACD,IAAI,CAAC,YAAY,GAAG,GAAG,CAAC;QACxB,IAAI,CAAC,SAAS,GAAO,IAAI,EAAE,QAAQ,CAAC;IACtC,CAAC;IAED,KAAK,CAAC,eAAe,CAAC,QAAgB,EAAE,QAAgB;QACtD,gFAAgF;QAChF,IAAI,IAAI,CAAC,SAAS,KAAK,SAAS,EAAE,CAAC;YACjC,MAAM,IAAI,CAAC,SAAS,CAAC;QACvB,CAAC;QAED,MAAM,MAAM,GAAG,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,GAAG,QAAQ,IAAI,QAAQ,EAAE,CAAC,CAAC;QAEhE,0CAA0C;QAC1C,qEAAqE;QACrE,IAAI,MAAM,KAAK,SAAS,IAAI,MAAM,CAAC,MAAM,KAAK,QAAQ,EAAE,CAAC;YACvD,MAAM,MAAM,CAAC,MAAM,CACjB,IAAI,KAAK,CACP,iFAAiF,CAClF,EACD,EAAE,IAAI,EAAE,kCAAkC,EAAE,CAC7C,CAAC;QACJ,CAAC;QACD,8DAA8D;IAChE,CAAC;CACF"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { AuthAdapter, VerifiedPrincipal } from "./types.js";
|
|
2
|
+
import type { HttpClient } from "./http-client.js";
|
|
3
|
+
/**
|
|
4
|
+
* Host-supplied synchronous function that maps a validated external JWT payload
|
|
5
|
+
* to a VerifiedPrincipal. Called only after signature, exp, nbf, iss, aud verified.
|
|
6
|
+
* Must throw if mapping is not possible; error is caught and re-thrown as
|
|
7
|
+
* EXTERNAL_IDP_CLAIM_MAPPER_THREW_001 (original message is NOT propagated).
|
|
8
|
+
*/
|
|
9
|
+
export type ClaimMapper = (payload: Record<string, unknown>) => VerifiedPrincipal;
|
|
10
|
+
export interface JwksKeyRingAuthAdapterConfig {
|
|
11
|
+
/** Full HTTPS URL to the JWKS endpoint. */
|
|
12
|
+
readonly jwksUri: string;
|
|
13
|
+
/** Expected iss claim — exact string match required. */
|
|
14
|
+
readonly issuer: string;
|
|
15
|
+
/** Expected aud claim — string or array; at least one must appear in JWT aud. */
|
|
16
|
+
readonly audience: string | readonly string[];
|
|
17
|
+
/** Signing algorithm. Only "RS256" is valid. */
|
|
18
|
+
readonly algorithm: "RS256";
|
|
19
|
+
/** Host-supplied claim mapper. Must not be async. */
|
|
20
|
+
readonly claimMapper: ClaimMapper;
|
|
21
|
+
/** HTTP client used for JWKS fetch. Host-supplied — no default bundled. */
|
|
22
|
+
readonly httpClient: HttpClient;
|
|
23
|
+
/** JWKS cache TTL in seconds. Default: 3600. */
|
|
24
|
+
readonly cacheTtlSeconds?: number;
|
|
25
|
+
/** JWKS fetch timeout in milliseconds. Default: 5000. */
|
|
26
|
+
readonly fetchTimeoutMs?: number;
|
|
27
|
+
/** Maximum JWKS response size in bytes. Default: 65536. */
|
|
28
|
+
readonly maxJwksByteSize?: number;
|
|
29
|
+
}
|
|
30
|
+
export declare class JwksKeyRingAuthAdapter implements AuthAdapter {
|
|
31
|
+
private readonly _issuer;
|
|
32
|
+
private readonly _audience;
|
|
33
|
+
private readonly _mapper;
|
|
34
|
+
private readonly _ring;
|
|
35
|
+
constructor(config: JwksKeyRingAuthAdapterConfig);
|
|
36
|
+
verify(credential: string): Promise<VerifiedPrincipal>;
|
|
37
|
+
private _validateAudience;
|
|
38
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// src/auth/jwks-key-ring-auth-adapter.ts
|
|
2
|
+
// INTERNAL — not exported from src/index.ts.
|
|
3
|
+
// RS256 AuthAdapter backed by a live JWKS endpoint.
|
|
4
|
+
// Algorithm: RS256 only. alg:none always rejected. kid required.
|
|
5
|
+
//
|
|
6
|
+
// CRYPTO_BOUNDARY_ONLY_001: does NOT import crypto primitives at runtime.
|
|
7
|
+
// All crypto is delegated to jwt-core.ts (verifyRsaSig).
|
|
8
|
+
//
|
|
9
|
+
// AUTH_FACTORY_AUTH_ADAPTER_OPAQUE_001: the production factory does not
|
|
10
|
+
// import this file. It is the host's responsibility to construct and inject.
|
|
11
|
+
//
|
|
12
|
+
// See docs/specs/post-mvp-external-idp-oidc-spec.md §7
|
|
13
|
+
// KeyObject not imported here — verifyRsaSig is called with inferred type from jwt-core
|
|
14
|
+
import { JwksKeyRing } from "./jwks-key-ring.js";
|
|
15
|
+
import { throwErr, splitJwt, parseJsonObject, verifyRsaSig, } from "./jwt-core.js";
|
|
16
|
+
// ─── JwksKeyRingAuthAdapter ───────────────────────────────────────────────────
|
|
17
|
+
export class JwksKeyRingAuthAdapter {
|
|
18
|
+
_issuer;
|
|
19
|
+
_audience;
|
|
20
|
+
_mapper;
|
|
21
|
+
_ring;
|
|
22
|
+
constructor(config) {
|
|
23
|
+
// Construction-time validation
|
|
24
|
+
if (config.algorithm !== "RS256") {
|
|
25
|
+
throwErr("EXTERNAL_IDP_ALG_UNSUPPORTED_001", "only RS256 is supported");
|
|
26
|
+
}
|
|
27
|
+
if (!config.jwksUri.startsWith("https://")) {
|
|
28
|
+
throwErr("EXTERNAL_IDP_JWKS_URI_INSECURE_001", "jwksUri must use HTTPS");
|
|
29
|
+
}
|
|
30
|
+
if (!config.issuer) {
|
|
31
|
+
throwErr("EXTERNAL_IDP_ISSUER_EMPTY_001", "issuer must be a non-empty string");
|
|
32
|
+
}
|
|
33
|
+
const aud = Array.isArray(config.audience) ? config.audience : [config.audience];
|
|
34
|
+
if (aud.length === 0 || (aud.length === 1 && aud[0] === "")) {
|
|
35
|
+
throwErr("EXTERNAL_IDP_AUDIENCE_EMPTY_001", "audience must be a non-empty value");
|
|
36
|
+
}
|
|
37
|
+
this._issuer = config.issuer;
|
|
38
|
+
this._audience = aud;
|
|
39
|
+
this._mapper = config.claimMapper;
|
|
40
|
+
this._ring = new JwksKeyRing({
|
|
41
|
+
jwksUri: config.jwksUri,
|
|
42
|
+
httpClient: config.httpClient,
|
|
43
|
+
cacheTtlMs: (config.cacheTtlSeconds ?? 3600) * 1000,
|
|
44
|
+
fetchTimeoutMs: config.fetchTimeoutMs ?? 5000,
|
|
45
|
+
maxBytes: config.maxJwksByteSize ?? 65536,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
async verify(credential) {
|
|
49
|
+
// Step 1 — Split JWT into exactly 3 segments
|
|
50
|
+
const [rawHeader, rawPayload, rawSig] = splitJwt(credential);
|
|
51
|
+
// Step 2 — Decode and parse header
|
|
52
|
+
const h = parseJsonObject(rawHeader, "header");
|
|
53
|
+
// Step 3 — Algorithm enforcement: alg:none always rejected; then binding check
|
|
54
|
+
const alg = h["alg"];
|
|
55
|
+
if (typeof alg === "string" && alg.toLowerCase() === "none") {
|
|
56
|
+
throwErr("EXTERNAL_IDP_ALG_NONE_REJECTED_001", "alg:none is not accepted");
|
|
57
|
+
}
|
|
58
|
+
if (alg !== "RS256") {
|
|
59
|
+
throwErr("EXTERNAL_IDP_ALG_MISMATCH_001", "unsupported algorithm; RS256 required");
|
|
60
|
+
}
|
|
61
|
+
// Step 4 — kid required
|
|
62
|
+
const kid = h["kid"];
|
|
63
|
+
if (typeof kid !== "string" || kid.length === 0) {
|
|
64
|
+
throwErr("EXTERNAL_IDP_KID_REQUIRED_001", "kid header is required and must be a non-empty string");
|
|
65
|
+
}
|
|
66
|
+
// Step 5 — Resolve public key (at-most-one JWKS fetch per verify call)
|
|
67
|
+
const publicKey = await this._ring.getPublicKey(kid);
|
|
68
|
+
if (publicKey === undefined) {
|
|
69
|
+
throwErr("EXTERNAL_IDP_KID_NOT_FOUND_001", "kid not found in JWKS");
|
|
70
|
+
}
|
|
71
|
+
// Step 6 — RS256 signature verification (delegates to jwt-core; no crypto import here)
|
|
72
|
+
verifyRsaSig(rawSig, rawHeader + "." + rawPayload, publicKey);
|
|
73
|
+
// Step 7 — Decode and parse payload
|
|
74
|
+
const p = parseJsonObject(rawPayload, "payload");
|
|
75
|
+
// Step 8 — Validate iss (exact match)
|
|
76
|
+
if (p["iss"] !== this._issuer) {
|
|
77
|
+
throwErr("EXTERNAL_IDP_ISSUER_MISMATCH_001", "iss does not match expected issuer");
|
|
78
|
+
}
|
|
79
|
+
// Step 9 — Validate aud
|
|
80
|
+
this._validateAudience(p["aud"]);
|
|
81
|
+
// Step 10 — Validate exp
|
|
82
|
+
const expRaw = p["exp"];
|
|
83
|
+
if (typeof expRaw !== "number") {
|
|
84
|
+
throwErr("EXTERNAL_IDP_TOKEN_EXPIRED_001", "exp claim is absent or not a number");
|
|
85
|
+
}
|
|
86
|
+
if (expRaw * 1000 <= Date.now()) {
|
|
87
|
+
throwErr("EXTERNAL_IDP_TOKEN_EXPIRED_001", "token has expired");
|
|
88
|
+
}
|
|
89
|
+
// Step 11 — Validate nbf (if present)
|
|
90
|
+
if ("nbf" in p) {
|
|
91
|
+
const nbfRaw = p["nbf"];
|
|
92
|
+
if (typeof nbfRaw !== "number") {
|
|
93
|
+
throwErr("EXTERNAL_IDP_TOKEN_NOT_YET_VALID_001", "nbf claim is not a number");
|
|
94
|
+
}
|
|
95
|
+
if (nbfRaw * 1000 > Date.now()) {
|
|
96
|
+
throwErr("EXTERNAL_IDP_TOKEN_NOT_YET_VALID_001", "token is not yet valid");
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
// Step 12 — Claim mapping (host-supplied; error message must not propagate)
|
|
100
|
+
let principal;
|
|
101
|
+
try {
|
|
102
|
+
principal = this._mapper(p);
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
throwErr("EXTERNAL_IDP_CLAIM_MAPPER_THREW_001", "claim mapper failed");
|
|
106
|
+
}
|
|
107
|
+
return principal;
|
|
108
|
+
}
|
|
109
|
+
// ─── Private helpers ──────────────────────────────────────────────────────
|
|
110
|
+
_validateAudience(jwtAud) {
|
|
111
|
+
const jwtAudArr = typeof jwtAud === "string" ? [jwtAud] :
|
|
112
|
+
Array.isArray(jwtAud) ? jwtAud :
|
|
113
|
+
[];
|
|
114
|
+
if (jwtAudArr.length === 0) {
|
|
115
|
+
throwErr("EXTERNAL_IDP_AUDIENCE_MISMATCH_001", "aud claim is absent or invalid");
|
|
116
|
+
}
|
|
117
|
+
const hasMatch = this._audience.some(a => jwtAudArr.includes(a));
|
|
118
|
+
if (!hasMatch) {
|
|
119
|
+
throwErr("EXTERNAL_IDP_AUDIENCE_MISMATCH_001", "aud does not include a configured audience value");
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
//# sourceMappingURL=jwks-key-ring-auth-adapter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"jwks-key-ring-auth-adapter.js","sourceRoot":"","sources":["../../src/auth/jwks-key-ring-auth-adapter.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { KeyObject } from "./jwt-core.js";
|
|
2
|
+
import type { HttpClient } from "./http-client.js";
|
|
3
|
+
export interface JwksKeyRingConfig {
|
|
4
|
+
readonly jwksUri: string;
|
|
5
|
+
readonly httpClient: HttpClient;
|
|
6
|
+
readonly cacheTtlMs: number;
|
|
7
|
+
readonly fetchTimeoutMs: number;
|
|
8
|
+
readonly maxBytes: number;
|
|
9
|
+
}
|
|
10
|
+
export declare class JwksKeyRing {
|
|
11
|
+
private readonly _config;
|
|
12
|
+
private _cache;
|
|
13
|
+
constructor(config: JwksKeyRingConfig);
|
|
14
|
+
/**
|
|
15
|
+
* Resolve the public key for the given kid.
|
|
16
|
+
* Cache hit (within TTL): returns immediately.
|
|
17
|
+
* Cache miss / expired: triggers at-most-one JWKS fetch, then looks up again.
|
|
18
|
+
* Returns undefined if kid is not found after fetch.
|
|
19
|
+
* Throws EXTERNAL_IDP_JWKS_FETCH_FAILED_001 or EXTERNAL_IDP_JWKS_PARSE_ERROR_001 on error.
|
|
20
|
+
*/
|
|
21
|
+
getPublicKey(kid: string): Promise<KeyObject | undefined>;
|
|
22
|
+
/** Force cache invalidation and re-fetch. */
|
|
23
|
+
forceRefresh(): Promise<void>;
|
|
24
|
+
private _fetchAndPopulate;
|
|
25
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// src/auth/jwks-key-ring.ts
|
|
2
|
+
// INTERNAL — not exported from src/index.ts.
|
|
3
|
+
// In-memory JWKS key cache with TTL and at-most-one-fetch-per-call semantics.
|
|
4
|
+
//
|
|
5
|
+
// CRYPTO_BOUNDARY_ONLY_001: does NOT import crypto primitives at runtime.
|
|
6
|
+
// All RSA key operations are delegated to importRsaPublicKey() in jwt-core.ts.
|
|
7
|
+
//
|
|
8
|
+
// See docs/specs/post-mvp-external-idp-oidc-spec.md §7.3, §7.4
|
|
9
|
+
import { importRsaPublicKey, throwErr } from "./jwt-core.js";
|
|
10
|
+
// ─── JwksKeyRing ─────────────────────────────────────────────────────────────
|
|
11
|
+
export class JwksKeyRing {
|
|
12
|
+
_config;
|
|
13
|
+
_cache;
|
|
14
|
+
constructor(config) {
|
|
15
|
+
this._config = config;
|
|
16
|
+
this._cache = new Map();
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Resolve the public key for the given kid.
|
|
20
|
+
* Cache hit (within TTL): returns immediately.
|
|
21
|
+
* Cache miss / expired: triggers at-most-one JWKS fetch, then looks up again.
|
|
22
|
+
* Returns undefined if kid is not found after fetch.
|
|
23
|
+
* Throws EXTERNAL_IDP_JWKS_FETCH_FAILED_001 or EXTERNAL_IDP_JWKS_PARSE_ERROR_001 on error.
|
|
24
|
+
*/
|
|
25
|
+
async getPublicKey(kid) {
|
|
26
|
+
const now = Date.now();
|
|
27
|
+
const cached = this._cache.get(kid);
|
|
28
|
+
if (cached !== undefined && cached.expiresAtMs > now) {
|
|
29
|
+
return cached.publicKey;
|
|
30
|
+
}
|
|
31
|
+
// Cache miss or expired — fetch once
|
|
32
|
+
await this._fetchAndPopulate();
|
|
33
|
+
const fresh = this._cache.get(kid);
|
|
34
|
+
if (fresh !== undefined && fresh.expiresAtMs > now) {
|
|
35
|
+
return fresh.publicKey;
|
|
36
|
+
}
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
/** Force cache invalidation and re-fetch. */
|
|
40
|
+
async forceRefresh() {
|
|
41
|
+
this._cache.clear();
|
|
42
|
+
await this._fetchAndPopulate();
|
|
43
|
+
}
|
|
44
|
+
// ─── Private ───────────────────────────────────────────────────────────────
|
|
45
|
+
async _fetchAndPopulate() {
|
|
46
|
+
const { jwksUri, httpClient, fetchTimeoutMs, maxBytes, cacheTtlMs } = this._config;
|
|
47
|
+
// Fetch JWKS endpoint
|
|
48
|
+
let responseText;
|
|
49
|
+
try {
|
|
50
|
+
responseText = await httpClient.getText(jwksUri, { timeoutMs: fetchTimeoutMs, maxBytes });
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
throwErr("EXTERNAL_IDP_JWKS_FETCH_FAILED_001", "JWKS fetch failed");
|
|
54
|
+
}
|
|
55
|
+
// Belt-and-suspenders size guard (in case client doesn't enforce maxBytes)
|
|
56
|
+
if (responseText.length > maxBytes) {
|
|
57
|
+
throwErr("EXTERNAL_IDP_JWKS_FETCH_FAILED_001", "JWKS response exceeds maximum allowed size");
|
|
58
|
+
}
|
|
59
|
+
// Parse JSON
|
|
60
|
+
let parsed;
|
|
61
|
+
try {
|
|
62
|
+
parsed = JSON.parse(responseText);
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
throwErr("EXTERNAL_IDP_JWKS_PARSE_ERROR_001", "JWKS response is not valid JSON");
|
|
66
|
+
}
|
|
67
|
+
if (typeof parsed !== "object" || parsed === null || !("keys" in parsed)) {
|
|
68
|
+
throwErr("EXTERNAL_IDP_JWKS_PARSE_ERROR_001", "JWKS response missing keys field");
|
|
69
|
+
}
|
|
70
|
+
const keysField = parsed.keys;
|
|
71
|
+
if (!Array.isArray(keysField)) {
|
|
72
|
+
throwErr("EXTERNAL_IDP_JWKS_PARSE_ERROR_001", "JWKS keys is not an array");
|
|
73
|
+
}
|
|
74
|
+
const expiresAtMs = Date.now() + cacheTtlMs;
|
|
75
|
+
let imported = 0;
|
|
76
|
+
for (const raw of keysField) {
|
|
77
|
+
if (typeof raw !== "object" || raw === null)
|
|
78
|
+
continue;
|
|
79
|
+
const entry = raw;
|
|
80
|
+
// Only RSA signing keys
|
|
81
|
+
if (entry["kty"] !== "RSA")
|
|
82
|
+
continue;
|
|
83
|
+
const useField = entry["use"];
|
|
84
|
+
if (useField !== undefined && useField !== "sig")
|
|
85
|
+
continue;
|
|
86
|
+
const kidField = entry["kid"];
|
|
87
|
+
if (typeof kidField !== "string" || kidField.length === 0)
|
|
88
|
+
continue;
|
|
89
|
+
// Delegate RSA key import to jwt-core (CRYPTO_BOUNDARY_ONLY_001)
|
|
90
|
+
// importRsaPublicKey throws on malformed or weak keys — catch and skip.
|
|
91
|
+
let publicKey;
|
|
92
|
+
try {
|
|
93
|
+
publicKey = importRsaPublicKey(entry);
|
|
94
|
+
}
|
|
95
|
+
catch (err) {
|
|
96
|
+
// Re-throw EXTERNAL_IDP_PUBLIC_KEY_TOO_SMALL_001 (security invariant) but skip parse errors.
|
|
97
|
+
if (err instanceof Error &&
|
|
98
|
+
err.code === "EXTERNAL_IDP_PUBLIC_KEY_TOO_SMALL_001") {
|
|
99
|
+
throw err;
|
|
100
|
+
}
|
|
101
|
+
continue; // skip malformed entries; keep trying remaining keys
|
|
102
|
+
}
|
|
103
|
+
this._cache.set(kidField, { publicKey, expiresAtMs });
|
|
104
|
+
imported++;
|
|
105
|
+
}
|
|
106
|
+
if (imported === 0) {
|
|
107
|
+
throwErr("EXTERNAL_IDP_JWKS_PARSE_ERROR_001", "JWKS contains no valid RSA signing keys");
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
//# sourceMappingURL=jwks-key-ring.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"jwks-key-ring.js","sourceRoot":"","sources":["../../src/auth/jwks-key-ring.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { AuthAdapter, Clock, VerifiedPrincipal } from "./types.js";
|
|
2
|
+
export interface JwtAuthAdapterConfig {
|
|
3
|
+
readonly secret: Uint8Array;
|
|
4
|
+
readonly expectedIssuer: string;
|
|
5
|
+
readonly expectedAudience: string;
|
|
6
|
+
readonly clock?: Clock;
|
|
7
|
+
}
|
|
8
|
+
export declare class JwtAuthAdapter implements AuthAdapter {
|
|
9
|
+
private readonly _secret;
|
|
10
|
+
private readonly _issuer;
|
|
11
|
+
private readonly _audience;
|
|
12
|
+
private readonly _clock;
|
|
13
|
+
constructor(config: JwtAuthAdapterConfig);
|
|
14
|
+
verify(credential: string): Promise<VerifiedPrincipal>;
|
|
15
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// src/auth/jwt-auth-adapter.ts
|
|
2
|
+
// TEST/DEV in Production-03-a — NOT exported from src/index.ts.
|
|
3
|
+
// Single-secret HS256 adapter. Cryptographic helpers live in jwt-core.ts
|
|
4
|
+
// (the credential-verification crypto boundary).
|
|
5
|
+
//
|
|
6
|
+
// AUTH_CREDENTIAL_KEY_CONFIG_INVALID_001 ✅ Production-03-a
|
|
7
|
+
// AUTH_CREDENTIAL_MALFORMED_001 ✅ Production-03-a
|
|
8
|
+
// AUTH_CREDENTIAL_ALGORITHM_UNSUPPORTED_001 ✅ Production-03-a
|
|
9
|
+
// AUTH_CREDENTIAL_KEY_ID_UNKNOWN_001 ✅ Production-03-a
|
|
10
|
+
// AUTH_CREDENTIAL_SIGNATURE_INVALID_001 ✅ Production-03-a
|
|
11
|
+
// AUTH_CREDENTIAL_TIME_CLAIM_INVALID_001 ✅ Production-03-a
|
|
12
|
+
// AUTH_CREDENTIAL_EXPIRED_001 ✅ Production-03-a
|
|
13
|
+
// AUTH_CREDENTIAL_NOT_YET_VALID_001 ✅ Production-03-a
|
|
14
|
+
// AUTH_CREDENTIAL_ISSUER_INVALID_001 ✅ Production-03-a
|
|
15
|
+
// AUTH_CREDENTIAL_AUDIENCE_INVALID_001 ✅ Production-03-a
|
|
16
|
+
// AUTH_CREDENTIAL_CLAIM_MISSING_001 ✅ Production-03-a
|
|
17
|
+
// AUTH_CREDENTIAL_CLAIM_INVALID_001 ✅ Production-03-a
|
|
18
|
+
import { throwErr, MIN_SECRET_BYTES, splitJwt, parseJsonObject, assertAlgHS256, verifyHmacSig, verifyPayloadAndBuildPrincipal, } from "./jwt-core.js";
|
|
19
|
+
export class JwtAuthAdapter {
|
|
20
|
+
_secret;
|
|
21
|
+
_issuer;
|
|
22
|
+
_audience;
|
|
23
|
+
_clock;
|
|
24
|
+
constructor(config) {
|
|
25
|
+
// C1 — construction-time guards (AUTH_CREDENTIAL_KEY_CONFIG_INVALID_001)
|
|
26
|
+
if (config.secret == null || config.secret.length < MIN_SECRET_BYTES) {
|
|
27
|
+
throwErr("AUTH_CREDENTIAL_KEY_CONFIG_INVALID_001", `secret must be a Uint8Array of at least ${MIN_SECRET_BYTES} bytes`);
|
|
28
|
+
}
|
|
29
|
+
if (!config.expectedIssuer) {
|
|
30
|
+
throwErr("AUTH_CREDENTIAL_KEY_CONFIG_INVALID_001", "expectedIssuer must be a non-empty string");
|
|
31
|
+
}
|
|
32
|
+
if (!config.expectedAudience) {
|
|
33
|
+
throwErr("AUTH_CREDENTIAL_KEY_CONFIG_INVALID_001", "expectedAudience must be a non-empty string");
|
|
34
|
+
}
|
|
35
|
+
this._secret = Buffer.from(config.secret);
|
|
36
|
+
this._issuer = config.expectedIssuer;
|
|
37
|
+
this._audience = config.expectedAudience;
|
|
38
|
+
this._clock = config.clock ?? { now: () => new Date().toISOString() };
|
|
39
|
+
}
|
|
40
|
+
async verify(credential) {
|
|
41
|
+
const nowMs = Date.parse(this._clock.now());
|
|
42
|
+
// Step 1 — structure: exactly 3 non-empty segments
|
|
43
|
+
const [rawHeader, rawPayload, rawSig] = splitJwt(credential);
|
|
44
|
+
// Steps 2-3 — header: base64url decode + JSON parse
|
|
45
|
+
const h = parseJsonObject(rawHeader, "header");
|
|
46
|
+
// Step 4 — algorithm enforcement
|
|
47
|
+
assertAlgHS256(h);
|
|
48
|
+
// Step 5 — kid rejection (single-key mode never accepts a kid-bearing token)
|
|
49
|
+
if ("kid" in h) {
|
|
50
|
+
throwErr("AUTH_CREDENTIAL_KEY_ID_UNKNOWN_001", "token contains kid; multi-key is not supported in single-secret mode");
|
|
51
|
+
}
|
|
52
|
+
// Steps 6-8 — HMAC-SHA256 with timingSafeEqual (via jwt-core.ts)
|
|
53
|
+
verifyHmacSig(rawHeader, rawPayload, rawSig, this._secret);
|
|
54
|
+
// Steps 9-21 — payload parse + full verification + VerifiedPrincipal assembly
|
|
55
|
+
const p = parseJsonObject(rawPayload, "payload");
|
|
56
|
+
return verifyPayloadAndBuildPrincipal(p, nowMs, this._issuer, this._audience);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=jwt-auth-adapter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"jwt-auth-adapter.js","sourceRoot":"","sources":["../../src/auth/jwt-auth-adapter.ts"],"names":[],"mappings":"AAAA,+BAA+B;AAC/B,gEAAgE;AAChE,yEAAyE;AACzE,iDAAiD;AACjD,EAAE;AACF,8DAA8D;AAC9D,8DAA8D;AAC9D,8DAA8D;AAC9D,8DAA8D;AAC9D,8DAA8D;AAC9D,8DAA8D;AAC9D,8DAA8D;AAC9D,8DAA8D;AAC9D,8DAA8D;AAC9D,8DAA8D;AAC9D,8DAA8D;AAC9D,8DAA8D;AAG9D,OAAO,EACL,QAAQ,EACR,gBAAgB,EAChB,QAAQ,EACR,eAAe,EACf,cAAc,EACd,aAAa,EACb,8BAA8B,GAC/B,MAAM,eAAe,CAAC;AAWvB,MAAM,OAAO,cAAc;IACR,OAAO,CAAW;IAClB,OAAO,CAAW;IAClB,SAAS,CAAS;IAClB,MAAM,CAAW;IAElC,YAAY,MAA4B;QACtC,yEAAyE;QACzE,IAAI,MAAM,CAAC,MAAM,IAAI,IAAI,IAAI,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,gBAAgB,EAAE,CAAC;YACrE,QAAQ,CACN,wCAAwC,EACxC,2CAA2C,gBAAgB,QAAQ,CACpE,CAAC;QACJ,CAAC;QACD,IAAI,CAAC,MAAM,CAAC,cAAc,EAAE,CAAC;YAC3B,QAAQ,CAAC,wCAAwC,EAAE,2CAA2C,CAAC,CAAC;QAClG,CAAC;QACD,IAAI,CAAC,MAAM,CAAC,gBAAgB,EAAE,CAAC;YAC7B,QAAQ,CAAC,wCAAwC,EAAE,6CAA6C,CAAC,CAAC;QACpG,CAAC;QACD,IAAI,CAAC,OAAO,GAAK,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;QAC5C,IAAI,CAAC,OAAO,GAAK,MAAM,CAAC,cAAc,CAAC;QACvC,IAAI,CAAC,SAAS,GAAG,MAAM,CAAC,gBAAgB,CAAC;QACzC,IAAI,CAAC,MAAM,GAAM,MAAM,CAAC,KAAK,IAAI,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,EAAE,CAAC;IAC3E,CAAC;IAED,KAAK,CAAC,MAAM,CAAC,UAAkB;QAC7B,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC,CAAC;QAE5C,mDAAmD;QACnD,MAAM,CAAC,SAAS,EAAE,UAAU,EAAE,MAAM,CAAC,GAAG,QAAQ,CAAC,UAAU,CAAC,CAAC;QAE7D,oDAAoD;QACpD,MAAM,CAAC,GAAG,eAAe,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;QAE/C,iCAAiC;QACjC,cAAc,CAAC,CAAC,CAAC,CAAC;QAElB,6EAA6E;QAC7E,IAAI,KAAK,IAAI,CAAC,EAAE,CAAC;YACf,QAAQ,CACN,oCAAoC,EACpC,sEAAsE,CACvE,CAAC;QACJ,CAAC;QAED,iEAAiE;QACjE,aAAa,CAAC,SAAS,EAAE,UAAU,EAAE,MAAM,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC;QAE3D,8EAA8E;QAC9E,MAAM,CAAC,GAAG,eAAe,CAAC,UAAU,EAAE,SAAS,CAAC,CAAC;QACjD,OAAO,8BAA8B,CAAC,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC;IAChF,CAAC;CACF"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { KeyObject } from "node:crypto";
|
|
2
|
+
export type { KeyObject };
|
|
3
|
+
import type { VerifiedPrincipal } from "./types.js";
|
|
4
|
+
export declare const MIN_SECRET_BYTES = 32;
|
|
5
|
+
export declare const VALID_PRINCIPAL_TYPES: Set<string>;
|
|
6
|
+
export declare const VALID_ROLES: Set<string>;
|
|
7
|
+
export declare function throwErr(code: string, detail: string): never;
|
|
8
|
+
export declare function decodeB64url(segment: string): Buffer;
|
|
9
|
+
export declare function parseJsonObject(segment: string, label: string): Record<string, unknown>;
|
|
10
|
+
export declare function splitJwt(credential: string): [string, string, string];
|
|
11
|
+
export declare function assertAlgHS256(h: Record<string, unknown>): void;
|
|
12
|
+
export declare function verifyHmacSig(rawHeader: string, rawPayload: string, rawSig: string, secret: Buffer): void;
|
|
13
|
+
export declare function requireStr(p: Record<string, unknown>, name: string): string;
|
|
14
|
+
export declare function requireStrArr(p: Record<string, unknown>, name: string, valid: ReadonlySet<string> | null): ReadonlyArray<string>;
|
|
15
|
+
export declare function verifyPayloadAndBuildPrincipal(p: Record<string, unknown>, nowMs: number, expectedIssuer: string, expectedAudience: string): VerifiedPrincipal;
|
|
16
|
+
/**
|
|
17
|
+
* Import an RSA public key from a JWK entry (Record<string, unknown>).
|
|
18
|
+
* Enforces: kty must be "RSA", minimum 2048-bit modulus.
|
|
19
|
+
* Throws EXTERNAL_IDP_PUBLIC_KEY_TOO_SMALL_001 for weak keys.
|
|
20
|
+
* Throws EXTERNAL_IDP_JWKS_PARSE_ERROR_001 for malformed entries.
|
|
21
|
+
* Called only from JwksKeyRing — never from the auth adapter directly.
|
|
22
|
+
*/
|
|
23
|
+
export declare function importRsaPublicKey(entry: Record<string, unknown>): KeyObject;
|
|
24
|
+
/**
|
|
25
|
+
* Verify an RS256 (PKCS#1 v1.5 / SHA-256) JWT signature.
|
|
26
|
+
* rawSig: base64url-encoded signature (JWT third segment).
|
|
27
|
+
* headerDotPayload: raw signing input ("header.payload", as sent over wire).
|
|
28
|
+
* publicKey: RSA KeyObject returned by importRsaPublicKey.
|
|
29
|
+
* Throws EXTERNAL_IDP_SIGNATURE_INVALID_001 on any verification failure.
|
|
30
|
+
* No sensitive data in error messages.
|
|
31
|
+
*/
|
|
32
|
+
export declare function verifyRsaSig(rawSig: string, headerDotPayload: string, publicKey: KeyObject): void;
|