@layervai/qurl 0.7.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. package/README.md +212 -1
  2. package/dist/cjs/client.d.ts +19 -1
  3. package/dist/cjs/client.js +520 -9
  4. package/dist/cjs/crid.d.ts +2 -1
  5. package/dist/cjs/crid.js +11 -2
  6. package/dist/cjs/errors.d.ts +20 -3
  7. package/dist/cjs/errors.js +31 -4
  8. package/dist/cjs/index.d.ts +2 -2
  9. package/dist/cjs/index.js +3 -1
  10. package/dist/cjs/node/agent-runtime.d.ts +108 -0
  11. package/dist/cjs/node/agent-runtime.js +952 -0
  12. package/dist/cjs/node/agent-state.d.ts +100 -0
  13. package/dist/cjs/node/agent-state.js +311 -0
  14. package/dist/cjs/node/agent-transport.d.ts +17 -0
  15. package/dist/cjs/node/agent-transport.js +118 -0
  16. package/dist/cjs/node/deployment.d.ts +6 -0
  17. package/dist/cjs/node/deployment.js +24 -3
  18. package/dist/cjs/node/file-agent-state.d.ts +15 -0
  19. package/dist/cjs/node/file-agent-state.js +137 -0
  20. package/dist/cjs/node/native-loader.cjs +7 -0
  21. package/dist/cjs/node/native-loader.d.cts +11 -0
  22. package/dist/cjs/node/native-udp.d.ts +8 -3
  23. package/dist/cjs/node/native-udp.js +8 -3
  24. package/dist/cjs/node/nhp-wire.d.ts +9 -0
  25. package/dist/cjs/node/nhp-wire.js +24 -7
  26. package/dist/cjs/node/portal-opener.d.ts +7 -0
  27. package/dist/cjs/node/portal-opener.js +28 -6
  28. package/dist/cjs/node/provider.d.ts +19 -0
  29. package/dist/cjs/node/provider.js +153 -0
  30. package/dist/cjs/node/qv2.d.ts +3 -1
  31. package/dist/cjs/node/qv2.js +16 -2
  32. package/dist/cjs/node/relay.d.ts +12 -0
  33. package/dist/cjs/node/relay.js +111 -0
  34. package/dist/cjs/node/sealed-agent-state.d.ts +19 -0
  35. package/dist/cjs/node/sealed-agent-state.js +193 -0
  36. package/dist/cjs/node/strict-json.d.ts +1 -1
  37. package/dist/cjs/node/strict-json.js +8 -3
  38. package/dist/cjs/node.d.ts +14 -0
  39. package/dist/cjs/node.js +24 -1
  40. package/dist/cjs/types.d.ts +97 -0
  41. package/dist/cjs/version.d.ts +1 -1
  42. package/dist/cjs/version.js +1 -1
  43. package/dist/esm/client.d.ts +19 -1
  44. package/dist/esm/client.d.ts.map +1 -1
  45. package/dist/esm/client.js +521 -10
  46. package/dist/esm/client.js.map +1 -1
  47. package/dist/esm/crid.d.ts +2 -1
  48. package/dist/esm/crid.d.ts.map +1 -1
  49. package/dist/esm/crid.js +10 -2
  50. package/dist/esm/crid.js.map +1 -1
  51. package/dist/esm/errors.d.ts +20 -3
  52. package/dist/esm/errors.d.ts.map +1 -1
  53. package/dist/esm/errors.js +29 -3
  54. package/dist/esm/errors.js.map +1 -1
  55. package/dist/esm/index.d.ts +2 -2
  56. package/dist/esm/index.d.ts.map +1 -1
  57. package/dist/esm/index.js +1 -1
  58. package/dist/esm/index.js.map +1 -1
  59. package/dist/esm/node/agent-runtime.d.ts +109 -0
  60. package/dist/esm/node/agent-runtime.d.ts.map +1 -0
  61. package/dist/esm/node/agent-runtime.js +946 -0
  62. package/dist/esm/node/agent-runtime.js.map +1 -0
  63. package/dist/esm/node/agent-state.d.ts +101 -0
  64. package/dist/esm/node/agent-state.d.ts.map +1 -0
  65. package/dist/esm/node/agent-state.js +298 -0
  66. package/dist/esm/node/agent-state.js.map +1 -0
  67. package/dist/esm/node/agent-transport.d.ts +18 -0
  68. package/dist/esm/node/agent-transport.d.ts.map +1 -0
  69. package/dist/esm/node/agent-transport.js +114 -0
  70. package/dist/esm/node/agent-transport.js.map +1 -0
  71. package/dist/esm/node/deployment.d.ts +6 -0
  72. package/dist/esm/node/deployment.d.ts.map +1 -1
  73. package/dist/esm/node/deployment.js +23 -3
  74. package/dist/esm/node/deployment.js.map +1 -1
  75. package/dist/esm/node/file-agent-state.d.ts +16 -0
  76. package/dist/esm/node/file-agent-state.d.ts.map +1 -0
  77. package/dist/esm/node/file-agent-state.js +134 -0
  78. package/dist/esm/node/file-agent-state.js.map +1 -0
  79. package/dist/esm/node/native-loader.cjs +7 -0
  80. package/dist/esm/node/native-loader.d.cts +11 -0
  81. package/dist/esm/node/native-udp.d.ts +8 -3
  82. package/dist/esm/node/native-udp.d.ts.map +1 -1
  83. package/dist/esm/node/native-udp.js +7 -5
  84. package/dist/esm/node/native-udp.js.map +1 -1
  85. package/dist/esm/node/nhp-wire.d.ts +9 -0
  86. package/dist/esm/node/nhp-wire.d.ts.map +1 -1
  87. package/dist/esm/node/nhp-wire.js +23 -6
  88. package/dist/esm/node/nhp-wire.js.map +1 -1
  89. package/dist/esm/node/portal-opener.d.ts +7 -0
  90. package/dist/esm/node/portal-opener.d.ts.map +1 -1
  91. package/dist/esm/node/portal-opener.js +28 -6
  92. package/dist/esm/node/portal-opener.js.map +1 -1
  93. package/dist/esm/node/provider.d.ts +20 -0
  94. package/dist/esm/node/provider.d.ts.map +1 -0
  95. package/dist/esm/node/provider.js +149 -0
  96. package/dist/esm/node/provider.js.map +1 -0
  97. package/dist/esm/node/qv2.d.ts +3 -1
  98. package/dist/esm/node/qv2.d.ts.map +1 -1
  99. package/dist/esm/node/qv2.js +17 -3
  100. package/dist/esm/node/qv2.js.map +1 -1
  101. package/dist/esm/node/relay.d.ts +13 -0
  102. package/dist/esm/node/relay.d.ts.map +1 -0
  103. package/dist/esm/node/relay.js +105 -0
  104. package/dist/esm/node/relay.js.map +1 -0
  105. package/dist/esm/node/sealed-agent-state.d.ts +20 -0
  106. package/dist/esm/node/sealed-agent-state.d.ts.map +1 -0
  107. package/dist/esm/node/sealed-agent-state.js +190 -0
  108. package/dist/esm/node/sealed-agent-state.js.map +1 -0
  109. package/dist/esm/node/strict-json.d.ts +1 -1
  110. package/dist/esm/node/strict-json.d.ts.map +1 -1
  111. package/dist/esm/node/strict-json.js +8 -3
  112. package/dist/esm/node/strict-json.js.map +1 -1
  113. package/dist/esm/node.d.ts +14 -0
  114. package/dist/esm/node.d.ts.map +1 -1
  115. package/dist/esm/node.js +7 -0
  116. package/dist/esm/node.js.map +1 -1
  117. package/dist/esm/types.d.ts +97 -0
  118. package/dist/esm/types.d.ts.map +1 -1
  119. package/dist/esm/version.d.ts +1 -1
  120. package/dist/esm/version.js +1 -1
  121. package/package.json +16 -7
