@comapeo/core 2.0.1 → 2.2.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/dist/blob-store/downloader.d.ts +43 -0
- package/dist/blob-store/downloader.d.ts.map +1 -0
- package/dist/blob-store/entries-stream.d.ts +13 -0
- package/dist/blob-store/entries-stream.d.ts.map +1 -0
- package/dist/blob-store/hyperdrive-index.d.ts +20 -0
- package/dist/blob-store/hyperdrive-index.d.ts.map +1 -0
- package/dist/blob-store/index.d.ts +34 -29
- package/dist/blob-store/index.d.ts.map +1 -1
- package/dist/blob-store/utils.d.ts +27 -0
- package/dist/blob-store/utils.d.ts.map +1 -0
- package/dist/constants.d.ts +2 -1
- package/dist/constants.d.ts.map +1 -1
- package/dist/core-manager/index.d.ts +11 -1
- package/dist/core-manager/index.d.ts.map +1 -1
- package/dist/core-ownership.d.ts.map +1 -1
- package/dist/datastore/index.d.ts +5 -4
- package/dist/datastore/index.d.ts.map +1 -1
- package/dist/datatype/index.d.ts +5 -1
- package/dist/discovery/local-discovery.d.ts.map +1 -1
- package/dist/errors.d.ts +6 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/fastify-plugins/blobs.d.ts.map +1 -1
- package/dist/fastify-plugins/maps.d.ts.map +1 -1
- package/dist/generated/extensions.d.ts +31 -0
- package/dist/generated/extensions.d.ts.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/lib/drizzle-helpers.d.ts +6 -0
- package/dist/lib/drizzle-helpers.d.ts.map +1 -0
- package/dist/lib/error.d.ts +51 -0
- package/dist/lib/error.d.ts.map +1 -0
- package/dist/lib/get-own.d.ts +9 -0
- package/dist/lib/get-own.d.ts.map +1 -0
- package/dist/lib/is-hostname-ip-address.d.ts +17 -0
- package/dist/lib/is-hostname-ip-address.d.ts.map +1 -0
- package/dist/lib/ws-core-replicator.d.ts +11 -0
- package/dist/lib/ws-core-replicator.d.ts.map +1 -0
- package/dist/mapeo-manager.d.ts +18 -22
- package/dist/mapeo-manager.d.ts.map +1 -1
- package/dist/mapeo-project.d.ts +459 -26
- package/dist/mapeo-project.d.ts.map +1 -1
- package/dist/member-api.d.ts +44 -1
- package/dist/member-api.d.ts.map +1 -1
- package/dist/roles.d.ts.map +1 -1
- package/dist/schema/client.d.ts +17 -5
- package/dist/schema/client.d.ts.map +1 -1
- package/dist/schema/project.d.ts +212 -2
- package/dist/schema/project.d.ts.map +1 -1
- package/dist/sync/core-sync-state.d.ts +20 -15
- package/dist/sync/core-sync-state.d.ts.map +1 -1
- package/dist/sync/namespace-sync-state.d.ts +13 -1
- package/dist/sync/namespace-sync-state.d.ts.map +1 -1
- package/dist/sync/peer-sync-controller.d.ts +1 -1
- package/dist/sync/peer-sync-controller.d.ts.map +1 -1
- package/dist/sync/sync-api.d.ts +47 -2
- package/dist/sync/sync-api.d.ts.map +1 -1
- package/dist/sync/sync-state.d.ts +12 -0
- package/dist/sync/sync-state.d.ts.map +1 -1
- package/dist/translation-api.d.ts +2 -2
- package/dist/translation-api.d.ts.map +1 -1
- package/dist/types.d.ts +10 -2
- package/dist/types.d.ts.map +1 -1
- package/drizzle/client/0001_chubby_cargill.sql +12 -0
- package/drizzle/client/meta/0001_snapshot.json +208 -0
- package/drizzle/client/meta/_journal.json +7 -0
- package/drizzle/project/0001_medical_wendell_rand.sql +22 -0
- package/drizzle/project/meta/0001_snapshot.json +1267 -0
- package/drizzle/project/meta/_journal.json +7 -0
- package/package.json +14 -5
- package/src/blob-store/downloader.js +130 -0
- package/src/blob-store/entries-stream.js +81 -0
- package/src/blob-store/hyperdrive-index.js +122 -0
- package/src/blob-store/index.js +59 -117
- package/src/blob-store/utils.js +54 -0
- package/src/constants.js +4 -1
- package/src/core-manager/index.js +60 -3
- package/src/core-ownership.js +2 -4
- package/src/datastore/README.md +1 -2
- package/src/datastore/index.js +8 -8
- package/src/datatype/index.d.ts +5 -1
- package/src/datatype/index.js +22 -9
- package/src/discovery/local-discovery.js +2 -1
- package/src/errors.js +11 -2
- package/src/fastify-plugins/blobs.js +17 -1
- package/src/fastify-plugins/maps.js +2 -1
- package/src/generated/extensions.d.ts +31 -0
- package/src/generated/extensions.js +150 -0
- package/src/generated/extensions.ts +181 -0
- package/src/index.js +10 -0
- package/src/invite-api.js +1 -1
- package/src/lib/drizzle-helpers.js +79 -0
- package/src/lib/error.js +71 -0
- package/src/lib/get-own.js +10 -0
- package/src/lib/is-hostname-ip-address.js +26 -0
- package/src/lib/ws-core-replicator.js +47 -0
- package/src/mapeo-manager.js +74 -45
- package/src/mapeo-project.js +238 -58
- package/src/member-api.js +295 -2
- package/src/roles.js +38 -32
- package/src/schema/client.js +4 -3
- package/src/schema/project.js +7 -0
- package/src/sync/core-sync-state.js +39 -23
- package/src/sync/namespace-sync-state.js +22 -0
- package/src/sync/peer-sync-controller.js +1 -0
- package/src/sync/sync-api.js +197 -3
- package/src/sync/sync-state.js +18 -0
- package/src/translation-api.js +5 -9
- package/src/types.ts +12 -3
- package/dist/blob-store/live-download.d.ts +0 -107
- package/dist/blob-store/live-download.d.ts.map +0 -1
- package/dist/lib/timing-safe-equal.d.ts +0 -15
- package/dist/lib/timing-safe-equal.d.ts.map +0 -1
- package/src/blob-store/live-download.js +0 -373
- package/src/lib/timing-safe-equal.js +0 -34
|
@@ -1,373 +0,0 @@
|
|
|
1
|
-
import { TypedEmitter } from 'tiny-typed-emitter'
|
|
2
|
-
import { once } from 'node:events'
|
|
3
|
-
import SubEncoder from 'sub-encoder'
|
|
4
|
-
|
|
5
|
-
const keyEncoding = new SubEncoder('files', 'utf-8')
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* @typedef {object} BlobDownloadState
|
|
9
|
-
* @property {number} haveCount The number of files already downloaded
|
|
10
|
-
* @property {number} haveBytes The bytes already downloaded
|
|
11
|
-
* @property {number} wantCount The number of files pending download
|
|
12
|
-
* @property {number} wantBytes The bytes pending download
|
|
13
|
-
* @property {null} error If status = 'error' then this will be an Error object
|
|
14
|
-
* @property {'checking' | 'downloading' | 'downloaded' | 'aborted'} status
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
/** @typedef {Omit<BlobDownloadState, 'error' | 'status'> & { status: 'error', error: Error }} BlobDownloadStateError */
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* @typedef {object} BlobDownloadEvents
|
|
21
|
-
* @property {(state: BlobDownloadState | BlobDownloadStateError ) => void} state Emitted with the current download state whenever it changes (not emitted during initial 'checking' status)
|
|
22
|
-
*/
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* LiveDownload class
|
|
26
|
-
* @extends {TypedEmitter<BlobDownloadEvents>}
|
|
27
|
-
*/
|
|
28
|
-
export class LiveDownload extends TypedEmitter {
|
|
29
|
-
/** @type {Set<DriveLiveDownload>} */
|
|
30
|
-
#driveLiveDownloads = new Set()
|
|
31
|
-
#signal
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Like drive.download() but 'live', and for multiple drives
|
|
35
|
-
* @param {Iterable<import('hyperdrive')>} drives
|
|
36
|
-
* @param {import('./index.js').InternalDriveEmitter} emitter
|
|
37
|
-
* @param {object} options
|
|
38
|
-
* @param {import('../types.js').BlobFilter} [options.filter] Filter blobs of specific types and/or sizes to download
|
|
39
|
-
* @param {AbortSignal} [options.signal]
|
|
40
|
-
*/
|
|
41
|
-
constructor(drives, emitter, { filter, signal }) {
|
|
42
|
-
super()
|
|
43
|
-
this.#signal = signal
|
|
44
|
-
|
|
45
|
-
const emitState = () => {
|
|
46
|
-
this.emit('state', this.state)
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/** @param {import('hyperdrive')} drive */
|
|
50
|
-
const addDrive = (drive) => {
|
|
51
|
-
const download = new DriveLiveDownload(drive, {
|
|
52
|
-
filter,
|
|
53
|
-
signal,
|
|
54
|
-
})
|
|
55
|
-
this.#driveLiveDownloads.add(download)
|
|
56
|
-
download.on('state', emitState)
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
for (const drive of drives) addDrive(drive)
|
|
60
|
-
emitter.on('add-drive', addDrive)
|
|
61
|
-
|
|
62
|
-
signal?.addEventListener(
|
|
63
|
-
'abort',
|
|
64
|
-
() => {
|
|
65
|
-
emitter.off('add-drive', addDrive)
|
|
66
|
-
for (const download of this.#driveLiveDownloads) {
|
|
67
|
-
download.off('state', emitState)
|
|
68
|
-
}
|
|
69
|
-
},
|
|
70
|
-
{ once: true }
|
|
71
|
-
)
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* @returns {BlobDownloadState | BlobDownloadStateError}
|
|
76
|
-
*/
|
|
77
|
-
get state() {
|
|
78
|
-
return combineStates(this.#driveLiveDownloads, { signal: this.#signal })
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* LiveDownload class
|
|
84
|
-
* @extends {TypedEmitter<BlobDownloadEvents>}
|
|
85
|
-
*/
|
|
86
|
-
export class DriveLiveDownload extends TypedEmitter {
|
|
87
|
-
#haveCount = 0
|
|
88
|
-
#haveBytes = 0
|
|
89
|
-
#wantBytes = 0
|
|
90
|
-
#initialCheck = true
|
|
91
|
-
#drive
|
|
92
|
-
#folders
|
|
93
|
-
/** @type {Set<{ done(): Promise<void>, destroy(): void }>} */
|
|
94
|
-
#downloads = new Set()
|
|
95
|
-
/** @type {Error | null} */
|
|
96
|
-
#error = null
|
|
97
|
-
#signal
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Like drive.download() but 'live',
|
|
101
|
-
* @param {import('hyperdrive')} drive
|
|
102
|
-
* @param {object} options
|
|
103
|
-
* @param {import('../types.js').BlobFilter} [options.filter] Filter blobs of specific types and/or sizes to download
|
|
104
|
-
* @param {AbortSignal} [options.signal]
|
|
105
|
-
*/
|
|
106
|
-
constructor(drive, { filter, signal } = {}) {
|
|
107
|
-
super()
|
|
108
|
-
this.#drive = drive
|
|
109
|
-
this.#folders = filterToFolders(filter)
|
|
110
|
-
this.#signal = signal
|
|
111
|
-
if (signal && !signal.aborted) {
|
|
112
|
-
signal.addEventListener(
|
|
113
|
-
'abort',
|
|
114
|
-
() => {
|
|
115
|
-
for (const download of this.#downloads) download.destroy()
|
|
116
|
-
this.#downloads.clear()
|
|
117
|
-
this.emit('state', this.state)
|
|
118
|
-
},
|
|
119
|
-
{ once: true }
|
|
120
|
-
)
|
|
121
|
-
}
|
|
122
|
-
this.#start().catch(this.#handleError.bind(this))
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* @returns {BlobDownloadState | BlobDownloadStateError}
|
|
127
|
-
*/
|
|
128
|
-
get state() {
|
|
129
|
-
if (this.#error) {
|
|
130
|
-
return {
|
|
131
|
-
haveCount: this.#haveCount,
|
|
132
|
-
haveBytes: this.#haveBytes,
|
|
133
|
-
wantCount: this.#downloads.size,
|
|
134
|
-
wantBytes: this.#wantBytes,
|
|
135
|
-
error: this.#error,
|
|
136
|
-
status: 'error',
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
return {
|
|
140
|
-
haveCount: this.#haveCount,
|
|
141
|
-
haveBytes: this.#haveBytes,
|
|
142
|
-
wantCount: this.#downloads.size,
|
|
143
|
-
wantBytes: this.#wantBytes,
|
|
144
|
-
error: null,
|
|
145
|
-
status: this.#signal?.aborted
|
|
146
|
-
? 'aborted'
|
|
147
|
-
: this.#initialCheck
|
|
148
|
-
? 'checking'
|
|
149
|
-
: this.#downloads.size > 0
|
|
150
|
-
? 'downloading'
|
|
151
|
-
: 'downloaded',
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
async #start() {
|
|
156
|
-
const blobsCore = await this.#getBlobsCore()
|
|
157
|
-
/* c8 ignore next */
|
|
158
|
-
if (this.#signal?.aborted || !blobsCore) return // Can't get here in tests
|
|
159
|
-
let seq = 0
|
|
160
|
-
|
|
161
|
-
for (const folder of this.#folders) {
|
|
162
|
-
// Don't emit state during initial iteration of existing data, since this is
|
|
163
|
-
// likely fast and not useful UX feedback
|
|
164
|
-
const entryStream = this.#drive.list(folder, { recursive: true })
|
|
165
|
-
if (this.#signal) {
|
|
166
|
-
this.#signal.addEventListener('abort', () => entryStream.destroy(), {
|
|
167
|
-
once: true,
|
|
168
|
-
})
|
|
169
|
-
}
|
|
170
|
-
for await (const entry of entryStream) {
|
|
171
|
-
if (this.#signal?.aborted) return
|
|
172
|
-
seq = Math.max(seq, entry.seq)
|
|
173
|
-
const { blob } = entry.value
|
|
174
|
-
if (!blob) continue
|
|
175
|
-
await this.#processEntry(blobsCore, blob)
|
|
176
|
-
}
|
|
177
|
-
if (this.#signal?.aborted) return
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
this.#initialCheck = false
|
|
181
|
-
this.emit('state', this.state)
|
|
182
|
-
|
|
183
|
-
const bee = this.#drive.db
|
|
184
|
-
// This will also download old versions of files, but it is the only way to
|
|
185
|
-
// get a live stream from a Hyperbee, however we currently do not support
|
|
186
|
-
// edits of blobs, so this should not be an issue. `keyEncoding` is
|
|
187
|
-
// necessary because hyperdrive stores file index data under the `files`
|
|
188
|
-
// sub-encoding key
|
|
189
|
-
const historyStream = bee.createHistoryStream({
|
|
190
|
-
live: true,
|
|
191
|
-
gt: seq,
|
|
192
|
-
keyEncoding,
|
|
193
|
-
})
|
|
194
|
-
if (this.#signal) {
|
|
195
|
-
this.#signal.addEventListener('abort', () => historyStream.destroy(), {
|
|
196
|
-
once: true,
|
|
197
|
-
})
|
|
198
|
-
}
|
|
199
|
-
for await (const entry of historyStream) {
|
|
200
|
-
if (this.#signal?.aborted) return
|
|
201
|
-
const { blob } = entry.value
|
|
202
|
-
if (!blob) continue
|
|
203
|
-
if (!matchesFolder(entry.key, this.#folders)) continue
|
|
204
|
-
// TODO: consider cancelling downloads when a delete entry is found?
|
|
205
|
-
// Probably not worth the extra work.
|
|
206
|
-
if (entry.type !== 'put') continue
|
|
207
|
-
const wasDownloaded = this.state.status === 'downloaded'
|
|
208
|
-
await this.#processEntry(blobsCore, blob)
|
|
209
|
-
if (wasDownloaded && this.state.status === 'downloading') {
|
|
210
|
-
// State has changed, so emit
|
|
211
|
-
this.emit('state', this.state)
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
/* c8 ignore next 2 */
|
|
215
|
-
// Could possibly reach here if aborted after check in loop, hard to test
|
|
216
|
-
this.emit('state', this.state)
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
/**
|
|
220
|
-
* If a Hyperdrive has been added by its key and has never replicated, then
|
|
221
|
-
* drive.getBlobs() will not resolve until replication starts. Since we do not
|
|
222
|
-
* want the downloader to remain in the "checking" state forever, we catch
|
|
223
|
-
* this case and update the state before waiting for the hyperdrive hyperblobs
|
|
224
|
-
* instance. This also makes waiting for the blobs instance cancellable.
|
|
225
|
-
*
|
|
226
|
-
* @returns {Promise<import('hypercore') | undefined>}
|
|
227
|
-
*/
|
|
228
|
-
async #getBlobsCore() {
|
|
229
|
-
if (this.#drive.blobs) return this.#drive.blobs.core
|
|
230
|
-
await this.#drive.ready()
|
|
231
|
-
await this.#drive.core.update({ wait: true })
|
|
232
|
-
|
|
233
|
-
// If no peers at this stage, we are not going to be able to get the blobs
|
|
234
|
-
// until a peer appears, so consider this state "downloaded", because
|
|
235
|
-
// otherwise this will just hang as "checking"
|
|
236
|
-
if (!this.#drive.core.peers.length) {
|
|
237
|
-
this.#initialCheck = false
|
|
238
|
-
this.emit('state', this.state)
|
|
239
|
-
}
|
|
240
|
-
try {
|
|
241
|
-
const [blobs] = await once(this.#drive, 'blobs', { signal: this.#signal })
|
|
242
|
-
return blobs.core
|
|
243
|
-
} catch (e) {
|
|
244
|
-
if (e instanceof Error && e.name === 'AbortError') return
|
|
245
|
-
throw e
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
/** @param {Error} e */
|
|
250
|
-
#handleError(e) {
|
|
251
|
-
this.#error = e
|
|
252
|
-
this.emit('state', this.state)
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
/**
|
|
256
|
-
* Update state and queue missing entries for download
|
|
257
|
-
*
|
|
258
|
-
* @param {import('hypercore')} core
|
|
259
|
-
* @param {{ blockOffset: number, blockLength: number, byteLength: number }} blob
|
|
260
|
-
*/
|
|
261
|
-
async #processEntry(
|
|
262
|
-
core,
|
|
263
|
-
{ blockOffset: start, blockLength: length, byteLength }
|
|
264
|
-
) {
|
|
265
|
-
const end = start + length
|
|
266
|
-
const have = await core.has(start, end)
|
|
267
|
-
if (have) {
|
|
268
|
-
this.#haveCount++
|
|
269
|
-
this.#haveBytes += byteLength
|
|
270
|
-
} else {
|
|
271
|
-
this.#wantBytes += byteLength
|
|
272
|
-
const download = core.download({ start, end })
|
|
273
|
-
this.#downloads.add(download)
|
|
274
|
-
download
|
|
275
|
-
.done()
|
|
276
|
-
.then(() => {
|
|
277
|
-
this.#downloads.delete(download)
|
|
278
|
-
this.#haveCount++
|
|
279
|
-
this.#haveBytes += byteLength
|
|
280
|
-
this.#wantBytes -= byteLength
|
|
281
|
-
this.emit('state', this.state)
|
|
282
|
-
})
|
|
283
|
-
.catch(this.#handleError.bind(this))
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
/**
|
|
289
|
-
* Reduce multiple states into one. Factored out for unit testing because I
|
|
290
|
-
* don't trust my coding. Probably a smarter way to do this, but this works.
|
|
291
|
-
*
|
|
292
|
-
* @param {Iterable<{ state: BlobDownloadState | BlobDownloadStateError }>} liveDownloads
|
|
293
|
-
* @param {{ signal?: AbortSignal }} options
|
|
294
|
-
* @returns {BlobDownloadState | BlobDownloadStateError}
|
|
295
|
-
*/
|
|
296
|
-
export function combineStates(liveDownloads, { signal } = {}) {
|
|
297
|
-
/** @type {BlobDownloadState | BlobDownloadStateError} */
|
|
298
|
-
let combinedState = {
|
|
299
|
-
haveCount: 0,
|
|
300
|
-
haveBytes: 0,
|
|
301
|
-
wantCount: 0,
|
|
302
|
-
wantBytes: 0,
|
|
303
|
-
error: null,
|
|
304
|
-
status: 'downloaded',
|
|
305
|
-
}
|
|
306
|
-
for (const { state } of liveDownloads) {
|
|
307
|
-
combinedState.haveCount += state.haveCount
|
|
308
|
-
combinedState.haveBytes += state.haveBytes
|
|
309
|
-
combinedState.wantCount += state.wantCount
|
|
310
|
-
combinedState.wantBytes += state.wantBytes
|
|
311
|
-
if (state.status === combinedState.status) continue
|
|
312
|
-
if (state.status === 'error') {
|
|
313
|
-
combinedState = { ...combinedState, error: state.error, status: 'error' }
|
|
314
|
-
} else if (
|
|
315
|
-
state.status === 'downloading' &&
|
|
316
|
-
combinedState.status === 'downloaded'
|
|
317
|
-
) {
|
|
318
|
-
combinedState = { ...combinedState, status: 'downloading' }
|
|
319
|
-
} else if (
|
|
320
|
-
state.status === 'checking' &&
|
|
321
|
-
(combinedState.status === 'downloaded' ||
|
|
322
|
-
combinedState.status === 'downloading')
|
|
323
|
-
) {
|
|
324
|
-
combinedState = { ...combinedState, status: 'checking' }
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
if (signal?.aborted) {
|
|
328
|
-
combinedState.status = 'aborted'
|
|
329
|
-
}
|
|
330
|
-
return combinedState
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
/**
|
|
334
|
-
* Convert a filter to an array of folders that need to be downloaded
|
|
335
|
-
*
|
|
336
|
-
* @param {import('../types.js').BlobFilter} [filter]
|
|
337
|
-
* @returns {string[]} array of folders that match the filter
|
|
338
|
-
*/
|
|
339
|
-
function filterToFolders(filter) {
|
|
340
|
-
if (!filter) return ['/']
|
|
341
|
-
const folders = []
|
|
342
|
-
for (const [
|
|
343
|
-
type,
|
|
344
|
-
variants,
|
|
345
|
-
] of /** @type {import('type-fest').Entries<typeof filter>} */ (
|
|
346
|
-
Object.entries(filter)
|
|
347
|
-
)) {
|
|
348
|
-
// De-dupe variants array
|
|
349
|
-
for (const variant of new Set(variants)) {
|
|
350
|
-
folders.push(makePath({ type, variant }))
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
return folders
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
/**
|
|
357
|
-
* Returns true if the path is within one of the given folders
|
|
358
|
-
*
|
|
359
|
-
* @param {string} path
|
|
360
|
-
* @param {string[]} folders
|
|
361
|
-
* @returns {boolean}
|
|
362
|
-
*/
|
|
363
|
-
function matchesFolder(path, folders) {
|
|
364
|
-
for (const folder of folders) {
|
|
365
|
-
if (path.startsWith(folder)) return true
|
|
366
|
-
}
|
|
367
|
-
return false
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
/** @param {Pick<import('../types.js').BlobId, 'type' | 'variant'>} opts */
|
|
371
|
-
function makePath({ type, variant }) {
|
|
372
|
-
return `/${type}/${variant}`
|
|
373
|
-
}
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import * as crypto from 'node:crypto'
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* @param {string | NodeJS.ArrayBufferView} value
|
|
5
|
-
* @returns {NodeJS.ArrayBufferView}
|
|
6
|
-
*/
|
|
7
|
-
const bufferify = (value) =>
|
|
8
|
-
// We use UTF-16 because it's the only supported encoding that doesn't
|
|
9
|
-
// touch surrogate pairs. See [this post][0] for more details.
|
|
10
|
-
//
|
|
11
|
-
// [0]: https://evanhahn.com/crypto-timingsafeequal-with-strings/
|
|
12
|
-
typeof value === 'string' ? Buffer.from(value, 'utf16le') : value
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Compare two values in constant time.
|
|
16
|
-
*
|
|
17
|
-
* Useful when you want to avoid leaking data.
|
|
18
|
-
*
|
|
19
|
-
* Like `crypto.timingSafeEqual`, but works with strings and doesn't throw if
|
|
20
|
-
* lengths differ.
|
|
21
|
-
*
|
|
22
|
-
* @template {string | NodeJS.ArrayBufferView} T
|
|
23
|
-
* @param {T} a
|
|
24
|
-
* @param {T} b
|
|
25
|
-
* @returns {boolean}
|
|
26
|
-
*/
|
|
27
|
-
export default function timingSafeEqual(a, b) {
|
|
28
|
-
const bufferA = bufferify(a)
|
|
29
|
-
const bufferB = bufferify(b)
|
|
30
|
-
return (
|
|
31
|
-
bufferA.byteLength === bufferB.byteLength &&
|
|
32
|
-
crypto.timingSafeEqual(bufferA, bufferB)
|
|
33
|
-
)
|
|
34
|
-
}
|