codex-chatgpt-control 0.5.1-alpha.2 → 0.5.1-alpha.4

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 (231) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/README.md +43 -0
  3. package/contracts/v1/fixtures/backend-capabilities.json +40 -1
  4. package/contracts/v1/fixtures/backend-compatibility.json +21 -0
  5. package/contracts/v1/fixtures/backend-version.json +6 -1
  6. package/contracts/v1/fixtures/download-blocked-by-browser.json +22 -0
  7. package/contracts/v1/fixtures/download-receipt-failed.json +22 -0
  8. package/contracts/v1/fixtures/download-receipt-timeout.json +22 -0
  9. package/contracts/v1/fixtures/journal-rpc-indeterminate.json +32 -0
  10. package/contracts/v1/fixtures/journal-rpc-unsupported-platform.json +32 -0
  11. package/contracts/v1/fixtures/journal-runtime-unavailable.json +32 -0
  12. package/contracts/v1/fixtures/operation-action-prepared-event.json +42 -0
  13. package/contracts/v1/fixtures/operation-action.json +13 -0
  14. package/contracts/v1/fixtures/operation-artifact-receipt.json +14 -0
  15. package/contracts/v1/fixtures/operation-artifact-transfer-intent-event.json +24 -0
  16. package/contracts/v1/fixtures/operation-artifact-transfer-receipt-event.json +26 -0
  17. package/contracts/v1/fixtures/operation-artifact-transfer-state.json +178 -0
  18. package/contracts/v1/fixtures/operation-blocker.json +10 -0
  19. package/contracts/v1/fixtures/operation-collect-request.json +17 -0
  20. package/contracts/v1/fixtures/operation-collect-result.json +42 -0
  21. package/contracts/v1/fixtures/operation-control-receipt.json +13 -0
  22. package/contracts/v1/fixtures/operation-control-request.json +18 -0
  23. package/contracts/v1/fixtures/operation-control-result.json +31 -0
  24. package/contracts/v1/fixtures/operation-event.json +18 -0
  25. package/contracts/v1/fixtures/operation-handle.json +10 -0
  26. package/contracts/v1/fixtures/operation-inspect-request.json +13 -0
  27. package/contracts/v1/fixtures/operation-inspect-result.json +205 -0
  28. package/contracts/v1/fixtures/operation-ownership-baseline-event.json +34 -0
  29. package/contracts/v1/fixtures/operation-receipt.json +32 -0
  30. package/contracts/v1/fixtures/operation-recovery-decision.json +7 -0
  31. package/contracts/v1/fixtures/operation-recovery-observation.json +12 -0
  32. package/contracts/v1/fixtures/operation-request.json +33 -0
  33. package/contracts/v1/fixtures/operation-state.json +144 -0
  34. package/contracts/v1/fixtures/operation-submission-witness-event.json +20 -0
  35. package/contracts/v1/fixtures/operation-submission-witness.json +11 -0
  36. package/contracts/v1/fixtures/operation-submit-result.json +16 -0
  37. package/contracts/v1/fixtures/operation-target-established-event.json +21 -0
  38. package/contracts/v1/manifest.json +185 -1
  39. package/contracts/v1/parity-suite.json +106 -8
  40. package/contracts/v1/schemas/backend-compatibility.schema.json +65 -0
  41. package/contracts/v1/schemas/backend-request.schema.json +8 -2
  42. package/contracts/v1/schemas/capabilities.schema.json +81 -1
  43. package/contracts/v1/schemas/manifest.schema.json +57 -0
  44. package/contracts/v1/schemas/operation-action.schema.json +155 -0
  45. package/contracts/v1/schemas/operation-artifact-receipt.schema.json +89 -0
  46. package/contracts/v1/schemas/operation-blocker.schema.json +70 -0
  47. package/contracts/v1/schemas/operation-collect-request.schema.json +46 -0
  48. package/contracts/v1/schemas/operation-collect-result.schema.json +74 -0
  49. package/contracts/v1/schemas/operation-control-receipt.schema.json +108 -0
  50. package/contracts/v1/schemas/operation-control-request.schema.json +111 -0
  51. package/contracts/v1/schemas/operation-control-result.schema.json +112 -0
  52. package/contracts/v1/schemas/operation-event.schema.json +756 -0
  53. package/contracts/v1/schemas/operation-handle.schema.json +55 -0
  54. package/contracts/v1/schemas/operation-inspect-request.schema.json +42 -0
  55. package/contracts/v1/schemas/operation-inspect-result.schema.json +1662 -0
  56. package/contracts/v1/schemas/operation-receipt.schema.json +153 -0
  57. package/contracts/v1/schemas/operation-recovery.schema.json +314 -0
  58. package/contracts/v1/schemas/operation-request.schema.json +158 -0
  59. package/contracts/v1/schemas/operation-state.schema.json +691 -0
  60. package/contracts/v1/schemas/operation-submission-witness.schema.json +48 -0
  61. package/contracts/v1/schemas/operation-submit-result.schema.json +94 -0
  62. package/contracts/v1/surface-drift-policy.json +50 -0
  63. package/contracts/v1/vectors/operation-request-digest-v1.json +43 -0
  64. package/dist/codex-chatgpt-control-backend.mjs +45882 -9361
  65. package/dist/codex-chatgpt-control-journal.mjs +5547 -0
  66. package/dist/codex-chatgpt-control-live-smoke.bundle.mjs +51677 -0
  67. package/dist/codex-chatgpt-control-release-canary.bundle.mjs +52104 -0
  68. package/dist/codex-chatgpt-control.bundle.mjs +48392 -10260
  69. package/dist/src/backend/client.d.ts +103 -2
  70. package/dist/src/backend/client.js +1436 -97
  71. package/dist/src/backend/compatibility.d.ts +12 -0
  72. package/dist/src/backend/compatibility.js +208 -0
  73. package/dist/src/backend/protocol.d.ts +84 -1
  74. package/dist/src/backend/protocol.js +31 -5
  75. package/dist/src/backend/runtime-identity.d.ts +10 -0
  76. package/dist/src/backend/runtime-identity.js +141 -0
  77. package/dist/src/backend/session.d.ts +10 -2
  78. package/dist/src/backend/session.js +478 -7
  79. package/dist/src/backend/stdio-server.d.ts +4 -1
  80. package/dist/src/backend/stdio-server.js +240 -28
  81. package/dist/src/browser/active-composer-file-input.d.ts +9 -0
  82. package/dist/src/browser/active-composer-file-input.js +33 -0
  83. package/dist/src/browser/attach.d.ts +4 -1
  84. package/dist/src/browser/attach.js +136 -28
  85. package/dist/src/browser/chatgpt-url.d.ts +2 -0
  86. package/dist/src/browser/chatgpt-url.js +9 -0
  87. package/dist/src/browser/downloads.d.ts +17 -1
  88. package/dist/src/browser/downloads.js +150 -28
  89. package/dist/src/browser/page-state.js +6 -0
  90. package/dist/src/client.d.ts +43 -0
  91. package/dist/src/client.js +1279 -59
  92. package/dist/src/commands/artifacts.js +6 -4
  93. package/dist/src/commands/chat-popover.d.ts +37 -0
  94. package/dist/src/commands/chat-popover.js +537 -0
  95. package/dist/src/commands/configuration.d.ts +5 -0
  96. package/dist/src/commands/configuration.js +134 -21
  97. package/dist/src/commands/context.js +10 -3
  98. package/dist/src/commands/doctor.d.ts +1 -1
  99. package/dist/src/commands/doctor.js +27 -4
  100. package/dist/src/commands/experience.d.ts +5 -0
  101. package/dist/src/commands/experience.js +104 -21
  102. package/dist/src/commands/files.js +14 -33
  103. package/dist/src/commands/modes.js +85 -21
  104. package/dist/src/commands/power-discovery.d.ts +130 -0
  105. package/dist/src/commands/power-discovery.js +669 -0
  106. package/dist/src/commands/project-sources.js +325 -31
  107. package/dist/src/commands/sequence.js +59 -32
  108. package/dist/src/commands/session.js +19 -6
  109. package/dist/src/commands/work.js +31 -0
  110. package/dist/src/dom/composer-text.d.ts +6 -0
  111. package/dist/src/dom/composer-text.js +85 -0
  112. package/dist/src/errors.d.ts +9 -0
  113. package/dist/src/errors.js +22 -0
  114. package/dist/src/index.d.ts +6 -0
  115. package/dist/src/index.js +9 -0
  116. package/dist/src/operations/artifact-output.d.ts +69 -0
  117. package/dist/src/operations/artifact-output.js +1748 -0
  118. package/dist/src/operations/artifact-stream.d.ts +19 -0
  119. package/dist/src/operations/artifact-stream.js +35 -0
  120. package/dist/src/operations/artifact-transfer.d.ts +149 -0
  121. package/dist/src/operations/artifact-transfer.js +1312 -0
  122. package/dist/src/operations/browser-adapter.d.ts +167 -0
  123. package/dist/src/operations/browser-adapter.js +2103 -0
  124. package/dist/src/operations/browser-observation.d.ts +110 -0
  125. package/dist/src/operations/browser-observation.js +1256 -0
  126. package/dist/src/operations/browser-target.d.ts +105 -0
  127. package/dist/src/operations/browser-target.js +531 -0
  128. package/dist/src/operations/canonical.d.ts +4 -0
  129. package/dist/src/operations/canonical.js +412 -0
  130. package/dist/src/operations/chatgpt-runtime.d.ts +72 -0
  131. package/dist/src/operations/chatgpt-runtime.js +1232 -0
  132. package/dist/src/operations/client.d.ts +138 -0
  133. package/dist/src/operations/client.js +1145 -0
  134. package/dist/src/operations/collector.d.ts +195 -0
  135. package/dist/src/operations/collector.js +1039 -0
  136. package/dist/src/operations/configuration-routing.d.ts +4 -0
  137. package/dist/src/operations/configuration-routing.js +14 -0
  138. package/dist/src/operations/control.d.ts +372 -0
  139. package/dist/src/operations/control.js +1498 -0
  140. package/dist/src/operations/file-identity.d.ts +33 -0
  141. package/dist/src/operations/file-identity.js +153 -0
  142. package/dist/src/operations/handle.d.ts +19 -0
  143. package/dist/src/operations/handle.js +727 -0
  144. package/dist/src/operations/index.d.ts +25 -0
  145. package/dist/src/operations/index.js +25 -0
  146. package/dist/src/operations/journal-authority.d.ts +12 -0
  147. package/dist/src/operations/journal-authority.js +1 -0
  148. package/dist/src/operations/journal-rpc-client.d.ts +12 -0
  149. package/dist/src/operations/journal-rpc-client.js +225 -0
  150. package/dist/src/operations/journal-rpc-protocol.d.ts +32 -0
  151. package/dist/src/operations/journal-rpc-protocol.js +214 -0
  152. package/dist/src/operations/journal-rpc-server.d.ts +15 -0
  153. package/dist/src/operations/journal-rpc-server.js +272 -0
  154. package/dist/src/operations/journal.d.ts +143 -0
  155. package/dist/src/operations/journal.js +1957 -0
  156. package/dist/src/operations/production-attachments.d.ts +118 -0
  157. package/dist/src/operations/production-attachments.js +1035 -0
  158. package/dist/src/operations/production-chatgpt-artifacts.d.ts +74 -0
  159. package/dist/src/operations/production-chatgpt-artifacts.js +1146 -0
  160. package/dist/src/operations/production-chatgpt-attachments.d.ts +80 -0
  161. package/dist/src/operations/production-chatgpt-attachments.js +1639 -0
  162. package/dist/src/operations/production-configuration.d.ts +42 -0
  163. package/dist/src/operations/production-configuration.js +1544 -0
  164. package/dist/src/operations/production-primitives.d.ts +53 -0
  165. package/dist/src/operations/production-primitives.js +856 -0
  166. package/dist/src/operations/production-work-steer.d.ts +203 -0
  167. package/dist/src/operations/production-work-steer.js +1218 -0
  168. package/dist/src/operations/recovery.d.ts +80 -0
  169. package/dist/src/operations/recovery.js +174 -0
  170. package/dist/src/operations/runtime-adapter.d.ts +126 -0
  171. package/dist/src/operations/runtime-adapter.js +802 -0
  172. package/dist/src/operations/send-once.d.ts +224 -0
  173. package/dist/src/operations/send-once.js +1081 -0
  174. package/dist/src/operations/service.d.ts +286 -0
  175. package/dist/src/operations/service.js +2831 -0
  176. package/dist/src/operations/staging.d.ts +136 -0
  177. package/dist/src/operations/staging.js +630 -0
  178. package/dist/src/operations/state-machine.d.ts +29 -0
  179. package/dist/src/operations/state-machine.js +2047 -0
  180. package/dist/src/operations/submission.d.ts +423 -0
  181. package/dist/src/operations/submission.js +1676 -0
  182. package/dist/src/operations/transactional-chat-power.d.ts +17 -0
  183. package/dist/src/operations/transactional-chat-power.js +212 -0
  184. package/dist/src/operations/turn-ownership.d.ts +202 -0
  185. package/dist/src/operations/turn-ownership.js +700 -0
  186. package/dist/src/operations/types.d.ts +454 -0
  187. package/dist/src/operations/types.js +20 -0
  188. package/dist/src/operations/wire-requests.d.ts +21 -0
  189. package/dist/src/operations/wire-requests.js +396 -0
  190. package/dist/src/operations/wire-results.d.ts +97 -0
  191. package/dist/src/operations/wire-results.js +818 -0
  192. package/dist/src/runner/responses.d.ts +3 -1
  193. package/dist/src/runner/responses.js +19 -1
  194. package/dist/src/runner/result.js +227 -55
  195. package/dist/src/runner/types.d.ts +12 -0
  196. package/dist/src/runtime/command-routing.d.ts +149 -0
  197. package/dist/src/runtime/command-routing.js +431 -0
  198. package/dist/src/runtime/coordinated-browser.d.ts +24 -0
  199. package/dist/src/runtime/coordinated-browser.js +315 -0
  200. package/dist/src/runtime/coordinated-page.d.ts +41 -0
  201. package/dist/src/runtime/coordinated-page.js +651 -0
  202. package/dist/src/runtime/operation-context.d.ts +142 -0
  203. package/dist/src/runtime/operation-context.js +410 -0
  204. package/dist/src/runtime/runtime-session.d.ts +95 -0
  205. package/dist/src/runtime/runtime-session.js +314 -0
  206. package/dist/src/runtime/tab-coordinator.d.ts +200 -0
  207. package/dist/src/runtime/tab-coordinator.js +1200 -0
  208. package/dist/src/runtime/value-boundaries.d.ts +18 -0
  209. package/dist/src/runtime/value-boundaries.js +49 -0
  210. package/dist/src/safety/blockers.js +8 -1
  211. package/dist/src/safety/untrusted-output.js +2 -1
  212. package/dist/src/scripts/backend-server.js +4 -1
  213. package/dist/src/scripts/capture-surface-profile.js +7 -2
  214. package/dist/src/scripts/journal-server.d.ts +2 -0
  215. package/dist/src/scripts/journal-server.js +37 -0
  216. package/dist/src/scripts/live-smoke/harness.js +79 -7
  217. package/dist/src/scripts/live-smoke/scenarios.d.ts +42 -1
  218. package/dist/src/scripts/live-smoke/scenarios.js +147 -30
  219. package/dist/src/scripts/live-smoke/transactional.d.ts +17 -0
  220. package/dist/src/scripts/live-smoke/transactional.js +254 -0
  221. package/dist/src/scripts/live-smoke/types.d.ts +3 -0
  222. package/dist/src/scripts/release-canary-module.js +17 -4
  223. package/dist/src/types.d.ts +38 -1
  224. package/package.json +8 -3
  225. package/references/2026-08-16-transactional-operations.md +459 -0
  226. package/references/2026-09-06-journal-service.md +140 -0
  227. package/references/agents-runner.md +34 -0
  228. package/references/backend-protocol.md +176 -0
  229. package/references/python-parity.md +137 -0
  230. package/references/responses-adapter.md +19 -0
  231. package/references/streaming.md +6 -0
