@korajs/server 0.3.2 → 0.4.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
@@ -30,11 +30,13 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/index.ts
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
+ AwarenessRelay: () => AwarenessRelay,
33
34
  ClientSession: () => ClientSession,
34
35
  HttpServerTransport: () => HttpServerTransport,
35
36
  KoraAuthProvider: () => KoraAuthProvider,
36
37
  KoraSyncServer: () => KoraSyncServer,
37
38
  MemoryServerStore: () => MemoryServerStore,
39
+ MixedAuthProvider: () => MixedAuthProvider,
38
40
  NoAuthProvider: () => NoAuthProvider,
39
41
  PostgresServerStore: () => PostgresServerStore,
40
42
  SqliteServerStore: () => SqliteServerStore,
@@ -116,9 +118,7 @@ ALTER TABLE ${name} ADD COLUMN ${colDef}`);
116
118
  `CREATE INDEX IF NOT EXISTS idx_${name}_${indexField} ON ${name} (${indexField})`
117
119
  );
118
120
  }
119
- statements.push(
120
- `CREATE INDEX IF NOT EXISTS idx_${name}__deleted ON ${name} (_deleted)`
121
- );
121
+ statements.push(`CREATE INDEX IF NOT EXISTS idx_${name}__deleted ON ${name} (_deleted)`);
122
122
  return statements;
123
123
  }
124
124
  function generateAllCollectionDDL(schema, dialect) {
@@ -233,7 +233,7 @@ var MemoryServerStore = class {
233
233
  if (op.sequenceNumber > currentSeq) {
234
234
  this.versionVector.set(op.nodeId, op.sequenceNumber);
235
235
  }
236
- if (this.schema && this.schema.collections[op.collection]) {
236
+ if (this.schema?.collections[op.collection]) {
237
237
  this.rebuildMaterializedRecord(op.collection, op.recordId);
238
238
  }
239
239
  return "applied";
@@ -250,7 +250,7 @@ var MemoryServerStore = class {
250
250
  }
251
251
  async materializeCollection(collection) {
252
252
  this.assertOpen();
253
- if (this.schema && this.schema.collections[collection]) {
253
+ if (this.schema?.collections[collection]) {
254
254
  return this.queryCollection(collection);
255
255
  }
256
256
  return this.materializeFromOps(collection);
@@ -259,13 +259,14 @@ var MemoryServerStore = class {
259
259
  this.assertOpen();
260
260
  this.assertSchema();
261
261
  this.assertCollection(collection);
262
+ const schema = this.schema;
262
263
  if (options?.where) {
263
264
  for (const key of Object.keys(options.where)) {
264
- validateFieldName(collection, key, this.schema);
265
+ validateFieldName(collection, key, schema);
265
266
  }
266
267
  }
267
268
  if (options?.orderBy) {
268
- validateFieldName(collection, options.orderBy, this.schema);
269
+ validateFieldName(collection, options.orderBy, schema);
269
270
  }
270
271
  const collectionMap = this.materializedRecords.get(collection);
271
272
  if (!collectionMap) return [];
@@ -332,9 +333,10 @@ var MemoryServerStore = class {
332
333
  this.assertOpen();
333
334
  this.assertSchema();
334
335
  this.assertCollection(collection);
336
+ const schema = this.schema;
335
337
  if (where) {
336
338
  for (const key of Object.keys(where)) {
337
- validateFieldName(collection, key, this.schema);
339
+ validateFieldName(collection, key, schema);
338
340
  }
339
341
  }
340
342
  const collectionMap = this.materializedRecords.get(collection);
@@ -370,7 +372,7 @@ var MemoryServerStore = class {
370
372
  // Materialization internals
371
373
  // ---------------------------------------------------------------------------
372
374
  rebuildMaterializedRecord(collection, recordId) {
373
- const collectionDef = this.schema.collections[collection];
375
+ const collectionDef = this.schema?.collections[collection];
374
376
  if (!collectionDef) return;
375
377
  let collectionMap = this.materializedRecords.get(collection);
376
378
  if (!collectionMap) {
@@ -378,8 +380,10 @@ var MemoryServerStore = class {
378
380
  this.materializedRecords.set(collection, collectionMap);
379
381
  }
380
382
  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;
383
+ if (a.timestamp.wallTime !== b.timestamp.wallTime)
384
+ return a.timestamp.wallTime - b.timestamp.wallTime;
385
+ if (a.timestamp.logical !== b.timestamp.logical)
386
+ return a.timestamp.logical - b.timestamp.logical;
383
387
  return a.sequenceNumber - b.sequenceNumber;
384
388
  });
385
389
  const parsedOps = recordOps.map((op) => ({
@@ -431,8 +435,10 @@ var MemoryServerStore = class {
431
435
  // ---------------------------------------------------------------------------
432
436
  materializeFromOps(collection) {
433
437
  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;
438
+ if (a.timestamp.wallTime !== b.timestamp.wallTime)
439
+ return a.timestamp.wallTime - b.timestamp.wallTime;
440
+ if (a.timestamp.logical !== b.timestamp.logical)
441
+ return a.timestamp.logical - b.timestamp.logical;
436
442
  return a.sequenceNumber - b.sequenceNumber;
437
443
  });
438
444
  const records = /* @__PURE__ */ new Map();
@@ -478,9 +484,10 @@ var MemoryServerStore = class {
478
484
  }
479
485
  }
480
486
  assertCollection(collection) {
481
- if (!this.schema.collections[collection]) {
487
+ const schema = this.schema;
488
+ if (!schema.collections[collection]) {
482
489
  throw new Error(
483
- `Unknown collection "${collection}". Available: ${Object.keys(this.schema.collections).join(", ")}`
490
+ `Unknown collection "${collection}". Available: ${Object.keys(schema.collections).join(", ")}`
484
491
  );
485
492
  }
486
493
  }
@@ -556,7 +563,9 @@ var PostgresServerStore = class {
556
563
  try {
557
564
  await this.db.execute(import_drizzle_orm.sql.raw(alterSql));
558
565
  } catch (e) {
559
- if (!(e instanceof Error && (e.message.includes("already exists") || e.message.includes("duplicate column")))) {
566
+ const msg = e instanceof Error ? e.message : "";
567
+ const causeMsg = e instanceof Error && e.cause instanceof Error ? e.cause.message : "";
568
+ if (!msg.includes("already exists") && !msg.includes("duplicate column") && !causeMsg.includes("already exists") && !causeMsg.includes("duplicate column")) {
560
569
  throw e;
561
570
  }
562
571
  }
@@ -588,7 +597,7 @@ var PostgresServerStore = class {
588
597
  lastSeenAt: import_drizzle_orm.sql`${now}`
589
598
  }
590
599
  });
591
- if (this.schema && this.schema.collections[op.collection]) {
600
+ if (this.schema?.collections[op.collection]) {
592
601
  await this.rebuildMaterializedRecord(tx, op.collection, op.recordId);
593
602
  }
594
603
  });
@@ -602,10 +611,7 @@ var PostgresServerStore = class {
602
611
  this.assertOpen();
603
612
  await this.ready;
604
613
  const rows = await this.db.select().from(pgOperations).where(
605
- (0, import_drizzle_orm.and)(
606
- (0, import_drizzle_orm.eq)(pgOperations.nodeId, nodeId),
607
- (0, import_drizzle_orm.between)(pgOperations.sequenceNumber, fromSeq, toSeq)
608
- )
614
+ (0, import_drizzle_orm.and)((0, import_drizzle_orm.eq)(pgOperations.nodeId, nodeId), (0, import_drizzle_orm.between)(pgOperations.sequenceNumber, fromSeq, toSeq))
609
615
  ).orderBy((0, import_drizzle_orm.asc)(pgOperations.sequenceNumber));
610
616
  return rows.map((row) => this.deserializeOperation(row));
611
617
  }
@@ -618,7 +624,7 @@ var PostgresServerStore = class {
618
624
  async materializeCollection(collection) {
619
625
  this.assertOpen();
620
626
  await this.ready;
621
- if (this.schema && this.schema.collections[collection]) {
627
+ if (this.schema?.collections[collection]) {
622
628
  return this.queryCollection(collection);
623
629
  }
624
630
  return this.materializeFromOpsLog(collection);
@@ -628,14 +634,15 @@ var PostgresServerStore = class {
628
634
  await this.ready;
629
635
  this.assertSchema();
630
636
  this.assertCollection(collection);
631
- const collectionDef = this.schema.collections[collection];
637
+ const schema = this.schema;
638
+ const collectionDef = schema.collections[collection];
632
639
  if (options?.where) {
633
640
  for (const key of Object.keys(options.where)) {
634
- validateFieldName(collection, key, this.schema);
641
+ validateFieldName(collection, key, schema);
635
642
  }
636
643
  }
637
644
  if (options?.orderBy) {
638
- validateFieldName(collection, options.orderBy, this.schema);
645
+ validateFieldName(collection, options.orderBy, schema);
639
646
  }
640
647
  const query = this.buildSelectQuery(collection, options);
641
648
  const rows = await this.db.execute(query);
@@ -646,7 +653,8 @@ var PostgresServerStore = class {
646
653
  await this.ready;
647
654
  this.assertSchema();
648
655
  this.assertCollection(collection);
649
- const collectionDef = this.schema.collections[collection];
656
+ const schema = this.schema;
657
+ const collectionDef = schema.collections[collection];
650
658
  const query = import_drizzle_orm.sql`SELECT * FROM ${import_drizzle_orm.sql.raw(collection)} WHERE id = ${id} AND _deleted = 0`;
651
659
  const rows = await this.db.execute(query);
652
660
  if (rows.length === 0) return null;
@@ -657,9 +665,10 @@ var PostgresServerStore = class {
657
665
  await this.ready;
658
666
  this.assertSchema();
659
667
  this.assertCollection(collection);
668
+ const schema = this.schema;
660
669
  if (where) {
661
670
  for (const key of Object.keys(where)) {
662
- validateFieldName(collection, key, this.schema);
671
+ validateFieldName(collection, key, schema);
663
672
  }
664
673
  }
665
674
  const whereClause = this.buildWhereClause(where ?? {}, false);
@@ -679,18 +688,13 @@ var PostgresServerStore = class {
679
688
  * all operations for that record.
680
689
  */
681
690
  async rebuildMaterializedRecord(txOrDb, collection, recordId) {
682
- const collectionDef = this.schema.collections[collection];
691
+ const collectionDef = this.schema?.collections[collection];
683
692
  if (!collectionDef) return;
684
693
  const ops = await txOrDb.select({
685
694
  type: pgOperations.type,
686
695
  data: pgOperations.data,
687
696
  wallTime: pgOperations.wallTime
688
- }).from(pgOperations).where(
689
- (0, import_drizzle_orm.and)(
690
- (0, import_drizzle_orm.eq)(pgOperations.collection, collection),
691
- (0, import_drizzle_orm.eq)(pgOperations.recordId, recordId)
692
- )
693
- ).orderBy(
697
+ }).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(
694
698
  (0, import_drizzle_orm.asc)(pgOperations.wallTime),
695
699
  (0, import_drizzle_orm.asc)(pgOperations.logical),
696
700
  (0, import_drizzle_orm.asc)(pgOperations.sequenceNumber)
@@ -760,7 +764,7 @@ var PostgresServerStore = class {
760
764
  * Backfill a single collection's materialized table from operations.
761
765
  */
762
766
  async backfillCollection(collectionName) {
763
- const collectionDef = this.schema.collections[collectionName];
767
+ const collectionDef = this.schema?.collections[collectionName];
764
768
  if (!collectionDef) return;
765
769
  const allOps = await this.db.select({
766
770
  recordId: pgOperations.recordId,
@@ -817,9 +821,7 @@ var PostgresServerStore = class {
817
821
  options?.where ?? {},
818
822
  options?.includeDeleted ?? false
819
823
  );
820
- const parts = [
821
- import_drizzle_orm.sql`SELECT * FROM ${import_drizzle_orm.sql.raw(collection)} WHERE ${whereClause}`
822
- ];
824
+ const parts = [import_drizzle_orm.sql`SELECT * FROM ${import_drizzle_orm.sql.raw(collection)} WHERE ${whereClause}`];
823
825
  if (options?.orderBy) {
824
826
  const dir = options.orderDirection === "desc" ? "DESC" : "ASC";
825
827
  parts.push(import_drizzle_orm.sql.raw(` ORDER BY ${options.orderBy} ${dir}`));
@@ -932,12 +934,8 @@ var PostgresServerStore = class {
932
934
  await this.db.execute(
933
935
  import_drizzle_orm.sql`CREATE INDEX IF NOT EXISTS idx_node_seq ON operations (node_id, sequence_number)`
934
936
  );
935
- await this.db.execute(
936
- import_drizzle_orm.sql`CREATE INDEX IF NOT EXISTS idx_collection ON operations (collection)`
937
- );
938
- await this.db.execute(
939
- import_drizzle_orm.sql`CREATE INDEX IF NOT EXISTS idx_received ON operations (received_at)`
940
- );
937
+ await this.db.execute(import_drizzle_orm.sql`CREATE INDEX IF NOT EXISTS idx_collection ON operations (collection)`);
938
+ await this.db.execute(import_drizzle_orm.sql`CREATE INDEX IF NOT EXISTS idx_received ON operations (received_at)`);
941
939
  await this.db.execute(
942
940
  import_drizzle_orm.sql`CREATE INDEX IF NOT EXISTS idx_collection_record ON operations (collection, record_id)`
943
941
  );
@@ -1005,9 +1003,10 @@ var PostgresServerStore = class {
1005
1003
  }
1006
1004
  }
1007
1005
  assertCollection(collection) {
1008
- if (!this.schema.collections[collection]) {
1006
+ const schema = this.schema;
1007
+ if (!schema.collections[collection]) {
1009
1008
  throw new Error(
1010
- `Unknown collection "${collection}". Available: ${Object.keys(this.schema.collections).join(", ")}`
1009
+ `Unknown collection "${collection}". Available: ${Object.keys(schema.collections).join(", ")}`
1011
1010
  );
1012
1011
  }
1013
1012
  }
@@ -1140,7 +1139,7 @@ var SqliteServerStore = class {
1140
1139
  lastSeenAt: import_drizzle_orm2.sql`${now}`
1141
1140
  }
1142
1141
  }).run();
1143
- if (this.schema && this.schema.collections[op.collection]) {
1142
+ if (this.schema?.collections[op.collection]) {
1144
1143
  this.rebuildMaterializedRecord(tx, op.collection, op.recordId);
1145
1144
  }
1146
1145
  return "applied";
@@ -1159,7 +1158,7 @@ var SqliteServerStore = class {
1159
1158
  }
1160
1159
  async materializeCollection(collection) {
1161
1160
  this.assertOpen();
1162
- if (this.schema && this.schema.collections[collection]) {
1161
+ if (this.schema?.collections[collection]) {
1163
1162
  return this.queryCollection(collection);
1164
1163
  }
1165
1164
  return this.materializeFromOpsLog(collection);
@@ -1168,14 +1167,15 @@ var SqliteServerStore = class {
1168
1167
  this.assertOpen();
1169
1168
  this.assertSchema();
1170
1169
  this.assertCollection(collection);
1171
- const collectionDef = this.schema.collections[collection];
1170
+ const schema = this.schema;
1171
+ const collectionDef = schema.collections[collection];
1172
1172
  if (options?.where) {
1173
1173
  for (const key of Object.keys(options.where)) {
1174
- validateFieldName(collection, key, this.schema);
1174
+ validateFieldName(collection, key, schema);
1175
1175
  }
1176
1176
  }
1177
1177
  if (options?.orderBy) {
1178
- validateFieldName(collection, options.orderBy, this.schema);
1178
+ validateFieldName(collection, options.orderBy, schema);
1179
1179
  }
1180
1180
  const query = this.buildSelectQuery(collection, options);
1181
1181
  const rows = this.db.all(query);
@@ -1185,7 +1185,8 @@ var SqliteServerStore = class {
1185
1185
  this.assertOpen();
1186
1186
  this.assertSchema();
1187
1187
  this.assertCollection(collection);
1188
- const collectionDef = this.schema.collections[collection];
1188
+ const schema = this.schema;
1189
+ const collectionDef = schema.collections[collection];
1189
1190
  const query = import_drizzle_orm2.sql`SELECT * FROM ${import_drizzle_orm2.sql.raw(collection)} WHERE id = ${id} AND _deleted = 0`;
1190
1191
  const rows = this.db.all(query);
1191
1192
  if (rows.length === 0) return null;
@@ -1195,9 +1196,10 @@ var SqliteServerStore = class {
1195
1196
  this.assertOpen();
1196
1197
  this.assertSchema();
1197
1198
  this.assertCollection(collection);
1199
+ const schema = this.schema;
1198
1200
  if (where) {
1199
1201
  for (const key of Object.keys(where)) {
1200
- validateFieldName(collection, key, this.schema);
1202
+ validateFieldName(collection, key, schema);
1201
1203
  }
1202
1204
  }
1203
1205
  const whereClause = this.buildWhereClause(where ?? {}, false);
@@ -1217,18 +1219,13 @@ var SqliteServerStore = class {
1217
1219
  * transaction for atomic dual-write.
1218
1220
  */
1219
1221
  rebuildMaterializedRecord(txOrDb, collection, recordId) {
1220
- const collectionDef = this.schema.collections[collection];
1222
+ const collectionDef = this.schema?.collections[collection];
1221
1223
  if (!collectionDef) return;
1222
1224
  const ops = txOrDb.select({
1223
1225
  type: operations.type,
1224
1226
  data: operations.data,
1225
1227
  wallTime: operations.wallTime
1226
- }).from(operations).where(
1227
- (0, import_drizzle_orm2.and)(
1228
- (0, import_drizzle_orm2.eq)(operations.collection, collection),
1229
- (0, import_drizzle_orm2.eq)(operations.recordId, recordId)
1230
- )
1231
- ).orderBy((0, import_drizzle_orm2.asc)(operations.wallTime), (0, import_drizzle_orm2.asc)(operations.logical), (0, import_drizzle_orm2.asc)(operations.sequenceNumber)).all();
1228
+ }).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();
1232
1229
  const parsedOps = ops.map((op) => ({
1233
1230
  type: op.type,
1234
1231
  data: op.data !== null ? JSON.parse(op.data) : null
@@ -1297,7 +1294,7 @@ var SqliteServerStore = class {
1297
1294
  * Backfill a single collection's materialized table from operations.
1298
1295
  */
1299
1296
  backfillCollection(collectionName) {
1300
- const collectionDef = this.schema.collections[collectionName];
1297
+ const collectionDef = this.schema?.collections[collectionName];
1301
1298
  if (!collectionDef) return;
1302
1299
  const allOps = this.db.select({
1303
1300
  recordId: operations.recordId,
@@ -1352,9 +1349,7 @@ var SqliteServerStore = class {
1352
1349
  options?.where ?? {},
1353
1350
  options?.includeDeleted ?? false
1354
1351
  );
1355
- const parts = [
1356
- import_drizzle_orm2.sql`SELECT * FROM ${import_drizzle_orm2.sql.raw(collection)} WHERE ${whereClause}`
1357
- ];
1352
+ const parts = [import_drizzle_orm2.sql`SELECT * FROM ${import_drizzle_orm2.sql.raw(collection)} WHERE ${whereClause}`];
1358
1353
  if (options?.orderBy) {
1359
1354
  const dir = options.orderDirection === "desc" ? "DESC" : "ASC";
1360
1355
  parts.push(import_drizzle_orm2.sql.raw(` ORDER BY ${options.orderBy} ${dir}`));
@@ -1529,9 +1524,10 @@ var SqliteServerStore = class {
1529
1524
  }
1530
1525
  }
1531
1526
  assertCollection(collection) {
1532
- if (!this.schema.collections[collection]) {
1527
+ const schema = this.schema;
1528
+ if (!schema.collections[collection]) {
1533
1529
  throw new Error(
1534
- `Unknown collection "${collection}". Available: ${Object.keys(this.schema.collections).join(", ")}`
1530
+ `Unknown collection "${collection}". Available: ${Object.keys(schema.collections).join(", ")}`
1535
1531
  );
1536
1532
  }
1537
1533
  }
@@ -1741,6 +1737,7 @@ var ClientSession = class {
1741
1737
  batchSize;
1742
1738
  schemaVersion;
1743
1739
  onRelay;
1740
+ onAwarenessUpdate;
1744
1741
  onClose;
1745
1742
  constructor(options) {
1746
1743
  this.sessionId = options.sessionId;
@@ -1752,6 +1749,7 @@ var ClientSession = class {
1752
1749
  this.batchSize = options.batchSize ?? DEFAULT_BATCH_SIZE;
1753
1750
  this.schemaVersion = options.schemaVersion ?? DEFAULT_SCHEMA_VERSION;
1754
1751
  this.onRelay = options.onRelay ?? null;
1752
+ this.onAwarenessUpdate = options.onAwarenessUpdate ?? null;
1755
1753
  this.onClose = options.onClose ?? null;
1756
1754
  }
1757
1755
  /**
@@ -1814,6 +1812,13 @@ var ClientSession = class {
1814
1812
  isStreaming() {
1815
1813
  return this.state === "streaming";
1816
1814
  }
1815
+ /**
1816
+ * Get the transport for this session.
1817
+ * Used by the awareness relay to send messages to this client.
1818
+ */
1819
+ getTransport() {
1820
+ return this.transport;
1821
+ }
1817
1822
  // --- Private protocol handlers ---
1818
1823
  handleMessage(message) {
1819
1824
  switch (message.type) {
@@ -1828,6 +1833,9 @@ var ClientSession = class {
1828
1833
  break;
1829
1834
  case "error":
1830
1835
  break;
1836
+ case "awareness-update":
1837
+ this.handleAwarenessUpdate(message);
1838
+ break;
1831
1839
  }
1832
1840
  }
1833
1841
  async handleHandshake(msg) {
@@ -1847,6 +1855,19 @@ var ClientSession = class {
1847
1855
  this.authContext = context;
1848
1856
  this.state = "authenticated";
1849
1857
  }
1858
+ if (msg.syncScope) {
1859
+ const mergedScopes = { ...msg.syncScope };
1860
+ if (this.authContext?.scopes) {
1861
+ for (const [collection, authScope] of Object.entries(this.authContext.scopes)) {
1862
+ mergedScopes[collection] = { ...mergedScopes[collection] ?? {}, ...authScope };
1863
+ }
1864
+ }
1865
+ if (this.authContext) {
1866
+ this.authContext = { ...this.authContext, scopes: mergedScopes };
1867
+ } else {
1868
+ this.authContext = { userId: msg.nodeId, scopes: mergedScopes };
1869
+ }
1870
+ }
1850
1871
  const serverVector = this.store.getVersionVector();
1851
1872
  const selectedWireFormat = selectWireFormat(msg.supportedWireFormats);
1852
1873
  this.setSerializerWireFormat(selectedWireFormat);
@@ -1857,7 +1878,10 @@ var ClientSession = class {
1857
1878
  versionVector: (0, import_sync2.versionVectorToWire)(serverVector),
1858
1879
  schemaVersion: this.schemaVersion,
1859
1880
  accepted: true,
1860
- selectedWireFormat
1881
+ selectedWireFormat,
1882
+ // Confirm the accepted scope so the client knows what data will be synced.
1883
+ // This may differ from what the client requested if auth scopes are narrower.
1884
+ ...this.authContext?.scopes ? { acceptedScope: this.authContext.scopes } : {}
1861
1885
  };
1862
1886
  this.transport.send(response);
1863
1887
  this.emitter?.emit({ type: "sync:connected", nodeId: msg.nodeId });
@@ -1869,8 +1893,10 @@ var ClientSession = class {
1869
1893
  async handleOperationBatch(msg) {
1870
1894
  const operations2 = msg.operations.map((s) => this.serializer.decodeOperation(s));
1871
1895
  const applied = [];
1896
+ const rejected = [];
1872
1897
  for (const op of operations2) {
1873
1898
  if (!operationMatchesScopes(op, this.authContext?.scopes)) {
1899
+ rejected.push(op);
1874
1900
  continue;
1875
1901
  }
1876
1902
  const result = await this.store.applyRemoteOperation(op);
@@ -1878,6 +1904,15 @@ var ClientSession = class {
1878
1904
  applied.push(op);
1879
1905
  }
1880
1906
  }
1907
+ if (rejected.length > 0) {
1908
+ for (const op of rejected) {
1909
+ this.sendError(
1910
+ "SCOPE_VIOLATION",
1911
+ `Operation "${op.id}" in collection "${op.collection}" is outside the client's sync scope`,
1912
+ false
1913
+ );
1914
+ }
1915
+ }
1881
1916
  if (operations2.length > 0) {
1882
1917
  this.emitter?.emit({
1883
1918
  type: "sync:received",
@@ -1940,6 +1975,9 @@ var ClientSession = class {
1940
1975
  });
1941
1976
  }
1942
1977
  }
