@layervai/qurl 0.3.1 → 0.5.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 +261 -11
  2. package/dist/cjs/client.d.ts +22 -12
  3. package/dist/cjs/client.js +675 -100
  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 +35 -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 +114 -0
  16. package/dist/cjs/node/portal-opener.js +1070 -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 +18 -0
  23. package/dist/cjs/types.d.ts +17 -20
  24. package/dist/cjs/version.d.ts +1 -1
  25. package/dist/cjs/version.js +1 -1
  26. package/dist/esm/client.d.ts +22 -12
  27. package/dist/esm/client.d.ts.map +1 -1
  28. package/dist/esm/client.js +673 -98
  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 +36 -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 +115 -0
  53. package/dist/esm/node/portal-opener.d.ts.map +1 -0
  54. package/dist/esm/node/portal-opener.js +1053 -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 +17 -20
  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 +26 -7
package/README.md CHANGED
@@ -39,7 +39,9 @@ endpoint in front of the same service:
39
39
  npm install @layervai/qurl
40
40
  ```
41
41
 
42
- Requires Node.js 20+ and has **no runtime dependencies**. Both `import { QURLClient } from '@layervai/qurl'` (ESM) and `const { QURLClient } = require('@layervai/qurl')` (CJS) work.
42
+ Requires Node.js 22.12+ and has **no runtime dependencies**. Both
43
+ `import { QURLClient } from '@layervai/qurl'` (ESM) and
44
+ `const { QURLClient } = require('@layervai/qurl')` (CJS) work.
43
45
 
44
46
  ## Quickstart
45
47
 
@@ -71,9 +73,17 @@ of calling `protectUrl`:
71
73
 
72
74
  ```typescript
73
75
  const resource = await client.connectorResource('prod-dashboard');
74
- const portal = await resource.createPortal({ validFor: '5m' });
76
+ const portal = await resource.createPortal({
77
+ validFor: '5m',
78
+ targetPath: '/api/detect/eib_example',
79
+ });
75
80
  ```
76
81
 
82
+ `targetPath` is available only when minting for an existing resource. The SDK
83
+ checks the non-empty 2048-byte boundary; the API remains authoritative for the
84
+ path grammar and the tunnel-only resource gate. `createPortalForUrl` rejects
85
+ this option because it creates a URL resource.
86
+
77
87
  If you persist the resource id, future calls do not need to recreate the
78
88
  handle (no API call is made until you mint):
79
89
 
@@ -102,9 +112,15 @@ const portal = await resource.createPortal({
102
112
  label: 'Alice from Acme',
103
113
  oneTimeUse: true,
104
114
  maxSessions: 1,
115
+ targetPath: '/api/detect/eib_example',
105
116
  });
106
117
  ```
107
118
 
119
+ The Node-only `@layervai/qurl/node` entry opens received qv2 links with native
120
+ NHP UDP. It has no relay or HTTP-resolve fallback. Connector assignment and
121
+ registration are separate producer operations and are not part of the portal
122
+ opener.
123
+
108
124
  ## Opening Portals
109
125
 
110
126
  Most recipients open qURL links directly and do not use this SDK at all. If
@@ -123,6 +139,161 @@ LayerV API: the client needs an API key with the `qurl:resolve` scope.
123
139
  `enterPortal` fails closed — if access is granted but no resource URL comes
124
140
  back, it throws instead of returning an empty handle.
125
141
 
