@postcept/receipt 1.1.0 → 1.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2026 Postcept, Inc.
3
+ Copyright (c) 2026 Postcept
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -1,9 +1,15 @@
1
1
  # @postcept/receipt
2
2
 
3
- Reference implementation for verifying Postcept Receipts and VCR-audit badges. It
4
- runs anywhere JavaScript runs (browser, Node, Deno, edge), and verifying a receipt
5
- needs nothing from Postcept except the published public key. You don't have to
6
- trust an API to trust the result.
3
+ A Postcept Receipt is a signed proof that an AI agent's high-risk action (a refund,
4
+ a cancellation, a credit) was checked against the system of record, and of what that
5
+ check found: verified, incomplete, duplicated, mismatched, or policy-failed. This
6
+ is the reference library for verifying one.
7
+
8
+ It runs anywhere JavaScript runs (browser, Node, Deno, edge), and verifying a
9
+ receipt needs nothing from Postcept except the published public key. No API call,
10
+ and no taking the agent's word for it. A valid signature proves the receipt is
11
+ authentic and unmodified since issue. See the spec for what that does and does not
12
+ cover.
7
13
 
8
14
  The format and signing scheme are written up in [SPEC.md](./SPEC.md). This is the
9
15
  canonical JS implementation of that spec. The Postcept API signs with a
@@ -51,6 +57,24 @@ One source of truth for the verification logic. Postcept's own site and audit to
51
57
  import this same package, and so can you. The "you don't have to trust us" claim
52
58
  only holds if the reference implementation is open and stands on its own.
53
59
 
60
+ ## Verifying a release
61
+
62
+ Every published version carries a signed npm provenance statement linking the
63
+ tarball to the commit and CI workflow that built it, plus a CycloneDX SBOM
64
+ attached to the GitHub release.
65
+
66
+ ```sh
67
+ # Check the installed package's provenance and registry signatures:
68
+ npm audit signatures
69
+
70
+ # Inspect what shipped (attached to the GitHub release):
71
+ # sbom.cyclonedx.json
72
+ ```
73
+
74
+ The receipt format is verifiable on its own with the public key, as the spec
75
+ describes. Release provenance is a separate check that the code you installed is
76
+ the code this repo built.
77
+
54
78
  ## License
55
79
 
56
80
  MIT.
package/SPEC.md CHANGED
@@ -5,10 +5,22 @@
5
5
  A Postcept Receipt is a signed, tamper-evident proof that a high-risk AI-agent
6
6
  action (a refund, a cancellation, a ticket resolution) was checked against the
7
7
  system of record and classified. This document specifies the receipt format and
8
- how to verify it without going through Postcept: no API call, no trust required.
8
+ how to verify it without going through Postcept: no API call required.
9
9
 
10
- The point is neutrality. A customer, an auditor, or a counterparty can verify a
11
- receipt with nothing but the public key and the rules below.
10
+ The point is independent verifiability. A customer, an auditor, or a counterparty
11
+ can check a receipt with nothing but the public key and the rules below.
12
+
13
+ ## What a receipt proves, and what it doesn't
14
+
15
+ A valid signature proves the receipt was issued by the holder of the signing key
16
+ and has not been altered since. The named postconditions were evaluated against
17
+ the named system of record, classified as recorded, at the recorded time, for the
18
+ recorded tenant, in test or live mode as flagged. It does not on its own prove
19
+ that the source system answered truthfully, that the state stayed the same after
20
+ `valid_as_of` (a later receipt can supersede this one when re-verification finds
21
+ the truth changed), or that the action was appropriate in the first place. Trust
22
+ in the receipt is trust in the signing key. Verify against the published key
23
+ registry, and pin it if your risk model calls for that.
12
24
 
13
25
  ## 1. Signature scheme
14
26
 
@@ -17,6 +29,9 @@ receipt with nothing but the public key and the rules below.
17
29
  field.
18
30
  - **Public key:** base64-encoded raw 32-byte Ed25519 public key. Fetch the current
19
31
  key from `GET /v1/signing-key`, which returns `{ algorithm, key_id, public_key }`.
32
+ `GET /v1/signing-keys` returns the full registry, the active key plus retired
33
+ keys kept published so receipts signed before a rotation stay verifiable. Pick
34
+ the entry whose `key_id` matches the receipt's `signing_key_id`.
20
35
  - **Key id:** `ed25519:<first-16-chars-of-url-safe-base64(public_key)>`, in
