@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.
Files changed (152) hide show
  1. package/README.md +259 -46
  2. package/dist/cjs/client-connectors.d.ts +44 -0
  3. package/dist/cjs/client-connectors.js +189 -0
  4. package/dist/cjs/client-delegated.d.ts +30 -0
  5. package/dist/cjs/client-delegated.js +312 -0
  6. package/dist/cjs/client-errors.d.ts +44 -0
  7. package/dist/cjs/client-errors.js +219 -0
  8. package/dist/cjs/client-http-types.d.ts +37 -0
  9. package/dist/cjs/client-http-types.js +2 -0
  10. package/dist/cjs/client-input-fields.d.ts +53 -0
  11. package/dist/cjs/client-input-fields.js +262 -0
  12. package/dist/cjs/client-limits.d.ts +36 -0
  13. package/dist/cjs/client-limits.js +61 -0
  14. package/dist/cjs/client-management.d.ts +12 -0
  15. package/dist/cjs/client-management.js +119 -0
  16. package/dist/cjs/client-paths.d.ts +21 -0
  17. package/dist/cjs/client-paths.js +143 -0
  18. package/dist/cjs/client-portals.d.ts +82 -0
  19. package/dist/cjs/client-portals.js +244 -0
  20. package/dist/cjs/client-resources.d.ts +29 -0
  21. package/dist/cjs/client-resources.js +331 -0
  22. package/dist/cjs/client-validation.d.ts +35 -0
  23. package/dist/cjs/client-validation.js +326 -0
  24. package/dist/cjs/client.d.ts +45 -75
  25. package/dist/cjs/client.js +621 -2448
  26. package/dist/cjs/errors.d.ts +9 -1
  27. package/dist/cjs/errors.js +10 -1
  28. package/dist/cjs/index.d.ts +4 -2
  29. package/dist/cjs/index.js +4 -1
  30. package/dist/cjs/response-metadata.d.ts +13 -0
  31. package/dist/cjs/response-metadata.js +38 -0
  32. package/dist/cjs/share.js +2 -0
  33. package/dist/cjs/target-path.d.ts +2 -0
  34. package/dist/cjs/target-path.js +34 -0
  35. package/dist/cjs/types.d.ts +83 -7
  36. package/dist/cjs/version.d.ts +1 -1
  37. package/dist/cjs/version.js +1 -1
  38. package/dist/esm/client-connectors.d.ts +44 -0
  39. package/dist/esm/client-connectors.js +180 -0
  40. package/dist/esm/client-delegated.d.ts +30 -0
  41. package/dist/esm/client-delegated.js +295 -0
  42. package/dist/esm/client-errors.d.ts +44 -0
  43. package/dist/esm/client-errors.js +206 -0
  44. package/dist/esm/client-http-types.d.ts +37 -0
  45. package/dist/esm/client-http-types.js +1 -0
  46. package/dist/esm/client-input-fields.d.ts +53 -0
  47. package/dist/esm/client-input-fields.js +258 -0
  48. package/dist/esm/client-limits.d.ts +36 -0
  49. package/dist/esm/client-limits.js +58 -0
  50. package/dist/esm/client-management.d.ts +12 -0
  51. package/dist/esm/client-management.js +113 -0
  52. package/dist/esm/client-paths.d.ts +21 -0
  53. package/dist/esm/client-paths.js +138 -0
  54. package/dist/esm/client-portals.d.ts +82 -0
  55. package/dist/esm/client-portals.js +236 -0
  56. package/dist/esm/client-resources.d.ts +29 -0
  57. package/dist/esm/client-resources.js +323 -0
  58. package/dist/esm/client-validation.d.ts +35 -0
  59. package/dist/esm/client-validation.js +302 -0
  60. package/dist/esm/client.d.ts +45 -76
  61. package/dist/esm/client.js +318 -2147
  62. package/dist/esm/crid.d.ts +0 -1
  63. package/dist/esm/crid.js +0 -1
  64. package/dist/esm/errors.d.ts +9 -2
  65. package/dist/esm/errors.js +9 -1
  66. package/dist/esm/index.d.ts +4 -3
  67. package/dist/esm/index.js +2 -2
  68. package/dist/esm/internal.d.ts +0 -1
  69. package/dist/esm/internal.js +0 -1
  70. package/dist/esm/node/agent-runtime.d.ts +0 -1
  71. package/dist/esm/node/agent-runtime.js +0 -1
  72. package/dist/esm/node/agent-state.d.ts +0 -1
  73. package/dist/esm/node/agent-state.js +0 -1
  74. package/dist/esm/node/agent-transport.d.ts +0 -1
  75. package/dist/esm/node/agent-transport.js +0 -1
  76. package/dist/esm/node/deployment.d.ts +0 -1
  77. package/dist/esm/node/deployment.js +0 -1
  78. package/dist/esm/node/file-agent-state.d.ts +0 -1
  79. package/dist/esm/node/file-agent-state.js +0 -1
  80. package/dist/esm/node/native-udp.d.ts +0 -1
  81. package/dist/esm/node/native-udp.js +0 -1
  82. package/dist/esm/node/nhp-wire.d.ts +0 -1
  83. package/dist/esm/node/nhp-wire.js +0 -1
  84. package/dist/esm/node/portal-opener.d.ts +0 -1
  85. package/dist/esm/node/portal-opener.js +0 -1
  86. package/dist/esm/node/provider.d.ts +0 -1
  87. package/dist/esm/node/provider.js +0 -1
  88. package/dist/esm/node/qv2.d.ts +0 -1
  89. package/dist/esm/node/qv2.js +0 -1
  90. package/dist/esm/node/relay.d.ts +0 -1
  91. package/dist/esm/node/relay.js +0 -1
  92. package/dist/esm/node/sealed-agent-state.d.ts +0 -1
  93. package/dist/esm/node/sealed-agent-state.js +0 -1
  94. package/dist/esm/node/strict-json.d.ts +0 -1
  95. package/dist/esm/node/strict-json.js +0 -1
  96. package/dist/esm/node.d.ts +0 -1
  97. package/dist/esm/node.js +0 -1
  98. package/dist/esm/response-metadata.d.ts +13 -0
  99. package/dist/esm/response-metadata.js +33 -0
  100. package/dist/esm/share.d.ts +0 -1
  101. package/dist/esm/share.js +2 -1
  102. package/dist/esm/target-path.d.ts +2 -0
  103. package/dist/esm/target-path.js +30 -0
  104. package/dist/esm/types.d.ts +83 -8
  105. package/dist/esm/types.js +0 -1
  106. package/dist/esm/version.d.ts +1 -2
  107. package/dist/esm/version.js +1 -2
  108. package/package.json +9 -7
  109. package/dist/esm/client.d.ts.map +0 -1
  110. package/dist/esm/client.js.map +0 -1
  111. package/dist/esm/crid.d.ts.map +0 -1
  112. package/dist/esm/crid.js.map +0 -1
  113. package/dist/esm/errors.d.ts.map +0 -1
  114. package/dist/esm/errors.js.map +0 -1
  115. package/dist/esm/index.d.ts.map +0 -1
  116. package/dist/esm/index.js.map +0 -1
  117. package/dist/esm/internal.d.ts.map +0 -1
  118. package/dist/esm/internal.js.map +0 -1
  119. package/dist/esm/node/agent-runtime.d.ts.map +0 -1
  120. package/dist/esm/node/agent-runtime.js.map +0 -1
  121. package/dist/esm/node/agent-state.d.ts.map +0 -1
  122. package/dist/esm/node/agent-state.js.map +0 -1
  123. package/dist/esm/node/agent-transport.d.ts.map +0 -1
  124. package/dist/esm/node/agent-transport.js.map +0 -1
  125. package/dist/esm/node/deployment.d.ts.map +0 -1
  126. package/dist/esm/node/deployment.js.map +0 -1
  127. package/dist/esm/node/file-agent-state.d.ts.map +0 -1
  128. package/dist/esm/node/file-agent-state.js.map +0 -1
  129. package/dist/esm/node/native-udp.d.ts.map +0 -1
  130. package/dist/esm/node/native-udp.js.map +0 -1
  131. package/dist/esm/node/nhp-wire.d.ts.map +0 -1
  132. package/dist/esm/node/nhp-wire.js.map +0 -1
  133. package/dist/esm/node/portal-opener.d.ts.map +0 -1
  134. package/dist/esm/node/portal-opener.js.map +0 -1
  135. package/dist/esm/node/provider.d.ts.map +0 -1
  136. package/dist/esm/node/provider.js.map +0 -1
  137. package/dist/esm/node/qv2.d.ts.map +0 -1
  138. package/dist/esm/node/qv2.js.map +0 -1
  139. package/dist/esm/node/relay.d.ts.map +0 -1
  140. package/dist/esm/node/relay.js.map +0 -1
  141. package/dist/esm/node/sealed-agent-state.d.ts.map +0 -1
  142. package/dist/esm/node/sealed-agent-state.js.map +0 -1
  143. package/dist/esm/node/strict-json.d.ts.map +0 -1
  144. package/dist/esm/node/strict-json.js.map +0 -1
  145. package/dist/esm/node.d.ts.map +0 -1
  146. package/dist/esm/node.js.map +0 -1
  147. package/dist/esm/share.d.ts.map +0 -1
  148. package/dist/esm/share.js.map +0 -1
  149. package/dist/esm/types.d.ts.map +0 -1
  150. package/dist/esm/types.js.map +0 -1
  151. package/dist/esm/version.d.ts.map +0 -1
  152. package/dist/esm/version.js.map +0 -1