@@ -0,0 +1,100 @@
1
+ import { type StrictJsonValue } from "./strict-json.js";
2
+ export interface NHPUDPEndpoint {
3
+ host: string;
4
+ port: number;
5
+ server_public_key_b64: string;
6
+ }
7
+ export interface AgentAssignment {
8
+ cell_id: string;
9
+ assignment_generation: bigint;
10
+ endpoint_revision: bigint;
11
+ lease_expires_at: string;
12
+ nhp_udp_endpoint: NHPUDPEndpoint;
13
+ }
14
+ export interface AssignmentRegistration {
15
+ key_id: string;
16
+ key_kind: string;
17
+ }
18
+ export interface PendingAgentActivation {
19
+ assignment_ticket: string;
20
+ assignment_ticket_expires_at: string;
21
+ recovery_anchor_ticket_expires_at?: string;
22
+ recovery_expires_at?: string;
23
+ agent_id: string;
24
+ agent_public_key_b64: string;
25
+ assignment: AgentAssignment;
26
+ registration: AssignmentRegistration;
27
+ hostname?: string;
28
+ agent_version?: string;
29
+ enrollment_credential_fingerprint_b64: string;
30
+ }
31
+ export interface PendingAgentCompletion {
32
+ device_api_key: string;
33
+ cell_id: string;
34
+ assignment_generation: bigint;
35
+ recovery_anchor_ticket_expires_at?: string;
36
+ recovery_expires_at?: string;
37
+ }
38
+ export interface PendingAgentCredentialRecovery {
39
+ recovery_grant: string;
40
+ recovery_grant_issued_at: string;
41
+ recovery_grant_expires_at: string;
42
+ recovery_anchor_grant_expires_at: string;
43
+ recovery_expires_at: string;
44
+ device_api_key: string;
45
+ assignment: AgentAssignment;
46
+ needs_fresh_grant?: boolean;
47
+ }
48
+ export interface PendingAgentCredentialRecoveryIssue {
49
+ request_nonce: string;
50
+ replay_not_after: string;
51
+ recovery_credential_fingerprint_b64: string;
52
+ agent_id: string;
53
+ agent_public_key_b64: string;
54
+ hub_host: string;
55
+ hub_port: number;
56
+ hub_server_public_key_b64: string;
57
+ }
58
+ export interface AgentState {
59
+ agent_id?: string;
60
+ private_key_b64: string;
61
+ public_key_b64: string;
62
+ registered_at?: string;
63
+ schema_version?: number;
64
+ device_api_key?: string;
65
+ assignment?: AgentAssignment;
66
+ device_api_key_id?: string;
67
+ enrollment_credential_kind?: string;
68
+ pending_activation?: PendingAgentActivation;
69
+ pending_completion?: PendingAgentCompletion;
70
+ pending_credential_recovery?: PendingAgentCredentialRecovery;
71
+ pending_credential_recovery_issue?: PendingAgentCredentialRecoveryIssue;
72
+ credential_recovery_refresh_required?: boolean;
73
+ }
74
+ export declare class AgentStateError extends Error {
75
+ readonly code: string;
76
+ constructor(code: string, options?: ErrorOptions);
77
+ }
78
+ export interface AgentStateStore {
79
+ load(signal?: AbortSignal): Promise<AgentState>;
80
+ save(state: AgentState, signal?: AbortSignal): Promise<void>;
81
+ /**
82
+ * Hold one store's setup lock across the complete lifecycle transition.
83
+ * Nested calls reuse the outer lock and its signal; pass a signal directly
84
+ * to load/save when an individual operation needs a different deadline.
85
+ */
86
+ withLock<T>(operation: (locked: AgentStateStore) => Promise<T>, signal?: AbortSignal): Promise<T>;
87
+ checkContinuity?(): void;
88
+ }
89
+ export declare const RECOVERY_HORIZON_MS: number;
90
+ /** Exact integer JSON, also used for NHP uint64 fields. */
91
+ export declare function encodeAgentJSON(value: unknown): Buffer;
92
+ export declare function exactObject(value: StrictJsonValue | undefined, keys: string): Record<string, StrictJsonValue>;
93
+ export declare function canonicalTime(value: unknown, allowFraction?: boolean): number;
94
+ export declare function canonicalKey(value: unknown, encoding?: "base64" | "base64url"): Buffer;
95
+ export declare function validateAgentID(value: unknown): asserts value is string;
96
+ export declare function validateDeviceCredential(value: unknown): asserts value is string;
97
+ export declare function validateEndpoint(value: StrictJsonValue | undefined): NHPUDPEndpoint;
98
+ export declare function validateAssignment(value: StrictJsonValue | undefined): AgentAssignment;
99
+ export declare function decodeAgentState(raw: Uint8Array): AgentState;
100
+ export declare function encodeAgentState(state: AgentState): Buffer;
@@ -0,0 +1,311 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RECOVERY_HORIZON_MS = exports.AgentStateError = void 0;
4
+ exports.encodeAgentJSON = encodeAgentJSON;
5
+ exports.exactObject = exactObject;
6
+ exports.canonicalTime = canonicalTime;
7
+ exports.canonicalKey = canonicalKey;
8
+ exports.validateAgentID = validateAgentID;
9
+ exports.validateDeviceCredential = validateDeviceCredential;
10
+ exports.validateEndpoint = validateEndpoint;
11
+ exports.validateAssignment = validateAssignment;
12
+ exports.decodeAgentState = decodeAgentState;
13
+ exports.encodeAgentState = encodeAgentState;
14
+ const node_crypto_1 = require("node:crypto");
15
+ const strict_json_js_1 = require("./strict-json.js");
16
+ class AgentStateError extends Error {
17
+ code;
18
+ constructor(code, options) {
19
+ super(`qURL agent state: ${code}`, options);
20
+ this.code = code;
21
+ this.name = "AgentStateError";
22
+ }
23
+ }
24
+ exports.AgentStateError = AgentStateError;
25
+ exports.RECOVERY_HORIZON_MS = 90 * 24 * 60 * 60 * 1000;
26
+ const MAX_INT64 = (1n << 63n) - 1n;
27
+ const KEYS = {
28
+ state: "agent_id private_key_b64 public_key_b64 registered_at schema_version device_api_key assignment device_api_key_id enrollment_credential_kind pending_activation pending_completion pending_credential_recovery pending_credential_recovery_issue credential_recovery_refresh_required",
29
+ assignment: "cell_id assignment_generation endpoint_revision lease_expires_at nhp_udp_endpoint",
30
+ endpoint: "host port server_public_key_b64",
31
+ activation: "assignment_ticket assignment_ticket_expires_at recovery_anchor_ticket_expires_at recovery_expires_at agent_id agent_public_key_b64 assignment registration hostname agent_version enrollment_credential_fingerprint_b64",
32
+ completion: "device_api_key cell_id assignment_generation recovery_anchor_ticket_expires_at recovery_expires_at",
33
+ recovery: "recovery_grant recovery_grant_issued_at recovery_grant_expires_at recovery_anchor_grant_expires_at recovery_expires_at device_api_key assignment needs_fresh_grant",
34
+ issue: "request_nonce replay_not_after recovery_credential_fingerprint_b64 agent_id agent_public_key_b64 hub_host hub_port hub_server_public_key_b64",
35
+ };
36
+ /** Exact integer JSON, also used for NHP uint64 fields. */
37
+ function encodeAgentJSON(value) {
38
+ const encode = (v) => {
39
+ if (typeof v === "bigint")
40
+ return v.toString();
41
+ if (v === null || typeof v !== "object") {
42
+ const out = JSON.stringify(v);
43
+ if (out === undefined || (typeof v === "number" && !Number.isFinite(v)))
44
+ throw new AgentStateError("INVALID_JSON");
45
+ return out.replace(/[<>&\u2028\u2029]/g, (ch) => `\\u${ch.charCodeAt(0).toString(16).padStart(4, "0")}`);
46
+ }
47
+ if (Array.isArray(v))
48
+ return `[${v.map(encode).join(",")}]`;
49
+ return `{${Object.entries(v)
50
+ .filter(([, child]) => child !== undefined)
51
+ .map(([key, child]) => `${encode(key)}:${encode(child)}`)
52
+ .join(",")}}`;
53
+ };
54
+ return Buffer.from(encode(value));
55
+ }
56
+ function exactObject(value, keys) {
57
+ const allowed = new Set(keys.split(" "));
58
+ if (!(0, strict_json_js_1.isStrictJsonObject)(value) || Object.keys(value).some((key) => !allowed.has(key)))
59
+ throw new AgentStateError("INVALID_SCHEMA");
60
+ return value;
61
+ }
62
+ function canonicalTime(value, allowFraction = false) {
63
+ if (typeof value !== "string" ||
64
+ (!allowFraction && value.includes(".")) ||
65
+ !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?Z$/.test(value))
66
+ throw new AgentStateError("INVALID_TIME");
67
+ const ms = Date.parse(value);
68
+ if (!Number.isFinite(ms) || new Date(ms).toISOString().slice(0, 19) !== value.slice(0, 19))
69
+ throw new AgentStateError("INVALID_TIME");
70
+ return ms;
71
+ }
72
+ function canonicalKey(value, encoding = "base64") {
73
+ if (typeof value !== "string")
74
+ throw new AgentStateError("INVALID_KEY");
75
+ const key = Buffer.from(value, encoding);
76
+ if (key.length !== 32 || key.toString(encoding) !== value)
77
+ throw new AgentStateError("INVALID_KEY");
78
+ return key;
79
+ }
80
+ function validateAgentID(value) {
81
+ if (typeof value !== "string" || !/^[a-z0-9][a-z0-9-]{0,62}[a-z0-9]$/.test(value))
82
+ throw new AgentStateError("INVALID_ID");
83
+ }
84
+ function validateDeviceCredential(value) {
85
+ if (typeof value !== "string" || !value.startsWith("lv_live_"))
86
+ throw new AgentStateError("INVALID_CREDENTIAL");
87
+ canonicalKey(value.slice(8), "base64url").fill(0);
88
+ }
89
+ function validateEndpoint(value) {
90
+ const item = exactObject(value, KEYS.endpoint);
91
+ if (typeof item.host !== "string" ||
92
+ item.host.length > 253 ||
93
+ !/\.(?:layerv\.ai|layerv\.xyz)$/.test(item.host) ||
94
+ item.host.split(".").some((label) => !/^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(label)) ||
95
+ item.port !== 443n)
96
+ throw new AgentStateError("INVALID_ENDPOINT");
97
+ canonicalKey(item.server_public_key_b64);
98
+ return {
99
+ host: item.host,
100
+ port: 443,
101
+ server_public_key_b64: item.server_public_key_b64,
102
+ };
103
+ }
104
+ function validateAssignment(value) {
105
+ const item = exactObject(value, KEYS.assignment);
106
+ if (typeof item.cell_id !== "string" || !/^[a-z](?:[a-z0-9-]{0,62}[a-z0-9])?$/.test(item.cell_id))
107
+ throw new AgentStateError("INVALID_ASSIGNMENT");
108
+ for (const key of ["assignment_generation", "endpoint_revision"]) {
109
+ const n = item[key];
110
+ if (typeof n !== "bigint" || n <= 0n || n > MAX_INT64)
111
+ throw new AgentStateError("INVALID_ASSIGNMENT");
112
+ }
113
+ canonicalTime(item.lease_expires_at);
114
+ return {
115
+ ...item,
116
+ nhp_udp_endpoint: validateEndpoint(item.nhp_udp_endpoint),
117
+ };
118
+ }
119
+ function recoveryWindow(anchor, expires) {
120
+ if (canonicalTime(expires) - canonicalTime(anchor) !== exports.RECOVERY_HORIZON_MS)
121
+ throw new AgentStateError("INVALID_RECOVERY_WINDOW");
122
+ }
123
+ function decodeAgentState(raw) {
124
+ try {
125
+ const value = exactObject((0, strict_json_js_1.parseStrictJson)(raw, 1_048_576), KEYS.state);
126
+ const schema = value.schema_version ?? 0n;
127
+ if (typeof schema !== "bigint" || schema < 0n || schema > 8n)
128
+ throw new AgentStateError("UNSUPPORTED_SCHEMA");
129
+ if ((schema < 7n &&
130
+ (value.pending_credential_recovery || value.pending_credential_recovery_issue)) ||
131
+ (schema < 8n && value.credential_recovery_refresh_required))
132
+ throw new AgentStateError("UNSUPPORTED_SCHEMA");
133
+ validateAgentID(value.agent_id);
134
+ const privateKey = canonicalKey(value.private_key_b64);
135
+ try {
136
+ const key = (0, node_crypto_1.createPrivateKey)({
137
+ key: Buffer.concat([Buffer.from("302e020100300506032b656e04220420", "hex"), privateKey]),
138
+ type: "pkcs8",
139
+ format: "der",
140
+ });
141
+ const publicKey = (0, node_crypto_1.createPublicKey)(key).export({ type: "spki", format: "der" }).subarray(-32);
142
+ if (!(0, node_crypto_1.timingSafeEqual)(publicKey, canonicalKey(value.public_key_b64)))
143
+ throw new AgentStateError("INVALID_KEYPAIR");
144
+ }
145
+ finally {
146
+ privateKey.fill(0);
147
+ }
148
+ const state = { ...value, schema_version: Number(schema) };
149
+ if (value.assignment !== undefined)
150
+ state.assignment = validateAssignment(value.assignment);
151
+ const assignmentEqual = (a) => {
152
+ const b = state.assignment;
153
+ return (b !== undefined &&
154
+ a.cell_id === b.cell_id &&
155
+ a.assignment_generation === b.assignment_generation &&
156
+ a.endpoint_revision === b.endpoint_revision &&
157
+ a.lease_expires_at === b.lease_expires_at &&
158
+ a.nhp_udp_endpoint.host === b.nhp_udp_endpoint.host &&
159
+ a.nhp_udp_endpoint.port === b.nhp_udp_endpoint.port &&
160
+ a.nhp_udp_endpoint.server_public_key_b64 === b.nhp_udp_endpoint.server_public_key_b64);
161
+ };
162
+ if (value.registered_at !== undefined)
163
+ canonicalTime(value.registered_at, true);
164
+ if (value.device_api_key !== undefined)
165
+ validateDeviceCredential(value.device_api_key);
166
+ if (value.device_api_key_id !== undefined &&
167
+ (typeof value.device_api_key_id !== "string" ||
168
+ !/^key_[A-Za-z0-9]{12}$/.test(value.device_api_key_id)))
169
+ throw new AgentStateError("INVALID_CREDENTIAL_ID");
170
+ if (value.enrollment_credential_kind !== undefined &&
171
+ !["account", "bootstrap", "connector_bootstrap", "agent"].includes(value.enrollment_credential_kind))
172
+ throw new AgentStateError("INVALID_KIND");
173
+ if (value.pending_activation !== undefined) {
174
+ const p = exactObject(value.pending_activation, KEYS.activation);
175
+ const registration = exactObject(p.registration, "key_id key_kind");
176
+ const assignment = validateAssignment(p.assignment);
177
+ if (p.agent_id !== state.agent_id ||
178
+ p.agent_public_key_b64 !== state.public_key_b64 ||
179
+ !assignmentEqual(assignment) ||
180
+ typeof p.assignment_ticket !== "string" ||
181
+ !/^[\x21-\x7e]{1,2304}$/.test(p.assignment_ticket) ||
182
+ typeof registration.key_id !== "string" ||
183
+ !/^key_[A-Za-z0-9]{12}$/.test(registration.key_id) ||
184
+ !["account", "bootstrap", "connector_bootstrap", "agent"].includes(registration.key_kind))
185
+ throw new AgentStateError("INVALID_ACTIVATION");
186
+ canonicalTime(p.assignment_ticket_expires_at);
187
+ if (schema < 6n) {
188
+ for (const field of ["recovery_anchor_ticket_expires_at", "recovery_expires_at"])
189
+ if (p[field] !== undefined && p[field] !== "0001-01-01T00:00:00Z")
190
+ throw new AgentStateError("INVALID_ACTIVATION");
191
+ p.recovery_anchor_ticket_expires_at = p.assignment_ticket_expires_at;
192
+ p.recovery_expires_at = new Date(canonicalTime(p.assignment_ticket_expires_at) + exports.RECOVERY_HORIZON_MS)
193
+ .toISOString()
194
+ .replace(".000Z", "Z");
195
+ // The normalized in-memory shape now carries the v6 recovery fields.
196
+ state.schema_version = 6;
197
+ }
198
+ recoveryWindow(p.recovery_anchor_ticket_expires_at, p.recovery_expires_at);
199
+ canonicalKey(p.enrollment_credential_fingerprint_b64, "base64url");
200
+ for (const field of ["hostname", "agent_version"])
201
+ if (p[field] !== undefined && typeof p[field] !== "string")
202
+ throw new AgentStateError("INVALID_ACTIVATION");
203
+ if (state.registered_at ||
204
+ state.device_api_key ||
205
+ state.device_api_key_id ||
206
+ value.pending_completion)
207
+ throw new AgentStateError("CONFLICTING_PHASES");
208
+ state.pending_activation = { ...p, assignment };
209
+ }
210
+ if (value.pending_completion !== undefined) {
211
+ const p = exactObject(value.pending_completion, KEYS.completion);
212
+ validateDeviceCredential(p.device_api_key);
213
+ if (schema < 6n) {
214
+ for (const field of ["recovery_anchor_ticket_expires_at", "recovery_expires_at"]) {
215
+ if (p[field] !== undefined && p[field] !== "0001-01-01T00:00:00Z")
216
+ throw new AgentStateError("INVALID_COMPLETION");
217
+ delete p[field];
218
+ }
219
+ }
220
+ else
221
+ recoveryWindow(p.recovery_anchor_ticket_expires_at, p.recovery_expires_at);
222
+ if (!state.assignment ||
223
+ p.cell_id !== state.assignment.cell_id ||
224
+ p.assignment_generation !== state.assignment.assignment_generation ||
225
+ state.registered_at ||
226
+ state.device_api_key ||
227
+ state.device_api_key_id)
228
+ throw new AgentStateError("INVALID_COMPLETION");
229
+ }
230
+ if (value.pending_credential_recovery !== undefined) {
231
+ const p = exactObject(value.pending_credential_recovery, KEYS.recovery);
232
+ const assignment = validateAssignment(p.assignment);
233
+ if (!assignmentEqual(assignment) ||
234
+ !state.registered_at ||
235
+ state.device_api_key ||
236
+ state.device_api_key_id ||
237
+ state.pending_activation ||
238
+ state.pending_completion ||
239
+ typeof p.recovery_grant !== "string" ||
240
+ !/^qrg1\.[A-Za-z0-9_-]{1,2299}$/.test(p.recovery_grant) ||
241
+ (p.needs_fresh_grant !== undefined && typeof p.needs_fresh_grant !== "boolean"))
242
+ throw new AgentStateError("INVALID_RECOVERY");
243
+ validateDeviceCredential(p.device_api_key);
244
+ recoveryWindow(p.recovery_anchor_grant_expires_at, p.recovery_expires_at);
245
+ if (canonicalTime(p.recovery_grant_expires_at) - canonicalTime(p.recovery_grant_issued_at) !==
246
+ 900_000)
247
+ throw new AgentStateError("INVALID_GRANT_WINDOW");
248
+ state.pending_credential_recovery = {
249
+ ...p,
250
+ assignment,
251
+ };
252
+ }
253
+ if (value.pending_credential_recovery_issue !== undefined) {
254
+ const p = exactObject(value.pending_credential_recovery_issue, KEYS.issue);
255
+ if (!state.registered_at ||
256
+ !state.assignment ||
257
+ state.pending_activation ||
258
+ state.pending_completion ||
259
+ p.agent_id !== state.agent_id ||
260
+ p.agent_public_key_b64 !== state.public_key_b64 ||
261
+ (state.pending_credential_recovery && !state.pending_credential_recovery.needs_fresh_grant))
262
+ throw new AgentStateError("INVALID_RECOVERY_ISSUE");
263
+ canonicalKey(p.request_nonce, "base64url");
264
+ canonicalKey(p.recovery_credential_fingerprint_b64, "base64url");
265
+ canonicalTime(p.replay_not_after);
266
+ if (state.pending_credential_recovery &&
267
+ canonicalTime(p.replay_not_after) >
268
+ canonicalTime(state.pending_credential_recovery.recovery_expires_at))
269
+ throw new AgentStateError("INVALID_RECOVERY_WINDOW");
270
+ const endpoint = validateEndpoint({
271
+ host: p.hub_host,
272
+ port: p.hub_port,
273
+ server_public_key_b64: p.hub_server_public_key_b64,
274
+ });
275
+ state.pending_credential_recovery_issue = {
276
+ ...p,
277
+ hub_port: endpoint.port,
278
+ };
279
+ }
280
+ if (value.credential_recovery_refresh_required !== undefined &&
281
+ typeof value.credential_recovery_refresh_required !== "boolean")
282
+ throw new AgentStateError("INVALID_RECOVERY");
283
+ if (state.credential_recovery_refresh_required &&
284
+ (!state.registered_at ||
285
+ !state.assignment ||
286
+ !state.device_api_key ||
287
+ !state.device_api_key_id ||
288
+ state.pending_activation ||
289
+ state.pending_completion ||
290
+ state.pending_credential_recovery ||
291
+ state.pending_credential_recovery_issue))
292
+ throw new AgentStateError("CONFLICTING_PHASES");
293
+ return state;
294
+ }
295
+ catch (cause) {
296
+ if (cause instanceof AgentStateError)
297
+ throw cause;
298
+ throw new AgentStateError("INVALID_STATE", { cause });
299
+ }
300
+ }
301
+ function encodeAgentState(state) {
302
+ const raw = encodeAgentJSON(state);
303
+ try {
304
+ decodeAgentState(raw);
305
+ return raw;
306
+ }
307
+ catch (error) {
308
+ raw.fill(0);
309
+ throw error;
310
+ }
311
+ }
@@ -0,0 +1,17 @@
1
+ import { type NHPMessage } from "./nhp-wire.js";
2
+ import { type NHPUDPEndpoint } from "./agent-state.js";
3
+ export interface AgentExchange {
4
+ endpoint: NHPUDPEndpoint;
5
+ privateKey: Uint8Array;
6
+ type: 1 | 5 | 12 | 13 | 16;
7
+ body: Uint8Array;
8
+ reknockBody?: Uint8Array;
9
+ assignment?: boolean;
10
+ signal: AbortSignal;
11
+ beforeSend: () => void;
12
+ }
13
+ export type AgentTransport = (exchange: AgentExchange) => Promise<NHPMessage | undefined>;
14
+ export declare class AgentTransportError extends Error {
15
+ constructor(options?: ErrorOptions);
16
+ }
17
+ export declare const nativeAgentTransport: AgentTransport;
@@ -0,0 +1,118 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.nativeAgentTransport = exports.AgentTransportError = void 0;
4
+ const nhp_wire_js_1 = require("./nhp-wire.js");
5
+ const native_udp_js_1 = require("./native-udp.js");
6
+ const agent_state_js_1 = require("./agent-state.js");
7
+ const strict_json_js_1 = require("./strict-json.js");
8
+ class AgentTransportError extends Error {
9
+ constructor(options) {
10
+ super("native agent transport failed", options);
11
+ this.name = "AgentTransportError";
12
+ }
13
+ }
14
+ exports.AgentTransportError = AgentTransportError;
15
+ const nativeAgentTransport = async (input) => {
16
+ const serverPublicKey = (0, agent_state_js_1.canonicalKey)(input.endpoint.server_public_key_b64);
17
+ const timestamp = BigInt(Date.now()) * 1000000n;
18
+ const first = (0, nhp_wire_js_1.buildNHPMessage)({
19
+ type: input.type,
20
+ devicePrivateKey: input.privateKey,
21
+ serverPublicKey,
22
+ body: input.body,
23
+ timestampNanos: timestamp,
24
+ });
25
+ const send = async (packet, counter, type, challenge = false) => {
26
+ input.signal.throwIfAborted();
27
+ let addresses;
28
+ try {
29
+ addresses = await (0, native_udp_js_1.resolvePublicAddresses)(input.endpoint.host, 3, input.signal);
30
+ }
31
+ catch (cause) {
32
+ throw new AgentTransportError({ cause });
33
+ }
34
+ let lastError;
35
+ for (const address of addresses) {
36
+ input.signal.throwIfAborted();
37
+ input.beforeSend();
38
+ let received;
39
+ try {
40
+ received = await (0, native_udp_js_1.exchangeDatagram)(address.address, address.family, input.endpoint.port, packet, 3000, input.signal, type === 12);
41
+ }
42
+ catch (cause) {
43
+ if (!(0, native_udp_js_1.isSocketExchangeError)(cause))
44
+ throw cause;
45
+ lastError = cause;
46
+ continue;
47
+ }
48
+ if (type === 12)
49
+ return undefined;
50
+ let reply;
51
+ try {
52
+ reply = (0, nhp_wire_js_1.decryptNHPReply)(input.privateKey, serverPublicKey, received);
53
+ }
54
+ finally {
55
+ received.fill(0);
56
+ }
57
+ const cookieAllowed = reply.type === 7 && (type === 1 || challenge);
58
+ const allowed = cookieAllowed || reply.type === (type === 5 ? 6 : type === 13 ? 14 : 2);
59
+ if (!allowed ||
60
+ (!cookieAllowed && reply.counter !== counter) ||
61
+ (input.assignment && reply.flags !== 0)) {
62
+ reply.body.fill(0);
63
+ throw new agent_state_js_1.AgentStateError("INVALID_NATIVE_REPLY");
64
+ }
65
+ return reply;
66
+ }
67
+ throw new AgentTransportError({ cause: lastError });
68
+ };
69
+ try {
70
+ const reply = await send(first.packet, first.counter, input.type, input.assignment);
71
+ if (input.assignment && reply?.type !== 7) {
72
+ reply?.body.fill(0);
73
+ throw new agent_state_js_1.AgentStateError("ASSIGNMENT_REQUIRES_SOURCE_PROOF");
74
+ }
75
+ if (reply?.type !== 7 || (!input.assignment && input.reknockBody === undefined))
76
+ return reply;
77
+ let cookie;
78
+ try {
79
+ const challenge = (0, agent_state_js_1.exactObject)((0, strict_json_js_1.parseStrictJson)(reply.body, 4096), "trxId cookie");
80
+ if (challenge.trxId !== first.counter ||
81
+ /"cookie"\s*:\s*"[^"\n]*\\/.test(Buffer.from(reply.body.buffer, reply.body.byteOffset, reply.body.byteLength).toString()))
82
+ throw new agent_state_js_1.AgentStateError("INVALID_COOKIE");
83
+ cookie = (0, agent_state_js_1.canonicalKey)(challenge.cookie);
84
+ }
85
+ finally {
86
+ reply.body.fill(0);
87
+ }
88
+ try {
89
+ input.signal.throwIfAborted();
90
+ const current = BigInt(Date.now()) * 1000000n;
91
+ const proof = (0, nhp_wire_js_1.buildNHPMessage)({
92
+ type: input.assignment ? 5 : 8,
93
+ devicePrivateKey: input.privateKey,
94
+ serverPublicKey,
95
+ cookie,
96
+ hubProof: input.assignment,
97
+ body: input.assignment ? input.body : input.reknockBody,
98
+ timestampNanos: current > timestamp ? current : timestamp + 1n,
99
+ });
100
+ try {
101
+ if (proof.counter === first.counter ||
102
+ Buffer.from(proof.packet.subarray(24, 56)).equals(first.packet.subarray(24, 56)))
103
+ throw new agent_state_js_1.AgentStateError("REPEATED_PROOF_RANDOMNESS");
104
+ return await send(proof.packet, proof.counter, input.assignment ? 5 : 8);
105
+ }
106
+ finally {
107
+ proof.packet.fill(0);
108
+ }
109
+ }
110
+ finally {
111
+ cookie.fill(0);
112
+ }
113
+ }
114
+ finally {
115
+ first.packet.fill(0);
116
+ }
117
+ };
118
+ exports.nativeAgentTransport = nativeAgentTransport;
@@ -1,8 +1,11 @@
1
1
  import type { KeyObject } from "node:crypto";
