@korajs/server 1.0.0-beta.1 → 1.0.0-beta.2

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
@@ -176,6 +176,7 @@ __export(index_exports, {
176
176
  MixedAuthProvider: () => MixedAuthProvider,
177
177
  NoAuthProvider: () => NoAuthProvider,
178
178
  PostgresServerStore: () => PostgresServerStore,
179
+ RETRIABLE_REJECTION_CODES: () => RETRIABLE_REJECTION_CODES,
179
180
  SqliteServerStore: () => SqliteServerStore,
180
181
  TokenAuthProvider: () => TokenAuthProvider,
181
182
  WsServerTransport: () => WsServerTransport,
@@ -186,7 +187,8 @@ __export(index_exports, {
186
187
  createPrettyLogger: () => createPrettyLogger,
187
188
  createProductionServer: () => createProductionServer,
188
189
  createSilentLogger: () => createSilentLogger,
189
- createSqliteServerStore: () => createSqliteServerStore
190
+ createSqliteServerStore: () => createSqliteServerStore,
191
+ isRetriableRejection: () => isRetriableRejection
190
192
  });
191
193
  module.exports = __toCommonJS(index_exports);
192
194
  init_cjs_shims();
@@ -248,6 +250,13 @@ function createDefaultLogger() {
248
250
  }
249
251
  }
250
252
 
253
+ // src/apply/rejection-taxonomy.ts
254
+ init_cjs_shims();
255
+ var RETRIABLE_REJECTION_CODES = /* @__PURE__ */ new Set(["RATE_LIMIT"]);
256
+ function isRetriableRejection(code) {
257
+ return RETRIABLE_REJECTION_CODES.has(code);
258
+ }
259
+
251
260
  // src/store/memory-server-store.ts
252
261
  init_cjs_shims();
253
262
  var import_core2 = require("@korajs/core");
@@ -269,6 +278,14 @@ function fieldTypeToSql(descriptor, dialect) {
269
278
  return dialect === "postgres" ? "BIGINT" : "INTEGER";
270
279
  case "array":
271
280
  return dialect === "postgres" ? "JSONB" : "TEXT";
281
+ case "object":
282
+ return dialect === "postgres" ? "JSONB" : "TEXT";
283
+ case "json":
284
+ return dialect === "postgres" ? "JSONB" : "TEXT";
285
+ case "blob":
286
+ return "TEXT";
287
+ case "secret":
288
+ return "TEXT";
272
289
  case "richtext":
273
290
  return dialect === "postgres" ? "BYTEA" : "BLOB";
274
291
  }
@@ -285,13 +302,13 @@ function generateCollectionDDL(name, collection, dialect) {
285
302
  const columns = ["id TEXT PRIMARY KEY NOT NULL"];
286
303
  for (const [fieldName, descriptor] of Object.entries(collection.fields)) {
287
304
  const sqlType = fieldTypeToSql(descriptor, dialect);
288
- let colDef = `${fieldName} ${sqlType}`;
305
+ let colDef = `${(0, import_core.quoteIdent)(fieldName)} ${sqlType}`;
289
306
  if (descriptor.defaultValue !== void 0) {
290
307
  colDef += ` DEFAULT ${sqlDefaultLiteral(descriptor.defaultValue)}`;
291
308
  }
292
309
  if (descriptor.kind === "enum" && descriptor.enumValues) {
293
310
  const values = descriptor.enumValues.map((v) => `'${v}'`).join(", ");
294
- colDef += ` CHECK (${fieldName} IN (${values}))`;
311
+ colDef += ` CHECK (${(0, import_core.quoteIdent)(fieldName)} IN (${values}))`;
295
312
  }
296
313
  columns.push(colDef);
297
314
  }
@@ -299,24 +316,26 @@ function generateCollectionDDL(name, collection, dialect) {
299
316
  columns.push(`_created_at ${tsType} NOT NULL DEFAULT 0`);
300
317
  columns.push(`_updated_at ${tsType} NOT NULL DEFAULT 0`);
301
318
  columns.push("_deleted INTEGER NOT NULL DEFAULT 0");
302
- statements.push(`CREATE TABLE IF NOT EXISTS ${name} (
319
+ statements.push(`CREATE TABLE IF NOT EXISTS ${(0, import_core.quoteIdent)(name)} (
303
320
  ${columns.join(",\n ")}
304
321
  )`);
305
322
  for (const [fieldName, descriptor] of Object.entries(collection.fields)) {
306
323
  const sqlType = fieldTypeToSql(descriptor, dialect);
307
- let colDef = `${fieldName} ${sqlType}`;
324
+ let colDef = `${(0, import_core.quoteIdent)(fieldName)} ${sqlType}`;
308
325
  if (descriptor.defaultValue !== void 0) {
309
326
  colDef += ` DEFAULT ${sqlDefaultLiteral(descriptor.defaultValue)}`;
310
327
  }
311
328
  statements.push(`--kora:safe-alter
312
- ALTER TABLE ${name} ADD COLUMN ${colDef}`);
329
+ ALTER TABLE ${(0, import_core.quoteIdent)(name)} ADD COLUMN ${colDef}`);
313
330
  }
314
331
  for (const indexField of collection.indexes) {
315
332
  statements.push(
316
- `CREATE INDEX IF NOT EXISTS idx_${name}_${indexField} ON ${name} (${indexField})`
333
+ `CREATE INDEX IF NOT EXISTS idx_${name}_${indexField} ON ${(0, import_core.quoteIdent)(name)} (${(0, import_core.quoteIdent)(indexField)})`
317
334
  );
318
335
  }
319
- statements.push(`CREATE INDEX IF NOT EXISTS idx_${name}__deleted ON ${name} (_deleted)`);
336
+ statements.push(
337
+ `CREATE INDEX IF NOT EXISTS idx_${name}__deleted ON ${(0, import_core.quoteIdent)(name)} (_deleted)`
338
+ );
320
339
  return statements;
321
340
  }
322
341
  function generateAllCollectionDDL(schema, dialect) {
@@ -952,7 +971,7 @@ var PostgresServerStore = class {
952
971
  this.assertCollection(collection);
953
972
  const schema = this.schema;
954
973
  const collectionDef = schema.collections[collection];
955
- const query = import_drizzle_orm.sql`SELECT * FROM ${import_drizzle_orm.sql.raw(collection)} WHERE id = ${id} AND _deleted = 0`;
974
+ const query = import_drizzle_orm.sql`SELECT * FROM ${import_drizzle_orm.sql.raw((0, import_core3.quoteIdent)(collection))} WHERE id = ${id} AND _deleted = 0`;
956
975
  const rows = await this.db.execute(query);
957
976
  if (rows.length === 0) return null;
958
977
  return this.deserializeRow(rows[0], collectionDef);
@@ -969,7 +988,7 @@ var PostgresServerStore = class {
969
988
  }
970
989
  }
971
990
  const whereClause = this.buildWhereClause(where ?? {}, false);
972
- const query = import_drizzle_orm.sql`SELECT COUNT(*) as cnt FROM ${import_drizzle_orm.sql.raw(collection)} WHERE ${whereClause}`;
991
+ const query = import_drizzle_orm.sql`SELECT COUNT(*) as cnt FROM ${import_drizzle_orm.sql.raw((0, import_core3.quoteIdent)(collection))} WHERE ${whereClause}`;
973
992
  const rows = await this.db.execute(query);
974
993
  const cnt = rows[0]?.cnt;
975
994
  return typeof cnt === "string" ? Number.parseInt(cnt, 10) : cnt ?? 0;
@@ -1056,7 +1075,7 @@ var PostgresServerStore = class {
1056
1075
  );
1057
1076
  } else {
1058
1077
  await txOrDb.execute(
1059
- import_drizzle_orm.sql`UPDATE ${import_drizzle_orm.sql.raw(collection)} SET _deleted = 1, _updated_at = ${Date.now()} WHERE id = ${recordId}`
1078
+ import_drizzle_orm.sql`UPDATE ${import_drizzle_orm.sql.raw((0, import_core3.quoteIdent)(collection))} SET _deleted = 1, _updated_at = ${Date.now()} WHERE id = ${recordId}`
1060
1079
  );
1061
1080
  }
1062
1081
  }
