@layervai/qurl 0.6.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.
- package/README.md +332 -15
- package/dist/cjs/client.d.ts +62 -31
- package/dist/cjs/client.js +932 -163
- package/dist/cjs/crid.d.ts +3 -0
- package/dist/cjs/crid.js +62 -0
- package/dist/cjs/errors.d.ts +30 -5
- package/dist/cjs/errors.js +41 -6
- package/dist/cjs/index.d.ts +5 -3
- package/dist/cjs/index.js +11 -1
- package/dist/cjs/node/agent-runtime.d.ts +108 -0
- package/dist/cjs/node/agent-runtime.js +952 -0
- package/dist/cjs/node/agent-state.d.ts +100 -0
- package/dist/cjs/node/agent-state.js +311 -0
- package/dist/cjs/node/agent-transport.d.ts +17 -0
- package/dist/cjs/node/agent-transport.js +118 -0
- package/dist/cjs/node/deployment.d.ts +6 -0
- package/dist/cjs/node/deployment.js +24 -3
- package/dist/cjs/node/file-agent-state.d.ts +15 -0
- package/dist/cjs/node/file-agent-state.js +137 -0
- package/dist/cjs/node/native-loader.cjs +7 -0
- package/dist/cjs/node/native-loader.d.cts +11 -0
- package/dist/cjs/node/native-udp.d.ts +8 -3
- package/dist/cjs/node/native-udp.js +8 -3
- package/dist/cjs/node/nhp-wire.d.ts +9 -0
- package/dist/cjs/node/nhp-wire.js +24 -7
- package/dist/cjs/node/portal-opener.d.ts +7 -0
- package/dist/cjs/node/portal-opener.js +28 -6
- package/dist/cjs/node/provider.d.ts +19 -0
- package/dist/cjs/node/provider.js +153 -0
- package/dist/cjs/node/qv2.d.ts +3 -1
- package/dist/cjs/node/qv2.js +16 -2
- package/dist/cjs/node/relay.d.ts +12 -0
- package/dist/cjs/node/relay.js +111 -0
- package/dist/cjs/node/sealed-agent-state.d.ts +19 -0
- package/dist/cjs/node/sealed-agent-state.js +193 -0
- package/dist/cjs/node/strict-json.d.ts +1 -1
- package/dist/cjs/node/strict-json.js +8 -3
- package/dist/cjs/node.d.ts +14 -0
- package/dist/cjs/node.js +24 -1
- package/dist/cjs/share.d.ts +56 -0
- package/dist/cjs/share.js +113 -0
- package/dist/cjs/types.d.ts +173 -13
- package/dist/cjs/version.d.ts +1 -1
- package/dist/cjs/version.js +1 -1
- package/dist/esm/client.d.ts +62 -31
- package/dist/esm/client.d.ts.map +1 -1
- package/dist/esm/client.js +932 -164
- package/dist/esm/client.js.map +1 -1
- package/dist/esm/crid.d.ts +4 -0
- package/dist/esm/crid.d.ts.map +1 -0
- package/dist/esm/crid.js +58 -0
- package/dist/esm/crid.js.map +1 -0
- package/dist/esm/errors.d.ts +30 -5
- package/dist/esm/errors.d.ts.map +1 -1
- package/dist/esm/errors.js +39 -5
- package/dist/esm/errors.js.map +1 -1
- package/dist/esm/index.d.ts +5 -3
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +3 -2
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/node/agent-runtime.d.ts +109 -0
- package/dist/esm/node/agent-runtime.d.ts.map +1 -0
- package/dist/esm/node/agent-runtime.js +946 -0
- package/dist/esm/node/agent-runtime.js.map +1 -0
- package/dist/esm/node/agent-state.d.ts +101 -0
- package/dist/esm/node/agent-state.d.ts.map +1 -0
- package/dist/esm/node/agent-state.js +298 -0
- package/dist/esm/node/agent-state.js.map +1 -0
- package/dist/esm/node/agent-transport.d.ts +18 -0
- package/dist/esm/node/agent-transport.d.ts.map +1 -0
- package/dist/esm/node/agent-transport.js +114 -0
- package/dist/esm/node/agent-transport.js.map +1 -0
- package/dist/esm/node/deployment.d.ts +6 -0
- package/dist/esm/node/deployment.d.ts.map +1 -1
- package/dist/esm/node/deployment.js +23 -3
- package/dist/esm/node/deployment.js.map +1 -1
- package/dist/esm/node/file-agent-state.d.ts +16 -0
- package/dist/esm/node/file-agent-state.d.ts.map +1 -0
- package/dist/esm/node/file-agent-state.js +134 -0
- package/dist/esm/node/file-agent-state.js.map +1 -0
- package/dist/esm/node/native-loader.cjs +7 -0
- package/dist/esm/node/native-loader.d.cts +11 -0
- package/dist/esm/node/native-udp.d.ts +8 -3
- package/dist/esm/node/native-udp.d.ts.map +1 -1
- package/dist/esm/node/native-udp.js +7 -5
- package/dist/esm/node/native-udp.js.map +1 -1
- package/dist/esm/node/nhp-wire.d.ts +9 -0
- package/dist/esm/node/nhp-wire.d.ts.map +1 -1
- package/dist/esm/node/nhp-wire.js +23 -6
- package/dist/esm/node/nhp-wire.js.map +1 -1
- package/dist/esm/node/portal-opener.d.ts +7 -0
- package/dist/esm/node/portal-opener.d.ts.map +1 -1
- package/dist/esm/node/portal-opener.js +28 -6
- package/dist/esm/node/portal-opener.js.map +1 -1
- package/dist/esm/node/provider.d.ts +20 -0
- package/dist/esm/node/provider.d.ts.map +1 -0
- package/dist/esm/node/provider.js +149 -0
- package/dist/esm/node/provider.js.map +1 -0
- package/dist/esm/node/qv2.d.ts +3 -1
- package/dist/esm/node/qv2.d.ts.map +1 -1
- package/dist/esm/node/qv2.js +17 -3
- package/dist/esm/node/qv2.js.map +1 -1
- package/dist/esm/node/relay.d.ts +13 -0
- package/dist/esm/node/relay.d.ts.map +1 -0
- package/dist/esm/node/relay.js +105 -0
- package/dist/esm/node/relay.js.map +1 -0
- package/dist/esm/node/sealed-agent-state.d.ts +20 -0
- package/dist/esm/node/sealed-agent-state.d.ts.map +1 -0
- package/dist/esm/node/sealed-agent-state.js +190 -0
- package/dist/esm/node/sealed-agent-state.js.map +1 -0
- package/dist/esm/node/strict-json.d.ts +1 -1
- package/dist/esm/node/strict-json.d.ts.map +1 -1
- package/dist/esm/node/strict-json.js +8 -3
- package/dist/esm/node/strict-json.js.map +1 -1
- package/dist/esm/node.d.ts +14 -0
- package/dist/esm/node.d.ts.map +1 -1
- package/dist/esm/node.js +7 -0
- package/dist/esm/node.js.map +1 -1
- package/dist/esm/share.d.ts +57 -0
- package/dist/esm/share.d.ts.map +1 -0
- package/dist/esm/share.js +109 -0
- package/dist/esm/share.js.map +1 -0
- package/dist/esm/types.d.ts +173 -13
- package/dist/esm/types.d.ts.map +1 -1
- package/dist/esm/version.d.ts +1 -1
- package/dist/esm/version.js +1 -1
- package/package.json +17 -8
|
@@ -0,0 +1,952 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.agentRuntimeTesting = exports.AgentRuntime = exports.AgentLifecycleError = void 0;
|
|
4
|
+
exports.connectAgentRuntime = connectAgentRuntime;
|
|
5
|
+
exports.recoverAgentRuntime = recoverAgentRuntime;
|
|
6
|
+
const deployment_js_1 = require("./deployment.js");
|
|
7
|
+
const node_crypto_1 = require("node:crypto");
|
|
8
|
+
const promises_1 = require("node:timers/promises");
|
|
9
|
+
const node_net_1 = require("node:net");
|
|
10
|
+
const client_js_1 = require("../client.js");
|
|
11
|
+
const crid_js_1 = require("../crid.js");
|
|
12
|
+
const strict_json_js_1 = require("./strict-json.js");
|
|
13
|
+
const agent_state_js_1 = require("./agent-state.js");
|
|
14
|
+
const agent_transport_js_1 = require("./agent-transport.js");
|
|
15
|
+
class AgentLifecycleError extends Error {
|
|
16
|
+
code;
|
|
17
|
+
retryAfterMs;
|
|
18
|
+
constructor(code, retryAfterMs, options) {
|
|
19
|
+
super(`qURL agent lifecycle: ${code}`, options);
|
|
20
|
+
this.code = code;
|
|
21
|
+
this.retryAfterMs = retryAfterMs;
|
|
22
|
+
this.name = "AgentLifecycleError";
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
exports.AgentLifecycleError = AgentLifecycleError;
|
|
26
|
+
function validAgentAddress(value) {
|
|
27
|
+
const match = /^(?:\[([^\]]+)\]|([^:]+)):(\d+)$/.exec(value);
|
|
28
|
+
return (!!match && !!(0, node_net_1.isIP)(match[1] ?? match[2]) && Number(match[3]) > 0 && Number(match[3]) <= 65535);
|
|
29
|
+
}
|
|
30
|
+
const UINT64_MAX = (1n << 64n) - 1n;
|
|
31
|
+
const KEY_ID = /^key_[A-Za-z0-9]{12}$/;
|
|
32
|
+
function copy(state) {
|
|
33
|
+
const raw = (0, agent_state_js_1.encodeAgentState)(state);
|
|
34
|
+
try {
|
|
35
|
+
return (0, agent_state_js_1.decodeAgentState)(raw);
|
|
36
|
+
}
|
|
37
|
+
finally {
|
|
38
|
+
raw.fill(0);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function fingerprint(kind, credential) {
|
|
42
|
+
return (0, node_crypto_1.createHash)("sha256")
|
|
43
|
+
.update(`qurl-go/pending-${kind}-credential-v1\0`)
|
|
44
|
+
.update(credential)
|
|
45
|
+
.digest("base64url");
|
|
46
|
+
}
|
|
47
|
+
function credential(value) {
|
|
48
|
+
if (typeof value !== "string" || !/^[\x21-\x7e]{32,4096}$/.test(value))
|
|
49
|
+
throw new AgentLifecycleError("INVALID_ENROLLMENT_CREDENTIAL");
|
|
50
|
+
}
|
|
51
|
+
function live(expires) {
|
|
52
|
+
if (!expires || (0, agent_state_js_1.canonicalTime)(expires) <= Date.now())
|
|
53
|
+
throw new AgentLifecycleError("RECOVERY_EXPIRED");
|
|
54
|
+
}
|
|
55
|
+
function ready(state) {
|
|
56
|
+
if (state.pending_credential_recovery || state.pending_credential_recovery_issue)
|
|
57
|
+
throw new AgentLifecycleError("CREDENTIAL_RECOVERY_REQUIRED");
|
|
58
|
+
if (state.credential_recovery_refresh_required)
|
|
59
|
+
throw new AgentLifecycleError("RECOVERED_ASSIGNMENT_REFRESH_REQUIRED");
|
|
60
|
+
if (!state.registered_at ||
|
|
61
|
+
!state.device_api_key ||
|
|
62
|
+
!state.device_api_key_id ||
|
|
63
|
+
!state.assignment)
|
|
64
|
+
throw new AgentLifecycleError("NOT_REGISTERED");
|
|
65
|
+
}
|
|
66
|
+
function checkIdentity(state, expected) {
|
|
67
|
+
if (expected !== undefined && state.agent_id !== expected)
|
|
68
|
+
throw new AgentLifecycleError("IDENTITY_CHANGED");
|
|
69
|
+
}
|
|
70
|
+
/** One driver per producer operation. Persistence failures never enter the network retry loop. */
|
|
71
|
+
class Lifecycle {
|
|
72
|
+
store;
|
|
73
|
+
options;
|
|
74
|
+
transport;
|
|
75
|
+
replacement;
|
|
76
|
+
constructor(store, options, transport, replacement) {
|
|
77
|
+
this.store = store;
|
|
78
|
+
this.options = options;
|
|
79
|
+
this.transport = transport;
|
|
80
|
+
this.replacement = replacement;
|
|
81
|
+
}
|
|
82
|
+
hub() {
|
|
83
|
+
const hub = this.options.hub ?? (0, deployment_js_1.loadDeploymentHub)();
|
|
84
|
+
return (0, agent_state_js_1.validateEndpoint)((0, strict_json_js_1.parseStrictJson)((0, agent_state_js_1.encodeAgentJSON)(hub), 4096));
|
|
85
|
+
}
|
|
86
|
+
async exchange(state, endpoint, type, value, phase, expires, reknock, detachedSignal) {
|
|
87
|
+
const body = (0, agent_state_js_1.encodeAgentJSON)(value);
|
|
88
|
+
const reknockBody = reknock === undefined ? undefined : (0, agent_state_js_1.encodeAgentJSON)(reknock);
|
|
89
|
+
const privateKey = (0, agent_state_js_1.canonicalKey)(state.private_key_b64);
|
|
90
|
+
const remaining = Math.min(30_000, expires ? (0, agent_state_js_1.canonicalTime)(expires) - Date.now() : Infinity, this.replacement ? (0, agent_state_js_1.canonicalTime)(this.replacement.deadline) - Date.now() : Infinity);
|
|
91
|
+
const deadline = Date.now() + remaining;
|
|
92
|
+
const controller = AbortSignal.timeout(Math.max(0, remaining));
|
|
93
|
+
const caller = detachedSignal ?? this.options.signal;
|
|
94
|
+
const signal = caller ? AbortSignal.any([controller, caller]) : controller;
|
|
95
|
+
const beforeSend = () => {
|
|
96
|
+
signal.throwIfAborted();
|
|
97
|
+
this.store.checkContinuity?.();
|
|
98
|
+
if (expires)
|
|
99
|
+
live(expires);
|
|
100
|
+
if (this.replacement)
|
|
101
|
+
live(this.replacement.deadline);
|
|
102
|
+
};
|
|
103
|
+
try {
|
|
104
|
+
for (let attempt = 0; attempt < 4; attempt++) {
|
|
105
|
+
beforeSend();
|
|
106
|
+
try {
|
|
107
|
+
const reply = await this.transport({
|
|
108
|
+
endpoint,
|
|
109
|
+
privateKey,
|
|
110
|
+
type,
|
|
111
|
+
body,
|
|
112
|
+
reknockBody,
|
|
113
|
+
assignment: phase === "assignment" ||
|
|
114
|
+
phase === "assignment-refresh" ||
|
|
115
|
+
phase === "recovery-issue",
|
|
116
|
+
signal,
|
|
117
|
+
beforeSend,
|
|
118
|
+
});
|
|
119
|
+
if (type === 12)
|
|
120
|
+
return {};
|
|
121
|
+
if (!reply)
|
|
122
|
+
throw new AgentLifecycleError("INVALID_REPLY");
|
|
123
|
+
let parsed;
|
|
124
|
+
try {
|
|
125
|
+
const value = (0, strict_json_js_1.parseStrictJson)(reply.body, 4096);
|
|
126
|
+
if (!(0, strict_json_js_1.isStrictJsonObject)(value))
|
|
127
|
+
throw new AgentLifecycleError("INVALID_REPLY");
|
|
128
|
+
parsed = value;
|
|
129
|
+
}
|
|
130
|
+
finally {
|
|
131
|
+
reply.body.fill(0);
|
|
132
|
+
}
|
|
133
|
+
if (phase === "session")
|
|
134
|
+
return parsed;
|
|
135
|
+
const envelope = (0, agent_state_js_1.exactObject)(parsed, phase === "registration"
|
|
136
|
+
? "errCode errMsg aspId"
|
|
137
|
+
: "errCode errMsg retryAfterSeconds list");
|
|
138
|
+
if (typeof envelope.errCode !== "string" || !/^(?:0|[1-9]\d{4})$/.test(envelope.errCode))
|
|
139
|
+
throw new AgentLifecycleError("INVALID_REPLY");
|
|
140
|
+
if (envelope.errMsg !== undefined && typeof envelope.errMsg !== "string")
|
|
141
|
+
throw new AgentLifecycleError("INVALID_REPLY");
|
|
142
|
+
if (phase === "registration" && envelope.aspId !== "agent")
|
|
143
|
+
throw new AgentLifecycleError("INVALID_REPLY");
|
|
144
|
+
if (envelope.errCode === "0") {
|
|
145
|
+
if (envelope.retryAfterSeconds !== undefined ||
|
|
146
|
+
(phase !== "registration" && envelope.errMsg !== undefined))
|
|
147
|
+
throw new AgentLifecycleError("INVALID_REPLY");
|
|
148
|
+
return envelope;
|
|
149
|
+
}
|
|
150
|
+
const allowed = phase === "assignment" || phase === "assignment-refresh"
|
|
151
|
+
? [
|
|
152
|
+
"52200",
|
|
153
|
+
"52201",
|
|
154
|
+
"52202",
|
|
155
|
+
"52203",
|
|
156
|
+
"52204",
|
|
157
|
+
"52205",
|
|
158
|
+
...(phase === "assignment" ? ["52106", "52107", "52108", "52109"] : []),
|
|
159
|
+
]
|
|
160
|
+
: phase === "registration"
|
|
161
|
+
? [
|
|
162
|
+
"52100",
|
|
163
|
+
"52101",
|
|
164
|
+
"52102",
|
|
165
|
+
"52103",
|
|
166
|
+
"52104",
|
|
167
|
+
"52105",
|
|
168
|
+
"52106",
|
|
169
|
+
"52107",
|
|
170
|
+
"52108",
|
|
171
|
+
"52109",
|
|
172
|
+
"52110",
|
|
173
|
+
"52111",
|
|
174
|
+
"52112",
|
|
175
|
+
]
|
|
176
|
+
: phase === "completion"
|
|
177
|
+
? ["52300", "52301", "52302", "52303", "52304"]
|
|
178
|
+
: phase === "recovery-issue"
|
|
179
|
+
? ["52400", "52401", "52402", "52403", "52404", "52405", "52406"]
|
|
180
|
+
: ["52410", "52411", "52412", "52413", "52414"];
|
|
181
|
+
if (!allowed.includes(envelope.errCode) || envelope.list !== undefined)
|
|
182
|
+
throw new AgentLifecycleError("INVALID_REPLY");
|
|
183
|
+
const retry = envelope.retryAfterSeconds;
|
|
184
|
+
const retryCodes = ["52200", "52204", "52300", "52400", "52404", "52410"];
|
|
185
|
+
if ((retry !== undefined &&
|
|
186
|
+
(!retryCodes.includes(envelope.errCode) ||
|
|
187
|
+
typeof retry !== "bigint" ||
|
|
188
|
+
retry <= 0n ||
|
|
189
|
+
retry > 9223372036n)) ||
|
|
190
|
+
(["52204", "52404"].includes(envelope.errCode) && retry === undefined))
|
|
191
|
+
throw new AgentLifecycleError("INVALID_REPLY");
|
|
192
|
+
throw new AgentLifecycleError(envelope.errCode, retry === undefined ? undefined : Number(retry) * 1000);
|
|
193
|
+
}
|
|
194
|
+
catch (error) {
|
|
195
|
+
const retryable = error instanceof agent_transport_js_1.AgentTransportError ||
|
|
196
|
+
(error instanceof AgentLifecycleError &&
|
|
197
|
+
["52200", "52202", "52204", "52300", "52400", "52404", "52410"].includes(error.code) &&
|
|
198
|
+
(!error.code.startsWith("524") || !!error.retryAfterMs));
|
|
199
|
+
if (!retryable || attempt === 3 || phase === "session" || type === 12)
|
|
200
|
+
throw error;
|
|
201
|
+
beforeSend();
|
|
202
|
+
const pause = Math.max(error instanceof AgentLifecycleError ? (error.retryAfterMs ?? 0) : 0, (0, node_crypto_1.randomInt)(Math.min(8000, 500 * 2 ** attempt)));
|
|
203
|
+
// Retry-After is a lower bound. Never let Node clamp a large delay to 1 ms.
|
|
204
|
+
if (pause >= deadline - Date.now())
|
|
205
|
+
throw error;
|
|
206
|
+
await (0, promises_1.setTimeout)(pause, undefined, { signal });
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
throw new AgentLifecycleError("RETRY_EXHAUSTED");
|
|
210
|
+
}
|
|
211
|
+
finally {
|
|
212
|
+
privateKey.fill(0);
|
|
213
|
+
body.fill(0);
|
|
214
|
+
reknockBody?.fill(0);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
async assignment(state, mode, enrollment) {
|
|
218
|
+
const envelope = await this.exchange(state, this.hub(), 5, {
|
|
219
|
+
usrId: "",
|
|
220
|
+
devId: state.agent_id,
|
|
221
|
+
aspId: "agent",
|
|
222
|
+
usrData: {
|
|
223
|
+
query: "cell_assignment",
|
|
224
|
+
version: 1,
|
|
225
|
+
mode,
|
|
226
|
+
request_nonce: (0, node_crypto_1.randomBytes)(32).toString("base64url"),
|
|
227
|
+
...(enrollment ? { credential: enrollment } : {}),
|
|
228
|
+
},
|
|
229
|
+
}, mode === "enroll" ? "assignment" : "assignment-refresh");
|
|
230
|
+
const list = (0, agent_state_js_1.exactObject)(envelope.list, `query version mode agent_id assignment${mode === "enroll" ? " registration assignment_ticket assignment_ticket_expires_at" : ""}`);
|
|
231
|
+
if (list.query !== "cell_assignment" ||
|
|
232
|
+
list.version !== 1n ||
|
|
233
|
+
list.mode !== mode ||
|
|
234
|
+
list.agent_id !== state.agent_id)
|
|
235
|
+
throw new AgentLifecycleError("INVALID_ASSIGNMENT_REPLY");
|
|
236
|
+
const assignment = (0, agent_state_js_1.validateAssignment)(list.assignment);
|
|
237
|
+
if ((0, agent_state_js_1.canonicalTime)(assignment.lease_expires_at) <= Date.now())
|
|
238
|
+
throw new AgentLifecycleError("ASSIGNMENT_EXPIRED");
|
|
239
|
+
return { list, assignment };
|
|
240
|
+
}
|
|
241
|
+
async refresh(state, allowRecovered = false) {
|
|
242
|
+
if (state.pending_credential_recovery ||
|
|
243
|
+
state.pending_credential_recovery_issue ||
|
|
244
|
+
state.pending_activation ||
|
|
245
|
+
state.pending_completion ||
|
|
246
|
+
!state.registered_at)
|
|
247
|
+
throw new AgentLifecycleError("INCOMPLETE_STATE");
|
|
248
|
+
if (!allowRecovered && state.credential_recovery_refresh_required)
|
|
249
|
+
throw new AgentLifecycleError("RECOVERED_ASSIGNMENT_REFRESH_REQUIRED");
|
|
250
|
+
const { assignment } = await this.assignment(state, "refresh");
|
|
251
|
+
const old = state.assignment;
|
|
252
|
+
if (!old ||
|
|
253
|
+
assignment.assignment_generation < old.assignment_generation ||
|
|
254
|
+
(assignment.assignment_generation === old.assignment_generation &&
|
|
255
|
+
(assignment.cell_id !== old.cell_id ||
|
|
256
|
+
assignment.endpoint_revision < old.endpoint_revision)) ||
|
|
257
|
+
(assignment.assignment_generation === old.assignment_generation &&
|
|
258
|
+
assignment.endpoint_revision === old.endpoint_revision &&
|
|
259
|
+
(assignment.nhp_udp_endpoint.host !== old.nhp_udp_endpoint.host ||
|
|
260
|
+
assignment.nhp_udp_endpoint.server_public_key_b64 !==
|
|
261
|
+
old.nhp_udp_endpoint.server_public_key_b64)))
|
|
262
|
+
throw new AgentLifecycleError("ASSIGNMENT_DOWNGRADE");
|
|
263
|
+
const next = copy(state);
|
|
264
|
+
next.schema_version = 8;
|
|
265
|
+
next.assignment = assignment;
|
|
266
|
+
delete next.credential_recovery_refresh_required;
|
|
267
|
+
await this.store.save(next, this.options.signal);
|
|
268
|
+
return next;
|
|
269
|
+
}
|
|
270
|
+
async complete(state) {
|
|
271
|
+
const pending = state.pending_completion;
|
|
272
|
+
live(pending.recovery_expires_at);
|
|
273
|
+
const envelope = await this.exchange(state, state.assignment.nhp_udp_endpoint, 5, {
|
|
274
|
+
usrId: "",
|
|
275
|
+
devId: state.agent_id,
|
|
276
|
+
aspId: "agent",
|
|
277
|
+
usrData: {
|
|
278
|
+
query: "agent_registration_completion",
|
|
279
|
+
version: 1,
|
|
280
|
+
device_api_key: pending.device_api_key,
|
|
281
|
+
},
|
|
282
|
+
}, "completion", pending.recovery_expires_at);
|
|
283
|
+
const list = (0, agent_state_js_1.exactObject)(envelope.list, "query version device_api_key_id");
|
|
284
|
+
if (list.query !== "agent_registration_completion" ||
|
|
285
|
+
list.version !== 1n ||
|
|
286
|
+
typeof list.device_api_key_id !== "string" ||
|
|
287
|
+
!KEY_ID.test(list.device_api_key_id))
|
|
288
|
+
throw new AgentLifecycleError("INVALID_COMPLETION_REPLY");
|
|
289
|
+
const next = copy(state);
|
|
290
|
+
next.device_api_key = pending.device_api_key;
|
|
291
|
+
next.device_api_key_id = list.device_api_key_id;
|
|
292
|
+
next.registered_at = new Date().toISOString();
|
|
293
|
+
delete next.pending_completion;
|
|
294
|
+
await this.store.save(next, AbortSignal.timeout(10_000));
|
|
295
|
+
return next;
|
|
296
|
+
}
|
|
297
|
+
async enroll(state) {
|
|
298
|
+
// Pre-v6 completion records have no authenticated deadline anchor. Keep them
|
|
299
|
+
// readable, but never invent a deadline or replay them during an upgrade.
|
|
300
|
+
if (state.pending_completion && state.pending_completion.recovery_expires_at === undefined)
|
|
301
|
+
throw new AgentLifecycleError("RECOVERY_MIGRATION_REQUIRED");
|
|
302
|
+
if (state.schema_version !== 8) {
|
|
303
|
+
state = copy(state);
|
|
304
|
+
state.schema_version = 8;
|
|
305
|
+
// Persist any legacy activation horizon before replaying authority-bound work.
|
|
306
|
+
await this.store.save(state, this.options.signal);
|
|
307
|
+
}
|
|
308
|
+
if (state.pending_completion)
|
|
309
|
+
return this.complete(state);
|
|
310
|
+
let enrollment = this.options.enrollmentCredential;
|
|
311
|
+
if (this.options.enrollmentCredentialProvider) {
|
|
312
|
+
enrollment = await this.options.enrollmentCredentialProvider({ agentID: state.agent_id, pendingActivationRecovery: !!state.pending_activation }, this.options.signal ?? AbortSignal.timeout(30_000));
|
|
313
|
+
}
|
|
314
|
+
credential(enrollment);
|
|
315
|
+
let code = enrollment;
|
|
316
|
+
let pending = state.pending_activation;
|
|
317
|
+
if (!pending) {
|
|
318
|
+
const { assignment, list } = await this.assignment(state, "enroll", enrollment);
|
|
319
|
+
const registration = (0, agent_state_js_1.exactObject)(list.registration, "key_id key_kind");
|
|
320
|
+
const kind = registration.key_kind;
|
|
321
|
+
if (typeof registration.key_id !== "string" ||
|
|
322
|
+
!KEY_ID.test(registration.key_id) ||
|
|
323
|
+
!(this.options.headless ? ["bootstrap", "connector_bootstrap"] : ["account"]).includes(kind))
|
|
324
|
+
throw new AgentLifecycleError("ENROLLMENT_KIND_REFUSED");
|
|
325
|
+
if (typeof list.assignment_ticket !== "string" ||
|
|
326
|
+
!/^[\x21-\x7e]{1,2304}$/.test(list.assignment_ticket) ||
|
|
327
|
+
typeof list.assignment_ticket_expires_at !== "string")
|
|
328
|
+
throw new AgentLifecycleError("INVALID_ASSIGNMENT_TICKET");
|
|
329
|
+
const ticketExpiry = (0, agent_state_js_1.canonicalTime)(list.assignment_ticket_expires_at);
|
|
330
|
+
if (ticketExpiry <= Date.now() || ticketExpiry >= (0, agent_state_js_1.canonicalTime)(assignment.lease_expires_at))
|
|
331
|
+
throw new AgentLifecycleError("INVALID_ASSIGNMENT_TICKET");
|
|
332
|
+
if (kind === "account") {
|
|
333
|
+
if (!this.options.otpProvider || ticketExpiry - Date.now() < 630_000)
|
|
334
|
+
throw new AgentLifecycleError("OTP_NOT_AVAILABLE");
|
|
335
|
+
await this.exchange(state, assignment.nhp_udp_endpoint, 12, {
|
|
336
|
+
usrId: registration.key_id,
|
|
337
|
+
devId: state.agent_id,
|
|
338
|
+
aspId: "agent",
|
|
339
|
+
pass: enrollment,
|
|
340
|
+
usrData: {
|
|
341
|
+
query: "agent_registration_otp",
|
|
342
|
+
version: 1,
|
|
343
|
+
assignment_ticket: list.assignment_ticket,
|
|
344
|
+
},
|
|
345
|
+
}, "registration");
|
|
346
|
+
code = await this.options.otpProvider({
|
|
347
|
+
agentID: state.agent_id,
|
|
348
|
+
cellID: assignment.cell_id,
|
|
349
|
+
assignmentTicketExpiresAt: list.assignment_ticket_expires_at,
|
|
350
|
+
pendingActivationRecovery: false,
|
|
351
|
+
}, this.options.signal ?? AbortSignal.timeout(30_000));
|
|
352
|
+
if (!/^\d{8}$/.test(code))
|
|
353
|
+
throw new AgentLifecycleError("INVALID_OTP");
|
|
354
|
+
}
|
|
355
|
+
pending = {
|
|
356
|
+
agent_id: state.agent_id,
|
|
357
|
+
agent_public_key_b64: state.public_key_b64,
|
|
358
|
+
assignment,
|
|
359
|
+
registration: { key_id: registration.key_id, key_kind: kind },
|
|
360
|
+
assignment_ticket: list.assignment_ticket,
|
|
361
|
+
assignment_ticket_expires_at: list.assignment_ticket_expires_at,
|
|
362
|
+
recovery_anchor_ticket_expires_at: this.replacement?.anchor ?? list.assignment_ticket_expires_at,
|
|
363
|
+
recovery_expires_at: this.replacement?.deadline ??
|
|
364
|
+
new Date(ticketExpiry + agent_state_js_1.RECOVERY_HORIZON_MS).toISOString().replace(".000Z", "Z"),
|
|
365
|
+
hostname: this.options.hostname,
|
|
366
|
+
agent_version: this.options.version,
|
|
367
|
+
enrollment_credential_fingerprint_b64: fingerprint("activation-enrollment", enrollment),
|
|
368
|
+
};
|
|
369
|
+
const next = copy(state);
|
|
370
|
+
next.assignment = assignment;
|
|
371
|
+
next.pending_activation = pending;
|
|
372
|
+
await this.store.save(next, this.options.signal);
|
|
373
|
+
state = next;
|
|
374
|
+
}
|
|
375
|
+
else {
|
|
376
|
+
live(pending.recovery_expires_at);
|
|
377
|
+
const expected = Buffer.from(pending.enrollment_credential_fingerprint_b64);
|
|
378
|
+
const supplied = Buffer.from(fingerprint("activation-enrollment", enrollment));
|
|
379
|
+
if (expected.length !== supplied.length ||
|
|
380
|
+
!(0, node_crypto_1.timingSafeEqual)(expected, supplied) ||
|
|
381
|
+
pending.hostname !== this.options.hostname ||
|
|
382
|
+
pending.agent_version !== this.options.version)
|
|
383
|
+
throw new AgentLifecycleError("ACTIVATION_INPUT_CHANGED");
|
|
384
|
+
if (pending.registration.key_kind === "account") {
|
|
385
|
+
if (!this.options.otpProvider)
|
|
386
|
+
throw new AgentLifecycleError("OTP_NOT_AVAILABLE");
|
|
387
|
+
code = await this.options.otpProvider({
|
|
388
|
+
agentID: state.agent_id,
|
|
389
|
+
cellID: pending.assignment.cell_id,
|
|
390
|
+
assignmentTicketExpiresAt: pending.assignment_ticket_expires_at,
|
|
391
|
+
pendingActivationRecovery: true,
|
|
392
|
+
}, this.options.signal ?? AbortSignal.timeout(30_000));
|
|
393
|
+
if (!/^\d{8}$/.test(code))
|
|
394
|
+
throw new AgentLifecycleError("INVALID_OTP");
|
|
395
|
+
}
|
|
396
|
+
else if (!this.options.headless)
|
|
397
|
+
throw new AgentLifecycleError("ENROLLMENT_KIND_REFUSED");
|
|
398
|
+
}
|
|
399
|
+
try {
|
|
400
|
+
await this.exchange(state, pending.assignment.nhp_udp_endpoint, 13, {
|
|
401
|
+
usrId: pending.registration.key_id,
|
|
402
|
+
devId: state.agent_id,
|
|
403
|
+
aspId: "agent",
|
|
404
|
+
otp: code,
|
|
405
|
+
usrData: {
|
|
406
|
+
hostname: pending.hostname,
|
|
407
|
+
version: pending.agent_version,
|
|
408
|
+
assignment_ticket: pending.assignment_ticket,
|
|
409
|
+
},
|
|
410
|
+
}, "registration", pending.recovery_expires_at);
|
|
411
|
+
}
|
|
412
|
+
catch (error) {
|
|
413
|
+
if (!this.replacement &&
|
|
414
|
+
error instanceof AgentLifecycleError &&
|
|
415
|
+
(error.code === "52111" ||
|
|
416
|
+
(error.code === "52101" && pending.registration.key_kind === "account"))) {
|
|
417
|
+
// Keep the durable record. The replacement is committed only after a fresh Hub result.
|
|
418
|
+
const anchor = pending.recovery_anchor_ticket_expires_at;
|
|
419
|
+
const deadline = pending.recovery_expires_at;
|
|
420
|
+
const next = copy(state);
|
|
421
|
+
delete next.pending_activation;
|
|
422
|
+
// Replacement follows the same registration path but must retain the original horizon.
|
|
423
|
+
return new Lifecycle(this.store, {
|
|
424
|
+
...this.options,
|
|
425
|
+
enrollmentCredential: enrollment,
|
|
426
|
+
enrollmentCredentialProvider: undefined,
|
|
427
|
+
}, this.transport, { anchor, deadline }).enroll(next);
|
|
428
|
+
}
|
|
429
|
+
throw error;
|
|
430
|
+
}
|
|
431
|
+
live(pending.recovery_expires_at);
|
|
432
|
+
const next = copy(state);
|
|
433
|
+
next.pending_completion = {
|
|
434
|
+
device_api_key: `lv_live_${(0, node_crypto_1.randomBytes)(32).toString("base64url")}`,
|
|
435
|
+
cell_id: pending.assignment.cell_id,
|
|
436
|
+
assignment_generation: pending.assignment.assignment_generation,
|
|
437
|
+
recovery_anchor_ticket_expires_at: pending.recovery_anchor_ticket_expires_at,
|
|
438
|
+
recovery_expires_at: pending.recovery_expires_at,
|
|
439
|
+
};
|
|
440
|
+
next.enrollment_credential_kind = pending.registration.key_kind;
|
|
441
|
+
delete next.pending_activation;
|
|
442
|
+
await this.store.save(next, this.options.signal);
|
|
443
|
+
return this.complete(next);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
const receiptAuthority = new WeakMap();
|
|
447
|
+
class AgentRuntime {
|
|
448
|
+
store;
|
|
449
|
+
transport;
|
|
450
|
+
client;
|
|
451
|
+
#state;
|
|
452
|
+
#options;
|
|
453
|
+
#renewal;
|
|
454
|
+
#credentialReload;
|
|
455
|
+
#nextRefreshAt = 0;
|
|
456
|
+
#closed = new AbortController();
|
|
457
|
+
constructor(state, store, options, transport) {
|
|
458
|
+
this.store = store;
|
|
459
|
+
this.transport = transport;
|
|
460
|
+
this.#state = copy(state);
|
|
461
|
+
this.#options = {
|
|
462
|
+
hub: options.hub,
|
|
463
|
+
offline: options.offline,
|
|
464
|
+
baseUrl: options.baseUrl,
|
|
465
|
+
fetch: options.fetch,
|
|
466
|
+
};
|
|
467
|
+
let cached = state.device_api_key;
|
|
468
|
+
let expiresAt = Date.now() + 60_000;
|
|
469
|
+
const agentID = state.agent_id;
|
|
470
|
+
const publicKey = state.public_key_b64;
|
|
471
|
+
const fetch = options.fetch ?? globalThis.fetch;
|
|
472
|
+
this.client = new client_js_1.QURLClient({
|
|
473
|
+
apiKey: cached,
|
|
474
|
+
baseUrl: options.baseUrl,
|
|
475
|
+
fetch: async (url, init) => {
|
|
476
|
+
this.#closed.signal.throwIfAborted();
|
|
477
|
+
if (Date.now() >= expiresAt) {
|
|
478
|
+
this.#credentialReload ??= (async () => {
|
|
479
|
+
const current = await store.load(this.#closed.signal);
|
|
480
|
+
ready(current);
|
|
481
|
+
checkIdentity(current, agentID);
|
|
482
|
+
if (current.public_key_b64 !== publicKey)
|
|
483
|
+
throw new AgentLifecycleError("IDENTITY_CHANGED");
|
|
484
|
+
cached = current.device_api_key;
|
|
485
|
+
expiresAt = Date.now() + 60_000;
|
|
486
|
+
})().finally(() => {
|
|
487
|
+
this.#credentialReload = undefined;
|
|
488
|
+
});
|
|
489
|
+
await this.#credentialReload;
|
|
490
|
+
}
|
|
491
|
+
this.#closed.signal.throwIfAborted();
|
|
492
|
+
const headers = new Headers(init?.headers);
|
|
493
|
+
headers.set("Authorization", `Bearer ${cached}`);
|
|
494
|
+
return fetch(url, {
|
|
495
|
+
...init,
|
|
496
|
+
headers,
|
|
497
|
+
signal: init?.signal
|
|
498
|
+
? AbortSignal.any([init.signal, this.#closed.signal])
|
|
499
|
+
: this.#closed.signal,
|
|
500
|
+
});
|
|
501
|
+
},
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
get agentID() {
|
|
505
|
+
return this.#requireState().agent_id;
|
|
506
|
+
}
|
|
507
|
+
assignment() {
|
|
508
|
+
return structuredClone(this.#requireState().assignment);
|
|
509
|
+
}
|
|
510
|
+
#requireState() {
|
|
511
|
+
this.#closed.signal.throwIfAborted();
|
|
512
|
+
if (!this.#state)
|
|
513
|
+
throw new AgentLifecycleError("CLOSED");
|
|
514
|
+
return this.#state;
|
|
515
|
+
}
|
|
516
|
+
async refresh(signal) {
|
|
517
|
+
if (this.#options.offline)
|
|
518
|
+
throw new AgentLifecycleError("OFFLINE");
|
|
519
|
+
this.#requireState();
|
|
520
|
+
signal = signal ? AbortSignal.any([signal, this.#closed.signal]) : this.#closed.signal;
|
|
521
|
+
this.#renewal ??= this.store
|
|
522
|
+
.withLock(async (locked) => {
|
|
523
|
+
const state = await locked.load(signal);
|
|
524
|
+
checkIdentity(state, this.agentID);
|
|
525
|
+
if (state.public_key_b64 !== this.#requireState().public_key_b64)
|
|
526
|
+
throw new AgentLifecycleError("IDENTITY_CHANGED");
|
|
527
|
+
const next = await new Lifecycle(locked, {
|
|
528
|
+
...this.#options,
|
|
529
|
+
signal,
|
|
530
|
+
}, this.transport).refresh(state, true);
|
|
531
|
+
this.#closed.signal.throwIfAborted();
|
|
532
|
+
this.#state = next;
|
|
533
|
+
}, signal)
|
|
534
|
+
.finally(() => {
|
|
535
|
+
this.#renewal = undefined;
|
|
536
|
+
this.#nextRefreshAt = Date.now() + 30_000;
|
|
537
|
+
});
|
|
538
|
+
await this.#renewal;
|
|
539
|
+
}
|
|
540
|
+
async knock(knockResourceID, options) {
|
|
541
|
+
if (!/^[0-9a-f]{16}$/.test(options.runID) ||
|
|
542
|
+
typeof options.runAttempt !== "bigint" ||
|
|
543
|
+
options.runAttempt <= 0n ||
|
|
544
|
+
options.runAttempt > UINT64_MAX ||
|
|
545
|
+
!(0, crid_js_1.parseCrid)(options.protectedResourceID, true) ||
|
|
546
|
+
options.protectedResourceID === knockResourceID ||
|
|
547
|
+
!knockResourceID ||
|
|
548
|
+
knockResourceID.trim() !== knockResourceID)
|
|
549
|
+
throw new AgentLifecycleError("INVALID_KNOCK_INPUT");
|
|
550
|
+
let state = this.#requireState();
|
|
551
|
+
const lease = (0, agent_state_js_1.canonicalTime)(state.assignment.lease_expires_at);
|
|
552
|
+
if (lease - Date.now() <= 300_000 &&
|
|
553
|
+
Date.now() >= this.#nextRefreshAt &&
|
|
554
|
+
!this.#options.offline) {
|
|
555
|
+
try {
|
|
556
|
+
await this.refresh(options.signal);
|
|
557
|
+
}
|
|
558
|
+
catch (error) {
|
|
559
|
+
if (lease <= Date.now())
|
|
560
|
+
throw error;
|
|
561
|
+
}
|
|
562
|
+
state = this.#requireState();
|
|
563
|
+
}
|
|
564
|
+
if ((0, agent_state_js_1.canonicalTime)(state.assignment.lease_expires_at) <= Date.now())
|
|
565
|
+
throw new AgentLifecycleError("ASSIGNMENT_EXPIRED");
|
|
566
|
+
const assignment = structuredClone(state.assignment);
|
|
567
|
+
const body = {
|
|
568
|
+
headerType: 1,
|
|
569
|
+
usrId: state.agent_id,
|
|
570
|
+
devId: state.agent_id,
|
|
571
|
+
aspId: "agent",
|
|
572
|
+
resId: knockResourceID,
|
|
573
|
+
runId: options.runID,
|
|
574
|
+
runAttempt: options.runAttempt,
|
|
575
|
+
protected_resource_id: options.protectedResourceID,
|
|
576
|
+
};
|
|
577
|
+
const ack = await new Lifecycle(this.store, {
|
|
578
|
+
...this.#options,
|
|
579
|
+
signal: options.signal
|
|
580
|
+
? AbortSignal.any([options.signal, this.#closed.signal])
|
|
581
|
+
: this.#closed.signal,
|
|
582
|
+
}, this.transport).exchange(state, assignment.nhp_udp_endpoint, 1, body, "session", undefined, {
|
|
583
|
+
...body,
|
|
584
|
+
headerType: 8,
|
|
585
|
+
});
|
|
586
|
+
const parsed = (0, agent_state_js_1.exactObject)(ack, "errCode errMsg sessId cellId sessIssuedAtMillis runId runAttempt resHost opnTime aspToken agentAddr acTokens preActions redirectUrl");
|
|
587
|
+
if (typeof parsed.errCode !== "string" || !/^(?:0|[1-9]\d*)$/.test(parsed.errCode))
|
|
588
|
+
throw new AgentLifecycleError("INVALID_KNOCK_REPLY");
|
|
589
|
+
if (parsed.errCode !== "0") {
|
|
590
|
+
if (typeof parsed.errMsg !== "string" ||
|
|
591
|
+
!parsed.errMsg.trim() ||
|
|
592
|
+
parsed.errMsg.trim() !== parsed.errMsg ||
|
|
593
|
+
(parsed.opnTime !== undefined && parsed.opnTime !== 0n) ||
|
|
594
|
+
Object.keys(parsed).some((key) => !["errCode", "errMsg", "opnTime"].includes(key)))
|
|
595
|
+
throw new AgentLifecycleError("INVALID_KNOCK_REPLY");
|
|
596
|
+
throw new AgentLifecycleError(parsed.errCode);
|
|
597
|
+
}
|
|
598
|
+
if (parsed.errMsg !== undefined ||
|
|
599
|
+
typeof parsed.sessId !== "bigint" ||
|
|
600
|
+
parsed.sessId <= 0n ||
|
|
601
|
+
parsed.sessId > UINT64_MAX ||
|
|
602
|
+
parsed.cellId !== assignment.cell_id ||
|
|
603
|
+
parsed.runId !== options.runID ||
|
|
604
|
+
parsed.runAttempt !== options.runAttempt ||
|
|
605
|
+
typeof parsed.sessIssuedAtMillis !== "bigint" ||
|
|
606
|
+
parsed.sessIssuedAtMillis <= 0n ||
|
|
607
|
+
parsed.sessIssuedAtMillis > (1n << 63n) - 1n ||
|
|
608
|
+
typeof parsed.opnTime !== "bigint" ||
|
|
609
|
+
parsed.opnTime <= 0n ||
|
|
610
|
+
parsed.opnTime > 0xffffffffn ||
|
|
611
|
+
typeof parsed.agentAddr !== "string" ||
|
|
612
|
+
!validAgentAddress(parsed.agentAddr) ||
|
|
613
|
+
!(0, strict_json_js_1.isStrictJsonObject)(parsed.acTokens) ||
|
|
614
|
+
!(0, strict_json_js_1.isStrictJsonObject)(parsed.resHost))
|
|
615
|
+
throw new AgentLifecycleError("INVALID_KNOCK_REPLY");
|
|
616
|
+
if (parsed.preActions !== undefined &&
|
|
617
|
+
(!(0, strict_json_js_1.isStrictJsonObject)(parsed.preActions) ||
|
|
618
|
+
Object.values(parsed.preActions).some((value) => value !== null)))
|
|
619
|
+
throw new AgentLifecycleError("UNSUPPORTED_PREACCESS_ACTION");
|
|
620
|
+
const token = parsed.acTokens[knockResourceID];
|
|
621
|
+
const resourceHost = parsed.resHost[knockResourceID];
|
|
622
|
+
if (typeof token !== "string" ||
|
|
623
|
+
!token ||
|
|
624
|
+
token.trim() !== token ||
|
|
625
|
+
typeof resourceHost !== "string" ||
|
|
626
|
+
!resourceHost ||
|
|
627
|
+
resourceHost.trim() !== resourceHost)
|
|
628
|
+
throw new AgentLifecycleError("INVALID_KNOCK_REPLY");
|
|
629
|
+
const receipt = Object.freeze({
|
|
630
|
+
cellID: parsed.cellId,
|
|
631
|
+
sessionID: parsed.sessId,
|
|
632
|
+
sessionIssuedAtMillis: parsed.sessIssuedAtMillis,
|
|
633
|
+
runID: options.runID,
|
|
634
|
+
runAttempt: options.runAttempt,
|
|
635
|
+
});
|
|
636
|
+
receiptAuthority.set(receipt, {
|
|
637
|
+
agentID: state.agent_id,
|
|
638
|
+
endpoint: assignment.nhp_udp_endpoint,
|
|
639
|
+
});
|
|
640
|
+
return {
|
|
641
|
+
token,
|
|
642
|
+
resourceHost,
|
|
643
|
+
openTime: Number(parsed.opnTime),
|
|
644
|
+
agentAddress: parsed.agentAddr,
|
|
645
|
+
receipt,
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
async retire(receipt, signal) {
|
|
649
|
+
const state = this.#requireState();
|
|
650
|
+
const authority = receiptAuthority.get(receipt);
|
|
651
|
+
if (!authority || authority.agentID !== state.agent_id)
|
|
652
|
+
throw new AgentLifecycleError("INVALID_SESSION_RECEIPT");
|
|
653
|
+
const body = {
|
|
654
|
+
headerType: 16,
|
|
655
|
+
aspId: "agent",
|
|
656
|
+
cellId: receipt.cellID,
|
|
657
|
+
sessId: receipt.sessionID,
|
|
658
|
+
sessIssuedAtMillis: receipt.sessionIssuedAtMillis,
|
|
659
|
+
runId: receipt.runID,
|
|
660
|
+
runAttempt: receipt.runAttempt,
|
|
661
|
+
};
|
|
662
|
+
const ack = await new Lifecycle(this.store, {
|
|
663
|
+
...this.#options,
|
|
664
|
+
signal: signal ? AbortSignal.any([signal, this.#closed.signal]) : this.#closed.signal,
|
|
665
|
+
}, this.transport).exchange(state, authority.endpoint, 16, body, "session");
|
|
666
|
+
(0, agent_state_js_1.exactObject)(ack, "errCode errMsg cellId sessId sessIssuedAtMillis runId runAttempt closeEventId state");
|
|
667
|
+
if (typeof ack.errCode !== "string" || !/^(?:0|[1-9]\d*)$/.test(ack.errCode))
|
|
668
|
+
throw new AgentLifecycleError("INVALID_RETIREMENT_REPLY");
|
|
669
|
+
if (ack.errCode !== "0") {
|
|
670
|
+
if (typeof ack.errMsg !== "string" ||
|
|
671
|
+
!ack.errMsg.trim() ||
|
|
672
|
+
ack.errMsg.trim() !== ack.errMsg ||
|
|
673
|
+
Object.keys(ack).some((key) => !["errCode", "errMsg"].includes(key)))
|
|
674
|
+
throw new AgentLifecycleError("INVALID_RETIREMENT_REPLY");
|
|
675
|
+
throw new AgentLifecycleError(ack.errCode);
|
|
676
|
+
}
|
|
677
|
+
if (ack.errMsg !== undefined ||
|
|
678
|
+
ack.cellId !== receipt.cellID ||
|
|
679
|
+
ack.sessId !== receipt.sessionID ||
|
|
680
|
+
ack.sessIssuedAtMillis !== receipt.sessionIssuedAtMillis ||
|
|
681
|
+
ack.runId !== receipt.runID ||
|
|
682
|
+
ack.runAttempt !== receipt.runAttempt ||
|
|
683
|
+
typeof ack.closeEventId !== "string" ||
|
|
684
|
+
!/^[0-9a-f]{32}$/.test(ack.closeEventId) ||
|
|
685
|
+
!["closing", "closed"].includes(ack.state))
|
|
686
|
+
throw new AgentLifecycleError("INVALID_RETIREMENT_REPLY");
|
|
687
|
+
return { closeEventID: ack.closeEventId, state: ack.state };
|
|
688
|
+
}
|
|
689
|
+
async close() {
|
|
690
|
+
this.#closed.abort(new AgentLifecycleError("CLOSED"));
|
|
691
|
+
this.#state = undefined;
|
|
692
|
+
// Drain all runtime-owned store operations before the caller closes the store.
|
|
693
|
+
await Promise.allSettled([this.#renewal, this.#credentialReload]);
|
|
694
|
+
}
|
|
695
|
+
toJSON() {
|
|
696
|
+
return { agentID: this.#state?.agent_id, closed: this.#closed.signal.aborted };
|
|
697
|
+
}
|
|
698
|
+
[Symbol.for("nodejs.util.inspect.custom")]() {
|
|
699
|
+
return this.toJSON();
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
exports.AgentRuntime = AgentRuntime;
|
|
703
|
+
async function connectAgentRuntime(store, options = {}) {
|
|
704
|
+
return connectWithTransport(store, options, agent_transport_js_1.nativeAgentTransport);
|
|
705
|
+
}
|
|
706
|
+
async function recoverAgentRuntime(store, recoveryCredential, options = {}) {
|
|
707
|
+
return recoverWithTransport(store, recoveryCredential, options, agent_transport_js_1.nativeAgentTransport);
|
|
708
|
+
}
|
|
709
|
+
async function recoverWithTransport(store, provider, options, transport) {
|
|
710
|
+
const supplied = { ...options, hub: options.hub ? structuredClone(options.hub) : undefined };
|
|
711
|
+
const state = await store.withLock(async (locked) => {
|
|
712
|
+
let state = copy(await locked.load(options.signal));
|
|
713
|
+
checkIdentity(state, options.agentID);
|
|
714
|
+
if (!state.registered_at ||
|
|
715
|
+
!state.assignment ||
|
|
716
|
+
state.pending_activation ||
|
|
717
|
+
state.pending_completion)
|
|
718
|
+
throw new AgentLifecycleError("NOT_REGISTERED");
|
|
719
|
+
const lifecycle = new Lifecycle(locked, supplied, transport);
|
|
720
|
+
state.schema_version = 8;
|
|
721
|
+
if (state.credential_recovery_refresh_required)
|
|
722
|
+
return lifecycle.refresh(state, true);
|
|
723
|
+
const hub = lifecycle.hub();
|
|
724
|
+
let recoveryCredential;
|
|
725
|
+
for (let issueAttempt = 0; issueAttempt < 2 &&
|
|
726
|
+
(!state.pending_credential_recovery || state.pending_credential_recovery.needs_fresh_grant); issueAttempt++) {
|
|
727
|
+
const pending = state.pending_credential_recovery;
|
|
728
|
+
if (pending)
|
|
729
|
+
live(pending.recovery_expires_at);
|
|
730
|
+
if (state.pending_credential_recovery_issue)
|
|
731
|
+
live(state.pending_credential_recovery_issue.replay_not_after);
|
|
732
|
+
recoveryCredential ??=
|
|
733
|
+
typeof provider === "string"
|
|
734
|
+
? provider
|
|
735
|
+
: await provider(options.signal ?? AbortSignal.timeout(30_000));
|
|
736
|
+
if (!/^lv_(?:live|test)_/.test(recoveryCredential))
|
|
737
|
+
throw new AgentLifecycleError("INVALID_RECOVERY_CREDENTIAL");
|
|
738
|
+
(0, agent_state_js_1.canonicalKey)(recoveryCredential.slice(8), "base64url").fill(0);
|
|
739
|
+
const identity = fingerprint("credential-recovery", recoveryCredential);
|
|
740
|
+
if (!state.pending_credential_recovery_issue) {
|
|
741
|
+
const next = copy(state);
|
|
742
|
+
next.pending_credential_recovery_issue = {
|
|
743
|
+
agent_id: state.agent_id,
|
|
744
|
+
agent_public_key_b64: state.public_key_b64,
|
|
745
|
+
request_nonce: (0, node_crypto_1.randomBytes)(32).toString("base64url"),
|
|
746
|
+
recovery_credential_fingerprint_b64: identity,
|
|
747
|
+
replay_not_after: new Date(Math.min(Math.floor(Date.now() / 1000) * 1000 + agent_state_js_1.RECOVERY_HORIZON_MS, pending ? (0, agent_state_js_1.canonicalTime)(pending.recovery_expires_at) : Infinity))
|
|
748
|
+
.toISOString()
|
|
749
|
+
.replace(".000Z", "Z"),
|
|
750
|
+
hub_host: hub.host,
|
|
751
|
+
hub_port: hub.port,
|
|
752
|
+
hub_server_public_key_b64: hub.server_public_key_b64,
|
|
753
|
+
};
|
|
754
|
+
await locked.save(next, options.signal);
|
|
755
|
+
state = next;
|
|
756
|
+
}
|
|
757
|
+
const intent = state.pending_credential_recovery_issue;
|
|
758
|
+
if (intent.recovery_credential_fingerprint_b64 !== identity ||
|
|
759
|
+
intent.hub_host !== hub.host ||
|
|
760
|
+
intent.hub_port !== hub.port ||
|
|
761
|
+
intent.hub_server_public_key_b64 !== hub.server_public_key_b64)
|
|
762
|
+
throw new AgentLifecycleError("RECOVERY_INPUT_CHANGED");
|
|
763
|
+
let envelope;
|
|
764
|
+
try {
|
|
765
|
+
envelope = await lifecycle.exchange(state, hub, 5, {
|
|
766
|
+
usrId: "",
|
|
767
|
+
devId: state.agent_id,
|
|
768
|
+
aspId: "agent",
|
|
769
|
+
usrData: {
|
|
770
|
+
query: "cell_assignment",
|
|
771
|
+
version: 1,
|
|
772
|
+
mode: "recover",
|
|
773
|
+
request_nonce: intent.request_nonce,
|
|
774
|
+
credential: recoveryCredential,
|
|
775
|
+
},
|
|
776
|
+
}, "recovery-issue", intent.replay_not_after);
|
|
777
|
+
}
|
|
778
|
+
catch (error) {
|
|
779
|
+
if (error instanceof AgentLifecycleError &&
|
|
780
|
+
["52401", "52402", "52403", "52405", "52406"].includes(error.code)) {
|
|
781
|
+
const next = copy(state);
|
|
782
|
+
delete next.pending_credential_recovery_issue;
|
|
783
|
+
await locked.save(next, AbortSignal.timeout(10_000));
|
|
784
|
+
}
|
|
785
|
+
throw error;
|
|
786
|
+
}
|
|
787
|
+
const list = (0, agent_state_js_1.exactObject)(envelope.list, "query version mode agent_id assignment recovery_grant recovery_grant_issued_at recovery_grant_expires_at");
|
|
788
|
+
if (list.query !== "cell_assignment" ||
|
|
789
|
+
list.version !== 1n ||
|
|
790
|
+
list.mode !== "recover" ||
|
|
791
|
+
list.agent_id !== state.agent_id ||
|
|
792
|
+
typeof list.recovery_grant !== "string" ||
|
|
793
|
+
!/^qrg1\.[A-Za-z0-9_-]{1,2299}$/.test(list.recovery_grant) ||
|
|
794
|
+
typeof list.recovery_grant_issued_at !== "string" ||
|
|
795
|
+
typeof list.recovery_grant_expires_at !== "string")
|
|
796
|
+
throw new AgentLifecycleError("INVALID_RECOVERY_REPLY");
|
|
797
|
+
const assignment = (0, agent_state_js_1.validateAssignment)(list.assignment);
|
|
798
|
+
const issued = (0, agent_state_js_1.canonicalTime)(list.recovery_grant_issued_at);
|
|
799
|
+
const expires = (0, agent_state_js_1.canonicalTime)(list.recovery_grant_expires_at);
|
|
800
|
+
if (expires - issued !== 900_000 || expires >= (0, agent_state_js_1.canonicalTime)(assignment.lease_expires_at))
|
|
801
|
+
throw new AgentLifecycleError("INVALID_RECOVERY_REPLY");
|
|
802
|
+
const anchor = pending?.recovery_anchor_grant_expires_at ?? list.recovery_grant_expires_at;
|
|
803
|
+
const deadline = pending?.recovery_expires_at ??
|
|
804
|
+
new Date(expires + agent_state_js_1.RECOVERY_HORIZON_MS).toISOString().replace(".000Z", "Z");
|
|
805
|
+
live(deadline);
|
|
806
|
+
const next = copy(state);
|
|
807
|
+
next.assignment = assignment;
|
|
808
|
+
delete next.pending_credential_recovery_issue;
|
|
809
|
+
delete next.device_api_key;
|
|
810
|
+
delete next.device_api_key_id;
|
|
811
|
+
next.pending_credential_recovery = {
|
|
812
|
+
recovery_grant: list.recovery_grant,
|
|
813
|
+
recovery_grant_issued_at: list.recovery_grant_issued_at,
|
|
814
|
+
recovery_grant_expires_at: list.recovery_grant_expires_at,
|
|
815
|
+
recovery_anchor_grant_expires_at: anchor,
|
|
816
|
+
recovery_expires_at: deadline,
|
|
817
|
+
device_api_key: pending?.device_api_key ?? `lv_live_${(0, node_crypto_1.randomBytes)(32).toString("base64url")}`,
|
|
818
|
+
assignment,
|
|
819
|
+
...(expires <= Date.now() || (0, agent_state_js_1.canonicalTime)(assignment.lease_expires_at) <= Date.now()
|
|
820
|
+
? { needs_fresh_grant: true }
|
|
821
|
+
: {}),
|
|
822
|
+
};
|
|
823
|
+
await locked.save(next, options.signal);
|
|
824
|
+
state = next;
|
|
825
|
+
}
|
|
826
|
+
const pending = state.pending_credential_recovery;
|
|
827
|
+
if (!pending || pending.needs_fresh_grant)
|
|
828
|
+
throw new AgentLifecycleError("FRESH_RECOVERY_GRANT_REQUIRED");
|
|
829
|
+
live(pending.recovery_expires_at);
|
|
830
|
+
let envelope;
|
|
831
|
+
try {
|
|
832
|
+
envelope = await lifecycle.exchange(state, pending.assignment.nhp_udp_endpoint, 5, {
|
|
833
|
+
usrId: "",
|
|
834
|
+
devId: state.agent_id,
|
|
835
|
+
aspId: "agent",
|
|
836
|
+
usrData: {
|
|
837
|
+
query: "agent_credential_recovery",
|
|
838
|
+
version: 1,
|
|
839
|
+
recovery_grant: pending.recovery_grant,
|
|
840
|
+
device_api_key: pending.device_api_key,
|
|
841
|
+
},
|
|
842
|
+
}, "recovery-completion", pending.recovery_expires_at);
|
|
843
|
+
}
|
|
844
|
+
catch (error) {
|
|
845
|
+
if (error instanceof AgentLifecycleError && error.code === "52411") {
|
|
846
|
+
const next = copy(state);
|
|
847
|
+
next.pending_credential_recovery.needs_fresh_grant = true;
|
|
848
|
+
await locked.save(next, AbortSignal.timeout(10_000));
|
|
849
|
+
}
|
|
850
|
+
throw error;
|
|
851
|
+
}
|
|
852
|
+
const list = (0, agent_state_js_1.exactObject)(envelope.list, "query version device_api_key_id");
|
|
853
|
+
if (list.query !== "agent_credential_recovery" ||
|
|
854
|
+
list.version !== 1n ||
|
|
855
|
+
typeof list.device_api_key_id !== "string" ||
|
|
856
|
+
!KEY_ID.test(list.device_api_key_id))
|
|
857
|
+
throw new AgentLifecycleError("INVALID_RECOVERY_REPLY");
|
|
858
|
+
const next = copy(state);
|
|
859
|
+
next.device_api_key = pending.device_api_key;
|
|
860
|
+
next.device_api_key_id = list.device_api_key_id;
|
|
861
|
+
delete next.pending_credential_recovery;
|
|
862
|
+
delete next.pending_credential_recovery_issue;
|
|
863
|
+
next.credential_recovery_refresh_required = true;
|
|
864
|
+
await locked.save(next, AbortSignal.timeout(10_000));
|
|
865
|
+
return lifecycle.refresh(next, true);
|
|
866
|
+
}, options.signal);
|
|
867
|
+
return new AgentRuntime(state, store, supplied, transport);
|
|
868
|
+
}
|
|
869
|
+
async function connectWithTransport(store, options, transport) {
|
|
870
|
+
if (!store ||
|
|
871
|
+
typeof store.withLock !== "function" ||
|
|
872
|
+
(options.headless && options.otpProvider) ||
|
|
873
|
+
(options.enrollmentCredentialProvider &&
|
|
874
|
+
(options.enrollmentCredential !== undefined || options.otpProvider || options.offline)) ||
|
|
875
|
+
(options.offline && (options.enrollmentCredential !== undefined || options.otpProvider)))
|
|
876
|
+
throw new AgentLifecycleError("INVALID_CONFIGURATION");
|
|
877
|
+
if (options.agentID !== undefined)
|
|
878
|
+
(0, agent_state_js_1.validateAgentID)(options.agentID);
|
|
879
|
+
const supplied = { ...options, hub: options.hub ? structuredClone(options.hub) : undefined };
|
|
880
|
+
const driver = new Lifecycle(store, supplied, transport);
|
|
881
|
+
if (supplied.hub)
|
|
882
|
+
driver.hub();
|
|
883
|
+
const load = async (source) => {
|
|
884
|
+
try {
|
|
885
|
+
const state = copy(await source.load(options.signal));
|
|
886
|
+
checkIdentity(state, options.agentID);
|
|
887
|
+
return state;
|
|
888
|
+
}
|
|
889
|
+
catch (error) {
|
|
890
|
+
if (error instanceof agent_state_js_1.AgentStateError && error.code === "NOT_FOUND")
|
|
891
|
+
return undefined;
|
|
892
|
+
throw error;
|
|
893
|
+
}
|
|
894
|
+
};
|
|
895
|
+
let state = await load(store);
|
|
896
|
+
if (state?.registered_at) {
|
|
897
|
+
ready(state);
|
|
898
|
+
if ((0, agent_state_js_1.canonicalTime)(state.assignment.lease_expires_at) > Date.now())
|
|
899
|
+
return new AgentRuntime(state, store, supplied, transport);
|
|
900
|
+
}
|
|
901
|
+
if (options.offline)
|
|
902
|
+
throw new AgentLifecycleError(state ? "ASSIGNMENT_EXPIRED" : "NOT_REGISTERED");
|
|
903
|
+
state = await store.withLock(async (locked) => {
|
|
904
|
+
let current = await load(locked);
|
|
905
|
+
const lifecycle = new Lifecycle(locked, supplied, transport);
|
|
906
|
+
if (current?.registered_at) {
|
|
907
|
+
ready(current);
|
|
908
|
+
return (0, agent_state_js_1.canonicalTime)(current.assignment.lease_expires_at) > Date.now()
|
|
909
|
+
? current
|
|
910
|
+
: lifecycle.refresh(current);
|
|
911
|
+
}
|
|
912
|
+
if (!current?.pending_completion &&
|
|
913
|
+
!options.enrollmentCredential &&
|
|
914
|
+
!options.enrollmentCredentialProvider)
|
|
915
|
+
throw new AgentLifecycleError("ENROLLMENT_NOT_CONFIGURED");
|
|
916
|
+
if (!current) {
|
|
917
|
+
lifecycle.hub();
|
|
918
|
+
if (!options.headless && !options.otpProvider)
|
|
919
|
+
throw new AgentLifecycleError("OTP_NOT_AVAILABLE");
|
|
920
|
+
if (options.enrollmentCredential !== undefined)
|
|
921
|
+
credential(options.enrollmentCredential);
|
|
922
|
+
const { privateKey, publicKey } = (0, node_crypto_1.generateKeyPairSync)("x25519");
|
|
923
|
+
const privateDER = privateKey.export({ type: "pkcs8", format: "der" });
|
|
924
|
+
try {
|
|
925
|
+
current = {
|
|
926
|
+
schema_version: 8,
|
|
927
|
+
agent_id: options.agentID ?? `agent-${(0, node_crypto_1.randomBytes)(16).toString("hex")}`,
|
|
928
|
+
private_key_b64: privateDER.subarray(-32).toString("base64"),
|
|
929
|
+
public_key_b64: publicKey
|
|
930
|
+
.export({ type: "spki", format: "der" })
|
|
931
|
+
.subarray(-32)
|
|
932
|
+
.toString("base64"),
|
|
933
|
+
};
|
|
934
|
+
}
|
|
935
|
+
finally {
|
|
936
|
+
privateDER.fill(0);
|
|
937
|
+
}
|
|
938
|
+
await locked.save(current, options.signal);
|
|
939
|
+
}
|
|
940
|
+
const completed = await lifecycle.enroll(current);
|
|
941
|
+
return (0, agent_state_js_1.canonicalTime)(completed.assignment.lease_expires_at) <= Date.now()
|
|
942
|
+
? lifecycle.refresh(completed)
|
|
943
|
+
: completed;
|
|
944
|
+
}, options.signal);
|
|
945
|
+
return new AgentRuntime(state, store, supplied, transport);
|
|
946
|
+
}
|
|
947
|
+
exports.agentRuntimeTesting = {
|
|
948
|
+
connectWithTransport,
|
|
949
|
+
recoverWithTransport,
|
|
950
|
+
Lifecycle,
|
|
951
|
+
fingerprint,
|
|
952
|
+
};
|