@layervai/qurl 3.1.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. package/README.md +259 -46
  2. package/dist/cjs/client-connectors.d.ts +44 -0
  3. package/dist/cjs/client-connectors.js +189 -0
  4. package/dist/cjs/client-delegated.d.ts +30 -0
  5. package/dist/cjs/client-delegated.js +312 -0
  6. package/dist/cjs/client-errors.d.ts +44 -0
  7. package/dist/cjs/client-errors.js +219 -0
  8. package/dist/cjs/client-http-types.d.ts +37 -0
  9. package/dist/cjs/client-http-types.js +2 -0
  10. package/dist/cjs/client-input-fields.d.ts +53 -0
  11. package/dist/cjs/client-input-fields.js +262 -0
  12. package/dist/cjs/client-limits.d.ts +36 -0
  13. package/dist/cjs/client-limits.js +61 -0
  14. package/dist/cjs/client-management.d.ts +12 -0
  15. package/dist/cjs/client-management.js +119 -0
  16. package/dist/cjs/client-paths.d.ts +21 -0
  17. package/dist/cjs/client-paths.js +143 -0
  18. package/dist/cjs/client-portals.d.ts +82 -0
  19. package/dist/cjs/client-portals.js +244 -0
  20. package/dist/cjs/client-resources.d.ts +29 -0
  21. package/dist/cjs/client-resources.js +331 -0
  22. package/dist/cjs/client-validation.d.ts +35 -0
  23. package/dist/cjs/client-validation.js +326 -0
  24. package/dist/cjs/client.d.ts +45 -75
  25. package/dist/cjs/client.js +621 -2448
  26. package/dist/cjs/errors.d.ts +9 -1
  27. package/dist/cjs/errors.js +10 -1
  28. package/dist/cjs/index.d.ts +4 -2
  29. package/dist/cjs/index.js +4 -1
  30. package/dist/cjs/response-metadata.d.ts +13 -0
  31. package/dist/cjs/response-metadata.js +38 -0
  32. package/dist/cjs/share.js +2 -0
  33. package/dist/cjs/target-path.d.ts +2 -0
  34. package/dist/cjs/target-path.js +34 -0
  35. package/dist/cjs/types.d.ts +83 -7
  36. package/dist/cjs/version.d.ts +1 -1
  37. package/dist/cjs/version.js +1 -1
  38. package/dist/esm/client-connectors.d.ts +44 -0
  39. package/dist/esm/client-connectors.js +180 -0
  40. package/dist/esm/client-delegated.d.ts +30 -0
  41. package/dist/esm/client-delegated.js +295 -0
  42. package/dist/esm/client-errors.d.ts +44 -0
  43. package/dist/esm/client-errors.js +206 -0
  44. package/dist/esm/client-http-types.d.ts +37 -0
  45. package/dist/esm/client-http-types.js +1 -0
  46. package/dist/esm/client-input-fields.d.ts +53 -0
  47. package/dist/esm/client-input-fields.js +258 -0
  48. package/dist/esm/client-limits.d.ts +36 -0
  49. package/dist/esm/client-limits.js +58 -0
  50. package/dist/esm/client-management.d.ts +12 -0
  51. package/dist/esm/client-management.js +113 -0
  52. package/dist/esm/client-paths.d.ts +21 -0
  53. package/dist/esm/client-paths.js +138 -0
  54. package/dist/esm/client-portals.d.ts +82 -0
  55. package/dist/esm/client-portals.js +236 -0
  56. package/dist/esm/client-resources.d.ts +29 -0
  57. package/dist/esm/client-resources.js +323 -0
  58. package/dist/esm/client-validation.d.ts +35 -0
  59. package/dist/esm/client-validation.js +302 -0
  60. package/dist/esm/client.d.ts +45 -76
  61. package/dist/esm/client.js +318 -2147
  62. package/dist/esm/crid.d.ts +0 -1
  63. package/dist/esm/crid.js +0 -1
  64. package/dist/esm/errors.d.ts +9 -2
  65. package/dist/esm/errors.js +9 -1
  66. package/dist/esm/index.d.ts +4 -3
  67. package/dist/esm/index.js +2 -2
  68. package/dist/esm/internal.d.ts +0 -1
  69. package/dist/esm/internal.js +0 -1
  70. package/dist/esm/node/agent-runtime.d.ts +0 -1
  71. package/dist/esm/node/agent-runtime.js +0 -1
  72. package/dist/esm/node/agent-state.d.ts +0 -1
  73. package/dist/esm/node/agent-state.js +0 -1
  74. package/dist/esm/node/agent-transport.d.ts +0 -1
  75. package/dist/esm/node/agent-transport.js +0 -1
  76. package/dist/esm/node/deployment.d.ts +0 -1
  77. package/dist/esm/node/deployment.js +0 -1
  78. package/dist/esm/node/file-agent-state.d.ts +0 -1
  79. package/dist/esm/node/file-agent-state.js +0 -1
  80. package/dist/esm/node/native-udp.d.ts +0 -1
  81. package/dist/esm/node/native-udp.js +0 -1
  82. package/dist/esm/node/nhp-wire.d.ts +0 -1
  83. package/dist/esm/node/nhp-wire.js +0 -1
  84. package/dist/esm/node/portal-opener.d.ts +0 -1
  85. package/dist/esm/node/portal-opener.js +0 -1
  86. package/dist/esm/node/provider.d.ts +0 -1
  87. package/dist/esm/node/provider.js +0 -1
  88. package/dist/esm/node/qv2.d.ts +0 -1
  89. package/dist/esm/node/qv2.js +0 -1
  90. package/dist/esm/node/relay.d.ts +0 -1
  91. package/dist/esm/node/relay.js +0 -1
  92. package/dist/esm/node/sealed-agent-state.d.ts +0 -1
  93. package/dist/esm/node/sealed-agent-state.js +0 -1
  94. package/dist/esm/node/strict-json.d.ts +0 -1
  95. package/dist/esm/node/strict-json.js +0 -1
  96. package/dist/esm/node.d.ts +0 -1
  97. package/dist/esm/node.js +0 -1
  98. package/dist/esm/response-metadata.d.ts +13 -0
  99. package/dist/esm/response-metadata.js +33 -0
  100. package/dist/esm/share.d.ts +0 -1
  101. package/dist/esm/share.js +2 -1
  102. package/dist/esm/target-path.d.ts +2 -0
  103. package/dist/esm/target-path.js +30 -0
  104. package/dist/esm/types.d.ts +83 -8
  105. package/dist/esm/types.js +0 -1
  106. package/dist/esm/version.d.ts +1 -2
  107. package/dist/esm/version.js +1 -2
  108. package/package.json +9 -7
  109. package/dist/esm/client.d.ts.map +0 -1
  110. package/dist/esm/client.js.map +0 -1
  111. package/dist/esm/crid.d.ts.map +0 -1
  112. package/dist/esm/crid.js.map +0 -1
  113. package/dist/esm/errors.d.ts.map +0 -1
  114. package/dist/esm/errors.js.map +0 -1
  115. package/dist/esm/index.d.ts.map +0 -1
  116. package/dist/esm/index.js.map +0 -1
  117. package/dist/esm/internal.d.ts.map +0 -1
  118. package/dist/esm/internal.js.map +0 -1
  119. package/dist/esm/node/agent-runtime.d.ts.map +0 -1
  120. package/dist/esm/node/agent-runtime.js.map +0 -1
  121. package/dist/esm/node/agent-state.d.ts.map +0 -1
  122. package/dist/esm/node/agent-state.js.map +0 -1
  123. package/dist/esm/node/agent-transport.d.ts.map +0 -1
  124. package/dist/esm/node/agent-transport.js.map +0 -1
  125. package/dist/esm/node/deployment.d.ts.map +0 -1
  126. package/dist/esm/node/deployment.js.map +0 -1
  127. package/dist/esm/node/file-agent-state.d.ts.map +0 -1
  128. package/dist/esm/node/file-agent-state.js.map +0 -1
  129. package/dist/esm/node/native-udp.d.ts.map +0 -1
  130. package/dist/esm/node/native-udp.js.map +0 -1
  131. package/dist/esm/node/nhp-wire.d.ts.map +0 -1
  132. package/dist/esm/node/nhp-wire.js.map +0 -1
  133. package/dist/esm/node/portal-opener.d.ts.map +0 -1
  134. package/dist/esm/node/portal-opener.js.map +0 -1
  135. package/dist/esm/node/provider.d.ts.map +0 -1
  136. package/dist/esm/node/provider.js.map +0 -1
  137. package/dist/esm/node/qv2.d.ts.map +0 -1
  138. package/dist/esm/node/qv2.js.map +0 -1
  139. package/dist/esm/node/relay.d.ts.map +0 -1
  140. package/dist/esm/node/relay.js.map +0 -1
  141. package/dist/esm/node/sealed-agent-state.d.ts.map +0 -1
  142. package/dist/esm/node/sealed-agent-state.js.map +0 -1
  143. package/dist/esm/node/strict-json.d.ts.map +0 -1
  144. package/dist/esm/node/strict-json.js.map +0 -1
  145. package/dist/esm/node.d.ts.map +0 -1
  146. package/dist/esm/node.js.map +0 -1
  147. package/dist/esm/share.d.ts.map +0 -1
  148. package/dist/esm/share.js.map +0 -1
  149. package/dist/esm/types.d.ts.map +0 -1
  150. package/dist/esm/types.js.map +0 -1
  151. package/dist/esm/version.d.ts.map +0 -1
  152. package/dist/esm/version.js.map +0 -1
@@ -1,46 +1,21 @@
1
- import { parseCrid, cridKeyMatches } from "./crid.js";
2
- import { ConnectorResourceOutcomeUnknownError, createError, DelegatedBatchOutcomeUnknownError, ERROR_CODE_AMBIGUOUS_RESOURCE, ERROR_CODE_CLIENT_VALIDATION, ERROR_CODE_RESOURCE_NOT_FOUND, ERROR_CODE_CONNECTOR_RESOURCE_REVOKED, ERROR_CODE_UNEXPECTED_RESPONSE, ERROR_CODE_UNKNOWN, NetworkError, NotFoundError, QURLError, RuntimeError, TimeoutError, ValidationError, } from "./errors.js";
1
+ import { ERROR_CODE_AMBIGUOUS_RESOURCE, ERROR_CODE_RESOURCE_NOT_FOUND, ERROR_CODE_UNEXPECTED_RESPONSE, ERROR_CODE_UNKNOWN, NetworkError, NotFoundError, QURLError, RuntimeError, TimeoutError, ValidationError, createError, } from "./errors.js";
2
+ import { copyResponseMetadata, withResponseMetadata } from "./response-metadata.js";
3
3
  import { stripTrailingSlashes } from "./internal.js";
4
- import { ShareLink } from "./share.js";
5
4
  import { VERSION } from "./version.js";
5
+ import { ShareLink } from "./share.js";
6
+ import { MAX_AUTO_PAGINATION_PAGES, MAX_BATCH_ITEMS, MAX_CUSTOM_DOMAIN, MAX_DELEGATED_ETAG, MAX_DESCRIPTION, MAX_IDEMPOTENCY_KEY, MAX_LABEL, MAX_RESPONSE_BODY_BYTES, MAX_CONFIGURABLE_RESPONSE_BODY_BYTES, RETRY_AFTER_HARD_CAP_MS, RETRY_AFTER_PARSE_LIMIT_S, RETRY_BASE_DELAY_MS, RETRY_MAX_DELAY_MS, } from "./client-limits.js";
7
+ import { TEXT_DECODER, TEXT_ENCODER, boundedErrorIdentifier, boundedErrorSnippet, boundedInvalidFields, boundedObjectKeys, boundedTombstone, clientValidationError, isIndependentAbort, isSdkTimeout, unexpectedResponseError, withErrorCause, } from "./client-errors.js";
8
+ import { describeShape, normalizePatchFields, requireAtLeastOneField, requireBooleanIfPresent, requireConsumerDecision, requireListLimitInRange, requireMaxLength, requireMaxSessionsInRange, requireNoUnknownFields, requireNonEmptyIfPresent, requireNonEmptyStringField, requireObjectInput, requirePortalTargetUrl, requireResourceNameIfPresent, requireValidAccessPolicy, requireValidTags, } from "./client-validation.js";
9
+ import { API_KEY_LIST_PARAM_KEYS, BILLING_INVOICE_LIST_PARAM_KEYS, CONNECTOR_INSTALLATION_LIST_PARAM_KEYS, CREATE_ACCESS_CODE_FIELD_KEYS, CREATE_API_KEY_FIELD_KEYS, CREATE_BILLING_CHECKOUT_FIELD_KEYS, CREATE_QURL_FOR_RESOURCE_FIELD_KEYS, CREATE_RESOURCE_FIELD_KEYS, CREATE_WEBHOOK_FIELD_KEYS, DOMAIN_LIST_PARAM_KEYS, LIST_PARAM_KEYS, MINT_FIELD_KEYS, PROTECT_URL_OPTION_KEYS, REDEEM_ACCESS_CODE_FIELD_KEYS, REGISTER_DOMAIN_FIELD_KEYS, REQUEST_OPTION_KEYS, RESOURCE_LIST_PARAM_KEYS, SHARE_RESOURCE_OPTION_KEYS, UPDATE_API_KEY_FIELD_KEYS, UPDATE_CUSTOMER_FIELD_KEYS, UPDATE_FIELD_KEYS, UPDATE_RESOURCE_FIELD_KEYS, UPDATE_RESOURCE_QURL_FIELD_KEYS, UPDATE_WEBHOOK_FIELD_KEYS, WEBHOOK_DELIVERY_LIST_PARAM_KEYS, WEBHOOK_LIST_PARAM_KEYS, } from "./client-input-fields.js";
10
+ import { batchItemResultValidationReason, sameConsumerPolicy, validateCreateInput, validateQurlTokenOptions, validateResourceQurlUpdateInput, validateResourceWriteFields, } from "./client-resources.js";
11
+ import { ProtectedResource, buildCreatePortalBody, extractAccessToken, parseApiDate, parsePortal, } from "./client-portals.js";
12
+ import { classifyConnectorMutationFailure, parseConnectorResource, requireConnectorSlug, requireConnectorSubtleCrypto, verifyResourceIdentity, } from "./client-connectors.js";
13
+ import { QURL_ID_PATH_OPTIONS, RESOURCE_OR_QURL_ID_PATH_OPTIONS, requireResourceCrid, validatePathId, } from "./client-paths.js";
14
+ import { DELEGATED_BATCH_ID_PATTERN, DELEGATED_QURL_ID_PATTERN, STRONG_ETAG_PATTERN, classifyDelegatedBatchCreateFailure, delegatedBatchBase, delegatedEnvelope, delegatedResponseError, delegatedResponseHeaders, hasDelegatedNoStore, isUtcTimestamp, requiredDelegatedHeader, validateCreateDelegatedQurlBatchInput, validateDelegatedBatchCreateOptions, validateDelegatedBatchResults, validateDelegatedLocation, validateDelegatedQurl, validateGetDelegatedQurlBatchOptions, } from "./client-delegated.js";
15
+ import { validateApiKeyWriteFields, validateEnrollmentTokenFields, validateWebhookWriteFields, } from "./client-management.js";
6
16
  const DEFAULT_BASE_URL = "https://api.layerv.ai";
7
17
  const DEFAULT_MAX_RETRIES = 3;
8
18
  const DEFAULT_TIMEOUT = 30_000;
9
- // Match qurl-go c4f2f98's exact API response cap. Current qurl-service list
10
- // routes cap pages at 100 items. A caller can still exceed this cap with a
11
- // large, heavily escaped webhook-delivery page, so list callers must request a
12
- // smaller page if the service reports the body-limit error. This cap is fixed:
13
- // do not add an override or widen one SDK independently from the other.
14
- // The limit is enforced from Content-Length when usable and again while
15
- // consuming the response stream.
16
- const MAX_RESPONSE_BODY_BYTES = 1 << 20;
17
- const MAX_ERROR_SNIPPET_BYTES = 512;
18
- // UTF-8 uses at least one byte per UTF-16 code unit. Reading more source units
19
- // cannot add a byte that fits in a 512-byte retained diagnostic.
20
- const MAX_ERROR_SNIPPET_SOURCE_CODE_UNITS = MAX_ERROR_SNIPPET_BYTES;
21
- const MAX_INVALID_FIELD_ENTRIES = 100;
22
- const MAX_DEBUG_BODY_KEYS = 100;
23
- // Bound each retained diagnostic collection as well as each string. Without
24
- // this aggregate cap, 100 valid 512-byte keys and values could turn a small
25
- // server error into a large caller-visible object. This is a diagnostic budget,
26
- // not part of the API wire contract.
27
- const MAX_DIAGNOSTIC_COLLECTION_BYTES = 8 << 10;
28
- const TEXT_ENCODER = new TextEncoder();
29
- const ERROR_SNIPPET_ELLIPSIS = "...";
30
- const ERROR_SNIPPET_ELLIPSIS_BYTES = 3;
31
- // RFC 8259 JSON exchanged between systems must be valid UTF-8. A non-fatal
32
- // decoder would replace malformed bytes with U+FFFD before JSON.parse and hide
33
- // the producer's exact contract violation.
34
- const TEXT_DECODER = new TextDecoder("utf-8", { fatal: true });
35
- const RETRY_BASE_DELAY_MS = 500;
36
- // Bounds local exponential backoff (NOT server-asserted Retry-After —
37
- // see `RETRY_AFTER_HARD_CAP_MS` for that).
38
- const RETRY_MAX_DELAY_MS = 30_000;
39
- // Hard cap on server-asserted Retry-After. Guards against `Retry-After`
40
- // values >2^31-1 ms overflowing Node's setTimeout (silently truncated to
41
- // 1ms — turns a "wait a month" directive into a hot retry loop).
42
- const RETRY_AFTER_HARD_CAP_MS = 60 * 60 * 1000;
43
- const RETRY_AFTER_PARSE_LIMIT_S = RETRY_AFTER_HARD_CAP_MS / 1000;
44
19
  const RETRYABLE_STATUS = new Set([429, 502, 503, 504]);
45
20
  // Keep mutating status retries 429-only until the service-side idempotency
46
21
  // cache contract is proven across 5xx, TTL, and replay-window edge cases.
@@ -50,12 +25,11 @@ const REDIRECT_RESPONSE_STATUSES = new Set([300, 301, 302, 303, 305, 307, 308]);
50
25
  const NO_RETRYABLE_STATUSES = new Set();
51
26
  const IDEMPOTENCY_KEY_METHODS = new Set(["POST", "PATCH"]);
52
27
  const MUTATING_RETRY_METHODS = new Set(["POST", "PATCH"]);
53
- const MAX_IDEMPOTENCY_KEY = 256;
54
28
  const IDEMPOTENCY_KEY_VALUE_RE = /^[\x21-\x7e](?:[\x20-\x7e]*[\x21-\x7e])?$/;
55
29
  const UUID_HEX = Array.from({ length: 256 }, (_, i) => i.toString(16).padStart(2, "0"));
