@korajs/server 0.3.3 → 0.5.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,34 +30,230 @@ 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, {
170
+ AwarenessRelay: () => AwarenessRelay,
33
171
  ClientSession: () => ClientSession,
34
172
  HttpServerTransport: () => HttpServerTransport,
35
173
  KoraAuthProvider: () => KoraAuthProvider,
36
174
  KoraSyncServer: () => KoraSyncServer,
37
175
  MemoryServerStore: () => MemoryServerStore,
176
+ MixedAuthProvider: () => MixedAuthProvider,
38
177
  NoAuthProvider: () => NoAuthProvider,
39
178
  PostgresServerStore: () => PostgresServerStore,
40
179
  SqliteServerStore: () => SqliteServerStore,
41
180
  TokenAuthProvider: () => TokenAuthProvider,
42
181
  WsServerTransport: () => WsServerTransport,
182
+ createDefaultLogger: () => createDefaultLogger,
183
+ createJsonLogger: () => createJsonLogger,
43
184
  createKoraServer: () => createKoraServer,
44
185
  createPostgresServerStore: () => createPostgresServerStore,
186
+ createPrettyLogger: () => createPrettyLogger,
45
187
  createProductionServer: () => createProductionServer,
188
+ createSilentLogger: () => createSilentLogger,
46
189
  createSqliteServerStore: () => createSqliteServerStore
47
190
  });
48
191
  module.exports = __toCommonJS(index_exports);
192
+ init_cjs_shims();
49
193
 
50
- // ../../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
51
- 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;
52
- 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
+ }
53
250
 
54
251
  // src/store/memory-server-store.ts
252
+ init_cjs_shims();
55
253
  var import_core = require("@korajs/core");
56
254
 
57
255
  // src/store/materialization.ts
