@atcute/did-plc 0.1.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/LICENSE +17 -0
- package/README.md +15 -0
- package/dist/constants.d.ts +1 -0
- package/dist/constants.js +5 -0
- package/dist/constants.js.map +1 -0
- package/dist/data.d.ts +32 -0
- package/dist/data.js +183 -0
- package/dist/data.js.map +1 -0
- package/dist/errors.d.ts +33 -0
- package/dist/errors.js +53 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/typedefs.d.ts +17 -0
- package/dist/typedefs.js +162 -0
- package/dist/typedefs.js.map +1 -0
- package/dist/types.d.ts +54 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/utils.d.ts +5 -0
- package/dist/utils.js +51 -0
- package/dist/utils.js.map +1 -0
- package/lib/constants.ts +5 -0
- package/lib/data.ts +245 -0
- package/lib/errors.ts +57 -0
- package/lib/index.ts +7 -0
- package/lib/typedefs.ts +201 -0
- package/lib/types.ts +71 -0
- package/lib/utils.ts +66 -0
- package/package.json +42 -0
package/dist/utils.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import * as CBOR from '@atcute/cbor';
|
|
2
|
+
import { verifySigWithDidKey } from '@atcute/crypto';
|
|
3
|
+
import { fromBase64Url } from '@atcute/multibase';
|
|
4
|
+
import * as t from './types.js';
|
|
5
|
+
export const wrapHttpPrefix = (str) => {
|
|
6
|
+
if (str.startsWith('http://') || str.startsWith('https://')) {
|
|
7
|
+
return str;
|
|
8
|
+
}
|
|
9
|
+
return `https://${str}`;
|
|
10
|
+
};
|
|
11
|
+
export const wrapAtprotoPrefix = (str) => {
|
|
12
|
+
if (str.startsWith('at://')) {
|
|
13
|
+
return str;
|
|
14
|
+
}
|
|
15
|
+
const stripped = str.replace('http://', '').replace('https://', '');
|
|
16
|
+
return `at://${stripped}`;
|
|
17
|
+
};
|
|
18
|
+
export const normalizeOp = (op) => {
|
|
19
|
+
if (op.type === 'create') {
|
|
20
|
+
return {
|
|
21
|
+
type: 'plc_operation',
|
|
22
|
+
prev: op.prev,
|
|
23
|
+
sig: op.sig,
|
|
24
|
+
rotationKeys: [op.recoveryKey, op.signingKey],
|
|
25
|
+
verificationMethods: {
|
|
26
|
+
atproto: op.signingKey,
|
|
27
|
+
},
|
|
28
|
+
alsoKnownAs: [wrapAtprotoPrefix(op.handle)],
|
|
29
|
+
services: {
|
|
30
|
+
atproto_pds: {
|
|
31
|
+
type: 'AtprotoPersonalDataServer',
|
|
32
|
+
endpoint: wrapHttpPrefix(op.service),
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
return op;
|
|
38
|
+
};
|
|
39
|
+
export const isSignedOperationValid = async (allowedKeys, op) => {
|
|
40
|
+
const { sig, ...unsignedOp } = op;
|
|
41
|
+
const sigBytes = fromBase64Url(sig);
|
|
42
|
+
const opBytes = CBOR.encode(unsignedOp);
|
|
43
|
+
for (const key of allowedKeys) {
|
|
44
|
+
const ok = await verifySigWithDidKey(key, sigBytes, opBytes);
|
|
45
|
+
if (ok) {
|
|
46
|
+
return key;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
};
|
|
51
|
+
//# sourceMappingURL=utils.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils.js","sourceRoot":"","sources":["../lib/utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,IAAI,MAAM,cAAc,CAAC;AACrC,OAAO,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AACrD,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAElD,OAAO,KAAK,CAAC,MAAM,YAAY,CAAC;AAEhC,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,GAAW,EAAU,EAAE;IACrD,IAAI,GAAG,CAAC,UAAU,CAAC,SAAS,CAAC,IAAI,GAAG,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;QAC7D,OAAO,GAAG,CAAC;IACZ,CAAC;IAED,OAAO,WAAW,GAAG,EAAE,CAAC;AACzB,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,GAAW,EAAU,EAAE;IACxD,IAAI,GAAG,CAAC,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC;QAC7B,OAAO,GAAG,CAAC;IACZ,CAAC;IAED,MAAM,QAAQ,GAAG,GAAG,CAAC,OAAO,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC;IAEpE,OAAO,QAAQ,QAAQ,EAAE,CAAC;AAC3B,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,EAAyB,EAAe,EAAE;IACrE,IAAI,EAAE,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;QAC1B,OAAO;YACN,IAAI,EAAE,eAAe;YACrB,IAAI,EAAE,EAAE,CAAC,IAAI;YACb,GAAG,EAAE,EAAE,CAAC,GAAG;YACX,YAAY,EAAE,CAAC,EAAE,CAAC,WAAW,EAAE,EAAE,CAAC,UAAU,CAAC;YAC7C,mBAAmB,EAAE;gBACpB,OAAO,EAAE,EAAE,CAAC,UAAU;aACtB;YACD,WAAW,EAAE,CAAC,iBAAiB,CAAC,EAAE,CAAC,MAAM,CAAC,CAAC;YAC3C,QAAQ,EAAE;gBACT,WAAW,EAAE;oBACZ,IAAI,EAAE,2BAA2B;oBACjC,QAAQ,EAAE,cAAc,CAAC,EAAE,CAAC,OAAO,CAAC;iBACpC;aACD;SACD,CAAC;IACH,CAAC;IAED,OAAO,EAAE,CAAC;AACX,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,sBAAsB,GAAG,KAAK,EAC1C,WAA6B,EAC7B,EAAoC,EACH,EAAE;IACnC,MAAM,EAAE,GAAG,EAAE,GAAG,UAAU,EAAE,GAAG,EAAE,CAAC;IAElC,MAAM,QAAQ,GAAG,aAAa,CAAC,GAAG,CAAC,CAAC;IACpC,MAAM,OAAO,GAAG,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;IAExC,KAAK,MAAM,GAAG,IAAI,WAAW,EAAE,CAAC;QAC/B,MAAM,EAAE,GAAG,MAAM,mBAAmB,CAAC,GAAG,EAAE,QAAQ,EAAE,OAAO,CAAC,CAAC;QAE7D,IAAI,EAAE,EAAE,CAAC;YACR,OAAO,GAAG,CAAC;QACZ,CAAC;IACF,CAAC;IAED,OAAO,IAAI,CAAC;AACb,CAAC,CAAC"}
|
package/lib/constants.ts
ADDED
package/lib/data.ts
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import * as CBOR from '@atcute/cbor';
|
|
2
|
+
import * as CID from '@atcute/cid';
|
|
3
|
+
import { toBase32 } from '@atcute/multibase';
|
|
4
|
+
import { toSha256 } from '@atcute/uint8array';
|
|
5
|
+
|
|
6
|
+
import { DISPUTE_WINDOW } from './constants.js';
|
|
7
|
+
import * as err from './errors.js';
|
|
8
|
+
import * as t from './types.js';
|
|
9
|
+
import { isSignedOperationValid, normalizeOp } from './utils.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Process an indexed entry by validating it and integrating it into the canonical log.
|
|
13
|
+
*/
|
|
14
|
+
export const processIndexedEntry = async (
|
|
15
|
+
did: t.DidPlcString,
|
|
16
|
+
canonical: t.IndexedEntryWithSigner[],
|
|
17
|
+
proposed: t.IndexedEntry,
|
|
18
|
+
): Promise<{
|
|
19
|
+
prev: string | null;
|
|
20
|
+
ops: t.IndexedEntryWithSigner[];
|
|
21
|
+
nullified: t.IndexedEntryWithSigner[];
|
|
22
|
+
}> => {
|
|
23
|
+
if (canonical.length === 0) {
|
|
24
|
+
if (proposed.operation.type === 'plc_tombstone') {
|
|
25
|
+
throw new err.ImproperOperationError(proposed, `expected genesis op to not be tombstone`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (proposed.operation.prev !== null) {
|
|
29
|
+
throw new err.ImproperOperationError(proposed, `expected null prev on genesis op`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Check if CID and DID matches
|
|
33
|
+
{
|
|
34
|
+
const opBytes = CBOR.encode(proposed.operation);
|
|
35
|
+
|
|
36
|
+
const expectedDid = `did:plc:${toBase32(await toSha256(opBytes)).slice(0, 24)}`;
|
|
37
|
+
if (expectedDid !== did) {
|
|
38
|
+
throw new err.GenesisHashError(proposed, did);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const expectedCid = CID.toString(await CID.create(CID.CODEC_DCBOR, opBytes));
|
|
42
|
+
if (expectedCid !== proposed.cid) {
|
|
43
|
+
throw new err.InvalidHashError(proposed, expectedCid);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Check if signature is valid
|
|
48
|
+
let allowedSigners: t.DidKeyString[];
|
|
49
|
+
let signedBy: t.DidKeyString;
|
|
50
|
+
|
|
51
|
+
{
|
|
52
|
+
const { rotationKeys } = normalizeOp(proposed.operation);
|
|
53
|
+
const ok = await isSignedOperationValid(rotationKeys, proposed.operation);
|
|
54
|
+
|
|
55
|
+
if (!ok) {
|
|
56
|
+
throw new err.InvalidSignatureError(proposed);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
allowedSigners = rotationKeys;
|
|
60
|
+
signedBy = ok;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
prev: null,
|
|
65
|
+
nullified: [],
|
|
66
|
+
ops: [{ ...proposed, allowedSigners, signedBy }],
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Grab the previous reference
|
|
71
|
+
const proposedPrev = proposed.operation.prev;
|
|
72
|
+
if (!proposedPrev) {
|
|
73
|
+
throw new err.ImproperOperationError(proposed, `expected prev op`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const indexOfPrev = canonical.findIndex((op) => op.cid === proposedPrev);
|
|
77
|
+
if (indexOfPrev === -1) {
|
|
78
|
+
throw new err.ImproperOperationError(proposed, `prev op not in history`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Check if the CID matches
|
|
82
|
+
{
|
|
83
|
+
const opBytes = CBOR.encode(proposed.operation);
|
|
84
|
+
const expectedCid = CID.toString(await CID.create(CID.CODEC_DCBOR, opBytes));
|
|
85
|
+
if (expectedCid !== proposed.cid) {
|
|
86
|
+
throw new err.InvalidHashError(proposed, expectedCid);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Get the proposed canonical history
|
|
91
|
+
const alteredHistory = canonical.slice(0, indexOfPrev + 1);
|
|
92
|
+
|
|
93
|
+
const nullified = canonical.slice(indexOfPrev + 1);
|
|
94
|
+
const lastOp = alteredHistory.at(-1);
|
|
95
|
+
|
|
96
|
+
if (!lastOp) {
|
|
97
|
+
throw new err.ImproperOperationError(proposed, `missing last op`);
|
|
98
|
+
}
|
|
99
|
+
if (lastOp.operation.type === 'plc_tombstone') {
|
|
100
|
+
throw new err.ImproperOperationError(proposed, `did is tombstoned`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const lastOpNormalized = normalizeOp(lastOp.operation);
|
|
104
|
+
const firstNullified = nullified[0];
|
|
105
|
+
|
|
106
|
+
// We're not nullifying, check if the signature is valid and move on
|
|
107
|
+
if (!firstNullified) {
|
|
108
|
+
const allowedSigners = lastOpNormalized.rotationKeys;
|
|
109
|
+
const signedBy = await isSignedOperationValid(allowedSigners, proposed.operation);
|
|
110
|
+
if (!signedBy) {
|
|
111
|
+
throw new err.InvalidSignatureError(proposed);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
prev: proposedPrev,
|
|
116
|
+
nullified: [],
|
|
117
|
+
ops: [...canonical, { ...proposed, allowedSigners, signedBy }],
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// The indexed log should say that all of the nullified has `nullified: true`
|
|
122
|
+
// for (let idx = 0, len = nullified.length; idx < len; idx++) {
|
|
123
|
+
// const op = nullified[idx];
|
|
124
|
+
|
|
125
|
+
// if (!op.nullified) {
|
|
126
|
+
// throw new err.ImproperOperationError(op, `expected nullified prop to be true`);
|
|
127
|
+
// }
|
|
128
|
+
// }
|
|
129
|
+
|
|
130
|
+
// Check if operation within the recovery window
|
|
131
|
+
{
|
|
132
|
+
const lapsed = new Date(proposed.createdAt).getTime() - new Date(firstNullified.createdAt).getTime();
|
|
133
|
+
|
|
134
|
+
if (lapsed > DISPUTE_WINDOW) {
|
|
135
|
+
throw new err.LateDisputeError(proposed, lapsed);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Check if the dispute is valid
|
|
140
|
+
{
|
|
141
|
+
let allowedSigners: t.DidKeyString[];
|
|
142
|
+
let signedBy: t.DidKeyString;
|
|
143
|
+
|
|
144
|
+
{
|
|
145
|
+
const disputedSigner = firstNullified.signedBy;
|
|
146
|
+
|
|
147
|
+
const indexOfSigner = lastOpNormalized.rotationKeys.indexOf(disputedSigner);
|
|
148
|
+
const morePowerfulKeys = lastOpNormalized.rotationKeys.slice(0, indexOfSigner);
|
|
149
|
+
|
|
150
|
+
const ok = await isSignedOperationValid(morePowerfulKeys, proposed.operation);
|
|
151
|
+
if (!ok) {
|
|
152
|
+
throw new err.InvalidSignatureError(proposed);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
allowedSigners = morePowerfulKeys;
|
|
156
|
+
signedBy = ok;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
prev: proposedPrev,
|
|
161
|
+
nullified: nullified,
|
|
162
|
+
ops: [...alteredHistory, { ...proposed, allowedSigners, signedBy }],
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Process an indexed entry log by sequentially processing each operation.
|
|
169
|
+
*/
|
|
170
|
+
export const processIndexedEntryLog = async (
|
|
171
|
+
did: t.DidPlcString,
|
|
172
|
+
ops: t.IndexedEntryLog,
|
|
173
|
+
): Promise<{ canonical: t.IndexedEntryWithSigner[]; nullified: t.IndexedEntryWithSigner[] }> => {
|
|
174
|
+
let nullified: t.IndexedEntryWithSigner[] = [];
|
|
175
|
+
let canonical: t.IndexedEntryWithSigner[] = [];
|
|
176
|
+
|
|
177
|
+
for (const operation of ops) {
|
|
178
|
+
const result = await processIndexedEntry(did, canonical, operation);
|
|
179
|
+
canonical = result.ops;
|
|
180
|
+
|
|
181
|
+
if (result.nullified.length > 0) {
|
|
182
|
+
nullified = nullified.concat(result.nullified);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return { canonical, nullified };
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Check whether an operation can still be disputed
|
|
191
|
+
*/
|
|
192
|
+
export const isDisputePeriodActive = (disputed: t.IndexedEntry, now = Date.now()): boolean => {
|
|
193
|
+
const lapsed = now - new Date(disputed.createdAt).getTime();
|
|
194
|
+
return lapsed <= DISPUTE_WINDOW;
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Check if a key is authorized to dispute an operation
|
|
199
|
+
*/
|
|
200
|
+
export const isAuthorizedForDispute = (
|
|
201
|
+
base: t.IndexedEntryWithSigner<t.CompatibleOperation>,
|
|
202
|
+
disputed: t.IndexedEntryWithSigner,
|
|
203
|
+
key: t.DidKeyString,
|
|
204
|
+
): boolean => {
|
|
205
|
+
const { rotationKeys } = normalizeOp(base.operation);
|
|
206
|
+
|
|
207
|
+
const disputedSigner = disputed.signedBy;
|
|
208
|
+
const disputedIndex = rotationKeys.indexOf(disputedSigner);
|
|
209
|
+
if (disputedIndex === -1) {
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const didKeyIndex = rotationKeys.indexOf(key);
|
|
214
|
+
return didKeyIndex !== -1 && didKeyIndex < disputedIndex;
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
export interface DisputeCandidate {
|
|
218
|
+
base: t.IndexedEntryWithSigner<t.CompatibleOperation>;
|
|
219
|
+
disputed: t.IndexedEntryWithSigner;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Finds operations that can be disputed by a given key
|
|
224
|
+
*/
|
|
225
|
+
export const getDisputeCandidates = (canonical: t.IndexedEntryWithSigner[], key: t.DidKeyString) => {
|
|
226
|
+
const candidates: DisputeCandidate[] = [];
|
|
227
|
+
const now = Date.now();
|
|
228
|
+
|
|
229
|
+
for (let idx = 1, len = canonical.length; idx < len; idx++) {
|
|
230
|
+
const base = canonical[idx - 1] as t.IndexedEntryWithSigner<t.CompatibleOperation>;
|
|
231
|
+
const disputed = canonical[idx];
|
|
232
|
+
|
|
233
|
+
// Only consider if it's still within the recovery window.
|
|
234
|
+
if (!isDisputePeriodActive(disputed, now)) {
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Check if the provided key is allowed to dispute this operation.
|
|
239
|
+
if (isAuthorizedForDispute(base, disputed, key)) {
|
|
240
|
+
candidates.push({ base, disputed });
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return candidates;
|
|
245
|
+
};
|
package/lib/errors.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import * as t from './types.js';
|
|
2
|
+
|
|
3
|
+
export class PlcError extends Error {
|
|
4
|
+
override name = 'PlcError';
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export class ImproperOperationError extends PlcError {
|
|
8
|
+
override name = 'ImproperOperationError';
|
|
9
|
+
|
|
10
|
+
constructor(
|
|
11
|
+
public operation: t.IndexedEntry,
|
|
12
|
+
public reason: string,
|
|
13
|
+
) {
|
|
14
|
+
super(`improper operation; cid=${operation.cid}; reason=${reason}`);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class InvalidSignatureError extends PlcError {
|
|
19
|
+
override name = 'InvalidSignatureError';
|
|
20
|
+
|
|
21
|
+
constructor(public operation: t.IndexedEntry) {
|
|
22
|
+
super(`invalid signature; cid=${operation.cid}`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export class InvalidHashError extends PlcError {
|
|
27
|
+
override name = 'InvalidHashError';
|
|
28
|
+
|
|
29
|
+
constructor(
|
|
30
|
+
public operation: t.IndexedEntry,
|
|
31
|
+
public expected: string,
|
|
32
|
+
) {
|
|
33
|
+
super(`invalid hash; expected=${expected}; got=${operation.cid}`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export class GenesisHashError extends PlcError {
|
|
38
|
+
override name = 'GenesisHashError';
|
|
39
|
+
|
|
40
|
+
constructor(
|
|
41
|
+
public operation: t.IndexedEntry,
|
|
42
|
+
public did: t.DidPlcString,
|
|
43
|
+
) {
|
|
44
|
+
super(`mismatching genesis hash; did=${did}; cid=${operation.cid}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export class LateDisputeError extends PlcError {
|
|
49
|
+
override name = 'LateDisputeError';
|
|
50
|
+
|
|
51
|
+
constructor(
|
|
52
|
+
public operation: t.IndexedEntry,
|
|
53
|
+
public lapsed: number,
|
|
54
|
+
) {
|
|
55
|
+
super(`dispute occured outside of permitted window; cid=${operation.cid}; lapsed=${lapsed}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
package/lib/index.ts
ADDED
package/lib/typedefs.ts
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import * as v from '@badrap/valita';
|
|
2
|
+
|
|
3
|
+
import * as CID from '@atcute/cid';
|
|
4
|
+
import { parseDidKey } from '@atcute/crypto';
|
|
5
|
+
|
|
6
|
+
import * as t from './types.js';
|
|
7
|
+
|
|
8
|
+
// #region Strings
|
|
9
|
+
const DID_PLC_RE = /^did:plc:([a-z2-7]{24})$/;
|
|
10
|
+
|
|
11
|
+
export const didPlcString = v
|
|
12
|
+
.string()
|
|
13
|
+
.assert((input): input is t.DidPlcString => DID_PLC_RE.test(input), `must be a did:plc`);
|
|
14
|
+
|
|
15
|
+
export const didKeyString = v.string().chain((input) => {
|
|
16
|
+
try {
|
|
17
|
+
parseDidKey(input);
|
|
18
|
+
} catch (err) {
|
|
19
|
+
if (err instanceof SyntaxError) {
|
|
20
|
+
return v.err(`did:key can't be parsed`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return v.err(`invalid did:key`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return v.ok(input as t.DidKeyString);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const cidString = v.string().chain((input) => {
|
|
30
|
+
try {
|
|
31
|
+
CID.fromString(input);
|
|
32
|
+
} catch {
|
|
33
|
+
return v.err(`invalid cid`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return v.ok(input);
|
|
37
|
+
});
|
|
38
|
+
// #endregion
|
|
39
|
+
|
|
40
|
+
// #region create
|
|
41
|
+
const _unsignedLegacyCreateOperation = v.object({
|
|
42
|
+
type: v.literal('create'),
|
|
43
|
+
prev: v.null(),
|
|
44
|
+
signingKey: didKeyString,
|
|
45
|
+
recoveryKey: didKeyString,
|
|
46
|
+
handle: v.string().assert((input) => input.length <= 256, `handle too long (max 256 characters)`),
|
|
47
|
+
service: v
|
|
48
|
+
.string()
|
|
49
|
+
.assert((input) => input.length <= 512, `service endpoint too long (max 512 characters)`),
|
|
50
|
+
}) satisfies v.Type<t.UnsignedLegacyCreateOperation>;
|
|
51
|
+
|
|
52
|
+
export const unsignedLegacyCreateOperation: v.Type<t.UnsignedLegacyCreateOperation> =
|
|
53
|
+
_unsignedLegacyCreateOperation;
|
|
54
|
+
|
|
55
|
+
export const legacyCreateOperation: v.Type<t.LegacyCreateOperation> = _unsignedLegacyCreateOperation.extend({
|
|
56
|
+
sig: v.string(),
|
|
57
|
+
});
|
|
58
|
+
// #endregion
|
|
59
|
+
|
|
60
|
+
// #region plc_operation
|
|
61
|
+
export const service: v.Type<t.Service> = v.object({
|
|
62
|
+
type: v.string().assert((input) => input.length <= 256, `service type too long (max 256 characters)`),
|
|
63
|
+
endpoint: v
|
|
64
|
+
.string()
|
|
65
|
+
.assert((input) => input.length <= 512, `service endpoint too long (max 512 characters)`),
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const _unsignedOperation = v.object({
|
|
69
|
+
type: v.literal('plc_operation'),
|
|
70
|
+
prev: v.string().nullable(),
|
|
71
|
+
rotationKeys: v.array(didKeyString).chain((input) => {
|
|
72
|
+
const length = input.length;
|
|
73
|
+
|
|
74
|
+
if (length === 0) {
|
|
75
|
+
return v.err(`missing rotation keys`);
|
|
76
|
+
} else if (length > 10) {
|
|
77
|
+
return v.err(`too many rotation keys (max 10 keys)`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
for (let i = 0; i < length; i++) {
|
|
81
|
+
const key = input[i];
|
|
82
|
+
|
|
83
|
+
for (let j = 0; j < i; j++) {
|
|
84
|
+
if (input[j] === key) {
|
|
85
|
+
return v.err({
|
|
86
|
+
message: `duplicate "${key}" rotation key`,
|
|
87
|
+
path: [i],
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return v.ok(input);
|
|
94
|
+
}),
|
|
95
|
+
verificationMethods: v.record(didKeyString).chain((input) => {
|
|
96
|
+
for (const id in input) {
|
|
97
|
+
if (id.length > 32) {
|
|
98
|
+
return v.err({
|
|
99
|
+
message: `verification method id too long (max 32 characters)`,
|
|
100
|
+
path: [id],
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return v.ok(input);
|
|
106
|
+
}),
|
|
107
|
+
alsoKnownAs: v
|
|
108
|
+
.array(v.string().assert((input) => input.length <= 256, `aka entry too long (max 256 characters)`))
|
|
109
|
+
.chain((input) => {
|
|
110
|
+
const length = input.length;
|
|
111
|
+
|
|
112
|
+
if (length > 10) {
|
|
113
|
+
return v.err(`too many aka entries (max 10)`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
for (let i = 0; i < length; i++) {
|
|
117
|
+
const aka = input[i];
|
|
118
|
+
|
|
119
|
+
for (let j = 0; j < i; j++) {
|
|
120
|
+
if (input[j] === aka) {
|
|
121
|
+
return v.err({
|
|
122
|
+
message: `duplicate "${aka}" aka entry`,
|
|
123
|
+
path: [i],
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return v.ok(input);
|
|
130
|
+
}),
|
|
131
|
+
services: v.record(service).chain((input) => {
|
|
132
|
+
const length = Object.keys(input).length;
|
|
133
|
+
|
|
134
|
+
if (length > 10) {
|
|
135
|
+
return v.err(`too many service entries (max 10)`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
for (const id in input) {
|
|
139
|
+
if (id.length > 32) {
|
|
140
|
+
return v.err({
|
|
141
|
+
message: `service id too long (max 32 characters)`,
|
|
142
|
+
path: [id],
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return v.ok(input);
|
|
148
|
+
}),
|
|
149
|
+
}) satisfies v.Type<t.UnsignedOperation>;
|
|
150
|
+
|
|
151
|
+
export const unsignedOperation: v.Type<t.UnsignedOperation> = _unsignedOperation;
|
|
152
|
+
|
|
153
|
+
export const operation: v.Type<t.Operation> = _unsignedOperation.extend({
|
|
154
|
+
sig: v.string(),
|
|
155
|
+
});
|
|
156
|
+
// #endregion
|
|
157
|
+
|
|
158
|
+
// #region plc_tombstone
|
|
159
|
+
const _unsignedTombstone = v.object({
|
|
160
|
+
type: v.literal('plc_tombstone'),
|
|
161
|
+
prev: v.string(),
|
|
162
|
+
}) satisfies v.Type<t.UnsignedTombstone>;
|
|
163
|
+
|
|
164
|
+
export const unsignedTombstone: v.Type<t.UnsignedTombstone> = _unsignedTombstone;
|
|
165
|
+
|
|
166
|
+
export const tombstone: v.Type<t.Tombstone> = _unsignedTombstone.extend({
|
|
167
|
+
sig: v.string(),
|
|
168
|
+
});
|
|
169
|
+
// #endregion
|
|
170
|
+
|
|
171
|
+
// #region Entry
|
|
172
|
+
export const compatibleOperation: v.Type<t.CompatibleOperation> = v.union(operation, legacyCreateOperation);
|
|
173
|
+
|
|
174
|
+
export const compatibleOperationOrTombstone: v.Type<t.CompatibleOperationOrTombstone> = v.union(
|
|
175
|
+
operation,
|
|
176
|
+
legacyCreateOperation,
|
|
177
|
+
tombstone,
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
export const operationOrTombstone: v.Type<t.OperationOrTombstone> = v.union(operation, tombstone);
|
|
181
|
+
|
|
182
|
+
export const operationLog: v.Type<t.OperationLog> = v
|
|
183
|
+
.tuple([compatibleOperation])
|
|
184
|
+
.concat(v.array(operationOrTombstone));
|
|
185
|
+
// #endregion
|
|
186
|
+
|
|
187
|
+
// #region Indexed entry
|
|
188
|
+
const _indexedEntry = v.object({
|
|
189
|
+
did: didPlcString,
|
|
190
|
+
operation: compatibleOperationOrTombstone,
|
|
191
|
+
cid: cidString,
|
|
192
|
+
nullified: v.boolean(),
|
|
193
|
+
createdAt: v.string().assert((input) => !Number.isNaN(new Date(input).getTime()), `invalid timestamp`),
|
|
194
|
+
}) satisfies v.Type<t.IndexedEntry>;
|
|
195
|
+
|
|
196
|
+
export const indexedEntry: v.Type<t.IndexedEntry> = _indexedEntry;
|
|
197
|
+
|
|
198
|
+
export const indexedEntryLog: v.Type<t.IndexedEntryLog> = v
|
|
199
|
+
.tuple([_indexedEntry.extend({ operation: compatibleOperation })])
|
|
200
|
+
.concat(v.array(_indexedEntry.extend({ operation: operationOrTombstone })));
|
|
201
|
+
// #endregion
|
package/lib/types.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
export type DidPlcString = `did:plc:${string}`;
|
|
2
|
+
|
|
3
|
+
export type DidKeyString = `did:key:${string}`;
|
|
4
|
+
|
|
5
|
+
export interface UnsignedLegacyCreateOperation {
|
|
6
|
+
type: 'create';
|
|
7
|
+
prev: null;
|
|
8
|
+
signingKey: DidKeyString;
|
|
9
|
+
recoveryKey: DidKeyString;
|
|
10
|
+
handle: string;
|
|
11
|
+
service: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface LegacyCreateOperation extends UnsignedLegacyCreateOperation {
|
|
15
|
+
sig: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface Service {
|
|
19
|
+
type: string;
|
|
20
|
+
endpoint: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface UnsignedOperation {
|
|
24
|
+
type: 'plc_operation';
|
|
25
|
+
prev: string | null;
|
|
26
|
+
alsoKnownAs: string[];
|
|
27
|
+
rotationKeys: DidKeyString[];
|
|
28
|
+
verificationMethods: Record<string, DidKeyString>;
|
|
29
|
+
services: Record<string, Service>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface Operation extends UnsignedOperation {
|
|
33
|
+
sig: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface UnsignedTombstone {
|
|
37
|
+
type: 'plc_tombstone';
|
|
38
|
+
prev: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface Tombstone extends UnsignedTombstone {
|
|
42
|
+
sig: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export type CompatibleOperation = Operation | LegacyCreateOperation;
|
|
46
|
+
|
|
47
|
+
export type CompatibleOperationOrTombstone = CompatibleOperation | Tombstone;
|
|
48
|
+
|
|
49
|
+
export type OperationOrTombstone = Operation | Tombstone;
|
|
50
|
+
|
|
51
|
+
export type OperationLog = [genesis: CompatibleOperation, ...OperationOrTombstone[]];
|
|
52
|
+
|
|
53
|
+
export interface IndexedEntry<T extends CompatibleOperationOrTombstone = CompatibleOperationOrTombstone> {
|
|
54
|
+
did: DidPlcString;
|
|
55
|
+
operation: T;
|
|
56
|
+
cid: string;
|
|
57
|
+
nullified: boolean;
|
|
58
|
+
createdAt: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface IndexedEntryWithSigner<
|
|
62
|
+
T extends CompatibleOperationOrTombstone = CompatibleOperationOrTombstone,
|
|
63
|
+
> extends IndexedEntry<T> {
|
|
64
|
+
allowedSigners: DidKeyString[];
|
|
65
|
+
signedBy: DidKeyString;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export type IndexedEntryLog = [
|
|
69
|
+
genesis: IndexedEntry<CompatibleOperation>,
|
|
70
|
+
...IndexedEntry<OperationOrTombstone>[],
|
|
71
|
+
];
|