@korajs/server 1.0.0-beta.11 → 1.0.0-beta.13

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.
@@ -1,5 +1,5 @@
1
1
  import { SyncMessage } from '@korajs/sync';
2
- import { S as ServerTransport, a as ServerMessageHandler, b as ServerCloseHandler, c as ServerErrorHandler } from './server-transport-CU1BLWgr.cjs';
2
+ import { S as ServerTransport, a as ServerMessageHandler, b as ServerCloseHandler, c as ServerErrorHandler } from './server-transport-BGQkRCI4.cjs';
3
3
 
4
4
  /**
5
5
  * In-memory server transport for testing. Wraps the server side of a
@@ -1,5 +1,5 @@
1
1
  import { SyncMessage } from '@korajs/sync';
2
- import { S as ServerTransport, a as ServerMessageHandler, b as ServerCloseHandler, c as ServerErrorHandler } from './server-transport-CU1BLWgr.js';
2
+ import { S as ServerTransport, a as ServerMessageHandler, b as ServerCloseHandler, c as ServerErrorHandler } from './server-transport-BGQkRCI4.js';
3
3
 
4
4
  /**
5
5
  * In-memory server transport for testing. Wraps the server side of a
@@ -0,0 +1,249 @@
1
+ import {
2
+ SequenceConflictError,
3
+ assertBackupOperationsIngestible
4
+ } from "./chunk-IRRXJ7YN.js";
5
+
6
+ // src/store/server-backup.ts
7
+ import { KoraError } from "@korajs/core";
8
+ import { isKeyRecordSuccessor, validateKeyRecord } from "@korajs/sync";
9
+ var BACKUP_VERSION = 1;
10
+ function encodeSection(name, content) {
11
+ const nameBytes = new TextEncoder().encode(name);
12
+ const header = new Uint8Array(8);
13
+ const dv = new DataView(header.buffer);
14
+ dv.setUint32(0, nameBytes.length, true);
15
+ dv.setUint32(4, content.length, true);
16
+ const result = new Uint8Array(header.length + nameBytes.length + content.length);
17
+ result.set(header, 0);
18
+ result.set(nameBytes, 8);
19
+ result.set(content, 8 + nameBytes.length);
20
+ return result;
21
+ }
22
+ function encodeJsonSection(name, data) {
23
+ return encodeSection(name, new TextEncoder().encode(JSON.stringify(data)));
24
+ }
25
+ function parseSections(data) {
26
+ const sections = [];
27
+ const dv = new DataView(data.buffer, data.byteOffset, data.byteLength);
28
+ let offset = 0;
29
+ while (offset + 8 <= data.byteLength) {
30
+ const nameLen = dv.getUint32(offset, true);
31
+ const contentLen = dv.getUint32(offset + 4, true);
32
+ offset += 8;
33
+ if (offset + nameLen + contentLen > data.byteLength) break;
34
+ const name = new TextDecoder().decode(data.slice(offset, offset + nameLen));
35
+ offset += nameLen;
36
+ const content = data.slice(offset, offset + contentLen);
37
+ offset += contentLen;
38
+ sections.push({ name, content });
39
+ }
40
+ return sections;
41
+ }
42
+ function findSection(sections, name) {
43
+ for (const s of sections) {
44
+ if (s.name === name) return s.content;
45
+ }
46
+ return null;
47
+ }
48
+ function parseJsonSection(sections, name) {
49
+ const content = findSection(sections, name);
50
+ if (!content) return null;
51
+ return JSON.parse(new TextDecoder().decode(content));
52
+ }
53
+ async function computeSha256(data) {
54
+ const digestInput = new Uint8Array(data);
55
+ const hashBuffer = await crypto.subtle.digest("SHA-256", digestInput);
56
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
57
+ return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
58
+ }
59
+ async function buildServerBackup(nodeId, operations, versionVector, keyRecords = []) {
60
+ const sections = [];
61
+ let allContentForChecksum = new Uint8Array(0);
62
+ const addSection = (name, data) => {
63
+ sections.push(data);
64
+ const newLen = allContentForChecksum.length + data.length;
65
+ const combined = new Uint8Array(newLen);
66
+ combined.set(allContentForChecksum, 0);
67
+ combined.set(data, allContentForChecksum.length);
68
+ allContentForChecksum = combined;
69
+ };
70
+ const vvObj = {};
71
+ for (const [nid, seq] of versionVector) {
72
+ vvObj[nid] = seq;
73
+ }
74
+ addSection("version_vector", encodeJsonSection("version_vector", vvObj));
75
+ const opLines = `${operations.map((op) => JSON.stringify(op)).join("\n")}
76
+ `;
77
+ addSection("operations", encodeSection("operations", new TextEncoder().encode(opLines)));
78
+ if (keyRecords.length > 0) {
79
+ addSection("encryption_keys", encodeJsonSection("encryption_keys", keyRecords));
80
+ }
81
+ const checksumHex = await computeSha256(allContentForChecksum);
82
+ const manifest = {
83
+ version: BACKUP_VERSION,
84
+ createdAt: Date.now(),
85
+ nodeId,
86
+ schemaVersion: 1,
87
+ operationCount: operations.length,
88
+ collections: [],
89
+ includesRecords: false,
90
+ encryptionKeyRecordCount: keyRecords.length,
91
+ checksum: checksumHex
92
+ };
93
+ const manifestSection = encodeJsonSection("manifest", manifest);
94
+ const checksumSection = encodeSection("checksum", new TextEncoder().encode(checksumHex));
95
+ const totalLen = manifestSection.length + allContentForChecksum.length + checksumSection.length;
96
+ const result = new Uint8Array(totalLen);
97
+ let pos = 0;
98
+ result.set(manifestSection, pos);
99
+ pos += manifestSection.length;
100
+ result.set(allContentForChecksum, pos);
101
+ pos += allContentForChecksum.length;
102
+ result.set(checksumSection, pos);
103
+ return result;
104
+ }
105
+ function parseServerBackup(data) {
106
+ const sections = parseSections(data);
107
+ const opsContent = findSection(sections, "operations");
108
+ let operations = [];
109
+ if (opsContent) {
110
+ const text = new TextDecoder().decode(opsContent);
111
+ const lines = text.trim().split("\n").filter((l) => l.length > 0);
112
+ operations = lines.map((line) => JSON.parse(line));
113
+ }
114
+ const vvData = parseJsonSection(sections, "version_vector");
115
+ const versionVector = /* @__PURE__ */ new Map();
116
+ if (vvData) {
117
+ for (const [nid, seq] of Object.entries(vvData)) {
118
+ versionVector.set(nid, seq);
119
+ }
120
+ }
121
+ assertBackupOperationsIngestible(operations);
122
+ const keyRecords = parseKeyRecords(parseJsonSection(sections, "encryption_keys"));
123
+ return { operations, versionVector, keyRecords };
124
+ }
125
+ function parseKeyRecords(value) {
126
+ if (value === null || value === void 0) return [];
127
+ const refuse = (reason) => {
128
+ throw new KoraError(
129
+ `Backup rejected: its encryption key records are malformed (${reason}).`,
130
+ "BACKUP_INVALID_KEY_RECORD",
131
+ { reason }
132
+ );
133
+ };
134
+ if (!Array.isArray(value)) return refuse("not an array");
135
+ return value.map((entry, index) => {
136
+ if (typeof entry !== "object" || entry === null) return refuse(`entry ${index}`);
137
+ const { owner, keyring, revision, record } = entry;
138
+ if (typeof owner !== "string" || typeof keyring !== "string" || typeof record !== "string") {
139
+ return refuse(`entry ${index} fields`);
140
+ }
141
+ let parsed;
142
+ try {
143
+ parsed = JSON.parse(record);
144
+ } catch {
145
+ return refuse(`entry ${index} record is not JSON`);
146
+ }
147
+ const validation = validateKeyRecord(parsed, keyring);
148
+ if (!validation.ok) return refuse(`entry ${index}: ${validation.reason}`);
149
+ if (parsed.revision !== revision) {
150
+ return refuse(`entry ${index} revision does not match its record`);
151
+ }
152
+ return { owner, keyring, revision, record };
153
+ });
154
+ }
155
+ var KEY_RESTORE_ATTEMPTS = 8;
156
+ async function readStoredKeyRecord(store, owner, keyring) {
157
+ if (typeof store.listEncryptionKeyRecords === "function") {
158
+ const rows = await store.listEncryptionKeyRecords(owner);
159
+ const row = rows.find((entry) => entry.owner === owner && entry.keyring === keyring);
160
+ if (row === void 0) return null;
161
+ return { revision: Number(row.revision), record: parseStoredRecord(row.record) };
162
+ }
163
+ const json = await store.getEncryptionKeyRecord?.(owner, keyring);
164
+ if (json === null || json === void 0) return null;
165
+ const record = parseStoredRecord(json);
166
+ return { revision: record?.revision ?? 0, record };
167
+ }
168
+ function parseStoredRecord(json) {
169
+ try {
170
+ const parsed = JSON.parse(json);
171
+ return typeof parsed === "object" && parsed !== null ? parsed : null;
172
+ } catch {
173
+ return null;
174
+ }
175
+ }
176
+ function keyRestoreDecision(stored, incoming, merge) {
177
+ if (stored === null) return "insert";
178
+ if (!merge) {
179
+ return stored.revision === incoming.revision && stored.record !== null && JSON.stringify(stored.record) === JSON.stringify(JSON.parse(incoming.record)) ? "keep" : "replace";
180
+ }
181
+ if (stored.record === null) return "replace";
182
+ if (stored.record.ringId !== JSON.parse(incoming.record).ringId) {
183
+ return "keep";
184
+ }
185
+ if (incoming.revision <= stored.revision) return "keep";
186
+ const successor = isKeyRecordSuccessor(
187
+ stored.record,
188
+ JSON.parse(incoming.record)
189
+ );
190
+ if (!successor.ok) {
191
+ console.warn(
192
+ `[kora] Backup merge: the key record of ${incoming.owner}/${incoming.keyring} in the backup (revision ${String(incoming.revision)}) is not a successor of the stored revision ${String(stored.revision)} (${successor.reason}); the stored record is kept.`
193
+ );
194
+ return "keep";
195
+ }
196
+ return "replace";
197
+ }
198
+ async function restoreBackupKeyRecords(store, keyRecords, merge = true) {
199
+ if (keyRecords.length === 0) return 0;
200
+ if (typeof store.putEncryptionKeyRecord !== "function") {
201
+ console.warn(
202
+ `[kora] Backup restore: ${String(keyRecords.length)} encryption key record(s) were not restored: this store has no key table.`
203
+ );
204
+ return 0;
205
+ }
206
+ let restored = 0;
207
+ for (const row of keyRecords) {
208
+ let written = false;
209
+ for (let attempt = 0; attempt < KEY_RESTORE_ATTEMPTS && !written; attempt++) {
210
+ const stored = await readStoredKeyRecord(store, row.owner, row.keyring);
211
+ const decision = keyRestoreDecision(stored, row, merge);
212
+ if (decision === "keep") break;
213
+ written = await store.putEncryptionKeyRecord(
214
+ row.owner,
215
+ row.keyring,
216
+ row.record,
217
+ row.revision,
218
+ stored?.revision ?? 0
219
+ );
220
+ if (written) restored++;
221
+ }
222
+ }
223
+ return restored;
224
+ }
225
+ async function mergeBackupOperations(operations, apply) {
226
+ let restored = 0;
227
+ const conflicts = [];
228
+ for (const op of operations) {
229
+ try {
230
+ if (await apply(op) === "applied") restored++;
231
+ } catch (error) {
232
+ if (!(error instanceof SequenceConflictError)) throw error;
233
+ conflicts.push(op.id);
234
+ }
235
+ }
236
+ if (conflicts.length > 0) {
237
+ console.warn(
238
+ `[kora] Backup merge: ${String(conflicts.length)} operation(s) were not restored because another operation already holds their node and sequence number (SEQUENCE_CONFLICT). First: ${conflicts.slice(0, 5).join(", ")}`
239
+ );
240
+ }
241
+ return { operationsRestored: restored, success: conflicts.length === 0 };
242
+ }
243
+ export {
244
+ buildServerBackup,
245
+ mergeBackupOperations,
246
+ parseServerBackup,
247
+ restoreBackupKeyRecords
248
+ };
249
+ //# sourceMappingURL=server-backup-WBIYHWYQ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/store/server-backup.ts"],"sourcesContent":["import type { Operation } from '@korajs/core'\nimport { KoraError } from '@korajs/core'\nimport type { WrappedKeyRecord } from '@korajs/sync'\nimport { isKeyRecordSuccessor, validateKeyRecord } from '@korajs/sync'\nimport { assertBackupOperationsIngestible } from '../apply/ingest-validation'\nimport type { EncryptionKeyRecordRow, ServerStore } from './server-store'\nimport { SequenceConflictError } from './server-store'\n\n/**\n * Server-side backup format: same portable section-based format as the client.\n *\n * Sections:\n * manifest — metadata (version, nodeId, operationCount, checksum)\n * version_vector — nodeId → maxSequenceNumber\n * operations — NDJSON of full Operation objects\n * encryption_keys — JSON array of the stored end-to-end key records (optional; RT-104)\n * checksum — SHA-256 of all content sections\n *\n * A reader that predates `encryption_keys` ignores it. Without it, a server restored\n * from its backup would hold encrypted history but no key record to open it with.\n */\n\nconst BACKUP_VERSION = 1\n\nfunction encodeSection(name: string, content: Uint8Array): Uint8Array {\n\tconst nameBytes = new TextEncoder().encode(name)\n\tconst header = new Uint8Array(8)\n\tconst dv = new DataView(header.buffer)\n\tdv.setUint32(0, nameBytes.length, true)\n\tdv.setUint32(4, content.length, true)\n\n\tconst result = new Uint8Array(header.length + nameBytes.length + content.length)\n\tresult.set(header, 0)\n\tresult.set(nameBytes, 8)\n\tresult.set(content, 8 + nameBytes.length)\n\treturn result\n}\n\nfunction encodeJsonSection(name: string, data: unknown): Uint8Array {\n\treturn encodeSection(name, new TextEncoder().encode(JSON.stringify(data)))\n}\n\ninterface Section {\n\tname: string\n\tcontent: Uint8Array\n}\n\nfunction parseSections(data: Uint8Array): Section[] {\n\tconst sections: Section[] = []\n\tconst dv = new DataView(data.buffer, data.byteOffset, data.byteLength)\n\tlet offset = 0\n\n\twhile (offset + 8 <= data.byteLength) {\n\t\tconst nameLen = dv.getUint32(offset, true)\n\t\tconst contentLen = dv.getUint32(offset + 4, true)\n\t\toffset += 8\n\n\t\tif (offset + nameLen + contentLen > data.byteLength) break\n\n\t\tconst name = new TextDecoder().decode(data.slice(offset, offset + nameLen))\n\t\toffset += nameLen\n\n\t\tconst content = data.slice(offset, offset + contentLen)\n\t\toffset += contentLen\n\n\t\tsections.push({ name, content })\n\t}\n\n\treturn sections\n}\n\nfunction findSection(sections: Section[], name: string): Uint8Array | null {\n\tfor (const s of sections) {\n\t\tif (s.name === name) return s.content\n\t}\n\treturn null\n}\n\nfunction parseJsonSection<T>(sections: Section[], name: string): T | null {\n\tconst content = findSection(sections, name)\n\tif (!content) return null\n\treturn JSON.parse(new TextDecoder().decode(content)) as T\n}\n\nasync function computeSha256(data: Uint8Array): Promise<string> {\n\tconst digestInput = new Uint8Array(data)\n\tconst hashBuffer = await crypto.subtle.digest('SHA-256', digestInput)\n\tconst hashArray = Array.from(new Uint8Array(hashBuffer))\n\treturn hashArray.map((b) => b.toString(16).padStart(2, '0')).join('')\n}\n\n/**\n * Build a backup binary from server store data.\n */\nexport async function buildServerBackup(\n\tnodeId: string,\n\toperations: Operation[],\n\tversionVector: Map<string, number>,\n\tkeyRecords: EncryptionKeyRecordRow[] = [],\n): Promise<Uint8Array> {\n\tconst sections: Uint8Array[] = []\n\tlet allContentForChecksum = new Uint8Array(0)\n\n\tconst addSection = (name: string, data: Uint8Array) => {\n\t\tsections.push(data)\n\t\tconst newLen = allContentForChecksum.length + data.length\n\t\tconst combined = new Uint8Array(newLen)\n\t\tcombined.set(allContentForChecksum, 0)\n\t\tcombined.set(data, allContentForChecksum.length)\n\t\tallContentForChecksum = combined\n\t}\n\n\t// Version vector\n\tconst vvObj: Record<string, number> = {}\n\tfor (const [nid, seq] of versionVector) {\n\t\tvvObj[nid] = seq\n\t}\n\taddSection('version_vector', encodeJsonSection('version_vector', vvObj))\n\n\t// Operations\n\tconst opLines = `${operations.map((op) => JSON.stringify(op)).join('\\n')}\\n`\n\taddSection('operations', encodeSection('operations', new TextEncoder().encode(opLines)))\n\n\t// End-to-end key records (opaque: the server cannot open them)\n\tif (keyRecords.length > 0) {\n\t\taddSection('encryption_keys', encodeJsonSection('encryption_keys', keyRecords))\n\t}\n\n\t// Checksum\n\tconst checksumHex = await computeSha256(allContentForChecksum)\n\n\t// Manifest\n\tconst manifest = {\n\t\tversion: BACKUP_VERSION,\n\t\tcreatedAt: Date.now(),\n\t\tnodeId,\n\t\tschemaVersion: 1,\n\t\toperationCount: operations.length,\n\t\tcollections: [] as string[],\n\t\tincludesRecords: false,\n\t\tencryptionKeyRecordCount: keyRecords.length,\n\t\tchecksum: checksumHex,\n\t}\n\n\tconst manifestSection = encodeJsonSection('manifest', manifest)\n\tconst checksumSection = encodeSection('checksum', new TextEncoder().encode(checksumHex))\n\n\tconst totalLen = manifestSection.length + allContentForChecksum.length + checksumSection.length\n\tconst result = new Uint8Array(totalLen)\n\tlet pos = 0\n\tresult.set(manifestSection, pos)\n\tpos += manifestSection.length\n\tresult.set(allContentForChecksum, pos)\n\tpos += allContentForChecksum.length\n\tresult.set(checksumSection, pos)\n\n\treturn result\n}\n\n/**\n * Parse a backup and return the operations, version vector and key records.\n */\nexport function parseServerBackup(data: Uint8Array): {\n\toperations: Operation[]\n\tversionVector: Map<string, number>\n\tkeyRecords: EncryptionKeyRecordRow[]\n} {\n\tconst sections = parseSections(data)\n\n\t// Parse operations\n\tconst opsContent = findSection(sections, 'operations')\n\tlet operations: Operation[] = []\n\tif (opsContent) {\n\t\tconst text = new TextDecoder().decode(opsContent)\n\t\tconst lines = text\n\t\t\t.trim()\n\t\t\t.split('\\n')\n\t\t\t.filter((l) => l.length > 0)\n\t\toperations = lines.map((line) => JSON.parse(line) as Operation)\n\t}\n\n\t// Parse version vector\n\tconst vvData = parseJsonSection<Record<string, number>>(sections, 'version_vector')\n\tconst versionVector = new Map<string, number>()\n\tif (vvData) {\n\t\tfor (const [nid, seq] of Object.entries(vvData)) {\n\t\t\tversionVector.set(nid, seq)\n\t\t}\n\t}\n\n\t// Every ingest path validates against server time (SYNC-7): a backup whose\n\t// operations are far-future or malformed is refused whole, before anything changes.\n\tassertBackupOperationsIngestible(operations)\n\tconst keyRecords = parseKeyRecords(parseJsonSection<unknown>(sections, 'encryption_keys'))\n\n\treturn { operations, versionVector, keyRecords }\n}\n\n/** Validate the `encryption_keys` section: a malformed entry refuses the whole backup. */\nfunction parseKeyRecords(value: unknown): EncryptionKeyRecordRow[] {\n\tif (value === null || value === undefined) return []\n\tconst refuse = (reason: string): never => {\n\t\tthrow new KoraError(\n\t\t\t`Backup rejected: its encryption key records are malformed (${reason}).`,\n\t\t\t'BACKUP_INVALID_KEY_RECORD',\n\t\t\t{ reason },\n\t\t)\n\t}\n\tif (!Array.isArray(value)) return refuse('not an array')\n\treturn value.map((entry: unknown, index): EncryptionKeyRecordRow => {\n\t\tif (typeof entry !== 'object' || entry === null) return refuse(`entry ${index}`)\n\t\tconst { owner, keyring, revision, record } = entry as Record<string, unknown>\n\t\tif (typeof owner !== 'string' || typeof keyring !== 'string' || typeof record !== 'string') {\n\t\t\treturn refuse(`entry ${index} fields`)\n\t\t}\n\t\tlet parsed: unknown\n\t\ttry {\n\t\t\tparsed = JSON.parse(record)\n\t\t} catch {\n\t\t\treturn refuse(`entry ${index} record is not JSON`)\n\t\t}\n\t\tconst validation = validateKeyRecord(parsed, keyring)\n\t\tif (!validation.ok) return refuse(`entry ${index}: ${validation.reason}`)\n\t\tif ((parsed as { revision: unknown }).revision !== revision) {\n\t\t\treturn refuse(`entry ${index} revision does not match its record`)\n\t\t}\n\t\treturn { owner, keyring, revision: revision as number, record }\n\t})\n}\n\n/** Maximum compare-and-set attempts per restored key record (a live write may race it). */\nconst KEY_RESTORE_ATTEMPTS = 8\n\n/** The stored row of one (owner, keyring), with its ring id when the record is readable. */\nasync function readStoredKeyRecord(\n\tstore: Pick<ServerStore, 'getEncryptionKeyRecord' | 'listEncryptionKeyRecords'>,\n\towner: string,\n\tkeyring: string,\n): Promise<{ revision: number; record: WrappedKeyRecord | null } | null> {\n\tif (typeof store.listEncryptionKeyRecords === 'function') {\n\t\tconst rows = await store.listEncryptionKeyRecords(owner)\n\t\tconst row = rows.find((entry) => entry.owner === owner && entry.keyring === keyring)\n\t\tif (row === undefined) return null\n\t\treturn { revision: Number(row.revision), record: parseStoredRecord(row.record) }\n\t}\n\tconst json = await store.getEncryptionKeyRecord?.(owner, keyring)\n\tif (json === null || json === undefined) return null\n\tconst record = parseStoredRecord(json)\n\treturn { revision: record?.revision ?? 0, record }\n}\n\nfunction parseStoredRecord(json: string): WrappedKeyRecord | null {\n\ttry {\n\t\tconst parsed = JSON.parse(json) as WrappedKeyRecord\n\t\treturn typeof parsed === 'object' && parsed !== null ? parsed : null\n\t} catch {\n\t\treturn null\n\t}\n}\n\n/**\n * What a restore does with one backed-up key record, given the stored one (RT-110).\n *\n * - **Replace mode**: the backup's record replaces the stored one, whatever its revision.\n * Replace mode makes the server what the backup was (an operator resetting a damaged\n * or tampered server); the key record the restored history was written under is the\n * backup's.\n * - **Merge mode**: an absent record is inserted. A stored record of the SAME ring is\n * advanced to the backup's only when the backup's is a valid successor (a higher\n * revision that keeps every key version and retired master). An equal or newer stored\n * revision is kept: it already holds every key the backup's does. A stored record of a\n * DIFFERENT ring is kept: the store holds one record per (owner, keyring), so that ring\n * was created after the loss; a device holding both rings merges them (KEY_RING_FORK).\n *\n * The server cannot check a record's MAC (only a holder of the ring's master key can),\n * so neither rule trusts the backup's record more than a device would: every device\n * authenticates a record before using it, refuses a revision below the one it pinned\n * (KEY_RECORD_ROLLBACK, and re-uploads its newer one), and refuses a record its keys do\n * not authenticate. A forged or rolled-back record in a backup can delay a device, never\n * make it adopt a key it did not authenticate.\n */\nfunction keyRestoreDecision(\n\tstored: { revision: number; record: WrappedKeyRecord | null } | null,\n\tincoming: EncryptionKeyRecordRow,\n\tmerge: boolean,\n): 'insert' | 'replace' | 'keep' {\n\tif (stored === null) return 'insert'\n\tif (!merge) {\n\t\treturn stored.revision === incoming.revision &&\n\t\t\tstored.record !== null &&\n\t\t\tJSON.stringify(stored.record) === JSON.stringify(JSON.parse(incoming.record))\n\t\t\t? 'keep'\n\t\t\t: 'replace'\n\t}\n\tif (stored.record === null) return 'replace'\n\tif (stored.record.ringId !== (JSON.parse(incoming.record) as WrappedKeyRecord).ringId) {\n\t\treturn 'keep'\n\t}\n\tif (incoming.revision <= stored.revision) return 'keep'\n\tconst successor = isKeyRecordSuccessor(\n\t\tstored.record,\n\t\tJSON.parse(incoming.record) as WrappedKeyRecord,\n\t)\n\tif (!successor.ok) {\n\t\tconsole.warn(\n\t\t\t`[kora] Backup merge: the key record of ${incoming.owner}/${incoming.keyring} in the backup (revision ${String(incoming.revision)}) is not a successor of the stored revision ${String(stored.revision)} (${successor.reason}); the stored record is kept.`,\n\t\t)\n\t\treturn 'keep'\n\t}\n\treturn 'replace'\n}\n\n/**\n * Restore backed-up key records (RT-104, RT-110) in either restore mode: the key table\n * is not part of the operation log, so it is reconciled record by record. See\n * {@link keyRestoreDecision} for the rules. Records the store holds that the backup does\n * not name are kept (a ring the backup never saw; removing it could only lose keys).\n *\n * @param merge - Merge-mode restore (true) or replace-mode restore (false)\n * @returns The number of records inserted or replaced\n */\nexport async function restoreBackupKeyRecords(\n\tstore: Pick<\n\t\tServerStore,\n\t\t'putEncryptionKeyRecord' | 'getEncryptionKeyRecord' | 'listEncryptionKeyRecords'\n\t>,\n\tkeyRecords: EncryptionKeyRecordRow[],\n\tmerge = true,\n): Promise<number> {\n\tif (keyRecords.length === 0) return 0\n\tif (typeof store.putEncryptionKeyRecord !== 'function') {\n\t\tconsole.warn(\n\t\t\t`[kora] Backup restore: ${String(keyRecords.length)} encryption key record(s) were not restored: this store has no key table.`,\n\t\t)\n\t\treturn 0\n\t}\n\tlet restored = 0\n\tfor (const row of keyRecords) {\n\t\tlet written = false\n\t\tfor (let attempt = 0; attempt < KEY_RESTORE_ATTEMPTS && !written; attempt++) {\n\t\t\tconst stored = await readStoredKeyRecord(store, row.owner, row.keyring)\n\t\t\tconst decision = keyRestoreDecision(stored, row, merge)\n\t\t\tif (decision === 'keep') break\n\t\t\t// Compare-and-set against the revision just read: a key write from a live device\n\t\t\t// in between makes it fail, and the record is decided again.\n\t\t\twritten = await store.putEncryptionKeyRecord(\n\t\t\t\trow.owner,\n\t\t\t\trow.keyring,\n\t\t\t\trow.record,\n\t\t\t\trow.revision,\n\t\t\t\tstored?.revision ?? 0,\n\t\t\t)\n\t\t\tif (written) restored++\n\t\t}\n\t}\n\treturn restored\n}\n\n/**\n * Merge-mode restore shared by the built-in stores: apply every backup operation\n * through the store's normal append (dedup by id). An operation refused for a\n * sequence conflict (another operation holds its node and sequence, W3 step 4) is\n * counted, not thrown, so the rest of the backup still restores; the result is then\n * `success: false` and the conflicts are logged, never silently dropped.\n */\nexport async function mergeBackupOperations(\n\toperations: Operation[],\n\tapply: (op: Operation) => Promise<string>,\n): Promise<{ operationsRestored: number; success: boolean }> {\n\tlet restored = 0\n\tconst conflicts: string[] = []\n\tfor (const op of operations) {\n\t\ttry {\n\t\t\tif ((await apply(op)) === 'applied') restored++\n\t\t} catch (error) {\n\t\t\tif (!(error instanceof SequenceConflictError)) throw error\n\t\t\tconflicts.push(op.id)\n\t\t}\n\t}\n\tif (conflicts.length > 0) {\n\t\tconsole.warn(\n\t\t\t`[kora] Backup merge: ${String(conflicts.length)} operation(s) were not restored because another operation already holds their node and sequence number (SEQUENCE_CONFLICT). First: ${conflicts.slice(0, 5).join(', ')}`,\n\t\t)\n\t}\n\treturn { operationsRestored: restored, success: conflicts.length === 0 }\n}\n"],"mappings":"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","names":[]}
@@ -30,6 +30,13 @@ interface ServerTransport {
30
30
  isConnected(): boolean;
31
31
  /** Close the connection to the client */
32
32
  close(code?: number, reason?: string): void;
33
+ /**
34
+ * Bytes queued for the client but not yet written to the network. The delivery
35
+ * stream pauses while this is above its high-water mark (backpressure), so a slow
36
+ * client cannot make the server buffer its whole backlog. Optional: transports
37
+ * that cannot tell are treated as always drained.
38
+ */
39
+ bufferedAmount?(): number;
33
40
  }
