@layervai/qurl 0.7.0 → 2.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 (121) hide show
  1. package/README.md +212 -1
  2. package/dist/cjs/client.d.ts +19 -1
  3. package/dist/cjs/client.js +520 -9
  4. package/dist/cjs/crid.d.ts +2 -1
  5. package/dist/cjs/crid.js +11 -2
  6. package/dist/cjs/errors.d.ts +20 -3
  7. package/dist/cjs/errors.js +31 -4
  8. package/dist/cjs/index.d.ts +2 -2
  9. package/dist/cjs/index.js +3 -1
  10. package/dist/cjs/node/agent-runtime.d.ts +108 -0
  11. package/dist/cjs/node/agent-runtime.js +952 -0
  12. package/dist/cjs/node/agent-state.d.ts +100 -0
  13. package/dist/cjs/node/agent-state.js +311 -0
  14. package/dist/cjs/node/agent-transport.d.ts +17 -0
  15. package/dist/cjs/node/agent-transport.js +118 -0
  16. package/dist/cjs/node/deployment.d.ts +6 -0
  17. package/dist/cjs/node/deployment.js +24 -3
  18. package/dist/cjs/node/file-agent-state.d.ts +15 -0
  19. package/dist/cjs/node/file-agent-state.js +137 -0
  20. package/dist/cjs/node/native-loader.cjs +7 -0
  21. package/dist/cjs/node/native-loader.d.cts +11 -0
  22. package/dist/cjs/node/native-udp.d.ts +8 -3
  23. package/dist/cjs/node/native-udp.js +8 -3
  24. package/dist/cjs/node/nhp-wire.d.ts +9 -0
  25. package/dist/cjs/node/nhp-wire.js +24 -7
  26. package/dist/cjs/node/portal-opener.d.ts +7 -0
  27. package/dist/cjs/node/portal-opener.js +28 -6
  28. package/dist/cjs/node/provider.d.ts +19 -0
  29. package/dist/cjs/node/provider.js +153 -0
  30. package/dist/cjs/node/qv2.d.ts +3 -1
  31. package/dist/cjs/node/qv2.js +16 -2
  32. package/dist/cjs/node/relay.d.ts +12 -0
  33. package/dist/cjs/node/relay.js +111 -0
  34. package/dist/cjs/node/sealed-agent-state.d.ts +19 -0
  35. package/dist/cjs/node/sealed-agent-state.js +193 -0
  36. package/dist/cjs/node/strict-json.d.ts +1 -1
  37. package/dist/cjs/node/strict-json.js +8 -3
  38. package/dist/cjs/node.d.ts +14 -0
  39. package/dist/cjs/node.js +24 -1
  40. package/dist/cjs/types.d.ts +97 -0
  41. package/dist/cjs/version.d.ts +1 -1
  42. package/dist/cjs/version.js +1 -1
  43. package/dist/esm/client.d.ts +19 -1
  44. package/dist/esm/client.d.ts.map +1 -1
  45. package/dist/esm/client.js +521 -10
  46. package/dist/esm/client.js.map +1 -1
  47. package/dist/esm/crid.d.ts +2 -1
  48. package/dist/esm/crid.d.ts.map +1 -1
  49. package/dist/esm/crid.js +10 -2
  50. package/dist/esm/crid.js.map +1 -1
  51. package/dist/esm/errors.d.ts +20 -3
  52. package/dist/esm/errors.d.ts.map +1 -1
  53. package/dist/esm/errors.js +29 -3
  54. package/dist/esm/errors.js.map +1 -1
  55. package/dist/esm/index.d.ts +2 -2
  56. package/dist/esm/index.d.ts.map +1 -1
  57. package/dist/esm/index.js +1 -1
  58. package/dist/esm/index.js.map +1 -1
  59. package/dist/esm/node/agent-runtime.d.ts +109 -0
  60. package/dist/esm/node/agent-runtime.d.ts.map +1 -0
  61. package/dist/esm/node/agent-runtime.js +946 -0
  62. package/dist/esm/node/agent-runtime.js.map +1 -0
  63. package/dist/esm/node/agent-state.d.ts +101 -0
  64. package/dist/esm/node/agent-state.d.ts.map +1 -0
  65. package/dist/esm/node/agent-state.js +298 -0
  66. package/dist/esm/node/agent-state.js.map +1 -0
  67. package/dist/esm/node/agent-transport.d.ts +18 -0
  68. package/dist/esm/node/agent-transport.d.ts.map +1 -0
  69. package/dist/esm/node/agent-transport.js +114 -0
  70. package/dist/esm/node/agent-transport.js.map +1 -0
  71. package/dist/esm/node/deployment.d.ts +6 -0
  72. package/dist/esm/node/deployment.d.ts.map +1 -1
  73. package/dist/esm/node/deployment.js +23 -3
  74. package/dist/esm/node/deployment.js.map +1 -1
  75. package/dist/esm/node/file-agent-state.d.ts +16 -0
  76. package/dist/esm/node/file-agent-state.d.ts.map +1 -0
  77. package/dist/esm/node/file-agent-state.js +134 -0
  78. package/dist/esm/node/file-agent-state.js.map +1 -0
  79. package/dist/esm/node/native-loader.cjs +7 -0
  80. package/dist/esm/node/native-loader.d.cts +11 -0
  81. package/dist/esm/node/native-udp.d.ts +8 -3
  82. package/dist/esm/node/native-udp.d.ts.map +1 -1
  83. package/dist/esm/node/native-udp.js +7 -5
  84. package/dist/esm/node/native-udp.js.map +1 -1
  85. package/dist/esm/node/nhp-wire.d.ts +9 -0
  86. package/dist/esm/node/nhp-wire.d.ts.map +1 -1
  87. package/dist/esm/node/nhp-wire.js +23 -6
  88. package/dist/esm/node/nhp-wire.js.map +1 -1
  89. package/dist/esm/node/portal-opener.d.ts +7 -0
  90. package/dist/esm/node/portal-opener.d.ts.map +1 -1
  91. package/dist/esm/node/portal-opener.js +28 -6
  92. package/dist/esm/node/portal-opener.js.map +1 -1
  93. package/dist/esm/node/provider.d.ts +20 -0
  94. package/dist/esm/node/provider.d.ts.map +1 -0
  95. package/dist/esm/node/provider.js +149 -0
  96. package/dist/esm/node/provider.js.map +1 -0
  97. package/dist/esm/node/qv2.d.ts +3 -1
  98. package/dist/esm/node/qv2.d.ts.map +1 -1
  99. package/dist/esm/node/qv2.js +17 -3
  100. package/dist/esm/node/qv2.js.map +1 -1
  101. package/dist/esm/node/relay.d.ts +13 -0
  102. package/dist/esm/node/relay.d.ts.map +1 -0
  103. package/dist/esm/node/relay.js +105 -0
  104. package/dist/esm/node/relay.js.map +1 -0
  105. package/dist/esm/node/sealed-agent-state.d.ts +20 -0
  106. package/dist/esm/node/sealed-agent-state.d.ts.map +1 -0
  107. package/dist/esm/node/sealed-agent-state.js +190 -0
  108. package/dist/esm/node/sealed-agent-state.js.map +1 -0
  109. package/dist/esm/node/strict-json.d.ts +1 -1
  110. package/dist/esm/node/strict-json.d.ts.map +1 -1
  111. package/dist/esm/node/strict-json.js +8 -3
  112. package/dist/esm/node/strict-json.js.map +1 -1
  113. package/dist/esm/node.d.ts +14 -0
  114. package/dist/esm/node.d.ts.map +1 -1
  115. package/dist/esm/node.js +7 -0
  116. package/dist/esm/node.js.map +1 -1
  117. package/dist/esm/types.d.ts +97 -0
  118. package/dist/esm/types.d.ts.map +1 -1
  119. package/dist/esm/version.d.ts +1 -1
  120. package/dist/esm/version.js +1 -1
  121. package/package.json +16 -7
