@layervai/qurl 0.3.1 → 0.4.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 +187 -7
  2. package/dist/cjs/client.d.ts +17 -10
  3. package/dist/cjs/client.js +649 -96
  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 +37 -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 +83 -0
  16. package/dist/cjs/node/portal-opener.js +767 -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 +11 -0
  23. package/dist/cjs/types.d.ts +7 -18
  24. package/dist/cjs/version.d.ts +1 -1
  25. package/dist/cjs/version.js +1 -1
  26. package/dist/esm/client.d.ts +17 -10
  27. package/dist/esm/client.d.ts.map +1 -1
  28. package/dist/esm/client.js +647 -94
  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 +38 -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 +84 -0
  53. package/dist/esm/node/portal-opener.d.ts.map +1 -0
  54. package/dist/esm/node/portal-opener.js +757 -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 +7 -18
  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 +25 -6
@@ -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, PortalStateError, PortalVerificationError, } from "./node/portal-opener.js";
2
+ export type { CreatePortalOpenerOptions, PortalFetchOptions, PortalOpener, PortalRenewalFailure, PortalRequestBuilder, PortalSessionHealth, PortalStartOptions, } from "./node/portal-opener.js";
3
+ export type { PortalDeployment, PortalDeploymentCell, PortalDeploymentIssuer, } from "./node/deployment.js";
@@ -0,0 +1,11 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PortalVerificationError = exports.PortalStateError = 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, "PortalStateError", { enumerable: true, get: function () { return portal_opener_js_1.PortalStateError; } });
11
+ Object.defineProperty(exports, "PortalVerificationError", { enumerable: true, get: function () { return portal_opener_js_1.PortalVerificationError; } });
@@ -787,23 +787,6 @@ export interface AccessCodeListOutput extends UnpaginatedOutput {
787
787
  */
788
788
  access_codes: AccessCode[];
789
789
  }
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
790
  /** API error from the qURL service (RFC 7807). */