256
+ init_cjs_shims();
58
257
  function fieldTypeToSql(descriptor, dialect) {
59
258
  switch (descriptor.kind) {
60
259
  case "string":
@@ -116,9 +315,7 @@ ALTER TABLE ${name} ADD COLUMN ${colDef}`);
116
315
  `CREATE INDEX IF NOT EXISTS idx_${name}_${indexField} ON ${name} (${indexField})`
117
316
  );
118
317
  }
119
- statements.push(
120
- `CREATE INDEX IF NOT EXISTS idx_${name}__deleted ON ${name} (_deleted)`
121
- );
318
+ statements.push(`CREATE INDEX IF NOT EXISTS idx_${name}__deleted ON ${name} (_deleted)`);
122
319
  return statements;
123
320
  }
124
321
  function generateAllCollectionDDL(schema, dialect) {
@@ -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;
@@ -233,7 +433,7 @@ var MemoryServerStore = class {
233
433
  if (op.sequenceNumber > currentSeq) {
234
434
  this.versionVector.set(op.nodeId, op.sequenceNumber);
235
435
  }
236
- if (this.schema && this.schema.collections[op.collection]) {
436
+ if (this.schema?.collections[op.collection]) {
237
437
  this.rebuildMaterializedRecord(op.collection, op.recordId);
238
438
  }
239
439
  return "applied";
@@ -250,7 +450,7 @@ var MemoryServerStore = class {
250
450
  }
251
451
  async materializeCollection(collection) {
252
452
  this.assertOpen();
253
- if (this.schema && this.schema.collections[collection]) {
453
+ if (this.schema?.collections[collection]) {
254
454
  return this.queryCollection(collection);
255
455
  }
256
456
  return this.materializeFromOps(collection);
@@ -259,13 +459,14 @@ var MemoryServerStore = class {
259
459
  this.assertOpen();
260
460
  this.assertSchema();
261
461
  this.assertCollection(collection);
462
+ const schema = this.schema;
262
463
  if (options?.where) {
263
464
  for (const key of Object.keys(options.where)) {
264
- validateFieldName(collection, key, this.schema);
465
+ validateFieldName(collection, key, schema);
265
466
  }
266
467
  }
267
468
  if (options?.orderBy) {
268
- validateFieldName(collection, options.orderBy, this.schema);
469
+ validateFieldName(collection, options.orderBy, schema);
269
470
  }
270
471
  const collectionMap = this.materializedRecords.get(collection);
271
472
  if (!collectionMap) return [];
@@ -332,9 +533,10 @@ var MemoryServerStore = class {
332
533
  this.assertOpen();
333
534
  this.assertSchema();
334
535
  this.assertCollection(collection);
536
+ const schema = this.schema;
335
537
  if (where) {
336
538
  for (const key of Object.keys(where)) {
337
- validateFieldName(collection, key, this.schema);
539
+ validateFieldName(collection, key, schema);
338
540
  }
339
541
  }
340
542
  const collectionMap = this.materializedRecords.get(collection);
@@ -359,6 +561,49 @@ var MemoryServerStore = class {
359
561
  async close() {
360
562
  this.closed = true;
361
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
+ }
362
607
  // --- Testing helpers (not on interface) ---
363
608
  /**
364
609
  * Get all stored operations (for test assertions).
@@ -370,7 +615,7 @@ var MemoryServerStore = class {
370
615
  // Materialization internals
371
616
  // ---------------------------------------------------------------------------
372
617
  rebuildMaterializedRecord(collection, recordId) {
373
- const collectionDef = this.schema.collections[collection];
618
+ const collectionDef = this.schema?.collections[collection];
374
619
  if (!collectionDef) return;
375
620
  let collectionMap = this.materializedRecords.get(collection);
376
621
  if (!collectionMap) {
@@ -378,8 +623,10 @@ var MemoryServerStore = class {
378
623
  this.materializedRecords.set(collection, collectionMap);
379
624
  }
380
625
  const recordOps = this.operations.filter((op) => op.collection === collection && op.recordId === recordId).sort((a, b) => {
381
- if (a.timestamp.wallTime !== b.timestamp.wallTime) return a.timestamp.wallTime - b.timestamp.wallTime;
382
- if (a.timestamp.logical !== b.timestamp.logical) return a.timestamp.logical - b.timestamp.logical;
626
+ if (a.timestamp.wallTime !== b.timestamp.wallTime)
627
+ return a.timestamp.wallTime - b.timestamp.wallTime;
628
+ if (a.timestamp.logical !== b.timestamp.logical)
629
+ return a.timestamp.logical - b.timestamp.logical;
383
630
  return a.sequenceNumber - b.sequenceNumber;
384
631
  });
385
632
  const parsedOps = recordOps.map((op) => ({
@@ -431,8 +678,10 @@ var MemoryServerStore = class {
431
678
  // ---------------------------------------------------------------------------
432
679
  materializeFromOps(collection) {
433
680
  const collectionOps = this.operations.filter((op) => op.collection === collection).sort((a, b) => {
434
- if (a.timestamp.wallTime !== b.timestamp.wallTime) return a.timestamp.wallTime - b.timestamp.wallTime;
435
- if (a.timestamp.logical !== b.timestamp.logical) return a.timestamp.logical - b.timestamp.logical;
681
+ if (a.timestamp.wallTime !== b.timestamp.wallTime)
682
+ return a.timestamp.wallTime - b.timestamp.wallTime;
683
+ if (a.timestamp.logical !== b.timestamp.logical)
684
+ return a.timestamp.logical - b.timestamp.logical;
436
685
  return a.sequenceNumber - b.sequenceNumber;
437
686
  });
438
687
  const records = /* @__PURE__ */ new Map();
@@ -478,19 +727,22 @@ var MemoryServerStore = class {
478
727
  }
479
728
  }
480
729
  assertCollection(collection) {
481
- if (!this.schema.collections[collection]) {
730
+ const schema = this.schema;
731
+ if (!schema.collections[collection]) {
482
732
  throw new Error(
483
- `Unknown collection "${collection}". Available: ${Object.keys(this.schema.collections).join(", ")}`
733
+ `Unknown collection "${collection}". Available: ${Object.keys(schema.collections).join(", ")}`
484
734
  );
485
735
  }
486
736
  }
487
737
  };
488
738
 
489
739
  // src/store/postgres-server-store.ts
740
+ init_cjs_shims();
490
741
  var import_core2 = require("@korajs/core");
491
742
  var import_drizzle_orm = require("drizzle-orm");
492
743
 
493
744
  // src/store/drizzle-pg-schema.ts
745
+ init_cjs_shims();
494
746
  var import_pg_core = require("drizzle-orm/pg-core");
495
747
  var pgOperations = (0, import_pg_core.pgTable)(
496
748
  "operations",
@@ -545,6 +797,9 @@ var PostgresServerStore = class {
545
797
  getNodeId() {
546
798
  return this.nodeId;
547
799
  }
800
+ getSchema() {
801
+ return this.schema;
802
+ }
548
803
  async setSchema(schema) {
549
804
  this.assertOpen();
550
805
  await this.ready;
@@ -590,7 +845,7 @@ var PostgresServerStore = class {
590
845
  lastSeenAt: import_drizzle_orm.sql`${now}`
591
846
  }
592
847
  });
593
- if (this.schema && this.schema.collections[op.collection]) {
848
+ if (this.schema?.collections[op.collection]) {
594
849
  await this.rebuildMaterializedRecord(tx, op.collection, op.recordId);
595
850
  }
596
851
  });
@@ -604,10 +859,7 @@ var PostgresServerStore = class {
604
859
  this.assertOpen();
605
860
  await this.ready;
606
861
  const rows = await this.db.select().from(pgOperations).where(
607
- (0, import_drizzle_orm.and)(
608
- (0, import_drizzle_orm.eq)(pgOperations.nodeId, nodeId),
609
- (0, import_drizzle_orm.between)(pgOperations.sequenceNumber, fromSeq, toSeq)
610
- )
862
+ (0, import_drizzle_orm.and)((0, import_drizzle_orm.eq)(pgOperations.nodeId, nodeId), (0, import_drizzle_orm.between)(pgOperations.sequenceNumber, fromSeq, toSeq))
611
863
  ).orderBy((0, import_drizzle_orm.asc)(pgOperations.sequenceNumber));
612
864
  return rows.map((row) => this.deserializeOperation(row));
613
865
  }
@@ -620,7 +872,7 @@ var PostgresServerStore = class {
620
872
  async materializeCollection(collection) {
621
873
  this.assertOpen();
622
874
  await this.ready;
623
- if (this.schema && this.schema.collections[collection]) {
875
+ if (this.schema?.collections[collection]) {
624
876
  return this.queryCollection(collection);
625
877
  }
626
878
  return this.materializeFromOpsLog(collection);
@@ -630,14 +882,15 @@ var PostgresServerStore = class {
630
882
  await this.ready;
631
883
  this.assertSchema();
632
884
  this.assertCollection(collection);
633
- const collectionDef = this.schema.collections[collection];
885
+ const schema = this.schema;
886
+ const collectionDef = schema.collections[collection];
634
887
  if (options?.where) {
635
888
  for (const key of Object.keys(options.where)) {
636
- validateFieldName(collection, key, this.schema);
889
+ validateFieldName(collection, key, schema);
637
890
  }
638
891
  }
639
892
  if (options?.orderBy) {
640
- validateFieldName(collection, options.orderBy, this.schema);
893
+ validateFieldName(collection, options.orderBy, schema);
641
894
  }
642
895
  const query = this.buildSelectQuery(collection, options);
643
896
  const rows = await this.db.execute(query);
@@ -648,7 +901,8 @@ var PostgresServerStore = class {
648
901
  await this.ready;
649
902
  this.assertSchema();
650
903
  this.assertCollection(collection);
651
- const collectionDef = this.schema.collections[collection];
904
+ const schema = this.schema;
905
+ const collectionDef = schema.collections[collection];
652
906
  const query = import_drizzle_orm.sql`SELECT * FROM ${import_drizzle_orm.sql.raw(collection)} WHERE id = ${id} AND _deleted = 0`;
653
907
  const rows = await this.db.execute(query);
654
908
  if (rows.length === 0) return null;
@@ -659,9 +913,10 @@ var PostgresServerStore = class {
659
913
  await this.ready;
660
914
  this.assertSchema();
661
915
  this.assertCollection(collection);
916
+ const schema = this.schema;
662
917
  if (where) {
663
918
  for (const key of Object.keys(where)) {
664
- validateFieldName(collection, key, this.schema);
919
+ validateFieldName(collection, key, schema);
665
920
  }
666
921
  }
667
922
  const whereClause = this.buildWhereClause(where ?? {}, false);
@@ -673,6 +928,45 @@ var PostgresServerStore = class {
673
928
  async close() {
674
929
  this.closed = true;
675
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
+ }
676
970
  // ---------------------------------------------------------------------------
677
971
  // Materialization internals
678
972
  // ---------------------------------------------------------------------------
@@ -681,18 +975,13 @@ var PostgresServerStore = class {
681
975
  * all operations for that record.
682
976
  */
683
977
  async rebuildMaterializedRecord(txOrDb, collection, recordId) {
684
- const collectionDef = this.schema.collections[collection];
978
+ const collectionDef = this.schema?.collections[collection];
685
979
  if (!collectionDef) return;
686
980
  const ops = await txOrDb.select({
687
981
  type: pgOperations.type,
688
982
  data: pgOperations.data,
689
983
  wallTime: pgOperations.wallTime
690
- }).from(pgOperations).where(
691
- (0, import_drizzle_orm.and)(
692
- (0, import_drizzle_orm.eq)(pgOperations.collection, collection),
693
- (0, import_drizzle_orm.eq)(pgOperations.recordId, recordId)
694
- )
695
- ).orderBy(
984
+ }).from(pgOperations).where((0, import_drizzle_orm.and)((0, import_drizzle_orm.eq)(pgOperations.collection, collection), (0, import_drizzle_orm.eq)(pgOperations.recordId, recordId))).orderBy(
696
985
  (0, import_drizzle_orm.asc)(pgOperations.wallTime),
697
986
  (0, import_drizzle_orm.asc)(pgOperations.logical),
698
987
  (0, import_drizzle_orm.asc)(pgOperations.sequenceNumber)
@@ -762,7 +1051,7 @@ var PostgresServerStore = class {
762
1051
  * Backfill a single collection's materialized table from operations.
763
1052
  */
764
1053
  async backfillCollection(collectionName) {
765
- const collectionDef = this.schema.collections[collectionName];
1054
+ const collectionDef = this.schema?.collections[collectionName];
766
1055
  if (!collectionDef) return;
767
1056
  const allOps = await this.db.select({
768
1057
  recordId: pgOperations.recordId,
@@ -819,9 +1108,7 @@ var PostgresServerStore = class {
819
1108
  options?.where ?? {},
820
1109
  options?.includeDeleted ?? false
821
1110
  );
822
- const parts = [
823
- import_drizzle_orm.sql`SELECT * FROM ${import_drizzle_orm.sql.raw(collection)} WHERE ${whereClause}`
824
- ];
1111
+ const parts = [import_drizzle_orm.sql`SELECT * FROM ${import_drizzle_orm.sql.raw(collection)} WHERE ${whereClause}`];
825
1112
  if (options?.orderBy) {
826
1113
  const dir = options.orderDirection === "desc" ? "DESC" : "ASC";
827
1114
  parts.push(import_drizzle_orm.sql.raw(` ORDER BY ${options.orderBy} ${dir}`));
@@ -934,12 +1221,8 @@ var PostgresServerStore = class {
934
1221
  await this.db.execute(
935
1222
  import_drizzle_orm.sql`CREATE INDEX IF NOT EXISTS idx_node_seq ON operations (node_id, sequence_number)`
936
1223
  );
937
- await this.db.execute(
938
- import_drizzle_orm.sql`CREATE INDEX IF NOT EXISTS idx_collection ON operations (collection)`
939
- );
940
- await this.db.execute(
941
- import_drizzle_orm.sql`CREATE INDEX IF NOT EXISTS idx_received ON operations (received_at)`
942
- );
1224
+ await this.db.execute(import_drizzle_orm.sql`CREATE INDEX IF NOT EXISTS idx_collection ON operations (collection)`);
1225
+ await this.db.execute(import_drizzle_orm.sql`CREATE INDEX IF NOT EXISTS idx_received ON operations (received_at)`);
943
1226
  await this.db.execute(
944
1227
  import_drizzle_orm.sql`CREATE INDEX IF NOT EXISTS idx_collection_record ON operations (collection, record_id)`
945
1228
  );
@@ -1007,9 +1290,10 @@ var PostgresServerStore = class {
1007
1290
  }
1008
1291
  }
1009
1292
  assertCollection(collection) {
1010
- if (!this.schema.collections[collection]) {
1293
+ const schema = this.schema;
1294
+ if (!schema.collections[collection]) {
1011
1295
  throw new Error(
1012
- `Unknown collection "${collection}". Available: ${Object.keys(this.schema.collections).join(", ")}`
1296
+ `Unknown collection "${collection}". Available: ${Object.keys(schema.collections).join(", ")}`
1013
1297
  );
1014
1298
  }
1015
1299
  }
@@ -1037,11 +1321,13 @@ async function loadPostgresDeps() {
1037
1321
  }
1038
1322
 
1039
1323
  // src/store/sqlite-server-store.ts
1324
+ init_cjs_shims();
1040
1325
  var import_node_module = require("module");
1041
1326
  var import_core3 = require("@korajs/core");
1042
1327
  var import_drizzle_orm2 = require("drizzle-orm");
1043
1328
 
1044
1329
  // src/store/drizzle-schema.ts
1330
+ init_cjs_shims();
1045
1331
  var import_sqlite_core = require("drizzle-orm/sqlite-core");
1046
1332
  var operations = (0, import_sqlite_core.sqliteTable)(
1047
1333
  "operations",
@@ -1100,6 +1386,9 @@ var SqliteServerStore = class {
1100
1386
  getNodeId() {
1101
1387
  return this.nodeId;
1102
1388
  }
1389
+ getSchema() {
1390
+ return this.schema;
1391
+ }
1103
1392
  async setSchema(schema) {
1104
1393
  this.assertOpen();
1105
1394
  this.schema = schema;
@@ -1142,7 +1431,7 @@ var SqliteServerStore = class {
1142
1431
  lastSeenAt: import_drizzle_orm2.sql`${now}`
1143
1432
  }
1144
1433
  }).run();
1145
- if (this.schema && this.schema.collections[op.collection]) {
1434
+ if (this.schema?.collections[op.collection]) {
1146
1435
  this.rebuildMaterializedRecord(tx, op.collection, op.recordId);
1147
1436
  }
1148
1437
  return "applied";
@@ -1161,7 +1450,7 @@ var SqliteServerStore = class {
1161
1450
  }
1162
1451
  async materializeCollection(collection) {
1163
1452
  this.assertOpen();
1164
- if (this.schema && this.schema.collections[collection]) {
1453
+ if (this.schema?.collections[collection]) {
1165
1454
  return this.queryCollection(collection);
1166
1455
  }
1167
1456
  return this.materializeFromOpsLog(collection);
@@ -1170,14 +1459,15 @@ var SqliteServerStore = class {
1170
1459
  this.assertOpen();
1171
1460
  this.assertSchema();
1172
1461
  this.assertCollection(collection);
1173
- const collectionDef = this.schema.collections[collection];
1462
+ const schema = this.schema;
1463
+ const collectionDef = schema.collections[collection];
1174
1464
  if (options?.where) {
1175
1465
  for (const key of Object.keys(options.where)) {
1176
- validateFieldName(collection, key, this.schema);
1466
+ validateFieldName(collection, key, schema);
1177
1467
  }
1178
1468
  }
1179
1469
  if (options?.orderBy) {
1180
- validateFieldName(collection, options.orderBy, this.schema);
1470
+ validateFieldName(collection, options.orderBy, schema);
1181
1471
  }
1182
1472
  const query = this.buildSelectQuery(collection, options);
1183
1473
  const rows = this.db.all(query);
@@ -1187,7 +1477,8 @@ var SqliteServerStore = class {
1187
1477
  this.assertOpen();
1188
1478
  this.assertSchema();
1189
1479
  this.assertCollection(collection);
1190
- const collectionDef = this.schema.collections[collection];
1480
+ const schema = this.schema;
1481
+ const collectionDef = schema.collections[collection];
1191
1482
  const query = import_drizzle_orm2.sql`SELECT * FROM ${import_drizzle_orm2.sql.raw(collection)} WHERE id = ${id} AND _deleted = 0`;
1192
1483
  const rows = this.db.all(query);
1193
1484
  if (rows.length === 0) return null;
@@ -1197,9 +1488,10 @@ var SqliteServerStore = class {
1197
1488
  this.assertOpen();
1198
1489
  this.assertSchema();
1199
1490
  this.assertCollection(collection);
1491
+ const schema = this.schema;
1200
1492
  if (where) {
1201
1493
  for (const key of Object.keys(where)) {
1202
- validateFieldName(collection, key, this.schema);
1494
+ validateFieldName(collection, key, schema);
1203
1495
  }
1204
1496
  }
1205
1497
  const whereClause = this.buildWhereClause(where ?? {}, false);
@@ -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
  // ---------------------------------------------------------------------------
@@ -1219,18 +1544,13 @@ var SqliteServerStore = class {
1219
1544
  * transaction for atomic dual-write.
1220
1545
  */
1221
1546
  rebuildMaterializedRecord(txOrDb, collection, recordId) {
1222
- const collectionDef = this.schema.collections[collection];
1547
+ const collectionDef = this.schema?.collections[collection];
1223
1548
  if (!collectionDef) return;
1224
1549
  const ops = txOrDb.select({
1225
1550
  type: operations.type,
1226
1551
  data: operations.data,
1227
1552
  wallTime: operations.wallTime
1228
- }).from(operations).where(
1229
- (0, import_drizzle_orm2.and)(
1230
- (0, import_drizzle_orm2.eq)(operations.collection, collection),
1231
- (0, import_drizzle_orm2.eq)(operations.recordId, recordId)
1232
- )
1233
- ).orderBy((0, import_drizzle_orm2.asc)(operations.wallTime), (0, import_drizzle_orm2.asc)(operations.logical), (0, import_drizzle_orm2.asc)(operations.sequenceNumber)).all();
1553
+ }).from(operations).where((0, import_drizzle_orm2.and)((0, import_drizzle_orm2.eq)(operations.collection, collection), (0, import_drizzle_orm2.eq)(operations.recordId, recordId))).orderBy((0, import_drizzle_orm2.asc)(operations.wallTime), (0, import_drizzle_orm2.asc)(operations.logical), (0, import_drizzle_orm2.asc)(operations.sequenceNumber)).all();
1234
1554
  const parsedOps = ops.map((op) => ({
1235
1555
  type: op.type,
1236
1556
  data: op.data !== null ? JSON.parse(op.data) : null
@@ -1299,7 +1619,7 @@ var SqliteServerStore = class {
1299
1619
  * Backfill a single collection's materialized table from operations.
1300
1620
  */
1301
1621
  backfillCollection(collectionName) {
1302
- const collectionDef = this.schema.collections[collectionName];
1622
+ const collectionDef = this.schema?.collections[collectionName];
1303
1623
  if (!collectionDef) return;
1304
1624
  const allOps = this.db.select({
1305
1625
  recordId: operations.recordId,
@@ -1354,9 +1674,7 @@ var SqliteServerStore = class {
1354
1674
  options?.where ?? {},
1355
1675
  options?.includeDeleted ?? false
1356
1676
  );
1357
- const parts = [
1358
- import_drizzle_orm2.sql`SELECT * FROM ${import_drizzle_orm2.sql.raw(collection)} WHERE ${whereClause}`
1359
- ];
1677
+ const parts = [import_drizzle_orm2.sql`SELECT * FROM ${import_drizzle_orm2.sql.raw(collection)} WHERE ${whereClause}`];
1360
1678
  if (options?.orderBy) {
1361
1679
  const dir = options.orderDirection === "desc" ? "DESC" : "ASC";
1362
1680
  parts.push(import_drizzle_orm2.sql.raw(` ORDER BY ${options.orderBy} ${dir}`));
@@ -1531,9 +1849,10 @@ var SqliteServerStore = class {
1531
1849
  }
1532
1850
  }
1533
1851
  assertCollection(collection) {
1534
- if (!this.schema.collections[collection]) {
1852
+ const schema = this.schema;
1853
+ if (!schema.collections[collection]) {
1535
1854
  throw new Error(
1536
- `Unknown collection "${collection}". Available: ${Object.keys(this.schema.collections).join(", ")}`
1855
+ `Unknown collection "${collection}". Available: ${Object.keys(schema.collections).join(", ")}`
1537
1856
  );
1538
1857
  }
1539
1858
  }
@@ -1549,6 +1868,7 @@ function createSqliteServerStore(options) {
1549
1868
  }
1550
1869
 
1551
1870
  // src/transport/http-server-transport.ts
1871
+ init_cjs_shims();
1552
1872
  var import_core4 = require("@korajs/core");
1553
1873
  var HttpServerTransport = class {
1554
1874
  serializer;
@@ -1630,6 +1950,7 @@ var HttpServerTransport = class {
1630
1950
  };
1631
1951
 
1632
1952
  // src/transport/ws-server-transport.ts
1953
+ init_cjs_shims();
1633
1954
  var import_core5 = require("@korajs/core");
1634
1955
  var import_sync = require("@korajs/sync");
1635
1956
  var WS_OPEN = 1;
@@ -1693,11 +2014,220 @@ var WsServerTransport = class {
1693
2014
  };
1694
2015
 
1695
2016
  // src/session/client-session.ts
1696
- var import_core6 = require("@korajs/core");
2017
+ init_cjs_shims();
2018
+ var import_core8 = require("@korajs/core");
1697
2019
  var import_internal = require("@korajs/core/internal");
1698
2020
  var import_sync2 = require("@korajs/sync");
1699
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
+
1700
2229
  // src/scopes/server-scope-filter.ts
2230
+ init_cjs_shims();
1701
2231
  function operationMatchesScopes(op, scopes) {
1702
2232
  if (!scopes) return true;
1703
2233
  const collectionScope = scopes[op.collection];
@@ -1727,6 +2257,57 @@ function asRecord(value) {
1727
2257
  return value;
1728
2258
  }
1729
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
+
1730
2311
  // src/session/client-session.ts
1731
2312
  var DEFAULT_BATCH_SIZE = 100;
1732
2313
  var DEFAULT_SCHEMA_VERSION = 1;
@@ -1734,6 +2315,8 @@ var ClientSession = class {
1734
2315
  state = "connected";
1735
2316
  clientNodeId = null;
1736
2317
  authContext = null;
2318
+ syncQuerySubsets = [];
2319
+ resumeDeltaCursor = null;
1737
2320
  sessionId;
1738
2321
  transport;
1739
2322
  store;
@@ -1742,8 +2325,14 @@ var ClientSession = class {
1742
2325
  emitter;
1743
2326
  batchSize;
1744
2327
  schemaVersion;
2328
+ supportedSchemaVersions;
1745
2329
  onRelay;
2330
+ onAwarenessUpdate;
2331
+ onYjsDocUpdate;
1746
2332
  onClose;
2333
+ maxOperationBytes;
2334
+ maxOpsPerMinute;
2335
+ rateLimiter;
1747
2336
  constructor(options) {
1748
2337
  this.sessionId = options.sessionId;
1749
2338
  this.transport = options.transport;
@@ -1753,14 +2342,24 @@ var ClientSession = class {
1753
2342
  this.emitter = options.emitter ?? null;
1754
2343
  this.batchSize = options.batchSize ?? DEFAULT_BATCH_SIZE;
1755
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
+ };
1756
2350
  this.onRelay = options.onRelay ?? null;
2351
+ this.onAwarenessUpdate = options.onAwarenessUpdate ?? null;
2352
+ this.onYjsDocUpdate = options.onYjsDocUpdate ?? null;
1757
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);
1758
2357
  }
1759
2358
  /**
1760
2359
  * Start handling messages from the client transport.
1761
2360
  */
1762
2361
  start() {
1763
- this.transport.onMessage((msg) => this.handleMessage(msg));
2362
+ this.transport.onMessage((msg) => this.enqueueMessage(msg));
1764
2363
  this.transport.onClose((_code, _reason) => this.handleTransportClose());
1765
2364
  this.transport.onError((_err) => {
1766
2365
  if (this.state !== "closed") {
@@ -1775,19 +2374,17 @@ var ClientSession = class {
1775
2374
  relayOperations(operations2) {
1776
2375
  if (this.state !== "streaming" || !this.transport.isConnected()) return;
1777
2376
  if (operations2.length === 0) return;
1778
- const visibleOperations = operations2.filter(
1779
- (op) => operationMatchesScopes(op, this.authContext?.scopes)
1780
- );
2377
+ const visibleOperations = operations2.filter((op) => this.operationVisibleToClient(op));
1781
2378
  if (visibleOperations.length === 0) return;
1782
2379
  const serializedOps = visibleOperations.map((op) => this.serializer.encodeOperation(op));
1783
2380
  const msg = {
1784
2381
  type: "operation-batch",
1785
- messageId: (0, import_core6.generateUUIDv7)(),
2382
+ messageId: (0, import_core8.generateUUIDv7)(),
1786
2383
  operations: serializedOps,
1787
2384
  isFinal: true,
1788
2385
  batchIndex: 0
1789
2386
  };
1790
- this.transport.send(msg);
2387
+ this.sendToClient(msg);
1791
2388
  }
1792
2389
  /**
1793
2390
  * Close this session.
@@ -1816,22 +2413,55 @@ var ClientSession = class {
1816
2413
  isStreaming() {
1817
2414
  return this.state === "streaming";
1818
2415
  }
2416
+ /**
2417
+ * Get the transport for this session.
2418
+ * Used by the awareness relay to send messages to this client.
2419
+ */
2420
+ getTransport() {
2421
+ return this.transport;
2422
+ }
1819
2423
  // --- Private protocol handlers ---
1820
- 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) {
1821
2441
  switch (message.type) {
1822
2442
  case "handshake":
1823
- this.handleHandshake(message);
2443
+ await this.handleHandshake(message);
1824
2444
  break;
1825
2445
  case "operation-batch":
1826
- this.handleOperationBatch(message);
2446
+ await this.handleOperationBatch(message);
1827
2447
  break;
1828
- // Acknowledgments from clients are noted but no action needed on server
1829
2448
  case "acknowledgment":
1830
2449
  break;
1831
2450
  case "error":
1832
2451
  break;
2452
+ case "awareness-update":
2453
+ this.handleAwarenessUpdate(message);
2454
+ break;
2455
+ case "yjs-doc-update":
2456
+ this.handleYjsDocUpdate(message);
2457
+ break;
1833
2458
  }
1834
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
+ }
1835
2465
  async handleHandshake(msg) {
1836
2466
  if (this.state !== "connected") {
1837
2467
  this.sendError("DUPLICATE_HANDSHAKE", "Handshake already completed", false);
@@ -1849,19 +2479,56 @@ var ClientSession = class {
1849
2479
  this.authContext = context;
1850
2480
  this.state = "authenticated";
1851
2481
  }
2482
+ const resolvedScopes = resolveSessionScopes(this.store.getSchema(), {
2483
+ handshakeScope: msg.syncScope,
2484
+ authScopes: this.authContext?.scopes
2485
+ });
2486
+ if (resolvedScopes) {
2487
+ if (this.authContext) {
2488
+ this.authContext = { ...this.authContext, scopes: resolvedScopes };
2489
+ } else {
2490
+ this.authContext = { userId: msg.nodeId, scopes: resolvedScopes };
2491
+ }
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;
1852
2499
  const serverVector = this.store.getVersionVector();
1853
2500
  const selectedWireFormat = selectWireFormat(msg.supportedWireFormats);
1854
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
+ }
1855
2519
  const response = {
1856
2520
  type: "handshake-response",
1857
- messageId: (0, import_core6.generateUUIDv7)(),
2521
+ messageId: (0, import_core8.generateUUIDv7)(),
1858
2522
  nodeId: this.store.getNodeId(),
1859
2523
  versionVector: (0, import_sync2.versionVectorToWire)(serverVector),
1860
2524
  schemaVersion: this.schemaVersion,
1861
2525
  accepted: true,
1862
- selectedWireFormat
2526
+ selectedWireFormat,
2527
+ // Confirm the accepted scope so the client knows what data will be synced.
2528
+ // This may differ from what the client requested if auth scopes are narrower.
2529
+ ...this.authContext?.scopes ? { acceptedScope: this.authContext.scopes } : {}
1863
2530
  };
1864
- this.transport.send(response);
2531
+ this.sendToClient(response);
1865
2532
  this.emitter?.emit({ type: "sync:connected", nodeId: msg.nodeId });
1866
2533
  this.state = "syncing";
1867
2534
  const clientVector = (0, import_sync2.wireToVersionVector)(msg.versionVector);
@@ -1871,13 +2538,56 @@ var ClientSession = class {
1871
2538
  async handleOperationBatch(msg) {
1872
2539
  const operations2 = msg.operations.map((s) => this.serializer.decodeOperation(s));
1873
2540
  const applied = [];
2541
+ const rejected = [];
1874
2542
  for (const op of operations2) {
1875
- if (!operationMatchesScopes(op, this.authContext?.scopes)) {
2543
+ if (!this.operationVisibleToClient(op)) {
2544
+ rejected.push(op);
2545
+ continue;
2546
+ }
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
+ );
1876
2561
  continue;
1877
2562
  }
1878
- const result = await this.store.applyRemoteOperation(op);
1879
- if (result === "applied") {
1880
- applied.push(op);
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 {
2582
+ }
2583
+ }
2584
+ if (rejected.length > 0) {
2585
+ for (const op of rejected) {
2586
+ this.sendError(
2587
+ "SCOPE_VIOLATION",
2588
+ `Operation "${op.id}" in collection "${op.collection}" is outside the client's sync scope`,
2589
+ false
2590
+ );
1881
2591
  }
