@sekiban/dcb-core 0.2.2 → 0.3.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.
package/README.md CHANGED
@@ -8,6 +8,15 @@ surface available from `@sekiban/dcb-core`.
8
8
 
9
9
  It is not the earlier, unrelated `@sekiban/core` package line.
10
10
 
11
+ ## Query contracts
12
+
13
+ The root export includes structural query contracts and nothing that runs them.
14
+ `QueryContract` and `ListQueryContract` carry a kind, an id, a version, and
15
+ bidirectional `encode` / `decode` codecs. `QueryPage` is the list result shape.
16
+ There is no handler, schema, or source view on these contracts. A later client
17
+ can accept a contract plus params and return the decoded result; this package
18
+ does not provide that call.
19
+
11
20
  ## License
12
21
 
13
22
  Elastic License 2.0. See [LICENSE](./LICENSE).
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Conditional-commit wire values and deterministic helpers for the public
3
+ * versioned conditional-commit route.
4
+ */
5
+ export declare const CONDITIONAL_COMMIT_VERSION: 1;
6
+ export declare const CONDITIONAL_COMMIT_DERIVATION_VERSION: 2;
7
+ export declare const CONDITIONAL_COMMIT_CANONICALIZATION_VERSION: 1;
8
+ export declare const CONDITIONAL_COMMIT_DOMAIN_SEPARATOR: "sekiban.dcb.conditional-append.unique-key";
9
+ export declare const CONDITIONAL_COMMIT_MAX_UTF8_FIELD_BYTES: 512;
10
+ export declare const CONDITIONAL_COMMIT_MAX_PAYLOAD_BYTES: number;
11
+ export declare const CONDITIONAL_COMMIT_MAX_REQUEST_BYTES: number;
12
+ export type ConditionalCommitValidationCode = "conditional_commit_invalid_json" | "conditional_commit_unknown_member" | "conditional_commit_invalid_version" | "conditional_commit_invalid_envelope" | "conditional_commit_invalid_key" | "conditional_commit_invalid_candidate" | "conditional_commit_invalid_base64" | "conditional_commit_invalid_utf8" | "conditional_commit_invalid_payload_json" | "conditional_commit_payload_root_not_object" | "conditional_commit_payload_not_canonicalizable" | "conditional_commit_event_not_registered" | "conditional_commit_invalid_tag_count" | "conditional_commit_size_limit_exceeded";
13
+ export type ConditionalCommitInDoubtReason = "winner_unreadable" | "ambiguous_after_append" | "committed_state_unverified" | "live_precondition_blocked" | "bootstrap_epoch_changed";
14
+ export interface SerializedConditionalEventCandidate {
15
+ readonly payload: string;
16
+ readonly eventPayloadName: string;
17
+ readonly tags: readonly [string];
18
+ }
19
+ export interface SerializedConditionalCommitRequest {
20
+ readonly version: 1;
21
+ readonly idempotencyKey: string;
22
+ readonly eventCandidate: SerializedConditionalEventCandidate;
23
+ }
24
+ export interface ConditionalCommitReceipt {
25
+ readonly winnerEventId: string;
26
+ readonly winnerSortableUniqueId: string;
27
+ readonly operationFingerprint: string;
28
+ }
29
+ export interface ConditionalWrittenEvent {
30
+ readonly payload: string;
31
+ readonly sortableUniqueIdValue: string;
32
+ readonly id: string;
33
+ readonly eventMetadata: {
34
+ readonly causationId: string;
35
+ readonly correlationId: "SerializedCommit";
36
+ readonly executedUser: "SerializedSekibanExecutor";
37
+ };
38
+ readonly tags: readonly [string];
39
+ readonly eventPayloadName: string;
40
+ }
41
+ export type ConditionalCommitResult = {
42
+ readonly version: 1;
43
+ readonly outcome: "Appended";
44
+ readonly receipt: ConditionalCommitReceipt;
45
+ readonly writtenEvents: readonly [ConditionalWrittenEvent];
46
+ } | {
47
+ readonly version: 1;
48
+ readonly outcome: "AlreadyCommittedSameOperation";
49
+ readonly receipt: ConditionalCommitReceipt;
50
+ readonly writtenEvents: readonly [];
51
+ } | {
52
+ readonly version: 1;
53
+ readonly outcome: "KeyReuseConflict";
54
+ readonly code: "conditional_commit_key_reuse_conflict";
55
+ readonly error: "The idempotency key is already bound to a different operation.";
56
+ readonly retryable: false;
57
+ } | {
58
+ readonly version: 1;
59
+ readonly outcome: "Unsupported";
60
+ readonly code: "conditional_commit_unsupported";
61
+ readonly error: "Conditional commit is not supported by this deployment.";
62
+ readonly retryable: false;
63
+ };
64
+ export type ConditionalCommitErrorResponse = {
65
+ readonly version: 1;
66
+ readonly code: ConditionalCommitValidationCode;
67
+ readonly error: string;
68
+ readonly retryable: false;
69
+ } | {
70
+ readonly version: 1;
71
+ readonly code: "conditional_commit_in_doubt";
72
+ readonly error: "The conditional commit outcome is not yet verified; resend the same prepared request.";
73
+ readonly retryable: true;
74
+ readonly reason: ConditionalCommitInDoubtReason;
75
+ } | {
76
+ readonly version: 1;
77
+ readonly code: "conditional_commit_committed_state_corrupt";
78
+ readonly error: "The durable conditional commit state could not be verified.";
79
+ readonly retryable: false;
80
+ };
81
+ export interface ConditionalCommitFingerprintInput {
82
+ readonly serviceId: string;
83
+ readonly idempotencyKey: string;
84
+ readonly eventPayloadName: string;
85
+ readonly canonicalPayload: Uint8Array;
86
+ readonly tag: string;
87
+ }
88
+ export declare class ConditionalCommitProtocolError extends Error {
89
+ readonly code: ConditionalCommitValidationCode;
90
+ readonly retryable = false;
91
+ constructor(code: ConditionalCommitValidationCode);
92
+ }
93
+ export declare function normalizeConditionalCommitKey(value: string): string;
94
+ export declare function canonicalizeConditionalCommitPayload(value: unknown): Uint8Array;
95
+ export declare function conditionalCommitFingerprintPreimage(input: ConditionalCommitFingerprintInput): Uint8Array;
96
+ export declare function computeConditionalCommitFingerprint(input: ConditionalCommitFingerprintInput): Promise<string>;
97
+ export declare function parseSerializedConditionalCommitRequest(value: unknown): SerializedConditionalCommitRequest;
98
+ export declare function parseSerializedConditionalCommitRequestBody(body: string | Uint8Array): SerializedConditionalCommitRequest;
@@ -0,0 +1,479 @@
1
+ /**
2
+ * Conditional-commit wire values and deterministic helpers for the public
3
+ * versioned conditional-commit route.
4
+ */
5
+ export const CONDITIONAL_COMMIT_VERSION = 1;
6
+ export const CONDITIONAL_COMMIT_DERIVATION_VERSION = 2;
7
+ export const CONDITIONAL_COMMIT_CANONICALIZATION_VERSION = 1;
8
+ export const CONDITIONAL_COMMIT_DOMAIN_SEPARATOR = "sekiban.dcb.conditional-append.unique-key";
9
+ export const CONDITIONAL_COMMIT_MAX_UTF8_FIELD_BYTES = 512;
10
+ export const CONDITIONAL_COMMIT_MAX_PAYLOAD_BYTES = 192 * 1024;
11
+ export const CONDITIONAL_COMMIT_MAX_REQUEST_BYTES = 320 * 1024;
12
+ const INT32_MIN = -2147483648;
13
+ const INT32_MAX = 2147483647;
14
+ const BASE64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
15
+ const BASE64_INDEX = new Map([...BASE64_ALPHABET].map((char, index) => [char, index]));
16
+ const VALIDATION_MESSAGES = Object.freeze({
17
+ conditional_commit_invalid_json: "The request body is not valid JSON.",
18
+ conditional_commit_unknown_member: "The request contains an unknown member.",
19
+ conditional_commit_invalid_version: "The conditional commit version is not supported.",
20
+ conditional_commit_invalid_envelope: "The conditional commit envelope is invalid.",
21
+ conditional_commit_invalid_key: "The idempotency key is invalid.",
22
+ conditional_commit_invalid_candidate: "The event candidate is invalid.",
23
+ conditional_commit_invalid_base64: "The event payload is not valid base64.",
24
+ conditional_commit_invalid_utf8: "The event payload is not valid UTF-8.",
25
+ conditional_commit_invalid_payload_json: "The event payload is not valid JSON.",
26
+ conditional_commit_payload_root_not_object: "The event payload root must be a plain object.",
27
+ conditional_commit_payload_not_canonicalizable: "The event payload cannot be canonicalized.",
28
+ conditional_commit_event_not_registered: "The event payload name is not registered.",
29
+ conditional_commit_invalid_tag_count: "Conditional commit requires exactly one tag.",
30
+ conditional_commit_size_limit_exceeded: "The conditional commit request exceeds a size limit.",
31
+ });
32
+ export class ConditionalCommitProtocolError extends Error {
33
+ code;
34
+ retryable = false;
35
+ constructor(code) {
36
+ super(VALIDATION_MESSAGES[code]);
37
+ this.name = "ConditionalCommitProtocolError";
38
+ this.code = code;
39
+ }
40
+ }
41
+ function fail(code) {
42
+ throw new ConditionalCommitProtocolError(code);
43
+ }
44
+ /** Ill-formed UTF-16 must fail before trim, NFC, or UTF-8 replacement. */
45
+ function assertWellFormedUtf16(value, code) {
46
+ for (let index = 0; index < value.length; index += 1) {
47
+ const unit = value.charCodeAt(index);
48
+ if (unit >= 0xd800 && unit <= 0xdbff) {
49
+ const next = index + 1 < value.length ? value.charCodeAt(index + 1) : -1;
50
+ if (next < 0xdc00 || next > 0xdfff)
51
+ fail(code);
52
+ index += 1;
53
+ continue;
54
+ }
55
+ if (unit >= 0xdc00 && unit <= 0xdfff)
56
+ fail(code);
57
+ }
58
+ }
59
+ function isTrimCodePoint(codePoint) {
60
+ return (codePoint >= 0x0009 && codePoint <= 0x000d)
61
+ || codePoint === 0x0020
62
+ || codePoint === 0x0085
63
+ || codePoint === 0x00a0
64
+ || codePoint === 0x1680
65
+ || (codePoint >= 0x2000 && codePoint <= 0x200a)
66
+ || codePoint === 0x2028
67
+ || codePoint === 0x2029
68
+ || codePoint === 0x202f
69
+ || codePoint === 0x205f
70
+ || codePoint === 0x3000;
71
+ }
72
+ function trimDotNet(value) {
73
+ let start = 0;
74
+ let end = value.length;
75
+ while (start < end) {
76
+ const codePoint = value.codePointAt(start);
77
+ if (codePoint === undefined || !isTrimCodePoint(codePoint))
78
+ break;
79
+ start += codePoint > 0xffff ? 2 : 1;
80
+ }
81
+ while (end > start) {
82
+ const previous = value.charCodeAt(end - 1);
83
+ let codePoint = previous;
84
+ let width = 1;
85
+ if (previous >= 0xdc00 && previous <= 0xdfff && end >= 2) {
86
+ const high = value.charCodeAt(end - 2);
87
+ if (high >= 0xd800 && high <= 0xdbff) {
88
+ codePoint = value.codePointAt(end - 2) ?? previous;
89
+ width = 2;
90
+ }
91
+ }
92
+ if (!isTrimCodePoint(codePoint))
93
+ break;
94
+ end -= width;
95
+ }
96
+ return value.slice(start, end);
97
+ }
98
+ export function normalizeConditionalCommitKey(value) {
99
+ if (typeof value !== "string")
100
+ fail("conditional_commit_invalid_key");
101
+ assertWellFormedUtf16(value, "conditional_commit_invalid_key");
102
+ const normalized = trimDotNet(value).normalize("NFC");
103
+ const size = new TextEncoder().encode(normalized).byteLength;
104
+ if (size < 1 || size > CONDITIONAL_COMMIT_MAX_UTF8_FIELD_BYTES)
105
+ fail("conditional_commit_invalid_key");
106
+ return normalized;
107
+ }
108
+ function isPlainRecord(value) {
109
+ if (typeof value !== "object" || value === null || Array.isArray(value))
110
+ return false;
111
+ const prototype = Object.getPrototypeOf(value);
112
+ return prototype === Object.prototype || prototype === null;
113
+ }
114
+ function emitValue(value, active) {
115
+ if (value === null)
116
+ return "null";
117
+ if (typeof value === "boolean")
118
+ return value ? "true" : "false";
119
+ if (typeof value === "string") {
120
+ assertWellFormedUtf16(value, "conditional_commit_payload_not_canonicalizable");
121
+ return JSON.stringify(value);
122
+ }
123
+ if (typeof value === "number") {
124
+ if (!Number.isFinite(value))
125
+ fail("conditional_commit_payload_not_canonicalizable");
126
+ return JSON.stringify(value);
127
+ }
128
+ if (typeof value !== "object")
129
+ fail("conditional_commit_payload_not_canonicalizable");
130
+ if (Array.isArray(value))
131
+ return emitArray(value, active);
132
+ if (!isPlainRecord(value))
133
+ fail("conditional_commit_payload_not_canonicalizable");
134
+ return emitRecord(value, active);
135
+ }
136
+ function emitArray(value, active) {
137
+ if (active.has(value))
138
+ fail("conditional_commit_payload_not_canonicalizable");
139
+ const keys = Reflect.ownKeys(value);
140
+ const lengthDescriptor = Object.getOwnPropertyDescriptor(value, "length");
141
+ if (lengthDescriptor === undefined
142
+ || lengthDescriptor.enumerable === true
143
+ || lengthDescriptor.get !== undefined
144
+ || lengthDescriptor.set !== undefined) {
145
+ fail("conditional_commit_payload_not_canonicalizable");
146
+ }
147
+ for (const key of keys) {
148
+ if (key === "length")
149
+ continue;
150
+ if (typeof key !== "string" || !/^(?:0|[1-9]\d*)$/.test(key) || Number(key) >= value.length) {
151
+ fail("conditional_commit_payload_not_canonicalizable");
152
+ }
153
+ }
154
+ const parts = [];
155
+ active.add(value);
156
+ try {
157
+ for (let index = 0; index < value.length; index += 1) {
158
+ const descriptor = Object.getOwnPropertyDescriptor(value, String(index));
159
+ if (descriptor === undefined
160
+ || descriptor.enumerable !== true
161
+ || descriptor.get !== undefined
162
+ || descriptor.set !== undefined
163
+ || !("value" in descriptor)) {
164
+ fail("conditional_commit_payload_not_canonicalizable");
165
+ }
166
+ parts.push(emitValue(descriptor.value, active));
167
+ }
168
+ }
169
+ finally {
170
+ active.delete(value);
171
+ }
172
+ return `[${parts.join(",")}]`;
173
+ }
174
+ /** Object keys are emitted in UTF-16 order. JSON.stringify would reorder integer-like names. */
175
+ function emitRecord(value, active) {
176
+ if (active.has(value))
177
+ fail("conditional_commit_payload_not_canonicalizable");
178
+ const keys = Reflect.ownKeys(value);
179
+ const names = [];
180
+ const values = new Map();
181
+ for (const key of keys) {
182
+ if (typeof key !== "string")
183
+ fail("conditional_commit_payload_not_canonicalizable");
184
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
185
+ if (descriptor === undefined
186
+ || descriptor.enumerable !== true
187
+ || descriptor.get !== undefined
188
+ || descriptor.set !== undefined
189
+ || !("value" in descriptor)) {
190
+ fail("conditional_commit_payload_not_canonicalizable");
191
+ }
192
+ names.push(key);
193
+ values.set(key, descriptor.value);
194
+ }
195
+ names.sort();
196
+ const parts = [];
197
+ active.add(value);
198
+ try {
199
+ for (const key of names) {
200
+ parts.push(`${JSON.stringify(key)}:${emitValue(values.get(key), active)}`);
201
+ }
202
+ }
203
+ finally {
204
+ active.delete(value);
205
+ }
206
+ return `{${parts.join(",")}}`;
207
+ }
208
+ export function canonicalizeConditionalCommitPayload(value) {
209
+ if (!isPlainRecord(value))
210
+ fail("conditional_commit_payload_root_not_object");
211
+ const bytes = new TextEncoder().encode(emitRecord(value, new WeakSet()));
212
+ if (bytes.byteLength > CONDITIONAL_COMMIT_MAX_PAYLOAD_BYTES)
213
+ fail("conditional_commit_size_limit_exceeded");
214
+ return bytes;
215
+ }
216
+ function appendInt32BE(target, value) {
217
+ if (!Number.isSafeInteger(value) || value < INT32_MIN || value > INT32_MAX) {
218
+ fail("conditional_commit_size_limit_exceeded");
219
+ }
220
+ target.push((value >>> 24) & 0xff, (value >>> 16) & 0xff, (value >>> 8) & 0xff, value & 0xff);
221
+ }
222
+ function appendUtf8(target, value, code, maxBytes) {
223
+ if (typeof value !== "string")
224
+ fail(code);
225
+ assertWellFormedUtf16(value, code);
226
+ const bytes = new TextEncoder().encode(value);
227
+ if (bytes.byteLength < 1)
228
+ fail(code);
229
+ if (maxBytes !== undefined && bytes.byteLength > maxBytes)
230
+ fail("conditional_commit_size_limit_exceeded");
231
+ appendInt32BE(target, bytes.byteLength);
232
+ for (const byte of bytes)
233
+ target.push(byte);
234
+ }
235
+ function appendPayload(target, payload) {
236
+ if (!(payload instanceof Uint8Array))
237
+ fail("conditional_commit_payload_not_canonicalizable");
238
+ if (!Number.isSafeInteger(payload.byteLength) || payload.byteLength < 0) {
239
+ fail("conditional_commit_size_limit_exceeded");
240
+ }
241
+ if (payload.byteLength > CONDITIONAL_COMMIT_MAX_PAYLOAD_BYTES)
242
+ fail("conditional_commit_size_limit_exceeded");
243
+ appendInt32BE(target, payload.byteLength);
244
+ for (let index = 0; index < payload.byteLength; index += 1) {
245
+ const byte = payload[index];
246
+ if (byte === undefined)
247
+ fail("conditional_commit_payload_not_canonicalizable");
248
+ target.push(byte);
249
+ }
250
+ }
251
+ export function conditionalCommitFingerprintPreimage(input) {
252
+ if (typeof input !== "object" || input === null)
253
+ fail("conditional_commit_invalid_envelope");
254
+ const key = normalizeConditionalCommitKey(input.idempotencyKey);
255
+ const bytes = [];
256
+ appendInt32BE(bytes, CONDITIONAL_COMMIT_DERIVATION_VERSION);
257
+ appendInt32BE(bytes, CONDITIONAL_COMMIT_CANONICALIZATION_VERSION);
258
+ appendUtf8(bytes, CONDITIONAL_COMMIT_DOMAIN_SEPARATOR, "conditional_commit_invalid_envelope");
259
+ appendUtf8(bytes, input.serviceId, "conditional_commit_invalid_envelope");
260
+ appendUtf8(bytes, key, "conditional_commit_invalid_key", CONDITIONAL_COMMIT_MAX_UTF8_FIELD_BYTES);
261
+ appendUtf8(bytes, input.eventPayloadName, "conditional_commit_invalid_candidate", CONDITIONAL_COMMIT_MAX_UTF8_FIELD_BYTES);
262
+ appendPayload(bytes, input.canonicalPayload);
263
+ appendInt32BE(bytes, 1);
264
+ appendUtf8(bytes, input.tag, "conditional_commit_invalid_candidate", CONDITIONAL_COMMIT_MAX_UTF8_FIELD_BYTES);
265
+ return Uint8Array.from(bytes);
266
+ }
267
+ export async function computeConditionalCommitFingerprint(input) {
268
+ const preimage = conditionalCommitFingerprintPreimage(input);
269
+ const buffer = new ArrayBuffer(preimage.byteLength);
270
+ new Uint8Array(buffer).set(preimage);
271
+ const digest = await globalThis.crypto.subtle.digest("SHA-256", buffer);
272
+ let hex = "";
273
+ for (const byte of new Uint8Array(digest))
274
+ hex += byte.toString(16).padStart(2, "0");
275
+ return hex;
276
+ }
277
+ const REQUEST_KEYS = ["version", "idempotencyKey", "eventCandidate"];
278
+ const CANDIDATE_KEYS = ["payload", "eventPayloadName", "tags"];
279
+ function descriptorValue(value, key, code) {
280
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
281
+ if (descriptor === undefined
282
+ || descriptor.enumerable !== true
283
+ || descriptor.get !== undefined
284
+ || descriptor.set !== undefined
285
+ || !("value" in descriptor)) {
286
+ fail(code);
287
+ }
288
+ return descriptor.value;
289
+ }
290
+ function assertClosedKeys(value, allowed, shapeCode) {
291
+ for (const key of Reflect.ownKeys(value)) {
292
+ if (typeof key !== "string" || !allowed.includes(key))
293
+ fail("conditional_commit_unknown_member");
294
+ descriptorValue(value, key, shapeCode);
295
+ }
296
+ for (const key of allowed) {
297
+ if (!Object.prototype.hasOwnProperty.call(value, key))
298
+ fail(shapeCode);
299
+ }
300
+ }
301
+ function readTag(value) {
302
+ if (!Array.isArray(value))
303
+ fail("conditional_commit_invalid_candidate");
304
+ const keys = Reflect.ownKeys(value);
305
+ if (keys.some((key) => typeof key === "symbol"))
306
+ fail("conditional_commit_unknown_member");
307
+ if (value.length !== 1)
308
+ fail("conditional_commit_invalid_tag_count");
309
+ const lengthDescriptor = Object.getOwnPropertyDescriptor(value, "length");
310
+ if (lengthDescriptor === undefined
311
+ || lengthDescriptor.enumerable === true
312
+ || lengthDescriptor.get !== undefined
313
+ || lengthDescriptor.set !== undefined) {
314
+ fail("conditional_commit_invalid_candidate");
315
+ }
316
+ for (const key of keys) {
317
+ if (key === "length")
318
+ continue;
319
+ if (key !== "0")
320
+ fail("conditional_commit_invalid_candidate");
321
+ }
322
+ const tag = descriptorValue(value, "0", "conditional_commit_invalid_candidate");
323
+ if (typeof tag !== "string")
324
+ fail("conditional_commit_invalid_candidate");
325
+ return tag;
326
+ }
327
+ function boundedField(value, code) {
328
+ if (typeof value !== "string")
329
+ fail(code);
330
+ assertWellFormedUtf16(value, code);
331
+ const size = new TextEncoder().encode(value).byteLength;
332
+ if (size < 1)
333
+ fail(code);
334
+ if (size > CONDITIONAL_COMMIT_MAX_UTF8_FIELD_BYTES)
335
+ fail("conditional_commit_size_limit_exceeded");
336
+ return value;
337
+ }
338
+ function encodeBase64(bytes) {
339
+ let output = "";
340
+ let index = 0;
341
+ for (; index + 2 < bytes.length; index += 3) {
342
+ const chunk = ((bytes[index] ?? 0) << 16) | ((bytes[index + 1] ?? 0) << 8) | (bytes[index + 2] ?? 0);
343
+ output += BASE64_ALPHABET[(chunk >>> 18) & 63]
344
+ + BASE64_ALPHABET[(chunk >>> 12) & 63]
345
+ + BASE64_ALPHABET[(chunk >>> 6) & 63]
346
+ + BASE64_ALPHABET[chunk & 63];
347
+ }
348
+ if (index < bytes.length) {
349
+ const first = bytes[index] ?? 0;
350
+ const second = index + 1 < bytes.length ? bytes[index + 1] ?? 0 : 0;
351
+ const chunk = (first << 16) | (second << 8);
352
+ output += BASE64_ALPHABET[(chunk >>> 18) & 63];
353
+ output += BASE64_ALPHABET[(chunk >>> 12) & 63];
354
+ output += index + 1 < bytes.length ? BASE64_ALPHABET[(chunk >>> 6) & 63] : "=";
355
+ output += "=";
356
+ }
357
+ return output;
358
+ }
359
+ function decodeCanonicalBase64(text) {
360
+ if (typeof text !== "string" || text.length === 0 || text.length % 4 !== 0)
361
+ fail("conditional_commit_invalid_base64");
362
+ const padding = text.endsWith("==") ? 2 : text.endsWith("=") ? 1 : 0;
363
+ if (padding === 1 && text.endsWith("=="))
364
+ fail("conditional_commit_invalid_base64");
365
+ const body = text.slice(0, text.length - padding);
366
+ if (body.includes("="))
367
+ fail("conditional_commit_invalid_base64");
368
+ for (const char of body) {
369
+ if (!BASE64_INDEX.has(char))
370
+ fail("conditional_commit_invalid_base64");
371
+ }
372
+ const decodedLength = (text.length / 4) * 3 - padding;
373
+ if (!Number.isSafeInteger(decodedLength) || decodedLength < 0)
374
+ fail("conditional_commit_invalid_base64");
375
+ if (decodedLength > CONDITIONAL_COMMIT_MAX_PAYLOAD_BYTES)
376
+ fail("conditional_commit_size_limit_exceeded");
377
+ const bytes = new Uint8Array(decodedLength);
378
+ let offset = 0;
379
+ for (let index = 0; index < text.length; index += 4) {
380
+ const last = index + 4 >= text.length;
381
+ const a = BASE64_INDEX.get(text[index] ?? "");
382
+ const b = BASE64_INDEX.get(text[index + 1] ?? "");
383
+ const cChar = text[index + 2] ?? "";
384
+ const dChar = text[index + 3] ?? "";
385
+ const c = last && cChar === "=" ? 0 : BASE64_INDEX.get(cChar);
386
+ const d = last && dChar === "=" ? 0 : BASE64_INDEX.get(dChar);
387
+ if (a === undefined || b === undefined || c === undefined || d === undefined)
388
+ fail("conditional_commit_invalid_base64");
389
+ const chunk = (a << 18) | (b << 12) | (c << 6) | d;
390
+ const produced = last ? 3 - padding : 3;
391
+ bytes[offset] = (chunk >>> 16) & 0xff;
392
+ if (produced > 1)
393
+ bytes[offset + 1] = (chunk >>> 8) & 0xff;
394
+ if (produced > 2)
395
+ bytes[offset + 2] = chunk & 0xff;
396
+ offset += produced;
397
+ }
398
+ if (offset !== decodedLength || encodeBase64(bytes) !== text)
399
+ fail("conditional_commit_invalid_base64");
400
+ return bytes;
401
+ }
402
+ function decodeStrictUtf8(bytes) {
403
+ try {
404
+ return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
405
+ }
406
+ catch {
407
+ fail("conditional_commit_invalid_utf8");
408
+ }
409
+ }
410
+ export function parseSerializedConditionalCommitRequest(value) {
411
+ if (!isPlainRecord(value))
412
+ fail("conditional_commit_invalid_envelope");
413
+ assertClosedKeys(value, REQUEST_KEYS, "conditional_commit_invalid_envelope");
414
+ if (descriptorValue(value, "version", "conditional_commit_invalid_envelope") !== 1) {
415
+ fail("conditional_commit_invalid_version");
416
+ }
417
+ const idempotencyKey = normalizeConditionalCommitKey(descriptorValue(value, "idempotencyKey", "conditional_commit_invalid_key"));
418
+ const candidate = descriptorValue(value, "eventCandidate", "conditional_commit_invalid_candidate");
419
+ if (!isPlainRecord(candidate))
420
+ fail("conditional_commit_invalid_candidate");
421
+ assertClosedKeys(candidate, CANDIDATE_KEYS, "conditional_commit_invalid_candidate");
422
+ const payloadText = descriptorValue(candidate, "payload", "conditional_commit_invalid_candidate");
423
+ if (typeof payloadText !== "string")
424
+ fail("conditional_commit_invalid_candidate");
425
+ const eventPayloadName = boundedField(descriptorValue(candidate, "eventPayloadName", "conditional_commit_invalid_candidate"), "conditional_commit_invalid_candidate");
426
+ const tag = boundedField(readTag(descriptorValue(candidate, "tags", "conditional_commit_invalid_candidate")), "conditional_commit_invalid_candidate");
427
+ const decoded = decodeCanonicalBase64(payloadText);
428
+ const jsonText = decodeStrictUtf8(decoded);
429
+ let parsed;
430
+ try {
431
+ parsed = JSON.parse(jsonText);
432
+ }
433
+ catch {
434
+ fail("conditional_commit_invalid_payload_json");
435
+ }
436
+ if (!isPlainRecord(parsed))
437
+ fail("conditional_commit_payload_root_not_object");
438
+ canonicalizeConditionalCommitPayload(parsed);
439
+ return {
440
+ version: 1,
441
+ idempotencyKey,
442
+ eventCandidate: {
443
+ payload: payloadText,
444
+ eventPayloadName,
445
+ tags: [tag],
446
+ },
447
+ };
448
+ }
449
+ export function parseSerializedConditionalCommitRequestBody(body) {
450
+ let text;
451
+ if (typeof body === "string") {
452
+ assertWellFormedUtf16(body, "conditional_commit_invalid_json");
453
+ if (new TextEncoder().encode(body).byteLength > CONDITIONAL_COMMIT_MAX_REQUEST_BYTES) {
454
+ fail("conditional_commit_size_limit_exceeded");
455
+ }
456
+ text = body;
457
+ }
458
+ else if (body instanceof Uint8Array) {
459
+ if (body.byteLength > CONDITIONAL_COMMIT_MAX_REQUEST_BYTES)
460
+ fail("conditional_commit_size_limit_exceeded");
461
+ try {
462
+ text = new TextDecoder("utf-8", { fatal: true }).decode(body);
463
+ }
464
+ catch {
465
+ fail("conditional_commit_invalid_json");
466
+ }
467
+ }
468
+ else {
469
+ fail("conditional_commit_invalid_json");
470
+ }
471
+ let value;
472
+ try {
473
+ value = JSON.parse(text);
474
+ }
475
+ catch {
476
+ fail("conditional_commit_invalid_json");
477
+ }
478
+ return parseSerializedConditionalCommitRequest(value);
479
+ }
package/dist/index.d.ts CHANGED
@@ -216,4 +216,57 @@ export type DomainDefinitionOptions = {
216
216
  readonly mvs?: readonly DomainComponentDefinition[];
217
217
  };
