@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.
Files changed (44) hide show
  1. package/README.md +32 -1
  2. package/dist/allocator/AllocatorDurableObject.js +25 -2
  3. package/dist/bootstrap/BootstrapCoordinatorDurableObject.d.ts +2 -0
  4. package/dist/bootstrap/BootstrapCoordinatorDurableObject.js +64 -1
  5. package/dist/bootstrap/OperatorBootstrap.d.ts +5 -1
  6. package/dist/bootstrap/OperatorBootstrap.js +17 -10
  7. package/dist/bootstrap/types.d.ts +6 -0
  8. package/dist/cli/OperatorRepairCli.d.ts +6 -1
  9. package/dist/cli/OperatorRepairCli.js +26 -6
  10. package/dist/cloudflare.d.ts +5 -0
  11. package/dist/cloudflare.js +1567 -465
  12. package/dist/commit/CommitWorker.d.ts +19 -0
  13. package/dist/commit/CommitWorker.js +176 -27
  14. package/dist/completeness/IncidentLifecycle.d.ts +75 -0
  15. package/dist/completeness/IncidentLifecycle.js +503 -0
  16. package/dist/completeness/types.d.ts +67 -0
  17. package/dist/cosmos.d.ts +34 -4
  18. package/dist/cosmos.js +214 -49
  19. package/dist/downstream/OutboxDrain.d.ts +4 -1
  20. package/dist/downstream/OutboxDrain.js +24 -2
  21. package/dist/generated/cosmos-layout.d.ts +87 -0
  22. package/dist/generated/cosmos-layout.js +99 -0
  23. package/dist/http/ConfiguredBearer.d.ts +12 -0
  24. package/dist/http/ConfiguredBearer.js +33 -0
  25. package/dist/http/IncidentMaintenance.d.ts +8 -0
  26. package/dist/http/IncidentMaintenance.js +124 -0
  27. package/dist/http/SerializedQueryWorker.js +37 -0
  28. package/dist/index.d.ts +13 -0
  29. package/dist/index.js +1627 -465
  30. package/dist/read/SerializedReadWorker.d.ts +3 -0
  31. package/dist/read/SerializedReadWorker.js +3 -3
  32. package/dist/shard/ShardRotation.d.ts +48 -0
  33. package/dist/shard/ShardRotation.js +120 -0
  34. package/dist/store/CosmosEventStore.js +56 -46
  35. package/dist/store/PostgresEventStore.d.ts +20 -0
  36. package/dist/store/PostgresEventStore.js +53 -0
  37. package/dist/store/provider.d.ts +4 -0
  38. package/dist/tag/TagDurableObject.js +66 -14
  39. package/dist/tag/TagSqlSchema.d.ts +6 -1
  40. package/dist/tag/TagSqlSchema.js +15 -0
  41. package/dist/tag/types.d.ts +9 -0
  42. package/dist/trace/CommitTrace.d.ts +3 -3
  43. package/dist/trace/CommitTraceVerifier.js +1 -1
  44. package/package.json +6 -15
