@metalabel/dfos-protocol 0.3.0 → 0.5.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/PROTOCOL.md CHANGED
@@ -20,15 +20,16 @@ The protocol is not coupled to the DFOS platform. Any system implementing the sa
20
20
 
21
21
  ## Protocol Overview
22
22
 
23
- The DFOS protocol has five components:
23
+ The DFOS protocol has six components:
24
24
 
25
- | Component | Concern |
26
- | --------------------- | ---------------------------------------------------------------------------- |
27
- | **Crypto core** | Identity chains + content chains — Ed25519 signatures, JWS tokens, CID links |
28
- | **Credentials** | Auth tokens (DID-signed JWT) and VC-JWT credentials for authorization |
29
- | **Beacons** | Signed merkle root announcements — periodic commitment over content sets |
30
- | **Countersignatures** | Witness attestationthird-party signatures over existing chain operations |
31
- | **Merkle trees** | SHA-256 binary trees over content IDs inclusion proofs for beacon roots |
25
+ | Component | Concern |
26
+ | --------------------- | ------------------------------------------------------------------------------- |
27
+ | **Crypto core** | Identity chains + content chains — Ed25519 signatures, JWS tokens, CID links |
28
+ | **Credentials** | Auth tokens (DID-signed JWT) and VC-JWT credentials for authorization |
29
+ | **Beacons** | Signed merkle root announcements — periodic commitment over content sets |
30
+ | **Artifacts** | Standalone signed inline documents immutable, CID-addressable structured data |
31
+ | **Countersignatures** | Standalone witness attestation signed references to any CID-addressable op |
32
+ | **Merkle trees** | SHA-256 binary trees over content IDs — inclusion proofs for beacon roots |
32
33
 
