orez-sync-cf-host 0.5.28 → 0.5.30

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
@@ -7,9 +7,10 @@ from `src/harness-worker.ts`. Consumers provide a JSON Zero schema, application
7
7
  DDL/seed initializer, normalized-claims authenticator, mutator registry, and an
8
8
  optional row-visibility hook.
9
9
 
10
- The Worker authenticates at the consumer edge, forwards only normalized claims
11
- to a per-namespace Durable Object, and never logs tokens, mutation arguments, or
12
- row contents. Pull runs `engine_handle_pull` inside `transactionSync`; push runs
10
+ The Worker authenticates at the consumer edge and forwards normalized claims
11
+ inside the binding request body, where request-header observability cannot record
12
+ them. It never logs tokens, mutation arguments, or row contents. Pull runs
13
+ `engine_handle_pull` inside `transactionSync`; push runs
13
14
  Rust `push_validate`/`preflight`/`finalize`/`record_app_error` steps around the
14
15
  registered asynchronous TypeScript mutator inside `ctx.storage.transaction`.
15
16
  Every SQL cursor is materialized before an await. Mutators may await only their
package/dist/host.d.ts CHANGED
@@ -2,7 +2,8 @@ import { DurableObject } from 'cloudflare:workers';
2
2
  import type { SyncHostConfig, SyncHostEnv } from './types.js';
3
3
  /**
4
4
  * Create the consumer-facing Worker router. Authentication happens here; the
5
- * Durable Object receives only normalized claims over a binding-private header.
5
+ * Durable Object receives normalized claims inside the binding request body so
6
+ * observability systems cannot record them as request-header metadata.
6
7
  */
7
8
  export declare function createSyncWorker<Env extends SyncHostEnv>(config: SyncHostConfig<Env>): ExportedHandler<Env>;
