@layervai/qurl 0.3.1 → 0.5.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 (72) hide show
  1. package/README.md +261 -11
  2. package/dist/cjs/client.d.ts +22 -12
  3. package/dist/cjs/client.js +675 -100
  4. package/dist/cjs/errors.d.ts +11 -11
  5. package/dist/cjs/errors.js +25 -20
  6. package/dist/cjs/index.d.ts +1 -1
  7. package/dist/cjs/internal.d.ts +4 -0
  8. package/dist/cjs/internal.js +16 -0
  9. package/dist/cjs/node/deployment.d.ts +44 -0
  10. package/dist/cjs/node/deployment.js +167 -0
  11. package/dist/cjs/node/native-udp.d.ts +35 -0
  12. package/dist/cjs/node/native-udp.js +294 -0
  13. package/dist/cjs/node/nhp-wire.d.ts +35 -0
  14. package/dist/cjs/node/nhp-wire.js +261 -0
  15. package/dist/cjs/node/portal-opener.d.ts +114 -0
  16. package/dist/cjs/node/portal-opener.js +1070 -0
  17. package/dist/cjs/node/qv2.d.ts +38 -0
  18. package/dist/cjs/node/qv2.js +292 -0
  19. package/dist/cjs/node/strict-json.d.ts +12 -0
  20. package/dist/cjs/node/strict-json.js +174 -0
  21. package/dist/cjs/node.d.ts +3 -0
  22. package/dist/cjs/node.js +18 -0
  23. package/dist/cjs/types.d.ts +17 -20
  24. package/dist/cjs/version.d.ts +1 -1
  25. package/dist/cjs/version.js +1 -1
  26. package/dist/esm/client.d.ts +22 -12
  27. package/dist/esm/client.d.ts.map +1 -1
  28. package/dist/esm/client.js +673 -98
  29. package/dist/esm/client.js.map +1 -1
  30. package/dist/esm/errors.d.ts +11 -11
  31. package/dist/esm/errors.d.ts.map +1 -1
  32. package/dist/esm/errors.js +25 -20
  33. package/dist/esm/errors.js.map +1 -1
  34. package/dist/esm/index.d.ts +1 -1
  35. package/dist/esm/index.d.ts.map +1 -1
  36. package/dist/esm/internal.d.ts +5 -0
  37. package/dist/esm/internal.d.ts.map +1 -0
  38. package/dist/esm/internal.js +14 -0
  39. package/dist/esm/internal.js.map +1 -0
  40. package/dist/esm/node/deployment.d.ts +45 -0
  41. package/dist/esm/node/deployment.d.ts.map +1 -0
  42. package/dist/esm/node/deployment.js +163 -0
  43. package/dist/esm/node/deployment.js.map +1 -0
  44. package/dist/esm/node/native-udp.d.ts +36 -0
  45. package/dist/esm/node/native-udp.d.ts.map +1 -0
  46. package/dist/esm/node/native-udp.js +291 -0
  47. package/dist/esm/node/native-udp.js.map +1 -0
  48. package/dist/esm/node/nhp-wire.d.ts +36 -0
  49. package/dist/esm/node/nhp-wire.d.ts.map +1 -0
  50. package/dist/esm/node/nhp-wire.js +257 -0
  51. package/dist/esm/node/nhp-wire.js.map +1 -0
  52. package/dist/esm/node/portal-opener.d.ts +115 -0
  53. package/dist/esm/node/portal-opener.d.ts.map +1 -0
  54. package/dist/esm/node/portal-opener.js +1053 -0
  55. package/dist/esm/node/portal-opener.js.map +1 -0
  56. package/dist/esm/node/qv2.d.ts +39 -0
  57. package/dist/esm/node/qv2.d.ts.map +1 -0
  58. package/dist/esm/node/qv2.js +286 -0
  59. package/dist/esm/node/qv2.js.map +1 -0
  60. package/dist/esm/node/strict-json.d.ts +13 -0
  61. package/dist/esm/node/strict-json.d.ts.map +1 -0
  62. package/dist/esm/node/strict-json.js +171 -0
  63. package/dist/esm/node/strict-json.js.map +1 -0
  64. package/dist/esm/node.d.ts +4 -0
  65. package/dist/esm/node.d.ts.map +1 -0
  66. package/dist/esm/node.js +2 -0
  67. package/dist/esm/node.js.map +1 -0
  68. package/dist/esm/types.d.ts +17 -20
  69. package/dist/esm/types.d.ts.map +1 -1
  70. package/dist/esm/version.d.ts +1 -1
  71. package/dist/esm/version.js +1 -1
  72. package/package.json +26 -7
