@runeya/runeya 2.0.49 → 2.0.51
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-manager-KX5MSTAK.js +7 -0
- package/{chunk-4AGTVGSH.js → chunk-NVKGIBJN.js} +23 -14
- package/chunk-NVKGIBJN.js.map +1 -0
- package/index.js +1 -1
- package/package.json +1 -1
- package/{src-WVK4WUI3.js → src-R6JOPDNL.js} +378 -402
- package/src-R6JOPDNL.js.map +1 -0
- package/agent-manager-JNA5ZALK.js +0 -7
- package/chunk-4AGTVGSH.js.map +0 -1
- package/src-WVK4WUI3.js.map +0 -1
- /package/{agent-manager-JNA5ZALK.js.map → agent-manager-KX5MSTAK.js.map} +0 -0
|
@@ -84,7 +84,7 @@ import {
|
|
|
84
84
|
traefikManager,
|
|
85
85
|
vaultFor,
|
|
86
86
|
writeEntity
|
|
87
|
-
} from "./chunk-
|
|
87
|
+
} from "./chunk-NVKGIBJN.js";
|
|
88
88
|
|
|
89
89
|
// ../server/src/create-server.ts
|
|
90
90
|
import express from "express";
|
|
@@ -6324,7 +6324,7 @@ var CloudSessionStore = class {
|
|
|
6324
6324
|
};
|
|
6325
6325
|
var cloudSessionStore = new CloudSessionStore();
|
|
6326
6326
|
|
|
6327
|
-
// ../server/src/
|
|
6327
|
+
// ../server/src/services/cloud-api.ts
|
|
6328
6328
|
var DEFAULT_CLOUD_API_URL = "https://api.runeya.dev";
|
|
6329
6329
|
var DEFAULT_CLOUD_APP_URL = "https://runeya.dev";
|
|
6330
6330
|
function getCloudApiUrl() {
|
|
@@ -6333,6 +6333,31 @@ function getCloudApiUrl() {
|
|
|
6333
6333
|
function getCloudAppUrl() {
|
|
6334
6334
|
return (env.CLOUD_APP_URL ?? DEFAULT_CLOUD_APP_URL).replace(/\/+$/, "");
|
|
6335
6335
|
}
|
|
6336
|
+
var CloudNotLinkedError = class extends Error {
|
|
6337
|
+
code = "CLOUD_NOT_LINKED";
|
|
6338
|
+
constructor() {
|
|
6339
|
+
super("Not linked to the Runeya cloud.");
|
|
6340
|
+
this.name = "CloudNotLinkedError";
|
|
6341
|
+
}
|
|
6342
|
+
};
|
|
6343
|
+
async function cloudFetch(path, init = {}) {
|
|
6344
|
+
const token = await cloudSessionStore.getToken();
|
|
6345
|
+
if (!token) throw new CloudNotLinkedError();
|
|
6346
|
+
return fetch(`${getCloudApiUrl()}${path}`, {
|
|
6347
|
+
method: init.method ?? "GET",
|
|
6348
|
+
headers: {
|
|
6349
|
+
Authorization: `Bearer ${token}`,
|
|
6350
|
+
// Le cloud refuse (426) les apps trop anciennes pour son schéma courant.
|
|
6351
|
+
[APP_VERSION_HEADER]: appVersion,
|
|
6352
|
+
...init.body !== void 0 ? { "Content-Type": "application/json" } : {}
|
|
6353
|
+
},
|
|
6354
|
+
...init.body !== void 0 ? { body: JSON.stringify(init.body) } : {},
|
|
6355
|
+
// 15s suits a JSON call; attachment downloads pass a longer budget.
|
|
6356
|
+
signal: AbortSignal.timeout(init.timeoutMs ?? 15e3)
|
|
6357
|
+
});
|
|
6358
|
+
}
|
|
6359
|
+
|
|
6360
|
+
// ../server/src/trpc/routers/cloud-auth.ts
|
|
6336
6361
|
function getDeviceName() {
|
|
6337
6362
|
if (env.DEVICE_NAME) return env.DEVICE_NAME.slice(0, 64);
|
|
6338
6363
|
const host = hostname() || "runeya";
|
|
@@ -6348,23 +6373,13 @@ function isLocalOrigin(url) {
|
|
|
6348
6373
|
return false;
|
|
6349
6374
|
}
|
|
6350
6375
|
}
|
|
6351
|
-
async function
|
|
6352
|
-
|
|
6353
|
-
|
|
6354
|
-
|
|
6376
|
+
async function cloudFetch2(path, init = {}) {
|
|
6377
|
+
try {
|
|
6378
|
+
return await cloudFetch(path, init);
|
|
6379
|
+
} catch (err) {
|
|
6380
|
+
if (!(err instanceof CloudNotLinkedError)) throw err;
|
|
6381
|
+
throw new TRPCError10({ code: "PRECONDITION_FAILED", message: err.message });
|
|
6355
6382
|
}
|
|
6356
|
-
return fetch(`${getCloudApiUrl()}${path}`, {
|
|
6357
|
-
method: init.method ?? "GET",
|
|
6358
|
-
headers: {
|
|
6359
|
-
Authorization: `Bearer ${token}`,
|
|
6360
|
-
// Le cloud refuse (426) les apps trop anciennes pour son schéma courant.
|
|
6361
|
-
[APP_VERSION_HEADER]: appVersion,
|
|
6362
|
-
...init.body !== void 0 ? { "Content-Type": "application/json" } : {}
|
|
6363
|
-
},
|
|
6364
|
-
...init.body !== void 0 ? { body: JSON.stringify(init.body) } : {},
|
|
6365
|
-
// 15s suits a JSON call; attachment downloads pass a longer budget.
|
|
6366
|
-
signal: AbortSignal.timeout(init.timeoutMs ?? 15e3)
|
|
6367
|
-
});
|
|
6368
6383
|
}
|
|
6369
6384
|
var PROXY_PREFIXES = ["/organization/", "/project/", "/chat/", "/kanban/", "/user/prefs"];
|
|
6370
6385
|
function isAllowedProxyPath(path) {
|
|
@@ -6391,7 +6406,7 @@ var cloudAuthRouter = router({
|
|
|
6391
6406
|
/** The cloud session (user + org), fetched with the stored token. */
|
|
6392
6407
|
session: protectedProcedure.query(async () => {
|
|
6393
6408
|
if (!await cloudSessionStore.isLinked()) return null;
|
|
6394
|
-
const res = await
|
|
6409
|
+
const res = await cloudFetch2("/api/auth/get-session");
|
|
6395
6410
|
if (!res.ok) {
|
|
6396
6411
|
if (res.status === 401) await cloudSessionStore.clear();
|
|
6397
6412
|
return null;
|
|
@@ -6405,7 +6420,7 @@ var cloudAuthRouter = router({
|
|
|
6405
6420
|
*/
|
|
6406
6421
|
devices: protectedProcedure.query(async () => {
|
|
6407
6422
|
if (!await cloudSessionStore.isLinked()) return { devices: [] };
|
|
6408
|
-
const res = await
|
|
6423
|
+
const res = await cloudFetch2("/api/auth/device/list");
|
|
6409
6424
|
if (!res.ok) {
|
|
6410
6425
|
if (res.status === 401) await cloudSessionStore.clear();
|
|
6411
6426
|
return { devices: [] };
|
|
@@ -6420,7 +6435,7 @@ var cloudAuthRouter = router({
|
|
|
6420
6435
|
* left holding a dead credential.
|
|
6421
6436
|
*/
|
|
6422
6437
|
revokeDevice: protectedProcedure.input(z14.object({ id: z14.string().min(1).max(128) })).mutation(async ({ input }) => {
|
|
6423
|
-
const res = await
|
|
6438
|
+
const res = await cloudFetch2("/api/auth/device/revoke", { method: "POST", body: { id: input.id } });
|
|
6424
6439
|
if (!res.ok) {
|
|
6425
6440
|
if (res.status === 401) await cloudSessionStore.clear();
|
|
6426
6441
|
throw new TRPCError10({ code: "BAD_REQUEST", message: "Could not revoke this session." });
|
|
@@ -6447,7 +6462,7 @@ var cloudAuthRouter = router({
|
|
|
6447
6462
|
throw new TRPCError10({ code: "FORBIDDEN", message: `Path not allowed: ${input.path}` });
|
|
6448
6463
|
}
|
|
6449
6464
|
const qs = input.query ? `?${new URLSearchParams(input.query).toString()}` : "";
|
|
6450
|
-
const res = await
|
|
6465
|
+
const res = await cloudFetch2(`/api/auth${input.path}${qs}`, {
|
|
6451
6466
|
method: input.method,
|
|
6452
6467
|
...input.method === "POST" ? { body: input.body ?? {} } : {}
|
|
6453
6468
|
});
|
|
@@ -6471,7 +6486,7 @@ var cloudAuthRouter = router({
|
|
|
6471
6486
|
signOut: protectedProcedure.mutation(async () => {
|
|
6472
6487
|
if (await cloudSessionStore.isLinked()) {
|
|
6473
6488
|
try {
|
|
6474
|
-
await
|
|
6489
|
+
await cloudFetch2("/api/auth/sign-out", { method: "POST", body: {} });
|
|
6475
6490
|
} catch {
|
|
6476
6491
|
}
|
|
6477
6492
|
}
|
|
@@ -6483,7 +6498,7 @@ var cloudAuthRouter = router({
|
|
|
6483
6498
|
// ../server/src/services/kanban-store.ts
|
|
6484
6499
|
async function callCloud(path, init = {}) {
|
|
6485
6500
|
try {
|
|
6486
|
-
const res = await
|
|
6501
|
+
const res = await cloudFetch2(`/api/auth${path}`, init);
|
|
6487
6502
|
if (!res.ok) {
|
|
6488
6503
|
console.error("[kanban-store] cloud call failed: %s %s", res.status, path);
|
|
6489
6504
|
return { ok: false, data: null };
|
|
@@ -6987,7 +7002,7 @@ function tailTranscript(opts) {
|
|
|
6987
7002
|
|
|
6988
7003
|
// ../server/src/services/ai-runners/claude-pty-runner.ts
|
|
6989
7004
|
async function getAgentManager() {
|
|
6990
|
-
const { agentManager: agentManager2 } = await import("./agent-manager-
|
|
7005
|
+
const { agentManager: agentManager2 } = await import("./agent-manager-KX5MSTAK.js");
|
|
6991
7006
|
return agentManager2;
|
|
6992
7007
|
}
|
|
6993
7008
|
function buildPtyArgs(params, sessionId, opts) {
|
|
@@ -7640,7 +7655,7 @@ async function shutdownCodexAppServer() {
|
|
|
7640
7655
|
}
|
|
7641
7656
|
}
|
|
7642
7657
|
async function getAgentManager2() {
|
|
7643
|
-
const { agentManager: agentManager2 } = await import("./agent-manager-
|
|
7658
|
+
const { agentManager: agentManager2 } = await import("./agent-manager-KX5MSTAK.js");
|
|
7644
7659
|
return agentManager2;
|
|
7645
7660
|
}
|
|
7646
7661
|
function parseRuneyaCurlCommand(command) {
|
|
@@ -12061,7 +12076,7 @@ var kanbanRouter = router({
|
|
|
12061
12076
|
if (!ATTACHMENT_ID_REGEX.test(id)) {
|
|
12062
12077
|
throw new TRPCError17({ code: "BAD_REQUEST", message: "Invalid attachment id" });
|
|
12063
12078
|
}
|
|
12064
|
-
const res = await
|
|
12079
|
+
const res = await cloudFetch2(`/api/kanban/file/${id}`, { timeoutMs: 12e4 });
|
|
12065
12080
|
if (res.status === 404) {
|
|
12066
12081
|
throw new TRPCError17({ code: "NOT_FOUND", message: `Attachment ${id} not found` });
|
|
12067
12082
|
}
|
|
@@ -12463,90 +12478,239 @@ var configRouter = router({
|
|
|
12463
12478
|
import { z as z24 } from "zod";
|
|
12464
12479
|
import { TRPCError as TRPCError20 } from "@trpc/server";
|
|
12465
12480
|
|
|
12466
|
-
// ../server/src/services/
|
|
12467
|
-
import {
|
|
12468
|
-
|
|
12469
|
-
|
|
12470
|
-
|
|
12471
|
-
|
|
12472
|
-
|
|
12473
|
-
|
|
12474
|
-
|
|
12481
|
+
// ../server/src/services/org-sync-scope.ts
|
|
12482
|
+
import { access as access2 } from "fs/promises";
|
|
12483
|
+
import { join as join17 } from "path";
|
|
12484
|
+
async function isMaterialized(root) {
|
|
12485
|
+
return access2(join17(root, "project.json")).then(() => true, () => false);
|
|
12486
|
+
}
|
|
12487
|
+
async function orgsToSync() {
|
|
12488
|
+
const cloudUrl = getCloudApiUrl();
|
|
12489
|
+
const stamps = await readOrgClouds();
|
|
12490
|
+
const orgIds = /* @__PURE__ */ new Set();
|
|
12491
|
+
for (const vault of await listCloudVaults()) {
|
|
12492
|
+
if (orgIds.has(vault.orgId)) continue;
|
|
12493
|
+
const stamped = stamps[vault.orgId];
|
|
12494
|
+
if (stamped && stamped.replace(/\/+$/, "") !== cloudUrl) continue;
|
|
12495
|
+
if (await isMaterialized(vault.root)) orgIds.add(vault.orgId);
|
|
12475
12496
|
}
|
|
12476
|
-
|
|
12477
|
-
|
|
12478
|
-
|
|
12497
|
+
return Array.from(orgIds, (orgId) => ({ orgId, cloudUrl }));
|
|
12498
|
+
}
|
|
12499
|
+
|
|
12500
|
+
// ../server/src/services/master-password.ts
|
|
12501
|
+
import { randomBytes as randomBytes4, createCipheriv, createDecipheriv } from "crypto";
|
|
12502
|
+
import { readFile as readFile15, writeFile as writeFile15, chmod as chmod10, stat as stat6, rename as rename12 } from "fs/promises";
|
|
12503
|
+
import { join as join18 } from "path";
|
|
12504
|
+
import { Entry } from "@napi-rs/keyring";
|
|
12505
|
+
import { hashRaw as argon2HashRaw } from "@node-rs/argon2";
|
|
12506
|
+
var KEYRING_SERVICE = "runeya";
|
|
12507
|
+
var KEYRING_ACCOUNT = "pepper";
|
|
12508
|
+
var VERIFY_FILENAME = ".runeya-verify";
|
|
12509
|
+
var ENCRYPTED_KEY_FILENAME = ".encryption.key.enc";
|
|
12510
|
+
var PEPPER_FILENAME = ".pepper.private";
|
|
12511
|
+
var CANARY_PLAINTEXT = "runeya:v1:key-verification";
|
|
12512
|
+
var MAX_KEY_FILE_SIZE = 1024;
|
|
12513
|
+
var ARGON2_OPTIONS = { memoryCost: 65536, timeCost: 3, parallelism: 4, outputLen: 32 };
|
|
12514
|
+
async function readKeyFile(filePath) {
|
|
12515
|
+
const s = await stat6(filePath);
|
|
12516
|
+
if (s.size > MAX_KEY_FILE_SIZE) throw new Error(`Fichier suspect : ${filePath} (${s.size} octets)`);
|
|
12517
|
+
return readFile15(filePath);
|
|
12518
|
+
}
|
|
12519
|
+
function aesEncrypt(key, plaintext) {
|
|
12520
|
+
const iv = randomBytes4(12);
|
|
12521
|
+
const cipher = createCipheriv("aes-256-gcm", key, iv);
|
|
12522
|
+
const encrypted = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
12523
|
+
const authTag = cipher.getAuthTag();
|
|
12524
|
+
return Buffer.concat([iv, authTag, encrypted]);
|
|
12525
|
+
}
|
|
12526
|
+
function aesDecrypt(key, data) {
|
|
12527
|
+
const iv = data.subarray(0, 12);
|
|
12528
|
+
const authTag = data.subarray(12, 28);
|
|
12529
|
+
const ciphertext = data.subarray(28);
|
|
12530
|
+
const decipher = createDecipheriv("aes-256-gcm", key, iv, { authTagLength: 16 });
|
|
12531
|
+
decipher.setAuthTag(authTag);
|
|
12532
|
+
return Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
12533
|
+
}
|
|
12534
|
+
async function getPepperFromFile(dataDir) {
|
|
12535
|
+
try {
|
|
12536
|
+
const raw = await readKeyFile(join18(dataDir, PEPPER_FILENAME));
|
|
12537
|
+
const hex = raw.toString("utf-8").trim();
|
|
12538
|
+
if (/^[0-9a-f]{64}$/i.test(hex)) return Buffer.from(hex, "hex");
|
|
12539
|
+
} catch {
|
|
12479
12540
|
}
|
|
12480
|
-
|
|
12481
|
-
|
|
12482
|
-
|
|
12483
|
-
|
|
12484
|
-
|
|
12485
|
-
|
|
12486
|
-
|
|
12487
|
-
|
|
12488
|
-
|
|
12489
|
-
|
|
12490
|
-
|
|
12491
|
-
|
|
12541
|
+
return null;
|
|
12542
|
+
}
|
|
12543
|
+
async function writePepperToFile(dataDir, pepper) {
|
|
12544
|
+
const filePath = join18(dataDir, PEPPER_FILENAME);
|
|
12545
|
+
const tmpPath = filePath + ".tmp";
|
|
12546
|
+
await writeFile15(tmpPath, pepper.toString("hex"), "utf-8");
|
|
12547
|
+
await chmod10(tmpPath, 384);
|
|
12548
|
+
await rename12(tmpPath, filePath);
|
|
12549
|
+
}
|
|
12550
|
+
function tryKeyringGet() {
|
|
12551
|
+
try {
|
|
12552
|
+
return new Entry(KEYRING_SERVICE, KEYRING_ACCOUNT).getPassword();
|
|
12553
|
+
} catch {
|
|
12554
|
+
return null;
|
|
12492
12555
|
}
|
|
12493
|
-
|
|
12494
|
-
|
|
12495
|
-
|
|
12496
|
-
|
|
12497
|
-
|
|
12498
|
-
|
|
12499
|
-
|
|
12500
|
-
void stampOrgCloud(l.orgId, l.cloudUrl);
|
|
12501
|
-
}
|
|
12502
|
-
} catch {
|
|
12503
|
-
}
|
|
12504
|
-
this.loaded = true;
|
|
12556
|
+
}
|
|
12557
|
+
function tryKeyringSet(value) {
|
|
12558
|
+
try {
|
|
12559
|
+
new Entry(KEYRING_SERVICE, KEYRING_ACCOUNT).setPassword(value);
|
|
12560
|
+
return true;
|
|
12561
|
+
} catch {
|
|
12562
|
+
return false;
|
|
12505
12563
|
}
|
|
12506
|
-
|
|
12507
|
-
|
|
12508
|
-
|
|
12509
|
-
|
|
12510
|
-
|
|
12511
|
-
await writeFile15(tmpPath, data, "utf-8");
|
|
12512
|
-
await rename12(tmpPath, filePath);
|
|
12513
|
-
await chmod10(filePath, 384);
|
|
12564
|
+
}
|
|
12565
|
+
async function getOrCreatePepper(dataDir) {
|
|
12566
|
+
const pepperHex = tryKeyringGet();
|
|
12567
|
+
if (pepperHex && /^[0-9a-f]{64}$/i.test(pepperHex)) {
|
|
12568
|
+
return Buffer.from(pepperHex, "hex");
|
|
12514
12569
|
}
|
|
12515
|
-
|
|
12516
|
-
|
|
12517
|
-
|
|
12518
|
-
|
|
12519
|
-
|
|
12520
|
-
|
|
12570
|
+
const filePepper = await getPepperFromFile(dataDir);
|
|
12571
|
+
if (filePepper) return filePepper;
|
|
12572
|
+
const pepper = randomBytes4(32);
|
|
12573
|
+
const saved = tryKeyringSet(pepper.toString("hex"));
|
|
12574
|
+
if (!saved) {
|
|
12575
|
+
console.warn("[master-password] Keyring unavailable, storing pepper in dataDir (fallback)");
|
|
12576
|
+
await writePepperToFile(dataDir, pepper);
|
|
12521
12577
|
}
|
|
12522
|
-
|
|
12523
|
-
|
|
12524
|
-
|
|
12578
|
+
return pepper;
|
|
12579
|
+
}
|
|
12580
|
+
async function deriveKey(masterPassword, pepper, salt) {
|
|
12581
|
+
const combined = masterPassword + pepper.toString("hex");
|
|
12582
|
+
const derived = await argon2HashRaw(combined, {
|
|
12583
|
+
...ARGON2_OPTIONS,
|
|
12584
|
+
salt
|
|
12585
|
+
});
|
|
12586
|
+
return Buffer.from(derived);
|
|
12587
|
+
}
|
|
12588
|
+
async function writeEncryptedKey(dataDir, masterPassword, encryptionKey) {
|
|
12589
|
+
const pepper = await getOrCreatePepper(dataDir);
|
|
12590
|
+
const salt = randomBytes4(16);
|
|
12591
|
+
const derivedKey = await deriveKey(masterPassword, pepper, salt);
|
|
12592
|
+
const payload = aesEncrypt(derivedKey, encryptionKey);
|
|
12593
|
+
const fileContent = Buffer.concat([salt, payload]);
|
|
12594
|
+
const encoded = fileContent.toString("base64");
|
|
12595
|
+
const filePath = join18(dataDir, ENCRYPTED_KEY_FILENAME);
|
|
12596
|
+
const tmpPath = filePath + ".tmp";
|
|
12597
|
+
await writeFile15(tmpPath, encoded, "utf-8");
|
|
12598
|
+
await chmod10(tmpPath, 384);
|
|
12599
|
+
await rename12(tmpPath, filePath);
|
|
12600
|
+
}
|
|
12601
|
+
async function readEncryptedKey(dataDir, masterPassword) {
|
|
12602
|
+
const filePath = join18(dataDir, ENCRYPTED_KEY_FILENAME);
|
|
12603
|
+
const raw = await readKeyFile(filePath);
|
|
12604
|
+
const fileContent = Buffer.from(raw.toString("utf-8").trim(), "base64");
|
|
12605
|
+
const salt = fileContent.subarray(0, 16);
|
|
12606
|
+
const payload = fileContent.subarray(16);
|
|
12607
|
+
const pepper = await getPepperFromKeychain(dataDir);
|
|
12608
|
+
const derivedKey = await deriveKey(masterPassword, pepper, salt);
|
|
12609
|
+
return aesDecrypt(derivedKey, payload);
|
|
12610
|
+
}
|
|
12611
|
+
async function getPepperFromKeychain(dataDir) {
|
|
12612
|
+
const pepperHex = tryKeyringGet();
|
|
12613
|
+
if (pepperHex && /^[0-9a-f]{64}$/i.test(pepperHex)) {
|
|
12614
|
+
return Buffer.from(pepperHex, "hex");
|
|
12525
12615
|
}
|
|
12526
|
-
|
|
12527
|
-
|
|
12528
|
-
|
|
12529
|
-
|
|
12616
|
+
const filePepper = await getPepperFromFile(dataDir);
|
|
12617
|
+
if (filePepper) return filePepper;
|
|
12618
|
+
throw new Error("Pepper introuvable (keyring et fichier). Re-setup requis.");
|
|
12619
|
+
}
|
|
12620
|
+
async function writeVerifyFile(dataDir, encryptionKey) {
|
|
12621
|
+
const payload = aesEncrypt(encryptionKey, Buffer.from(CANARY_PLAINTEXT, "utf-8"));
|
|
12622
|
+
const filePath = join18(dataDir, VERIFY_FILENAME);
|
|
12623
|
+
const tmpPath = filePath + ".tmp";
|
|
12624
|
+
await writeFile15(tmpPath, payload.toString("base64"), "utf-8");
|
|
12625
|
+
await rename12(tmpPath, filePath);
|
|
12626
|
+
}
|
|
12627
|
+
async function verifyEncryptionKey(dataDir, encryptionKey) {
|
|
12628
|
+
const filePath = join18(dataDir, VERIFY_FILENAME);
|
|
12629
|
+
const raw = await readKeyFile(filePath);
|
|
12630
|
+
if (!matchesCanary(raw.toString("utf-8"), encryptionKey)) {
|
|
12631
|
+
throw new Error("Cl\xE9 d'encryption incorrecte");
|
|
12530
12632
|
}
|
|
12531
|
-
|
|
12532
|
-
|
|
12533
|
-
|
|
12633
|
+
}
|
|
12634
|
+
function matchesCanary(canaryBase64, encryptionKey) {
|
|
12635
|
+
try {
|
|
12636
|
+
const payload = Buffer.from(canaryBase64.trim(), "base64");
|
|
12637
|
+
return aesDecrypt(encryptionKey, payload).toString("utf-8") === CANARY_PLAINTEXT;
|
|
12638
|
+
} catch {
|
|
12639
|
+
return false;
|
|
12640
|
+
}
|
|
12641
|
+
}
|
|
12642
|
+
async function readVerifyFile(dataDir) {
|
|
12643
|
+
try {
|
|
12644
|
+
return (await readKeyFile(join18(dataDir, VERIFY_FILENAME))).toString("utf-8").trim();
|
|
12645
|
+
} catch {
|
|
12646
|
+
return null;
|
|
12647
|
+
}
|
|
12648
|
+
}
|
|
12649
|
+
async function hasVerifyFile(dataDir) {
|
|
12650
|
+
try {
|
|
12651
|
+
await stat6(join18(dataDir, VERIFY_FILENAME));
|
|
12652
|
+
return true;
|
|
12653
|
+
} catch {
|
|
12654
|
+
return false;
|
|
12655
|
+
}
|
|
12656
|
+
}
|
|
12657
|
+
async function hasEncryptedKeyFile(dataDir) {
|
|
12658
|
+
try {
|
|
12659
|
+
await stat6(join18(dataDir, ENCRYPTED_KEY_FILENAME));
|
|
12660
|
+
return true;
|
|
12661
|
+
} catch {
|
|
12662
|
+
return false;
|
|
12663
|
+
}
|
|
12664
|
+
}
|
|
12665
|
+
|
|
12666
|
+
// ../server/src/services/org-canary.ts
|
|
12667
|
+
var CanaryUnavailableError = class extends Error {
|
|
12668
|
+
code = "CANARY_UNAVAILABLE";
|
|
12669
|
+
constructor(message) {
|
|
12670
|
+
super(message);
|
|
12671
|
+
this.name = "CanaryUnavailableError";
|
|
12534
12672
|
}
|
|
12535
12673
|
};
|
|
12536
|
-
var
|
|
12674
|
+
var OrgKeyMismatchError = class extends Error {
|
|
12675
|
+
code = "ORG_KEY_MISMATCH";
|
|
12676
|
+
constructor() {
|
|
12677
|
+
super(
|
|
12678
|
+
"This organization uses a different encryption key. Ask a member for the key already in use, then set it up on this machine before syncing."
|
|
12679
|
+
);
|
|
12680
|
+
this.name = "OrgKeyMismatchError";
|
|
12681
|
+
}
|
|
12682
|
+
};
|
|
12683
|
+
async function reconcileOrgCanary(orgId) {
|
|
12684
|
+
const key = getEncryptionKey();
|
|
12685
|
+
if (!key) throw new CanaryUnavailableError("Encryption key not loaded.");
|
|
12686
|
+
const local = await readVerifyFile(await keyRoot());
|
|
12687
|
+
if (!local) throw new CanaryUnavailableError("This machine has no key canary yet.");
|
|
12688
|
+
const res = await cloudFetch("/api/auth/organization/canary", {
|
|
12689
|
+
method: "POST",
|
|
12690
|
+
body: { organizationId: orgId, canary: local }
|
|
12691
|
+
});
|
|
12692
|
+
if (!res.ok) {
|
|
12693
|
+
throw new CanaryUnavailableError("Could not reach the organization on the cloud.");
|
|
12694
|
+
}
|
|
12695
|
+
const { canary } = await res.json();
|
|
12696
|
+
if (typeof canary !== "string") {
|
|
12697
|
+
throw new CanaryUnavailableError("The cloud returned no key canary.");
|
|
12698
|
+
}
|
|
12699
|
+
if (!matchesCanary(canary, key)) throw new OrgKeyMismatchError();
|
|
12700
|
+
}
|
|
12537
12701
|
|
|
12538
12702
|
// ../server/src/services/cloud-sync-bridge.ts
|
|
12539
12703
|
import { randomUUID as randomUUID11 } from "crypto";
|
|
12540
|
-
import { mkdir as
|
|
12704
|
+
import { mkdir as mkdir18, rm as rm5 } from "fs/promises";
|
|
12541
12705
|
import * as Y from "yjs";
|
|
12542
12706
|
|
|
12543
12707
|
// ../server/src/services/org-lock.ts
|
|
12544
|
-
import { open as open2, readFile as readFile16, unlink as unlink4, mkdir as
|
|
12708
|
+
import { open as open2, readFile as readFile16, unlink as unlink4, mkdir as mkdir17 } from "fs/promises";
|
|
12545
12709
|
import { readFileSync, unlinkSync } from "fs";
|
|
12546
|
-
import { join as
|
|
12710
|
+
import { join as join19 } from "path";
|
|
12547
12711
|
var held = /* @__PURE__ */ new Set();
|
|
12548
12712
|
function lockPath(orgId) {
|
|
12549
|
-
return
|
|
12713
|
+
return join19(orgVaultRoot(orgId), "org.lock");
|
|
12550
12714
|
}
|
|
12551
12715
|
function isRunning(pid) {
|
|
12552
12716
|
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
@@ -12567,7 +12731,7 @@ async function readLock(path) {
|
|
|
12567
12731
|
}
|
|
12568
12732
|
async function acquireOrgLock(orgId) {
|
|
12569
12733
|
const path = lockPath(orgId);
|
|
12570
|
-
await
|
|
12734
|
+
await mkdir17(orgVaultRoot(orgId), { recursive: true });
|
|
12571
12735
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
12572
12736
|
try {
|
|
12573
12737
|
const handle = await open2(path, "wx");
|
|
@@ -12978,7 +13142,7 @@ var OrgSyncConnection = class {
|
|
|
12978
13142
|
}
|
|
12979
13143
|
const isLocal = existing?.orgId === this.link.orgId;
|
|
12980
13144
|
if (!inDoc && !isLocal) return false;
|
|
12981
|
-
await
|
|
13145
|
+
await mkdir18(projectVaultRoot(this.link.orgId, projectId), { recursive: true });
|
|
12982
13146
|
this.tracked.add(projectId);
|
|
12983
13147
|
if (inDoc) await this.pullFromDoc([projectId]);
|
|
12984
13148
|
else await this.reconcileLocalToDoc();
|
|
@@ -13187,15 +13351,21 @@ var CloudSyncBridge = class {
|
|
|
13187
13351
|
/** Orgs another instance holds the lock on — reported, not retried blindly. */
|
|
13188
13352
|
locked = /* @__PURE__ */ new Set();
|
|
13189
13353
|
started = false;
|
|
13190
|
-
/**
|
|
13354
|
+
/**
|
|
13355
|
+
* Start syncing every organization this machine holds a project of.
|
|
13356
|
+
*
|
|
13357
|
+
* The scope is read from the disk (see `org-sync-scope.ts`) rather than from
|
|
13358
|
+
* a list of links: opening a project is what makes its org worth syncing, and
|
|
13359
|
+
* dropping the last one is what ends it.
|
|
13360
|
+
*/
|
|
13191
13361
|
async start() {
|
|
13192
13362
|
if (this.started) return;
|
|
13193
13363
|
this.started = true;
|
|
13194
13364
|
let links = [];
|
|
13195
13365
|
try {
|
|
13196
|
-
links = await
|
|
13366
|
+
links = await orgsToSync();
|
|
13197
13367
|
} catch (err) {
|
|
13198
|
-
console.error("[cloud-sync] could not
|
|
13368
|
+
console.error("[cloud-sync] could not read which organizations to sync:", err.message);
|
|
13199
13369
|
return;
|
|
13200
13370
|
}
|
|
13201
13371
|
for (const link of links) await this.connect(link);
|
|
@@ -13246,6 +13416,19 @@ var CloudSyncBridge = class {
|
|
|
13246
13416
|
*/
|
|
13247
13417
|
async connect(link) {
|
|
13248
13418
|
this.disconnect(link.orgId);
|
|
13419
|
+
try {
|
|
13420
|
+
await reconcileOrgCanary(link.orgId);
|
|
13421
|
+
} catch (err) {
|
|
13422
|
+
if (err instanceof OrgKeyMismatchError) {
|
|
13423
|
+
console.error("[cloud-sync] org %s: %s", link.orgId, err.message);
|
|
13424
|
+
return;
|
|
13425
|
+
}
|
|
13426
|
+
console.warn(
|
|
13427
|
+
"[cloud-sync] org %s: could not check the organization key (%s) \u2014 syncing anyway.",
|
|
13428
|
+
link.orgId,
|
|
13429
|
+
err.message
|
|
13430
|
+
);
|
|
13431
|
+
}
|
|
13249
13432
|
const lock = await acquireOrgLock(link.orgId).catch((err) => {
|
|
13250
13433
|
console.error("[cloud-sync] could not take the lock for org %s:", link.orgId, err.message);
|
|
13251
13434
|
return { acquired: false, heldBy: 0 };
|
|
@@ -13285,172 +13468,6 @@ var CloudSyncBridge = class {
|
|
|
13285
13468
|
};
|
|
13286
13469
|
var cloudSyncBridge = new CloudSyncBridge();
|
|
13287
13470
|
|
|
13288
|
-
// ../server/src/services/master-password.ts
|
|
13289
|
-
import { randomBytes as randomBytes4, createCipheriv, createDecipheriv } from "crypto";
|
|
13290
|
-
import { readFile as readFile17, writeFile as writeFile16, chmod as chmod11, stat as stat6, rename as rename13 } from "fs/promises";
|
|
13291
|
-
import { join as join18 } from "path";
|
|
13292
|
-
import { Entry } from "@napi-rs/keyring";
|
|
13293
|
-
import { hashRaw as argon2HashRaw } from "@node-rs/argon2";
|
|
13294
|
-
var KEYRING_SERVICE = "runeya";
|
|
13295
|
-
var KEYRING_ACCOUNT = "pepper";
|
|
13296
|
-
var VERIFY_FILENAME = ".runeya-verify";
|
|
13297
|
-
var ENCRYPTED_KEY_FILENAME = ".encryption.key.enc";
|
|
13298
|
-
var PEPPER_FILENAME = ".pepper.private";
|
|
13299
|
-
var CANARY_PLAINTEXT = "runeya:v1:key-verification";
|
|
13300
|
-
var MAX_KEY_FILE_SIZE = 1024;
|
|
13301
|
-
var ARGON2_OPTIONS = { memoryCost: 65536, timeCost: 3, parallelism: 4, outputLen: 32 };
|
|
13302
|
-
async function readKeyFile(filePath) {
|
|
13303
|
-
const s = await stat6(filePath);
|
|
13304
|
-
if (s.size > MAX_KEY_FILE_SIZE) throw new Error(`Fichier suspect : ${filePath} (${s.size} octets)`);
|
|
13305
|
-
return readFile17(filePath);
|
|
13306
|
-
}
|
|
13307
|
-
function aesEncrypt(key, plaintext) {
|
|
13308
|
-
const iv = randomBytes4(12);
|
|
13309
|
-
const cipher = createCipheriv("aes-256-gcm", key, iv);
|
|
13310
|
-
const encrypted = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
13311
|
-
const authTag = cipher.getAuthTag();
|
|
13312
|
-
return Buffer.concat([iv, authTag, encrypted]);
|
|
13313
|
-
}
|
|
13314
|
-
function aesDecrypt(key, data) {
|
|
13315
|
-
const iv = data.subarray(0, 12);
|
|
13316
|
-
const authTag = data.subarray(12, 28);
|
|
13317
|
-
const ciphertext = data.subarray(28);
|
|
13318
|
-
const decipher = createDecipheriv("aes-256-gcm", key, iv, { authTagLength: 16 });
|
|
13319
|
-
decipher.setAuthTag(authTag);
|
|
13320
|
-
return Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
13321
|
-
}
|
|
13322
|
-
async function getPepperFromFile(dataDir) {
|
|
13323
|
-
try {
|
|
13324
|
-
const raw = await readKeyFile(join18(dataDir, PEPPER_FILENAME));
|
|
13325
|
-
const hex = raw.toString("utf-8").trim();
|
|
13326
|
-
if (/^[0-9a-f]{64}$/i.test(hex)) return Buffer.from(hex, "hex");
|
|
13327
|
-
} catch {
|
|
13328
|
-
}
|
|
13329
|
-
return null;
|
|
13330
|
-
}
|
|
13331
|
-
async function writePepperToFile(dataDir, pepper) {
|
|
13332
|
-
const filePath = join18(dataDir, PEPPER_FILENAME);
|
|
13333
|
-
const tmpPath = filePath + ".tmp";
|
|
13334
|
-
await writeFile16(tmpPath, pepper.toString("hex"), "utf-8");
|
|
13335
|
-
await chmod11(tmpPath, 384);
|
|
13336
|
-
await rename13(tmpPath, filePath);
|
|
13337
|
-
}
|
|
13338
|
-
function tryKeyringGet() {
|
|
13339
|
-
try {
|
|
13340
|
-
return new Entry(KEYRING_SERVICE, KEYRING_ACCOUNT).getPassword();
|
|
13341
|
-
} catch {
|
|
13342
|
-
return null;
|
|
13343
|
-
}
|
|
13344
|
-
}
|
|
13345
|
-
function tryKeyringSet(value) {
|
|
13346
|
-
try {
|
|
13347
|
-
new Entry(KEYRING_SERVICE, KEYRING_ACCOUNT).setPassword(value);
|
|
13348
|
-
return true;
|
|
13349
|
-
} catch {
|
|
13350
|
-
return false;
|
|
13351
|
-
}
|
|
13352
|
-
}
|
|
13353
|
-
async function getOrCreatePepper(dataDir) {
|
|
13354
|
-
const pepperHex = tryKeyringGet();
|
|
13355
|
-
if (pepperHex && /^[0-9a-f]{64}$/i.test(pepperHex)) {
|
|
13356
|
-
return Buffer.from(pepperHex, "hex");
|
|
13357
|
-
}
|
|
13358
|
-
const filePepper = await getPepperFromFile(dataDir);
|
|
13359
|
-
if (filePepper) return filePepper;
|
|
13360
|
-
const pepper = randomBytes4(32);
|
|
13361
|
-
const saved = tryKeyringSet(pepper.toString("hex"));
|
|
13362
|
-
if (!saved) {
|
|
13363
|
-
console.warn("[master-password] Keyring unavailable, storing pepper in dataDir (fallback)");
|
|
13364
|
-
await writePepperToFile(dataDir, pepper);
|
|
13365
|
-
}
|
|
13366
|
-
return pepper;
|
|
13367
|
-
}
|
|
13368
|
-
async function deriveKey(masterPassword, pepper, salt) {
|
|
13369
|
-
const combined = masterPassword + pepper.toString("hex");
|
|
13370
|
-
const derived = await argon2HashRaw(combined, {
|
|
13371
|
-
...ARGON2_OPTIONS,
|
|
13372
|
-
salt
|
|
13373
|
-
});
|
|
13374
|
-
return Buffer.from(derived);
|
|
13375
|
-
}
|
|
13376
|
-
async function writeEncryptedKey(dataDir, masterPassword, encryptionKey) {
|
|
13377
|
-
const pepper = await getOrCreatePepper(dataDir);
|
|
13378
|
-
const salt = randomBytes4(16);
|
|
13379
|
-
const derivedKey = await deriveKey(masterPassword, pepper, salt);
|
|
13380
|
-
const payload = aesEncrypt(derivedKey, encryptionKey);
|
|
13381
|
-
const fileContent = Buffer.concat([salt, payload]);
|
|
13382
|
-
const encoded = fileContent.toString("base64");
|
|
13383
|
-
const filePath = join18(dataDir, ENCRYPTED_KEY_FILENAME);
|
|
13384
|
-
const tmpPath = filePath + ".tmp";
|
|
13385
|
-
await writeFile16(tmpPath, encoded, "utf-8");
|
|
13386
|
-
await chmod11(tmpPath, 384);
|
|
13387
|
-
await rename13(tmpPath, filePath);
|
|
13388
|
-
}
|
|
13389
|
-
async function readEncryptedKey(dataDir, masterPassword) {
|
|
13390
|
-
const filePath = join18(dataDir, ENCRYPTED_KEY_FILENAME);
|
|
13391
|
-
const raw = await readKeyFile(filePath);
|
|
13392
|
-
const fileContent = Buffer.from(raw.toString("utf-8").trim(), "base64");
|
|
13393
|
-
const salt = fileContent.subarray(0, 16);
|
|
13394
|
-
const payload = fileContent.subarray(16);
|
|
13395
|
-
const pepper = await getPepperFromKeychain(dataDir);
|
|
13396
|
-
const derivedKey = await deriveKey(masterPassword, pepper, salt);
|
|
13397
|
-
return aesDecrypt(derivedKey, payload);
|
|
13398
|
-
}
|
|
13399
|
-
async function getPepperFromKeychain(dataDir) {
|
|
13400
|
-
const pepperHex = tryKeyringGet();
|
|
13401
|
-
if (pepperHex && /^[0-9a-f]{64}$/i.test(pepperHex)) {
|
|
13402
|
-
return Buffer.from(pepperHex, "hex");
|
|
13403
|
-
}
|
|
13404
|
-
const filePepper = await getPepperFromFile(dataDir);
|
|
13405
|
-
if (filePepper) return filePepper;
|
|
13406
|
-
throw new Error("Pepper introuvable (keyring et fichier). Re-setup requis.");
|
|
13407
|
-
}
|
|
13408
|
-
async function writeVerifyFile(dataDir, encryptionKey) {
|
|
13409
|
-
const payload = aesEncrypt(encryptionKey, Buffer.from(CANARY_PLAINTEXT, "utf-8"));
|
|
13410
|
-
const filePath = join18(dataDir, VERIFY_FILENAME);
|
|
13411
|
-
const tmpPath = filePath + ".tmp";
|
|
13412
|
-
await writeFile16(tmpPath, payload.toString("base64"), "utf-8");
|
|
13413
|
-
await rename13(tmpPath, filePath);
|
|
13414
|
-
}
|
|
13415
|
-
async function verifyEncryptionKey(dataDir, encryptionKey) {
|
|
13416
|
-
const filePath = join18(dataDir, VERIFY_FILENAME);
|
|
13417
|
-
const raw = await readKeyFile(filePath);
|
|
13418
|
-
if (!matchesCanary(raw.toString("utf-8"), encryptionKey)) {
|
|
13419
|
-
throw new Error("Cl\xE9 d'encryption incorrecte");
|
|
13420
|
-
}
|
|
13421
|
-
}
|
|
13422
|
-
function matchesCanary(canaryBase64, encryptionKey) {
|
|
13423
|
-
try {
|
|
13424
|
-
const payload = Buffer.from(canaryBase64.trim(), "base64");
|
|
13425
|
-
return aesDecrypt(encryptionKey, payload).toString("utf-8") === CANARY_PLAINTEXT;
|
|
13426
|
-
} catch {
|
|
13427
|
-
return false;
|
|
13428
|
-
}
|
|
13429
|
-
}
|
|
13430
|
-
async function readVerifyFile(dataDir) {
|
|
13431
|
-
try {
|
|
13432
|
-
return (await readKeyFile(join18(dataDir, VERIFY_FILENAME))).toString("utf-8").trim();
|
|
13433
|
-
} catch {
|
|
13434
|
-
return null;
|
|
13435
|
-
}
|
|
13436
|
-
}
|
|
13437
|
-
async function hasVerifyFile(dataDir) {
|
|
13438
|
-
try {
|
|
13439
|
-
await stat6(join18(dataDir, VERIFY_FILENAME));
|
|
13440
|
-
return true;
|
|
13441
|
-
} catch {
|
|
13442
|
-
return false;
|
|
13443
|
-
}
|
|
13444
|
-
}
|
|
13445
|
-
async function hasEncryptedKeyFile(dataDir) {
|
|
13446
|
-
try {
|
|
13447
|
-
await stat6(join18(dataDir, ENCRYPTED_KEY_FILENAME));
|
|
13448
|
-
return true;
|
|
13449
|
-
} catch {
|
|
13450
|
-
return false;
|
|
13451
|
-
}
|
|
13452
|
-
}
|
|
13453
|
-
|
|
13454
13471
|
// ../server/src/trpc/routers/cloud-sync.ts
|
|
13455
13472
|
async function cloudMessage(res) {
|
|
13456
13473
|
try {
|
|
@@ -13466,59 +13483,23 @@ async function cloudMessage(res) {
|
|
|
13466
13483
|
return "";
|
|
13467
13484
|
}
|
|
13468
13485
|
}
|
|
13469
|
-
async function
|
|
13470
|
-
|
|
13471
|
-
|
|
13472
|
-
|
|
13473
|
-
|
|
13474
|
-
|
|
13475
|
-
|
|
13476
|
-
|
|
13477
|
-
|
|
13478
|
-
|
|
13479
|
-
|
|
13480
|
-
body: { organizationId: orgId, canary: local }
|
|
13481
|
-
});
|
|
13482
|
-
if (!res.ok) {
|
|
13483
|
-
throw new TRPCError20({ code: "BAD_GATEWAY", message: "Could not reach the organization on the cloud." });
|
|
13484
|
-
}
|
|
13485
|
-
const { canary } = await res.json();
|
|
13486
|
-
if (typeof canary !== "string") {
|
|
13487
|
-
throw new TRPCError20({ code: "BAD_GATEWAY", message: "The cloud returned no key canary." });
|
|
13488
|
-
}
|
|
13489
|
-
if (!matchesCanary(canary, key)) {
|
|
13490
|
-
throw new TRPCError20({
|
|
13491
|
-
code: "FORBIDDEN",
|
|
13492
|
-
message: "This organization uses a different encryption key. Ask a member for the key already in use, then set it up on this machine before syncing."
|
|
13493
|
-
});
|
|
13486
|
+
async function assertOrgKey(orgId) {
|
|
13487
|
+
try {
|
|
13488
|
+
await reconcileOrgCanary(orgId);
|
|
13489
|
+
} catch (err) {
|
|
13490
|
+
if (err instanceof OrgKeyMismatchError) {
|
|
13491
|
+
throw new TRPCError20({ code: "FORBIDDEN", message: err.message });
|
|
13492
|
+
}
|
|
13493
|
+
if (err instanceof CanaryUnavailableError) {
|
|
13494
|
+
throw new TRPCError20({ code: "PRECONDITION_FAILED", message: err.message });
|
|
13495
|
+
}
|
|
13496
|
+
throw err;
|
|
13494
13497
|
}
|
|
13495
13498
|
}
|
|
13496
13499
|
function syncState(orgId) {
|
|
13497
13500
|
return cloudSyncBridge.states().find((s) => s.orgId === orgId)?.state ?? "stopped";
|
|
13498
13501
|
}
|
|
13499
13502
|
var cloudSyncRouter = router({
|
|
13500
|
-
/**
|
|
13501
|
-
* Register/replace a sync link for an org and start syncing immediately.
|
|
13502
|
-
*
|
|
13503
|
-
* Refuses when this machine's key is not the org's — better to stop here than
|
|
13504
|
-
* to sync a project whose secrets will not open.
|
|
13505
|
-
*/
|
|
13506
|
-
link: protectedProcedure.input(
|
|
13507
|
-
z24.object({
|
|
13508
|
-
orgId: z24.string().min(1).max(128),
|
|
13509
|
-
cloudUrl: z24.string().url().max(2048),
|
|
13510
|
-
// No project selection: which projects this machine materializes is
|
|
13511
|
-
// read from the vaults on disk, so linking an org never re-downloads
|
|
13512
|
-
// projects the user had dropped.
|
|
13513
|
-
projectIds: z24.array(z24.string().min(1).max(128)).max(500).optional()
|
|
13514
|
-
})
|
|
13515
|
-
).mutation(async ({ input }) => {
|
|
13516
|
-
await reconcileOrgCanary(input.orgId);
|
|
13517
|
-
await cloudSyncStore.upsert(input.orgId, input.cloudUrl);
|
|
13518
|
-
const link = await cloudSyncStore.get(input.orgId);
|
|
13519
|
-
await cloudSyncBridge.connect(link ?? { orgId: input.orgId, cloudUrl: input.cloudUrl });
|
|
13520
|
-
return { success: true };
|
|
13521
|
-
}),
|
|
13522
13503
|
/**
|
|
13523
13504
|
* Follow one more project of an org, and pull it now.
|
|
13524
13505
|
*
|
|
@@ -13533,12 +13514,11 @@ var cloudSyncRouter = router({
|
|
|
13533
13514
|
projectId: z24.string().min(1).max(128)
|
|
13534
13515
|
})
|
|
13535
13516
|
).mutation(async ({ input }) => {
|
|
13536
|
-
const
|
|
13537
|
-
if (!
|
|
13538
|
-
await
|
|
13539
|
-
await
|
|
13540
|
-
|
|
13541
|
-
if (link) await cloudSyncBridge.connect(link);
|
|
13517
|
+
const connected = cloudSyncBridge.states().some((s) => s.orgId === input.orgId);
|
|
13518
|
+
if (!connected) {
|
|
13519
|
+
await assertOrgKey(input.orgId);
|
|
13520
|
+
await stampOrgCloud(input.orgId, input.cloudUrl);
|
|
13521
|
+
await cloudSyncBridge.connect({ orgId: input.orgId, cloudUrl: input.cloudUrl });
|
|
13542
13522
|
}
|
|
13543
13523
|
try {
|
|
13544
13524
|
const pulled = await cloudSyncBridge.followProject(input.orgId, input.projectId);
|
|
@@ -13582,7 +13562,7 @@ var cloudSyncRouter = router({
|
|
|
13582
13562
|
).mutation(async ({ input }) => {
|
|
13583
13563
|
let res;
|
|
13584
13564
|
try {
|
|
13585
|
-
res = await
|
|
13565
|
+
res = await cloudFetch2("/api/auth/project/delete", {
|
|
13586
13566
|
method: "POST",
|
|
13587
13567
|
body: { organizationId: input.orgId, projectId: input.projectId }
|
|
13588
13568
|
});
|
|
@@ -13606,18 +13586,15 @@ var cloudSyncRouter = router({
|
|
|
13606
13586
|
message: detail || `PROJECT_DELETE_FAILED (${res.status})`
|
|
13607
13587
|
});
|
|
13608
13588
|
}),
|
|
13609
|
-
/** Stop syncing an org and forget its token. */
|
|
13610
|
-
unlink: protectedProcedure.input(z24.object({ orgId: z24.string().min(1).max(128) })).mutation(async ({ input }) => {
|
|
13611
|
-
cloudSyncBridge.disconnect(input.orgId);
|
|
13612
|
-
await cloudSyncStore.remove(input.orgId);
|
|
13613
|
-
return { success: true };
|
|
13614
|
-
}),
|
|
13615
13589
|
/**
|
|
13616
|
-
* List orgs this
|
|
13617
|
-
* surface
|
|
13590
|
+
* List the orgs this machine syncs, with their live sync state so the UI can
|
|
13591
|
+
* surface one whose sync is paused awaiting a fresh cloud sign-in.
|
|
13592
|
+
*
|
|
13593
|
+
* Derived from the vaults on disk, so it says what is actually synced rather
|
|
13594
|
+
* than what someone once asked for.
|
|
13618
13595
|
*/
|
|
13619
13596
|
list: protectedProcedure.query(async () => {
|
|
13620
|
-
const links = await
|
|
13597
|
+
const links = await orgsToSync();
|
|
13621
13598
|
const states = new Map(cloudSyncBridge.states().map((s) => [s.orgId, s]));
|
|
13622
13599
|
return links.map((l) => ({
|
|
13623
13600
|
orgId: l.orgId,
|
|
@@ -13632,14 +13609,13 @@ var cloudSyncRouter = router({
|
|
|
13632
13609
|
/**
|
|
13633
13610
|
* The cloud instance behind every org vault on this machine.
|
|
13634
13611
|
*
|
|
13635
|
-
* Wider than `list`, which only
|
|
13636
|
-
*
|
|
13637
|
-
*
|
|
13638
|
-
* entirely.
|
|
13612
|
+
* Wider than `list`, which only reports the orgs actually synced here: a vault
|
|
13613
|
+
* may come from another cloud entirely, and saying so is how the UI explains
|
|
13614
|
+
* an org it can see but not reach.
|
|
13639
13615
|
*/
|
|
13640
13616
|
orgClouds: protectedProcedure.query(async () => {
|
|
13641
13617
|
const clouds = await readOrgClouds();
|
|
13642
|
-
for (const
|
|
13618
|
+
for (const { orgId } of await orgsToSync()) clouds[orgId] ??= getCloudApiUrl();
|
|
13643
13619
|
return clouds;
|
|
13644
13620
|
})
|
|
13645
13621
|
});
|
|
@@ -13647,13 +13623,13 @@ var cloudSyncRouter = router({
|
|
|
13647
13623
|
// ../server/src/trpc/routers/filesystem.ts
|
|
13648
13624
|
import { readdir as readdir7, stat as stat7 } from "fs/promises";
|
|
13649
13625
|
import { homedir as homedir5 } from "os";
|
|
13650
|
-
import { isAbsolute as isAbsolute4, join as
|
|
13626
|
+
import { isAbsolute as isAbsolute4, join as join20, resolve as resolve5, dirname as dirname9, basename as basename2 } from "path";
|
|
13651
13627
|
import { z as z25 } from "zod";
|
|
13652
13628
|
var MAX_ENTRIES = 500;
|
|
13653
13629
|
var PATH_SCHEMA = z25.string().min(1).max(4096);
|
|
13654
13630
|
function safeResolve(input) {
|
|
13655
13631
|
if (input.includes("\0")) throw new Error("Chemin invalide");
|
|
13656
|
-
const expanded = input.startsWith("~") ?
|
|
13632
|
+
const expanded = input.startsWith("~") ? join20(homedir5(), input.slice(1)) : input;
|
|
13657
13633
|
if (!isAbsolute4(expanded)) throw new Error("Le chemin doit \xEAtre absolu");
|
|
13658
13634
|
return resolve5(expanded);
|
|
13659
13635
|
}
|
|
@@ -13689,7 +13665,7 @@ var filesystemRouter = router({
|
|
|
13689
13665
|
const truncated = names.length > MAX_ENTRIES;
|
|
13690
13666
|
const entries = [];
|
|
13691
13667
|
for (const name of names.slice(0, MAX_ENTRIES)) {
|
|
13692
|
-
const path =
|
|
13668
|
+
const path = join20(dir, name);
|
|
13693
13669
|
entries.push({ name, path, hasRuneyaProject: await holdsRuneyaProject(path) });
|
|
13694
13670
|
}
|
|
13695
13671
|
return {
|
|
@@ -13789,7 +13765,7 @@ var MigrationEngine = class {
|
|
|
13789
13765
|
var migrationEngine = new MigrationEngine();
|
|
13790
13766
|
|
|
13791
13767
|
// ../server/src/services/file-security.ts
|
|
13792
|
-
import { chmod as
|
|
13768
|
+
import { chmod as chmod11, stat as stat8 } from "fs/promises";
|
|
13793
13769
|
var SENSITIVE_FILES = [
|
|
13794
13770
|
"agents.json",
|
|
13795
13771
|
"settings.json",
|
|
@@ -13807,7 +13783,7 @@ async function ensureFilePermissions() {
|
|
|
13807
13783
|
const mode = stats.mode & 511;
|
|
13808
13784
|
if (mode !== SECURE_MODE) {
|
|
13809
13785
|
console.warn("[security] File", filepath, "has mode", mode.toString(8) + ", fixing to 600");
|
|
13810
|
-
await
|
|
13786
|
+
await chmod11(filepath, SECURE_MODE);
|
|
13811
13787
|
}
|
|
13812
13788
|
} catch (err) {
|
|
13813
13789
|
if (err.code !== "ENOENT") {
|
|
@@ -13819,8 +13795,8 @@ async function ensureFilePermissions() {
|
|
|
13819
13795
|
}
|
|
13820
13796
|
|
|
13821
13797
|
// ../server/src/services/vault-migration.ts
|
|
13822
|
-
import { readdir as readdir8, readFile as
|
|
13823
|
-
import { join as
|
|
13798
|
+
import { readdir as readdir8, readFile as readFile17, writeFile as writeFile16, mkdir as mkdir19, copyFile, unlink as unlink5, rename as rename13, stat as stat9 } from "fs/promises";
|
|
13799
|
+
import { join as join21 } from "path";
|
|
13824
13800
|
var KEY_FILES = [".encryption.key.enc", ".runeya-verify"];
|
|
13825
13801
|
var MACHINE_FILES = [
|
|
13826
13802
|
"agents.json",
|
|
@@ -13871,17 +13847,17 @@ async function openLegacyVault(legacyRoot, masterPassword) {
|
|
|
13871
13847
|
}
|
|
13872
13848
|
async function migrateLegacyVault(legacyRoot, machineRootDir, legacyKey, machineKey, now) {
|
|
13873
13849
|
const stamp = now.toISOString().replace(/[:.]/g, "-").substring(0, 19);
|
|
13874
|
-
const backupDir =
|
|
13875
|
-
await
|
|
13850
|
+
const backupDir = join21(legacyRoot, `backup-${stamp}`);
|
|
13851
|
+
await mkdir19(backupDir, { recursive: true });
|
|
13876
13852
|
const jsonFiles = await vaultJsonFiles(legacyRoot);
|
|
13877
13853
|
for (const name of [...jsonFiles, ...KEY_FILES]) {
|
|
13878
|
-
if (await exists(
|
|
13879
|
-
await copyFile(
|
|
13854
|
+
if (await exists(join21(legacyRoot, name))) {
|
|
13855
|
+
await copyFile(join21(legacyRoot, name), join21(backupDir, name));
|
|
13880
13856
|
}
|
|
13881
13857
|
}
|
|
13882
13858
|
const rekeyed = [];
|
|
13883
13859
|
for (const name of jsonFiles) {
|
|
13884
|
-
const raw = await
|
|
13860
|
+
const raw = await readFile17(join21(legacyRoot, name), "utf-8");
|
|
13885
13861
|
let parsed;
|
|
13886
13862
|
try {
|
|
13887
13863
|
parsed = JSON.parse(raw);
|
|
@@ -13891,13 +13867,13 @@ async function migrateLegacyVault(legacyRoot, machineRootDir, legacyKey, machine
|
|
|
13891
13867
|
const counter = { n: 0 };
|
|
13892
13868
|
const converted = rekeyValue(parsed, legacyKey, machineKey, counter);
|
|
13893
13869
|
if (counter.n === 0) continue;
|
|
13894
|
-
const tmp =
|
|
13895
|
-
await
|
|
13896
|
-
await
|
|
13870
|
+
const tmp = join21(legacyRoot, `${name}.tmp`);
|
|
13871
|
+
await writeFile16(tmp, JSON.stringify(converted, null, 2), "utf-8");
|
|
13872
|
+
await rename13(tmp, join21(legacyRoot, name));
|
|
13897
13873
|
rekeyed.push({ file: name, values: counter.n });
|
|
13898
13874
|
}
|
|
13899
13875
|
for (const { file, values } of rekeyed) {
|
|
13900
|
-
const parsed = JSON.parse(await
|
|
13876
|
+
const parsed = JSON.parse(await readFile17(join21(legacyRoot, file), "utf-8"));
|
|
13901
13877
|
const check = { n: 0 };
|
|
13902
13878
|
rekeyValue(parsed, machineKey, machineKey, check);
|
|
13903
13879
|
if (check.n !== values) {
|
|
@@ -13906,13 +13882,13 @@ async function migrateLegacyVault(legacyRoot, machineRootDir, legacyKey, machine
|
|
|
13906
13882
|
);
|
|
13907
13883
|
}
|
|
13908
13884
|
}
|
|
13909
|
-
await
|
|
13885
|
+
await mkdir19(machineRootDir, { recursive: true });
|
|
13910
13886
|
const moved = [];
|
|
13911
13887
|
const skipped = [];
|
|
13912
13888
|
for (const name of MACHINE_FILES) {
|
|
13913
|
-
const from =
|
|
13889
|
+
const from = join21(legacyRoot, name);
|
|
13914
13890
|
if (!await exists(from)) continue;
|
|
13915
|
-
const to =
|
|
13891
|
+
const to = join21(machineRootDir, name);
|
|
13916
13892
|
if (await exists(to)) {
|
|
13917
13893
|
skipped.push(name);
|
|
13918
13894
|
continue;
|
|
@@ -13922,7 +13898,7 @@ async function migrateLegacyVault(legacyRoot, machineRootDir, legacyKey, machine
|
|
|
13922
13898
|
moved.push(name);
|
|
13923
13899
|
}
|
|
13924
13900
|
for (const name of KEY_FILES) {
|
|
13925
|
-
const path =
|
|
13901
|
+
const path = join21(legacyRoot, name);
|
|
13926
13902
|
if (await exists(path)) await unlink5(path);
|
|
13927
13903
|
}
|
|
13928
13904
|
return { backupDir, rekeyed, moved, skipped };
|
|
@@ -13933,14 +13909,14 @@ import { createRequire } from "module";
|
|
|
13933
13909
|
import { randomBytes as randomBytes5, createCipheriv as createCipheriv2, createDecipheriv as createDecipheriv2 } from "crypto";
|
|
13934
13910
|
import { randomUUID as randomUUID12 } from "crypto";
|
|
13935
13911
|
import {
|
|
13936
|
-
readFile as
|
|
13937
|
-
writeFile as
|
|
13938
|
-
rename as
|
|
13939
|
-
mkdir as
|
|
13940
|
-
access as
|
|
13912
|
+
readFile as readFile18,
|
|
13913
|
+
writeFile as writeFile17,
|
|
13914
|
+
rename as rename14,
|
|
13915
|
+
mkdir as mkdir20,
|
|
13916
|
+
access as access3,
|
|
13941
13917
|
readdir as readdir9
|
|
13942
13918
|
} from "fs/promises";
|
|
13943
|
-
import { join as
|
|
13919
|
+
import { join as join22, dirname as dirname10 } from "path";
|
|
13944
13920
|
var _require = createRequire(import.meta.url);
|
|
13945
13921
|
var PARSER_ID_MAP = {
|
|
13946
13922
|
"stack-monitor-parser-jsons": "native:json",
|
|
@@ -14049,7 +14025,7 @@ async function getSodium() {
|
|
|
14049
14025
|
return _sodium;
|
|
14050
14026
|
}
|
|
14051
14027
|
async function decryptLegacyFile(filePath, key, sodium) {
|
|
14052
|
-
const raw = (await
|
|
14028
|
+
const raw = (await readFile18(filePath, "utf-8")).trim();
|
|
14053
14029
|
if (!raw || raw === "[]" || raw === "{}") {
|
|
14054
14030
|
try {
|
|
14055
14031
|
return JSON.parse(raw);
|
|
@@ -14069,33 +14045,33 @@ async function decryptLegacyFile(filePath, key, sodium) {
|
|
|
14069
14045
|
}
|
|
14070
14046
|
async function atomicWrite(path, data) {
|
|
14071
14047
|
const tmp = path + ".tmp";
|
|
14072
|
-
await
|
|
14073
|
-
await
|
|
14048
|
+
await writeFile17(tmp, JSON.stringify(data, null, 2), "utf-8");
|
|
14049
|
+
await rename14(tmp, path);
|
|
14074
14050
|
}
|
|
14075
14051
|
async function runLegacyMigration(dataDir) {
|
|
14076
14052
|
const legacyRoot = env.DATA_DIR;
|
|
14077
|
-
const legacyKeyPath =
|
|
14053
|
+
const legacyKeyPath = join22(legacyRoot, "dbs", "encryption-key.json");
|
|
14078
14054
|
try {
|
|
14079
|
-
await
|
|
14055
|
+
await access3(legacyKeyPath);
|
|
14080
14056
|
} catch {
|
|
14081
14057
|
return false;
|
|
14082
14058
|
}
|
|
14083
14059
|
console.log("[legacy-migration] Legacy Runeya data detected, starting migration\u2026");
|
|
14084
14060
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").substring(0, 19);
|
|
14085
|
-
const legacyTmp =
|
|
14086
|
-
await
|
|
14087
|
-
await
|
|
14088
|
-
const legacyOld =
|
|
14089
|
-
await
|
|
14061
|
+
const legacyTmp = join22(abCwd, ".runeya-tmp");
|
|
14062
|
+
await rename14(legacyRoot, legacyTmp);
|
|
14063
|
+
await mkdir20(dataDir, { recursive: true });
|
|
14064
|
+
const legacyOld = join22(dataDir, `backup-${timestamp}`);
|
|
14065
|
+
await rename14(legacyTmp, legacyOld);
|
|
14090
14066
|
console.log(`[legacy-migration] Legacy data backed up to ${legacyOld}`);
|
|
14091
|
-
const keyJson = JSON.parse(await
|
|
14067
|
+
const keyJson = JSON.parse(await readFile18(join22(legacyOld, "dbs", "encryption-key.json"), "utf-8"));
|
|
14092
14068
|
const sodium = await getSodium();
|
|
14093
14069
|
const sodiumKey = sodium.from_base64(keyJson.encryptionKey);
|
|
14094
14070
|
const keyBuffer = Buffer.from(sodiumKey);
|
|
14095
14071
|
const runyeaKeyHex = keyBuffer.toString("hex");
|
|
14096
14072
|
await setEncryptionKey(runyeaKeyHex);
|
|
14097
14073
|
console.log("[legacy-migration] Encryption key loaded in memory (temporary)");
|
|
14098
|
-
const dbsDir =
|
|
14074
|
+
const dbsDir = join22(legacyOld, "dbs");
|
|
14099
14075
|
async function readEncrypted(filePath) {
|
|
14100
14076
|
try {
|
|
14101
14077
|
return await decryptLegacyFile(filePath, sodiumKey, sodium);
|
|
@@ -14104,29 +14080,29 @@ async function runLegacyMigration(dataDir) {
|
|
|
14104
14080
|
return null;
|
|
14105
14081
|
}
|
|
14106
14082
|
}
|
|
14107
|
-
const envsDir =
|
|
14083
|
+
const envsDir = join22(dbsDir, "envs");
|
|
14108
14084
|
const envFiles = (await readdir9(envsDir).catch(() => [])).filter((f) => f.endsWith(".encrypted.json"));
|
|
14109
14085
|
const legacyEnvs = {};
|
|
14110
14086
|
for (const file of envFiles) {
|
|
14111
14087
|
const name = file.replace(".encrypted.json", "");
|
|
14112
|
-
legacyEnvs[name] = await readEncrypted(
|
|
14088
|
+
legacyEnvs[name] = await readEncrypted(join22(envsDir, file));
|
|
14113
14089
|
}
|
|
14114
|
-
const servicesDir =
|
|
14090
|
+
const servicesDir = join22(dbsDir, "services");
|
|
14115
14091
|
const serviceFiles = (await readdir9(servicesDir).catch(() => [])).filter((f) => f.endsWith(".encrypted.json"));
|
|
14116
14092
|
const legacyServices = {};
|
|
14117
14093
|
for (const file of serviceFiles) {
|
|
14118
14094
|
const name = file.replace(".encrypted.json", "");
|
|
14119
14095
|
if (!name) continue;
|
|
14120
|
-
const data = await readEncrypted(
|
|
14096
|
+
const data = await readEncrypted(join22(servicesDir, file));
|
|
14121
14097
|
if (data?.label) legacyServices[name] = data;
|
|
14122
14098
|
}
|
|
14123
|
-
const overridesDir =
|
|
14099
|
+
const overridesDir = join22(dbsDir, "overrides");
|
|
14124
14100
|
const overrideFiles = (await readdir9(overridesDir).catch(() => [])).filter((f) => f.endsWith(".encrypted.json"));
|
|
14125
14101
|
const legacyServiceOverrides = {};
|
|
14126
14102
|
const legacyEnvOverrides = {};
|
|
14127
14103
|
for (const file of overrideFiles) {
|
|
14128
14104
|
const baseName = file.replace(".encrypted.json", "");
|
|
14129
|
-
const data = await readEncrypted(
|
|
14105
|
+
const data = await readEncrypted(join22(overridesDir, file));
|
|
14130
14106
|
if (!data) continue;
|
|
14131
14107
|
if (baseName.endsWith("-envs")) {
|
|
14132
14108
|
legacyServiceOverrides[baseName.slice(0, -5)] = data;
|
|
@@ -14134,11 +14110,11 @@ async function runLegacyMigration(dataDir) {
|
|
|
14134
14110
|
legacyEnvOverrides[baseName.slice(0, -12)] = data;
|
|
14135
14111
|
}
|
|
14136
14112
|
}
|
|
14137
|
-
const parsersDir =
|
|
14113
|
+
const parsersDir = join22(dbsDir, "parsers");
|
|
14138
14114
|
const parserFiles = (await readdir9(parsersDir).catch(() => [])).filter((f) => f.endsWith(".encrypted.json"));
|
|
14139
14115
|
const legacyParsers = [];
|
|
14140
14116
|
for (const file of parserFiles) {
|
|
14141
|
-
const data = await readEncrypted(
|
|
14117
|
+
const data = await readEncrypted(join22(parsersDir, file));
|
|
14142
14118
|
if (data?.id) legacyParsers.push(data);
|
|
14143
14119
|
}
|
|
14144
14120
|
const projectMonoId = randomUUID12();
|
|
@@ -14479,7 +14455,7 @@ async function runLegacyMigration(dataDir) {
|
|
|
14479
14455
|
}
|
|
14480
14456
|
let existingSettings = {};
|
|
14481
14457
|
try {
|
|
14482
|
-
existingSettings = JSON.parse(await
|
|
14458
|
+
existingSettings = JSON.parse(await readFile18(join22(dataDir, "settings.json"), "utf-8"));
|
|
14483
14459
|
} catch {
|
|
14484
14460
|
}
|
|
14485
14461
|
const migratedParsers = legacyParsers.map((p) => ({
|
|
@@ -14500,11 +14476,11 @@ ${p.transform ?? ""}`,
|
|
|
14500
14476
|
...migratedParsers
|
|
14501
14477
|
]
|
|
14502
14478
|
};
|
|
14503
|
-
await atomicWrite(
|
|
14504
|
-
await atomicWrite(
|
|
14505
|
-
await atomicWrite(
|
|
14506
|
-
await atomicWrite(
|
|
14507
|
-
await atomicWrite(
|
|
14479
|
+
await atomicWrite(join22(dataDir, "projects.json"), runeProjects);
|
|
14480
|
+
await atomicWrite(join22(dataDir, "services.json"), runeServices);
|
|
14481
|
+
await atomicWrite(join22(dataDir, "environments.json"), runeEnvs);
|
|
14482
|
+
await atomicWrite(join22(dataDir, "environment-overrides.json"), flatOverrides);
|
|
14483
|
+
await atomicWrite(join22(dataDir, "settings.json"), newSettings);
|
|
14508
14484
|
const svcEnvCount = runeEnvs.filter((e) => e.scope === "service").length;
|
|
14509
14485
|
console.log(`[legacy-migration] Written: ${runeProjects.length} projects, ${runeServices.length} services, ${runeEnvs.length} environments (${svcEnvCount} service-scoped)`);
|
|
14510
14486
|
console.log(`[legacy-migration] Overrides: ${Object.keys(flatOverrides).length}, custom parsers: ${migratedParsers.length} (all disabled \u2014 rewrite required)`);
|
|
@@ -14518,9 +14494,9 @@ async function findLegacyKeyInBackup(dataDir) {
|
|
|
14518
14494
|
const entries = await readdir9(dataDir);
|
|
14519
14495
|
const backupDirs = entries.filter((e) => e.startsWith("backup-")).sort((a, b) => a < b ? -1 : a > b ? 1 : 0).reverse();
|
|
14520
14496
|
for (const dir of backupDirs) {
|
|
14521
|
-
const keyJsonPath =
|
|
14497
|
+
const keyJsonPath = join22(dataDir, dir, "dbs", "encryption-key.json");
|
|
14522
14498
|
try {
|
|
14523
|
-
const raw = await
|
|
14499
|
+
const raw = await readFile18(keyJsonPath, "utf-8");
|
|
14524
14500
|
const parsed = JSON.parse(raw);
|
|
14525
14501
|
if (!parsed.encryptionKey) continue;
|
|
14526
14502
|
const sodium = await getSodium();
|
|
@@ -14541,8 +14517,8 @@ async function runOverrideOnlyMigration(dataDir) {
|
|
|
14541
14517
|
const { resolve: resolve7 } = await import("path");
|
|
14542
14518
|
const abDataDir = resolve7(dataDir);
|
|
14543
14519
|
const candidateDirs = [
|
|
14544
|
-
|
|
14545
|
-
|
|
14520
|
+
join22(dirname10(env.DATA_DIR), ".runeya", "dbs", "overrides"),
|
|
14521
|
+
join22(abDataDir, "dbs", "overrides")
|
|
14546
14522
|
];
|
|
14547
14523
|
let overridesDir = null;
|
|
14548
14524
|
let overrideFiles = [];
|
|
@@ -14558,9 +14534,9 @@ async function runOverrideOnlyMigration(dataDir) {
|
|
|
14558
14534
|
}
|
|
14559
14535
|
}
|
|
14560
14536
|
if (!overridesDir || overrideFiles.length === 0) return false;
|
|
14561
|
-
const legacyKeyPath =
|
|
14537
|
+
const legacyKeyPath = join22(overridesDir, "..", "encryption-key.json");
|
|
14562
14538
|
try {
|
|
14563
|
-
await
|
|
14539
|
+
await access3(legacyKeyPath);
|
|
14564
14540
|
return false;
|
|
14565
14541
|
} catch {
|
|
14566
14542
|
}
|
|
@@ -14576,7 +14552,7 @@ async function runOverrideOnlyMigration(dataDir) {
|
|
|
14576
14552
|
const envNameToId = {};
|
|
14577
14553
|
let envsRaw;
|
|
14578
14554
|
try {
|
|
14579
|
-
envsRaw = JSON.parse(await
|
|
14555
|
+
envsRaw = JSON.parse(await readFile18(join22(abDataDir, "environments.json"), "utf-8"));
|
|
14580
14556
|
for (const _env of envsRaw) {
|
|
14581
14557
|
const env2 = _env;
|
|
14582
14558
|
if (env2["name"] && env2["id"]) {
|
|
@@ -14595,7 +14571,7 @@ async function runOverrideOnlyMigration(dataDir) {
|
|
|
14595
14571
|
let servicesRaw = [];
|
|
14596
14572
|
const serviceNameToId = {};
|
|
14597
14573
|
try {
|
|
14598
|
-
servicesRaw = JSON.parse(await
|
|
14574
|
+
servicesRaw = JSON.parse(await readFile18(join22(abDataDir, "services.json"), "utf-8"));
|
|
14599
14575
|
for (const _svc of servicesRaw) {
|
|
14600
14576
|
const svc = _svc;
|
|
14601
14577
|
if (svc["name"] && svc["id"]) serviceNameToId[svc["name"]] = svc["id"];
|
|
@@ -14612,7 +14588,7 @@ async function runOverrideOnlyMigration(dataDir) {
|
|
|
14612
14588
|
const envName = baseName.slice(0, -"-environment".length);
|
|
14613
14589
|
let data;
|
|
14614
14590
|
try {
|
|
14615
|
-
data = await decryptLegacyFile(
|
|
14591
|
+
data = await decryptLegacyFile(join22(overridesDir, file), sodiumKey, sodium);
|
|
14616
14592
|
} catch (err) {
|
|
14617
14593
|
console.warn(`[override-migration] Failed to decrypt ${file}: ${err.message}`);
|
|
14618
14594
|
continue;
|
|
@@ -14639,7 +14615,7 @@ async function runOverrideOnlyMigration(dataDir) {
|
|
|
14639
14615
|
}
|
|
14640
14616
|
}
|
|
14641
14617
|
if (Object.keys(flatOverrides).length > 0) {
|
|
14642
|
-
await atomicWrite(
|
|
14618
|
+
await atomicWrite(join22(abDataDir, "environment-overrides.json"), flatOverrides);
|
|
14643
14619
|
console.log(`[override-migration] Written ${Object.keys(flatOverrides).length} override(s) to environment-overrides.json`);
|
|
14644
14620
|
}
|
|
14645
14621
|
let envsModified = false;
|
|
@@ -14649,7 +14625,7 @@ async function runOverrideOnlyMigration(dataDir) {
|
|
|
14649
14625
|
const serviceName = baseName.slice(0, -"-envs".length);
|
|
14650
14626
|
let data;
|
|
14651
14627
|
try {
|
|
14652
|
-
data = await decryptLegacyFile(
|
|
14628
|
+
data = await decryptLegacyFile(join22(overridesDir, file), sodiumKey, sodium);
|
|
14653
14629
|
} catch (err) {
|
|
14654
14630
|
console.warn(`[override-migration] Failed to decrypt ${file}: ${err.message}`);
|
|
14655
14631
|
continue;
|
|
@@ -14708,12 +14684,12 @@ async function runOverrideOnlyMigration(dataDir) {
|
|
|
14708
14684
|
}
|
|
14709
14685
|
}
|
|
14710
14686
|
if (envsModified) {
|
|
14711
|
-
await atomicWrite(
|
|
14687
|
+
await atomicWrite(join22(abDataDir, "environments.json"), envsRaw);
|
|
14712
14688
|
console.log("[override-migration] Merged service overrides into environments.json");
|
|
14713
14689
|
}
|
|
14714
14690
|
const migratedDir = overridesDir.replace(/overrides$/, "overrides-migrated");
|
|
14715
14691
|
try {
|
|
14716
|
-
await
|
|
14692
|
+
await rename14(overridesDir, migratedDir);
|
|
14717
14693
|
console.log(`[override-migration] Moved overrides/ \u2192 overrides-migrated/`);
|
|
14718
14694
|
} catch (err) {
|
|
14719
14695
|
console.warn(`[override-migration] Could not rename overrides dir: ${err.message}`);
|
|
@@ -14723,7 +14699,7 @@ async function runOverrideOnlyMigration(dataDir) {
|
|
|
14723
14699
|
}
|
|
14724
14700
|
|
|
14725
14701
|
// ../server/src/services/server-state.ts
|
|
14726
|
-
import { join as
|
|
14702
|
+
import { join as join23 } from "path";
|
|
14727
14703
|
import { stat as stat10 } from "fs/promises";
|
|
14728
14704
|
var ENCRYPTED_KEY_FILENAME2 = ".encryption.key.enc";
|
|
14729
14705
|
var VERIFY_FILENAME2 = ".runeya-verify";
|
|
@@ -14733,22 +14709,22 @@ async function getServerState() {
|
|
|
14733
14709
|
let hasVerify = false;
|
|
14734
14710
|
let hasData = false;
|
|
14735
14711
|
try {
|
|
14736
|
-
await stat10(
|
|
14712
|
+
await stat10(join23(dataDir, ENCRYPTED_KEY_FILENAME2));
|
|
14737
14713
|
hasEncFile = true;
|
|
14738
14714
|
} catch {
|
|
14739
14715
|
}
|
|
14740
14716
|
try {
|
|
14741
|
-
await stat10(
|
|
14717
|
+
await stat10(join23(dataDir, VERIFY_FILENAME2));
|
|
14742
14718
|
hasVerify = true;
|
|
14743
14719
|
} catch {
|
|
14744
14720
|
}
|
|
14745
14721
|
try {
|
|
14746
|
-
await stat10(
|
|
14722
|
+
await stat10(join23(dataDir, "projects.json"));
|
|
14747
14723
|
hasData = true;
|
|
14748
14724
|
} catch {
|
|
14749
14725
|
}
|
|
14750
14726
|
try {
|
|
14751
|
-
await stat10(
|
|
14727
|
+
await stat10(join23(dataDir, "project.json"));
|
|
14752
14728
|
hasData = true;
|
|
14753
14729
|
} catch {
|
|
14754
14730
|
}
|
|
@@ -15962,4 +15938,4 @@ export {
|
|
|
15962
15938
|
createLocalServer,
|
|
15963
15939
|
pullEnv
|
|
15964
15940
|
};
|
|
15965
|
-
//# sourceMappingURL=src-
|
|
15941
|
+
//# sourceMappingURL=src-R6JOPDNL.js.map
|