8
9
  export interface SyncDurableObjectConstructor<Env extends SyncHostEnv> {
package/dist/host.js CHANGED
@@ -6,7 +6,6 @@ import { decodeSqlParams, SqlStorageDirect, SqlStorageMutatorTransaction, SqlSto
6
6
  import { isMutationApplicationError } from './types.js';
7
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';
8
8
  import { IngestBreakerError, IngestCircuitBreaker, retryDelayMs, shouldRetryDelegatedPush, } from './write-safeguards.js';
9
- const CLAIMS_HEADER = 'x-orez-sync-claims';
10
9
  const NAMESPACE_HEADER = 'x-orez-sync-namespace';
11
10
  const UPSTREAM_PATH_HEADER = 'x-orez-sync-upstream-path';
12
11
  const DEFAULT_SNAPSHOT_PAGE_ROWS = 2_000;
@@ -89,19 +88,38 @@ function routeAfterNamespace(pathname) {
89
88
  const [, , ...parts] = pathname.split('/');
90
89
  return `/${parts.join('/')}`;
91
90
  }
92
- function claimsFromRequest(request) {
93
- const encoded = request.headers.get(CLAIMS_HEADER);
94
- if (!encoded)
95
- return null;
96
- try {
97
- const value = JSON.parse(decodeURIComponent(encoded));
98
- return value && typeof value.userID === 'string' && value.userID.length > 0
99
- ? value
100
- : null;
91
+ function jsonBodyRequest(request, headers, body) {
92
+ headers.delete('content-encoding');
93
+ headers.delete('content-length');
94
+ headers.set('content-type', 'application/json');
95
+ return new Request(request.url, {
96
+ method: request.method,
97
+ headers,
98
+ body: JSON.stringify(body),
99
+ });
100
+ }
101
+ async function forwardedSyncRequest(request) {
102
+ const value = await requestObject(request);
103
+ const claims = value.claims;
104
+ const body = value.body;
105
+ const userID = claims && typeof claims === 'object' && !Array.isArray(claims)
106
+ ? claims.userID
107
+ : null;
108
+ if (!claims ||
109
+ typeof claims !== 'object' ||
110
+ Array.isArray(claims) ||
111
+ typeof userID !== 'string' ||
112
+ userID.length === 0) {
113
+ throw requestError('missing normalized claims', 401);
101
114
  }
102
- catch {
103
- return null;
115
+ if (!body || typeof body !== 'object' || Array.isArray(body)) {
116
+ throw requestError('request body must be a JSON object');
104
117
  }
118
+ const headers = new Headers(request.headers);
119
+ return {
120
+ claims: claims,
121
+ request: jsonBodyRequest(request, headers, body),
122
+ };
105
123
  }
106
124
  async function namespaceHash(namespace) {
107
125
  const bytes = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(namespace));
@@ -124,7 +142,8 @@ function socketCloseQuietly(socket, code, reason) {
124
142
  }
125
143
  /**
126
144
  * Create the consumer-facing Worker router. Authentication happens here; the
127
- * Durable Object receives only normalized claims over a binding-private header.
145
+ * Durable Object receives normalized claims inside the binding request body so
146
+ * observability systems cannot record them as request-header metadata.
128
147
  */
129
148
  export function createSyncWorker(config) {
130
149
  validateSyncHostConfig(config);
@@ -153,14 +172,22 @@ export function createSyncWorker(config) {
153
172
  return json({ error: 'forbidden' }, 403);
154
173
  }
155
174
  const headers = new Headers(request.headers);
156
- headers.delete(CLAIMS_HEADER);
157
175
  headers.delete(NAMESPACE_HEADER);
158
176
  headers.delete(UPSTREAM_PATH_HEADER);
177
+ let forwardedBody = null;
159
178
  if (!isAdmin && route !== '/wake' && route !== '/notify') {
160
179
  const claims = await config.authenticate(request, env);
161
180
  if (!claims)
162
181
  return json({ error: 'missing authentication' }, 401);
163
- headers.set(CLAIMS_HEADER, encodeURIComponent(JSON.stringify(claims)));
182
+ if ((route === '/pull' || route === '/push') && request.method === 'POST') {
183
+ try {
184
+ const body = await requestObject(request);
185
+ forwardedBody = { claims, body };
186
+ }
187
+ catch (error) {
188
+ return json(errorBody(error), statusOf(error));
189
+ }
190
+ }
164
191
  }
165
192
  headers.set(NAMESPACE_HEADER, await namespaceHash(namespace));
166
193
  if (config.upstream) {
@@ -172,7 +199,9 @@ export function createSyncWorker(config) {
172
199
  }
173
200
  headers.set(UPSTREAM_PATH_HEADER, namespacePath.replace(/\/$/, ''));
174
201
  }
175
- const forwarded = new Request(request, { headers });
202
+ const forwarded = forwardedBody
203
+ ? jsonBodyRequest(request, headers, forwardedBody)
204
+ : new Request(request, { headers });
176
205
  const id = env.SYNC_DO.idFromName(namespace);
177
206
  return env.SYNC_DO.get(id).fetch(forwarded);
178
207
  },
@@ -1028,7 +1057,6 @@ export function createSyncDurableObject(config) {
1028
1057
  return json(plan.response);
1029
1058
  const endpoint = new URL(`${upstreamPath ?? ''}${config.mutateUrl}`, config.mutateOrigin ?? 'https://upstream.invalid');
1030
1059
  const headers = new Headers(request.headers);
1031
- headers.delete(CLAIMS_HEADER);
1032
1060
  headers.delete(NAMESPACE_HEADER);
1033
1061
  headers.delete(UPSTREAM_PATH_HEADER);
1034
1062
  headers.set('host', endpoint.host);
@@ -1440,35 +1468,25 @@ export function createSyncDurableObject(config) {
1440
1468
  return json(errorBody(error), statusOf(error));
1441
1469
  }
1442
1470
  }
1443
- const claims = claimsFromRequest(request);
1444
- if (!claims)
1445
- return json({ error: 'missing normalized claims' }, 401);
1446
- if (route === '/pull' && request.method === 'POST') {
1471
+ if ((route === '/pull' || route === '/push') && request.method === 'POST') {
1472
+ let forwarded;
1447
1473
  try {
1448
- await this.#ingest(upstreamPath);
1474
+ forwarded = await forwardedSyncRequest(request);
1449
1475
  }
1450
1476
  catch (error) {
1451
- // Workerd requires a forwarded request body to be consumed even when
1452
- // ingest fails before the pull handler parses it.
1453
- await request.arrayBuffer();
1454
1477
  return json(errorBody(error), statusOf(error));
1455
1478
  }
1456
- return this.#pull(request, claims, namespace);
1457
- }
1458
- if (route === '/push' && request.method === 'POST') {
1459
- // A push may be the first request for a fresh namespace. DATA's
1460
- // /changes call is also its schema-provisioning barrier, so complete it
1461
- // before delegating the mutation to APP. Pull already enforces this
1462
- // ordering above; skipping it here let APP race half-created tables and
1463
- // surface a terminal, bodyless 500 to Zero.
1479
+ // pull and push both establish the upstream schema and snapshot barrier.
1464
1480
  try {
1465
1481
  await this.#ingest(upstreamPath);
1466
1482
  }
1467
1483
  catch (error) {
1468
- await request.arrayBuffer();
1469
1484
  return json(errorBody(error), statusOf(error));
1470
1485
  }
1471
- return this.#push(request, claims, namespace, upstreamPath);
1486
+ if (route === '/pull') {
1487
+ return this.#pull(forwarded.request, forwarded.claims, namespace);
1488
+ }
1489
+ return this.#push(forwarded.request, forwarded.claims, namespace, upstreamPath);
1472
1490
  }
1473
1491
  return json({ error: 'not found' }, 404);
1474
1492
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "orez-sync-cf-host",
3
- "version": "0.5.28",
3
+ "version": "0.5.30",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {