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

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,311 @@ 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
+
3281
+ // src/apply/route-predicate.ts
3282
+ init_cjs_shims();
3283
+ function evaluateField(actual, operators) {
3284
+ if ("$eq" in operators && !Object.is(actual, operators.$eq)) {
3285
+ return false;
3286
+ }
3287
+ if ("$ne" in operators && Object.is(actual, operators.$ne)) {
3288
+ return false;
3289
+ }
3290
+ if (operators.$in !== void 0 && !operators.$in.some((candidate) => Object.is(actual, candidate))) {
3291
+ return false;
3292
+ }
3293
+ const numericChecks = [
3294
+ ["$lt", (a, b) => a < b],
3295
+ ["$lte", (a, b) => a <= b],
3296
+ ["$gt", (a, b) => a > b],
3297
+ ["$gte", (a, b) => a >= b]
3298
+ ];
3299
+ for (const [operator, compare] of numericChecks) {
3300
+ const bound = operators[operator];
3301
+ if (bound === void 0) {
3302
+ continue;
3303
+ }
3304
+ if (typeof actual !== "number" || typeof bound !== "number") {
3305
+ return false;
3306
+ }
3307
+ if (!compare(actual, bound)) {
3308
+ return false;
3309
+ }
3310
+ }
3311
+ return true;
3312
+ }
3313
+ function evaluateRoutePredicate(record, predicate) {
3314
+ const fields = Object.keys(predicate);
3315
+ if (fields.length === 0) {
3316
+ return true;
3317
+ }
3318
+ if (record === null) {
3319
+ return false;
3320
+ }
3321
+ for (const field of fields) {
3322
+ const operators = predicate[field];
3323
+ if (operators && !evaluateField(record[field], operators)) {
3324
+ return false;
3325
+ }
3326
+ }
3327
+ return true;
3328
+ }
3329
+
3330
+ // src/server/route-context.ts
3331
+ function recordMatchesScope(collection, record, scope) {
3332
+ const collectionScope = scope[collection];
3333
+ if (!collectionScope) {
3334
+ return false;
3335
+ }
3336
+ for (const [field, expected] of Object.entries(collectionScope)) {
3337
+ if (record[field] !== expected) {
3338
+ return false;
3339
+ }
3340
+ }
3341
+ return true;
3342
+ }
3343
+ function pickFields(record, keys) {
3344
+ const picked = {};
3345
+ for (const key of keys) {
3346
+ if (key in record) {
3347
+ picked[key] = record[key];
3348
+ }
3349
+ }
3350
+ return picked;
3351
+ }
3352
+ async function buildRouteOperation(store, mutation) {
3353
+ const nodeId = store.getNodeId();
3354
+ const clock = new import_core12.HybridLogicalClock(nodeId);
3355
+ const schemaVersion = store.getSchema()?.version ?? 1;
3356
+ const sequenceNumber = nextServerSequenceNumber(store);
3357
+ if (mutation.type === "insert") {
3358
+ return (0, import_core12.createOperation)(
3359
+ {
3360
+ nodeId,
3361
+ type: "insert",
3362
+ collection: mutation.collection,
3363
+ recordId: mutation.recordId ?? (0, import_core12.generateUUIDv7)(),
3364
+ data: mutation.data ?? {},
3365
+ previousData: null,
3366
+ sequenceNumber,
3367
+ causalDeps: [],
3368
+ schemaVersion
3369
+ },
3370
+ clock
3371
+ );
3372
+ }
3373
+ if (!mutation.recordId) {
3374
+ throw new RouteMutationError(
3375
+ "MISSING_RECORD_ID",
3376
+ `A ${mutation.type} mutation on "${mutation.collection}" requires a recordId.`
3377
+ );
3378
+ }
3379
+ const current = await store.findRecord(mutation.collection, mutation.recordId);
3380
+ if (mutation.type === "update") {
3381
+ const rawData = mutation.data ?? {};
3382
+ const data = {};
3383
+ const atomicOps = {};
3384
+ for (const key of Object.keys(rawData)) {
3385
+ const value = rawData[key];
3386
+ if ((0, import_core12.isAtomicOp)(value)) {
3387
+ data[key] = (0, import_core12.resolveAtomicOp)(current ? current[key] : void 0, value);
3388
+ atomicOps[key] = (0, import_core12.toAtomicOp)(value);
3389
+ } else {
3390
+ data[key] = value;
3391
+ }
3392
+ }
3393
+ return (0, import_core12.createOperation)(
3394
+ {
3395
+ nodeId,
3396
+ type: "update",
3397
+ collection: mutation.collection,
3398
+ recordId: mutation.recordId,
3399
+ data,
3400
+ previousData: current ? pickFields(current, Object.keys(rawData)) : {},
3401
+ sequenceNumber,
3402
+ causalDeps: [],
3403
+ schemaVersion,
3404
+ ...Object.keys(atomicOps).length > 0 ? { atomicOps } : {}
3405
+ },
3406
+ clock
3407
+ );
3408
+ }
3409
+ return (0, import_core12.createOperation)(
3410
+ {
3411
+ nodeId,
3412
+ type: "delete",
3413
+ collection: mutation.collection,
3414
+ recordId: mutation.recordId,
3415
+ data: null,
3416
+ previousData: current ? { ...current } : null,
3417
+ sequenceNumber,
3418
+ causalDeps: [],
3419
+ schemaVersion
3420
+ },
3421
+ clock
3422
+ );
3423
+ }
3424
+ var RouteMutationError = class extends Error {
3425
+ constructor(code, message) {
3426
+ super(message);
3427
+ this.code = code;
3428
+ this.name = "RouteMutationError";
3429
+ }
3430
+ code;
3431
+ };
3432
+ function createRouteContext(server, store) {
3433
+ let mutationTail = Promise.resolve();
3434
+ function serialize(work) {
3435
+ const run = mutationTail.then(work, work);
3436
+ mutationTail = run.then(
3437
+ () => void 0,
3438
+ () => void 0
3439
+ );
3440
+ return run;
3441
+ }
3442
+ async function prepareMutation(mutation, scope) {
3443
+ let op;
3444
+ try {
3445
+ op = await buildRouteOperation(store, mutation);
3446
+ } catch (error) {
3447
+ if (error instanceof RouteMutationError) {
3448
+ return {
3449
+ ok: false,
3450
+ rejection: {
3451
+ ok: false,
3452
+ code: error.code,
3453
+ message: error.message,
3454
+ retriable: isRetriableRejection(error.code)
3455
+ }
3456
+ };
3457
+ }
3458
+ throw error;
3459
+ }
3460
+ if (scope && !operationMatchesScopes(op, scope)) {
3461
+ return {
3462
+ ok: false,
3463
+ rejection: {
3464
+ ok: false,
3465
+ code: "SCOPE_VIOLATION",
3466
+ message: `Mutation on "${mutation.collection}" is outside the provided scope.`,
3467
+ retriable: isRetriableRejection("SCOPE_VIOLATION")
3468
+ }
3469
+ };
3470
+ }
3471
+ return { ok: true, op };
3472
+ }
3473
+ async function commitOperation(op, collection) {
3474
+ const result = await server.applyLocalOperation(op);
3475
+ if (result.result !== "applied") {
3476
+ const code = result.rejection?.code ?? "NOT_APPLIED";
3477
+ return {
3478
+ ok: false,
3479
+ code,
3480
+ message: result.rejection?.message ?? `Operation on "${collection}" was not applied (${result.result}).`,
3481
+ // Prefer the pipeline's own classification when present; fall back
3482
+ // to classifying the code so this stays correct if new codes appear.
3483
+ retriable: result.rejection?.retriable ?? isRetriableRejection(code)
3484
+ };
3485
+ }
3486
+ const record = await store.findRecord(collection, op.recordId);
3487
+ return { ok: true, operation: op, record };
3488
+ }
3489
+ async function rawApply(mutation, scope) {
3490
+ const prepared = await prepareMutation(mutation, scope);
3491
+ if (!prepared.ok) {
3492
+ return prepared.rejection;
3493
+ }
3494
+ return commitOperation(prepared.op, mutation.collection);
3495
+ }
3496
+ return {
3497
+ apply(mutation, options) {
3498
+ return serialize(() => rawApply(mutation, options?.scope));
3499
+ },
3500
+ applyConditional(spec, options) {
3501
+ return serialize(async () => {
3502
+ if (spec.idempotencyKey) {
3503
+ const existing = await store.findRecord(
3504
+ spec.idempotencyKey.collection,
3505
+ spec.idempotencyKey.id
3506
+ );
3507
+ if (existing) {
3508
+ return { ok: true, idempotent: true, operations: [], records: [existing] };
3509
+ }
3510
+ }
3511
+ if (spec.if && Object.keys(spec.if).length > 0) {
3512
+ const target = await store.findRecord(spec.collection, spec.id);
3513
+ if (!evaluateRoutePredicate(target, spec.if)) {
3514
+ const code = spec.reject?.code ?? "CONDITION_NOT_MET";
3515
+ return {
3516
+ ok: false,
3517
+ code,
3518
+ message: spec.reject?.message ?? `Admission condition not met on "${spec.collection}" record "${spec.id}".`,
3519
+ retriable: false
3520
+ };
3521
+ }
3522
+ }
3523
+ const mutations = [];
3524
+ if (spec.update !== void 0 && spec.update !== null) {
3525
+ mutations.push({
3526
+ collection: spec.collection,
3527
+ type: "update",
3528
+ recordId: spec.id,
3529
+ data: spec.update
3530
+ });
3531
+ }
3532
+ if (spec.also) {
3533
+ mutations.push(...spec.also);
3534
+ }
3535
+ const prepared = [];
3536
+ for (const mutation of mutations) {
3537
+ const p = await prepareMutation(mutation, options?.scope);
3538
+ if (!p.ok) {
3539
+ return p.rejection;
3540
+ }
3541
+ prepared.push({ op: p.op, collection: mutation.collection });
3542
+ }
3543
+ const operations2 = [];
3544
+ const records = [];
3545
+ for (const { op, collection } of prepared) {
3546
+ const result = await commitOperation(op, collection);
3547
+ if (!result.ok) {
3548
+ return {
3549
+ ok: false,
3550
+ code: result.code,
3551
+ message: result.message,
3552
+ retriable: result.retriable
3553
+ };
3554
+ }
3555
+ operations2.push(result.operation);
3556
+ records.push(result.record);
3557
+ }
3558
+ return { ok: true, idempotent: false, operations: operations2, records };
3559
+ });
3560
+ },
3561
+ async query(collection, options) {
3562
+ const { scope, ...queryOptions } = options ?? {};
3563
+ const records = await store.queryCollection(collection, queryOptions);
3564
+ if (!scope) {
3565
+ return records;
3566
+ }
3567
+ return records.filter((record) => recordMatchesScope(collection, record, scope));
3568
+ },
3569
+ async findById(collection, id, options) {
3570
+ const record = await store.findRecord(collection, id);
3571
+ if (!record) {
3572
+ return null;
3573
+ }
3574
+ if (options?.scope && !recordMatchesScope(collection, record, options.scope)) {
3575
+ return null;
3576
+ }
3577
+ return record;
3578
+ }
3579
+ };
3580
+ }
3581
+
3199
3582
  // src/server/kora-sync-server.ts
