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
package/DISCLOSURE ADDED
@@ -0,0 +1,47 @@
1
+ MECORD CONNECT — DUAL-USE CONTENT DISCLOSURE
2
+
3
+ Package: mecord-connect
4
+ Publisher: Kinthala Samuel Sampath Kumar
5
+ Contact: support@splcart.in
6
+
7
+ Purpose
8
+
9
+ mecord-connect is the Windows runtime used to pair a user-authorized computer with Mecord Connect for legitimate development and computer-automation workflows initiated through ChatGPT.
10
+
11
+ Why this package is declared dual-use
12
+
13
+ The package contains security-relevant remote-management capabilities because it can:
14
+ - connect a local runtime to an authenticated remote relay;
15
+ - inspect and modify files within locally authorized project roots;
16
+ - expose bounded Git/project operations;
17
+ - run locally trusted project commands subject to policy and approval controls;
18
+ - interact with browser/UI automation capabilities that are available to the private local runtime but are not broadly exposed by the public plugin.
19
+
20
+ These capabilities can be useful for legitimate development, administration, automation, support, and security work, but similar capabilities could be misused if operated without authorization. For that reason the package declares:
21
+
22
+ "contentPolicy": {
23
+ "class": "dual-use"
24
+ }
25
+
26
+ Intended legitimate use
27
+
28
+ The Runtime is intended only for computers, accounts, repositories, websites, data, and services the user owns or is authorized to control.
29
+
30
+ Safeguards
31
+
32
+ Mecord Connect is designed to reduce misuse through multiple independent controls, including:
33
+ - authenticated OAuth access to the hosted MCP service;
34
+ - explicit device pairing and signed device identity;
35
+ - locally enforced capability policy and authorized filesystem roots;
36
+ - approval requirements for risk-bearing operations;
37
+ - restricted public plugin tooling rather than unrestricted terminal/browser/UIA exposure;
38
+ - credential/restricted-data guards and minimized public responses;
39
+ - fail-closed relay/session behavior;
40
+ - ambient credential stripping in the npm launcher;
41
+ - immutable source/payload binding, integrity hashes, and Windows-native helper self-tests.
42
+
43
+ The package is not intended to provide stealth, persistence, credential theft, malware delivery, access-control bypass, or unauthorized surveillance.
44
+
45
+ Security and abuse reports
46
+
47
+ Report suspected abuse or security issues privately to support@splcart.in. Do not send live passwords, access tokens, private keys, recovery credentials, or other active secrets.
package/LICENSE ADDED
@@ -0,0 +1,64 @@
1
+ MECORD CONNECT RUNTIME LICENSE AGREEMENT
2
+
3
+ Copyright (c) 2026 Kinthala Samuel Sampath Kumar. All rights reserved.
4
+
5
+ This Mecord Connect Runtime License Agreement ("Agreement") governs your use of the software distributed as the mecord-connect package, including its bundled runtime payload, launchers, native helpers, updates, and accompanying documentation (collectively, the "Runtime").
6
+
7
+ Licensor
8
+ Kinthala Samuel Sampath Kumar
9
+ Akkayapalem, Visakhapatnam, Andhra Pradesh, India
10
+ Support contact: support@splcart.in
11
+
12
+ 1. License Grant
13
+
14
+ Subject to this Agreement, the Licensor grants you a limited, non-exclusive, revocable, non-transferable, non-sublicensable license to install and use the Runtime on computers and accounts that you own or are authorized to control, solely for legitimate use with Mecord Connect and its supported local-development workflows.
15
+
16
+ 2. Permitted Use
17
+
18
+ You may:
19
+ - install and run the Runtime on authorized Windows computers;
20
+ - use it to connect an authorized computer to Mecord Connect;
21
+ - use functionality exposed through the Runtime only within the authorization, safety, approval, and access-control boundaries provided by Mecord Connect and the local Runtime;
22
+ - make a reasonable backup copy solely for your own permitted use where applicable law allows.
23
+
24
+ 3. Restrictions
25
+
26
+ Unless applicable law expressly requires otherwise, you may not:
27
+ - redistribute, sell, sublicense, rent, lease, host, mirror, or republish the Runtime or a modified copy;
28
+ - remove or alter copyright, license, safety, provenance, or security notices;
29
+ - reverse engineer, decompile, disassemble, or attempt to derive source code from compiled components except to the limited extent such restriction is prohibited by applicable law;
30
+ - bypass, disable, tamper with, or circumvent pairing, authentication, local approvals, capability policy, authorized-root restrictions, credential protections, rate limits, security checks, or other safeguards;
31
+ - use the Runtime to access computers, accounts, data, services, repositories, or networks without authorization;
32
+ - use the Runtime to distribute malware, steal credentials, evade access controls, interfere with systems, or violate applicable law or third-party rights.
33
+
34
+ 4. Ownership
35
+
36
+ The Runtime is licensed, not sold. The Licensor and applicable contributors retain all right, title, and interest in and to the Runtime and all associated intellectual-property rights, except for third-party components governed by their own licenses.
37
+
38
+ 5. Hosted Services and Third Parties
39
+
40
+ The Runtime may connect to Mecord Connect hosted services and third-party services. Access to those services may be subject to separate terms, privacy notices, availability limits, provider rules, and charges. This Agreement does not grant rights in third-party software or services.
41
+
42
+ 6. Updates and Changes
43
+
44
+ The Licensor may update, replace, suspend, or discontinue the Runtime or related hosted functionality. Updated versions may require acceptance of updated terms where permitted by law.
45
+
46
+ 7. Termination
47
+
48
+ This Agreement terminates automatically if you materially breach it. Upon termination, you must stop using and delete copies of the Runtime, except where retention is required by law. Provisions that by their nature should survive termination, including ownership, restrictions, disclaimers, liability limits, and governing law, survive.
49
+
50
+ 8. Disclaimer
51
+
52
+ TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW, THE RUNTIME IS PROVIDED "AS IS" AND "AS AVAILABLE" WITHOUT WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, OR UNINTERRUPTED OR ERROR-FREE OPERATION. NOTHING IN THIS AGREEMENT EXCLUDES WARRANTIES OR RIGHTS THAT CANNOT LEGALLY BE EXCLUDED.
53
+
54
+ 9. Limitation of Liability
55
+
56
+ TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSOR WILL NOT BE LIABLE FOR INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, PUNITIVE, OR CONSEQUENTIAL DAMAGES, OR FOR LOSS OF PROFITS, REVENUE, DATA, BUSINESS, OR GOODWILL ARISING FROM OR RELATED TO THE RUNTIME. NOTHING IN THIS AGREEMENT LIMITS LIABILITY THAT CANNOT LEGALLY BE LIMITED.
57
+
58
+ 10. Governing Law and Jurisdiction
59
+
60
+ This Agreement is governed by the laws of India. Subject to mandatory consumer-protection and jurisdiction rules that cannot be waived, disputes relating to this Agreement are subject to the competent courts at Visakhapatnam, Andhra Pradesh, India.
61
+
62
+ 11. Contact
63
+
64
+ Questions about this Agreement may be sent to support@splcart.in.
package/README.md ADDED
@@ -0,0 +1,44 @@
1
+ # mecord-connect
2
+
3
+ One-command Windows runtime for connecting a user-authorized computer to Mecord Connect in ChatGPT.
4
+
5
+ ```powershell
6
+ npx mecord-connect@latest remote
7
+ ```
8
+
9
+ Requires Windows x64 with a supported Node.js line: 22.14+, 24.x, or 26.x. Git features additionally require Git 2.45 or newer; older or missing Git disables only the Git capabilities while the rest of the Mecord Connect runtime remains available.
10
+
11
+ The command does **not** install an MSIX and does **not** start a local MCP server. It verifies the package's runtime manifest, starts the existing hardened local policy/runtime agent, and connects that agent to the pinned production relay at `wss://operator.splcart.in/device`. ChatGPT talks to the hosted MCP edge.
12
+
13
+ On first use the runtime prints a one-time device pairing code. Claim that code from Mecord Connect in the authenticated ChatGPT connection. The resulting short-lived relay credential is protected for the current Windows user with DPAPI and is rotated by the existing relay-session authority.
14
+
15
+ By default the current directory is the authorized filesystem/project root. To authorize another folder explicitly:
16
+
17
+ ```powershell
18
+ npx mecord-connect@latest remote --root C:\path\to\project
19
+ ```
20
+
21
+ Use `--no-browser` to disable automatic launch of Mecord Connect's managed-browser capability. Generic terminal execution remains disabled by default; project commands continue to use the existing policy/approval boundary.
22
+
23
+ Before starting a session you can verify the package payload and all three Windows-native security helpers:
24
+
25
+ ```powershell
26
+ npx mecord-connect@latest doctor
27
+ ```
28
+
29
+ `doctor` verifies every packaged runtime file against SHA-256/size metadata, exercises the DPAPI and Windows path-lease self-tests, and performs a protocol health check against the UI Automation sidecar. It does not start the relay connection.
30
+
31
+ ## Security model
32
+
33
+ The npm launcher deliberately rebuilds the child environment from a small desktop allowlist. npm/GitHub/cloud tokens, `.npmrc` pointers, `NODE_OPTIONS`, caller-supplied Mecord Connect relay variables, and other ambient credentials are not inherited by the long-running runtime. The public package pins its relay/result authority to `operator.splcart.in`; callers cannot redirect a paired device by setting environment variables.
34
+
35
+ The runtime payload is produced from the canonical repository sources at publication time, and includes the native DPAPI, UI Automation, and Windows path-lease helpers compiled in the Windows release workflow. The payload manifest records the exact Git commit and hashes every shipped file.
36
+
37
+ ## Support and policies
38
+
39
+ - Support: https://operator.splcart.in/support
40
+ - Privacy: https://operator.splcart.in/privacy
41
+ - Terms: https://operator.splcart.in/terms
42
+ - License: proprietary Mecord Connect Runtime License in `LICENSE`
43
+ - Dual-use disclosure: `DISCLOSURE`
44
+ - Issues: https://github.com/sampathkumar-co/Operator-runtime/issues
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env node
2
+ import { main } from '../src/cli.mjs';
3
+
4
+ try {
5
+ await main(process.argv.slice(2));
6
+ } catch (error) {
7
+ const message = error instanceof Error ? error.message : String(error);
8
+ console.error(`[mecord-connect] ${message}`);
9
+ process.exitCode = 1;
10
+ }
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "mecord-connect",
3
+ "version": "1.0.0",
4
+ "description": "Secure one-command Mecord Connect runtime for connecting ChatGPT to a user-authorized Windows computer.",
5
+ "type": "module",
6
+ "bin": {
7
+ "mecord-connect": "bin/operator.mjs"
8
+ },
9
+ "files": [
10
+ "bin",
11
+ "src",
12
+ "runtime",
13
+ "README.md",
14
+ "DISCLOSURE",
15
+ "LICENSE"
16
+ ],
17
+ "engines": {
18
+ "node": ">=22.14.0 <23 || >=24.0.0 <25 || >=26.0.0 <27"
19
+ },
20
+ "os": [
21
+ "win32"
22
+ ],
23
+ "cpu": [
24
+ "x64"
25
+ ],
26
+ "publishConfig": {
27
+ "access": "public"
28
+ },
29
+ "license": "SEE LICENSE IN LICENSE",
30
+ "scripts": {
31
+ "build:runtime": "node ./scripts/build-runtime-payload.mjs",
32
+ "prepublishOnly": "node ./src/publish-check.mjs"
33
+ },
34
+ "repository": {
35
+ "type": "git",
36
+ "url": "git+https://github.com/sampathkumar-co/Operator-runtime.git"
37
+ },
38
+ "homepage": "https://operator.splcart.in",
39
+ "keywords": [
40
+ "mecord-connect",
41
+ "chatgpt",
42
+ "mcp",
43
+ "windows",
44
+ "automation",
45
+ "developer-tools"
46
+ ],
47
+ "bugs": {
48
+ "url": "https://github.com/sampathkumar-co/Operator-runtime/issues",
49
+ "email": "support@splcart.in"
50
+ },
51
+ "contentPolicy": {
52
+ "class": "dual-use"
53
+ },
54
+ "author": {
55
+ "name": "Kinthala Samuel Sampath Kumar",
56
+ "email": "support@splcart.in"
57
+ }
58
+ }
@@ -0,0 +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
+ }