1978
+ handleAwarenessUpdate(msg) {
1979
+ this.onAwarenessUpdate?.(this.sessionId, msg);
1980
+ }
1943
1981
  sendError(code, message, retriable) {
1944
1982
  const errorMsg = {
1945
1983
  type: "error",
@@ -1970,8 +2008,107 @@ function selectWireFormat(supportedWireFormats) {
1970
2008
  }
1971
2009
 
1972
2010
  // src/server/kora-sync-server.ts
1973
- var import_core7 = require("@korajs/core");
2011
+ var import_core8 = require("@korajs/core");
1974
2012
  var import_sync3 = require("@korajs/sync");
2013
+
2014
+ // src/awareness/awareness-relay.ts
2015
+ var import_core7 = require("@korajs/core");
2016
+ var AwarenessRelay = class {
2017
+ clients = /* @__PURE__ */ new Map();
2018
+ /**
2019
+ * Register a client for awareness broadcasting.
2020
+ *
2021
+ * @param sessionId - Unique session identifier
2022
+ * @param clientId - Client-assigned awareness ID
2023
+ * @param transport - Transport for sending messages to this client
2024
+ */
2025
+ addClient(sessionId, clientId, transport) {
2026
+ this.clients.set(sessionId, {
2027
+ sessionId,
2028
+ clientId,
2029
+ transport,
2030
+ state: null
2031
+ });
2032
+ const existingStates = {};
2033
+ let hasStates = false;
2034
+ for (const [, client] of this.clients) {
2035
+ if (client.sessionId === sessionId) continue;
2036
+ if (client.state) {
2037
+ existingStates[String(client.clientId)] = client.state;
2038
+ hasStates = true;
2039
+ }
2040
+ }
2041
+ if (hasStates) {
2042
+ const catchUpMsg = {
2043
+ type: "awareness-update",
2044
+ messageId: (0, import_core7.generateUUIDv7)(),
2045
+ clientId: 0,
2046
+ // Server-sourced
2047
+ states: existingStates
2048
+ };
2049
+ transport.send(catchUpMsg);
2050
+ }
2051
+ }
2052
+ /**
2053
+ * Remove a client and broadcast its removal to all remaining clients.
2054
+ *
2055
+ * @param sessionId - Session ID of the disconnecting client
2056
+ */
2057
+ removeClient(sessionId) {
2058
+ const client = this.clients.get(sessionId);
2059
+ if (!client) return;
2060
+ this.clients.delete(sessionId);
2061
+ if (client.state === null) return;
2062
+ const removalStates = {
2063
+ [String(client.clientId)]: null
2064
+ };
2065
+ const msg = {
2066
+ type: "awareness-update",
2067
+ messageId: (0, import_core7.generateUUIDv7)(),
2068
+ clientId: client.clientId,
2069
+ states: removalStates
2070
+ };
2071
+ this.broadcastExcept(sessionId, msg);
2072
+ }
2073
+ /**
2074
+ * Handle an incoming awareness update from a client.
2075
+ * Stores the state and relays to all other connected clients.
2076
+ *
2077
+ * @param sessionId - Session ID of the sending client
2078
+ * @param message - The awareness update message
2079
+ */
2080
+ handleUpdate(sessionId, message) {
2081
+ const sender = this.clients.get(sessionId);
2082
+ if (!sender) return;
2083
+ const senderState = message.states[String(message.clientId)];
2084
+ if (senderState !== void 0) {
2085
+ sender.state = senderState;
2086
+ }
2087
+ this.broadcastExcept(sessionId, message);
2088
+ }
2089
+ /**
2090
+ * Get the number of registered awareness clients.
2091
+ */
2092
+ getClientCount() {
2093
+ return this.clients.size;
2094
+ }
2095
+ /**
2096
+ * Remove all clients and clear all state.
2097
+ */
2098
+ clear() {
2099
+ this.clients.clear();
2100
+ }
2101
+ // --- Private ---
2102
+ broadcastExcept(excludeSessionId, message) {
2103
+ for (const [, client] of this.clients) {
2104
+ if (client.sessionId === excludeSessionId) continue;
2105
+ if (!client.transport.isConnected()) continue;
2106
+ client.transport.send(message);
2107
+ }
2108
+ }
2109
+ };
2110
+
2111
+ // src/server/kora-sync-server.ts
1975
2112
  var DEFAULT_MAX_CONNECTIONS = 0;
1976
2113
  var DEFAULT_BATCH_SIZE2 = 100;
1977
2114
  var DEFAULT_SCHEMA_VERSION2 = 1;
@@ -1988,6 +2125,7 @@ var KoraSyncServer = class {
1988
2125
  port;
1989
2126
  host;
1990
2127
  path;
2128
+ awarenessRelay = new AwarenessRelay();
1991
2129
  sessions = /* @__PURE__ */ new Map();
1992
2130
  httpClients = /* @__PURE__ */ new Map();
1993
2131
  httpSessionToClient = /* @__PURE__ */ new Map();
@@ -2012,10 +2150,10 @@ var KoraSyncServer = class {
2012
2150
  */
2013
2151
  async start(wsServerImpl) {
2014
2152
  if (this.running) {
2015
- throw new import_core7.SyncError("Server is already running", { port: this.port });
2153
+ throw new import_core8.SyncError("Server is already running", { port: this.port });
2016
2154
  }
2017
2155
  if (!wsServerImpl && this.port === void 0) {
2018
- throw new import_core7.SyncError(
2156
+ throw new import_core8.SyncError(
2019
2157
  "Port is required for standalone mode. Provide port in config or use handleConnection() for attach mode.",
2020
2158
  {}
2021
2159
  );
@@ -2049,6 +2187,7 @@ var KoraSyncServer = class {
2049
2187
  * Stop the server. Closes all sessions and the WebSocket server.
2050
2188
  */
2051
2189
  async stop() {
2190
+ this.awarenessRelay.clear();
2052
2191
  for (const session of this.sessions.values()) {
2053
2192
  session.close("server shutting down");
2054
2193
  }
@@ -2105,18 +2244,18 @@ var KoraSyncServer = class {
2105
2244
  if (this.maxConnections > 0 && this.sessions.size >= this.maxConnections) {
2106
2245
  transport.send({
2107
2246
  type: "error",
2108
- messageId: (0, import_core7.generateUUIDv7)(),
2247
+ messageId: (0, import_core8.generateUUIDv7)(),
2109
2248
  code: "MAX_CONNECTIONS",
2110
2249
  message: `Server has reached maximum connections (${this.maxConnections})`,
2111
2250
  retriable: true
2112
2251
  });
2113
2252
  transport.close(4029, "max connections reached");
2114
- throw new import_core7.SyncError("Maximum connections reached", {
2253
+ throw new import_core8.SyncError("Maximum connections reached", {
2115
2254
  current: this.sessions.size,
2116
2255
  max: this.maxConnections
2117
2256
  });
2118
2257
  }
2119
- const sessionId = (0, import_core7.generateUUIDv7)();
2258
+ const sessionId = (0, import_core8.generateUUIDv7)();
2120
2259
  const session = new ClientSession({
2121
2260
  sessionId,
2122
2261
  transport,
@@ -2129,6 +2268,9 @@ var KoraSyncServer = class {
2129
2268
  onRelay: (sourceSessionId, operations2) => {
2130
2269
  this.handleRelay(sourceSessionId, operations2);
2131
2270
  },
2271
+ onAwarenessUpdate: (sourceSessionId, message) => {
2272
+ this.handleAwarenessRelay(sourceSessionId, message);
2273
+ },
2132
2274
  onClose: (sid) => {
2133
2275
  this.handleSessionClose(sid);
2134
2276
  }
@@ -2162,6 +2304,7 @@ var KoraSyncServer = class {
2162
2304
  }
2163
2305
  }
2164
2306
  handleSessionClose(sessionId) {
2307
+ this.awarenessRelay.removeClient(sessionId);
2165
2308
  this.sessions.delete(sessionId);
2166
2309
  const clientId = this.httpSessionToClient.get(sessionId);
2167
2310
  if (clientId) {
@@ -2169,6 +2312,15 @@ var KoraSyncServer = class {
2169
2312
  this.httpClients.delete(clientId);
2170
2313
  }
2171
2314
  }
2315
+ handleAwarenessRelay(sourceSessionId, message) {
2316
+ const session = this.sessions.get(sourceSessionId);
2317
+ if (!session) return;
2318
+ const transport = session.getTransport();
2319
+ if (!this.awarenessRelay.getClientCount() || !transport) {
2320
+ }
2321
+ this.awarenessRelay.addClient(sourceSessionId, message.clientId, transport);
2322
+ this.awarenessRelay.handleUpdate(sourceSessionId, message);
2323
+ }
2172
2324
  getOrCreateHttpClient(clientId) {
2173
2325
  const existing = this.httpClients.get(clientId);
2174
2326
  if (existing) {
@@ -2250,6 +2402,30 @@ var KoraAuthProvider = class {
2250
2402
  }
2251
2403
  };
2252
2404
 
2405
+ // src/auth/mixed-auth-provider.ts
2406
+ var MixedAuthProvider = class {
2407
+ primary;
2408
+ anonymousScopes;
2409
+ anonymousPrefix;
2410
+ anonymousCounter = 0;
2411
+ constructor(options) {
2412
+ this.primary = options.primary;
2413
+ this.anonymousScopes = options.anonymousScopes;
2414
+ this.anonymousPrefix = options.anonymousPrefix ?? "anon";
2415
+ }
2416
+ async authenticate(token) {
2417
+ if (token) {
2418
+ const ctx = await this.primary.authenticate(token);
2419
+ if (ctx) return ctx;
2420
+ }
2421
+ this.anonymousCounter++;
2422
+ return {
2423
+ userId: `${this.anonymousPrefix}-${Date.now()}-${this.anonymousCounter}`,
2424
+ scopes: this.anonymousScopes
2425
+ };
2426
+ }
2427
+ };
2428
+
2253
2429
  // src/server/create-server.ts
2254
2430
  function createKoraServer(config) {
2255
2431
  return new KoraSyncServer(config);
@@ -2338,7 +2514,7 @@ function createProductionServer(config) {
2338
2514
  }
2339
2515
  });
2340
2516
  return new Promise((resolve2) => {
2341
- httpServer.listen(port, "0.0.0.0", () => {
2517
+ httpServer?.listen(port, "0.0.0.0", () => {
2342
2518
  resolve2(`http://localhost:${port}`);
2343
2519
  });
2344
2520
  });
@@ -2347,7 +2523,7 @@ function createProductionServer(config) {
2347
2523
  await syncServer.stop();
2348
2524
  if (httpServer) {
2349
2525
  await new Promise((resolve) => {
2350
- httpServer.close(() => resolve());
2526
+ httpServer?.close(() => resolve());
2351
2527
  });
2352
2528
  httpServer = null;
2353
2529
  }
@@ -2356,11 +2532,13 @@ function createProductionServer(config) {
2356
2532
  }
2357
2533
  // Annotate the CommonJS export names for ESM import in node:
2358
2534
  0 && (module.exports = {
2535
+ AwarenessRelay,
2359
2536
  ClientSession,
2360
2537
  HttpServerTransport,
2361
2538
  KoraAuthProvider,
2362
2539
  KoraSyncServer,
2363
2540
  MemoryServerStore,
2541
+ MixedAuthProvider,
2364
2542
  NoAuthProvider,
2365
2543
  PostgresServerStore,
2366
2544
  SqliteServerStore,