@layervai/qurl 3.1.1 → 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
@@ -0,0 +1,323 @@
1
+ import { requireCanonicalTargetPath } from "./target-path.js";
2
+ import { ValidationError } from "./errors.js";
3
+ import { TEXT_ENCODER, clientValidationError } from "./client-errors.js";
4
+ import { describeShape, requireBooleanIfPresent, requireMaxLength, requireMaxSessionsInRange, requireNoUnknownFields, requireNonEmptyIfPresent, requireObjectInput, requireResourceNameIfPresent, requireStringArrayElements, requireValidAccessPolicy, requireValidTags, requireValidTargetUrl, } from "./client-validation.js";
5
+ import { CREATE_FIELD_KEYS } from "./client-input-fields.js";
6
+ import { MAX_ALIAS, MAX_ALLOWED_DEVICE_KEYS, MAX_CONSUMER_CONTRACT_ID_BYTES, MAX_CUSTOM_DOMAIN, MAX_DESCRIPTION, MAX_LABEL, MAX_SLUG, } from "./client-limits.js";
7
+ export function validateCreateInput(input) {
8
+ // Untyped-JS guard: surface as ValidationError instead of raw TypeError.
9
+ if (typeof input !== "object" || input === null) {
10
+ throw clientValidationError(`create: input must be an object (got ${input === null ? "null" : typeof input})`);
11
+ }
12
+ // Collect-all: run every validator and aggregate the messages so a
13
+ // caller fixing multiple bad fields sees them all at once instead
14
+ // of fix-re-run-repeat. Matches the documented batchCreate UX
15
+ // ("All failures are collected into a single ValidationError")
16
+ // and mirrors the requireValidTags collect-all pattern.
17
+ const errors = [];
18
+ const collect = (fn) => {
19
+ try {
20
+ fn();
21
+ }
22
+ catch (err) {
23
+ if (err instanceof ValidationError) {
24
+ errors.push(err.detail);
25
+ return;
26
+ }
27
+ throw err;
28
+ }
29
+ };
30
+ collect(() => requireNoUnknownFields(input, CREATE_FIELD_KEYS, "create"));
31
+ const resourceType = input.type;
32
+ if (resourceType !== undefined && typeof resourceType !== "string") {
33
+ errors.push(`type: must be a string (got ${resourceType === null ? "null" : typeof resourceType})`);
34
+ }
35
+ collect(() => {
36
+ if (input.target_url === undefined) {
37
+ if (resourceType === undefined || resourceType === "url") {
38
+ throw clientValidationError("target_url: is required for url qURLs");
39
+ }
40
+ return;
41
+ }
42
+ requireValidTargetUrl(input.target_url);
43
+ });
44
+ collect(() => requireResourceNameIfPresent(input.name));
45
+ collect(() => requireMaxLength(input.label, "label", MAX_LABEL));
46
+ collect(() => requireNonEmptyIfPresent(input.label, "label"));
47
+ collect(() => requireMaxLength(input.custom_domain, "custom_domain", MAX_CUSTOM_DOMAIN));
48
+ collect(() => requireNonEmptyIfPresent(input.custom_domain, "custom_domain"));
49
+ collect(() => requireBooleanIfPresent(input.private, "private"));
50
+ collect(() => requireBooleanIfPresent(input.one_time_use, "one_time_use"));
51
+ collect(() => requireMaxSessionsInRange(input.max_sessions));
52
+ collect(() => requireNonEmptyIfPresent(input.expires_in, "expires_in"));
53
+ collect(() => requireNonEmptyIfPresent(input.session_duration, "session_duration"));
54
+ collect(() => requireValidAccessPolicy(input.access_policy));
55
+ if (errors.length > 0) {
56
+ // Inner `; ` is contractual (paired with batchCreate's outer ` | `).
57
+ // Future field-level validators must not aggregate via `; ` internally.
58
+ throw clientValidationError(errors.join("; "));
59
+ }
60
+ // Intentional omission: duration *grammar* (`expires_in`,
61
+ // `session_duration`) is server-authoritative. Empty strings are
62
+ // rejected up front (see collect() calls above) but unit/whitespace
63
+ // edge cases ("24hh", "1w 3d") surface as a clean server 400 — a
64
+ // client-side regex would just create a drift surface against the
65
+ // server's rego/duration parser.
66
+ //
67
+ // `access_policy` contents (CIDRs, ISO-3166 codes, regex patterns)
68
+ // are pass-through for the same reason. The shape-guard above only
69
+ // catches structural mistakes (string-instead-of-array on list fields)
70
+ // an untyped-JS caller is likely to make.
71
+ }
72
+ export function validateQurlTokenOptions(input) {
73
+ if (input === undefined)
74
+ return;
75
+ requireMaxLength(input.label, "label", MAX_LABEL);
76
+ requireNonEmptyIfPresent(input.label, "label");
77
+ requireNonEmptyIfPresent(input.expires_in, "expires_in");
78
+ requireNonEmptyIfPresent(input.session_duration, "session_duration");
79
+ requireCanonicalTargetPath(input.target_path, "target_path");
80
+ requireBooleanIfPresent(input.one_time_use, "one_time_use");
81
+ requireMaxSessionsInRange(input.max_sessions);
82
+ requireValidAccessPolicy(input.access_policy);
83
+ }
84
+ export function validateResourceQurlUpdateInput(input) {
85
+ requireMaxLength(input.label, "label", MAX_LABEL);
86
+ requireNonEmptyIfPresent(input.label, "label");
87
+ requireNonEmptyIfPresent(input.extend_by, "extend_by");
88
+ requireNonEmptyIfPresent(input.expires_at, "expires_at");
89
+ requireNonEmptyIfPresent(input.session_duration, "session_duration");
90
+ requireMaxSessionsInRange(input.max_sessions);
91
+ requireValidAccessPolicy(input.access_policy);
92
+ if (input.extend_by !== undefined && input.expires_at !== undefined) {
93
+ throw clientValidationError("updateResourceQurl: `extend_by` and `expires_at` are mutually exclusive — provide at most one");
94
+ }
95
+ }
96
+ export function validateResourceWriteFields(input, options = {}) {
97
+ const errors = [];
98
+ const collect = (fn) => {
99
+ try {
100
+ fn();
101
+ }
102
+ catch (err) {
103
+ if (err instanceof ValidationError) {
104
+ errors.push(err.detail);
105
+ return;
106
+ }
107
+ throw err;
108
+ }
109
+ };
110
+ collect(() => requireResourceNameIfPresent(input.name));
111
+ collect(() => {
112
+ if (input.private !== undefined && typeof input.private !== "boolean") {
113
+ throw clientValidationError("private: must be a boolean");
114
+ }
115
+ if (input.consumer_access !== undefined) {
116
+ const policy = input.consumer_access;
117
+ requireObjectInput(policy, "consumer_access");
118
+ const p = policy;
119
+ requireNoUnknownFields(p, ["mode", "version", "contract_id", "payment"], "consumer_access");
120
+ if (!["keys", "invite_only", "email_capture", "review", "contract"].includes(p.mode) ||
121
+ !Number.isSafeInteger(p.version) ||
122
+ p.version < 1) {
123
+ throw clientValidationError("consumer_access: invalid mode or version");
124
+ }
125
+ if (p.mode === "contract") {
126
+ if (typeof p.contract_id !== "string" ||
127
+ !p.contract_id.trim() ||
128
+ TEXT_ENCODER.encode(p.contract_id).length > MAX_CONSUMER_CONTRACT_ID_BYTES) {
129
+ throw clientValidationError("consumer_access: contract mode requires a bounded contract_id");
130
+ }
131
+ }
132
+ else if (p.contract_id !== undefined) {
133
+ throw clientValidationError("consumer_access: contract_id requires contract mode");
134
+ }
135
+ if (p.payment !== undefined) {
136
+ requireObjectInput(p.payment, "consumer_access.payment");
137
+ const payment = p.payment;
138
+ requireNoUnknownFields(payment, ["account_id", "price_id", "access_seconds"], "consumer_access.payment");
139
+ if (p.mode !== "contract" ||
140
+ typeof payment.account_id !== "string" ||
141
+ !/^acct_[A-Za-z0-9]{1,128}$/.test(payment.account_id) ||
142
+ typeof payment.price_id !== "string" ||
143
+ !/^price_[A-Za-z0-9]{1,128}$/.test(payment.price_id) ||
144
+ !Number.isSafeInteger(payment.access_seconds) ||
145
+ Number(payment.access_seconds) < 0 ||
146
+ Number(payment.access_seconds) > 315360000)
147
+ throw clientValidationError("consumer_access.payment: invalid Stripe contract");
148
+ }
149
+ if (input.private === false)
150
+ throw clientValidationError("consumer_access: requires a private resource");
151
+ if (options.requireUrlTarget) {
152
+ if (p.version !== 1 || input.private !== true)
153
+ throw clientValidationError("consumer_access: creation requires private true and version one");
154
+ }
155
+ else if (options.allowAliasClear) {
156
+ const expected = input.expected_consumer_access_version;
157
+ if (!Number.isSafeInteger(expected) ||
158
+ expected < 0 ||
159
+ p.version !== expected + 1 ||
160
+ Object.keys(input).some((key) => key !== "consumer_access" && key !== "expected_consumer_access_version")) {
161
+ throw clientValidationError("consumer_access: update requires its expected version and no metadata changes");
162
+ }
163
+ }
164
+ }
165
+ else if (input.expected_consumer_access_version !== undefined) {
166
+ throw clientValidationError("expected_consumer_access_version: requires consumer_access");
167
+ }
168
+ });
169
+ collect(() => {
170
+ const fields = ["allowed_device_keys_add", "allowed_device_keys_remove"];
171
+ for (const field of fields) {
172
+ const keys = input[field];
173
+ if (keys === undefined)
174
+ continue;
175
+ requireStringArrayElements(keys, field, "updateResource");
176
+ if (keys.length > MAX_ALLOWED_DEVICE_KEYS || new Set(keys).size !== keys.length) {
177
+ throw clientValidationError(`${field}: must contain at most ${MAX_ALLOWED_DEVICE_KEYS} unique keys`);
178
+ }
179
+ for (const key of keys) {
180
+ let canonical = false;
181
+ try {
182
+ canonical = key.length === 44 && atob(key).length === 32 && btoa(atob(key)) === key;
183
+ }
184
+ catch {
185
+ /* Invalid base64 is rejected below. */
186
+ }
187
+ if (!canonical) {
188
+ throw clientValidationError(`${field}: keys must be canonical padded base64 of 32 bytes`);
189
+ }
190
+ }
191
+ if (input.custom_domain !== undefined) {
192
+ throw clientValidationError(`${field}: update separately from custom_domain`);
193
+ }
194
+ }
195
+ const added = new Set(input.allowed_device_keys_add);
196
+ if (input.allowed_device_keys_remove?.some((key) => added.has(key))) {
197
+ throw clientValidationError("allowed device key additions and removals must not overlap");
198
+ }
199
+ });
200
+ const resourceType = input.type;
201
+ if (resourceType !== undefined && typeof resourceType !== "string") {
202
+ errors.push(`type: must be a string (got ${resourceType === null ? "null" : typeof resourceType})`);
203
+ }
204
+ collect(() => {
205
+ if (input.target_url === undefined) {
206
+ if (options.requireUrlTarget && (resourceType === undefined || resourceType === "url")) {
207
+ throw clientValidationError("target_url: is required for url resources");
208
+ }
209
+ return;
210
+ }
211
+ requireValidTargetUrl(input.target_url);
212
+ });
213
+ collect(() => requireMaxLength(input.description, "description", MAX_DESCRIPTION));
214
+ collect(() => requireMaxLength(input.custom_domain, "custom_domain", MAX_CUSTOM_DOMAIN));
215
+ collect(() => {
216
+ if (options.allowCustomDomainClear && input.custom_domain === "")
217
+ return;
218
+ requireNonEmptyIfPresent(input.custom_domain, "custom_domain");
219
+ });
220
+ collect(() => requireValidTags(input.tags));
221
+ collect(() => {
222
+ if (input.alias !== undefined) {
223
+ if (input.alias === null) {
224
+ if (!options.allowAliasClear) {
225
+ throw clientValidationError("alias: must be a string (got null)");
226
+ }
227
+ }
228
+ else {
229
+ requireMaxLength(input.alias, "alias", MAX_ALIAS);
230
+ requireNonEmptyIfPresent(input.alias, "alias");
231
+ }
232
+ }
233
+ });
234
+ collect(() => {
235
+ if (input.slug === undefined)
236
+ return;
237
+ requireMaxLength(input.slug, "slug", MAX_SLUG);
238
+ requireNonEmptyIfPresent(input.slug, "slug");
239
+ });
240
+ collect(() => {
241
+ if (options.validateFindOrCreate &&
242
+ input.find_or_create !== undefined &&
243
+ typeof input.find_or_create !== "boolean") {
244
+ throw clientValidationError(`find_or_create: must be a boolean (got ${describeShape(input.find_or_create)})`);
245
+ }
246
+ });
247
+ collect(() => {
248
+ if (options.validatePreserveHost &&
249
+ input.preserve_host !== undefined &&
250
+ typeof input.preserve_host !== "boolean") {
251
+ throw clientValidationError(`preserve_host: must be a boolean (got ${describeShape(input.preserve_host)})`);
252
+ }
253
+ });
254
+ if (errors.length > 0) {
255
+ throw clientValidationError(errors.join("; "));
256
+ }
257
+ }
258
+ /**
259
+ * Per-entry shape guard for {@link BatchItemResult}. Verifies every
260
+ * non-optional field on the branch of the discriminated union selected
261
+ * by the `success` discriminant. Protects consumers who narrow on
262
+ * `success` and then access `resource_id` / `qurl_link` / `error` as
263
+ * guaranteed-present — if the API ever omits one, we trip the guard
264
+ * rather than returning `undefined` where the type says `string`.
265
+ *
266
+ * Returns `null` when the entry is valid, or a human-readable reason
267
+ * string describing the *specific* field that's missing or wrong. The
268
+ * reason never echoes the entry's values — only field names and shape
269
+ * descriptors — so the caller can safely surface it in error messages
270
+ * that may end up in observability pipelines. Contrast with the outer
271
+ * guard's intentional silence about response-body content for the
272
+ * same info-leak reason.
273
+ */
274
+ export function batchItemResultValidationReason(entry) {
275
+ if (!entry || typeof entry !== "object")
276
+ return "not an object";
277
+ const e = entry;
278
+ // `Number.isInteger` rejects NaN, Infinity, and floats; combined
279
+ // with `>= 0`, the check rules out pathological proxy bodies that
280
+ // would attribute to a useless index. `index` is informational, so
281
+ // this isn't a correctness hazard, but tightening the constraint
282
+ // matches the same posture as the count integer guards above.
283
+ if (!Number.isInteger(e.index) || e.index < 0) {
284
+ return "missing/invalid 'index' (expected non-negative integer)";
285
+ }
286
+ if (e.success === true) {
287
+ if (typeof e.resource_id !== "string") {
288
+ return "success-branch missing required field 'resource_id' (expected string)";
289
+ }
290
+ if (typeof e.qurl_link !== "string") {
291
+ return "success-branch missing required field 'qurl_link' (expected string)";
292
+ }
293
+ if (typeof e.qurl_site !== "string") {
294
+ return "success-branch missing required field 'qurl_site' (expected string)";
295
+ }
296
+ return null;
297
+ }
298
+ if (e.success === false) {
299
+ if (!e.error || typeof e.error !== "object") {
300
+ return "failure-branch missing required field 'error' (expected object)";
301
+ }
302
+ const err = e.error;
303
+ if (typeof err.code !== "string") {
304
+ return "failure-branch missing required field 'error.code' (expected string)";
305
+ }
306
+ if (typeof err.message !== "string") {
307
+ return "failure-branch missing required field 'error.message' (expected string)";
308
+ }
309
+ return null;
310
+ }
311
+ return "missing/invalid 'success' discriminant (expected boolean)";
312
+ }
313
+ export function sameConsumerPolicy(actual, expected, foundExisting) {
314
+ return (actual?.mode === expected.mode &&
315
+ (actual?.version === expected.version ||
316
+ (foundExisting &&
317
+ Number.isSafeInteger(actual?.version) &&
318
+ Number(actual?.version) > expected.version)) &&
319
+ actual?.contract_id === expected.contract_id &&
320
+ actual?.payment?.account_id === expected.payment?.account_id &&
321
+ actual?.payment?.price_id === expected.payment?.price_id &&
322
+ actual?.payment?.access_seconds === expected.payment?.access_seconds);
323
+ }
@@ -0,0 +1,35 @@
1
+ import type { AccessPolicy } from "./types.js";
2
+ export declare function requireMaxLength(value: string | undefined, field: string, max: number): void;
3
+ export declare function requireMaxUtf8Bytes(value: string | undefined, field: string, max: number): void;
4
+ /** Mirror qurl-service ValidatePreviewName: 100 code points; no Cc, Cf, U+2028/9 or padding. */
5
+ export declare function requireResourceNameIfPresent(value: unknown, field?: string): void;
6
+ export declare function requireNonEmptyIfPresent(value: unknown, field: string): void;
7
+ export declare function requireMaxSessionsInRange(value: number | undefined, field?: string): void;
8
+ export declare function requireBooleanIfPresent(value: unknown, field: string): void;
9
+ export declare function requireListLimitInRange(value: unknown, methodName: string): void;
10
+ export declare function containsControlCharacter(value: string): boolean;
11
+ /** Match the API's tag grammar, which permits embedded spaces; values stay in JSON bodies. */
12
+ export declare function requireValidTags(tags: string[] | null | undefined): void;
13
+ export declare function describeShape(value: unknown): string;
14
+ export declare function requireValidAccessPolicy(policy: AccessPolicy | null | undefined): void;
15
+ export declare function requireValidHttpUrl(value: unknown, field: string): void;
16
+ export declare function requireValidTargetUrl(target_url: unknown): void;
17
+ export declare function requireObjectInput(input: unknown, method: string): asserts input is Record<string, unknown>;
18
+ export declare function requireNonEmptyStringField(input: Record<string, unknown>, field: string, method: string): void;
19
+ export declare function requireNonEmptyArrayField(input: Record<string, unknown>, field: string, method: string): void;
20
+ export declare function requireStringArrayElements(value: unknown, field: string, method: string): asserts value is string[];
21
+ export declare function requireAtLeastOneField(input: Record<string, unknown>, fields: readonly string[], method: string): void;
22
+ export declare function requireNoUnknownFields(input: Record<string, unknown>, fields: readonly string[], method: string): void;
23
+ export declare function normalizePatchFields(input: Record<string, unknown>, fields: readonly string[], options?: {
24
+ preserveNullFields?: readonly string[];
25
+ }): Record<string, unknown>;
26
+ export declare function requireConsumerDecision(id: string, version: number, allowNew?: boolean): void;
27
+ /**
28
+ * Portal-verb target URL validation (qurl-go `ProtectURL` parity). On top of
29
+ * the shared scheme/length check, the URL must carry a host and must parse,
30
+ * and embedded credentials (userinfo) are rejected. The added checks never
31
+ * echo the URL — a userinfo URL contains credentials that must stay out of
32
+ * logs and error messages. The REST-shaped compatibility layer keeps its
33
+ * original prefix-only check so its accepted inputs don't change.
34
+ */
35
+ export declare function requirePortalTargetUrl(targetUrl: unknown): void;
@@ -0,0 +1,302 @@
1
+ import { TEXT_ENCODER, clientValidationError } from "./client-errors.js";
2
+ import { MAX_LIST_LIMIT, MAX_MAX_SESSIONS, MAX_RESOURCE_NAME, MAX_TAGS, MAX_TAG_LENGTH, MAX_TARGET_URL, } from "./client-limits.js";
3
+ // CreateQurlRequest.target_url pattern is loose (just a URI) but
4
+ // UpdateQurlRequest.tags pattern is specific — enforce it here.
5
+ const TAG_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9 _-]*$/;
6
+ export function requireMaxLength(value, field, max) {
7
+ if (value === undefined)
8
+ return;
9
+ // Untyped-JS safety: a non-string `value` would silently pass since
10
+ // `(42).length === undefined > max` is `false` and the bad value would
11
+ // sail through to the wire. Match the typeof guard pattern used by
12
+ // requireValidTags / requireValidTargetUrl.
13
+ if (typeof value !== "string") {
14
+ throw clientValidationError(`${field}: must be a string (got ${value === null ? "null" : typeof value})`);
15
+ }
16
+ if (value.length > max) {
17
+ throw clientValidationError(`${field}: must be ${max} characters or fewer (got ${value.length})`);
18
+ }
19
+ }
20
+ export function requireMaxUtf8Bytes(value, field, max) {
21
+ if (value === undefined)
22
+ return;
23
+ if (typeof value !== "string") {
24
+ throw clientValidationError(`${field}: must be a string (got ${value === null ? "null" : typeof value})`);
25
+ }
26
+ const bytes = TEXT_ENCODER.encode(value).byteLength;
27
+ if (bytes > max) {
28
+ throw clientValidationError(`${field}: must be ${max} UTF-8 bytes or fewer (got ${bytes})`);
29
+ }
30
+ }
31
+ /** Mirror qurl-service ValidatePreviewName: 100 code points; no Cc, Cf, U+2028/9 or padding. */
32
+ export function requireResourceNameIfPresent(value, field = "name") {
33
+ if (value === undefined)
34
+ return;
35
+ if (typeof value !== "string" || value.trim() === "") {
36
+ throw clientValidationError(`${field}: must be a non-empty string`);
37
+ }
38
+ if ([...value].length > MAX_RESOURCE_NAME) {
39
+ throw clientValidationError(`${field}: must be ${MAX_RESOURCE_NAME} characters or fewer`);
40
+ }
41
+ if (value.trim() !== value || /[\p{Cc}\p{Cf}\u2028\u2029]/u.test(value)) {
42
+ throw clientValidationError(`${field}: must be one line without hidden characters or surrounding whitespace`);
43
+ }
44
+ }
45
+ export function requireNonEmptyIfPresent(value, field) {
46
+ // Reject `""` so uninitialized form state doesn't reach the wire.
47
+ if (value === "") {
48
+ throw clientValidationError(`${field}: must not be an empty string`);
49
+ }
50
+ }
51
+ export function requireMaxSessionsInRange(value, field = "max_sessions") {
52
+ if (value === undefined)
53
+ return;
54
+ if (!Number.isInteger(value) || value < 0 || value > MAX_MAX_SESSIONS) {
55
+ throw clientValidationError(`${field}: must be an integer between 0 and ${MAX_MAX_SESSIONS} (got ${value})`);
56
+ }
57
+ }
58
+ export function requireBooleanIfPresent(value, field) {
59
+ if (value === undefined)
60
+ return;
61
+ if (typeof value !== "boolean") {
62
+ throw clientValidationError(`${field}: must be a boolean (got ${describeShape(value)})`);
63
+ }
64
+ }
65
+ export function requireListLimitInRange(value, methodName) {
66
+ if (value === undefined || value === null)
67
+ return;
68
+ // All current paginated OpenAPI query params share 1..100. If a future
69
+ // endpoint diverges, add a per-endpoint bound rather than changing this
70
+ // shared guard silently.
71
+ if (typeof value !== "number" ||
72
+ !Number.isInteger(value) ||
73
+ value < 1 ||
74
+ value > MAX_LIST_LIMIT) {
75
+ const rendered = typeof value === "string" ? JSON.stringify(value) : String(value);
76
+ throw clientValidationError(`${methodName}: limit: must be an integer between 1 and ${MAX_LIST_LIMIT} (got ${rendered})`);
77
+ }
78
+ }
79
+ export function containsControlCharacter(value) {
80
+ return Array.from(value).some((character) => {
81
+ const codePoint = character.codePointAt(0);
82
+ return codePoint <= 0x1f || (codePoint >= 0x7f && codePoint <= 0x9f);
83
+ });
84
+ }
85
+ /** Match the API's tag grammar, which permits embedded spaces; values stay in JSON bodies. */
86
+ export function requireValidTags(tags) {
87
+ // null tolerance for untyped-JS callers — treated as "don't touch."
88
+ // Duplicates pass through to the server (authoritative on dedupe policy).
89
+ if (tags === undefined || tags === null)
90
+ return;
91
+ if (!Array.isArray(tags)) {
92
+ throw clientValidationError(`tags: must be an array of strings (got ${typeof tags})`);
93
+ }
94
+ if (tags.length > MAX_TAGS) {
95
+ throw clientValidationError(`tags: max ${MAX_TAGS} items allowed (got ${tags.length})`);
96
+ }
97
+ const errors = [];
98
+ for (let i = 0; i < tags.length; i++) {
99
+ const tag = tags[i];
100
+ // Per-element type guard for untyped-JS callers. Without this, a
101
+ // non-string element would silently pass (`.length` on a number is
102
+ // `undefined`; `TAG_PATTERN.test(42)` coerces to `"42"`) or TypeError
103
+ // (`null.length`).
104
+ if (typeof tag !== "string") {
105
+ errors.push(`tags[${i}]: must be a string (got ${tag === null ? "null" : typeof tag})`);
106
+ continue;
107
+ }
108
+ if (tag.length < 1 || tag.length > MAX_TAG_LENGTH) {
109
+ errors.push(`tags[${i}]: must be 1-${MAX_TAG_LENGTH} characters (got ${tag.length})`);
110
+ continue;
111
+ }
112
+ if (!TAG_PATTERN.test(tag)) {
113
+ errors.push(`tags[${i}]: must start with an alphanumeric and contain only letters, numbers, spaces, underscores, or hyphens`);
114
+ }
115
+ }
116
+ if (errors.length > 0) {
117
+ throw clientValidationError(errors.join("; "));
118
+ }
119
+ }
120
+ export function describeShape(value) {
121
+ if (value === null)
122
+ return "null";
123
+ if (Array.isArray(value))
124
+ return "array";
125
+ return typeof value;
126
+ }
127
+ const ACCESS_POLICY_LIST_FIELDS = [
128
+ "ip_allowlist",
129
+ "ip_denylist",
130
+ "geo_allowlist",
131
+ "geo_denylist",
132
+ ];
133
+ const ACCESS_POLICY_STRING_FIELDS = [
134
+ "user_agent_allow_regex",
135
+ "user_agent_deny_regex",
136
+ ];
137
+ const AI_AGENT_POLICY_LIST_FIELDS = [
138
+ "deny_categories",
139
+ "allow_categories",
140
+ ];
141
+ export function requireValidAccessPolicy(policy) {
142
+ // null/undefined → "don't touch". Server is authoritative on CIDR /
143
+ // ISO-3166 / regex grammar; this guard catches only the shape errors
144
+ // an untyped-JS caller is likely to make (e.g. `geo_allowlist: "US"`
145
+ // instead of `["US"]`) so they get a structured ValidationError
146
+ // instead of a server 400 they have to debug.
147
+ if (policy === undefined || policy === null)
148
+ return;
149
+ if (typeof policy !== "object" || Array.isArray(policy)) {
150
+ throw clientValidationError(`access_policy: must be an object (got ${describeShape(policy)})`);
151
+ }
152
+ for (const field of ACCESS_POLICY_LIST_FIELDS) {
153
+ const value = policy[field];
154
+ if (value === undefined)
155
+ continue;
156
+ if (!Array.isArray(value)) {
157
+ throw clientValidationError(`access_policy.${field}: must be an array (got ${describeShape(value)})`);
158
+ }
159
+ }
160
+ for (const field of ACCESS_POLICY_STRING_FIELDS) {
161
+ const value = policy[field];
162
+ if (value === undefined)
163
+ continue;
164
+ if (typeof value !== "string") {
165
+ throw clientValidationError(`access_policy.${field}: must be a string (got ${describeShape(value)})`);
166
+ }
167
+ }
168
+ const aip = policy.ai_agent_policy;
169
+ if (aip !== undefined) {
170
+ if (aip === null || typeof aip !== "object" || Array.isArray(aip)) {
171
+ throw clientValidationError(`access_policy.ai_agent_policy: must be an object (got ${describeShape(aip)})`);
172
+ }
173
+ if (aip.block_all !== undefined && typeof aip.block_all !== "boolean") {
174
+ throw clientValidationError(`access_policy.ai_agent_policy.block_all: must be a boolean (got ${describeShape(aip.block_all)})`);
175
+ }
176
+ for (const field of AI_AGENT_POLICY_LIST_FIELDS) {
177
+ const value = aip[field];
178
+ if (value === undefined)
179
+ continue;
180
+ if (!Array.isArray(value)) {
181
+ throw clientValidationError(`access_policy.ai_agent_policy.${field}: must be an array (got ${describeShape(value)})`);
182
+ }
183
+ }
184
+ }
185
+ }
186
+ // `format: uri` in the OpenAPI spec allows schemes the SDK doesn't
187
+ // usefully support (`ftp://`, `file://`, `javascript:`, …). This is a
188
+ // cheap client-side sanity check — the server is still the
189
+ // authoritative validator (e.g. it rejects localhost, cloud metadata,
190
+ // and private-range hosts; the SDK doesn't need to duplicate that).
191
+ const ALLOWED_URL_SCHEMES = ["http://", "https://"];
192
+ export function requireValidHttpUrl(value, field) {
193
+ // Three checks in priority order: type guard, scheme, length.
194
+ // Stop at the first failure so a non-string URL doesn't
195
+ // produce both "must be a string" AND "must be ≤ 2048 characters"
196
+ // (the length check on a non-string would also typeof-fail and
197
+ // duplicate the type message). The collect-all loop in
198
+ // validateCreateInput runs each FIELD's validator independently — within
199
+ // a single URL field itself we want fail-fast.
200
+ if (typeof value !== "string") {
201
+ throw clientValidationError(`${field}: must be a string (got ${value === null ? "null" : typeof value})`);
202
+ }
203
+ if (!ALLOWED_URL_SCHEMES.some((scheme) => value.startsWith(scheme))) {
204
+ // Truncate to keep the error message compact and avoid
205
+ // pathologically long schemes from filling logs.
206
+ const repr = JSON.stringify(value).slice(0, 40);
207
+ throw clientValidationError(`${field}: must start with http:// or https:// (got ${repr})`);
208
+ }
209
+ if (value.length > MAX_TARGET_URL) {
210
+ throw clientValidationError(`${field}: must be ${MAX_TARGET_URL} characters or fewer (got ${value.length})`);
211
+ }
212
+ }
213
+ export function requireValidTargetUrl(target_url) {
214
+ requireValidHttpUrl(target_url, "target_url");
215
+ }
216
+ export function requireObjectInput(input, method) {
217
+ if (typeof input !== "object" || input === null || Array.isArray(input)) {
218
+ throw clientValidationError(`${method}: input must be an object (got ${input === null ? "null" : typeof input})`);
219
+ }
220
+ }
221
+ export function requireNonEmptyStringField(input, field, method) {
222
+ const value = input[field];
223
+ if (typeof value !== "string" || value.length === 0) {
224
+ throw clientValidationError(`${method}: ${field} must be a non-empty string`);
225
+ }
226
+ }
227
+ export function requireNonEmptyArrayField(input, field, method) {
228
+ const value = input[field];
229
+ if (!Array.isArray(value) || value.length === 0) {
230
+ throw clientValidationError(`${method}: ${field} must be a non-empty array`);
231
+ }
232
+ }
233
+ export function requireStringArrayElements(value, field, method) {
234
+ if (!Array.isArray(value)) {
235
+ throw clientValidationError(`${method}: ${field} must be a non-empty array`);
236
+ }
237
+ value.forEach((entry, index) => {
238
+ if (typeof entry !== "string" || entry.length === 0) {
239
+ throw clientValidationError(`${method}: ${field}[${index}] must be a non-empty string`);
240
+ }
241
+ });
242
+ }
243
+ export function requireAtLeastOneField(input, fields, method) {
244
+ if (!fields.some((field) => input[field] !== undefined)) {
245
+ throw clientValidationError(`${method}: at least one field (${fields.join(", ")}) must be provided`);
246
+ }
247
+ }
248
+ export function requireNoUnknownFields(input, fields, method) {
249
+ const unknown = Object.keys(input).filter((field) => !fields.includes(field));
250
+ if (unknown.length > 0) {
251
+ const rendered = unknown.map((field) => `"${field}"`).join(", ");
252
+ const label = unknown.length === 1 ? "unknown field" : "unknown fields";
253
+ throw clientValidationError(`${method}: ${label} ${rendered}`);
254
+ }
255
+ }
256
+ export function normalizePatchFields(input, fields, options = {}) {
257
+ const preserveNull = new Set(options.preserveNullFields ?? []);
258
+ const normalized = {};
259
+ for (const key of fields) {
260
+ const value = input[key];
261
+ if (value !== undefined && (value !== null || preserveNull.has(key))) {
262
+ normalized[key] = value;
263
+ }
264
+ }
265
+ return normalized;
266
+ }
267
+ export function requireConsumerDecision(id, version, allowNew = false) {
268
+ if (typeof id !== "string" ||
269
+ (!/^[a-f0-9]{64}$/.test(id) && !(allowNew && id === "new")) ||
270
+ !Number.isSafeInteger(version) ||
271
+ version < (allowNew ? 0 : 1))
272
+ throw clientValidationError("consumer decision: invalid id or expected version");
273
+ }
274
+ /**
275
+ * Portal-verb target URL validation (qurl-go `ProtectURL` parity). On top of
276
+ * the shared scheme/length check, the URL must carry a host and must parse,
277
+ * and embedded credentials (userinfo) are rejected. The added checks never
278
+ * echo the URL — a userinfo URL contains credentials that must stay out of
279
+ * logs and error messages. The REST-shaped compatibility layer keeps its
280
+ * original prefix-only check so its accepted inputs don't change.
281
+ */
282
+ export function requirePortalTargetUrl(targetUrl) {
283
+ requireValidTargetUrl(targetUrl);
284
+ const url = targetUrl; // requireValidTargetUrl only returns for http(s) strings
285
+ // WHATWG URL normalizes away an empty authority ("https:///x" parses
286
+ // with host "x"), so the host-presence check runs on the raw string,
287
+ // mirroring qurl-go's url.Parse which rejects authority-less URLs.
288
+ const afterScheme = url.slice(url.indexOf("://") + 3);
289
+ if (afterScheme === "" || "/?#".includes(afterScheme[0])) {
290
+ throw clientValidationError("target_url: must include a host");
291
+ }
292
+ let parsed;
293
+ try {
294
+ parsed = new URL(url);
295
+ }
296
+ catch {
297
+ throw clientValidationError("target_url: malformed URL");
298
+ }
299
+ if (parsed.username !== "" || parsed.password !== "") {
300
+ throw clientValidationError("target_url: must not include embedded credentials (userinfo)");
301
+ }
302
+ }