21
36
  `signing_key_id`. A receipt names the key that signed it, so old receipts stay
22
37
  verifiable after a key rotation.
@@ -37,6 +52,27 @@ receipt JSON as transmitted. To reproduce the signed bytes:
37
52
  This is what Python's `json.dumps(body, sort_keys=True, separators=(",", ":"))`
38
53
  produces.
39
54
 
55
+ ### Constraints and rejection
56
+
57
+ The scheme is injective over the value domain the signing bodies use, given these
58
+ constraints. A compliant verifier MUST reject anything outside them rather than
59
+ guess.
60
+
61
+ - **Numbers** are finite integers or decimals. `NaN`, `Infinity`, and `-Infinity`
62
+ are not valid JSON and MUST be rejected, not serialized.
63
+ - **Strings** are valid UTF-8. Every code point outside printable ASCII is
64
+ `\uXXXX`-escaped (surrogate pairs for astral characters), so the signed bytes
65
+ are pure ASCII regardless of transport encoding.
66
+ - **Malformed input**, such as a non-base64 signature or public key, a
67
+ wrong-length key, or a missing signed field, MUST cause verification to return
68
+ `false` rather than throw. Both reference implementations do this.
69
+
70
+ Cross-language parity is checked from both sides against one fixed source of
71
+ truth. The golden vectors in `test/vectors/` carry a public key and a signature
72
+ over specific canonical bytes. The TypeScript suite verifies them and the Python
73
+ implementation verifies byte-identical copies, so a canonicalization drift in
74
+ either language fails its own vector test, not only the other's.
75
+
40
76
  ### Timestamps
41
77
 
42
78
  Timestamps are ISO-8601 UTC. The signer emits a trailing `Z`
@@ -95,6 +131,47 @@ field is treated as version 1.
95
131
  }
96
132
  ```
97
133
 
134
+ ### Version 3 (dual-signature)
135
+
136
+ v3 adds the fields that were server-asserted metadata in v2 to the signed body,
137
+ so they become tamper-evident, and adds an optional observation role. A v3 receipt
138
+ has two signatures that verify independently.
139
+
140
+ - **Evaluation role.** Postcept's signature (the receipt's `signature` field) over
141
+ the v3 signing body. This is what `verifyReceipt` checks.
142
+ - **Observation role.** Present only when a customer-side relay produced the source
143
+ observation. The relay signs its observation envelope, and the receipt binds
144
+ `{relay_id, key_id, digest, signature}` where `digest = "sha256:" + hex(sha256(
145
+ canonicalize(envelope_signing_body)))` and `signature` is the relay's signature
146
+ over that envelope. `verifyObservation(receipt, envelopeSigningBody, relayKey)`
147
+ confirms the digest matches and the relay signature verifies. Neither signer can
148
+ forge the other's statement. The relay attests it observed the source, and
149
+ Postcept attests it evaluated that observation.
150
+
151
+ The v3 signing body is the v2 body with `version: "3"` plus:
152
+
153
+ ```jsonc
154
+ {
155
+ // ...all v2 fields...
156
+ "version": "3",
157
+ "supersedes": "<receipt_id | null>",
158
+ "contract_digest": "sha256:... | null",
159
+ "lifecycle": "finalized | pending_finality | ... | null",
160
+ "safe_to_claim_complete": true, // or false / null
161
+ "correlation_strength": "deterministic | heuristic | ... | null",
162
+ "observation": { // null when no relay produced the source
163
+ "relay_id": "rly_...",
164
+ "key_id": "ed25519:...",
165
+ "digest": "sha256:...",
166
+ "signature": "<base64 relay signature over the envelope>"
167
+ }
168
+ }
169
+ ```
170
+
171
+ v3 issuance is gated on the server. v1 and v2 receipts remain valid forever, and
172
+ the verifier dispatches on `version`. A verifier MAY require the observation role
173
+ (for high-assurance, relay-only acceptance) or accept evaluation-only receipts.
174
+
98
175
  ## 4. Verification algorithm
99
176
 
100
177
  ```
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export { asciiJson, base64ToBytes, canonicalize, TIMESTAMP_SPELLINGS } from "./canonical.js";
2
- export { type Postcondition, type Receipt, receiptSigningBody, verifyReceipt } from "./receipt.js";
2
+ export { type Observation, type Postcondition, type Receipt, receiptSigningBody, verifyObservation, verifyReceipt, } from "./receipt.js";
3
3
  export { type AuditBadge, badgeSigningBody, verifyBadge } from "./badge.js";
