@layervai/qurl 0.3.0 → 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 (75) hide show
  1. package/README.md +340 -23
  2. package/dist/cjs/client.d.ts +180 -13
  3. package/dist/cjs/client.js +1126 -96
  4. package/dist/cjs/errors.d.ts +15 -11
  5. package/dist/cjs/errors.js +30 -21
  6. package/dist/cjs/index.d.ts +3 -3
  7. package/dist/cjs/index.js +4 -1
  8. package/dist/cjs/internal.d.ts +4 -0
  9. package/dist/cjs/internal.js +16 -0
  10. package/dist/cjs/node/deployment.d.ts +44 -0
  11. package/dist/cjs/node/deployment.js +167 -0
  12. package/dist/cjs/node/native-udp.d.ts +37 -0
  13. package/dist/cjs/node/native-udp.js +294 -0
  14. package/dist/cjs/node/nhp-wire.d.ts +35 -0
  15. package/dist/cjs/node/nhp-wire.js +261 -0
  16. package/dist/cjs/node/portal-opener.d.ts +83 -0
  17. package/dist/cjs/node/portal-opener.js +767 -0
  18. package/dist/cjs/node/qv2.d.ts +38 -0
  19. package/dist/cjs/node/qv2.js +292 -0
  20. package/dist/cjs/node/strict-json.d.ts +12 -0
  21. package/dist/cjs/node/strict-json.js +174 -0
  22. package/dist/cjs/node.d.ts +3 -0
  23. package/dist/cjs/node.js +11 -0
  24. package/dist/cjs/types.d.ts +90 -18
  25. package/dist/cjs/version.d.ts +1 -1
  26. package/dist/cjs/version.js +1 -1
  27. package/dist/esm/client.d.ts +180 -13
  28. package/dist/esm/client.d.ts.map +1 -1
  29. package/dist/esm/client.js +1125 -96
  30. package/dist/esm/client.js.map +1 -1
  31. package/dist/esm/errors.d.ts +15 -11
  32. package/dist/esm/errors.d.ts.map +1 -1
  33. package/dist/esm/errors.js +29 -20
  34. package/dist/esm/errors.js.map +1 -1
  35. package/dist/esm/index.d.ts +3 -3
  36. package/dist/esm/index.d.ts.map +1 -1
  37. package/dist/esm/index.js +2 -2
  38. package/dist/esm/index.js.map +1 -1
  39. package/dist/esm/internal.d.ts +5 -0
  40. package/dist/esm/internal.d.ts.map +1 -0
  41. package/dist/esm/internal.js +14 -0
  42. package/dist/esm/internal.js.map +1 -0
  43. package/dist/esm/node/deployment.d.ts +45 -0
  44. package/dist/esm/node/deployment.d.ts.map +1 -0
  45. package/dist/esm/node/deployment.js +163 -0
  46. package/dist/esm/node/deployment.js.map +1 -0
  47. package/dist/esm/node/native-udp.d.ts +38 -0
  48. package/dist/esm/node/native-udp.d.ts.map +1 -0
  49. package/dist/esm/node/native-udp.js +291 -0
  50. package/dist/esm/node/native-udp.js.map +1 -0
  51. package/dist/esm/node/nhp-wire.d.ts +36 -0
  52. package/dist/esm/node/nhp-wire.d.ts.map +1 -0
  53. package/dist/esm/node/nhp-wire.js +257 -0
  54. package/dist/esm/node/nhp-wire.js.map +1 -0
  55. package/dist/esm/node/portal-opener.d.ts +84 -0
  56. package/dist/esm/node/portal-opener.d.ts.map +1 -0
  57. package/dist/esm/node/portal-opener.js +757 -0
  58. package/dist/esm/node/portal-opener.js.map +1 -0
  59. package/dist/esm/node/qv2.d.ts +39 -0
  60. package/dist/esm/node/qv2.d.ts.map +1 -0
  61. package/dist/esm/node/qv2.js +286 -0
  62. package/dist/esm/node/qv2.js.map +1 -0
  63. package/dist/esm/node/strict-json.d.ts +13 -0
  64. package/dist/esm/node/strict-json.d.ts.map +1 -0
  65. package/dist/esm/node/strict-json.js +171 -0
  66. package/dist/esm/node/strict-json.js.map +1 -0
  67. package/dist/esm/node.d.ts +4 -0
  68. package/dist/esm/node.d.ts.map +1 -0
  69. package/dist/esm/node.js +2 -0
  70. package/dist/esm/node.js.map +1 -0
  71. package/dist/esm/types.d.ts +90 -18
  72. package/dist/esm/types.d.ts.map +1 -1
  73. package/dist/esm/version.d.ts +1 -1
  74. package/dist/esm/version.js +1 -1
  75. package/package.json +25 -6
@@ -14,19 +14,21 @@ export declare const ERROR_CODE_UNEXPECTED_RESPONSE = "unexpected_response";
14
14
  export declare const ERROR_CODE_NETWORK = "network_error";
15
15
  export declare const ERROR_CODE_TIMEOUT = "timeout";
