mecord-connect 1.0.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 (120) hide show
  1. package/DISCLOSURE +47 -47
  2. package/LICENSE +64 -64
  3. package/README.md +46 -44
  4. package/bin/operator.mjs +10 -10
  5. package/package.json +58 -58
  6. package/runtime/app/apps/local-agent/src/approval-store.js +342 -342
  7. package/runtime/app/apps/local-agent/src/bootstrap-config.js +255 -255
  8. package/runtime/app/apps/local-agent/src/cli.js +237 -237
  9. package/runtime/app/apps/local-agent/src/control-center.js +61 -0
  10. package/runtime/app/apps/local-agent/src/device-reset.js +260 -260
  11. package/runtime/app/apps/local-agent/src/emergency-stop.js +141 -141
  12. package/runtime/app/apps/local-agent/src/main.js +448 -311
  13. package/runtime/app/apps/local-agent/src/migration-proofs.js +194 -0
  14. package/runtime/app/apps/local-agent/src/privacy-data.js +129 -129
  15. package/runtime/app/apps/local-agent/src/relay-agent.js +542 -333
  16. package/runtime/app/apps/local-agent/src/relay-enrollment.js +156 -156
  17. package/runtime/app/apps/local-agent/src/relay-session-credentials.js +436 -436
  18. package/runtime/app/apps/local-agent/src/remote.js +130 -130
  19. package/runtime/app/apps/local-agent/src/runtime-factory.js +78 -66
  20. package/runtime/app/apps/local-agent/src/server.js +2069 -578
  21. package/runtime/app/apps/local-agent/src/session-approval.js +119 -0
  22. package/runtime/app/src/capabilities/browser-cdp-connection.js +199 -199
  23. package/runtime/app/src/capabilities/browser-cdp-frames.js +288 -288
  24. package/runtime/app/src/capabilities/browser-cdp-page.js +430 -430
  25. package/runtime/app/src/capabilities/browser-cdp.js +384 -384
  26. package/runtime/app/src/capabilities/browser-managed.js +345 -345
  27. package/runtime/app/src/capabilities/docker.js +448 -448
  28. package/runtime/app/src/capabilities/filesystem.js +546 -264
  29. package/runtime/app/src/capabilities/git-checkpoint.js +415 -415
  30. package/runtime/app/src/capabilities/git-write.js +321 -321
  31. package/runtime/app/src/capabilities/git.js +252 -252
  32. package/runtime/app/src/capabilities/index.js +18 -16
  33. package/runtime/app/src/capabilities/path-scope.js +86 -86
  34. package/runtime/app/src/capabilities/perception.js +138 -0
  35. package/runtime/app/src/capabilities/postgres.js +566 -566
  36. package/runtime/app/src/capabilities/process.js +625 -258
  37. package/runtime/app/src/capabilities/project-command.js +598 -598
  38. package/runtime/app/src/capabilities/project-transaction.js +215 -215
  39. package/runtime/app/src/capabilities/project.js +67 -67
  40. package/runtime/app/src/capabilities/sandboxed-compute.js +226 -0
  41. package/runtime/app/src/capabilities/system.js +40 -40
  42. package/runtime/app/src/capabilities/vscode.js +312 -312
  43. package/runtime/app/src/capabilities/windows-uia.js +616 -331
  44. package/runtime/app/src/core/account-device-registry.js +604 -604
  45. package/runtime/app/src/core/action-identity.js +44 -44
  46. package/runtime/app/src/core/audit.js +383 -295
  47. package/runtime/app/src/core/authority-kernel.js +209 -0
  48. package/runtime/app/src/core/capability-policy.js +69 -58
  49. package/runtime/app/src/core/capability-sdk.js +188 -0
  50. package/runtime/app/src/core/child-environment.js +28 -28
  51. package/runtime/app/src/core/conversation-ledger.js +146 -0
  52. package/runtime/app/src/core/desired-state-reconciler.js +90 -0
  53. package/runtime/app/src/core/desired-state.js +470 -0
  54. package/runtime/app/src/core/developer-relay-surface.js +44 -0
  55. package/runtime/app/src/core/device-enrollment.js +417 -417
  56. package/runtime/app/src/core/device-identity.js +436 -436
  57. package/runtime/app/src/core/device-pool.js +318 -0
  58. package/runtime/app/src/core/device-registry.js +477 -477
  59. package/runtime/app/src/core/device-reset.js +178 -178
  60. package/runtime/app/src/core/device-routing.js +330 -305
  61. package/runtime/app/src/core/digital-operations.js +707 -0
  62. package/runtime/app/src/core/durable-state.js +321 -306
  63. package/runtime/app/src/core/enterprise-policy.js +290 -0
  64. package/runtime/app/src/core/errors.js +20 -20
  65. package/runtime/app/src/core/evaluation.js +336 -0
  66. package/runtime/app/src/core/event-runtime.js +267 -0
  67. package/runtime/app/src/core/event-ticker.js +66 -0
  68. package/runtime/app/src/core/evidence.js +9 -9
  69. package/runtime/app/src/core/execution-optimizer.js +255 -0
  70. package/runtime/app/src/core/index.js +25 -17
  71. package/runtime/app/src/core/intent-kernel.js +152 -0
  72. package/runtime/app/src/core/machine-state.js +161 -160
  73. package/runtime/app/src/core/network-authority.js +20 -20
  74. package/runtime/app/src/core/organization-coordinator.js +410 -0
  75. package/runtime/app/src/core/outcome-planner.js +179 -0
  76. package/runtime/app/src/core/perception-graph.js +365 -0
  77. package/runtime/app/src/core/perception-publication.js +139 -0
  78. package/runtime/app/src/core/policy.js +76 -67
  79. package/runtime/app/src/core/procedure-memory.js +399 -0
  80. package/runtime/app/src/core/product-identity.js +10 -7
  81. package/runtime/app/src/core/provenance.js +18 -18
  82. package/runtime/app/src/core/provider-learning.js +274 -274
  83. package/runtime/app/src/core/public-plugin-surface.js +25 -25
  84. package/runtime/app/src/core/public-restricted-data.js +105 -105
  85. package/runtime/app/src/core/rate-limit.js +118 -118
  86. package/runtime/app/src/core/relay-client.js +629 -568
  87. package/runtime/app/src/core/relay-delivery-store.js +512 -512
  88. package/runtime/app/src/core/relay-result-store.js +321 -321
  89. package/runtime/app/src/core/release-update.js +253 -253
  90. package/runtime/app/src/core/resilience-matrix.js +176 -0
  91. package/runtime/app/src/core/resilience-simulator.js +156 -0
  92. package/runtime/app/src/core/resource-identity.js +55 -0
  93. package/runtime/app/src/core/resource-leases.js +249 -0
  94. package/runtime/app/src/core/router.js +96 -96
  95. package/runtime/app/src/core/runtime.js +138 -136
  96. package/runtime/app/src/core/scoped-path-syntax.js +16 -16
  97. package/runtime/app/src/core/semantic-checkpoint.js +420 -0
  98. package/runtime/app/src/core/session-token.js +499 -498
  99. package/runtime/app/src/core/side-effect.js +30 -0
  100. package/runtime/app/src/core/studio-executor.js +527 -0
  101. package/runtime/app/src/core/studio-teach.js +494 -0
  102. package/runtime/app/src/core/task-failure.js +61 -0
  103. package/runtime/app/src/core/task-orchestrator.js +1682 -1148
  104. package/runtime/app/src/core/task-store.js +526 -517
  105. package/runtime/app/src/core/task-verifier.js +154 -0
  106. package/runtime/app/src/core/task.js +175 -142
  107. package/runtime/app/src/core/team-coordinator.js +988 -0
  108. package/runtime/app/src/core/trusted-executable.js +124 -124
  109. package/runtime/app/src/core/types.js +101 -83
  110. package/runtime/app/src/core/verification-kernel.js +82 -0
  111. package/runtime/app/src/core/windows-path-lease.js +138 -138
  112. package/runtime/app/src/core/world-model.js +528 -0
  113. package/runtime/native/operator-windows-dpapi.exe +0 -0
  114. package/runtime/native/operator-windows-path-lease.exe +0 -0
  115. package/runtime/native/operator-windows-uia.exe +0 -0
  116. package/runtime/runtime-manifest.json +332 -147
  117. package/src/approval-console.mjs +279 -0
  118. package/src/approval-window.ps1 +99 -0
  119. package/src/cli.mjs +425 -345
  120. package/src/publish-check.mjs +22 -22