1882
2592
  }
1883
2593
  if (operations2.length > 0) {
@@ -1890,11 +2600,11 @@ var ClientSession = class {
1890
2600
  const lastOp = operations2[operations2.length - 1];
1891
2601
  const ack = {
1892
2602
  type: "acknowledgment",
1893
- messageId: (0, import_core6.generateUUIDv7)(),
2603
+ messageId: (0, import_core8.generateUUIDv7)(),
1894
2604
  acknowledgedMessageId: msg.messageId,
1895
2605
  lastSequenceNumber: lastOp ? lastOp.sequenceNumber : 0
1896
2606
  };
1897
- this.transport.send(ack);
2607
+ this.sendToClient(ack);
1898
2608
  if (applied.length > 0) {
1899
2609
  this.onRelay?.(this.sessionId, applied);
1900
2610
  }
@@ -1906,35 +2616,52 @@ var ClientSession = class {
1906
2616
  const clientSeq = clientVector.get(nodeId) ?? 0;
1907
2617
  if (serverSeq > clientSeq) {
1908
2618
  const ops = await this.store.getOperationRange(nodeId, clientSeq + 1, serverSeq);
1909
- const visible = ops.filter((op) => operationMatchesScopes(op, this.authContext?.scopes));
2619
+ const visible = ops.filter((op) => this.operationVisibleToClient(op));
1910
2620
  missing.push(...visible);
1911
2621
  }
1912
2622
  }
1913
2623
  if (missing.length === 0) {
1914
2624
  const emptyBatch = {
1915
2625
  type: "operation-batch",
1916
- messageId: (0, import_core6.generateUUIDv7)(),
2626
+ messageId: (0, import_core8.generateUUIDv7)(),
1917
2627
  operations: [],
1918
2628
  isFinal: true,
1919
- batchIndex: 0
2629
+ batchIndex: 0,
2630
+ totalBatches: 1
1920
2631
  };
1921
- this.transport.send(emptyBatch);
2632
+ this.sendToClient(emptyBatch);
1922
2633
  return;
1923
2634
  }
1924
2635
  const sorted = (0, import_internal.topologicalSort)(missing);
1925
- 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
+ }
1926
2650
  for (let i = 0; i < totalBatches; i++) {
1927
2651
  const start = i * this.batchSize;
1928
- const batchOps = sorted.slice(start, start + this.batchSize);
2652
+ const batchOps = afterCursor.slice(start, start + this.batchSize);
1929
2653
  const serializedOps = batchOps.map((op) => this.serializer.encodeOperation(op));
2654
+ const batchCursor = (0, import_sync2.createDeltaCursorFromBatch)(batchOps, i);
1930
2655
  const batchMsg = {
1931
2656
  type: "operation-batch",
1932
- messageId: (0, import_core6.generateUUIDv7)(),
2657
+ messageId: (0, import_core8.generateUUIDv7)(),
1933
2658
  operations: serializedOps,
1934
2659
  isFinal: i === totalBatches - 1,
1935
- batchIndex: i
2660
+ batchIndex: i,
2661
+ totalBatches,
2662
+ ...batchCursor ? { cursor: (0, import_sync2.encodeDeltaCursor)(batchCursor) } : {}
1936
2663
  };
1937
- this.transport.send(batchMsg);
2664
+ this.sendToClient(batchMsg);
1938
2665
  this.emitter?.emit({
1939
2666
  type: "sync:sent",
1940
2667
  operations: batchOps,
@@ -1942,15 +2669,27 @@ var ClientSession = class {
1942
2669
  });
1943
2670
  }
1944
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
+ }
2678
+ handleAwarenessUpdate(msg) {
2679
+ this.onAwarenessUpdate?.(this.sessionId, msg);
2680
+ }
2681
+ handleYjsDocUpdate(msg) {
2682
+ this.onYjsDocUpdate?.(this.sessionId, msg);
2683
+ }
1945
2684
  sendError(code, message, retriable) {
1946
2685
  const errorMsg = {
1947
2686
  type: "error",
1948
- messageId: (0, import_core6.generateUUIDv7)(),
2687
+ messageId: (0, import_core8.generateUUIDv7)(),
1949
2688
  code,
1950
2689
  message,
1951
2690
  retriable
1952
2691
  };
1953
- this.transport.send(errorMsg);
2692
+ this.sendToClient(errorMsg);
1954
2693
  }
1955
2694
  setSerializerWireFormat(format) {
1956
2695
  if (typeof this.serializer.setWireFormat === "function") {
@@ -1972,8 +2711,262 @@ function selectWireFormat(supportedWireFormats) {
1972
2711
  }
1973
2712
 
1974
2713
  // src/server/kora-sync-server.ts
1975
- var import_core7 = require("@korajs/core");
2714
+ init_cjs_shims();
2715
+ var import_core10 = require("@korajs/core");
2716
+ var import_internal2 = require("@korajs/core/internal");
1976
2717
  var import_sync3 = require("@korajs/sync");
2718
+
2719
+ // src/awareness/awareness-relay.ts
2720
+ init_cjs_shims();
2721
+ var import_core9 = require("@korajs/core");
2722
+ var AwarenessRelay = class {
2723
+ clients = /* @__PURE__ */ new Map();
2724
+ /**
2725
+ * Register a client for awareness broadcasting.
2726
+ *
2727
+ * @param sessionId - Unique session identifier
2728
+ * @param clientId - Client-assigned awareness ID
2729
+ * @param transport - Transport for sending messages to this client
2730
+ */
2731
+ addClient(sessionId, clientId, transport) {
2732
+ this.clients.set(sessionId, {
2733
+ sessionId,
2734
+ clientId,
2735
+ transport,
2736
+ state: null
2737
+ });
2738
+ const existingStates = {};
2739
+ let hasStates = false;
2740
+ for (const [, client] of this.clients) {
2741
+ if (client.sessionId === sessionId) continue;
2742
+ if (client.state) {
2743
+ existingStates[String(client.clientId)] = client.state;
2744
+ hasStates = true;
2745
+ }
2746
+ }
2747
+ if (hasStates) {
2748
+ const catchUpMsg = {
2749
+ type: "awareness-update",
2750
+ messageId: (0, import_core9.generateUUIDv7)(),
2751
+ clientId: 0,
2752
+ // Server-sourced
2753
+ states: existingStates
2754
+ };
2755
+ transport.send(catchUpMsg);
2756
+ }
2757
+ }
2758
+ /**
2759
+ * Remove a client and broadcast its removal to all remaining clients.
2760
+ *
2761
+ * @param sessionId - Session ID of the disconnecting client
2762
+ */
2763
+ removeClient(sessionId) {
2764
+ const client = this.clients.get(sessionId);
2765
+ if (!client) return;
2766
+ this.clients.delete(sessionId);
2767
+ if (client.state === null) return;
2768
+ const removalStates = {
2769
+ [String(client.clientId)]: null
2770
+ };
2771
+ const msg = {
2772
+ type: "awareness-update",
2773
+ messageId: (0, import_core9.generateUUIDv7)(),
2774
+ clientId: client.clientId,
2775
+ states: removalStates
2776
+ };
2777
+ this.broadcastExcept(sessionId, msg);
2778
+ }
2779
+ /**
2780
+ * Handle an incoming awareness update from a client.
2781
+ * Stores the state and relays to all other connected clients.
2782
+ *
2783
+ * @param sessionId - Session ID of the sending client
2784
+ * @param message - The awareness update message
2785
+ */
2786
+ handleUpdate(sessionId, message) {
2787
+ const sender = this.clients.get(sessionId);
2788
+ if (!sender) return;
2789
+ const senderState = message.states[String(message.clientId)];
2790
+ if (senderState !== void 0) {
2791
+ sender.state = senderState;
2792
+ }
2793
+ this.broadcastExcept(sessionId, message);
2794
+ }
2795
+ /**
2796
+ * Get the number of registered awareness clients.
2797
+ */
2798
+ getClientCount() {
2799
+ return this.clients.size;
2800
+ }
2801
+ /**
2802
+ * Remove all clients and clear all state.
2803
+ */
2804
+ clear() {
2805
+ this.clients.clear();
2806
+ }
2807
+ // --- Private ---
2808
+ broadcastExcept(excludeSessionId, message) {
2809
+ for (const [, client] of this.clients) {
2810
+ if (client.sessionId === excludeSessionId) continue;
2811
+ if (!client.transport.isConnected()) continue;
2812
+ client.transport.send(message);
2813
+ }
2814
+ }
2815
+ };
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
+
2969
+ // src/server/kora-sync-server.ts
1977
2970
  var DEFAULT_MAX_CONNECTIONS = 0;
1978
2971
  var DEFAULT_BATCH_SIZE2 = 100;
1979
2972
  var DEFAULT_SCHEMA_VERSION2 = 1;
@@ -1987,12 +2980,18 @@ var KoraSyncServer = class {
1987
2980
  maxConnections;
1988
2981
  batchSize;
1989
2982
  schemaVersion;
2983
+ supportedSchemaVersions;
1990
2984
  port;
1991
2985
  host;
1992
2986
  path;
2987
+ logger;
2988
+ metrics;
2989
+ awarenessRelay = new AwarenessRelay();
2990
+ yjsDocRelay = new YjsDocRelay();
1993
2991
  sessions = /* @__PURE__ */ new Map();
1994
2992
  httpClients = /* @__PURE__ */ new Map();
1995
2993
  httpSessionToClient = /* @__PURE__ */ new Map();
2994
+ serverVersion = "0.4.0";
1996
2995
  wsServer = null;
1997
2996
  running = false;
1998
2997
  constructor(config) {
@@ -2003,9 +3002,80 @@ var KoraSyncServer = class {
2003
3002
  this.maxConnections = config.maxConnections ?? DEFAULT_MAX_CONNECTIONS;
2004
3003
  this.batchSize = config.batchSize ?? DEFAULT_BATCH_SIZE2;
2005
3004
  this.schemaVersion = config.schemaVersion ?? DEFAULT_SCHEMA_VERSION2;
3005
+ this.supportedSchemaVersions = config.supportedSchemaVersions ?? {
3006
+ min: this.schemaVersion,
3007
+ max: this.schemaVersion
3008
+ };
2006
3009
  this.port = config.port;
2007
3010
  this.host = config.host ?? DEFAULT_HOST;
2008
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;
2009
3079
  }
2010
3080
  /**
2011
3081
  * Start the WebSocket server in standalone mode.
@@ -2014,10 +3084,10 @@ var KoraSyncServer = class {
2014
3084
  */
2015
3085
  async start(wsServerImpl) {
2016
3086
  if (this.running) {
2017
- throw new import_core7.SyncError("Server is already running", { port: this.port });
3087
+ throw new import_core10.SyncError("Server is already running", { port: this.port });
2018
3088
  }
2019
3089
  if (!wsServerImpl && this.port === void 0) {
2020
- throw new import_core7.SyncError(
3090
+ throw new import_core10.SyncError(
2021
3091
  "Port is required for standalone mode. Provide port in config or use handleConnection() for attach mode.",
2022
3092
  {}
2023
3093
  );
@@ -2046,11 +3116,25 @@ var KoraSyncServer = class {
2046
3116
  this.handleConnection(transport);
2047
3117
  });
2048
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
+ });
2049
3125
  }
2050
3126
  /**
2051
3127
  * Stop the server. Closes all sessions and the WebSocket server.
2052
3128
  */
2053
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
+ });
3136
+ this.awarenessRelay.clear();
3137
+ this.yjsDocRelay.clear();
2054
3138
  for (const session of this.sessions.values()) {
2055
3139
  session.close("server shutting down");
2056
3140
  }
@@ -2064,6 +3148,11 @@ var KoraSyncServer = class {
2064
3148
  this.wsServer = null;
2065
3149
  }
2066
3150
  this.running = false;
3151
+ this.logger.log({
3152
+ timestamp: Date.now(),
3153
+ level: "info",
3154
+ event: "server.stopped"
3155
+ });
2067
3156
  }
2068
3157
  /**
2069
3158
  * Handle one HTTP sync request for a long-polling client.
@@ -2107,47 +3196,125 @@ var KoraSyncServer = class {
2107
3196
  if (this.maxConnections > 0 && this.sessions.size >= this.maxConnections) {
2108
3197
  transport.send({
2109
3198
  type: "error",
2110
- messageId: (0, import_core7.generateUUIDv7)(),
3199
+ messageId: (0, import_core10.generateUUIDv7)(),
2111
3200
  code: "MAX_CONNECTIONS",
2112
3201
  message: `Server has reached maximum connections (${this.maxConnections})`,
2113
3202
  retriable: true
2114
3203
  });
2115
3204
  transport.close(4029, "max connections reached");
2116
- throw new import_core7.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", {
2117
3213
  current: this.sessions.size,
2118
3214
  max: this.maxConnections
2119
3215
  });
2120
3216
  }
2121
- const sessionId = (0, import_core7.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
+ });
2122
3263
  const session = new ClientSession({
2123
3264
  sessionId,
2124
3265
  transport,
2125
3266
  store: this.store,
2126
3267
  auth: this.auth ?? void 0,
2127
3268
  serializer: this.serializer,
2128
- emitter: this.emitter ?? void 0,
3269
+ emitter: sessionEmitter,
2129
3270
  batchSize: this.batchSize,
2130
3271
  schemaVersion: this.schemaVersion,
3272
+ supportedSchemaVersions: this.supportedSchemaVersions,
2131
3273
  onRelay: (sourceSessionId, operations2) => {
2132
3274
  this.handleRelay(sourceSessionId, operations2);
2133
3275
  },
3276
+ onAwarenessUpdate: (sourceSessionId, message) => {
3277
+ this.handleAwarenessRelay(sourceSessionId, message);
3278
+ },
3279
+ onYjsDocUpdate: (sourceSessionId, message) => {
3280
+ this.handleYjsDocRelay(sourceSessionId, message);
3281
+ },
2134
3282
  onClose: (sid) => {
2135
3283
  this.handleSessionClose(sid);
2136
3284
  }
2137
3285
  });
2138
3286
  this.sessions.set(sessionId, session);
3287
+ this.yjsDocRelay.addClient(sessionId, transport);
2139
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
+ });
2140
3296
  return sessionId;
2141
3297
  }
2142
3298
  /**
2143
3299
  * Get the current server status.
2144
3300
  */
2145
3301
  async getStatus() {
3302
+ const totalOps = await this.store.getOperationCount();
3303
+ const snapshot = this.metrics.getSnapshot(totalOps);
2146
3304
  return {
2147
3305
  running: this.running,
2148
- connectedClients: this.sessions.size,
3306
+ connectedClients: snapshot.connectedClients,
2149
3307
  port: this.port ?? null,
2150
- 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
2151
3318
  };
2152
3319
  }
2153
3320
  /**
@@ -2158,12 +3325,31 @@ var KoraSyncServer = class {
2158
3325
  }
2159
3326
  // --- Private ---
2160
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
+ });
2161
3344
  for (const [sessionId, session] of this.sessions) {
2162
3345
  if (sessionId === sourceSessionId) continue;
2163
3346
  session.relayOperations(operations2);
2164
3347
  }
2165
3348
  }
2166
3349
  handleSessionClose(sessionId) {
3350
+ this.metrics.recordDisconnection(sessionId);
3351
+ this.awarenessRelay.removeClient(sessionId);
3352
+ this.yjsDocRelay.removeClient(sessionId);
2167
3353
  this.sessions.delete(sessionId);
2168
3354
  const clientId = this.httpSessionToClient.get(sessionId);
2169
3355
  if (clientId) {
@@ -2171,6 +3357,21 @@ var KoraSyncServer = class {
2171
3357
  this.httpClients.delete(clientId);
2172
3358
  }
2173
3359
  }
3360
+ handleAwarenessRelay(sourceSessionId, message) {
3361
+ const session = this.sessions.get(sourceSessionId);
3362
+ if (!session) return;
3363
+ const transport = session.getTransport();
3364
+ if (!this.awarenessRelay.getClientCount() || !transport) {
3365
+ }
3366
+ this.awarenessRelay.addClient(sourceSessionId, message.clientId, transport);
3367
+ this.awarenessRelay.handleUpdate(sourceSessionId, message);
3368
+ }
3369
+ handleYjsDocRelay(sourceSessionId, message) {
3370
+ if (!this.sessions.has(sourceSessionId)) {
3371
+ return;
3372
+ }
3373
+ this.yjsDocRelay.handleUpdate(sourceSessionId, message);
3374
+ }
2174
3375
  getOrCreateHttpClient(clientId) {
2175
3376
  const existing = this.httpClients.get(clientId);
2176
3377
  if (existing) {
@@ -2184,6 +3385,13 @@ var KoraSyncServer = class {
2184
3385
  return client;
2185
3386
  }
2186
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
+ }
2187
3395
  function normalizeHttpBody(body, contentType) {
2188
3396
  if (body instanceof Uint8Array) {
2189
3397
  return body;
@@ -2195,6 +3403,7 @@ function normalizeHttpBody(body, contentType) {
2195
3403
  }
2196
3404
 
2197
3405
  // src/auth/no-auth.ts
3406
+ init_cjs_shims();
2198
3407
  var NoAuthProvider = class {
2199
3408
  async authenticate(_token) {
2200
3409
  return { userId: "anonymous" };
@@ -2202,6 +3411,7 @@ var NoAuthProvider = class {
2202
3411
  };
2203
3412
 
2204
3413
  // src/auth/token-auth.ts
3414
+ init_cjs_shims();
2205
3415
  var TokenAuthProvider = class {
2206
3416
  validate;
2207
3417
  constructor(options) {
@@ -2213,6 +3423,7 @@ var TokenAuthProvider = class {
2213
3423
  };
2214
3424
 
2215
3425
  // src/auth/kora-auth-provider.ts
3426
+ init_cjs_shims();
2216
3427
  var KoraAuthProvider = class {
2217
3428
  tokenValidator;
2218
3429
  userLookup;
@@ -2225,7 +3436,7 @@ var KoraAuthProvider = class {
2225
3436
  this.resolveScopes = options.resolveScopes;
2226
3437
  }
2227
3438
  async authenticate(token) {
2228
- const payload = this.tokenValidator.validateToken(token);
3439
+ const payload = this.tokenValidator.validateTokenWithRevocation ? await this.tokenValidator.validateTokenWithRevocation(token) : this.tokenValidator.validateToken(token);
2229
3440
  if (payload === null) {
2230
3441
  return null;
2231
3442
  }
@@ -2252,12 +3463,39 @@ var KoraAuthProvider = class {
2252
3463
  }
2253
3464
  };
2254
3465
 
3466
+ // src/auth/mixed-auth-provider.ts
3467
+ init_cjs_shims();
3468
+ var MixedAuthProvider = class {
3469
+ primary;
3470
+ anonymousScopes;
3471
+ anonymousPrefix;
3472
+ anonymousCounter = 0;
3473
+ constructor(options) {
3474
+ this.primary = options.primary;
3475
+ this.anonymousScopes = options.anonymousScopes;
3476
+ this.anonymousPrefix = options.anonymousPrefix ?? "anon";
3477
+ }
3478
+ async authenticate(token) {
3479
+ if (token) {
3480
+ const ctx = await this.primary.authenticate(token);
3481
+ if (ctx) return ctx;
3482
+ }
3483
+ this.anonymousCounter++;
3484
+ return {
3485
+ userId: `${this.anonymousPrefix}-${Date.now()}-${this.anonymousCounter}`,
3486
+ scopes: this.anonymousScopes
3487
+ };
3488
+ }
3489
+ };
3490
+
2255
3491
  // src/server/create-server.ts
3492
+ init_cjs_shims();
2256
3493
  function createKoraServer(config) {
2257
3494
  return new KoraSyncServer(config);
2258
3495
  }
2259
3496
 
2260
3497
  // src/server/production-server.ts
3498
+ init_cjs_shims();
2261
3499
  var MIME_TYPES = {
2262
3500
  ".html": "text/html",
2263
3501
  ".js": "text/javascript",
@@ -2278,9 +3516,143 @@ function createProductionServer(config) {
2278
3516
  const syncPath = config.syncPath ?? "/kora-sync";
2279
3517
  const syncServer = new KoraSyncServer({
2280
3518
  store: config.store,
3519
+ enableDashboard: true,
2281
3520
  ...config.syncOptions
2282
3521
  });
2283
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
+ }
2284
3656
  return {
2285
3657
  async start() {
2286
3658
  const { createServer } = await import("http");
@@ -2288,13 +3660,137 @@ function createProductionServer(config) {
2288
3660
  const { extname, join, resolve } = await import("path");
2289
3661
  const { WebSocketServer } = await import("ws");
2290
3662
  const distDir = resolve(staticDir);
2291
- httpServer = createServer((req, res) => {
3663
+ httpServer = createServer(async (req, res) => {
2292
3664
  res.setHeader("Cross-Origin-Opener-Policy", "same-origin");
2293
3665
  res.setHeader("Cross-Origin-Embedder-Policy", "require-corp");
2294
3666
  const url = new URL(req.url || "/", `http://${req.headers.host}`);
2295
3667
  if (url.pathname === "/health") {
3668
+ const status = await syncServer.getStatus();
3669
+ res.writeHead(200, { "Content-Type": "application/json" });
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();
2296
3688
  res.writeHead(200, { "Content-Type": "application/json" });
2297
- res.end(JSON.stringify({ status: "ok", timestamp: Date.now() }));
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);
2298
3794
  return;
2299
3795
  }
2300
3796
  let filePath = join(distDir, url.pathname);
@@ -2340,7 +3836,7 @@ function createProductionServer(config) {
2340
3836
  }
2341
3837
  });
2342
3838
  return new Promise((resolve2) => {
2343
- httpServer.listen(port, "0.0.0.0", () => {
3839
+ httpServer?.listen(port, "0.0.0.0", () => {
2344
3840
  resolve2(`http://localhost:${port}`);
2345
3841
  });
2346
3842
  });
@@ -2349,28 +3845,110 @@ function createProductionServer(config) {
2349
3845
  await syncServer.stop();
2350
3846
  if (httpServer) {
2351
3847
  await new Promise((resolve) => {
2352
- httpServer.close(() => resolve());
3848
+ httpServer?.close(() => resolve());
2353
3849
  });
2354
3850
  httpServer = null;
2355
3851
  }
2356
3852
  }
2357
3853
  };
2358
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
+ }
2359
3931
  // Annotate the CommonJS export names for ESM import in node:
2360
3932
  0 && (module.exports = {
3933
+ AwarenessRelay,
2361
3934
  ClientSession,
2362
3935
  HttpServerTransport,
2363
3936
  KoraAuthProvider,
2364
3937
  KoraSyncServer,
2365
3938
  MemoryServerStore,
3939
+ MixedAuthProvider,
2366
3940
  NoAuthProvider,
2367
3941
  PostgresServerStore,
2368
3942
  SqliteServerStore,
2369
3943
  TokenAuthProvider,
2370
3944
  WsServerTransport,
3945
+ createDefaultLogger,
3946
+ createJsonLogger,
2371
3947
  createKoraServer,
2372
3948
  createPostgresServerStore,
3949
+ createPrettyLogger,
2373
3950
  createProductionServer,
3951
+ createSilentLogger,
2374
3952
  createSqliteServerStore
2375
3953
  });
2376
3954
  //# sourceMappingURL=index.cjs.map