@atcute/repo 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 +14 -0
- package/README.md +58 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/reader.d.ts +3 -0
- package/dist/reader.d.ts.map +1 -0
- package/dist/reader.js +67 -0
- package/dist/reader.js.map +1 -0
- package/dist/streamed-reader.d.ts +25 -0
- package/dist/streamed-reader.d.ts.map +1 -0
- package/dist/streamed-reader.js +143 -0
- package/dist/streamed-reader.js.map +1 -0
- package/dist/types.d.ts +37 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +54 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/queue.d.ts +42 -0
- package/dist/utils/queue.d.ts.map +1 -0
- package/dist/utils/queue.js +121 -0
- package/dist/utils/queue.js.map +1 -0
- package/dist/utils.d.ts +4 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +6 -0
- package/dist/utils.js.map +1 -0
- package/dist/verify.d.ts +17 -0
- package/dist/verify.d.ts.map +1 -0
- package/dist/verify.js +166 -0
- package/dist/verify.js.map +1 -0
- package/lib/index.ts +5 -0
- package/lib/reader.ts +98 -0
- package/lib/streamed-reader.ts +208 -0
- package/lib/types.ts +64 -0
- package/lib/utils/queue.ts +148 -0
- package/lib/utils.ts +7 -0
- package/lib/verify.ts +244 -0
- package/package.json +44 -0
package/dist/verify.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { PublicKey } from '@atcute/crypto';
|
|
2
|
+
import type { AtprotoDid } from '@atcute/lexicons/syntax';
|
|
3
|
+
export interface VerifiedRecord {
|
|
4
|
+
/** CID of the record */
|
|
5
|
+
cid: string;
|
|
6
|
+
/** Record data */
|
|
7
|
+
record: unknown;
|
|
8
|
+
}
|
|
9
|
+
export interface VerifyRecordOptions {
|
|
10
|
+
did?: AtprotoDid;
|
|
11
|
+
collection: string;
|
|
12
|
+
rkey: string;
|
|
13
|
+
publicKey?: PublicKey;
|
|
14
|
+
carBytes: Uint8Array;
|
|
15
|
+
}
|
|
16
|
+
export declare const verifyRecord: ({ did, collection, rkey, publicKey, carBytes, }: VerifyRecordOptions) => Promise<VerifiedRecord>;
|
|
17
|
+
//# sourceMappingURL=verify.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"verify.d.ts","sourceRoot":"","sources":["../lib/verify.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AAQ1D,MAAM,WAAW,cAAc;IAC9B,wBAAwB;IACxB,GAAG,EAAE,MAAM,CAAC;IACZ,kBAAkB;IAClB,MAAM,EAAE,OAAO,CAAC;CAChB;AAED,MAAM,WAAW,mBAAmB;IACnC,GAAG,CAAC,EAAE,UAAU,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB,QAAQ,EAAE,UAAU,CAAC;CACrB;AAED,eAAO,MAAM,YAAY,GAAU,iDAMhC,mBAAmB,KAAG,OAAO,CAAC,cAAc,CA6D9C,CAAC"}
|
package/dist/verify.js
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import * as CAR from '@atcute/car';
|
|
2
|
+
import * as CBOR from '@atcute/cbor';
|
|
3
|
+
import * as CID from '@atcute/cid';
|
|
4
|
+
import { isNodeData } from '@atcute/mst';
|
|
5
|
+
import { decodeUtf8From, encodeUtf8, toSha256 } from '@atcute/uint8array';
|
|
6
|
+
import { isCommit } from './types.js';
|
|
7
|
+
export const verifyRecord = async ({ did, collection, rkey, publicKey, carBytes, }) => {
|
|
8
|
+
// read the car
|
|
9
|
+
let blockmap;
|
|
10
|
+
let commit;
|
|
11
|
+
{
|
|
12
|
+
const reader = CAR.fromUint8Array(carBytes);
|
|
13
|
+
if (reader.header.data.roots.length !== 1) {
|
|
14
|
+
throw new Error(`car must have exactly one root`);
|
|
15
|
+
}
|
|
16
|
+
blockmap = new Map();
|
|
17
|
+
for (const entry of reader) {
|
|
18
|
+
const cidString = CID.toString(entry.cid);
|
|
19
|
+
// Verify that `bytes` matches its associated CID
|
|
20
|
+
const expectedCid = CID.toString(await CID.create(entry.cid.codec, entry.bytes));
|
|
21
|
+
if (cidString !== expectedCid) {
|
|
22
|
+
throw new Error(`cid does not match bytes`);
|
|
23
|
+
}
|
|
24
|
+
blockmap.set(cidString, entry.bytes);
|
|
25
|
+
}
|
|
26
|
+
if (blockmap.size === 0) {
|
|
27
|
+
throw new Error(`car must have at least one block`);
|
|
28
|
+
}
|
|
29
|
+
commit = readBlock(blockmap, reader.header.data.roots[0].$link, isCommit);
|
|
30
|
+
}
|
|
31
|
+
// verify did in commit matches the did
|
|
32
|
+
if (did !== undefined && commit.did !== did) {
|
|
33
|
+
throw new Error(`did in commit does not match expected did`);
|
|
34
|
+
}
|
|
35
|
+
// verify signature contained in commit is valid (if publicKey provided)
|
|
36
|
+
if (publicKey) {
|
|
37
|
+
const { sig, ...unsigned } = commit;
|
|
38
|
+
const data = CBOR.encode(unsigned);
|
|
39
|
+
const valid = await publicKey.verify(CBOR.fromBytes(sig), data);
|
|
40
|
+
if (!valid) {
|
|
41
|
+
throw new Error(`signature verification failed`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
// find and verify the record in the commit
|
|
45
|
+
const targetKey = `${collection}/${rkey}`;
|
|
46
|
+
const { found } = await dfs(blockmap, commit.data.$link, targetKey);
|
|
47
|
+
if (!found) {
|
|
48
|
+
throw new Error(`could not find record in car`);
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
cid: found.cid,
|
|
52
|
+
record: found.record,
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
const readBlock = (blockmap, cid, validate) => {
|
|
56
|
+
const bytes = blockmap.get(cid);
|
|
57
|
+
if (!bytes) {
|
|
58
|
+
throw new Error(`cid not found in blockmap; cid=${cid}`);
|
|
59
|
+
}
|
|
60
|
+
const decoded = CBOR.decode(bytes);
|
|
61
|
+
if (!validate(decoded)) {
|
|
62
|
+
throw new Error(`validation failed for cid=${cid}`);
|
|
63
|
+
}
|
|
64
|
+
return decoded;
|
|
65
|
+
};
|
|
66
|
+
const dfs = async (blockmap, from, targetKey, visited = new Set()) => {
|
|
67
|
+
// If there's no starting point, return empty state
|
|
68
|
+
if (from == null) {
|
|
69
|
+
return { found: false };
|
|
70
|
+
}
|
|
71
|
+
// Check for cycles
|
|
72
|
+
{
|
|
73
|
+
if (visited.has(from)) {
|
|
74
|
+
throw new Error(`cycle detected; cid=${from}`);
|
|
75
|
+
}
|
|
76
|
+
visited.add(from);
|
|
77
|
+
}
|
|
78
|
+
// Get the block data
|
|
79
|
+
let node;
|
|
80
|
+
{
|
|
81
|
+
const bytes = blockmap.get(from);
|
|
82
|
+
if (!bytes) {
|
|
83
|
+
return { found: false };
|
|
84
|
+
}
|
|
85
|
+
const decoded = CBOR.decode(bytes);
|
|
86
|
+
if (!isNodeData(decoded)) {
|
|
87
|
+
throw new Error(`invalid mst node; cid=${from}`);
|
|
88
|
+
}
|
|
89
|
+
node = decoded;
|
|
90
|
+
}
|
|
91
|
+
// Recursively process the left child
|
|
92
|
+
const left = await dfs(blockmap, node.l?.$link, targetKey, visited);
|
|
93
|
+
let key = '';
|
|
94
|
+
let found = left.found;
|
|
95
|
+
let depth;
|
|
96
|
+
let firstKey;
|
|
97
|
+
let lastKey;
|
|
98
|
+
// Process all entries in this node
|
|
99
|
+
for (const entry of node.e) {
|
|
100
|
+
// Construct the key by truncating and appending
|
|
101
|
+
key = key.substring(0, entry.p) + decodeUtf8From(CBOR.fromBytes(entry.k));
|
|
102
|
+
// Check if this is our target key
|
|
103
|
+
if (key === targetKey) {
|
|
104
|
+
const recordBytes = blockmap.get(entry.v.$link);
|
|
105
|
+
if (recordBytes) {
|
|
106
|
+
const record = CBOR.decode(recordBytes);
|
|
107
|
+
found = { cid: entry.v.$link, record };
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
// Calculate depth based on leading zeros in the hash
|
|
111
|
+
const keyDigest = await toSha256(encodeUtf8(key));
|
|
112
|
+
let zeroCount = 0;
|
|
113
|
+
outerLoop: for (const byte of keyDigest) {
|
|
114
|
+
for (let bit = 7; bit >= 0; bit--) {
|
|
115
|
+
if (((byte >> bit) & 1) !== 0) {
|
|
116
|
+
break outerLoop;
|
|
117
|
+
}
|
|
118
|
+
zeroCount++;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
const thisDepth = Math.floor(zeroCount / 2);
|
|
122
|
+
// Ensure consistent depth
|
|
123
|
+
if (depth === undefined) {
|
|
124
|
+
depth = thisDepth;
|
|
125
|
+
}
|
|
126
|
+
else if (depth !== thisDepth) {
|
|
127
|
+
throw new Error(`node has entries with different depths; cid=${from}`);
|
|
128
|
+
}
|
|
129
|
+
// Track first and last keys
|
|
130
|
+
if (lastKey === undefined) {
|
|
131
|
+
firstKey = key;
|
|
132
|
+
lastKey = key;
|
|
133
|
+
}
|
|
134
|
+
// Check key ordering
|
|
135
|
+
if (lastKey > key) {
|
|
136
|
+
throw new Error(`entries are out of order; cid=${from}`);
|
|
137
|
+
}
|
|
138
|
+
// Process right child
|
|
139
|
+
const right = await dfs(blockmap, entry.t?.$link, targetKey, visited);
|
|
140
|
+
// Check ordering with right subtree
|
|
141
|
+
if (right.min && right.min < lastKey) {
|
|
142
|
+
throw new Error(`entries are out of order; cid=${from}`);
|
|
143
|
+
}
|
|
144
|
+
found = found || right.found;
|
|
145
|
+
// Check depth ordering
|
|
146
|
+
if (left.depth !== undefined && left.depth >= thisDepth) {
|
|
147
|
+
throw new Error(`depths are out of order; cid=${from}`);
|
|
148
|
+
}
|
|
149
|
+
if (right.depth !== undefined && right.depth >= thisDepth) {
|
|
150
|
+
throw new Error(`depths are out of order; cid=${from}`);
|
|
151
|
+
}
|
|
152
|
+
// Update last key based on right subtree
|
|
153
|
+
lastKey = right.max ?? key;
|
|
154
|
+
}
|
|
155
|
+
// Check ordering with left subtree
|
|
156
|
+
if (left.max && firstKey && left.max > firstKey) {
|
|
157
|
+
throw new Error(`entries are out of order; cid=${from}`);
|
|
158
|
+
}
|
|
159
|
+
return {
|
|
160
|
+
found,
|
|
161
|
+
min: firstKey,
|
|
162
|
+
max: lastKey,
|
|
163
|
+
depth,
|
|
164
|
+
};
|
|
165
|
+
};
|
|
166
|
+
//# sourceMappingURL=verify.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"verify.js","sourceRoot":"","sources":["../lib/verify.ts"],"names":[],"mappings":"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"}
|
package/lib/index.ts
ADDED
package/lib/reader.ts
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import type { CarEntry } from '@atcute/car';
|
|
2
|
+
import * as CAR from '@atcute/car';
|
|
3
|
+
import * as CBOR from '@atcute/cbor';
|
|
4
|
+
import type { CidLink } from '@atcute/cid';
|
|
5
|
+
import * as CID from '@atcute/cid';
|
|
6
|
+
import { isNodeData } from '@atcute/mst';
|
|
7
|
+
import { decodeUtf8From } from '@atcute/uint8array';
|
|
8
|
+
|
|
9
|
+
import { isCommit, RepoEntry } from './types.js';
|
|
10
|
+
import { assert } from './utils.js';
|
|
11
|
+
|
|
12
|
+
/** @internal */
|
|
13
|
+
type EntryMap = Map<string, CarEntry>;
|
|
14
|
+
|
|
15
|
+
/** node entry object */
|
|
16
|
+
interface NodeEntry {
|
|
17
|
+
key: string;
|
|
18
|
+
cid: CidLink;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function* fromUint8Array(buf: Uint8Array): Generator<RepoEntry> {
|
|
22
|
+
const car = CAR.fromUint8Array(buf);
|
|
23
|
+
const roots = car.roots;
|
|
24
|
+
|
|
25
|
+
assert(roots.length === 1, `expected only 1 root in the car archive; got=${roots.length}`);
|
|
26
|
+
|
|
27
|
+
const map: EntryMap = new Map();
|
|
28
|
+
for (const entry of car) {
|
|
29
|
+
map.set(CID.toString(entry.cid), entry);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// [commit, mst node, record?]
|
|
33
|
+
assert(map.size >= 2, `expected at least 2 blocks in the archive; got=${map.size}`);
|
|
34
|
+
|
|
35
|
+
const commit = readEntry(map, roots[0], isCommit);
|
|
36
|
+
|
|
37
|
+
for (const { key, cid } of walkMstEntries(map, commit.data)) {
|
|
38
|
+
const [collection, rkey] = key.split('/');
|
|
39
|
+
|
|
40
|
+
const carEntry = map.get(cid.$link);
|
|
41
|
+
assert(carEntry != null, `cid not found in blockmap; cid=${cid}`);
|
|
42
|
+
|
|
43
|
+
yield new RepoEntry(collection, rkey, cid, carEntry);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* reads a block from the blockmap and validates it against the provided validation function
|
|
49
|
+
* @internal
|
|
50
|
+
* @param map a mapping of CID string -> actual bytes
|
|
51
|
+
* @param link a CID link to read
|
|
52
|
+
* @param validate a validation function to validate the decoded data
|
|
53
|
+
* @returns the decoded and validated data
|
|
54
|
+
*/
|
|
55
|
+
export const readEntry = <T>(map: EntryMap, link: CidLink, validate: (value: unknown) => value is T): T => {
|
|
56
|
+
const cid = link.$link;
|
|
57
|
+
|
|
58
|
+
const entry = map.get(cid);
|
|
59
|
+
assert(entry != null, `cid not found in blockmap; cid=${cid}`);
|
|
60
|
+
|
|
61
|
+
const data = CBOR.decode(entry.bytes);
|
|
62
|
+
assert(validate(data), `validation failed for cid=${cid}`);
|
|
63
|
+
|
|
64
|
+
return data;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* walks the entries of a Merkle Sorted Tree (MST) in a depth-first manner
|
|
69
|
+
* @internal
|
|
70
|
+
* @param map a mapping of CID string -> actual bytes
|
|
71
|
+
* @param pointer a CID link to the root of the MST
|
|
72
|
+
* @returns a generator that yields the entries of the MST
|
|
73
|
+
*/
|
|
74
|
+
export function* walkMstEntries(map: EntryMap, pointer: CidLink): Generator<NodeEntry> {
|
|
75
|
+
const data = readEntry(map, pointer, isNodeData);
|
|
76
|
+
const entries = data.e;
|
|
77
|
+
|
|
78
|
+
let lastKey = '';
|
|
79
|
+
|
|
80
|
+
if (data.l !== null) {
|
|
81
|
+
yield* walkMstEntries(map, data.l);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
for (let i = 0, il = entries.length; i < il; i++) {
|
|
85
|
+
const entry = entries[i];
|
|
86
|
+
|
|
87
|
+
const key_str = decodeUtf8From(CBOR.fromBytes(entry.k));
|
|
88
|
+
const key = lastKey.slice(0, entry.p) + key_str;
|
|
89
|
+
|
|
90
|
+
lastKey = key;
|
|
91
|
+
|
|
92
|
+
yield { key: key, cid: entry.v };
|
|
93
|
+
|
|
94
|
+
if (entry.t !== null) {
|
|
95
|
+
yield* walkMstEntries(map, entry.t);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import type { CarEntry } from '@atcute/car';
|
|
2
|
+
import * as CAR from '@atcute/car';
|
|
3
|
+
import * as CBOR from '@atcute/cbor';
|
|
4
|
+
import * as CID from '@atcute/cid';
|
|
5
|
+
import { isNodeData } from '@atcute/mst';
|
|
6
|
+
import { decodeUtf8From } from '@atcute/uint8array';
|
|
7
|
+
|
|
8
|
+
import { isCommit, RepoEntry } from './types.js';
|
|
9
|
+
import { assert } from './utils.js';
|
|
10
|
+
import Queue from './utils/queue.js';
|
|
11
|
+
|
|
12
|
+
type EntryMeta = { t: 0 } | { t: 1 } | { t: 2; k: string };
|
|
13
|
+
|
|
14
|
+
type Task = {
|
|
15
|
+
c: string;
|
|
16
|
+
e: CarEntry;
|
|
17
|
+
m: EntryMeta;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type MissingBlockEntry =
|
|
21
|
+
| {
|
|
22
|
+
cid: string;
|
|
23
|
+
type: 'record';
|
|
24
|
+
key: string;
|
|
25
|
+
}
|
|
26
|
+
| {
|
|
27
|
+
cid: string;
|
|
28
|
+
type: 'mst-node';
|
|
29
|
+
}
|
|
30
|
+
| {
|
|
31
|
+
cid: string;
|
|
32
|
+
type: 'commit';
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export interface StreamedRepoReader {
|
|
36
|
+
/**
|
|
37
|
+
* list of blocks that were referenced but not found in the repository.
|
|
38
|
+
* blocks may be reported as missing if multiple records share the same CID.
|
|
39
|
+
*/
|
|
40
|
+
readonly missingBlocks: readonly MissingBlockEntry[];
|
|
41
|
+
|
|
42
|
+
dispose(): Promise<void>;
|
|
43
|
+
|
|
44
|
+
[Symbol.asyncDispose](): Promise<void>;
|
|
45
|
+
[Symbol.asyncIterator](): AsyncIterator<RepoEntry>;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export const repoEntryTransform = (): ReadableWritablePair<RepoEntry, Uint8Array> => {
|
|
49
|
+
const transform = new TransformStream<Uint8Array, Uint8Array>();
|
|
50
|
+
let repo: StreamedRepoReader | undefined;
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
readable: new ReadableStream({
|
|
54
|
+
async start(controller) {
|
|
55
|
+
repo = fromStream(transform.readable);
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
for await (const entry of repo) {
|
|
59
|
+
controller.enqueue(entry);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
await repo.dispose();
|
|
63
|
+
|
|
64
|
+
controller.close();
|
|
65
|
+
} catch (err) {
|
|
66
|
+
controller.error(err);
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
async cancel() {
|
|
70
|
+
if (repo !== undefined) {
|
|
71
|
+
await repo.dispose();
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
}),
|
|
75
|
+
writable: transform.writable,
|
|
76
|
+
};
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export const fromStream = (stream: ReadableStream<Uint8Array>): StreamedRepoReader => {
|
|
80
|
+
let missingBlocks: MissingBlockEntry[] = [];
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
get missingBlocks() {
|
|
84
|
+
return missingBlocks;
|
|
85
|
+
},
|
|
86
|
+
|
|
87
|
+
async dispose() {
|
|
88
|
+
// does nothing for now
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
[Symbol.asyncDispose]() {
|
|
92
|
+
return this.dispose();
|
|
93
|
+
},
|
|
94
|
+
async *[Symbol.asyncIterator]() {
|
|
95
|
+
// await using car = CarReader.fromStream(stream);
|
|
96
|
+
const car = CAR.fromStream(stream);
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
const pending = new Map<string, EntryMeta>();
|
|
100
|
+
const strays = new Map<string, CarEntry>();
|
|
101
|
+
|
|
102
|
+
const queue = new Queue<Task>();
|
|
103
|
+
|
|
104
|
+
const request = (cid: string, meta: EntryMeta): void => {
|
|
105
|
+
const entry = strays.get(cid);
|
|
106
|
+
|
|
107
|
+
if (entry !== undefined) {
|
|
108
|
+
strays.delete(cid);
|
|
109
|
+
queue.enqueue({ c: cid, e: entry, m: meta });
|
|
110
|
+
} else {
|
|
111
|
+
pending.set(cid, meta);
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
{
|
|
116
|
+
const roots = await car.roots();
|
|
117
|
+
assert(roots.length === 1, `expected only 1 root in the car archive; got=${roots.length}`);
|
|
118
|
+
|
|
119
|
+
const rootCid = roots[0].$link;
|
|
120
|
+
request(rootCid, { t: 0 });
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
for await (const entry of car) {
|
|
124
|
+
const cid = CID.toString(entry.cid);
|
|
125
|
+
|
|
126
|
+
{
|
|
127
|
+
const meta = pending.get(cid);
|
|
128
|
+
|
|
129
|
+
if (meta !== undefined) {
|
|
130
|
+
pending.delete(cid);
|
|
131
|
+
queue.enqueue({ c: cid, e: entry, m: meta });
|
|
132
|
+
} else {
|
|
133
|
+
strays.set(cid, entry);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
let task: Task | undefined;
|
|
138
|
+
while ((task = queue.dequeue())) {
|
|
139
|
+
const { c: cid, e: entry, m: meta } = task;
|
|
140
|
+
|
|
141
|
+
switch (meta.t) {
|
|
142
|
+
case 0: {
|
|
143
|
+
const commit = CBOR.decode(entry.bytes);
|
|
144
|
+
assert(isCommit(commit), `expected commit block; cid=${cid}`);
|
|
145
|
+
|
|
146
|
+
request(commit.data.$link, { t: 1 });
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
case 1: {
|
|
150
|
+
const node = CBOR.decode(entry.bytes);
|
|
151
|
+
assert(isNodeData(node), `expected mst node block; cid=${cid}`);
|
|
152
|
+
|
|
153
|
+
const entries = node.e;
|
|
154
|
+
const left = node.l;
|
|
155
|
+
|
|
156
|
+
let lastKey = '';
|
|
157
|
+
|
|
158
|
+
if (left !== null) {
|
|
159
|
+
request(left.$link, meta);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
for (let i = 0, il = entries.length; i < il; i++) {
|
|
163
|
+
const entry = entries[i];
|
|
164
|
+
const next = entry.t;
|
|
165
|
+
|
|
166
|
+
const key_str = decodeUtf8From(CBOR.fromBytes(entry.k));
|
|
167
|
+
const key = lastKey.slice(0, entry.p) + key_str;
|
|
168
|
+
|
|
169
|
+
lastKey = key;
|
|
170
|
+
|
|
171
|
+
request(entry.v.$link, { t: 2, k: key });
|
|
172
|
+
|
|
173
|
+
if (next !== null) {
|
|
174
|
+
request(next.$link, { t: 1 });
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
break;
|
|
179
|
+
}
|
|
180
|
+
case 2: {
|
|
181
|
+
const [collection, rkey] = meta.k.split('/');
|
|
182
|
+
|
|
183
|
+
yield new RepoEntry(collection, rkey, CID.toCidLink(entry.cid), entry);
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
missingBlocks = Array.from(pending, ([cid, meta]): MissingBlockEntry => {
|
|
191
|
+
switch (meta.t) {
|
|
192
|
+
case 0: {
|
|
193
|
+
return { cid, type: 'commit' };
|
|
194
|
+
}
|
|
195
|
+
case 1: {
|
|
196
|
+
return { cid, type: 'mst-node' };
|
|
197
|
+
}
|
|
198
|
+
case 2: {
|
|
199
|
+
return { cid, type: 'record', key: meta.k };
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
} finally {
|
|
204
|
+
await car.dispose();
|
|
205
|
+
}
|
|
206
|
+
},
|
|
207
|
+
};
|
|
208
|
+
};
|
package/lib/types.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { CarEntry } from '@atcute/car';
|
|
2
|
+
import * as CBOR from '@atcute/cbor';
|
|
3
|
+
import { isBytes, type Bytes } from '@atcute/cbor';
|
|
4
|
+
import { isCidLink, type CidLink } from '@atcute/cid';
|
|
5
|
+
|
|
6
|
+
export class RepoEntry {
|
|
7
|
+
/** @internal */
|
|
8
|
+
constructor(
|
|
9
|
+
/** the collection this record belongs to */
|
|
10
|
+
public readonly collection: string,
|
|
11
|
+
/** record key */
|
|
12
|
+
public readonly rkey: string,
|
|
13
|
+
/** CID of this record */
|
|
14
|
+
public readonly cid: CidLink,
|
|
15
|
+
/** the associated CarEntry for this record */
|
|
16
|
+
public readonly carEntry: CarEntry,
|
|
17
|
+
) {}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* raw contents of this record
|
|
21
|
+
*/
|
|
22
|
+
get bytes(): Uint8Array {
|
|
23
|
+
return this.carEntry.bytes;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* decoded contents of this record
|
|
28
|
+
*/
|
|
29
|
+
get record(): unknown {
|
|
30
|
+
return CBOR.decode(this.bytes);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** commit object */
|
|
35
|
+
export interface Commit {
|
|
36
|
+
version: 3;
|
|
37
|
+
did: string;
|
|
38
|
+
data: CidLink;
|
|
39
|
+
rev: string;
|
|
40
|
+
prev: CidLink | null;
|
|
41
|
+
sig: Bytes;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* checks if value is a valid commit object
|
|
46
|
+
* @param value value to check
|
|
47
|
+
* @returns true if the value is a valid commit object, false otherwise
|
|
48
|
+
*/
|
|
49
|
+
export const isCommit = (value: unknown): value is Commit => {
|
|
50
|
+
if (value === null || typeof value !== 'object') {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const obj = value as Record<string, unknown>;
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
obj.version === 3 &&
|
|
58
|
+
typeof obj.did === 'string' &&
|
|
59
|
+
isCidLink(obj.data) &&
|
|
60
|
+
typeof obj.rev === 'string' &&
|
|
61
|
+
(obj.prev === null || isCidLink(obj.prev)) &&
|
|
62
|
+
isBytes(obj.sig)
|
|
63
|
+
);
|
|
64
|
+
};
|