@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 +122 -35
- package/README.md +13 -0
- package/dist/chain/index.d.ts +107 -32
- package/dist/chain/index.js +17 -5
- package/dist/{chunk-GEVJ3SEV.js → chunk-QKHP7UVL.js} +251 -54
- package/dist/index.d.ts +1 -1
- package/dist/index.js +16 -4
- package/package.json +2 -2
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
|
|
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
|
-
| **
|
|
31
|
-
| **
|
|
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.
|
|
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** (
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
753
|
+
The `did` field is the witness identity — the DID signing the attestation. The `targetCID` references the operation being attested to.
|
|
682
754
|
|
|
683
|
-
|
|
755
|
+
### Properties
|
|
684
756
|
|
|
685
|
-
-
|
|
686
|
-
-
|
|
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
|
-
###
|
|
763
|
+
### Verification
|
|
689
764
|
|
|
690
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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 |
|
|
1079
|
-
|
|
|
1080
|
-
|
|
|
1081
|
-
|
|
|
1082
|
-
| 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
|
package/dist/chain/index.d.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
348
|
+
* Verify a countersignature JWS — stateless verification
|
|
283
349
|
*
|
|
284
|
-
* Checks: valid signature, CID
|
|
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
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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
|
-
*
|
|
364
|
+
* Sign an artifact as a JWS
|
|
300
365
|
*
|
|
301
|
-
*
|
|
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
|
|
379
|
+
declare const verifyArtifact: (input: {
|
|
304
380
|
jwsToken: string;
|
|
305
|
-
expectedCID: string;
|
|
306
381
|
resolveKey: (kid: string) => Promise<Uint8Array>;
|
|
307
|
-
}) => Promise<
|
|
382
|
+
}) => Promise<VerifiedArtifact>;
|
|
308
383
|
|
|
309
|
-
export { BeaconPayload, ContentOperation, ED25519_PRIV_MULTICODEC, ED25519_PUB_MULTICODEC, IdentityOperation, MultikeyPublicKey, type Signer, type
|
|
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 };
|
package/dist/chain/index.js
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
503
|
-
const
|
|
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:
|
|
506
|
-
payload: input.
|
|
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,
|
|
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:
|
|
695
|
+
if (decoded.header.typ !== "did:dfos:countersign") {
|
|
515
696
|
throw new Error(`invalid countersignature typ: ${decoded.header.typ}`);
|
|
516
697
|
}
|
|
517
|
-
const result =
|
|
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
|
|
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
|
-
|
|
546
|
-
|
|
547
|
-
|
|
722
|
+
countersignCID,
|
|
723
|
+
witnessDID: payload.did,
|
|
724
|
+
targetCID: payload.targetCID
|
|
548
725
|
};
|
|
549
726
|
};
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
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
|
|
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
|
|
750
|
+
throw new Error(`invalid artifact payload: ${messages}`);
|
|
560
751
|
}
|
|
561
|
-
const
|
|
562
|
-
|
|
563
|
-
|
|
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
|
|
767
|
+
throw new Error("invalid artifact signature");
|
|
576
768
|
}
|
|
577
|
-
const
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
if (
|
|
581
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
+
"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
|
|
72
|
+
"vitest": "^4.1.0"
|
|
73
73
|
},
|
|
74
74
|
"scripts": {
|
|
75
75
|
"build": "tsup",
|