@sekiban/dcb-runtime 0.2.0 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -1
- package/dist/allocator/AllocatorDurableObject.js +25 -2
- package/dist/bootstrap/BootstrapCoordinatorDurableObject.d.ts +2 -0
- package/dist/bootstrap/BootstrapCoordinatorDurableObject.js +64 -1
- package/dist/bootstrap/OperatorBootstrap.d.ts +5 -1
- package/dist/bootstrap/OperatorBootstrap.js +17 -10
- package/dist/bootstrap/types.d.ts +6 -0
- package/dist/cli/OperatorRepairCli.d.ts +6 -1
- package/dist/cli/OperatorRepairCli.js +26 -6
- package/dist/cloudflare.d.ts +5 -0
- package/dist/cloudflare.js +1567 -465
- package/dist/commit/CommitWorker.d.ts +19 -0
- package/dist/commit/CommitWorker.js +176 -27
- package/dist/completeness/IncidentLifecycle.d.ts +75 -0
- package/dist/completeness/IncidentLifecycle.js +503 -0
- package/dist/completeness/types.d.ts +67 -0
- package/dist/cosmos.d.ts +34 -4
- package/dist/cosmos.js +214 -49
- package/dist/downstream/OutboxDrain.d.ts +4 -1
- package/dist/downstream/OutboxDrain.js +24 -2
- package/dist/generated/cosmos-layout.d.ts +87 -0
- package/dist/generated/cosmos-layout.js +99 -0
- package/dist/http/ConfiguredBearer.d.ts +12 -0
- package/dist/http/ConfiguredBearer.js +33 -0
- package/dist/http/IncidentMaintenance.d.ts +8 -0
- package/dist/http/IncidentMaintenance.js +124 -0
- package/dist/http/SerializedQueryWorker.js +37 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +1627 -465
- package/dist/read/SerializedReadWorker.d.ts +3 -0
- package/dist/read/SerializedReadWorker.js +3 -3
- package/dist/shard/ShardRotation.d.ts +48 -0
- package/dist/shard/ShardRotation.js +120 -0
- package/dist/store/CosmosEventStore.js +56 -46
- package/dist/store/PostgresEventStore.d.ts +20 -0
- package/dist/store/PostgresEventStore.js +53 -0
- package/dist/store/provider.d.ts +4 -0
- package/dist/tag/TagDurableObject.js +66 -14
- package/dist/tag/TagSqlSchema.d.ts +6 -1
- package/dist/tag/TagSqlSchema.js +15 -0
- package/dist/tag/types.d.ts +9 -0
- package/dist/trace/CommitTrace.d.ts +3 -3
- package/dist/trace/CommitTraceVerifier.js +1 -1
- package/package.json +6 -15
package/README.md
CHANGED
|
@@ -5,9 +5,40 @@ Cloudflare Durable Object and HTTP runtime for Serialized DCB V1.
|
|
|
5
5
|
This package is the matched-set runtime companion to `@sekiban/dcb-core`,
|
|
6
6
|
`@sekiban/dcb-domain`, and `@sekiban/dcb-client`. Consumers such as the
|
|
7
7
|
meeting-room Cloudflare sample import public entry points (`@sekiban/dcb-runtime`,
|
|
8
|
-
`@sekiban/dcb-runtime/cloudflare`, `/d1`, `/d1-mv`, `/mv`, `/cosmos`) and re-export
|
|
8
|
+
`@sekiban/dcb-runtime/cloudflare`, `/d1`, `/d1-mv`, `/mv`, and the experimental `/cosmos`) and re-export
|
|
9
9
|
Durable Object classes from their Worker module graph as Cloudflare requires.
|
|
10
10
|
|
|
11
|
+
The experimental Cosmos provider is never selected by environment values. A
|
|
12
|
+
consumer must import `createCosmosStoreProvider` from `/cosmos` and pass it as
|
|
13
|
+
`createRuntimeWorker({ storeProvider })`.
|
|
14
|
+
|
|
15
|
+
## Query parameters
|
|
16
|
+
|
|
17
|
+
A scalar query accepts only a parsed empty object. A list query accepts only
|
|
18
|
+
`PageNumber`, `PageSize`, `NewestFirst`, and `consistency`. Any other parsed
|
|
19
|
+
scalar value, and any unknown list key, is HTTP 400 `query_params_unsupported`
|
|
20
|
+
with the message "The selected query does not support these parameters." The
|
|
21
|
+
check runs after JSON parsing and endpoint matching and before projector
|
|
22
|
+
lookup or a store read, on every backing. The V1 response envelope is
|
|
23
|
+
unchanged. Typed query handlers are planned for `0.3.0`; their API is not
|
|
24
|
+
defined here.
|
|
25
|
+
|
|
26
|
+
## Configured bearer checks
|
|
27
|
+
|
|
28
|
+
`checkConfiguredBearer(request, binding, options?)` is exported from
|
|
29
|
+
`@sekiban/dcb-runtime` and `@sekiban/dcb-runtime/cloudflare`. `binding` is
|
|
30
|
+
`unknown`. The result is `"unconfigured" | "missing" | "invalid" | "authorized"`.
|
|
31
|
+
Callers choose the HTTP status. A binding is configured only when it is a
|
|
32
|
+
string containing a non-whitespace character. The default normalization keeps
|
|
33
|
+
the original bytes. `{ tokenNormalization: "trim" }` is the only other mode
|
|
34
|
+
and is reserved for incident maintenance, which already trimmed its token.
|
|
35
|
+
|
|
36
|
+
The helper hashes the complete expected `Bearer ` credential and the complete
|
|
37
|
+
supplied `Authorization` header with global WebCrypto SHA-256, then compares
|
|
38
|
+
every digest byte. It does not use Node crypto. Scheme spelling and case are
|
|
39
|
+
exact. Treat `unconfigured` as disabled on every exposed shared-secret route.
|
|
40
|
+
Do not compare a blank binding as if it were a credential.
|
|
41
|
+
|
|
11
42
|
## License
|
|
12
43
|
|
|
13
44
|
Elastic License 2.0. See [LICENSE](./LICENSE).
|
|
@@ -10,6 +10,8 @@ const ATTEMPT_KEY_PREFIX = "attempt:";
|
|
|
10
10
|
const ROLLBACK_WARNING_WINDOW_MS = 1000n;
|
|
11
11
|
class AllocationTransactionFault extends Error {
|
|
12
12
|
}
|
|
13
|
+
class ImportSeedRequiredError extends Error {
|
|
14
|
+
}
|
|
13
15
|
function json(body, status = 200) {
|
|
14
16
|
return new Response(JSON.stringify(body), {
|
|
15
17
|
status,
|
|
@@ -55,7 +57,19 @@ function seedFrom(value) {
|
|
|
55
57
|
catch {
|
|
56
58
|
return { error: "highWatermark must be an allocator SUID" };
|
|
57
59
|
}
|
|
58
|
-
|
|
60
|
+
let storeMaximum;
|
|
61
|
+
if (value.storeMaximum !== undefined) {
|
|
62
|
+
if (!isNonEmptyString(value.storeMaximum))
|
|
63
|
+
return { error: "storeMaximum must be an allocator SUID" };
|
|
64
|
+
try {
|
|
65
|
+
decodeSuid(value.storeMaximum);
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return { error: "storeMaximum must be an allocator SUID" };
|
|
69
|
+
}
|
|
70
|
+
storeMaximum = value.storeMaximum;
|
|
71
|
+
}
|
|
72
|
+
return { value: { importId: value.importId, leaseEpoch: value.leaseEpoch, highWatermark: value.highWatermark, ...(storeMaximum !== undefined ? { storeMaximum } : {}) } };
|
|
59
73
|
}
|
|
60
74
|
function newAllocatorLineageId() {
|
|
61
75
|
// A fresh token is generated when the allocator has no durable state, so a
|
|
@@ -118,6 +132,7 @@ function allocateFrom(value) {
|
|
|
118
132
|
...(serviceId !== undefined ? { serviceId } : {}),
|
|
119
133
|
...(bootstrapCommandId !== undefined ? { bootstrapCommandId } : {}),
|
|
120
134
|
...(bootstrapEpoch !== undefined ? { bootstrapEpoch } : {}),
|
|
135
|
+
...(value.requiresImportSeed === true ? { requiresImportSeed: true } : {}),
|
|
121
136
|
},
|
|
122
137
|
};
|
|
123
138
|
}
|
|
@@ -519,6 +534,8 @@ export class AllocatorDurableObject {
|
|
|
519
534
|
const state = persistedState === undefined
|
|
520
535
|
? currentState(lineage)
|
|
521
536
|
: { ...persistedState, allocatorLineageId: lineage, schemaVersion: 5, bootstrapSeed: persistedState.bootstrapSeed ?? null, lastRollbackWarningFingerprint: persistedState.lastRollbackWarningFingerprint ?? null };
|
|
537
|
+
if (input.requiresImportSeed === true && state.bootstrapSeed === null)
|
|
538
|
+
throw new ImportSeedRequiredError();
|
|
522
539
|
// This is intentionally before the first transaction write. A clock
|
|
523
540
|
// failure therefore cannot leave a vector, watermark, or warning fact.
|
|
524
541
|
let clockTick;
|
|
@@ -609,6 +626,9 @@ export class AllocatorDurableObject {
|
|
|
609
626
|
return json(result.vector, result.created ? 201 : 200);
|
|
610
627
|
}
|
|
611
628
|
catch (failure) {
|
|
629
|
+
if (failure instanceof ImportSeedRequiredError) {
|
|
630
|
+
return error(409, "allocator_seed_required", "commit before import seed");
|
|
631
|
+
}
|
|
612
632
|
if (failure instanceof AllocationTransactionFault) {
|
|
613
633
|
return error(503, "simulated_allocation_crash", "Simulated interruption before durable allocation transaction commit");
|
|
614
634
|
}
|
|
@@ -649,7 +669,10 @@ export class AllocatorDurableObject {
|
|
|
649
669
|
}
|
|
650
670
|
if (state.allocatedWatermark !== null)
|
|
651
671
|
return { status: 409, body: { code: "allocator_seed_rejected", error: "allocator already allocating" } };
|
|
652
|
-
|
|
672
|
+
if (input.storeMaximum !== undefined && input.highWatermark < input.storeMaximum) {
|
|
673
|
+
return { status: 409, body: { code: "allocator_seed_below_store_max", error: "seed below store maximum" } };
|
|
674
|
+
}
|
|
675
|
+
const updated = { ...state, allocatedWatermark: input.highWatermark, bootstrapSeed: { importId: input.importId, leaseEpoch: input.leaseEpoch, highWatermark: input.highWatermark } };
|
|
653
676
|
await txn.put(STATE_KEY, updated);
|
|
654
677
|
return { status: 201, body: updated };
|
|
655
678
|
});
|
|
@@ -23,6 +23,8 @@ export declare class BootstrapCoordinatorDurableObject implements DurableObject
|
|
|
23
23
|
/** Operator abort retains the durable failed plan for audit/retry; it never clears target state. */
|
|
24
24
|
private abort;
|
|
25
25
|
private route;
|
|
26
|
+
private permit;
|
|
27
|
+
private permitRelease;
|
|
26
28
|
private command;
|
|
27
29
|
}
|
|
28
30
|
export {};
|
|
@@ -13,13 +13,22 @@ const epoch = (value) => typeof value === "number" && Number.isSafeInteger(value
|
|
|
13
13
|
const response = (body, status = 200) => new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json; charset=utf-8" } });
|
|
14
14
|
const reject = (code, message, status = 409) => response({ code, error: message }, status);
|
|
15
15
|
function empty(serviceId) {
|
|
16
|
-
return { schemaVersion: 1, status: "EMPTY", importId: null, targetServiceId: serviceId, allocatorLineageId: null, source: null, digest: null, manifest: null, progress: {}, leaseEpoch: 0, leaseUntil: null, failure: null, readyAt: null, normalInFlight: 0, normalCommands: {}, releasedCommands: {}, verifiedImportId: null, verifiedLeaseEpoch: null, storeCompletion: null };
|
|
16
|
+
return { schemaVersion: 1, status: "EMPTY", importId: null, targetServiceId: serviceId, allocatorLineageId: null, source: null, digest: null, manifest: null, progress: {}, leaseEpoch: 0, leaseUntil: null, failure: null, readyAt: null, normalInFlight: 0, normalCommands: {}, releasedCommands: {}, writePermits: {}, verifiedImportId: null, verifiedLeaseEpoch: null, storeCompletion: null };
|
|
17
17
|
}
|
|
18
18
|
function expired(control) { return control.leaseUntil !== null && control.leaseUntil <= Date.now(); }
|
|
19
19
|
function commands(control) { return { ...(control.normalCommands ?? {}) }; }
|
|
20
20
|
function released(control) { return { ...(control.releasedCommands ?? {}) }; }
|
|
21
|
+
function permits(control) { return { ...(control.writePermits ?? {}) }; }
|
|
21
22
|
function fault(body, point) { return object(body) && body.faultAt === point; }
|
|
22
23
|
function encodedBytes(value) { return new TextEncoder().encode(JSON.stringify(value)).byteLength; }
|
|
24
|
+
async function readBody(request) {
|
|
25
|
+
try {
|
|
26
|
+
return await request.json();
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
23
32
|
/** Per-service authority and linearization point for bootstrap and normal writes. */
|
|
24
33
|
export class BootstrapCoordinatorDurableObject {
|
|
25
34
|
ctx;
|
|
@@ -48,6 +57,10 @@ export class BootstrapCoordinatorDurableObject {
|
|
|
48
57
|
return this.traceCommand(request, serviceId, activation, observation, "finalize");
|
|
49
58
|
if (request.method === "POST" && url.pathname === "/command/release")
|
|
50
59
|
return this.traceCommand(request, serviceId, activation, observation, "release");
|
|
60
|
+
if (request.method === "POST" && url.pathname === "/command/permit")
|
|
61
|
+
return this.permit(serviceId, await readBody(request));
|
|
62
|
+
if (request.method === "POST" && url.pathname === "/command/permit-release")
|
|
63
|
+
return this.permitRelease(serviceId, await readBody(request));
|
|
51
64
|
let body;
|
|
52
65
|
try {
|
|
53
66
|
body = await request.json();
|
|
@@ -119,6 +132,8 @@ export class BootstrapCoordinatorDurableObject {
|
|
|
119
132
|
return { error: "bootstrap_ready_permanent" };
|
|
120
133
|
if (control.status !== "EMPTY")
|
|
121
134
|
return control.importId === importId && control.digest === dump.manifest.contentDigest ? { control } : { error: "bootstrap_plan_conflict" };
|
|
135
|
+
if (Object.keys(permits(control)).length !== 0)
|
|
136
|
+
return { error: "bootstrap_write_permit_active" };
|
|
122
137
|
if (control.normalInFlight !== 0 || Object.keys(commands(control)).length !== 0)
|
|
123
138
|
return { error: "bootstrap_normal_command_in_flight" };
|
|
124
139
|
const next = { ...control, status: "PLANNED", importId, allocatorLineageId: dump.manifest.target.allocatorLineageId, source: dump.manifest.source, digest: dump.manifest.contentDigest, manifest: dump.manifest, leaseEpoch: control.leaseEpoch + 1, leaseUntil: Date.now() + 30_000, failure: null, progress: {}, verifiedImportId: null, verifiedLeaseEpoch: null, storeCompletion: null };
|
|
@@ -315,6 +330,54 @@ export class BootstrapCoordinatorDurableObject {
|
|
|
315
330
|
const control = await this.control(serviceId);
|
|
316
331
|
return control.status === "EMPTY" || control.status === "READY" ? response({ admitted: true, leaseEpoch: control.leaseEpoch, route: body.route }) : reject("bootstrap_route_rejected", `${body.route} is unavailable during bootstrap`);
|
|
317
332
|
}
|
|
333
|
+
async permit(serviceId, body) {
|
|
334
|
+
if (!object(body) || !string(body.commandId) || !string(body.digest))
|
|
335
|
+
return reject("bootstrap_permit_invalid", "commandId and digest are required", 400);
|
|
336
|
+
const commandId = body.commandId;
|
|
337
|
+
const digest = body.digest;
|
|
338
|
+
const result = await this.ctx.storage.transaction(async (txn) => {
|
|
339
|
+
const control = (await txn.get(CONTROL)) ?? empty(serviceId);
|
|
340
|
+
const active = permits(control);
|
|
341
|
+
if (active[commandId] !== undefined) {
|
|
342
|
+
return active[commandId] === digest
|
|
343
|
+
? { control }
|
|
344
|
+
: { digestMismatch: true };
|
|
345
|
+
}
|
|
346
|
+
if (control.status !== "EMPTY" && control.status !== "READY")
|
|
347
|
+
return { rejected: true };
|
|
348
|
+
const updated = { ...control, writePermits: { ...active, [commandId]: digest } };
|
|
349
|
+
await txn.put(CONTROL, updated);
|
|
350
|
+
return { control: updated };
|
|
351
|
+
});
|
|
352
|
+
if ("digestMismatch" in result)
|
|
353
|
+
return reject("bootstrap_permit_digest_mismatch", "bootstrap permit digest does not match the held permit");
|
|
354
|
+
if ("rejected" in result)
|
|
355
|
+
return reject("bootstrap_command_rejected", "bootstrap epoch rejects normal durable mutation");
|
|
356
|
+
return response({ leaseEpoch: result.control.leaseEpoch, admitted: true });
|
|
357
|
+
}
|
|
358
|
+
async permitRelease(serviceId, body) {
|
|
359
|
+
if (!object(body) || !string(body.commandId))
|
|
360
|
+
return reject("bootstrap_permit_invalid", "commandId is required", 400);
|
|
361
|
+
const commandId = body.commandId;
|
|
362
|
+
const digest = string(body.digest) ? body.digest : undefined;
|
|
363
|
+
const result = await this.ctx.storage.transaction(async (txn) => {
|
|
364
|
+
const control = (await txn.get(CONTROL)) ?? empty(serviceId);
|
|
365
|
+
const active = permits(control);
|
|
366
|
+
if (active[commandId] !== undefined && digest !== undefined && active[commandId] !== digest) {
|
|
367
|
+
return { digestMismatch: true };
|
|
368
|
+
}
|
|
369
|
+
if (active[commandId] === undefined)
|
|
370
|
+
return { control };
|
|
371
|
+
const next = { ...active };
|
|
372
|
+
delete next[commandId];
|
|
373
|
+
const updated = { ...control, writePermits: next };
|
|
374
|
+
await txn.put(CONTROL, updated);
|
|
375
|
+
return { control: updated };
|
|
376
|
+
});
|
|
377
|
+
if ("digestMismatch" in result)
|
|
378
|
+
return reject("bootstrap_permit_digest_mismatch", "bootstrap permit digest does not match the held permit");
|
|
379
|
+
return response({ leaseEpoch: result.control.leaseEpoch, admitted: true });
|
|
380
|
+
}
|
|
318
381
|
async command(serviceId, body, action, observation) {
|
|
319
382
|
if (!object(body) || !string(body.commandId))
|
|
320
383
|
return reject("bootstrap_command_invalid", "commandId is required", 400);
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { BootstrapManifest } from "./types";
|
|
2
2
|
import type { StoreProvider, StoreProviderEnvironment } from "../store/provider";
|
|
3
|
+
/** Missing and unconfigured stay indistinguishable from an unknown path. */
|
|
4
|
+
export declare function authenticateOperatorBootstrap(request: Request, binding: unknown): Promise<Response | undefined>;
|
|
3
5
|
export interface OperatorBootstrapEnv extends StoreProviderEnvironment {
|
|
4
6
|
readonly BOOTSTRAP: DurableObjectNamespace;
|
|
5
7
|
readonly ALLOCATOR: DurableObjectNamespace;
|
|
@@ -16,5 +18,7 @@ export interface OperatorBootstrapHooks {
|
|
|
16
18
|
readonly manifest: BootstrapManifest;
|
|
17
19
|
}) => Promise<void>;
|
|
18
20
|
}
|
|
19
|
-
/**
|
|
21
|
+
/** Authorized continuation. Authentication runs before this function. */
|
|
22
|
+
export declare function handleAuthorizedOperatorBootstrap(request: Request, env: OperatorBootstrapEnv, storeProvider: StoreProvider, hooks?: OperatorBootstrapHooks): Promise<Response>;
|
|
23
|
+
/** Bearer-only bootstrap lane. Incoming headers are never forwarded to a DO. */
|
|
20
24
|
export declare function handleOperatorBootstrap(request: Request, env: OperatorBootstrapEnv, storeProvider: StoreProvider, hooks?: OperatorBootstrapHooks): Promise<Response>;
|
|
@@ -1,22 +1,22 @@
|
|
|
1
1
|
import { parseBootstrapDump } from "./manifest";
|
|
2
2
|
import { createBootstrapStoreAdapter } from "./BootstrapStoreAdapter";
|
|
3
|
+
import { checkConfiguredBearer } from "../http/ConfiguredBearer";
|
|
3
4
|
import { scopeIdFor } from "../scope/ScopeName";
|
|
4
5
|
function json(body, status = 200) { return new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json; charset=utf-8" } }); }
|
|
5
6
|
function object(value) { return typeof value === "object" && value !== null && !Array.isArray(value); }
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
if (
|
|
7
|
+
/** Missing and unconfigured stay indistinguishable from an unknown path. */
|
|
8
|
+
export async function authenticateOperatorBootstrap(request, binding) {
|
|
9
|
+
const decision = await checkConfiguredBearer(request, binding);
|
|
10
|
+
if (decision === "unconfigured" || decision === "missing") {
|
|
10
11
|
return json({ code: "not_found", error: "not found" }, 404);
|
|
11
|
-
|
|
12
|
+
}
|
|
13
|
+
if (decision === "invalid") {
|
|
12
14
|
return json({ code: "operator_auth_required", error: "valid bearer required" }, 403);
|
|
15
|
+
}
|
|
13
16
|
return undefined;
|
|
14
17
|
}
|
|
15
|
-
/**
|
|
16
|
-
export async function
|
|
17
|
-
const denied = bearer(request, env.REPAIR_OPERATOR_TOKEN);
|
|
18
|
-
if (denied !== undefined)
|
|
19
|
-
return denied;
|
|
18
|
+
/** Authorized continuation. Authentication runs before this function. */
|
|
19
|
+
export async function handleAuthorizedOperatorBootstrap(request, env, storeProvider, hooks = {}) {
|
|
20
20
|
const url = new URL(request.url);
|
|
21
21
|
const match = url.pathname.match(/^\/operator\/bootstrap\/([^/]+)\/(plan|import|status|abort|export)$/);
|
|
22
22
|
if (match === null)
|
|
@@ -102,3 +102,10 @@ export async function handleOperatorBootstrap(request, env, storeProvider, hooks
|
|
|
102
102
|
}
|
|
103
103
|
return invoke("/ready", body);
|
|
104
104
|
}
|
|
105
|
+
/** Bearer-only bootstrap lane. Incoming headers are never forwarded to a DO. */
|
|
106
|
+
export async function handleOperatorBootstrap(request, env, storeProvider, hooks = {}) {
|
|
107
|
+
const denied = await authenticateOperatorBootstrap(request, env.REPAIR_OPERATOR_TOKEN);
|
|
108
|
+
if (denied !== undefined)
|
|
109
|
+
return denied;
|
|
110
|
+
return handleAuthorizedOperatorBootstrap(request, env, storeProvider, hooks);
|
|
111
|
+
}
|
|
@@ -55,6 +55,12 @@ export interface BootstrapControlRecord {
|
|
|
55
55
|
readonly normalCommands?: Readonly<Record<string, number>>;
|
|
56
56
|
/** Recently released entry admissions, retained only for their final epoch check. */
|
|
57
57
|
readonly releasedCommands?: Readonly<Record<string, number>>;
|
|
58
|
+
/**
|
|
59
|
+
* SDT-G36 durable write permits. Held from before allocation until every
|
|
60
|
+
* target tag of that attempt is durably resolved. Absent on old records
|
|
61
|
+
* (treated as `{}`). Only a bootstrap barrier, never commit authority.
|
|
62
|
+
*/
|
|
63
|
+
readonly writePermits?: Readonly<Record<string, string>>;
|
|
58
64
|
/** READY is a post-verification state, never merely an import completion. */
|
|
59
65
|
readonly verifiedImportId?: string | null;
|
|
60
66
|
readonly verifiedLeaseEpoch?: number | null;
|
|
@@ -7,5 +7,10 @@ export interface OperatorRepairEnv {
|
|
|
7
7
|
REPAIR_EXCLUSION_LOOKUP?: Fetcher;
|
|
8
8
|
SDT_SERVICE_ID?: string;
|
|
9
9
|
}
|
|
10
|
-
/**
|
|
10
|
+
/** Non-POST stays a route miss for every binding state, before authentication. */
|
|
11
|
+
export declare function nonPostOperatorRepairResponse(request: Request): Response | undefined;
|
|
12
|
+
export declare function authenticateOperatorRepair(request: Request, binding: unknown): Promise<Response | undefined>;
|
|
13
|
+
/** Authorized continuation. Authentication and the POST check run before this. */
|
|
14
|
+
export declare function handleAuthorizedOperatorRepair(request: Request, env: OperatorRepairEnv, nativeTracing?: NativeTracing, serviceIdentityProvider?: ServiceIdentityProvider): Promise<Response>;
|
|
15
|
+
/** Authenticated, bounded repair command; it is not a V1 public wire endpoint. */
|
|
11
16
|
export declare function handleOperatorRepair(request: Request, env: OperatorRepairEnv, nativeTracing?: NativeTracing, serviceIdentityProvider?: ServiceIdentityProvider): Promise<Response>;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { BindingExclusionLookupClient } from "../downstream/ExclusionLookup";
|
|
2
|
+
import { checkConfiguredBearer } from "../http/ConfiguredBearer";
|
|
2
3
|
import { RepairWorker, RepairWorkerFailure } from "../repair/RepairWorker";
|
|
3
4
|
import { noOpNativeTracing } from "../trace/CommitTrace";
|
|
4
5
|
import { envServiceIdentity, requireServiceIdentity } from "../service/ServiceIdentityProvider";
|
|
@@ -74,15 +75,24 @@ function testFaultFromRequest(request) {
|
|
|
74
75
|
? value
|
|
75
76
|
: undefined;
|
|
76
77
|
}
|
|
77
|
-
/**
|
|
78
|
-
export
|
|
79
|
-
if (request.method
|
|
80
|
-
return
|
|
78
|
+
/** Non-POST stays a route miss for every binding state, before authentication. */
|
|
79
|
+
export function nonPostOperatorRepairResponse(request) {
|
|
80
|
+
if (request.method === "POST")
|
|
81
|
+
return undefined;
|
|
82
|
+
return error(404, "operator_repair_route_not_found", "Operator repair requires POST");
|
|
83
|
+
}
|
|
84
|
+
export async function authenticateOperatorRepair(request, binding) {
|
|
85
|
+
const decision = await checkConfiguredBearer(request, binding);
|
|
86
|
+
if (decision === "unconfigured") {
|
|
87
|
+
return error(404, "operator_repair_route_not_found", "Operator repair route not found");
|
|
81
88
|
}
|
|
82
|
-
|
|
83
|
-
if (!isNonEmptyString(token) || request.headers.get("authorization") !== `Bearer ${token}`) {
|
|
89
|
+
if (decision === "missing" || decision === "invalid") {
|
|
84
90
|
return error(401, "operator_auth_required", "A valid operator repair bearer token is required");
|
|
85
91
|
}
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
94
|
+
/** Authorized continuation. Authentication and the POST check run before this. */
|
|
95
|
+
export async function handleAuthorizedOperatorRepair(request, env, nativeTracing = noOpNativeTracing, serviceIdentityProvider) {
|
|
86
96
|
let body;
|
|
87
97
|
try {
|
|
88
98
|
body = await request.json();
|
|
@@ -106,3 +116,13 @@ export async function handleOperatorRepair(request, env, nativeTracing = noOpNat
|
|
|
106
116
|
return error(500, "operator_repair_failed", message);
|
|
107
117
|
}
|
|
108
118
|
}
|
|
119
|
+
/** Authenticated, bounded repair command; it is not a V1 public wire endpoint. */
|
|
120
|
+
export async function handleOperatorRepair(request, env, nativeTracing = noOpNativeTracing, serviceIdentityProvider) {
|
|
121
|
+
const methodDenied = nonPostOperatorRepairResponse(request);
|
|
122
|
+
if (methodDenied !== undefined)
|
|
123
|
+
return methodDenied;
|
|
124
|
+
const denied = await authenticateOperatorRepair(request, env.REPAIR_OPERATOR_TOKEN);
|
|
125
|
+
if (denied !== undefined)
|
|
126
|
+
return denied;
|
|
127
|
+
return handleAuthorizedOperatorRepair(request, env, nativeTracing, serviceIdentityProvider);
|
|
128
|
+
}
|
package/dist/cloudflare.d.ts
CHANGED
|
@@ -30,6 +30,8 @@ export { buildScopeName, DURABLE_OBJECT_SCOPE_CLASSES, isScopeServiceId, parseSc
|
|
|
30
30
|
export type { DurableObjectScope, DurableObjectScopeClass, ScopeNameNamespace } from "./scope/ScopeName";
|
|
31
31
|
export { envServiceIdentity, G11_SERVICE_ID_HEADER, injectableServiceIdentity, requestServiceIdentity, requireServiceIdentity, ServiceIdentityMissingError, TEST_SERVICE_ID_HEADER, } from "./service/ServiceIdentityProvider";
|
|
32
32
|
export type { ServiceIdentityBehavior, ServiceIdentityEnvironment, ServiceIdentityProvider, ServiceIdentityRequestOptions, ServiceIdentityResolution, } from "./service/ServiceIdentityProvider";
|
|
33
|
+
export { checkConfiguredBearer } from "./http/ConfiguredBearer";
|
|
34
|
+
export type { CheckConfiguredBearerOptions, ConfiguredBearerResult, ConfiguredBearerTokenNormalization, } from "./http/ConfiguredBearer";
|
|
33
35
|
export interface CloudflareOnlyEnv {
|
|
34
36
|
ALLOCATOR: DurableObjectNamespace;
|
|
35
37
|
BOOTSTRAP: DurableObjectNamespace;
|
|
@@ -42,6 +44,7 @@ export interface CloudflareOnlyEnv {
|
|
|
42
44
|
D1_MV: D1Database;
|
|
43
45
|
AUTO_DRAIN_OUTBOX?: string;
|
|
44
46
|
REPAIR_OPERATOR_TOKEN: string;
|
|
47
|
+
INCIDENT_MAINTAINER_TOKEN?: string;
|
|
45
48
|
REPAIR_EXCLUSION_LOOKUP?: Fetcher;
|
|
46
49
|
G11_VERIFICATION_ENABLED?: string;
|
|
47
50
|
SDT_SERVICE_ID?: string;
|
|
@@ -152,6 +155,8 @@ export type { ClosedPrefixCertificate } from "./allocator/types";
|
|
|
152
155
|
export type { SafeViewCoverageContext } from "./projection/ProjectionRuntime";
|
|
153
156
|
export { downstreamEnvelopeBytes, classifyDirectDoorbellFailure, preflightDirectDoorbell, readDomainDeliveryClass, readDirectDoorbellConfig, selectDirectDoorbellViews, MAX_SERVICE_BINDING_INVOCATIONS_PER_REQUEST, } from "./downstream/Doorbell";
|
|
154
157
|
export { deliveryCorrelationId } from "./downstream/DeliveryCore";
|
|
158
|
+
export { admitActiveWrite, assertNoSilentLedgerMerge, assertStrictlyBefore, createRotationState, maxSortableUniqueId, recordLateArrival, refuseSafeWindowSeal, refuseSealedWrite, refuseSilentLedgerMerge, replaceSealedIdentity, rotateAppend, ShardRotationError, } from "./shard/ShardRotation";
|
|
159
|
+
export type { LateArrival, SealedShard, ShardRotationDiagnostic, ShardRotationState, } from "./shard/ShardRotation";
|
|
155
160
|
export { GlobalCompletenessReconciler } from "./completeness/GlobalCompletenessReconciler";
|
|
156
161
|
export { G44_HEALTH_STALE_AFTER_MS, G44_SCANNER_VERSION, GLOBAL_COMPLETENESS_INTERIM_DISPOSITION, } from "./completeness/types";
|
|
157
162
|
export type { GlobalCompletenessHealth, GlobalCompletenessCoverage, GlobalCompletenessHealthRecord, GlobalCompletenessScanResult, SourceObligationFact, SourceObligationPage, SourcePartitionSnapshot, } from "./completeness/types";
|