218
218
  export declare function defineDomain(options: DomainDefinitionOptions): DomainDefinition;
219
+ export type QueryJsonValue = null | boolean | number | string | readonly QueryJsonValue[] | {
220
+ readonly [key: string]: QueryJsonValue;
221
+ };
222
+ export interface QueryCodec<Value> {
223
+ readonly encode: (value: Value) => QueryJsonValue;
224
+ readonly decode: (value: unknown) => Value;
225
+ }
226
+ export type QueryRow<Row> = Readonly<{
227
+ key: string;
228
+ value: Readonly<Row>;
229
+ sourceSuid: string;
230
+ }>;
231
+ export type QueryProjection<Row> = Readonly<{
232
+ rows: readonly QueryRow<Row>[];
233
+ readHead?: string;
234
+ }>;
235
+ export interface QueryHandlerContext {
236
+ readonly readHead?: string;
237
+ }
238
+ export interface QueryContract<Params, Result> {
239
+ readonly kind: "query";
240
+ readonly id: string;
241
+ readonly version: number;
242
+ readonly params: QueryCodec<Params>;
243
+ readonly result: QueryCodec<Result>;
244
+ }
245
+ export interface ListQueryContract<Params, Item> {
246
+ readonly kind: "list-query";
247
+ readonly id: string;
248
+ readonly version: number;
249
+ readonly params: QueryCodec<Params>;
250
+ readonly item: QueryCodec<Item>;
251
+ }
252
+ export interface ListQueryControls {
253
+ readonly pageNumber?: number;
254
+ readonly pageSize?: number;
255
+ readonly descending?: boolean;
256
+ readonly consistency?: "safe" | "unsafe";
257
+ readonly waitForSortableUniqueId?: string;
258
+ readonly signal?: AbortSignal;
259
+ }
260
+ export interface QueryPageMetadata {
261
+ readonly totalCount: number;
262
+ readonly totalPages: number;
263
+ readonly currentPage: number;
264
+ readonly pageSize: number;
265
+ readonly readHead?: string;
266
+ }
267
+ export interface QueryPage<Item> extends QueryPageMetadata {
268
+ readonly items: readonly Item[];
269
+ }
270
+ export { CONDITIONAL_COMMIT_CANONICALIZATION_VERSION, CONDITIONAL_COMMIT_DERIVATION_VERSION, CONDITIONAL_COMMIT_DOMAIN_SEPARATOR, CONDITIONAL_COMMIT_MAX_PAYLOAD_BYTES, CONDITIONAL_COMMIT_MAX_REQUEST_BYTES, CONDITIONAL_COMMIT_MAX_UTF8_FIELD_BYTES, CONDITIONAL_COMMIT_VERSION, ConditionalCommitProtocolError, canonicalizeConditionalCommitPayload, computeConditionalCommitFingerprint, conditionalCommitFingerprintPreimage, normalizeConditionalCommitKey, parseSerializedConditionalCommitRequest, parseSerializedConditionalCommitRequestBody, } from "./conditionalCommit.js";
271
+ export type { ConditionalCommitErrorResponse, ConditionalCommitFingerprintInput, ConditionalCommitInDoubtReason, ConditionalCommitReceipt, ConditionalCommitResult, ConditionalCommitValidationCode, ConditionalWrittenEvent, SerializedConditionalCommitRequest, SerializedConditionalEventCandidate, } from "./conditionalCommit.js";
219
272
  export * from "./materializedView.js";
