@korajs/server 0.6.1 → 1.0.0-beta.1

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
@@ -250,10 +250,11 @@ function createDefaultLogger() {
250
250
 
251
251
  // src/store/memory-server-store.ts
252
252
  init_cjs_shims();
253
- var import_core = require("@korajs/core");
253
+ var import_core2 = require("@korajs/core");
254
254
 
255
255
  // src/store/materialization.ts
256
256
  init_cjs_shims();
257
+ var import_core = require("@korajs/core");
257
258
  function fieldTypeToSql(descriptor, dialect) {
258
259
  switch (descriptor.kind) {
259
260
  case "string":
@@ -349,6 +350,52 @@ function replayOperationsForRecord(ops) {
349
350
  }
350
351
  return deleted ? null : record;
351
352
  }
353
+ var BASE64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
354
+ var BASE64_REVERSE = new Map(
355
+ Array.from(BASE64_ALPHABET, (char, index3) => [char, index3])
356
+ );
357
+ function base64ToBytes(base64) {
358
+ const cleaned = base64.replace(/=+$/, "");
359
+ const out = new Uint8Array(Math.floor(cleaned.length * 6 / 8));
360
+ let buffer = 0;
361
+ let bits = 0;
362
+ let index3 = 0;
363
+ for (const char of cleaned) {
364
+ const value = BASE64_REVERSE.get(char);
365
+ if (value === void 0) {
366
+ throw new import_core.StorageError(`Invalid base64 character "${char}" in tagged richtext value`, {
367
+ char
368
+ });
369
+ }
370
+ buffer = buffer << 6 | value;
371
+ bits += 6;
372
+ if (bits >= 8) {
373
+ bits -= 8;
374
+ out[index3] = buffer >> bits & 255;
375
+ index3 += 1;
376
+ }
377
+ }
378
+ return out;
379
+ }
380
+ function decodeRichtextColumnValue(value) {
381
+ if (typeof value !== "object" || value === null || ArrayBuffer.isView(value)) {
382
+ return value;
383
+ }
384
+ const record = value;
385
+ if (Object.keys(record).length === 1 && typeof record.$koraBytes === "string") {
386
+ return base64ToBytes(record.$koraBytes);
387
+ }
388
+ const keys = Object.keys(record);
389
+ if (keys.length > 0 && keys.every((k, i) => k === String(i) && typeof record[k] === "number")) {
390
+ const bytes = new Uint8Array(keys.length);
391
+ for (let i = 0; i < keys.length; i++) {
392
+ const byte = record[String(i)];
393
+ bytes[i] = typeof byte === "number" ? byte & 255 : 0;
394
+ }
395
+ return bytes;
396
+ }
397
+ return value;
398
+ }
352
399
  function serializeFieldValue(value, descriptor) {
353
400
  if (value === null || value === void 0) return null;
354
401
  switch (descriptor.kind) {
@@ -356,6 +403,8 @@ function serializeFieldValue(value, descriptor) {
356
403
  return typeof value === "string" ? value : JSON.stringify(value);
357
404
  case "boolean":
358
405
  return value ? 1 : 0;
406
+ case "richtext":
407
+ return decodeRichtextColumnValue(value);
359
408
  default:
360
409
  return value;
361
410
  }
@@ -401,7 +450,7 @@ var MemoryServerStore = class {
401
450
  materializedRecords = /* @__PURE__ */ new Map();
402
451
  closed = false;
403
452
  constructor(nodeId) {
404
- this.nodeId = nodeId ?? (0, import_core.generateUUIDv7)();
453
+ this.nodeId = nodeId ?? (0, import_core2.generateUUIDv7)();
405
454
  }
406
455
  getVersionVector() {
407
456
  return new Map(this.versionVector);
@@ -738,7 +787,7 @@ var MemoryServerStore = class {
738
787
 
739
788
  // src/store/postgres-server-store.ts
740
789
  init_cjs_shims();
741
- var import_core2 = require("@korajs/core");
790
+ var import_core3 = require("@korajs/core");
742
791
  var import_drizzle_orm = require("drizzle-orm");
743
792
 
744
793
  // src/store/drizzle-pg-schema.ts
@@ -787,7 +836,7 @@ var PostgresServerStore = class {
787
836
  closed = false;
788
837
  constructor(db, nodeId) {
789
838
  this.db = db;
790
- this.nodeId = nodeId ?? (0, import_core2.generateUUIDv7)();
839
+ this.nodeId = nodeId ?? (0, import_core3.generateUUIDv7)();
791
840
  this.ready = this.initialize();
792
841
  }
793
842
  getVersionVector() {
@@ -1323,7 +1372,7 @@ async function loadPostgresDeps() {
1323
1372
  // src/store/sqlite-server-store.ts
1324
1373
  init_cjs_shims();
1325
1374
  var import_node_module = require("module");
1326
- var import_core3 = require("@korajs/core");
1375
+ var import_core4 = require("@korajs/core");
1327
1376
  var import_drizzle_orm2 = require("drizzle-orm");
1328
1377
 
1329
1378
  // src/store/drizzle-schema.ts
@@ -1371,7 +1420,7 @@ var SqliteServerStore = class {
1371
1420
  closed = false;
1372
1421
  constructor(db, nodeId) {
1373
1422
  this.db = db;
1374
- this.nodeId = nodeId ?? (0, import_core3.generateUUIDv7)();
1423
+ this.nodeId = nodeId ?? (0, import_core4.generateUUIDv7)();
1375
1424
  this.ensureTables();
1376
1425
  }
1377
1426
  getVersionVector() {
@@ -1869,7 +1918,7 @@ function createSqliteServerStore(options) {
1869
1918
 
1870
1919
  // src/transport/http-server-transport.ts
1871
1920
  init_cjs_shims();
1872
- var import_core4 = require("@korajs/core");
1921
+ var import_core5 = require("@korajs/core");
1873
1922
  var HttpServerTransport = class {
1874
1923
  serializer;
1875
1924
  messageHandler = null;
@@ -1911,7 +1960,7 @@ var HttpServerTransport = class {
1911
1960
  }
1912
1961
  receive(payload) {
1913
1962
  if (!this.connected) {
1914
- throw new import_core4.SyncError("HTTP server transport is closed");
1963
+ throw new import_core5.SyncError("HTTP server transport is closed");
1915
1964
  }
1916
1965
  try {
1917
1966
  const message = this.serializer.decode(payload);
@@ -1951,7 +2000,7 @@ var HttpServerTransport = class {
1951
2000
 
1952
2001
  // src/transport/ws-server-transport.ts
1953
2002
  init_cjs_shims();
1954
- var import_core5 = require("@korajs/core");
2003
+ var import_core6 = require("@korajs/core");
1955
2004
  var import_sync = require("@korajs/sync");
1956
2005
  var WS_OPEN = 1;
1957
2006
  var WsServerTransport = class {
@@ -1967,7 +2016,7 @@ var WsServerTransport = class {
1967
2016
  }
1968
2017
  send(message) {
1969
2018
  if (this.ws.readyState !== WS_OPEN) {
1970
- throw new import_core5.SyncError("Cannot send message: WebSocket is not open", {
2019
+ throw new import_core6.SyncError("Cannot send message: WebSocket is not open", {
1971
2020
  readyState: this.ws.readyState,
1972
2021
  messageType: message.type
1973
2022
  });
@@ -1994,7 +2043,7 @@ var WsServerTransport = class {
1994
2043
  this.ws.on("message", (data) => {
1995
2044
  try {
1996
2045
  if (typeof data !== "string" && !(data instanceof Uint8Array) && !(data instanceof ArrayBuffer)) {
1997
- throw new import_core5.SyncError("Unsupported WebSocket payload type", {
2046
+ throw new import_core6.SyncError("Unsupported WebSocket payload type", {
1998
2047
  payloadType: typeof data
1999
2048
  });
2000
2049
  }
@@ -2015,9 +2064,10 @@ var WsServerTransport = class {
2015
2064
 
2016
2065
  // src/session/client-session.ts
2017
2066
  init_cjs_shims();
2018
- var import_core8 = require("@korajs/core");
2067
+ var import_core10 = require("@korajs/core");
2019
2068
  var import_internal = require("@korajs/core/internal");
2020
2069
  var import_sync2 = require("@korajs/sync");
2070
+ var import_sync3 = require("@korajs/sync");
2021
2071
 
2022
2072
  // src/apply/apply-server-operation.ts
2023
2073
  init_cjs_shims();
@@ -2107,12 +2157,12 @@ function createServerReferentialContext(store) {
2107
2157
 
2108
2158
  // src/apply/server-side-effect-operation.ts
2109
2159
  init_cjs_shims();
2110
- var import_core6 = require("@korajs/core");
2160
+ var import_core7 = require("@korajs/core");
2111
2161
  async function createServerSideEffectOperation(store, parentOp, effect, schemaVersion, sequenceNumber) {
2112
2162
  const nodeId = store.getNodeId();
2113
- const clock = new import_core6.HybridLogicalClock(nodeId);
2163
+ const clock = new import_core7.HybridLogicalClock(nodeId);
2114
2164
  clock.receive(parentOp.timestamp);
2115
- return (0, import_core6.createOperation)(
2165
+ return (0, import_core7.createOperation)(
2116
2166
  {
2117
2167
  nodeId,
2118
2168
  type: effect.type === "delete" ? "delete" : "update",
@@ -2190,21 +2240,29 @@ async function applyServerOperation(store, op, relationLookup) {
2190
2240
  };
2191
2241
  }
2192
2242
 
2243
+ // src/auth/no-auth.ts
2244
+ init_cjs_shims();
2245
+ var NoAuthProvider = class {
2246
+ async authenticate(_token) {
2247
+ return { userId: "anonymous" };
2248
+ }
2249
+ };
2250
+
2193
2251
  // src/scopes/resolve-session-scopes.ts
2194
2252
  init_cjs_shims();
2195
- var import_core7 = require("@korajs/core");
2253
+ var import_core8 = require("@korajs/core");
2196
2254
  function resolveSessionScopes(schema, options) {
2197
2255
  const { handshakeScope, authScopes, scopeValues } = options;
2198
2256
  let resolved;
2199
2257
  if (schema && scopeValues && Object.keys(scopeValues).length > 0) {
2200
- resolved = (0, import_core7.buildScopeMap)(schema, scopeValues);
2258
+ resolved = (0, import_core8.buildScopeMap)(schema, scopeValues);
2201
2259
  } else if (handshakeScope) {
2202
2260
  resolved = { ...handshakeScope };
2203
2261
  } else if (authScopes) {
2204
2262
  resolved = { ...authScopes };
2205
2263
  }
2206
2264
  if (schema && !resolved && scopeValues) {
2207
- resolved = (0, import_core7.buildScopeMap)(schema, scopeValues);
2265
+ resolved = (0, import_core8.buildScopeMap)(schema, scopeValues);
2208
2266
  }
2209
2267
  if (authScopes) {
2210
2268
  resolved = mergeScopeMaps(resolved, authScopes, "auth");
@@ -2259,9 +2317,17 @@ function asRecord(value) {
2259
2317
 
2260
2318
  // src/session/operation-validation.ts
2261
2319
  init_cjs_shims();
2320
+ var import_core9 = require("@korajs/core");
2262
2321
  var SERVER_MAX_TIMESTAMP_FUTURE_MS = 6e4;
2263
2322
  function isOperationTimestampValid(op, now = Date.now()) {
2264
- return op.timestamp.wallTime <= now + SERVER_MAX_TIMESTAMP_FUTURE_MS;
2323
+ const { wallTime, logical } = op.timestamp;
2324
+ if (!Number.isInteger(wallTime) || wallTime < 0) {
2325
+ return false;
2326
+ }
2327
+ if (!Number.isInteger(logical) || logical < 0 || logical > import_core9.MAX_LOGICAL) {
2328
+ return false;
2329
+ }
2330
+ return wallTime <= now + SERVER_MAX_TIMESTAMP_FUTURE_MS;
2265
2331
  }
2266
2332
 
2267
2333
  // src/session/session-operation-limits.ts
@@ -2311,6 +2377,25 @@ var SessionRateLimiter = class {
2311
2377
  // src/session/client-session.ts
2312
2378
  var DEFAULT_BATCH_SIZE = 100;
2313
2379
  var DEFAULT_SCHEMA_VERSION = 1;
2380
+ var warnedUnscopedProviders = /* @__PURE__ */ new WeakSet();
2381
+ function warnIfMultiTenantWithoutScopes(auth, resolvedScopes, schema) {
2382
+ if (!auth || auth instanceof NoAuthProvider) {
2383
+ return;
2384
+ }
2385
+ if (resolvedScopes) {
2386
+ return;
2387
+ }
2388
+ if (!schema || Object.keys(schema.collections).length === 0) {
2389
+ return;
2390
+ }
2391
+ if (warnedUnscopedProviders.has(auth)) {
2392
+ return;
2393
+ }
2394
+ warnedUnscopedProviders.add(auth);
2395
+ console.warn(
2396
+ "[kora] An authenticated session resolved to no sync scopes, so every user will sync every other user's data. Return per-user sync scopes from your auth provider (for example KoraAuthProvider's resolveScopes) to isolate tenants. Note: declaring sync rules in your schema is not enough on its own \u2014 the per-user values come from the auth provider. (This warning is expected for single-tenant apps where all authenticated users are meant to share the same data.)"
2397
+ );
2398
+ }
2314
2399
  var ClientSession = class {
2315
2400
  state = "connected";
2316
2401
  clientNodeId = null;
@@ -2329,6 +2414,9 @@ var ClientSession = class {
2329
2414
  onRelay;
2330
2415
  onAwarenessUpdate;
2331
2416
  onYjsDocUpdate;
2417
+ onBlobChunkRequest;
2418
+ onBlobChunkResponse;
2419
+ persistBlobChunk;
2332
2420
  onClose;
2333
2421
  maxOperationBytes;
2334
2422
  maxOpsPerMinute;
@@ -2338,7 +2426,7 @@ var ClientSession = class {
2338
2426
  this.transport = options.transport;
2339
2427
  this.store = options.store;
2340
2428
  this.auth = options.auth ?? null;
2341
- this.serializer = options.serializer ?? new import_sync2.NegotiatedMessageSerializer("json");
2429
+ this.serializer = options.serializer ?? new import_sync3.NegotiatedMessageSerializer("json");
2342
2430
  this.emitter = options.emitter ?? null;
2343
2431
  this.batchSize = options.batchSize ?? DEFAULT_BATCH_SIZE;
2344
2432
  this.schemaVersion = options.schemaVersion ?? DEFAULT_SCHEMA_VERSION;
@@ -2350,6 +2438,9 @@ var ClientSession = class {
2350
2438
  this.onRelay = options.onRelay ?? null;
2351
2439
  this.onAwarenessUpdate = options.onAwarenessUpdate ?? null;
2352
2440
  this.onYjsDocUpdate = options.onYjsDocUpdate ?? null;
2441
+ this.onBlobChunkRequest = options.onBlobChunkRequest ?? null;
2442
+ this.onBlobChunkResponse = options.onBlobChunkResponse ?? null;
2443
+ this.persistBlobChunk = options.persistBlobChunk ?? null;
2353
2444
  this.onClose = options.onClose ?? null;
2354
2445
  this.maxOperationBytes = options.maxOperationBytes ?? DEFAULT_MAX_OPERATION_BYTES;
2355
2446
  this.maxOpsPerMinute = options.maxOpsPerMinute ?? DEFAULT_MAX_OPS_PER_MINUTE;
@@ -2379,7 +2470,7 @@ var ClientSession = class {
2379
2470
  const serializedOps = visibleOperations.map((op) => this.serializer.encodeOperation(op));
2380
2471
  const msg = {
2381
2472
  type: "operation-batch",
2382
- messageId: (0, import_core8.generateUUIDv7)(),
2473
+ messageId: (0, import_core10.generateUUIDv7)(),
2383
2474
  operations: serializedOps,
2384
2475
  isFinal: true,
2385
2476
  batchIndex: 0
@@ -2455,7 +2546,32 @@ var ClientSession = class {
2455
2546
  case "yjs-doc-update":
2456
2547
  this.handleYjsDocUpdate(message);
2457
2548
  break;
2549
+ case "blob-chunk-request":
2550
+ this.onBlobChunkRequest?.(this.sessionId, message);
2551
+ break;
2552
+ case "blob-chunk-response":
2553
+ this.onBlobChunkResponse?.(this.sessionId, message);
2554
+ break;
2555
+ case "blob-chunk-push":
2556
+ await this.handleBlobChunkPush(message);
2557
+ break;
2558
+ }
2559
+ }
2560
+ /**
2561
+ * Persist a client-uploaded blob chunk (or manifest) centrally. The bytes are
2562
+ * verified to hash to the declared hash before storing, so a corrupt or
2563
+ * mislabeled upload is rejected rather than served later as trusted content.
2564
+ */
2565
+ async handleBlobChunkPush(message) {
2566
+ if (!this.persistBlobChunk) {
2567
+ return;
2568
+ }
2569
+ const bytes = (0, import_sync2.decodeBlobChunkBytes)(message.bytes);
2570
+ const actual = await (0, import_core10.hashBlob)(bytes);
2571
+ if (actual !== message.hash) {
2572
+ return;
2458
2573
  }
2574
+ await this.persistBlobChunk(message.hash, bytes);
2459
2575
  }
2460
2576
  handleMessageFailure(error) {
2461
2577
  const reason = error instanceof Error ? error.message : "Message handling failed";
@@ -2490,27 +2606,29 @@ var ClientSession = class {
2490
2606
  this.authContext = { userId: msg.nodeId, scopes: resolvedScopes };
2491
2607
  }
2492
2608
  }
2609
+ warnIfMultiTenantWithoutScopes(this.auth, resolvedScopes, this.store.getSchema());
2493
2610
  if (msg.syncQueries && msg.syncQueries.length > 0) {
2494
- this.syncQuerySubsets = (0, import_sync2.dedupeQuerySubsets)(msg.syncQueries);
2611
+ this.syncQuerySubsets = (0, import_sync3.dedupeQuerySubsets)(msg.syncQueries);
2495
2612
  } else {
2496
2613
  this.syncQuerySubsets = [];
2497
2614
  }
2498
- this.resumeDeltaCursor = msg.deltaCursor ? (0, import_sync2.decodeDeltaCursor)(msg.deltaCursor) : null;
2615
+ this.resumeDeltaCursor = msg.deltaCursor ? (0, import_sync3.decodeDeltaCursor)(msg.deltaCursor) : null;
2499
2616
  const serverVector = this.store.getVersionVector();
2500
2617
  const selectedWireFormat = selectWireFormat(msg.supportedWireFormats);
2501
2618
  this.setSerializerWireFormat(selectedWireFormat);
2502
- if (!(0, import_sync2.isClientSchemaVersionSupported)(msg.schemaVersion, this.supportedSchemaVersions)) {
2619
+ if (!(0, import_sync3.isClientSchemaVersionSupported)(msg.schemaVersion, this.supportedSchemaVersions)) {
2503
2620
  const { min, max } = this.supportedSchemaVersions;
2504
2621
  const response2 = {
2505
2622
  type: "handshake-response",
2506
- messageId: (0, import_core8.generateUUIDv7)(),
2623
+ messageId: (0, import_core10.generateUUIDv7)(),
2507
2624
  nodeId: this.store.getNodeId(),
2508
- versionVector: (0, import_sync2.versionVectorToWire)(serverVector),
2625
+ versionVector: (0, import_sync3.versionVectorToWire)(serverVector),
2509
2626
  schemaVersion: this.schemaVersion,
2510
2627
  accepted: false,
2511
- rejectReason: `${import_sync2.SCHEMA_MISMATCH_PREFIX}: client schema version ${msg.schemaVersion} not in supported range [${min}, ${max}]`,
2628
+ rejectReason: `${import_sync3.SCHEMA_MISMATCH_PREFIX}: client schema version ${msg.schemaVersion} not in supported range [${min}, ${max}]`,
2512
2629
  supportedSchemaMin: min,
2513
- supportedSchemaMax: max
2630
+ supportedSchemaMax: max,
2631
+ serverTime: Date.now()
2514
2632
  };
2515
2633
  this.sendToClient(response2);
2516
2634
  this.close("schema version mismatch");
@@ -2518,12 +2636,16 @@ var ClientSession = class {
2518
2636
  }
2519
2637
  const response = {
2520
2638
  type: "handshake-response",
2521
- messageId: (0, import_core8.generateUUIDv7)(),
2639
+ messageId: (0, import_core10.generateUUIDv7)(),
2522
2640
  nodeId: this.store.getNodeId(),
2523
- versionVector: (0, import_sync2.versionVectorToWire)(serverVector),
2641
+ versionVector: (0, import_sync3.versionVectorToWire)(serverVector),
2524
2642
  schemaVersion: this.schemaVersion,
2525
2643
  accepted: true,
2526
2644
  selectedWireFormat,
2645
+ serverTime: Date.now(),
2646
+ // Advertise central blob storage so the client uploads the bytes behind
2647
+ // its blob fields, keeping them available after the author goes offline.
2648
+ ...this.persistBlobChunk ? { blobStorageEnabled: true } : {},
2527
2649
  // Confirm the accepted scope so the client knows what data will be synced.
2528
2650
  // This may differ from what the client requested if auth scopes are narrower.
2529
2651
  ...this.authContext?.scopes ? { acceptedScope: this.authContext.scopes } : {}
@@ -2531,7 +2653,7 @@ var ClientSession = class {
2531
2653
  this.sendToClient(response);
2532
2654
  this.emitter?.emit({ type: "sync:connected", nodeId: msg.nodeId });
2533
2655
  this.state = "syncing";
2534
- const clientVector = (0, import_sync2.wireToVersionVector)(msg.versionVector);
2656
+ const clientVector = (0, import_sync3.wireToVersionVector)(msg.versionVector);
2535
2657
  await this.sendDelta(clientVector);
2536
2658
  this.state = "streaming";
2537
2659
  }
@@ -2600,7 +2722,7 @@ var ClientSession = class {
2600
2722
  const lastOp = operations2[operations2.length - 1];
2601
2723
  const ack = {
2602
2724
  type: "acknowledgment",
2603
- messageId: (0, import_core8.generateUUIDv7)(),
2725
+ messageId: (0, import_core10.generateUUIDv7)(),
2604
2726
  acknowledgedMessageId: msg.messageId,
2605
2727
  lastSequenceNumber: lastOp ? lastOp.sequenceNumber : 0
2606
2728
  };
@@ -2623,7 +2745,7 @@ var ClientSession = class {
2623
2745
  if (missing.length === 0) {
2624
2746
  const emptyBatch = {
2625
2747
  type: "operation-batch",
2626
- messageId: (0, import_core8.generateUUIDv7)(),
2748
+ messageId: (0, import_core10.generateUUIDv7)(),
2627
2749
  operations: [],
2628
2750
  isFinal: true,
2629
2751
  batchIndex: 0,
@@ -2633,12 +2755,12 @@ var ClientSession = class {
2633
2755
  return;
2634
2756
  }
2635
2757
  const sorted = (0, import_internal.topologicalSort)(missing);
2636
- const afterCursor = (0, import_sync2.sliceOperationsAfterCursor)(sorted, this.resumeDeltaCursor);
2758
+ const afterCursor = (0, import_sync3.sliceOperationsAfterCursor)(sorted, this.resumeDeltaCursor);
2637
2759
  const totalBatches = Math.ceil(afterCursor.length / this.batchSize);
2638
2760
  if (afterCursor.length === 0) {
2639
2761
  const emptyBatch = {
2640
2762
  type: "operation-batch",
2641
- messageId: (0, import_core8.generateUUIDv7)(),
2763
+ messageId: (0, import_core10.generateUUIDv7)(),
2642
2764
  operations: [],
2643
2765
  isFinal: true,
2644
2766
  batchIndex: this.resumeDeltaCursor?.batchIndex ?? 0,
@@ -2651,15 +2773,15 @@ var ClientSession = class {
2651
2773
  const start = i * this.batchSize;
2652
2774
  const batchOps = afterCursor.slice(start, start + this.batchSize);
2653
2775
  const serializedOps = batchOps.map((op) => this.serializer.encodeOperation(op));
2654
- const batchCursor = (0, import_sync2.createDeltaCursorFromBatch)(batchOps, i);
2776
+ const batchCursor = (0, import_sync3.createDeltaCursorFromBatch)(batchOps, i);
2655
2777
  const batchMsg = {
2656
2778
  type: "operation-batch",
2657
- messageId: (0, import_core8.generateUUIDv7)(),
2779
+ messageId: (0, import_core10.generateUUIDv7)(),
2658
2780
  operations: serializedOps,
2659
2781
  isFinal: i === totalBatches - 1,
2660
2782
  batchIndex: i,
2661
2783
  totalBatches,
2662
- ...batchCursor ? { cursor: (0, import_sync2.encodeDeltaCursor)(batchCursor) } : {}
2784
+ ...batchCursor ? { cursor: (0, import_sync3.encodeDeltaCursor)(batchCursor) } : {}
2663
2785
  };
2664
2786
  this.sendToClient(batchMsg);
2665
2787
  this.emitter?.emit({
@@ -2673,7 +2795,7 @@ var ClientSession = class {
2673
2795
  if (!operationMatchesScopes(op, this.authContext?.scopes)) {
2674
2796
  return false;
2675
2797
  }
2676
- return (0, import_sync2.operationMatchesQuerySubsets)(op, this.syncQuerySubsets);
2798
+ return (0, import_sync3.operationMatchesQuerySubsets)(op, this.syncQuerySubsets);
2677
2799
  }
2678
2800
  handleAwarenessUpdate(msg) {
2679
2801
  this.onAwarenessUpdate?.(this.sessionId, msg);
@@ -2684,7 +2806,7 @@ var ClientSession = class {
2684
2806
  sendError(code, message, retriable) {
2685
2807
  const errorMsg = {
2686
2808
  type: "error",
2687
- messageId: (0, import_core8.generateUUIDv7)(),
2809
+ messageId: (0, import_core10.generateUUIDv7)(),
2688
2810
  code,
2689
2811
  message,
2690
2812
  retriable
@@ -2712,13 +2834,13 @@ function selectWireFormat(supportedWireFormats) {
2712
2834
 
2713
2835
  // src/server/kora-sync-server.ts
2714
2836
  init_cjs_shims();
2715
- var import_core10 = require("@korajs/core");
2837
+ var import_core12 = require("@korajs/core");
2716
2838
  var import_internal2 = require("@korajs/core/internal");
2717
- var import_sync3 = require("@korajs/sync");
2839
+ var import_sync5 = require("@korajs/sync");
2718
2840
 
2719
2841
  // src/awareness/awareness-relay.ts
2720
2842
  init_cjs_shims();
2721
- var import_core9 = require("@korajs/core");
2843
+ var import_core11 = require("@korajs/core");
2722
2844
  var AwarenessRelay = class {
2723
2845
  clients = /* @__PURE__ */ new Map();
2724
2846
  /**
@@ -2747,7 +2869,7 @@ var AwarenessRelay = class {
2747
2869
  if (hasStates) {
2748
2870
  const catchUpMsg = {
2749
2871
  type: "awareness-update",
2750
- messageId: (0, import_core9.generateUUIDv7)(),
2872
+ messageId: (0, import_core11.generateUUIDv7)(),
2751
2873
  clientId: 0,
2752
2874
  // Server-sourced
2753
2875
  states: existingStates
@@ -2770,7 +2892,7 @@ var AwarenessRelay = class {
2770
2892
  };
2771
2893
  const msg = {
2772
2894
  type: "awareness-update",
2773
- messageId: (0, import_core9.generateUUIDv7)(),
2895
+ messageId: (0, import_core11.generateUUIDv7)(),
2774
2896
  clientId: client.clientId,
2775
2897
  states: removalStates
2776
2898
  };
@@ -2931,6 +3053,114 @@ var ServerMetricsCollector = class {
2931
3053
  }
2932
3054
  };
2933
3055
 
3056
+ // src/richtext/blob-chunk-relay.ts
3057
+ init_cjs_shims();
3058
+ var import_sync4 = require("@korajs/sync");
3059
+ var BlobChunkRelay = class {
3060
+ clients = /* @__PURE__ */ new Map();
3061
+ /** requestId -> the session that originated the request (to route the answer back). */
3062
+ pending = /* @__PURE__ */ new Map();
3063
+ resolveBlobChunk;
3064
+ constructor(resolveBlobChunk) {
3065
+ this.resolveBlobChunk = resolveBlobChunk ?? null;
3066
+ }
3067
+ addClient(sessionId, transport) {
3068
+ this.clients.set(sessionId, { sessionId, transport });
3069
+ }
3070
+ removeClient(sessionId) {
3071
+ this.clients.delete(sessionId);
3072
+ for (const [requestId, originSessionId] of this.pending) {
3073
+ if (originSessionId === sessionId) {
3074
+ this.pending.delete(requestId);
3075
+ }
3076
+ }
3077
+ }
3078
+ getClientCount() {
3079
+ return this.clients.size;
3080
+ }
3081
+ getPendingCount() {
3082
+ return this.pending.size;
3083
+ }
3084
+ clear() {
3085
+ this.clients.clear();
3086
+ this.pending.clear();
3087
+ }
3088
+ /**
3089
+ * Handle an inbound chunk request from a session. Tries the server's own
3090
+ * store first (if configured), otherwise forwards the request to peer
3091
+ * sessions and remembers who to route the answer back to.
3092
+ */
3093
+ handleRequest(sourceSessionId, message) {
3094
+ if (!this.clients.has(sourceSessionId)) {
3095
+ return;
3096
+ }
3097
+ if (this.resolveBlobChunk) {
3098
+ void this.resolveBlobChunk(message.hash).then(
3099
+ (bytes) => {
3100
+ if (bytes !== null) {
3101
+ this.sendResponseTo(sourceSessionId, message.requestId, (0, import_sync4.encodeBlobChunkBytes)(bytes));
3102
+ return;
3103
+ }
3104
+ this.forwardRequestToPeers(sourceSessionId, message);
3105
+ },
3106
+ () => {
3107
+ this.forwardRequestToPeers(sourceSessionId, message);
3108
+ }
3109
+ );
3110
+ return;
3111
+ }
3112
+ this.forwardRequestToPeers(sourceSessionId, message);
3113
+ }
3114
+ /**
3115
+ * Handle an inbound chunk response from a peer. Routes it back to the session
3116
+ * that originally requested it. A "not held" answer (bytes === null) is
3117
+ * ignored so a peer without the chunk does not preempt one that has it; the
3118
+ * requester's own per-request timeout bounds the wait.
3119
+ */
3120
+ handleResponse(sourceSessionId, message) {
3121
+ if (!this.clients.has(sourceSessionId)) {
3122
+ return;
3123
+ }
3124
+ if (message.bytes === null) {
3125
+ return;
3126
+ }
3127
+ const originSessionId = this.pending.get(message.requestId);
3128
+ if (!originSessionId) {
3129
+ return;
3130
+ }
3131
+ this.pending.delete(message.requestId);
3132
+ this.sendResponseTo(originSessionId, message.requestId, message.bytes);
3133
+ }
3134
+ forwardRequestToPeers(sourceSessionId, message) {
3135
+ this.pending.set(message.requestId, sourceSessionId);
3136
+ for (const [, client] of this.clients) {
3137
+ if (client.sessionId === sourceSessionId) {
3138
+ continue;
3139
+ }
3140
+ if (!client.transport.isConnected()) {
3141
+ continue;
3142
+ }
3143
+ client.transport.send(message);
3144
+ }
3145
+ }
3146
+ sendResponseTo(sessionId, requestId, bytes) {
3147
+ const client = this.clients.get(sessionId);
3148
+ if (!client || !client.transport.isConnected()) {
3149
+ return;
3150
+ }
3151
+ const response = {
3152
+ type: "blob-chunk-response",
3153
+ messageId: generateResponseId(requestId),
3154
+ requestId,
3155
+ bytes
3156
+ };
3157
+ client.transport.send(response);
3158
+ }
3159
+ };
3160
+ function generateResponseId(requestId) {
3161
+ return `blob-resp-${requestId}`;
3162
+ }
3163
+
2934
3164
  // src/richtext/yjs-doc-relay.ts
2935
3165
  init_cjs_shims();
2936
3166
  var YjsDocRelay = class {
@@ -2988,6 +3218,8 @@ var KoraSyncServer = class {
2988
3218
  metrics;
2989
3219
  awarenessRelay = new AwarenessRelay();
2990
3220
  yjsDocRelay = new YjsDocRelay();
3221
+ blobChunkRelay;
3222
+ persistBlobChunk;
2991
3223
  sessions = /* @__PURE__ */ new Map();
2992
3224
  httpClients = /* @__PURE__ */ new Map();
2993
3225
  httpSessionToClient = /* @__PURE__ */ new Map();
@@ -2997,7 +3229,7 @@ var KoraSyncServer = class {
2997
3229
  constructor(config) {
2998
3230
  this.store = config.store;
2999
3231
  this.auth = config.auth ?? null;
3000
- this.serializer = config.serializer ?? new import_sync3.JsonMessageSerializer();
3232
+ this.serializer = config.serializer ?? new import_sync5.JsonMessageSerializer();
3001
3233
  this.emitter = config.emitter ?? null;
3002
3234
  this.maxConnections = config.maxConnections ?? DEFAULT_MAX_CONNECTIONS;
3003
3235
  this.batchSize = config.batchSize ?? DEFAULT_BATCH_SIZE2;
@@ -3012,6 +3244,8 @@ var KoraSyncServer = class {
3012
3244
  this.logger = config.logger ?? createDefaultLogger();
3013
3245
  this.metrics = config.metricsCollector ?? new ServerMetricsCollector();
3014
3246
  this.metrics.setSchemaVersion(this.schemaVersion);
3247
+ this.blobChunkRelay = new BlobChunkRelay(config.resolveBlobChunk);
3248
+ this.persistBlobChunk = config.persistBlobChunk ?? null;
3015
3249
  if (!this.emitter) {
3016
3250
  this.emitter = new import_internal2.SimpleEventEmitter();
3017
3251
  }
@@ -3084,10 +3318,10 @@ var KoraSyncServer = class {
3084
3318
  */
3085
3319
  async start(wsServerImpl) {
3086
3320
  if (this.running) {
3087
- throw new import_core10.SyncError("Server is already running", { port: this.port });
3321
+ throw new import_core12.SyncError("Server is already running", { port: this.port });
3088
3322
  }
3089
3323
  if (!wsServerImpl && this.port === void 0) {
3090
- throw new import_core10.SyncError(
3324
+ throw new import_core12.SyncError(
3091
3325
  "Port is required for standalone mode. Provide port in config or use handleConnection() for attach mode.",
3092
3326
  {}
3093
3327
  );
@@ -3135,6 +3369,7 @@ var KoraSyncServer = class {
3135
3369
  });
3136
3370
  this.awarenessRelay.clear();
3137
3371
  this.yjsDocRelay.clear();
3372
+ this.blobChunkRelay.clear();
3138
3373
  for (const session of this.sessions.values()) {
3139
3374
  session.close("server shutting down");
3140
3375
  }
@@ -3196,7 +3431,7 @@ var KoraSyncServer = class {
3196
3431
  if (this.maxConnections > 0 && this.sessions.size >= this.maxConnections) {
3197
3432
  transport.send({
3198
3433
  type: "error",
3199
- messageId: (0, import_core10.generateUUIDv7)(),
3434
+ messageId: (0, import_core12.generateUUIDv7)(),
3200
3435
  code: "MAX_CONNECTIONS",
3201
3436
  message: `Server has reached maximum connections (${this.maxConnections})`,
3202
3437
  retriable: true
@@ -3209,12 +3444,12 @@ var KoraSyncServer = class {
3209
3444
  event: "connection.rejected",
3210
3445
  details: { reason: "max_connections", max: this.maxConnections }
3211
3446
  });
3212
- throw new import_core10.SyncError("Maximum connections reached", {
3447
+ throw new import_core12.SyncError("Maximum connections reached", {
3213
3448
  current: this.sessions.size,
3214
3449
  max: this.maxConnections
3215
3450
  });
3216
3451
  }
3217
- const sessionId = (0, import_core10.generateUUIDv7)();
3452
+ const sessionId = (0, import_core12.generateUUIDv7)();
3218
3453
  this.metrics.recordConnection(sessionId);
3219
3454
  const sessionEmitter = new import_internal2.SimpleEventEmitter();
3220
3455
  sessionEmitter.on("sync:connected", (event) => {
@@ -3279,12 +3514,20 @@ var KoraSyncServer = class {
3279
3514
  onYjsDocUpdate: (sourceSessionId, message) => {
3280
3515
  this.handleYjsDocRelay(sourceSessionId, message);
3281
3516
  },
3517
+ onBlobChunkRequest: (sourceSessionId, message) => {
3518
+ this.blobChunkRelay.handleRequest(sourceSessionId, message);
3519
+ },
3520
+ onBlobChunkResponse: (sourceSessionId, message) => {
3521
+ this.blobChunkRelay.handleResponse(sourceSessionId, message);
3522
+ },
3523
+ ...this.persistBlobChunk ? { persistBlobChunk: this.persistBlobChunk } : {},
3282
3524
  onClose: (sid) => {
3283
3525
  this.handleSessionClose(sid);
3284
3526
  }
3285
3527
  });
3286
3528
  this.sessions.set(sessionId, session);
3287
3529
  this.yjsDocRelay.addClient(sessionId, transport);
3530
+ this.blobChunkRelay.addClient(sessionId, transport);
3288
3531
  session.start();
3289
3532
  this.logger.log({
3290
3533
  timestamp: Date.now(),
@@ -3317,6 +3560,61 @@ var KoraSyncServer = class {
3317
3560
  errorCount: snapshot.errorCount
3318
3561
  };
3319
3562
  }
3563
+ /**
3564
+ * Apply a server-originated operation (for example one created by a custom
3565
+ * HTTP route) through the same validated pipeline that incoming client
3566
+ * operations use — Tier 2 constraints, referential integrity, and cascade
3567
+ * side effects — then relay every applied operation to connected clients.
3568
+ *
3569
+ * Because the operation did not come from a client session, it is relayed to
3570
+ * ALL sessions (there is no source session to exclude). Each session still
3571
+ * applies its own per-scope visibility filter in `relayOperations`, so a
3572
+ * client only receives the operation if it falls within that client's scope.
3573
+ *
3574
+ * @param op - A fully-formed, server-originated operation to apply
3575
+ * @returns The apply result, including any server-generated side-effect ops
3576
+ */
3577
+ async applyLocalOperation(op) {
3578
+ const result = await applyServerOperation(this.store, op);
3579
+ if (result.result === "applied" && result.appliedOperations.length > 0) {
3580
+ for (const session of this.sessions.values()) {
3581
+ session.relayOperations(result.appliedOperations);
3582
+ }
3583
+ }
3584
+ return result;
3585
+ }
3586
+ /**
3587
+ * Collect every blob reference still reachable from live records on the server.
3588
+ *
3589
+ * This is the live set for garbage-collecting the server's central blob store:
3590
+ * pass the result to `collectBlobGarbage(blobStore, liveRefs)` from
3591
+ * `@korajs/store` to reclaim bytes no record references any more. Only
3592
+ * collections that declare a `blob` field are scanned.
3593
+ *
3594
+ * @returns Every live blob reference across all collections
3595
+ */
3596
+ async getLiveBlobRefs() {
3597
+ const schema = this.store.getSchema();
3598
+ if (!schema) {
3599
+ return [];
3600
+ }
3601
+ const refs = [];
3602
+ for (const [name, collection] of Object.entries(schema.collections)) {
3603
+ const hasBlobField = Object.values(collection.fields).some((field) => field.kind === "blob");
3604
+ if (!hasBlobField) {
3605
+ continue;
3606
+ }
3607
+ const records = await this.store.materializeCollection(name);
3608
+ for (const record of records) {
3609
+ for (const value of Object.values(record)) {
3610
+ if ((0, import_core12.isBlobRef)(value)) {
3611
+ refs.push(value);
3612
+ }
3613
+ }
3614
+ }
3615
+ }
3616
+ return refs;
3617
+ }
3320
3618
  /**
3321
3619
  * Get the number of currently connected clients.
3322
3620
  */
@@ -3350,6 +3648,7 @@ var KoraSyncServer = class {
3350
3648
  this.metrics.recordDisconnection(sessionId);
3351
3649
  this.awarenessRelay.removeClient(sessionId);
3352
3650
  this.yjsDocRelay.removeClient(sessionId);
3651
+ this.blobChunkRelay.removeClient(sessionId);
3353
3652
  this.sessions.delete(sessionId);
3354
3653
  const clientId = this.httpSessionToClient.get(sessionId);
3355
3654
  if (clientId) {
@@ -3402,14 +3701,6 @@ function normalizeHttpBody(body, contentType) {
3402
3701
  return body;
3403
3702
  }
3404
3703
 
3405
- // src/auth/no-auth.ts
3406
- init_cjs_shims();
3407
- var NoAuthProvider = class {
3408
- async authenticate(_token) {
3409
- return { userId: "anonymous" };
3410
- }
3411
- };
3412
-
3413
3704
  // src/auth/token-auth.ts
3414
3705
  init_cjs_shims();
3415
3706
  var TokenAuthProvider = class {
@@ -3496,6 +3787,162 @@ function createKoraServer(config) {
3496
3787
 
3497
3788
  // src/server/production-server.ts
3498
3789
  init_cjs_shims();
3790
+
3791
+ // src/server/route-context.ts
3792
+ init_cjs_shims();
3793
+ var import_core13 = require("@korajs/core");
3794
+ function recordMatchesScope(collection, record, scope) {
3795
+ const collectionScope = scope[collection];
3796
+ if (!collectionScope) {
3797
+ return false;
3798
+ }
3799
+ for (const [field, expected] of Object.entries(collectionScope)) {
3800
+ if (record[field] !== expected) {
3801
+ return false;
3802
+ }
3803
+ }
3804
+ return true;
3805
+ }
3806
+ function pickFields(record, keys) {
3807
+ const picked = {};
3808
+ for (const key of keys) {
3809
+ if (key in record) {
3810
+ picked[key] = record[key];
3811
+ }
3812
+ }
3813
+ return picked;
3814
+ }
3815
+ async function buildRouteOperation(store, mutation) {
3816
+ const nodeId = store.getNodeId();
3817
+ const clock = new import_core13.HybridLogicalClock(nodeId);
3818
+ const schemaVersion = store.getSchema()?.version ?? 1;
3819
+ const sequenceNumber = nextServerSequenceNumber(store);
3820
+ if (mutation.type === "insert") {
3821
+ return (0, import_core13.createOperation)(
3822
+ {
3823
+ nodeId,
3824
+ type: "insert",
3825
+ collection: mutation.collection,
3826
+ recordId: mutation.recordId ?? (0, import_core13.generateUUIDv7)(),
3827
+ data: mutation.data ?? {},
3828
+ previousData: null,
3829
+ sequenceNumber,
3830
+ causalDeps: [],
3831
+ schemaVersion
3832
+ },
3833
+ clock
3834
+ );
3835
+ }
3836
+ if (!mutation.recordId) {
3837
+ throw new RouteMutationError(
3838
+ "MISSING_RECORD_ID",
3839
+ `A ${mutation.type} mutation on "${mutation.collection}" requires a recordId.`
3840
+ );
3841
+ }
3842
+ const current = await store.findRecord(mutation.collection, mutation.recordId);
3843
+ if (mutation.type === "update") {
3844
+ const data = mutation.data ?? {};
3845
+ return (0, import_core13.createOperation)(
3846
+ {
3847
+ nodeId,
3848
+ type: "update",
3849
+ collection: mutation.collection,
3850
+ recordId: mutation.recordId,
3851
+ data,
3852
+ previousData: current ? pickFields(current, Object.keys(data)) : {},
3853
+ sequenceNumber,
3854
+ causalDeps: [],
3855
+ schemaVersion
3856
+ },
3857
+ clock
3858
+ );
3859
+ }
3860
+ return (0, import_core13.createOperation)(
3861
+ {
3862
+ nodeId,
3863
+ type: "delete",
3864
+ collection: mutation.collection,
3865
+ recordId: mutation.recordId,
3866
+ data: null,
3867
+ previousData: current ? { ...current } : null,
3868
+ sequenceNumber,
3869
+ causalDeps: [],
3870
+ schemaVersion
3871
+ },
3872
+ clock
3873
+ );
3874
+ }
3875
+ var RouteMutationError = class extends Error {
3876
+ constructor(code, message) {
3877
+ super(message);
3878
+ this.code = code;
3879
+ this.name = "RouteMutationError";
3880
+ }
3881
+ code;
3882
+ };
3883
+ function createRouteContext(server, store) {
3884
+ let mutationTail = Promise.resolve();
3885
+ function serialize(work) {
3886
+ const run = mutationTail.then(work, work);
3887
+ mutationTail = run.then(
3888
+ () => void 0,
3889
+ () => void 0
3890
+ );
3891
+ return run;
3892
+ }
3893
+ return {
3894
+ apply(mutation, options) {
3895
+ return serialize(async () => {
3896
+ let op;
3897
+ try {
3898
+ op = await buildRouteOperation(store, mutation);
3899
+ } catch (error) {
3900
+ if (error instanceof RouteMutationError) {
3901
+ return { ok: false, code: error.code, message: error.message };
3902
+ }
3903
+ throw error;
3904
+ }
3905
+ if (options?.scope && !operationMatchesScopes(op, options.scope)) {
3906
+ return {
3907
+ ok: false,
3908
+ code: "SCOPE_VIOLATION",
3909
+ message: `Mutation on "${mutation.collection}" is outside the provided scope.`
3910
+ };
3911
+ }
3912
+ const result = await server.applyLocalOperation(op);
3913
+ if (result.result !== "applied") {
3914
+ return {
3915
+ ok: false,
3916
+ code: result.rejection?.code ?? "NOT_APPLIED",
3917
+ message: result.rejection?.message ?? `Operation on "${mutation.collection}" was not applied (${result.result}).`
3918
+ };
3919
+ }
3920
+ const record = await store.findRecord(mutation.collection, op.recordId);
3921
+ return { ok: true, operation: op, record };
3922
+ });
3923
+ },
3924
+ async query(collection, options) {
3925
+ const { scope, ...queryOptions } = options ?? {};
3926
+ const records = await store.queryCollection(collection, queryOptions);
3927
+ if (!scope) {
3928
+ return records;
3929
+ }
3930
+ return records.filter((record) => recordMatchesScope(collection, record, scope));
3931
+ },
3932
+ async findById(collection, id, options) {
3933
+ const record = await store.findRecord(collection, id);
3934
+ if (!record) {
3935
+ return null;
3936
+ }
3937
+ if (options?.scope && !recordMatchesScope(collection, record, options.scope)) {
3938
+ return null;
3939
+ }
3940
+ return record;
3941
+ }
3942
+ };
3943
+ }
3944
+
3945
+ // src/server/production-server.ts
3499
3946
  var MIME_TYPES = {
3500
3947
  ".html": "text/html",
3501
3948
  ".js": "text/javascript",
@@ -3519,6 +3966,7 @@ function createProductionServer(config) {
3519
3966
  enableDashboard: true,
3520
3967
  ...config.syncOptions
3521
3968
  });
3969
+ const routeContext = createRouteContext(syncServer, config.store);
3522
3970
  let httpServer = null;
3523
3971
  function getOperationalToken(kind) {
3524
3972
  const auth = config.operationalAuth;
@@ -3605,6 +4053,10 @@ function createProductionServer(config) {
3605
4053
  const chunks = [];
3606
4054
  req.on("data", (chunk) => chunks.push(chunk));
3607
4055
  req.on("end", () => resolve(Buffer.concat(chunks)));
4056
+ if (!req.readableFlowing) {
4057
+ req.resume();
4058
+ }
4059
+ req.on("error", () => resolve(Buffer.concat(chunks)));
3608
4060
  });
3609
4061
  }
3610
4062
  async function readJsonBody(req) {
@@ -3661,6 +4113,19 @@ function createProductionServer(config) {
3661
4113
  const { WebSocketServer } = await import("ws");
3662
4114
  const distDir = resolve(staticDir);
3663
4115
  httpServer = createServer(async (req, res) => {
4116
+ try {
4117
+ await handleRequest(req, res);
4118
+ } catch (error) {
4119
+ console.error("[kora] Unhandled error in HTTP request handler:", error);
4120
+ if (!res.headersSent) {
4121
+ res.writeHead(500, { "Content-Type": "application/json" });
4122
+ res.end(JSON.stringify({ error: "Internal server error" }));
4123
+ } else if (!res.writableEnded) {
4124
+ res.end();
4125
+ }
4126
+ }
4127
+ });
4128
+ async function handleRequest(req, res) {
3664
4129
  res.setHeader("Cross-Origin-Opener-Policy", "same-origin");
3665
4130
  res.setHeader("Cross-Origin-Embedder-Policy", "require-corp");
3666
4131
  const url = new URL(req.url || "/", `http://${req.headers.host}`);
@@ -3788,7 +4253,8 @@ data: ${JSON.stringify(s)}
3788
4253
  body: await readJsonBody(req),
3789
4254
  headers: req.headers,
3790
4255
  query: getQuery(url),
3791
- ip: getClientIp(req)
4256
+ ip: getClientIp(req),
4257
+ kora: routeContext
3792
4258
  });
3793
4259
  writeJsonResponse(res, result);
3794
4260
  return;
@@ -3822,7 +4288,7 @@ data: ${JSON.stringify(s)}
3822
4288
  const contentType = MIME_TYPES[ext] || "application/octet-stream";
3823
4289
  res.writeHead(200, { "Content-Type": contentType });
3824
4290
  createReadStream(filePath).pipe(res);
3825
- });
4291
+ }
3826
4292
  const wss = new WebSocketServer({ noServer: true });
3827
4293
  httpServer.on("upgrade", (req, socket, head) => {
3828
4294
  const url = new URL(req.url || "/", `http://${req.headers.host}`);