@layervai/qurl 0.4.0 → 0.6.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 (51) hide show
  1. package/README.md +174 -61
  2. package/dist/cjs/client.d.ts +46 -20
  3. package/dist/cjs/client.js +338 -54
  4. package/dist/cjs/errors.d.ts +24 -3
  5. package/dist/cjs/errors.js +34 -4
  6. package/dist/cjs/index.d.ts +3 -2
  7. package/dist/cjs/index.js +5 -1
  8. package/dist/cjs/node/deployment.d.ts +1 -1
  9. package/dist/cjs/node/deployment.js +2 -2
  10. package/dist/cjs/node/native-udp.d.ts +2 -4
  11. package/dist/cjs/node/native-udp.js +8 -8
  12. package/dist/cjs/node/portal-opener.d.ts +54 -23
  13. package/dist/cjs/node/portal-opener.js +643 -340
  14. package/dist/cjs/node.d.ts +2 -2
  15. package/dist/cjs/node.js +8 -1
  16. package/dist/cjs/types.d.ts +18 -2
  17. package/dist/cjs/version.d.ts +1 -1
  18. package/dist/cjs/version.js +1 -1
  19. package/dist/esm/client.d.ts +46 -20
  20. package/dist/esm/client.d.ts.map +1 -1
  21. package/dist/esm/client.js +337 -54
  22. package/dist/esm/client.js.map +1 -1
  23. package/dist/esm/errors.d.ts +24 -3
  24. package/dist/esm/errors.d.ts.map +1 -1
  25. package/dist/esm/errors.js +32 -3
  26. package/dist/esm/errors.js.map +1 -1
  27. package/dist/esm/index.d.ts +3 -2
  28. package/dist/esm/index.d.ts.map +1 -1
  29. package/dist/esm/index.js +2 -2
  30. package/dist/esm/index.js.map +1 -1
  31. package/dist/esm/node/deployment.d.ts +1 -1
  32. package/dist/esm/node/deployment.d.ts.map +1 -1
  33. package/dist/esm/node/deployment.js +2 -2
  34. package/dist/esm/node/deployment.js.map +1 -1
  35. package/dist/esm/node/native-udp.d.ts +2 -4
  36. package/dist/esm/node/native-udp.d.ts.map +1 -1
  37. package/dist/esm/node/native-udp.js +5 -5
  38. package/dist/esm/node/native-udp.js.map +1 -1
  39. package/dist/esm/node/portal-opener.d.ts +54 -23
  40. package/dist/esm/node/portal-opener.d.ts.map +1 -1
  41. package/dist/esm/node/portal-opener.js +636 -340
  42. package/dist/esm/node/portal-opener.js.map +1 -1
  43. package/dist/esm/node.d.ts +2 -2
  44. package/dist/esm/node.d.ts.map +1 -1
  45. package/dist/esm/node.js +1 -1
  46. package/dist/esm/node.js.map +1 -1
  47. package/dist/esm/types.d.ts +18 -2
  48. package/dist/esm/types.d.ts.map +1 -1
  49. package/dist/esm/version.d.ts +1 -1
  50. package/dist/esm/version.js +1 -1
  51. package/package.json +4 -4
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
 
@@ -66,14 +68,60 @@ same URL again returns the existing resource. `validFor` accepts a duration
66
68
  string (`'5m'`, `'24h'`) or a number of milliseconds (whole seconds, at least
67
69
  one minute); prefer short portal lifetimes.
68
70
 
69
- If qURL Connector already protects the service, use the connector id instead
70
- of calling `protectUrl`:
71
+ If qURL Connector protects the service, address its management-plane resource
72
+ by immutable slug instead of calling `protectUrl`:
71
73
 
72
74
  ```typescript
73
- const resource = await client.connectorResource('prod-dashboard');
74
- const portal = await resource.createPortal({ validFor: '5m' });
75
+ const { resource, foundExisting } = await client.ensureConnectorResource(
76
+ 'prod-dashboard',
77
+ { idempotencyKey: 'connector-bootstrap-prod-dashboard' },
78
+ );
79
+ console.log(foundExisting ? 'Using existing connector resource' : 'Created connector resource');
80
+ const portal = await resource.createPortal({
81
+ validFor: '5m',
82
+ targetPath: '/api/detect/eib_example',
83
+ });
75
84
  ```
