dscs-server 0.0.1 → 0.1.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/http.js CHANGED
@@ -4,10 +4,11 @@ import { join } from 'node:path';
4
4
  import { createServer } from 'node:http';
5
5
  import { PROTOCOL_VERSION, errorBody } from './shared/protocol.js';
6
6
  import { SyncError, statusOf } from './shared/wire.js';
7
+ import { BLOB_BATCH_CONTENT_TYPE, MAX_BATCH_BLOBS, MAX_BATCH_BODY_BYTES, decodeBlobBatch, encodeBlobBatch } from './shared/blob-batch.js';
7
8
  import { deriveMasterKey, newSalt } from './shared/crypto.js';
8
9
  import { hasBlob, putBlob, readBlob } from './blobs.js';
9
10
  import { DEFAULT_DATASET, SERVER_SCHEMA_VERSION, adminPassword, blobRecord, isAdminInitialized, markAdminInitialized, readMaxBytes, readMeta, recordBlob, readRevision, setAdminPassword, usedBlobBytes, writeMaxBytes } from './db.js';
10
- import { MAX_APPEND_BODY_BYTES, adminSessions, appendFrames, deleteSession, getManifest, listChanges, listBatches, sessionCounts } from './sessions.js';
11
+ import { MAX_APPEND_BODY_BYTES, adminSessions, appendFrames, deleteSession, getManifest, getV2SessionSummary, listV2Batches, listV2Changes, listV2Sessions, sessionCounts } from './sessions.js';
11
12
  import { acquireLease, releaseLease, SYNC_RESOURCE, vaultResource } from './leases.js';
12
13
  import { appendVaultRecord, readVaultCursor, readVaultStream, validVaultWriter } from './vault.js';
13
14
  function readServerVersion() {
@@ -118,13 +119,93 @@ function clientIdFrom(req) {
118
119
  }
119
120
  return value;
120
121
  }
122
+ function idempotencyKeyFrom(req) {
123
+ const raw = req.headers['idempotency-key'];
124
+ const value = Array.isArray(raw) ? raw[0] ?? '' : String(raw ?? '');
125
+ if (!/^[A-Za-z0-9._:-]{1,255}$/u.test(value)) {
126
+ throw new SyncError('bad-request', '需要 1–255 位合法 Idempotency-Key');
127
+ }
128
+ return value;
129
+ }
130
+ const SESSION_ID_PATTERN = /^[A-Za-z0-9._:-]{1,128}$/u;
121
131
  function sessionIdFrom(pathname, pattern) {
122
132
  const match = pattern.exec(pathname);
123
133
  if (match === null) throw new SyncError('bad-request', '会话 id 缺失');
124
134
  const id = decodeURIComponent(match[1]);
125
- if (!/^[A-Za-z0-9._:-]{1,128}$/u.test(id)) throw new SyncError('bad-request', `会话 id 非法:${id}`);
135
+ if (!SESSION_ID_PATTERN.test(id)) throw new SyncError('bad-request', `会话 id 非法:${id}`);
126
136
  return id;
127
137
  }
