@agentcontextdistributionprotocol/acdp 0.5.0 → 0.7.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 +307 -0
  2. package/index.js +2 -1
  3. package/package.json +5 -5
package/index.d.ts CHANGED
@@ -258,6 +258,33 @@ export declare class AcdpCanonicalizer {
258
258
  */
259
259
  static contentHash(jsonStr: string): string
260
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
+ }
261
288
  /**
262
289
  * An ACDP producer: an Ed25519 signing key and its DID identity
263
290
  * (`did:web`, or `did:key` via the `*DidKey` factories — ACDP 0.2).
@@ -634,4 +661,284 @@ export declare class AcdpVerifier {
634
661
  * message otherwise.
635
662
  */
636
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
860
+ /**
861
+ * Mint a signed transparency-log witness cosignature
862
+ * (RFC-ACDP-0015 §5) — the MINT surface a host-language witness
863
+ * service uses. The witness observes a checkpoint and cosigns it
864
+ * with its OWN Ed25519 key (a witness key, distinct from the
865
+ * registry receipt key); the returned `log_cosignature` uses the
866
+ * RFC-ACDP-0010 §5 construction verbatim (the witness signs the
867
+ * ASCII bytes of the `"sha256:<hex>"` cosignature-hash string).
868
+ *
869
+ * * `witnessedCheckpointJson` — the identity-bearing subset of the
870
+ * checkpoint the witness observed: `{"log_id", "tree_size",
871
+ * "root_hash", "timestamp"}` (closed schema).
872
+ * * `witnessDid` — the witness's own DID (`did:web` or `did:key`).
873
+ * The signing-key DID URL is derived as
874
+ * `"<witnessDid>#witness-key-1"` (the §5/§9 witness-key
875
+ * convention).
876
+ * * `witnessSeedHex` — the witness Ed25519 signing seed, hex-
877
+ * encoded (64 hex chars → 32 bytes). The same seed produces
878
+ * byte-identical cosignatures across bindings.
879
+ * * `witnessedAtRfc3339` — the witness-clock observation time
880
+ * (canonical millisecond RFC 3339 UTC; truncated to ms).
881
+ *
882
+ * Returns the signed `log_cosignature` as a JSON string. This is
883
+ * the RAW mint (no §7 obligation — the checkpoint's own signature
884
+ * and consistency against a retained head are the host's job).
885
+ * Throws on malformed input (bad seed / timestamp / witness DID /
886
+ * witnessed_checkpoint).
887
+ */
888
+ static buildWitnessCosignature(witnessedCheckpointJson: string, witnessDid: string, witnessSeedHex: string, witnessedAtRfc3339: string): string
889
+ /**
890
+ * Verify one witness cosignature against a checkpoint the consumer
891
+ * has itself verified, offline per RFC-ACDP-0015 §8 (steps 1–5):
892
+ * closed parse + witness binding, checkpoint binding, the witness
893
+ * signature over the RAW wire preimage against the key resolved
894
+ * from the caller-supplied witness DID document (§9: looked up in
895
+ * `verificationMethod`, retired keys stay verifiable), and the
896
+ * `witnessed_at` well-formedness + forward-skew check.
897
+ *
898
+ * * `cosigJson` — the `log_cosignature` object as received.
899
+ * * `witnessDidDocJson` — the WITNESS's resolved DID document
900
+ * (resolution stays in JS land). Its `id` MUST equal the
901
+ * cosignature's `witness_id`.
902
+ * * `expectedCheckpointJson` — the RFC-ACDP-0012 checkpoint the
903
+ * consumer independently holds and verified; the cosignature's
904
+ * `{log_id, tree_size, root_hash}` MUST match it (§8 step 4).
905
+ * * `nowRfc3339` — the consumer clock (defaults to now).
906
+ * * `maxClockSkewSecs` — §8 step 5 forward allowance (default 120).
907
+ *
908
+ * Returns a JSON verdict: `{"valid": true, "witness_id": ...,
909
+ * "age_secs": int, "stale": bool}` — staleness (§8.1) is policy,
910
+ * not a verification failure — or `{"valid": false, "code":
911
+ * "invalid_witness_cosignature"|..., "error": ...}`. Throws only
912
+ * on malformed host input.
913
+ */
914
+ static verifyWitnessCosignature(cosigJson: string, witnessDidDocJson: string, expectedCheckpointJson: string, nowRfc3339?: string | undefined | null, maxClockSkewSecs?: number | undefined | null): string
915
+ /**
916
+ * Compute the RFC-ACDP-0015 §8 N-witnessed report over a set of
917
+ * cosignatures for a checkpoint the consumer has itself verified.
918
+ * A cosignature counts toward N iff it names a TRUSTED witness,
919
+ * covers the checkpoint's `(log_id, tree_size, root_hash)` tuple,
920
+ * and passes every §8 step; DISTINCT `witness_id` values are
921
+ * counted. A cosignature that fails a step does not fail the
922
+ * checkpoint — it is recorded in `failures` and simply does not
923
+ * count.
924
+ *
925
+ * * `cosignaturesJson` — a JSON array of `log_cosignature` objects.
926
+ * * `expectedCheckpointJson` — the verified checkpoint the quorum
927
+ * is over.
928
+ * * `trustedWitnessDidsJson` — a JSON array of the witness DIDs the
929
+ * consumer trusts; only these can count.
930
+ * * `witnessDidDocsJson` — a JSON object mapping each `witness_id`
931
+ * to its resolved DID document.
932
+ * * `policyJson` — `{"min_witnesses"?, "max_age_secs"?,
933
+ * "max_clock_skew_secs"?}`. Defaults mirror the Rust
934
+ * `WitnessPolicy`: `min_witnesses=1`, `max_age_secs=300` (an
935
+ * explicit `null` disables the freshness split),
936
+ * `max_clock_skew_secs=120`.
937
+ * * `nowRfc3339` — the consumer clock (defaults to now).
938
+ *
939
+ * Returns a JSON report: `{"witnessed_count", "witnesses",
940
+ * "meets_quorum", "fresh_witnessed_count", "meets_fresh_quorum",
941
+ * "failures"}`. Throws on malformed host input.
942
+ */
943
+ static evaluateWitnessQuorum(cosignaturesJson: string, expectedCheckpointJson: string, trustedWitnessDidsJson: string, witnessDidDocsJson: string, policyJson: string, nowRfc3339?: string | undefined | null): string
637
944
  }
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.5.0",
3
+ "version": "0.7.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.5.0",
51
- "@agentcontextdistributionprotocol/acdp-darwin-arm64": "0.5.0",
52
- "@agentcontextdistributionprotocol/acdp-linux-x64-gnu": "0.5.0",
53
- "@agentcontextdistributionprotocol/acdp-linux-arm64-gnu": "0.5.0"
50
+ "@agentcontextdistributionprotocol/acdp-darwin-x64": "0.7.0",
51
+ "@agentcontextdistributionprotocol/acdp-darwin-arm64": "0.7.0",
52
+ "@agentcontextdistributionprotocol/acdp-linux-x64-gnu": "0.7.0",
53
+ "@agentcontextdistributionprotocol/acdp-linux-arm64-gnu": "0.7.0"
54
54
  }
55
55
  }