@@ -0,0 +1,38 @@
1
+ import type { KeyObject } from "node:crypto";
2
+ export interface VerifiedQv2Link {
3
+ readonly claimsB64: string;
4
+ readonly signatureB64: string;
5
+ readonly claims: {
6
+ readonly kid: string;
7
+ readonly cellPublicKey: Uint8Array;
8
+ readonly resourcePublicKeyB64: string;
9
+ };
10
+ readonly devicePrivateKey: Uint8Array;
11
+ }
12
+ export declare function verifyQv2Link(qurl: string, issuers: ReadonlyMap<string, KeyObject>): VerifiedQv2Link;
13
+ export declare function issuerKeyFromSpki(spki: Uint8Array): KeyObject;
14
+ export declare function x25519PublicFromPrivate(raw: Uint8Array): Uint8Array;
15
+ export declare function strictBase64Url(value: string): Uint8Array;
16
+ declare function decodeTransport(value: string): string;
17
+ type ParsedClaims = {
18
+ kid: string;
19
+ cellPublicKeyB64: string;
20
+ resourcePublicKeyB64: string;
21
+ qurlUserPublicKeyB64: string;
22
+ };
23
+ declare function parseClaims(raw: Uint8Array): ParsedClaims;
24
+ declare function verifyIssuerClaims(claimsB64: string, signatureB64: string, issuers: ReadonlyMap<string, KeyObject>, parsedClaims?: ParsedClaims): ParsedClaims;
25
+ declare function parseSecret(raw: Uint8Array): {
26
+ qurlUserPrivateKeyB64: string;
27
+ };
28
+ declare function parseAndWipeSecret(raw: Uint8Array): {
29
+ qurlUserPrivateKeyB64: string;
30
+ };
31
+ export declare const qv2Testing: {
32
+ decodeTransport: typeof decodeTransport;
33
+ parseClaims: typeof parseClaims;
34
+ parseSecret: typeof parseSecret;
35
+ parseAndWipeSecret: typeof parseAndWipeSecret;
36
+ verifyIssuerClaims: typeof verifyIssuerClaims;
37
+ };
38
+ export {};
@@ -0,0 +1,292 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.qv2Testing = void 0;
4
+ exports.verifyQv2Link = verifyQv2Link;
5
+ exports.issuerKeyFromSpki = issuerKeyFromSpki;
6
+ exports.x25519PublicFromPrivate = x25519PublicFromPrivate;
7
+ exports.strictBase64Url = strictBase64Url;
8
+ const node_crypto_1 = require("node:crypto");
9
+ const strict_json_js_1 = require("./strict-json.js");
10
+ const TRANSPORT_PREFIX = "qv2t1";
11
+ const FRAGMENT_PREFIX = "qv2";
12
+ const TRANSPORT_MAX_LENGTH = 6_826;
13
+ const TRANSPORT_COMPONENT_MAX = 240;
14
+ const SIGNING_DOMAIN = Buffer.from("NHP-QURL-V2-ISSUER\0", "utf8");
15
+ const P256_ORDER = BigInt("0xffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551");
16
+ const P256_HALF_ORDER = P256_ORDER >> 1n;
17
+ const X25519_PKCS8_PREFIX = Buffer.from("302e020100300506032b656e04220420", "hex");
18
+ const CLAIM_KEYS = new Set([
19
+ "v",
20
+ "iss",
21
+ "kid",
22
+ "iat",
23
+ "nbf",
24
+ "exp",
25
+ "jti",
26
+ "cell_public_key_b64",
27
+ "cell_id",
28
+ "relay_url",
29
+ "resource_public_key_b64",
30
+ "qurl_user_public_key_b64",
31
+ ]);
32
+ const REQUIRED_CLAIM_KEYS = [...CLAIM_KEYS].filter((key) => key !== "cell_id");
33
+ function verifyQv2Link(qurl, issuers) {
34
+ const hash = qurl.indexOf("#");
35
+ if (hash < 0)
36
+ throw new Error("qURL link has no credential fragment");
37
+ const canonical = decodeTransport(qurl.slice(hash + 1));
38
+ const parts = canonical.split(".");
39
+ if (parts.length !== 4 || parts[0] !== FRAGMENT_PREFIX || parts.slice(1).some((p) => p === "")) {
40
+ throw new Error("invalid qURL credential fragment");
41
+ }
42
+ const [, claimsB64, secretB64, signatureB64] = parts;
43
+ // Match Go ParseFragment -> Verify ordering: strict claims and secret parsing
44
+ // precede signature/trust errors for an otherwise valid outer transport.
45
+ const claims = parseClaims(decodeCanonicalBase64Url(claimsB64));
46
+ const secret = parseAndWipeSecret(decodeCanonicalBase64Url(secretB64));
47
+ verifyIssuerClaims(claimsB64, signatureB64, issuers, claims);
48
+ const privateKey = decodeCanonicalBase64Url(secret.qurlUserPrivateKeyB64);
49
+ let retainPrivateKey = false;
50
+ try {
51
+ if (privateKey.byteLength !== 32)
52
+ throw new Error("qURL private key has an invalid length");
53
+ const derivedPublicKey = x25519PublicFromPrivate(privateKey);
54
+ const signedPublicKey = decodeX25519(claims.qurlUserPublicKeyB64, "qURL public key");
55
+ if (!(0, node_crypto_1.timingSafeEqual)(derivedPublicKey, signedPublicKey)) {
56
+ throw new Error("qURL private key does not match its signed public key");
57
+ }
58
+ retainPrivateKey = true;
59
+ return {
60
+ claimsB64,
61
+ signatureB64,
62
+ claims: {
63
+ kid: claims.kid,
64
+ cellPublicKey: decodeX25519(claims.cellPublicKeyB64, "cell public key"),
65
+ resourcePublicKeyB64: claims.resourcePublicKeyB64,
66
+ },
67
+ devicePrivateKey: privateKey,
68
+ };
69
+ }
70
+ finally {
71
+ if (!retainPrivateKey)
72
+ privateKey.fill(0);
73
+ }
74
+ }
75
+ function issuerKeyFromSpki(spki) {
76
+ const key = (0, node_crypto_1.createPublicKey)({ key: spki, format: "der", type: "spki" });
77
+ const canonical = key.export({ format: "der", type: "spki" });
78
+ const supplied = Buffer.from(spki.buffer, spki.byteOffset, spki.byteLength);
79
+ // Node accepts a valid SPKI followed by unrelated DER bytes. Go's
80
+ // x509.ParsePKIXPublicKey requires the complete input to contain one key, so
81
+ // require the same byte-exact consumption before this key becomes trust.
82
+ if (canonical.byteLength !== supplied.byteLength || !canonical.equals(supplied)) {
83
+ throw new Error("issuer key must contain exactly one canonical SPKI public key");
84
+ }
85
+ if (key.asymmetricKeyType !== "ec" || key.asymmetricKeyDetails?.namedCurve !== "prime256v1") {
86
+ throw new Error("issuer key must be a P-256 SPKI public key");
87
+ }
88
+ return key;
89
+ }
90
+ function x25519PublicFromPrivate(raw) {
91
+ if (raw.byteLength !== 32)
92
+ throw new Error("X25519 private key must be 32 bytes");
93
+ const der = Buffer.alloc(X25519_PKCS8_PREFIX.byteLength + raw.byteLength);
94
+ X25519_PKCS8_PREFIX.copy(der);
95
+ der.set(raw, X25519_PKCS8_PREFIX.byteLength);
96
+ let privateKey;
97
+ try {
98
+ privateKey = (0, node_crypto_1.createPrivateKey)({ key: der, format: "der", type: "pkcs8" });
99
+ }
100
+ finally {
101
+ der.fill(0);
102
+ }
103
+ const spki = (0, node_crypto_1.createPublicKey)(privateKey).export({ format: "der", type: "spki" });
104
+ return new Uint8Array(spki.subarray(spki.byteLength - 32));
105
+ }
106
+ function strictBase64Url(value) {
107
+ return decodeCanonicalBase64Url(value);
108
+ }
109
+ function decodeTransport(value) {
110
+ if (value.length > TRANSPORT_MAX_LENGTH)
111
+ throw new Error("qURL transport exceeds its size limit");
112
+ const parts = value.split(".");
113
+ if (parts.length < 4 || parts[0] !== TRANSPORT_PREFIX) {
114
+ throw new Error("qURL does not use the qv2t1 credential transport");
115
+ }
116
+ const counts = [parseCount(parts[1], 26), parseCount(parts[2], 3), parseCount(parts[3], 1)];
117
+ if (parts.length !== 4 + counts[0] + counts[1] + counts[2]) {
118
+ throw new Error("qURL transport part count does not match its declarations");
119
+ }
120
+ let offset = 4;
121
+ const limits = [6_144, 512, 128];
122
+ const fields = counts.map((count, index) => {
123
+ const chunks = parts.slice(offset, offset + count);
124
+ offset += count;
125
+ let total = 0;
126
+ for (let i = 0; i < chunks.length; i++) {
127
+ const chunk = chunks[i];
128
+ if (chunk.length === 0 ||
129
+ chunk.length > TRANSPORT_COMPONENT_MAX ||
130
+ (i < chunks.length - 1 && chunk.length !== TRANSPORT_COMPONENT_MAX) ||
131
+ !/^[A-Za-z0-9_-]+$/.test(chunk)) {
132
+ throw new Error("qURL transport has an invalid chunk");
133
+ }
134
+ total += chunk.length;
135
+ if (total > limits[index])
136
+ throw new Error("qURL transport field exceeds its size limit");
137
+ }
138
+ return chunks.join("");
139
+ });
140
+ return `${FRAGMENT_PREFIX}.${fields[0]}.${fields[1]}.${fields[2]}`;
141
+ }
142
+ function parseCount(value, maximum) {
143
+ if (!/^[1-9][0-9]*$/.test(value))
144
+ throw new Error("qURL transport count is not canonical");
145
+ const count = Number(value);
146
+ if (!Number.isSafeInteger(count) || count > maximum) {
147
+ throw new Error("qURL transport count exceeds its limit");
148
+ }
149
+ return count;
150
+ }
151
+ function decodeCanonicalBase64Url(value) {
152
+ if (value === "" || !/^[A-Za-z0-9_-]+$/.test(value) || value.length % 4 === 1) {
153
+ throw new Error("value is not canonical unpadded base64url");
154
+ }
155
+ const decoded = Buffer.from(value, "base64url");
156
+ if (decoded.toString("base64url") !== value) {
157
+ decoded.fill(0);
158
+ throw new Error("value is not canonical unpadded base64url");
159
+ }
160
+ return decoded;
161
+ }
162
+ function decodeX25519(value, name) {
163
+ const decoded = decodeCanonicalBase64Url(value);
164
+ if (decoded.byteLength !== 32)
165
+ throw new Error(`${name} has an invalid length`);
166
+ return decoded;
167
+ }
168
+ function parseClaims(raw) {
169
+ const value = (0, strict_json_js_1.parseStrictJson)(raw, 4_608);
170
+ if (!(0, strict_json_js_1.isStrictJsonObject)(value))
171
+ throw new Error("qURL claims must be one JSON object");
172
+ for (const key of Object.keys(value)) {
173
+ if (!CLAIM_KEYS.has(key))
174
+ throw new Error("qURL claims contain an unknown field");
175
+ if (value[key] === null)
176
+ throw new Error("qURL claims contain a null field");
177
+ }
178
+ for (const key of REQUIRED_CLAIM_KEYS) {
179
+ if (!(key in value))
180
+ throw new Error("qURL claims are missing a required field");
181
+ }
182
+ if (value.v !== 2n || value.iss !== "qurl-service")
183
+ throw new Error("qURL claims version or issuer is invalid");
184
+ const times = {
185
+ iat: 0n,
186
+ nbf: 0n,
187
+ exp: 0n,
188
+ };
189
+ for (const key of ["iat", "nbf", "exp"]) {
190
+ const item = value[key];
191
+ if (typeof item !== "bigint" || item <= 0n || item > 9999999999n) {
192
+ throw new Error("qURL claims contain an invalid time");
193
+ }
194
+ times[key] = item;
195
+ }
196
+ if (times.iat > times.exp || times.nbf > times.exp) {
197
+ throw new Error("qURL claims contain an invalid time window");
198
+ }
199
+ const kid = requireNonEmptyString(value.kid, "issuer key id");
200
+ requireNonEmptyString(value.jti, "qURL id");
201
+ const cellPublicKeyB64 = requireNonEmptyString(value.cell_public_key_b64, "cell public key");
202
+ decodeX25519(cellPublicKeyB64, "cell public key");
203
+ const qurlPublic = requireNonEmptyString(value.qurl_user_public_key_b64, "qURL public key");
204
+ decodeX25519(qurlPublic, "qURL public key");
205
+ const resourcePublicKeyB64 = requireNonEmptyString(value.resource_public_key_b64, "resource public key");
206
+ const resourcePublic = decodeCanonicalBase64Url(resourcePublicKeyB64);
207
+ if (resourcePublic.byteLength < 80 || resourcePublic.byteLength > 160) {
208
+ throw new Error("resource public key has an invalid length");
209
+ }
210
+ requireNonEmptyString(value.relay_url, "relay URL");
211
+ // cell_id is descriptive deployment metadata, not a security identity. The
212
+ // signed 32-byte cell public key selects and then fully binds the UDP peer.
213
+ if ("cell_id" in value && typeof value.cell_id !== "string") {
214
+ throw new Error("qURL cell id must be a string");
215
+ }
216
+ return {
217
+ kid,
218
+ cellPublicKeyB64,
219
+ resourcePublicKeyB64,
220
+ qurlUserPublicKeyB64: qurlPublic,
221
+ };
222
+ }
223
+ function verifyIssuerClaims(claimsB64, signatureB64, issuers, parsedClaims) {
224
+ // Full-link verification supplies the claims it parsed before the secret so
225
+ // this shared verification path preserves Go's ParseFragment error order.
226
+ const claims = parsedClaims ?? parseClaims(decodeCanonicalBase64Url(claimsB64));
227
+ verifyParsedIssuerClaims(claims, claimsB64, signatureB64, issuers);
228
+ return claims;
229
+ }
230
+ function verifyParsedIssuerClaims(claims, claimsB64, signatureB64, issuers) {
231
+ const signature = decodeCanonicalBase64Url(signatureB64);
232
+ const issuer = issuers.get(claims.kid);
233
+ if (!issuer)
234
+ throw new Error("qURL uses an unknown issuer key id");
235
+ // Go resolves trust after base64 decoding but before raw signature shape
236
+ // validation. Preserve that error precedence for full-link verification.
237
+ validateRawP256Signature(signature);
238
+ const signingInput = Buffer.concat([SIGNING_DOMAIN, Buffer.from(claimsB64, "ascii")]);
239
+ if (!(0, node_crypto_1.verify)("sha256", signingInput, { key: issuer, dsaEncoding: "ieee-p1363" }, signature)) {
240
+ throw new Error("qURL issuer signature verification failed");
241
+ }
242
+ }
243
+ function parseSecret(raw) {
244
+ const value = (0, strict_json_js_1.parseStrictJson)(raw, 1_024);
245
+ if (!(0, strict_json_js_1.isStrictJsonObject)(value) || Object.keys(value).length !== 1) {
246
+ throw new Error("qURL secret has an invalid object shape");
247
+ }
248
+ const qurlUserPrivateKeyB64 = requireNonEmptyString(value.qurl_user_private_key_b64, "qURL private key");
249
+ const privateKey = decodeCanonicalBase64Url(qurlUserPrivateKeyB64);
250
+ try {
251
+ if (privateKey.byteLength !== 32)
252
+ throw new Error("qURL private key has an invalid length");
253
+ }
254
+ finally {
255
+ privateKey.fill(0);
256
+ }
257
+ return { qurlUserPrivateKeyB64 };
258
+ }
259
+ function parseAndWipeSecret(raw) {
260
+ try {
261
+ return parseSecret(raw);
262
+ }
263
+ finally {
264
+ raw.fill(0);
265
+ }
266
+ }
267
+ function requireNonEmptyString(value, name) {
268
+ if (typeof value !== "string" || value === "")
269
+ throw new Error(`${name} must be a non-empty string`);
270
+ return value;
271
+ }
272
+ function validateRawP256Signature(signature) {
273
+ if (signature.byteLength !== 64)
274
+ throw new Error("qURL signature must be 64 bytes");
275
+ const r = bytesToBigInt(signature.subarray(0, 32));
276
+ const s = bytesToBigInt(signature.subarray(32));
277
+ if (r <= 0n || r >= P256_ORDER || s <= 0n || s >= P256_ORDER) {
278
+ throw new Error("qURL signature scalar is outside the P-256 range");
279
+ }
280
+ if (s > P256_HALF_ORDER)
281
+ throw new Error("qURL signature is not low-S normalized");
282
+ }
283
+ function bytesToBigInt(value) {
284
+ return BigInt(`0x${Buffer.from(value).toString("hex")}`);
285
+ }
286
+ exports.qv2Testing = {
287
+ decodeTransport,
288
+ parseClaims,
289
+ parseSecret,
290
+ parseAndWipeSecret,
291
+ verifyIssuerClaims,
292
+ };
@@ -0,0 +1,12 @@
1
+ export type StrictJsonValue = null | boolean | number | bigint | string | StrictJsonValue[] | {
2
+ [key: string]: StrictJsonValue;
3
+ };
4
+ /**
5
+ * Parse a bounded JSON value without JavaScript's duplicate-key or unsafe-
6
+ * integer ambiguity. NHP uses uint64 session identifiers, so integral values
7
+ * outside Number's safe range remain bigint instead of losing wire bytes.
8
+ */
9
+ export declare function parseStrictJson(input: Uint8Array, maxBytes: number): StrictJsonValue;
10
+ export declare function isStrictJsonObject(value: StrictJsonValue | undefined): value is {
11
+ [key: string]: StrictJsonValue;
12
+ };
@@ -0,0 +1,174 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseStrictJson = parseStrictJson;
4
+ exports.isStrictJsonObject = isStrictJsonObject;
5
+ const MAX_JSON_DEPTH = 32;
6
+ const JSON_NUMBER = /-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?/y;
7
+ /**
8
+ * Parse a bounded JSON value without JavaScript's duplicate-key or unsafe-
9
+ * integer ambiguity. NHP uses uint64 session identifiers, so integral values
10
+ * outside Number's safe range remain bigint instead of losing wire bytes.
11
+ */
12
+ function parseStrictJson(input, maxBytes) {
13
+ if (input.byteLength > maxBytes)
14
+ throw new Error(`JSON exceeds ${maxBytes}-byte limit`);
15
+ const text = new TextDecoder("utf-8", { fatal: true }).decode(input);
16
+ const parser = new StrictJsonParser(text);
17
+ const value = parser.parseValue(0);
18
+ parser.skipWhitespace();
19
+ if (!parser.done)
20
+ throw new Error("trailing data after JSON value");
21
+ return value;
22
+ }
23
+ class StrictJsonParser {
24
+ input;
25
+ #offset = 0;
26
+ constructor(input) {
27
+ this.input = input;
28
+ }
29
+ get done() {
30
+ return this.#offset === this.input.length;
31
+ }
32
+ skipWhitespace() {
33
+ while (this.#offset < this.input.length) {
34
+ const code = this.input.charCodeAt(this.#offset);
35
+ if (code !== 0x20 && code !== 0x09 && code !== 0x0a && code !== 0x0d)
36
+ break;
37
+ this.#offset++;
38
+ }
39
+ }
40
+ parseValue(depth) {
41
+ if (depth > MAX_JSON_DEPTH)
42
+ throw new Error("JSON nesting is too deep");
43
+ this.skipWhitespace();
44
+ const token = this.input[this.#offset];
45
+ if (token === '"')
46
+ return this.parseString();
47
+ if (token === "{")
48
+ return this.parseObject(depth + 1);
49
+ if (token === "[")
50
+ return this.parseArray(depth + 1);
51
+ if (token === "t")
52
+ return this.parseLiteral("true", true);
53
+ if (token === "f")
54
+ return this.parseLiteral("false", false);
55
+ if (token === "n")
56
+ return this.parseLiteral("null", null);
57
+ if (token === "-" || (token !== undefined && token >= "0" && token <= "9")) {
58
+ return this.parseNumber();
59
+ }
60
+ throw new Error("invalid JSON value");
61
+ }
62
+ parseObject(depth) {
63
+ this.#offset++;
64
+ const value = Object.create(null);
65
+ const keys = new Set();
66
+ this.skipWhitespace();
67
+ if (this.consume("}"))
68
+ return value;
69
+ while (true) {
70
+ this.skipWhitespace();
71
+ if (this.input[this.#offset] !== '"')
72
+ throw new Error("JSON object key must be a string");
73
+ const key = this.parseString();
74
+ if (keys.has(key))
75
+ throw new Error("duplicate JSON object key");
76
+ keys.add(key);
77
+ this.skipWhitespace();
78
+ if (!this.consume(":"))
79
+ throw new Error("JSON object key has no value");
80
+ value[key] = this.parseValue(depth);
81
+ this.skipWhitespace();
82
+ if (this.consume("}"))
83
+ return value;
84
+ if (!this.consume(","))
85
+ throw new Error("invalid JSON object separator");
86
+ }
87
+ }
88
+ parseArray(depth) {
89
+ this.#offset++;
90
+ const value = [];
91
+ this.skipWhitespace();
92
+ if (this.consume("]"))
93
+ return value;
94
+ while (true) {
95
+ value.push(this.parseValue(depth));
96
+ this.skipWhitespace();
97
+ if (this.consume("]"))
98
+ return value;
99
+ if (!this.consume(","))
100
+ throw new Error("invalid JSON array separator");
101
+ }
102
+ }
103
+ parseString() {
104
+ const start = this.#offset++;
105
+ while (this.#offset < this.input.length) {
106
+ const code = this.input.charCodeAt(this.#offset++);
107
+ if (code === 0x22) {
108
+ const encoded = this.input.slice(start, this.#offset);
109
+ const value = JSON.parse(encoded);
110
+ if (typeof value !== "string")
111
+ throw new Error("invalid JSON string");
112
+ return value;
113
+ }
114
+ if (code < 0x20)
115
+ throw new Error("unescaped control in JSON string");
116
+ if (code === 0x5c) {
117
+ if (this.#offset >= this.input.length)
118
+ throw new Error("unterminated JSON escape");
119
+ if (this.input[this.#offset] === "u") {
120
+ const hex = this.input.slice(this.#offset + 1, this.#offset + 5);
121
+ if (!/^[0-9a-fA-F]{4}$/.test(hex))
122
+ throw new Error("invalid JSON unicode escape");
123
+ this.#offset += 5;
124
+ }
125
+ else {
126
+ if (!'"\\/bfnrt'.includes(this.input[this.#offset])) {
127
+ throw new Error("invalid JSON escape");
128
+ }
129
+ this.#offset++;
130
+ }
131
+ }
132
+ }
133
+ throw new Error("unterminated JSON string");
134
+ }
135
+ parseNumber() {
136
+ // A sticky expression scans at the current offset without copying the
137
+ // remaining document for every number in a large deployment file.
138
+ JSON_NUMBER.lastIndex = this.#offset;
139
+ const match = JSON_NUMBER.exec(this.input);
140
+ if (!match)
141
+ throw new Error("invalid JSON number");
142
+ const encoded = match[0];
143
+ this.#offset = JSON_NUMBER.lastIndex;
144
+ // Keep every syntactic JSON integer distinct from fractional or exponent
145
+ // notation. NHP integer fields reject 1.0 and 1e0 even when Number would
146
+ // reduce them to the same mathematical value as 1.
147
+ if (!encoded.includes(".") && !/[eE]/.test(encoded)) {
148
+ // BigInt("-0") becomes 0n and loses the wire sign. Keep negative zero as
149
+ // a number so unsigned integer fields reject this noncanonical syntax.
150
+ if (encoded === "-0")
151
+ return -0;
152
+ return BigInt(encoded);
153
+ }
154
+ const value = Number(encoded);
155
+ if (!Number.isFinite(value))
156
+ throw new Error("JSON number is outside the finite range");
157
+ return value;
158
+ }
159
+ parseLiteral(literal, value) {
160
+ if (!this.input.startsWith(literal, this.#offset))
161
+ throw new Error("invalid JSON literal");
162
+ this.#offset += literal.length;
163
+ return value;
164
+ }
165
+ consume(value) {
166
+ if (this.input[this.#offset] !== value)
167
+ return false;
168
+ this.#offset++;
169
+ return true;
170
+ }
171
+ }
172
+ function isStrictJsonObject(value) {
173
+ return typeof value === "object" && value !== null && !Array.isArray(value);
174
+ }
@@ -0,0 +1,3 @@
1
+ export { createPortalOpener, PortalBusyError, PortalConfigurationError, PortalDenyError, PortalInvalidReplyError, PortalOpenerClosedError, PortalOpenerNotReadyError, PortalOpenerNotStartedError, PortalOpenTimeoutError, PortalRedirectError, PortalStateError, PortalTargetChangedError, PortalTooManyRedirectsError, PortalVerificationError, } from "./node/portal-opener.js";
2
+ export type { CreatePortalOpenerOptions, PortalFetchOptions, PortalOpener, PortalOpenerFailureClass, PortalOpenerHealth, PortalOpenerState, PortalRequestBuilder, PortalStartOptions, } from "./node/portal-opener.js";
3
+ export type { PortalDeployment, PortalDeploymentCell, PortalDeploymentIssuer, } from "./node/deployment.js";
@@ -0,0 +1,18 @@
1
+ "use strict";
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;
4
+ var portal_opener_js_1 = require("./node/portal-opener.js");
5
+ Object.defineProperty(exports, "createPortalOpener", { enumerable: true, get: function () { return portal_opener_js_1.createPortalOpener; } });
6
+ Object.defineProperty(exports, "PortalBusyError", { enumerable: true, get: function () { return portal_opener_js_1.PortalBusyError; } });
7
+ Object.defineProperty(exports, "PortalConfigurationError", { enumerable: true, get: function () { return portal_opener_js_1.PortalConfigurationError; } });
8
+ Object.defineProperty(exports, "PortalDenyError", { enumerable: true, get: function () { return portal_opener_js_1.PortalDenyError; } });
9
+ Object.defineProperty(exports, "PortalInvalidReplyError", { enumerable: true, get: function () { return portal_opener_js_1.PortalInvalidReplyError; } });
10
+ Object.defineProperty(exports, "PortalOpenerClosedError", { enumerable: true, get: function () { return portal_opener_js_1.PortalOpenerClosedError; } });
11
+ Object.defineProperty(exports, "PortalOpenerNotReadyError", { enumerable: true, get: function () { return portal_opener_js_1.PortalOpenerNotReadyError; } });
12
+ Object.defineProperty(exports, "PortalOpenerNotStartedError", { enumerable: true, get: function () { return portal_opener_js_1.PortalOpenerNotStartedError; } });
13
+ Object.defineProperty(exports, "PortalOpenTimeoutError", { enumerable: true, get: function () { return portal_opener_js_1.PortalOpenTimeoutError; } });
14
+ Object.defineProperty(exports, "PortalRedirectError", { enumerable: true, get: function () { return portal_opener_js_1.PortalRedirectError; } });
15
+ Object.defineProperty(exports, "PortalStateError", { enumerable: true, get: function () { return portal_opener_js_1.PortalStateError; } });
16
+ Object.defineProperty(exports, "PortalTargetChangedError", { enumerable: true, get: function () { return portal_opener_js_1.PortalTargetChangedError; } });
17
+ Object.defineProperty(exports, "PortalTooManyRedirectsError", { enumerable: true, get: function () { return portal_opener_js_1.PortalTooManyRedirectsError; } });
18
+ Object.defineProperty(exports, "PortalVerificationError", { enumerable: true, get: function () { return portal_opener_js_1.PortalVerificationError; } });
@@ -272,6 +272,13 @@ export interface CreatePortalOptions {
272
272
  * server default; zero is rejected rather than treated as default.
273
273
  */
274
274
  sessionDuration?: string | number;
275
+ /**
276
+ * Scope a qURL for an existing tunnel resource to one path and its
277
+ * descendants. The SDK rejects only empty values and values over 2048 UTF-8
278
+ * bytes; the API remains authoritative for the path grammar and resource
279
+ * type. This option is invalid with `createPortalForUrl`.
280
+ */
281
+ targetPath?: string;
275
282
  }
276
283
  /**
277
284
  * A qURL link minted for a protected resource.
@@ -459,8 +466,9 @@ export type CreateQurlForResourceInput = Omit<CreateInput, "target_url" | "custo
459
466
  * Path this resource qURL resolves to (e.g. "/api/detect").
460
467
  *
461
468
  * Creation-only and valid only for tunnel resources. The SDK rejects empty
462
- * or overlong values before sending a request; server validation rejects
463
- * invalid path grammar, including paths missing a leading "/".
469
+ * values and values over 2048 UTF-8 bytes before sending a request; server
470
+ * validation rejects invalid path grammar, including paths missing a
471
+ * leading "/".
464
472
  */
465
473
  target_path?: string;
466
474
  };
@@ -787,23 +795,6 @@ export interface AccessCodeListOutput extends UnpaginatedOutput {
787
795
  */
788
796
  access_codes: AccessCode[];
789
797
  }
790
- export interface AgentBootstrapInput {
791
- public_key: string;
792
- agent_id?: string;
793
- hostname?: string;
794
- version?: string;
795
- }
796
- export interface NHPServerPeerInfo {
797
- public_key_b64: string;
798
- host: string;
799
- port: number;
800
- expire_time: number;
801
- }
802
- export interface AgentBootstrapOutput {
803
- agent_id: string;
804
- registered_at: string;
805
- nhp_server_peer: NHPServerPeerInfo;
806
- }
807
798
  /** API error from the qURL service (RFC 7807). */
808
799
  export interface QURLErrorData {
809
800
  status: number;
@@ -838,7 +829,13 @@ export interface ClientOptions {
838
829
  apiKey: string;
839
830
  /** Base URL. Defaults to https://api.layerv.ai */
840
831
  baseUrl?: string;
841
- /** Custom fetch implementation. */
832
+ /**
833
+ * Custom fetch implementation. Redirect-safety requires it to honor
834
+ * `redirect: "manual"`, accurately expose `Response.redirected`, and leave
835
+ * `Response.url` empty or set it to the normalized request URL when no
836
+ * redirect was followed. Every Response-like result must also provide a
837
+ * `headers` object with a WHATWG-compatible `headers.get(name)` method.
838
+ */
842
839
  fetch?: typeof globalThis.fetch;
843
840
  /** Maximum retry attempts for transient errors (429, 5xx). Default: 3. */
844
841
  maxRetries?: number;
@@ -1 +1 @@
1
- export declare const VERSION = "0.3.1";
1
+ export declare const VERSION = "0.5.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.3.1"; // x-release-please-version
4
+ exports.VERSION = "0.5.0"; // x-release-please-version