16
16
  export declare const ERROR_CODE_RUNTIME = "runtime_error";
17
+ /** `connectorResource` found no resource for the connector id (client-detected, `status: 0`). */
18
+ export declare const ERROR_CODE_RESOURCE_NOT_FOUND = "resource_not_found";
19
+ /** `connectorResource` matched more than one resource where exactly one is required (client-detected, `status: 0`). */
20
+ export declare const ERROR_CODE_AMBIGUOUS_RESOURCE = "ambiguous_resource";
17
21
  /** Fallback `.code` when the server returns a non-RFC-7807 response (HTML proxy page, plaintext gateway error, JSON without `error` envelope). */
18
22
  export declare const ERROR_CODE_UNKNOWN = "unknown";
19
23
  /**
20
24
  * Base error thrown by the qURL API client. Catch this to handle all SDK errors.
21
25
  *
22
- * **`status: 0` convention:** Client-detected failures — validation errors
23
- * (`code: "client_validation"`), unexpected response shapes
24
- * (`code: "unexpected_response"`), runtime capability errors
25
- * (`code: "runtime_error"`), network errors (`code: "network_error"`), and
26
- * timeouts (`code: "timeout"`) — all use `status: 0` because no real HTTP
27
- * status code applies. To distinguish between these cases, branch on `.code`
28
- * rather than `.status`. Non-zero `.status` always reflects a real HTTP
29
- * status from the API (e.g. 400, 401, 429, 500).
26
+ * **`status: 0` convention:** Client-only validation, runtime, network, and
27
+ * timeout failures use `status: 0` because no HTTP status applies. Logical
28
+ * response-shape guards can also use zero. An `unexpected_response` tied to a
29
+ * concrete HTTP contract violation (redirect, oversized body, wrong
30
+ * no-content status/body) instead preserves the observed status. Branch on
31
+ * `.code` first and then `.status`; see {@link ValidationError}.
30
32
  *
31
33
  * **`.code === "unknown"`** is a possible value when the server returns a
32
34
  * non-RFC-7807 response (e.g. a Cloudflare HTML error page, a gateway
@@ -84,9 +86,11 @@ export declare class NotFoundError extends QURLError {
84
86
  * counts/length mismatch, per-entry contract violation): `.status`
85
87
  * is `0`. The HTTP status that produced the bad body is appended
86
88
  * to `.detail` as `(HTTP 400)` / `(HTTP 207)` etc. for diagnostics.
87
- * - Non-JSON body on a 2xx or passthrough status (e.g. proxy HTML
88
- * error page, plaintext gateway error, truncated body): `.status`
89
- * is the actual HTTP status (e.g. `400`, `200`).
89
+ * - Non-JSON, oversized, redirect, or exact-status/body-contract failure on a
90
+ * 2xx, passthrough, or other observed HTTP status: `.status` is the actual
91
+ * HTTP status (e.g. `200`, `302`, `400`, `503`). Filtered opaque redirects
92
+ * in browsers and Node native fetch use `.status === 0` because Fetch does
93
+ * not expose their 3xx status.
90
94
  *
91
95
  * Consumers branching purely on `.status` should branch on `.code`
92
96
  * first, then `.detail` for shape-guard cases. See #59 for tracking
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.RuntimeError = exports.TimeoutError = exports.NetworkError = exports.ServerError = exports.RateLimitError = exports.ValidationError = exports.NotFoundError = exports.AuthorizationError = exports.AuthenticationError = exports.QURLError = exports.ERROR_CODE_UNKNOWN = exports.ERROR_CODE_RUNTIME = exports.ERROR_CODE_TIMEOUT = exports.ERROR_CODE_NETWORK = exports.ERROR_CODE_UNEXPECTED_RESPONSE = exports.ERROR_CODE_CLIENT_VALIDATION = void 0;
3
+ exports.RuntimeError = exports.TimeoutError = exports.NetworkError = exports.ServerError = exports.RateLimitError = exports.ValidationError = exports.NotFoundError = exports.AuthorizationError = exports.AuthenticationError = exports.QURLError = exports.ERROR_CODE_UNKNOWN = exports.ERROR_CODE_AMBIGUOUS_RESOURCE = exports.ERROR_CODE_RESOURCE_NOT_FOUND = exports.ERROR_CODE_RUNTIME = exports.ERROR_CODE_TIMEOUT = exports.ERROR_CODE_NETWORK = exports.ERROR_CODE_UNEXPECTED_RESPONSE = exports.ERROR_CODE_CLIENT_VALIDATION = void 0;
4
4
  exports.createError = createError;
5
5
  /**
6
6
  * Sentinel `.code` values for SDK-internal failure modes (no real HTTP
@@ -17,19 +17,21 @@ exports.ERROR_CODE_UNEXPECTED_RESPONSE = "unexpected_response";
17
17
  exports.ERROR_CODE_NETWORK = "network_error";
18
18
  exports.ERROR_CODE_TIMEOUT = "timeout";
19
19
  exports.ERROR_CODE_RUNTIME = "runtime_error";
20
+ /** `connectorResource` found no resource for the connector id (client-detected, `status: 0`). */
21
+ exports.ERROR_CODE_RESOURCE_NOT_FOUND = "resource_not_found";
22
+ /** `connectorResource` matched more than one resource where exactly one is required (client-detected, `status: 0`). */
23
+ exports.ERROR_CODE_AMBIGUOUS_RESOURCE = "ambiguous_resource";
20
24
  /** Fallback `.code` when the server returns a non-RFC-7807 response (HTML proxy page, plaintext gateway error, JSON without `error` envelope). */