142
+ ### Proactive native Node opener
143
+
144
+ Use the Node subpath when a service receives a qv2 link and must keep one NHP
145
+ session ready for a low-latency private request. Construct and start one opener
146
+ for that link during setup. `start()` sends the native UDP knock and schedules
147
+ background renewal before the admission expires. A transient renewal failure
148
+ retries after 500 ms, then 1 second, then at most every 2 seconds while the old
149
+ admission remains valid. `fetch()` never opens or renews a session, and it never
150
+ sleeps. It fails if the cached admission has expired.
151
+
152
+ Renewal waits at least 5 seconds after a successful open. If a server grants a
153
+ session that expires before that safe renewal point, the grant expires without
154
+ a background attempt and `start()` is the explicit recovery path. Production
155
+ admissions should be longer than this minimum gap.
156
+
157
+ ```javascript
158
+ const { createPortalOpener } = require('@layervai/qurl/node');
159
+
160
+ async function uploadPrivateObject(uploadBody) {
161
+ const opener = createPortalOpener({
162
+ qurl: process.env.PRIVATE_UPLOAD_QURL,
163
+ });
164
+ try {
165
+ await opener.start();
166
+ return await opener.fetch(
167
+ (authenticatedTarget) => ({
168
+ method: 'POST',
169
+ headers: signUploadForExactTarget(authenticatedTarget),
170
+ body: uploadBody,
171
+ }),
172
+ { redirects: 'error' },
173
+ );
174
+ } finally {
175
+ await opener.close();
176
+ }
177
+ }
178
+ ```
179
+
180
+ For a service whose ACK target is a base path, append only trusted raw path
181
+ segments with `fetchDescendant()`. The opener rejects empty, dot, and delimiter
182
+ segments and escapes each accepted segment before it sends the request:
183
+
184
+ ```javascript
185
+ const response = await opener.fetchDescendant(
186
+ ['eib_example'],
187
+ (authenticatedTarget) => ({ method: 'POST' }),
188
+ { redirects: 'error' },
189
+ );
190
+ ```
191
+
192
+ The request builder receives a copy of the selected authenticated target. The
193
+ opener ignores mutations to that copy and sends the initial request only to the
194
+ fixed ACK URL or the validated descendant. A descendant call accepts raw path
195
+ segments, not a URL or path string. It preserves the ACK target query and
196
+ escapes each segment separately. It adds the private `qurl_vsession` cookie,
197
+ replaces a caller-supplied cookie with that name, and preserves other valid
198
+ cookies, including duplicate `Cookie` entries that Node joins with semicolons.
199
+ It does not accept a caller URL, and it rejects a caller-supplied `Host` header
200
+ so Fetch derives the authority from the pinned target. Both `fetch()` and
201
+ `fetchDescendant()` use only the cached NHP 1.1 admission. Use
202
+ `redirects: 'error'` to keep a descendant request at its initial target. The
203
+ default `follow` mode can move outside that path subtree, but only within the
204
+ authenticated origin. Also use `redirects: 'error'` for a request whose
205
+ signature binds its method, target, timestamp, or nonce. This mode closes a
206
+ redirect response and does not replay the request. Follow mode permits at most
207
+ 10 requests, including the initial request, and uses the standard 301/302/303
208
+ method rewrite rules. As in Go, a 3xx response with no `Location` header, or a
209
+ 307/308 response whose streaming body cannot be replayed, is returned to the
210
+ caller without a follow-up request. The caller then owns that response body and
211
+ must consume or cancel it. Local admission expiry is checked before the first
212
+ request; the protected service remains authoritative while a permitted redirect
213
+ chain is in progress.
214
+
215
+ When `fetch()` rejects before it sends a direct `RequestInit`, it releases a
216
+ caller-owned streaming body. It cancels a web `ReadableStream`, destroys a Node
217
+ `Readable`, or closes another iterator when that body supports the operation. A
218
+ request builder remains lazy and is not called until a grant is ready.
219
+
220
+ Each open has a whole-operation deadline that covers DNS and UDP.
221
+ `openTimeoutMs` sets this ceiling; the default is 15 seconds and the maximum is
222
+ 60 seconds. Native DNS or address attempts can fail before this ceiling, so a
223
+ larger value does not extend their internal timeouts. A renewal also stops at
224
+ the old grant's expiry. Pass an abort signal to `start()` when lifecycle code
225
+ needs a shorter deadline. Concurrent `start()` calls share one attempt. The
226
+ first caller's signal owns that attempt, so its abort fails all waiters but does
227
+ not count as an open failure in health. A cold cancellation restores `new`; a
228
+ recovery cancellation preserves its prior `degraded` health. A later waiter's
229
+ abort stops only that wait.
230
+ Content requests use the caller's `RequestInit.signal`; `fetch()` does not add
231
+ an independent application-request deadline. The signal stays active while the
232
+ returned response body is read. Set the opener's optional `fetch` when the
233
+ protected request must use a custom Fetch implementation. Native NHP opening
234
+ never uses this function. The custom function receives the `qurl_vsession`
235
+ bearer cookie and is inside the credential boundary. It must implement standard
236
+ Fetch signal behavior and honor `redirect: 'manual'`. If it needs a receiver,
237
+ pass it already bound; the SDK invokes it with the standard global Fetch
238
+ receiver. An unfollowed synthetic response can leave `Response.url` empty.
239
+ Otherwise, it must report
240
+ `Response.url` and `Response.redirected` accurately. It must not log or forward
241
+ protected request headers.
242
+
243
+ TypeScript consumers of `@layervai/qurl/node` must provide Node and Fetch API
244
+ declarations, for example current `@types/node`, or a configuration that includes
245
+ the `DOM` library for Fetch types. The native opener requires Node 22.12 or later.
246
+ This floor ensures that composite request signals are reclaimed on long-lived
247
+ openers.
248
+
249
+ Native opening requires public deployment trust. Set `QURL_DEPLOYMENT` to one
250
+ strict JSON object or to a path that contains that object. The object must have
251
+ trusted P-256 issuer keys and native cell host, UDP port 443, and X25519 public
252
+ key entries. A configured path must be a regular file no larger than 1 MiB.
253
+ Construction does no I/O. `start()` resolves and validates the deployment, and
254
+ the first successful open pins that resolved trust for renewal and recovery.
255
+ The opener does not perform discovery. It fails before DNS if the verified link
256
+ names an unknown cell.
257
+
258
+ Use `opener.health()` for the local `new`, `starting`, `ready`, `degraded`, or
259
+ `closed` state. It reports absolute expiry, renewal, and last-success times, a
260
+ secret-free failure class, and the number of consecutive unsuccessful open
261
+ attempts. A renewal window can contain more than one such attempt. The
262
+ `starting` state is only for the first cold open; an explicit recovery from a
263
+ failure reports `degraded` until it succeeds. Date fields describe the most
264
+ recent successful grant, so use `ready` as the authority for current usability.
265
+ A transient renewal failure keeps the state `ready` while the prior admission
266
+ is usable. A changed authenticated target is not retried. It keeps the prior
267
+ admission until expiry. During that time, `start()` remains idempotent because
268
+ the old admission is still usable; `health().lastFailureClass` reports the
269
+ target change. After expiry, an explicit `start()` attempts recovery. If the
270
+ target change is intentional and permanent, create a new opener for the new
271
+ qURL; the old opener stays bound to its first authenticated target. Other
272
+ failures retry through the remaining admission window. This does not put an
273
+ open or sleep on the `fetch()` path.
274
+
275
+ Close cancels and waits for an active NHP open, then wipes the mutable
276
+ private-key, visitor-secret, and session-token buffers. Close also aborts a
277
+ protected request and its returned response body, and prevents another redirect
278
+ leg from starting. JavaScript can create immutable string copies during JSON
279
+ and HTTP processing, so the SDK cannot promise full memory zeroization before garbage
280
+ collection. The opener retains the immutable qURL string until close so it can
281
+ verify each renewal. Never log the qURL, ACK body, request cookies, or request
282
+ headers.
283
+
284
+ A cold `start()` failure leaves health at `degraded`. Catch `PortalBusyError`
285
+ for an authenticated COOKIE busy response and `PortalInvalidReplyError` for a
286
+ malformed authenticated reply. An open deadline throws `PortalOpenTimeoutError`.
287
+ The thrown typed error is the cold-start
288
+ diagnostic; health does not expose its message or secret values.
289
+
290
+ Local qv2 verification checks the signed bytes, trust, and clock-free claim
291
+ ordering. As in the Go SDK, it does not compare `nbf` or `exp` with the local
292
+ clock. The authenticated NHP open is the authoritative live validity check.
293
+ The opener also follows the Go NHP COOKIE rule: an authenticated COOKIE is
294
+ a busy result and does not use ACK counter correlation or local clock skew
295
+ checks. Replaying it can only force another busy result; it cannot grant access.
296
+
126
297
  ## REST-Shaped API (Compatibility)
