@metalabel/dfos-protocol 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/PROTOCOL.md +873 -0
- package/README.md +50 -0
- package/dist/chain/index.d.ts +196 -0
- package/dist/chain/index.js +33 -0
- package/dist/chunk-3PB644X2.js +330 -0
- package/dist/chunk-LWC4PWGZ.js +385 -0
- package/dist/chunk-ZXXP5W5N.js +251 -0
- package/dist/crypto/index.d.ts +205 -0
- package/dist/crypto/index.js +46 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +108 -0
- package/dist/registry/index.d.ts +151 -0
- package/dist/registry/index.js +36 -0
- package/examples/content-delete.json +28 -0
- package/examples/content-lifecycle.json +39 -0
- package/examples/identity-delete.json +19 -0
- package/examples/identity-genesis.json +18 -0
- package/examples/identity-rotation.json +19 -0
- package/openapi.yaml +408 -0
- package/package.json +75 -0
- package/schemas/document-envelope.v1.json +37 -0
- package/schemas/post.v1.json +59 -0
- package/schemas/profile.v1.json +52 -0
package/README.md
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# @metalabel/dfos-protocol
|
|
2
|
+
|
|
3
|
+
Ed25519 signed chain primitives, verifiable content, and registry for the DFOS Protocol.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @metalabel/dfos-protocol
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
// Chain verification
|
|
15
|
+
import { verifyContentChain, verifyIdentityChain } from '@metalabel/dfos-protocol/chain';
|
|
16
|
+
// Crypto primitives
|
|
17
|
+
import { createJws, dagCborCanonicalEncode, verifyJws } from '@metalabel/dfos-protocol/crypto';
|
|
18
|
+
// Reference registry server
|
|
19
|
+
import { createRegistryServer } from '@metalabel/dfos-protocol/registry';
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Subpath Exports
|
|
23
|
+
|
|
24
|
+
| Export | Description |
|
|
25
|
+
| ----------------------------------- | ------------------------------------------------------------------- |
|
|
26
|
+
| `@metalabel/dfos-protocol/chain` | Identity and content chain signing, verification, multikey encoding |
|
|
27
|
+
| `@metalabel/dfos-protocol/crypto` | Ed25519, JWS, JWT, dag-cbor, base64url, ID generation |
|
|
28
|
+
| `@metalabel/dfos-protocol/registry` | Hono-based registry server, in-memory store, Zod schemas |
|
|
29
|
+
|
|
30
|
+
## Protocol Specification
|
|
31
|
+
|
|
32
|
+
See [PROTOCOL.md](./PROTOCOL.md) for the complete protocol specification with worked examples and test vectors.
|
|
33
|
+
|
|
34
|
+
## Examples
|
|
35
|
+
|
|
36
|
+
The `examples/` directory contains deterministic reference chain fixtures that can be independently verified by any Ed25519 + dag-cbor implementation:
|
|
37
|
+
|
|
38
|
+
- `identity-genesis.json` — single create operation
|
|
39
|
+
- `identity-rotation.json` — genesis + key rotation
|
|
40
|
+
- `identity-delete.json` — genesis + delete (terminal)
|
|
41
|
+
- `content-lifecycle.json` — create + update (with both documents)
|
|
42
|
+
- `content-delete.json` — create + delete
|
|
43
|
+
|
|
44
|
+
## API Documentation
|
|
45
|
+
|
|
46
|
+
See [openapi.yaml](./openapi.yaml) for the registry API specification.
|
|
47
|
+
|
|
48
|
+
## License
|
|
49
|
+
|
|
50
|
+
MIT
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
/** Function that signs a byte array and returns a signature */
|
|
4
|
+
type Signer = (message: Uint8Array) => Promise<Uint8Array>;
|
|
5
|
+
declare const MultikeyPublicKey: z.ZodObject<{
|
|
6
|
+
id: z.ZodString;
|
|
7
|
+
type: z.ZodLiteral<"Multikey">;
|
|
8
|
+
publicKeyMultibase: z.ZodString;
|
|
9
|
+
}, z.core.$strict>;
|
|
10
|
+
type MultikeyPublicKey = z.infer<typeof MultikeyPublicKey>;
|
|
11
|
+
declare const IdentityOperation: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
12
|
+
version: z.ZodLiteral<1>;
|
|
13
|
+
type: z.ZodLiteral<"create">;
|
|
14
|
+
authKeys: z.ZodArray<z.ZodObject<{
|
|
15
|
+
id: z.ZodString;
|
|
16
|
+
type: z.ZodLiteral<"Multikey">;
|
|
17
|
+
publicKeyMultibase: z.ZodString;
|
|
18
|
+
}, z.core.$strict>>;
|
|
19
|
+
assertKeys: z.ZodArray<z.ZodObject<{
|
|
20
|
+
id: z.ZodString;
|
|
21
|
+
type: z.ZodLiteral<"Multikey">;
|
|
22
|
+
publicKeyMultibase: z.ZodString;
|
|
23
|
+
}, z.core.$strict>>;
|
|
24
|
+
controllerKeys: z.ZodArray<z.ZodObject<{
|
|
25
|
+
id: z.ZodString;
|
|
26
|
+
type: z.ZodLiteral<"Multikey">;
|
|
27
|
+
publicKeyMultibase: z.ZodString;
|
|
28
|
+
}, z.core.$strict>>;
|
|
29
|
+
createdAt: z.ZodISODateTime;
|
|
30
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
31
|
+
version: z.ZodLiteral<1>;
|
|
32
|
+
type: z.ZodLiteral<"update">;
|
|
33
|
+
previousOperationCID: z.ZodString;
|
|
34
|
+
authKeys: z.ZodArray<z.ZodObject<{
|
|
35
|
+
id: z.ZodString;
|
|
36
|
+
type: z.ZodLiteral<"Multikey">;
|
|
37
|
+
publicKeyMultibase: z.ZodString;
|
|
38
|
+
}, z.core.$strict>>;
|
|
39
|
+
assertKeys: z.ZodArray<z.ZodObject<{
|
|
40
|
+
id: z.ZodString;
|
|
41
|
+
type: z.ZodLiteral<"Multikey">;
|
|
42
|
+
publicKeyMultibase: z.ZodString;
|
|
43
|
+
}, z.core.$strict>>;
|
|
44
|
+
controllerKeys: z.ZodArray<z.ZodObject<{
|
|
45
|
+
id: z.ZodString;
|
|
46
|
+
type: z.ZodLiteral<"Multikey">;
|
|
47
|
+
publicKeyMultibase: z.ZodString;
|
|
48
|
+
}, z.core.$strict>>;
|
|
49
|
+
createdAt: z.ZodISODateTime;
|
|
50
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
51
|
+
version: z.ZodLiteral<1>;
|
|
52
|
+
type: z.ZodLiteral<"delete">;
|
|
53
|
+
previousOperationCID: z.ZodString;
|
|
54
|
+
createdAt: z.ZodISODateTime;
|
|
55
|
+
}, z.core.$strict>], "type">;
|
|
56
|
+
type IdentityOperation = z.infer<typeof IdentityOperation>;
|
|
57
|
+
declare const VerifiedIdentity: z.ZodObject<{
|
|
58
|
+
did: z.ZodString;
|
|
59
|
+
isDeleted: z.ZodBoolean;
|
|
60
|
+
authKeys: z.ZodArray<z.ZodObject<{
|
|
61
|
+
id: z.ZodString;
|
|
62
|
+
type: z.ZodLiteral<"Multikey">;
|
|
63
|
+
publicKeyMultibase: z.ZodString;
|
|
64
|
+
}, z.core.$strict>>;
|
|
65
|
+
assertKeys: z.ZodArray<z.ZodObject<{
|
|
66
|
+
id: z.ZodString;
|
|
67
|
+
type: z.ZodLiteral<"Multikey">;
|
|
68
|
+
publicKeyMultibase: z.ZodString;
|
|
69
|
+
}, z.core.$strict>>;
|
|
70
|
+
controllerKeys: z.ZodArray<z.ZodObject<{
|
|
71
|
+
id: z.ZodString;
|
|
72
|
+
type: z.ZodLiteral<"Multikey">;
|
|
73
|
+
publicKeyMultibase: z.ZodString;
|
|
74
|
+
}, z.core.$strict>>;
|
|
75
|
+
}, z.core.$strict>;
|
|
76
|
+
type VerifiedIdentity = z.infer<typeof VerifiedIdentity>;
|
|
77
|
+
declare const ContentOperation: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
78
|
+
version: z.ZodLiteral<1>;
|
|
79
|
+
type: z.ZodLiteral<"create">;
|
|
80
|
+
documentCID: z.ZodString;
|
|
81
|
+
createdAt: z.ZodISODateTime;
|
|
82
|
+
note: z.ZodNullable<z.ZodString>;
|
|
83
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
84
|
+
version: z.ZodLiteral<1>;
|
|
85
|
+
type: z.ZodLiteral<"update">;
|
|
86
|
+
previousOperationCID: z.ZodString;
|
|
87
|
+
documentCID: z.ZodNullable<z.ZodString>;
|
|
88
|
+
createdAt: z.ZodISODateTime;
|
|
89
|
+
note: z.ZodNullable<z.ZodString>;
|
|
90
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
91
|
+
version: z.ZodLiteral<1>;
|
|
92
|
+
type: z.ZodLiteral<"delete">;
|
|
93
|
+
previousOperationCID: z.ZodString;
|
|
94
|
+
createdAt: z.ZodISODateTime;
|
|
95
|
+
note: z.ZodNullable<z.ZodString>;
|
|
96
|
+
}, z.core.$strict>], "type">;
|
|
97
|
+
type ContentOperation = z.infer<typeof ContentOperation>;
|
|
98
|
+
|
|
99
|
+
/** Ed25519 public key multicodec value */
|
|
100
|
+
declare const ED25519_PUB_MULTICODEC = 237;
|
|
101
|
+
/** Ed25519 private key multicodec value */
|
|
102
|
+
declare const ED25519_PRIV_MULTICODEC = 4864;
|
|
103
|
+
/**
|
|
104
|
+
* Encode an Ed25519 public key as a W3C Multikey multibase string
|
|
105
|
+
*
|
|
106
|
+
* Format: 'z' + base58btc([0xed, 0x01] + publicKeyBytes)
|
|
107
|
+
* Result starts with "z6Mk..."
|
|
108
|
+
*/
|
|
109
|
+
declare const encodeEd25519Multikey: (publicKeyBytes: Uint8Array) => string;
|
|
110
|
+
/**
|
|
111
|
+
* Decode a Multikey multibase string to raw key bytes and codec
|
|
112
|
+
*
|
|
113
|
+
* Supports Ed25519 public keys (z6Mk... prefix)
|
|
114
|
+
*/
|
|
115
|
+
declare const decodeMultikey: (multibase: string) => {
|
|
116
|
+
keyBytes: Uint8Array;
|
|
117
|
+
codec: number;
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Derive a prefixed chain identifier from CID bytes
|
|
122
|
+
*
|
|
123
|
+
* Used for identity DIDs: deriveChainIdentifier(cidBytes, 'did:dfos') → 'did:dfos:xxxx'
|
|
124
|
+
*/
|
|
125
|
+
declare const deriveChainIdentifier: (cidBytes: Uint8Array, prefix: string) => string;
|
|
126
|
+
/**
|
|
127
|
+
* Derive a bare entity identifier from CID bytes
|
|
128
|
+
*
|
|
129
|
+
* Returns the raw 22-char hash with no prefix. Applications may add
|
|
130
|
+
* their own prefix for routing (e.g., post_xxxx) — that's semantic sugar.
|
|
131
|
+
*/
|
|
132
|
+
declare const deriveEntityId: (cidBytes: Uint8Array) => string;
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Sign an identity operation as a JWS and derive the operation CID
|
|
136
|
+
*/
|
|
137
|
+
declare const signIdentityOperation: (input: {
|
|
138
|
+
operation: IdentityOperation;
|
|
139
|
+
signer: Signer;
|
|
140
|
+
keyId: string;
|
|
141
|
+
/** DID of the identity — omit for genesis (bare kid) */
|
|
142
|
+
identityDID?: string;
|
|
143
|
+
}) => Promise<{
|
|
144
|
+
jwsToken: string;
|
|
145
|
+
operationCID: string;
|
|
146
|
+
}>;
|
|
147
|
+
/**
|
|
148
|
+
* Verify a log of JWS identity operations and derive the identity
|
|
149
|
+
*
|
|
150
|
+
* Walks the chain from genesis, verifying signatures and chain integrity.
|
|
151
|
+
* Returns the final verified identity state.
|
|
152
|
+
*/
|
|
153
|
+
declare const verifyIdentityChain: (input: {
|
|
154
|
+
didPrefix: string;
|
|
155
|
+
log: string[];
|
|
156
|
+
}) => Promise<VerifiedIdentity>;
|
|
157
|
+
|
|
158
|
+
interface VerifiedContentChain {
|
|
159
|
+
/** Entity identifier — bare 22-char hash derived from genesis CID */
|
|
160
|
+
entityId: string;
|
|
161
|
+
/** CID of the genesis operation */
|
|
162
|
+
genesisCID: string;
|
|
163
|
+
/** CID of the most recent operation */
|
|
164
|
+
headCID: string;
|
|
165
|
+
/** Whether the chain has been terminated by a delete */
|
|
166
|
+
isDeleted: boolean;
|
|
167
|
+
/** The current documentCID (null if cleared or deleted) */
|
|
168
|
+
currentDocumentCID: string | null;
|
|
169
|
+
/** Number of operations in the chain */
|
|
170
|
+
length: number;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Sign a content chain operation as a JWS and derive the operation CID
|
|
174
|
+
*/
|
|
175
|
+
declare const signContentOperation: (input: {
|
|
176
|
+
operation: ContentOperation;
|
|
177
|
+
signer: Signer;
|
|
178
|
+
/** kid for the JWS header — should be a DID URL: "did:dfos:xxx#key_yyy" */
|
|
179
|
+
kid: string;
|
|
180
|
+
}) => Promise<{
|
|
181
|
+
jwsToken: string;
|
|
182
|
+
operationCID: string;
|
|
183
|
+
}>;
|
|
184
|
+
/**
|
|
185
|
+
* Verify a content chain's structural integrity and signatures
|
|
186
|
+
*
|
|
187
|
+
* The caller provides a key resolver to look up public keys from kid values.
|
|
188
|
+
* This keeps the content chain protocol independent of identity resolution.
|
|
189
|
+
*/
|
|
190
|
+
declare const verifyContentChain: (input: {
|
|
191
|
+
log: string[];
|
|
192
|
+
/** Resolve a kid (DID URL) to the raw Ed25519 public key bytes */
|
|
193
|
+
resolveKey: (kid: string) => Promise<Uint8Array>;
|
|
194
|
+
}) => Promise<VerifiedContentChain>;
|
|
195
|
+
|
|
196
|
+
export { ContentOperation, ED25519_PRIV_MULTICODEC, ED25519_PUB_MULTICODEC, IdentityOperation, MultikeyPublicKey, type Signer, type VerifiedContentChain, VerifiedIdentity, decodeMultikey, deriveChainIdentifier, deriveEntityId, encodeEd25519Multikey, signContentOperation, signIdentityOperation, verifyContentChain, verifyIdentityChain };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ContentOperation,
|
|
3
|
+
ED25519_PRIV_MULTICODEC,
|
|
4
|
+
ED25519_PUB_MULTICODEC,
|
|
5
|
+
IdentityOperation,
|
|
6
|
+
MultikeyPublicKey,
|
|
7
|
+
VerifiedIdentity,
|
|
8
|
+
decodeMultikey,
|
|
9
|
+
deriveChainIdentifier,
|
|
10
|
+
deriveEntityId,
|
|
11
|
+
encodeEd25519Multikey,
|
|
12
|
+
signContentOperation,
|
|
13
|
+
signIdentityOperation,
|
|
14
|
+
verifyContentChain,
|
|
15
|
+
verifyIdentityChain
|
|
16
|
+
} from "../chunk-LWC4PWGZ.js";
|
|
17
|
+
import "../chunk-ZXXP5W5N.js";
|
|
18
|
+
export {
|
|
19
|
+
ContentOperation,
|
|
20
|
+
ED25519_PRIV_MULTICODEC,
|
|
21
|
+
ED25519_PUB_MULTICODEC,
|
|
22
|
+
IdentityOperation,
|
|
23
|
+
MultikeyPublicKey,
|
|
24
|
+
VerifiedIdentity,
|
|
25
|
+
decodeMultikey,
|
|
26
|
+
deriveChainIdentifier,
|
|
27
|
+
deriveEntityId,
|
|
28
|
+
encodeEd25519Multikey,
|
|
29
|
+
signContentOperation,
|
|
30
|
+
signIdentityOperation,
|
|
31
|
+
verifyContentChain,
|
|
32
|
+
verifyIdentityChain
|
|
33
|
+
};
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
import {
|
|
2
|
+
MultikeyPublicKey,
|
|
3
|
+
decodeMultikey,
|
|
4
|
+
verifyContentChain,
|
|
5
|
+
verifyIdentityChain
|
|
6
|
+
} from "./chunk-LWC4PWGZ.js";
|
|
7
|
+
import {
|
|
8
|
+
dagCborCanonicalEncode,
|
|
9
|
+
decodeJwsUnsafe
|
|
10
|
+
} from "./chunk-ZXXP5W5N.js";
|
|
11
|
+
|
|
12
|
+
// src/registry/schemas.ts
|
|
13
|
+
import { z } from "zod";
|
|
14
|
+
var OperationEntry = z.strictObject({
|
|
15
|
+
cid: z.string(),
|
|
16
|
+
jwsToken: z.string(),
|
|
17
|
+
createdAt: z.string()
|
|
18
|
+
});
|
|
19
|
+
var PaginatedOperations = z.strictObject({
|
|
20
|
+
operations: z.array(OperationEntry),
|
|
21
|
+
nextCursor: z.string().nullable()
|
|
22
|
+
});
|
|
23
|
+
var PaginationParams = z.object({
|
|
24
|
+
cursor: z.string().optional(),
|
|
25
|
+
limit: z.coerce.number().int().min(1).max(100).default(25)
|
|
26
|
+
});
|
|
27
|
+
var SubmitIdentityChainRequest = z.strictObject({
|
|
28
|
+
chain: z.array(z.string()).min(1)
|
|
29
|
+
});
|
|
30
|
+
var SubmitIdentityChainResponse = z.strictObject({
|
|
31
|
+
did: z.string(),
|
|
32
|
+
isDeleted: z.boolean(),
|
|
33
|
+
authKeys: z.array(MultikeyPublicKey),
|
|
34
|
+
assertKeys: z.array(MultikeyPublicKey),
|
|
35
|
+
controllerKeys: z.array(MultikeyPublicKey)
|
|
36
|
+
});
|
|
37
|
+
var ResolveIdentityResponse = SubmitIdentityChainResponse;
|
|
38
|
+
var IdentityOperationsParams = PaginationParams;
|
|
39
|
+
var IdentityOperationsResponse = PaginatedOperations;
|
|
40
|
+
var SubmitContentChainRequest = z.strictObject({
|
|
41
|
+
chain: z.array(z.string()).min(1)
|
|
42
|
+
});
|
|
43
|
+
var SubmitContentChainResponse = z.strictObject({
|
|
44
|
+
entityId: z.string(),
|
|
45
|
+
isDeleted: z.boolean(),
|
|
46
|
+
currentDocumentCID: z.string().nullable(),
|
|
47
|
+
genesisCID: z.string(),
|
|
48
|
+
headCID: z.string()
|
|
49
|
+
});
|
|
50
|
+
var ResolveEntityResponse = SubmitContentChainResponse;
|
|
51
|
+
var EntityOperationsParams = PaginationParams;
|
|
52
|
+
var EntityOperationsResponse = PaginatedOperations;
|
|
53
|
+
var ResolveOperationResponse = z.strictObject({
|
|
54
|
+
cid: z.string(),
|
|
55
|
+
jwsToken: z.string()
|
|
56
|
+
});
|
|
57
|
+
var ResolveDocumentResponse = z.strictObject({
|
|
58
|
+
cid: z.string(),
|
|
59
|
+
content: z.unknown()
|
|
60
|
+
});
|
|
61
|
+
var RegistryError = z.strictObject({
|
|
62
|
+
error: z.string(),
|
|
63
|
+
message: z.string()
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// src/registry/server.ts
|
|
67
|
+
import { Hono } from "hono";
|
|
68
|
+
|
|
69
|
+
// src/registry/store.ts
|
|
70
|
+
var ChainStore = class {
|
|
71
|
+
identities = /* @__PURE__ */ new Map();
|
|
72
|
+
entities = /* @__PURE__ */ new Map();
|
|
73
|
+
documents = /* @__PURE__ */ new Map();
|
|
74
|
+
// --- identities ---
|
|
75
|
+
getIdentityChain(did) {
|
|
76
|
+
return this.identities.get(did);
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Submit an identity chain. Returns 'accepted' | 'noop' | 'conflict'.
|
|
80
|
+
* - accepted: chain was stored or extended
|
|
81
|
+
* - noop: submitted chain is same as or prefix of stored chain
|
|
82
|
+
* - conflict: submitted chain diverges from stored chain (fork)
|
|
83
|
+
*/
|
|
84
|
+
submitIdentityChain(did, operations) {
|
|
85
|
+
return this.submitChain(this.identities, did, operations);
|
|
86
|
+
}
|
|
87
|
+
// --- entities ---
|
|
88
|
+
getEntityChain(entityId) {
|
|
89
|
+
return this.entities.get(entityId);
|
|
90
|
+
}
|
|
91
|
+
submitEntityChain(entityId, operations) {
|
|
92
|
+
return this.submitChain(this.entities, entityId, operations);
|
|
93
|
+
}
|
|
94
|
+
// --- documents ---
|
|
95
|
+
getDocument(cid) {
|
|
96
|
+
return this.documents.get(cid);
|
|
97
|
+
}
|
|
98
|
+
setDocument(cid, content) {
|
|
99
|
+
this.documents.set(cid, content);
|
|
100
|
+
}
|
|
101
|
+
// --- operations (lookup across all chains) ---
|
|
102
|
+
getOperation(cid) {
|
|
103
|
+
for (const chain of this.identities.values()) {
|
|
104
|
+
const op = chain.operations.find((o) => o.cid === cid);
|
|
105
|
+
if (op) return op;
|
|
106
|
+
}
|
|
107
|
+
for (const chain of this.entities.values()) {
|
|
108
|
+
const op = chain.operations.find((o) => o.cid === cid);
|
|
109
|
+
if (op) return op;
|
|
110
|
+
}
|
|
111
|
+
return void 0;
|
|
112
|
+
}
|
|
113
|
+
// --- shared chain submission logic ---
|
|
114
|
+
submitChain(store, id, operations) {
|
|
115
|
+
const existing = store.get(id);
|
|
116
|
+
if (!existing) {
|
|
117
|
+
store.set(id, { operations });
|
|
118
|
+
return "accepted";
|
|
119
|
+
}
|
|
120
|
+
if (operations.length <= existing.operations.length) {
|
|
121
|
+
for (let i = 0; i < operations.length; i++) {
|
|
122
|
+
if (operations[i].cid !== existing.operations[i].cid) {
|
|
123
|
+
return "conflict";
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return "noop";
|
|
127
|
+
}
|
|
128
|
+
for (let i = 0; i < existing.operations.length; i++) {
|
|
129
|
+
if (operations[i].cid !== existing.operations[i].cid) {
|
|
130
|
+
return "conflict";
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
store.set(id, { operations });
|
|
134
|
+
return "accepted";
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
// src/registry/server.ts
|
|
139
|
+
var err = (code, message) => ({
|
|
140
|
+
error: code,
|
|
141
|
+
message
|
|
142
|
+
});
|
|
143
|
+
var extractOperationEntries = async (chain) => {
|
|
144
|
+
const entries = [];
|
|
145
|
+
for (const jwsToken of chain) {
|
|
146
|
+
const decoded = decodeJwsUnsafe(jwsToken);
|
|
147
|
+
if (!decoded) throw new Error("invalid JWS token");
|
|
148
|
+
const payload = decoded.payload;
|
|
149
|
+
const encoded = await dagCborCanonicalEncode(payload);
|
|
150
|
+
entries.push({
|
|
151
|
+
cid: encoded.cid.toString(),
|
|
152
|
+
jwsToken,
|
|
153
|
+
createdAt: payload.createdAt ?? ""
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
return entries;
|
|
157
|
+
};
|
|
158
|
+
var paginate = (operations, cursor, limit) => {
|
|
159
|
+
const reversed = [...operations].reverse();
|
|
160
|
+
let startIdx = 0;
|
|
161
|
+
if (cursor) {
|
|
162
|
+
const cursorIdx = reversed.findIndex((op) => op.cid === cursor);
|
|
163
|
+
if (cursorIdx >= 0) startIdx = cursorIdx + 1;
|
|
164
|
+
}
|
|
165
|
+
const page = reversed.slice(startIdx, startIdx + limit);
|
|
166
|
+
const hasMore = startIdx + limit < reversed.length;
|
|
167
|
+
return {
|
|
168
|
+
operations: page,
|
|
169
|
+
nextCursor: hasMore ? page[page.length - 1].cid : null
|
|
170
|
+
};
|
|
171
|
+
};
|
|
172
|
+
var paginationParams = (c) => ({
|
|
173
|
+
cursor: c.query("cursor"),
|
|
174
|
+
limit: Math.min(Math.max(parseInt(c.query("limit") ?? "25"), 1), 100)
|
|
175
|
+
});
|
|
176
|
+
var createKeyResolver = (store) => async (kid) => {
|
|
177
|
+
const hashIdx = kid.indexOf("#");
|
|
178
|
+
if (hashIdx < 0) throw new Error(`invalid kid format: ${kid}`);
|
|
179
|
+
const did = kid.substring(0, hashIdx);
|
|
180
|
+
const keyId = kid.substring(hashIdx + 1);
|
|
181
|
+
const identityChain = store.getIdentityChain(did);
|
|
182
|
+
if (!identityChain) throw new Error(`identity not found: ${did}`);
|
|
183
|
+
const identity = await verifyIdentityChain({
|
|
184
|
+
didPrefix: "did:dfos",
|
|
185
|
+
log: identityChain.operations.map((o) => o.jwsToken)
|
|
186
|
+
});
|
|
187
|
+
const allKeys = [...identity.authKeys, ...identity.assertKeys, ...identity.controllerKeys];
|
|
188
|
+
const key = allKeys.find((k) => k.id === keyId);
|
|
189
|
+
if (!key) throw new Error(`key not found: ${keyId}`);
|
|
190
|
+
return decodeMultikey(key.publicKeyMultibase).keyBytes;
|
|
191
|
+
};
|
|
192
|
+
var createRegistryServer = (store = new ChainStore()) => {
|
|
193
|
+
const app = new Hono();
|
|
194
|
+
const resolveKey = createKeyResolver(store);
|
|
195
|
+
app.post("/identities", async (c) => {
|
|
196
|
+
const body = await c.req.json();
|
|
197
|
+
if (!body.chain || !Array.isArray(body.chain) || body.chain.length === 0) {
|
|
198
|
+
return c.json(err("BAD_REQUEST", "chain must be a non-empty array of JWS tokens"), 400);
|
|
199
|
+
}
|
|
200
|
+
let verified;
|
|
201
|
+
try {
|
|
202
|
+
verified = await verifyIdentityChain({
|
|
203
|
+
didPrefix: "did:dfos",
|
|
204
|
+
log: body.chain
|
|
205
|
+
});
|
|
206
|
+
} catch (e) {
|
|
207
|
+
return c.json(err("BAD_REQUEST", `chain verification failed: ${e.message}`), 400);
|
|
208
|
+
}
|
|
209
|
+
const operations = await extractOperationEntries(body.chain);
|
|
210
|
+
const result = store.submitIdentityChain(verified.did, operations);
|
|
211
|
+
if (result === "conflict") {
|
|
212
|
+
return c.json(err("CONFLICT", "submitted chain conflicts with stored chain"), 409);
|
|
213
|
+
}
|
|
214
|
+
return c.json(
|
|
215
|
+
{
|
|
216
|
+
did: verified.did,
|
|
217
|
+
isDeleted: verified.isDeleted,
|
|
218
|
+
authKeys: verified.authKeys,
|
|
219
|
+
assertKeys: verified.assertKeys,
|
|
220
|
+
controllerKeys: verified.controllerKeys
|
|
221
|
+
},
|
|
222
|
+
result === "accepted" ? 201 : 200
|
|
223
|
+
);
|
|
224
|
+
});
|
|
225
|
+
app.get("/identities/:did", async (c) => {
|
|
226
|
+
const did = c.req.param("did");
|
|
227
|
+
const chain = store.getIdentityChain(did);
|
|
228
|
+
if (!chain) return c.json(err("NOT_FOUND", "identity not found"), 404);
|
|
229
|
+
const verified = await verifyIdentityChain({
|
|
230
|
+
didPrefix: "did:dfos",
|
|
231
|
+
log: chain.operations.map((o) => o.jwsToken)
|
|
232
|
+
});
|
|
233
|
+
return c.json({
|
|
234
|
+
did: verified.did,
|
|
235
|
+
isDeleted: verified.isDeleted,
|
|
236
|
+
authKeys: verified.authKeys,
|
|
237
|
+
assertKeys: verified.assertKeys,
|
|
238
|
+
controllerKeys: verified.controllerKeys
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
app.get("/identities/:did/operations", (c) => {
|
|
242
|
+
const did = c.req.param("did");
|
|
243
|
+
const chain = store.getIdentityChain(did);
|
|
244
|
+
if (!chain) return c.json(err("NOT_FOUND", "identity not found"), 404);
|
|
245
|
+
const { cursor, limit } = paginationParams(c.req);
|
|
246
|
+
return c.json(paginate(chain.operations, cursor, limit));
|
|
247
|
+
});
|
|
248
|
+
app.post("/entities", async (c) => {
|
|
249
|
+
const body = await c.req.json();
|
|
250
|
+
if (!body.chain || !Array.isArray(body.chain) || body.chain.length === 0) {
|
|
251
|
+
return c.json(err("BAD_REQUEST", "chain must be a non-empty array of JWS tokens"), 400);
|
|
252
|
+
}
|
|
253
|
+
const operations = await extractOperationEntries(body.chain);
|
|
254
|
+
let verified;
|
|
255
|
+
try {
|
|
256
|
+
verified = await verifyContentChain({ log: body.chain, resolveKey });
|
|
257
|
+
} catch (e) {
|
|
258
|
+
return c.json(err("BAD_REQUEST", `chain verification failed: ${e.message}`), 400);
|
|
259
|
+
}
|
|
260
|
+
const result = store.submitEntityChain(verified.entityId, operations);
|
|
261
|
+
if (result === "conflict") {
|
|
262
|
+
return c.json(err("CONFLICT", "submitted chain conflicts with stored chain"), 409);
|
|
263
|
+
}
|
|
264
|
+
return c.json(
|
|
265
|
+
{
|
|
266
|
+
entityId: verified.entityId,
|
|
267
|
+
isDeleted: verified.isDeleted,
|
|
268
|
+
currentDocumentCID: verified.currentDocumentCID,
|
|
269
|
+
genesisCID: verified.genesisCID,
|
|
270
|
+
headCID: verified.headCID
|
|
271
|
+
},
|
|
272
|
+
result === "accepted" ? 201 : 200
|
|
273
|
+
);
|
|
274
|
+
});
|
|
275
|
+
app.get("/entities/:entityId", (c) => {
|
|
276
|
+
const entityId = c.req.param("entityId");
|
|
277
|
+
const chain = store.getEntityChain(entityId);
|
|
278
|
+
if (!chain) return c.json(err("NOT_FOUND", "entity not found"), 404);
|
|
279
|
+
const genesis = chain.operations[0];
|
|
280
|
+
const head = chain.operations[chain.operations.length - 1];
|
|
281
|
+
const headDecoded = decodeJwsUnsafe(head.jwsToken);
|
|
282
|
+
const headPayload = headDecoded?.payload;
|
|
283
|
+
const headType = headPayload?.type;
|
|
284
|
+
return c.json({
|
|
285
|
+
entityId,
|
|
286
|
+
isDeleted: headType === "delete",
|
|
287
|
+
currentDocumentCID: headType === "delete" ? null : headPayload?.documentCID ?? null,
|
|
288
|
+
genesisCID: genesis.cid,
|
|
289
|
+
headCID: head.cid
|
|
290
|
+
});
|
|
291
|
+
});
|
|
292
|
+
app.get("/entities/:entityId/operations", (c) => {
|
|
293
|
+
const entityId = c.req.param("entityId");
|
|
294
|
+
const chain = store.getEntityChain(entityId);
|
|
295
|
+
if (!chain) return c.json(err("NOT_FOUND", "entity not found"), 404);
|
|
296
|
+
const { cursor, limit } = paginationParams(c.req);
|
|
297
|
+
return c.json(paginate(chain.operations, cursor, limit));
|
|
298
|
+
});
|
|
299
|
+
app.get("/operations/:cid", (c) => {
|
|
300
|
+
const cid = c.req.param("cid");
|
|
301
|
+
const op = store.getOperation(cid);
|
|
302
|
+
if (!op) return c.json(err("NOT_FOUND", "operation not found"), 404);
|
|
303
|
+
return c.json({ cid: op.cid, jwsToken: op.jwsToken });
|
|
304
|
+
});
|
|
305
|
+
app.get("/documents/:cid", (c) => {
|
|
306
|
+
const cid = c.req.param("cid");
|
|
307
|
+
const content = store.getDocument(cid);
|
|
308
|
+
if (content === void 0) return c.json(err("NOT_FOUND", "document not found"), 404);
|
|
309
|
+
return c.json({ cid, content });
|
|
310
|
+
});
|
|
311
|
+
return { app, store };
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
export {
|
|
315
|
+
SubmitIdentityChainRequest,
|
|
316
|
+
SubmitIdentityChainResponse,
|
|
317
|
+
ResolveIdentityResponse,
|
|
318
|
+
IdentityOperationsParams,
|
|
319
|
+
IdentityOperationsResponse,
|
|
320
|
+
SubmitContentChainRequest,
|
|
321
|
+
SubmitContentChainResponse,
|
|
322
|
+
ResolveEntityResponse,
|
|
323
|
+
EntityOperationsParams,
|
|
324
|
+
EntityOperationsResponse,
|
|
325
|
+
ResolveOperationResponse,
|
|
326
|
+
ResolveDocumentResponse,
|
|
327
|
+
RegistryError,
|
|
328
|
+
ChainStore,
|
|
329
|
+
createRegistryServer
|
|
330
|
+
};
|