@arcote.tech/arc-cli 0.7.31 → 0.8.0

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/dist/index.js CHANGED
@@ -23070,7 +23070,7 @@ var {
23070
23070
  // ../../node_modules/.bun/find-up@7.0.0/node_modules/find-up/index.js
23071
23071
  import path2 from "path";
23072
23072
 
23073
- // ../../node_modules/.bun/locate-path@7.2.0/node_modules/locate-path/index.js
23073
+ // ../../node_modules/.bun/find-up@7.0.0/node_modules/find-up/node_modules/locate-path/index.js
23074
23074
  import process2 from "process";
23075
23075
  import path from "path";
23076
23076
  import fs, { promises as fsPromises } from "fs";
@@ -31298,20 +31298,20 @@ ${colors3.yellow}Type declaration errors:${colors3.reset}`);
31298
31298
  }
31299
31299
 
31300
31300
  // src/platform/shared.ts
31301
- import { copyFileSync, existsSync as existsSync10, mkdirSync as mkdirSync9, readdirSync as readdirSync6, readFileSync as readFileSync11, writeFileSync as writeFileSync9 } from "fs";
31302
- import { dirname as dirname8, join as join12 } from "path";
31301
+ import { copyFileSync, existsSync as existsSync11, mkdirSync as mkdirSync10, readdirSync as readdirSync6, readFileSync as readFileSync12, writeFileSync as writeFileSync10 } from "fs";
31302
+ import { dirname as dirname8, join as join13 } from "path";
31303
31303
 
31304
31304
  // src/builder/module-builder.ts
31305
31305
  import { execSync } from "child_process";
31306
31306
  import {
31307
- existsSync as existsSync7,
31308
- mkdirSync as mkdirSync6,
31309
- readFileSync as readFileSync7,
31307
+ existsSync as existsSync8,
31308
+ mkdirSync as mkdirSync7,
31309
+ readFileSync as readFileSync8,
31310
31310
  readdirSync as readdirSync4,
31311
- rmSync as rmSync2,
31312
- writeFileSync as writeFileSync6
31311
+ rmSync as rmSync3,
31312
+ writeFileSync as writeFileSync7
31313
31313
  } from "fs";
31314
- import { basename as basename2, dirname as dirname6, join as join8, relative as relative3 } from "path";
31314
+ import { basename as basename3, dirname as dirname6, join as join9, relative as relative3 } from "path";
31315
31315
  import { builtinModules } from "module";
31316
31316
  init_i18n();
31317
31317
  init_compile();
@@ -31390,10 +31390,21 @@ var FRAMEWORK_PEERS = [...ARC_PEERS, ...REACT_PEERS];
31390
31390
  var SHELL_EXTERNALS = [
31391
31391
  ...FRAMEWORK_PEERS,
31392
31392
  "react/jsx-runtime",
31393
- "react/jsx-dev-runtime"
31393
+ "react-dom/client"
31394
31394
  ];
31395
31395
  var FRAMEWORK_PEER_SET = new Set(FRAMEWORK_PEERS);
31396
31396
  var SHELL_EXTERNAL_SET = new Set(SHELL_EXTERNALS);
31397
+ function shortNameOf(pkg) {
31398
+ if (pkg === "@arcote.tech/platform")
31399
+ return "platform";
31400
+ if (pkg.startsWith("@arcote.tech/"))
31401
+ return pkg.slice("@arcote.tech/".length);
31402
+ return pkg;
31403
+ }
31404
+
31405
+ // src/builder/peer-shell.ts
31406
+ import { existsSync as existsSync7, mkdirSync as mkdirSync6, readFileSync as readFileSync7, writeFileSync as writeFileSync6 } from "fs";
31407
+ import { basename as basename2, join as join8 } from "path";
31397
31408
 
31398
31409
  // src/builder/hash.ts
31399
31410
  import { existsSync as existsSync6, readFileSync as readFileSync6, readdirSync as readdirSync3, statSync } from "fs";
@@ -31474,6 +31485,88 @@ function mtimeOf(path4) {
31474
31485
  }
31475
31486
  }
31476
31487
 
31488
+ // src/builder/peer-shell.ts
31489
+ var RUNTIME_GLOBAL = "__ARC_RT";
31490
+ function peerVersion(rootDir, pkg) {
31491
+ try {
31492
+ const p = JSON.parse(readFileSync7(join8(rootDir, "node_modules", pkg, "package.json"), "utf-8"));
31493
+ return p.version ?? "0.0.0";
31494
+ } catch {
31495
+ return "0.0.0";
31496
+ }
31497
+ }
31498
+ async function namedExportsOf(specifier) {
31499
+ try {
31500
+ const mod = await import(specifier);
31501
+ return Object.keys(mod).filter((k) => k !== "default" && /^[A-Za-z_$][\w$]*$/.test(k));
31502
+ } catch {
31503
+ return [];
31504
+ }
31505
+ }
31506
+ function runtimeExposePrelude() {
31507
+ const lines = SHELL_EXTERNALS.map((spec, i) => `import * as __rt${i} from ${JSON.stringify(spec)};`);
31508
+ const assigns = SHELL_EXTERNALS.map((spec, i) => ` ${JSON.stringify(spec)}: (__rt${i}.default ?? __rt${i}),
31509
+ ${JSON.stringify(spec + "#ns")}: __rt${i},`);
31510
+ return lines.join(`
31511
+ `) + `
31512
+ globalThis.${RUNTIME_GLOBAL} = Object.assign(globalThis.${RUNTIME_GLOBAL} ?? {}, {
31513
+ ` + assigns.join(`
31514
+ `) + `
31515
+ });
31516
+ `;
31517
+ }
31518
+ async function buildPeerShell(rootDir, outDir, cache, noCache) {
31519
+ mkdirSync6(outDir, { recursive: true });
31520
+ const peers = {};
31521
+ for (const pkg of FRAMEWORK_PEERS)
31522
+ peers[pkg] = peerVersion(rootDir, pkg);
31523
+ const namedBySpec = {};
31524
+ for (const spec of SHELL_EXTERNALS)
31525
+ namedBySpec[spec] = await namedExportsOf(spec);
31526
+ const unitId = "peer-shell";
31527
+ const inputHash = sha256OfJson({
31528
+ peers,
31529
+ externals: SHELL_EXTERNALS,
31530
+ named: namedBySpec,
31531
+ mode: "globalThis",
31532
+ rev: 5
31533
+ });
31534
+ if (!noCache && isCacheHit(cache, unitId, inputHash)) {
31535
+ const cached = cache.units[unitId]?.outputHashes;
31536
+ if (cached?._manifest) {
31537
+ try {
31538
+ const m = JSON.parse(cached._manifest);
31539
+ const files = Object.values(m.imports).map((p) => basename2(p));
31540
+ if (files.every((f) => existsSync7(join8(outDir, f)))) {
31541
+ console.log(` \u2713 cached: ${unitId}`);
31542
+ return { ...m, cached: true };
31543
+ }
31544
+ } catch {}
31545
+ }
31546
+ }
31547
+ console.log(` building: ${unitId} (${SHELL_EXTERNALS.length} peers, globalThis)`);
31548
+ const imports = {};
31549
+ for (const specifier of SHELL_EXTERNALS) {
31550
+ const short = shortNameOf(specifier).replace(/\//g, "-");
31551
+ const named = namedBySpec[specifier] ?? [];
31552
+ const src = `const M = globalThis.${RUNTIME_GLOBAL}[${JSON.stringify(specifier)}];
31553
+ const N = globalThis.${RUNTIME_GLOBAL}[${JSON.stringify(specifier + "#ns")}] ?? M;
31554
+ export default M;
31555
+ ` + named.map((n) => `export const ${n} = N[${JSON.stringify(n)}];`).join(`
31556
+ `) + `
31557
+ `;
31558
+ const hash = sha256Hex(src).slice(0, 16);
31559
+ const finalName = `${short}.${hash}.js`;
31560
+ writeFileSync6(join8(outDir, finalName), src);
31561
+ imports[specifier] = `/shell/${finalName}`;
31562
+ }
31563
+ const manifest = { imports, peers, cached: false };
31564
+ updateCache(cache, unitId, inputHash, {
31565
+ outputHashes: { _manifest: JSON.stringify(manifest) }
31566
+ });
31567
+ return manifest;
31568
+ }
31569
+
31477
31570
  // src/builder/parallel.ts
31478
31571
  import { cpus } from "os";
31479
31572
  var DEFAULT_CONCURRENCY = Math.max(1, cpus().length - 1);
@@ -31527,10 +31620,10 @@ function isBuiltinSpec(spec) {
31527
31620
  function versionFromImporter(importer, name) {
31528
31621
  let dir = importer ? dirname6(importer) : "";
31529
31622
  while (dir && dir !== dirname6(dir)) {
31530
- const pj = join8(dir, "package.json");
31531
- if (existsSync7(pj)) {
31623
+ const pj = join9(dir, "package.json");
31624
+ if (existsSync8(pj)) {
31532
31625
  try {
31533
- const json = JSON.parse(readFileSync7(pj, "utf-8"));
31626
+ const json = JSON.parse(readFileSync8(pj, "utf-8"));
31534
31627
  const spec = json.dependencies?.[name] ?? json.peerDependencies?.[name] ?? json.devDependencies?.[name];
31535
31628
  if (typeof spec === "string")
31536
31629
  return spec;
@@ -31584,11 +31677,7 @@ function jsxDevShimPlugin() {
31584
31677
  namespace: "jsx-dev-shim"
31585
31678
  }));
31586
31679
  build2.onLoad({ filter: /.*/, namespace: "jsx-dev-shim" }, () => ({
31587
- contents: `import { jsx, jsxs, Fragment } from "react/jsx-runtime";
31588
- export const jsxDEV = jsx;
31589
- export const jsxsDEV = jsxs;
31590
- export { Fragment };
31591
- `,
31680
+ contents: `export { jsx as jsxDEV, jsxs as jsxsDEV, Fragment } from "react/jsx-runtime";`,
31592
31681
  loader: "ts"
31593
31682
  }));
31594
31683
  }
@@ -31601,13 +31690,13 @@ var SERVER_DEFINES = { ONLY_SERVER: "true", ONLY_BROWSER: "false", ONLY_CLIENT:
31601
31690
  var SERVER_ENTRY_FILE = "_server.js";
31602
31691
  var SERVER_EXTERNALS_FILE = "_externals.json";
31603
31692
  function discoverPackages(rootDir) {
31604
- const rootPkg = JSON.parse(readFileSync7(join8(rootDir, "package.json"), "utf-8"));
31693
+ const rootPkg = JSON.parse(readFileSync8(join9(rootDir, "package.json"), "utf-8"));
31605
31694
  const workspaceGlobs = rootPkg.workspaces ?? [];
31606
31695
  const results = [];
31607
31696
  for (const glob2 of workspaceGlobs) {
31608
31697
  const base2 = glob2.replace("/*", "");
31609
- const baseDir = join8(rootDir, base2);
31610
- if (!existsSync7(baseDir))
31698
+ const baseDir = join9(rootDir, base2);
31699
+ if (!existsSync8(baseDir))
31611
31700
  continue;
31612
31701
  let entries;
31613
31702
  try {
@@ -31616,25 +31705,25 @@ function discoverPackages(rootDir) {
31616
31705
  continue;
31617
31706
  }
31618
31707
  for (const entry of entries) {
31619
- const pkgPath = join8(baseDir, entry, "package.json");
31620
- if (!existsSync7(pkgPath))
31708
+ const pkgPath = join9(baseDir, entry, "package.json");
31709
+ if (!existsSync8(pkgPath))
31621
31710
  continue;
31622
- const pkg = JSON.parse(readFileSync7(pkgPath, "utf-8"));
31711
+ const pkg = JSON.parse(readFileSync8(pkgPath, "utf-8"));
31623
31712
  if (pkg.name?.startsWith("@arcote.tech/"))
31624
31713
  continue;
31625
- const pkgDir = join8(baseDir, entry);
31714
+ const pkgDir = join9(baseDir, entry);
31626
31715
  const candidates = [
31627
- join8(pkgDir, "src", "index.ts"),
31628
- join8(pkgDir, "src", "index.tsx"),
31629
- join8(pkgDir, "index.ts"),
31630
- join8(pkgDir, "index.tsx")
31716
+ join9(pkgDir, "src", "index.ts"),
31717
+ join9(pkgDir, "src", "index.tsx"),
31718
+ join9(pkgDir, "index.ts"),
31719
+ join9(pkgDir, "index.tsx")
31631
31720
  ];
31632
- const entrypoint = candidates.find((c) => existsSync7(c)) ?? null;
31721
+ const entrypoint = candidates.find((c) => existsSync8(c)) ?? null;
31633
31722
  if (!entrypoint)
31634
31723
  continue;
31635
31724
  results.push({
31636
31725
  name: pkg.name,
31637
- path: join8(baseDir, entry),
31726
+ path: join9(baseDir, entry),
31638
31727
  entrypoint,
31639
31728
  packageJson: pkg
31640
31729
  });
@@ -31663,7 +31752,7 @@ var sourceFilter = (rel) => {
31663
31752
  return true;
31664
31753
  };
31665
31754
  function pkgSourceHash(pkg) {
31666
- return sha256OfDir(join8(pkg.path, "src"), sourceFilter);
31755
+ return sha256OfDir(join9(pkg.path, "src"), sourceFilter);
31667
31756
  }
31668
31757
  function depVersionsHash(rootDir, pkg) {
31669
31758
  const peerDeps = Object.keys(pkg.packageJson.peerDependencies ?? {});
@@ -31676,7 +31765,7 @@ function depVersionsHash(rootDir, pkg) {
31676
31765
  }
31677
31766
  async function buildContextClient(pkg, rootDir, client, cache, noCache) {
31678
31767
  const unitId = `context-pkg:${pkg.name}:${client.name}`;
31679
- const outDir = join8(pkg.path, "dist", client.name);
31768
+ const outDir = join9(pkg.path, "dist", client.name);
31680
31769
  const inputHash = sha256OfJson({
31681
31770
  src: pkgSourceHash(pkg),
31682
31771
  pkg: pkg.packageJson,
@@ -31685,7 +31774,7 @@ async function buildContextClient(pkg, rootDir, client, cache, noCache) {
31685
31774
  target: client.target,
31686
31775
  defines: client.defines
31687
31776
  });
31688
- if (!noCache && isCacheHit(cache, unitId, inputHash, [join8(outDir, "main", "index.js")])) {
31777
+ if (!noCache && isCacheHit(cache, unitId, inputHash, [join9(outDir, "main", "index.js")])) {
31689
31778
  console.log(` \u2713 cached: ${pkg.name} (${client.name})`);
31690
31779
  return { pkgName: pkg.name, client: client.name, declarationErrors: [], cached: true };
31691
31780
  }
@@ -31695,7 +31784,7 @@ async function buildContextClient(pkg, rootDir, client, cache, noCache) {
31695
31784
  const externals = [...peerDeps, ...Object.keys(allDeps)];
31696
31785
  const result = await Bun.build({
31697
31786
  entrypoints: [pkg.entrypoint],
31698
- outdir: join8(outDir, "main"),
31787
+ outdir: join9(outDir, "main"),
31699
31788
  target: client.target,
31700
31789
  format: "esm",
31701
31790
  naming: "index.[ext]",
@@ -31765,7 +31854,7 @@ async function buildContextPackages(rootDir, packages, cache, noCache) {
31765
31854
  }
31766
31855
  async function buildServerApp(rootDir, serverDir, packages, cache, noCache) {
31767
31856
  const contexts = packages.filter((p) => isContextPackage(p.packageJson));
31768
- mkdirSync6(serverDir, { recursive: true });
31857
+ mkdirSync7(serverDir, { recursive: true });
31769
31858
  const srcByName = new Map(packages.map((p) => [p.name, p.entrypoint]));
31770
31859
  const externalSet = new Set(FRAMEWORK_PEERS);
31771
31860
  for (const p of packages) {
@@ -31786,8 +31875,8 @@ async function buildServerApp(rootDir, serverDir, packages, cache, noCache) {
31786
31875
  defines: SERVER_DEFINES,
31787
31876
  sourcemap: "linked"
31788
31877
  });
31789
- const entryFileAbs = join8(serverDir, SERVER_ENTRY_FILE);
31790
- const externalsFileAbs = join8(serverDir, SERVER_EXTERNALS_FILE);
31878
+ const entryFileAbs = join9(serverDir, SERVER_ENTRY_FILE);
31879
+ const externalsFileAbs = join9(serverDir, SERVER_EXTERNALS_FILE);
31791
31880
  if (!noCache && isCacheHit(cache, unitId, inputHash, [entryFileAbs])) {
31792
31881
  console.log(` \u2713 cached: ${unitId}`);
31793
31882
  return {
@@ -31799,13 +31888,13 @@ async function buildServerApp(rootDir, serverDir, packages, cache, noCache) {
31799
31888
  console.log(` building: ${unitId} (${contexts.length} server modules)`);
31800
31889
  for (const f of readdirSync4(serverDir)) {
31801
31890
  if (f.endsWith(".js") || f.endsWith(".js.map")) {
31802
- rmSync2(join8(serverDir, f), { force: true });
31891
+ rmSync3(join9(serverDir, f), { force: true });
31803
31892
  }
31804
31893
  }
31805
- const tmpDir = join8(serverDir, "_entries");
31806
- mkdirSync6(tmpDir, { recursive: true });
31807
- const entrySrc = join8(tmpDir, SERVER_ENTRY_FILE.replace(/\.js$/, ".ts"));
31808
- writeFileSync6(entrySrc, contexts.map((p) => `import "${p.name}";`).join(`
31894
+ const tmpDir = join9(serverDir, "_entries");
31895
+ mkdirSync7(tmpDir, { recursive: true });
31896
+ const entrySrc = join9(tmpDir, SERVER_ENTRY_FILE.replace(/\.js$/, ".ts"));
31897
+ writeFileSync7(entrySrc, contexts.map((p) => `import "${p.name}";`).join(`
31809
31898
  `) + `
31810
31899
  `);
31811
31900
  const recorded = new Map;
@@ -31828,7 +31917,7 @@ async function buildServerApp(rootDir, serverDir, packages, cache, noCache) {
31828
31917
  define: SERVER_DEFINES
31829
31918
  });