@@ -1,342 +1,342 @@
1
- import crypto from 'node:crypto';
2
- import path from 'node:path';
3
-
4
- import { actionHash, canonicalJson } from '../../../src/core/action-identity.js';
5
- import { OperatorError } from '../../../src/core/errors.js';
6
- import { readDurableStateText, writeDurableStateText } from '../../../src/core/durable-state.js';
7
-
8
- const MAX_RECORDS = 2000;
9
- const RETENTION_MS = 24 * 60 * 60_000;
10
- const PENDING_TTL_MS = 10 * 60_000;
11
- const APPROVAL_TTL_MS = 10 * 60_000;
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
- export class ApprovalStore {
41
- #file ;
42
- #clock ;
43
- #queue = Promise.resolve();
44
-
45
- constructor(stateDir , options = {}) {
46
- this.#file = path.join(path.resolve(stateDir), 'approvals.json');
47
- this.#clock = options.clock ?? (() => new Date());
48
- }
49
-
50
- async register(action , authority ) {
51
- const hash = actionHash(action);
52
- const boundAuthority = approvalAuthorityHash(authority);
53
- return await this.#mutate((state) => {
54
- const now = this.#clock();
55
- prune(state, now.getTime());
56
- const existing = state.records.find((entry) => entry.actionId === action.id);
57
- if (existing && existing.actionHash !== hash) {
58
- throw new OperatorError('APPROVAL_ACTION_MISMATCH', 'Action ID is already bound to different action content.');
59
- }
60
- if (
61
- existing
62
- && existing.authorityHash === boundAuthority
63
- && existing.status === 'pending'
64
- && Date.parse(existing.pendingExpiresAt) > now.getTime()
65
- ) return clone(existing);
66
- if (
67
- existing
68
- && existing.authorityHash === boundAuthority
69
- && existing.status === 'approved'
70
- && existing.approvalExpiresAt
71
- && Date.parse(existing.approvalExpiresAt) > now.getTime()
72
- ) return clone(existing);
73
-
74
- const record = {
75
- actionId: action.id,
76
- actionHash: hash,
77
- authorityHash: boundAuthority,
78
- approvalRequestId: crypto.randomUUID(),
79
- capability: action.capability,
80
- risk: action.risk,
81
- target: action.target,
82
- status: 'pending',
83
- createdAt: now.toISOString(),
84
- pendingExpiresAt: new Date(now.getTime() + PENDING_TTL_MS).toISOString()
85
- };
86
- if (existing) Object.assign(existing, record, {
87
- approvedAt: undefined,
88
- approvalExpiresAt: undefined,
89
- consumedAt: undefined,
90
- deniedAt: undefined
91
- });
92
- else {
93
- if (state.records.length >= MAX_RECORDS) throw new OperatorError('APPROVAL_STORE_LIMIT', 'Approval store is full.');
94
- state.records.push(record);
95
- }
96
- return clone(existing ?? record);
97
- });
98
- }
99
-
100
- async list() {
101
- return await this.#mutate((state) => {
102
- prune(state, this.#clock().getTime());
103
- return state.records.slice().reverse().map(clone);
104
- });
105
- }
106
-
107
- async approve(actionId , approvalRequestId ) {
108
- const outcome = await this.#mutate((state) => {
109
- const record = requireRecord(state, actionId);
110
- requireApprovalRequest(record, approvalRequestId);
111
- const now = this.#clock();
112
- if (Date.parse(record.pendingExpiresAt) <= now.getTime()) {
113
- removeRecord(state, actionId);
114
- return { expired: true };
115
- }
116
- if (record.status !== 'pending') {
117
- throw new OperatorError('APPROVAL_NOT_PENDING', 'Only pending actions can be approved.');
118
- }
119
- record.status = 'approved';
120
- record.approvedAt = now.toISOString();
121
- record.approvalExpiresAt = new Date(now.getTime() + APPROVAL_TTL_MS).toISOString();
122
- return { record: clone(record) };
123
- });
124
- if ('expired' in outcome) throw new OperatorError('APPROVAL_EXPIRED', 'Approval request has expired.');
125
- return outcome.record;
126
- }
127
-
128
- async deny(actionId , approvalRequestId ) {
129
- const outcome = await this.#mutate((state) => {
130
- const record = requireRecord(state, actionId);
131
- requireApprovalRequest(record, approvalRequestId);
132
- const now = this.#clock();
133
- const expired = record.status === 'pending'
134
- ? Date.parse(record.pendingExpiresAt) <= now.getTime()
135
- : record.status === 'approved'
136
- ? !record.approvalExpiresAt || Date.parse(record.approvalExpiresAt) <= now.getTime()
137
- : false;
138
- if (expired) {
139
- removeRecord(state, actionId);
140
- return { expired: true };
141
- }
142
- if (record.status !== 'pending' && record.status !== 'approved') {
143
- throw new OperatorError('APPROVAL_NOT_ACTIVE', 'Only pending or approved actions can be denied.');
144
- }
145
- record.status = 'denied';
146
- record.deniedAt = now.toISOString();
147
- return { record: clone(record) };
148
- });
149
- if ('expired' in outcome) throw new OperatorError('APPROVAL_EXPIRED', 'Approval request has expired.');
150
- return outcome.record;
151
- }
152
-
153
- async isApproved(action , authority ) {
154
- const expectedHash = actionHash(action);
155
- const expectedAuthority = approvalAuthorityHash(authority);
156
- return await this.#mutate((state) => {
157
- prune(state, this.#clock().getTime());
158
- const record = state.records.find((entry) => entry.actionId === action.id);
159
- if (!record || record.status !== 'approved') return false;
160
- if (record.actionHash !== expectedHash || record.authorityHash !== expectedAuthority) return false;
161
- if (!record.approvalExpiresAt) return false;
162
- return Date.parse(record.approvalExpiresAt) > this.#clock().getTime();
163
- });
164
- }
165
-
166
- async consume(action , authority ) {
167
- const expectedHash = actionHash(action);
168
- const expectedAuthority = approvalAuthorityHash(authority);
169
- const outcome = await this.#mutate((state) => {
170
- const record = requireRecord(state, action.id);
171
- if (record.status !== 'approved' || record.actionHash !== expectedHash || record.authorityHash !== expectedAuthority) {
172
- throw new OperatorError('APPROVAL_NOT_VALID', 'Action approval is not valid for this exact action and authority.');
173
- }
174
- if (!record.approvalExpiresAt || Date.parse(record.approvalExpiresAt) <= this.#clock().getTime()) {
175
- removeRecord(state, action.id);
176
- return { expired: true };
177
- }
178
- record.status = 'consumed';
179
- record.consumedAt = this.#clock().toISOString();
180
- return { ok: true };
181
- });
182
- if ('expired' in outcome) throw new OperatorError('APPROVAL_EXPIRED', 'Action approval has expired.');
183
- }
184
-
185
- async #read() {
186
- try {
187
- const text = await readDurableStateText(this.#file, {
188
- maxBytes: 4 * 1024 * 1024,
189
- errorCode: 'APPROVAL_STORE_CORRUPT',
190
- invalidMessage: 'Approval store is invalid.'
191
- });
192
- const parsed = JSON.parse(text) ;
193
- if (parsed?.version === 1) return { version: 2, records: [] };
194
- return validateState(parsed );
195
- } catch (error) {
196
- if ((error ).code === 'ENOENT') return { version: 2, records: [] };
197
- throw error;
198
- }
199
- }
200
-
201
- async #write(state ) {
202
- await writeDurableStateText(this.#file, JSON.stringify(validateState(state), null, 2), {
203
- maxBytes: 4 * 1024 * 1024,
204
- errorCode: 'APPROVAL_STORE_CORRUPT',
205
- invalidMessage: 'Approval store is invalid.'
206
- });
207
- }
208
-
209
- async #mutate (fn ) {
210
- let release ;
211
- const previous = this.#queue;
212
- this.#queue = new Promise ((resolve) => { release = resolve; });
213
- await previous;
214
- try {
215
- const state = await this.#read();
216
- const value = await fn(state);
217
- await this.#write(state);
218
- return value;
219
- } finally {
220
- release();
221
- }
222
- }
223
- }
224
-
225
- function approvalAuthorityHash(authority ) {
226
- const normalized = authority === undefined
227
- ? { kind: 'local' }
228
- : {
229
- kind: 'account' ,
230
- accountId: validUuid(authority.accountId, 'approval accountId'),
231
- deviceId: validUuid(authority.deviceId, 'approval deviceId'),
232
- generation: validGeneration(authority.generation)
233
- };
234
- return crypto.createHash('sha256').update(canonicalJson(normalized), 'utf8').digest('hex');
235
- }
236
- function requireRecord(state , actionId ) {
237
- const record = state.records.find((entry) => entry.actionId === actionId);
238
- if (!record) throw new OperatorError('APPROVAL_NOT_FOUND', 'Approval request was not found.');
239
- return record;
240
- }
241
-
242
- function requireApprovalRequest(record , approvalRequestId ) {
243
- if (typeof approvalRequestId !== 'string' || approvalRequestId !== record.approvalRequestId) {
244
- throw new OperatorError('APPROVAL_REQUEST_MISMATCH', 'Approval request identity is stale or invalid.');
245
- }
246
- }
247
-
248
- function removeRecord(state , actionId ) {
249
- state.records = state.records.filter((entry) => entry.actionId !== actionId);
250
- }
251
-
252
- function prune(state , now ) {
253
- state.records = state.records.filter((entry) => {
254
- if (Date.parse(entry.createdAt) < now - RETENTION_MS) return false;
255
- if (entry.status === 'pending' && Date.parse(entry.pendingExpiresAt) <= now) return false;
256
- if (entry.status === 'approved' && (!entry.approvalExpiresAt || Date.parse(entry.approvalExpiresAt) <= now)) return false;
257
- return true;
258
- });
259
- }
260
-
261
- function clone(record ) { return { ...record }; }
262
- function validateState(input ) {
263
- if (!input || typeof input !== 'object' || input.version !== 2 || !Array.isArray(input.records) || input.records.length > MAX_RECORDS) {
264
- throw corrupt('Approval store structure is invalid.');
265
- }
266
- const actionIds = new Set ();
267
- const requestIds = new Set ();
268
- const records = input.records.map((raw) => {
269
- if (!raw || typeof raw !== 'object') throw corrupt('Approval record is invalid.');
270
- const actionId = validStoredText(raw.actionId, 256, 'action ID');
271
- if (actionIds.has(actionId)) throw corrupt('Approval action ID is duplicated.');
272
- actionIds.add(actionId);
273
- const actionHashValue = String(raw.actionHash ?? '');
274
- const authorityHash = String(raw.authorityHash ?? '');
275
- if (!/^[0-9a-f]{64}$/.test(actionHashValue) || !/^[0-9a-f]{64}$/.test(authorityHash)) {
276
- throw corrupt('Approval hashes are invalid.');
277
- }
278
- const approvalRequestId = String(raw.approvalRequestId ?? '');
279
- if (!isUuid(approvalRequestId) || requestIds.has(approvalRequestId)) throw corrupt('Approval request ID is invalid or duplicated.');
280
- requestIds.add(approvalRequestId);
281
- const capability = validStoredText(raw.capability, 128, 'capability');
282
- const risk = raw.risk;
283
- if (!['read', 'write', 'external', 'system', 'destructive'].includes(risk)) throw corrupt('Approval risk is invalid.');
284
- const status = raw.status;
285
- if (!['pending', 'approved', 'consumed', 'denied'].includes(status)) throw corrupt('Approval status is invalid.');
286
- const target = raw.target === undefined ? undefined : validStoredText(raw.target, 4096, 'target');
287
- const createdAt = validIso(raw.createdAt, 'createdAt');
288
- const pendingExpiresAt = validIso(raw.pendingExpiresAt, 'pendingExpiresAt');
289
- if (Date.parse(pendingExpiresAt) <= Date.parse(createdAt)) throw corrupt('Pending approval expiry is invalid.');
290
- const approvedAt = raw.approvedAt === undefined ? undefined : validIso(raw.approvedAt, 'approvedAt');
291
- const approvalExpiresAt = raw.approvalExpiresAt === undefined ? undefined : validIso(raw.approvalExpiresAt, 'approvalExpiresAt');
292
- const consumedAt = raw.consumedAt === undefined ? undefined : validIso(raw.consumedAt, 'consumedAt');
293
- const deniedAt = raw.deniedAt === undefined ? undefined : validIso(raw.deniedAt, 'deniedAt');
294
-
295
- if (status === 'pending' && (approvedAt || approvalExpiresAt || consumedAt || deniedAt)) throw corrupt('Pending approval metadata is inconsistent.');
296
- if (status === 'approved' && (!approvedAt || !approvalExpiresAt || consumedAt || deniedAt)) throw corrupt('Approved metadata is inconsistent.');
297
- if (status === 'consumed' && (!approvedAt || !approvalExpiresAt || !consumedAt || deniedAt)) throw corrupt('Consumed approval metadata is inconsistent.');
298
- if (status === 'denied' && (!deniedAt || consumedAt)) throw corrupt('Denied approval metadata is inconsistent.');
299
- if (approvedAt && Date.parse(approvedAt) < Date.parse(createdAt)) throw corrupt('Approval timestamp ordering is invalid.');
300
- if (approvalExpiresAt && (!approvedAt || Date.parse(approvalExpiresAt) <= Date.parse(approvedAt))) throw corrupt('Approval expiry ordering is invalid.');
301
- if (consumedAt && approvedAt && Date.parse(consumedAt) < Date.parse(approvedAt)) throw corrupt('Approval consumption ordering is invalid.');
302
-
303
- return {
304
- actionId, actionHash: actionHashValue, authorityHash, approvalRequestId,
305
- capability, risk: risk , target, status: status ,
306
- createdAt, pendingExpiresAt, approvedAt, approvalExpiresAt, consumedAt, deniedAt
307
- } ;
308
- });
309
- return { version: 2, records };
310
- }
311
- function validStoredText(value , max , label ) {
312
- const text = String(value ?? '');
313
- if (!text || text.length > max || /\0/.test(text)) throw corrupt(`Approval ${label} is invalid.`);
314
- return text;
315
- }
316
-
317
- function validIso(value , label ) {
318
- const text = String(value ?? '');
319
- const time = Date.parse(text);
320
- if (!Number.isFinite(time) || new Date(time).toISOString() !== text) throw corrupt(`Approval ${label} is invalid.`);
321
- return text;
322
- }
323
-
324
- function validUuid(value , label ) {
325
- const text = String(value ?? '');
326
- if (!isUuid(text)) throw new OperatorError('APPROVAL_AUTHORITY_INVALID', `${label} must be a UUID.`);
327
- return text.toLowerCase();
328
- }
329
-
330
- function validGeneration(value ) {
331
- const generation = Number(value);
332
- if (!Number.isSafeInteger(generation) || generation < 1) throw new OperatorError('APPROVAL_AUTHORITY_INVALID', 'Approval authority generation is invalid.');
333
- return generation;
334
- }
335
-
336
- function isUuid(value ) {
337
- return /^[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(value);
338
- }
339
-
340
- function corrupt(message ) {
341
- return new OperatorError('APPROVAL_STORE_CORRUPT', message);
342
- }
1
+ import crypto from 'node:crypto';
2
+ import path from 'node:path';
3
+
4
+ import { actionHash, canonicalJson } from '../../../src/core/action-identity.js';
5
+ import { OperatorError } from '../../../src/core/errors.js';
6
+ import { readDurableStateText, writeDurableStateText } from '../../../src/core/durable-state.js';
7
+
8
+ const MAX_RECORDS = 2000;
9
+ const RETENTION_MS = 24 * 60 * 60_000;
10
+ const PENDING_TTL_MS = 10 * 60_000;
11
+ const APPROVAL_TTL_MS = 10 * 60_000;
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
+ export class ApprovalStore {
41
+ #file ;
42
+ #clock ;
43
+ #queue = Promise.resolve();
44
+
45
+ constructor(stateDir , options = {}) {
46
+ this.#file = path.join(path.resolve(stateDir), 'approvals.json');
47
+ this.#clock = options.clock ?? (() => new Date());
48
+ }
49
+
50
+ async register(action , authority ) {
51
+ const hash = actionHash(action);
52
+ const boundAuthority = approvalAuthorityFingerprint(authority);
53
+ return await this.#mutate((state) => {
54
+ const now = this.#clock();
55
+ prune(state, now.getTime());
56
+ const existing = state.records.find((entry) => entry.actionId === action.id);
57
+ if (existing && existing.actionHash !== hash) {
58
+ throw new OperatorError('APPROVAL_ACTION_MISMATCH', 'Action ID is already bound to different action content.');
59
+ }
60
+ if (
61
+ existing
62
+ && existing.authorityHash === boundAuthority
63
+ && existing.status === 'pending'
64
+ && Date.parse(existing.pendingExpiresAt) > now.getTime()
65
+ ) return clone(existing);
66
+ if (
67
+ existing
68
+ && existing.authorityHash === boundAuthority
69
+ && existing.status === 'approved'
70
+ && existing.approvalExpiresAt
71
+ && Date.parse(existing.approvalExpiresAt) > now.getTime()
72
+ ) return clone(existing);
73
+
74
+ const record = {
75
+ actionId: action.id,
76
+ actionHash: hash,
77
+ authorityHash: boundAuthority,
78
+ approvalRequestId: crypto.randomUUID(),
79
+ capability: action.capability,
80
+ risk: action.risk,
81
+ target: action.target,
82
+ status: 'pending',
83
+ createdAt: now.toISOString(),
84
+ pendingExpiresAt: new Date(now.getTime() + PENDING_TTL_MS).toISOString()
85
+ };
86
+ if (existing) Object.assign(existing, record, {
87
+ approvedAt: undefined,
88
+ approvalExpiresAt: undefined,
89
+ consumedAt: undefined,
90
+ deniedAt: undefined
91
+ });
92
+ else {
93
+ if (state.records.length >= MAX_RECORDS) throw new OperatorError('APPROVAL_STORE_LIMIT', 'Approval store is full.');
94
+ state.records.push(record);
95
+ }
96
+ return clone(existing ?? record);
97
+ });
98
+ }
99
+
100
+ async list() {
101
+ return await this.#mutate((state) => {
102
+ prune(state, this.#clock().getTime());
103
+ return state.records.slice().reverse().map(clone);
104
+ });
105
+ }
106
+
107
+ async approve(actionId , approvalRequestId ) {
108
+ const outcome = await this.#mutate((state) => {
109
+ const record = requireRecord(state, actionId);
110
+ requireApprovalRequest(record, approvalRequestId);
111
+ const now = this.#clock();
112
+ if (Date.parse(record.pendingExpiresAt) <= now.getTime()) {
113
+ removeRecord(state, actionId);
114
+ return { expired: true };
115
+ }
116
+ if (record.status !== 'pending') {
117
+ throw new OperatorError('APPROVAL_NOT_PENDING', 'Only pending actions can be approved.');
118
+ }
119
+ record.status = 'approved';
120
+ record.approvedAt = now.toISOString();
121
+ record.approvalExpiresAt = new Date(now.getTime() + APPROVAL_TTL_MS).toISOString();
122
+ return { record: clone(record) };
123
+ });
124
+ if ('expired' in outcome) throw new OperatorError('APPROVAL_EXPIRED', 'Approval request has expired.');
125
+ return outcome.record;
126
+ }
127
+
128
+ async deny(actionId , approvalRequestId ) {
129
+ const outcome = await this.#mutate((state) => {
130
+ const record = requireRecord(state, actionId);
131
+ requireApprovalRequest(record, approvalRequestId);
132
+ const now = this.#clock();
133
+ const expired = record.status === 'pending'
134
+ ? Date.parse(record.pendingExpiresAt) <= now.getTime()
135
+ : record.status === 'approved'
136
+ ? !record.approvalExpiresAt || Date.parse(record.approvalExpiresAt) <= now.getTime()
137
+ : false;
138
+ if (expired) {
139
+ removeRecord(state, actionId);
140
+ return { expired: true };
141
+ }
142
+ if (record.status !== 'pending' && record.status !== 'approved') {
143
+ throw new OperatorError('APPROVAL_NOT_ACTIVE', 'Only pending or approved actions can be denied.');
144
+ }
145
+ record.status = 'denied';
146
+ record.deniedAt = now.toISOString();
147
+ return { record: clone(record) };
148
+ });
149
+ if ('expired' in outcome) throw new OperatorError('APPROVAL_EXPIRED', 'Approval request has expired.');
150
+ return outcome.record;
151
+ }
152
+
153
+ async isApproved(action , authority ) {
154
+ const expectedHash = actionHash(action);
155
+ const expectedAuthority = approvalAuthorityFingerprint(authority);
156
+ return await this.#mutate((state) => {
157
+ prune(state, this.#clock().getTime());
158
+ const record = state.records.find((entry) => entry.actionId === action.id);
159
+ if (!record || record.status !== 'approved') return false;
160
+ if (record.actionHash !== expectedHash || record.authorityHash !== expectedAuthority) return false;
161
+ if (!record.approvalExpiresAt) return false;
162
+ return Date.parse(record.approvalExpiresAt) > this.#clock().getTime();
163
+ });
164
+ }
165
+
166
+ async consume(action , authority ) {
167
+ const expectedHash = actionHash(action);
168
+ const expectedAuthority = approvalAuthorityFingerprint(authority);
169
+ const outcome = await this.#mutate((state) => {
170
+ const record = requireRecord(state, action.id);
171
+ if (record.status !== 'approved' || record.actionHash !== expectedHash || record.authorityHash !== expectedAuthority) {
172
+ throw new OperatorError('APPROVAL_NOT_VALID', 'Action approval is not valid for this exact action and authority.');
173
+ }
174
+ if (!record.approvalExpiresAt || Date.parse(record.approvalExpiresAt) <= this.#clock().getTime()) {
175
+ removeRecord(state, action.id);
176
+ return { expired: true };
177
+ }
178
+ record.status = 'consumed';
179
+ record.consumedAt = this.#clock().toISOString();
180
+ return { ok: true };
181
+ });
182
+ if ('expired' in outcome) throw new OperatorError('APPROVAL_EXPIRED', 'Action approval has expired.');
183
+ }
184
+
185
+ async #read() {
186
+ try {
187
+ const text = await readDurableStateText(this.#file, {
188
+ maxBytes: 4 * 1024 * 1024,
189
+ errorCode: 'APPROVAL_STORE_CORRUPT',
190
+ invalidMessage: 'Approval store is invalid.'
191
+ });
192
+ const parsed = JSON.parse(text) ;
193
+ if (parsed?.version === 1) return { version: 2, records: [] };
194
+ return validateState(parsed );
195
+ } catch (error) {
196
+ if ((error ).code === 'ENOENT') return { version: 2, records: [] };
197
+ throw error;
198
+ }
199
+ }
200
+
201
+ async #write(state ) {
202
+ await writeDurableStateText(this.#file, JSON.stringify(validateState(state), null, 2), {
203
+ maxBytes: 4 * 1024 * 1024,
204
+ errorCode: 'APPROVAL_STORE_CORRUPT',
205
+ invalidMessage: 'Approval store is invalid.'
206
+ });
207
+ }
208
+
209
+ async #mutate (fn ) {
210
+ let release ;
211
+ const previous = this.#queue;
212
+ this.#queue = new Promise ((resolve) => { release = resolve; });
213
+ await previous;
214
+ try {
215
+ const state = await this.#read();
216
+ const value = await fn(state);
217
+ await this.#write(state);
218
+ return value;
219
+ } finally {
220
+ release();
221
+ }
222
+ }
223
+ }
224
+
225
+ export function approvalAuthorityFingerprint(authority ) {
226
+ const normalized = authority === undefined
227
+ ? { kind: 'local' }
228
+ : {
229
+ kind: 'account' ,
230
+ accountId: validUuid(authority.accountId, 'approval accountId'),
231
+ deviceId: validUuid(authority.deviceId, 'approval deviceId'),
232
+ generation: validGeneration(authority.generation)
233
+ };
234
+ return crypto.createHash('sha256').update(canonicalJson(normalized), 'utf8').digest('hex');
235
+ }
236
+ function requireRecord(state , actionId ) {
237
+ const record = state.records.find((entry) => entry.actionId === actionId);
238
+ if (!record) throw new OperatorError('APPROVAL_NOT_FOUND', 'Approval request was not found.');
239
+ return record;
240
+ }
241
+
242
+ function requireApprovalRequest(record , approvalRequestId ) {
243
+ if (typeof approvalRequestId !== 'string' || approvalRequestId !== record.approvalRequestId) {
244
+ throw new OperatorError('APPROVAL_REQUEST_MISMATCH', 'Approval request identity is stale or invalid.');
245
+ }
246
+ }
247
+
248
+ function removeRecord(state , actionId ) {
249
+ state.records = state.records.filter((entry) => entry.actionId !== actionId);
250
+ }
251
+
252
+ function prune(state , now ) {
253
+ state.records = state.records.filter((entry) => {
254
+ if (Date.parse(entry.createdAt) < now - RETENTION_MS) return false;
255
+ if (entry.status === 'pending' && Date.parse(entry.pendingExpiresAt) <= now) return false;
256
+ if (entry.status === 'approved' && (!entry.approvalExpiresAt || Date.parse(entry.approvalExpiresAt) <= now)) return false;
257
+ return true;
258
+ });
259
+ }
260
+
261
+ function clone(record ) { return { ...record }; }
262
+ function validateState(input ) {
263
+ if (!input || typeof input !== 'object' || input.version !== 2 || !Array.isArray(input.records) || input.records.length > MAX_RECORDS) {
264
+ throw corrupt('Approval store structure is invalid.');
265
+ }
266
+ const actionIds = new Set ();
267
+ const requestIds = new Set ();
268
+ const records = input.records.map((raw) => {
269
+ if (!raw || typeof raw !== 'object') throw corrupt('Approval record is invalid.');
270
+ const actionId = validStoredText(raw.actionId, 256, 'action ID');
271
+ if (actionIds.has(actionId)) throw corrupt('Approval action ID is duplicated.');
272
+ actionIds.add(actionId);
273
+ const actionHashValue = String(raw.actionHash ?? '');
274
+ const authorityHash = String(raw.authorityHash ?? '');
275
+ if (!/^[0-9a-f]{64}$/.test(actionHashValue) || !/^[0-9a-f]{64}$/.test(authorityHash)) {
276
+ throw corrupt('Approval hashes are invalid.');
277
+ }
278
+ const approvalRequestId = String(raw.approvalRequestId ?? '');
279
+ if (!isUuid(approvalRequestId) || requestIds.has(approvalRequestId)) throw corrupt('Approval request ID is invalid or duplicated.');
280
+ requestIds.add(approvalRequestId);
281
+ const capability = validStoredText(raw.capability, 128, 'capability');
282
+ const risk = raw.risk;
283
+ if (!['read', 'write', 'external', 'system', 'destructive'].includes(risk)) throw corrupt('Approval risk is invalid.');
284
+ const status = raw.status;
285
+ if (!['pending', 'approved', 'consumed', 'denied'].includes(status)) throw corrupt('Approval status is invalid.');
286
+ const target = raw.target === undefined ? undefined : validStoredText(raw.target, 4096, 'target');
287
+ const createdAt = validIso(raw.createdAt, 'createdAt');
288
+ const pendingExpiresAt = validIso(raw.pendingExpiresAt, 'pendingExpiresAt');
289
+ if (Date.parse(pendingExpiresAt) <= Date.parse(createdAt)) throw corrupt('Pending approval expiry is invalid.');
290
+ const approvedAt = raw.approvedAt === undefined ? undefined : validIso(raw.approvedAt, 'approvedAt');
291
+ const approvalExpiresAt = raw.approvalExpiresAt === undefined ? undefined : validIso(raw.approvalExpiresAt, 'approvalExpiresAt');
292
+ const consumedAt = raw.consumedAt === undefined ? undefined : validIso(raw.consumedAt, 'consumedAt');
293
+ const deniedAt = raw.deniedAt === undefined ? undefined : validIso(raw.deniedAt, 'deniedAt');
294
+
295
+ if (status === 'pending' && (approvedAt || approvalExpiresAt || consumedAt || deniedAt)) throw corrupt('Pending approval metadata is inconsistent.');
296
+ if (status === 'approved' && (!approvedAt || !approvalExpiresAt || consumedAt || deniedAt)) throw corrupt('Approved metadata is inconsistent.');
297
+ if (status === 'consumed' && (!approvedAt || !approvalExpiresAt || !consumedAt || deniedAt)) throw corrupt('Consumed approval metadata is inconsistent.');
298
+ if (status === 'denied' && (!deniedAt || consumedAt)) throw corrupt('Denied approval metadata is inconsistent.');
299
+ if (approvedAt && Date.parse(approvedAt) < Date.parse(createdAt)) throw corrupt('Approval timestamp ordering is invalid.');
300
+ if (approvalExpiresAt && (!approvedAt || Date.parse(approvalExpiresAt) <= Date.parse(approvedAt))) throw corrupt('Approval expiry ordering is invalid.');
301
+ if (consumedAt && approvedAt && Date.parse(consumedAt) < Date.parse(approvedAt)) throw corrupt('Approval consumption ordering is invalid.');
302
+
303
+ return {
304
+ actionId, actionHash: actionHashValue, authorityHash, approvalRequestId,
305
+ capability, risk: risk , target, status: status ,
306
+ createdAt, pendingExpiresAt, approvedAt, approvalExpiresAt, consumedAt, deniedAt
307
+ } ;
308
+ });
309
+ return { version: 2, records };
310
+ }
311
+ function validStoredText(value , max , label ) {
312
+ const text = String(value ?? '');
313
+ if (!text || text.length > max || /\0/.test(text)) throw corrupt(`Approval ${label} is invalid.`);
314
+ return text;
315
+ }
316
+
317
+ function validIso(value , label ) {
318
+ const text = String(value ?? '');
319
+ const time = Date.parse(text);
320
+ if (!Number.isFinite(time) || new Date(time).toISOString() !== text) throw corrupt(`Approval ${label} is invalid.`);
321
+ return text;
322
+ }
323
+
324
+ function validUuid(value , label ) {
325
+ const text = String(value ?? '');
326
+ if (!isUuid(text)) throw new OperatorError('APPROVAL_AUTHORITY_INVALID', `${label} must be a UUID.`);
327
+ return text.toLowerCase();
328
+ }
329
+
330
+ function validGeneration(value ) {
331
+ const generation = Number(value);
332
+ if (!Number.isSafeInteger(generation) || generation < 1) throw new OperatorError('APPROVAL_AUTHORITY_INVALID', 'Approval authority generation is invalid.');
333
+ return generation;
334
+ }
335
+
336
+ function isUuid(value ) {
337
+ return /^[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(value);
338
+ }
339
+
340
+ function corrupt(message ) {
341
+ return new OperatorError('APPROVAL_STORE_CORRUPT', message);
342
+ }