@sekiban/dcb-runtime 0.2.0

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 (153) hide show
  1. package/LICENSE +86 -0
  2. package/README.md +13 -0
  3. package/dist/allocator/AllocatorDurableObject.d.ts +36 -0
  4. package/dist/allocator/AllocatorDurableObject.js +658 -0
  5. package/dist/allocator/IssuanceLedger.d.ts +188 -0
  6. package/dist/allocator/IssuanceLedger.js +608 -0
  7. package/dist/allocator/IssuanceReconciler.d.ts +41 -0
  8. package/dist/allocator/IssuanceReconciler.js +191 -0
  9. package/dist/allocator/OrderClock.d.ts +41 -0
  10. package/dist/allocator/OrderClock.js +92 -0
  11. package/dist/allocator/SortableUniqueId.d.ts +46 -0
  12. package/dist/allocator/SortableUniqueId.js +156 -0
  13. package/dist/allocator/types.d.ts +52 -0
  14. package/dist/allocator/types.js +1 -0
  15. package/dist/bootstrap/BootstrapCoordinatorDurableObject.d.ts +28 -0
  16. package/dist/bootstrap/BootstrapCoordinatorDurableObject.js +357 -0
  17. package/dist/bootstrap/BootstrapStoreAdapter.d.ts +51 -0
  18. package/dist/bootstrap/BootstrapStoreAdapter.js +162 -0
  19. package/dist/bootstrap/OperatorBootstrap.d.ts +20 -0
  20. package/dist/bootstrap/OperatorBootstrap.js +104 -0
  21. package/dist/bootstrap/manifest.d.ts +9 -0
  22. package/dist/bootstrap/manifest.js +115 -0
  23. package/dist/bootstrap/types.d.ts +75 -0
  24. package/dist/bootstrap/types.js +1 -0
  25. package/dist/cli/OperatorRepairCli.d.ts +11 -0
  26. package/dist/cli/OperatorRepairCli.js +108 -0
  27. package/dist/cloudflare.d.ts +164 -0
  28. package/dist/cloudflare.js +19487 -0
  29. package/dist/commit/CommitWorker.d.ts +101 -0
  30. package/dist/commit/CommitWorker.js +921 -0
  31. package/dist/commit/types.d.ts +25 -0
  32. package/dist/commit/types.js +13 -0
  33. package/dist/completeness/GlobalCompletenessReconciler.d.ts +77 -0
  34. package/dist/completeness/GlobalCompletenessReconciler.js +572 -0
  35. package/dist/completeness/types.d.ts +85 -0
  36. package/dist/completeness/types.js +6 -0
  37. package/dist/composition.d.ts +123 -0
  38. package/dist/composition.js +237 -0
  39. package/dist/cosmos.d.ts +11 -0
  40. package/dist/cosmos.js +1411 -0
  41. package/dist/d1-mv.d.ts +8 -0
  42. package/dist/d1-mv.js +2001 -0
  43. package/dist/d1.d.ts +16 -0
  44. package/dist/d1.js +3418 -0
  45. package/dist/diagnostics/G60DurableHop.d.ts +99 -0
  46. package/dist/diagnostics/G60DurableHop.js +248 -0
  47. package/dist/diagnostics/G65Admission.d.ts +23 -0
  48. package/dist/diagnostics/G65Admission.js +43 -0
  49. package/dist/diagnostics/G65DirectRing.d.ts +33 -0
  50. package/dist/diagnostics/G65DirectRing.js +85 -0
  51. package/dist/diagnostics/G69AdmissionAttempt.d.ts +43 -0
  52. package/dist/diagnostics/G69AdmissionAttempt.js +22 -0
  53. package/dist/downstream/DeliveryCore.d.ts +127 -0
  54. package/dist/downstream/DeliveryCore.js +304 -0
  55. package/dist/downstream/Doorbell.d.ts +43 -0
  56. package/dist/downstream/Doorbell.js +137 -0
  57. package/dist/downstream/DownstreamAdapter.d.ts +52 -0
  58. package/dist/downstream/DownstreamAdapter.js +299 -0
  59. package/dist/downstream/ExclusionLookup.d.ts +36 -0
  60. package/dist/downstream/ExclusionLookup.js +50 -0
  61. package/dist/downstream/InconsistencyDetector.d.ts +27 -0
  62. package/dist/downstream/InconsistencyDetector.js +82 -0
  63. package/dist/downstream/OutboxDrain.d.ts +35 -0
  64. package/dist/downstream/OutboxDrain.js +126 -0
  65. package/dist/downstream/types.d.ts +77 -0
  66. package/dist/downstream/types.js +64 -0
  67. package/dist/eventIdentity.d.ts +30 -0
  68. package/dist/eventIdentity.js +49 -0
  69. package/dist/eventRecord.d.ts +26 -0
  70. package/dist/eventRecord.js +53 -0
  71. package/dist/g42-probe.d.ts +9 -0
  72. package/dist/g42-probe.js +9 -0
  73. package/dist/http/SerializedQueryWorker.d.ts +58 -0
  74. package/dist/http/SerializedQueryWorker.js +497 -0
  75. package/dist/http/commitResponse.d.ts +46 -0
  76. package/dist/http/commitResponse.js +80 -0
  77. package/dist/http/testServiceId.d.ts +17 -0
  78. package/dist/http/testServiceId.js +14 -0
  79. package/dist/index.d.ts +112 -0
  80. package/dist/index.js +19926 -0
  81. package/dist/journal/JournalDurableObject.d.ts +30 -0
  82. package/dist/journal/JournalDurableObject.js +481 -0
  83. package/dist/journal/JournalFirstTouchProbe.d.ts +190 -0
  84. package/dist/journal/JournalFirstTouchProbe.js +375 -0
  85. package/dist/journal/types.d.ts +116 -0
  86. package/dist/journal/types.js +52 -0
  87. package/dist/mv/MaterializedViewCatchUp.d.ts +60 -0
  88. package/dist/mv/MaterializedViewCatchUp.js +353 -0
  89. package/dist/mv/MaterializedViewRuntime.d.ts +80 -0
  90. package/dist/mv/MaterializedViewRuntime.js +328 -0
  91. package/dist/mv/MaterializedViewStore.d.ts +257 -0
  92. package/dist/mv/MaterializedViewStore.js +875 -0
  93. package/dist/mv/UnsafeWindowMaterializedView.d.ts +112 -0
  94. package/dist/mv/UnsafeWindowMaterializedView.js +495 -0
  95. package/dist/mv/index.d.ts +8 -0
  96. package/dist/mv/index.js +2357 -0
  97. package/dist/projection/LiveProjectionWorker.d.ts +86 -0
  98. package/dist/projection/LiveProjectionWorker.js +240 -0
  99. package/dist/projection/ProjectionRuntime.d.ts +113 -0
  100. package/dist/projection/ProjectionRuntime.js +346 -0
  101. package/dist/projection/ProjectorRegistry.d.ts +51 -0
  102. package/dist/projection/ProjectorRegistry.js +116 -0
  103. package/dist/query/ProjectionQueryStore.d.ts +102 -0
  104. package/dist/query/ProjectionQueryStore.js +185 -0
  105. package/dist/query/QueryRegistry.d.ts +22 -0
  106. package/dist/query/QueryRegistry.js +36 -0
  107. package/dist/read/SerializedReadWorker.d.ts +36 -0
  108. package/dist/read/SerializedReadWorker.js +225 -0
  109. package/dist/repair/RepairWorker.d.ts +72 -0
  110. package/dist/repair/RepairWorker.js +461 -0
  111. package/dist/safeWindow.d.ts +20 -0
  112. package/dist/safeWindow.js +36 -0
  113. package/dist/scope/ControlRouteScope.d.ts +22 -0
  114. package/dist/scope/ControlRouteScope.js +36 -0
  115. package/dist/scope/ScopeName.d.ts +34 -0
  116. package/dist/scope/ScopeName.js +67 -0
  117. package/dist/service/ServiceIdentityProvider.d.ts +35 -0
  118. package/dist/service/ServiceIdentityProvider.js +56 -0
  119. package/dist/store/CosmosEventStore.d.ts +126 -0
  120. package/dist/store/CosmosEventStore.js +950 -0
  121. package/dist/store/D1EventStore.d.ts +111 -0
  122. package/dist/store/D1EventStore.js +1111 -0
  123. package/dist/store/PostgresEventStore.d.ts +35 -0
  124. package/dist/store/PostgresEventStore.js +811 -0
  125. package/dist/store/provider.d.ts +25 -0
  126. package/dist/store/provider.js +32 -0
  127. package/dist/store/types.d.ts +205 -0
  128. package/dist/store/types.js +18 -0
  129. package/dist/tag/EventDigest.d.ts +40 -0
  130. package/dist/tag/EventDigest.js +158 -0
  131. package/dist/tag/TagDurableObject.d.ts +286 -0
  132. package/dist/tag/TagDurableObject.js +4071 -0
  133. package/dist/tag/TagSqlMeasurement.d.ts +26 -0
  134. package/dist/tag/TagSqlMeasurement.js +44 -0
  135. package/dist/tag/TagSqlSchema.d.ts +29 -0
  136. package/dist/tag/TagSqlSchema.js +211 -0
  137. package/dist/tag/types.d.ts +173 -0
  138. package/dist/tag/types.js +3 -0
  139. package/dist/tagstate/TagStateDurableObject.d.ts +57 -0
  140. package/dist/tagstate/TagStateDurableObject.js +499 -0
  141. package/dist/trace/AttributionRatio.d.ts +12 -0
  142. package/dist/trace/AttributionRatio.js +68 -0
  143. package/dist/trace/CloudflareTracing.d.ts +7 -0
  144. package/dist/trace/CloudflareTracing.js +9 -0
  145. package/dist/trace/CommitTrace.d.ts +405 -0
  146. package/dist/trace/CommitTrace.js +1008 -0
  147. package/dist/trace/CommitTraceConsoleSink.d.ts +68 -0
  148. package/dist/trace/CommitTraceConsoleSink.js +114 -0
  149. package/dist/trace/CommitTraceVerifier.d.ts +22 -0
  150. package/dist/trace/CommitTraceVerifier.js +295 -0
  151. package/dist/trace/ObservationStream.d.ts +118 -0
  152. package/dist/trace/ObservationStream.js +148 -0
  153. package/package.json +71 -0