56
30
  class ResponseBodyTooLargeError extends Error {
57
- constructor() {
58
- super(`Response body exceeds ${MAX_RESPONSE_BODY_BYTES}-byte limit`);
31
+ constructor(limit) {
32
+ super(`Response body exceeds ${limit}-byte limit`);
59
33
  this.name = "ResponseBodyTooLargeError";
60
34
  }
61
35
  }
@@ -74,272 +48,6 @@ class ResponseBodyMaterializationError extends Error {
74
48
  const NO_PASSTHROUGH_STATUSES = [];
75
49
  const BATCH_PASSTHROUGH_STATUSES = [400];
76
50
  const DELEGATED_BATCH_PASSTHROUGH_STATUSES = [304];
77
- /** Allowlist of known query-param keys for the `list()` endpoint. */
78
- const LIST_PARAM_KEYS = [
79
- "limit",
80
- "cursor",
81
- "status",
82
- "q",
83
- "sort",
84
- "created_after",
85
- "created_before",
86
- "expires_before",
87
- "expires_after",
88
- ];
89
- const REQUEST_OPTION_KEYS = ["idempotencyKey"];
90
- const DELEGATED_BATCH_INPUT_KEYS = [
91
- "mint_capability",
92
- "grants",
93
- ];
94
- const DELEGATED_BATCH_GRANT_KEYS = [
95
- "expires_in",
96
- "label",
97
- "one_time_use",
98
- "max_sessions",
99
- "session_duration",
100
- "access_policy",
101
- ];
102
- const DELEGATED_BATCH_CREATE_OPTION_KEYS = [
103
- "idempotencyKey",
104
- ];
105
- const DELEGATED_BATCH_GET_OPTION_KEYS = [
106
- "etag",
107
- ];
108
- // Compile-time witness: `Exclude<keyof X, (typeof KEYS)[number]>` is
109
- // `never` iff KEYS lists every key of X. Paired with the
110
- // `satisfies readonly (keyof X)[]` clause on the array (which catches
111
- // the other direction), this gives bidirectional drift detection on
112
- // the *_KEYS allowlists at zero runtime cost.
113
- function assertExhaustive(_) {
114
- /* type-only check */
115
- }
116
- assertExhaustive(true);
117
- assertExhaustive(true);
118
- assertExhaustive(true);
119
- assertExhaustive(true);
120
- assertExhaustive(true);
121
- assertExhaustive(true);
122
- const CREATE_FIELD_KEYS = [
123
- "private",
124
- "type",
125
- "target_url",
126
- "expires_in",
127
- "one_time_use",
128
- "max_sessions",
129
- "session_duration",
130
- "label",
131
- "access_policy",
132
- "custom_domain",
133
- ];
134
- assertExhaustive(true);
135
- /**
136
- * Fields accepted by `update()`. The empty-input pre-flight check in
137
- * {@link QURLClient.update} iterates this const — so adding a new field to
138
- * {@link UpdateInput} without listing it here will fail the
139
- * paired `assertExhaustive` witness at compile time rather than silently
140
- * sneaking through an empty-input request.
141
- */
142
- const UPDATE_FIELD_KEYS = [
143
- "extend_by",
144
- "expires_at",
145
- "description",
146
- "tags",
147
- ];
148
- assertExhaustive(true);
149
- /**
150
- * Fields accepted by `mintLink()`. Iterated by the null-stripping
151
- * normalization loop in {@link QURLClient.mintLink} so unknown keys
152
- * from untyped-JS callers don't leak through to the wire body.
153
- * Symmetric with `LIST_PARAM_KEYS` / `UPDATE_FIELD_KEYS`.
154
- */
155
- const MINT_FIELD_KEYS = [
156
- "expires_in",
157
- "expires_at",
158
- "label",
159
- "one_time_use",
160
- "max_sessions",
161
- "session_duration",
162
- "access_policy",
163
- ];
164
- assertExhaustive(true);
165
- const RESOURCE_LIST_PARAM_KEYS = [
166
- "cursor",
167
- "limit",
168
- "alias",
169
- "slug",
170
- "status",
171
- "type",
172
- ];
173
- assertExhaustive(true);
174
- const CREATE_RESOURCE_FIELD_KEYS = [
175
- "private",
176
- "consumer_access",
177
- "type",
178
- "target_url",
179
- "description",
180
- "tags",
181
- "custom_domain",
182
- "alias",
183
- "slug",
184
- "find_or_create",
185
- ];
186
- assertExhaustive(true);
187
- const UPDATE_RESOURCE_FIELD_KEYS = [
188
- "allowed_device_keys_add",
189
- "allowed_device_keys_remove",
190
- "consumer_access",
191
- "expected_consumer_access_version",
192
- "description",
193
- "tags",
194
- "custom_domain",
195
- "preserve_host",
196
- "alias",
197
- ];
198
- assertExhaustive(true);
199
- const CREATE_QURL_FOR_RESOURCE_FIELD_KEYS = [
200
- "expires_in",
201
- "one_time_use",
202
- "max_sessions",
203
- "session_duration",
204
- "label",
205
- "access_policy",
206
- "target_path",
207
- ];
208
- const SHARE_RESOURCE_OPTION_KEYS = [
209
- "ttlSeconds",
210
- ];
211
- assertExhaustive(true);
212
- assertExhaustive(true);
213
- /**
214
- * Options accepted by the portal-verb `protectUrl` / `createPortal`
215
- * surface. These are camelCase caller-facing options (the portal verbs are
216
- * the idiomatic layer); the build helpers map them onto the same wire
217
- * fields the REST-shaped methods send.
218
- */
219
- const PROTECT_URL_OPTION_KEYS = [
220
- "private",
221
- "consumerAccess",
222
- "description",
223
- "tags",
224
- "customDomain",
225
- "alias",
226
- ];
227
- assertExhaustive(true);
228
- const CREATE_PORTAL_OPTION_KEYS = [
229
- "validFor",
230
- "label",
231
- "oneTimeUse",
232
- "maxSessions",
233
- "sessionDuration",
234
- "targetPath",
235
- ];
236
- assertExhaustive(true);
237
- const UPDATE_RESOURCE_QURL_FIELD_KEYS = [
238
- "extend_by",
239
- "expires_at",
240
- "label",
241
- "access_policy",
242
- "max_sessions",
243
- "session_duration",
244
- ];
245
- assertExhaustive(true);
246
- const UPDATE_CUSTOMER_FIELD_KEYS = [
247
- "spending_cap_cents",
248
- ];
249
- assertExhaustive(true);
250
- const CREATE_BILLING_CHECKOUT_FIELD_KEYS = [
251
- "plan",
252
- ];
253
- assertExhaustive(true);
254
- const REGISTER_DOMAIN_FIELD_KEYS = [
255
- "domain",
256
- ];
257
- assertExhaustive(true);
258
- const CREATE_WEBHOOK_FIELD_KEYS = [
259
- "url",
260
- "events",
261
- "description",
262
- ];
263
- assertExhaustive(true);
264
- const UPDATE_WEBHOOK_FIELD_KEYS = [
265
- "url",
266
- "events",
267
- "description",
268
- "status",
269
- ];
270
- assertExhaustive(true);
271
- const CREATE_API_KEY_FIELD_KEYS = [
272
- "kind",
273
- "name",
274
- "scopes",
275
- "target",
276
- "claims",
277
- "expires_in",
278
- ];
279
- assertExhaustive(true);
280
- const UPDATE_API_KEY_FIELD_KEYS = [
281
- "name",
282
- "scopes",
283
- ];
284
- assertExhaustive(true);
285
- const REDEEM_ACCESS_CODE_FIELD_KEYS = [
286
- "code",
287
- "honeypot",
288
- "elapsed_ms",
289
- ];
290
- assertExhaustive(true);
291
- const CREATE_ACCESS_CODE_FIELD_KEYS = [
292
- "resource_id",
293
- "name",
294
- "max_uses",
295
- "expires_at",
296
- ];
297
- assertExhaustive(true);
298
- const CONNECTOR_INSTALLATION_LIST_PARAM_KEYS = [
299
- "cursor",
300
- "limit",
301
- ];
302
- assertExhaustive(true);
303
- const BILLING_INVOICE_LIST_PARAM_KEYS = [
304
- "limit",
305
- "cursor",
306
- ];
307
- assertExhaustive(true);
308
- const DOMAIN_LIST_PARAM_KEYS = [
309
- "limit",
310
- "cursor",
311
- ];
312
- assertExhaustive(true);
313
- const WEBHOOK_LIST_PARAM_KEYS = [
314
- "limit",
315
- "cursor",
316
- ];
317
- assertExhaustive(true);
318
- const WEBHOOK_DELIVERY_LIST_PARAM_KEYS = [
319
- "limit",
320
- "cursor",
321
- ];
322
- assertExhaustive(true);
323
- const API_KEY_LIST_PARAM_KEYS = [
324
- "limit",
325
- "cursor",
326
- "status",
327
- ];
328
- assertExhaustive(true);
329
- /**
330
- * Construct a {@link ValidationError} for a client-side pre-flight check.
331
- * Uses `status: 0` (matching {@link NetworkError}/{@link TimeoutError}) and
332
- * `code: "client_validation"` so catch-by-class still works and callers can
333
- * tell the error originated inside the SDK rather than from the API.
334
- */
335
- function clientValidationError(detail) {
336
- return new ValidationError({
337
- status: 0,
338
- code: ERROR_CODE_CLIENT_VALIDATION,
339
- title: "Invalid Argument",
340
- detail,
341
- });
342
- }
343
51
  function validateRequestOptions(options) {
344
52
  if (options === undefined) {
345
53
  return;
@@ -411,171 +119,6 @@ function fillRandomBytes(bytes) {
411
119
  }
412
120
  throw new RuntimeError("globalThis.crypto.getRandomValues is required to generate Idempotency-Key");
413
121
  }
414
- /**
415
- * Construct a {@link ValidationError} for the case where the API returned
416
- * a response that parsed as JSON but didn't match the expected schema
417
- * shape (e.g. 400 with a body that isn't a {@link BatchCreateOutput}).
418
- *
419
- * Distinct from {@link clientValidationError} so callers can `.code`-branch
420
- * between "I passed bad input locally" (`"client_validation"`) and "the
421
- * server returned a body I can't interpret" (`"unexpected_response"`).
422
- * Uses `status: 0` by default because a logical shape mismatch is not tied to
423
- * the HTTP classification. Callers that enforce an observed HTTP contract
424
- * pass that status explicitly.
425
- *
426
- * Threads `request_id` through to {@link QURLError.requestId} when the
427
- * server-side correlation ID is available — operators debugging
428
- * "unexpected response" tickets need it on the *error* path too, not
429
- * just on success/passthrough returns.
430
- */
431
- function unexpectedResponseError(detail, options = {}) {
432
- // Every detail passed here is an SDK-authored contract message whose dynamic
433
- // inputs were validated by the caller-side shape guards. Do not pass raw
434
- // server strings to this helper. Server diagnostics use boundedErrorSnippet.
435
- const safeRequestId = boundedErrorIdentifier(options.requestId);
436
- return new ValidationError({
437
- status: options.status ?? 0,
438
- code: ERROR_CODE_UNEXPECTED_RESPONSE,
439
- title: "Unexpected Response",
440
- detail,
441
- request_id: safeRequestId,
442
- });
443
- }
444
- function withErrorCause(error, cause) {
445
- // Match the native Error `cause` descriptor. In particular, do not leak a
446
- // transport object through Object.keys(), object spread, or JSON logging.
447
- Object.defineProperty(error, "cause", {
448
- value: cause,
449
- writable: true,
450
- configurable: true,
451
- });
452
- return error;
453
- }
454
- function isIndependentAbort(error, requestSignal) {
455
- return (error instanceof Error &&
456
- (error.name === "AbortError" || error.name === "TimeoutError") &&
457
- !requestSignal.aborted);
458
- }
459
- function isSdkTimeout(error, requestSignal) {
460
- return (error instanceof Error &&
461
- requestSignal.aborted &&
462
- (error.name === "TimeoutError" || error.name === "AbortError"));
463
- }
464
- /**
465
- * Normalize a server-provided problem title/detail into a single-line,
466
- * UTF-8-safe snippet. Error messages must stay bounded even when a proxy or
467
- * compromised upstream returns very large structured problem fields.
468
- */
469
- function normalizedErrorSnippet(value) {
470
- if (typeof value !== "string")
471
- return undefined;
472
- // Bound source work before the Unicode regular expressions allocate
473
- // normalized copies. The response body cap is much larger than one retained
474
- // diagnostic, so processing the full server string would waste memory.
475
- const normalized = value
476
- .slice(0, MAX_ERROR_SNIPPET_SOURCE_CODE_UNITS)
477
- // Strip control characters plus bidi override/isolate controls that can
478
- // visually reorder diagnostics. Preserve other formatting characters
479
- // such as ZWJ/ZWNJ, which are required by legitimate scripts and emoji.
480
- .replace(/[\p{Cc}\u061C\u200E\u200F\u202A-\u202E\u2066-\u2069]/gu, " ")
481
- .trim()
482
- .replace(/\s+/g, " ");
483
- return normalized === "" ? undefined : normalized;
484
- }
485
- function boundedErrorSnippet(value) {
486
- const sourceTruncated = typeof value === "string" && value.length > MAX_ERROR_SNIPPET_SOURCE_CODE_UNITS;
487
- const normalized = normalizedErrorSnippet(value);
488
- if (normalized === undefined)
489
- return undefined;
490
- const contentLimit = sourceTruncated
491
- ? MAX_ERROR_SNIPPET_BYTES - ERROR_SNIPPET_ELLIPSIS_BYTES
492
- : MAX_ERROR_SNIPPET_BYTES;
493
- // UTF-8 uses at most three bytes per UTF-16 code unit. Most diagnostics are
494
- // short ASCII, so avoid an allocation only to prove they fit.
495
- if (normalized.length * 3 <= contentLimit) {
496
- return sourceTruncated ? `${normalized}${ERROR_SNIPPET_ELLIPSIS}` : normalized;
497
- }
498
- const encoded = TEXT_ENCODER.encode(normalized);
499
- if (encoded.byteLength <= contentLimit) {
500
- return sourceTruncated ? `${normalized}${ERROR_SNIPPET_ELLIPSIS}` : normalized;
501
- }
502
- let end = MAX_ERROR_SNIPPET_BYTES - ERROR_SNIPPET_ELLIPSIS_BYTES;
503
- // Do not split a multi-byte UTF-8 sequence at the cap.
504
- while (end > 0 && (encoded[end] & 0xc0) === 0x80)
505
- end--;
506
- return `${TEXT_DECODER.decode(encoded.subarray(0, end))}${ERROR_SNIPPET_ELLIPSIS}`;
507
- }
508
- /** Keep server identifiers useful as exact machine-readable discriminants. */
509
- function boundedErrorIdentifier(value) {
510
- if (typeof value !== "string" || value.length > MAX_ERROR_SNIPPET_SOURCE_CODE_UNITS) {
511
- return undefined;
512
- }
513
- const normalized = normalizedErrorSnippet(value);
514
- if (normalized === undefined ||
515
- normalized !== value ||
516
- TEXT_ENCODER.encode(value).byteLength > MAX_ERROR_SNIPPET_BYTES) {
517
- return undefined;
518
- }
519
- return value;
520
- }
521
- /** Collect at most the first N enumerable own string keys without a full key-array copy. */
522
- function firstEnumerableOwnKeys(value, limit) {
523
- const keys = [];
524
- for (const key in value) {
525
- if (!Object.prototype.hasOwnProperty.call(value, key))
526
- continue;
527
- keys.push(key);
528
- if (keys.length === limit)
529
- break;
530
- }
531
- return keys;
532
- }
533
- /** Bound the structured per-field diagnostics exposed on ValidationError. */
534
- function boundedInvalidFields(value) {
535
- if (typeof value !== "object" || value === null || Array.isArray(value))
536
- return undefined;
537
- const entries = [];
538
- const retainedKeys = new Set();
539
- let retainedBytes = 0;
540
- // Bound inspected entries as well as retained entries. A hostile envelope
541
- // whose values all have the wrong type must not make diagnostics scan every
542
- // key in the response body.
543
- for (const rawKey of firstEnumerableOwnKeys(value, MAX_INVALID_FIELD_ENTRIES)) {
544
- const rawValue = value[rawKey];
545
- const key = boundedErrorSnippet(rawKey);
546
- const fieldDetail = boundedErrorSnippet(rawValue);
547
- if (key !== undefined && fieldDetail !== undefined && !retainedKeys.has(key)) {
548
- const entryBytes = TEXT_ENCODER.encode(key).byteLength + TEXT_ENCODER.encode(fieldDetail).byteLength;
549
- // A later, smaller entry can still fit even when this one cannot.
550
- if (retainedBytes + entryBytes > MAX_DIAGNOSTIC_COLLECTION_BYTES)
551
- continue;
552
- retainedKeys.add(key);
553
- entries.push([key, fieldDetail]);
554
- retainedBytes += entryBytes;
555
- }
556
- }
557
- return entries.length > 0 ? Object.fromEntries(entries) : undefined;
558
- }
559
- /** Bound server-controlled object keys before forwarding them to a debug sink. */
560
- function boundedObjectKeys(value) {
561
- // Stop before normalization so a hostile envelope cannot force work over
562
- // every enumerable own key. Some early keys may normalize away; bounded
563
- // diagnostics are more important here than filling every available slot.
564
- const retained = [];
565
- let retainedBytes = 0;
566
- for (const rawKey of firstEnumerableOwnKeys(value, MAX_DEBUG_BODY_KEYS)) {
567
- const key = boundedErrorSnippet(rawKey);
568
- if (key === undefined)
569
- continue;
570
- const keyBytes = TEXT_ENCODER.encode(key).byteLength;
571
- // A later, smaller key can still fit even when this one cannot.
572
- if (retainedBytes + keyBytes > MAX_DIAGNOSTIC_COLLECTION_BYTES)
573
- continue;
574
- retained.push(key);
575
- retainedBytes += keyBytes;
576
- }
577
- return retained;
578
- }
579
122
  async function cancelResponseBody(body) {
580
123
  if (!body)
581
124
  return;
@@ -586,24 +129,27 @@ async function cancelResponseBody(body) {
586
129
  // Best-effort resource cleanup must not replace the deterministic SDK error.
587
130
  }
588
131
  }
589
- function materializedResponseBodyExceedsLimit(value) {
590
- if (value.length > MAX_RESPONSE_BODY_BYTES)
132
+ function materializedResponseBodyExceedsLimit(value, limit) {
133
+ if (value.length > limit)
591
134
  return true;
592
135
  // Encoding creates another buffer. UTF-8 uses at most three bytes per
593
136
  // UTF-16 code unit, so shorter strings are provably safe without that copy.
594
- return (value.length * 3 > MAX_RESPONSE_BODY_BYTES &&
595
- TEXT_ENCODER.encode(value).byteLength > MAX_RESPONSE_BODY_BYTES);
137
+ return value.length * 3 > limit && TEXT_ENCODER.encode(value).byteLength > limit;
596
138
  }
597
- function contentLengthExceedsLimit(response) {
139
+ function contentLengthExceedsLimit(response, limit) {
598
140
  // 204/205 never carry content, and a 304 Content-Length describes the
599
141
  // selected representation rather than a response body. Do not reject those
600
142
  // statuses for metadata about bytes that fetch will not deliver.
601
143
  if (response.status === 204 || response.status === 205 || response.status === 304)
602
144
  return false;
145
+ // Encoded Content-Length describes wire bytes; fetch exposes decoded bytes.
146
+ const encoding = response.headers.get("content-encoding");
147
+ if (encoding && encoding.trim().toLowerCase() !== "identity")
148
+ return false;
603
149
  const value = response.headers.get("content-length");
604
150
  if (value === null || !/^\d+$/.test(value.trim()))
605
151
  return false;
606
- return Number(value) > MAX_RESPONSE_BODY_BYTES;
152
+ return Number(value) > limit;
607
153
  }
608
154
  /** Detect a followed redirect from fetch implementations that expose only the final URL. */
609
155
  function responseUrlState(responseUrl, requestUrl) {
@@ -631,16 +177,16 @@ function responseUrlState(responseUrl, requestUrl) {
631
177
  * optimization only; streaming byte accounting remains authoritative because
632
178
  * that header may be absent or false.
633
179
  */
634
- async function readBoundedResponseBody(response) {
180
+ async function readBoundedResponseBody(response, limit) {
635
181
  // `headers.get` is part of the documented injected-fetch contract. Detect a
636
182
  // malformed local shim before it can be mistaken for a retryable body-stream
637
183
  // failure and replay an otherwise deterministic GET programming error.
638
184
  if (typeof response.headers?.get !== "function") {
639
185
  throw new ResponseBodyMaterializationError("Response-like fetch must provide headers.get(name)");
640
186
  }
641
- if (contentLengthExceedsLimit(response)) {
187
+ if (contentLengthExceedsLimit(response, limit)) {
642
188
  await cancelResponseBody(response.body);
643
- throw new ResponseBodyTooLargeError();
189
+ throw new ResponseBodyTooLargeError(limit);
644
190
  }
645
191
  const body = response.body;
646
192
  if (body === null)
@@ -662,8 +208,8 @@ async function readBoundedResponseBody(response) {
662
208
  }
663
209
  }
664
210
  const text = typeof materializedText === "string" ? materializedText : "";
665
- if (materializedResponseBodyExceedsLimit(text)) {
666
- throw new ResponseBodyTooLargeError();
211
+ if (materializedResponseBodyExceedsLimit(text, limit)) {
212
+ throw new ResponseBodyTooLargeError(limit);
667
213
  }
668
214
  if (text !== "" ||
669
215
  response.status === 204 ||
@@ -691,8 +237,8 @@ async function readBoundedResponseBody(response) {
691
237
  }
692
238
  if (serialized === undefined)
693
239
  return "";
694
- if (materializedResponseBodyExceedsLimit(serialized)) {
695
- throw new ResponseBodyTooLargeError();
240
+ if (materializedResponseBodyExceedsLimit(serialized, limit)) {
241
+ throw new ResponseBodyTooLargeError(limit);
696
242
  }
697
243
  return serialized;
698
244
  }
@@ -708,14 +254,14 @@ async function readBoundedResponseBody(response) {
708
254
  throw new ResponseBodyMaterializationError("Response-like fetch body stream must yield Uint8Array chunks");
709
255
  }
710
256
  const nextTotal = total + value.byteLength;
711
- if (nextTotal > MAX_RESPONSE_BODY_BYTES) {
257
+ if (nextTotal > limit) {
712
258
  try {
713
259
  await reader.cancel();
714
260
  }
715
261
  catch {
716
262
  // Preserve the body-limit failure even if stream cancellation fails.
717
263
  }
718
- throw new ResponseBodyTooLargeError();
264
+ throw new ResponseBodyTooLargeError(limit);
719
265
  }
720
266
  chunks.push(value);
721
267
  total = nextTotal;
@@ -743,133 +289,6 @@ async function readBoundedResponseBody(response) {
743
289
  throw new InvalidResponseEncodingError();
744
290
  }
745
291
  }
746
- // ---- Spec-derived validation helpers ------------------------------------
747
- // These mirror constraints documented on each request schema in
748
- // `openapi.yaml` so obvious mistakes fail fast instead of round-tripping
749
- // to the API and coming back as a generic 400.
750
- const MAX_TARGET_URL = 2048;
751
- const MAX_LABEL = 500;
752
- const MAX_BATCH_ITEMS = 100;
753
- const MAX_DESCRIPTION = 500;
754
- const MAX_CUSTOM_DOMAIN = 253;
755
- const MAX_MAX_SESSIONS = 1000;
756
- const MAX_API_KEY_NAME = 100;
757
- const API_KEY_REQUEST_SCOPE_VALUES = [
758
- "qurl:read",
759
- "qurl:write",
760
- "qurl:resolve",
761
- "qurl:agent",
762
- ];
763
- assertExhaustive(true);
764
- const API_KEY_REQUEST_SCOPES = new Set(API_KEY_REQUEST_SCOPE_VALUES);
765
- /** Narrow a response scope before reusing it in a create/update request. */
766
- export function isApiKeyRequestScope(scope) {
767
- return API_KEY_REQUEST_SCOPES.has(scope);
768
- }
769
- const CREDENTIAL_TARGET_VALUES = [
770
- "agent",
771
- "connector",
772
- ];
773
- assertExhaustive(true);
774
- const CREDENTIAL_TARGETS = new Set(CREDENTIAL_TARGET_VALUES);
775
- const CREDENTIAL_CLAIM_TYPE_VALUES = [
776
- "connector",
777
- ];
778
- assertExhaustive(true);
779
- const CREDENTIAL_CLAIM_TYPES = new Set(CREDENTIAL_CLAIM_TYPE_VALUES);
780
- // Mirrors CreateApiKeyRequest.expires_in in the service OpenAPI. Composite
781
- // durations are deliberately invalid on this endpoint. The 24h cap permits
782
- // 1d, but no positive whole-week duration.
783
- const CREDENTIAL_DURATION_PATTERN = /^([0-9]+)([smhdw])$/;
784
- const CREDENTIAL_DURATION_MULTIPLIERS = {
785
- s: 1n,
786
- m: 60n,
787
- h: 3600n,
788
- d: 86400n,
789
- w: 604800n,
790
- };
791
- const MAX_ENROLLMENT_TOKEN_TTL_SECONDS = 86400n;
792
- const MAX_ALIAS = 64;
793
- const MAX_SLUG = 64;
794
- const MAX_TARGET_PATH = 2048;
795
- const MAX_TAGS = 10;
796
- const MAX_TAG_LENGTH = 50;
797
- const MAX_AUTO_PAGINATION_PAGES = 10_000;
798
- const MIN_DELEGATED_BATCH_IDEMPOTENCY_KEY = 32;
799
- const MAX_DELEGATED_MINT_CAPABILITY = 8192;
800
- const MAX_DELEGATED_ETAG = 96;
801
- const MAX_DELEGATED_BATCH_ITEMS = 100;
802
- const DELEGATED_BATCH_ID_PATTERN = /^dqb_[A-Za-z0-9_-]{22}$/;
803
- const DELEGATED_QURL_ID_PATTERN = /^q_[0-9a-f]{11}$/;
804
- const STRONG_ETAG_PATTERN = /^"[\x21\x23-\x7e]+"$/;
805
- const UTC_TIMESTAMP_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z$/;
806
- // Keep these identity contracts aligned with qurl-go and the public API.
807
- const CONNECTOR_SLUG_PATTERN = /^[a-z][a-z0-9-]{1,62}[a-z0-9]$/;
808
- // Current service schemas intentionally share this grammar, but keep the
809
- // mutable alias contract separate from the immutable lookup identity.
810
- const CONNECTOR_ALIAS_PATTERN = /^[a-z][a-z0-9-]{1,62}[a-z0-9]$/;
811
- const CONNECTOR_RESOURCE_ID_PATTERN = /^[A-Za-z0-9_-]{122}$/;
812
- const CONNECTOR_RESOURCE_ID_DER_PREFIX = "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE";
813
- const CONNECTOR_ROUTING_ID_PATTERN = /^c-[a-z2-7]{51}[aq]$/;
814
- // CreateQurlRequest.target_url pattern is loose (just a URI) but
815
- // UpdateQurlRequest.tags pattern is specific — enforce it here.
816
- const TAG_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9 _-]*$/;
817
- function requireMaxLength(value, field, max) {
818
- if (value === undefined)
819
- return;
820
- // Untyped-JS safety: a non-string `value` would silently pass since
821
- // `(42).length === undefined > max` is `false` and the bad value would
822
- // sail through to the wire. Match the typeof guard pattern used by
823
- // requireValidTags / requireValidTargetUrl.
824
- if (typeof value !== "string") {
825
- throw clientValidationError(`${field}: must be a string (got ${value === null ? "null" : typeof value})`);
826
- }
827
- if (value.length > max) {
828
- throw clientValidationError(`${field}: must be ${max} characters or fewer (got ${value.length})`);
829
- }
830
- }
831
- function requireMaxUtf8Bytes(value, field, max) {
832
- if (value === undefined)
833
- return;
834
- if (typeof value !== "string") {
835
- throw clientValidationError(`${field}: must be a string (got ${value === null ? "null" : typeof value})`);
836
- }
837
- const bytes = TEXT_ENCODER.encode(value).byteLength;
838
- if (bytes > max) {
839
- throw clientValidationError(`${field}: must be ${max} UTF-8 bytes or fewer (got ${bytes})`);
840
- }
841
- }
842
- function requireNonEmptyIfPresent(value, field) {
843
- // Reject `""` so uninitialized form state doesn't reach the wire.
844
- if (value === "") {
845
- throw clientValidationError(`${field}: must not be an empty string`);
846
- }
847
- }
848
- function requireMaxSessionsInRange(value, field = "max_sessions") {
849
- if (value === undefined)
850
- return;
851
- if (!Number.isInteger(value) || value < 0 || value > MAX_MAX_SESSIONS) {
852
- throw clientValidationError(`${field}: must be an integer between 0 and ${MAX_MAX_SESSIONS} (got ${value})`);
853
- }
854
- }
855
- function requireBooleanIfPresent(value, field) {
856
- if (value === undefined)
857
- return;
858
- if (typeof value !== "boolean") {
859
- throw clientValidationError(`${field}: must be a boolean (got ${describeShape(value)})`);
860
- }
861
- }
862
- function requireListLimitInRange(value, methodName) {
863
- if (value === undefined || value === null)
864
- return;
865
- // All current paginated OpenAPI query params share 1..100. If a future
866
- // endpoint diverges, add a per-endpoint bound rather than changing this
867
- // shared guard silently.
868
- if (typeof value !== "number" || !Number.isInteger(value) || value < 1 || value > 100) {
869
- const rendered = typeof value === "string" ? JSON.stringify(value) : String(value);
870
- throw clientValidationError(`${methodName}: limit: must be an integer between 1 and 100 (got ${rendered})`);
871
- }
872
- }
873
292
  function appendQuery(path, input, keys, methodName) {
874
293
  validateQueryInput(input, keys, methodName);
875
294
  const params = new URLSearchParams();
@@ -905,1330 +324,17 @@ function validateListAllInput(input, keys, methodName) {
905
324
  // each request revalidates the threaded cursor/filter set before dispatch.
906
325
  validateQueryInput(input, keys, methodName);
907
326
  }
327
+ function withEnvelopeMetadata(value, meta) {
328
+ return withResponseMetadata(value, boundedErrorIdentifier(meta?.request_id));
329
+ }
908
330
  function pageFromMeta(out, meta) {
909
- return {
331
+ return withEnvelopeMetadata({
910
332
  ...out,
911
333
  next_cursor: meta?.next_cursor,
912
334
  has_more: meta?.has_more ?? Boolean(meta?.next_cursor),
913
335
  request_id: meta?.request_id,
914
336
  page_size: meta?.page_size,
915
- };
916
- }
917
- // Best-effort defence-in-depth against today's access-token grammar. The
918
- // service remains authoritative because resource IDs and credentials are both
919
- // opaque and future credential formats may not retain the `at_` prefix. The
920
- // delimiter requirement cannot collide with current base64url public keys,
921
- // standard-base64 keys, or base32 CRIDs.
922
- const PATH_EMBEDDED_ACCESS_TOKEN_RE = /[?&#=:\s]at_/i;
923
- const PATH_PREFIXED_ACCESS_TOKEN_RE = /\/at_/i;
924
- const QURL_LINK_URL_RE = /^(?:https?:\/\/)?(?:[a-z0-9-]+\.)*qurl\.link(?::\d{1,5})?(?:\/|[?#])/i;
925
- // ponytail: today's 22-character token suffix only improves URL-paste diagnostics;
926
- // other lengths are still rejected as URLs without echoing the input.
927
- const URL_EMBEDDED_ACCESS_TOKEN_RE = /[?&#=:/]at_[a-z0-9_-]{22}(?:$|[?&#:/])/i;
928
- // A host without a slash can be a legitimate domain identifier. Require the
929
- // path boundary so shared validation does not break the domain namespace.
930
- const PATH_SCHEMELESS_URL_RE = /^[a-z0-9-]+(?:\.[a-z0-9-]+)+(?::\d{1,5})?\//i;
931
- const QURL_DISPLAY_ID_PREFIX_RE = /^q_/i;
932
- const MAX_PATH_ID_DECODE_PASSES = 3;
933
- const MAX_PATH_ID_LENGTH = 4096;
934
- function isUrlLikePathId(value) {
935
- return /^https?:\/\//i.test(value) || PATH_SCHEMELESS_URL_RE.test(value);
936
- }
937
- const REVOKE_INDIVIDUAL_QURL_RECOVERY = "revoke individual tokens with revokeResourceQurl(crid, qurlId)";
938
- const DELETE_RESOURCE_RECOVERY = "pass a CRID returned by the API; to revoke one qURL, call revokeResourceQurl(crid, qurlId)";
939
- const RESOURCE_OR_QURL_ID_PATH_OPTIONS = {
940
- requireCridUnlessQurlDisplayId: true,
941
- };
942
- const QURL_ID_PATH_OPTIONS = {
943
- rejectBareAccessToken: true,
944
- accessTokenRecovery: "pass the qURL display ID, not its access token",
945
- };
946
- /**
947
- * Validates that a path-parameter argument is a non-empty string. Some callers
948
- * pass resource/qURL display IDs, while newer endpoints also pass domains,
949
- * session IDs, webhook IDs, and API-key IDs, so this intentionally enforces
950
- * only basic shape plus transport-safety rules and leaves endpoint-specific
951
- * identifier grammar to the service.
952
- */
953
- function validatePathId(id, method, field = "id", options = {}) {
954
- if (options.requireCridUnlessQurlDisplayId && typeof id === "string" && !id.startsWith("q_")) {
955
- requireResourceCrid(id, method, field);
956
- return;
957
- }
958
- // `.trim()` catches whitespace-only and padded IDs before they round-trip as
959
- // `%20...%20` paths that the server can only reject with a less useful 404.
960
- // The pre-flight error is more actionable than the 404.
961
- if (typeof id !== "string") {
962
- const observedType = id === null ? "null" : typeof id;
963
- throw clientValidationError(`${method}: ${field} is required (got ${observedType})`);
964
- }
965
- if (id.trim() === "") {
966
- throw clientValidationError(`${method}: ${field} is required`);
967
- }
968
- if (id.trim() !== id) {
969
- throw clientValidationError(`${method}: ${field} must not include leading or trailing whitespace`);
970
- }
971
- if (id.length > MAX_PATH_ID_LENGTH) {
972
- throw clientValidationError(`${method}: ${field} must be ${MAX_PATH_ID_LENGTH} characters or fewer`);
973
- }
974
- const decodedIds = decodedPathIdForms(id);
975
- // URL resolvers and intermediaries can normalize raw or pre-encoded dot
976
- // segments before routing. Reject every decoded form we inspected so an
977
- // item operation cannot be retargeted to a collection endpoint. Embedded
978
- // `a/../b` remains an opaque single segment because the later URL builder
979
- // percent-encodes its slash.
980
- if (decodedIds.some((value) => value === "." || value === "..")) {
981
- throw clientValidationError(`${method}: ${field} is an invalid URL path segment`);
982
- }
983
- // `/` is deliberately outside the general delimiter class so a URL path
984
- // such as `/at_a_glance` is not misread as a credential. For non-URL input,
985
- // `/at_` remains a strong pasted-path signal. Fragment, query, header, and
986
- // pasted `Bearer at_...` / `token:at_...` forms are also strong signals.
987
- // Bare `at_...` values are rejected only for resource/qURL identifiers:
988
- // those current formats have fixed non-colliding prefixes, while domain,
989
- // webhook, session, and API-key IDs are deliberately opaque/open contracts.
990
- // Full URLs and delimiter/path-prefixed credentials remain rejected in
991
- // every namespace.
992
- const containsUrl = decodedIds.some(isUrlLikePathId);
993
- const containsAccessToken = decodedIds.some((value) => {
994
- const valueIsUrl = isUrlLikePathId(value);
995
- return ((QURL_LINK_URL_RE.test(value) &&
996
- (PATH_EMBEDDED_ACCESS_TOKEN_RE.test(value) || PATH_PREFIXED_ACCESS_TOKEN_RE.test(value))) ||
997
- (valueIsUrl
998
- ? URL_EMBEDDED_ACCESS_TOKEN_RE.test(value)
999
- : PATH_EMBEDDED_ACCESS_TOKEN_RE.test(value) || PATH_PREFIXED_ACCESS_TOKEN_RE.test(value)) ||
1000
- (options.rejectBareAccessToken === true && value.slice(0, 3).toLowerCase() === "at_"));
1001
- });
1002
- if (containsAccessToken) {
1003
- const recovery = options.accessTokenRecovery ?? "pass the identifier returned by the API";
1004
- throw clientValidationError(`${method}: ${field} must not contain an access token; ${recovery}`);
1005
- }
1006
- // A full target/qURL is a common argument mix-up, but it is distinct from
1007
- // passing an access-token credential. Diagnose it accurately without
1008
- // echoing the URL, which may itself contain sensitive query parameters.
1009
- if (containsUrl) {
1010
- throw clientValidationError(`${method}: ${field} must be an identifier, not a URL`);
1011
- }
1012
- if (options.rejectQurlDisplayId === true &&
1013
- decodedIds.some((value) => QURL_DISPLAY_ID_PREFIX_RE.test(value))) {
1014
- throw clientValidationError(`${method}: ${field} must not be a qURL display ID; ${REVOKE_INDIVIDUAL_QURL_RECOVERY}`);
1015
- }
1016
- }
1017
- function decodedPathIdForms(id) {
1018
- // Probe repeatedly encoded input too. Callers sometimes pre-encode a copied
1019
- // link before passing it through a URL builder that encodes it again. Three
1020
- // passes cover the common accidental cases without allowing hostile input
1021
- // to create unbounded work. More deeply encoded or malformed-percent input
1022
- // deliberately fails open to the authoritative service after safe segment
1023
- // encoding; this heuristic is not an identifier parser.
1024
- const decodedIds = [id];
1025
- let decodedId = id;
1026
- for (let pass = 0; pass < MAX_PATH_ID_DECODE_PASSES; pass += 1) {
1027
- try {
1028
- const next = decodeURIComponent(decodedId);
1029
- if (next === decodedId)
1030
- break;
1031
- decodedIds.push(next);
1032
- decodedId = next;
1033
- }
1034
- catch {
1035
- // Malformed percent escapes remain opaque and are left to the service.
1036
- break;
1037
- }
1038
- }
1039
- return decodedIds;
1040
- }
1041
- function requireConnectorSlug(slug, method) {
1042
- if (typeof slug !== "string" || !CONNECTOR_SLUG_PATTERN.test(slug)) {
1043
- throw clientValidationError(`${method}: slug must be 3-64 lowercase alphanumeric or hyphen characters, start with a letter, and end alphanumeric`);
1044
- }
1045
- }
1046
- function classifyConnectorMutationFailure(operation, error) {
1047
- if (!(error instanceof QURLError)) {
1048
- throw new ConnectorResourceOutcomeUnknownError(new RuntimeError("Unexpected failure after Connector mutation dispatch", { cause: error }));
1049
- }
1050
- // Most authoritative 4xx responses prove that the mutation was rejected.
1051
- // bootstrap_key_consumed is the exception: the service returns it from the
1052
- // consume phase after the resource may have been bound or found.
1053
- // Callers perform argument/runtime preflight outside their mutation try
1054
- // blocks; every other surfaced failure therefore follows dispatch or a
1055
- // nominal success whose resource contract could not be consumed, so callers
1056
- // must reconcile before retrying. Ensure's exact 201 + valid resource but
1057
- // missing found_existing is handled after this classifier: the row proves
1058
- // the selected resource, while only its required metadata is unavailable.
1059
- if (operation === "ensure" && error.status === 409 && error.code === "bootstrap_key_consumed") {
1060
- throw new ConnectorResourceOutcomeUnknownError(error);
1061
- }
1062
- if (error.status >= 400 && error.status < 500) {
1063
- throw error;
1064
- }
1065
- throw new ConnectorResourceOutcomeUnknownError(error);
1066
- }
1067
- function decodeCanonicalBase64Url(value) {
1068
- if (!CONNECTOR_RESOURCE_ID_PATTERN.test(value) ||
1069
- !value.startsWith(CONNECTOR_RESOURCE_ID_DER_PREFIX)) {
1070
- return undefined;
1071
- }
1072
- try {
1073
- const padded = value.replace(/-/g, "+").replace(/_/g, "/") + "==";
1074
- const binary = atob(padded);
1075
- const bytes = Uint8Array.from(binary, (character) => character.charCodeAt(0));
1076
- const canonical = btoa(String.fromCharCode(...bytes))
1077
- .replace(/\+/g, "-")
1078
- .replace(/\//g, "_")
1079
- .replace(/=+$/, "");
1080
- return canonical === value ? bytes : undefined;
1081
- }
1082
- catch {
1083
- return undefined;
1084
- }
1085
- }
1086
- function requireConnectorSubtleCrypto(method) {
1087
- if (!globalThis.crypto?.subtle) {
1088
- throw new RuntimeError(`${method}: requires the Web Crypto SubtleCrypto API`);
1089
- }
1090
- }
1091
- function requireResourceCrid(crid, method, field = "id") {
1092
- if (parseCrid(crid))
1093
- return;
1094
- // Keep actionable paste errors without inspecting valid CRIDs twice.
1095
- validatePathId(crid, method, field, {
1096
- rejectBareAccessToken: true,
1097
- rejectQurlDisplayId: true,
1098
- accessTokenRecovery: method === "delete" || method === "deleteResource"
1099
- ? DELETE_RESOURCE_RECOVERY
1100
- : "pass a CRID returned by the API",
1101
- });
1102
- throw clientValidationError(`${method}: requires a valid CRID`);
1103
- }
1104
- async function importsAsConnectorPublicKey(der) {
1105
- try {
1106
- const keyData = new ArrayBuffer(der.byteLength);
1107
- new Uint8Array(keyData).set(der);
1108
- await globalThis.crypto.subtle.importKey("spki", keyData, { name: "ECDSA", namedCurve: "P-256" }, false, ["verify"]);
1109
- return true;
1110
- }
1111
- catch {
1112
- return false;
1113
- }
1114
- }
1115
- function containsControlCharacter(value) {
1116
- return Array.from(value).some((character) => {
1117
- const codePoint = character.codePointAt(0);
1118
- return codePoint <= 0x1f || (codePoint >= 0x7f && codePoint <= 0x9f);
1119
- });
1120
- }
1121
- async function verifyResourceIdentity(resource, method, expectedCrid) {
1122
- if (!resource || typeof resource.resource_id !== "string") {
1123
- throw unexpectedResponseError(`${method}: response has missing or invalid resource_id`);
1124
- }
1125
- const resourceKey = decodeCanonicalBase64Url(resource.resource_id);
1126
- requireConnectorSubtleCrypto(method);
1127
- if (!resourceKey || !(await importsAsConnectorPublicKey(resourceKey))) {
1128
- throw unexpectedResponseError(`${method}: response has missing or invalid resource_id`);
1129
- }
1130
- if (expectedCrid !== undefined && resource.crid !== expectedCrid) {
1131
- throw unexpectedResponseError(`${method}: response crid does not match the request`);
1132
- }
1133
- if (!(await cridKeyMatches(resource.crid, resourceKey))) {
1134
- throw unexpectedResponseError(`${method}: response has missing, invalid, or public-key-mismatched crid`);
1135
- }
1136
- }
1137
- async function parseConnectorResource(client, value, method, expectation = {}) {
1138
- if (typeof value !== "object" || value === null || Array.isArray(value)) {
1139
- throw unexpectedResponseError(`${method}: response is missing connector resource data`);
1140
- }
1141
- const resource = value;
1142
- await verifyResourceIdentity(resource, method, expectation.crid);
1143
- if (typeof resource.connector_routing_id !== "string" ||
1144
- !CONNECTOR_ROUTING_ID_PATTERN.test(resource.connector_routing_id)) {
1145
- throw unexpectedResponseError(`${method}: response has missing or invalid connector_routing_id`);
1146
- }
1147
- if (typeof resource.knock_resource_id !== "string" ||
1148
- resource.knock_resource_id.trim() === "" ||
1149
- resource.knock_resource_id.trim() !== resource.knock_resource_id ||
1150
- containsControlCharacter(resource.knock_resource_id)) {
1151
- throw unexpectedResponseError(`${method}: response has missing or invalid knock_resource_id`);
1152
- }
1153
- if (resource.resource_id === resource.knock_resource_id ||
1154
- resource.connector_routing_id === resource.knock_resource_id) {
1155
- throw unexpectedResponseError(`${method}: response cross-wires resource identity, routing, and admission values`);
1156
- }
1157
- if (resource.type !== "tunnel") {
1158
- throw unexpectedResponseError(`${method}: response resource type is not tunnel`);
1159
- }
1160
- if (typeof resource.slug !== "string" || !CONNECTOR_SLUG_PATTERN.test(resource.slug)) {
1161
- throw unexpectedResponseError(`${method}: response has missing or invalid slug`);
1162
- }
1163
- if (expectation.slug !== undefined && resource.slug !== expectation.slug) {
1164
- throw unexpectedResponseError(`${method}: response slug does not match the request`);
1165
- }
1166
- if (resource.alias !== undefined &&
1167
- resource.alias !== null &&
1168
- (typeof resource.alias !== "string" || !CONNECTOR_ALIAS_PATTERN.test(resource.alias))) {
1169
- // The public API intentionally gives mutable aliases and immutable slugs the
1170
- // same canonical wire grammar (domain aliasPattern/slugPattern). Keeping
1171
- // response validation aligned avoids accepting a row the service itself
1172
- // could not create or update.
1173
- throw unexpectedResponseError(`${method}: response has invalid alias`);
1174
- }
1175
- if (resource.status === "revoked") {
1176
- if (expectation.revokedIsLifecycleError !== true) {
1177
- throw unexpectedResponseError(`${method}: active-only operation returned a revoked connector resource`);
1178
- }
1179
- throw new QURLError({
1180
- status: 0,
1181
- code: ERROR_CODE_CONNECTOR_RESOURCE_REVOKED,
1182
- title: "Connector Resource Revoked",
1183
- detail: `${method}: qURL Connector resource is revoked`,
1184
- });
1185
- }
1186
- if (resource.status !== "active") {
1187
- throw unexpectedResponseError(`${method}: response has invalid resource status`);
1188
- }
1189
- return new ConnectorResource(client, resource, CONNECTOR_RESOURCE_CONSTRUCTOR_TOKEN);
1190
- }
1191
- function requireValidTags(tags) {
1192
- // null tolerance for untyped-JS callers — treated as "don't touch."
1193
- // Duplicates pass through to the server (authoritative on dedupe policy).
1194
- if (tags === undefined || tags === null)
1195
- return;
1196
- if (!Array.isArray(tags)) {
1197
- throw clientValidationError(`tags: must be an array of strings (got ${typeof tags})`);
1198
- }
1199
- if (tags.length > MAX_TAGS) {
1200
- throw clientValidationError(`tags: max ${MAX_TAGS} items allowed (got ${tags.length})`);
1201
- }
1202
- const errors = [];
1203
- for (let i = 0; i < tags.length; i++) {
1204
- const tag = tags[i];
1205
- // Per-element type guard for untyped-JS callers. Without this, a
1206
- // non-string element would silently pass (`.length` on a number is
1207
- // `undefined`; `TAG_PATTERN.test(42)` coerces to `"42"`) or TypeError
1208
- // (`null.length`).
1209
- if (typeof tag !== "string") {
1210
- errors.push(`tags[${i}]: must be a string (got ${tag === null ? "null" : typeof tag})`);
1211
- continue;
1212
- }
1213
- if (tag.length < 1 || tag.length > MAX_TAG_LENGTH) {
1214
- errors.push(`tags[${i}]: must be 1-${MAX_TAG_LENGTH} characters (got ${tag.length})`);
1215
- continue;
1216
- }
1217
- if (!TAG_PATTERN.test(tag)) {
1218
- errors.push(`tags[${i}]: must start with an alphanumeric and contain only letters, numbers, spaces, underscores, or hyphens`);
1219
- }
1220
- }
1221
- if (errors.length > 0) {
1222
- throw clientValidationError(errors.join("; "));
1223
- }
1224
- }
1225
- function describeShape(value) {
1226
- if (value === null)
1227
- return "null";
1228
- if (Array.isArray(value))
1229
- return "array";
1230
- return typeof value;
1231
- }
1232
- const ACCESS_POLICY_LIST_FIELDS = [
1233
- "ip_allowlist",
1234
- "ip_denylist",
1235
- "geo_allowlist",
1236
- "geo_denylist",
1237
- ];
1238
- const ACCESS_POLICY_STRING_FIELDS = [
1239
- "user_agent_allow_regex",
1240
- "user_agent_deny_regex",
1241
- ];
1242
- const AI_AGENT_POLICY_LIST_FIELDS = [
1243
- "deny_categories",
1244
- "allow_categories",
1245
- ];
1246
- function requireValidAccessPolicy(policy) {
1247
- // null/undefined → "don't touch". Server is authoritative on CIDR /
1248
- // ISO-3166 / regex grammar; this guard catches only the shape errors
1249
- // an untyped-JS caller is likely to make (e.g. `geo_allowlist: "US"`
1250
- // instead of `["US"]`) so they get a structured ValidationError
1251
- // instead of a server 400 they have to debug.
1252
- if (policy === undefined || policy === null)
1253
- return;
1254
- if (typeof policy !== "object" || Array.isArray(policy)) {
1255
- throw clientValidationError(`access_policy: must be an object (got ${describeShape(policy)})`);
1256
- }
1257
- for (const field of ACCESS_POLICY_LIST_FIELDS) {
1258
- const value = policy[field];
1259
- if (value === undefined)
1260
- continue;
1261
- if (!Array.isArray(value)) {
1262
- throw clientValidationError(`access_policy.${field}: must be an array (got ${describeShape(value)})`);
1263
- }
1264
- }
1265
- for (const field of ACCESS_POLICY_STRING_FIELDS) {
1266
- const value = policy[field];
1267
- if (value === undefined)
1268
- continue;
1269
- if (typeof value !== "string") {
1270
- throw clientValidationError(`access_policy.${field}: must be a string (got ${describeShape(value)})`);
1271
- }
1272
- }
1273
- const aip = policy.ai_agent_policy;
1274
- if (aip !== undefined) {
1275
- if (aip === null || typeof aip !== "object" || Array.isArray(aip)) {
1276
- throw clientValidationError(`access_policy.ai_agent_policy: must be an object (got ${describeShape(aip)})`);
1277
- }
1278
- if (aip.block_all !== undefined && typeof aip.block_all !== "boolean") {
1279
- throw clientValidationError(`access_policy.ai_agent_policy.block_all: must be a boolean (got ${describeShape(aip.block_all)})`);
1280
- }
1281
- for (const field of AI_AGENT_POLICY_LIST_FIELDS) {
1282
- const value = aip[field];
1283
- if (value === undefined)
1284
- continue;
1285
- if (!Array.isArray(value)) {
1286
- throw clientValidationError(`access_policy.ai_agent_policy.${field}: must be an array (got ${describeShape(value)})`);
1287
- }
1288
- }
1289
- }
1290
- }
1291
- function validateCreateDelegatedQurlBatchInput(input) {
1292
- if (typeof input !== "object" || input === null || Array.isArray(input)) {
1293
- throw clientValidationError(`createDelegatedQurlBatch: input must be an object (got ${describeShape(input)})`);
1294
- }
1295
- const typed = input;
1296
- requireNoUnknownFields(typed, DELEGATED_BATCH_INPUT_KEYS, "createDelegatedQurlBatch");
1297
- if (typeof typed.mint_capability !== "string" ||
1298
- typed.mint_capability.length < 1 ||
1299
- typed.mint_capability.length > MAX_DELEGATED_MINT_CAPABILITY) {
1300
- throw clientValidationError(`createDelegatedQurlBatch: mint_capability must be a string of 1-${MAX_DELEGATED_MINT_CAPABILITY} characters`);
1301
- }
1302
- if (!Array.isArray(typed.grants) ||
1303
- typed.grants.length < 1 ||
1304
- typed.grants.length > MAX_DELEGATED_BATCH_ITEMS) {
1305
- throw clientValidationError(`createDelegatedQurlBatch: grants must contain 1-${MAX_DELEGATED_BATCH_ITEMS} items`);
1306
- }
1307
- for (const [index, grant] of typed.grants.entries()) {
1308
- const field = `createDelegatedQurlBatch: grants[${index}]`;
1309
- if (typeof grant !== "object" || grant === null || Array.isArray(grant)) {
1310
- throw clientValidationError(`${field}: must be an object (got ${describeShape(grant)})`);
1311
- }
1312
- requireNoUnknownFields(grant, DELEGATED_BATCH_GRANT_KEYS, field);
1313
- requireMaxUtf8Bytes(grant.label, `${field}.label`, MAX_LABEL);
1314
- requireBooleanIfPresent(grant.one_time_use, `${field}.one_time_use`);
1315
- requireMaxSessionsInRange(grant.max_sessions, `${field}.max_sessions`);
1316
- for (const durationField of ["expires_in", "session_duration"]) {
1317
- const value = grant[durationField];
1318
- if (value !== undefined && (typeof value !== "string" || value.length === 0)) {
1319
- throw clientValidationError(`${field}.${durationField}: must be a non-empty string`);
1320
- }
1321
- }
1322
- if (grant.access_policy !== undefined) {
1323
- requireValidAccessPolicy(grant.access_policy);
1324
- }
1325
- }
1326
- }
1327
- function validateDelegatedBatchCreateOptions(options) {
1328
- if (typeof options !== "object" || options === null || Array.isArray(options)) {
1329
- throw clientValidationError("createDelegatedQurlBatch: options are required");
1330
- }
1331
- requireNoUnknownFields(options, DELEGATED_BATCH_CREATE_OPTION_KEYS, "createDelegatedQurlBatch options");
1332
- const key = options.idempotencyKey;
1333
- if (typeof key !== "string" ||
1334
- key.length < MIN_DELEGATED_BATCH_IDEMPOTENCY_KEY ||
1335
- key.length > MAX_IDEMPOTENCY_KEY ||
1336
- !/^[!-~]+$/.test(key)) {
1337
- throw clientValidationError(`createDelegatedQurlBatch: idempotencyKey must contain ${MIN_DELEGATED_BATCH_IDEMPOTENCY_KEY}-${MAX_IDEMPOTENCY_KEY} visible ASCII characters`);
1338
- }
1339
- }
1340
- function validateGetDelegatedQurlBatchOptions(options) {
1341
- if (options === undefined)
1342
- return;
1343
- if (typeof options !== "object" || options === null || Array.isArray(options)) {
1344
- throw clientValidationError(`getDelegatedQurlBatch: options must be an object (got ${describeShape(options)})`);
1345
- }
1346
- requireNoUnknownFields(options, DELEGATED_BATCH_GET_OPTION_KEYS, "getDelegatedQurlBatch options");
1347
- const etag = options.etag;
1348
- if (etag !== undefined &&
1349
- (typeof etag !== "string" ||
1350
- etag.length > MAX_DELEGATED_ETAG ||
1351
- !STRONG_ETAG_PATTERN.test(etag))) {
1352
- throw clientValidationError("getDelegatedQurlBatch: etag must be a valid strong ETag");
1353
- }
1354
- }
1355
- function delegatedResponseError(status, detail, requestId) {
1356
- return unexpectedResponseError(`Delegated qURL batch ${detail}`, { status, requestId });
1357
- }
1358
- function delegatedResponseObject(value, status, label, requestId) {
1359
- if (typeof value !== "object" || value === null || Array.isArray(value)) {
1360
- throw delegatedResponseError(status, `${label} is not an object`, requestId);
1361
- }
1362
- return value;
1363
- }
1364
- function delegatedEnvelope(envelope, status) {
1365
- if (typeof envelope.meta !== "object" || envelope.meta === null || Array.isArray(envelope.meta)) {
1366
- throw delegatedResponseError(status, "response meta is not an object");
1367
- }
1368
- const meta = envelope.meta;
1369
- if (typeof meta.request_id !== "string") {
1370
- throw delegatedResponseError(status, "response meta has invalid request_id");
1371
- }
1372
- const data = delegatedResponseObject(envelope.data, status, "response data", meta.request_id);
1373
- return { data, requestId: meta.request_id };
1374
- }
1375
- function requiredDelegatedHeader(envelope, name, status, requestId) {
1376
- const headers = envelope.__http_headers;
1377
- if (!headers || typeof headers.get !== "function") {
1378
- throw delegatedResponseError(status, "response headers are unavailable", requestId);
1379
- }
1380
- const value = headers.get(name);
1381
- if (value === null) {
1382
- throw delegatedResponseError(status, `response is missing ${name}`, requestId);
1383
- }
1384
- return value;
1385
- }
1386
- function hasDelegatedNoStore(value) {
1387
- if (value === null)
1388
- return false;
1389
- const directives = new Set(value.split(",").map((directive) => directive.trim().split("=", 1)[0].toLowerCase()));
1390
- return directives.has("private") && directives.has("no-store");
1391
- }
1392
- function delegatedResponseHeaders(envelope, status, requestId) {
1393
- const cacheControl = envelope.__http_headers?.get("Cache-Control") ?? null;
1394
- if (cacheControl !== null) {
1395
- if (!hasDelegatedNoStore(cacheControl)) {
1396
- throw delegatedResponseError(status, "response has invalid Cache-Control", requestId);
1397
- }
1398
- }
1399
- else if (status !== 304) {
1400
- requiredDelegatedHeader(envelope, "Cache-Control", status, requestId);
1401
- }
1402
- const etag = envelope.__http_headers?.get("ETag") ?? undefined;
1403
- if (etag !== undefined && (etag.length > MAX_DELEGATED_ETAG || !STRONG_ETAG_PATTERN.test(etag))) {
1404
- throw delegatedResponseError(status, "response has invalid strong ETag", requestId);
1405
- }
1406
- const rawRetryAfter = envelope.__http_headers?.get("Retry-After") ?? "";
1407
- const trimmed = rawRetryAfter.trim();
1408
- const seconds = /^\d+$/.test(trimmed)
1409
- ? Number(trimmed)
1410
- : /^[A-Z][a-z]{2}, \d{2} [A-Z][a-z]{2} \d{4} \d{2}:\d{2}:\d{2} GMT$/.test(trimmed)
1411
- ? Math.ceil((Date.parse(trimmed) - Date.now()) / 1000)
1412
- : NaN;
1413
- const retryAfter = Number.isSafeInteger(seconds) && seconds > 0 && seconds <= RETRY_AFTER_PARSE_LIMIT_S
1414
- ? seconds
1415
- : undefined;
1416
- return { etag, retryAfter };
1417
- }
1418
- function isUtcTimestamp(value) {
1419
- return (typeof value === "string" &&
1420
- UTC_TIMESTAMP_PATTERN.test(value) &&
1421
- Number.isFinite(Date.parse(value)));
1422
- }
1423
- function delegatedBatchBase(data, status, requestId, expectedBatchId) {
1424
- if (typeof data.batch_id !== "string" ||
1425
- !DELEGATED_BATCH_ID_PATTERN.test(data.batch_id) ||
1426
- (expectedBatchId !== undefined && data.batch_id !== expectedBatchId)) {
1427
- throw delegatedResponseError(status, "response has invalid batch_id", requestId);
1428
- }
1429
- if (!Number.isInteger(data.item_count) ||
1430
- Number(data.item_count) < 1 ||
1431
- Number(data.item_count) > MAX_DELEGATED_BATCH_ITEMS) {
1432
- throw delegatedResponseError(status, "response has invalid item_count", requestId);
1433
- }
1434
- if (!isUtcTimestamp(data.submitted_at)) {
1435
- throw delegatedResponseError(status, "response has invalid submitted_at", requestId);
1436
- }
1437
- return {
1438
- batchId: data.batch_id,
1439
- itemCount: Number(data.item_count),
1440
- submittedAt: data.submitted_at,
1441
- };
1442
- }
1443
- function validateDelegatedLocation(value, baseUrl, batchId, status, requestId) {
1444
- let location;
1445
- try {
1446
- const base = new URL(`${baseUrl}/`);
1447
- const relative = value.startsWith("/v1/delegated-qurl-batches/")
1448
- ? `${base.pathname.replace(/\/$/, "")}${value}`
1449
- : value;
1450
- location = new URL(relative, base);
1451
- }
1452
- catch {
1453
- throw delegatedResponseError(status, "response has invalid Location", requestId);
1454
- }
1455
- const expected = new URL(`${baseUrl}/v1/delegated-qurl-batches/${batchId}`);
1456
- if (location.username !== "" ||
1457
- location.password !== "" ||
1458
- location.search !== "" ||
1459
- location.hash !== "" ||
1460
- location.href !== expected.href) {
1461
- throw delegatedResponseError(status, "response has invalid Location", requestId);
1462
- }
1463
- return location.href;
1464
- }
1465
- function validateDelegatedQurlLink(value) {
1466
- if (typeof value !== "string")
1467
- return false;
1468
- try {
1469
- const link = new URL(value);
1470
- return (link.protocol === "https:" &&
1471
- link.username === "" &&
1472
- link.password === "" &&
1473
- link.hash.length > 1);
1474
- }
1475
- catch {
1476
- return false;
1477
- }
1478
- }
1479
- function validateDelegatedQurl(value, expectedQurlId, status, requestId) {
1480
- const data = delegatedResponseObject(value, status, "response data", requestId);
1481
- if (data.qurl_id !== expectedQurlId) {
1482
- throw delegatedResponseError(status, "response has invalid qurl_id", requestId);
1483
- }
1484
- if (data.status !== "active" &&
1485
- data.status !== "consumed" &&
1486
- data.status !== "expired" &&
1487
- data.status !== "revoked") {
1488
- throw delegatedResponseError(status, "response has invalid status", requestId);
1489
- }
1490
- if (!isUtcTimestamp(data.expires_at) ||
1491
- typeof data.one_time_use !== "boolean" ||
1492
- !Number.isInteger(data.max_sessions) ||
1493
- Number(data.max_sessions) < 0 ||
1494
- !Number.isInteger(data.session_duration) ||
1495
- Number(data.session_duration) <= 0 ||
1496
- (data.label !== undefined && typeof data.label !== "string") ||
1497
- (data.access_policy !== undefined &&
1498
- (typeof data.access_policy !== "object" ||
1499
- data.access_policy === null ||
1500
- Array.isArray(data.access_policy)))) {
1501
- throw delegatedResponseError(status, "response has invalid qURL metadata", requestId);
1502
- }
1503
- return { ...data, request_id: requestId };
1504
- }
1505
- function validateDelegatedBatchResults(value, itemCount, batchStatus, status, requestId) {
1506
- if (!Array.isArray(value) || value.length !== itemCount) {
1507
- throw delegatedResponseError(status, "terminal response has invalid results", requestId);
1508
- }
1509
- let succeeded = 0;
1510
- const qurlIds = new Set();
1511
- const links = new Set();
1512
- const results = value.map((raw, index) => {
1513
- const item = delegatedResponseObject(raw, status, `result ${index}`, requestId);
1514
- if (item.index !== index) {
1515
- throw delegatedResponseError(status, `result ${index} has invalid index`, requestId);
1516
- }
1517
- if (item.status === "succeeded") {
1518
- if (item.error !== undefined && item.error !== null) {
1519
- throw delegatedResponseError(status, `result ${index} has conflicting fields`, requestId);
1520
- }
1521
- const qurl = delegatedResponseObject(item.qurl, status, `result ${index} qurl`, requestId);
1522
- if (typeof qurl.qurl_id !== "string" ||
1523
- !DELEGATED_QURL_ID_PATTERN.test(qurl.qurl_id) ||
1524
- !validateDelegatedQurlLink(qurl.qurl_link) ||
1525
- !isUtcTimestamp(qurl.expires_at) ||
1526
- qurlIds.has(qurl.qurl_id) ||
1527
- links.has(qurl.qurl_link)) {
1528
- throw delegatedResponseError(status, `result ${index} has invalid qurl`, requestId);
1529
- }
1530
- qurlIds.add(qurl.qurl_id);
1531
- links.add(qurl.qurl_link);
1532
- succeeded++;
1533
- return {
1534
- index,
1535
- status: "succeeded",
1536
- qurl: {
1537
- qurl_id: qurl.qurl_id,
1538
- qurl_link: qurl.qurl_link,
1539
- expires_at: qurl.expires_at,
1540
- },
1541
- };
1542
- }
1543
- if (item.status !== "failed" || (item.qurl !== undefined && item.qurl !== null)) {
1544
- throw delegatedResponseError(status, `result ${index} has invalid status`, requestId);
1545
- }
1546
- const error = delegatedResponseObject(item.error, status, `result ${index} error`, requestId);
1547
- if (typeof error.code !== "string" ||
1548
- error.code.length === 0 ||
1549
- typeof error.message !== "string") {
1550
- throw delegatedResponseError(status, `result ${index} has invalid error`, requestId);
1551
- }
1552
- return {
1553
- index,
1554
- status: "failed",
1555
- error: { code: error.code, message: error.message },
1556
- };
1557
- });
1558
- if ((batchStatus === "succeeded" && succeeded !== itemCount) ||
1559
- (batchStatus === "failed" && succeeded !== 0) ||
1560
- (batchStatus === "partially_failed" && (succeeded === 0 || succeeded === itemCount))) {
1561
- throw delegatedResponseError(status, "terminal status does not match results", requestId);
1562
- }
1563
- return results;
1564
- }
1565
- function classifyDelegatedBatchCreateFailure(error, batchId) {
1566
- if (!(error instanceof QURLError)) {
1567
- throw new DelegatedBatchOutcomeUnknownError(new RuntimeError("Unexpected failure after delegated batch dispatch", { cause: error }), batchId);
1568
- }
1569
- if ((error.status >= 400 && error.status < 500 && error.status !== 408) || error.status === 501) {
1570
- throw error;
1571
- }
1572
- throw new DelegatedBatchOutcomeUnknownError(error, batchId);
1573
- }
1574
- // `format: uri` in the OpenAPI spec allows schemes the SDK doesn't
1575
- // usefully support (`ftp://`, `file://`, `javascript:`, …). This is a
1576
- // cheap client-side sanity check — the server is still the
1577
- // authoritative validator (e.g. it rejects localhost, cloud metadata,
1578
- // and private-range hosts; the SDK doesn't need to duplicate that).
1579
- const ALLOWED_URL_SCHEMES = ["http://", "https://"];
1580
- function requireValidHttpUrl(value, field) {
1581
- // Three checks in priority order: type guard, scheme, length.
1582
- // Stop at the first failure so a non-string URL doesn't
1583
- // produce both "must be a string" AND "must be ≤ 2048 characters"
1584
- // (the length check on a non-string would also typeof-fail and
1585
- // duplicate the type message). The collect-all loop in
1586
- // validateCreateInput runs each FIELD's validator independently — within
1587
- // a single URL field itself we want fail-fast.
1588
- if (typeof value !== "string") {
1589
- throw clientValidationError(`${field}: must be a string (got ${value === null ? "null" : typeof value})`);
1590
- }
1591
- if (!ALLOWED_URL_SCHEMES.some((scheme) => value.startsWith(scheme))) {
1592
- // Truncate to keep the error message compact and avoid
1593
- // pathologically long schemes from filling logs.
1594
- const repr = JSON.stringify(value).slice(0, 40);
1595
- throw clientValidationError(`${field}: must start with http:// or https:// (got ${repr})`);
1596
- }
1597
- if (value.length > MAX_TARGET_URL) {
1598
- throw clientValidationError(`${field}: must be ${MAX_TARGET_URL} characters or fewer (got ${value.length})`);
1599
- }
1600
- }
1601
- function requireValidTargetUrl(target_url) {
1602
- requireValidHttpUrl(target_url, "target_url");
1603
- }
1604
- function validateCreateInput(input) {
1605
- // Untyped-JS guard: surface as ValidationError instead of raw TypeError.
1606
- if (typeof input !== "object" || input === null) {
1607
- throw clientValidationError(`create: input must be an object (got ${input === null ? "null" : typeof input})`);
1608
- }
1609
- // Collect-all: run every validator and aggregate the messages so a
1610
- // caller fixing multiple bad fields sees them all at once instead
1611
- // of fix-re-run-repeat. Matches the documented batchCreate UX
1612
- // ("All failures are collected into a single ValidationError")
1613
- // and mirrors the requireValidTags collect-all pattern.
1614
- const errors = [];
1615
- const collect = (fn) => {
1616
- try {
1617
- fn();
1618
- }
1619
- catch (err) {
1620
- if (err instanceof ValidationError) {
1621
- errors.push(err.detail);
1622
- return;
1623
- }
1624
- throw err;
1625
- }
1626
- };
1627
- collect(() => requireNoUnknownFields(input, CREATE_FIELD_KEYS, "create"));
1628
- const resourceType = input.type;
1629
- if (resourceType !== undefined && typeof resourceType !== "string") {
1630
- errors.push(`type: must be a string (got ${resourceType === null ? "null" : typeof resourceType})`);
1631
- }
1632
- collect(() => {
1633
- if (input.target_url === undefined) {
1634
- if (resourceType === undefined || resourceType === "url") {
1635
- throw clientValidationError("target_url: is required for url qURLs");
1636
- }
1637
- return;
1638
- }
1639
- requireValidTargetUrl(input.target_url);
1640
- });
1641
- collect(() => requireMaxLength(input.label, "label", MAX_LABEL));
1642
- collect(() => requireNonEmptyIfPresent(input.label, "label"));
1643
- collect(() => requireMaxLength(input.custom_domain, "custom_domain", MAX_CUSTOM_DOMAIN));
1644
- collect(() => requireNonEmptyIfPresent(input.custom_domain, "custom_domain"));
1645
- collect(() => requireBooleanIfPresent(input.private, "private"));
1646
- collect(() => requireBooleanIfPresent(input.one_time_use, "one_time_use"));
1647
- collect(() => requireMaxSessionsInRange(input.max_sessions));
1648
- collect(() => requireNonEmptyIfPresent(input.expires_in, "expires_in"));
1649
- collect(() => requireNonEmptyIfPresent(input.session_duration, "session_duration"));
1650
- collect(() => requireValidAccessPolicy(input.access_policy));
1651
- if (errors.length > 0) {
1652
- // Inner `; ` is contractual (paired with batchCreate's outer ` | `).
1653
- // Future field-level validators must not aggregate via `; ` internally.
1654
- throw clientValidationError(errors.join("; "));
1655
- }
1656
- // Intentional omission: duration *grammar* (`expires_in`,
1657
- // `session_duration`) is server-authoritative. Empty strings are
1658
- // rejected up front (see collect() calls above) but unit/whitespace
1659
- // edge cases ("24hh", "1w 3d") surface as a clean server 400 — a
1660
- // client-side regex would just create a drift surface against the
1661
- // server's rego/duration parser.
1662
- //
1663
- // `access_policy` contents (CIDRs, ISO-3166 codes, regex patterns)
1664
- // are pass-through for the same reason. The shape-guard above only
1665
- // catches structural mistakes (string-instead-of-array on list fields)
1666
- // an untyped-JS caller is likely to make.
1667
- }
1668
- function validateQurlTokenOptions(input) {
1669
- if (input === undefined)
1670
- return;
1671
- requireMaxLength(input.label, "label", MAX_LABEL);
1672
- requireNonEmptyIfPresent(input.label, "label");
1673
- requireNonEmptyIfPresent(input.expires_in, "expires_in");
1674
- requireNonEmptyIfPresent(input.session_duration, "session_duration");
1675
- // Mirror the service-documented size cap, but keep host-safety grammar
1676
- // (single leading slash, no scheme/host/backslash/etc.) and the tunnel-only
1677
- // gate server-authoritative (`invalid_target_path`); a client-side regex
1678
- // would create a drift surface. Keep max-before-empty consistent with label;
1679
- // empty strings pass the length check and are rejected by the next guard.
1680
- requireMaxUtf8Bytes(input.target_path, "target_path", MAX_TARGET_PATH);
1681
- requireNonEmptyIfPresent(input.target_path, "target_path");
1682
- requireBooleanIfPresent(input.one_time_use, "one_time_use");
1683
- requireMaxSessionsInRange(input.max_sessions);
1684
- requireValidAccessPolicy(input.access_policy);
1685
- }
1686
- function validateResourceQurlUpdateInput(input) {
1687
- requireMaxLength(input.label, "label", MAX_LABEL);
1688
- requireNonEmptyIfPresent(input.label, "label");
1689
- requireNonEmptyIfPresent(input.extend_by, "extend_by");
1690
- requireNonEmptyIfPresent(input.expires_at, "expires_at");
1691
- requireNonEmptyIfPresent(input.session_duration, "session_duration");
1692
- requireMaxSessionsInRange(input.max_sessions);
1693
- requireValidAccessPolicy(input.access_policy);
1694
- if (input.extend_by !== undefined && input.expires_at !== undefined) {
1695
- throw clientValidationError("updateResourceQurl: `extend_by` and `expires_at` are mutually exclusive — provide at most one");
1696
- }
1697
- }
1698
- function requireObjectInput(input, method) {
1699
- if (typeof input !== "object" || input === null || Array.isArray(input)) {
1700
- throw clientValidationError(`${method}: input must be an object (got ${input === null ? "null" : typeof input})`);
1701
- }
1702
- }
1703
- function requireNonEmptyStringField(input, field, method) {
1704
- const value = input[field];
1705
- if (typeof value !== "string" || value.length === 0) {
1706
- throw clientValidationError(`${method}: ${field} must be a non-empty string`);
1707
- }
1708
- }
1709
- function requireNonEmptyArrayField(input, field, method) {
1710
- const value = input[field];
1711
- if (!Array.isArray(value) || value.length === 0) {
1712
- throw clientValidationError(`${method}: ${field} must be a non-empty array`);
1713
- }
1714
- }
1715
- function requireStringArrayElements(value, field, method) {
1716
- if (!Array.isArray(value)) {
1717
- throw clientValidationError(`${method}: ${field} must be a non-empty array`);
1718
- }
1719
- value.forEach((entry, index) => {
1720
- if (typeof entry !== "string" || entry.length === 0) {
1721
- throw clientValidationError(`${method}: ${field}[${index}] must be a non-empty string`);
1722
- }
1723
- });
1724
- }
1725
- function requireAtLeastOneField(input, fields, method) {
1726
- if (!fields.some((field) => input[field] !== undefined)) {
1727
- throw clientValidationError(`${method}: at least one field (${fields.join(", ")}) must be provided`);
1728
- }
1729
- }
1730
- function requireNoUnknownFields(input, fields, method) {
1731
- const unknown = Object.keys(input).filter((field) => !fields.includes(field));
1732
- if (unknown.length > 0) {
1733
- const rendered = unknown.map((field) => `"${field}"`).join(", ");
1734
- const label = unknown.length === 1 ? "unknown field" : "unknown fields";
1735
- throw clientValidationError(`${method}: ${label} ${rendered}`);
1736
- }
1737
- }
1738
- function normalizePatchFields(input, fields, options = {}) {
1739
- const preserveNull = new Set(options.preserveNullFields ?? []);
1740
- const normalized = {};
1741
- for (const key of fields) {
1742
- const value = input[key];
1743
- if (value !== undefined && (value !== null || preserveNull.has(key))) {
1744
- normalized[key] = value;
1745
- }
1746
- }
1747
- return normalized;
1748
- }
1749
- function requireConsumerDecision(id, version, allowNew = false) {
1750
- if (typeof id !== "string" ||
1751
- (!/^[a-f0-9]{64}$/.test(id) && !(allowNew && id === "new")) ||
1752
- !Number.isSafeInteger(version) ||
1753
- version < (allowNew ? 0 : 1))
1754
- throw clientValidationError("consumer decision: invalid id or expected version");
1755
- }
1756
- function validateResourceWriteFields(input, options = {}) {
1757
- const errors = [];
1758
- const collect = (fn) => {
1759
- try {
1760
- fn();
1761
- }
1762
- catch (err) {
1763
- if (err instanceof ValidationError) {
1764
- errors.push(err.detail);
1765
- return;
1766
- }
1767
- throw err;
1768
- }
1769
- };
1770
- collect(() => {
1771
- if (input.private !== undefined && typeof input.private !== "boolean") {
1772
- throw clientValidationError("private: must be a boolean");
1773
- }
1774
- if (input.consumer_access !== undefined) {
1775
- const policy = input.consumer_access;
1776
- requireObjectInput(policy, "consumer_access");
1777
- const p = policy;
1778
- requireNoUnknownFields(p, ["mode", "version", "contract_id"], "consumer_access");
1779
- if (!["keys", "invite_only", "email_capture", "review", "contract"].includes(p.mode) ||
1780
- !Number.isSafeInteger(p.version) ||
1781
- p.version < 1) {
1782
- throw clientValidationError("consumer_access: invalid mode or version");
1783
- }
1784
- if (p.mode === "contract") {
1785
- if (typeof p.contract_id !== "string" ||
1786
- !p.contract_id.trim() ||
1787
- new TextEncoder().encode(p.contract_id).length > 128) {
1788
- throw clientValidationError("consumer_access: contract mode requires a bounded contract_id");
1789
- }
1790
- }
1791
- else if (p.contract_id !== undefined) {
1792
- throw clientValidationError("consumer_access: contract_id requires contract mode");
1793
- }
1794
- if (input.private === false)
1795
- throw clientValidationError("consumer_access: requires a private resource");
1796
- if (options.requireUrlTarget) {
1797
- if (p.version !== 1 || input.private !== true)
1798
- throw clientValidationError("consumer_access: creation requires private true and version one");
1799
- }
1800
- else if (options.allowAliasClear) {
1801
- const expected = input.expected_consumer_access_version;
1802
- if (!Number.isSafeInteger(expected) ||
1803
- expected < 0 ||
1804
- p.version !== expected + 1 ||
1805
- Object.keys(input).some((key) => key !== "consumer_access" && key !== "expected_consumer_access_version")) {
1806
- throw clientValidationError("consumer_access: update requires its expected version and no metadata changes");
1807
- }
1808
- }
1809
- }
1810
- else if (input.expected_consumer_access_version !== undefined) {
1811
- throw clientValidationError("expected_consumer_access_version: requires consumer_access");
1812
- }
1813
- });
1814
- collect(() => {
1815
- const fields = ["allowed_device_keys_add", "allowed_device_keys_remove"];
1816
- for (const field of fields) {
1817
- const keys = input[field];
1818
- if (keys === undefined)
1819
- continue;
1820
- requireStringArrayElements(keys, field, "updateResource");
1821
- if (keys.length > 256 || new Set(keys).size !== keys.length) {
1822
- throw clientValidationError(`${field}: must contain at most 256 unique keys`);
1823
- }
1824
- for (const key of keys) {
1825
- let canonical = false;
1826
- try {
1827
- canonical = key.length === 44 && atob(key).length === 32 && btoa(atob(key)) === key;
1828
- }
1829
- catch {
1830
- /* Invalid base64 is rejected below. */
1831
- }
1832
- if (!canonical) {
1833
- throw clientValidationError(`${field}: keys must be canonical padded base64 of 32 bytes`);
1834
- }
1835
- }
1836
- if (input.custom_domain !== undefined) {
1837
- throw clientValidationError(`${field}: update separately from custom_domain`);
1838
- }
1839
- }
1840
- const added = new Set(input.allowed_device_keys_add);
1841
- if (input.allowed_device_keys_remove?.some((key) => added.has(key))) {
1842
- throw clientValidationError("allowed device key additions and removals must not overlap");
1843
- }
1844
- });
1845
- const resourceType = input.type;
1846
- if (resourceType !== undefined && typeof resourceType !== "string") {
1847
- errors.push(`type: must be a string (got ${resourceType === null ? "null" : typeof resourceType})`);
1848
- }
1849
- collect(() => {
1850
- if (input.target_url === undefined) {
1851
- if (options.requireUrlTarget && (resourceType === undefined || resourceType === "url")) {
1852
- throw clientValidationError("target_url: is required for url resources");
1853
- }
1854
- return;
1855
- }
1856
- requireValidTargetUrl(input.target_url);
1857
- });
1858
- collect(() => requireMaxLength(input.description, "description", MAX_DESCRIPTION));
1859
- collect(() => requireMaxLength(input.custom_domain, "custom_domain", MAX_CUSTOM_DOMAIN));
1860
- collect(() => {
1861
- if (options.allowCustomDomainClear && input.custom_domain === "")
1862
- return;
1863
- requireNonEmptyIfPresent(input.custom_domain, "custom_domain");
1864
- });
1865
- collect(() => requireValidTags(input.tags));
1866
- collect(() => {
1867
- if (input.alias !== undefined) {
1868
- if (input.alias === null) {
1869
- if (!options.allowAliasClear) {
1870
- throw clientValidationError("alias: must be a string (got null)");
1871
- }
1872
- }
1873
- else {
1874
- requireMaxLength(input.alias, "alias", MAX_ALIAS);
1875
- requireNonEmptyIfPresent(input.alias, "alias");
1876
- }
1877
- }
1878
- });
1879
- collect(() => {
1880
- if (input.slug === undefined)
1881
- return;
1882
- requireMaxLength(input.slug, "slug", MAX_SLUG);
1883
- requireNonEmptyIfPresent(input.slug, "slug");
1884
- });
1885
- collect(() => {
1886
- if (options.validateFindOrCreate &&
1887
- input.find_or_create !== undefined &&
1888
- typeof input.find_or_create !== "boolean") {
1889
- throw clientValidationError(`find_or_create: must be a boolean (got ${describeShape(input.find_or_create)})`);
1890
- }
1891
- });
1892
- collect(() => {
1893
- if (options.validatePreserveHost &&
1894
- input.preserve_host !== undefined &&
1895
- typeof input.preserve_host !== "boolean") {
1896
- throw clientValidationError(`preserve_host: must be a boolean (got ${describeShape(input.preserve_host)})`);
1897
- }
1898
- });
1899
- if (errors.length > 0) {
1900
- throw clientValidationError(errors.join("; "));
1901
- }
1902
- }
1903
- function validateWebhookWriteFields(input, method, requiredFields = {}) {
1904
- if (requiredFields.url || input.url !== undefined) {
1905
- requireNonEmptyStringField(input, "url", method);
1906
- requireValidHttpUrl(input.url, "url");
1907
- }
1908
- if (requiredFields.events || input.events !== undefined) {
1909
- requireNonEmptyArrayField(input, "events", method);
1910
- requireStringArrayElements(input.events, "events", method);
1911
- }
1912
- requireMaxLength(input.description, "description", MAX_DESCRIPTION);
1913
- if (input.status !== undefined) {
1914
- requireNonEmptyStringField(input, "status", method);
1915
- }
1916
- }
1917
- function validateApiKeyWriteFields(input, method, requiredFields = {}) {
1918
- if (requiredFields.name || input.name !== undefined) {
1919
- requireNonEmptyStringField(input, "name", method);
1920
- requireMaxLength(input.name, "name", MAX_API_KEY_NAME);
1921
- }
1922
- if (requiredFields.scopes || input.scopes !== undefined) {
1923
- requireNonEmptyArrayField(input, "scopes", method);
1924
- requireStringArrayElements(input.scopes, "scopes", method);
1925
- const unsupported = input.scopes.find((scope) => !API_KEY_REQUEST_SCOPES.has(scope));
1926
- if (unsupported !== undefined) {
1927
- throw clientValidationError(`${method}: scopes contains an unsupported permission ${JSON.stringify(unsupported)}; this SDK accepts ${API_KEY_REQUEST_SCOPE_VALUES.join(", ")}`);
1928
- }
1929
- }
1930
- }
1931
- function validateEnrollmentTokenFields(input, method) {
1932
- if (input.target !== undefined &&
1933
- (typeof input.target !== "string" || !CREDENTIAL_TARGETS.has(input.target))) {
1934
- throw clientValidationError(`${method}: target must be 'agent' or 'connector'`);
1935
- }
1936
- let claimCount = 0;
1937
- if (input.claims !== undefined) {
1938
- if (!Array.isArray(input.claims)) {
1939
- throw clientValidationError(`${method}: claims must be an array`);
1940
- }
1941
- if (input.claims.length > 1) {
1942
- throw clientValidationError(`${method}: claims must contain at most one claim`);
1943
- }
1944
- claimCount = input.claims.length;
1945
- for (const [index, claim] of input.claims.entries()) {
1946
- if (!claim || typeof claim !== "object" || Array.isArray(claim)) {
1947
- throw clientValidationError(`${method}: claims[${index}] must be an object`);
1948
- }
1949
- requireNoUnknownFields(claim, ["type", "id"], `${method}: claims[${index}]`);
1950
- const record = claim;
1951
- if (typeof record.type !== "string" || !CREDENTIAL_CLAIM_TYPES.has(record.type)) {
1952
- throw clientValidationError(`${method}: claims[${index}].type must be 'connector'`);
1953
- }
1954
- if (typeof record.id !== "string" || !CONNECTOR_SLUG_PATTERN.test(record.id)) {
1955
- throw clientValidationError(`${method}: claims[${index}].id is not a valid connector slug; use 3-64 lowercase letters, digits, or hyphens, starting with a letter and ending with a letter or digit`);
1956
- }
1957
- }
1958
- }
1959
- // CreateApiKeyRequest.target permits an agent with zero or one connector claim.
1960
- if (input.target === "connector" && claimCount !== 1) {
1961
- throw clientValidationError(`${method}: target 'connector' requires exactly one claim`);
1962
- }
1963
- if (input.expires_in !== undefined) {
1964
- if (typeof input.expires_in !== "string") {
1965
- throw clientValidationError(`${method}: expires_in must be a duration string`);
1966
- }
1967
- const match = CREDENTIAL_DURATION_PATTERN.exec(input.expires_in);
1968
- if (!match) {
1969
- throw clientValidationError(`${method}: expires_in must match <integer><s|m|h|d|w>`);
1970
- }
1971
- const seconds = BigInt(match[1]) * CREDENTIAL_DURATION_MULTIPLIERS[match[2]];
1972
- if (seconds <= 0n || seconds > MAX_ENROLLMENT_TOKEN_TTL_SECONDS) {
1973
- throw clientValidationError(`${method}: expires_in must be greater than zero and at most 24h`);
1974
- }
1975
- }
1976
- }
1977
- /**
1978
- * Per-entry shape guard for {@link BatchItemResult}. Verifies every
1979
- * non-optional field on the branch of the discriminated union selected
1980
- * by the `success` discriminant. Protects consumers who narrow on
1981
- * `success` and then access `resource_id` / `qurl_link` / `error` as
1982
- * guaranteed-present — if the API ever omits one, we trip the guard
1983
- * rather than returning `undefined` where the type says `string`.
1984
- *
1985
- * Returns `null` when the entry is valid, or a human-readable reason
1986
- * string describing the *specific* field that's missing or wrong. The
1987
- * reason never echoes the entry's values — only field names and shape
1988
- * descriptors — so the caller can safely surface it in error messages
1989
- * that may end up in observability pipelines. Contrast with the outer
1990
- * guard's intentional silence about response-body content for the
1991
- * same info-leak reason.
1992
- */
1993
- function batchItemResultValidationReason(entry) {
1994
- if (!entry || typeof entry !== "object")
1995
- return "not an object";
1996
- const e = entry;
1997
- // `Number.isInteger` rejects NaN, Infinity, and floats; combined
1998
- // with `>= 0`, the check rules out pathological proxy bodies that
1999
- // would attribute to a useless index. `index` is informational, so
2000
- // this isn't a correctness hazard, but tightening the constraint
2001
- // matches the same posture as the count integer guards above.
2002
- if (!Number.isInteger(e.index) || e.index < 0) {
2003
- return "missing/invalid 'index' (expected non-negative integer)";
2004
- }
2005
- if (e.success === true) {
2006
- if (typeof e.resource_id !== "string") {
2007
- return "success-branch missing required field 'resource_id' (expected string)";
2008
- }
2009
- if (typeof e.qurl_link !== "string") {
2010
- return "success-branch missing required field 'qurl_link' (expected string)";
2011
- }
2012
- if (typeof e.qurl_site !== "string") {
2013
- return "success-branch missing required field 'qurl_site' (expected string)";
2014
- }
2015
- return null;
2016
- }
2017
- if (e.success === false) {
2018
- if (!e.error || typeof e.error !== "object") {
2019
- return "failure-branch missing required field 'error' (expected object)";
2020
- }
2021
- const err = e.error;
2022
- if (typeof err.code !== "string") {
2023
- return "failure-branch missing required field 'error.code' (expected string)";
2024
- }
2025
- if (typeof err.message !== "string") {
2026
- return "failure-branch missing required field 'error.message' (expected string)";
2027
- }
2028
- return null;
2029
- }
2030
- return "missing/invalid 'success' discriminant (expected boolean)";
2031
- }
2032
- // ---- Portal-verb helpers (mirrors qurl-go) -------------------------------
2033
- // Client-side guardrails for the portal flow (protectUrl → createPortal →
2034
- // enterPortal), kept in lockstep with qurl-go's option validation and
2035
- // qurl-python's port of it.
2036
- /**
2037
- * Client-side floor for portal lifetimes, mirroring qurl-go's `ValidFor`.
2038
- * The LayerV API remains the source of truth for account limits; there is
2039
- * intentionally no SDK-side maximum because an account may allow
2040
- * longer-lived portals.
2041
- */
2042
- const MIN_PORTAL_VALID_FOR_SECONDS = 60;
2043
- /**
2044
- * Client-side floor for session durations, mirroring qurl-go's
2045
- * `WithSessionDuration` (at least one second; zero is rejected rather than
2046
- * treated as the server default).
2047
- */
2048
- const MIN_SESSION_DURATION_SECONDS = 1;
2049
- /**
2050
- * Shape of qurl-go's offline signed-fragment links: three or more
2051
- * dot-separated base64url parts (<version>.<claims>.<secret>.<sig>).
2052
- * Detected only to give those links a precise error in extractAccessToken.
2053
- *
2054
- * @internal Exported for in-package tests; not re-exported from index.ts.
2055
- */
2056
- export const SIGNED_FRAGMENT_RE = /^[A-Za-z0-9_-]+(?:\.[A-Za-z0-9_-]+){2,}$/;
2057
- // Access-token grammar accepted by enterPortal (base64url charset).
2058
- const ACCESS_TOKEN_RE = /^[A-Za-z0-9_-]+$/;
2059
- /**
2060
- * Serialize a duration option to the API's h/m/s duration grammar.
2061
- *
2062
- * Strings pass through unvalidated — the server is authoritative, same as
2063
- * the REST-shaped `expires_in` fields. Numbers are milliseconds and get
2064
- * qurl-go's client-side guardrails: whole seconds only and at least
2065
- * `minimumSeconds` (checked in that order so e.g. 30.5s reports "at least
2066
- * 60s" in every SDK). Serialized with hours as the largest unit so all API
2067
- * duration fields use the same grammar across SDKs.
2068
- */
2069
- function serializeApiDuration(value, field, minimumSeconds) {
2070
- if (value === undefined)
2071
- return undefined;
2072
- if (typeof value === "string") {
2073
- if (value.trim() === "") {
2074
- throw clientValidationError(`${field}: must be a non-empty duration string`);
2075
- }
2076
- return value;
2077
- }
2078
- if (typeof value === "number") {
2079
- // NaN fails no comparison, so an explicit finite check keeps the
2080
- // error actionable instead of a misleading "whole seconds" report.
2081
- if (!Number.isFinite(value)) {
2082
- throw clientValidationError(`${field}: must be a finite number of milliseconds`);
2083
- }
2084
- if (value < minimumSeconds * 1000) {
2085
- throw clientValidationError(`${field}: must be at least ${minimumSeconds}s`);
2086
- }
2087
- if (value % 1000 !== 0) {
2088
- throw clientValidationError(`${field}: must be whole seconds`);
2089
- }
2090
- const seconds = value / 1000;
2091
- if (seconds % 3600 === 0)
2092
- return `${seconds / 3600}h`;
2093
- if (seconds % 60 === 0)
2094
- return `${seconds / 60}m`;
2095
- return `${seconds}s`;
2096
- }
2097
- throw clientValidationError(`${field}: must be a duration string (e.g. "5m") or a number of milliseconds`);
2098
- }
2099
- /**
2100
- * Portal-verb target URL validation (qurl-go `ProtectURL` parity). On top of
2101
- * the shared scheme/length check, the URL must carry a host and must parse,
2102
- * and embedded credentials (userinfo) are rejected. The added checks never
2103
- * echo the URL — a userinfo URL contains credentials that must stay out of
2104
- * logs and error messages. The REST-shaped compatibility layer keeps its
2105
- * original prefix-only check so its accepted inputs don't change.
2106
- */
2107
- function requirePortalTargetUrl(targetUrl) {
2108
- requireValidTargetUrl(targetUrl);
2109
- const url = targetUrl; // requireValidTargetUrl only returns for http(s) strings
2110
- // WHATWG URL normalizes away an empty authority ("https:///x" parses
2111
- // with host "x"), so the host-presence check runs on the raw string,
2112
- // mirroring qurl-go's url.Parse which rejects authority-less URLs.
2113
- const afterScheme = url.slice(url.indexOf("://") + 3);
2114
- if (afterScheme === "" || "/?#".includes(afterScheme[0])) {
2115
- throw clientValidationError("target_url: must include a host");
2116
- }
2117
- let parsed;
2118
- try {
2119
- parsed = new URL(url);
2120
- }
2121
- catch {
2122
- throw clientValidationError("target_url: malformed URL");
2123
- }
2124
- if (parsed.username !== "" || parsed.password !== "") {
2125
- throw clientValidationError("target_url: must not include embedded credentials (userinfo)");
2126
- }
2127
- }
2128
- /**
2129
- * Extract the access token from a qURL link, or pass a bare token through.
2130
- *
2131
- * Platform-issued links carry the access token in the URL fragment
2132
- * (`https://qurl.link/#at_...`). Error messages deliberately never echo the
2133
- * input — qURL links are credentials and must stay out of logs.
2134
- */
2135
- function extractAccessToken(qurlLink) {
2136
- if (typeof qurlLink !== "string" || qurlLink.trim() === "") {
2137
- throw clientValidationError("qurlLink: must be a non-empty string");
2138
- }
2139
- const hashIndex = qurlLink.indexOf("#");
2140
- const token = hashIndex >= 0 ? qurlLink.slice(hashIndex + 1) : qurlLink;
2141
- if (SIGNED_FRAGMENT_RE.test(token)) {
2142
- throw clientValidationError("qurlLink: this is a signed qURL link (offline fragment format), which cannot be " +
2143
- "opened through the API resolver — enterPortal only accepts platform links " +
2144
- "(https://qurl.link/#at_...) or bare access tokens");
2145
- }
2146
- if (token === "" || !ACCESS_TOKEN_RE.test(token)) {
2147
- throw clientValidationError("qurlLink: no access token found — pass a platform qURL link " +
2148
- "(https://qurl.link/#at_...) or a bare access token");
2149
- }
2150
- return token;
2151
- }
2152
- /**
2153
- * Parse an RFC 3339 timestamp from the API into a `Date`, or `undefined`
2154
- * when absent or unparseable. The portal surface exposes `Date` (matching
2155
- * qurl-go's `time.Time` and qurl-python's `datetime`); the REST-shaped
2156
- * layer keeps raw wire strings.
2157
- */
2158
- function parseApiDate(value) {
2159
- if (typeof value !== "string" || value === "")
2160
- return undefined;
2161
- const parsed = new Date(value);
2162
- return Number.isNaN(parsed.getTime()) ? undefined : parsed;
2163
- }
2164
- /**
2165
- * Parse a {@link Portal} from a create-portal API response. `resource_id`
2166
- * and `qurl_link` are required, mirroring qurl-go's fail-closed response
2167
- * check; everything else is optional because the two mint endpoints differ
2168
- * in which ancillary fields they return.
2169
- */
2170
- async function parsePortal(data, method, expectedCrid) {
2171
- if (typeof data?.resource_id !== "string" || data.resource_id.trim() === "") {
2172
- throw unexpectedResponseError(`${method}: response is missing resource_id`);
2173
- }
2174
- if (typeof data.qurl_link !== "string" || data.qurl_link.trim() === "") {
2175
- throw unexpectedResponseError(`${method}: response is missing qurl_link`);
2176
- }
2177
- await verifyResourceIdentity(data, method, expectedCrid);
2178
- return {
2179
- crid: data.crid,
2180
- resourcePublicKey: data.resource_id,
2181
- link: data.qurl_link,
2182
- site: data.qurl_site,
2183
- expiresAt: parseApiDate(data.expires_at),
2184
- // Same empty-string → absent normalization the REST-shaped layer's
2185
- // consumers apply to qurl_id.
2186
- qurlId: data.qurl_id || undefined,
2187
- label: data.label,
2188
- };
2189
- }
2190
- /**
2191
- * Validate portal options and build the create-portal wire body. The service
2192
- * requires a JSON object even when every option is omitted; `{}` selects the
2193
- * owner's plan defaults and matches qurl-go's zero-value request struct.
2194
- */
2195
- function buildCreatePortalBody(opts, method) {
2196
- requireObjectInput(opts, method);
2197
- requireNoUnknownFields(opts, CREATE_PORTAL_OPTION_KEYS, method);
2198
- // null → omitted, via the SDK-wide normalizePatchFields convention.
2199
- const { validFor, label, oneTimeUse, maxSessions, sessionDuration, targetPath } = normalizePatchFields(opts, CREATE_PORTAL_OPTION_KEYS);
2200
- if (label !== undefined) {
2201
- requireMaxLength(label, "label", MAX_LABEL);
2202
- // qurl-go's WithLabel rejects blank labels; the REST-shaped mintLink
2203
- // keeps its permissive whitespace behavior, so the check lives here.
2204
- if (label.trim() === "") {
2205
- throw clientValidationError("label: must not be empty");
2206
- }
2207
- }
2208
- requireBooleanIfPresent(oneTimeUse, "oneTimeUse");
2209
- requireMaxSessionsInRange(maxSessions, "maxSessions");
2210
- requireMaxUtf8Bytes(targetPath, "targetPath", MAX_TARGET_PATH);
2211
- requireNonEmptyIfPresent(targetPath, "targetPath");
2212
- if (targetPath !== undefined && method === "createPortalForUrl") {
2213
- throw clientValidationError("targetPath: requires an existing resource");
2214
- }
2215
- const body = {};
2216
- const expiresIn = serializeApiDuration(validFor, "validFor", MIN_PORTAL_VALID_FOR_SECONDS);
2217
- if (expiresIn !== undefined)
2218
- body.expires_in = expiresIn;
2219
- if (label !== undefined)
2220
- body.label = label;
2221
- if (oneTimeUse !== undefined)
2222
- body.one_time_use = oneTimeUse;
2223
- // Explicit 0 means unlimited and must survive body construction.
2224
- if (maxSessions !== undefined)
2225
- body.max_sessions = maxSessions;
2226
- const wireSessionDuration = serializeApiDuration(sessionDuration, "sessionDuration", MIN_SESSION_DURATION_SECONDS);
2227
- if (wireSessionDuration !== undefined)
2228
- body.session_duration = wireSessionDuration;
2229
- if (targetPath !== undefined)
2230
- body.target_path = targetPath;
2231
- return body;
337
+ }, meta);
2232
338
  }
2233
339
  /** qURL API client. */
2234
340
  export class QURLClient {
@@ -2236,6 +342,7 @@ export class QURLClient {
2236
342
  apiKey;
2237
343
  fetchFn;
2238
344
  maxRetries;
345
+ maxResponseBodyBytes;
2239
346
  timeout;
2240
347
  userAgent;
2241
348
  debugFn;
@@ -2249,6 +356,15 @@ export class QURLClient {
2249
356
  if (/[\r\n]/.test(options.apiKey)) {
2250
357
  throw clientValidationError("apiKey: must not contain CR/LF characters");
2251
358
  }
359
+ const responseLimit = options.maxResponseBodyBytes === undefined
360
+ ? MAX_RESPONSE_BODY_BYTES
361
+ : options.maxResponseBodyBytes;
362
+ if (!Number.isSafeInteger(responseLimit) ||
363
+ responseLimit < 1 ||
364
+ responseLimit > MAX_CONFIGURABLE_RESPONSE_BODY_BYTES) {
365
+ throw clientValidationError("maxResponseBodyBytes: must be an integer from 1 through 16777216 bytes");
366
+ }
367
+ this.maxResponseBodyBytes = responseLimit;
2252
368
  this.apiKey = options.apiKey;
2253
369
  const rawBaseUrl = stripTrailingSlashes(options.baseUrl ?? DEFAULT_BASE_URL);
2254
370
  // Parse the URL so the loopback exemption matches on the hostname,
@@ -2345,21 +461,21 @@ export class QURLClient {
2345
461
  branch: "non-array",
2346
462
  qurls_type: typeof qurls,
2347
463
  });
2348
- return rest;
464
+ return copyResponseMetadata(raw, rest);
2349
465
  }
2350
466
  // null is semantically "no tokens" — silent drop is fine, but surface
2351
467
  // via debug so operators spot it if the API starts emitting null.
2352
468
  if (qurls === null) {
2353
469
  this.log("mapQurlsField: 'qurls' was null; dropping", { branch: "null" });
2354
- return rest;
470
+ return copyResponseMetadata(raw, rest);
2355
471
  }
2356
472
  if (qurls === undefined) {
2357
473
  // Symmetric with null / non-array branches.
2358
474
  this.log("mapQurlsField: 'qurls' was undefined; dropping", { branch: "undefined" });
2359
- return rest;
475
+ return copyResponseMetadata(raw, rest);
2360
476
  }
2361
477
  if (!rest.access_tokens) {
2362
- return { ...rest, access_tokens: qurls };
478
+ return copyResponseMetadata(raw, { ...rest, access_tokens: qurls });
2363
479
  }
2364
480
  // Server-side bug — drop `qurls` rather than merge. If the arrays
2365
481
  // disagree, no consistent reconciliation exists; a silent merge
@@ -2369,7 +485,7 @@ export class QURLClient {
2369
485
  qurls_count: qurls.length,
2370
486
  access_tokens_count: rest.access_tokens.length,
2371
487
  });
2372
- return rest;
488
+ return copyResponseMetadata(raw, rest);
2373
489
  }
2374
490
  /**
2375
491
  * Validate the response envelope from POST /v1/qurls/batch and return
@@ -2393,9 +509,9 @@ export class QURLClient {
2393
509
  * is malformed, additional shape errors don't help debug the upstream
2394
510
  * issue.
2395
511
  */
2396
- validateBatchCreateResponse(envelope, requestItemCount) {
2397
- const result = envelope.data;
2398
- const statusSuffix = envelope.__http_status !== undefined ? ` (HTTP ${envelope.__http_status})` : "";
512
+ validateBatchCreateResponse(response, requestItemCount) {
513
+ const result = response.envelope.data;
514
+ const statusSuffix = ` (HTTP ${response.status})`;
2399
515
  // Thread the server's request_id into the shape-guard error so
2400
516
  // operators debugging "unexpected response" tickets have the
2401
517
  // correlation handle on the error path, mirroring the success-
@@ -2405,7 +521,7 @@ export class QURLClient {
2405
521
  // .requestId property AND the message string so a stack trace
2406
522
  // pasted into a support ticket carries the correlation handle
2407
523
  // without a follow-up round-trip.
2408
- const requestId = boundedErrorIdentifier(envelope.meta?.request_id);
524
+ const requestId = boundedErrorIdentifier(response.envelope.meta?.request_id);
2409
525
  const requestIdSuffix = requestId !== undefined ? ` [request_id=${requestId}]` : "";
2410
526
  // `Number.isInteger` rejects NaN, Infinity, and floats. Combined
2411
527
  // with `>= 0`, this rules out pathological proxy bodies (e.g.
@@ -2469,7 +585,7 @@ export class QURLClient {
2469
585
  pageCount += 1;
2470
586
  const page = await loadPage({ ...input, cursor });
2471
587
  for (const item of itemsFromPage(page)) {
2472
- yield item;
588
+ yield copyResponseMetadata(page, item);
2473
589
  }
2474
590
  cursor = page.next_cursor;
2475
591
  if (page.has_more === false) {
@@ -2496,7 +612,9 @@ export class QURLClient {
2496
612
  * {@link ProtectedResource.createPortal}:
2497
613
  *
2498
614
  * ```ts
2499
- * const resource = await client.protectUrl("https://internal.example.com/dashboard");
615
+ * const resource = await client.protectUrl("https://internal.example.com/dashboard", {
616
+ * name: "Team dashboard",
617
+ * });
2500
618
  * const portal = await resource.createPortal({ validFor: "5m" });
2501
619
  * share(portal.link);
2502
620
  * ```
@@ -2514,6 +632,8 @@ export class QURLClient {
2514
632
  // SDK-wide normalizePatchFields convention.
2515
633
  const normalized = normalizePatchFields(opts, PROTECT_URL_OPTION_KEYS);
2516
634
  const body = { target_url: targetUrl };
635
+ if (normalized.name !== undefined)
636
+ body.name = normalized.name;
2517
637
  if (normalized.private !== undefined)
2518
638
  body.private = normalized.private;
2519
639
  if (normalized.consumerAccess !== undefined) {
@@ -2532,13 +652,13 @@ export class QURLClient {
2532
652
  const response = await this.rawRequest("POST", "/v1/resources", body, {
2533
653
  requestOptions,
2534
654
  });
2535
- if (response.__http_status === 204)
655
+ if (response.status === 204)
2536
656
  throw unexpectedResponseError("protectUrl: expected a resource response body");
2537
- const resource = response.data;
657
+ const resource = withEnvelopeMetadata(response.envelope.data, response.envelope.meta);
2538
658
  await verifyResourceIdentity(resource, "protectUrl");
2539
659
  if (normalized.consumerAccess &&
2540
660
  (resource.private !== true ||
2541
- !sameConsumerPolicy(resource.consumer_access, normalized.consumerAccess, response.meta?.found_existing === true))) {
661
+ !sameConsumerPolicy(resource.consumer_access, normalized.consumerAccess, response.envelope.meta?.found_existing === true))) {
2542
662
  throw unexpectedResponseError("protectUrl: private policy was not confirmed");
2543
663
  }
2544
664
  return new ProtectedResource(this, resource.crid,
@@ -2565,9 +685,20 @@ export class QURLClient {
2565
685
  requireResourceCrid(id, "resourceByCrid");
2566
686
  return new ProtectedResource(this, id);
2567
687
  }
2568
- /** Find or create the active qURL Connector resource for an immutable slug. */
2569
- async ensureConnectorResource(slug, requestOptions, consumerAccess) {
688
+ /**
689
+ * Find or create the active qURL Connector resource for an immutable slug.
690
+ *
691
+ * `name` is the public name recipients see. The service requires it when the
692
+ * slug is new (`400 name_required` otherwise) and ignores it on reuse.
693
+ *
694
+ * @param slug - Immutable owner-scoped Connector slug.
695
+ * @param requestOptions - Per-call request options.
696
+ * @param consumerAccess - Private consumer policy for a new resource.
697
+ * @param name - Public resource name; required when the slug is new.
698
+ */
699
+ async ensureConnectorResource(slug, requestOptions, consumerAccess, name) {
2570
700
  requireConnectorSlug(slug, "ensureConnectorResource");
701
+ requireResourceNameIfPresent(name);
2571
702
  if (consumerAccess !== undefined) {
2572
703
  validateResourceWriteFields({ consumer_access: consumerAccess, private: true });
2573
704
  if (consumerAccess.version !== 1)
@@ -2587,52 +718,52 @@ export class QURLClient {
2587
718
  type: "tunnel",
2588
719
  slug,
2589
720
  find_or_create: true,
721
+ ...(name !== undefined ? { name } : {}),
2590
722
  ...(consumerAccess ? { private: true, consumer_access: consumerAccess } : {}),
2591
723
  }, { requestOptions, retry: false, generateIdempotencyKey: false });
2592
724
  // The find-or-create endpoint returns 201 for both newly-created
2593
725
  // and found-existing rows; meta.found_existing distinguishes the arms.
2594
- if (response.__http_status !== 201) {
2595
- throw unexpectedResponseError(`ensureConnectorResource: expected HTTP 201, got ${response.__http_status ?? "unknown"}`);
726
+ if (response.status !== 201) {
727
+ throw unexpectedResponseError(`ensureConnectorResource: expected HTTP 201, got ${response.status ?? "unknown"}`);
2596
728
  }
2597
729
  if (consumerAccess &&
2598
- (response.data.private !== true ||
2599
- !sameConsumerPolicy(response.data.consumer_access, consumerAccess, response.meta?.found_existing === true))) {
730
+ (response.envelope.data.private !== true ||
731
+ !sameConsumerPolicy(response.envelope.data.consumer_access, consumerAccess, response.envelope.meta?.found_existing === true))) {
2600
732
  throw unexpectedResponseError("ensureConnectorResource: private policy was not confirmed");
2601
733
  }
2602
- resource = await parseConnectorResource(this, response.data, "ensureConnectorResource", {
734
+ resource = await parseConnectorResource(this, response.envelope.data, "ensureConnectorResource", {
2603
735
  slug,
2604
736
  });
2605
737
  }
2606
738
  catch (error) {
2607
739
  classifyConnectorMutationFailure("ensure", error);
2608
740
  }
2609
- if (response.meta !== undefined &&
2610
- response.meta !== null &&
2611
- (typeof response.meta !== "object" || Array.isArray(response.meta))) {
741
+ if (response.envelope.meta !== undefined &&
742
+ response.envelope.meta !== null &&
743
+ (typeof response.envelope.meta !== "object" || Array.isArray(response.envelope.meta))) {
2612
744
  classifyConnectorMutationFailure("ensure", unexpectedResponseError("ensureConnectorResource: response has invalid meta"));
2613
745
  }
2614
- const foundExisting = response.meta
2615
- ?.found_existing;
746
+ const foundExisting = response.envelope.meta?.found_existing;
2616
747
  if (foundExisting === undefined || foundExisting === null) {
2617
748
  throw unexpectedResponseError("ensureConnectorResource: response is missing meta.found_existing; the validated resource exists, so reconcile by slug before retrying");
2618
749
  }
2619
750
  if (typeof foundExisting !== "boolean") {
2620
751
  classifyConnectorMutationFailure("ensure", unexpectedResponseError("ensureConnectorResource: response has invalid meta.found_existing"));
2621
752
  }
2622
- return { resource, foundExisting };
753
+ return withEnvelopeMetadata({ resource: withEnvelopeMetadata(resource, response.envelope.meta), foundExisting }, response.envelope.meta);
2623
754
  }
2624
755
  /** Fetch a qURL Connector resource by its CRID. */
2625
756
  async getConnectorResource(crid) {
2626
757
  requireResourceCrid(crid, "getConnectorResource");
2627
758
  requireConnectorSubtleCrypto("getConnectorResource");
2628
- const { data, __http_status } = await this.rawRequest("GET", `/v1/resources/${encodeURIComponent(crid)}`, undefined, { retry: false });
2629
- if (__http_status !== 200) {
2630
- throw unexpectedResponseError(`getConnectorResource: expected HTTP 200, got ${__http_status ?? "unknown"}`);
759
+ const { envelope: { data, meta }, status: httpStatus, } = await this.rawRequest("GET", `/v1/resources/${encodeURIComponent(crid)}`, undefined, { retry: false });
760
+ if (httpStatus !== 200) {
761
+ throw unexpectedResponseError(`getConnectorResource: expected HTTP 200, got ${httpStatus ?? "unknown"}`);
2631
762
  }
2632
- return parseConnectorResource(this, data?.resource, "getConnectorResource", {
763
+ return withEnvelopeMetadata(await parseConnectorResource(this, data?.resource, "getConnectorResource", {
2633
764
  crid,
2634
765
  revokedIsLifecycleError: true,
2635
- });
766
+ }), meta);
2636
767
  }
2637
768
  /** Fetch the single active qURL Connector resource for an immutable slug. */
2638
769
  async getConnectorResourceBySlug(slug) {
@@ -2642,9 +773,9 @@ export class QURLClient {
2642
773
  requireConnectorSubtleCrypto("getConnectorResourceBySlug");
2643
774
  // The slug point lookup is intrinsically active-only. It also
2644
775
  // rejects combining `slug` with `status`, so this query must stay slug-only.
2645
- const { data, __http_status } = await this.rawRequest("GET", `/v1/resources?slug=${encodeURIComponent(slug)}`, undefined, { retry: false });
2646
- if (__http_status !== 200) {
2647
- throw unexpectedResponseError(`getConnectorResourceBySlug: expected HTTP 200, got ${__http_status ?? "unknown"}`);
776
+ const { envelope: { data, meta }, status: httpStatus, } = await this.rawRequest("GET", `/v1/resources?slug=${encodeURIComponent(slug)}`, undefined, { retry: false });
777
+ if (httpStatus !== 200) {
778
+ throw unexpectedResponseError(`getConnectorResourceBySlug: expected HTTP 200, got ${httpStatus ?? "unknown"}`);
2648
779
  }
2649
780
  if (!Array.isArray(data)) {
2650
781
  throw unexpectedResponseError("getConnectorResourceBySlug: response has missing or invalid data");
@@ -2665,7 +796,7 @@ export class QURLClient {
2665
796
  detail: `getConnectorResourceBySlug: expected one active resource, got ${data.length}`,
2666
797
  });
2667
798
  }
2668
- return parseConnectorResource(this, data[0], "getConnectorResourceBySlug", { slug });
799
+ return withEnvelopeMetadata(await parseConnectorResource(this, data[0], "getConnectorResourceBySlug", { slug }), meta);
2669
800
  }
2670
801
  /**
2671
802
  * Revoke a qURL Connector resource by CRID.
@@ -2716,7 +847,7 @@ export class QURLClient {
2716
847
  requireResourceCrid(resourceId, "createPortal");
2717
848
  const body = buildCreatePortalBody(opts, "createPortal");
2718
849
  const data = await this.request("POST", `/v1/resources/${encodeURIComponent(resourceId)}/qurls`, body, requestOptions);
2719
- return parsePortal(data, "createPortal", resourceId);
850
+ return parsePortal(data, "createPortal", resourceId, body.target_path);
2720
851
  }
2721
852
  /**
2722
853
  * Protect `targetUrl` and mint a portal in one API call.
@@ -2739,10 +870,10 @@ export class QURLClient {
2739
870
  };
2740
871
  const data = await this.request("POST", "/v1/qurls", body, requestOptions);
2741
872
  const portal = await parsePortal(data, "createPortalForUrl");
2742
- return {
873
+ return copyResponseMetadata(data, {
2743
874
  portal,
2744
- resource: new ProtectedResource(this, portal.crid, targetUrl),
2745
- };
875
+ resource: copyResponseMetadata(data, new ProtectedResource(this, portal.crid, targetUrl)),
876
+ });
2746
877
  }
2747
878
  /**
2748
879
  * Open a qURL link programmatically and return the reachable resource.
@@ -2772,11 +903,11 @@ export class QURLClient {
2772
903
  if (typeof resolved?.target_url !== "string" || resolved.target_url === "") {
2773
904
  throw unexpectedResponseError("enterPortal: access was granted but the API returned no resource URL");
2774
905
  }
2775
- return {
906
+ return copyResponseMetadata(resolved, {
2776
907
  resourceUrl: resolved.target_url,
2777
908
  openSeconds: resolved.access_grant?.expires_in ?? 0,
2778
909
  resourcePublicKey: resolved.resource_id,
2779
- };
910
+ });
2780
911
  }
2781
912
  // --- Public API ---
2782
913
  /**
@@ -2800,6 +931,8 @@ export class QURLClient {
2800
931
  * populated `BatchCreateOutput` body in this case; whitelisted via
2801
932
  * `passthroughStatuses` so the structured per-item errors aren't
2802
933
  * swallowed by the generic error path.
934
+ * HTTP 400 is the documented all-item validation failure. HTTP 422 is
935
+ * not a batch passthrough status and follows normal error handling.
2803
936
  *
2804
937
  * Callers that only catch thrown errors will **silently miss partial
2805
938
  * failures**. Always branch on `result.failed > 0` before treating the
@@ -2890,11 +1023,11 @@ export class QURLClient {
2890
1023
  // from the envelope and propagate it into the returned
2891
1024
  // BatchCreateOutput — consumers filing support tickets on partial
2892
1025
  // or total batch failures need the correlation ID.
2893
- const envelope = await this.rawRequest("POST", "/v1/qurls/batch", input, {
1026
+ const response = await this.rawRequest("POST", "/v1/qurls/batch", input, {
2894
1027
  passthroughStatuses: BATCH_PASSTHROUGH_STATUSES,
2895
1028
  requestOptions: options,
2896
1029
  });
2897
- const result = this.validateBatchCreateResponse(envelope, input.items.length);
1030
+ const result = this.validateBatchCreateResponse(response, input.items.length);
2898
1031
  // Attach the server request_id from the envelope meta without
2899
1032
  // mutating `result` (which is the parsed JSON body). Mirrors the
2900
1033
  // non-mutating style used elsewhere (e.g. `mapQurlsField`). In the
@@ -2911,7 +1044,7 @@ export class QURLClient {
2911
1044
  // as the source, and `meta.request_id` is the canonical envelope
2912
1045
  // field. The data-side variant would be a wire-format duplication
2913
1046
  // we don't want to silently honor.
2914
- const requestId = envelope.meta?.request_id;
1047
+ const requestId = response.envelope.meta?.request_id;
2915
1048
  // Both-fields-present and data-only-present are both worth surfacing
2916
1049
  // via debug — the former because meta wins silently, the latter
2917
1050
  // because it suggests an envelope-shape transition. Data-side value
@@ -2933,7 +1066,7 @@ export class QURLClient {
2933
1066
  });
2934
1067
  }
2935
1068
  }
2936
- return requestId !== undefined ? { ...result, request_id: requestId } : result;
1069
+ return withEnvelopeMetadata(requestId !== undefined ? { ...result, request_id: requestId } : result, response.envelope.meta);
2937
1070
  }
2938
1071
  /**
2939
1072
  * Start one durable delegated qURL batch. The SDK sends this mutation once.
@@ -2946,7 +1079,7 @@ export class QURLClient {
2946
1079
  let dispatched = false;
2947
1080
  let acceptedBatchId;
2948
1081
  try {
2949
- const envelope = await this.rawRequest("POST", "/v1/delegated-qurl-batches", input, {
1082
+ const response = await this.rawRequest("POST", "/v1/delegated-qurl-batches", input, {
2950
1083
  requestOptions: options,
2951
1084
  retry: false,
2952
1085
  generateIdempotencyKey: false,
@@ -2955,25 +1088,25 @@ export class QURLClient {
2955
1088
  dispatched = true;
2956
1089
  },
2957
1090
  });
2958
- const status = envelope.__http_status ?? 0;
1091
+ const status = response.status ?? 0;
2959
1092
  if (status !== 202) {
2960
1093
  throw delegatedResponseError(status, "create returned an unexpected success status");
2961
1094
  }
2962
- const rawData = envelope.data;
1095
+ const rawData = response.envelope.data;
2963
1096
  if (typeof rawData === "object" && rawData !== null && !Array.isArray(rawData)) {
2964
1097
  const candidate = rawData.batch_id;
2965
1098
  if (typeof candidate === "string" && DELEGATED_BATCH_ID_PATTERN.test(candidate)) {
2966
1099
  acceptedBatchId = candidate;
2967
1100
  }
2968
1101
  }
2969
- const { data, requestId } = delegatedEnvelope(envelope, status);
1102
+ const { data, requestId } = delegatedEnvelope(response, status);
2970
1103
  const base = delegatedBatchBase(data, status, requestId);
2971
1104
  if (data.status !== "queued" || base.itemCount !== input.grants.length) {
2972
1105
  throw delegatedResponseError(status, "acceptance data is inconsistent", requestId);
2973
1106
  }
2974
- const responseHeaders = delegatedResponseHeaders(envelope, status, requestId);
2975
- const location = validateDelegatedLocation(requiredDelegatedHeader(envelope, "Location", status, requestId), this.baseUrl, base.batchId, status, requestId);
2976
- return {
1107
+ const responseHeaders = delegatedResponseHeaders(response, status, requestId);
1108
+ const location = validateDelegatedLocation(requiredDelegatedHeader(response, "Location", status, requestId), this.baseUrl, base.batchId, status, requestId);
1109
+ return withEnvelopeMetadata({
2977
1110
  http_status: 202,
2978
1111
  batch_id: base.batchId,
2979
1112
  status: "queued",
@@ -2983,7 +1116,7 @@ export class QURLClient {
2983
1116
  retry_after: responseHeaders.retryAfter,
2984
1117
  location,
2985
1118
  request_id: requestId,
2986
- };
1119
+ }, response.envelope.meta);
2987
1120
  }
2988
1121
  catch (error) {
2989
1122
  if (!dispatched)
@@ -3000,41 +1133,41 @@ export class QURLClient {
3000
1133
  throw clientValidationError("getDelegatedQurlBatch: batchId must match dqb_ plus 22 URL-safe characters");
3001
1134
  }
3002
1135
  validateGetDelegatedQurlBatchOptions(options);
3003
- const envelope = await this.rawRequest("GET", `/v1/delegated-qurl-batches/${encodeURIComponent(batchId)}`, undefined, {
1136
+ const response = await this.rawRequest("GET", `/v1/delegated-qurl-batches/${encodeURIComponent(batchId)}`, undefined, {
3004
1137
  passthroughStatuses: DELEGATED_BATCH_PASSTHROUGH_STATUSES,
3005
1138
  allowEmptyPassthroughBody: true,
3006
1139
  ifNoneMatch: options?.etag,
3007
1140
  retry: false,
3008
1141
  captureResponseHeaders: true,
3009
1142
  });
3010
- const status = envelope.__http_status ?? 0;
1143
+ const status = response.status ?? 0;
3011
1144
  if (status === 304) {
3012
- if (envelope.__http_body_empty !== true || options?.etag === undefined) {
1145
+ if (response.bodyEmpty !== true || options?.etag === undefined) {
3013
1146
  throw delegatedResponseError(status, "304 response is inconsistent");
3014
1147
  }
3015
- const responseHeaders = delegatedResponseHeaders(envelope, status);
1148
+ const responseHeaders = delegatedResponseHeaders(response, status);
3016
1149
  if (responseHeaders.etag !== undefined && responseHeaders.etag !== options.etag) {
3017
1150
  throw delegatedResponseError(status, "304 response changed the ETag");
3018
1151
  }
3019
- return {
1152
+ return withEnvelopeMetadata({
3020
1153
  http_status: 304,
3021
1154
  etag: responseHeaders.etag ?? options.etag,
3022
1155
  ...(responseHeaders.retryAfter === undefined
3023
1156
  ? {}
3024
1157
  : { retry_after: responseHeaders.retryAfter }),
3025
- };
1158
+ }, response.envelope.meta);
3026
1159
  }
3027
1160
  if (status !== 200 && status !== 202) {
3028
1161
  throw delegatedResponseError(status, "read returned an unexpected success status");
3029
1162
  }
3030
- const { data, requestId } = delegatedEnvelope(envelope, status);
1163
+ const { data, requestId } = delegatedEnvelope(response, status);
3031
1164
  const base = delegatedBatchBase(data, status, requestId, batchId);
3032
1165
  if (status === 202) {
3033
1166
  if (data.status !== "queued" && data.status !== "running") {
3034
1167
  throw delegatedResponseError(status, "pending data has a terminal status", requestId);
3035
1168
  }
3036
- const responseHeaders = delegatedResponseHeaders(envelope, status, requestId);
3037
- return {
1169
+ const responseHeaders = delegatedResponseHeaders(response, status, requestId);
1170
+ return withEnvelopeMetadata({
3038
1171
  http_status: 202,
3039
1172
  batch_id: base.batchId,
3040
1173
  status: data.status,
@@ -3043,7 +1176,7 @@ export class QURLClient {
3043
1176
  etag: responseHeaders.etag,
3044
1177
  retry_after: responseHeaders.retryAfter,
3045
1178
  request_id: requestId,
3046
- };
1179
+ }, response.envelope.meta);
3047
1180
  }
3048
1181
  if (data.status !== "succeeded" &&
3049
1182
  data.status !== "partially_failed" &&
@@ -3079,17 +1212,17 @@ export class QURLClient {
3079
1212
  // This is the caller's only copy of the bearer links. Do not discard a
3080
1213
  // valid terminal body because an intermediary removed a cache validator;
3081
1214
  // qurl-service owns the required no-store response policy.
3082
- const cacheControl = envelope.__http_headers?.get("Cache-Control") ?? null;
1215
+ const cacheControl = response.headers?.get("Cache-Control") ?? null;
3083
1216
  if (!hasDelegatedNoStore(cacheControl)) {
3084
1217
  this.log("delegated batch terminal response is missing private, no-store", {
3085
1218
  batch_id: base.batchId,
3086
1219
  });
3087
1220
  }
3088
- const etag = envelope.__http_headers?.get("ETag") ?? undefined;
1221
+ const etag = response.headers?.get("ETag") ?? undefined;
3089
1222
  const terminalEtag = etag !== undefined && etag.length <= MAX_DELEGATED_ETAG && STRONG_ETAG_PATTERN.test(etag)
3090
1223
  ? etag
3091
1224
  : undefined;
3092
- return {
1225
+ return withEnvelopeMetadata({
3093
1226
  http_status: 200,
3094
1227
  batch_id: base.batchId,
3095
1228
  status: data.status,
@@ -3099,20 +1232,20 @@ export class QURLClient {
3099
1232
  results,
3100
1233
  ...(terminalEtag === undefined ? {} : { etag: terminalEtag }),
3101
1234
  request_id: requestId,
3102
- };
1235
+ }, response.envelope.meta);
3103
1236
  }
3104
1237
  /** Read owner-visible metadata for one delegated qURL. */
3105
1238
  async getDelegatedQurl(qurlId) {
3106
1239
  if (typeof qurlId !== "string" || !DELEGATED_QURL_ID_PATTERN.test(qurlId)) {
3107
1240
  throw clientValidationError("getDelegatedQurl: qurlId must match q_ plus 11 lowercase hexadecimal characters");
3108
1241
  }
3109
- const envelope = await this.rawRequest("GET", `/v1/delegated-qurls/${encodeURIComponent(qurlId)}`);
3110
- const status = envelope.__http_status ?? 0;
1242
+ const response = await this.rawRequest("GET", `/v1/delegated-qurls/${encodeURIComponent(qurlId)}`);
1243
+ const status = response.status ?? 0;
3111
1244
  if (status !== 200) {
3112
1245
  throw delegatedResponseError(status, "read returned an unexpected success status");
3113
1246
  }
3114
- const { data, requestId } = delegatedEnvelope(envelope, status);
3115
- return validateDelegatedQurl(data, qurlId, status, requestId);
1247
+ const { data, requestId } = delegatedEnvelope(response, status);
1248
+ return withEnvelopeMetadata(validateDelegatedQurl(data, qurlId, status, requestId), response.envelope.meta);
3116
1249
  }
3117
1250
  /**
3118
1251
  * Revoke one delegated qURL. DELETE is never retried automatically. A 503
@@ -3150,7 +1283,7 @@ export class QURLClient {
3150
1283
  */
3151
1284
  async list(input = {}) {
3152
1285
  const path = appendQuery("/v1/qurls", input, LIST_PARAM_KEYS, "list");
3153
- const { data, meta } = await this.rawRequest("GET", path);
1286
+ const { envelope: { data, meta }, } = await this.rawRequest("GET", path);
3154
1287
  return pageFromMeta({
3155
1288
  // Defensive: map in case API includes nested tokens on list items in the future.
3156
1289
  qurls: (data ?? []).map((raw) => this.mapQurlsField(raw)),
@@ -3331,13 +1464,31 @@ export class QURLClient {
3331
1464
  }
3332
1465
  return this.request("POST", "/v1/resolve", body, options);
3333
1466
  }
1467
+ /** Get the caller's account and credential identity. */
1468
+ async getIdentity() {
1469
+ const identity = await this.request("GET", "/v1/me");
1470
+ if (!identity ||
1471
+ typeof identity !== "object" ||
1472
+ Array.isArray(identity) ||
1473
+ typeof identity.owner_id !== "string" ||
1474
+ !identity.owner_id ||
1475
+ typeof identity.auth_type !== "string" ||
1476
+ !identity.auth_type ||
1477
+ (identity.api_key !== undefined &&
1478
+ (!identity.api_key ||
1479
+ typeof identity.api_key !== "object" ||
1480
+ Array.isArray(identity.api_key)))) {
1481
+ throw unexpectedResponseError("getIdentity: invalid caller identity response");
1482
+ }
1483
+ return identity;
1484
+ }
3334
1485
  /** Get quota and usage information. */
3335
1486
  async getQuota() {
3336
1487
  return this.request("GET", "/v1/quota");
3337
1488
  }
3338
1489
  /** List resources from the `/v1/resources` API. */
3339
1490
  async listResources(input = {}) {
3340
- const { data, meta } = await this.rawRequest("GET", appendQuery("/v1/resources", input, RESOURCE_LIST_PARAM_KEYS, "listResources"));
1491
+ const { envelope: { data, meta }, } = await this.rawRequest("GET", appendQuery("/v1/resources", input, RESOURCE_LIST_PARAM_KEYS, "listResources"));
3341
1492
  return pageFromMeta({ resources: data ?? [] }, meta);
3342
1493
  }
3343
1494
  /**
@@ -3385,6 +1536,31 @@ export class QURLClient {
3385
1536
  });
3386
1537
  return this.request("PATCH", `/v1/resources/${encodeURIComponent(id)}`, validationInput, options);
3387
1538
  }
1539
+ /** Start Stripe Connect onboarding for this publisher. LayerV collects 3%. */
1540
+ async connectPublisherStripe() {
1541
+ const result = await this.request("POST", "/v1/payments/connect");
1542
+ let target;
1543
+ try {
1544
+ target = new URL(result.url);
1545
+ }
1546
+ catch {
1547
+ throw clientValidationError("invalid Stripe connection URL");
1548
+ }
1549
+ if (target.protocol !== "https:" ||
1550
+ target.host !== "connect.stripe.com" ||
1551
+ target.username ||
1552
+ target.password)
1553
+ throw clientValidationError("invalid Stripe connection URL");
1554
+ return result;
1555
+ }
1556
+ /** Read this publisher's connected account and the fixed application fee. */
1557
+ async publisherStripeAccount() {
1558
+ const result = await this.request("GET", "/v1/payments/account");
1559
+ if ((result.account_id !== "" && !/^acct_[A-Za-z0-9]{1,128}$/.test(result.account_id)) ||
1560
+ result.layerv_fee_percent !== 3)
1561
+ throw clientValidationError("invalid Stripe account response");
1562
+ return result;
1563
+ }
3388
1564
  /** Send a publisher-only invitation. Mail delivery does not prove enrollment. */
3389
1565
  async inviteConsumer(id, email) {
3390
1566
  requireResourceCrid(id, "inviteConsumer");
@@ -3395,12 +1571,12 @@ export class QURLClient {
3395
1571
  }
3396
1572
  async listConsumerInvitations(id, input = {}) {
3397
1573
  requireResourceCrid(id, "listConsumerInvitations");
3398
- const { data, meta } = await this.rawRequest("GET", appendQuery(`/v1/resources/${encodeURIComponent(id)}/invitations`, input, ["cursor", "limit"], "listConsumerInvitations"));
1574
+ const { envelope: { data, meta }, } = await this.rawRequest("GET", appendQuery(`/v1/resources/${encodeURIComponent(id)}/invitations`, input, ["cursor", "limit"], "listConsumerInvitations"));
3399
1575
  return pageFromMeta({ invitations: data ?? [] }, meta);
3400
1576
  }
3401
1577
  async listConsumerGrants(id, input = {}) {
3402
1578
  requireResourceCrid(id, "listConsumerGrants");
3403
- const { data, meta } = await this.rawRequest("GET", appendQuery(`/v1/resources/${encodeURIComponent(id)}/consumer-grants`, input, ["cursor", "limit"], "listConsumerGrants"));
1579
+ const { envelope: { data, meta }, } = await this.rawRequest("GET", appendQuery(`/v1/resources/${encodeURIComponent(id)}/consumer-grants`, input, ["cursor", "limit"], "listConsumerGrants"));
3404
1580
  return pageFromMeta({ grants: data ?? [] }, meta);
3405
1581
  }
3406
1582
  async resendConsumerInvitation(id, invitationId, expectedVersion) {
@@ -3536,7 +1712,7 @@ export class QURLClient {
3536
1712
  if (typeof wire.expires_at === "string" && expiresAt === undefined) {
3537
1713
  throw unexpectedResponseError("shareResource: response has invalid expires_at");
3538
1714
  }
3539
- return new ShareLink({
1715
+ return new ShareLink(copyResponseMetadata(data, {
3540
1716
  // Repair transport-only edge whitespace on the one-time credential;
3541
1717
  // ancillary identifiers fail closed instead of being normalized.
3542
1718
  link: data.qurl.trim(),
@@ -3546,7 +1722,7 @@ export class QURLClient {
3546
1722
  expiresAt,
3547
1723
  expiresInSeconds: typeof wire.expires_in_seconds === "number" ? wire.expires_in_seconds : undefined,
3548
1724
  singleUse: typeof wire.single_use === "boolean" ? wire.single_use : undefined,
3549
- });
1725
+ }));
3550
1726
  }
3551
1727
  /** Revoke a specific qURL token on a resource. */
3552
1728
  async revokeResourceQurl(id, qurlId) {
@@ -3572,7 +1748,7 @@ export class QURLClient {
3572
1748
  let request_id;
3573
1749
  let page_size;
3574
1750
  for await (const session of this.paginateAll("listResourceSessions", {}, async ({ cursor }) => {
3575
- const { data, meta } = await this.rawRequest("GET", appendQuery(`/v1/resources/${encodeURIComponent(id)}/sessions`, { paged: "true", cursor }, ["paged", "cursor"], "listResourceSessions"));
1751
+ const { envelope: { data, meta }, } = await this.rawRequest("GET", appendQuery(`/v1/resources/${encodeURIComponent(id)}/sessions`, { paged: "true", cursor }, ["paged", "cursor"], "listResourceSessions"));
3576
1752
  if (meta?.has_more && !meta.next_cursor) {
3577
1753
  throw unexpectedResponseError("Session page has no continuation cursor", {
3578
1754
  requestId: meta.request_id,
@@ -3580,11 +1756,11 @@ export class QURLClient {
3580
1756
  }
3581
1757
  request_id = meta?.request_id;
3582
1758
  page_size = meta?.page_size;
3583
- return { ...meta, sessions: data ?? [] };
1759
+ return withEnvelopeMetadata({ ...meta, sessions: data ?? [] }, meta);
3584
1760
  }, (page) => page.sessions)) {
3585
1761
  sessions.push(session);
3586
1762
  }
3587
- return { sessions, request_id, has_more: false, page_size };
1763
+ return withEnvelopeMetadata({ sessions, request_id, has_more: false, page_size }, { request_id });
3588
1764
  }
3589
1765
  /**
3590
1766
  * End every session page. This is not atomic: a failed page throws after earlier
@@ -3597,8 +1773,8 @@ export class QURLClient {
3597
1773
  let terminated = 0;
3598
1774
  let request_id;
3599
1775
  for await (const count of this.paginateAll("terminateAllResourceSessions", {}, async ({ cursor }) => {
3600
- const { data, meta, __http_status } = await this.rawRequest("DELETE", appendQuery(path, { paged: "true", cursor }, ["paged", "cursor"], "terminateAllResourceSessions"));
3601
- if (__http_status === 204) {
1776
+ const { envelope: { data, meta }, status: httpStatus, } = await this.rawRequest("DELETE", appendQuery(path, { paged: "true", cursor }, ["paged", "cursor"], "terminateAllResourceSessions"));
1777
+ if (httpStatus === 204) {
3602
1778
  throw unexpectedResponseError(`Unexpected 204 No Content from DELETE ${path}; expected response body`, { status: 204, requestId: meta?.request_id });
3603
1779
  }
3604
1780
  if (meta?.has_more && !meta.next_cursor) {
@@ -3606,22 +1782,19 @@ export class QURLClient {
3606
1782
  requestId: meta.request_id,
3607
1783
  });
3608
1784
  }
3609
- if (data?.terminated === undefined) {
3610
- this.log("terminateAllResourceSessions: missing terminated count; defaulting to zero", {
3611
- request_id: meta?.request_id,
3612
- });
3613
- }
3614
- else if (!Number.isSafeInteger(data.terminated) || data.terminated < 0) {
3615
- throw unexpectedResponseError("terminateAllResourceSessions: invalid terminated count", {
3616
- requestId: meta?.request_id,
3617
- });
1785
+ const count = data?.terminated;
1786
+ if (typeof count !== "number" || !Number.isSafeInteger(count) || count < 0) {
1787
+ throw unexpectedResponseError("terminateAllResourceSessions: missing or invalid terminated count; inspect sessions before retrying", { requestId: meta?.request_id });
3618
1788
  }
3619
1789
  request_id = meta?.request_id;
3620
- return { ...meta, counts: [data?.terminated ?? 0] };
1790
+ return withEnvelopeMetadata({ ...meta, counts: [count] }, meta);
3621
1791
  }, (page) => page.counts)) {
3622
1792
  terminated += count;
1793
+ if (!Number.isSafeInteger(terminated)) {
1794
+ throw unexpectedResponseError("terminateAllResourceSessions: total exceeds safe integer range; inspect sessions before retrying", { requestId: request_id });
1795
+ }
3623
1796
  }
3624
- return { terminated, request_id };
1797
+ return withEnvelopeMetadata({ terminated, request_id }, { request_id });
3625
1798
  }
3626
1799
  /** Terminate a specific resource session. */
3627
1800
  async terminateResourceSession(id, sessionId) {
@@ -3631,7 +1804,7 @@ export class QURLClient {
3631
1804
  }
3632
1805
  /** List connector installations. */
3633
1806
  async listConnectorInstallations(input = {}) {
3634
- const { data, meta } = await this.rawRequest("GET", appendQuery("/v1/connectors/installations", input, CONNECTOR_INSTALLATION_LIST_PARAM_KEYS, "listConnectorInstallations"));
1807
+ const { envelope: { data, meta }, } = await this.rawRequest("GET", appendQuery("/v1/connectors/installations", input, CONNECTOR_INSTALLATION_LIST_PARAM_KEYS, "listConnectorInstallations"));
3635
1808
  return pageFromMeta({ installations: data ?? [] }, meta);
3636
1809
  }
3637
1810
  /**
@@ -3689,7 +1862,7 @@ export class QURLClient {
3689
1862
  async listBillingInvoices(input = {}) {
3690
1863
  // This endpoint nests the list in `data.invoices` unlike the other list
3691
1864
  // endpoints, which return `data` as the array directly.
3692
- const { data, meta } = await this.rawRequest("GET", appendQuery("/v1/billing/invoices", input, BILLING_INVOICE_LIST_PARAM_KEYS, "listBillingInvoices"));
1865
+ const { envelope: { data, meta }, } = await this.rawRequest("GET", appendQuery("/v1/billing/invoices", input, BILLING_INVOICE_LIST_PARAM_KEYS, "listBillingInvoices"));
3693
1866
  return pageFromMeta({ invoices: data?.invoices ?? [] }, meta);
3694
1867
  }
3695
1868
  /**
@@ -3714,7 +1887,7 @@ export class QURLClient {
3714
1887
  }
3715
1888
  /** List custom domains. */
3716
1889
  async listDomains(input = {}) {
3717
- const { data, meta } = await this.rawRequest("GET", appendQuery("/v1/domains", input, DOMAIN_LIST_PARAM_KEYS, "listDomains"));
1890
+ const { envelope: { data, meta }, } = await this.rawRequest("GET", appendQuery("/v1/domains", input, DOMAIN_LIST_PARAM_KEYS, "listDomains"));
3718
1891
  return pageFromMeta({ domains: data ?? [] }, meta);
3719
1892
  }
3720
1893
  /**
@@ -3749,7 +1922,7 @@ export class QURLClient {
3749
1922
  }
3750
1923
  /** List webhooks. */
3751
1924
  async listWebhooks(input = {}) {
3752
- const { data, meta } = await this.rawRequest("GET", appendQuery("/v1/webhooks", input, WEBHOOK_LIST_PARAM_KEYS, "listWebhooks"));
1925
+ const { envelope: { data, meta }, } = await this.rawRequest("GET", appendQuery("/v1/webhooks", input, WEBHOOK_LIST_PARAM_KEYS, "listWebhooks"));
3753
1926
  return pageFromMeta({ webhooks: data ?? [] }, meta);
3754
1927
  }
3755
1928
  /**
@@ -3806,7 +1979,7 @@ export class QURLClient {
3806
1979
  /** List delivery attempts for a webhook. */
3807
1980
  async listWebhookDeliveries(id, input = {}) {
3808
1981
  validatePathId(id, "listWebhookDeliveries");
3809
- const { data, meta } = await this.rawRequest("GET", appendQuery(`/v1/webhooks/${encodeURIComponent(id)}/deliveries`, input, WEBHOOK_DELIVERY_LIST_PARAM_KEYS, "listWebhookDeliveries"));
1982
+ const { envelope: { data, meta }, } = await this.rawRequest("GET", appendQuery(`/v1/webhooks/${encodeURIComponent(id)}/deliveries`, input, WEBHOOK_DELIVERY_LIST_PARAM_KEYS, "listWebhookDeliveries"));
3810
1983
  return pageFromMeta({ deliveries: data ?? [] }, meta);
3811
1984
  }
3812
1985
  /**
@@ -3820,6 +1993,87 @@ export class QURLClient {
3820
1993
  validateListAllInput(input, WEBHOOK_DELIVERY_LIST_PARAM_KEYS, "listAllWebhookDeliveries");
3821
1994
  return this.paginateAll("listAllWebhookDeliveries", input, (pageInput) => this.listWebhookDeliveries(id, pageInput), (page) => page.deliveries);
3822
1995
  }
1996
+ /**
1997
+ * Bind an external identity using the provider's trusted OAuth JWT as the
1998
+ * client's bearer credential. Persist the request key before calling, then
1999
+ * securely store the returned one-time plaintext key without logging it.
2000
+ * Retry a lost response within 24 hours with the same key and body.
2001
+ */
2002
+ async createExternalIdentityBinding(input, options) {
2003
+ const method = "createExternalIdentityBinding";
2004
+ requireObjectInput(input, method);
2005
+ requireNoUnknownFields(input, ["provider", "external_id", "display_name", "rotate_existing"], method);
2006
+ if (!["slack", "discord", "teams"].includes(input.provider)) {
2007
+ throw clientValidationError(`${method}: provider must be slack, discord, or teams`);
2008
+ }
2009
+ if (typeof input.external_id !== "string" ||
2010
+ input.external_id.length > 256 ||
2011
+ !/^[\x21-\x22\x24-\x7e]+$/.test(input.external_id)) {
2012
+ throw clientValidationError(`${method}: external_id must contain 1-256 visible ASCII characters without #`);
2013
+ }
2014
+ if (input.display_name !== undefined &&
2015
+ (typeof input.display_name !== "string" ||
2016
+ [...input.display_name].length > 100 ||
2017
+ /[\uD800-\uDFFF]/u.test(input.display_name))) {
2018
+ throw clientValidationError(`${method}: display_name must contain at most 100 Unicode characters`);
2019
+ }
2020
+ requireBooleanIfPresent(input.rotate_existing, `${method}.rotate_existing`);
2021
+ validateRequestOptions(options);
2022
+ if (!options?.idempotencyKey || options.idempotencyKey.length < 32) {
2023
+ throw clientValidationError(`${method}: a persisted idempotencyKey of 32-256 characters is required`);
2024
+ }
2025
+ const body = {
2026
+ provider: input.provider,
2027
+ external_id: input.external_id,
2028
+ ...(input.display_name !== undefined ? { display_name: input.display_name } : {}),
2029
+ ...(input.rotate_existing !== undefined ? { rotate_existing: input.rotate_existing } : {}),
2030
+ };
2031
+ const raw = await this.rawRequest("POST", "/v1/external-identity-bindings", body, {
2032
+ requestOptions: options,
2033
+ generateIdempotencyKey: false,
2034
+ });
2035
+ // This operation returns a flat document, unlike the data/meta endpoints.
2036
+ const result = raw.envelope;
2037
+ const expectedScopes = body.provider === "discord"
2038
+ ? ["qurl:read", "qurl:write"]
2039
+ : ["qurl:read", "qurl:write", "qurl:agent"];
2040
+ if (raw.status !== 201 ||
2041
+ typeof result.binding_id !== "string" ||
2042
+ !/^eib_[A-Za-z0-9]{11}$/.test(result.binding_id) ||
2043
+ result.provider !== body.provider ||
2044
+ result.external_id !== body.external_id ||
2045
+ !result.api_key ||
2046
+ typeof result.api_key !== "object" ||
2047
+ Array.isArray(result.api_key) ||
2048
+ typeof result.api_key.key_id !== "string" ||
2049
+ !/^key_[A-Za-z0-9]{12}$/.test(result.api_key.key_id) ||
2050
+ typeof result.api_key.plaintext !== "string" ||
2051
+ !/^lv_(?:live|test)_[A-Za-z0-9_-]{43}$/.test(result.api_key.plaintext) ||
2052
+ result.api_key.key_prefix !== result.api_key.plaintext.slice(0, 12) ||
2053
+ !Array.isArray(result.scopes) ||
2054
+ result.scopes.length !== expectedScopes.length ||
2055
+ !expectedScopes.every((scope) => result.scopes.includes(scope)) ||
2056
+ !isUtcTimestamp(result.created_at) ||
2057
+ (result.display_name !== undefined &&
2058
+ (typeof result.display_name !== "string" ||
2059
+ [...result.display_name].length > 100 ||
2060
+ /[\uD800-\uDFFF]/u.test(result.display_name)))) {
2061
+ throw unexpectedResponseError(`${method}: invalid binding response; reconcile with the same request key before retrying`);
2062
+ }
2063
+ return withEnvelopeMetadata({
2064
+ binding_id: result.binding_id,
2065
+ provider: result.provider,
2066
+ external_id: result.external_id,
2067
+ ...(result.display_name !== undefined ? { display_name: result.display_name } : {}),
2068
+ api_key: {
2069
+ key_id: result.api_key.key_id,
2070
+ key_prefix: result.api_key.key_prefix,
2071
+ plaintext: result.api_key.plaintext,
2072
+ },
2073
+ scopes: result.scopes,
2074
+ created_at: result.created_at,
2075
+ }, raw.envelope.meta);
2076
+ }
3823
2077
  /** Create a new API key. */
3824
2078
  async createApiKey(input, options) {
3825
2079
  requireObjectInput(input, "createApiKey");
@@ -3858,7 +2112,7 @@ export class QURLClient {
3858
2112
  }
3859
2113
  /** List API keys. */
3860
2114
  async listApiKeys(input = {}) {
3861
- const { data, meta } = await this.rawRequest("GET", appendQuery("/v1/api-keys", input, API_KEY_LIST_PARAM_KEYS, "listApiKeys"));
2115
+ const { envelope: { data, meta }, } = await this.rawRequest("GET", appendQuery("/v1/api-keys", input, API_KEY_LIST_PARAM_KEYS, "listApiKeys"));
3862
2116
  return pageFromMeta({ api_keys: data ?? [] }, meta);
3863
2117
  }
3864
2118
  /**
@@ -3912,19 +2166,19 @@ export class QURLClient {
3912
2166
  * this page and emits a debug log rather than pretending it fetched all pages.
3913
2167
  */
3914
2168
  async listAccessCodes() {
3915
- const { data, meta } = await this.rawRequest("GET", "/v1/access-codes");
2169
+ const { envelope: { data, meta }, } = await this.rawRequest("GET", "/v1/access-codes");
3916
2170
  if (meta?.has_more || meta?.next_cursor) {
3917
2171
  this.log("listAccessCodes: pagination metadata surfaced on unpaginated endpoint", {
3918
2172
  has_more: meta.has_more,
3919
2173
  next_cursor: meta.next_cursor,
3920
2174
  });
3921
2175
  }
3922
- return {
2176
+ return withEnvelopeMetadata({
3923
2177
  access_codes: data ?? [],
3924
2178
  request_id: meta?.request_id,
3925
2179
  has_more: false,
3926
2180
  page_size: meta?.page_size,
3927
- };
2181
+ }, meta);
3928
2182
  }
3929
2183
  /** Revoke an access code. */
3930
2184
  async revokeAccessCode(id) {
@@ -3935,13 +2189,13 @@ export class QURLClient {
3935
2189
  async request(method, path, body, requestOptions) {
3936
2190
  // List methods call rawRequest directly so 204 can degrade to an empty page.
3937
2191
  // Body-returning endpoints declare 200 JSON today, so this path fails closed.
3938
- const { data, __http_status } = await this.rawRequest(method, path, body, {
2192
+ const { envelope: { data, meta }, status: httpStatus, } = await this.rawRequest(method, path, body, {
3939
2193
  requestOptions,
3940
2194
  });
3941
- if (__http_status === 204) {
2195
+ if (httpStatus === 204) {
3942
2196
  throw unexpectedResponseError(`Unexpected 204 No Content from ${method} ${path}; expected response body`);
3943
2197
  }
3944
- return data;
2198
+ return withEnvelopeMetadata(data, meta);
3945
2199
  }
3946
2200
  /**
3947
2201
  * Require the service's exact 204 response with no response bytes exposed by
@@ -3952,9 +2206,9 @@ export class QURLClient {
3952
2206
  const response = await this.rawRequest("DELETE", path, undefined, {
3953
2207
  allowEmptySuccessBody: true,
3954
2208
  });
3955
- if (response.__http_status !== 204 || response.__http_body_empty !== true) {
3956
- const status = response.__http_status ?? 0;
3957
- const bodyShape = response.__http_body_empty ? "with an empty body" : "with response bytes";
2209
+ if (response.status !== 204 || response.bodyEmpty !== true) {
2210
+ const status = response.status ?? 0;
2211
+ const bodyShape = response.bodyEmpty ? "with an empty body" : "with response bytes";
3958
2212
  throw unexpectedResponseError(`Unexpected response from DELETE; expected empty HTTP 204, received HTTP ${status} ${bodyShape}. ` +
3959
2213
  "The delete may already have been applied; reconcile resource state before retrying.", { status });
3960
2214
  }
@@ -4023,7 +2277,8 @@ export class QURLClient {
4023
2277
  // request bound: roughly `timeout * (maxRetries + 1) +
4024
2278
  // sum(retryDelay)`.
4025
2279
  onDispatch?.();
4026
- response = await this.fetchFn(url, {
2280
+ // Native edge fetch requires the global receiver, not this client.
2281
+ response = await this.fetchFn.call(globalThis, url, {
4027
2282
  method,
4028
2283
  headers,
4029
2284
  body: serializedBody,
@@ -4071,14 +2326,14 @@ export class QURLClient {
4071
2326
  }
4072
2327
  let responseBody;
4073
2328
  try {
4074
- responseBody = await readBoundedResponseBody(response);
2329
+ responseBody = await readBoundedResponseBody(response, this.maxResponseBodyBytes);
4075
2330
  }
4076
2331
  catch (err) {
4077
2332
  if (err instanceof ResponseBodyTooLargeError) {
4078
2333
  this.log(`rejected oversized response body from ${response.status}`, {
4079
2334
  status: response.status,
4080
2335
  });
4081
- const detail = `Response body exceeds ${MAX_RESPONSE_BODY_BYTES}-byte limit on HTTP ${response.status}; ` +
2336
+ const detail = `Response body exceeds ${this.maxResponseBodyBytes}-byte limit on HTTP ${response.status}; ` +
4082
2337
  "request a smaller page when listing collections";
4083
2338
  if (response.ok) {
4084
2339
  throw unexpectedResponseError(detail, { status: response.status });
@@ -4195,10 +2450,10 @@ export class QURLClient {
4195
2450
  });
4196
2451
  }
4197
2452
  return {
4198
- data: undefined,
4199
- __http_status: response.status,
4200
- __http_body_empty: responseBody.length === 0,
4201
- __http_headers: captureResponseHeaders ? response.headers : undefined,
2453
+ envelope: { data: undefined },
2454
+ status: response.status,
2455
+ bodyEmpty: responseBody.length === 0,
2456
+ headers: captureResponseHeaders ? response.headers : undefined,
4202
2457
  };
4203
2458
  }
4204
2459
  // Exact no-content helpers must inspect both axes of the success
@@ -4208,20 +2463,20 @@ export class QURLClient {
4208
2463
  (isPassthrough && allowEmptyPassthroughBody)) &&
4209
2464
  responseBody.length === 0) {
4210
2465
  return {
4211
- data: undefined,
4212
- __http_status: response.status,
4213
- __http_body_empty: true,
4214
- __http_headers: captureResponseHeaders ? response.headers : undefined,
2466
+ envelope: { data: undefined },
2467
+ status: response.status,
2468
+ bodyEmpty: true,
2469
+ headers: captureResponseHeaders ? response.headers : undefined,
4215
2470
  };
4216
2471
  }
4217
2472
  try {
4218
2473
  const json = JSON.parse(responseBody);
4219
2474
  return {
4220
- ...json,
4221
- __http_status: response.status,
2475
+ envelope: { ...json },
2476
+ status: response.status,
4222
2477
  // JSON.parse("") throws, so parsed JSON is necessarily non-empty.
4223
- __http_body_empty: false,
4224
- __http_headers: captureResponseHeaders ? response.headers : undefined,
2478
+ bodyEmpty: false,
2479
+ headers: captureResponseHeaders ? response.headers : undefined,
4225
2480
  };
4226
2481
  }
4227
2482
  catch {
@@ -4289,6 +2544,7 @@ export class QURLClient {
4289
2544
  instance: boundedErrorIdentifier(err.instance),
4290
2545
  invalid_fields: boundedInvalidFields(err.invalid_fields),
4291
2546
  request_id: boundedErrorIdentifier(json.meta?.request_id),
2547
+ tombstone: response.status === 410 ? boundedTombstone(json.meta?.tombstone) : undefined,
4292
2548
  retry_after: this.parseRetryAfter(response),
4293
2549
  };
4294
2550
  }
@@ -4335,8 +2591,8 @@ export class QURLClient {
4335
2591
  const trimmed = header.trim();
4336
2592
  if (/^[A-Z][a-z]{2}, \d{2} [A-Z][a-z]{2} \d{4} \d{2}:\d{2}:\d{2} GMT$/.test(trimmed)) {
4337
2593
  const seconds = Math.ceil((Date.parse(trimmed) - Date.now()) / 1000);
4338
- return Number.isSafeInteger(seconds) && seconds > 0 && seconds <= RETRY_AFTER_PARSE_LIMIT_S
4339
- ? seconds
2594
+ return Number.isSafeInteger(seconds)
2595
+ ? Math.min(Math.max(0, seconds), RETRY_AFTER_PARSE_LIMIT_S)
4340
2596
  : undefined;
4341
2597
  }
4342
2598
  if (!/^\d+$/.test(trimmed)) {
@@ -4420,92 +2676,7 @@ export class QURLClient {
4420
2676
  return new NetworkError(`Network failure while reading response body after HTTP ${response.status}`, { cause: err });
4421
2677
  }
4422
2678
  }
4423
- /**
4424
- * A LayerV-protected resource, bound to the client that produced it.
4425
- *
4426
- * Obtained from {@link QURLClient.protectUrl} or {@link QURLClient.resourceByCrid} —
4427
- * not constructed directly. Mint short-lived access links for the resource
4428
- * with {@link createPortal} (qurl-go: `qurl.Resource`).
4429
- */
4430
- export class ProtectedResource {
4431
- // A true private field (not a module-scope WeakMap registry, which
4432
- // CONTRIBUTING.md's dual-build rule bars) so the binding stays on the
4433
- // instance and out of Object.keys/JSON.stringify output.
4434
- #client;
4435
- /** The public resource CRID. */
4436
- crid;
4437
- /** The private URL protected by this resource, when known. */
4438
- targetUrl;
4439
- /**
4440
- * Full server-populated {@link Resource} metadata when the handle came
4441
- * from an API response; `undefined` for {@link QURLClient.resourceByCrid}
4442
- * handles and for the one-call {@link QURLClient.createPortalForUrl} path.
4443
- */
4444
- details;
4445
- constructor(client, id, targetUrl, details) {
4446
- this.#client = client;
4447
- this.crid = id;
4448
- this.targetUrl = targetUrl;
4449
- this.details = details;
4450
- }
4451
- /**
4452
- * Whether `resource` was produced by `client`. A static because the
4453
- * binding lives in a private field that only this class can read; used
4454
- * by {@link QURLClient.createPortal} to refuse handles bound to a
4455
- * different client.
4456
- */
4457
- static isBoundTo(resource, client) {
4458
- return resource.#client === client;
4459
- }
4460
- /**
4461
- * Mint a short-lived qURL link (a portal) for this resource. See
4462
- * {@link QURLClient.createPortal} for the option reference.
4463
- */
4464
- async createPortal(opts = {}, requestOptions) {
4465
- return this.#client.createPortal(this, opts, requestOptions);
4466
- }
4467
- }
4468
- const CONNECTOR_RESOURCE_CONSTRUCTOR_TOKEN = Symbol("validated ConnectorResource");
4469
- /**
4470
- * A validated qURL Connector resource bound to the client that loaded it.
4471
- *
4472
- * Resource identity, reverse-routing identity, and NHP admission identity are
4473
- * distinct server-issued values. Callers must consume each field verbatim and
4474
- * must never derive one from another.
4475
- */
4476
- export class ConnectorResource {
4477
- #client;
4478
- resourcePublicKey;
4479
- /** Required public identifier, verified against the returned resource key. */
4480
- crid;
4481
- connectorRoutingId;
4482
- knockResourceId;
4483
- slug;
4484
- alias;
4485
- /** @internal Instances are returned by QURLClient after wire validation. */
4486
- constructor(client, details, validationToken) {
4487
- if (validationToken !== CONNECTOR_RESOURCE_CONSTRUCTOR_TOKEN) {
4488
- throw clientValidationError("ConnectorResource cannot be constructed directly; load it through QURLClient");
4489
- }
4490
- this.#client = client;
4491
- this.resourcePublicKey = details.resource_id;
4492
- this.crid = details.crid;
4493
- this.connectorRoutingId = details.connector_routing_id;
4494
- this.knockResourceId = details.knock_resource_id;
4495
- this.slug = details.slug;
4496
- this.alias = details.alias ?? undefined;
4497
- }
4498
- /** Mint a short-lived portal for this Connector resource. */
4499
- async createPortal(opts = {}, requestOptions) {
4500
- return this.#client.createPortal(this.crid, opts, requestOptions);
4501
- }
4502
- }
4503
- function sameConsumerPolicy(actual, expected, foundExisting) {
4504
- return (actual?.mode === expected.mode &&
4505
- (actual?.version === expected.version ||
4506
- (foundExisting &&
4507
- Number.isSafeInteger(actual?.version) &&
4508
- Number(actual?.version) > expected.version)) &&
4509
- actual?.contract_id === expected.contract_id);
4510
- }
4511
- //# sourceMappingURL=client.js.map
2679
+ export { isApiKeyRequestScope } from "./client-management.js";
2680
+ export { SIGNED_FRAGMENT_RE } from "./client-portals.js";
2681
+ export { ProtectedResource } from "./client-portals.js";
2682
+ export { ConnectorResource } from "./client-connectors.js";