dscs-server 0.0.0-stage → 0.0.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.
package/dist/vault.js ADDED
@@ -0,0 +1,147 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { SyncError } from './shared/wire.js';
3
+ import { assertVaultRecordRef, vaultRecordHashOf } from './shared/vault.js';
4
+ import { blobPath } from './blobs.js';
5
+ import { DEFAULT_DATASET, bumpRevision, readRevision, withTransaction } from './db.js';
6
+ import { requireLease, vaultResource } from './leases.js';
7
+ export const MAX_VAULT_RECORD_BYTES = 1024 * 1024;
8
+ const HASH_PATTERN = /^[0-9a-f]{64}$/u;
9
+ const KEY_ID_PATTERN = /^[0-9a-f]{16}$/u;
10
+ function viewOf(row) {
11
+ let sealed;
12
+ try {
13
+ sealed = JSON.parse(row.sealed_json);
14
+ } catch {
15
+ throw new SyncError('bad-request', 'Vault sealed_json 损坏');
16
+ }
17
+ const ref = {
18
+ v: 1,
19
+ writer: row.writer,
20
+ seq: row.seq,
21
+ prevHash: row.prev_hash,
22
+ blobSha256: row.blob_sha256,
23
+ bytes: row.bytes,
24
+ createdAt: row.created_at,
25
+ sealed,
26
+ recordHash: row.record_hash
27
+ };
28
+ assertVaultRecordRef(ref);
29
+ return {
30
+ ...ref,
31
+ revision: row.revision
32
+ };
33
+ }
34
+ function lastRow(db, writer) {
35
+ return db.handle.prepare('SELECT writer, seq, prev_hash, blob_sha256, bytes, created_at, sealed_json, record_hash, revision FROM vault_records WHERE dataset = ? AND writer = ? ORDER BY seq DESC LIMIT 1').get(DEFAULT_DATASET, writer);
36
+ }
37
+ function rowAt(db, writer, seq) {
38
+ return db.handle.prepare('SELECT writer, seq, prev_hash, blob_sha256, bytes, created_at, sealed_json, record_hash, revision FROM vault_records WHERE dataset = ? AND writer = ? AND seq = ?').get(DEFAULT_DATASET, writer, seq);
39
+ }
40
+ function validateInput(input) {
41
+ if (input.writer !== input.deviceId) throw new SyncError('bad-request', 'writer 必须等于当前 deviceId');
42
+ if (!Number.isSafeInteger(input.seq) || input.seq < 1) throw new SyncError('bad-request', 'Vault seq 必须是正整数');
43
+ if (!Number.isSafeInteger(input.createdAt) || input.createdAt < 0) throw new SyncError('bad-request', 'Vault createdAt 非法');
44
+ if (!HASH_PATTERN.test(input.blobSha256)) throw new SyncError('bad-request', 'Vault blobSha256 非法');
45
+ if (!Number.isSafeInteger(input.bytes) || input.bytes <= 0 || input.bytes > MAX_VAULT_RECORD_BYTES) {
46
+ throw new SyncError('bad-request', 'Vault bytes 非法或超过 1 MiB');
47
+ }
48
+ if (input.sealed === null || typeof input.sealed !== 'object') throw new SyncError('bad-request', 'Vault sealed 非法');
49
+ if (input.sealed.formatVersion !== 1) throw new SyncError('bad-request', 'Vault sealed.formatVersion 必须是 1');
50
+ if (!KEY_ID_PATTERN.test(input.sealed.keyId)) throw new SyncError('bad-request', 'Vault sealed.keyId 非法');
51
+ if (input.seq === 1 && input.prevHash !== null) throw new SyncError('bad-request', 'Vault seq=1 的 prevHash 必须为 null');
52
+ if (input.seq > 1 && (input.prevHash === null || !HASH_PATTERN.test(input.prevHash))) {
53
+ throw new SyncError('bad-request', 'Vault prevHash 非法');
54
+ }
55
+ const fields = {
56
+ v: 1,
57
+ writer: input.writer,
58
+ seq: input.seq,
59
+ prevHash: input.prevHash,
60
+ blobSha256: input.blobSha256,
61
+ bytes: input.bytes,
62
+ createdAt: input.createdAt,
63
+ sealed: {
64
+ formatVersion: input.sealed.formatVersion,
65
+ keyId: input.sealed.keyId
66
+ }
67
+ };
68
+ const ref = {
69
+ ...fields,
70
+ recordHash: vaultRecordHashOf(fields)
71
+ };
72
+ assertVaultRecordRef(ref);
73
+ return ref;
74
+ }
75
+ function sameRow(row, ref) {
76
+ return row.prev_hash === ref.prevHash && row.blob_sha256 === ref.blobSha256 && row.bytes === ref.bytes && row.created_at === ref.createdAt && row.record_hash === ref.recordHash && row.sealed_json === JSON.stringify(ref.sealed);
77
+ }
78
+ export function appendVaultRecord(db, dataDir, input, now = Date.now()) {
79
+ const ref = validateInput(input);
80
+ const existing = rowAt(db, input.writer, input.seq);
81
+ if (existing !== undefined) {
82
+ if (sameRow(existing, ref)) return {
83
+ record: viewOf(existing),
84
+ dedup: true
85
+ };
86
+ throw new SyncError('conflict', `Vault writer ${input.writer} 的 seq ${input.seq} 已有不同内容`);
87
+ }
88
+ return withTransaction(db, ()=>{
89
+ requireLease(db, vaultResource(input.writer), input.deviceId, input.clientId, now);
90
+ const raced = rowAt(db, input.writer, input.seq);
91
+ if (raced !== undefined) {
92
+ if (sameRow(raced, ref)) return {
93
+ record: viewOf(raced),
94
+ dedup: true
95
+ };
96
+ throw new SyncError('conflict', `Vault writer ${input.writer} 的 seq ${input.seq} 已有不同内容`);
97
+ }
98
+ const last = lastRow(db, input.writer);
99
+ if (last === undefined) {
100
+ if (ref.seq !== 1 || ref.prevHash !== null) throw new SyncError('conflict', 'Vault 首条必须从 seq=1 / prevHash=null 开始');
101
+ } else {
102
+ if (ref.seq !== last.seq + 1) throw new SyncError('conflict', `Vault 下一条 seq 应为 ${last.seq + 1}`);
103
+ if (ref.prevHash !== last.record_hash) throw new SyncError('conflict', 'Vault prevHash 与当前链不匹配');
104
+ }
105
+ let blob;
106
+ try {
107
+ blob = readBlobSync(dataDir, ref.blobSha256);
108
+ } catch {
109
+ throw new SyncError('bad-request', `Vault blob 尚未上传:${ref.blobSha256}`);
110
+ }
111
+ if (blob.length !== ref.bytes) throw new SyncError('bad-request', 'Vault blob 字节数与索引不一致');
112
+ const revision = bumpRevision(db);
113
+ db.handle.prepare('INSERT INTO vault_records (dataset, writer, seq, prev_hash, blob_sha256, bytes, created_at, sealed_json, record_hash, revision) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)').run(DEFAULT_DATASET, ref.writer, ref.seq, ref.prevHash, ref.blobSha256, ref.bytes, ref.createdAt, JSON.stringify(ref.sealed), ref.recordHash, revision);
114
+ return {
115
+ record: {
116
+ ...ref,
117
+ revision
118
+ },
119
+ dedup: false
120
+ };
121
+ });
122
+ }
123
+ function readBlobSync(dataDir, sha256) {
124
+ return readFileSync(blobPath(dataDir, sha256));
125
+ }
126
+ export function readVaultCursor(db, since, limit) {
127
+ const rows = db.handle.prepare('SELECT writer, seq, prev_hash, blob_sha256, bytes, created_at, sealed_json, record_hash, revision FROM vault_records WHERE dataset = ? AND revision > ? ORDER BY revision ASC LIMIT ?').all(DEFAULT_DATASET, since, limit + 1);
128
+ return {
129
+ revision: readRevision(db),
130
+ records: rows.slice(0, limit).map(viewOf),
131
+ hasMore: rows.length > limit
132
+ };
133
+ }
134
+ export function readVaultStream(db, writer, from, limit) {
135
+ const rows = db.handle.prepare('SELECT writer, seq, prev_hash, blob_sha256, bytes, created_at, sealed_json, record_hash, revision FROM vault_records WHERE dataset = ? AND writer = ? AND seq >= ? ORDER BY seq ASC LIMIT ?').all(DEFAULT_DATASET, writer, from, limit + 1);
136
+ return {
137
+ writer,
138
+ records: rows.slice(0, limit).map(viewOf),
139
+ hasMore: rows.length > limit
140
+ };
141
+ }
142
+ export function validVaultWriter(writer) {
143
+ return /^[A-Za-z0-9._:-]{1,128}$/u.test(writer);
144
+ }
145
+
146
+
147
+ //# sourceURL=vault.ts
package/package.json CHANGED
@@ -1,6 +1,24 @@
1
1
  {
2
2
  "name": "dscs-server",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.0.1",
4
+ "private": false,
5
+ "type": "module",
6
+ "description": "DSH Session CloudSync self-hosted server.",
7
+ "license": "AGPL-3.0-only",
8
+ "bin": {
9
+ "dscs-server": "./dist/index.js"
10
+ },
11
+ "files": [
12
+ "dist",
13
+ "web",
14
+ "LICENSE",
15
+ "README.md"
16
+ ],
17
+ "scripts": {
18
+ "build": "node scripts/build.mjs",
19
+ "prepack": "node scripts/build.mjs"
20
+ },
21
+ "engines": {
22
+ "node": ">=24"
23
+ }
24
+ }