@alteran/astro 0.3.9 → 0.6.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.
Files changed (150) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +19 -30
  3. package/index.js +34 -28
  4. package/migrations/0007_bored_spitfire.sql +26 -0
  5. package/migrations/0008_furry_ozymandias.sql +2 -0
  6. package/migrations/meta/0007_snapshot.json +534 -0
  7. package/migrations/meta/0008_snapshot.json +548 -0
  8. package/migrations/meta/_journal.json +14 -0
  9. package/package.json +10 -9
  10. package/src/app.ts +8 -4
  11. package/src/db/account.ts +25 -6
  12. package/src/db/client.ts +1 -1
  13. package/src/db/dal.ts +34 -23
  14. package/src/db/repo.ts +38 -38
  15. package/src/db/schema.ts +5 -1
  16. package/src/db/seed.ts +5 -13
  17. package/src/entrypoints/server.ts +2 -22
  18. package/src/handlers/debug.ts +1 -1
  19. package/src/handlers/ready.ts +1 -1
  20. package/src/handlers/root.ts +4 -4
  21. package/src/handlers/xrpc.server.refreshSession.ts +6 -6
  22. package/src/lib/account-state.ts +156 -0
  23. package/src/lib/actor.ts +29 -13
  24. package/src/lib/appview/auth-policy.ts +66 -0
  25. package/src/lib/appview/did-resolver.ts +233 -0
  26. package/src/lib/appview/proxy.ts +221 -0
  27. package/src/lib/appview/service-config.ts +61 -0
  28. package/src/lib/appview/service-jwt.ts +93 -0
  29. package/src/lib/appview/types.ts +25 -0
  30. package/src/lib/appview.ts +5 -532
  31. package/src/lib/auth-errors.ts +24 -0
  32. package/src/lib/auth.ts +63 -15
  33. package/src/lib/blockstore-gc.ts +6 -5
  34. package/src/lib/cache.ts +30 -4
  35. package/src/lib/chat.ts +20 -14
  36. package/src/lib/commit-log-pruning.ts +2 -2
  37. package/src/lib/commit.ts +26 -36
  38. package/src/lib/config.ts +26 -15
  39. package/src/lib/did-document.ts +32 -0
  40. package/src/lib/errors.ts +54 -0
  41. package/src/lib/feed.ts +18 -19
  42. package/src/lib/firehose/frames.ts +87 -47
  43. package/src/lib/firehose/validation.ts +3 -3
  44. package/src/lib/jwt.ts +85 -177
  45. package/src/lib/labeler.ts +43 -30
  46. package/src/lib/logger.ts +4 -0
  47. package/src/lib/mst/block-map.ts +172 -0
  48. package/src/lib/mst/blockstore.ts +56 -93
  49. package/src/lib/mst/index.ts +1 -0
  50. package/src/lib/mst/leaf.ts +25 -0
  51. package/src/lib/mst/mst.ts +81 -237
  52. package/src/lib/mst/serialize.ts +97 -0
  53. package/src/lib/mst/types.ts +21 -0
  54. package/src/lib/oauth/clients.ts +67 -0
  55. package/src/lib/oauth/dpop-errors.ts +15 -0
  56. package/src/lib/oauth/dpop.ts +150 -0
  57. package/src/lib/oauth/resource.ts +199 -0
  58. package/src/lib/oauth/store.ts +77 -0
  59. package/src/lib/preferences.ts +12 -37
  60. package/src/lib/ratelimit.ts +4 -4
  61. package/src/lib/refresh-session.ts +161 -0
  62. package/src/lib/relay.ts +10 -8
  63. package/src/lib/secrets.ts +6 -7
  64. package/src/lib/sequencer.ts +14 -5
  65. package/src/lib/service-auth.ts +184 -0
  66. package/src/lib/session-tokens.ts +28 -76
  67. package/src/lib/streaming-car.ts +3 -0
  68. package/src/lib/tracing.ts +4 -3
  69. package/src/lib/util.ts +65 -15
  70. package/src/middleware.ts +1 -1
  71. package/src/pages/.well-known/did.json.ts +27 -30
  72. package/src/pages/.well-known/oauth-authorization-server.ts +31 -0
  73. package/src/pages/.well-known/oauth-protected-resource.ts +22 -0
  74. package/src/pages/debug/blob/[...key].ts +2 -2
  75. package/src/pages/debug/db/bootstrap.ts +1 -1
  76. package/src/pages/debug/db/commits.ts +1 -1
  77. package/src/pages/debug/gc/blobs.ts +1 -1
  78. package/src/pages/debug/record.ts +1 -1
  79. package/src/pages/debug/sequencer.ts +28 -0
  80. package/src/pages/health.ts +4 -4
  81. package/src/pages/oauth/authorize.ts +78 -0
  82. package/src/pages/oauth/consent.ts +80 -0
  83. package/src/pages/oauth/par.ts +121 -0
  84. package/src/pages/oauth/token.ts +158 -0
  85. package/src/pages/ready.ts +2 -2
  86. package/src/pages/xrpc/[...nsid].ts +61 -0
  87. package/src/pages/xrpc/app.bsky.actor.getPreferences.ts +12 -13
  88. package/src/pages/xrpc/app.bsky.actor.putPreferences.ts +23 -23
  89. package/src/pages/xrpc/app.bsky.unspecced.getAgeAssuranceState.ts +9 -2
  90. package/src/pages/xrpc/chat.bsky.convo.getLog.ts +9 -2
  91. package/src/pages/xrpc/chat.bsky.convo.listConvos.ts +9 -2
  92. package/src/pages/xrpc/com.atproto.identity.getRecommendedDidCredentials.ts +43 -41
  93. package/src/pages/xrpc/com.atproto.identity.requestPlcOperationSignature.ts +10 -3
  94. package/src/pages/xrpc/com.atproto.identity.resolveHandle.ts +40 -9
  95. package/src/pages/xrpc/com.atproto.identity.signPlcOperation.ts +41 -29
  96. package/src/pages/xrpc/com.atproto.identity.submitPlcOperation.ts +20 -6
  97. package/src/pages/xrpc/com.atproto.identity.updateHandle.ts +1 -1
  98. package/src/pages/xrpc/com.atproto.repo.applyWrites.ts +101 -11
  99. package/src/pages/xrpc/com.atproto.repo.createRecord.ts +44 -14
  100. package/src/pages/xrpc/com.atproto.repo.deleteRecord.ts +41 -13
  101. package/src/pages/xrpc/com.atproto.repo.describeRepo.ts +2 -2
  102. package/src/pages/xrpc/com.atproto.repo.getRecord.ts +14 -1
  103. package/src/pages/xrpc/com.atproto.repo.listMissingBlobs.ts +14 -6
  104. package/src/pages/xrpc/com.atproto.repo.listRecords.ts +1 -1
  105. package/src/pages/xrpc/com.atproto.repo.putRecord.ts +42 -14
  106. package/src/pages/xrpc/com.atproto.repo.uploadBlob.ts +76 -15
  107. package/src/pages/xrpc/com.atproto.server.checkAccountStatus.ts +20 -8
  108. package/src/pages/xrpc/com.atproto.server.createSession.ts +32 -12
  109. package/src/pages/xrpc/com.atproto.server.describeServer.ts +1 -1
  110. package/src/pages/xrpc/com.atproto.server.getServiceAuth.ts +12 -5
  111. package/src/pages/xrpc/com.atproto.server.getSession.ts +22 -8
  112. package/src/pages/xrpc/com.atproto.server.refreshSession.ts +30 -72
  113. package/src/pages/xrpc/com.atproto.sync.getBlob.ts +72 -23
  114. package/src/pages/xrpc/com.atproto.sync.getCheckout.json.ts +1 -1
  115. package/src/pages/xrpc/com.atproto.sync.getCheckout.ts +1 -1
  116. package/src/pages/xrpc/com.atproto.sync.getHead.ts +7 -2
  117. package/src/pages/xrpc/com.atproto.sync.getLatestCommit.ts +1 -1
  118. package/src/pages/xrpc/com.atproto.sync.getRecord.ts +5 -27
  119. package/src/pages/xrpc/com.atproto.sync.getRepo.json.ts +1 -1
  120. package/src/pages/xrpc/com.atproto.sync.getRepo.ts +50 -5
  121. package/src/pages/xrpc/com.atproto.sync.getRepoStatus.ts +58 -0
  122. package/src/pages/xrpc/com.atproto.sync.listBlobs.ts +2 -2
  123. package/src/pages/xrpc/com.atproto.sync.listRepos.ts +5 -3
  124. package/src/services/car.ts +209 -57
  125. package/src/services/r2-blob-store.ts +4 -4
  126. package/src/services/repo/blockstore-ops.ts +29 -0
  127. package/src/services/repo/operations.ts +133 -0
  128. package/src/services/repo-manager.ts +203 -254
  129. package/src/worker/runtime.ts +56 -11
  130. package/src/worker/sequencer/broadcast.ts +91 -0
  131. package/src/worker/sequencer/cid-helpers.ts +39 -0
  132. package/src/worker/sequencer/payload.ts +84 -0
  133. package/src/worker/sequencer/types.ts +36 -0
  134. package/src/worker/sequencer/upgrade.ts +141 -0
  135. package/src/worker/sequencer.ts +264 -406
  136. package/types/env.d.ts +18 -6
  137. package/src/pages/xrpc/app.bsky.actor.getProfile.ts +0 -49
  138. package/src/pages/xrpc/app.bsky.actor.getProfiles.ts +0 -51
  139. package/src/pages/xrpc/app.bsky.feed.getActorFeeds.ts +0 -25
  140. package/src/pages/xrpc/app.bsky.feed.getAuthorFeed.ts +0 -42
  141. package/src/pages/xrpc/app.bsky.feed.getFeedGenerators.ts +0 -25
  142. package/src/pages/xrpc/app.bsky.feed.getPostThread.ts +0 -37
  143. package/src/pages/xrpc/app.bsky.feed.getPosts.ts +0 -26
  144. package/src/pages/xrpc/app.bsky.feed.getSuggestedFeeds.ts +0 -23
  145. package/src/pages/xrpc/app.bsky.feed.getTimeline.ts +0 -47
  146. package/src/pages/xrpc/app.bsky.graph.getFollowers.ts +0 -29
  147. package/src/pages/xrpc/app.bsky.graph.getFollows.ts +0 -29
  148. package/src/pages/xrpc/app.bsky.notification.getUnreadCount.ts +0 -20
  149. package/src/pages/xrpc/app.bsky.notification.listNotifications.ts +0 -27
  150. package/src/pages/xrpc/app.bsky.unspecced.getSuggestedFeeds.ts +0 -23
@@ -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
- * MST Node Data Structure
9
- * Represents the CBOR-encoded format of an MST node
10
- */
11
- export interface NodeData {
12
- l: CID | null; // left-most subtree
13
- e: TreeEntry[]; // entries (leaves with optional right subtrees)
14
- }
15
-
16
- /**
17
- * Tree Entry in MST node
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
- * An ordered, insert-order-independent, deterministic tree structure.
39
- * Keys are laid out in alphabetic order, with each key hashed to determine
40
- * which layer it belongs to based on leading zeros (~4 fanout, 2 bits per layer).
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
- // Update any outdated child pointers first
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
- * Add a new key/value pair to the MST
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
- } else if (keyZeros < layer) {
218
- // Key belongs on a lower layer
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
- const updated: NodeEntry[] = [];
243
- if (left) updated.push(left);
244
- updated.push(new Leaf(key, value));
245
- if (right) updated.push(right);
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
- const newRoot = await MST.create(this.storage, updated, { layer: keyZeros });
248
- newRoot.outdatedPointer = true;
249
- return newRoot;
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
- } else {
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
- const update = [
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
- const updated = [
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
- const update = [
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
+ }