@harperfast/harper 5.2.0-beta.4 → 5.2.1

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 (320) hide show
  1. package/bin/backup.ts +169 -0
  2. package/bin/cliOperations.ts +92 -73
  3. package/bin/copyDb.ts +21 -4
  4. package/bin/harper.ts +43 -55
  5. package/bin/help.ts +216 -0
  6. package/components/Application.ts +236 -46
  7. package/components/ApplicationScope.ts +26 -0
  8. package/components/EntryHandler.ts +410 -105
  9. package/components/RuntimeModuleTracker.ts +189 -0
  10. package/components/Scope.ts +120 -30
  11. package/components/componentLoader.ts +177 -25
  12. package/components/deployLifecycle.ts +119 -33
  13. package/components/mcp/toolRegistry.ts +10 -0
  14. package/components/mcp/tools/application.ts +12 -5
  15. package/components/mcp/tools/operations.ts +9 -0
  16. package/components/mcp/tools/schemas/operationDescriptions.ts +3 -0
  17. package/components/mcp/tools/schemas/operations.ts +9 -0
  18. package/components/operations.js +5 -6
  19. package/components/operationsValidation.js +32 -2
  20. package/components/scopeMount.ts +150 -0
  21. package/config/configUtils.ts +12 -9
  22. package/config-root.schema.json +14 -0
  23. package/dataLayer/backupManifest.ts +102 -0
  24. package/dataLayer/blobBackup.ts +286 -0
  25. package/dataLayer/harperBridge/ResourceBridge.ts +69 -5
  26. package/dataLayer/hdbInfoController.ts +8 -0
  27. package/dataLayer/restoreMarker.ts +276 -0
  28. package/dataLayer/rocksdbBackup.ts +1100 -0
  29. package/dataLayer/schemaDescribe.ts +2 -1
  30. package/dist/bin/backup.d.ts +9 -0
  31. package/dist/bin/backup.js +192 -0
  32. package/dist/bin/backup.js.map +1 -0
  33. package/dist/bin/cliOperations.d.ts +13 -0
  34. package/dist/bin/cliOperations.js +89 -70
  35. package/dist/bin/cliOperations.js.map +1 -1
  36. package/dist/bin/copyDb.js +13 -2
  37. package/dist/bin/copyDb.js.map +1 -1
  38. package/dist/bin/harper.d.ts +13 -0
  39. package/dist/bin/harper.js +45 -56
  40. package/dist/bin/harper.js.map +1 -1
  41. package/dist/bin/help.d.ts +8 -0
  42. package/dist/bin/help.js +192 -0
  43. package/dist/bin/help.js.map +1 -0
  44. package/dist/components/Application.d.ts +16 -1
  45. package/dist/components/Application.js +210 -38
  46. package/dist/components/Application.js.map +1 -1
  47. package/dist/components/ApplicationScope.d.ts +7 -0
  48. package/dist/components/ApplicationScope.js +22 -0
  49. package/dist/components/ApplicationScope.js.map +1 -1
  50. package/dist/components/EntryHandler.d.ts +4 -4
  51. package/dist/components/EntryHandler.js +386 -95
  52. package/dist/components/EntryHandler.js.map +1 -1
  53. package/dist/components/RuntimeModuleTracker.d.ts +11 -0
  54. package/dist/components/RuntimeModuleTracker.js +189 -0
  55. package/dist/components/RuntimeModuleTracker.js.map +1 -0
  56. package/dist/components/Scope.d.ts +38 -1
  57. package/dist/components/Scope.js +117 -28
  58. package/dist/components/Scope.js.map +1 -1
  59. package/dist/components/componentLoader.d.ts +2 -6
  60. package/dist/components/componentLoader.js +160 -18
  61. package/dist/components/componentLoader.js.map +1 -1
  62. package/dist/components/deployLifecycle.d.ts +6 -2
  63. package/dist/components/deployLifecycle.js +109 -31
  64. package/dist/components/deployLifecycle.js.map +1 -1
  65. package/dist/components/mcp/toolRegistry.d.ts +7 -0
  66. package/dist/components/mcp/toolRegistry.js +10 -0
  67. package/dist/components/mcp/toolRegistry.js.map +1 -1
  68. package/dist/components/mcp/tools/application.js +11 -5
  69. package/dist/components/mcp/tools/application.js.map +1 -1
  70. package/dist/components/mcp/tools/operations.js +9 -0
  71. package/dist/components/mcp/tools/operations.js.map +1 -1
  72. package/dist/components/mcp/tools/schemas/operationDescriptions.js +2 -0
  73. package/dist/components/mcp/tools/schemas/operationDescriptions.js.map +1 -1
  74. package/dist/components/mcp/tools/schemas/operations.js +9 -0
  75. package/dist/components/mcp/tools/schemas/operations.js.map +1 -1
  76. package/dist/components/operations.js +6 -6
  77. package/dist/components/operations.js.map +1 -1
  78. package/dist/components/operationsValidation.js +34 -2
  79. package/dist/components/operationsValidation.js.map +1 -1
  80. package/dist/components/scopeMount.d.ts +86 -0
  81. package/dist/components/scopeMount.js +131 -0
  82. package/dist/components/scopeMount.js.map +1 -0
  83. package/dist/config/configUtils.js +13 -9
  84. package/dist/config/configUtils.js.map +1 -1
  85. package/dist/dataLayer/backupManifest.d.ts +26 -0
  86. package/dist/dataLayer/backupManifest.js +97 -0
  87. package/dist/dataLayer/backupManifest.js.map +1 -0
  88. package/dist/dataLayer/blobBackup.d.ts +87 -0
  89. package/dist/dataLayer/blobBackup.js +282 -0
  90. package/dist/dataLayer/blobBackup.js.map +1 -0
  91. package/dist/dataLayer/harperBridge/ResourceBridge.d.ts +3 -0
  92. package/dist/dataLayer/harperBridge/ResourceBridge.js +45 -5
  93. package/dist/dataLayer/harperBridge/ResourceBridge.js.map +1 -1
  94. package/dist/dataLayer/hdbInfoController.js +4 -0
  95. package/dist/dataLayer/hdbInfoController.js.map +1 -1
  96. package/dist/dataLayer/restoreMarker.d.ts +122 -0
  97. package/dist/dataLayer/restoreMarker.js +261 -0
  98. package/dist/dataLayer/restoreMarker.js.map +1 -0
  99. package/dist/dataLayer/rocksdbBackup.d.ts +127 -0
  100. package/dist/dataLayer/rocksdbBackup.js +1039 -0
  101. package/dist/dataLayer/rocksdbBackup.js.map +1 -0
  102. package/dist/dataLayer/schemaDescribe.js +2 -1
  103. package/dist/dataLayer/schemaDescribe.js.map +1 -1
  104. package/dist/resources/DatabaseTransaction.d.ts +55 -0
  105. package/dist/resources/DatabaseTransaction.js +282 -109
  106. package/dist/resources/DatabaseTransaction.js.map +1 -1
  107. package/dist/resources/ResourceInterface.d.ts +8 -5
  108. package/dist/resources/ResourceInterface.js.map +1 -1
  109. package/dist/resources/Resources.js +22 -4
  110. package/dist/resources/Resources.js.map +1 -1
  111. package/dist/resources/Table.d.ts +6 -5
  112. package/dist/resources/Table.js +114 -37
  113. package/dist/resources/Table.js.map +1 -1
  114. package/dist/resources/analytics/write.js +6 -6
  115. package/dist/resources/analytics/write.js.map +1 -1
  116. package/dist/resources/blob.d.ts +8 -1
  117. package/dist/resources/blob.js +31 -14
  118. package/dist/resources/blob.js.map +1 -1
  119. package/dist/resources/databases.d.ts +59 -1
  120. package/dist/resources/databases.js +386 -41
  121. package/dist/resources/databases.js.map +1 -1
  122. package/dist/resources/jsResource.d.ts +4 -26
  123. package/dist/resources/jsResource.js +5 -59
  124. package/dist/resources/jsResource.js.map +1 -1
  125. package/dist/resources/models/Models.d.ts +11 -1
  126. package/dist/resources/models/Models.js +10 -1
  127. package/dist/resources/models/Models.js.map +1 -1
  128. package/dist/resources/models/backendRegistry.d.ts +9 -0
  129. package/dist/resources/models/backendRegistry.js +10 -0
  130. package/dist/resources/models/backendRegistry.js.map +1 -1
  131. package/dist/resources/models/openaiStream.d.ts +16 -1
  132. package/dist/resources/models/openaiStream.js +113 -21
  133. package/dist/resources/models/openaiStream.js.map +1 -1
  134. package/dist/resources/models/v1/chatCompletions.d.ts +54 -0
  135. package/dist/resources/models/v1/chatCompletions.js +115 -0
  136. package/dist/resources/models/v1/chatCompletions.js.map +1 -0
  137. package/dist/resources/models/v1/embeddings.d.ts +11 -0
  138. package/dist/resources/models/v1/embeddings.js +71 -0
  139. package/dist/resources/models/v1/embeddings.js.map +1 -0
  140. package/dist/resources/models/v1/errors.d.ts +54 -0
  141. package/dist/resources/models/v1/errors.js +130 -0
  142. package/dist/resources/models/v1/errors.js.map +1 -0
  143. package/dist/resources/models/v1/index.d.ts +36 -0
  144. package/dist/resources/models/v1/index.js +75 -0
  145. package/dist/resources/models/v1/index.js.map +1 -0
  146. package/dist/resources/models/v1/models.d.ts +26 -0
  147. package/dist/resources/models/v1/models.js +44 -0
  148. package/dist/resources/models/v1/models.js.map +1 -0
  149. package/dist/resources/models/v1/translation.d.ts +133 -0
  150. package/dist/resources/models/v1/translation.js +298 -0
  151. package/dist/resources/models/v1/translation.js.map +1 -0
  152. package/dist/resources/roles.d.ts +1 -1
  153. package/dist/resources/roles.js +54 -7
  154. package/dist/resources/roles.js.map +1 -1
  155. package/dist/resources/transaction.js +0 -3
  156. package/dist/resources/transaction.js.map +1 -1
  157. package/dist/security/jsLoader.js +84 -33
  158. package/dist/security/jsLoader.js.map +1 -1
  159. package/dist/security/role.js +4 -0
  160. package/dist/security/role.js.map +1 -1
  161. package/dist/security/superUserGuard.d.ts +7 -0
  162. package/dist/security/superUserGuard.js +23 -0
  163. package/dist/security/superUserGuard.js.map +1 -0
  164. package/dist/security/tokenAuthentication.d.ts +0 -1
  165. package/dist/security/tokenAuthentication.js +6 -2
  166. package/dist/security/tokenAuthentication.js.map +1 -1
  167. package/dist/security/user.d.ts +6 -1
  168. package/dist/security/user.js +23 -1
  169. package/dist/security/user.js.map +1 -1
  170. package/dist/server/REST.js +25 -9
  171. package/dist/server/REST.js.map +1 -1
  172. package/dist/server/fastifyRoutes.js +15 -1
  173. package/dist/server/fastifyRoutes.js.map +1 -1
  174. package/dist/server/http.d.ts +23 -2
  175. package/dist/server/http.js +99 -12
  176. package/dist/server/http.js.map +1 -1
  177. package/dist/server/itc/serverHandlers.js +7 -1
  178. package/dist/server/itc/serverHandlers.js.map +1 -1
  179. package/dist/server/jobs/jobProcess.js +20 -1
  180. package/dist/server/jobs/jobProcess.js.map +1 -1
  181. package/dist/server/jobs/jobRunner.js +10 -0
  182. package/dist/server/jobs/jobRunner.js.map +1 -1
  183. package/dist/server/jobs/jobs.js +11 -0
  184. package/dist/server/jobs/jobs.js.map +1 -1
  185. package/dist/server/middlewareChain.d.ts +10 -1
  186. package/dist/server/middlewareChain.js +81 -21
  187. package/dist/server/middlewareChain.js.map +1 -1
  188. package/dist/server/operationsServer.d.ts +0 -2
  189. package/dist/server/operationsServer.js.map +1 -1
  190. package/dist/server/serverHelpers/multipartParser.js +9 -0
  191. package/dist/server/serverHelpers/multipartParser.js.map +1 -1
  192. package/dist/server/serverHelpers/operationAuthorizationState.d.ts +2 -0
  193. package/dist/server/serverHelpers/operationAuthorizationState.js +13 -0
  194. package/dist/server/serverHelpers/operationAuthorizationState.js.map +1 -0
  195. package/dist/server/serverHelpers/registeredOperations.d.ts +3 -2
  196. package/dist/server/serverHelpers/registeredOperations.js +14 -13
  197. package/dist/server/serverHelpers/registeredOperations.js.map +1 -1
  198. package/dist/server/serverHelpers/serverHandlers.js +27 -7
  199. package/dist/server/serverHelpers/serverHandlers.js.map +1 -1
  200. package/dist/server/serverHelpers/serverUtilities.d.ts +1 -1
  201. package/dist/server/serverHelpers/serverUtilities.js +21 -7
  202. package/dist/server/serverHelpers/serverUtilities.js.map +1 -1
  203. package/dist/server/static.js +68 -21
  204. package/dist/server/static.js.map +1 -1
  205. package/dist/server/storageReclamation.d.ts +17 -0
  206. package/dist/server/storageReclamation.js +90 -6
  207. package/dist/server/storageReclamation.js.map +1 -1
  208. package/dist/server/threads/socketRouter.js +20 -0
  209. package/dist/server/threads/socketRouter.js.map +1 -1
  210. package/dist/server/threads/threadServer.js +12 -0
  211. package/dist/server/threads/threadServer.js.map +1 -1
  212. package/dist/sqlEngine/diff/differential.js +7 -3
  213. package/dist/sqlEngine/diff/differential.js.map +1 -1
  214. package/dist/sqlTranslator/index.js +6 -1
  215. package/dist/sqlTranslator/index.js.map +1 -1
  216. package/dist/upgrade/upgradePrompt.d.ts +2 -2
  217. package/dist/upgrade/upgradePrompt.js +22 -3
  218. package/dist/upgrade/upgradePrompt.js.map +1 -1
  219. package/dist/utility/OperationFunctionCaller.js +25 -6
  220. package/dist/utility/OperationFunctionCaller.js.map +1 -1
  221. package/dist/utility/errors/commonErrors.d.ts +1 -0
  222. package/dist/utility/errors/commonErrors.js +1 -0
  223. package/dist/utility/errors/commonErrors.js.map +1 -1
  224. package/dist/utility/hdbTerms.d.ts +13 -1
  225. package/dist/utility/hdbTerms.js +13 -1
  226. package/dist/utility/hdbTerms.js.map +1 -1
  227. package/dist/utility/install/installer.js +32 -0
  228. package/dist/utility/install/installer.js.map +1 -1
  229. package/dist/utility/logging/harper_logger.d.ts +47 -0
  230. package/dist/utility/logging/harper_logger.js +803 -38
  231. package/dist/utility/logging/harper_logger.js.map +1 -1
  232. package/dist/utility/operation_authorization.js +13 -3
  233. package/dist/utility/operation_authorization.js.map +1 -1
  234. package/npm-shrinkwrap.json +198 -193
  235. package/package.json +12 -7
  236. package/resources/DESIGN.md +2 -0
  237. package/resources/DatabaseTransaction.ts +310 -104
  238. package/resources/ResourceInterface.ts +8 -5
  239. package/resources/Resources.ts +22 -4
  240. package/resources/Table.ts +341 -263
  241. package/resources/analytics/write.ts +22 -20
  242. package/resources/blob.ts +31 -14
  243. package/resources/databases.ts +387 -46
  244. package/resources/jsResource.ts +5 -62
  245. package/resources/models/Models.ts +14 -1
  246. package/resources/models/backendRegistry.ts +10 -0
  247. package/resources/models/openaiStream.ts +131 -19
  248. package/resources/models/v1/chatCompletions.ts +128 -0
  249. package/resources/models/v1/embeddings.ts +70 -0
  250. package/resources/models/v1/errors.ts +141 -0
  251. package/resources/models/v1/index.ts +72 -0
  252. package/resources/models/v1/models.ts +53 -0
  253. package/resources/models/v1/translation.ts +362 -0
  254. package/resources/roles.ts +67 -7
  255. package/resources/transaction.ts +0 -3
  256. package/security/jsLoader.ts +84 -30
  257. package/security/role.ts +7 -0
  258. package/security/superUserGuard.ts +20 -0
  259. package/security/tokenAuthentication.ts +6 -3
  260. package/security/user.ts +26 -1
  261. package/server/DESIGN.md +61 -34
  262. package/server/REST.ts +25 -9
  263. package/server/fastifyRoutes.ts +20 -1
  264. package/server/http.ts +100 -13
  265. package/server/itc/serverHandlers.js +7 -1
  266. package/server/jobs/jobProcess.ts +18 -1
  267. package/server/jobs/jobRunner.ts +10 -0
  268. package/server/jobs/jobs.ts +11 -0
  269. package/server/middlewareChain.ts +79 -20
  270. package/server/operationsServer.ts +0 -2
  271. package/server/serverHelpers/multipartParser.ts +9 -0
  272. package/server/serverHelpers/operationAuthorizationState.ts +11 -0
  273. package/server/serverHelpers/registeredOperations.ts +19 -15
  274. package/server/serverHelpers/serverHandlers.js +28 -7
  275. package/server/serverHelpers/serverUtilities.ts +29 -7
  276. package/server/static.ts +87 -23
  277. package/server/storageReclamation.ts +104 -8
  278. package/server/threads/socketRouter.ts +20 -0
  279. package/server/threads/threadServer.js +11 -0
  280. package/sqlTranslator/index.ts +6 -1
  281. package/static/defaultConfig.yaml +3 -0
  282. package/studio/web/assets/{Chat-DK3GlWEb.js → Chat-aApwhRmz.js} +2 -2
  283. package/studio/web/assets/{Chat-DK3GlWEb.js.map → Chat-aApwhRmz.js.map} +1 -1
  284. package/studio/web/assets/{FloatingChat-fBcC1Ew_.js → FloatingChat-sC0H91n9.js} +4 -4
  285. package/studio/web/assets/{FloatingChat-fBcC1Ew_.js.map → FloatingChat-sC0H91n9.js.map} +1 -1
  286. package/studio/web/assets/{apiToken-DJo1nakA.js → apiToken-DZ8JhHOI.js} +2 -2
  287. package/studio/web/assets/{apiToken-DJo1nakA.js.map → apiToken-DZ8JhHOI.js.map} +1 -1
  288. package/studio/web/assets/{applications-BDfH8urd.js → applications-9SEVLIO5.js} +2 -2
  289. package/studio/web/assets/{applications-BDfH8urd.js.map → applications-9SEVLIO5.js.map} +1 -1
  290. package/studio/web/assets/{index-BHo3c2Gk.js → index-Dy3uDGXb.js} +6 -6
  291. package/studio/web/assets/index-Dy3uDGXb.js.map +1 -0
  292. package/studio/web/assets/{index.lazy-BTo0y6UM.js → index.lazy-CN1zq4I4.js} +4 -4
  293. package/studio/web/assets/{index.lazy-BTo0y6UM.js.map → index.lazy-CN1zq4I4.js.map} +1 -1
  294. package/studio/web/assets/{notifications-CMxvWNnz.js → notifications-BFAF07xr.js} +2 -2
  295. package/studio/web/assets/{notifications-CMxvWNnz.js.map → notifications-BFAF07xr.js.map} +1 -1
  296. package/studio/web/assets/{notifications-D3GoB26g.js → notifications-CKlYVvVN.js} +2 -2
  297. package/studio/web/assets/{notifications-D3GoB26g.js.map → notifications-CKlYVvVN.js.map} +1 -1
  298. package/studio/web/assets/{profile-Doj5FVDE.js → profile-A1zhEdFG.js} +2 -2
  299. package/studio/web/assets/{profile-Doj5FVDE.js.map → profile-A1zhEdFG.js.map} +1 -1
  300. package/studio/web/assets/{setComponentFile-yinsqJy0.js → setComponentFile-SEtBt_GV.js} +2 -2
  301. package/studio/web/assets/{setComponentFile-yinsqJy0.js.map → setComponentFile-SEtBt_GV.js.map} +1 -1
  302. package/studio/web/assets/{setup-DJwR0BHd.js → setup-BisINqdH.js} +2 -2
  303. package/studio/web/assets/{setup-DJwR0BHd.js.map → setup-BisINqdH.js.map} +1 -1
  304. package/studio/web/assets/{status-Br_AbsJs.js → status-BuQoCc7l.js} +2 -2
  305. package/studio/web/assets/{status-Br_AbsJs.js.map → status-BuQoCc7l.js.map} +1 -1
  306. package/studio/web/assets/{swagger-ui-react-02XH5sVf.js → swagger-ui-react-CAi_s1PC.js} +2 -2
  307. package/studio/web/assets/{swagger-ui-react-02XH5sVf.js.map → swagger-ui-react-CAi_s1PC.js.map} +1 -1
  308. package/studio/web/assets/{tsMode-D1DMKY7h.js → tsMode-DT74tlkM.js} +2 -2
  309. package/studio/web/assets/{tsMode-D1DMKY7h.js.map → tsMode-DT74tlkM.js.map} +1 -1
  310. package/studio/web/assets/{useEntityRestURL-DtRblRgw.js → useEntityRestURL-CGRGc1n7.js} +2 -2
  311. package/studio/web/assets/{useEntityRestURL-DtRblRgw.js.map → useEntityRestURL-CGRGc1n7.js.map} +1 -1
  312. package/studio/web/index.html +1 -1
  313. package/upgrade/upgradePrompt.ts +22 -3
  314. package/utility/OperationFunctionCaller.ts +24 -3
  315. package/utility/errors/commonErrors.ts +2 -0
  316. package/utility/hdbTerms.ts +13 -1
  317. package/utility/install/installer.ts +37 -0
  318. package/utility/logging/harper_logger.ts +786 -41
  319. package/utility/operation_authorization.ts +31 -3
  320. package/studio/web/assets/index-BHo3c2Gk.js.map +0 -1