21
25
  exports.ERROR_CODE_UNKNOWN = "unknown";
22
26
  /**
23
27
  * Base error thrown by the qURL API client. Catch this to handle all SDK errors.
24
28
  *
25
- * **`status: 0` convention:** Client-detected failures — validation errors
26
- * (`code: "client_validation"`), unexpected response shapes
27
- * (`code: "unexpected_response"`), runtime capability errors
28
- * (`code: "runtime_error"`), network errors (`code: "network_error"`), and
29
- * timeouts (`code: "timeout"`) — all use `status: 0` because no real HTTP
30
- * status code applies. To distinguish between these cases, branch on `.code`
31
- * rather than `.status`. Non-zero `.status` always reflects a real HTTP
32
- * status from the API (e.g. 400, 401, 429, 500).
29
+ * **`status: 0` convention:** Client-only validation, runtime, network, and
30
+ * timeout failures use `status: 0` because no HTTP status applies. Logical
31
+ * response-shape guards can also use zero. An `unexpected_response` tied to a
32
+ * concrete HTTP contract violation (redirect, oversized body, wrong
33
+ * no-content status/body) instead preserves the observed status. Branch on
34
+ * `.code` first and then `.status`; see {@link ValidationError}.
33
35
  *
34
36
  * **`.code === "unknown"`** is a possible value when the server returns a
35
37
  * non-RFC-7807 response (e.g. a Cloudflare HTML error page, a gateway
@@ -115,9 +117,11 @@ exports.NotFoundError = NotFoundError;
115
117
  * counts/length mismatch, per-entry contract violation): `.status`
116
118
  * is `0`. The HTTP status that produced the bad body is appended
117
119
  * to `.detail` as `(HTTP 400)` / `(HTTP 207)` etc. for diagnostics.
118
- * - Non-JSON body on a 2xx or passthrough status (e.g. proxy HTML
119
- * error page, plaintext gateway error, truncated body): `.status`
120
- * is the actual HTTP status (e.g. `400`, `200`).
120
+ * - Non-JSON, oversized, redirect, or exact-status/body-contract failure on a
121
+ * 2xx, passthrough, or other observed HTTP status: `.status` is the actual
122
+ * HTTP status (e.g. `200`, `302`, `400`, `503`). Filtered opaque redirects
123
+ * in browsers and Node native fetch use `.status === 0` because Fetch does
124
+ * not expose their 3xx status.
121
125
  *
122
126
  * Consumers branching purely on `.status` should branch on `.code`
123
127
  * first, then `.detail` for shape-guard cases. See #59 for tracking
@@ -146,14 +150,23 @@ class ServerError extends QURLError {
146
150
  }
147
151
  }
148
152
  exports.ServerError = ServerError;
153
+ function attachErrorCause(error, options) {
154
+ if (!options || !("cause" in options))
155
+ return;
156
+ // Match the native Error `cause` descriptor. In particular, keep transport
157
+ // objects out of Object.keys(), object spread, and JSON logging.
158
+ Object.defineProperty(error, "cause", {
159
+ value: options.cause,
160
+ writable: true,
161
+ configurable: true,
162
+ });
163
+ }
149
164
  /** Transport-level error — DNS failure, connection refused, etc. */
150
165
  class NetworkError extends QURLError {
151
166
  constructor(message, options) {
152
167
  super({ status: 0, code: exports.ERROR_CODE_NETWORK, title: "Network Error", detail: message });
153
168
  this.name = "NetworkError";
154
- if (options?.cause) {
155
- this.cause = options.cause;
156
- }
169
+ attachErrorCause(this, options);
157
170
  }
158
171
  }
159
172
  exports.NetworkError = NetworkError;
@@ -162,9 +175,7 @@ class TimeoutError extends QURLError {
162
175
  constructor(message = "Request timed out", options) {
163
176
  super({ status: 0, code: exports.ERROR_CODE_TIMEOUT, title: "Timeout", detail: message });
164
177
  this.name = "TimeoutError";
165
- if (options?.cause) {
166
- this.cause = options.cause;
167
- }
178
+ attachErrorCause(this, options);
168
179
  }
169
180
  }
170
181
  exports.TimeoutError = TimeoutError;
