macp-sdk-typescript 0.17.1 → 0.19.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/CHANGELOG.md +15 -0
- package/README.md +90 -35
- package/dist/client.d.ts +24 -1
- package/dist/client.js +43 -1
- package/dist/commitment-hash.d.ts +49 -0
- package/dist/commitment-hash.js +32 -7
- package/dist/constants.d.ts +5 -0
- package/dist/constants.js +6 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +8 -1
- package/dist/receipt.d.ts +124 -0
- package/dist/receipt.js +376 -0
- package/dist/types.d.ts +59 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +8 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,21 @@ All notable changes to `macp-sdk-typescript` are documented here. The format
|
|
|
4
4
|
follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the
|
|
5
5
|
project uses [Semantic Versioning](https://semver.org/).
|
|
6
6
|
|
|
7
|
+
## [0.19.0](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/compare/v0.18.0...v0.19.0) (2026-10-26)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
### Features
|
|
11
|
+
|
|
12
|
+
* capability-gated whatIfReplay and HISTORY_UNAVAILABLE (RFC-MACP-0012 §8.1) ([c5bc5cf](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/commit/c5bc5cf6d8206c30f5d69f6fde44800e8d294fa6))
|
|
13
|
+
* **claude-agent-sdk:** Claude Agent SDK subagents reach a binding MACP decision (W3.5) ([573bf33](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/commit/573bf335d433ad13bcb5668d1563b67494df4442))
|
|
14
|
+
|
|
15
|
+
## [0.18.0](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/compare/v0.17.1...v0.18.0) (2026-10-25)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
### Features
|
|
19
|
+
|
|
20
|
+
* **receipt:** RFC-MACP-0013 Appendix A decision receipts (W3.10) ([#195](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/issues/195)) ([1ef38ef](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/commit/1ef38ef4e899a74eb7b4c8207f9556a31d0dba0f))
|
|
21
|
+
|
|
7
22
|
## [0.17.1](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/compare/v0.17.0...v0.17.1) (2026-10-20)
|
|
8
23
|
|
|
9
24
|
|
package/README.md
CHANGED
|
@@ -8,25 +8,25 @@ runtime behavior is documented in the [runtime repo's docs](https://github.com/m
|
|
|
8
8
|
|
|
9
9
|
## Install
|
|
10
10
|
|
|
11
|
-
Requires Node.js >= 20.
|
|
11
|
+
Requires Node.js >= 20 (`engines` in `package.json`); CI runs on Node 22 and 24.
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
14
|
npm install macp-sdk-typescript
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
Everything installs from npmjs, including the `@multiagentcoordinationprotocol/proto` dependency (published there from 0.1.11). No `.npmrc` or GitHub token is needed.
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
@multiagentcoordinationprotocol:registry=https://npm.pkg.github.com
|
|
21
|
-
```
|
|
19
|
+
## Quick Start
|
|
22
20
|
|
|
23
|
-
|
|
21
|
+
Start a local runtime (the same image CI pins):
|
|
24
22
|
|
|
25
23
|
```bash
|
|
26
|
-
|
|
24
|
+
docker run -p 50051:50051 -e MACP_ALLOW_INSECURE=1 -e MACP_MEMORY_ONLY=1 ghcr.io/multiagentcoordinationprotocol/macp-runtime:0.9.0
|
|
27
25
|
```
|
|
28
26
|
|
|
29
|
-
|
|
27
|
+
`MACP_ALLOW_INSECURE=1` permits plaintext and dev-agent auth (local development only —
|
|
28
|
+
since runtime 0.5.0 a runtime with no auth configured refuses to start without it);
|
|
29
|
+
`MACP_MEMORY_ONLY=1` skips on-disk persistence. Then connect:
|
|
30
30
|
|
|
31
31
|
```typescript
|
|
32
32
|
import { Auth, MacpClient, DecisionSession } from 'macp-sdk-typescript';
|
|
@@ -72,13 +72,14 @@ client.close();
|
|
|
72
72
|
|
|
73
73
|
The SDK uses a **three-layer** design:
|
|
74
74
|
|
|
75
|
-
- **Low-level transport** (`MacpClient`): typed wrappers around every `MACPRuntimeService` RPC — `initialize`, `send`, `openStream`, `getSession`, `cancelSession`, `suspendSession`, `resumeSession`, `listSessions`, `getManifest`, `listModes`, `listRoots`, `listExtModes`, `registerExtMode`, `unregisterExtMode`, `promoteMode`, the policy RPCs (`register`/`unregister`/`get`/`listPolicies`), plus streaming watchers (`ModeRegistryWatcher`, `RootsWatcher`, `SignalWatcher`, `PolicyWatcher`, `SessionLifecycleWatcher`).
|
|
75
|
+
- **Low-level transport** (`MacpClient`): typed wrappers around every `MACPRuntimeService` RPC — `initialize`, `send`, `openStream`, `getSession`, `cancelSession`, `suspendSession`, `resumeSession`, `listSessions`, `getManifest`, `listModes`, `listRoots`, `listExtModes`, `registerExtMode`, `unregisterExtMode`, `promoteMode`, the policy RPCs (`register`/`unregister`/`get`/`listPolicies`, and `whatIfReplay` where the runtime advertises it), plus streaming watchers (`ModeRegistryWatcher`, `RootsWatcher`, `SignalWatcher`, `PolicyWatcher`, `SessionLifecycleWatcher`).
|
|
76
76
|
- **High-level session helpers**: one class per coordination mode (`DecisionSession`, `ProposalSession`, `TaskSession`, `HandoffSession`, `QuorumSession`) — wraps `MacpClient`, builds envelopes, encodes payloads, and maintains a local state projection.
|
|
77
77
|
- **Agent framework** (`src/agent/`): event-driven `Participant` with strategy-based handlers, bootstrap runner (`fromBootstrap`), gRPC/HTTP transports, and the optional cancel-callback HTTP server.
|
|
78
78
|
|
|
79
79
|
See [`docs/guides/architecture.md`](docs/guides/architecture.md) for the full map.
|
|
80
80
|
|
|
81
81
|
Every session class follows the same pattern:
|
|
82
|
+
|
|
82
83
|
1. Construct with a `MacpClient` and options
|
|
83
84
|
2. Call `start()` to initiate the session
|
|
84
85
|
3. Call mode-specific methods (propose, vote, accept, etc.)
|
|
@@ -94,8 +95,8 @@ await session.start({
|
|
|
94
95
|
intent: 'pick a deployment strategy',
|
|
95
96
|
participants: ['alice', 'bob'],
|
|
96
97
|
ttlMs: 60_000,
|
|
97
|
-
contextId: 'ctx-abc',
|
|
98
|
-
extensions: { 'x-trace': Buffer.from('tid') },
|
|
98
|
+
contextId: 'ctx-abc', // optional opaque context reference
|
|
99
|
+
extensions: { 'x-trace': Buffer.from('tid') }, // optional typed extension buffers
|
|
99
100
|
roots: [{ uri: 'file:///workspace', name: 'repo' }],
|
|
100
101
|
});
|
|
101
102
|
```
|
|
@@ -121,9 +122,9 @@ await session.vote({ proposalId: 'p1', vote: 'approve' });
|
|
|
121
122
|
await session.commit({ action: 'decided', authorityScope: 'team', reason: '...' });
|
|
122
123
|
|
|
123
124
|
// Projection queries
|
|
124
|
-
session.projection.voteTotals();
|
|
125
|
-
session.projection.majorityWinner();
|
|
126
|
-
session.projection.hasBlockingObjection('p1');
|
|
125
|
+
session.projection.voteTotals(); // { p1: 1 }
|
|
126
|
+
session.projection.majorityWinner(); // 'p1'
|
|
127
|
+
session.projection.hasBlockingObjection('p1'); // false (only severity 'critical' blocks)
|
|
127
128
|
```
|
|
128
129
|
|
|
129
130
|
### Proposal Mode
|
|
@@ -145,9 +146,9 @@ await session.accept({ proposalId: 'p2', reason: 'bob agrees', sender: 'bob', au
|
|
|
145
146
|
await session.commit({ action: 'proposal.accepted', authorityScope: 'team', reason: '...' });
|
|
146
147
|
|
|
147
148
|
// Projection queries
|
|
148
|
-
session.projection.activeProposals();
|
|
149
|
-
session.projection.isAccepted('p2');
|
|
150
|
-
session.projection.isTerminallyRejected('p1');
|
|
149
|
+
session.projection.activeProposals(); // proposals with status 'open'
|
|
150
|
+
session.projection.isAccepted('p2'); // true
|
|
151
|
+
session.projection.isTerminallyRejected('p1'); // false
|
|
151
152
|
```
|
|
152
153
|
|
|
153
154
|
### Task Mode
|
|
@@ -170,9 +171,9 @@ await session.completeTask({ taskId: 't1', assignee: 'worker', summary: 'done',
|
|
|
170
171
|
await session.commit({ action: 'task.completed', authorityScope: 'lead', reason: '...' });
|
|
171
172
|
|
|
172
173
|
// Projection queries
|
|
173
|
-
session.projection.progressOf('t1');
|
|
174
|
-
session.projection.isCompleted('t1');
|
|
175
|
-
session.projection.activeTasks();
|
|
174
|
+
session.projection.progressOf('t1'); // 1.0
|
|
175
|
+
session.projection.isCompleted('t1'); // true
|
|
176
|
+
session.projection.activeTasks(); // []
|
|
176
177
|
```
|
|
177
178
|
|
|
178
179
|
### Handoff Mode
|
|
@@ -199,8 +200,8 @@ await session.acceptHandoff({
|
|
|
199
200
|
await session.commit({ action: 'handoff.accepted', authorityScope: 'team', reason: '...' });
|
|
200
201
|
|
|
201
202
|
// Projection queries
|
|
202
|
-
session.projection.isAccepted('h1');
|
|
203
|
-
session.projection.pendingHandoffs();
|
|
203
|
+
session.projection.isAccepted('h1'); // true
|
|
204
|
+
session.projection.pendingHandoffs(); // []
|
|
204
205
|
```
|
|
205
206
|
|
|
206
207
|
### Quorum Mode
|
|
@@ -222,10 +223,10 @@ await session.approve({ requestId: 'r1', reason: 'ok', sender: 'bob', auth: Auth
|
|
|
222
223
|
await session.commit({ action: 'quorum.approved', authorityScope: 'ops', reason: '...' });
|
|
223
224
|
|
|
224
225
|
// Projection queries
|
|
225
|
-
session.projection.hasQuorum('r1');
|
|
226
|
-
session.projection.approvalCount('r1');
|
|
227
|
-
session.projection.remainingVotesNeeded('r1');
|
|
228
|
-
session.projection.votedSenders('r1');
|
|
226
|
+
session.projection.hasQuorum('r1'); // true
|
|
227
|
+
session.projection.approvalCount('r1'); // 2
|
|
228
|
+
session.projection.remainingVotesNeeded('r1'); // 0
|
|
229
|
+
session.projection.votedSenders('r1'); // ['alice', 'bob']
|
|
229
230
|
```
|
|
230
231
|
|
|
231
232
|
## Agent Framework
|
|
@@ -305,9 +306,16 @@ Composable handler factories for Decision Mode — register them directly on a
|
|
|
305
306
|
|
|
306
307
|
```typescript
|
|
307
308
|
participant
|
|
308
|
-
.on(
|
|
309
|
-
|
|
310
|
-
|
|
309
|
+
.on(
|
|
310
|
+
'Proposal',
|
|
311
|
+
agent.evaluationHandler(
|
|
312
|
+
agent.functionEvaluator(async (p) => ({
|
|
313
|
+
recommendation: 'APPROVE',
|
|
314
|
+
confidence: 0.9,
|
|
315
|
+
reason: 'looks good',
|
|
316
|
+
})),
|
|
317
|
+
),
|
|
318
|
+
)
|
|
311
319
|
.on('Evaluation', agent.votingHandler(agent.majorityVoter({ positiveThreshold: 0.6 })))
|
|
312
320
|
.on('Vote', agent.commitmentHandler(agent.majorityCommitter({ quorumSize: 2 })));
|
|
313
321
|
```
|
|
@@ -327,6 +335,17 @@ server via `fromBootstrap()`; for the manual path use
|
|
|
327
335
|
See [`docs/api/cancel-callback.md`](docs/api/cancel-callback.md) and
|
|
328
336
|
[`examples/cancel-callback.ts`](examples/cancel-callback.ts).
|
|
329
337
|
|
|
338
|
+
## Framework Integrations
|
|
339
|
+
|
|
340
|
+
Framework adapters live under [`integrations/`](integrations/). Each is a separate npm package
|
|
341
|
+
with its own version and release tag.
|
|
342
|
+
|
|
343
|
+
| Framework | Package | |
|
|
344
|
+
| --- | --- | --- |
|
|
345
|
+
| LangGraph | [`@multiagentcoordinationprotocol/langgraph`](integrations/langgraph/) | `MacpConsensusNode`: a graph node whose participants evaluate and vote through a Decision or Quorum session and write the commitment into graph state |
|
|
346
|
+
| Claude Agent SDK | [`@multiagentcoordinationprotocol/claude-agent-sdk`](integrations/claude-agent-sdk/) | `claudeEvaluator` runs a Claude agent as a participant; `macpConsensusServer` gives a lead agent an `mcp__macp__consensus` tool whose subagents evaluate and vote, returning the commitment |
|
|
347
|
+
| OpenAI Agents SDK | planned | |
|
|
348
|
+
|
|
330
349
|
## Authentication
|
|
331
350
|
|
|
332
351
|
```typescript
|
|
@@ -389,9 +408,9 @@ To attach to a session that is already in flight and receive accepted history be
|
|
|
389
408
|
|
|
390
409
|
```typescript
|
|
391
410
|
const stream = client.openStream({ auth });
|
|
392
|
-
await stream.sendSubscribe(sessionId);
|
|
411
|
+
await stream.sendSubscribe(sessionId); // full replay then live
|
|
393
412
|
// or, after a reconnect:
|
|
394
|
-
await stream.sendSubscribe(sessionId, lastSeq);
|
|
413
|
+
await stream.sendSubscribe(sessionId, lastSeq); // resume from cursor
|
|
395
414
|
```
|
|
396
415
|
|
|
397
416
|
The agent framework's `GrpcTransportAdapter` calls this for you on start, so non-initiator agents see `SessionStart` regardless of join order — and it passes its own resume cursor on a second `start()` (after a `stop()`), so that call resumes rather than replaying the whole session again.
|
|
@@ -413,6 +432,43 @@ for await (const change of watcher.changes(controller.signal)) {
|
|
|
413
432
|
const next = await watcher.nextChange();
|
|
414
433
|
```
|
|
415
434
|
|
|
435
|
+
## Decision Receipts
|
|
436
|
+
|
|
437
|
+
A decision receipt (RFC-MACP-0013 Appendix A, `macp-receipt/1`) records one Commitment in a form a third party can check offline. The verifier reports two things separately:
|
|
438
|
+
|
|
439
|
+
- **Integrity**: the receipt's `commitment` has its `commitment_hash`. This needs no keys.
|
|
440
|
+
- **Authenticity**: an attestation over the receipt is valid for a key you trust. The SDK implements Ed25519 `jws` attestations only.
|
|
441
|
+
|
|
442
|
+
```typescript
|
|
443
|
+
import { buildReceipt, verifyReceipt } from 'macp-sdk-typescript/receipt';
|
|
444
|
+
|
|
445
|
+
// Issue: hash the commitment and sign the receipt body (detached JWS, alg EdDSA).
|
|
446
|
+
const receipt = buildReceipt(commitmentPayload, {
|
|
447
|
+
sessionId,
|
|
448
|
+
mode: 'macp.mode.decision.v1',
|
|
449
|
+
issuer: 'runtime:prod-eu',
|
|
450
|
+
issuedAt: new Date().toISOString(),
|
|
451
|
+
signer: { kid: 'prod-2026', privateKey: seed32 }, // or { kid, sign: (bytes) => signature64 }
|
|
452
|
+
commitmentWire, // optional: Commitment wire bytes, recorded as commitment_wire_b64
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
// Verify: trusted keys map a kid to an Ed25519 public key (JWK `x`, base64url).
|
|
456
|
+
const result = verifyReceipt(receipt, { 'prod-2026': 'TDVzkxaSf6_Oxu7mmn5TbCkF80s8Ulh33qKQsi0gnZM' });
|
|
457
|
+
// { integrity: 'match', attestations: ['valid'], verdict: 'attested' }
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
| Field | Values |
|
|
461
|
+
| ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
462
|
+
| `integrity` | `match`, `mismatch`, `cannot_verify`, or `null` when the receipt is refused |
|
|
463
|
+
| `attestations[i]` | `valid`, `invalid`, `unknown_key` or `unsupported` (any other `alg`, and every `vendor` attestation) |
|
|
464
|
+
| `verdict` | `attested` (integrity holds and at least one attestation is valid), `integrity_only`, `unverified` (integrity fails, so attestations are not checked), or `refused` (unknown `receipt_format`) |
|
|
465
|
+
|
|
466
|
+
If you hold the snake_case §3 projection (a receipt's `commitment`) instead of a `CommitmentPayload`, convert it with `commitmentFromProjection(projection)`.
|
|
467
|
+
|
|
468
|
+
`verifyReceipt` never throws. `buildReceipt` throws `MacpSdkError` on a missing field, and when `commitmentWire` is not the wire encoding of the commitment. Signing is deterministic: the same inputs always produce the same receipt.
|
|
469
|
+
|
|
470
|
+
The `macp-sdk-typescript/receipt` subpath is browser-safe: it pulls in no gRPC, no `node:crypto` and no `Buffer`. The same functions are also exported from the package root. The tests replay the spec's `schemas/conformance/receipt/` vectors and rebuild the spec's example receipt byte for byte.
|
|
471
|
+
|
|
416
472
|
## Error Handling
|
|
417
473
|
|
|
418
474
|
```typescript
|
|
@@ -454,11 +510,10 @@ cleanly fails that workflow, not just a local check.
|
|
|
454
510
|
Run the full SDK against a live MACP runtime:
|
|
455
511
|
|
|
456
512
|
```bash
|
|
457
|
-
#
|
|
458
|
-
docker build -t macp-runtime ../macp-runtime/
|
|
513
|
+
# Start the runtime (CI's pinned image)
|
|
459
514
|
docker run -d --name macp-runtime-test -p 50051:50051 \
|
|
460
|
-
-e
|
|
461
|
-
|
|
515
|
+
-e MACP_ALLOW_INSECURE=1 -e MACP_MEMORY_ONLY=1 \
|
|
516
|
+
ghcr.io/multiagentcoordinationprotocol/macp-runtime:0.9.0
|
|
462
517
|
|
|
463
518
|
# Run tests
|
|
464
519
|
npm run test:integration
|
package/dist/client.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as grpc from '@grpc/grpc-js';
|
|
2
2
|
import { type AuthConfig } from './auth';
|
|
3
3
|
import { ProtoRegistry } from './proto-registry';
|
|
4
|
-
import type { Ack, AgentManifest, Envelope, InitializeResult, ModeDescriptor, PolicyDescriptor, SessionMetadata, Root } from './types';
|
|
4
|
+
import type { Ack, AgentManifest, Envelope, InitializeResult, ModeDescriptor, PolicyDescriptor, SessionMetadata, Root, WhatIfReplayResult } from './types';
|
|
5
5
|
interface MacpClientOptions {
|
|
6
6
|
address: string;
|
|
7
7
|
/**
|
|
@@ -99,6 +99,10 @@ export declare class MacpClient {
|
|
|
99
99
|
private readonly defaultDeadlineMs?;
|
|
100
100
|
private readonly clientName;
|
|
101
101
|
private readonly clientVersion;
|
|
102
|
+
/** The last {@link initialize} result; {@link whatIfReplay} reads its capabilities. */
|
|
103
|
+
private initializeResult?;
|
|
104
|
+
/** The runtime's capabilities from the last {@link initialize}; `undefined` before one. */
|
|
105
|
+
get serverCapabilities(): Record<string, unknown> | undefined;
|
|
102
106
|
constructor(options: MacpClientOptions);
|
|
103
107
|
private requireAuth;
|
|
104
108
|
private metadata;
|
|
@@ -226,6 +230,25 @@ export declare class MacpClient {
|
|
|
226
230
|
auth?: AuthConfig;
|
|
227
231
|
deadlineMs?: number;
|
|
228
232
|
}): Promise<PolicyDescriptor[]>;
|
|
233
|
+
/**
|
|
234
|
+
* Replay a session's accepted history under a candidate policy and report how
|
|
235
|
+
* the outcome would differ (RFC-MACP-0012 §8.1). Read-only and advisory: the
|
|
236
|
+
* session, its history and the policy registry are untouched, and the
|
|
237
|
+
* candidate is never registered.
|
|
238
|
+
*
|
|
239
|
+
* The RPC is optional, so this checks `capabilities.policyRegistry.whatIfReplay`
|
|
240
|
+
* from {@link initialize} first (calling it once if this client hasn't) and
|
|
241
|
+
* throws {@link MacpSdkError} without sending anything when the runtime
|
|
242
|
+
* doesn't advertise it. Runtime refusals arrive as {@link MacpTransportError}
|
|
243
|
+
* with `macpErrorCode` set: `SESSION_NOT_FOUND`, `FORBIDDEN` (authorized as
|
|
244
|
+
* {@link getSession}), `INVALID_POLICY_DEFINITION` (the candidate fails
|
|
245
|
+
* RegisterPolicy admission, or its mode is neither the session's nor `*`),
|
|
246
|
+
* `HISTORY_UNAVAILABLE` (history compacted), `INTERNAL_ERROR`.
|
|
247
|
+
*/
|
|
248
|
+
whatIfReplay(sessionId: string, candidate: PolicyDescriptor, options?: {
|
|
249
|
+
auth?: AuthConfig;
|
|
250
|
+
deadlineMs?: number;
|
|
251
|
+
}): Promise<WhatIfReplayResult>;
|
|
229
252
|
/**
|
|
230
253
|
* Fetch a single page of sessions (proto ≥ 0.1.6). `pageSize: 0` (or absent)
|
|
231
254
|
* lets the server choose the page size; the server MAY cap it. Callers MUST
|
package/dist/client.js
CHANGED
|
@@ -276,6 +276,12 @@ class MacpClient {
|
|
|
276
276
|
defaultDeadlineMs;
|
|
277
277
|
clientName;
|
|
278
278
|
clientVersion;
|
|
279
|
+
/** The last {@link initialize} result; {@link whatIfReplay} reads its capabilities. */
|
|
280
|
+
initializeResult;
|
|
281
|
+
/** The runtime's capabilities from the last {@link initialize}; `undefined` before one. */
|
|
282
|
+
get serverCapabilities() {
|
|
283
|
+
return this.initializeResult?.capabilities;
|
|
284
|
+
}
|
|
279
285
|
constructor(options) {
|
|
280
286
|
this.auth = options.auth;
|
|
281
287
|
this.secure = options.secure ?? true;
|
|
@@ -360,7 +366,7 @@ class MacpClient {
|
|
|
360
366
|
});
|
|
361
367
|
}
|
|
362
368
|
async initialize(deadlineMs) {
|
|
363
|
-
|
|
369
|
+
const result = await this.unary('Initialize', {
|
|
364
370
|
supportedProtocolVersions: ['1.0'],
|
|
365
371
|
clientInfo: {
|
|
366
372
|
name: this.clientName,
|
|
@@ -380,6 +386,8 @@ class MacpClient {
|
|
|
380
386
|
experimental: { features: {} },
|
|
381
387
|
},
|
|
382
388
|
}, undefined, deadlineMs);
|
|
389
|
+
this.initializeResult = result;
|
|
390
|
+
return result;
|
|
383
391
|
}
|
|
384
392
|
async send(envelope, options) {
|
|
385
393
|
const auth = this.requireAuth(options?.auth);
|
|
@@ -513,6 +521,40 @@ class MacpClient {
|
|
|
513
521
|
const res = await this.unary('ListPolicies', { mode: mode || '' }, auth, options?.deadlineMs);
|
|
514
522
|
return res.descriptors || [];
|
|
515
523
|
}
|
|
524
|
+
/**
|
|
525
|
+
* Replay a session's accepted history under a candidate policy and report how
|
|
526
|
+
* the outcome would differ (RFC-MACP-0012 §8.1). Read-only and advisory: the
|
|
527
|
+
* session, its history and the policy registry are untouched, and the
|
|
528
|
+
* candidate is never registered.
|
|
529
|
+
*
|
|
530
|
+
* The RPC is optional, so this checks `capabilities.policyRegistry.whatIfReplay`
|
|
531
|
+
* from {@link initialize} first (calling it once if this client hasn't) and
|
|
532
|
+
* throws {@link MacpSdkError} without sending anything when the runtime
|
|
533
|
+
* doesn't advertise it. Runtime refusals arrive as {@link MacpTransportError}
|
|
534
|
+
* with `macpErrorCode` set: `SESSION_NOT_FOUND`, `FORBIDDEN` (authorized as
|
|
535
|
+
* {@link getSession}), `INVALID_POLICY_DEFINITION` (the candidate fails
|
|
536
|
+
* RegisterPolicy admission, or its mode is neither the session's nor `*`),
|
|
537
|
+
* `HISTORY_UNAVAILABLE` (history compacted), `INTERNAL_ERROR`.
|
|
538
|
+
*/
|
|
539
|
+
async whatIfReplay(sessionId, candidate, options) {
|
|
540
|
+
const auth = this.requireAuth(options?.auth);
|
|
541
|
+
const capabilities = this.serverCapabilities ?? (await this.initialize(options?.deadlineMs)).capabilities;
|
|
542
|
+
const policyRegistry = capabilities?.policyRegistry;
|
|
543
|
+
if (!policyRegistry?.whatIfReplay) {
|
|
544
|
+
throw new errors_1.MacpSdkError('this runtime does not advertise capabilities.policyRegistry.whatIfReplay; ' +
|
|
545
|
+
'WhatIfReplay is optional and a client MUST NOT call it otherwise (RFC-MACP-0012 §8.1)');
|
|
546
|
+
}
|
|
547
|
+
const res = await this.unary('WhatIfReplay', { sessionId, candidate }, auth, options?.deadlineMs);
|
|
548
|
+
// proto3 `optional` decodes with a synthetic oneof discriminator; an unset
|
|
549
|
+
// first_divergence is absent, never "0" (index 0 is the SessionStart).
|
|
550
|
+
const result = {
|
|
551
|
+
...res,
|
|
552
|
+
denials: res.denials ?? [],
|
|
553
|
+
entries: res.entries ?? [],
|
|
554
|
+
};
|
|
555
|
+
delete result._firstDivergence;
|
|
556
|
+
return result;
|
|
557
|
+
}
|
|
516
558
|
/**
|
|
517
559
|
* Fetch a single page of sessions (proto ≥ 0.1.6). `pageSize: 0` (or absent)
|
|
518
560
|
* lets the server choose the page size; the server MAY cap it. Callers MUST
|
|
@@ -1,4 +1,46 @@
|
|
|
1
1
|
import type { CommitmentPayload } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Escapes a single JSON string value per RFC 8785 §3.2.2.2:
|
|
4
|
+
* - short-form escapes for backspace/tab/newline/formfeed/carriage-return,
|
|
5
|
+
* `"`, and `\`
|
|
6
|
+
* - `\u00xx` (lowercase hex) for other C0 control characters (0x00-0x1F)
|
|
7
|
+
* - every other character, including non-ASCII and astral-plane
|
|
8
|
+
* codepoints, is emitted literally (NOT escaped).
|
|
9
|
+
*
|
|
10
|
+
* Lone surrogates: a bare UTF-16 surrogate code unit not paired into a valid
|
|
11
|
+
* codepoint is out-of-contract input for this function — a `CommitmentPayload`
|
|
12
|
+
* should never legitimately contain one, since this SDK's strings originate
|
|
13
|
+
* from valid UTF-16 sources (JS string literals, JSON-decoded wire payloads,
|
|
14
|
+
* etc.). If one appears anyway, this function does not detect or escape it
|
|
15
|
+
* specially: `for (const ch of str)` below yields it as its own one-code-unit
|
|
16
|
+
* step and it is emitted literally, unescaped, like any other character. The
|
|
17
|
+
* actual loss happens one step later, in `commitmentHash`, when the finished
|
|
18
|
+
* JCS string is encoded to bytes via `TextEncoder` — the standard lossy UTF-8
|
|
19
|
+
* encoder silently substitutes a lone surrogate with
|
|
20
|
+
* U+FFFD (the Unicode replacement character) before hashing. The observable
|
|
21
|
+
* consequence: a field containing a lone surrogate (e.g. `'\uD800'`) hashes
|
|
22
|
+
* IDENTICALLY to the same field containing a literal U+FFFD in that position
|
|
23
|
+
* — pinned by the "lone surrogate" test in commitment-hash.test.ts. This is
|
|
24
|
+
* an accepted consequence of D3 (never throw on malformed input), not a
|
|
25
|
+
* claimed point of compatibility (or divergence) with any other
|
|
26
|
+
* implementation of RFC-MACP-0013.
|
|
27
|
+
*
|
|
28
|
+
* `value` is typed `unknown`, not `string`, on purpose: this function is the
|
|
29
|
+
* single choke point that keeps `commitmentHash` from ever throwing (D3) —
|
|
30
|
+
* every field of a `CommitmentPayload` flows through here, and at runtime a
|
|
31
|
+
* decoded or hand-built payload is not guaranteed to actually match its
|
|
32
|
+
* static type. Non-string input is coerced via `String()` (never a template
|
|
33
|
+
* literal or `+`, both of which throw on a bare `Symbol`) rather than
|
|
34
|
+
* rejected — this file does not "fix" a malformed payload into a valid one,
|
|
35
|
+
* it only guarantees serialization cannot crash.
|
|
36
|
+
*
|
|
37
|
+
* Iterates with `for (const ch of str)`, which walks Unicode codepoints, not
|
|
38
|
+
* UTF-16 code units. Indexing by code unit would split an astral-plane
|
|
39
|
+
* codepoint's surrogate pair (e.g. U+1F702) and emit half of it — iterating
|
|
40
|
+
* by codepoint is what keeps a full character, including a surrogate pair,
|
|
41
|
+
* together as a single loop step so it can be appended whole.
|
|
42
|
+
*/
|
|
43
|
+
export declare function jcsString(value: unknown): string;
|
|
2
44
|
/**
|
|
3
45
|
* Projects a `CommitmentPayload` (this SDK's camelCase shape) to its RFC
|
|
4
46
|
* 8785 canonical JSON string (snake_case, D2.1) and serializes it directly
|
|
@@ -74,6 +116,13 @@ export declare function isCanonicalCommitmentHash(value: string): boolean;
|
|
|
74
116
|
* is a verdict on the payload encoding — never on the claim.
|
|
75
117
|
*/
|
|
76
118
|
export type VerifyOutcome = 'match' | 'mismatch' | 'cannot_verify';
|
|
119
|
+
/**
|
|
120
|
+
* Strictly decodes a `CommitmentPayload` wire encoding for the decision-receipt
|
|
121
|
+
* verifier (`./receipt`), which must compare the wire against the receipt's
|
|
122
|
+
* own `commitment`. `undefined` wherever {@link verifyCommitmentHash} would say
|
|
123
|
+
* `'cannot_verify'`. Internal: not re-exported from the package root.
|
|
124
|
+
*/
|
|
125
|
+
export declare function decodeCommitmentWire(wire: Uint8Array): CommitmentPayload | undefined;
|
|
77
126
|
/**
|
|
78
127
|
* Verifies a claimed commitment hash against a `CommitmentPayload`'s Protobuf
|
|
79
128
|
* wire encoding (RFC-MACP-0013 §6.1), returning exactly one of:
|
package/dist/commitment-hash.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.jcsString = jcsString;
|
|
3
4
|
exports.canonicalizeCommitmentPayload = canonicalizeCommitmentPayload;
|
|
4
5
|
exports.commitmentHash = commitmentHash;
|
|
5
6
|
exports.isCanonicalCommitmentHash = isCanonicalCommitmentHash;
|
|
7
|
+
exports.decodeCommitmentWire = decodeCommitmentWire;
|
|
6
8
|
exports.verifyCommitmentHash = verifyCommitmentHash;
|
|
7
9
|
/**
|
|
8
10
|
* RFC-MACP-0013 canonical commitment hashing.
|
|
@@ -19,16 +21,22 @@ exports.verifyCommitmentHash = verifyCommitmentHash;
|
|
|
19
21
|
* only strings, one boolean, and one optional 2-string nested object. No
|
|
20
22
|
* JSON numbers, no JSON arrays. That leaves only object-member ordering and
|
|
21
23
|
* string escaping, both of which are small enough to hand-write and test
|
|
22
|
-
* directly rather than pull in
|
|
23
|
-
*
|
|
24
|
+
* directly rather than pull in a JCS dependency.
|
|
25
|
+
*
|
|
26
|
+
* SHA-256 comes from `@noble/hashes` (pure JS, audited, zero dependencies)
|
|
27
|
+
* rather than `node:crypto`, so this module, and the decision-receipt module
|
|
28
|
+
* built on it (`./receipt`), load in a browser bundle as well as in Node.
|
|
29
|
+
* Strings are encoded with `TextEncoder`, not `Buffer`, for the same reason.
|
|
24
30
|
*/
|
|
25
|
-
const
|
|
31
|
+
const sha2_1 = require("@noble/hashes/sha2");
|
|
32
|
+
const utils_1 = require("@noble/hashes/utils");
|
|
26
33
|
/**
|
|
27
34
|
* Domain-separation label mixed into the hash preimage (RFC-MACP-0013 §4).
|
|
28
35
|
* Changing this string changes every hash this module produces — that is a
|
|
29
36
|
* protocol MINOR version bump, not a routine edit.
|
|
30
37
|
*/
|
|
31
38
|
const LABEL = 'macp-commitment-hash/1';
|
|
39
|
+
const UTF8 = new TextEncoder();
|
|
32
40
|
/**
|
|
33
41
|
* Escapes a single JSON string value per RFC 8785 §3.2.2.2:
|
|
34
42
|
* - short-form escapes for backspace/tab/newline/formfeed/carriage-return,
|
|
@@ -45,8 +53,8 @@ const LABEL = 'macp-commitment-hash/1';
|
|
|
45
53
|
* specially: `for (const ch of str)` below yields it as its own one-code-unit
|
|
46
54
|
* step and it is emitted literally, unescaped, like any other character. The
|
|
47
55
|
* actual loss happens one step later, in `commitmentHash`, when the finished
|
|
48
|
-
* JCS string is encoded to bytes via `
|
|
49
|
-
*
|
|
56
|
+
* JCS string is encoded to bytes via `TextEncoder` — the standard lossy UTF-8
|
|
57
|
+
* encoder silently substitutes a lone surrogate with
|
|
50
58
|
* U+FFFD (the Unicode replacement character) before hashing. The observable
|
|
51
59
|
* consequence: a field containing a lone surrogate (e.g. `'\uD800'`) hashes
|
|
52
60
|
* IDENTICALLY to the same field containing a literal U+FFFD in that position
|
|
@@ -211,8 +219,7 @@ function canonicalizeCommitmentPayload(payload) {
|
|
|
211
219
|
function commitmentHash(payload) {
|
|
212
220
|
const jcs = canonicalizeCommitmentPayload(payload);
|
|
213
221
|
const preimage = `${LABEL}:${jcs}`;
|
|
214
|
-
|
|
215
|
-
return `sha256:${digest}`;
|
|
222
|
+
return `sha256:${(0, utils_1.bytesToHex)((0, sha2_1.sha256)(UTF8.encode(preimage)))}`;
|
|
216
223
|
}
|
|
217
224
|
/**
|
|
218
225
|
* The same shape `commitmentHash` always produces: `sha256:` followed by 64
|
|
@@ -376,6 +383,24 @@ function decodeCommitmentPayloadStrict(wire) {
|
|
|
376
383
|
}
|
|
377
384
|
return payload;
|
|
378
385
|
}
|
|
386
|
+
/**
|
|
387
|
+
* Strictly decodes a `CommitmentPayload` wire encoding for the decision-receipt
|
|
388
|
+
* verifier (`./receipt`), which must compare the wire against the receipt's
|
|
389
|
+
* own `commitment`. `undefined` wherever {@link verifyCommitmentHash} would say
|
|
390
|
+
* `'cannot_verify'`. Internal: not re-exported from the package root.
|
|
391
|
+
*/
|
|
392
|
+
function decodeCommitmentWire(wire) {
|
|
393
|
+
if (!(wire instanceof Uint8Array))
|
|
394
|
+
return undefined;
|
|
395
|
+
try {
|
|
396
|
+
return decodeCommitmentPayloadStrict(wire);
|
|
397
|
+
}
|
|
398
|
+
catch (err) {
|
|
399
|
+
if (err instanceof CannotVerify)
|
|
400
|
+
return undefined;
|
|
401
|
+
throw err;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
379
404
|
/**
|
|
380
405
|
* Verifies a claimed commitment hash against a `CommitmentPayload`'s Protobuf
|
|
381
406
|
* wire encoding (RFC-MACP-0013 §6.1), returning exactly one of:
|
package/dist/constants.d.ts
CHANGED
|
@@ -37,6 +37,11 @@ export declare const INVALID_REQUEST = "INVALID_REQUEST";
|
|
|
37
37
|
export declare const STREAM_LAGGED = "STREAM_LAGGED";
|
|
38
38
|
/** GetManifest for an agent this runtime does not host — NOT_FOUND. */
|
|
39
39
|
export declare const AGENT_NOT_FOUND = "AGENT_NOT_FOUND";
|
|
40
|
+
/**
|
|
41
|
+
* WhatIfReplay on a session whose full accepted history the runtime no longer
|
|
42
|
+
* holds (e.g. compacted when it ended) — FAILED_PRECONDITION. RFC-MACP-0012 §8.1.
|
|
43
|
+
*/
|
|
44
|
+
export declare const HISTORY_UNAVAILABLE = "HISTORY_UNAVAILABLE";
|
|
40
45
|
export declare const DENY_VETO = "veto";
|
|
41
46
|
export declare const DENY_VETO_HOLD = "veto_hold";
|
|
42
47
|
export declare const DENY_QUORUM_NOT_MET = "quorum_not_met";
|
package/dist/constants.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.KNOWN_DENY_CODES = exports.DENY_UNSPECIFIED = exports.DENY_INVALID_RULES = exports.DENY_UNSUPPORTED_SCHEMA_VERSION = exports.DENY_REASSIGNMENT_NOT_ALLOWED = exports.DENY_OUTPUT_REQUIRED = exports.DENY_CRITERION_NOT_MET = exports.DENY_DECLINE_NOT_BACKED = exports.DENY_DECLINE_OVER_APPROVAL = exports.DENY_NO_VOTES = exports.DENY_EVALUATION_REQUIRED = exports.DENY_CONFIDENCE_BELOW_MINIMUM = exports.DENY_THRESHOLD_NOT_MET = exports.DENY_QUORUM_NOT_MET = exports.DENY_VETO_HOLD = exports.DENY_VETO = exports.AGENT_NOT_FOUND = exports.STREAM_LAGGED = exports.INVALID_REQUEST = exports.REGISTRY_READ_ONLY = exports.INVALID_POLICY_DEFINITION = exports.UNKNOWN_POLICY_VERSION = exports.INVALID_SESSION_ID = exports.POLICY_DENIED = exports.INTERNAL_ERROR = exports.RATE_LIMITED = exports.PAYLOAD_TOO_LARGE = exports.DUPLICATE_MESSAGE = exports.UNAUTHENTICATED = exports.FORBIDDEN = exports.MODE_NOT_SUPPORTED = exports.SESSION_NOT_OPEN = exports.SESSION_NOT_FOUND = exports.SESSION_ALREADY_EXISTS = exports.INVALID_ENVELOPE = exports.UNSUPPORTED_PROTOCOL_VERSION = exports.STANDARD_MODES = exports.MODE_MULTI_ROUND = exports.MODE_QUORUM = exports.MODE_HANDOFF = exports.MODE_TASK = exports.MODE_PROPOSAL = exports.MODE_DECISION = exports.DEFAULT_POLICY_VERSION = exports.DEFAULT_CONFIGURATION_VERSION = exports.DEFAULT_MODE_VERSION = exports.MACP_VERSION = void 0;
|
|
3
|
+
exports.KNOWN_DENY_CODES = exports.DENY_UNSPECIFIED = exports.DENY_INVALID_RULES = exports.DENY_UNSUPPORTED_SCHEMA_VERSION = exports.DENY_REASSIGNMENT_NOT_ALLOWED = exports.DENY_OUTPUT_REQUIRED = exports.DENY_CRITERION_NOT_MET = exports.DENY_DECLINE_NOT_BACKED = exports.DENY_DECLINE_OVER_APPROVAL = exports.DENY_NO_VOTES = exports.DENY_EVALUATION_REQUIRED = exports.DENY_CONFIDENCE_BELOW_MINIMUM = exports.DENY_THRESHOLD_NOT_MET = exports.DENY_QUORUM_NOT_MET = exports.DENY_VETO_HOLD = exports.DENY_VETO = exports.HISTORY_UNAVAILABLE = exports.AGENT_NOT_FOUND = exports.STREAM_LAGGED = exports.INVALID_REQUEST = exports.REGISTRY_READ_ONLY = exports.INVALID_POLICY_DEFINITION = exports.UNKNOWN_POLICY_VERSION = exports.INVALID_SESSION_ID = exports.POLICY_DENIED = exports.INTERNAL_ERROR = exports.RATE_LIMITED = exports.PAYLOAD_TOO_LARGE = exports.DUPLICATE_MESSAGE = exports.UNAUTHENTICATED = exports.FORBIDDEN = exports.MODE_NOT_SUPPORTED = exports.SESSION_NOT_OPEN = exports.SESSION_NOT_FOUND = exports.SESSION_ALREADY_EXISTS = exports.INVALID_ENVELOPE = exports.UNSUPPORTED_PROTOCOL_VERSION = exports.STANDARD_MODES = exports.MODE_MULTI_ROUND = exports.MODE_QUORUM = exports.MODE_HANDOFF = exports.MODE_TASK = exports.MODE_PROPOSAL = exports.MODE_DECISION = exports.DEFAULT_POLICY_VERSION = exports.DEFAULT_CONFIGURATION_VERSION = exports.DEFAULT_MODE_VERSION = exports.MACP_VERSION = void 0;
|
|
4
4
|
exports.MACP_VERSION = '1.0';
|
|
5
5
|
exports.DEFAULT_MODE_VERSION = '1.0.0';
|
|
6
6
|
exports.DEFAULT_CONFIGURATION_VERSION = 'config.default';
|
|
@@ -44,6 +44,11 @@ exports.INVALID_REQUEST = 'INVALID_REQUEST';
|
|
|
44
44
|
exports.STREAM_LAGGED = 'STREAM_LAGGED';
|
|
45
45
|
/** GetManifest for an agent this runtime does not host — NOT_FOUND. */
|
|
46
46
|
exports.AGENT_NOT_FOUND = 'AGENT_NOT_FOUND';
|
|
47
|
+
/**
|
|
48
|
+
* WhatIfReplay on a session whose full accepted history the runtime no longer
|
|
49
|
+
* holds (e.g. compacted when it ended) — FAILED_PRECONDITION. RFC-MACP-0012 §8.1.
|
|
50
|
+
*/
|
|
51
|
+
exports.HISTORY_UNAVAILABLE = 'HISTORY_UNAVAILABLE';
|
|
47
52
|
// Structured POLICY_DENIED denial codes (`PolicyDenial.code`, runtime ≥ 0.9.0;
|
|
48
53
|
// RFC-MACP-0012 §6.2). Open set — names match python-sdk's exports.
|
|
49
54
|
exports.DENY_VETO = 'veto';
|
package/dist/index.d.ts
CHANGED
|
@@ -14,6 +14,7 @@ export * from './proposal';
|
|
|
14
14
|
export * from './proto-registry';
|
|
15
15
|
export * from './retry';
|
|
16
16
|
export * from './quorum';
|
|
17
|
+
export { RECEIPT_FORMAT, buildReceipt, commitmentFromProjection, verifyReceipt, type AttestationOutcome, type BuildReceiptOptions, type DecisionReceipt, type JwsAttestation, type ReceiptAttestation, type ReceiptCommitment, type ReceiptIntegrity, type ReceiptSigner, type ReceiptVerdict, type ReceiptVerification, type VendorAttestation, } from './receipt';
|
|
17
18
|
export * from './task';
|
|
18
19
|
export * from './types';
|
|
19
20
|
export * from './validation';
|
package/dist/index.js
CHANGED
|
@@ -36,7 +36,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
36
36
|
};
|
|
37
37
|
})();
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
-
exports.agent = exports.VERSION = exports.logger = exports.configureLogging = exports.verifyCommitmentHash = exports.isCanonicalCommitmentHash = exports.commitmentHash = exports.Auth = void 0;
|
|
39
|
+
exports.agent = exports.VERSION = exports.verifyReceipt = exports.commitmentFromProjection = exports.buildReceipt = exports.RECEIPT_FORMAT = exports.logger = exports.configureLogging = exports.verifyCommitmentHash = exports.isCanonicalCommitmentHash = exports.commitmentHash = exports.Auth = void 0;
|
|
40
40
|
// Auth: export only the public constructors and types. The helpers
|
|
41
41
|
// `validateAuth`, `authSender`, `assertSenderMatchesIdentity`, and
|
|
42
42
|
// `metadataFromAuth` are SDK-internal — importable from the `./auth`
|
|
@@ -78,6 +78,13 @@ __exportStar(require("./proposal"), exports);
|
|
|
78
78
|
__exportStar(require("./proto-registry"), exports);
|
|
79
79
|
__exportStar(require("./retry"), exports);
|
|
80
80
|
__exportStar(require("./quorum"), exports);
|
|
81
|
+
// RFC-MACP-0013 Appendix A decision receipts. Also published as the
|
|
82
|
+
// browser-safe `macp-sdk-typescript/receipt` subpath (no gRPC, no node:crypto).
|
|
83
|
+
var receipt_1 = require("./receipt");
|
|
84
|
+
Object.defineProperty(exports, "RECEIPT_FORMAT", { enumerable: true, get: function () { return receipt_1.RECEIPT_FORMAT; } });
|
|
85
|
+
Object.defineProperty(exports, "buildReceipt", { enumerable: true, get: function () { return receipt_1.buildReceipt; } });
|
|
86
|
+
Object.defineProperty(exports, "commitmentFromProjection", { enumerable: true, get: function () { return receipt_1.commitmentFromProjection; } });
|
|
87
|
+
Object.defineProperty(exports, "verifyReceipt", { enumerable: true, get: function () { return receipt_1.verifyReceipt; } });
|
|
81
88
|
__exportStar(require("./task"), exports);
|
|
82
89
|
__exportStar(require("./types"), exports);
|
|
83
90
|
__exportStar(require("./validation"), exports);
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import type { CommitmentPayload } from './types';
|
|
2
|
+
/** The only receipt format this module issues and verifies. */
|
|
3
|
+
export declare const RECEIPT_FORMAT = "macp-receipt/1";
|
|
4
|
+
/** RFC-MACP-0013 §3 projection of a `CommitmentPayload`, as recorded in a receipt. */
|
|
5
|
+
export interface ReceiptCommitment {
|
|
6
|
+
commitment_id: string;
|
|
7
|
+
action: string;
|
|
8
|
+
authority_scope: string;
|
|
9
|
+
reason: string;
|
|
10
|
+
mode_version: string;
|
|
11
|
+
policy_version: string;
|
|
12
|
+
configuration_version: string;
|
|
13
|
+
outcome_positive: boolean;
|
|
14
|
+
supersedes?: {
|
|
15
|
+
session_id: string;
|
|
16
|
+
commitment_hash: string;
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
export interface JwsAttestation {
|
|
20
|
+
type: 'jws';
|
|
21
|
+
/** Compact JWS with a detached payload: `<protected header>..<signature>`. */
|
|
22
|
+
jws: string;
|
|
23
|
+
}
|
|
24
|
+
export interface VendorAttestation {
|
|
25
|
+
type: 'vendor';
|
|
26
|
+
format: string;
|
|
27
|
+
binds: 'commitment_hash' | 'receipt' | 'other';
|
|
28
|
+
value: string;
|
|
29
|
+
}
|
|
30
|
+
export type ReceiptAttestation = JwsAttestation | VendorAttestation;
|
|
31
|
+
/** A `macp-receipt/1` decision receipt (RFC-MACP-0013 Appendix A.2). */
|
|
32
|
+
export interface DecisionReceipt {
|
|
33
|
+
receipt_format: typeof RECEIPT_FORMAT;
|
|
34
|
+
session_id: string;
|
|
35
|
+
mode: string;
|
|
36
|
+
commitment: ReceiptCommitment;
|
|
37
|
+
commitment_hash: string;
|
|
38
|
+
commitment_wire_b64?: string;
|
|
39
|
+
semantics_rev?: {
|
|
40
|
+
implementation: string;
|
|
41
|
+
rev: number;
|
|
42
|
+
};
|
|
43
|
+
transcript?: {
|
|
44
|
+
digest_alg: string;
|
|
45
|
+
digest: string;
|
|
46
|
+
message_count?: number;
|
|
47
|
+
};
|
|
48
|
+
issuer: string;
|
|
49
|
+
issued_at: string;
|
|
50
|
+
attestations: ReceiptAttestation[];
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Signs a receipt with Ed25519: either a 32-byte private key seed, or a
|
|
54
|
+
* callback that returns the 64-byte signature over `signingInput` (for a key
|
|
55
|
+
* held elsewhere, such as an HSM, as long as it signs synchronously).
|
|
56
|
+
*/
|
|
57
|
+
export type ReceiptSigner = {
|
|
58
|
+
kid: string;
|
|
59
|
+
privateKey: Uint8Array;
|
|
60
|
+
} | {
|
|
61
|
+
kid: string;
|
|
62
|
+
sign: (signingInput: Uint8Array) => Uint8Array;
|
|
63
|
+
};
|
|
64
|
+
export interface BuildReceiptOptions {
|
|
65
|
+
sessionId: string;
|
|
66
|
+
mode: string;
|
|
67
|
+
/** Opaque identifier of the issuer (A.2). Not a trust anchor. */
|
|
68
|
+
issuer: string;
|
|
69
|
+
/** RFC 3339 UTC time of issue, recorded verbatim. */
|
|
70
|
+
issuedAt: string;
|
|
71
|
+
/** Adds one `jws` attestation. Without it, `attestations` is `[]`. */
|
|
72
|
+
signer?: ReceiptSigner;
|
|
73
|
+
/** RFC-MACP-0003 §3.2 semantics revision, scoped to its implementation. */
|
|
74
|
+
semanticsRev?: {
|
|
75
|
+
implementation: string;
|
|
76
|
+
rev: number;
|
|
77
|
+
};
|
|
78
|
+
/** Implementation-defined digest of the session history (RFC-MACP-0013 §10). */
|
|
79
|
+
transcript?: {
|
|
80
|
+
digestAlg: string;
|
|
81
|
+
digest: string;
|
|
82
|
+
messageCount?: number;
|
|
83
|
+
};
|
|
84
|
+
/** The payload's Protobuf wire encoding; recorded as `commitment_wire_b64`. */
|
|
85
|
+
commitmentWire?: Uint8Array;
|
|
86
|
+
}
|
|
87
|
+
/** A.4 step 2. `null` only when the receipt is refused at step 1. */
|
|
88
|
+
export type ReceiptIntegrity = 'match' | 'mismatch' | 'cannot_verify' | null;
|
|
89
|
+
/** A.4 step 3, one per attestation. */
|
|
90
|
+
export type AttestationOutcome = 'valid' | 'invalid' | 'unknown_key' | 'unsupported';
|
|
91
|
+
/** A.4 step 4 (step 1 for `refused`). */
|
|
92
|
+
export type ReceiptVerdict = 'attested' | 'integrity_only' | 'unverified' | 'refused';
|
|
93
|
+
export interface ReceiptVerification {
|
|
94
|
+
integrity: ReceiptIntegrity;
|
|
95
|
+
/** One outcome per entry of `attestations`, in order; `[]` unless integrity is `match`. */
|
|
96
|
+
attestations: AttestationOutcome[];
|
|
97
|
+
verdict: ReceiptVerdict;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Converts a snake_case §3 projection (a receipt's `commitment`, or the same
|
|
101
|
+
* shape from a control plane) into the camelCase `CommitmentPayload` that
|
|
102
|
+
* {@link buildReceipt} and `commitmentHash` take. Throws `MacpSdkError` when
|
|
103
|
+
* it is not a complete, well-typed projection.
|
|
104
|
+
*/
|
|
105
|
+
export declare function commitmentFromProjection(projection: ReceiptCommitment): CommitmentPayload;
|
|
106
|
+
/**
|
|
107
|
+
* Builds a `macp-receipt/1` receipt for `commitment` (RFC-MACP-0013
|
|
108
|
+
* Appendix A). Computes `commitment_hash`; with a `signer`, adds one
|
|
109
|
+
* detached-payload EdDSA `jws` attestation over the receipt body. Ed25519 is
|
|
110
|
+
* deterministic, so the same inputs always give the same receipt.
|
|
111
|
+
*
|
|
112
|
+
* Throws `MacpSdkError` on a missing required option, and when
|
|
113
|
+
* `commitmentWire` does not verify against, or does not decode to,
|
|
114
|
+
* `commitment`, so no receipt is issued that its own verifier would reject.
|
|
115
|
+
*/
|
|
116
|
+
export declare function buildReceipt(commitment: CommitmentPayload, options: BuildReceiptOptions): DecisionReceipt;
|
|
117
|
+
/**
|
|
118
|
+
* Verifies a decision receipt (RFC-MACP-0013 Appendix A.4) against the keys
|
|
119
|
+
* the caller trusts: `trustedKeys` maps a `kid` to an Ed25519 public key as a
|
|
120
|
+
* JWK `x` (base64url). Returns the integrity outcome, one outcome per
|
|
121
|
+
* attestation, and the overall verdict, in the spec vectors' vocabulary.
|
|
122
|
+
* Never throws: malformed input yields an outcome, not an exception.
|
|
123
|
+
*/
|
|
124
|
+
export declare function verifyReceipt(receipt: unknown, trustedKeys: Record<string, string>): ReceiptVerification;
|
package/dist/receipt.js
ADDED
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.RECEIPT_FORMAT = void 0;
|
|
4
|
+
exports.commitmentFromProjection = commitmentFromProjection;
|
|
5
|
+
exports.buildReceipt = buildReceipt;
|
|
6
|
+
exports.verifyReceipt = verifyReceipt;
|
|
7
|
+
/**
|
|
8
|
+
* RFC-MACP-0013 Appendix A decision receipts (`macp-receipt/1`).
|
|
9
|
+
*
|
|
10
|
+
* A receipt is a portable record of one binding Commitment that a third party
|
|
11
|
+
* can check offline. Two properties are reported separately (A.1):
|
|
12
|
+
*
|
|
13
|
+
* - **integrity**: the recorded `commitment` has the recorded
|
|
14
|
+
* `commitment_hash` (and, when `commitment_wire_b64` is present, the wire
|
|
15
|
+
* passes §6.1 verification and decodes to `commitment`). Needs no keys.
|
|
16
|
+
* - **authenticity**: some attestation over the receipt body is valid for a
|
|
17
|
+
* key the verifier trusts. Only `jws` attestations with `alg: EdDSA` are
|
|
18
|
+
* implemented; `vendor` attestations are always `unsupported`.
|
|
19
|
+
*
|
|
20
|
+
* Receipt JSON keeps the schema's snake_case keys
|
|
21
|
+
* (`schemas/json/macp-decision-receipt.schema.json` in the spec repo); the
|
|
22
|
+
* functions here take camelCase options, like the rest of the SDK.
|
|
23
|
+
*
|
|
24
|
+
* Browser-safe: this module and `./commitment-hash` import no gRPC, no
|
|
25
|
+
* `node:crypto` and no `Buffer`. It is published as the
|
|
26
|
+
* `macp-sdk-typescript/receipt` subpath for client bundles.
|
|
27
|
+
*/
|
|
28
|
+
const ed25519_1 = require("@noble/curves/ed25519");
|
|
29
|
+
const commitment_hash_1 = require("./commitment-hash");
|
|
30
|
+
const errors_1 = require("./errors");
|
|
31
|
+
/** The only receipt format this module issues and verifies. */
|
|
32
|
+
exports.RECEIPT_FORMAT = 'macp-receipt/1';
|
|
33
|
+
// --- encodings -------------------------------------------------------------
|
|
34
|
+
const UTF8 = new TextEncoder();
|
|
35
|
+
const B64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
|
36
|
+
const B64URL = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_';
|
|
37
|
+
const STRICT_B64 = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
|
|
38
|
+
const STRICT_B64URL = /^[A-Za-z0-9_-]+$/;
|
|
39
|
+
function encodeBase64(bytes, alphabet, pad) {
|
|
40
|
+
let out = '';
|
|
41
|
+
for (let i = 0; i < bytes.length; i += 3) {
|
|
42
|
+
const n = (bytes[i] << 16) | ((bytes[i + 1] ?? 0) << 8) | (bytes[i + 2] ?? 0);
|
|
43
|
+
const chars = Math.min(4, Math.ceil(((bytes.length - i) * 8) / 6));
|
|
44
|
+
for (let c = 0; c < 4; c++) {
|
|
45
|
+
if (c < chars)
|
|
46
|
+
out += alphabet[(n >> (18 - 6 * c)) & 63];
|
|
47
|
+
else if (pad)
|
|
48
|
+
out += '=';
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
/** Decodes unpadded base64 digits; the caller has already checked the alphabet. */
|
|
54
|
+
function decodeDigits(text, alphabet) {
|
|
55
|
+
if (text.length % 4 === 1)
|
|
56
|
+
return undefined;
|
|
57
|
+
const out = new Uint8Array(Math.floor((text.length * 6) / 8));
|
|
58
|
+
let acc = 0;
|
|
59
|
+
let bits = 0;
|
|
60
|
+
let j = 0;
|
|
61
|
+
for (const ch of text) {
|
|
62
|
+
acc = (acc << 6) | alphabet.indexOf(ch);
|
|
63
|
+
bits += 6;
|
|
64
|
+
if (bits >= 8) {
|
|
65
|
+
bits -= 8;
|
|
66
|
+
out[j++] = (acc >> bits) & 0xff;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return out;
|
|
70
|
+
}
|
|
71
|
+
/** Strict standard base64, padding required (Python's `b64decode(validate=True)`). */
|
|
72
|
+
function decodeBase64(text) {
|
|
73
|
+
if (!STRICT_B64.test(text))
|
|
74
|
+
return undefined;
|
|
75
|
+
return decodeDigits(text.replace(/=+$/, ''), B64);
|
|
76
|
+
}
|
|
77
|
+
/** Strict unpadded base64url, non-empty. */
|
|
78
|
+
function decodeBase64Url(text) {
|
|
79
|
+
if (!STRICT_B64URL.test(text))
|
|
80
|
+
return undefined;
|
|
81
|
+
return decodeDigits(text, B64URL);
|
|
82
|
+
}
|
|
83
|
+
const b64url = (bytes) => encodeBase64(bytes, B64URL, false);
|
|
84
|
+
/**
|
|
85
|
+
* RFC 8785 serialization of a receipt body. Unlike the §3 projection, a body
|
|
86
|
+
* carries integers (`rev`, `message_count`); `JSON.stringify` of a finite
|
|
87
|
+
* number is exactly the JCS number form. Keys sort by UTF-16 code units,
|
|
88
|
+
* which is JavaScript's default string order.
|
|
89
|
+
*/
|
|
90
|
+
function jcs(value) {
|
|
91
|
+
if (value === null)
|
|
92
|
+
return 'null';
|
|
93
|
+
if (typeof value === 'boolean')
|
|
94
|
+
return value ? 'true' : 'false';
|
|
95
|
+
if (typeof value === 'string')
|
|
96
|
+
return (0, commitment_hash_1.jcsString)(value);
|
|
97
|
+
if (typeof value === 'number') {
|
|
98
|
+
if (!Number.isFinite(value))
|
|
99
|
+
throw new errors_1.MacpSdkError('a receipt body cannot contain a non-finite number');
|
|
100
|
+
return JSON.stringify(value);
|
|
101
|
+
}
|
|
102
|
+
if (Array.isArray(value))
|
|
103
|
+
return `[${value.map(jcs).join(',')}]`;
|
|
104
|
+
if (typeof value === 'object') {
|
|
105
|
+
const entries = Object.entries(value).filter(([, v]) => v !== undefined);
|
|
106
|
+
entries.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
|
|
107
|
+
return `{${entries.map(([k, v]) => `${(0, commitment_hash_1.jcsString)(k)}:${jcs(v)}`).join(',')}}`;
|
|
108
|
+
}
|
|
109
|
+
throw new errors_1.MacpSdkError(`a receipt body cannot contain a ${typeof value}`);
|
|
110
|
+
}
|
|
111
|
+
/** The receipt body (A.2): the receipt without `attestations`, in JCS. */
|
|
112
|
+
function receiptBody(receipt) {
|
|
113
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars -- removed from the body
|
|
114
|
+
const { attestations: _omitted, ...body } = receipt;
|
|
115
|
+
return UTF8.encode(jcs(body));
|
|
116
|
+
}
|
|
117
|
+
// --- projection ------------------------------------------------------------
|
|
118
|
+
const STRING_FIELDS = [
|
|
119
|
+
'commitment_id',
|
|
120
|
+
'action',
|
|
121
|
+
'authority_scope',
|
|
122
|
+
'reason',
|
|
123
|
+
'mode_version',
|
|
124
|
+
'policy_version',
|
|
125
|
+
'configuration_version',
|
|
126
|
+
];
|
|
127
|
+
const ALLOWED_FIELDS = new Set([...STRING_FIELDS, 'outcome_positive', 'supersedes']);
|
|
128
|
+
/** The §3 projection, with `policy_version`/`outcome_positive` materialized and `supersedes` only when present. */
|
|
129
|
+
function project(payload) {
|
|
130
|
+
const out = {
|
|
131
|
+
commitment_id: payload.commitmentId ?? '',
|
|
132
|
+
action: payload.action ?? '',
|
|
133
|
+
authority_scope: payload.authorityScope ?? '',
|
|
134
|
+
reason: payload.reason ?? '',
|
|
135
|
+
mode_version: payload.modeVersion ?? '',
|
|
136
|
+
policy_version: payload.policyVersion ?? '',
|
|
137
|
+
configuration_version: payload.configurationVersion ?? '',
|
|
138
|
+
outcome_positive: payload.outcomePositive ?? false,
|
|
139
|
+
};
|
|
140
|
+
if (payload.supersedes != null) {
|
|
141
|
+
out.supersedes = {
|
|
142
|
+
session_id: payload.supersedes.sessionId ?? '',
|
|
143
|
+
commitment_hash: payload.supersedes.commitmentHash ?? '',
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
return out;
|
|
147
|
+
}
|
|
148
|
+
const isRecord = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
149
|
+
/**
|
|
150
|
+
* Reads a receipt's `commitment` back into a `CommitmentPayload`, or
|
|
151
|
+
* `undefined` when it is not a complete, well-typed §3 projection: an unknown
|
|
152
|
+
* member at either level, a missing member, or a wrongly typed one. That is
|
|
153
|
+
* outside what `macp-commitment-hash/1` covers, so it is `cannot_verify`.
|
|
154
|
+
*/
|
|
155
|
+
function readCommitment(value) {
|
|
156
|
+
if (!isRecord(value) || Object.keys(value).some((k) => !ALLOWED_FIELDS.has(k)))
|
|
157
|
+
return undefined;
|
|
158
|
+
if (STRING_FIELDS.some((f) => typeof value[f] !== 'string') || typeof value.outcome_positive !== 'boolean') {
|
|
159
|
+
return undefined;
|
|
160
|
+
}
|
|
161
|
+
const payload = {
|
|
162
|
+
commitmentId: value.commitment_id,
|
|
163
|
+
action: value.action,
|
|
164
|
+
authorityScope: value.authority_scope,
|
|
165
|
+
reason: value.reason,
|
|
166
|
+
modeVersion: value.mode_version,
|
|
167
|
+
policyVersion: value.policy_version,
|
|
168
|
+
configurationVersion: value.configuration_version,
|
|
169
|
+
outcomePositive: value.outcome_positive,
|
|
170
|
+
};
|
|
171
|
+
if ('supersedes' in value) {
|
|
172
|
+
const sup = value.supersedes;
|
|
173
|
+
if (!isRecord(sup) ||
|
|
174
|
+
Object.keys(sup).some((k) => k !== 'session_id' && k !== 'commitment_hash') ||
|
|
175
|
+
typeof sup.session_id !== 'string' ||
|
|
176
|
+
typeof sup.commitment_hash !== 'string') {
|
|
177
|
+
return undefined;
|
|
178
|
+
}
|
|
179
|
+
payload.supersedes = { sessionId: sup.session_id, commitmentHash: sup.commitment_hash };
|
|
180
|
+
}
|
|
181
|
+
return payload;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Converts a snake_case §3 projection (a receipt's `commitment`, or the same
|
|
185
|
+
* shape from a control plane) into the camelCase `CommitmentPayload` that
|
|
186
|
+
* {@link buildReceipt} and `commitmentHash` take. Throws `MacpSdkError` when
|
|
187
|
+
* it is not a complete, well-typed projection.
|
|
188
|
+
*/
|
|
189
|
+
function commitmentFromProjection(projection) {
|
|
190
|
+
const payload = readCommitment(projection);
|
|
191
|
+
if (!payload)
|
|
192
|
+
throw new errors_1.MacpSdkError('commitmentFromProjection: not a complete RFC-MACP-0013 §3 projection');
|
|
193
|
+
return payload;
|
|
194
|
+
}
|
|
195
|
+
function sameProjection(a, b) {
|
|
196
|
+
if (STRING_FIELDS.some((f) => a[f] !== b[f]) || a.outcome_positive !== b.outcome_positive)
|
|
197
|
+
return false;
|
|
198
|
+
if (!a.supersedes || !b.supersedes)
|
|
199
|
+
return !a.supersedes && !b.supersedes;
|
|
200
|
+
return (a.supersedes.session_id === b.supersedes.session_id && a.supersedes.commitment_hash === b.supersedes.commitment_hash);
|
|
201
|
+
}
|
|
202
|
+
// --- build -----------------------------------------------------------------
|
|
203
|
+
function requireText(value, name) {
|
|
204
|
+
if (typeof value !== 'string' || value === '')
|
|
205
|
+
throw new errors_1.MacpSdkError(`buildReceipt: ${name} must be a non-empty string`);
|
|
206
|
+
return value;
|
|
207
|
+
}
|
|
208
|
+
function requireCount(value, name) {
|
|
209
|
+
if (!Number.isSafeInteger(value) || value < 0) {
|
|
210
|
+
throw new errors_1.MacpSdkError(`buildReceipt: ${name} must be a non-negative integer`);
|
|
211
|
+
}
|
|
212
|
+
return value;
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Builds a `macp-receipt/1` receipt for `commitment` (RFC-MACP-0013
|
|
216
|
+
* Appendix A). Computes `commitment_hash`; with a `signer`, adds one
|
|
217
|
+
* detached-payload EdDSA `jws` attestation over the receipt body. Ed25519 is
|
|
218
|
+
* deterministic, so the same inputs always give the same receipt.
|
|
219
|
+
*
|
|
220
|
+
* Throws `MacpSdkError` on a missing required option, and when
|
|
221
|
+
* `commitmentWire` does not verify against, or does not decode to,
|
|
222
|
+
* `commitment`, so no receipt is issued that its own verifier would reject.
|
|
223
|
+
*/
|
|
224
|
+
function buildReceipt(commitment, options) {
|
|
225
|
+
const projection = project(commitment);
|
|
226
|
+
const hash = (0, commitment_hash_1.commitmentHash)(commitment);
|
|
227
|
+
const receipt = {
|
|
228
|
+
receipt_format: exports.RECEIPT_FORMAT,
|
|
229
|
+
session_id: requireText(options.sessionId, 'sessionId'),
|
|
230
|
+
mode: requireText(options.mode, 'mode'),
|
|
231
|
+
commitment: projection,
|
|
232
|
+
commitment_hash: hash,
|
|
233
|
+
};
|
|
234
|
+
// Key order follows the spec's example and vectors: optional members after issued_at.
|
|
235
|
+
if (options.semanticsRev !== undefined) {
|
|
236
|
+
receipt.semantics_rev = {
|
|
237
|
+
implementation: requireText(options.semanticsRev.implementation, 'semanticsRev.implementation'),
|
|
238
|
+
rev: requireCount(options.semanticsRev.rev, 'semanticsRev.rev'),
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
receipt.issuer = requireText(options.issuer, 'issuer');
|
|
242
|
+
receipt.issued_at = requireText(options.issuedAt, 'issuedAt');
|
|
243
|
+
if (options.commitmentWire !== undefined) {
|
|
244
|
+
const wire = options.commitmentWire;
|
|
245
|
+
// A §6.1 match means the wire projects to exactly this commitment.
|
|
246
|
+
if (!(wire instanceof Uint8Array) || (0, commitment_hash_1.verifyCommitmentHash)(wire, hash) !== 'match') {
|
|
247
|
+
throw new errors_1.MacpSdkError('buildReceipt: commitmentWire is not the wire encoding of commitment');
|
|
248
|
+
}
|
|
249
|
+
receipt.commitment_wire_b64 = encodeBase64(wire, B64, true);
|
|
250
|
+
}
|
|
251
|
+
if (options.transcript !== undefined) {
|
|
252
|
+
const t = options.transcript;
|
|
253
|
+
receipt.transcript = {
|
|
254
|
+
digest_alg: requireText(t.digestAlg, 'transcript.digestAlg'),
|
|
255
|
+
digest: requireText(t.digest, 'transcript.digest'),
|
|
256
|
+
};
|
|
257
|
+
if (t.messageCount !== undefined)
|
|
258
|
+
receipt.transcript.message_count = requireCount(t.messageCount, 'transcript.messageCount');
|
|
259
|
+
}
|
|
260
|
+
receipt.attestations = [];
|
|
261
|
+
if (options.signer)
|
|
262
|
+
receipt.attestations.push(sign(receipt, options.signer));
|
|
263
|
+
return receipt;
|
|
264
|
+
}
|
|
265
|
+
function sign(receipt, signer) {
|
|
266
|
+
const kid = requireText(signer.kid, 'signer.kid');
|
|
267
|
+
// The protected header in JCS form: {"alg":"EdDSA","kid":"..."}.
|
|
268
|
+
const header = b64url(UTF8.encode(`{"alg":"EdDSA","kid":${(0, commitment_hash_1.jcsString)(kid)}}`));
|
|
269
|
+
const signingInput = UTF8.encode(`${header}.${b64url(receiptBody(receipt))}`);
|
|
270
|
+
let signature;
|
|
271
|
+
if ('privateKey' in signer) {
|
|
272
|
+
if (!(signer.privateKey instanceof Uint8Array) || signer.privateKey.length !== 32) {
|
|
273
|
+
throw new errors_1.MacpSdkError('buildReceipt: signer.privateKey must be a 32-byte Ed25519 seed');
|
|
274
|
+
}
|
|
275
|
+
signature = ed25519_1.ed25519.sign(signingInput, signer.privateKey);
|
|
276
|
+
}
|
|
277
|
+
else {
|
|
278
|
+
signature = signer.sign(signingInput);
|
|
279
|
+
if (!(signature instanceof Uint8Array) || signature.length !== 64) {
|
|
280
|
+
throw new errors_1.MacpSdkError('buildReceipt: signer.sign must return a 64-byte Ed25519 signature');
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
return { type: 'jws', jws: `${header}..${b64url(signature)}` };
|
|
284
|
+
}
|
|
285
|
+
// --- verify ----------------------------------------------------------------
|
|
286
|
+
function integrity(receipt) {
|
|
287
|
+
const payload = readCommitment(receipt.commitment);
|
|
288
|
+
if (!payload)
|
|
289
|
+
return 'cannot_verify';
|
|
290
|
+
const claimed = receipt.commitment_hash;
|
|
291
|
+
if (typeof claimed !== 'string' || (0, commitment_hash_1.commitmentHash)(payload) !== claimed)
|
|
292
|
+
return 'mismatch';
|
|
293
|
+
if ('commitment_wire_b64' in receipt) {
|
|
294
|
+
const encoded = receipt.commitment_wire_b64;
|
|
295
|
+
const wire = typeof encoded === 'string' ? decodeBase64(encoded) : undefined;
|
|
296
|
+
if (!wire)
|
|
297
|
+
return 'cannot_verify';
|
|
298
|
+
const outcome = (0, commitment_hash_1.verifyCommitmentHash)(wire, claimed);
|
|
299
|
+
if (outcome !== 'match')
|
|
300
|
+
return outcome;
|
|
301
|
+
const decoded = (0, commitment_hash_1.decodeCommitmentWire)(wire);
|
|
302
|
+
if (!decoded || !sameProjection(project(decoded), project(payload)))
|
|
303
|
+
return 'mismatch';
|
|
304
|
+
}
|
|
305
|
+
return 'match';
|
|
306
|
+
}
|
|
307
|
+
function attestation(att, body, trustedKeys) {
|
|
308
|
+
// `vendor` (whatever it binds) and unknown types: no other format is implemented.
|
|
309
|
+
if (!isRecord(att) || att.type !== 'jws')
|
|
310
|
+
return 'unsupported';
|
|
311
|
+
const parts = typeof att.jws === 'string' ? att.jws.split('.') : [];
|
|
312
|
+
if (parts.length !== 3 || parts[1] !== '')
|
|
313
|
+
return 'invalid';
|
|
314
|
+
const headerBytes = decodeBase64Url(parts[0]);
|
|
315
|
+
const signature = decodeBase64Url(parts[2]);
|
|
316
|
+
if (!headerBytes || !signature)
|
|
317
|
+
return 'invalid';
|
|
318
|
+
let header;
|
|
319
|
+
try {
|
|
320
|
+
header = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(headerBytes));
|
|
321
|
+
}
|
|
322
|
+
catch {
|
|
323
|
+
return 'invalid';
|
|
324
|
+
}
|
|
325
|
+
if (!isRecord(header) || header.alg !== 'EdDSA')
|
|
326
|
+
return 'unsupported';
|
|
327
|
+
const kid = header.kid;
|
|
328
|
+
const key = typeof kid === 'string' && Object.prototype.hasOwnProperty.call(trustedKeys, kid) ? trustedKeys[kid] : undefined;
|
|
329
|
+
if (key === undefined)
|
|
330
|
+
return 'unknown_key';
|
|
331
|
+
const publicKey = typeof key === 'string' ? decodeBase64Url(key) : undefined;
|
|
332
|
+
if (!publicKey || publicKey.length !== 32 || signature.length !== 64)
|
|
333
|
+
return 'invalid';
|
|
334
|
+
try {
|
|
335
|
+
// zip215: false is RFC 8032's strict verification (canonical encodings, s < L).
|
|
336
|
+
return ed25519_1.ed25519.verify(signature, UTF8.encode(`${parts[0]}.${body}`), publicKey, { zip215: false })
|
|
337
|
+
? 'valid'
|
|
338
|
+
: 'invalid';
|
|
339
|
+
}
|
|
340
|
+
catch {
|
|
341
|
+
return 'invalid';
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* Verifies a decision receipt (RFC-MACP-0013 Appendix A.4) against the keys
|
|
346
|
+
* the caller trusts: `trustedKeys` maps a `kid` to an Ed25519 public key as a
|
|
347
|
+
* JWK `x` (base64url). Returns the integrity outcome, one outcome per
|
|
348
|
+
* attestation, and the overall verdict, in the spec vectors' vocabulary.
|
|
349
|
+
* Never throws: malformed input yields an outcome, not an exception.
|
|
350
|
+
*/
|
|
351
|
+
function verifyReceipt(receipt, trustedKeys) {
|
|
352
|
+
if (!isRecord(receipt) || receipt.receipt_format !== exports.RECEIPT_FORMAT) {
|
|
353
|
+
return { integrity: null, attestations: [], verdict: 'refused' };
|
|
354
|
+
}
|
|
355
|
+
const result = integrity(receipt);
|
|
356
|
+
if (result !== 'match')
|
|
357
|
+
return { integrity: result, attestations: [], verdict: 'unverified' };
|
|
358
|
+
let body;
|
|
359
|
+
try {
|
|
360
|
+
body = b64url(receiptBody(receipt));
|
|
361
|
+
}
|
|
362
|
+
catch {
|
|
363
|
+
body = undefined; // a body with no JCS form: no signature over it can be valid
|
|
364
|
+
}
|
|
365
|
+
const keys = isRecord(trustedKeys) ? trustedKeys : {};
|
|
366
|
+
const outcomes = (Array.isArray(receipt.attestations) ? receipt.attestations : []).map((att) => body === undefined
|
|
367
|
+
? isRecord(att) && att.type === 'jws'
|
|
368
|
+
? 'invalid'
|
|
369
|
+
: 'unsupported'
|
|
370
|
+
: attestation(att, body, keys));
|
|
371
|
+
return {
|
|
372
|
+
integrity: 'match',
|
|
373
|
+
attestations: outcomes,
|
|
374
|
+
verdict: outcomes.includes('valid') ? 'attested' : 'integrity_only',
|
|
375
|
+
};
|
|
376
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -100,6 +100,65 @@ export interface PolicyRegistryCapability {
|
|
|
100
100
|
* admission, not this list, is authoritative. Runtime ≥ 0.9.0.
|
|
101
101
|
*/
|
|
102
102
|
ruleExtensions?: string[];
|
|
103
|
+
/**
|
|
104
|
+
* The runtime implements the optional, read-only `WhatIfReplay` RPC
|
|
105
|
+
* (RFC-MACP-0012 §8.1); {@link MacpClient.whatIfReplay} refuses to call it
|
|
106
|
+
* otherwise. Proto ≥ 0.1.12.
|
|
107
|
+
*/
|
|
108
|
+
whatIfReplay?: boolean;
|
|
109
|
+
}
|
|
110
|
+
/** How one replay handled one history entry (`macp.v1.WhatIfEntryOutcome`). */
|
|
111
|
+
export interface WhatIfEntryOutcome {
|
|
112
|
+
kind: 'KIND_UNSPECIFIED' | 'KIND_APPLIED' | 'KIND_NOT_OPEN' | 'KIND_REFUSED' | 'KIND_NO_SESSION';
|
|
113
|
+
/** Session state after the entry, for `KIND_APPLIED`. */
|
|
114
|
+
state: string;
|
|
115
|
+
/** MACP error code, for `KIND_REFUSED`. */
|
|
116
|
+
errorCode: string;
|
|
117
|
+
/** For a `POLICY_DENIED` refusal, the §6.2 detail JSON; see {@link parseErrorDetails}. */
|
|
118
|
+
details: Buffer;
|
|
119
|
+
}
|
|
120
|
+
/** One accepted-history entry as both replays handled it. `historyIndex` is 0-based; the SessionStart is 0. */
|
|
121
|
+
export interface WhatIfEntry {
|
|
122
|
+
historyIndex: string;
|
|
123
|
+
messageId: string;
|
|
124
|
+
messageType: string;
|
|
125
|
+
recorded: WhatIfEntryOutcome | null;
|
|
126
|
+
candidate: WhatIfEntryOutcome | null;
|
|
127
|
+
}
|
|
128
|
+
/** One replay's final outcome (`macp.v1.WhatIfOutcome`). */
|
|
129
|
+
export interface WhatIfOutcome {
|
|
130
|
+
/** `SESSION_STATE_UNSPECIFIED` when the candidate refused the SessionStart. */
|
|
131
|
+
state: string;
|
|
132
|
+
/** The resolving Commitment's payload bytes; empty if unresolved. */
|
|
133
|
+
resolution: Buffer;
|
|
134
|
+
resolvedByHistoryIndex: string;
|
|
135
|
+
/** Empty if unresolved. */
|
|
136
|
+
resolvedByMessageId: string;
|
|
137
|
+
resolvedByMessageType: string;
|
|
138
|
+
}
|
|
139
|
+
/** A `POLICY_DENIED` the candidate produced; `details` as {@link WhatIfEntryOutcome.details}. */
|
|
140
|
+
export interface WhatIfDenial {
|
|
141
|
+
historyIndex: string;
|
|
142
|
+
messageId: string;
|
|
143
|
+
details: Buffer;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* `macp.v1.WhatIfReplayResponse`: a session's accepted history replayed under
|
|
147
|
+
* its bound policy (`recorded`) and under a candidate. Advisory, never binding.
|
|
148
|
+
* uint64 fields are decimal strings, as elsewhere in this client.
|
|
149
|
+
*/
|
|
150
|
+
export interface WhatIfReplayResult {
|
|
151
|
+
candidatePolicyId: string;
|
|
152
|
+
/** The policy the session is bound to (`policy.default` if none was named). */
|
|
153
|
+
recordedPolicyId: string;
|
|
154
|
+
recorded: WhatIfOutcome | null;
|
|
155
|
+
candidate: WhatIfOutcome | null;
|
|
156
|
+
/** First history index the two replays handled differently; absent when they agree throughout. */
|
|
157
|
+
firstDivergence?: string;
|
|
158
|
+
/** Every POLICY_DENIED the candidate produced, in history order. */
|
|
159
|
+
denials: WhatIfDenial[];
|
|
160
|
+
/** One entry per accepted-history entry, in history order. */
|
|
161
|
+
entries: WhatIfEntry[];
|
|
103
162
|
}
|
|
104
163
|
export interface InitializeResult {
|
|
105
164
|
selectedProtocolVersion: string;
|
package/dist/version.d.ts
CHANGED
package/dist/version.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "macp-sdk-typescript",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.0",
|
|
4
4
|
"description": "TypeScript SDK for the MACP Rust runtime",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "commonjs",
|
|
@@ -11,6 +11,10 @@
|
|
|
11
11
|
"types": "./dist/index.d.ts",
|
|
12
12
|
"default": "./dist/index.js"
|
|
13
13
|
},
|
|
14
|
+
"./receipt": {
|
|
15
|
+
"types": "./dist/receipt.d.ts",
|
|
16
|
+
"default": "./dist/receipt.js"
|
|
17
|
+
},
|
|
14
18
|
"./package.json": "./package.json"
|
|
15
19
|
},
|
|
16
20
|
"sideEffects": false,
|
|
@@ -56,7 +60,9 @@
|
|
|
56
60
|
"dependencies": {
|
|
57
61
|
"@grpc/grpc-js": "^1.14.3",
|
|
58
62
|
"@grpc/proto-loader": "^0.8.0",
|
|
59
|
-
"@multiagentcoordinationprotocol/proto": "^0.1.
|
|
63
|
+
"@multiagentcoordinationprotocol/proto": "^0.1.12",
|
|
64
|
+
"@noble/curves": "^1.9.7",
|
|
65
|
+
"@noble/hashes": "^1.8.0",
|
|
60
66
|
"protobufjs": "^8.6.5"
|
|
61
67
|
},
|
|
62
68
|
"devDependencies": {
|