@layervai/qurl 0.2.0 → 0.3.1

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
@@ -4,13 +4,34 @@
4
4
  [![CI](https://github.com/layervai/qurl-typescript/actions/workflows/ci.yml/badge.svg)](https://github.com/layervai/qurl-typescript/actions/workflows/ci.yml)
5
5
  [![License](https://img.shields.io/github/license/layervai/qurl-typescript)](LICENSE)
6
6
 
7
- TypeScript SDK for the [qURL™ API](https://docs.layerv.ai) — secure, time-limited access links for AI agents.
7
+ **Use the LayerV [qURL™ Platform](https://docs.layerv.ai) from TypeScript: protect a
8
+ private URL once, then mint short-lived portal links for it.**
8
9
 
9
10
  > **Quantum URL (qURL)** · The internet has a hidden layer. This is how you enter.
10
11
 
12
+ Portal recipients do not need LayerV credentials, API keys, or SDK state. They
13
+ open the qURL link. Credentials are only for software that protects URLs or
14
+ creates portals.
15
+
11
16
  ## Why qURL?
12
17
 
13
- AI agents need to access protected resources — APIs, databases, internal tools — but giving them permanent credentials is a security risk. qURL creates time-limited, auditable access links that expire automatically. The SDK handles authentication, retries, pagination, and error handling so you can focus on your agent logic.
18
+ Agents and services increasingly need to reach private MCP servers, APIs, and
19
+ internal tools. The issue is visibility: every standing public endpoint becomes
20
+ inventory for scanners, fingerprinting, credential attacks, and AI-assisted
21
+ probing before a legitimate user or agent ever arrives.
22
+
23
+ Opening an inbound port, running a VPN, shipping a bastion, publishing a
24
+ Cloudflare Tunnel or ngrok URL, or passing around a long-lived key all leave
25
+ something durable to find, scan, or steal. qURL flips that model: it is an
26
+ invisibility primitive for authenticated access, and the private resource is
27
+ not public inventory. A portal is **cryptographic, just-in-time permission for
28
+ one actor to reach one private resource** — not another externally visible
29
+ endpoint in front of the same service:
30
+
31
+ - **Time-limited** — portals expire after minutes, hours, or days
32
+ - **IP-scoped** — access is granted only to the requesting IP via NHP
33
+ - **Auditable** — every access is logged with who, when, and from where
34
+ - **Revocable** — kill access instantly if something goes wrong
14
35
 
15
36
  ## Installation
16
37
 
@@ -18,16 +39,99 @@ AI agents need to access protected resources — APIs, databases, internal tools
18
39
  npm install @layervai/qurl
19
40
  ```
20
41
 
21
- Requires Node.js 18+. Both `import { QURLClient } from '@layervai/qurl'` (ESM) and `const { QURLClient } = require('@layervai/qurl')` (CJS) work.
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.
22
43
 
23
- ## Quick Start
44
+ ## Quickstart
24
45
 
25
46
  ```typescript
26
47
  import { QURLClient } from '@layervai/qurl';
27
48
 
28
- const client = new QURLClient({ apiKey: 'lv_live_xxx' });
49
+ const client = new QURLClient({ apiKey: 'YOUR_API_KEY' });
50
+
51
+ const resource = await client.protectUrl('https://internal.example.com/dashboard');
52
+ const portal = await resource.createPortal({ validFor: '5m' });
53
+
54
+ console.log(portal.link); // Share this link — recipients need no credentials
55
+ ```
56
+
57
+ That is the core flow:
58
+
59
+ | Step | Call | What you provide |
60
+ | --- | --- | --- |
61
+ | Protect a private URL | `client.protectUrl` | The target URL you already know |
62
+ | Mint a short-lived access link | `resource.createPortal` | The returned resource handle |
63
+
64
+ `protectUrl` is idempotent for the same account and target URL: protecting the
65
+ same URL again returns the existing resource. `validFor` accepts a duration
66
+ string (`'5m'`, `'24h'`) or a number of milliseconds (whole seconds, at least
67
+ one minute); prefer short portal lifetimes.
68
+
69
+ If qURL Connector already protects the service, use the connector id instead
70
+ of calling `protectUrl`:
71
+
72
+ ```typescript
73
+ const resource = await client.connectorResource('prod-dashboard');
74
+ const portal = await resource.createPortal({ validFor: '5m' });
75
+ ```
76
+
77
+ If you persist the resource id, future calls do not need to recreate the
78
+ handle (no API call is made until you mint):
79
+
80
+ ```typescript
81
+ const resource = client.resourceById('r_demo1234567');
82
+ const portal = await resource.createPortal({ validFor: '1h' });
83
+ ```
84
+
85
+ For one-off scripts, `client.createPortalForUrl` combines the two API calls
86
+ and returns both the portal and a reusable resource handle. The handle carries
87
+ the resource id and target URL; use `protectUrl` when you need the full
88
+ server-populated resource metadata:
29
89
 
30
- // Create a protected link
90
+ ```typescript
91
+ const { portal, resource } = await client.createPortalForUrl(
92
+ 'https://internal.example.com/dashboard',
93
+ { validFor: '5m' },
94
+ );
95
+ ```
96
+
97
+ Portal options mirror qurl-go:
98
+
99
+ ```typescript
100
+ const portal = await resource.createPortal({
101
+ validFor: 5 * 60 * 1000, // milliseconds work too
102
+ label: 'Alice from Acme',
103
+ oneTimeUse: true,
104
+ maxSessions: 1,
105
+ });
106
+ ```
107
+
108
+ ## Opening Portals
109
+
110
+ Most recipients open qURL links directly and do not use this SDK at all. If
111
+ you are building a service or agent that opens received qURL links
112
+ programmatically, `enterPortal` accepts a full link or a bare access token,
113
+ grants network access for the caller's IP, and returns the reachable resource:
114
+
115
+ ```typescript
116
+ const handle = await client.enterPortal(link);
117
+ console.log(handle.resourceUrl); // The reachable resource location
118
+ console.log(handle.openSeconds); // How long access stays open
119
+ ```
120
+
121
+ Unlike qurl-go's offline `EnterPortal`, this SDK opens links through the
122
+ LayerV API: the client needs an API key with the `qurl:resolve` scope.
123
+ `enterPortal` fails closed — if access is granted but no resource URL comes
124
+ back, it throws instead of returning an empty handle.
125
+
126
+ ## REST-Shaped API (Compatibility)
127
+
128
+ The original REST-shaped methods remain fully supported and share the same
129
+ client. Use them for the qURL/resource/token management surface that has no
130
+ portal-verb equivalent (listing, updating, revoking, quotas, webhooks, ...) or
131
+ if you already build on them:
132
+
133
+ ```typescript
134
+ // Create a protected link (portal equivalent: createPortalForUrl)
31
135
  const result = await client.create({
32
136
  target_url: 'https://api.example.com/data',
33
137
  expires_in: '24h',
@@ -35,8 +139,8 @@ const result = await client.create({
35
139
  });
36
140
  console.log(result.qurl_link);
37
141
 
38
- // Resolve a token (grants network access for your IP)
39
- const access = await client.resolve('at_...');
142
+ // Resolve a token headlessly (portal equivalent: enterPortal)
143
+ const access = await client.resolve('at_k8xqp9h2sj9lx7r4a');
40
144
  console.log(`Access granted to ${access.target_url} for ${access.access_grant?.expires_in}s`);
41
145
  ```
42
146
 
@@ -54,7 +158,18 @@ console.log(`Access granted to ${access.target_url} for ${access.access_grant?.e
54
158
  | `userAgent` | No | `qurl-typescript/<version>` |
55
159
  | `debug` | No | `false` |
56
160
 
57
- ### Methods
161
+ ### Portal methods
162
+
163
+ | Method | Description |
164
+ |--------|-------------|
165
+ | `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 }` |
168
+ | `connectorResource(connectorId)` | Handle for a service qURL Connector already protects |
169
+ | `resourceById(id)` | Handle from a stored resource id (no API call) |
170
+ | `enterPortal(linkOrToken)` | Open a qURL link programmatically → `ResourceHandle` |
171
+
172
+ ### REST-shaped methods
58
173
 
59
174
  | Method | Description |
60
175
  |--------|-------------|
@@ -122,7 +237,7 @@ Non-400 errors (401, 403, 429, 5xx, and unexpected 400 body shapes) still throw
122
237
 
123
238
  ## Error Handling
124
239
 
125
- All API errors throw typed error subclasses, so you can catch specific failure modes:
240
+ Match errors by type, not message text — every failure throws a typed `QURLError` subclass, and message wording is not part of the API contract:
126
241
 
127
242
  ```typescript
128
243
  import {
@@ -134,16 +249,16 @@ import {
134
249
  } from '@layervai/qurl';
135
250
 
136
251
  try {
137
- await client.create({ target_url: '' });
252
+ await client.enterPortal('https://qurl.link/#at_k8xqp9h2sj9lx7r4a');
138
253
  } catch (err) {
139
- if (err instanceof ValidationError) {
140
- console.error('Invalid input:', err.invalidFields);
141
- } else if (err instanceof RateLimitError) {
142
- console.error(`Rate limited — retry after ${err.retryAfter}s`);
143
- } else if (err instanceof AuthenticationError) {
254
+ if (err instanceof AuthenticationError) {
144
255
  console.error('Bad API key');
145
256
  } else if (err instanceof NotFoundError) {
146
- console.error('Resource not found');
257
+ console.error('Portal doesn\'t exist or already expired');
258
+ } else if (err instanceof RateLimitError) {
259
+ console.error(`Rate limited — retry after ${err.retryAfter}s`);
260
+ } else if (err instanceof ValidationError) {
261
+ console.error('Invalid input:', err.detail, err.invalidFields);
147
262
  } else if (err instanceof QURLError) {
148
263
  console.error(`API error [${err.code}]: ${err.detail}`);
149
264
  }
@@ -161,6 +276,13 @@ try {
161
276
  | `NetworkError` | — | Connection failure |
162
277
  | `TimeoutError` | — | Request exceeded timeout |
163
278
 
279
+ Client-detected failures use `status: 0` with a discriminating `code`:
280
+ `"client_validation"` for bad input caught before a request, and — on the
281
+ portal surface — `"resource_not_found"` / `"ambiguous_resource"` when
282
+ `connectorResource` cannot resolve a connector id to exactly one resource,
283
+ and `"unexpected_response"` when a response is missing required fields (e.g.
284
+ `enterPortal` failing closed on a grant with no resource URL).
285
+
164
286
  ## Pagination
165
287
 
166
288
  ```typescript
@@ -179,11 +301,11 @@ Enable debug output to see all HTTP requests and retries:
179
301
 
180
302
  ```typescript
181
303
  // Log to console
182
- const client = new QURLClient({ apiKey: 'lv_live_xxx', debug: true });
304
+ const client = new QURLClient({ apiKey: 'YOUR_API_KEY', debug: true });
183
305
 
184
306
  // Custom logger
185
- const client = new QURLClient({
186
- apiKey: 'lv_live_xxx',
307
+ const clientWithLogger = new QURLClient({
308
+ apiKey: 'YOUR_API_KEY',
187
309
  debug: (message, data) => myLogger.debug(message, data),
188
310
  });
189
311
  ```
@@ -210,8 +332,23 @@ await client.create(
210
332
  );
211
333
  ```
212
334
 
335
+ The portal verbs take the same per-call options as their final argument, e.g.
336
+ `resource.createPortal({ validFor: '5m' }, { idempotencyKey: 'mint-alice-1' })`.
337
+
213
338
  SDK-generated keys require `globalThis.crypto.getRandomValues`, which is available in supported Node 20+ runtimes and modern edge/browser runtimes. In constrained runtimes without Web Crypto, pass a caller-provided key with `idempotencyKey`; otherwise POST/PATCH calls throw `RuntimeError` before sending a request.
214
339
 
340
+ ## Security Notes
341
+
342
+ - Treat API keys and qURL links like credentials. Do not log them.
343
+ - Prefer short portal lifetimes such as `validFor: '5m'`.
344
+ - Do not ask portal recipients to handle credentials. Recipients only need
345
+ the link.
346
+ - `protectUrl` and `createPortalForUrl` reject malformed target URLs and URLs
347
+ with embedded credentials (`https://user:pass@...`) before any request,
348
+ matching qurl-go.
349
+ - The programmatic opener fails closed: `enterPortal` throws when access is
350
+ granted without a resource URL, and its error messages never echo the link.
351
+
215
352
  ## Versioning & breaking changes
216
353
 
217
354
  This SDK is pre-1.0; breaking changes between minor versions are possible until the API surface stabilizes. Significant changes are called out in [`CHANGELOG.md`](CHANGELOG.md) and in the corresponding GitHub release notes.
@@ -1,4 +1,4 @@
1
- import type { AccessCodeListOutput, AgentBootstrapInput, AgentBootstrapOutput, ApiKey, ApiKeyListOutput, BillingInvoiceListOutput, BatchCreateInput, BatchCreateOutput, ClientOptions, ConnectorInstallation, ConnectorInstallationListOutput, CreateAccessCodeInput, CreateAccessCodeOutput, CreateApiKeyInput, CreateApiKeyOutput, CreateBillingCheckoutInput, CreateInput, CreateOutput, CreateQurlForResourceInput, CreateResourceInput, CreateWebhookInput, Customer, CheckoutSession, Domain, DomainListOutput, DomainVerifyResult, ExtendInput, Invoice, ListInput, ListOutput, ListApiKeysInput, ListBillingInvoicesInput, ListConnectorInstallationsInput, ListDomainsInput, ListWebhookDeliveriesInput, ListWebhooksInput, MintInput, MintOutput, PortalSession, QURL, QurlSummary, Quota, RedeemAccessCodeInput, RedeemAccessCodeOutput, RegisterDomainInput, Resource, ResourceDetail, 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, 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";
2
2
  /** qURL API client. */
3
3
  export declare class QURLClient {
4
4
  private readonly baseUrl;
@@ -49,7 +49,119 @@ export declare class QURLClient {
49
49
  */
50
50
  private validateBatchCreateResponse;
51
51
  private paginateAll;
52
- /** Create a new qURL. */
52
+ /**
53
+ * Protect a private URL — create or reuse its LayerV resource.
54
+ *
55
+ * The first step of the portal flow (qurl-go: `Client.ProtectURL`).
56
+ * Protect the URL once, then mint short-lived links for it with
57
+ * {@link ProtectedResource.createPortal}:
58
+ *
59
+ * ```ts
60
+ * const resource = await client.protectUrl("https://internal.example.com/dashboard");
61
+ * const portal = await resource.createPortal({ validFor: "5m" });
62
+ * share(portal.link);
63
+ * ```
64
+ *
65
+ * Protecting the same target URL again returns the existing resource.
66
+ * Malformed URLs and URLs with embedded credentials (userinfo) are
67
+ * rejected before any request, mirroring qurl-go. REST-shaped equivalent:
68
+ * {@link createResource}.
69
+ */
70
+ protectUrl(targetUrl: string, opts?: ProtectUrlOptions, requestOptions?: RequestOptions): Promise<ProtectedResource>;
71
+ /**
72
+ * Return a portal-minting handle for a stored resource id, without an API
73
+ * call.
74
+ *
75
+ * Use when you persisted a LayerV resource id and want to mint more
76
+ * portals for it (qurl-go: `Client.ResourceByID`):
77
+ *
78
+ * ```ts
79
+ * const resource = client.resourceById("r_demo1234567");
80
+ * const portal = await resource.createPortal({ validFor: "1h" });
81
+ * ```
82
+ *
83
+ * The handle carries no server metadata; use {@link getResource} when you
84
+ * need the full resource details.
85
+ */
86
+ resourceById(id: string): ProtectedResource;
87
+ /**
88
+ * Return the resource qURL Connector created for `connectorId`.
89
+ *
90
+ * Use when qURL Connector already protects the service — do not call
91
+ * {@link protectUrl} again for the same service. The connector id is the
92
+ * resource slug LayerV stores for that connector; the LayerV API performs
93
+ * the slug lookup, and the SDK confirms the returned alias matches
94
+ * `connectorId` before binding the handle (qurl-go:
95
+ * `Client.ConnectorResource`).
96
+ *
97
+ * Throws {@link NotFoundError} (`status: 0`, `code: "resource_not_found"`)
98
+ * when no resource exists for the connector id, {@link QURLError}
99
+ * (`code: "ambiguous_resource"`) when the lookup returns more than one
100
+ * resource, and {@link ValidationError} (`code: "unexpected_response"`)
101
+ * when the returned resource's alias is missing or does not match.
102
+ */
103
+ connectorResource(connectorId: string): Promise<ProtectedResource>;
104
+ /**
105
+ * Mint a short-lived qURL link (a portal) for a protected resource.
106
+ *
107
+ * A portal is cryptographic, just-in-time permission for one actor to
108
+ * reach one private resource. Recipients open `portal.link` directly and
109
+ * need no LayerV credentials. Prefer short lifetimes such as
110
+ * `{ validFor: "5m" }`. Accepts a {@link ProtectedResource} handle or a
111
+ * resource id (`r_` prefix). REST-shaped equivalents: {@link mintLink} /
112
+ * {@link createQurlForResource}.
113
+ *
114
+ * Duration options take a string (`"5m"`, `"24h"`; server-validated) or a
115
+ * number of milliseconds with qurl-go's client-side guardrails: whole
116
+ * seconds only, at least one minute for `validFor` and one second for
117
+ * `sessionDuration`. `maxSessions: 0` is sent explicitly and means
118
+ * unlimited; blank labels are rejected.
119
+ */
120
+ createPortal(resource: ProtectedResource | string, opts?: CreatePortalOptions, requestOptions?: RequestOptions): Promise<Portal>;
121
+ /**
122
+ * Protect `targetUrl` and mint a portal in one API call.
123
+ *
124
+ * Convenience for one-off scripts (qurl-go: `Client.CreatePortalForURL`).
125
+ * The returned resource handle is reusable for minting more portals but
126
+ * carries only the resource id and the caller-supplied target URL; use
127
+ * {@link protectUrl} when you need the full server-populated resource
128
+ * metadata. Portal options match {@link createPortal}; `targetUrl` gets
129
+ * the same malformed-URL and embedded-credentials rejection as
130
+ * {@link protectUrl}. REST-shaped equivalent: {@link create}.
131
+ */
132
+ createPortalForUrl(targetUrl: string, opts?: CreatePortalOptions, requestOptions?: RequestOptions): Promise<{
133
+ portal: Portal;
134
+ resource: ProtectedResource;
135
+ }>;
136
+ /**
137
+ * Open a qURL link programmatically and return the reachable resource.
138
+ *
139
+ * For services and agents that open received qURL links in code — human
140
+ * recipients just open the link. Accepts a full platform link
141
+ * (`https://qurl.link/#at_...`) or a bare access token. Triggers an NHP
142
+ * knock that grants network access for the caller's IP and returns a
143
+ * {@link ResourceHandle} with the reachable resource URL and access
144
+ * lifetime.
145
+ *
146
+ * Unlike qurl-go's `EnterPortal` (which verifies links locally and needs
147
+ * no LayerV credentials), this SDK opens links through the LayerV API:
148
+ * the client needs an API key with the `qurl:resolve` scope. REST-shaped
149
+ * equivalent: {@link resolve}.
150
+ *
151
+ * Throws {@link ValidationError} (`code: "client_validation"`) when no
152
+ * access token can be extracted from `qurlLink`, and
153
+ * (`code: "unexpected_response"`) when access is granted but the API
154
+ * reports no resource URL — it fails closed rather than returning an
155
+ * empty handle. Error messages never echo the link; qURL links are
156
+ * credentials.
157
+ */
158
+ enterPortal(qurlLink: string, requestOptions?: RequestOptions): Promise<ResourceHandle>;
159
+ /**
160
+ * Create a new qURL.
161
+ *
162
+ * Portal-flow equivalent: {@link createPortalForUrl} (same endpoint,
163
+ * returns a {@link Portal} plus a reusable resource handle).
164
+ */
53
165
  create(input: CreateInput, options?: RequestOptions): Promise<CreateOutput>;
54
166
  /**
55
167
  * Batch create multiple qURLs (1-`MAX_BATCH_ITEMS` items).
@@ -171,6 +283,8 @@ export declare class QURLClient {
171
283
  /**
172
284
  * Mint a new access link for a qURL.
173
285
  *
286
+ * Portal-flow equivalent: {@link createPortal}.
287
+ *
174
288
  * Accepts either a resource ID (`r_` prefix) or a qURL display ID (`q_`
175
289
  * prefix); the API resolves `q_` IDs to the parent resource automatically.
176
290
  *
@@ -182,6 +296,9 @@ export declare class QURLClient {
182
296
  /**
183
297
  * Resolve a qURL access token (headless).
184
298
  *
299
+ * Portal-flow equivalent: {@link enterPortal} (also accepts full qURL
300
+ * links and fails closed when no resource URL is returned).
301
+ *
185
302
  * Triggers an NHP knock to grant network access for the caller's IP.
186
303
  * Requires `qurl:resolve` scope on the API key.
187
304
  *
@@ -201,7 +318,12 @@ export declare class QURLClient {
201
318
  * iteration begins.
202
319
  */
203
320
  listAllResources(input?: Omit<ResourceListInput, "cursor">): AsyncGenerator<Resource, void, undefined>;
204
- /** Create a resource directly. */
321
+ /**
322
+ * Create a resource directly.
323
+ *
324
+ * Portal-flow equivalent for URL resources: {@link protectUrl} (returns
325
+ * a portal-minting handle).
326
+ */
205
327
  createResource(input: CreateResourceInput, options?: RequestOptions): Promise<Resource>;
206
328
  /** Get one resource plus its bounded qURL preview. */
207
329
  getResource(id: string): Promise<ResourceDetail>;
@@ -209,7 +331,11 @@ export declare class QURLClient {
209
331
  updateResource(id: string, input: UpdateResourceInput, options?: RequestOptions): Promise<Resource>;
210
332
  /** Revoke a resource and all of its qURLs. */
211
333
  deleteResource(id: string): Promise<void>;
212
- /** Mint a qURL against an existing resource. */
334
+ /**
335
+ * Mint a qURL against an existing resource.
336
+ *
337
+ * Portal-flow equivalent: {@link createPortal} (same endpoint).
338
+ */
213
339
  createQurlForResource(id: string, input?: CreateQurlForResourceInput, options?: RequestOptions): Promise<CreateOutput>;
214
340
  /** Revoke a specific qURL token on a resource. */
215
341
  revokeResourceQurl(id: string, qurlId: string): Promise<void>;
@@ -370,3 +496,37 @@ export declare class QURLClient {
370
496
  private retryDelay;
371
497
  private classifyFetchError;
372
498
  }
499
+ /**
500
+ * A LayerV-protected resource, bound to the client that produced it.
501
+ *
502
+ * Obtained from {@link QURLClient.protectUrl},
503
+ * {@link QURLClient.connectorResource}, or {@link QURLClient.resourceById} —
504
+ * not constructed directly. Mint short-lived access links for the resource
505
+ * with {@link createPortal} (qurl-go: `qurl.Resource`).
506
+ */
507
+ export declare class ProtectedResource {
508
+ #private;
509
+ /** The LayerV resource id (`r_` prefix). */
510
+ readonly id: string;
511
+ /** The private URL protected by this resource, when known. */
512
+ readonly targetUrl?: string;
513
+ /**
514
+ * Full server-populated {@link Resource} metadata when the handle came
515
+ * from an API response; `undefined` for {@link QURLClient.resourceById}
516
+ * handles and for the one-call {@link QURLClient.createPortalForUrl} path.
517
+ */
518
+ readonly details?: Resource;
519
+ constructor(client: QURLClient, id: string, targetUrl?: string, details?: Resource);
520
+ /**
521
+ * Whether `resource` was produced by `client`. A static because the
522
+ * binding lives in a private field that only this class can read; used
523
+ * by {@link QURLClient.createPortal} to refuse handles bound to a
524
+ * different client.
525
+ */
526
+ static isBoundTo(resource: ProtectedResource, client: QURLClient): boolean;
527
+ /**
528
+ * Mint a short-lived qURL link (a portal) for this resource. See
529
+ * {@link QURLClient.createPortal} for the option reference.
530
+ */
531
+ createPortal(opts?: CreatePortalOptions, requestOptions?: RequestOptions): Promise<Portal>;
532
+ }