package/dist/index.js CHANGED
@@ -1,8 +1,395 @@
1
+ // src/conditionalCommit.ts
2
+ var CONDITIONAL_COMMIT_VERSION = 1;
3
+ var CONDITIONAL_COMMIT_DERIVATION_VERSION = 2;
4
+ var CONDITIONAL_COMMIT_CANONICALIZATION_VERSION = 1;
5
+ var CONDITIONAL_COMMIT_DOMAIN_SEPARATOR = "sekiban.dcb.conditional-append.unique-key";
6
+ var CONDITIONAL_COMMIT_MAX_UTF8_FIELD_BYTES = 512;
7
+ var CONDITIONAL_COMMIT_MAX_PAYLOAD_BYTES = 192 * 1024;
8
+ var CONDITIONAL_COMMIT_MAX_REQUEST_BYTES = 320 * 1024;
9
+ var INT32_MIN = -2147483648;
10
+ var INT32_MAX = 2147483647;
11
+ var BASE64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
12
+ var BASE64_INDEX = new Map([...BASE64_ALPHABET].map((char, index) => [char, index]));
13
+ var VALIDATION_MESSAGES = Object.freeze({
14
+ conditional_commit_invalid_json: "The request body is not valid JSON.",
15
+ conditional_commit_unknown_member: "The request contains an unknown member.",
16
+ conditional_commit_invalid_version: "The conditional commit version is not supported.",
17
+ conditional_commit_invalid_envelope: "The conditional commit envelope is invalid.",
18
+ conditional_commit_invalid_key: "The idempotency key is invalid.",
19
+ conditional_commit_invalid_candidate: "The event candidate is invalid.",
20
+ conditional_commit_invalid_base64: "The event payload is not valid base64.",
21
+ conditional_commit_invalid_utf8: "The event payload is not valid UTF-8.",
22
+ conditional_commit_invalid_payload_json: "The event payload is not valid JSON.",
23
+ conditional_commit_payload_root_not_object: "The event payload root must be a plain object.",
24
+ conditional_commit_payload_not_canonicalizable: "The event payload cannot be canonicalized.",
25
+ conditional_commit_event_not_registered: "The event payload name is not registered.",
26
+ conditional_commit_invalid_tag_count: "Conditional commit requires exactly one tag.",
27
+ conditional_commit_size_limit_exceeded: "The conditional commit request exceeds a size limit."
28
+ });
29
+ var ConditionalCommitProtocolError = class extends Error {
30
+ code;
31
+ retryable = false;
32
+ constructor(code) {
33
+ super(VALIDATION_MESSAGES[code]);
34
+ this.name = "ConditionalCommitProtocolError";
35
+ this.code = code;
36
+ }
37
+ };
38
+ function fail(code) {
39
+ throw new ConditionalCommitProtocolError(code);
40
+ }
41
+ function assertWellFormedUtf16(value, code) {
42
+ for (let index = 0; index < value.length; index += 1) {
43
+ const unit = value.charCodeAt(index);
44
+ if (unit >= 55296 && unit <= 56319) {
45
+ const next = index + 1 < value.length ? value.charCodeAt(index + 1) : -1;
46
+ if (next < 56320 || next > 57343) fail(code);
47
+ index += 1;
48
+ continue;
49
+ }
50
+ if (unit >= 56320 && unit <= 57343) fail(code);
51
+ }
52
+ }
53
+ function isTrimCodePoint(codePoint) {
54
+ return codePoint >= 9 && codePoint <= 13 || codePoint === 32 || codePoint === 133 || codePoint === 160 || codePoint === 5760 || codePoint >= 8192 && codePoint <= 8202 || codePoint === 8232 || codePoint === 8233 || codePoint === 8239 || codePoint === 8287 || codePoint === 12288;
55
+ }
56
+ function trimDotNet(value) {
57
+ let start = 0;
58
+ let end = value.length;
59
+ while (start < end) {
60
+ const codePoint = value.codePointAt(start);
61
+ if (codePoint === void 0 || !isTrimCodePoint(codePoint)) break;
62
+ start += codePoint > 65535 ? 2 : 1;
63
+ }
64
+ while (end > start) {
65
+ const previous = value.charCodeAt(end - 1);
66
+ let codePoint = previous;
67
+ let width = 1;
68
+ if (previous >= 56320 && previous <= 57343 && end >= 2) {
69
+ const high = value.charCodeAt(end - 2);
70
+ if (high >= 55296 && high <= 56319) {
71
+ codePoint = value.codePointAt(end - 2) ?? previous;
72
+ width = 2;
73
+ }
74
+ }
75
+ if (!isTrimCodePoint(codePoint)) break;
76
+ end -= width;
77
+ }
78
+ return value.slice(start, end);
79
+ }
80
+ function normalizeConditionalCommitKey(value) {
81
+ if (typeof value !== "string") fail("conditional_commit_invalid_key");
82
+ assertWellFormedUtf16(value, "conditional_commit_invalid_key");
83
+ const normalized = trimDotNet(value).normalize("NFC");
84
+ const size = new TextEncoder().encode(normalized).byteLength;
85
+ if (size < 1 || size > CONDITIONAL_COMMIT_MAX_UTF8_FIELD_BYTES) fail("conditional_commit_invalid_key");
86
+ return normalized;
87
+ }
88
+ function isPlainRecord(value) {
89
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
90
+ const prototype = Object.getPrototypeOf(value);
91
+ return prototype === Object.prototype || prototype === null;
92
+ }
93
+ function emitValue(value, active) {
94
+ if (value === null) return "null";
95
+ if (typeof value === "boolean") return value ? "true" : "false";
96
+ if (typeof value === "string") {
97
+ assertWellFormedUtf16(value, "conditional_commit_payload_not_canonicalizable");
98
+ return JSON.stringify(value);
99
+ }
100
+ if (typeof value === "number") {
101
+ if (!Number.isFinite(value)) fail("conditional_commit_payload_not_canonicalizable");
102
+ return JSON.stringify(value);
103
+ }
104
+ if (typeof value !== "object") fail("conditional_commit_payload_not_canonicalizable");
105
+ if (Array.isArray(value)) return emitArray(value, active);
106
+ if (!isPlainRecord(value)) fail("conditional_commit_payload_not_canonicalizable");
107
+ return emitRecord(value, active);
108
+ }
109
+ function emitArray(value, active) {
110
+ if (active.has(value)) fail("conditional_commit_payload_not_canonicalizable");
111
+ const keys = Reflect.ownKeys(value);
112
+ const lengthDescriptor = Object.getOwnPropertyDescriptor(value, "length");
113
+ if (lengthDescriptor === void 0 || lengthDescriptor.enumerable === true || lengthDescriptor.get !== void 0 || lengthDescriptor.set !== void 0) {
114
+ fail("conditional_commit_payload_not_canonicalizable");
115
+ }
116
+ for (const key of keys) {
117
+ if (key === "length") continue;
118
+ if (typeof key !== "string" || !/^(?:0|[1-9]\d*)$/.test(key) || Number(key) >= value.length) {
119
+ fail("conditional_commit_payload_not_canonicalizable");
120
+ }
121
+ }
122
+ const parts = [];
123
+ active.add(value);
124
+ try {
125
+ for (let index = 0; index < value.length; index += 1) {
126
+ const descriptor = Object.getOwnPropertyDescriptor(value, String(index));
127
+ if (descriptor === void 0 || descriptor.enumerable !== true || descriptor.get !== void 0 || descriptor.set !== void 0 || !("value" in descriptor)) {
128
+ fail("conditional_commit_payload_not_canonicalizable");
129
+ }
130
+ parts.push(emitValue(descriptor.value, active));
131
+ }
132
+ } finally {
133
+ active.delete(value);
134
+ }
135
+ return `[${parts.join(",")}]`;
136
+ }
137
+ function emitRecord(value, active) {
138
+ if (active.has(value)) fail("conditional_commit_payload_not_canonicalizable");
139
+ const keys = Reflect.ownKeys(value);
140
+ const names = [];
141
+ const values = /* @__PURE__ */ new Map();
142
+ for (const key of keys) {
143
+ if (typeof key !== "string") fail("conditional_commit_payload_not_canonicalizable");
144
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
145
+ if (descriptor === void 0 || descriptor.enumerable !== true || descriptor.get !== void 0 || descriptor.set !== void 0 || !("value" in descriptor)) {
146
+ fail("conditional_commit_payload_not_canonicalizable");
147
+ }
148
+ names.push(key);
149
+ values.set(key, descriptor.value);
150
+ }
151
+ names.sort();
152
+ const parts = [];
153
+ active.add(value);
154
+ try {
155
+ for (const key of names) {
156
+ parts.push(`${JSON.stringify(key)}:${emitValue(values.get(key), active)}`);
157
+ }
158
+ } finally {
159
+ active.delete(value);
160
+ }
161
+ return `{${parts.join(",")}}`;
162
+ }
163
+ function canonicalizeConditionalCommitPayload(value) {
164
+ if (!isPlainRecord(value)) fail("conditional_commit_payload_root_not_object");
165
+ const bytes = new TextEncoder().encode(emitRecord(value, /* @__PURE__ */ new WeakSet()));
166
+ if (bytes.byteLength > CONDITIONAL_COMMIT_MAX_PAYLOAD_BYTES) fail("conditional_commit_size_limit_exceeded");
167
+ return bytes;
168
+ }
169
+ function appendInt32BE(target, value) {
170
+ if (!Number.isSafeInteger(value) || value < INT32_MIN || value > INT32_MAX) {
171
+ fail("conditional_commit_size_limit_exceeded");
172
+ }
173
+ target.push(value >>> 24 & 255, value >>> 16 & 255, value >>> 8 & 255, value & 255);
174
+ }
175
+ function appendUtf8(target, value, code, maxBytes) {
176
+ if (typeof value !== "string") fail(code);
177
+ assertWellFormedUtf16(value, code);
178
+ const bytes = new TextEncoder().encode(value);
179
+ if (bytes.byteLength < 1) fail(code);
180
+ if (maxBytes !== void 0 && bytes.byteLength > maxBytes) fail("conditional_commit_size_limit_exceeded");
181
+ appendInt32BE(target, bytes.byteLength);
182
+ for (const byte of bytes) target.push(byte);
183
+ }
184
+ function appendPayload(target, payload) {
185
+ if (!(payload instanceof Uint8Array)) fail("conditional_commit_payload_not_canonicalizable");
186
+ if (!Number.isSafeInteger(payload.byteLength) || payload.byteLength < 0) {
187
+ fail("conditional_commit_size_limit_exceeded");
188
+ }
189
+ if (payload.byteLength > CONDITIONAL_COMMIT_MAX_PAYLOAD_BYTES) fail("conditional_commit_size_limit_exceeded");
190
+ appendInt32BE(target, payload.byteLength);
191
+ for (let index = 0; index < payload.byteLength; index += 1) {
192
+ const byte = payload[index];
193
+ if (byte === void 0) fail("conditional_commit_payload_not_canonicalizable");
194
+ target.push(byte);
195
+ }
196
+ }
197
+ function conditionalCommitFingerprintPreimage(input) {
198
+ if (typeof input !== "object" || input === null) fail("conditional_commit_invalid_envelope");
199
+ const key = normalizeConditionalCommitKey(input.idempotencyKey);
200
+ const bytes = [];
201
+ appendInt32BE(bytes, CONDITIONAL_COMMIT_DERIVATION_VERSION);
202
+ appendInt32BE(bytes, CONDITIONAL_COMMIT_CANONICALIZATION_VERSION);
203
+ appendUtf8(bytes, CONDITIONAL_COMMIT_DOMAIN_SEPARATOR, "conditional_commit_invalid_envelope");
204
+ appendUtf8(bytes, input.serviceId, "conditional_commit_invalid_envelope");
205
+ appendUtf8(bytes, key, "conditional_commit_invalid_key", CONDITIONAL_COMMIT_MAX_UTF8_FIELD_BYTES);
206
+ appendUtf8(bytes, input.eventPayloadName, "conditional_commit_invalid_candidate", CONDITIONAL_COMMIT_MAX_UTF8_FIELD_BYTES);
207
+ appendPayload(bytes, input.canonicalPayload);
208
+ appendInt32BE(bytes, 1);
209
+ appendUtf8(bytes, input.tag, "conditional_commit_invalid_candidate", CONDITIONAL_COMMIT_MAX_UTF8_FIELD_BYTES);
210
+ return Uint8Array.from(bytes);
211
+ }
212
+ async function computeConditionalCommitFingerprint(input) {
213
+ const preimage = conditionalCommitFingerprintPreimage(input);
214
+ const buffer = new ArrayBuffer(preimage.byteLength);
215
+ new Uint8Array(buffer).set(preimage);
216
+ const digest = await globalThis.crypto.subtle.digest("SHA-256", buffer);
217
+ let hex = "";
218
+ for (const byte of new Uint8Array(digest)) hex += byte.toString(16).padStart(2, "0");
219
+ return hex;
220
+ }
221
+ var REQUEST_KEYS = ["version", "idempotencyKey", "eventCandidate"];
222
+ var CANDIDATE_KEYS = ["payload", "eventPayloadName", "tags"];
223
+ function descriptorValue(value, key, code) {
224
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
225
+ if (descriptor === void 0 || descriptor.enumerable !== true || descriptor.get !== void 0 || descriptor.set !== void 0 || !("value" in descriptor)) {
226
+ fail(code);
227
+ }
228
+ return descriptor.value;
229
+ }
230
+ function assertClosedKeys(value, allowed, shapeCode) {
231
+ for (const key of Reflect.ownKeys(value)) {
232
+ if (typeof key !== "string" || !allowed.includes(key)) fail("conditional_commit_unknown_member");
233
+ descriptorValue(value, key, shapeCode);
234
+ }
235
+ for (const key of allowed) {
236
+ if (!Object.prototype.hasOwnProperty.call(value, key)) fail(shapeCode);
237
+ }
238
+ }
239
+ function readTag(value) {
240
+ if (!Array.isArray(value)) fail("conditional_commit_invalid_candidate");
241
+ const keys = Reflect.ownKeys(value);
242
+ if (keys.some((key) => typeof key === "symbol")) fail("conditional_commit_unknown_member");
243
+ if (value.length !== 1) fail("conditional_commit_invalid_tag_count");
244
+ const lengthDescriptor = Object.getOwnPropertyDescriptor(value, "length");
245
+ if (lengthDescriptor === void 0 || lengthDescriptor.enumerable === true || lengthDescriptor.get !== void 0 || lengthDescriptor.set !== void 0) {
246
+ fail("conditional_commit_invalid_candidate");
247
+ }
248
+ for (const key of keys) {
249
+ if (key === "length") continue;
250
+ if (key !== "0") fail("conditional_commit_invalid_candidate");
251
+ }
252
+ const tag = descriptorValue(value, "0", "conditional_commit_invalid_candidate");
253
+ if (typeof tag !== "string") fail("conditional_commit_invalid_candidate");
254
+ return tag;
255
+ }
256
+ function boundedField(value, code) {
257
+ if (typeof value !== "string") fail(code);
258
+ assertWellFormedUtf16(value, code);
259
+ const size = new TextEncoder().encode(value).byteLength;
260
+ if (size < 1) fail(code);
261
+ if (size > CONDITIONAL_COMMIT_MAX_UTF8_FIELD_BYTES) fail("conditional_commit_size_limit_exceeded");
262
+ return value;
263
+ }
264
+ function encodeBase64(bytes) {
265
+ let output = "";
266
+ let index = 0;
267
+ for (; index + 2 < bytes.length; index += 3) {
268
+ const chunk = (bytes[index] ?? 0) << 16 | (bytes[index + 1] ?? 0) << 8 | (bytes[index + 2] ?? 0);
269
+ output += BASE64_ALPHABET[chunk >>> 18 & 63] + BASE64_ALPHABET[chunk >>> 12 & 63] + BASE64_ALPHABET[chunk >>> 6 & 63] + BASE64_ALPHABET[chunk & 63];
270
+ }
271
+ if (index < bytes.length) {
272
+ const first = bytes[index] ?? 0;
273
+ const second = index + 1 < bytes.length ? bytes[index + 1] ?? 0 : 0;
274
+ const chunk = first << 16 | second << 8;
275
+ output += BASE64_ALPHABET[chunk >>> 18 & 63];
276
+ output += BASE64_ALPHABET[chunk >>> 12 & 63];
277
+ output += index + 1 < bytes.length ? BASE64_ALPHABET[chunk >>> 6 & 63] : "=";
278
+ output += "=";
279
+ }
280
+ return output;
281
+ }
282
+ function decodeCanonicalBase64(text) {
283
+ if (typeof text !== "string" || text.length === 0 || text.length % 4 !== 0) fail("conditional_commit_invalid_base64");
284
+ const padding = text.endsWith("==") ? 2 : text.endsWith("=") ? 1 : 0;
285
+ if (padding === 1 && text.endsWith("==")) fail("conditional_commit_invalid_base64");
286
+ const body = text.slice(0, text.length - padding);
287
+ if (body.includes("=")) fail("conditional_commit_invalid_base64");
288
+ for (const char of body) {
289
+ if (!BASE64_INDEX.has(char)) fail("conditional_commit_invalid_base64");
290
+ }
291
+ const decodedLength = text.length / 4 * 3 - padding;
292
+ if (!Number.isSafeInteger(decodedLength) || decodedLength < 0) fail("conditional_commit_invalid_base64");
293
+ if (decodedLength > CONDITIONAL_COMMIT_MAX_PAYLOAD_BYTES) fail("conditional_commit_size_limit_exceeded");
294
+ const bytes = new Uint8Array(decodedLength);
295
+ let offset = 0;
296
+ for (let index = 0; index < text.length; index += 4) {
297
+ const last = index + 4 >= text.length;
298
+ const a = BASE64_INDEX.get(text[index] ?? "");
299
+ const b = BASE64_INDEX.get(text[index + 1] ?? "");
300
+ const cChar = text[index + 2] ?? "";
301
+ const dChar = text[index + 3] ?? "";
302
+ const c = last && cChar === "=" ? 0 : BASE64_INDEX.get(cChar);
303
+ const d = last && dChar === "=" ? 0 : BASE64_INDEX.get(dChar);
304
+ if (a === void 0 || b === void 0 || c === void 0 || d === void 0) fail("conditional_commit_invalid_base64");
305
+ const chunk = a << 18 | b << 12 | c << 6 | d;
306
+ const produced = last ? 3 - padding : 3;
307
+ bytes[offset] = chunk >>> 16 & 255;
308
+ if (produced > 1) bytes[offset + 1] = chunk >>> 8 & 255;
309
+ if (produced > 2) bytes[offset + 2] = chunk & 255;
310
+ offset += produced;
311
+ }
312
+ if (offset !== decodedLength || encodeBase64(bytes) !== text) fail("conditional_commit_invalid_base64");
313
+ return bytes;
314
+ }
315
+ function decodeStrictUtf8(bytes) {
316
+ try {
317
+ return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
318
+ } catch {
319
+ fail("conditional_commit_invalid_utf8");
320
+ }
321
+ }
322
+ function parseSerializedConditionalCommitRequest(value) {
323
+ if (!isPlainRecord(value)) fail("conditional_commit_invalid_envelope");
324
+ assertClosedKeys(value, REQUEST_KEYS, "conditional_commit_invalid_envelope");
325
+ if (descriptorValue(value, "version", "conditional_commit_invalid_envelope") !== 1) {
326
+ fail("conditional_commit_invalid_version");
327
+ }
328
+ const idempotencyKey = normalizeConditionalCommitKey(
329
+ descriptorValue(value, "idempotencyKey", "conditional_commit_invalid_key")
330
+ );
331
+ const candidate = descriptorValue(value, "eventCandidate", "conditional_commit_invalid_candidate");
332
+ if (!isPlainRecord(candidate)) fail("conditional_commit_invalid_candidate");
333
+ assertClosedKeys(candidate, CANDIDATE_KEYS, "conditional_commit_invalid_candidate");
334
+ const payloadText = descriptorValue(candidate, "payload", "conditional_commit_invalid_candidate");
335
+ if (typeof payloadText !== "string") fail("conditional_commit_invalid_candidate");
336
+ const eventPayloadName = boundedField(
337
+ descriptorValue(candidate, "eventPayloadName", "conditional_commit_invalid_candidate"),
338
+ "conditional_commit_invalid_candidate"
339
+ );
340
+ const tag = boundedField(readTag(descriptorValue(candidate, "tags", "conditional_commit_invalid_candidate")), "conditional_commit_invalid_candidate");
341
+ const decoded = decodeCanonicalBase64(payloadText);
342
+ const jsonText = decodeStrictUtf8(decoded);
343
+ let parsed;
344
+ try {
345
+ parsed = JSON.parse(jsonText);
346
+ } catch {
347
+ fail("conditional_commit_invalid_payload_json");
348
+ }
349
+ if (!isPlainRecord(parsed)) fail("conditional_commit_payload_root_not_object");
350
+ canonicalizeConditionalCommitPayload(parsed);
351
+ return {
352
+ version: 1,
353
+ idempotencyKey,
354
+ eventCandidate: {
355
+ payload: payloadText,
356
+ eventPayloadName,
357
+ tags: [tag]
358
+ }
359
+ };
360
+ }
361
+ function parseSerializedConditionalCommitRequestBody(body) {
362
+ let text;
363
+ if (typeof body === "string") {
364
+ assertWellFormedUtf16(body, "conditional_commit_invalid_json");
365
+ if (new TextEncoder().encode(body).byteLength > CONDITIONAL_COMMIT_MAX_REQUEST_BYTES) {
366
+ fail("conditional_commit_size_limit_exceeded");
367
+ }
368
+ text = body;
369
+ } else if (body instanceof Uint8Array) {
370
+ if (body.byteLength > CONDITIONAL_COMMIT_MAX_REQUEST_BYTES) fail("conditional_commit_size_limit_exceeded");
371
+ try {
372
+ text = new TextDecoder("utf-8", { fatal: true }).decode(body);
373
+ } catch {
374
+ fail("conditional_commit_invalid_json");
375
+ }
376
+ } else {
377
+ fail("conditional_commit_invalid_json");
378
+ }
379
+ let value;
380
+ try {
381
+ value = JSON.parse(text);
382
+ } catch {
383
+ fail("conditional_commit_invalid_json");
384
+ }
385
+ return parseSerializedConditionalCommitRequest(value);
386
+ }
387
+
1
388
  // src/materializedView.ts