@@ -1075,16 +1094,16 @@ var PostgresServerStore = class {
1075
1094
  updatedAt,
1076
1095
  0
1077
1096
  ];
1078
- const columnsSql = import_drizzle_orm.sql.raw(allColumns.join(", "));
1097
+ const columnsSql = import_drizzle_orm.sql.raw(allColumns.map((c) => (0, import_core3.quoteIdent)(c)).join(", "));
1079
1098
  const valuesSql = import_drizzle_orm.sql.join(
1080
1099
  values.map((v) => import_drizzle_orm.sql`${v}`),
1081
1100
  import_drizzle_orm.sql.raw(", ")
1082
1101
  );
1083
1102
  const updateSet = import_drizzle_orm.sql.raw(
1084
- allColumns.slice(1).map((c) => `${c} = excluded.${c}`).join(", ")
1103
+ allColumns.slice(1).map((c) => `${(0, import_core3.quoteIdent)(c)} = excluded.${(0, import_core3.quoteIdent)(c)}`).join(", ")
1085
1104
  );
1086
1105
  await txOrDb.execute(
1087
- import_drizzle_orm.sql`INSERT INTO ${import_drizzle_orm.sql.raw(tableName)} (${columnsSql}) VALUES (${valuesSql}) ON CONFLICT (id) DO UPDATE SET ${updateSet}`
1106
+ import_drizzle_orm.sql`INSERT INTO ${import_drizzle_orm.sql.raw((0, import_core3.quoteIdent)(tableName))} (${columnsSql}) VALUES (${valuesSql}) ON CONFLICT (id) DO UPDATE SET ${updateSet}`
1088
1107
  );
1089
1108
  }
