@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
@@ -0,0 +1,180 @@
1
+ import { getResponseMetadata } from "./response-metadata.js";
2
+ import { ConnectorResourceOutcomeUnknownError, ERROR_CODE_CONNECTOR_RESOURCE_REVOKED, QURLError, RuntimeError, } from "./errors.js";
3
+ import { cridKeyMatches } from "./crid.js";
4
+ import { clientValidationError, unexpectedResponseError } from "./client-errors.js";
5
+ import { containsControlCharacter } from "./client-validation.js";
6
+ // Keep these identity contracts aligned with qurl-go and the public API.
7
+ export const CONNECTOR_SLUG_PATTERN = /^[a-z][a-z0-9-]{1,62}[a-z0-9]$/;
8
+ // Current service schemas intentionally share this grammar, but keep the
9
+ // mutable alias contract separate from the immutable lookup identity.
10
+ const CONNECTOR_ALIAS_PATTERN = /^[a-z][a-z0-9-]{1,62}[a-z0-9]$/;
11
+ const CONNECTOR_RESOURCE_ID_PATTERN = /^[A-Za-z0-9_-]{122}$/;
12
+ const CONNECTOR_RESOURCE_ID_DER_PREFIX = "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE";
13
+ const CONNECTOR_ROUTING_ID_PATTERN = /^c-[a-z2-7]{51}[aq]$/;
14
+ export function requireConnectorSlug(slug, method) {
15
+ if (typeof slug !== "string" || !CONNECTOR_SLUG_PATTERN.test(slug)) {
16
+ throw clientValidationError(`${method}: slug must be 3-64 lowercase alphanumeric or hyphen characters, start with a letter, and end alphanumeric`);
17
+ }
18
+ }
19
+ export function classifyConnectorMutationFailure(operation, error) {
20
+ if (!(error instanceof QURLError)) {
21
+ throw new ConnectorResourceOutcomeUnknownError(new RuntimeError("Unexpected failure after Connector mutation dispatch", { cause: error }));
22
+ }
23
+ // Most authoritative 4xx responses prove that the mutation was rejected.
24
+ // bootstrap_key_consumed is the exception: the service returns it from the
25
+ // consume phase after the resource may have been bound or found.
26
+ // Callers perform argument/runtime preflight outside their mutation try
27
+ // blocks; every other surfaced failure therefore follows dispatch or a
28
+ // nominal success whose resource contract could not be consumed, so callers
29
+ // must reconcile before retrying. Ensure's exact 201 + valid resource but
30
+ // missing found_existing is handled after this classifier: the row proves
31
+ // the selected resource, while only its required metadata is unavailable.
32
+ if (operation === "ensure" && error.status === 409 && error.code === "bootstrap_key_consumed") {
33
+ throw new ConnectorResourceOutcomeUnknownError(error);
34
+ }
35
+ if (error.status >= 400 && error.status < 500) {
36
+ throw error;
37
+ }
38
+ throw new ConnectorResourceOutcomeUnknownError(error);
39
+ }
40
+ function decodeCanonicalBase64Url(value) {
41
+ if (!CONNECTOR_RESOURCE_ID_PATTERN.test(value) ||
42
+ !value.startsWith(CONNECTOR_RESOURCE_ID_DER_PREFIX)) {
43
+ return undefined;
44
+ }
45
+ try {
46
+ const padded = value.replace(/-/g, "+").replace(/_/g, "/") + "==";
47
+ const binary = atob(padded);
48
+ const bytes = Uint8Array.from(binary, (character) => character.charCodeAt(0));
49
+ const canonical = btoa(String.fromCharCode(...bytes))
50
+ .replace(/\+/g, "-")
51
+ .replace(/\//g, "_")
52
+ .replace(/=+$/, "");
53
+ return canonical === value ? bytes : undefined;
54
+ }
55
+ catch {
56
+ return undefined;
57
+ }
58
+ }
59
+ export function requireConnectorSubtleCrypto(method) {
60
+ if (!globalThis.crypto?.subtle) {
61
+ throw new RuntimeError(`${method}: requires the Web Crypto SubtleCrypto API`);
62
+ }
63
+ }
64
+ async function importsAsConnectorPublicKey(der) {
65
+ try {
66
+ const keyData = new ArrayBuffer(der.byteLength);
67
+ new Uint8Array(keyData).set(der);
68
+ await globalThis.crypto.subtle.importKey("spki", keyData, { name: "ECDSA", namedCurve: "P-256" }, false, ["verify"]);
69
+ return true;
70
+ }
71
+ catch {
72
+ return false;
73
+ }
74
+ }
75
+ export async function verifyResourceIdentity(resource, method, expectedCrid) {
76
+ const errorOptions = { requestId: getResponseMetadata(resource)?.requestId };
77
+ if (!resource || typeof resource.resource_id !== "string") {
78
+ throw unexpectedResponseError(`${method}: response has missing or invalid resource_id`, errorOptions);
79
+ }
80
+ const resourceKey = decodeCanonicalBase64Url(resource.resource_id);
81
+ requireConnectorSubtleCrypto(method);
82
+ if (!resourceKey || !(await importsAsConnectorPublicKey(resourceKey))) {
83
+ throw unexpectedResponseError(`${method}: response has missing or invalid resource_id`, errorOptions);
84
+ }
85
+ if (expectedCrid !== undefined && resource.crid !== expectedCrid) {
86
+ throw unexpectedResponseError(`${method}: response crid does not match the request`, errorOptions);
87
+ }
88
+ if (!(await cridKeyMatches(resource.crid, resourceKey))) {
89
+ throw unexpectedResponseError(`${method}: response has missing, invalid, or public-key-mismatched crid`, errorOptions);
90
+ }
91
+ }
92
+ export async function parseConnectorResource(client, value, method, expectation = {}) {
93
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
94
+ throw unexpectedResponseError(`${method}: response is missing connector resource data`);
95
+ }
96
+ const resource = value;
97
+ await verifyResourceIdentity(resource, method, expectation.crid);
98
+ if (typeof resource.connector_routing_id !== "string" ||
99
+ !CONNECTOR_ROUTING_ID_PATTERN.test(resource.connector_routing_id)) {
100
+ throw unexpectedResponseError(`${method}: response has missing or invalid connector_routing_id`);
101
+ }
102
+ if (typeof resource.knock_resource_id !== "string" ||
103
+ resource.knock_resource_id.trim() === "" ||
104
+ resource.knock_resource_id.trim() !== resource.knock_resource_id ||
105
+ containsControlCharacter(resource.knock_resource_id)) {
106
+ throw unexpectedResponseError(`${method}: response has missing or invalid knock_resource_id`);
107
+ }
108
+ if (resource.resource_id === resource.knock_resource_id ||
109
+ resource.connector_routing_id === resource.knock_resource_id) {
110
+ throw unexpectedResponseError(`${method}: response cross-wires resource identity, routing, and admission values`);
111
+ }
112
+ if (resource.type !== "tunnel") {
113
+ throw unexpectedResponseError(`${method}: response resource type is not tunnel`);
114
+ }
115
+ if (typeof resource.slug !== "string" || !CONNECTOR_SLUG_PATTERN.test(resource.slug)) {
116
+ throw unexpectedResponseError(`${method}: response has missing or invalid slug`);
117
+ }
118
+ if (expectation.slug !== undefined && resource.slug !== expectation.slug) {
119
+ throw unexpectedResponseError(`${method}: response slug does not match the request`);
120
+ }
121
+ if (resource.alias !== undefined &&
122
+ resource.alias !== null &&
123
+ (typeof resource.alias !== "string" || !CONNECTOR_ALIAS_PATTERN.test(resource.alias))) {
124
+ // The public API intentionally gives mutable aliases and immutable slugs the
125
+ // same canonical wire grammar (domain aliasPattern/slugPattern). Keeping
126
+ // response validation aligned avoids accepting a row the service itself
127
+ // could not create or update.
128
+ throw unexpectedResponseError(`${method}: response has invalid alias`);
129
+ }
130
+ if (resource.status === "revoked") {
131
+ if (expectation.revokedIsLifecycleError !== true) {
132
+ throw unexpectedResponseError(`${method}: active-only operation returned a revoked connector resource`);
133
+ }
134
+ throw new QURLError({
135
+ status: 0,
136
+ code: ERROR_CODE_CONNECTOR_RESOURCE_REVOKED,
137
+ title: "Connector Resource Revoked",
138
+ detail: `${method}: qURL Connector resource is revoked`,
139
+ });
140
+ }
141
+ if (resource.status !== "active") {
142
+ throw unexpectedResponseError(`${method}: response has invalid resource status`);
143
+ }
144
+ return new ConnectorResource(client, resource, CONNECTOR_RESOURCE_CONSTRUCTOR_TOKEN);
145
+ }
146
+ const CONNECTOR_RESOURCE_CONSTRUCTOR_TOKEN = Symbol("validated ConnectorResource");
147
+ /**
148
+ * A validated qURL Connector resource bound to the client that loaded it.
149
+ *
150
+ * Resource identity, reverse-routing identity, and NHP admission identity are
151
+ * distinct server-issued values. Callers must consume each field verbatim and
152
+ * must never derive one from another.
153
+ */
154
+ export class ConnectorResource {
155
+ #client;
156
+ resourcePublicKey;
157
+ /** Required public identifier, verified against the returned resource key. */
158
+ crid;
159
+ connectorRoutingId;
160
+ knockResourceId;
161
+ slug;
162
+ alias;
163
+ /** @internal Instances are returned by QURLClient after wire validation. */
164
+ constructor(client, details, validationToken) {
165
+ if (validationToken !== CONNECTOR_RESOURCE_CONSTRUCTOR_TOKEN) {
166
+ throw clientValidationError("ConnectorResource cannot be constructed directly; load it through QURLClient");
167
+ }
168
+ this.#client = client;
169
+ this.resourcePublicKey = details.resource_id;
170
+ this.crid = details.crid;
171
+ this.connectorRoutingId = details.connector_routing_id;
172
+ this.knockResourceId = details.knock_resource_id;
173
+ this.slug = details.slug;
174
+ this.alias = details.alias ?? undefined;
175
+ }
176
+ /** Mint a short-lived portal for this Connector resource. */
177
+ async createPortal(opts = {}, requestOptions) {
178
+ return this.#client.createPortal(this.crid, opts, requestOptions);
179
+ }
180
+ }
@@ -0,0 +1,30 @@
1
+ import type { CreateDelegatedQurlBatchInput, CreateDelegatedQurlBatchOptions, DelegatedQurl, DelegatedQurlBatchItemResult, GetDelegatedQurlBatchOptions } from "./types.js";
2
+ import { ValidationError } from "./errors.js";
3
+ import type { HttpResponse } from "./client-http-types.js";
4
+ export declare const DELEGATED_BATCH_ID_PATTERN: RegExp;
5
+ export declare const DELEGATED_QURL_ID_PATTERN: RegExp;
6
+ export declare const STRONG_ETAG_PATTERN: RegExp;
7
+ export declare function validateCreateDelegatedQurlBatchInput(input: unknown): asserts input is CreateDelegatedQurlBatchInput;
8
+ export declare function validateDelegatedBatchCreateOptions(options: unknown): asserts options is CreateDelegatedQurlBatchOptions;
9
+ export declare function validateGetDelegatedQurlBatchOptions(options: unknown): asserts options is GetDelegatedQurlBatchOptions | undefined;
10
+ export declare function delegatedResponseError(status: number, detail: string, requestId?: string): ValidationError;
11
+ export declare function delegatedEnvelope(response: HttpResponse<unknown>, status: number): {
12
+ data: Record<string, unknown>;
13
+ requestId: string;
14
+ };
15
+ export declare function requiredDelegatedHeader(response: HttpResponse<unknown>, name: string, status: number, requestId?: string): string;
16
+ export declare function hasDelegatedNoStore(value: string | null): boolean;
17
+ export declare function delegatedResponseHeaders(response: HttpResponse<unknown>, status: number, requestId?: string): {
18
+ etag?: string;
19
+ retryAfter?: number;
20
+ };
21
+ export declare function isUtcTimestamp(value: unknown): value is string;
22
+ export declare function delegatedBatchBase(data: Record<string, unknown>, status: number, requestId: string, expectedBatchId?: string): {
23
+ batchId: string;
24
+ itemCount: number;
25
+ submittedAt: string;
26
+ };
27
+ export declare function validateDelegatedLocation(value: string, baseUrl: string, batchId: string, status: number, requestId: string): string;
28
+ export declare function validateDelegatedQurl(value: unknown, expectedQurlId: string, status: number, requestId: string): DelegatedQurl;
29
+ export declare function validateDelegatedBatchResults(value: unknown, itemCount: number, batchStatus: "succeeded" | "partially_failed" | "failed", status: number, requestId: string): DelegatedQurlBatchItemResult[];
30
+ export declare function classifyDelegatedBatchCreateFailure(error: unknown, batchId?: string): never;
@@ -0,0 +1,295 @@
1
+ import { DelegatedBatchOutcomeUnknownError, QURLError, RuntimeError, } from "./errors.js";
2
+ import { clientValidationError, unexpectedResponseError } from "./client-errors.js";
3
+ import { describeShape, requireBooleanIfPresent, requireMaxSessionsInRange, requireMaxUtf8Bytes, requireNoUnknownFields, requireValidAccessPolicy, } from "./client-validation.js";
4
+ import { DELEGATED_BATCH_CREATE_OPTION_KEYS, DELEGATED_BATCH_GET_OPTION_KEYS, DELEGATED_BATCH_GRANT_KEYS, DELEGATED_BATCH_INPUT_KEYS, } from "./client-input-fields.js";
5
+ import { MAX_DELEGATED_BATCH_ITEMS, MAX_DELEGATED_ETAG, MAX_DELEGATED_MINT_CAPABILITY, MAX_IDEMPOTENCY_KEY, MAX_LABEL, RETRY_AFTER_PARSE_LIMIT_S, } from "./client-limits.js";
6
+ const MIN_DELEGATED_BATCH_IDEMPOTENCY_KEY = 32;
7
+ export const DELEGATED_BATCH_ID_PATTERN = /^dqb_[A-Za-z0-9_-]{22}$/;
8
+ export const DELEGATED_QURL_ID_PATTERN = /^q_[0-9a-f]{11}$/;
9
+ export const STRONG_ETAG_PATTERN = /^"[\x21\x23-\x7e]+"$/;
10
+ const UTC_TIMESTAMP_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z$/;
11
+ export function validateCreateDelegatedQurlBatchInput(input) {
12
+ if (typeof input !== "object" || input === null || Array.isArray(input)) {
13
+ throw clientValidationError(`createDelegatedQurlBatch: input must be an object (got ${describeShape(input)})`);
14
+ }
15
+ const typed = input;
16
+ requireNoUnknownFields(typed, DELEGATED_BATCH_INPUT_KEYS, "createDelegatedQurlBatch");
17
+ if (typeof typed.mint_capability !== "string" ||
18
+ typed.mint_capability.length < 1 ||
19
+ typed.mint_capability.length > MAX_DELEGATED_MINT_CAPABILITY) {
20
+ throw clientValidationError(`createDelegatedQurlBatch: mint_capability must be a string of 1-${MAX_DELEGATED_MINT_CAPABILITY} characters`);
21
+ }
22
+ if (!Array.isArray(typed.grants) ||
23
+ typed.grants.length < 1 ||
24
+ typed.grants.length > MAX_DELEGATED_BATCH_ITEMS) {
25
+ throw clientValidationError(`createDelegatedQurlBatch: grants must contain 1-${MAX_DELEGATED_BATCH_ITEMS} items`);
26
+ }
27
+ for (const [index, grant] of typed.grants.entries()) {
28
+ const field = `createDelegatedQurlBatch: grants[${index}]`;
29
+ if (typeof grant !== "object" || grant === null || Array.isArray(grant)) {
30
+ throw clientValidationError(`${field}: must be an object (got ${describeShape(grant)})`);
31
+ }
32
+ requireNoUnknownFields(grant, DELEGATED_BATCH_GRANT_KEYS, field);
33
+ requireMaxUtf8Bytes(grant.label, `${field}.label`, MAX_LABEL);
34
+ requireBooleanIfPresent(grant.one_time_use, `${field}.one_time_use`);
35
+ requireMaxSessionsInRange(grant.max_sessions, `${field}.max_sessions`);
36
+ for (const durationField of ["expires_in", "session_duration"]) {
37
+ const value = grant[durationField];
38
+ if (value !== undefined && (typeof value !== "string" || value.length === 0)) {
39
+ throw clientValidationError(`${field}.${durationField}: must be a non-empty string`);
40
+ }
41
+ }
42
+ if (grant.access_policy !== undefined) {
43
+ requireValidAccessPolicy(grant.access_policy);
44
+ }
45
+ }
46
+ }
47
+ export function validateDelegatedBatchCreateOptions(options) {
48
+ if (typeof options !== "object" || options === null || Array.isArray(options)) {
49
+ throw clientValidationError("createDelegatedQurlBatch: options are required");
50
+ }
51
+ requireNoUnknownFields(options, DELEGATED_BATCH_CREATE_OPTION_KEYS, "createDelegatedQurlBatch options");
52
+ const key = options.idempotencyKey;
53
+ if (typeof key !== "string" ||
54
+ key.length < MIN_DELEGATED_BATCH_IDEMPOTENCY_KEY ||
55
+ key.length > MAX_IDEMPOTENCY_KEY ||
56
+ !/^[!-~]+$/.test(key)) {
57
+ throw clientValidationError(`createDelegatedQurlBatch: idempotencyKey must contain ${MIN_DELEGATED_BATCH_IDEMPOTENCY_KEY}-${MAX_IDEMPOTENCY_KEY} visible ASCII characters`);
58
+ }
59
+ }
60
+ export function validateGetDelegatedQurlBatchOptions(options) {
61
+ if (options === undefined)
62
+ return;
63
+ if (typeof options !== "object" || options === null || Array.isArray(options)) {
64
+ throw clientValidationError(`getDelegatedQurlBatch: options must be an object (got ${describeShape(options)})`);
65
+ }
66
+ requireNoUnknownFields(options, DELEGATED_BATCH_GET_OPTION_KEYS, "getDelegatedQurlBatch options");
67
+ const etag = options.etag;
68
+ if (etag !== undefined &&
69
+ (typeof etag !== "string" ||
70
+ etag.length > MAX_DELEGATED_ETAG ||
71
+ !STRONG_ETAG_PATTERN.test(etag))) {
72
+ throw clientValidationError("getDelegatedQurlBatch: etag must be a valid strong ETag");
73
+ }
74
+ }
75
+ export function delegatedResponseError(status, detail, requestId) {
76
+ return unexpectedResponseError(`Delegated qURL batch ${detail}`, { status, requestId });
77
+ }
78
+ function delegatedResponseObject(value, status, label, requestId) {
79
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
80
+ throw delegatedResponseError(status, `${label} is not an object`, requestId);
81
+ }
82
+ return value;
83
+ }
84
+ export function delegatedEnvelope(response, status) {
85
+ if (typeof response.envelope.meta !== "object" ||
86
+ response.envelope.meta === null ||
87
+ Array.isArray(response.envelope.meta)) {
88
+ throw delegatedResponseError(status, "response meta is not an object");
89
+ }
90
+ const meta = response.envelope.meta;
91
+ if (typeof meta.request_id !== "string") {
92
+ throw delegatedResponseError(status, "response meta has invalid request_id");
93
+ }
94
+ const data = delegatedResponseObject(response.envelope.data, status, "response data", meta.request_id);
95
+ return { data, requestId: meta.request_id };
96
+ }
97
+ export function requiredDelegatedHeader(response, name, status, requestId) {
98
+ const headers = response.headers;
99
+ if (!headers || typeof headers.get !== "function") {
100
+ throw delegatedResponseError(status, "response headers are unavailable", requestId);
101
+ }
102
+ const value = headers.get(name);
103
+ if (value === null) {
104
+ throw delegatedResponseError(status, `response is missing ${name}`, requestId);
105
+ }
106
+ return value;
107
+ }
108
+ export function hasDelegatedNoStore(value) {
109
+ if (value === null)
110
+ return false;
111
+ const directives = new Set(value.split(",").map((directive) => directive.trim().split("=", 1)[0].toLowerCase()));
112
+ return directives.has("private") && directives.has("no-store");
113
+ }
114
+ export function delegatedResponseHeaders(response, status, requestId) {
115
+ const cacheControl = response.headers?.get("Cache-Control") ?? null;
116
+ if (cacheControl !== null) {
117
+ if (!hasDelegatedNoStore(cacheControl)) {
118
+ throw delegatedResponseError(status, "response has invalid Cache-Control", requestId);
119
+ }
120
+ }
121
+ else if (status !== 304) {
122
+ requiredDelegatedHeader(response, "Cache-Control", status, requestId);
123
+ }
124
+ const etag = response.headers?.get("ETag") ?? undefined;
125
+ if (etag !== undefined && (etag.length > MAX_DELEGATED_ETAG || !STRONG_ETAG_PATTERN.test(etag))) {
126
+ throw delegatedResponseError(status, "response has invalid strong ETag", requestId);
127
+ }
128
+ const rawRetryAfter = response.headers?.get("Retry-After") ?? "";
129
+ const trimmed = rawRetryAfter.trim();
130
+ const seconds = /^\d+$/.test(trimmed)
131
+ ? Number(trimmed)
132
+ : /^[A-Z][a-z]{2}, \d{2} [A-Z][a-z]{2} \d{4} \d{2}:\d{2}:\d{2} GMT$/.test(trimmed)
133
+ ? Math.ceil((Date.parse(trimmed) - Date.now()) / 1000)
134
+ : NaN;
135
+ const retryAfter = Number.isSafeInteger(seconds) && seconds > 0 && seconds <= RETRY_AFTER_PARSE_LIMIT_S
136
+ ? seconds
137
+ : undefined;
138
+ return { etag, retryAfter };
139
+ }
140
+ export function isUtcTimestamp(value) {
141
+ return (typeof value === "string" &&
142
+ UTC_TIMESTAMP_PATTERN.test(value) &&
143
+ Number.isFinite(Date.parse(value)));
144
+ }
145
+ export function delegatedBatchBase(data, status, requestId, expectedBatchId) {
146
+ if (typeof data.batch_id !== "string" ||
147
+ !DELEGATED_BATCH_ID_PATTERN.test(data.batch_id) ||
148
+ (expectedBatchId !== undefined && data.batch_id !== expectedBatchId)) {
149
+ throw delegatedResponseError(status, "response has invalid batch_id", requestId);
150
+ }
151
+ if (!Number.isInteger(data.item_count) ||
152
+ Number(data.item_count) < 1 ||
153
+ Number(data.item_count) > MAX_DELEGATED_BATCH_ITEMS) {
154
+ throw delegatedResponseError(status, "response has invalid item_count", requestId);
155
+ }
156
+ if (!isUtcTimestamp(data.submitted_at)) {
157
+ throw delegatedResponseError(status, "response has invalid submitted_at", requestId);
158
+ }
159
+ return {
160
+ batchId: data.batch_id,
161
+ itemCount: Number(data.item_count),
162
+ submittedAt: data.submitted_at,
163
+ };
164
+ }
165
+ export function validateDelegatedLocation(value, baseUrl, batchId, status, requestId) {
166
+ let location;
167
+ try {
168
+ const base = new URL(`${baseUrl}/`);
169
+ const relative = value.startsWith("/v1/delegated-qurl-batches/")
170
+ ? `${base.pathname.replace(/\/$/, "")}${value}`
171
+ : value;
172
+ location = new URL(relative, base);
173
+ }
174
+ catch {
175
+ throw delegatedResponseError(status, "response has invalid Location", requestId);
176
+ }
177
+ const expected = new URL(`${baseUrl}/v1/delegated-qurl-batches/${batchId}`);
178
+ if (location.username !== "" ||
179
+ location.password !== "" ||
180
+ location.search !== "" ||
181
+ location.hash !== "" ||
182
+ location.href !== expected.href) {
183
+ throw delegatedResponseError(status, "response has invalid Location", requestId);
184
+ }
185
+ return location.href;
186
+ }
187
+ function validateDelegatedQurlLink(value) {
188
+ if (typeof value !== "string")
189
+ return false;
190
+ try {
191
+ const link = new URL(value);
192
+ return (link.protocol === "https:" &&
193
+ link.username === "" &&
194
+ link.password === "" &&
195
+ link.hash.length > 1);
196
+ }
197
+ catch {
198
+ return false;
199
+ }
200
+ }
201
+ export function validateDelegatedQurl(value, expectedQurlId, status, requestId) {
202
+ const data = delegatedResponseObject(value, status, "response data", requestId);
203
+ if (data.qurl_id !== expectedQurlId) {
204
+ throw delegatedResponseError(status, "response has invalid qurl_id", requestId);
205
+ }
206
+ if (data.status !== "active" &&
207
+ data.status !== "consumed" &&
208
+ data.status !== "expired" &&
209
+ data.status !== "revoked") {
210
+ throw delegatedResponseError(status, "response has invalid status", requestId);
211
+ }
212
+ if (!isUtcTimestamp(data.expires_at) ||
213
+ typeof data.one_time_use !== "boolean" ||
214
+ !Number.isInteger(data.max_sessions) ||
215
+ Number(data.max_sessions) < 0 ||
216
+ !Number.isInteger(data.session_duration) ||
217
+ Number(data.session_duration) <= 0 ||
218
+ (data.label !== undefined && typeof data.label !== "string") ||
219
+ (data.access_policy !== undefined &&
220
+ (typeof data.access_policy !== "object" ||
221
+ data.access_policy === null ||
222
+ Array.isArray(data.access_policy)))) {
223
+ throw delegatedResponseError(status, "response has invalid qURL metadata", requestId);
224
+ }
225
+ return { ...data, request_id: requestId };
226
+ }
227
+ export function validateDelegatedBatchResults(value, itemCount, batchStatus, status, requestId) {
228
+ if (!Array.isArray(value) || value.length !== itemCount) {
229
+ throw delegatedResponseError(status, "terminal response has invalid results", requestId);
230
+ }
231
+ let succeeded = 0;
232
+ const qurlIds = new Set();
233
+ const links = new Set();
234
+ const results = value.map((raw, index) => {
235
+ const item = delegatedResponseObject(raw, status, `result ${index}`, requestId);
236
+ if (item.index !== index) {
237
+ throw delegatedResponseError(status, `result ${index} has invalid index`, requestId);
238
+ }
239
+ if (item.status === "succeeded") {
240
+ if (item.error !== undefined && item.error !== null) {
241
+ throw delegatedResponseError(status, `result ${index} has conflicting fields`, requestId);
242
+ }
243
+ const qurl = delegatedResponseObject(item.qurl, status, `result ${index} qurl`, requestId);
244
+ if (typeof qurl.qurl_id !== "string" ||
245
+ !DELEGATED_QURL_ID_PATTERN.test(qurl.qurl_id) ||
246
+ !validateDelegatedQurlLink(qurl.qurl_link) ||
247
+ !isUtcTimestamp(qurl.expires_at) ||
248
+ qurlIds.has(qurl.qurl_id) ||
249
+ links.has(qurl.qurl_link)) {
250
+ throw delegatedResponseError(status, `result ${index} has invalid qurl`, requestId);
251
+ }
252
+ qurlIds.add(qurl.qurl_id);
253
+ links.add(qurl.qurl_link);
254
+ succeeded++;
255
+ return {
256
+ index,
257
+ status: "succeeded",
258
+ qurl: {
259
+ qurl_id: qurl.qurl_id,
260
+ qurl_link: qurl.qurl_link,
261
+ expires_at: qurl.expires_at,
262
+ },
263
+ };
264
+ }
265
+ if (item.status !== "failed" || (item.qurl !== undefined && item.qurl !== null)) {
266
+ throw delegatedResponseError(status, `result ${index} has invalid status`, requestId);
267
+ }
268
+ const error = delegatedResponseObject(item.error, status, `result ${index} error`, requestId);
269
+ if (typeof error.code !== "string" ||
270
+ error.code.length === 0 ||
271
+ typeof error.message !== "string") {
272
+ throw delegatedResponseError(status, `result ${index} has invalid error`, requestId);
273
+ }
274
+ return {
275
+ index,
276
+ status: "failed",
277
+ error: { code: error.code, message: error.message },
278
+ };
279
+ });
280
+ if ((batchStatus === "succeeded" && succeeded !== itemCount) ||
281
+ (batchStatus === "failed" && succeeded !== 0) ||
282
+ (batchStatus === "partially_failed" && (succeeded === 0 || succeeded === itemCount))) {
283
+ throw delegatedResponseError(status, "terminal status does not match results", requestId);
284
+ }
285
+ return results;
286
+ }
287
+ export function classifyDelegatedBatchCreateFailure(error, batchId) {
288
+ if (!(error instanceof QURLError)) {
289
+ throw new DelegatedBatchOutcomeUnknownError(new RuntimeError("Unexpected failure after delegated batch dispatch", { cause: error }), batchId);
290
+ }
291
+ if ((error.status >= 400 && error.status < 500 && error.status !== 408) || error.status === 501) {
292
+ throw error;
293
+ }
294
+ throw new DelegatedBatchOutcomeUnknownError(error, batchId);
295
+ }
@@ -0,0 +1,44 @@
1
+ import { ValidationError } from "./errors.js";
2
+ import type { TombstoneInfo } from "./types.js";
3
+ export declare const TEXT_ENCODER: import("node:util").TextEncoder;
4
+ export declare const TEXT_DECODER: import("node:util").TextDecoder;
5
+ /**
6
+ * Construct a {@link ValidationError} for a client-side pre-flight check.
7
+ * Uses `status: 0` (matching {@link NetworkError}/{@link TimeoutError}) and
8
+ * `code: "client_validation"` so catch-by-class still works and callers can
9
+ * tell the error originated inside the SDK rather than from the API.
10
+ */
11
+ export declare function clientValidationError(detail: string): ValidationError;
12
+ /**
13
+ * Construct a {@link ValidationError} for the case where the API returned
14
+ * a response that parsed as JSON but didn't match the expected schema
15
+ * shape (e.g. 400 with a body that isn't a {@link BatchCreateOutput}).
16
+ *
17
+ * Distinct from {@link clientValidationError} so callers can `.code`-branch
18
+ * between "I passed bad input locally" (`"client_validation"`) and "the
19
+ * server returned a body I can't interpret" (`"unexpected_response"`).
20
+ * Uses `status: 0` by default because a logical shape mismatch is not tied to
21
+ * the HTTP classification. Callers that enforce an observed HTTP contract
22
+ * pass that status explicitly.
23
+ *
24
+ * Threads `request_id` through to {@link QURLError.requestId} when the
25
+ * server-side correlation ID is available — operators debugging
26
+ * "unexpected response" tickets need it on the *error* path too, not
27
+ * just on success/passthrough returns.
28
+ */
29
+ export declare function unexpectedResponseError(detail: string, options?: {
30
+ status?: number;
31
+ requestId?: string;
32
+ }): ValidationError;
33
+ export declare function withErrorCause<T extends Error>(error: T, cause: unknown): T;
34
+ export declare function isIndependentAbort(error: unknown, requestSignal: AbortSignal): boolean;
35
+ export declare function isSdkTimeout(error: unknown, requestSignal: AbortSignal): boolean;
36
+ export declare function boundedErrorSnippet(value: unknown): string | undefined;
37
+ /** Keep server identifiers useful as exact machine-readable discriminants. */
38
+ export declare function boundedErrorIdentifier(value: unknown): string | undefined;
39
+ /** Keep only the documented retained metadata, with bounded wire values. */
40
+ export declare function boundedTombstone(value: unknown): TombstoneInfo | undefined;
41
+ /** Bound the structured per-field diagnostics exposed on ValidationError. */
42
+ export declare function boundedInvalidFields(value: unknown): Record<string, string> | undefined;
43
+ /** Bound server-controlled object keys before forwarding them to a debug sink. */
44
+ export declare function boundedObjectKeys(value: object): string[];