@datacules/agent-identity 0.8.0 → 0.10.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 +128 -0
- package/dist/cjs/attestation.js +131 -29
- package/dist/cjs/attestation.js.map +1 -1
- package/dist/cjs/identity-providers.js +100 -0
- package/dist/cjs/identity-providers.js.map +1 -0
- package/dist/cjs/index.js +5 -0
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/revocation-listener.js +78 -0
- package/dist/cjs/revocation-listener.js.map +1 -0
- package/dist/cjs/revocation.js +59 -0
- package/dist/cjs/revocation.js.map +1 -0
- package/dist/cjs/rotation.js +6 -1
- package/dist/cjs/rotation.js.map +1 -1
- package/dist/cjs/router.js +27 -5
- package/dist/cjs/router.js.map +1 -1
- package/dist/cjs/schemas.js +26 -2
- package/dist/cjs/schemas.js.map +1 -1
- package/dist/esm/attestation.js +129 -28
- package/dist/esm/attestation.js.map +1 -1
- package/dist/esm/identity-providers.js +97 -0
- package/dist/esm/identity-providers.js.map +1 -0
- package/dist/esm/index.js +5 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/revocation-listener.js +74 -0
- package/dist/esm/revocation-listener.js.map +1 -0
- package/dist/esm/revocation.js +55 -0
- package/dist/esm/revocation.js.map +1 -0
- package/dist/esm/rotation.js +6 -1
- package/dist/esm/rotation.js.map +1 -1
- package/dist/esm/router.js +27 -5
- package/dist/esm/router.js.map +1 -1
- package/dist/esm/schemas.js +25 -1
- package/dist/esm/schemas.js.map +1 -1
- package/dist/types/attestation.d.ts +34 -6
- package/dist/types/attestation.d.ts.map +1 -1
- package/dist/types/identity-providers.d.ts +53 -0
- package/dist/types/identity-providers.d.ts.map +1 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/revocation-listener.d.ts +63 -0
- package/dist/types/revocation-listener.d.ts.map +1 -0
- package/dist/types/revocation.d.ts +52 -0
- package/dist/types/revocation.d.ts.map +1 -0
- package/dist/types/rotation.d.ts.map +1 -1
- package/dist/types/router.d.ts +14 -0
- package/dist/types/router.d.ts.map +1 -1
- package/dist/types/schemas.d.ts +89 -4
- package/dist/types/schemas.d.ts.map +1 -1
- package/dist/types/types.d.ts +82 -1
- package/dist/types/types.d.ts.map +1 -1
- package/package.json +1 -1
package/README.md
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="../../assets/logo.svg" alt="Agent Identity — by Datacules LLC" width="360"/>
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
# `@datacules/agent-identity`
|
|
6
|
+
|
|
7
|
+
Core credential routing engine for the agent-identity framework. Provider-agnostic; works with OpenAI, Anthropic, Gemini, Mistral, and local models.
|
|
8
|
+
|
|
9
|
+
Published as the `@datacules/agent-identity` npm package from `packages/core/`.
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install @datacules/agent-identity
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## What's in this package
|
|
18
|
+
|
|
19
|
+
| Export | Description |
|
|
20
|
+
|--------|-------------|
|
|
21
|
+
| `createRouter` | Build a `CredentialRouter` from an array of credentials + rules |
|
|
22
|
+
| `createRouterFromStore` | Build a router backed by any `CredentialStore` |
|
|
23
|
+
| `createRouterWithConfig` | Full-featured factory — accepts attestation signer, budget enforcer, approval gate |
|
|
24
|
+
| `MemoryCredentialStore` | In-memory store for dev + tests |
|
|
25
|
+
| `CredentialRouter` | Class with `resolve()`, `resolveAsync()`, `resolvePair()`, `resolvePairAsync()` |
|
|
26
|
+
| `@datacules/agent-identity/schemas` | Zod schemas for `AgentRequestContext`, `MigrationContext`, `Credential` |
|
|
27
|
+
| `@datacules/agent-identity/react` | `useAgentIdentity` React hook (server-side credential resolution) |
|
|
28
|
+
|
|
29
|
+
## Quick start
|
|
30
|
+
|
|
31
|
+
```typescript
|
|
32
|
+
import { createRouter } from '@datacules/agent-identity';
|
|
33
|
+
import type { AgentRequestContext, Credential, RoutingRule } from '@datacules/agent-identity';
|
|
34
|
+
|
|
35
|
+
const credentials: Credential[] = [
|
|
36
|
+
{
|
|
37
|
+
id: 'cred-openai-prod',
|
|
38
|
+
ref: 'vault:openai-prod-key',
|
|
39
|
+
kind: 'fixed',
|
|
40
|
+
provider: 'openai',
|
|
41
|
+
status: 'active',
|
|
42
|
+
},
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
const rules: RoutingRule[] = [
|
|
46
|
+
{
|
|
47
|
+
id: 'rule-default',
|
|
48
|
+
credentialRef: 'vault:openai-prod-key',
|
|
49
|
+
credentialKind: 'fixed',
|
|
50
|
+
priority: 10,
|
|
51
|
+
},
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
const router = createRouter(credentials, rules);
|
|
55
|
+
|
|
56
|
+
const ctx: AgentRequestContext = {
|
|
57
|
+
userId: 'user-abc',
|
|
58
|
+
resourceId: 'knowledge-base',
|
|
59
|
+
resourceKind:'personal',
|
|
60
|
+
provider: 'openai',
|
|
61
|
+
model: 'gpt-4o',
|
|
62
|
+
action: 'read',
|
|
63
|
+
traceId: crypto.randomUUID(),
|
|
64
|
+
requestedAt: new Date().toISOString(),
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const resolved = router.resolve(ctx);
|
|
68
|
+
// resolved.ref → 'vault:openai-prod-key' — look this up in your vault, server-side
|
|
69
|
+
// resolved.resolvedFor → 'service' (or the userId for user-delegated)
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Routing rule fields
|
|
73
|
+
|
|
74
|
+
```typescript
|
|
75
|
+
const rule: RoutingRule = {
|
|
76
|
+
id: 'rule-personal-docs',
|
|
77
|
+
credentialRef: 'user-oauth-slot', // ref to a Credential
|
|
78
|
+
credentialKind: 'user-delegated', // 'fixed' | 'user-delegated'
|
|
79
|
+
priority: 10, // higher = evaluated first
|
|
80
|
+
resourceKind: 'personal', // optional match
|
|
81
|
+
matchProvider: 'anthropic', // optional match
|
|
82
|
+
matchAction: ['read', 'write'], // optional match
|
|
83
|
+
matchUserId: 'user-abc', // optional match
|
|
84
|
+
matchSpiffeId: 'spiffe://acme.com/ns/prod/sa/agent', // optional
|
|
85
|
+
matchPhase: 'extract', // migration phase match
|
|
86
|
+
canaryRef: 'user-oauth-slot-v2', // optional canary credential
|
|
87
|
+
canaryWeight: 5, // 0–100% traffic to canary
|
|
88
|
+
readOnly: true, // enforce read scope
|
|
89
|
+
};
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Migration: `resolvePair`
|
|
93
|
+
|
|
94
|
+
```typescript
|
|
95
|
+
import type { MigrationContext } from '@datacules/agent-identity';
|
|
96
|
+
|
|
97
|
+
const pair = router.resolvePair(ctx as MigrationContext);
|
|
98
|
+
// pair.source — read credential for sourceResourceId
|
|
99
|
+
// pair.target — write credential for targetResourceId
|
|
100
|
+
// pair.expiresAt — earliest expiry of both
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Zod schemas
|
|
104
|
+
|
|
105
|
+
```typescript
|
|
106
|
+
import { AgentRequestContextSchema } from '@datacules/agent-identity/schemas';
|
|
107
|
+
|
|
108
|
+
const parsed = AgentRequestContextSchema.safeParse(body);
|
|
109
|
+
if (!parsed.success) return Response.json({ error: parsed.error.flatten() }, { status: 400 });
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## React hook
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
import { useAgentIdentity } from '@datacules/agent-identity/react';
|
|
116
|
+
|
|
117
|
+
function Component({ userId }: { userId: string }) {
|
|
118
|
+
const { resolvedFor, loading, error, expiresAt } = useAgentIdentity({
|
|
119
|
+
userId, resourceId: 'kb', resourceKind: 'personal',
|
|
120
|
+
provider: 'anthropic', model: 'claude-sonnet-4-20250514',
|
|
121
|
+
action: 'read', traceId: crypto.randomUUID(),
|
|
122
|
+
requestedAt: new Date().toISOString(),
|
|
123
|
+
});
|
|
124
|
+
// ...
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
See the [root README](../../README.md) for the full API reference and all integration options.
|
package/dist/cjs/attestation.js
CHANGED
|
@@ -1,42 +1,74 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.HmacAttestationSigner = void 0;
|
|
3
|
+
exports.AsymmetricAttestationSigner = exports.HmacAttestationSigner = void 0;
|
|
4
4
|
exports.buildAttestation = buildAttestation;
|
|
5
5
|
exports.verifyAttestation = verifyAttestation;
|
|
6
|
-
// ───
|
|
6
|
+
// ─── Shared base64url helpers (module-level; used by both signers) ──────────
|
|
7
|
+
/** Encode a UTF-8 string to base64url */
|
|
8
|
+
function base64urlEncode(input) {
|
|
9
|
+
if (typeof Buffer !== 'undefined') {
|
|
10
|
+
return Buffer.from(input).toString('base64url');
|
|
11
|
+
}
|
|
12
|
+
return btoa(input).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
|
|
13
|
+
}
|
|
14
|
+
/** Encode an ArrayBuffer to base64url */
|
|
15
|
+
function bufToBase64url(buf) {
|
|
16
|
+
if (typeof Buffer !== 'undefined') {
|
|
17
|
+
return Buffer.from(buf).toString('base64url');
|
|
18
|
+
}
|
|
19
|
+
const bytes = new Uint8Array(buf);
|
|
20
|
+
let binary = '';
|
|
21
|
+
for (let i = 0; i < bytes.byteLength; i++)
|
|
22
|
+
binary += String.fromCharCode(bytes[i]);
|
|
23
|
+
return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Decode a base64url string to a Uint8Array<ArrayBuffer>.
|
|
27
|
+
*
|
|
28
|
+
* Returns Uint8Array<ArrayBuffer> (not ArrayBufferLike) so the result is
|
|
29
|
+
* directly usable as BufferSource in crypto.subtle.verify() without an
|
|
30
|
+
* extra cast — required since TypeScript 5.5 made Uint8Array generic.
|
|
31
|
+
*/
|
|
32
|
+
function base64urlToBuffer(s) {
|
|
33
|
+
if (typeof Buffer !== 'undefined') {
|
|
34
|
+
// Buffer.from() returns Uint8Array<ArrayBufferLike>; copy into a fresh
|
|
35
|
+
// Uint8Array<ArrayBuffer> so crypto.subtle accepts it as BufferSource.
|
|
36
|
+
const nodeBuf = Buffer.from(s, 'base64url');
|
|
37
|
+
const out = new Uint8Array(nodeBuf.length);
|
|
38
|
+
out.set(nodeBuf);
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
41
|
+
const b64 = s.replace(/-/g, '+').replace(/_/g, '/');
|
|
42
|
+
const padded = b64 + '='.repeat((4 - (b64.length % 4)) % 4);
|
|
43
|
+
const binary = atob(padded);
|
|
44
|
+
const bytes = new Uint8Array(binary.length);
|
|
45
|
+
for (let i = 0; i < binary.length; i++)
|
|
46
|
+
bytes[i] = binary.charCodeAt(i);
|
|
47
|
+
return bytes;
|
|
48
|
+
}
|
|
49
|
+
/** Decode a base64url body segment to a UTF-8 string */
|
|
50
|
+
function base64urlDecodeString(s) {
|
|
51
|
+
if (typeof Buffer !== 'undefined') {
|
|
52
|
+
return Buffer.from(s, 'base64url').toString('utf8');
|
|
53
|
+
}
|
|
54
|
+
return atob(s.replace(/-/g, '+').replace(/_/g, '/'));
|
|
55
|
+
}
|
|
56
|
+
// ─── HMAC Signer (built-in, zero deps) ───────────────────────────────────
|
|
7
57
|
class HmacAttestationSigner {
|
|
8
58
|
constructor(options) {
|
|
9
59
|
this.secret = options.secret;
|
|
10
60
|
this.issuer = options.issuer ?? 'agent-identity';
|
|
11
61
|
this.ttlSeconds = options.ttlSeconds ?? 300;
|
|
12
62
|
}
|
|
13
|
-
base64url(input) {
|
|
14
|
-
// Works in both browser and Node 18+ (Buffer is global in Node)
|
|
15
|
-
if (typeof Buffer !== 'undefined') {
|
|
16
|
-
return Buffer.from(input).toString('base64url');
|
|
17
|
-
}
|
|
18
|
-
// Browser fallback via btoa
|
|
19
|
-
return btoa(input).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
|
|
20
|
-
}
|
|
21
|
-
bufToBase64url(buf) {
|
|
22
|
-
if (typeof Buffer !== 'undefined') {
|
|
23
|
-
return Buffer.from(buf).toString('base64url');
|
|
24
|
-
}
|
|
25
|
-
const bytes = new Uint8Array(buf);
|
|
26
|
-
let binary = '';
|
|
27
|
-
for (let i = 0; i < bytes.byteLength; i++)
|
|
28
|
-
binary += String.fromCharCode(bytes[i]);
|
|
29
|
-
return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
|
|
30
|
-
}
|
|
31
63
|
async hmacSign(data) {
|
|
32
64
|
const enc = new TextEncoder();
|
|
33
65
|
const key = await crypto.subtle.importKey('raw', enc.encode(this.secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
|
|
34
66
|
const sig = await crypto.subtle.sign('HMAC', key, enc.encode(data));
|
|
35
|
-
return
|
|
67
|
+
return bufToBase64url(sig);
|
|
36
68
|
}
|
|
37
69
|
async sign(payload) {
|
|
38
|
-
const header =
|
|
39
|
-
const body =
|
|
70
|
+
const header = base64urlEncode(JSON.stringify({ alg: 'HS256', typ: 'JWT' }));
|
|
71
|
+
const body = base64urlEncode(JSON.stringify(payload));
|
|
40
72
|
const sig = await this.hmacSign(`${header}.${body}`);
|
|
41
73
|
return `${header}.${body}.${sig}`;
|
|
42
74
|
}
|
|
@@ -48,11 +80,7 @@ class HmacAttestationSigner {
|
|
|
48
80
|
const expected = await this.hmacSign(`${header}.${body}`);
|
|
49
81
|
if (expected !== sig)
|
|
50
82
|
return null;
|
|
51
|
-
|
|
52
|
-
const decoded = typeof Buffer !== 'undefined'
|
|
53
|
-
? Buffer.from(body, 'base64url').toString('utf8')
|
|
54
|
-
: atob(body.replace(/-/g, '+').replace(/_/g, '/'));
|
|
55
|
-
return JSON.parse(decoded);
|
|
83
|
+
return JSON.parse(base64urlDecodeString(body));
|
|
56
84
|
}
|
|
57
85
|
catch {
|
|
58
86
|
return null;
|
|
@@ -60,6 +88,80 @@ class HmacAttestationSigner {
|
|
|
60
88
|
}
|
|
61
89
|
}
|
|
62
90
|
exports.HmacAttestationSigner = HmacAttestationSigner;
|
|
91
|
+
// ─── Asymmetric Signer (RS256 / ES256) ──────────────────────────────────
|
|
92
|
+
/**
|
|
93
|
+
* Asymmetric JWT signer/verifier using Web Crypto (RS256 or ES256).
|
|
94
|
+
* Uses only crypto.subtle — no external dependencies.
|
|
95
|
+
*
|
|
96
|
+
* For signing (e.g. minting your own attestations):
|
|
97
|
+
* const signer = await AsymmetricAttestationSigner.fromKeyPair(privateKey, publicKey, 'RS256');
|
|
98
|
+
*
|
|
99
|
+
* For verification only (e.g. verifying incoming ID-JAGs from JWKS):
|
|
100
|
+
* const verifier = await AsymmetricAttestationSigner.fromPublicJwk(publicJwk, 'RS256');
|
|
101
|
+
*/
|
|
102
|
+
class AsymmetricAttestationSigner {
|
|
103
|
+
constructor(privateKey, publicKey, algorithm, ttlSeconds) {
|
|
104
|
+
this.privateKey = privateKey;
|
|
105
|
+
this.publicKey = publicKey;
|
|
106
|
+
this.algorithm = algorithm;
|
|
107
|
+
this.ttlSeconds = ttlSeconds;
|
|
108
|
+
}
|
|
109
|
+
// ─── Static factory methods ──────────────────────────────────────────────
|
|
110
|
+
/**
|
|
111
|
+
* Create a signing+verification instance from an already-imported key pair.
|
|
112
|
+
*/
|
|
113
|
+
static async fromKeyPair(privateKey, publicKey, algorithm, options) {
|
|
114
|
+
return new AsymmetricAttestationSigner(privateKey, publicKey, algorithm, options?.ttlSeconds ?? 300);
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Create a verification-only instance from a JSON Web Key.
|
|
118
|
+
* Calling sign() on this instance will throw.
|
|
119
|
+
*/
|
|
120
|
+
static async fromPublicJwk(jwk, algorithm) {
|
|
121
|
+
const importAlgo = algorithm === 'RS256'
|
|
122
|
+
? { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-256' }
|
|
123
|
+
: { name: 'ECDSA', namedCurve: 'P-256' };
|
|
124
|
+
const publicKey = await crypto.subtle.importKey('jwk', jwk, importAlgo, true, ['verify']);
|
|
125
|
+
return new AsymmetricAttestationSigner(null, publicKey, algorithm, 300);
|
|
126
|
+
}
|
|
127
|
+
// ─── Sign / Verify ────────────────────────────────────────────────────────────
|
|
128
|
+
async sign(payload) {
|
|
129
|
+
if (!this.privateKey) {
|
|
130
|
+
throw new Error('AsymmetricAttestationSigner: no private key — verification-only instance');
|
|
131
|
+
}
|
|
132
|
+
const header = base64urlEncode(JSON.stringify({ alg: this.algorithm, typ: 'JWT' }));
|
|
133
|
+
const body = base64urlEncode(JSON.stringify(payload));
|
|
134
|
+
const signingInput = `${header}.${body}`;
|
|
135
|
+
const data = new TextEncoder().encode(signingInput);
|
|
136
|
+
const algo = this.algorithm === 'RS256'
|
|
137
|
+
? 'RSASSA-PKCS1-v1_5'
|
|
138
|
+
: { name: 'ECDSA', hash: 'SHA-256' };
|
|
139
|
+
const sigBuf = await crypto.subtle.sign(algo, this.privateKey, data);
|
|
140
|
+
const sig = bufToBase64url(sigBuf);
|
|
141
|
+
return `${header}.${body}.${sig}`;
|
|
142
|
+
}
|
|
143
|
+
async verify(token) {
|
|
144
|
+
try {
|
|
145
|
+
const [header, body, sig] = token.split('.');
|
|
146
|
+
if (!header || !body || !sig)
|
|
147
|
+
return null;
|
|
148
|
+
const signingInput = `${header}.${body}`;
|
|
149
|
+
const data = new TextEncoder().encode(signingInput);
|
|
150
|
+
const sigBytes = base64urlToBuffer(sig);
|
|
151
|
+
const algo = this.algorithm === 'RS256'
|
|
152
|
+
? 'RSASSA-PKCS1-v1_5'
|
|
153
|
+
: { name: 'ECDSA', hash: 'SHA-256' };
|
|
154
|
+
const valid = await crypto.subtle.verify(algo, this.publicKey, sigBytes, data);
|
|
155
|
+
if (!valid)
|
|
156
|
+
return null;
|
|
157
|
+
return JSON.parse(base64urlDecodeString(body));
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
exports.AsymmetricAttestationSigner = AsymmetricAttestationSigner;
|
|
63
165
|
async function buildAttestation(ctx, resolved, options) {
|
|
64
166
|
const now = Math.floor(Date.now() / 1000);
|
|
65
167
|
const payload = {
|
|
@@ -76,7 +178,7 @@ async function buildAttestation(ctx, resolved, options) {
|
|
|
76
178
|
};
|
|
77
179
|
return options.signer.sign(payload);
|
|
78
180
|
}
|
|
79
|
-
// ─── Standalone verifyAttestation helper
|
|
181
|
+
// ─── Standalone verifyAttestation helper ──────────────────────────────────
|
|
80
182
|
async function verifyAttestation(token, signer) {
|
|
81
183
|
const raw = await signer.verify(token);
|
|
82
184
|
if (!raw)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"attestation.js","sourceRoot":"","sources":["../../src/attestation.ts"],"names":[],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"attestation.js","sourceRoot":"","sources":["../../src/attestation.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* ID-JAG verification utilities — validates the claims on a decoded ID-JAG
|
|
4
|
+
* payload against a TrustedProviderRegistry.
|
|
5
|
+
*
|
|
6
|
+
* Signature verification is left to the caller (requires a JWT library or
|
|
7
|
+
* Web Crypto with the provider's JWKS). This module validates claims only.
|
|
8
|
+
*
|
|
9
|
+
* @module identity-providers
|
|
10
|
+
*/
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.validateIdJagClaims = validateIdJagClaims;
|
|
13
|
+
// ─── validateIdJagClaims ──────────────────────────────────────────────────────
|
|
14
|
+
/**
|
|
15
|
+
* Validate ID-JAG claims (NOT signature — that's the caller's responsibility).
|
|
16
|
+
*
|
|
17
|
+
* Steps:
|
|
18
|
+
* 1. Find provider by payload.iss — return issuer_not_trusted if absent.
|
|
19
|
+
* 2. If provider.enabled === false — return provider_disabled.
|
|
20
|
+
* 3. If token is expired (with clock skew tolerance) — return expired.
|
|
21
|
+
* 4. If audience does not include the expected audience — return audience_mismatch.
|
|
22
|
+
* 5. If neither email_verified nor phone_number_verified — return missing_verified_identity.
|
|
23
|
+
* 6. If provider.requiredAmr is set and none of its values appear in payload.amr
|
|
24
|
+
* — return amr_not_satisfied.
|
|
25
|
+
* 7. Return { valid: true, provider }.
|
|
26
|
+
*
|
|
27
|
+
* @param payload Decoded JWT payload (signature NOT verified here)
|
|
28
|
+
* @param audience Expected aud (this service's authorization server URL)
|
|
29
|
+
* @param registry Configured trusted providers
|
|
30
|
+
* @param nowMs Current time in ms (injectable for testing; defaults to Date.now())
|
|
31
|
+
* @param clockSkewMs Accepted clock skew in ms (default: 120_000 = 2 minutes)
|
|
32
|
+
*/
|
|
33
|
+
function validateIdJagClaims(payload, audience, registry, nowMs, clockSkewMs) {
|
|
34
|
+
const now = nowMs ?? Date.now();
|
|
35
|
+
const skew = clockSkewMs ?? 120000;
|
|
36
|
+
// 1. Issuer lookup
|
|
37
|
+
const provider = registry.providers.find((p) => p.issuerUrl === payload.iss);
|
|
38
|
+
if (!provider) {
|
|
39
|
+
return {
|
|
40
|
+
valid: false,
|
|
41
|
+
error: 'issuer_not_trusted',
|
|
42
|
+
errorMessage: `Issuer '${payload.iss}' is not in the trusted provider registry`,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
// 2. Provider enabled check (undefined → enabled)
|
|
46
|
+
if (provider.enabled === false) {
|
|
47
|
+
return {
|
|
48
|
+
valid: false,
|
|
49
|
+
provider,
|
|
50
|
+
error: 'provider_disabled',
|
|
51
|
+
errorMessage: `Provider '${provider.label}' is currently disabled`,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
// 3. Expiry check (exp is in seconds; add clock skew tolerance)
|
|
55
|
+
if (payload.exp * 1000 < now - skew) {
|
|
56
|
+
return {
|
|
57
|
+
valid: false,
|
|
58
|
+
provider,
|
|
59
|
+
error: 'expired',
|
|
60
|
+
errorMessage: `Token expired at ${new Date(payload.exp * 1000).toISOString()}`,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
// 4. Audience check
|
|
64
|
+
const audiences = Array.isArray(payload.aud) ? payload.aud : [payload.aud];
|
|
65
|
+
if (!audiences.includes(audience)) {
|
|
66
|
+
return {
|
|
67
|
+
valid: false,
|
|
68
|
+
provider,
|
|
69
|
+
error: 'audience_mismatch',
|
|
70
|
+
errorMessage: `Expected audience '${audience}' not found in token aud claim`,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
// 5. Verified identity check — must have at least one verified identity claim
|
|
74
|
+
const hasVerifiedEmail = payload.email_verified === true;
|
|
75
|
+
const hasVerifiedPhone = payload.phone_number_verified === true;
|
|
76
|
+
if (!hasVerifiedEmail && !hasVerifiedPhone) {
|
|
77
|
+
return {
|
|
78
|
+
valid: false,
|
|
79
|
+
provider,
|
|
80
|
+
error: 'missing_verified_identity',
|
|
81
|
+
errorMessage: 'Token must have either email_verified=true or phone_number_verified=true',
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
// 6. AMR check
|
|
85
|
+
if (provider.requiredAmr && provider.requiredAmr.length > 0) {
|
|
86
|
+
const tokenAmr = payload.amr ?? [];
|
|
87
|
+
const satisfied = provider.requiredAmr.some((required) => tokenAmr.includes(required));
|
|
88
|
+
if (!satisfied) {
|
|
89
|
+
return {
|
|
90
|
+
valid: false,
|
|
91
|
+
provider,
|
|
92
|
+
error: 'amr_not_satisfied',
|
|
93
|
+
errorMessage: `Required AMR values [${provider.requiredAmr.join(', ')}] not found in token amr: [${tokenAmr.join(', ')}]`,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
// 7. All checks passed
|
|
98
|
+
return { valid: true, provider };
|
|
99
|
+
}
|
|
100
|
+
//# sourceMappingURL=identity-providers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"identity-providers.js","sourceRoot":"","sources":["../../src/identity-providers.ts"],"names":[],"mappings":";AAAA;;;;;;;;GAQG;;AA2DH,kDA+EC;AApGD,iFAAiF;AAEjF;;;;;;;;;;;;;;;;;;GAkBG;AACH,SAAgB,mBAAmB,CACjC,OAAqB,EACrB,QAAgB,EAChB,QAAiC,EACjC,KAAc,EACd,WAAoB;IAEpB,MAAM,GAAG,GAAG,KAAK,IAAI,IAAI,CAAC,GAAG,EAAE,CAAC;IAChC,MAAM,IAAI,GAAG,WAAW,IAAI,MAAO,CAAC;IAEpC,mBAAmB;IACnB,MAAM,QAAQ,GAAG,QAAQ,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,SAAS,KAAK,OAAO,CAAC,GAAG,CAAC,CAAC;IAC7E,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,OAAO;YACL,KAAK,EAAE,KAAK;YACZ,KAAK,EAAE,oBAAoB;YAC3B,YAAY,EAAE,WAAW,OAAO,CAAC,GAAG,2CAA2C;SAChF,CAAC;IACJ,CAAC;IAED,kDAAkD;IAClD,IAAI,QAAQ,CAAC,OAAO,KAAK,KAAK,EAAE,CAAC;QAC/B,OAAO;YACL,KAAK,EAAE,KAAK;YACZ,QAAQ;YACR,KAAK,EAAE,mBAAmB;YAC1B,YAAY,EAAE,aAAa,QAAQ,CAAC,KAAK,yBAAyB;SACnE,CAAC;IACJ,CAAC;IAED,gEAAgE;IAChE,IAAI,OAAO,CAAC,GAAG,GAAG,IAAI,GAAG,GAAG,GAAG,IAAI,EAAE,CAAC;QACpC,OAAO;YACL,KAAK,EAAE,KAAK;YACZ,QAAQ;YACR,KAAK,EAAE,SAAS;YAChB,YAAY,EAAE,oBAAoB,IAAI,IAAI,CAAC,OAAO,CAAC,GAAG,GAAG,IAAI,CAAC,CAAC,WAAW,EAAE,EAAE;SAC/E,CAAC;IACJ,CAAC;IAED,oBAAoB;IACpB,MAAM,SAAS,GAAG,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IAC3E,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC;QAClC,OAAO;YACL,KAAK,EAAE,KAAK;YACZ,QAAQ;YACR,KAAK,EAAE,mBAAmB;YAC1B,YAAY,EAAE,sBAAsB,QAAQ,gCAAgC;SAC7E,CAAC;IACJ,CAAC;IAED,8EAA8E;IAC9E,MAAM,gBAAgB,GAAG,OAAO,CAAC,cAAc,KAAK,IAAI,CAAC;IACzD,MAAM,gBAAgB,GAAG,OAAO,CAAC,qBAAqB,KAAK,IAAI,CAAC;IAChE,IAAI,CAAC,gBAAgB,IAAI,CAAC,gBAAgB,EAAE,CAAC;QAC3C,OAAO;YACL,KAAK,EAAE,KAAK;YACZ,QAAQ;YACR,KAAK,EAAE,2BAA2B;YAClC,YAAY,EAAE,0EAA0E;SACzF,CAAC;IACJ,CAAC;IAED,eAAe;IACf,IAAI,QAAQ,CAAC,WAAW,IAAI,QAAQ,CAAC,WAAW,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC5D,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,IAAI,EAAE,CAAC;QACnC,MAAM,SAAS,GAAG,QAAQ,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAC;QACvF,IAAI,CAAC,SAAS,EAAE,CAAC;YACf,OAAO;gBACL,KAAK,EAAE,KAAK;gBACZ,QAAQ;gBACR,KAAK,EAAE,mBAAmB;gBAC1B,YAAY,EAAE,wBAAwB,QAAQ,CAAC,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,8BAA8B,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG;aAC1H,CAAC;QACJ,CAAC;IACH,CAAC;IAED,uBAAuB;IACvB,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC;AACnC,CAAC"}
|
package/dist/cjs/index.js
CHANGED
|
@@ -30,6 +30,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
30
30
|
};
|
|
31
31
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
32
32
|
// ─── Runtime modules (classes, functions, const) ─────────────────────────────
|
|
33
|
+
// Core router + built-in stores
|
|
33
34
|
__exportStar(require("./router"), exports);
|
|
34
35
|
__exportStar(require("./providers"), exports);
|
|
35
36
|
__exportStar(require("./credentials"), exports);
|
|
@@ -39,4 +40,8 @@ __exportStar(require("./attestation"), exports);
|
|
|
39
40
|
__exportStar(require("./approval"), exports);
|
|
40
41
|
__exportStar(require("./budget"), exports);
|
|
41
42
|
__exportStar(require("./federation"), exports);
|
|
43
|
+
// auth.md compatibility — identity providers, revocation, and claim lifecycle
|
|
44
|
+
__exportStar(require("./identity-providers"), exports);
|
|
45
|
+
__exportStar(require("./revocation"), exports);
|
|
46
|
+
__exportStar(require("./revocation-listener"), exports);
|
|
42
47
|
//# sourceMappingURL=index.js.map
|
package/dist/cjs/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;GAcG;;;;;;;;;;;;;;;;AAOH,gFAAgF;AAChF,2CAAyB;AACzB,8CAA4B;AAC5B,gDAA8B;AAC9B,6CAA2B;AAC3B,6CAA2B;AAC3B,gDAA8B;AAC9B,6CAA2B;AAC3B,2CAAyB;AACzB,+CAA6B"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;GAcG;;;;;;;;;;;;;;;;AAOH,gFAAgF;AAChF,gCAAgC;AAChC,2CAAyB;AACzB,8CAA4B;AAC5B,gDAA8B;AAC9B,6CAA2B;AAC3B,6CAA2B;AAC3B,gDAA8B;AAC9B,6CAA2B;AAC3B,2CAAyB;AACzB,+CAA6B;AAE7B,8EAA8E;AAC9E,uDAAqC;AACrC,+CAA6B;AAC7B,wDAAsC"}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* RevocationListener — framework-agnostic inbound revocation handler.
|
|
4
|
+
*
|
|
5
|
+
* The listener does NOT handle JWKS fetching or JWT signature verification —
|
|
6
|
+
* those depend on external libraries. Pass a LogoutJwtVerifier that handles
|
|
7
|
+
* verification for your environment, then wire handleRequest() into your router.
|
|
8
|
+
*
|
|
9
|
+
* Express example:
|
|
10
|
+
* app.post('/agent/auth/revoke', async (req, res) => {
|
|
11
|
+
* const result = await listener.handleRequest(req.body, req.headers);
|
|
12
|
+
* res.status(result.httpStatus).json(result.body);
|
|
13
|
+
* });
|
|
14
|
+
*
|
|
15
|
+
* Fastify example:
|
|
16
|
+
* fastify.post('/agent/auth/revoke', async (req, reply) => {
|
|
17
|
+
* const result = await listener.handleRequest(req.body as string, req.headers);
|
|
18
|
+
* reply.code(result.httpStatus).send(result.body);
|
|
19
|
+
* });
|
|
20
|
+
*
|
|
21
|
+
* @module revocation-listener
|
|
22
|
+
*/
|
|
23
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
24
|
+
exports.RevocationListener = void 0;
|
|
25
|
+
// ─── RevocationListener ──────────────────────────────────────────────────
|
|
26
|
+
class RevocationListener {
|
|
27
|
+
constructor(opts) {
|
|
28
|
+
this.opts = opts;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Handle a raw revocation request body and headers.
|
|
32
|
+
*
|
|
33
|
+
* Processing steps:
|
|
34
|
+
* 1. Validate Content-Type contains 'application/logout+jwt'.
|
|
35
|
+
* 2. Call verifier.verify(rawBody) — if null, reject with 400.
|
|
36
|
+
* 3. Call handler.process(payload) — if replay, return 200 with 0 revoked.
|
|
37
|
+
* 4. Return 200 with credentialsRevoked count.
|
|
38
|
+
*
|
|
39
|
+
* @param rawBody The request body string (the logout+jwt token itself,
|
|
40
|
+
* per Content-Type: application/logout+jwt)
|
|
41
|
+
* @param headers Request headers (for Content-Type validation)
|
|
42
|
+
*/
|
|
43
|
+
async handleRequest(rawBody, headers) {
|
|
44
|
+
// 1. Content-Type validation
|
|
45
|
+
const contentType = headers['content-type'] ?? headers['Content-Type'] ?? '';
|
|
46
|
+
const ctString = Array.isArray(contentType) ? contentType[0] : contentType;
|
|
47
|
+
if (!ctString.includes('application/logout+jwt')) {
|
|
48
|
+
return {
|
|
49
|
+
httpStatus: 400,
|
|
50
|
+
body: { error: 'invalid_content_type' },
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
// 2. Signature verification (delegated to caller-supplied verifier)
|
|
54
|
+
const payload = await this.opts.verifier.verify(rawBody);
|
|
55
|
+
if (!payload) {
|
|
56
|
+
return {
|
|
57
|
+
httpStatus: 400,
|
|
58
|
+
body: { error: 'invalid_logout_token' },
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
// 3. Process revocation (includes replay detection)
|
|
62
|
+
const result = await this.opts.handler.process(payload);
|
|
63
|
+
// Replay: return 200 with 0 revoked (idempotent)
|
|
64
|
+
if (result.replay) {
|
|
65
|
+
return {
|
|
66
|
+
httpStatus: 200,
|
|
67
|
+
body: { status: 'ok', credentialsRevoked: 0 },
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
// 4. Success
|
|
71
|
+
return {
|
|
72
|
+
httpStatus: 200,
|
|
73
|
+
body: { status: 'ok', credentialsRevoked: result.credentialsRevoked },
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
exports.RevocationListener = RevocationListener;
|
|
78
|
+
//# sourceMappingURL=revocation-listener.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"revocation-listener.js","sourceRoot":"","sources":["../../src/revocation-listener.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;;;AA0BH,4EAA4E;AAE5E,MAAa,kBAAkB;IAC7B,YAA6B,IAA+B;QAA/B,SAAI,GAAJ,IAAI,CAA2B;IAAG,CAAC;IAEhE;;;;;;;;;;;;OAYG;IACH,KAAK,CAAC,aAAa,CACjB,OAAe,EACf,OAAsD;QAEtD,6BAA6B;QAC7B,MAAM,WAAW,GAAG,OAAO,CAAC,cAAc,CAAC,IAAI,OAAO,CAAC,cAAc,CAAC,IAAI,EAAE,CAAC;QAC7E,MAAM,QAAQ,GAAG,KAAK,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC;QAC3E,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,wBAAwB,CAAC,EAAE,CAAC;YACjD,OAAO;gBACL,UAAU,EAAE,GAAG;gBACf,IAAI,EAAE,EAAE,KAAK,EAAE,sBAAsB,EAAE;aACxC,CAAC;QACJ,CAAC;QAED,oEAAoE;QACpE,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;QACzD,IAAI,CAAC,OAAO,EAAE,CAAC;YACb,OAAO;gBACL,UAAU,EAAE,GAAG;gBACf,IAAI,EAAE,EAAE,KAAK,EAAE,sBAAsB,EAAE;aACxC,CAAC;QACJ,CAAC;QAED,oDAAoD;QACpD,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;QAExD,iDAAiD;QACjD,IAAI,MAAM,CAAC,MAAM,EAAE,CAAC;YAClB,OAAO;gBACL,UAAU,EAAE,GAAG;gBACf,IAAI,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,kBAAkB,EAAE,CAAC,EAAE;aAC9C,CAAC;QACJ,CAAC;QAED,aAAa;QACb,OAAO;YACL,UAAU,EAAE,GAAG;YACf,IAAI,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,kBAAkB,EAAE,MAAM,CAAC,kBAAkB,EAAE;SACtE,CAAC;IACJ,CAAC;CACF;AAxDD,gDAwDC"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Inbound revocation handler — receives logout+jwt tokens from identity
|
|
4
|
+
* providers and propagates revocation to the CredentialStore.
|
|
5
|
+
*
|
|
6
|
+
* This module validates the logout+jwt STRUCTURE (does NOT verify the
|
|
7
|
+
* signature). The caller (e.g. an Express/Fastify route handler) is
|
|
8
|
+
* responsible for JWKS-based signature verification before passing the
|
|
9
|
+
* decoded payload here.
|
|
10
|
+
*
|
|
11
|
+
* @module revocation
|
|
12
|
+
*/
|
|
13
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
exports.RevocationHandler = void 0;
|
|
15
|
+
// ─── RevocationHandler ─────────────────────────────────────────────────────
|
|
16
|
+
/**
|
|
17
|
+
* RevocationHandler validates and processes inbound logout tokens.
|
|
18
|
+
*
|
|
19
|
+
* Usage:
|
|
20
|
+
* const handler = new RevocationHandler(store);
|
|
21
|
+
* // In your route: const payload = await verifyLogoutJwt(token, jwks); // caller's job
|
|
22
|
+
* const result = await handler.process(payload);
|
|
23
|
+
*
|
|
24
|
+
* The handler keeps an in-memory jti replay cache with configurable TTL.
|
|
25
|
+
* Stale entries are evicted lazily on each process() call.
|
|
26
|
+
*/
|
|
27
|
+
class RevocationHandler {
|
|
28
|
+
constructor(store, options) {
|
|
29
|
+
this.store = store;
|
|
30
|
+
/**
|
|
31
|
+
* jti → processed-at timestamp (ms).
|
|
32
|
+
* Evict entries older than maxAgeMs.
|
|
33
|
+
*/
|
|
34
|
+
this.seen = new Map();
|
|
35
|
+
this.maxAgeMs = options?.maxAgeMs ?? 10 * 60 * 1000; // 10 minutes default
|
|
36
|
+
}
|
|
37
|
+
async process(payload) {
|
|
38
|
+
this.evictStale();
|
|
39
|
+
// Replay detection
|
|
40
|
+
if (this.seen.has(payload.jti)) {
|
|
41
|
+
return { jti: payload.jti, credentialsRevoked: 0, replay: true };
|
|
42
|
+
}
|
|
43
|
+
this.seen.set(payload.jti, Date.now());
|
|
44
|
+
// Propagate revocation to the store (optional method; graceful if absent)
|
|
45
|
+
const count = this.store.revokeByIdentity
|
|
46
|
+
? await this.store.revokeByIdentity(payload.iss, payload.sub, payload.aud)
|
|
47
|
+
: 0;
|
|
48
|
+
return { jti: payload.jti, credentialsRevoked: count, replay: false };
|
|
49
|
+
}
|
|
50
|
+
evictStale() {
|
|
51
|
+
const cutoff = Date.now() - this.maxAgeMs;
|
|
52
|
+
for (const [jti, ts] of this.seen) {
|
|
53
|
+
if (ts < cutoff)
|
|
54
|
+
this.seen.delete(jti);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
exports.RevocationHandler = RevocationHandler;
|
|
59
|
+
//# sourceMappingURL=revocation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"revocation.js","sourceRoot":"","sources":["../../src/revocation.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;GAUG;;;AAsBH,8EAA8E;AAE9E;;;;;;;;;;GAUG;AACH,MAAa,iBAAiB;IAQ5B,YACmB,KAAsB,EACvC,OAA+B;QADd,UAAK,GAAL,KAAK,CAAiB;QARzC;;;WAGG;QACc,SAAI,GAAG,IAAI,GAAG,EAAkB,CAAC;QAOhD,IAAI,CAAC,QAAQ,GAAG,OAAO,EAAE,QAAQ,IAAI,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC,qBAAqB;IAC5E,CAAC;IAED,KAAK,CAAC,OAAO,CAAC,OAA2B;QACvC,IAAI,CAAC,UAAU,EAAE,CAAC;QAElB,mBAAmB;QACnB,IAAI,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;YAC/B,OAAO,EAAE,GAAG,EAAE,OAAO,CAAC,GAAG,EAAE,kBAAkB,EAAE,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;QACnE,CAAC;QACD,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,GAAG,EAAE,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC;QAEvC,0EAA0E;QAC1E,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,gBAAgB;YACvC,CAAC,CAAC,MAAM,IAAI,CAAC,KAAK,CAAC,gBAAgB,CAAC,OAAO,CAAC,GAAG,EAAE,OAAO,CAAC,GAAG,EAAE,OAAO,CAAC,GAAG,CAAC;YAC1E,CAAC,CAAC,CAAC,CAAC;QAEN,OAAO,EAAE,GAAG,EAAE,OAAO,CAAC,GAAG,EAAE,kBAAkB,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC;IACxE,CAAC;IAEO,UAAU;QAChB,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,QAAQ,CAAC;QAC1C,KAAK,MAAM,CAAC,GAAG,EAAE,EAAE,CAAC,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC;YAClC,IAAI,EAAE,GAAG,MAAM;gBAAE,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QACzC,CAAC;IACH,CAAC;CACF;AAtCD,8CAsCC"}
|
package/dist/cjs/rotation.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.CredentialRotationScheduler = void 0;
|
|
4
|
-
// ─── CredentialRotationScheduler
|
|
4
|
+
// ─── CredentialRotationScheduler ───────────────────────────────────────────────
|
|
5
5
|
class CredentialRotationScheduler {
|
|
6
6
|
constructor(repository, auditLogger) {
|
|
7
7
|
this.repository = repository;
|
|
@@ -20,8 +20,13 @@ class CredentialRotationScheduler {
|
|
|
20
20
|
const credentials = await this.repository.listActive();
|
|
21
21
|
const now = new Date();
|
|
22
22
|
for (const cred of credentials) {
|
|
23
|
+
// Skip credentials with no rotation policy
|
|
23
24
|
if (!cred.rotation)
|
|
24
25
|
continue;
|
|
26
|
+
// Skip unclaimed auth.md credentials — they cannot be rotated until
|
|
27
|
+
// the claim ceremony is complete and status flips to 'active'
|
|
28
|
+
if (cred.status === 'unclaimed')
|
|
29
|
+
continue;
|
|
25
30
|
const due = this.isRotationDue(cred, cred.rotation, now);
|
|
26
31
|
if (!due) {
|
|
27
32
|
await this.maybeEmitWarning(cred, cred.rotation, now);
|