@layervai/qurl 0.5.0 → 0.7.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.
package/README.md CHANGED
@@ -68,11 +68,15 @@ same URL again returns the existing resource. `validFor` accepts a duration
68
68
  string (`'5m'`, `'24h'`) or a number of milliseconds (whole seconds, at least
69
69
  one minute); prefer short portal lifetimes.
70
70
 
71
- If qURL Connector already protects the service, use the connector id instead
72
- of calling `protectUrl`:
71
+ If qURL Connector protects the service, address its management-plane resource
72
+ by immutable slug instead of calling `protectUrl`:
73
73
 
74
74
  ```typescript
75
- const resource = await client.connectorResource('prod-dashboard');
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');
76
80
  const portal = await resource.createPortal({
77
81
  validFor: '5m',
78
82
  targetPath: '/api/detect/eib_example',
@@ -84,12 +88,52 @@ checks the non-empty 2048-byte boundary; the API remains authoritative for the
84
88
  path grammar and the tunnel-only resource gate. `createPortalForUrl` rejects
85
89
  this option because it creates a URL resource.
86
90
 
87
- If you persist the resource id, future calls do not need to recreate the
91
+ `resource.crid` is the required management identifier. Connector lookup, delete,
92
+ and portal minting use it with no public-key or private-ID fallback. The SDK
93
+ checks that the returned public key matches the CRID.
94
+
95
+ `resource.resourcePublicKey`, `resource.connectorRoutingId`, and
96
+ `resource.knockResourceId` are three distinct server-issued values for verification, reverse routing, and NHP admission. Consume each verbatim; never
97
+ derive or substitute one for another. Use `getConnectorResource(crid)`
98
+ or `getConnectorResourceBySlug(slug)` for read-only lookup and
99
+ `deleteConnectorResource(crid)` to revoke it. This replaces the old
100
+ alias-based `connectorResource(connectorId)` method.
101
+ `ConnectorResource` instances cannot be constructed directly; the client
102
+ returns them only after validating the complete response contract.
103
+
104
+ `ensureConnectorResource` and `deleteConnectorResource` throw
105
+ `ConnectorResourceOutcomeUnknownError` when a dispatched mutation may have
106
+ committed but its response cannot prove the result. Reconcile by immutable slug
107
+ or CRID before deciding whether to retry. The wrapper deliberately uses
108
+ `status: 0`; the original typed error is available as `cause`, including its
109
+ observed HTTP status.
110
+
111
+ Connector lifecycle calls make one HTTP attempt. The SDK does not pace or
112
+ replay them; the caller controls any retry after it reconciles state.
113
+
114
+ `ensureConnectorResource` does not generate an idempotency key because the slug
115
+ operation is already idempotent. If you supply an `idempotencyKey`, the SDK
116
+ forwards it, and you must reuse it on any deliberate retry. A 409
117
+ `bootstrap_key_consumed` response is outcome-unknown because resource binding
118
+ can finish before bootstrap-key consumption fails. The original bootstrap key
119
+ is terminal: do not retry it. Obtain a new bootstrap key and use normal
120
+ owner-authenticated lookup by immutable slug to reconcile the resource first.
121
+
122
+ The API does not apply idempotency replay to DELETE, so after an outcome-unknown
123
+ `deleteConnectorResource` call, reconcile by CRID before issuing a
124
+ deliberate retry. A valid exact-201 resource missing only `meta.found_existing`
125
+ is known to have selected that row but still fails as an unwrapped
126
+ `unexpected_response` because required ensure metadata is absent.
127
+
128
+ If you persist the CRID, future calls do not need to recreate the
88
129
  handle (no API call is made until you mint):
89
130
 
90
131
  ```typescript