@@ -173,9 +184,7 @@ class RuntimeError extends QURLError {
173
184
  constructor(message, options) {
174
185
  super({ status: 0, code: exports.ERROR_CODE_RUNTIME, title: "Runtime Error", detail: message });
175
186
  this.name = "RuntimeError";
176
- if (options && "cause" in options) {
177
- this.cause = options.cause;
178
- }
187
+ attachErrorCause(this, options);
179
188
  }
180
189
  }
181
190
  exports.RuntimeError = RuntimeError;
@@ -1,4 +1,4 @@
1
- export { QURLClient } from "./client.js";
2
- export { AuthenticationError, AuthorizationError, createError, ERROR_CODE_CLIENT_VALIDATION, ERROR_CODE_NETWORK, ERROR_CODE_RUNTIME, ERROR_CODE_TIMEOUT, ERROR_CODE_UNEXPECTED_RESPONSE, ERROR_CODE_UNKNOWN, NetworkError, NotFoundError, QURLError, RateLimitError, RuntimeError, ServerError, TimeoutError, ValidationError, } from "./errors.js";
1
+ export { ProtectedResource, QURLClient } from "./client.js";
2
+ export { AuthenticationError, AuthorizationError, createError, ERROR_CODE_AMBIGUOUS_RESOURCE, ERROR_CODE_CLIENT_VALIDATION, ERROR_CODE_NETWORK, ERROR_CODE_RESOURCE_NOT_FOUND, ERROR_CODE_RUNTIME, ERROR_CODE_TIMEOUT, ERROR_CODE_UNEXPECTED_RESPONSE, ERROR_CODE_UNKNOWN, NetworkError, NotFoundError, QURLError, RateLimitError, RuntimeError, ServerError, TimeoutError, ValidationError, } from "./errors.js";
3
3
  export { VERSION } from "./version.js";
4
- export type { AccessCode, AccessCodeListOutput, AccessGrant, AccessPolicy, AccessToken, AgentBootstrapInput, AgentBootstrapOutput, AIAgentCategory, AIAgentPolicy, ApiKey, ApiKeyListOutput, ApiKeyScope, BatchCreateInput, BatchCreateOutput, BatchItemFailure, BatchItemResult, BatchItemSuccess, BillingInvoiceListOutput, ClientOptions, ConnectorInstallation, ConnectorInstallationCapabilities, ConnectorInstallationListOutput, ConnectorInstallationStats, CreateAccessCodeInput, CreateAccessCodeOutput, CreateApiKeyInput, CreateApiKeyOutput, CreateBillingCheckoutInput, CreateInput, CreateOutput, CreateQurlForResourceInput, CreateResourceInput, CreateWebhookInput, Customer, CheckoutSession, DnsRecord, Domain, DomainCheckDetail, DomainListOutput, DomainVerifyResult, ExtendInput, Invoice, ListApiKeysInput, ListBillingInvoicesInput, ListConnectorInstallationsInput, ListDomainsInput, ListInput, ListOutput, ListWebhooksInput, ListWebhookDeliveriesInput, Meta, MintInput, MintOutput, NHPServerPeerInfo, PaginatedOutput, PortalSession, QURL, QURLErrorData, QurlSummary, Quota, RedeemAccessCodeInput, RedeemAccessCodeOutput, RegisterDomainInput, Resource, ResourceDetail, ResourceListInput, ResourceListOutput, ResourceType, RequestOptions, ResolveInput, ResolveOutput, Session, SessionListOutput, SessionTerminateOutput, TombstoneInfo, UnpaginatedOutput, UpdateApiKeyInput, UpdateCustomerInput, UpdateInput, UpdateResourceInput, UpdateResourceQurlInput, UpdateWebhookInput, UsageCostEstimate, UsageCurrentPeriod, UsageDaily, UsageDailyEntry, Webhook, WebhookDelivery, WebhookDeliveryListOutput, WebhookEventType, WebhookEventTypeInfo, WebhookListOutput, WebhookPayload, WebhookWithSecret, } from "./types.js";
4
+ export type { AccessCode, AccessCodeListOutput, AccessGrant, AccessPolicy, AccessToken, AIAgentCategory, AIAgentPolicy, ApiKey, ApiKeyListOutput, ApiKeyScope, BatchCreateInput, BatchCreateOutput, BatchItemFailure, BatchItemResult, BatchItemSuccess, BillingInvoiceListOutput, ClientOptions, ConnectorInstallation, ConnectorInstallationCapabilities, ConnectorInstallationListOutput, ConnectorInstallationStats, CreateAccessCodeInput, CreateAccessCodeOutput, CreateApiKeyInput, CreateApiKeyOutput, CreateBillingCheckoutInput, CreateInput, CreateOutput, CreatePortalOptions, CreateQurlForResourceInput, CreateResourceInput, CreateWebhookInput, Customer, CheckoutSession, DnsRecord, Domain, DomainCheckDetail, DomainListOutput, DomainVerifyResult, ExtendInput, Invoice, ListApiKeysInput, ListBillingInvoicesInput, ListConnectorInstallationsInput, ListDomainsInput, ListInput, ListOutput, ListWebhooksInput, ListWebhookDeliveriesInput, Meta, MintInput, MintOutput, PaginatedOutput, Portal, PortalSession, ProtectUrlOptions, QURL, QURLErrorData, QurlSummary, Quota, RedeemAccessCodeInput, RedeemAccessCodeOutput, RegisterDomainInput, Resource, ResourceDetail, ResourceHandle, ResourceListInput, ResourceListOutput, ResourceType, RequestOptions, ResolveInput, ResolveOutput, Session, SessionListOutput, SessionTerminateOutput, TombstoneInfo, UnpaginatedOutput, UpdateApiKeyInput, UpdateCustomerInput, UpdateInput, UpdateResourceInput, UpdateResourceQurlInput, UpdateWebhookInput, UsageCostEstimate, UsageCurrentPeriod, UsageDaily, UsageDailyEntry, Webhook, WebhookDelivery, WebhookDeliveryListOutput, WebhookEventType, WebhookEventTypeInfo, WebhookListOutput, WebhookPayload, WebhookWithSecret, } from "./types.js";
package/dist/cjs/index.js CHANGED
@@ -1,14 +1,17 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.VERSION = exports.ValidationError = exports.TimeoutError = exports.ServerError = exports.RuntimeError = exports.RateLimitError = exports.QURLError = exports.NotFoundError = exports.NetworkError = exports.ERROR_CODE_UNKNOWN = exports.ERROR_CODE_UNEXPECTED_RESPONSE = exports.ERROR_CODE_TIMEOUT = exports.ERROR_CODE_RUNTIME = exports.ERROR_CODE_NETWORK = exports.ERROR_CODE_CLIENT_VALIDATION = exports.createError = exports.AuthorizationError = exports.AuthenticationError = exports.QURLClient = void 0;
3
+ exports.VERSION = exports.ValidationError = exports.TimeoutError = exports.ServerError = exports.RuntimeError = exports.RateLimitError = exports.QURLError = exports.NotFoundError = exports.NetworkError = exports.ERROR_CODE_UNKNOWN = exports.ERROR_CODE_UNEXPECTED_RESPONSE = exports.ERROR_CODE_TIMEOUT = exports.ERROR_CODE_RUNTIME = exports.ERROR_CODE_RESOURCE_NOT_FOUND = exports.ERROR_CODE_NETWORK = exports.ERROR_CODE_CLIENT_VALIDATION = exports.ERROR_CODE_AMBIGUOUS_RESOURCE = exports.createError = exports.AuthorizationError = exports.AuthenticationError = exports.QURLClient = exports.ProtectedResource = void 0;
4
4
  var client_js_1 = require("./client.js");
