@runeya/runeya 2.0.145 → 2.0.147

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 (194) hide show
  1. package/agent/index.js +36 -25
  2. package/agent/index.js.map +1 -1
  3. package/agent-manager-46C4NIQ2.js +13 -0
  4. package/chat-CMNDMXIP.js +28 -0
  5. package/{chunk-ONVAH65J.js → chunk-2NE52CL5.js} +422 -2172
  6. package/chunk-2NE52CL5.js.map +1 -0
  7. package/chunk-4D3GCBPO.js +462 -0
  8. package/chunk-4D3GCBPO.js.map +1 -0
  9. package/{chunk-NCO4OOYT.js → chunk-4LJQIQMN.js} +2 -2
  10. package/{chunk-QF56Z6LI.js → chunk-5XG3X2TM.js} +29 -1481
  11. package/chunk-5XG3X2TM.js.map +1 -0
  12. package/chunk-6IBPK6UE.js +1063 -0
  13. package/chunk-6IBPK6UE.js.map +1 -0
  14. package/chunk-7QXP4FXD.js +969 -0
  15. package/chunk-7QXP4FXD.js.map +1 -0
  16. package/chunk-DA2DZBTL.js +43 -0
  17. package/chunk-DA2DZBTL.js.map +1 -0
  18. package/chunk-DFPZ4MTI.js +1041 -0
  19. package/chunk-DFPZ4MTI.js.map +1 -0
  20. package/{chunk-Q4RPY3VW.js → chunk-JNK6H5KH.js} +185 -1014
  21. package/chunk-JNK6H5KH.js.map +1 -0
  22. package/{chunk-FE53XX4M.js → chunk-NPMSBYXC.js} +3 -3
  23. package/{chunk-KS7A5CTW.js → chunk-VPSQJOSN.js} +12 -2
  24. package/{chunk-KS7A5CTW.js.map → chunk-VPSQJOSN.js.map} +1 -1
  25. package/chunk-WDHLEO5S.js +809 -0
  26. package/chunk-WDHLEO5S.js.map +1 -0
  27. package/{companion-HBYSY2D5.js → companion-XKMNRIJB.js} +10 -5
  28. package/environment-resolver-ZDTF3MHI.js +18 -0
  29. package/external-NMKQPROK.js +387 -0
  30. package/external-NMKQPROK.js.map +1 -0
  31. package/index.js +15 -6
  32. package/index.js.map +1 -1
  33. package/{install-gate-JAEK4BDZ.js → install-gate-OJJEUU4E.js} +2 -2
  34. package/mcp-ready-TAPBAOZY.js +9 -0
  35. package/mcp-server/external.d.ts +44 -0
  36. package/mcp-server/external.js +2679 -0
  37. package/mcp-server/external.js.map +1 -0
  38. package/mcp-server/index.js +159 -39
  39. package/mcp-server/index.js.map +1 -1
  40. package/mcp-server/launch.d.ts +25 -1
  41. package/mcp-server/launch.js +22 -1
  42. package/mcp-server/launch.js.map +1 -1
  43. package/package.json +1 -1
  44. package/project-store-FLXTAD3J.js +23 -0
  45. package/project-store-FLXTAD3J.js.map +1 -0
  46. package/settings-manager-XPLBO35P.js +10 -0
  47. package/settings-manager-XPLBO35P.js.map +1 -0
  48. package/{src-IODFUZ7A.js → src-KWO3CKZU.js} +1083 -551
  49. package/src-KWO3CKZU.js.map +1 -0
  50. package/{vault-resolver-PN3TYCLJ.js → vault-resolver-E7KPKA5C.js} +2 -2
  51. package/vault-resolver-E7KPKA5C.js.map +1 -0
  52. package/web/assets/{AgentScalarChatInterface.vue-npQgDxeP.js → AgentScalarChatInterface.vue-DKBinB_l.js} +2 -2
  53. package/web/assets/AgentTerminalPage-CixU7x6O.js +1 -0
  54. package/web/assets/AiChatModals-BvGUd-QD.css +1 -0
  55. package/web/assets/AiChatModals-C-hXImkO.js +4 -0
  56. package/web/assets/AppLayout-At9wk0sx.js +1 -0
  57. package/web/assets/{AppLayout-tsw5Ow0w.css → AppLayout-BlhwflMq.css} +1 -1
  58. package/web/assets/AuditPage-BkqbHxjY.js +1 -0
  59. package/web/assets/CloudAccountPage-D5C2SCS9.js +1 -0
  60. package/web/assets/CloudMessagingPage-Bi57GGHO.js +2 -0
  61. package/web/assets/DashboardPage-dEQop9ql.js +1 -0
  62. package/web/assets/DirectoryPickerDialog-DIMSeDhw.js +1 -0
  63. package/web/assets/{EditorIcon-CypGLV6w.js → EditorIcon-W3Szca4J.js} +1 -1
  64. package/web/assets/EnvironmentForm-C5emavNh.js +1 -0
  65. package/web/assets/EnvironmentFormPage-BIoy8fsW.js +1 -0
  66. package/web/assets/EnvironmentListPage-BDYeh7XF.js +1 -0
  67. package/web/assets/{FlagChip-R_T5qYpL.js → FlagChip-DPtIBCjQ.js} +1 -1
  68. package/web/assets/KanbanBoardPage-BlCcjBwV.js +4 -0
  69. package/web/assets/KanbanListPage-DbXONxfC.js +1 -0
  70. package/web/assets/LaunchProjectPage-Cg92hHwQ.js +1 -0
  71. package/web/assets/LaunchServicesPage-CO8gOB9D.js +1 -0
  72. package/web/assets/LaunchServicesPage-h5o3zLcy.css +1 -0
  73. package/web/assets/{LaunchShell-CGNYr-do.js → LaunchShell-CDb21ihZ.js} +1 -1
  74. package/web/assets/LogViewer-DsusKk2b.js +3 -0
  75. package/web/assets/OrgKeyPrompt-CX9K7r9I.js +1 -0
  76. package/web/assets/ProcessControls-B7hWVqiR.css +1 -0
  77. package/web/assets/ProcessControls-TEZutuxq.js +1 -0
  78. package/web/assets/ProcessDetailPage-Cj23FECq.js +1 -0
  79. package/web/assets/ProcessMetrics-1P6iE2Il.js +1 -0
  80. package/web/assets/ProjectCreateDialog-D3aMWKfw.js +1 -0
  81. package/web/assets/{ProjectLocationGate-bUd_tFpI.js → ProjectLocationGate-DU0Jj8Nw.js} +1 -1
  82. package/web/assets/ProjectPublishDialog-D7sNKei3.js +1 -0
  83. package/web/assets/{RandomJobTestPage-BFNC827R.js → RandomJobTestPage-DQUMRHyf.js} +1 -1
  84. package/web/assets/{ScenarioEditorPage-vMoRt0Qn.css → ScenarioEditorPage-BSjSX4bw.css} +1 -1
  85. package/web/assets/ScenarioEditorPage-DvCdcY9m.js +14 -0
  86. package/web/assets/SendToChatDialog-BdWKjYmv.js +1 -0
  87. package/web/assets/ServiceControlsBar-B_J5m-Kc.js +1 -0
  88. package/web/assets/ServiceDetailPage-DhJcjFfY.js +2 -0
  89. package/web/assets/{ServiceDetailPage-COdaCZ6r.css → ServiceDetailPage-DnSf5cm9.css} +1 -1
  90. package/web/assets/ServiceFormPage-fp8NCHSv.js +1 -0
  91. package/web/assets/SettingsPage-NH2fQap4.css +1 -0
  92. package/web/assets/SettingsPage-s28BodWe.js +2086 -0
  93. package/web/assets/UnlockPage-BXXIPHW_.js +1 -0
  94. package/web/assets/VaultMigrationPage-BfNuKtkJ.js +1 -0
  95. package/web/assets/{VersionBadge-DBquJl76.js → VersionBadge-CW7czacg.js} +1 -1
  96. package/web/assets/{WikiPage-GtwpJPqf.js → WikiPage-BiwpCM3E.js} +1 -1
  97. package/web/assets/WikiView-CMch_06Z.js +1 -0
  98. package/web/assets/{add-scalar-classes-DhSSqZ5l.js → add-scalar-classes-Ckanjq1h.js} +14 -14
  99. package/web/assets/{agent.store-Co44dMZf.js → agent.store-piokQ9Dt.js} +1 -1
  100. package/web/assets/{auth-Bk1GP5sB.js → auth-BT_jf-5e.js} +2 -2
  101. package/web/assets/cloud-chat.store-CDKh4tIy.js +1 -0
  102. package/web/assets/{cloud-client-0zD_NI6q.js → cloud-client-CzPdZLDq.js} +2 -2
  103. package/web/assets/{cloud-session-Cjw54Y--.js → cloud-session-BYfn6YM4.js} +2 -2
  104. package/web/assets/floating-ui.dom-DF3oyfHd.js +1 -0
  105. package/web/assets/i18n-D0FT7rCj.js +1 -0
  106. package/web/assets/index-3LTzgeU-.css +1 -0
  107. package/web/assets/index-Ci2CNnit.js +309 -0
  108. package/web/assets/modal-iRaP1izE.js +5 -0
  109. package/web/assets/notification.store-CvvuTBVz.js +1 -0
  110. package/web/assets/{project-launch.store-B5nhdGdh.js → project-launch.store-CTmbl8UY.js} +1 -1
  111. package/web/assets/{project.store-Bm2JWNbd.js → project.store-CNC6_4r4.js} +1 -1
  112. package/web/assets/runtime-dom.esm-bundler-CyEVRK6B.js +1 -0
  113. package/web/assets/scenario.store-BIFQPXgl.js +7 -0
  114. package/web/assets/{service.store-XS50iGKo.js → service.store-B40U4QzI.js} +1 -1
  115. package/web/assets/settings-scope-CyWMCjRY.js +1 -0
  116. package/web/assets/settings.store-Dw6bAoWE.js +1 -0
  117. package/web/assets/{src-Cgix2VOi.js → src-CrNi4Pjg.js} +3 -3
  118. package/web/assets/src-DIBHat7I.js +3 -0
  119. package/web/assets/src-DtCW_XCh.js +1 -0
  120. package/web/assets/{src-BDZKZr9n.css → src-g1DRo8xZ.css} +1 -1
  121. package/web/assets/{trpc-CfuFkKgp.js → trpc-BAZGRZBD.js} +3 -3
  122. package/web/assets/{useCloudOrg-J8DAnocE.js → useCloudOrg-EnXFBwt1.js} +1 -1
  123. package/web/assets/useEnvVarAutocomplete-BxJh2Lw0.js +1 -0
  124. package/web/assets/useProjectCatalog-BrvUGv9A.js +1 -0
  125. package/web/assets/{useProjectCloudOrg-BnlKts7x.js → useProjectCloudOrg-Czxlg00k.js} +1 -1
  126. package/web/assets/{useServiceUrlHost-hKbKtPMu.js → useServiceUrlHost-Szz9y3rh.js} +1 -1
  127. package/web/assets/vue-i18n-o7yraQpR.js +3 -0
  128. package/web/index.html +21 -21
  129. package/agent-manager-THV3LJOF.js +0 -11
  130. package/chat-XYCHMTFE.js +0 -23
  131. package/chunk-ONVAH65J.js.map +0 -1
  132. package/chunk-Q4RPY3VW.js.map +0 -1
  133. package/chunk-QF56Z6LI.js.map +0 -1
  134. package/settings-manager-QVQRJO6V.js +0 -10
  135. package/src-IODFUZ7A.js.map +0 -1
  136. package/web/assets/AgentTerminalPage-BTH3yCS2.js +0 -1
  137. package/web/assets/AiChatModals-EfRuvId7.css +0 -1
  138. package/web/assets/AiChatModals-It9jnB26.js +0 -4
  139. package/web/assets/AppLayout-BTg2CADo.js +0 -1
  140. package/web/assets/AuditPage-2PKKGLZs.js +0 -1
  141. package/web/assets/CloudAccountPage-CHL61gLe.js +0 -1
  142. package/web/assets/CloudMessagingPage-cgKnABPH.js +0 -2
  143. package/web/assets/DashboardPage-CRntTMKY.js +0 -1
  144. package/web/assets/DirectoryPickerDialog-DCyhT3-W.js +0 -1
  145. package/web/assets/EnvironmentForm-C96PFWRE.js +0 -1
  146. package/web/assets/EnvironmentFormPage-ChorAd63.js +0 -1
  147. package/web/assets/EnvironmentListPage-Dw9ziaI8.js +0 -1
  148. package/web/assets/KanbanBoardPage-BZHbEj-q.js +0 -4
  149. package/web/assets/KanbanListPage-B0jlfKqs.js +0 -1
  150. package/web/assets/LaunchProjectPage-BVs1JwbV.js +0 -1
  151. package/web/assets/LaunchServicesPage-DoLDPqQq.css +0 -1
  152. package/web/assets/LaunchServicesPage-PBYJly9N.js +0 -1
  153. package/web/assets/LogViewer-BDObx29F.js +0 -3
  154. package/web/assets/OrgKeyPrompt-BbcwiwGw.js +0 -1
  155. package/web/assets/ProcessControls-BlpnVylq.css +0 -1
  156. package/web/assets/ProcessControls-DY0HTKde.js +0 -1
  157. package/web/assets/ProcessDetailPage-BZ9kIQqn.js +0 -1
  158. package/web/assets/ProcessMetrics-Crlfkelj.js +0 -1
  159. package/web/assets/ProjectCreateDialog-C1wVDPcK.js +0 -1
  160. package/web/assets/ProjectPublishDialog-APUS1WEN.js +0 -1
  161. package/web/assets/ScenarioEditorPage-DFVsMVu5.js +0 -14
  162. package/web/assets/SendToChatDialog-QHIkxNio.js +0 -1
  163. package/web/assets/ServiceControlsBar-_UQXb6g0.js +0 -1
  164. package/web/assets/ServiceDetailPage-DGIXgHwR.js +0 -2
  165. package/web/assets/ServiceFormPage-RliAfS3_.js +0 -1
  166. package/web/assets/SettingsPage-BSSHxxjM.js +0 -2086
  167. package/web/assets/SettingsPage-W8YszhUL.css +0 -1
  168. package/web/assets/UnlockPage-17EgfpJs.js +0 -1
  169. package/web/assets/VaultMigrationPage-DeMxpXXJ.js +0 -1
  170. package/web/assets/WikiView-BqaqxMxW.js +0 -1
  171. package/web/assets/cloud-chat.store-CadB14dZ.js +0 -1
  172. package/web/assets/editor.store-DmM6ZlTm.js +0 -1
  173. package/web/assets/floating-ui.dom-DWhsZyqT.js +0 -1
  174. package/web/assets/i18n-8FjeJaKW.js +0 -1
  175. package/web/assets/index-B9DJf2V0.js +0 -309
  176. package/web/assets/index-teBqdnuv.css +0 -1
  177. package/web/assets/modal-UK8xRSnh.js +0 -5
  178. package/web/assets/scenario.store-88w6P6s2.js +0 -7
  179. package/web/assets/settings-scope-CKPq86HZ.js +0 -1
  180. package/web/assets/settings.store-BCJ0LJQA.js +0 -1
  181. package/web/assets/src--GO-VSJ0.js +0 -5
  182. package/web/assets/src-DBipSD56.js +0 -1
  183. package/web/assets/useEnvVarAutocomplete-C6zOXYqi.js +0 -1
  184. package/web/assets/useProjectCatalog-BB5IR6o8.js +0 -1
  185. package/web/assets/vue-draggable-plus-BHsveB8w.css +0 -1
  186. package/web/assets/vue-draggable-plus-D5ni4jMk.js +0 -1
  187. /package/{agent-manager-THV3LJOF.js.map → agent-manager-46C4NIQ2.js.map} +0 -0
  188. /package/{chat-XYCHMTFE.js.map → chat-CMNDMXIP.js.map} +0 -0
  189. /package/{chunk-NCO4OOYT.js.map → chunk-4LJQIQMN.js.map} +0 -0
  190. /package/{chunk-FE53XX4M.js.map → chunk-NPMSBYXC.js.map} +0 -0
  191. /package/{companion-HBYSY2D5.js.map → companion-XKMNRIJB.js.map} +0 -0
  192. /package/{settings-manager-QVQRJO6V.js.map → environment-resolver-ZDTF3MHI.js.map} +0 -0
  193. /package/{install-gate-JAEK4BDZ.js.map → install-gate-OJJEUU4E.js.map} +0 -0
  194. /package/{vault-resolver-PN3TYCLJ.js.map → mcp-ready-TAPBAOZY.js.map} +0 -0