@@ -0,0 +1,25 @@
1
+ import type { PipelineStore } from "./types";
2
+ import { type BootstrapStoreAdapter } from "../bootstrap/BootstrapStoreAdapter";
3
+ /** The storage bindings visible to a provider; no provider may inspect request headers. */
4
+ export interface StoreProviderEnvironment {
5
+ readonly POSTGRES_URL?: string;
6
+ readonly HYPERDRIVE?: {
7
+ readonly connectionString?: string;
8
+ };
9
+ /** Explicit opt-in D1 binding; the default provider never requires it. */
10
+ readonly D1?: D1Database;
11
+ /** Separate opt-in MV D1 binding; never used by the PipelineStore provider. */
12
+ readonly D1_MV?: D1Database;
13
+ }
14
+ /** Typed composition seam shared by the default Postgres and opt-in adapters. */
15
+ export interface StoreProvider {
16
+ readonly name: string;
17
+ readonly isConfigured?: (env: StoreProviderEnvironment) => boolean;
18
+ create(env: StoreProviderEnvironment): PipelineStore;
19
+ createBootstrapAdapter?(env: StoreProviderEnvironment): BootstrapStoreAdapter;
20
+ }
21
+ /** The unchanged default composition. It never requires Cosmos bindings or secrets. */
22
+ export declare const POSTGRES_STORE_PROVIDER: StoreProvider;
23
+ export declare function createPostgresStoreProvider(): StoreProvider;
24
+ /** Explicit bootstrap adapter for the concrete Postgres PipelineStore. */
25
+ export declare function createPostgresBootstrapAdapter(store: PipelineStore): BootstrapStoreAdapter;
@@ -0,0 +1,32 @@
1
+ import { PostgresEventStore } from "./PostgresEventStore";
2
+ import { bootstrapEventIdentityMatches, createBootstrapStoreAdapter } from "../bootstrap/BootstrapStoreAdapter";
3
+ function postgresConnectionString(env) {
4
+ const connectionString = env.HYPERDRIVE?.connectionString ?? env.POSTGRES_URL;
5
+ if (connectionString === undefined || connectionString.length === 0) {
6
+ throw new Error("A Hyperdrive binding or POSTGRES_URL is required for the Postgres store");
7
+ }
8
+ return connectionString;
9
+ }
10
+ /** The unchanged default composition. It never requires Cosmos bindings or secrets. */
11
+ export const POSTGRES_STORE_PROVIDER = Object.freeze({
12
+ name: "postgres",
13
+ isConfigured: (env) => {
14
+ const connectionString = env.HYPERDRIVE?.connectionString ?? env.POSTGRES_URL;
15
+ return connectionString !== undefined && connectionString.length > 0;
16
+ },
17
+ create(env) {
18
+ return new PostgresEventStore(postgresConnectionString(env));
19
+ },
20
+ createBootstrapAdapter(env) {
21
+ return createPostgresBootstrapAdapter(this.create(env));
22
+ },
23
+ });
24
+ export function createPostgresStoreProvider() {
25
+ return POSTGRES_STORE_PROVIDER;
26
+ }
27
+ /** Explicit bootstrap adapter for the concrete Postgres PipelineStore. */
28
+ export function createPostgresBootstrapAdapter(store) {
29
+ // Kept in the Postgres composition module so its guard cannot leak into the
30
+ // Cloudflare-only bundle.
31
+ return createBootstrapStoreAdapter("postgres", store, (event, record) => bootstrapEventIdentityMatches(event, record));
32
+ }
@@ -0,0 +1,205 @@
1
+ import type { DeliverySource, DownstreamOutboxMessage } from "../downstream/types";
2
+ import type { EventProvenance } from "../eventIdentity";
3
+ /** A provider must reject canonical-key-only identity divergence before any durable write. */
4
+ export declare class CanonicalEventIdentityConflictError extends Error {
5
+ readonly provider: string;
6
+ readonly eventId: string;
7
+ readonly code: string;
8
+ constructor(provider: string, eventId: string, message?: string);
9
+ }
10
+ export interface DeliveryLagRecord {
11
+ serviceId: string;
12
+ eventId: string;
13
+ tag: string;
14
+ enqueuedAt: number;
15
+ arrivedAt: number;
16
+ lagMs: number;
17
+ }
18
+ export interface StoredEvent {
19
+ serviceId: string;
20
+ /** C# logical-record Id; eventId remains the runtime alias. */
21
+ id: string;
22
+ eventId: string;
23
+ /** C# logical-record SortableUniqueId; suid remains the runtime alias. */
24
+ sortableUniqueId: string;
25
+ suid: string;
26
+ /** Byte-identical decoded UTF-8 JSON text. */
27
+ payload: string;
28
+ /** C# logical-record Tags in source emission order. */
29
+ tags: string[];
30
+ /** Complete durable tag membership, retained from the outbox envelope. */
31
+ eventTags: string[];
32
+ /** C# durable EventType = eventPayloadName. */
33
+ eventType: string;
34
+ timestamp: string;
35
+ /** C# DbEvent keeps serialized metadata nullable for imported historical rows. */
36
+ causationId: string | null;
37
+ correlationId: string | null;
38
+ executedUser: string | null;
39
+ /** G32 internal provenance; it is never persisted in dcb_events. */
40
+ provenance: EventProvenance;
41
+ /** G43 source digest retained by the global receipt authority when present. */
42
+ eventDigest?: string;
43
+ firstArrivedAt: number;
44
+ lastArrivedAt: number;
45
+ maxDeliveryLagMs: number;
46
+ arrivals: DeliveryLagRecord[];
47
+ }
48
+ /** Per-invocation diagnostic ownership; never part of admission semantics. */
49
+ export interface DeliveryAttemptContext {
50
+ /** The platform Queue wrapper identity, when the caller has one. */
51
+ readonly queueMessageId?: string | null;
52
+ /** Registers diagnostic work with the current invocation lifetime. */
53
+ readonly waitUntil?: (promise: Promise<void>) => void;
54
+ }
55
+ export declare const DELIVERY_INCIDENT_CLASSIFICATIONS: readonly ["SUID_COLLISION", "ORDER_VIOLATION", "LINEAGE_MISMATCH", "ORDERING_DETECTOR_UNKNOWN"];
56
+ export type DeliveryIncidentClassification = (typeof DELIVERY_INCIDENT_CLASSIFICATIONS)[number];
57
+ /** A durable, idempotent fact for a poisoned delivery or provider ordering defect. */
58
+ export interface DeliveryIncident {
59
+ serviceId: string;
60
+ identityKey: string;
61
+ classification: DeliveryIncidentClassification;
62
+ suid?: string;
63
+ existingEventId?: string;
64
+ incomingEventId?: string;
65
+ eventId?: string;
66
+ boundLineageId?: string;
67
+ incomingLineageId?: string;
68
+ observedAt: number;
69
+ }
70
+ export type DeliveryOutcome = {
71
+ outcome: "stored";
72
+ kind: "stored";
73
+ event: StoredEvent;
74
+ duplicate?: boolean;
75
+ mutationEvidence?: "first-admission" | "duplicate-admission" | "unverified";
76
+ } | {
77
+ outcome: "suid-collision";
78
+ kind: "suid-collision";
79
+ incident: DeliveryIncident;
80
+ } | {
81
+ outcome: "lineage-mismatch";
82
+ kind: "lineage-mismatch";
83
+ incident: DeliveryIncident;
84
+ };
85
+ /** A receipt is useful only when this complete event/membership join matches. */
86
+ export interface GlobalReceiptJoin {
87
+ readonly serviceId: string;
88
+ readonly eventId: string;
89
+ readonly partitionTag: string;
90
+ readonly obligationSequence: number;
91
+ readonly eventDigest: string;
92
+ readonly membershipTag: string;
93
+ readonly receivedAt: number;
94
+ }
95
+ /**
96
+ * A projection checkpoint stores both its opaque projector state and its
97
+ * source SUID position. They advance atomically so a restarted poller cannot
98
+ * double-apply an event that was already reflected in durable state.
99
+ */
100
+ export interface ProjectionCheckpoint {
101
+ serviceId: string;
102
+ projectionId: string;
103
+ lastSuid: string;
104
+ stateJson: string;
105
+ version: number;
106
+ updatedAt: number;
107
+ }
108
+ export interface ProjectionCheckpointAdvance {
109
+ serviceId: string;
110
+ projectionId: string;
111
+ /** Null means this is the first checkpoint for the projection. */
112
+ expectedLastSuid: string | null;
113
+ lastSuid: string;
114
+ stateJson: string;
115
+ version: number;
116
+ updatedAt: number;
117
+ }
118
+ export interface ProjectionLag {
119
+ serviceId: string;
120
+ projectionId: string;
121
+ tag: string;
122
+ checkpointSuid: string;
123
+ headSuid: string;
124
+ behindEvents: number;
125
+ }
126
+ export interface PendingArrivalRecord {
127
+ serviceId: string;
128
+ attemptId: string;
129
+ eventId: string;
130
+ suid: string;
131
+ expectedPaths: string[];
132
+ observedPaths: string[];
133
+ firstObservedAt: number;
134
+ lagBoundMs: number;
135
+ }
136
+ export type InconsistencyClassification = "MISSING_STABLE" | "EXCLUDED_AUDITED" | "RESOLVED_LATE";
137
+ export interface InconsistencyFinding {
138
+ serviceId: string;
139
+ eventId: string;
140
+ path: string;
141
+ classification: InconsistencyClassification;
142
+ firstObservedAt: number;
143
+ lagBoundMs: number;
144
+ observedAt: number;
145
+ }
146
+ /** Adapter-only persistence port. The detector deliberately does not receive it. */
147
+ export interface EventStore {
148
+ initialize(): Promise<void>;
149
+ recordDelivery(message: DownstreamOutboxMessage, arrivedAt: number, deliverySource?: DeliverySource, attemptContext?: DeliveryAttemptContext): Promise<DeliveryOutcome>;
150
+ readAllEvents(serviceId: string, since: string): Promise<StoredEvent[]>;
151
+ currentLagBound(serviceId: string, nowMs?: number): Promise<number>;
152
+ /** Conformance-only read diagnostic; production callers must not use it. */
153
+ lagBoundDiagnostics?(serviceId: string, nowMs: number): Promise<{
154
+ serviceIdUsed: string;
155
+ dynamicLagBoundMs: number;
156
+ rowFound: boolean;
157
+ rawEstimateMs: number | null;
158
+ rawObservedAt: number | null;
159
+ nowMs: number;
160
+ }>;
161
+ /** Cosmos uses this optional retryable async landing projection. */
162
+ projectDeliveryIncidents?(serviceId?: string): Promise<number>;
163
+ /**
164
+ * G44 source acknowledgement must be based on this read-back join, never
165
+ * on a transport acknowledgement or a receipt row by itself.
166
+ */
167
+ readGlobalReceiptJoin?(message: DownstreamOutboxMessage): Promise<GlobalReceiptJoin | undefined>;
168
+ }
169
+ /** Detector-only persistence port. It cannot write event rows by construction. */
170
+ export interface DetectorStore {
171
+ upsertPending(message: DownstreamOutboxMessage, firstObservedAt: number, lagBoundMs: number): Promise<PendingArrivalRecord>;
172
+ listPending(serviceId?: string): Promise<PendingArrivalRecord[]>;
173
+ appendFinding(finding: InconsistencyFinding): Promise<void>;
174
+ hasFinding(serviceId: string, eventId: string, path: string, classification: InconsistencyClassification): Promise<boolean>;
175
+ listFindings(serviceId?: string, eventId?: string): Promise<InconsistencyFinding[]>;
176
+ appendDeliveryIncident(incident: DeliveryIncident): Promise<void>;
177
+ hasDeliveryIncident(serviceId: string, identityKey: string): Promise<boolean>;
178
+ listDeliveryIncidents(serviceId?: string): Promise<DeliveryIncident[]>;
179
+ }
180
+ /**
181
+ * Read-only event consumption plus durable checkpoint operations. This port
182
+ * intentionally omits recordDelivery, keeping SafeWindow projection unable to
183
+ * write event data by construction.
184
+ */
185
+ export interface ProjectionStore {
186
+ readAllEvents(serviceId: string, since: string): Promise<StoredEvent[]>;
187
+ /** Optional D1-backed detector for a newly admitted lower SUID. */
188
+ findLateLowerSuid?(serviceId: string, checkpointSuid: string, checkpointUpdatedAt: number): Promise<StoredEvent | undefined>;
189
+ /** Structured detector result; unknown evidence must not become a refusal. */
190
+ findLateLowerSuidEvidence?(serviceId: string, checkpointSuid: string, checkpointUpdatedAt: number, generation?: number): Promise<{
191
+ readonly kind: "late-lower-suid" | "replay" | "miss" | "unknown";
192
+ readonly event?: StoredEvent;
193
+ readonly reason?: string;
194
+ /** Duration of the bounded detector query, when the provider measures it. */
195
+ readonly durationMs?: number;
196
+ }>;
197
+ currentLagBound(serviceId: string, nowMs?: number): Promise<number>;
198
+ listProjectionTags(serviceId: string): Promise<string[]>;
199
+ readProjectionCheckpoint(serviceId: string, projectionId: string): Promise<ProjectionCheckpoint | undefined>;
200
+ advanceProjectionCheckpoint(input: ProjectionCheckpointAdvance): Promise<boolean>;
201
+ projectionLag(serviceId: string, projectionId: string, tag: string): Promise<ProjectionLag>;
202
+ /** Catch-up must persist an ORDER_VIOLATION before failing closed. */
203
+ appendDeliveryIncident(incident: DeliveryIncident): Promise<void>;
204
+ }
205
+ export type PipelineStore = EventStore & DetectorStore & ProjectionStore;
@@ -0,0 +1,18 @@
1
+ /** A provider must reject canonical-key-only identity divergence before any durable write. */
2
+ export class CanonicalEventIdentityConflictError extends Error {
3
+ provider;
4
+ eventId;
5
+ code = "CANONICAL_EVENT_IDENTITY_CONFLICT";
6
+ constructor(provider, eventId, message) {
7
+ super(message ?? `${provider} EventId ${eventId} conflicts with its canonical event identity`);
8
+ this.provider = provider;
9
+ this.eventId = eventId;
10
+ this.name = "CanonicalEventIdentityConflictError";
11
+ }
12
+ }
13
+ export const DELIVERY_INCIDENT_CLASSIFICATIONS = [
14
+ "SUID_COLLISION",
15
+ "ORDER_VIOLATION",
16
+ "LINEAGE_MISMATCH",
17
+ "ORDERING_DETECTOR_UNKNOWN",
18
+ ];
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Runtime implementation of the packet-owned G43 eventDigest v2 byte
3
+ * contract. The companion checker deliberately reimplements this framing
4
+ * from `contracts/g43-digest-spec.json`; do not share helpers with it.
5
+ */
6
+ export declare const EVENT_DIGEST_DOMAIN_SEPARATOR = "sekiban-dcb-ts/eventDigest/v2";
7
+ export interface EventDigestInput {
8
+ readonly serviceId: string;
9
+ readonly eventId: string;
10
+ readonly sortableUniqueId: string;
11
+ readonly eventType: string;
12
+ /** Exact persisted UTC spelling; this encoder never reparses it. */
13
+ readonly timestamp: string;
14
+ readonly allocatorLineageId?: string;
15
+ readonly attemptId: string;
16
+ readonly declaredTagSet: readonly string[];
17
+ /** Exact persisted payload bytes. Never decoded or text-normalised. */
18
+ readonly payload: Uint8Array;
19
+ /** Closed, explicitly excluded delivery-envelope fields. */
20
+ readonly causationId?: unknown;
21
+ readonly correlationId?: unknown;
22
+ readonly executedUser?: unknown;
23
+ }
24
+ export declare class EventDigestValidationError extends TypeError {
25
+ constructor(message: string);
26
+ }
27
+ /**
28
+ * Canonical set semantics are intentionally byte based: sorting uses UTF-8
29
+ * code units and exact byte-equal duplicates collapse to one element. Empty
30
+ * tag strings are representable by the byte contract even though normal tag
31
+ * admission rejects them before an event reaches this encoder.
32
+ */
33
+ export declare function canonicalDeclaredTagSet(tags: readonly string[]): readonly string[];
34
+ /**
35
+ * Produces the exact preimage bytes that are stored alongside an obligation.
36
+ * The caller can retain these canonical bytes while `eventDigestHex` hashes
37
+ * them without applying a second serialization.
38
+ */
39
+ export declare function eventDigestBytes(input: EventDigestInput): Uint8Array;
40
+ export declare function eventDigestHex(input: EventDigestInput): Promise<string>;
@@ -0,0 +1,158 @@
1
+ /**
2
+ * Runtime implementation of the packet-owned G43 eventDigest v2 byte
3
+ * contract. The companion checker deliberately reimplements this framing
4
+ * from `contracts/g43-digest-spec.json`; do not share helpers with it.
5
+ */
6
+ export const EVENT_DIGEST_DOMAIN_SEPARATOR = "sekiban-dcb-ts/eventDigest/v2";
7
+ export class EventDigestValidationError extends TypeError {
8
+ constructor(message) {
9
+ super(message);
10
+ this.name = "EventDigestValidationError";
11
+ }
12
+ }
13
+ const encoder = new TextEncoder();
14
+ const requiredFieldNames = [
15
+ "serviceId",
16
+ "eventId",
17
+ "sortableUniqueId",
18
+ "eventType",
19
+ "timestamp",
20
+ "attemptId",
21
+ "declaredTagSet",
22
+ "payload",
23
+ ];
24
+ const optionalFieldNames = ["allocatorLineageId"];
25
+ const excludedFieldNames = ["causationId", "correlationId", "executedUser"];
26
+ const allowedFieldNames = new Set([...requiredFieldNames, ...optionalFieldNames, ...excludedFieldNames]);
27
+ function byteCompare(left, right) {
28
+ const length = Math.min(left.byteLength, right.byteLength);
29
+ for (let index = 0; index < length; index += 1) {
30
+ const delta = left[index] - right[index];
31
+ if (delta !== 0)
32
+ return delta;
33
+ }
34
+ return left.byteLength - right.byteLength;
35
+ }
36
+ function concat(parts) {
37
+ const length = parts.reduce((total, part) => total + part.byteLength, 0);
38
+ const bytes = new Uint8Array(length);
39
+ let offset = 0;
40
+ for (const part of parts) {
41
+ bytes.set(part, offset);
42
+ offset += part.byteLength;
43
+ }
44
+ return bytes;
45
+ }
46
+ function lengthPrefix(length) {
47
+ if (!Number.isSafeInteger(length) || length < 0 || length > 0xffff_ffff) {
48
+ throw new EventDigestValidationError("eventDigest field length is outside uint32 range");
49
+ }
50
+ return Uint8Array.of((length >>> 24) & 0xff, (length >>> 16) & 0xff, (length >>> 8) & 0xff, length & 0xff);
51
+ }
52
+ function utf8(value) {
53
+ return encoder.encode(value);
54
+ }
55
+ function hasOwn(input, name) {
56
+ return Object.prototype.hasOwnProperty.call(input, name);
57
+ }
58
+ function requiredUtf8(input, name) {
59
+ const value = input[name];
60
+ if (typeof value !== "string") {
61
+ throw new EventDigestValidationError(`eventDigest required field ${name} is absent or not a string`);
62
+ }
63
+ return utf8(value);
64
+ }
65
+ function tagSetBytes(tags) {
66
+ const canonical = canonicalDeclaredTagSet(tags);
67
+ return concat(canonical.map((tag) => {
68
+ const bytes = utf8(tag);
69
+ return concat([lengthPrefix(bytes.byteLength), bytes]);
70
+ }));
71
+ }
72
+ /**
73
+ * Canonical set semantics are intentionally byte based: sorting uses UTF-8
74
+ * code units and exact byte-equal duplicates collapse to one element. Empty
75
+ * tag strings are representable by the byte contract even though normal tag
76
+ * admission rejects them before an event reaches this encoder.
77
+ */
78
+ export function canonicalDeclaredTagSet(tags) {
79
+ if (!Array.isArray(tags) || !tags.every((tag) => typeof tag === "string")) {
80
+ throw new EventDigestValidationError("eventDigest declaredTagSet must contain only strings");
81
+ }
82
+ const encoded = tags
83
+ .map((tag) => ({ tag, bytes: utf8(tag) }))
84
+ .sort((left, right) => byteCompare(left.bytes, right.bytes));
85
+ const unique = [];
86
+ let previous;
87
+ for (const item of encoded) {
88
+ if (previous !== undefined && byteCompare(previous, item.bytes) === 0)
89
+ continue;
90
+ unique.push(item.tag);
91
+ previous = item.bytes;
92
+ }
93
+ return Object.freeze(unique);
94
+ }
95
+ function assertClosedProjection(input) {
96
+ for (const name of Object.keys(input)) {
97
+ if (!allowedFieldNames.has(name)) {
98
+ throw new EventDigestValidationError(`eventDigest field ${name} is neither included nor explicitly excluded`);
99
+ }
100
+ }
101
+ for (const name of requiredFieldNames) {
102
+ if (!hasOwn(input, name)) {
103
+ throw new EventDigestValidationError(`eventDigest required field ${name} is absent`);
104
+ }
105
+ }
106
+ if (!Array.isArray(input.declaredTagSet)) {
107
+ throw new EventDigestValidationError("eventDigest required field declaredTagSet is absent or invalid");
108
+ }
109
+ if (!(input.payload instanceof Uint8Array)) {
110
+ throw new EventDigestValidationError("eventDigest required field payload is absent or not raw bytes");
111
+ }
112
+ if (hasOwn(input, "allocatorLineageId") && typeof input.allocatorLineageId !== "string") {
113
+ throw new EventDigestValidationError("eventDigest optional field allocatorLineageId must be absent or a string");
114
+ }
115
+ }
116
+ /** `encodeField` exactly as specified by contracts/g43-digest-spec.json. */
117
+ function encodeField(name, value) {
118
+ const nameBytes = utf8(name);
119
+ const present = value === undefined ? Uint8Array.of(0x00) : Uint8Array.of(0x01);
120
+ const valueBytes = value ?? new Uint8Array();
121
+ return concat([
122
+ present,
123
+ lengthPrefix(nameBytes.byteLength),
124
+ nameBytes,
125
+ lengthPrefix(valueBytes.byteLength),
126
+ valueBytes,
127
+ ]);
128
+ }
129
+ /**
130
+ * Produces the exact preimage bytes that are stored alongside an obligation.
131
+ * The caller can retain these canonical bytes while `eventDigestHex` hashes
132
+ * them without applying a second serialization.
133
+ */
134
+ export function eventDigestBytes(input) {
135
+ assertClosedProjection(input);
136
+ const fields = [
137
+ ["serviceId", requiredUtf8(input, "serviceId")],
138
+ ["eventId", requiredUtf8(input, "eventId")],
139
+ ["sortableUniqueId", requiredUtf8(input, "sortableUniqueId")],
140
+ ["eventType", requiredUtf8(input, "eventType")],
141
+ ["timestamp", requiredUtf8(input, "timestamp")],
142
+ ["allocatorLineageId", hasOwn(input, "allocatorLineageId") ? utf8(input.allocatorLineageId) : undefined],
143
+ ["attemptId", requiredUtf8(input, "attemptId")],
144
+ ["declaredTagSet", tagSetBytes(input.declaredTagSet)],
145
+ ["payload", input.payload],
146
+ ];
147
+ return concat([
148
+ utf8(EVENT_DIGEST_DOMAIN_SEPARATOR),
149
+ Uint8Array.of(0x00),
150
+ ...fields.map(([name, value]) => encodeField(name, value)),
151
+ ]);
152
+ }
153
+ export async function eventDigestHex(input) {
154
+ const bytes = eventDigestBytes(input);
155
+ const source = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
156
+ const digest = await crypto.subtle.digest("SHA-256", source);
157
+ return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
158
+ }