2
2
  import { type StrictJsonValue } from "./strict-json.js";
3
+ import type { NHPUDPEndpoint } from "./agent-state.js";
3
4
  export interface PortalDeployment {
5
+ readonly hub?: NHPUDPEndpoint;
4
6
  readonly issuers: readonly PortalDeploymentIssuer[];
5
7
  readonly cells: readonly PortalDeploymentCell[];
8
+ readonly relay_allowlist?: readonly string[];
6
9
  }
7
10
  export interface PortalDeploymentIssuer {
8
11
  readonly kid: string;
@@ -17,6 +20,7 @@ export interface PortalDeploymentCell {
17
20
  export interface ValidatedDeployment {
18
21
  readonly issuers: ReadonlyMap<string, KeyObject>;
19
22
  readonly cells: ReadonlyMap<string, ValidatedCell>;
23
+ readonly relayAllowlist: readonly string[];
20
24
  }
21
25
  export interface ValidatedCell {
22
26
  readonly host: string;
@@ -41,4 +45,6 @@ export declare const deploymentTesting: {
41
45
  readBoundedDeploymentFile: typeof readBoundedDeploymentFile;
42
46
  decodeFlexibleBase64: typeof decodeFlexibleBase64;
43
47
  };
48
+ /** Producer-only deployments need a pinned Hub, but need no portal issuer keys. */
49
+ export declare function loadDeploymentHub(): unknown;
44
50
  export {};
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.deploymentTesting = void 0;
4
4
  exports.loadPortalDeployment = loadPortalDeployment;
5
5
  exports.fingerprintKey = fingerprintKey;
6
+ exports.loadDeploymentHub = loadDeploymentHub;
6
7
  const node_fs_1 = require("node:fs");
7
8
  const node_crypto_1 = require("node:crypto");
8
9
  const strict_json_js_1 = require("./strict-json.js");
@@ -83,8 +84,13 @@ function validateDeploymentValue(value) {
83
84
  const cellRows = requireArray(root.cells, "deployment cells");
84
85
  if (issuerRows.length === 0)
85
86
  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");
87
+ const relayAllowlist = root.relay_allowlist === undefined
88
+ ? []
89
+ : requireArray(root.relay_allowlist, "relay allowlist")
90
+ .map((value) => requireString(value, "relay host").trim().toLowerCase())
91
+ .filter(Boolean);
92
+ if (cellRows.length === 0 && relayAllowlist.length === 0)
93
+ throw new Error("deployment must contain at least one cell or relay host");
88
94
  const issuers = new Map();
89
95
  for (const row of issuerRows) {
90
96
  const item = requireObject(row, "deployment issuer");
@@ -113,7 +119,7 @@ function validateDeploymentValue(value) {
113
119
  requireString(item.cell_id, "cell id");
114
120
  cells.set(fingerprint, { host, port: 443, serverPublicKey });
115
121
  }
116
- return { issuers, cells };
122
+ return { issuers, cells, relayAllowlist };
117
123
  }
118
124
  function fingerprintKey(key) {
119
125
  return (0, node_crypto_1.createHash)("sha256").update(key).digest().subarray(0, 8).toString("base64url");
@@ -165,3 +171,18 @@ function rejectUnknown(value, allowed, name) {
165
171
  }
166
172
  }
167
173
  exports.deploymentTesting = { readBoundedDeploymentFile, decodeFlexibleBase64 };
174
+ /** Producer-only deployments need a pinned Hub, but need no portal issuer keys. */
175
+ function loadDeploymentHub() {
176
+ const configured = process.env.QURL_DEPLOYMENT?.trim();
177
+ if (!configured)
178
+ throw new Error("QURL_DEPLOYMENT has no producer Hub");
179
+ const raw = configured.startsWith("{")
180
+ ? Buffer.from(configured)
181
+ : readBoundedDeploymentFile(configured);
182
+ const value = (0, strict_json_js_1.parseStrictJson)(raw, MAX_DEPLOYMENT_BYTES);
183
+ if (!(0, strict_json_js_1.isStrictJsonObject)(value) ||
184
+ Object.keys(value).some((key) => !DEPLOYMENT_KEYS.has(key)) ||
185
+ !(0, strict_json_js_1.isStrictJsonObject)(value.hub))
186
+ throw new Error("QURL_DEPLOYMENT has no producer Hub");
187
+ return value.hub;
188
+ }
@@ -0,0 +1,15 @@
1
+ import { type AgentState, type AgentStateStore } from "./agent-state.js";
2
+ export interface AgentStateCodec {
3
+ encode(state: AgentState, signal?: AbortSignal): Promise<Buffer>;
4
+ decode(raw: Uint8Array, signal?: AbortSignal): Promise<AgentState>;
5
+ }
6
+ /** Own this handle until all clients and lifecycle operations finish. */
7
+ export declare class FileAgentState implements AgentStateStore {
8
+ #private;
9
+ constructor(path: string, codec?: AgentStateCodec);
10
+ checkContinuity(): void;
11
+ load(signal?: AbortSignal): Promise<AgentState>;
12
+ save(state: AgentState, signal?: AbortSignal): Promise<void>;
13
+ withLock<T>(operation: (locked: AgentStateStore) => Promise<T>, signal?: AbortSignal): Promise<T>;
14
+ close(): void;
15
+ }