@layervai/qurl 3.1.0 → 4.0.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 +259 -46
- package/dist/cjs/client-connectors.d.ts +44 -0
- package/dist/cjs/client-connectors.js +189 -0
- package/dist/cjs/client-delegated.d.ts +30 -0
- package/dist/cjs/client-delegated.js +312 -0
- package/dist/cjs/client-errors.d.ts +44 -0
- package/dist/cjs/client-errors.js +219 -0
- package/dist/cjs/client-http-types.d.ts +37 -0
- package/dist/cjs/client-http-types.js +2 -0
- package/dist/cjs/client-input-fields.d.ts +53 -0
- package/dist/cjs/client-input-fields.js +262 -0
- package/dist/cjs/client-limits.d.ts +36 -0
- package/dist/cjs/client-limits.js +61 -0
- package/dist/cjs/client-management.d.ts +12 -0
- package/dist/cjs/client-management.js +119 -0
- package/dist/cjs/client-paths.d.ts +21 -0
- package/dist/cjs/client-paths.js +143 -0
- package/dist/cjs/client-portals.d.ts +82 -0
- package/dist/cjs/client-portals.js +244 -0
- package/dist/cjs/client-resources.d.ts +29 -0
- package/dist/cjs/client-resources.js +331 -0
- package/dist/cjs/client-validation.d.ts +35 -0
- package/dist/cjs/client-validation.js +326 -0
- package/dist/cjs/client.d.ts +45 -75
- package/dist/cjs/client.js +621 -2448
- package/dist/cjs/errors.d.ts +9 -1
- package/dist/cjs/errors.js +10 -1
- package/dist/cjs/index.d.ts +4 -2
- package/dist/cjs/index.js +4 -1
- package/dist/cjs/response-metadata.d.ts +13 -0
- package/dist/cjs/response-metadata.js +38 -0
- package/dist/cjs/share.js +2 -0
- package/dist/cjs/target-path.d.ts +2 -0
- package/dist/cjs/target-path.js +34 -0
- package/dist/cjs/types.d.ts +83 -7
- package/dist/cjs/version.d.ts +1 -1
- package/dist/cjs/version.js +1 -1
- package/dist/esm/client-connectors.d.ts +44 -0
- package/dist/esm/client-connectors.js +180 -0
- package/dist/esm/client-delegated.d.ts +30 -0
- package/dist/esm/client-delegated.js +295 -0
- package/dist/esm/client-errors.d.ts +44 -0
- package/dist/esm/client-errors.js +206 -0
- package/dist/esm/client-http-types.d.ts +37 -0
- package/dist/esm/client-http-types.js +1 -0
- package/dist/esm/client-input-fields.d.ts +53 -0
- package/dist/esm/client-input-fields.js +258 -0
- package/dist/esm/client-limits.d.ts +36 -0
- package/dist/esm/client-limits.js +58 -0
- package/dist/esm/client-management.d.ts +12 -0
- package/dist/esm/client-management.js +113 -0
- package/dist/esm/client-paths.d.ts +21 -0
- package/dist/esm/client-paths.js +138 -0
- package/dist/esm/client-portals.d.ts +82 -0
- package/dist/esm/client-portals.js +236 -0
- package/dist/esm/client-resources.d.ts +29 -0
- package/dist/esm/client-resources.js +323 -0
- package/dist/esm/client-validation.d.ts +35 -0
- package/dist/esm/client-validation.js +302 -0
- package/dist/esm/client.d.ts +45 -76
- package/dist/esm/client.js +318 -2147
- package/dist/esm/crid.d.ts +0 -1
- package/dist/esm/crid.js +0 -1
- package/dist/esm/errors.d.ts +9 -2
- package/dist/esm/errors.js +9 -1
- package/dist/esm/index.d.ts +4 -3
- package/dist/esm/index.js +2 -2
- package/dist/esm/internal.d.ts +0 -1
- package/dist/esm/internal.js +0 -1
- package/dist/esm/node/agent-runtime.d.ts +0 -1
- package/dist/esm/node/agent-runtime.js +0 -1
- package/dist/esm/node/agent-state.d.ts +0 -1
- package/dist/esm/node/agent-state.js +0 -1
- package/dist/esm/node/agent-transport.d.ts +0 -1
- package/dist/esm/node/agent-transport.js +0 -1
- package/dist/esm/node/deployment.d.ts +0 -1
- package/dist/esm/node/deployment.js +0 -1
- package/dist/esm/node/file-agent-state.d.ts +0 -1
- package/dist/esm/node/file-agent-state.js +0 -1
- package/dist/esm/node/native-udp.d.ts +0 -1
- package/dist/esm/node/native-udp.js +0 -1
- package/dist/esm/node/nhp-wire.d.ts +0 -1
- package/dist/esm/node/nhp-wire.js +0 -1
- package/dist/esm/node/portal-opener.d.ts +0 -1
- package/dist/esm/node/portal-opener.js +0 -1
- package/dist/esm/node/provider.d.ts +0 -1
- package/dist/esm/node/provider.js +0 -1
- package/dist/esm/node/qv2.d.ts +0 -1
- package/dist/esm/node/qv2.js +0 -1
- package/dist/esm/node/relay.d.ts +0 -1
- package/dist/esm/node/relay.js +0 -1
- package/dist/esm/node/sealed-agent-state.d.ts +0 -1
- package/dist/esm/node/sealed-agent-state.js +0 -1
- package/dist/esm/node/strict-json.d.ts +0 -1
- package/dist/esm/node/strict-json.js +0 -1
- package/dist/esm/node.d.ts +0 -1
- package/dist/esm/node.js +0 -1
- package/dist/esm/response-metadata.d.ts +13 -0
- package/dist/esm/response-metadata.js +33 -0
- package/dist/esm/share.d.ts +0 -1
- package/dist/esm/share.js +2 -1
- package/dist/esm/target-path.d.ts +2 -0
- package/dist/esm/target-path.js +30 -0
- package/dist/esm/types.d.ts +83 -8
- package/dist/esm/types.js +0 -1
- package/dist/esm/version.d.ts +1 -2
- package/dist/esm/version.js +1 -2
- package/package.json +9 -7
- package/dist/esm/client.d.ts.map +0 -1
- package/dist/esm/client.js.map +0 -1
- package/dist/esm/crid.d.ts.map +0 -1
- package/dist/esm/crid.js.map +0 -1
- package/dist/esm/errors.d.ts.map +0 -1
- package/dist/esm/errors.js.map +0 -1
- package/dist/esm/index.d.ts.map +0 -1
- package/dist/esm/index.js.map +0 -1
- package/dist/esm/internal.d.ts.map +0 -1
- package/dist/esm/internal.js.map +0 -1
- package/dist/esm/node/agent-runtime.d.ts.map +0 -1
- package/dist/esm/node/agent-runtime.js.map +0 -1
- package/dist/esm/node/agent-state.d.ts.map +0 -1
- package/dist/esm/node/agent-state.js.map +0 -1
- package/dist/esm/node/agent-transport.d.ts.map +0 -1
- package/dist/esm/node/agent-transport.js.map +0 -1
- package/dist/esm/node/deployment.d.ts.map +0 -1
- package/dist/esm/node/deployment.js.map +0 -1
- package/dist/esm/node/file-agent-state.d.ts.map +0 -1
- package/dist/esm/node/file-agent-state.js.map +0 -1
- package/dist/esm/node/native-udp.d.ts.map +0 -1
- package/dist/esm/node/native-udp.js.map +0 -1
- package/dist/esm/node/nhp-wire.d.ts.map +0 -1
- package/dist/esm/node/nhp-wire.js.map +0 -1
- package/dist/esm/node/portal-opener.d.ts.map +0 -1
- package/dist/esm/node/portal-opener.js.map +0 -1
- package/dist/esm/node/provider.d.ts.map +0 -1
- package/dist/esm/node/provider.js.map +0 -1
- package/dist/esm/node/qv2.d.ts.map +0 -1
- package/dist/esm/node/qv2.js.map +0 -1
- package/dist/esm/node/relay.d.ts.map +0 -1
- package/dist/esm/node/relay.js.map +0 -1
- package/dist/esm/node/sealed-agent-state.d.ts.map +0 -1
- package/dist/esm/node/sealed-agent-state.js.map +0 -1
- package/dist/esm/node/strict-json.d.ts.map +0 -1
- package/dist/esm/node/strict-json.js.map +0 -1
- package/dist/esm/node.d.ts.map +0 -1
- package/dist/esm/node.js.map +0 -1
- package/dist/esm/share.d.ts.map +0 -1
- package/dist/esm/share.js.map +0 -1
- package/dist/esm/types.d.ts.map +0 -1
- package/dist/esm/types.js.map +0 -1
- package/dist/esm/version.d.ts.map +0 -1
- package/dist/esm/version.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,37 +1,127 @@
|
|
|
1
|
-
#
|
|
1
|
+
# qURL TypeScript and Node.js SDK
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@layervai/qurl)
|
|
4
4
|
[](https://github.com/layervai/qurl-typescript/actions/workflows/ci.yml)
|
|
5
5
|
[](LICENSE)
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
private
|
|
7
|
+
Use [qURL™ by LayerV](https://layerv.ai/qurl/) to share localhost web apps,
|
|
8
|
+
private APIs, and files through expiring access links. This
|
|
9
|
+
[TypeScript and Node.js SDK](https://layerv.ai/sdk/) creates and manages access
|
|
10
|
+
from JavaScript or TypeScript. Use the
|
|
11
|
+
`@layervai/qurl` package in a service or automation script to register
|
|
12
|
+
resources, choose a link lifetime, and manage resource access.
|
|
9
13
|
|
|
10
|
-
|
|
14
|
+
For a local app, [publish it with the CLI](https://github.com/layervai/qurl-integrations/tree/main/apps/cli)
|
|
15
|
+
or configure a connector before issuing links. Creating an access link does
|
|
16
|
+
not start the app or close any separate public route to it. Your app's own
|
|
17
|
+
sign-in still applies.
|
|
11
18
|
|
|
12
19
|
Portal recipients do not need LayerV credentials, API keys, or SDK state. They
|
|
13
20
|
open the qURL link. Credentials are only for software that protects URLs or
|
|
14
21
|
creates portals.
|
|
15
22
|
|
|
16
|
-
|
|
23
|
+
[Installation](#installation) · [Quickstart](#quickstart) ·
|
|
24
|
+
[Opening received links](#opening-portals) · [API reference](#api) ·
|
|
25
|
+
[Resource and token management](#rest-shaped-api-compatibility)
|
|
26
|
+
|
|
27
|
+
[Local preview workflow](#share-a-localhost-preview-from-nodejs) ·
|
|
28
|
+
[File-sharing workflows](#share-files-from-a-nodejs-workflow) ·
|
|
29
|
+
[Sharing questions](#typescript-and-nodejs-sharing-questions) · [Private links](https://qurl.link/)
|
|
30
|
+
|
|
31
|
+
## Share a localhost preview from Node.js
|
|
32
|
+
|
|
33
|
+
Use `@layervai/qurl` when a Node.js backend or automation job should issue
|
|
34
|
+
temporary access to a preview, internal dashboard, or private API. Publish
|
|
35
|
+
the running local app with the [CLI](https://github.com/layervai/qurl-integrations#share-a-localhost-web-app)
|
|
36
|
+
or configure a compatible Connector before issuing a link from the SDK.
|
|
37
|
+
|
|
38
|
+
For an existing SDK-managed Connector resource, use its immutable slug to
|
|
39
|
+
look it up and create a portal. Set `QURL_API_KEY` to an issuer credential
|
|
40
|
+
authorized for that resource, and `QURL_CONNECTOR_SLUG` to its configured slug:
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
import { QURLClient } from '@layervai/qurl';
|
|
44
|
+
|
|
45
|
+
const apiKey = process.env.QURL_API_KEY;
|
|
46
|
+
const slug = process.env.QURL_CONNECTOR_SLUG;
|
|
47
|
+
if (!apiKey || !slug) {
|
|
48
|
+
throw new Error('Set QURL_API_KEY and QURL_CONNECTOR_SLUG for your configured resource');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const client = new QURLClient({ apiKey });
|
|
52
|
+
const resource = await client.getConnectorResourceBySlug(slug);
|
|
53
|
+
const portal = await resource.createPortal({ validFor: '5m' });
|
|
54
|
+
console.log(portal.link);
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Read-only lookup requires the resource to exist. The API credential must have
|
|
58
|
+
the required lookup and mint permissions. A Connector slug, a CRID, and an
|
|
59
|
+
account key are different things; do not substitute a CLI CRID for the slug
|
|
60
|
+
or assume your API key controls another device's published resource.
|
|
61
|
+
For a preview published solely through the CLI, `qurl share` is the direct
|
|
62
|
+
sharing path until compatible SDK resource management is configured.
|
|
63
|
+
|
|
64
|
+
Send the full link through a private channel. A browser recipient can use the
|
|
65
|
+
preview without installing the SDK; the app's own login continues to apply.
|
|
66
|
+
The local host, app, and serving runtime must stay available. Existing resource
|
|
67
|
+
handles can also mint links for a specific `targetPath` where the service
|
|
68
|
+
supports it; see [quickstart](#quickstart) for its resource and path constraints.
|
|
69
|
+
|
|
70
|
+
## Share files from a Node.js workflow
|
|
71
|
+
|
|
72
|
+
qURL also supports private document and file sharing. The SDK's resource and
|
|
73
|
+
portal calls issue access; they do not upload a file from your process or start
|
|
74
|
+
a localhost server.
|
|
75
|
+
|
|
76
|
+
| Use case | Serving or upload step | SDK role |
|
|
77
|
+
| --- | --- | --- |
|
|
78
|
+
| Share static preview files | Publish a dedicated localhost HTTP directory with the [CLI](https://github.com/layervai/qurl-integrations#share-files-and-documents). | Automate access only when the service is configured as a compatible resource for your credential; otherwise share through the CLI. |
|
|
79
|
+
| Share a generated PDF or supported image | Use the [MCP upload workflow](https://github.com/layervai/qurl-mcp#let-an-ai-agent-share-a-pdf-or-image) or the [Gmail integration](https://github.com/layervai/qurl-integrations/tree/main/apps/chrome-extension). | Keep the upload service's identifiers, expiry, and revocation contract; its returned file ID is not automatically an SDK Connector resource. |
|
|
80
|
+
| Give automation access to a private file service | Configure and register the service with the appropriate runtime. | Create a portal for the registered resource and use the documented opening and content-request flow. |
|
|
81
|
+
|
|
82
|
+
For programmatic access, follow [opening portals](#opening-portals), including
|
|
83
|
+
resource binding, HTTP request authorization, and redirect rules. Fetching the
|
|
84
|
+
browser share URL itself does not substitute for that opening flow.
|
|
85
|
+
|
|
86
|
+
## Choosing a Node.js preview workflow
|
|
87
|
+
|
|
88
|
+
A Node.js service can issue a qURL link to an authorized, configured resource, establish connectivity with an Agent SDK, or provide a deployed preview. Choose the operation before choosing the package:
|
|
89
|
+
|
|
90
|
+
| What the reviewer needs | Workflow |
|
|
91
|
+
| --- | --- |
|
|
92
|
+
| Temporary access to the current local app or private API | Publish through the CLI or configure the serving runtime, then use this SDK's resource and portal APIs with the appropriate issuer authority. |
|
|
93
|
+
| An endpoint established from the Node.js process | The [ngrok JavaScript Agent SDK](https://github.com/ngrok/ngrok-javascript) can forward to a local service; configure its endpoint access policies and keep the origin available. |
|
|
94
|
+
| A hosted application build that can outlive your development session | Create a [Vercel Preview deployment](https://vercel.com/docs/deployments/environments), with its environment and deployment protection configured. |
|
|
95
|
+
|
|
96
|
+
Keep issuer credentials in the issuing service. A browser reviewer receives the access link and still follows the app's login requirements; a programmatic consumer follows [opening portals](#opening-portals). For generated files, first decide between a local file-serving resource and uploaded storage using the [file-sharing comparison](https://github.com/layervai/qurl-integrations#should-a-file-stay-on-the-local-host-or-be-uploaded). Link issuance does not perform the upload or deploy the app.
|
|
97
|
+
|
|
98
|
+
## TypeScript and Node.js sharing questions
|
|
99
|
+
|
|
100
|
+
### Does `protectUrl` publish the app on my laptop?
|
|
101
|
+
|
|
102
|
+
No. It registers a URL resource. Local serving needs the CLI or another
|
|
103
|
+
configured runtime. An existing public route remains reachable unless you
|
|
104
|
+
close or restrict it.
|
|
105
|
+
|
|
106
|
+
### Does link expiry terminate an already open session?
|
|
17
107
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
probing before a legitimate user or agent ever arrives.
|
|
108
|
+
Link redemption and session lifetime are separate. Configure both through
|
|
109
|
+
the options supported by your resource and service. An expired link can deny
|
|
110
|
+
a new opening while an earlier session remains valid.
|
|
22
111
|
|
|
23
|
-
|
|
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:
|
|
112
|
+
### Can I identify a recipient by setting a link label?
|
|
30
113
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
114
|
+
A label is metadata. It does not make the link non-transferable or replace
|
|
115
|
+
application login. Treat returned links as access credentials, and keep issuer
|
|
116
|
+
keys in the issuing service. Use [opening portals](#opening-portals) for the
|
|
117
|
+
credential and transport requirements of a programmatic recipient.
|
|
118
|
+
|
|
119
|
+
## Why qURL?
|
|
120
|
+
|
|
121
|
+
qURL separates a private resource from the access links your code issues for
|
|
122
|
+
it. A running Connector supplies the access path to the origin; this SDK
|
|
123
|
+
manages resources, links, and supported sessions. It can fit a preview-review
|
|
124
|
+
or private API workflow without moving the app to a public staging server.
|
|
35
125
|
|
|
36
126
|
## Installation
|
|
37
127
|
|
|
@@ -50,12 +140,26 @@ import { QURLClient } from '@layervai/qurl';
|
|
|
50
140
|
|
|
51
141
|
const client = new QURLClient({ apiKey: 'YOUR_API_KEY' });
|
|
52
142
|
|
|
53
|
-
const resource = await client.protectUrl('https://internal.example.com/dashboard'
|
|
143
|
+
const resource = await client.protectUrl('https://internal.example.com/dashboard', {
|
|
144
|
+
name: 'Team dashboard', // public name recipients see; required to create a resource
|
|
145
|
+
});
|
|
54
146
|
const portal = await resource.createPortal({ validFor: '5m' });
|
|
55
147
|
|
|
56
148
|
console.log(portal.link); // Share this link — recipients need no credentials
|
|
57
149
|
```
|
|
58
150
|
|
|
151
|
+
`name` is the public name recipients see on the qURL page, in link previews,
|
|
152
|
+
and on the passkey they save; `description` stays a private owner note. The
|
|
153
|
+
service requires it whenever a call creates a resource (`protectUrl`,
|
|
154
|
+
`createResource`, `createPortalForUrl`, `create`, each `batchCreate` item, and
|
|
155
|
+
`ensureConnectorResource` for a new slug) and refuses a create without it with
|
|
156
|
+
a 400 `ValidationError` whose `code` is `ERROR_CODE_NAME_REQUIRED`
|
|
157
|
+
(`"name_required"`); nothing is written. It is ignored when the call reuses an
|
|
158
|
+
existing resource. `createPortal` rejects it because its resource already
|
|
159
|
+
exists. The SDK sends `name` only when you pass it, and it needs a qURL API
|
|
160
|
+
version that accepts the field. Names follow the service rule: at most 100
|
|
161
|
+
characters, one line, no surrounding whitespace or hidden characters.
|
|
162
|
+
|
|
59
163
|
That is the core flow:
|
|
60
164
|
|
|
61
165
|
| Step | Call | What you provide |
|
|
@@ -75,6 +179,8 @@ by immutable slug instead of calling `protectUrl`:
|
|
|
75
179
|
const { resource, foundExisting } = await client.ensureConnectorResource(
|
|
76
180
|
'prod-dashboard',
|
|
77
181
|
{ idempotencyKey: 'connector-bootstrap-prod-dashboard' },
|
|
182
|
+
undefined, // consumer access policy
|
|
183
|
+
'Prod dashboard', // public name; required when the slug is new
|
|
78
184
|
);
|
|
79
185
|
console.log(foundExisting ? 'Using existing connector resource' : 'Created connector resource');
|
|
80
186
|
const portal = await resource.createPortal({
|
|
@@ -83,10 +189,27 @@ const portal = await resource.createPortal({
|
|
|
83
189
|
});
|
|
84
190
|
```
|
|
85
191
|
|
|
86
|
-
`targetPath` is available only when minting for an existing resource.
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
192
|
+
`targetPath` is available only when minting for an existing resource. It must
|
|
193
|
+
be a canonical ASCII path with optional query, at most 2048 bytes. The SDK
|
|
194
|
+
rejects authority changes, fragments, backslashes, controls, dot segments,
|
|
195
|
+
interior empty segments, path percent escapes, and path `!()*;`. Query escapes
|
|
196
|
+
stay encoded and byte-exact. The API enforces the tunnel-only resource gate;
|
|
197
|
+
`createPortalForUrl` rejects this option.
|
|
198
|
+
|
|
199
|
+
`portal.targetPath` reports the recorded scope. It is absent for the bare origin.
|
|
200
|
+
A missing, changed, malformed, or unexpected scope in a mint response fails with
|
|
201
|
+
`unexpected_response`. The mint may already have committed; do not blindly
|
|
202
|
+
repeat it. The raw REST response exposes the recorded `target_path` field.
|
|
203
|
+
|
|
204
|
+
Go and TypeScript test the same 119 released target-path cases. The artifact in
|
|
205
|
+
this SDK's existing `@layervai/qurl-conformance` 0.15.0 dependency is byte-identical
|
|
206
|
+
to Go's v0.17.2 artifact: SHA-256
|
|
207
|
+
`898e5bf24c825bbd57426fe799d71893951c5735b6cd15e9b95d436981b09a97`.
|
|
208
|
+
The merged producer OpenAPI baseline is qurl-service
|
|
209
|
+
`376752482f42b43630b9d0d5734ff3be1048e341`, file SHA-256
|
|
210
|
+
`06e1a25319d0f696de40213b7578ccb9c93440f03fad5dbf7e37a32cbc295b35`.
|
|
211
|
+
The complete Sandbox Connector journey must still prove scoped-path access after
|
|
212
|
+
NHP admission, sibling-path denial, and direct-origin denial.
|
|
90
213
|
|
|
91
214
|
`resource.crid` is the required management identifier. Connector lookup, delete,
|
|
92
215
|
and portal minting use it with no public-key or private-ID fallback. The SDK
|
|
@@ -144,7 +267,7 @@ server-populated resource metadata:
|
|
|
144
267
|
```typescript
|
|
145
268
|
const { portal, resource } = await client.createPortalForUrl(
|
|
146
269
|
'https://internal.example.com/dashboard',
|
|
147
|
-
{ validFor: '5m' },
|
|
270
|
+
{ name: 'Team dashboard', validFor: '5m' },
|
|
148
271
|
);
|
|
149
272
|
```
|
|
150
273
|
|
|
@@ -521,6 +644,7 @@ if you already build on them:
|
|
|
521
644
|
// Create a protected link (portal equivalent: createPortalForUrl)
|
|
522
645
|
const result = await client.create({
|
|
523
646
|
target_url: 'https://api.example.com/data',
|
|
647
|
+
name: 'Agent data API',
|
|
524
648
|
expires_in: '24h',
|
|
525
649
|
label: 'API access for agent',
|
|
526
650
|
});
|
|
@@ -552,7 +676,7 @@ console.log(`Access granted to ${access.target_url} for ${access.access_grant?.e
|
|
|
552
676
|
| `protectUrl(targetUrl, opts?)` | Protect a private URL → portal-minting `ProtectedResource` handle |
|
|
553
677
|
| `resource.createPortal(opts?)` / `createPortal(resourceOrId, opts?)` | Mint a short-lived portal link; existing resources can set `targetPath` |
|
|
554
678
|
| `createPortalForUrl(targetUrl, opts?)` | Protect + mint a URL resource; rejects `targetPath` |
|
|
555
|
-
| `ensureConnectorResource(slug, requestOptions?)` | Find or create an active Connector resource by immutable slug |
|
|
679
|
+
| `ensureConnectorResource(slug, requestOptions?, consumerAccess?, name?)` | Find or create an active Connector resource by immutable slug |
|
|
556
680
|
| `getConnectorResource(crid)` / `getConnectorResourceBySlug(slug)` | Load a validated Connector resource by immutable identity |
|
|
557
681
|
| `deleteConnectorResource(crid)` | Revoke a Connector resource by CRID |
|
|
558
682
|
| `resourceByCrid(crid)` | Handle from a stored CRID (no API call) |
|
|
@@ -572,6 +696,7 @@ console.log(`Access granted to ${access.target_url} for ${access.access_grant?.e
|
|
|
572
696
|
| `update(id, input)` | Update qURL resource properties |
|
|
573
697
|
| `mintLink(id, input?)` | Mint a new access link |
|
|
574
698
|
| `resolve(input)` | Resolve token + grant network access |
|
|
699
|
+
| `getIdentity()` | Get the current account and credential identity |
|
|
575
700
|
| `getQuota()` | Get quota/usage info |
|
|
576
701
|
| `listResources(input?)` / `listAllResources(input?)` / `createResource(input)` / `getResource(id)` | Resource management |
|
|
577
702
|
| `updateResource(id, input)` / `deleteResource(id)` | Update or revoke resources |
|
|
@@ -603,8 +728,8 @@ Create up to 100 qURLs in a single request. **Does not throw on partial or total
|
|
|
603
728
|
```typescript
|
|
604
729
|
const result = await client.batchCreate({
|
|
605
730
|
items: [
|
|
606
|
-
{ target_url: 'https://api.example.com/data', expires_in: '24h' },
|
|
607
|
-
{ target_url: 'https://api.example.com/admin', expires_in: '1h' },
|
|
731
|
+
{ target_url: 'https://api.example.com/data', name: 'Data API', expires_in: '24h' },
|
|
732
|
+
{ target_url: 'https://api.example.com/admin', name: 'Admin API', expires_in: '1h' },
|
|
608
733
|
],
|
|
609
734
|
});
|
|
610
735
|
|
|
@@ -667,6 +792,28 @@ try {
|
|
|
667
792
|
| `TimeoutError` | — | Request exceeded timeout |
|
|
668
793
|
| `DelegatedBatchOutcomeUnknownError` | — | Dispatched batch create needs reconciliation |
|
|
669
794
|
|
|
795
|
+
On `QURLError`, `requestId` identifies the API request. A `410` error can also
|
|
796
|
+
include `tombstone`, with the retained closure timestamp and an optional access
|
|
797
|
+
count. An absent count does not imply zero accesses.
|
|
798
|
+
|
|
799
|
+
Successful API results expose an optional correlation ID through
|
|
800
|
+
`getResponseMetadata`. This works for result objects, page objects, SDK handles,
|
|
801
|
+
and items yielded by page iterators. Each value keeps the ID of the response
|
|
802
|
+
that produced it, including concurrent calls. Aggregated results use the final
|
|
803
|
+
page's ID. The ID is for diagnostics; it does not authenticate a result.
|
|
804
|
+
|
|
805
|
+
```typescript
|
|
806
|
+
import { getResponseMetadata } from '@layervai/qurl';
|
|
807
|
+
|
|
808
|
+
const page = await client.list({ limit: 10 });
|
|
809
|
+
console.log(getResponseMetadata(page)?.requestId);
|
|
810
|
+
```
|
|
811
|
+
|
|
812
|
+
Metadata is read-only and does not change enumerable fields or JSON output.
|
|
813
|
+
It is lost when a result is serialized or copied with object spread. Local
|
|
814
|
+
handles, no-content responses, and responses without a valid ID return
|
|
815
|
+
`undefined`. Error responses continue to use `QURLError.requestId`.
|
|
816
|
+
|
|
670
817
|
Client-detected failures use `status: 0` with a discriminating `code`:
|
|
671
818
|
`"client_validation"` for bad input caught before a request, and — on the
|
|
672
819
|
portal surface — `"resource_not_found"` / `"ambiguous_resource"` when
|
|
@@ -687,6 +834,29 @@ for await (const qurl of client.listAll({ status: 'active' })) {
|
|
|
687
834
|
}
|
|
688
835
|
```
|
|
689
836
|
|
|
837
|
+
### Restarting an API-key inventory after cutover
|
|
838
|
+
|
|
839
|
+
An API-key inventory cursor can become invalid after a server-side identity
|
|
840
|
+
migration. The SDK reports this as `QURLError` with `status === 400` and
|
|
841
|
+
`code === "invalid_cursor"`. Do not resend that cursor or treat a partial
|
|
842
|
+
inventory as complete.
|
|
843
|
+
|
|
844
|
+
`listAllApiKeys()` propagates the error after any earlier keys have been yielded.
|
|
845
|
+
It does not rewind automatically: that would deliver those keys twice. An
|
|
846
|
+
application collecting an inventory before displaying it can recover once:
|
|
847
|
+
|
|
848
|
+
1. Discard all keys collected by the failed iteration.
|
|
849
|
+
2. Create a new `client.listAllApiKeys()` iterator with the same filters and
|
|
850
|
+
page size. The new iterator starts at the first page without a cursor.
|
|
851
|
+
3. Publish only the completed replacement inventory. If the replacement also
|
|
852
|
+
fails, report that failure instead of restarting again.
|
|
853
|
+
|
|
854
|
+
Keep the application's inventory size and request limits in force across both
|
|
855
|
+
attempts. Do not use this pattern to repeat side effects already performed for
|
|
856
|
+
yielded keys. Other errors retain their existing handling. A caller using
|
|
857
|
+
`listApiKeys()` directly must also discard its partial inventory and omit the
|
|
858
|
+
old `cursor` on the replacement first-page request.
|
|
859
|
+
|
|
690
860
|
## Debug Logging
|
|
691
861
|
|
|
692
862
|
Enable debug output to see all HTTP requests and retries:
|
|
@@ -754,7 +924,7 @@ For POST/PATCH requests, the SDK generates a UUIDv7 `Idempotency-Key` once per l
|
|
|
754
924
|
|
|
755
925
|
```typescript
|
|
756
926
|
await client.create(
|
|
757
|
-
{ target_url: 'https://api.example.com/data' },
|
|
927
|
+
{ target_url: 'https://api.example.com/data', name: 'Data API' },
|
|
758
928
|
{ idempotencyKey: 'job_12345_create_qurl' },
|
|
759
929
|
);
|
|
760
930
|
```
|
|
@@ -785,17 +955,17 @@ SDK-generated keys require `globalThis.crypto.getRandomValues`, which is availab
|
|
|
785
955
|
that names this shim requirement but does not reflect the invalid value. A
|
|
786
956
|
non-redirecting 304 is handled as an ordinary unsuccessful API response
|
|
787
957
|
rather than mislabeled as a redirect.
|
|
788
|
-
- API success and error bodies
|
|
789
|
-
matching qurl-go
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
958
|
+
- API success and error bodies default to **1 MiB (1,048,576 bytes)**,
|
|
959
|
+
matching qurl-go. `maxResponseBodyBytes` accepts a positive integer through
|
|
960
|
+
**16 MiB**. Streamed decoded bytes are counted even with missing or false-small
|
|
961
|
+
Content-Length. A valid oversized unencoded length may reject early;
|
|
962
|
+
compressed lengths are not compared with the decoded-byte cap. Exactly the
|
|
963
|
+
configured number of bytes is accepted.
|
|
964
|
+
Large legal pages can exceed the default. Prefer a smaller page or explicitly
|
|
965
|
+
raise the cap within the shared ceiling. Save each successful `next_cursor`
|
|
966
|
+
and retry a failed read from that same cursor with unchanged filters and a
|
|
967
|
+
smaller `limit`. See the [shared Go/TypeScript response-limit decision](docs/api-response-limits.md)
|
|
968
|
+
for typed errors, the published service bounds, cursor recovery and memory cost.
|
|
799
969
|
Standards-compliant fetch implementations are bounded while streaming;
|
|
800
970
|
custom Response-like shims that omit `body` are validated after their
|
|
801
971
|
`text()`/`json()` result has already been materialized by that shim.
|
|
@@ -979,9 +1149,10 @@ OTP, and completion packet construction and reply decryption.
|
|
|
979
1149
|
`updateResource` changes the policy with `expected_consumer_access_version` and
|
|
980
1150
|
the next version. Send policy changes separately from metadata updates.
|
|
981
1151
|
|
|
982
|
-
For a URL, `protectUrl(url, { consumerAccess: { mode: "invite_only", version: 1 } })`
|
|
1152
|
+
For a URL, `protectUrl(url, { name, consumerAccess: { mode: "invite_only", version: 1 } })`
|
|
983
1153
|
creates the private resource and checks the returned policy. For a local
|
|
984
|
-
Connector, pass the policy as the third argument to `ensureConnectorResource
|
|
1154
|
+
Connector, pass the policy as the third argument to `ensureConnectorResource`
|
|
1155
|
+
and the name as the fourth.
|
|
985
1156
|
A private policy cannot be combined with public access. A service that ignores
|
|
986
1157
|
the requested policy is refused.
|
|
987
1158
|
|
|
@@ -989,16 +1160,58 @@ Publisher methods are `inviteConsumer`, `listConsumerInvitations`,
|
|
|
989
1160
|
`resendConsumerInvitation`, `revokeConsumerInvitation`, `listConsumerGrants`,
|
|
990
1161
|
`approveConsumerGrant`, `denyConsumerGrant` and `revokeConsumerGrant`. Decisions
|
|
991
1162
|
use the version read from the invitation or grant. Lists return the usual
|
|
992
|
-
`nextCursor`.
|
|
993
|
-
receipt and expiry; the SDK does not verify
|
|
1163
|
+
`nextCursor`. Without a Stripe payment policy, contract entitlement approval
|
|
1164
|
+
requires a trusted publisher backend receipt and expiry; the SDK does not verify
|
|
1165
|
+
payment. Contract replies omit email
|
|
994
1166
|
and globally reusable credential data.
|
|
995
1167
|
|
|
1168
|
+
For paid contracts, call `connectPublisherStripe()`, open the returned `url`,
|
|
1169
|
+
then read `publisherStripeAccount()`. Set `consumer_access.payment` to
|
|
1170
|
+
`{ account_id, price_id, access_seconds }` using the connected `account_id` and an
|
|
1171
|
+
active fixed Stripe price from that account. Use the policy when creating a
|
|
1172
|
+
resource or updating an existing CRID. The service makes paid access private
|
|
1173
|
+
and verifies payment; manual approval is unavailable. LayerV collects 3%, including
|
|
1174
|
+
renewals. For a one-time price, `access_seconds: 0` lasts for the resource lifetime.
|
|
1175
|
+
A recurring price requires zero and grants only the paid invoice period.
|
|
1176
|
+
|
|
996
1177
|
A removal receipt reports `session_cleanup: pending` until connection cleanup
|
|
997
1178
|
is confirmed. Retry the same removal and version to confirm `complete`. These
|
|
998
1179
|
APIs require the matching service feature and remain off by default during
|
|
999
1180
|
integration. Consumer Noise enrollment and browser access are in the NHP agent.
|
|
1000
1181
|
|
|
1001
|
-
Email access is
|
|
1182
|
+
Email access is available as a backup to passkeys, except for Stripe paid
|
|
1183
|
+
contracts, which require a passkey. The recipient chooses
|
|
1002
1184
|
**Email me an access link** and verifies the mailbox. No publisher setting is
|
|
1003
1185
|
needed or available. Invitations, approval, entitlement, expiry, and revocation
|
|
1004
1186
|
still apply.
|
|
1187
|
+
|
|
1188
|
+
|
|
1189
|
+
## Bind a Slack, Discord, or Teams identity
|
|
1190
|
+
|
|
1191
|
+
`createExternalIdentityBinding` uses the JWT from the provider's configured
|
|
1192
|
+
confidential OAuth flow as the client's bearer credential (`apiKey`). The
|
|
1193
|
+
provider determines the new key's scopes. Slack and Teams also require the
|
|
1194
|
+
JWT's `qurl:agent` permission.
|
|
1195
|
+
|
|
1196
|
+
Persist a unique `RequestOptions.idempotencyKey` (32–256 printable ASCII
|
|
1197
|
+
characters) before the first request. Retry a lost response within 24 hours
|
|
1198
|
+
with that same key and body. Store `result.api_key.plaintext` in your secret
|
|
1199
|
+
store immediately. It is a one-time secret: never log or serialize the result
|
|
1200
|
+
into diagnostics. After the replay window, the secret cannot be retrieved.
|
|
1201
|
+
|
|
1202
|
+
```ts
|
|
1203
|
+
const setup = new QURLClient({ apiKey: providerOAuthJwt });
|
|
1204
|
+
const result = await setup.createExternalIdentityBinding(
|
|
1205
|
+
{ provider: "teams", external_id: verifiedTenantId, display_name: "Example tenant" },
|
|
1206
|
+
{ idempotencyKey: persistedSetupRequestId },
|
|
1207
|
+
);
|
|
1208
|
+
await secretStore.save(result.binding_id, result.api_key.plaintext);
|
|
1209
|
+
```
|
|
1210
|
+
|
|
1211
|
+
Your integration supplies the verified identity, OAuth JWT, durable setup ID,
|
|
1212
|
+
and secret store. Use `rotate_existing: true` only for explicit reauthorization:
|
|
1213
|
+
it revokes the previous binding key. Serialize setup operations for each identity.
|
|
1214
|
+
A `409` retains `QURLError.status` and the `already_exists` or
|
|
1215
|
+
`idempotency_conflict` code. A `503 service_unavailable` retains `retryAfter`;
|
|
1216
|
+
wait and retry with the same request key and body. `503 bindings_disabled`
|
|
1217
|
+
requires a fallback flow. Ordinary retries cannot enable the feature.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { CreatePortalOptions, Portal, RequestOptions, Resource } from "./types.js";
|
|
2
|
+
import type { QURLClient } from "./client.js";
|
|
3
|
+
export declare const CONNECTOR_SLUG_PATTERN: RegExp;
|
|
4
|
+
export declare function requireConnectorSlug(slug: string, method: string): void;
|
|
5
|
+
export declare function classifyConnectorMutationFailure(operation: "ensure" | "delete", error: unknown): never;
|
|
6
|
+
export declare function requireConnectorSubtleCrypto(method: string): void;
|
|
7
|
+
type ConnectorResourceExpectation = {
|
|
8
|
+
slug?: string;
|
|
9
|
+
crid?: string;
|
|
10
|
+
revokedIsLifecycleError?: boolean;
|
|
11
|
+
};
|
|
12
|
+
export declare function verifyResourceIdentity(resource: {
|
|
13
|
+
resource_id?: unknown;
|
|
14
|
+
crid?: unknown;
|
|
15
|
+
} | undefined, method: string, expectedCrid?: string): Promise<void>;
|
|
16
|
+
export declare function parseConnectorResource(client: QURLClient, value: unknown, method: string, expectation?: ConnectorResourceExpectation): Promise<ConnectorResource>;
|
|
17
|
+
/** Result of idempotently ensuring a qURL Connector resource. */
|
|
18
|
+
export interface EnsureConnectorResourceResult {
|
|
19
|
+
resource: ConnectorResource;
|
|
20
|
+
foundExisting: boolean;
|
|
21
|
+
}
|
|
22
|
+
declare const CONNECTOR_RESOURCE_CONSTRUCTOR_TOKEN: unique symbol;
|
|
23
|
+
/**
|
|
24
|
+
* A validated qURL Connector resource bound to the client that loaded it.
|
|
25
|
+
*
|
|
26
|
+
* Resource identity, reverse-routing identity, and NHP admission identity are
|
|
27
|
+
* distinct server-issued values. Callers must consume each field verbatim and
|
|
28
|
+
* must never derive one from another.
|
|
29
|
+
*/
|
|
30
|
+
export declare class ConnectorResource {
|
|
31
|
+
#private;
|
|
32
|
+
readonly resourcePublicKey: string;
|
|
33
|
+
/** Required public identifier, verified against the returned resource key. */
|
|
34
|
+
readonly crid: string;
|
|
35
|
+
readonly connectorRoutingId: string;
|
|
36
|
+
readonly knockResourceId: string;
|
|
37
|
+
readonly slug: string;
|
|
38
|
+
readonly alias?: string;
|
|
39
|
+
/** @internal Instances are returned by QURLClient after wire validation. */
|
|
40
|
+
constructor(client: QURLClient, details: Resource, validationToken: typeof CONNECTOR_RESOURCE_CONSTRUCTOR_TOKEN);
|
|
41
|
+
/** Mint a short-lived portal for this Connector resource. */
|
|
42
|
+
createPortal(opts?: CreatePortalOptions, requestOptions?: RequestOptions): Promise<Portal>;
|
|
43
|
+
}
|
|
44
|
+
export {};
|