@alteran/astro 0.3.8 → 0.5.2
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/README.md +19 -30
- package/index.js +34 -28
- package/migrations/0007_bored_spitfire.sql +26 -0
- package/migrations/0008_furry_ozymandias.sql +2 -0
- package/migrations/meta/0007_snapshot.json +534 -0
- package/migrations/meta/0008_snapshot.json +548 -0
- package/migrations/meta/_journal.json +14 -0
- package/package.json +10 -9
- package/src/app.ts +8 -4
- package/src/db/account.ts +25 -6
- package/src/db/dal.ts +34 -23
- package/src/db/repo.ts +35 -35
- package/src/db/schema.ts +5 -1
- package/src/db/seed.ts +5 -13
- package/src/entrypoints/server.ts +2 -22
- package/src/handlers/root.ts +4 -4
- package/src/lib/account-state.ts +156 -0
- package/src/lib/actor.ts +28 -12
- package/src/lib/appview/auth-policy.ts +66 -0
- package/src/lib/appview/did-resolver.ts +233 -0
- package/src/lib/appview/proxy.ts +221 -0
- package/src/lib/appview/service-config.ts +61 -0
- package/src/lib/appview/service-jwt.ts +93 -0
- package/src/lib/appview/types.ts +25 -0
- package/src/lib/appview.ts +5 -532
- package/src/lib/auth-errors.ts +24 -0
- package/src/lib/auth.ts +63 -15
- package/src/lib/blockstore-gc.ts +2 -1
- package/src/lib/cache.ts +30 -4
- package/src/lib/chat.ts +14 -8
- package/src/lib/commit.ts +26 -36
- package/src/lib/config.ts +26 -15
- package/src/lib/did-document.ts +32 -0
- package/src/lib/errors.ts +54 -0
- package/src/lib/feed.ts +18 -19
- package/src/lib/firehose/frames.ts +87 -47
- package/src/lib/firehose/validation.ts +3 -3
- package/src/lib/jwt.ts +85 -177
- package/src/lib/labeler.ts +43 -30
- package/src/lib/logger.ts +4 -0
- package/src/lib/mst/block-map.ts +172 -0
- package/src/lib/mst/blockstore.ts +56 -93
- package/src/lib/mst/index.ts +1 -0
- package/src/lib/mst/leaf.ts +25 -0
- package/src/lib/mst/mst.ts +81 -237
- package/src/lib/mst/serialize.ts +97 -0
- package/src/lib/mst/types.ts +21 -0
- package/src/lib/oauth/clients.ts +67 -0
- package/src/lib/oauth/dpop-errors.ts +15 -0
- package/src/lib/oauth/dpop.ts +150 -0
- package/src/lib/oauth/resource.ts +199 -0
- package/src/lib/oauth/store.ts +77 -0
- package/src/lib/preferences.ts +9 -34
- package/src/lib/refresh-session.ts +161 -0
- package/src/lib/relay.ts +10 -8
- package/src/lib/secrets.ts +6 -7
- package/src/lib/sequencer.ts +12 -3
- package/src/lib/service-auth.ts +184 -0
- package/src/lib/session-tokens.ts +28 -76
- package/src/lib/streaming-car.ts +3 -0
- package/src/lib/tracing.ts +4 -3
- package/src/lib/util.ts +65 -15
- package/src/middleware.ts +1 -1
- package/src/pages/.well-known/did.json.ts +27 -30
- package/src/pages/.well-known/oauth-authorization-server.ts +31 -0
- package/src/pages/.well-known/oauth-protected-resource.ts +22 -0
- package/src/pages/debug/record.ts +1 -1
- package/src/pages/debug/sequencer.ts +28 -0
- package/src/pages/oauth/authorize.ts +78 -0
- package/src/pages/oauth/consent.ts +80 -0
- package/src/pages/oauth/par.ts +121 -0
- package/src/pages/oauth/token.ts +158 -0
- package/src/pages/xrpc/[...nsid].ts +61 -0
- package/src/pages/xrpc/app.bsky.actor.getPreferences.ts +12 -13
- package/src/pages/xrpc/app.bsky.actor.putPreferences.ts +23 -23
- package/src/pages/xrpc/app.bsky.unspecced.getAgeAssuranceState.ts +9 -2
- package/src/pages/xrpc/chat.bsky.convo.getLog.ts +9 -2
- package/src/pages/xrpc/chat.bsky.convo.listConvos.ts +9 -2
- package/src/pages/xrpc/com.atproto.identity.getRecommendedDidCredentials.ts +43 -41
- package/src/pages/xrpc/com.atproto.identity.requestPlcOperationSignature.ts +10 -3
- package/src/pages/xrpc/com.atproto.identity.resolveHandle.ts +40 -9
- package/src/pages/xrpc/com.atproto.identity.signPlcOperation.ts +41 -29
- package/src/pages/xrpc/com.atproto.identity.submitPlcOperation.ts +20 -6
- package/src/pages/xrpc/com.atproto.identity.updateHandle.ts +1 -1
- package/src/pages/xrpc/com.atproto.repo.applyWrites.ts +101 -11
- package/src/pages/xrpc/com.atproto.repo.createRecord.ts +44 -14
- package/src/pages/xrpc/com.atproto.repo.deleteRecord.ts +41 -13
- package/src/pages/xrpc/com.atproto.repo.describeRepo.ts +2 -2
- package/src/pages/xrpc/com.atproto.repo.getRecord.ts +14 -1
- package/src/pages/xrpc/com.atproto.repo.listMissingBlobs.ts +14 -6
- package/src/pages/xrpc/com.atproto.repo.listRecords.ts +1 -1
- package/src/pages/xrpc/com.atproto.repo.putRecord.ts +42 -14
- package/src/pages/xrpc/com.atproto.repo.uploadBlob.ts +76 -15
- package/src/pages/xrpc/com.atproto.server.checkAccountStatus.ts +20 -8
- package/src/pages/xrpc/com.atproto.server.createSession.ts +31 -11
- package/src/pages/xrpc/com.atproto.server.describeServer.ts +1 -1
- package/src/pages/xrpc/com.atproto.server.getServiceAuth.ts +12 -5
- package/src/pages/xrpc/com.atproto.server.getSession.ts +22 -8
- package/src/pages/xrpc/com.atproto.server.refreshSession.ts +30 -72
- package/src/pages/xrpc/com.atproto.sync.getBlob.ts +71 -22
- package/src/pages/xrpc/com.atproto.sync.getCheckout.json.ts +1 -1
- package/src/pages/xrpc/com.atproto.sync.getCheckout.ts +1 -1
- package/src/pages/xrpc/com.atproto.sync.getHead.ts +7 -2
- package/src/pages/xrpc/com.atproto.sync.getLatestCommit.ts +1 -1
- package/src/pages/xrpc/com.atproto.sync.getRecord.ts +5 -27
- package/src/pages/xrpc/com.atproto.sync.getRepo.json.ts +1 -1
- package/src/pages/xrpc/com.atproto.sync.getRepo.ts +50 -5
- package/src/pages/xrpc/com.atproto.sync.getRepoStatus.ts +58 -0
- package/src/pages/xrpc/com.atproto.sync.listBlobs.ts +1 -1
- package/src/pages/xrpc/com.atproto.sync.listRepos.ts +5 -3
- package/src/services/car.ts +207 -55
- package/src/services/r2-blob-store.ts +1 -1
- package/src/services/repo/blockstore-ops.ts +29 -0
- package/src/services/repo/operations.ts +133 -0
- package/src/services/repo-manager.ts +202 -253
- package/src/worker/runtime.ts +53 -8
- package/src/worker/sequencer/broadcast.ts +91 -0
- package/src/worker/sequencer/cid-helpers.ts +39 -0
- package/src/worker/sequencer/payload.ts +84 -0
- package/src/worker/sequencer/types.ts +36 -0
- package/src/worker/sequencer/upgrade.ts +141 -0
- package/src/worker/sequencer.ts +288 -412
- package/types/env.d.ts +15 -3
- package/src/pages/xrpc/app.bsky.actor.getProfile.ts +0 -49
- package/src/pages/xrpc/app.bsky.actor.getProfiles.ts +0 -51
- package/src/pages/xrpc/app.bsky.feed.getActorFeeds.ts +0 -25
- package/src/pages/xrpc/app.bsky.feed.getAuthorFeed.ts +0 -42
- package/src/pages/xrpc/app.bsky.feed.getFeedGenerators.ts +0 -25
- package/src/pages/xrpc/app.bsky.feed.getPostThread.ts +0 -37
- package/src/pages/xrpc/app.bsky.feed.getPosts.ts +0 -26
- package/src/pages/xrpc/app.bsky.feed.getTimeline.ts +0 -47
- package/src/pages/xrpc/app.bsky.graph.getFollowers.ts +0 -29
- package/src/pages/xrpc/app.bsky.graph.getFollows.ts +0 -29
- package/src/pages/xrpc/app.bsky.notification.getUnreadCount.ts +0 -20
- package/src/pages/xrpc/app.bsky.notification.listNotifications.ts +0 -27
package/src/lib/mst/mst.ts
CHANGED
|
@@ -1,43 +1,26 @@
|
|
|
1
1
|
import { CID } from 'multiformats/cid';
|
|
2
|
-
import * as uint8arrays from 'uint8arrays';
|
|
3
2
|
import * as dagCbor from '@ipld/dag-cbor';
|
|
4
3
|
import type { ReadableBlockstore } from './blockstore';
|
|
5
4
|
import * as util from './util';
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
*/
|
|
19
|
-
export interface TreeEntry {
|
|
20
|
-
p: number; // prefix count shared with previous key
|
|
21
|
-
k: Uint8Array; // rest of key after prefix
|
|
22
|
-
v: CID; // value CID
|
|
23
|
-
t: CID | null; // next subtree (to right of leaf)
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Node entry can be either an MST subtree or a Leaf
|
|
28
|
-
*/
|
|
29
|
-
export type NodeEntry = MST | Leaf;
|
|
30
|
-
|
|
31
|
-
export interface MstOpts {
|
|
32
|
-
layer: number;
|
|
33
|
-
}
|
|
5
|
+
import { BlockMap } from './block-map';
|
|
6
|
+
import { Leaf } from './leaf';
|
|
7
|
+
import type { NodeData, NodeEntry, MstOpts } from './types';
|
|
8
|
+
import {
|
|
9
|
+
cidForEntries,
|
|
10
|
+
deserializeNodeData,
|
|
11
|
+
layerForEntries,
|
|
12
|
+
serializeNodeData,
|
|
13
|
+
} from './serialize';
|
|
14
|
+
|
|
15
|
+
export type { NodeData, TreeEntry, NodeEntry, MstOpts } from './types';
|
|
16
|
+
export { Leaf } from './leaf';
|
|
34
17
|
|
|
35
18
|
/**
|
|
36
19
|
* Merkle Search Tree (MST) Implementation
|
|
37
20
|
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
21
|
+
* Ordered, insert-order-independent, deterministic tree. Keys are laid out
|
|
22
|
+
* alphabetically; each key's layer is determined by leading zeros on its hash
|
|
23
|
+
* (~4 fanout, 2 bits per layer).
|
|
41
24
|
*/
|
|
42
25
|
export class MST {
|
|
43
26
|
storage: ReadableBlockstore;
|
|
@@ -58,9 +41,6 @@ export class MST {
|
|
|
58
41
|
this.pointer = pointer;
|
|
59
42
|
}
|
|
60
43
|
|
|
61
|
-
/**
|
|
62
|
-
* Create a new MST from entries
|
|
63
|
-
*/
|
|
64
44
|
static async create(
|
|
65
45
|
storage: ReadableBlockstore,
|
|
66
46
|
entries: NodeEntry[] = [],
|
|
@@ -71,23 +51,17 @@ export class MST {
|
|
|
71
51
|
return new MST(storage, pointer, entries, layer);
|
|
72
52
|
}
|
|
73
53
|
|
|
74
|
-
/**
|
|
75
|
-
* Load MST from NodeData
|
|
76
|
-
*/
|
|
77
54
|
static async fromData(
|
|
78
55
|
storage: ReadableBlockstore,
|
|
79
56
|
data: NodeData,
|
|
80
57
|
opts?: Partial<MstOpts>,
|
|
81
58
|
): Promise<MST> {
|
|
82
59
|
const { layer = null } = opts || {};
|
|
83
|
-
const entries = await deserializeNodeData(storage, data, opts);
|
|
60
|
+
const entries = await deserializeNodeData(storage, data, MST.load, opts);
|
|
84
61
|
const pointer = await util.cidForCbor(data);
|
|
85
62
|
return new MST(storage, pointer, entries, layer);
|
|
86
63
|
}
|
|
87
64
|
|
|
88
|
-
/**
|
|
89
|
-
* Lazy load MST from CID (doesn't fetch from storage yet)
|
|
90
|
-
*/
|
|
91
65
|
static load(
|
|
92
66
|
storage: ReadableBlockstore,
|
|
93
67
|
cid: CID,
|
|
@@ -97,18 +71,12 @@ export class MST {
|
|
|
97
71
|
return new MST(storage, cid, null, layer);
|
|
98
72
|
}
|
|
99
73
|
|
|
100
|
-
/**
|
|
101
|
-
* Create new tree with updated entries (immutable operation)
|
|
102
|
-
*/
|
|
103
74
|
async newTree(entries: NodeEntry[]): Promise<MST> {
|
|
104
75
|
const mst = new MST(this.storage, this.pointer, entries, this.layer);
|
|
105
76
|
mst.outdatedPointer = true;
|
|
106
77
|
return mst;
|
|
107
78
|
}
|
|
108
79
|
|
|
109
|
-
/**
|
|
110
|
-
* Get entries (lazy load from storage if needed)
|
|
111
|
-
*/
|
|
112
80
|
async getEntries(): Promise<NodeEntry[]> {
|
|
113
81
|
if (this.entries) return [...this.entries];
|
|
114
82
|
|
|
@@ -118,49 +86,36 @@ export class MST {
|
|
|
118
86
|
const layer = firstLeaf !== undefined
|
|
119
87
|
? await util.leadingZerosOnHash(firstLeaf.k)
|
|
120
88
|
: undefined;
|
|
121
|
-
|
|
122
|
-
this.entries = await deserializeNodeData(this.storage, data, { layer });
|
|
89
|
+
this.entries = await deserializeNodeData(this.storage, data, MST.load, { layer });
|
|
123
90
|
return this.entries;
|
|
124
91
|
}
|
|
125
92
|
|
|
126
93
|
throw new Error('No entries or CID provided');
|
|
127
94
|
}
|
|
128
95
|
|
|
129
|
-
/**
|
|
130
|
-
* Get pointer CID (recalculate if outdated)
|
|
131
|
-
*/
|
|
132
96
|
async getPointer(): Promise<CID> {
|
|
133
97
|
if (!this.outdatedPointer) return this.pointer;
|
|
134
|
-
|
|
135
98
|
const { cid } = await this.serialize();
|
|
136
99
|
this.pointer = cid;
|
|
137
100
|
this.outdatedPointer = false;
|
|
138
101
|
return this.pointer;
|
|
139
102
|
}
|
|
140
103
|
|
|
141
|
-
/**
|
|
142
|
-
* Serialize MST to CBOR bytes
|
|
143
|
-
*/
|
|
144
104
|
async serialize(): Promise<{ cid: CID; bytes: Uint8Array }> {
|
|
145
105
|
let entries = await this.getEntries();
|
|
146
106
|
|
|
147
|
-
|
|
148
|
-
const outdated = entries.filter(e => e.isTree() && e.outdatedPointer) as MST[];
|
|
107
|
+
const outdated = entries.filter((e) => e.isTree() && e.outdatedPointer) as MST[];
|
|
149
108
|
if (outdated.length > 0) {
|
|
150
|
-
await Promise.all(outdated.map(e => e.getPointer()));
|
|
109
|
+
await Promise.all(outdated.map((e) => e.getPointer()));
|
|
151
110
|
entries = await this.getEntries();
|
|
152
111
|
}
|
|
153
112
|
|
|
154
113
|
const data = serializeNodeData(entries);
|
|
155
114
|
const bytes = dagCbor.encode(data);
|
|
156
115
|
const cid = await util.cidForCbor(data);
|
|
157
|
-
|
|
158
116
|
return { cid, bytes };
|
|
159
117
|
}
|
|
160
118
|
|
|
161
|
-
/**
|
|
162
|
-
* Get layer of this node
|
|
163
|
-
*/
|
|
164
119
|
async getLayer(): Promise<number> {
|
|
165
120
|
this.layer = await this.attemptGetLayer();
|
|
166
121
|
if (this.layer === null) this.layer = 0;
|
|
@@ -189,9 +144,30 @@ export class MST {
|
|
|
189
144
|
return layer;
|
|
190
145
|
}
|
|
191
146
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
147
|
+
// Returns the set of blocks reachable from this node that aren't yet
|
|
148
|
+
// persisted in storage. Used to compute the minimal write set per commit.
|
|
149
|
+
async getUnstoredBlocks(): Promise<{ root: CID; blocks: BlockMap }> {
|
|
150
|
+
const blocks = new BlockMap();
|
|
151
|
+
const pointer = await this.getPointer();
|
|
152
|
+
|
|
153
|
+
if (await this.storage.has(pointer)) {
|
|
154
|
+
return { root: pointer, blocks };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const entries = await this.getEntries();
|
|
158
|
+
const data = serializeNodeData(entries);
|
|
159
|
+
await blocks.add(data);
|
|
160
|
+
|
|
161
|
+
for (const entry of entries) {
|
|
162
|
+
if (entry.isTree()) {
|
|
163
|
+
const subtree = await entry.getUnstoredBlocks();
|
|
164
|
+
blocks.addMap(subtree.blocks);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return { root: pointer, blocks };
|
|
169
|
+
}
|
|
170
|
+
|
|
195
171
|
async add(key: string, value: CID, knownZeros?: number): Promise<MST> {
|
|
196
172
|
util.ensureValidMstKey(key);
|
|
197
173
|
const keyZeros = knownZeros ?? (await util.leadingZerosOnHash(key));
|
|
@@ -199,7 +175,6 @@ export class MST {
|
|
|
199
175
|
const newLeaf = new Leaf(key, value);
|
|
200
176
|
|
|
201
177
|
if (keyZeros === layer) {
|
|
202
|
-
// Key belongs in this layer
|
|
203
178
|
const index = await this.findGtOrEqualLeafIndex(key);
|
|
204
179
|
const found = await this.atIndex(index);
|
|
205
180
|
|
|
@@ -210,49 +185,45 @@ export class MST {
|
|
|
210
185
|
const prevNode = await this.atIndex(index - 1);
|
|
211
186
|
if (!prevNode || prevNode.isLeaf()) {
|
|
212
187
|
return this.spliceIn(newLeaf, index);
|
|
213
|
-
} else {
|
|
214
|
-
const splitSubTree = await prevNode.splitAround(key);
|
|
215
|
-
return this.replaceWithSplit(index - 1, splitSubTree[0], newLeaf, splitSubTree[1]);
|
|
216
188
|
}
|
|
217
|
-
|
|
218
|
-
|
|
189
|
+
const splitSubTree = await prevNode.splitAround(key);
|
|
190
|
+
return this.replaceWithSplit(index - 1, splitSubTree[0], newLeaf, splitSubTree[1]);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (keyZeros < layer) {
|
|
219
194
|
const index = await this.findGtOrEqualLeafIndex(key);
|
|
220
195
|
const prevNode = await this.atIndex(index - 1);
|
|
221
196
|
|
|
222
197
|
if (prevNode && prevNode.isTree()) {
|
|
223
198
|
const newSubtree = await prevNode.add(key, value, keyZeros);
|
|
224
199
|
return this.updateEntry(index - 1, newSubtree);
|
|
225
|
-
} else {
|
|
226
|
-
const subTree = await this.createChild();
|
|
227
|
-
const newSubTree = await subTree.add(key, value, keyZeros);
|
|
228
|
-
return this.spliceIn(newSubTree, index);
|
|
229
|
-
}
|
|
230
|
-
} else {
|
|
231
|
-
// Key belongs on a higher layer - push rest of tree down
|
|
232
|
-
const split = await this.splitAround(key);
|
|
233
|
-
let left: MST | null = split[0];
|
|
234
|
-
let right: MST | null = split[1];
|
|
235
|
-
const extraLayersToAdd = keyZeros - layer;
|
|
236
|
-
|
|
237
|
-
for (let i = 1; i < extraLayersToAdd; i++) {
|
|
238
|
-
if (left !== null) left = await left.createParent();
|
|
239
|
-
if (right !== null) right = await right.createParent();
|
|
240
200
|
}
|
|
201
|
+
const subTree = await this.createChild();
|
|
202
|
+
const newSubTree = await subTree.add(key, value, keyZeros);
|
|
203
|
+
return this.spliceIn(newSubTree, index);
|
|
204
|
+
}
|
|
241
205
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
206
|
+
// keyZeros > layer: push rest of tree down
|
|
207
|
+
const split = await this.splitAround(key);
|
|
208
|
+
let left: MST | null = split[0];
|
|
209
|
+
let right: MST | null = split[1];
|
|
210
|
+
const extraLayersToAdd = keyZeros - layer;
|
|
246
211
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
212
|
+
for (let i = 1; i < extraLayersToAdd; i++) {
|
|
213
|
+
if (left !== null) left = await left.createParent();
|
|
214
|
+
if (right !== null) right = await right.createParent();
|
|
250
215
|
}
|
|
216
|
+
|
|
217
|
+
const updated: NodeEntry[] = [];
|
|
218
|
+
if (left) updated.push(left);
|
|
219
|
+
updated.push(new Leaf(key, value));
|
|
220
|
+
if (right) updated.push(right);
|
|
221
|
+
|
|
222
|
+
const newRoot = await MST.create(this.storage, updated, { layer: keyZeros });
|
|
223
|
+
newRoot.outdatedPointer = true;
|
|
224
|
+
return newRoot;
|
|
251
225
|
}
|
|
252
226
|
|
|
253
|
-
/**
|
|
254
|
-
* Get value for a key
|
|
255
|
-
*/
|
|
256
227
|
async get(key: string): Promise<CID | null> {
|
|
257
228
|
const index = await this.findGtOrEqualLeafIndex(key);
|
|
258
229
|
const found = await this.atIndex(index);
|
|
@@ -269,9 +240,6 @@ export class MST {
|
|
|
269
240
|
return null;
|
|
270
241
|
}
|
|
271
242
|
|
|
272
|
-
/**
|
|
273
|
-
* Update value for existing key
|
|
274
|
-
*/
|
|
275
243
|
async update(key: string, value: CID): Promise<MST> {
|
|
276
244
|
util.ensureValidMstKey(key);
|
|
277
245
|
const index = await this.findGtOrEqualLeafIndex(key);
|
|
@@ -290,9 +258,6 @@ export class MST {
|
|
|
290
258
|
throw new Error(`Could not find a record with key: ${key}`);
|
|
291
259
|
}
|
|
292
260
|
|
|
293
|
-
/**
|
|
294
|
-
* Delete a key from the MST
|
|
295
|
-
*/
|
|
296
261
|
async delete(key: string): Promise<MST> {
|
|
297
262
|
const altered = await this.deleteRecurse(key);
|
|
298
263
|
return altered.trimTop();
|
|
@@ -313,29 +278,23 @@ export class MST {
|
|
|
313
278
|
merged,
|
|
314
279
|
...(await this.slice(index + 2)),
|
|
315
280
|
]);
|
|
316
|
-
} else {
|
|
317
|
-
return this.removeEntry(index);
|
|
318
281
|
}
|
|
282
|
+
return this.removeEntry(index);
|
|
319
283
|
}
|
|
320
284
|
|
|
321
285
|
const prev = await this.atIndex(index - 1);
|
|
322
286
|
if (prev?.isTree()) {
|
|
323
287
|
const subtree = await prev.deleteRecurse(key);
|
|
324
288
|
const subTreeEntries = await subtree.getEntries();
|
|
325
|
-
|
|
326
289
|
if (subTreeEntries.length === 0) {
|
|
327
290
|
return this.removeEntry(index - 1);
|
|
328
|
-
} else {
|
|
329
|
-
return this.updateEntry(index - 1, subtree);
|
|
330
291
|
}
|
|
331
|
-
|
|
332
|
-
throw new Error(`Could not find a record with key: ${key}`);
|
|
292
|
+
return this.updateEntry(index - 1, subtree);
|
|
333
293
|
}
|
|
294
|
+
|
|
295
|
+
throw new Error(`Could not find a record with key: ${key}`);
|
|
334
296
|
}
|
|
335
297
|
|
|
336
|
-
/**
|
|
337
|
-
* List entries with optional pagination
|
|
338
|
-
*/
|
|
339
298
|
async list(count = Number.MAX_SAFE_INTEGER, after?: string, before?: string): Promise<Leaf[]> {
|
|
340
299
|
const vals: Leaf[] = [];
|
|
341
300
|
for await (const leaf of this.walkLeavesFrom(after || '')) {
|
|
@@ -347,9 +306,6 @@ export class MST {
|
|
|
347
306
|
return vals;
|
|
348
307
|
}
|
|
349
308
|
|
|
350
|
-
/**
|
|
351
|
-
* List entries with a given prefix
|
|
352
|
-
*/
|
|
353
309
|
async listWithPrefix(prefix: string, count = Number.MAX_SAFE_INTEGER): Promise<Leaf[]> {
|
|
354
310
|
const vals: Leaf[] = [];
|
|
355
311
|
for await (const leaf of this.walkLeavesFrom(prefix)) {
|
|
@@ -359,23 +315,19 @@ export class MST {
|
|
|
359
315
|
return vals;
|
|
360
316
|
}
|
|
361
317
|
|
|
362
|
-
// Helper methods
|
|
363
|
-
|
|
364
318
|
async updateEntry(index: number, entry: NodeEntry): Promise<MST> {
|
|
365
|
-
|
|
319
|
+
return this.newTree([
|
|
366
320
|
...(await this.slice(0, index)),
|
|
367
321
|
entry,
|
|
368
322
|
...(await this.slice(index + 1)),
|
|
369
|
-
];
|
|
370
|
-
return this.newTree(update);
|
|
323
|
+
]);
|
|
371
324
|
}
|
|
372
325
|
|
|
373
326
|
async removeEntry(index: number): Promise<MST> {
|
|
374
|
-
|
|
327
|
+
return this.newTree([
|
|
375
328
|
...(await this.slice(0, index)),
|
|
376
329
|
...(await this.slice(index + 1)),
|
|
377
|
-
];
|
|
378
|
-
return this.newTree(updated);
|
|
330
|
+
]);
|
|
379
331
|
}
|
|
380
332
|
|
|
381
333
|
async atIndex(index: number): Promise<NodeEntry | null> {
|
|
@@ -389,12 +341,11 @@ export class MST {
|
|
|
389
341
|
}
|
|
390
342
|
|
|
391
343
|
async spliceIn(entry: NodeEntry, index: number): Promise<MST> {
|
|
392
|
-
|
|
344
|
+
return this.newTree([
|
|
393
345
|
...(await this.slice(0, index)),
|
|
394
346
|
entry,
|
|
395
347
|
...(await this.slice(index)),
|
|
396
|
-
];
|
|
397
|
-
return this.newTree(update);
|
|
348
|
+
]);
|
|
398
349
|
}
|
|
399
350
|
|
|
400
351
|
async replaceWithSplit(
|
|
@@ -457,9 +408,8 @@ export class MST {
|
|
|
457
408
|
merged,
|
|
458
409
|
...toMergeEntries.slice(1),
|
|
459
410
|
]);
|
|
460
|
-
} else {
|
|
461
|
-
return this.newTree([...thisEntries, ...toMergeEntries]);
|
|
462
411
|
}
|
|
412
|
+
return this.newTree([...thisEntries, ...toMergeEntries]);
|
|
463
413
|
}
|
|
464
414
|
|
|
465
415
|
async append(entry: NodeEntry): Promise<MST> {
|
|
@@ -486,7 +436,7 @@ export class MST {
|
|
|
486
436
|
|
|
487
437
|
async findGtOrEqualLeafIndex(key: string): Promise<number> {
|
|
488
438
|
const entries = await this.getEntries();
|
|
489
|
-
const maybeIndex = entries.findIndex(entry => entry.isLeaf() && entry.key >= key);
|
|
439
|
+
const maybeIndex = entries.findIndex((entry) => entry.isLeaf() && entry.key >= key);
|
|
490
440
|
return maybeIndex >= 0 ? maybeIndex : entries.length;
|
|
491
441
|
}
|
|
492
442
|
|
|
@@ -535,109 +485,3 @@ export class MST {
|
|
|
535
485
|
return false;
|
|
536
486
|
}
|
|
537
487
|
}
|
|
538
|
-
|
|
539
|
-
/**
|
|
540
|
-
* Leaf node in the MST
|
|
541
|
-
*/
|
|
542
|
-
export class Leaf {
|
|
543
|
-
constructor(
|
|
544
|
-
public key: string,
|
|
545
|
-
public value: CID,
|
|
546
|
-
) {}
|
|
547
|
-
|
|
548
|
-
isTree(): this is MST {
|
|
549
|
-
return false;
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
isLeaf(): this is Leaf {
|
|
553
|
-
return true;
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
equals(entry: NodeEntry): boolean {
|
|
557
|
-
if (entry.isLeaf()) {
|
|
558
|
-
return this.key === entry.key && this.value.equals(entry.value);
|
|
559
|
-
}
|
|
560
|
-
return false;
|
|
561
|
-
}
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
// Utility functions
|
|
565
|
-
|
|
566
|
-
async function layerForEntries(entries: NodeEntry[]): Promise<number | null> {
|
|
567
|
-
const firstLeaf = entries.find(entry => entry.isLeaf());
|
|
568
|
-
if (!firstLeaf || firstLeaf.isTree()) return null;
|
|
569
|
-
return await util.leadingZerosOnHash(firstLeaf.key);
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
async function deserializeNodeData(
|
|
573
|
-
storage: ReadableBlockstore,
|
|
574
|
-
data: NodeData,
|
|
575
|
-
opts?: Partial<MstOpts>,
|
|
576
|
-
): Promise<NodeEntry[]> {
|
|
577
|
-
const { layer } = opts || {};
|
|
578
|
-
const entries: NodeEntry[] = [];
|
|
579
|
-
|
|
580
|
-
if (data.l !== null) {
|
|
581
|
-
entries.push(MST.load(storage, data.l, { layer: layer ? layer - 1 : undefined }));
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
let lastKey = '';
|
|
585
|
-
for (const entry of data.e) {
|
|
586
|
-
const keyStr = uint8arrays.toString(entry.k, 'ascii');
|
|
587
|
-
const key = lastKey.slice(0, entry.p) + keyStr;
|
|
588
|
-
util.ensureValidMstKey(key);
|
|
589
|
-
entries.push(new Leaf(key, entry.v));
|
|
590
|
-
lastKey = key;
|
|
591
|
-
|
|
592
|
-
if (entry.t !== null) {
|
|
593
|
-
entries.push(MST.load(storage, entry.t, { layer: layer ? layer - 1 : undefined }));
|
|
594
|
-
}
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
return entries;
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
function serializeNodeData(entries: NodeEntry[]): NodeData {
|
|
601
|
-
const data: NodeData = { l: null, e: [] };
|
|
602
|
-
let i = 0;
|
|
603
|
-
|
|
604
|
-
if (entries[0]?.isTree()) {
|
|
605
|
-
i++;
|
|
606
|
-
data.l = entries[0].pointer;
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
let lastKey = '';
|
|
610
|
-
while (i < entries.length) {
|
|
611
|
-
const leaf = entries[i];
|
|
612
|
-
const next = entries[i + 1];
|
|
613
|
-
|
|
614
|
-
if (!leaf.isLeaf()) {
|
|
615
|
-
throw new Error('Not a valid node: two subtrees next to each other');
|
|
616
|
-
}
|
|
617
|
-
i++;
|
|
618
|
-
|
|
619
|
-
let subtree: CID | null = null;
|
|
620
|
-
if (next?.isTree()) {
|
|
621
|
-
subtree = next.pointer;
|
|
622
|
-
i++;
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
util.ensureValidMstKey(leaf.key);
|
|
626
|
-
const prefixLen = util.countPrefixLen(lastKey, leaf.key);
|
|
627
|
-
data.e.push({
|
|
628
|
-
p: prefixLen,
|
|
629
|
-
k: uint8arrays.fromString(leaf.key.slice(prefixLen), 'ascii'),
|
|
630
|
-
v: leaf.value,
|
|
631
|
-
t: subtree,
|
|
632
|
-
});
|
|
633
|
-
|
|
634
|
-
lastKey = leaf.key;
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
return data;
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
async function cidForEntries(entries: NodeEntry[]): Promise<CID> {
|
|
641
|
-
const data = serializeNodeData(entries);
|
|
642
|
-
return util.cidForCbor(data);
|
|
643
|
-
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { CID } from 'multiformats/cid';
|
|
2
|
+
import * as uint8arrays from 'uint8arrays';
|
|
3
|
+
import type { ReadableBlockstore } from './blockstore';
|
|
4
|
+
import * as util from './util';
|
|
5
|
+
import { Leaf } from './leaf';
|
|
6
|
+
import type { NodeData, NodeEntry, MstOpts } from './types';
|
|
7
|
+
|
|
8
|
+
// Caller-supplied loader keeps serialize.ts free of a back-reference to mst.ts.
|
|
9
|
+
// Previously the cycle was broken by `await import('./mst')` inside this
|
|
10
|
+
// function — that worked but added a microtask per deserialize and depended on
|
|
11
|
+
// module-eval order. Inverting the dependency lets both files import each
|
|
12
|
+
// other's types without runtime hacks.
|
|
13
|
+
export type SubtreeLoader = (
|
|
14
|
+
storage: ReadableBlockstore,
|
|
15
|
+
cid: CID,
|
|
16
|
+
opts?: Partial<MstOpts>,
|
|
17
|
+
) => NodeEntry;
|
|
18
|
+
|
|
19
|
+
export async function layerForEntries(entries: NodeEntry[]): Promise<number | null> {
|
|
20
|
+
const firstLeaf = entries.find((entry) => entry.isLeaf());
|
|
21
|
+
if (!firstLeaf || firstLeaf.isTree()) return null;
|
|
22
|
+
return util.leadingZerosOnHash(firstLeaf.key);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function deserializeNodeData(
|
|
26
|
+
storage: ReadableBlockstore,
|
|
27
|
+
data: NodeData,
|
|
28
|
+
loadSubtree: SubtreeLoader,
|
|
29
|
+
opts?: Partial<MstOpts>,
|
|
30
|
+
): Promise<NodeEntry[]> {
|
|
31
|
+
const { layer } = opts || {};
|
|
32
|
+
const entries: NodeEntry[] = [];
|
|
33
|
+
|
|
34
|
+
if (data.l !== null) {
|
|
35
|
+
entries.push(loadSubtree(storage, data.l, { layer: layer ? layer - 1 : undefined }));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
let lastKey = '';
|
|
39
|
+
for (const entry of data.e) {
|
|
40
|
+
const keyStr = uint8arrays.toString(entry.k, 'ascii');
|
|
41
|
+
const key = lastKey.slice(0, entry.p) + keyStr;
|
|
42
|
+
util.ensureValidMstKey(key);
|
|
43
|
+
entries.push(new Leaf(key, entry.v));
|
|
44
|
+
lastKey = key;
|
|
45
|
+
|
|
46
|
+
if (entry.t !== null) {
|
|
47
|
+
entries.push(loadSubtree(storage, entry.t, { layer: layer ? layer - 1 : undefined }));
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return entries;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function serializeNodeData(entries: NodeEntry[]): NodeData {
|
|
55
|
+
const data: NodeData = { l: null, e: [] };
|
|
56
|
+
let i = 0;
|
|
57
|
+
|
|
58
|
+
if (entries[0]?.isTree()) {
|
|
59
|
+
i++;
|
|
60
|
+
data.l = entries[0].pointer;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
let lastKey = '';
|
|
64
|
+
while (i < entries.length) {
|
|
65
|
+
const leaf = entries[i];
|
|
66
|
+
const next = entries[i + 1];
|
|
67
|
+
|
|
68
|
+
if (!leaf.isLeaf()) {
|
|
69
|
+
throw new Error('Not a valid node: two subtrees next to each other');
|
|
70
|
+
}
|
|
71
|
+
i++;
|
|
72
|
+
|
|
73
|
+
let subtree: import('multiformats/cid').CID | null = null;
|
|
74
|
+
if (next?.isTree()) {
|
|
75
|
+
subtree = next.pointer;
|
|
76
|
+
i++;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
util.ensureValidMstKey(leaf.key);
|
|
80
|
+
const prefixLen = util.countPrefixLen(lastKey, leaf.key);
|
|
81
|
+
data.e.push({
|
|
82
|
+
p: prefixLen,
|
|
83
|
+
k: uint8arrays.fromString(leaf.key.slice(prefixLen), 'ascii'),
|
|
84
|
+
v: leaf.value,
|
|
85
|
+
t: subtree,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
lastKey = leaf.key;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return data;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export async function cidForEntries(entries: NodeEntry[]): Promise<import('multiformats/cid').CID> {
|
|
95
|
+
const data = serializeNodeData(entries);
|
|
96
|
+
return util.cidForCbor(data);
|
|
97
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { CID } from 'multiformats/cid';
|
|
2
|
+
import type { MST } from './mst';
|
|
3
|
+
import type { Leaf } from './leaf';
|
|
4
|
+
|
|
5
|
+
export interface NodeData {
|
|
6
|
+
l: CID | null;
|
|
7
|
+
e: TreeEntry[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface TreeEntry {
|
|
11
|
+
p: number;
|
|
12
|
+
k: Uint8Array;
|
|
13
|
+
v: CID;
|
|
14
|
+
t: CID | null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type NodeEntry = MST | Leaf;
|
|
18
|
+
|
|
19
|
+
export interface MstOpts {
|
|
20
|
+
layer: number;
|
|
21
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { decodeProtectedHeader, importJWK, compactVerify, type JWK as JoseJWK } from 'jose';
|
|
2
|
+
|
|
3
|
+
export function isHttpsUrl(u: string): boolean {
|
|
4
|
+
try {
|
|
5
|
+
const url = new URL(u);
|
|
6
|
+
if (url.protocol !== 'https:') return false;
|
|
7
|
+
const host = url.hostname.toLowerCase();
|
|
8
|
+
if (host === 'localhost' || host.endsWith('.local')) return false;
|
|
9
|
+
if (/^(\d+\.){3}\d+$/.test(host)) return false;
|
|
10
|
+
return true;
|
|
11
|
+
} catch {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function fetchClientMetadata(client_id: string): Promise<any> {
|
|
17
|
+
const ctl = new AbortController();
|
|
18
|
+
const t = setTimeout(() => ctl.abort(), 3000);
|
|
19
|
+
try {
|
|
20
|
+
const response = await fetch(client_id, { signal: ctl.signal });
|
|
21
|
+
if (!response.ok) throw new Error(`client metadata fetch failed: ${response.status}`);
|
|
22
|
+
const ctype = response.headers.get('content-type') || '';
|
|
23
|
+
if (!ctype.includes('application/json') && !ctype.includes('json'))
|
|
24
|
+
throw new Error('client metadata must be JSON');
|
|
25
|
+
return await response.json();
|
|
26
|
+
} finally {
|
|
27
|
+
clearTimeout(t);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// removed local b64url/DER helpers in favor of jose
|
|
32
|
+
|
|
33
|
+
export async function verifyClientAssertion(client_id: string, issuerOrigin: string, assertionJwt: string, jwks: any): Promise<boolean> {
|
|
34
|
+
try {
|
|
35
|
+
const [h, p] = assertionJwt.split('.');
|
|
36
|
+
if (!h || !p) return false;
|
|
37
|
+
const header = decodeProtectedHeader(assertionJwt) as any;
|
|
38
|
+
if (header.alg !== 'ES256') return false;
|
|
39
|
+
const keys: any[] = Array.isArray(jwks?.keys) ? jwks.keys : [];
|
|
40
|
+
if (!keys.length) return false;
|
|
41
|
+
const byKid = typeof header.kid === 'string' ? keys.find((k) => k.kid === header.kid) : null;
|
|
42
|
+
const candidates = byKid ? [byKid] : keys;
|
|
43
|
+
|
|
44
|
+
let payload: any | null = null;
|
|
45
|
+
for (const jwk of candidates) {
|
|
46
|
+
try {
|
|
47
|
+
const key = await importJWK(jwk as JoseJWK, 'ES256');
|
|
48
|
+
const verified = await compactVerify(assertionJwt, key);
|
|
49
|
+
payload = JSON.parse(new TextDecoder().decode(verified.payload));
|
|
50
|
+
break;
|
|
51
|
+
} catch {
|
|
52
|
+
// Try the next JWK candidate; only the final no-payload check matters.
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (!payload) return false;
|
|
56
|
+
|
|
57
|
+
const now = Math.floor(Date.now() / 1000);
|
|
58
|
+
if (payload.iss !== client_id) return false;
|
|
59
|
+
if (payload.sub !== client_id) return false;
|
|
60
|
+
if (payload.aud !== issuerOrigin) return false;
|
|
61
|
+
if (typeof payload.iat !== 'number' || now - payload.iat > 300) return false;
|
|
62
|
+
if (typeof payload.jti !== 'string' || payload.jti.length < 8) return false;
|
|
63
|
+
return true;
|
|
64
|
+
} catch {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
}
|