@syncular/server 0.0.6-159 → 0.0.6-167
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/blobs/adapters/database.d.ts +26 -9
- package/dist/blobs/adapters/database.d.ts.map +1 -1
- package/dist/blobs/adapters/database.js +65 -21
- package/dist/blobs/adapters/database.js.map +1 -1
- package/dist/blobs/manager.d.ts +60 -3
- package/dist/blobs/manager.d.ts.map +1 -1
- package/dist/blobs/manager.js +227 -56
- package/dist/blobs/manager.js.map +1 -1
- package/dist/blobs/migrate.d.ts.map +1 -1
- package/dist/blobs/migrate.js +16 -8
- package/dist/blobs/migrate.js.map +1 -1
- package/dist/blobs/types.d.ts +4 -0
- package/dist/blobs/types.d.ts.map +1 -1
- package/dist/dialect/helpers.d.ts +3 -0
- package/dist/dialect/helpers.d.ts.map +1 -1
- package/dist/dialect/helpers.js +17 -0
- package/dist/dialect/helpers.js.map +1 -1
- package/dist/handlers/collection.d.ts +0 -2
- package/dist/handlers/collection.d.ts.map +1 -1
- package/dist/handlers/collection.js +5 -56
- package/dist/handlers/collection.js.map +1 -1
- package/dist/handlers/create-handler.d.ts +0 -4
- package/dist/handlers/create-handler.d.ts.map +1 -1
- package/dist/handlers/create-handler.js +6 -34
- package/dist/handlers/create-handler.js.map +1 -1
- package/dist/notify.d.ts.map +1 -1
- package/dist/notify.js +13 -37
- package/dist/notify.js.map +1 -1
- package/dist/proxy/collection.d.ts +0 -2
- package/dist/proxy/collection.d.ts.map +1 -1
- package/dist/proxy/collection.js +2 -17
- package/dist/proxy/collection.js.map +1 -1
- package/dist/proxy/handler.d.ts +1 -1
- package/dist/proxy/handler.d.ts.map +1 -1
- package/dist/proxy/handler.js +1 -2
- package/dist/proxy/handler.js.map +1 -1
- package/dist/proxy/index.d.ts +1 -1
- package/dist/proxy/index.d.ts.map +1 -1
- package/dist/proxy/index.js +1 -1
- package/dist/proxy/index.js.map +1 -1
- package/dist/proxy/oplog.d.ts.map +1 -1
- package/dist/proxy/oplog.js +1 -7
- package/dist/proxy/oplog.js.map +1 -1
- package/dist/prune.d.ts.map +1 -1
- package/dist/prune.js +1 -13
- package/dist/prune.js.map +1 -1
- package/dist/pull.d.ts.map +1 -1
- package/dist/pull.js +186 -54
- package/dist/pull.js.map +1 -1
- package/dist/push.d.ts +1 -1
- package/dist/push.d.ts.map +1 -1
- package/dist/push.js +9 -36
- package/dist/push.js.map +1 -1
- package/dist/snapshot-chunks/db-metadata.d.ts +18 -0
- package/dist/snapshot-chunks/db-metadata.d.ts.map +1 -1
- package/dist/snapshot-chunks/db-metadata.js +71 -23
- package/dist/snapshot-chunks/db-metadata.js.map +1 -1
- package/dist/snapshot-chunks.d.ts +5 -1
- package/dist/snapshot-chunks.d.ts.map +1 -1
- package/dist/snapshot-chunks.js +14 -1
- package/dist/snapshot-chunks.js.map +1 -1
- package/dist/stats.d.ts.map +1 -1
- package/dist/stats.js +1 -13
- package/dist/stats.js.map +1 -1
- package/dist/subscriptions/resolve.d.ts +1 -1
- package/dist/subscriptions/resolve.d.ts.map +1 -1
- package/dist/subscriptions/resolve.js +3 -16
- package/dist/subscriptions/resolve.js.map +1 -1
- package/dist/sync.d.ts.map +1 -1
- package/dist/sync.js +2 -4
- package/dist/sync.js.map +1 -1
- package/package.json +2 -2
- package/src/blobs/adapters/database.test.ts +7 -0
- package/src/blobs/adapters/database.ts +119 -39
- package/src/blobs/manager.ts +339 -53
- package/src/blobs/migrate.ts +16 -8
- package/src/blobs/types.ts +4 -0
- package/src/dialect/helpers.ts +19 -0
- package/src/handlers/collection.ts +17 -86
- package/src/handlers/create-handler.ts +9 -44
- package/src/notify.ts +15 -40
- package/src/proxy/collection.ts +5 -27
- package/src/proxy/handler.ts +2 -2
- package/src/proxy/index.ts +0 -2
- package/src/proxy/oplog.ts +1 -9
- package/src/prune.ts +1 -12
- package/src/pull.ts +280 -105
- package/src/push.ts +14 -43
- package/src/snapshot-chunks/db-metadata.ts +107 -27
- package/src/snapshot-chunks.ts +18 -0
- package/src/stats.ts +1 -12
- package/src/subscriptions/resolve.ts +4 -20
- package/src/sync.ts +6 -6
|
@@ -6,7 +6,9 @@ describe('createHmacTokenSigner', () => {
|
|
|
6
6
|
const signer = createHmacTokenSigner('test-secret');
|
|
7
7
|
const payload = {
|
|
8
8
|
hash: 'sha256:abc',
|
|
9
|
+
partitionId: 'default',
|
|
9
10
|
action: 'upload' as const,
|
|
11
|
+
size: 3,
|
|
10
12
|
expiresAt: Date.now() + 60_000,
|
|
11
13
|
};
|
|
12
14
|
|
|
@@ -20,6 +22,7 @@ describe('createHmacTokenSigner', () => {
|
|
|
20
22
|
const signer = createHmacTokenSigner('test-secret');
|
|
21
23
|
const payload = {
|
|
22
24
|
hash: 'sha256:def',
|
|
25
|
+
partitionId: 'default',
|
|
23
26
|
action: 'download' as const,
|
|
24
27
|
expiresAt: Date.now() + 60_000,
|
|
25
28
|
};
|
|
@@ -40,7 +43,9 @@ describe('createHmacTokenSigner', () => {
|
|
|
40
43
|
const signer = createHmacTokenSigner('test-secret');
|
|
41
44
|
const payload = {
|
|
42
45
|
hash: 'sha256:ghi',
|
|
46
|
+
partitionId: 'default',
|
|
43
47
|
action: 'upload' as const,
|
|
48
|
+
size: 3,
|
|
44
49
|
expiresAt: Date.now() + 60_000,
|
|
45
50
|
};
|
|
46
51
|
|
|
@@ -57,7 +62,9 @@ describe('createHmacTokenSigner', () => {
|
|
|
57
62
|
const signer = createHmacTokenSigner('test-secret');
|
|
58
63
|
const payload = {
|
|
59
64
|
hash: 'sha256:jkl',
|
|
65
|
+
partitionId: 'default',
|
|
60
66
|
action: 'upload' as const,
|
|
67
|
+
size: 3,
|
|
61
68
|
expiresAt: Date.now() - 1,
|
|
62
69
|
};
|
|
63
70
|
|
|
@@ -12,6 +12,7 @@ import type {
|
|
|
12
12
|
BlobSignUploadOptions,
|
|
13
13
|
BlobStorageAdapter,
|
|
14
14
|
} from '@syncular/core';
|
|
15
|
+
import { resolveUrlFromBase } from '@syncular/core';
|
|
15
16
|
import { type Kysely, sql } from 'kysely';
|
|
16
17
|
import type { SyncBlobsDb } from '../types';
|
|
17
18
|
|
|
@@ -20,25 +21,49 @@ import type { SyncBlobsDb } from '../types';
|
|
|
20
21
|
*/
|
|
21
22
|
export interface BlobTokenSigner {
|
|
22
23
|
/**
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* @param expiresIn Expiration time in seconds
|
|
26
|
-
* @returns A signed token string
|
|
24
|
+
* Token payload for upload/download authorization.
|
|
25
|
+
* Upload tokens are bound to hash + expected byte size.
|
|
27
26
|
*/
|
|
28
27
|
sign(
|
|
29
|
-
payload:
|
|
28
|
+
payload:
|
|
29
|
+
| {
|
|
30
|
+
hash: string;
|
|
31
|
+
partitionId: string;
|
|
32
|
+
action: 'upload';
|
|
33
|
+
size: number;
|
|
34
|
+
expiresAt: number;
|
|
35
|
+
}
|
|
36
|
+
| {
|
|
37
|
+
hash: string;
|
|
38
|
+
partitionId: string;
|
|
39
|
+
action: 'download';
|
|
40
|
+
expiresAt: number;
|
|
41
|
+
},
|
|
30
42
|
expiresIn: number
|
|
31
43
|
): Promise<string>;
|
|
32
44
|
|
|
33
45
|
/**
|
|
34
|
-
*
|
|
35
|
-
* @
|
|
46
|
+
* Sign a token for blob upload/download authorization.
|
|
47
|
+
* @param payload The data to sign
|
|
48
|
+
* @param expiresIn Expiration time in seconds
|
|
49
|
+
* @returns A signed token string
|
|
36
50
|
*/
|
|
37
|
-
verify(token: string): Promise<
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
51
|
+
verify(token: string): Promise<
|
|
52
|
+
| {
|
|
53
|
+
hash: string;
|
|
54
|
+
partitionId: string;
|
|
55
|
+
action: 'upload';
|
|
56
|
+
size: number;
|
|
57
|
+
expiresAt: number;
|
|
58
|
+
}
|
|
59
|
+
| {
|
|
60
|
+
hash: string;
|
|
61
|
+
partitionId: string;
|
|
62
|
+
action: 'download';
|
|
63
|
+
expiresAt: number;
|
|
64
|
+
}
|
|
65
|
+
| null
|
|
66
|
+
>;
|
|
42
67
|
}
|
|
43
68
|
|
|
44
69
|
/**
|
|
@@ -92,15 +117,37 @@ export function createHmacTokenSigner(secret: string): BlobTokenSigner {
|
|
|
92
117
|
if (!isValidSig) return null;
|
|
93
118
|
|
|
94
119
|
try {
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
120
|
+
const parsed = JSON.parse(atob(dataB64)) as Record<string, unknown>;
|
|
121
|
+
if (typeof parsed.hash !== 'string') return null;
|
|
122
|
+
if (typeof parsed.partitionId !== 'string') return null;
|
|
123
|
+
if (typeof parsed.expiresAt !== 'number') return null;
|
|
124
|
+
if (Date.now() > parsed.expiresAt) return null;
|
|
125
|
+
|
|
126
|
+
if (parsed.action === 'download') {
|
|
127
|
+
return {
|
|
128
|
+
hash: parsed.hash,
|
|
129
|
+
partitionId: parsed.partitionId,
|
|
130
|
+
action: 'download',
|
|
131
|
+
expiresAt: parsed.expiresAt,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (
|
|
136
|
+
parsed.action === 'upload' &&
|
|
137
|
+
typeof parsed.size === 'number' &&
|
|
138
|
+
Number.isFinite(parsed.size) &&
|
|
139
|
+
parsed.size >= 0
|
|
140
|
+
) {
|
|
141
|
+
return {
|
|
142
|
+
hash: parsed.hash,
|
|
143
|
+
partitionId: parsed.partitionId,
|
|
144
|
+
action: 'upload',
|
|
145
|
+
size: parsed.size,
|
|
146
|
+
expiresAt: parsed.expiresAt,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
100
149
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
return data;
|
|
150
|
+
return null;
|
|
104
151
|
} catch {
|
|
105
152
|
return null;
|
|
106
153
|
}
|
|
@@ -156,22 +203,29 @@ export function createDatabaseBlobStorageAdapter<DB extends SyncBlobsDb>(
|
|
|
156
203
|
options: DatabaseBlobStorageAdapterOptions<DB>
|
|
157
204
|
): BlobStorageAdapter {
|
|
158
205
|
const { db, baseUrl, tokenSigner } = options;
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
const normalizedBaseUrl = baseUrl.replace(/\/$/, '');
|
|
206
|
+
const resolvePartitionId = (partitionId?: string): string =>
|
|
207
|
+
partitionId ?? 'default';
|
|
162
208
|
|
|
163
209
|
return {
|
|
164
210
|
name: 'database',
|
|
165
211
|
|
|
166
212
|
async signUpload(opts: BlobSignUploadOptions): Promise<BlobSignedUpload> {
|
|
213
|
+
const partitionId = resolvePartitionId(opts.partitionId);
|
|
167
214
|
const expiresAt = Date.now() + opts.expiresIn * 1000;
|
|
168
215
|
const token = await tokenSigner.sign(
|
|
169
|
-
{
|
|
216
|
+
{
|
|
217
|
+
hash: opts.hash,
|
|
218
|
+
partitionId,
|
|
219
|
+
action: 'upload',
|
|
220
|
+
size: opts.size,
|
|
221
|
+
expiresAt,
|
|
222
|
+
},
|
|
170
223
|
opts.expiresIn
|
|
171
224
|
);
|
|
172
225
|
|
|
173
226
|
// URL points to server's blob upload endpoint
|
|
174
|
-
const
|
|
227
|
+
const uploadPath = `/blobs/${encodeURIComponent(opts.hash)}/upload?token=${encodeURIComponent(token)}`;
|
|
228
|
+
const url = resolveUrlFromBase(baseUrl, uploadPath);
|
|
175
229
|
|
|
176
230
|
return {
|
|
177
231
|
url,
|
|
@@ -184,39 +238,53 @@ export function createDatabaseBlobStorageAdapter<DB extends SyncBlobsDb>(
|
|
|
184
238
|
},
|
|
185
239
|
|
|
186
240
|
async signDownload(opts: BlobSignDownloadOptions): Promise<string> {
|
|
241
|
+
const partitionId = resolvePartitionId(opts.partitionId);
|
|
187
242
|
const expiresAt = Date.now() + opts.expiresIn * 1000;
|
|
188
243
|
const token = await tokenSigner.sign(
|
|
189
|
-
{ hash: opts.hash, action: 'download', expiresAt },
|
|
244
|
+
{ hash: opts.hash, partitionId, action: 'download', expiresAt },
|
|
190
245
|
opts.expiresIn
|
|
191
246
|
);
|
|
192
247
|
|
|
193
|
-
return
|
|
248
|
+
return resolveUrlFromBase(
|
|
249
|
+
baseUrl,
|
|
250
|
+
`/blobs/${encodeURIComponent(opts.hash)}/download?token=${encodeURIComponent(token)}`
|
|
251
|
+
);
|
|
194
252
|
},
|
|
195
253
|
|
|
196
|
-
async exists(
|
|
254
|
+
async exists(
|
|
255
|
+
hash: string,
|
|
256
|
+
options?: { partitionId?: string }
|
|
257
|
+
): Promise<boolean> {
|
|
258
|
+
const partitionId = resolvePartitionId(options?.partitionId);
|
|
197
259
|
const rowResult = await sql<{ hash: string }>`
|
|
198
260
|
select hash
|
|
199
261
|
from ${sql.table('sync_blobs')}
|
|
200
|
-
where hash = ${hash}
|
|
262
|
+
where partition_id = ${partitionId} and hash = ${hash}
|
|
201
263
|
limit 1
|
|
202
264
|
`.execute(db);
|
|
203
265
|
return rowResult.rows.length > 0;
|
|
204
266
|
},
|
|
205
267
|
|
|
206
|
-
async delete(
|
|
268
|
+
async delete(
|
|
269
|
+
hash: string,
|
|
270
|
+
options?: { partitionId?: string }
|
|
271
|
+
): Promise<void> {
|
|
272
|
+
const partitionId = resolvePartitionId(options?.partitionId);
|
|
207
273
|
await sql`
|
|
208
274
|
delete from ${sql.table('sync_blobs')}
|
|
209
|
-
where hash = ${hash}
|
|
275
|
+
where partition_id = ${partitionId} and hash = ${hash}
|
|
210
276
|
`.execute(db);
|
|
211
277
|
},
|
|
212
278
|
|
|
213
279
|
async getMetadata(
|
|
214
|
-
hash: string
|
|
280
|
+
hash: string,
|
|
281
|
+
options?: { partitionId?: string }
|
|
215
282
|
): Promise<{ size: number; mimeType?: string } | null> {
|
|
283
|
+
const partitionId = resolvePartitionId(options?.partitionId);
|
|
216
284
|
const rowResult = await sql<{ size: number; mime_type: string }>`
|
|
217
285
|
select size, mime_type
|
|
218
286
|
from ${sql.table('sync_blobs')}
|
|
219
|
-
where hash = ${hash}
|
|
287
|
+
where partition_id = ${partitionId} and hash = ${hash}
|
|
220
288
|
limit 1
|
|
221
289
|
`.execute(db);
|
|
222
290
|
const row = rowResult.rows[0];
|
|
@@ -232,13 +300,16 @@ export function createDatabaseBlobStorageAdapter<DB extends SyncBlobsDb>(
|
|
|
232
300
|
async put(
|
|
233
301
|
hash: string,
|
|
234
302
|
data: Uint8Array,
|
|
235
|
-
metadata?: Record<string, unknown
|
|
303
|
+
metadata?: Record<string, unknown>,
|
|
304
|
+
options?: { partitionId?: string }
|
|
236
305
|
): Promise<void> {
|
|
306
|
+
const partitionId = resolvePartitionId(options?.partitionId);
|
|
237
307
|
const mimeType =
|
|
238
308
|
typeof metadata?.mimeType === 'string'
|
|
239
309
|
? metadata.mimeType
|
|
240
310
|
: 'application/octet-stream';
|
|
241
311
|
await storeBlobInDatabase(db, {
|
|
312
|
+
partitionId,
|
|
242
313
|
hash,
|
|
243
314
|
size: data.length,
|
|
244
315
|
mimeType,
|
|
@@ -246,8 +317,12 @@ export function createDatabaseBlobStorageAdapter<DB extends SyncBlobsDb>(
|
|
|
246
317
|
});
|
|
247
318
|
},
|
|
248
319
|
|
|
249
|
-
async get(
|
|
250
|
-
|
|
320
|
+
async get(
|
|
321
|
+
hash: string,
|
|
322
|
+
options?: { partitionId?: string }
|
|
323
|
+
): Promise<Uint8Array | null> {
|
|
324
|
+
const partitionId = resolvePartitionId(options?.partitionId);
|
|
325
|
+
const result = await readBlobFromDatabase(db, hash, { partitionId });
|
|
251
326
|
return result?.body ?? null;
|
|
252
327
|
},
|
|
253
328
|
};
|
|
@@ -260,6 +335,7 @@ export function createDatabaseBlobStorageAdapter<DB extends SyncBlobsDb>(
|
|
|
260
335
|
export async function storeBlobInDatabase<DB extends SyncBlobsDb>(
|
|
261
336
|
db: Kysely<DB>,
|
|
262
337
|
args: {
|
|
338
|
+
partitionId: string;
|
|
263
339
|
hash: string;
|
|
264
340
|
size: number;
|
|
265
341
|
mimeType: string;
|
|
@@ -268,6 +344,7 @@ export async function storeBlobInDatabase<DB extends SyncBlobsDb>(
|
|
|
268
344
|
): Promise<void> {
|
|
269
345
|
await sql`
|
|
270
346
|
insert into ${sql.table('sync_blobs')} (
|
|
347
|
+
partition_id,
|
|
271
348
|
hash,
|
|
272
349
|
size,
|
|
273
350
|
mime_type,
|
|
@@ -275,13 +352,14 @@ export async function storeBlobInDatabase<DB extends SyncBlobsDb>(
|
|
|
275
352
|
created_at
|
|
276
353
|
)
|
|
277
354
|
values (
|
|
355
|
+
${args.partitionId},
|
|
278
356
|
${args.hash},
|
|
279
357
|
${args.size},
|
|
280
358
|
${args.mimeType},
|
|
281
359
|
${args.body},
|
|
282
360
|
${new Date().toISOString()}
|
|
283
361
|
)
|
|
284
|
-
on conflict (hash) do nothing
|
|
362
|
+
on conflict (partition_id, hash) do nothing
|
|
285
363
|
`.execute(db);
|
|
286
364
|
}
|
|
287
365
|
|
|
@@ -291,8 +369,10 @@ export async function storeBlobInDatabase<DB extends SyncBlobsDb>(
|
|
|
291
369
|
*/
|
|
292
370
|
export async function readBlobFromDatabase<DB extends SyncBlobsDb>(
|
|
293
371
|
db: Kysely<DB>,
|
|
294
|
-
hash: string
|
|
372
|
+
hash: string,
|
|
373
|
+
options?: { partitionId?: string }
|
|
295
374
|
): Promise<{ body: Uint8Array; mimeType: string; size: number } | null> {
|
|
375
|
+
const partitionId = options?.partitionId ?? 'default';
|
|
296
376
|
const rowResult = await sql<{
|
|
297
377
|
body: Uint8Array;
|
|
298
378
|
mime_type: string;
|
|
@@ -300,7 +380,7 @@ export async function readBlobFromDatabase<DB extends SyncBlobsDb>(
|
|
|
300
380
|
}>`
|
|
301
381
|
select body, mime_type, size
|
|
302
382
|
from ${sql.table('sync_blobs')}
|
|
303
|
-
where hash = ${hash}
|
|
383
|
+
where partition_id = ${partitionId} and hash = ${hash}
|
|
304
384
|
limit 1
|
|
305
385
|
`.execute(db);
|
|
306
386
|
const row = rowResult.rows[0];
|