@@ -0,0 +1,124 @@
1
+ import { IncidentLifecycle, IncidentLifecycleError, normalizeIncidentIdentity, } from "../completeness/IncidentLifecycle";
2
+ import { requestServiceIdentity, } from "../service/ServiceIdentityProvider";
3
+ import { checkConfiguredBearer } from "./ConfiguredBearer";
4
+ function json(body, status = 200) {
5
+ return new Response(JSON.stringify(body), {
6
+ status,
7
+ headers: { "content-type": "application/json; charset=utf-8" },
8
+ });
9
+ }
10
+ function error(status, code, message) {
11
+ return json({ error: message, code }, status);
12
+ }
13
+ function boolFilter(value, name) {
14
+ if (value === null)
15
+ return undefined;
16
+ if (value === "true")
17
+ return true;
18
+ if (value === "false")
19
+ return false;
20
+ throw new IncidentLifecycleError("incident_invalid_filter", 400, `${name} must be true or false`);
21
+ }
22
+ function parseFilters(url) {
23
+ const allowed = new Set(["state", "owner", "unowned", "overdue", "observedAfterClose"]);
24
+ let unknown = false;
25
+ url.searchParams.forEach((_value, key) => {
26
+ if (!allowed.has(key))
27
+ unknown = true;
28
+ });
29
+ if (unknown)
30
+ throw new IncidentLifecycleError("incident_invalid_filter", 400, "Unknown incident list filter");
31
+ const state = url.searchParams.get("state");
32
+ if (state !== null && !["OPEN", "ACKNOWLEDGED", "CORRECTION_RECORDED", "CLOSED", "REOPENED"].includes(state)) {
33
+ throw new IncidentLifecycleError("incident_invalid_filter", 400, "state is invalid");
34
+ }
35
+ const owner = url.searchParams.get("owner");
36
+ if (owner !== null) {
37
+ const normalizedOwner = owner.trim();
38
+ if (normalizedOwner.length === 0) {
39
+ throw new IncidentLifecycleError("incident_invalid_filter", 400, "owner must be non-empty");
40
+ }
41
+ if (new TextEncoder().encode(normalizedOwner).byteLength > 256) {
42
+ throw new IncidentLifecycleError("incident_invalid_filter", 400, "owner is too long");
43
+ }
44
+ }
45
+ const unowned = boolFilter(url.searchParams.get("unowned"), "unowned");
46
+ const overdue = boolFilter(url.searchParams.get("overdue"), "overdue");
47
+ const observedAfterClose = boolFilter(url.searchParams.get("observedAfterClose"), "observedAfterClose");
48
+ return {
49
+ ...(state === null ? {} : { state: state }),
50
+ ...(owner === null ? {} : { owner: owner.trim() }),
51
+ ...(unowned === undefined ? {} : { unowned }),
52
+ ...(overdue === undefined ? {} : { overdue }),
53
+ ...(observedAfterClose === undefined ? {} : { observedAfterClose }),
54
+ };
55
+ }
56
+ function incidentIdentityFromPath(pathname) {
57
+ const encoded = pathname.slice("/maintenance/incidents/".length);
58
+ if (encoded.length === 0 || encoded.includes("/")) {
59
+ throw new IncidentLifecycleError("incident_route_not_found", 404, "Incident route not found");
60
+ }
61
+ try {
62
+ return normalizeIncidentIdentity(decodeURIComponent(encoded));
63
+ }
64
+ catch {
65
+ throw new IncidentLifecycleError("incident_invalid_request", 400, "Incident identity must be URI encoded");
66
+ }
67
+ }
68
+ export function isIncidentMaintenancePath(pathname) {
69
+ return pathname === "/maintenance/incidents" || pathname.startsWith("/maintenance/incidents/");
70
+ }
71
+ export async function handleIncidentMaintenance(request, env, serviceIdentityProvider) {
72
+ const decision = await checkConfiguredBearer(request, env.INCIDENT_MAINTAINER_TOKEN, {
73
+ tokenNormalization: "trim",
74
+ });
75
+ if (decision === "unconfigured") {
76
+ return error(503, "incident_maintenance_unavailable", "Incident maintenance is not configured");
77
+ }
78
+ if (decision === "missing" || decision === "invalid") {
79
+ return error(401, "incident_auth_required", "A valid incident maintenance bearer token is required");
80
+ }
81
+ let serviceId;
82
+ try {
83
+ serviceId = requestServiceIdentity(request, serviceIdentityProvider, {
84
+ allowG11Verification: env.G11_VERIFICATION_ENABLED === "true",
85
+ });
86
+ }
87
+ catch {
88
+ return error(503, "scope.identity_missing", "A deployment service identity is required for this scope");
89
+ }
90
+ if (env.D1 === undefined) {
91
+ return error(503, "incident_maintenance_unavailable", "Incident maintenance storage is unavailable");
92
+ }
93
+ const lifecycle = new IncidentLifecycle(env.D1);
94
+ const url = new URL(request.url);
95
+ try {
96
+ if (request.method === "GET" && url.pathname === "/maintenance/incidents") {
97
+ return json(await lifecycle.list(serviceId, parseFilters(url)));
98
+ }
99
+ if (request.method === "GET" && url.pathname.startsWith("/maintenance/incidents/")) {
100
+ return json(await lifecycle.detail(serviceId, incidentIdentityFromPath(url.pathname)));
101
+ }
102
+ if (request.method === "POST" && url.pathname === "/maintenance/incidents/transitions") {
103
+ let body;
104
+ try {
105
+ body = await request.json();
106
+ }
107
+ catch {
108
+ return error(400, "incident_invalid_request", "Transition request must be JSON");
109
+ }
110
+ const requestValue = IncidentLifecycle.parseTransition(body);
111
+ const actor = request.headers.get("x-sdt-maintainer");
112
+ if (actor === null) {
113
+ return error(400, "incident_invalid_actor", "x-sdt-maintainer is required");
114
+ }
115
+ return json(await lifecycle.transition(serviceId, requestValue, actor));
116
+ }
117
+ return error(404, "incident_route_not_found", "Incident maintenance route not found");
118
+ }
119
+ catch (caught) {
120
+ if (caught instanceof IncidentLifecycleError)
121
+ return error(caught.status, caught.code, caught.message);
122
+ return error(500, "incident_maintenance_failed", "Incident maintenance could not complete the request");
123
+ }
124
+ }
@@ -57,6 +57,39 @@ function parseRequest(value) {
57
57
  },
58
58
  };
59
59
  }
