@korajs/server 0.4.0 → 0.6.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/index.cjs CHANGED
@@ -5,6 +5,9 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
6
  var __getProtoOf = Object.getPrototypeOf;
7
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __esm = (fn, res) => function __init() {
9
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
10
+ };
8
11
  var __export = (target, all) => {
9
12
  for (var name in all)
10
13
  __defProp(target, name, { get: all[name], enumerable: true });
@@ -27,6 +30,140 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
27
30
  ));
28
31
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
32
 
33
+ // ../../node_modules/.pnpm/tsup@8.5.1_jiti@2.7.0_postcss@8.5.8_tsx@4.21.0_typescript@5.9.3/node_modules/tsup/assets/cjs_shims.js
34
+ var getImportMetaUrl, importMetaUrl;
35
+ var init_cjs_shims = __esm({
36
+ "../../node_modules/.pnpm/tsup@8.5.1_jiti@2.7.0_postcss@8.5.8_tsx@4.21.0_typescript@5.9.3/node_modules/tsup/assets/cjs_shims.js"() {
37
+ "use strict";
38
+ getImportMetaUrl = () => typeof document === "undefined" ? new URL(`file:${__filename}`).href : document.currentScript && document.currentScript.tagName.toUpperCase() === "SCRIPT" ? document.currentScript.src : new URL("main.js", document.baseURI).href;
39
+ importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
40
+ }
41
+ });
42
+
43
+ // src/store/server-backup.ts
44
+ var server_backup_exports = {};
45
+ __export(server_backup_exports, {
46
+ buildServerBackup: () => buildServerBackup,
47
+ parseServerBackup: () => parseServerBackup
48
+ });
49
+ function encodeSection(name, content) {
50
+ const nameBytes = new TextEncoder().encode(name);
51
+ const header = new Uint8Array(8);
52
+ const dv = new DataView(header.buffer);
53
+ dv.setUint32(0, nameBytes.length, true);
54
+ dv.setUint32(4, content.length, true);
55
+ const result = new Uint8Array(header.length + nameBytes.length + content.length);
56
+ result.set(header, 0);
57
+ result.set(nameBytes, 8);
58
+ result.set(content, 8 + nameBytes.length);
59
+ return result;
60
+ }
61
+ function encodeJsonSection(name, data) {
62
+ return encodeSection(name, new TextEncoder().encode(JSON.stringify(data)));
63
+ }
64
+ function parseSections(data) {
65
+ const sections = [];
66
+ const dv = new DataView(data.buffer, data.byteOffset, data.byteLength);
67
+ let offset = 0;
68
+ while (offset + 8 <= data.byteLength) {
69
+ const nameLen = dv.getUint32(offset, true);
70
+ const contentLen = dv.getUint32(offset + 4, true);
71
+ offset += 8;
72
+ if (offset + nameLen + contentLen > data.byteLength) break;
73
+ const name = new TextDecoder().decode(data.slice(offset, offset + nameLen));
74
+ offset += nameLen;
75
+ const content = data.slice(offset, offset + contentLen);
76
+ offset += contentLen;
77
+ sections.push({ name, content });
78
+ }
79
+ return sections;
80
+ }
81
+ function findSection(sections, name) {
82
+ for (const s of sections) {
83
+ if (s.name === name) return s.content;
84
+ }
85
+ return null;
86
+ }
87
+ function parseJsonSection(sections, name) {
88
+ const content = findSection(sections, name);
89
+ if (!content) return null;
90
+ return JSON.parse(new TextDecoder().decode(content));
91
+ }
92
+ async function computeSha256(data) {
93
+ const digestInput = new Uint8Array(data);
94
+ const hashBuffer = await crypto.subtle.digest("SHA-256", digestInput);
95
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
96
+ return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
97
+ }
98
+ async function buildServerBackup(nodeId, operations2, versionVector) {
99
+ const sections = [];
100
+ let allContentForChecksum = new Uint8Array(0);
101
+ const addSection = (name, data) => {
102
+ sections.push(data);
103
+ const newLen = allContentForChecksum.length + data.length;
104
+ const combined = new Uint8Array(newLen);
105
+ combined.set(allContentForChecksum, 0);
106
+ combined.set(data, allContentForChecksum.length);
107
+ allContentForChecksum = combined;
108
+ };
109
+ const vvObj = {};
110
+ for (const [nid, seq] of versionVector) {
111
+ vvObj[nid] = seq;
112
+ }
113
+ addSection("version_vector", encodeJsonSection("version_vector", vvObj));
114
+ const opLines = `${operations2.map((op) => JSON.stringify(op)).join("\n")}
115
+ `;
116
+ addSection("operations", encodeSection("operations", new TextEncoder().encode(opLines)));
117
+ const checksumHex = await computeSha256(allContentForChecksum);
118
+ const manifest = {
119
+ version: BACKUP_VERSION,
120
+ createdAt: Date.now(),
121
+ nodeId,
122
+ schemaVersion: 1,
123
+ operationCount: operations2.length,
124
+ collections: [],
125
+ includesRecords: false,
126
+ checksum: checksumHex
127
+ };
128
+ const manifestSection = encodeJsonSection("manifest", manifest);
129
+ const checksumSection = encodeSection("checksum", new TextEncoder().encode(checksumHex));
130
+ const totalLen = manifestSection.length + allContentForChecksum.length + checksumSection.length;
131
+ const result = new Uint8Array(totalLen);
132
+ let pos = 0;
133
+ result.set(manifestSection, pos);
134
+ pos += manifestSection.length;
135
+ result.set(allContentForChecksum, pos);
136
+ pos += allContentForChecksum.length;
137
+ result.set(checksumSection, pos);
138
+ return result;
139
+ }
140
+ function parseServerBackup(data) {
141
+ const sections = parseSections(data);
142
+ const opsContent = findSection(sections, "operations");
143
+ let operations2 = [];
144
+ if (opsContent) {
145
+ const text3 = new TextDecoder().decode(opsContent);
146
+ const lines = text3.trim().split("\n").filter((l) => l.length > 0);
147
+ operations2 = lines.map((line) => JSON.parse(line));
148
+ }
149
+ const vvData = parseJsonSection(sections, "version_vector");
150
+ const versionVector = /* @__PURE__ */ new Map();
151
+ if (vvData) {
152
+ for (const [nid, seq] of Object.entries(vvData)) {
153
+ versionVector.set(nid, seq);
154
+ }
155
+ }
156
+ return { operations: operations2, versionVector };
157
+ }
158
+ var BACKUP_VERSION;
159
+ var init_server_backup = __esm({
160
+ "src/store/server-backup.ts"() {
161
+ "use strict";
162
+ init_cjs_shims();
163
+ BACKUP_VERSION = 1;
164
+ }
165
+ });
166
+
30
167
  // src/index.ts
31
168
  var index_exports = {};
