@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.
Files changed (93) hide show
  1. package/dist/blobs/adapters/database.d.ts +26 -9
  2. package/dist/blobs/adapters/database.d.ts.map +1 -1
  3. package/dist/blobs/adapters/database.js +65 -21
  4. package/dist/blobs/adapters/database.js.map +1 -1
  5. package/dist/blobs/manager.d.ts +60 -3
  6. package/dist/blobs/manager.d.ts.map +1 -1
  7. package/dist/blobs/manager.js +227 -56
  8. package/dist/blobs/manager.js.map +1 -1
  9. package/dist/blobs/migrate.d.ts.map +1 -1
  10. package/dist/blobs/migrate.js +16 -8
  11. package/dist/blobs/migrate.js.map +1 -1
  12. package/dist/blobs/types.d.ts +4 -0
  13. package/dist/blobs/types.d.ts.map +1 -1
  14. package/dist/dialect/helpers.d.ts +3 -0
  15. package/dist/dialect/helpers.d.ts.map +1 -1
  16. package/dist/dialect/helpers.js +17 -0
  17. package/dist/dialect/helpers.js.map +1 -1
  18. package/dist/handlers/collection.d.ts +0 -2
  19. package/dist/handlers/collection.d.ts.map +1 -1
  20. package/dist/handlers/collection.js +5 -56
  21. package/dist/handlers/collection.js.map +1 -1
  22. package/dist/handlers/create-handler.d.ts +0 -4
  23. package/dist/handlers/create-handler.d.ts.map +1 -1
  24. package/dist/handlers/create-handler.js +6 -34
  25. package/dist/handlers/create-handler.js.map +1 -1
  26. package/dist/notify.d.ts.map +1 -1
  27. package/dist/notify.js +13 -37
  28. package/dist/notify.js.map +1 -1
  29. package/dist/proxy/collection.d.ts +0 -2
  30. package/dist/proxy/collection.d.ts.map +1 -1
  31. package/dist/proxy/collection.js +2 -17
  32. package/dist/proxy/collection.js.map +1 -1
  33. package/dist/proxy/handler.d.ts +1 -1
  34. package/dist/proxy/handler.d.ts.map +1 -1
  35. package/dist/proxy/handler.js +1 -2
  36. package/dist/proxy/handler.js.map +1 -1
  37. package/dist/proxy/index.d.ts +1 -1
  38. package/dist/proxy/index.d.ts.map +1 -1
  39. package/dist/proxy/index.js +1 -1
  40. package/dist/proxy/index.js.map +1 -1
  41. package/dist/proxy/oplog.d.ts.map +1 -1
  42. package/dist/proxy/oplog.js +1 -7
  43. package/dist/proxy/oplog.js.map +1 -1
  44. package/dist/prune.d.ts.map +1 -1
  45. package/dist/prune.js +1 -13
  46. package/dist/prune.js.map +1 -1
  47. package/dist/pull.d.ts.map +1 -1
  48. package/dist/pull.js +186 -54
  49. package/dist/pull.js.map +1 -1
  50. package/dist/push.d.ts +1 -1
  51. package/dist/push.d.ts.map +1 -1
  52. package/dist/push.js +9 -36
  53. package/dist/push.js.map +1 -1
  54. package/dist/snapshot-chunks/db-metadata.d.ts +18 -0
  55. package/dist/snapshot-chunks/db-metadata.d.ts.map +1 -1
  56. package/dist/snapshot-chunks/db-metadata.js +71 -23
  57. package/dist/snapshot-chunks/db-metadata.js.map +1 -1
  58. package/dist/snapshot-chunks.d.ts +5 -1
  59. package/dist/snapshot-chunks.d.ts.map +1 -1
  60. package/dist/snapshot-chunks.js +14 -1
  61. package/dist/snapshot-chunks.js.map +1 -1
  62. package/dist/stats.d.ts.map +1 -1
  63. package/dist/stats.js +1 -13
  64. package/dist/stats.js.map +1 -1
  65. package/dist/subscriptions/resolve.d.ts +1 -1
  66. package/dist/subscriptions/resolve.d.ts.map +1 -1
  67. package/dist/subscriptions/resolve.js +3 -16
  68. package/dist/subscriptions/resolve.js.map +1 -1
  69. package/dist/sync.d.ts.map +1 -1
  70. package/dist/sync.js +2 -4
  71. package/dist/sync.js.map +1 -1
  72. package/package.json +2 -2
  73. package/src/blobs/adapters/database.test.ts +7 -0
  74. package/src/blobs/adapters/database.ts +119 -39
  75. package/src/blobs/manager.ts +339 -53
  76. package/src/blobs/migrate.ts +16 -8
  77. package/src/blobs/types.ts +4 -0
  78. package/src/dialect/helpers.ts +19 -0
  79. package/src/handlers/collection.ts +17 -86
  80. package/src/handlers/create-handler.ts +9 -44
  81. package/src/notify.ts +15 -40
  82. package/src/proxy/collection.ts +5 -27
  83. package/src/proxy/handler.ts +2 -2
  84. package/src/proxy/index.ts +0 -2
  85. package/src/proxy/oplog.ts +1 -9
  86. package/src/prune.ts +1 -12
  87. package/src/pull.ts +280 -105
  88. package/src/push.ts +14 -43
  89. package/src/snapshot-chunks/db-metadata.ts +107 -27
  90. package/src/snapshot-chunks.ts +18 -0
  91. package/src/stats.ts +1 -12
  92. package/src/subscriptions/resolve.ts +4 -20
  93. 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
- * Sign a token for blob upload/download authorization.
24
- * @param payload The data to sign
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: { hash: string; action: 'upload' | 'download'; expiresAt: number },
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
- * Verify and decode a signed token.
35
- * @returns The payload if valid, null if invalid/expired
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
- hash: string;
39
- action: 'upload' | 'download';
40
- expiresAt: number;
41
- } | null>;
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 data = JSON.parse(atob(dataB64)) as {
96
- hash: string;
97
- action: 'upload' | 'download';
98
- expiresAt: number;
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
- if (Date.now() > data.expiresAt) return null;
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
- // Normalize base URL (remove trailing slash)
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
- { hash: opts.hash, action: 'upload', expiresAt },
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 url = `${normalizedBaseUrl}/blobs/${encodeURIComponent(opts.hash)}/upload?token=${encodeURIComponent(token)}`;
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 `${normalizedBaseUrl}/blobs/${encodeURIComponent(opts.hash)}/download?token=${encodeURIComponent(token)}`;
248
+ return resolveUrlFromBase(
249
+ baseUrl,
250
+ `/blobs/${encodeURIComponent(opts.hash)}/download?token=${encodeURIComponent(token)}`
251
+ );
194
252
  },
195
253
 
196
- async exists(hash: string): Promise<boolean> {
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(hash: string): Promise<void> {
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(hash: string): Promise<Uint8Array | null> {
250
- const result = await readBlobFromDatabase(db, hash);
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];