808
791
  export interface QURLErrorData {
809
792
  status: number;
@@ -838,7 +821,13 @@ export interface ClientOptions {
838
821
  apiKey: string;
839
822
  /** Base URL. Defaults to https://api.layerv.ai */
840
823
  baseUrl?: string;
841
- /** Custom fetch implementation. */
824
+ /**
825
+ * Custom fetch implementation. Redirect-safety requires it to honor
826
+ * `redirect: "manual"`, accurately expose `Response.redirected`, and leave
827
+ * `Response.url` empty or set it to the normalized request URL when no
828
+ * redirect was followed. Every Response-like result must also provide a
829
+ * `headers` object with a WHATWG-compatible `headers.get(name)` method.
830
+ */
842
831
  fetch?: typeof globalThis.fetch;
843
832
  /** Maximum retry attempts for transient errors (429, 5xx). Default: 3. */
844
833
  maxRetries?: number;
@@ -1 +1 @@
1
- export declare const VERSION = "0.3.1";
1
+ export declare const VERSION = "0.4.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.4.0"; // x-release-please-version
@@ -1,4 +1,12 @@
1
- import type { AccessCodeListOutput, AgentBootstrapInput, AgentBootstrapOutput, ApiKey, ApiKeyListOutput, BillingInvoiceListOutput, BatchCreateInput, BatchCreateOutput, ClientOptions, ConnectorInstallation, ConnectorInstallationListOutput, CreateAccessCodeInput, CreateAccessCodeOutput, CreateApiKeyInput, CreateApiKeyOutput, CreateBillingCheckoutInput, CreateInput, CreateOutput, CreatePortalOptions, CreateQurlForResourceInput, CreateResourceInput, CreateWebhookInput, Customer, CheckoutSession, Domain, DomainListOutput, DomainVerifyResult, ExtendInput, Invoice, ListInput, ListOutput, ListApiKeysInput, ListBillingInvoicesInput, ListConnectorInstallationsInput, ListDomainsInput, ListWebhookDeliveriesInput, ListWebhooksInput, MintInput, MintOutput, Portal, PortalSession, ProtectUrlOptions, QURL, QurlSummary, Quota, RedeemAccessCodeInput, RedeemAccessCodeOutput, RegisterDomainInput, Resource, ResourceDetail, ResourceHandle, ResourceListInput, ResourceListOutput, RequestOptions, ResolveInput, ResolveOutput, SessionListOutput, SessionTerminateOutput, UpdateApiKeyInput, UpdateCustomerInput, UpdateInput, UpdateResourceInput, UpdateResourceQurlInput, UpdateWebhookInput, UsageCurrentPeriod, UsageDaily, Webhook, WebhookDelivery, WebhookDeliveryListOutput, WebhookEventTypeInfo, WebhookListOutput, WebhookWithSecret } from "./types.js";
1
+ import type { AccessCodeListOutput, ApiKey, ApiKeyListOutput, BillingInvoiceListOutput, BatchCreateInput, BatchCreateOutput, ClientOptions, ConnectorInstallation, ConnectorInstallationListOutput, CreateAccessCodeInput, CreateAccessCodeOutput, CreateApiKeyInput, CreateApiKeyOutput, CreateBillingCheckoutInput, CreateInput, CreateOutput, CreatePortalOptions, CreateQurlForResourceInput, CreateResourceInput, CreateWebhookInput, Customer, CheckoutSession, Domain, DomainListOutput, DomainVerifyResult, ExtendInput, Invoice, ListInput, ListOutput, ListApiKeysInput, ListBillingInvoicesInput, ListConnectorInstallationsInput, ListDomainsInput, ListWebhookDeliveriesInput, ListWebhooksInput, MintInput, MintOutput, Portal, PortalSession, ProtectUrlOptions, QURL, QurlSummary, Quota, RedeemAccessCodeInput, RedeemAccessCodeOutput, RegisterDomainInput, Resource, ResourceDetail, ResourceHandle, ResourceListInput, ResourceListOutput, RequestOptions, ResolveInput, ResolveOutput, SessionListOutput, SessionTerminateOutput, UpdateApiKeyInput, UpdateCustomerInput, UpdateInput, UpdateResourceInput, UpdateResourceQurlInput, UpdateWebhookInput, UsageCurrentPeriod, UsageDaily, Webhook, WebhookDelivery, WebhookDeliveryListOutput, WebhookEventTypeInfo, WebhookListOutput, WebhookWithSecret } from "./types.js";
2
+ /**
3
+ * Shape of qurl-go's offline signed-fragment links: three or more
4
+ * dot-separated base64url parts (<version>.<claims>.<secret>.<sig>).
5
+ * Detected only to give those links a precise error in extractAccessToken.
6
+ *
7
+ * @internal Exported for in-package tests; not re-exported from index.ts.
8
+ */
9
+ export declare const SIGNED_FRAGMENT_RE: RegExp;
2
10
  /** qURL API client. */
3
11
  export declare class QURLClient {
4
12
  private readonly baseUrl;
@@ -307,8 +315,6 @@ export declare class QURLClient {
307
315
  resolve(input: ResolveInput | string, options?: RequestOptions): Promise<ResolveOutput>;
308
316
  /** Get quota and usage information. */
309
317
  getQuota(): Promise<Quota>;
310
- /** Bootstrap a LayerV qURL Connector agent. */
311
- bootstrapAgent(input: AgentBootstrapInput, options?: RequestOptions): Promise<AgentBootstrapOutput>;
312
318
  /** List resources from the `/v1/resources` API. */
313
319
  listResources(input?: ResourceListInput): Promise<ResourceListOutput>;
314
320
  /**
@@ -349,13 +355,7 @@ export declare class QURLClient {
349
355
  * this page and emits a debug log rather than pretending it fetched all pages.
350
356
  */
351
357
  listResourceSessions(id: string): Promise<SessionListOutput>;
352
- /**
353
- * Terminate all active sessions for a resource.
354
- *
355
- * The returned count is best-effort under retries: if the first DELETE
356
- * succeeds server-side but the response is lost, a retried request may
357
- * return `0` because there are no sessions left to terminate.
358
- */
358
+ /** Terminate all active sessions for a resource and return the server count. */
359
359
  terminateAllResourceSessions(id: string): Promise<SessionTerminateOutput>;
360
360
  /** Terminate a specific resource session. */
361
361
  terminateResourceSession(id: string, sessionId: string): Promise<void>;
@@ -468,6 +468,12 @@ export declare class QURLClient {
468
468
  /** Revoke an access code. */
469
469
  revokeAccessCode(id: string): Promise<void>;
470
470
  private request;
471
+ /**
472
+ * Require the service's exact 204 response with no response bytes exposed by
473
+ * Fetch. A standards-compliant Fetch implementation discards any forbidden
474
+ * wire body on 204, so the SDK cannot detect bytes that Fetch did not expose.
475
+ */
476
+ private requestNoContent;
471
477
  /**
472
478
  * Issue an HTTP request and parse the JSON response.
473
479
  *
@@ -495,6 +501,7 @@ export declare class QURLClient {
495
501
  */
496
502
  private retryDelay;
497
503
  private classifyFetchError;
504
+ private classifyResponseReadError;
498
505
  }
499
506
  /**
500
507
  * A LayerV-protected resource, bound to the client that produced it.