@korajs/server 0.3.3 → 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
  }
@@ -590,7 +597,7 @@ var PostgresServerStore = class {
590
597
  lastSeenAt: import_drizzle_orm.sql`${now}`
591
598
  }
592
599
  });
593
- if (this.schema && this.schema.collections[op.collection]) {
600
+ if (this.schema?.collections[op.collection]) {
594
601
  await this.rebuildMaterializedRecord(tx, op.collection, op.recordId);
595
602
  }
596
603
  });
@@ -604,10 +611,7 @@ var PostgresServerStore = class {
604
611
  this.assertOpen();
605
612
  await this.ready;
606
613
  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
- )
614
+ (0, import_drizzle_orm.and)((0, import_drizzle_orm.eq)(pgOperations.nodeId, nodeId), (0, import_drizzle_orm.between)(pgOperations.sequenceNumber, fromSeq, toSeq))
611
615
  ).orderBy((0, import_drizzle_orm.asc)(pgOperations.sequenceNumber));
612
616
  return rows.map((row) => this.deserializeOperation(row));
613
617
  }
@@ -620,7 +624,7 @@ var PostgresServerStore = class {
620
624
  async materializeCollection(collection) {
621
625
  this.assertOpen();
622
626
  await this.ready;
623
- if (this.schema && this.schema.collections[collection]) {
627
+ if (this.schema?.collections[collection]) {
624
628
  return this.queryCollection(collection);
625
629
  }
626
630
  return this.materializeFromOpsLog(collection);
@@ -630,14 +634,15 @@ var PostgresServerStore = class {
630
634
  await this.ready;
631
635
  this.assertSchema();
632
636
  this.assertCollection(collection);
633
- const collectionDef = this.schema.collections[collection];
637
+ const schema = this.schema;
638
+ const collectionDef = schema.collections[collection];
634
639
  if (options?.where) {
635
640
  for (const key of Object.keys(options.where)) {
636
- validateFieldName(collection, key, this.schema);
641
+ validateFieldName(collection, key, schema);
637
642
  }
638
643
  }
639
644
  if (options?.orderBy) {
640
- validateFieldName(collection, options.orderBy, this.schema);
645
+ validateFieldName(collection, options.orderBy, schema);
641
646
  }
642
647
  const query = this.buildSelectQuery(collection, options);
643
648
  const rows = await this.db.execute(query);
@@ -648,7 +653,8 @@ var PostgresServerStore = class {
648
653
  await this.ready;
649
654
  this.assertSchema();
650
655
  this.assertCollection(collection);
651
- const collectionDef = this.schema.collections[collection];
656
+ const schema = this.schema;
657
+ const collectionDef = schema.collections[collection];
652
658
  const query = import_drizzle_orm.sql`SELECT * FROM ${import_drizzle_orm.sql.raw(collection)} WHERE id = ${id} AND _deleted = 0`;
653
659
  const rows = await this.db.execute(query);
654
660
  if (rows.length === 0) return null;
@@ -659,9 +665,10 @@ var PostgresServerStore = class {
659
665
  await this.ready;
660
666
  this.assertSchema();
661
667
  this.assertCollection(collection);
668
+ const schema = this.schema;
662
669
  if (where) {
663
670
  for (const key of Object.keys(where)) {
664
- validateFieldName(collection, key, this.schema);
671
+ validateFieldName(collection, key, schema);
665
672
  }
666
673
  }
667
674
  const whereClause = this.buildWhereClause(where ?? {}, false);
@@ -681,18 +688,13 @@ var PostgresServerStore = class {
681
688
  * all operations for that record.
682
689
  */
683
690
  async rebuildMaterializedRecord(txOrDb, collection, recordId) {
684
- const collectionDef = this.schema.collections[collection];
691
+ const collectionDef = this.schema?.collections[collection];
685
692
  if (!collectionDef) return;
686
693
  const ops = await txOrDb.select({
687
694
  type: pgOperations.type,
688
695
  data: pgOperations.data,
689
696
  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(
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(
696
698
  (0, import_drizzle_orm.asc)(pgOperations.wallTime),
697
699
  (0, import_drizzle_orm.asc)(pgOperations.logical),
698
700
  (0, import_drizzle_orm.asc)(pgOperations.sequenceNumber)
@@ -762,7 +764,7 @@ var PostgresServerStore = class {
762
764
  * Backfill a single collection's materialized table from operations.
763
765
  */
764
766
  async backfillCollection(collectionName) {
765
- const collectionDef = this.schema.collections[collectionName];
767
+ const collectionDef = this.schema?.collections[collectionName];
766
768
  if (!collectionDef) return;
767
769
  const allOps = await this.db.select({
768
770
  recordId: pgOperations.recordId,
@@ -819,9 +821,7 @@ var PostgresServerStore = class {
819
821
  options?.where ?? {},
820
822
  options?.includeDeleted ?? false
821
823
  );
822
- const parts = [
823
- import_drizzle_orm.sql`SELECT * FROM ${import_drizzle_orm.sql.raw(collection)} WHERE ${whereClause}`
824
- ];
824
+ const parts = [import_drizzle_orm.sql`SELECT * FROM ${import_drizzle_orm.sql.raw(collection)} WHERE ${whereClause}`];
825
825
  if (options?.orderBy) {
826
826
  const dir = options.orderDirection === "desc" ? "DESC" : "ASC";
827
827
  parts.push(import_drizzle_orm.sql.raw(` ORDER BY ${options.orderBy} ${dir}`));
@@ -934,12 +934,8 @@ var PostgresServerStore = class {
934
934
  await this.db.execute(
935
935
  import_drizzle_orm.sql`CREATE INDEX IF NOT EXISTS idx_node_seq ON operations (node_id, sequence_number)`
936
936
  );
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
- );
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)`);
943
939
  await this.db.execute(
944
940
  import_drizzle_orm.sql`CREATE INDEX IF NOT EXISTS idx_collection_record ON operations (collection, record_id)`
945
941
  );
@@ -1007,9 +1003,10 @@ var PostgresServerStore = class {
1007
1003
  }
1008
1004
  }
1009
1005
  assertCollection(collection) {
1010
- if (!this.schema.collections[collection]) {
1006
+ const schema = this.schema;
1007
+ if (!schema.collections[collection]) {
1011
1008
  throw new Error(
1012
- `Unknown collection "${collection}". Available: ${Object.keys(this.schema.collections).join(", ")}`
1009
+ `Unknown collection "${collection}". Available: ${Object.keys(schema.collections).join(", ")}`
1013
1010
  );
1014
1011
  }
1015
1012
  }
@@ -1142,7 +1139,7 @@ var SqliteServerStore = class {
1142
1139
  lastSeenAt: import_drizzle_orm2.sql`${now}`
1143
1140
  }
1144
1141
  }).run();
1145
- if (this.schema && this.schema.collections[op.collection]) {
1142
+ if (this.schema?.collections[op.collection]) {
1146
1143
  this.rebuildMaterializedRecord(tx, op.collection, op.recordId);
1147
1144
  }
1148
1145
  return "applied";
@@ -1161,7 +1158,7 @@ var SqliteServerStore = class {
1161
1158
  }
1162
1159
  async materializeCollection(collection) {
1163
1160
  this.assertOpen();
1164
- if (this.schema && this.schema.collections[collection]) {
1161
+ if (this.schema?.collections[collection]) {
1165
1162
  return this.queryCollection(collection);
1166
1163
  }
1167
1164
  return this.materializeFromOpsLog(collection);
@@ -1170,14 +1167,15 @@ var SqliteServerStore = class {
1170
1167
  this.assertOpen();
1171
1168
  this.assertSchema();
1172
1169
  this.assertCollection(collection);
1173
- const collectionDef = this.schema.collections[collection];
1170
+ const schema = this.schema;
1171
+ const collectionDef = schema.collections[collection];
1174
1172
  if (options?.where) {
1175
1173
  for (const key of Object.keys(options.where)) {
1176
- validateFieldName(collection, key, this.schema);
1174
+ validateFieldName(collection, key, schema);
1177
1175
  }
1178
1176
  }
1179
1177
  if (options?.orderBy) {
1180
- validateFieldName(collection, options.orderBy, this.schema);
1178
+ validateFieldName(collection, options.orderBy, schema);
1181
1179
  }
1182
1180
  const query = this.buildSelectQuery(collection, options);
1183
1181
  const rows = this.db.all(query);
@@ -1187,7 +1185,8 @@ var SqliteServerStore = class {
1187
1185
  this.assertOpen();
1188
1186
  this.assertSchema();
1189
1187
  this.assertCollection(collection);
1190
- const collectionDef = this.schema.collections[collection];
1188
+ const schema = this.schema;
1189
+ const collectionDef = schema.collections[collection];
1191
1190
  const query = import_drizzle_orm2.sql`SELECT * FROM ${import_drizzle_orm2.sql.raw(collection)} WHERE id = ${id} AND _deleted = 0`;
1192
1191
  const rows = this.db.all(query);
1193
1192
  if (rows.length === 0) return null;
@@ -1197,9 +1196,10 @@ var SqliteServerStore = class {
1197
1196
  this.assertOpen();
1198
1197
  this.assertSchema();
1199
1198
  this.assertCollection(collection);
1199
+ const schema = this.schema;
1200
1200
  if (where) {
1201
1201
  for (const key of Object.keys(where)) {
1202
- validateFieldName(collection, key, this.schema);
1202
+ validateFieldName(collection, key, schema);
1203
1203
  }
1204
1204
  }
1205
1205
  const whereClause = this.buildWhereClause(where ?? {}, false);
@@ -1219,18 +1219,13 @@ var SqliteServerStore = class {
1219
1219
  * transaction for atomic dual-write.
1220
1220
  */
1221
1221
  rebuildMaterializedRecord(txOrDb, collection, recordId) {
1222
- const collectionDef = this.schema.collections[collection];
1222
+ const collectionDef = this.schema?.collections[collection];
1223
1223
  if (!collectionDef) return;
1224
1224
  const ops = txOrDb.select({
1225
1225
  type: operations.type,
1226
1226
  data: operations.data,
1227
1227
  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();
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();
1234
1229
  const parsedOps = ops.map((op) => ({
1235
1230
  type: op.type,
1236
1231
  data: op.data !== null ? JSON.parse(op.data) : null
@@ -1299,7 +1294,7 @@ var SqliteServerStore = class {
1299
1294
  * Backfill a single collection's materialized table from operations.
1300
1295
  */
1301
1296
  backfillCollection(collectionName) {
1302
- const collectionDef = this.schema.collections[collectionName];
1297
+ const collectionDef = this.schema?.collections[collectionName];
1303
1298
  if (!collectionDef) return;
1304
1299
  const allOps = this.db.select({
1305
1300
  recordId: operations.recordId,
@@ -1354,9 +1349,7 @@ var SqliteServerStore = class {
1354
1349
  options?.where ?? {},
1355
1350
  options?.includeDeleted ?? false
1356
1351
  );
1357
- const parts = [
1358
- import_drizzle_orm2.sql`SELECT * FROM ${import_drizzle_orm2.sql.raw(collection)} WHERE ${whereClause}`
1359
- ];
1352
+ const parts = [import_drizzle_orm2.sql`SELECT * FROM ${import_drizzle_orm2.sql.raw(collection)} WHERE ${whereClause}`];
1360
1353
  if (options?.orderBy) {
1361
1354
  const dir = options.orderDirection === "desc" ? "DESC" : "ASC";
1362
1355
  parts.push(import_drizzle_orm2.sql.raw(` ORDER BY ${options.orderBy} ${dir}`));
@@ -1531,9 +1524,10 @@ var SqliteServerStore = class {
1531
1524
  }
1532
1525
  }
1533
1526
  assertCollection(collection) {
1534
- if (!this.schema.collections[collection]) {
1527
+ const schema = this.schema;
1528
+ if (!schema.collections[collection]) {
1535
1529
  throw new Error(
1536
- `Unknown collection "${collection}". Available: ${Object.keys(this.schema.collections).join(", ")}`
1530
+ `Unknown collection "${collection}". Available: ${Object.keys(schema.collections).join(", ")}`
1537
1531
  );
1538
1532
  }
1539
1533
  }
@@ -1743,6 +1737,7 @@ var ClientSession = class {
1743
1737
  batchSize;
1744
1738
  schemaVersion;
1745
1739
  onRelay;
1740
+ onAwarenessUpdate;
1746
1741
  onClose;
1747
1742
  constructor(options) {
1748
1743
  this.sessionId = options.sessionId;
@@ -1754,6 +1749,7 @@ var ClientSession = class {
1754
1749
  this.batchSize = options.batchSize ?? DEFAULT_BATCH_SIZE;
1755
1750
  this.schemaVersion = options.schemaVersion ?? DEFAULT_SCHEMA_VERSION;
1756
1751
  this.onRelay = options.onRelay ?? null;
1752
+ this.onAwarenessUpdate = options.onAwarenessUpdate ?? null;
1757
1753
  this.onClose = options.onClose ?? null;
1758
1754
  }
1759
1755
  /**
@@ -1816,6 +1812,13 @@ var ClientSession = class {
1816
1812
  isStreaming() {
1817
1813
  return this.state === "streaming";
1818
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
+ }
1819
1822
  // --- Private protocol handlers ---
1820
1823
  handleMessage(message) {
1821
1824
  switch (message.type) {
@@ -1830,6 +1833,9 @@ var ClientSession = class {
1830
1833
  break;
1831
1834
  case "error":
1832
1835
  break;
1836
+ case "awareness-update":
1837
+ this.handleAwarenessUpdate(message);
1838
+ break;
1833
1839
  }
1834
1840
  }
1835
1841
  async handleHandshake(msg) {
@@ -1849,6 +1855,19 @@ var ClientSession = class {
1849
1855
  this.authContext = context;
1850
1856
  this.state = "authenticated";
1851
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
+ }
1852
1871
  const serverVector = this.store.getVersionVector();
1853
1872
  const selectedWireFormat = selectWireFormat(msg.supportedWireFormats);
1854
1873
  this.setSerializerWireFormat(selectedWireFormat);
@@ -1859,7 +1878,10 @@ var ClientSession = class {
1859
1878
  versionVector: (0, import_sync2.versionVectorToWire)(serverVector),
1860
1879
  schemaVersion: this.schemaVersion,
1861
1880
  accepted: true,
1862
- 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 } : {}
1863
1885
  };
1864
1886
  this.transport.send(response);
1865
1887
  this.emitter?.emit({ type: "sync:connected", nodeId: msg.nodeId });
@@ -1871,8 +1893,10 @@ var ClientSession = class {
1871
1893
  async handleOperationBatch(msg) {
1872
1894
  const operations2 = msg.operations.map((s) => this.serializer.decodeOperation(s));
1873
1895
  const applied = [];
1896
+ const rejected = [];
1874
1897
  for (const op of operations2) {
1875
1898
  if (!operationMatchesScopes(op, this.authContext?.scopes)) {
1899
+ rejected.push(op);
1876
1900
  continue;
1877
1901
  }
1878
1902
  const result = await this.store.applyRemoteOperation(op);
@@ -1880,6 +1904,15 @@ var ClientSession = class {
1880
1904
  applied.push(op);
1881
1905
  }
1882
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
+ }
1883
1916
  if (operations2.length > 0) {
1884
1917
  this.emitter?.emit({
1885
1918
  type: "sync:received",
@@ -1942,6 +1975,9 @@ var ClientSession = class {
1942
1975
  });
1943
1976
  }
1944
1977
  }
1978
+ handleAwarenessUpdate(msg) {
1979
+ this.onAwarenessUpdate?.(this.sessionId, msg);
1980
+ }
1945
1981
  sendError(code, message, retriable) {
1946
1982
  const errorMsg = {
1947
1983
  type: "error",
@@ -1972,8 +2008,107 @@ function selectWireFormat(supportedWireFormats) {
1972
2008
  }
1973
2009
 
1974
2010
  // src/server/kora-sync-server.ts
1975
- var import_core7 = require("@korajs/core");
2011
+ var import_core8 = require("@korajs/core");
1976
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
1977
2112
  var DEFAULT_MAX_CONNECTIONS = 0;
1978
2113
  var DEFAULT_BATCH_SIZE2 = 100;
1979
2114
  var DEFAULT_SCHEMA_VERSION2 = 1;
@@ -1990,6 +2125,7 @@ var KoraSyncServer = class {
1990
2125
  port;
1991
2126
  host;
1992
2127
  path;
2128
+ awarenessRelay = new AwarenessRelay();
1993
2129
  sessions = /* @__PURE__ */ new Map();
1994
2130
  httpClients = /* @__PURE__ */ new Map();
1995
2131
  httpSessionToClient = /* @__PURE__ */ new Map();
@@ -2014,10 +2150,10 @@ var KoraSyncServer = class {
2014
2150
  */
2015
2151
  async start(wsServerImpl) {
2016
2152
  if (this.running) {
2017
- 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 });
2018
2154
  }
2019
2155
  if (!wsServerImpl && this.port === void 0) {
2020
- throw new import_core7.SyncError(
2156
+ throw new import_core8.SyncError(
2021
2157
  "Port is required for standalone mode. Provide port in config or use handleConnection() for attach mode.",
2022
2158
  {}
2023
2159
  );
@@ -2051,6 +2187,7 @@ var KoraSyncServer = class {
2051
2187
  * Stop the server. Closes all sessions and the WebSocket server.
2052
2188
  */
2053
2189
  async stop() {
2190
+ this.awarenessRelay.clear();
2054
2191
  for (const session of this.sessions.values()) {
2055
2192
  session.close("server shutting down");
2056
2193
  }
@@ -2107,18 +2244,18 @@ var KoraSyncServer = class {
2107
2244
  if (this.maxConnections > 0 && this.sessions.size >= this.maxConnections) {
2108
2245
  transport.send({
2109
2246
  type: "error",
2110
- messageId: (0, import_core7.generateUUIDv7)(),
2247
+ messageId: (0, import_core8.generateUUIDv7)(),
2111
2248
  code: "MAX_CONNECTIONS",
2112
2249
  message: `Server has reached maximum connections (${this.maxConnections})`,
2113
2250
  retriable: true
2114
2251
  });
2115
2252
  transport.close(4029, "max connections reached");
2116
- throw new import_core7.SyncError("Maximum connections reached", {
2253
+ throw new import_core8.SyncError("Maximum connections reached", {
2117
2254
  current: this.sessions.size,
2118
2255
  max: this.maxConnections
2119
2256
  });
2120
2257
  }
2121
- const sessionId = (0, import_core7.generateUUIDv7)();
2258
+ const sessionId = (0, import_core8.generateUUIDv7)();
2122
2259
  const session = new ClientSession({
2123
2260
  sessionId,
2124
2261
  transport,
@@ -2131,6 +2268,9 @@ var KoraSyncServer = class {
2131
2268
  onRelay: (sourceSessionId, operations2) => {
2132
2269
  this.handleRelay(sourceSessionId, operations2);
2133
2270
  },
2271
+ onAwarenessUpdate: (sourceSessionId, message) => {
2272
+ this.handleAwarenessRelay(sourceSessionId, message);
2273
+ },
2134
2274
  onClose: (sid) => {
2135
2275
  this.handleSessionClose(sid);
2136
2276
  }
@@ -2164,6 +2304,7 @@ var KoraSyncServer = class {
2164
2304
  }
2165
2305
  }
2166
2306
  handleSessionClose(sessionId) {
2307
+ this.awarenessRelay.removeClient(sessionId);
2167
2308
  this.sessions.delete(sessionId);
2168
2309
  const clientId = this.httpSessionToClient.get(sessionId);
2169
2310
  if (clientId) {
@@ -2171,6 +2312,15 @@ var KoraSyncServer = class {
2171
2312
  this.httpClients.delete(clientId);
2172
2313
  }
2173
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
+ }
2174
2324
  getOrCreateHttpClient(clientId) {
2175
2325
  const existing = this.httpClients.get(clientId);
2176
2326
  if (existing) {
@@ -2252,6 +2402,30 @@ var KoraAuthProvider = class {
2252
2402
  }
2253
2403
  };
2254
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
+
2255
2429
  // src/server/create-server.ts
2256
2430
  function createKoraServer(config) {
2257
2431
  return new KoraSyncServer(config);
@@ -2340,7 +2514,7 @@ function createProductionServer(config) {
2340
2514
  }
2341
2515
  });
2342
2516
  return new Promise((resolve2) => {
2343
- httpServer.listen(port, "0.0.0.0", () => {
2517
+ httpServer?.listen(port, "0.0.0.0", () => {
2344
2518
  resolve2(`http://localhost:${port}`);
2345
2519
  });
2346
2520
  });
@@ -2349,7 +2523,7 @@ function createProductionServer(config) {
2349
2523
  await syncServer.stop();
2350
2524
  if (httpServer) {
2351
2525
  await new Promise((resolve) => {
2352
- httpServer.close(() => resolve());
2526
+ httpServer?.close(() => resolve());
2353
2527
  });
2354
2528
  httpServer = null;
2355
2529
  }
@@ -2358,11 +2532,13 @@ function createProductionServer(config) {
2358
2532
  }
2359
2533
  // Annotate the CommonJS export names for ESM import in node:
2360
2534
  0 && (module.exports = {
2535
+ AwarenessRelay,
2361
2536
  ClientSession,
2362
2537
  HttpServerTransport,
2363
2538
  KoraAuthProvider,
2364
2539
  KoraSyncServer,
2365
2540
  MemoryServerStore,
2541
+ MixedAuthProvider,
2366
2542
  NoAuthProvider,
2367
2543
  PostgresServerStore,
2368
2544
  SqliteServerStore,