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,560 @@
1
+ import * as plugins from '../ts/plugins.js';
2
+ import {
3
+ isSocketListening,
4
+ } from '../ts/functions.embeddeddb.js';
5
+
6
+ const migrationVersion = 1;
7
+ const journalFileName = '.smartdb-location-migration.json';
8
+ const maximumJournalBytes = 16 * 1024;
9
+
10
+ interface IDirectoryIdentity {
11
+ device: string;
12
+ inode: string;
13
+ }
14
+
15
+ interface IMigrationJournalBase extends IDirectoryIdentity {
16
+ version: typeof migrationVersion;
17
+ sourceDirectory: string;
18
+ destinationDirectory: string;
19
+ }
20
+
21
+ interface IPreparedMigrationJournal extends IMigrationJournalBase {
22
+ phase: 'prepared';
23
+ }
24
+
25
+ interface ICompletedMigrationJournal extends IMigrationJournalBase {
26
+ phase: 'completed';
27
+ destinationDevice: string;
28
+ destinationInode: string;
29
+ contentDigestSha256: string;
30
+ }
31
+
32
+ type TMigrationJournal = IPreparedMigrationJournal | ICompletedMigrationJournal;
33
+
34
+ export interface IEmbeddedDatabaseLocationMigrationOptions {
35
+ sourceDirectory: string;
36
+ destinationDirectory: string;
37
+ sourceSocketPath: string;
38
+ destinationSocketPath: string;
39
+ }
40
+
41
+ export interface IEmbeddedDatabaseLocationMigrationResult {
42
+ localDb: plugins.smartdb.LocalSmartDb;
43
+ connectionUri: string;
44
+ }
45
+
46
+ export interface IRebaseCompletedEmbeddedDatabaseLocationMigrationOptions {
47
+ sourceDirectory: string;
48
+ previousDestinationDirectory: string;
49
+ destinationDirectory: string;
50
+ }
51
+
52
+ const directoryIdentity = async (
53
+ directoryArg: string,
54
+ ): Promise<IDirectoryIdentity | undefined> => {
55
+ try {
56
+ const stats = await plugins.fs.promises.lstat(directoryArg, { bigint: true });
57
+ if (!stats.isDirectory() || stats.isSymbolicLink()) {
58
+ throw new Error(`Controller database path must be a real directory: ${directoryArg}`);
59
+ }
60
+ return {
61
+ device: stats.dev.toString(10),
62
+ inode: stats.ino.toString(10),
63
+ };
64
+ } catch (errorArg) {
65
+ if ((errorArg as NodeJS.ErrnoException).code === 'ENOENT') return undefined;
66
+ throw errorArg;
67
+ }
68
+ };
69
+
70
+ const identitiesEqual = (
71
+ leftArg: IDirectoryIdentity,
72
+ rightArg: IDirectoryIdentity,
73
+ ): boolean => leftArg.device === rightArg.device && leftArg.inode === rightArg.inode;
74
+
75
+ const assertExactKeys = (
76
+ valueArg: Record<string, unknown>,
77
+ expectedArg: string[],
78
+ ): void => {
79
+ const actual = Object.keys(valueArg).sort();
80
+ const expected = [...expectedArg].sort();
81
+ if (actual.length !== expected.length || actual.some((key, index) => key !== expected[index])) {
82
+ throw new Error('The embedded database migration journal has unexpected fields.');
83
+ }
84
+ };
85
+
86
+ const parseJournal = (valueArg: unknown): TMigrationJournal => {
87
+ if (!valueArg || typeof valueArg !== 'object' || Array.isArray(valueArg)) {
88
+ throw new Error('The embedded database migration journal is malformed.');
89
+ }
90
+ const value = valueArg as Record<string, unknown>;
91
+ if (value.version !== migrationVersion) {
92
+ throw new Error('The embedded database migration journal version is unsupported.');
93
+ }
94
+ if (value.phase !== 'prepared' && value.phase !== 'completed') {
95
+ throw new Error('The embedded database migration journal phase is invalid.');
96
+ }
97
+ assertExactKeys(value, [
98
+ 'version',
99
+ 'phase',
100
+ 'sourceDirectory',
101
+ 'destinationDirectory',
102
+ 'device',
103
+ 'inode',
104
+ ...(value.phase === 'completed'
105
+ ? ['destinationDevice', 'destinationInode', 'contentDigestSha256']
106
+ : []),
107
+ ]);
108
+ for (const key of ['sourceDirectory', 'destinationDirectory', 'device', 'inode']) {
109
+ if (typeof value[key] !== 'string' || value[key].length === 0) {
110
+ throw new Error(`The embedded database migration journal ${key} is invalid.`);
111
+ }
112
+ }
113
+ if (
114
+ !/^\d+$/.test(value.device as string)
115
+ || !/^\d+$/.test(value.inode as string)
116
+ ) {
117
+ throw new Error('The embedded database migration journal source identity is invalid.');
118
+ }
119
+ if (value.phase === 'completed') {
120
+ if (
121
+ typeof value.destinationDevice !== 'string'
122
+ || !/^\d+$/.test(value.destinationDevice)
123
+ || typeof value.destinationInode !== 'string'
124
+ || !/^\d+$/.test(value.destinationInode)
125
+ || typeof value.contentDigestSha256 !== 'string'
126
+ || !/^[a-f0-9]{64}$/.test(value.contentDigestSha256)
127
+ ) {
128
+ throw new Error('The embedded database migration journal completion is invalid.');
129
+ }
130
+ }
131
+ return value as unknown as TMigrationJournal;
132
+ };
133
+
134
+ const readJournal = async (journalPathArg: string): Promise<TMigrationJournal | undefined> => {
135
+ let handle: plugins.fs.promises.FileHandle;
136
+ try {
137
+ handle = await plugins.fs.promises.open(
138
+ journalPathArg,
139
+ plugins.fs.constants.O_RDONLY | plugins.fs.constants.O_NOFOLLOW,
140
+ );
141
+ } catch (errorArg) {
142
+ if ((errorArg as NodeJS.ErrnoException).code === 'ENOENT') return undefined;
143
+ throw errorArg;
144
+ }
145
+ try {
146
+ const stats = await handle.stat();
147
+ if (!stats.isFile() || stats.size < 2 || stats.size > maximumJournalBytes) {
148
+ throw new Error('The embedded database migration journal is not a bounded regular file.');
149
+ }
150
+ if (typeof process.getuid === 'function' && stats.uid !== process.getuid()) {
151
+ throw new Error('The embedded database migration journal is not owned by the current user.');
152
+ }
153
+ if ((stats.mode & 0o077) !== 0) {
154
+ throw new Error('The embedded database migration journal must not be group or world accessible.');
155
+ }
156
+ return parseJournal(JSON.parse(await handle.readFile('utf8')) as unknown);
157
+ } finally {
158
+ await handle.close();
159
+ }
160
+ };
161
+
162
+ const syncDirectory = async (directoryArg: string): Promise<void> => {
163
+ const handle = await plugins.fs.promises.open(directoryArg, plugins.fs.constants.O_RDONLY);
164
+ try {
165
+ await handle.sync();
166
+ } finally {
167
+ await handle.close();
168
+ }
169
+ };
170
+
171
+ const writeJournal = async (
172
+ journalPathArg: string,
173
+ journalArg: TMigrationJournal,
174
+ ): Promise<void> => {
175
+ const parentDirectory = plugins.path.dirname(journalPathArg);
176
+ await plugins.fs.promises.mkdir(parentDirectory, { recursive: true, mode: 0o700 });
177
+ const parentStats = await plugins.fs.promises.lstat(parentDirectory);
178
+ if (!parentStats.isDirectory() || parentStats.isSymbolicLink()) {
179
+ throw new Error(`Controller data path must be a real directory: ${parentDirectory}`);
180
+ }
181
+ const temporaryPath = `${journalPathArg}.${process.pid}-${plugins.crypto.randomBytes(8).toString('hex')}.tmp`;
182
+ let handle: plugins.fs.promises.FileHandle | undefined;
183
+ try {
184
+ handle = await plugins.fs.promises.open(
185
+ temporaryPath,
186
+ plugins.fs.constants.O_WRONLY
187
+ | plugins.fs.constants.O_CREAT
188
+ | plugins.fs.constants.O_EXCL
189
+ | plugins.fs.constants.O_NOFOLLOW,
190
+ 0o600,
191
+ );
192
+ await handle.writeFile(`${JSON.stringify(journalArg)}\n`, 'utf8');
193
+ await handle.sync();
194
+ await handle.close();
195
+ handle = undefined;
196
+ await plugins.fs.promises.rename(temporaryPath, journalPathArg);
197
+ await syncDirectory(parentDirectory);
198
+ } catch (errorArg) {
199
+ if (handle) await handle.close().catch(() => undefined);
200
+ await plugins.fs.promises.unlink(temporaryPath).catch(() => undefined);
201
+ throw errorArg;
202
+ }
203
+ };
204
+
205
+ const assertJournalMatches = (
206
+ journalArg: TMigrationJournal,
207
+ optionsArg: IEmbeddedDatabaseLocationMigrationOptions,
208
+ ): void => {
209
+ if (
210
+ journalArg.sourceDirectory !== optionsArg.sourceDirectory
211
+ || journalArg.destinationDirectory !== optionsArg.destinationDirectory
212
+ ) {
213
+ throw new Error('The embedded database migration journal belongs to another migration.');
214
+ }
215
+ };
216
+
217
+ export const rebaseCompletedEmbeddedDatabaseLocationMigration = async (
218
+ optionsArg: IRebaseCompletedEmbeddedDatabaseLocationMigrationOptions,
219
+ ): Promise<void> => {
220
+ const sourceDirectory = plugins.path.resolve(optionsArg.sourceDirectory);
221
+ const previousDestinationDirectory = plugins.path.resolve(
222
+ optionsArg.previousDestinationDirectory,
223
+ );
224
+ const destinationDirectory = plugins.path.resolve(optionsArg.destinationDirectory);
225
+ const previousJournalPath = plugins.path.join(
226
+ plugins.path.dirname(previousDestinationDirectory),
227
+ journalFileName,
228
+ );
229
+ const destinationJournalPath = plugins.path.join(
230
+ plugins.path.dirname(destinationDirectory),
231
+ journalFileName,
232
+ );
233
+ const previous = await readJournal(previousJournalPath);
234
+ if (!previous || previous.phase !== 'completed') {
235
+ throw new Error('The embedded database migration journal is not completed for rebasing.');
236
+ }
237
+ assertJournalMatches(previous, {
238
+ sourceDirectory,
239
+ destinationDirectory: previousDestinationDirectory,
240
+ sourceSocketPath: '',
241
+ destinationSocketPath: '',
242
+ });
243
+ const destinationIdentity = await directoryIdentity(destinationDirectory);
244
+ if (!destinationIdentity || !identitiesEqual(destinationIdentity, {
245
+ device: previous.destinationDevice,
246
+ inode: previous.destinationInode,
247
+ })) {
248
+ throw new Error('The rebased embedded database directory identity does not match its journal.');
249
+ }
250
+ const rebased: ICompletedMigrationJournal = {
251
+ ...previous,
252
+ destinationDirectory,
253
+ destinationDevice: destinationIdentity.device,
254
+ destinationInode: destinationIdentity.inode,
255
+ };
256
+ const existing = await readJournal(destinationJournalPath);
257
+ if (existing) {
258
+ if (
259
+ existing.phase !== 'completed'
260
+ || existing.sourceDirectory !== rebased.sourceDirectory
261
+ || existing.destinationDirectory !== rebased.destinationDirectory
262
+ || existing.device !== rebased.device
263
+ || existing.inode !== rebased.inode
264
+ || existing.destinationDevice !== rebased.destinationDevice
265
+ || existing.destinationInode !== rebased.destinationInode
266
+ || existing.contentDigestSha256 !== rebased.contentDigestSha256
267
+ ) {
268
+ throw new Error('The rebased embedded database migration journal conflicts with its source.');
269
+ }
270
+ return;
271
+ }
272
+ await writeJournal(destinationJournalPath, rebased);
273
+ const persisted = await readJournal(destinationJournalPath);
274
+ if (
275
+ !persisted
276
+ || persisted.phase !== 'completed'
277
+ || persisted.sourceDirectory !== rebased.sourceDirectory
278
+ || persisted.destinationDirectory !== rebased.destinationDirectory
279
+ || persisted.destinationDevice !== rebased.destinationDevice
280
+ || persisted.destinationInode !== rebased.destinationInode
281
+ || persisted.contentDigestSha256 !== rebased.contentDigestSha256
282
+ ) {
283
+ throw new Error('The rebased embedded database migration journal was not persisted.');
284
+ }
285
+ };
286
+
287
+ const databaseNamesFromCollections = (
288
+ collectionsArg: plugins.smartdb.ICollectionInfo[],
289
+ ): string[] => [...new Set(collectionsArg.map((collection) => collection.db))].sort();
290
+
291
+ const assertDatabaseNamesMatch = (
292
+ sourceArg: string[],
293
+ destinationArg: string[],
294
+ ): void => {
295
+ if (
296
+ sourceArg.length !== destinationArg.length
297
+ || sourceArg.some((databaseName, index) => databaseName !== destinationArg[index])
298
+ ) {
299
+ throw new Error('The migrated SmartDB root contains a different logical database set.');
300
+ }
301
+ };
302
+
303
+ const aggregateContentDigests = (
304
+ digestsArg: Map<string, plugins.smartdb.ISmartDbDatabaseContentDigest>,
305
+ ): string => {
306
+ const hash = plugins.crypto.createHash('sha256');
307
+ for (const [databaseName, digest] of [...digestsArg.entries()].sort(([left], [right]) => (
308
+ left < right ? -1 : left > right ? 1 : 0
309
+ ))) {
310
+ hash.update(databaseName, 'utf8');
311
+ hash.update('\0');
312
+ hash.update(digest.sha256, 'ascii');
313
+ hash.update('\0');
314
+ }
315
+ return hash.digest('hex');
316
+ };
317
+
318
+ const assertCurrentStorageFormat = async (directoryArg: string): Promise<void> => {
319
+ const databaseEntries = await plugins.fs.promises.readdir(directoryArg, { withFileTypes: true });
320
+ for (const databaseEntry of databaseEntries) {
321
+ if (!databaseEntry.isDirectory() || databaseEntry.name.startsWith('.__rustdb_')) continue;
322
+ const collectionEntries = await plugins.fs.promises.readdir(
323
+ plugins.path.join(directoryArg, databaseEntry.name),
324
+ { withFileTypes: true },
325
+ );
326
+ if (collectionEntries.some((entry) => (
327
+ entry.isFile()
328
+ && entry.name.endsWith('.json')
329
+ && !entry.name.endsWith('.indexes.json')
330
+ ))) {
331
+ throw new Error(
332
+ `The legacy controller database uses an old SmartDB storage format at ${directoryArg}. `
333
+ + 'Start it once with the previous controller version before upgrading.',
334
+ );
335
+ }
336
+ }
337
+ };
338
+
339
+ const assertDigestMatch = (
340
+ sourceArg: plugins.smartdb.ISmartDbDatabaseContentDigest,
341
+ destinationArg: plugins.smartdb.ISmartDbDatabaseContentDigest,
342
+ ): void => {
343
+ if (
344
+ sourceArg.format !== destinationArg.format
345
+ || sourceArg.algorithm !== destinationArg.algorithm
346
+ || sourceArg.sha256 !== destinationArg.sha256
347
+ || sourceArg.scannedBsonBytes !== destinationArg.scannedBsonBytes
348
+ || sourceArg.collections !== destinationArg.collections
349
+ || sourceArg.documents !== destinationArg.documents
350
+ || sourceArg.indexes !== destinationArg.indexes
351
+ ) {
352
+ throw new Error('The migrated controller database content digest does not match its source.');
353
+ }
354
+ };
355
+
356
+ export class EmbeddedDatabaseLocationMigration {
357
+ private readonly journalPath: string;
358
+ private sourceDb?: plugins.smartdb.LocalSmartDb;
359
+ private importDestinationDb?: plugins.smartdb.LocalSmartDb;
360
+ private operationalDestinationDb?: plugins.smartdb.LocalSmartDb;
361
+
362
+ constructor(private readonly options: IEmbeddedDatabaseLocationMigrationOptions) {
363
+ this.options = {
364
+ ...options,
365
+ sourceDirectory: plugins.path.resolve(options.sourceDirectory),
366
+ destinationDirectory: plugins.path.resolve(options.destinationDirectory),
367
+ };
368
+ this.journalPath = plugins.path.join(
369
+ plugins.path.dirname(this.options.destinationDirectory),
370
+ journalFileName,
371
+ );
372
+ }
373
+
374
+ public get hasOwnedResources(): boolean {
375
+ return Boolean(this.sourceDb || this.importDestinationDb || this.operationalDestinationDb);
376
+ }
377
+
378
+ public async stop(): Promise<void> {
379
+ const errors: unknown[] = [];
380
+ for (const entry of [
381
+ ['operationalDestinationDb', this.operationalDestinationDb],
382
+ ['importDestinationDb', this.importDestinationDb],
383
+ ['sourceDb', this.sourceDb],
384
+ ] as const) {
385
+ const [key, database] = entry;
386
+ if (!database) continue;
387
+ try {
388
+ await database.stop();
389
+ if (this[key] === database) this[key] = undefined;
390
+ } catch (errorArg) {
391
+ errors.push(errorArg);
392
+ }
393
+ }
394
+ if (errors.length === 1) throw errors[0];
395
+ if (errors.length > 1) {
396
+ throw new AggregateError(errors, 'Embedded database migration cleanup failed.');
397
+ }
398
+ }
399
+
400
+ public async run(): Promise<IEmbeddedDatabaseLocationMigrationResult | undefined> {
401
+ if (this.options.sourceDirectory === this.options.destinationDirectory) return undefined;
402
+
403
+ let journal = await readJournal(this.journalPath);
404
+ if (journal) assertJournalMatches(journal, this.options);
405
+ const sourceIdentity = await directoryIdentity(this.options.sourceDirectory);
406
+ const destinationIdentity = await directoryIdentity(this.options.destinationDirectory);
407
+
408
+ if (journal?.phase === 'completed') {
409
+ if (!destinationIdentity) {
410
+ throw new Error(
411
+ `The migrated controller database is missing at ${this.options.destinationDirectory}. `
412
+ + `The retained legacy backup at ${this.options.sourceDirectory} was not restored automatically.`,
413
+ );
414
+ }
415
+ if (!identitiesEqual(destinationIdentity, {
416
+ device: journal.destinationDevice,
417
+ inode: journal.destinationInode,
418
+ })) {
419
+ throw new Error('The migrated controller database directory identity has changed.');
420
+ }
421
+ return undefined;
422
+ }
423
+
424
+ if (!journal) {
425
+ if (!sourceIdentity) return undefined;
426
+ if (destinationIdentity) {
427
+ throw new Error(
428
+ `Both the legacy and current controller database directories exist: `
429
+ + `${this.options.sourceDirectory} and ${this.options.destinationDirectory}. `
430
+ + 'Refusing to choose between them without a migration journal.',
431
+ );
432
+ }
433
+ journal = {
434
+ version: migrationVersion,
435
+ phase: 'prepared',
436
+ sourceDirectory: this.options.sourceDirectory,
437
+ destinationDirectory: this.options.destinationDirectory,
438
+ ...sourceIdentity,
439
+ };
440
+ await writeJournal(this.journalPath, journal);
441
+ journal = await readJournal(this.journalPath);
442
+ if (!journal) throw new Error('The embedded database migration journal was not persisted.');
443
+ assertJournalMatches(journal, this.options);
444
+ }
445
+
446
+ if (journal.phase !== 'prepared') {
447
+ throw new Error('The embedded database migration journal is not resumable.');
448
+ }
449
+ if (!sourceIdentity || !identitiesEqual(sourceIdentity, journal)) {
450
+ throw new Error('The legacy controller database directory identity has changed.');
451
+ }
452
+ if (await isSocketListening(this.options.sourceSocketPath)) {
453
+ throw new Error(
454
+ `The legacy controller database is still in use at ${this.options.sourceDirectory}. `
455
+ + 'Stop the old controller before starting this version so its data can be migrated safely.',
456
+ );
457
+ }
458
+ await assertCurrentStorageFormat(this.options.sourceDirectory);
459
+
460
+ try {
461
+ this.sourceDb = new plugins.smartdb.LocalSmartDb({
462
+ folderPath: this.options.sourceDirectory,
463
+ });
464
+ await this.sourceDb.start();
465
+ const sourceServer = this.sourceDb.getServer();
466
+ const sourceDatabaseNames = databaseNamesFromCollections(
467
+ await sourceServer.getCollections(),
468
+ );
469
+ const sourceDigests = new Map<string, plugins.smartdb.ISmartDbDatabaseContentDigest>();
470
+
471
+ this.importDestinationDb = new plugins.smartdb.LocalSmartDb({
472
+ folderPath: this.options.destinationDirectory,
473
+ });
474
+ await this.importDestinationDb.start();
475
+ const destinationServer = this.importDestinationDb.getServer();
476
+ for (const databaseName of sourceDatabaseNames) {
477
+ const exported = await sourceServer.exportDatabase({ databaseName });
478
+ const sourceDigest = await sourceServer.getDatabaseContentDigest({ databaseName });
479
+ const importResult = await destinationServer.importDatabase({
480
+ databaseName,
481
+ source: exported,
482
+ });
483
+ const destinationDigest = await destinationServer.getDatabaseContentDigest({ databaseName });
484
+ assertDigestMatch(sourceDigest, destinationDigest);
485
+ const exportedDocuments = exported.collections.reduce(
486
+ (sum, collection) => sum + collection.documents.length,
487
+ 0,
488
+ );
489
+ const exportedIndexes = exported.collections.reduce(
490
+ (sum, collection) => sum + collection.indexes.length,
491
+ 0,
492
+ );
493
+ if (
494
+ importResult.collections !== exported.collections.length
495
+ || importResult.documents !== exportedDocuments
496
+ || sourceDigest.collections !== exported.collections.length
497
+ || sourceDigest.documents !== exportedDocuments
498
+ || sourceDigest.indexes !== exportedIndexes
499
+ ) {
500
+ throw new Error(`The migrated database '${databaseName}' counts do not match its export.`);
501
+ }
502
+ sourceDigests.set(databaseName, sourceDigest);
503
+ }
504
+ assertDatabaseNamesMatch(
505
+ sourceDatabaseNames,
506
+ databaseNamesFromCollections(await destinationServer.getCollections()),
507
+ );
508
+
509
+ await this.importDestinationDb.stop();
510
+ this.importDestinationDb = undefined;
511
+ this.operationalDestinationDb = new plugins.smartdb.LocalSmartDb({
512
+ folderPath: this.options.destinationDirectory,
513
+ socketPath: this.options.destinationSocketPath,
514
+ });
515
+ const operationalConnection = await this.operationalDestinationDb.start();
516
+ const operationalServer = this.operationalDestinationDb.getServer();
517
+ assertDatabaseNamesMatch(
518
+ sourceDatabaseNames,
519
+ databaseNamesFromCollections(await operationalServer.getCollections()),
520
+ );
521
+ for (const [databaseName, sourceDigest] of sourceDigests) {
522
+ assertDigestMatch(
523
+ sourceDigest,
524
+ await operationalServer.getDatabaseContentDigest({ databaseName }),
525
+ );
526
+ }
527
+ const completedDestinationIdentity = await directoryIdentity(this.options.destinationDirectory);
528
+ if (!completedDestinationIdentity) {
529
+ throw new Error('The migrated controller database directory disappeared before completion.');
530
+ }
531
+ const completed: ICompletedMigrationJournal = {
532
+ ...journal,
533
+ phase: 'completed',
534
+ destinationDevice: completedDestinationIdentity.device,
535
+ destinationInode: completedDestinationIdentity.inode,
536
+ contentDigestSha256: aggregateContentDigests(sourceDigests),
537
+ };
538
+ await writeJournal(this.journalPath, completed);
539
+ await this.sourceDb.stop();
540
+ this.sourceDb = undefined;
541
+ const result = {
542
+ localDb: this.operationalDestinationDb,
543
+ connectionUri: operationalConnection.connectionUri,
544
+ };
545
+ this.operationalDestinationDb = undefined;
546
+ return result;
547
+ } catch (errorArg) {
548
+ try {
549
+ await this.stop();
550
+ } catch (cleanupErrorArg) {
551
+ throw new AggregateError(
552
+ [errorArg, cleanupErrorArg],
553
+ 'Embedded database migration failed and cleanup was incomplete.',
554
+ { cause: errorArg },
555
+ );
556
+ }
557
+ throw errorArg;
558
+ }
559
+ }
560
+ }
@@ -0,0 +1,121 @@
1
+ import * as plugins from '../ts/plugins.js';
2
+ import {
3
+ assertControllerProjectDocument,
4
+ ControllerProjectModel,
5
+ } from '../ts/classes.authmodels.js';
6
+ import {
7
+ CanonicalDirectoryError,
8
+ resolveCanonicalDirectory,
9
+ } from '../ts/functions.canonicaldirectory.js';
10
+ import { AuthError } from '../ts/interfaces.auth.js';
11
+ import type { IControllerProjectDocument } from '../ts/interfaces.projects.js';
12
+
13
+ const migrationPageLimit = 128;
14
+
15
+ const persistedBodyFromRawDocument = (
16
+ valueArg: Record<string, unknown>,
17
+ ): Record<string, unknown> => {
18
+ const body = { ...valueArg };
19
+ delete body._id;
20
+ delete body._smartdataRevision;
21
+ return body;
22
+ };
23
+
24
+ const hasIdentityState = (valueArg: Record<string, unknown>): boolean => Object.hasOwn(
25
+ valueArg,
26
+ 'directoryIdentityState',
27
+ );
28
+
29
+ export class ProtocolV22ProjectFilesystemIdentityMigration {
30
+ constructor(
31
+ private readonly database: plugins.smartdata.SmartdataDb | undefined,
32
+ private readonly controllerId: string,
33
+ ) {}
34
+
35
+ public async run(): Promise<void> {
36
+ if (!this.database) {
37
+ throw new AuthError('not_initialized', 'The authentication store database is unavailable.');
38
+ }
39
+ const collection = ControllerProjectModel.collection.mongoDbCollection;
40
+ let lastId: plugins.smartdata.TStoredDocument<IControllerProjectDocument>['_id'] | undefined;
41
+ while (true) {
42
+ const cursor = collection.find({
43
+ controllerId: this.controllerId,
44
+ ...(lastId ? { _id: { $gt: lastId } } : {}),
45
+ }).sort({ _id: 1 }).limit(migrationPageLimit);
46
+ let documents: Awaited<ReturnType<typeof cursor.toArray>>;
47
+ try {
48
+ documents = await cursor.toArray();
49
+ } finally {
50
+ await cursor.close();
51
+ }
52
+ for (const raw of documents) {
53
+ const body = persistedBodyFromRawDocument(raw);
54
+ if (body.directoryIdentityState === 'unbound') continue;
55
+ if (
56
+ body.directoryIdentityState === 'bound'
57
+ && Object.hasOwn(body, 'directoryIdentity')
58
+ ) {
59
+ try {
60
+ assertControllerProjectDocument(body);
61
+ continue;
62
+ } catch {
63
+ // Pre-release leaf-only or incomplete bindings are terminally unbound below.
64
+ }
65
+ }
66
+ const trulyLegacy = !hasIdentityState(body);
67
+ let identity: Awaited<ReturnType<typeof resolveCanonicalDirectory>> | undefined;
68
+ if (trulyLegacy) {
69
+ try {
70
+ identity = await resolveCanonicalDirectory(String(body.directory));
71
+ } catch (errorArg) {
72
+ if (!(errorArg instanceof CanonicalDirectoryError)) throw errorArg;
73
+ identity = undefined;
74
+ }
75
+ }
76
+ const migratedBody = { ...body };
77
+ delete migratedBody.directoryDevice;
78
+ delete migratedBody.directoryInode;
79
+ delete migratedBody.directoryIdentity;
80
+ const migrated = {
81
+ ...migratedBody,
82
+ directoryIdentityState: identity ? 'bound' as const : 'unbound' as const,
83
+ ...(identity ? { directoryIdentity: { ancestry: identity.ancestry } } : {}),
84
+ };
85
+ assertControllerProjectDocument(migrated);
86
+ const selector = raw._smartdataRevision === undefined
87
+ ? { _id: raw._id, _smartdataRevision: { $exists: false } }
88
+ : { _id: raw._id, _smartdataRevision: raw._smartdataRevision };
89
+ const replaced = await collection.findOneAndReplace(
90
+ selector,
91
+ { _id: raw._id, ...migrated, _smartdataRevision: plugins.crypto.randomUUID() },
92
+ { returnDocument: 'after', includeResultMetadata: false, upsert: false },
93
+ );
94
+ if (!replaced) {
95
+ const concurrent = await collection.findOne({ _id: raw._id });
96
+ const concurrentBody = concurrent
97
+ ? persistedBodyFromRawDocument(concurrent)
98
+ : undefined;
99
+ let concurrentlyMigrated = false;
100
+ if (concurrentBody && hasIdentityState(concurrentBody)) {
101
+ try {
102
+ assertControllerProjectDocument(concurrentBody);
103
+ concurrentlyMigrated = true;
104
+ } catch {
105
+ concurrentlyMigrated = false;
106
+ }
107
+ }
108
+ if (!concurrentlyMigrated) {
109
+ throw new AuthError(
110
+ 'concurrent_change',
111
+ 'The project filesystem identity migration changed concurrently.',
112
+ );
113
+ }
114
+ }
115
+ }
116
+ const last = documents.at(-1);
117
+ if (!last || documents.length < migrationPageLimit) break;
118
+ lastId = last._id;
119
+ }
120
+ }
121
+ }