91
- const resource = client.resourceById('r_demo1234567');
92
- const portal = await resource.createPortal({ validFor: '1h' });
132
+ async function createPortalForStoredResource(storedCrid: string) {
133
+ // Pass the CRID previously returned by the API.
134
+ const resource = client.resourceByCrid(storedCrid);
135
+ return resource.createPortal({ validFor: '1h' });
136
+ }
93
137
  ```
94
138
 
95
139
  For one-off scripts, `client.createPortalForUrl` combines the two API calls
@@ -116,11 +160,105 @@ const portal = await resource.createPortal({
116
160
  });
117
161
  ```
118
162
 
163
+ `createPortal` sends `{}` when no options are set because the service requires
164
+ a JSON object at the wire level.
165
+
119
166
  The Node-only `@layervai/qurl/node` entry opens received qv2 links with native
120
167
  NHP UDP. It has no relay or HTTP-resolve fallback. Connector assignment and
121
168
  registration are separate producer operations and are not part of the portal
122
169
  opener.
123
170
 
171
+ If you already hold a CRID, mint a fresh share link directly:
172
+
173
+ ```typescript
174
+ const share = await client.shareResource(crid, { ttlSeconds: 300 });
175
+ // Optional: verify the response CRID against DER SPKI bytes you already trust
176
+ // before the secret leaves this process.
177
+ await share.verifyCrid(resourcePublicKeyDer);
178
+ await deliverToRecipient(share.link); // Secret; returned once and not retrievable
179
+ console.log(share.qurlId); // Safe handle for revoking only this link later
180
+ ```
181
+
182
+ `shareResource` returns the current share-safe `#qv2t1...` link. Recipients
183
+ open that URL in the qURL browser flow. For native TypeScript opening, pass
184
+ `share.link` to `createPortalOpener` from `@layervai/qurl/node` with trusted
185
+ `QURL_DEPLOYMENT` configuration. See the native opener example above.
186
+ `verifyCrid` does not open the link or make a network request.
187
+
188
+ `verifyCrid` verifies that the response CRID derives from the trusted resource
189
+ key; it does not independently bind the secret link fragment to that key.
190
+ It intentionally follows qurl-go's environment-agnostic key-match rule: CRID
191
+ version zero is rejected as reserved, while every other structurally valid
192
+ version is accepted and environment classification is not part of key-to-digest
193
+ verification. It compares all 32 digest bytes for a full CRID and all 24 for
194
+ a truncated CRID; the response determines the digest width. CRIDs use their canonical lowercase base32 spelling; uppercase or
195
+ mixed-case spellings fail closed. Binary inputs of any length are hashed;
196
+ `invalid_crid_key` is reserved for non-binary or unreadable (detached) runtime
197
+ values, and a readable binary key that does not match reports `crid_mismatch`.
198
+ Use `error instanceof CRIDVerificationError` (or `error.status === 0`) to
199
+ distinguish these local verification failures from a server error that happens
200
+ to use the same problem code.
201
+ `qurlId` and `singleUse` are undefined when omitted by the service.
202
+ `ShareLink` keeps `.link` directly readable but redacts it from JSON, Node
203
+ inspection, and object spread to reduce accidental credential logging. Read
204
+ `.link` directly before delivery; do not spread or clone a `ShareLink`, because
205
+ those operations deliberately omit the credential. Browser developer consoles
206
+ can still display non-enumerable properties, so do not log the object there.
207
+ The constructor snapshots a caller-provided expiry, and the `ShareLink`
208
+ properties are frozen. Each `expiresAt` read returns a defensive `Date` copy;
209
+ changing that copy does not change the share metadata.
210
+
211
+ Omitting `ttlSeconds` matches qurl-go's zero-value behavior and requests the
212
+ platform default. TypeScript additionally rejects an explicit zero so a
213
+ computed countdown cannot silently become a longer-lived default credential.
214
+ A response can report `expiresInSeconds: 0`; do not reuse that value as a
215
+ request TTL. Expiry parsing uses the SDK's shared API date parser.
216
+
217
+ The service chooses single-use policy; this matches the Go SDK options.
218
+
219
+ qURL Connector assignment and registration use native UDP through
220
+ `qurl-connector` and `qurl-go`. This package does not expose an HTTP enrollment API.
221
+ Like the Go SDK, credential minting uses HTTPS and token consumption uses
222
+ native UDP. The Go SDK currently exposes minting through its restricted
223
+ `RegisteredAgentResourceHTTPDoer` bridge; this SDK uses `createApiKey`.
224
+ The service controls which credential kinds each caller can mint.
225
+ It can mint the one-shot credential consumed by that native enrollment flow:
226
+
227
+ ```typescript
228
+ const enrollment = await client.createApiKey({
229
+ kind: 'enrollment_token',
230
+ name: 'prod-dashboard enrollment',
231
+ target: 'connector',
232
+ claims: [{ type: 'connector', id: 'prod-dashboard' }],
233
+ expires_in: '15m',
234
+ });
235
+ if (!enrollment.api_key) throw new Error('Enrollment response omitted its one-time token');
236
+ await deliverEnrollmentTokenSecurely(enrollment.api_key);
237
+ ```
238
+
239
+ Durable `api_key` credentials require explicit scopes and do not accept
240
+ `expires_in`; enrollment tokens derive their scopes from `target`/`claims` and
241
+ expire within 24 hours. A connector claim's `id` is its immutable connector
242
+ slug. Request enums are validated against the current service contract;
243
+ response types remain additive for forward-compatible reads, and new request
244
+ enum values require a matching SDK release. Use
245
+ `isApiKeyRequestScope(scope)` to validate response scopes before writing them
246
+ back. Reject unknown scopes; do not filter them out, which can remove permissions.
247
+ When changing only a name, omit `scopes` from `updateApiKey`.
248
+
249
+ ```typescript
250
+ import { isApiKeyRequestScope } from '@layervai/qurl';
251
+
252
+ const scopes = key.scopes;
253
+ if (!scopes?.length || !scopes.every(isApiKeyRequestScope)) {
254
+ throw new Error('Cannot reuse these scopes; check the current SDK contract');
255
+ }
256
+ await client.updateApiKey(keyId, { scopes });
257
+ ```
258
+
259
+ This credential surface requires the kind-first qurl-service contract at or
260
+ after commit `047cf31e1cdf545e3060e0f9294d738a19fb997b`.
261
+
124
262
  ## Opening Portals
