agl 1.0.17 → 21.0.2

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 (277) hide show
  1. package/.smartconfig.json +55 -0
  2. package/changelog.md +1026 -0
  3. package/cli.js +14 -0
  4. package/dist_serve/bundle.js +44778 -0
  5. package/dist_serve/index.html +13 -0
  6. package/dist_ts/00_commitinfo_data.d.ts +8 -0
  7. package/dist_ts/00_commitinfo_data.js +9 -0
  8. package/dist_ts/classes.authmodels.d.ts +126 -0
  9. package/dist_ts/classes.authmodels.js +1422 -0
  10. package/dist_ts/classes.authstore.d.ts +160 -0
  11. package/dist_ts/classes.authstore.js +2483 -0
  12. package/dist_ts/classes.browserresourcehost.d.ts +85 -0
  13. package/dist_ts/classes.browserresourcehost.js +912 -0
  14. package/dist_ts/classes.cli.d.ts +21 -0
  15. package/dist_ts/classes.cli.js +1082 -0
  16. package/dist_ts/classes.config.d.ts +23 -0
  17. package/dist_ts/classes.config.js +267 -0
  18. package/dist_ts/classes.controller.d.ts +523 -0
  19. package/dist_ts/classes.controller.js +10611 -0
  20. package/dist_ts/classes.controllermanagement.d.ts +32 -0
  21. package/dist_ts/classes.controllermanagement.js +222 -0
  22. package/dist_ts/classes.draftmanager.d.ts +30 -0
  23. package/dist_ts/classes.draftmanager.js +179 -0
  24. package/dist_ts/classes.embeddeddb.d.ts +30 -0
  25. package/dist_ts/classes.embeddeddb.js +159 -0
  26. package/dist_ts/classes.flexframedtransport.d.ts +56 -0
  27. package/dist_ts/classes.flexframedtransport.js +275 -0
  28. package/dist_ts/classes.flexmodels.private.d.ts +231 -0
  29. package/dist_ts/classes.flexmodels.private.js +985 -0
  30. package/dist_ts/classes.flexmodels.public.d.ts +207 -0
  31. package/dist_ts/classes.flexmodels.public.js +885 -0
  32. package/dist_ts/classes.flexprojectionreader.d.ts +89 -0
  33. package/dist_ts/classes.flexprojectionreader.js +580 -0
  34. package/dist_ts/classes.flexservice.d.ts +196 -0
  35. package/dist_ts/classes.flexservice.js +3704 -0
  36. package/dist_ts/classes.flexstore.d.ts +174 -0
  37. package/dist_ts/classes.flexstore.js +3400 -0
  38. package/dist_ts/classes.flexsupervisor.d.ts +105 -0
  39. package/dist_ts/classes.flexsupervisor.js +1191 -0
  40. package/dist_ts/classes.gitreversion.d.ts +431 -0
  41. package/dist_ts/classes.gitreversion.js +10592 -0
  42. package/dist_ts/classes.harnessoperationmanager.d.ts +113 -0
  43. package/dist_ts/classes.harnessoperationmanager.js +602 -0
  44. package/dist_ts/classes.opencodeclient.d.ts +146 -0
  45. package/dist_ts/classes.opencodeclient.js +3003 -0
  46. package/dist_ts/classes.opencodesupervisor.d.ts +60 -0
  47. package/dist_ts/classes.opencodesupervisor.js +1003 -0
  48. package/dist_ts/classes.passkeymanager.d.ts +34 -0
  49. package/dist_ts/classes.passkeymanager.js +456 -0
  50. package/dist_ts/classes.processinspection.d.ts +50 -0
  51. package/dist_ts/classes.processinspection.js +484 -0
  52. package/dist_ts/classes.resourcecoordinator.d.ts +64 -0
  53. package/dist_ts/classes.resourcecoordinator.js +294 -0
  54. package/dist_ts/classes.sessionintelligencereader.d.ts +13 -0
  55. package/dist_ts/classes.sessionintelligencereader.js +40 -0
  56. package/dist_ts/classes.systemmetricssampler.d.ts +53 -0
  57. package/dist_ts/classes.systemmetricssampler.js +168 -0
  58. package/dist_ts/classes.terminalmanager.d.ts +95 -0
  59. package/dist_ts/classes.terminalmanager.js +981 -0
  60. package/dist_ts/classes.terminalresourcehost.d.ts +20 -0
  61. package/dist_ts/classes.terminalresourcehost.js +84 -0
  62. package/dist_ts/classes.upgradecoordinator.d.ts +244 -0
  63. package/dist_ts/classes.upgradecoordinator.js +2155 -0
  64. package/dist_ts/classes.upgradetransaction.d.ts +92 -0
  65. package/dist_ts/classes.upgradetransaction.js +2125 -0
  66. package/dist_ts/classes.uploadmanager.d.ts +24 -0
  67. package/dist_ts/classes.uploadmanager.js +210 -0
  68. package/dist_ts/classes.validation.d.ts +91 -0
  69. package/dist_ts/classes.validation.js +1087 -0
  70. package/dist_ts/flexharness.child.d.ts +1 -0
  71. package/dist_ts/flexharness.child.js +28 -0
  72. package/dist_ts/functions.canonicaldirectory.d.ts +21 -0
  73. package/dist_ts/functions.canonicaldirectory.js +144 -0
  74. package/dist_ts/functions.controllerdataroot.d.ts +15 -0
  75. package/dist_ts/functions.controllerdataroot.js +76 -0
  76. package/dist_ts/functions.embeddeddb.d.ts +2 -0
  77. package/dist_ts/functions.embeddeddb.js +21 -0
  78. package/dist_ts/functions.flexcredentialstore.d.ts +16 -0
  79. package/dist_ts/functions.flexcredentialstore.js +93 -0
  80. package/dist_ts/functions.opencodeauth.d.ts +3 -0
  81. package/dist_ts/functions.opencodeauth.js +31 -0
  82. package/dist_ts/functions.runtimeenvironment.d.ts +2 -0
  83. package/dist_ts/functions.runtimeenvironment.js +79 -0
  84. package/dist_ts/functions.upgradefollower.d.ts +3 -0
  85. package/dist_ts/functions.upgradefollower.js +95 -0
  86. package/dist_ts/index.d.ts +24 -0
  87. package/dist_ts/index.js +25 -0
  88. package/dist_ts/interfaces.auth.d.ts +181 -0
  89. package/dist_ts/interfaces.auth.js +82 -0
  90. package/dist_ts/interfaces.config.d.ts +34 -0
  91. package/dist_ts/interfaces.config.js +2 -0
  92. package/dist_ts/interfaces.flexipc.d.ts +939 -0
  93. package/dist_ts/interfaces.flexipc.js +1492 -0
  94. package/dist_ts/interfaces.gitreversion.d.ts +246 -0
  95. package/dist_ts/interfaces.gitreversion.js +2 -0
  96. package/dist_ts/interfaces.lifecycle.d.ts +37 -0
  97. package/dist_ts/interfaces.lifecycle.js +2 -0
  98. package/dist_ts/interfaces.opencode.d.ts +101 -0
  99. package/dist_ts/interfaces.opencode.js +3 -0
  100. package/dist_ts/interfaces.projects.d.ts +156 -0
  101. package/dist_ts/interfaces.projects.js +5 -0
  102. package/dist_ts/plugins.d.ts +36 -0
  103. package/dist_ts/plugins.js +40 -0
  104. package/dist_ts_interfaces/index.d.ts +2 -0
  105. package/dist_ts_interfaces/index.js +3 -0
  106. package/dist_ts_interfaces/interfaces.d.ts +747 -0
  107. package/dist_ts_interfaces/interfaces.js +47 -0
  108. package/dist_ts_interfaces/plugins.d.ts +4 -0
  109. package/dist_ts_interfaces/plugins.js +3 -0
  110. package/dist_ts_interfaces/requests.d.ts +806 -0
  111. package/dist_ts_interfaces/requests.js +2 -0
  112. package/dist_ts_migration/classes.documentmigrationrunner.d.ts +8 -0
  113. package/dist_ts_migration/classes.documentmigrationrunner.js +24 -0
  114. package/dist_ts_migration/classes.flexmigrationrunner.d.ts +11 -0
  115. package/dist_ts_migration/classes.flexmigrationrunner.js +25 -0
  116. package/dist_ts_migration/classes.flexpostharnessmigrationrunner.d.ts +10 -0
  117. package/dist_ts_migration/classes.flexpostharnessmigrationrunner.js +15 -0
  118. package/dist_ts_migration/classes.migrationrunner.d.ts +8 -0
  119. package/dist_ts_migration/classes.migrationrunner.js +34 -0
  120. package/dist_ts_migration/index.d.ts +12 -0
  121. package/dist_ts_migration/index.js +13 -0
  122. package/dist_ts_migration/v10_flexharnessv3migration.d.ts +11 -0
  123. package/dist_ts_migration/v10_flexharnessv3migration.js +23 -0
  124. package/dist_ts_migration/v13_flexprojectionorder.d.ts +11 -0
  125. package/dist_ts_migration/v13_flexprojectionorder.js +23 -0
  126. package/dist_ts_migration/v16_flexprovidercredentials.d.ts +22 -0
  127. package/dist_ts_migration/v16_flexprovidercredentials.js +149 -0
  128. package/dist_ts_migration/v18_resourcelayout.d.ts +22 -0
  129. package/dist_ts_migration/v18_resourcelayout.js +23 -0
  130. package/dist_ts_migration/v18_resourcelayoutmigration.d.ts +6 -0
  131. package/dist_ts_migration/v18_resourcelayoutmigration.js +61 -0
  132. package/dist_ts_migration/v19_flexprojectmanagement.d.ts +11 -0
  133. package/dist_ts_migration/v19_flexprojectmanagement.js +21 -0
  134. package/dist_ts_migration/v1_embeddeddatabaselocation.d.ts +28 -0
  135. package/dist_ts_migration/v1_embeddeddatabaselocation.js +415 -0
  136. package/dist_ts_migration/v22_projectfilesystemidentitymigration.d.ts +7 -0
  137. package/dist_ts_migration/v22_projectfilesystemidentitymigration.js +105 -0
  138. package/dist_ts_migration/v2_controllerdataroot.d.ts +130 -0
  139. package/dist_ts_migration/v2_controllerdataroot.js +1887 -0
  140. package/dist_ts_migration/v6_protocoldocumentmigration.d.ts +7 -0
  141. package/dist_ts_migration/v6_protocoldocumentmigration.js +100 -0
  142. package/dist_ts_migration/v6_protocoldocuments.d.ts +7 -0
  143. package/dist_ts_migration/v6_protocoldocuments.js +138 -0
  144. package/html/index.html +13 -0
  145. package/{LICENSE → license.md} +2 -2
  146. package/package.json +86 -48
  147. package/readme.md +480 -0
  148. package/readme.plan.md +188 -0
  149. package/ts/00_commitinfo_data.ts +8 -0
  150. package/ts/classes.authmodels.ts +1245 -0
  151. package/ts/classes.authstore.ts +3274 -0
  152. package/ts/classes.browserresourcehost.ts +1205 -0
  153. package/ts/classes.cli.ts +1254 -0
  154. package/ts/classes.config.ts +351 -0
  155. package/ts/classes.controller.ts +14426 -0
  156. package/ts/classes.controllermanagement.ts +330 -0
  157. package/ts/classes.draftmanager.ts +232 -0
  158. package/ts/classes.embeddeddb.ts +166 -0
  159. package/ts/classes.flexframedtransport.ts +283 -0
  160. package/ts/classes.flexmodels.private.ts +823 -0
  161. package/ts/classes.flexmodels.public.ts +732 -0
  162. package/ts/classes.flexprojectionreader.ts +816 -0
  163. package/ts/classes.flexservice.ts +4302 -0
  164. package/ts/classes.flexstore.ts +4565 -0
  165. package/ts/classes.flexsupervisor.ts +1453 -0
  166. package/ts/classes.gitreversion.ts +13353 -0
  167. package/ts/classes.harnessoperationmanager.ts +870 -0
  168. package/ts/classes.opencodeclient.ts +4110 -0
  169. package/ts/classes.opencodesupervisor.ts +1280 -0
  170. package/ts/classes.passkeymanager.ts +570 -0
  171. package/ts/classes.processinspection.ts +623 -0
  172. package/ts/classes.resourcecoordinator.ts +436 -0
  173. package/ts/classes.sessionintelligencereader.ts +52 -0
  174. package/ts/classes.systemmetricssampler.ts +215 -0
  175. package/ts/classes.terminalmanager.ts +1207 -0
  176. package/ts/classes.terminalresourcehost.ts +122 -0
  177. package/ts/classes.upgradecoordinator.ts +2675 -0
  178. package/ts/classes.upgradetransaction.ts +2900 -0
  179. package/ts/classes.uploadmanager.ts +240 -0
  180. package/ts/classes.validation.ts +1409 -0
  181. package/ts/flexharness.child.ts +35 -0
  182. package/ts/functions.canonicaldirectory.ts +213 -0
  183. package/ts/functions.controllerdataroot.ts +107 -0
  184. package/ts/functions.embeddeddb.ts +22 -0
  185. package/ts/functions.flexcredentialstore.ts +128 -0
  186. package/ts/functions.opencodeauth.ts +34 -0
  187. package/ts/functions.runtimeenvironment.ts +90 -0
  188. package/ts/functions.upgradefollower.ts +115 -0
  189. package/ts/index.ts +36 -6
  190. package/ts/interfaces.auth.ts +316 -0
  191. package/ts/interfaces.config.ts +37 -0
  192. package/ts/interfaces.flexipc.ts +2586 -0
  193. package/ts/interfaces.gitreversion.ts +336 -0
  194. package/ts/interfaces.lifecycle.ts +46 -0
  195. package/ts/interfaces.opencode.ts +153 -0
  196. package/ts/interfaces.projects.ts +182 -0
  197. package/ts/plugins.ts +64 -0
  198. package/ts_interfaces/index.ts +2 -0
  199. package/ts_interfaces/interfaces.ts +827 -0
  200. package/ts_interfaces/plugins.ts +15 -0
  201. package/ts_interfaces/requests.ts +1186 -0
  202. package/ts_migration/classes.documentmigrationrunner.ts +32 -0
  203. package/ts_migration/classes.flexmigrationrunner.ts +33 -0
  204. package/ts_migration/classes.flexpostharnessmigrationrunner.ts +22 -0
  205. package/ts_migration/classes.migrationrunner.ts +40 -0
  206. package/ts_migration/index.ts +12 -0
  207. package/ts_migration/v10_flexharnessv3migration.ts +35 -0
  208. package/ts_migration/v13_flexprojectionorder.ts +33 -0
  209. package/ts_migration/v16_flexprovidercredentials.ts +196 -0
  210. package/ts_migration/v18_resourcelayout.ts +55 -0
  211. package/ts_migration/v18_resourcelayoutmigration.ts +68 -0
  212. package/ts_migration/v19_flexprojectmanagement.ts +28 -0
  213. package/ts_migration/v1_embeddeddatabaselocation.ts +560 -0
  214. package/ts_migration/v22_projectfilesystemidentitymigration.ts +121 -0
  215. package/ts_migration/v2_controllerdataroot.ts +2349 -0
  216. package/ts_migration/v6_protocoldocumentmigration.ts +144 -0
  217. package/ts_migration/v6_protocoldocuments.ts +198 -0
  218. package/ts_web/00_commitinfo_data.ts +8 -0
  219. package/ts_web/classes.controllersocketclient.ts +517 -0
  220. package/ts_web/classes.sessiondraftsync.ts +390 -0
  221. package/ts_web/elements.harnesscontrollerapp.ts +11494 -0
  222. package/ts_web/index.ts +2 -0
  223. package/ts_web/plugins.ts +31 -0
  224. package/.gitlab-ci.yml +0 -71
  225. package/.npmignore +0 -5
  226. package/README.md +0 -40
  227. package/dist/cli.js +0 -2
  228. package/dist/index.d.ts +0 -0
  229. package/dist/index.js +0 -9
  230. package/dist/mod_bundle/index.d.ts +0 -1
  231. package/dist/mod_bundle/index.js +0 -18
  232. package/dist/mod_bundle/mod.plugins.d.ts +0 -4
  233. package/dist/mod_bundle/mod.plugins.js +0 -11
  234. package/dist/mod_install/index.d.ts +0 -4
  235. package/dist/mod_install/index.js +0 -21
  236. package/dist/mod_install/mod.plugins.d.ts +0 -1
  237. package/dist/mod_install/mod.plugins.js +0 -7
  238. package/dist/mod_standard/index.d.ts +0 -4
  239. package/dist/mod_standard/index.js +0 -21
  240. package/dist/mod_standard/mod.browser.d.ts +0 -6
  241. package/dist/mod_standard/mod.browser.js +0 -14
  242. package/dist/mod_standard/mod.clean.d.ts +0 -4
  243. package/dist/mod_standard/mod.clean.js +0 -12
  244. package/dist/mod_standard/mod.compile.d.ts +0 -4
  245. package/dist/mod_standard/mod.compile.js +0 -21
  246. package/dist/mod_standard/mod.plugins.d.ts +0 -8
  247. package/dist/mod_standard/mod.plugins.js +0 -19
  248. package/dist/mod_standard/mod.test.d.ts +0 -4
  249. package/dist/mod_standard/mod.test.js +0 -12
  250. package/dist/plr.cli.d.ts +0 -3
  251. package/dist/plr.cli.js +0 -57
  252. package/dist/plr.config.d.ts +0 -11
  253. package/dist/plr.config.js +0 -26
  254. package/dist/plr.mods.d.ts +0 -17
  255. package/dist/plr.mods.js +0 -17
  256. package/dist/plr.paths.d.ts +0 -19
  257. package/dist/plr.paths.js +0 -26
  258. package/dist/plr.plugins.d.ts +0 -8
  259. package/dist/plr.plugins.js +0 -16
  260. package/npmextra.json +0 -10
  261. package/ts/mod_bundle/index.ts +0 -20
  262. package/ts/mod_bundle/mod.plugins.ts +0 -9
  263. package/ts/mod_install/index.ts +0 -20
  264. package/ts/mod_install/mod.plugins.ts +0 -1
  265. package/ts/mod_standard/index.ts +0 -23
  266. package/ts/mod_standard/mod.browser.ts +0 -17
  267. package/ts/mod_standard/mod.clean.ts +0 -13
  268. package/ts/mod_standard/mod.compile.ts +0 -23
  269. package/ts/mod_standard/mod.plugins.ts +0 -17
  270. package/ts/mod_standard/mod.test.ts +0 -13
  271. package/ts/plr.cli.ts +0 -60
  272. package/ts/plr.config.ts +0 -36
  273. package/ts/plr.mods.ts +0 -25
  274. package/ts/plr.paths.ts +0 -29
  275. package/ts/plr.plugins.ts +0 -17
  276. package/tslint.json +0 -3
  277. package/yarn.lock +0 -3464