34
41
 
35
42
  export type { ServerTransport as S, ServerMessageHandler as a, ServerCloseHandler as b, ServerErrorHandler as c };
@@ -30,6 +30,13 @@ interface ServerTransport {
30
30
  isConnected(): boolean;
31
31
  /** Close the connection to the client */
32
32
  close(code?: number, reason?: string): void;
33
+ /**
34
+ * Bytes queued for the client but not yet written to the network. The delivery
35
+ * stream pauses while this is above its high-water mark (backpressure), so a slow
36
+ * client cannot make the server buffer its whole backlog. Optional: transports
37
+ * that cannot tell are treated as always drained.
38
+ */
39
+ bufferedAmount?(): number;
33
40
  }
34
41
 
35
42
  export type { ServerTransport as S, ServerMessageHandler as a, ServerCloseHandler as b, ServerErrorHandler as c };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@korajs/server",
3
- "version": "1.0.0-beta.11",
3
+ "version": "1.0.0-beta.13",
4
4
  "description": "Kora.js self-hosted sync server",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -36,9 +36,9 @@
36
36
  "better-sqlite3": "^12.8.0",
37
37
  "drizzle-orm": "^0.45.2",
38
38
  "ws": "^8.18.0",
39
- "@korajs/merge": "1.0.0-beta.11",
40
- "@korajs/sync": "1.0.0-beta.11",
41
- "@korajs/core": "1.0.0-beta.11"
39
+ "@korajs/core": "1.0.0-beta.13",
40
+ "@korajs/merge": "1.0.0-beta.13",
41
+ "@korajs/sync": "1.0.0-beta.13"
42
42
  },
