orez-sync-cf-host 0.5.30 → 0.5.34

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/README.md CHANGED
@@ -2,26 +2,22 @@
2
2
 
3
3
  This is the production-shaped Cloudflare Durable Object host for the Rust sync
4
4
  engine. The package exposes a consumer integration surface (`createSyncWorker`,
5
- `createSyncDurableObject`, `registerMutators`) and a harness deployment built
5
+ `createSyncDurableObject`) and a harness deployment built
6
6
  from `src/harness-worker.ts`. Consumers provide a JSON Zero schema, application
7
- DDL/seed initializer, normalized-claims authenticator, mutator registry, and an
8
- optional row-visibility hook.
7
+ DDL/seed initializer, normalized-claims authenticator, an
8
+ `orez-sync-executor` mutator registry, and an optional row-visibility hook.
9
9
 
10
- The Worker authenticates at the consumer edge and forwards normalized claims
10
+ The Worker authenticates and authorizes at the consumer edge, then forwards normalized claims
11
11
  inside the binding request body, where request-header observability cannot record
12
12
  them. It never logs tokens, mutation arguments, or row contents. Pull runs
13
- `engine_handle_pull` inside `transactionSync`; push runs
14
- Rust `push_validate`/`preflight`/`finalize`/`record_app_error` steps around the
15
- registered asynchronous TypeScript mutator inside `ctx.storage.transaction`.
16
- Every SQL cursor is materialized before an await. Mutators may await only their
17
- `MutatorSql` operations; timers, fetches, and other external work belong in
18
- `context.defer`, which runs only after commit. Application failures use the
13
+ `engine_handle_pull` inside `transactionSync`; local push delegates validation,
14
+ replay, LMID advancement, CRUD, and committed effects to `orez-sync-executor`
15
+ inside `ctx.storage.transaction`. Every SQL cursor is materialized before an
16
+ await. Mutators may await only their application transaction operations;
17
+ timers, fetches, and other external work belong in
18
+ `ctx.defer`, which runs only after commit. Application failures use the
19
19
  required second transaction to advance the LMID marker.
20
20
 
21
- The root `orez/cf-do` executor and this host consume the same `post-commit`
22
- module, so transaction retries discard effects from abandoned attempts in both
23
- paths.
24
-
25
21
  Transaction-query `ILIKE` folding is ASCII-only on Durable Object SQLite;
26
22
  non-ASCII case pairs can diverge from PostgreSQL.
27
23
 
package/dist/config.d.ts CHANGED
@@ -1,3 +1,4 @@
1
1
  import type { PullCaps, SyncHostConfig, SyncHostEnv } from './types.js';
2
+ import type { Schema } from '@rocicorp/zero';
2
3
  export declare function validatePullCaps(caps: PullCaps): PullCaps;
