@arcote.tech/arc-cli 0.7.32 → 0.8.1

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
@@ -10204,11 +10204,18 @@ function notifyTokenChange(scope) {
10204
10204
 
10205
10205
  class AuthAdapter {
10206
10206
  scopes = new Map;
10207
+ namespace;
10208
+ constructor(options) {
10209
+ this.namespace = options?.storageNamespace || undefined;
10210
+ }
10211
+ storageKey(scope) {
10212
+ return this.namespace ? `${TOKEN_PREFIX}${this.namespace}:${scope}` : TOKEN_PREFIX + scope;
10213
+ }
10207
10214
  setToken(token, scope = "default") {
10208
10215
  if (!token) {
10209
10216
  this.scopes.delete(scope);
10210
10217
  if (hasLocalStorage()) {
10211
- localStorage.removeItem(TOKEN_PREFIX + scope);
10218
+ localStorage.removeItem(this.storageKey(scope));
10212
10219
  notifyTokenChange(scope);
10213
10220
  }
10214
10221
  return;
@@ -10218,7 +10225,7 @@ class AuthAdapter {
10218
10225
  if (parts.length !== 3) {
10219
10226
  this.scopes.delete(scope);
10220
10227
  if (hasLocalStorage())
10221
- localStorage.removeItem(TOKEN_PREFIX + scope);
10228
+ localStorage.removeItem(this.storageKey(scope));
10222
10229
  return;
10223
10230
  }
10224
10231
  const payload = JSON.parse(atob(parts[1]));
@@ -10232,13 +10239,13 @@ class AuthAdapter {
10232
10239
  }
10233
10240
  });
10234
10241
  if (hasLocalStorage()) {
10235
- localStorage.setItem(TOKEN_PREFIX + scope, token);
10242
+ localStorage.setItem(this.storageKey(scope), token);
10236
10243
  notifyTokenChange(scope);
10237
10244
  }
10238
10245
  } catch {
10239
10246
  this.scopes.delete(scope);
10240
10247
  if (hasLocalStorage())
10241
- localStorage.removeItem(TOKEN_PREFIX + scope);
10248
+ localStorage.removeItem(this.storageKey(scope));
10242
10249
  }
10243
10250
  }
10244
10251
  setDecoded(decoded, scope = "default") {
@@ -10247,10 +10254,13 @@ class AuthAdapter {
10247
10254
  loadPersisted() {
10248
10255
  if (!hasLocalStorage())
10249
10256
  return;
10257
+ const prefix = this.namespace ? `${TOKEN_PREFIX}${this.namespace}:` : TOKEN_PREFIX;
10250
10258
  for (let i = 0;i < localStorage.length; i++) {
10251
10259
  const key = localStorage.key(i);
10252
- if (key?.startsWith(TOKEN_PREFIX)) {
10253
- const scope = key.slice(TOKEN_PREFIX.length);
10260
+ if (key?.startsWith(prefix)) {
10261
+ const scope = key.slice(prefix.length);
10262
+ if (!this.namespace && scope.includes(":"))
10263
+ continue;
10254
10264
  const raw = localStorage.getItem(key);
10255
10265
  if (raw) {
10256
10266
  try {
@@ -10304,7 +10314,7 @@ class AuthAdapter {
10304
10314
  clear() {
10305
10315
  if (hasLocalStorage()) {
10306
10316
  for (const scope of this.scopes.keys()) {
10307
- localStorage.removeItem(TOKEN_PREFIX + scope);
10317
+ localStorage.removeItem(this.storageKey(scope));
10308
10318
  }
10309
10319
  }
10310
10320
  this.scopes.clear();
@@ -11393,6 +11403,8 @@ class ArcFunction {
11393
11403
  if (!tokenInstance) {
11394
11404
  return false;
11395
11405
  }
11406
+ if (!protection.check)
11407
+ continue;
11396
11408
  const result = await protection.check(tokenInstance);
11397
11409
  if (result === false) {
11398
11410
  return false;
@@ -17839,11 +17851,18 @@ function notifyTokenChange2(scope) {
17839
17851
 
17840
17852
  class AuthAdapter2 {
17841
17853
  scopes = new Map;
17854
+ namespace;
17855
+ constructor(options) {
17856
+ this.namespace = options?.storageNamespace || undefined;
17857
+ }
17858
+ storageKey(scope) {
17859
+ return this.namespace ? `${TOKEN_PREFIX2}${this.namespace}:${scope}` : TOKEN_PREFIX2 + scope;
17860
+ }
17842
17861
  setToken(token, scope = "default") {
17843
17862
  if (!token) {
17844
17863
  this.scopes.delete(scope);
17845
17864
  if (hasLocalStorage2()) {
17846
- localStorage.removeItem(TOKEN_PREFIX2 + scope);
17865
+ localStorage.removeItem(this.storageKey(scope));
17847
17866
  notifyTokenChange2(scope);
17848
17867
  }
17849
17868
  return;
@@ -17853,7 +17872,7 @@ class AuthAdapter2 {
17853
17872
  if (parts.length !== 3) {
17854
17873
  this.scopes.delete(scope);
17855
17874
  if (hasLocalStorage2())
17856
- localStorage.removeItem(TOKEN_PREFIX2 + scope);
17875
+ localStorage.removeItem(this.storageKey(scope));
17857
17876
  return;
17858
17877
  }
17859
17878
  const payload = JSON.parse(atob(parts[1]));
@@ -17867,13 +17886,13 @@ class AuthAdapter2 {
17867
17886
  }
17868
17887
  });
17869
17888
  if (hasLocalStorage2()) {
17870
- localStorage.setItem(TOKEN_PREFIX2 + scope, token);
17889
+ localStorage.setItem(this.storageKey(scope), token);
17871
17890
  notifyTokenChange2(scope);
17872
17891
  }
17873
17892
  } catch {
17874
17893
  this.scopes.delete(scope);
17875
17894
  if (hasLocalStorage2())
17876
- localStorage.removeItem(TOKEN_PREFIX2 + scope);
17895
+ localStorage.removeItem(this.storageKey(scope));
17877
17896
  }
17878
17897
  }
17879
17898
  setDecoded(decoded, scope = "default") {
@@ -17882,10 +17901,13 @@ class AuthAdapter2 {
17882
17901
  loadPersisted() {
17883
17902
  if (!hasLocalStorage2())
17884
17903
  return;
17904
+ const prefix = this.namespace ? `${TOKEN_PREFIX2}${this.namespace}:` : TOKEN_PREFIX2;
17885
17905
  for (let i = 0;i < localStorage.length; i++) {
17886
17906
  const key = localStorage.key(i);
17887
- if (key?.startsWith(TOKEN_PREFIX2)) {
17888
- const scope = key.slice(TOKEN_PREFIX2.length);
17907
+ if (key?.startsWith(prefix)) {
17908
+ const scope = key.slice(prefix.length);
17909
+ if (!this.namespace && scope.includes(":"))
17910
+ continue;
17889
17911
  const raw = localStorage.getItem(key);
17890
17912
  if (raw) {
17891
17913
  try {
@@ -17939,7 +17961,7 @@ class AuthAdapter2 {
17939
17961
  clear() {
17940
17962
  if (hasLocalStorage2()) {
17941
17963
  for (const scope of this.scopes.keys()) {
17942
- localStorage.removeItem(TOKEN_PREFIX2 + scope);
17964
+ localStorage.removeItem(this.storageKey(scope));
17943
17965
  }
17944
17966
  }
17945
17967
  this.scopes.clear();
@@ -19028,6 +19050,8 @@ class ArcFunction2 {
19028
19050
  if (!tokenInstance) {
19029
19051
  return false;
19030
19052
  }
19053
+ if (!protection.check)
19054
+ continue;
19031
19055
  const result = await protection.check(tokenInstance);
19032
19056
  if (result === false) {
19033
19057
  return false;
@@ -31298,20 +31322,20 @@ ${colors3.yellow}Type declaration errors:${colors3.reset}`);
31298
31322
  }
31299
31323
 
31300
31324
  // 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";
31325
+ import { copyFileSync, existsSync as existsSync11, mkdirSync as mkdirSync10, readdirSync as readdirSync6, readFileSync as readFileSync12, writeFileSync as writeFileSync10 } from "fs";
31326
+ import { dirname as dirname8, join as join13 } from "path";
31303
31327
 
31304
31328
  // src/builder/module-builder.ts
31305
31329
  import { execSync } from "child_process";
31306
31330
  import {
31307
- existsSync as existsSync7,
31308
- mkdirSync as mkdirSync6,
31309
- readFileSync as readFileSync7,
31331
+ existsSync as existsSync8,
31332
+ mkdirSync as mkdirSync7,
31333
+ readFileSync as readFileSync8,
31310
31334
  readdirSync as readdirSync4,
31311
- rmSync as rmSync2,
31312
- writeFileSync as writeFileSync6
31335
+ rmSync as rmSync3,
31336
+ writeFileSync as writeFileSync7
31313
31337
  } from "fs";
31314
- import { basename as basename2, dirname as dirname6, join as join8, relative as relative3 } from "path";
31338
+ import { basename as basename3, dirname as dirname6, join as join9, relative as relative3 } from "path";
31315
31339
  import { builtinModules } from "module";
31316
31340
  init_i18n();
31317
31341
  init_compile();
@@ -31390,10 +31414,21 @@ var FRAMEWORK_PEERS = [...ARC_PEERS, ...REACT_PEERS];
31390
31414
  var SHELL_EXTERNALS = [
31391
31415
  ...FRAMEWORK_PEERS,
31392
31416
  "react/jsx-runtime",
31393
- "react/jsx-dev-runtime"
31417
+ "react-dom/client"
31394
31418
  ];
31395
31419
  var FRAMEWORK_PEER_SET = new Set(FRAMEWORK_PEERS);
31396
31420
  var SHELL_EXTERNAL_SET = new Set(SHELL_EXTERNALS);
31421
+ function shortNameOf(pkg) {
31422
+ if (pkg === "@arcote.tech/platform")
31423
+ return "platform";
31424
+ if (pkg.startsWith("@arcote.tech/"))
31425
+ return pkg.slice("@arcote.tech/".length);
31426
+ return pkg;
31427
+ }
31428
+
31429
+ // src/builder/peer-shell.ts
31430
+ import { existsSync as existsSync7, mkdirSync as mkdirSync6, readFileSync as readFileSync7, writeFileSync as writeFileSync6 } from "fs";
31431
+ import { basename as basename2, join as join8 } from "path";
31397
31432
 
31398
31433
  // src/builder/hash.ts
31399
31434
  import { existsSync as existsSync6, readFileSync as readFileSync6, readdirSync as readdirSync3, statSync } from "fs";
@@ -31474,6 +31509,88 @@ function mtimeOf(path4) {
31474
31509
  }
31475
31510
  }
31476
31511
 
31512
+ // src/builder/peer-shell.ts
31513
+ var RUNTIME_GLOBAL = "__ARC_RT";
31514
+ function peerVersion(rootDir, pkg) {
31515
+ try {
31516
+ const p = JSON.parse(readFileSync7(join8(rootDir, "node_modules", pkg, "package.json"), "utf-8"));
31517
+ return p.version ?? "0.0.0";
31518
+ } catch {
31519
+ return "0.0.0";
31520
+ }
31521
+ }
31522
+ async function namedExportsOf(specifier) {
31523
+ try {
31524
+ const mod = await import(specifier);
31525
+ return Object.keys(mod).filter((k) => k !== "default" && /^[A-Za-z_$][\w$]*$/.test(k));
31526
+ } catch {
31527
+ return [];
31528
+ }
31529
+ }
31530
+ function runtimeExposePrelude() {
31531
+ const lines = SHELL_EXTERNALS.map((spec, i) => `import * as __rt${i} from ${JSON.stringify(spec)};`);
31532
+ const assigns = SHELL_EXTERNALS.map((spec, i) => ` ${JSON.stringify(spec)}: (__rt${i}.default ?? __rt${i}),
31533
+ ${JSON.stringify(spec + "#ns")}: __rt${i},`);
31534
+ return lines.join(`
31535
+ `) + `
31536
+ globalThis.${RUNTIME_GLOBAL} = Object.assign(globalThis.${RUNTIME_GLOBAL} ?? {}, {
31537
+ ` + assigns.join(`
31538
+ `) + `
31539
+ });
31540
+ `;
31541
+ }
31542
+ async function buildPeerShell(rootDir, outDir, cache, noCache) {
31543
+ mkdirSync6(outDir, { recursive: true });
31544
+ const peers = {};
31545
+ for (const pkg of FRAMEWORK_PEERS)
31546
+ peers[pkg] = peerVersion(rootDir, pkg);
31547
+ const namedBySpec = {};
31548
+ for (const spec of SHELL_EXTERNALS)
31549
+ namedBySpec[spec] = await namedExportsOf(spec);
31550
+ const unitId = "peer-shell";
31551
+ const inputHash = sha256OfJson({
31552
+ peers,
31553
+ externals: SHELL_EXTERNALS,
31554
+ named: namedBySpec,
31555
+ mode: "globalThis",
31556
+ rev: 5
31557
+ });
31558
+ if (!noCache && isCacheHit(cache, unitId, inputHash)) {
31559
+ const cached = cache.units[unitId]?.outputHashes;
31560
+ if (cached?._manifest) {
31561
+ try {
31562
+ const m = JSON.parse(cached._manifest);
31563
+ const files = Object.values(m.imports).map((p) => basename2(p));
31564
+ if (files.every((f) => existsSync7(join8(outDir, f)))) {
31565
+ console.log(` \u2713 cached: ${unitId}`);
31566
+ return { ...m, cached: true };
31567
+ }
31568
+ } catch {}
31569
+ }
31570
+ }
31571
+ console.log(` building: ${unitId} (${SHELL_EXTERNALS.length} peers, globalThis)`);
31572
+ const imports = {};
31573
+ for (const specifier of SHELL_EXTERNALS) {
31574
+ const short = shortNameOf(specifier).replace(/\//g, "-");
31575
+ const named = namedBySpec[specifier] ?? [];
31576
+ const src = `const M = globalThis.${RUNTIME_GLOBAL}[${JSON.stringify(specifier)}];
31577
+ const N = globalThis.${RUNTIME_GLOBAL}[${JSON.stringify(specifier + "#ns")}] ?? M;
31578
+ export default M;
31579
+ ` + named.map((n) => `export const ${n} = N[${JSON.stringify(n)}];`).join(`
31580
+ `) + `
31581
+ `;
31582
+ const hash = sha256Hex(src).slice(0, 16);
31583
+ const finalName = `${short}.${hash}.js`;
31584
+ writeFileSync6(join8(outDir, finalName), src);
31585
+ imports[specifier] = `/shell/${finalName}`;
31586
+ }
31587
+ const manifest = { imports, peers, cached: false };
31588
+ updateCache(cache, unitId, inputHash, {
31589
+ outputHashes: { _manifest: JSON.stringify(manifest) }
31590
+ });
31591
+ return manifest;
31592
+ }
31593
+
31477
31594
  // src/builder/parallel.ts
31478
31595
  import { cpus } from "os";
31479
31596
  var DEFAULT_CONCURRENCY = Math.max(1, cpus().length - 1);
@@ -31527,10 +31644,10 @@ function isBuiltinSpec(spec) {
31527
31644
  function versionFromImporter(importer, name) {
31528
31645
  let dir = importer ? dirname6(importer) : "";
31529
31646
  while (dir && dir !== dirname6(dir)) {
31530
- const pj = join8(dir, "package.json");
31531
- if (existsSync7(pj)) {
31647
+ const pj = join9(dir, "package.json");
31648
+ if (existsSync8(pj)) {
31532
31649
  try {
31533
- const json = JSON.parse(readFileSync7(pj, "utf-8"));
31650
+ const json = JSON.parse(readFileSync8(pj, "utf-8"));
31534
31651
  const spec = json.dependencies?.[name] ?? json.peerDependencies?.[name] ?? json.devDependencies?.[name];
31535
31652
  if (typeof spec === "string")
31536
31653
  return spec;
@@ -31584,11 +31701,7 @@ function jsxDevShimPlugin() {
31584
31701
  namespace: "jsx-dev-shim"
31585
31702
  }));
31586
31703
  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
- `,
31704
+ contents: `export { jsx as jsxDEV, jsxs as jsxsDEV, Fragment } from "react/jsx-runtime";`,
31592
31705
  loader: "ts"
31593
31706
  }));
31594
31707
  }
@@ -31601,13 +31714,13 @@ var SERVER_DEFINES = { ONLY_SERVER: "true", ONLY_BROWSER: "false", ONLY_CLIENT:
31601
31714
  var SERVER_ENTRY_FILE = "_server.js";
31602
31715
  var SERVER_EXTERNALS_FILE = "_externals.json";
31603
31716
  function discoverPackages(rootDir) {
31604
- const rootPkg = JSON.parse(readFileSync7(join8(rootDir, "package.json"), "utf-8"));
31717
+ const rootPkg = JSON.parse(readFileSync8(join9(rootDir, "package.json"), "utf-8"));
31605
31718
  const workspaceGlobs = rootPkg.workspaces ?? [];
31606
31719
  const results = [];
31607
31720
  for (const glob2 of workspaceGlobs) {
31608
31721
  const base2 = glob2.replace("/*", "");
31609
- const baseDir = join8(rootDir, base2);
31610
- if (!existsSync7(baseDir))
31722
+ const baseDir = join9(rootDir, base2);
31723
+ if (!existsSync8(baseDir))
31611
31724
  continue;
31612
31725
  let entries;
31613
31726
  try {
@@ -31616,25 +31729,25 @@ function discoverPackages(rootDir) {
31616
31729
  continue;
31617
31730
  }
31618
31731
  for (const entry of entries) {
31619
- const pkgPath = join8(baseDir, entry, "package.json");
31620
- if (!existsSync7(pkgPath))
31732
+ const pkgPath = join9(baseDir, entry, "package.json");
31733
+ if (!existsSync8(pkgPath))
31621
31734
  continue;
31622
- const pkg = JSON.parse(readFileSync7(pkgPath, "utf-8"));
31735
+ const pkg = JSON.parse(readFileSync8(pkgPath, "utf-8"));
31623
31736
  if (pkg.name?.startsWith("@arcote.tech/"))
31624
31737
  continue;
31625
- const pkgDir = join8(baseDir, entry);
31738
+ const pkgDir = join9(baseDir, entry);
31626
31739
  const candidates = [
31627
- join8(pkgDir, "src", "index.ts"),
31628
- join8(pkgDir, "src", "index.tsx"),
31629
- join8(pkgDir, "index.ts"),
31630
- join8(pkgDir, "index.tsx")
31740
+ join9(pkgDir, "src", "index.ts"),
31741
+ join9(pkgDir, "src", "index.tsx"),
31742
+ join9(pkgDir, "index.ts"),
31743
+ join9(pkgDir, "index.tsx")
31631
31744
  ];
31632
- const entrypoint = candidates.find((c) => existsSync7(c)) ?? null;
31745
+ const entrypoint = candidates.find((c) => existsSync8(c)) ?? null;
31633
31746
  if (!entrypoint)
31634
31747
  continue;
31635
31748
  results.push({
31636
31749
  name: pkg.name,
31637
- path: join8(baseDir, entry),
31750
+ path: join9(baseDir, entry),
31638
31751
  entrypoint,
31639
31752
  packageJson: pkg
31640
31753
  });
@@ -31663,7 +31776,7 @@ var sourceFilter = (rel) => {
31663
31776
  return true;
31664
31777
  };
31665
31778
  function pkgSourceHash(pkg) {
31666
- return sha256OfDir(join8(pkg.path, "src"), sourceFilter);
31779
+ return sha256OfDir(join9(pkg.path, "src"), sourceFilter);
31667
31780
  }
31668
31781
  function depVersionsHash(rootDir, pkg) {
31669
31782
  const peerDeps = Object.keys(pkg.packageJson.peerDependencies ?? {});
@@ -31676,7 +31789,7 @@ function depVersionsHash(rootDir, pkg) {
31676
31789
  }
31677
31790
  async function buildContextClient(pkg, rootDir, client, cache, noCache) {
31678
31791
  const unitId = `context-pkg:${pkg.name}:${client.name}`;
31679
- const outDir = join8(pkg.path, "dist", client.name);
31792
+ const outDir = join9(pkg.path, "dist", client.name);
31680
31793
  const inputHash = sha256OfJson({
31681
31794
  src: pkgSourceHash(pkg),
31682
31795
  pkg: pkg.packageJson,
@@ -31685,7 +31798,7 @@ async function buildContextClient(pkg, rootDir, client, cache, noCache) {
31685
31798
  target: client.target,
31686
31799
  defines: client.defines
31687
31800
  });
31688
- if (!noCache && isCacheHit(cache, unitId, inputHash, [join8(outDir, "main", "index.js")])) {
31801
+ if (!noCache && isCacheHit(cache, unitId, inputHash, [join9(outDir, "main", "index.js")])) {
31689
31802
  console.log(` \u2713 cached: ${pkg.name} (${client.name})`);
31690
31803
  return { pkgName: pkg.name, client: client.name, declarationErrors: [], cached: true };
31691
31804
  }
@@ -31695,7 +31808,7 @@ async function buildContextClient(pkg, rootDir, client, cache, noCache) {
31695
31808
  const externals = [...peerDeps, ...Object.keys(allDeps)];
31696
31809
  const result = await Bun.build({
31697
31810
  entrypoints: [pkg.entrypoint],
31698
- outdir: join8(outDir, "main"),
31811
+ outdir: join9(outDir, "main"),
31699
31812
  target: client.target,
31700
31813
  format: "esm",
31701
31814
  naming: "index.[ext]",
@@ -31765,7 +31878,7 @@ async function buildContextPackages(rootDir, packages, cache, noCache) {
31765
31878
  }
31766
31879
  async function buildServerApp(rootDir, serverDir, packages, cache, noCache) {
31767
31880
  const contexts = packages.filter((p) => isContextPackage(p.packageJson));
31768
- mkdirSync6(serverDir, { recursive: true });
31881
+ mkdirSync7(serverDir, { recursive: true });
31769
31882
  const srcByName = new Map(packages.map((p) => [p.name, p.entrypoint]));
31770
31883
  const externalSet = new Set(FRAMEWORK_PEERS);
31771
31884
  for (const p of packages) {
@@ -31786,8 +31899,8 @@ async function buildServerApp(rootDir, serverDir, packages, cache, noCache) {
31786
31899
  defines: SERVER_DEFINES,
31787
31900
  sourcemap: "linked"
31788
31901
  });
31789
- const entryFileAbs = join8(serverDir, SERVER_ENTRY_FILE);
31790
- const externalsFileAbs = join8(serverDir, SERVER_EXTERNALS_FILE);
31902
+ const entryFileAbs = join9(serverDir, SERVER_ENTRY_FILE);
31903
+ const externalsFileAbs = join9(serverDir, SERVER_EXTERNALS_FILE);
31791
31904
  if (!noCache && isCacheHit(cache, unitId, inputHash, [entryFileAbs])) {
31792
31905
  console.log(` \u2713 cached: ${unitId}`);
31793
31906
  return {
@@ -31799,13 +31912,13 @@ async function buildServerApp(rootDir, serverDir, packages, cache, noCache) {
31799
31912
  console.log(` building: ${unitId} (${contexts.length} server modules)`);
31800
31913
  for (const f of readdirSync4(serverDir)) {
31801
31914
  if (f.endsWith(".js") || f.endsWith(".js.map")) {
31802
- rmSync2(join8(serverDir, f), { force: true });
31915
+ rmSync3(join9(serverDir, f), { force: true });
31803
31916
  }
31804
31917
  }
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(`
31918
+ const tmpDir = join9(serverDir, "_entries");
31919
+ mkdirSync7(tmpDir, { recursive: true });
31920
+ const entrySrc = join9(tmpDir, SERVER_ENTRY_FILE.replace(/\.js$/, ".ts"));
31921
+ writeFileSync7(entrySrc, contexts.map((p) => `import "${p.name}";`).join(`
31809
31922
  `) + `
31810
31923
  `);
31811
31924
  const recorded = new Map;
@@ -31828,7 +31941,7 @@ async function buildServerApp(rootDir, serverDir, packages, cache, noCache) {
31828
31941
  define: SERVER_DEFINES
31829
31942
  });
31830
31943
  } finally {
31831
- rmSync2(tmpDir, { recursive: true, force: true });
31944
+ rmSync3(tmpDir, { recursive: true, force: true });
31832
31945
  }
31833
31946
  if (!result.success) {
31834
31947
  for (const log2 of result.logs)
@@ -31839,31 +31952,33 @@ async function buildServerApp(rootDir, serverDir, packages, cache, noCache) {
31839
31952
  if (!entryOut) {
31840
31953
  throw new Error("Server app build: entry not found in outputs");
31841
31954
  }
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})`);
31955
+ if (basename3(entryOut.path) !== SERVER_ENTRY_FILE) {
31956
+ throw new Error(`Server app build: unexpected entry name ${basename3(entryOut.path)} (wanted ${SERVER_ENTRY_FILE})`);
31844
31957
  }
31845
31958
  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) + `
31959
+ writeFileSync7(externalsFileAbs, JSON.stringify(externals, null, 2) + `
31847
31960
  `);
31848
31961
  const outputHash = sha256OfDir(serverDir);
31849
31962
  updateCache(cache, unitId, inputHash, { outputHash });
31850
31963
  return { entryFile: SERVER_ENTRY_FILE, cached: false, externals };
31851
31964
  }
31852
31965
  function readServerExternals(path4) {
31853
- if (!existsSync7(path4))
31966
+ if (!existsSync8(path4))
31854
31967
  return [];
31855
31968
  try {
31856
- const parsed = JSON.parse(readFileSync7(path4, "utf-8"));
31969
+ const parsed = JSON.parse(readFileSync8(path4, "utf-8"));
31857
31970
  return Array.isArray(parsed) ? parsed : [];
31858
31971
  } catch {
31859
31972
  return [];
31860
31973
  }
31861
31974
  }
31862
- async function buildBrowserApp(rootDir, outDir, plan, cache, noCache, i18nCollector) {
31863
- mkdirSync6(outDir, { recursive: true });
31975
+ async function buildBrowserApp(rootDir, outDir, plan, cache, noCache, i18nCollector, opts) {
31976
+ const federated = opts?.federated ?? false;
31977
+ const exposeRuntime = opts?.exposeRuntime ?? false;
31978
+ mkdirSync7(outDir, { recursive: true });
31864
31979
  const publicMembers = plan.groups.get("public") ?? [];
31865
31980
  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";
31981
+ const unitId = opts?.unitId ?? "browser-app";
31867
31982
  const allMembers = [];
31868
31983
  for (const m of publicMembers) {
31869
31984
  allMembers.push({ name: m.pkg.name, group: "public", srcHash: pkgSourceHash(m.pkg) });
@@ -31879,7 +31994,11 @@ async function buildBrowserApp(rootDir, outDir, plan, cache, noCache, i18nCollec
31879
31994
  "initial",
31880
31995
  ...protectedGroups.map((g) => g.name).sort()
31881
31996
  ],
31882
- define: { ONLY_SERVER: "false", ONLY_BROWSER: "true", ONLY_CLIENT: "true" }
31997
+ define: { ONLY_SERVER: "false", ONLY_BROWSER: "true", ONLY_CLIENT: "true" },
31998
+ federated,
31999
+ exposeRuntime,
32000
+ externals: federated ? [...SHELL_EXTERNALS] : [],
32001
+ builderRev: 3
31883
32002
  });
31884
32003
  if (!noCache && isCacheHit(cache, unitId, inputHash)) {
31885
32004
  const cached = cache.units[unitId]?.outputHashes;
@@ -31891,7 +32010,7 @@ async function buildBrowserApp(rootDir, outDir, plan, cache, noCache, i18nCollec
31891
32010
  ...Object.values(m.groups).map((g) => g.file),
31892
32011
  ...m.sharedChunks
31893
32012
  ];
31894
- if (allFiles.every((f) => existsSync7(join8(outDir, f)))) {
32013
+ if (allFiles.every((f) => existsSync8(join9(outDir, f)))) {
31895
32014
  console.log(` \u2713 cached: ${unitId}`);
31896
32015
  return { ...m, cached: true };
31897
32016
  }
@@ -31899,25 +32018,26 @@ async function buildBrowserApp(rootDir, outDir, plan, cache, noCache, i18nCollec
31899
32018
  }
31900
32019
  }
31901
32020
  console.log(` building: ${unitId} (initial: ${publicMembers.length} modules, groups: ${protectedGroups.map((g) => `${g.name}=${g.members.length}`).join(",") || "none"})`);
31902
- if (existsSync7(outDir)) {
32021
+ if (existsSync8(outDir)) {
31903
32022
  for (const f of readdirSync4(outDir)) {
31904
32023
  if (f.endsWith(".js"))
31905
- rmSync2(join8(outDir, f), { force: true });
32024
+ rmSync3(join9(outDir, f), { force: true });
31906
32025
  }
31907
32026
  }
31908
- const tmpDir = join8(outDir, "_entries");
31909
- mkdirSync6(tmpDir, { recursive: true });
32027
+ const tmpDir = join9(outDir, "_entries");
32028
+ mkdirSync7(tmpDir, { recursive: true });
31910
32029
  const importLines = (pkgs) => pkgs.map((m) => `import "${m.pkg.name}";`).join(`
31911
32030
  `);
31912
- const initialEntry = join8(tmpDir, "initial.ts");
31913
- writeFileSync6(initialEntry, `${importLines(publicMembers)}
32031
+ const initialEntry = join9(tmpDir, "initial.ts");
32032
+ writeFileSync7(initialEntry, (exposeRuntime ? runtimeExposePrelude() + `
32033
+ ` : "") + `${importLines(publicMembers)}
31914
32034
  export { startApp } from "@arcote.tech/platform";
31915
32035
  `);
31916
32036
  const entryPaths = [initialEntry];
31917
32037
  const groupModuleMap = new Map;
31918
32038
  for (const g of protectedGroups) {
31919
- const entry = join8(tmpDir, `${g.name}.ts`);
31920
- writeFileSync6(entry, `${importLines(g.members)}
32039
+ const entry = join9(tmpDir, `${g.name}.ts`);
32040
+ writeFileSync7(entry, `${importLines(g.members)}
31921
32041
  `);
31922
32042
  entryPaths.push(entry);
31923
32043
  groupModuleMap.set(g.name, g.members.map((m) => m.moduleName));
@@ -31930,15 +32050,15 @@ export { startApp } from "@arcote.tech/platform";
31930
32050
  allMemberPkgs.set(m.pkg.name, m.pkg);
31931
32051
  const patchedPkgJsons = [];
31932
32052
  for (const pkg of allMemberPkgs.values()) {
31933
- const pkgJsonPath = join8(pkg.path, "package.json");
31934
- if (!existsSync7(pkgJsonPath))
32053
+ const pkgJsonPath = join9(pkg.path, "package.json");
32054
+ if (!existsSync8(pkgJsonPath))
31935
32055
  continue;
31936
- const original = readFileSync7(pkgJsonPath, "utf-8");
32056
+ const original = readFileSync8(pkgJsonPath, "utf-8");
31937
32057
  const parsed = JSON.parse(original);
31938
32058
  if (parsed.sideEffects === true)
31939
32059
  continue;
31940
32060
  parsed.sideEffects = true;
31941
- writeFileSync6(pkgJsonPath, JSON.stringify(parsed, null, 2) + `
32061
+ writeFileSync7(pkgJsonPath, JSON.stringify(parsed, null, 2) + `
31942
32062
  `);
31943
32063
  patchedPkgJsons.push({ path: pkgJsonPath, original });
31944
32064
  }
@@ -31950,9 +32070,9 @@ export { startApp } from "@arcote.tech/platform";
31950
32070
  splitting: true,
31951
32071
  format: "esm",
31952
32072
  target: "browser",
31953
- external: [],
32073
+ external: federated ? [...SHELL_EXTERNALS] : [],
31954
32074
  plugins: [
31955
- singleReactPlugin(rootDir),
32075
+ ...federated ? [] : [singleReactPlugin(rootDir)],
31956
32076
  jsxDevShimPlugin(),
31957
32077
  i18nExtractPlugin(i18nCollector, rootDir)
31958
32078
  ],
@@ -31966,9 +32086,9 @@ export { startApp } from "@arcote.tech/platform";
31966
32086
  });
31967
32087
  } finally {
31968
32088
  for (const p of patchedPkgJsons)
31969
- writeFileSync6(p.path, p.original);
32089
+ writeFileSync7(p.path, p.original);
31970
32090
  }
31971
- rmSync2(tmpDir, { recursive: true, force: true });
32091
+ rmSync3(tmpDir, { recursive: true, force: true });
31972
32092
  if (!result.success) {
31973
32093
  for (const log2 of result.logs)
31974
32094
  console.error(log2);
@@ -31979,16 +32099,16 @@ export { startApp } from "@arcote.tech/platform";
31979
32099
  const groups = {};
31980
32100
  const sharedChunks = [];
31981
32101
  for (const out of result.outputs) {
31982
- const name = basename2(out.path);
32102
+ const name = basename3(out.path);
31983
32103
  if (out.kind === "entry-point") {
31984
- const bytes = readFileSync7(out.path);
32104
+ const bytes = readFileSync8(out.path);
31985
32105
  const hash = sha256Hex(bytes).slice(0, 16);
31986
32106
  const stem = name.replace(/\.js$/, "");
31987
32107
  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 });
32108
+ const finalPath = join9(outDir, finalName);
32109
+ rmSync3(finalPath, { force: true });
32110
+ writeFileSync7(finalPath, bytes);
32111
+ rmSync3(out.path, { force: true });
31992
32112
  if (stem === "initial") {
31993
32113
  initialFile = finalName;
31994
32114
  initialHash = hash;
@@ -32018,21 +32138,21 @@ export { startApp } from "@arcote.tech/platform";
32018
32138
  return manifest;
32019
32139
  }
32020
32140
  async function buildTranslations(rootDir, arcDir, cache, noCache) {
32021
- const localesDir = join8(rootDir, "locales");
32022
- if (!existsSync7(localesDir))
32141
+ const localesDir = join9(rootDir, "locales");
32142
+ if (!existsSync8(localesDir))
32023
32143
  return;
32024
32144
  const unitId = "translations";
32025
- const poFiles = readdirSync4(localesDir).filter((f) => f.endsWith(".po")).map((f) => join8(localesDir, f));
32145
+ const poFiles = readdirSync4(localesDir).filter((f) => f.endsWith(".po")).map((f) => join9(localesDir, f));
32026
32146
  if (poFiles.length === 0)
32027
32147
  return;
32028
32148
  const inputHash = sha256OfFiles(poFiles);
32029
- if (!noCache && isCacheHit(cache, unitId, inputHash, [join8(arcDir, "locales")])) {
32149
+ if (!noCache && isCacheHit(cache, unitId, inputHash, [join9(arcDir, "locales")])) {
32030
32150
  console.log(` \u2713 cached: translations`);
32031
32151
  return;
32032
32152
  }
32033
32153
  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));
32154
+ compileAllCatalogs(localesDir, join9(arcDir, "locales"));
32155
+ const jsonFiles = readdirSync4(join9(arcDir, "locales")).filter((f) => f.endsWith(".json")).map((f) => join9(arcDir, "locales", f));
32036
32156
  const outputHash = sha256OfFiles(jsonFiles);
32037
32157
  updateCache(cache, unitId, inputHash, { outputHash });
32038
32158
  }
@@ -32099,10 +32219,10 @@ var TAILWIND_INPUT_TEMPLATE = (rootRel) => `@import "tailwindcss";
32099
32219
  }
32100
32220
  `;
32101
32221
  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");
32222
+ mkdirSync7(arcDir, { recursive: true });
32223
+ const inputCss = join9(arcDir, "_input.css");
32224
+ const outputCss = join9(arcDir, "styles.css");
32225
+ const themeOutput = join9(arcDir, "theme.css");
32106
32226
  const rootRel = relative3(arcDir, rootDir).replace(/\\/g, "/");
32107
32227
  const inputCssContent = TAILWIND_INPUT_TEMPLATE(rootRel);
32108
32228
  const tsxFilter = (rel) => {
@@ -32114,15 +32234,15 @@ async function buildStyles(rootDir, arcDir, packages, themePath, cache, noCache)
32114
32234
  };
32115
32235
  const wsHashes = {};
32116
32236
  for (const p of packages) {
32117
- wsHashes[p.name] = sha256OfDir(join8(p.path, "src"), tsxFilter);
32237
+ wsHashes[p.name] = sha256OfDir(join9(p.path, "src"), tsxFilter);
32118
32238
  }
32119
- const platformSrc = join8(rootDir, "node_modules", "@arcote.tech", "platform", "src");
32120
- const arcDsSrc = join8(rootDir, "node_modules", "@arcote.tech", "arc-ds", "src");
32239
+ const platformSrc = join9(rootDir, "node_modules", "@arcote.tech", "platform", "src");
32240
+ const arcDsSrc = join9(rootDir, "node_modules", "@arcote.tech", "arc-ds", "src");
32121
32241
  const frameworkHashes = {
32122
32242
  platform: sha256OfDir(platformSrc, tsxFilter),
32123
32243
  arcDs: sha256OfDir(arcDsSrc, tsxFilter)
32124
32244
  };
32125
- const themeContent = themePath && existsSync7(join8(rootDir, themePath)) ? readFileSync7(join8(rootDir, themePath)) : null;
32245
+ const themeContent = themePath && existsSync8(join9(rootDir, themePath)) ? readFileSync8(join9(rootDir, themePath)) : null;
32126
32246
  const themeHash = themeContent ? sha256Hex(themeContent) : null;
32127
32247
  const unitId = "styles";
32128
32248
  const inputHash = sha256OfJson({
@@ -32139,16 +32259,16 @@ async function buildStyles(rootDir, arcDir, packages, themePath, cache, noCache)
32139
32259
  return;
32140
32260
  }
32141
32261
  console.log(` building: styles`);
32142
- writeFileSync6(inputCss, inputCssContent);
32262
+ writeFileSync7(inputCss, inputCssContent);
32143
32263
  execSync(`bunx @tailwindcss/cli -i ${inputCss} -o ${outputCss} --minify`, {
32144
32264
  cwd: rootDir,
32145
32265
  stdio: "inherit"
32146
32266
  });
32147
32267
  if (themePath && themeContent) {
32148
- writeFileSync6(themeOutput, themeContent);
32268
+ writeFileSync7(themeOutput, themeContent);
32149
32269
  }
32150
32270
  const outFiles = [outputCss];
32151
- if (themePath && existsSync7(themeOutput))
32271
+ if (themePath && existsSync8(themeOutput))
32152
32272
  outFiles.push(themeOutput);
32153
32273
  const outputHash = sha256OfFiles(outFiles);
32154
32274
  updateCache(cache, unitId, inputHash, { outputHash });
@@ -32157,20 +32277,20 @@ async function buildStyles(rootDir, arcDir, packages, themePath, cache, noCache)
32157
32277
  // src/builder/access-extractor.ts
32158
32278
  var {spawn: spawn2 } = globalThis.Bun;
32159
32279
  import {
32160
- existsSync as existsSync8,
32161
- mkdirSync as mkdirSync7,
32162
- readFileSync as readFileSync8,
32280
+ existsSync as existsSync9,
32281
+ mkdirSync as mkdirSync8,
32282
+ readFileSync as readFileSync9,
32163
32283
  unlinkSync as unlinkSync2,
32164
- writeFileSync as writeFileSync7
32284
+ writeFileSync as writeFileSync8
32165
32285
  } from "fs";
32166
- import { join as join9 } from "path";
32286
+ import { join as join10 } from "path";
32167
32287
  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);
32288
+ const serverBundles = existsSync9(serverBundlePath) ? [{ name: "server", path: serverBundlePath }] : [];
32289
+ const workerDir = join10(rootDir, ".arc", ".tmp");
32290
+ mkdirSync8(workerDir, { recursive: true });
32291
+ const workerPath = join10(workerDir, `access-extractor-${Date.now()}.mjs`);
32292
+ const outPath = join10(workerDir, `access-${Date.now()}.json`);
32293
+ writeFileSync8(workerPath, WORKER_SOURCE);
32174
32294
  try {
32175
32295
  const proc2 = spawn2({
32176
32296
  cmd: ["bun", "run", workerPath],
@@ -32187,7 +32307,7 @@ async function extractAccessMap(rootDir, serverBundlePath) {
32187
32307
  if (exit !== 0) {
32188
32308
  throw new Error(`access-extractor subprocess exited with ${exit}`);
32189
32309
  }
32190
- return JSON.parse(readFileSync8(outPath, "utf-8"));
32310
+ return JSON.parse(readFileSync9(outPath, "utf-8"));
32191
32311
  } finally {
32192
32312
  try {
32193
32313
  unlinkSync2(workerPath);
@@ -32224,23 +32344,35 @@ for (const { name, path } of bundles) {
32224
32344
  }
32225
32345
  }
32226
32346
 
32227
- const result = {};
32228
- for (const [name, access] of platform.getAllModuleAccess()) {
32229
- result[name] = {
32230
- rules: (access.rules ?? []).map((r) => ({
32347
+ const access = {};
32348
+ for (const [name, a] of platform.getAllModuleAccess()) {
32349
+ access[name] = {
32350
+ rules: (a.rules ?? []).map((r) => ({
32231
32351
  token: { name: r.token?.name ?? "" },
32232
32352
  hasCheck: typeof r.check === "function",
32233
32353
  })),
32234
32354
  };
32235
32355
  }
32236
32356
 
32357
+ const exposure = {};
32358
+ const getExposure = platform.getAllModuleExposure;
32359
+ if (typeof getExposure === "function") {
32360
+ for (const [name, e] of getExposure()) {
32361
+ exposure[name] = {
32362
+ exposure: e.exposure,
32363
+ permissions: e.permissions ?? [],
32364
+ slots: e.slots ?? [],
32365
+ };
32366
+ }
32367
+ }
32368
+
32237
32369
  const { writeFileSync } = await import("node:fs");
32238
- writeFileSync(out, JSON.stringify(result, null, 2) + "\\n");
32370
+ writeFileSync(out, JSON.stringify({ access, exposure }, null, 2) + "\\n");
32239
32371
  `.trim();
32240
32372
 
32241
32373
  // 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";
32374
+ import { readFileSync as readFileSync10, readdirSync as readdirSync5, statSync as statSync2 } from "fs";
32375
+ import { basename as basename4, join as join11 } from "path";
32244
32376
  var PUBLIC_CHUNK = "public";
32245
32377
  function planChunks(packages, accessMap) {
32246
32378
  const assignments = new Map;
@@ -32253,7 +32385,7 @@ function planChunks(packages, accessMap) {
32253
32385
  pkg,
32254
32386
  chunk,
32255
32387
  moduleName,
32256
- safeName: basename3(pkg.path)
32388
+ safeName: basename4(pkg.path)
32257
32389
  };
32258
32390
  assignments.set(moduleName, entry);
32259
32391
  const bucket = groups.get(chunk);
@@ -32283,7 +32415,7 @@ function collectModuleNames(dir, acc) {
32283
32415
  for (const e of entries) {
32284
32416
  if (e === "node_modules" || e === "dist")
32285
32417
  continue;
32286
- const full = join10(dir, e);
32418
+ const full = join11(dir, e);
32287
32419
  let isDir = false;
32288
32420
  try {
32289
32421
  isDir = statSync2(full).isDirectory();
@@ -32293,7 +32425,7 @@ function collectModuleNames(dir, acc) {
32293
32425
  if (isDir) {
32294
32426
  collectModuleNames(full, acc);
32295
32427
  } else if (e.endsWith(".ts") || e.endsWith(".tsx")) {
32296
- const src = readFileSync9(full, "utf-8");
32428
+ const src = readFileSync10(full, "utf-8");
32297
32429
  MODULE_CALL.lastIndex = 0;
32298
32430
  let m;
32299
32431
  while (m = MODULE_CALL.exec(src))
@@ -32305,7 +32437,7 @@ function assertOneModulePerPackage(packages) {
32305
32437
  const offenders = [];
32306
32438
  for (const pkg of packages) {
32307
32439
  const names = new Set;
32308
- collectModuleNames(join10(pkg.path, "src"), names);
32440
+ collectModuleNames(join11(pkg.path, "src"), names);
32309
32441
  if (names.size > 1) {
32310
32442
  offenders.push({ pkg: pkg.name, modules: [...names].sort() });
32311
32443
  }
@@ -32337,11 +32469,11 @@ function resolveChunk(moduleName, access) {
32337
32469
 
32338
32470
  // src/builder/dependency-collector.ts
32339
32471
  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";
32472
+ import { existsSync as existsSync10, mkdirSync as mkdirSync9, readFileSync as readFileSync11, writeFileSync as writeFileSync9 } from "fs";
32473
+ import { basename as basename5, dirname as dirname7, join as join12 } from "path";
32342
32474
  import { fileURLToPath as fileURLToPath6 } from "url";
32343
32475
  function collectFrameworkDeps(arcDir, rootDir, packages, sharedDeps = [], serverExternals = []) {
32344
- mkdirSync8(arcDir, { recursive: true });
32476
+ mkdirSync9(arcDir, { recursive: true });
32345
32477
  const versions = resolveFrameworkVersions(rootDir, packages);
32346
32478
  mergeServerExternals(versions, rootDir, packages, serverExternals);
32347
32479
  for (const { name, version } of sharedDeps) {
@@ -32350,7 +32482,7 @@ function collectFrameworkDeps(arcDir, rootDir, packages, sharedDeps = [], server
32350
32482
  try {
32351
32483
  const cliDir = dirname7(fileURLToPath6(import.meta.url));
32352
32484
  const arcOtelPkgPath = Bun.resolveSync("@arcote.tech/arc-otel/package.json", cliDir);
32353
- const arcOtelPkg = JSON.parse(readFileSync10(arcOtelPkgPath, "utf-8"));
32485
+ const arcOtelPkg = JSON.parse(readFileSync11(arcOtelPkgPath, "utf-8"));
32354
32486
  for (const [name, spec] of Object.entries(arcOtelPkg.dependencies ?? {})) {
32355
32487
  if (name.startsWith("@opentelemetry/")) {
32356
32488
  versions[name] = spec;
@@ -32361,10 +32493,10 @@ function collectFrameworkDeps(arcDir, rootDir, packages, sharedDeps = [], server
32361
32493
  console.warn(`[arc-otel] could not resolve @arcote.tech/arc-otel \u2014 image will run without telemetry deps: ${e.message}`);
32362
32494
  }
32363
32495
  let rootArc;
32364
- const rootPkgPath = join11(rootDir, "package.json");
32365
- if (existsSync9(rootPkgPath)) {
32496
+ const rootPkgPath = join12(rootDir, "package.json");
32497
+ if (existsSync10(rootPkgPath)) {
32366
32498
  try {
32367
- const rootPkg = JSON.parse(readFileSync10(rootPkgPath, "utf-8"));
32499
+ const rootPkg = JSON.parse(readFileSync11(rootPkgPath, "utf-8"));
32368
32500
  if (rootPkg.arc && typeof rootPkg.arc === "object")
32369
32501
  rootArc = rootPkg.arc;
32370
32502
  } catch {}
@@ -32376,11 +32508,11 @@ function collectFrameworkDeps(arcDir, rootDir, packages, sharedDeps = [], server
32376
32508
  dependencies: versions,
32377
32509
  ...rootArc ? { arc: rootArc } : {}
32378
32510
  };
32379
- const manifestPath = join11(arcDir, "package.json");
32380
- writeFileSync8(manifestPath, JSON.stringify(manifest, null, 2) + `
32511
+ const manifestPath = join12(arcDir, "package.json");
32512
+ writeFileSync9(manifestPath, JSON.stringify(manifest, null, 2) + `
32381
32513
  `);
32382
- const hash = sha256Hex2(readFileSync10(manifestPath));
32383
- writeFileSync8(join11(arcDir, ".deps-hash"), hash + `
32514
+ const hash = sha256Hex2(readFileSync11(manifestPath));
32515
+ writeFileSync9(join12(arcDir, ".deps-hash"), hash + `
32384
32516
  `);
32385
32517
  return { hash, manifestPath };
32386
32518
  }
@@ -32388,7 +32520,7 @@ function sha256Hex2(bytes) {
32388
32520
  return createHash("sha256").update(bytes).digest("hex");
32389
32521
  }
32390
32522
  function resolveFrameworkVersions(rootDir, packages) {
32391
- const rootPkg = JSON.parse(readFileSync10(join11(rootDir, "package.json"), "utf-8"));
32523
+ const rootPkg = JSON.parse(readFileSync11(join12(rootDir, "package.json"), "utf-8"));
32392
32524
  const rootDeps = rootPkg.dependencies ?? {};
32393
32525
  const rootDevDeps = rootPkg.devDependencies ?? {};
32394
32526
  const required = new Set(FRAMEWORK_PEERS);
@@ -32419,7 +32551,7 @@ function findWorkspaceSpec(packages, name) {
32419
32551
  return;
32420
32552
  }
32421
32553
  function mergeServerExternals(versions, rootDir, packages, externals) {
32422
- const rootPkg = JSON.parse(readFileSync10(join11(rootDir, "package.json"), "utf-8"));
32554
+ const rootPkg = JSON.parse(readFileSync11(join12(rootDir, "package.json"), "utf-8"));
32423
32555
  const rootDeps = rootPkg.dependencies ?? {};
32424
32556
  const rootDevDeps = rootPkg.devDependencies ?? {};
32425
32557
  for (const { name, version } of externals) {
@@ -32447,9 +32579,9 @@ function resolveWorkspace() {
32447
32579
  process.exit(1);
32448
32580
  }
32449
32581
  const rootDir = dirname8(packageJsonPath);
32450
- const rootPkg = JSON.parse(readFileSync11(packageJsonPath, "utf-8"));
32582
+ const rootPkg = JSON.parse(readFileSync12(packageJsonPath, "utf-8"));
32451
32583
  const appName = rootPkg.name ?? "Arc App";
32452
- const arcDir = join12(rootDir, ".arc", "platform");
32584
+ const arcDir = join13(rootDir, ".arc", "platform");
32453
32585
  log2("Scanning workspaces...");
32454
32586
  const packages = discoverPackages(rootDir);
32455
32587
  if (packages.length > 0) {
@@ -32459,10 +32591,10 @@ function resolveWorkspace() {
32459
32591
  }
32460
32592
  let manifest;
32461
32593
  for (const name of ["manifest.json", "manifest.webmanifest"]) {
32462
- const manifestPath = join12(rootDir, name);
32463
- if (existsSync10(manifestPath)) {
32594
+ const manifestPath = join13(rootDir, name);
32595
+ if (existsSync11(manifestPath)) {
32464
32596
  try {
32465
- const data = JSON.parse(readFileSync11(manifestPath, "utf-8"));
32597
+ const data = JSON.parse(readFileSync12(manifestPath, "utf-8"));
32466
32598
  const icons = data.icons;
32467
32599
  manifest = {
32468
32600
  path: manifestPath,
@@ -32481,9 +32613,9 @@ function resolveWorkspace() {
32481
32613
  rootPkg,
32482
32614
  appName,
32483
32615
  arcDir,
32484
- browserDir: join12(arcDir, "browser"),
32485
- assetsDir: join12(arcDir, "assets"),
32486
- publicDir: join12(rootDir, "public"),
32616
+ browserDir: join13(arcDir, "browser"),
32617
+ assetsDir: join13(arcDir, "assets"),
32618
+ publicDir: join13(rootDir, "public"),
32487
32619
  packages,
32488
32620
  manifest
32489
32621
  };
@@ -32492,73 +32624,120 @@ async function buildAll(ws, opts = {}) {
32492
32624
  const cache = loadBuildCache(ws.arcDir);
32493
32625
  const noCache = opts.noCache ?? false;
32494
32626
  const themePath = ws.rootPkg.arc?.theme;
32627
+ const federation = getFederationConfig(ws);
32495
32628
  log2(`Building (concurrency parallel${noCache ? ", no-cache" : ""})...`);
32496
32629
  assertOneModulePerPackage(ws.packages);
32497
32630
  await buildContextPackages(ws.rootDir, ws.packages, cache, noCache);
32498
- const serverDir = join12(ws.arcDir, "server");
32631
+ const serverDir = join13(ws.arcDir, "server");
32499
32632
  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) + `
32633
+ const accessMap = await extractAccessMap(ws.rootDir, join13(serverDir, serverEntry));
32634
+ mkdirSync10(ws.arcDir, { recursive: true });
32635
+ writeFileSync10(join13(ws.arcDir, "access.json"), JSON.stringify(accessMap.access, null, 2) + `
32503
32636
  `);
32504
- const plan = planChunks(ws.packages, accessMap);
32637
+ const isHost = Boolean(federation?.host);
32638
+ const moduleNameOf2 = (name) => name.split("/").pop() ?? name;
32639
+ const exposureOf = (pkgName) => accessMap.exposure[moduleNameOf2(pkgName)]?.exposure ?? "local";
32640
+ const localPackages = isHost ? ws.packages : ws.packages.filter((p) => exposureOf(p.name) !== "external");
32641
+ const fedPackages = ws.packages.filter((p) => ["external", "both"].includes(exposureOf(p.name)));
32642
+ const hasFederatedModules = fedPackages.length > 0;
32643
+ const needsPeerShell = isHost || hasFederatedModules;
32644
+ log2();
32645
+ const plan = planChunks(localPackages, accessMap.access);
32505
32646
  ok(`Chunks: ${plan.chunks.map((c) => `${c}(${plan.groups.get(c)?.length ?? 0})`).join(", ")}`);
32647
+ const fedPlan = hasFederatedModules ? planChunks(fedPackages, accessMap.access) : null;
32648
+ if (fedPlan) {
32649
+ ok(`Federated modules: ${fedPackages.map((p) => moduleNameOf2(p.name)).join(", ")}`);
32650
+ }
32506
32651
  const i18nCollector = new Map;
32507
- const [browserResult] = await Promise.all([
32508
- buildBrowserApp(ws.rootDir, ws.browserDir, plan, cache, noCache, i18nCollector),
32652
+ const [browserResult, , , , shellResult, fedResult] = await Promise.all([
32653
+ buildBrowserApp(ws.rootDir, ws.browserDir, plan, cache, noCache, i18nCollector, {
32654
+ federated: false,
32655
+ exposeRuntime: isHost
32656
+ }),
32509
32657
  buildStyles(ws.rootDir, ws.arcDir, ws.packages, themePath, cache, noCache),
32510
32658
  copyBrowserAssets(ws, cache, noCache),
32511
- buildTranslations(ws.rootDir, ws.arcDir, cache, noCache)
32659
+ buildTranslations(ws.rootDir, ws.arcDir, cache, noCache),
32660
+ needsPeerShell ? buildPeerShell(ws.rootDir, join13(ws.arcDir, "shell"), cache, noCache) : Promise.resolve(undefined),
32661
+ fedPlan ? buildBrowserApp(ws.rootDir, join13(ws.arcDir, "browser-fed"), fedPlan, cache, noCache, i18nCollector, { federated: true, unitId: "browser-app-fed" }) : Promise.resolve(undefined)
32512
32662
  ]);
32513
32663
  const { finalizeTranslations: finalizeTranslations3 } = await Promise.resolve().then(() => (init_i18n(), exports_i18n));
32514
32664
  await finalizeTranslations3(ws.rootDir, ws.arcDir, i18nCollector);
32515
32665
  collectFrameworkDeps(ws.arcDir, ws.rootDir, ws.packages, [], serverExternals);
32516
32666
  saveBuildCache(ws.arcDir, cache);
32517
- const finalManifest = assembleManifest(ws, browserResult, cache);
32518
- writeFileSync9(join12(ws.arcDir, "manifest.json"), JSON.stringify(finalManifest, null, 2));
32667
+ const federationBuild = fedResult && hasFederatedModules ? {
32668
+ config: {
32669
+ slug: federation?.slug,
32670
+ auth: federation?.auth
32671
+ },
32672
+ modules: fedPackages.map((p) => moduleNameOf2(p.name)),
32673
+ permissions: [
32674
+ ...new Set(fedPackages.flatMap((p) => accessMap.exposure[moduleNameOf2(p.name)]?.permissions ?? []))
32675
+ ],
32676
+ slots: [
32677
+ ...new Set(fedPackages.flatMap((p) => accessMap.exposure[moduleNameOf2(p.name)]?.slots ?? []))
32678
+ ],
32679
+ build: {
32680
+ initial: fedResult.initial,
32681
+ groups: fedResult.groups,
32682
+ sharedChunks: fedResult.sharedChunks
32683
+ }
32684
+ } : undefined;
32685
+ const finalManifest = assembleManifest(ws, browserResult, cache, shellResult, federationBuild);
32686
+ writeFileSync10(join13(ws.arcDir, "manifest.json"), JSON.stringify(finalManifest, null, 2));
32519
32687
  return finalManifest;
32520
32688
  }
32521
- function assembleManifest(ws, browser, cache) {
32689
+ function assembleManifest(ws, browser, cache, shell, federation) {
32522
32690
  const stylesHash = cache.units["styles"]?.outputHash ?? "";
32523
32691
  return {
32524
32692
  initial: browser.initial,
32525
32693
  groups: browser.groups,
32526
32694
  sharedChunks: browser.sharedChunks,
32527
32695
  stylesHash,
32528
- buildTime: new Date().toISOString()
32696
+ buildTime: new Date().toISOString(),
32697
+ ...shell ? { shell: { imports: shell.imports, peers: shell.peers } } : {},
32698
+ ...federation ? { federation } : {}
32529
32699
  };
32530
32700
  }
32701
+ function getFederationConfig(ws) {
32702
+ const raw = ws.rootPkg.arc?.federation;
32703
+ if (!raw)
32704
+ return;
32705
+ if (raw.slug && !/^[a-z0-9-]+$/.test(raw.slug)) {
32706
+ throw new Error(`arc.federation.slug "${raw.slug}" \u2014 dozwolone tylko [a-z0-9-]+`);
32707
+ }
32708
+ return raw;
32709
+ }
32531
32710
  function resolveAssetSource(from, pkgDir, rootDir) {
32532
32711
  if (from.startsWith("./") || from.startsWith("../")) {
32533
- const resolved = join12(pkgDir, from);
32534
- return existsSync10(resolved) ? resolved : null;
32712
+ const resolved = join13(pkgDir, from);
32713
+ return existsSync11(resolved) ? resolved : null;
32535
32714
  }
32536
32715
  const candidates = [
32537
- join12(rootDir, "node_modules", from),
32538
- join12(pkgDir, "node_modules", from)
32716
+ join13(rootDir, "node_modules", from),
32717
+ join13(pkgDir, "node_modules", from)
32539
32718
  ];
32540
32719
  for (const c of candidates) {
32541
- if (existsSync10(c))
32720
+ if (existsSync11(c))
32542
32721
  return c;
32543
32722
  }
32544
- const bunCacheDir = join12(rootDir, "node_modules", ".bun");
32545
- if (existsSync10(bunCacheDir)) {
32723
+ const bunCacheDir = join13(rootDir, "node_modules", ".bun");
32724
+ if (existsSync11(bunCacheDir)) {
32546
32725
  for (const entry of readdirSync6(bunCacheDir, { withFileTypes: true })) {
32547
32726
  if (!entry.isDirectory())
32548
32727
  continue;
32549
- const candidate = join12(bunCacheDir, entry.name, "node_modules", from);
32550
- if (existsSync10(candidate))
32728
+ const candidate = join13(bunCacheDir, entry.name, "node_modules", from);
32729
+ if (existsSync11(candidate))
32551
32730
  return candidate;
32552
32731
  }
32553
32732
  }
32554
32733
  return null;
32555
32734
  }
32556
32735
  function readBrowserAssets(pkgDir) {
32557
- const pkgJsonPath = join12(pkgDir, "package.json");
32558
- if (!existsSync10(pkgJsonPath))
32736
+ const pkgJsonPath = join13(pkgDir, "package.json");
32737
+ if (!existsSync11(pkgJsonPath))
32559
32738
  return [];
32560
32739
  try {
32561
- const pkg = JSON.parse(readFileSync11(pkgJsonPath, "utf-8"));
32740
+ const pkg = JSON.parse(readFileSync12(pkgJsonPath, "utf-8"));
32562
32741
  const assets = pkg.arc?.browserAssets;
32563
32742
  if (!Array.isArray(assets))
32564
32743
  return [];
@@ -32568,14 +32747,14 @@ function readBrowserAssets(pkgDir) {
32568
32747
  }
32569
32748
  }
32570
32749
  function discoverBrowserAssets(ws) {
32571
- const arcDir = join12(ws.rootDir, "node_modules", "@arcote.tech");
32572
- if (!existsSync10(arcDir))
32750
+ const arcDir = join13(ws.rootDir, "node_modules", "@arcote.tech");
32751
+ if (!existsSync11(arcDir))
32573
32752
  return [];
32574
32753
  const out = [];
32575
32754
  for (const entry of readdirSync6(arcDir, { withFileTypes: true })) {
32576
32755
  if (!entry.isDirectory() && !entry.isSymbolicLink())
32577
32756
  continue;
32578
- const pkgDir = join12(arcDir, entry.name);
32757
+ const pkgDir = join13(arcDir, entry.name);
32579
32758
  const assets = readBrowserAssets(pkgDir);
32580
32759
  for (const asset of assets) {
32581
32760
  const src = resolveAssetSource(asset.from, pkgDir, ws.rootDir);
@@ -32589,7 +32768,7 @@ function discoverBrowserAssets(ws) {
32589
32768
  return out;
32590
32769
  }
32591
32770
  async function copyBrowserAssets(ws, cache, noCache) {
32592
- mkdirSync9(ws.assetsDir, { recursive: true });
32771
+ mkdirSync10(ws.assetsDir, { recursive: true });
32593
32772
  const assets = discoverBrowserAssets(ws);
32594
32773
  if (assets.length === 0)
32595
32774
  return;
@@ -32600,7 +32779,7 @@ async function copyBrowserAssets(ws, cache, noCache) {
32600
32779
  to: a.to,
32601
32780
  mtime: mtimeOf(a.src)
32602
32781
  })));
32603
- const requiredOutputs = assets.map((a) => join12(ws.assetsDir, a.to));
32782
+ const requiredOutputs = assets.map((a) => join13(ws.assetsDir, a.to));
32604
32783
  if (!noCache && isCacheHit(cache, unitId, inputHash, requiredOutputs)) {
32605
32784
  console.log(` \u2713 cached: browser-assets (${assets.length})`);
32606
32785
  return;
@@ -32608,10 +32787,10 @@ async function copyBrowserAssets(ws, cache, noCache) {
32608
32787
  console.log(` building: browser-assets (${assets.length})`);
32609
32788
  const outputHashes = {};
32610
32789
  for (const asset of assets) {
32611
- const dest = join12(ws.assetsDir, asset.to);
32612
- mkdirSync9(dirname8(dest), { recursive: true });
32790
+ const dest = join13(ws.assetsDir, asset.to);
32791
+ mkdirSync10(dirname8(dest), { recursive: true });
32613
32792
  copyFileSync(asset.src, dest);
32614
- outputHashes[asset.to] = sha256Hex(readFileSync11(dest));
32793
+ outputHashes[asset.to] = sha256Hex(readFileSync12(dest));
32615
32794
  }
32616
32795
  updateCache(cache, unitId, inputHash, { outputHashes });
32617
32796
  }
@@ -32619,12 +32798,12 @@ async function loadServerContext(ws) {
32619
32798
  globalThis.ONLY_SERVER = true;
32620
32799
  globalThis.ONLY_BROWSER = false;
32621
32800
  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");
32801
+ const platformDir = join13(process.cwd(), "node_modules", "@arcote.tech", "platform");
32802
+ const platformPkg = JSON.parse(readFileSync12(join13(platformDir, "package.json"), "utf-8"));
32803
+ const platformEntry = join13(platformDir, platformPkg.main ?? "src/index.ts");
32625
32804
  await import(platformEntry);
32626
- const serverEntry = join12(ws.arcDir, "server", SERVER_ENTRY_FILE);
32627
- if (!existsSync10(serverEntry)) {
32805
+ const serverEntry = join13(ws.arcDir, "server", SERVER_ENTRY_FILE);
32806
+ if (!existsSync11(serverEntry)) {
32628
32807
  return { context: null, moduleAccess: new Map };
32629
32808
  }
32630
32809
  try {
@@ -32648,24 +32827,26 @@ async function platformBuild(opts = {}) {
32648
32827
  }
32649
32828
 
32650
32829
  // 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";
32830
+ import { randomBytes } from "crypto";
32831
+ import { existsSync as existsSync18, readFileSync as readFileSync16 } from "fs";
32832
+ import { dirname as dirname11, join as join21 } from "path";
32653
32833
  import { fileURLToPath as fileURLToPath8 } from "url";
32654
32834
 
32655
32835
  // src/deploy/bootstrap.ts
32656
32836
  var {spawn: spawn4 } = globalThis.Bun;
32657
- import { mkdirSync as mkdirSync12, writeFileSync as writeFileSync13 } from "fs";
32837
+ import { mkdirSync as mkdirSync13, writeFileSync as writeFileSync14 } from "fs";
32658
32838
  import { tmpdir as tmpdir2 } from "os";
32659
- import { dirname as dirname9, join as join18 } from "path";
32839
+ import { dirname as dirname9, join as join19 } from "path";
32660
32840
 
32661
32841
  // src/deploy/ansible.ts
32662
32842
  import { spawn as nodeSpawn } from "child_process";
32663
- import { existsSync as existsSync11, mkdirSync as mkdirSync10, writeFileSync as writeFileSync10 } from "fs";
32843
+ import { existsSync as existsSync12, mkdirSync as mkdirSync11, writeFileSync as writeFileSync11 } from "fs";
32664
32844
  import { homedir, tmpdir } from "os";
32665
- import { join as join13 } from "path";
32845
+ import { join as join14 } from "path";
32666
32846
 
32667
32847
  // src/deploy/assets.ts
32668
32848
  var TERRAFORM_MAIN_TF = `terraform {
32849
+ required_version = ">= 1.1"
32669
32850
  required_providers {
32670
32851
  hcloud = {
32671
32852
  source = "hetznercloud/hcloud"
@@ -32678,17 +32859,50 @@ provider "hcloud" {
32678
32859
  token = var.hcloud_token
32679
32860
  }
32680
32861
 
32862
+ locals {
32863
+ # cloud-init injects the operator public key into the default image user's
32864
+ # authorized_keys (root on Hetzner Ubuntu images) \u2014 matching the root-first
32865
+ # ansible bootstrap. Used only when use_cloud_init_key = true.
32866
+ cloud_init_user_data = <<-EOT
32867
+ #cloud-config
32868
+ ssh_pwauth: false
32869
+ chpasswd:
32870
+ expire: false
32871
+ ssh_authorized_keys:
32872
+ - \${trimspace(file(var.ssh_public_key))}
32873
+ # Hetzner marks the root password expired (must-change-on-login) when no
32874
+ # hcloud_ssh_key object is attached \u2014 which blocks non-interactive key
32875
+ # login ("password change required, no TTY"). Clear the expiry so the
32876
+ # deploy user / ansible can SSH in as root right away.
32877
+ runcmd:
32878
+ - chage -d $(date +%Y-%m-%d) -M -1 root
32879
+ EOT
32880
+ }
32881
+
32882
+ # Legacy path: a managed SSH key object in the Hetzner project. Skipped when
32883
+ # use_cloud_init_key = true, because several apps sharing one project with the
32884
+ # same local public key would collide on both key NAME and FINGERPRINT.
32681
32885
  resource "hcloud_ssh_key" "deploy" {
32886
+ count = var.use_cloud_init_key ? 0 : 1
32682
32887
  name = "\${var.server_name}-deploy"
32683
32888
  public_key = file(var.ssh_public_key)
32684
32889
  }
32685
32890
 
32891
+ # Re-home the pre-count address so an already-provisioned host (state created
32892
+ # before count existed) plans as a no-op instead of destroy+recreate \u2014 which
32893
+ # would ForceNew-replace the server. Ignored for fresh (empty) state.
32894
+ moved {
32895
+ from = hcloud_ssh_key.deploy
32896
+ to = hcloud_ssh_key.deploy[0]
32897
+ }
32898
+
32686
32899
  resource "hcloud_server" "arc" {
32687
32900
  name = var.server_name
32688
32901
  image = var.server_image
32689
32902
  server_type = var.server_type
32690
32903
  location = var.server_location
32691
- ssh_keys = [hcloud_ssh_key.deploy.id]
32904
+ ssh_keys = var.use_cloud_init_key ? [] : [hcloud_ssh_key.deploy[0].id]
32905
+ user_data = var.use_cloud_init_key ? local.cloud_init_user_data : null
32692
32906
 
32693
32907
  public_net {
32694
32908
  ipv4_enabled = true
@@ -32739,6 +32953,12 @@ variable "ssh_public_key" {
32739
32953
  type = string
32740
32954
  default = "~/.ssh/id_ed25519.pub"
32741
32955
  }
32956
+
32957
+ variable "use_cloud_init_key" {
32958
+ 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)."
32959
+ type = bool
32960
+ default = false
32961
+ }
32742
32962
  `;
32743
32963
  var ANSIBLE_SITE_YML = `---
32744
32964
  # Arc platform bootstrap playbook \u2014 minimal hardened Docker host.
@@ -32935,30 +33155,30 @@ var ASSETS = {
32935
33155
  }
32936
33156
  };
32937
33157
  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 });
33158
+ const { mkdirSync: mkdirSync11, writeFileSync: writeFileSync11 } = await import("fs");
33159
+ const { join: join14 } = await import("path");
33160
+ mkdirSync11(targetDir, { recursive: true });
32941
33161
  for (const [name, content] of Object.entries(files)) {
32942
- writeFileSync10(join13(targetDir, name), content);
33162
+ writeFileSync11(join14(targetDir, name), content);
32943
33163
  }
32944
33164
  }
32945
33165
 
32946
33166
  // src/deploy/ansible.ts
32947
33167
  function pickSshKeyForAnsible(configured) {
32948
33168
  if (configured) {
32949
- const expanded = configured.startsWith("~") ? join13(homedir(), configured.slice(1)) : configured;
32950
- return existsSync11(expanded) ? expanded : null;
33169
+ const expanded = configured.startsWith("~") ? join14(homedir(), configured.slice(1)) : configured;
33170
+ return existsSync12(expanded) ? expanded : null;
32951
33171
  }
32952
33172
  for (const name of ["id_ed25519", "id_ecdsa", "id_rsa"]) {
32953
- const path4 = join13(homedir(), ".ssh", name);
32954
- if (existsSync11(path4))
33173
+ const path4 = join14(homedir(), ".ssh", name);
33174
+ if (existsSync12(path4))
32955
33175
  return path4;
32956
33176
  }
32957
33177
  return null;
32958
33178
  }
32959
33179
  async function runAnsible(inputs) {
32960
- const workDir = join13(tmpdir(), "arc-deploy", `ansible-${Date.now()}`);
32961
- mkdirSync10(workDir, { recursive: true });
33180
+ const workDir = join14(tmpdir(), "arc-deploy", `ansible-${Date.now()}`);
33181
+ mkdirSync11(workDir, { recursive: true });
32962
33182
  await materializeAssets(workDir, ASSETS.ansible);
32963
33183
  const user = inputs.asRoot ? "root" : inputs.target.user;
32964
33184
  const port = inputs.ansible?.sshPort ?? inputs.target.port;
@@ -32974,7 +33194,7 @@ async function runAnsible(inputs) {
32974
33194
  ""
32975
33195
  ].join(`
32976
33196
  `);
32977
- writeFileSync10(join13(workDir, "inventory.ini"), inventory);
33197
+ writeFileSync11(join14(workDir, "inventory.ini"), inventory);
32978
33198
  const extraVarsJson = JSON.stringify({
32979
33199
  username: inputs.target.user,
32980
33200
  ssh_port: port,
@@ -34533,16 +34753,16 @@ function panelTimeseries(title, gridPos, query, legend, unit, datasource = PROME
34533
34753
  // src/deploy/terraform.ts
34534
34754
  import { spawn as nodeSpawn2 } from "child_process";
34535
34755
  import { createHash as createHash2 } from "crypto";
34536
- import { existsSync as existsSync12, mkdirSync as mkdirSync11, writeFileSync as writeFileSync11 } from "fs";
34756
+ import { existsSync as existsSync13, mkdirSync as mkdirSync12, writeFileSync as writeFileSync12 } from "fs";
34537
34757
  import { homedir as homedir2 } from "os";
34538
- import { join as join14 } from "path";
34758
+ import { join as join15 } from "path";
34539
34759
  async function runTerraform(inputs) {
34540
34760
  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 });
34761
+ const workDir = join15(homedir2(), ".arc-deploy", wsHash, "tf");
34762
+ mkdirSync12(workDir, { recursive: true });
34543
34763
  await materializeAssets(workDir, ASSETS.terraform);
34544
34764
  const sshPubKey = inputs.tf.sshPublicKey ?? expandHome("~/.ssh/id_ed25519.pub");
34545
- if (!existsSync12(expandHome(sshPubKey))) {
34765
+ if (!existsSync13(expandHome(sshPubKey))) {
34546
34766
  throw new Error(`SSH public key not found at ${sshPubKey}. Set provision.terraform.sshPublicKey in deploy.arc.json.`);
34547
34767
  }
34548
34768
  const tfvars = [
@@ -34551,11 +34771,12 @@ async function runTerraform(inputs) {
34551
34771
  `server_type = "${inputs.tf.serverType}"`,
34552
34772
  `server_location = "${inputs.tf.location}"`,
34553
34773
  `server_image = "${inputs.tf.image}"`,
34554
- `ssh_public_key = "${expandHome(sshPubKey)}"`
34774
+ `ssh_public_key = "${expandHome(sshPubKey)}"`,
34775
+ `use_cloud_init_key = ${inputs.useCloudInitKey}`
34555
34776
  ].join(`
34556
34777
  `) + `
34557
34778
  `;
34558
- writeFileSync11(join14(workDir, "terraform.tfvars"), tfvars);
34779
+ writeFileSync12(join15(workDir, "terraform.tfvars"), tfvars);
34559
34780
  await runTf(workDir, ["init", "-input=false", "-no-color"]);
34560
34781
  await runTf(workDir, [
34561
34782
  "apply",
@@ -34613,20 +34834,20 @@ function expandHome(p) {
34613
34834
  }
34614
34835
 
34615
34836
  // src/deploy/config.ts
34616
- import { existsSync as existsSync14, readFileSync as readFileSync13, writeFileSync as writeFileSync12 } from "fs";
34617
- import { join as join16 } from "path";
34837
+ import { existsSync as existsSync15, readFileSync as readFileSync14, writeFileSync as writeFileSync13 } from "fs";
34838
+ import { join as join17 } from "path";
34618
34839
 
34619
34840
  // src/deploy/env-file.ts
34620
- import { appendFileSync, existsSync as existsSync13, readFileSync as readFileSync12 } from "fs";
34621
- import { join as join15 } from "path";
34841
+ import { appendFileSync, existsSync as existsSync14, readFileSync as readFileSync13 } from "fs";
34842
+ import { join as join16 } from "path";
34622
34843
  function loadDeployEnvFiles(rootDir, envNames) {
34623
- const globalsPath = join15(rootDir, "deploy.arc.env");
34624
- const globals = existsSync13(globalsPath) ? parseEnvFile(readFileSync12(globalsPath, "utf-8"), globalsPath) : {};
34844
+ const globalsPath = join16(rootDir, "deploy.arc.env");
34845
+ const globals = existsSync14(globalsPath) ? parseEnvFile(readFileSync13(globalsPath, "utf-8"), globalsPath) : {};
34625
34846
  const perEnv = {};
34626
34847
  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);
34848
+ const envPath = join16(rootDir, `deploy.arc.${name}.env`);
34849
+ if (existsSync14(envPath)) {
34850
+ perEnv[name] = parseEnvFile(readFileSync13(envPath, "utf-8"), envPath);
34630
34851
  }
34631
34852
  }
34632
34853
  return { globals, perEnv };
@@ -34642,16 +34863,16 @@ function ensurePersistedSecret(rootDir, scope, key, generate) {
34642
34863
  if (process.env[key])
34643
34864
  return process.env[key];
34644
34865
  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);
34866
+ const path4 = join16(rootDir, fileName);
34867
+ if (existsSync14(path4)) {
34868
+ const existing = parseEnvFile(readFileSync13(path4, "utf-8"), path4);
34648
34869
  if (existing[key]) {
34649
34870
  process.env[key] = existing[key];
34650
34871
  return existing[key];
34651
34872
  }
34652
34873
  }
34653
34874
  const value = generate();
34654
- const prefix = existsSync13(path4) && !readFileSync12(path4, "utf-8").endsWith(`
34875
+ const prefix = existsSync14(path4) && !readFileSync13(path4, "utf-8").endsWith(`
34655
34876
  `) ? `
34656
34877
  ` : "";
34657
34878
  appendFileSync(path4, `${prefix}${key}=${value}
@@ -34687,17 +34908,17 @@ function parseEnvFile(content, pathForErrors) {
34687
34908
  // src/deploy/config.ts
34688
34909
  var DEPLOY_CONFIG_FILE = "deploy.arc.json";
34689
34910
  function deployConfigPath(rootDir) {
34690
- return join16(rootDir, DEPLOY_CONFIG_FILE);
34911
+ return join17(rootDir, DEPLOY_CONFIG_FILE);
34691
34912
  }
34692
34913
  function deployConfigExists(rootDir) {
34693
- return existsSync14(deployConfigPath(rootDir));
34914
+ return existsSync15(deployConfigPath(rootDir));
34694
34915
  }
34695
34916
  function loadDeployConfig(rootDir) {
34696
34917
  const path4 = deployConfigPath(rootDir);
34697
- if (!existsSync14(path4)) {
34918
+ if (!existsSync15(path4)) {
34698
34919
  throw new Error(`Missing ${DEPLOY_CONFIG_FILE} at ${path4}`);
34699
34920
  }
34700
- const raw = readFileSync13(path4, "utf-8");
34921
+ const raw = readFileSync14(path4, "utf-8");
34701
34922
  let parsed;
34702
34923
  try {
34703
34924
  parsed = JSON.parse(raw);
@@ -34720,9 +34941,9 @@ function loadDeployConfig(rootDir) {
34720
34941
  }
34721
34942
  function saveDeployConfig(rootDir, cfg) {
34722
34943
  const path4 = deployConfigPath(rootDir);
34723
- const raw = existsSync14(path4) ? JSON.parse(readFileSync13(path4, "utf-8")) : {};
34944
+ const raw = existsSync15(path4) ? JSON.parse(readFileSync14(path4, "utf-8")) : {};
34724
34945
  raw.target = { ...raw.target, ...cfg.target };
34725
- writeFileSync12(path4, JSON.stringify(raw, null, 2) + `
34946
+ writeFileSync13(path4, JSON.stringify(raw, null, 2) + `
34726
34947
  `);
34727
34948
  }
34728
34949
  var VAR_REGEX = /\$\{([A-Z0-9_]+)\}|\$([A-Z0-9_]+)/g;
@@ -34757,9 +34978,20 @@ function validateDeployConfig(input) {
34757
34978
  if (!/^[A-Z][A-Z0-9_]*$/.test(passwordEnv)) {
34758
34979
  throw new Error(`deploy.arc.json: registry.passwordEnv "${passwordEnv}" must be an UPPER_SNAKE_CASE env var name`);
34759
34980
  }
34981
+ const provisionRaw = input.provision;
34982
+ const hasProvisionTf = isObject(provisionRaw) && isObject(provisionRaw.terraform);
34983
+ const hostRaw = optionalString(target, "target.host");
34984
+ let host;
34985
+ if (hostRaw && hostRaw.length > 0) {
34986
+ host = hostRaw;
34987
+ } else if (hasProvisionTf) {
34988
+ host = "PENDING_TERRAFORM";
34989
+ } else {
34990
+ throw new Error(`deploy.arc.json: target.host is required unless provision.terraform is present (with provision, terraform fills it on first deploy).`);
34991
+ }
34760
34992
  const validated = {
34761
34993
  target: {
34762
- host: requireString(target, "target.host"),
34994
+ host,
34763
34995
  user: optionalString(target, "target.user") ?? "deploy",
34764
34996
  port: optionalNumber(target, "target.port") ?? 22,
34765
34997
  remoteDir: optionalString(target, "target.remoteDir") ?? "/opt/arc",
@@ -34860,13 +35092,23 @@ function validateDeployConfig(input) {
34860
35092
  if (providerVal !== "hcloud") {
34861
35093
  throw new Error(`deploy.arc.json: provision.terraform.provider must be "hcloud" (got "${providerVal}")`);
34862
35094
  }
35095
+ const serverName = optionalString(tf, "provision.terraform.serverName");
35096
+ if (serverName !== undefined && !/^[a-z0-9-]{1,63}$/.test(serverName)) {
35097
+ throw new Error(`deploy.arc.json: provision.terraform.serverName "${serverName}" must match [a-z0-9-] and be at most 63 chars`);
35098
+ }
35099
+ const sshKeyStrategyRaw = optionalString(tf, "provision.terraform.sshKeyStrategy");
35100
+ if (sshKeyStrategyRaw !== undefined && sshKeyStrategyRaw !== "resource" && sshKeyStrategyRaw !== "cloud-init") {
35101
+ throw new Error(`deploy.arc.json: provision.terraform.sshKeyStrategy must be "resource" or "cloud-init" (got "${sshKeyStrategyRaw}")`);
35102
+ }
34863
35103
  const terraform = {
34864
35104
  provider: "hcloud",
34865
35105
  serverType: requireString(tf, "provision.terraform.serverType"),
34866
35106
  location: requireString(tf, "provision.terraform.location"),
34867
35107
  image: optionalString(tf, "provision.terraform.image") ?? "ubuntu-22.04",
34868
35108
  tokenEnv: requireString(tf, "provision.terraform.tokenEnv"),
34869
- sshPublicKey: optionalString(tf, "provision.terraform.sshPublicKey")
35109
+ sshPublicKey: optionalString(tf, "provision.terraform.sshPublicKey"),
35110
+ serverName,
35111
+ sshKeyStrategy: sshKeyStrategyRaw
34870
35112
  };
34871
35113
  let ansible;
34872
35114
  const ansibleRaw = provision.ansible;
@@ -34936,17 +35178,17 @@ function cfgErr(path4, expected) {
34936
35178
 
34937
35179
  // src/deploy/ssh.ts
34938
35180
  var {spawn: spawn3 } = globalThis.Bun;
34939
- import { existsSync as existsSync15 } from "fs";
35181
+ import { existsSync as existsSync16 } from "fs";
34940
35182
  import { homedir as homedir3 } from "os";
34941
- import { join as join17 } from "path";
35183
+ import { join as join18 } from "path";
34942
35184
  function pickSshKey(target) {
34943
35185
  if (target.sshKey) {
34944
- const expanded = target.sshKey.startsWith("~") ? join17(homedir3(), target.sshKey.slice(1)) : target.sshKey;
34945
- return existsSync15(expanded) ? expanded : null;
35186
+ const expanded = target.sshKey.startsWith("~") ? join18(homedir3(), target.sshKey.slice(1)) : target.sshKey;
35187
+ return existsSync16(expanded) ? expanded : null;
34946
35188
  }
34947
35189
  for (const name of ["id_ed25519", "id_ecdsa", "id_rsa"]) {
34948
- const path4 = join17(homedir3(), ".ssh", name);
34949
- if (existsSync15(path4))
35190
+ const path4 = join18(homedir3(), ".ssh", name);
35191
+ if (existsSync16(path4))
34950
35192
  return path4;
34951
35193
  }
34952
35194
  return null;
@@ -34961,7 +35203,7 @@ function sshMuxArgs() {
34961
35203
  "-o",
34962
35204
  "ControlMaster=auto",
34963
35205
  "-o",
34964
- `ControlPath=${join17(homedir3(), ".ssh", "cm-arc-%C")}`,
35206
+ `ControlPath=${join18(homedir3(), ".ssh", "cm-arc-%C")}`,
34965
35207
  "-o",
34966
35208
  "ControlPersist=120"
34967
35209
  ];
@@ -35161,10 +35403,14 @@ async function bootstrap(inputs) {
35161
35403
  if (!token) {
35162
35404
  throw new Error(`Environment variable ${cfg.provision.terraform.tokenEnv} is not set`);
35163
35405
  }
35406
+ const firstEnv = Object.keys(cfg.envs)[0] ?? "host";
35407
+ const serverName = cfg.provision.terraform.serverName ?? `arc-${firstEnv}`;
35408
+ const useCloudInitKey = cfg.provision.terraform.sshKeyStrategy === "cloud-init";
35164
35409
  const tfOut = await runTerraform({
35165
35410
  tf: cfg.provision.terraform,
35166
35411
  token,
35167
- serverName: `arc-${Object.keys(cfg.envs)[0] ?? "host"}`,
35412
+ serverName,
35413
+ useCloudInitKey,
35168
35414
  workspaceDir: rootDir
35169
35415
  });
35170
35416
  ok(`Server provisioned: ${tfOut.serverIp}`);
@@ -35226,17 +35472,17 @@ async function isRegistryRunning(cfg) {
35226
35472
  }
35227
35473
  async function upStack(inputs) {
35228
35474
  const { cfg } = inputs;
35229
- const workDir = join18(tmpdir2(), "arc-deploy", `stack-${Date.now()}`);
35230
- mkdirSync12(workDir, { recursive: true });
35475
+ const workDir = join19(tmpdir2(), "arc-deploy", `stack-${Date.now()}`);
35476
+ mkdirSync13(workDir, { recursive: true });
35231
35477
  await assertRegistryDnsResolves(cfg);
35232
35478
  const password = process.env[cfg.registry.passwordEnv];
35233
35479
  if (!password) {
35234
35480
  throw new Error(`Registry password env var ${cfg.registry.passwordEnv} is not set. ` + `Set it (e.g. \`export ${cfg.registry.passwordEnv}=...\`) before bootstrap.`);
35235
35481
  }
35236
35482
  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 }));
35483
+ writeFileSync14(join19(workDir, "htpasswd"), htpasswdLine);
35484
+ writeFileSync14(join19(workDir, "Caddyfile"), generateCaddyfile(cfg));
35485
+ writeFileSync14(join19(workDir, "docker-compose.yml"), generateCompose({ cfg }));
35240
35486
  let observabilityFiles = null;
35241
35487
  let observabilityHtpasswd = null;
35242
35488
  if (cfg.observability?.enabled) {
@@ -35248,30 +35494,30 @@ async function upStack(inputs) {
35248
35494
  }
35249
35495
  observabilityHtpasswd = await generateCaddyBasicAuthLine("admin", adminPassword);
35250
35496
  for (const [relPath, contents] of Object.entries(observabilityFiles)) {
35251
- const fullPath = join18(workDir, relPath);
35252
- mkdirSync12(dirname9(fullPath), { recursive: true });
35253
- writeFileSync13(fullPath, contents);
35497
+ const fullPath = join19(workDir, relPath);
35498
+ mkdirSync13(dirname9(fullPath), { recursive: true });
35499
+ writeFileSync14(fullPath, contents);
35254
35500
  }
35255
- writeFileSync13(join18(workDir, "observability-htpasswd"), observabilityHtpasswd);
35501
+ writeFileSync14(join19(workDir, "observability-htpasswd"), observabilityHtpasswd);
35256
35502
  }
35257
35503
  await assertExec(cfg.target, `sudo mkdir -p ${cfg.target.remoteDir} && sudo chown ${cfg.target.user}:${cfg.target.user} ${cfg.target.remoteDir}`);
35258
35504
  for (const name of Object.keys(cfg.envs)) {
35259
35505
  await assertExec(cfg.target, `mkdir -p ${cfg.target.remoteDir}/${name}`);
35260
35506
  }
35261
35507
  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`);
35508
+ await scpUpload(cfg.target, join19(workDir, "Caddyfile"), `${cfg.target.remoteDir}/Caddyfile`);
35509
+ await scpUpload(cfg.target, join19(workDir, "docker-compose.yml"), `${cfg.target.remoteDir}/docker-compose.yml`);
35510
+ await scpUpload(cfg.target, join19(workDir, "htpasswd"), `${cfg.target.remoteDir}/registry-auth/htpasswd`);
35265
35511
  if (observabilityFiles && observabilityHtpasswd) {
35266
35512
  await assertExec(cfg.target, `mkdir -p ${cfg.target.remoteDir}/observability/grafana-dashboards ${cfg.target.remoteDir}/observability/grafana-alerting`);
35267
35513
  for (const relPath of Object.keys(observabilityFiles)) {
35268
- const localDir = dirname9(join18(workDir, relPath));
35269
- mkdirSync12(localDir, { recursive: true });
35514
+ const localDir = dirname9(join19(workDir, relPath));
35515
+ mkdirSync13(localDir, { recursive: true });
35270
35516
  }
35271
35517
  for (const relPath of Object.keys(observabilityFiles)) {
35272
- await scpUpload(cfg.target, join18(workDir, relPath), `${cfg.target.remoteDir}/${relPath}`);
35518
+ await scpUpload(cfg.target, join19(workDir, relPath), `${cfg.target.remoteDir}/${relPath}`);
35273
35519
  }
35274
- await scpUpload(cfg.target, join18(workDir, "observability-htpasswd"), `${cfg.target.remoteDir}/observability-htpasswd`);
35520
+ await scpUpload(cfg.target, join19(workDir, "observability-htpasswd"), `${cfg.target.remoteDir}/observability-htpasswd`);
35275
35521
  }
35276
35522
  await assertExec(cfg.target, `touch ${cfg.target.remoteDir}/.env`);
35277
35523
  if (cfg.observability?.enabled) {
@@ -35426,14 +35672,14 @@ var {spawn: spawn5 } = globalThis.Bun;
35426
35672
  import { createHash as createHash3 } from "crypto";
35427
35673
  import {
35428
35674
  copyFileSync as copyFileSync2,
35429
- existsSync as existsSync16,
35430
- mkdirSync as mkdirSync13,
35431
- readFileSync as readFileSync14,
35675
+ existsSync as existsSync17,
35676
+ mkdirSync as mkdirSync14,
35677
+ readFileSync as readFileSync15,
35432
35678
  realpathSync as realpathSync2,
35433
- writeFileSync as writeFileSync14
35679
+ writeFileSync as writeFileSync15
35434
35680
  } from "fs";
35435
35681
  import { tmpdir as tmpdir3 } from "os";
35436
- import { dirname as dirname10, join as join19 } from "path";
35682
+ import { dirname as dirname10, join as join20 } from "path";
35437
35683
  import { fileURLToPath as fileURLToPath7 } from "url";
35438
35684
 
35439
35685
  // src/deploy/image-template.ts
@@ -35483,8 +35729,8 @@ function generateDockerfile(inputs) {
35483
35729
  // src/deploy/image.ts
35484
35730
  async function buildImage(ws, opts) {
35485
35731
  await ensureDocker();
35486
- const manifestPath = join19(ws.arcDir, "manifest.json");
35487
- if (!existsSync16(manifestPath)) {
35732
+ const manifestPath = join20(ws.arcDir, "manifest.json");
35733
+ if (!existsSync17(manifestPath)) {
35488
35734
  throw new Error(`No build manifest at ${manifestPath}. Run \`arc platform build\` first or omit --skip-build.`);
35489
35735
  }
35490
35736
  embedCliBundle(ws);
@@ -35494,10 +35740,10 @@ async function buildImage(ws, opts) {
35494
35740
  const dockerfileInputs = collectDockerfileInputs(ws);
35495
35741
  const dockerfile = generateDockerfile(dockerfileInputs);
35496
35742
  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);
35743
+ const dockerfileDir = join20(tmpdir3(), `arc-image-${Date.now()}`);
35744
+ mkdirSync14(dockerfileDir, { recursive: true });
35745
+ const dockerfilePath = join20(dockerfileDir, "Dockerfile");
35746
+ writeFileSync15(dockerfilePath, dockerfile);
35501
35747
  const buildArgs = [
35502
35748
  "build",
35503
35749
  "--platform=linux/amd64",
@@ -35522,20 +35768,20 @@ async function buildImage(ws, opts) {
35522
35768
  }
35523
35769
  function embedCliBundle(ws) {
35524
35770
  const source = locateCliBundle();
35525
- const target = join19(ws.arcDir, "host.js");
35771
+ const target = join20(ws.arcDir, "host.js");
35526
35772
  copyFileSync2(source, target);
35527
35773
  }
35528
35774
  function locateCliBundle() {
35529
35775
  const here = fileURLToPath7(import.meta.url);
35530
35776
  let cur = dirname10(here);
35531
35777
  while (cur !== "/" && cur !== "") {
35532
- const candidate = join19(cur, "package.json");
35533
- if (existsSync16(candidate)) {
35778
+ const candidate = join20(cur, "package.json");
35779
+ if (existsSync17(candidate)) {
35534
35780
  try {
35535
- const pkg = JSON.parse(readFileSync14(candidate, "utf-8"));
35781
+ const pkg = JSON.parse(readFileSync15(candidate, "utf-8"));
35536
35782
  if (pkg.name === "@arcote.tech/arc-cli") {
35537
- const distIndex = join19(realpathSync2(cur), "dist", "index.js");
35538
- if (!existsSync16(distIndex)) {
35783
+ const distIndex = join20(realpathSync2(cur), "dist", "index.js");
35784
+ if (!existsSync17(distIndex)) {
35539
35785
  throw new Error(`arc-cli bundle missing at ${distIndex}. Run \`bun run build\` in packages/cli/.`);
35540
35786
  }
35541
35787
  return distIndex;
@@ -35567,17 +35813,17 @@ async function ensureDocker() {
35567
35813
  }
35568
35814
  }
35569
35815
  function computeContentHash(manifestPath) {
35570
- const raw = JSON.parse(readFileSync14(manifestPath, "utf-8"));
35816
+ const raw = JSON.parse(readFileSync15(manifestPath, "utf-8"));
35571
35817
  delete raw.buildTime;
35572
35818
  const canonical = JSON.stringify(raw);
35573
35819
  return createHash3("sha256").update(canonical).digest("hex").slice(0, 12);
35574
35820
  }
35575
35821
  function collectDockerfileInputs(ws) {
35576
- const hasPublicDir = existsSync16(join19(ws.rootDir, "public"));
35577
- const hasLocales = existsSync16(join19(ws.rootDir, "locales"));
35822
+ const hasPublicDir = existsSync17(join20(ws.rootDir, "public"));
35823
+ const hasLocales = existsSync17(join20(ws.rootDir, "locales"));
35578
35824
  let manifestPath;
35579
35825
  for (const name of ["manifest.webmanifest", "manifest.json"]) {
35580
- if (existsSync16(join19(ws.rootDir, name))) {
35826
+ if (existsSync17(join20(ws.rootDir, name))) {
35581
35827
  manifestPath = name;
35582
35828
  break;
35583
35829
  }
@@ -36192,7 +36438,7 @@ ${import_picocolors2.default.gray(m2)} ${s}
36192
36438
  };
36193
36439
 
36194
36440
  // src/deploy/survey.ts
36195
- async function runSurvey() {
36441
+ async function runSurvey(appName) {
36196
36442
  we("arc platform deploy \u2014 initial setup");
36197
36443
  const mode = await de({
36198
36444
  message: "How should deploy reach the target server?",
@@ -36294,12 +36540,17 @@ async function runSurvey() {
36294
36540
  });
36295
36541
  if (BD(location))
36296
36542
  cancel();
36543
+ const firstEnv = Object.keys(envs)[0] ?? "host";
36544
+ const appSlug = sanitizeImageName(appName ?? "arc-app").replace(/[^a-z0-9-]+/g, "-");
36545
+ const serverName = `arc-${appSlug}-${firstEnv}`.slice(0, 63);
36297
36546
  const terraform = {
36298
36547
  provider: "hcloud",
36299
36548
  serverType,
36300
36549
  location,
36301
36550
  image: "ubuntu-22.04",
36302
- tokenEnv
36551
+ tokenEnv,
36552
+ serverName,
36553
+ sshKeyStrategy: "cloud-init"
36303
36554
  };
36304
36555
  provision = { terraform };
36305
36556
  }
@@ -36384,12 +36635,15 @@ function cancel() {
36384
36635
  }
36385
36636
 
36386
36637
  // src/commands/platform-deploy.ts
36638
+ function federationExposesModules(ws) {
36639
+ return Boolean(ws.rootPkg?.arc?.federation?.slug);
36640
+ }
36387
36641
  async function platformDeploy(envArg, options = {}) {
36388
36642
  const ws = resolveWorkspace();
36389
36643
  let cfg;
36390
36644
  if (!deployConfigExists(ws.rootDir)) {
36391
36645
  log2("No deploy.arc.json found \u2014 launching survey.");
36392
- cfg = await runSurvey();
36646
+ cfg = await runSurvey(ws.appName);
36393
36647
  saveDeployConfig(ws.rootDir, cfg);
36394
36648
  ok("Saved deploy.arc.json");
36395
36649
  }
@@ -36398,6 +36652,13 @@ async function platformDeploy(envArg, options = {}) {
36398
36652
  for (const pg of pgEnvs) {
36399
36653
  ensurePersistedSecret(ws.rootDir, pg.name, pg.passwordKey, () => crypto.randomUUID().replace(/-/g, ""));
36400
36654
  }
36655
+ if (federationExposesModules(ws)) {
36656
+ for (const name of Object.keys(cfg.envs)) {
36657
+ ensurePersistedSecret(ws.rootDir, name, "ARC_APP_SECRET", () => randomBytes(32).toString("base64url"));
36658
+ ensurePersistedSecret(ws.rootDir, name, "ARC_PAIRING_TOKEN", () => randomBytes(16).toString("base64url"));
36659
+ }
36660
+ cfg = loadDeployConfig(ws.rootDir);
36661
+ }
36401
36662
  if (cfg.observability?.enabled) {
36402
36663
  const key = cfg.observability.adminPasswordEnv ?? "ARC_OBSERVABILITY_PASSWORD";
36403
36664
  ensurePersistedSecret(ws.rootDir, "globals", key, () => crypto.randomUUID().replace(/-/g, ""));
@@ -36406,14 +36667,14 @@ async function platformDeploy(envArg, options = {}) {
36406
36667
  err(`Unknown env "${envArg}". Known: ${Object.keys(cfg.envs).join(", ")}`);
36407
36668
  process.exit(1);
36408
36669
  })() : Object.keys(cfg.envs);
36409
- const manifestPath = join20(ws.arcDir, "manifest.json");
36670
+ const manifestPath = join21(ws.arcDir, "manifest.json");
36410
36671
  if (!options.imageTag) {
36411
- const needBuild = options.rebuild || !existsSync17(manifestPath);
36672
+ const needBuild = options.rebuild || !existsSync18(manifestPath);
36412
36673
  if (needBuild && !options.skipBuild) {
36413
36674
  log2("Building platform...");
36414
36675
  await buildAll(ws, { noCache: options.rebuild });
36415
36676
  ok("Build complete");
36416
- } else if (!existsSync17(manifestPath)) {
36677
+ } else if (!existsSync18(manifestPath)) {
36417
36678
  err("No build found and --skip-build was set.");
36418
36679
  process.exit(1);
36419
36680
  }
@@ -36483,9 +36744,9 @@ function readCliVersion2() {
36483
36744
  let cur = dirname11(fileURLToPath8(import.meta.url));
36484
36745
  const root = dirname11(cur).startsWith("/") ? "/" : ".";
36485
36746
  while (cur !== root && cur !== "") {
36486
- const candidate = join20(cur, "package.json");
36487
- if (existsSync17(candidate)) {
36488
- const pkg = JSON.parse(readFileSync15(candidate, "utf-8"));
36747
+ const candidate = join21(cur, "package.json");
36748
+ if (existsSync18(candidate)) {
36749
+ const pkg = JSON.parse(readFileSync16(candidate, "utf-8"));
36489
36750
  if (pkg.name === "@arcote.tech/arc-cli") {
36490
36751
  return pkg.version ?? "unknown";
36491
36752
  }
@@ -36501,8 +36762,8 @@ function readCliVersion2() {
36501
36762
  }
36502
36763
  }
36503
36764
  async function hashDeployConfig(rootDir) {
36504
- const p2 = join20(rootDir, "deploy.arc.json");
36505
- const content = readFileSync15(p2);
36765
+ const p2 = join21(rootDir, "deploy.arc.json");
36766
+ const content = readFileSync16(p2);
36506
36767
  const hasher = new Bun.CryptoHasher("sha256");
36507
36768
  hasher.update(content);
36508
36769
  hasher.update(readCliVersion2());
@@ -36510,8 +36771,9 @@ async function hashDeployConfig(rootDir) {
36510
36771
  }
36511
36772
 
36512
36773
  // src/platform/startup.ts
36513
- import { existsSync as existsSync19, readFileSync as readFileSync16, watch } from "fs";
36514
- import { join as join22 } from "path";
36774
+ import { randomBytes as randomBytes2 } from "crypto";
36775
+ import { existsSync as existsSync20, mkdirSync as mkdirSync16, readFileSync as readFileSync17, watch, writeFileSync as writeFileSync16 } from "fs";
36776
+ import { join as join23 } from "path";
36515
36777
 
36516
36778
  // ../host/src/connection-manager.ts
36517
36779
  class ConnectionManager {
@@ -37947,6 +38209,7 @@ async function createArcServer(config) {
37947
38209
  const messageChains = new Map;
37948
38210
  const coep = config.coep ?? "unsafe-none";
37949
38211
  const corsOrigins = config.corsOrigins;
38212
+ const allowPrivateNetwork = config.allowPrivateNetwork ?? false;
37950
38213
  const maxRequestBodySize = config.maxRequestBodySize ?? 10 * 1024 * 1024;
37951
38214
  const securityHeaders = {
37952
38215
  "X-Content-Type-Options": "nosniff",
@@ -37958,7 +38221,7 @@ async function createArcServer(config) {
37958
38221
  const origin = req?.headers.get("Origin") || null;
37959
38222
  const headers = {
37960
38223
  "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, PATCH, OPTIONS",
37961
- "Access-Control-Allow-Headers": "Content-Type, Authorization, X-Arc-Scope, X-Arc-Tokens",
38224
+ "Access-Control-Allow-Headers": "Content-Type, Authorization, X-Arc-Scope, X-Arc-Tokens, X-Arc-Pairing-Token",
37962
38225
  "Cross-Origin-Opener-Policy": "same-origin",
37963
38226
  "Cross-Origin-Embedder-Policy": coep,
37964
38227
  "Cross-Origin-Resource-Policy": "cross-origin",
@@ -37995,7 +38258,8 @@ async function createArcServer(config) {
37995
38258
  const url = new URL(req.url);
37996
38259
  const corsHeaders2 = buildCorsHeaders(req);
37997
38260
  if (req.method === "OPTIONS") {
37998
- return new Response(null, { headers: corsHeaders2 });
38261
+ const pna = allowPrivateNetwork && req.headers.get("Access-Control-Request-Private-Network") ? { "Access-Control-Allow-Private-Network": "true" } : {};
38262
+ return new Response(null, { headers: { ...corsHeaders2, ...pna } });
37999
38263
  }
38000
38264
  const handleRequest = async (span) => {
38001
38265
  const contentLength = Number(req.headers.get("Content-Length") ?? 0);
@@ -38137,8 +38401,9 @@ async function createArcServer(config) {
38137
38401
  }
38138
38402
  // src/platform/server.ts
38139
38403
  init_i18n();
38140
- import { existsSync as existsSync18, mkdirSync as mkdirSync14 } from "fs";
38141
- import { join as join21 } from "path";
38404
+ import { timingSafeEqual } from "crypto";
38405
+ import { existsSync as existsSync19, mkdirSync as mkdirSync15 } from "fs";
38406
+ import { join as join22 } from "path";
38142
38407
  async function resolveDbAdapterFactory(dbPath) {
38143
38408
  const databaseUrl = process.env.DATABASE_URL;
38144
38409
  if (databaseUrl && databaseUrl.length > 0) {
@@ -38148,10 +38413,10 @@ async function resolveDbAdapterFactory(dbPath) {
38148
38413
  const { createBunSQLiteAdapterFactory: createBunSQLiteAdapterFactory2 } = await Promise.resolve().then(() => (init_dist2(), exports_dist3));
38149
38414
  const dbDir = dbPath.substring(0, dbPath.lastIndexOf("/"));
38150
38415
  if (dbDir)
38151
- mkdirSync14(dbDir, { recursive: true });
38416
+ mkdirSync15(dbDir, { recursive: true });
38152
38417
  return createBunSQLiteAdapterFactory2(dbPath);
38153
38418
  }
38154
- function generateShellHtml(appName, manifest, initial, stylesHash) {
38419
+ function generateShellHtml(appName, manifest, initial, stylesHash, shell) {
38155
38420
  const otelConfig = process.env.ARC_OTEL_ENABLED === "true" ? {
38156
38421
  enabled: true,
38157
38422
  endpoint: "/otel",
@@ -38165,6 +38430,9 @@ function generateShellHtml(appName, manifest, initial, stylesHash) {
38165
38430
  if (!initialUrl) {
38166
38431
  throw new Error("generateShellHtml: initial bundle missing from manifest");
38167
38432
  }
38433
+ const importMapTag = shell ? `
38434
+ <script type="importmap">${JSON.stringify({ imports: shell.imports })}</script>
38435
+ <script>window.__ARC_PEERS__=${JSON.stringify(shell.peers)};</script>` : "";
38168
38436
  const stylesQs = stylesHash ? `?v=${stylesHash.slice(0, 16)}` : "";
38169
38437
  return `<!doctype html>
38170
38438
  <html lang="en">
@@ -38175,7 +38443,7 @@ function generateShellHtml(appName, manifest, initial, stylesHash) {
38175
38443
  <link rel="icon" href="${manifest.favicon}">` : ""}${manifest ? `
38176
38444
  <link rel="manifest" href="/manifest.json${stylesQs}">` : ""}
38177
38445
  <link rel="stylesheet" href="/styles.css${stylesQs}" />
38178
- <link rel="stylesheet" href="/theme.css${stylesQs}" />
38446
+ <link rel="stylesheet" href="/theme.css${stylesQs}" />${importMapTag}
38179
38447
  <link rel="modulepreload" href="${initialUrl}" />${otelTag}
38180
38448
  </head>
38181
38449
  <body>
@@ -38207,7 +38475,7 @@ function getMime(path4) {
38207
38475
  return MIME[ext2] ?? "application/octet-stream";
38208
38476
  }
38209
38477
  function serveFile(filePath, headers = {}) {
38210
- if (!existsSync18(filePath))
38478
+ if (!existsSync19(filePath))
38211
38479
  return new Response("Not Found", { status: 404 });
38212
38480
  return new Response(Bun.file(filePath), {
38213
38481
  headers: { "Content-Type": getMime(filePath), ...headers }
@@ -38225,11 +38493,44 @@ function ensureModuleSigSecret(ws, devMode) {
38225
38493
  MODULE_SIG_SECRET = crypto.randomUUID();
38226
38494
  }
38227
38495
  }
38228
- function signGroupUrl(file) {
38496
+ function signGroupUrl(file, prefix = "/browser") {
38229
38497
  const hasher = new Bun.CryptoHasher("sha256");
38230
38498
  hasher.update(`${file}:${MODULE_SIG_SECRET}`);
38231
38499
  const sig = hasher.digest("hex").slice(0, 16);
38232
- return `/browser/${file}?sig=${sig}`;
38500
+ return `${prefix}/${file}?sig=${sig}`;
38501
+ }
38502
+ async function filterGroupsForTokens(groups, moduleAccessMap, tokenPayloads, urlPrefix) {
38503
+ const tokensByName = new Map;
38504
+ for (const t of tokenPayloads) {
38505
+ if (t?.tokenType)
38506
+ tokensByName.set(t.tokenType, t);
38507
+ }
38508
+ const filtered = {};
38509
+ for (const [name, group] of Object.entries(groups)) {
38510
+ let allGranted = true;
38511
+ for (const moduleName of group.modules) {
38512
+ const access = moduleAccessMap.get(moduleName);
38513
+ if (!access || access.rules.length === 0)
38514
+ continue;
38515
+ let granted = false;
38516
+ for (const rule of access.rules) {
38517
+ const matching = tokensByName.get(rule.token.name);
38518
+ if (!matching)
38519
+ continue;
38520
+ granted = rule.check ? await rule.check(matching) : true;
38521
+ if (granted)
38522
+ break;
38523
+ }
38524
+ if (!granted) {
38525
+ allGranted = false;
38526
+ break;
38527
+ }
38528
+ }
38529
+ if (allGranted) {
38530
+ filtered[name] = { ...group, url: signGroupUrl(group.file, urlPrefix) };
38531
+ }
38532
+ }
38533
+ return filtered;
38233
38534
  }
38234
38535
  function verifyGroupSignature(file, sig) {
38235
38536
  if (!sig)
@@ -38270,36 +38571,7 @@ function parseArcTokensHeader(header) {
38270
38571
  return payloads;
38271
38572
  }
38272
38573
  async function filterManifestForTokens(manifest, moduleAccessMap, tokenPayloads) {
38273
- const tokensByName = new Map;
38274
- for (const t of tokenPayloads) {
38275
- if (t?.tokenType)
38276
- tokensByName.set(t.tokenType, t);
38277
- }
38278
- const filteredGroups = {};
38279
- for (const [name, group] of Object.entries(manifest.groups)) {
38280
- let allGranted = true;
38281
- for (const moduleName of group.modules) {
38282
- const access = moduleAccessMap.get(moduleName);
38283
- if (!access || access.rules.length === 0)
38284
- continue;
38285
- let granted = false;
38286
- for (const rule of access.rules) {
38287
- const matching = tokensByName.get(rule.token.name);
38288
- if (!matching)
38289
- continue;
38290
- granted = rule.check ? await rule.check(matching) : true;
38291
- if (granted)
38292
- break;
38293
- }
38294
- if (!granted) {
38295
- allGranted = false;
38296
- break;
38297
- }
38298
- }
38299
- if (allGranted) {
38300
- filteredGroups[name] = { ...group, url: signGroupUrl(group.file) };
38301
- }
38302
- }
38574
+ const filteredGroups = await filterGroupsForTokens(manifest.groups, moduleAccessMap, tokenPayloads, "/browser");
38303
38575
  return {
38304
38576
  initial: manifest.initial,
38305
38577
  groups: filteredGroups,
@@ -38325,28 +38597,58 @@ function staticFilesHandler(ws, devMode, getManifest) {
38325
38597
  return new Response("Forbidden", { status: 403, headers: ctx.corsHeaders });
38326
38598
  }
38327
38599
  }
38328
- return serveFile(join21(ws.browserDir, file), {
38600
+ return serveFile(join22(ws.browserDir, file), {
38601
+ ...ctx.corsHeaders,
38602
+ "Cache-Control": devMode ? "no-cache" : "max-age=31536000,immutable"
38603
+ });
38604
+ }
38605
+ if (path4.startsWith("/shell/")) {
38606
+ const file = path4.slice(7);
38607
+ if (!BROWSER_FILE_RE.test(file)) {
38608
+ return new Response("Not Found", { status: 404, headers: ctx.corsHeaders });
38609
+ }
38610
+ return serveFile(join22(ws.arcDir, "shell", file), {
38611
+ ...ctx.corsHeaders,
38612
+ "Cross-Origin-Resource-Policy": "cross-origin",
38613
+ "Cache-Control": devMode ? "no-cache" : "max-age=31536000,immutable"
38614
+ });
38615
+ }
38616
+ if (path4.startsWith("/browser-fed/")) {
38617
+ const file = path4.slice(13);
38618
+ if (!BROWSER_FILE_RE.test(file)) {
38619
+ return new Response("Not Found", { status: 404, headers: ctx.corsHeaders });
38620
+ }
38621
+ const fed = getManifest().federation;
38622
+ const isGroupEntry = fed ? Object.values(fed.build.groups).some((g3) => g3.file === file) : false;
38623
+ if (isGroupEntry) {
38624
+ const sig = url.searchParams.get("sig");
38625
+ if (!verifyGroupSignature(file, sig)) {
38626
+ return new Response("Forbidden", { status: 403, headers: ctx.corsHeaders });
38627
+ }
38628
+ }
38629
+ return serveFile(join22(ws.arcDir, "browser-fed", file), {
38329
38630
  ...ctx.corsHeaders,
38631
+ "Cross-Origin-Resource-Policy": "cross-origin",
38330
38632
  "Cache-Control": devMode ? "no-cache" : "max-age=31536000,immutable"
38331
38633
  });
38332
38634
  }
38333
38635
  if (path4.startsWith("/locales/"))
38334
- return serveFile(join21(ws.arcDir, path4.slice(1)), {
38636
+ return serveFile(join22(ws.arcDir, path4.slice(1)), {
38335
38637
  ...ctx.corsHeaders,
38336
38638
  "Cache-Control": devMode ? "no-cache" : "max-age=300,stale-while-revalidate=3600"
38337
38639
  });
38338
38640
  if (path4.startsWith("/assets/"))
38339
- return serveFile(join21(ws.assetsDir, path4.slice(8)), {
38641
+ return serveFile(join22(ws.assetsDir, path4.slice(8)), {
38340
38642
  ...ctx.corsHeaders,
38341
38643
  "Cache-Control": devMode ? "no-cache" : "max-age=31536000,immutable"
38342
38644
  });
38343
38645
  if (path4 === "/styles.css")
38344
- return serveFile(join21(ws.arcDir, "styles.css"), {
38646
+ return serveFile(join22(ws.arcDir, "styles.css"), {
38345
38647
  ...ctx.corsHeaders,
38346
38648
  "Cache-Control": devMode ? "no-cache" : "max-age=31536000,immutable"
38347
38649
  });
38348
38650
  if (path4 === "/theme.css")
38349
- return serveFile(join21(ws.arcDir, "theme.css"), {
38651
+ return serveFile(join22(ws.arcDir, "theme.css"), {
38350
38652
  ...ctx.corsHeaders,
38351
38653
  "Cache-Control": devMode ? "no-cache" : "max-age=31536000,immutable"
38352
38654
  });
@@ -38357,15 +38659,114 @@ function staticFilesHandler(ws, devMode, getManifest) {
38357
38659
  });
38358
38660
  }
38359
38661
  if (path4.lastIndexOf(".") > path4.lastIndexOf("/")) {
38360
- const publicFile = join21(ws.publicDir, path4.slice(1));
38361
- if (existsSync18(publicFile))
38662
+ const publicFile = join22(ws.publicDir, path4.slice(1));
38663
+ if (existsSync19(publicFile))
38362
38664
  return serveFile(publicFile, ctx.corsHeaders);
38363
38665
  }
38364
38666
  return null;
38365
38667
  };
38366
38668
  }
38367
- function apiEndpointsHandler(ws, getManifest, cm, moduleAccessMap, telemetry) {
38669
+ function parseCorsOrigins(raw) {
38670
+ if (!raw)
38671
+ return;
38672
+ const list = raw.split(",").map((s) => s.trim()).filter(Boolean);
38673
+ return list.length > 0 ? list : undefined;
38674
+ }
38675
+ var DISCOVERY_SCHEMA_VERSION = 1;
38676
+ function safeEqual(a2, b4) {
38677
+ const ab = Buffer.from(a2);
38678
+ const bb = Buffer.from(b4);
38679
+ if (ab.length !== bb.length)
38680
+ return false;
38681
+ return timingSafeEqual(ab, bb);
38682
+ }
38683
+ function handleDiscovery(req, ctx, ws, manifest, federation, mapUrl) {
38684
+ if (!federation) {
38685
+ return new Response("Not Found", { status: 404, headers: ctx.corsHeaders });
38686
+ }
38687
+ const presented = req.headers.get("X-Arc-Pairing-Token");
38688
+ if (!presented || !safeEqual(presented, federation.registrationToken)) {
38689
+ return new Response("Unauthorized", {
38690
+ status: 401,
38691
+ headers: ctx.corsHeaders
38692
+ });
38693
+ }
38694
+ const fed = manifest.federation;
38695
+ const platformUrl = process.env.ARC_PUBLIC_URL || new URL(req.url).origin;
38696
+ return Response.json({
38697
+ meta: {
38698
+ schemaVersion: DISCOVERY_SCHEMA_VERSION,
38699
+ name: ws.appName,
38700
+ version: ws.rootPkg?.version ?? undefined
38701
+ },
38702
+ platformUrl,
38703
+ federation: fed ? {
38704
+ slug: fed.config.slug,
38705
+ auth: fed.config.auth,
38706
+ modules: fed.modules,
38707
+ permissions: fed.permissions,
38708
+ slots: fed.slots
38709
+ } : null,
38710
+ peers: manifest.shell?.peers ?? null,
38711
+ federationManifestUrl: fed ? `${platformUrl}/api/federation/manifest` : null,
38712
+ mapUrl: mapUrl ?? null
38713
+ }, { headers: { ...ctx.corsHeaders, "Cache-Control": "no-cache, private" } });
38714
+ }
38715
+ async function handleFederationRegister(req, ctx, manifest, federation) {
38716
+ const fed = manifest.federation;
38717
+ if (!fed || !federation) {
38718
+ return new Response("Not Found", { status: 404, headers: ctx.corsHeaders });
38719
+ }
38720
+ const presented = req.headers.get("X-Arc-Pairing-Token");
38721
+ if (!presented || !safeEqual(presented, federation.registrationToken)) {
38722
+ return new Response("Unauthorized", {
38723
+ status: 401,
38724
+ headers: ctx.corsHeaders
38725
+ });
38726
+ }
38727
+ let body;
38728
+ try {
38729
+ body = await req.json();
38730
+ } catch {
38731
+ return new Response("Bad Request", {
38732
+ status: 400,
38733
+ headers: ctx.corsHeaders
38734
+ });
38735
+ }
38736
+ const hostId = typeof body.hostId === "string" ? body.hostId : "";
38737
+ const hostUrl = typeof body.hostUrl === "string" ? body.hostUrl : "";
38738
+ const hostName = typeof body.hostName === "string" ? body.hostName : hostId;
38739
+ const organizationId = typeof body.organizationId === "string" ? body.organizationId : "";
38740
+ if (!hostId || !hostUrl) {
38741
+ return new Response("Bad Request: hostId + hostUrl required", {
38742
+ status: 400,
38743
+ headers: ctx.corsHeaders
38744
+ });
38745
+ }
38746
+ ok(`Federation: host "${hostName}" (${hostUrl}) registered for org ${organizationId || "\u2014"}`);
38747
+ return Response.json({
38748
+ secret: federation.appSecret,
38749
+ slug: fed.config.slug,
38750
+ auth: fed.config.auth,
38751
+ modules: fed.modules,
38752
+ permissions: fed.permissions,
38753
+ slots: fed.slots
38754
+ }, { headers: { ...ctx.corsHeaders, "Cache-Control": "no-store" } });
38755
+ }
38756
+ function apiEndpointsHandler(ws, getManifest, cm, moduleAccessMap, telemetry, federation, getMapUrl) {
38368
38757
  return (req, url, ctx) => {
38758
+ if (url.pathname === "/api/discovery") {
38759
+ return handleDiscovery(req, ctx, ws, getManifest(), federation, getMapUrl?.());
38760
+ }
38761
+ if (url.pathname === "/api/federation/register") {
38762
+ if (req.method !== "POST") {
38763
+ return new Response("Method Not Allowed", {
38764
+ status: 405,
38765
+ headers: { ...ctx.corsHeaders, Allow: "POST, OPTIONS" }
38766
+ });
38767
+ }
38768
+ return handleFederationRegister(req, ctx, getManifest(), federation);
38769
+ }
38369
38770
  if (url.pathname === "/api/modules") {
38370
38771
  const arcTokensHeader = req.headers.get("X-Arc-Tokens");
38371
38772
  let payloads = parseArcTokensHeader(arcTokensHeader);
@@ -38379,6 +38780,36 @@ function apiEndpointsHandler(ws, getManifest, cm, moduleAccessMap, telemetry) {
38379
38780
  }
38380
38781
  }));
38381
38782
  }
38783
+ if (url.pathname === "/api/federation/manifest") {
38784
+ const m4 = getManifest();
38785
+ if (!m4.federation || !m4.shell) {
38786
+ return new Response("Not Found", { status: 404, headers: ctx.corsHeaders });
38787
+ }
38788
+ const arcTokensHeader = req.headers.get("X-Arc-Tokens");
38789
+ let payloads = parseArcTokensHeader(arcTokensHeader);
38790
+ if (payloads.length === 0 && ctx.tokenPayload) {
38791
+ payloads = [ctx.tokenPayload];
38792
+ }
38793
+ const fed = m4.federation;
38794
+ return filterGroupsForTokens(fed.build.groups, moduleAccessMap, payloads, "/browser-fed").then((groups) => Response.json({
38795
+ initial: fed.build.initial,
38796
+ groups,
38797
+ sharedChunks: fed.build.sharedChunks,
38798
+ stylesHash: m4.stylesHash,
38799
+ peers: m4.shell.peers,
38800
+ app: {
38801
+ slug: fed.config.slug,
38802
+ name: ws.appName,
38803
+ version: ws.rootPkg.version ?? undefined,
38804
+ modules: fed.modules,
38805
+ permissions: fed.permissions,
38806
+ slots: fed.slots,
38807
+ auth: fed.config.auth
38808
+ }
38809
+ }, {
38810
+ headers: { ...ctx.corsHeaders, "Cache-Control": "no-cache, private" }
38811
+ }));
38812
+ }
38382
38813
  if (url.pathname === "/api/translations") {
38383
38814
  const config = readTranslationsConfig(ws.rootDir);
38384
38815
  return Response.json(config ?? { locales: [], sourceLocale: "" }, {
@@ -38424,6 +38855,17 @@ function devReloadHandler(sseClients) {
38424
38855
  });
38425
38856
  };
38426
38857
  }
38858
+ function headlessFallbackHandler(ws, getManifest) {
38859
+ return (_req, _url, ctx) => {
38860
+ const m4 = getManifest();
38861
+ return Response.json({
38862
+ arc: "headless",
38863
+ app: ws.appName,
38864
+ federation: m4.federation ?? null,
38865
+ endpoints: ["/api/federation/manifest", "/api/modules", "/health"]
38866
+ }, { headers: ctx.corsHeaders });
38867
+ };
38868
+ }
38427
38869
  function spaFallbackHandler(getShellHtml) {
38428
38870
  return (_req, _url, ctx) => {
38429
38871
  return new Response(getShellHtml(), {
@@ -38466,8 +38908,14 @@ async function startPlatformServer(opts) {
38466
38908
  const setManifest = (m4) => {
38467
38909
  manifest = m4;
38468
38910
  };
38469
- const getShellHtml = () => generateShellHtml(ws.appName, ws.manifest, manifest?.initial, manifest?.stylesHash);
38911
+ let mapUrl = opts.mapUrl;
38912
+ const setMapUrl = (u2) => {
38913
+ mapUrl = u2;
38914
+ };
38915
+ const getMapUrl = () => mapUrl;
38916
+ const getShellHtml = () => generateShellHtml(ws.appName, ws.manifest, manifest?.initial, manifest?.stylesHash, manifest?.shell);
38470
38917
  const sseClients = new Set;
38918
+ const fallbackHandler = opts.headless ? headlessFallbackHandler(ws, getManifest) : spaFallbackHandler(getShellHtml);
38471
38919
  const notifyReload = (m4) => {
38472
38920
  const data = JSON.stringify(m4);
38473
38921
  for (const c2 of sseClients) {
@@ -38493,18 +38941,23 @@ async function startPlatformServer(opts) {
38493
38941
  port,
38494
38942
  async fetch(req) {
38495
38943
  const url = new URL(req.url);
38496
- if (req.method === "OPTIONS")
38497
- return new Response(null, { headers: cors });
38944
+ if (req.method === "OPTIONS") {
38945
+ const headers = { ...cors };
38946
+ if (req.headers.get("Access-Control-Request-Private-Network")) {
38947
+ headers["Access-Control-Allow-Private-Network"] = "true";
38948
+ }
38949
+ return new Response(null, { headers });
38950
+ }
38498
38951
  const ctx = {
38499
38952
  rawToken: null,
38500
38953
  tokenPayload: null,
38501
38954
  corsHeaders: cors
38502
38955
  };
38503
38956
  const handlers = [
38504
- apiEndpointsHandler(ws, getManifest, null, moduleAccessMap, telemetry),
38957
+ apiEndpointsHandler(ws, getManifest, null, moduleAccessMap, telemetry, opts.federation, getMapUrl),
38505
38958
  devReloadHandler(sseClients),
38506
38959
  staticFilesHandler(ws, !!devMode, getManifest),
38507
- spaFallbackHandler(getShellHtml)
38960
+ fallbackHandler
38508
38961
  ];
38509
38962
  for (const handler of handlers) {
38510
38963
  const response = await handler(req, url, ctx);
@@ -38519,11 +38972,12 @@ async function startPlatformServer(opts) {
38519
38972
  contextHandler: null,
38520
38973
  connectionManager: null,
38521
38974
  setManifest,
38975
+ setMapUrl,
38522
38976
  notifyReload,
38523
38977
  stop: () => server.stop()
38524
38978
  };
38525
38979
  }
38526
- const dbPath = opts.dbPath || join21(ws.arcDir, "data", "arc.db");
38980
+ const dbPath = opts.dbPath || join22(ws.arcDir, "data", "arc.db");
38527
38981
  const baseDbFactory = await resolveDbAdapterFactory(dbPath);
38528
38982
  let dbAdapterFactory = baseDbFactory;
38529
38983
  if (telemetry) {
@@ -38539,11 +38993,13 @@ async function startPlatformServer(opts) {
38539
38993
  dbAdapterFactory,
38540
38994
  port,
38541
38995
  coep,
38996
+ corsOrigins: parseCorsOrigins(process.env.ARC_CORS_ORIGINS),
38997
+ allowPrivateNetwork: !!devMode,
38542
38998
  httpHandlers: [
38543
- apiEndpointsHandler(ws, getManifest, null, moduleAccessMap, telemetry),
38999
+ apiEndpointsHandler(ws, getManifest, null, moduleAccessMap, telemetry, opts.federation, getMapUrl),
38544
39000
  devReloadHandler(sseClients),
38545
39001
  staticFilesHandler(ws, !!devMode, getManifest),
38546
- spaFallbackHandler(getShellHtml)
39002
+ fallbackHandler
38547
39003
  ],
38548
39004
  onWsClose: (clientId) => cleanupClientSubs(clientId),
38549
39005
  telemetry
@@ -38553,6 +39009,7 @@ async function startPlatformServer(opts) {
38553
39009
  contextHandler: arcServer.contextHandler,
38554
39010
  connectionManager: arcServer.connectionManager,
38555
39011
  setManifest,
39012
+ setMapUrl,
38556
39013
  notifyReload,
38557
39014
  stop: () => {
38558
39015
  arcServer.stop();
@@ -38565,17 +39022,23 @@ async function startPlatformServer(opts) {
38565
39022
  async function startPlatform(opts) {
38566
39023
  const { ws, devMode } = opts;
38567
39024
  const port = opts.port ?? parseInt(process.env.PORT || "5005", 10);
38568
- const dbPath = opts.dbPath ?? join22(ws.rootDir, ".arc", "data", devMode ? "dev.db" : "prod.db");
39025
+ const dbPath = opts.dbPath ?? join23(ws.rootDir, ".arc", "data", devMode ? "dev.db" : "prod.db");
38569
39026
  let manifest;
38570
39027
  if (devMode) {
38571
39028
  manifest = await buildAll(ws);
38572
39029
  } else {
38573
- const manifestPath = join22(ws.arcDir, "manifest.json");
38574
- if (!existsSync19(manifestPath)) {
39030
+ const manifestPath = join23(ws.arcDir, "manifest.json");
39031
+ if (!existsSync20(manifestPath)) {
38575
39032
  err("No build found. Run `arc platform build` first.");
38576
39033
  process.exit(1);
38577
39034
  }
38578
- manifest = JSON.parse(readFileSync16(manifestPath, "utf-8"));
39035
+ manifest = JSON.parse(readFileSync17(manifestPath, "utf-8"));
39036
+ }
39037
+ assertProdFederationSecrets(devMode, !!manifest.federation);
39038
+ const needsPairing = !!manifest.federation || devMode && (opts.map || opts.headless);
39039
+ const pairingToken = needsPairing ? resolvePairingToken(ws.rootDir) : undefined;
39040
+ if (manifest.federation) {
39041
+ process.env.ARC_APP_SECRET = resolveAppSecret(ws.rootDir);
38579
39042
  }
38580
39043
  log2("Loading server context...");
38581
39044
  const { context: context2, moduleAccess } = await loadServerContext(ws);
@@ -38596,7 +39059,12 @@ async function startPlatform(opts) {
38596
39059
  context: context2,
38597
39060
  moduleAccess,
38598
39061
  dbPath,
38599
- devMode
39062
+ devMode,
39063
+ headless: opts.headless,
39064
+ federation: manifest.federation && pairingToken ? {
39065
+ registrationToken: pairingToken,
39066
+ appSecret: process.env.ARC_APP_SECRET
39067
+ } : undefined
38600
39068
  });
38601
39069
  break;
38602
39070
  } catch (e2) {
@@ -38617,8 +39085,18 @@ async function startPlatform(opts) {
38617
39085
  ok("Commands, queries, WebSocket \u2014 all on same port");
38618
39086
  }
38619
39087
  let onReload;
38620
- if (devMode && opts.map) {
38621
- onReload = await startMapTool(ws, actualPort + 1);
39088
+ if (devMode && (opts.map || opts.headless)) {
39089
+ onReload = await startMapTool(ws, actualPort + 1, pairingToken, platform3, {
39090
+ platformUrl: `http://localhost:${actualPort}`,
39091
+ federation: manifest.federation ? {
39092
+ slug: manifest.federation.config.slug,
39093
+ auth: manifest.federation.config.auth,
39094
+ modules: manifest.federation.modules,
39095
+ permissions: manifest.federation.permissions,
39096
+ slots: manifest.federation.slots
39097
+ } : undefined,
39098
+ peers: manifest.shell?.peers
39099
+ });
38622
39100
  }
38623
39101
  if (devMode) {
38624
39102
  attachDevWatcher(ws, platform3, onReload);
@@ -38630,25 +39108,98 @@ async function startPlatform(opts) {
38630
39108
  process.on("SIGTERM", cleanup);
38631
39109
  process.on("SIGINT", cleanup);
38632
39110
  }
38633
- async function startMapTool(ws, port) {
39111
+ async function startMapTool(ws, port, authToken, platformServer, platform3) {
38634
39112
  try {
38635
- const { startMapServer } = await import("@arcote.tech/arc-map");
39113
+ const { startMapServer, MAP_SCHEMA_VERSION } = await import("@arcote.tech/arc-map");
38636
39114
  const { getContext } = await import("@arcote.tech/platform");
38637
- const globs = ws.packages.map((p3) => join22(p3.path, "src", "**", "*.{ts,tsx}"));
38638
- const packageOf = (absFile) => ws.packages.find((p3) => absFile.startsWith(join22(p3.path, "src")))?.name;
39115
+ const globs = ws.packages.map((p3) => join23(p3.path, "src", "**", "*.{ts,tsx}"));
39116
+ const packageOf = (absFile) => ws.packages.find((p3) => absFile.startsWith(join23(p3.path, "src")))?.name;
39117
+ const meta = readAppMeta(ws.rootDir, MAP_SCHEMA_VERSION);
38639
39118
  const map = await startMapServer({
38640
39119
  getContext: () => getContext(),
38641
39120
  scan: { globs, workspaceRoot: ws.rootDir, packageOf },
38642
39121
  useCasePackages: ws.packages.map((p3) => ({ name: p3.name, path: p3.path })),
38643
- port
39122
+ port,
39123
+ authToken,
39124
+ meta,
39125
+ platform: platform3
38644
39126
  });
39127
+ platformServer.setMapUrl(map.url);
38645
39128
  ok(`Context map \u2192 ${map.url}`);
39129
+ ok(`Pairing token: ${authToken}`);
38646
39130
  return map.notifyReload;
38647
39131
  } catch (e2) {
38648
39132
  err(`Context map failed to start: ${e2.message}`);
38649
39133
  return;
38650
39134
  }
38651
39135
  }
39136
+ function assertProdFederationSecrets(devMode, hasFederation) {
39137
+ if (devMode || !hasFederation)
39138
+ return;
39139
+ const missing = [];
39140
+ if (!process.env.ARC_APP_SECRET)
39141
+ missing.push("ARC_APP_SECRET");
39142
+ if (!process.env.ARC_PAIRING_TOKEN && !process.env.ARC_MAP_TOKEN) {
39143
+ missing.push("ARC_PAIRING_TOKEN");
39144
+ }
39145
+ if (missing.length === 0)
39146
+ return;
39147
+ err(`Federacja w produkcji wymaga zmiennych \u015Brodowiskowych: ${missing.join(", ")}.
39148
+ ` + ` Bez nich sekret i token s\u0105 generowane w kontenerze i GIN\u0104 przy pierwszym
39149
+ ` + ` recreate \u2014 hosty musia\u0142yby przeinstalowa\u0107 aplikacj\u0119, bez \u017Cadnego b\u0142\u0119du.
39150
+ ` + ` Ustaw je w deployu (deploy.arc.json \u2192 envVars + deploy.arc.<env>.env).`);
39151
+ process.exit(1);
39152
+ }
39153
+ function resolveAppSecret(rootDir) {
39154
+ const fromEnv = process.env.ARC_APP_SECRET;
39155
+ if (fromEnv)
39156
+ return fromEnv;
39157
+ const file = join23(rootDir, ".arc", "app-secret");
39158
+ try {
39159
+ const saved = readFileSync17(file, "utf-8").trim();
39160
+ if (saved)
39161
+ return saved;
39162
+ } catch {}
39163
+ const secret = randomBytes2(32).toString("base64url");
39164
+ try {
39165
+ mkdirSync16(join23(rootDir, ".arc"), { recursive: true });
39166
+ writeFileSync16(file, secret, { mode: 384 });
39167
+ } catch (e2) {
39168
+ err(`App secret not persisted (${e2.message}) \u2014 hosts will need re-install after restart`);
39169
+ }
39170
+ return secret;
39171
+ }
39172
+ function resolvePairingToken(rootDir) {
39173
+ const fromEnv = process.env.ARC_PAIRING_TOKEN || process.env.ARC_MAP_TOKEN;
39174
+ if (fromEnv)
39175
+ return fromEnv;
39176
+ const file = join23(rootDir, ".arc", "pairing-token");
39177
+ try {
39178
+ const saved = readFileSync17(file, "utf-8").trim();
39179
+ if (saved)
39180
+ return saved;
39181
+ } catch {}
39182
+ const token = randomBytes2(16).toString("base64url");
39183
+ try {
39184
+ mkdirSync16(join23(rootDir, ".arc"), { recursive: true });
39185
+ writeFileSync16(file, token, { mode: 384 });
39186
+ } catch (e2) {
39187
+ err(`Pairing token not persisted (${e2.message}) \u2014 it will change on restart`);
39188
+ }
39189
+ return token;
39190
+ }
39191
+ function readAppMeta(rootDir, schemaVersion) {
39192
+ try {
39193
+ const pkg = JSON.parse(readFileSync17(join23(rootDir, "package.json"), "utf-8"));
39194
+ return {
39195
+ schemaVersion,
39196
+ name: pkg.name ?? rootDir.split("/").pop() ?? "arc-app",
39197
+ version: pkg.version
39198
+ };
39199
+ } catch {
39200
+ return { schemaVersion, name: rootDir.split("/").pop() ?? "arc-app" };
39201
+ }
39202
+ }
38652
39203
  function attachDevWatcher(ws, platform3, onReload) {
38653
39204
  log2("Watching for changes...");
38654
39205
  let rebuildTimer = null;
@@ -38675,8 +39226,8 @@ function attachDevWatcher(ws, platform3, onReload) {
38675
39226
  }, 300);
38676
39227
  };
38677
39228
  for (const pkg of ws.packages) {
38678
- const srcDir = join22(pkg.path, "src");
38679
- if (!existsSync19(srcDir))
39229
+ const srcDir = join23(pkg.path, "src");
39230
+ if (!existsSync20(srcDir))
38680
39231
  continue;
38681
39232
  watch(srcDir, { recursive: true }, (_event, filename) => {
38682
39233
  if (!filename || filename.includes(".arc") || filename.endsWith(".d.ts") || filename.includes("node_modules") || filename.includes("dist"))
@@ -38686,8 +39237,8 @@ function attachDevWatcher(ws, platform3, onReload) {
38686
39237
  triggerRebuild();
38687
39238
  });
38688
39239
  }
38689
- const localesDir = join22(ws.rootDir, "locales");
38690
- if (existsSync19(localesDir)) {
39240
+ const localesDir = join23(ws.rootDir, "locales");
39241
+ if (existsSync20(localesDir)) {
38691
39242
  watch(localesDir, { recursive: false }, (_event, filename) => {
38692
39243
  if (!filename?.endsWith(".po"))
38693
39244
  return;
@@ -38699,7 +39250,12 @@ function attachDevWatcher(ws, platform3, onReload) {
38699
39250
  // src/commands/platform-dev.ts
38700
39251
  async function platformDev(opts = {}) {
38701
39252
  const ws = resolveWorkspace();
38702
- await startPlatform({ ws, devMode: true, map: opts.map });
39253
+ await startPlatform({
39254
+ ws,
39255
+ devMode: true,
39256
+ map: opts.map,
39257
+ headless: opts.headless
39258
+ });
38703
39259
  }
38704
39260
 
38705
39261
  // src/commands/platform-start.ts
@@ -38714,7 +39270,11 @@ program2.name("arc").description("CLI tool for Arc framework").version("0.0.3");
38714
39270
  program2.command("dev").description("Run development mode for Arc framework").action(dev);
38715
39271
  program2.command("build").description("Build all clients and declarations").action(build);
38716
39272
  var platform3 = program2.command("platform").description("Platform commands \u2014 run full stack (server + UI)");
38717
- 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 }));
39273
+ 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({
39274
+ noCache: opts.cache === false,
39275
+ map: opts.map,
39276
+ headless: opts.headless
39277
+ }));
38718
39278
  platform3.command("build").description("Build platform for production").option("--no-cache", "Force full rebuild").action((opts) => platformBuild({ noCache: opts.cache === false }));
38719
39279
  platform3.command("start").description("Start platform in production mode (requires prior build)").action(platformStart);
38720
39280
  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));