31830
31919
  } finally {
31831
- rmSync2(tmpDir, { recursive: true, force: true });
31920
+ rmSync3(tmpDir, { recursive: true, force: true });
31832
31921
  }
31833
31922
  if (!result.success) {
31834
31923
  for (const log2 of result.logs)
@@ -31839,31 +31928,33 @@ async function buildServerApp(rootDir, serverDir, packages, cache, noCache) {
31839
31928
  if (!entryOut) {
31840
31929
  throw new Error("Server app build: entry not found in outputs");
31841
31930
  }
31842
- if (basename2(entryOut.path) !== SERVER_ENTRY_FILE) {
31843
- throw new Error(`Server app build: unexpected entry name ${basename2(entryOut.path)} (wanted ${SERVER_ENTRY_FILE})`);
31931
+ if (basename3(entryOut.path) !== SERVER_ENTRY_FILE) {
31932
+ throw new Error(`Server app build: unexpected entry name ${basename3(entryOut.path)} (wanted ${SERVER_ENTRY_FILE})`);
31844
31933
  }
31845
31934
  const externals = [...recorded].filter(([name]) => !FRAMEWORK_PEER_SET.has(name)).map(([name, version]) => ({ name, version })).sort((a, b) => a.name.localeCompare(b.name));
31846
- writeFileSync6(externalsFileAbs, JSON.stringify(externals, null, 2) + `
31935
+ writeFileSync7(externalsFileAbs, JSON.stringify(externals, null, 2) + `
31847
31936
  `);
31848
31937
  const outputHash = sha256OfDir(serverDir);
31849
31938
  updateCache(cache, unitId, inputHash, { outputHash });
31850
31939
  return { entryFile: SERVER_ENTRY_FILE, cached: false, externals };
31851
31940
  }
31852
31941
  function readServerExternals(path4) {
31853
- if (!existsSync7(path4))
31942
+ if (!existsSync8(path4))
31854
31943
  return [];
31855
31944
  try {
31856
- const parsed = JSON.parse(readFileSync7(path4, "utf-8"));
31945
+ const parsed = JSON.parse(readFileSync8(path4, "utf-8"));
31857
31946
  return Array.isArray(parsed) ? parsed : [];
31858
31947
  } catch {
31859
31948
  return [];
31860
31949
  }
31861
31950
  }
31862
- async function buildBrowserApp(rootDir, outDir, plan, cache, noCache, i18nCollector) {
31863
- mkdirSync6(outDir, { recursive: true });
31951
+ async function buildBrowserApp(rootDir, outDir, plan, cache, noCache, i18nCollector, opts) {
31952
+ const federated = opts?.federated ?? false;
31953
+ const exposeRuntime = opts?.exposeRuntime ?? false;
31954
+ mkdirSync7(outDir, { recursive: true });
31864
31955
  const publicMembers = plan.groups.get("public") ?? [];
31865
31956
  const protectedGroups = plan.chunks.filter((c) => c !== "public").map((c) => ({ name: c, members: plan.groups.get(c) ?? [] })).filter((g) => g.members.length > 0);
31866
- const unitId = "browser-app";
31957
+ const unitId = opts?.unitId ?? "browser-app";
31867
31958
  const allMembers = [];
31868
31959
  for (const m of publicMembers) {
31869
31960
  allMembers.push({ name: m.pkg.name, group: "public", srcHash: pkgSourceHash(m.pkg) });
@@ -31879,7 +31970,11 @@ async function buildBrowserApp(rootDir, outDir, plan, cache, noCache, i18nCollec
31879
31970
  "initial",
31880
31971
  ...protectedGroups.map((g) => g.name).sort()
31881
31972
  ],
31882
- define: { ONLY_SERVER: "false", ONLY_BROWSER: "true", ONLY_CLIENT: "true" }
31973
+ define: { ONLY_SERVER: "false", ONLY_BROWSER: "true", ONLY_CLIENT: "true" },
31974
+ federated,
31975
+ exposeRuntime,
31976
+ externals: federated ? [...SHELL_EXTERNALS] : [],
31977
+ builderRev: 3
31883
31978
  });
31884
31979
  if (!noCache && isCacheHit(cache, unitId, inputHash)) {
31885
31980
  const cached = cache.units[unitId]?.outputHashes;
@@ -31891,7 +31986,7 @@ async function buildBrowserApp(rootDir, outDir, plan, cache, noCache, i18nCollec
31891
31986
  ...Object.values(m.groups).map((g) => g.file),
31892
31987
  ...m.sharedChunks
31893
31988
  ];
31894
- if (allFiles.every((f) => existsSync7(join8(outDir, f)))) {
31989
+ if (allFiles.every((f) => existsSync8(join9(outDir, f)))) {
31895
31990
  console.log(` \u2713 cached: ${unitId}`);
31896
31991
  return { ...m, cached: true };
31897
31992
  }
@@ -31899,25 +31994,26 @@ async function buildBrowserApp(rootDir, outDir, plan, cache, noCache, i18nCollec
31899
31994
  }
31900
31995
  }
31901
31996
  console.log(` building: ${unitId} (initial: ${publicMembers.length} modules, groups: ${protectedGroups.map((g) => `${g.name}=${g.members.length}`).join(",") || "none"})`);
31902
- if (existsSync7(outDir)) {
31997
+ if (existsSync8(outDir)) {
31903
31998
  for (const f of readdirSync4(outDir)) {
31904
31999
  if (f.endsWith(".js"))
31905
- rmSync2(join8(outDir, f), { force: true });
32000
+ rmSync3(join9(outDir, f), { force: true });
31906
32001
  }
31907
32002
  }
31908
- const tmpDir = join8(outDir, "_entries");
31909
- mkdirSync6(tmpDir, { recursive: true });
32003
+ const tmpDir = join9(outDir, "_entries");
32004
+ mkdirSync7(tmpDir, { recursive: true });
31910
32005
  const importLines = (pkgs) => pkgs.map((m) => `import "${m.pkg.name}";`).join(`
31911
32006
  `);
31912
- const initialEntry = join8(tmpDir, "initial.ts");
31913
- writeFileSync6(initialEntry, `${importLines(publicMembers)}
32007
+ const initialEntry = join9(tmpDir, "initial.ts");
32008
+ writeFileSync7(initialEntry, (exposeRuntime ? runtimeExposePrelude() + `
32009
+ ` : "") + `${importLines(publicMembers)}
31914
32010
  export { startApp } from "@arcote.tech/platform";
31915
32011
  `);
31916
32012
  const entryPaths = [initialEntry];
31917
32013
  const groupModuleMap = new Map;
31918
32014
  for (const g of protectedGroups) {
31919
- const entry = join8(tmpDir, `${g.name}.ts`);
31920
- writeFileSync6(entry, `${importLines(g.members)}
32015
+ const entry = join9(tmpDir, `${g.name}.ts`);
32016
+ writeFileSync7(entry, `${importLines(g.members)}
31921
32017
  `);
31922
32018
  entryPaths.push(entry);
31923
32019
  groupModuleMap.set(g.name, g.members.map((m) => m.moduleName));
@@ -31930,15 +32026,15 @@ export { startApp } from "@arcote.tech/platform";
31930
32026
  allMemberPkgs.set(m.pkg.name, m.pkg);
31931
32027
  const patchedPkgJsons = [];
31932
32028
  for (const pkg of allMemberPkgs.values()) {
31933
- const pkgJsonPath = join8(pkg.path, "package.json");
31934
- if (!existsSync7(pkgJsonPath))
32029
+ const pkgJsonPath = join9(pkg.path, "package.json");
32030
+ if (!existsSync8(pkgJsonPath))
31935
32031
  continue;
31936
- const original = readFileSync7(pkgJsonPath, "utf-8");
32032
+ const original = readFileSync8(pkgJsonPath, "utf-8");
31937
32033
  const parsed = JSON.parse(original);
31938
32034
  if (parsed.sideEffects === true)
31939
32035
  continue;
31940
32036
  parsed.sideEffects = true;
31941
- writeFileSync6(pkgJsonPath, JSON.stringify(parsed, null, 2) + `
32037
+ writeFileSync7(pkgJsonPath, JSON.stringify(parsed, null, 2) + `
31942
32038
  `);
31943
32039
  patchedPkgJsons.push({ path: pkgJsonPath, original });
31944
32040
  }
@@ -31950,9 +32046,9 @@ export { startApp } from "@arcote.tech/platform";
31950
32046
  splitting: true,
31951
32047
  format: "esm",
31952
32048
  target: "browser",
31953
- external: [],
32049
+ external: federated ? [...SHELL_EXTERNALS] : [],
31954
32050
  plugins: [
31955
- singleReactPlugin(rootDir),
32051
+ ...federated ? [] : [singleReactPlugin(rootDir)],
31956
32052
  jsxDevShimPlugin(),
31957
32053
  i18nExtractPlugin(i18nCollector, rootDir)
31958
32054
  ],
@@ -31966,9 +32062,9 @@ export { startApp } from "@arcote.tech/platform";
31966
32062
  });
31967
32063
  } finally {
31968
32064
  for (const p of patchedPkgJsons)
31969
- writeFileSync6(p.path, p.original);
32065
+ writeFileSync7(p.path, p.original);
31970
32066
  }
31971
- rmSync2(tmpDir, { recursive: true, force: true });
32067
+ rmSync3(tmpDir, { recursive: true, force: true });
31972
32068
  if (!result.success) {
31973
32069
  for (const log2 of result.logs)
31974
32070
  console.error(log2);
@@ -31979,16 +32075,16 @@ export { startApp } from "@arcote.tech/platform";
31979
32075
  const groups = {};
31980
32076
  const sharedChunks = [];
31981
32077
  for (const out of result.outputs) {
31982
- const name = basename2(out.path);
32078
+ const name = basename3(out.path);
31983
32079
  if (out.kind === "entry-point") {
31984
- const bytes = readFileSync7(out.path);
32080
+ const bytes = readFileSync8(out.path);
31985
32081
  const hash = sha256Hex(bytes).slice(0, 16);
31986
32082
  const stem = name.replace(/\.js$/, "");
31987
32083
  const finalName = `${stem}.${hash}.js`;
31988
- const finalPath = join8(outDir, finalName);
31989
- rmSync2(finalPath, { force: true });
31990
- writeFileSync6(finalPath, bytes);
31991
- rmSync2(out.path, { force: true });
32084
+ const finalPath = join9(outDir, finalName);
32085
+ rmSync3(finalPath, { force: true });
32086
+ writeFileSync7(finalPath, bytes);
32087
+ rmSync3(out.path, { force: true });
31992
32088
  if (stem === "initial") {
31993
32089
  initialFile = finalName;
31994
32090
  initialHash = hash;
@@ -32018,21 +32114,21 @@ export { startApp } from "@arcote.tech/platform";
32018
32114
  return manifest;
32019
32115
  }
32020
32116
  async function buildTranslations(rootDir, arcDir, cache, noCache) {
32021
- const localesDir = join8(rootDir, "locales");
32022
- if (!existsSync7(localesDir))
32117
+ const localesDir = join9(rootDir, "locales");
32118
+ if (!existsSync8(localesDir))
32023
32119
  return;
32024
32120
  const unitId = "translations";
32025
- const poFiles = readdirSync4(localesDir).filter((f) => f.endsWith(".po")).map((f) => join8(localesDir, f));
32121
+ const poFiles = readdirSync4(localesDir).filter((f) => f.endsWith(".po")).map((f) => join9(localesDir, f));
32026
32122
  if (poFiles.length === 0)
32027
32123
  return;
32028
32124
  const inputHash = sha256OfFiles(poFiles);
32029
- if (!noCache && isCacheHit(cache, unitId, inputHash, [join8(arcDir, "locales")])) {
32125
+ if (!noCache && isCacheHit(cache, unitId, inputHash, [join9(arcDir, "locales")])) {
32030
32126
  console.log(` \u2713 cached: translations`);
32031
32127
  return;
32032
32128
  }
32033
32129
  console.log(` building: translations (${poFiles.length} catalog(s))`);
32034
- compileAllCatalogs(localesDir, join8(arcDir, "locales"));
32035
- const jsonFiles = readdirSync4(join8(arcDir, "locales")).filter((f) => f.endsWith(".json")).map((f) => join8(arcDir, "locales", f));
32130
+ compileAllCatalogs(localesDir, join9(arcDir, "locales"));
32131
+ const jsonFiles = readdirSync4(join9(arcDir, "locales")).filter((f) => f.endsWith(".json")).map((f) => join9(arcDir, "locales", f));
32036
32132
  const outputHash = sha256OfFiles(jsonFiles);
32037
32133
  updateCache(cache, unitId, inputHash, { outputHash });
32038
32134
  }
@@ -32099,10 +32195,10 @@ var TAILWIND_INPUT_TEMPLATE = (rootRel) => `@import "tailwindcss";
32099
32195
  }
32100
32196
  `;
32101
32197
  async function buildStyles(rootDir, arcDir, packages, themePath, cache, noCache) {
32102
- mkdirSync6(arcDir, { recursive: true });
32103
- const inputCss = join8(arcDir, "_input.css");
32104
- const outputCss = join8(arcDir, "styles.css");
32105
- const themeOutput = join8(arcDir, "theme.css");
32198
+ mkdirSync7(arcDir, { recursive: true });
32199
+ const inputCss = join9(arcDir, "_input.css");
32200
+ const outputCss = join9(arcDir, "styles.css");
32201
+ const themeOutput = join9(arcDir, "theme.css");
32106
32202
  const rootRel = relative3(arcDir, rootDir).replace(/\\/g, "/");
32107
32203
  const inputCssContent = TAILWIND_INPUT_TEMPLATE(rootRel);
32108
32204
  const tsxFilter = (rel) => {
@@ -32114,15 +32210,15 @@ async function buildStyles(rootDir, arcDir, packages, themePath, cache, noCache)
32114
32210
  };
32115
32211
  const wsHashes = {};
32116
32212
  for (const p of packages) {
32117
- wsHashes[p.name] = sha256OfDir(join8(p.path, "src"), tsxFilter);
32213
+ wsHashes[p.name] = sha256OfDir(join9(p.path, "src"), tsxFilter);
32118
32214
  }
32119
- const platformSrc = join8(rootDir, "node_modules", "@arcote.tech", "platform", "src");
32120
- const arcDsSrc = join8(rootDir, "node_modules", "@arcote.tech", "arc-ds", "src");
32215
+ const platformSrc = join9(rootDir, "node_modules", "@arcote.tech", "platform", "src");
32216
+ const arcDsSrc = join9(rootDir, "node_modules", "@arcote.tech", "arc-ds", "src");
32121
32217
  const frameworkHashes = {
32122
32218
  platform: sha256OfDir(platformSrc, tsxFilter),
32123
32219
  arcDs: sha256OfDir(arcDsSrc, tsxFilter)
32124
32220
  };
32125
- const themeContent = themePath && existsSync7(join8(rootDir, themePath)) ? readFileSync7(join8(rootDir, themePath)) : null;
32221
+ const themeContent = themePath && existsSync8(join9(rootDir, themePath)) ? readFileSync8(join9(rootDir, themePath)) : null;
32126
32222
  const themeHash = themeContent ? sha256Hex(themeContent) : null;
32127
32223
  const unitId = "styles";
32128
32224
  const inputHash = sha256OfJson({
@@ -32139,16 +32235,16 @@ async function buildStyles(rootDir, arcDir, packages, themePath, cache, noCache)
32139
32235
  return;
32140
32236
  }
32141
32237
  console.log(` building: styles`);
32142
- writeFileSync6(inputCss, inputCssContent);
32238
+ writeFileSync7(inputCss, inputCssContent);
32143
32239
  execSync(`bunx @tailwindcss/cli -i ${inputCss} -o ${outputCss} --minify`, {
32144
32240
  cwd: rootDir,
32145
32241
  stdio: "inherit"
32146
32242
  });
32147
32243
  if (themePath && themeContent) {
32148
- writeFileSync6(themeOutput, themeContent);
32244
+ writeFileSync7(themeOutput, themeContent);
32149
32245
  }
32150
32246
  const outFiles = [outputCss];
32151
- if (themePath && existsSync7(themeOutput))
32247
+ if (themePath && existsSync8(themeOutput))
32152
32248
  outFiles.push(themeOutput);
32153
32249
  const outputHash = sha256OfFiles(outFiles);
32154
32250
  updateCache(cache, unitId, inputHash, { outputHash });
@@ -32157,20 +32253,20 @@ async function buildStyles(rootDir, arcDir, packages, themePath, cache, noCache)
32157
32253
  // src/builder/access-extractor.ts
32158
32254
  var {spawn: spawn2 } = globalThis.Bun;
32159
32255
  import {
32160
- existsSync as existsSync8,
32161
- mkdirSync as mkdirSync7,
32162
- readFileSync as readFileSync8,
32256
+ existsSync as existsSync9,
32257
+ mkdirSync as mkdirSync8,
32258
+ readFileSync as readFileSync9,
32163
32259
  unlinkSync as unlinkSync2,
32164
- writeFileSync as writeFileSync7
32260
+ writeFileSync as writeFileSync8
32165
32261
  } from "fs";
32166
- import { join as join9 } from "path";
32262
+ import { join as join10 } from "path";
32167
32263
  async function extractAccessMap(rootDir, serverBundlePath) {
32168
- const serverBundles = existsSync8(serverBundlePath) ? [{ name: "server", path: serverBundlePath }] : [];
32169
- const workerDir = join9(rootDir, ".arc", ".tmp");
32170
- mkdirSync7(workerDir, { recursive: true });
32171
- const workerPath = join9(workerDir, `access-extractor-${Date.now()}.mjs`);
32172
- const outPath = join9(workerDir, `access-${Date.now()}.json`);
32173
- writeFileSync7(workerPath, WORKER_SOURCE);
32264
+ const serverBundles = existsSync9(serverBundlePath) ? [{ name: "server", path: serverBundlePath }] : [];
32265
+ const workerDir = join10(rootDir, ".arc", ".tmp");
32266
+ mkdirSync8(workerDir, { recursive: true });
32267
+ const workerPath = join10(workerDir, `access-extractor-${Date.now()}.mjs`);
32268
+ const outPath = join10(workerDir, `access-${Date.now()}.json`);
32269
+ writeFileSync8(workerPath, WORKER_SOURCE);
32174
32270
  try {
32175
32271
  const proc2 = spawn2({
32176
32272
  cmd: ["bun", "run", workerPath],
@@ -32187,7 +32283,7 @@ async function extractAccessMap(rootDir, serverBundlePath) {
32187
32283
  if (exit !== 0) {
32188
32284
  throw new Error(`access-extractor subprocess exited with ${exit}`);
32189
32285
  }
32190
- return JSON.parse(readFileSync8(outPath, "utf-8"));
32286
+ return JSON.parse(readFileSync9(outPath, "utf-8"));
32191
32287
  } finally {
32192
32288
  try {
32193
32289
  unlinkSync2(workerPath);
@@ -32224,23 +32320,35 @@ for (const { name, path } of bundles) {
32224
32320
  }
32225
32321
  }
32226
32322
 
32227
- const result = {};
32228
- for (const [name, access] of platform.getAllModuleAccess()) {
32229
- result[name] = {
32230
- rules: (access.rules ?? []).map((r) => ({
32323
+ const access = {};
32324
+ for (const [name, a] of platform.getAllModuleAccess()) {
32325
+ access[name] = {
32326
+ rules: (a.rules ?? []).map((r) => ({
32231
32327
  token: { name: r.token?.name ?? "" },
32232
32328
  hasCheck: typeof r.check === "function",
32233
32329
  })),
32234
32330
  };
32235
32331
  }
32236
32332
 
32333
+ const exposure = {};
32334
+ const getExposure = platform.getAllModuleExposure;
32335
+ if (typeof getExposure === "function") {
32336
+ for (const [name, e] of getExposure()) {
32337
+ exposure[name] = {
32338
+ exposure: e.exposure,
32339
+ permissions: e.permissions ?? [],
32340
+ slots: e.slots ?? [],
32341
+ };
32342
+ }
32343
+ }
32344
+
32237
32345
  const { writeFileSync } = await import("node:fs");
32238
- writeFileSync(out, JSON.stringify(result, null, 2) + "\\n");
32346
+ writeFileSync(out, JSON.stringify({ access, exposure }, null, 2) + "\\n");
32239
32347
  `.trim();
32240
32348
 
32241
32349
  // src/builder/chunk-planner.ts
32242
- import { readFileSync as readFileSync9, readdirSync as readdirSync5, statSync as statSync2 } from "fs";
32243
- import { basename as basename3, join as join10 } from "path";
32350
+ import { readFileSync as readFileSync10, readdirSync as readdirSync5, statSync as statSync2 } from "fs";
32351
+ import { basename as basename4, join as join11 } from "path";
32244
32352
  var PUBLIC_CHUNK = "public";
32245
32353
  function planChunks(packages, accessMap) {
32246
32354
  const assignments = new Map;
@@ -32253,7 +32361,7 @@ function planChunks(packages, accessMap) {
32253
32361
  pkg,
32254
32362
  chunk,
32255
32363
  moduleName,
32256
- safeName: basename3(pkg.path)
32364
+ safeName: basename4(pkg.path)
32257
32365
  };
32258
32366
  assignments.set(moduleName, entry);
32259
32367
  const bucket = groups.get(chunk);
@@ -32283,7 +32391,7 @@ function collectModuleNames(dir, acc) {
32283
32391
  for (const e of entries) {
32284
32392
  if (e === "node_modules" || e === "dist")
32285
32393
  continue;
32286
- const full = join10(dir, e);
32394
+ const full = join11(dir, e);
32287
32395
  let isDir = false;
32288
32396
  try {
32289
32397
  isDir = statSync2(full).isDirectory();
@@ -32293,7 +32401,7 @@ function collectModuleNames(dir, acc) {
32293
32401
  if (isDir) {
32294
32402
  collectModuleNames(full, acc);
32295
32403
  } else if (e.endsWith(".ts") || e.endsWith(".tsx")) {
32296
- const src = readFileSync9(full, "utf-8");
32404
+ const src = readFileSync10(full, "utf-8");
32297
32405
  MODULE_CALL.lastIndex = 0;
32298
32406
  let m;
32299
32407
  while (m = MODULE_CALL.exec(src))
@@ -32305,7 +32413,7 @@ function assertOneModulePerPackage(packages) {
32305
32413
  const offenders = [];
32306
32414
  for (const pkg of packages) {
32307
32415
  const names = new Set;
32308
- collectModuleNames(join10(pkg.path, "src"), names);
32416
+ collectModuleNames(join11(pkg.path, "src"), names);
32309
32417
  if (names.size > 1) {
32310
32418
  offenders.push({ pkg: pkg.name, modules: [...names].sort() });
32311
32419
  }
@@ -32337,11 +32445,11 @@ function resolveChunk(moduleName, access) {
32337
32445
 
32338
32446
  // src/builder/dependency-collector.ts
32339
32447
  import { createHash } from "crypto";
32340
- import { existsSync as existsSync9, mkdirSync as mkdirSync8, readFileSync as readFileSync10, writeFileSync as writeFileSync8 } from "fs";
32341
- import { basename as basename4, dirname as dirname7, join as join11 } from "path";
32448
+ import { existsSync as existsSync10, mkdirSync as mkdirSync9, readFileSync as readFileSync11, writeFileSync as writeFileSync9 } from "fs";
32449
+ import { basename as basename5, dirname as dirname7, join as join12 } from "path";
32342
32450
  import { fileURLToPath as fileURLToPath6 } from "url";
32343
32451
  function collectFrameworkDeps(arcDir, rootDir, packages, sharedDeps = [], serverExternals = []) {
32344
- mkdirSync8(arcDir, { recursive: true });
32452
+ mkdirSync9(arcDir, { recursive: true });
32345
32453
  const versions = resolveFrameworkVersions(rootDir, packages);
32346
32454
  mergeServerExternals(versions, rootDir, packages, serverExternals);
32347
32455
  for (const { name, version } of sharedDeps) {
@@ -32350,7 +32458,7 @@ function collectFrameworkDeps(arcDir, rootDir, packages, sharedDeps = [], server
32350
32458
  try {
32351
32459
  const cliDir = dirname7(fileURLToPath6(import.meta.url));
32352
32460
  const arcOtelPkgPath = Bun.resolveSync("@arcote.tech/arc-otel/package.json", cliDir);
32353
- const arcOtelPkg = JSON.parse(readFileSync10(arcOtelPkgPath, "utf-8"));
32461
+ const arcOtelPkg = JSON.parse(readFileSync11(arcOtelPkgPath, "utf-8"));
32354
32462
  for (const [name, spec] of Object.entries(arcOtelPkg.dependencies ?? {})) {
32355
32463
  if (name.startsWith("@opentelemetry/")) {
32356
32464
  versions[name] = spec;
@@ -32361,10 +32469,10 @@ function collectFrameworkDeps(arcDir, rootDir, packages, sharedDeps = [], server
32361
32469
  console.warn(`[arc-otel] could not resolve @arcote.tech/arc-otel \u2014 image will run without telemetry deps: ${e.message}`);
32362
32470
  }
32363
32471
  let rootArc;
32364
- const rootPkgPath = join11(rootDir, "package.json");
32365
- if (existsSync9(rootPkgPath)) {
32472
+ const rootPkgPath = join12(rootDir, "package.json");
32473
+ if (existsSync10(rootPkgPath)) {
32366
32474
  try {
32367
- const rootPkg = JSON.parse(readFileSync10(rootPkgPath, "utf-8"));
32475
+ const rootPkg = JSON.parse(readFileSync11(rootPkgPath, "utf-8"));
32368
32476
  if (rootPkg.arc && typeof rootPkg.arc === "object")
32369
32477
  rootArc = rootPkg.arc;
32370
32478
  } catch {}
@@ -32376,11 +32484,11 @@ function collectFrameworkDeps(arcDir, rootDir, packages, sharedDeps = [], server
32376
32484
  dependencies: versions,
32377
32485
  ...rootArc ? { arc: rootArc } : {}
32378
32486
  };
32379
- const manifestPath = join11(arcDir, "package.json");
32380
- writeFileSync8(manifestPath, JSON.stringify(manifest, null, 2) + `
32487
+ const manifestPath = join12(arcDir, "package.json");
32488
+ writeFileSync9(manifestPath, JSON.stringify(manifest, null, 2) + `
32381
32489
  `);
32382
- const hash = sha256Hex2(readFileSync10(manifestPath));
32383
- writeFileSync8(join11(arcDir, ".deps-hash"), hash + `
32490
+ const hash = sha256Hex2(readFileSync11(manifestPath));
32491
+ writeFileSync9(join12(arcDir, ".deps-hash"), hash + `
32384
32492
  `);
32385
32493
  return { hash, manifestPath };
32386
32494
  }
@@ -32388,7 +32496,7 @@ function sha256Hex2(bytes) {
32388
32496
  return createHash("sha256").update(bytes).digest("hex");
32389
32497
  }
32390
32498
  function resolveFrameworkVersions(rootDir, packages) {
32391
- const rootPkg = JSON.parse(readFileSync10(join11(rootDir, "package.json"), "utf-8"));
32499
+ const rootPkg = JSON.parse(readFileSync11(join12(rootDir, "package.json"), "utf-8"));
32392
32500
  const rootDeps = rootPkg.dependencies ?? {};
32393
32501
  const rootDevDeps = rootPkg.devDependencies ?? {};
32394
32502
  const required = new Set(FRAMEWORK_PEERS);
@@ -32419,7 +32527,7 @@ function findWorkspaceSpec(packages, name) {
32419
32527
  return;
32420
32528
  }
32421
32529
  function mergeServerExternals(versions, rootDir, packages, externals) {
32422
- const rootPkg = JSON.parse(readFileSync10(join11(rootDir, "package.json"), "utf-8"));
32530
+ const rootPkg = JSON.parse(readFileSync11(join12(rootDir, "package.json"), "utf-8"));
32423
32531
  const rootDeps = rootPkg.dependencies ?? {};
32424
32532
  const rootDevDeps = rootPkg.devDependencies ?? {};
32425
32533
  for (const { name, version } of externals) {
@@ -32447,9 +32555,9 @@ function resolveWorkspace() {
32447
32555
  process.exit(1);
32448
32556
  }
32449
32557
  const rootDir = dirname8(packageJsonPath);
32450
- const rootPkg = JSON.parse(readFileSync11(packageJsonPath, "utf-8"));
32558
+ const rootPkg = JSON.parse(readFileSync12(packageJsonPath, "utf-8"));
32451
32559
  const appName = rootPkg.name ?? "Arc App";
32452
- const arcDir = join12(rootDir, ".arc", "platform");
32560
+ const arcDir = join13(rootDir, ".arc", "platform");
32453
32561
  log2("Scanning workspaces...");
32454
32562
  const packages = discoverPackages(rootDir);
32455
32563
  if (packages.length > 0) {
@@ -32459,10 +32567,10 @@ function resolveWorkspace() {
32459
32567
  }
32460
32568
  let manifest;
32461
32569
  for (const name of ["manifest.json", "manifest.webmanifest"]) {
32462
- const manifestPath = join12(rootDir, name);
32463
- if (existsSync10(manifestPath)) {
32570
+ const manifestPath = join13(rootDir, name);
32571
+ if (existsSync11(manifestPath)) {
32464
32572
  try {
32465
- const data = JSON.parse(readFileSync11(manifestPath, "utf-8"));
32573
+ const data = JSON.parse(readFileSync12(manifestPath, "utf-8"));
32466
32574
  const icons = data.icons;
32467
32575
  manifest = {
32468
32576
  path: manifestPath,
@@ -32481,9 +32589,9 @@ function resolveWorkspace() {
32481
32589
  rootPkg,
32482
32590
  appName,
32483
32591
  arcDir,
32484
- browserDir: join12(arcDir, "browser"),
32485
- assetsDir: join12(arcDir, "assets"),
32486
- publicDir: join12(rootDir, "public"),
32592
+ browserDir: join13(arcDir, "browser"),
32593
+ assetsDir: join13(arcDir, "assets"),
32594
+ publicDir: join13(rootDir, "public"),
32487
32595
  packages,
32488
32596
  manifest
32489
32597
  };
@@ -32492,73 +32600,120 @@ async function buildAll(ws, opts = {}) {
32492
32600
  const cache = loadBuildCache(ws.arcDir);
32493
32601
  const noCache = opts.noCache ?? false;
32494
32602
  const themePath = ws.rootPkg.arc?.theme;
32603
+ const federation = getFederationConfig(ws);
32495
32604
  log2(`Building (concurrency parallel${noCache ? ", no-cache" : ""})...`);
32496
32605
  assertOneModulePerPackage(ws.packages);
32497
32606
  await buildContextPackages(ws.rootDir, ws.packages, cache, noCache);
32498
- const serverDir = join12(ws.arcDir, "server");
32607
+ const serverDir = join13(ws.arcDir, "server");
32499
32608
  const { entryFile: serverEntry, externals: serverExternals } = await buildServerApp(ws.rootDir, serverDir, ws.packages, cache, noCache);
32500
- const accessMap = await extractAccessMap(ws.rootDir, join12(serverDir, serverEntry));
32501
- mkdirSync9(ws.arcDir, { recursive: true });
32502
- writeFileSync9(join12(ws.arcDir, "access.json"), JSON.stringify(accessMap, null, 2) + `
32609
+ const accessMap = await extractAccessMap(ws.rootDir, join13(serverDir, serverEntry));
32610
+ mkdirSync10(ws.arcDir, { recursive: true });
32611
+ writeFileSync10(join13(ws.arcDir, "access.json"), JSON.stringify(accessMap.access, null, 2) + `
32503
32612
  `);
32504
- const plan = planChunks(ws.packages, accessMap);
32613
+ const isHost = Boolean(federation?.host);
32614
+ const moduleNameOf2 = (name) => name.split("/").pop() ?? name;
32615
+ const exposureOf = (pkgName) => accessMap.exposure[moduleNameOf2(pkgName)]?.exposure ?? "local";
32616
+ const localPackages = isHost ? ws.packages : ws.packages.filter((p) => exposureOf(p.name) !== "external");
32617
+ const fedPackages = ws.packages.filter((p) => ["external", "both"].includes(exposureOf(p.name)));
32618
+ const hasFederatedModules = fedPackages.length > 0;
32619
+ const needsPeerShell = isHost || hasFederatedModules;
32620
+ log2();
32621
+ const plan = planChunks(localPackages, accessMap.access);
32505
32622
  ok(`Chunks: ${plan.chunks.map((c) => `${c}(${plan.groups.get(c)?.length ?? 0})`).join(", ")}`);
32623
+ const fedPlan = hasFederatedModules ? planChunks(fedPackages, accessMap.access) : null;
32624
+ if (fedPlan) {
32625
+ ok(`Federated modules: ${fedPackages.map((p) => moduleNameOf2(p.name)).join(", ")}`);
32626
+ }
32506
32627
  const i18nCollector = new Map;
32507
- const [browserResult] = await Promise.all([
32508
- buildBrowserApp(ws.rootDir, ws.browserDir, plan, cache, noCache, i18nCollector),
32628
+ const [browserResult, , , , shellResult, fedResult] = await Promise.all([
32629
+ buildBrowserApp(ws.rootDir, ws.browserDir, plan, cache, noCache, i18nCollector, {
32630
+ federated: false,
32631
+ exposeRuntime: isHost
32632
+ }),
32509
32633
  buildStyles(ws.rootDir, ws.arcDir, ws.packages, themePath, cache, noCache),
32510
32634
  copyBrowserAssets(ws, cache, noCache),
32511
- buildTranslations(ws.rootDir, ws.arcDir, cache, noCache)
32635
+ buildTranslations(ws.rootDir, ws.arcDir, cache, noCache),
32636
+ needsPeerShell ? buildPeerShell(ws.rootDir, join13(ws.arcDir, "shell"), cache, noCache) : Promise.resolve(undefined),
32637
+ fedPlan ? buildBrowserApp(ws.rootDir, join13(ws.arcDir, "browser-fed"), fedPlan, cache, noCache, i18nCollector, { federated: true, unitId: "browser-app-fed" }) : Promise.resolve(undefined)
32512
32638
  ]);
32513
32639
  const { finalizeTranslations: finalizeTranslations3 } = await Promise.resolve().then(() => (init_i18n(), exports_i18n));
32514
32640
  await finalizeTranslations3(ws.rootDir, ws.arcDir, i18nCollector);
32515
32641
  collectFrameworkDeps(ws.arcDir, ws.rootDir, ws.packages, [], serverExternals);
32516
32642
  saveBuildCache(ws.arcDir, cache);
32517
- const finalManifest = assembleManifest(ws, browserResult, cache);
32518
- writeFileSync9(join12(ws.arcDir, "manifest.json"), JSON.stringify(finalManifest, null, 2));
32643
+ const federationBuild = fedResult && hasFederatedModules ? {
32644
+ config: {
32645
+ slug: federation?.slug,
32646
+ auth: federation?.auth
32647
+ },
32648
+ modules: fedPackages.map((p) => moduleNameOf2(p.name)),
32649
+ permissions: [
32650
+ ...new Set(fedPackages.flatMap((p) => accessMap.exposure[moduleNameOf2(p.name)]?.permissions ?? []))
32651
+ ],
32652
+ slots: [
32653
+ ...new Set(fedPackages.flatMap((p) => accessMap.exposure[moduleNameOf2(p.name)]?.slots ?? []))
32654
+ ],
32655
+ build: {
32656
+ initial: fedResult.initial,
32657
+ groups: fedResult.groups,
32658
+ sharedChunks: fedResult.sharedChunks
32659
+ }
32660
+ } : undefined;
32661
+ const finalManifest = assembleManifest(ws, browserResult, cache, shellResult, federationBuild);
32662
+ writeFileSync10(join13(ws.arcDir, "manifest.json"), JSON.stringify(finalManifest, null, 2));
32519
32663
  return finalManifest;
32520
32664
  }
32521
- function assembleManifest(ws, browser, cache) {
32665
+ function assembleManifest(ws, browser, cache, shell, federation) {
32522
32666
  const stylesHash = cache.units["styles"]?.outputHash ?? "";
32523
32667
  return {
32524
32668
  initial: browser.initial,
32525
32669
  groups: browser.groups,
32526
32670
  sharedChunks: browser.sharedChunks,
32527
32671
  stylesHash,
32528
- buildTime: new Date().toISOString()
32672
+ buildTime: new Date().toISOString(),
32673
+ ...shell ? { shell: { imports: shell.imports, peers: shell.peers } } : {},
32674
+ ...federation ? { federation } : {}
32529
32675
  };
32530
32676
  }
32677
+ function getFederationConfig(ws) {
32678
+ const raw = ws.rootPkg.arc?.federation;
32679
+ if (!raw)
32680
+ return;
32681
+ if (raw.slug && !/^[a-z0-9-]+$/.test(raw.slug)) {
32682
+ throw new Error(`arc.federation.slug "${raw.slug}" \u2014 dozwolone tylko [a-z0-9-]+`);
32683
+ }
32684
+ return raw;
32685
+ }
32531
32686
  function resolveAssetSource(from, pkgDir, rootDir) {
32532
32687
  if (from.startsWith("./") || from.startsWith("../")) {
32533
- const resolved = join12(pkgDir, from);
32534
- return existsSync10(resolved) ? resolved : null;
32688
+ const resolved = join13(pkgDir, from);
32689
+ return existsSync11(resolved) ? resolved : null;
32535
32690
  }
32536
32691
  const candidates = [
32537
- join12(rootDir, "node_modules", from),
32538
- join12(pkgDir, "node_modules", from)
32692
+ join13(rootDir, "node_modules", from),
32693
+ join13(pkgDir, "node_modules", from)
32539
32694
  ];
32540
32695
  for (const c of candidates) {
32541
- if (existsSync10(c))
32696
+ if (existsSync11(c))
32542
32697
  return c;
32543
32698
  }
32544
- const bunCacheDir = join12(rootDir, "node_modules", ".bun");
32545
- if (existsSync10(bunCacheDir)) {
32699
+ const bunCacheDir = join13(rootDir, "node_modules", ".bun");
32700
+ if (existsSync11(bunCacheDir)) {
32546
32701
  for (const entry of readdirSync6(bunCacheDir, { withFileTypes: true })) {
32547
32702
  if (!entry.isDirectory())
32548
32703
  continue;
32549
- const candidate = join12(bunCacheDir, entry.name, "node_modules", from);
32550
- if (existsSync10(candidate))
32704
+ const candidate = join13(bunCacheDir, entry.name, "node_modules", from);
32705
+ if (existsSync11(candidate))
32551
32706
  return candidate;
32552
32707
  }
32553
32708
  }
32554
32709
  return null;
32555
32710
  }
32556
32711
  function readBrowserAssets(pkgDir) {
32557
- const pkgJsonPath = join12(pkgDir, "package.json");
32558
- if (!existsSync10(pkgJsonPath))
32712
+ const pkgJsonPath = join13(pkgDir, "package.json");
32713
+ if (!existsSync11(pkgJsonPath))
32559
32714
  return [];
32560
32715
  try {
32561
- const pkg = JSON.parse(readFileSync11(pkgJsonPath, "utf-8"));
32716
+ const pkg = JSON.parse(readFileSync12(pkgJsonPath, "utf-8"));
32562
32717
  const assets = pkg.arc?.browserAssets;
32563
32718
  if (!Array.isArray(assets))
32564
32719
  return [];
@@ -32568,14 +32723,14 @@ function readBrowserAssets(pkgDir) {
32568
32723
  }
32569
32724
  }
32570
32725
  function discoverBrowserAssets(ws) {
32571
- const arcDir = join12(ws.rootDir, "node_modules", "@arcote.tech");
32572
- if (!existsSync10(arcDir))
32726
+ const arcDir = join13(ws.rootDir, "node_modules", "@arcote.tech");
32727
+ if (!existsSync11(arcDir))
32573
32728
  return [];
32574
32729
  const out = [];
32575
32730
  for (const entry of readdirSync6(arcDir, { withFileTypes: true })) {
32576
32731
  if (!entry.isDirectory() && !entry.isSymbolicLink())
32577
32732
  continue;
32578
- const pkgDir = join12(arcDir, entry.name);
32733
+ const pkgDir = join13(arcDir, entry.name);
32579
32734
  const assets = readBrowserAssets(pkgDir);
32580
32735
  for (const asset of assets) {
32581
32736
  const src = resolveAssetSource(asset.from, pkgDir, ws.rootDir);
@@ -32589,7 +32744,7 @@ function discoverBrowserAssets(ws) {
32589
32744
  return out;
32590
32745
  }
32591
32746
  async function copyBrowserAssets(ws, cache, noCache) {
32592
- mkdirSync9(ws.assetsDir, { recursive: true });
32747
+ mkdirSync10(ws.assetsDir, { recursive: true });
32593
32748
  const assets = discoverBrowserAssets(ws);
32594
32749
  if (assets.length === 0)
32595
32750
  return;
@@ -32600,7 +32755,7 @@ async function copyBrowserAssets(ws, cache, noCache) {
32600
32755
  to: a.to,
32601
32756
  mtime: mtimeOf(a.src)
32602
32757
  })));
32603
- const requiredOutputs = assets.map((a) => join12(ws.assetsDir, a.to));
32758
+ const requiredOutputs = assets.map((a) => join13(ws.assetsDir, a.to));
32604
32759
  if (!noCache && isCacheHit(cache, unitId, inputHash, requiredOutputs)) {
32605
32760
  console.log(` \u2713 cached: browser-assets (${assets.length})`);
32606
32761
  return;
@@ -32608,10 +32763,10 @@ async function copyBrowserAssets(ws, cache, noCache) {
32608
32763
  console.log(` building: browser-assets (${assets.length})`);
32609
32764
  const outputHashes = {};
32610
32765
  for (const asset of assets) {
32611
- const dest = join12(ws.assetsDir, asset.to);
32612
- mkdirSync9(dirname8(dest), { recursive: true });
32766
+ const dest = join13(ws.assetsDir, asset.to);
32767
+ mkdirSync10(dirname8(dest), { recursive: true });
32613
32768
  copyFileSync(asset.src, dest);
32614
- outputHashes[asset.to] = sha256Hex(readFileSync11(dest));
32769
+ outputHashes[asset.to] = sha256Hex(readFileSync12(dest));
32615
32770
  }
32616
32771
  updateCache(cache, unitId, inputHash, { outputHashes });
32617
32772
  }
@@ -32619,12 +32774,12 @@ async function loadServerContext(ws) {
32619
32774
  globalThis.ONLY_SERVER = true;
32620
32775
  globalThis.ONLY_BROWSER = false;
32621
32776
  globalThis.ONLY_CLIENT = false;
32622
- const platformDir = join12(process.cwd(), "node_modules", "@arcote.tech", "platform");
32623
- const platformPkg = JSON.parse(readFileSync11(join12(platformDir, "package.json"), "utf-8"));
32624
- const platformEntry = join12(platformDir, platformPkg.main ?? "src/index.ts");
32777
+ const platformDir = join13(process.cwd(), "node_modules", "@arcote.tech", "platform");
32778
+ const platformPkg = JSON.parse(readFileSync12(join13(platformDir, "package.json"), "utf-8"));
32779
+ const platformEntry = join13(platformDir, platformPkg.main ?? "src/index.ts");
32625
32780
  await import(platformEntry);
32626
- const serverEntry = join12(ws.arcDir, "server", SERVER_ENTRY_FILE);
32627
- if (!existsSync10(serverEntry)) {
32781
+ const serverEntry = join13(ws.arcDir, "server", SERVER_ENTRY_FILE);
32782
+ if (!existsSync11(serverEntry)) {
32628
32783
  return { context: null, moduleAccess: new Map };
32629
32784
  }
32630
32785
  try {
@@ -32648,24 +32803,26 @@ async function platformBuild(opts = {}) {
32648
32803
  }
32649
32804
 
32650
32805
  // src/commands/platform-deploy.ts
32651
- import { existsSync as existsSync17, readFileSync as readFileSync15 } from "fs";
32652
- import { dirname as dirname11, join as join20 } from "path";
32806
+ import { randomBytes } from "crypto";
32807
+ import { existsSync as existsSync18, readFileSync as readFileSync16 } from "fs";
32808
+ import { dirname as dirname11, join as join21 } from "path";
32653
32809
  import { fileURLToPath as fileURLToPath8 } from "url";
32654
32810
 
32655
32811
  // src/deploy/bootstrap.ts
32656
32812
  var {spawn: spawn4 } = globalThis.Bun;
32657
- import { mkdirSync as mkdirSync12, writeFileSync as writeFileSync13 } from "fs";
32813
+ import { mkdirSync as mkdirSync13, writeFileSync as writeFileSync14 } from "fs";
32658
32814
  import { tmpdir as tmpdir2 } from "os";
32659
- import { dirname as dirname9, join as join18 } from "path";
32815
+ import { dirname as dirname9, join as join19 } from "path";
32660
32816
 
32661
32817
  // src/deploy/ansible.ts
32662
32818
  import { spawn as nodeSpawn } from "child_process";
32663
- import { existsSync as existsSync11, mkdirSync as mkdirSync10, writeFileSync as writeFileSync10 } from "fs";
32819
+ import { existsSync as existsSync12, mkdirSync as mkdirSync11, writeFileSync as writeFileSync11 } from "fs";
32664
32820
  import { homedir, tmpdir } from "os";
32665
- import { join as join13 } from "path";
32821
+ import { join as join14 } from "path";
32666
32822
 
32667
32823
  // src/deploy/assets.ts
32668
32824
  var TERRAFORM_MAIN_TF = `terraform {
32825
+ required_version = ">= 1.1"
32669
32826
  required_providers {
32670
32827
  hcloud = {
32671
32828
  source = "hetznercloud/hcloud"
@@ -32678,17 +32835,50 @@ provider "hcloud" {
32678
32835
  token = var.hcloud_token
32679
32836
  }
32680
32837
 
32838
+ locals {
32839
+ # cloud-init injects the operator public key into the default image user's
32840
+ # authorized_keys (root on Hetzner Ubuntu images) \u2014 matching the root-first
32841
+ # ansible bootstrap. Used only when use_cloud_init_key = true.
32842
+ cloud_init_user_data = <<-EOT
32843
+ #cloud-config
32844
+ ssh_pwauth: false
32845
+ chpasswd:
32846
+ expire: false
32847
+ ssh_authorized_keys:
32848
+ - \${trimspace(file(var.ssh_public_key))}
32849
+ # Hetzner marks the root password expired (must-change-on-login) when no
32850
+ # hcloud_ssh_key object is attached \u2014 which blocks non-interactive key
32851
+ # login ("password change required, no TTY"). Clear the expiry so the
32852
+ # deploy user / ansible can SSH in as root right away.
32853
+ runcmd:
32854
+ - chage -d $(date +%Y-%m-%d) -M -1 root
32855
+ EOT
32856
+ }
32857
+
32858
+ # Legacy path: a managed SSH key object in the Hetzner project. Skipped when
32859
+ # use_cloud_init_key = true, because several apps sharing one project with the
32860
+ # same local public key would collide on both key NAME and FINGERPRINT.
32681
32861
  resource "hcloud_ssh_key" "deploy" {
32862
+ count = var.use_cloud_init_key ? 0 : 1
32682
32863
  name = "\${var.server_name}-deploy"
32683
32864
  public_key = file(var.ssh_public_key)
32684
32865
  }
32685
32866
 
32867
+ # Re-home the pre-count address so an already-provisioned host (state created
32868
+ # before count existed) plans as a no-op instead of destroy+recreate \u2014 which
32869
+ # would ForceNew-replace the server. Ignored for fresh (empty) state.
32870
+ moved {
32871
+ from = hcloud_ssh_key.deploy
32872
+ to = hcloud_ssh_key.deploy[0]
32873
+ }
32874
+
32686
32875
  resource "hcloud_server" "arc" {
32687
32876
  name = var.server_name
32688
32877
  image = var.server_image
32689
32878
  server_type = var.server_type
32690
32879
  location = var.server_location
32691
- ssh_keys = [hcloud_ssh_key.deploy.id]
32880
+ ssh_keys = var.use_cloud_init_key ? [] : [hcloud_ssh_key.deploy[0].id]
32881
+ user_data = var.use_cloud_init_key ? local.cloud_init_user_data : null
32692
32882
 
32693
32883
  public_net {
32694
32884
  ipv4_enabled = true
@@ -32739,6 +32929,12 @@ variable "ssh_public_key" {
32739
32929
  type = string
32740
32930
  default = "~/.ssh/id_ed25519.pub"
32741
32931
  }
32932
+
32933
+ variable "use_cloud_init_key" {
32934
+ description = "Inject the SSH public key via cloud-init user_data instead of a managed hcloud_ssh_key object (avoids name/fingerprint collisions when multiple apps share one Hetzner project)."
32935
+ type = bool
32936
+ default = false
32937
+ }
32742
32938
  `;
32743
32939
  var ANSIBLE_SITE_YML = `---
32744
32940
  # Arc platform bootstrap playbook \u2014 minimal hardened Docker host.
@@ -32935,30 +33131,30 @@ var ASSETS = {
32935
33131
  }
32936
33132
  };
32937
33133
  async function materializeAssets(targetDir, files) {
32938
- const { mkdirSync: mkdirSync10, writeFileSync: writeFileSync10 } = await import("fs");
32939
- const { join: join13 } = await import("path");
32940
- mkdirSync10(targetDir, { recursive: true });
33134
+ const { mkdirSync: mkdirSync11, writeFileSync: writeFileSync11 } = await import("fs");
33135
+ const { join: join14 } = await import("path");
33136
+ mkdirSync11(targetDir, { recursive: true });
32941
33137
  for (const [name, content] of Object.entries(files)) {
32942
- writeFileSync10(join13(targetDir, name), content);
33138
+ writeFileSync11(join14(targetDir, name), content);
32943
33139
  }
32944
33140
  }
32945
33141
 
32946
33142
  // src/deploy/ansible.ts
32947
33143
  function pickSshKeyForAnsible(configured) {
32948
33144
  if (configured) {
32949
- const expanded = configured.startsWith("~") ? join13(homedir(), configured.slice(1)) : configured;
32950
- return existsSync11(expanded) ? expanded : null;
33145
+ const expanded = configured.startsWith("~") ? join14(homedir(), configured.slice(1)) : configured;
33146
+ return existsSync12(expanded) ? expanded : null;
32951
33147
  }
32952
33148
  for (const name of ["id_ed25519", "id_ecdsa", "id_rsa"]) {
32953
- const path4 = join13(homedir(), ".ssh", name);
32954
- if (existsSync11(path4))
33149
+ const path4 = join14(homedir(), ".ssh", name);
33150
+ if (existsSync12(path4))
32955
33151
  return path4;
32956
33152
  }
32957
33153
  return null;
32958
33154
  }
32959
33155
  async function runAnsible(inputs) {
32960
- const workDir = join13(tmpdir(), "arc-deploy", `ansible-${Date.now()}`);
32961
- mkdirSync10(workDir, { recursive: true });
33156
+ const workDir = join14(tmpdir(), "arc-deploy", `ansible-${Date.now()}`);
33157
+ mkdirSync11(workDir, { recursive: true });
32962
33158
  await materializeAssets(workDir, ASSETS.ansible);
32963
33159
  const user = inputs.asRoot ? "root" : inputs.target.user;
32964
33160
  const port = inputs.ansible?.sshPort ?? inputs.target.port;
@@ -32974,7 +33170,7 @@ async function runAnsible(inputs) {
32974
33170
  ""
32975
33171
  ].join(`
32976
33172
  `);
32977
- writeFileSync10(join13(workDir, "inventory.ini"), inventory);
33173
+ writeFileSync11(join14(workDir, "inventory.ini"), inventory);
32978
33174
  const extraVarsJson = JSON.stringify({
32979
33175
  username: inputs.target.user,
32980
33176
  ssh_port: port,
@@ -34533,16 +34729,16 @@ function panelTimeseries(title, gridPos, query, legend, unit, datasource = PROME
34533
34729
  // src/deploy/terraform.ts
34534
34730
  import { spawn as nodeSpawn2 } from "child_process";
34535
34731
  import { createHash as createHash2 } from "crypto";
34536
- import { existsSync as existsSync12, mkdirSync as mkdirSync11, writeFileSync as writeFileSync11 } from "fs";
34732
+ import { existsSync as existsSync13, mkdirSync as mkdirSync12, writeFileSync as writeFileSync12 } from "fs";
34537
34733
  import { homedir as homedir2 } from "os";
34538
- import { join as join14 } from "path";
34734
+ import { join as join15 } from "path";
34539
34735
  async function runTerraform(inputs) {
34540
34736
  const wsHash = createHash2("sha256").update(inputs.workspaceDir).digest("hex").slice(0, 16);
34541
- const workDir = join14(homedir2(), ".arc-deploy", wsHash, "tf");
34542
- mkdirSync11(workDir, { recursive: true });
34737
+ const workDir = join15(homedir2(), ".arc-deploy", wsHash, "tf");
34738
+ mkdirSync12(workDir, { recursive: true });
34543
34739
  await materializeAssets(workDir, ASSETS.terraform);
34544
34740
  const sshPubKey = inputs.tf.sshPublicKey ?? expandHome("~/.ssh/id_ed25519.pub");
34545
- if (!existsSync12(expandHome(sshPubKey))) {
34741
+ if (!existsSync13(expandHome(sshPubKey))) {
34546
34742
  throw new Error(`SSH public key not found at ${sshPubKey}. Set provision.terraform.sshPublicKey in deploy.arc.json.`);
34547
34743
  }
34548
34744
  const tfvars = [
@@ -34551,11 +34747,12 @@ async function runTerraform(inputs) {
34551
34747
  `server_type = "${inputs.tf.serverType}"`,
34552
34748
  `server_location = "${inputs.tf.location}"`,
34553
34749
  `server_image = "${inputs.tf.image}"`,
34554
- `ssh_public_key = "${expandHome(sshPubKey)}"`
34750
+ `ssh_public_key = "${expandHome(sshPubKey)}"`,
34751
+ `use_cloud_init_key = ${inputs.useCloudInitKey}`
34555
34752
  ].join(`
34556
34753
  `) + `
34557
34754
  `;
34558
- writeFileSync11(join14(workDir, "terraform.tfvars"), tfvars);
34755
+ writeFileSync12(join15(workDir, "terraform.tfvars"), tfvars);
34559
34756
  await runTf(workDir, ["init", "-input=false", "-no-color"]);
34560
34757
  await runTf(workDir, [
34561
34758
  "apply",
@@ -34613,20 +34810,20 @@ function expandHome(p) {
34613
34810
  }
34614
34811
 
34615
34812
  // src/deploy/config.ts
34616
- import { existsSync as existsSync14, readFileSync as readFileSync13, writeFileSync as writeFileSync12 } from "fs";
34617
- import { join as join16 } from "path";
34813
+ import { existsSync as existsSync15, readFileSync as readFileSync14, writeFileSync as writeFileSync13 } from "fs";
34814
+ import { join as join17 } from "path";
34618
34815
 
34619
34816
  // src/deploy/env-file.ts
34620
- import { appendFileSync, existsSync as existsSync13, readFileSync as readFileSync12 } from "fs";
34621
- import { join as join15 } from "path";
34817
+ import { appendFileSync, existsSync as existsSync14, readFileSync as readFileSync13 } from "fs";
34818
+ import { join as join16 } from "path";
34622
34819
  function loadDeployEnvFiles(rootDir, envNames) {
34623
- const globalsPath = join15(rootDir, "deploy.arc.env");
34624
- const globals = existsSync13(globalsPath) ? parseEnvFile(readFileSync12(globalsPath, "utf-8"), globalsPath) : {};
34820
+ const globalsPath = join16(rootDir, "deploy.arc.env");
34821
+ const globals = existsSync14(globalsPath) ? parseEnvFile(readFileSync13(globalsPath, "utf-8"), globalsPath) : {};
34625
34822
  const perEnv = {};
34626
34823
  for (const name of envNames) {
34627
- const envPath = join15(rootDir, `deploy.arc.${name}.env`);
34628
- if (existsSync13(envPath)) {
34629
- perEnv[name] = parseEnvFile(readFileSync12(envPath, "utf-8"), envPath);
34824
+ const envPath = join16(rootDir, `deploy.arc.${name}.env`);
34825
+ if (existsSync14(envPath)) {
34826
+ perEnv[name] = parseEnvFile(readFileSync13(envPath, "utf-8"), envPath);
34630
34827
  }
34631
34828
  }
34632
34829
  return { globals, perEnv };
@@ -34642,16 +34839,16 @@ function ensurePersistedSecret(rootDir, scope, key, generate) {
34642
34839
  if (process.env[key])
34643
34840
  return process.env[key];
34644
34841
  const fileName = scope === "globals" ? "deploy.arc.env" : `deploy.arc.${scope}.env`;
34645
- const path4 = join15(rootDir, fileName);
34646
- if (existsSync13(path4)) {
34647
- const existing = parseEnvFile(readFileSync12(path4, "utf-8"), path4);
34842
+ const path4 = join16(rootDir, fileName);
34843
+ if (existsSync14(path4)) {
34844
+ const existing = parseEnvFile(readFileSync13(path4, "utf-8"), path4);
34648
34845
  if (existing[key]) {
34649
34846
  process.env[key] = existing[key];
34650
34847
  return existing[key];
34651
34848
  }
34652
34849
  }
34653
34850
  const value = generate();
34654
- const prefix = existsSync13(path4) && !readFileSync12(path4, "utf-8").endsWith(`
34851
+ const prefix = existsSync14(path4) && !readFileSync13(path4, "utf-8").endsWith(`
34655
34852
  `) ? `
34656
34853
  ` : "";
34657
34854
  appendFileSync(path4, `${prefix}${key}=${value}
@@ -34687,17 +34884,17 @@ function parseEnvFile(content, pathForErrors) {
34687
34884
  // src/deploy/config.ts
34688
34885
  var DEPLOY_CONFIG_FILE = "deploy.arc.json";
34689
34886
  function deployConfigPath(rootDir) {
34690
- return join16(rootDir, DEPLOY_CONFIG_FILE);
34887
+ return join17(rootDir, DEPLOY_CONFIG_FILE);
34691
34888
  }
34692
34889
  function deployConfigExists(rootDir) {
34693
- return existsSync14(deployConfigPath(rootDir));
34890
+ return existsSync15(deployConfigPath(rootDir));
34694
34891
  }
34695
34892
  function loadDeployConfig(rootDir) {
34696
34893
  const path4 = deployConfigPath(rootDir);
34697
- if (!existsSync14(path4)) {
34894
+ if (!existsSync15(path4)) {
34698
34895
  throw new Error(`Missing ${DEPLOY_CONFIG_FILE} at ${path4}`);
34699
34896
  }
34700
- const raw = readFileSync13(path4, "utf-8");
34897
+ const raw = readFileSync14(path4, "utf-8");
34701
34898
  let parsed;
34702
34899
  try {
34703
34900
  parsed = JSON.parse(raw);
@@ -34720,9 +34917,9 @@ function loadDeployConfig(rootDir) {
34720
34917
  }
34721
34918
  function saveDeployConfig(rootDir, cfg) {
34722
34919
  const path4 = deployConfigPath(rootDir);
34723
- const raw = existsSync14(path4) ? JSON.parse(readFileSync13(path4, "utf-8")) : {};
34920
+ const raw = existsSync15(path4) ? JSON.parse(readFileSync14(path4, "utf-8")) : {};
34724
34921
  raw.target = { ...raw.target, ...cfg.target };
34725
- writeFileSync12(path4, JSON.stringify(raw, null, 2) + `
34922
+ writeFileSync13(path4, JSON.stringify(raw, null, 2) + `
34726
34923
  `);
34727
34924
  }
34728
34925
  var VAR_REGEX = /\$\{([A-Z0-9_]+)\}|\$([A-Z0-9_]+)/g;
@@ -34757,9 +34954,20 @@ function validateDeployConfig(input) {
34757
34954
  if (!/^[A-Z][A-Z0-9_]*$/.test(passwordEnv)) {
34758
34955
  throw new Error(`deploy.arc.json: registry.passwordEnv "${passwordEnv}" must be an UPPER_SNAKE_CASE env var name`);
34759
34956
  }
34957
+ const provisionRaw = input.provision;
34958
+ const hasProvisionTf = isObject(provisionRaw) && isObject(provisionRaw.terraform);
34959
+ const hostRaw = optionalString(target, "target.host");
34960
+ let host;
34961
+ if (hostRaw && hostRaw.length > 0) {
34962
+ host = hostRaw;
34963
+ } else if (hasProvisionTf) {
34964
+ host = "PENDING_TERRAFORM";
34965
+ } else {
34966
+ throw new Error(`deploy.arc.json: target.host is required unless provision.terraform is present (with provision, terraform fills it on first deploy).`);
34967
+ }
34760
34968
  const validated = {
34761
34969
  target: {
34762
- host: requireString(target, "target.host"),
34970
+ host,
34763
34971
  user: optionalString(target, "target.user") ?? "deploy",
34764
34972
  port: optionalNumber(target, "target.port") ?? 22,
34765
34973
  remoteDir: optionalString(target, "target.remoteDir") ?? "/opt/arc",
@@ -34860,13 +35068,23 @@ function validateDeployConfig(input) {
34860
35068
  if (providerVal !== "hcloud") {
34861
35069
  throw new Error(`deploy.arc.json: provision.terraform.provider must be "hcloud" (got "${providerVal}")`);
34862
35070
  }
35071
+ const serverName = optionalString(tf, "provision.terraform.serverName");
35072
+ if (serverName !== undefined && !/^[a-z0-9-]{1,63}$/.test(serverName)) {
35073
+ throw new Error(`deploy.arc.json: provision.terraform.serverName "${serverName}" must match [a-z0-9-] and be at most 63 chars`);
35074
+ }
35075
+ const sshKeyStrategyRaw = optionalString(tf, "provision.terraform.sshKeyStrategy");
35076
+ if (sshKeyStrategyRaw !== undefined && sshKeyStrategyRaw !== "resource" && sshKeyStrategyRaw !== "cloud-init") {
35077
+ throw new Error(`deploy.arc.json: provision.terraform.sshKeyStrategy must be "resource" or "cloud-init" (got "${sshKeyStrategyRaw}")`);
35078
+ }
34863
35079
  const terraform = {
34864
35080
  provider: "hcloud",
34865
35081
  serverType: requireString(tf, "provision.terraform.serverType"),
34866
35082
  location: requireString(tf, "provision.terraform.location"),
34867
35083
  image: optionalString(tf, "provision.terraform.image") ?? "ubuntu-22.04",
34868
35084
  tokenEnv: requireString(tf, "provision.terraform.tokenEnv"),
34869
- sshPublicKey: optionalString(tf, "provision.terraform.sshPublicKey")
35085
+ sshPublicKey: optionalString(tf, "provision.terraform.sshPublicKey"),
35086
+ serverName,
35087
+ sshKeyStrategy: sshKeyStrategyRaw
34870
35088
  };
34871
35089
  let ansible;
34872
35090
  const ansibleRaw = provision.ansible;
@@ -34936,17 +35154,17 @@ function cfgErr(path4, expected) {
34936
35154
 
34937
35155
  // src/deploy/ssh.ts
34938
35156
  var {spawn: spawn3 } = globalThis.Bun;
34939
- import { existsSync as existsSync15 } from "fs";
35157
+ import { existsSync as existsSync16 } from "fs";
34940
35158
  import { homedir as homedir3 } from "os";
34941
- import { join as join17 } from "path";
35159
+ import { join as join18 } from "path";
34942
35160
  function pickSshKey(target) {
34943
35161
  if (target.sshKey) {
34944
- const expanded = target.sshKey.startsWith("~") ? join17(homedir3(), target.sshKey.slice(1)) : target.sshKey;
34945
- return existsSync15(expanded) ? expanded : null;
35162
+ const expanded = target.sshKey.startsWith("~") ? join18(homedir3(), target.sshKey.slice(1)) : target.sshKey;
35163
+ return existsSync16(expanded) ? expanded : null;
34946
35164
  }
34947
35165
  for (const name of ["id_ed25519", "id_ecdsa", "id_rsa"]) {
34948
- const path4 = join17(homedir3(), ".ssh", name);
34949
- if (existsSync15(path4))
35166
+ const path4 = join18(homedir3(), ".ssh", name);
35167
+ if (existsSync16(path4))
34950
35168
  return path4;
34951
35169
  }
34952
35170
  return null;
@@ -34961,7 +35179,7 @@ function sshMuxArgs() {
34961
35179
  "-o",
34962
35180
  "ControlMaster=auto",
34963
35181
  "-o",
34964
- `ControlPath=${join17(homedir3(), ".ssh", "cm-arc-%C")}`,
35182
+ `ControlPath=${join18(homedir3(), ".ssh", "cm-arc-%C")}`,
34965
35183
  "-o",
34966
35184
  "ControlPersist=120"
34967
35185
  ];
@@ -35161,10 +35379,14 @@ async function bootstrap(inputs) {
35161
35379
  if (!token) {
35162
35380
  throw new Error(`Environment variable ${cfg.provision.terraform.tokenEnv} is not set`);
35163
35381
  }
35382
+ const firstEnv = Object.keys(cfg.envs)[0] ?? "host";
35383
+ const serverName = cfg.provision.terraform.serverName ?? `arc-${firstEnv}`;
35384
+ const useCloudInitKey = cfg.provision.terraform.sshKeyStrategy === "cloud-init";
35164
35385
  const tfOut = await runTerraform({
35165
35386
  tf: cfg.provision.terraform,
35166
35387
  token,
35167
- serverName: `arc-${Object.keys(cfg.envs)[0] ?? "host"}`,
35388
+ serverName,
35389
+ useCloudInitKey,
35168
35390
  workspaceDir: rootDir
35169
35391
  });
35170
35392
  ok(`Server provisioned: ${tfOut.serverIp}`);
@@ -35226,17 +35448,17 @@ async function isRegistryRunning(cfg) {
35226
35448
  }
35227
35449
  async function upStack(inputs) {
35228
35450
  const { cfg } = inputs;
35229
- const workDir = join18(tmpdir2(), "arc-deploy", `stack-${Date.now()}`);
35230
- mkdirSync12(workDir, { recursive: true });
35451
+ const workDir = join19(tmpdir2(), "arc-deploy", `stack-${Date.now()}`);
35452
+ mkdirSync13(workDir, { recursive: true });
35231
35453
  await assertRegistryDnsResolves(cfg);
35232
35454
  const password = process.env[cfg.registry.passwordEnv];
35233
35455
  if (!password) {
35234
35456
  throw new Error(`Registry password env var ${cfg.registry.passwordEnv} is not set. ` + `Set it (e.g. \`export ${cfg.registry.passwordEnv}=...\`) before bootstrap.`);
35235
35457
  }
35236
35458
  const htpasswdLine = await generateHtpasswd(cfg.registry.username, password);
35237
- writeFileSync13(join18(workDir, "htpasswd"), htpasswdLine);
35238
- writeFileSync13(join18(workDir, "Caddyfile"), generateCaddyfile(cfg));
35239
- writeFileSync13(join18(workDir, "docker-compose.yml"), generateCompose({ cfg }));
35459
+ writeFileSync14(join19(workDir, "htpasswd"), htpasswdLine);
35460
+ writeFileSync14(join19(workDir, "Caddyfile"), generateCaddyfile(cfg));
35461
+ writeFileSync14(join19(workDir, "docker-compose.yml"), generateCompose({ cfg }));
35240
35462
  let observabilityFiles = null;
35241
35463
  let observabilityHtpasswd = null;
35242
35464
  if (cfg.observability?.enabled) {
@@ -35248,30 +35470,30 @@ async function upStack(inputs) {
35248
35470
  }
35249
35471
  observabilityHtpasswd = await generateCaddyBasicAuthLine("admin", adminPassword);
35250
35472
  for (const [relPath, contents] of Object.entries(observabilityFiles)) {
35251
- const fullPath = join18(workDir, relPath);
35252
- mkdirSync12(dirname9(fullPath), { recursive: true });
35253
- writeFileSync13(fullPath, contents);
35473
+ const fullPath = join19(workDir, relPath);
35474
+ mkdirSync13(dirname9(fullPath), { recursive: true });
35475
+ writeFileSync14(fullPath, contents);
35254
35476
  }
35255
- writeFileSync13(join18(workDir, "observability-htpasswd"), observabilityHtpasswd);
35477
+ writeFileSync14(join19(workDir, "observability-htpasswd"), observabilityHtpasswd);
35256
35478
  }
35257
35479
  await assertExec(cfg.target, `sudo mkdir -p ${cfg.target.remoteDir} && sudo chown ${cfg.target.user}:${cfg.target.user} ${cfg.target.remoteDir}`);
35258
35480
  for (const name of Object.keys(cfg.envs)) {
35259
35481
  await assertExec(cfg.target, `mkdir -p ${cfg.target.remoteDir}/${name}`);
35260
35482
  }
35261
35483
  await assertExec(cfg.target, `mkdir -p ${cfg.target.remoteDir}/registry-auth`);
35262
- await scpUpload(cfg.target, join18(workDir, "Caddyfile"), `${cfg.target.remoteDir}/Caddyfile`);
35263
- await scpUpload(cfg.target, join18(workDir, "docker-compose.yml"), `${cfg.target.remoteDir}/docker-compose.yml`);
35264
- await scpUpload(cfg.target, join18(workDir, "htpasswd"), `${cfg.target.remoteDir}/registry-auth/htpasswd`);
35484
+ await scpUpload(cfg.target, join19(workDir, "Caddyfile"), `${cfg.target.remoteDir}/Caddyfile`);
35485
+ await scpUpload(cfg.target, join19(workDir, "docker-compose.yml"), `${cfg.target.remoteDir}/docker-compose.yml`);
35486
+ await scpUpload(cfg.target, join19(workDir, "htpasswd"), `${cfg.target.remoteDir}/registry-auth/htpasswd`);
35265
35487
  if (observabilityFiles && observabilityHtpasswd) {
35266
35488
  await assertExec(cfg.target, `mkdir -p ${cfg.target.remoteDir}/observability/grafana-dashboards ${cfg.target.remoteDir}/observability/grafana-alerting`);
35267
35489
  for (const relPath of Object.keys(observabilityFiles)) {
35268
- const localDir = dirname9(join18(workDir, relPath));
35269
- mkdirSync12(localDir, { recursive: true });
35490
+ const localDir = dirname9(join19(workDir, relPath));
35491
+ mkdirSync13(localDir, { recursive: true });
35270
35492
  }
35271
35493
  for (const relPath of Object.keys(observabilityFiles)) {
35272
- await scpUpload(cfg.target, join18(workDir, relPath), `${cfg.target.remoteDir}/${relPath}`);
35494
+ await scpUpload(cfg.target, join19(workDir, relPath), `${cfg.target.remoteDir}/${relPath}`);
35273
35495
  }
35274
- await scpUpload(cfg.target, join18(workDir, "observability-htpasswd"), `${cfg.target.remoteDir}/observability-htpasswd`);
35496
+ await scpUpload(cfg.target, join19(workDir, "observability-htpasswd"), `${cfg.target.remoteDir}/observability-htpasswd`);
35275
35497
  }
35276
35498
  await assertExec(cfg.target, `touch ${cfg.target.remoteDir}/.env`);
35277
35499
  if (cfg.observability?.enabled) {
@@ -35426,14 +35648,14 @@ var {spawn: spawn5 } = globalThis.Bun;
35426
35648
  import { createHash as createHash3 } from "crypto";
35427
35649
  import {
35428
35650
  copyFileSync as copyFileSync2,
35429
- existsSync as existsSync16,
35430
- mkdirSync as mkdirSync13,
35431
- readFileSync as readFileSync14,
35651
+ existsSync as existsSync17,
35652
+ mkdirSync as mkdirSync14,
35653
+ readFileSync as readFileSync15,
35432
35654
  realpathSync as realpathSync2,
35433
- writeFileSync as writeFileSync14
35655
+ writeFileSync as writeFileSync15
35434
35656
  } from "fs";
35435
35657
  import { tmpdir as tmpdir3 } from "os";
35436
- import { dirname as dirname10, join as join19 } from "path";
35658
+ import { dirname as dirname10, join as join20 } from "path";
35437
35659
  import { fileURLToPath as fileURLToPath7 } from "url";
35438
35660
 
35439
35661
  // src/deploy/image-template.ts
@@ -35483,8 +35705,8 @@ function generateDockerfile(inputs) {
35483
35705
  // src/deploy/image.ts
35484
35706
  async function buildImage(ws, opts) {
35485
35707
  await ensureDocker();
35486
- const manifestPath = join19(ws.arcDir, "manifest.json");
35487
- if (!existsSync16(manifestPath)) {
35708
+ const manifestPath = join20(ws.arcDir, "manifest.json");
35709
+ if (!existsSync17(manifestPath)) {
35488
35710
  throw new Error(`No build manifest at ${manifestPath}. Run \`arc platform build\` first or omit --skip-build.`);
35489
35711
  }
35490
35712
  embedCliBundle(ws);
@@ -35494,10 +35716,10 @@ async function buildImage(ws, opts) {
35494
35716
  const dockerfileInputs = collectDockerfileInputs(ws);
35495
35717
  const dockerfile = generateDockerfile(dockerfileInputs);
35496
35718
  const buildContextDir = ws.rootDir;
35497
- const dockerfileDir = join19(tmpdir3(), `arc-image-${Date.now()}`);
35498
- mkdirSync13(dockerfileDir, { recursive: true });
35499
- const dockerfilePath = join19(dockerfileDir, "Dockerfile");
35500
- writeFileSync14(dockerfilePath, dockerfile);
35719
+ const dockerfileDir = join20(tmpdir3(), `arc-image-${Date.now()}`);
35720
+ mkdirSync14(dockerfileDir, { recursive: true });
35721
+ const dockerfilePath = join20(dockerfileDir, "Dockerfile");
35722
+ writeFileSync15(dockerfilePath, dockerfile);
35501
35723
  const buildArgs = [
35502
35724
  "build",
35503
35725
  "--platform=linux/amd64",
@@ -35522,20 +35744,20 @@ async function buildImage(ws, opts) {
35522
35744
  }
35523
35745
  function embedCliBundle(ws) {
35524
35746
  const source = locateCliBundle();
35525
- const target = join19(ws.arcDir, "host.js");
35747
+ const target = join20(ws.arcDir, "host.js");
35526
35748
  copyFileSync2(source, target);
35527
35749
  }
35528
35750
  function locateCliBundle() {
35529
35751
  const here = fileURLToPath7(import.meta.url);
35530
35752
  let cur = dirname10(here);
35531
35753
  while (cur !== "/" && cur !== "") {
35532
- const candidate = join19(cur, "package.json");
35533
- if (existsSync16(candidate)) {
35754
+ const candidate = join20(cur, "package.json");
35755
+ if (existsSync17(candidate)) {
35534
35756
  try {
35535
- const pkg = JSON.parse(readFileSync14(candidate, "utf-8"));
35757
+ const pkg = JSON.parse(readFileSync15(candidate, "utf-8"));
35536
35758
  if (pkg.name === "@arcote.tech/arc-cli") {
35537
- const distIndex = join19(realpathSync2(cur), "dist", "index.js");
35538
- if (!existsSync16(distIndex)) {
35759
+ const distIndex = join20(realpathSync2(cur), "dist", "index.js");
35760
+ if (!existsSync17(distIndex)) {
35539
35761
  throw new Error(`arc-cli bundle missing at ${distIndex}. Run \`bun run build\` in packages/cli/.`);
35540
35762
  }
35541
35763
  return distIndex;
@@ -35567,17 +35789,17 @@ async function ensureDocker() {
35567
35789
  }
35568
35790
  }
35569
35791
  function computeContentHash(manifestPath) {
35570
- const raw = JSON.parse(readFileSync14(manifestPath, "utf-8"));
35792
+ const raw = JSON.parse(readFileSync15(manifestPath, "utf-8"));
35571
35793
  delete raw.buildTime;
35572
35794
  const canonical = JSON.stringify(raw);
35573
35795
  return createHash3("sha256").update(canonical).digest("hex").slice(0, 12);
35574
35796
  }
35575
35797
  function collectDockerfileInputs(ws) {
35576
- const hasPublicDir = existsSync16(join19(ws.rootDir, "public"));
35577
- const hasLocales = existsSync16(join19(ws.rootDir, "locales"));
35798
+ const hasPublicDir = existsSync17(join20(ws.rootDir, "public"));
35799
+ const hasLocales = existsSync17(join20(ws.rootDir, "locales"));
35578
35800
  let manifestPath;
35579
35801
  for (const name of ["manifest.webmanifest", "manifest.json"]) {
35580
- if (existsSync16(join19(ws.rootDir, name))) {
35802
+ if (existsSync17(join20(ws.rootDir, name))) {
35581
35803
  manifestPath = name;
35582
35804
  break;
35583
35805
  }
@@ -36192,7 +36414,7 @@ ${import_picocolors2.default.gray(m2)} ${s}
36192
36414
  };
36193
36415
 
36194
36416
  // src/deploy/survey.ts
36195
- async function runSurvey() {
36417
+ async function runSurvey(appName) {
36196
36418
  we("arc platform deploy \u2014 initial setup");
36197
36419
  const mode = await de({
36198
36420
  message: "How should deploy reach the target server?",
@@ -36294,12 +36516,17 @@ async function runSurvey() {
36294
36516
  });
36295
36517
  if (BD(location))
36296
36518
  cancel();
36519
+ const firstEnv = Object.keys(envs)[0] ?? "host";
36520
+ const appSlug = sanitizeImageName(appName ?? "arc-app").replace(/[^a-z0-9-]+/g, "-");
36521
+ const serverName = `arc-${appSlug}-${firstEnv}`.slice(0, 63);
36297
36522
  const terraform = {
36298
36523
  provider: "hcloud",
36299
36524
  serverType,
36300
36525
  location,
36301
36526
  image: "ubuntu-22.04",
36302
- tokenEnv
36527
+ tokenEnv,
36528
+ serverName,
36529
+ sshKeyStrategy: "cloud-init"
36303
36530
  };
36304
36531
  provision = { terraform };
36305
36532
  }
@@ -36384,12 +36611,15 @@ function cancel() {
36384
36611
  }
36385
36612
 
36386
36613
  // src/commands/platform-deploy.ts
36614
+ function federationExposesModules(ws) {
36615
+ return Boolean(ws.rootPkg?.arc?.federation?.slug);
36616
+ }
36387
36617
  async function platformDeploy(envArg, options = {}) {
36388
36618
  const ws = resolveWorkspace();
36389
36619
  let cfg;
36390
36620
  if (!deployConfigExists(ws.rootDir)) {
36391
36621
  log2("No deploy.arc.json found \u2014 launching survey.");
36392
- cfg = await runSurvey();
36622
+ cfg = await runSurvey(ws.appName);
36393
36623
  saveDeployConfig(ws.rootDir, cfg);
36394
36624
  ok("Saved deploy.arc.json");
36395
36625
  }
@@ -36398,6 +36628,12 @@ async function platformDeploy(envArg, options = {}) {
36398
36628
  for (const pg of pgEnvs) {
36399
36629
  ensurePersistedSecret(ws.rootDir, pg.name, pg.passwordKey, () => crypto.randomUUID().replace(/-/g, ""));
36400
36630
  }
36631
+ if (federationExposesModules(ws)) {
36632
+ for (const name of Object.keys(cfg.envs)) {
36633
+ ensurePersistedSecret(ws.rootDir, name, "ARC_APP_SECRET", () => randomBytes(32).toString("base64url"));
36634
+ ensurePersistedSecret(ws.rootDir, name, "ARC_PAIRING_TOKEN", () => randomBytes(16).toString("base64url"));
36635
+ }
36636
+ }
36401
36637
  if (cfg.observability?.enabled) {
36402
36638
  const key = cfg.observability.adminPasswordEnv ?? "ARC_OBSERVABILITY_PASSWORD";
36403
36639
  ensurePersistedSecret(ws.rootDir, "globals", key, () => crypto.randomUUID().replace(/-/g, ""));
@@ -36406,14 +36642,14 @@ async function platformDeploy(envArg, options = {}) {
36406
36642
  err(`Unknown env "${envArg}". Known: ${Object.keys(cfg.envs).join(", ")}`);
36407
36643
  process.exit(1);
36408
36644
  })() : Object.keys(cfg.envs);
36409
- const manifestPath = join20(ws.arcDir, "manifest.json");
36645
+ const manifestPath = join21(ws.arcDir, "manifest.json");
36410
36646
  if (!options.imageTag) {
36411
- const needBuild = options.rebuild || !existsSync17(manifestPath);
36647
+ const needBuild = options.rebuild || !existsSync18(manifestPath);
36412
36648
  if (needBuild && !options.skipBuild) {
36413
36649
  log2("Building platform...");
36414
36650
  await buildAll(ws, { noCache: options.rebuild });
36415
36651
  ok("Build complete");
36416
- } else if (!existsSync17(manifestPath)) {
36652
+ } else if (!existsSync18(manifestPath)) {
36417
36653
  err("No build found and --skip-build was set.");
36418
36654
  process.exit(1);
36419
36655
  }
@@ -36483,9 +36719,9 @@ function readCliVersion2() {
36483
36719
  let cur = dirname11(fileURLToPath8(import.meta.url));
36484
36720
  const root = dirname11(cur).startsWith("/") ? "/" : ".";
36485
36721
  while (cur !== root && cur !== "") {
36486
- const candidate = join20(cur, "package.json");
36487
- if (existsSync17(candidate)) {
36488
- const pkg = JSON.parse(readFileSync15(candidate, "utf-8"));
36722
+ const candidate = join21(cur, "package.json");
36723
+ if (existsSync18(candidate)) {
36724
+ const pkg = JSON.parse(readFileSync16(candidate, "utf-8"));
36489
36725
  if (pkg.name === "@arcote.tech/arc-cli") {
36490
36726
  return pkg.version ?? "unknown";
36491
36727
  }
@@ -36501,8 +36737,8 @@ function readCliVersion2() {
36501
36737
  }
36502
36738
  }
36503
36739
  async function hashDeployConfig(rootDir) {
36504
- const p2 = join20(rootDir, "deploy.arc.json");
36505
- const content = readFileSync15(p2);
36740
+ const p2 = join21(rootDir, "deploy.arc.json");
36741
+ const content = readFileSync16(p2);
36506
36742
  const hasher = new Bun.CryptoHasher("sha256");
36507
36743
  hasher.update(content);
36508
36744
  hasher.update(readCliVersion2());
@@ -36510,8 +36746,9 @@ async function hashDeployConfig(rootDir) {
36510
36746
  }
36511
36747
 
36512
36748
  // src/platform/startup.ts
36513
- import { existsSync as existsSync19, readFileSync as readFileSync16, watch } from "fs";
36514
- import { join as join22 } from "path";
36749
+ import { randomBytes as randomBytes2 } from "crypto";
36750
+ import { existsSync as existsSync20, mkdirSync as mkdirSync16, readFileSync as readFileSync17, watch, writeFileSync as writeFileSync16 } from "fs";
36751
+ import { join as join23 } from "path";
36515
36752
 
36516
36753
  // ../host/src/connection-manager.ts
36517
36754
  class ConnectionManager {
@@ -37944,8 +38181,10 @@ async function createArcServer(config) {
37944
38181
  const cronScheduler = new CronScheduler(contextHandler);
37945
38182
  cronScheduler.start();
37946
38183
  const connectionManager = new ConnectionManager;
38184
+ const messageChains = new Map;
37947
38185
  const coep = config.coep ?? "unsafe-none";
37948
38186
  const corsOrigins = config.corsOrigins;
38187
+ const allowPrivateNetwork = config.allowPrivateNetwork ?? false;
37949
38188
  const maxRequestBodySize = config.maxRequestBodySize ?? 10 * 1024 * 1024;
37950
38189
  const securityHeaders = {
37951
38190
  "X-Content-Type-Options": "nosniff",
@@ -37957,7 +38196,7 @@ async function createArcServer(config) {
37957
38196
  const origin = req?.headers.get("Origin") || null;
37958
38197
  const headers = {
37959
38198
  "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, PATCH, OPTIONS",
37960
- "Access-Control-Allow-Headers": "Content-Type, Authorization, X-Arc-Scope, X-Arc-Tokens",
38199
+ "Access-Control-Allow-Headers": "Content-Type, Authorization, X-Arc-Scope, X-Arc-Tokens, X-Arc-Pairing-Token",
37961
38200
  "Cross-Origin-Opener-Policy": "same-origin",
37962
38201
  "Cross-Origin-Embedder-Policy": coep,
37963
38202
  "Cross-Origin-Resource-Policy": "cross-origin",
@@ -37994,7 +38233,8 @@ async function createArcServer(config) {
37994
38233
  const url = new URL(req.url);
37995
38234
  const corsHeaders2 = buildCorsHeaders(req);
37996
38235
  if (req.method === "OPTIONS") {
37997
- return new Response(null, { headers: corsHeaders2 });
38236
+ const pna = allowPrivateNetwork && req.headers.get("Access-Control-Request-Private-Network") ? { "Access-Control-Allow-Private-Network": "true" } : {};
38237
+ return new Response(null, { headers: { ...corsHeaders2, ...pna } });
37998
38238
  }
37999
38239
  const handleRequest = async (span) => {
38000
38240
  const contentLength = Number(req.headers.get("Content-Length") ?? 0);
@@ -38082,29 +38322,38 @@ async function createArcServer(config) {
38082
38322
  console.error("WS handler error:", error);
38083
38323
  }
38084
38324
  };
38085
- const telemetry = config.telemetry;
38086
- if (!telemetry) {
38087
- await dispatch();
38088
- return;
38089
- }
38090
- const carrier = {};
38091
- if (typeof message?.traceparent === "string")
38092
- carrier.traceparent = message.traceparent;
38093
- if (typeof message?.tracestate === "string")
38094
- carrier.tracestate = message.tracestate;
38095
- await telemetry.runWithExtractedContext(carrier, () => telemetry.startSpan(`ws.${message?.type ?? "message"}`, () => dispatch(), {
38096
- kind: 2,
38097
- attributes: {
38098
- "messaging.system": "arc-ws",
38099
- "messaging.operation": "process",
38100
- "messaging.message.type": message?.type,
38101
- "arc.ws.client_id": client.id
38325
+ const processMessage = async () => {
38326
+ const telemetry = config.telemetry;
38327
+ if (!telemetry) {
38328
+ await dispatch();
38329
+ return;
38102
38330
  }
38103
- }));
38331
+ const carrier = {};
38332
+ if (typeof message?.traceparent === "string")
38333
+ carrier.traceparent = message.traceparent;
38334
+ if (typeof message?.tracestate === "string")
38335
+ carrier.tracestate = message.tracestate;
38336
+ await telemetry.runWithExtractedContext(carrier, () => telemetry.startSpan(`ws.${message?.type ?? "message"}`, () => dispatch(), {
38337
+ kind: 2,
38338
+ attributes: {
38339
+ "messaging.system": "arc-ws",
38340
+ "messaging.operation": "process",
38341
+ "messaging.message.type": message?.type,
38342
+ "arc.ws.client_id": client.id
38343
+ }
38344
+ }));
38345
+ };
38346
+ const prev = messageChains.get(client.id) ?? Promise.resolve();
38347
+ const next = prev.then(processMessage, processMessage).catch((error) => {
38348
+ console.error("WS message chain error:", error);
38349
+ });
38350
+ messageChains.set(client.id, next);
38351
+ await next;
38104
38352
  },
38105
38353
  close(ws) {
38106
38354
  const client = connectionManager.getClientByWs(ws);
38107
38355
  if (client) {
38356
+ messageChains.delete(client.id);
38108
38357
  cleanupClientSubs(client.id);
38109
38358
  config.onWsClose?.(client.id);
38110
38359
  connectionManager.removeClient(client.id);
@@ -38127,8 +38376,10 @@ async function createArcServer(config) {
38127
38376
  }
38128
38377
  // src/platform/server.ts
38129
38378
  init_i18n();
38130
- import { existsSync as existsSync18, mkdirSync as mkdirSync14 } from "fs";
38131
- import { join as join21 } from "path";
38379
+ import { timingSafeEqual } from "crypto";
38380
+ import { existsSync as existsSync19, mkdirSync as mkdirSync15 } from "fs";
38381
+ import { join as join22 } from "path";
38382
+ import { MAP_SCHEMA_VERSION } from "@arcote.tech/arc-map/types";
38132
38383
  async function resolveDbAdapterFactory(dbPath) {
38133
38384
  const databaseUrl = process.env.DATABASE_URL;
38134
38385
  if (databaseUrl && databaseUrl.length > 0) {
@@ -38138,10 +38389,10 @@ async function resolveDbAdapterFactory(dbPath) {
38138
38389
  const { createBunSQLiteAdapterFactory: createBunSQLiteAdapterFactory2 } = await Promise.resolve().then(() => (init_dist2(), exports_dist3));
38139
38390
  const dbDir = dbPath.substring(0, dbPath.lastIndexOf("/"));
38140
38391
  if (dbDir)
38141
- mkdirSync14(dbDir, { recursive: true });
38392
+ mkdirSync15(dbDir, { recursive: true });
38142
38393
  return createBunSQLiteAdapterFactory2(dbPath);
38143
38394
  }
38144
- function generateShellHtml(appName, manifest, initial, stylesHash) {
38395
+ function generateShellHtml(appName, manifest, initial, stylesHash, shell) {
38145
38396
  const otelConfig = process.env.ARC_OTEL_ENABLED === "true" ? {
38146
38397
  enabled: true,
38147
38398
  endpoint: "/otel",
@@ -38155,6 +38406,9 @@ function generateShellHtml(appName, manifest, initial, stylesHash) {
38155
38406
  if (!initialUrl) {
38156
38407
  throw new Error("generateShellHtml: initial bundle missing from manifest");
38157
38408
  }
38409
+ const importMapTag = shell ? `
38410
+ <script type="importmap">${JSON.stringify({ imports: shell.imports })}</script>
38411
+ <script>window.__ARC_PEERS__=${JSON.stringify(shell.peers)};</script>` : "";
38158
38412
  const stylesQs = stylesHash ? `?v=${stylesHash.slice(0, 16)}` : "";
38159
38413
  return `<!doctype html>
38160
38414
  <html lang="en">
@@ -38165,7 +38419,7 @@ function generateShellHtml(appName, manifest, initial, stylesHash) {
38165
38419
  <link rel="icon" href="${manifest.favicon}">` : ""}${manifest ? `
38166
38420
  <link rel="manifest" href="/manifest.json${stylesQs}">` : ""}
38167
38421
  <link rel="stylesheet" href="/styles.css${stylesQs}" />
38168
- <link rel="stylesheet" href="/theme.css${stylesQs}" />
38422
+ <link rel="stylesheet" href="/theme.css${stylesQs}" />${importMapTag}
38169
38423
  <link rel="modulepreload" href="${initialUrl}" />${otelTag}
38170
38424
  </head>
38171
38425
  <body>
@@ -38197,7 +38451,7 @@ function getMime(path4) {
38197
38451
  return MIME[ext2] ?? "application/octet-stream";
38198
38452
  }
38199
38453
  function serveFile(filePath, headers = {}) {
38200
- if (!existsSync18(filePath))
38454
+ if (!existsSync19(filePath))
38201
38455
  return new Response("Not Found", { status: 404 });
38202
38456
  return new Response(Bun.file(filePath), {
38203
38457
  headers: { "Content-Type": getMime(filePath), ...headers }
@@ -38215,11 +38469,44 @@ function ensureModuleSigSecret(ws, devMode) {
38215
38469
  MODULE_SIG_SECRET = crypto.randomUUID();
38216
38470
  }
38217
38471
  }
38218
- function signGroupUrl(file) {
38472
+ function signGroupUrl(file, prefix = "/browser") {
38219
38473
  const hasher = new Bun.CryptoHasher("sha256");
38220
38474
  hasher.update(`${file}:${MODULE_SIG_SECRET}`);
38221
38475
  const sig = hasher.digest("hex").slice(0, 16);
38222
- return `/browser/${file}?sig=${sig}`;
38476
+ return `${prefix}/${file}?sig=${sig}`;
38477
+ }
38478
+ async function filterGroupsForTokens(groups, moduleAccessMap, tokenPayloads, urlPrefix) {
38479
+ const tokensByName = new Map;
38480
+ for (const t of tokenPayloads) {
38481
+ if (t?.tokenType)
38482
+ tokensByName.set(t.tokenType, t);
38483
+ }
38484
+ const filtered = {};
38485
+ for (const [name, group] of Object.entries(groups)) {
38486
+ let allGranted = true;
38487
+ for (const moduleName of group.modules) {
38488
+ const access = moduleAccessMap.get(moduleName);
38489
+ if (!access || access.rules.length === 0)
38490
+ continue;
38491
+ let granted = false;
38492
+ for (const rule of access.rules) {
38493
+ const matching = tokensByName.get(rule.token.name);
38494
+ if (!matching)
38495
+ continue;
38496
+ granted = rule.check ? await rule.check(matching) : true;
38497
+ if (granted)
38498
+ break;
38499
+ }
38500
+ if (!granted) {
38501
+ allGranted = false;
38502
+ break;
38503
+ }
38504
+ }
38505
+ if (allGranted) {
38506
+ filtered[name] = { ...group, url: signGroupUrl(group.file, urlPrefix) };
38507
+ }
38508
+ }
38509
+ return filtered;
38223
38510
  }
38224
38511
  function verifyGroupSignature(file, sig) {
38225
38512
  if (!sig)
@@ -38260,36 +38547,7 @@ function parseArcTokensHeader(header) {
38260
38547
  return payloads;
38261
38548
  }
38262
38549
  async function filterManifestForTokens(manifest, moduleAccessMap, tokenPayloads) {
38263
- const tokensByName = new Map;
38264
- for (const t of tokenPayloads) {
38265
- if (t?.tokenType)
38266
- tokensByName.set(t.tokenType, t);
38267
- }
38268
- const filteredGroups = {};
38269
- for (const [name, group] of Object.entries(manifest.groups)) {
38270
- let allGranted = true;
38271
- for (const moduleName of group.modules) {
38272
- const access = moduleAccessMap.get(moduleName);
38273
- if (!access || access.rules.length === 0)
38274
- continue;
38275
- let granted = false;
38276
- for (const rule of access.rules) {
38277
- const matching = tokensByName.get(rule.token.name);
38278
- if (!matching)
38279
- continue;
38280
- granted = rule.check ? await rule.check(matching) : true;
38281
- if (granted)
38282
- break;
38283
- }
38284
- if (!granted) {
38285
- allGranted = false;
38286
- break;
38287
- }
38288
- }
38289
- if (allGranted) {
38290
- filteredGroups[name] = { ...group, url: signGroupUrl(group.file) };
38291
- }
38292
- }
38550
+ const filteredGroups = await filterGroupsForTokens(manifest.groups, moduleAccessMap, tokenPayloads, "/browser");
38293
38551
  return {
38294
38552
  initial: manifest.initial,
38295
38553
  groups: filteredGroups,
@@ -38315,28 +38573,58 @@ function staticFilesHandler(ws, devMode, getManifest) {
38315
38573
  return new Response("Forbidden", { status: 403, headers: ctx.corsHeaders });
38316
38574
  }
38317
38575
  }
38318
- return serveFile(join21(ws.browserDir, file), {
38576
+ return serveFile(join22(ws.browserDir, file), {
38577
+ ...ctx.corsHeaders,
38578
+ "Cache-Control": devMode ? "no-cache" : "max-age=31536000,immutable"
38579
+ });
38580
+ }
38581
+ if (path4.startsWith("/shell/")) {
38582
+ const file = path4.slice(7);
38583
+ if (!BROWSER_FILE_RE.test(file)) {
38584
+ return new Response("Not Found", { status: 404, headers: ctx.corsHeaders });
38585
+ }
38586
+ return serveFile(join22(ws.arcDir, "shell", file), {
38319
38587
  ...ctx.corsHeaders,
38588
+ "Cross-Origin-Resource-Policy": "cross-origin",
38589
+ "Cache-Control": devMode ? "no-cache" : "max-age=31536000,immutable"
38590
+ });
38591
+ }
38592
+ if (path4.startsWith("/browser-fed/")) {
38593
+ const file = path4.slice(13);
38594
+ if (!BROWSER_FILE_RE.test(file)) {
38595
+ return new Response("Not Found", { status: 404, headers: ctx.corsHeaders });
38596
+ }
38597
+ const fed = getManifest().federation;
38598
+ const isGroupEntry = fed ? Object.values(fed.build.groups).some((g3) => g3.file === file) : false;
38599
+ if (isGroupEntry) {
38600
+ const sig = url.searchParams.get("sig");
38601
+ if (!verifyGroupSignature(file, sig)) {
38602
+ return new Response("Forbidden", { status: 403, headers: ctx.corsHeaders });
38603
+ }
38604
+ }
38605
+ return serveFile(join22(ws.arcDir, "browser-fed", file), {
38606
+ ...ctx.corsHeaders,
38607
+ "Cross-Origin-Resource-Policy": "cross-origin",
38320
38608
  "Cache-Control": devMode ? "no-cache" : "max-age=31536000,immutable"
38321
38609
  });
38322
38610
  }
38323
38611
  if (path4.startsWith("/locales/"))
38324
- return serveFile(join21(ws.arcDir, path4.slice(1)), {
38612
+ return serveFile(join22(ws.arcDir, path4.slice(1)), {
38325
38613
  ...ctx.corsHeaders,
38326
38614
  "Cache-Control": devMode ? "no-cache" : "max-age=300,stale-while-revalidate=3600"
38327
38615
  });
38328
38616
  if (path4.startsWith("/assets/"))
38329
- return serveFile(join21(ws.assetsDir, path4.slice(8)), {
38617
+ return serveFile(join22(ws.assetsDir, path4.slice(8)), {
38330
38618
  ...ctx.corsHeaders,
38331
38619
  "Cache-Control": devMode ? "no-cache" : "max-age=31536000,immutable"
38332
38620
  });
38333
38621
  if (path4 === "/styles.css")
38334
- return serveFile(join21(ws.arcDir, "styles.css"), {
38622
+ return serveFile(join22(ws.arcDir, "styles.css"), {
38335
38623
  ...ctx.corsHeaders,
38336
38624
  "Cache-Control": devMode ? "no-cache" : "max-age=31536000,immutable"
38337
38625
  });
38338
38626
  if (path4 === "/theme.css")
38339
- return serveFile(join21(ws.arcDir, "theme.css"), {
38627
+ return serveFile(join22(ws.arcDir, "theme.css"), {
38340
38628
  ...ctx.corsHeaders,
38341
38629
  "Cache-Control": devMode ? "no-cache" : "max-age=31536000,immutable"
38342
38630
  });
@@ -38347,15 +38635,113 @@ function staticFilesHandler(ws, devMode, getManifest) {
38347
38635
  });
38348
38636
  }
38349
38637
  if (path4.lastIndexOf(".") > path4.lastIndexOf("/")) {
38350
- const publicFile = join21(ws.publicDir, path4.slice(1));
38351
- if (existsSync18(publicFile))
38638
+ const publicFile = join22(ws.publicDir, path4.slice(1));
38639
+ if (existsSync19(publicFile))
38352
38640
  return serveFile(publicFile, ctx.corsHeaders);
38353
38641
  }
38354
38642
  return null;
38355
38643
  };
38356
38644
  }
38357
- function apiEndpointsHandler(ws, getManifest, cm, moduleAccessMap, telemetry) {
38645
+ function parseCorsOrigins(raw) {
38646
+ if (!raw)
38647
+ return;
38648
+ const list = raw.split(",").map((s) => s.trim()).filter(Boolean);
38649
+ return list.length > 0 ? list : undefined;
38650
+ }
38651
+ function safeEqual(a2, b4) {
38652
+ const ab = Buffer.from(a2);
38653
+ const bb = Buffer.from(b4);
38654
+ if (ab.length !== bb.length)
38655
+ return false;
38656
+ return timingSafeEqual(ab, bb);
38657
+ }
38658
+ function handleDiscovery(req, ctx, ws, manifest, federation, mapUrl) {
38659
+ if (!federation) {
38660
+ return new Response("Not Found", { status: 404, headers: ctx.corsHeaders });
38661
+ }
38662
+ const presented = req.headers.get("X-Arc-Pairing-Token");
38663
+ if (!presented || !safeEqual(presented, federation.registrationToken)) {
38664
+ return new Response("Unauthorized", {
38665
+ status: 401,
38666
+ headers: ctx.corsHeaders
38667
+ });
38668
+ }
38669
+ const fed = manifest.federation;
38670
+ const platformUrl = process.env.ARC_PUBLIC_URL || new URL(req.url).origin;
38671
+ return Response.json({
38672
+ meta: {
38673
+ schemaVersion: MAP_SCHEMA_VERSION,
38674
+ name: ws.appName,
38675
+ version: ws.rootPkg?.version ?? undefined
38676
+ },
38677
+ platformUrl,
38678
+ federation: fed ? {
38679
+ slug: fed.config.slug,
38680
+ auth: fed.config.auth,
38681
+ modules: fed.modules,
38682
+ permissions: fed.permissions,
38683
+ slots: fed.slots
38684
+ } : null,
38685
+ peers: manifest.shell?.peers ?? null,
38686
+ federationManifestUrl: fed ? `${platformUrl}/api/federation/manifest` : null,
38687
+ mapUrl: mapUrl ?? null
38688
+ }, { headers: { ...ctx.corsHeaders, "Cache-Control": "no-cache, private" } });
38689
+ }
38690
+ async function handleFederationRegister(req, ctx, manifest, federation) {
38691
+ const fed = manifest.federation;
38692
+ if (!fed || !federation) {
38693
+ return new Response("Not Found", { status: 404, headers: ctx.corsHeaders });
38694
+ }
38695
+ const presented = req.headers.get("X-Arc-Pairing-Token");
38696
+ if (!presented || !safeEqual(presented, federation.registrationToken)) {
38697
+ return new Response("Unauthorized", {
38698
+ status: 401,
38699
+ headers: ctx.corsHeaders
38700
+ });
38701
+ }
38702
+ let body;
38703
+ try {
38704
+ body = await req.json();
38705
+ } catch {
38706
+ return new Response("Bad Request", {
38707
+ status: 400,
38708
+ headers: ctx.corsHeaders
38709
+ });
38710
+ }
38711
+ const hostId = typeof body.hostId === "string" ? body.hostId : "";
38712
+ const hostUrl = typeof body.hostUrl === "string" ? body.hostUrl : "";
38713
+ const hostName = typeof body.hostName === "string" ? body.hostName : hostId;
38714
+ const organizationId = typeof body.organizationId === "string" ? body.organizationId : "";
38715
+ if (!hostId || !hostUrl) {
38716
+ return new Response("Bad Request: hostId + hostUrl required", {
38717
+ status: 400,
38718
+ headers: ctx.corsHeaders
38719
+ });
38720
+ }
38721
+ ok(`Federation: host "${hostName}" (${hostUrl}) registered for org ${organizationId || "\u2014"}`);
38722
+ return Response.json({
38723
+ secret: federation.appSecret,
38724
+ slug: fed.config.slug,
38725
+ auth: fed.config.auth,
38726
+ modules: fed.modules,
38727
+ permissions: fed.permissions,
38728
+ slots: fed.slots
38729
+ }, { headers: { ...ctx.corsHeaders, "Cache-Control": "no-store" } });
38730
+ }
38731
+ function apiEndpointsHandler(ws, getManifest, cm, moduleAccessMap, telemetry, federation, getMapUrl) {
38358
38732
  return (req, url, ctx) => {
38733
+ if (url.pathname === "/api/discovery") {
38734
+ return handleDiscovery(req, ctx, ws, getManifest(), federation, getMapUrl?.());
38735
+ }
38736
+ if (url.pathname === "/api/federation/register") {
38737
+ if (req.method !== "POST") {
38738
+ return new Response("Method Not Allowed", {
38739
+ status: 405,
38740
+ headers: { ...ctx.corsHeaders, Allow: "POST, OPTIONS" }
38741
+ });
38742
+ }
38743
+ return handleFederationRegister(req, ctx, getManifest(), federation);
38744
+ }
38359
38745
  if (url.pathname === "/api/modules") {
38360
38746
  const arcTokensHeader = req.headers.get("X-Arc-Tokens");
38361
38747
  let payloads = parseArcTokensHeader(arcTokensHeader);
@@ -38369,6 +38755,36 @@ function apiEndpointsHandler(ws, getManifest, cm, moduleAccessMap, telemetry) {
38369
38755
  }
38370
38756
  }));
38371
38757
  }
38758
+ if (url.pathname === "/api/federation/manifest") {
38759
+ const m4 = getManifest();
38760
+ if (!m4.federation || !m4.shell) {
38761
+ return new Response("Not Found", { status: 404, headers: ctx.corsHeaders });
38762
+ }
38763
+ const arcTokensHeader = req.headers.get("X-Arc-Tokens");
38764
+ let payloads = parseArcTokensHeader(arcTokensHeader);
38765
+ if (payloads.length === 0 && ctx.tokenPayload) {
38766
+ payloads = [ctx.tokenPayload];
38767
+ }
38768
+ const fed = m4.federation;
38769
+ return filterGroupsForTokens(fed.build.groups, moduleAccessMap, payloads, "/browser-fed").then((groups) => Response.json({
38770
+ initial: fed.build.initial,
38771
+ groups,
38772
+ sharedChunks: fed.build.sharedChunks,
38773
+ stylesHash: m4.stylesHash,
38774
+ peers: m4.shell.peers,
38775
+ app: {
38776
+ slug: fed.config.slug,
38777
+ name: ws.appName,
38778
+ version: ws.rootPkg.version ?? undefined,
38779
+ modules: fed.modules,
38780
+ permissions: fed.permissions,
38781
+ slots: fed.slots,
38782
+ auth: fed.config.auth
38783
+ }
38784
+ }, {
38785
+ headers: { ...ctx.corsHeaders, "Cache-Control": "no-cache, private" }
38786
+ }));
38787
+ }
38372
38788
  if (url.pathname === "/api/translations") {
38373
38789
  const config = readTranslationsConfig(ws.rootDir);
38374
38790
  return Response.json(config ?? { locales: [], sourceLocale: "" }, {
@@ -38414,6 +38830,17 @@ function devReloadHandler(sseClients) {
38414
38830
  });
38415
38831
  };
38416
38832
  }
38833
+ function headlessFallbackHandler(ws, getManifest) {
38834
+ return (_req, _url, ctx) => {
38835
+ const m4 = getManifest();
38836
+ return Response.json({
38837
+ arc: "headless",
38838
+ app: ws.appName,
38839
+ federation: m4.federation ?? null,
38840
+ endpoints: ["/api/federation/manifest", "/api/modules", "/health"]
38841
+ }, { headers: ctx.corsHeaders });
38842
+ };
38843
+ }
38417
38844
  function spaFallbackHandler(getShellHtml) {
38418
38845
  return (_req, _url, ctx) => {
38419
38846
  return new Response(getShellHtml(), {
@@ -38456,8 +38883,14 @@ async function startPlatformServer(opts) {
38456
38883
  const setManifest = (m4) => {
38457
38884
  manifest = m4;
38458
38885
  };
38459
- const getShellHtml = () => generateShellHtml(ws.appName, ws.manifest, manifest?.initial, manifest?.stylesHash);
38886
+ let mapUrl = opts.mapUrl;
38887
+ const setMapUrl = (u2) => {
38888
+ mapUrl = u2;
38889
+ };
38890
+ const getMapUrl = () => mapUrl;
38891
+ const getShellHtml = () => generateShellHtml(ws.appName, ws.manifest, manifest?.initial, manifest?.stylesHash, manifest?.shell);
38460
38892
  const sseClients = new Set;
38893
+ const fallbackHandler = opts.headless ? headlessFallbackHandler(ws, getManifest) : spaFallbackHandler(getShellHtml);
38461
38894
  const notifyReload = (m4) => {
38462
38895
  const data = JSON.stringify(m4);
38463
38896
  for (const c2 of sseClients) {
@@ -38483,18 +38916,23 @@ async function startPlatformServer(opts) {
38483
38916
  port,
38484
38917
  async fetch(req) {
38485
38918
  const url = new URL(req.url);
38486
- if (req.method === "OPTIONS")
38487
- return new Response(null, { headers: cors });
38919
+ if (req.method === "OPTIONS") {
38920
+ const headers = { ...cors };
38921
+ if (req.headers.get("Access-Control-Request-Private-Network")) {
38922
+ headers["Access-Control-Allow-Private-Network"] = "true";
38923
+ }
38924
+ return new Response(null, { headers });
38925
+ }
38488
38926
  const ctx = {
38489
38927
  rawToken: null,
38490
38928
  tokenPayload: null,
38491
38929
  corsHeaders: cors
38492
38930
  };
38493
38931
  const handlers = [
38494
- apiEndpointsHandler(ws, getManifest, null, moduleAccessMap, telemetry),
38932
+ apiEndpointsHandler(ws, getManifest, null, moduleAccessMap, telemetry, opts.federation, getMapUrl),
38495
38933
  devReloadHandler(sseClients),
38496
38934
  staticFilesHandler(ws, !!devMode, getManifest),
38497
- spaFallbackHandler(getShellHtml)
38935
+ fallbackHandler
38498
38936
  ];
38499
38937
  for (const handler of handlers) {
38500
38938
  const response = await handler(req, url, ctx);
@@ -38509,11 +38947,12 @@ async function startPlatformServer(opts) {
38509
38947
  contextHandler: null,
38510
38948
  connectionManager: null,
38511
38949
  setManifest,
38950
+ setMapUrl,
38512
38951
  notifyReload,
38513
38952
  stop: () => server.stop()
38514
38953
  };
38515
38954
  }
38516
- const dbPath = opts.dbPath || join21(ws.arcDir, "data", "arc.db");
38955
+ const dbPath = opts.dbPath || join22(ws.arcDir, "data", "arc.db");
38517
38956
  const baseDbFactory = await resolveDbAdapterFactory(dbPath);
38518
38957
  let dbAdapterFactory = baseDbFactory;
38519
38958
  if (telemetry) {
@@ -38529,11 +38968,13 @@ async function startPlatformServer(opts) {
38529
38968
  dbAdapterFactory,
38530
38969
  port,
38531
38970
  coep,
38971
+ corsOrigins: parseCorsOrigins(process.env.ARC_CORS_ORIGINS),
38972
+ allowPrivateNetwork: !!devMode,
38532
38973
  httpHandlers: [
38533
- apiEndpointsHandler(ws, getManifest, null, moduleAccessMap, telemetry),
38974
+ apiEndpointsHandler(ws, getManifest, null, moduleAccessMap, telemetry, opts.federation, getMapUrl),
38534
38975
  devReloadHandler(sseClients),
38535
38976
  staticFilesHandler(ws, !!devMode, getManifest),
38536
- spaFallbackHandler(getShellHtml)
38977
+ fallbackHandler
38537
38978
  ],
38538
38979
  onWsClose: (clientId) => cleanupClientSubs(clientId),
38539
38980
  telemetry
@@ -38543,6 +38984,7 @@ async function startPlatformServer(opts) {
38543
38984
  contextHandler: arcServer.contextHandler,
38544
38985
  connectionManager: arcServer.connectionManager,
38545
38986
  setManifest,
38987
+ setMapUrl,
38546
38988
  notifyReload,
38547
38989
  stop: () => {
38548
38990
  arcServer.stop();
@@ -38555,17 +38997,23 @@ async function startPlatformServer(opts) {
38555
38997
  async function startPlatform(opts) {
38556
38998
  const { ws, devMode } = opts;
38557
38999
  const port = opts.port ?? parseInt(process.env.PORT || "5005", 10);
38558
- const dbPath = opts.dbPath ?? join22(ws.rootDir, ".arc", "data", devMode ? "dev.db" : "prod.db");
39000
+ const dbPath = opts.dbPath ?? join23(ws.rootDir, ".arc", "data", devMode ? "dev.db" : "prod.db");
38559
39001
  let manifest;
38560
39002
  if (devMode) {
38561
39003
  manifest = await buildAll(ws);
38562
39004
  } else {
38563
- const manifestPath = join22(ws.arcDir, "manifest.json");
38564
- if (!existsSync19(manifestPath)) {
39005
+ const manifestPath = join23(ws.arcDir, "manifest.json");
39006
+ if (!existsSync20(manifestPath)) {
38565
39007
  err("No build found. Run `arc platform build` first.");
38566
39008
  process.exit(1);
38567
39009
  }
38568
- manifest = JSON.parse(readFileSync16(manifestPath, "utf-8"));
39010
+ manifest = JSON.parse(readFileSync17(manifestPath, "utf-8"));
39011
+ }
39012
+ assertProdFederationSecrets(devMode, !!manifest.federation);
39013
+ const needsPairing = !!manifest.federation || devMode && (opts.map || opts.headless);
39014
+ const pairingToken = needsPairing ? resolvePairingToken(ws.rootDir) : undefined;
39015
+ if (manifest.federation) {
39016
+ process.env.ARC_APP_SECRET = resolveAppSecret(ws.rootDir);
38569
39017
  }
38570
39018
  log2("Loading server context...");
38571
39019
  const { context: context2, moduleAccess } = await loadServerContext(ws);
@@ -38586,7 +39034,12 @@ async function startPlatform(opts) {
38586
39034
  context: context2,
38587
39035
  moduleAccess,
38588
39036
  dbPath,
38589
- devMode
39037
+ devMode,
39038
+ headless: opts.headless,
39039
+ federation: manifest.federation && pairingToken ? {
39040
+ registrationToken: pairingToken,
39041
+ appSecret: process.env.ARC_APP_SECRET
39042
+ } : undefined
38590
39043
  });
38591
39044
  break;
38592
39045
  } catch (e2) {
@@ -38607,8 +39060,18 @@ async function startPlatform(opts) {
38607
39060
  ok("Commands, queries, WebSocket \u2014 all on same port");
38608
39061
  }
38609
39062
  let onReload;
38610
- if (devMode && opts.map) {
38611
- onReload = await startMapTool(ws, actualPort + 1);
39063
+ if (devMode && (opts.map || opts.headless)) {
39064
+ onReload = await startMapTool(ws, actualPort + 1, pairingToken, platform3, {
39065
+ platformUrl: `http://localhost:${actualPort}`,
39066
+ federation: manifest.federation ? {
39067
+ slug: manifest.federation.config.slug,
39068
+ auth: manifest.federation.config.auth,
39069
+ modules: manifest.federation.modules,
39070
+ permissions: manifest.federation.permissions,
39071
+ slots: manifest.federation.slots
39072
+ } : undefined,
39073
+ peers: manifest.shell?.peers
39074
+ });
38612
39075
  }
38613
39076
  if (devMode) {
38614
39077
  attachDevWatcher(ws, platform3, onReload);
@@ -38620,25 +39083,98 @@ async function startPlatform(opts) {
38620
39083
  process.on("SIGTERM", cleanup);
38621
39084
  process.on("SIGINT", cleanup);
38622
39085
  }
38623
- async function startMapTool(ws, port) {
39086
+ async function startMapTool(ws, port, authToken, platformServer, platform3) {
38624
39087
  try {
38625
- const { startMapServer } = await import("@arcote.tech/arc-map");
39088
+ const { startMapServer, MAP_SCHEMA_VERSION: MAP_SCHEMA_VERSION2 } = await import("@arcote.tech/arc-map");
38626
39089
  const { getContext } = await import("@arcote.tech/platform");
38627
- const globs = ws.packages.map((p3) => join22(p3.path, "src", "**", "*.{ts,tsx}"));
38628
- const packageOf = (absFile) => ws.packages.find((p3) => absFile.startsWith(join22(p3.path, "src")))?.name;
39090
+ const globs = ws.packages.map((p3) => join23(p3.path, "src", "**", "*.{ts,tsx}"));
39091
+ const packageOf = (absFile) => ws.packages.find((p3) => absFile.startsWith(join23(p3.path, "src")))?.name;
39092
+ const meta = readAppMeta(ws.rootDir, MAP_SCHEMA_VERSION2);
38629
39093
  const map = await startMapServer({
38630
39094
  getContext: () => getContext(),
38631
39095
  scan: { globs, workspaceRoot: ws.rootDir, packageOf },
38632
39096
  useCasePackages: ws.packages.map((p3) => ({ name: p3.name, path: p3.path })),
38633
- port
39097
+ port,
39098
+ authToken,
39099
+ meta,
39100
+ platform: platform3
38634
39101
  });
39102
+ platformServer.setMapUrl(map.url);
38635
39103
  ok(`Context map \u2192 ${map.url}`);
39104
+ ok(`Pairing token: ${authToken}`);
38636
39105
  return map.notifyReload;
38637
39106
  } catch (e2) {
38638
39107
  err(`Context map failed to start: ${e2.message}`);
38639
39108
  return;
38640
39109
  }
38641
39110
  }
39111
+ function assertProdFederationSecrets(devMode, hasFederation) {
39112
+ if (devMode || !hasFederation)
39113
+ return;
39114
+ const missing = [];
39115
+ if (!process.env.ARC_APP_SECRET)
39116
+ missing.push("ARC_APP_SECRET");
39117
+ if (!process.env.ARC_PAIRING_TOKEN && !process.env.ARC_MAP_TOKEN) {
39118
+ missing.push("ARC_PAIRING_TOKEN");
39119
+ }
39120
+ if (missing.length === 0)
39121
+ return;
39122
+ err(`Federacja w produkcji wymaga zmiennych \u015Brodowiskowych: ${missing.join(", ")}.
39123
+ ` + ` Bez nich sekret i token s\u0105 generowane w kontenerze i GIN\u0104 przy pierwszym
39124
+ ` + ` recreate \u2014 hosty musia\u0142yby przeinstalowa\u0107 aplikacj\u0119, bez \u017Cadnego b\u0142\u0119du.
39125
+ ` + ` Ustaw je w deployu (deploy.arc.json \u2192 envVars + deploy.arc.<env>.env).`);
39126
+ process.exit(1);
39127
+ }
39128
+ function resolveAppSecret(rootDir) {
39129
+ const fromEnv = process.env.ARC_APP_SECRET;
39130
+ if (fromEnv)
39131
+ return fromEnv;
39132
+ const file = join23(rootDir, ".arc", "app-secret");
39133
+ try {
39134
+ const saved = readFileSync17(file, "utf-8").trim();
39135
+ if (saved)
39136
+ return saved;
39137
+ } catch {}
39138
+ const secret = randomBytes2(32).toString("base64url");
39139
+ try {
39140
+ mkdirSync16(join23(rootDir, ".arc"), { recursive: true });
39141
+ writeFileSync16(file, secret, { mode: 384 });
39142
+ } catch (e2) {
39143
+ err(`App secret not persisted (${e2.message}) \u2014 hosts will need re-install after restart`);
39144
+ }
39145
+ return secret;
39146
+ }
39147
+ function resolvePairingToken(rootDir) {
39148
+ const fromEnv = process.env.ARC_PAIRING_TOKEN || process.env.ARC_MAP_TOKEN;
39149
+ if (fromEnv)
39150
+ return fromEnv;
39151
+ const file = join23(rootDir, ".arc", "pairing-token");
39152
+ try {
39153
+ const saved = readFileSync17(file, "utf-8").trim();
39154
+ if (saved)
39155
+ return saved;
39156
+ } catch {}
39157
+ const token = randomBytes2(16).toString("base64url");
39158
+ try {
39159
+ mkdirSync16(join23(rootDir, ".arc"), { recursive: true });
39160
+ writeFileSync16(file, token, { mode: 384 });
39161
+ } catch (e2) {
39162
+ err(`Pairing token not persisted (${e2.message}) \u2014 it will change on restart`);
39163
+ }
39164
+ return token;
39165
+ }
39166
+ function readAppMeta(rootDir, schemaVersion) {
39167
+ try {
39168
+ const pkg = JSON.parse(readFileSync17(join23(rootDir, "package.json"), "utf-8"));
39169
+ return {
39170
+ schemaVersion,
39171
+ name: pkg.name ?? rootDir.split("/").pop() ?? "arc-app",
39172
+ version: pkg.version
39173
+ };
39174
+ } catch {
39175
+ return { schemaVersion, name: rootDir.split("/").pop() ?? "arc-app" };
39176
+ }
39177
+ }
38642
39178
  function attachDevWatcher(ws, platform3, onReload) {
38643
39179
  log2("Watching for changes...");
38644
39180
  let rebuildTimer = null;
@@ -38665,8 +39201,8 @@ function attachDevWatcher(ws, platform3, onReload) {
38665
39201
  }, 300);
38666
39202
  };
38667
39203
  for (const pkg of ws.packages) {
38668
- const srcDir = join22(pkg.path, "src");
38669
- if (!existsSync19(srcDir))
39204
+ const srcDir = join23(pkg.path, "src");
39205
+ if (!existsSync20(srcDir))
38670
39206
  continue;
38671
39207
  watch(srcDir, { recursive: true }, (_event, filename) => {
38672
39208
  if (!filename || filename.includes(".arc") || filename.endsWith(".d.ts") || filename.includes("node_modules") || filename.includes("dist"))
@@ -38676,8 +39212,8 @@ function attachDevWatcher(ws, platform3, onReload) {
38676
39212
  triggerRebuild();
38677
39213
  });
38678
39214
  }
38679
- const localesDir = join22(ws.rootDir, "locales");
38680
- if (existsSync19(localesDir)) {
39215
+ const localesDir = join23(ws.rootDir, "locales");
39216
+ if (existsSync20(localesDir)) {
38681
39217
  watch(localesDir, { recursive: false }, (_event, filename) => {
38682
39218
  if (!filename?.endsWith(".po"))
38683
39219
  return;
@@ -38689,7 +39225,12 @@ function attachDevWatcher(ws, platform3, onReload) {
38689
39225
  // src/commands/platform-dev.ts
38690
39226
  async function platformDev(opts = {}) {
38691
39227
  const ws = resolveWorkspace();
38692
- await startPlatform({ ws, devMode: true, map: opts.map });
39228
+ await startPlatform({
39229
+ ws,
39230
+ devMode: true,
39231
+ map: opts.map,
39232
+ headless: opts.headless
39233
+ });
38693
39234
  }
38694
39235
 
38695
39236
  // src/commands/platform-start.ts
@@ -38704,7 +39245,11 @@ program2.name("arc").description("CLI tool for Arc framework").version("0.0.3");
38704
39245
  program2.command("dev").description("Run development mode for Arc framework").action(dev);
38705
39246
  program2.command("build").description("Build all clients and declarations").action(build);
38706
39247
  var platform3 = program2.command("platform").description("Platform commands \u2014 run full stack (server + UI)");
38707
- platform3.command("dev").description("Start platform in dev mode (Bun server + Vite HMR)").option("--no-cache", "Force full rebuild on startup").option("--map", "Also start the Arc Context Map dev tool on a separate port").action((opts) => platformDev({ noCache: opts.cache === false, map: opts.map }));
39248
+ platform3.command("dev").description("Start platform in dev mode (Bun server + Vite HMR)").option("--no-cache", "Force full rebuild on startup").option("--map", "Also start the Arc Context Map dev tool on a separate port").option("--headless", "Host API + federated module chunks + map, WITHOUT the app's own SPA shell (implies --map)").action((opts) => platformDev({
39249
+ noCache: opts.cache === false,
39250
+ map: opts.map,
39251
+ headless: opts.headless
39252
+ }));
38708
39253
  platform3.command("build").description("Build platform for production").option("--no-cache", "Force full rebuild").action((opts) => platformBuild({ noCache: opts.cache === false }));
38709
39254
  platform3.command("start").description("Start platform in production mode (requires prior build)").action(platformStart);
38710
39255
  platform3.command("deploy [env]").description("Deploy platform to a remote server (reads deploy.arc.json, surveys if missing)").option("--skip-build", "Skip local build step").option("--rebuild", "Force rebuild before deploy").option("--build-only", "Build the Docker image locally, then exit (no remote push)").option("--image-tag <hash>", "Roll back / pin to an existing image tag instead of building a new one").option("--force-bootstrap", "Re-run Ansible host bootstrap even if the server is already configured").action((env2, opts) => platformDeploy(env2, opts));