macp-sdk-typescript 0.17.1 → 0.18.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 CHANGED
@@ -4,6 +4,13 @@ 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.18.0](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/compare/v0.17.1...v0.18.0) (2026-10-25)
8
+
9
+
10
+ ### Features
11
+
12
+ * **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))
13
+
7
14
  ## [0.17.1](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/compare/v0.17.0...v0.17.1) (2026-10-20)
8
15
 
9
16
 
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
- The SDK depends on `@multiagentcoordinationprotocol/proto` from GitHub Packages. Create a `.npmrc` in your project root:
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
- And configure a GitHub PAT with `read:packages` scope:
21
+ Start a local runtime (the same image CI pins):
24
22
 
25
23
  ```bash
26
- npm config set //npm.pkg.github.com/:_authToken YOUR_GITHUB_PAT
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
- ## Quick Start
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';
@@ -79,6 +79,7 @@ The SDK uses a **three-layer** design:
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', // optional opaque context reference
98
- extensions: { 'x-trace': Buffer.from('tid') }, // optional typed extension buffers
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(); // { p1: 1 }
125
- session.projection.majorityWinner(); // 'p1'
126
- session.projection.hasBlockingObjection('p1'); // false (only severity 'critical' blocks)
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(); // proposals with status 'open'
149
- session.projection.isAccepted('p2'); // true
150
- session.projection.isTerminallyRejected('p1'); // false
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'); // 1.0
174
- session.projection.isCompleted('t1'); // true
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'); // true
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'); // true
226
- session.projection.approvalCount('r1'); // 2
227
- session.projection.remainingVotesNeeded('r1'); // 0
228
- session.projection.votedSenders('r1'); // ['alice', 'bob']
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('Proposal', agent.evaluationHandler(agent.functionEvaluator(async (p) => ({
309
- recommendation: 'APPROVE', confidence: 0.9, reason: 'looks good',
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
  ```
@@ -389,9 +397,9 @@ To attach to a session that is already in flight and receive accepted history be
389
397
 
390
398
  ```typescript
391
399
  const stream = client.openStream({ auth });
392
- await stream.sendSubscribe(sessionId); // full replay then live
400
+ await stream.sendSubscribe(sessionId); // full replay then live
393
401
  // or, after a reconnect:
394
- await stream.sendSubscribe(sessionId, lastSeq); // resume from cursor
402
+ await stream.sendSubscribe(sessionId, lastSeq); // resume from cursor
395
403
  ```
396
404
 
397
405
  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 +421,43 @@ for await (const change of watcher.changes(controller.signal)) {
413
421
  const next = await watcher.nextChange();
414
422
  ```
415
423
 
424
+ ## Decision Receipts
425
+
426
+ 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:
427
+
428
+ - **Integrity**: the receipt's `commitment` has its `commitment_hash`. This needs no keys.
429
+ - **Authenticity**: an attestation over the receipt is valid for a key you trust. The SDK implements Ed25519 `jws` attestations only.
430
+
431
+ ```typescript
432
+ import { buildReceipt, verifyReceipt } from 'macp-sdk-typescript/receipt';
433
+
434
+ // Issue: hash the commitment and sign the receipt body (detached JWS, alg EdDSA).
435
+ const receipt = buildReceipt(commitmentPayload, {
436
+ sessionId,
437
+ mode: 'macp.mode.decision.v1',
438
+ issuer: 'runtime:prod-eu',
439
+ issuedAt: new Date().toISOString(),
440
+ signer: { kid: 'prod-2026', privateKey: seed32 }, // or { kid, sign: (bytes) => signature64 }
441
+ commitmentWire, // optional: Commitment wire bytes, recorded as commitment_wire_b64
442
+ });
443
+
444
+ // Verify: trusted keys map a kid to an Ed25519 public key (JWK `x`, base64url).
445
+ const result = verifyReceipt(receipt, { 'prod-2026': 'TDVzkxaSf6_Oxu7mmn5TbCkF80s8Ulh33qKQsi0gnZM' });
446
+ // { integrity: 'match', attestations: ['valid'], verdict: 'attested' }
447
+ ```
448
+
449
+ | Field | Values |
450
+ | ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
451
+ | `integrity` | `match`, `mismatch`, `cannot_verify`, or `null` when the receipt is refused |
452
+ | `attestations[i]` | `valid`, `invalid`, `unknown_key` or `unsupported` (any other `alg`, and every `vendor` attestation) |
453
+ | `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`) |
454
+
455
+ If you hold the snake_case §3 projection (a receipt's `commitment`) instead of a `CommitmentPayload`, convert it with `commitmentFromProjection(projection)`.
456
+
457
+ `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.
458
+
459
+ 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.
460
+
416
461
  ## Error Handling
417
462
 
418
463
  ```typescript
@@ -454,11 +499,10 @@ cleanly fails that workflow, not just a local check.
454
499
  Run the full SDK against a live MACP runtime:
455
500
 
456
501
  ```bash
457
- # Build and start the runtime
458
- docker build -t macp-runtime ../macp-runtime/
502
+ # Start the runtime (CI's pinned image)
459
503
  docker run -d --name macp-runtime-test -p 50051:50051 \
460
- -e MACP_BIND_ADDR=0.0.0.0:50051 -e MACP_ALLOW_INSECURE=1 \
461
- -e MACP_MEMORY_ONLY=1 macp-runtime
504
+ -e MACP_ALLOW_INSECURE=1 -e MACP_MEMORY_ONLY=1 \
505
+ ghcr.io/multiagentcoordinationprotocol/macp-runtime:0.9.0
462
506
 
463
507
  # Run tests
464
508
  npm run test:integration
@@ -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:
@@ -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 this SDK's first non-gRPC/proto runtime
23
- * dependency. `node:crypto` is a Node builtin, not a dependency.
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 node_crypto_1 = require("node:crypto");
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 `Buffer.from(preimage, 'utf8')` — Node's
49
- * standard lossy UTF-8 encoder silently substitutes a lone surrogate with
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
- const digest = (0, node_crypto_1.createHash)('sha256').update(Buffer.from(preimage, 'utf8')).digest('hex');
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/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;
@@ -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/version.d.ts CHANGED
@@ -4,4 +4,4 @@
4
4
  * Kept in sync with package.json by release-please (see the
5
5
  * `x-release-please-version` annotation).
6
6
  */
7
- export declare const VERSION = "0.17.1";
7
+ export declare const VERSION = "0.18.0";
package/dist/version.js CHANGED
@@ -7,4 +7,4 @@ exports.VERSION = void 0;
7
7
  * Kept in sync with package.json by release-please (see the
8
8
  * `x-release-please-version` annotation).
9
9
  */
10
- exports.VERSION = '0.17.1'; // x-release-please-version
10
+ exports.VERSION = '0.18.0'; // x-release-please-version
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "macp-sdk-typescript",
3
- "version": "0.17.1",
3
+ "version": "0.18.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,
@@ -57,6 +61,8 @@
57
61
  "@grpc/grpc-js": "^1.14.3",
58
62
  "@grpc/proto-loader": "^0.8.0",
59
63
  "@multiagentcoordinationprotocol/proto": "^0.1.11",
64
+ "@noble/curves": "^1.9.7",
65
+ "@noble/hashes": "^1.8.0",
60
66
  "protobufjs": "^8.6.5"
61
67
  },
62
68
  "devDependencies": {