@runeya/runeya 2.0.146 → 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.
- package/agent/index.js +10 -4
- package/agent/index.js.map +1 -1
- package/{agent-manager-R7BPQO6D.js → agent-manager-46C4NIQ2.js} +4 -2
- package/{chat-LPML22J5.js → chat-CMNDMXIP.js} +9 -5
- package/{chunk-SEPAURTQ.js → chunk-2NE52CL5.js} +156 -923
- package/chunk-2NE52CL5.js.map +1 -0
- package/chunk-4D3GCBPO.js +462 -0
- package/chunk-4D3GCBPO.js.map +1 -0
- package/{chunk-KV4GJKXU.js → chunk-5XG3X2TM.js} +27 -1479
- package/chunk-5XG3X2TM.js.map +1 -0
- package/chunk-7QXP4FXD.js +969 -0
- package/chunk-7QXP4FXD.js.map +1 -0
- package/chunk-DA2DZBTL.js +43 -0
- package/chunk-DA2DZBTL.js.map +1 -0
- package/chunk-DFPZ4MTI.js +1041 -0
- package/chunk-DFPZ4MTI.js.map +1 -0
- package/{chunk-UD42LACH.js → chunk-JNK6H5KH.js} +164 -1007
- package/chunk-JNK6H5KH.js.map +1 -0
- package/chunk-WDHLEO5S.js +809 -0
- package/chunk-WDHLEO5S.js.map +1 -0
- package/{companion-W3UMZWGH.js → companion-XKMNRIJB.js} +8 -3
- package/environment-resolver-ZDTF3MHI.js +18 -0
- package/environment-resolver-ZDTF3MHI.js.map +1 -0
- package/index.js +7 -7
- package/{install-gate-PVMGJDWE.js → install-gate-OJJEUU4E.js} +2 -2
- package/mcp-ready-TAPBAOZY.js +9 -0
- package/mcp-ready-TAPBAOZY.js.map +1 -0
- package/mcp-server/index.js +24 -0
- package/mcp-server/index.js.map +1 -1
- package/mcp-server/launch.d.ts +6 -0
- package/mcp-server/launch.js +2 -1
- package/mcp-server/launch.js.map +1 -1
- package/package.json +1 -1
- package/project-store-FLXTAD3J.js +23 -0
- package/project-store-FLXTAD3J.js.map +1 -0
- package/{src-LEDGW5KP.js → src-KWO3CKZU.js} +139 -59
- package/src-KWO3CKZU.js.map +1 -0
- package/web/assets/{AgentScalarChatInterface.vue-BYszptaB.js → AgentScalarChatInterface.vue-DKBinB_l.js} +2 -2
- package/web/assets/AgentTerminalPage-CixU7x6O.js +1 -0
- package/web/assets/AiChatModals-BvGUd-QD.css +1 -0
- package/web/assets/AiChatModals-C-hXImkO.js +4 -0
- package/web/assets/AppLayout-At9wk0sx.js +1 -0
- package/web/assets/{AppLayout-tsw5Ow0w.css → AppLayout-BlhwflMq.css} +1 -1
- package/web/assets/AuditPage-BkqbHxjY.js +1 -0
- package/web/assets/CloudAccountPage-D5C2SCS9.js +1 -0
- package/web/assets/CloudMessagingPage-Bi57GGHO.js +2 -0
- package/web/assets/DashboardPage-dEQop9ql.js +1 -0
- package/web/assets/DirectoryPickerDialog-DIMSeDhw.js +1 -0
- package/web/assets/{EditorIcon-CypGLV6w.js → EditorIcon-W3Szca4J.js} +1 -1
- package/web/assets/EnvironmentForm-C5emavNh.js +1 -0
- package/web/assets/EnvironmentFormPage-BIoy8fsW.js +1 -0
- package/web/assets/EnvironmentListPage-BDYeh7XF.js +1 -0
- package/web/assets/{FlagChip-R_T5qYpL.js → FlagChip-DPtIBCjQ.js} +1 -1
- package/web/assets/KanbanBoardPage-BlCcjBwV.js +4 -0
- package/web/assets/KanbanListPage-DbXONxfC.js +1 -0
- package/web/assets/LaunchProjectPage-Cg92hHwQ.js +1 -0
- package/web/assets/LaunchServicesPage-CO8gOB9D.js +1 -0
- package/web/assets/LaunchServicesPage-h5o3zLcy.css +1 -0
- package/web/assets/{LaunchShell-DUrSunB4.js → LaunchShell-CDb21ihZ.js} +1 -1
- package/web/assets/LogViewer-DsusKk2b.js +3 -0
- package/web/assets/OrgKeyPrompt-CX9K7r9I.js +1 -0
- package/web/assets/ProcessControls-B7hWVqiR.css +1 -0
- package/web/assets/ProcessControls-TEZutuxq.js +1 -0
- package/web/assets/ProcessDetailPage-Cj23FECq.js +1 -0
- package/web/assets/ProcessMetrics-1P6iE2Il.js +1 -0
- package/web/assets/ProjectCreateDialog-D3aMWKfw.js +1 -0
- package/web/assets/{ProjectLocationGate-DhMVdIjN.js → ProjectLocationGate-DU0Jj8Nw.js} +1 -1
- package/web/assets/ProjectPublishDialog-D7sNKei3.js +1 -0
- package/web/assets/{RandomJobTestPage-BUS2Ty6-.js → RandomJobTestPage-DQUMRHyf.js} +1 -1
- package/web/assets/{ScenarioEditorPage-vMoRt0Qn.css → ScenarioEditorPage-BSjSX4bw.css} +1 -1
- package/web/assets/ScenarioEditorPage-DvCdcY9m.js +14 -0
- package/web/assets/SendToChatDialog-BdWKjYmv.js +1 -0
- package/web/assets/ServiceControlsBar-B_J5m-Kc.js +1 -0
- package/web/assets/ServiceDetailPage-DhJcjFfY.js +2 -0
- package/web/assets/{ServiceDetailPage-COdaCZ6r.css → ServiceDetailPage-DnSf5cm9.css} +1 -1
- package/web/assets/ServiceFormPage-fp8NCHSv.js +1 -0
- package/web/assets/{SettingsPage-4pKUgKPm.css → SettingsPage-NH2fQap4.css} +1 -1
- package/web/assets/SettingsPage-s28BodWe.js +2086 -0
- package/web/assets/UnlockPage-BXXIPHW_.js +1 -0
- package/web/assets/VaultMigrationPage-BfNuKtkJ.js +1 -0
- package/web/assets/{VersionBadge-BmArk24p.js → VersionBadge-CW7czacg.js} +1 -1
- package/web/assets/{WikiPage-DADu8WUA.js → WikiPage-BiwpCM3E.js} +1 -1
- package/web/assets/WikiView-CMch_06Z.js +1 -0
- package/web/assets/{add-scalar-classes-dWEWPlFD.js → add-scalar-classes-Ckanjq1h.js} +14 -14
- package/web/assets/{agent.store-CKLAkF9a.js → agent.store-piokQ9Dt.js} +1 -1
- package/web/assets/{auth-Cg9BWTfK.js → auth-BT_jf-5e.js} +2 -2
- package/web/assets/cloud-chat.store-CDKh4tIy.js +1 -0
- package/web/assets/{cloud-client-D_zCiW0s.js → cloud-client-CzPdZLDq.js} +2 -2
- package/web/assets/{cloud-session-DOaDpZTy.js → cloud-session-BYfn6YM4.js} +2 -2
- package/web/assets/floating-ui.dom-DF3oyfHd.js +1 -0
- package/web/assets/i18n-D0FT7rCj.js +1 -0
- package/web/assets/index-3LTzgeU-.css +1 -0
- package/web/assets/index-Ci2CNnit.js +309 -0
- package/web/assets/modal-iRaP1izE.js +5 -0
- package/web/assets/notification.store-CvvuTBVz.js +1 -0
- package/web/assets/{project-launch.store-y7JBMNiE.js → project-launch.store-CTmbl8UY.js} +1 -1
- package/web/assets/{project.store-DGJr0Qr9.js → project.store-CNC6_4r4.js} +1 -1
- package/web/assets/runtime-dom.esm-bundler-CyEVRK6B.js +1 -0
- package/web/assets/scenario.store-BIFQPXgl.js +7 -0
- package/web/assets/{service.store-5jWsJxPx.js → service.store-B40U4QzI.js} +1 -1
- package/web/assets/settings-scope-CyWMCjRY.js +1 -0
- package/web/assets/settings.store-Dw6bAoWE.js +1 -0
- package/web/assets/{src-Dnwk6NpV.js → src-CrNi4Pjg.js} +3 -3
- package/web/assets/src-DIBHat7I.js +3 -0
- package/web/assets/src-DtCW_XCh.js +1 -0
- package/web/assets/{trpc-Drabzwpd.js → trpc-BAZGRZBD.js} +3 -3
- package/web/assets/{useCloudOrg-BAQEtFPl.js → useCloudOrg-EnXFBwt1.js} +1 -1
- package/web/assets/useEnvVarAutocomplete-BxJh2Lw0.js +1 -0
- package/web/assets/useProjectCatalog-BrvUGv9A.js +1 -0
- package/web/assets/{useProjectCloudOrg-CEN9-_OA.js → useProjectCloudOrg-Czxlg00k.js} +1 -1
- package/web/assets/{useServiceUrlHost-BuoPEXZX.js → useServiceUrlHost-Szz9y3rh.js} +1 -1
- package/web/assets/vue-i18n-o7yraQpR.js +3 -0
- package/web/index.html +21 -21
- package/chunk-KV4GJKXU.js.map +0 -1
- package/chunk-SEPAURTQ.js.map +0 -1
- package/chunk-UD42LACH.js.map +0 -1
- package/src-LEDGW5KP.js.map +0 -1
- package/web/assets/AgentTerminalPage-C3TJ1NL9.js +0 -1
- package/web/assets/AiChatModals-D3scRE3S.js +0 -4
- package/web/assets/AiChatModals-EfRuvId7.css +0 -1
- package/web/assets/AppLayout-BMQUjPKe.js +0 -1
- package/web/assets/AuditPage-CKcExBws.js +0 -1
- package/web/assets/CloudAccountPage-B6oPg0BZ.js +0 -1
- package/web/assets/CloudMessagingPage-C5fwVe-F.js +0 -2
- package/web/assets/DashboardPage-9RV7sx3e.js +0 -1
- package/web/assets/DirectoryPickerDialog-C06BXyfJ.js +0 -1
- package/web/assets/EnvironmentForm-CAvDoira.js +0 -1
- package/web/assets/EnvironmentFormPage-BmAxhN-m.js +0 -1
- package/web/assets/EnvironmentListPage-B8uATPBI.js +0 -1
- package/web/assets/KanbanBoardPage-CQsOkRUf.js +0 -4
- package/web/assets/KanbanListPage-CWZqkjD6.js +0 -1
- package/web/assets/LaunchProjectPage-saSmC7Pr.js +0 -1
- package/web/assets/LaunchServicesPage-BkKyLMEz.js +0 -1
- package/web/assets/LaunchServicesPage-DoLDPqQq.css +0 -1
- package/web/assets/LogViewer-CZBYkH9J.js +0 -3
- package/web/assets/OrgKeyPrompt-DzQ7pwbs.js +0 -1
- package/web/assets/ProcessControls-BEeoNlNV.js +0 -1
- package/web/assets/ProcessControls-BlpnVylq.css +0 -1
- package/web/assets/ProcessDetailPage-CQgu5uKm.js +0 -1
- package/web/assets/ProcessMetrics-Crlfkelj.js +0 -1
- package/web/assets/ProjectCreateDialog-BolrhRD-.js +0 -1
- package/web/assets/ProjectPublishDialog-DsZNWrfz.js +0 -1
- package/web/assets/ScenarioEditorPage-MIAGh2us.js +0 -14
- package/web/assets/SendToChatDialog-BxWsyv29.js +0 -1
- package/web/assets/ServiceControlsBar-DqDPAZt7.js +0 -1
- package/web/assets/ServiceDetailPage-hkSAzZ-l.js +0 -2
- package/web/assets/ServiceFormPage-DXqhqXg8.js +0 -1
- package/web/assets/SettingsPage-DS2YhMIM.js +0 -2086
- package/web/assets/UnlockPage-s8SgVcKk.js +0 -1
- package/web/assets/VaultMigrationPage-s0b44MDM.js +0 -1
- package/web/assets/WikiView-DtGwIt_Q.js +0 -1
- package/web/assets/cloud-chat.store-b-BHGz7I.js +0 -1
- package/web/assets/editor.store-DCAZSeVG.js +0 -1
- package/web/assets/floating-ui.dom-DWhsZyqT.js +0 -1
- package/web/assets/i18n-D0afQEGs.js +0 -1
- package/web/assets/index-D8VRbyoP.js +0 -309
- package/web/assets/index-DsezNbSM.css +0 -1
- package/web/assets/modal-qYgTMkYb.js +0 -5
- package/web/assets/scenario.store-YWHL2_wu.js +0 -7
- package/web/assets/settings-scope-Bv7QGPrt.js +0 -1
- package/web/assets/settings.store-bEsB7oKb.js +0 -1
- package/web/assets/src--GO-VSJ0.js +0 -5
- package/web/assets/src-C-D_y7ou.js +0 -1
- package/web/assets/useEnvVarAutocomplete-BqGTODUv.js +0 -1
- package/web/assets/useProjectCatalog-yocjIKt6.js +0 -1
- package/web/assets/vue-draggable-plus-BHsveB8w.css +0 -1
- package/web/assets/vue-draggable-plus-DX4rTBiu.js +0 -1
- /package/{agent-manager-R7BPQO6D.js.map → agent-manager-46C4NIQ2.js.map} +0 -0
- /package/{chat-LPML22J5.js.map → chat-CMNDMXIP.js.map} +0 -0
- /package/{companion-W3UMZWGH.js.map → companion-XKMNRIJB.js.map} +0 -0
- /package/{install-gate-PVMGJDWE.js.map → install-gate-OJJEUU4E.js.map} +0 -0
|
@@ -1,35 +1,28 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
} from "./chunk-OZBSOSAS.js";
|
|
2
|
+
serviceStore
|
|
3
|
+
} from "./chunk-4D3GCBPO.js";
|
|
5
4
|
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
5
|
+
decryptValue,
|
|
6
|
+
deriveStableSecret,
|
|
7
|
+
getEncryptionKey,
|
|
8
|
+
isEncryptedValue,
|
|
9
|
+
stableEncryptValue
|
|
10
|
+
} from "./chunk-DFPZ4MTI.js";
|
|
11
|
+
import {
|
|
12
|
+
renameWithRetry
|
|
12
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
|
|
20
|
+
orgVaultRoot
|
|
25
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
|
|
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
|
|
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
|
|
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
|
|
347
|
+
return join2(machineRoot(), "instances");
|
|
1757
348
|
}
|
|
1758
349
|
function fileFor(pid) {
|
|
1759
|
-
return
|
|
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(
|
|
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
|
|
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 =
|
|
445
|
+
const path = join2(registryDir(), name);
|
|
1855
446
|
let record;
|
|
1856
447
|
try {
|
|
1857
|
-
const parsed = InstanceFileSchema.safeParse(JSON.parse(await
|
|
448
|
+
const parsed = InstanceFileSchema.safeParse(JSON.parse(await readFile2(path, "utf8")));
|
|
1858
449
|
if (!parsed.success) {
|
|
1859
|
-
await
|
|
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
|
|
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
|
|
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
|
|
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 ||
|
|
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 =
|
|
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-
|
|
1669
|
+
//# sourceMappingURL=chunk-5XG3X2TM.js.map
|