@hol-org/cigar 0.9.4

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 (78) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +5 -0
  3. package/README.md +68 -0
  4. package/dist/client.d.ts +16 -0
  5. package/dist/client.d.ts.map +1 -0
  6. package/dist/client.js +839 -0
  7. package/dist/client.js.map +1 -0
  8. package/dist/digest.d.ts +21 -0
  9. package/dist/digest.d.ts.map +1 -0
  10. package/dist/digest.js +406 -0
  11. package/dist/digest.js.map +1 -0
  12. package/dist/errors.d.ts +35 -0
  13. package/dist/errors.d.ts.map +1 -0
  14. package/dist/errors.js +54 -0
  15. package/dist/errors.js.map +1 -0
  16. package/dist/examples/quickstart.d.ts +2 -0
  17. package/dist/examples/quickstart.d.ts.map +1 -0
  18. package/dist/examples/quickstart.js +32 -0
  19. package/dist/examples/quickstart.js.map +1 -0
  20. package/dist/examples/two-agent-observer.d.ts +9 -0
  21. package/dist/examples/two-agent-observer.d.ts.map +1 -0
  22. package/dist/examples/two-agent-observer.js +59 -0
  23. package/dist/examples/two-agent-observer.js.map +1 -0
  24. package/dist/examples/verify-shared-bundle.d.ts +2 -0
  25. package/dist/examples/verify-shared-bundle.d.ts.map +1 -0
  26. package/dist/examples/verify-shared-bundle.js +13 -0
  27. package/dist/examples/verify-shared-bundle.js.map +1 -0
  28. package/dist/generated/cigar_service_pb.d.ts +642 -0
  29. package/dist/generated/cigar_service_pb.d.ts.map +1 -0
  30. package/dist/generated/cigar_service_pb.js +55 -0
  31. package/dist/generated/cigar_service_pb.js.map +1 -0
  32. package/dist/generated/context_abi_pb.d.ts +3429 -0
  33. package/dist/generated/context_abi_pb.d.ts.map +1 -0
  34. package/dist/generated/context_abi_pb.js +1386 -0
  35. package/dist/generated/context_abi_pb.js.map +1 -0
  36. package/dist/generated/errors.d.ts +242 -0
  37. package/dist/generated/errors.d.ts.map +1 -0
  38. package/dist/generated/errors.js +38 -0
  39. package/dist/generated/errors.js.map +1 -0
  40. package/dist/generated/generated/error_codes_pb.d.ts +188 -0
  41. package/dist/generated/generated/error_codes_pb.d.ts.map +1 -0
  42. package/dist/generated/generated/error_codes_pb.js +191 -0
  43. package/dist/generated/generated/error_codes_pb.js.map +1 -0
  44. package/dist/generated/models.d.ts +472 -0
  45. package/dist/generated/models.d.ts.map +1 -0
  46. package/dist/generated/models.js +314 -0
  47. package/dist/generated/models.js.map +1 -0
  48. package/dist/generated/operations.d.ts +912 -0
  49. package/dist/generated/operations.d.ts.map +1 -0
  50. package/dist/generated/operations.js +50 -0
  51. package/dist/generated/operations.js.map +1 -0
  52. package/dist/idempotency.d.ts +3 -0
  53. package/dist/idempotency.d.ts.map +1 -0
  54. package/dist/idempotency.js +16 -0
  55. package/dist/idempotency.js.map +1 -0
  56. package/dist/index.d.ts +13 -0
  57. package/dist/index.d.ts.map +1 -0
  58. package/dist/index.js +13 -0
  59. package/dist/index.js.map +1 -0
  60. package/dist/release.json +6 -0
  61. package/dist/types.d.ts +115 -0
  62. package/dist/types.d.ts.map +1 -0
  63. package/dist/types.js +3 -0
  64. package/dist/types.js.map +1 -0
  65. package/dist/verify-replay.d.ts +3 -0
  66. package/dist/verify-replay.d.ts.map +1 -0
  67. package/dist/verify-replay.js +391 -0
  68. package/dist/verify-replay.js.map +1 -0
  69. package/dist/verify-vectors.d.ts +3 -0
  70. package/dist/verify-vectors.d.ts.map +1 -0
  71. package/dist/verify-vectors.js +383 -0
  72. package/dist/verify-vectors.js.map +1 -0
  73. package/dist/workflow-session.d.ts +93 -0
  74. package/dist/workflow-session.d.ts.map +1 -0
  75. package/dist/workflow-session.js +550 -0
  76. package/dist/workflow-session.js.map +1 -0
  77. package/fixtures/semantic-bundle-v1.json +13 -0
  78. package/package.json +60 -0
