@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.
@@ -84,7 +84,7 @@ import {
84
84
  traefikManager,
85
85
  vaultFor,
86
86
  writeEntity
87
- } from "./chunk-4AGTVGSH.js";
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/trpc/routers/cloud-auth.ts
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 cloudFetch(path, init = {}) {
6352
- const token = await cloudSessionStore.getToken();
6353
- if (!token) {
6354
- throw new TRPCError10({ code: "PRECONDITION_FAILED", message: "Not linked to the Runeya cloud." });
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 cloudFetch("/api/auth/get-session");
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 cloudFetch("/api/auth/device/list");
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 cloudFetch("/api/auth/device/revoke", { method: "POST", body: { id: input.id } });
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 cloudFetch(`/api/auth${input.path}${qs}`, {
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 cloudFetch("/api/auth/sign-out", { method: "POST", body: {} });
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 cloudFetch(`/api/auth${path}`, init);
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-JNA5ZALK.js");
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-JNA5ZALK.js");
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 cloudFetch(`/api/kanban/file/${id}`, { timeoutMs: 12e4 });
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/cloud-sync-store.ts
12467
- import { readFile as readFile15, writeFile as writeFile15, rename as rename12, mkdir as mkdir17, chmod as chmod10 } from "fs/promises";
12468
- var FILENAME6 = "cloud-sync.json";
12469
- var CloudSyncStore = class {
12470
- links = /* @__PURE__ */ new Map();
12471
- loaded = false;
12472
- /** Machine-wide: this describes the machine, not any one project. */
12473
- getFilePath() {
12474
- return machineFilePath(FILENAME6);
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
- /** Where older versions kept it — read only, so an older Runeya keeps working. */
12477
- getLegacyFilePath() {
12478
- return legacyMachineFilePath(FILENAME6);
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
- * Read the machine file, falling back to the launch directory of old.
12482
- *
12483
- * The legacy file is left in place: the next save writes the new location,
12484
- * and a directory still opened by an older Runeya keeps working meanwhile.
12485
- */
12486
- async readFromDisk() {
12487
- try {
12488
- return await readFile15(this.getFilePath(), "utf-8");
12489
- } catch {
12490
- return readFile15(this.getLegacyFilePath(), "utf-8");
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
- async load() {
12494
- if (this.loaded) return;
12495
- try {
12496
- const raw = await this.readFromDisk();
12497
- const data = JSON.parse(raw);
12498
- for (const l of data) {
12499
- this.links.set(l.orgId, { orgId: l.orgId, cloudUrl: l.cloudUrl });
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
- async save() {
12507
- await mkdir17(machineRoot(), { recursive: true });
12508
- const filePath = this.getFilePath();
12509
- const tmpPath = filePath + ".tmp";
12510
- const data = JSON.stringify(Array.from(this.links.values()), null, 2);
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
- /** Store (or replace) the sync link for an organization. */
12516
- async upsert(orgId, cloudUrl) {
12517
- await this.load();
12518
- this.links.set(orgId, { orgId, cloudUrl });
12519
- await stampOrgCloud(orgId, cloudUrl);
12520
- await this.save();
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
- async remove(orgId) {
12523
- await this.load();
12524
- if (this.links.delete(orgId)) await this.save();
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
- /** All sync links (for the bridge on startup). */
12527
- async list() {
12528
- await this.load();
12529
- return Array.from(this.links.values());
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
- async get(orgId) {
12532
- await this.load();
12533
- return this.links.get(orgId) ?? null;
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 cloudSyncStore = new CloudSyncStore();
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 mkdir19, rm as rm5 } from "fs/promises";
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 mkdir18 } from "fs/promises";
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 join17 } from "path";
12710
+ import { join as join19 } from "path";
12547
12711
  var held = /* @__PURE__ */ new Set();
12548
12712
  function lockPath(orgId) {
12549
- return join17(orgVaultRoot(orgId), "org.lock");
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 mkdir18(orgVaultRoot(orgId), { recursive: true });
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 mkdir19(projectVaultRoot(this.link.orgId, projectId), { recursive: true });
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
- /** Start syncing every organization already linked on disk. */
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 cloudSyncStore.list();
13366
+ links = await orgsToSync();
13197
13367
  } catch (err) {
13198
- console.error("[cloud-sync] could not load links:", err.message);
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 reconcileOrgCanary(orgId) {
13470
- const key = getEncryptionKey();
13471
- if (!key) {
13472
- throw new TRPCError20({ code: "PRECONDITION_FAILED", message: "Encryption key not loaded." });
13473
- }
13474
- const local = await readVerifyFile(await keyRoot());
13475
- if (!local) {
13476
- throw new TRPCError20({ code: "PRECONDITION_FAILED", message: "This machine has no key canary yet." });
13477
- }
13478
- const res = await cloudFetch("/api/auth/organization/canary", {
13479
- method: "POST",
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 existing = await cloudSyncStore.get(input.orgId);
13537
- if (!existing) {
13538
- await reconcileOrgCanary(input.orgId);
13539
- await cloudSyncStore.upsert(input.orgId, input.cloudUrl);
13540
- const link = await cloudSyncStore.get(input.orgId);
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 cloudFetch("/api/auth/project/delete", {
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 server is linked to, with their live sync state so the UI can
13617
- * surface an org whose sync is paused awaiting a fresh cloud sign-in.
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 cloudSyncStore.list();
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 sees the links of the directory this server
13636
- * was launched in: org vaults are shared machine-wide, so a project can very
13637
- * well belong to an org linked from another directory or from another cloud
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 l of await cloudSyncStore.list()) clouds[l.orgId] = l.cloudUrl;
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 join19, resolve as resolve5, dirname as dirname9, basename as basename2 } from "path";
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("~") ? join19(homedir5(), input.slice(1)) : input;
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 = join19(dir, name);
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 chmod12, stat as stat8 } from "fs/promises";
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 chmod12(filepath, SECURE_MODE);
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 readFile18, writeFile as writeFile17, mkdir as mkdir20, copyFile, unlink as unlink5, rename as rename14, stat as stat9 } from "fs/promises";
13823
- import { join as join20 } from "path";
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 = join20(legacyRoot, `backup-${stamp}`);
13875
- await mkdir20(backupDir, { recursive: true });
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(join20(legacyRoot, name))) {
13879
- await copyFile(join20(legacyRoot, name), join20(backupDir, name));
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 readFile18(join20(legacyRoot, name), "utf-8");
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 = join20(legacyRoot, `${name}.tmp`);
13895
- await writeFile17(tmp, JSON.stringify(converted, null, 2), "utf-8");
13896
- await rename14(tmp, join20(legacyRoot, name));
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 readFile18(join20(legacyRoot, file), "utf-8"));
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 mkdir20(machineRootDir, { recursive: true });
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 = join20(legacyRoot, name);
13889
+ const from = join21(legacyRoot, name);
13914
13890
  if (!await exists(from)) continue;
13915
- const to = join20(machineRootDir, name);
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 = join20(legacyRoot, name);
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 readFile19,
13937
- writeFile as writeFile18,
13938
- rename as rename15,
13939
- mkdir as mkdir21,
13940
- access as access2,
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 join21, dirname as dirname10 } from "path";
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 readFile19(filePath, "utf-8")).trim();
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 writeFile18(tmp, JSON.stringify(data, null, 2), "utf-8");
14073
- await rename15(tmp, path);
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 = join21(legacyRoot, "dbs", "encryption-key.json");
14053
+ const legacyKeyPath = join22(legacyRoot, "dbs", "encryption-key.json");
14078
14054
  try {
14079
- await access2(legacyKeyPath);
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 = join21(abCwd, ".runeya-tmp");
14086
- await rename15(legacyRoot, legacyTmp);
14087
- await mkdir21(dataDir, { recursive: true });
14088
- const legacyOld = join21(dataDir, `backup-${timestamp}`);
14089
- await rename15(legacyTmp, legacyOld);
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 readFile19(join21(legacyOld, "dbs", "encryption-key.json"), "utf-8"));
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 = join21(legacyOld, "dbs");
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 = join21(dbsDir, "envs");
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(join21(envsDir, file));
14088
+ legacyEnvs[name] = await readEncrypted(join22(envsDir, file));
14113
14089
  }
14114
- const servicesDir = join21(dbsDir, "services");
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(join21(servicesDir, file));
14096
+ const data = await readEncrypted(join22(servicesDir, file));
14121
14097
  if (data?.label) legacyServices[name] = data;
14122
14098
  }
14123
- const overridesDir = join21(dbsDir, "overrides");
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(join21(overridesDir, file));
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 = join21(dbsDir, "parsers");
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(join21(parsersDir, file));
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 readFile19(join21(dataDir, "settings.json"), "utf-8"));
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(join21(dataDir, "projects.json"), runeProjects);
14504
- await atomicWrite(join21(dataDir, "services.json"), runeServices);
14505
- await atomicWrite(join21(dataDir, "environments.json"), runeEnvs);
14506
- await atomicWrite(join21(dataDir, "environment-overrides.json"), flatOverrides);
14507
- await atomicWrite(join21(dataDir, "settings.json"), newSettings);
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 = join21(dataDir, dir, "dbs", "encryption-key.json");
14497
+ const keyJsonPath = join22(dataDir, dir, "dbs", "encryption-key.json");
14522
14498
  try {
14523
- const raw = await readFile19(keyJsonPath, "utf-8");
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
- join21(dirname10(env.DATA_DIR), ".runeya", "dbs", "overrides"),
14545
- join21(abDataDir, "dbs", "overrides")
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 = join21(overridesDir, "..", "encryption-key.json");
14537
+ const legacyKeyPath = join22(overridesDir, "..", "encryption-key.json");
14562
14538
  try {
14563
- await access2(legacyKeyPath);
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 readFile19(join21(abDataDir, "environments.json"), "utf-8"));
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 readFile19(join21(abDataDir, "services.json"), "utf-8"));
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(join21(overridesDir, file), sodiumKey, sodium);
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(join21(abDataDir, "environment-overrides.json"), flatOverrides);
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(join21(overridesDir, file), sodiumKey, sodium);
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(join21(abDataDir, "environments.json"), envsRaw);
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 rename15(overridesDir, migratedDir);
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 join22 } from "path";
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(join22(dataDir, ENCRYPTED_KEY_FILENAME2));
14712
+ await stat10(join23(dataDir, ENCRYPTED_KEY_FILENAME2));
14737
14713
  hasEncFile = true;
14738
14714
  } catch {
14739
14715
  }
14740
14716
  try {
14741
- await stat10(join22(dataDir, VERIFY_FILENAME2));
14717
+ await stat10(join23(dataDir, VERIFY_FILENAME2));
14742
14718
  hasVerify = true;
14743
14719
  } catch {
14744
14720
  }
14745
14721
  try {
14746
- await stat10(join22(dataDir, "projects.json"));
14722
+ await stat10(join23(dataDir, "projects.json"));
14747
14723
  hasData = true;
14748
14724
  } catch {
14749
14725
  }
14750
14726
  try {
14751
- await stat10(join22(dataDir, "project.json"));
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-WVK4WUI3.js.map
15941
+ //# sourceMappingURL=src-R6JOPDNL.js.map