@platless/axom-cli 0.5.9 → 0.6.52

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 (258) hide show
  1. package/README.md +25 -6
  2. package/dist/axom-patch/cli/README.md +129 -12
  3. package/dist/axom-patch/cli/account-limits/generate.py +15 -12
  4. package/dist/axom-patch/cli/account-limits/kimi-proxy.mjs +20 -3
  5. package/dist/axom-patch/cli/claude-account-limits.json +9 -9
  6. package/dist/axom-patch/cli/claude-replay-lifecycle.json +16 -0
  7. package/dist/axom-patch/cli/codex-account-limits.json +11 -11
  8. package/dist/axom-patch/cli/codex-terminal-output.json +16 -0
  9. package/dist/axom-patch/cli/commandcode-account-limits.json +9 -9
  10. package/dist/axom-patch/cli/kimi-quota.mjs +12 -0
  11. package/dist/axom-patch/cli/kimi-subagents.mjs +173 -0
  12. package/dist/axom-patch/cli/nightly-task.md +55 -13
  13. package/dist/axom-patch/cli/probes/claude-replay-lifecycle.mjs +31 -0
  14. package/dist/axom-patch/cli/probes/codex-terminal-output.mjs +33 -0
  15. package/dist/axom-patch/cli/probes/kimi-subagent-subwork.mjs +82 -0
  16. package/dist/axom-patch/cli/probes/zcode-env-account.mjs +81 -0
  17. package/dist/axom-patch/cli/probes/zcode-subagent-subwork.mjs +7 -0
  18. package/dist/axom-patch/cli/qoder/dist/axom-acp-compat.mjs +1687 -0
  19. package/dist/axom-patch/cli/qoder/releases/1.1.75.json +277 -0
  20. package/dist/axom-patch/cli/qoder-background-terminal.md +13 -6
  21. package/dist/axom-patch/cli/qoder-foreground-output.md +14 -8
  22. package/dist/axom-patch/cli/qoder-native-acp.json +227 -0
  23. package/dist/axom-patch/cli/qoder-steering.md +20 -25
  24. package/dist/axom-patch/cli/qoder-subagent-subwork.md +17 -68
  25. package/dist/axom-patch/cli/runtime.json +19 -19
  26. package/dist/axom-patch/cli/zcode/env-account.mjs +13 -0
  27. package/dist/axom-patch/cli/zcode/generate-env-account.py +52 -0
  28. package/dist/axom-patch/cli/zcode-account-limits.json +10 -11
  29. package/dist/axom-patch/cli/zcode-env-account.json +58 -0
  30. package/dist/axom-patch/cli/zcode-models.json +9 -10
  31. package/dist/axom-patch/cli/zcode-subagent-subwork.json +10 -11
  32. package/dist/cli.js +11 -45
  33. package/dist/commands/auth.js +4 -1
  34. package/dist/commands/knowledge.js +47 -0
  35. package/dist/commands/update.js +126 -3
  36. package/dist/commands/workstation.js +101 -164
  37. package/dist/commands/workstationRegistrations.js +116 -0
  38. package/dist/credentialBroker.js +13 -2
  39. package/dist/credentials.js +16 -0
  40. package/dist/install/claude/commands.js +15 -15
  41. package/dist/keepers/terminal/keeper.mjs +1089 -673
  42. package/dist/keepers/terminal/manifest.json +4 -4
  43. package/dist/mcp.js +1 -1
  44. package/dist/native/darwin-x64/axom-quic +0 -0
  45. package/dist/workstation/acp/acpAgents.js +113 -102
  46. package/dist/workstation/acp/acpClient.js +26 -0
  47. package/dist/workstation/acp/builds/install/npm.js +99 -10
  48. package/dist/workstation/acp/initializeResponse.js +6 -1
  49. package/dist/workstation/acp/managedPatches.js +189 -25
  50. package/dist/workstation/acp/managedRuntime.js +314 -0
  51. package/dist/workstation/acp/outbox.js +12 -5
  52. package/dist/workstation/acp/patchAssets.js +40 -0
  53. package/dist/workstation/acp/permission.js +2 -2
  54. package/dist/workstation/acp/processor/checkpoint.js +26 -34
  55. package/dist/workstation/acp/processor/control.js +7 -80
  56. package/dist/workstation/acp/processor/jsonrpc.js +8 -0
  57. package/dist/workstation/acp/processor/runtime.js +5 -81
  58. package/dist/workstation/acp/processor/session.js +54 -82
  59. package/dist/workstation/acp/processor/state.js +7 -13
  60. package/dist/workstation/acp/processor/step.js +3 -20
  61. package/dist/workstation/acp/processor/test/fixtureLog.js +9 -16
  62. package/dist/workstation/acp/processor/transport/local.js +19 -3
  63. package/dist/workstation/acp/processor/updates.js +156 -105
  64. package/dist/workstation/acp/processorSession.js +20 -173
  65. package/dist/workstation/acp/registry/catalog.js +65 -39
  66. package/dist/workstation/acp/registry/launch.js +16 -1
  67. package/dist/workstation/acp/switching.js +0 -3
  68. package/dist/workstation/acp/test/fakeAgent.js +13 -0
  69. package/dist/workstation/acp/test/keeperWorld.js +32 -3
  70. package/dist/workstation/acp/test/raw/sources.js +16 -130
  71. package/dist/workstation/acp/test/realAdapter.js +1 -47
  72. package/dist/workstation/acp/test/scriptAgent.js +11 -3
  73. package/dist/workstation/agentFlow.js +1 -1
  74. package/dist/workstation/agentSessions.js +114 -166
  75. package/dist/workstation/channels/agent.js +9 -1
  76. package/dist/workstation/channels/computer.js +27 -4
  77. package/dist/workstation/channels/management.js +130 -15
  78. package/dist/workstation/channels/url.js +1 -1
  79. package/dist/workstation/config.js +33 -9
  80. package/dist/workstation/connection/encryptedChannel.js +4 -1
  81. package/dist/workstation/control/bindingStore.js +204 -0
  82. package/dist/workstation/control/commands.js +125 -0
  83. package/dist/workstation/control/daemonIdentity.js +143 -0
  84. package/dist/workstation/control/deviceCommands.js +191 -0
  85. package/dist/workstation/control/deviceProtocol.js +68 -0
  86. package/dist/workstation/control/enrollment.js +106 -0
  87. package/dist/workstation/control/inspect.js +71 -0
  88. package/dist/workstation/control/locks.js +110 -0
  89. package/dist/workstation/control/protocol.js +40 -0
  90. package/dist/workstation/control/restartGuard.js +46 -0
  91. package/dist/workstation/control/rootFormat.js +16 -0
  92. package/dist/workstation/control/runtimeClient.js +152 -0
  93. package/dist/workstation/control/runtimeHost.js +235 -0
  94. package/dist/workstation/control/serverIdentity.js +35 -0
  95. package/dist/workstation/control/updateTransition.js +91 -0
  96. package/dist/workstation/daemon.js +266 -264
  97. package/dist/workstation/daemonctl.js +226 -25
  98. package/dist/workstation/desktopControl/audio.js +99 -0
  99. package/dist/workstation/desktopControl/automationDevice.js +219 -0
  100. package/dist/workstation/desktopControl/automationJson.js +74 -0
  101. package/dist/workstation/desktopControl/automationLocalState.js +23 -0
  102. package/dist/workstation/desktopControl/automationProtocol.js +253 -0
  103. package/dist/workstation/desktopControl/automationSchema.generated.js +1 -0
  104. package/dist/workstation/desktopControl/automationSession.js +81 -0
  105. package/dist/workstation/desktopControl/automationTransport.js +138 -0
  106. package/dist/workstation/desktopControl/capabilities.js +93 -0
  107. package/dist/workstation/desktopControl/captureModes.js +9 -0
  108. package/dist/workstation/desktopControl/captureTargetLabel.js +9 -0
  109. package/dist/workstation/desktopControl/deviceAdmission.js +229 -0
  110. package/dist/workstation/desktopControl/deviceOwnership.js +130 -0
  111. package/dist/workstation/desktopControl/deviceSockets.js +246 -0
  112. package/dist/workstation/desktopControl/helperProcess.js +602 -0
  113. package/dist/workstation/desktopControl/hostGrant.js +65 -0
  114. package/dist/workstation/desktopControl/lobby.js +131 -0
  115. package/dist/workstation/desktopControl/macProvider.js +156 -0
  116. package/dist/workstation/desktopControl/media.js +331 -0
  117. package/dist/workstation/desktopControl/nativeDeviceAdmission.js +207 -0
  118. package/dist/workstation/desktopControl/nativeProvider.js +1044 -0
  119. package/dist/workstation/desktopControl/networkEvidence.js +43 -0
  120. package/dist/workstation/desktopControl/overview.js +58 -0
  121. package/dist/workstation/desktopControl/ownershipProtocol.js +31 -0
  122. package/dist/workstation/desktopControl/protocol.js +311 -0
  123. package/dist/workstation/desktopControl/provider.js +1 -0
  124. package/dist/workstation/desktopControl/quicMedia.js +240 -0
  125. package/dist/workstation/desktopControl/rateController.js +226 -0
  126. package/dist/workstation/desktopControl/supervisor.js +1774 -0
  127. package/dist/workstation/desktopControl/transport.js +27 -0
  128. package/dist/workstation/desktopControl/wallpaper.js +98 -0
  129. package/dist/workstation/exec/policy.js +5 -0
  130. package/dist/workstation/files/client.js +10 -1
  131. package/dist/workstation/files/credentials.js +142 -0
  132. package/dist/workstation/files/host.js +9 -2
  133. package/dist/workstation/files/text.js +42 -8
  134. package/dist/workstation/journal.js +5 -2
  135. package/dist/workstation/keepers/acp/client.js +38 -2
  136. package/dist/workstation/keepers/kit/buildVersion.js +35 -11
  137. package/dist/workstation/keepers/kit/bundle.js +21 -17
  138. package/dist/workstation/keepers/kit/client.js +2 -2
  139. package/dist/workstation/keepers/kit/launcher.js +123 -7
  140. package/dist/workstation/keepers/kit/paths.js +2 -1
  141. package/dist/workstation/keepers/legacy.js +16 -2
  142. package/dist/workstation/keepers/policy.js +5 -34
  143. package/dist/workstation/keepers/terminal/client.js +115 -23
  144. package/dist/workstation/keepers/terminal/exec.js +6 -3
  145. package/dist/workstation/keepers/terminal/frames.js +73 -0
  146. package/dist/workstation/keepers/terminal/generations.js +5 -8
  147. package/dist/workstation/keepers/terminal/gridProtocol.js +16 -0
  148. package/dist/workstation/keepers/terminal/input.js +146 -0
  149. package/dist/workstation/keepers/terminal/keeper.js +22 -8
  150. package/dist/workstation/keepers/terminal/main.js +1 -1
  151. package/dist/workstation/keepers/terminal/metadata.js +138 -0
  152. package/dist/workstation/keepers/terminal/protocol.js +4 -28
  153. package/dist/workstation/keepers/terminal/pty.js +1 -330
  154. package/dist/workstation/keepers/terminal/screen.js +246 -0
  155. package/dist/workstation/keepers/terminal/sessions.js +526 -0
  156. package/dist/workstation/managedInstall.js +57 -0
  157. package/dist/workstation/oauth/deviceAuthorization.js +31 -17
  158. package/dist/workstation/protocol/agent.js +2 -15
  159. package/dist/workstation/protocol/management.js +113 -5
  160. package/dist/workstation/protocol/runtimeDiagnostics.js +15 -0
  161. package/dist/workstation/rd/artifact.js +120 -0
  162. package/dist/workstation/rd/control.js +117 -0
  163. package/dist/workstation/rd/display.js +29 -0
  164. package/dist/workstation/rd/host.js +213 -0
  165. package/dist/workstation/rd/migrationReadiness.js +46 -0
  166. package/dist/workstation/rd/provider.js +256 -0
  167. package/dist/workstation/rd/sessions.js +391 -0
  168. package/dist/workstation/rd/state.js +57 -0
  169. package/dist/workstation/rd/update.js +168 -0
  170. package/dist/workstation/rd/upkeep.js +80 -0
  171. package/dist/workstation/register.js +27 -37
  172. package/dist/workstation/registrations/enrollment.js +250 -0
  173. package/dist/workstation/registrations/enrollmentPending.js +361 -0
  174. package/dist/workstation/registrations/migration.js +356 -0
  175. package/dist/workstation/registrations/model.js +34 -0
  176. package/dist/workstation/registrations/removal.js +222 -0
  177. package/dist/workstation/registrations/store.js +737 -0
  178. package/dist/workstation/runtime/desktopBinding.js +20 -0
  179. package/dist/workstation/runtime/deviceDesktop.js +193 -0
  180. package/dist/workstation/runtime/deviceRuntime.js +568 -0
  181. package/dist/workstation/runtime/deviceServices.js +117 -0
  182. package/dist/workstation/runtime/events.js +20 -0
  183. package/dist/workstation/runtime/ownership.js +100 -0
  184. package/dist/workstation/runtime/registrationRuntime.js +575 -0
  185. package/dist/workstation/runtime/remoteDesktopAdapter.js +186 -0
  186. package/dist/workstation/runtime/removalActivity.js +184 -0
  187. package/dist/workstation/service/installer.js +197 -14
  188. package/dist/workstation/service/launchd.js +2 -0
  189. package/dist/workstation/service/systemd.js +1 -0
  190. package/dist/workstation/storageMetrics.js +118 -0
  191. package/dist/workstation/systemMetrics.js +47 -92
  192. package/dist/workstation/toolchain.js +94 -0
  193. package/dist/workstation/tools/browser.js +28 -4
  194. package/dist/workstation/tools/client.js +8 -27
  195. package/dist/workstation/tools/info.js +6 -13
  196. package/dist/workstation/tools/terminalRouter.js +17 -172
  197. package/dist/workstation/tools/validation.js +55 -1
  198. package/dist/workstation/update/archiveEvidence.js +206 -0
  199. package/dist/workstation/update/artifact.js +2 -1
  200. package/dist/workstation/update/coordinator.js +34 -13
  201. package/dist/workstation/update/deviceCompatibility.js +119 -0
  202. package/dist/workstation/update/deviceCoordinator.js +369 -0
  203. package/dist/workstation/update/deviceHealth.js +104 -0
  204. package/dist/workstation/update/deviceHealthProtocol.js +26 -0
  205. package/dist/workstation/update/deviceRecoveryLifecycle.js +46 -0
  206. package/dist/workstation/update/deviceRollback.js +78 -0
  207. package/dist/workstation/update/globalInstall.js +186 -0
  208. package/dist/workstation/update/legacyBridge.js +414 -0
  209. package/dist/workstation/update/legacyOwner.js +39 -0
  210. package/dist/workstation/update/npmInstaller.js +51 -25
  211. package/dist/workstation/update/privateDurability.js +61 -0
  212. package/dist/workstation/update/rdRollback.js +145 -0
  213. package/dist/workstation/update/recoveryPlan.js +124 -0
  214. package/dist/workstation/update/runUpdate.js +10 -3
  215. package/dist/workstation/update/taskRegistry.js +161 -3
  216. package/dist/workstation/update/watchdog.js +29806 -333
  217. package/dist/workstation/update/watchdogProcess.js +72 -0
  218. package/package.json +11 -7
  219. package/skills/axom/SKILL.md +11 -11
  220. package/skills/axom-definition/SKILL.md +3 -3
  221. package/skills/axom-platform/SKILL.md +3 -3
  222. package/dist/axom-patch/cli/probes/qoder-steering.mjs +0 -89
  223. package/dist/axom-patch/cli/probes/qoder-subagent-origin.mjs +0 -61
  224. package/dist/axom-patch/cli/qoder-steering.json +0 -52
  225. package/dist/axom-patch/cli/qoder-subagent-origin.json +0 -73
  226. package/dist/commands/guide.js +0 -38
  227. package/dist/runtime-deps/qoder-sdk/package-lock.json +0 -1189
  228. package/dist/runtime-deps/qoder-sdk/package.json +0 -8
  229. package/dist/workstation/acp/delegationMcp.js +0 -24
  230. package/dist/workstation/acp/processor/sideChannel.js +0 -80
  231. package/dist/workstation/acp/qoderStartup.js +0 -45
  232. package/dist/workstation/acp/test/raw/qoderCaptures.js +0 -125
  233. package/dist/workstation/qoderAdapter/backgroundProcess.js +0 -118
  234. package/dist/workstation/qoderAdapter/backgroundTerminal.js +0 -193
  235. package/dist/workstation/qoderAdapter/backgroundTerminalFold.js +0 -372
  236. package/dist/workstation/qoderAdapter/callbackContext.js +0 -76
  237. package/dist/workstation/qoderAdapter/completedContinuation.js +0 -139
  238. package/dist/workstation/qoderAdapter/contract.js +0 -78
  239. package/dist/workstation/qoderAdapter/foregroundOutput.js +0 -36
  240. package/dist/workstation/qoderAdapter/history.js +0 -85
  241. package/dist/workstation/qoderAdapter/identityIndex.js +0 -191
  242. package/dist/workstation/qoderAdapter/inputQueue.js +0 -42
  243. package/dist/workstation/qoderAdapter/main.js +0 -95
  244. package/dist/workstation/qoderAdapter/mapper.js +0 -117
  245. package/dist/workstation/qoderAdapter/observerPlugin.js +0 -245
  246. package/dist/workstation/qoderAdapter/observerReceiver.js +0 -187
  247. package/dist/workstation/qoderAdapter/permissions.js +0 -61
  248. package/dist/workstation/qoderAdapter/profile.js +0 -93
  249. package/dist/workstation/qoderAdapter/sdkDriver.js +0 -48
  250. package/dist/workstation/qoderAdapter/sdkRuntime.js +0 -93
  251. package/dist/workstation/qoderAdapter/sdkRuntimeInstall.js +0 -145
  252. package/dist/workstation/qoderAdapter/session.js +0 -319
  253. package/dist/workstation/qoderAdapter/subagentMeta.js +0 -28
  254. package/dist/workstation/qoderAdapter/subwork.js +0 -85
  255. package/dist/workstation/qoderAdapter/taskControl.js +0 -38
  256. package/dist/workstation/qoderAdapter/taskLedger.js +0 -163
  257. package/dist/workstation/qoderAdapter/usage.js +0 -37
  258. /package/dist/workstation/{qoderAdapter/types.js → desktopControl/automationPreparation.js} +0 -0