76
85
 
86
+ `targetPath` is available only when minting for an existing resource. The SDK
87
+ checks the non-empty 2048-byte boundary; the API remains authoritative for the
88
+ path grammar and the tunnel-only resource gate. `createPortalForUrl` rejects
89
+ this option because it creates a URL resource.
90
+
91
+ `resource.resourceId`, `resource.connectorRoutingId`, and
92
+ `resource.knockResourceId` are three distinct server-issued values for public
93
+ identity, reverse routing, and NHP admission. Consume each verbatim; never
94
+ derive or substitute one for another. Use `getConnectorResource(resourceId)`
95
+ or `getConnectorResourceBySlug(slug)` for read-only lookup and
96
+ `deleteConnectorResource(resourceId)` to revoke it. This replaces the old
97
+ alias-based `connectorResource(connectorId)` method.
98
+ `ConnectorResource` instances cannot be constructed directly; the client
99
+ returns them only after validating the complete response contract.
100
+
101
+ `ensureConnectorResource` and `deleteConnectorResource` throw
102
+ `ConnectorResourceOutcomeUnknownError` when a dispatched mutation may have
103
+ committed but its response cannot prove the result. Reconcile by immutable slug
104
+ or resource ID before deciding whether to retry. The wrapper deliberately uses
105
+ `status: 0`; the original typed error is available as `cause`, including its
106
+ observed HTTP status.
107
+
108
+ Connector lifecycle calls make one HTTP attempt. The SDK does not pace or
109
+ replay them; the caller controls any retry after it reconciles state.
110
+
111
+ `ensureConnectorResource` does not generate an idempotency key because the slug
112
+ operation is already idempotent. If you supply an `idempotencyKey`, the SDK
113
+ forwards it, and you must reuse it on any deliberate retry. A 409
114
+ `bootstrap_key_consumed` response is outcome-unknown because resource binding
115
+ can finish before bootstrap-key consumption fails. The original bootstrap key
116
+ is terminal: do not retry it. Obtain a new bootstrap key and use normal
117
+ owner-authenticated lookup by immutable slug to reconcile the resource first.
118
+
119
+ The API does not apply idempotency replay to DELETE, so after an outcome-unknown
120
+ `deleteConnectorResource` call, reconcile by resource ID before issuing a
121
+ deliberate retry. A valid exact-201 resource missing only `meta.found_existing`
122
+ is known to have selected that row but still fails as an unwrapped
123
+ `unexpected_response` because required ensure metadata is absent.
124
+
77
125
  If you persist the resource id, future calls do not need to recreate the
78
126
  handle (no API call is made until you mint):
79
127
 
@@ -102,11 +150,15 @@ const portal = await resource.createPortal({
102
150
  label: 'Alice from Acme',
103
151
  oneTimeUse: true,
104
152
  maxSessions: 1,
153
+ targetPath: '/api/detect/eib_example',
105
154
  });
106
155
  ```
107
156
 
157
+ `createPortal` sends `{}` when no options are set because the service requires
158
+ a JSON object at the wire level.
159
+
108
160
  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
161
+ NHP UDP. It has no relay or HTTP-resolve fallback. Connector assignment and
110
162
  registration are separate producer operations and are not part of the portal
111
163
  opener.
112
164
 
@@ -133,10 +185,15 @@ back, it throws instead of returning an empty handle.
133
185
  Use the Node subpath when a service receives a qv2 link and must keep one NHP
134
186
  session ready for a low-latency private request. Construct and start one opener
135
187
  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.
188
+ background renewal before the admission expires. A transient renewal failure
189
+ retries after 500 ms, then 1 second, then at most every 2 seconds while the old
190
+ admission remains valid. `fetch()` never opens or renews a session, and it never
191
+ sleeps. It fails if the cached admission has expired.
192
+
193
+ Renewal waits at least 5 seconds after a successful open. If a server grants a
194
+ session that expires before that safe renewal point, the grant expires without
195
+ a background attempt and `start()` is the explicit recovery path. Production
196
+ admissions should be longer than this minimum gap.
140
197
 
141
198
  ```javascript
142
199
  const { createPortalOpener } = require('@layervai/qurl/node');
