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.
- package/DISCLOSURE +47 -47
- package/LICENSE +64 -64
- package/README.md +46 -44
- package/bin/operator.mjs +10 -10
- package/package.json +58 -58
- package/runtime/app/apps/local-agent/src/approval-store.js +342 -342
- package/runtime/app/apps/local-agent/src/bootstrap-config.js +255 -255
- package/runtime/app/apps/local-agent/src/cli.js +237 -237
- package/runtime/app/apps/local-agent/src/control-center.js +61 -0
- package/runtime/app/apps/local-agent/src/device-reset.js +260 -260
- package/runtime/app/apps/local-agent/src/emergency-stop.js +141 -141
- package/runtime/app/apps/local-agent/src/main.js +448 -311
- package/runtime/app/apps/local-agent/src/migration-proofs.js +194 -0
- package/runtime/app/apps/local-agent/src/privacy-data.js +129 -129
- package/runtime/app/apps/local-agent/src/relay-agent.js +542 -333
- package/runtime/app/apps/local-agent/src/relay-enrollment.js +156 -156
- package/runtime/app/apps/local-agent/src/relay-session-credentials.js +436 -436
- package/runtime/app/apps/local-agent/src/remote.js +130 -130
- package/runtime/app/apps/local-agent/src/runtime-factory.js +78 -66
- package/runtime/app/apps/local-agent/src/server.js +2069 -578
- package/runtime/app/apps/local-agent/src/session-approval.js +119 -0
- package/runtime/app/src/capabilities/browser-cdp-connection.js +199 -199
- package/runtime/app/src/capabilities/browser-cdp-frames.js +288 -288
- package/runtime/app/src/capabilities/browser-cdp-page.js +430 -430
- package/runtime/app/src/capabilities/browser-cdp.js +384 -384
- package/runtime/app/src/capabilities/browser-managed.js +345 -345
- package/runtime/app/src/capabilities/docker.js +448 -448
- package/runtime/app/src/capabilities/filesystem.js +546 -264
- package/runtime/app/src/capabilities/git-checkpoint.js +415 -415
- package/runtime/app/src/capabilities/git-write.js +321 -321
- package/runtime/app/src/capabilities/git.js +252 -252
- package/runtime/app/src/capabilities/index.js +18 -16
- package/runtime/app/src/capabilities/path-scope.js +86 -86
- package/runtime/app/src/capabilities/perception.js +138 -0
- package/runtime/app/src/capabilities/postgres.js +566 -566
- package/runtime/app/src/capabilities/process.js +625 -258
- package/runtime/app/src/capabilities/project-command.js +598 -598
- package/runtime/app/src/capabilities/project-transaction.js +215 -215
- package/runtime/app/src/capabilities/project.js +67 -67
- package/runtime/app/src/capabilities/sandboxed-compute.js +226 -0
- package/runtime/app/src/capabilities/system.js +40 -40
- package/runtime/app/src/capabilities/vscode.js +312 -312
- package/runtime/app/src/capabilities/windows-uia.js +616 -331
- package/runtime/app/src/core/account-device-registry.js +604 -604
- package/runtime/app/src/core/action-identity.js +44 -44
- package/runtime/app/src/core/audit.js +383 -295
- package/runtime/app/src/core/authority-kernel.js +209 -0
- package/runtime/app/src/core/capability-policy.js +69 -58
- package/runtime/app/src/core/capability-sdk.js +188 -0
- package/runtime/app/src/core/child-environment.js +28 -28
- package/runtime/app/src/core/conversation-ledger.js +146 -0
- package/runtime/app/src/core/desired-state-reconciler.js +90 -0
- package/runtime/app/src/core/desired-state.js +470 -0
- package/runtime/app/src/core/developer-relay-surface.js +44 -0
- package/runtime/app/src/core/device-enrollment.js +417 -417
- package/runtime/app/src/core/device-identity.js +436 -436
- package/runtime/app/src/core/device-pool.js +318 -0
- package/runtime/app/src/core/device-registry.js +477 -477
- package/runtime/app/src/core/device-reset.js +178 -178
- package/runtime/app/src/core/device-routing.js +330 -305
- package/runtime/app/src/core/digital-operations.js +707 -0
- package/runtime/app/src/core/durable-state.js +321 -306
- package/runtime/app/src/core/enterprise-policy.js +290 -0
- package/runtime/app/src/core/errors.js +20 -20
- package/runtime/app/src/core/evaluation.js +336 -0
- package/runtime/app/src/core/event-runtime.js +267 -0
- package/runtime/app/src/core/event-ticker.js +66 -0
- package/runtime/app/src/core/evidence.js +9 -9
- package/runtime/app/src/core/execution-optimizer.js +255 -0
- package/runtime/app/src/core/index.js +25 -17
- package/runtime/app/src/core/intent-kernel.js +152 -0
- package/runtime/app/src/core/machine-state.js +161 -160
- package/runtime/app/src/core/network-authority.js +20 -20
- package/runtime/app/src/core/organization-coordinator.js +410 -0
- package/runtime/app/src/core/outcome-planner.js +179 -0
- package/runtime/app/src/core/perception-graph.js +365 -0
- package/runtime/app/src/core/perception-publication.js +139 -0
- package/runtime/app/src/core/policy.js +76 -67
- package/runtime/app/src/core/procedure-memory.js +399 -0
- package/runtime/app/src/core/product-identity.js +10 -7
- package/runtime/app/src/core/provenance.js +18 -18
- package/runtime/app/src/core/provider-learning.js +274 -274
- package/runtime/app/src/core/public-plugin-surface.js +25 -25
- package/runtime/app/src/core/public-restricted-data.js +105 -105
- package/runtime/app/src/core/rate-limit.js +118 -118
- package/runtime/app/src/core/relay-client.js +629 -568
- package/runtime/app/src/core/relay-delivery-store.js +512 -512
- package/runtime/app/src/core/relay-result-store.js +321 -321
- package/runtime/app/src/core/release-update.js +253 -253
- package/runtime/app/src/core/resilience-matrix.js +176 -0
- package/runtime/app/src/core/resilience-simulator.js +156 -0
- package/runtime/app/src/core/resource-identity.js +55 -0
- package/runtime/app/src/core/resource-leases.js +249 -0
- package/runtime/app/src/core/router.js +96 -96
- package/runtime/app/src/core/runtime.js +138 -136
- package/runtime/app/src/core/scoped-path-syntax.js +16 -16
- package/runtime/app/src/core/semantic-checkpoint.js +420 -0
- package/runtime/app/src/core/session-token.js +499 -498
- package/runtime/app/src/core/side-effect.js +30 -0
- package/runtime/app/src/core/studio-executor.js +527 -0
- package/runtime/app/src/core/studio-teach.js +494 -0
- package/runtime/app/src/core/task-failure.js +61 -0
- package/runtime/app/src/core/task-orchestrator.js +1682 -1148
- package/runtime/app/src/core/task-store.js +526 -517
- package/runtime/app/src/core/task-verifier.js +154 -0
- package/runtime/app/src/core/task.js +175 -142
- package/runtime/app/src/core/team-coordinator.js +988 -0
- package/runtime/app/src/core/trusted-executable.js +124 -124
- package/runtime/app/src/core/types.js +101 -83
- package/runtime/app/src/core/verification-kernel.js +82 -0
- package/runtime/app/src/core/windows-path-lease.js +138 -138
- package/runtime/app/src/core/world-model.js +528 -0
- package/runtime/native/operator-windows-dpapi.exe +0 -0
- package/runtime/native/operator-windows-path-lease.exe +0 -0
- package/runtime/native/operator-windows-uia.exe +0 -0
- package/runtime/runtime-manifest.json +332 -147
- package/src/approval-console.mjs +279 -0
- package/src/approval-window.ps1 +99 -0
- package/src/cli.mjs +425 -345
- 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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
+
}
|