@sekiban/dcb-runtime 0.2.0 → 0.2.1
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 +5 -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/types.d.ts +6 -0
- package/dist/cloudflare.d.ts +3 -0
- package/dist/cloudflare.js +1493 -451
- 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/IncidentMaintenance.d.ts +8 -0
- package/dist/http/IncidentMaintenance.js +145 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +1548 -451
- 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
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { IncidentLifecycle, IncidentLifecycleError, normalizeIncidentIdentity, } from "../completeness/IncidentLifecycle";
|
|
2
|
+
import { requestServiceIdentity, } from "../service/ServiceIdentityProvider";
|
|
3
|
+
function json(body, status = 200) {
|
|
4
|
+
return new Response(JSON.stringify(body), {
|
|
5
|
+
status,
|
|
6
|
+
headers: { "content-type": "application/json; charset=utf-8" },
|
|
7
|
+
});
|
|
8
|
+
}
|
|
9
|
+
function error(status, code, message) {
|
|
10
|
+
return json({ error: message, code }, status);
|
|
11
|
+
}
|
|
12
|
+
function configuredToken(value) {
|
|
13
|
+
if (typeof value !== "string")
|
|
14
|
+
return undefined;
|
|
15
|
+
const token = value.trim();
|
|
16
|
+
return token.length === 0 ? undefined : token;
|
|
17
|
+
}
|
|
18
|
+
async function digest(value) {
|
|
19
|
+
const bytes = new TextEncoder().encode(value);
|
|
20
|
+
return new Uint8Array(await crypto.subtle.digest("SHA-256", bytes));
|
|
21
|
+
}
|
|
22
|
+
function fixedLengthEqual(left, right) {
|
|
23
|
+
let difference = left.length ^ right.length;
|
|
24
|
+
const length = Math.max(left.length, right.length);
|
|
25
|
+
for (let index = 0; index < length; index += 1) {
|
|
26
|
+
difference |= (left[index] ?? 0) ^ (right[index] ?? 0);
|
|
27
|
+
}
|
|
28
|
+
return difference === 0;
|
|
29
|
+
}
|
|
30
|
+
async function authenticated(request, token) {
|
|
31
|
+
const credential = request.headers.get("authorization") ?? "";
|
|
32
|
+
const suppliedToken = credential.startsWith("Bearer ") ? credential.slice("Bearer ".length) : "";
|
|
33
|
+
const [expectedDigest, suppliedDigest] = await Promise.all([digest(token), digest(suppliedToken)]);
|
|
34
|
+
return fixedLengthEqual(expectedDigest, suppliedDigest);
|
|
35
|
+
}
|
|
36
|
+
function boolFilter(value, name) {
|
|
37
|
+
if (value === null)
|
|
38
|
+
return undefined;
|
|
39
|
+
if (value === "true")
|
|
40
|
+
return true;
|
|
41
|
+
if (value === "false")
|
|
42
|
+
return false;
|
|
43
|
+
throw new IncidentLifecycleError("incident_invalid_filter", 400, `${name} must be true or false`);
|
|
44
|
+
}
|
|
45
|
+
function parseFilters(url) {
|
|
46
|
+
const allowed = new Set(["state", "owner", "unowned", "overdue", "observedAfterClose"]);
|
|
47
|
+
let unknown = false;
|
|
48
|
+
url.searchParams.forEach((_value, key) => {
|
|
49
|
+
if (!allowed.has(key))
|
|
50
|
+
unknown = true;
|
|
51
|
+
});
|
|
52
|
+
if (unknown)
|
|
53
|
+
throw new IncidentLifecycleError("incident_invalid_filter", 400, "Unknown incident list filter");
|
|
54
|
+
const state = url.searchParams.get("state");
|
|
55
|
+
if (state !== null && !["OPEN", "ACKNOWLEDGED", "CORRECTION_RECORDED", "CLOSED", "REOPENED"].includes(state)) {
|
|
56
|
+
throw new IncidentLifecycleError("incident_invalid_filter", 400, "state is invalid");
|
|
57
|
+
}
|
|
58
|
+
const owner = url.searchParams.get("owner");
|
|
59
|
+
if (owner !== null) {
|
|
60
|
+
const normalizedOwner = owner.trim();
|
|
61
|
+
if (normalizedOwner.length === 0) {
|
|
62
|
+
throw new IncidentLifecycleError("incident_invalid_filter", 400, "owner must be non-empty");
|
|
63
|
+
}
|
|
64
|
+
if (new TextEncoder().encode(normalizedOwner).byteLength > 256) {
|
|
65
|
+
throw new IncidentLifecycleError("incident_invalid_filter", 400, "owner is too long");
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const unowned = boolFilter(url.searchParams.get("unowned"), "unowned");
|
|
69
|
+
const overdue = boolFilter(url.searchParams.get("overdue"), "overdue");
|
|
70
|
+
const observedAfterClose = boolFilter(url.searchParams.get("observedAfterClose"), "observedAfterClose");
|
|
71
|
+
return {
|
|
72
|
+
...(state === null ? {} : { state: state }),
|
|
73
|
+
...(owner === null ? {} : { owner: owner.trim() }),
|
|
74
|
+
...(unowned === undefined ? {} : { unowned }),
|
|
75
|
+
...(overdue === undefined ? {} : { overdue }),
|
|
76
|
+
...(observedAfterClose === undefined ? {} : { observedAfterClose }),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
function incidentIdentityFromPath(pathname) {
|
|
80
|
+
const encoded = pathname.slice("/maintenance/incidents/".length);
|
|
81
|
+
if (encoded.length === 0 || encoded.includes("/")) {
|
|
82
|
+
throw new IncidentLifecycleError("incident_route_not_found", 404, "Incident route not found");
|
|
83
|
+
}
|
|
84
|
+
try {
|
|
85
|
+
return normalizeIncidentIdentity(decodeURIComponent(encoded));
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
throw new IncidentLifecycleError("incident_invalid_request", 400, "Incident identity must be URI encoded");
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
export function isIncidentMaintenancePath(pathname) {
|
|
92
|
+
return pathname === "/maintenance/incidents" || pathname.startsWith("/maintenance/incidents/");
|
|
93
|
+
}
|
|
94
|
+
export async function handleIncidentMaintenance(request, env, serviceIdentityProvider) {
|
|
95
|
+
const token = configuredToken(env.INCIDENT_MAINTAINER_TOKEN);
|
|
96
|
+
if (token === undefined) {
|
|
97
|
+
return error(503, "incident_maintenance_unavailable", "Incident maintenance is not configured");
|
|
98
|
+
}
|
|
99
|
+
if (!(await authenticated(request, token))) {
|
|
100
|
+
return error(401, "incident_auth_required", "A valid incident maintenance bearer token is required");
|
|
101
|
+
}
|
|
102
|
+
let serviceId;
|
|
103
|
+
try {
|
|
104
|
+
serviceId = requestServiceIdentity(request, serviceIdentityProvider, {
|
|
105
|
+
allowG11Verification: env.G11_VERIFICATION_ENABLED === "true",
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
return error(503, "scope.identity_missing", "A deployment service identity is required for this scope");
|
|
110
|
+
}
|
|
111
|
+
if (env.D1 === undefined) {
|
|
112
|
+
return error(503, "incident_maintenance_unavailable", "Incident maintenance storage is unavailable");
|
|
113
|
+
}
|
|
114
|
+
const lifecycle = new IncidentLifecycle(env.D1);
|
|
115
|
+
const url = new URL(request.url);
|
|
116
|
+
try {
|
|
117
|
+
if (request.method === "GET" && url.pathname === "/maintenance/incidents") {
|
|
118
|
+
return json(await lifecycle.list(serviceId, parseFilters(url)));
|
|
119
|
+
}
|
|
120
|
+
if (request.method === "GET" && url.pathname.startsWith("/maintenance/incidents/")) {
|
|
121
|
+
return json(await lifecycle.detail(serviceId, incidentIdentityFromPath(url.pathname)));
|
|
122
|
+
}
|
|
123
|
+
if (request.method === "POST" && url.pathname === "/maintenance/incidents/transitions") {
|
|
124
|
+
let body;
|
|
125
|
+
try {
|
|
126
|
+
body = await request.json();
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
return error(400, "incident_invalid_request", "Transition request must be JSON");
|
|
130
|
+
}
|
|
131
|
+
const requestValue = IncidentLifecycle.parseTransition(body);
|
|
132
|
+
const actor = request.headers.get("x-sdt-maintainer");
|
|
133
|
+
if (actor === null) {
|
|
134
|
+
return error(400, "incident_invalid_actor", "x-sdt-maintainer is required");
|
|
135
|
+
}
|
|
136
|
+
return json(await lifecycle.transition(serviceId, requestValue, actor));
|
|
137
|
+
}
|
|
138
|
+
return error(404, "incident_route_not_found", "Incident maintenance route not found");
|
|
139
|
+
}
|
|
140
|
+
catch (caught) {
|
|
141
|
+
if (caught instanceof IncidentLifecycleError)
|
|
142
|
+
return error(caught.status, caught.code, caught.message);
|
|
143
|
+
return error(500, "incident_maintenance_failed", "Incident maintenance could not complete the request");
|
|
144
|
+
}
|
|
145
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -37,6 +37,9 @@ export { downstreamEnvelopeBytes, classifyDirectDoorbellFailure, readDomainDeliv
|
|
|
37
37
|
export { deliveryCorrelationId } from "./downstream/DeliveryCore";
|
|
38
38
|
export { GlobalCompletenessReconciler } from "./completeness/GlobalCompletenessReconciler";
|
|
39
39
|
export { G44_HEALTH_STALE_AFTER_MS, G44_SCANNER_VERSION, GLOBAL_COMPLETENESS_INTERIM_DISPOSITION, } from "./completeness/types";
|
|
40
|
+
export { IncidentLifecycle, IncidentLifecycleError, normalizeIncidentIdentity, } from "./completeness/IncidentLifecycle";
|
|
41
|
+
export type { IncidentDetailResult, IncidentFinding, IncidentListFilters, IncidentListItem, IncidentListResult, IncidentTransitionResult, } from "./completeness/IncidentLifecycle";
|
|
42
|
+
export type { IncidentCloseResolution, IncidentCorrection, IncidentLifecycleProjection, IncidentLifecycleState, IncidentTransitionRecord, IncidentTransitionRequest, } from "./completeness/types";
|
|
40
43
|
export type { GlobalCompletenessHealth, GlobalCompletenessHealthRecord, GlobalCompletenessScanResult, SourceObligationFact, SourceObligationPage, SourcePartitionSnapshot, } from "./completeness/types";
|
|
41
44
|
export { assertCanonicalEventType, canonicalEventType, DeliveryIdentityError, MissingCanonicalEventIdentityError, resolveDeliveryIdentity, } from "./eventIdentity";
|
|
42
45
|
export type { DeliveryProvenance, EventProvenance, ResolvedDeliveryIdentity } from "./eventIdentity";
|
|
@@ -44,6 +47,8 @@ export { allocateOrderRange, diagnosticAllocatedAt, decodeOrderOrdinal, encodeOr
|
|
|
44
47
|
export type { OrderAllocationRange, OrderClock } from "./allocator/OrderClock";
|
|
45
48
|
export { DOTNET_MAX_TICKS, DOTNET_TICKS_PER_MILLISECOND, DOTNET_UNIX_EPOCH_TICKS, MAX_UNIX_MILLISECONDS, SORTABLE_UNIQUE_ID_DIGITS, SORTABLE_UNIQUE_ID_RANDOM_DIGITS, SORTABLE_UNIQUE_ID_TICKS_DIGITS, SortableUniqueIdError, assertDotNetTicks, assertSortableUniqueId, compareSortableUniqueId, cryptoRandomSortableUniqueIdSuffix, dotNetTicksToUnixMs, formatSortableUniqueId, isSortableUniqueId, observeLegacySortableUniqueIdDecision, unixMsToDotNetTicks, } from "./allocator/SortableUniqueId";
|
|
46
49
|
export type { ParsedSortableUniqueId, SortableUniqueIdErrorCode } from "./allocator/SortableUniqueId";
|
|
50
|
+
export { admitActiveWrite, assertNoSilentLedgerMerge, assertStrictlyBefore, createRotationState, maxSortableUniqueId, recordLateArrival, refuseSafeWindowSeal, refuseSealedWrite, refuseSilentLedgerMerge, replaceSealedIdentity, rotateAppend, ShardRotationError, } from "./shard/ShardRotation";
|
|
51
|
+
export type { LateArrival, SealedShard, ShardRotationDiagnostic, ShardRotationState, } from "./shard/ShardRotation";
|
|
47
52
|
export { MAX_PUBLISHED_SAFE_WINDOW_MS, PUBLISHED_SAFE_WINDOW_MS, safeWindowCeilingExceeded, safeWindowCutoffSuid, safeWindowMs, } from "./safeWindow";
|
|
48
53
|
export { SERIALIZED_COMMIT_CORRELATION_ID, SERIALIZED_SEKIBAN_EXECUTOR, createUuidV7, isRfc4122Uuid, isUuidV7, serializedEventMetadata, writeTimestampUtc, } from "./eventRecord";
|
|
49
54
|
export type { SerializedEventMetadata } from "./eventRecord";
|
|
@@ -64,6 +69,7 @@ export interface Env {
|
|
|
64
69
|
TAG_STATE: DurableObjectNamespace;
|
|
65
70
|
/** Secret binding; deployment must configure this rather than a public var. */
|
|
66
71
|
REPAIR_OPERATOR_TOKEN: string;
|
|
72
|
+
INCIDENT_MAINTAINER_TOKEN?: string;
|
|
67
73
|
/** Queue producer/consumer for durable Tag outbox rows. */
|
|
68
74
|
DOWNSTREAM_QUEUE: Queue<DownstreamOutboxMessage>;
|
|
69
75
|
DOWNSTREAM_DOORBELL?: DownstreamDoorbellBinding;
|
|
@@ -72,6 +78,11 @@ export interface Env {
|
|
|
72
78
|
/** Local Docker/CI connection; deployed Workers normally use HYPERDRIVE. */
|
|
73
79
|
POSTGRES_URL?: string;
|
|
74
80
|
HYPERDRIVE?: Hyperdrive;
|
|
81
|
+
/** Experimental Cosmos endpoint and database deployment values. */
|
|
82
|
+
COSMOS_ENDPOINT?: string;
|
|
83
|
+
COSMOS_DATABASE?: string;
|
|
84
|
+
/** Experimental Cosmos account key; configure it as a Worker secret. */
|
|
85
|
+
COSMOS_KEY?: string;
|
|
75
86
|
REPAIR_EXCLUSION_LOOKUP?: Fetcher;
|
|
76
87
|
/** Only an authenticated deployment-verification lane may set this. */
|
|
77
88
|
G11_VERIFICATION_ENABLED?: string;
|