@agentcontextdistributionprotocol/acdp 0.4.0 → 0.6.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.
Files changed (3) hide show
  1. package/index.d.ts +262 -0
  2. package/index.js +2 -1
  3. package/package.json +5 -5
package/index.d.ts CHANGED
@@ -19,6 +19,25 @@ export interface ResolvedDidKey {
19
19
  */
20
20
  publicKeyB64: string
21
21
  }
22
+ /**
23
+ * [`ResolvedDidKey`] plus the RFC-ACDP-0010 §9 lifecycle signal for
24
+ * registry receipt keys.
25
+ */
26
+ export interface ResolvedReceiptKey {
27
+ /** Verification-method id (full DID URL with `#fragment`). */
28
+ keyId: string
29
+ /** `ed25519` or `ecdsa-p256`. */
30
+ algorithm: string
31
+ /** Standard base64 of the raw key bytes (see [`ResolvedDidKey`]). */
32
+ publicKeyB64: string
33
+ /**
34
+ * `true` when the key is retained in `verificationMethod` but no
35
+ * longer referenced by `assertionMethod` — a retired receipt key.
36
+ * Verify the receipt, but report it with the distinguishable
37
+ * *historically authorized* status (RFC-ACDP-0010 §9).
38
+ */
39
+ historical: boolean
40
+ }
22
41
  /**
23
42
  * Options for `buildPublishRequest`. Field names map directly to the
24
43
  * PublishRequest wire schema (camelCase on the JS side).
@@ -193,6 +212,26 @@ export declare class AcdpDidDocument {
193
212
  * is the full DID URL from the signature's `key_id`.
194
213
  */
195
214
  keyForAlgorithm(requestedKeyId: string, requestedAlg: string): ResolvedDidKey
215
+ /**
216
+ * Resolve a **registry receipt** signing key, applying the
217
+ * RFC-ACDP-0010 §9 lifecycle instead of the `assertionMethod` gate:
218
+ * retired receipt keys MUST remain in `verificationMethod`
219
+ * indefinitely and MUST still verify historical receipts even after
220
+ * rotation removes them from `assertionMethod`. A key absent from
221
+ * `verificationMethod` entirely still fails
222
+ * (`.code === "key_not_found"`) — full removal is the registry's
223
+ * compromise-revocation signal.
224
+ *
225
+ * `historical` on the result is `true` when the key is no longer in
226
+ * `assertionMethod`: verify the receipt, but report it with the
227
+ * distinguishable *historically authorized* status. The
228
+ * algorithm-downgrade defense (RFC-ACDP-0008 §3.9) and key decoding
229
+ * are enforced identically to `keyForAlgorithm`.
230
+ *
231
+ * Use `keyForAlgorithm` for producer keys and auth challenges —
232
+ * publish-time authorization still requires `assertionMethod`.
233
+ */
234
+ receiptKeyForAlgorithm(requestedKeyId: string, requestedAlg: string): ResolvedReceiptKey
196
235
  }
197
236
  /** RFC 8785 canonicalization utilities. All methods are static. */