@@ -144,7 +201,6 @@ const { createPortalOpener } = require('@layervai/qurl/node');
144
201
  async function uploadPrivateObject(uploadBody) {
145
202
  const opener = createPortalOpener({
146
203
  qurl: process.env.PRIVATE_UPLOAD_QURL,
147
- transport: 'native-only',
148
204
  });
149
205
  try {
150
206
  await opener.start();
@@ -162,65 +218,120 @@ async function uploadPrivateObject(uploadBody) {
162
218
  }
163
219
  ```
164
220
 
165
- The request builder receives a copy of the exact authenticated ACK target. The
221
+ For a service whose ACK target is a base path, append only trusted raw path
222
+ segments with `fetchDescendant()`. The opener rejects empty, dot, and delimiter
223
+ segments and escapes each accepted segment before it sends the request:
224
+
225
+ ```javascript
226
+ const response = await opener.fetchDescendant(
227
+ ['eib_example'],
228
+ (authenticatedTarget) => ({ method: 'POST' }),
229
+ { redirects: 'error' },
230
+ );
231
+ ```
232
+
233
+ The request builder receives a copy of the selected authenticated target. The
166
234
  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.
235
+ fixed ACK URL or the validated descendant. A descendant call accepts raw path
236
+ segments, not a URL or path string. It preserves the ACK target query and
237
+ escapes each segment separately. It adds the private `qurl_vsession` cookie,
238
+ replaces a caller-supplied cookie with that name, and preserves other valid
239
+ cookies, including duplicate `Cookie` entries that Node joins with semicolons.
240
+ It does not accept a caller URL, and it rejects a caller-supplied `Host` header
241
+ so Fetch derives the authority from the pinned target. Both `fetch()` and
242
+ `fetchDescendant()` use only the cached NHP 1.1 admission. Use
243
+ `redirects: 'error'` to keep a descendant request at its initial target. The
244
+ default `follow` mode can move outside that path subtree, but only within the
245
+ authenticated origin. Also use `redirects: 'error'` for a request whose
246
+ signature binds its method, target, timestamp, or nonce. This mode closes a
247
+ redirect response and does not replay the request. Follow mode permits at most
248
+ 10 requests, including the initial request, and uses the standard 301/302/303
249
+ method rewrite rules. As in Go, a 3xx response with no `Location` header, or a
250
+ 307/308 response whose streaming body cannot be replayed, is returned to the
251
+ caller without a follow-up request. The caller then owns that response body and
252
+ must consume or cancel it. Local admission expiry is checked before the first
253
+ request; the protected service remains authoritative while a permitted redirect
254
+ chain is in progress.
255
+
256
+ When `fetch()` rejects before it sends a direct `RequestInit`, it releases a
257
+ caller-owned streaming body. It cancels a web `ReadableStream`, destroys a Node
258
+ `Readable`, or closes another iterator when that body supports the operation. A
259
+ request builder remains lazy and is not called until a grant is ready.
260
+
261
+ Each open has a whole-operation deadline that covers DNS and UDP.
262
+ `openTimeoutMs` sets this ceiling; the default is 15 seconds and the maximum is
263
+ 60 seconds. Native DNS or address attempts can fail before this ceiling, so a
264
+ larger value does not extend their internal timeouts. A renewal also stops at
265
+ the old grant's expiry. Pass an abort signal to `start()` when lifecycle code
266
+ needs a shorter deadline. Concurrent `start()` calls share one attempt. The
267
+ first caller's signal owns that attempt, so its abort fails all waiters but does
268
+ not count as an open failure in health. A cold cancellation restores `new`; a
269
+ recovery cancellation preserves its prior `degraded` health. A later waiter's
270
+ abort stops only that wait.
187
271
  Content requests use the caller's `RequestInit.signal`; `fetch()` does not add
188
- an independent application-request deadline.
272
+ an independent application-request deadline. The signal stays active while the
273
+ returned response body is read. Set the opener's optional `fetch` when the
274
+ protected request must use a custom Fetch implementation. Native NHP opening
275
+ never uses this function. The custom function receives the `qurl_vsession`
276
+ bearer cookie and is inside the credential boundary. It must implement standard
277
+ Fetch signal behavior and honor `redirect: 'manual'`. If it needs a receiver,
278
+ pass it already bound; the SDK invokes it with the standard global Fetch
279
+ receiver. An unfollowed synthetic response can leave `Response.url` empty.
280
+ Otherwise, it must report
281
+ `Response.url` and `Response.redirected` accurately. It must not log or forward
282
+ protected request headers.
189
283
 
190
284
  TypeScript consumers of `@layervai/qurl/node` must provide Node and Fetch API
191
285
  declarations, for example current `@types/node`, or a configuration that includes
192
- the `DOM` library for Fetch types.
286
+ the `DOM` library for Fetch types. The native opener requires Node 22.12 or later.
287
+ This floor ensures that composite request signals are reclaimed on long-lived
288
+ openers.
193
289
 
194
290
  Native opening requires public deployment trust. Set `QURL_DEPLOYMENT` to one
195
291
  strict JSON object or to a path that contains that object. The object must have
196
292
  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.
293
+ key entries. A configured path must be a regular file no larger than 1 MiB.
294
+ Construction does no I/O. `start()` resolves and validates the deployment, and
295
+ the first successful open pins that resolved trust for renewal and recovery.
296
+ The opener does not perform discovery. It fails before DNS if the verified link
297
+ names an unknown cell.
298
+
299
+ Use `opener.health()` for the local `new`, `starting`, `ready`, `degraded`, or
300
+ `closed` state. It reports absolute expiry, renewal, and last-success times, a
301
+ secret-free failure class, and the number of consecutive unsuccessful open
302
+ attempts. A renewal window can contain more than one such attempt. The
303
+ `starting` state is only for the first cold open; an explicit recovery from a
304
+ failure reports `degraded` until it succeeds. Date fields describe the most
305
+ recent successful grant, so use `ready` as the authority for current usability.
306
+ A transient renewal failure keeps the state `ready` while the prior admission
307
+ is usable. A changed authenticated target is not retried. It keeps the prior
308
+ admission until expiry. During that time, `start()` remains idempotent because
309
+ the old admission is still usable; `health().lastFailureClass` reports the
310
+ target change. After expiry, an explicit `start()` attempts recovery. If the
311
+ target change is intentional and permanent, create a new opener for the new
312
+ qURL; the old opener stays bound to its first authenticated target. Other
313
+ failures retry through the remaining admission window. This does not put an
314
+ open or sleep on the `fetch()` path.
315
+
316
+ Close cancels and waits for an active NHP open, then wipes the mutable
317
+ private-key, visitor-secret, and session-token buffers. Close also aborts a
318
+ protected request and its returned response body, and prevents another redirect
319
+ leg from starting. JavaScript can create immutable string copies during JSON
320
+ and HTTP processing, so the SDK cannot promise full memory zeroization before garbage
321
+ collection. The opener retains the immutable qURL string until close so it can
322
+ verify each renewal. Never log the qURL, ACK body, request cookies, or request
323
+ headers.
324
+
325
+ A cold `start()` failure leaves health at `degraded`. Catch `PortalBusyError`
326
+ for an authenticated COOKIE busy response and `PortalInvalidReplyError` for a
327
+ malformed authenticated reply. An open deadline throws `PortalOpenTimeoutError`.
328
+ The thrown typed error is the cold-start
329
+ diagnostic; health does not expose its message or secret values.
219
330
 
220
331
  Local qv2 verification checks the signed bytes, trust, and clock-free claim
221
332
  ordering. As in the Go SDK, it does not compare `nbf` or `exp` with the local
222
333
  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
334
+ The opener also follows the Go NHP COOKIE rule: an authenticated COOKIE is
224
335
  a busy result and does not use ACK counter correlation or local clock skew
225
336
  checks. Replaying it can only force another busy result; it cannot grant access.
226
337
 
@@ -264,9 +375,11 @@ console.log(`Access granted to ${access.target_url} for ${access.access_grant?.e
264
375
  | Method | Description |
265
376
  |--------|-------------|
266
377
  | `protectUrl(targetUrl, opts?)` | Protect a private URL → portal-minting `ProtectedResource` handle |
267
- | `resource.createPortal(opts?)` / `createPortal(resourceOrId, opts?)` | Mint a short-lived portal link (`Portal`) |
268
- | `createPortalForUrl(targetUrl, opts?)` | Protect + mint in one API call → `{ portal, resource }` |
269
- | `connectorResource(connectorId)` | Handle for a service qURL Connector already protects |
378
+ | `resource.createPortal(opts?)` / `createPortal(resourceOrId, opts?)` | Mint a short-lived portal link; existing resources can set `targetPath` |
379
+ | `createPortalForUrl(targetUrl, opts?)` | Protect + mint a URL resource; rejects `targetPath` |
380
+ | `ensureConnectorResource(slug, requestOptions?)` | Find or create an active Connector resource by immutable slug |
381
+ | `getConnectorResource(resourceId)` / `getConnectorResourceBySlug(slug)` | Load a validated Connector resource by immutable identity |
382
+ | `deleteConnectorResource(resourceId)` | Revoke a Connector resource by immutable public resource ID |
270
383
  | `resourceById(id)` | Handle from a stored resource id (no API call) |
271
384
  | `enterPortal(linkOrToken)` | Open a qURL link programmatically → `ResourceHandle` |
272
385
 
@@ -379,7 +492,7 @@ try {
379
492
  Client-detected failures use `status: 0` with a discriminating `code`:
380
493
  `"client_validation"` for bad input caught before a request, and — on the
381
494
  portal surface — `"resource_not_found"` / `"ambiguous_resource"` when
382
- `connectorResource` cannot resolve a connector id to exactly one resource,
495
+ `getConnectorResourceBySlug` cannot resolve a slug to exactly one resource,
383
496
  and `"unexpected_response"` when a response is missing required fields (e.g.
384
497
  `enterPortal` failing closed on a grant with no resource URL).
385
498
 
@@ -92,23 +92,19 @@ export declare class QURLClient {
92
92
  * need the full resource details.
93
93
  */
94
94
  resourceById(id: string): ProtectedResource;
95
+ /** Find or create the active qURL Connector resource for an immutable slug. */
96
+ ensureConnectorResource(slug: string, requestOptions?: RequestOptions): Promise<EnsureConnectorResourceResult>;
97
+ /** Fetch a qURL Connector resource by its immutable public resource ID. */
98
+ getConnectorResource(resourceId: string): Promise<ConnectorResource>;
99
+ /** Fetch the single active qURL Connector resource for an immutable slug. */
100
+ getConnectorResourceBySlug(slug: string): Promise<ConnectorResource>;
95
101
  /**
96
- * Return the resource qURL Connector created for `connectorId`.
102
+ * Revoke a qURL Connector resource by immutable public resource ID.
97
103
  *
98
- * Use when qURL Connector already protects the service — do not call
99
- * {@link protectUrl} again for the same service. The connector id is the
100
- * resource slug LayerV stores for that connector; the LayerV API performs
101
- * the slug lookup, and the SDK confirms the returned alias matches
102
- * `connectorId` before binding the handle (qurl-go:
103
- * `Client.ConnectorResource`).
104
- *
105
- * Throws {@link NotFoundError} (`status: 0`, `code: "resource_not_found"`)
106
- * when no resource exists for the connector id, {@link QURLError}
107
- * (`code: "ambiguous_resource"`) when the lookup returns more than one
108
- * resource, and {@link ValidationError} (`code: "unexpected_response"`)
109
- * when the returned resource's alias is missing or does not match.
104
+ * The API does not apply Idempotency-Key replay to DELETE. After an
105
+ * outcome-unknown failure, reconcile by ID before issuing a deliberate retry.
110
106
  */
111
- connectorResource(connectorId: string): Promise<ProtectedResource>;
107
+ deleteConnectorResource(resourceId: string): Promise<void>;
112
108
  /**
113
109
  * Mint a short-lived qURL link (a portal) for a protected resource.
114
110
  *
@@ -116,14 +112,15 @@ export declare class QURLClient {
116
112
  * reach one private resource. Recipients open `portal.link` directly and
117
113
  * need no LayerV credentials. Prefer short lifetimes such as
118
114
  * `{ validFor: "5m" }`. Accepts a {@link ProtectedResource} handle or a
119
- * resource id (`r_` prefix). REST-shaped equivalents: {@link mintLink} /
115
+ * public resource ID string. REST-shaped equivalents: {@link mintLink} /
120
116
  * {@link createQurlForResource}.
121
117
  *
122
118
  * Duration options take a string (`"5m"`, `"24h"`; server-validated) or a
123
119
  * number of milliseconds with qurl-go's client-side guardrails: whole
124
120
  * seconds only, at least one minute for `validFor` and one second for
125
121
  * `sessionDuration`. `maxSessions: 0` is sent explicitly and means
126
- * unlimited; blank labels are rejected.
122
+ * unlimited; blank labels are rejected. `targetPath` is valid only for an
123
+ * existing resource. The API owns its full grammar and tunnel-only gate.
127
124
  */
128
125
  createPortal(resource: ProtectedResource | string, opts?: CreatePortalOptions, requestOptions?: RequestOptions): Promise<Portal>;
129
126
  /**
@@ -135,7 +132,9 @@ export declare class QURLClient {
135
132
  * {@link protectUrl} when you need the full server-populated resource
136
133
  * metadata. Portal options match {@link createPortal}; `targetUrl` gets
137
134
  * the same malformed-URL and embedded-credentials rejection as
138
- * {@link protectUrl}. REST-shaped equivalent: {@link create}.
135
+ * {@link protectUrl}. `targetPath` is rejected because this call creates a
136
+ * URL resource instead of addressing an existing resource. REST-shaped
137
+ * equivalent: {@link create}.
139
138
  */
140
139
  createPortalForUrl(targetUrl: string, opts?: CreatePortalOptions, requestOptions?: RequestOptions): Promise<{
141
140
  portal: Portal;
@@ -506,14 +505,13 @@ export declare class QURLClient {
506
505
  /**
507
506
  * A LayerV-protected resource, bound to the client that produced it.
508
507
  *
509
- * Obtained from {@link QURLClient.protectUrl},
510
- * {@link QURLClient.connectorResource}, or {@link QURLClient.resourceById} —
508
+ * Obtained from {@link QURLClient.protectUrl} or {@link QURLClient.resourceById} —
511
509
  * not constructed directly. Mint short-lived access links for the resource
512
510
  * with {@link createPortal} (qurl-go: `qurl.Resource`).
513
511
  */
514
512
  export declare class ProtectedResource {
515
513
  #private;
516
- /** The LayerV resource id (`r_` prefix). */
514
+ /** The LayerV public resource ID. */
517
515
  readonly id: string;
518
516
  /** The private URL protected by this resource, when known. */
519
517
  readonly targetUrl?: string;
@@ -537,3 +535,31 @@ export declare class ProtectedResource {
537
535
  */
538
536
  createPortal(opts?: CreatePortalOptions, requestOptions?: RequestOptions): Promise<Portal>;
539
537
  }
538
+ /** Result of idempotently ensuring a qURL Connector resource. */
539
+ export interface EnsureConnectorResourceResult {
540
+ resource: ConnectorResource;
541
+ foundExisting: boolean;
542
+ }
543
+ declare const CONNECTOR_RESOURCE_CONSTRUCTOR_TOKEN: unique symbol;
544
+ /**
545
+ * A validated qURL Connector resource bound to the client that loaded it.
546
+ *
547
+ * Resource identity, reverse-routing identity, and NHP admission identity are
548
+ * distinct server-issued values. Callers must consume each field verbatim and
549
+ * must never derive one from another.
550
+ */
551
+ export declare class ConnectorResource {
552
+ #private;
553
+ readonly resourceId: string;
554
+ /** Producer-supplied CRID carried verbatim; verify it against a trusted key before trust. */
555
+ readonly crid?: string;
556
+ readonly connectorRoutingId: string;
557
+ readonly knockResourceId: string;
558
+ readonly slug: string;
559
+ readonly alias?: string;
560
+ /** @internal Instances are returned by QURLClient after wire validation. */
561
+ constructor(client: QURLClient, details: Resource, validationToken: typeof CONNECTOR_RESOURCE_CONSTRUCTOR_TOKEN);
562
+ /** Mint a short-lived portal for this Connector resource. */
563
+ createPortal(opts?: CreatePortalOptions, requestOptions?: RequestOptions): Promise<Portal>;
564
+ }
565
+ export {};