mecord-connect 1.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 (82) hide show
  1. package/DISCLOSURE +47 -0
  2. package/LICENSE +64 -0
  3. package/README.md +44 -0
  4. package/bin/operator.mjs +10 -0
  5. package/package.json +58 -0
  6. package/runtime/app/apps/local-agent/src/approval-store.js +342 -0
  7. package/runtime/app/apps/local-agent/src/bootstrap-config.js +255 -0
  8. package/runtime/app/apps/local-agent/src/cli.js +237 -0
  9. package/runtime/app/apps/local-agent/src/device-reset.js +260 -0
  10. package/runtime/app/apps/local-agent/src/emergency-stop.js +141 -0
  11. package/runtime/app/apps/local-agent/src/main.js +311 -0
  12. package/runtime/app/apps/local-agent/src/privacy-data.js +129 -0
  13. package/runtime/app/apps/local-agent/src/relay-agent.js +333 -0
  14. package/runtime/app/apps/local-agent/src/relay-enrollment.js +156 -0
  15. package/runtime/app/apps/local-agent/src/relay-session-credentials.js +436 -0
  16. package/runtime/app/apps/local-agent/src/remote.js +130 -0
  17. package/runtime/app/apps/local-agent/src/runtime-factory.js +66 -0
  18. package/runtime/app/apps/local-agent/src/server.js +578 -0
  19. package/runtime/app/package.json +4 -0
  20. package/runtime/app/src/capabilities/browser-cdp-connection.js +199 -0
  21. package/runtime/app/src/capabilities/browser-cdp-frames.js +288 -0
  22. package/runtime/app/src/capabilities/browser-cdp-page.js +430 -0
  23. package/runtime/app/src/capabilities/browser-cdp.js +384 -0
  24. package/runtime/app/src/capabilities/browser-managed.js +345 -0
  25. package/runtime/app/src/capabilities/docker.js +448 -0
  26. package/runtime/app/src/capabilities/filesystem.js +264 -0
  27. package/runtime/app/src/capabilities/git-checkpoint.js +415 -0
  28. package/runtime/app/src/capabilities/git-write.js +321 -0
  29. package/runtime/app/src/capabilities/git.js +252 -0
  30. package/runtime/app/src/capabilities/index.js +16 -0
  31. package/runtime/app/src/capabilities/path-scope.js +86 -0
  32. package/runtime/app/src/capabilities/postgres.js +566 -0
  33. package/runtime/app/src/capabilities/process.js +258 -0
  34. package/runtime/app/src/capabilities/project-command.js +599 -0
  35. package/runtime/app/src/capabilities/project-transaction.js +215 -0
  36. package/runtime/app/src/capabilities/project.js +67 -0
  37. package/runtime/app/src/capabilities/system.js +40 -0
  38. package/runtime/app/src/capabilities/vscode.js +312 -0
  39. package/runtime/app/src/capabilities/windows-uia.js +331 -0
  40. package/runtime/app/src/core/account-device-registry.js +604 -0
  41. package/runtime/app/src/core/action-identity.js +44 -0
  42. package/runtime/app/src/core/audit.js +295 -0
  43. package/runtime/app/src/core/capability-policy.js +58 -0
  44. package/runtime/app/src/core/child-environment.js +29 -0
  45. package/runtime/app/src/core/device-enrollment.js +417 -0
  46. package/runtime/app/src/core/device-identity.js +436 -0
  47. package/runtime/app/src/core/device-registry.js +477 -0
  48. package/runtime/app/src/core/device-reset.js +178 -0
  49. package/runtime/app/src/core/device-routing.js +305 -0
  50. package/runtime/app/src/core/durable-state.js +306 -0
  51. package/runtime/app/src/core/errors.js +20 -0
  52. package/runtime/app/src/core/evidence.js +9 -0
  53. package/runtime/app/src/core/index.js +17 -0
  54. package/runtime/app/src/core/machine-state.js +160 -0
  55. package/runtime/app/src/core/network-authority.js +20 -0
  56. package/runtime/app/src/core/policy.js +67 -0
  57. package/runtime/app/src/core/product-identity.js +7 -0
  58. package/runtime/app/src/core/provenance.js +18 -0
  59. package/runtime/app/src/core/provider-learning.js +274 -0
  60. package/runtime/app/src/core/public-plugin-surface.js +25 -0
  61. package/runtime/app/src/core/public-restricted-data.js +105 -0
  62. package/runtime/app/src/core/rate-limit.js +118 -0
  63. package/runtime/app/src/core/relay-client.js +568 -0
  64. package/runtime/app/src/core/relay-delivery-store.js +512 -0
  65. package/runtime/app/src/core/relay-result-store.js +321 -0
  66. package/runtime/app/src/core/release-update.js +253 -0
  67. package/runtime/app/src/core/router.js +96 -0
  68. package/runtime/app/src/core/runtime.js +136 -0
  69. package/runtime/app/src/core/scoped-path-syntax.js +16 -0
  70. package/runtime/app/src/core/session-token.js +498 -0
  71. package/runtime/app/src/core/task-orchestrator.js +1148 -0
  72. package/runtime/app/src/core/task-store.js +517 -0
  73. package/runtime/app/src/core/task.js +142 -0
  74. package/runtime/app/src/core/trusted-executable.js +124 -0
  75. package/runtime/app/src/core/types.js +83 -0
  76. package/runtime/app/src/core/windows-path-lease.js +138 -0
  77. package/runtime/native/operator-windows-dpapi.exe +0 -0
  78. package/runtime/native/operator-windows-path-lease.exe +0 -0
  79. package/runtime/native/operator-windows-uia.exe +0 -0
  80. package/runtime/runtime-manifest.json +380 -0
  81. package/src/cli.mjs +345 -0
  82. package/src/publish-check.mjs +22 -0
