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 +4 -3
- package/dist/host.d.ts +2 -1
- package/dist/host.js +53 -35
- package/package.json +1 -1
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
|
|
11
|
-
|
|
12
|
-
|
|
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
|
|
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
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
103
|
-
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
1444
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|