127
298
 
128
299
  The original REST-shaped methods remain fully supported and share the same
@@ -163,8 +334,8 @@ console.log(`Access granted to ${access.target_url} for ${access.access_grant?.e
163
334
  | Method | Description |
164
335
  |--------|-------------|
165
336
  | `protectUrl(targetUrl, opts?)` | Protect a private URL → portal-minting `ProtectedResource` handle |
166
- | `resource.createPortal(opts?)` / `createPortal(resourceOrId, opts?)` | Mint a short-lived portal link (`Portal`) |
167
- | `createPortalForUrl(targetUrl, opts?)` | Protect + mint in one API call → `{ portal, resource }` |
337
+ | `resource.createPortal(opts?)` / `createPortal(resourceOrId, opts?)` | Mint a short-lived portal link; existing resources can set `targetPath` |
338
+ | `createPortalForUrl(targetUrl, opts?)` | Protect + mint a URL resource; rejects `targetPath` |
168
339
  | `connectorResource(connectorId)` | Handle for a service qURL Connector already protects |
169
340
  | `resourceById(id)` | Handle from a stored resource id (no API call) |
170
341
  | `enterPortal(linkOrToken)` | Open a qURL link programmatically → `ResourceHandle` |
@@ -184,7 +355,6 @@ console.log(`Access granted to ${access.target_url} for ${access.access_grant?.e
184
355
  | `mintLink(id, input?)` | Mint a new access link |
185
356
  | `resolve(input)` | Resolve token + grant network access |
186
357
  | `getQuota()` | Get quota/usage info |
187
- | `bootstrapAgent(input)` | Bootstrap a qURL Connector agent |
188
358
  | `listResources(input?)` / `listAllResources(input?)` / `createResource(input)` / `getResource(id)` | Resource management |
189
359
  | `updateResource(id, input)` / `deleteResource(id)` | Update or revoke resources |
190
360
  | `createQurlForResource(id, input?)` | Mint a qURL for an existing resource |
@@ -312,15 +482,50 @@ const clientWithLogger = new QURLClient({
312
482
 
313
483
  ## Retry Behavior
314
484
 
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).
485
+ The client retries only requests whose replay contract is explicit:
486
+
487
+ - **GET**: Retries on 429, 502, 503, 504 and transport failures, including a
488
+ dropped response body after successful headers arrive
489
+ - **POST/PATCH**: Retries complete status responses only on 429. A transport
490
+ failure while reading a response body is not replayed because the mutation
491
+ may have applied.
492
+ - **POST/PATCH fetch failures**: Retried with the `Idempotency-Key` generated on
493
+ the first attempt when no response is available
494
+ - **DELETE**: Never replayed automatically, including on 429. A transport
495
+ failure after dispatch makes the mutation outcome unknown, and the HTTP verb
496
+ alone cannot prove a replay safe. Reconcile resource state before you issue
497
+ a deliberate retry. This matches qurl-go's explicit-caller-retry model.
498
+ - **`Retry-After` header**: Preserved on 429 and 503 responses, including when
499
+ a mid-body transport failure produces a status-derived error (RFC 7231
500
+ §7.1.3), and honored by automatic GET retries. Currently the SDK only parses
501
+ **delta-seconds** values (e.g.
502
+ `Retry-After: 30`); HTTP-date values (`Retry-After: Wed, 21 Oct 2026 07:28:00 GMT`)
503
+ silently fall back to exponential backoff. Tracked in
504
+ [#61](https://github.com/layervai/qurl-typescript/issues/61).
505
+ - **Response failures**: Redirects, oversized successful bodies, and malformed
506
+ UTF-8 successful bodies are not retried. Retryable error statuses remain
507
+ retryable when an intermediary returns HTML, an empty body, malformed UTF-8,
508
+ or another complete non-envelope error response. A mid-stream transport
509
+ failure is retried for a successful GET or a GET with a retryable status.
510
+ A hard 4xx GET is not retried. An independent `AbortError` or `TimeoutError`
511
+ from an injected fetch, or while reading a successful response body, is a
512
+ non-retried `NetworkError`. After non-success headers arrive, the SDK
513
+ preserves the status-derived error class and attaches the independent
514
+ failure as its cause without retrying it. Only the SDK's timeout signal
515
+ produces a `TimeoutError`. Mutations require reconciliation.
516
+
517
+ All documented no-content DELETE operations require exactly HTTP 204 with an
518
+ empty response body. Alternate success statuses or response bytes fail closed
519
+ as `unexpected_response` contract errors whose `.status` preserves the
520
+ observed HTTP status. The delete may already have applied; reconcile resource
521
+ state before retrying.
321
522
 
322
523
  Configure with `maxRetries` (default: 3). Set to `0` to disable.
323
524
 
525
+ When DELETE returns `RateLimitError`, use `retryAfter` for scheduling but
526
+ reconcile current resource state before issuing a deliberate retry; the SDK
527
+ does not assume the rejected response proves the mutation never ran.
528
+
324
529
  > **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
530
 
326
531
  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 +545,51 @@ SDK-generated keys require `globalThis.crypto.getRandomValues`, which is availab
340
545
  ## Security Notes
341
546
 
342
547
  - Treat API keys and qURL links like credentials. Do not log them.
548
+ - SDK API requests use manual redirect handling. Redirect-capable HTTP statuses
549
+ (300, 301, 302, 303, 305, 307, 308), filtered `opaqueredirect` responses in
550
+ browsers and Node native fetch, and responses a custom fetch reports as
551
+ already redirected are rejected as a typed `QURLError`
552
+ (`code: "unexpected_response"`) without requesting the `Location` target.
553
+ This prevents forwarding `Authorization` and
554
+ `Idempotency-Key` when the fetch implementation honors `redirect: "manual"`
555
+ and accurately exposes `Response.redirected`. A shim must also leave
556
+ `Response.url` empty or report the normalized request URL when it did not
557
+ follow a redirect. Every Response-like shim must provide `headers.get(name)`.
558
+ A non-empty invalid `Response.url` fails closed with an SDK-authored error
559
+ that names this shim requirement but does not reflect the invalid value. A
560
+ non-redirecting 304 is handled as an ordinary unsuccessful API response
561
+ rather than mislabeled as a redirect.
562
+ - API success and error bodies are limited to **1 MiB (1,048,576 bytes)**,
563
+ matching qurl-go's security posture. The SDK checks `Content-Length` when
564
+ present and independently counts streamed bytes, so missing or inaccurate
565
+ headers cannot bypass the limit. Bodies exactly at the limit are accepted.
566
+ This fixed security limit has no override. Callers must request a smaller
567
+ page, and the SDKs must not independently widen the limit.
568
+ A maximum-size list page can exceed the cap after JSON escaping; request a
569
+ smaller page if a list call reports the body-limit error. For resumable list
570
+ reads, use the page method, save each successful `next_cursor`, and resume
571
+ from that cursor with a smaller `limit`. A shared configurable-cap decision
572
+ is tracked in [#249](https://github.com/layervai/qurl-typescript/issues/249).
573
+ Standards-compliant fetch implementations are bounded while streaming;
574
+ custom Response-like shims that omit `body` are validated after their
575
+ `text()`/`json()` result has already been materialized by that shim.
576
+ Oversized bodies fail with a typed error before JSON decoding while
577
+ preserving the observed status-derived error class and `Retry-After`.
578
+ Transient error statuses retain the normal retry policy for GET and
579
+ idempotency-key-backed mutations; oversized successful responses and DELETE
580
+ responses are not retried.
581
+ Successful JSON bodies with malformed UTF-8 are rejected without replay.
582
+ Server-provided error title/detail snippets and `invalidFields` keys/values
583
+ have controls and bidirectional formatting characters removed, are
584
+ normalized to one line, and are capped at 512 UTF-8 bytes. Machine-readable
585
+ error codes, RFC 7807 type/instance values, and request IDs are kept only
586
+ when they need no normalization and fit the same limit. At most 100
587
+ `invalidFields` entries are retained, and each retained
588
+ `invalidFields` or debug `body_keys` collection has an 8 KiB UTF-8 budget.
589
+ Redirect/body-limit contract-error details do not include `Location` values
590
+ or response-body snippets. Standard request debug logging includes the request
591
+ URL, so do not place credentials in identifiers or enable debug output in a
592
+ sensitive logging environment.
343
593
  - Prefer short portal lifetimes such as `validFor: '5m'`.
344
594
  - Do not ask portal recipients to handle credentials. Recipients only need
345
595
  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;
@@ -115,7 +123,8 @@ export declare class QURLClient {
115
123
  * number of milliseconds with qurl-go's client-side guardrails: whole
116
124
  * seconds only, at least one minute for `validFor` and one second for
117
125
  * `sessionDuration`. `maxSessions: 0` is sent explicitly and means
118
- * unlimited; blank labels are rejected.
126
+ * unlimited; blank labels are rejected. `targetPath` is valid only for an
127
+ * existing resource. The API owns its full grammar and tunnel-only gate.
119
128
  */
120
129
  createPortal(resource: ProtectedResource | string, opts?: CreatePortalOptions, requestOptions?: RequestOptions): Promise<Portal>;
121
130
  /**
@@ -127,7 +136,9 @@ export declare class QURLClient {
127
136
  * {@link protectUrl} when you need the full server-populated resource
128
137
  * metadata. Portal options match {@link createPortal}; `targetUrl` gets
129
138
  * the same malformed-URL and embedded-credentials rejection as
130
- * {@link protectUrl}. REST-shaped equivalent: {@link create}.
139
+ * {@link protectUrl}. `targetPath` is rejected because this call creates a
140
+ * URL resource instead of addressing an existing resource. REST-shaped
141
+ * equivalent: {@link create}.
131
142
  */
132
143
  createPortalForUrl(targetUrl: string, opts?: CreatePortalOptions, requestOptions?: RequestOptions): Promise<{
133
144
  portal: Portal;
@@ -307,8 +318,6 @@ export declare class QURLClient {
307
318
  resolve(input: ResolveInput | string, options?: RequestOptions): Promise<ResolveOutput>;
308
319
  /** Get quota and usage information. */
309
320
  getQuota(): Promise<Quota>;
310
- /** Bootstrap a LayerV qURL Connector agent. */
311
- bootstrapAgent(input: AgentBootstrapInput, options?: RequestOptions): Promise<AgentBootstrapOutput>;
312
321
  /** List resources from the `/v1/resources` API. */
313
322
  listResources(input?: ResourceListInput): Promise<ResourceListOutput>;
314
323
  /**
@@ -349,13 +358,7 @@ export declare class QURLClient {
349
358
  * this page and emits a debug log rather than pretending it fetched all pages.
350
359
  */
351
360
  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
- */
361
+ /** Terminate all active sessions for a resource and return the server count. */
359
362
  terminateAllResourceSessions(id: string): Promise<SessionTerminateOutput>;
360
363
  /** Terminate a specific resource session. */
361
364
  terminateResourceSession(id: string, sessionId: string): Promise<void>;
@@ -468,6 +471,12 @@ export declare class QURLClient {
468
471
  /** Revoke an access code. */
469
472
  revokeAccessCode(id: string): Promise<void>;
470
473
  private request;
474
+ /**
475
+ * Require the service's exact 204 response with no response bytes exposed by
476
+ * Fetch. A standards-compliant Fetch implementation discards any forbidden
477
+ * wire body on 204, so the SDK cannot detect bytes that Fetch did not expose.
478
+ */
479
+ private requestNoContent;
471
480
  /**
472
481
  * Issue an HTTP request and parse the JSON response.
473
482
  *
@@ -495,6 +504,7 @@ export declare class QURLClient {
495
504
  */
496
505
  private retryDelay;
497
506
  private classifyFetchError;
507
+ private classifyResponseReadError;
498
508
  }
499
509
  /**
500
510
  * A LayerV-protected resource, bound to the client that produced it.