@layervai/qurl 0.3.1 → 0.4.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 (72) hide show
  1. package/README.md +187 -7
  2. package/dist/cjs/client.d.ts +17 -10
  3. package/dist/cjs/client.js +649 -96
  4. package/dist/cjs/errors.d.ts +11 -11
  5. package/dist/cjs/errors.js +25 -20
  6. package/dist/cjs/index.d.ts +1 -1
  7. package/dist/cjs/internal.d.ts +4 -0
  8. package/dist/cjs/internal.js +16 -0
  9. package/dist/cjs/node/deployment.d.ts +44 -0
  10. package/dist/cjs/node/deployment.js +167 -0
  11. package/dist/cjs/node/native-udp.d.ts +37 -0
  12. package/dist/cjs/node/native-udp.js +294 -0
  13. package/dist/cjs/node/nhp-wire.d.ts +35 -0
  14. package/dist/cjs/node/nhp-wire.js +261 -0
  15. package/dist/cjs/node/portal-opener.d.ts +83 -0
  16. package/dist/cjs/node/portal-opener.js +767 -0
  17. package/dist/cjs/node/qv2.d.ts +38 -0
  18. package/dist/cjs/node/qv2.js +292 -0
  19. package/dist/cjs/node/strict-json.d.ts +12 -0
  20. package/dist/cjs/node/strict-json.js +174 -0
  21. package/dist/cjs/node.d.ts +3 -0
  22. package/dist/cjs/node.js +11 -0
  23. package/dist/cjs/types.d.ts +7 -18
  24. package/dist/cjs/version.d.ts +1 -1
  25. package/dist/cjs/version.js +1 -1
  26. package/dist/esm/client.d.ts +17 -10
  27. package/dist/esm/client.d.ts.map +1 -1
  28. package/dist/esm/client.js +647 -94
  29. package/dist/esm/client.js.map +1 -1
  30. package/dist/esm/errors.d.ts +11 -11
  31. package/dist/esm/errors.d.ts.map +1 -1
  32. package/dist/esm/errors.js +25 -20
  33. package/dist/esm/errors.js.map +1 -1
  34. package/dist/esm/index.d.ts +1 -1
  35. package/dist/esm/index.d.ts.map +1 -1
  36. package/dist/esm/internal.d.ts +5 -0
  37. package/dist/esm/internal.d.ts.map +1 -0
  38. package/dist/esm/internal.js +14 -0
  39. package/dist/esm/internal.js.map +1 -0
  40. package/dist/esm/node/deployment.d.ts +45 -0
  41. package/dist/esm/node/deployment.d.ts.map +1 -0
  42. package/dist/esm/node/deployment.js +163 -0
  43. package/dist/esm/node/deployment.js.map +1 -0
  44. package/dist/esm/node/native-udp.d.ts +38 -0
  45. package/dist/esm/node/native-udp.d.ts.map +1 -0
  46. package/dist/esm/node/native-udp.js +291 -0
  47. package/dist/esm/node/native-udp.js.map +1 -0
  48. package/dist/esm/node/nhp-wire.d.ts +36 -0
  49. package/dist/esm/node/nhp-wire.d.ts.map +1 -0
  50. package/dist/esm/node/nhp-wire.js +257 -0
  51. package/dist/esm/node/nhp-wire.js.map +1 -0
  52. package/dist/esm/node/portal-opener.d.ts +84 -0
  53. package/dist/esm/node/portal-opener.d.ts.map +1 -0
  54. package/dist/esm/node/portal-opener.js +757 -0
  55. package/dist/esm/node/portal-opener.js.map +1 -0
  56. package/dist/esm/node/qv2.d.ts +39 -0
  57. package/dist/esm/node/qv2.d.ts.map +1 -0
  58. package/dist/esm/node/qv2.js +286 -0
  59. package/dist/esm/node/qv2.js.map +1 -0
  60. package/dist/esm/node/strict-json.d.ts +13 -0
  61. package/dist/esm/node/strict-json.d.ts.map +1 -0
  62. package/dist/esm/node/strict-json.js +171 -0
  63. package/dist/esm/node/strict-json.js.map +1 -0
  64. package/dist/esm/node.d.ts +4 -0
  65. package/dist/esm/node.d.ts.map +1 -0
  66. package/dist/esm/node.js +2 -0
  67. package/dist/esm/node.js.map +1 -0
  68. package/dist/esm/types.d.ts +7 -18
  69. package/dist/esm/types.d.ts.map +1 -1
  70. package/dist/esm/version.d.ts +1 -1
  71. package/dist/esm/version.js +1 -1
  72. package/package.json +25 -6
