@whooshbang/sdk 1.0.0-rc.14

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/dist/client.js ADDED
@@ -0,0 +1,924 @@
1
+ import { contractValidationErrors, validateAccountProjection, validateApiCredential, validateApiCredentialCollection, validateApiCredentialRotation, validateApiCredentialSecretReveal, validateCreateApiCredentialRequest, validateCreateCustomerEndpointRequest, validateCreateMachineClientRequest, validateCreateMessageRequest, validateCreateProjectRequest, validateCreateSubscriptionLinkRequest, validateCustomerEndpoint, validateCustomerEndpointAttemptCollection, validateCustomerEndpointCollection, validateCustomerEndpointMutationResult, validateInteractionSummary, validateMachineClient, validateMachineCredential, validateMachineEventAckRequest, validateMachineEventAckResponse, validateMachineEventPollResponse, validateMessage, validateProblem, validateProject, validateProjectCollection, validateProviderCapabilitiesCollection, validateRegisterMachineCredentialRequest, validateRotateApiCredentialRequest, validateRotateCustomerEndpointRequest, validateSubscriptionLink, } from "@whooshbang/contracts";
2
+ import { validateAdditiveCompatible } from "./additive-compatibility.js";
3
+ import { WhooshBangAbortError, WhooshBangContractError, WhooshBangProblemError, WhooshBangProtocolError, WhooshBangTimeoutError, WhooshBangTransportError, validationError, } from "./errors.js";
4
+ const MAX_RESPONSE_BYTES = 1024 * 1024;
5
+ const MAX_PROBLEM_BYTES = 64 * 1024;
6
+ const MAX_TIMEOUT_MS = 300_000;
7
+ const MAX_INSPECTION_WAIT_SECONDS = 30;
8
+ const MAX_MACHINE_POLL_LIMIT = 50;
9
+ const MAX_PAGE_LIMIT = 100;
10
+ const DIAGNOSTIC_HEADER = "whooshbang-diagnostic-id";
11
+ const visibleAscii = /^[!-~]+$/u;
12
+ function monotonicNow() {
13
+ return typeof performance === "object" &&
14
+ typeof performance.now === "function"
15
+ ? performance.now()
16
+ : Date.now();
17
+ }
18
+ function normalizeBaseUrl(value) {
19
+ const url = new URL(value);
20
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
21
+ throw new TypeError("baseUrl must use http or https.");
22
+ }
23
+ if (url.username !== "" || url.password !== "") {
24
+ throw new TypeError("baseUrl must not contain credentials.");
25
+ }
26
+ url.pathname = `${url.pathname.replace(/\/+$/u, "")}/`;
27
+ url.search = "";
28
+ url.hash = "";
29
+ return url;
30
+ }
31
+ function validateCredential(value) {
32
+ if (value.length === 0 || value.length > 4096 || !visibleAscii.test(value)) {
33
+ throw new TypeError("credential must be a bounded visible-ASCII bearer token.");
34
+ }
35
+ }
36
+ function codePointLength(value) {
37
+ return Array.from(value).length;
38
+ }
39
+ function containsAsciiControl(value) {
40
+ for (const character of value) {
41
+ const codePoint = character.codePointAt(0);
42
+ if (codePoint !== undefined && (codePoint <= 0x1f || codePoint === 0x7f)) {
43
+ return true;
44
+ }
45
+ }
46
+ return false;
47
+ }
48
+ function pathSegment(operation, value, label) {
49
+ if (codePointLength(value) === 0 ||
50
+ codePointLength(value) > 128 ||
51
+ value === "." ||
52
+ value === ".." ||
53
+ containsAsciiControl(value)) {
54
+ throw validationError(operation, label, "invalid_identifier", `${label} must be a bounded opaque identifier.`);
55
+ }
56
+ return encodeURIComponent(value);
57
+ }
58
+ function assertInput(operation, value, validator) {
59
+ if (!validator(value)) {
60
+ throw new WhooshBangContractError(operation, contractValidationErrors(validator), "The request does not match the WhooshBang contract.");
61
+ }
62
+ }
63
+ async function readBoundedJson(response, operation, limit) {
64
+ const declaredLength = response.headers.get("content-length");
65
+ if (declaredLength !== null &&
66
+ (!/^(?:0|[1-9]\d*)$/u.test(declaredLength) ||
67
+ !Number.isSafeInteger(Number(declaredLength)) ||
68
+ Number(declaredLength) > limit)) {
69
+ throw new WhooshBangProtocolError(operation, "The WhooshBang response exceeded the client safety limit.", response.status);
70
+ }
71
+ const chunks = [];
72
+ let byteLength = 0;
73
+ const reader = response.body?.getReader();
74
+ if (reader !== undefined) {
75
+ try {
76
+ for (;;) {
77
+ let result;
78
+ try {
79
+ result = await reader.read();
80
+ }
81
+ catch {
82
+ throw new WhooshBangProtocolError(operation, "The WhooshBang response body could not be read safely.", response.status);
83
+ }
84
+ if (result.done) {
85
+ break;
86
+ }
87
+ byteLength += result.value.byteLength;
88
+ if (byteLength > limit) {
89
+ try {
90
+ await reader.cancel();
91
+ }
92
+ catch {
93
+ // The bounded protocol error below intentionally redacts stream
94
+ // cancellation failures from an untrusted Fetch implementation.
95
+ }
96
+ throw new WhooshBangProtocolError(operation, "The WhooshBang response exceeded the client safety limit.", response.status);
97
+ }
98
+ chunks.push(result.value);
99
+ }
100
+ }
101
+ finally {
102
+ try {
103
+ reader.releaseLock();
104
+ }
105
+ catch {
106
+ // Releasing a hostile or already-failed reader must not replace the
107
+ // bounded public protocol error with an implementation detail.
108
+ }
109
+ }
110
+ }
111
+ const bytes = new Uint8Array(byteLength);
112
+ let offset = 0;
113
+ for (const chunk of chunks) {
114
+ bytes.set(chunk, offset);
115
+ offset += chunk.byteLength;
116
+ }
117
+ try {
118
+ return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
119
+ }
120
+ catch {
121
+ throw new WhooshBangProtocolError(operation, "The WhooshBang response was not valid UTF-8 JSON.", response.status);
122
+ }
123
+ }
124
+ function assertIdempotencyKey(operation, value) {
125
+ if (value.length < 8 || value.length > 255 || !visibleAscii.test(value)) {
126
+ throw validationError(operation, "idempotencyKey", "invalid_key", "idempotencyKey must contain 8 to 255 visible ASCII characters.");
127
+ }
128
+ }
129
+ function assertMachineCursor(operation, value) {
130
+ if (value.length < 8 ||
131
+ value.length > 128 ||
132
+ !/^mcur_[A-Za-z0-9_-]+$/u.test(value)) {
133
+ throw validationError(operation, "after", "invalid_cursor", "after must be a bounded machine cursor from a prior response.");
134
+ }
135
+ }
136
+ function assertOpaqueCursor(operation, value, subject) {
137
+ if (value.length === 0 ||
138
+ value.length > 256 ||
139
+ !/^[A-Za-z0-9_-]+$/u.test(value)) {
140
+ throw validationError(operation, "after", "invalid_cursor", `after must be a bounded ${subject} cursor from a prior response.`);
141
+ }
142
+ }
143
+ function boundedLimit(operation, limit, maximum) {
144
+ if (!Number.isInteger(limit) || limit < 1 || limit > maximum) {
145
+ throw validationError(operation, "limit", "out_of_range", `limit must be an integer from 1 through ${maximum}.`);
146
+ }
147
+ return String(limit);
148
+ }
149
+ function boundedWait(operation, wait) {
150
+ if (!Number.isInteger(wait) ||
151
+ wait < 0 ||
152
+ wait > MAX_INSPECTION_WAIT_SECONDS) {
153
+ throw validationError(operation, "wait", "out_of_range", "wait must be an integer from 0 through 30.");
154
+ }
155
+ return String(wait);
156
+ }
157
+ /**
158
+ * Renders the shared bounded-wait query for a response-inspection read.
159
+ *
160
+ * The service owns the default. Omitting `wait` therefore sends no parameter
161
+ * at all rather than a client-invented value, which keeps the documented
162
+ * 30-second default exactly where the contract puts it.
163
+ */
164
+ function inspectionWait(operation, wait) {
165
+ return wait === undefined ? "" : `?wait=${boundedWait(operation, wait)}`;
166
+ }
167
+ function responseMediaType(response) {
168
+ return (response.headers
169
+ .get("content-type")
170
+ ?.split(";", 1)[0]
171
+ ?.trim()
172
+ .toLowerCase() ?? "");
173
+ }
174
+ /**
175
+ * Combines the caller's signal with an optional local deadline.
176
+ *
177
+ * When only one of the two exists it is passed through by identity, so a
178
+ * caller inspecting the outgoing request sees exactly the signal they gave.
179
+ */
180
+ function resolveSignal(operation, options) {
181
+ const caller = options.signal;
182
+ const timeoutMs = options.timeoutMs;
183
+ if (timeoutMs === undefined) {
184
+ return {
185
+ caller,
186
+ signal: caller,
187
+ timeoutMs: undefined,
188
+ timeoutSignal: undefined,
189
+ };
190
+ }
191
+ if (!Number.isInteger(timeoutMs) ||
192
+ timeoutMs < 1 ||
193
+ timeoutMs > MAX_TIMEOUT_MS) {
194
+ throw validationError(operation, "timeoutMs", "out_of_range", "timeoutMs must be an integer from 1 through 300000.");
195
+ }
196
+ const timeoutSignal = AbortSignal.timeout(timeoutMs);
197
+ return {
198
+ caller,
199
+ signal: caller === undefined
200
+ ? timeoutSignal
201
+ : AbortSignal.any([caller, timeoutSignal]),
202
+ timeoutMs,
203
+ timeoutSignal,
204
+ };
205
+ }
206
+ /**
207
+ * Classifies a rejected fetch without inventing a server outcome.
208
+ *
209
+ * A cancelled side effect may already have been applied, so the resulting
210
+ * error carries `outcomeUnknown` exactly as an ordinary transport failure
211
+ * does. The client still sends the request exactly once either way.
212
+ */
213
+ function cancellationOrTransport(operation, sideEffect, resolved) {
214
+ if (resolved.caller?.aborted === true) {
215
+ return new WhooshBangAbortError(operation, sideEffect);
216
+ }
217
+ if (resolved.timeoutSignal?.aborted === true &&
218
+ resolved.timeoutMs !== undefined) {
219
+ return new WhooshBangTimeoutError(operation, sideEffect, resolved.timeoutMs);
220
+ }
221
+ return new WhooshBangTransportError(operation, sideEffect);
222
+ }
223
+ function describeFailure(error) {
224
+ if (error instanceof WhooshBangProblemError) {
225
+ return {
226
+ diagnosticId: error.diagnosticId,
227
+ outcome: "problem",
228
+ problemCode: error.code,
229
+ ...(error.retryAt === undefined ? {} : { retryAt: error.retryAt }),
230
+ retryable: error.retryable,
231
+ status: error.status,
232
+ };
233
+ }
234
+ if (error instanceof WhooshBangProtocolError) {
235
+ return {
236
+ outcome: "protocol",
237
+ ...(error.status === undefined ? {} : { status: error.status }),
238
+ };
239
+ }
240
+ if (error instanceof WhooshBangContractError) {
241
+ return { outcome: "contract" };
242
+ }
243
+ if (error instanceof WhooshBangTimeoutError) {
244
+ return { outcome: "timeout" };
245
+ }
246
+ if (error instanceof WhooshBangAbortError) {
247
+ return { outcome: "abort" };
248
+ }
249
+ return { outcome: "transport" };
250
+ }
251
+ export class WhooshBangClient {
252
+ #baseUrl;
253
+ #credential;
254
+ #fetch;
255
+ #logger;
256
+ constructor(options) {
257
+ this.#baseUrl = normalizeBaseUrl(options.baseUrl);
258
+ validateCredential(options.credential);
259
+ this.#credential = options.credential;
260
+ const runtimeFetch = options.fetch ?? globalThis.fetch;
261
+ if (typeof runtimeFetch !== "function") {
262
+ throw new TypeError("A Fetch-compatible implementation is required.");
263
+ }
264
+ this.#fetch = runtimeFetch.bind(globalThis);
265
+ this.#logger = options.logger;
266
+ }
267
+ async getAccount(options = {}) {
268
+ return this.#request({
269
+ method: "GET",
270
+ operation: "getAccount",
271
+ options,
272
+ path: "v1/account",
273
+ sideEffect: false,
274
+ validator: validateAccountProjection,
275
+ });
276
+ }
277
+ async listProjects(options = {}) {
278
+ const search = new URLSearchParams();
279
+ if (options.after !== undefined) {
280
+ assertOpaqueCursor("listProjects", options.after, "project");
281
+ search.set("after", options.after);
282
+ }
283
+ if (options.limit !== undefined) {
284
+ search.set("limit", boundedLimit("listProjects", options.limit, MAX_PAGE_LIMIT));
285
+ }
286
+ const suffix = search.size === 0 ? "" : `?${search.toString()}`;
287
+ return this.#request({
288
+ method: "GET",
289
+ operation: "listProjects",
290
+ options,
291
+ path: `v1/projects${suffix}`,
292
+ sideEffect: false,
293
+ validator: validateProjectCollection,
294
+ });
295
+ }
296
+ async createProject(request, options) {
297
+ assertInput("createProject", request, validateCreateProjectRequest);
298
+ return this.#request({
299
+ body: request,
300
+ idempotencyKey: options.idempotencyKey,
301
+ method: "POST",
302
+ operation: "createProject",
303
+ options,
304
+ path: "v1/projects",
305
+ sideEffect: true,
306
+ validator: validateProject,
307
+ });
308
+ }
309
+ async getProject(projectId, options = {}) {
310
+ return this.#request({
311
+ method: "GET",
312
+ operation: "getProject",
313
+ options,
314
+ path: `v1/projects/${pathSegment("getProject", projectId, "projectId")}`,
315
+ sideEffect: false,
316
+ validator: validateProject,
317
+ });
318
+ }
319
+ async listApiCredentials(projectId, environmentId, options = {}) {
320
+ const operation = "listApiCredentials";
321
+ const search = new URLSearchParams();
322
+ if (options.after !== undefined) {
323
+ assertOpaqueCursor(operation, options.after, "API credential");
324
+ search.set("after", options.after);
325
+ }
326
+ if (options.limit !== undefined) {
327
+ search.set("limit", boundedLimit(operation, options.limit, MAX_PAGE_LIMIT));
328
+ }
329
+ const suffix = search.size === 0 ? "" : `?${search.toString()}`;
330
+ return this.#request({
331
+ method: "GET",
332
+ operation,
333
+ options,
334
+ path: `${environmentRoot(operation, projectId, environmentId)}/credentials` +
335
+ suffix,
336
+ sideEffect: false,
337
+ validator: validateApiCredentialCollection,
338
+ });
339
+ }
340
+ async createApiCredential(projectId, environmentId, request, options = {}) {
341
+ const operation = "createApiCredential";
342
+ assertInput(operation, request, validateCreateApiCredentialRequest);
343
+ return this.#request({
344
+ body: request,
345
+ method: "POST",
346
+ operation,
347
+ options,
348
+ path: `${environmentRoot(operation, projectId, environmentId)}/credentials`,
349
+ sideEffect: true,
350
+ validator: validateApiCredentialSecretReveal,
351
+ });
352
+ }
353
+ async rotateApiCredential(projectId, environmentId, credentialId, request, options = {}) {
354
+ const operation = "rotateApiCredential";
355
+ assertInput(operation, request, validateRotateApiCredentialRequest);
356
+ return this.#request({
357
+ body: request,
358
+ method: "POST",
359
+ operation,
360
+ options,
361
+ path: `${environmentRoot(operation, projectId, environmentId)}/credentials/` +
362
+ `${pathSegment(operation, credentialId, "credentialId")}/rotate`,
363
+ sideEffect: true,
364
+ validator: validateApiCredentialRotation,
365
+ });
366
+ }
367
+ async revokeApiCredential(projectId, environmentId, credentialId, options = {}) {
368
+ const operation = "revokeApiCredential";
369
+ return this.#request({
370
+ method: "POST",
371
+ operation,
372
+ options,
373
+ path: `${environmentRoot(operation, projectId, environmentId)}/credentials/` +
374
+ `${pathSegment(operation, credentialId, "credentialId")}/revoke`,
375
+ sideEffect: true,
376
+ validator: validateApiCredential,
377
+ });
378
+ }
379
+ async createSubscriptionLink(request, options) {
380
+ assertInput("createSubscriptionLink", request, validateCreateSubscriptionLinkRequest);
381
+ return this.#request({
382
+ body: request,
383
+ idempotencyKey: options.idempotencyKey,
384
+ method: "POST",
385
+ operation: "createSubscriptionLink",
386
+ options,
387
+ path: "v1/subscription-links",
388
+ sideEffect: true,
389
+ validator: validateSubscriptionLink,
390
+ });
391
+ }
392
+ async getSubscriptionLink(subscriptionLinkId, options = {}) {
393
+ const operation = "getSubscriptionLink";
394
+ return this.#request({
395
+ method: "GET",
396
+ operation,
397
+ options,
398
+ path: `v1/subscription-links/${pathSegment(operation, subscriptionLinkId, "subscriptionLinkId")}`,
399
+ sideEffect: false,
400
+ validator: validateSubscriptionLink,
401
+ });
402
+ }
403
+ async cancelSubscriptionLink(subscriptionLinkId, options) {
404
+ const operation = "cancelSubscriptionLink";
405
+ return this.#request({
406
+ idempotencyKey: options.idempotencyKey,
407
+ method: "POST",
408
+ operation,
409
+ options,
410
+ path: `v1/subscription-links/${pathSegment(operation, subscriptionLinkId, "subscriptionLinkId")}/cancel`,
411
+ sideEffect: true,
412
+ validator: validateSubscriptionLink,
413
+ });
414
+ }
415
+ async createMessage(request, options) {
416
+ assertInput("createMessage", request, validateCreateMessageRequest);
417
+ return this.#request({
418
+ body: request,
419
+ idempotencyKey: options.idempotencyKey,
420
+ method: "POST",
421
+ operation: "createMessage",
422
+ options,
423
+ path: "v1/messages",
424
+ sideEffect: true,
425
+ validator: validateMessage,
426
+ });
427
+ }
428
+ /**
429
+ * Reads the canonical message state, optionally holding for an open
430
+ * interaction's terminal state.
431
+ *
432
+ * A `wait` that expires still resolves with the current message. Inspection
433
+ * never acknowledges, consumes, retries, or replays anything.
434
+ */
435
+ async getMessage(messageId, options = {}) {
436
+ const operation = "getMessage";
437
+ const suffix = inspectionWait(operation, options.wait);
438
+ return this.#request({
439
+ method: "GET",
440
+ operation,
441
+ options,
442
+ path: `v1/messages/${pathSegment(operation, messageId, "messageId")}${suffix}`,
443
+ sideEffect: false,
444
+ validator: validateMessage,
445
+ });
446
+ }
447
+ /**
448
+ * Reads the canonical interaction summary, optionally holding for its
449
+ * terminal state.
450
+ *
451
+ * Expiry without resolution returns the current `open` summary successfully
452
+ * and can be re-polled immediately.
453
+ */
454
+ async getInteraction(interactionId, options = {}) {
455
+ const operation = "getInteraction";
456
+ const suffix = inspectionWait(operation, options.wait);
457
+ return this.#request({
458
+ method: "GET",
459
+ operation,
460
+ options,
461
+ path: `v1/interactions/${pathSegment(operation, interactionId, "interactionId")}${suffix}`,
462
+ sideEffect: false,
463
+ validator: validateInteractionSummary,
464
+ });
465
+ }
466
+ async cancelMessage(messageId, options) {
467
+ const operation = "cancelMessage";
468
+ return this.#request({
469
+ idempotencyKey: options.idempotencyKey,
470
+ method: "POST",
471
+ operation,
472
+ options,
473
+ path: `v1/messages/${pathSegment(operation, messageId, "messageId")}/cancel`,
474
+ sideEffect: true,
475
+ validator: validateMessage,
476
+ });
477
+ }
478
+ async getCapabilities(options = {}) {
479
+ return this.#request({
480
+ method: "GET",
481
+ operation: "getCapabilities",
482
+ options,
483
+ path: "v1/capabilities",
484
+ sideEffect: false,
485
+ validator: validateProviderCapabilitiesCollection,
486
+ });
487
+ }
488
+ async listCustomerEndpoints(projectId, environmentId, options = {}) {
489
+ const operation = "listCustomerEndpoints";
490
+ return this.#request({
491
+ method: "GET",
492
+ operation,
493
+ options,
494
+ path: customerEndpointRoot(operation, projectId, environmentId),
495
+ sideEffect: false,
496
+ validator: validateCustomerEndpointCollection,
497
+ });
498
+ }
499
+ /**
500
+ * Creates one signing endpoint.
501
+ *
502
+ * The signing secret is revealed exactly once, in this result. An equivalent
503
+ * idempotent replay returns the same logical endpoint without it, so a caller
504
+ * that loses the value must rotate rather than re-read.
505
+ */
506
+ async createCustomerEndpoint(projectId, environmentId, request, options) {
507
+ const operation = "createCustomerEndpoint";
508
+ assertInput(operation, request, validateCreateCustomerEndpointRequest);
509
+ return this.#request({
510
+ body: request,
511
+ idempotencyKey: options.idempotencyKey,
512
+ method: "POST",
513
+ operation,
514
+ options,
515
+ path: customerEndpointRoot(operation, projectId, environmentId),
516
+ sideEffect: true,
517
+ validator: validateCustomerEndpointMutationResult,
518
+ });
519
+ }
520
+ async getCustomerEndpoint(projectId, environmentId, endpointId, options = {}) {
521
+ const operation = "getCustomerEndpoint";
522
+ return this.#request({
523
+ method: "GET",
524
+ operation,
525
+ options,
526
+ path: customerEndpointPath(operation, projectId, environmentId, endpointId),
527
+ sideEffect: false,
528
+ validator: validateCustomerEndpoint,
529
+ });
530
+ }
531
+ async verifyCustomerEndpoint(projectId, environmentId, endpointId, options = {}) {
532
+ const operation = "verifyCustomerEndpoint";
533
+ return this.#request({
534
+ method: "POST",
535
+ operation,
536
+ options,
537
+ path: `${customerEndpointPath(operation, projectId, environmentId, endpointId)}/verify`,
538
+ sideEffect: true,
539
+ validator: validateCustomerEndpoint,
540
+ });
541
+ }
542
+ /**
543
+ * Starts a rotation with a bounded overlap window.
544
+ *
545
+ * Both secrets verify for `overlap_seconds`, so a receiver that accepts the
546
+ * previous and the new secret together has no verification gap.
547
+ */
548
+ async rotateCustomerEndpoint(projectId, environmentId, endpointId, request, options) {
549
+ const operation = "rotateCustomerEndpoint";
550
+ assertInput(operation, request, validateRotateCustomerEndpointRequest);
551
+ return this.#request({
552
+ body: request,
553
+ idempotencyKey: options.idempotencyKey,
554
+ method: "POST",
555
+ operation,
556
+ options,
557
+ path: `${customerEndpointPath(operation, projectId, environmentId, endpointId)}/rotate`,
558
+ sideEffect: true,
559
+ validator: validateCustomerEndpointMutationResult,
560
+ });
561
+ }
562
+ async pauseCustomerEndpoint(projectId, environmentId, endpointId, options = {}) {
563
+ return this.#transitionCustomerEndpoint("pauseCustomerEndpoint", "pause", projectId, environmentId, endpointId, options);
564
+ }
565
+ async resumeCustomerEndpoint(projectId, environmentId, endpointId, options = {}) {
566
+ return this.#transitionCustomerEndpoint("resumeCustomerEndpoint", "resume", projectId, environmentId, endpointId, options);
567
+ }
568
+ async disableCustomerEndpoint(projectId, environmentId, endpointId, options = {}) {
569
+ return this.#transitionCustomerEndpoint("disableCustomerEndpoint", "disable", projectId, environmentId, endpointId, options);
570
+ }
571
+ async listCustomerEndpointAttempts(projectId, environmentId, endpointId, options = {}) {
572
+ const operation = "listCustomerEndpointAttempts";
573
+ return this.#request({
574
+ method: "GET",
575
+ operation,
576
+ options,
577
+ path: `${customerEndpointPath(operation, projectId, environmentId, endpointId)}/attempts`,
578
+ sideEffect: false,
579
+ validator: validateCustomerEndpointAttemptCollection,
580
+ });
581
+ }
582
+ /**
583
+ * The environment-scoped forms of the product operations.
584
+ *
585
+ * They exist for a caller whose authority selects an account rather than a
586
+ * project — a person's OAuth grant — and name the project and environment in
587
+ * the path that the flat forms take from a credential. Request, response,
588
+ * and semantics are otherwise identical, so a caller that changes authority
589
+ * changes which method it calls and nothing else.
590
+ */
591
+ async createSubscriptionLinkInEnvironment(projectId, environmentId, request, options) {
592
+ const operation = "createSubscriptionLinkInEnvironment";
593
+ assertInput(operation, request, validateCreateSubscriptionLinkRequest);
594
+ return this.#request({
595
+ body: request,
596
+ idempotencyKey: options.idempotencyKey,
597
+ method: "POST",
598
+ operation,
599
+ options,
600
+ path: `${environmentRoot(operation, projectId, environmentId)}/subscription-links`,
601
+ sideEffect: true,
602
+ validator: validateSubscriptionLink,
603
+ });
604
+ }
605
+ async getSubscriptionLinkInEnvironment(projectId, environmentId, subscriptionLinkId, options = {}) {
606
+ const operation = "getSubscriptionLinkInEnvironment";
607
+ return this.#request({
608
+ method: "GET",
609
+ operation,
610
+ options,
611
+ path: `${environmentRoot(operation, projectId, environmentId)}/subscription-links/` +
612
+ pathSegment(operation, subscriptionLinkId, "subscriptionLinkId"),
613
+ sideEffect: false,
614
+ validator: validateSubscriptionLink,
615
+ });
616
+ }
617
+ async createMessageInEnvironment(projectId, environmentId, request, options) {
618
+ const operation = "createMessageInEnvironment";
619
+ assertInput(operation, request, validateCreateMessageRequest);
620
+ return this.#request({
621
+ body: request,
622
+ idempotencyKey: options.idempotencyKey,
623
+ method: "POST",
624
+ operation,
625
+ options,
626
+ path: `${environmentRoot(operation, projectId, environmentId)}/messages`,
627
+ sideEffect: true,
628
+ validator: validateMessage,
629
+ });
630
+ }
631
+ async getMessageInEnvironment(projectId, environmentId, messageId, options = {}) {
632
+ const operation = "getMessageInEnvironment";
633
+ const suffix = inspectionWait(operation, options.wait);
634
+ return this.#request({
635
+ method: "GET",
636
+ operation,
637
+ options,
638
+ path: `${environmentRoot(operation, projectId, environmentId)}/messages/` +
639
+ `${pathSegment(operation, messageId, "messageId")}${suffix}`,
640
+ sideEffect: false,
641
+ validator: validateMessage,
642
+ });
643
+ }
644
+ async getInteractionInEnvironment(projectId, environmentId, interactionId, options = {}) {
645
+ const operation = "getInteractionInEnvironment";
646
+ const suffix = inspectionWait(operation, options.wait);
647
+ return this.#request({
648
+ method: "GET",
649
+ operation,
650
+ options,
651
+ path: `${environmentRoot(operation, projectId, environmentId)}/interactions/` +
652
+ `${pathSegment(operation, interactionId, "interactionId")}${suffix}`,
653
+ sideEffect: false,
654
+ validator: validateInteractionSummary,
655
+ });
656
+ }
657
+ async createMachineClientInEnvironment(projectId, environmentId, request, options) {
658
+ const operation = "createMachineClientInEnvironment";
659
+ assertInput(operation, request, validateCreateMachineClientRequest);
660
+ return this.#request({
661
+ body: request,
662
+ idempotencyKey: options.idempotencyKey,
663
+ method: "POST",
664
+ operation,
665
+ options,
666
+ path: `${environmentRoot(operation, projectId, environmentId)}/machine-clients`,
667
+ sideEffect: true,
668
+ validator: validateMachineClient,
669
+ });
670
+ }
671
+ async getCapabilitiesInEnvironment(projectId, environmentId, options = {}) {
672
+ const operation = "getCapabilitiesInEnvironment";
673
+ return this.#request({
674
+ method: "GET",
675
+ operation,
676
+ options,
677
+ path: `${environmentRoot(operation, projectId, environmentId)}/capabilities`,
678
+ sideEffect: false,
679
+ validator: validateProviderCapabilitiesCollection,
680
+ });
681
+ }
682
+ async createMachineClient(request, options) {
683
+ assertInput("createMachineClient", request, validateCreateMachineClientRequest);
684
+ return this.#request({
685
+ body: request,
686
+ idempotencyKey: options.idempotencyKey,
687
+ method: "POST",
688
+ operation: "createMachineClient",
689
+ options,
690
+ path: "v1/machine-clients",
691
+ sideEffect: true,
692
+ validator: validateMachineClient,
693
+ });
694
+ }
695
+ async getMachineClient(machineClientId, options = {}) {
696
+ const operation = "getMachineClient";
697
+ return this.#request({
698
+ method: "GET",
699
+ operation,
700
+ options,
701
+ path: `v1/machine-clients/${pathSegment(operation, machineClientId, "machineClientId")}`,
702
+ sideEffect: false,
703
+ validator: validateMachineClient,
704
+ });
705
+ }
706
+ /**
707
+ * Registers a locally generated credential by digest only.
708
+ *
709
+ * The readable secret never leaves the caller's process. See
710
+ * `createMachineCredential` for the generator that produces the pair.
711
+ */
712
+ async registerMachineCredential(machineClientId, request, options) {
713
+ const operation = "registerMachineCredential";
714
+ assertInput(operation, request, validateRegisterMachineCredentialRequest);
715
+ return this.#request({
716
+ body: request,
717
+ idempotencyKey: options.idempotencyKey,
718
+ method: "POST",
719
+ operation,
720
+ options,
721
+ path: `v1/machine-clients/${pathSegment(operation, machineClientId, "machineClientId")}/credentials`,
722
+ sideEffect: true,
723
+ validator: validateMachineCredential,
724
+ });
725
+ }
726
+ async revokeMachineCredential(machineClientId, credentialId, options) {
727
+ const operation = "revokeMachineCredential";
728
+ return this.#request({
729
+ idempotencyKey: options.idempotencyKey,
730
+ method: "POST",
731
+ operation,
732
+ options,
733
+ path: `v1/machine-clients/${pathSegment(operation, machineClientId, "machineClientId")}/credentials/` +
734
+ `${pathSegment(operation, credentialId, "credentialId")}/revoke`,
735
+ sideEffect: true,
736
+ validator: validateMachineCredential,
737
+ });
738
+ }
739
+ async revokeMachineClient(machineClientId, options) {
740
+ const operation = "revokeMachineClient";
741
+ return this.#request({
742
+ idempotencyKey: options.idempotencyKey,
743
+ method: "POST",
744
+ operation,
745
+ options,
746
+ path: `v1/machine-clients/${pathSegment(operation, machineClientId, "machineClientId")}/revoke`,
747
+ sideEffect: true,
748
+ validator: validateMachineClient,
749
+ });
750
+ }
751
+ async pollMachineEvents(options = {}) {
752
+ const operation = "pollMachineEvents";
753
+ const search = new URLSearchParams();
754
+ if (options.after !== undefined) {
755
+ assertMachineCursor(operation, options.after);
756
+ search.set("after", options.after);
757
+ }
758
+ if (options.wait !== undefined) {
759
+ search.set("wait", boundedWait(operation, options.wait));
760
+ }
761
+ if (options.limit !== undefined) {
762
+ search.set("limit", boundedLimit(operation, options.limit, MAX_MACHINE_POLL_LIMIT));
763
+ }
764
+ const suffix = search.size === 0 ? "" : `?${search.toString()}`;
765
+ return this.#request({
766
+ method: "GET",
767
+ operation,
768
+ options,
769
+ path: `v1/machine-events${suffix}`,
770
+ sideEffect: false,
771
+ validator: validateMachineEventPollResponse,
772
+ });
773
+ }
774
+ async acknowledgeMachineEvent(eventId, request, options) {
775
+ const operation = "acknowledgeMachineEvent";
776
+ assertInput(operation, request, validateMachineEventAckRequest);
777
+ return this.#request({
778
+ body: request,
779
+ idempotencyKey: options.idempotencyKey,
780
+ method: "POST",
781
+ operation,
782
+ options,
783
+ path: `v1/machine-events/${pathSegment(operation, eventId, "eventId")}/ack`,
784
+ sideEffect: true,
785
+ validator: validateMachineEventAckResponse,
786
+ });
787
+ }
788
+ async #transitionCustomerEndpoint(operation, action, projectId, environmentId, endpointId, options) {
789
+ return this.#request({
790
+ method: "POST",
791
+ operation,
792
+ options,
793
+ path: `${customerEndpointPath(operation, projectId, environmentId, endpointId)}/${action}`,
794
+ sideEffect: true,
795
+ validator: validateCustomerEndpoint,
796
+ });
797
+ }
798
+ #log(record) {
799
+ const logger = this.#logger;
800
+ if (logger === undefined) {
801
+ return;
802
+ }
803
+ try {
804
+ logger(record);
805
+ }
806
+ catch {
807
+ // A caller's logging hook must never change the operation's outcome.
808
+ }
809
+ }
810
+ /**
811
+ * Runs one attempt and reports exactly one log record for it.
812
+ *
813
+ * Caller input is validated first, outside the logged region, so the hook
814
+ * describes attempted requests only. A local refusal throws at the caller's
815
+ * own call site, where it is already unmissable.
816
+ */
817
+ async #request(descriptor) {
818
+ if (descriptor.idempotencyKey !== undefined) {
819
+ assertIdempotencyKey(descriptor.operation, descriptor.idempotencyKey);
820
+ }
821
+ const resolved = resolveSignal(descriptor.operation, descriptor.options);
822
+ const startedAt = monotonicNow();
823
+ try {
824
+ const result = await this.#send(descriptor, resolved);
825
+ this.#log({
826
+ ...(result.diagnosticId === undefined
827
+ ? {}
828
+ : { diagnosticId: result.diagnosticId }),
829
+ durationMs: Math.max(0, monotonicNow() - startedAt),
830
+ operation: descriptor.operation,
831
+ outcome: "success",
832
+ status: result.status,
833
+ });
834
+ return result.body;
835
+ }
836
+ catch (error) {
837
+ this.#log({
838
+ durationMs: Math.max(0, monotonicNow() - startedAt),
839
+ operation: descriptor.operation,
840
+ ...describeFailure(error),
841
+ });
842
+ throw error;
843
+ }
844
+ }
845
+ async #send(descriptor, resolved) {
846
+ const headers = new Headers({
847
+ accept: "application/json, application/problem+json",
848
+ authorization: `Bearer ${this.#credential}`,
849
+ });
850
+ if (descriptor.idempotencyKey !== undefined) {
851
+ headers.set("idempotency-key", descriptor.idempotencyKey);
852
+ }
853
+ let body;
854
+ if (descriptor.body !== undefined) {
855
+ headers.set("content-type", "application/json");
856
+ body = JSON.stringify(descriptor.body);
857
+ }
858
+ const url = new URL(descriptor.path, this.#baseUrl);
859
+ let response;
860
+ try {
861
+ response = await this.#fetch(url, {
862
+ ...(body === undefined ? {} : { body }),
863
+ headers,
864
+ method: descriptor.method,
865
+ ...(resolved.signal === undefined ? {} : { signal: resolved.signal }),
866
+ });
867
+ }
868
+ catch {
869
+ throw cancellationOrTransport(descriptor.operation, descriptor.sideEffect, resolved);
870
+ }
871
+ /**
872
+ * Reads the body, keeping a cancelled read a cancellation.
873
+ *
874
+ * A signal that fires after the response headers arrive rejects the
875
+ * stream read. Reporting that as a protocol error would blame the server
876
+ * and drop `outcomeUnknown`, when in fact the side effect may already
877
+ * have been applied.
878
+ */
879
+ const readBody = async (limit) => {
880
+ try {
881
+ return await readBoundedJson(response, descriptor.operation, limit);
882
+ }
883
+ catch (error) {
884
+ if (resolved.caller?.aborted === true ||
885
+ resolved.timeoutSignal?.aborted === true) {
886
+ throw cancellationOrTransport(descriptor.operation, descriptor.sideEffect, resolved);
887
+ }
888
+ throw error;
889
+ }
890
+ };
891
+ const diagnosticId = response.headers.get(DIAGNOSTIC_HEADER) ?? undefined;
892
+ const mediaType = responseMediaType(response);
893
+ if (!response.ok) {
894
+ if (mediaType !== "application/problem+json") {
895
+ throw new WhooshBangProtocolError(descriptor.operation, "The WhooshBang error response had an unexpected media type.", response.status);
896
+ }
897
+ const value = await readBody(MAX_PROBLEM_BYTES);
898
+ if (!validateProblem(value) || value.status !== response.status) {
899
+ throw new WhooshBangProtocolError(descriptor.operation, "The WhooshBang error response did not match Problem Details.", response.status);
900
+ }
901
+ throw new WhooshBangProblemError(value);
902
+ }
903
+ if (mediaType !== "application/json") {
904
+ throw new WhooshBangProtocolError(descriptor.operation, "The WhooshBang success response had an unexpected media type.", response.status);
905
+ }
906
+ const value = await readBody(MAX_RESPONSE_BYTES);
907
+ if (!validateAdditiveCompatible(value, descriptor.validator)) {
908
+ throw new WhooshBangContractError(descriptor.operation, contractValidationErrors(descriptor.validator), "The WhooshBang response did not match the pinned contract.");
909
+ }
910
+ return { body: value, diagnosticId, status: response.status };
911
+ }
912
+ }
913
+ function environmentRoot(operation, projectId, environmentId) {
914
+ const project = pathSegment(operation, projectId, "projectId");
915
+ const environment = pathSegment(operation, environmentId, "environmentId");
916
+ return `v1/projects/${project}/environments/${environment}`;
917
+ }
918
+ function customerEndpointRoot(operation, projectId, environmentId) {
919
+ return `${environmentRoot(operation, projectId, environmentId)}/customer-endpoints`;
920
+ }
921
+ function customerEndpointPath(operation, projectId, environmentId, endpointId) {
922
+ return `${customerEndpointRoot(operation, projectId, environmentId)}/${pathSegment(operation, endpointId, "endpointId")}`;
923
+ }
924
+ //# sourceMappingURL=client.js.map