2
389
  function nonEmpty(value, code, message) {
3
390
  if (typeof value !== "string" || value.length === 0) throw new Error(`${code}: ${message}`);
4
391
  }
5
- function descriptorValue(descriptor, value) {
392
+ function descriptorValue2(descriptor, value) {
6
393
  if (descriptor.valueType === "text") {
7
394
  if (typeof value !== "string") throw new Error(`MV index ${descriptor.id} expected a text value`);
8
395
  return value;
@@ -61,7 +448,7 @@ function normalizeIndexEntries(values, descriptors, rowKey) {
61
448
  return Object.freeze({
62
449
  indexId: entry.indexId,
63
450
  valueType,
64
- value: descriptorValue(descriptor, entry.value),
451
+ value: descriptorValue2(descriptor, entry.value),
65
452
  rowKey: entry.rowKey
66
453
  });
67
454
  }));
@@ -111,7 +498,7 @@ function normalizePlan(result, descriptors, event) {
111
498
  generatedEntries.push(Object.freeze({
112
499
  indexId: descriptor.id,
113
500
  valueType: descriptor.valueType,
114
- value: descriptorValue(descriptor, candidate),
501
+ value: descriptorValue2(descriptor, candidate),
115
502
  rowKey: row.rowKey
116
503
  }));
117
504
  }
@@ -165,7 +552,7 @@ var JsonValidationError = class extends Error {
165
552
  this.path = path;
166
553
  }
167
554
  };