package/dist/client.js ADDED
@@ -0,0 +1,839 @@
1
+ import { setTimeout as delay } from "node:timers/promises";
2
+ import { CigarApiError, CigarTimeoutError, CompatibilityError, TransportError, ValidationError, isRetryable, } from "./errors.js";
3
+ import { OPERATIONS } from "./generated/operations.js";
4
+ import { ERROR_CATALOG } from "./generated/errors.js";
5
+ import * as models from "./generated/models.js";
6
+ import { validateIdempotencyKey } from "./idempotency.js";
7
+ import { decodeOperationPayload, encodeOperationPayload } from "./digest.js";
8
+ const MAX_PAYLOAD_BYTES = 16 * 1024 * 1024;
9
+ const MAX_EVENT_BYTES = 1024 * 1024;
10
+ const MAX_PROBLEM_BYTES = 64 * 1024;
11
+ const MAX_TIMEOUT_MS = 300_000;
12
+ const MAX_CURSOR_BYTES = 4096;
13
+ const PATH_NAME = /^[a-z][a-z0-9_]{0,63}$/u;
14
+ const PATH_VALUE = /^[A-Za-z0-9._~-]{1,256}$/u;
15
+ function exactBase64url(bytes) {
16
+ return Buffer.from(bytes).toString("base64url");
17
+ }
18
+ function decodeBase64url(value, maximum) {
19
+ if (typeof value !== "string" || !/^[A-Za-z0-9_-]*$/u.test(value)) {
20
+ throw new TransportError("server returned invalid base64url payload");
21
+ }
22
+ const decoded = Buffer.from(value, "base64url");
23
+ if (decoded.length > maximum || decoded.toString("base64url") !== value) {
24
+ throw new TransportError("server returned non-canonical or oversized payload");
25
+ }
26
+ return Uint8Array.from(decoded);
27
+ }
28
+ function boundedText(value, maximum, field, required = false) {
29
+ if (value === undefined && !required)
30
+ return undefined;
31
+ if (typeof value !== "string" || (required && value.length === 0) || Buffer.byteLength(value) > maximum) {
32
+ throw new TransportError(`server returned invalid ${field}`);
33
+ }
34
+ return value;
35
+ }
36
+ function parseResponse(operationId, value, maximum = MAX_PAYLOAD_BYTES) {
37
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
38
+ throw new TransportError("server returned a non-object response");
39
+ }
40
+ const record = value;
41
+ const keys = Object.keys(record).sort().join(",");
42
+ const allowed = ["next_page_cursor", "operation_id", "payload_cbor", "semantic_etag"];
43
+ if (Object.keys(record).some((key) => !allowed.includes(key))) {
44
+ throw new TransportError(`server returned unknown response fields: ${keys}`);
45
+ }
46
+ if (record.operation_id !== operationId)
47
+ throw new TransportError("server operation identity mismatch");
48
+ const semanticEtag = boundedText(record.semantic_etag, 256, "semantic_etag");
49
+ const nextPageCursor = boundedText(record.next_page_cursor, MAX_CURSOR_BYTES, "next_page_cursor");
50
+ return {
51
+ operationId,
52
+ payloadCbor: decodeBase64url(record.payload_cbor, maximum),
53
+ ...(semanticEtag === undefined ? {} : { semanticEtag }),
54
+ ...(nextPageCursor === undefined ? {} : { nextPageCursor }),
55
+ };
56
+ }
57
+ async function boundedBytes(response, maximum, field) {
58
+ const declared = response.headers.get("content-length");
59
+ if (declared !== null && (!/^(?:0|[1-9][0-9]*)$/u.test(declared) || Number(declared) > maximum)) {
60
+ throw new TransportError(`${field} content length exceeds its bound`);
61
+ }
62
+ if (response.body === null)
63
+ return new Uint8Array();
64
+ const reader = response.body.getReader();
65
+ const chunks = [];
66
+ let total = 0;
67
+ for (;;) {
68
+ const { done, value } = await reader.read();
69
+ if (done)
70
+ break;
71
+ total += value.length;
72
+ if (total > maximum) {
73
+ await reader.cancel();
74
+ throw new TransportError(`${field} exceeds its bound`);
75
+ }
76
+ chunks.push(value);
77
+ }
78
+ const result = new Uint8Array(total);
79
+ let offset = 0;
80
+ for (const chunk of chunks) {
81
+ result.set(chunk, offset);
82
+ offset += chunk.length;
83
+ }
84
+ return result;
85
+ }
86
+ function parseJson(bytes, field) {
87
+ try {
88
+ const source = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
89
+ assertUniqueJsonKeys(source);
90
+ return JSON.parse(source);
91
+ }
92
+ catch (cause) {
93
+ throw new TransportError(`${field} is not valid UTF-8 JSON`, { cause });
94
+ }
95
+ }
96
+ function assertUniqueJsonKeys(source) {
97
+ let position = 0;
98
+ let nodes = 0;
99
+ const whitespace = /[\u0009\u000a\u000d\u0020]/u;
100
+ const skipWhitespace = () => {
101
+ while (position < source.length && whitespace.test(source[position] ?? ""))
102
+ position += 1;
103
+ };
104
+ const stringToken = () => {
105
+ if (source[position] !== '"')
106
+ throw new Error("expected JSON string");
107
+ const start = position;
108
+ position += 1;
109
+ for (;;) {
110
+ const character = source[position];
111
+ if (character === undefined || character.charCodeAt(0) < 0x20)
112
+ throw new Error("invalid JSON string");
113
+ position += 1;
114
+ if (character === '"')
115
+ return JSON.parse(source.slice(start, position));
116
+ if (character !== "\\")
117
+ continue;
118
+ const escape = source[position];
119
+ if (escape === undefined || !'"\\/bfnrtu'.includes(escape))
120
+ throw new Error("invalid JSON escape");
121
+ position += 1;
122
+ if (escape === "u") {
123
+ const scalar = source.slice(position, position + 4);
124
+ if (!/^[0-9a-fA-F]{4}$/u.test(scalar))
125
+ throw new Error("invalid JSON Unicode escape");
126
+ position += 4;
127
+ }
128
+ }
129
+ };
130
+ const value = (depth) => {
131
+ nodes += 1;
132
+ if (depth > 64 || nodes > 100_000)
133
+ throw new Error("JSON exceeds nesting or node bounds");
134
+ skipWhitespace();
135
+ const initial = source[position];
136
+ if (initial === '"') {
137
+ stringToken();
138
+ return;
139
+ }
140
+ if (initial === "{") {
141
+ position += 1;
142
+ skipWhitespace();
143
+ const keys = new Set();
144
+ if (source[position] === "}") {
145
+ position += 1;
146
+ return;
147
+ }
148
+ for (;;) {
149
+ skipWhitespace();
150
+ const key = stringToken();
151
+ if (keys.has(key))
152
+ throw new Error("JSON object contains a duplicate key");
153
+ keys.add(key);
154
+ skipWhitespace();
155
+ if (source[position] !== ":")
156
+ throw new Error("JSON object lacks a colon");
157
+ position += 1;
158
+ value(depth + 1);
159
+ skipWhitespace();
160
+ if (source[position] === "}") {
161
+ position += 1;
162
+ return;
163
+ }
164
+ if (source[position] !== ",")
165
+ throw new Error("JSON object lacks a separator");
166
+ position += 1;
167
+ }
168
+ }
169
+ if (initial === "[") {
170
+ position += 1;
171
+ skipWhitespace();
172
+ if (source[position] === "]") {
173
+ position += 1;
174
+ return;
175
+ }
176
+ for (;;) {
177
+ value(depth + 1);
178
+ skipWhitespace();
179
+ if (source[position] === "]") {
180
+ position += 1;
181
+ return;
182
+ }
183
+ if (source[position] !== ",")
184
+ throw new Error("JSON array lacks a separator");
185
+ position += 1;
186
+ }
187
+ }
188
+ const start = position;
189
+ while (position < source.length && !/[\u0009\u000a\u000d\u0020,\]}]/u.test(source[position] ?? ""))
190
+ position += 1;
191
+ if (position === start)
192
+ throw new Error("JSON value is missing");
193
+ JSON.parse(source.slice(start, position));
194
+ };
195
+ value(0);
196
+ skipWhitespace();
197
+ if (position !== source.length)
198
+ throw new Error("JSON contains trailing data");
199
+ }
200
+ async function problem(response) {
201
+ const contentType = response.headers.get("content-type")?.split(";", 1)[0]?.trim();
202
+ if (contentType !== "application/problem+json") {
203
+ throw new TransportError(`HTTP ${response.status} did not use application/problem+json`);
204
+ }
205
+ const value = parseJson(await boundedBytes(response, MAX_PROBLEM_BYTES, `HTTP ${response.status} problem`), `HTTP ${response.status} problem`);
206
+ return decodeProblem(response.status, value);
207
+ }
208
+ function decodeProblem(status, value) {
209
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
210
+ throw new TransportError(`HTTP ${status} contained an invalid CIGAR problem`);
211
+ }
212
+ const candidate = value;
213
+ const expected = ["code", "correlation_id", "details", "http_status", "message", "remediation", "retry", "schema_version"].sort();
214
+ const actual = Object.keys(candidate).sort();
215
+ if (actual.length !== expected.length || actual.some((key, index) => key !== expected[index])) {
216
+ throw new TransportError(`HTTP ${status} contained unknown or missing problem fields`);
217
+ }
218
+ if (candidate.schema_version !== "cigar.problem.v1" || typeof candidate.code !== "string" || !(candidate.code in ERROR_CATALOG)) {
219
+ throw new TransportError(`HTTP ${status} contained an unsupported CIGAR problem`);
220
+ }
221
+ const code = candidate.code;
222
+ const definition = ERROR_CATALOG[code];
223
+ if (candidate.http_status !== status || candidate.http_status !== definition.httpStatus || candidate.retry !== definition.retry) {
224
+ throw new TransportError(`HTTP ${status} problem disagrees with the frozen error catalog`);
225
+ }
226
+ if (typeof candidate.message !== "string" || candidate.message.length === 0 || Buffer.byteLength(candidate.message) > 4096
227
+ || typeof candidate.remediation !== "string" || candidate.remediation.length === 0 || Buffer.byteLength(candidate.remediation) > 4096
228
+ || typeof candidate.correlation_id !== "string"
229
+ || !/^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u.test(candidate.correlation_id)
230
+ || typeof candidate.details !== "object" || candidate.details === null || Array.isArray(candidate.details)
231
+ || Object.keys(candidate.details).length > 256) {
232
+ throw new TransportError(`HTTP ${status} contained an invalid bounded CIGAR problem`);
233
+ }
234
+ return new CigarApiError(status, candidate);
235
+ }
236
+ function validateTimeout(value) {
237
+ if (!Number.isInteger(value) || value < 1 || value > MAX_TIMEOUT_MS) {
238
+ throw new ValidationError(`timeout must be an integer in 1..${MAX_TIMEOUT_MS} milliseconds`);
239
+ }
240
+ return value;
241
+ }
242
+ const AMBIENT_PROXY_ENVIRONMENT = [
243
+ "ALL_PROXY",
244
+ "HTTPS_PROXY",
245
+ "HTTP_PROXY",
246
+ "NODE_USE_ENV_PROXY",
247
+ "all_proxy",
248
+ "https_proxy",
249
+ "http_proxy",
250
+ ];
251
+ function ambientProxyConfigured() {
252
+ const runtime = globalThis;
253
+ const environment = runtime.process?.env;
254
+ return environment !== undefined && AMBIENT_PROXY_ENVIRONMENT.some((name) => {
255
+ const value = environment[name];
256
+ return value !== undefined && value.trim() !== "" && value.trim() !== "0";
257
+ });
258
+ }
259
+ function pathParameters(parameters) {
260
+ const sorted = [...(parameters ?? [])].sort((left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0);
261
+ if (sorted.length > 8)
262
+ throw new ValidationError("at most eight path parameters are allowed");
263
+ for (let index = 0; index < sorted.length; index += 1) {
264
+ const item = sorted[index];
265
+ if (item === undefined || !PATH_NAME.test(item.name) || !PATH_VALUE.test(item.value)) {
266
+ throw new ValidationError("path parameters must use the frozen name/value alphabets");
267
+ }
268
+ if (index > 0 && sorted[index - 1]?.name === item.name) {
269
+ throw new ValidationError("path parameter names must be unique");
270
+ }
271
+ }
272
+ return sorted;
273
+ }
274
+ function bindPath(template, parameters) {
275
+ const expected = [...template.matchAll(/\{([a-z][a-z0-9_]*)\}/gu)].map((match) => match[1]);
276
+ if (expected.length !== parameters.length || expected.some((name) => !parameters.some((item) => item.name === name))) {
277
+ throw new ValidationError("request path parameters do not exactly match the operation path");
278
+ }
279
+ return parameters.reduce((path, item) => path.replace(`{${item.name}}`, item.value), template);
280
+ }
281
+ class HttpEventStream {
282
+ #closed = false;
283
+ #lastEventId;
284
+ #controller = new AbortController();
285
+ #iterate;
286
+ #iterator;
287
+ constructor(iterate) {
288
+ this.#iterate = iterate;
289
+ }
290
+ get lastEventId() {
291
+ return this.#lastEventId;
292
+ }
293
+ remember(eventId) {
294
+ this.#lastEventId = eventId;
295
+ }
296
+ get closed() {
297
+ return this.#closed;
298
+ }
299
+ get signal() {
300
+ return this.#controller.signal;
301
+ }
302
+ close() {
303
+ if (this.#closed)
304
+ return;
305
+ this.#closed = true;
306
+ this.#controller.abort(new DOMException("CIGAR event stream closed", "AbortError"));
307
+ }
308
+ async [Symbol.asyncDispose]() {
309
+ this.close();
310
+ }
311
+ [Symbol.asyncIterator]() {
312
+ this.#iterator ??= this.#iterate();
313
+ return this.#iterator;
314
+ }
315
+ }
316
+ class TypedHttpEventStream {
317
+ #raw;
318
+ #model;
319
+ constructor(raw, model) {
320
+ this.#raw = raw;
321
+ this.#model = model;
322
+ }
323
+ get lastEventId() {
324
+ return this.#raw.lastEventId;
325
+ }
326
+ close() {
327
+ this.#raw.close();
328
+ }
329
+ async [Symbol.asyncDispose]() {
330
+ this.close();
331
+ }
332
+ async *[Symbol.asyncIterator]() {
333
+ for await (const event of this.#raw) {
334
+ const decoded = decodeOperationPayload(event.payloadCbor);
335
+ yield {
336
+ operationId: event.operationId,
337
+ eventId: event.eventId,
338
+ payload: this.#model.create(decoded),
339
+ payloadCbor: Uint8Array.from(event.payloadCbor),
340
+ };
341
+ }
342
+ }
343
+ }
344
+ export class CigarClient {
345
+ #baseUrl;
346
+ #token;
347
+ #timeoutMs;
348
+ #maxAttempts;
349
+ #fetch;
350
+ #apiVersion;
351
+ constructor(options) {
352
+ try {
353
+ this.#baseUrl = new URL(options.baseUrl);
354
+ }
355
+ catch (cause) {
356
+ throw new ValidationError("baseUrl must be an absolute HTTP(S) origin", { cause });
357
+ }
358
+ if (!new Set(["http:", "https:"]).has(this.#baseUrl.protocol)) {
359
+ throw new ValidationError("baseUrl must use HTTP or HTTPS");
360
+ }
361
+ if (this.#baseUrl.username !== "" || this.#baseUrl.password !== "" || this.#baseUrl.search !== "" || this.#baseUrl.hash !== "") {
362
+ throw new ValidationError("baseUrl must not contain credentials, query, or fragment");
363
+ }
364
+ if (this.#baseUrl.pathname !== "/" && this.#baseUrl.pathname !== "") {
365
+ throw new ValidationError("baseUrl must be an origin with no path prefix");
366
+ }
367
+ const loopback = this.#baseUrl.hostname === "localhost"
368
+ || this.#baseUrl.hostname === "127.0.0.1"
369
+ || this.#baseUrl.hostname === "::1"
370
+ || this.#baseUrl.hostname === "[::1]";
371
+ if (this.#baseUrl.protocol === "http:" && (!loopback || options.allowInsecureLoopback !== true)) {
372
+ throw new ValidationError("cleartext HTTP requires explicit allowInsecureLoopback for a loopback host");
373
+ }
374
+ if (this.#baseUrl.protocol === "https:" && options.bearerToken === undefined) {
375
+ throw new ValidationError("remote HTTPS requires an explicit bearer token provider");
376
+ }
377
+ if (typeof options.bearerToken === "string" && !/^[\x21-\x7e]{1,8192}$/u.test(options.bearerToken)) {
378
+ throw new ValidationError("bearer token must be 1..8192 visible ASCII bytes");
379
+ }
380
+ this.#baseUrl.pathname = "";
381
+ this.#token = options.bearerToken;
382
+ this.#timeoutMs = validateTimeout(options.defaultTimeoutMs ?? 30_000);
383
+ this.#maxAttempts = options.maxAttempts ?? 3;
384
+ if (!Number.isInteger(this.#maxAttempts) || this.#maxAttempts < 1 || this.#maxAttempts > 8) {
385
+ throw new ValidationError("maxAttempts must be in 1..8");
386
+ }
387
+ if (options.fetch !== undefined && options.trustCustomFetch !== true) {
388
+ throw new ValidationError("custom fetch requires explicit trustCustomFetch acknowledgement");
389
+ }
390
+ if (options.fetch === undefined && ambientProxyConfigured()) {
391
+ throw new ValidationError("ambient proxy configuration requires an explicitly trusted custom fetch");
392
+ }
393
+ this.#fetch = options.fetch ?? globalThis.fetch;
394
+ if (options.apiVersion !== undefined && options.apiVersion !== "1") {
395
+ throw new ValidationError("apiVersion must be the frozen v1 line");
396
+ }
397
+ this.#apiVersion = options.apiVersion ?? "1";
398
+ }
399
+ async negotiate(options) {
400
+ const empty = { payloadCbor: new Uint8Array() };
401
+ const version = await this.invokeOperation("getVersion", empty, options);
402
+ const capabilities = await this.invokeOperation("getCapabilities", empty, options);
403
+ return { apiVersion: this.#apiVersion, version, capabilities };
404
+ }
405
+ async invokeTypedOperation(operationId, request, responseModel, options) {
406
+ const raw = await this.invokeOperation(operationId, this.#typedRequest(operationId, request), options);
407
+ const decoded = decodeOperationPayload(raw.payloadCbor);
408
+ return {
409
+ operationId: raw.operationId,
410
+ payload: responseModel.create(decoded),
411
+ payloadCbor: Uint8Array.from(raw.payloadCbor),
412
+ ...(raw.semanticEtag === undefined ? {} : { semanticEtag: raw.semanticEtag }),
413
+ ...(raw.nextPageCursor === undefined ? {} : { nextPageCursor: raw.nextPageCursor }),
414
+ };
415
+ }
416
+ streamTypedOperation(operationId, request, eventModel, options) {
417
+ return new TypedHttpEventStream(this.streamOperation(operationId, this.#typedRequest(operationId, request), options), eventModel);
418
+ }
419
+ #typedRequest(operationId, request) {
420
+ const definition = OPERATIONS[operationId];
421
+ if (definition.stream && request.pageCursor !== undefined) {
422
+ throw new ValidationError("SSE resume uses CallOptions.resumeFrom, not a pagination cursor");
423
+ }
424
+ const validatedPayload = payloadModel(definition.requestType).create(request.payload);
425
+ if (typeof validatedPayload !== "object" || validatedPayload === null) {
426
+ throw new ValidationError(`${definition.requestType} payload must be an object`);
427
+ }
428
+ const record = validatedPayload;
429
+ const pathParameters = definition.pathFields.map((name) => {
430
+ const value = record[name];
431
+ if (typeof value !== "string")
432
+ throw new ValidationError(`${definition.requestType}.${name} must be a path string`);
433
+ return { name, value };
434
+ });
435
+ return {
436
+ payloadCbor: definition.httpMethod === "GET" ? new Uint8Array() : encodeOperationPayload(validatedPayload),
437
+ pathParameters,
438
+ ...(request.idempotencyKey === undefined ? {} : { idempotencyKey: request.idempotencyKey }),
439
+ ...(request.expectedRevision === undefined ? {} : { expectedRevision: request.expectedRevision }),
440
+ ...(request.dryRun === undefined ? {} : { dryRun: request.dryRun }),
441
+ ...(request.pageCursor === undefined ? {} : { pageCursor: request.pageCursor }),
442
+ ...(request.pageSize === undefined ? {} : { pageSize: request.pageSize }),
443
+ };
444
+ }
445
+ async *paginate(operationId, request, options) {
446
+ if (OPERATIONS[operationId].stream)
447
+ throw new ValidationError("stream operations cannot be paginated");
448
+ let cursor = request.pageCursor;
449
+ const seen = new Set();
450
+ for (;;) {
451
+ const pageRequest = cursor === undefined
452
+ ? { ...request }
453
+ : { ...request, pageCursor: cursor };
454
+ const response = await this.invokeOperation(operationId, pageRequest, options);
455
+ yield response;
456
+ if (response.nextPageCursor === undefined)
457
+ return;
458
+ if (seen.has(response.nextPageCursor))
459
+ throw new TransportError("pagination cursor cycle detected");
460
+ seen.add(response.nextPageCursor);
461
+ cursor = response.nextPageCursor;
462
+ }
463
+ }
464
+ async invokeOperation(operationId, request, options) {
465
+ const definition = OPERATIONS[operationId];
466
+ if (definition.stream)
467
+ throw new ValidationError("use streamOperation for streaming operations");
468
+ const attempts = this.#attempts(operationId, request, options);
469
+ const timeoutMs = validateTimeout(options?.timeoutMs ?? this.#timeoutMs);
470
+ const deadline = Date.now() + timeoutMs;
471
+ let lastError;
472
+ for (let attempt = 1; attempt <= attempts; attempt += 1) {
473
+ if (deadline <= Date.now())
474
+ throw new CigarTimeoutError("CIGAR request deadline elapsed");
475
+ try {
476
+ return await this.#unary(operationId, request, deadline, options);
477
+ }
478
+ catch (error) {
479
+ lastError = error;
480
+ if (attempt === attempts || !isRetryable(error))
481
+ throw error;
482
+ const backoff = Math.min(100 * 2 ** (attempt - 1), 1_000);
483
+ const remaining = deadline - Date.now();
484
+ if (remaining <= backoff)
485
+ throw new CigarTimeoutError("CIGAR request deadline elapsed", { cause: error });
486
+ await delay(backoff, undefined, { signal: options?.signal });
487
+ }
488
+ }
489
+ throw lastError;
490
+ }
491
+ streamOperation(operationId, request, options) {
492
+ if (!OPERATIONS[operationId].stream)
493
+ throw new ValidationError("operation is not a stream");
494
+ if (request.pageCursor !== undefined)
495
+ throw new ValidationError("SSE resume uses CallOptions.resumeFrom, not pageCursor");
496
+ let stream;
497
+ stream = new HttpEventStream(() => this.#events(stream, operationId, request, options));
498
+ return stream;
499
+ }
500
+ #attempts(operationId, request, options) {
501
+ const requested = options?.maxAttempts ?? this.#maxAttempts;
502
+ if (!Number.isInteger(requested) || requested < 1 || requested > 8) {
503
+ throw new ValidationError("maxAttempts must be in 1..8");
504
+ }
505
+ if (operationId === "dispatchEffect")
506
+ return 1;
507
+ const definition = OPERATIONS[operationId];
508
+ if (!definition.mutation)
509
+ return requested;
510
+ return request.idempotencyKey === undefined ? 1 : requested;
511
+ }
512
+ async #headers(operationId, timeoutMs, signal, externalSignal) {
513
+ const headers = new Headers({
514
+ accept: "application/json, application/problem+json",
515
+ "x-cigar-api-version": this.#apiVersion,
516
+ "x-cigar-operation-id": operationId,
517
+ "x-cigar-timeout-ms": String(timeoutMs),
518
+ });
519
+ if (this.#token !== undefined) {
520
+ let token;
521
+ try {
522
+ if (typeof this.#token !== "function")
523
+ token = this.#token;
524
+ else if (signal === undefined)
525
+ token = await this.#token();
526
+ else {
527
+ const provider = Promise.resolve(this.#token(signal));
528
+ token = await new Promise((resolve, reject) => {
529
+ const aborted = () => reject(signal.reason);
530
+ signal.addEventListener("abort", aborted, { once: true });
531
+ provider.then(resolve, reject).finally(() => signal.removeEventListener("abort", aborted)).catch(() => undefined);
532
+ });
533
+ }
534
+ }
535
+ catch (cause) {
536
+ if (externalSignal?.aborted === true)
537
+ throw cause;
538
+ if (signal?.aborted === true)
539
+ throw new CigarTimeoutError("CIGAR request deadline elapsed", { cause });
540
+ throw new TransportError("bearer token provider failed", { cause });
541
+ }
542
+ if (!/^[\x21-\x7e]{1,8192}$/u.test(token))
543
+ throw new ValidationError("bearer token must be 1..8192 visible ASCII bytes");
544
+ headers.set("authorization", `Bearer ${token}`);
545
+ }
546
+ return headers;
547
+ }
548
+ #signal(options, timeoutMs, internal) {
549
+ const timeout = AbortSignal.timeout(timeoutMs);
550
+ const signals = [timeout];
551
+ if (options?.signal !== undefined)
552
+ signals.push(options.signal);
553
+ if (internal !== undefined)
554
+ signals.push(internal);
555
+ return signals.length === 1 ? timeout : AbortSignal.any(signals);
556
+ }
557
+ async #unary(operationId, request, deadline, options) {
558
+ const definition = OPERATIONS[operationId];
559
+ const timeoutMs = Math.max(1, deadline - Date.now());
560
+ const parameters = pathParameters(request.pathParameters);
561
+ let path = bindPath(definition.httpPath, parameters);
562
+ const requestSignal = this.#signal(options, timeoutMs);
563
+ const headers = await this.#headers(operationId, timeoutMs, requestSignal, options?.signal);
564
+ const payload = Uint8Array.from(request.payloadCbor ?? []);
565
+ if (payload.length > definition.requestMaxBytes)
566
+ throw new ValidationError("request payload exceeds operation bound");
567
+ let body;
568
+ if (definition.httpMethod === "GET") {
569
+ if (payload.length !== 0 || request.idempotencyKey !== undefined || request.expectedRevision !== undefined || request.dryRun === true) {
570
+ throw new ValidationError("GET operations do not carry payload or mutation metadata");
571
+ }
572
+ const query = new URLSearchParams();
573
+ if (request.pageCursor !== undefined) {
574
+ if (Buffer.byteLength(request.pageCursor) > MAX_CURSOR_BYTES)
575
+ throw new ValidationError("page cursor exceeds bound");
576
+ query.set("page_cursor", request.pageCursor);
577
+ }
578
+ if (request.pageSize !== undefined) {
579
+ if (!Number.isInteger(request.pageSize) || request.pageSize < 1 || request.pageSize > 1000)
580
+ throw new ValidationError("pageSize must be in 1..1000");
581
+ query.set("page_size", String(request.pageSize));
582
+ }
583
+ const encoded = query.toString();
584
+ if (encoded !== "")
585
+ path += `?${encoded}`;
586
+ }
587
+ else {
588
+ if (definition.idempotencyRequired) {
589
+ if (request.idempotencyKey === undefined)
590
+ throw new ValidationError(`${operationId} requires an idempotency key`);
591
+ headers.set("idempotency-key", validateIdempotencyKey(request.idempotencyKey));
592
+ }
593
+ else if (request.idempotencyKey !== undefined) {
594
+ throw new ValidationError(`${operationId} does not accept an idempotency key`);
595
+ }
596
+ if (definition.revisionRequired) {
597
+ if (request.expectedRevision === undefined || request.expectedRevision.length === 0 || request.expectedRevision.length > 256) {
598
+ throw new ValidationError(`${operationId} requires a bounded expected revision`);
599
+ }
600
+ headers.set("if-match", request.expectedRevision);
601
+ }
602
+ else if (request.expectedRevision !== undefined) {
603
+ throw new ValidationError(`${operationId} does not accept an expected revision`);
604
+ }
605
+ headers.set("content-type", "application/json");
606
+ body = JSON.stringify({
607
+ operation_id: operationId,
608
+ payload_cbor: exactBase64url(payload),
609
+ dry_run: request.dryRun ?? false,
610
+ ...(request.idempotencyKey === undefined ? {} : { idempotency_key: request.idempotencyKey }),
611
+ ...(request.expectedRevision === undefined ? {} : { expected_revision: request.expectedRevision }),
612
+ ...(request.pageCursor === undefined ? {} : { page_cursor: request.pageCursor }),
613
+ ...(request.pageSize === undefined ? {} : { page_size: request.pageSize }),
614
+ path_parameters: parameters,
615
+ });
616
+ }
617
+ let response;
618
+ try {
619
+ const init = {
620
+ method: definition.httpMethod,
621
+ headers,
622
+ signal: requestSignal,
623
+ redirect: "error",
624
+ ...(body === undefined ? {} : { body }),
625
+ };
626
+ response = await this.#fetch(new URL(path, `${this.#baseUrl.toString()}/`), init);
627
+ }
628
+ catch (cause) {
629
+ if (options?.signal?.aborted === true)
630
+ throw cause;
631
+ if (cause instanceof DOMException && cause.name === "TimeoutError")
632
+ throw new CigarTimeoutError("CIGAR request deadline elapsed", { cause });
633
+ throw new TransportError("CIGAR transport failed", { cause });
634
+ }
635
+ if (!response.ok)
636
+ throw await problem(response);
637
+ const serverVersion = response.headers.get("x-cigar-api-version");
638
+ if (serverVersion !== null && serverVersion !== this.#apiVersion) {
639
+ throw new CompatibilityError(`server API version ${serverVersion} is incompatible with ${this.#apiVersion}`);
640
+ }
641
+ const contentType = response.headers.get("content-type")?.split(";", 1)[0]?.trim();
642
+ if (contentType === "application/openmetrics-text") {
643
+ return {
644
+ operationId,
645
+ payloadCbor: await boundedBytes(response, definition.responseMaxBytes, "metrics response"),
646
+ };
647
+ }
648
+ if (contentType !== "application/json") {
649
+ throw new TransportError(`unexpected response content type ${contentType}`);
650
+ }
651
+ const wrapperMaximum = Math.ceil(definition.responseMaxBytes * 4 / 3) + 16_384;
652
+ return parseResponse(operationId, parseJson(await boundedBytes(response, wrapperMaximum, "operation response"), "operation response"), definition.responseMaxBytes);
653
+ }
654
+ async *#events(stream, operationId, request, options) {
655
+ const timeoutMs = validateTimeout(options?.timeoutMs ?? this.#timeoutMs);
656
+ const deadline = Date.now() + timeoutMs;
657
+ const parameters = pathParameters(request.pathParameters);
658
+ const definition = OPERATIONS[operationId];
659
+ const path = bindPath(definition.httpPath, parameters);
660
+ let resume = options?.resumeFrom;
661
+ if (resume !== undefined && !/^[\x21-\x7e]{1,256}$/u.test(resume)) {
662
+ throw new ValidationError("resumeFrom must be a bounded visible-ASCII event ID");
663
+ }
664
+ const attempts = options?.maxAttempts ?? this.#maxAttempts;
665
+ if (!Number.isInteger(attempts) || attempts < 1 || attempts > 8) {
666
+ throw new ValidationError("maxAttempts must be in 1..8");
667
+ }
668
+ const seen = new Set(resume === undefined ? [] : [resume]);
669
+ for (let attempt = 1; attempt <= attempts && !stream.closed; attempt += 1) {
670
+ const remaining = deadline - Date.now();
671
+ if (remaining <= 0)
672
+ throw new CigarTimeoutError("CIGAR stream deadline elapsed");
673
+ const requestSignal = this.#signal(options, remaining, stream.signal);
674
+ let headers;
675
+ try {
676
+ headers = await this.#headers(operationId, remaining, requestSignal, options?.signal);
677
+ }
678
+ catch (error) {
679
+ if (stream.closed)
680
+ return;
681
+ throw error;
682
+ }
683
+ headers.set("accept", "text/event-stream, application/problem+json");
684
+ if (resume !== undefined)
685
+ headers.set("last-event-id", resume);
686
+ let retryCause = new TransportError("CIGAR event stream ended before close");
687
+ let cleanEnd = false;
688
+ let response;
689
+ try {
690
+ response = await this.#fetch(new URL(path, `${this.#baseUrl.toString()}/`), {
691
+ method: "GET",
692
+ headers,
693
+ signal: requestSignal,
694
+ redirect: "error",
695
+ });
696
+ }
697
+ catch (cause) {
698
+ if (stream.closed || options?.signal?.aborted === true)
699
+ return;
700
+ retryCause = new TransportError("CIGAR event stream failed", { cause });
701
+ }
702
+ if (response !== undefined) {
703
+ if (!response.ok) {
704
+ retryCause = await problem(response);
705
+ if (!isRetryable(retryCause))
706
+ throw retryCause;
707
+ }
708
+ else {
709
+ const contentType = response.headers.get("content-type")?.split(";", 1)[0]?.trim();
710
+ if (contentType !== "text/event-stream")
711
+ throw new TransportError("stream response must use text/event-stream");
712
+ if (response.body === null)
713
+ throw new TransportError("event stream response has no body");
714
+ const iterator = response.body.pipeThrough(new TextDecoderStream("utf-8", { fatal: true }))[Symbol.asyncIterator]();
715
+ let pending = "";
716
+ for (;;) {
717
+ let next;
718
+ try {
719
+ next = await iterator.next();
720
+ }
721
+ catch (cause) {
722
+ retryCause = new TransportError("event stream body read failed", { cause });
723
+ break;
724
+ }
725
+ if (next.done) {
726
+ cleanEnd = true;
727
+ break;
728
+ }
729
+ if (stream.closed)
730
+ return;
731
+ pending += next.value.replace(/\r\n/gu, "\n");
732
+ if (Buffer.byteLength(pending) > MAX_EVENT_BYTES * 2)
733
+ throw new TransportError("event frame exceeds bound");
734
+ let boundary;
735
+ while ((boundary = pending.indexOf("\n\n")) >= 0) {
736
+ const frame = pending.slice(0, boundary);
737
+ pending = pending.slice(boundary + 2);
738
+ const parsed = this.#event(operationId, frame);
739
+ if (parsed === undefined)
740
+ continue;
741
+ if (parsed instanceof CigarApiError)
742
+ throw parsed;
743
+ if (seen.has(parsed.eventId))
744
+ continue;
745
+ if (seen.size >= 100_000)
746
+ throw new TransportError("event identity set exceeds its bound");
747
+ seen.add(parsed.eventId);
748
+ resume = parsed.eventId;
749
+ stream.remember(parsed.eventId);
750
+ yield parsed;
751
+ }
752
+ }
753
+ }
754
+ }
755
+ if (stream.closed)
756
+ return;
757
+ if (attempt === attempts) {
758
+ if (cleanEnd)
759
+ return;
760
+ throw retryCause;
761
+ }
762
+ const backoff = Math.min(100 * 2 ** (attempt - 1), 1_000);
763
+ if (deadline - Date.now() <= backoff) {
764
+ throw new CigarTimeoutError("CIGAR stream deadline elapsed", { cause: retryCause });
765
+ }
766
+ await delay(backoff, undefined, { signal: options?.signal });
767
+ }
768
+ }
769
+ #event(operationId, frame) {
770
+ let eventType = "message";
771
+ let id;
772
+ const data = [];
773
+ for (const line of frame.split("\n")) {
774
+ if (line === "" || line.startsWith(":"))
775
+ continue;
776
+ const colon = line.indexOf(":");
777
+ const field = colon < 0 ? line : line.slice(0, colon);
778
+ const value = colon < 0 ? "" : line.slice(colon + 1).replace(/^ /u, "");
779
+ if (field === "event")
780
+ eventType = value;
781
+ else if (field === "id")
782
+ id = value;
783
+ else if (field === "data")
784
+ data.push(value);
785
+ }
786
+ if (data.length === 0)
787
+ return undefined;
788
+ let decoded;
789
+ try {
790
+ decoded = parseJson(new TextEncoder().encode(data.join("\n")), "event data");
791
+ }
792
+ catch (cause) {
793
+ throw new TransportError("event data is not valid JSON", { cause });
794
+ }
795
+ if (eventType === "problem") {
796
+ if (typeof decoded !== "object" || decoded === null || Array.isArray(decoded)) {
797
+ throw new TransportError("problem event is not an object");
798
+ }
799
+ const status = decoded.http_status;
800
+ if (!Number.isInteger(status))
801
+ throw new TransportError("problem event lacks its HTTP status");
802
+ return decodeProblem(status, decoded);
803
+ }
804
+ if (typeof decoded !== "object" || decoded === null || Array.isArray(decoded))
805
+ throw new TransportError("event is not an object");
806
+ const record = decoded;
807
+ const eventFields = Object.keys(record).sort();
808
+ if (eventFields.join(",") !== "event_id,operation_id,payload_cbor") {
809
+ throw new TransportError("event contains unknown or missing fields");
810
+ }
811
+ if (record.operation_id !== operationId
812
+ || typeof record.event_id !== "string"
813
+ || !/^[\x21-\x7e]{1,256}$/u.test(record.event_id)
814
+ || record.event_id !== id) {
815
+ throw new TransportError("event identity mismatch");
816
+ }
817
+ return { operationId, eventId: record.event_id, payloadCbor: decodeBase64url(record.payload_cbor, MAX_EVENT_BYTES) };
818
+ }
819
+ }
820
+ function payloadModel(name) {
821
+ const candidate = models[name];
822
+ if (typeof candidate !== "object" || candidate === null || !("create" in candidate)) {
823
+ throw new Error(`generated payload model ${name} is missing`);
824
+ }
825
+ return candidate;
826
+ }
827
+ for (const operationId of Object.keys(OPERATIONS)) {
828
+ Object.defineProperty(CigarClient.prototype, operationId, {
829
+ configurable: false,
830
+ enumerable: false,
831
+ value(request, options) {
832
+ return OPERATIONS[operationId].stream
833
+ ? this.streamTypedOperation(operationId, request, payloadModel(OPERATIONS[operationId].eventType ?? ""), options)
834
+ : this.invokeTypedOperation(operationId, request, payloadModel(OPERATIONS[operationId].responseType), options);
835
+ },
836
+ writable: false,
837
+ });
838
+ }
839
+ //# sourceMappingURL=client.js.map