3200
3583
  var DEFAULT_MAX_CONNECTIONS = 0;
3201
3584
  var DEFAULT_BATCH_SIZE2 = 100;
@@ -3220,10 +3603,16 @@ var KoraSyncServer = class {
3220
3603
  yjsDocRelay = new YjsDocRelay();
3221
3604
  blobChunkRelay;
3222
3605
  persistBlobChunk;
3606
+ maxOperationBytes;
3607
+ maxOpsPerMinute;
3608
+ validateOperation;
3609
+ koraContext;
3223
3610
  sessions = /* @__PURE__ */ new Map();
3224
3611
  httpClients = /* @__PURE__ */ new Map();
3225
3612
  httpSessionToClient = /* @__PURE__ */ new Map();
3226
- serverVersion = "0.4.0";
3613
+ // Informational value reported by getStatus(). Keep in sync with the
3614
+ // @korajs/server package version on release; it is not used for protocol negotiation.
3615
+ serverVersion = "1.0.0-beta.0";
3227
3616
  wsServer = null;
3228
3617
  running = false;
3229
3618
  constructor(config) {
@@ -3246,10 +3635,22 @@ var KoraSyncServer = class {
3246
3635
  this.metrics.setSchemaVersion(this.schemaVersion);
3247
3636
  this.blobChunkRelay = new BlobChunkRelay(config.resolveBlobChunk);
3248
3637
  this.persistBlobChunk = config.persistBlobChunk ?? null;
3638
+ this.maxOperationBytes = config.maxOperationBytes;
3639
+ this.maxOpsPerMinute = config.maxOpsPerMinute;
3640
+ this.validateOperation = config.validateOperation;
3641
+ this.koraContext = createRouteContext(this, this.store);
3249
3642
  if (!this.emitter) {
3250
3643
  this.emitter = new import_internal2.SimpleEventEmitter();
3251
3644
  }
3252
3645
  }
3646
+ /**
3647
+ * The trusted, scoped data-plane context (`apply` / `query` / `findById`).
3648
+ * Shared with the production server so HTTP routes, the operation validator,
3649
+ * and `server.kora` are all the same object over the same pipeline.
3650
+ */
3651
+ getKoraContext() {
3652
+ return this.koraContext;
3653
+ }
3253
3654
  /**
3254
3655
  * Subscribe to session-level events for metrics collection and logging.
3255
3656
  * Called when a new session is created.
@@ -3318,10 +3719,10 @@ var KoraSyncServer = class {
3318
3719
  */
3319
3720
  async start(wsServerImpl) {
3320
3721
  if (this.running) {
3321
- throw new import_core12.SyncError("Server is already running", { port: this.port });
3722
+ throw new import_core13.SyncError("Server is already running", { port: this.port });
3322
3723
  }
3323
3724
  if (!wsServerImpl && this.port === void 0) {
3324
- throw new import_core12.SyncError(
3725
+ throw new import_core13.SyncError(
3325
3726
  "Port is required for standalone mode. Provide port in config or use handleConnection() for attach mode.",
3326
3727
  {}
3327
3728
  );
@@ -3431,7 +3832,7 @@ var KoraSyncServer = class {
3431
3832
  if (this.maxConnections > 0 && this.sessions.size >= this.maxConnections) {
3432
3833
  transport.send({
3433
3834
  type: "error",
3434
- messageId: (0, import_core12.generateUUIDv7)(),
3835
+ messageId: (0, import_core13.generateUUIDv7)(),
3435
3836
  code: "MAX_CONNECTIONS",
3436
3837
  message: `Server has reached maximum connections (${this.maxConnections})`,
3437
3838
  retriable: true
@@ -3444,12 +3845,12 @@ var KoraSyncServer = class {
3444
3845
  event: "connection.rejected",
3445
3846
  details: { reason: "max_connections", max: this.maxConnections }
3446
3847
  });
3447
- throw new import_core12.SyncError("Maximum connections reached", {
3848
+ throw new import_core13.SyncError("Maximum connections reached", {
3448
3849
  current: this.sessions.size,
3449
3850
  max: this.maxConnections
3450
3851
  });
3451
3852
  }
3452
- const sessionId = (0, import_core12.generateUUIDv7)();
3853
+ const sessionId = (0, import_core13.generateUUIDv7)();
3453
3854
  this.metrics.recordConnection(sessionId);
3454
3855
  const sessionEmitter = new import_internal2.SimpleEventEmitter();
3455
3856
  sessionEmitter.on("sync:connected", (event) => {
@@ -3521,6 +3922,11 @@ var KoraSyncServer = class {
3521
3922
  this.blobChunkRelay.handleResponse(sourceSessionId, message);
3522
3923
  },
3523
3924
  ...this.persistBlobChunk ? { persistBlobChunk: this.persistBlobChunk } : {},
3925
+ // Only forward when configured, so an unset server value leaves the
3926
+ // session on its own documented default rather than `undefined`.
3927
+ ...this.maxOperationBytes !== void 0 ? { maxOperationBytes: this.maxOperationBytes } : {},
3928
+ ...this.maxOpsPerMinute !== void 0 ? { maxOpsPerMinute: this.maxOpsPerMinute } : {},
3929
+ ...this.validateOperation ? { validateOperation: this.validateOperation, koraContext: this.koraContext } : {},
3524
3930
  onClose: (sid) => {
3525
3931
  this.handleSessionClose(sid);
3526
3932
  }
@@ -3607,7 +4013,7 @@ var KoraSyncServer = class {
3607
4013
  const records = await this.store.materializeCollection(name);
3608
4014
  for (const record of records) {
3609
4015
  for (const value of Object.values(record)) {
3610
- if ((0, import_core12.isBlobRef)(value)) {
4016
+ if ((0, import_core13.isBlobRef)(value)) {
3611
4017
  refs.push(value);
3612
4018
  }
3613
4019
  }
@@ -3787,162 +4193,6 @@ function createKoraServer(config) {
3787
4193
 
3788
4194
  // src/server/production-server.ts
3789
4195
  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
4196
  var MIME_TYPES = {
3947
4197
  ".html": "text/html",
3948
4198
  ".js": "text/javascript",
@@ -3966,7 +4216,7 @@ function createProductionServer(config) {
3966
4216
  enableDashboard: true,
3967
4217
  ...config.syncOptions
3968
4218
  });
3969
- const routeContext = createRouteContext(syncServer, config.store);
4219
+ const routeContext = syncServer.getKoraContext();
3970
4220
  let httpServer = null;
3971
4221
  function getOperationalToken(kind) {
3972
4222
  const auth = config.operationalAuth;
@@ -4106,6 +4356,10 @@ function createProductionServer(config) {
4106
4356
  res.end(JSON.stringify(result.body ?? null));
4107
4357
  }
4108
4358
  return {
4359
+ kora: routeContext,
4360
+ getLiveBlobRefs() {
4361
+ return syncServer.getLiveBlobRefs();
4362
+ },
4109
4363
  async start() {
4110
4364
  const { createServer } = await import("http");
4111
4365
  const { createReadStream, existsSync, statSync } = await import("fs");
@@ -4405,6 +4659,7 @@ function formatUptime(ms) {
4405
4659
  MixedAuthProvider,
4406
4660
  NoAuthProvider,
4407
4661
  PostgresServerStore,
4662
+ RETRIABLE_REJECTION_CODES,
4408
4663
  SqliteServerStore,
4409
4664
  TokenAuthProvider,
4410
4665
  WsServerTransport,
@@ -4415,6 +4670,7 @@ function formatUptime(ms) {
4415
4670
  createPrettyLogger,
4416
4671
  createProductionServer,
4417
4672
  createSilentLogger,
4418
- createSqliteServerStore
4673
+ createSqliteServerStore,
4674
+ isRetriableRejection
4419
4675
  });
4420
4676
  //# sourceMappingURL=index.cjs.map