4
4
  export { type ConsistencyProof, type InclusionProof, type SignedTreeHead, receiptLeafHash, verifyConsistency, verifyInclusion, verifyReceiptInLog, verifySignedTreeHead, } from "./transparency.js";
5
5
  export { type EvidenceExportLike, type EvidenceManifest, evidenceContentDigest, verifyEvidenceExport, verifyEvidenceManifest, } from "./evidence.js";
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
  // and VCR-audit badges. Works in the browser, Node, Deno, and edge runtimes. See
3
3
  // SPEC.md for the format and signing scheme.
4
4
  export { asciiJson, base64ToBytes, canonicalize, TIMESTAMP_SPELLINGS } from "./canonical.js";
5
- export { receiptSigningBody, verifyReceipt } from "./receipt.js";
5
+ export { receiptSigningBody, verifyObservation, verifyReceipt, } from "./receipt.js";
6
6
  export { badgeSigningBody, verifyBadge } from "./badge.js";
7
7
  export { receiptLeafHash, verifyConsistency, verifyInclusion, verifyReceiptInLog, verifySignedTreeHead, } from "./transparency.js";
8
8
  export { evidenceContentDigest, verifyEvidenceExport, verifyEvidenceManifest, } from "./evidence.js";
package/dist/receipt.d.ts CHANGED
@@ -22,11 +22,22 @@ export interface Receipt {
22
22
  algorithm?: string;
23
23
  signing_key_id?: string;
24
24
  signature: string;
25
+ supersedes?: string | null;
26
+ contract_digest?: string | null;
27
+ lifecycle?: string | null;
28
+ safe_to_claim_complete?: boolean | null;
29
+ correlation_strength?: string | null;
30
+ observation_relay_id?: string | null;
31
+ observation_key_id?: string | null;
32
+ observation_digest?: string | null;
33
+ observation_signature?: string | null;
34
+ }
35
+ export interface Observation {
36
+ relay_id: string | null;
37
+ key_id: string | null;
38
+ digest: string | null;
39
+ signature: string | null;
25
40
  }
26
- /**
27
- * The exact subset of a receipt the signature covers, by version. `ts` applies a
28
- * timestamp spelling so the caller can retry "Z" vs "+00:00" (see verifyReceipt).
29
- */
30
41
  export declare function receiptSigningBody(r: Receipt, ts: (s: string) => string): Record<string, unknown>;
31
42
  /**
32
43
  * Verify a receipt's Ed25519 signature against a published public key (base64 raw
@@ -34,3 +45,4 @@ export declare function receiptSigningBody(r: Receipt, ts: (s: string) => string
34
45
  * input instead of throwing.
35
46
  */
36
47
  export declare function verifyReceipt(receipt: Receipt, publicKeyB64: string): Promise<boolean>;
48
+ export declare function verifyObservation(receipt: Receipt, envelopeSigningBody: Record<string, unknown>, relayPublicKeyB64: string): Promise<boolean>;
package/dist/receipt.js CHANGED
@@ -1,34 +1,59 @@
1
1
  // The signed receipt object and its verification. Reference implementation of the
2
2
  // open standard in SPEC.md.
3
3
  import * as ed from "@noble/ed25519";
4
- import { base64ToBytes, canonicalize, TIMESTAMP_SPELLINGS } from "./canonical.js";
4
+ import { base64ToBytes, canonicalize, sha256Hex, TIMESTAMP_SPELLINGS } from "./canonical.js";
5
5
  /**
6
6
  * The exact subset of a receipt the signature covers, by version. `ts` applies a
7
7
  * timestamp spelling so the caller can retry "Z" vs "+00:00" (see verifyReceipt).
8
8
  */