3
- export declare function validateSyncHostConfig<Env extends SyncHostEnv>(config: SyncHostConfig<Env>): SyncHostConfig<Env>;
4
+ export declare function validateSyncHostConfig<Env extends SyncHostEnv, S extends Schema = Schema>(config: SyncHostConfig<Env, S>): SyncHostConfig<Env, S>;
package/dist/config.js CHANGED
@@ -11,6 +11,9 @@ export function validateSyncHostConfig(config) {
11
11
  if (typeof config.authorizeNotify !== 'function') {
12
12
  throw new TypeError('sync host config authorizeNotify is required');
13
13
  }
14
+ if (typeof config.authorize !== 'function') {
15
+ throw new TypeError('sync host config authorize is required');
16
+ }
14
17
  const hasMutators = config.mutators !== undefined;
15
18
  const hasDelegate = config.mutateUrl !== undefined;
16
19
  if (hasMutators === hasDelegate) {
Binary file
package/dist/host.d.ts CHANGED
@@ -1,13 +1,14 @@
1
1
  import { DurableObject } from 'cloudflare:workers';
2
2
  import type { SyncHostConfig, SyncHostEnv } from './types.js';
3
+ import type { Schema } from '@rocicorp/zero';
3
4
  /**
4
5
  * Create the consumer-facing Worker router. Authentication happens here; the
5
6
  * Durable Object receives normalized claims inside the binding request body so
6
7
  * observability systems cannot record them as request-header metadata.
7
8
  */
8
- export declare function createSyncWorker<Env extends SyncHostEnv>(config: SyncHostConfig<Env>): ExportedHandler<Env>;
9
+ export declare function createSyncWorker<Env extends SyncHostEnv, S extends Schema = Schema>(config: SyncHostConfig<Env, S>): ExportedHandler<Env>;
9
10
  export interface SyncDurableObjectConstructor<Env extends SyncHostEnv> {
10
11
  new (ctx: DurableObjectState, env: Env): DurableObject<Env>;
11
12
  }
12
13
  /** Create the namespace Durable Object class for one bundled consumer config. */
13
- export declare function createSyncDurableObject<Env extends SyncHostEnv>(config: SyncHostConfig<Env>): SyncDurableObjectConstructor<Env>;
14
+ export declare function createSyncDurableObject<Env extends SyncHostEnv, S extends Schema = Schema>(config: SyncHostConfig<Env, S>): SyncDurableObjectConstructor<Env>;
package/dist/host.js CHANGED
@@ -1,10 +1,9 @@
1
1
  import { DurableObject } from 'cloudflare:workers';
2
+ import { createSyncExecutor } from 'orez-sync-executor/core';
2
3
  import { validatePullCaps, validateSyncHostConfig } from './config.js';
3
- import { createPostCommitEffects } from './post-commit.js';
4
4
  import { createQueryCompiler } from './query-compiler.js';
5
5
  import { decodeSqlParams, SqlStorageDirect, SqlStorageMutatorTransaction, SqlStorageSyncDb, } from './sql-storage-adapter.js';
6
- import { isMutationApplicationError } from './types.js';
7
- import { engine_apply_snapshot_changes, engine_apply_snapshot_page, engine_apply_upstream, engine_assemble_push_response, engine_begin_snapshot_generation, engine_finalize, engine_finalize_snapshot_generation, engine_handle_pull, engine_handle_query_pull, engine_init_query_schema, engine_init_schema, engine_invalidate, engine_memory_bytes, engine_preflight, engine_prune, engine_push_validate, engine_read_snapshot_progress, engine_record_app_error, engine_state, engine_version, } from './wasm.js';
6
+ import { engine_apply_snapshot_changes, engine_apply_snapshot_page, engine_apply_upstream, engine_begin_snapshot_generation, engine_finalize, engine_finalize_snapshot_generation, engine_handle_pull, engine_handle_query_pull, engine_init_query_schema, engine_init_schema, engine_invalidate, engine_memory_bytes, engine_preflight, engine_prune, engine_push_validate, engine_read_snapshot_progress, engine_state, engine_version, } from './wasm.js';
8
7
  import { IngestBreakerError, IngestCircuitBreaker, retryDelayMs, shouldRetryDelegatedPush, } from './write-safeguards.js';
9
8
  const NAMESPACE_HEADER = 'x-orez-sync-namespace';
10
9
  const UPSTREAM_PATH_HEADER = 'x-orez-sync-upstream-path';
@@ -177,8 +176,12 @@ export function createSyncWorker(config) {
177
176
  let forwardedBody = null;
178
177
  if (!isAdmin && route !== '/wake' && route !== '/notify') {
179
178
  const claims = await config.authenticate(request, env);
180
- if (!claims)
179
+ if (!claims || typeof claims.userID !== 'string' || claims.userID.length === 0) {
181
180
  return json({ error: 'missing authentication' }, 401);
181
+ }
182
+ if (!(await config.authorize(request, claims, namespace, env))) {
183
+ return json({ error: 'forbidden' }, 403);
184
+ }
182
185
  if ((route === '/pull' || route === '/push') && request.method === 'POST') {
183
186
  try {
184
187
  const body = await requestObject(request);
@@ -231,6 +234,8 @@ export function createSyncDurableObject(config) {
231
234
  #engineDb;
232
235
  #directSql;
233
236
  #mutatorSql;
237
+ #executor;
238
+ #executorBeforeCommitFault = null;
234
239
  #bootID = crypto.randomUUID();
235
240
  #lastRequestAt = 0;
236
241
  #hibernations = 0;
@@ -259,6 +264,50 @@ export function createSyncDurableObject(config) {
259
264
  this.#engineDb = new SqlStorageSyncDb(ctx.storage.sql, recordRowsWritten);
260
265
  this.#directSql = new SqlStorageDirect(ctx.storage.sql, recordRowsWritten);
261
266
  this.#mutatorSql = new SqlStorageMutatorTransaction(this.#directSql, (ast, format) => this.#wasm(() => compileQuery(ast, format)), config.transactionQueryBudget);
267
+ const database = {
268
+ dialect: 'sqlite',
269
+ transaction: async (work) => this.ctx.storage.transaction(async () => {
270
+ let applicationWrite = false;
271
+ const tx = {
272
+ exec: async (sql, params, metadata) => {
273
+ if (metadata !== undefined ||
274
+ (!/^\s*CREATE\s+(?:SCHEMA|TABLE)\b/i.test(sql) &&
275
+ !/\b_zsync_[A-Za-z0-9_]+\b/.test(sql))) {
276
+ applicationWrite = true;
277
+ }
278
+ return this.#mutatorSql.exec(sql, params, metadata);
279
+ },
280
+ query: (sql, params) => this.#mutatorSql.query(sql, params),
281
+ queryAst: (ast, format, queryName) => this.#mutatorSql.queryAst(ast, format, queryName),
282
+ };
283
+ const value = await work(tx);
284
+ if (applicationWrite && this.#executorBeforeCommitFault) {
285
+ const fault = this.#executorBeforeCommitFault;
286
+ this.#executorBeforeCommitFault = null;
287
+ throw this.#faultError(fault, 'push_after_write_before_commit');
288
+ }
289
+ return value;
290
+ }),
291
+ query: (sql, params) => this.#mutatorSql.query(sql, params),
292
+ };
293
+ this.#executor = config.mutators
294
+ ? createSyncExecutor({
295
+ database,
296
+ effects: {
297
+ runBackground: (promise) => this.ctx.waitUntil(promise),
298
+ report: (error) => {
299
+ this.#counters.externalEffectFailures++;
300
+ console.error(JSON.stringify({
301
+ event: 'sync_external_effect_error',
302
+ hostVersion: config.hostVersion,
303
+ error: errorMessage(error),
304
+ }));
305
+ },
306
+ },
307
+ mutators: config.mutators,
308
+ schema: config.schema,
309
+ })
310
+ : null;
262
311
  ctx.blockConcurrencyWhile(async () => {
263
312
  ctx.storage.transactionSync(() => {
264
313
  config.initialize(this.#directSql);
@@ -329,7 +378,16 @@ export function createSyncDurableObject(config) {
329
378
  filters: Object.keys(config.schema.tables).flatMap((table) => {
330
379
  const filter = config.visibility?.filter(table, claims);
331
380
  return filter
332
- ? [{ table, sql: filter.sql, params: [...(filter.params ?? [])] }]
381
+ ? [
382
+ filter.kind === 'expression'
383
+ ? { kind: 'expression', table, expression: filter.expression }
384
+ : {
385
+ kind: 'raw',
386
+ table,
387
+ sql: filter.sql,
388
+ params: [...(filter.params ?? [])],
389
+ },
390
+ ]
333
391
  : [];
334
392
  }),
335
393
  };
@@ -1117,99 +1175,34 @@ export function createSyncDurableObject(config) {
1117
1175
  const beforeMutationFault = this.#takeFault('push_before_mutation');
1118
1176
  if (beforeMutationFault)
1119
1177
  throw this.#faultError(beforeMutationFault, 'push_before_mutation');
1120
- const plan = this.#wasm(() => engine_push_validate(body));
1121
- if (plan.kind === 'respond')
1122
- return json(plan.response);
1123
- const results = [];
1124
- for (const mutation of plan.mutations) {
1125
- const deferred = createPostCommitEffects();
1126
- try {
1127
- const txStarted = performance.now();
1128
- // consume the before-commit fault OUTSIDE the transaction it is
1129
- // about to abort: taken inside, the control-table delete rolls
1130
- // back with the abort and the fault re-fires on every retry
1131
- // instead of being one-shot.
1132
- const beforeCommitFault = this.#takeFault('push_after_write_before_commit');
1133
- const preflight = await this.ctx.storage.transaction(async () => {
1134
- // Storage transactions may retry their closure. Never carry a
1135
- // deferred effect from an abandoned attempt into the commit.
1136
- deferred.beginAttempt();
1137
- const decision = this.#wasm(() => engine_preflight(this.#engineDb, plan.clientGroupID, mutation.clientID, mutation.id, claims.userID));
1138
- if (decision.kind === 'replay')
1139
- return decision;
1140
- const mutator = config.mutators?.[mutation.name];
1141
- if (!mutator)
1142
- throw new Error(`unknown mutator: ${mutation.name}`);
1143
- await mutator(this.#mutatorSql, mutation.args[0] ?? null, {
1144
- claims,
1145
- clientID: mutation.clientID,
1146
- mutationID: mutation.id,
1147
- defer: deferred.defer,
1148
- });
1149
- if (beforeCommitFault)
1150
- throw this.#faultError(beforeCommitFault, 'push_after_write_before_commit');
1151
- this.#wasm(() => engine_finalize(this.#engineDb, plan.clientGroupID, mutation.clientID, mutation.id));
1152
- return decision;
1153
- });
1154
- transactionMs += performance.now() - txStarted;
1155
- if (preflight.kind === 'replay') {
1156
- const expected = preflight
1157
- .expected;
1158
- results.push({
1159
- clientID: mutation.clientID,
1160
- id: mutation.id,
1161
- result: {
1162
- error: 'alreadyProcessed',
1163
- details: `Ignoring mutation from ${mutation.clientID} with ID ${mutation.id} as it was already processed. Expected: ${expected}`,
1164
- },
1165
- });
1166
- continue;
1167
- }
1168
- lmidAdvances++;
1169
- results.push({
1170
- clientID: mutation.clientID,
1171
- id: mutation.id,
1172
- result: {},
1173
- });
1174
- // Keep the advisory fan-out off the push response's critical path.
1175
- // waitUntil anchors the coalescing timer across request completion,
1176
- // while the next serialized client push can join the same batch.
1177
- this.ctx.waitUntil(this.#enqueueWake(mutation.clientID));
1178
- await deferred.runAfterCommit((error) => {
1179
- this.#counters.externalEffectFailures++;
1180
- console.error(JSON.stringify({
1181
- event: 'sync_external_effect_error',
1182
- hostVersion: config.hostVersion,
1183
- error: errorMessage(error),
1184
- }));
1185
- });
1178
+ if (!this.#executor)
1179
+ throw new Error('local sync executor is not configured');
1180
+ // Consume the fault outside the transaction it aborts so a rollback
1181
+ // cannot restore the one-shot control flag.
1182
+ this.#executorBeforeCommitFault = this.#takeFault('push_after_write_before_commit');
1183
+ const txStarted = performance.now();
1184
+ let result;
1185
+ try {
1186
+ result = await this.#executor.push(body, claims);
1187
+ }
1188
+ finally {
1189
+ this.#executorBeforeCommitFault = null;
1190
+ }
1191
+ transactionMs += performance.now() - txStarted;
1192
+ const mutationResults = 'mutations' in result.pushResponse ? result.pushResponse.mutations : [];
1193
+ for (const mutation of mutationResults) {
1194
+ if ('error' in mutation.result &&
1195
+ mutation.result.error === 'alreadyProcessed') {
1196
+ continue;
1186
1197
  }
1187
- catch (error) {
1188
- const isAppError = isMutationApplicationError(error);
1189
- if (!isAppError)
1190
- throw error;
1198
+ lmidAdvances++;
1199
+ if ('error' in mutation.result && mutation.result.error === 'app') {
1191
1200
  this.#counters.applicationErrors++;
1192
1201
  resultClass = 'application_error';
1193
- const appError = error;
1194
- const txStarted = performance.now();
1195
- await this.ctx.storage.transaction(async () => {
1196
- this.#wasm(() => engine_record_app_error(this.#engineDb, plan.clientGroupID, mutation.clientID, mutation.id, claims.userID));
1197
- });
1198
- transactionMs += performance.now() - txStarted;
1199
- lmidAdvances++;
1200
- results.push({
1201
- clientID: mutation.clientID,
1202
- id: mutation.id,
1203
- result: {
1204
- error: 'app',
1205
- message: appError.message,
1206
- details: appError.details ?? appError.message,
1207
- },
1208
- });
1209
- this.ctx.waitUntil(this.#enqueueWake(mutation.clientID));
1210
1202
  }
1203
+ this.ctx.waitUntil(this.#enqueueWake(mutation.id.clientID));
1211
1204
  }
1212
- if (plan.mutations.length > 0) {
1205
+ if (mutationResults.length > 0) {
1213
1206
  const txStarted = performance.now();
1214
1207
  await this.ctx.storage.transaction(async () => {
1215
1208
  this.#wasm(() => engine_prune(this.#engineDb, this.#retainChanges()));
@@ -1220,7 +1213,6 @@ export function createSyncDurableObject(config) {
1220
1213
  const afterCommitFault = this.#takeFault('push_after_commit_before_response');
1221
1214
  if (afterCommitFault)
1222
1215
  throw this.#faultError(afterCommitFault, 'push_after_commit_before_response');
1223
- const response = this.#wasm(() => engine_assemble_push_response(results));
1224
1216
  const state = this.#engineStateBestEffort();
1225
1217
  this.#log({
1226
1218
  namespaceHash: namespace,
@@ -1246,7 +1238,7 @@ export function createSyncDurableObject(config) {
1246
1238
  this.#dropNextPushResponse = false;
1247
1239
  return json({ error: 'intentionally dropped push response' }, 503);
1248
1240
  }
1249
- return json(response);
1241
+ return json(result);
1250
1242
  }
1251
1243
  catch (error) {
1252
1244
  const status = statusOf(error);
package/dist/index.d.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  export { createSyncDurableObject, createSyncWorker } from './host.js';
2
2
  export { createQueryCompiler } from './query-compiler.js';
3
- export { MutationApplicationError, isMutationApplicationError, registerMutators, } from './types.js';
3
+ export { visibility } from './types.js';
4
4
  export { DEFAULT_TRANSACTION_QUERY_BUDGET, TransactionQueryBudgetError, executeTransactionQueryPlan, executeTransactionQueryPlanAsync, } from './transaction-query.js';
5
- export type { DeferredEffect, JsonPrimitive, JsonValue, MutatorContext, MutatorRegistry, MutatorSql, NormalizedClaims, PullCaps, QueryResolver, RegisteredMutator, SyncHostConfig, SyncHostEnv, ServiceBinding, SQLiteExecResult, SqlStatementMetadata, SyncSql, UpstreamConfig, VisibilityConfig, VisibilityFilter, ZeroSchemaConfig, } from './types.js';
5
+ export type { PullCaps, QueryResolver, SyncHostConfig, SyncHostEnv, ServiceBinding, SyncSql, UpstreamConfig, VisibilityConfig, VisibilityExpression, VisibilityFilter, VisibilityOperand, VisibilityValue, } from './types.js';
6
6
  export type { SyncDurableObjectConstructor } from './host.js';
7
- export type { CompiledTransactionQueryNode, CompiledTransactionQueryPlan, CompiledTransactionQueryRelationship, TransactionQueryBinding, TransactionQueryBudget, TransactionQueryColumn, TransactionQueryColumnType, TransactionQueryExecutionOptions, TransactionQueryFormat, TransactionQueryWireValue, } from './transaction-query.js';
7
+ export type { CompiledTransactionQueryNode, CompiledTransactionQueryPlan, CompiledTransactionQueryRelationship, TransactionQueryBinding, TransactionQueryBudget, TransactionQueryColumn, TransactionQueryColumnType, TransactionQueryExecutionOptions, TransactionQueryWireValue, } from './transaction-query.js';
8
8
  export type { TransactionQueryCompiler } from './query-compiler.js';
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
1
  export { createSyncDurableObject, createSyncWorker } from './host.js';
2
2
  export { createQueryCompiler } from './query-compiler.js';
3
- export { MutationApplicationError, isMutationApplicationError, registerMutators, } from './types.js';
3
+ export { visibility } from './types.js';
4
4
  export { DEFAULT_TRANSACTION_QUERY_BUDGET, TransactionQueryBudgetError, executeTransactionQueryPlan, executeTransactionQueryPlanAsync, } from './transaction-query.js';
@@ -1,4 +1,4 @@
1
- import type { CompiledTransactionQueryPlan, TransactionQueryFormat } from './transaction-query.js';
2
- import type { ZeroSchemaConfig } from './types.js';
1
+ import type { CompiledTransactionQueryPlan } from './transaction-query.js';
2
+ import type { TransactionQueryFormat, ZeroSchemaConfig } from 'orez-sync-executor';
3
3
  export type TransactionQueryCompiler = (ast: unknown, format: TransactionQueryFormat) => CompiledTransactionQueryPlan;
4
4
  export declare function createQueryCompiler(schema: ZeroSchemaConfig): TransactionQueryCompiler;
@@ -1,5 +1,7 @@
1
- import type { CompiledTransactionQueryPlan, TransactionQueryBudget, TransactionQueryFormat } from './transaction-query.js';
2
- import type { MutatorSql, SQLiteExecResult, SqlStatementMetadata, SyncSql } from './types.js';
1
+ import type { CompiledTransactionQueryPlan, TransactionQueryBudget } from './transaction-query.js';
2
+ import type { SyncSql } from './types.js';
3
+ import type { ApplicationTransaction, ExecResult, SqlStatementMetadata } from 'orez-sync-executor';
4
+ import type { TransactionQueryFormat } from 'orez-sync-executor';
3
5
  type WireValue = {
4
6
  kind: 'null';
5
7
  } | {
@@ -49,7 +51,7 @@ export declare class SqlStorageDirect implements SyncSql {
49
51
  private readonly sql;
50
52
  private readonly recordRowsWritten;
51
53
  constructor(sql: SqlStorage, recordRowsWritten?: (rows: number) => void);
52
- exec(sql: string, params?: readonly unknown[], _metadata?: SqlStatementMetadata): SQLiteExecResult;
54
+ exec(sql: string, params?: readonly unknown[], _metadata?: SqlStatementMetadata): ExecResult;
53
55
  query<Row extends Record<string, unknown> = Record<string, unknown>>(sql: string, params?: readonly unknown[]): Row[];
54
56
  }
55
57
  /**
@@ -57,12 +59,12 @@ export declare class SqlStorageDirect implements SyncSql {
57
59
  * materializes its SqlStorage operation before its promise resolves, so an
58
60
  * existing `await tx.query(...)` cannot carry a cursor across an await.
59
61
  */
60
- export declare class SqlStorageMutatorTransaction implements MutatorSql {
62
+ export declare class SqlStorageMutatorTransaction implements ApplicationTransaction {
61
63
  private readonly direct;
62
64
  private readonly compileQuery;
63
65
  private readonly queryBudget?;
64
66
  constructor(direct: SqlStorageDirect, compileQuery: (ast: unknown, format: TransactionQueryFormat) => CompiledTransactionQueryPlan, queryBudget?: Partial<TransactionQueryBudget> | undefined);
65
- exec(sql: string, params?: readonly unknown[], metadata?: SqlStatementMetadata): Promise<SQLiteExecResult>;
67
+ exec(sql: string, params?: readonly unknown[], metadata?: SqlStatementMetadata): Promise<ExecResult>;
66
68
  query<Row extends Record<string, unknown> = Record<string, unknown>>(sql: string, params?: readonly unknown[]): Promise<Row[]>;
67
69
  queryAst<Result = unknown>(ast: unknown, format: TransactionQueryFormat, queryName?: string): Promise<Result>;
68
70
  }
@@ -50,10 +50,6 @@ export type TransactionQueryExecutionOptions = {
50
50
  queryName?: string;
51
51
  budget?: Partial<TransactionQueryBudget>;
52
52
  };
53
- export type TransactionQueryFormat = {
54
- singular: boolean;
55
- relationships: Readonly<Record<string, TransactionQueryFormat>>;
56
- };
57
53
  export declare const DEFAULT_TRANSACTION_QUERY_BUDGET: TransactionQueryBudget;
58
54
  export declare class TransactionQueryBudgetError extends Error {
59
55
  readonly query: string;
package/dist/types.d.ts CHANGED
@@ -1,73 +1,12 @@
1
- import type { DeferredEffect } from './post-commit.js';
2
- import type { TransactionQueryBudget, TransactionQueryFormat } from './transaction-query.js';
3
- export type { DeferredEffect } from './post-commit.js';
4
- export type JsonPrimitive = string | number | boolean | null;
5
- export type JsonValue = JsonPrimitive | JsonValue[] | {
6
- [key: string]: JsonValue;
7
- };
8
- export type NormalizedClaims = {
9
- /** Stable consumer user id used for client-group ownership. */
10
- userID: string;
11
- [claim: string]: JsonValue;
12
- };
13
- export type ZeroSchemaConfig = {
14
- readonly tables: Readonly<Record<string, {
15
- readonly name?: string;
16
- readonly serverName?: string;
17
- readonly columns: Readonly<Record<string, {
18
- readonly type: string;
19
- readonly serverName?: string;
20
- }>>;
21
- readonly primaryKey: readonly string[];
22
- }>>;
23
- };
24
- /**
25
- * Explicit identity for a SQLite write to a published application table.
26
- *
27
- * The physical table installs CDC. `publicTable` is the logical Zero name
28
- * emitted in the change feed. This replaces metadata recovered by parsing
29
- * PostgreSQL statements.
30
- */
31
- export type SqlStatementMetadata = {
32
- table: string;
33
- publicTable: string;
34
- kind: 'insert' | 'update' | 'delete' | 'upsert';
35
- };
36
- export type SQLiteExecResult = {
37
- changes: number;
38
- };
1
+ import type { TransactionQueryBudget } from './transaction-query.js';
2
+ import type { Schema } from '@rocicorp/zero';
3
+ import type { ExecResult, JsonValue, MutatorRegistry, NormalizedClaims, SqlStatementMetadata, VisibilityConfig } from 'orez-sync-executor';
4
+ export { visibility, type VisibilityExpression, type VisibilityFilter, type VisibilityOperand, type VisibilityValue, } from './visibility.js';
5
+ export type { VisibilityConfig } from 'orez-sync-executor';
39
6
  export interface SyncSql {
40
- exec(sql: string, params?: readonly unknown[], metadata?: SqlStatementMetadata): SQLiteExecResult;
7
+ exec(sql: string, params?: readonly unknown[], metadata?: SqlStatementMetadata): ExecResult;
41
8
  query<Row extends Record<string, unknown> = Record<string, unknown>>(sql: string, params?: readonly unknown[]): Row[];
42
9
  }
43
- export interface MutatorSql {
44
- exec(sql: string, params?: readonly unknown[], metadata?: SqlStatementMetadata): Promise<SQLiteExecResult>;
45
- query<Row extends Record<string, unknown> = Record<string, unknown>>(sql: string, params?: readonly unknown[]): Promise<Row[]>;
46
- /** Execute a validated Zero AST inside the current application transaction. */
47
- queryAst<Result = unknown>(ast: JsonValue, format: TransactionQueryFormat, queryName?: string): Promise<Result>;
48
- }
49
- export type MutatorContext = {
50
- claims: NormalizedClaims;
51
- /** Current Zero mutation identity, when invoked by the production host. */
52
- clientID?: string;
53
- mutationID?: string | number;
54
- defer(effect: DeferredEffect): void;
55
- };
56
- export type RegisteredMutator = (tx: MutatorSql, args: JsonValue, context: MutatorContext) => void | Promise<void>;
57
- export type MutatorRegistry = Readonly<Record<string, RegisteredMutator>>;
58
- /** Preserve mutator names while making the host-facing registry immutable. */
59
- export declare function registerMutators<const Registry extends Record<string, RegisteredMutator>>(registry: Registry): Readonly<Registry>;
60
- export { MutationApplicationError, isMutationApplicationError } from './mutation-error.js';
61
- export type VisibilityFilter = {
62
- /** SQL WHERE fragment only, without the WHERE keyword. */
63
- sql: string;
64
- params?: readonly JsonPrimitive[];
65
- };
66
- export type VisibilityConfig = {
67
- /** True only when every predicate depends on the selected row alone. */
68
- rowLocal: boolean | ((claims: NormalizedClaims) => boolean);
69
- filter(table: string, claims: NormalizedClaims): VisibilityFilter | undefined;
70
- };
71
10
  export type QueryResolver = (name: string, args: readonly JsonValue[], claims: NormalizedClaims, env: SyncHostEnv) => JsonValue | Promise<JsonValue>;
72
11
  export type PullCaps = {
73
12
  maxChangeRows: number;
@@ -108,10 +47,10 @@ export type DelegatedPushRetryConfig = {
108
47
  /** Per-attempt service-binding timeout. Defaults to 5,000ms. */
109
48
  timeoutMs?: number;
110
49
  };
111
- export type SyncHostConfig<Env extends SyncHostEnv = SyncHostEnv> = {
50
+ export type SyncHostConfig<Env extends SyncHostEnv = SyncHostEnv, S extends Schema = Schema> = {
112
51
  hostVersion: string;
113
- schema: ZeroSchemaConfig;
114
- mutators?: MutatorRegistry;
52
+ schema: S;
53
+ mutators?: MutatorRegistry<S>;
115
54
  /**
116
55
  * absolute app push path on the delegated mutation service. a successful
117
56
  * response must be causally visible through the configured upstream data
@@ -132,6 +71,8 @@ export type SyncHostConfig<Env extends SyncHostEnv = SyncHostEnv> = {
132
71
  /** Application DDL and optional seed, called before sync-core schema init. */
133
72
  initialize(sql: SyncSql): void;
134
73
  authenticate(request: Request, env: Env): NormalizedClaims | null | Promise<NormalizedClaims | null>;
74
+ /** Authorize authenticated application access before selecting a namespace DO. */
75
+ authorize(request: Request, claims: NormalizedClaims, namespace: string, env: Env): boolean | Promise<boolean>;
135
76
  /** Authorize the advisory wake socket before selecting a namespace DO.
136
77
  * Browser clients should present a short-lived, namespace-scoped capability
137
78
  * in the query string because WebSocket cannot set request headers. */
package/dist/types.js CHANGED
@@ -1,5 +1 @@
1
- /** Preserve mutator names while making the host-facing registry immutable. */
2
- export function registerMutators(registry) {
3
- return Object.freeze({ ...registry });
4
- }
5
- export { MutationApplicationError, isMutationApplicationError } from './mutation-error.js';
1
+ export { visibility, } from './visibility.js';
@@ -0,0 +1,12 @@
1
+ import type { VisibilityExpression, VisibilityFilter, VisibilityOperand, VisibilityValue } from 'orez-sync-executor';
2
+ export type { VisibilityExpression, VisibilityFilter, VisibilityOperand, VisibilityValue, } from 'orez-sync-executor';
3
+ export declare const visibility: {
4
+ readonly column: (table: string, column: string, qualifier?: string) => VisibilityOperand;
5
+ readonly value: (value: VisibilityValue) => VisibilityOperand;
6
+ readonly comparison: (left: VisibilityOperand, operator: "=" | "!=" | "<" | ">" | "<=" | ">=" | "IS" | "IS NOT", right: VisibilityOperand) => VisibilityExpression;
7
+ readonly and: (...conditions: VisibilityExpression[]) => VisibilityExpression;
8
+ readonly or: (...conditions: VisibilityExpression[]) => VisibilityExpression;
9
+ readonly exists: (table: string, where: VisibilityExpression, qualifier?: string) => VisibilityExpression;
10
+ readonly filter: (expression: VisibilityExpression) => VisibilityFilter;
11
+ readonly raw: (sql: string, params?: readonly VisibilityValue[]) => VisibilityFilter;
12
+ };
@@ -0,0 +1,30 @@
1
+ export const visibility = {
2
+ column(table, column, qualifier) {
3
+ return qualifier
4
+ ? { type: 'column', table, column, qualifier }
5
+ : { type: 'column', table, column };
6
+ },
7
+ value(value) {
8
+ return { type: 'value', value };
9
+ },
10
+ comparison(left, operator, right) {
11
+ return { type: 'comparison', operator, left, right };
12
+ },
13
+ and(...conditions) {
14
+ return { type: 'and', conditions };
15
+ },
16
+ or(...conditions) {
17
+ return { type: 'or', conditions };
18
+ },
19
+ exists(table, where, qualifier) {
20
+ return qualifier
21
+ ? { type: 'exists', table, qualifier, where }
22
+ : { type: 'exists', table, where };
23
+ },
24
+ filter(expression) {
25
+ return { kind: 'expression', expression };
26
+ },
27
+ raw(sql, params = []) {
28
+ return { kind: 'raw', sql, params };
29
+ },
30
+ };
package/package.json CHANGED
@@ -1,20 +1,12 @@
1
1
  {
2
2
  "name": "orez-sync-cf-host",
3
- "version": "0.5.30",
3
+ "version": "0.5.34",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
7
7
  "types": "./dist/index.d.ts",
8
8
  "import": "./dist/index.js"
9
9
  },
10
- "./post-commit": {
11
- "types": "./dist/post-commit.d.ts",
12
- "import": "./dist/post-commit.js"
13
- },
14
- "./mutation-error": {
15
- "types": "./dist/mutation-error.d.ts",
16
- "import": "./dist/mutation-error.js"
17
- },
18
10
  "./node-wasm-loader": {
19
11
  "types": "./dist/node-wasm-loader.d.ts",
20
12
  "import": "./dist/node-wasm-loader.js"
@@ -31,6 +23,10 @@
31
23
  "types": "./dist/transaction-query.d.ts",
32
24
  "import": "./dist/transaction-query.js"
33
25
  },
26
+ "./visibility": {
27
+ "types": "./dist/visibility.d.ts",
28
+ "import": "./dist/visibility.js"
29
+ },
34
30
  "./vite-wasm-loader": {
35
31
  "types": "./dist/vite-wasm-loader.d.ts",
36
32
  "import": "./dist/vite-wasm-loader.js"
@@ -49,7 +45,7 @@
49
45
  "bundle": "bun run build:dist && wrangler deploy --dry-run --outdir .wrangler/bundle",
50
46
  "measure": "bun run build:dist && bun measure.mjs",
51
47
  "test": "bun run test:config && bun run test:query-compiler && bun run test:package && bun run test:platform && bun run test:integration && bun run test:ingest && bun run test:restart",
52
- "test:config": "bun test config.test.mjs write-safeguards.test.mjs mutation-error.test.mjs post-commit.test.mjs transaction-query.test.mjs",
48
+ "test:config": "bun test config.test.mjs write-safeguards.test.mjs transaction-query.test.mjs",
53
49
  "test:integration": "bun run build:dist && bun integration-test.mjs",
54
50
  "test:ingest": "bun run build:dist && bun ingest-test.mjs",
55
51
  "test:package": "bun run build:dist && node package-runtime-test.mjs",
@@ -58,6 +54,9 @@
58
54
  "test:platform": "bun run build:dist:platform && bun platform-test.mjs",
59
55
  "typecheck": "bun run build:dist:platform && tsc --noEmit"
60
56
  },
57
+ "dependencies": {
58
+ "orez-sync-executor": "0.5.34"
59
+ },
61
60
  "devDependencies": {
62
61
  "@cloudflare/workers-types": "4.20260617.1",
63
62
  "@types/bun": "1.3.14",
@@ -67,6 +66,7 @@
67
66
  "wrangler": "4.103.0"
68
67
  },
69
68
  "peerDependencies": {
69
+ "@rocicorp/zero": ">=1.7.0",
70
70
  "vite": ">=5"
71
71
  },
72
72
  "peerDependenciesMeta": {
@@ -1,9 +0,0 @@
1
- export declare class MutationApplicationError extends Error {
2
- readonly details: string;
3
- constructor(details: string, message?: string);
4
- }
5
- export declare function isMutationApplicationError(error: unknown): error is {
6
- name: string;
7
- message: string;
8
- details: string;
9
- };
@@ -1,23 +0,0 @@
1
- // the app-rejection contract shared by every orez sync host and by
2
- // protocol-neutral authoring layers (on-zero adapters). classification is
3
- // STRUCTURAL, never instanceof: adapters must not depend on orez to reject a
4
- // mutation, and instanceof breaks across package copies (dual-package
5
- // installs, --into tarballs, workspace duplication). any error whose name is
6
- // 'MutationApplicationError' with string details/message is claiming this
7
- // contract deliberately.
8
- export class MutationApplicationError extends Error {
9
- details;
10
- constructor(details, message = details) {
11
- super(message);
12
- this.details = details;
13
- this.name = 'MutationApplicationError';
14
- }
15
- }
16
- export function isMutationApplicationError(error) {
17
- if (typeof error !== 'object' || error === null)
18
- return false;
19
- const shaped = error;
20
- return (shaped.name === 'MutationApplicationError' &&
21
- typeof shaped.message === 'string' &&
22
- typeof shaped.details === 'string');
23
- }
@@ -1,13 +0,0 @@
1
- export type DeferredEffect = () => void | Promise<void>;
2
- export type PostCommitEffects = {
3
- /** discard effects from a storage-transaction attempt before starting another */
4
- beginAttempt(): void;
5
- defer(effect: DeferredEffect): void;
6
- /** run the committed attempt's effects in order without rejecting */
7
- runAfterCommit(onError: (error: unknown) => void): Promise<void>;
8
- };
9
- /**
10
- * collect external work during a retryable transaction and run only the effects
11
- * belonging to the attempt that commits.
12
- */
13
- export declare function createPostCommitEffects(): PostCommitEffects;
@@ -1,31 +0,0 @@
1
- /**
2
- * collect external work during a retryable transaction and run only the effects
3
- * belonging to the attempt that commits.
4
- */
5
- export function createPostCommitEffects() {
6
- const effects = [];
7
- return {
8
- beginAttempt() {
9
- effects.length = 0;
10
- },
11
- defer(effect) {
12
- effects.push(effect);
13
- },
14
- async runAfterCommit(onError) {
15
- const committed = effects.splice(0);
16
- for (const effect of committed) {
17
- try {
18
- await effect();
19
- }
20
- catch (error) {
21
- try {
22
- onError(error);
23
- }
24
- catch {
25
- // reporting must not turn a committed effect failure into a rejection
26
- }
27
- }
28
- }
29
- },
30
- };
31
- }