@atlasauth/pca-gnap 0.0.0-stage → 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/dist/index.js ADDED
@@ -0,0 +1,697 @@
1
+ "use strict";
2
+ /**
3
+ * @atlasauth/pca-gnap — a GNAP (RFC 9635) bridge for Proof-Carrying Authority, defining the
4
+ * ================= AGENT-GNAP PROFILE =================
5
+ *
6
+ * GNAP (Grant Negotiation and Authorization Protocol, RFC 9635) negotiates an access grant between a
7
+ * client instance and an authorization server (AS) using a rich, structured `access` rights model,
8
+ * key-bound requests, and a stateful *continuation* handle. No agent-delegation profile of GNAP exists
9
+ * publicly yet. This package DEFINES one, because GNAP's primitives map almost 1:1 onto PCA's:
10
+ *
11
+ * GNAP concept PCA concept
12
+ * ------------------------------------ ---------------------------------------------------------
13
+ * `access` rights array (§8) Policy-envelope `Predicate[]` (verb/resource/where)
14
+ * - type → resource namespace `gnap:<type>:<location>`
15
+ * - actions → predicate `verb` (list, or `*`)
16
+ * - locations → predicate `resource` (exact / prefix)
17
+ * - datatypes / identifier → predicate `where` conditions on `action.params.*`
18
+ * client instance key (§7.1) Capability `holder` (the bound key, `cnf`)
19
+ * grant request (§2) `mintGrant` (root) / `delegate` (a delegated hop)
20
+ * grant response access_token (§3.2) A verifiable PCA capability chain (bound, attenuated)
21
+ * `continue` handle (§3.1, §5) A PCA step-up: a FROST/CIBA co-signature folded into a PCActn
22
+ * `interact` (§3.3, §4) The tier-2/3 human/guardian step-up prompt
23
+ * key-bound proof `jwsd` (§7.3.4) Detached-JWS proof that the request was signed by `holder`
24
+ *
25
+ * THE AGENT-GNAP PROFILE (multi-hop delegation). An agent-to-agent (A2A) task is a chain of grants:
26
+ * each hop is a GNAP grant request from one agent to the next, and the AS issues a PCA capability that
27
+ * is ATTENUATED — never wider than the issuing agent's own authority. Attenuation is structural: PCA
28
+ * capability caveats are append-only and conjunctive (see `@atlasauth/pca` capability.ts), so a child
29
+ * can only ever NARROW. Each hop records its granted `access` as a signed `gnap_access` caveat, and the
30
+ * EFFECTIVE authority of the chain is the INTERSECTION of the root policy envelope and every hop's
31
+ * recorded rights. A hop that requests more than its issuer held is clamped at issue time
32
+ * (`clampToIssuer`) AND denied at action time (the issuer's narrower rights still gate every action),
33
+ * so "never wider than the issuer" holds by construction across any number of hops.
34
+ *
35
+ * All verification FAILS CLOSED: a malformed token, a broken chain, a bad jwsd proof, a stale request,
36
+ * or an action outside the granted rights denies. The machine-readable profile spec is exported as
37
+ * {@link AGENT_GNAP_PROFILE}.
38
+ */
39
+ Object.defineProperty(exports, "__esModule", { value: true });
40
+ exports.AGENT_GNAP_PROFILE = exports.GNAP_ACCESS_CAVEAT = exports.GNAP_GRANT_CAVEAT = exports.GnapError = void 0;
41
+ exports.gnapActionResource = gnapActionResource;
42
+ exports.normalizeAccess = normalizeAccess;
43
+ exports.accessToPredicates = accessToPredicates;
44
+ exports.accessContains = accessContains;
45
+ exports.accessWithin = accessWithin;
46
+ exports.clampAccess = clampAccess;
47
+ exports.grantedAccessOf = grantedAccessOf;
48
+ exports.resolveHolder = resolveHolder;
49
+ exports.holderJwk = holderJwk;
50
+ exports.grantRequestToCapability = grantRequestToCapability;
51
+ exports.authorizeGnapAction = authorizeGnapAction;
52
+ exports.encodeBoundToken = encodeBoundToken;
53
+ exports.capabilityToGrantResponse = capabilityToGrantResponse;
54
+ exports.verifyGnapToken = verifyGnapToken;
55
+ exports.signGnapRequest = signGnapRequest;
56
+ exports.verifyGnapRequest = verifyGnapRequest;
57
+ exports.gnapRequestRequiresStepUp = gnapRequestRequiresStepUp;
58
+ exports.beginGnapContinuation = beginGnapContinuation;
59
+ exports.gnapStepUpShare = gnapStepUpShare;
60
+ exports.completeGnapContinuation = completeGnapContinuation;
61
+ const jose_1 = require("jose");
62
+ const pca_1 = require("@atlasauth/pca");
63
+ // ============================================================================================
64
+ // Errors + small guards
65
+ // ============================================================================================
66
+ /** Thrown by the issuance paths on a malformed request (the verify paths fail closed instead). */
67
+ class GnapError extends Error {
68
+ constructor(message) {
69
+ super(message);
70
+ this.name = 'GnapError';
71
+ }
72
+ }
73
+ exports.GnapError = GnapError;
74
+ const isRecord = (x) => x !== null && typeof x === 'object' && !Array.isArray(x);
75
+ const strArr = (u) => Array.isArray(u) ? u.filter((s) => typeof s === 'string') : undefined;
76
+ /** A canonical, unpadded base64url Ed25519 public key (exactly 32 bytes), or null. */
77
+ function asEd25519(x) {
78
+ return typeof x === 'string' && (0, pca_1.decodeB64uStrict)(x, 32) !== null ? x : null;
79
+ }
80
+ const GNAP_RESOURCE_PREFIX = 'gnap';
81
+ /** The PCA `resource` string for a GNAP (type, location) pair — the namespace actions are matched in. */
82
+ function gnapActionResource(type, location) {
83
+ return `${GNAP_RESOURCE_PREFIX}:${type}:${location}`;
84
+ }
85
+ // ============================================================================================
86
+ // Access-rights normalization + mapping onto PCA predicates
87
+ // ============================================================================================
88
+ /** Total, fail-closed normalization of one access item. Returns null for anything malformed. */
89
+ function tryNormalizeAccessItem(item) {
90
+ if (typeof item === 'string')
91
+ return item.length > 0 ? { type: item } : null;
92
+ if (!isRecord(item))
93
+ return null;
94
+ const type = item.type;
95
+ if (typeof type !== 'string' || type.length === 0)
96
+ return null;
97
+ const out = { type };
98
+ const actions = strArr(item.actions);
99
+ if (actions && actions.length > 0)
100
+ out.actions = actions;
101
+ const locations = strArr(item.locations);
102
+ if (locations && locations.length > 0)
103
+ out.locations = locations;
104
+ const datatypes = strArr(item.datatypes);
105
+ if (datatypes && datatypes.length > 0)
106
+ out.datatypes = datatypes;
107
+ if (typeof item.identifier === 'string')
108
+ out.identifier = item.identifier;
109
+ const privileges = strArr(item.privileges);
110
+ if (privileges && privileges.length > 0)
111
+ out.privileges = privileges;
112
+ return out;
113
+ }
114
+ /** Normalize one access item, THROWING on malformed input (issuance path). */
115
+ function normalizeAccessItem(item) {
116
+ const n = tryNormalizeAccessItem(item);
117
+ if (n === null)
118
+ throw new GnapError('malformed access item (needs a non-empty string `type`)');
119
+ return n;
120
+ }
121
+ /** Normalize an access array for issuance (throws on any malformed entry). */
122
+ function normalizeAccess(items) {
123
+ if (!Array.isArray(items))
124
+ throw new GnapError('access must be an array');
125
+ return items.map(normalizeAccessItem);
126
+ }
127
+ /** Total, fail-closed read of a stored access array (reading signed caveats). Skips malformed entries. */
128
+ function readAccessArray(u) {
129
+ if (!Array.isArray(u))
130
+ return [];
131
+ const out = [];
132
+ for (const item of u) {
133
+ const n = tryNormalizeAccessItem(item);
134
+ if (n !== null)
135
+ out.push(n);
136
+ }
137
+ return out;
138
+ }
139
+ /**
140
+ * Map a GNAP access right onto PCA predicates (one per location). `type` namespaces the resource,
141
+ * `actions` become verbs, `locations` become resources, and `datatypes`/`identifier` become `where`
142
+ * conditions on the action's params. An absent `actions`/`locations` means "any" (verb `*` / a
143
+ * prefix-matching `*` resource).
144
+ */
145
+ function accessToPredicates(access) {
146
+ const verb = access.actions && access.actions.length > 0 ? [...access.actions] : '*';
147
+ const locations = access.locations && access.locations.length > 0 ? access.locations : ['*'];
148
+ const where = [];
149
+ if (access.datatypes && access.datatypes.length > 0) {
150
+ where.push({ field: 'action.params.datatype', op: 'in', value: [...access.datatypes] });
151
+ }
152
+ if (typeof access.identifier === 'string') {
153
+ where.push({ field: 'action.params.identifier', op: 'eq', value: access.identifier });
154
+ }
155
+ return locations.map((loc) => {
156
+ const p = { verb, resource: gnapActionResource(access.type, loc) };
157
+ if (where.length > 0)
158
+ p.where = where.map((c) => ({ ...c }));
159
+ return p;
160
+ });
161
+ }
162
+ /** All predicates for an access array. */
163
+ function accessArrayToPredicates(access) {
164
+ return access.flatMap(accessToPredicates);
165
+ }
166
+ // ============================================================================================
167
+ // Access-level containment (the AS's "is this request within my authority?" check)
168
+ // ============================================================================================
169
+ function coversStrings(sup, sub) {
170
+ if (sup === undefined)
171
+ return true; // issuer allows all
172
+ if (sub === undefined)
173
+ return false; // request wants all, issuer has a concrete set
174
+ return sub.every((s) => sup.includes(s));
175
+ }
176
+ function coversLocations(sup, sub) {
177
+ if (sup === undefined)
178
+ return true;
179
+ if (sub === undefined)
180
+ return sup.includes('*');
181
+ return sub.every((s) => sup.some((g) => g === s || g === '*' || (g.endsWith('*') && s.startsWith(g.slice(0, -1)))));
182
+ }
183
+ /** Does the issuer's granted right `g` fully contain the requested right `r`? */
184
+ function accessContains(g, r) {
185
+ if (g.type !== r.type)
186
+ return false;
187
+ if (!coversStrings(g.actions, r.actions))
188
+ return false;
189
+ if (!coversLocations(g.locations, r.locations))
190
+ return false;
191
+ if (!coversStrings(g.datatypes, r.datatypes))
192
+ return false;
193
+ if (r.identifier !== undefined && g.identifier !== undefined && g.identifier !== r.identifier)
194
+ return false;
195
+ return true;
196
+ }
197
+ /** Is `r` within the issuer's granted set `G` (covered by at least one of its rights)? */
198
+ function accessWithin(G, r) {
199
+ return G.some((g) => accessContains(g, r));
200
+ }
201
+ /** Split a requested access array into the subset within the issuer's authority and the rest. */
202
+ function clampAccess(issuerGranted, requested) {
203
+ const granted = [];
204
+ const dropped = [];
205
+ for (const r of requested)
206
+ (accessWithin(issuerGranted, r) ? granted : dropped).push(r);
207
+ return { granted, dropped };
208
+ }
209
+ // ============================================================================================
210
+ // GNAP provenance caveats (recorded in the signed capability)
211
+ // ============================================================================================
212
+ /** The caveat recorded inside a ROOT GNAP grant's policy envelope (grant provenance). */
213
+ exports.GNAP_GRANT_CAVEAT = 'gnap_grant';
214
+ /** The caveat appended on each DELEGATED GNAP hop (the rights that hop was granted). */
215
+ exports.GNAP_ACCESS_CAVEAT = 'gnap_access';
216
+ /** Read the root grant's GNAP provenance caveat (inside the policy envelope), or null. */
217
+ function readGnapGrant(root) {
218
+ const env = (0, pca_1.readEnvelope)(root);
219
+ const cv = env?.caveats.find((c) => c?.type === exports.GNAP_GRANT_CAVEAT);
220
+ if (!cv)
221
+ return null;
222
+ const clientId = cv.client_id;
223
+ const digest = cv.request_digest;
224
+ if (typeof clientId !== 'string' || typeof digest !== 'string')
225
+ return null;
226
+ return {
227
+ type: exports.GNAP_GRANT_CAVEAT,
228
+ client_id: clientId,
229
+ granted_access: readAccessArray(cv.granted_access),
230
+ request_digest: digest,
231
+ interacted: cv.interacted === true,
232
+ ts: typeof cv.ts === 'number' ? cv.ts : 0,
233
+ };
234
+ }
235
+ /**
236
+ * The EFFECTIVE granted access of a chain: the most-recent delegated hop's recorded rights, or (for a
237
+ * root-only grant) the root's recorded rights. Because every hop is clamped to its issuer at issue
238
+ * time, the leaf's recorded rights are the narrowest in the chain.
239
+ */
240
+ function grantedAccessOf(chain) {
241
+ if (!Array.isArray(chain) || chain.length === 0)
242
+ return [];
243
+ const leaf = chain[chain.length - 1];
244
+ const caveats = Array.isArray(leaf.caveats) ? leaf.caveats : [];
245
+ for (let i = caveats.length - 1; i >= 0; i--) {
246
+ const cv = caveats[i];
247
+ if (cv?.type === exports.GNAP_ACCESS_CAVEAT)
248
+ return readAccessArray(cv.access);
249
+ }
250
+ const grant = readGnapGrant(chain[0]);
251
+ return grant ? grant.granted_access : [];
252
+ }
253
+ // ============================================================================================
254
+ // Holder resolution (the GNAP client key becomes the PCA holder)
255
+ // ============================================================================================
256
+ /** Resolve the b64u Ed25519 holder key from a request's client key (or an explicit override). */
257
+ function resolveHolder(req, holderOverride) {
258
+ if (holderOverride !== undefined) {
259
+ const h = asEd25519(holderOverride);
260
+ if (h === null)
261
+ throw new GnapError('holder override is not a canonical base64url Ed25519 public key');
262
+ return h;
263
+ }
264
+ const client = req.client;
265
+ const key = typeof client === 'object' && client !== null ? client.key : undefined;
266
+ if (key && typeof key === 'object' && key.jwk && key.jwk.kty === 'OKP' && key.jwk.crv === 'Ed25519') {
267
+ const x = asEd25519(key.jwk.x);
268
+ if (x !== null)
269
+ return x;
270
+ }
271
+ throw new GnapError('cannot resolve holder: supply client.key.jwk (OKP/Ed25519) or a holder override');
272
+ }
273
+ /** The holder key, rendered as a GNAP JWK (for a bound access-token response). */
274
+ function holderJwk(holder) {
275
+ return { kty: 'OKP', crv: 'Ed25519', x: holder, use: 'sig', alg: 'EdDSA' };
276
+ }
277
+ function accessOf(req) {
278
+ const at = req.access_token;
279
+ if (isRecord(at) && Array.isArray(at.access))
280
+ return at.access;
281
+ throw new GnapError('grant request must carry access_token.access (single-token request)');
282
+ }
283
+ function requestDigestOf(holder, granted, interact) {
284
+ return (0, pca_1.hashCanonical)({
285
+ d: 'atlas-pca/gnap/request/v1',
286
+ client_id: holder,
287
+ access: granted,
288
+ interact: interact ?? null,
289
+ });
290
+ }
291
+ /**
292
+ * Convert a GNAP grant request into a PCA capability bound to the client key, ATTENUATED to never
293
+ * exceed the issuer. With no `parent` this MINTS a root grant whose policy-envelope predicates ARE the
294
+ * requested access rights. With a `parent` it DELEGATES a narrower hop, recording the (clamped) rights
295
+ * as an append-only `gnap_access` caveat — so the effective authority is the intersection of the whole
296
+ * chain, and no hop can widen.
297
+ */
298
+ function grantRequestToCapability(req, opts) {
299
+ const holder = resolveHolder(req, opts.holder);
300
+ const requested = normalizeAccess(accessOf(req));
301
+ const now = Math.floor(opts.now ?? Date.now());
302
+ const interacted = opts.interacted === true;
303
+ let granted = requested;
304
+ let dropped = [];
305
+ if (opts.parent) {
306
+ const issuerChain = opts.issuerChain ?? [opts.parent];
307
+ if (opts.clampToIssuer !== false) {
308
+ const split = clampAccess(grantedAccessOf(issuerChain), requested);
309
+ granted = split.granted;
310
+ dropped = split.dropped;
311
+ }
312
+ }
313
+ const requestDigest = requestDigestOf(holder, granted, req.interact);
314
+ if (opts.parent) {
315
+ const issuerChain = opts.issuerChain ?? [opts.parent];
316
+ const provenance = {
317
+ type: exports.GNAP_ACCESS_CAVEAT,
318
+ access: granted,
319
+ predicates: accessArrayToPredicates(granted),
320
+ request_digest: requestDigest,
321
+ client_id: holder,
322
+ ts: now,
323
+ };
324
+ const child = (0, pca_1.delegate)(opts.parent, holder, [provenance], opts.issuerSecret);
325
+ return { grant: child, chain: [...issuerChain, child], holder, grantedAccess: granted, droppedAccess: dropped, provenance };
326
+ }
327
+ const provenance = {
328
+ type: exports.GNAP_GRANT_CAVEAT,
329
+ client_id: holder,
330
+ granted_access: granted,
331
+ request_digest: requestDigest,
332
+ interacted,
333
+ ts: now,
334
+ };
335
+ const { grant } = (0, pca_1.mintGrant)({
336
+ principalSecret: opts.issuerSecret,
337
+ principalPublic: opts.issuerPublic,
338
+ holder,
339
+ goal: opts.goal ?? `gnap:grant:${holder}`,
340
+ envelope: {
341
+ predicates: accessArrayToPredicates(granted),
342
+ caveats: [provenance],
343
+ agent_binding: opts.agentBinding ?? {},
344
+ risk_policy: opts.riskPolicy ?? pca_1.DEFAULT_RISK_POLICY,
345
+ },
346
+ ...(opts.salt !== undefined ? { salt: opts.salt } : {}),
347
+ });
348
+ return { grant, chain: [grant], holder, grantedAccess: granted, droppedAccess: dropped, provenance };
349
+ }
350
+ /**
351
+ * Decide whether `action` is authorized by a GNAP-issued capability chain. FAILS CLOSED. The chain must
352
+ * verify structurally, the root policy envelope must permit the action, AND every delegated hop's
353
+ * recorded `gnap_access` rights must permit it too (the per-hop intersection that enforces "never wider
354
+ * than the issuer" across the whole chain).
355
+ */
356
+ function authorizeGnapAction(chain, action, opts = {}) {
357
+ if (!Array.isArray(chain) || chain.length === 0)
358
+ return { allowed: false, reason: 'empty chain' };
359
+ const vc = (0, pca_1.verifyChain)(chain, opts.expectedRootIssuer);
360
+ if (!vc.ok)
361
+ return { allowed: false, reason: `capability chain invalid: ${vc.reason ?? 'unknown'}` };
362
+ const env = (0, pca_1.readEnvelope)(chain[0]);
363
+ if (!env)
364
+ return { allowed: false, reason: 'root grant carries no policy envelope' };
365
+ const ctx = {
366
+ action: {
367
+ verb: action.verb,
368
+ resource: action.resource,
369
+ ...(action.params !== undefined ? { params: action.params } : {}),
370
+ ...(action.reversibility_class !== undefined ? { reversibility_class: action.reversibility_class } : {}),
371
+ },
372
+ };
373
+ const rootDecision = (0, pca_1.evaluatePredicates)(env.predicates, ctx);
374
+ if (!rootDecision.allowed) {
375
+ return { allowed: false, reason: `root authority denies: ${rootDecision.reason ?? 'no predicate matched'}` };
376
+ }
377
+ const leaf = chain[chain.length - 1];
378
+ const caveats = Array.isArray(leaf.caveats) ? leaf.caveats : [];
379
+ for (const cv of caveats) {
380
+ if (cv?.type !== exports.GNAP_ACCESS_CAVEAT)
381
+ continue;
382
+ const hopPreds = accessArrayToPredicates(readAccessArray(cv.access));
383
+ const hopDecision = (0, pca_1.evaluatePredicates)(hopPreds, ctx);
384
+ if (!hopDecision.allowed) {
385
+ return { allowed: false, reason: `delegated hop denies: ${hopDecision.reason ?? 'no predicate matched'}` };
386
+ }
387
+ }
388
+ return { allowed: true };
389
+ }
390
+ // ============================================================================================
391
+ // Bound token: PCA capability chain ↔ GNAP access token
392
+ // ============================================================================================
393
+ /** Encode a capability chain as a GNAP access-token `value` (self-contained, re-verified on use). */
394
+ function encodeBoundToken(chain) {
395
+ return (0, pca_1.b64u)((0, pca_1.utf8)(JSON.stringify(chain)));
396
+ }
397
+ /** Structural guard: the required shape of a serialized capability hop (verifyChain re-checks crypto). */
398
+ function isCapabilityShape(x) {
399
+ if (!isRecord(x))
400
+ return false;
401
+ return (typeof x.id === 'string' &&
402
+ typeof x.issuer === 'string' &&
403
+ typeof x.holder === 'string' &&
404
+ typeof x.body_digest === 'string' &&
405
+ typeof x.sig === 'string' &&
406
+ Array.isArray(x.caveats) &&
407
+ (x.parent === undefined || typeof x.parent === 'string') &&
408
+ (x.alg === undefined || typeof x.alg === 'string') &&
409
+ (x.pq_pk === undefined || typeof x.pq_pk === 'string') &&
410
+ (x.pq_sig === undefined || typeof x.pq_sig === 'string'));
411
+ }
412
+ /** Total, fail-closed decode of a bound token back into a capability chain (null on any deviation). */
413
+ function decodeBoundToken(value) {
414
+ try {
415
+ const text = new TextDecoder('utf-8', { fatal: true }).decode((0, pca_1.unb64u)(value));
416
+ const parsed = (0, pca_1.strictParse)(text);
417
+ if (!Array.isArray(parsed))
418
+ return null;
419
+ const chain = [];
420
+ for (const hop of parsed) {
421
+ if (!isCapabilityShape(hop))
422
+ return null;
423
+ chain.push(hop);
424
+ }
425
+ return chain;
426
+ }
427
+ catch {
428
+ return null;
429
+ }
430
+ }
431
+ /** Render a (verified) PCA capability chain as a GNAP grant response with a bound access token. */
432
+ function capabilityToGrantResponse(chain, opts = {}) {
433
+ if (!Array.isArray(chain) || chain.length === 0)
434
+ throw new GnapError('capabilityToGrantResponse: empty chain');
435
+ const leaf = chain[chain.length - 1];
436
+ const token = {
437
+ value: encodeBoundToken(chain),
438
+ access: grantedAccessOf(chain),
439
+ key: opts.bearer === true ? false : holderJwk(leaf.holder),
440
+ ...(opts.expiresIn !== undefined ? { expires_in: opts.expiresIn } : {}),
441
+ ...(opts.manageUri !== undefined ? { manage: { uri: opts.manageUri } } : {}),
442
+ };
443
+ return {
444
+ access_token: token,
445
+ ...(opts.continue !== undefined ? { continue: opts.continue } : {}),
446
+ ...(opts.instanceId !== undefined ? { instance_id: opts.instanceId } : {}),
447
+ };
448
+ }
449
+ /**
450
+ * Verify an incoming GNAP bound token, mapping it back to a PCA principal + capability. FAILS CLOSED:
451
+ * a token that is not a decodable chain, or whose chain does not verify (optionally against
452
+ * `expectedRootIssuer`), returns `{ ok:false }`. The `principal` is the chain's root issuer key.
453
+ */
454
+ function verifyGnapToken(value, opts = {}) {
455
+ if (typeof value !== 'string' || value.length === 0)
456
+ return { ok: false, reason: 'token value is empty' };
457
+ const chain = decodeBoundToken(value);
458
+ if (chain === null || chain.length === 0)
459
+ return { ok: false, reason: 'token is not a decodable PCA capability chain' };
460
+ const vc = (0, pca_1.verifyChain)(chain, opts.expectedRootIssuer);
461
+ if (!vc.ok)
462
+ return { ok: false, reason: `capability chain invalid: ${vc.reason ?? 'unknown'}` };
463
+ const root = chain[0];
464
+ const leaf = chain[chain.length - 1];
465
+ return { ok: true, principal: root.issuer, capability: leaf, chain, access: grantedAccessOf(chain) };
466
+ }
467
+ const JWSD_TYP = 'gnap-binding-jwsd';
468
+ function athOf(accessTokenValue) {
469
+ return (0, pca_1.b64u)((0, pca_1.sha256)((0, pca_1.utf8)(accessTokenValue)));
470
+ }
471
+ function toPlain(value) {
472
+ return JSON.parse(JSON.stringify(value));
473
+ }
474
+ function canonicalPayloadBytes(body) {
475
+ return (0, pca_1.utf8)((0, pca_1.canonicalizeStrict)(toPlain(body)));
476
+ }
477
+ /**
478
+ * Produce a GNAP `jwsd` detached-JWS proof over `body`, signed by the client key (via `jose`). The JWS
479
+ * payload is the canonical body; the compact detached form (payload omitted) is what travels in the
480
+ * `Detached-JWS` header. The signing key's public half IS the PCA holder the grant binds to.
481
+ */
482
+ async function signGnapRequest(args) {
483
+ const created = args.created ?? Math.floor(Date.now() / 1000);
484
+ const holder = (0, pca_1.b64u)(args.key.publicKey);
485
+ const header = {
486
+ alg: 'EdDSA',
487
+ typ: JWSD_TYP,
488
+ htm: args.htm.toUpperCase(),
489
+ uri: args.uri,
490
+ created,
491
+ ...(args.kid !== undefined ? { kid: args.kid } : {}),
492
+ ...(args.accessTokenValue !== undefined ? { ath: athOf(args.accessTokenValue) } : {}),
493
+ };
494
+ const payload = canonicalPayloadBytes(args.body);
495
+ const privateJwk = { kty: 'OKP', crv: 'Ed25519', x: holder, d: (0, pca_1.b64u)(args.key.secretKey) };
496
+ const signingKey = await (0, jose_1.importJWK)(privateJwk, 'EdDSA');
497
+ // JWSHeaderParameters carries an index signature; copy the typed header into an object literal that
498
+ // satisfies it (GnapJwsdHeader is a closed interface and is not directly assignable).
499
+ const protectedHeader = {
500
+ alg: header.alg,
501
+ typ: header.typ,
502
+ htm: header.htm,
503
+ uri: header.uri,
504
+ created: header.created,
505
+ ...(header.kid !== undefined ? { kid: header.kid } : {}),
506
+ ...(header.ath !== undefined ? { ath: header.ath } : {}),
507
+ };
508
+ const jws = await new jose_1.FlattenedSign(payload).setProtectedHeader(protectedHeader).sign(signingKey);
509
+ return { detachedJws: `${jws.protected}..${jws.signature}`, header, holder };
510
+ }
511
+ /**
512
+ * Verify a GNAP `jwsd` detached-JWS proof (via `jose`) and bind the request to the holder key. FAILS
513
+ * CLOSED on any deviation: a bad signature, a tampered body, a wrong holder, a mismatched `htm`/`uri`,
514
+ * a stale/future `created`, or an `ath` that does not bind the named access token.
515
+ */
516
+ async function verifyGnapRequest(args) {
517
+ const fail = (reason) => ({ ok: false, reason });
518
+ try {
519
+ const parts = args.detachedJws.split('.');
520
+ if (parts.length !== 3)
521
+ return fail('not a compact JWS (expected three dot-separated parts)');
522
+ const [protectedB64u, mid, signature] = parts;
523
+ if (protectedB64u === undefined || signature === undefined || mid !== '')
524
+ return fail('not a DETACHED compact JWS');
525
+ const holder = asEd25519(args.holder);
526
+ if (holder === null)
527
+ return fail('holder is not a canonical base64url Ed25519 public key');
528
+ const payloadB64u = jose_1.base64url.encode(canonicalPayloadBytes(args.body));
529
+ const verifyKey = await (0, jose_1.importJWK)({ kty: 'OKP', crv: 'Ed25519', x: holder }, 'EdDSA');
530
+ let header;
531
+ try {
532
+ const res = await (0, jose_1.flattenedVerify)({ protected: protectedB64u, payload: payloadB64u, signature }, verifyKey);
533
+ header = res.protectedHeader;
534
+ }
535
+ catch {
536
+ return fail('jwsd signature does not verify under the holder key (or body was tampered)');
537
+ }
538
+ if (header === undefined)
539
+ return fail('jwsd carries no protected header');
540
+ if (header.typ !== JWSD_TYP)
541
+ return fail('unexpected typ (not gnap-binding-jwsd)');
542
+ if (header.alg !== 'EdDSA')
543
+ return fail('unexpected alg (not EdDSA)');
544
+ const htm = header.htm;
545
+ if (typeof htm !== 'string' || htm.toUpperCase() !== args.htm.toUpperCase())
546
+ return fail('htm does not match the request method');
547
+ const uri = header.uri;
548
+ if (typeof uri !== 'string' || uri !== args.uri)
549
+ return fail('uri does not match the request target');
550
+ const created = header.created;
551
+ if (typeof created !== 'number' || !Number.isFinite(created))
552
+ return fail('missing or malformed created');
553
+ const nowSec = Math.floor((args.now ?? Date.now()) / 1000);
554
+ const maxAge = args.maxAgeSec ?? 300;
555
+ if (created > nowSec + 60)
556
+ return fail('created is in the future');
557
+ if (nowSec - created > maxAge)
558
+ return fail('jwsd proof is stale');
559
+ const ath = header.ath;
560
+ if (args.accessTokenValue !== undefined) {
561
+ if (typeof ath !== 'string' || ath !== athOf(args.accessTokenValue))
562
+ return fail('ath does not bind the named access token');
563
+ }
564
+ const out = {
565
+ alg: 'EdDSA',
566
+ typ: JWSD_TYP,
567
+ htm,
568
+ uri,
569
+ created,
570
+ ...(typeof header.kid === 'string' ? { kid: header.kid } : {}),
571
+ ...(typeof ath === 'string' ? { ath } : {}),
572
+ };
573
+ return { ok: true, holder, header: out };
574
+ }
575
+ catch {
576
+ return fail('malformed jwsd request');
577
+ }
578
+ }
579
+ /**
580
+ * Decide whether a grant request needs a continuation/step-up: it does when GNAP interaction is
581
+ * requested, when an explicit higher tier is asked for, or when (given the issuer's chain) the request
582
+ * asks for rights BEYOND the issuer's own authority — which can only be unlocked by a human/guardian
583
+ * co-signature, not minted by the agent alone.
584
+ */
585
+ function gnapRequestRequiresStepUp(req, opts = {}) {
586
+ const beyond = [];
587
+ if (opts.issuerChain) {
588
+ const G = grantedAccessOf(opts.issuerChain);
589
+ for (const r of normalizeAccess(accessOf(req)))
590
+ if (!accessWithin(G, r))
591
+ beyond.push(r);
592
+ }
593
+ const interactRequested = req.interact !== undefined && Array.isArray(req.interact.start) && req.interact.start.length > 0;
594
+ const required = interactRequested || beyond.length > 0 || (opts.tier !== undefined && opts.tier > 1);
595
+ const tier = opts.tier ?? (beyond.length > 0 ? 3 : 2);
596
+ const reason = !required
597
+ ? 'no step-up required'
598
+ : beyond.length > 0
599
+ ? `requested rights exceed the issuer's authority (${beyond.length} right(s) beyond)`
600
+ : interactRequested
601
+ ? 'GNAP interaction requested'
602
+ : `tier ${tier} step-up required`;
603
+ return { required, reason, beyond, tier };
604
+ }
605
+ /** Begin a GNAP continuation: emit the `continue` handle + the PCA step-up it resolves through. */
606
+ function beginGnapContinuation(args) {
607
+ const cont = {
608
+ uri: args.continueUri,
609
+ access_token: { value: args.continueToken, bound: true },
610
+ ...(args.wait !== undefined ? { wait: args.wait } : {}),
611
+ };
612
+ return {
613
+ response: { continue: cont },
614
+ stepUp: {
615
+ tier: args.tier,
616
+ t: args.t,
617
+ signerSet: args.signerSet,
618
+ reason: args.reason ?? `tier ${args.tier} co-signature required to continue`,
619
+ continueToken: args.continueToken,
620
+ },
621
+ };
622
+ }
623
+ /**
624
+ * Produce one role's step-up co-signature share over the action's PCActn — the co-signature a
625
+ * guardian/principal device returns through the GNAP continuation. For 'guardian'/'principal' it binds
626
+ * the signer set + `t` (replay-safe); the 'agent' share is the PCActn's own leaf signature.
627
+ */
628
+ function gnapStepUpShare(body, role, secret, signerSet, t) {
629
+ return (0, pca_1.signShare)(role, secret, (0, pca_1.thresholdMessage)(body), { signerSet, t });
630
+ }
631
+ /**
632
+ * Resolve a continuation: build the action's PCActn (agent leaf signature) and FOLD the step-up
633
+ * co-signatures into its `threshold`, then verify the threshold `t` is reached. This is the concrete
634
+ * realization of "a grant that needs more authority continues via a step-up co-signature folded into
635
+ * the PCActn" — the multi-signature carrier; a FROST aggregate share slots in the same `agent`/group
636
+ * position without changing the property.
637
+ */
638
+ function completeGnapContinuation(args) {
639
+ if (!Array.isArray(args.chain) || args.chain.length === 0)
640
+ throw new GnapError('completeGnapContinuation: empty chain');
641
+ const pcactn = (0, pca_1.buildPCActn)({
642
+ grant: args.grant,
643
+ chain: args.chain,
644
+ plan: args.plan,
645
+ nodeId: args.nodeId,
646
+ counter: args.counter,
647
+ signerSecret: args.agentLeafSecret,
648
+ aud: args.aud,
649
+ ...(args.params !== undefined ? { params: args.params } : {}),
650
+ ...(args.now !== undefined ? { now: args.now } : {}),
651
+ ...(args.ttlMs !== undefined ? { ttlMs: args.ttlMs } : {}),
652
+ });
653
+ const message = (0, pca_1.thresholdMessage)(pcactn);
654
+ // The agent's vote is a role-bound share like every other (a bare leaf signature is valid under any signer set / t and is rejected).
655
+ const agentShare = (0, pca_1.signShare)('agent', args.agentLeafSecret, message, { signerSet: args.signerSet, t: args.t });
656
+ const cosignShares = args.cosignSecrets.map((cs) => (0, pca_1.signShare)(cs.role, cs.secret, message, { signerSet: args.signerSet, t: args.t }));
657
+ const threshold = (0, pca_1.assembleThreshold)([agentShare, ...cosignShares]);
658
+ const verdict = (0, pca_1.verifyThreshold)(threshold, message, args.signerSet, args.t);
659
+ return { pcactn: { ...pcactn, threshold }, threshold, verdict };
660
+ }
661
+ /** The machine-readable Agent-GNAP profile spec (see the module header for the full narrative). */
662
+ exports.AGENT_GNAP_PROFILE = {
663
+ name: 'agent-gnap',
664
+ version: '1.0',
665
+ rfc: 'RFC 9635',
666
+ summary: 'A GNAP profile for autonomous-agent delegation: each A2A hop is a GNAP grant whose authority is ' +
667
+ 'issued as an attenuated, key-bound PCA capability, never wider than the issuing agent.',
668
+ mapping: [
669
+ { gnap: 'access[].type', pca: 'predicate resource namespace gnap:<type>:<location>', note: 'the resource family the rights live in' },
670
+ { gnap: 'access[].actions', pca: 'predicate.verb (list, or * for any)', note: 'the operations permitted' },
671
+ { gnap: 'access[].locations', pca: 'predicate.resource (exact or trailing-* prefix)', note: 'the resource targets' },
672
+ { gnap: 'access[].datatypes / identifier', pca: "predicate.where on action.params.*", note: 'value-level constraints' },
673
+ { gnap: 'client.key.jwk (OKP/Ed25519)', pca: 'capability.holder (cnf-bound key)', note: 'the proof-of-possession key' },
674
+ { gnap: 'grant request (no prior grant)', pca: 'mintGrant → root capability', note: 'envelope predicates = requested access' },
675
+ { gnap: 'grant request (delegated hop)', pca: 'delegate → child capability + gnap_access caveat', note: 'append-only attenuation' },
676
+ { gnap: 'access_token (§3.2)', pca: 'a verifiable PCA capability chain (encodeBoundToken)', note: 'bound, self-verifying token' },
677
+ { gnap: 'continue (§3.1) + interact (§3.3)', pca: 'PCA step-up: FROST/threshold co-signature folded into a PCActn', note: 'tier-2/3 escalation' },
678
+ { gnap: 'jwsd detached JWS (§7.3.4)', pca: 'request proof binding the body to capability.holder', note: 'verified with jose' },
679
+ ],
680
+ proofMethods: ['jwsd'],
681
+ multiHop: 'An A2A task is a chain of GNAP grants. Each hop delegates from the issuing agent to the next, ' +
682
+ 'recording its (clamped) rights as an append-only gnap_access caveat. Effective authority is the ' +
683
+ 'intersection of the root policy envelope and every hop; PCA caveats are conjunctive and ' +
684
+ 'append-only, so no hop can widen — "never wider than the issuer" holds across any depth.',
685
+ stepUp: 'A request for authority beyond the issuer (or an explicit interaction) returns a GNAP continue ' +
686
+ 'handle instead of a token. The client returns with the action PCActn plus a guardian/principal ' +
687
+ 'co-signature, which is folded into the PCActn threshold and verified at tier 2/3 (FROST or ' +
688
+ 'multi-signature) before the action is admitted.',
689
+ security: [
690
+ 'Attenuation is structural and append-only: a delegated hop can only ever narrow authority.',
691
+ 'Every grant is key-bound (cnf): the holder key must prove possession via a jwsd detached JWS.',
692
+ 'All verification fails closed: bad token, broken chain, bad proof, stale request, or out-of-scope action denies.',
693
+ 'Rights beyond the issuer are clamped at issue time and denied at action time (defense in depth).',
694
+ 'Elevation beyond the agent requires a human/guardian co-signature, not a self-minted grant.',
695
+ ],
696
+ };
697
+ //# sourceMappingURL=index.js.map