9
+ function receiptSigningBodyV2(r, ts) {
10
+ return {
11
+ version: "2",
12
+ id: r.id,
13
+ org_id: r.org_id ?? null,
14
+ operation_id: r.operation_id,
15
+ agent_id: r.agent_id,
16
+ action: r.action,
17
+ connectors_checked: r.connectors_checked,
18
+ test: r.test ?? false,
19
+ postconditions: (r.postconditions ?? []).map((p) => ({
20
+ name: p.name,
21
+ category: p.category ?? null,
22
+ status: p.status,
23
+ expected: p.expected ?? null,
24
+ actual: p.actual ?? null,
25
+ })),
26
+ result: r.result,
27
+ issued_at: ts(r.issued_at),
28
+ valid_as_of: r.valid_as_of ? ts(r.valid_as_of) : null,
29
+ };
30
+ }
9
31
  export function receiptSigningBody(r, ts) {
10
- if (r.version === "2") {
32
+ if (r.version === "3") {
33
+ // v3 extends the v2 body with the fields that were server-asserted metadata
34
+ // in v2, plus the observation role when a relay produced the source.
35
+ const v2 = receiptSigningBodyV2(r, ts);
11
36
  return {
12
- version: "2",
13
- id: r.id,
14
- org_id: r.org_id ?? null,
15
- operation_id: r.operation_id,
16
- agent_id: r.agent_id,
17
- action: r.action,
18
- connectors_checked: r.connectors_checked,
19
- test: r.test ?? false,
20
- postconditions: (r.postconditions ?? []).map((p) => ({
21
- name: p.name,
22
- category: p.category ?? null,
23
- status: p.status,
24
- expected: p.expected ?? null,
25
- actual: p.actual ?? null,
26
- })),
27
- result: r.result,
28
- issued_at: ts(r.issued_at),
29
- valid_as_of: r.valid_as_of ? ts(r.valid_as_of) : null,
37
+ ...v2,
38
+ version: "3",
39
+ supersedes: r.supersedes ?? null,
40
+ contract_digest: r.contract_digest ?? null,
41
+ lifecycle: r.lifecycle ?? null,
42
+ safe_to_claim_complete: r.safe_to_claim_complete ?? null,
43
+ correlation_strength: r.correlation_strength ?? null,
44
+ observation: r.observation_signature != null
45
+ ? {
46
+ relay_id: r.observation_relay_id ?? null,
47
+ key_id: r.observation_key_id ?? null,
48
+ digest: r.observation_digest ?? null,
49
+ signature: r.observation_signature,
50
+ }
51
+ : null,
30
52
  };
31
53
  }
54
+ if (r.version === "2") {
55
+ return receiptSigningBodyV2(r, ts);
56
+ }
32
57
  // Legacy v1 body (name + status only).
33
58
  return {
34
59
  id: r.id,
@@ -62,3 +87,26 @@ export async function verifyReceipt(receipt, publicKeyB64) {
62
87
  return false;
63
88
  }
64
89
  }
90
+ // The observation role of a v3 receipt. `verifyReceipt` checks the evaluation
91
+ // role, where Postcept signed the result. This checks the source observation:
92
+ // given the original relay envelope's signing body, that it hashes to the digest
93
+ // in the receipt and that the relay's signature verifies against the relay's
94
+ // public key. The two roles together show that the relay observed the source and
95
+ // Postcept evaluated it, with neither able to forge the other's statement.
96
+ // Returns false, never throws, if there is no observation role or anything fails.
97
+ export async function verifyObservation(receipt, envelopeSigningBody, relayPublicKeyB64) {
98
+ try {
99
+ if (receipt.observation_signature == null || receipt.observation_digest == null)
100
+ return false;
101
+ const canonical = new TextEncoder().encode(canonicalize(envelopeSigningBody));
102
+ const digest = `sha256:${await sha256Hex(canonical)}`;
103
+ if (digest !== receipt.observation_digest)
104
+ return false;
105
+ const sig = base64ToBytes(receipt.observation_signature);
106
+ const pub = base64ToBytes(relayPublicKeyB64);
107
+ return await ed.verifyAsync(sig, canonical, pub);
108
+ }
109
+ catch {
110
+ return false;
111
+ }
112
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@postcept/receipt",
3
- "version": "1.1.0",
4
- "description": "Reference implementation for verifying Postcept Receipts and VCR-audit badges, independently and in any environment.",
3
+ "version": "1.1.2",
4
+ "description": "Independently verify a Postcept Receipt, the signed proof that an AI agent's high-risk action actually happened in the system of record. Runs anywhere, needs only the public key.",
5
5
  "license": "MIT",
6
6
  "author": "Postcept",
7
7
  "type": "module",
@@ -13,7 +13,7 @@
13
13
  "type": "git",
14
14
  "url": "git+https://github.com/Postcept/receipt.git"
15
15
  },
16
- "homepage": "https://github.com/Postcept/receipt#readme",
16
+ "homepage": "https://postcept.com",
17
17
  "bugs": "https://github.com/Postcept/receipt/issues",
18
18
  "keywords": [
19
19
  "postcept",