198
237
  export declare class AcdpCanonicalizer {
@@ -219,6 +258,33 @@ export declare class AcdpCanonicalizer {
219
258
  */
220
259
  static contentHash(jsonStr: string): string
221
260
  }
261
+ /** RFC-ACDP-0012 §5 Merkle helpers. All methods are static. */
262
+ export declare class AcdpMerkle {
263
+ /**
264
+ * The §5.1 leaf hash of a transparency-log leaf:
265
+ * `SHA-256(0x00 ‖ JCS(leaf))`, returned in the wire form
266
+ * `"sha256:<64-hex>"`.
267
+ *
268
+ * * `leafJson` — the closed leaf object (e.g. the output of
269
+ * `AcdpVerifier.buildLogLeaf`). Shape-validated first; a
270
+ * malformed leaf throws (`.code === "invalid_log_proof"`)
271
+ * rather than hashing bytes no conformant log ever committed.
272
+ */
273
+ static leafHash(leafJson: string): string
274
+ /**
275
+ * The §5.1 interior-node hash `SHA-256(0x01 ‖ left ‖ right)` over
276
+ * the raw digests the two wire-form (`"sha256:<hex>"`) arguments
277
+ * encode. The 0x00/0x01 domain-separation prefixes are what stop
278
+ * leaf/node second-preimage forgeries — never hash without them.
279
+ */
280
+ static nodeHash(leftHash: string, rightHash: string): string
281
+ /**
282
+ * The §5.2 RFC 6962 Merkle tree hash `MTH(D[n])` over an ordered
283
+ * JSON array of wire-form leaf hashes (`'["sha256:...", ...]'`).
284
+ * An empty array yields the empty-tree root, `SHA-256("")`.
285
+ */
286
+ static rootHash(leafHashesJson: string): string
287
+ }
222
288
  /**
223
289
  * An ACDP producer: an Ed25519 signing key and its DID identity
224
290
  * (`did:web`, or `did:key` via the `*DidKey` factories — ACDP 0.2).
@@ -595,4 +661,200 @@ export declare class AcdpVerifier {
595
661
  * message otherwise.
596
662
  */
597
663
  static verifyReceipt(receiptJson: string, registryPublicKeyB64: string, expectedCtxId: string, recomputedBodyHash: string, producerKeyFingerprint: string): boolean
664
+ /**
665
+ * Verify a lineage-head receipt offline per RFC-ACDP-0011 §7:
666
+ * closed parse, registry/lineage/head bindings, `as_of` clock
667
+ * skew, and the registry signature over the RAW wire preimage —
668
+ * against the registry key extracted from the caller-supplied DID
669
+ * document (RFC-ACDP-0010 §9 receipt-key lifecycle: retired keys
670
+ * verify with `historical: true`; fully removed keys fail closed).
671
+ *
672
+ * * `receiptJson` — the `lineage_head_receipt` object as received
673
+ * on the wire.
674
+ * * `expectedJson` — the consumer's own expectations:
675
+ * `{"authority" and/or "registry_did", "lineage_id",
676
+ * "head_ctx_id", "head_version", "head_status",
677
+ * "on_current_endpoint"?}`. `authority` is the authority the
678
+ * response was *actually fetched from* (compare your HTTP
679
+ * client's URL, not any response field); `registry_did` is
680
+ * `capabilities.registry_did`; either derives the other.
681
+ * `on_current_endpoint` defaults to `true` (`GET /current` — §7
682
+ * step 5 byte-match); pass `false` for a full retrieval, where
683
+ * the §7 step 5b stale-consistency rule applies.
684
+ * * `registryDidDocJson` — the registry's resolved DID document
685
+ * (resolution stays in JS land: `AcdpDid.webToUrl` + `fetch`).
686
+ * * `nowRfc3339` — the consumer clock (defaults to now).
687
+ * * `maxSkewSecs` — §7 step 6 allowance (default 120).
688
+ * * `maxAgeSecs` — §6 freshness policy (default 300).
689
+ *
690
+ * Returns a JSON verdict: `{"valid": true, "stale": bool,
691
+ * "age_secs": int, "historical": bool}` — staleness is policy,
692
+ * not verification failure — or `{"valid": false, "code":
693
+ * "invalid_receipt"|..., "error": ...}`. Throws only on malformed
694
+ * host input.
695
+ */
696
+ static verifyLineageHeadReceipt(receiptJson: string, expectedJson: string, registryDidDocJson: string, nowRfc3339?: string | undefined | null, maxSkewSecs?: number | undefined | null, maxAgeSecs?: number | undefined | null): string
697
+ /**
698
+ * Verify a transparency-log checkpoint (signed tree head) offline
699
+ * per RFC-ACDP-0012 §9.3: closed parse, optional `logId` pin
700
+ * (§7.4 — a new `log_id` is an explicit history reset), timestamp
701
+ * form + clock skew, and the registry signature over the RAW wire
702
+ * preimage against the receipt key from the caller-supplied DID
703
+ * document (retired keys verify with `historical: true`).
704
+ *
705
+ * The HOST still owns the §9.3 step 3 serving-authority half:
706
+ * confirm the `log_id`'s registry DID matches the authority the
707
+ * checkpoint was actually fetched from and
708
+ * `capabilities.registry_did`.
709
+ *
710
+ * Returns `{"valid": true, "log_id", "tree_size", "root_hash",
711
+ * "age_secs", "historical"}` (retain `tree_size`/`root_hash` for
712
+ * future §9.2 consistency checks) or `{"valid": false, "code":
713
+ * "invalid_log_proof", "error": ...}`.
714
+ */
715
+ static verifyLogCheckpoint(checkpointJson: string, registryDidDocJson: string, expectedLogId?: string | undefined | null, nowRfc3339?: string | undefined | null, maxSkewSecs?: number | undefined | null): string
716
+ /**
717
+ * Verify a transparency-log inclusion proof offline —
718
+ * RFC-ACDP-0012 §9.1 steps 2 and 4–6: hash the RECONSTRUCTED
719
+ * leaf, check the proof ↔ checkpoint bindings, fold the audit
720
+ * path, compare against the checkpoint root.
721
+ *
722
+ * * `inclusionJson` — the proof (`log_id`, `leaf_index`,
723
+ * `tree_size`, `inclusion_path`, optionally an embedded
724
+ * `log_checkpoint`).
725
+ * * `checkpointJson` — the checkpoint the proof verifies against.
726
+ * Inserted when the proof carries none; when the proof embeds
727
+ * one, the two MUST be byte-equal (a proof quietly carrying a
728
+ * different checkpoint is the substitution §9.1 step 3 exists
729
+ * to stop). Verify its signature separately with
730
+ * `verifyLogCheckpoint` — the verdicts are independent.
731
+ * * `reconstructedLeafJson` — the leaf built from *verified* body
732
+ * + receipt material via `buildLogLeaf` (§9.1 step 1). NEVER
733
+ * pass a leaf echoed by the registry — the whole point is that
734
+ * the verifier vouches for the leaf bytes itself.
735
+ *
736
+ * Returns `{"valid": true, "leaf_hash": "sha256:..."}` or
737
+ * `{"valid": false, "code": "invalid_log_proof", "error": ...}`.
738
+ */
739
+ static verifyLogInclusion(inclusionJson: string, checkpointJson: string, reconstructedLeafJson: string): string
740
+ /**
741
+ * Verify a transparency-log consistency proof offline —
742
+ * RFC-ACDP-0012 §9.2, the history-rewrite detector: prove the
743
+ * tree the verifier RETAINED a root for (`firstRootHash`, at
744
+ * `first_tree_size`) is a prefix of the checkpointed later tree.
745
+ *
746
+ * * `consistencyJson` — the proof (`log_id`, `first_tree_size`,
747
+ * `second_tree_size`, `consistency_path`, optionally an
748
+ * embedded `log_checkpoint`).
749
+ * * `checkpointJson` — the later checkpoint (merged/byte-checked
750
+ * exactly as in `verifyLogInclusion`; verify its signature
751
+ * separately with `verifyLogCheckpoint`).
752
+ * * `firstRootHash` — the verifier's own retained root
753
+ * (`"sha256:<hex>"`) — retaining it is the whole point.
754
+ *
755
+ * Returns `{"valid": true}` or `{"valid": false, "code":
756
+ * "invalid_log_proof", "error": ...}`. A fold failure between two
757
+ * signature-valid checkpoints of one `log_id` is cryptographic
758
+ * evidence of a logged-history rewrite — retain both checkpoints
759
+ * and the failing path (§9.2, §15).
760
+ */
761
+ static verifyLogConsistency(consistencyJson: string, checkpointJson: string, firstRootHash: string): string
762
+ /**
763
+ * Build the canonical RFC-ACDP-0012 §4 log leaf from a VERIFIED
764
+ * RFC-ACDP-0010 receipt (§9.1 step 1) — every leaf field other
765
+ * than `receipt_hash` duplicates a receipt field, and
766
+ * `receipt_hash` is the receipt's §5 preimage hash, computed here
767
+ * over the RAW wire JSON as received. Returns the leaf as a JSON
768
+ * string, ready for `verifyLogInclusion` / `AcdpMerkle.leafHash`.
769
+ *
770
+ * Run `verifyReceipt` on the receipt FIRST: a leaf reconstructed
771
+ * from an unverified receipt proves membership of a claim nobody
772
+ * has checked. Throws on a malformed receipt
773
+ * (`.code === "invalid_receipt"`).
774
+ */
775
+ static buildLogLeaf(receiptJson: string): string
776
+ /**
777
+ * Verify one `registry_state.lifecycle_events` entry offline per
778
+ * RFC-ACDP-0013 §5: closed §4 parse, binding to `expectedCtxId`
779
+ * (a signed event cannot be replayed against another context),
780
+ * the §5 actor binding (`signature.key_id` DID = `actor`), and
781
+ * the signature over the RAW wire preimage.
782
+ *
783
+ * * `eventJson` — the event object as received.
784
+ * * `actorDidDocJson` — the ACTOR's resolved DID document, or
785
+ * `null` for a `did:key` actor (self-certifying — verified
786
+ * natively with no document). For `did:web` actors the key must
787
+ * pass the `assertionMethod` gate, like a body signature.
788
+ * * `expectedCtxId` — the ctx_id of the context whose registry
789
+ * state carries the event.
790
+ *
791
+ * The HOST still owns the §4/§12 authorization check that `actor`
792
+ * equals the context's `body.agent_id` (producer-initiated) or
793
+ * the registry's `capabilities.registry_did` (registry-initiated)
794
+ * — this binding sees neither document. Retraction state itself
795
+ * is derived from array order, last `retracted`/`republished`
796
+ * event wins; unknown event types are inert (§7.1, §7.3).
797
+ *
798
+ * Returns `{"valid": true, "event_id", "event_type", "actor"}` or
799
+ * `{"valid": false, "code": ..., "error": ...}` (an unsigned
800
+ * event fails — producer-initiated events MUST be signed).
801
+ */
802
+ static verifyLifecycleEvent(eventJson: string, actorDidDocJson: string | undefined | null, expectedCtxId: string): string
803
+ /**
804
+ * Parse and shape-validate a `key-revocation` context body
805
+ * (RFC-ACDP-0014 §4) and derive its §5/§6 trust class. Returns
806
+ * the typed revocation as JSON: `{"revoked_key_fingerprint",
807
+ * "compromised_since", "reason"?, "revoked_key_id"?,
808
+ * "revoked_key_controller", "publisher", "trust_class":
809
+ * "producer_signed"|"registry_attested"}`. The fingerprint is
810
+ * authoritative; `compromised_since` is the compromise boundary
811
+ * T. Never collapse the two trust classes when reporting (§6).
812
+ *
813
+ * * `bodyJson` — the retrieved context `body` (the §5.7 layout
814
+ * including registry-assigned fields).
815
+ * * `signerFingerprint` — the RFC-ACDP-0010 §6 fingerprint of the
816
+ * RESOLVED key that signed the body, for the §5 step 2
817
+ * not-self-signed rule. For `did:key` signers the check runs
818
+ * natively from the body itself; for `did:web` signers resolve
819
+ * the key in JS land (`AcdpDidDocument.keyForAlgorithm` +
820
+ * `fingerprintEd25519B64`) and pass its fingerprint here — a
821
+ * revocation signed by the very key it revokes proves only
822
+ * possession of the attacker-held key and throws
823
+ * (`.code === "key_not_authorized"`).
824
+ *
825
+ * Parsing does NOT verify the body: run the ordinary hash +
826
+ * signature pipeline (`verifyContentHash` + `verifySignature`, or
827
+ * `verifyBodyOffline` for did:key) before trusting the result.
828
+ * Throws with `.code === "schema_violation"` on §4 shape
829
+ * violations.
830
+ */
831
+ static parseKeyRevocation(bodyJson: string, signerFingerprint?: string | undefined | null): string
832
+ /**
833
+ * Apply the RFC-ACDP-0014 §7 compromise-boundary rule — the
834
+ * fail-closed classification the Rust client uses, over the
835
+ * earliest `compromised_since` among the supplied revocations
836
+ * naming the key (§4 monotonicity: a superseding revocation can
837
+ * widen, never quietly shrink, the window — feed the whole
838
+ * lineage through, superseded revocations included).
839
+ *
840
+ * * `revocationsJson` — JSON array of VERIFIED revocations (the
841
+ * shapes `parseKeyRevocation` returns). Which trust classes to
842
+ * act on is the caller's §6 policy.
843
+ * * `signerFingerprint` — fingerprint of the key that signed the
844
+ * context under verification.
845
+ * * `receiptCreatedAtRfc3339` — `created_at` from a registry
846
+ * receipt VERIFIED per RFC-ACDP-0010 §8, or `null` when there
847
+ * is no verified receipt. NEVER the bare body `created_at` — it
848
+ * is registry-assigned, producer-unsigned, and
849
+ * attacker-backdatable (§7 step 1).
850
+ *
851
+ * Returns `{"authorization": "none"}` (no revocation names the
852
+ * key — ordinary rules apply), `{"authorization":
853
+ * "historically_authorized_pre_compromise", "boundary": ...}` (§7
854
+ * step 2 — still verify the signature itself, under the
855
+ * RFC-ACDP-0010 §10 historical rule), or `{"authorization":
856
+ * "none", "boundary": ..., "error": ...}` — fail closed (§7
857
+ * steps 3–4).
858
+ */
859
+ static classifyUnderRevocation(revocationsJson: string, signerFingerprint: string, receiptCreatedAtRfc3339?: string | undefined | null): string
598
860
  }