60
+ const QUERY_PARAMS_UNSUPPORTED_MESSAGE = "The selected query does not support these parameters.";
61
+ const LIST_QUERY_PARAM_KEYS = new Set(["PageNumber", "PageSize", "NewestFirst", "consistency"]);
62
+ /**
63
+ * Scalar queries have no parameter interpreter, so only an empty plain object
64
+ * is a supported no-parameter call. List queries keep the four existing
65
+ * controls and leave their value checks to paginationFrom. Non-object list
66
+ * input is also left to paginationFrom so that validation response stays put.
67
+ */
68
+ function unsupportedQueryParams(endpoint, queryParams) {
69
+ if (endpoint === "query") {
70
+ if (isObject(queryParams) && Object.keys(queryParams).length === 0) {
71
+ return undefined;
72
+ }
73
+ return {
74
+ status: 400,
75
+ code: "query_params_unsupported",
76
+ error: QUERY_PARAMS_UNSUPPORTED_MESSAGE,
77
+ };
78
+ }
79
+ if (!isObject(queryParams)) {
80
+ return undefined;
81
+ }
82
+ for (const key of Object.keys(queryParams)) {
83
+ if (!LIST_QUERY_PARAM_KEYS.has(key)) {
84
+ return {
85
+ status: 400,
86
+ code: "query_params_unsupported",
87
+ error: QUERY_PARAMS_UNSUPPORTED_MESSAGE,
88
+ };
89
+ }
90
+ }
91
+ return undefined;
92
+ }
60
93
  function paginationFrom(value) {
61
94
  if (!isObject(value)) {
62
95
  return { error: "list-query queryParamsJson must contain an object" };
@@ -295,6 +328,10 @@ export async function handleSerializedQuery(request, env, options = {}) {
295
328
  if (definition === undefined || definition.endpoint !== endpoint) {
296
329
  return error(400, "validation_error", "queryType is not mapped for this endpoint");
297
330
  }
331
+ const unsupported = unsupportedQueryParams(endpoint, parsed.value.queryParams);
332
+ if (unsupported !== undefined) {
333
+ return error(unsupported.status, unsupported.code, unsupported.error);
334
+ }
298
335
  if (!definition.enabled || (options.projectors ?? DEPLOYED_PROJECTOR_REGISTRY).resolve(definition.tagProjector) === undefined) {
299
336
  return error(503, "projection_unavailable", "The mapped query projection is unavailable");
300
337
  }
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,12 +47,16 @@ 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";
50
55
  export type { DeliveryCoreEnvironment, DeliveryCoreFailure, DeliveryCoreOptions, DeliveryCoreResult, DeliverySource, DeliveryViewApplyResult, DeliveryViewFailureClass, DeliveryViewHandler, DeliveryViewInput, DeliveryViewResult, } from "./downstream/DeliveryCore";
51
56
  export type { DeliveryClass, DirectDoorbellDegradation, DirectDoorbellDeploymentConfig, DirectDoorbellPreflightResult, DirectDoorbellReceiverMode, DirectDoorbellFailureKind, DownstreamDoorbellBinding, } from "./downstream/Doorbell";
52
57
  export type { JsonValue, MaterializedViewRowPatch } from "@sekiban/dcb-core";
58
+ export { checkConfiguredBearer } from "./http/ConfiguredBearer";
59
+ export type { CheckConfiguredBearerOptions, ConfiguredBearerResult, ConfiguredBearerTokenNormalization, } from "./http/ConfiguredBearer";
53
60
  export { composeRuntime, createRuntimeCommitPort, registeredEventParsers, RuntimeCommandRegistry, } from "./composition";
54
61
  export type { RuntimeCommitAllocationLike, RuntimeCommitCandidateLike, RuntimeCommitPort, RuntimeCommitPortOptions, RuntimeCommitPortResult, RuntimeCommandLike, RuntimeDomainLike, RuntimeQueryDefinition, RuntimeWorkerConfig, } from "./composition";
55
62
  export { POSTGRES_STORE_PROVIDER, createPostgresBootstrapAdapter, createPostgresStoreProvider } from "./store/provider";
@@ -64,6 +71,7 @@ export interface Env {
64
71
  TAG_STATE: DurableObjectNamespace;
65
72
  /** Secret binding; deployment must configure this rather than a public var. */
66
73
  REPAIR_OPERATOR_TOKEN: string;
74
+ INCIDENT_MAINTAINER_TOKEN?: string;
67
75
  /** Queue producer/consumer for durable Tag outbox rows. */
68
76
  DOWNSTREAM_QUEUE: Queue<DownstreamOutboxMessage>;
69
77
  DOWNSTREAM_DOORBELL?: DownstreamDoorbellBinding;
@@ -72,6 +80,11 @@ export interface Env {
72
80
  /** Local Docker/CI connection; deployed Workers normally use HYPERDRIVE. */
73
81
  POSTGRES_URL?: string;
74
82
  HYPERDRIVE?: Hyperdrive;
83
+ /** Experimental Cosmos endpoint and database deployment values. */
84
+ COSMOS_ENDPOINT?: string;
85
+ COSMOS_DATABASE?: string;
86
+ /** Experimental Cosmos account key; configure it as a Worker secret. */
87
+ COSMOS_KEY?: string;
75
88
  REPAIR_EXCLUSION_LOOKUP?: Fetcher;
76
89
  /** Only an authenticated deployment-verification lane may set this. */
77
90
  G11_VERIFICATION_ENABLED?: string;