43
43
  "peerDependencies": {
44
44
  "postgres": "^3.4.0"
@@ -1,116 +0,0 @@
1
- // src/store/server-backup.ts
2
- var BACKUP_VERSION = 1;
3
- function encodeSection(name, content) {
4
- const nameBytes = new TextEncoder().encode(name);
5
- const header = new Uint8Array(8);
6
- const dv = new DataView(header.buffer);
7
- dv.setUint32(0, nameBytes.length, true);
8
- dv.setUint32(4, content.length, true);
9
- const result = new Uint8Array(header.length + nameBytes.length + content.length);
10
- result.set(header, 0);
11
- result.set(nameBytes, 8);
12
- result.set(content, 8 + nameBytes.length);
13
- return result;
14
- }
15
- function encodeJsonSection(name, data) {
16
- return encodeSection(name, new TextEncoder().encode(JSON.stringify(data)));
17
- }
18
- function parseSections(data) {
19
- const sections = [];
20
- const dv = new DataView(data.buffer, data.byteOffset, data.byteLength);
21
- let offset = 0;
22
- while (offset + 8 <= data.byteLength) {
23
- const nameLen = dv.getUint32(offset, true);
24
- const contentLen = dv.getUint32(offset + 4, true);
25
- offset += 8;
26
- if (offset + nameLen + contentLen > data.byteLength) break;
27
- const name = new TextDecoder().decode(data.slice(offset, offset + nameLen));
28
- offset += nameLen;
29
- const content = data.slice(offset, offset + contentLen);
30
- offset += contentLen;
31
- sections.push({ name, content });
32
- }
33
- return sections;
34
- }
35
- function findSection(sections, name) {
36
- for (const s of sections) {
37
- if (s.name === name) return s.content;
38
- }
39
- return null;
40
- }
41
- function parseJsonSection(sections, name) {
42
- const content = findSection(sections, name);
43
- if (!content) return null;
44
- return JSON.parse(new TextDecoder().decode(content));
45
- }
46
- async function computeSha256(data) {
47
- const digestInput = new Uint8Array(data);
48
- const hashBuffer = await crypto.subtle.digest("SHA-256", digestInput);
49
- const hashArray = Array.from(new Uint8Array(hashBuffer));
50
- return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
51
- }
52
- async function buildServerBackup(nodeId, operations, versionVector) {
53
- const sections = [];
54
- let allContentForChecksum = new Uint8Array(0);
55
- const addSection = (name, data) => {
56
- sections.push(data);
57
- const newLen = allContentForChecksum.length + data.length;
58
- const combined = new Uint8Array(newLen);
59
- combined.set(allContentForChecksum, 0);
60
- combined.set(data, allContentForChecksum.length);
61
- allContentForChecksum = combined;
62
- };
63
- const vvObj = {};
64
- for (const [nid, seq] of versionVector) {
65
- vvObj[nid] = seq;
66
- }
67
- addSection("version_vector", encodeJsonSection("version_vector", vvObj));
68
- const opLines = `${operations.map((op) => JSON.stringify(op)).join("\n")}
69
- `;
70
- addSection("operations", encodeSection("operations", new TextEncoder().encode(opLines)));
71
- const checksumHex = await computeSha256(allContentForChecksum);
72
- const manifest = {
73
- version: BACKUP_VERSION,
74
- createdAt: Date.now(),
75
- nodeId,
76
- schemaVersion: 1,
77
- operationCount: operations.length,
78
- collections: [],
79
- includesRecords: false,
80
- checksum: checksumHex
81
- };
82
- const manifestSection = encodeJsonSection("manifest", manifest);
83
- const checksumSection = encodeSection("checksum", new TextEncoder().encode(checksumHex));
84
- const totalLen = manifestSection.length + allContentForChecksum.length + checksumSection.length;
85
- const result = new Uint8Array(totalLen);
86
- let pos = 0;
87
- result.set(manifestSection, pos);
88
- pos += manifestSection.length;
89
- result.set(allContentForChecksum, pos);
90
- pos += allContentForChecksum.length;
91
- result.set(checksumSection, pos);
92
- return result;
93
- }
94
- function parseServerBackup(data) {
95
- const sections = parseSections(data);
96
- const opsContent = findSection(sections, "operations");
97
- let operations = [];
98
- if (opsContent) {
99
- const text = new TextDecoder().decode(opsContent);
100
- const lines = text.trim().split("\n").filter((l) => l.length > 0);
101
- operations = lines.map((line) => JSON.parse(line));
102
- }
103
- const vvData = parseJsonSection(sections, "version_vector");
104
- const versionVector = /* @__PURE__ */ new Map();
105
- if (vvData) {
106
- for (const [nid, seq] of Object.entries(vvData)) {
107
- versionVector.set(nid, seq);
108
- }
109
- }
110
- return { operations, versionVector };
111
- }
112
- export {
113
- buildServerBackup,
114
- parseServerBackup
115
- };
116
- //# sourceMappingURL=server-backup-MOICK6MI.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/store/server-backup.ts"],"sourcesContent":["import type { Operation } from '@korajs/core'\n\n/**\n * Server-side backup format: same portable section-based format as the client.\n *\n * Sections:\n * manifest — metadata (version, nodeId, operationCount, checksum)\n * version_vector — nodeId → maxSequenceNumber\n * operations — NDJSON of full Operation objects\n * checksum — SHA-256 of all content sections\n */\n\nconst BACKUP_VERSION = 1\n\nfunction encodeSection(name: string, content: Uint8Array): Uint8Array {\n\tconst nameBytes = new TextEncoder().encode(name)\n\tconst header = new Uint8Array(8)\n\tconst dv = new DataView(header.buffer)\n\tdv.setUint32(0, nameBytes.length, true)\n\tdv.setUint32(4, content.length, true)\n\n\tconst result = new Uint8Array(header.length + nameBytes.length + content.length)\n\tresult.set(header, 0)\n\tresult.set(nameBytes, 8)\n\tresult.set(content, 8 + nameBytes.length)\n\treturn result\n}\n\nfunction encodeJsonSection(name: string, data: unknown): Uint8Array {\n\treturn encodeSection(name, new TextEncoder().encode(JSON.stringify(data)))\n}\n\ninterface Section {\n\tname: string\n\tcontent: Uint8Array\n}\n\nfunction parseSections(data: Uint8Array): Section[] {\n\tconst sections: Section[] = []\n\tconst dv = new DataView(data.buffer, data.byteOffset, data.byteLength)\n\tlet offset = 0\n\n\twhile (offset + 8 <= data.byteLength) {\n\t\tconst nameLen = dv.getUint32(offset, true)\n\t\tconst contentLen = dv.getUint32(offset + 4, true)\n\t\toffset += 8\n\n\t\tif (offset + nameLen + contentLen > data.byteLength) break\n\n\t\tconst name = new TextDecoder().decode(data.slice(offset, offset + nameLen))\n\t\toffset += nameLen\n\n\t\tconst content = data.slice(offset, offset + contentLen)\n\t\toffset += contentLen\n\n\t\tsections.push({ name, content })\n\t}\n\n\treturn sections\n}\n\nfunction findSection(sections: Section[], name: string): Uint8Array | null {\n\tfor (const s of sections) {\n\t\tif (s.name === name) return s.content\n\t}\n\treturn null\n}\n\nfunction parseJsonSection<T>(sections: Section[], name: string): T | null {\n\tconst content = findSection(sections, name)\n\tif (!content) return null\n\treturn JSON.parse(new TextDecoder().decode(content)) as T\n}\n\nasync function computeSha256(data: Uint8Array): Promise<string> {\n\tconst digestInput = new Uint8Array(data)\n\tconst hashBuffer = await crypto.subtle.digest('SHA-256', digestInput)\n\tconst hashArray = Array.from(new Uint8Array(hashBuffer))\n\treturn hashArray.map((b) => b.toString(16).padStart(2, '0')).join('')\n}\n\n/**\n * Build a backup binary from server store data.\n */\nexport async function buildServerBackup(\n\tnodeId: string,\n\toperations: Operation[],\n\tversionVector: Map<string, number>,\n): Promise<Uint8Array> {\n\tconst sections: Uint8Array[] = []\n\tlet allContentForChecksum = new Uint8Array(0)\n\n\tconst addSection = (name: string, data: Uint8Array) => {\n\t\tsections.push(data)\n\t\tconst newLen = allContentForChecksum.length + data.length\n\t\tconst combined = new Uint8Array(newLen)\n\t\tcombined.set(allContentForChecksum, 0)\n\t\tcombined.set(data, allContentForChecksum.length)\n\t\tallContentForChecksum = combined\n\t}\n\n\t// Version vector\n\tconst vvObj: Record<string, number> = {}\n\tfor (const [nid, seq] of versionVector) {\n\t\tvvObj[nid] = seq\n\t}\n\taddSection('version_vector', encodeJsonSection('version_vector', vvObj))\n\n\t// Operations\n\tconst opLines = `${operations.map((op) => JSON.stringify(op)).join('\\n')}\\n`\n\taddSection('operations', encodeSection('operations', new TextEncoder().encode(opLines)))\n\n\t// Checksum\n\tconst checksumHex = await computeSha256(allContentForChecksum)\n\n\t// Manifest\n\tconst manifest = {\n\t\tversion: BACKUP_VERSION,\n\t\tcreatedAt: Date.now(),\n\t\tnodeId,\n\t\tschemaVersion: 1,\n\t\toperationCount: operations.length,\n\t\tcollections: [] as string[],\n\t\tincludesRecords: false,\n\t\tchecksum: checksumHex,\n\t}\n\n\tconst manifestSection = encodeJsonSection('manifest', manifest)\n\tconst checksumSection = encodeSection('checksum', new TextEncoder().encode(checksumHex))\n\n\tconst totalLen = manifestSection.length + allContentForChecksum.length + checksumSection.length\n\tconst result = new Uint8Array(totalLen)\n\tlet pos = 0\n\tresult.set(manifestSection, pos)\n\tpos += manifestSection.length\n\tresult.set(allContentForChecksum, pos)\n\tpos += allContentForChecksum.length\n\tresult.set(checksumSection, pos)\n\n\treturn result\n}\n\n/**\n * Parse a backup and return the operations and version vector.\n */\nexport function parseServerBackup(data: Uint8Array): {\n\toperations: Operation[]\n\tversionVector: Map<string, number>\n} {\n\tconst sections = parseSections(data)\n\n\t// Parse operations\n\tconst opsContent = findSection(sections, 'operations')\n\tlet operations: Operation[] = []\n\tif (opsContent) {\n\t\tconst text = new TextDecoder().decode(opsContent)\n\t\tconst lines = text\n\t\t\t.trim()\n\t\t\t.split('\\n')\n\t\t\t.filter((l) => l.length > 0)\n\t\toperations = lines.map((line) => JSON.parse(line) as Operation)\n\t}\n\n\t// Parse version vector\n\tconst vvData = parseJsonSection<Record<string, number>>(sections, 'version_vector')\n\tconst versionVector = new Map<string, number>()\n\tif (vvData) {\n\t\tfor (const [nid, seq] of Object.entries(vvData)) {\n\t\t\tversionVector.set(nid, seq)\n\t\t}\n\t}\n\n\treturn { operations, versionVector }\n}\n"],"mappings":"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","names":[]}