@@ -0,0 +1,2675 @@
1
+ import * as plugins from './plugins.js';
2
+ import {
3
+ controllerPackageName,
4
+ type IControllerRuntimeId,
5
+ type IControllerUpgradeStatus,
6
+ } from '../ts_interfaces/index.js';
7
+ import {
8
+ processIdentityHasCliCommand,
9
+ readControllerProcessIdentity,
10
+ readProcessGroupMemberPids,
11
+ } from './classes.processinspection.js';
12
+
13
+ export const upgradeCoordinationVersion = 2 as const;
14
+ const coordinationVersion = upgradeCoordinationVersion;
15
+ export const upgradePackageTransitionTransactionVersion = 3 as const;
16
+ export const upgradePackageTransitionSource = {
17
+ packageName: '@modelprofile.com/harness-controller',
18
+ version: '20.0.3',
19
+ managementVersion: 1,
20
+ cliName: 'hcon',
21
+ cliRelativePath: './cli.js',
22
+ } as const;
23
+ export const upgradePackageTransitionTarget = {
24
+ packageName: 'agl',
25
+ version: '21.0.0',
26
+ managementVersion: 2,
27
+ cliName: 'agl',
28
+ cliRelativePath: './cli.js',
29
+ } as const;
30
+ const currentCliName = String(controllerPackageName) === upgradePackageTransitionSource.packageName
31
+ ? upgradePackageTransitionSource.cliName
32
+ : upgradePackageTransitionTarget.cliName;
33
+ const ownerFileName = 'owner.json';
34
+ const maximumMetadataBytes = 256 * 1024;
35
+ const initializationGraceMs = 30_000;
36
+ const startLeaseDrainTimeoutMs = 60_000;
37
+ const upgradeTokenPattern = /^[A-Za-z0-9_-]{43}$/;
38
+ const upgradeTokenHashPattern = /^[a-f0-9]{64}$/;
39
+ const leaseDirectoryPattern = /^[a-f0-9]{20}$/;
40
+ const tokenMetadataPattern = /^(?:grant|ack|action-(?:prepare|finalize))-[a-f0-9]{20}\.json(?:\.consuming-[1-9][0-9]*-[a-f0-9]{8})?$/;
41
+ const transactionMetadataPattern = /^transaction-[a-f0-9]{20}\.json$/;
42
+ const transactionTemporaryMetadataPattern = /^transaction-[a-f0-9]{20}\.json\.tmp-[1-9][0-9]*-[a-f0-9]{8}$/;
43
+ const transactionMutationDirectoryPattern = /^transaction-[a-f0-9]{20}\.json\.lock$/;
44
+ const ownershipGuardDirectoryPattern = /^[a-f0-9]{64}\.guard$/;
45
+ const tokenMetadataRetentionMs = 10 * 60 * 1000;
46
+ const transactionRetentionMs = 24 * 60 * 60 * 1000;
47
+ const terminalStatusRetentionMs = 10 * 60 * 1000;
48
+ const transactionMutationTimeoutMs = 10_000;
49
+ const ownershipGuardTimeoutMs = 10_000;
50
+ const workerTerminationGraceMs = 5_000;
51
+
52
+ export const upgradeTokenEnvironmentVariable = 'HARNESS_CONTROLLER_UPGRADE_TOKEN';
53
+
54
+ type TUpgradeOwnerKind = 'upgrade' | 'start';
55
+
56
+ interface IUpgradeProcessOwner {
57
+ version: typeof coordinationVersion;
58
+ kind: TUpgradeOwnerKind;
59
+ uid: number;
60
+ tokenHash: string;
61
+ pid: number;
62
+ processGroupId: number;
63
+ fingerprint: string;
64
+ cliPath: string;
65
+ command: '__serve' | '__upgrade-worker' | 'upgrade' | 'start' | 'foreground' | 'temp-password';
66
+ createdAt: number;
67
+ }
68
+
69
+ export interface IUpgradeExpectedController {
70
+ pid: number;
71
+ processGroupId: number;
72
+ processFingerprint: string;
73
+ }
74
+
75
+ interface IUpgradeLaunchGrant {
76
+ version: typeof coordinationVersion;
77
+ tokenHash: string;
78
+ expiresAt: number;
79
+ port: number;
80
+ packageName: string;
81
+ packageVersion: string;
82
+ cliPath: string;
83
+ controller: IUpgradeExpectedController;
84
+ }
85
+
86
+ export interface IUpgradeWorkerPayload {
87
+ version: typeof coordinationVersion;
88
+ token: string;
89
+ port: number;
90
+ gracePeriodMs: number;
91
+ continueSessions: boolean;
92
+ expectedController?: IUpgradeExpectedController;
93
+ }
94
+
95
+ export type TUpgradeSessionTransitionState =
96
+ | 'pending'
97
+ | 'submitting'
98
+ | 'accepted'
99
+ | 'paused'
100
+ | 'outcomeUnknown'
101
+ | 'failed';
102
+
103
+ export interface IUpgradeSessionState {
104
+ projectId: string;
105
+ sessionId: IControllerRuntimeId;
106
+ pauseState: TUpgradeSessionTransitionState;
107
+ pauseQueueId?: string;
108
+ cleanupState?: TUpgradeSessionTransitionState;
109
+ continueState?: TUpgradeSessionTransitionState;
110
+ error?: string;
111
+ }
112
+
113
+ interface IUpgradeTransactionBase {
114
+ tokenHash: string;
115
+ revision: number;
116
+ port: number;
117
+ controllerWasRunning: boolean;
118
+ continueSessions: boolean;
119
+ gracePeriodMs: number;
120
+ preparationAcceptedAt?: number;
121
+ preparationDeadlineAt?: number;
122
+ preparationCompletedAt?: number;
123
+ phase: IControllerUpgradeStatus['phase'] | 'starting' | 'checking' | 'stopping';
124
+ message: string;
125
+ createdAt: number;
126
+ phaseStartedAt: number;
127
+ updatedAt: number;
128
+ retainedUntil?: number;
129
+ worker?: {
130
+ pid: number;
131
+ processGroupId: number;
132
+ fingerprint: string;
133
+ cliPath: string;
134
+ logFilePath?: string;
135
+ };
136
+ controller?: {
137
+ pid: number;
138
+ processGroupId: number;
139
+ fingerprint: string;
140
+ cliPath: string;
141
+ command: '__serve' | 'foreground';
142
+ };
143
+ sessions: IUpgradeSessionState[];
144
+ terminal?: {
145
+ success: boolean;
146
+ error?: string;
147
+ };
148
+ }
149
+
150
+ export interface IUpgradeTransactionV2 extends IUpgradeTransactionBase {
151
+ version: typeof coordinationVersion;
152
+ sourceVersion: string;
153
+ targetVersion?: string;
154
+ targetStartupInvoked?: true;
155
+ }
156
+
157
+ export interface IUpgradeTransactionV3 extends IUpgradeTransactionBase {
158
+ version: typeof upgradePackageTransitionTransactionVersion;
159
+ sourcePackageName: typeof upgradePackageTransitionSource.packageName;
160
+ sourceVersion: typeof upgradePackageTransitionSource.version;
161
+ sourceManagementVersion: typeof upgradePackageTransitionSource.managementVersion;
162
+ sourceCliName: typeof upgradePackageTransitionSource.cliName;
163
+ sourceCliRelativePath: typeof upgradePackageTransitionSource.cliRelativePath;
164
+ targetPackageName: typeof upgradePackageTransitionTarget.packageName;
165
+ targetVersion: typeof upgradePackageTransitionTarget.version;
166
+ targetManagementVersion: typeof upgradePackageTransitionTarget.managementVersion;
167
+ targetCliName: typeof upgradePackageTransitionTarget.cliName;
168
+ targetCliRelativePath: typeof upgradePackageTransitionTarget.cliRelativePath;
169
+ packageTransitionStarted?: true;
170
+ groupedTargetVerified?: true;
171
+ targetPackageCommitStarted?: true;
172
+ targetPackageCommitted?: true;
173
+ targetStartupInvoked?: true;
174
+ }
175
+
176
+ export type TUpgradeTransaction = IUpgradeTransactionV2 | IUpgradeTransactionV3;
177
+
178
+ export type TUpgradeControllerAction = 'prepare' | 'finalize';
179
+
180
+ interface IUpgradeControllerActionGrant {
181
+ version: typeof coordinationVersion;
182
+ tokenHash: string;
183
+ action: TUpgradeControllerAction;
184
+ mode?: 'continue' | 'compensate' | 'reopen';
185
+ expiresAt: number;
186
+ packageVersion: string;
187
+ controller: IUpgradeExpectedController;
188
+ }
189
+
190
+ interface IUpgradeTransactionMutationOwner {
191
+ version: typeof coordinationVersion;
192
+ uid: number;
193
+ tokenHash: string;
194
+ pid: number;
195
+ fingerprint: string;
196
+ nonce: string;
197
+ createdAt: number;
198
+ }
199
+
200
+ interface IUpgradeOwnershipMutationGuardOwner {
201
+ version: typeof coordinationVersion;
202
+ uid: number;
203
+ targetHash: string;
204
+ pid: number;
205
+ fingerprint: string;
206
+ nonce: string;
207
+ createdAt: number;
208
+ }
209
+
210
+ const assertUid = (): number => {
211
+ if (typeof process.getuid !== 'function') {
212
+ throw new Error(`${currentCliName} upgrade requires a POSIX effective user identity.`);
213
+ }
214
+ const uid = process.getuid();
215
+ if (!Number.isSafeInteger(uid) || uid < 0) {
216
+ throw new Error('The effective user identity is invalid.');
217
+ }
218
+ return uid;
219
+ };
220
+
221
+ const assertToken = (tokenArg: unknown): string => {
222
+ if (typeof tokenArg !== 'string' || !upgradeTokenPattern.test(tokenArg)) {
223
+ throw new Error('The upgrade coordination token is invalid.');
224
+ }
225
+ return tokenArg;
226
+ };
227
+
228
+ const hashToken = (tokenArg: unknown): string => plugins.crypto
229
+ .createHash('sha256')
230
+ .update(assertToken(tokenArg), 'utf8')
231
+ .digest('hex');
232
+
233
+ const tokensEqual = (leftArg: string, rightArg: string): boolean => {
234
+ const left = Buffer.from(leftArg, 'utf8');
235
+ const right = Buffer.from(rightArg, 'utf8');
236
+ return left.byteLength === right.byteLength && plugins.crypto.timingSafeEqual(left, right);
237
+ };
238
+
239
+ const assertPort = (portArg: unknown): number => {
240
+ if (!Number.isSafeInteger(portArg) || (portArg as number) < 1 || (portArg as number) > 65_535) {
241
+ throw new Error('The upgrade coordination port is invalid.');
242
+ }
243
+ return portArg as number;
244
+ };
245
+
246
+ const exactKeys = (valueArg: Record<string, unknown>, expectedArg: string[]): boolean => {
247
+ const actual = Object.keys(valueArg).sort();
248
+ const expected = [...expectedArg].sort();
249
+ return actual.length === expected.length
250
+ && actual.every((key, index) => key === expected[index]);
251
+ };
252
+
253
+ const assertPrivateDirectory = async (directoryArg: string): Promise<void> => {
254
+ const stats = await plugins.fs.promises.lstat(directoryArg);
255
+ if (!stats.isDirectory() || stats.isSymbolicLink()) {
256
+ throw new Error(`Upgrade coordination path must be a real directory: ${directoryArg}`);
257
+ }
258
+ if (typeof process.getuid === 'function' && stats.uid !== process.getuid()) {
259
+ throw new Error(`Upgrade coordination path is not owned by the current user: ${directoryArg}`);
260
+ }
261
+ if ((stats.mode & 0o077) !== 0) {
262
+ throw new Error(`Upgrade coordination path must not be group or world accessible: ${directoryArg}`);
263
+ }
264
+ };
265
+
266
+ const lstatIfPresent = async (filePathArg: string): Promise<plugins.fs.Stats | undefined> => {
267
+ try {
268
+ return await plugins.fs.promises.lstat(filePathArg);
269
+ } catch (errorArg) {
270
+ if ((errorArg as NodeJS.ErrnoException).code === 'ENOENT') return undefined;
271
+ throw errorArg;
272
+ }
273
+ };
274
+
275
+ const ensurePrivateDirectory = async (directoryArg: string): Promise<void> => {
276
+ try {
277
+ await plugins.fs.promises.mkdir(directoryArg, { mode: 0o700 });
278
+ } catch (errorArg) {
279
+ if ((errorArg as NodeJS.ErrnoException).code !== 'EEXIST') throw errorArg;
280
+ }
281
+ await assertPrivateDirectory(directoryArg);
282
+ };
283
+
284
+ const syncDirectory = async (directoryArg: string): Promise<void> => {
285
+ const handle = await plugins.fs.promises.open(directoryArg, plugins.fs.constants.O_RDONLY);
286
+ try {
287
+ await handle.sync();
288
+ } finally {
289
+ await handle.close();
290
+ }
291
+ };
292
+
293
+ const writePrivateJson = async (
294
+ filePathArg: string,
295
+ valueArg: unknown,
296
+ exclusiveArg = true,
297
+ ): Promise<void> => {
298
+ const flags = plugins.fs.constants.O_WRONLY
299
+ | plugins.fs.constants.O_CREAT
300
+ | plugins.fs.constants.O_NOFOLLOW
301
+ | (exclusiveArg ? plugins.fs.constants.O_EXCL : plugins.fs.constants.O_TRUNC);
302
+ const handle = await plugins.fs.promises.open(filePathArg, flags, 0o600);
303
+ try {
304
+ await handle.writeFile(`${JSON.stringify(valueArg)}\n`, 'utf8');
305
+ await handle.sync();
306
+ } finally {
307
+ await handle.close();
308
+ }
309
+ await syncDirectory(plugins.path.dirname(filePathArg));
310
+ };
311
+
312
+ const readPrivateJson = async (filePathArg: string): Promise<unknown> => {
313
+ const handle = await plugins.fs.promises.open(
314
+ filePathArg,
315
+ plugins.fs.constants.O_RDONLY | plugins.fs.constants.O_NOFOLLOW,
316
+ );
317
+ try {
318
+ const stats = await handle.stat();
319
+ if (!stats.isFile() || stats.size < 2 || stats.size > maximumMetadataBytes) {
320
+ throw new Error(`Upgrade coordination metadata is not a bounded regular file: ${filePathArg}`);
321
+ }
322
+ if (typeof process.getuid === 'function' && stats.uid !== process.getuid()) {
323
+ throw new Error(`Upgrade coordination metadata is not owned by the current user: ${filePathArg}`);
324
+ }
325
+ if ((stats.mode & 0o077) !== 0) {
326
+ throw new Error(`Upgrade coordination metadata is not private: ${filePathArg}`);
327
+ }
328
+ try {
329
+ return JSON.parse(await handle.readFile('utf8')) as unknown;
330
+ } catch (errorArg) {
331
+ const error = new Error(`Upgrade coordination metadata is malformed: ${filePathArg}`, {
332
+ cause: errorArg,
333
+ }) as NodeJS.ErrnoException;
334
+ error.code = 'HCON_INVALID_JSON';
335
+ throw error;
336
+ }
337
+ } finally {
338
+ await handle.close();
339
+ }
340
+ };
341
+
342
+ const parseOwner = (valueArg: unknown): IUpgradeProcessOwner => {
343
+ if (!valueArg || typeof valueArg !== 'object' || Array.isArray(valueArg)) {
344
+ throw new Error('Upgrade owner metadata is malformed.');
345
+ }
346
+ const value = valueArg as Record<string, unknown>;
347
+ if (!exactKeys(value, [
348
+ 'version',
349
+ 'kind',
350
+ 'uid',
351
+ 'tokenHash',
352
+ 'pid',
353
+ 'processGroupId',
354
+ 'fingerprint',
355
+ 'cliPath',
356
+ 'command',
357
+ 'createdAt',
358
+ ])) {
359
+ throw new Error('Upgrade owner metadata has unexpected fields.');
360
+ }
361
+ if (
362
+ value.version !== coordinationVersion
363
+ || (value.kind !== 'upgrade' && value.kind !== 'start')
364
+ || !Number.isSafeInteger(value.uid)
365
+ || (value.uid as number) < 0
366
+ || !Number.isSafeInteger(value.pid)
367
+ || (value.pid as number) < 2
368
+ || !Number.isSafeInteger(value.processGroupId)
369
+ || (value.processGroupId as number) < 2
370
+ || !Number.isSafeInteger(value.createdAt)
371
+ || (value.createdAt as number) < 1
372
+ || typeof value.fingerprint !== 'string'
373
+ || value.fingerprint.length < 8
374
+ || value.fingerprint.length > 256
375
+ || typeof value.cliPath !== 'string'
376
+ || !plugins.path.isAbsolute(value.cliPath)
377
+ || (value.command !== '__serve'
378
+ && value.command !== '__upgrade-worker'
379
+ && value.command !== 'start'
380
+ && value.command !== 'upgrade'
381
+ && value.command !== 'foreground'
382
+ && value.command !== 'temp-password')
383
+ ) {
384
+ throw new Error('Upgrade owner metadata is invalid.');
385
+ }
386
+ if (typeof value.tokenHash !== 'string' || !upgradeTokenHashPattern.test(value.tokenHash)) {
387
+ throw new Error('Upgrade owner metadata token binding is invalid.');
388
+ }
389
+ if (
390
+ (value.kind === 'upgrade'
391
+ && value.command !== '__serve'
392
+ && value.command !== '__upgrade-worker'
393
+ && value.command !== 'upgrade')
394
+ || (value.kind === 'start' && value.command === '__upgrade-worker')
395
+ ) {
396
+ throw new Error('Upgrade owner metadata kind and command do not match.');
397
+ }
398
+ return value as unknown as IUpgradeProcessOwner;
399
+ };
400
+
401
+ const parseExpectedController = (valueArg: unknown): IUpgradeExpectedController => {
402
+ if (!valueArg || typeof valueArg !== 'object' || Array.isArray(valueArg)) {
403
+ throw new Error('The expected controller identity is malformed.');
404
+ }
405
+ const value = valueArg as Record<string, unknown>;
406
+ if (
407
+ !exactKeys(value, ['pid', 'processGroupId', 'processFingerprint'])
408
+ || !Number.isSafeInteger(value.pid)
409
+ || (value.pid as number) < 2
410
+ || !Number.isSafeInteger(value.processGroupId)
411
+ || (value.processGroupId as number) < 2
412
+ || typeof value.processFingerprint !== 'string'
413
+ || value.processFingerprint.length < 8
414
+ || value.processFingerprint.length > 256
415
+ ) {
416
+ throw new Error('The expected controller identity is invalid.');
417
+ }
418
+ return value as unknown as IUpgradeExpectedController;
419
+ };
420
+
421
+ const assertBoundedText = (valueArg: unknown, nameArg: string, maximumBytesArg: number): string => {
422
+ if (
423
+ typeof valueArg !== 'string'
424
+ || valueArg.length === 0
425
+ || Buffer.byteLength(valueArg, 'utf8') > maximumBytesArg
426
+ ) {
427
+ throw new Error(`${nameArg} is invalid.`);
428
+ }
429
+ return valueArg;
430
+ };
431
+
432
+ const assertRuntimeId = (valueArg: unknown): IControllerRuntimeId => {
433
+ if (!valueArg || typeof valueArg !== 'object' || Array.isArray(valueArg)) {
434
+ throw new Error('The upgrade session identity is malformed.');
435
+ }
436
+ const value = valueArg as Record<string, unknown>;
437
+ if (
438
+ !exactKeys(value, ['harnessId', 'nativeId'])
439
+ || (value.harnessId !== 'opencode' && value.harnessId !== 'flex')
440
+ ) throw new Error('The upgrade session identity is invalid.');
441
+ return {
442
+ harnessId: value.harnessId,
443
+ nativeId: assertBoundedText(value.nativeId, 'The upgrade native session ID', 512),
444
+ };
445
+ };
446
+
447
+ const sessionTransitionStates: ReadonlySet<string> = new Set([
448
+ 'pending',
449
+ 'submitting',
450
+ 'accepted',
451
+ 'paused',
452
+ 'outcomeUnknown',
453
+ 'failed',
454
+ ]);
455
+
456
+ const parseUpgradeSessionState = (valueArg: unknown): IUpgradeSessionState => {
457
+ if (!valueArg || typeof valueArg !== 'object' || Array.isArray(valueArg)) {
458
+ throw new Error('Upgrade session state is malformed.');
459
+ }
460
+ const value = valueArg as Record<string, unknown>;
461
+ if (!exactKeys(value, [
462
+ 'projectId',
463
+ 'sessionId',
464
+ 'pauseState',
465
+ ...(value.pauseQueueId === undefined ? [] : ['pauseQueueId']),
466
+ ...(value.cleanupState === undefined ? [] : ['cleanupState']),
467
+ ...(value.continueState === undefined ? [] : ['continueState']),
468
+ ...(value.error === undefined ? [] : ['error']),
469
+ ])) throw new Error('Upgrade session state has unexpected fields.');
470
+ if (
471
+ typeof value.pauseState !== 'string'
472
+ || !sessionTransitionStates.has(value.pauseState)
473
+ || (value.cleanupState !== undefined && (
474
+ typeof value.cleanupState !== 'string' || !sessionTransitionStates.has(value.cleanupState)
475
+ ))
476
+ || (value.continueState !== undefined && (
477
+ typeof value.continueState !== 'string' || !sessionTransitionStates.has(value.continueState)
478
+ ))
479
+ ) throw new Error('Upgrade session transition state is invalid.');
480
+ const sessionId = assertRuntimeId(value.sessionId);
481
+ if (value.pauseQueueId !== undefined && sessionId.harnessId !== 'flex') {
482
+ throw new Error('Only Flex upgrade sessions may retain a pause queue ID.');
483
+ }
484
+ return {
485
+ projectId: assertBoundedText(value.projectId, 'The upgrade project ID', 512),
486
+ sessionId,
487
+ pauseState: value.pauseState as TUpgradeSessionTransitionState,
488
+ ...(value.pauseQueueId === undefined
489
+ ? {}
490
+ : { pauseQueueId: assertBoundedText(value.pauseQueueId, 'The Flex pause queue ID', 512) }),
491
+ ...(value.cleanupState === undefined
492
+ ? {}
493
+ : { cleanupState: value.cleanupState as TUpgradeSessionTransitionState }),
494
+ ...(value.continueState === undefined
495
+ ? {}
496
+ : { continueState: value.continueState as TUpgradeSessionTransitionState }),
497
+ ...(value.error === undefined
498
+ ? {}
499
+ : { error: assertBoundedText(value.error, 'The upgrade session error', 2_048) }),
500
+ };
501
+ };
502
+
503
+ const transactionPhases: ReadonlySet<string> = new Set([
504
+ 'starting',
505
+ 'checking',
506
+ 'preparing',
507
+ 'pausing',
508
+ 'stopping',
509
+ 'installing',
510
+ 'restarting',
511
+ 'continuing',
512
+ 'completed',
513
+ 'failed',
514
+ ]);
515
+
516
+ const transactionPhaseOrder: Record<TUpgradeTransaction['phase'], number> = {
517
+ starting: 0,
518
+ checking: 1,
519
+ preparing: 2,
520
+ pausing: 3,
521
+ stopping: 4,
522
+ installing: 5,
523
+ restarting: 6,
524
+ continuing: 7,
525
+ completed: 8,
526
+ failed: 8,
527
+ };
528
+
529
+ const parseUpgradeTransactionV2 = (valueArg: unknown): IUpgradeTransactionV2 => {
530
+ if (!valueArg || typeof valueArg !== 'object' || Array.isArray(valueArg)) {
531
+ throw new Error('Upgrade transaction metadata is malformed.');
532
+ }
533
+ const value = valueArg as Record<string, unknown>;
534
+ if (!exactKeys(value, [
535
+ 'version', 'tokenHash', 'revision', 'port', 'sourceVersion', 'controllerWasRunning',
536
+ 'continueSessions', 'gracePeriodMs', 'phase', 'message', 'createdAt', 'updatedAt',
537
+ 'phaseStartedAt', 'sessions',
538
+ ...(value.targetVersion === undefined ? [] : ['targetVersion']),
539
+ ...(value.targetStartupInvoked === undefined ? [] : ['targetStartupInvoked']),
540
+ ...(value.preparationAcceptedAt === undefined ? [] : ['preparationAcceptedAt']),
541
+ ...(value.preparationDeadlineAt === undefined ? [] : ['preparationDeadlineAt']),
542
+ ...(value.preparationCompletedAt === undefined ? [] : ['preparationCompletedAt']),
543
+ ...(value.retainedUntil === undefined ? [] : ['retainedUntil']),
544
+ ...(value.worker === undefined ? [] : ['worker']),
545
+ ...(value.controller === undefined ? [] : ['controller']),
546
+ ...(value.terminal === undefined ? [] : ['terminal']),
547
+ ])) throw new Error('Upgrade transaction metadata has unexpected fields.');
548
+ if (
549
+ value.version !== coordinationVersion
550
+ || typeof value.tokenHash !== 'string'
551
+ || !upgradeTokenHashPattern.test(value.tokenHash)
552
+ || !Number.isSafeInteger(value.revision)
553
+ || (value.revision as number) < 0
554
+ || typeof value.controllerWasRunning !== 'boolean'
555
+ || (value.targetStartupInvoked !== undefined && value.targetStartupInvoked !== true)
556
+ || typeof value.continueSessions !== 'boolean'
557
+ || !Number.isSafeInteger(value.gracePeriodMs)
558
+ || (value.gracePeriodMs as number) < 1_000
559
+ || (value.gracePeriodMs as number) > 60 * 60 * 1000
560
+ || ((value.preparationAcceptedAt === undefined)
561
+ !== (value.preparationDeadlineAt === undefined))
562
+ || (value.preparationAcceptedAt !== undefined && (
563
+ !Number.isSafeInteger(value.preparationAcceptedAt)
564
+ || (value.preparationAcceptedAt as number) < (value.createdAt as number)
565
+ || (value.preparationAcceptedAt as number) > (value.updatedAt as number)
566
+ || !Number.isSafeInteger(value.preparationDeadlineAt)
567
+ || (value.preparationDeadlineAt as number)
568
+ !== (value.preparationAcceptedAt as number) + (value.gracePeriodMs as number)
569
+ ))
570
+ || (value.preparationCompletedAt !== undefined && (
571
+ value.preparationAcceptedAt === undefined
572
+ || !Number.isSafeInteger(value.preparationCompletedAt)
573
+ || (value.preparationCompletedAt as number) < (value.preparationAcceptedAt as number)
574
+ || (value.preparationCompletedAt as number) > (value.preparationDeadlineAt as number)
575
+ || typeof value.phase !== 'string'
576
+ || (transactionPhaseOrder[value.phase as TUpgradeTransaction['phase']] ?? -1)
577
+ < transactionPhaseOrder.stopping
578
+ ))
579
+ || typeof value.phase !== 'string'
580
+ || !transactionPhases.has(value.phase)
581
+ || !Number.isSafeInteger(value.createdAt)
582
+ || (value.createdAt as number) < 1
583
+ || !Number.isSafeInteger(value.updatedAt)
584
+ || (value.updatedAt as number) < (value.createdAt as number)
585
+ || !Number.isSafeInteger(value.phaseStartedAt)
586
+ || (value.phaseStartedAt as number) < (value.createdAt as number)
587
+ || (value.phaseStartedAt as number) > (value.updatedAt as number)
588
+ || (value.retainedUntil !== undefined && (
589
+ !Number.isSafeInteger(value.retainedUntil)
590
+ || (value.retainedUntil as number) < (value.updatedAt as number)
591
+ ))
592
+ || !Array.isArray(value.sessions)
593
+ || value.sessions.length > 64
594
+ ) throw new Error('Upgrade transaction metadata is invalid.');
595
+ let worker: IUpgradeTransactionV2['worker'];
596
+ if (value.worker !== undefined) {
597
+ if (!value.worker || typeof value.worker !== 'object' || Array.isArray(value.worker)) {
598
+ throw new Error('Upgrade worker metadata is malformed.');
599
+ }
600
+ const rawWorker = value.worker as Record<string, unknown>;
601
+ if (
602
+ !exactKeys(rawWorker, [
603
+ 'pid', 'processGroupId', 'fingerprint', 'cliPath',
604
+ ...(rawWorker.logFilePath === undefined ? [] : ['logFilePath']),
605
+ ])
606
+ || !Number.isSafeInteger(rawWorker.pid)
607
+ || (rawWorker.pid as number) < 2
608
+ || !Number.isSafeInteger(rawWorker.processGroupId)
609
+ || (rawWorker.processGroupId as number) < 2
610
+ || typeof rawWorker.cliPath !== 'string'
611
+ || !plugins.path.isAbsolute(rawWorker.cliPath)
612
+ ) throw new Error('Upgrade worker metadata is invalid.');
613
+ worker = {
614
+ pid: rawWorker.pid as number,
615
+ processGroupId: rawWorker.processGroupId as number,
616
+ fingerprint: assertBoundedText(rawWorker.fingerprint, 'The upgrade worker fingerprint', 256),
617
+ cliPath: rawWorker.cliPath,
618
+ ...(rawWorker.logFilePath === undefined
619
+ ? {}
620
+ : {
621
+ logFilePath: assertBoundedText(
622
+ rawWorker.logFilePath,
623
+ 'The upgrade worker log path',
624
+ 4_096,
625
+ ),
626
+ }),
627
+ };
628
+ }
629
+ let terminal: IUpgradeTransactionV2['terminal'];
630
+ if (value.terminal !== undefined) {
631
+ if (!value.terminal || typeof value.terminal !== 'object' || Array.isArray(value.terminal)) {
632
+ throw new Error('Upgrade terminal metadata is malformed.');
633
+ }
634
+ const rawTerminal = value.terminal as Record<string, unknown>;
635
+ if (
636
+ !exactKeys(rawTerminal, ['success', ...(rawTerminal.error === undefined ? [] : ['error'])])
637
+ || typeof rawTerminal.success !== 'boolean'
638
+ ) throw new Error('Upgrade terminal metadata is invalid.');
639
+ terminal = {
640
+ success: rawTerminal.success,
641
+ ...(rawTerminal.error === undefined
642
+ ? {}
643
+ : { error: assertBoundedText(rawTerminal.error, 'The upgrade terminal error', 4_096) }),
644
+ };
645
+ }
646
+ let controller: IUpgradeTransactionV2['controller'];
647
+ if (value.controller !== undefined) {
648
+ if (!value.controller || typeof value.controller !== 'object' || Array.isArray(value.controller)) {
649
+ throw new Error('Upgrade controller metadata is malformed.');
650
+ }
651
+ const rawController = value.controller as Record<string, unknown>;
652
+ if (
653
+ !exactKeys(rawController, [
654
+ 'pid', 'processGroupId', 'fingerprint', 'cliPath', 'command',
655
+ ])
656
+ || !Number.isSafeInteger(rawController.pid)
657
+ || (rawController.pid as number) < 2
658
+ || !Number.isSafeInteger(rawController.processGroupId)
659
+ || (rawController.processGroupId as number) < 2
660
+ || typeof rawController.cliPath !== 'string'
661
+ || !plugins.path.isAbsolute(rawController.cliPath)
662
+ || (rawController.command !== '__serve' && rawController.command !== 'foreground')
663
+ ) throw new Error('Upgrade controller metadata is invalid.');
664
+ controller = {
665
+ pid: rawController.pid as number,
666
+ processGroupId: rawController.processGroupId as number,
667
+ fingerprint: assertBoundedText(
668
+ rawController.fingerprint,
669
+ 'The upgrade controller fingerprint',
670
+ 256,
671
+ ),
672
+ cliPath: rawController.cliPath,
673
+ command: rawController.command,
674
+ };
675
+ }
676
+ return {
677
+ version: coordinationVersion,
678
+ tokenHash: value.tokenHash,
679
+ revision: value.revision as number,
680
+ port: assertPort(value.port),
681
+ sourceVersion: assertBoundedText(value.sourceVersion, 'The upgrade source version', 128),
682
+ ...(value.targetVersion === undefined
683
+ ? {}
684
+ : { targetVersion: assertBoundedText(value.targetVersion, 'The upgrade target version', 128) }),
685
+ ...(value.targetStartupInvoked === true ? { targetStartupInvoked: true } : {}),
686
+ controllerWasRunning: value.controllerWasRunning,
687
+ continueSessions: value.continueSessions,
688
+ gracePeriodMs: value.gracePeriodMs as number,
689
+ ...(value.preparationAcceptedAt === undefined
690
+ ? {}
691
+ : {
692
+ preparationAcceptedAt: value.preparationAcceptedAt as number,
693
+ preparationDeadlineAt: value.preparationDeadlineAt as number,
694
+ }),
695
+ ...(value.preparationCompletedAt === undefined
696
+ ? {}
697
+ : { preparationCompletedAt: value.preparationCompletedAt as number }),
698
+ phase: value.phase as TUpgradeTransaction['phase'],
699
+ message: assertBoundedText(value.message, 'The upgrade transaction message', 4_096),
700
+ createdAt: value.createdAt as number,
701
+ phaseStartedAt: value.phaseStartedAt as number,
702
+ updatedAt: value.updatedAt as number,
703
+ ...(value.retainedUntil === undefined
704
+ ? {}
705
+ : { retainedUntil: value.retainedUntil as number }),
706
+ ...(worker ? { worker } : {}),
707
+ ...(controller ? { controller } : {}),
708
+ sessions: value.sessions.map(parseUpgradeSessionState),
709
+ ...(terminal ? { terminal } : {}),
710
+ };
711
+ };
712
+
713
+ const parseUpgradeTransactionV3 = (valueArg: unknown): IUpgradeTransactionV3 => {
714
+ if (!valueArg || typeof valueArg !== 'object' || Array.isArray(valueArg)) {
715
+ throw new Error('Upgrade transaction metadata is malformed.');
716
+ }
717
+ const value = valueArg as Record<string, unknown>;
718
+ if (!exactKeys(value, [
719
+ 'version', 'tokenHash', 'revision', 'port',
720
+ 'sourcePackageName', 'sourceVersion', 'sourceManagementVersion',
721
+ 'sourceCliName', 'sourceCliRelativePath',
722
+ 'targetPackageName', 'targetVersion', 'targetManagementVersion',
723
+ 'targetCliName', 'targetCliRelativePath',
724
+ 'controllerWasRunning', 'continueSessions', 'gracePeriodMs', 'phase', 'message',
725
+ 'createdAt', 'updatedAt', 'phaseStartedAt', 'sessions',
726
+ ...(value.packageTransitionStarted === undefined ? [] : ['packageTransitionStarted']),
727
+ ...(value.groupedTargetVerified === undefined ? [] : ['groupedTargetVerified']),
728
+ ...(value.targetPackageCommitStarted === undefined ? [] : ['targetPackageCommitStarted']),
729
+ ...(value.targetPackageCommitted === undefined ? [] : ['targetPackageCommitted']),
730
+ ...(value.targetStartupInvoked === undefined ? [] : ['targetStartupInvoked']),
731
+ ...(value.preparationAcceptedAt === undefined ? [] : ['preparationAcceptedAt']),
732
+ ...(value.preparationDeadlineAt === undefined ? [] : ['preparationDeadlineAt']),
733
+ ...(value.preparationCompletedAt === undefined ? [] : ['preparationCompletedAt']),
734
+ ...(value.retainedUntil === undefined ? [] : ['retainedUntil']),
735
+ ...(value.worker === undefined ? [] : ['worker']),
736
+ ...(value.controller === undefined ? [] : ['controller']),
737
+ ...(value.terminal === undefined ? [] : ['terminal']),
738
+ ])) throw new Error('Upgrade transaction metadata has unexpected fields.');
739
+ if (
740
+ value.version !== upgradePackageTransitionTransactionVersion
741
+ || value.sourcePackageName !== upgradePackageTransitionSource.packageName
742
+ || value.sourceVersion !== upgradePackageTransitionSource.version
743
+ || value.sourceManagementVersion !== upgradePackageTransitionSource.managementVersion
744
+ || value.sourceCliName !== upgradePackageTransitionSource.cliName
745
+ || value.sourceCliRelativePath !== upgradePackageTransitionSource.cliRelativePath
746
+ || value.targetPackageName !== upgradePackageTransitionTarget.packageName
747
+ || value.targetVersion !== upgradePackageTransitionTarget.version
748
+ || value.targetManagementVersion !== upgradePackageTransitionTarget.managementVersion
749
+ || value.targetCliName !== upgradePackageTransitionTarget.cliName
750
+ || value.targetCliRelativePath !== upgradePackageTransitionTarget.cliRelativePath
751
+ || (value.packageTransitionStarted !== undefined && value.packageTransitionStarted !== true)
752
+ || (value.groupedTargetVerified !== undefined && value.groupedTargetVerified !== true)
753
+ || (value.targetPackageCommitStarted !== undefined
754
+ && value.targetPackageCommitStarted !== true)
755
+ || (value.targetPackageCommitted !== undefined && value.targetPackageCommitted !== true)
756
+ || (value.targetStartupInvoked !== undefined && value.targetStartupInvoked !== true)
757
+ ) throw new Error('Upgrade package-transition identity or checkpoint metadata is invalid.');
758
+ if (
759
+ (value.groupedTargetVerified === true && value.packageTransitionStarted !== true)
760
+ || (value.targetPackageCommitStarted === true && value.groupedTargetVerified !== true)
761
+ || (value.targetPackageCommitted === true && value.targetPackageCommitStarted !== true)
762
+ || (value.targetStartupInvoked === true && value.targetPackageCommitted !== true)
763
+ || (value.targetStartupInvoked === true && value.controllerWasRunning !== true)
764
+ ) throw new Error('Upgrade package-transition checkpoints violate their implication chain.');
765
+
766
+ const compatible = parseUpgradeTransactionV2({
767
+ version: coordinationVersion,
768
+ tokenHash: value.tokenHash,
769
+ revision: value.revision,
770
+ port: value.port,
771
+ sourceVersion: value.sourceVersion,
772
+ targetVersion: value.targetVersion,
773
+ ...(value.targetStartupInvoked === true ? { targetStartupInvoked: true } : {}),
774
+ controllerWasRunning: value.controllerWasRunning,
775
+ continueSessions: value.continueSessions,
776
+ gracePeriodMs: value.gracePeriodMs,
777
+ ...(value.preparationAcceptedAt === undefined
778
+ ? {}
779
+ : {
780
+ preparationAcceptedAt: value.preparationAcceptedAt,
781
+ preparationDeadlineAt: value.preparationDeadlineAt,
782
+ }),
783
+ ...(value.preparationCompletedAt === undefined
784
+ ? {}
785
+ : { preparationCompletedAt: value.preparationCompletedAt }),
786
+ phase: value.phase,
787
+ message: value.message,
788
+ createdAt: value.createdAt,
789
+ phaseStartedAt: value.phaseStartedAt,
790
+ updatedAt: value.updatedAt,
791
+ ...(value.retainedUntil === undefined ? {} : { retainedUntil: value.retainedUntil }),
792
+ ...(value.worker === undefined ? {} : { worker: value.worker }),
793
+ ...(value.controller === undefined ? {} : { controller: value.controller }),
794
+ sessions: value.sessions,
795
+ ...(value.terminal === undefined ? {} : { terminal: value.terminal }),
796
+ });
797
+ if (
798
+ compatible.terminal?.success === true
799
+ && (
800
+ value.targetPackageCommitted !== true
801
+ || (compatible.controllerWasRunning && value.targetStartupInvoked !== true)
802
+ || (!compatible.controllerWasRunning && value.targetStartupInvoked === true)
803
+ )
804
+ ) throw new Error('A successful package transition is missing its terminal checkpoint.');
805
+ return {
806
+ ...compatible,
807
+ version: upgradePackageTransitionTransactionVersion,
808
+ sourcePackageName: upgradePackageTransitionSource.packageName,
809
+ sourceVersion: upgradePackageTransitionSource.version,
810
+ sourceManagementVersion: upgradePackageTransitionSource.managementVersion,
811
+ sourceCliName: upgradePackageTransitionSource.cliName,
812
+ sourceCliRelativePath: upgradePackageTransitionSource.cliRelativePath,
813
+ targetPackageName: upgradePackageTransitionTarget.packageName,
814
+ targetVersion: upgradePackageTransitionTarget.version,
815
+ targetManagementVersion: upgradePackageTransitionTarget.managementVersion,
816
+ targetCliName: upgradePackageTransitionTarget.cliName,
817
+ targetCliRelativePath: upgradePackageTransitionTarget.cliRelativePath,
818
+ ...(value.packageTransitionStarted === true ? { packageTransitionStarted: true } : {}),
819
+ ...(value.groupedTargetVerified === true ? { groupedTargetVerified: true } : {}),
820
+ ...(value.targetPackageCommitStarted === true ? { targetPackageCommitStarted: true } : {}),
821
+ ...(value.targetPackageCommitted === true ? { targetPackageCommitted: true } : {}),
822
+ ...(value.targetStartupInvoked === true ? { targetStartupInvoked: true } : {}),
823
+ };
824
+ };
825
+
826
+ const parseUpgradeTransaction = (valueArg: unknown): TUpgradeTransaction => {
827
+ if (!valueArg || typeof valueArg !== 'object' || Array.isArray(valueArg)) {
828
+ throw new Error('Upgrade transaction metadata is malformed.');
829
+ }
830
+ return (valueArg as Record<string, unknown>).version === coordinationVersion
831
+ ? parseUpgradeTransactionV2(valueArg)
832
+ : parseUpgradeTransactionV3(valueArg);
833
+ };
834
+
835
+ const parseTransactionMutationOwner = (valueArg: unknown): IUpgradeTransactionMutationOwner => {
836
+ if (!valueArg || typeof valueArg !== 'object' || Array.isArray(valueArg)) {
837
+ throw new Error('The upgrade transaction mutation owner is malformed.');
838
+ }
839
+ const value = valueArg as Record<string, unknown>;
840
+ if (
841
+ !exactKeys(value, [
842
+ 'version', 'uid', 'tokenHash', 'pid', 'fingerprint', 'nonce', 'createdAt',
843
+ ])
844
+ || value.version !== coordinationVersion
845
+ || !Number.isSafeInteger(value.uid)
846
+ || (value.uid as number) < 0
847
+ || typeof value.tokenHash !== 'string'
848
+ || !upgradeTokenHashPattern.test(value.tokenHash)
849
+ || !Number.isSafeInteger(value.pid)
850
+ || (value.pid as number) < 2
851
+ || typeof value.nonce !== 'string'
852
+ || !/^[a-f0-9]{32}$/.test(value.nonce)
853
+ || !Number.isSafeInteger(value.createdAt)
854
+ || (value.createdAt as number) < 1
855
+ ) throw new Error('The upgrade transaction mutation owner is invalid.');
856
+ return {
857
+ version: coordinationVersion,
858
+ uid: value.uid as number,
859
+ tokenHash: value.tokenHash,
860
+ pid: value.pid as number,
861
+ fingerprint: assertBoundedText(
862
+ value.fingerprint,
863
+ 'The upgrade transaction mutation fingerprint',
864
+ 256,
865
+ ),
866
+ nonce: value.nonce,
867
+ createdAt: value.createdAt as number,
868
+ };
869
+ };
870
+
871
+ const parseOwnershipMutationGuardOwner = (
872
+ valueArg: unknown,
873
+ ): IUpgradeOwnershipMutationGuardOwner => {
874
+ if (!valueArg || typeof valueArg !== 'object' || Array.isArray(valueArg)) {
875
+ throw new Error('The upgrade ownership mutation guard is malformed.');
876
+ }
877
+ const value = valueArg as Record<string, unknown>;
878
+ if (
879
+ !exactKeys(value, [
880
+ 'version', 'uid', 'targetHash', 'pid', 'fingerprint', 'nonce', 'createdAt',
881
+ ])
882
+ || value.version !== coordinationVersion
883
+ || !Number.isSafeInteger(value.uid)
884
+ || (value.uid as number) < 0
885
+ || typeof value.targetHash !== 'string'
886
+ || !upgradeTokenHashPattern.test(value.targetHash)
887
+ || !Number.isSafeInteger(value.pid)
888
+ || (value.pid as number) < 2
889
+ || typeof value.nonce !== 'string'
890
+ || !/^[a-f0-9]{32}$/.test(value.nonce)
891
+ || !Number.isSafeInteger(value.createdAt)
892
+ || (value.createdAt as number) < 1
893
+ ) throw new Error('The upgrade ownership mutation guard is invalid.');
894
+ return {
895
+ version: coordinationVersion,
896
+ uid: value.uid as number,
897
+ targetHash: value.targetHash,
898
+ pid: value.pid as number,
899
+ fingerprint: assertBoundedText(
900
+ value.fingerprint,
901
+ 'The upgrade ownership mutation guard fingerprint',
902
+ 256,
903
+ ),
904
+ nonce: value.nonce,
905
+ createdAt: value.createdAt as number,
906
+ };
907
+ };
908
+
909
+ export const upgradeTransactionIsStalled = (
910
+ transactionArg: TUpgradeTransaction,
911
+ nowArg = Date.now(),
912
+ ): boolean => {
913
+ if (transactionArg.terminal) return false;
914
+ if (
915
+ transactionArg.preparationDeadlineAt !== undefined
916
+ && transactionArg.preparationCompletedAt === undefined
917
+ && (transactionArg.phase === 'preparing' || transactionArg.phase === 'pausing')
918
+ ) return nowArg > transactionArg.preparationDeadlineAt + 60_000;
919
+ const maximumPhaseDurationMs: Record<TUpgradeTransaction['phase'], number> = {
920
+ starting: 2 * 60 * 1000,
921
+ checking: 3 * 60 * 1000,
922
+ preparing: transactionArg.gracePeriodMs + 60_000,
923
+ pausing: transactionArg.gracePeriodMs + 60_000,
924
+ stopping: 90_000,
925
+ installing: 22 * 60 * 1000,
926
+ restarting: 3 * 60 * 1000,
927
+ continuing: 2 * 60 * 1000,
928
+ completed: Number.MAX_SAFE_INTEGER,
929
+ failed: Number.MAX_SAFE_INTEGER,
930
+ };
931
+ return nowArg - transactionArg.phaseStartedAt > maximumPhaseDurationMs[transactionArg.phase];
932
+ };
933
+
934
+ const ownerIsLive = async (ownerArg: IUpgradeProcessOwner): Promise<boolean> => {
935
+ if (ownerArg.uid !== assertUid()) return false;
936
+ const identity = await readControllerProcessIdentity(ownerArg.pid);
937
+ if (identity && identity.fingerprint === ownerArg.fingerprint) {
938
+ return await processIdentityHasCliCommand(
939
+ identity,
940
+ ownerArg.cliPath,
941
+ ownerArg.command,
942
+ { allowMissingAbsolutePath: true },
943
+ );
944
+ }
945
+ if (ownerArg.kind !== 'upgrade' || ownerArg.processGroupId !== ownerArg.pid) return false;
946
+ return (await readProcessGroupMemberPids(ownerArg.processGroupId))
947
+ .some((processIdArg) => processIdArg !== ownerArg.pid);
948
+ };
949
+
950
+ const ownerForCurrentProcess = async (
951
+ kindArg: TUpgradeOwnerKind,
952
+ tokenArg: string,
953
+ cliPathArg: string,
954
+ commandArg: IUpgradeProcessOwner['command'],
955
+ ): Promise<IUpgradeProcessOwner> => {
956
+ const identity = await readControllerProcessIdentity(process.pid);
957
+ if (!identity) throw new Error('Unable to establish upgrade process ownership.');
958
+ return {
959
+ version: coordinationVersion,
960
+ kind: kindArg,
961
+ uid: assertUid(),
962
+ tokenHash: hashToken(tokenArg),
963
+ pid: process.pid,
964
+ processGroupId: identity.processGroupId,
965
+ fingerprint: identity.fingerprint,
966
+ cliPath: cliPathArg,
967
+ command: commandArg,
968
+ createdAt: Date.now(),
969
+ };
970
+ };
971
+
972
+ const coordinationPathHash = (valueArg: string): string => plugins.crypto
973
+ .createHash('sha256')
974
+ .update(valueArg)
975
+ .digest('hex')
976
+ .slice(0, 20);
977
+
978
+ const ownershipTargetHash = (valueArg: string): string => plugins.crypto
979
+ .createHash('sha256')
980
+ .update(valueArg)
981
+ .digest('hex');
982
+
983
+ interface IOwnedDirectoryRemovalExpectation {
984
+ identity: { dev: number; ino: number };
985
+ assertOwner?: (valueArg: unknown) => void;
986
+ assertRemovalAuthorized?: () => void;
987
+ }
988
+
989
+ const sameOwnedDirectoryIdentity = (
990
+ statsArg: plugins.fs.Stats,
991
+ identityArg: IOwnedDirectoryRemovalExpectation['identity'],
992
+ ): boolean => statsArg.dev === identityArg.dev && statsArg.ino === identityArg.ino;
993
+
994
+ const exactPrimitiveRecordMatches = (leftArg: object, rightArg: object): boolean => {
995
+ const left = leftArg as Record<string, unknown>;
996
+ const right = rightArg as Record<string, unknown>;
997
+ const keys = Object.keys(left);
998
+ return exactKeys(right, keys) && keys.every((keyArg) => left[keyArg] === right[keyArg]);
999
+ };
1000
+
1001
+ const removeExactOwnedDirectory = async (
1002
+ directoryArg: string,
1003
+ expectationArg: IOwnedDirectoryRemovalExpectation,
1004
+ ): Promise<void> => {
1005
+ const initialStats = await plugins.fs.promises.lstat(directoryArg);
1006
+ if (!sameOwnedDirectoryIdentity(initialStats, expectationArg.identity)) {
1007
+ throw new Error('Upgrade coordination ownership changed before removal.');
1008
+ }
1009
+ const entries = await plugins.fs.promises.readdir(directoryArg, { withFileTypes: true });
1010
+ if (
1011
+ entries.length > 1
1012
+ || entries.some((entryArg) => (
1013
+ entryArg.name !== ownerFileName
1014
+ || !entryArg.isFile()
1015
+ || entryArg.isSymbolicLink()
1016
+ ))
1017
+ ) throw new Error(`Upgrade coordination owner directory contains unsafe entries: ${directoryArg}`);
1018
+ if (Boolean(entries.length) !== Boolean(expectationArg.assertOwner)) {
1019
+ throw new Error('Upgrade coordination ownership changed before removal.');
1020
+ }
1021
+ if (entries.length === 1) {
1022
+ const ownerPath = plugins.path.join(directoryArg, ownerFileName);
1023
+ const tombstonePath = plugins.path.join(
1024
+ directoryArg,
1025
+ `.owner-removing-${process.pid}-${plugins.crypto.randomBytes(8).toString('hex')}`,
1026
+ );
1027
+ expectationArg.assertRemovalAuthorized?.();
1028
+ await plugins.fs.promises.rename(ownerPath, tombstonePath);
1029
+ try {
1030
+ expectationArg.assertOwner!(await readPrivateJson(tombstonePath));
1031
+ } catch (errorArg) {
1032
+ let restoreError: unknown;
1033
+ try {
1034
+ await plugins.fs.promises.link(tombstonePath, ownerPath);
1035
+ await plugins.fs.promises.unlink(tombstonePath);
1036
+ await syncDirectory(directoryArg);
1037
+ } catch (restoreErrorArg) {
1038
+ restoreError = restoreErrorArg;
1039
+ }
1040
+ if (restoreError !== undefined) {
1041
+ throw new AggregateError(
1042
+ [errorArg, restoreError],
1043
+ 'Upgrade coordination ownership changed and could not be restored safely.',
1044
+ );
1045
+ }
1046
+ throw errorArg;
1047
+ }
1048
+ await plugins.fs.promises.unlink(tombstonePath);
1049
+ await syncDirectory(directoryArg);
1050
+ }
1051
+ const currentStats = await lstatIfPresent(directoryArg);
1052
+ if (!currentStats || !sameOwnedDirectoryIdentity(currentStats, expectationArg.identity)) return;
1053
+ try {
1054
+ await plugins.fs.promises.rmdir(directoryArg);
1055
+ } catch (errorArg) {
1056
+ const code = (errorArg as NodeJS.ErrnoException).code;
1057
+ if (code === 'ENOENT') return;
1058
+ if (code === 'ENOTEMPTY' || code === 'EEXIST') {
1059
+ throw new Error('Upgrade coordination ownership changed during removal.', {
1060
+ cause: errorArg,
1061
+ });
1062
+ }
1063
+ throw errorArg;
1064
+ }
1065
+ await syncDirectory(plugins.path.dirname(directoryArg));
1066
+ };
1067
+
1068
+ class UpgradeOwnershipMutationGuard {
1069
+ private released = false;
1070
+ private releaseTask?: Promise<void>;
1071
+
1072
+ constructor(
1073
+ public readonly directory: string,
1074
+ public readonly owner: IUpgradeOwnershipMutationGuardOwner,
1075
+ ) {}
1076
+
1077
+ public release(): Promise<void> {
1078
+ if (this.released) return Promise.resolve();
1079
+ if (!this.releaseTask) {
1080
+ let task: Promise<void>;
1081
+ task = (async () => {
1082
+ const stats = await plugins.fs.promises.lstat(this.directory);
1083
+ const owner = parseOwnershipMutationGuardOwner(await readPrivateJson(
1084
+ plugins.path.join(this.directory, ownerFileName),
1085
+ ));
1086
+ if (!exactPrimitiveRecordMatches(owner, this.owner)) {
1087
+ throw new Error('Upgrade ownership mutation guard changed unexpectedly.');
1088
+ }
1089
+ await removeExactOwnedDirectory(this.directory, {
1090
+ identity: { dev: stats.dev, ino: stats.ino },
1091
+ assertOwner: (valueArg) => {
1092
+ const removedOwner = parseOwnershipMutationGuardOwner(valueArg);
1093
+ if (!exactPrimitiveRecordMatches(removedOwner, this.owner)) {
1094
+ throw new Error('Upgrade ownership mutation guard changed during release.');
1095
+ }
1096
+ },
1097
+ });
1098
+ this.released = true;
1099
+ })().finally(() => {
1100
+ if (this.releaseTask === task && !this.released) this.releaseTask = undefined;
1101
+ });
1102
+ this.releaseTask = task;
1103
+ }
1104
+ return this.releaseTask;
1105
+ }
1106
+ }
1107
+
1108
+ class UpgradeOwnedDirectory {
1109
+ private released = false;
1110
+ private releaseTask?: Promise<void>;
1111
+
1112
+ constructor(
1113
+ public readonly directory: string,
1114
+ public readonly owner: IUpgradeProcessOwner,
1115
+ private readonly releaseOwned: (
1116
+ directoryArg: string,
1117
+ ownerArg: IUpgradeProcessOwner,
1118
+ shouldReleaseArg?: () => boolean,
1119
+ ) => Promise<void>,
1120
+ ) {}
1121
+
1122
+ public async assertOwned(): Promise<void> {
1123
+ if (this.released) throw new Error('Upgrade coordination ownership has already been released.');
1124
+ let owner: IUpgradeProcessOwner;
1125
+ try {
1126
+ owner = parseOwner(await readPrivateJson(plugins.path.join(this.directory, ownerFileName)));
1127
+ } catch (errorArg) {
1128
+ if ((errorArg as NodeJS.ErrnoException).code === 'ENOENT') {
1129
+ throw new Error('Upgrade coordination ownership changed unexpectedly.', { cause: errorArg });
1130
+ }
1131
+ throw errorArg;
1132
+ }
1133
+ if (!exactPrimitiveRecordMatches(owner, this.owner) || !await ownerIsLive(owner)) {
1134
+ throw new Error('Upgrade coordination ownership changed unexpectedly.');
1135
+ }
1136
+ }
1137
+
1138
+ public release(shouldReleaseArg?: () => boolean): Promise<void> {
1139
+ if (this.released) return Promise.resolve();
1140
+ if (!this.releaseTask) {
1141
+ let task: Promise<void>;
1142
+ task = this.releaseOwned(this.directory, this.owner, shouldReleaseArg).then(() => {
1143
+ this.released = true;
1144
+ }).finally(() => {
1145
+ if (this.releaseTask === task && !this.released) this.releaseTask = undefined;
1146
+ });
1147
+ this.releaseTask = task;
1148
+ }
1149
+ return this.releaseTask;
1150
+ }
1151
+
1152
+ public relinquishAfterTransfer(): void {
1153
+ if (this.released) throw new Error('Upgrade coordination ownership has already been released.');
1154
+ this.released = true;
1155
+ }
1156
+ }
1157
+
1158
+ export class UpgradeInstallationLock extends UpgradeOwnedDirectory {}
1159
+ export class UpgradeStartLease extends UpgradeOwnedDirectory {}
1160
+
1161
+ export class UpgradeCoordinator {
1162
+ public readonly uid: number;
1163
+ public readonly baseDirectory: string;
1164
+ public readonly upgradeLockDirectory: string;
1165
+ public readonly startLeasesDirectory: string;
1166
+ public readonly transactionsDirectory: string;
1167
+ public readonly ownershipGuardsDirectory: string;
1168
+ private initializationTask?: Promise<void>;
1169
+
1170
+ constructor(public readonly canonicalGlobalRoot: string) {
1171
+ if (!plugins.path.isAbsolute(canonicalGlobalRoot)) {
1172
+ throw new Error('The canonical pnpm global root must be absolute.');
1173
+ }
1174
+ this.uid = assertUid();
1175
+ this.baseDirectory = plugins.path.join(
1176
+ '/tmp',
1177
+ `harness-controller-upgrade-${this.uid}-${coordinationPathHash(canonicalGlobalRoot)}`,
1178
+ );
1179
+ this.upgradeLockDirectory = plugins.path.join(this.baseDirectory, 'upgrade.lock');
1180
+ this.startLeasesDirectory = plugins.path.join(this.baseDirectory, 'start-leases');
1181
+ this.transactionsDirectory = plugins.path.join(this.baseDirectory, 'transactions');
1182
+ this.ownershipGuardsDirectory = plugins.path.join(this.baseDirectory, 'ownership-guards');
1183
+ }
1184
+
1185
+ public async init(): Promise<void> {
1186
+ if (!this.initializationTask) {
1187
+ let task: Promise<void>;
1188
+ task = this.performInitialization().catch((errorArg) => {
1189
+ if (this.initializationTask === task) this.initializationTask = undefined;
1190
+ throw errorArg;
1191
+ });
1192
+ this.initializationTask = task;
1193
+ }
1194
+ await this.initializationTask;
1195
+ }
1196
+
1197
+ private async performInitialization(): Promise<void> {
1198
+ await ensurePrivateDirectory(this.baseDirectory);
1199
+ await ensurePrivateDirectory(this.startLeasesDirectory);
1200
+ await ensurePrivateDirectory(this.transactionsDirectory);
1201
+ await ensurePrivateDirectory(this.ownershipGuardsDirectory);
1202
+ for (const entry of await plugins.fs.promises.readdir(this.ownershipGuardsDirectory, {
1203
+ withFileTypes: true,
1204
+ })) {
1205
+ const guardPath = plugins.path.join(this.ownershipGuardsDirectory, entry.name);
1206
+ if (!entry.isDirectory() || !ownershipGuardDirectoryPattern.test(entry.name)) {
1207
+ throw new Error(`Upgrade ownership guard directory contains an unexpected entry: ${guardPath}`);
1208
+ }
1209
+ await assertPrivateDirectory(guardPath);
1210
+ }
1211
+ const entries = await plugins.fs.promises.readdir(this.baseDirectory, { withFileTypes: true });
1212
+ for (const entry of entries) {
1213
+ if (!entry.isFile() || !tokenMetadataPattern.test(entry.name)) continue;
1214
+ const filePath = plugins.path.join(this.baseDirectory, entry.name);
1215
+ const stats = await lstatIfPresent(filePath);
1216
+ if (!stats) continue;
1217
+ if (
1218
+ !stats.isFile()
1219
+ || stats.isSymbolicLink()
1220
+ || stats.uid !== this.uid
1221
+ || (stats.mode & 0o077) !== 0
1222
+ ) {
1223
+ throw new Error(`Upgrade coordination metadata is unsafe: ${filePath}`);
1224
+ }
1225
+ if (Date.now() - stats.mtimeMs > tokenMetadataRetentionMs) {
1226
+ await plugins.fs.promises.unlink(filePath).catch((errorArg) => {
1227
+ if ((errorArg as NodeJS.ErrnoException).code !== 'ENOENT') throw errorArg;
1228
+ });
1229
+ }
1230
+ }
1231
+ for (const entry of await plugins.fs.promises.readdir(this.transactionsDirectory, {
1232
+ withFileTypes: true,
1233
+ })) {
1234
+ const filePath = plugins.path.join(this.transactionsDirectory, entry.name);
1235
+ if (entry.isFile() && transactionTemporaryMetadataPattern.test(entry.name)) {
1236
+ const stats = await lstatIfPresent(filePath);
1237
+ if (!stats) continue;
1238
+ if (
1239
+ !stats.isFile()
1240
+ || stats.isSymbolicLink()
1241
+ || stats.uid !== this.uid
1242
+ || (stats.mode & 0o077) !== 0
1243
+ ) throw new Error(`Upgrade transaction temporary metadata is unsafe: ${filePath}`);
1244
+ if (Date.now() - stats.mtimeMs > initializationGraceMs) {
1245
+ await plugins.fs.promises.unlink(filePath).catch((errorArg) => {
1246
+ if ((errorArg as NodeJS.ErrnoException).code !== 'ENOENT') throw errorArg;
1247
+ });
1248
+ }
1249
+ continue;
1250
+ }
1251
+ if (entry.isDirectory() && transactionMutationDirectoryPattern.test(entry.name)) {
1252
+ const stats = await lstatIfPresent(filePath);
1253
+ if (!stats) continue;
1254
+ if (
1255
+ !stats.isDirectory()
1256
+ || stats.isSymbolicLink()
1257
+ || stats.uid !== this.uid
1258
+ || (stats.mode & 0o077) !== 0
1259
+ ) throw new Error(`Upgrade transaction mutation metadata is unsafe: ${filePath}`);
1260
+ if (Date.now() - stats.mtimeMs > transactionRetentionMs) {
1261
+ await this.reclaimStaleTransactionMutationDirectory(filePath);
1262
+ }
1263
+ continue;
1264
+ }
1265
+ if (!entry.isFile() || !transactionMetadataPattern.test(entry.name)) continue;
1266
+ const stats = await lstatIfPresent(filePath);
1267
+ if (!stats) continue;
1268
+ if (
1269
+ !stats.isFile()
1270
+ || stats.isSymbolicLink()
1271
+ || stats.uid !== this.uid
1272
+ || (stats.mode & 0o077) !== 0
1273
+ ) throw new Error(`Upgrade transaction metadata is unsafe: ${filePath}`);
1274
+ if (Date.now() - stats.mtimeMs > transactionRetentionMs) {
1275
+ const transaction = parseUpgradeTransaction(await readPrivateJson(filePath));
1276
+ const workerIsLive = await this.transactionWorkerIsLive(transaction).catch(() => true);
1277
+ const controllerIsLive = transaction.controller
1278
+ ? await this.transactionControllerIsLive(transaction).catch(() => true)
1279
+ : false;
1280
+ if (transaction.terminal || (!workerIsLive && !controllerIsLive)) {
1281
+ await plugins.fs.promises.unlink(filePath).catch((errorArg) => {
1282
+ if ((errorArg as NodeJS.ErrnoException).code !== 'ENOENT') throw errorArg;
1283
+ });
1284
+ }
1285
+ }
1286
+ }
1287
+ }
1288
+
1289
+ private ownershipGuardDirectory(directoryArg: string): string {
1290
+ return plugins.path.join(
1291
+ this.ownershipGuardsDirectory,
1292
+ `${ownershipTargetHash(directoryArg)}.guard`,
1293
+ );
1294
+ }
1295
+
1296
+ private async acquireOwnershipMutationGuard(
1297
+ directoryArg: string,
1298
+ ): Promise<UpgradeOwnershipMutationGuard> {
1299
+ const targetHash = ownershipTargetHash(directoryArg);
1300
+ const guardDirectory = this.ownershipGuardDirectory(directoryArg);
1301
+ const identity = await readControllerProcessIdentity(process.pid);
1302
+ if (!identity) throw new Error('Unable to establish upgrade ownership mutation guard identity.');
1303
+ const owner: IUpgradeOwnershipMutationGuardOwner = {
1304
+ version: coordinationVersion,
1305
+ uid: this.uid,
1306
+ targetHash,
1307
+ pid: identity.pid,
1308
+ fingerprint: identity.fingerprint,
1309
+ nonce: plugins.crypto.randomBytes(16).toString('hex'),
1310
+ createdAt: Date.now(),
1311
+ };
1312
+ const deadline = Date.now() + ownershipGuardTimeoutMs;
1313
+ while (true) {
1314
+ try {
1315
+ await plugins.fs.promises.mkdir(guardDirectory, { mode: 0o700 });
1316
+ try {
1317
+ await writePrivateJson(plugins.path.join(guardDirectory, ownerFileName), owner);
1318
+ await syncDirectory(this.ownershipGuardsDirectory);
1319
+ } catch (errorArg) {
1320
+ // A partially initialized guard is deliberately fail-closed. It must
1321
+ // never be reclaimed automatically, or the guard would repeat the
1322
+ // fixed-path ownership race that it exists to prevent.
1323
+ throw errorArg;
1324
+ }
1325
+ return new UpgradeOwnershipMutationGuard(guardDirectory, owner);
1326
+ } catch (errorArg) {
1327
+ if ((errorArg as NodeJS.ErrnoException).code !== 'EEXIST') throw errorArg;
1328
+ }
1329
+
1330
+ const stats = await lstatIfPresent(guardDirectory);
1331
+ if (!stats) continue;
1332
+ if (
1333
+ !stats.isDirectory()
1334
+ || stats.isSymbolicLink()
1335
+ || stats.uid !== this.uid
1336
+ || (stats.mode & 0o077) !== 0
1337
+ ) throw new Error(`Upgrade ownership mutation guard is unsafe: ${guardDirectory}`);
1338
+
1339
+ let existingOwner: IUpgradeOwnershipMutationGuardOwner;
1340
+ try {
1341
+ existingOwner = parseOwnershipMutationGuardOwner(await readPrivateJson(
1342
+ plugins.path.join(guardDirectory, ownerFileName),
1343
+ ));
1344
+ } catch (errorArg) {
1345
+ const refreshedStats = await lstatIfPresent(guardDirectory);
1346
+ if (!refreshedStats || refreshedStats.dev !== stats.dev || refreshedStats.ino !== stats.ino) {
1347
+ continue;
1348
+ }
1349
+ if (
1350
+ Date.now() - refreshedStats.mtimeMs < initializationGraceMs
1351
+ && Date.now() < deadline
1352
+ ) {
1353
+ await new Promise((resolve) => setTimeout(resolve, 25));
1354
+ continue;
1355
+ }
1356
+ throw new Error(
1357
+ `Upgrade ownership mutation guard is incomplete and cannot be reclaimed automatically: ${guardDirectory}. Inspect it after proving its owner is gone, or reboot the host to clear /tmp.`,
1358
+ { cause: errorArg },
1359
+ );
1360
+ }
1361
+ if (existingOwner.uid !== this.uid || existingOwner.targetHash !== targetHash) {
1362
+ throw new Error(`Upgrade ownership mutation guard binding is invalid: ${guardDirectory}`);
1363
+ }
1364
+ let existingIdentity: Awaited<ReturnType<typeof readControllerProcessIdentity>>;
1365
+ try {
1366
+ existingIdentity = await readControllerProcessIdentity(existingOwner.pid);
1367
+ } catch (errorArg) {
1368
+ throw new Error(
1369
+ `The existing upgrade ownership mutation guard cannot be verified safely: ${guardDirectory}`,
1370
+ { cause: errorArg },
1371
+ );
1372
+ }
1373
+ if (existingIdentity?.fingerprint !== existingOwner.fingerprint) {
1374
+ throw new Error(
1375
+ `Upgrade ownership mutation guard is stale and cannot be reclaimed automatically: ${guardDirectory}. Inspect it after proving its owner is gone, or reboot the host to clear /tmp.`,
1376
+ );
1377
+ }
1378
+ if (Date.now() >= deadline) {
1379
+ throw new Error('Timed out waiting for an upgrade ownership mutation guard.');
1380
+ }
1381
+ await new Promise((resolve) => setTimeout(resolve, 25));
1382
+ }
1383
+ }
1384
+
1385
+ private async withOwnershipMutationGuard<T>(
1386
+ directoryArg: string,
1387
+ operationArg: () => Promise<T>,
1388
+ ): Promise<T> {
1389
+ const guard = await this.acquireOwnershipMutationGuard(directoryArg);
1390
+ let result: T | undefined;
1391
+ let operationError: unknown;
1392
+ try {
1393
+ result = await operationArg();
1394
+ } catch (errorArg) {
1395
+ operationError = errorArg;
1396
+ }
1397
+ let releaseError: unknown;
1398
+ try {
1399
+ await guard.release();
1400
+ } catch (errorArg) {
1401
+ releaseError = errorArg;
1402
+ }
1403
+ if (operationError !== undefined && releaseError !== undefined) {
1404
+ throw new AggregateError(
1405
+ [operationError, releaseError],
1406
+ 'Upgrade ownership mutation failed and its guard could not be released.',
1407
+ );
1408
+ }
1409
+ if (operationError !== undefined) throw operationError;
1410
+ if (releaseError !== undefined) throw releaseError;
1411
+ return result as T;
1412
+ }
1413
+
1414
+ private async reclaimStaleOwnedDirectory(directoryArg: string): Promise<boolean> {
1415
+ return await this.withOwnershipMutationGuard(directoryArg, async () => {
1416
+ const current = await this.inspectOwnedDirectory(directoryArg);
1417
+ if (current.state === 'absent') return false;
1418
+ if (current.state === 'indeterminate') {
1419
+ throw new Error(`The existing ${currentCliName} ownership cannot be verified safely.`);
1420
+ }
1421
+ if (current.state !== 'stale') return false;
1422
+ await removeExactOwnedDirectory(directoryArg, {
1423
+ identity: current.identity!,
1424
+ ...(current.owner
1425
+ ? {
1426
+ assertOwner: (valueArg: unknown) => {
1427
+ const removedOwner = parseOwner(valueArg);
1428
+ if (!exactPrimitiveRecordMatches(removedOwner, current.owner!)) {
1429
+ throw new Error('Upgrade coordination ownership changed during stale removal.');
1430
+ }
1431
+ },
1432
+ }
1433
+ : {}),
1434
+ });
1435
+ return true;
1436
+ });
1437
+ }
1438
+
1439
+ private async releaseOwnedDirectory(
1440
+ directoryArg: string,
1441
+ expectedOwnerArg: IUpgradeProcessOwner,
1442
+ shouldReleaseArg?: () => boolean,
1443
+ ): Promise<void> {
1444
+ await this.withOwnershipMutationGuard(directoryArg, async () => {
1445
+ const stats = await plugins.fs.promises.lstat(directoryArg);
1446
+ let currentOwner: IUpgradeProcessOwner;
1447
+ try {
1448
+ currentOwner = parseOwner(await readPrivateJson(
1449
+ plugins.path.join(directoryArg, ownerFileName),
1450
+ ));
1451
+ } catch (errorArg) {
1452
+ if ((errorArg as NodeJS.ErrnoException).code === 'ENOENT') {
1453
+ throw new Error('Upgrade coordination ownership changed unexpectedly.', { cause: errorArg });
1454
+ }
1455
+ throw errorArg;
1456
+ }
1457
+ if (!exactPrimitiveRecordMatches(currentOwner, expectedOwnerArg)) {
1458
+ throw new Error('Upgrade coordination ownership changed unexpectedly.');
1459
+ }
1460
+ await removeExactOwnedDirectory(directoryArg, {
1461
+ identity: { dev: stats.dev, ino: stats.ino },
1462
+ ...(shouldReleaseArg
1463
+ ? {
1464
+ assertRemovalAuthorized: () => {
1465
+ if (!shouldReleaseArg()) {
1466
+ throw new Error('Upgrade coordination release is no longer authorized.');
1467
+ }
1468
+ },
1469
+ }
1470
+ : {}),
1471
+ assertOwner: (valueArg) => {
1472
+ const removedOwner = parseOwner(valueArg);
1473
+ if (!exactPrimitiveRecordMatches(removedOwner, expectedOwnerArg)) {
1474
+ throw new Error('Upgrade coordination ownership changed during release.');
1475
+ }
1476
+ },
1477
+ });
1478
+ });
1479
+ }
1480
+
1481
+ private async inspectTransactionMutationDirectory(
1482
+ directoryArg: string,
1483
+ expectedTokenHashArg?: string,
1484
+ ): Promise<{
1485
+ state: 'absent' | 'live' | 'recent-invalid' | 'stale' | 'indeterminate';
1486
+ identity?: { dev: number; ino: number };
1487
+ owner?: IUpgradeTransactionMutationOwner;
1488
+ }> {
1489
+ const stats = await lstatIfPresent(directoryArg);
1490
+ if (!stats) return { state: 'absent' };
1491
+ if (
1492
+ !stats.isDirectory()
1493
+ || stats.isSymbolicLink()
1494
+ || stats.uid !== this.uid
1495
+ || (stats.mode & 0o077) !== 0
1496
+ ) throw new Error('The upgrade transaction mutation lock is unsafe.');
1497
+ let rawOwner: unknown;
1498
+ try {
1499
+ rawOwner = await readPrivateJson(plugins.path.join(directoryArg, ownerFileName));
1500
+ } catch (errorArg) {
1501
+ const code = (errorArg as NodeJS.ErrnoException).code;
1502
+ if (code === 'HCON_INVALID_JSON') {
1503
+ return { state: 'indeterminate', identity: { dev: stats.dev, ino: stats.ino } };
1504
+ }
1505
+ if (code !== 'ENOENT') throw errorArg;
1506
+ return {
1507
+ state: Date.now() - stats.mtimeMs < initializationGraceMs
1508
+ ? 'recent-invalid'
1509
+ : 'stale',
1510
+ identity: { dev: stats.dev, ino: stats.ino },
1511
+ };
1512
+ }
1513
+ let owner: IUpgradeTransactionMutationOwner;
1514
+ try {
1515
+ owner = parseTransactionMutationOwner(rawOwner);
1516
+ } catch {
1517
+ return { state: 'indeterminate', identity: { dev: stats.dev, ino: stats.ino } };
1518
+ }
1519
+ if (
1520
+ owner.uid !== this.uid
1521
+ || (expectedTokenHashArg !== undefined
1522
+ && !tokensEqual(owner.tokenHash, expectedTokenHashArg))
1523
+ ) throw new Error('The upgrade transaction mutation lock binding is invalid.');
1524
+ try {
1525
+ const ownerIdentity = await readControllerProcessIdentity(owner.pid);
1526
+ return {
1527
+ state: ownerIdentity?.fingerprint === owner.fingerprint ? 'live' : 'stale',
1528
+ identity: { dev: stats.dev, ino: stats.ino },
1529
+ owner,
1530
+ };
1531
+ } catch {
1532
+ return { state: 'indeterminate', identity: { dev: stats.dev, ino: stats.ino } };
1533
+ }
1534
+ }
1535
+
1536
+ private async reclaimStaleTransactionMutationDirectory(
1537
+ directoryArg: string,
1538
+ expectedTokenHashArg?: string,
1539
+ ): Promise<boolean> {
1540
+ return await this.withOwnershipMutationGuard(directoryArg, async () => {
1541
+ const current = await this.inspectTransactionMutationDirectory(
1542
+ directoryArg,
1543
+ expectedTokenHashArg,
1544
+ );
1545
+ if (current.state === 'absent') return false;
1546
+ if (current.state === 'indeterminate') {
1547
+ throw new Error('The upgrade transaction mutation owner cannot be verified safely.');
1548
+ }
1549
+ if (current.state !== 'stale') return false;
1550
+ await removeExactOwnedDirectory(directoryArg, {
1551
+ identity: current.identity!,
1552
+ ...(current.owner
1553
+ ? {
1554
+ assertOwner: (valueArg: unknown) => {
1555
+ const removedOwner = parseTransactionMutationOwner(valueArg);
1556
+ if (!exactPrimitiveRecordMatches(removedOwner, current.owner!)) {
1557
+ throw new Error(
1558
+ 'The upgrade transaction mutation owner changed during stale removal.',
1559
+ );
1560
+ }
1561
+ },
1562
+ }
1563
+ : {}),
1564
+ });
1565
+ return true;
1566
+ });
1567
+ }
1568
+
1569
+ private async inspectOwnedDirectory(
1570
+ directoryArg: string,
1571
+ ): Promise<{
1572
+ state: 'absent' | 'live' | 'recent-invalid' | 'stale' | 'indeterminate';
1573
+ identity?: { dev: number; ino: number };
1574
+ owner?: IUpgradeProcessOwner;
1575
+ }> {
1576
+ let stats: plugins.fs.Stats;
1577
+ try {
1578
+ stats = await plugins.fs.promises.lstat(directoryArg);
1579
+ } catch (errorArg) {
1580
+ if ((errorArg as NodeJS.ErrnoException).code === 'ENOENT') return { state: 'absent' };
1581
+ throw errorArg;
1582
+ }
1583
+ if (!stats.isDirectory() || stats.isSymbolicLink()) {
1584
+ throw new Error(`Upgrade coordination owner path is unsafe: ${directoryArg}`);
1585
+ }
1586
+ if (stats.uid !== this.uid || (stats.mode & 0o077) !== 0) {
1587
+ throw new Error(`Upgrade coordination owner path is not private: ${directoryArg}`);
1588
+ }
1589
+ let ownerValue: unknown;
1590
+ try {
1591
+ ownerValue = await readPrivateJson(plugins.path.join(directoryArg, ownerFileName));
1592
+ } catch (errorArg) {
1593
+ const code = (errorArg as NodeJS.ErrnoException).code;
1594
+ if (code === 'HCON_INVALID_JSON') {
1595
+ return { state: 'indeterminate', identity: { dev: stats.dev, ino: stats.ino } };
1596
+ }
1597
+ if (code !== 'ENOENT') throw errorArg;
1598
+ return {
1599
+ state: Date.now() - stats.mtimeMs < initializationGraceMs
1600
+ ? 'recent-invalid'
1601
+ : 'stale',
1602
+ identity: { dev: stats.dev, ino: stats.ino },
1603
+ };
1604
+ }
1605
+ let owner: IUpgradeProcessOwner;
1606
+ try {
1607
+ owner = parseOwner(ownerValue);
1608
+ } catch {
1609
+ return { state: 'indeterminate', identity: { dev: stats.dev, ino: stats.ino } };
1610
+ }
1611
+ try {
1612
+ return {
1613
+ state: await ownerIsLive(owner) ? 'live' : 'stale',
1614
+ identity: { dev: stats.dev, ino: stats.ino },
1615
+ owner,
1616
+ };
1617
+ } catch {
1618
+ return { state: 'indeterminate', identity: { dev: stats.dev, ino: stats.ino } };
1619
+ }
1620
+ }
1621
+
1622
+ private async createOwnedDirectory(
1623
+ directoryArg: string,
1624
+ ownerArg: IUpgradeProcessOwner,
1625
+ ): Promise<{ directory: string; owner: IUpgradeProcessOwner } | undefined> {
1626
+ return await this.withOwnershipMutationGuard(directoryArg, async () => {
1627
+ try {
1628
+ await plugins.fs.promises.mkdir(directoryArg, { mode: 0o700 });
1629
+ } catch (errorArg) {
1630
+ if ((errorArg as NodeJS.ErrnoException).code === 'EEXIST') return undefined;
1631
+ throw errorArg;
1632
+ }
1633
+ try {
1634
+ await writePrivateJson(plugins.path.join(directoryArg, ownerFileName), ownerArg);
1635
+ await syncDirectory(plugins.path.dirname(directoryArg));
1636
+ return { directory: directoryArg, owner: ownerArg };
1637
+ } catch (errorArg) {
1638
+ // Preserve a failed initialization directory. Contenders treat it as
1639
+ // recent-invalid, then reclaim it only after the bounded grace period.
1640
+ throw errorArg;
1641
+ }
1642
+ });
1643
+ }
1644
+
1645
+ public async acquireUpgradeLock(optionsArg: {
1646
+ token: string;
1647
+ cliPath: string;
1648
+ command?: '__serve' | '__upgrade-worker' | 'upgrade' | 'foreground';
1649
+ }): Promise<UpgradeInstallationLock> {
1650
+ await this.init();
1651
+ const owner = await ownerForCurrentProcess(
1652
+ 'upgrade',
1653
+ optionsArg.token,
1654
+ optionsArg.cliPath,
1655
+ optionsArg.command ?? '__upgrade-worker',
1656
+ );
1657
+ for (let attempt = 0; attempt < 4; attempt++) {
1658
+ const created = await this.createOwnedDirectory(this.upgradeLockDirectory, owner);
1659
+ if (created) return new UpgradeInstallationLock(
1660
+ created.directory,
1661
+ created.owner,
1662
+ (directoryArg, ownerArg, shouldReleaseArg) => this.releaseOwnedDirectory(
1663
+ directoryArg,
1664
+ ownerArg,
1665
+ shouldReleaseArg,
1666
+ ),
1667
+ );
1668
+ const state = await this.inspectOwnedDirectory(this.upgradeLockDirectory);
1669
+ if (state.state === 'live') {
1670
+ let currentOwner: IUpgradeProcessOwner;
1671
+ try {
1672
+ currentOwner = parseOwner(await readPrivateJson(
1673
+ plugins.path.join(this.upgradeLockDirectory, ownerFileName),
1674
+ ));
1675
+ } catch (errorArg) {
1676
+ if ((errorArg as NodeJS.ErrnoException).code === 'ENOENT') continue;
1677
+ throw errorArg;
1678
+ }
1679
+ if (
1680
+ currentOwner.pid === owner.pid
1681
+ && currentOwner.fingerprint === owner.fingerprint
1682
+ && tokensEqual(currentOwner.tokenHash, owner.tokenHash)
1683
+ ) return new UpgradeInstallationLock(
1684
+ this.upgradeLockDirectory,
1685
+ currentOwner,
1686
+ (directoryArg, ownerArg, shouldReleaseArg) => this.releaseOwnedDirectory(
1687
+ directoryArg,
1688
+ ownerArg,
1689
+ shouldReleaseArg,
1690
+ ),
1691
+ );
1692
+ throw new Error(`Another ${currentCliName} upgrade is already running.`);
1693
+ }
1694
+ if (state.state === 'recent-invalid') {
1695
+ throw new Error(`Another ${currentCliName} upgrade is initializing; retry shortly.`);
1696
+ }
1697
+ if (state.state === 'stale') {
1698
+ await this.reclaimStaleOwnedDirectory(this.upgradeLockDirectory);
1699
+ }
1700
+ if (state.state === 'indeterminate') {
1701
+ throw new Error(`The existing ${currentCliName} upgrade owner cannot be verified safely.`);
1702
+ }
1703
+ }
1704
+ throw new Error(`Unable to acquire exclusive ${currentCliName} upgrade ownership.`);
1705
+ }
1706
+
1707
+ public async transferUpgradeLockToWorker(optionsArg: {
1708
+ lock: UpgradeInstallationLock;
1709
+ token: string;
1710
+ worker: NonNullable<TUpgradeTransaction['worker']>;
1711
+ }): Promise<void> {
1712
+ const identity = await readControllerProcessIdentity(optionsArg.worker.pid);
1713
+ if (
1714
+ !identity
1715
+ || identity.fingerprint !== optionsArg.worker.fingerprint
1716
+ || identity.processGroupId !== optionsArg.worker.processGroupId
1717
+ || !identity.processGroupLeader
1718
+ || !await processIdentityHasCliCommand(
1719
+ identity,
1720
+ optionsArg.worker.cliPath,
1721
+ '__upgrade-worker',
1722
+ { allowMissingAbsolutePath: true },
1723
+ )
1724
+ ) throw new Error('The upgrade lock transfer worker identity is invalid.');
1725
+ const owner: IUpgradeProcessOwner = {
1726
+ version: coordinationVersion,
1727
+ kind: 'upgrade',
1728
+ uid: this.uid,
1729
+ tokenHash: hashToken(optionsArg.token),
1730
+ pid: identity.pid,
1731
+ processGroupId: identity.processGroupId,
1732
+ fingerprint: identity.fingerprint,
1733
+ cliPath: optionsArg.worker.cliPath,
1734
+ command: '__upgrade-worker',
1735
+ createdAt: Date.now(),
1736
+ };
1737
+ await this.withOwnershipMutationGuard(this.upgradeLockDirectory, async () => {
1738
+ await optionsArg.lock.assertOwned();
1739
+ const ownerPath = plugins.path.join(this.upgradeLockDirectory, ownerFileName);
1740
+ const temporaryPath = `${ownerPath}.transfer-${process.pid}-${plugins.crypto.randomBytes(4).toString('hex')}`;
1741
+ try {
1742
+ await writePrivateJson(temporaryPath, owner);
1743
+ await plugins.fs.promises.rename(temporaryPath, ownerPath);
1744
+ await syncDirectory(this.upgradeLockDirectory);
1745
+ optionsArg.lock.relinquishAfterTransfer();
1746
+ } finally {
1747
+ await plugins.fs.promises.unlink(temporaryPath).catch(() => undefined);
1748
+ }
1749
+ });
1750
+ }
1751
+
1752
+ private async readLiveUpgradeOwner(): Promise<IUpgradeProcessOwner | undefined> {
1753
+ for (let attempt = 0; attempt < 4; attempt++) {
1754
+ const state = await this.inspectOwnedDirectory(this.upgradeLockDirectory);
1755
+ if (state.state === 'absent') return undefined;
1756
+ if (state.state === 'recent-invalid') {
1757
+ throw new Error(`An ${currentCliName} upgrade is initializing; retry shortly.`);
1758
+ }
1759
+ if (state.state === 'indeterminate') {
1760
+ throw new Error(`The existing ${currentCliName} upgrade owner cannot be verified safely.`);
1761
+ }
1762
+ if (state.state === 'stale') {
1763
+ await this.reclaimStaleOwnedDirectory(this.upgradeLockDirectory);
1764
+ continue;
1765
+ }
1766
+ try {
1767
+ return parseOwner(await readPrivateJson(
1768
+ plugins.path.join(this.upgradeLockDirectory, ownerFileName),
1769
+ ));
1770
+ } catch (errorArg) {
1771
+ if ((errorArg as NodeJS.ErrnoException).code !== 'ENOENT') throw errorArg;
1772
+ }
1773
+ }
1774
+ throw new Error(`The ${currentCliName} upgrade owner changed repeatedly during admission.`);
1775
+ }
1776
+
1777
+ public async acquireStartLease(optionsArg: {
1778
+ token: string;
1779
+ cliPath: string;
1780
+ command: 'start' | 'foreground' | 'temp-password';
1781
+ upgradeToken?: string;
1782
+ }): Promise<UpgradeStartLease> {
1783
+ await this.init();
1784
+ const owner = await ownerForCurrentProcess(
1785
+ 'start',
1786
+ optionsArg.token,
1787
+ optionsArg.cliPath,
1788
+ optionsArg.command,
1789
+ );
1790
+ const upgradeOwnerBefore = await this.readLiveUpgradeOwner();
1791
+ const authorized = upgradeOwnerBefore !== undefined
1792
+ && optionsArg.upgradeToken !== undefined
1793
+ && tokensEqual(upgradeOwnerBefore.tokenHash, hashToken(optionsArg.upgradeToken));
1794
+ if (upgradeOwnerBefore && !authorized) {
1795
+ throw new Error(`${currentCliName} cannot start while a global upgrade is in progress.`);
1796
+ }
1797
+ const leaseDirectory = plugins.path.join(
1798
+ this.startLeasesDirectory,
1799
+ coordinationPathHash(assertToken(optionsArg.token)),
1800
+ );
1801
+ const created = await this.createOwnedDirectory(leaseDirectory, owner);
1802
+ if (!created) throw new Error(`The ${currentCliName} start admission token is already in use.`);
1803
+ const lease = new UpgradeStartLease(
1804
+ created.directory,
1805
+ created.owner,
1806
+ (directoryArg, ownerArg, shouldReleaseArg) => this.releaseOwnedDirectory(
1807
+ directoryArg,
1808
+ ownerArg,
1809
+ shouldReleaseArg,
1810
+ ),
1811
+ );
1812
+ try {
1813
+ const upgradeOwnerAfter = await this.readLiveUpgradeOwner();
1814
+ if (upgradeOwnerAfter) {
1815
+ const remainsAuthorized = optionsArg.upgradeToken !== undefined
1816
+ && tokensEqual(upgradeOwnerAfter.tokenHash, hashToken(optionsArg.upgradeToken));
1817
+ if (!remainsAuthorized) {
1818
+ throw new Error(`${currentCliName} upgrade began while startup was entering admission.`);
1819
+ }
1820
+ }
1821
+ return lease;
1822
+ } catch (errorArg) {
1823
+ await lease.release();
1824
+ throw errorArg;
1825
+ }
1826
+ }
1827
+
1828
+ public async waitForStartLeasesToDrain(lockArg: UpgradeInstallationLock): Promise<void> {
1829
+ const deadline = Date.now() + startLeaseDrainTimeoutMs;
1830
+ while (Date.now() < deadline) {
1831
+ await lockArg.assertOwned();
1832
+ const entries = await plugins.fs.promises.readdir(this.startLeasesDirectory, {
1833
+ withFileTypes: true,
1834
+ });
1835
+ let blocked = false;
1836
+ for (const entry of entries) {
1837
+ if (!entry.isDirectory() || !leaseDirectoryPattern.test(entry.name)) {
1838
+ throw new Error(`The ${currentCliName} start admission directory contains an unexpected entry.`);
1839
+ }
1840
+ const leaseDirectory = plugins.path.join(this.startLeasesDirectory, entry.name);
1841
+ const state = await this.inspectOwnedDirectory(leaseDirectory);
1842
+ if (state.state === 'live' || state.state === 'recent-invalid') {
1843
+ blocked = true;
1844
+ continue;
1845
+ }
1846
+ if (state.state === 'indeterminate') {
1847
+ throw new Error(`An ${currentCliName} start admission owner cannot be verified safely.`);
1848
+ }
1849
+ if (state.state === 'stale') {
1850
+ if (!await this.reclaimStaleOwnedDirectory(leaseDirectory)) blocked = true;
1851
+ }
1852
+ }
1853
+ if (!blocked) return;
1854
+ await new Promise((resolve) => setTimeout(resolve, 100));
1855
+ }
1856
+ throw new Error(`Timed out waiting for an in-flight ${currentCliName} start to finish.`);
1857
+ }
1858
+
1859
+ private tokenFilePath(kindArg: 'grant' | 'ack', tokenArg: string): string {
1860
+ return plugins.path.join(
1861
+ this.baseDirectory,
1862
+ `${kindArg}-${coordinationPathHash(assertToken(tokenArg))}.json`,
1863
+ );
1864
+ }
1865
+
1866
+ private transactionFilePath(tokenArg: string): string {
1867
+ return plugins.path.join(
1868
+ this.transactionsDirectory,
1869
+ `transaction-${coordinationPathHash(assertToken(tokenArg))}.json`,
1870
+ );
1871
+ }
1872
+
1873
+ private transactionMutationDirectory(tokenArg: string): string {
1874
+ return `${this.transactionFilePath(tokenArg)}.lock`;
1875
+ }
1876
+
1877
+ private async withTransactionMutation<T>(
1878
+ tokenArg: string,
1879
+ operationArg: () => Promise<T>,
1880
+ ): Promise<T> {
1881
+ await this.init();
1882
+ const lockDirectory = this.transactionMutationDirectory(tokenArg);
1883
+ const deadline = Date.now() + transactionMutationTimeoutMs;
1884
+ const identity = await readControllerProcessIdentity(process.pid);
1885
+ if (!identity) throw new Error('Unable to establish transaction mutation ownership.');
1886
+ const owner: IUpgradeTransactionMutationOwner = {
1887
+ version: coordinationVersion,
1888
+ uid: this.uid,
1889
+ tokenHash: hashToken(tokenArg),
1890
+ pid: identity.pid,
1891
+ fingerprint: identity.fingerprint,
1892
+ nonce: plugins.crypto.randomBytes(16).toString('hex'),
1893
+ createdAt: Date.now(),
1894
+ };
1895
+ while (true) {
1896
+ try {
1897
+ await plugins.fs.promises.mkdir(lockDirectory, { mode: 0o700 });
1898
+ try {
1899
+ await writePrivateJson(plugins.path.join(lockDirectory, ownerFileName), owner);
1900
+ } catch (errorArg) {
1901
+ // Leave partial initialization for the next contender's bounded stale check.
1902
+ throw errorArg;
1903
+ }
1904
+ break;
1905
+ } catch (errorArg) {
1906
+ if ((errorArg as NodeJS.ErrnoException).code !== 'EEXIST') throw errorArg;
1907
+ const current = await this.inspectTransactionMutationDirectory(
1908
+ lockDirectory,
1909
+ owner.tokenHash,
1910
+ );
1911
+ if (current.state === 'absent') continue;
1912
+ if (current.state === 'stale') {
1913
+ await this.reclaimStaleTransactionMutationDirectory(
1914
+ lockDirectory,
1915
+ owner.tokenHash,
1916
+ );
1917
+ continue;
1918
+ }
1919
+ if (current.state === 'indeterminate') {
1920
+ throw new Error('The upgrade transaction mutation owner cannot be verified safely.');
1921
+ }
1922
+ if (Date.now() >= deadline) {
1923
+ throw new Error('Timed out waiting to update the upgrade transaction.');
1924
+ }
1925
+ await new Promise((resolve) => setTimeout(resolve, 25));
1926
+ }
1927
+ }
1928
+ let result: T | undefined;
1929
+ let operationError: unknown;
1930
+ try {
1931
+ result = await operationArg();
1932
+ } catch (errorArg) {
1933
+ operationError = errorArg;
1934
+ }
1935
+ let releaseError: unknown;
1936
+ try {
1937
+ await this.withOwnershipMutationGuard(lockDirectory, async () => {
1938
+ const stats = await plugins.fs.promises.lstat(lockDirectory);
1939
+ let currentOwner: IUpgradeTransactionMutationOwner;
1940
+ try {
1941
+ currentOwner = parseTransactionMutationOwner(await readPrivateJson(
1942
+ plugins.path.join(lockDirectory, ownerFileName),
1943
+ ));
1944
+ } catch (errorArg) {
1945
+ if ((errorArg as NodeJS.ErrnoException).code === 'ENOENT') {
1946
+ throw new Error(
1947
+ 'The upgrade transaction mutation lock ownership changed unexpectedly.',
1948
+ { cause: errorArg },
1949
+ );
1950
+ }
1951
+ throw errorArg;
1952
+ }
1953
+ if (!exactPrimitiveRecordMatches(currentOwner, owner)) {
1954
+ throw new Error('The upgrade transaction mutation lock ownership changed unexpectedly.');
1955
+ }
1956
+ await removeExactOwnedDirectory(lockDirectory, {
1957
+ identity: { dev: stats.dev, ino: stats.ino },
1958
+ assertOwner: (valueArg) => {
1959
+ const removedOwner = parseTransactionMutationOwner(valueArg);
1960
+ if (!exactPrimitiveRecordMatches(removedOwner, owner)) {
1961
+ throw new Error(
1962
+ 'The upgrade transaction mutation lock ownership changed during release.',
1963
+ );
1964
+ }
1965
+ },
1966
+ });
1967
+ });
1968
+ } catch (errorArg) {
1969
+ releaseError = errorArg;
1970
+ }
1971
+ if (operationError !== undefined && releaseError !== undefined) {
1972
+ throw new AggregateError(
1973
+ [operationError, releaseError],
1974
+ 'The upgrade transaction mutation failed and its ownership could not be released.',
1975
+ );
1976
+ }
1977
+ if (operationError !== undefined) throw operationError;
1978
+ if (releaseError !== undefined) throw releaseError;
1979
+ return result as T;
1980
+ }
1981
+
1982
+ public async createTransaction(optionsArg: {
1983
+ token: string;
1984
+ port: number;
1985
+ sourceVersion: string;
1986
+ controllerWasRunning: boolean;
1987
+ continueSessions: boolean;
1988
+ gracePeriodMs: number;
1989
+ }): Promise<IUpgradeTransactionV2> {
1990
+ await this.init();
1991
+ const now = Date.now();
1992
+ const transaction: IUpgradeTransactionV2 = {
1993
+ version: coordinationVersion,
1994
+ tokenHash: hashToken(optionsArg.token),
1995
+ revision: 0,
1996
+ port: assertPort(optionsArg.port),
1997
+ sourceVersion: assertBoundedText(optionsArg.sourceVersion, 'The upgrade source version', 128),
1998
+ controllerWasRunning: optionsArg.controllerWasRunning,
1999
+ continueSessions: optionsArg.continueSessions,
2000
+ gracePeriodMs: optionsArg.gracePeriodMs,
2001
+ phase: 'starting',
2002
+ message: 'Upgrade worker is starting.',
2003
+ createdAt: now,
2004
+ phaseStartedAt: now,
2005
+ updatedAt: now,
2006
+ sessions: [],
2007
+ };
2008
+ const validated = parseUpgradeTransactionV2(transaction);
2009
+ await writePrivateJson(this.transactionFilePath(optionsArg.token), validated);
2010
+ return validated;
2011
+ }
2012
+
2013
+ public async createPackageTransitionTransaction(optionsArg: {
2014
+ token: string;
2015
+ port: number;
2016
+ controllerWasRunning: boolean;
2017
+ continueSessions: boolean;
2018
+ gracePeriodMs: number;
2019
+ }): Promise<IUpgradeTransactionV3> {
2020
+ await this.init();
2021
+ const now = Date.now();
2022
+ const transaction: IUpgradeTransactionV3 = {
2023
+ version: upgradePackageTransitionTransactionVersion,
2024
+ tokenHash: hashToken(optionsArg.token),
2025
+ revision: 0,
2026
+ port: assertPort(optionsArg.port),
2027
+ sourcePackageName: upgradePackageTransitionSource.packageName,
2028
+ sourceVersion: upgradePackageTransitionSource.version,
2029
+ sourceManagementVersion: upgradePackageTransitionSource.managementVersion,
2030
+ sourceCliName: upgradePackageTransitionSource.cliName,
2031
+ sourceCliRelativePath: upgradePackageTransitionSource.cliRelativePath,
2032
+ targetPackageName: upgradePackageTransitionTarget.packageName,
2033
+ targetVersion: upgradePackageTransitionTarget.version,
2034
+ targetManagementVersion: upgradePackageTransitionTarget.managementVersion,
2035
+ targetCliName: upgradePackageTransitionTarget.cliName,
2036
+ targetCliRelativePath: upgradePackageTransitionTarget.cliRelativePath,
2037
+ controllerWasRunning: optionsArg.controllerWasRunning,
2038
+ continueSessions: optionsArg.continueSessions,
2039
+ gracePeriodMs: optionsArg.gracePeriodMs,
2040
+ phase: 'starting',
2041
+ message: 'Upgrade worker is starting.',
2042
+ createdAt: now,
2043
+ phaseStartedAt: now,
2044
+ updatedAt: now,
2045
+ sessions: [],
2046
+ };
2047
+ const validated = parseUpgradeTransactionV3(transaction);
2048
+ await writePrivateJson(this.transactionFilePath(optionsArg.token), validated);
2049
+ return validated;
2050
+ }
2051
+
2052
+ public async readTransaction(tokenArg: string): Promise<TUpgradeTransaction> {
2053
+ await this.init();
2054
+ const transaction = parseUpgradeTransaction(await readPrivateJson(
2055
+ this.transactionFilePath(tokenArg),
2056
+ ));
2057
+ if (!tokensEqual(transaction.tokenHash, hashToken(tokenArg))) {
2058
+ throw new Error('The upgrade transaction token binding is invalid.');
2059
+ }
2060
+ return transaction;
2061
+ }
2062
+
2063
+ public async updateTransaction(
2064
+ tokenArg: string,
2065
+ expectedRevisionArg: number,
2066
+ updateArg: (currentArg: TUpgradeTransaction) => TUpgradeTransaction,
2067
+ ): Promise<TUpgradeTransaction> {
2068
+ return await this.withTransactionMutation(tokenArg, async () => {
2069
+ const current = await this.readTransaction(tokenArg);
2070
+ if (current.revision !== expectedRevisionArg) {
2071
+ throw new Error('The upgrade transaction changed concurrently.');
2072
+ }
2073
+ return await this.writeTransactionUpdate(tokenArg, current, updateArg(current));
2074
+ });
2075
+ }
2076
+
2077
+ public async mutateTransaction(
2078
+ tokenArg: string,
2079
+ updateArg: (currentArg: TUpgradeTransaction) => TUpgradeTransaction,
2080
+ ): Promise<TUpgradeTransaction> {
2081
+ return await this.withTransactionMutation(tokenArg, async () => {
2082
+ const current = await this.readTransaction(tokenArg);
2083
+ return await this.writeTransactionUpdate(tokenArg, current, updateArg(current));
2084
+ });
2085
+ }
2086
+
2087
+ private async writeTransactionUpdate(
2088
+ tokenArg: string,
2089
+ currentArg: TUpgradeTransaction,
2090
+ updatedArg: TUpgradeTransaction,
2091
+ ): Promise<TUpgradeTransaction> {
2092
+ const updatedAt = Date.now();
2093
+ const workerIdentityChanged = (
2094
+ currentArg.worker?.pid !== updatedArg.worker?.pid
2095
+ || currentArg.worker?.processGroupId !== updatedArg.worker?.processGroupId
2096
+ || currentArg.worker?.fingerprint !== updatedArg.worker?.fingerprint
2097
+ || currentArg.worker?.cliPath !== updatedArg.worker?.cliPath
2098
+ );
2099
+ const next = parseUpgradeTransaction({
2100
+ ...updatedArg,
2101
+ version: currentArg.version,
2102
+ tokenHash: currentArg.tokenHash,
2103
+ revision: currentArg.revision + 1,
2104
+ createdAt: currentArg.createdAt,
2105
+ phaseStartedAt: updatedArg.phase === currentArg.phase && !workerIdentityChanged
2106
+ ? currentArg.phaseStartedAt
2107
+ : updatedAt,
2108
+ updatedAt,
2109
+ });
2110
+ if (transactionPhaseOrder[next.phase] < transactionPhaseOrder[currentArg.phase]) {
2111
+ throw new Error('The upgrade transaction phase cannot regress.');
2112
+ }
2113
+ if (currentArg.version === upgradePackageTransitionTransactionVersion) {
2114
+ if (next.version !== upgradePackageTransitionTransactionVersion) {
2115
+ throw new Error('The upgrade transaction version cannot change.');
2116
+ }
2117
+ for (const checkpoint of [
2118
+ 'packageTransitionStarted',
2119
+ 'groupedTargetVerified',
2120
+ 'targetPackageCommitStarted',
2121
+ 'targetPackageCommitted',
2122
+ 'targetStartupInvoked',
2123
+ ] as const) {
2124
+ if (currentArg[checkpoint] === true && next[checkpoint] !== true) {
2125
+ throw new Error(`The upgrade package-transition checkpoint ${checkpoint} cannot regress.`);
2126
+ }
2127
+ }
2128
+ }
2129
+ if (
2130
+ (currentArg.preparationAcceptedAt !== undefined
2131
+ && next.preparationAcceptedAt !== currentArg.preparationAcceptedAt)
2132
+ || (currentArg.preparationDeadlineAt !== undefined
2133
+ && next.preparationDeadlineAt !== currentArg.preparationDeadlineAt)
2134
+ || (currentArg.preparationCompletedAt !== undefined
2135
+ && next.preparationCompletedAt !== currentArg.preparationCompletedAt)
2136
+ || (currentArg.preparationAcceptedAt === undefined
2137
+ && currentArg.preparationDeadlineAt === undefined
2138
+ && next.preparationCompletedAt !== undefined)
2139
+ ) throw new Error('The upgrade preparation timeline cannot change after it is committed.');
2140
+ const targetPath = this.transactionFilePath(tokenArg);
2141
+ const temporaryPath = `${targetPath}.tmp-${process.pid}-${plugins.crypto.randomBytes(4).toString('hex')}`;
2142
+ try {
2143
+ await writePrivateJson(temporaryPath, next);
2144
+ await plugins.fs.promises.rename(temporaryPath, targetPath);
2145
+ await syncDirectory(this.transactionsDirectory);
2146
+ } finally {
2147
+ await plugins.fs.promises.unlink(temporaryPath).catch(() => undefined);
2148
+ }
2149
+ return next;
2150
+ }
2151
+
2152
+ public async registerWorker(tokenArg: string, cliPathArg: string): Promise<TUpgradeTransaction> {
2153
+ const identity = await readControllerProcessIdentity(process.pid);
2154
+ if (!identity) throw new Error('Unable to establish upgrade worker ownership.');
2155
+ return await this.mutateTransaction(tokenArg, (current) => ({
2156
+ ...current,
2157
+ worker: {
2158
+ pid: identity.pid,
2159
+ processGroupId: identity.processGroupId,
2160
+ fingerprint: identity.fingerprint,
2161
+ cliPath: cliPathArg,
2162
+ ...(current.worker?.logFilePath ? { logFilePath: current.worker.logFilePath } : {}),
2163
+ },
2164
+ phase: 'checking',
2165
+ message: 'Checking the npm latest version.',
2166
+ }));
2167
+ }
2168
+
2169
+ public async recordWorkerLaunch(
2170
+ tokenArg: string,
2171
+ pidArg: number,
2172
+ cliPathArg: string,
2173
+ logFilePathArg: string,
2174
+ ): Promise<TUpgradeTransaction> {
2175
+ const identity = await readControllerProcessIdentity(pidArg);
2176
+ if (
2177
+ !identity
2178
+ || !identity.processGroupLeader
2179
+ || !await processIdentityHasCliCommand(
2180
+ identity,
2181
+ cliPathArg,
2182
+ '__upgrade-worker',
2183
+ { allowMissingAbsolutePath: true },
2184
+ )
2185
+ ) throw new Error('Unable to verify the launched upgrade worker identity.');
2186
+ const logFilePath = assertBoundedText(logFilePathArg, 'The upgrade worker log path', 4_096);
2187
+ if (!plugins.path.isAbsolute(logFilePath)) {
2188
+ throw new Error('The upgrade worker log path must be absolute.');
2189
+ }
2190
+ return await this.mutateTransaction(tokenArg, (current) => ({
2191
+ ...current,
2192
+ worker: {
2193
+ pid: identity.pid,
2194
+ processGroupId: identity.processGroupId,
2195
+ fingerprint: identity.fingerprint,
2196
+ cliPath: cliPathArg,
2197
+ logFilePath,
2198
+ },
2199
+ }));
2200
+ }
2201
+
2202
+ public async recordController(
2203
+ tokenArg: string,
2204
+ controllerArg: TUpgradeTransaction['controller'],
2205
+ ): Promise<TUpgradeTransaction> {
2206
+ if (!controllerArg) throw new Error('The upgrade controller identity is required.');
2207
+ const identity = await readControllerProcessIdentity(controllerArg.pid);
2208
+ if (
2209
+ !identity
2210
+ || identity.fingerprint !== controllerArg.fingerprint
2211
+ || identity.processGroupId !== controllerArg.processGroupId
2212
+ || !await processIdentityHasCliCommand(
2213
+ identity,
2214
+ controllerArg.cliPath,
2215
+ controllerArg.command,
2216
+ { allowMissingAbsolutePath: true },
2217
+ )
2218
+ ) throw new Error('Unable to verify the upgrade controller identity.');
2219
+ return await this.mutateTransaction(tokenArg, (current) => ({
2220
+ ...current,
2221
+ controller: { ...controllerArg },
2222
+ }));
2223
+ }
2224
+
2225
+ public async transactionControllerIsLive(transactionArg: TUpgradeTransaction): Promise<boolean> {
2226
+ const controller = transactionArg.controller;
2227
+ if (!controller) return false;
2228
+ const identity = await readControllerProcessIdentity(controller.pid);
2229
+ return Boolean(
2230
+ identity?.fingerprint === controller.fingerprint
2231
+ && identity.processGroupId === controller.processGroupId
2232
+ && await processIdentityHasCliCommand(
2233
+ identity,
2234
+ controller.cliPath,
2235
+ controller.command,
2236
+ { allowMissingAbsolutePath: true },
2237
+ )
2238
+ );
2239
+ }
2240
+
2241
+ public async transactionWorkerIsLive(transactionArg: TUpgradeTransaction): Promise<boolean> {
2242
+ const worker = transactionArg.worker;
2243
+ if (!worker) return false;
2244
+ const identity = await readControllerProcessIdentity(worker.pid);
2245
+ if (Boolean(
2246
+ identity?.fingerprint === worker.fingerprint
2247
+ && await processIdentityHasCliCommand(
2248
+ identity,
2249
+ worker.cliPath,
2250
+ '__upgrade-worker',
2251
+ { allowMissingAbsolutePath: true },
2252
+ )
2253
+ )) return true;
2254
+ if (worker.processGroupId !== worker.pid) return false;
2255
+ return (await readProcessGroupMemberPids(worker.processGroupId))
2256
+ .some((processIdArg) => processIdArg !== worker.pid);
2257
+ }
2258
+
2259
+ public async terminateTransactionWorker(
2260
+ transactionArg: TUpgradeTransaction,
2261
+ ): Promise<void> {
2262
+ const worker = transactionArg.worker;
2263
+ if (!worker) return;
2264
+ await this.terminateUpgradeWorkerCandidate(worker);
2265
+ }
2266
+
2267
+ public async terminateUpgradeWorkerCandidate(
2268
+ worker: NonNullable<TUpgradeTransaction['worker']>,
2269
+ ): Promise<void> {
2270
+ const identity = await readControllerProcessIdentity(worker.pid);
2271
+ if (identity) {
2272
+ if (
2273
+ identity.fingerprint !== worker.fingerprint
2274
+ || identity.processGroupId !== worker.processGroupId
2275
+ || !identity.processGroupLeader
2276
+ || !await processIdentityHasCliCommand(
2277
+ identity,
2278
+ worker.cliPath,
2279
+ '__upgrade-worker',
2280
+ { allowMissingAbsolutePath: true },
2281
+ )
2282
+ ) throw new Error('The stalled upgrade worker identity changed unexpectedly.');
2283
+ } else if (
2284
+ worker.processGroupId !== worker.pid
2285
+ || (await readProcessGroupMemberPids(worker.processGroupId)).length === 0
2286
+ ) {
2287
+ return;
2288
+ }
2289
+ const signalOwnedWorker = (signalArg: NodeJS.Signals): void => {
2290
+ try {
2291
+ process.kill(-worker.processGroupId, signalArg);
2292
+ } catch (errorArg) {
2293
+ if ((errorArg as NodeJS.ErrnoException).code !== 'ESRCH') throw errorArg;
2294
+ }
2295
+ };
2296
+ const waitForDrain = async (): Promise<boolean> => {
2297
+ const deadline = Date.now() + workerTerminationGraceMs;
2298
+ while (Date.now() < deadline) {
2299
+ if ((await readProcessGroupMemberPids(worker.processGroupId)).length === 0) return true;
2300
+ await new Promise((resolve) => setTimeout(resolve, 50));
2301
+ }
2302
+ return (await readProcessGroupMemberPids(worker.processGroupId)).length === 0;
2303
+ };
2304
+ signalOwnedWorker('SIGTERM');
2305
+ if (await waitForDrain()) return;
2306
+ signalOwnedWorker('SIGKILL');
2307
+ if (!await waitForDrain()) throw new Error('The stalled upgrade worker group did not terminate.');
2308
+ }
2309
+
2310
+ public async readSanitizedUpgradeStatus(
2311
+ tokenArg: string,
2312
+ ): Promise<IControllerUpgradeStatus | undefined> {
2313
+ const transaction = await this.readTransaction(tokenArg);
2314
+ if (!transaction.targetVersion) return undefined;
2315
+ if (transaction.retainedUntil !== undefined && transaction.retainedUntil < Date.now()) return undefined;
2316
+ const phase = transaction.phase === 'starting'
2317
+ || transaction.phase === 'checking'
2318
+ || transaction.phase === 'stopping'
2319
+ ? 'preparing'
2320
+ : transaction.phase;
2321
+ return {
2322
+ fromVersion: transaction.sourceVersion,
2323
+ toVersion: transaction.targetVersion,
2324
+ phase,
2325
+ };
2326
+ }
2327
+
2328
+ public async finishTransaction(
2329
+ tokenArg: string,
2330
+ successArg: boolean,
2331
+ messageArg: string,
2332
+ errorArg?: string,
2333
+ ): Promise<TUpgradeTransaction> {
2334
+ return await this.mutateTransaction(tokenArg, (current) => {
2335
+ if (current.terminal) return current;
2336
+ return {
2337
+ ...current,
2338
+ phase: successArg ? 'completed' : 'failed',
2339
+ message: assertBoundedText(messageArg, 'The upgrade terminal message', 4_096),
2340
+ retainedUntil: Date.now() + terminalStatusRetentionMs,
2341
+ terminal: {
2342
+ success: successArg,
2343
+ ...(errorArg === undefined
2344
+ ? {}
2345
+ : { error: assertBoundedText(errorArg, 'The upgrade terminal error', 4_096) }),
2346
+ },
2347
+ };
2348
+ });
2349
+ }
2350
+
2351
+ public async createLaunchGrant(optionsArg: {
2352
+ token: string;
2353
+ port: number;
2354
+ packageName: string;
2355
+ packageVersion: string;
2356
+ cliPath: string;
2357
+ controller: IUpgradeExpectedController;
2358
+ }): Promise<void> {
2359
+ await this.init();
2360
+ if (
2361
+ typeof optionsArg.packageName !== 'string'
2362
+ || optionsArg.packageName.length < 1
2363
+ || optionsArg.packageName.length > 256
2364
+ || typeof optionsArg.packageVersion !== 'string'
2365
+ || optionsArg.packageVersion.length < 1
2366
+ || optionsArg.packageVersion.length > 128
2367
+ || !plugins.path.isAbsolute(optionsArg.cliPath)
2368
+ ) {
2369
+ throw new Error(`The ${currentCliName} upgrade launch grant package binding is invalid.`);
2370
+ }
2371
+ const grant: IUpgradeLaunchGrant = {
2372
+ version: coordinationVersion,
2373
+ tokenHash: hashToken(optionsArg.token),
2374
+ expiresAt: Date.now() + 30_000,
2375
+ port: assertPort(optionsArg.port),
2376
+ packageName: optionsArg.packageName,
2377
+ packageVersion: optionsArg.packageVersion,
2378
+ cliPath: optionsArg.cliPath,
2379
+ controller: parseExpectedController(optionsArg.controller),
2380
+ };
2381
+ await writePrivateJson(this.tokenFilePath('grant', optionsArg.token), grant);
2382
+ }
2383
+
2384
+ public async removeLaunchGrant(tokenArg: string): Promise<void> {
2385
+ await plugins.fs.promises.unlink(this.tokenFilePath('grant', tokenArg)).catch((errorArg) => {
2386
+ if ((errorArg as NodeJS.ErrnoException).code !== 'ENOENT') throw errorArg;
2387
+ });
2388
+ await syncDirectory(this.baseDirectory);
2389
+ }
2390
+
2391
+ public async consumeLaunchGrant(optionsArg: {
2392
+ token: string;
2393
+ port: number;
2394
+ packageName: string;
2395
+ packageVersion: string;
2396
+ cliPath: string;
2397
+ controller: IUpgradeExpectedController;
2398
+ }): Promise<void> {
2399
+ await this.init();
2400
+ const token = assertToken(optionsArg.token);
2401
+ const grantPath = this.tokenFilePath('grant', token);
2402
+ const consumedPath = `${grantPath}.consuming-${process.pid}-${plugins.crypto.randomBytes(4).toString('hex')}`;
2403
+ await plugins.fs.promises.rename(grantPath, consumedPath).catch((errorArg) => {
2404
+ if ((errorArg as NodeJS.ErrnoException).code === 'ENOENT') {
2405
+ throw new Error(`The ${currentCliName} upgrade launch grant is missing or already consumed.`);
2406
+ }
2407
+ throw errorArg;
2408
+ });
2409
+ try {
2410
+ const value = await readPrivateJson(consumedPath);
2411
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
2412
+ throw new Error(`The ${currentCliName} upgrade launch grant is malformed.`);
2413
+ }
2414
+ const grant = value as Record<string, unknown>;
2415
+ if (!exactKeys(grant, [
2416
+ 'version',
2417
+ 'tokenHash',
2418
+ 'expiresAt',
2419
+ 'port',
2420
+ 'packageName',
2421
+ 'packageVersion',
2422
+ 'cliPath',
2423
+ 'controller',
2424
+ ])) {
2425
+ throw new Error(`The ${currentCliName} upgrade launch grant has unexpected fields.`);
2426
+ }
2427
+ const controller = parseExpectedController(grant.controller);
2428
+ const expected = parseExpectedController(optionsArg.controller);
2429
+ if (
2430
+ grant.version !== coordinationVersion
2431
+ || typeof grant.expiresAt !== 'number'
2432
+ || !Number.isSafeInteger(grant.expiresAt)
2433
+ || grant.expiresAt < Date.now()
2434
+ || grant.port !== assertPort(optionsArg.port)
2435
+ || grant.packageName !== optionsArg.packageName
2436
+ || grant.packageVersion !== optionsArg.packageVersion
2437
+ || grant.cliPath !== optionsArg.cliPath
2438
+ || controller.pid !== expected.pid
2439
+ || controller.processGroupId !== expected.processGroupId
2440
+ || controller.processFingerprint !== expected.processFingerprint
2441
+ || typeof grant.tokenHash !== 'string'
2442
+ || !upgradeTokenHashPattern.test(grant.tokenHash)
2443
+ || !tokensEqual(grant.tokenHash, hashToken(token))
2444
+ ) {
2445
+ throw new Error(`The ${currentCliName} upgrade launch grant binding is invalid or expired.`);
2446
+ }
2447
+ } finally {
2448
+ await plugins.fs.promises.unlink(consumedPath).catch(() => undefined);
2449
+ await syncDirectory(this.baseDirectory);
2450
+ }
2451
+ }
2452
+
2453
+ private actionGrantFilePath(actionArg: TUpgradeControllerAction, tokenArg: string): string {
2454
+ return plugins.path.join(
2455
+ this.baseDirectory,
2456
+ `action-${actionArg}-${coordinationPathHash(assertToken(tokenArg))}.json`,
2457
+ );
2458
+ }
2459
+
2460
+ public async removeControllerActionGrant(
2461
+ actionArg: TUpgradeControllerAction,
2462
+ tokenArg: string,
2463
+ ): Promise<void> {
2464
+ await plugins.fs.promises.unlink(this.actionGrantFilePath(actionArg, tokenArg)).catch((errorArg) => {
2465
+ if ((errorArg as NodeJS.ErrnoException).code !== 'ENOENT') throw errorArg;
2466
+ });
2467
+ await syncDirectory(this.baseDirectory);
2468
+ }
2469
+
2470
+ public async createControllerActionGrant(optionsArg: {
2471
+ token: string;
2472
+ action: TUpgradeControllerAction;
2473
+ mode?: 'continue' | 'compensate' | 'reopen';
2474
+ packageVersion: string;
2475
+ controller: IUpgradeExpectedController;
2476
+ }): Promise<void> {
2477
+ await this.init();
2478
+ if (
2479
+ (optionsArg.action === 'prepare' && optionsArg.mode !== undefined)
2480
+ || (optionsArg.action === 'finalize' && optionsArg.mode === undefined)
2481
+ ) throw new Error('The upgrade controller action mode is invalid.');
2482
+ const grant: IUpgradeControllerActionGrant = {
2483
+ version: coordinationVersion,
2484
+ tokenHash: hashToken(optionsArg.token),
2485
+ action: optionsArg.action,
2486
+ ...(optionsArg.mode === undefined ? {} : { mode: optionsArg.mode }),
2487
+ expiresAt: Date.now() + 30_000,
2488
+ packageVersion: assertBoundedText(
2489
+ optionsArg.packageVersion,
2490
+ 'The upgrade action package version',
2491
+ 128,
2492
+ ),
2493
+ controller: parseExpectedController(optionsArg.controller),
2494
+ };
2495
+ await writePrivateJson(
2496
+ this.actionGrantFilePath(optionsArg.action, optionsArg.token),
2497
+ grant,
2498
+ );
2499
+ }
2500
+
2501
+ public async consumeControllerActionGrant(optionsArg: {
2502
+ token: string;
2503
+ action: TUpgradeControllerAction;
2504
+ mode?: 'continue' | 'compensate' | 'reopen';
2505
+ packageVersion: string;
2506
+ controller: IUpgradeExpectedController;
2507
+ }): Promise<void> {
2508
+ await this.init();
2509
+ const grantPath = this.actionGrantFilePath(optionsArg.action, optionsArg.token);
2510
+ const consumedPath = `${grantPath}.consuming-${process.pid}-${plugins.crypto.randomBytes(4).toString('hex')}`;
2511
+ await plugins.fs.promises.rename(grantPath, consumedPath).catch((errorArg) => {
2512
+ if ((errorArg as NodeJS.ErrnoException).code === 'ENOENT') {
2513
+ throw new Error('The upgrade controller action grant is missing or already consumed.');
2514
+ }
2515
+ throw errorArg;
2516
+ });
2517
+ try {
2518
+ const raw = await readPrivateJson(consumedPath);
2519
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
2520
+ throw new Error('The upgrade controller action grant is malformed.');
2521
+ }
2522
+ const grant = raw as Record<string, unknown>;
2523
+ const expectedController = parseExpectedController(optionsArg.controller);
2524
+ const controller = parseExpectedController(grant.controller);
2525
+ if (
2526
+ !exactKeys(grant, [
2527
+ 'version', 'tokenHash', 'action', 'expiresAt', 'packageVersion', 'controller',
2528
+ ...(grant.mode === undefined ? [] : ['mode']),
2529
+ ])
2530
+ || grant.version !== coordinationVersion
2531
+ || grant.action !== optionsArg.action
2532
+ || grant.mode !== optionsArg.mode
2533
+ || !Number.isSafeInteger(grant.expiresAt)
2534
+ || (grant.expiresAt as number) < Date.now()
2535
+ || grant.packageVersion !== optionsArg.packageVersion
2536
+ || controller.pid !== expectedController.pid
2537
+ || controller.processGroupId !== expectedController.processGroupId
2538
+ || controller.processFingerprint !== expectedController.processFingerprint
2539
+ || typeof grant.tokenHash !== 'string'
2540
+ || !tokensEqual(grant.tokenHash, hashToken(optionsArg.token))
2541
+ ) throw new Error('The upgrade controller action grant binding is invalid or expired.');
2542
+ } finally {
2543
+ await plugins.fs.promises.unlink(consumedPath).catch(() => undefined);
2544
+ await syncDirectory(this.baseDirectory);
2545
+ }
2546
+ }
2547
+
2548
+ public async acknowledgeWorker(tokenArg: string): Promise<void> {
2549
+ await this.init();
2550
+ const acknowledgement = {
2551
+ version: coordinationVersion,
2552
+ tokenHash: hashToken(tokenArg),
2553
+ };
2554
+ const acknowledgementPath = this.tokenFilePath('ack', tokenArg);
2555
+ try {
2556
+ await writePrivateJson(acknowledgementPath, acknowledgement);
2557
+ } catch (errorArg) {
2558
+ if ((errorArg as NodeJS.ErrnoException).code !== 'EEXIST') throw errorArg;
2559
+ const existing = await readPrivateJson(acknowledgementPath);
2560
+ if (
2561
+ !existing
2562
+ || typeof existing !== 'object'
2563
+ || Array.isArray(existing)
2564
+ || !exactKeys(existing as Record<string, unknown>, ['version', 'tokenHash'])
2565
+ || (existing as Record<string, unknown>).version !== coordinationVersion
2566
+ || (existing as Record<string, unknown>).tokenHash !== acknowledgement.tokenHash
2567
+ ) throw new Error('The existing upgrade acknowledgement is invalid.');
2568
+ }
2569
+ }
2570
+
2571
+ public async waitForWorkerAcknowledgement(
2572
+ tokenArg: string,
2573
+ timeoutMsArg = 10_000,
2574
+ ): Promise<void> {
2575
+ const token = assertToken(tokenArg);
2576
+ const ackPath = this.tokenFilePath('ack', token);
2577
+ const deadline = Date.now() + timeoutMsArg;
2578
+ while (Date.now() < deadline) {
2579
+ try {
2580
+ const value = await readPrivateJson(ackPath);
2581
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
2582
+ throw new Error(`The ${currentCliName} upgrade acknowledgement is malformed.`);
2583
+ }
2584
+ const ack = value as Record<string, unknown>;
2585
+ if (
2586
+ !exactKeys(ack, ['version', 'tokenHash'])
2587
+ || ack.version !== coordinationVersion
2588
+ || typeof ack.tokenHash !== 'string'
2589
+ || !upgradeTokenHashPattern.test(ack.tokenHash)
2590
+ || !tokensEqual(ack.tokenHash, hashToken(token))
2591
+ ) {
2592
+ throw new Error(`The ${currentCliName} upgrade acknowledgement binding is invalid.`);
2593
+ }
2594
+ await plugins.fs.promises.unlink(ackPath);
2595
+ await syncDirectory(this.baseDirectory);
2596
+ return;
2597
+ } catch (errorArg) {
2598
+ if ((errorArg as NodeJS.ErrnoException).code !== 'ENOENT') throw errorArg;
2599
+ }
2600
+ await new Promise((resolve) => setTimeout(resolve, 50));
2601
+ }
2602
+ throw new Error(`The ${currentCliName} upgrade caller did not acknowledge the detached worker.`);
2603
+ }
2604
+ }
2605
+
2606
+ export const createUpgradeToken = (): string => plugins.crypto
2607
+ .randomBytes(32)
2608
+ .toString('base64url');
2609
+
2610
+ export const serializeUpgradeWorkerPayload = (payloadArg: IUpgradeWorkerPayload): string => {
2611
+ assertToken(payloadArg.token);
2612
+ const payload = {
2613
+ version: coordinationVersion,
2614
+ port: assertPort(payloadArg.port),
2615
+ gracePeriodMs: payloadArg.gracePeriodMs,
2616
+ continueSessions: payloadArg.continueSessions,
2617
+ ...(payloadArg.expectedController
2618
+ ? { expectedController: parseExpectedController(payloadArg.expectedController) }
2619
+ : {}),
2620
+ };
2621
+ return Buffer.from(JSON.stringify(payload), 'utf8').toString('base64url');
2622
+ };
2623
+
2624
+ export const parseUpgradeWorkerPayload = (
2625
+ payloadArg: unknown,
2626
+ tokenArg: unknown,
2627
+ ): IUpgradeWorkerPayload => {
2628
+ if (typeof payloadArg !== 'string' || payloadArg.length < 8 || payloadArg.length > 4096) {
2629
+ throw new Error(`The ${currentCliName} upgrade worker payload is invalid.`);
2630
+ }
2631
+ let value: unknown;
2632
+ try {
2633
+ value = JSON.parse(Buffer.from(payloadArg, 'base64url').toString('utf8')) as unknown;
2634
+ } catch {
2635
+ throw new Error(`The ${currentCliName} upgrade worker payload is malformed.`);
2636
+ }
2637
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
2638
+ throw new Error(`The ${currentCliName} upgrade worker payload is malformed.`);
2639
+ }
2640
+ const payload = value as Record<string, unknown>;
2641
+ if (!exactKeys(payload, [
2642
+ 'version',
2643
+ 'port',
2644
+ 'gracePeriodMs',
2645
+ 'continueSessions',
2646
+ ...(payload.expectedController === undefined ? [] : ['expectedController']),
2647
+ ])) {
2648
+ throw new Error(`The ${currentCliName} upgrade worker payload has unexpected fields.`);
2649
+ }
2650
+ if (payload.version !== coordinationVersion) {
2651
+ throw new Error(`The ${currentCliName} upgrade worker payload version is unsupported.`);
2652
+ }
2653
+ return {
2654
+ version: coordinationVersion,
2655
+ token: assertToken(tokenArg),
2656
+ port: assertPort(payload.port),
2657
+ gracePeriodMs: (() => {
2658
+ if (
2659
+ !Number.isSafeInteger(payload.gracePeriodMs)
2660
+ || (payload.gracePeriodMs as number) < 1_000
2661
+ || (payload.gracePeriodMs as number) > 60 * 60 * 1000
2662
+ ) throw new Error(`The ${currentCliName} upgrade grace period is invalid.`);
2663
+ return payload.gracePeriodMs as number;
2664
+ })(),
2665
+ continueSessions: (() => {
2666
+ if (typeof payload.continueSessions !== 'boolean') {
2667
+ throw new Error(`The ${currentCliName} upgrade continuation option is invalid.`);
2668
+ }
2669
+ return payload.continueSessions;
2670
+ })(),
2671
+ ...(payload.expectedController
2672
+ ? { expectedController: parseExpectedController(payload.expectedController) }
2673
+ : {}),
2674
+ };
2675
+ };