package/README.md CHANGED
@@ -1,37 +1,127 @@
1
- # @layervai/qurl
1
+ # qURL TypeScript and Node.js SDK
2
2
 
3
3
  [![npm](https://img.shields.io/npm/v/@layervai/qurl)](https://www.npmjs.com/package/@layervai/qurl)
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
- **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.**
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
- > **Quantum URL (qURL)** · The internet has a hidden layer. This is how you enter.
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
- ## Why qURL?
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
- 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.
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
- 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:
112
+ ### Can I identify a recipient by setting a link label?
30
113
 
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
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. The SDK
87
- checks the non-empty 2048-byte boundary; the API remains authoritative for the
88
- path grammar and the tunnel-only resource gate. `createPortalForUrl` rejects
89
- this option because it creates a URL resource.
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 are limited to **1 MiB (1,048,576 bytes)**,
789
- matching qurl-go's security posture. The SDK checks `Content-Length` when
790
- present and independently counts streamed bytes, so missing or inaccurate
791
- headers cannot bypass the limit. Bodies exactly at the limit are accepted.
792
- This fixed security limit has no override. Callers must request a smaller
793
- page, and the SDKs must not independently widen the limit.
794
- A maximum-size list page can exceed the cap after JSON escaping; request a
795
- smaller page if a list call reports the body-limit error. For resumable list
796
- reads, use the page method, save each successful `next_cursor`, and resume
797
- from that cursor with a smaller `limit`. A shared configurable-cap decision
798
- is tracked in [#249](https://github.com/layervai/qurl-typescript/issues/249).
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`. Contract entitlement approval requires a trusted publisher backend
993
- receipt and expiry; the SDK does not verify payment. Contract replies omit email
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 always available as a backup to passkeys. The recipient chooses
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 {};