125
263
 
126
264
  Most recipients open qURL links directly and do not use this SDK at all. If
@@ -336,8 +474,10 @@ console.log(`Access granted to ${access.target_url} for ${access.access_grant?.e
336
474
  | `protectUrl(targetUrl, opts?)` | Protect a private URL → portal-minting `ProtectedResource` handle |
337
475
  | `resource.createPortal(opts?)` / `createPortal(resourceOrId, opts?)` | Mint a short-lived portal link; existing resources can set `targetPath` |
338
476
  | `createPortalForUrl(targetUrl, opts?)` | Protect + mint a URL resource; rejects `targetPath` |
339
- | `connectorResource(connectorId)` | Handle for a service qURL Connector already protects |
340
- | `resourceById(id)` | Handle from a stored resource id (no API call) |
477
+ | `ensureConnectorResource(slug, requestOptions?)` | Find or create an active Connector resource by immutable slug |
478
+ | `getConnectorResource(crid)` / `getConnectorResourceBySlug(slug)` | Load a validated Connector resource by immutable identity |
479
+ | `deleteConnectorResource(crid)` | Revoke a Connector resource by CRID |
480
+ | `resourceByCrid(crid)` | Handle from a stored CRID (no API call) |
341
481
  | `enterPortal(linkOrToken)` | Open a qURL link programmatically → `ResourceHandle` |
342
482
 
343
483
  ### REST-shaped methods
@@ -399,7 +539,7 @@ if (result.failed > 0) {
399
539
 
400
540
  Non-400 errors (401, 403, 429, 5xx, and unexpected 400 body shapes) still throw the appropriate `QURLError` subclass.
401
541
 
402
- **Slimmer per-item shape** — `BatchItemSuccess` returns `{ resource_id, qurl_link, qurl_site, expires_at? }` per item. Unlike single `client.create()`, the batch response intentionally **omits `qurl_id` and `label`** to keep the payload compact. If you migrate a per-item `create()` loop to `batchCreate` and rely on `qurl_id` for downstream addressing, fetch each via `client.get(resource_id)` after the batch (or stay on the single-create path).
542
+ **Slimmer per-item shape** — `BatchItemSuccess` returns `{ crid, resource_id, qurl_link, qurl_site, expires_at? }` per item. Unlike single `client.create()`, the batch response intentionally **omits `qurl_id` and `label`** to keep the payload compact. If you migrate a per-item `create()` loop to `batchCreate` and rely on `qurl_id` for downstream addressing, check `item.crid` is present, then fetch each via `client.get(item.crid)` after the batch (or stay on the single-create path).
403
543
 
404
544
  **Result ordering** — `result.results` is **not** guaranteed to be sorted by `index`. Each entry's `index` field carries the position in the original `items` array, so build per-input-position state by keying on `r.index` (e.g., `for (const r of result.results) { byInputIndex[r.index] = r; }`) rather than relying on iteration order.
405
545
 
@@ -449,7 +589,7 @@ try {
449
589
  Client-detected failures use `status: 0` with a discriminating `code`:
450
590
  `"client_validation"` for bad input caught before a request, and — on the
451
591
  portal surface — `"resource_not_found"` / `"ambiguous_resource"` when
452
- `connectorResource` cannot resolve a connector id to exactly one resource,
592
+ `getConnectorResourceBySlug` cannot resolve a slug to exactly one resource,
453
593
  and `"unexpected_response"` when a response is missing required fields (e.g.
454
594
  `enterPortal` failing closed on a grant with no resource URL).
455
595
 
@@ -461,7 +601,8 @@ const page = await client.list({ limit: 10, status: 'active' });
461
601
 
462
602
  // Auto-paginate through all results
463
603
  for await (const qurl of client.listAll({ status: 'active' })) {
464
- console.log(qurl.resource_id);
604
+ if (!qurl.crid) throw new Error("The API response has no resource CRID");
605
+ console.log(qurl.crid);
465
606
  }
466
607
  ```
467
608
 
@@ -545,6 +686,10 @@ SDK-generated keys require `globalThis.crypto.getRandomValues`, which is availab
545
686
  ## Security Notes
546
687
 
547
688
  - Treat API keys and qURL links like credentials. Do not log them.
689
+ - Resource/qURL path arguments reject pasted access tokens and full URLs before
690
+ dispatch without echoing the caller input. Pass the opaque identifier returned
691
+ by the API (public resource key or CRID); never derive it from a secret qURL
692
+ link. Private `r_` storage IDs are not public API identifiers.
548
693
  - SDK API requests use manual redirect handling. Redirect-capable HTTP statuses
549
694
  (300, 301, 302, 303, 305, 307, 308), filtered `opaqueredirect` responses in
550
695
  browsers and Node native fetch, and responses a custom fetch reports as
@@ -608,3 +753,7 @@ When upgrading, check the release notes for migration guidance — recent breaki
608
753
  ## License
609
754
 
610
755
  MIT
756
+
757
+ Resource verification requires Web Crypto (`crypto.subtle`). In browsers, use
758
+ an HTTPS page or localhost. Raw REST response types keep `crid` optional;
759
+ resource handles and portals require a validated CRID.
@@ -1,4 +1,7 @@
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";
1
+ import { ShareLink } from "./share.js";
2
+ import type { AccessCodeListOutput, ApiKey, ApiKeyListOutput, ApiKeyRequestScope, 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, ShareResourceOptions, UpdateApiKeyInput, UpdateCustomerInput, UpdateInput, UpdateResourceInput, UpdateResourceQurlInput, UpdateWebhookInput, UsageCurrentPeriod, UsageDaily, Webhook, WebhookDelivery, WebhookDeliveryListOutput, WebhookEventTypeInfo, WebhookListOutput, WebhookWithSecret } from "./types.js";
3
+ /** Narrow a response scope before reusing it in a create/update request. */
4
+ export declare function isApiKeyRequestScope(scope: string): scope is ApiKeyRequestScope;
2
5
  /**
3
6
  * Shape of qurl-go's offline signed-fragment links: three or more
4
7
  * dot-separated base64url parts (<version>.<claims>.<secret>.<sig>).
@@ -77,38 +80,34 @@ export declare class QURLClient {
77
80
  */
78
81
  protectUrl(targetUrl: string, opts?: ProtectUrlOptions, requestOptions?: RequestOptions): Promise<ProtectedResource>;
79
82
  /**
80
- * Return a portal-minting handle for a stored resource id, without an API
83
+ * Return a portal-minting handle for a stored CRID, without an API
81
84
  * call.
82
85
  *
83
- * Use when you persisted a LayerV resource id and want to mint more
84
- * portals for it (qurl-go: `Client.ResourceByID`):
86
+ * Use when you persisted a LayerV CRID and want to mint more
87
+ * portals for it (qurl-go: `Client.ResourceByCRID`):
85
88
  *
86
89
  * ```ts
87
- * const resource = client.resourceById("r_demo1234567");
90
+ * const resource = client.resourceByCrid(storedCrid);
88
91
  * const portal = await resource.createPortal({ validFor: "1h" });
89
92
  * ```
90
93
  *
91
94
  * The handle carries no server metadata; use {@link getResource} when you
92
95
  * need the full resource details.
93
96
  */
94
- resourceById(id: string): ProtectedResource;
97
+ resourceByCrid(id: string): ProtectedResource;
98
+ /** Find or create the active qURL Connector resource for an immutable slug. */
99
+ ensureConnectorResource(slug: string, requestOptions?: RequestOptions): Promise<EnsureConnectorResourceResult>;
100
+ /** Fetch a qURL Connector resource by its CRID. */
101
+ getConnectorResource(crid: string): Promise<ConnectorResource>;
102
+ /** Fetch the single active qURL Connector resource for an immutable slug. */
103
+ getConnectorResourceBySlug(slug: string): Promise<ConnectorResource>;
95
104
  /**
96
- * Return the resource qURL Connector created for `connectorId`.
105
+ * Revoke a qURL Connector resource by CRID.
97
106
  *
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.
107
+ * The API does not apply Idempotency-Key replay to DELETE. After an
108
+ * outcome-unknown failure, reconcile by ID before issuing a deliberate retry.
110
109
  */
111
- connectorResource(connectorId: string): Promise<ProtectedResource>;
110
+ deleteConnectorResource(crid: string): Promise<void>;
112
111
  /**
113
112
  * Mint a short-lived qURL link (a portal) for a protected resource.
114
113
  *
@@ -116,7 +115,7 @@ export declare class QURLClient {
116
115
  * reach one private resource. Recipients open `portal.link` directly and
117
116
  * need no LayerV credentials. Prefer short lifetimes such as
118
117
  * `{ validFor: "5m" }`. Accepts a {@link ProtectedResource} handle or a
119
- * resource id (`r_` prefix). REST-shaped equivalents: {@link mintLink} /
118
+ * CRID string. REST-shaped equivalents: {@link mintLink} /
120
119
  * {@link createQurlForResource}.
121
120
  *
122
121
  * Duration options take a string (`"5m"`, `"24h"`; server-validated) or a
@@ -132,7 +131,7 @@ export declare class QURLClient {
132
131
  *
133
132
  * Convenience for one-off scripts (qurl-go: `Client.CreatePortalForURL`).
134
133
  * The returned resource handle is reusable for minting more portals but
135
- * carries only the resource id and the caller-supplied target URL; use
134
+ * carries the CRID, verification key, and the caller-supplied target URL; use
136
135
  * {@link protectUrl} when you need the full server-populated resource
137
136
  * metadata. Portal options match {@link createPortal}; `targetUrl` gets
138
137
  * the same malformed-URL and embedded-credentials rejection as
@@ -230,8 +229,9 @@ export declare class QURLClient {
230
229
  /**
231
230
  * Get a qURL resource and its access tokens.
232
231
  *
233
- * Accepts either a resource ID (`r_` prefix) or a qURL display ID (`q_`
234
- * prefix); the API resolves `q_` IDs to the parent resource automatically.
232
+ * Accepts a CRID or qURL
233
+ * display ID (`q_` prefix); the API resolves display IDs to the parent
234
+ * resource automatically.
235
235
  */
236
236
  get(id: string): Promise<QURL>;
237
237
  /**
@@ -262,17 +262,17 @@ export declare class QURLClient {
262
262
  /**
263
263
  * Delete (revoke) a qURL resource and all its access tokens.
264
264
  *
265
- * Only accepts a resource ID (`r_` prefix), not a qURL display ID (`q_`
266
- * prefix). Per the OpenAPI spec: *"Requires a resource ID (r_ prefix).
267
- * To revoke a single token, use DELETE /v1/resources/:id/qurls/:qurl_id"*.
268
- * A client-side prefix check catches the mistake before the API round-trip.
265
+ * Requires a CRID returned by the API. To revoke one qURL, use
266
+ * {@link revokeResourceQurl} instead.
267
+ *
268
+ * @throws {ValidationError} If `id` is not a canonical CRID.
269
269
  */
270
270
  delete(id: string): Promise<void>;
271
271
  /**
272
272
  * Extend a qURL's expiration.
273
273
  *
274
- * Accepts either a resource ID (`r_` prefix) or a qURL display ID (`q_`
275
- * prefix). Convenience method — delegates to {@link update} with only the
274
+ * Accepts a CRID or qURL
275
+ * display ID (`q_` prefix). Delegates to {@link update} with only the
276
276
  * expiration fields. `ExtendInput` shares its `extend_by` / `expires_at`
277
277
  * fields with `UpdateInput` but is *narrower in two ways*: (1) exactly
278
278
  * one of the two must be present (XOR via `?: never`), where `UpdateInput`
@@ -287,8 +287,9 @@ export declare class QURLClient {
287
287
  /**
288
288
  * Update a qURL — extend expiration, change description, rename tags.
289
289
  *
290
- * Accepts either a resource ID (`r_` prefix) or a qURL display ID (`q_`
291
- * prefix); the API resolves `q_` IDs to the parent resource automatically.
290
+ * Accepts a CRID or qURL
291
+ * display ID (`q_` prefix); the API resolves display IDs to the parent
292
+ * resource automatically.
292
293
  */
293
294
  update(id: string, input: UpdateInput, options?: RequestOptions): Promise<QURL>;
294
295
  /**
@@ -296,8 +297,9 @@ export declare class QURLClient {
296
297
  *
297
298
  * Portal-flow equivalent: {@link createPortal}.
298
299
  *
299
- * Accepts either a resource ID (`r_` prefix) or a qURL display ID (`q_`
300
- * prefix); the API resolves `q_` IDs to the parent resource automatically.
300
+ * Accepts a CRID or qURL
301
+ * display ID (`q_` prefix); the API resolves display IDs to the parent
302
+ * resource automatically.
301
303
  *
302
304
  * Passing `{}` or an object with all fields `null`/`undefined` is
303
305
  * equivalent to omitting the second argument: no body is sent, and the
@@ -346,6 +348,13 @@ export declare class QURLClient {
346
348
  * Portal-flow equivalent: {@link createPortal} (same endpoint).
347
349
  */
348
350
  createQurlForResource(id: string, input?: CreateQurlForResourceInput, options?: RequestOptions): Promise<CreateOutput>;
351
+ /**
352
+ * Mint a fresh, short-lived share link for an existing CRID.
353
+ * The returned link is secret and is not retrievable after this response.
354
+ * If you hold the trusted resource DER SPKI, call `ShareLink.verifyCrid()`
355
+ * before delivery to verify that the response CRID derives from that key.
356
+ */
357
+ shareResource(id: string, options?: ShareResourceOptions, requestOptions?: RequestOptions): Promise<ShareLink>;
349
358
  /** Revoke a specific qURL token on a resource. */
350
359
  revokeResourceQurl(id: string, qurlId: string): Promise<void>;
351
360
  /** Update a specific qURL token on a resource. */
@@ -509,20 +518,19 @@ export declare class QURLClient {
509
518
  /**
510
519
  * A LayerV-protected resource, bound to the client that produced it.
511
520
  *
512
- * Obtained from {@link QURLClient.protectUrl},
513
- * {@link QURLClient.connectorResource}, or {@link QURLClient.resourceById} —
521
+ * Obtained from {@link QURLClient.protectUrl} or {@link QURLClient.resourceByCrid} —
514
522
  * not constructed directly. Mint short-lived access links for the resource
515
523
  * with {@link createPortal} (qurl-go: `qurl.Resource`).
516
524
  */
517
525
  export declare class ProtectedResource {
518
526
  #private;
519
- /** The LayerV resource id (`r_` prefix). */
520
- readonly id: string;
527
+ /** The public resource CRID. */
528
+ readonly crid: string;
521
529
  /** The private URL protected by this resource, when known. */
522
530
  readonly targetUrl?: string;
523
531
  /**
524
532
  * Full server-populated {@link Resource} metadata when the handle came
525
- * from an API response; `undefined` for {@link QURLClient.resourceById}
533
+ * from an API response; `undefined` for {@link QURLClient.resourceByCrid}
526
534
  * handles and for the one-call {@link QURLClient.createPortalForUrl} path.
527
535
  */
528
536
  readonly details?: Resource;
@@ -540,3 +548,31 @@ export declare class ProtectedResource {
540
548
  */
541
549
  createPortal(opts?: CreatePortalOptions, requestOptions?: RequestOptions): Promise<Portal>;
542
550
  }
551
+ /** Result of idempotently ensuring a qURL Connector resource. */
552
+ export interface EnsureConnectorResourceResult {
553
+ resource: ConnectorResource;
554
+ foundExisting: boolean;
555
+ }
556
+ declare const CONNECTOR_RESOURCE_CONSTRUCTOR_TOKEN: unique symbol;
557
+ /**
558
+ * A validated qURL Connector resource bound to the client that loaded it.
559
+ *
560
+ * Resource identity, reverse-routing identity, and NHP admission identity are
561
+ * distinct server-issued values. Callers must consume each field verbatim and
562
+ * must never derive one from another.
563
+ */
564
+ export declare class ConnectorResource {
565
+ #private;
566
+ readonly resourcePublicKey: string;
567
+ /** Required public identifier, verified against the returned resource key. */
568
+ readonly crid: string;
569
+ readonly connectorRoutingId: string;
570
+ readonly knockResourceId: string;
571
+ readonly slug: string;
572
+ readonly alias?: string;
573
+ /** @internal Instances are returned by QURLClient after wire validation. */
574
+ constructor(client: QURLClient, details: Resource, validationToken: typeof CONNECTOR_RESOURCE_CONSTRUCTOR_TOKEN);
575
+ /** Mint a short-lived portal for this Connector resource. */
576
+ createPortal(opts?: CreatePortalOptions, requestOptions?: RequestOptions): Promise<Portal>;
577
+ }
578
+ export {};