@@ -0,0 +1,189 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { lstatSync, readFileSync, readlinkSync } from 'node:fs';
3
+ import { lstat, readFile, readlink } from 'node:fs/promises';
4
+ import { createRequire, Module } from 'node:module';
5
+ import { dirname, isAbsolute, relative, resolve, sep } from 'node:path';
6
+ import { fileURLToPath, pathToFileURL } from 'node:url';
7
+
8
+ type Source = string | Buffer;
9
+
10
+ const MODULE_COMPARE_CONCURRENCY = 16;
11
+ const RESOLUTION_EXTENSIONS = ['', '.js', '.json', '.node', '.ts', '.tsx', '.cjs', '.mjs'];
12
+
13
+ type Resolution = {
14
+ resolvedUrl: string;
15
+ higherPriorityCandidates?: Map<string, string>;
16
+ };
17
+
18
+ export class RuntimeModuleTracker {
19
+ #getRoot: () => string | undefined;
20
+ #modules = new Map<string, string>();
21
+ #resolutions = new Map<string, Resolution>();
22
+ #nativeRuntime = false;
23
+ #deployInFlight = false;
24
+ #loadedDuringDeploy = false;
25
+ #comparison?: Promise<boolean>;
26
+
27
+ constructor(getRoot: () => string | undefined) {
28
+ this.#getRoot = getRoot;
29
+ }
30
+
31
+ beginDeploy(): void {
32
+ if (this.#deployInFlight) return;
33
+ this.#deployInFlight = true;
34
+ this.#loadedDuringDeploy = false;
35
+ this.#comparison = undefined;
36
+ }
37
+
38
+ markNativeRuntime(): void {
39
+ this.#nativeRuntime = true;
40
+ if (this.#deployInFlight) this.#loadedDuringDeploy = true;
41
+ }
42
+
43
+ recordModule(moduleUrl: string, source: Source): void {
44
+ const modulePath = this.#localPath(moduleUrl);
45
+ if (!modulePath) return;
46
+ if (!this.#modules.has(modulePath)) this.#modules.set(modulePath, digest(source));
47
+ if (this.#deployInFlight) this.#loadedDuringDeploy = true;
48
+ }
49
+
50
+ recordResolution(specifier: string, referrer: string, resolvedUrl: string): void {
51
+ const referrerPath = this.#localPath(referrer);
52
+ const resolvedPath = this.#localPath(resolvedUrl);
53
+ if (!referrerPath || !resolvedPath) return;
54
+ const key = `${referrerPath}\0${specifier}`;
55
+ if (!this.#resolutions.has(key)) {
56
+ const candidates = higherPriorityResolutionCandidates(specifier, referrerPath, resolvedPath);
57
+ this.#resolutions.set(key, {
58
+ resolvedUrl,
59
+ higherPriorityCandidates: candidates
60
+ ? new Map(candidates.map((candidate) => [candidate, candidateState(candidate)]))
61
+ : undefined,
62
+ });
63
+ if (this.#deployInFlight) this.#loadedDuringDeploy = true;
64
+ }
65
+ }
66
+
67
+ finishDeploy(): Promise<boolean> {
68
+ if (!this.#deployInFlight) return this.#comparison ?? Promise.resolve(false);
69
+ this.#deployInFlight = false;
70
+ this.#comparison = this.#compare();
71
+ return this.#comparison;
72
+ }
73
+
74
+ async #compare(): Promise<boolean> {
75
+ if (this.#nativeRuntime || this.#loadedDuringDeploy) return true;
76
+ const modules = [...this.#modules];
77
+ for (let start = 0; start < modules.length; start += MODULE_COMPARE_CONCURRENCY) {
78
+ const changed = await Promise.all(
79
+ modules.slice(start, start + MODULE_COMPARE_CONCURRENCY).map(async ([modulePath, previousDigest]) => {
80
+ try {
81
+ return digest(await readFile(modulePath)) !== previousDigest;
82
+ } catch {
83
+ return true;
84
+ }
85
+ })
86
+ );
87
+ if (changed.includes(true)) return true;
88
+ }
89
+ for (const [key, resolution] of this.#resolutions) {
90
+ const separator = key.indexOf('\0');
91
+ const referrerPath = key.slice(0, separator);
92
+ const specifier = key.slice(separator + 1);
93
+ if (
94
+ resolution.higherPriorityCandidates &&
95
+ (
96
+ await Promise.all(
97
+ [...resolution.higherPriorityCandidates].map(
98
+ async ([candidate, previousState]) => (await candidateStateAsync(candidate)) !== previousState
99
+ )
100
+ )
101
+ ).includes(true)
102
+ )
103
+ return true;
104
+ try {
105
+ // Node does not invalidate this cache when a component tree is replaced in place.
106
+ if (!invalidateResolutionCache(specifier, referrerPath, resolution.resolvedUrl)) return true;
107
+ const resolved = createRequire(pathToFileURL(referrerPath)).resolve(specifier);
108
+ const resolvedUrl = isAbsolute(resolved) ? pathToFileURL(resolved).toString() : resolved;
109
+ if (resolvedUrl !== resolution.resolvedUrl) return true;
110
+ } catch {
111
+ return true;
112
+ }
113
+ }
114
+ return false;
115
+ }
116
+
117
+ #localPath(moduleUrl: string): string | undefined {
118
+ if (!moduleUrl.startsWith('file:')) return;
119
+ const root = this.#getRoot();
120
+ if (!root) return;
121
+ const modulePath = fileURLToPath(moduleUrl);
122
+ const relativePath = relative(resolve(root), modulePath);
123
+ if (
124
+ relativePath === '' ||
125
+ (!relativePath.startsWith(`..${sep}`) && relativePath !== '..' && !isAbsolute(relativePath))
126
+ )
127
+ return modulePath;
128
+ }
129
+ }
130
+
131
+ function higherPriorityResolutionCandidates(
132
+ specifier: string,
133
+ referrerPath: string,
134
+ resolvedPath: string
135
+ ): string[] | undefined {
136
+ if (!specifier.startsWith('.')) return;
137
+ const basePath = resolve(dirname(referrerPath), specifier);
138
+ const candidates = [...new Set(RESOLUTION_EXTENSIONS.map((extension) => basePath + extension))];
139
+ candidates.push(resolve(basePath, 'package.json'));
140
+ for (const extension of RESOLUTION_EXTENSIONS.slice(1)) candidates.push(resolve(basePath, `index${extension}`));
141
+ const resolvedIndex = candidates.indexOf(resolvedPath);
142
+ if (resolvedIndex === -1) return;
143
+ return candidates.slice(0, resolvedIndex);
144
+ }
145
+
146
+ function candidateState(path: string): string {
147
+ try {
148
+ const stats = lstatSync(path);
149
+ if (stats.isSymbolicLink()) return `link:${readlinkSync(path)}`;
150
+ if (stats.isDirectory()) return 'directory';
151
+ if (!stats.isFile()) return 'other';
152
+ return path.endsWith('package.json') ? `file:${digest(readFileSync(path))}` : 'file';
153
+ } catch (error) {
154
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') return 'missing';
155
+ return `error:${(error as NodeJS.ErrnoException).code ?? 'unknown'}`;
156
+ }
157
+ }
158
+
159
+ async function candidateStateAsync(path: string): Promise<string> {
160
+ try {
161
+ const stats = await lstat(path);
162
+ if (stats.isSymbolicLink()) return `link:${await readlink(path)}`;
163
+ if (stats.isDirectory()) return 'directory';
164
+ if (!stats.isFile()) return 'other';
165
+ return path.endsWith('package.json') ? `file:${digest(await readFile(path))}` : 'file';
166
+ } catch (error) {
167
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') return 'missing';
168
+ return `error:${(error as NodeJS.ErrnoException).code ?? 'unknown'}`;
169
+ }
170
+ }
171
+
172
+ function invalidateResolutionCache(specifier: string, referrerPath: string, previousResolvedUrl: string): boolean {
173
+ const pathCache = (Module as unknown as { _pathCache?: Record<string, string> })._pathCache;
174
+ if (!pathCache) return false;
175
+ const previousPath = previousResolvedUrl.startsWith('file:')
176
+ ? fileURLToPath(previousResolvedUrl)
177
+ : previousResolvedUrl;
178
+ const relativeKey = `${specifier}\0${dirname(referrerPath)}`;
179
+ if (pathCache[relativeKey] === previousPath) delete pathCache[relativeKey];
180
+ if (specifier.startsWith('.')) return true;
181
+ for (const cacheKey of Object.keys(pathCache)) {
182
+ if (cacheKey.startsWith(`${specifier}\0`) && pathCache[cacheKey] === previousPath) delete pathCache[cacheKey];
183
+ }
184
+ return true;
185
+ }
186
+
187
+ function digest(source: Source): string {
188
+ return createHash('sha256').update(source).digest('base64');
189
+ }
@@ -11,6 +11,7 @@ import type { FileAndURLPathConfig } from './Component.ts';
11
11
  import { FilesOption } from './deriveGlobOptions.ts';
12
12
  import { requestRestart } from './requestRestart.ts';
13
13
  import { resolveBaseURLPath } from './resolveBaseURLPath.ts';
14
+ import { composeMountedUrlPath, type ScopeMount } from './scopeMount.ts';
14
15
  import { ApplicationScope } from './ApplicationScope.ts';
15
16
  import {
16
17
  getSecretsForComponent,
@@ -37,8 +38,10 @@ export type ScopeEventsMap = {
37
38
  // file-driven work to avoid acting on intermediate states.
38
39
  'deploy:start': [componentName: string];
39
40
  // Fired after deploy I/O completes (success or failure). The scope's
40
- // EntryHandlers have been recreated by this point; subsequent `add`/`change`
41
- // events reflect the post-deploy tree.
41
+ // EntryHandlers have been resumed by this point; their replacement watcher
42
+ // generation compares the post-deploy scan with the retained pre-deploy
43
+ // snapshot, so subsequent events are the logical differences of that tree,
44
+ // not a replay of every surviving file as an `add`.
42
45
  'deploy:end': [componentName: string];
43
46
  [record: string]: [...args: unknown[]];
44
47
  };
@@ -63,11 +66,8 @@ export class Scope extends EventEmitter<ScopeEventsMap> {
63
66
  #pendingInitialLoads: Set<Promise<void>>;
64
67
  #deployStartHandler: (name: string) => void;
65
68
  #deployEndHandler: (name: string) => void;
66
- // While a deploy of this component is in flight, EntryHandler events do not
67
- // drive requestRestart() — the deploy itself produces hundreds of file
68
- // changes that would otherwise pile up. A single coalesced restart is
69
- // triggered by the post-deploy re-scan instead.
70
69
  #deployInFlight: boolean = false;
70
+ #restartRequestedDuringDeploy: boolean = false;
71
71
  applicationScope?: ApplicationScope;
72
72
 
73
73
  options: OptionsWatcher;
@@ -81,6 +81,18 @@ export class Scope extends EventEmitter<ScopeEventsMap> {
81
81
  // process-global side effects should validate fully but skip activation.
82
82
  isTransientValidation?: boolean;
83
83
 
84
+ /**
85
+ * Routing the operator declared for this application in the root config. Applied automatically
86
+ * to handlers registered through `scope.server`, so plugins normally don't touch it — the
87
+ * router strips the mount before a handler runs and everything inside the application
88
+ * addresses itself mount-relative.
89
+ *
90
+ * Read it only when a plugin emits an absolute URL back to the client (e.g. a redirect
91
+ * `Location`, via `externalBasePath()`) or bypasses the routed chain entirely (legacy
92
+ * fastify routes register on the bare server).
93
+ */
94
+ mount?: ScopeMount;
95
+
84
96
  constructor(
85
97
  appName: string,
86
98
  pluginName: string,
@@ -88,16 +100,19 @@ export class Scope extends EventEmitter<ScopeEventsMap> {
88
100
  configFilePath: string,
89
101
  applicationScope: ApplicationScope,
90
102
  origin: string = appName,
91
- isRootConfig?: boolean
103
+ isRootConfig?: boolean,
104
+ mount?: ScopeMount
92
105
  ) {
93
106
  super();
94
107
 
108
+ this.mount = mount;
95
109
  this.#appName = appName;
96
110
  this.#pluginName = pluginName;
97
111
  this.#origin = typeof origin === 'string' ? origin : appName;
98
112
  this.#directory = directory;
99
113
  this.#configFilePath = configFilePath;
100
114
  this.#logger = loggerWithTag(this.#appName);
115
+ this.#deployInFlight = deployLifecycle.isDeployInFlight(this.#appName);
101
116
 
102
117
  this.databaseEvents = databaseEventsEmitter;
103
118
  this.applicationScope = applicationScope;
@@ -117,14 +132,10 @@ export class Scope extends EventEmitter<ScopeEventsMap> {
117
132
  const method = Reflect.get(target, prop, receiver);
118
133
  if (typeof method === 'function') {
119
134
  return (listener: any, options?: any) => {
120
- const scopeConfig = (scopeRef.options?.getAll() as any) ?? {};
121
135
  return method.call(target, listener, {
122
136
  name: pluginName,
123
- // resolve to the same base the entry pipeline uses ('assets' -> '/assets/',
124
- // './x' -> '/<name>/x/') so route matching sees a real pathname prefix (#1583)
125
- urlPath: scopeConfig.urlPath ? resolveBaseURLPath(pluginName, scopeConfig.urlPath) : undefined,
126
- host: scopeConfig.host || undefined,
127
137
  ...options,
138
+ ...scopeRef.routeFor(options),
128
139
  });
129
140
  };
130
141
  }
@@ -145,6 +156,7 @@ export class Scope extends EventEmitter<ScopeEventsMap> {
145
156
  this.options = new OptionsWatcher(pluginName, configFilePath, this.#logger, isRootConfig)
146
157
  .on('error', this.#handleError.bind(this))
147
158
  .on('change', this.#optionsWatcherChangeListener.bind(this)())
159
+ .on('remove', this.#optionsWatcherRemoveListener())
148
160
  .on('ready', this.#handleOptionsWatcherReady.bind(this));
149
161
 
150
162
  // Bridge cross-thread deploy lifecycle events for this component. The
@@ -184,6 +196,41 @@ export class Scope extends EventEmitter<ScopeEventsMap> {
184
196
  return this.#pluginName;
185
197
  }
186
198
 
199
+ /**
200
+ * Turns a mount-relative base path into the absolute path a client sees, by prefixing the
201
+ * application's mount. Use it for anything sent back to the client — a redirect `Location`, a
202
+ * generated link — since the router strips the mount before a handler runs and a handler's own
203
+ * view of the path therefore excludes it.
204
+ */
205
+ externalBasePath(baseURLPath: string): string {
206
+ return this.mount?.urlPath ? `${this.mount.urlPath}${baseURLPath}` : baseURLPath;
207
+ }
208
+
209
+ /**
210
+ * The route a handler registered through `scope.server` with these options will answer on — the
211
+ * single place the application mount is applied. `scope.server` uses it, and a plugin that must
212
+ * identify its own route (e.g. REST deduplicating registration per mount) calls it rather than
213
+ * recomposing the parts, so there is one definition of "which route is this".
214
+ *
215
+ * An explicit call option wins over config, but either way `urlPath` is *resolved* rather than
216
+ * passed through: plugins that spread their whole config section into these options (REST does)
217
+ * would otherwise hand the router a raw value — './' became the literal, unmatchable route '/.'.
218
+ */
219
+ routeFor(options?: { urlPath?: string; host?: string }): { host?: string; urlPath?: string } {
220
+ const scopeConfig = (this.options?.getAll() as any) ?? {};
221
+ const rawUrlPath = options?.urlPath ?? scopeConfig.urlPath;
222
+ // resolve to the same base the entry pipeline uses ('assets' -> '/assets/', './x' ->
223
+ // '/<name>/x/') so route matching sees a real pathname prefix (#1583), then prefix the
224
+ // application's mount. The mount is applied ONLY here, at the routing boundary: the router
225
+ // strips it before the handler runs, so entry URL paths — and the resource paths
226
+ // graphqlSchema/jsResource derive from them — stay mount-relative.
227
+ const pluginUrlPath = rawUrlPath ? resolveBaseURLPath(this.#pluginName, rawUrlPath) : undefined;
228
+ return {
229
+ host: this.mount?.host || options?.host || scopeConfig.host || undefined,
230
+ urlPath: composeMountedUrlPath(this.mount?.urlPath, this.#pluginName, pluginUrlPath) || undefined,
231
+ };
232
+ }
233
+
187
234
  get directory(): string {
188
235
  return this.#directory;
189
236
  }
@@ -208,7 +255,8 @@ export class Scope extends EventEmitter<ScopeEventsMap> {
208
255
  }
209
256
 
210
257
  #handleError(error: unknown): void {
211
- this.emit('error', error);
258
+ if (this.listenerCount('error') > 0) this.emit('error', error);
259
+ else this.#logger.error?.('Error in component scope:', error);
212
260
  }
213
261
 
214
262
  async close(): Promise<this> {
@@ -255,6 +303,8 @@ export class Scope extends EventEmitter<ScopeEventsMap> {
255
303
 
256
304
  #onDeployStart(componentName: string): void {
257
305
  this.#deployInFlight = true;
306
+ this.#restartRequestedDuringDeploy = false;
307
+ this.applicationScope?.beginDeploy();
258
308
  // Pause each EntryHandler so it stops emitting events for the
259
309
  // intermediate filesystem state the deploy is writing, and so it
260
310
  // releases its inotify handles while npm install is unpacking
@@ -269,25 +319,27 @@ export class Scope extends EventEmitter<ScopeEventsMap> {
269
319
 
270
320
  #onDeployEnd(componentName: string): void {
271
321
  this.#deployInFlight = false;
322
+ const restartRequestedDuringDeploy = this.#restartRequestedDuringDeploy;
323
+ this.#restartRequestedDuringDeploy = false;
272
324
 
273
- // Resume each EntryHandler BEFORE notifying plugins. Otherwise a plugin
274
- // throwing in its deploy:end handler would abort this function and
275
- // leave the watchers permanently paused (surfaced by Gemini review).
276
- // The fresh chokidar watcher does an initial scan and emits add events
277
- // for the post-deploy tree; the existing per-event listener calls
278
- // scope.requestRestart() for each, and the restart debounce in
279
- // componentLoader collapses them into a single restart cycle. Plugin
280
- // handlers stay attached across the pause.
325
+ // Resume before notifying plugins so a throwing deploy:end listener cannot strand the watchers.
281
326
  for (const entryHandler of this.#entryHandlers) {
282
- void entryHandler.resume();
327
+ void entryHandler.resume().catch(() => {});
283
328
  }
329
+ if (restartRequestedDuringDeploy) this.requestRestart();
330
+ void this.applicationScope
331
+ ?.finishDeploy()
332
+ .then((runtimeChanged) => {
333
+ if (runtimeChanged) this.requestRestart();
334
+ })
335
+ .catch((error) => {
336
+ this.#logger.error?.(`Could not verify the loaded runtime after deploying ${this.#appName}:`, error);
337
+ this.requestRestart();
338
+ });
284
339
 
285
340
  this.#safeEmit('deploy:end', componentName);
286
341
  }
287
342
 
288
- // Swallow and log listener exceptions so one buggy plugin can't stop us
289
- // from running the rest of the deploy-lifecycle bookkeeping. EventEmitter
290
- // by default rethrows from synchronous listeners.
291
343
  #safeEmit(event: 'deploy:start' | 'deploy:end', componentName: string): void {
292
344
  try {
293
345
  this.emit(event, componentName);
@@ -304,6 +356,7 @@ export class Scope extends EventEmitter<ScopeEventsMap> {
304
356
  .on('unlink', this.#defaultEntryHandlerListener('unlink'))
305
357
  .on('addDir', this.#defaultEntryHandlerListener('addDir'))
306
358
  .on('unlinkDir', this.#defaultEntryHandlerListener('unlinkDir'));
359
+ if (this.#deployInFlight) entryHandler.pause();
307
360
 
308
361
  this.#entryHandlers.push(entryHandler);
309
362
 
@@ -345,8 +398,7 @@ export class Scope extends EventEmitter<ScopeEventsMap> {
345
398
  return;
346
399
  }
347
400
 
348
- // Otherwise, if an entry handler exists, update it with the new config
349
- scope.#entryHandler.update(config as FileAndURLPathConfig);
401
+ void scope.#entryHandler.update(config as FileAndURLPathConfig).catch(() => {});
350
402
 
351
403
  return;
352
404
  }
@@ -361,6 +413,23 @@ export class Scope extends EventEmitter<ScopeEventsMap> {
361
413
  };
362
414
  }
363
415
 
416
+ #optionsWatcherRemoveListener() {
417
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
418
+ const scope = this;
419
+ // Deleting a component's config block emits `remove` (not `change`), so without
420
+ // this listener a running component keeps serving until some unrelated restart —
421
+ // even though absence is the canonical disabled state for opt-in built-ins like
422
+ // the /v1 models gateway. Mirrors the change listener: a plugin that registers
423
+ // its own `remove` handler owns the response and no restart is requested.
424
+ return function handleOptionsWatcherRemove(this: OptionsWatcher) {
425
+ if (this.listenerCount('remove') > 1) {
426
+ return;
427
+ }
428
+ scope.#logger.debug?.('Options removed, requesting restart');
429
+ scope.requestRestart();
430
+ };
431
+ }
432
+
364
433
  #getFilesOption(): FileAndURLPathConfig | undefined {
365
434
  const config = this.options.getAll();
366
435
  if (
@@ -474,9 +543,7 @@ export class Scope extends EventEmitter<ScopeEventsMap> {
474
543
 
475
544
  requestRestart() {
476
545
  if (this.#deployInFlight) {
477
- // Suppressed: a deploy is rewriting this component's files. The
478
- // post-deploy re-scan in #onDeployEnd will trigger the coalesced
479
- // restart instead.
546
+ this.#restartRequestedDuringDeploy = true;
480
547
  this.#logger.debug?.(`Restart suppressed (deploy in flight) for ${this.#appName}`);
481
548
  return;
482
549
  }
@@ -495,6 +562,29 @@ export class Scope extends EventEmitter<ScopeEventsMap> {
495
562
  }
496
563
  }
497
564
 
565
+ waitForDeployCompletion(timeoutMs = 6 * 60 * 60 * 1000): Promise<void> {
566
+ if (!this.#deployInFlight) return Promise.resolve();
567
+ return new Promise((resolve, reject) => {
568
+ const timer = setTimeout(() => {
569
+ deployLifecycle.off('deploy:end', handleDeployEnd);
570
+ reject(new Error(`Timed out waiting for deployment of ${this.#appName} to complete`));
571
+ }, timeoutMs);
572
+ timer.unref?.();
573
+ const handleDeployEnd = (componentName: string) => {
574
+ if (componentName !== this.#appName || this.#deployInFlight) return;
575
+ deployLifecycle.off('deploy:end', handleDeployEnd);
576
+ clearTimeout(timer);
577
+ resolve();
578
+ };
579
+ deployLifecycle.on('deploy:end', handleDeployEnd);
580
+ if (!this.#deployInFlight) {
581
+ deployLifecycle.off('deploy:end', handleDeployEnd);
582
+ clearTimeout(timer);
583
+ resolve();
584
+ }
585
+ });
586
+ }
587
+
498
588
  /**
499
589
  * Import a file into the scope's sandbox.
500
590
  * @param filePath - The path of the file to import.