@@ -0,0 +1,272 @@
1
+ import { randomBytes, randomUUID } from "node:crypto";
2
+ import { mkdir, opendir, unlink } from "node:fs/promises";
3
+ import { isAbsolute, join, resolve } from "node:path";
4
+ import { OperationJournal } from "./journal.js";
5
+ import { assertOperationEventShape, assertOperationId, assertOperationStateShape } from "./state-machine.js";
6
+ import { JOURNAL_RPC_DESCRIPTOR_VERSION, JOURNAL_RPC_DIGEST, JOURNAL_RPC_METHODS, JOURNAL_RPC_UUID, assertJournalRpcPlatform, assertPrivateDirectory, exactRecord, isMissing, openRpcValue, publishPrivateFile, readPrivateFile, rpcError, rpcSleep, rpcTimeout, sealRpcValue } from "./journal-rpc-protocol.js";
7
+ const SAFE_JOURNAL_CODES = new Set([
8
+ "operation_receipt_expired", "operation_compacted", "creation_event_required", "duplicate_operation_created", "journal_corrupt",
9
+ "revision_conflict", "operation_not_found", "operation_binding_mismatch", "operation_request_mismatch", "operation_tombstoned",
10
+ "journal_quota_exceeded", "journal_lock_timeout", "lock_timeout", "journal_snapshot_corrupt",
11
+ "journal_record_corrupt", "journal_state_corrupt", "journal_quota_counter_corrupt", "journal_scan_limit",
12
+ "operation_handle_mismatch", "operation_handle_ahead", "operation_handle_state_mismatch", "operation_handle_target_mismatch",
13
+ "invalid_operation_handle", "invalid_operation_request", "invalid_expected_revision", "invalid_operation_event",
14
+ "invalid_evidence_domain", "invalid_evidence_material", "evidence_material_too_large"
15
+ ]);
16
+ // Only these native failures prove the requested event was not persisted.
17
+ // Errors from quota repair, sync, close or lock release may follow a write.
18
+ const PREWRITE_REJECTION_CODES = new Set([
19
+ "revision_conflict", "operation_request_mismatch", "operation_binding_mismatch",
20
+ "invalid_expected_revision", "invalid_operation_event", "creation_event_required",
21
+ "duplicate_operation_created", "operation_receipt_expired", "operation_compacted",
22
+ "journal_quota_exceeded", "journal_lock_timeout", "journal_rpc_protocol_error"
23
+ ]);
24
+ const LIVE_ID_TTL_MS = 60_000;
25
+ const MAX_RECENT_IDS = 4096;
26
+ export async function startJournalRpcServer(options) {
27
+ // Refuse unsupported filesystem authority before opening the journal,
28
+ // creating session directories or generating any connection secret.
29
+ assertJournalRpcPlatform();
30
+ const directory = options.directory;
31
+ if (!isAbsolute(directory) || resolve(directory) !== directory || !isAbsolute(options.stateRoot)) {
32
+ throw rpcError("journal_rpc_protocol_error");
33
+ }
34
+ const pollMs = boundedInteger(options.pollIntervalMs ?? 10, 1, 1000);
35
+ const maxConcurrent = boundedInteger(options.maxConcurrent ?? 4, 1, 8);
36
+ const maxEntries = boundedInteger(options.maxPendingEntries ?? 128, 1, 128);
37
+ const callTimeoutMs = rpcTimeout(options.callTimeoutMs);
38
+ const maxRecentRequests = boundedInteger(options.maxRecentRequests ?? MAX_RECENT_IDS, 1, MAX_RECENT_IDS);
39
+ const journal = await OperationJournal.open({ stateRoot: options.stateRoot });
40
+ await mkdir(directory, { mode: 0o700 });
41
+ await assertPrivateDirectory(directory);
42
+ const requestDirectory = join(directory, "requests");
43
+ const responseDirectory = join(directory, "responses");
44
+ await mkdir(requestDirectory, { mode: 0o700 });
45
+ await mkdir(responseDirectory, { mode: 0o700 });
46
+ const descriptor = {
47
+ schemaVersion: JOURNAL_RPC_DESCRIPTOR_VERSION,
48
+ transport: "file", directory, instanceId: randomUUID(), token: randomBytes(32).toString("base64url")
49
+ };
50
+ const descriptorPath = join(directory, "connection.json");
51
+ await publishPrivateFile(directory, "connection.json", JSON.stringify(descriptor));
52
+ const inFlight = new Set();
53
+ const recent = new Map();
54
+ let closing = false;
55
+ let closePromise;
56
+ const loop = (async () => {
57
+ while (!closing) {
58
+ try {
59
+ await assertPrivateDirectory(directory);
60
+ await assertPrivateDirectory(requestDirectory);
61
+ await assertPrivateDirectory(responseDirectory);
62
+ const now = Date.now();
63
+ for (const [id, expires] of recent) {
64
+ if (expires > now)
65
+ break;
66
+ recent.delete(id);
67
+ await unlink(join(responseDirectory, `${id}.response.json`)).catch(() => undefined);
68
+ await unlink(join(requestDirectory, `${id}.request.json`)).catch(() => undefined);
69
+ }
70
+ if (inFlight.size < maxConcurrent) {
71
+ const names = await boundedEntries(requestDirectory, maxEntries);
72
+ for (const name of names) {
73
+ if (closing || inFlight.size >= maxConcurrent)
74
+ break;
75
+ const id = requestId(name);
76
+ if (id === undefined || recent.has(id))
77
+ continue;
78
+ let payload;
79
+ try {
80
+ payload = openRpcValue(descriptor, id, "request", await readPrivateFile(join(requestDirectory, name)));
81
+ }
82
+ catch (error) {
83
+ if (!isMissing(error)) {
84
+ // Invalid/partial files never authorize an operation. They are
85
+ // retained for the request's owner to remove; no private data is logged.
86
+ }
87
+ continue;
88
+ }
89
+ if (recent.size >= maxRecentRequests)
90
+ break;
91
+ try {
92
+ exactRecord(payload, ["method", "args", "issuedAt", "expiresAt"]);
93
+ const issued = payload.issuedAt;
94
+ const expires = payload.expiresAt;
95
+ if (!Number.isSafeInteger(issued) || !Number.isSafeInteger(expires)
96
+ || issued > Date.now() + 1000 || expires <= Date.now()
97
+ || expires <= issued || expires - issued > 30_000
98
+ || !JOURNAL_RPC_METHODS.has(payload.method) || !Array.isArray(payload.args)) {
99
+ throw rpcError("journal_rpc_protocol_error");
100
+ }
101
+ recent.set(id, Date.now() + LIVE_ID_TTL_MS);
102
+ await unlink(join(requestDirectory, name));
103
+ const method = payload.method;
104
+ const args = payload.args;
105
+ const deadline = Math.min(expires, Date.now() + callTimeoutMs);
106
+ let task;
107
+ task = execute(method, args, id, deadline).finally(() => { inFlight.delete(task); });
108
+ inFlight.add(task);
109
+ }
110
+ catch {
111
+ // Authenticated but malformed requests still cannot mutate.
112
+ recent.set(id, Date.now() + LIVE_ID_TTL_MS);
113
+ await unlink(join(requestDirectory, name)).catch(() => undefined);
114
+ await respond(id, { ok: false, error: { code: "journal_rpc_protocol_error" } });
115
+ }
116
+ }
117
+ }
118
+ }
119
+ catch {
120
+ // Missing/replaced/oversized mailbox evidence is unavailable, never permission to truncate a scan.
121
+ }
122
+ if (!closing)
123
+ await rpcSleep(pollMs);
124
+ }
125
+ })();
126
+ async function respond(id, value) {
127
+ try {
128
+ await publishPrivateFile(responseDirectory, `${id}.response.json`, sealRpcValue(descriptor, id, "response", value));
129
+ }
130
+ catch {
131
+ // A lost response is explicitly indeterminate for mutation calls. Never overwrite another response.
132
+ }
133
+ }
134
+ async function execute(method, args, id, deadline) {
135
+ if (Date.now() >= deadline) {
136
+ await respond(id, { ok: false, error: { code: "journal_rpc_unavailable" } });
137
+ return;
138
+ }
139
+ let responded = false;
140
+ const timer = setTimeout(() => {
141
+ if (responded)
142
+ return;
143
+ responded = true;
144
+ void respond(id, { ok: false, error: { code: mutation(method)
145
+ ? "journal_rpc_outcome_indeterminate" : "journal_rpc_unavailable" } });
146
+ }, Math.max(1, deadline - Date.now()));
147
+ try {
148
+ const result = await invokeJournal(journal, method, args);
149
+ if (!responded) {
150
+ responded = true;
151
+ await respond(id, { ok: true, result });
152
+ }
153
+ }
154
+ catch (error) {
155
+ if (!responded) {
156
+ responded = true;
157
+ const observed = safeJournalCode(error);
158
+ const code = mutation(method) && !PREWRITE_REJECTION_CODES.has(observed)
159
+ ? "journal_rpc_outcome_indeterminate" : observed;
160
+ await respond(id, { ok: false, error: { code } });
161
+ }
162
+ }
163
+ finally {
164
+ clearTimeout(timer);
165
+ }
166
+ }
167
+ return {
168
+ descriptorPath,
169
+ close: () => closePromise ??= (async () => {
170
+ closing = true;
171
+ // Invalidates discovery while allowing admitted work to release real process locks.
172
+ await unlink(descriptorPath).catch(() => undefined);
173
+ await loop;
174
+ let timer;
175
+ try {
176
+ const settled = await Promise.race([
177
+ Promise.allSettled([...inFlight]).then(() => true),
178
+ new Promise(resolve => { timer = setTimeout(() => resolve(false), callTimeoutMs); })
179
+ ]);
180
+ if (!settled)
181
+ throw rpcError("journal_rpc_outcome_indeterminate");
182
+ }
183
+ finally {
184
+ if (timer !== undefined)
185
+ clearTimeout(timer);
186
+ }
187
+ })()
188
+ };
189
+ }
190
+ async function invokeJournal(journal, method, args) {
191
+ switch (method) {
192
+ case "create":
193
+ requireCount(args, 1);
194
+ assertOperationEventShape(args[0]);
195
+ if (args[0].type !== "operation_created")
196
+ throw rpcError("journal_rpc_protocol_error");
197
+ return publicLoaded(await journal.create(args[0]));
198
+ case "append":
199
+ requireCount(args, 3);
200
+ assertOperationId(args[0]);
201
+ assertOperationEventShape(args[2]);
202
+ if (!Number.isSafeInteger(args[1]) || args[1] < 0)
203
+ throw rpcError("journal_rpc_protocol_error");
204
+ return publicLoaded(await journal.append(args[0], args[1], args[2]));
205
+ case "load":
206
+ if (args.length !== 1 && args.length !== 2)
207
+ throw rpcError("journal_rpc_protocol_error");
208
+ assertOperationId(args[0]);
209
+ if (args[1] !== undefined && (typeof args[1] !== "string" || !JOURNAL_RPC_DIGEST.test(args[1])))
210
+ throw rpcError("journal_rpc_protocol_error");
211
+ return publicLoaded(await journal.load(args[0], args[1]));
212
+ case "handleFromState":
213
+ requireCount(args, 1);
214
+ assertOperationStateShape(args[0]);
215
+ return journal.handleFromState(args[0]);
216
+ case "validateHandle":
217
+ requireCount(args, 2);
218
+ assertOperationStateShape(args[1]);
219
+ return journal.validateHandle(args[0], args[1]);
220
+ case "submitRequestDigest":
221
+ requireCount(args, 2);
222
+ return journal.submitRequestDigest(...args);
223
+ case "controlRequestDigest":
224
+ requireCount(args, 1);
225
+ return journal.controlRequestDigest(...args);
226
+ case "evidenceDigest":
227
+ requireCount(args, 2);
228
+ if (typeof args[0] !== "string")
229
+ throw rpcError("journal_rpc_protocol_error");
230
+ return journal.evidenceDigest(args[0], args[1]);
231
+ }
232
+ }
233
+ function requireCount(args, count) {
234
+ if (args.length !== count)
235
+ throw rpcError("journal_rpc_protocol_error");
236
+ }
237
+ function safeJournalCode(error) {
238
+ if (typeof error === "object" && error !== null && "code" in error && typeof error.code === "string"
239
+ && (SAFE_JOURNAL_CODES.has(error.code) || error.code === "journal_rpc_protocol_error"))
240
+ return error.code;
241
+ return "journal_rpc_request_rejected";
242
+ }
243
+ function mutation(method) { return method === "create" || method === "append"; }
244
+ function boundedInteger(value, min, max) {
245
+ if (!Number.isSafeInteger(value) || value < min || value > max)
246
+ throw rpcError("journal_rpc_limit_exceeded");
247
+ return value;
248
+ }
249
+ function requestId(name) {
250
+ const id = name.replace(/\.request\.json$/u, "");
251
+ return `${id}.request.json` === name && JOURNAL_RPC_UUID.test(id) ? id : undefined;
252
+ }
253
+ async function boundedEntries(directory, max) {
254
+ const names = [];
255
+ const entries = await opendir(directory);
256
+ let count = 0;
257
+ for await (const entry of entries) {
258
+ count += 1;
259
+ if (count > max)
260
+ throw rpcError("journal_rpc_limit_exceeded");
261
+ if (!entry.isFile() || entry.name.startsWith(".pending-"))
262
+ continue;
263
+ names.push(entry.name);
264
+ }
265
+ return names;
266
+ }
267
+ function publicLoaded(value) {
268
+ // Journal parsing metadata is private; transmit only the declared authority result.
269
+ return { state: value.state, envelopes: value.envelopes, committedBytes: value.committedBytes,
270
+ partialTailBytes: value.partialTailBytes,
271
+ ...(value.lastEventDigest === undefined ? {} : { lastEventDigest: value.lastEventDigest }) };
272
+ }
@@ -0,0 +1,143 @@
1
+ import { validateOperationHandle } from "./handle.js";
2
+ import type { OperationFileManifestEntryV1 } from "./file-identity.js";
3
+ import { type OperationJournalSnapshotV1, type OperationEventEnvelopeV1, type OperationEventV1, type OperationControlRequestV1, type OperationHandleV1, type OperationSubmitRequestV1, type OperationStateV1 } from "./types.js";
4
+ export type JournalFaultPoint = "after_lock_acquired" | "after_partial_tail_truncated" | "after_record_written" | "after_record_synced" | "after_snapshot_synced" | "after_terminal_synced" | "after_tombstone_synced" | "after_log_deleted";
5
+ /**
6
+ * Injectable wall-clock and wait boundary used by lock acquisition. The
7
+ * default implementation is backed by the host clock and timers; tests may
8
+ * provide a deterministic implementation without changing journal semantics.
9
+ */
10
+ export type OperationJournalClock = Readonly<{
11
+ now: () => number;
12
+ sleep: (milliseconds: number) => void | Promise<void>;
13
+ }>;
14
+ /**
15
+ * Injectable entropy boundary for journal keys, lock tokens, and temporary
16
+ * filenames. Defaults always use the OS CSPRNG and crypto UUID generator.
17
+ */
18
+ export type OperationJournalEntropy = Readonly<{
19
+ randomBytes: (size: number) => Uint8Array;
20
+ randomUUID: () => string;
21
+ }>;
22
+ export type OperationJournalOptions = {
23
+ stateRoot?: string;
24
+ maxStateBytes?: number;
25
+ lockTimeoutMs?: number;
26
+ faultInjector?: (point: JournalFaultPoint) => void | Promise<void>;
27
+ clock?: OperationJournalClock;
28
+ entropy?: OperationJournalEntropy;
29
+ };
30
+ export type LoadedOperationJournalV1 = {
31
+ state: OperationStateV1;
32
+ envelopes: OperationEventEnvelopeV1[];
33
+ committedBytes: number;
34
+ partialTailBytes: number;
35
+ /** The authenticated final event digest. Compacted records have no envelopes. */
36
+ lastEventDigest?: string;
37
+ };
38
+ export type OperationCompactionResultV1 = {
39
+ status: "compacted" | "already_compacted";
40
+ operationId: string;
41
+ requestDigest: string;
42
+ revision: number;
43
+ lastEventDigest: string;
44
+ deletedLogBytes: number;
45
+ };
46
+ export type OperationSnapshotResultV1 = OperationJournalSnapshotV1;
47
+ export type OperationReceiptPruneResultV1 = {
48
+ status: "pruned" | "already_pruned";
49
+ operationId: string;
50
+ requestDigest: string;
51
+ deletedTerminalBytes: number;
52
+ deletedSnapshotBytes: number;
53
+ };
54
+ export type OperationTombstonePurgeResultV1 = {
55
+ operationId: string;
56
+ requestDigest: string;
57
+ deleted: Array<"tombstone" | "terminal" | "snapshot" | "log">;
58
+ deletedBytes: number;
59
+ };
60
+ export declare class OperationJournalError extends Error {
61
+ readonly code: string;
62
+ constructor(code: string, message: string);
63
+ }
64
+ export declare class OperationJournal {
65
+ private readonly key;
66
+ private readonly clock;
67
+ private readonly entropy;
68
+ readonly stateRoot: string;
69
+ readonly maxStateBytes: number;
70
+ readonly lockTimeoutMs: number;
71
+ private constructor();
72
+ private readonly faultInjector;
73
+ /** Compute immutable request identity inside the journal key domain. */
74
+ submitRequestDigest(request: OperationSubmitRequestV1, files: readonly OperationFileManifestEntryV1[]): string;
75
+ /** Compute child control identity inside the same stable key domain. */
76
+ controlRequestDigest(request: OperationControlRequestV1): string;
77
+ /**
78
+ * Derive privacy-preserving evidence in the stable journal key domain.
79
+ * Callers receive only the HMAC, never the state-root key. The bounded,
80
+ * explicit domain keeps unrelated evidence classes cryptographically
81
+ * separated while allowing browser adapters to avoid bare hashes of user
82
+ * content.
83
+ */
84
+ evidenceDigest(domain: string, material: unknown): string;
85
+ /** Issue a locator from durable state without exposing the journal key. */
86
+ handleFromState(state: OperationStateV1): OperationHandleV1;
87
+ /** Reconcile a caller locator with freshly loaded durable state. */
88
+ validateHandle(handle: OperationHandleV1, state: OperationStateV1): ReturnType<typeof validateOperationHandle>;
89
+ static open(options?: OperationJournalOptions): Promise<OperationJournal>;
90
+ create(event: Extract<OperationEventV1, {
91
+ type: "operation_created";
92
+ }>): Promise<LoadedOperationJournalV1>;
93
+ append(operationId: string, expectedRevision: number, event: OperationEventV1): Promise<LoadedOperationJournalV1>;
94
+ load(operationId: string, expectedRequestDigest?: string): Promise<LoadedOperationJournalV1>;
95
+ /**
96
+ * Rebuilds the materialized snapshot cache from authoritative journal state.
97
+ * A snapshot is never used as a substitute for the authenticated log or
98
+ * terminal record, so a damaged cache can always be safely overwritten.
99
+ */
100
+ refreshSnapshot(operationId: string): Promise<OperationSnapshotResultV1>;
101
+ /** Reads and authenticates a snapshot cache without treating it as authority. */
102
+ readSnapshot(operationId: string): Promise<OperationSnapshotResultV1>;
103
+ /**
104
+ * Converts a completed, receipt-bearing log into a durable terminal record.
105
+ * The terminal is fsynced before the historical log is removed. If a crash
106
+ * occurs between those steps, load() authenticates and reconciles both.
107
+ */
108
+ compactCompleted(operationId: string): Promise<OperationCompactionResultV1>;
109
+ /**
110
+ * Explicitly prunes a completed terminal into a minimal authenticated
111
+ * tombstone. Pruning is never automatic and leaves an unmistakable expiry
112
+ * result instead of turning the operation into operation_not_found.
113
+ */
114
+ pruneReceipt(operationId: string): Promise<OperationReceiptPruneResultV1>;
115
+ /**
116
+ * Permanently removes a pruned tombstone and any crash-residue state. This
117
+ * is intentionally a separately named destructive operation and requires an
118
+ * explicit acknowledgement at the call site.
119
+ */
120
+ purgeTombstone(operationId: string, options?: {
121
+ acknowledge?: boolean;
122
+ }): Promise<OperationTombstonePurgeResultV1>;
123
+ private operationLogPath;
124
+ private operationLockPath;
125
+ private quotaLockPath;
126
+ private quotaCounterPath;
127
+ private terminalPath;
128
+ private snapshotPath;
129
+ private tombstonePath;
130
+ private operationStem;
131
+ private loadLocked;
132
+ private assertNoTerminalOrTombstoneForAppend;
133
+ private compactCompletedLocked;
134
+ private ensureQuotaCounter;
135
+ private mutateQuotaTrackedState;
136
+ private removeQuotaTrackedFile;
137
+ private loadCurrentQuotaCounter;
138
+ private rebuildQuotaCounter;
139
+ private assertQuotaForNewOperation;
140
+ private inject;
141
+ }
142
+ export declare const JOURNAL_RUNTIME_UNAVAILABLE_MESSAGE = "Transactional operations require a Node host with process identity, file ownership, and process liveness APIs. This host cannot safely open the operation journal.";
143
+ export declare function defaultOperationStateRoot(): string;