138
+ const MAX_CURSOR_LENGTH = 512;
139
+ function encodeCursor(value) {
140
+ return Buffer.from(JSON.stringify(value), 'utf8').toString('base64url');
141
+ }
142
+ function decodeCursorObject(raw) {
143
+ if (raw === null || raw === '') return undefined;
144
+ if (raw.length > MAX_CURSOR_LENGTH || !/^[A-Za-z0-9_-]+$/u.test(raw)) {
145
+ throw new SyncError('bad-request', 'cursor 非法');
146
+ }
147
+ let parsed;
148
+ try {
149
+ parsed = JSON.parse(Buffer.from(raw, 'base64url').toString('utf8'));
150
+ } catch {
151
+ throw new SyncError('bad-request', 'cursor 非法');
152
+ }
153
+ if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
154
+ throw new SyncError('bad-request', 'cursor 非法');
155
+ }
156
+ return parsed;
157
+ }
158
+ function decodeSessionsCursor(raw) {
159
+ const value = decodeCursorObject(raw);
160
+ if (value === undefined) return undefined;
161
+ if (value['v'] !== 2 || typeof value['after'] !== 'string' || !SESSION_ID_PATTERN.test(value['after'])) {
162
+ throw new SyncError('bad-request', 'cursor 非法');
163
+ }
164
+ return value['after'];
165
+ }
166
+ function decodeChangesCursor(raw) {
167
+ const value = decodeCursorObject(raw);
168
+ if (value === undefined) {
169
+ return {
170
+ revision: 0,
171
+ tombstone: {
172
+ deletedAt: 0,
173
+ sessionId: '',
174
+ createdAt: 0
175
+ }
176
+ };
177
+ }
178
+ const revision = value['revision'];
179
+ const tombstone = value['tombstone'];
180
+ if (value['v'] !== 2 || typeof revision !== 'number' || !Number.isSafeInteger(revision) || revision < 0 || tombstone === null || typeof tombstone !== 'object' || Array.isArray(tombstone)) {
181
+ throw new SyncError('bad-request', 'cursor 非法');
182
+ }
183
+ const record = tombstone;
184
+ const deletedAt = record['deletedAt'];
185
+ const sessionId = record['sessionId'];
186
+ const createdAt = record['createdAt'];
187
+ const empty = deletedAt === 0 && sessionId === '' && createdAt === 0;
188
+ if (typeof deletedAt !== 'number' || !Number.isSafeInteger(deletedAt) || deletedAt < 0 || typeof sessionId !== 'string' || !empty && !SESSION_ID_PATTERN.test(sessionId) || typeof createdAt !== 'number' || !Number.isSafeInteger(createdAt) || createdAt < 0) {
189
+ throw new SyncError('bad-request', 'cursor 非法');
190
+ }
191
+ return {
192
+ revision,
193
+ tombstone: {
194
+ deletedAt,
195
+ sessionId,
196
+ createdAt
197
+ }
198
+ };
199
+ }
200
+ function etagMatches(header, digest) {
201
+ const raw = Array.isArray(header) ? header.join(',') : header;
202
+ if (raw === undefined) return false;
203
+ const target = `"${digest}"`;
204
+ return raw.split(',').some((entry)=>{
205
+ const value = entry.trim();
206
+ return value === '*' || value === target || value === `W/${target}`;
207
+ });
208
+ }
128
209
  function parseAppend(body) {
129
210
  if (!Number.isSafeInteger(body.baseIndex)) throw new SyncError('bad-request', 'baseIndex 必须是整数');
130
211
  const baseChainHash = body.baseChainHash;
@@ -165,16 +246,24 @@ function intParam(value, fallback, min, max) {
165
246
  if (!Number.isSafeInteger(parsed)) throw new SyncError('bad-request', `查询参数必须是整数:${value}`);
166
247
  return Math.min(max, Math.max(min, parsed));
167
248
  }
168
- const SYNC_LEASE_PATH = /^\/v1\/sync\/lease$/u;
169
- const APPEND_PATH = /^\/v1\/sessions\/([^/]+)\/append$/u;
170
- const DELETE_PATH = /^\/v1\/sessions\/([^/]+)\/delete$/u;
171
- const MANIFEST_PATH = /^\/v1\/sessions\/([^/]+)\/manifest$/u;
172
- const BATCHES_PATH = /^\/v1\/sessions\/([^/]+)\/batches$/u;
173
- const CURSOR_PATH = /^\/v1\/cursor$/u;
174
- const VAULT_APPEND_PATH = /^\/v1\/vault\/append$/u;
175
- const VAULT_LEASE_PATH = /^\/v1\/vault\/lease$/u;
176
- const VAULT_CURSOR_PATH = /^\/v1\/vault\/cursor$/u;
177
- const VAULT_STREAM_PATH = /^\/v1\/vault\/stream\/([^/]+)$/u;
249
+ const SYNC_LEASE_PATH = /^\/v2\/sync\/lease$/u;
250
+ const V2_SESSIONS_PATH = /^\/v2\/sessions$/u;
251
+ const V2_CHANGES_PATH = /^\/v2\/changes$/u;
252
+ const V2_SUMMARY_PATH = /^\/v2\/sessions\/([^/]+)\/summary$/u;
253
+ const V2_BATCHES_PATH = /^\/v2\/sessions\/([^/]+)\/batches$/u;
254
+ const V2_MANIFEST_PATH = /^\/v2\/sessions\/([^/]+)\/manifest$/u;
255
+ const V2_APPEND_PATH = /^\/v2\/sessions\/([^/]+)\/append$/u;
256
+ const V2_DELETE_PATH = /^\/v2\/sessions\/([^/]+)\/delete$/u;
257
+ const DEVICES_PATH = /^\/v2\/devices$/u;
258
+ const DEVICE_PATH = /^\/v2\/devices\/([0-9a-f]+)$/u;
259
+ const BLOB_PATH = /^\/v2\/blobs\/([0-9a-f]{64})$/u;
260
+ const BLOB_BATCH_PATH = /^\/v2\/blobs\/batch$/u;
261
+ const BLOB_BATCH_READ_PATH = /^\/v2\/blobs\/batch\/read$/u;
262
+ const ME_PATH = /^\/v2\/me$/u;
263
+ const VAULT_APPEND_PATH = /^\/v2\/vault\/append$/u;
264
+ const VAULT_LEASE_PATH = /^\/v2\/vault\/lease$/u;
265
+ const VAULT_CURSOR_PATH = /^\/v2\/vault\/cursor$/u;
266
+ const VAULT_STREAM_PATH = /^\/v2\/vault\/stream\/([^/]+)$/u;
178
267
  const ADMIN_DEVICE_PATH = /^\/admin\/devices\/([0-9a-f]+)$/u;
179
268
  function withBatchFields(value) {
180
269
  if (value === null || typeof value !== 'object') return value;
@@ -341,16 +430,28 @@ export function createHttpServer(options) {
341
430
  server: {
342
431
  version,
343
432
  apiVersions: [
344
- 1
433
+ 2
345
434
  ],
346
435
  protocols: [
347
436
  PROTOCOL_VERSION
348
437
  ],
349
438
  schemaVersion: SERVER_SCHEMA_VERSION,
350
439
  capabilities: [
440
+ 'session-summary',
441
+ 'changes-cursor',
442
+ 'conditional-get',
443
+ 'idempotent-append',
351
444
  'manifest-digest',
352
- 'batches'
353
- ]
445
+ 'batches',
446
+ 'batch-upload',
447
+ 'batch-download',
448
+ 'zstd-frame'
449
+ ],
450
+ limits: {
451
+ batchBytes: MAX_BATCH_BODY_BYTES,
452
+ batchBlobs: MAX_BATCH_BLOBS,
453
+ blobBytes: maxBlobBytes
454
+ }
354
455
  },
355
456
  now: Date.now()
356
457
  });