@@ -0,0 +1,737 @@
1
+ import { constants, promises as fs } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { createHash, randomUUID } from 'node:crypto';
4
+ import { AsyncLocalStorage } from 'node:async_hooks';
5
+ import { z } from 'zod';
6
+ import { registrationIdSchema, registrationIdentitySchema, registrationKey, registrationSchema } from './model.js';
7
+ import { assertLockOwnership, assertWorkstationOperationLock, withPrivateLock } from '../control/locks.js';
8
+ import { assertDeviceUpdateTransitionSafe, assertDurableDeviceMutationAllowed } from '../control/updateTransition.js';
9
+ import { LocalControlError } from '../control/protocol.js';
10
+ import { inspectPendingEnrollments } from './enrollmentPending.js';
11
+ import { resolveBrowserBinding } from '../desktop/binding.js';
12
+ import { registrationServerSchema } from './model.js';
13
+ const collectionSchema = z.array(registrationSchema).max(1024);
14
+ const atomicTemporaryPattern = /^\.write-[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/;
15
+ const stagedGenerationPattern = /^\.stage-[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/;
16
+ const tombstoneSchema = z.object({ server: registrationServerSchema, ownerUserId: z.string().min(1), workstationId: z.string().min(1), completed: z.literal(true) }).strict();
17
+ const tombstonesSchema = z.array(tombstoneSchema).max(4096);
18
+ const retiredSchema = z.object({ retired: z.literal(true), registration: registrationIdentitySchema }).strict();
19
+ export const removalIntentSchema = z.object({
20
+ schemaVersion: z.literal(1), registration: registrationSchema,
21
+ operationId: z.string().min(1).max(128).refine(value => value.trim().length > 0 && !/[\u0000-\u001f\u007f-\u009f]/.test(value)),
22
+ runtimeGeneration: z.string().min(1).max(128), force: z.boolean(),
23
+ phase: z.enum(['intent', 'sealed', 'disposed', 'credentials-retired', 'completed', 'restoring', 'cancelled']),
24
+ }).strict();
25
+ const removalIntentsSchema = z.array(removalIntentSchema).max(4096);
26
+ const deviceSchema = z.object({ schemaVersion: z.literal(1), deviceId: z.string().uuid(), initialized: z.boolean() }).strict();
27
+ const transactionSchema = z.object({ collection: collectionSchema, previous: collectionSchema, previousTombstones: tombstonesSchema, changed: registrationIdSchema.optional(), removed: registrationIdSchema.optional(), tombstones: tombstonesSchema }).strict().refine(value => Boolean(value.changed) !== Boolean(value.removed));
28
+ const localPath = z.string().max(4096).refine(value => path.isAbsolute(value) && !value.includes('\0'));
29
+ const configurationSchema = z.object({
30
+ server: registrationServerSchema, workstationId: z.string().min(1), ownerUserId: z.string().min(1).optional(),
31
+ workspaces: z.array(z.object({ name: z.string().max(1024), cwd: localPath }).strict()).max(256).optional(),
32
+ browserBinding: z.object({ bindingId: z.string(), displayName: z.string(), vncEndpoint: z.string(), cdpEndpoint: z.string().optional(), downloadsRoot: localPath.optional() }).strict().optional(),
33
+ desktopProvider: z.object({ kind: z.literal('linux-native'), helperPath: localPath, runtimeDir: localPath }).strict().optional(),
34
+ }).strict();
35
+ const missing = (error) => error.code === 'ENOENT';
36
+ const corrupt = () => new LocalControlError('CORRUPT_IDENTITY', 'Unsafe or corrupt registration storage');
37
+ const storeQueues = new Map();
38
+ const storeOperations = new AsyncLocalStorage();
39
+ async function serializeStore(root, work) {
40
+ const inherited = storeOperations.getStore();
41
+ if (inherited?.get(root)?.active)
42
+ throw new LocalControlError('BUSY', 'A registration store operation cannot call itself');
43
+ const lease = { active: true };
44
+ const context = new Map(inherited);
45
+ context.set(root, lease);
46
+ const operation = (storeQueues.get(root) ?? Promise.resolve()).then(() => storeOperations.run(context, async () => {
47
+ try {
48
+ return await work();
49
+ }
50
+ finally {
51
+ lease.active = false;
52
+ }
53
+ }));
54
+ const tail = operation.then(() => { }, () => { });
55
+ storeQueues.set(root, tail);
56
+ try {
57
+ return await operation;
58
+ }
59
+ finally {
60
+ if (storeQueues.get(root) === tail)
61
+ storeQueues.delete(root);
62
+ }
63
+ }
64
+ function digest(registration, clientKey, serverPub, nonce, configuration) {
65
+ const { revision: _revision, ...metadata } = registrationSchema.parse(registration);
66
+ return createHash('sha256').update(JSON.stringify(metadata)).update(clientKey).update(serverPub).update(nonce).update(JSON.stringify(configuration ?? null)).digest('hex');
67
+ }
68
+ export class RegistrationStore {
69
+ options;
70
+ root;
71
+ constructor(root, options = {}) {
72
+ this.options = options;
73
+ this.root = path.resolve(root);
74
+ }
75
+ async directory(directory, create = false) {
76
+ const relative = path.relative(this.root, directory);
77
+ if (relative.startsWith('..') || path.isAbsolute(relative))
78
+ throw corrupt();
79
+ let current = this.root;
80
+ for (const component of ['', ...relative.split(path.sep).filter(Boolean)]) {
81
+ if (component)
82
+ current = path.join(current, component);
83
+ if (create) {
84
+ try {
85
+ await fs.mkdir(current, { mode: 0o700 });
86
+ }
87
+ catch (error) {
88
+ if (error.code !== 'EEXIST')
89
+ throw error;
90
+ }
91
+ }
92
+ const stat = await fs.lstat(current);
93
+ if (!stat.isDirectory() || stat.isSymbolicLink() || (stat.mode & 0o077) !== 0 || (process.getuid && stat.uid !== process.getuid()))
94
+ throw corrupt();
95
+ }
96
+ }
97
+ async file(file, limit = 1_048_576) {
98
+ await this.directory(path.dirname(file));
99
+ const handle = await fs.open(file, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
100
+ try {
101
+ const stat = await handle.stat();
102
+ if (!stat.isFile() || stat.nlink !== 1 || stat.size > limit || (stat.mode & 0o077) !== 0 || (process.getuid && stat.uid !== process.getuid()))
103
+ throw corrupt();
104
+ const buffer = Buffer.alloc(stat.size + 1);
105
+ const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
106
+ if (bytesRead !== stat.size)
107
+ throw corrupt();
108
+ return buffer.subarray(0, bytesRead);
109
+ }
110
+ finally {
111
+ await handle.close();
112
+ }
113
+ }
114
+ async sync(directory) {
115
+ await this.directory(directory);
116
+ const handle = await fs.open(directory, constants.O_RDONLY | constants.O_NOFOLLOW);
117
+ try {
118
+ await handle.sync();
119
+ }
120
+ finally {
121
+ await handle.close();
122
+ }
123
+ }
124
+ async write(file, value) {
125
+ const text = JSON.stringify(value);
126
+ const limit = ['identity.json', 'retired.json'].includes(path.basename(file)) ? 16_384 : 1_048_576;
127
+ if (Buffer.byteLength(text) > limit)
128
+ throw corrupt();
129
+ assertLockOwnership();
130
+ await this.directory(path.dirname(file));
131
+ try {
132
+ await this.file(file);
133
+ }
134
+ catch (error) {
135
+ if (!missing(error))
136
+ throw error;
137
+ }
138
+ const temporary = path.join(path.dirname(file), `.write-${randomUUID()}`);
139
+ try {
140
+ const handle = await fs.open(temporary, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY | constants.O_NOFOLLOW, 0o600);
141
+ try {
142
+ await handle.writeFile(text);
143
+ await handle.sync();
144
+ }
145
+ finally {
146
+ await handle.close();
147
+ }
148
+ if (path.basename(file) === 'identity.json')
149
+ await this.options.afterStage?.('identity-file-synced');
150
+ if (path.basename(file) === 'removal-intents.json')
151
+ await this.options.afterStage?.('removal-intent-file-synced');
152
+ assertLockOwnership();
153
+ await fs.rename(temporary, file);
154
+ await this.sync(path.dirname(file));
155
+ }
156
+ finally {
157
+ assertLockOwnership();
158
+ await fs.rm(temporary, { force: true });
159
+ await this.sync(path.dirname(file));
160
+ }
161
+ }
162
+ async locked(work) {
163
+ await this.directory(this.root, true);
164
+ const canonicalRoot = await fs.realpath(this.root);
165
+ return serializeStore(canonicalRoot, () => this.lockedFile(work));
166
+ }
167
+ async lockedFile(work) {
168
+ try {
169
+ if (!deviceSchema.parse(JSON.parse((await this.file(path.join(this.root, 'device.json'), 4096)).toString())).initialized)
170
+ await assertDeviceUpdateTransitionSafe(this.root);
171
+ }
172
+ catch (error) {
173
+ if (!missing(error))
174
+ throw error;
175
+ await assertDeviceUpdateTransitionSafe(this.root);
176
+ }
177
+ await this.directory(path.join(this.root, 'registrations'), true);
178
+ return withPrivateLock(path.join(this.root, 'registration-store'), async () => {
179
+ let device;
180
+ try {
181
+ device = deviceSchema.parse(JSON.parse((await this.file(path.join(this.root, 'device.json'), 4096)).toString()));
182
+ }
183
+ catch (error) {
184
+ if (!missing(error))
185
+ throw error;
186
+ await assertDeviceUpdateTransitionSafe(this.root);
187
+ if ((await fs.readdir(path.join(this.root, 'registrations'))).length !== 0)
188
+ throw corrupt();
189
+ for (const name of ['registrations.json', 'registration-tombstones.json', 'registrations.pending.json']) {
190
+ try {
191
+ await fs.lstat(path.join(this.root, name));
192
+ throw corrupt();
193
+ }
194
+ catch (existingError) {
195
+ if (!missing(existingError))
196
+ throw existingError;
197
+ }
198
+ }
199
+ device = { schemaVersion: 1, deviceId: randomUUID(), initialized: false };
200
+ await this.write(path.join(this.root, 'device.json'), device);
201
+ }
202
+ if (!device.initialized) {
203
+ await assertDeviceUpdateTransitionSafe(this.root);
204
+ if ((await fs.readdir(path.join(this.root, 'registrations'))).length !== 0)
205
+ throw corrupt();
206
+ try {
207
+ await fs.lstat(path.join(this.root, 'registrations.pending.json'));
208
+ throw corrupt();
209
+ }
210
+ catch (error) {
211
+ if (!missing(error))
212
+ throw error;
213
+ }
214
+ for (const name of ['registrations.json', 'registration-tombstones.json']) {
215
+ try {
216
+ if ((await this.file(path.join(this.root, name))).toString() !== '[]')
217
+ throw corrupt();
218
+ }
219
+ catch (error) {
220
+ if (!missing(error))
221
+ throw error;
222
+ await this.write(path.join(this.root, name), []);
223
+ }
224
+ }
225
+ await this.write(path.join(this.root, 'device.json'), { ...device, initialized: true });
226
+ }
227
+ return work();
228
+ });
229
+ }
230
+ validate(registration) {
231
+ const parsed = registrationSchema.parse(registration);
232
+ const data = path.join(this.root, 'registrations', parsed.id, 'data');
233
+ if (parsed.dataDirectory !== data || ![data, this.root].includes(parsed.keeperConfigDirectory) || !/^[a-f0-9]{64}$/.test(parsed.revision))
234
+ throw corrupt();
235
+ return parsed;
236
+ }
237
+ configuration(value, registration) {
238
+ if (value === undefined)
239
+ return undefined;
240
+ const parsed = configurationSchema.parse(value);
241
+ if (parsed.server !== registration.server || parsed.workstationId !== registration.workstationId || (parsed.ownerUserId !== undefined && parsed.ownerUserId !== registration.ownerUserId))
242
+ throw corrupt();
243
+ if (parsed.browserBinding)
244
+ resolveBrowserBinding(parsed.browserBinding, {});
245
+ return parsed;
246
+ }
247
+ async collection() {
248
+ try {
249
+ const entries = collectionSchema.parse(JSON.parse((await this.file(path.join(this.root, 'registrations.json'))).toString()));
250
+ const ids = new Set();
251
+ const keys = new Set();
252
+ for (const entry of entries) {
253
+ this.validate(entry);
254
+ const key = registrationKey(entry.server, entry.ownerUserId);
255
+ if (ids.has(entry.id) || keys.has(key))
256
+ throw corrupt();
257
+ ids.add(entry.id);
258
+ keys.add(key);
259
+ }
260
+ return entries;
261
+ }
262
+ catch {
263
+ throw corrupt();
264
+ }
265
+ }
266
+ async identity(registration, generation = registration.revision) {
267
+ this.validate(registration);
268
+ await this.directory(registration.dataDirectory);
269
+ await this.directory(registration.keeperConfigDirectory);
270
+ const identity = z.object({ registration: registrationSchema, clientKey: z.string(), serverPub: z.string(), nonce: z.string().uuid(), configuration: configurationSchema.optional() }).strict().parse(JSON.parse((await this.file(path.join(this.root, 'registrations', registration.id, generation, 'identity.json'), 16_384)).toString()));
271
+ if (JSON.stringify(identity.registration) !== JSON.stringify(registration))
272
+ throw corrupt();
273
+ const clientKey = Buffer.from(identity.clientKey, 'base64');
274
+ const serverPub = Buffer.from(identity.serverPub, 'base64');
275
+ if (clientKey.length !== 32 || serverPub.length !== 32 || clientKey.toString('base64') !== identity.clientKey || serverPub.toString('base64') !== identity.serverPub)
276
+ throw corrupt();
277
+ const configuration = this.configuration(identity.configuration, registration);
278
+ if (digest(registration, clientKey, serverPub, identity.nonce, configuration) !== registration.revision)
279
+ throw corrupt();
280
+ return { registration, clientKey, serverPub, ...(configuration ? { configuration } : {}) };
281
+ }
282
+ async inspect() {
283
+ return this.locked(async () => {
284
+ let collection;
285
+ try {
286
+ await this.recoverLocked();
287
+ collection = await this.collection();
288
+ }
289
+ catch {
290
+ return { known: false, entries: [], diagnostic: 'Registration collection needs verification' };
291
+ }
292
+ const entries = [];
293
+ for (const registration of collection) {
294
+ try {
295
+ await this.identity(registration);
296
+ entries.push({ id: registration.id, state: 'ready', registration });
297
+ }
298
+ catch {
299
+ entries.push({ id: registration.id, state: 'corrupt', target: { server: registration.server, ownerUserId: registration.ownerUserId, workstationId: registration.workstationId, revision: registration.revision }, diagnostic: 'Registration identity needs repair' });
300
+ }
301
+ }
302
+ return { known: true, entries };
303
+ });
304
+ }
305
+ async list() {
306
+ const inspection = await this.inspect();
307
+ if (!inspection.known)
308
+ throw corrupt();
309
+ return inspection.entries.flatMap(entry => entry.state === 'ready' ? [entry.registration] : []);
310
+ }
311
+ async pendingEnrollments() {
312
+ return inspectPendingEnrollments(this.root);
313
+ }
314
+ async read(id) {
315
+ registrationIdSchema.parse(id);
316
+ return this.locked(async () => {
317
+ await this.recoverLocked();
318
+ const registration = (await this.collection()).find(entry => entry.id === id);
319
+ if (!registration)
320
+ throw new LocalControlError('BINDING_CONFLICT', 'Registration is no longer present');
321
+ return this.identity(registration);
322
+ });
323
+ }
324
+ async readMetadata(id, expectedRevision) {
325
+ registrationIdSchema.parse(id);
326
+ return this.locked(async () => {
327
+ await this.recoverLocked();
328
+ const registration = (await this.collection()).find(entry => entry.id === id);
329
+ if (!registration || registration.revision !== expectedRevision)
330
+ throw new LocalControlError('BINDING_CONFLICT', 'Registration changed; inspect again');
331
+ await this.directory(registration.dataDirectory);
332
+ await this.directory(registration.keeperConfigDirectory);
333
+ return { ...registration };
334
+ });
335
+ }
336
+ async removalIntentsLocked() {
337
+ try {
338
+ const intents = removalIntentsSchema.parse(JSON.parse((await this.file(path.join(this.root, 'removal-intents.json'))).toString()));
339
+ const ids = new Set();
340
+ const operations = new Set();
341
+ for (const intent of intents) {
342
+ this.validate(intent.registration);
343
+ if (ids.has(intent.registration.id) || operations.has(intent.operationId))
344
+ throw corrupt();
345
+ ids.add(intent.registration.id);
346
+ operations.add(intent.operationId);
347
+ }
348
+ return intents;
349
+ }
350
+ catch (error) {
351
+ if (missing(error))
352
+ return [];
353
+ throw corrupt();
354
+ }
355
+ }
356
+ async removalIntents() {
357
+ return this.locked(async () => {
358
+ const intents = await this.removalIntentsLocked();
359
+ const recovered = [];
360
+ let changed = false;
361
+ for (const intent of intents) {
362
+ if (intent.phase === 'credentials-retired' && await this.completedRemovalLocked(intent.registration)) {
363
+ recovered.push({ ...intent, phase: 'completed' });
364
+ changed = true;
365
+ }
366
+ else
367
+ recovered.push(intent);
368
+ }
369
+ if (changed) {
370
+ await assertDurableDeviceMutationAllowed(this.root);
371
+ await this.write(path.join(this.root, 'removal-intents.json'), recovered);
372
+ }
373
+ return recovered;
374
+ });
375
+ }
376
+ sameRemoval(left, right) {
377
+ return left.operationId === right.operationId && left.runtimeGeneration === right.runtimeGeneration && left.force === right.force && JSON.stringify(left.registration) === JSON.stringify(right.registration);
378
+ }
379
+ async beginRemoval(input) {
380
+ await assertDurableDeviceMutationAllowed(this.root);
381
+ assertWorkstationOperationLock(this.root);
382
+ const intent = removalIntentSchema.parse(input);
383
+ if (intent.phase !== 'intent')
384
+ throw corrupt();
385
+ return this.locked(async () => {
386
+ await assertDurableDeviceMutationAllowed(this.root);
387
+ await this.recoverLocked();
388
+ const intents = await this.removalIntentsLocked();
389
+ const previous = intents.find(entry => entry.operationId === intent.operationId || entry.registration.id === intent.registration.id && entry.phase !== 'cancelled');
390
+ if (previous) {
391
+ if (!this.sameRemoval(previous, intent))
392
+ throw new LocalControlError('BINDING_CONFLICT', 'Removal operation identity changed');
393
+ return previous;
394
+ }
395
+ const registration = (await this.collection()).find(entry => entry.id === intent.registration.id);
396
+ if (!registration || JSON.stringify(registration) !== JSON.stringify(intent.registration))
397
+ throw new LocalControlError('BINDING_CONFLICT', 'Removal target changed');
398
+ await this.identity(registration);
399
+ await this.write(path.join(this.root, 'removal-intents.json'), removalIntentsSchema.parse([...intents.filter(entry => entry.registration.id !== intent.registration.id), intent]));
400
+ await this.options.afterStage?.('removal-intent-published');
401
+ return intent;
402
+ });
403
+ }
404
+ async advanceRemoval(input, phase) {
405
+ await assertDurableDeviceMutationAllowed(this.root);
406
+ assertWorkstationOperationLock(this.root);
407
+ return this.locked(async () => {
408
+ await assertDurableDeviceMutationAllowed(this.root);
409
+ const intents = await this.removalIntentsLocked();
410
+ const previous = intents.find(entry => entry.operationId === input.operationId);
411
+ if (!previous || !this.sameRemoval(previous, input))
412
+ throw new LocalControlError('BINDING_CONFLICT', 'Removal operation identity changed');
413
+ const phases = removalIntentSchema.shape.phase.options;
414
+ if (['restoring', 'cancelled'].includes(phase) || ['restoring', 'cancelled'].includes(previous.phase))
415
+ throw new LocalControlError('BINDING_CONFLICT', 'Cancelled removal cannot resume termination');
416
+ if (phases.indexOf(phase) < phases.indexOf(previous.phase))
417
+ return previous;
418
+ if (phases.indexOf(phase) > phases.indexOf(previous.phase) + 1)
419
+ throw corrupt();
420
+ if (phase === 'completed' && !await this.completedRemovalLocked(previous.registration))
421
+ throw new LocalControlError('OUTCOME_UNKNOWN', 'Removal finalization needs verification');
422
+ const next = { ...previous, phase };
423
+ await this.write(path.join(this.root, 'removal-intents.json'), intents.map(entry => entry === previous ? next : entry));
424
+ await this.options.afterStage?.('removal-' + phase + '-published');
425
+ return next;
426
+ });
427
+ }
428
+ async cancelRemoval(input) {
429
+ await assertDurableDeviceMutationAllowed(this.root);
430
+ assertWorkstationOperationLock(this.root);
431
+ await this.locked(async () => {
432
+ await assertDurableDeviceMutationAllowed(this.root);
433
+ const intents = await this.removalIntentsLocked();
434
+ const previous = intents.find(entry => entry.operationId === input.operationId);
435
+ if (!previous)
436
+ return;
437
+ if (!this.sameRemoval(previous, input) || !['intent', 'restoring'].includes(previous.phase))
438
+ throw new LocalControlError('OUTCOME_UNKNOWN', 'Removal cannot reopen original admission');
439
+ await this.write(path.join(this.root, 'removal-intents.json'), intents.map(entry => entry === previous ? { ...previous, phase: 'restoring' } : entry));
440
+ await this.options.afterStage?.('removal-cancelled');
441
+ });
442
+ }
443
+ async finishRemovalRestoration(input) {
444
+ await assertDurableDeviceMutationAllowed(this.root);
445
+ assertWorkstationOperationLock(this.root);
446
+ await this.locked(async () => {
447
+ await assertDurableDeviceMutationAllowed(this.root);
448
+ const intents = await this.removalIntentsLocked();
449
+ const previous = intents.find(entry => entry.operationId === input.operationId);
450
+ if (!previous)
451
+ return;
452
+ if (!this.sameRemoval(previous, input) || previous.phase !== 'restoring')
453
+ throw new LocalControlError('BINDING_CONFLICT', 'Original removal restoration changed');
454
+ await this.write(path.join(this.root, 'removal-intents.json'), intents.map(entry => entry === previous ? { ...previous, phase: 'cancelled' } : entry));
455
+ });
456
+ }
457
+ async assertEnrollmentAllowed(server, ownerUserId) {
458
+ await assertDurableDeviceMutationAllowed(this.root);
459
+ await this.locked(async () => {
460
+ await assertDurableDeviceMutationAllowed(this.root);
461
+ if ((await this.removalIntentsLocked()).some(intent => !['completed', 'cancelled'].includes(intent.phase) && registrationKey(intent.registration.server, intent.registration.ownerUserId) === registrationKey(server, ownerUserId)))
462
+ throw new LocalControlError('BUSY', 'Matching registration removal remains pending');
463
+ });
464
+ }
465
+ async tombstones() {
466
+ try {
467
+ return z.array(tombstoneSchema).max(4096).parse(JSON.parse((await this.file(path.join(this.root, 'registration-tombstones.json'))).toString()));
468
+ }
469
+ catch {
470
+ throw corrupt();
471
+ }
472
+ }
473
+ async completedTombstones() {
474
+ return this.locked(async () => { await this.recoverLocked(); await this.collection(); return this.tombstones(); });
475
+ }
476
+ async hasCompletedRemoval(target) {
477
+ const parsed = registrationIdentitySchema.omit({ revision: true }).parse(target);
478
+ return this.locked(() => this.completedRemovalLocked(parsed));
479
+ }
480
+ async verifyRemovalCompletion(registration) {
481
+ const parsed = this.validate(registration);
482
+ return this.locked(() => this.completedRemovalLocked(parsed));
483
+ }
484
+ async completedRemovalLocked(parsed) {
485
+ await this.recoverLocked();
486
+ if ((await this.collection()).some(entry => entry.id === parsed.id))
487
+ return false;
488
+ let retired;
489
+ try {
490
+ retired = retiredSchema.parse(JSON.parse((await this.file(path.join(this.root, 'registrations', parsed.id, 'retired.json'), 16_384)).toString()));
491
+ }
492
+ catch (error) {
493
+ if (missing(error))
494
+ return false;
495
+ throw corrupt();
496
+ }
497
+ const registration = retired.registration;
498
+ if (registration.id !== parsed.id || registration.server !== parsed.server || registration.ownerUserId !== parsed.ownerUserId || registration.workstationId !== parsed.workstationId)
499
+ return false;
500
+ if (parsed.revision !== undefined && registration.revision !== parsed.revision)
501
+ return false;
502
+ if ((await this.scrubTargets(parsed.id)).length)
503
+ return false;
504
+ return (await this.tombstones()).some(entry => entry.server === parsed.server && entry.ownerUserId === parsed.ownerUserId && entry.workstationId === parsed.workstationId);
505
+ }
506
+ async commit(input) {
507
+ await assertDurableDeviceMutationAllowed(this.root);
508
+ if (!Buffer.isBuffer(input.clientKey) || !Buffer.isBuffer(input.serverPub) || input.clientKey.length !== 32 || input.serverPub.length !== 32)
509
+ throw corrupt();
510
+ const nonce = randomUUID();
511
+ const normalized = registrationSchema.parse({ ...input.registration, revision: 'pending' });
512
+ const configuration = this.configuration(input.configuration, normalized);
513
+ const registration = this.validate({ ...normalized, revision: digest(normalized, input.clientKey, input.serverPub, nonce, configuration) });
514
+ return this.locked(async () => {
515
+ await assertDurableDeviceMutationAllowed(this.root);
516
+ await this.recoverLocked();
517
+ const collection = await this.collection();
518
+ const current = collection.find(entry => entry.id === registration.id);
519
+ if ((await this.removalIntentsLocked()).some(intent => !['completed', 'cancelled'].includes(intent.phase) && (intent.registration.id === registration.id || registrationKey(intent.registration.server, intent.registration.ownerUserId) === registrationKey(registration.server, registration.ownerUserId))))
520
+ throw new LocalControlError('BUSY', 'Matching registration removal remains pending');
521
+ if ((current?.revision ?? null) !== input.expectedRevision || collection.some(entry => entry.id !== registration.id && registrationKey(entry.server, entry.ownerUserId) === registrationKey(registration.server, registration.ownerUserId)))
522
+ throw new LocalControlError('BINDING_CONFLICT', 'Registration changed or account already registered');
523
+ if (current)
524
+ await this.identity(current);
525
+ const base = path.join(this.root, 'registrations', registration.id);
526
+ if (!current) {
527
+ try {
528
+ await this.directory(base);
529
+ try {
530
+ await this.file(path.join(base, 'retired.json'), 16_384);
531
+ throw new LocalControlError('BINDING_CONFLICT', 'Registration ID was retired; choose a new ID');
532
+ }
533
+ catch (error) {
534
+ if (!missing(error))
535
+ throw error;
536
+ }
537
+ let matching = false;
538
+ const names = await fs.readdir(base);
539
+ let unpublished = names.every(name => name === 'data' || stagedGenerationPattern.test(name));
540
+ if (names.includes('data'))
541
+ await this.directory(registration.dataDirectory);
542
+ for (const name of names) {
543
+ if (!/^[a-f0-9]{64}$/.test(name) && !stagedGenerationPattern.test(name))
544
+ continue;
545
+ let text;
546
+ try {
547
+ text = await this.file(path.join(base, name, 'identity.json'), 16_384);
548
+ }
549
+ catch (error) {
550
+ if (!missing(error) || !name.startsWith('.stage-'))
551
+ throw error;
552
+ await this.discardUnpublishedStage(path.join(base, name));
553
+ continue;
554
+ }
555
+ unpublished = false;
556
+ const raw = JSON.parse(text.toString());
557
+ const old = await this.identity(registrationSchema.parse(raw.registration), name);
558
+ const { revision: _oldRevision, ...oldMetadata } = old.registration;
559
+ const { revision: _newRevision, ...newMetadata } = registration;
560
+ if (JSON.stringify(oldMetadata) !== JSON.stringify(newMetadata) || !old.clientKey.equals(input.clientKey) || !old.serverPub.equals(input.serverPub) || JSON.stringify(old.configuration) !== JSON.stringify(configuration))
561
+ throw new LocalControlError('BINDING_CONFLICT', 'Unpublished registration identity differs');
562
+ matching = true;
563
+ }
564
+ if (!matching && !unpublished)
565
+ throw new LocalControlError('BINDING_CONFLICT', 'Unrecognized existing registration data');
566
+ }
567
+ catch (error) {
568
+ if (!missing(error))
569
+ throw error;
570
+ try {
571
+ await fs.lstat(base);
572
+ }
573
+ catch (baseError) {
574
+ if (missing(baseError)) {
575
+ await this.directory(base, true);
576
+ }
577
+ else
578
+ throw baseError;
579
+ }
580
+ if ((await fs.readdir(base)).length !== 0)
581
+ throw corrupt();
582
+ }
583
+ }
584
+ await this.directory(base, true);
585
+ await this.directory(registration.dataDirectory, true);
586
+ await this.directory(registration.keeperConfigDirectory);
587
+ const staged = path.join(base, `.stage-${randomUUID()}`);
588
+ await this.directory(staged, true);
589
+ await this.write(path.join(staged, 'identity.json'), { registration, clientKey: input.clientKey.toString('base64'), serverPub: input.serverPub.toString('base64'), nonce, ...(configuration ? { configuration } : {}) });
590
+ await this.options.afterStage?.('staged');
591
+ await fs.rename(staged, path.join(base, registration.revision));
592
+ await this.sync(base);
593
+ await this.options.afterStage?.('identity-published');
594
+ const tombstones = await this.tombstones();
595
+ await this.write(path.join(this.root, 'registrations.pending.json'), transactionSchema.parse({ previous: collection, previousTombstones: tombstones, collection: [...collection.filter(entry => entry.id !== registration.id), registration], changed: registration.id, tombstones }));
596
+ await this.options.afterStage?.('transaction-ready');
597
+ await this.recoverLocked(true);
598
+ return registration;
599
+ });
600
+ }
601
+ async remove(id, expectedRevision) {
602
+ await assertDurableDeviceMutationAllowed(this.root);
603
+ registrationIdSchema.parse(id);
604
+ return this.locked(async () => {
605
+ await assertDurableDeviceMutationAllowed(this.root);
606
+ await this.recoverLocked();
607
+ const collection = await this.collection();
608
+ const current = collection.find(entry => entry.id === id);
609
+ if (!current || current.revision !== expectedRevision)
610
+ throw new LocalControlError('BINDING_CONFLICT', 'Registration changed; inspect again');
611
+ await this.scrubTargets(id);
612
+ const previousTombstones = await this.tombstones();
613
+ const tombstones = [...previousTombstones, { server: current.server, ownerUserId: current.ownerUserId, workstationId: current.workstationId, completed: true }];
614
+ await this.write(path.join(this.root, 'registrations.pending.json'), transactionSchema.parse({ previous: collection, previousTombstones, collection: collection.filter(entry => entry.id !== id), removed: id, tombstones }));
615
+ await this.options.afterStage?.('transaction-ready');
616
+ await this.recoverLocked(true);
617
+ });
618
+ }
619
+ async recover() { return this.locked(() => this.recoverLocked()); }
620
+ async discardUnpublishedStage(directory) {
621
+ await this.directory(directory);
622
+ const names = await fs.readdir(directory);
623
+ if (names.some(name => !atomicTemporaryPattern.test(name)))
624
+ throw corrupt();
625
+ for (const name of names)
626
+ if ((await fs.lstat(path.join(directory, name))).isDirectory())
627
+ throw corrupt();
628
+ for (const name of names) {
629
+ assertLockOwnership();
630
+ await fs.unlink(path.join(directory, name));
631
+ }
632
+ await this.sync(directory);
633
+ assertLockOwnership();
634
+ await fs.rmdir(directory);
635
+ await this.sync(path.dirname(directory));
636
+ }
637
+ async scrubTargets(id) {
638
+ const base = path.join(this.root, 'registrations', id);
639
+ await this.directory(base);
640
+ const targets = [];
641
+ for (const name of await fs.readdir(base)) {
642
+ if (!/^[a-f0-9]{64}$/.test(name) && !stagedGenerationPattern.test(name))
643
+ continue;
644
+ const directory = path.join(base, name);
645
+ await this.directory(directory);
646
+ for (const file of await fs.readdir(directory)) {
647
+ if (file !== 'identity.json' && !atomicTemporaryPattern.test(file))
648
+ continue;
649
+ const target = path.join(directory, file);
650
+ try {
651
+ const stat = await fs.lstat(target);
652
+ if (stat.isDirectory())
653
+ throw corrupt();
654
+ targets.push(target);
655
+ }
656
+ catch (error) {
657
+ if (!missing(error))
658
+ throw error;
659
+ }
660
+ }
661
+ }
662
+ return targets;
663
+ }
664
+ async recoverLocked(inject = false) {
665
+ let transaction;
666
+ try {
667
+ transaction = transactionSchema.parse(JSON.parse((await this.file(path.join(this.root, 'registrations.pending.json'))).toString()));
668
+ }
669
+ catch (error) {
670
+ if (missing(error))
671
+ return;
672
+ throw corrupt();
673
+ }
674
+ const current = await this.collection();
675
+ const previous = transaction.previous;
676
+ const same = (left, right) => JSON.stringify(left) === JSON.stringify(right);
677
+ if (!same(current, previous) && !same(current, transaction.collection))
678
+ throw corrupt();
679
+ const currentTombstones = await this.tombstones();
680
+ let expectedTombstones = transaction.previousTombstones;
681
+ if (transaction.removed) {
682
+ const removed = previous.find(entry => entry.id === transaction.removed);
683
+ if (!removed)
684
+ throw corrupt();
685
+ expectedTombstones = [...expectedTombstones, { server: removed.server, ownerUserId: removed.ownerUserId, workstationId: removed.workstationId, completed: true }];
686
+ }
687
+ if (!same(transaction.tombstones, expectedTombstones) || (!same(currentTombstones, transaction.previousTombstones) && !same(currentTombstones, expectedTombstones)))
688
+ throw corrupt();
689
+ if (transaction.changed && same(previous, transaction.collection))
690
+ throw corrupt();
691
+ for (const old of previous) {
692
+ const next = transaction.collection.find(entry => entry.id === old.id);
693
+ if (!next && old.id !== transaction.removed)
694
+ throw corrupt();
695
+ if (next && JSON.stringify(next) !== JSON.stringify(old) && old.id !== transaction.changed)
696
+ throw corrupt();
697
+ }
698
+ const ids = new Set();
699
+ const keys = new Set();
700
+ for (const registration of transaction.collection) {
701
+ const key = registrationKey(registration.server, registration.ownerUserId);
702
+ if (ids.has(registration.id) || keys.has(key) || registration.id === transaction.removed)
703
+ throw corrupt();
704
+ this.validate(registration);
705
+ ids.add(registration.id);
706
+ keys.add(key);
707
+ const old = previous.find(entry => entry.id === registration.id);
708
+ if (!old && registration.id !== transaction.changed)
709
+ throw corrupt();
710
+ if (registration.id === transaction.changed)
711
+ await this.identity(registration);
712
+ }
713
+ if (transaction.changed && !ids.has(transaction.changed))
714
+ throw corrupt();
715
+ const targets = transaction.removed ? await this.scrubTargets(transaction.removed) : [];
716
+ if (transaction.removed) {
717
+ const { dataDirectory: _data, keeperConfigDirectory: _keeper, ...registration } = previous.find(entry => entry.id === transaction.removed);
718
+ await this.write(path.join(this.root, 'registrations', transaction.removed, 'retired.json'), { retired: true, registration: registrationIdentitySchema.parse(registration) });
719
+ }
720
+ for (const target of targets) {
721
+ assertLockOwnership();
722
+ await fs.unlink(target);
723
+ await this.sync(path.dirname(target));
724
+ if (inject)
725
+ await this.options.afterStage?.('identity-scrubbed');
726
+ }
727
+ await this.write(path.join(this.root, 'registration-tombstones.json'), transaction.tombstones);
728
+ if (inject)
729
+ await this.options.afterStage?.('tombstone-published');
730
+ await this.write(path.join(this.root, 'registrations.json'), transaction.collection);
731
+ if (inject)
732
+ await this.options.afterStage?.('collection-published');
733
+ assertLockOwnership();
734
+ await fs.unlink(path.join(this.root, 'registrations.pending.json'));
735
+ await this.sync(this.root);
736
+ }
737
+ }