33
34
  The crypto core is the trust boundary — everything below it is cryptographically verified. Documents are flat content objects, content-addressed directly: `documentCID = CID(dagCborCanonicalEncode(contentObject))`. What goes inside the content object is application-defined — see the [DFOS Content Model](https://protocol.dfos.com/content-model) for the standard schema library.
34
35
 
@@ -126,6 +127,8 @@ The JWS `typ` header uses protocol-specific values (not IANA media types):
126
127
  | `did:dfos:identity-op` | Identity chain operations |
127
128
  | `did:dfos:content-op` | Content chain operations |
128
129
  | `did:dfos:beacon` | Beacon announcements |
130
+ | `did:dfos:artifact` | Standalone signed inline documents |
131
+ | `did:dfos:countersign` | Standalone witness attestations |
129
132
  | `JWT` | Auth tokens (DID-signed relay authentication) |
130
133
  | `vc+jwt` | VC-JWT credentials (W3C VC Data Model v2) |
131
134
 
@@ -219,7 +222,33 @@ Note: `[0xed, 0x01]` is the unsigned varint encoding of 237 (`0xed`). Since `0xe
219
222
  5. Base32lower multibase encode → "bafyrei..."
220
223
  ```
221
224
 
222
- dag-cbor canonical ordering: map keys sorted by encoded byte length first, then lexicographic. JSON numbers map to CBOR integers. Strings to CBOR text strings. Null to CBOR null. Arrays to CBOR arrays. Objects to CBOR maps with sorted keys.
225
+ dag-cbor canonical ordering: map keys sorted by encoded byte length first, then lexicographic. Strings to CBOR text strings. Null to CBOR null. Arrays to CBOR arrays. Objects to CBOR maps with sorted keys.
226
+
227
+ #### Number Encoding (Critical for CID Determinism)
228
+
229
+ JSON has a single number type (IEEE 754 double). CBOR has distinct integer and floating-point types with different byte encodings. This difference is the most common source of CID divergence across implementations.
230
+
231
+ **Rule: JSON numbers that are mathematically integers (no fractional part) MUST be encoded as CBOR integers (major type 0/1), never as CBOR floats.** This is consistent with the [IPLD data model](https://ipld.io/docs/data-model/) integer/float distinction and required by the [dag-cbor codec spec](https://ipld.io/specs/codecs/dag-cbor/spec/).
232
+
233
+ Why this matters: CBOR integer `1` encodes as a single byte `0x01`. CBOR float `1.0` encodes as three bytes `0xf9 0x3c 0x00` (half-precision). Same logical value, different bytes, different SHA-256, different CID. An implementation that encodes `version: 1` as a float will produce a valid CBOR document but a wrong CID — silent, undetectable without cross-implementation testing.
234
+
235
+ **Common trap**: Languages that decode JSON into untyped maps (Go's `map[string]any`, Python's `dict`, etc.) typically represent all JSON numbers as floating-point. When this decoded value is then CBOR-encoded, it becomes a CBOR float instead of an integer. Implementations MUST normalize number types after JSON deserialization and before CBOR encoding.
236
+
237
+ **Integer bounds**: dag-cbor integers are limited to the range `[-(2^64), 2^64 - 1]`. All integer fields in the current protocol (`version: 1`) are small positive values. Future protocol extensions SHOULD NOT introduce integer fields that exceed JSON's safe integer range (`2^53 - 1`), as JSON serialization would lose precision.
238
+
239
+ **Verification test vector** — encodes `{"version": 1, "type": "test"}`:
240
+
241
+ ```
242
+ Integer encoding (CORRECT):
243
+ CBOR: a2647479706564746573746776657273696f6e01
244
+ CID: bafyreihp6omsp6icc6ee63ox2ovsaxm6s7ikd2a7k5eh2qz2qd5soh5bsa
245
+
246
+ Float encoding (WRONG — different bytes, different CID):
247
+ CBOR: a2647479706564746573746776657273696f6ef93c00
248
+ CID: bafyreiawbms4476m5jlrmqtyvtwe5ta3eo2bh7mdprtomfgfype7j57o4q
249
+ ```
250
+
251
+ If your implementation produces the float CID, your number encoding is incorrect. The byte at offset 19 in the CBOR output is the discriminator: `0x01` = correct (CBOR integer), `0xf9` = wrong (CBOR float16 header).
223
252
 
224
253
  **Worked example (genesis identity operation):**
225
254
 
@@ -558,15 +587,9 @@ typ: did:dfos:beacon
558
587
  cid: bafyreihholuui7s7ns74iem6ahfxsb472hwogbqd32yrrp5fztc3kxa5qu
559
588
  ```
560
589
 
561
- **Witness countersignature** (key 2 signs the same payload — same CID, different kid):
562
-
563
- ```
564
- kid: did:dfos:e3vvtck42d4eacdnzvtrn6#key_ez9a874tckr3dv933d3ckd
565
- typ: did:dfos:beacon
566
- cid: bafyreihholuui7s7ns74iem6ahfxsb472hwogbqd32yrrp5fztc3kxa5qu
567
- ```
590
+ **Witness countersignature** (a separate identity countersigns the beacon by CID):
568
591
 
569
- Both JWS tokens commit to identical bytes (same CID). The controller/witness distinction is determined at verification time by comparing the `kid` DID to the payload `did`.
592
+ A countersignature is a standalone operation with its own CID and `typ: did:dfos:countersign`. See the [Countersignatures](#countersignatures) section below.
570
593
 
571
594
  Full JWS tokens are in [`examples/beacon.json`](https://github.com/metalabel/dfos/blob/main/packages/dfos-protocol/examples/beacon.json).
572
595
 
@@ -674,22 +697,78 @@ Proof path (from [`examples/merkle-tree.json`](https://github.com/metalabel/dfos
674
697
 
675
698
  ---
676
699
 
700
+ ## Artifacts
701
+
702
+ Artifacts are standalone signed inline documents — immutable, CID-addressable proof plane primitives. Unlike chain operations which extend a sequence, an artifact is a single signed statement with no predecessor or successor.
703
+
704
+ ### Payload
705
+
706
+ ```json
707
+ {
708
+ "version": 1,
709
+ "type": "artifact",
710
+ "did": "did:dfos:...",
711
+ "content": {
712
+ "$schema": "https://schemas.dfos.com/profile/v1",
713
+ "name": "Example"
714
+ },
715
+ "createdAt": "2026-03-25T00:00:00.000Z"
716
+ }
717
+ ```
718
+
719
+ The `content` object MUST include a `$schema` string that identifies the artifact's schema. The schema acts as a discriminator — consumers use it to determine how to interpret the artifact's content. Schema names are free-form strings (no protocol-level registry).
720
+
721
+ ### Constraints
722
+
723
+ - **JWS `typ` header**: `did:dfos:artifact`
724
+ - **Max payload size**: 16384 bytes CBOR-encoded. Protocol constant — not configurable
725
+ - **Immutability**: Once published, an artifact is never updated or replaced
726
+ - **CID-addressable**: Each artifact is addressed by the CID of its CBOR-encoded payload
727
+
728
+ ### Verification
729
+
730
+ 1. JWS signature verification against the signing DID's current key state
731
+ 2. CID integrity — `header.cid` matches the CID computed from dag-cbor canonical encoding the raw payload
732
+ 3. Payload schema validation — `version`, `type: "artifact"`, `did`, `content` with `$schema`, `createdAt`
733
+ 4. Size limit — CBOR-encoded payload does not exceed 16384 bytes
734
+
735
+ ---
736
+
677
737
  ## Countersignatures
678
738
 
679
- A countersignature is a witness attestation — a third-party identity signing the same CID-committed bytes as an existing chain operation. Countersignatures use the same JWS format and `typ` (`did:dfos:content-op`) as the original operation.
739
+ A countersignature is a standalone witness attestation — a signed statement that references a target operation by CID. Each countersignature has its own `typ` header (`did:dfos:countersign`), its own payload, and its own CID distinct from the target.
740
+
741
+ ### Payload
742
+
743
+ ```json
744
+ {
745
+ "version": 1,
746
+ "type": "countersign",
747
+ "did": "did:dfos:witness...",
748
+ "targetCID": "bafy...",
749
+ "createdAt": "2026-03-25T00:00:00.000Z"
750
+ }
751
+ ```
680
752
 
681
- ### Discrimination Rule
753
+ The `did` field is the witness identity — the DID signing the attestation. The `targetCID` references the operation being attested to.
682
754
 
683
- The protocol distinguishes author operations from countersignatures by comparing the `kid` DID in the JWS header to the `did` field in the operation payload:
755
+ ### Properties
684
756
 
685
- - **`kid` DID === payload `did`** → author operation (chain operation)
686
- - **`kid` DID !== payload `did`** witness countersignature
757
+ - **JWS `typ` header**: `did:dfos:countersign`
758
+ - **Own CID**: Each countersignature has its own CID derived from its own payload, distinct from the target. This avoids the ambiguity of multiple JWS tokens sharing the same CID
759
+ - **Stateless verification**: Signature + CID integrity + payload schema. No chain state required to verify the cryptographic validity of a countersignature
760
+ - **Composable**: The `targetCID` can reference any CID-addressable operation — content ops, beacons, artifacts, identity ops, even other countersignatures
761
+ - **Immutable**: Once published, a countersignature is permanent
687
762
 
688
- ### Semantics
763
+ ### Verification
689
764
 
690
- A countersignature proves that a witness identity has seen and attested to a specific operation. The witness signs the exact same payload (same CID), but with their own key. The countersignature's JWS header will contain the witness's `kid` (their DID URL), while the payload's `did` field remains the original author's DID.
765
+ 1. Decode JWS, verify `typ` is `did:dfos:countersign`
766
+ 2. Parse and validate countersign payload (`version`, `type: "countersign"`, `did`, `targetCID`, `createdAt`)
767
+ 3. Verify the `kid` DID matches the payload `did` (the witness must sign with their own key)
768
+ 4. CID integrity — `header.cid` matches the CID computed from dag-cbor canonical encoding the raw payload
769
+ 5. Verify EdDSA JWS signature against the witness's public key
691
770
 
692
- Countersignatures are not part of the chain they do not have `previousOperationCID` links and do not affect chain state. They are auxiliary attestations stored alongside chain operations.
771
+ Relay-level semantic checks (target exists, witness author, deduplication) are enforcement concerns, not protocol verification.
693
772
 
694
773
  ---
695
774
 
@@ -715,7 +794,7 @@ Countersignatures are not part of the chain — they do not have `previousOperat
715
794
  2. First op must be `type: "create"` — the signer is the chain creator
716
795
  3. For each subsequent op: verify `previousOperationCID` matches, verify `createdAt` increasing
717
796
  4. Derive the operation CID via dag-cbor canonical encoding. Verify `header.cid` matches the derived CID.
718
- 5. Verify the `kid` DID matches the payload `did` field (mismatches indicate a countersignature, not a chain operation)
797
+ 5. Verify the `kid` DID matches the payload `did` field
719
798
  6. Resolve `kid` via external key resolver (caller provides)
720
799
  7. Verify EdDSA JWS signature
721
800
  8. If `enforceAuthorization` is enabled and the signer DID differs from the chain creator: verify the `authorization` field contains a valid `DFOSContentWrite` VC-JWT issued by the creator DID, with `sub` matching the signer, not expired at `op.createdAt`, and `contentId` (if present) matching this chain
@@ -1041,11 +1120,17 @@ Given the artifacts above, verify:
1041
1120
 
1042
1121
  13. **Delegated content chain verify**: using [`examples/content-delegated.json`](https://github.com/metalabel/dfos/blob/main/packages/dfos-protocol/examples/content-delegated.json), verify a content chain where the genesis is signed by the creator and a subsequent update is signed by a delegate with an embedded `DFOSContentWrite` VC-JWT in the `authorization` field. The VC must be issued by the creator DID, with `sub` matching the delegate DID.
1043
1122
 
1123
+ 14. **Number encoding determinism**: dag-cbor encode `{"version": 1, "type": "test"}` and verify:
1124
+ - CBOR hex is `a2647479706564746573746776657273696f6e01` (20 bytes)
1125
+ - CID is `bafyreihp6omsp6icc6ee63ox2ovsaxm6s7ikd2a7k5eh2qz2qd5soh5bsa`
1126
+ - Byte at offset 19 is `0x01` (CBOR integer 1), NOT `0xf9` (CBOR float header)
1127
+ - If your implementation decodes this payload from JSON (e.g., from a JWS token) and then re-encodes to dag-cbor, the CID MUST still match. This catches the JSON `float64` → CBOR float trap.
1128
+
1044
1129
  ---
1045
1130
 
1046
1131
  ## Source and Verification
1047
1132
 
1048
- All source lives in [`packages/dfos-protocol/`](https://github.com/metalabel/dfos/tree/main/packages/dfos-protocol) — self-contained, zero monorepo dependencies. 293 checks across 5 languages.
1133
+ All source lives in [`packages/dfos-protocol/`](https://github.com/metalabel/dfos/tree/main/packages/dfos-protocol) — self-contained, zero monorepo dependencies. 266 checks across 5 languages.
1049
1134
 
1050
1135
  - [`crypto/ed25519`](https://github.com/metalabel/dfos/blob/main/packages/dfos-protocol/src/crypto/ed25519.ts) — `createNewEd25519Keypair`, `importEd25519Keypair`, `signPayloadEd25519`, `isValidEd25519Signature`
1051
1136
  - [`crypto/jws`](https://github.com/metalabel/dfos/blob/main/packages/dfos-protocol/src/crypto/jws.ts) — `createJws`, `verifyJws`, `decodeJwsUnsafe`
@@ -1054,11 +1139,12 @@ All source lives in [`packages/dfos-protocol/`](https://github.com/metalabel/dfo
1054
1139
  - [`crypto/multiformats`](https://github.com/metalabel/dfos/blob/main/packages/dfos-protocol/src/crypto/multiformats.ts) — `dagCborCanonicalEncode`, `dagCborCanonicalEqual`
1055
1140
  - [`crypto/id`](https://github.com/metalabel/dfos/blob/main/packages/dfos-protocol/src/crypto/id.ts) — `generateId`, `generateIdNoPrefix`, `isValidId`
1056
1141
  - [`chain/multikey`](https://github.com/metalabel/dfos/blob/main/packages/dfos-protocol/src/chain/multikey.ts) — `encodeEd25519Multikey`, `decodeMultikey`
1057
- - [`chain/schemas`](https://github.com/metalabel/dfos/blob/main/packages/dfos-protocol/src/chain/schemas.ts) — `IdentityOperation`, `ContentOperation`, `MultikeyPublicKey`, `VerifiedIdentity`
1058
- - [`chain/identity-chain`](https://github.com/metalabel/dfos/blob/main/packages/dfos-protocol/src/chain/identity-chain.ts) — `signIdentityOperation`, `verifyIdentityChain`
1059
- - [`chain/content-chain`](https://github.com/metalabel/dfos/blob/main/packages/dfos-protocol/src/chain/content-chain.ts) — `signContentOperation`, `verifyContentChain`
1142
+ - [`chain/schemas`](https://github.com/metalabel/dfos/blob/main/packages/dfos-protocol/src/chain/schemas.ts) — `IdentityOperation`, `ContentOperation`, `ArtifactPayload`, `CountersignPayload`, `MultikeyPublicKey`, `VerifiedIdentity`
1143
+ - [`chain/identity-chain`](https://github.com/metalabel/dfos/blob/main/packages/dfos-protocol/src/chain/identity-chain.ts) — `signIdentityOperation`, `verifyIdentityChain`, `verifyIdentityExtensionFromTrustedState`
1144
+ - [`chain/content-chain`](https://github.com/metalabel/dfos/blob/main/packages/dfos-protocol/src/chain/content-chain.ts) — `signContentOperation`, `verifyContentChain`, `verifyContentExtensionFromTrustedState`
1060
1145
  - [`chain/derivation`](https://github.com/metalabel/dfos/blob/main/packages/dfos-protocol/src/chain/derivation.ts) — `deriveChainIdentifier`, `deriveContentId`
1061
1146
  - [`chain/beacon`](https://github.com/metalabel/dfos/blob/main/packages/dfos-protocol/src/chain/beacon.ts) — `signBeacon`, `verifyBeacon`
1147
+ - [`chain/artifact`](https://github.com/metalabel/dfos/blob/main/packages/dfos-protocol/src/chain/artifact.ts) — `signArtifact`, `verifyArtifact`
1062
1148
  - [`chain/countersign`](https://github.com/metalabel/dfos/blob/main/packages/dfos-protocol/src/chain/countersign.ts) — `signCountersignature`, `verifyCountersignature`
1063
1149
  - [`credentials/auth-token`](https://github.com/metalabel/dfos/blob/main/packages/dfos-protocol/src/credentials/auth-token.ts) — `createAuthToken`, `verifyAuthToken`
1064
1150
  - [`credentials/credential`](https://github.com/metalabel/dfos/blob/main/packages/dfos-protocol/src/credentials/credential.ts) — `createCredential`, `verifyCredential`, `decodeCredentialUnsafe`
@@ -1070,16 +1156,17 @@ All source lives in [`packages/dfos-protocol/`](https://github.com/metalabel/dfo
1070
1156
 
1071
1157
  - [DID Method: `did:dfos`](https://protocol.dfos.com/did-method) — W3C DID method specification for identity chains
1072
1158
  - [Content Model](https://protocol.dfos.com/content-model) — Standard content schemas (post, profile) for document content objects
1159
+ - [Web Relay](https://protocol.dfos.com/web-relay) — HTTP relay specification for ingestion, state, and content plane
1073
1160
 
1074
1161
  ### Cross-Language Verification
1075
1162
 
1076
1163
  | Language | Tests | Source |
1077
1164
  | ---------- | ----- | ---------------------------------------------------------------------------------------------------- |
1078
- | TypeScript | 190 | [`tests/`](https://github.com/metalabel/dfos/tree/main/packages/dfos-protocol/tests) |
1079
- | Python | 59 | [`verify/python/`](https://github.com/metalabel/dfos/tree/main/packages/dfos-protocol/verify/python) |
1080
- | Go | 15 | [`verify/go/`](https://github.com/metalabel/dfos/tree/main/packages/dfos-protocol/verify/go) |
1081
- | Rust | 15 | [`verify/rust/`](https://github.com/metalabel/dfos/tree/main/packages/dfos-protocol/verify/rust) |
1082
- | Swift | 14 | [`verify/swift/`](https://github.com/metalabel/dfos/tree/main/packages/dfos-protocol/verify/swift) |
1165
+ | TypeScript | 224 | [`tests/`](https://github.com/metalabel/dfos/tree/main/packages/dfos-protocol/tests) |
1166
+ | Go | 18 | [`verify/go/`](https://github.com/metalabel/dfos/tree/main/packages/dfos-protocol/verify/go) |
1167
+ | Rust | 18 | [`verify/rust/`](https://github.com/metalabel/dfos/tree/main/packages/dfos-protocol/verify/rust) |
1168
+ | Python | 3 | [`verify/python/`](https://github.com/metalabel/dfos/tree/main/packages/dfos-protocol/verify/python) |
1169
+ | Swift | 3 | [`verify/swift/`](https://github.com/metalabel/dfos/tree/main/packages/dfos-protocol/verify/swift) |
1083
1170
 
1084
1171
  ---
1085
1172
 
package/README.md CHANGED
@@ -53,6 +53,19 @@ The `examples/` directory contains deterministic reference fixtures that can be
53
53
  - `merkle-tree.json` — 5 content IDs → sorted tree → root, with inclusion proof
54
54
  - `beacon.json` — signed merkle root announcement with witness countersignature
55
55
 
56
+ ## Cross-Language Verification
57
+
58
+ The `verify/` directory contains independent verification suites that re-derive CIDs and verify signatures from the reference fixtures — proving protocol correctness across implementations:
59
+
60
+ | Language | Path | Status |
61
+ | -------- | ---------------- | ------- |
62
+ | Go | `verify/go/` | Passing |
63
+ | Python | `verify/python/` | Passing |
64
+ | Rust | `verify/rust/` | Passing |
65
+ | Swift | `verify/swift/` | Passing |
66
+
67
+ Each suite uses only its language's native Ed25519, dag-cbor, and multihash implementations — no shared code with the TypeScript reference.
68
+
56
69
  ## License
57
70
 
58
71
  MIT
@@ -113,6 +113,28 @@ declare const BeaconPayload: z.ZodObject<{
113
113
  createdAt: z.ZodISODateTime;
114
114
  }, z.core.$strict>;
115
115
  type BeaconPayload = z.infer<typeof BeaconPayload>;
116
+ /** Max CBOR-encoded payload size for artifacts (bytes) — protocol constant */
117
+ declare const MAX_ARTIFACT_PAYLOAD_SIZE = 16384;
118
+ /** Artifact: standalone signed inline document, immutable, CID-addressable */
119
+ declare const ArtifactPayload: z.ZodObject<{
120
+ version: z.ZodLiteral<1>;
121
+ type: z.ZodLiteral<"artifact">;
122
+ did: z.ZodString;
123
+ content: z.ZodObject<{
124
+ $schema: z.ZodString;
125
+ }, z.core.$catchall<z.ZodUnknown>>;
126
+ createdAt: z.ZodISODateTime;
127
+ }, z.core.$strict>;
128
+ type ArtifactPayload = z.infer<typeof ArtifactPayload>;
129
+ /** Countersign: standalone witness attestation referencing a target operation by CID */
130
+ declare const CountersignPayload: z.ZodObject<{
131
+ version: z.ZodLiteral<1>;
132
+ type: z.ZodLiteral<"countersign">;
133
+ did: z.ZodString;
134
+ targetCID: z.ZodString;
135
+ createdAt: z.ZodISODateTime;
136
+ }, z.core.$strict>;
137
+ type CountersignPayload = z.infer<typeof CountersignPayload>;
116
138
 
117
139
  /** Ed25519 public key multicodec value */
118
140
  declare const ED25519_PUB_MULTICODEC = 237;
@@ -172,6 +194,33 @@ declare const verifyIdentityChain: (input: {
172
194
  didPrefix: string;
173
195
  log: string[];
174
196
  }) => Promise<VerifiedIdentity>;
197
+ /**
198
+ * Verify a single new operation against already-verified identity state
199
+ *
200
+ * The caller guarantees that `currentState` was produced by a correct prior
201
+ * verification (full chain replay or a chain of trusted extensions from a
202
+ * verified genesis). This function performs one signature verification and one
203
+ * state transition — constant time regardless of chain length.
204
+ *
205
+ * Note: key-ID consistency across the full chain history is NOT checked here.
206
+ * That invariant is established during genesis verification and maintained by
207
+ * the protocol's key consistency rules. Periodic full re-verification can
208
+ * audit this property.
209
+ */
210
+ declare const verifyIdentityExtensionFromTrustedState: (input: {
211
+ /** Previously verified identity state */
212
+ currentState: VerifiedIdentity;
213
+ /** CID of the most recent operation in the chain */
214
+ headCID: string;
215
+ /** createdAt timestamp of the most recent operation */
216
+ lastCreatedAt: string;
217
+ /** The new JWS operation to verify */
218
+ newOp: string;
219
+ }) => Promise<{
220
+ state: VerifiedIdentity;
221
+ operationCID: string;
222
+ createdAt: string;
223
+ }>;
175
224
 
176
225
  interface VerifiedContentChain {
177
226
  /** Content identifier — bare 22-char hash derived from genesis CID */
@@ -227,6 +276,29 @@ declare const verifyContentChain: (input: {
227
276
  */
228
277
  enforceAuthorization?: boolean;
229
278
  }) => Promise<VerifiedContentChain>;
279
+ /**
280
+ * Verify a single new content operation against already-verified chain state
281
+ *
282
+ * Same trust model as verifyIdentityExtensionFromTrustedState — the caller
283
+ * guarantees `currentState` was correctly verified. One signature verification,
284
+ * one key resolution, one state transition.
285
+ */
286
+ declare const verifyContentExtensionFromTrustedState: (input: {
287
+ /** Previously verified content chain state */
288
+ currentState: VerifiedContentChain;
289
+ /** createdAt timestamp of the most recent operation */
290
+ lastCreatedAt: string;
291
+ /** The new JWS operation to verify */
292
+ newOp: string;
293
+ /** Resolve a kid (DID URL) to the raw Ed25519 public key bytes */
294
+ resolveKey: (kid: string) => Promise<Uint8Array>;
295
+ /** Enforce creator-sovereignty authorization (see verifyContentChain) */
296
+ enforceAuthorization?: boolean;
297
+ }) => Promise<{
298
+ state: VerifiedContentChain;
299
+ operationCID: string;
300
+ createdAt: string;
301
+ }>;
230
302
 
231
303
  /**
232
304
  * Sign a beacon announcement as a JWS
@@ -253,57 +325,60 @@ declare const verifyBeacon: (input: {
253
325
  now?: number;
254
326
  }) => Promise<VerifiedBeacon>;
255
327
 
328
+ interface VerifiedCountersignature {
329
+ /** CID of this countersign operation (distinct from the target) */
330
+ countersignCID: string;
331
+ /** The witness DID (payload.did — the DID that signed this attestation) */
332
+ witnessDID: string;
333
+ /** The CID being attested to */
334
+ targetCID: string;
335
+ }
256
336
  /**
257
- * Sign an existing content operation as a countersignature (witness JWS)
258
- *
259
- * The witness signs the same payload as the author, producing a different
260
- * JWS token with their own kid. The CID is identical because the payload
261
- * is identical.
337
+ * Sign a countersignature attesting to a target operation by CID
262
338
  */
263
339
  declare const signCountersignature: (input: {
264
- /** The original operation payload (must include did of the author) */
265
- operationPayload: ContentOperation;
266
- /** Witness signer */
340
+ payload: CountersignPayload;
267
341
  signer: Signer;
268
- /** Witness kid — DID URL of the witness (must differ from payload.did) */
269
342
  kid: string;
270
343
  }) => Promise<{
271
344
  jwsToken: string;
272
- operationCID: string;
345
+ countersignCID: string;
273
346
  }>;
274
- interface VerifiedCountersignature {
275
- operationCID: string;
276
- /** The DID that authored the operation (payload.did) */
277
- authorDID: string;
278
- /** The DID that witnessed the operation (kid DID) */
279
- witnessDID: string;
280
- }
281
347
  /**
282
- * Verify a countersignature JWS against an expected operation CID
348
+ * Verify a countersignature JWS stateless verification
283
349
  *
284
- * Checks: valid signature, CID matches, kid DID differs from payload did
350
+ * Checks: valid signature, CID integrity, payload schema. Does NOT check
351
+ * whether the target exists or whether the witness differs from the target
352
+ * author — those are relay-level semantic checks.
285
353
  */
286
354
  declare const verifyCountersignature: (input: {
287
355
  jwsToken: string;
288
- expectedCID: string;
289
356
  resolveKey: (kid: string) => Promise<Uint8Array>;
290
357
  }) => Promise<VerifiedCountersignature>;
291
- interface VerifiedBeaconCountersignature {
292
- beaconCID: string;
293
- /** The DID that controls the beacon (payload.did) */
294
- controllerDID: string;
295
- /** The DID that witnessed the beacon (kid DID) */
296
- witnessDID: string;
358
+
359
+ interface VerifiedArtifact {
360
+ payload: ArtifactPayload;
361
+ artifactCID: string;
297
362
  }
298
363
  /**
299
- * Verify a beacon countersignature JWS against an expected beacon CID
364
+ * Sign an artifact as a JWS
300
365
  *
301
- * Checks: valid signature, CID matches, kid DID differs from payload did
366
+ * Enforces the protocol size limit on the CBOR-encoded payload.
367
+ */
368
+ declare const signArtifact: (input: {
369
+ payload: ArtifactPayload;
370
+ signer: Signer;
371
+ kid: string;
372
+ }) => Promise<{
373
+ jwsToken: string;
374
+ artifactCID: string;
375
+ }>;
376
+ /**
377
+ * Verify an artifact JWS — signature, CID, payload schema, size limit
302
378
  */
303
- declare const verifyBeaconCountersignature: (input: {
379
+ declare const verifyArtifact: (input: {
304
380
  jwsToken: string;
305
- expectedCID: string;
306
381
  resolveKey: (kid: string) => Promise<Uint8Array>;
307
- }) => Promise<VerifiedBeaconCountersignature>;
382
+ }) => Promise<VerifiedArtifact>;
308
383
 
309
- export { BeaconPayload, ContentOperation, ED25519_PRIV_MULTICODEC, ED25519_PUB_MULTICODEC, IdentityOperation, MultikeyPublicKey, type Signer, type VerifiedBeacon, type VerifiedBeaconCountersignature, type VerifiedContentChain, type VerifiedCountersignature, VerifiedIdentity, decodeMultikey, deriveChainIdentifier, deriveContentId, encodeEd25519Multikey, signBeacon, signContentOperation, signCountersignature, signIdentityOperation, verifyBeacon, verifyBeaconCountersignature, verifyContentChain, verifyCountersignature, verifyIdentityChain };
384
+ export { ArtifactPayload, BeaconPayload, ContentOperation, CountersignPayload, ED25519_PRIV_MULTICODEC, ED25519_PUB_MULTICODEC, IdentityOperation, MAX_ARTIFACT_PAYLOAD_SIZE, MultikeyPublicKey, type Signer, type VerifiedArtifact, type VerifiedBeacon, type VerifiedContentChain, type VerifiedCountersignature, VerifiedIdentity, decodeMultikey, deriveChainIdentifier, deriveContentId, encodeEd25519Multikey, signArtifact, signBeacon, signContentOperation, signCountersignature, signIdentityOperation, verifyArtifact, verifyBeacon, verifyContentChain, verifyContentExtensionFromTrustedState, verifyCountersignature, verifyIdentityChain, verifyIdentityExtensionFromTrustedState };
@@ -1,46 +1,58 @@
1
1
  import {
2
+ ArtifactPayload,
2
3
  BeaconPayload,
3
4
  ContentOperation,
5
+ CountersignPayload,
4
6
  ED25519_PRIV_MULTICODEC,
5
7
  ED25519_PUB_MULTICODEC,
6
8
  IdentityOperation,
9
+ MAX_ARTIFACT_PAYLOAD_SIZE,
7
10
  MultikeyPublicKey,
8
11
  VerifiedIdentity,
9
12
  decodeMultikey,
10
13
  deriveChainIdentifier,
11
14
  deriveContentId,
12
15
  encodeEd25519Multikey,
16
+ signArtifact,
13
17
  signBeacon,
14
18
  signContentOperation,
15
19
  signCountersignature,
16
20
  signIdentityOperation,
21
+ verifyArtifact,
17
22
  verifyBeacon,
18
- verifyBeaconCountersignature,
19
23
  verifyContentChain,
24
+ verifyContentExtensionFromTrustedState,
20
25
  verifyCountersignature,
21
- verifyIdentityChain
22
- } from "../chunk-GEVJ3SEV.js";
26
+ verifyIdentityChain,
27
+ verifyIdentityExtensionFromTrustedState
28
+ } from "../chunk-QKHP7UVL.js";
23
29
  import "../chunk-CZSEEZLL.js";
24
30
  import "../chunk-ZXXP5W5N.js";
25
31
  export {
32
+ ArtifactPayload,
26
33
  BeaconPayload,
27
34
  ContentOperation,
35
+ CountersignPayload,
28
36
  ED25519_PRIV_MULTICODEC,
29
37
  ED25519_PUB_MULTICODEC,
30
38
  IdentityOperation,
39
+ MAX_ARTIFACT_PAYLOAD_SIZE,
31
40
  MultikeyPublicKey,
32
41
  VerifiedIdentity,
33
42
  decodeMultikey,
34
43
  deriveChainIdentifier,
35
44
  deriveContentId,
36
45
  encodeEd25519Multikey,
46
+ signArtifact,
37
47
  signBeacon,
38
48
  signContentOperation,
39
49
  signCountersignature,
40
50
  signIdentityOperation,
51
+ verifyArtifact,
41
52
  verifyBeacon,
42
- verifyBeaconCountersignature,
43
53
  verifyContentChain,
54
+ verifyContentExtensionFromTrustedState,
44
55
  verifyCountersignature,
45
- verifyIdentityChain
56
+ verifyIdentityChain,
57
+ verifyIdentityExtensionFromTrustedState
46
58
  };
@@ -104,6 +104,23 @@ var BeaconPayload = z.strictObject({
104
104
  merkleRoot: z.string().regex(/^[0-9a-f]{64}$/),
105
105
  createdAt: Iso8601
106
106
  });
107
+ var MAX_SCHEMA = 256;
108
+ var MAX_ARTIFACT_PAYLOAD_SIZE = 16384;
109
+ var ArtifactContent = z.object({ $schema: z.string().max(MAX_SCHEMA) }).catchall(z.unknown());
110
+ var ArtifactPayload = z.strictObject({
111
+ version: z.literal(1),
112
+ type: z.literal("artifact"),
113
+ did: z.string().max(MAX_DID),
114
+ content: ArtifactContent,
115
+ createdAt: Iso8601
116
+ });
117
+ var CountersignPayload = z.strictObject({
118
+ version: z.literal(1),
119
+ type: z.literal("countersign"),
120
+ did: z.string().max(MAX_DID),
121
+ targetCID: CIDString,
122
+ createdAt: Iso8601
123
+ });
107
124
 
108
125
  // src/chain/multikey.ts
109
126
  import { base58btc } from "multiformats/bases/base58";
@@ -299,6 +316,78 @@ var verifyIdentityChain = async (input) => {
299
316
  controllerKeys: state.controllerKeys
300
317
  };
301
318
  };
319
+ var verifyIdentityExtensionFromTrustedState = async (input) => {
320
+ const { currentState, headCID, lastCreatedAt, newOp } = input;
321
+ if (currentState.isDeleted) {
322
+ throw new Error("cannot extend a deleted identity");
323
+ }
324
+ const decoded = decodeJwsUnsafe(newOp);
325
+ if (!decoded) throw new Error("failed to decode JWS");
326
+ const result = IdentityOperation.safeParse(decoded.payload);
327
+ if (!result.success) {
328
+ const messages = result.error.issues.map((e) => e.message).join(", ");
329
+ throw new Error(messages);
330
+ }
331
+ const op = result.data;
332
+ if (decoded.header.typ !== "did:dfos:identity-op") {
333
+ throw new Error(`invalid typ: ${decoded.header.typ}`);
334
+ }
335
+ if (op.type === "create") {
336
+ throw new Error("extension cannot be a create operation");
337
+ }
338
+ if (op.previousOperationCID !== headCID) {
339
+ throw new Error("previousCID is incorrect");
340
+ }
341
+ if (op.createdAt <= lastCreatedAt) {
342
+ throw new Error("createdAt must be after last op");
343
+ }
344
+ const encoded = await dagCborCanonicalEncode(op);
345
+ const operationCID = encoded.cid.toString();
346
+ if (!decoded.header.cid) throw new Error("missing cid in protected header");
347
+ if (decoded.header.cid !== operationCID) throw new Error("cid mismatch in protected header");
348
+ const kid = decoded.header.kid;
349
+ if (!kid.includes("#")) {
350
+ throw new Error("non-genesis op kid must be DID URL, got bare key ID");
351
+ }
352
+ const hashIdx = kid.indexOf("#");
353
+ const signingKeyId = kid.substring(hashIdx + 1);
354
+ const kidDid = kid.substring(0, hashIdx);
355
+ if (kidDid !== currentState.did) {
356
+ throw new Error("kid DID does not match identity DID");
357
+ }
358
+ const signingKey = currentState.controllerKeys.find((k) => k.id === signingKeyId);
359
+ if (!signingKey) {
360
+ throw new Error(`kid references unknown key: ${signingKeyId}`);
361
+ }
362
+ const { keyBytes } = decodeMultikey(signingKey.publicKeyMultibase);
363
+ try {
364
+ verifyJws({ token: newOp, publicKey: keyBytes });
365
+ } catch {
366
+ throw new Error("invalid signature");
367
+ }
368
+ if (op.type === "update") {
369
+ [op.authKeys, op.assertKeys, op.controllerKeys].forEach((keys) => {
370
+ const set = new Set(keys.map((k) => k.id));
371
+ if (set.size !== keys.length) {
372
+ throw new Error("cannot repeat key ids in same usage");
373
+ }
374
+ });
375
+ }
376
+ const newState = op.type === "update" ? {
377
+ did: currentState.did,
378
+ isDeleted: false,
379
+ authKeys: op.authKeys,
380
+ assertKeys: op.assertKeys,
381
+ controllerKeys: op.controllerKeys
382
+ } : {
383
+ did: currentState.did,
384
+ isDeleted: true,
385
+ authKeys: currentState.authKeys,
386
+ assertKeys: currentState.assertKeys,
387
+ controllerKeys: currentState.controllerKeys
388
+ };
389
+ return { state: newState, operationCID, createdAt: op.createdAt };
390
+ };
302
391
 
303
392
  // src/chain/content-chain.ts
304
393
  var signContentOperation = async (input) => {
@@ -447,6 +536,98 @@ var verifyContentChain = async (input) => {
447
536
  creatorDID: state.creatorDID
448
537
  };
449
538
  };
539
+ var verifyContentExtensionFromTrustedState = async (input) => {
540
+ const { currentState, lastCreatedAt, newOp, resolveKey } = input;
541
+ if (currentState.isDeleted) {
542
+ throw new Error("cannot extend a deleted chain");
543
+ }
544
+ const decoded = decodeJwsUnsafe(newOp);
545
+ if (!decoded) throw new Error("failed to decode JWS");
546
+ const result = ContentOperation.safeParse(decoded.payload);
547
+ if (!result.success) {
548
+ const messages = result.error.issues.map((e) => e.message).join(", ");
549
+ throw new Error(messages);
550
+ }
551
+ const op = result.data;
552
+ if (decoded.header.typ !== "did:dfos:content-op") {
553
+ throw new Error(`invalid typ: ${decoded.header.typ}`);
554
+ }
555
+ if (op.type === "create") {
556
+ throw new Error("extension cannot be a create operation");
557
+ }
558
+ if (op.previousOperationCID !== currentState.headCID) {
559
+ throw new Error("previousOperationCID is incorrect");
560
+ }
561
+ if (op.createdAt <= lastCreatedAt) {
562
+ throw new Error("createdAt must be after last op");
563
+ }
564
+ const kid = decoded.header.kid;
565
+ const hashIdx = kid.indexOf("#");
566
+ if (hashIdx < 0) throw new Error("kid must be a DID URL");
567
+ const kidDid = kid.substring(0, hashIdx);
568
+ if (kidDid !== op.did) {
569
+ throw new Error("kid DID does not match operation did");
570
+ }
571
+ const publicKey = await resolveKey(kid);
572
+ try {
573
+ verifyJws({ token: newOp, publicKey });
574
+ } catch {
575
+ throw new Error("invalid signature");
576
+ }
577
+ if (op.did !== currentState.creatorDID && input.enforceAuthorization) {
578
+ const authorization = op.authorization;
579
+ if (!authorization) {
580
+ throw new Error(`signer ${op.did} is not the chain creator \u2014 authorization VC required`);
581
+ }
582
+ const vcDecoded = decodeCredentialUnsafe(authorization);
583
+ if (!vcDecoded) throw new Error("failed to decode authorization VC");
584
+ const vcKid = vcDecoded.header.kid;
585
+ if (!vcKid || !vcKid.includes("#")) {
586
+ throw new Error("authorization VC kid must be a DID URL");
587
+ }
588
+ let creatorPublicKey;
589
+ try {
590
+ creatorPublicKey = await resolveKey(vcKid);
591
+ } catch {
592
+ throw new Error("cannot resolve creator key for authorization verification");
593
+ }
594
+ const opCreatedAtUnix = Math.floor(new Date(op.createdAt).getTime() / 1e3);
595
+ try {
596
+ const credential = verifyCredential({
597
+ token: authorization,
598
+ publicKey: creatorPublicKey,
599
+ subject: op.did,
600
+ expectedType: VC_TYPE_CONTENT_WRITE,
601
+ currentTime: opCreatedAtUnix
602
+ });
603
+ if (credential.iss !== currentState.creatorDID) {
604
+ throw new Error("VC issuer is not the chain creator");
605
+ }
606
+ if (credential.contentId && credential.contentId !== currentState.contentId) {
607
+ throw new Error(
608
+ `VC contentId ${credential.contentId} does not match chain ${currentState.contentId}`
609
+ );
610
+ }
611
+ } catch (err) {
612
+ const message = err instanceof Error ? err.message : "unknown error";
613
+ throw new Error(`authorization verification failed: ${message}`);
614
+ }
615
+ }
616
+ const encoded = await dagCborCanonicalEncode(op);
617
+ const operationCID = encoded.cid.toString();
618
+ if (!decoded.header.cid) throw new Error("missing cid in protected header");
619
+ if (decoded.header.cid !== operationCID) throw new Error("cid mismatch in protected header");
620
+ const newState = {
621
+ contentId: currentState.contentId,
622
+ genesisCID: currentState.genesisCID,
623
+ headCID: operationCID,
624
+ isDeleted: op.type === "delete",
625
+ currentDocumentCID: op.type === "update" ? op.documentCID : null,
626
+ length: currentState.length + 1,
627
+ creatorDID: currentState.creatorDID
628
+ };
629
+ return { state: newState, operationCID, createdAt: op.createdAt };
630
+ };
450
631
 
451
632
  // src/chain/beacon.ts
452
633
  var signBeacon = async (input) => {
@@ -499,92 +680,102 @@ var verifyBeacon = async (input) => {
499
680
 
500
681
  // src/chain/countersign.ts
501
682
  var signCountersignature = async (input) => {
502
- const encoded = await dagCborCanonicalEncode(input.operationPayload);
503
- const operationCID = encoded.cid.toString();
683
+ const encoded = await dagCborCanonicalEncode(input.payload);
684
+ const countersignCID = encoded.cid.toString();
504
685
  const jwsToken = await createJws({
505
- header: { alg: "EdDSA", typ: "did:dfos:content-op", kid: input.kid, cid: operationCID },
506
- payload: input.operationPayload,
686
+ header: { alg: "EdDSA", typ: "did:dfos:countersign", kid: input.kid, cid: countersignCID },
687
+ payload: input.payload,
507
688
  sign: input.signer
508
689
  });
509
- return { jwsToken, operationCID };
690
+ return { jwsToken, countersignCID };
510
691
  };
511
692
  var verifyCountersignature = async (input) => {
512
693
  const decoded = decodeJwsUnsafe(input.jwsToken);
513
694
  if (!decoded) throw new Error("failed to decode countersignature JWS");
514
- if (decoded.header.typ !== "did:dfos:content-op") {
695
+ if (decoded.header.typ !== "did:dfos:countersign") {
515
696
  throw new Error(`invalid countersignature typ: ${decoded.header.typ}`);
516
697
  }
517
- const result = ContentOperation.safeParse(decoded.payload);
698
+ const result = CountersignPayload.safeParse(decoded.payload);
518
699
  if (!result.success) {
519
700
  const messages = result.error.issues.map((e) => e.message).join(", ");
520
- throw new Error(`invalid operation payload: ${messages}`);
521
- }
522
- const op = result.data;
523
- const encoded = await dagCborCanonicalEncode(op);
524
- const operationCID = encoded.cid.toString();
525
- if (operationCID !== input.expectedCID) {
526
- throw new Error("countersignature CID does not match expected CID");
527
- }
528
- if (decoded.header.cid !== operationCID) {
529
- throw new Error("countersignature header cid mismatch");
701
+ throw new Error(`invalid countersignature payload: ${messages}`);
530
702
  }
703
+ const payload = result.data;
531
704
  const kid = decoded.header.kid;
705
+ const hashIdx = kid.indexOf("#");
706
+ if (hashIdx < 0) throw new Error("countersignature kid must be a DID URL");
707
+ const kidDid = kid.substring(0, hashIdx);
708
+ if (kidDid !== payload.did) {
709
+ throw new Error("countersignature kid DID does not match payload did");
710
+ }
532
711
  const publicKey = await input.resolveKey(kid);
533
712
  try {
534
713
  verifyJws({ token: input.jwsToken, publicKey });
535
714
  } catch {
536
- throw new Error("invalid countersignature");
537
- }
538
- const hashIdx = kid.indexOf("#");
539
- if (hashIdx < 0) throw new Error("countersignature kid must be a DID URL");
540
- const witnessDID = kid.substring(0, hashIdx);
541
- if (witnessDID === op.did) {
542
- throw new Error("countersignature kid DID must differ from operation did (not a witness)");
715
+ throw new Error("invalid countersignature signature");
543
716
  }
717
+ const encoded = await dagCborCanonicalEncode(decoded.payload);
718
+ const countersignCID = encoded.cid.toString();
719
+ if (!decoded.header.cid) throw new Error("missing cid in countersignature header");
720
+ if (decoded.header.cid !== countersignCID) throw new Error("countersignature cid mismatch");
544
721
  return {
545
- operationCID,
546
- authorDID: op.did,
547
- witnessDID
722
+ countersignCID,
723
+ witnessDID: payload.did,
724
+ targetCID: payload.targetCID
548
725
  };
549
726
  };
550
- var verifyBeaconCountersignature = async (input) => {
551
- const decoded = decodeJwsUnsafe(input.jwsToken);
552
- if (!decoded) throw new Error("failed to decode beacon countersignature JWS");
553
- if (decoded.header.typ !== "did:dfos:beacon") {
554
- throw new Error(`invalid beacon countersignature typ: ${decoded.header.typ}`);
727
+
728
+ // src/chain/artifact.ts
729
+ var signArtifact = async (input) => {
730
+ const encoded = await dagCborCanonicalEncode(input.payload);
731
+ const artifactCID = encoded.cid.toString();
732
+ if (encoded.bytes.length > MAX_ARTIFACT_PAYLOAD_SIZE) {
733
+ throw new Error(
734
+ `artifact payload exceeds max size: ${encoded.bytes.length} > ${MAX_ARTIFACT_PAYLOAD_SIZE}`
735
+ );
555
736
  }
556
- const result = BeaconPayload.safeParse(decoded.payload);
737
+ const jwsToken = await createJws({
738
+ header: { alg: "EdDSA", typ: "did:dfos:artifact", kid: input.kid, cid: artifactCID },
739
+ payload: input.payload,
740
+ sign: input.signer
741
+ });
742
+ return { jwsToken, artifactCID };
743
+ };
744
+ var verifyArtifact = async (input) => {
745
+ const decoded = decodeJwsUnsafe(input.jwsToken);
746
+ if (!decoded) throw new Error("failed to decode artifact JWS");
747
+ const result = ArtifactPayload.safeParse(decoded.payload);
557
748
  if (!result.success) {
558
749
  const messages = result.error.issues.map((e) => e.message).join(", ");
559
- throw new Error(`invalid beacon payload: ${messages}`);
750
+ throw new Error(`invalid artifact payload: ${messages}`);
560
751
  }
561
- const beacon = result.data;
562
- const encoded = await dagCborCanonicalEncode(beacon);
563
- const beaconCID = encoded.cid.toString();
564
- if (beaconCID !== input.expectedCID) {
565
- throw new Error("beacon countersignature CID does not match expected CID");
566
- }
567
- if (decoded.header.cid !== beaconCID) {
568
- throw new Error("beacon countersignature header cid mismatch");
752
+ const payload = result.data;
753
+ if (decoded.header.typ !== "did:dfos:artifact") {
754
+ throw new Error(`invalid artifact typ: ${decoded.header.typ}`);
569
755
  }
570
756
  const kid = decoded.header.kid;
757
+ const hashIdx = kid.indexOf("#");
758
+ if (hashIdx < 0) throw new Error("artifact kid must be a DID URL");
759
+ const kidDid = kid.substring(0, hashIdx);
760
+ if (kidDid !== payload.did) {
761
+ throw new Error("artifact kid DID does not match payload did");
762
+ }
571
763
  const publicKey = await input.resolveKey(kid);
572
764
  try {
573
765
  verifyJws({ token: input.jwsToken, publicKey });
574
766
  } catch {
575
- throw new Error("invalid beacon countersignature");
767
+ throw new Error("invalid artifact signature");
576
768
  }
577
- const hashIdx = kid.indexOf("#");
578
- if (hashIdx < 0) throw new Error("beacon countersignature kid must be a DID URL");
579
- const witnessDID = kid.substring(0, hashIdx);
580
- if (witnessDID === beacon.did) {
581
- throw new Error("beacon countersignature kid DID must differ from beacon did (not a witness)");
769
+ const encoded = await dagCborCanonicalEncode(decoded.payload);
770
+ const artifactCID = encoded.cid.toString();
771
+ if (!decoded.header.cid) throw new Error("missing cid in artifact header");
772
+ if (decoded.header.cid !== artifactCID) throw new Error("artifact cid mismatch");
773
+ if (encoded.bytes.length > MAX_ARTIFACT_PAYLOAD_SIZE) {
774
+ throw new Error(
775
+ `artifact payload exceeds max size: ${encoded.bytes.length} > ${MAX_ARTIFACT_PAYLOAD_SIZE}`
776
+ );
582
777
  }
583
- return {
584
- beaconCID,
585
- controllerDID: beacon.did,
586
- witnessDID
587
- };
778
+ return { payload, artifactCID };
588
779
  };
589
780
 
590
781
  export {
@@ -593,6 +784,9 @@ export {
593
784
  VerifiedIdentity,
594
785
  ContentOperation,
595
786
  BeaconPayload,
787
+ MAX_ARTIFACT_PAYLOAD_SIZE,
788
+ ArtifactPayload,
789
+ CountersignPayload,
596
790
  ED25519_PUB_MULTICODEC,
597
791
  ED25519_PRIV_MULTICODEC,
598
792
  encodeEd25519Multikey,
@@ -601,11 +795,14 @@ export {
601
795
  deriveContentId,
602
796
  signIdentityOperation,
603
797
  verifyIdentityChain,
798
+ verifyIdentityExtensionFromTrustedState,
604
799
  signContentOperation,
605
800
  verifyContentChain,
801
+ verifyContentExtensionFromTrustedState,
606
802
  signBeacon,
607
803
  verifyBeacon,
608
804
  signCountersignature,
609
805
  verifyCountersignature,
610
- verifyBeaconCountersignature
806
+ signArtifact,
807
+ verifyArtifact
611
808
  };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export { JwsHeader, JwsVerificationError, JwtClaims, JwtCreateOptions, JwtHeader, JwtVerificationError, JwtVerifyOptions, PrefixedID, base64urlDecode, base64urlEncode, createJws, createJwt, createNewEd25519Keypair, dagCborCanonicalEncode, decodeJwsUnsafe, decodeJwtUnsafe, generateId, generateIdNoPrefix, importEd25519Keypair, isCanonicallyEqual, isValidEd25519Signature, isValidId, normalizedId, parseDagCborCID, signPayloadEd25519, verifyJws, verifyJwt } from './crypto/index.js';
2
- export { BeaconPayload, ContentOperation, ED25519_PRIV_MULTICODEC, ED25519_PUB_MULTICODEC, IdentityOperation, MultikeyPublicKey, Signer, VerifiedBeacon, VerifiedBeaconCountersignature, VerifiedContentChain, VerifiedCountersignature, VerifiedIdentity, decodeMultikey, deriveChainIdentifier, deriveContentId, encodeEd25519Multikey, signBeacon, signContentOperation, signCountersignature, signIdentityOperation, verifyBeacon, verifyBeaconCountersignature, verifyContentChain, verifyCountersignature, verifyIdentityChain } from './chain/index.js';
2
+ export { ArtifactPayload, BeaconPayload, ContentOperation, CountersignPayload, ED25519_PRIV_MULTICODEC, ED25519_PUB_MULTICODEC, IdentityOperation, MAX_ARTIFACT_PAYLOAD_SIZE, MultikeyPublicKey, Signer, VerifiedArtifact, VerifiedBeacon, VerifiedContentChain, VerifiedCountersignature, VerifiedIdentity, decodeMultikey, deriveChainIdentifier, deriveContentId, encodeEd25519Multikey, signArtifact, signBeacon, signContentOperation, signCountersignature, signIdentityOperation, verifyArtifact, verifyBeacon, verifyContentChain, verifyContentExtensionFromTrustedState, verifyCountersignature, verifyIdentityChain, verifyIdentityExtensionFromTrustedState } from './chain/index.js';
3
3
  export { MerkleProof, buildMerkleTree, generateMerkleProof, hashLeaf, hexToBytes, verifyMerkleProof } from './merkle/index.js';
4
4
  export { AuthTokenClaims, AuthTokenCreateOptions, AuthTokenVerificationError, AuthTokenVerifyOptions, ContentReadSubject, ContentWriteSubject, CredentialClaims, CredentialCreateOptions, CredentialVerificationError, CredentialVerifyOptions, DFOSCredentialType, VCClaim, VC_TYPE_CONTENT_READ, VC_TYPE_CONTENT_WRITE, VerifiedAuthToken, VerifiedCredential, createAuthToken, createCredential, decodeCredentialUnsafe, verifyAuthToken, verifyCredential } from './credentials/index.js';
5
5
  import 'multiformats';
package/dist/index.js CHANGED
@@ -1,25 +1,31 @@
1
1
  import {
2
+ ArtifactPayload,
2
3
  BeaconPayload,
3
4
  ContentOperation,
5
+ CountersignPayload,
4
6
  ED25519_PRIV_MULTICODEC,
5
7
  ED25519_PUB_MULTICODEC,
6
8
  IdentityOperation,
9
+ MAX_ARTIFACT_PAYLOAD_SIZE,
7
10
  MultikeyPublicKey,
8
11
  VerifiedIdentity,
9
12
  decodeMultikey,
10
13
  deriveChainIdentifier,
11
14
  deriveContentId,
12
15
  encodeEd25519Multikey,
16
+ signArtifact,
13
17
  signBeacon,
14
18
  signContentOperation,
15
19
  signCountersignature,
16
20
  signIdentityOperation,
21
+ verifyArtifact,
17
22
  verifyBeacon,
18
- verifyBeaconCountersignature,
19
23
  verifyContentChain,
24
+ verifyContentExtensionFromTrustedState,
20
25
  verifyCountersignature,
21
- verifyIdentityChain
22
- } from "./chunk-GEVJ3SEV.js";
26
+ verifyIdentityChain,
27
+ verifyIdentityExtensionFromTrustedState
28
+ } from "./chunk-QKHP7UVL.js";
23
29
  import {
24
30
  buildMerkleTree,
25
31
  generateMerkleProof,
@@ -68,12 +74,14 @@ import {
68
74
  verifyJwt
69
75
  } from "./chunk-ZXXP5W5N.js";
70
76
  export {
77
+ ArtifactPayload,
71
78
  AuthTokenClaims,
72
79
  AuthTokenVerificationError,
73
80
  BeaconPayload,
74
81
  ContentOperation,
75
82
  ContentReadSubject,
76
83
  ContentWriteSubject,
84
+ CountersignPayload,
77
85
  CredentialClaims,
78
86
  CredentialVerificationError,
79
87
  DFOSCredentialType,
@@ -82,6 +90,7 @@ export {
82
90
  IdentityOperation,
83
91
  JwsVerificationError,
84
92
  JwtVerificationError,
93
+ MAX_ARTIFACT_PAYLOAD_SIZE,
85
94
  MultikeyPublicKey,
86
95
  VCClaim,
87
96
  VC_TYPE_CONTENT_READ,
@@ -114,18 +123,21 @@ export {
114
123
  isValidId,
115
124
  normalizedId,
116
125
  parseDagCborCID,
126
+ signArtifact,
117
127
  signBeacon,
118
128
  signContentOperation,
119
129
  signCountersignature,
120
130
  signIdentityOperation,
121
131
  signPayloadEd25519,
132
+ verifyArtifact,
122
133
  verifyAuthToken,
123
134
  verifyBeacon,
124
- verifyBeaconCountersignature,
125
135
  verifyContentChain,
136
+ verifyContentExtensionFromTrustedState,
126
137
  verifyCountersignature,
127
138
  verifyCredential,
128
139
  verifyIdentityChain,
140
+ verifyIdentityExtensionFromTrustedState,
129
141
  verifyJws,
130
142
  verifyJwt,
131
143
  verifyMerkleProof
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@metalabel/dfos-protocol",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "type": "module",
5
5
  "description": "DFOS Protocol — Ed25519 signed chain primitives, beacons, merkle trees, and verification",
6
6
  "license": "MIT",
@@ -69,7 +69,7 @@
69
69
  "ajv-formats": "^3.0.1",
70
70
  "tsup": "^8.5.1",
71
71
  "tsx": "^4.21.0",
72
- "vitest": "^4.0.18"
72
+ "vitest": "^4.1.0"
73
73
  },
74
74
  "scripts": {
75
75
  "build": "tsup",