@@ -358,19 +459,148 @@ export function createHttpServer(options) {
358
459
  },
359
460
  {
360
461
  method: 'GET',
361
- path: /^\/v1\/health$/,
362
- handler: async (_req, res)=>{
363
- json(res, 200, {
462
+ path: V2_SESSIONS_PATH,
463
+ handler: async (req, res)=>{
464
+ requireDevice(db, req);
465
+ const url = new URL(req.url ?? '/', 'http://localhost');
466
+ const after = decodeSessionsCursor(url.searchParams.get('cursor'));
467
+ const limit = intParam(url.searchParams.get('limit'), 200, 1, 1000);
468
+ const page = listV2Sessions(db, after, limit);
469
+ return json(res, 200, {
364
470
  ok: true,
365
- version,
366
- protocol: PROTOCOL_VERSION,
367
- now: Date.now()
471
+ value: {
472
+ sessions: page.sessions,
473
+ nextCursor: page.nextSessionId === null ? null : encodeCursor({
474
+ v: 2,
475
+ after: page.nextSessionId
476
+ })
477
+ }
478
+ });
479
+ }
480
+ },
481
+ {
482
+ method: 'GET',
483
+ path: V2_CHANGES_PATH,
484
+ handler: async (req, res)=>{
485
+ requireDevice(db, req);
486
+ const url = new URL(req.url ?? '/', 'http://localhost');
487
+ const cursor = decodeChangesCursor(url.searchParams.get('cursor'));
488
+ const limit = intParam(url.searchParams.get('limit'), 200, 1, 1000);
489
+ const page = listV2Changes(db, cursor.revision, cursor.tombstone, limit);
490
+ return json(res, 200, {
491
+ ok: true,
492
+ value: {
493
+ sessions: page.sessions,
494
+ tombstones: page.tombstones,
495
+ nextCursor: encodeCursor({
496
+ v: 2,
497
+ revision: page.nextRevision,
498
+ tombstone: page.nextTombstone
499
+ }),
500
+ hasMore: page.hasMore
501
+ }
502
+ });
503
+ }
504
+ },
505
+ {
506
+ method: 'GET',
507
+ path: V2_SUMMARY_PATH,
508
+ handler: async (req, res)=>{
509
+ requireDevice(db, req);
510
+ const id = sessionIdFrom(new URL(req.url ?? '/', 'http://localhost').pathname, V2_SUMMARY_PATH);
511
+ const summary = getV2SessionSummary(db, id);
512
+ if (summary === undefined) return json(res, 404, errorBody('not-found', `没有会话 ${id}`));
513
+ res.setHeader('etag', `"${summary.manifestDigest}"`);
514
+ if (etagMatches(req.headers['if-none-match'], summary.manifestDigest)) {
515
+ res.statusCode = 304;
516
+ res.end();
517
+ return;
518
+ }
519
+ return json(res, 200, {
520
+ ok: true,
521
+ value: summary
522
+ });
523
+ }
524
+ },
525
+ {
526
+ method: 'GET',
527
+ path: V2_BATCHES_PATH,
528
+ handler: async (req, res)=>{
529
+ requireDevice(db, req);
530
+ const url = new URL(req.url ?? '/', 'http://localhost');
531
+ const id = sessionIdFrom(url.pathname, V2_BATCHES_PATH);
532
+ const from = intParam(url.searchParams.get('from'), 0, 0, Number.MAX_SAFE_INTEGER);
533
+ const limit = intParam(url.searchParams.get('limit'), 500, 1, 1000);
534
+ const expected = url.searchParams.get('expectedManifestDigest');
535
+ if (expected !== null && !/^[0-9a-f]{64}$/u.test(expected)) {
536
+ throw new SyncError('bad-request', 'expectedManifestDigest 必须是 64 位十六进制');
537
+ }
538
+ const page = listV2Batches(db, id, from, limit);
539
+ if (page === undefined) return json(res, 404, errorBody('not-found', `没有会话 ${id}`));
540
+ if (expected !== null && expected !== page.manifestDigest) {
541
+ throw new SyncError('conflict', 'manifest 已变化,请重新读取摘要');
542
+ }
543
+ return json(res, 200, {
544
+ ok: true,
545
+ value: page
546
+ });
547
+ }
548
+ },
549
+ {
550
+ method: 'POST',
551
+ path: V2_APPEND_PATH,
552
+ handler: async (req, res)=>{
553
+ const device = requireDevice(db, req);
554
+ const clientId = clientIdFrom(req);
555
+ const idempotencyKey = idempotencyKeyFrom(req);
556
+ const id = sessionIdFrom(new URL(req.url ?? '/', 'http://localhost').pathname, V2_APPEND_PATH);
557
+ const body = await readJson(req, MAX_APPEND_BODY_BYTES);
558
+ const parsed = parseAppend(body);
559
+ const value = appendFrames(db, options.dataDir, {
560
+ sessionId: id,
561
+ deviceId: device.deviceId,
562
+ clientId,
563
+ idempotencyKey,
564
+ ...parsed
565
+ });
566
+ return json(res, 200, {
567
+ ok: true,
568
+ value
569
+ });
570
+ }
571
+ },
572
+ {
573
+ method: 'GET',
574
+ path: V2_MANIFEST_PATH,
575
+ handler: async (req, res)=>{
576
+ requireDevice(db, req);
577
+ const id = sessionIdFrom(new URL(req.url ?? '/', 'http://localhost').pathname, V2_MANIFEST_PATH);
578
+ const found = getManifest(db, id);
579
+ if (found === undefined) return json(res, 404, errorBody('not-found', `没有会话 ${id}`));
580
+ return json(res, 200, {
581
+ ok: true,
582
+ value: withBatchFields(found)
368
583
  });
369
584
  }
370
585
  },
371
586
  {
372
587
  method: 'POST',
373
- path: /^\/v1\/devices$/,
588
+ path: V2_DELETE_PATH,
589
+ handler: async (req, res)=>{
590
+ const device = requireDevice(db, req);
591
+ const clientId = clientIdFrom(req);
592
+ const id = sessionIdFrom(new URL(req.url ?? '/', 'http://localhost').pathname, V2_DELETE_PATH);
593
+ const body = await readJson(req);
594
+ if (!Number.isSafeInteger(body.createdAt)) throw new SyncError('bad-request', 'createdAt 必须是整数');
595
+ return json(res, 200, {
596
+ ok: true,
597
+ value: deleteSession(db, id, device.deviceId, clientId, Number(body.createdAt))
598
+ });
599
+ }
600
+ },
601
+ {
602
+ method: 'POST',
603
+ path: DEVICES_PATH,
374
604
  handler: async (req, res)=>{
375
605
  const auth = authenticate(db, req);
376
606
  if (auth?.role !== 'admin') return json(res, 401, errorBody('unauthorized', '需要 setup token'));
@@ -385,7 +615,7 @@ export function createHttpServer(options) {
385
615
  },
386
616
  {
387
617
  method: 'GET',
388
- path: /^\/v1\/devices$/,
618
+ path: DEVICES_PATH,
389
619
  handler: async (req, res)=>{
390
620
  const auth = authenticate(db, req);
391
621
  if (auth?.role !== 'admin') return json(res, 401, errorBody('unauthorized', '需要 setup token'));
@@ -397,11 +627,11 @@ export function createHttpServer(options) {
397
627
  },
398
628
  {
399
629
  method: 'DELETE',
400
- path: /^\/v1\/devices\/([0-9a-f]+)$/,
630
+ path: DEVICE_PATH,
401
631
  handler: async (req, res)=>{
402
632
  const auth = authenticate(db, req);
403
633
  if (auth?.role !== 'admin') return json(res, 401, errorBody('unauthorized', '需要 setup token'));
404
- const match = /^\/v1\/devices\/([0-9a-f]+)$/.exec(new URL(req.url ?? '/', 'http://localhost').pathname);
634
+ const match = DEVICE_PATH.exec(new URL(req.url ?? '/', 'http://localhost').pathname);
405
635
  if (match === null) return json(res, 404, errorBody('not-found', '没有这个设备'));
406
636
  const revoked = revokeDevice(db, match[1]);
407
637
  return revoked ? json(res, 200, {
@@ -415,7 +645,107 @@ export function createHttpServer(options) {
415
645
  },
416
646
  {
417
647
  method: 'POST',
418
- path: /^\/v1\/blobs$/,
648
+ path: BLOB_BATCH_PATH,
649
+ handler: async (req, res)=>{
650
+ requireDevice(db, req);
651
+ const body = await readRaw(req, MAX_BATCH_BODY_BYTES);
652
+ const entries = decodeBlobBatch(body);
653
+ const results = [];
654
+ const fresh = new Map();
655
+ for (const entry of entries){
656
+ const actual = createHash('sha256').update(entry.data).digest('hex');
657
+ if (actual !== entry.sha256) {
658
+ throw new SyncError('bad-request', `批量 blob 的哈希不匹配:${entry.sha256}`);
659
+ }
660
+ const existing = blobRecord(db, actual);
661
+ if (existing !== undefined || fresh.has(actual)) {
662
+ results.push({
663
+ sha256: actual,
664
+ bytes: entry.data.length,
665
+ dedup: true
666
+ });
667
+ continue;
668
+ }
669
+ fresh.set(actual, {
670
+ kind: entry.kind,
671
+ data: entry.data
672
+ });
673
+ results.push({
674
+ sha256: actual,
675
+ bytes: entry.data.length,
676
+ dedup: false
677
+ });
678
+ }
679
+ const newBytes = [
680
+ ...fresh.values()
681
+ ].reduce((sum, entry)=>sum + entry.data.length, 0);
682
+ const used = usedBlobBytes(db);
683
+ const limit = effectiveMaxBytes();
684
+ if (used + newBytes > limit) {
685
+ throw new SyncError('quota', `存储超额:已用 ${used} 字节,本批新增 ${newBytes} 字节,上限 ${limit} 字节`);
686
+ }
687
+ for (const [sha256, entry] of fresh){
688
+ await putBlob(options.dataDir, entry.data);
689
+ recordBlob(db, sha256, entry.data.length, entry.kind);
690
+ }
691
+ return json(res, 200, {
692
+ ok: true,
693
+ value: {
694
+ blobs: results
695
+ }
696
+ });
697
+ }
698
+ },
699
+ {
700
+ method: 'POST',
701
+ path: BLOB_BATCH_READ_PATH,
702
+ handler: async (req, res)=>{
703
+ requireDevice(db, req);
704
+ const body = await readJson(req);
705
+ const raw = body.sha256;
706
+ if (!Array.isArray(raw) || raw.length < 1 || raw.length > MAX_BATCH_BLOBS) {
707
+ throw new SyncError('bad-request', `sha256 必须是 1–${MAX_BATCH_BLOBS} 个数组项`);
708
+ }
709
+ const hashes = [];
710
+ for (const value of raw){
711
+ if (typeof value !== 'string' || !/^[0-9a-f]{64}$/u.test(value)) {
712
+ throw new SyncError('bad-request', 'sha256 必须是 64 位小写十六进制');
713
+ }
714
+ hashes.push(value);
715
+ }
716
+ const kinds = [];
717
+ for (const sha256 of hashes){
718
+ if (!await hasBlob(options.dataDir, sha256)) return json(res, 404, errorBody('not-found', `没有这个 blob:${sha256}`));
719
+ const record = blobRecord(db, sha256);
720
+ if (record === undefined) return json(res, 404, errorBody('not-found', `没有这个 blob:${sha256}`));
721
+ if (record.kind !== 'frame' && record.kind !== 'attachment' && record.kind !== 'vault') {
722
+ throw new SyncError('bad-request', `blob 类别非法:${record.kind}`);
723
+ }
724
+ kinds.push(record.kind);
725
+ }
726
+ const blobs = [];
727
+ for(let index = 0; index < hashes.length; index += 1){
728
+ blobs.push({
729
+ kind: kinds[index],
730
+ data: await readBlob(options.dataDir, hashes[index])
731
+ });
732
+ }
733
+ let encoded;
734
+ try {
735
+ encoded = encodeBlobBatch(blobs);
736
+ } catch (error) {
737
+ throw new SyncError('bad-request', error instanceof Error ? error.message : String(error));
738
+ }
739
+ res.statusCode = 200;
740
+ res.setHeader('content-type', BLOB_BATCH_CONTENT_TYPE);
741
+ res.setHeader('content-length', encoded.length);
742
+ res.setHeader('cache-control', 'no-store');
743
+ res.end(encoded);
744
+ }
745
+ },
746
+ {
747
+ method: 'POST',
748
+ path: /^\/v2\/blobs$/,
419
749
  handler: async (req, res)=>{
420
750
  const auth = authenticate(db, req);
421
751
  if (auth === undefined) return json(res, 401, errorBody('unauthorized', '需要设备或 setup token'));
@@ -455,7 +785,7 @@ export function createHttpServer(options) {
455
785
  },
456
786
  {
457
787
  method: 'HEAD',
458
- path: /^\/v1\/blobs\/([0-9a-f]{64})$/,
788
+ path: BLOB_PATH,
459
789
  handler: async (req, res)=>{
460
790
  const auth = authenticate(db, req);
461
791
  if (auth === undefined) {
@@ -463,18 +793,18 @@ export function createHttpServer(options) {
463
793
  res.end();
464
794
  return;
465
795
  }
466
- const match = /^\/v1\/blobs\/([0-9a-f]{64})$/.exec(new URL(req.url ?? '/', 'http://localhost').pathname);
796
+ const match = BLOB_PATH.exec(new URL(req.url ?? '/', 'http://localhost').pathname);
467
797
  res.statusCode = match !== null && await hasBlob(options.dataDir, match[1]) ? 200 : 404;
468
798
  res.end();
469
799
  }
470
800
  },
471
801
  {
472
802
  method: 'GET',
473
- path: /^\/v1\/blobs\/([0-9a-f]{64})$/,
803
+ path: BLOB_PATH,
474
804
  handler: async (req, res)=>{
475
805
  const auth = authenticate(db, req);
476
806
  if (auth === undefined) return json(res, 401, errorBody('unauthorized', '需要设备或 setup token'));
477
- const match = /^\/v1\/blobs\/([0-9a-f]{64})$/.exec(new URL(req.url ?? '/', 'http://localhost').pathname);
807
+ const match = BLOB_PATH.exec(new URL(req.url ?? '/', 'http://localhost').pathname);
478
808
  if (match === null) return json(res, 404, errorBody('not-found', '没有这个 blob'));
479
809
  const sha256 = match[1];
480
810
  if (!await hasBlob(options.dataDir, sha256)) return json(res, 404, errorBody('not-found', '没有这个 blob'));
@@ -511,85 +841,6 @@ export function createHttpServer(options) {
511
841
  });
512
842
  }
513
843
  },
514
- {
515
- method: 'POST',
516
- path: APPEND_PATH,
517
- handler: async (req, res)=>{
518
- const device = requireDevice(db, req);
519
- const clientId = clientIdFrom(req);
520
- const id = sessionIdFrom(new URL(req.url ?? '/', 'http://localhost').pathname, APPEND_PATH);
521
- const body = await readJson(req, MAX_APPEND_BODY_BYTES);
522
- const parsed = parseAppend(body);
523
- const value = appendFrames(db, options.dataDir, {
524
- sessionId: id,
525
- deviceId: device.deviceId,
526
- clientId,
527
- ...parsed
528
- });
529
- return json(res, 200, {
530
- ok: true,
531
- value: withBatchFields(value)
532
- });
533
- }
534
- },
535
- {
536
- method: 'POST',
537
- path: DELETE_PATH,
538
- handler: async (req, res)=>{
539
- const device = requireDevice(db, req);
540
- const clientId = clientIdFrom(req);
541
- const id = sessionIdFrom(new URL(req.url ?? '/', 'http://localhost').pathname, DELETE_PATH);
542
- const body = await readJson(req);
543
- if (!Number.isSafeInteger(body.createdAt)) throw new SyncError('bad-request', 'createdAt 必须是整数');
544
- return json(res, 200, {
545
- ok: true,
546
- value: deleteSession(db, id, device.deviceId, clientId, Number(body.createdAt))
547
- });
548
- }
549
- },
550
- {
551
- method: 'GET',
552
- path: MANIFEST_PATH,
553
- handler: async (req, res)=>{
554
- requireDevice(db, req);
555
- const id = sessionIdFrom(new URL(req.url ?? '/', 'http://localhost').pathname, MANIFEST_PATH);
556
- const found = getManifest(db, id);
557
- if (found === undefined) return json(res, 404, errorBody('not-found', `没有会话 ${id}`));
558
- return json(res, 200, {
559
- ok: true,
560
- value: withBatchFields(found)
561
- });
562
- }
563
- },
564
- {
565
- method: 'GET',
566
- path: BATCHES_PATH,
567
- handler: async (req, res)=>{
568
- requireDevice(db, req);
569
- const url = new URL(req.url ?? '/', 'http://localhost');
570
- const id = sessionIdFrom(url.pathname, BATCHES_PATH);
571
- const found = listBatches(db, id, intParam(url.searchParams.get('from'), 0, 0, Number.MAX_SAFE_INTEGER), intParam(url.searchParams.get('limit'), 500, 1, 1000));
572
- if (found === undefined) return json(res, 404, errorBody('not-found', `没有会话 ${id}`));
573
- return json(res, 200, {
574
- ok: true,
575
- value: withBatchFields(found)
576
- });
577
- }
578
- },
579
- {
580
- method: 'GET',
581
- path: CURSOR_PATH,
582
- handler: async (req, res)=>{
583
- requireDevice(db, req);
584
- const url = new URL(req.url ?? '/', 'http://localhost');
585
- const since = intParam(url.searchParams.get('since'), 0, 0, Number.MAX_SAFE_INTEGER);
586
- const limit = intParam(url.searchParams.get('limit'), 200, 1, 1000);
587
- return json(res, 200, {
588
- ok: true,
589
- value: listChanges(db, since, limit)
590
- });
591
- }
592
- },
593
844
  {
594
845
  method: 'POST',
595
846
  path: VAULT_LEASE_PATH,
@@ -937,7 +1188,7 @@ export function createHttpServer(options) {
937
1188
  },
938
1189
  {
939
1190
  method: 'GET',
940
- path: /^\/v1\/me$/,
1191
+ path: ME_PATH,
941
1192
  handler: async (req, res)=>{
942
1193
  const auth = authenticate(db, req);
943
1194
  if (auth === undefined) return json(res, 401, errorBody('unauthorized', '未知 token'));
package/dist/index.js CHANGED
File without changes