package/README.md CHANGED
@@ -105,6 +105,11 @@ const portal = await resource.createPortal({
105
105
  });
106
106
  ```
107
107
 
108
+ The Node-only `@layervai/qurl/node` entry opens received qv2 links with native
109
+ NHP 1.1 UDP. It has no relay or HTTP-resolve fallback. Connector assignment and
110
+ registration are separate producer operations and are not part of the portal
111
+ opener.
112
+
108
113
  ## Opening Portals
109
114
 
110
115
  Most recipients open qURL links directly and do not use this SDK at all. If
@@ -123,6 +128,102 @@ LayerV API: the client needs an API key with the `qurl:resolve` scope.
123
128
  `enterPortal` fails closed — if access is granted but no resource URL comes
124
129
  back, it throws instead of returning an empty handle.
125
130
 
131
+ ### Proactive native Node opener
132
+
133
+ Use the Node subpath when a service receives a qv2 link and must keep one NHP
134
+ session ready for a low-latency private request. Construct and start one opener
135
+ for that link during setup. `start()` sends the native UDP knock and schedules
136
+ bounded background renewal before the admission expires. Failed renewal tries
137
+ are spread across the remaining valid grant window. `fetch()` never opens or
138
+ renews a session, and it never sleeps. It fails if the cached admission has
139
+ expired.
140
+
141
+ ```javascript
142
+ const { createPortalOpener } = require('@layervai/qurl/node');
143
+
144
+ async function uploadPrivateObject(uploadBody) {
145
+ const opener = createPortalOpener({
146
+ qurl: process.env.PRIVATE_UPLOAD_QURL,
147
+ transport: 'native-only',
148
+ });
149
+ try {
150
+ await opener.start();
151
+ return await opener.fetch(
152
+ (authenticatedTarget) => ({
153
+ method: 'POST',
154
+ headers: signUploadForExactTarget(authenticatedTarget),
155
+ body: uploadBody,
156
+ }),
157
+ { redirects: 'error' },
158
+ );
159
+ } finally {
160
+ await opener.close();
161
+ }
162
+ }
163
+ ```
164
+
165
+ The request builder receives a copy of the exact authenticated ACK target. The
166
+ opener ignores mutations to that copy and sends the initial request only to the
167
+ fixed ACK URL. It adds the private `qurl_vsession` cookie, replaces a
168
+ caller-supplied cookie with that name, and preserves other valid cookies,
169
+ including duplicate `Cookie` entries that Node joins with semicolons. It does
170
+ not accept a caller URL or path. Use `redirects: 'error'` for a request
171
+ whose signature binds its method, target, timestamp, or nonce. This mode closes
172
+ a redirect response and does not replay the request. The default `follow` mode
173
+ can then move within the authenticated origin. It permits at most 10 requests,
174
+ including the initial request, and uses the standard 301/302/303 method rewrite
175
+ rules. As in Go, a 3xx response with no `Location` header, or a 307/308 response
176
+ whose streaming body cannot be replayed, is returned to the caller without a
177
+ follow-up request. The caller then owns that response body and must consume or
178
+ cancel it. Local admission expiry is checked before the first request; the
179
+ protected service remains authoritative while a permitted redirect chain is in
180
+ progress.
181
+
182
+ `timeoutMs` bounds each resolved-address attempt, and `maxAddresses` caps the
183
+ serial address attempts. This matches qurl-go. Every open also has a whole-open
184
+ deadline that covers DNS and UDP: at least 15 seconds, or the configured
185
+ per-address budget times `maxAddresses + 1` when that is larger. Pass an abort
186
+ signal to `start()` when lifecycle code needs a shorter deadline.
187
+ Content requests use the caller's `RequestInit.signal`; `fetch()` does not add
188
+ an independent application-request deadline.
189
+
190
+ TypeScript consumers of `@layervai/qurl/node` must provide Node and Fetch API
191
+ declarations, for example current `@types/node`, or a configuration that includes
192
+ the `DOM` library for Fetch types.
193
+
194
+ Native opening requires public deployment trust. Set `QURL_DEPLOYMENT` to one
195
+ strict JSON object or to a path that contains that object. The object must have
196
+ trusted P-256 issuer keys and native cell host, UDP port 443, and X25519 public
197
+ key entries. A configured path must be a regular file no larger than 1 MiB. The
198
+ SDK loads and validates this value once when it constructs the opener. It does
199
+ not perform discovery and it fails before DNS if the verified link names an
200
+ unknown cell.
201
+
202
+ Use `opener.health()` for the local `idle`, `starting`, `healthy`, `renewing`,
203
+ `degraded`, `expired`, or `closed` state. A degraded state includes a typed
204
+ renewal failure class but no raw session capability. After bounded background
205
+ retries stop, `start()` makes one explicit recovery attempt and refreshes the
206
+ health result. If that explicit attempt fails while the prior grant is still
207
+ usable, the opener re-arms its bounded background attempts after it returns the
208
+ error. This does not put an open or sleep on the `fetch()` path. Stop and await
209
+ in-flight content requests before you call `await opener.close()` during
210
+ shutdown. Close cancels and waits for an active NHP exchange, then wipes the
211
+ mutable private-key, visitor-secret, and session-token buffers. JavaScript can
212
+ create immutable string copies during JSON and HTTP processing, so the SDK
213
+ cannot promise full memory zeroization before garbage collection. Never log the
214
+ qURL, ACK body, request cookies, or request headers.
215
+
216
+ A cold `start()` failure leaves health at `idle`. Catch `PortalBusyError` for an
217
+ authenticated COOKIE busy response and `PortalInvalidReplyError` for a malformed
218
+ authenticated reply. The thrown typed error is the cold-start diagnostic.
219
+
220
+ Local qv2 verification checks the signed bytes, trust, and clock-free claim
221
+ ordering. As in the Go SDK, it does not compare `nbf` or `exp` with the local
222
+ clock. The authenticated NHP open is the authoritative live validity check.
223
+ The opener also follows the Go NHP 1.1 COOKIE rule: an authenticated COOKIE is
224
+ a busy result and does not use ACK counter correlation or local clock skew
225
+ checks. Replaying it can only force another busy result; it cannot grant access.
226
+
126
227
  ## REST-Shaped API (Compatibility)
127
228
 
128
229
  The original REST-shaped methods remain fully supported and share the same
@@ -184,7 +285,6 @@ console.log(`Access granted to ${access.target_url} for ${access.access_grant?.e
184
285
  | `mintLink(id, input?)` | Mint a new access link |
185
286
  | `resolve(input)` | Resolve token + grant network access |
186
287
  | `getQuota()` | Get quota/usage info |
187
- | `bootstrapAgent(input)` | Bootstrap a qURL Connector agent |
188
288
  | `listResources(input?)` / `listAllResources(input?)` / `createResource(input)` / `getResource(id)` | Resource management |
189
289
  | `updateResource(id, input)` / `deleteResource(id)` | Update or revoke resources |
190
290
  | `createQurlForResource(id, input?)` | Mint a qURL for an existing resource |
@@ -312,15 +412,50 @@ const clientWithLogger = new QURLClient({
312
412
 
313
413
  ## Retry Behavior
314
414
 
315
- The client automatically retries failed requests with exponential backoff:
316
-
317
- - **GET/DELETE**: Retries on 429, 502, 503, 504
318
- - **POST/PATCH**: Retries status responses only on 429
319
- - **Network errors**: Always retried; POST/PATCH requests send an `Idempotency-Key` on the first attempt and reuse it on retries
320
- - **`Retry-After` header**: Honored on 429 and 503 responses (RFC 7231 §7.1.3). Currently the SDK only parses **delta-seconds** values (e.g. `Retry-After: 30`); HTTP-date values (`Retry-After: Wed, 21 Oct 2026 07:28:00 GMT`) silently fall back to exponential backoff. Tracked in [#61](https://github.com/layervai/qurl-typescript/issues/61).
415
+ The client retries only requests whose replay contract is explicit:
416
+
417
+ - **GET**: Retries on 429, 502, 503, 504 and transport failures, including a
418
+ dropped response body after successful headers arrive
419
+ - **POST/PATCH**: Retries complete status responses only on 429. A transport
420
+ failure while reading a response body is not replayed because the mutation
421
+ may have applied.
422
+ - **POST/PATCH fetch failures**: Retried with the `Idempotency-Key` generated on
423
+ the first attempt when no response is available
424
+ - **DELETE**: Never replayed automatically, including on 429. A transport
425
+ failure after dispatch makes the mutation outcome unknown, and the HTTP verb
426
+ alone cannot prove a replay safe. Reconcile resource state before you issue
427
+ a deliberate retry. This matches qurl-go's explicit-caller-retry model.
428
+ - **`Retry-After` header**: Preserved on 429 and 503 responses, including when
429
+ a mid-body transport failure produces a status-derived error (RFC 7231
430
+ §7.1.3), and honored by automatic GET retries. Currently the SDK only parses
431
+ **delta-seconds** values (e.g.
432
+ `Retry-After: 30`); HTTP-date values (`Retry-After: Wed, 21 Oct 2026 07:28:00 GMT`)
433
+ silently fall back to exponential backoff. Tracked in
434
+ [#61](https://github.com/layervai/qurl-typescript/issues/61).
435
+ - **Response failures**: Redirects, oversized successful bodies, and malformed
436
+ UTF-8 successful bodies are not retried. Retryable error statuses remain
437
+ retryable when an intermediary returns HTML, an empty body, malformed UTF-8,
438
+ or another complete non-envelope error response. A mid-stream transport
439
+ failure is retried for a successful GET or a GET with a retryable status.
440
+ A hard 4xx GET is not retried. An independent `AbortError` or `TimeoutError`
441
+ from an injected fetch, or while reading a successful response body, is a
442
+ non-retried `NetworkError`. After non-success headers arrive, the SDK
443
+ preserves the status-derived error class and attaches the independent
444
+ failure as its cause without retrying it. Only the SDK's timeout signal
445
+ produces a `TimeoutError`. Mutations require reconciliation.
446
+
447
+ All documented no-content DELETE operations require exactly HTTP 204 with an
448
+ empty response body. Alternate success statuses or response bytes fail closed
449
+ as `unexpected_response` contract errors whose `.status` preserves the
450
+ observed HTTP status. The delete may already have applied; reconcile resource
451
+ state before retrying.
321
452
 
322
453
  Configure with `maxRetries` (default: 3). Set to `0` to disable.
323
454
 
455
+ When DELETE returns `RateLimitError`, use `retryAfter` for scheduling but
456
+ reconcile current resource state before issuing a deliberate retry; the SDK
457
+ does not assume the rejected response proves the mutation never ran.
458
+
324
459
  > **Worst-case latency**: `timeout` is enforced per *attempt*, not for the whole request. Total worst-case latency is roughly `timeout × (maxRetries + 1) + sum(retry delays)`. Operators tuning `timeout` should account for this when sizing health-check budgets.
325
460
 
326
461
  For POST/PATCH requests, the SDK generates a UUIDv7 `Idempotency-Key` once per logical call and reuses it across SDK-managed retries, so the API can return the original result instead of creating duplicate resources. If your application catches an error and calls the SDK again, pass a stable override so the new call deduplicates with the first one. Caller-provided keys must be non-empty printable ASCII strings of at most 256 characters and must not start or end with spaces. Use a unique key for each logical operation; reusing one key for a different request can return the first cached response. To tie retries to your own upstream job or request ID, pass a per-call override:
@@ -340,6 +475,51 @@ SDK-generated keys require `globalThis.crypto.getRandomValues`, which is availab
340
475
  ## Security Notes
341
476
 
342
477
  - Treat API keys and qURL links like credentials. Do not log them.
478
+ - SDK API requests use manual redirect handling. Redirect-capable HTTP statuses
479
+ (300, 301, 302, 303, 305, 307, 308), filtered `opaqueredirect` responses in
480
+ browsers and Node native fetch, and responses a custom fetch reports as
481
+ already redirected are rejected as a typed `QURLError`
482
+ (`code: "unexpected_response"`) without requesting the `Location` target.
483
+ This prevents forwarding `Authorization` and
484
+ `Idempotency-Key` when the fetch implementation honors `redirect: "manual"`
485
+ and accurately exposes `Response.redirected`. A shim must also leave
486
+ `Response.url` empty or report the normalized request URL when it did not
487
+ follow a redirect. Every Response-like shim must provide `headers.get(name)`.
488
+ A non-empty invalid `Response.url` fails closed with an SDK-authored error
489
+ that names this shim requirement but does not reflect the invalid value. A
490
+ non-redirecting 304 is handled as an ordinary unsuccessful API response
491
+ rather than mislabeled as a redirect.
492
+ - API success and error bodies are limited to **1 MiB (1,048,576 bytes)**,
493
+ matching qurl-go's security posture. The SDK checks `Content-Length` when
494
+ present and independently counts streamed bytes, so missing or inaccurate
495
+ headers cannot bypass the limit. Bodies exactly at the limit are accepted.
496
+ This fixed security limit has no override. Callers must request a smaller
497
+ page, and the SDKs must not independently widen the limit.
498
+ A maximum-size list page can exceed the cap after JSON escaping; request a
499
+ smaller page if a list call reports the body-limit error. For resumable list
500
+ reads, use the page method, save each successful `next_cursor`, and resume
501
+ from that cursor with a smaller `limit`. A shared configurable-cap decision
502
+ is tracked in [#249](https://github.com/layervai/qurl-typescript/issues/249).
503
+ Standards-compliant fetch implementations are bounded while streaming;
504
+ custom Response-like shims that omit `body` are validated after their
505
+ `text()`/`json()` result has already been materialized by that shim.
506
+ Oversized bodies fail with a typed error before JSON decoding while
507
+ preserving the observed status-derived error class and `Retry-After`.
508
+ Transient error statuses retain the normal retry policy for GET and
509
+ idempotency-key-backed mutations; oversized successful responses and DELETE
510
+ responses are not retried.
511
+ Successful JSON bodies with malformed UTF-8 are rejected without replay.
512
+ Server-provided error title/detail snippets and `invalidFields` keys/values
513
+ have controls and bidirectional formatting characters removed, are
514
+ normalized to one line, and are capped at 512 UTF-8 bytes. Machine-readable
515
+ error codes, RFC 7807 type/instance values, and request IDs are kept only
516
+ when they need no normalization and fit the same limit. At most 100
517
+ `invalidFields` entries are retained, and each retained
518
+ `invalidFields` or debug `body_keys` collection has an 8 KiB UTF-8 budget.
519
+ Redirect/body-limit contract-error details do not include `Location` values
520
+ or response-body snippets. Standard request debug logging includes the request
521
+ URL, so do not place credentials in identifiers or enable debug output in a
522
+ sensitive logging environment.
343
523
  - Prefer short portal lifetimes such as `validFor: '5m'`.
344
524
  - Do not ask portal recipients to handle credentials. Recipients only need
345
525
  the link.
@@ -1,4 +1,12 @@
1
- import type { AccessCodeListOutput, AgentBootstrapInput, AgentBootstrapOutput, ApiKey, ApiKeyListOutput, BillingInvoiceListOutput, BatchCreateInput, BatchCreateOutput, ClientOptions, ConnectorInstallation, ConnectorInstallationListOutput, CreateAccessCodeInput, CreateAccessCodeOutput, CreateApiKeyInput, CreateApiKeyOutput, CreateBillingCheckoutInput, CreateInput, CreateOutput, CreatePortalOptions, CreateQurlForResourceInput, CreateResourceInput, CreateWebhookInput, Customer, CheckoutSession, Domain, DomainListOutput, DomainVerifyResult, ExtendInput, Invoice, ListInput, ListOutput, ListApiKeysInput, ListBillingInvoicesInput, ListConnectorInstallationsInput, ListDomainsInput, ListWebhookDeliveriesInput, ListWebhooksInput, MintInput, MintOutput, Portal, PortalSession, ProtectUrlOptions, QURL, QurlSummary, Quota, RedeemAccessCodeInput, RedeemAccessCodeOutput, RegisterDomainInput, Resource, ResourceDetail, ResourceHandle, ResourceListInput, ResourceListOutput, RequestOptions, ResolveInput, ResolveOutput, SessionListOutput, SessionTerminateOutput, UpdateApiKeyInput, UpdateCustomerInput, UpdateInput, UpdateResourceInput, UpdateResourceQurlInput, UpdateWebhookInput, UsageCurrentPeriod, UsageDaily, Webhook, WebhookDelivery, WebhookDeliveryListOutput, WebhookEventTypeInfo, WebhookListOutput, WebhookWithSecret } from "./types.js";
1
+ import type { AccessCodeListOutput, ApiKey, ApiKeyListOutput, BillingInvoiceListOutput, BatchCreateInput, BatchCreateOutput, ClientOptions, ConnectorInstallation, ConnectorInstallationListOutput, CreateAccessCodeInput, CreateAccessCodeOutput, CreateApiKeyInput, CreateApiKeyOutput, CreateBillingCheckoutInput, CreateInput, CreateOutput, CreatePortalOptions, CreateQurlForResourceInput, CreateResourceInput, CreateWebhookInput, Customer, CheckoutSession, Domain, DomainListOutput, DomainVerifyResult, ExtendInput, Invoice, ListInput, ListOutput, ListApiKeysInput, ListBillingInvoicesInput, ListConnectorInstallationsInput, ListDomainsInput, ListWebhookDeliveriesInput, ListWebhooksInput, MintInput, MintOutput, Portal, PortalSession, ProtectUrlOptions, QURL, QurlSummary, Quota, RedeemAccessCodeInput, RedeemAccessCodeOutput, RegisterDomainInput, Resource, ResourceDetail, ResourceHandle, ResourceListInput, ResourceListOutput, RequestOptions, ResolveInput, ResolveOutput, SessionListOutput, SessionTerminateOutput, UpdateApiKeyInput, UpdateCustomerInput, UpdateInput, UpdateResourceInput, UpdateResourceQurlInput, UpdateWebhookInput, UsageCurrentPeriod, UsageDaily, Webhook, WebhookDelivery, WebhookDeliveryListOutput, WebhookEventTypeInfo, WebhookListOutput, WebhookWithSecret } from "./types.js";
2
+ /**
3
+ * Shape of qurl-go's offline signed-fragment links: three or more
4
+ * dot-separated base64url parts (<version>.<claims>.<secret>.<sig>).
5
+ * Detected only to give those links a precise error in extractAccessToken.
6
+ *
7
+ * @internal Exported for in-package tests; not re-exported from index.ts.
8
+ */
9
+ export declare const SIGNED_FRAGMENT_RE: RegExp;
2
10
  /** qURL API client. */
3
11
  export declare class QURLClient {
4
12
  private readonly baseUrl;
@@ -307,8 +315,6 @@ export declare class QURLClient {
307
315
  resolve(input: ResolveInput | string, options?: RequestOptions): Promise<ResolveOutput>;
308
316
  /** Get quota and usage information. */
309
317
  getQuota(): Promise<Quota>;
310
- /** Bootstrap a LayerV qURL Connector agent. */
311
- bootstrapAgent(input: AgentBootstrapInput, options?: RequestOptions): Promise<AgentBootstrapOutput>;
312
318
  /** List resources from the `/v1/resources` API. */
313
319
  listResources(input?: ResourceListInput): Promise<ResourceListOutput>;
314
320
  /**
@@ -349,13 +355,7 @@ export declare class QURLClient {
349
355
  * this page and emits a debug log rather than pretending it fetched all pages.
350
356
  */
351
357
  listResourceSessions(id: string): Promise<SessionListOutput>;
352
- /**
353
- * Terminate all active sessions for a resource.
354
- *
355
- * The returned count is best-effort under retries: if the first DELETE
356
- * succeeds server-side but the response is lost, a retried request may
357
- * return `0` because there are no sessions left to terminate.
358
- */
358
+ /** Terminate all active sessions for a resource and return the server count. */
359
359
  terminateAllResourceSessions(id: string): Promise<SessionTerminateOutput>;
360
360
  /** Terminate a specific resource session. */
361
361
  terminateResourceSession(id: string, sessionId: string): Promise<void>;
@@ -468,6 +468,12 @@ export declare class QURLClient {
468
468
  /** Revoke an access code. */
469
469
  revokeAccessCode(id: string): Promise<void>;
470
470
  private request;
471
+ /**
472
+ * Require the service's exact 204 response with no response bytes exposed by
473
+ * Fetch. A standards-compliant Fetch implementation discards any forbidden
474
+ * wire body on 204, so the SDK cannot detect bytes that Fetch did not expose.
475
+ */
476
+ private requestNoContent;
471
477
  /**
472
478
  * Issue an HTTP request and parse the JSON response.
473
479
  *
@@ -495,6 +501,7 @@ export declare class QURLClient {
495
501
  */
496
502
  private retryDelay;
497
503
  private classifyFetchError;
504
+ private classifyResponseReadError;
498
505
  }
499
506
  /**
500
507
  * A LayerV-protected resource, bound to the client that produced it.