1090
1109
  /**
@@ -1144,7 +1163,7 @@ var PostgresServerStore = class {
1144
1163
  );
1145
1164
  } else {
1146
1165
  await this.db.execute(
1147
- import_drizzle_orm.sql`INSERT INTO ${import_drizzle_orm.sql.raw(collectionName)} (id, _deleted, _created_at, _updated_at) VALUES (${recordId}, 1, ${Date.now()}, ${Date.now()}) ON CONFLICT (id) DO UPDATE SET _deleted = 1, _updated_at = ${Date.now()}`
1166
+ import_drizzle_orm.sql`INSERT INTO ${import_drizzle_orm.sql.raw((0, import_core3.quoteIdent)(collectionName))} (id, _deleted, _created_at, _updated_at) VALUES (${recordId}, 1, ${Date.now()}, ${Date.now()}) ON CONFLICT (id) DO UPDATE SET _deleted = 1, _updated_at = ${Date.now()}`
1148
1167
  );
1149
1168
  }
1150
1169
  }
@@ -1157,10 +1176,12 @@ var PostgresServerStore = class {
1157
1176
  options?.where ?? {},
1158
1177
  options?.includeDeleted ?? false
1159
1178
  );
1160
- const parts = [import_drizzle_orm.sql`SELECT * FROM ${import_drizzle_orm.sql.raw(collection)} WHERE ${whereClause}`];
1179
+ const parts = [
1180
+ import_drizzle_orm.sql`SELECT * FROM ${import_drizzle_orm.sql.raw((0, import_core3.quoteIdent)(collection))} WHERE ${whereClause}`
1181
+ ];
1161
1182
  if (options?.orderBy) {
1162
1183
  const dir = options.orderDirection === "desc" ? "DESC" : "ASC";
1163
- parts.push(import_drizzle_orm.sql.raw(` ORDER BY ${options.orderBy} ${dir}`));
1184
+ parts.push(import_drizzle_orm.sql.raw(` ORDER BY ${(0, import_core3.quoteIdent)(options.orderBy)} ${dir}`));
1164
1185
  }
1165
1186
  if (options?.limit !== void 0) {
1166
1187
  parts.push(import_drizzle_orm.sql` LIMIT ${options.limit}`);
@@ -1176,7 +1197,7 @@ var PostgresServerStore = class {
1176
1197
  conditions.push(import_drizzle_orm.sql.raw("_deleted = 0"));
1177
1198
  }
1178
1199
  for (const [key, value] of Object.entries(where)) {
1179
- conditions.push(import_drizzle_orm.sql`${import_drizzle_orm.sql.raw(key)} = ${value}`);
1200
+ conditions.push(import_drizzle_orm.sql`${import_drizzle_orm.sql.raw((0, import_core3.quoteIdent)(key))} = ${value}`);
1180
1201
  }
1181
1202
  if (conditions.length === 0) {
1182
1203
  return import_drizzle_orm.sql.raw("1 = 1");
@@ -1528,7 +1549,7 @@ var SqliteServerStore = class {
1528
1549
  this.assertCollection(collection);
1529
1550
  const schema = this.schema;
1530
1551
  const collectionDef = schema.collections[collection];
1531
- const query = import_drizzle_orm2.sql`SELECT * FROM ${import_drizzle_orm2.sql.raw(collection)} WHERE id = ${id} AND _deleted = 0`;
1552
+ const query = import_drizzle_orm2.sql`SELECT * FROM ${import_drizzle_orm2.sql.raw((0, import_core4.quoteIdent)(collection))} WHERE id = ${id} AND _deleted = 0`;
1532
1553
  const rows = this.db.all(query);
1533
1554
  if (rows.length === 0) return null;
1534
1555
  return this.deserializeRow(rows[0], collectionDef);
@@ -1544,7 +1565,7 @@ var SqliteServerStore = class {
1544
1565
  }
1545
1566
  }
1546
1567
  const whereClause = this.buildWhereClause(where ?? {}, false);
1547
- const query = import_drizzle_orm2.sql`SELECT COUNT(*) as cnt FROM ${import_drizzle_orm2.sql.raw(collection)} WHERE ${whereClause}`;
1568
+ const query = import_drizzle_orm2.sql`SELECT COUNT(*) as cnt FROM ${import_drizzle_orm2.sql.raw((0, import_core4.quoteIdent)(collection))} WHERE ${whereClause}`;
1548
1569
  const rows = this.db.all(query);
1549
1570
  return rows[0]?.cnt ?? 0;
1550
1571
  }
@@ -1621,7 +1642,7 @@ var SqliteServerStore = class {
1621
1642
  );
1622
1643
  } else {
1623
1644
  txOrDb.run(
1624
- import_drizzle_orm2.sql`UPDATE ${import_drizzle_orm2.sql.raw(collection)} SET _deleted = 1, _updated_at = ${Date.now()} WHERE id = ${recordId}`
1645
+ import_drizzle_orm2.sql`UPDATE ${import_drizzle_orm2.sql.raw((0, import_core4.quoteIdent)(collection))} SET _deleted = 1, _updated_at = ${Date.now()} WHERE id = ${recordId}`
1625
1646
  );
1626
1647
  }
1627
1648
  }
@@ -1642,16 +1663,16 @@ var SqliteServerStore = class {
1642
1663
  0
1643
1664
  // _deleted = false
1644
1665
  ];
1645
- const columnsSql = import_drizzle_orm2.sql.raw(allColumns.join(", "));
1666
+ const columnsSql = import_drizzle_orm2.sql.raw(allColumns.map((c) => (0, import_core4.quoteIdent)(c)).join(", "));
1646
1667
  const valuesSql = import_drizzle_orm2.sql.join(
1647
1668
  values.map((v) => import_drizzle_orm2.sql`${v}`),
1648
1669
  import_drizzle_orm2.sql.raw(", ")
1649
1670
  );
1650
1671
  const updateSet = import_drizzle_orm2.sql.raw(
1651
- allColumns.slice(1).map((c) => `${c} = excluded.${c}`).join(", ")
1672
+ allColumns.slice(1).map((c) => `${(0, import_core4.quoteIdent)(c)} = excluded.${(0, import_core4.quoteIdent)(c)}`).join(", ")
1652
1673
  );
1653
1674
  txOrDb.run(
1654
- import_drizzle_orm2.sql`INSERT INTO ${import_drizzle_orm2.sql.raw(tableName)} (${columnsSql}) VALUES (${valuesSql}) ON CONFLICT (id) DO UPDATE SET ${updateSet}`
1675
+ import_drizzle_orm2.sql`INSERT INTO ${import_drizzle_orm2.sql.raw((0, import_core4.quoteIdent)(tableName))} (${columnsSql}) VALUES (${valuesSql}) ON CONFLICT (id) DO UPDATE SET ${updateSet}`
1655
1676
  );
1656
1677
  }
1657
1678
  /**
@@ -1709,7 +1730,7 @@ var SqliteServerStore = class {
1709
1730
  );
1710
1731
  } else {
1711
1732
  tx.run(
1712
- import_drizzle_orm2.sql`INSERT INTO ${import_drizzle_orm2.sql.raw(collectionName)} (id, _deleted, _created_at, _updated_at) VALUES (${recordId}, 1, ${Date.now()}, ${Date.now()}) ON CONFLICT (id) DO UPDATE SET _deleted = 1, _updated_at = ${Date.now()}`
1733
+ import_drizzle_orm2.sql`INSERT INTO ${import_drizzle_orm2.sql.raw((0, import_core4.quoteIdent)(collectionName))} (id, _deleted, _created_at, _updated_at) VALUES (${recordId}, 1, ${Date.now()}, ${Date.now()}) ON CONFLICT (id) DO UPDATE SET _deleted = 1, _updated_at = ${Date.now()}`
1713
1734
  );
1714
1735
  }
1715
1736
  }
@@ -1723,10 +1744,12 @@ var SqliteServerStore = class {
1723
1744
  options?.where ?? {},
1724
1745
  options?.includeDeleted ?? false
1725
1746
  );
1726
- const parts = [import_drizzle_orm2.sql`SELECT * FROM ${import_drizzle_orm2.sql.raw(collection)} WHERE ${whereClause}`];
1747
+ const parts = [
1748
+ import_drizzle_orm2.sql`SELECT * FROM ${import_drizzle_orm2.sql.raw((0, import_core4.quoteIdent)(collection))} WHERE ${whereClause}`
1749
+ ];
1727
1750
  if (options?.orderBy) {
1728
1751
  const dir = options.orderDirection === "desc" ? "DESC" : "ASC";
1729
- parts.push(import_drizzle_orm2.sql.raw(` ORDER BY ${options.orderBy} ${dir}`));
1752
+ parts.push(import_drizzle_orm2.sql.raw(` ORDER BY ${(0, import_core4.quoteIdent)(options.orderBy)} ${dir}`));
1730
1753
  }
1731
1754
  if (options?.limit !== void 0) {
1732
1755
  parts.push(import_drizzle_orm2.sql` LIMIT ${options.limit}`);
@@ -1742,7 +1765,7 @@ var SqliteServerStore = class {
1742
1765
  conditions.push(import_drizzle_orm2.sql.raw("_deleted = 0"));
1743
1766
  }
1744
1767
  for (const [key, value] of Object.entries(where)) {
1745
- conditions.push(import_drizzle_orm2.sql`${import_drizzle_orm2.sql.raw(key)} = ${value}`);
1768
+ conditions.push(import_drizzle_orm2.sql`${import_drizzle_orm2.sql.raw((0, import_core4.quoteIdent)(key))} = ${value}`);
1746
1769
  }
1747
1770
  if (conditions.length === 0) {
1748
1771
  return import_drizzle_orm2.sql.raw("1 = 1");
@@ -2189,12 +2212,14 @@ async function applyServerOperation(store, op, relationLookup) {
2189
2212
  const lookup = relationLookup ?? (schema ? (0, import_merge2.buildMergeRelationLookup)(schema) : /* @__PURE__ */ new Map());