package/index.js CHANGED
@@ -310,11 +310,12 @@ if (!nativeBinding) {
310
310
  throw new Error(`Failed to load native binding`)
311
311
  }
312
312
 
313
- const { AcdpDid, AcdpDidDocument, AcdpCanonicalizer, AcdpProducer, AcdpP256Producer, AcdpSsrfPolicy, AcdpVerifier } = nativeBinding
313
+ const { AcdpDid, AcdpDidDocument, AcdpCanonicalizer, AcdpMerkle, AcdpProducer, AcdpP256Producer, AcdpSsrfPolicy, AcdpVerifier } = nativeBinding
314
314
 
315
315
  module.exports.AcdpDid = AcdpDid
316
316
  module.exports.AcdpDidDocument = AcdpDidDocument
317
317
  module.exports.AcdpCanonicalizer = AcdpCanonicalizer
318
+ module.exports.AcdpMerkle = AcdpMerkle
318
319
  module.exports.AcdpProducer = AcdpProducer
319
320
  module.exports.AcdpP256Producer = AcdpP256Producer
320
321
  module.exports.AcdpSsrfPolicy = AcdpSsrfPolicy
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentcontextdistributionprotocol/acdp",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "description": "Agent Context Distribution Protocol — Node.js SDK",
5
5
  "license": "MIT OR Apache-2.0",
6
6
  "repository": {
@@ -47,9 +47,9 @@
47
47
  "registry": "https://registry.npmjs.org/"
48
48
  },
49
49
  "optionalDependencies": {
50
- "@agentcontextdistributionprotocol/acdp-darwin-x64": "0.4.0",
51
- "@agentcontextdistributionprotocol/acdp-darwin-arm64": "0.4.0",
52
- "@agentcontextdistributionprotocol/acdp-linux-x64-gnu": "0.4.0",
53
- "@agentcontextdistributionprotocol/acdp-linux-arm64-gnu": "0.4.0"
50
+ "@agentcontextdistributionprotocol/acdp-darwin-x64": "0.6.0",
51
+ "@agentcontextdistributionprotocol/acdp-darwin-arm64": "0.6.0",
52
+ "@agentcontextdistributionprotocol/acdp-linux-x64-gnu": "0.6.0",
53
+ "@agentcontextdistributionprotocol/acdp-linux-arm64-gnu": "0.6.0"
54
54
  }
55
55
  }