5
+ Object.defineProperty(exports, "ProtectedResource", { enumerable: true, get: function () { return client_js_1.ProtectedResource; } });
5
6
  Object.defineProperty(exports, "QURLClient", { enumerable: true, get: function () { return client_js_1.QURLClient; } });
6
7
  var errors_js_1 = require("./errors.js");
7
8
  Object.defineProperty(exports, "AuthenticationError", { enumerable: true, get: function () { return errors_js_1.AuthenticationError; } });
8
9
  Object.defineProperty(exports, "AuthorizationError", { enumerable: true, get: function () { return errors_js_1.AuthorizationError; } });
9
10
  Object.defineProperty(exports, "createError", { enumerable: true, get: function () { return errors_js_1.createError; } });
11
+ Object.defineProperty(exports, "ERROR_CODE_AMBIGUOUS_RESOURCE", { enumerable: true, get: function () { return errors_js_1.ERROR_CODE_AMBIGUOUS_RESOURCE; } });
10
12
  Object.defineProperty(exports, "ERROR_CODE_CLIENT_VALIDATION", { enumerable: true, get: function () { return errors_js_1.ERROR_CODE_CLIENT_VALIDATION; } });
11
13
  Object.defineProperty(exports, "ERROR_CODE_NETWORK", { enumerable: true, get: function () { return errors_js_1.ERROR_CODE_NETWORK; } });
14
+ Object.defineProperty(exports, "ERROR_CODE_RESOURCE_NOT_FOUND", { enumerable: true, get: function () { return errors_js_1.ERROR_CODE_RESOURCE_NOT_FOUND; } });
12
15
  Object.defineProperty(exports, "ERROR_CODE_RUNTIME", { enumerable: true, get: function () { return errors_js_1.ERROR_CODE_RUNTIME; } });
13
16
  Object.defineProperty(exports, "ERROR_CODE_TIMEOUT", { enumerable: true, get: function () { return errors_js_1.ERROR_CODE_TIMEOUT; } });
14
17
  Object.defineProperty(exports, "ERROR_CODE_UNEXPECTED_RESPONSE", { enumerable: true, get: function () { return errors_js_1.ERROR_CODE_UNEXPECTED_RESPONSE; } });