2190
2213
  const constraintCheck = await validateIncomingOperationConstraints(store, op, schema);
2191
2214
  if (!constraintCheck.valid) {
2215
+ const code = constraintCheck.code ?? "CONSTRAINT_VIOLATION";
2192
2216
  return {
2193
2217
  result: "skipped",
2194
2218
  appliedOperations: [],
2195
2219
  rejection: {
2196
- code: constraintCheck.code ?? "CONSTRAINT_VIOLATION",
2197
- message: constraintCheck.message ?? `Operation "${op.id}" violates a schema constraint`
2220
+ code,
2221
+ message: constraintCheck.message ?? `Operation "${op.id}" violates a schema constraint`,
2222
+ retriable: isRetriableRejection(code)
2198
2223
  }
2199
2224
  };
2200
2225
  }
@@ -2207,7 +2232,8 @@ async function applyServerOperation(store, op, relationLookup) {
2207
2232
  appliedOperations: [],
2208
2233
  rejection: {
2209
2234
  code: "REFERENTIAL_INTEGRITY",
2210
- message: `Operation "${op.id}" violates referential integrity on "${op.collection}"`
2235
+ message: `Operation "${op.id}" violates referential integrity on "${op.collection}"`,
2236
+ retriable: isRetriableRejection("REFERENTIAL_INTEGRITY")
2211
2237
  }
2212
2238
  };