32
169
  __export(index_exports, {
@@ -42,21 +179,81 @@ __export(index_exports, {
42
179
  SqliteServerStore: () => SqliteServerStore,
43
180
  TokenAuthProvider: () => TokenAuthProvider,
44
181
  WsServerTransport: () => WsServerTransport,
182
+ createDefaultLogger: () => createDefaultLogger,
183
+ createJsonLogger: () => createJsonLogger,
45
184
  createKoraServer: () => createKoraServer,
46
185
  createPostgresServerStore: () => createPostgresServerStore,
186
+ createPrettyLogger: () => createPrettyLogger,
47
187
  createProductionServer: () => createProductionServer,
188
+ createSilentLogger: () => createSilentLogger,
48
189
  createSqliteServerStore: () => createSqliteServerStore
49
190
  });
50
191
  module.exports = __toCommonJS(index_exports);
192
+ init_cjs_shims();
51
193
 
52
- // ../../node_modules/.pnpm/tsup@8.5.1_postcss@8.5.8_tsx@4.21.0_typescript@5.9.3/node_modules/tsup/assets/cjs_shims.js
53
- var getImportMetaUrl = () => typeof document === "undefined" ? new URL(`file:${__filename}`).href : document.currentScript && document.currentScript.tagName.toUpperCase() === "SCRIPT" ? document.currentScript.src : new URL("main.js", document.baseURI).href;
54
- var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
194
+ // src/logging/structured-logger.ts
195
+ init_cjs_shims();
196
+ function defaultJsonSerializer(entry) {
197
+ return JSON.stringify(entry);
198
+ }
199
+ function defaultPrettySerializer(entry) {
200
+ const time = new Date(entry.timestamp).toISOString().slice(11, 23);
201
+ const levelTag = { info: " INFO", warn: " WARN", error: "ERROR" }[entry.level];
202
+ const node = entry.nodeId ? ` [${entry.nodeId.slice(0, 8)}]` : "";
203
+ const session = entry.sessionId ? ` [session:${entry.sessionId.slice(0, 8)}]` : "";
204
+ const dur = entry.duration !== void 0 ? ` +${entry.duration}ms` : "";
205
+ const count3 = entry.count !== void 0 ? ` (${entry.count})` : "";
206
+ const err = entry.error ? ` \u2014 ${entry.error}` : "";
207
+ return `${time} ${levelTag}${node}${session} ${entry.event}${count3}${dur}${err}`;
208
+ }
209
+ function createJsonLogger(writer) {
210
+ const out = writer ?? {
211
+ info: (s) => process.stdout.write(`${s}
212
+ `),
213
+ error: (s) => process.stderr.write(`${s}
214
+ `)
215
+ };
216
+ return {
217
+ log(entry) {
218
+ const line = defaultJsonSerializer(entry);
219
+ if (entry.level === "error") {
220
+ out.error(line);
221
+ } else {
222
+ out.info(line);
223
+ }
224
+ }
225
+ };
226
+ }
227
+ function createPrettyLogger(writer) {
228
+ const out = writer ?? { write: (s) => process.stdout.write(`${s}
229
+ `) };
230
+ return {
231
+ log(entry) {
232
+ out.write(defaultPrettySerializer(entry));
233
+ }
234
+ };
235
+ }
236
+ function createSilentLogger() {
237
+ return { log() {
238
+ } };
239
+ }
240
+ function createDefaultLogger() {
241
+ switch (process.env.NODE_ENV) {
242
+ case "production":
243
+ return createJsonLogger();
244
+ case "test":
245
+ return createSilentLogger();
246
+ default:
247
+ return createPrettyLogger();
248
+ }
249
+ }
55
250
 
56
251
  // src/store/memory-server-store.ts
252
+ init_cjs_shims();
57
253
  var import_core = require("@korajs/core");
58
254
 
59
255
  // src/store/materialization.ts
256
+ init_cjs_shims();
60
257
  function fieldTypeToSql(descriptor, dialect) {
61
258
  switch (descriptor.kind) {
62
259
  case "string":
@@ -212,6 +409,9 @@ var MemoryServerStore = class {
212
409
  getNodeId() {
213
410
  return this.nodeId;
214
411
  }
412
+ getSchema() {
413
+ return this.schema;
414
+ }
215
415
  async setSchema(schema) {
216
416
  this.assertOpen();
217
417
  this.schema = schema;
@@ -361,6 +561,49 @@ var MemoryServerStore = class {
361
561
  async close() {
362
562
  this.closed = true;
363
563
  }
564
+ /**
565
+ * Wipes all in-memory state. For tests and E2E isolation only.
566
+ */
567
+ resetForTests() {
568
+ this.assertOpen();
569
+ this.operations.length = 0;
570
+ this.operationIndex.clear();
571
+ this.versionVector.clear();
572
+ this.materializedRecords.clear();
573
+ this.schema = null;
574
+ }
575
+ async exportBackup() {
576
+ this.assertOpen();
577
+ const { buildServerBackup: buildServerBackup2 } = await Promise.resolve().then(() => (init_server_backup(), server_backup_exports));
578
+ return buildServerBackup2(this.nodeId, this.operations, this.versionVector);
579
+ }
580
+ async importBackup(data, merge) {
581
+ this.assertOpen();
582
+ const { parseServerBackup: parseServerBackup2 } = await Promise.resolve().then(() => (init_server_backup(), server_backup_exports));
583
+ const { operations: operations2, versionVector } = parseServerBackup2(data);
584
+ if (merge) {
585
+ let restored = 0;
586
+ for (const op of operations2) {
587
+ const result = await this.applyRemoteOperation(op);
588
+ if (result === "applied") restored++;
589
+ }
590
+ return { operationsRestored: restored, success: true };
591
+ }
592
+ this.operations.length = 0;
593
+ this.operationIndex.clear();
594
+ this.versionVector.clear();
595
+ for (const [nid, seq] of versionVector) {
596
+ this.versionVector.set(nid, seq);
597
+ }
598
+ for (const op of operations2) {
599
+ this.operations.push(op);
600
+ this.operationIndex.set(op.id, op);
601
+ if (this.schema?.collections[op.collection]) {
602
+ this.rebuildMaterializedRecord(op.collection, op.recordId);
603
+ }
604
+ }
605
+ return { operationsRestored: operations2.length, success: true };
606
+ }
364
607
  // --- Testing helpers (not on interface) ---
365
608
  /**
366
609
  * Get all stored operations (for test assertions).
@@ -494,10 +737,12 @@ var MemoryServerStore = class {
494
737
  };
495
738
 
496
739
  // src/store/postgres-server-store.ts
740
+ init_cjs_shims();
497
741
  var import_core2 = require("@korajs/core");
498
742
  var import_drizzle_orm = require("drizzle-orm");
499
743
 
500
744
  // src/store/drizzle-pg-schema.ts
745
+ init_cjs_shims();
501
746
  var import_pg_core = require("drizzle-orm/pg-core");
502
747
  var pgOperations = (0, import_pg_core.pgTable)(
503
748
  "operations",
@@ -552,6 +797,9 @@ var PostgresServerStore = class {
552
797
  getNodeId() {
553
798
  return this.nodeId;
554
799
  }
800
+ getSchema() {
801
+ return this.schema;
802
+ }
555
803
  async setSchema(schema) {
556
804
  this.assertOpen();
557
805
  await this.ready;
@@ -680,6 +928,45 @@ var PostgresServerStore = class {
680
928
  async close() {
681
929
  this.closed = true;
682
930
  }
931
+ async exportBackup() {
932
+ this.assertOpen();
933
+ await this.ready;
934
+ const { buildServerBackup: buildServerBackup2 } = await Promise.resolve().then(() => (init_server_backup(), server_backup_exports));
935
+ const rows = await this.db.select().from(pgOperations).orderBy((0, import_drizzle_orm.asc)(pgOperations.sequenceNumber));
936
+ const operations2 = rows.map((row) => this.deserializeOperation(row));
937
+ return buildServerBackup2(this.nodeId, operations2, this.versionVector);
938
+ }
939
+ async importBackup(data, merge) {
940
+ this.assertOpen();
941
+ await this.ready;
942
+ const { parseServerBackup: parseServerBackup2 } = await Promise.resolve().then(() => (init_server_backup(), server_backup_exports));
943
+ const { operations: operations2, versionVector } = parseServerBackup2(data);
944
+ if (merge) {
945
+ let restored = 0;
946
+ for (const op of operations2) {
947
+ const result = await this.applyRemoteOperation(op);
948
+ if (result === "applied") restored++;
949
+ }
950
+ return { operationsRestored: restored, success: true };
951
+ }
952
+ const now = Date.now();
953
+ await this.db.transaction(async (tx) => {
954
+ await tx.delete(pgOperations);
955
+ await tx.delete(pgSyncState);
956
+ for (const [nid, seq] of versionVector) {
957
+ await tx.insert(pgSyncState).values({ nodeId: nid, maxSequenceNumber: seq, lastSeenAt: now }).onConflictDoNothing({ target: pgSyncState.nodeId });
958
+ }
959
+ for (const op of operations2) {
960
+ const row = this.serializeOperation(op, now);
961
+ await tx.insert(pgOperations).values(row).onConflictDoNothing({ target: pgOperations.id });
962
+ }
963
+ });
964
+ this.versionVector.clear();
965
+ for (const [nid, seq] of versionVector) {
966
+ this.versionVector.set(nid, seq);
967
+ }
968
+ return { operationsRestored: operations2.length, success: true };
969
+ }
683
970
  // ---------------------------------------------------------------------------
684
971
  // Materialization internals
685
972
  // ---------------------------------------------------------------------------
@@ -1034,11 +1321,13 @@ async function loadPostgresDeps() {
1034
1321
  }
1035
1322
 
1036
1323
  // src/store/sqlite-server-store.ts
1324
+ init_cjs_shims();
1037
1325
  var import_node_module = require("module");
1038
1326
  var import_core3 = require("@korajs/core");
1039
1327
  var import_drizzle_orm2 = require("drizzle-orm");
1040
1328
 
1041
1329
  // src/store/drizzle-schema.ts
1330
+ init_cjs_shims();
1042
1331
  var import_sqlite_core = require("drizzle-orm/sqlite-core");
1043
1332
  var operations = (0, import_sqlite_core.sqliteTable)(
1044
1333
  "operations",
@@ -1097,6 +1386,9 @@ var SqliteServerStore = class {
1097
1386
  getNodeId() {
1098
1387
  return this.nodeId;
1099
1388
  }
1389
+ getSchema() {
1390
+ return this.schema;
1391
+ }
1100
1392
  async setSchema(schema) {
1101
1393
  this.assertOpen();
1102
1394
  this.schema = schema;
@@ -1210,6 +1502,39 @@ var SqliteServerStore = class {
1210
1502
  async close() {
1211
1503
  this.closed = true;
1212
1504
  }
1505
+ async exportBackup() {
1506
+ this.assertOpen();
1507
+ const { buildServerBackup: buildServerBackup2 } = await Promise.resolve().then(() => (init_server_backup(), server_backup_exports));
1508
+ const rows = this.db.select().from(operations).all();
1509
+ const deserialized = rows.map((row) => this.deserializeOperation(row));
1510
+ const vv = this.getVersionVector();
1511
+ return buildServerBackup2(this.nodeId, deserialized, vv);
1512
+ }
1513
+ async importBackup(data, merge) {
1514
+ this.assertOpen();
1515
+ const { parseServerBackup: parseServerBackup2 } = await Promise.resolve().then(() => (init_server_backup(), server_backup_exports));
1516
+ const { operations: ops, versionVector } = parseServerBackup2(data);
1517
+ if (merge) {
1518
+ let restored = 0;
1519
+ for (const op of ops) {
1520
+ const result = await this.applyRemoteOperation(op);
1521
+ if (result === "applied") restored++;
1522
+ }
1523
+ return { operationsRestored: restored, success: true };
1524
+ }
1525
+ this.db.transaction((tx) => {
1526
+ tx.run(import_drizzle_orm2.sql.raw("DELETE FROM operations"));
1527
+ tx.run(import_drizzle_orm2.sql.raw("DELETE FROM sync_state"));
1528
+ for (const [nid, seq] of versionVector) {
1529
+ tx.insert(syncState).values({ nodeId: nid, maxSequenceNumber: seq, lastSeenAt: Date.now() }).run();
1530
+ }
1531
+ for (const op of ops) {
1532
+ const row = this.serializeOperation(op, Date.now());
1533
+ tx.insert(operations).values(row).run();
1534
+ }
1535
+ });
1536
+ return { operationsRestored: ops.length, success: true };
1537
+ }
1213
1538
  // ---------------------------------------------------------------------------
1214
1539
  // Materialization internals
1215
1540
  // ---------------------------------------------------------------------------
@@ -1543,6 +1868,7 @@ function createSqliteServerStore(options) {
1543
1868
  }
1544
1869
 
1545
1870
  // src/transport/http-server-transport.ts
1871
+ init_cjs_shims();
1546
1872
  var import_core4 = require("@korajs/core");
1547
1873
  var HttpServerTransport = class {
1548
1874
  serializer;
@@ -1624,6 +1950,7 @@ var HttpServerTransport = class {
1624
1950
  };
1625
1951
 
1626
1952
  // src/transport/ws-server-transport.ts
1953
+ init_cjs_shims();
1627
1954
  var import_core5 = require("@korajs/core");
1628
1955
  var import_sync = require("@korajs/sync");
1629
1956
  var WS_OPEN = 1;
@@ -1687,11 +2014,220 @@ var WsServerTransport = class {
1687
2014
  };
1688
2015
 
1689
2016
  // src/session/client-session.ts
1690
- var import_core6 = require("@korajs/core");
2017
+ init_cjs_shims();
2018
+ var import_core8 = require("@korajs/core");
1691
2019
  var import_internal = require("@korajs/core/internal");
1692
2020
  var import_sync2 = require("@korajs/sync");
1693
2021
 
2022
+ // src/apply/apply-server-operation.ts
2023
+ init_cjs_shims();
2024
+ var import_merge2 = require("@korajs/merge");
2025
+
2026
+ // src/constraints/operation-constraint-validator.ts
2027
+ init_cjs_shims();
2028
+ var import_merge = require("@korajs/merge");
2029
+
2030
+ // src/constraints/server-constraint-context.ts
2031
+ init_cjs_shims();
2032
+ function createServerConstraintContext(store) {
2033
+ return {
2034
+ async queryRecords(collection, where) {
2035
+ const rows = await store.queryCollection(collection, { where });
2036
+ return rows.map((row) => ({ ...row }));
2037
+ },
2038
+ async countRecords(collection, where) {
2039
+ return store.countCollection(collection, where);
2040
+ }
2041
+ };
2042
+ }
2043
+
2044
+ // src/constraints/operation-constraint-validator.ts
2045
+ async function validateIncomingOperationConstraints(store, op, schema) {
2046
+ if (!schema) {
2047
+ return { valid: true };
2048
+ }
2049
+ if (op.type === "delete") {
2050
+ return { valid: true };
2051
+ }
2052
+ const collectionDef = schema.collections[op.collection];
2053
+ if (!collectionDef || collectionDef.constraints.length === 0) {
2054
+ return { valid: true };
2055
+ }
2056
+ const candidate = await projectCandidateRecord(store, op);
2057
+ if (candidate === null) {
2058
+ return { valid: true };
2059
+ }
2060
+ const ctx = createServerConstraintContext(store);
2061
+ const violations = await (0, import_merge.checkConstraints)(
2062
+ candidate,
2063
+ op.recordId,
2064
+ op.collection,
2065
+ collectionDef,
2066
+ ctx
2067
+ );
2068
+ if (violations.length === 0) {
2069
+ return { valid: true };
2070
+ }
2071
+ const first = violations[0];
2072
+ if (first === void 0) {
2073
+ return { valid: true };
2074
+ }
2075
+ return {
2076
+ valid: false,
2077
+ code: "CONSTRAINT_VIOLATION",
2078
+ message: first.message
2079
+ };
2080
+ }
2081
+ async function projectCandidateRecord(store, op) {
2082
+ if (op.data === null) {
2083
+ return null;
2084
+ }
2085
+ const existing = await store.findRecord(op.collection, op.recordId);
2086
+ const base = existing ? { ...existing } : {};
2087
+ if (op.type === "insert") {
2088
+ return { ...op.data, id: op.recordId };
2089
+ }
2090
+ return { ...base, ...op.data, id: op.recordId };
2091
+ }
2092
+
2093
+ // src/constraints/server-referential-context.ts
2094
+ init_cjs_shims();
2095
+ function createServerReferentialContext(store) {
2096
+ return {
2097
+ async queryRecords(collection, where) {
2098
+ const rows = await store.queryCollection(collection, { where });
2099
+ return rows.map((row) => ({ ...row }));
2100
+ },
2101
+ async recordExists(collection, recordId) {
2102
+ const row = await store.findRecord(collection, recordId);
2103
+ return row !== null && row._deleted !== 1;
2104
+ }
2105
+ };
2106
+ }
2107
+
2108
+ // src/apply/server-side-effect-operation.ts
2109
+ init_cjs_shims();
2110
+ var import_core6 = require("@korajs/core");
2111
+ async function createServerSideEffectOperation(store, parentOp, effect, schemaVersion, sequenceNumber) {
2112
+ const nodeId = store.getNodeId();
2113
+ const clock = new import_core6.HybridLogicalClock(nodeId);
2114
+ clock.receive(parentOp.timestamp);
2115
+ return (0, import_core6.createOperation)(
2116
+ {
2117
+ nodeId,
2118
+ type: effect.type === "delete" ? "delete" : "update",
2119
+ collection: effect.collection,
2120
+ recordId: effect.recordId,
2121
+ data: effect.data,
2122
+ previousData: effect.previousData,
2123
+ sequenceNumber,
2124
+ causalDeps: [parentOp.id],
2125
+ schemaVersion
2126
+ },
2127
+ clock
2128
+ );
2129
+ }
2130
+ function nextServerSequenceNumber(store) {
2131
+ const nodeId = store.getNodeId();
2132
+ const current = store.getVersionVector().get(nodeId) ?? 0;
2133
+ return current + 1;
2134
+ }
2135
+
2136
+ // src/apply/apply-server-operation.ts
2137
+ async function applyServerOperation(store, op, relationLookup) {
2138
+ const schema = store.getSchema();
2139
+ const lookup = relationLookup ?? (schema ? (0, import_merge2.buildMergeRelationLookup)(schema) : /* @__PURE__ */ new Map());
2140
+ const constraintCheck = await validateIncomingOperationConstraints(store, op, schema);
2141
+ if (!constraintCheck.valid) {
2142
+ return {
2143
+ result: "skipped",
2144
+ appliedOperations: [],
2145
+ rejection: {
2146
+ code: constraintCheck.code ?? "CONSTRAINT_VIOLATION",
2147
+ message: constraintCheck.message ?? `Operation "${op.id}" violates a schema constraint`
2148
+ }
2149
+ };
2150
+ }
2151
+ if (op.type === "delete" && schema) {
2152
+ const refCtx = createServerReferentialContext(store);
2153
+ const referential = await (0, import_merge2.checkReferentialIntegrityOnDelete)(op, schema, refCtx, lookup);
2154
+ if (!referential.allowed) {
2155
+ return {
2156
+ result: "skipped",
2157
+ appliedOperations: [],
2158
+ rejection: {
2159
+ code: "REFERENTIAL_INTEGRITY",
2160
+ message: `Operation "${op.id}" violates referential integrity on "${op.collection}"`
2161
+ }
2162
+ };
2163
+ }
2164
+ const primaryResult = await store.applyRemoteOperation(op);
2165
+ if (primaryResult !== "applied") {
2166
+ return { result: primaryResult, appliedOperations: [] };
2167
+ }
2168
+ const appliedOperations = [op];
2169
+ let serverSeq = nextServerSequenceNumber(store);
2170
+ for (const effect of referential.sideEffectOps) {
2171
+ const sideOp = await createServerSideEffectOperation(
2172
+ store,
2173
+ op,
2174
+ effect,
2175
+ op.schemaVersion,
2176
+ serverSeq
2177
+ );
2178
+ serverSeq += 1;
2179
+ const sideResult = await store.applyRemoteOperation(sideOp);
2180
+ if (sideResult === "applied") {
2181
+ appliedOperations.push(sideOp);
2182
+ }
2183
+ }
2184
+ return { result: "applied", appliedOperations };
2185
+ }
2186
+ const result = await store.applyRemoteOperation(op);
2187
+ return {
2188
+ result,
2189
+ appliedOperations: result === "applied" ? [op] : []
2190
+ };
2191
+ }
2192
+
2193
+ // src/scopes/resolve-session-scopes.ts
2194
+ init_cjs_shims();
2195
+ var import_core7 = require("@korajs/core");
2196
+ function resolveSessionScopes(schema, options) {
2197
+ const { handshakeScope, authScopes, scopeValues } = options;
2198
+ let resolved;
2199
+ if (schema && scopeValues && Object.keys(scopeValues).length > 0) {
2200
+ resolved = (0, import_core7.buildScopeMap)(schema, scopeValues);
2201
+ } else if (handshakeScope) {
2202
+ resolved = { ...handshakeScope };
2203
+ } else if (authScopes) {
2204
+ resolved = { ...authScopes };
2205
+ }
2206
+ if (schema && !resolved && scopeValues) {
2207
+ resolved = (0, import_core7.buildScopeMap)(schema, scopeValues);
2208
+ }
2209
+ if (authScopes) {
2210
+ resolved = mergeScopeMaps(resolved, authScopes, "auth");
2211
+ }
2212
+ if (handshakeScope) {
2213
+ resolved = mergeScopeMaps(resolved, handshakeScope, "handshake");
2214
+ }
2215
+ return resolved && Object.keys(resolved).length > 0 ? resolved : void 0;
2216
+ }
2217
+ function mergeScopeMaps(base, overlay, source) {
2218
+ const merged = { ...base ?? {} };
2219
+ for (const [collection, overlayScope] of Object.entries(overlay)) {
2220
+ if (source === "auth") {
2221
+ merged[collection] = { ...merged[collection] ?? {}, ...overlayScope };
2222
+ } else {
2223
+ merged[collection] = { ...overlayScope ?? {}, ...merged[collection] ?? {} };
2224
+ }
2225
+ }
2226
+ return merged;
2227
+ }
2228
+
1694
2229
  // src/scopes/server-scope-filter.ts
2230
+ init_cjs_shims();
1695
2231
  function operationMatchesScopes(op, scopes) {
1696
2232
  if (!scopes) return true;
1697
2233
  const collectionScope = scopes[op.collection];
@@ -1721,6 +2257,57 @@ function asRecord(value) {
1721
2257
  return value;
1722
2258
  }
1723
2259
 
2260
+ // src/session/operation-validation.ts
2261
+ init_cjs_shims();
2262
+ var SERVER_MAX_TIMESTAMP_FUTURE_MS = 6e4;
2263
+ function isOperationTimestampValid(op, now = Date.now()) {
2264
+ return op.timestamp.wallTime <= now + SERVER_MAX_TIMESTAMP_FUTURE_MS;
2265
+ }
2266
+
2267
+ // src/session/session-operation-limits.ts
2268
+ init_cjs_shims();
2269
+ var DEFAULT_MAX_OPERATION_BYTES = 256 * 1024;
2270
+ var DEFAULT_MAX_OPS_PER_MINUTE = 600;
2271
+ function measureOperationBytes(op) {
2272
+ return Buffer.byteLength(JSON.stringify(op), "utf8");
2273
+ }
2274
+ function validateOperationSize(op, maxBytes = DEFAULT_MAX_OPERATION_BYTES) {
2275
+ const bytes = measureOperationBytes(op);
2276
+ if (bytes <= maxBytes) {
2277
+ return { valid: true, bytes };
2278
+ }
2279
+ return {
2280
+ valid: false,
2281
+ bytes,
2282
+ message: `Operation "${op.id}" exceeds max size (${String(bytes)} > ${String(maxBytes)} bytes)`
2283
+ };
2284
+ }
2285
+ var SessionRateLimiter = class {
2286
+ constructor(maxOpsPerMinute = DEFAULT_MAX_OPS_PER_MINUTE) {
2287
+ this.maxOpsPerMinute = maxOpsPerMinute;
2288
+ }
2289
+ maxOpsPerMinute;
2290
+ windowStartMs = Date.now();
2291
+ count = 0;
2292
+ get limit() {
2293
+ return this.maxOpsPerMinute;
2294
+ }
2295
+ /** Record N operations and return false when the limit is exceeded. */
2296
+ allow(count3 = 1) {
2297
+ const now = Date.now();
2298
+ if (now - this.windowStartMs >= 6e4) {
2299
+ this.windowStartMs = now;
2300
+ this.count = 0;
2301
+ }
2302
+ this.count += count3;
2303
+ return this.count <= this.maxOpsPerMinute;
2304
+ }
2305
+ reset() {
2306
+ this.windowStartMs = Date.now();
2307
+ this.count = 0;
2308
+ }
2309
+ };
2310
+
1724
2311
  // src/session/client-session.ts
1725
2312
  var DEFAULT_BATCH_SIZE = 100;
1726
2313
  var DEFAULT_SCHEMA_VERSION = 1;
@@ -1728,6 +2315,8 @@ var ClientSession = class {
1728
2315
  state = "connected";
1729
2316
  clientNodeId = null;
1730
2317
  authContext = null;
2318
+ syncQuerySubsets = [];
2319
+ resumeDeltaCursor = null;
1731
2320
  sessionId;
1732
2321
  transport;
1733
2322
  store;
@@ -1736,9 +2325,14 @@ var ClientSession = class {
1736
2325
  emitter;
1737
2326
  batchSize;
1738
2327
  schemaVersion;
2328
+ supportedSchemaVersions;
1739
2329
  onRelay;
1740
2330
  onAwarenessUpdate;
2331
+ onYjsDocUpdate;
1741
2332
  onClose;
2333
+ maxOperationBytes;
2334
+ maxOpsPerMinute;
2335
+ rateLimiter;
1742
2336
  constructor(options) {
1743
2337
  this.sessionId = options.sessionId;
1744
2338
  this.transport = options.transport;
@@ -1748,15 +2342,24 @@ var ClientSession = class {
1748
2342
  this.emitter = options.emitter ?? null;
1749
2343
  this.batchSize = options.batchSize ?? DEFAULT_BATCH_SIZE;
1750
2344
  this.schemaVersion = options.schemaVersion ?? DEFAULT_SCHEMA_VERSION;
2345
+ const supported = options.supportedSchemaVersions;
2346
+ this.supportedSchemaVersions = supported ?? {
2347
+ min: this.schemaVersion,
2348
+ max: this.schemaVersion
2349
+ };
1751
2350
  this.onRelay = options.onRelay ?? null;
1752
2351
  this.onAwarenessUpdate = options.onAwarenessUpdate ?? null;
2352
+ this.onYjsDocUpdate = options.onYjsDocUpdate ?? null;
1753
2353
  this.onClose = options.onClose ?? null;
2354
+ this.maxOperationBytes = options.maxOperationBytes ?? DEFAULT_MAX_OPERATION_BYTES;
2355
+ this.maxOpsPerMinute = options.maxOpsPerMinute ?? DEFAULT_MAX_OPS_PER_MINUTE;
2356
+ this.rateLimiter = new SessionRateLimiter(this.maxOpsPerMinute);
1754
2357
  }
1755
2358
  /**
1756
2359
  * Start handling messages from the client transport.
1757
2360
  */
1758
2361
  start() {
1759
- this.transport.onMessage((msg) => this.handleMessage(msg));
2362
+ this.transport.onMessage((msg) => this.enqueueMessage(msg));
1760
2363
  this.transport.onClose((_code, _reason) => this.handleTransportClose());
1761
2364
  this.transport.onError((_err) => {
1762
2365
  if (this.state !== "closed") {
@@ -1771,19 +2374,17 @@ var ClientSession = class {
1771
2374
  relayOperations(operations2) {
1772
2375
  if (this.state !== "streaming" || !this.transport.isConnected()) return;
1773
2376
  if (operations2.length === 0) return;
1774
- const visibleOperations = operations2.filter(
1775
- (op) => operationMatchesScopes(op, this.authContext?.scopes)
1776
- );
2377
+ const visibleOperations = operations2.filter((op) => this.operationVisibleToClient(op));
1777
2378
  if (visibleOperations.length === 0) return;
1778
2379
  const serializedOps = visibleOperations.map((op) => this.serializer.encodeOperation(op));
1779
2380
  const msg = {
1780
2381
  type: "operation-batch",
1781
- messageId: (0, import_core6.generateUUIDv7)(),
2382
+ messageId: (0, import_core8.generateUUIDv7)(),
1782
2383
  operations: serializedOps,
1783
2384
  isFinal: true,
1784
2385
  batchIndex: 0
1785
2386
  };
1786
- this.transport.send(msg);
2387
+ this.sendToClient(msg);
1787
2388
  }
1788
2389
  /**
1789
2390
  * Close this session.
@@ -1820,15 +2421,30 @@ var ClientSession = class {
1820
2421
  return this.transport;
1821
2422
  }
1822
2423
  // --- Private protocol handlers ---
1823
- handleMessage(message) {
2424
+ messageChain = Promise.resolve();
2425
+ /** Send to the client when the transport is still connected; no-op otherwise. */
2426
+ sendToClient(message) {
2427
+ if (!this.transport.isConnected()) {
2428
+ return false;
2429
+ }
2430
+ try {
2431
+ this.transport.send(message);
2432
+ return true;
2433
+ } catch {
2434
+ return false;
2435
+ }
2436
+ }
2437
+ enqueueMessage(message) {
2438
+ this.messageChain = this.messageChain.then(() => this.handleMessageAsync(message)).catch((error) => this.handleMessageFailure(error));
2439
+ }
2440
+ async handleMessageAsync(message) {
1824
2441
  switch (message.type) {
1825
2442
  case "handshake":
1826
- this.handleHandshake(message);
2443
+ await this.handleHandshake(message);
1827
2444
  break;
1828
2445
  case "operation-batch":
1829
- this.handleOperationBatch(message);
2446
+ await this.handleOperationBatch(message);
1830
2447
  break;
1831
- // Acknowledgments from clients are noted but no action needed on server
1832
2448
  case "acknowledgment":
1833
2449
  break;
1834
2450
  case "error":
@@ -1836,8 +2452,16 @@ var ClientSession = class {
1836
2452
  case "awareness-update":
1837
2453
  this.handleAwarenessUpdate(message);
1838
2454
  break;
2455
+ case "yjs-doc-update":
2456
+ this.handleYjsDocUpdate(message);
2457
+ break;
1839
2458
  }
1840
2459
  }
2460
+ handleMessageFailure(error) {
2461
+ const reason = error instanceof Error ? error.message : "Message handling failed";
2462
+ this.sendError("SYNC_ERROR", reason, true);
2463
+ this.close(reason);
2464
+ }
1841
2465
  async handleHandshake(msg) {
1842
2466
  if (this.state !== "connected") {
1843
2467
  this.sendError("DUPLICATE_HANDSHAKE", "Handshake already completed", false);
@@ -1855,25 +2479,46 @@ var ClientSession = class {
1855
2479
  this.authContext = context;
1856
2480
  this.state = "authenticated";
1857
2481
  }
1858
- if (msg.syncScope) {
1859
- const mergedScopes = { ...msg.syncScope };
1860
- if (this.authContext?.scopes) {
1861
- for (const [collection, authScope] of Object.entries(this.authContext.scopes)) {
1862
- mergedScopes[collection] = { ...mergedScopes[collection] ?? {}, ...authScope };
1863
- }
1864
- }
2482
+ const resolvedScopes = resolveSessionScopes(this.store.getSchema(), {
2483
+ handshakeScope: msg.syncScope,
2484
+ authScopes: this.authContext?.scopes
2485
+ });
2486
+ if (resolvedScopes) {
1865
2487
  if (this.authContext) {
1866
- this.authContext = { ...this.authContext, scopes: mergedScopes };
2488
+ this.authContext = { ...this.authContext, scopes: resolvedScopes };
1867
2489
  } else {
1868
- this.authContext = { userId: msg.nodeId, scopes: mergedScopes };
2490
+ this.authContext = { userId: msg.nodeId, scopes: resolvedScopes };
1869
2491
  }
1870
2492
  }
2493
+ if (msg.syncQueries && msg.syncQueries.length > 0) {
2494
+ this.syncQuerySubsets = (0, import_sync2.dedupeQuerySubsets)(msg.syncQueries);
2495
+ } else {
2496
+ this.syncQuerySubsets = [];
2497
+ }
2498
+ this.resumeDeltaCursor = msg.deltaCursor ? (0, import_sync2.decodeDeltaCursor)(msg.deltaCursor) : null;
1871
2499
  const serverVector = this.store.getVersionVector();
1872
2500
  const selectedWireFormat = selectWireFormat(msg.supportedWireFormats);
1873
2501
  this.setSerializerWireFormat(selectedWireFormat);
2502
+ if (!(0, import_sync2.isClientSchemaVersionSupported)(msg.schemaVersion, this.supportedSchemaVersions)) {
2503
+ const { min, max } = this.supportedSchemaVersions;
2504
+ const response2 = {
2505
+ type: "handshake-response",
2506
+ messageId: (0, import_core8.generateUUIDv7)(),
2507
+ nodeId: this.store.getNodeId(),
2508
+ versionVector: (0, import_sync2.versionVectorToWire)(serverVector),
2509
+ schemaVersion: this.schemaVersion,
2510
+ accepted: false,
2511
+ rejectReason: `${import_sync2.SCHEMA_MISMATCH_PREFIX}: client schema version ${msg.schemaVersion} not in supported range [${min}, ${max}]`,
2512
+ supportedSchemaMin: min,
2513
+ supportedSchemaMax: max
2514
+ };
2515
+ this.sendToClient(response2);
2516
+ this.close("schema version mismatch");
2517
+ return;
2518
+ }
1874
2519
  const response = {
1875
2520
  type: "handshake-response",
1876
- messageId: (0, import_core6.generateUUIDv7)(),
2521
+ messageId: (0, import_core8.generateUUIDv7)(),
1877
2522
  nodeId: this.store.getNodeId(),
1878
2523
  versionVector: (0, import_sync2.versionVectorToWire)(serverVector),
1879
2524
  schemaVersion: this.schemaVersion,
@@ -1883,7 +2528,7 @@ var ClientSession = class {
1883
2528
  // This may differ from what the client requested if auth scopes are narrower.
1884
2529
  ...this.authContext?.scopes ? { acceptedScope: this.authContext.scopes } : {}
1885
2530
  };
1886
- this.transport.send(response);
2531
+ this.sendToClient(response);
1887
2532
  this.emitter?.emit({ type: "sync:connected", nodeId: msg.nodeId });
1888
2533
  this.state = "syncing";
1889
2534
  const clientVector = (0, import_sync2.wireToVersionVector)(msg.versionVector);
@@ -1895,13 +2540,45 @@ var ClientSession = class {
1895
2540
  const applied = [];
1896
2541
  const rejected = [];
1897
2542
  for (const op of operations2) {
1898
- if (!operationMatchesScopes(op, this.authContext?.scopes)) {
2543
+ if (!this.operationVisibleToClient(op)) {
1899
2544
  rejected.push(op);
1900
2545
  continue;
1901
2546
  }
1902
- const result = await this.store.applyRemoteOperation(op);
1903
- if (result === "applied") {
1904
- applied.push(op);
2547
+ if (!isOperationTimestampValid(op)) {
2548
+ this.sendError(
2549
+ "INVALID_TIMESTAMP",
2550
+ `Operation "${op.id}" timestamp is too far in the future`,
2551
+ false
2552
+ );
2553
+ continue;
2554
+ }
2555
+ if (!this.rateLimiter.allow(1)) {
2556
+ this.sendError(
2557
+ "RATE_LIMIT",
2558
+ `Session exceeded operation rate limit (${String(this.maxOpsPerMinute)} ops/min)`,
2559
+ true
2560
+ );
2561
+ continue;
2562
+ }
2563
+ const sizeCheck = validateOperationSize(op, this.maxOperationBytes);
2564
+ if (!sizeCheck.valid) {
2565
+ this.sendError(
2566
+ "OPERATION_TOO_LARGE",
2567
+ sizeCheck.message ?? `Operation "${op.id}" is too large`,
2568
+ false
2569
+ );
2570
+ continue;
2571
+ }
2572
+ try {
2573
+ const applyResult = await applyServerOperation(this.store, op);
2574
+ if (applyResult.rejection) {
2575
+ this.sendError(applyResult.rejection.code, applyResult.rejection.message, false);
2576
+ continue;
2577
+ }
2578
+ if (applyResult.result === "applied") {
2579
+ applied.push(...applyResult.appliedOperations);
2580
+ }
2581
+ } catch {
1905
2582
  }
1906
2583
  }
1907
2584
  if (rejected.length > 0) {
@@ -1923,11 +2600,11 @@ var ClientSession = class {
1923
2600
  const lastOp = operations2[operations2.length - 1];
1924
2601
  const ack = {
1925
2602
  type: "acknowledgment",
1926
- messageId: (0, import_core6.generateUUIDv7)(),
2603
+ messageId: (0, import_core8.generateUUIDv7)(),
1927
2604
  acknowledgedMessageId: msg.messageId,
1928
2605
  lastSequenceNumber: lastOp ? lastOp.sequenceNumber : 0
1929
2606
  };
1930
- this.transport.send(ack);
2607
+ this.sendToClient(ack);
1931
2608
  if (applied.length > 0) {
1932
2609
  this.onRelay?.(this.sessionId, applied);
1933
2610
  }
@@ -1939,35 +2616,52 @@ var ClientSession = class {
1939
2616
  const clientSeq = clientVector.get(nodeId) ?? 0;
1940
2617
  if (serverSeq > clientSeq) {
1941
2618
  const ops = await this.store.getOperationRange(nodeId, clientSeq + 1, serverSeq);
1942
- const visible = ops.filter((op) => operationMatchesScopes(op, this.authContext?.scopes));
2619
+ const visible = ops.filter((op) => this.operationVisibleToClient(op));
1943
2620
  missing.push(...visible);
1944
2621
  }
1945
2622
  }
1946
2623
  if (missing.length === 0) {
1947
2624
  const emptyBatch = {
1948
2625
  type: "operation-batch",
1949
- messageId: (0, import_core6.generateUUIDv7)(),
2626
+ messageId: (0, import_core8.generateUUIDv7)(),
1950
2627
  operations: [],
1951
2628
  isFinal: true,
1952
- batchIndex: 0
2629
+ batchIndex: 0,
2630
+ totalBatches: 1
1953
2631
  };
1954
- this.transport.send(emptyBatch);
2632
+ this.sendToClient(emptyBatch);
1955
2633
  return;
1956
2634
  }
1957
2635
  const sorted = (0, import_internal.topologicalSort)(missing);
1958
- const totalBatches = Math.ceil(sorted.length / this.batchSize);
2636
+ const afterCursor = (0, import_sync2.sliceOperationsAfterCursor)(sorted, this.resumeDeltaCursor);
2637
+ const totalBatches = Math.ceil(afterCursor.length / this.batchSize);
2638
+ if (afterCursor.length === 0) {
2639
+ const emptyBatch = {
2640
+ type: "operation-batch",
2641
+ messageId: (0, import_core8.generateUUIDv7)(),
2642
+ operations: [],
2643
+ isFinal: true,
2644
+ batchIndex: this.resumeDeltaCursor?.batchIndex ?? 0,
2645
+ totalBatches: 1
2646
+ };
2647
+ this.sendToClient(emptyBatch);
2648
+ return;
2649
+ }
1959
2650
  for (let i = 0; i < totalBatches; i++) {
1960
2651
  const start = i * this.batchSize;
1961
- const batchOps = sorted.slice(start, start + this.batchSize);
2652
+ const batchOps = afterCursor.slice(start, start + this.batchSize);
1962
2653
  const serializedOps = batchOps.map((op) => this.serializer.encodeOperation(op));
2654
+ const batchCursor = (0, import_sync2.createDeltaCursorFromBatch)(batchOps, i);
1963
2655
  const batchMsg = {
1964
2656
  type: "operation-batch",
1965
- messageId: (0, import_core6.generateUUIDv7)(),
2657
+ messageId: (0, import_core8.generateUUIDv7)(),
1966
2658
  operations: serializedOps,
1967
2659
  isFinal: i === totalBatches - 1,
1968
- batchIndex: i
2660
+ batchIndex: i,
2661
+ totalBatches,
2662
+ ...batchCursor ? { cursor: (0, import_sync2.encodeDeltaCursor)(batchCursor) } : {}
1969
2663
  };
1970
- this.transport.send(batchMsg);
2664
+ this.sendToClient(batchMsg);
1971
2665
  this.emitter?.emit({
1972
2666
  type: "sync:sent",
1973
2667
  operations: batchOps,
@@ -1975,18 +2669,27 @@ var ClientSession = class {
1975
2669
  });
1976
2670
  }
1977
2671
  }
2672
+ operationVisibleToClient(op) {
2673
+ if (!operationMatchesScopes(op, this.authContext?.scopes)) {
2674
+ return false;
2675
+ }
2676
+ return (0, import_sync2.operationMatchesQuerySubsets)(op, this.syncQuerySubsets);
2677
+ }
1978
2678
  handleAwarenessUpdate(msg) {
1979
2679
  this.onAwarenessUpdate?.(this.sessionId, msg);
1980
2680
  }
2681
+ handleYjsDocUpdate(msg) {
2682
+ this.onYjsDocUpdate?.(this.sessionId, msg);
2683
+ }
1981
2684
  sendError(code, message, retriable) {
1982
2685
  const errorMsg = {
1983
2686
  type: "error",
1984
- messageId: (0, import_core6.generateUUIDv7)(),
2687
+ messageId: (0, import_core8.generateUUIDv7)(),
1985
2688
  code,
1986
2689
  message,
1987
2690
  retriable
1988
2691
  };
1989
- this.transport.send(errorMsg);
2692
+ this.sendToClient(errorMsg);
1990
2693
  }
1991
2694
  setSerializerWireFormat(format) {
1992
2695
  if (typeof this.serializer.setWireFormat === "function") {
@@ -2008,11 +2711,14 @@ function selectWireFormat(supportedWireFormats) {
2008
2711
  }
2009
2712
 
2010
2713
  // src/server/kora-sync-server.ts
2011
- var import_core8 = require("@korajs/core");
2714
+ init_cjs_shims();
2715
+ var import_core10 = require("@korajs/core");
2716
+ var import_internal2 = require("@korajs/core/internal");
2012
2717
  var import_sync3 = require("@korajs/sync");
2013
2718
 
2014
2719
  // src/awareness/awareness-relay.ts
2015
- var import_core7 = require("@korajs/core");
2720
+ init_cjs_shims();
2721
+ var import_core9 = require("@korajs/core");
2016
2722
  var AwarenessRelay = class {
2017
2723
  clients = /* @__PURE__ */ new Map();
2018
2724
  /**
@@ -2041,7 +2747,7 @@ var AwarenessRelay = class {
2041
2747
  if (hasStates) {
2042
2748
  const catchUpMsg = {
2043
2749
  type: "awareness-update",
2044
- messageId: (0, import_core7.generateUUIDv7)(),
2750
+ messageId: (0, import_core9.generateUUIDv7)(),
2045
2751
  clientId: 0,
2046
2752
  // Server-sourced
2047
2753
  states: existingStates
@@ -2064,7 +2770,7 @@ var AwarenessRelay = class {
2064
2770
  };
2065
2771
  const msg = {
2066
2772
  type: "awareness-update",
2067
- messageId: (0, import_core7.generateUUIDv7)(),
2773
+ messageId: (0, import_core9.generateUUIDv7)(),
2068
2774
  clientId: client.clientId,
2069
2775
  states: removalStates
2070
2776
  };
@@ -2108,6 +2814,158 @@ var AwarenessRelay = class {
2108
2814
  }
2109
2815
  };
2110
2816
 
2817
+ // src/diagnostics/server-metrics-collector.ts
2818
+ init_cjs_shims();
2819
+ function estimateByteSize(operations2) {
2820
+ let total = 0;
2821
+ for (const op of operations2) {
2822
+ total += JSON.stringify(op).length;
2823
+ }
2824
+ return total;
2825
+ }
2826
+ var ServerMetricsCollector = class {
2827
+ startedAt = Date.now();
2828
+ peakConnections = 0;
2829
+ connectionsTotal = 0;
2830
+ operationsReceived = 0;
2831
+ operationsSent = 0;
2832
+ bytesReceived = 0;
2833
+ bytesSent = 0;
2834
+ errorCount = 0;
2835
+ schemaVersion = 1;
2836
+ clientMetrics = /* @__PURE__ */ new Map();
2837
+ /** Record a new client connection. */
2838
+ recordConnection(sessionId) {
2839
+ this.connectionsTotal++;
2840
+ this.clientMetrics.set(sessionId, {
2841
+ sessionId,
2842
+ nodeId: null,
2843
+ state: "connected",
2844
+ connectedAt: Date.now(),
2845
+ operationsReceived: 0,
2846
+ operationsSent: 0,
2847
+ authContext: null
2848
+ });
2849
+ this.peakConnections = Math.max(this.peakConnections, this.clientMetrics.size);
2850
+ }
2851
+ /** Record a client disconnection. */
2852
+ recordDisconnection(sessionId) {
2853
+ this.clientMetrics.delete(sessionId);
2854
+ }
2855
+ /** Update the node ID after a handshake completes. */
2856
+ recordHandshake(sessionId, nodeId) {
2857
+ const client = this.clientMetrics.get(sessionId);
2858
+ if (client) {
2859
+ client.nodeId = nodeId;
2860
+ }
2861
+ }
2862
+ /** Update session state. */
2863
+ updateSessionState(sessionId, state) {
2864
+ const client = this.clientMetrics.get(sessionId);
2865
+ if (client) {
2866
+ client.state = state;
2867
+ }
2868
+ }
2869
+ /** Record authentication context for a session. */
2870
+ recordAuth(sessionId, authContext) {
2871
+ const client = this.clientMetrics.get(sessionId);
2872
+ if (client) {
2873
+ client.authContext = authContext;
2874
+ }
2875
+ }
2876
+ /** Record operations received from a client. */
2877
+ recordReceived(sessionId, count3, byteSize) {
2878
+ this.operationsReceived += count3;
2879
+ this.bytesReceived += byteSize;
2880
+ const client = this.clientMetrics.get(sessionId);
2881
+ if (client) {
2882
+ client.operationsReceived += count3;
2883
+ }
2884
+ }
2885
+ /** Record operations sent to a client. */
2886
+ recordSent(sessionId, count3, byteSize) {
2887
+ this.operationsSent += count3;
2888
+ this.bytesSent += byteSize;
2889
+ const client = this.clientMetrics.get(sessionId);
2890
+ if (client) {
2891
+ client.operationsSent += count3;
2892
+ }
2893
+ }
2894
+ /** Record an error. */
2895
+ recordError() {
2896
+ this.errorCount++;
2897
+ }
2898
+ /** Set the schema version. */
2899
+ setSchemaVersion(version) {
2900
+ this.schemaVersion = version;
2901
+ }
2902
+ /** Return a full snapshot of current metrics. */
2903
+ getSnapshot(totalOperations) {
2904
+ return {
2905
+ connectedClients: this.clientMetrics.size,
2906
+ connectedNodeIds: Array.from(this.clientMetrics.values()).filter((c) => c.nodeId !== null).map((c) => c.nodeId),
2907
+ peakConnections: this.peakConnections,
2908
+ connectionsTotal: this.connectionsTotal,
2909
+ operationsReceived: this.operationsReceived,
2910
+ operationsSent: this.operationsSent,
2911
+ bytesReceived: this.bytesReceived,
2912
+ bytesSent: this.bytesSent,
2913
+ clients: Array.from(this.clientMetrics.values()),
2914
+ uptime: Date.now() - this.startedAt,
2915
+ totalOperations,
2916
+ errorCount: this.errorCount,
2917
+ schemaVersion: this.schemaVersion
2918
+ };
2919
+ }
2920
+ /** Reset all metrics. */
2921
+ reset() {
2922
+ this.startedAt = Date.now();
2923
+ this.peakConnections = 0;
2924
+ this.connectionsTotal = 0;
2925
+ this.operationsReceived = 0;
2926
+ this.operationsSent = 0;
2927
+ this.bytesReceived = 0;
2928
+ this.bytesSent = 0;
2929
+ this.errorCount = 0;
2930
+ this.clientMetrics.clear();
2931
+ }
2932
+ };
2933
+
2934
+ // src/richtext/yjs-doc-relay.ts
2935
+ init_cjs_shims();
2936
+ var YjsDocRelay = class {
2937
+ clients = /* @__PURE__ */ new Map();
2938
+ addClient(sessionId, transport) {
2939
+ this.clients.set(sessionId, { sessionId, transport });
2940
+ }
2941
+ removeClient(sessionId) {
2942
+ this.clients.delete(sessionId);
2943
+ }
2944
+ handleUpdate(sourceSessionId, message) {
2945
+ if (!this.clients.has(sourceSessionId)) {
2946
+ return;
2947
+ }
2948
+ this.broadcastExcept(sourceSessionId, message);
2949
+ }
2950
+ getClientCount() {
2951
+ return this.clients.size;
2952
+ }
2953
+ clear() {
2954
+ this.clients.clear();
2955
+ }
2956
+ broadcastExcept(excludeSessionId, message) {
2957
+ for (const [, client] of this.clients) {
2958
+ if (client.sessionId === excludeSessionId) {
2959
+ continue;
2960
+ }
2961
+ if (!client.transport.isConnected()) {
2962
+ continue;
2963
+ }
2964
+ client.transport.send(message);
2965
+ }
2966
+ }
2967
+ };
2968
+
2111
2969
  // src/server/kora-sync-server.ts
2112
2970
  var DEFAULT_MAX_CONNECTIONS = 0;
2113
2971
  var DEFAULT_BATCH_SIZE2 = 100;
@@ -2122,13 +2980,18 @@ var KoraSyncServer = class {
2122
2980
  maxConnections;
2123
2981
  batchSize;
2124
2982
  schemaVersion;
2983
+ supportedSchemaVersions;
2125
2984
  port;
2126
2985
  host;
2127
2986
  path;
2987
+ logger;
2988
+ metrics;
2128
2989
  awarenessRelay = new AwarenessRelay();
2990
+ yjsDocRelay = new YjsDocRelay();
2129
2991
  sessions = /* @__PURE__ */ new Map();
2130
2992
  httpClients = /* @__PURE__ */ new Map();
2131
2993
  httpSessionToClient = /* @__PURE__ */ new Map();
2994
+ serverVersion = "0.4.0";
2132
2995
  wsServer = null;
2133
2996
  running = false;
2134
2997
  constructor(config) {
@@ -2139,9 +3002,80 @@ var KoraSyncServer = class {
2139
3002
  this.maxConnections = config.maxConnections ?? DEFAULT_MAX_CONNECTIONS;
2140
3003
  this.batchSize = config.batchSize ?? DEFAULT_BATCH_SIZE2;
2141
3004
  this.schemaVersion = config.schemaVersion ?? DEFAULT_SCHEMA_VERSION2;
3005
+ this.supportedSchemaVersions = config.supportedSchemaVersions ?? {
3006
+ min: this.schemaVersion,
3007
+ max: this.schemaVersion
3008
+ };
2142
3009
  this.port = config.port;
2143
3010
  this.host = config.host ?? DEFAULT_HOST;
2144
3011
  this.path = config.path ?? DEFAULT_PATH;
3012
+ this.logger = config.logger ?? createDefaultLogger();
3013
+ this.metrics = config.metricsCollector ?? new ServerMetricsCollector();
3014
+ this.metrics.setSchemaVersion(this.schemaVersion);
3015
+ if (!this.emitter) {
3016
+ this.emitter = new import_internal2.SimpleEventEmitter();
3017
+ }
3018
+ }
3019
+ /**
3020
+ * Subscribe to session-level events for metrics collection and logging.
3021
+ * Called when a new session is created.
3022
+ */
3023
+ attachSessionEvents(sessionId, sessionEmitter) {
3024
+ sessionEmitter.on("sync:connected", (event) => {
3025
+ this.metrics.recordHandshake(sessionId, event.nodeId);
3026
+ this.logger.log({
3027
+ timestamp: Date.now(),
3028
+ level: "info",
3029
+ event: "session.handshake",
3030
+ sessionId,
3031
+ nodeId: event.nodeId
3032
+ });
3033
+ });
3034
+ sessionEmitter.on("sync:received", (event) => {
3035
+ const byteSize = estimateByteSize(event.operations);
3036
+ this.metrics.recordReceived(sessionId, event.batchSize, byteSize);
3037
+ this.logger.log({
3038
+ timestamp: Date.now(),
3039
+ level: "info",
3040
+ event: "operations.received",
3041
+ sessionId,
3042
+ count: event.batchSize,
3043
+ bytes: byteSize
3044
+ });
3045
+ });
3046
+ sessionEmitter.on("sync:sent", (event) => {
3047
+ const byteSize = estimateByteSize(event.operations);
3048
+ this.metrics.recordSent(sessionId, event.batchSize, byteSize);
3049
+ this.logger.log({
3050
+ timestamp: Date.now(),
3051
+ level: "info",
3052
+ event: "operations.sent",
3053
+ sessionId,
3054
+ count: event.batchSize,
3055
+ bytes: byteSize
3056
+ });
3057
+ });
3058
+ sessionEmitter.on("sync:disconnected", (event) => {
3059
+ this.logger.log({
3060
+ timestamp: Date.now(),
3061
+ level: "info",
3062
+ event: "session.disconnected",
3063
+ sessionId,
3064
+ details: { reason: event.reason }
3065
+ });
3066
+ });
3067
+ }
3068
+ /**
3069
+ * Get the metrics collector for external access (e.g., HTTP endpoints).
3070
+ */
3071
+ getMetricsCollector() {
3072
+ return this.metrics;
3073
+ }
3074
+ /**
3075
+ * Get the logger for external access (e.g., event streaming).
3076
+ */
3077
+ getLogger() {
3078
+ return this.logger;
2145
3079
  }
2146
3080
  /**
2147
3081
  * Start the WebSocket server in standalone mode.
@@ -2150,10 +3084,10 @@ var KoraSyncServer = class {
2150
3084
  */
2151
3085
  async start(wsServerImpl) {
2152
3086
  if (this.running) {
2153
- throw new import_core8.SyncError("Server is already running", { port: this.port });
3087
+ throw new import_core10.SyncError("Server is already running", { port: this.port });
2154
3088
  }
2155
3089
  if (!wsServerImpl && this.port === void 0) {
2156
- throw new import_core8.SyncError(
3090
+ throw new import_core10.SyncError(
2157
3091
  "Port is required for standalone mode. Provide port in config or use handleConnection() for attach mode.",
2158
3092
  {}
2159
3093
  );
@@ -2182,12 +3116,25 @@ var KoraSyncServer = class {
2182
3116
  this.handleConnection(transport);
2183
3117
  });
2184
3118
  this.running = true;
3119
+ this.logger.log({
3120
+ timestamp: Date.now(),
3121
+ level: "info",
3122
+ event: "server.started",
3123
+ details: { port: this.port, host: this.host, path: this.path }
3124
+ });
2185
3125
  }
2186
3126
  /**
2187
3127
  * Stop the server. Closes all sessions and the WebSocket server.
2188
3128
  */
2189
3129
  async stop() {
3130
+ this.logger.log({
3131
+ timestamp: Date.now(),
3132
+ level: "info",
3133
+ event: "server.stopping",
3134
+ details: { connectedClients: this.sessions.size }
3135
+ });
2190
3136
  this.awarenessRelay.clear();
3137
+ this.yjsDocRelay.clear();
2191
3138
  for (const session of this.sessions.values()) {
2192
3139
  session.close("server shutting down");
2193
3140
  }
@@ -2201,6 +3148,11 @@ var KoraSyncServer = class {
2201
3148
  this.wsServer = null;
2202
3149
  }
2203
3150
  this.running = false;
3151
+ this.logger.log({
3152
+ timestamp: Date.now(),
3153
+ level: "info",
3154
+ event: "server.stopped"
3155
+ });
2204
3156
  }
2205
3157
  /**
2206
3158
  * Handle one HTTP sync request for a long-polling client.
@@ -2244,50 +3196,125 @@ var KoraSyncServer = class {
2244
3196
  if (this.maxConnections > 0 && this.sessions.size >= this.maxConnections) {
2245
3197
  transport.send({
2246
3198
  type: "error",
2247
- messageId: (0, import_core8.generateUUIDv7)(),
3199
+ messageId: (0, import_core10.generateUUIDv7)(),
2248
3200
  code: "MAX_CONNECTIONS",
2249
3201
  message: `Server has reached maximum connections (${this.maxConnections})`,
2250
3202
  retriable: true
2251
3203
  });
2252
3204
  transport.close(4029, "max connections reached");
2253
- throw new import_core8.SyncError("Maximum connections reached", {
3205
+ this.metrics.recordError();
3206
+ this.logger.log({
3207
+ timestamp: Date.now(),
3208
+ level: "warn",
3209
+ event: "connection.rejected",
3210
+ details: { reason: "max_connections", max: this.maxConnections }
3211
+ });
3212
+ throw new import_core10.SyncError("Maximum connections reached", {
2254
3213
  current: this.sessions.size,
2255
3214
  max: this.maxConnections
2256
3215
  });
2257
3216
  }
2258
- const sessionId = (0, import_core8.generateUUIDv7)();
3217
+ const sessionId = (0, import_core10.generateUUIDv7)();
3218
+ this.metrics.recordConnection(sessionId);
3219
+ const sessionEmitter = new import_internal2.SimpleEventEmitter();
3220
+ sessionEmitter.on("sync:connected", (event) => {
3221
+ this.metrics.recordHandshake(sessionId, event.nodeId);
3222
+ this.metrics.updateSessionState(sessionId, "authenticated");
3223
+ this.logger.log({
3224
+ timestamp: Date.now(),
3225
+ level: "info",
3226
+ event: "session.handshake",
3227
+ sessionId,
3228
+ nodeId: event.nodeId
3229
+ });
3230
+ });
3231
+ sessionEmitter.on("sync:received", (event) => {
3232
+ const byteSize = estimateOperationByteSize(event.operations);
3233
+ this.metrics.recordReceived(sessionId, event.batchSize, byteSize);
3234
+ this.logger.log({
3235
+ timestamp: Date.now(),
3236
+ level: "info",
3237
+ event: "operations.received",
3238
+ sessionId,
3239
+ count: event.batchSize,
3240
+ bytes: byteSize
3241
+ });
3242
+ });
3243
+ sessionEmitter.on("sync:sent", (event) => {
3244
+ const byteSize = estimateOperationByteSize(event.operations);
3245
+ this.metrics.recordSent(sessionId, event.batchSize, byteSize);
3246
+ this.logger.log({
3247
+ timestamp: Date.now(),
3248
+ level: "info",
3249
+ event: "operations.sent",
3250
+ sessionId,
3251
+ count: event.batchSize,
3252
+ bytes: byteSize
3253
+ });
3254
+ });
3255
+ sessionEmitter.on("sync:disconnected", () => {
3256
+ this.logger.log({
3257
+ timestamp: Date.now(),
3258
+ level: "info",
3259
+ event: "session.disconnected",
3260
+ sessionId
3261
+ });
3262
+ });
2259
3263
  const session = new ClientSession({
2260
3264
  sessionId,
2261
3265
  transport,
2262
3266
  store: this.store,
2263
3267
  auth: this.auth ?? void 0,
2264
3268
  serializer: this.serializer,
2265
- emitter: this.emitter ?? void 0,
3269
+ emitter: sessionEmitter,
2266
3270
  batchSize: this.batchSize,
2267
3271
  schemaVersion: this.schemaVersion,
3272
+ supportedSchemaVersions: this.supportedSchemaVersions,
2268
3273
  onRelay: (sourceSessionId, operations2) => {
2269
3274
  this.handleRelay(sourceSessionId, operations2);
2270
3275
  },
2271
3276
  onAwarenessUpdate: (sourceSessionId, message) => {
2272
3277
  this.handleAwarenessRelay(sourceSessionId, message);
2273
3278
  },
3279
+ onYjsDocUpdate: (sourceSessionId, message) => {
3280
+ this.handleYjsDocRelay(sourceSessionId, message);
3281
+ },
2274
3282
  onClose: (sid) => {
2275
3283
  this.handleSessionClose(sid);
2276
3284
  }
2277
3285
  });
2278
3286
  this.sessions.set(sessionId, session);
3287
+ this.yjsDocRelay.addClient(sessionId, transport);
2279
3288
  session.start();
3289
+ this.logger.log({
3290
+ timestamp: Date.now(),
3291
+ level: "info",
3292
+ event: "session.connected",
3293
+ sessionId,
3294
+ details: { totalSessions: this.sessions.size }
3295
+ });
2280
3296
  return sessionId;
2281
3297
  }
2282
3298
  /**
2283
3299
  * Get the current server status.
2284
3300
  */
2285
3301
  async getStatus() {
3302
+ const totalOps = await this.store.getOperationCount();
3303
+ const snapshot = this.metrics.getSnapshot(totalOps);
2286
3304
  return {
2287
3305
  running: this.running,
2288
- connectedClients: this.sessions.size,
3306
+ connectedClients: snapshot.connectedClients,
2289
3307
  port: this.port ?? null,
2290
- totalOperations: await this.store.getOperationCount()
3308
+ totalOperations: snapshot.totalOperations,
3309
+ uptime: snapshot.uptime,
3310
+ version: this.serverVersion,
3311
+ schemaVersion: this.schemaVersion,
3312
+ connectedNodeIds: snapshot.connectedNodeIds,
3313
+ peakConnections: snapshot.peakConnections,
3314
+ connectionsTotal: snapshot.connectionsTotal,
3315
+ operationsReceived: snapshot.operationsReceived,
3316
+ operationsSent: snapshot.operationsSent,
3317
+ errorCount: snapshot.errorCount
2291
3318
  };
2292
3319
  }
2293
3320
  /**
@@ -2298,13 +3325,31 @@ var KoraSyncServer = class {
2298
3325
  }
2299
3326
  // --- Private ---
2300
3327
  handleRelay(sourceSessionId, operations2) {
3328
+ const targetCount = this.sessions.size - 1;
3329
+ const byteSize = estimateOperationByteSize(operations2);
3330
+ this.metrics.recordSent(
3331
+ sourceSessionId,
3332
+ operations2.length * targetCount,
3333
+ byteSize * targetCount
3334
+ );
3335
+ this.logger.log({
3336
+ timestamp: Date.now(),
3337
+ level: "info",
3338
+ event: "operations.relayed",
3339
+ sessionId: sourceSessionId,
3340
+ count: operations2.length,
3341
+ bytes: byteSize * targetCount,
3342
+ details: { targetSessions: targetCount }
3343
+ });
2301
3344
  for (const [sessionId, session] of this.sessions) {
2302
3345
  if (sessionId === sourceSessionId) continue;
2303
3346
  session.relayOperations(operations2);
2304
3347
  }
2305
3348
  }
2306
3349
  handleSessionClose(sessionId) {
3350
+ this.metrics.recordDisconnection(sessionId);
2307
3351
  this.awarenessRelay.removeClient(sessionId);
3352
+ this.yjsDocRelay.removeClient(sessionId);
2308
3353
  this.sessions.delete(sessionId);
2309
3354
  const clientId = this.httpSessionToClient.get(sessionId);
2310
3355
  if (clientId) {
@@ -2321,6 +3366,12 @@ var KoraSyncServer = class {
2321
3366
  this.awarenessRelay.addClient(sourceSessionId, message.clientId, transport);
2322
3367
  this.awarenessRelay.handleUpdate(sourceSessionId, message);
2323
3368
  }
3369
+ handleYjsDocRelay(sourceSessionId, message) {
3370
+ if (!this.sessions.has(sourceSessionId)) {
3371
+ return;
3372
+ }
3373
+ this.yjsDocRelay.handleUpdate(sourceSessionId, message);
3374
+ }
2324
3375
  getOrCreateHttpClient(clientId) {
2325
3376
  const existing = this.httpClients.get(clientId);
2326
3377
  if (existing) {
@@ -2334,6 +3385,13 @@ var KoraSyncServer = class {
2334
3385
  return client;
2335
3386
  }
2336
3387
  };
3388
+ function estimateOperationByteSize(operations2) {
3389
+ let total = 0;
3390
+ for (const op of operations2) {
3391
+ total += JSON.stringify(op).length;
3392
+ }
3393
+ return total;
3394
+ }
2337
3395
  function normalizeHttpBody(body, contentType) {
2338
3396
  if (body instanceof Uint8Array) {
2339
3397
  return body;
@@ -2345,6 +3403,7 @@ function normalizeHttpBody(body, contentType) {
2345
3403
  }
2346
3404
 
2347
3405
  // src/auth/no-auth.ts
3406
+ init_cjs_shims();
2348
3407
  var NoAuthProvider = class {
2349
3408
  async authenticate(_token) {
2350
3409
  return { userId: "anonymous" };
@@ -2352,6 +3411,7 @@ var NoAuthProvider = class {
2352
3411
  };
2353
3412
 
2354
3413
  // src/auth/token-auth.ts
3414
+ init_cjs_shims();
2355
3415
  var TokenAuthProvider = class {
2356
3416
  validate;
2357
3417
  constructor(options) {
@@ -2363,6 +3423,7 @@ var TokenAuthProvider = class {
2363
3423
  };
2364
3424
 
2365
3425
  // src/auth/kora-auth-provider.ts
3426
+ init_cjs_shims();
2366
3427
  var KoraAuthProvider = class {
2367
3428
  tokenValidator;
2368
3429
  userLookup;
@@ -2375,7 +3436,7 @@ var KoraAuthProvider = class {
2375
3436
  this.resolveScopes = options.resolveScopes;
2376
3437
  }
2377
3438
  async authenticate(token) {
2378
- const payload = this.tokenValidator.validateToken(token);
3439
+ const payload = this.tokenValidator.validateTokenWithRevocation ? await this.tokenValidator.validateTokenWithRevocation(token) : this.tokenValidator.validateToken(token);
2379
3440
  if (payload === null) {
2380
3441
  return null;
2381
3442
  }
@@ -2403,6 +3464,7 @@ var KoraAuthProvider = class {
2403
3464
  };
2404
3465
 
2405
3466
  // src/auth/mixed-auth-provider.ts
3467
+ init_cjs_shims();
2406
3468
  var MixedAuthProvider = class {
2407
3469
  primary;
2408
3470
  anonymousScopes;
@@ -2427,11 +3489,13 @@ var MixedAuthProvider = class {
2427
3489
  };
2428
3490
 
2429
3491
  // src/server/create-server.ts
3492
+ init_cjs_shims();
2430
3493
  function createKoraServer(config) {
2431
3494
  return new KoraSyncServer(config);
2432
3495
  }
2433
3496
 
2434
3497
  // src/server/production-server.ts
3498
+ init_cjs_shims();
2435
3499
  var MIME_TYPES = {
2436
3500
  ".html": "text/html",
2437
3501
  ".js": "text/javascript",
@@ -2452,9 +3516,143 @@ function createProductionServer(config) {
2452
3516
  const syncPath = config.syncPath ?? "/kora-sync";
2453
3517
  const syncServer = new KoraSyncServer({
2454
3518
  store: config.store,
3519
+ enableDashboard: true,
2455
3520
  ...config.syncOptions
2456
3521
  });
2457
3522
  let httpServer = null;
3523
+ function getOperationalToken(kind) {
3524
+ const auth = config.operationalAuth;
3525
+ if (!auth) return void 0;
3526
+ if (kind === "metrics") return auth.metricsToken || auth.adminToken;
3527
+ if (kind === "backup") return auth.backupToken || auth.adminToken;
3528
+ return auth.adminToken;
3529
+ }
3530
+ function extractRequestToken(req) {
3531
+ const authorization = req.headers.authorization;
3532
+ if (authorization?.startsWith("Bearer ")) {
3533
+ return authorization.slice("Bearer ".length).trim();
3534
+ }
3535
+ const headerNames = ["x-kora-admin-token", "x-kora-metrics-token", "x-kora-backup-token"];
3536
+ for (const name of headerNames) {
3537
+ const value = req.headers[name];
3538
+ if (typeof value === "string" && value.length > 0) return value;
3539
+ if (Array.isArray(value) && typeof value[0] === "string" && value[0].length > 0) {
3540
+ return value[0];
3541
+ }
3542
+ }
3543
+ return null;
3544
+ }
3545
+ function isOperationalRequestAllowed(req, kind) {
3546
+ const expected = getOperationalToken(kind);
3547
+ if (!expected) return true;
3548
+ return extractRequestToken(req) === expected;
3549
+ }
3550
+ function rejectUnauthorized(res) {
3551
+ res.writeHead(401, {
3552
+ "Content-Type": "application/json",
3553
+ "WWW-Authenticate": 'Bearer realm="kora"'
3554
+ });
3555
+ res.end(JSON.stringify({ error: "Unauthorized" }));
3556
+ }
3557
+ function formatPrometheusMetrics() {
3558
+ const status = syncServer.getMetricsCollector().getSnapshot(0);
3559
+ const lines = [
3560
+ "# HELP kora_connected_clients Current number of connected clients",
3561
+ "# TYPE kora_connected_clients gauge",
3562
+ `kora_connected_clients ${status.connectedClients}`,
3563
+ "",
3564
+ "# HELP kora_peak_connections Peak number of simultaneous connections since server start",
3565
+ "# TYPE kora_peak_connections gauge",
3566
+ `kora_peak_connections ${status.peakConnections}`,
3567
+ "",
3568
+ "# HELP kora_connections_total Total number of connections handled since server start",
3569
+ "# TYPE kora_connections_total counter",
3570
+ `kora_connections_total ${status.connectionsTotal}`,
3571
+ "",
3572
+ "# HELP kora_operations_received_total Total operations received from clients",
3573
+ "# TYPE kora_operations_received_total counter",
3574
+ `kora_operations_received_total ${status.operationsReceived}`,
3575
+ "",
3576
+ "# HELP kora_operations_sent_total Total operations sent to clients",
3577
+ "# TYPE kora_operations_sent_total counter",
3578
+ `kora_operations_sent_total ${status.operationsSent}`,
3579
+ "",
3580
+ "# HELP kora_bytes_received_total Total bytes received from clients",
3581
+ "# TYPE kora_bytes_received_total counter",
3582
+ `kora_bytes_received_total ${status.bytesReceived}`,
3583
+ "",
3584
+ "# HELP kora_bytes_sent_total Total bytes sent to clients",
3585
+ "# TYPE kora_bytes_sent_total counter",
3586
+ `kora_bytes_sent_total ${status.bytesSent}`,
3587
+ "",
3588
+ "# HELP kora_errors_total Total errors since server start",
3589
+ "# TYPE kora_errors_total counter",
3590
+ `kora_errors_total ${status.errorCount}`,
3591
+ "",
3592
+ "# HELP kora_uptime_seconds Server uptime in seconds",
3593
+ "# TYPE kora_uptime_seconds gauge",
3594
+ `kora_uptime_seconds ${Math.floor(status.uptime / 1e3)}`,
3595
+ "",
3596
+ "# HELP kora_schema_version Schema version the server expects",
3597
+ "# TYPE kora_schema_version gauge",
3598
+ `kora_schema_version ${status.schemaVersion}`,
3599
+ ""
3600
+ ];
3601
+ return lines.join("\n");
3602
+ }
3603
+ function readBodyBuffer(req) {
3604
+ return new Promise((resolve) => {
3605
+ const chunks = [];
3606
+ req.on("data", (chunk) => chunks.push(chunk));
3607
+ req.on("end", () => resolve(Buffer.concat(chunks)));
3608
+ });
3609
+ }
3610
+ async function readJsonBody(req) {
3611
+ const buffer = await readBodyBuffer(req);
3612
+ if (buffer.byteLength === 0) return void 0;
3613
+ try {
3614
+ return JSON.parse(buffer.toString("utf8"));
3615
+ } catch {
3616
+ return void 0;
3617
+ }
3618
+ }
3619
+ function matchesRoutePrefix(pathname, prefix) {
3620
+ const normalizedPrefix = normalizeRoutePath(prefix);
3621
+ return pathname === normalizedPrefix || pathname.startsWith(`${normalizedPrefix}/`);
3622
+ }
3623
+ function normalizeRoutePath(path) {
3624
+ const prefixed = path.startsWith("/") ? path : `/${path}`;
3625
+ return prefixed.length > 1 ? prefixed.replace(/\/+$/, "") : prefixed;
3626
+ }
3627
+ function getClientIp(req) {
3628
+ const forwarded = req.headers["x-forwarded-for"];
3629
+ if (typeof forwarded === "string" && forwarded.length > 0) {
3630
+ return forwarded.split(",")[0]?.trim();
3631
+ }
3632
+ return req.socket.remoteAddress;
3633
+ }
3634
+ function getQuery(url) {
3635
+ const query = {};
3636
+ for (const [key, value] of url.searchParams) {
3637
+ const existing = query[key];
3638
+ if (existing === void 0) {
3639
+ query[key] = value;
3640
+ } else if (Array.isArray(existing)) {
3641
+ existing.push(value);
3642
+ } else {
3643
+ query[key] = [existing, value];
3644
+ }
3645
+ }
3646
+ return query;
3647
+ }
3648
+ function writeJsonResponse(res, result) {
3649
+ const headers = {
3650
+ "Content-Type": "application/json",
3651
+ ...result.headers ?? {}
3652
+ };
3653
+ res.writeHead(result.status, headers);
3654
+ res.end(JSON.stringify(result.body ?? null));
3655
+ }
2458
3656
  return {
2459
3657
  async start() {
2460
3658
  const { createServer } = await import("http");
@@ -2462,13 +3660,137 @@ function createProductionServer(config) {
2462
3660
  const { extname, join, resolve } = await import("path");
2463
3661
  const { WebSocketServer } = await import("ws");
2464
3662
  const distDir = resolve(staticDir);
2465
- httpServer = createServer((req, res) => {
3663
+ httpServer = createServer(async (req, res) => {
2466
3664
  res.setHeader("Cross-Origin-Opener-Policy", "same-origin");
2467
3665
  res.setHeader("Cross-Origin-Embedder-Policy", "require-corp");
2468
3666
  const url = new URL(req.url || "/", `http://${req.headers.host}`);
2469
3667
  if (url.pathname === "/health") {
3668
+ const status = await syncServer.getStatus();
2470
3669
  res.writeHead(200, { "Content-Type": "application/json" });
2471
- res.end(JSON.stringify({ status: "ok", timestamp: Date.now() }));
3670
+ res.end(
3671
+ JSON.stringify({
3672
+ status: "ok",
3673
+ version: status.version,
3674
+ uptime: status.uptime,
3675
+ connectedClients: status.connectedClients,
3676
+ totalOperations: status.totalOperations,
3677
+ timestamp: Date.now()
3678
+ })
3679
+ );
3680
+ return;
3681
+ }
3682
+ if (url.pathname === "/__kora/status") {
3683
+ if (!isOperationalRequestAllowed(req, "admin")) {
3684
+ rejectUnauthorized(res);
3685
+ return;
3686
+ }
3687
+ const status = await syncServer.getStatus();
3688
+ res.writeHead(200, { "Content-Type": "application/json" });
3689
+ res.end(JSON.stringify(status, null, 2));
3690
+ return;
3691
+ }
3692
+ if (url.pathname === "/__kora/metrics") {
3693
+ if (!isOperationalRequestAllowed(req, "metrics")) {
3694
+ rejectUnauthorized(res);
3695
+ return;
3696
+ }
3697
+ res.writeHead(200, { "Content-Type": "text/plain; version=0.0.4" });
3698
+ res.end(formatPrometheusMetrics());
3699
+ return;
3700
+ }
3701
+ if (url.pathname === "/__kora/events") {
3702
+ if (!isOperationalRequestAllowed(req, "admin")) {
3703
+ rejectUnauthorized(res);
3704
+ return;
3705
+ }
3706
+ res.writeHead(200, {
3707
+ "Content-Type": "text/event-stream",
3708
+ "Cache-Control": "no-cache",
3709
+ Connection: "keep-alive",
3710
+ "X-Accel-Buffering": "no"
3711
+ });
3712
+ const status = await syncServer.getStatus();
3713
+ res.write(`event: status
3714
+ data: ${JSON.stringify(status)}
3715
+
3716
+ `);
3717
+ const interval = setInterval(async () => {
3718
+ try {
3719
+ const s = await syncServer.getStatus();
3720
+ res.write(`event: status
3721
+ data: ${JSON.stringify(s)}
3722
+
3723
+ `);
3724
+ } catch {
3725
+ }
3726
+ }, 2e3);
3727
+ req.on("close", () => {
3728
+ clearInterval(interval);
3729
+ });
3730
+ return;
3731
+ }
3732
+ if (url.pathname === "/__kora" || url.pathname === "/__kora/") {
3733
+ if (!isOperationalRequestAllowed(req, "admin")) {
3734
+ rejectUnauthorized(res);
3735
+ return;
3736
+ }
3737
+ const status = await syncServer.getStatus();
3738
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
3739
+ res.end(renderDashboardHtml(status));
3740
+ return;
3741
+ }
3742
+ if (url.pathname === "/__kora/backup/export" && req.method === "POST") {
3743
+ if (!isOperationalRequestAllowed(req, "backup")) {
3744
+ rejectUnauthorized(res);
3745
+ return;
3746
+ }
3747
+ try {
3748
+ const backup = await config.store.exportBackup();
3749
+ res.writeHead(200, {
3750
+ "Content-Type": "application/octet-stream",
3751
+ "Content-Disposition": `attachment; filename="kora-backup-${Date.now()}.kora"`,
3752
+ "Content-Length": String(backup.byteLength)
3753
+ });
3754
+ res.end(Buffer.from(backup));
3755
+ } catch (error) {
3756
+ res.writeHead(500, { "Content-Type": "application/json" });
3757
+ res.end(JSON.stringify({ error: "Backup failed", message: error.message }));
3758
+ }
3759
+ return;
3760
+ }
3761
+ if (url.pathname === "/__kora/backup/import" && req.method === "POST") {
3762
+ if (!isOperationalRequestAllowed(req, "backup")) {
3763
+ rejectUnauthorized(res);
3764
+ return;
3765
+ }
3766
+ try {
3767
+ const body = await readBodyBuffer(req);
3768
+ const merge = url.searchParams.get("merge") === "true";
3769
+ const result = await config.store.importBackup(
3770
+ new Uint8Array(body.buffer, body.byteOffset, body.byteLength),
3771
+ merge
3772
+ );
3773
+ res.writeHead(result.success ? 200 : 400, { "Content-Type": "application/json" });
3774
+ res.end(JSON.stringify(result));
3775
+ } catch (error) {
3776
+ res.writeHead(500, { "Content-Type": "application/json" });
3777
+ res.end(JSON.stringify({ error: "Restore failed", message: error.message }));
3778
+ }
3779
+ return;
3780
+ }
3781
+ const customRoute = config.httpRoutes?.find(
3782
+ (route) => matchesRoutePrefix(url.pathname, route.path)
3783
+ );
3784
+ if (customRoute) {
3785
+ const result = await customRoute.handle({
3786
+ method: req.method ?? "GET",
3787
+ path: url.pathname,
3788
+ body: await readJsonBody(req),
3789
+ headers: req.headers,
3790
+ query: getQuery(url),
3791
+ ip: getClientIp(req)
3792
+ });
3793
+ writeJsonResponse(res, result);
2472
3794
  return;
2473
3795
  }
2474
3796
  let filePath = join(distDir, url.pathname);
@@ -2530,6 +3852,82 @@ function createProductionServer(config) {
2530
3852
  }
2531
3853
  };
2532
3854
  }
3855
+ function renderDashboardHtml(status) {
3856
+ const version = status.version;
3857
+ const uptime = formatUptime(status.uptime);
3858
+ return `<!DOCTYPE html>
3859
+ <html lang="en">
3860
+ <head>
3861
+ <meta charset="UTF-8">
3862
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
3863
+ <title>Kora Dashboard</title>
3864
+ <style>
3865
+ * { box-sizing: border-box; margin: 0; padding: 0 }
3866
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0f172a; color: #e2e8f0; padding: 2rem }
3867
+ h1 { font-size: 1.5rem; font-weight: 600; margin-bottom: 0.25rem }
3868
+ .subtitle { color: #64748b; margin-bottom: 2rem; font-size: 0.875rem }
3869
+ .grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 1rem; margin-bottom: 2rem }
3870
+ .card { background: #1e293b; border: 1px solid #334155; border-radius: 0.75rem; padding: 1.25rem }
3871
+ .card .label { font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.05em; color: #64748b; margin-bottom: 0.5rem }
3872
+ .card .value { font-size: 1.75rem; font-weight: 700; color: #38bdf8 }
3873
+ .card .value.green { color: #4ade80 }
3874
+ .card .value.red { color: #f87171 }
3875
+ .card .value.yellow { color: #fbbf24 }
3876
+ .section-title { font-size: 1rem; font-weight: 600; margin-bottom: 0.75rem; margin-top: 1.5rem }
3877
+ table { width: 100%; border-collapse: collapse; font-size: 0.875rem }
3878
+ th { text-align: left; padding: 0.5rem 0.75rem; color: #64748b; font-weight: 500; border-bottom: 1px solid #334155 }
3879
+ td { padding: 0.5rem 0.75rem; border-bottom: 1px solid #1e293b }
3880
+ .status-dot { display: inline-block; width: 0.5rem; height: 0.5rem; border-radius: 50%; margin-right: 0.375rem }
3881
+ .status-dot.running { background: #4ade80 }
3882
+ .status-dot.stopped { background: #f87171 }
3883
+ </style>
3884
+ </head>
3885
+ <body>
3886
+ <h1>Kora Sync Server</h1>
3887
+ <p class="subtitle">v${version} &middot; <span class="status-dot running"></span>Running</p>
3888
+ <div class="grid">
3889
+ <div class="card"><div class="label">Uptime</div><div class="value">${uptime}</div></div>
3890
+ <div class="card"><div class="label">Connected Clients</div><div class="value" id="connectedClients">${status.connectedClients}</div></div>
3891
+ <div class="card"><div class="label">Total Operations</div><div class="value" id="totalOperations">${status.totalOperations}</div></div>
3892
+ <div class="card"><div class="label">Peak Connections</div><div class="value green" id="peakConnections">${status.peakConnections}</div></div>
3893
+ <div class="card"><div class="label">Ops Received</div><div class="value" id="opsReceived">${status.operationsReceived}</div></div>
3894
+ <div class="card"><div class="label">Ops Sent</div><div class="value" id="opsSent">${status.operationsSent}</div></div>
3895
+ <div class="card"><div class="label">Errors</div><div class="value ${status.errorCount > 0 ? "red" : "green"}" id="errors">${status.errorCount}</div></div>
3896
+ <div class="card"><div class="label">Schema Version</div><div class="value">${status.schemaVersion}</div></div>
3897
+ </div>
3898
+ <script>
3899
+ (function() {
3900
+ const es = new EventSource('/__kora/events');
3901
+ es.addEventListener('status', (e) => {
3902
+ const s = JSON.parse(e.data);
3903
+ for (const [id, val] of Object.entries({
3904
+ connectedClients: s.connectedClients,
3905
+ totalOperations: s.totalOperations,
3906
+ peakConnections: s.peakConnections,
3907
+ opsReceived: s.operationsReceived,
3908
+ opsSent: s.operationsSent,
3909
+ errors: s.errorCount,
3910
+ })) {
3911
+ const el = document.getElementById(id);
3912
+ if (el) { el.textContent = String(val); el.className = 'value' + (id === 'errors' && val > 0 ? ' red' : id === 'errors' ? ' green' : ''); }
3913
+ }
3914
+ });
3915
+ es.onerror = () => { setTimeout(() => document.location.reload(), 5000); };
3916
+ })();
3917
+ </script>
3918
+ </body>
3919
+ </html>`;
3920
+ }
3921
+ function formatUptime(ms) {
3922
+ const seconds = Math.floor(ms / 1e3);
3923
+ const minutes = Math.floor(seconds / 60);
3924
+ const hours = Math.floor(minutes / 60);
3925
+ const parts = [];
3926
+ if (hours > 0) parts.push(`${hours}h`);
3927
+ if (minutes % 60 > 0) parts.push(`${minutes % 60}m`);
3928
+ parts.push(`${seconds % 60}s`);
3929
+ return parts.join(" ");
3930
+ }
2533
3931
  // Annotate the CommonJS export names for ESM import in node:
2534
3932
  0 && (module.exports = {
2535
3933
  AwarenessRelay,
@@ -2544,9 +3942,13 @@ function createProductionServer(config) {
2544
3942
  SqliteServerStore,
2545
3943
  TokenAuthProvider,
2546
3944
  WsServerTransport,
3945
+ createDefaultLogger,
3946
+ createJsonLogger,
2547
3947
  createKoraServer,
2548
3948
  createPostgresServerStore,
3949
+ createPrettyLogger,
2549
3950
  createProductionServer,
3951
+ createSilentLogger,
2550
3952
  createSqliteServerStore
2551
3953
  });
2552
3954
  //# sourceMappingURL=index.cjs.map