@@ -0,0 +1,4 @@
1
+ /** Strips trailing "/" characters. A manual scan rather than `replace(/\/+$/, "")`
2
+ * so it is linear on any input (the regex form is polynomial-ReDoS on a long run
3
+ * of slashes over caller-supplied input, e.g. a `baseUrl` option). */
4
+ export declare function stripTrailingSlashes(url: string): string;
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+ // Small internal string helpers shared across the SDK. Not exported from the
3
+ // package barrel (`index.ts`) and unreachable via the `exports` map, so this is
4
+ // private surface.
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.stripTrailingSlashes = stripTrailingSlashes;
7
+ /** Strips trailing "/" characters. A manual scan rather than `replace(/\/+$/, "")`
8
+ * so it is linear on any input (the regex form is polynomial-ReDoS on a long run
9
+ * of slashes over caller-supplied input, e.g. a `baseUrl` option). */
10
+ function stripTrailingSlashes(url) {
11
+ let end = url.length;
12
+ while (end > 0 && url.charCodeAt(end - 1) === 47 /* "/" */) {
13
+ end--;
14
+ }
15
+ return end === url.length ? url : url.slice(0, end);
16
+ }
@@ -0,0 +1,44 @@
1
+ import type { KeyObject } from "node:crypto";
2
+ import { type StrictJsonValue } from "./strict-json.js";
3
+ export interface PortalDeployment {
4
+ readonly issuers: readonly PortalDeploymentIssuer[];
5
+ readonly cells: readonly PortalDeploymentCell[];
6
+ }
7
+ export interface PortalDeploymentIssuer {
8
+ readonly kid: string;
9
+ readonly spki_der_b64: string;
10
+ }
11
+ export interface PortalDeploymentCell {
12
+ readonly cell_id?: string;
13
+ readonly host: string;
14
+ readonly port: number;
15
+ readonly server_public_key_b64: string;
16
+ }
17
+ export interface ValidatedDeployment {
18
+ readonly issuers: ReadonlyMap<string, KeyObject>;
19
+ readonly cells: ReadonlyMap<string, ValidatedCell>;
20
+ }
21
+ export interface ValidatedCell {
22
+ readonly host: string;
23
+ readonly port: 443;
24
+ readonly serverPublicKey: Uint8Array;
25
+ }
26
+ interface DeploymentFileRuntime {
27
+ readonly open: (path: string, flags: number) => number;
28
+ readonly stat: (descriptor: number) => {
29
+ readonly size: number;
30
+ isFile(): boolean;
31
+ };
32
+ readonly read: (descriptor: number, buffer: Buffer, offset: number, length: number, position: number) => number;
33
+ readonly close: (descriptor: number) => void;
34
+ }
35
+ /** Load and validate deployment trust once, before an opener can do network I/O. */
36
+ export declare function loadPortalDeployment(explicit?: PortalDeployment): ValidatedDeployment;
37
+ declare function readBoundedDeploymentFile(path: string, runtime?: DeploymentFileRuntime): Buffer;
38
+ export declare function fingerprintKey(key: Uint8Array): string;
39
+ declare function decodeFlexibleBase64(value: StrictJsonValue | undefined, name: string): Uint8Array;
40
+ export declare const deploymentTesting: {
41
+ readBoundedDeploymentFile: typeof readBoundedDeploymentFile;
42
+ decodeFlexibleBase64: typeof decodeFlexibleBase64;
43
+ };
44
+ export {};
@@ -0,0 +1,167 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.deploymentTesting = void 0;
4
+ exports.loadPortalDeployment = loadPortalDeployment;
5
+ exports.fingerprintKey = fingerprintKey;
6
+ const node_fs_1 = require("node:fs");
7
+ const node_crypto_1 = require("node:crypto");
8
+ const strict_json_js_1 = require("./strict-json.js");
9
+ const qv2_js_1 = require("./qv2.js");
10
+ const DEPLOYMENT_KEYS = new Set(["issuers", "cells", "relay_allowlist", "hub"]);
11
+ const ISSUER_KEYS = new Set(["kid", "spki_der_b64"]);
12
+ const CELL_KEYS = new Set(["cell_id", "host", "port", "server_public_key_b64"]);
13
+ const MAX_DEPLOYMENT_BYTES = 1_048_576;
14
+ const defaultDeploymentFileRuntime = {
15
+ open: node_fs_1.openSync,
16
+ stat: node_fs_1.fstatSync,
17
+ read: node_fs_1.readSync,
18
+ close: node_fs_1.closeSync,
19
+ };
20
+ /** Load and validate deployment trust once, before an opener can do network I/O. */
21
+ function loadPortalDeployment(explicit) {
22
+ if (explicit !== undefined)
23
+ return validateDeployment(explicit);
24
+ const configured = process.env.QURL_DEPLOYMENT;
25
+ if (!configured || configured.trim() === "") {
26
+ throw new Error("native qURL opening requires deployment trust in QURL_DEPLOYMENT");
27
+ }
28
+ const trimmed = configured.trim();
29
+ const raw = trimmed.startsWith("{")
30
+ ? Buffer.from(trimmed, "utf8")
31
+ : readBoundedDeploymentFile(trimmed);
32
+ return validateDeploymentValue((0, strict_json_js_1.parseStrictJson)(raw, MAX_DEPLOYMENT_BYTES));
33
+ }
34
+ function readBoundedDeploymentFile(path, runtime = defaultDeploymentFileRuntime) {
35
+ // O_NONBLOCK prevents a FIFO or device path from hanging construction before
36
+ // fstat can enforce the regular-file contract. It has no effect on files.
37
+ const descriptor = runtime.open(path, node_fs_1.constants.O_RDONLY | node_fs_1.constants.O_NONBLOCK);
38
+ try {
39
+ const metadata = runtime.stat(descriptor);
40
+ if (!metadata.isFile())
41
+ throw new Error("native qURL deployment path must be a regular file");
42
+ if (!Number.isSafeInteger(metadata.size) || metadata.size > MAX_DEPLOYMENT_BYTES) {
43
+ throw new Error("native qURL deployment file exceeds its 1 MiB limit");
44
+ }
45
+ const bytes = Buffer.alloc(metadata.size);
46
+ let offset = 0;
47
+ while (offset < bytes.byteLength) {
48
+ const count = runtime.read(descriptor, bytes, offset, bytes.byteLength - offset, offset);
49
+ if (count === 0) {
50
+ throw new Error("native qURL deployment file changed while it was read");
51
+ }
52
+ offset += count;
53
+ }
54
+ const extra = Buffer.alloc(1);
55
+ if (runtime.read(descriptor, extra, 0, 1, offset) !== 0) {
56
+ throw new Error("native qURL deployment file changed while it was read");
57
+ }
58
+ return bytes.subarray(0, offset);
59
+ }
60
+ finally {
61
+ runtime.close(descriptor);
62
+ }
63
+ }
64
+ function validateDeployment(explicit) {
65
+ // Convert through JSON only to give the programmatic form the same strict,
66
+ // immutable validation path as the environment/file form. Deployment data is
67
+ // public trust material, not a secret.
68
+ let encoded;
69
+ try {
70
+ encoded = JSON.stringify(explicit);
71
+ }
72
+ catch {
73
+ throw new Error("native qURL deployment is not JSON serializable");
74
+ }
75
+ if (encoded === undefined)
76
+ throw new Error("native qURL deployment is missing");
77
+ return validateDeploymentValue((0, strict_json_js_1.parseStrictJson)(Buffer.from(encoded, "utf8"), MAX_DEPLOYMENT_BYTES));
78
+ }
79
+ function validateDeploymentValue(value) {
80
+ const root = requireObject(value, "deployment");
81
+ rejectUnknown(root, DEPLOYMENT_KEYS, "deployment");
82
+ const issuerRows = requireArray(root.issuers, "deployment issuers");
83
+ const cellRows = requireArray(root.cells, "deployment cells");
84
+ if (issuerRows.length === 0)
85
+ throw new Error("deployment must contain at least one issuer");
86
+ if (cellRows.length === 0)
87
+ throw new Error("native-only deployment must contain at least one cell");
88
+ const issuers = new Map();
89
+ for (const row of issuerRows) {
90
+ const item = requireObject(row, "deployment issuer");
91
+ rejectUnknown(item, ISSUER_KEYS, "deployment issuer");
92
+ const kid = requireTrimmed(item.kid, "deployment issuer kid");
93
+ if (issuers.has(kid))
94
+ throw new Error("deployment contains a duplicate issuer kid");
95
+ issuers.set(kid, (0, qv2_js_1.issuerKeyFromSpki)(decodeFlexibleBase64(item.spki_der_b64, "issuer key")));
96
+ }
97
+ const cells = new Map();
98
+ for (const row of cellRows) {
99
+ const item = requireObject(row, "deployment cell");
100
+ rejectUnknown(item, CELL_KEYS, "deployment cell");
101
+ const host = requireTrimmed(item.host, "deployment cell host");
102
+ if (item.port !== 443n)
103
+ throw new Error("deployment cell must use NHP UDP port 443");
104
+ const serverPublicKey = decodeFlexibleBase64(item.server_public_key_b64, "cell public key");
105
+ if (serverPublicKey.byteLength !== 32)
106
+ throw new Error("cell public key must be 32 bytes");
107
+ const fingerprint = fingerprintKey(serverPublicKey);
108
+ if (cells.has(fingerprint))
109
+ throw new Error("deployment contains a duplicate cell public key");
110
+ // Validate cell_id for schema parity, but do not use it as identity. The
111
+ // full X25519 public key is the cryptographic cell binding.
112
+ if (item.cell_id !== undefined)
113
+ requireString(item.cell_id, "cell id");
114
+ cells.set(fingerprint, { host, port: 443, serverPublicKey });
115
+ }
116
+ return { issuers, cells };
117
+ }
118
+ function fingerprintKey(key) {
119
+ return (0, node_crypto_1.createHash)("sha256").update(key).digest().subarray(0, 8).toString("base64url");
120
+ }
121
+ function decodeFlexibleBase64(value, name) {
122
+ const encoded = requireTrimmed(value, name);
123
+ const hasStandard = /[+/]/.test(encoded);
124
+ const hasUrl = /[-_]/.test(encoded);
125
+ if (hasStandard && hasUrl)
126
+ throw new Error(`${name} mixes base64 alphabets`);
127
+ if (!/^(?:[A-Za-z0-9+/_-]{4})*(?:[A-Za-z0-9+/_-]{2}==|[A-Za-z0-9+/_-]{3}=)?$/.test(encoded)) {
128
+ // Also permit the unpadded two/three-character tail used by Raw encodings.
129
+ if (!/^[A-Za-z0-9+/_-]+$/.test(encoded) || encoded.length % 4 === 1) {
130
+ throw new Error(`${name} is not valid base64`);
131
+ }
132
+ }
133
+ const normalized = encoded.replace(/-/g, "+").replace(/_/g, "/").replace(/=+$/, "");
134
+ const decoded = Buffer.from(normalized, "base64");
135
+ const canonical = decoded.toString("base64").replace(/=+$/, "");
136
+ if (canonical !== normalized)
137
+ throw new Error(`${name} is not canonical base64`);
138
+ return new Uint8Array(decoded);
139
+ }
140
+ function requireObject(value, name) {
141
+ if (!(0, strict_json_js_1.isStrictJsonObject)(value))
142
+ throw new Error(`${name} must be one JSON object`);
143
+ return value;
144
+ }
145
+ function requireArray(value, name) {
146
+ if (!Array.isArray(value))
147
+ throw new Error(`${name} must be an array`);
148
+ return value;
149
+ }
150
+ function requireString(value, name) {
151
+ if (typeof value !== "string")
152
+ throw new Error(`${name} must be a string`);
153
+ return value;
154
+ }
155
+ function requireTrimmed(value, name) {
156
+ const string = requireString(value, name);
157
+ if (string === "" || string.trim() !== string)
158
+ throw new Error(`${name} must be non-empty and trimmed`);
159
+ return string;
160
+ }
161
+ function rejectUnknown(value, allowed, name) {
162
+ for (const key of Object.keys(value)) {
163
+ if (!allowed.has(key))
164
+ throw new Error(`${name} contains an unknown field`);
165
+ }
166
+ }
167
+ exports.deploymentTesting = { readBoundedDeploymentFile, decodeFlexibleBase64 };
@@ -0,0 +1,37 @@
1
+ import { lookup } from "node:dns/promises";
2
+ import { type Socket } from "node:dgram";
3
+ import { buildNHPMessage, decryptNHPReply, type NHPMessage } from "./nhp-wire.js";
4
+ import type { ValidatedCell } from "./deployment.js";
5
+ export declare const NATIVE_DEFAULT_TIMEOUT_MS = 3000;
6
+ export declare const NATIVE_DEFAULT_MAX_ADDRESSES = 3;
7
+ export interface NativeExchangeOptions {
8
+ readonly signal?: AbortSignal;
9
+ /** Per-address timeout; serial fallback can use maxAddresses times this value. */
10
+ readonly timeoutMs?: number;
11
+ /** Maximum number of resolved addresses tried serially. */
12
+ readonly maxAddresses?: number;
13
+ }
14
+ interface NativeKnockRuntime {
15
+ readonly buildMessage: typeof buildNHPMessage;
16
+ readonly resolveAddresses: typeof resolvePublicAddresses;
17
+ readonly exchange: typeof exchangeDatagram;
18
+ readonly decryptReply: typeof decryptNHPReply;
19
+ }
20
+ export declare function nativeKnock(cell: ValidatedCell, devicePrivateKey: Uint8Array, body: Uint8Array, options?: NativeExchangeOptions): Promise<NHPMessage>;
21
+ declare function nativeKnockWithRuntime(cell: ValidatedCell, devicePrivateKey: Uint8Array, body: Uint8Array, options: NativeExchangeOptions, runtime: NativeKnockRuntime): Promise<NHPMessage>;
22
+ declare function resolvePublicAddresses(host: string, maximum: number, signal?: AbortSignal, lookupAddresses?: typeof lookup): Promise<Array<{
23
+ address: string;
24
+ family: 4 | 6;
25
+ }>>;
26
+ declare function isPublicAddress(address: string, family: number): boolean;
27
+ declare function exchangeDatagram(address: string, family: 4 | 6, port: number, packet: Uint8Array, timeoutMs: number, signal?: AbortSignal): Promise<Uint8Array>;
28
+ declare function sendOwnedDatagram(socket: Socket, packet: Uint8Array, finish: (error: unknown) => void): void;
29
+ export declare const nativeUdpTesting: {
30
+ isPublicAddress: typeof isPublicAddress;
31
+ resolvePublicAddresses: typeof resolvePublicAddresses;
32
+ nativeKnockWithRuntime: typeof nativeKnockWithRuntime;
33
+ exchangeDatagram: typeof exchangeDatagram;
34
+ sendOwnedDatagram: typeof sendOwnedDatagram;
35
+ socketExchangeError: (cause: unknown) => Error;
36
+ };
37
+ export {};