168
- var isPlainRecord = (value) => {
555
+ var isPlainRecord2 = (value) => {
169
556
  const prototype = Object.getPrototypeOf(value);
170
557
  return prototype === Object.prototype || prototype === null;
171
558
  };
@@ -190,7 +577,7 @@ function assertJsonValue(value, boundary = "value") {
190
577
  if (Array.isArray(candidate)) {
191
578
  return candidate.map((item, index) => visit(item, `${path}[${index}]`));
192
579
  }
193
- if (!isPlainRecord(candidate)) {
580
+ if (!isPlainRecord2(candidate)) {
194
581
  throw new JsonValidationError(`Value at ${path} must be a plain JSON object`, boundary, path);
195
582
  }
196
583
  const result = {};
@@ -470,12 +857,23 @@ function defineDomain(options) {
470
857
  });
471
858
  }
472
859
  export {
860
+ CONDITIONAL_COMMIT_CANONICALIZATION_VERSION,
861
+ CONDITIONAL_COMMIT_DERIVATION_VERSION,
862
+ CONDITIONAL_COMMIT_DOMAIN_SEPARATOR,
863
+ CONDITIONAL_COMMIT_MAX_PAYLOAD_BYTES,
864
+ CONDITIONAL_COMMIT_MAX_REQUEST_BYTES,
865
+ CONDITIONAL_COMMIT_MAX_UTF8_FIELD_BYTES,
866
+ CONDITIONAL_COMMIT_VERSION,
473
867
  CanonicalEventIdentityError,
868
+ ConditionalCommitProtocolError,
474
869
  DcbDefinitionError,
475
870
  DomainDefinitionError,
476
871
  JsonValidationError,
477
872
  assertJsonValue,
478
873
  canonicalEventKey,
874
+ canonicalizeConditionalCommitPayload,
875
+ computeConditionalCommitFingerprint,
876
+ conditionalCommitFingerprintPreimage,
479
877
  defineCommand,
480
878
  defineDomain,
481
879
  defineEvent,
@@ -486,7 +884,10 @@ export {
486
884
  defineTag,
487
885
  done,
488
886
  noop,
887
+ normalizeConditionalCommitKey,
489
888
  parseCanonicalEventKey,
889
+ parseSerializedConditionalCommitRequest,
890
+ parseSerializedConditionalCommitRequestBody,
490
891
  reject,
491
892
  validateJsonValue
492
893
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sekiban/dcb-core",
3
- "version": "0.2.2",
3
+ "version": "0.3.0",
4
4
  "private": false,
5
5
  "description": "Cloudflare-independent Serialized DCB definitions and domain algebra.",
6
6
  "license": "Elastic-2.0",