2213
2239
  }
@@ -2421,6 +2447,8 @@ var ClientSession = class {
2421
2447
  maxOperationBytes;
2422
2448
  maxOpsPerMinute;
2423
2449
  rateLimiter;
2450
+ validateOperation;
2451
+ koraContext;
2424
2452
  constructor(options) {
2425
2453
  this.sessionId = options.sessionId;
2426
2454
  this.transport = options.transport;
@@ -2445,6 +2473,8 @@ var ClientSession = class {
2445
2473
  this.maxOperationBytes = options.maxOperationBytes ?? DEFAULT_MAX_OPERATION_BYTES;
2446
2474
  this.maxOpsPerMinute = options.maxOpsPerMinute ?? DEFAULT_MAX_OPS_PER_MINUTE;
2447
2475
  this.rateLimiter = new SessionRateLimiter(this.maxOpsPerMinute);
2476
+ this.validateOperation = options.validateOperation ?? null;
2477
+ this.koraContext = options.koraContext ?? null;
2448
2478
  }
2449
2479
  /**
2450
2480
  * Start handling messages from the client transport.
@@ -2691,10 +2721,39 @@ var ClientSession = class {
2691
2721
  );
2692
2722
  continue;
2693
2723
  }
2724
+ if (this.validateOperation && this.koraContext) {
2725
+ let decision;
2726
+ try {
2727
+ decision = await this.validateOperation(op, {
2728
+ auth: this.authContext,
2729
+ kora: this.koraContext
2730
+ });
2731
+ } catch (error) {
2732
+ const message = error instanceof Error ? error.message : "validator error";
2733
+ this.sendOperationRejected(op, "VALIDATION_ERROR", `Validator threw: ${message}`, true);
2734
+ continue;
2735
+ }
2736
+ if (decision.action === "reject") {
2737
+ this.sendOperationRejected(
2738
+ op,
2739
+ decision.code,
2740
+ decision.message,
2741
+ decision.retriable ?? isRetriableRejection(decision.code)
2742
+ );
2743
+ continue;
2744
+ }
2745
+ if (decision.action === "ignore") {
2746
+ continue;
2747
+ }
2748
+ }
2694
2749
  try {
2695
2750
  const applyResult = await applyServerOperation(this.store, op);
2696
2751
  if (applyResult.rejection) {
2697
- this.sendError(applyResult.rejection.code, applyResult.rejection.message, false);
2752
+ this.sendError(
2753
+ applyResult.rejection.code,
2754
+ applyResult.rejection.message,
2755
+ applyResult.rejection.retriable
2756
+ );
2698
2757
  continue;
2699
2758
  }
2700
2759
  if (applyResult.result === "applied") {
@@ -2813,6 +2872,25 @@ var ClientSession = class {
2813
2872
  };
2814
2873
  this.sendToClient(errorMsg);
2815
2874
  }
2875
+ /**
2876
+ * Reject one specific client operation, tied to its id, so the submitter can
2877
+ * divert it out of its pending queue into a durable rejected store rather than
2878
+ * losing it or retrying forever. The op is NOT materialized, so no other
2879
+ * replica ever sees it.
2880
+ */
2881
+ sendOperationRejected(op, code, message, retriable) {
2882
+ const rejectedMsg = {
2883
+ type: "operation-rejected",
2884
+ messageId: (0, import_core10.generateUUIDv7)(),
2885
+ operationId: op.id,
2886
+ collection: op.collection,
2887
+ recordId: op.recordId,
2888
+ code,
2889
+ message,
2890
+ retriable
2891
+ };
2892
+ this.sendToClient(rejectedMsg);
2893
+ }
2816
2894
  setSerializerWireFormat(format) {
2817
2895
  if (typeof this.serializer.setWireFormat === "function") {
2818
2896
  this.serializer.setWireFormat(format);
@@ -2834,7 +2912,7 @@ function selectWireFormat(supportedWireFormats) {
2834
2912
 
2835
2913
  // src/server/kora-sync-server.ts
2836
2914
  init_cjs_shims();
2837
- var import_core12 = require("@korajs/core");
2915
+ var import_core13 = require("@korajs/core");
2838
2916
  var import_internal2 = require("@korajs/core/internal");
2839
2917
  var import_sync5 = require("@korajs/sync");
2840
2918
 
@@ -3196,6 +3274,170 @@ var YjsDocRelay = class {
3196
3274
  }
3197
3275
  };
3198
3276
 
3277
+ // src/server/route-context.ts
3278
+ init_cjs_shims();
3279
+ var import_core12 = require("@korajs/core");
3280
+ function recordMatchesScope(collection, record, scope) {
3281
+ const collectionScope = scope[collection];
3282
+ if (!collectionScope) {
3283
+ return false;
3284
+ }
3285
+ for (const [field, expected] of Object.entries(collectionScope)) {
3286
+ if (record[field] !== expected) {
3287
+ return false;
3288
+ }
3289
+ }
3290
+ return true;
3291
+ }
3292
+ function pickFields(record, keys) {
3293
+ const picked = {};
3294
+ for (const key of keys) {
3295
+ if (key in record) {
3296
+ picked[key] = record[key];
3297
+ }
3298
+ }
3299
+ return picked;
3300
+ }
3301
+ async function buildRouteOperation(store, mutation) {
3302
+ const nodeId = store.getNodeId();
3303
+ const clock = new import_core12.HybridLogicalClock(nodeId);
3304
+ const schemaVersion = store.getSchema()?.version ?? 1;
3305
+ const sequenceNumber = nextServerSequenceNumber(store);
3306
+ if (mutation.type === "insert") {
3307
+ return (0, import_core12.createOperation)(
3308
+ {
3309
+ nodeId,
3310
+ type: "insert",
3311
+ collection: mutation.collection,
3312
+ recordId: mutation.recordId ?? (0, import_core12.generateUUIDv7)(),
3313
+ data: mutation.data ?? {},
3314
+ previousData: null,
3315
+ sequenceNumber,
3316
+ causalDeps: [],
3317
+ schemaVersion
3318
+ },
3319
+ clock
3320
+ );
3321
+ }
3322
+ if (!mutation.recordId) {
3323
+ throw new RouteMutationError(
3324
+ "MISSING_RECORD_ID",
3325
+ `A ${mutation.type} mutation on "${mutation.collection}" requires a recordId.`
3326
+ );
3327
+ }
3328
+ const current = await store.findRecord(mutation.collection, mutation.recordId);
3329
+ if (mutation.type === "update") {
3330
+ const data = mutation.data ?? {};
3331
+ return (0, import_core12.createOperation)(
3332
+ {
3333
+ nodeId,
3334
+ type: "update",
3335
+ collection: mutation.collection,
3336
+ recordId: mutation.recordId,
3337
+ data,
3338
+ previousData: current ? pickFields(current, Object.keys(data)) : {},
3339
+ sequenceNumber,
3340
+ causalDeps: [],
3341
+ schemaVersion
3342
+ },
3343
+ clock
3344
+ );
3345
+ }
3346
+ return (0, import_core12.createOperation)(
3347
+ {
3348
+ nodeId,
3349
+ type: "delete",
3350
+ collection: mutation.collection,
3351
+ recordId: mutation.recordId,
3352
+ data: null,
3353
+ previousData: current ? { ...current } : null,
3354
+ sequenceNumber,
3355
+ causalDeps: [],
3356
+ schemaVersion
3357
+ },
3358
+ clock
3359
+ );
3360
+ }
3361
+ var RouteMutationError = class extends Error {
3362
+ constructor(code, message) {
3363
+ super(message);
3364
+ this.code = code;
3365
+ this.name = "RouteMutationError";
3366
+ }
3367
+ code;
3368
+ };
3369
+ function createRouteContext(server, store) {
3370
+ let mutationTail = Promise.resolve();
3371
+ function serialize(work) {
3372
+ const run = mutationTail.then(work, work);
3373
+ mutationTail = run.then(
3374
+ () => void 0,
3375
+ () => void 0
3376
+ );
3377
+ return run;
3378
+ }
3379
+ return {
3380
+ apply(mutation, options) {
3381
+ return serialize(async () => {
3382
+ let op;
3383
+ try {
3384
+ op = await buildRouteOperation(store, mutation);
3385
+ } catch (error) {
3386
+ if (error instanceof RouteMutationError) {
3387
+ return {
3388
+ ok: false,
3389
+ code: error.code,
3390
+ message: error.message,
3391
+ retriable: isRetriableRejection(error.code)
3392
+ };
3393
+ }
3394
+ throw error;
3395
+ }
3396
+ if (options?.scope && !operationMatchesScopes(op, options.scope)) {
3397
+ return {
3398
+ ok: false,
3399
+ code: "SCOPE_VIOLATION",
3400
+ message: `Mutation on "${mutation.collection}" is outside the provided scope.`,
3401
+ retriable: isRetriableRejection("SCOPE_VIOLATION")
3402
+ };
3403
+ }
3404
+ const result = await server.applyLocalOperation(op);
3405
+ if (result.result !== "applied") {
3406
+ const code = result.rejection?.code ?? "NOT_APPLIED";
3407
+ return {
3408
+ ok: false,
3409
+ code,
3410
+ message: result.rejection?.message ?? `Operation on "${mutation.collection}" was not applied (${result.result}).`,
3411
+ // Prefer the pipeline's own classification when present; fall back
3412
+ // to classifying the code so this stays correct if new codes appear.
3413
+ retriable: result.rejection?.retriable ?? isRetriableRejection(code)
3414
+ };
3415
+ }
3416
+ const record = await store.findRecord(mutation.collection, op.recordId);
3417
+ return { ok: true, operation: op, record };
3418
+ });
3419
+ },
3420
+ async query(collection, options) {
3421
+ const { scope, ...queryOptions } = options ?? {};
3422
+ const records = await store.queryCollection(collection, queryOptions);
3423
+ if (!scope) {
3424
+ return records;
3425
+ }
3426
+ return records.filter((record) => recordMatchesScope(collection, record, scope));
3427
+ },
3428
+ async findById(collection, id, options) {
3429
+ const record = await store.findRecord(collection, id);
3430
+ if (!record) {
3431
+ return null;
3432
+ }
3433
+ if (options?.scope && !recordMatchesScope(collection, record, options.scope)) {
3434
+ return null;
3435
+ }
3436
+ return record;
3437
+ }
3438
+ };
3439
+ }
3440
+
3199
3441
  // src/server/kora-sync-server.ts
3200
3442
  var DEFAULT_MAX_CONNECTIONS = 0;
3201
3443
  var DEFAULT_BATCH_SIZE2 = 100;
@@ -3220,10 +3462,16 @@ var KoraSyncServer = class {
3220
3462
  yjsDocRelay = new YjsDocRelay();
3221
3463
  blobChunkRelay;
3222
3464
  persistBlobChunk;
3465
+ maxOperationBytes;
3466
+ maxOpsPerMinute;
3467
+ validateOperation;
3468
+ koraContext;
3223
3469
  sessions = /* @__PURE__ */ new Map();
3224
3470
  httpClients = /* @__PURE__ */ new Map();
3225
3471
  httpSessionToClient = /* @__PURE__ */ new Map();
3226
- serverVersion = "0.4.0";
3472
+ // Informational value reported by getStatus(). Keep in sync with the
3473
+ // @korajs/server package version on release; it is not used for protocol negotiation.
3474
+ serverVersion = "1.0.0-beta.0";
3227
3475
  wsServer = null;
3228
3476
  running = false;
3229
3477
  constructor(config) {
@@ -3246,10 +3494,22 @@ var KoraSyncServer = class {
3246
3494
  this.metrics.setSchemaVersion(this.schemaVersion);
3247
3495
  this.blobChunkRelay = new BlobChunkRelay(config.resolveBlobChunk);
3248
3496
  this.persistBlobChunk = config.persistBlobChunk ?? null;
3497
+ this.maxOperationBytes = config.maxOperationBytes;
3498
+ this.maxOpsPerMinute = config.maxOpsPerMinute;
3499
+ this.validateOperation = config.validateOperation;
3500
+ this.koraContext = createRouteContext(this, this.store);
3249
3501
  if (!this.emitter) {
3250
3502
  this.emitter = new import_internal2.SimpleEventEmitter();
3251
3503
  }
3252
3504
  }
3505
+ /**
3506
+ * The trusted, scoped data-plane context (`apply` / `query` / `findById`).
3507
+ * Shared with the production server so HTTP routes, the operation validator,
3508
+ * and `server.kora` are all the same object over the same pipeline.
3509
+ */
3510
+ getKoraContext() {
3511
+ return this.koraContext;
3512
+ }
3253
3513
  /**
3254
3514
  * Subscribe to session-level events for metrics collection and logging.
3255
3515
  * Called when a new session is created.
@@ -3318,10 +3578,10 @@ var KoraSyncServer = class {
3318
3578
  */
3319
3579
  async start(wsServerImpl) {
3320
3580
  if (this.running) {
3321
- throw new import_core12.SyncError("Server is already running", { port: this.port });
3581
+ throw new import_core13.SyncError("Server is already running", { port: this.port });
3322
3582
  }
3323
3583
  if (!wsServerImpl && this.port === void 0) {
3324
- throw new import_core12.SyncError(
3584
+ throw new import_core13.SyncError(
3325
3585
  "Port is required for standalone mode. Provide port in config or use handleConnection() for attach mode.",
3326
3586
  {}
3327
3587
  );
@@ -3431,7 +3691,7 @@ var KoraSyncServer = class {
3431
3691
  if (this.maxConnections > 0 && this.sessions.size >= this.maxConnections) {
3432
3692
  transport.send({
3433
3693
  type: "error",
3434
- messageId: (0, import_core12.generateUUIDv7)(),
3694
+ messageId: (0, import_core13.generateUUIDv7)(),
3435
3695
  code: "MAX_CONNECTIONS",
3436
3696
  message: `Server has reached maximum connections (${this.maxConnections})`,
3437
3697
  retriable: true
@@ -3444,12 +3704,12 @@ var KoraSyncServer = class {
3444
3704
  event: "connection.rejected",
3445
3705
  details: { reason: "max_connections", max: this.maxConnections }
3446
3706
  });
3447
- throw new import_core12.SyncError("Maximum connections reached", {
3707
+ throw new import_core13.SyncError("Maximum connections reached", {
3448
3708
  current: this.sessions.size,
3449
3709
  max: this.maxConnections
3450
3710
  });
3451
3711
  }
3452
- const sessionId = (0, import_core12.generateUUIDv7)();
3712
+ const sessionId = (0, import_core13.generateUUIDv7)();
3453
3713
  this.metrics.recordConnection(sessionId);
3454
3714
  const sessionEmitter = new import_internal2.SimpleEventEmitter();
3455
3715
  sessionEmitter.on("sync:connected", (event) => {
@@ -3521,6 +3781,11 @@ var KoraSyncServer = class {
3521
3781
  this.blobChunkRelay.handleResponse(sourceSessionId, message);
3522
3782
  },
3523
3783
  ...this.persistBlobChunk ? { persistBlobChunk: this.persistBlobChunk } : {},
3784
+ // Only forward when configured, so an unset server value leaves the
3785
+ // session on its own documented default rather than `undefined`.
3786
+ ...this.maxOperationBytes !== void 0 ? { maxOperationBytes: this.maxOperationBytes } : {},
3787
+ ...this.maxOpsPerMinute !== void 0 ? { maxOpsPerMinute: this.maxOpsPerMinute } : {},
3788
+ ...this.validateOperation ? { validateOperation: this.validateOperation, koraContext: this.koraContext } : {},
3524
3789
  onClose: (sid) => {
3525
3790
  this.handleSessionClose(sid);
3526
3791
  }
@@ -3607,7 +3872,7 @@ var KoraSyncServer = class {
3607
3872
  const records = await this.store.materializeCollection(name);
3608
3873
  for (const record of records) {
3609
3874
  for (const value of Object.values(record)) {
3610
- if ((0, import_core12.isBlobRef)(value)) {
3875
+ if ((0, import_core13.isBlobRef)(value)) {
3611
3876
  refs.push(value);
3612
3877
  }
3613
3878
  }
@@ -3787,162 +4052,6 @@ function createKoraServer(config) {
3787
4052
 
3788
4053
  // src/server/production-server.ts
3789
4054
  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
3946
4055
  var MIME_TYPES = {
3947
4056
  ".html": "text/html",
3948
4057
  ".js": "text/javascript",
@@ -3966,7 +4075,7 @@ function createProductionServer(config) {
3966
4075
  enableDashboard: true,
3967
4076
  ...config.syncOptions
3968
4077
  });
3969
- const routeContext = createRouteContext(syncServer, config.store);
4078
+ const routeContext = syncServer.getKoraContext();
3970
4079
  let httpServer = null;
3971
4080
  function getOperationalToken(kind) {
3972
4081
  const auth = config.operationalAuth;
@@ -4106,6 +4215,10 @@ function createProductionServer(config) {
4106
4215
  res.end(JSON.stringify(result.body ?? null));
4107
4216
  }
4108
4217
  return {
4218
+ kora: routeContext,
4219
+ getLiveBlobRefs() {
4220
+ return syncServer.getLiveBlobRefs();
4221
+ },
4109
4222
  async start() {
4110
4223
  const { createServer } = await import("http");
4111
4224
  const { createReadStream, existsSync, statSync } = await import("fs");
@@ -4405,6 +4518,7 @@ function formatUptime(ms) {
4405
4518
  MixedAuthProvider,
4406
4519
  NoAuthProvider,
4407
4520
  PostgresServerStore,
4521
+ RETRIABLE_REJECTION_CODES,
4408
4522
  SqliteServerStore,
4409
4523
  TokenAuthProvider,
4410
4524
  WsServerTransport,
@@ -4415,6 +4529,7 @@ function formatUptime(ms) {
4415
4529
  createPrettyLogger,
4416
4530
  createProductionServer,
4417
4531
  createSilentLogger,
4418
- createSqliteServerStore
4532
+ createSqliteServerStore,
4533
+ isRetriableRejection
4419
4534
  });
4420
4535
  //# sourceMappingURL=index.cjs.map