@@ -7,6 +7,7 @@ exports.x25519PublicFromPrivate = x25519PublicFromPrivate;
7
7
  exports.strictBase64Url = strictBase64Url;
8
8
  const node_crypto_1 = require("node:crypto");
9
9
  const strict_json_js_1 = require("./strict-json.js");
10
+ const crid_js_1 = require("../crid.js");
10
11
  const TRANSPORT_PREFIX = "qv2t1";
11
12
  const FRAGMENT_PREFIX = "qv2";
12
13
  const TRANSPORT_MAX_LENGTH = 6_826;
@@ -30,7 +31,7 @@ const CLAIM_KEYS = new Set([
30
31
  "qurl_user_public_key_b64",
31
32
  ]);
32
33
  const REQUIRED_CLAIM_KEYS = [...CLAIM_KEYS].filter((key) => key !== "cell_id");
33
- function verifyQv2Link(qurl, issuers) {
34
+ function verifyQv2Link(qurl, issuers, expectedCRID) {
34
35
  const hash = qurl.indexOf("#");
35
36
  if (hash < 0)
36
37
  throw new Error("qURL link has no credential fragment");
@@ -45,6 +46,18 @@ function verifyQv2Link(qurl, issuers) {
45
46
  const claims = parseClaims(decodeCanonicalBase64Url(claimsB64));
46
47
  const secret = parseAndWipeSecret(decodeCanonicalBase64Url(secretB64));
47
48
  verifyIssuerClaims(claimsB64, signatureB64, issuers, claims);
49
+ if (expectedCRID !== undefined) {
50
+ const expected = (0, crid_js_1.parseCrid)(expectedCRID, true);
51
+ if (!expected)
52
+ throw new Error("invalid or unsupported expected CRID");
53
+ // The native verifier is synchronous; the portable helper uses WebCrypto.
54
+ const digest = (0, node_crypto_1.createHash)("sha256")
55
+ .update(crid_js_1.CRID_DOMAIN)
56
+ .update(decodeCanonicalBase64Url(claims.resourcePublicKeyB64))
57
+ .digest();
58
+ if (!(0, node_crypto_1.timingSafeEqual)(expected, digest.subarray(0, expected.length)))
59
+ throw new Error("qURL resource key does not match the expected CRID");
60
+ }
48
61
  const privateKey = decodeCanonicalBase64Url(secret.qurlUserPrivateKeyB64);
49
62
  let retainPrivateKey = false;
50
63
  try {
@@ -63,6 +76,7 @@ function verifyQv2Link(qurl, issuers) {
63
76
  kid: claims.kid,
64
77
  cellPublicKey: decodeX25519(claims.cellPublicKeyB64, "cell public key"),
65
78
  resourcePublicKeyB64: claims.resourcePublicKeyB64,
79
+ relayUrl: claims.relayUrl,
66
80
  },
67
81
  devicePrivateKey: privateKey,
68
82
  };
@@ -207,7 +221,6 @@ function parseClaims(raw) {
207
221
  if (resourcePublic.byteLength < 80 || resourcePublic.byteLength > 160) {
208
222
  throw new Error("resource public key has an invalid length");
209
223
  }
210
- requireNonEmptyString(value.relay_url, "relay URL");
211
224
  // cell_id is descriptive deployment metadata, not a security identity. The
212
225
  // signed 32-byte cell public key selects and then fully binds the UDP peer.
213
226
  if ("cell_id" in value && typeof value.cell_id !== "string") {
@@ -218,6 +231,7 @@ function parseClaims(raw) {
218
231
  cellPublicKeyB64,
219
232
  resourcePublicKeyB64,
220
233
  qurlUserPublicKeyB64: qurlPublic,
234
+ relayUrl: requireNonEmptyString(value.relay_url, "relay URL"),
221
235
  };
222
236
  }
223
237
  function verifyIssuerClaims(claimsB64, signatureB64, issuers, parsedClaims) {
@@ -0,0 +1,12 @@
1
+ import { type NHPMessage } from "./nhp-wire.js";
2
+ export declare class RelayError extends Error {
3
+ readonly status: number;
4
+ constructor(status: number, message: string, options?: ErrorOptions);
5
+ }
6
+ /** Call only after verifying the issuer signature over the relay URL. */
7
+ export declare function validateRelayURL(value: string, allowlist: readonly string[]): URL;
8
+ export declare function readBoundedBody(response: Response, limit: number): Promise<Uint8Array>;
9
+ export declare function relayKnock(relayURL: string, allowlist: readonly string[], serverPublicKey: Uint8Array, devicePrivateKey: Uint8Array, body: Uint8Array, options?: {
10
+ signal?: AbortSignal;
11
+ fetch?: typeof globalThis.fetch;
12
+ }): Promise<NHPMessage>;
@@ -0,0 +1,111 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RelayError = void 0;
4
+ exports.validateRelayURL = validateRelayURL;
5
+ exports.readBoundedBody = readBoundedBody;
6
+ exports.relayKnock = relayKnock;
7
+ const deployment_js_1 = require("./deployment.js");
8
+ const nhp_wire_js_1 = require("./nhp-wire.js");
9
+ class RelayError extends Error {
10
+ status;
11
+ constructor(status, message, options) {
12
+ super(message, options);
13
+ this.status = status;
14
+ this.name = "RelayError";
15
+ }
16
+ }
17
+ exports.RelayError = RelayError;
18
+ /** Call only after verifying the issuer signature over the relay URL. */
19
+ function validateRelayURL(value, allowlist) {
20
+ const url = new URL(value);
21
+ if (url.protocol !== "https:" ||
22
+ url.username ||
23
+ url.password ||
24
+ !url.hostname ||
25
+ !allowlist.some((entry) => {
26
+ const host = entry.trim().toLowerCase();
27
+ return (host === url.host.toLowerCase() ||
28
+ host === url.hostname.toLowerCase() ||
29
+ (url.port === "" && host === `${url.hostname.toLowerCase()}:443`));
30
+ }))
31
+ throw new Error("qURL relay URL is not permitted by deployment trust");
32
+ if (url.search || url.hash)
33
+ throw new Error("qURL relay base URL must not contain a query or fragment");
34
+ return url;
35
+ }
36
+ async function readBoundedBody(response, limit) {
37
+ if (!response.body)
38
+ return new Uint8Array();
39
+ const reader = response.body.getReader();
40
+ const chunks = [];
41
+ let length = 0;
42
+ try {
43
+ for (;;) {
44
+ const { done, value } = await reader.read();
45
+ if (done)
46
+ return Buffer.concat(chunks, length);
47
+ length += value.byteLength;
48
+ if (length > limit)
49
+ throw new Error("qURL HTTP response exceeds its size limit");
50
+ chunks.push(value);
51
+ }
52
+ }
53
+ finally {
54
+ await reader.cancel().catch(() => undefined);
55
+ reader.releaseLock();
56
+ }
57
+ }
58
+ async function relayKnock(relayURL, allowlist, serverPublicKey, devicePrivateKey, body, options = {}) {
59
+ let base;
60
+ try {
61
+ base = validateRelayURL(relayURL, allowlist);
62
+ }
63
+ catch (cause) {
64
+ throw new RelayError(0, "qURL relay URL is invalid or not trusted", { cause });
65
+ }
66
+ options.signal?.throwIfAborted();
67
+ const built = (0, nhp_wire_js_1.buildNHPMessage)({ type: nhp_wire_js_1.NHP_TYPE_KNOCK, serverPublicKey, devicePrivateKey, body });
68
+ let packet;
69
+ try {
70
+ const url = `${base.href.replace(/\/+$/, "")}/relay/${(0, deployment_js_1.fingerprintKey)(serverPublicKey)}`;
71
+ let response;
72
+ try {
73
+ response = await (options.fetch ?? globalThis.fetch)(url, {
74
+ method: "POST",
75
+ headers: { "Content-Type": "application/octet-stream" },
76
+ body: Buffer.from(built.packet),
77
+ redirect: "error",
78
+ signal: options.signal
79
+ ? AbortSignal.any([options.signal, AbortSignal.timeout(15_000)])
80
+ : AbortSignal.timeout(15_000),
81
+ });
82
+ }
83
+ catch (cause) {
84
+ options.signal?.throwIfAborted();
85
+ throw new RelayError(0, "qURL relay transport failed", { cause });
86
+ }
87
+ if (response.status !== 200) {
88
+ await response.body?.cancel().catch(() => undefined);
89
+ throw new RelayError(response.status, "qURL relay returned an unexpected HTTP status");
90
+ }
91
+ try {
92
+ packet = await readBoundedBody(response, nhp_wire_js_1.NHP_PACKET_SIZE);
93
+ const reply = (0, nhp_wire_js_1.decryptNHPReply)(devicePrivateKey, serverPublicKey, packet);
94
+ if (reply.type === nhp_wire_js_1.NHP_TYPE_COOKIE)
95
+ return reply;
96
+ if (reply.type !== nhp_wire_js_1.NHP_TYPE_ACK || reply.counter !== built.counter) {
97
+ reply.body.fill(0);
98
+ throw new Error("NHP relay reply does not match the request");
99
+ }
100
+ return reply;
101
+ }
102
+ catch (cause) {
103
+ options.signal?.throwIfAborted();
104
+ throw new RelayError(200, "qURL relay reply is invalid", { cause });
105
+ }
106
+ }
107
+ finally {
108
+ built.packet.fill(0);
109
+ packet?.fill(0);
110
+ }
111
+ }
@@ -0,0 +1,19 @@
1
+ import { FileAgentState, type AgentStateCodec } from "./file-agent-state.js";
2
+ import { type StrictJsonValue } from "./strict-json.js";
3
+ export interface AgentStateKeyBinding {
4
+ purpose: string;
5
+ envelopeVersion: number;
6
+ providerID: string;
7
+ agentID: string;
8
+ }
9
+ export interface WrappedAgentStateKey {
10
+ version: number;
11
+ ciphertext: Uint8Array;
12
+ metadata?: StrictJsonValue;
13
+ }
14
+ export interface AgentStateKeyWrapper {
15
+ wrapKey(key: Uint8Array, binding: AgentStateKeyBinding, signal?: AbortSignal): Promise<WrappedAgentStateKey>;
16
+ unwrapKey(wrapped: WrappedAgentStateKey, binding: AgentStateKeyBinding, signal?: AbortSignal): Promise<Uint8Array>;
17
+ }
18
+ export declare function createSealedAgentStateCodec(providerID: string, wrapper: AgentStateKeyWrapper, expectedAgentID?: string): AgentStateCodec;
19
+ export declare function openSealedFileAgentState(path: string, providerID: string, wrapper: AgentStateKeyWrapper, expectedAgentID?: string): FileAgentState;
@@ -0,0 +1,193 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createSealedAgentStateCodec = createSealedAgentStateCodec;
4
+ exports.openSealedFileAgentState = openSealedFileAgentState;
5
+ const node_crypto_1 = require("node:crypto");
6
+ const agent_state_js_1 = require("./agent-state.js");
7
+ const file_agent_state_js_1 = require("./file-agent-state.js");
8
+ const strict_json_js_1 = require("./strict-json.js");
9
+ const PURPOSE = "qurl-go/agent-state";
10
+ function base64(value, maximum) {
11
+ if (typeof value !== "string")
12
+ throw new agent_state_js_1.AgentStateError("INVALID_ENVELOPE");
13
+ const bytes = Buffer.from(value, "base64");
14
+ if (bytes.toString("base64") !== value || bytes.length === 0 || bytes.length > maximum)
15
+ throw new agent_state_js_1.AgentStateError("INVALID_ENVELOPE");
16
+ return bytes;
17
+ }
18
+ // Go authenticates compact RawMessage bytes: keep key order and number lexemes.
19
+ function compactMetadata(raw) {
20
+ return raw.replace(/"(?:\\.|[^"\\])*"|\s+/g, (token) => token.startsWith('"')
21
+ ? token.replace(/[<>&\u2028\u2029]/g, (ch) => `\\u${ch.charCodeAt(0).toString(16).padStart(4, "0")}`)
22
+ : "");
23
+ }
24
+ function wrappedWire(wrapped) {
25
+ if (!Number.isSafeInteger(wrapped.version) ||
26
+ wrapped.version < 1 ||
27
+ !(wrapped.ciphertext instanceof Uint8Array) ||
28
+ wrapped.ciphertext.length === 0 ||
29
+ wrapped.ciphertext.length > 65536)
30
+ throw new agent_state_js_1.AgentStateError("INVALID_WRAPPED_KEY");
31
+ const metadata = wrapped.metadata === undefined
32
+ ? undefined
33
+ : (0, strict_json_js_1.parseStrictJson)((0, agent_state_js_1.encodeAgentJSON)(wrapped.metadata), 16384);
34
+ return {
35
+ version: wrapped.version,
36
+ ciphertext: Buffer.from(wrapped.ciphertext).toString("base64"),
37
+ ...(metadata === undefined ? {} : { metadata }),
38
+ };
39
+ }
40
+ function aad(binding, wrapped, metadataRaw) {
41
+ const raw = (0, agent_state_js_1.encodeAgentJSON)({
42
+ purpose: binding.purpose,
43
+ envelope_version: binding.envelopeVersion,
44
+ provider_id: binding.providerID,
45
+ agent_id: binding.agentID,
46
+ wrapped_key: { version: wrapped.version, ciphertext: wrapped.ciphertext },
47
+ });
48
+ const metadata = metadataRaw === undefined
49
+ ? wrapped.metadata === undefined
50
+ ? undefined
51
+ : (0, agent_state_js_1.encodeAgentJSON)(wrapped.metadata).toString()
52
+ : compactMetadata(metadataRaw);
53
+ // wrapped_key is the final field; raw ends with its closing brace and the envelope brace.
54
+ return metadata === undefined
55
+ ? raw
56
+ : Buffer.concat([raw.subarray(0, -2), Buffer.from(',"metadata":' + metadata + "}}")]);
57
+ }
58
+ function decrypt(key, nonce, ciphertext, associated) {
59
+ if (key.length !== 32 || nonce.length !== 12 || ciphertext.length < 16)
60
+ throw new agent_state_js_1.AgentStateError("INVALID_ENVELOPE");
61
+ const cipher = (0, node_crypto_1.createDecipheriv)("aes-256-gcm", key, nonce);
62
+ cipher.setAAD(associated);
63
+ cipher.setAuthTag(ciphertext.subarray(-16));
64
+ let plain;
65
+ try {
66
+ plain = cipher.update(ciphertext.subarray(0, -16));
67
+ const final = cipher.final();
68
+ const result = Buffer.concat([plain, final]);
69
+ final.fill(0);
70
+ return result;
71
+ }
72
+ catch (cause) {
73
+ throw new agent_state_js_1.AgentStateError("INVALID_ENVELOPE", { cause });
74
+ }
75
+ finally {
76
+ plain?.fill(0);
77
+ }
78
+ }
79
+ function createSealedAgentStateCodec(providerID, wrapper, expectedAgentID) {
80
+ if (!/^[a-z][a-z0-9]*([.-][a-z0-9]+)*$/.test(providerID) ||
81
+ providerID.length > 64 ||
82
+ !wrapper ||
83
+ typeof wrapper.wrapKey !== "function" ||
84
+ typeof wrapper.unwrapKey !== "function")
85
+ throw new agent_state_js_1.AgentStateError("INVALID_WRAPPER_CONFIG");
86
+ const bindingFor = (agentID) => {
87
+ if (typeof agentID !== "string" ||
88
+ !agentID ||
89
+ agentID.trim() !== agentID ||
90
+ Buffer.byteLength(agentID) > 256 ||
91
+ [...agentID].some((ch) => ch.charCodeAt(0) < 32 || ch.charCodeAt(0) === 127) ||
92
+ (expectedAgentID !== undefined && expectedAgentID !== agentID))
93
+ throw new agent_state_js_1.AgentStateError("ENVELOPE_IDENTITY_MISMATCH");
94
+ return { purpose: PURPOSE, envelopeVersion: 1, providerID, agentID };
95
+ };
96
+ if (expectedAgentID !== undefined)
97
+ bindingFor(expectedAgentID);
98
+ return {
99
+ async encode(state, signal) {
100
+ signal?.throwIfAborted();
101
+ const raw = (0, agent_state_js_1.encodeAgentState)(state);
102
+ const key = (0, node_crypto_1.randomBytes)(32);
103
+ const copy = Buffer.from(key);
104
+ try {
105
+ const binding = bindingFor(state.agent_id);
106
+ const wrapped = wrappedWire(await wrapper.wrapKey(copy, { ...binding }, signal));
107
+ signal?.throwIfAborted();
108
+ const nonce = (0, node_crypto_1.randomBytes)(12);
109
+ const associated = aad(binding, wrapped);
110
+ const cipher = (0, node_crypto_1.createCipheriv)("aes-256-gcm", key, nonce);
111
+ cipher.setAAD(associated);
112
+ const ciphertext = Buffer.concat([cipher.update(raw), cipher.final(), cipher.getAuthTag()]);
113
+ const recovered = await wrapper.unwrapKey({
114
+ version: wrapped.version,
115
+ ciphertext: Buffer.from(wrapped.ciphertext, "base64"),
116
+ ...(wrapped.metadata === undefined ? {} : { metadata: wrapped.metadata }),
117
+ }, { ...binding }, signal);
118
+ let check;
119
+ try {
120
+ signal?.throwIfAborted();
121
+ check = decrypt(recovered, nonce, ciphertext, associated);
122
+ if (!check.equals(raw))
123
+ throw new agent_state_js_1.AgentStateError("ENVELOPE_ROUNDTRIP");
124
+ }
125
+ finally {
126
+ check?.fill(0);
127
+ recovered.fill(0);
128
+ }
129
+ const envelope = (0, agent_state_js_1.encodeAgentJSON)({
130
+ version: 1,
131
+ purpose: PURPOSE,
132
+ agent_id: binding.agentID,
133
+ provider_id: providerID,
134
+ wrapped_key: wrapped,
135
+ nonce: nonce.toString("base64"),
136
+ ciphertext: ciphertext.toString("base64"),
137
+ });
138
+ (0, strict_json_js_1.parseStrictJson)(envelope, 2 << 20);
139
+ return envelope;
140
+ }
141
+ finally {
142
+ raw.fill(0);
143
+ key.fill(0);
144
+ copy.fill(0);
145
+ }
146
+ },
147
+ async decode(raw, signal) {
148
+ signal?.throwIfAborted();
149
+ const metadataMembers = new Map();
150
+ const envelope = (0, agent_state_js_1.exactObject)((0, strict_json_js_1.parseStrictJson)(raw, 2 << 20, (object, key, value) => {
151
+ if (key === "metadata")
152
+ metadataMembers.set(object, value);
153
+ }), "version purpose agent_id provider_id wrapped_key nonce ciphertext");
154
+ if (envelope.version !== 1n ||
155
+ envelope.purpose !== PURPOSE ||
156
+ envelope.provider_id !== providerID)
157
+ throw new agent_state_js_1.AgentStateError("INVALID_ENVELOPE");
158
+ const binding = bindingFor(envelope.agent_id);
159
+ const record = (0, agent_state_js_1.exactObject)(envelope.wrapped_key, "version ciphertext metadata");
160
+ if (typeof record.version !== "bigint" ||
161
+ record.version < 1n ||
162
+ record.version > BigInt(Number.MAX_SAFE_INTEGER))
163
+ throw new agent_state_js_1.AgentStateError("INVALID_WRAPPED_KEY");
164
+ const wrapped = {
165
+ version: Number(record.version),
166
+ ciphertext: base64(record.ciphertext, 65536),
167
+ ...(record.metadata === undefined ? {} : { metadata: record.metadata }),
168
+ };
169
+ const wire = wrappedWire(wrapped);
170
+ const nonce = base64(envelope.nonce, 12);
171
+ const ciphertext = base64(envelope.ciphertext, (1 << 20) + 16);
172
+ if (nonce.length !== 12 || ciphertext.length < 16)
173
+ throw new agent_state_js_1.AgentStateError("INVALID_ENVELOPE");
174
+ const key = await wrapper.unwrapKey(wrapped, { ...binding }, signal);
175
+ let plaintext;
176
+ try {
177
+ signal?.throwIfAborted();
178
+ plaintext = decrypt(key, nonce, ciphertext, aad(binding, wire, metadataMembers.get(record)));
179
+ const state = (0, agent_state_js_1.decodeAgentState)(plaintext);
180
+ if (state.agent_id !== binding.agentID)
181
+ throw new agent_state_js_1.AgentStateError("ENVELOPE_IDENTITY_MISMATCH");
182
+ return state;
183
+ }
184
+ finally {
185
+ key.fill(0);
186
+ plaintext?.fill(0);
187
+ }
188
+ },
189
+ };
190
+ }
191
+ function openSealedFileAgentState(path, providerID, wrapper, expectedAgentID) {
192
+ return new file_agent_state_js_1.FileAgentState(path, createSealedAgentStateCodec(providerID, wrapper, expectedAgentID));
193
+ }
@@ -6,7 +6,7 @@ export type StrictJsonValue = null | boolean | number | bigint | string | Strict
6
6
  * integer ambiguity. NHP uses uint64 session identifiers, so integral values
7
7
  * outside Number's safe range remain bigint instead of losing wire bytes.
8
8
  */
9
- export declare function parseStrictJson(input: Uint8Array, maxBytes: number): StrictJsonValue;
9
+ export declare function parseStrictJson(input: Uint8Array, maxBytes: number, onMember?: (object: object, key: string, raw: string) => void): StrictJsonValue;
10
10
  export declare function isStrictJsonObject(value: StrictJsonValue | undefined): value is {
11
11
  [key: string]: StrictJsonValue;
12
12
  };
@@ -9,11 +9,11 @@ const JSON_NUMBER = /-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?/y;
9
9
  * integer ambiguity. NHP uses uint64 session identifiers, so integral values
10
10
  * outside Number's safe range remain bigint instead of losing wire bytes.
11
11
  */
12
- function parseStrictJson(input, maxBytes) {
12
+ function parseStrictJson(input, maxBytes, onMember) {
13
13
  if (input.byteLength > maxBytes)
14
14
  throw new Error(`JSON exceeds ${maxBytes}-byte limit`);
15
15
  const text = new TextDecoder("utf-8", { fatal: true }).decode(input);
16
- const parser = new StrictJsonParser(text);
16
+ const parser = new StrictJsonParser(text, onMember);
17
17
  const value = parser.parseValue(0);
18
18
  parser.skipWhitespace();
19
19
  if (!parser.done)
@@ -22,9 +22,11 @@ function parseStrictJson(input, maxBytes) {
22
22
  }
23
23
  class StrictJsonParser {
24
24
  input;
25
+ onMember;
25
26
  #offset = 0;
26
- constructor(input) {
27
+ constructor(input, onMember) {
27
28
  this.input = input;
29
+ this.onMember = onMember;
28
30
  }
29
31
  get done() {
30
32
  return this.#offset === this.input.length;
@@ -77,7 +79,10 @@ class StrictJsonParser {
77
79
  this.skipWhitespace();
78
80
  if (!this.consume(":"))
79
81
  throw new Error("JSON object key has no value");
82
+ this.skipWhitespace();
83
+ const start = this.#offset;
80
84
  value[key] = this.parseValue(depth);
85
+ this.onMember?.(value, key, this.input.slice(start, this.#offset));
81
86
  this.skipWhitespace();
82
87
  if (this.consume("}"))
83
88
  return value;
@@ -1,3 +1,17 @@
1
1
  export { createPortalOpener, PortalBusyError, PortalConfigurationError, PortalDenyError, PortalInvalidReplyError, PortalOpenerClosedError, PortalOpenerNotReadyError, PortalOpenerNotStartedError, PortalOpenTimeoutError, PortalRedirectError, PortalStateError, PortalTargetChangedError, PortalTooManyRedirectsError, PortalVerificationError, } from "./node/portal-opener.js";
2
2
  export type { CreatePortalOpenerOptions, PortalFetchOptions, PortalOpener, PortalOpenerFailureClass, PortalOpenerHealth, PortalOpenerState, PortalRequestBuilder, PortalStartOptions, } from "./node/portal-opener.js";
3
3
  export type { PortalDeployment, PortalDeploymentCell, PortalDeploymentIssuer, } from "./node/deployment.js";
4
+ export { RelayError, validateRelayURL } from "./node/relay.js";
5
+ export { createStaticProvider, createDiscoveryProvider, createHTTPManifestFetcher, } from "./node/provider.js";
6
+ export type { PortalProvider, DiscoveryProviderOptions } from "./node/provider.js";
7
+ export { AgentStateError, decodeAgentState, encodeAgentState } from "./node/agent-state.js";
8
+ export type { AgentState, AgentAssignment, NHPUDPEndpoint, AgentStateStore, AssignmentRegistration, PendingAgentActivation, PendingAgentCompletion, PendingAgentCredentialRecovery, PendingAgentCredentialRecoveryIssue, } from "./node/agent-state.js";
9
+ export type { AgentStateCodec } from "./node/file-agent-state.js";
10
+ export type { AgentExchange, AgentTransport } from "./node/agent-transport.js";
11
+ export { AgentTransportError } from "./node/agent-transport.js";
12
+ export type { NHPMessage } from "./node/nhp-wire.js";
13
+ export { FileAgentState } from "./node/file-agent-state.js";
14
+ export { createSealedAgentStateCodec, openSealedFileAgentState, } from "./node/sealed-agent-state.js";
15
+ export type { AgentStateKeyBinding, WrappedAgentStateKey, AgentStateKeyWrapper, } from "./node/sealed-agent-state.js";
16
+ export { AgentRuntime, AgentLifecycleError, connectAgentRuntime, recoverAgentRuntime, } from "./node/agent-runtime.js";
17
+ export type { AgentRuntimeOptions, AgentRecoveryOptions, AgentOTPChallenge, NativeAgentKnockOptions, NativeAgentGrant, NativeSessionReceipt, } from "./node/agent-runtime.js";
package/dist/cjs/node.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.PortalVerificationError = exports.PortalTooManyRedirectsError = exports.PortalTargetChangedError = exports.PortalStateError = exports.PortalRedirectError = exports.PortalOpenTimeoutError = exports.PortalOpenerNotStartedError = exports.PortalOpenerNotReadyError = exports.PortalOpenerClosedError = exports.PortalInvalidReplyError = exports.PortalDenyError = exports.PortalConfigurationError = exports.PortalBusyError = exports.createPortalOpener = void 0;
3
+ exports.recoverAgentRuntime = exports.connectAgentRuntime = exports.AgentLifecycleError = exports.AgentRuntime = exports.openSealedFileAgentState = exports.createSealedAgentStateCodec = exports.FileAgentState = exports.AgentTransportError = exports.encodeAgentState = exports.decodeAgentState = exports.AgentStateError = exports.createHTTPManifestFetcher = exports.createDiscoveryProvider = exports.createStaticProvider = exports.validateRelayURL = exports.RelayError = exports.PortalVerificationError = exports.PortalTooManyRedirectsError = exports.PortalTargetChangedError = exports.PortalStateError = exports.PortalRedirectError = exports.PortalOpenTimeoutError = exports.PortalOpenerNotStartedError = exports.PortalOpenerNotReadyError = exports.PortalOpenerClosedError = exports.PortalInvalidReplyError = exports.PortalDenyError = exports.PortalConfigurationError = exports.PortalBusyError = exports.createPortalOpener = void 0;
4
4
  var portal_opener_js_1 = require("./node/portal-opener.js");
5
5
  Object.defineProperty(exports, "createPortalOpener", { enumerable: true, get: function () { return portal_opener_js_1.createPortalOpener; } });
6
6
  Object.defineProperty(exports, "PortalBusyError", { enumerable: true, get: function () { return portal_opener_js_1.PortalBusyError; } });
@@ -16,3 +16,26 @@ Object.defineProperty(exports, "PortalStateError", { enumerable: true, get: func
16
16
  Object.defineProperty(exports, "PortalTargetChangedError", { enumerable: true, get: function () { return portal_opener_js_1.PortalTargetChangedError; } });
17
17
  Object.defineProperty(exports, "PortalTooManyRedirectsError", { enumerable: true, get: function () { return portal_opener_js_1.PortalTooManyRedirectsError; } });
18
18
  Object.defineProperty(exports, "PortalVerificationError", { enumerable: true, get: function () { return portal_opener_js_1.PortalVerificationError; } });
19
+ var relay_js_1 = require("./node/relay.js");
20
+ Object.defineProperty(exports, "RelayError", { enumerable: true, get: function () { return relay_js_1.RelayError; } });
21
+ Object.defineProperty(exports, "validateRelayURL", { enumerable: true, get: function () { return relay_js_1.validateRelayURL; } });
22
+ var provider_js_1 = require("./node/provider.js");
23
+ Object.defineProperty(exports, "createStaticProvider", { enumerable: true, get: function () { return provider_js_1.createStaticProvider; } });
24
+ Object.defineProperty(exports, "createDiscoveryProvider", { enumerable: true, get: function () { return provider_js_1.createDiscoveryProvider; } });
25
+ Object.defineProperty(exports, "createHTTPManifestFetcher", { enumerable: true, get: function () { return provider_js_1.createHTTPManifestFetcher; } });
26
+ var agent_state_js_1 = require("./node/agent-state.js");
27
+ Object.defineProperty(exports, "AgentStateError", { enumerable: true, get: function () { return agent_state_js_1.AgentStateError; } });
28
+ Object.defineProperty(exports, "decodeAgentState", { enumerable: true, get: function () { return agent_state_js_1.decodeAgentState; } });
29
+ Object.defineProperty(exports, "encodeAgentState", { enumerable: true, get: function () { return agent_state_js_1.encodeAgentState; } });
30
+ var agent_transport_js_1 = require("./node/agent-transport.js");
31
+ Object.defineProperty(exports, "AgentTransportError", { enumerable: true, get: function () { return agent_transport_js_1.AgentTransportError; } });
32
+ var file_agent_state_js_1 = require("./node/file-agent-state.js");
33
+ Object.defineProperty(exports, "FileAgentState", { enumerable: true, get: function () { return file_agent_state_js_1.FileAgentState; } });
34
+ var sealed_agent_state_js_1 = require("./node/sealed-agent-state.js");
35
+ Object.defineProperty(exports, "createSealedAgentStateCodec", { enumerable: true, get: function () { return sealed_agent_state_js_1.createSealedAgentStateCodec; } });
36
+ Object.defineProperty(exports, "openSealedFileAgentState", { enumerable: true, get: function () { return sealed_agent_state_js_1.openSealedFileAgentState; } });
37
+ var agent_runtime_js_1 = require("./node/agent-runtime.js");
38
+ Object.defineProperty(exports, "AgentRuntime", { enumerable: true, get: function () { return agent_runtime_js_1.AgentRuntime; } });
39
+ Object.defineProperty(exports, "AgentLifecycleError", { enumerable: true, get: function () { return agent_runtime_js_1.AgentLifecycleError; } });
40
+ Object.defineProperty(exports, "connectAgentRuntime", { enumerable: true, get: function () { return agent_runtime_js_1.connectAgentRuntime; } });
41
+ Object.defineProperty(exports, "recoverAgentRuntime", { enumerable: true, get: function () { return agent_runtime_js_1.recoverAgentRuntime; } });
@@ -412,6 +412,103 @@ export interface BatchCreateOutput {
412
412
  results: BatchItemResult[];
413
413
  request_id?: string;
414
414
  }
415
+ /** One recipient grant redeemed from an opaque delegated mint capability. */
416
+ export interface DelegatedQurlGrant {
417
+ expires_in?: string;
418
+ label?: string;
419
+ one_time_use?: boolean;
420
+ /** Zero means unlimited. */
421
+ max_sessions?: number;
422
+ session_duration?: string;
423
+ access_policy?: AccessPolicy;
424
+ }
425
+ /** Input for one durable asynchronous delegated qURL batch. */
426
+ export interface CreateDelegatedQurlBatchInput {
427
+ mint_capability: string;
428
+ /** One through 100 independent grants, preserved in input order. */
429
+ grants: DelegatedQurlGrant[];
430
+ }
431
+ /** A stable caller key is required because the mutation is never retried automatically. */
432
+ export interface CreateDelegatedQurlBatchOptions {
433
+ idempotencyKey: string;
434
+ }
435
+ /** Optional strong validator from an earlier delegated batch response. */
436
+ export interface GetDelegatedQurlBatchOptions {
437
+ etag?: string;
438
+ }
439
+ export type DelegatedQurlBatchStatus = "queued" | "running" | "succeeded" | "partially_failed" | "failed";
440
+ interface DelegatedQurlBatchBase {
441
+ batch_id: string;
442
+ item_count: number;
443
+ submitted_at: string;
444
+ /** Strong validator when the response provides one. */
445
+ etag?: string;
446
+ request_id: string;
447
+ }
448
+ /** Exact HTTP 202 result from delegated batch creation. */
449
+ export interface DelegatedQurlBatchAccepted extends DelegatedQurlBatchBase {
450
+ http_status: 202;
451
+ status: "queued";
452
+ etag?: string;
453
+ location: string;
454
+ /** Positive Retry-After delta in seconds. */
455
+ retry_after?: number;
456
+ }
457
+ export interface DelegatedQurlCreatedData {
458
+ qurl_id: string;
459
+ /** Fresh bearer link returned only in a terminal batch result. */
460
+ qurl_link: string;
461
+ expires_at: string;
462
+ }
463
+ export interface DelegatedQurlBatchItemSuccess {
464
+ index: number;
465
+ status: "succeeded";
466
+ qurl: DelegatedQurlCreatedData;
467
+ }
468
+ export interface DelegatedQurlBatchItemFailure {
469
+ index: number;
470
+ status: "failed";
471
+ error: {
472
+ code: string;
473
+ message: string;
474
+ };
475
+ }
476
+ export type DelegatedQurlBatchItemResult = DelegatedQurlBatchItemSuccess | DelegatedQurlBatchItemFailure;
477
+ /** Exact HTTP 202 result for a queued or running batch. */
478
+ export interface DelegatedQurlBatchPending extends DelegatedQurlBatchBase {
479
+ http_status: 202;
480
+ status: "queued" | "running";
481
+ etag?: string;
482
+ /** Positive Retry-After delta in seconds. */
483
+ retry_after?: number;
484
+ }
485
+ /** Exact HTTP 200 terminal result. */
486
+ export interface DelegatedQurlBatchTerminal extends DelegatedQurlBatchBase {
487
+ http_status: 200;
488
+ status: "succeeded" | "partially_failed" | "failed";
489
+ completed_at: string;
490
+ results: DelegatedQurlBatchItemResult[];
491
+ }
492
+ /** Exact body-free HTTP 304 result for an unchanged non-terminal batch. */
493
+ export interface DelegatedQurlBatchNotModified {
494
+ http_status: 304;
495
+ etag: string;
496
+ /** Positive Retry-After delta in seconds, when the batch is still pending. */
497
+ retry_after?: number;
498
+ }
499
+ export type GetDelegatedQurlBatchOutput = DelegatedQurlBatchPending | DelegatedQurlBatchTerminal | DelegatedQurlBatchNotModified;
500
+ /** Owner-visible metadata for one delegated qURL. The bearer link is never returned here. */
501
+ export interface DelegatedQurl {
502
+ request_id: string;
503
+ qurl_id: string;
504
+ status: "active" | "consumed" | "expired" | "revoked";
505
+ expires_at: string;
506
+ label?: string;
507
+ one_time_use: boolean;
508
+ max_sessions: number;
509
+ session_duration: number;
510
+ access_policy?: AccessPolicy;
511
+ }
415
512
  /** Input for creating a resource directly. */
416
513
  export interface CreateResourceInput {
417
514
  type?: ResourceType;
@@ -1 +1 @@
1
- export declare const VERSION = "0.7.0";
1
+ export declare const VERSION = "2.0.0";
@@ -1,4 +1,4 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.VERSION = void 0;
4
- exports.VERSION = "0.7.0"; // x-release-please-version
4
+ exports.VERSION = "2.0.0"; // x-release-please-version
@@ -1,5 +1,5 @@
1
1
  import { ShareLink } from "./share.js";
2
- import type { AccessCodeListOutput, ApiKey, ApiKeyListOutput, ApiKeyRequestScope, BillingInvoiceListOutput, BatchCreateInput, BatchCreateOutput, ClientOptions, ConnectorInstallation, ConnectorInstallationListOutput, CreateAccessCodeInput, CreateAccessCodeOutput, CreateApiKeyInput, CreateApiKeyOutput, CreateBillingCheckoutInput, CreateInput, CreateOutput, CreatePortalOptions, CreateQurlForResourceInput, CreateResourceInput, CreateWebhookInput, Customer, CheckoutSession, Domain, DomainListOutput, DomainVerifyResult, ExtendInput, Invoice, ListInput, ListOutput, ListApiKeysInput, ListBillingInvoicesInput, ListConnectorInstallationsInput, ListDomainsInput, ListWebhookDeliveriesInput, ListWebhooksInput, MintInput, MintOutput, Portal, PortalSession, ProtectUrlOptions, QURL, QurlSummary, Quota, RedeemAccessCodeInput, RedeemAccessCodeOutput, RegisterDomainInput, Resource, ResourceDetail, ResourceHandle, ResourceListInput, ResourceListOutput, RequestOptions, ResolveInput, ResolveOutput, SessionListOutput, SessionTerminateOutput, ShareResourceOptions, UpdateApiKeyInput, UpdateCustomerInput, UpdateInput, UpdateResourceInput, UpdateResourceQurlInput, UpdateWebhookInput, UsageCurrentPeriod, UsageDaily, Webhook, WebhookDelivery, WebhookDeliveryListOutput, WebhookEventTypeInfo, WebhookListOutput, WebhookWithSecret } from "./types.js";
2
+ import type { AccessCodeListOutput, ApiKey, ApiKeyListOutput, ApiKeyRequestScope, BillingInvoiceListOutput, BatchCreateInput, BatchCreateOutput, ClientOptions, ConnectorInstallation, ConnectorInstallationListOutput, CreateAccessCodeInput, CreateAccessCodeOutput, CreateApiKeyInput, CreateApiKeyOutput, CreateBillingCheckoutInput, CreateDelegatedQurlBatchInput, CreateDelegatedQurlBatchOptions, CreateInput, CreateOutput, CreatePortalOptions, CreateQurlForResourceInput, CreateResourceInput, CreateWebhookInput, Customer, CheckoutSession, Domain, DomainListOutput, DomainVerifyResult, DelegatedQurl, DelegatedQurlBatchAccepted, GetDelegatedQurlBatchOptions, GetDelegatedQurlBatchOutput, ExtendInput, Invoice, ListInput, ListOutput, ListApiKeysInput, ListBillingInvoicesInput, ListConnectorInstallationsInput, ListDomainsInput, ListWebhookDeliveriesInput, ListWebhooksInput, MintInput, MintOutput, Portal, PortalSession, ProtectUrlOptions, QURL, QurlSummary, Quota, RedeemAccessCodeInput, RedeemAccessCodeOutput, RegisterDomainInput, Resource, ResourceDetail, ResourceHandle, ResourceListInput, ResourceListOutput, RequestOptions, ResolveInput, ResolveOutput, SessionListOutput, SessionTerminateOutput, ShareResourceOptions, UpdateApiKeyInput, UpdateCustomerInput, UpdateInput, UpdateResourceInput, UpdateResourceQurlInput, UpdateWebhookInput, UsageCurrentPeriod, UsageDaily, Webhook, WebhookDelivery, WebhookDeliveryListOutput, WebhookEventTypeInfo, WebhookListOutput, WebhookWithSecret } from "./types.js";
3
3
  /** Narrow a response scope before reusing it in a create/update request. */
4
4
  export declare function isApiKeyRequestScope(scope: string): scope is ApiKeyRequestScope;
5
5
  /**
@@ -226,6 +226,24 @@ export declare class QURLClient {
226
226
  * ```
227
227
  */
228
228
  batchCreate(input: BatchCreateInput, options?: RequestOptions): Promise<BatchCreateOutput>;
229
+ /**
230
+ * Start one durable delegated qURL batch. The SDK sends this mutation once.
231
+ * If the result may have committed, retry explicitly with the same key and
232
+ * semantic body.
233
+ */
234
+ createDelegatedQurlBatch(input: CreateDelegatedQurlBatchInput, options: CreateDelegatedQurlBatchOptions): Promise<DelegatedQurlBatchAccepted>;
235
+ /**
236
+ * Read one delegated qURL batch state. One call performs one logical read;
237
+ * callers retain control of the poll count and total deadline.
238
+ */
239
+ getDelegatedQurlBatch(batchId: string, options?: GetDelegatedQurlBatchOptions): Promise<GetDelegatedQurlBatchOutput>;
240
+ /** Read owner-visible metadata for one delegated qURL. */
241
+ getDelegatedQurl(qurlId: string): Promise<DelegatedQurl>;
242
+ /**
243
+ * Revoke one delegated qURL. DELETE is never retried automatically. A 503
244
+ * `mutation_outcome_unknown` is safe to retry with the same qURL ID.
245
+ */
246
+ deleteDelegatedQurl(qurlId: string): Promise<void>;
229
247
  /**
230
248
  * Get a qURL resource and its access tokens.
231
249
  *