@@ -0,0 +1,305 @@
1
+ import path from 'node:path';
2
+ import { DeviceRegistryStore, } from './device-registry.js';
3
+ import { OperatorError } from './errors.js';
4
+ import { readDurableStateText, writeDurableStateText } from './durable-state.js';
5
+
6
+ const MAX_BINDINGS = 4096;
7
+ const MAX_ONLINE_DEVICES = 1000;
8
+ const MAX_CAPABILITIES = 256;
9
+ const DEFAULT_LIVENESS_MS = 45_000;
10
+ const MAX_LIVENESS_MS = 5 * 60_000;
11
+
12
+
13
+
14
+
15
+
16
+
17
+
18
+
19
+
20
+
21
+
22
+
23
+
24
+
25
+
26
+
27
+
28
+
29
+
30
+
31
+
32
+
33
+
34
+
35
+
36
+
37
+
38
+
39
+
40
+
41
+
42
+
43
+
44
+
45
+
46
+
47
+
48
+ export class DeviceRoutingStore {
49
+ #file ;
50
+ #registry ;
51
+ #clock ;
52
+ #queue = Promise.resolve();
53
+
54
+ constructor(stateDir , registry , options = {}) {
55
+ this.#file = path.join(path.resolve(stateDir), 'device-routing.json');
56
+ this.#registry = registry;
57
+ this.#clock = options.clock ?? (() => new Date());
58
+ }
59
+
60
+ async listBindings() {
61
+ const state = await this.#read();
62
+ return state.bindings.map((binding) => ({ ...binding }));
63
+ }
64
+
65
+ async bindProject(projectKeyInput , deviceIdInput ) {
66
+ const projectKey = validProjectKey(projectKeyInput);
67
+ const deviceId = validUuid(deviceIdInput, 'deviceId');
68
+ await this.#requireActiveDevice(deviceId);
69
+ return await this.#mutate((state) => {
70
+ const existing = state.bindings.find((binding) => binding.projectKey === projectKey);
71
+ const updatedAt = this.#clock().toISOString();
72
+ if (existing) {
73
+ existing.deviceId = deviceId;
74
+ existing.updatedAt = updatedAt;
75
+ return { ...existing };
76
+ }
77
+ if (state.bindings.length >= MAX_BINDINGS) throw new OperatorError('ROUTE_BINDING_LIMIT', `At most ${MAX_BINDINGS} project bindings may be stored.`);
78
+ const binding = { projectKey, deviceId, updatedAt };
79
+ state.bindings.push(binding);
80
+ state.bindings.sort((a, b) => a.projectKey.localeCompare(b.projectKey));
81
+ return { ...binding };
82
+ });
83
+ }
84
+
85
+ async unbindProject(projectKeyInput ) {
86
+ const projectKey = validProjectKey(projectKeyInput);
87
+ return await this.#mutate((state) => {
88
+ const before = state.bindings.length;
89
+ state.bindings = state.bindings.filter((binding) => binding.projectKey !== projectKey);
90
+ return state.bindings.length !== before;
91
+ });
92
+ }
93
+
94
+ async unbindDevice(deviceIdInput ) {
95
+ const deviceId = validUuid(deviceIdInput, 'deviceId');
96
+ return await this.#mutate((state) => {
97
+ const before = state.bindings.length;
98
+ state.bindings = state.bindings.filter((binding) => binding.deviceId !== deviceId);
99
+ return before - state.bindings.length;
100
+ });
101
+ }
102
+
103
+ async resolve(requestInput , onlineInput ) {
104
+ const request = validateRequest(requestInput);
105
+ const online = validateOnlineDevices(onlineInput, this.#clock(), request.livenessMs);
106
+ const registered = await this.#registry.listDevices();
107
+ const active = new Map(registered.filter((device) => device.status === 'active').map((device) => [device.deviceId, device]));
108
+ const bindings = (await this.#read()).bindings;
109
+ const binding = request.projectKey ? bindings.find((candidate) => candidate.projectKey === request.projectKey) : undefined;
110
+
111
+ if (request.explicitDeviceId && binding && request.explicitDeviceId !== binding.deviceId) {
112
+ throw new OperatorError('ROUTE_PROJECT_DEVICE_CONFLICT', 'Explicit device conflicts with the persisted project-to-device binding.', {
113
+ details: { projectKey: request.projectKey, boundDeviceId: binding.deviceId, explicitDeviceId: request.explicitDeviceId }
114
+ });
115
+ }
116
+
117
+ if (request.explicitDeviceId) {
118
+ const candidate = requireRoutable(request.explicitDeviceId, online, active, request.requiredCapabilities, 'explicit');
119
+ return decision(candidate, request, 'explicit');
120
+ }
121
+
122
+ if (binding) {
123
+ const candidate = requireRoutable(binding.deviceId, online, active, request.requiredCapabilities, 'project binding');
124
+ return decision(candidate, request, 'project-binding');
125
+ }
126
+
127
+ const candidates = online.filter((candidate) => active.has(candidate.deviceId) && hasCapabilities(candidate, request.requiredCapabilities));
128
+ if (candidates.length === 0) {
129
+ if (request.projectKey) {
130
+ throw new OperatorError('ROUTE_PROJECT_UNBOUND', 'Project has no device binding and no unique eligible online device can be inferred safely.', {
131
+ details: { projectKey: request.projectKey }
132
+ });
133
+ }
134
+ throw new OperatorError('ROUTE_NO_DEVICE', 'No active online device satisfies the requested capabilities.');
135
+ }
136
+ if (candidates.length > 1) {
137
+ throw new OperatorError('ROUTE_AMBIGUOUS', 'Multiple active online devices satisfy the route; select a device explicitly or bind the project.', {
138
+ details: { candidateDeviceIds: candidates.map((candidate) => candidate.deviceId).sort() }
139
+ });
140
+ }
141
+ return decision(candidates[0], request, 'unique-candidate');
142
+ }
143
+
144
+ async #requireActiveDevice(deviceId ) {
145
+ const device = (await this.#registry.listDevices()).find((candidate) => candidate.deviceId === deviceId);
146
+ if (!device) throw new OperatorError('DEVICE_NOT_FOUND', 'Cannot bind an unpaired device.');
147
+ if (device.status !== 'active') throw new OperatorError('DEVICE_REVOKED', 'Cannot bind a revoked device.');
148
+ return device;
149
+ }
150
+
151
+ async #read() {
152
+ try {
153
+ const text = await readDurableStateText(this.#file, {
154
+ maxBytes: 512 * 1024,
155
+ errorCode: 'ROUTE_STATE_CORRUPT',
156
+ invalidMessage: 'Device routing state is invalid.'
157
+ });
158
+ return validateState(JSON.parse(text));
159
+ } catch (error) {
160
+ if ((error ).code === 'ENOENT') return { version: 1, bindings: [] };
161
+ if (error instanceof OperatorError) throw error;
162
+ throw new OperatorError('ROUTE_STATE_CORRUPT', 'Device routing state could not be read.');
163
+ }
164
+ }
165
+
166
+ async #write(stateInput ) {
167
+ const state = validateState(stateInput);
168
+ await writeDurableStateText(this.#file, JSON.stringify(state, null, 2), {
169
+ maxBytes: 512 * 1024,
170
+ errorCode: 'ROUTE_STATE_CORRUPT',
171
+ invalidMessage: 'Device routing state is invalid.'
172
+ });
173
+ }
174
+
175
+ async #mutate (mutator ) {
176
+ let release ;
177
+ const previous = this.#queue;
178
+ this.#queue = new Promise ((resolve) => { release = resolve; });
179
+ await previous;
180
+ try {
181
+ const state = await this.#read();
182
+ const result = await mutator(state);
183
+ await this.#write(state);
184
+ return result;
185
+ } finally {
186
+ release();
187
+ }
188
+ }
189
+ }
190
+
191
+ function validateRequest(input ) {
192
+ if (!input || typeof input !== 'object') throw new OperatorError('ROUTE_REQUEST_INVALID', 'Device route request is invalid.');
193
+ const requiredCapabilities = validCapabilities(input.requiredCapabilities ?? []);
194
+ const explicitDeviceId = input.explicitDeviceId === undefined ? undefined : validUuid(input.explicitDeviceId, 'explicitDeviceId');
195
+ const projectKey = input.projectKey === undefined ? undefined : validProjectKey(input.projectKey);
196
+ const livenessMs = Number(input.livenessMs ?? DEFAULT_LIVENESS_MS);
197
+ if (!Number.isInteger(livenessMs) || livenessMs < 5_000 || livenessMs > MAX_LIVENESS_MS) {
198
+ throw new OperatorError('ROUTE_LIVENESS_INVALID', `livenessMs must be between 5000 and ${MAX_LIVENESS_MS}.`);
199
+ }
200
+ return { requiredCapabilities, explicitDeviceId, projectKey, livenessMs };
201
+ }
202
+
203
+ function validateOnlineDevices(input , now , livenessMs ) {
204
+ if (!Array.isArray(input) || input.length > MAX_ONLINE_DEVICES) throw new OperatorError('ROUTE_ONLINE_STATE_INVALID', `Online device list must contain at most ${MAX_ONLINE_DEVICES} entries.`);
205
+ const seenDevices = new Set ();
206
+ const seenSessions = new Set ();
207
+ const nowMs = now.getTime();
208
+ const result = [];
209
+ for (const raw of input) {
210
+ if (!raw || typeof raw !== 'object') throw new OperatorError('ROUTE_ONLINE_STATE_INVALID', 'Online device descriptor is invalid.');
211
+ const deviceId = validUuid(raw.deviceId, 'online deviceId');
212
+ const sessionId = validSessionId(raw.sessionId);
213
+ if (seenDevices.has(deviceId)) throw new OperatorError('ROUTE_ONLINE_STATE_INVALID', 'Online device list contains duplicate device IDs.');
214
+ if (seenSessions.has(sessionId)) throw new OperatorError('ROUTE_ONLINE_STATE_INVALID', 'Online device list contains duplicate session IDs.');
215
+ seenDevices.add(deviceId);
216
+ seenSessions.add(sessionId);
217
+ const capabilities = validCapabilities(raw.capabilities);
218
+ const connectedAt = validIso(raw.connectedAt, 'connectedAt');
219
+ const lastSeenAt = validIso(raw.lastSeenAt, 'lastSeenAt');
220
+ const connectedMs = Date.parse(connectedAt);
221
+ const seenMs = Date.parse(lastSeenAt);
222
+ if (seenMs < connectedMs || seenMs > nowMs + 30_000) throw new OperatorError('ROUTE_ONLINE_STATE_INVALID', 'Online device timestamps are inconsistent.');
223
+ if (nowMs - seenMs > livenessMs) continue;
224
+ result.push({ deviceId, sessionId, capabilities, connectedAt, lastSeenAt });
225
+ }
226
+ return result;
227
+ }
228
+
229
+ function requireRoutable(
230
+ deviceId ,
231
+ online ,
232
+ active ,
233
+ capabilities ,
234
+ source
235
+ ) {
236
+ if (!active.has(deviceId)) throw new OperatorError('ROUTE_DEVICE_INACTIVE', `The ${source} device is not an active paired device.`, { details: { deviceId } });
237
+ const candidate = online.find((device) => device.deviceId === deviceId);
238
+ if (!candidate) throw new OperatorError('ROUTE_DEVICE_OFFLINE', `The ${source} device is offline or stale.`, { retryable: true, details: { deviceId } });
239
+ const missing = capabilities.filter((capability) => !candidate.capabilities.includes(capability));
240
+ if (missing.length) throw new OperatorError('ROUTE_CAPABILITY_MISMATCH', `The ${source} device does not advertise every required capability.`, { details: { deviceId, missing } });
241
+ return candidate;
242
+ }
243
+
244
+ function decision(candidate , request , reason ) {
245
+ return {
246
+ deviceId: candidate.deviceId,
247
+ sessionId: candidate.sessionId,
248
+ projectKey: request.projectKey,
249
+ matchedCapabilities: [...request.requiredCapabilities],
250
+ reason
251
+ };
252
+ }
253
+
254
+ function hasCapabilities(device , required ) {
255
+ return required.every((capability) => device.capabilities.includes(capability));
256
+ }
257
+
258
+ function validateState(input ) {
259
+ if (!input || typeof input !== 'object' || input.version !== 1 || !Array.isArray(input.bindings) || input.bindings.length > MAX_BINDINGS) {
260
+ throw new OperatorError('ROUTE_STATE_CORRUPT', 'Device routing state structure is invalid.');
261
+ }
262
+ const projects = new Set ();
263
+ const bindings = input.bindings.map((raw) => {
264
+ const projectKey = validProjectKey(raw.projectKey);
265
+ if (projects.has(projectKey)) throw new OperatorError('ROUTE_STATE_CORRUPT', 'Device routing state contains duplicate project keys.');
266
+ projects.add(projectKey);
267
+ return { projectKey, deviceId: validUuid(raw.deviceId, 'deviceId'), updatedAt: validIso(raw.updatedAt, 'updatedAt') };
268
+ });
269
+ return { version: 1, bindings };
270
+ }
271
+
272
+ function validCapabilities(input ) {
273
+ if (!Array.isArray(input) || input.length > MAX_CAPABILITIES) throw new OperatorError('ROUTE_CAPABILITY_INVALID', `Capabilities must contain at most ${MAX_CAPABILITIES} entries.`);
274
+ const capabilities = input.map(String);
275
+ for (const capability of capabilities) {
276
+ if (!/^[A-Za-z0-9][A-Za-z0-9._:/*-]{0,127}$/.test(capability)) throw new OperatorError('ROUTE_CAPABILITY_INVALID', `Invalid capability ${capability}.`);
277
+ }
278
+ return [...new Set(capabilities)].sort();
279
+ }
280
+
281
+ function validProjectKey(value ) {
282
+ const key = String(value ?? '');
283
+ if (!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(key)) throw new OperatorError('ROUTE_PROJECT_KEY_INVALID', 'projectKey must be an opaque logical identifier, not a filesystem path.');
284
+ if (key.includes('/') || key.includes('\\')) throw new OperatorError('ROUTE_PROJECT_KEY_INVALID', 'projectKey must not contain path separators.');
285
+ return key;
286
+ }
287
+
288
+ function validSessionId(value ) {
289
+ const id = String(value ?? '');
290
+ if (!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(id)) throw new OperatorError('ROUTE_ONLINE_STATE_INVALID', 'Relay session ID is invalid.');
291
+ return id;
292
+ }
293
+
294
+ function validUuid(value , label ) {
295
+ const text = String(value ?? '');
296
+ if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(text)) throw new OperatorError('ROUTE_DEVICE_ID_INVALID', `${label} must be a UUID.`);
297
+ return text.toLowerCase();
298
+ }
299
+
300
+ function validIso(value , label ) {
301
+ const text = String(value ?? '');
302
+ const time = Date.parse(text);
303
+ if (!Number.isFinite(time) || new Date(time).toISOString() !== text) throw new OperatorError('ROUTE_STATE_INVALID', `${label} must be an ISO timestamp.`);
304
+ return text;
305
+ }
@@ -0,0 +1,306 @@
1
+ import crypto from 'node:crypto';
2
+ import { constants as fsConstants, } from 'node:fs';
3
+ import fs from 'node:fs/promises';
4
+ import path from 'node:path';
5
+ import { OperatorError } from './errors.js';
6
+
7
+
8
+
9
+
10
+
11
+
12
+
13
+ export async function readDurableStateText(file , options ) {
14
+ return (await readDurableStateBytes(file, options)).toString('utf8');
15
+ }
16
+
17
+ class DurableStateReadRace extends Error {}
18
+
19
+ export async function readDurableStateBytes(file , options ) {
20
+ validateOptions(options);
21
+ let lastRace ;
22
+ for (let attempt = 0; attempt < 4; attempt += 1) {
23
+ try {
24
+ return await readDurableStateBytesOnce(file, options);
25
+ } catch (error) {
26
+ if (!(error instanceof DurableStateReadRace)) throw error;
27
+ lastRace = error;
28
+ }
29
+ }
30
+ throw invalid(options, lastRace?.message ?? 'State file changed while it was being read.');
31
+ }
32
+
33
+ async function readDurableStateBytesOnce(file , options ) {
34
+ const initial = await fs.lstat(file);
35
+ assertStableRegular(initial, options);
36
+
37
+ const noFollow = process.platform === 'win32'
38
+ ? 0
39
+ : Number((fsConstants ).O_NOFOLLOW ?? 0);
40
+ let handle;
41
+ try {
42
+ handle = await fs.open(file, fsConstants.O_RDONLY | noFollow);
43
+ } catch (error) {
44
+ const code = (error ).code;
45
+ if (code === 'ELOOP') throw invalid(options, 'Symbolic links are not permitted.');
46
+ if (code === 'ENOENT') throw new DurableStateReadRace('State file changed while it was being opened.');
47
+ throw error;
48
+ }
49
+
50
+ try {
51
+ const opened = await handle.stat();
52
+ assertOpenedRegular(opened, options, 'State file changed while it was being opened.');
53
+ if (!sameFile(initial, opened)) throw new DurableStateReadRace('State file changed while it was being opened.');
54
+
55
+ const bytes = await handle.readFile();
56
+ if (bytes.byteLength > options.maxBytes) throw invalid(options, 'State file exceeds the bounded size.');
57
+ const afterRead = await handle.stat();
58
+ assertOpenedRegular(afterRead, options, 'State file changed while it was being read.');
59
+ if (!sameFile(opened, afterRead) || opened.size !== afterRead.size || opened.mtimeMs !== afterRead.mtimeMs || opened.ctimeMs !== afterRead.ctimeMs) {
60
+ throw new DurableStateReadRace('State file changed while it was being read.');
61
+ }
62
+
63
+ let current ;
64
+ try {
65
+ current = await fs.lstat(file);
66
+ } catch (error) {
67
+ if ((error ).code === 'ENOENT') throw new DurableStateReadRace('State file path changed while it was being read.');
68
+ throw error;
69
+ }
70
+ assertStableRegular(current, options);
71
+ if (!sameFile(afterRead, current)) throw new DurableStateReadRace('State file path changed while it was being read.');
72
+ return bytes;
73
+ } finally {
74
+ await handle.close();
75
+ }
76
+ }
77
+
78
+ export async function appendDurableStateText(file , content , options ) {
79
+ validateOptions(options);
80
+ const byteLength = Buffer.byteLength(content, 'utf8');
81
+ if (byteLength < 1 || byteLength > options.maxBytes) throw invalid(options, 'Appended state content is empty or exceeds the bounded size.');
82
+
83
+ const directory = path.dirname(file);
84
+ await fs.mkdir(directory, { recursive: true, mode: 0o700 });
85
+
86
+ let initial = null;
87
+ try {
88
+ initial = await fs.lstat(file);
89
+ assertStableRegular(initial, options);
90
+ } catch (error) {
91
+ if ((error ).code !== 'ENOENT') throw error;
92
+ }
93
+
94
+ const noFollow = process.platform === 'win32'
95
+ ? 0
96
+ : Number((fsConstants ).O_NOFOLLOW ?? 0);
97
+ let handle;
98
+ let created = false;
99
+ try {
100
+ if (initial) {
101
+ handle = await fs.open(file, fsConstants.O_WRONLY | fsConstants.O_APPEND | noFollow);
102
+ } else {
103
+ handle = await fs.open(file, 'wx', 0o600);
104
+ created = true;
105
+ }
106
+ } catch (error) {
107
+ if ((error ).code === 'ELOOP') throw invalid(options, 'Symbolic links are not permitted.');
108
+ throw error;
109
+ }
110
+
111
+ try {
112
+ const opened = await handle.stat();
113
+ assertStableRegular(opened, options);
114
+ if (initial && !sameFile(initial, opened)) throw invalid(options, 'State file changed while it was being opened for append.');
115
+
116
+ const current = await fs.lstat(file);
117
+ assertStableRegular(current, options);
118
+ if (!sameFile(opened, current)) throw invalid(options, 'State file path changed before append.');
119
+ if (opened.size + byteLength > options.maxBytes) throw invalid(options, 'State file would exceed the bounded size after append.');
120
+
121
+ await handle.writeFile(content, { encoding: 'utf8' });
122
+ await handle.sync();
123
+
124
+ const appended = await handle.stat();
125
+ assertStableRegular(appended, options);
126
+ if (!sameFile(opened, appended) || appended.size !== opened.size + byteLength) {
127
+ throw invalid(options, 'State append did not produce the expected file size.');
128
+ }
129
+ const committed = await fs.lstat(file);
130
+ assertStableRegular(committed, options);
131
+ if (!sameFile(appended, committed)) throw invalid(options, 'State file path changed during append.');
132
+ } finally {
133
+ await handle.close();
134
+ }
135
+
136
+ if (created && process.platform !== 'win32') await syncDirectory(directory);
137
+ }
138
+
139
+ export async function writeDurableStateText(file , content , options ) {
140
+ await writeDurableStateBytes(file, Buffer.from(content, 'utf8'), options);
141
+ }
142
+
143
+ export async function writeDurableStateBytes(file , content , options ) {
144
+ validateOptions(options);
145
+ const bytes = Buffer.from(content);
146
+ const byteLength = bytes.byteLength;
147
+ if (byteLength > options.maxBytes) throw invalid(options, 'Serialized state exceeds the bounded size.');
148
+
149
+ const directory = path.dirname(file);
150
+ await fs.mkdir(directory, { recursive: true, mode: 0o700 });
151
+ await assertReplaceTarget(file, options);
152
+
153
+ const temp = `${file}.${process.pid}.${crypto.randomUUID()}.tmp`;
154
+ let renamed = false;
155
+ let handle;
156
+ try {
157
+ handle = await fs.open(temp, 'wx', 0o600);
158
+ try {
159
+ await handle.writeFile(bytes);
160
+ await handle.sync();
161
+ } finally {
162
+ await handle.close();
163
+ handle = undefined;
164
+ }
165
+
166
+ const staged = await fs.lstat(temp);
167
+ assertStableRegular(staged, options);
168
+ if (staged.size !== byteLength) throw invalid(options, 'Staged state size did not match the serialized state.');
169
+
170
+ await publishDurableReplacement(temp, file, options);
171
+ renamed = true;
172
+
173
+ const committed = await fs.lstat(file);
174
+ assertStableRegular(committed, options);
175
+ if (committed.size !== byteLength) throw invalid(options, 'Committed state size did not match the serialized state.');
176
+
177
+ if (process.platform !== 'win32') await syncDirectory(directory);
178
+ } finally {
179
+ if (handle) await handle.close().catch(() => undefined);
180
+ if (!renamed) await fs.rm(temp, { force: true }).catch(() => undefined);
181
+ }
182
+ }
183
+
184
+ export async function createDurableStateBytes(file , content , options ) {
185
+ validateOptions(options);
186
+ const bytes = Buffer.from(content);
187
+ const byteLength = bytes.byteLength;
188
+ if (byteLength > options.maxBytes) throw invalid(options, 'Serialized state exceeds the bounded size.');
189
+
190
+ const directory = path.dirname(file);
191
+ await fs.mkdir(directory, { recursive: true, mode: 0o700 });
192
+ try {
193
+ const existing = await fs.lstat(file);
194
+ assertStableRegular(existing, options);
195
+ const exists = new Error('State file already exists.') ;
196
+ exists.code = 'EEXIST';
197
+ throw exists;
198
+ } catch (error) {
199
+ if ((error ).code !== 'ENOENT') throw error;
200
+ }
201
+
202
+ const temp = `${file}.${process.pid}.${crypto.randomUUID()}.tmp`;
203
+ let handle;
204
+ let published = false;
205
+ try {
206
+ handle = await fs.open(temp, 'wx', 0o600);
207
+ try {
208
+ await handle.writeFile(bytes);
209
+ await handle.sync();
210
+ } finally {
211
+ await handle.close();
212
+ handle = undefined;
213
+ }
214
+
215
+ const staged = await fs.lstat(temp);
216
+ assertStableRegular(staged, options);
217
+ if (staged.size !== byteLength) throw invalid(options, 'Staged state size did not match the serialized state.');
218
+
219
+ await fs.link(temp, file);
220
+ published = true;
221
+ await fs.rm(temp);
222
+
223
+ const committed = await fs.lstat(file);
224
+ assertStableRegular(committed, options);
225
+ if (committed.size !== byteLength) throw invalid(options, 'Committed state size did not match the serialized state.');
226
+
227
+ if (process.platform !== 'win32') await syncDirectory(directory);
228
+ } finally {
229
+ if (handle) await handle.close().catch(() => undefined);
230
+ await fs.rm(temp, { force: true }).catch(() => undefined);
231
+ if (published) {
232
+ try {
233
+ const committed = await fs.lstat(file);
234
+ assertStableRegular(committed, options);
235
+ } catch (error) {
236
+ if ((error ).code !== 'ENOENT') throw error;
237
+ }
238
+ }
239
+ }
240
+ }
241
+
242
+ async function assertReplaceTarget(file , options ) {
243
+ try {
244
+ const stat = await fs.lstat(file);
245
+ assertStableRegular(stat, options);
246
+ } catch (error) {
247
+ if ((error ).code === 'ENOENT') return;
248
+ throw error;
249
+ }
250
+ }
251
+
252
+ async function publishDurableReplacement(temp , file , options ) {
253
+ const retryDelaysMs = [5, 10, 25, 50, 100];
254
+ for (let attempt = 0; ; attempt += 1) {
255
+ const staged = await fs.lstat(temp);
256
+ assertStableRegular(staged, options);
257
+ await assertReplaceTarget(file, options);
258
+ try {
259
+ await fs.rename(temp, file);
260
+ return;
261
+ } catch (error) {
262
+ const code = (error ).code;
263
+ const transientWindowsRename =
264
+ process.platform === 'win32' && (code === 'EPERM' || code === 'EACCES' || code === 'EBUSY');
265
+ if (!transientWindowsRename || attempt >= retryDelaysMs.length) throw error;
266
+ await new Promise ((resolve) => setTimeout(resolve, retryDelaysMs[attempt]));
267
+ }
268
+ }
269
+ }
270
+
271
+ function assertOpenedRegular(stat , options , raceMessage ) {
272
+ if (!stat.isFile()) throw invalid(options, 'Opened state path must be a regular file.');
273
+ if (stat.nlink > 1) throw invalid(options, 'Hard-linked state files are not permitted.');
274
+ if (stat.nlink < 1) throw new DurableStateReadRace(raceMessage);
275
+ if (stat.size > options.maxBytes) throw invalid(options, 'State file exceeds the bounded size.');
276
+ }
277
+
278
+ function assertStableRegular(stat , options ) {
279
+ if (stat.isSymbolicLink() || !stat.isFile()) throw invalid(options, 'State path must be a regular file, not a link or special file.');
280
+ if (stat.nlink !== 1) throw invalid(options, 'Hard-linked state files are not permitted.');
281
+ if (stat.size > options.maxBytes) throw invalid(options, 'State file exceeds the bounded size.');
282
+ }
283
+
284
+ function sameFile(left , right ) {
285
+ // Node 22 on Windows can report dev=0 for path stat/lstat while the same
286
+ // file opened by handle reports the NT volume serial as dev. The file index
287
+ // (ino) remains stable across both views, so use that as the fail-closed
288
+ // identity on Windows and keep the POSIX dev+ino pair elsewhere.
289
+ if (process.platform === 'win32') return left.ino !== 0 && right.ino !== 0 && left.ino === right.ino;
290
+ return left.dev === right.dev && left.ino === right.ino;
291
+ }
292
+
293
+ async function syncDirectory(directory ) {
294
+ const handle = await fs.open(directory, 'r');
295
+ try { await handle.sync(); } finally { await handle.close(); }
296
+ }
297
+
298
+ function validateOptions(options ) {
299
+ if (!Number.isSafeInteger(options.maxBytes) || options.maxBytes < 1 || !options.errorCode || !options.invalidMessage) {
300
+ throw new OperatorError('DURABLE_STATE_CONFIGURATION_INVALID', 'Durable-state file options are invalid.');
301
+ }
302
+ }
303
+
304
+ function invalid(options , detail ) {
305
+ return new OperatorError(options.errorCode, `${options.invalidMessage} ${detail}`);
306
+ }
@@ -0,0 +1,20 @@
1
+ export class OperatorError extends Error {
2
+ code ;
3
+ retryable ;
4
+ details ;
5
+
6
+ constructor(code , message , options = {}) {
7
+ super(message);
8
+ this.name = 'OperatorError';
9
+ this.code = code;
10
+ this.retryable = options.retryable ?? false;
11
+ this.details = options.details;
12
+ }
13
+ }
14
+
15
+ export class PolicyError extends OperatorError {
16
+ constructor(code , message , details ) {
17
+ super(code, message, { retryable: false, details });
18
+ this.name = 'PolicyError';
19
+ }
20
+ }
@@ -0,0 +1,9 @@
1
+
2
+
3
+ export function evidence(kind , status , message , data ) {
4
+ return { kind, status, message, ...(data === undefined ? {} : { data }), timestamp: new Date().toISOString() };
5
+ }
6
+
7
+ export function verdict(items ) {
8
+ return items.some((item) => item.status === 'fail') ? 'NOT_VERIFIED' : 'VERIFIED';
9
+ }
@@ -0,0 +1,17 @@
1
+ export * from './types.js';
2
+ export * from './errors.js';
3
+ export * from './evidence.js';
4
+ export * from './provenance.js';
5
+ export * from './policy.js';
6
+ export * from './router.js';
7
+ export * from './runtime.js';
8
+ export * from './task.js';
9
+ export * from './task-store.js';
10
+ export * from './task-orchestrator.js';
11
+ export * from './audit.js';
12
+ export * from './device-identity.js';
13
+ export * from './device-registry.js';
14
+ export * from './session-token.js';
15
+ export * from './relay-client.js';
16
+ export * from './device-routing.js';
17
+ export * from './account-device-registry.js';