@@ -1,35 +1,28 @@
1
1
  import {
2
- decryptValueWith,
3
- encryptValueWith
4
- } from "./chunk-OZBSOSAS.js";
2
+ serviceStore
3
+ } from "./chunk-4D3GCBPO.js";
5
4
  import {
6
- atomicWriteSecure,
7
- fingerprintFiles,
8
- hasChangedSinceRead,
9
- rememberFile,
10
- renameWithRetry,
11
- wasRead
12
- } from "./chunk-NCO4OOYT.js";
5
+ decryptValue,
6
+ deriveStableSecret,
7
+ getEncryptionKey,
8
+ isEncryptedValue,
9
+ stableEncryptValue
10
+ } from "./chunk-DFPZ4MTI.js";
11
+ import {
12
+ renameWithRetry
13
+ } from "./chunk-4LJQIQMN.js";
13
14
  import {
14
- allVaultRoots,
15
15
  env,
16
16
  keyedFilePath,
17
17
  legacyMachineFilePath,
18
18
  listOrgVaults,
19
- localVaultRoot,
20
19
  machineRoot,
21
- orgVaultRoot,
22
- projectVaultRoot,
23
- vaultFor,
24
- vaultIdOfRoot
25
- } from "./chunk-KS7A5CTW.js";
20
+ orgVaultRoot
21
+ } from "./chunk-VPSQJOSN.js";
26
22
  import {
27
23
  AGENT_FETCH_TIMEOUT,
28
24
  AGENT_HEALTH_POLL_INTERVAL,
29
- CreateEnvironmentSchema,
30
- EnvironmentSchema,
31
25
  PASSPHRASE_TIMEOUT,
32
- ServiceConfigSchema,
33
26
  TraefikConfigSchema,
34
27
  WS_RECONNECT_MAX,
35
28
  WS_RECONNECT_MIN
@@ -44,106 +37,6 @@ import { z as z2 } from "zod";
44
37
  import { randomUUID, randomBytes, createCipheriv, createDecipheriv } from "crypto";
45
38
  import { readFile, writeFile, mkdir, chmod } from "fs/promises";
46
39
  import { dirname, join } from "path";
47
-
48
- // ../server/src/utils/encryption-key.ts
49
- import { createHash } from "crypto";
50
- var KEY_SIZE_BYTES = 32;
51
- var DEFAULT_VAULT = "\0default";
52
- var keyring = /* @__PURE__ */ new Map();
53
- var lockedVaults = /* @__PURE__ */ new Set();
54
- var _ciMode = false;
55
- var _pendingLegacyKey = null;
56
- function isCiMode() {
57
- return _ciMode;
58
- }
59
- async function loadFromEnv() {
60
- const keyHex = process.env["RUNEYA_ENCRYPTION_KEY"];
61
- if (!keyHex) return false;
62
- if (!/^[0-9a-f]{64}$/i.test(keyHex)) {
63
- throw new Error("RUNEYA_ENCRYPTION_KEY invalide : attendu 64 caract\xE8res hex");
64
- }
65
- console.log("");
66
- console.log("\u26A0 ATTENTION : RUNEYA_ENCRYPTION_KEY est d\xE9fini \u2014 l'authentification par master password est d\xE9sactiv\xE9e.");
67
- console.log(" Cette variable d'environnement est r\xE9serv\xE9e aux environnements CI/CD.");
68
- console.log(" Ne la d\xE9finissez jamais sur une machine de d\xE9veloppement.");
69
- console.log("");
70
- setEncryptionKeyInMemory(Buffer.from(keyHex, "hex"));
71
- _ciMode = true;
72
- return true;
73
- }
74
- function setPendingLegacyKey(key) {
75
- if (key.length !== KEY_SIZE_BYTES) throw new Error("Taille de cl\xE9 invalide");
76
- _pendingLegacyKey = key;
77
- }
78
- function getPendingLegacyKey() {
79
- return _pendingLegacyKey;
80
- }
81
- function clearPendingLegacyKey() {
82
- _pendingLegacyKey = null;
83
- }
84
- function hasPendingLegacyKey() {
85
- return _pendingLegacyKey !== null;
86
- }
87
- function setEncryptionKeyInMemory(key, vault = DEFAULT_VAULT) {
88
- if (key.length !== KEY_SIZE_BYTES) throw new Error("Taille de cl\xE9 invalide");
89
- keyring.set(vault, key);
90
- lockedVaults.delete(vault);
91
- announceUnlock();
92
- }
93
- var unlockListeners = /* @__PURE__ */ new Set();
94
- function onVaultUnlocked(listener) {
95
- unlockListeners.add(listener);
96
- return () => unlockListeners.delete(listener);
97
- }
98
- function announceUnlock() {
99
- for (const listener of unlockListeners) {
100
- try {
101
- listener();
102
- } catch {
103
- }
104
- }
105
- }
106
- function clearEncryptionKey(vault) {
107
- if (vault === void 0) {
108
- keyring.clear();
109
- lockedVaults.clear();
110
- } else {
111
- keyring.delete(vault);
112
- }
113
- }
114
- function hasEncryptionKey(vault = DEFAULT_VAULT) {
115
- return keyring.has(vault);
116
- }
117
- function getEncryptionKey(vault = DEFAULT_VAULT) {
118
- const own = keyring.get(vault);
119
- if (own) return own;
120
- if (vault === DEFAULT_VAULT || lockedVaults.has(vault)) return null;
121
- return keyring.get(DEFAULT_VAULT) ?? null;
122
- }
123
- function markVaultLocked(vault) {
124
- lockedVaults.add(vault);
125
- keyring.delete(vault);
126
- }
127
- function isVaultLocked(vault) {
128
- return lockedVaults.has(vault);
129
- }
130
- function clearVaultLock(vault) {
131
- lockedVaults.delete(vault);
132
- }
133
- function deriveStableSecret(purpose, vault = DEFAULT_VAULT) {
134
- const key = getEncryptionKey(vault);
135
- if (!key) return null;
136
- return createHash("sha256").update(key).update(purpose).digest("hex");
137
- }
138
- async function ensureEncryptionKey() {
139
- return getEncryptionKey();
140
- }
141
- async function setEncryptionKey(keyHex) {
142
- if (!/^[0-9a-f]{64}$/i.test(keyHex)) throw new Error("Invalid key format");
143
- setEncryptionKeyInMemory(Buffer.from(keyHex, "hex"));
144
- }
145
-
146
- // ../server/src/services/agent-store.ts
147
40
  var FILENAME = "agents.json";
148
41
  var ALGORITHM = "aes-256-gcm";
149
42
  function encrypt(plaintext, vault) {
@@ -395,1321 +288,19 @@ var AgentStore = class {
395
288
  };
396
289
  var agentStore = new AgentStore();
397
290
 
398
- // ../server/src/services/service-store.ts
399
- import { randomUUID as randomUUID3 } from "crypto";
400
- import { readFile as readFile3, rm as rm2 } from "fs/promises";
401
- import { join as join3 } from "path";
402
-
403
- // ../server/src/services/project-key.ts
404
- function projectKey(ref) {
405
- return `${ref.orgId ?? "local"}:${ref.id}`;
406
- }
407
- var AmbiguousProjectError = class extends Error {
408
- constructor(projectId, orgIds) {
409
- const where = orgIds.map((o) => o ?? "local").join(", ");
410
- super(
411
- `Project id ${projectId} exists in several organizations (${where}). Name the organization alongside the project id.`
412
- );
413
- this.projectId = projectId;
414
- this.orgIds = orgIds;
415
- this.name = "AmbiguousProjectError";
416
- }
417
- code = "AMBIGUOUS_PROJECT_ID";
418
- };
419
-
420
- // ../server/src/services/vault-registry.ts
421
- var loadProjects = null;
422
- var projectRoots = /* @__PURE__ */ new Map();
423
- var projectRefs = /* @__PURE__ */ new Map();
424
- var serviceOwners = /* @__PURE__ */ new Map();
425
- function registerProjectLoader(load) {
426
- loadProjects = load;
427
- }
428
- async function ensureProjectsLoaded() {
429
- await loadProjects?.();
430
- }
431
- function setProjectVault(ref, root) {
432
- const key = projectKey(ref);
433
- projectRoots.set(key, root);
434
- projectRefs.set(key, { id: ref.id, orgId: ref.orgId ?? null });
435
- }
436
- function forgetProject(ref) {
437
- const key = projectKey(ref);
438
- projectRoots.delete(key);
439
- projectRefs.delete(key);
440
- for (const [serviceId, owners] of serviceOwners) {
441
- owners.delete(key);
442
- if (owners.size === 0) serviceOwners.delete(serviceId);
443
- }
444
- }
445
- function setProjectServices(ref, serviceIds) {
446
- const key = projectKey(ref);
447
- for (const [serviceId, owners] of serviceOwners) {
448
- if (!serviceIds.includes(serviceId)) {
449
- owners.delete(key);
450
- if (owners.size === 0) serviceOwners.delete(serviceId);
451
- }
452
- }
453
- for (const serviceId of serviceIds) {
454
- const owners = serviceOwners.get(serviceId) ?? /* @__PURE__ */ new Set();
455
- owners.add(key);
456
- serviceOwners.set(serviceId, owners);
457
- }
458
- }
459
- function rootOfProjectRef(ref) {
460
- return projectRoots.get(projectKey(ref)) ?? localVaultRoot();
461
- }
462
- function refsOfProjectId(projectId) {
463
- const matches = [];
464
- for (const ref of projectRefs.values()) {
465
- if (ref.id === projectId) matches.push(ref);
466
- }
467
- return matches;
468
- }
469
- function rootOfProjectAmong(projectId, hint) {
470
- const matches = refsOfProjectId(projectId);
471
- if (matches.length > 1) {
472
- const known = matches.map((ref2) => rootOfProjectRef(ref2));
473
- if (hint && known.includes(hint)) return hint;
474
- throw new AmbiguousProjectError(projectId, matches.map((r) => r.orgId ?? null));
475
- }
476
- const [ref] = matches;
477
- return ref ? rootOfProjectRef(ref) : hint ?? localVaultRoot();
478
- }
479
- function refOfProjectAmong(projectId, hint) {
480
- const matches = refsOfProjectId(projectId);
481
- if (matches.length > 1) {
482
- const found = hint ? matches.find((ref) => rootOfProjectRef(ref) === hint) : void 0;
483
- if (found) return found;
484
- throw new AmbiguousProjectError(projectId, matches.map((r) => r.orgId ?? null));
485
- }
486
- return matches[0] ?? { id: projectId, orgId: null };
487
- }
488
- function rootOfService(serviceId, hint = null) {
489
- const owners = serviceOwners.get(serviceId);
490
- if (!owners || owners.size === 0) return hint ?? localVaultRoot();
491
- const roots = Array.from(owners, (key) => projectRoots.get(key)).filter((r) => !!r);
492
- if (hint && roots.includes(hint)) return hint;
493
- return roots[0] ?? hint ?? localVaultRoot();
494
- }
495
-
496
- // ../server/src/services/local-removals.ts
497
- var removed = /* @__PURE__ */ new Map();
498
- var detached = /* @__PURE__ */ new Map();
499
- var linkKey = (projectId, field) => `${projectId}:${field}`;
500
- function recordLocalRemoval(kind, id) {
501
- const ids = removed.get(kind) ?? /* @__PURE__ */ new Set();
502
- ids.add(id);
503
- removed.set(kind, ids);
504
- }
505
- function isLocallyRemoved(kind, id) {
506
- return removed.get(kind)?.has(id) ?? false;
507
- }
508
- function clearLocalRemoval(kind, id) {
509
- removed.get(kind)?.delete(id);
510
- }
511
- function recordDetached(projectId, field, before, after) {
512
- const kept = new Set(after);
513
- const gone = before.filter((id) => !kept.has(id));
514
- if (gone.length === 0) return;
515
- const key = linkKey(projectId, field);
516
- const ids = detached.get(key) ?? /* @__PURE__ */ new Set();
517
- for (const id of gone) ids.add(id);
518
- detached.set(key, ids);
519
- }
520
- function isDetached(projectId, field, id) {
521
- return detached.get(linkKey(projectId, field))?.has(id) ?? false;
522
- }
523
- function clearDetached(projectId, field) {
524
- detached.delete(linkKey(projectId, field));
525
- }
526
-
527
- // ../server/src/services/vault-conflicts.ts
528
- var conflicted = /* @__PURE__ */ new Map();
529
- function isVaultClash(root, heldIn) {
530
- return heldIn !== void 0 && heldIn !== root && heldIn !== localVaultRoot();
531
- }
532
- function markVaultConflict(root, detail) {
533
- if (conflicted.has(root)) return;
534
- conflicted.set(root, detail);
535
- console.error(
536
- "[vaults] %s partage des identifiants avec un autre coffre (%s) \u2014 son contenu est ignor\xE9 et laiss\xE9 intact. Cessez de suivre l'un des deux projets.",
537
- root,
538
- detail
539
- );
540
- }
541
- function isVaultConflicted(root) {
542
- return conflicted.has(root);
543
- }
544
-
545
- // ../server/src/services/store-disk-coordinator.ts
546
- import { AsyncLocalStorage } from "async_hooks";
547
- var insideReload = new AsyncLocalStorage();
548
- var StoreDiskCoordinator = class {
549
- reloading = null;
550
- saves = 0;
551
- queue = Promise.resolve();
552
- /**
553
- * `read` rend de quoi substituer le nouvel état, ou `null` s'il n'y a rien à
554
- * relire. La substitution doit être synchrone : c'est ce qui la rend atomique.
555
- */
556
- load(read) {
557
- const inside = insideReload.getStore();
558
- if (inside && (inside.has(this) || this.reloading)) return Promise.resolve();
559
- this.reloading ??= this.reload(read).finally(() => {
560
- this.reloading = null;
561
- });
562
- return this.reloading;
563
- }
564
- async reload(read) {
565
- for (; ; ) {
566
- await this.queue;
567
- const savesBefore = this.saves;
568
- const outer = insideReload.getStore() ?? /* @__PURE__ */ new Set();
569
- const commit = await insideReload.run(/* @__PURE__ */ new Set([...outer, this]), read);
570
- if (!commit) return;
571
- if (this.saves !== savesBefore) continue;
572
- commit();
573
- return;
574
- }
575
- }
576
- save(write) {
577
- this.saves += 1;
578
- const run = this.queue.then(write);
579
- this.queue = run.catch(() => {
580
- });
581
- return run;
582
- }
583
- };
584
-
585
- // ../server/src/services/environment-store.ts
586
- import { randomUUID as randomUUID2 } from "crypto";
587
- import { readFile as readFile2, rm } from "fs/promises";
588
- import { join as join2 } from "path";
589
-
590
- // ../server/src/services/crypto-helpers.ts
591
- function encryptValue(plaintext, vault) {
592
- return encryptValueWith(requireKey(vault), plaintext);
593
- }
594
- function decryptValue(encrypted, vault) {
595
- return decryptValueWith(requireKey(vault), encrypted);
596
- }
597
- function requireKey(vault) {
598
- const key = getEncryptionKey(vault);
599
- if (!key) {
600
- throw new Error("Encryption key not loaded \u2014 please provide the key via setup UI");
601
- }
602
- return key;
603
- }
604
- function stableEncryptValue(plaintext, existingEncrypted, vault) {
605
- if (existingEncrypted) {
606
- try {
607
- if (decryptValue(existingEncrypted, vault) === plaintext) return existingEncrypted;
608
- } catch {
609
- }
610
- }
611
- return encryptValue(plaintext, vault);
612
- }
613
- function encryptVariableSlots(variables, existingEncrypted, vault) {
614
- const result = {};
615
- for (const [key, variable] of Object.entries(variables)) {
616
- const slot = variable.value;
617
- if (slot.isSecret && slot.value) {
618
- result[key] = { ...variable, value: { ...slot, value: stableEncryptValue(slot.value, existingEncrypted?.[key], vault) } };
619
- } else {
620
- result[key] = { ...variable, value: { ...slot } };
621
- }
622
- }
623
- return result;
624
- }
625
- function decryptVariableSlots(variables, contextLabel, onFailure, vault) {
626
- const result = {};
627
- for (const [key, variable] of Object.entries(variables)) {
628
- const slot = variable.value;
629
- if (slot.isSecret && slot.value) {
630
- try {
631
- result[key] = { ...variable, value: { ...slot, value: decryptValue(slot.value, vault) } };
632
- } catch (err) {
633
- console.error(`[crypto-helpers] Failed to decrypt variable "${key}" for ${contextLabel}: ${err.message} \u2014 la valeur chiffr\xE9e est conserv\xE9e telle quelle sur le disque`);
634
- onFailure?.(key);
635
- result[key] = { ...variable, value: { ...slot, value: "" } };
636
- }
637
- } else {
638
- result[key] = { ...variable, value: { ...slot } };
639
- }
640
- }
641
- return result;
642
- }
643
- function extractEncryptedVariableSlots(variables) {
644
- const slots = {};
645
- for (const [key, variable] of Object.entries(variables)) {
646
- if (variable.value?.isSecret && variable.value?.value) {
647
- slots[key] = variable.value.value;
648
- }
649
- }
650
- return slots;
651
- }
652
- function isEncryptedValue(s) {
653
- const parts = s.split(":");
654
- return parts.length === 3 && /^[0-9a-f]{32}$/i.test(parts[0]) && /^[0-9a-f]{32}$/i.test(parts[1]);
655
- }
656
-
657
- // ../server/src/services/environment-store.ts
658
- var FILENAME2 = "environments.json";
659
- var EnvironmentStore = class {
660
- environments = /* @__PURE__ */ new Map();
661
- loaded = false;
662
- /** Cache of encrypted secret values as last written to disk */
663
- diskEncryptedSlots = /* @__PURE__ */ new Map();
664
- /**
665
- * Les secrets qu'aucune clé chargée n'a ouverts, par environnement.
666
- *
667
- * Vides en mémoire, ils doivent repartir sur le disque sous leur forme
668
- * chiffrée : c'est ce que retient cette liste. Voir `keepUnreadableSecrets`.
669
- */
670
- unreadableSecrets = /* @__PURE__ */ new Map();
671
- /**
672
- * Coffres dont le fichier a bougé depuis la lecture.
673
- *
674
- * On ne les réécrit pas. Ni avec notre copie — elle décrirait l'état d'avant
675
- * la réparation — ni sans elle, ce qui effacerait les secrets qu'elle
676
- * protège. Entre les deux, la seule issue sûre est de ne pas toucher au
677
- * fichier et de laisser le prochain chargement faire foi.
678
- */
679
- staleRoots = /* @__PURE__ */ new Set();
680
- /**
681
- * L'état du disque au dernier chargement — voir `ProjectStore.fingerprint`.
682
- *
683
- * `staleRoots` dit « ne réécris pas ce fichier, il a bougé » ; celui-ci dit
684
- * « relis-le ». Les deux vont ensemble : refuser d'écrire protège le travail
685
- * de l'autre instance, mais tant que la mémoire n'est pas reprise, l'écriture
686
- * refusée l'est pour toujours et la modification de l'utilisateur se perd.
687
- */
688
- fingerprint = "";
689
- disk = new StoreDiskCoordinator();
690
- listeners = /* @__PURE__ */ new Set();
691
- /** Notified after every persisted mutation, for the cloud sync bridge. */
692
- onChange(fn) {
693
- this.listeners.add(fn);
694
- return () => this.listeners.delete(fn);
695
- }
696
- notify() {
697
- for (const fn of this.listeners) fn();
698
- }
699
- /**
700
- * Which vault each environment was read from, so a save puts it back where it
701
- * belongs even before its project has been loaded.
702
- */
703
- roots = /* @__PURE__ */ new Map();
704
- /** L'organisation d'où vient un environnement reçu du cloud — voir `vaultOf`. */
705
- remoteOrgs = /* @__PURE__ */ new Map();
706
- /**
707
- * Oublier ce qui a été lu, pour tout relire sous la clé courante.
708
- *
709
- * Appelé après l'adoption d'une clé d'organisation : le disque vient d'être
710
- * re-chiffré sous les pieds du store, si bien que sa mémoire décrit un état
711
- * qui n'existe plus. La laisser en place n'affichait pas seulement des
712
- * comptes périmés — la sauvegarde suivante réécrivait l'ancien chiffré
713
- * par-dessus le nouveau, et défaisait l'adoption.
714
- */
715
- resetCache() {
716
- this.environments.clear();
717
- this.diskEncryptedSlots.clear();
718
- this.unreadableSecrets.clear();
719
- this.roots.clear();
720
- this.remoteOrgs.clear();
721
- this.loaded = false;
722
- this.notify();
723
- }
724
- /**
725
- * Combien de secrets ce coffre garde chiffrés sans savoir les ouvrir, par
726
- * organisation (`null` pour la machine).
727
- *
728
- * Ils sont déjà préservés à l'écriture (`keepUnreadableSecrets`) ; ce qui
729
- * manquait, c'est de le dire. Un secret qui s'affiche vide et se sauvegarde
730
- * intact est rassurant sur le disque et déroutant à l'écran.
731
- */
732
- async unreadableByVault() {
733
- await this.load();
734
- const out = /* @__PURE__ */ new Map();
735
- for (const [environmentId, failed] of this.unreadableSecrets) {
736
- if (!failed.size) continue;
737
- const environment = this.environments.get(environmentId);
738
- const vault = (environment ? this.vaultOf(environment) : void 0) ?? null;
739
- out.set(vault, (out.get(vault) ?? 0) + failed.size);
740
- }
741
- return out;
742
- }
743
- getFilePath(root) {
744
- return join2(root, FILENAME2);
745
- }
746
- /**
747
- * The vault an environment must be written to: its project's.
748
- *
749
- * An environment names its project by id alone, and that id can answer for a
750
- * project in each organization. The vault it was read from names the couple —
751
- * so it is handed over as the tie-breaker rather than left as a mere fallback.
752
- *
753
- * Falls back to where it was last read, then to the local vault — an
754
- * environment whose project is unknown belongs to the launch directory, which
755
- * is where it lived before vaults existed.
756
- */
757
- /**
758
- * La clé sous laquelle chiffrer cet environnement.
759
- *
760
- * La provenance l'emporte sur le coffre tant que le projet n'est pas local :
761
- * un environnement reçu du cloud arrive avant lui, et `rootFor` désignerait
762
- * alors le répertoire de lancement — donc la clé de la machine plutôt que
763
- * celle de l'organisation qui a chiffré ces secrets.
764
- */
765
- vaultOf(environment) {
766
- return this.remoteOrgs.get(environment.id) ?? vaultIdOfRoot(this.rootFor(environment));
767
- }
768
- rootFor(environment) {
769
- const known = this.roots.get(environment.id) ?? null;
770
- const projectRoot = environment.projectId ? rootOfProjectAmong(environment.projectId, known) : null;
771
- return projectRoot ?? known ?? localVaultRoot();
772
- }
773
- /**
774
- * Le projet d'un environnement, organisation comprise.
775
- *
776
- * L'environnement ne nomme que l'id ; le coffre d'où il vient fournit
777
- * l'organisation, et c'est ce couple qu'il faut pour agir sur son projet.
778
- */
779
- projectRefOf(environment) {
780
- if (!environment.projectId) return null;
781
- return refOfProjectAmong(environment.projectId, this.roots.get(environment.id) ?? null);
782
- }
783
- /**
784
- * The vault holding the environment that defines a variable.
785
- *
786
- * Overrides answer a variable, not a project: this is what lets them find the
787
- * same vault without re-resolving an id that may be ambiguous.
788
- */
789
- async rootOfVariable(variableId) {
790
- await this.load();
791
- for (const environment of this.environments.values()) {
792
- if (environment.variables && variableId in environment.variables) {
793
- return this.rootFor(environment);
794
- }
795
- }
796
- return null;
797
- }
798
- extractEncryptedCache(environment) {
799
- const envSlots = extractEncryptedVariableSlots(
800
- environment.variables
801
- );
802
- return { envSlots };
803
- }
804
- /**
805
- * `vault` est fourni quand l'appelant sait à qui appartient l'environnement
806
- * mieux que le disque : à l'arrivée d'une entité distante, son projet n'est
807
- * pas encore local, et le coffre déduit serait celui du répertoire de
808
- * lancement — donc la mauvaise clé.
809
- */
810
- encryptEnvironment(environment, vault) {
811
- const cache = this.diskEncryptedSlots.get(environment.id);
812
- let encryptedVars = environment.variables;
813
- if (environment.variables && Object.keys(environment.variables).length > 0) {
814
- encryptedVars = encryptVariableSlots(
815
- environment.variables,
816
- cache?.envSlots,
817
- // La clé est celle du coffre où l'environnement sera écrit : les
818
- // secrets d'une organisation sont chiffrés par la sienne.
819
- vault ?? this.vaultOf(environment)
820
- );
821
- }
822
- return { ...environment, variables: this.keepUnreadableSecrets(environment.id, encryptedVars) };
823
- }
824
- /**
825
- * Réécrire un secret illisible tel qu'il est sur le disque, jamais à blanc.
826
- *
827
- * Un secret qui ne s'ouvre pas est vide en mémoire (voir
828
- * `decryptVariableSlots`) : le chiffrer donnerait un chiffré de la chaîne
829
- * vide, et la sauvegarde effacerait pour de bon une valeur dont il ne
830
- * manquait que la clé. Son chiffré d'origine est encore là — c'est celui que
831
- * `diskEncryptedSlots` a mis de côté avant le déchiffrement — et c'est lui
832
- * qui repart sur le disque.
833
- *
834
- * La condition tient à la valeur en mémoire : tant qu'elle est vide, la
835
- * variable n'a pas été retouchée et son chiffré fait foi. Dès que
836
- * l'utilisateur en saisit une nouvelle, elle l'emporte — et vider un secret
837
- * volontairement reste possible, puisque cela ne concerne que les variables
838
- * dont le déchiffrement a échoué.
839
- */
840
- keepUnreadableSecrets(environmentId, variables) {
841
- const failed = this.unreadableSecrets.get(environmentId);
842
- if (!failed?.size || !variables) return variables;
843
- const onDisk = this.diskEncryptedSlots.get(environmentId)?.envSlots ?? {};
844
- const result = { ...variables };
845
- for (const variableId of failed) {
846
- const ciphertext = onDisk[variableId];
847
- const variable = result[variableId];
848
- if (!ciphertext || !variable || variable.value.value) continue;
849
- result[variableId] = { ...variable, value: { ...variable.value, value: ciphertext } };
850
- }
851
- return result;
852
- }
853
- decryptEnvironment(environment, vault, unreadable = this.unreadableSecrets) {
854
- let decryptedVars = environment.variables;
855
- if (environment.variables && Object.keys(environment.variables).length > 0) {
856
- const failed = /* @__PURE__ */ new Set();
857
- decryptedVars = decryptVariableSlots(
858
- environment.variables,
859
- `environment ${environment.id}`,
860
- (variableId) => failed.add(variableId),
861
- vault ?? this.vaultOf(environment)
862
- );
863
- if (failed.size > 0) unreadable.set(environment.id, failed);
864
- else unreadable.delete(environment.id);
865
- }
866
- return { ...environment, variables: decryptedVars };
867
- }
868
- /** Ce que le disque porte : un fichier d'environnements par coffre. */
869
- async diskFingerprint() {
870
- return fingerprintFiles((await allVaultRoots()).map((root) => this.getFilePath(root)));
871
- }
872
- /**
873
- * Read every vault — the launch directory and each materialized cloud project.
874
- *
875
- * The roots come from the disk rather than from the project store, so loading
876
- * environments never has to wait on projects being loaded first (the project
877
- * store reads environments while absorbing a project, which would deadlock).
878
- */
879
- async load() {
880
- await this.disk.load(async () => {
881
- const fingerprint = await this.diskFingerprint();
882
- if (this.loaded && fingerprint === this.fingerprint) return null;
883
- await ensureProjectsLoaded();
884
- if (!getEncryptionKey()) return null;
885
- const next = await this.readVaults();
886
- return () => {
887
- this.environments = next.environments;
888
- this.diskEncryptedSlots = next.diskEncryptedSlots;
889
- this.roots = next.roots;
890
- this.unreadableSecrets = next.unreadableSecrets;
891
- this.fingerprint = fingerprint;
892
- this.loaded = true;
893
- };
894
- });
895
- }
896
- /** Lire tous les coffres dans des tables neuves, que `load` substitue d'un coup. */
897
- async readVaults() {
898
- const environments = /* @__PURE__ */ new Map();
899
- const diskEncryptedSlots = /* @__PURE__ */ new Map();
900
- const roots = /* @__PURE__ */ new Map();
901
- const unreadableSecrets = /* @__PURE__ */ new Map();
902
- for (const root of await allVaultRoots()) {
903
- try {
904
- const raw = await readFile2(this.getFilePath(root), "utf-8");
905
- await rememberFile(this.getFilePath(root));
906
- const data = JSON.parse(raw);
907
- const clash = data.find((e) => isVaultClash(root, roots.get(e.id)));
908
- if (clash) {
909
- markVaultConflict(root, `environnement ${clash.id}`);
910
- continue;
911
- }
912
- for (const e of data) {
913
- diskEncryptedSlots.set(e.id, this.extractEncryptedCache(e));
914
- roots.set(e.id, root);
915
- environments.set(e.id, this.decryptEnvironment(e, vaultIdOfRoot(root), unreadableSecrets));
916
- }
917
- } catch {
918
- }
919
- }
920
- return { environments, diskEncryptedSlots, roots, unreadableSecrets };
921
- }
922
- /**
923
- * Write each vault that holds — or held — an environment.
924
- *
925
- * A vault emptied by this save loses its file: an environment that moved to
926
- * another vault must not survive in the old one, and a `[]` left behind would
927
- * keep (or recreate) a `.runeya` in a directory that holds nothing.
928
- */
929
- /**
930
- * Repérer les coffres réparés sous nos pieds, avant d'écrire.
931
- *
932
- * Le test est fait ici, une fois par sauvegarde, plutôt que par
933
- * environnement : c'est le fichier du coffre qui change, pas la variable.
934
- */
935
- async markStaleRoots() {
936
- this.staleRoots.clear();
937
- for (const root of new Set(this.roots.values())) {
938
- const path = this.getFilePath(root);
939
- if (wasRead(path) && await hasChangedSinceRead(path)) this.staleRoots.add(root);
940
- }
941
- }
942
- save() {
943
- return this.disk.save(() => this.writeVaults());
944
- }
945
- async writeVaults() {
946
- await this.markStaleRoots();
947
- const byRoot = /* @__PURE__ */ new Map();
948
- for (const root of this.roots.values()) {
949
- if (!isVaultConflicted(root)) byRoot.set(root, []);
950
- }
951
- byRoot.set(localVaultRoot(), byRoot.get(localVaultRoot()) ?? []);
952
- for (const environment of this.environments.values()) {
953
- const root = this.rootFor(environment);
954
- const prepared = this.encryptEnvironment(environment);
955
- this.diskEncryptedSlots.set(prepared.id, this.extractEncryptedCache(prepared));
956
- this.roots.set(environment.id, root);
957
- if (isVaultConflicted(root)) continue;
958
- const bucket = byRoot.get(root);
959
- if (bucket) bucket.push(prepared);
960
- else byRoot.set(root, [prepared]);
961
- }
962
- for (const [root, environments] of byRoot) {
963
- if (this.staleRoots.has(root)) {
964
- console.warn(
965
- `[environment-store] ${this.getFilePath(root)} a chang\xE9 depuis sa lecture \u2014 \xE9criture ignor\xE9e pour ne pas \xE9craser une r\xE9paration faite en dehors du serveur.`
966
- );
967
- continue;
968
- }
969
- const filePath = this.getFilePath(root);
970
- if (environments.length === 0) {
971
- await rm(filePath, { force: true }).catch(() => {
972
- });
973
- continue;
974
- }
975
- try {
976
- await atomicWriteSecure(filePath, JSON.stringify(environments, null, 2));
977
- } catch (err) {
978
- if (err.code !== "ENOENT") throw err;
979
- console.warn(`[environment-store] ${filePath} a disparu pendant l'\xE9criture (projet supprim\xE9 ?) \u2014 coffre ignor\xE9.`);
980
- continue;
981
- }
982
- await rememberFile(filePath);
983
- }
984
- this.fingerprint = await this.diskFingerprint();
985
- this.notify();
986
- }
987
- /**
988
- * Rewrite every vault, moving entities that now belong elsewhere.
989
- *
990
- * Called once the project store knows where projects live: entities read from
991
- * the launch directory whose project turns out to be a cloud one are written
992
- * into that project's vault, and disappear from the directory they came from.
993
- */
994
- async relocateAll() {
995
- await this.load();
996
- await this.save();
997
- }
998
- /**
999
- * Which project a variable belongs to, by walking the environments that
1000
- * define it.
1001
- *
1002
- * Overrides are keyed by variable id alone — nothing in an override says
1003
- * which project it belongs to — so this is what lets them be stored with
1004
- * their project rather than in one machine-wide pile.
1005
- */
1006
- /**
1007
- * Drop a project's environments from memory, without touching the disk.
1008
- *
1009
- * Used when a project stops being followed: its vault is removed as a whole,
1010
- * and environments left in memory would be written back on the next save —
1011
- * into the local vault, since their project is no longer known.
1012
- */
1013
- async forgetByProject(projectId) {
1014
- await this.load();
1015
- for (const [id, environment] of this.environments) {
1016
- if (environment.projectId !== projectId) continue;
1017
- this.environments.delete(id);
1018
- this.diskEncryptedSlots.delete(id);
1019
- this.unreadableSecrets.delete(id);
1020
- this.roots.delete(id);
1021
- this.remoteOrgs.delete(id);
1022
- }
1023
- this.notify();
1024
- }
1025
- /**
1026
- * Les environnements d'un projet.
1027
- *
1028
- * `orgId` restreint au projet de cette organisation : deux projets homonymes
1029
- * ont chacun les siens, et filtrer sur le seul `projectId` mélangeait les deux
1030
- * listes à l'écran. Omis, le filtre reste sur l'id — ce que fait un appelant
1031
- * qui n'a pas d'organisation à donner.
1032
- */
1033
- async list(projectId, orgId) {
1034
- await this.load();
1035
- return this.ofProject(projectId, orgId);
1036
- }
1037
- /** Le filtre commun : l'id, puis le coffre quand l'organisation est connue. */
1038
- ofProject(projectId, orgId) {
1039
- const byId = Array.from(this.environments.values()).filter((e) => e.projectId === projectId);
1040
- if (orgId === void 0) return byId;
1041
- const root = vaultFor({ id: projectId, orgId }).root;
1042
- return byId.filter((e) => this.rootFor(e) === root);
1043
- }
1044
- async get(id) {
1045
- await this.load();
1046
- return this.environments.get(id) ?? null;
1047
- }
1048
- /**
1049
- * `orgId` ne fait pas partie de l'environnement : il dit dans quel coffre le
1050
- * poser. Un environnement neuf n'a pas encore de provenance, et son
1051
- * `projectId` seul peut répondre pour un projet de chaque organisation — sans
1052
- * cette précision, la création échouerait sur l'ambiguïté au lieu de viser le
1053
- * bon coffre.
1054
- */
1055
- async create(input, orgId) {
1056
- await this.load();
1057
- const parsed = CreateEnvironmentSchema.parse(input);
1058
- const environment = {
1059
- id: randomUUID2(),
1060
- projectId: parsed.projectId,
1061
- name: parsed.name,
1062
- description: parsed.description,
1063
- parentIds: parsed.parentIds,
1064
- variables: parsed.variables,
1065
- scope: parsed.scope,
1066
- reference: parsed.reference,
1067
- activateOn: parsed.activateOn,
1068
- immutable: parsed.immutable,
1069
- default: parsed.default,
1070
- color: parsed.color,
1071
- bgColor: parsed.bgColor,
1072
- agentId: parsed.agentId
1073
- };
1074
- if (environment.default) {
1075
- for (const [id, e] of this.environments) {
1076
- if (e.projectId === environment.projectId && e.default) {
1077
- this.environments.set(id, { ...e, default: false });
1078
- }
1079
- }
1080
- }
1081
- this.environments.set(environment.id, environment);
1082
- if (environment.projectId && orgId !== void 0) {
1083
- this.roots.set(environment.id, vaultFor({ id: environment.projectId, orgId }).root);
1084
- }
1085
- await this.save();
1086
- return environment;
1087
- }
1088
- async update(input) {
1089
- await this.load();
1090
- const existing = this.environments.get(input.id);
1091
- if (!existing) return null;
1092
- const updated = {
1093
- ...existing,
1094
- // Immutable environments: allow changing variables, color, description — but not name
1095
- ...!existing.immutable && input.name !== void 0 && { name: input.name },
1096
- ...input.description !== void 0 && { description: input.description },
1097
- ...input.parentIds !== void 0 && { parentIds: input.parentIds },
1098
- ...input.variables !== void 0 && { variables: input.variables },
1099
- ...input.scope !== void 0 && { scope: input.scope },
1100
- ...input.reference !== void 0 && { reference: input.reference },
1101
- ...input.activateOn !== void 0 && { activateOn: input.activateOn },
1102
- ...input.color !== void 0 && { color: input.color },
1103
- ...input.bgColor !== void 0 && { bgColor: input.bgColor },
1104
- ...input.default !== void 0 && { default: input.default },
1105
- ...input.agentId !== void 0 && { agentId: input.agentId ?? void 0 }
1106
- };
1107
- if (updated.default) {
1108
- for (const [id, e] of this.environments) {
1109
- if (id !== input.id && e.projectId === updated.projectId && e.default) {
1110
- this.environments.set(id, { ...e, default: false });
1111
- }
1112
- }
1113
- }
1114
- this.environments.set(input.id, updated);
1115
- await this.save();
1116
- return updated;
1117
- }
1118
- async upsertVariable(environmentId, variableId, variable) {
1119
- const existing = this.environments.get(environmentId);
1120
- if (!existing) return null;
1121
- const variables = { ...existing.variables, [variableId]: variable };
1122
- return this.update({ id: environmentId, variables });
1123
- }
1124
- async removeVariable(environmentId, variableId) {
1125
- const existing = this.environments.get(environmentId);
1126
- if (!existing) return null;
1127
- const variables = { ...existing.variables };
1128
- delete variables[variableId];
1129
- return this.update({ id: environmentId, variables });
1130
- }
1131
- async delete(id) {
1132
- await this.load();
1133
- const env2 = this.environments.get(id);
1134
- if (!env2) return false;
1135
- if (env2.immutable) return false;
1136
- this.environments.delete(id);
1137
- recordLocalRemoval("environments", id);
1138
- await this.save();
1139
- return true;
1140
- }
1141
- async getByProjectId(projectId, orgId) {
1142
- await this.load();
1143
- return this.ofProject(projectId, orgId);
1144
- }
1145
- async listAllGlobal() {
1146
- await this.load();
1147
- return Array.from(this.environments.values()).filter((e) => e.scope === "global");
1148
- }
1149
- // --- Scope-based Query Methods ---
1150
- async listByScope(projectId, scope, orgId) {
1151
- await this.load();
1152
- return this.ofProject(projectId, orgId).filter((e) => e.scope === scope);
1153
- }
1154
- /** Les environnements de service qui s'activent avec un environnement global. */
1155
- async listByActivateOn(globalEnvId) {
1156
- await this.load();
1157
- return Array.from(this.environments.values()).filter((e) => e.scope === "service" && e.activateOn === globalEnvId);
1158
- }
1159
- async findServiceEnvironment(serviceId, globalEnvId) {
1160
- await this.load();
1161
- for (const e of this.environments.values()) {
1162
- if (e.scope === "service" && e.reference === serviceId && e.activateOn === globalEnvId) return e;
1163
- }
1164
- return null;
1165
- }
1166
- async findServiceEnvironments(serviceId) {
1167
- await this.load();
1168
- return Array.from(this.environments.values()).filter((e) => e.scope === "service" && e.reference === serviceId);
1169
- }
1170
- async deleteByReference(serviceId) {
1171
- await this.load();
1172
- let changed = false;
1173
- for (const [id, e] of this.environments) {
1174
- if (e.scope === "service" && e.reference === serviceId) {
1175
- this.environments.delete(id);
1176
- recordLocalRemoval("environments", id);
1177
- changed = true;
1178
- }
1179
- }
1180
- if (changed) await this.save();
1181
- }
1182
- async deleteByActivateOn(globalEnvId) {
1183
- await this.load();
1184
- let changed = false;
1185
- for (const [id, e] of this.environments) {
1186
- if (e.scope === "service" && e.activateOn === globalEnvId) {
1187
- this.environments.delete(id);
1188
- recordLocalRemoval("environments", id);
1189
- changed = true;
1190
- }
1191
- }
1192
- if (changed) await this.save();
1193
- }
1194
- /**
1195
- * When a service is renamed, update the name of all its service-scoped environments
1196
- * to keep the `${serviceName} (${globalEnvName})` pattern in sync.
1197
- */
1198
- async renameByReference(serviceId, newServiceName) {
1199
- await this.load();
1200
- let changed = false;
1201
- for (const [id, e] of this.environments) {
1202
- if (e.scope === "service" && e.reference === serviceId && e.activateOn) {
1203
- const globalEnv = this.environments.get(e.activateOn);
1204
- if (!globalEnv) continue;
1205
- this.environments.set(id, { ...e, name: `${newServiceName} (${globalEnv.name})` });
1206
- changed = true;
1207
- }
1208
- }
1209
- if (changed) await this.save();
1210
- }
1211
- /**
1212
- * When a global environment is renamed, update the name of all service-scoped
1213
- * environments that activateOn it to keep the naming pattern in sync.
1214
- * Caller provides a serviceId→name map since environmentStore can't access serviceStore.
1215
- */
1216
- async renameByActivateOn(globalEnvId, newGlobalName, serviceNames) {
1217
- await this.load();
1218
- let changed = false;
1219
- for (const [id, e] of this.environments) {
1220
- if (e.scope === "service" && e.activateOn === globalEnvId && e.reference) {
1221
- const serviceName = serviceNames.get(e.reference);
1222
- if (!serviceName) continue;
1223
- this.environments.set(id, { ...e, name: `${serviceName} (${newGlobalName})` });
1224
- changed = true;
1225
- }
1226
- }
1227
- if (changed) await this.save();
1228
- }
1229
- // ---- Cloud sync ------------------------------------------------------
1230
- /** Environments of the given projects, in their on-disk (encrypted) form. */
1231
- async listEncryptedByProjectIds(projectIds) {
1232
- await this.load();
1233
- const wanted = new Set(projectIds);
1234
- return Array.from(this.environments.values()).filter((e) => wanted.has(e.projectId)).map((e) => this.encryptEnvironment(e));
1235
- }
1236
- /**
1237
- * Apply an environment received from the cloud, in its on-disk form.
1238
- *
1239
- * Validated rather than trusted, and diff-guarded against the on-disk form so
1240
- * re-applying an identical environment neither saves nor notifies (loop-safe).
1241
- */
1242
- async applyRemoteUpsert(id, fields, vault) {
1243
- await this.load();
1244
- if (vault) this.remoteOrgs.set(id, vault);
1245
- const parsed = EnvironmentSchema.safeParse({ ...fields, id });
1246
- if (!parsed.success) {
1247
- console.error("[environment-store] Rejected remote environment %s:", id, parsed.error.message);
1248
- return { created: false };
1249
- }
1250
- const incoming = parsed.data;
1251
- const existing = this.environments.get(id);
1252
- if (existing && JSON.stringify(this.encryptEnvironment(existing, vault)) === JSON.stringify(incoming)) {
1253
- return { created: false };
1254
- }
1255
- this.diskEncryptedSlots.set(id, this.extractEncryptedCache(incoming));
1256
- this.environments.set(id, this.decryptEnvironment(incoming, vault));
1257
- await this.save();
1258
- return { created: !existing };
1259
- }
1260
- /** Delete an environment that was removed on another peer. */
1261
- async applyRemoteDelete(id) {
1262
- await this.load();
1263
- if (!this.environments.has(id)) return false;
1264
- this.environments.delete(id);
1265
- this.diskEncryptedSlots.delete(id);
1266
- this.unreadableSecrets.delete(id);
1267
- this.remoteOrgs.delete(id);
1268
- await this.save();
1269
- return true;
1270
- }
1271
- };
1272
- var environmentStore = new EnvironmentStore();
1273
-
1274
- // ../server/src/services/service-store.ts
1275
- var FILENAME3 = "services.json";
1276
- function encryptLogSources(logSources, previous, vault) {
1277
- return logSources.map((src, i) => {
1278
- const prev = previous?.[i];
1279
- if (src.type === "mongo") {
1280
- if (isEncryptedValue(src.connectionString)) return src;
1281
- const was = prev?.type === "mongo" ? prev.connectionString : void 0;
1282
- return { ...src, connectionString: stableEncryptValue(src.connectionString, was, vault) };
1283
- }
1284
- if (src.type === "mysql" && src.password) {
1285
- if (isEncryptedValue(src.password)) return src;
1286
- const was = prev?.type === "mysql" ? prev.password : void 0;
1287
- return { ...src, password: stableEncryptValue(src.password, was, vault) };
1288
- }
1289
- return src;
1290
- });
1291
- }
1292
- function decryptLogSources(logSources, vault) {
1293
- return logSources.map((src) => {
1294
- if (src.type === "mongo" && isEncryptedValue(src.connectionString)) {
1295
- try {
1296
- return { ...src, connectionString: decryptValue(src.connectionString, vault) };
1297
- } catch {
1298
- console.error("[service-store] Failed to decrypt mongo connectionString");
1299
- return src;
1300
- }
1301
- }
1302
- if (src.type === "mysql" && src.password && isEncryptedValue(src.password)) {
1303
- try {
1304
- return { ...src, password: decryptValue(src.password, vault) };
1305
- } catch {
1306
- console.error("[service-store] Failed to decrypt mysql password");
1307
- return src;
1308
- }
1309
- }
1310
- return src;
1311
- });
1312
- }
1313
- function prepareForDisk(service, previous, vault) {
1314
- return {
1315
- ...service,
1316
- env: {},
1317
- envSecrets: [],
1318
- resolvedLogParsers: void 0,
1319
- resolvedCwd: void 0,
1320
- logSources: encryptLogSources(service.logSources ?? [], previous?.logSources, vault)
1321
- };
1322
- }
1323
- var ServiceStore = class {
1324
- services = /* @__PURE__ */ new Map();
1325
- loaded = false;
1326
- /**
1327
- * L'état du disque au dernier chargement — voir `ProjectStore.fingerprint` :
1328
- * plusieurs Runeya partagent la racine machine, et un store chargé une fois
1329
- * pour toutes ne verrait jamais les services créés par l'autre.
1330
- */
1331
- fingerprint = "";
1332
- disk = new StoreDiskCoordinator();
1333
- listeners = /* @__PURE__ */ new Set();
1334
- /**
1335
- * Last on-disk (encrypted) form per service id, so `prepareForDisk` can reuse
1336
- * existing ciphertext instead of re-encrypting unchanged credentials.
1337
- */
1338
- diskForm = /* @__PURE__ */ new Map();
1339
- onChange(fn) {
1340
- this.listeners.add(fn);
1341
- return () => this.listeners.delete(fn);
1342
- }
1343
- notify() {
1344
- for (const fn of this.listeners) fn();
1345
- }
1346
- /** Which vault each service was read from, used until its project is known. */
1347
- roots = /* @__PURE__ */ new Map();
1348
- /**
1349
- * L'organisation d'où vient un service reçu du cloud.
1350
- *
1351
- * Le coffre ne peut pas encore répondre : les contenus arrivent avant leur
1352
- * projet, donc `rootFor` désignerait le répertoire de lancement et
1353
- * chiffrerait les secrets de l'organisation avec la clé de la machine. Cette
1354
- * provenance tient le temps que le projet devienne local — après quoi elle
1355
- * dit la même chose que le coffre.
1356
- */
1357
- remoteOrgs = /* @__PURE__ */ new Map();
1358
- /**
1359
- * Oublier ce qui a été lu, pour tout relire sous la clé courante.
1360
- *
1361
- * Appelé après l'adoption d'une clé d'organisation : le disque vient d'être
1362
- * re-chiffré sous les pieds du store, si bien que sa mémoire décrit un état
1363
- * qui n'existe plus. La laisser en place n'affichait pas seulement des
1364
- * comptes périmés — la sauvegarde suivante réécrivait l'ancien chiffré
1365
- * par-dessus le nouveau, et défaisait l'adoption.
1366
- */
1367
- resetCache() {
1368
- this.services.clear();
1369
- this.diskForm.clear();
1370
- this.roots.clear();
1371
- this.remoteOrgs.clear();
1372
- this.loaded = false;
1373
- this.notify();
1374
- }
1375
- getFilePath(root) {
1376
- return join3(root, FILENAME3);
1377
- }
1378
- /**
1379
- * The vault a service must be written to: the one of the project owning it.
1380
- *
1381
- * A service carries no project id — ownership lives in `project.serviceIds` —
1382
- * so the registry answers, falling back to where the service was read and
1383
- * finally to the local vault.
1384
- */
1385
- /** La clé sous laquelle chiffrer ce service : son organisation, ou son coffre. */
1386
- vaultOf(serviceId) {
1387
- return this.remoteOrgs.get(serviceId) ?? vaultIdOfRoot(this.rootFor(serviceId));
1388
- }
1389
- rootFor(serviceId) {
1390
- return rootOfService(serviceId, this.roots.get(serviceId) ?? null) ?? localVaultRoot();
1391
- }
1392
- /** Ce que le disque porte : un fichier de services par coffre. */
1393
- async diskFingerprint() {
1394
- return fingerprintFiles((await allVaultRoots()).map((root) => this.getFilePath(root)));
1395
- }
1396
- /**
1397
- * Read every vault: the launch directory and each materialized cloud project.
1398
- *
1399
- * Roots come from the disk, not from the project store, so services load
1400
- * without waiting on projects.
1401
- */
1402
- async load() {
1403
- if (!getEncryptionKey()) return;
1404
- await this.disk.load(async () => {
1405
- const fingerprint = await this.diskFingerprint();
1406
- if (this.loaded && fingerprint === this.fingerprint) return null;
1407
- await ensureProjectsLoaded();
1408
- const next = await this.readVaults();
1409
- return () => {
1410
- this.services = next.services;
1411
- this.diskForm = next.diskForm;
1412
- this.roots = next.roots;
1413
- this.fingerprint = fingerprint;
1414
- this.loaded = true;
1415
- if (this.services.size > 0) this.notify();
1416
- };
1417
- });
1418
- }
1419
- async readVaults() {
1420
- const services = /* @__PURE__ */ new Map();
1421
- const diskForm = /* @__PURE__ */ new Map();
1422
- const roots = /* @__PURE__ */ new Map();
1423
- for (const root of await allVaultRoots()) {
1424
- try {
1425
- const raw = await readFile3(this.getFilePath(root), "utf-8");
1426
- const data = JSON.parse(raw);
1427
- const clash = data.find((s) => isVaultClash(root, roots.get(s.id)));
1428
- if (clash) {
1429
- markVaultConflict(root, `service ${clash.id}`);
1430
- continue;
1431
- }
1432
- for (const s of data) {
1433
- if (s.commands) {
1434
- for (const cmd of s.commands) {
1435
- if (cmd.shell === void 0) cmd.shell = true;
1436
- }
1437
- }
1438
- diskForm.set(s.id, s);
1439
- services.set(s.id, {
1440
- ...s,
1441
- logSources: decryptLogSources(s.logSources ?? [], vaultIdOfRoot(root))
1442
- });
1443
- roots.set(s.id, root);
1444
- }
1445
- } catch {
1446
- }
1447
- }
1448
- return { services, diskForm, roots };
1449
- }
1450
- /**
1451
- * Write each vault that holds — or held — a service.
1452
- *
1453
- * A vault emptied by this save loses its file: a service that moved to
1454
- * another vault must not survive in the one it left, and a `[]` left behind
1455
- * would keep (or recreate) a `.runeya` in a directory that holds nothing.
1456
- */
1457
- save() {
1458
- return this.disk.save(() => this.writeVaults());
1459
- }
1460
- async writeVaults() {
1461
- const byRoot = /* @__PURE__ */ new Map();
1462
- for (const root of this.roots.values()) {
1463
- if (!isVaultConflicted(root)) byRoot.set(root, []);
1464
- }
1465
- byRoot.set(localVaultRoot(), byRoot.get(localVaultRoot()) ?? []);
1466
- for (const service of this.services.values()) {
1467
- const root = this.rootFor(service.id);
1468
- const prepared = prepareForDisk(service, this.diskForm.get(service.id), this.vaultOf(service.id));
1469
- this.diskForm.set(prepared.id, prepared);
1470
- this.roots.set(service.id, root);
1471
- if (isVaultConflicted(root)) continue;
1472
- const bucket = byRoot.get(root);
1473
- if (bucket) bucket.push(prepared);
1474
- else byRoot.set(root, [prepared]);
1475
- }
1476
- for (const [root, services] of byRoot) {
1477
- const filePath = this.getFilePath(root);
1478
- if (services.length === 0) {
1479
- await rm2(filePath, { force: true }).catch(() => {
1480
- });
1481
- continue;
1482
- }
1483
- await atomicWriteSecure(filePath, JSON.stringify(services, null, 2));
1484
- }
1485
- this.fingerprint = await this.diskFingerprint();
1486
- }
1487
- /**
1488
- * Drop services from memory, without touching the disk.
1489
- *
1490
- * Used when their project stops being followed: its vault goes away whole,
1491
- * and services left in memory would be written back on the next save — into
1492
- * the local vault, since their owner is no longer known.
1493
- */
1494
- async forgetLocally(serviceIds) {
1495
- await this.load();
1496
- for (const id of serviceIds) {
1497
- this.services.delete(id);
1498
- this.diskForm.delete(id);
1499
- this.roots.delete(id);
1500
- this.remoteOrgs.delete(id);
1501
- }
1502
- this.notify();
1503
- }
1504
- /**
1505
- * Rewrite every vault, moving entities that now belong elsewhere.
1506
- *
1507
- * Called once the project store knows where projects live: entities read from
1508
- * the launch directory whose project turns out to be a cloud one are written
1509
- * into that project's vault, and disappear from the directory they came from.
1510
- */
1511
- async relocateAll() {
1512
- await this.load();
1513
- await this.save();
1514
- }
1515
- /**
1516
- * Réattribuer l'organisation de services dont le projet vient de changer de
1517
- * coffre.
1518
- *
1519
- * Un service porte son `orgId` en propre : laissé à `null` après le passage
1520
- * du projet dans une organisation, il partirait dans le doc en se disant
1521
- * local. La sauvegarde qui suit les écrit dans leur nouveau coffre et les
1522
- * retire de l'ancien, le registre ayant déjà été mis à jour par le projet.
1523
- */
1524
- async reassignOrg(serviceIds, orgId) {
1525
- await this.load();
1526
- for (const id of serviceIds) {
1527
- const service = this.services.get(id);
1528
- if (service) this.services.set(id, { ...service, orgId });
1529
- }
1530
- await this.save();
1531
- this.notify();
1532
- }
1533
- async list() {
1534
- await this.load();
1535
- return Array.from(this.services.values());
1536
- }
1537
- async get(id) {
1538
- await this.load();
1539
- return this.services.get(id) ?? null;
1540
- }
1541
- /**
1542
- * Service in its on-disk (sanitized) form: transient runtime env and plaintext
1543
- * secrets stripped. Used when exporting a project to the cloud so that no
1544
- * secret ever leaves the machine.
1545
- */
1546
- async getSanitized(id) {
1547
- await this.load();
1548
- const service = this.services.get(id);
1549
- return service ? prepareForDisk(service, this.diskForm.get(id), this.vaultOf(id)) : null;
1550
- }
1551
- /**
1552
- * `project` : le projet qui va l'adopter, quand on le connaît. Le service est
1553
- * alors écrit d'emblée dans son coffre — sans lui, il naissait dans le coffre
1554
- * de lancement et y laissait une copie une fois rattaché.
1555
- */
1556
- async create(input, project) {
1557
- await this.load();
1558
- const service = {
1559
- id: randomUUID3(),
1560
- orgId: input.orgId ?? null,
1561
- agentId: input.agentId,
1562
- name: input.name,
1563
- commands: input.commands ?? [],
1564
- cwd: input.cwd,
1565
- ports: input.ports ?? [],
1566
- groups: input.groups ?? [],
1567
- description: input.description,
1568
- docsPath: input.docsPath,
1569
- url: input.url,
1570
- localUrls: input.localUrls ?? [],
1571
- openapiUrl: input.openapiUrl,
1572
- git: input.git,
1573
- meta: input.meta,
1574
- env: {},
1575
- envSecrets: [],
1576
- runner: input.runner ?? "native",
1577
- dockerConfig: input.dockerConfig,
1578
- healthCheck: input.healthCheck,
1579
- parserIds: input.parserIds ?? [],
1580
- shortcuts: input.shortcuts ?? [],
1581
- logSources: input.logSources ?? [],
1582
- autoRestart: input.autoRestart ?? false,
1583
- restartBackoffMs: input.restartBackoffMs ?? 1e3,
1584
- ...input.restartStrategy !== void 0 && { restartStrategy: input.restartStrategy },
1585
- ...input.maxRestarts !== void 0 && { maxRestarts: input.maxRestarts },
1586
- clearLogsOnStart: input.clearLogsOnStart ?? false
1587
- };
1588
- this.services.set(service.id, service);
1589
- if (project) this.roots.set(service.id, rootOfProjectRef(project));
1590
- await this.save();
1591
- this.notify();
1592
- return service;
1593
- }
1594
- async update(input) {
1595
- await this.load();
1596
- const existing = this.services.get(input.id);
1597
- if (!existing) return null;
1598
- const updated = {
1599
- ...existing,
1600
- ...input.agentId !== void 0 && { agentId: input.agentId ?? void 0 },
1601
- ...input.name !== void 0 && { name: input.name },
1602
- ...input.commands !== void 0 && { commands: input.commands },
1603
- ...input.cwd !== void 0 && { cwd: input.cwd ?? void 0 },
1604
- ...input.ports !== void 0 && { ports: input.ports },
1605
- ...input.groups !== void 0 && { groups: input.groups },
1606
- ...input.description !== void 0 && { description: input.description },
1607
- ...input.docsPath !== void 0 && { docsPath: input.docsPath },
1608
- ...input.url !== void 0 && { url: input.url },
1609
- ...input.localUrls !== void 0 && { localUrls: input.localUrls },
1610
- ...input.openapiUrl !== void 0 && { openapiUrl: input.openapiUrl },
1611
- ...input.git !== void 0 && { git: input.git ?? void 0 },
1612
- ...input.meta !== void 0 && { meta: input.meta ?? void 0 },
1613
- ...input.runner !== void 0 && { runner: input.runner },
1614
- ...input.dockerConfig !== void 0 && { dockerConfig: input.dockerConfig },
1615
- ...input.healthCheck !== void 0 && { healthCheck: input.healthCheck ?? void 0 },
1616
- ...input.autoRestart !== void 0 && { autoRestart: input.autoRestart },
1617
- ...input.clearLogsOnStart !== void 0 && { clearLogsOnStart: input.clearLogsOnStart },
1618
- ...input.restartBackoffMs !== void 0 && { restartBackoffMs: input.restartBackoffMs },
1619
- ...input.restartStrategy !== void 0 && { restartStrategy: input.restartStrategy },
1620
- ...input.maxRestarts !== void 0 && { maxRestarts: input.maxRestarts },
1621
- ...input.orgId !== void 0 && { orgId: input.orgId },
1622
- ...input.shortcuts !== void 0 && { shortcuts: input.shortcuts },
1623
- ...input.parserIds !== void 0 && { parserIds: input.parserIds },
1624
- ...input.logSources !== void 0 && { logSources: input.logSources }
1625
- };
1626
- this.services.set(input.id, updated);
1627
- await this.save();
1628
- this.notify();
1629
- return updated;
1630
- }
1631
- async delete(id) {
1632
- await this.load();
1633
- if (!this.services.has(id)) return false;
1634
- this.services.delete(id);
1635
- recordLocalRemoval("services", id);
1636
- await this.save();
1637
- this.notify();
1638
- await environmentStore.deleteByReference(id);
1639
- return true;
1640
- }
1641
- // ---- Cloud sync ------------------------------------------------------
1642
- /** The services of a project, in their on-disk form (what the cloud mirrors). */
1643
- async listSanitizedByIds(ids) {
1644
- await this.load();
1645
- const out = [];
1646
- for (const id of ids) {
1647
- const service = this.services.get(id);
1648
- if (service) out.push(prepareForDisk(service, this.diskForm.get(id), this.vaultOf(id)));
1649
- }
1650
- return out;
1651
- }
1652
- /**
1653
- * Apply a service received from the cloud, in its on-disk form.
1654
- *
1655
- * Validated rather than trusted: a peer running a different version — or a
1656
- * tampered doc — must not be able to write arbitrary shapes into
1657
- * `services.json`. Diff-guarded against the on-disk form so re-applying an
1658
- * identical service neither saves nor notifies (loop-safe).
1659
- */
1660
- async applyRemoteUpsert(id, fields, vault, projectId) {
1661
- await this.load();
1662
- if (vault) this.remoteOrgs.set(id, vault);
1663
- if (vault && projectId && !this.roots.has(id)) this.roots.set(id, projectVaultRoot(vault, projectId));
1664
- const parsed = ServiceConfigSchema.safeParse({ ...fields, id });
1665
- if (!parsed.success) {
1666
- console.error("[service-store] Rejected remote service %s:", id, parsed.error.message);
1667
- return { created: false };
1668
- }
1669
- const incoming = parsed.data;
1670
- const existing = this.services.get(id);
1671
- if (existing) {
1672
- const currentDisk = prepareForDisk(existing, this.diskForm.get(id), vault ?? this.vaultOf(id));
1673
- if (JSON.stringify(currentDisk) === JSON.stringify(incoming)) return { created: false };
1674
- }
1675
- this.diskForm.set(id, incoming);
1676
- this.services.set(id, {
1677
- ...incoming,
1678
- // The doc carries the on-disk form, where credentials are ciphertext;
1679
- // in memory they are expected in the clear, as `load()` keeps them.
1680
- logSources: decryptLogSources(incoming.logSources ?? [], vault ?? this.vaultOf(id))
1681
- });
1682
- await this.save();
1683
- this.notify();
1684
- return { created: !existing };
1685
- }
1686
- /** Delete a service that was removed on another peer. */
1687
- async applyRemoteDelete(id) {
1688
- await this.load();
1689
- if (!this.services.has(id)) return false;
1690
- this.services.delete(id);
1691
- this.diskForm.delete(id);
1692
- await this.save();
1693
- this.notify();
1694
- await environmentStore.deleteByReference(id);
1695
- return true;
1696
- }
1697
- };
1698
- var serviceStore = new ServiceStore();
1699
-
1700
291
  // ../server/src/services/agent-spawner.ts
1701
292
  import { spawn as spawn2 } from "child_process";
1702
293
  import { createInterface } from "readline";
1703
294
  import { watch } from "fs";
1704
- import { dirname as dirname2, join as join5 } from "path";
295
+ import { dirname as dirname2, join as join3 } from "path";
1705
296
  import { fileURLToPath } from "url";
1706
297
 
1707
298
  // ../server/src/services/instance-registry.ts
1708
- import { mkdir as mkdir2, readdir, readFile as readFile4, writeFile as writeFile2, rm as rm3 } from "fs/promises";
299
+ import { mkdir as mkdir2, readdir, readFile as readFile2, writeFile as writeFile2, rm } from "fs/promises";
1709
300
  import { readFileSync } from "fs";
1710
301
  import dotenv from "dotenv";
1711
302
  import { spawn } from "child_process";
1712
- import { join as join4 } from "path";
303
+ import { join as join2 } from "path";
1713
304
  import { z } from "zod";
1714
305
  var InstanceFileSchema = z.object({
1715
306
  pid: z.number().int().positive(),
@@ -1753,10 +344,10 @@ var InstanceFileSchema = z.object({
1753
344
  needsSecretEnv: z.boolean().optional()
1754
345
  });
1755
346
  function registryDir() {
1756
- return join4(machineRoot(), "instances");
347
+ return join2(machineRoot(), "instances");
1757
348
  }
1758
349
  function fileFor(pid) {
1759
- return join4(registryDir(), `${pid}.json`);
350
+ return join2(registryDir(), `${pid}.json`);
1760
351
  }
1761
352
  var published = null;
1762
353
  var SECRET_ENV_KEYS = ["RUNEYA_ENCRYPTION_KEY", "RUNEYA_MASTER_PASSWORD"];
@@ -1776,7 +367,7 @@ function secretsAreReproducible() {
1776
367
  if (present.length === 0) return true;
1777
368
  let fromFile;
1778
369
  try {
1779
- fromFile = dotenv.parse(readFileSync(join4(process.cwd(), ".env")));
370
+ fromFile = dotenv.parse(readFileSync(join2(process.cwd(), ".env")));
1780
371
  } catch {
1781
372
  return false;
1782
373
  }
@@ -1810,7 +401,7 @@ async function registerInstance() {
1810
401
  }
1811
402
  }
1812
403
  async function unregisterInstance() {
1813
- await rm3(fileFor(process.pid), { force: true }).catch(() => {
404
+ await rm(fileFor(process.pid), { force: true }).catch(() => {
1814
405
  });
1815
406
  }
1816
407
  async function setInstanceAgentPid(agentPid) {
@@ -1851,12 +442,12 @@ async function listInstances() {
1851
442
  }
1852
443
  const found = await Promise.all(
1853
444
  files.filter((f) => f.endsWith(".json")).map(async (name) => {
1854
- const path = join4(registryDir(), name);
445
+ const path = join2(registryDir(), name);
1855
446
  let record;
1856
447
  try {
1857
- const parsed = InstanceFileSchema.safeParse(JSON.parse(await readFile4(path, "utf8")));
448
+ const parsed = InstanceFileSchema.safeParse(JSON.parse(await readFile2(path, "utf8")));
1858
449
  if (!parsed.success) {
1859
- await rm3(path, { force: true }).catch(() => {
450
+ await rm(path, { force: true }).catch(() => {
1860
451
  });
1861
452
  return null;
1862
453
  }
@@ -1865,13 +456,13 @@ async function listInstances() {
1865
456
  return null;
1866
457
  }
1867
458
  if (!isProcessAlive(record.pid)) {
1868
- await rm3(path, { force: true }).catch(() => {
459
+ await rm(path, { force: true }).catch(() => {
1869
460
  });
1870
461
  return null;
1871
462
  }
1872
463
  const { ok, uptimeMs, version } = await probe(record.port);
1873
464
  if (!ok) {
1874
- await rm3(path, { force: true }).catch(() => {
465
+ await rm(path, { force: true }).catch(() => {
1875
466
  });
1876
467
  return null;
1877
468
  }
@@ -1939,7 +530,7 @@ async function killInstance(pid) {
1939
530
  const agent = target.agentPid ? await terminate(target.agentPid) : "absent";
1940
531
  const server = await terminate(pid);
1941
532
  if (server === "terminated" || server === "killed") {
1942
- await rm3(fileFor(pid), { force: true }).catch(() => {
533
+ await rm(fileFor(pid), { force: true }).catch(() => {
1943
534
  });
1944
535
  }
1945
536
  return { agent, server };
@@ -2169,7 +760,7 @@ var AgentSpawner = class {
2169
760
  }
2170
761
  console.log(`[agent-spawner] Spawning local agent on ${agentUrl}...`);
2171
762
  const isDev = process.env.NODE_ENV === "development";
2172
- this.agentDistPath = env.AGENT_BINARY_PATH || join5(dirname2(fileURLToPath(import.meta.url)), "..", "..", "agent", "dist", "index.js");
763
+ this.agentDistPath = env.AGENT_BINARY_PATH || join3(dirname2(fileURLToPath(import.meta.url)), "..", "..", "agent", "dist", "index.js");
2173
764
  this.agentProcess = spawn2("node", [this.agentDistPath], {
2174
765
  env: agentEnv,
2175
766
  stdio: ["ignore", "pipe", "pipe"],
@@ -2279,7 +870,7 @@ var AgentSpawner = class {
2279
870
  this.fileWatcher.close();
2280
871
  }
2281
872
  console.log("[agent-spawner] \u{1F50D} Watching agent dist for changes (hot reload enabled)");
2282
- const distDir = join5(this.agentDistPath, "..");
873
+ const distDir = join3(this.agentDistPath, "..");
2283
874
  this.fileWatcher = watch(distDir, { persistent: false }, (eventType, filename) => {
2284
875
  if ((eventType === "change" || eventType === "rename") && filename === "index.js" && !this.isRestarting) {
2285
876
  this.isRestarting = true;
@@ -3064,49 +1655,6 @@ var AgentManager = class extends EventEmitter2 {
3064
1655
  var agentManager = new AgentManager();
3065
1656
 
3066
1657
  export {
3067
- isCiMode,
3068
- loadFromEnv,
3069
- setPendingLegacyKey,
3070
- getPendingLegacyKey,
3071
- clearPendingLegacyKey,
3072
- hasPendingLegacyKey,
3073
- setEncryptionKeyInMemory,
3074
- onVaultUnlocked,
3075
- clearEncryptionKey,
3076
- hasEncryptionKey,
3077
- getEncryptionKey,
3078
- markVaultLocked,
3079
- isVaultLocked,
3080
- clearVaultLock,
3081
- ensureEncryptionKey,
3082
- setEncryptionKey,
3083
- encryptValue,
3084
- decryptValue,
3085
- stableEncryptValue,
3086
- encryptVariableSlots,
3087
- decryptVariableSlots,
3088
- extractEncryptedVariableSlots,
3089
- isEncryptedValue,
3090
- StoreDiskCoordinator,
3091
- projectKey,
3092
- AmbiguousProjectError,
3093
- registerProjectLoader,
3094
- ensureProjectsLoaded,
3095
- setProjectVault,
3096
- forgetProject,
3097
- setProjectServices,
3098
- rootOfProjectRef,
3099
- refsOfProjectId,
3100
- rootOfProjectAmong,
3101
- refOfProjectAmong,
3102
- recordLocalRemoval,
3103
- isLocallyRemoved,
3104
- clearLocalRemoval,
3105
- recordDetached,
3106
- isDetached,
3107
- clearDetached,
3108
- environmentStore,
3109
- serviceStore,
3110
1658
  agentStore,
3111
1659
  registerInstance,
3112
1660
  unregisterInstance,
@@ -3118,4 +1666,4 @@ export {
3118
1666
  traefikManager,
3119
1667
  agentManager
3120
1668
  };
3121
- //# sourceMappingURL=chunk-QF56Z6LI.js.map
1669
+ //# sourceMappingURL=chunk-5XG3X2TM.js.map