@arcote.tech/arc-cli 0.4.7 → 0.4.9

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
@@ -13529,14 +13529,23 @@ function apply(state, patches, applyOptions) {
13529
13529
  function hasLocalStorage() {
13530
13530
  return typeof localStorage !== "undefined";
13531
13531
  }
13532
+ function notifyTokenChange(scope) {
13533
+ if (typeof window !== "undefined") {
13534
+ queueMicrotask(() => {
13535
+ window.dispatchEvent(new CustomEvent("arc:token-change", { detail: { scope } }));
13536
+ });
13537
+ }
13538
+ }
13532
13539
 
13533
13540
  class AuthAdapter {
13534
13541
  scopes = new Map;
13535
13542
  setToken(token, scope = "default") {
13536
13543
  if (!token) {
13537
13544
  this.scopes.delete(scope);
13538
- if (hasLocalStorage())
13545
+ if (hasLocalStorage()) {
13539
13546
  localStorage.removeItem(TOKEN_PREFIX + scope);
13547
+ notifyTokenChange(scope);
13548
+ }
13540
13549
  return;
13541
13550
  }
13542
13551
  try {
@@ -13557,8 +13566,10 @@ class AuthAdapter {
13557
13566
  exp: payload.exp
13558
13567
  }
13559
13568
  });
13560
- if (hasLocalStorage())
13569
+ if (hasLocalStorage()) {
13561
13570
  localStorage.setItem(TOKEN_PREFIX + scope, token);
13571
+ notifyTokenChange(scope);
13572
+ }
13562
13573
  } catch {
13563
13574
  this.scopes.delete(scope);
13564
13575
  if (hasLocalStorage())
@@ -17501,10 +17512,10 @@ var {
17501
17512
  Help
17502
17513
  } = import__.default;
17503
17514
 
17504
- // ../../node_modules/find-up/index.js
17515
+ // node_modules/find-up/index.js
17505
17516
  import path2 from "node:path";
17506
17517
 
17507
- // ../../node_modules/locate-path/index.js
17518
+ // node_modules/find-up/node_modules/locate-path/index.js
17508
17519
  import process2 from "node:process";
17509
17520
  import path from "node:path";
17510
17521
  import fs, { promises as fsPromises } from "node:fs";
@@ -17550,7 +17561,7 @@ function toPath2(urlOrPath) {
17550
17561
  return urlOrPath instanceof URL ? fileURLToPath2(urlOrPath) : urlOrPath;
17551
17562
  }
17552
17563
 
17553
- // ../../node_modules/find-up/index.js
17564
+ // node_modules/find-up/index.js
17554
17565
  var findUpStop = Symbol("findUpStop");
17555
17566
  function findUpMultipleSync(name, options = {}) {
17556
17567
  let directory = path2.resolve(toPath2(options.cwd) ?? "");
@@ -26121,11 +26132,28 @@ async function buildAll(ws) {
26121
26132
  }
26122
26133
  }
26123
26134
  log2("Building shell...");
26124
- await buildShell(ws.shellDir);
26135
+ await buildShell(ws.shellDir, ws.packages);
26125
26136
  ok("Shell built");
26126
26137
  return manifest;
26127
26138
  }
26128
- async function buildShell(outDir) {
26139
+ function collectArcPeerDeps(packages) {
26140
+ const seen = new Set;
26141
+ for (const pkg of ["@arcote.tech/arc", "@arcote.tech/arc-ds", "@arcote.tech/arc-react", "@arcote.tech/platform"]) {
26142
+ seen.add(pkg);
26143
+ }
26144
+ for (const wp of packages) {
26145
+ const peerDeps = wp.packageJson.peerDependencies ?? {};
26146
+ for (const dep of Object.keys(peerDeps)) {
26147
+ if (dep.startsWith("@arcote.tech/"))
26148
+ seen.add(dep);
26149
+ }
26150
+ }
26151
+ return [...seen].map((pkg) => {
26152
+ const short = pkg === "@arcote.tech/platform" ? "platform" : pkg.replace("@arcote.tech/", "");
26153
+ return [short, pkg];
26154
+ });
26155
+ }
26156
+ async function buildShell(outDir, packages) {
26129
26157
  mkdirSync6(outDir, { recursive: true });
26130
26158
  const tmpDir = join7(outDir, "_tmp");
26131
26159
  mkdirSync6(tmpDir, { recursive: true });
@@ -26178,13 +26206,10 @@ export const { createPortal, flushSync } = ReactDOM;`
26178
26206
  console.error(l);
26179
26207
  throw new Error("Shell React build failed");
26180
26208
  }
26181
- const arcEntries = [
26209
+ const arcEntries = packages ? collectArcPeerDeps(packages) : [
26182
26210
  ["arc", "@arcote.tech/arc"],
26183
26211
  ["arc-ds", "@arcote.tech/arc-ds"],
26184
26212
  ["arc-react", "@arcote.tech/arc-react"],
26185
- ["arc-auth", "@arcote.tech/arc-auth"],
26186
- ["arc-utils", "@arcote.tech/arc-utils"],
26187
- ["arc-workspace", "@arcote.tech/arc-workspace"],
26188
26213
  ["platform", "@arcote.tech/platform"]
26189
26214
  ];
26190
26215
  const baseExternal = [
@@ -27580,7 +27605,7 @@ async function createArcServer(config) {
27580
27605
  const corsHeaders = {
27581
27606
  "Access-Control-Allow-Origin": "*",
27582
27607
  "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, PATCH, OPTIONS",
27583
- "Access-Control-Allow-Headers": "Content-Type, Authorization, X-Arc-Scope"
27608
+ "Access-Control-Allow-Headers": "Content-Type, Authorization, X-Arc-Scope, X-Arc-Tokens"
27584
27609
  };
27585
27610
  function verifyToken(token) {
27586
27611
  try {
@@ -27699,7 +27724,13 @@ async function createArcServer(config) {
27699
27724
  // src/platform/server.ts
27700
27725
  import { existsSync as existsSync7, mkdirSync as mkdirSync7 } from "node:fs";
27701
27726
  import { join as join8 } from "node:path";
27702
- function generateShellHtml(appName, manifest) {
27727
+ function generateShellHtml(appName, manifest, arcEntries) {
27728
+ const arcImports = {};
27729
+ if (arcEntries) {
27730
+ for (const [short, pkg] of arcEntries) {
27731
+ arcImports[pkg] = `/shell/${short}.js`;
27732
+ }
27733
+ }
27703
27734
  const importMap = {
27704
27735
  imports: {
27705
27736
  react: "/shell/react.js",
@@ -27707,13 +27738,7 @@ function generateShellHtml(appName, manifest) {
27707
27738
  "react/jsx-dev-runtime": "/shell/jsx-dev-runtime.js",
27708
27739
  "react-dom": "/shell/react-dom.js",
27709
27740
  "react-dom/client": "/shell/react-dom-client.js",
27710
- "@arcote.tech/arc": "/shell/arc.js",
27711
- "@arcote.tech/arc-ds": "/shell/arc-ds.js",
27712
- "@arcote.tech/arc-react": "/shell/arc-react.js",
27713
- "@arcote.tech/arc-auth": "/shell/arc-auth.js",
27714
- "@arcote.tech/arc-utils": "/shell/arc-utils.js",
27715
- "@arcote.tech/arc-workspace": "/shell/arc-workspace.js",
27716
- "@arcote.tech/platform": "/shell/platform.js"
27741
+ ...arcImports
27717
27742
  }
27718
27743
  };
27719
27744
  return `<!doctype html>
@@ -27785,28 +27810,68 @@ function verifyModuleSignature(filename, sig, exp) {
27785
27810
  hasher.update(`${filename}:${exp}:${MODULE_SIG_SECRET}`);
27786
27811
  return hasher.digest("hex").slice(0, 16) === sig;
27787
27812
  }
27788
- async function filterManifestForToken(manifest, moduleAccessMap, tokenPayload) {
27813
+ function decodeTokenPayload(jwt2) {
27814
+ try {
27815
+ const parts = jwt2.split(".");
27816
+ if (parts.length !== 3)
27817
+ return null;
27818
+ const payload = JSON.parse(atob(parts[1]));
27819
+ return {
27820
+ tokenType: payload.tokenName ?? payload.tokenType,
27821
+ params: payload.params || {},
27822
+ iat: payload.iat,
27823
+ exp: payload.exp
27824
+ };
27825
+ } catch {
27826
+ return null;
27827
+ }
27828
+ }
27829
+ function parseArcTokensHeader(header) {
27830
+ if (!header)
27831
+ return [];
27832
+ const payloads = [];
27833
+ for (const entry of header.split(",")) {
27834
+ const colonIdx = entry.indexOf(":");
27835
+ if (colonIdx < 0)
27836
+ continue;
27837
+ const jwt2 = entry.slice(colonIdx + 1);
27838
+ const payload = decodeTokenPayload(jwt2);
27839
+ if (payload)
27840
+ payloads.push(payload);
27841
+ }
27842
+ return payloads;
27843
+ }
27844
+ async function filterManifestForTokens(manifest, moduleAccessMap, tokenPayloads) {
27789
27845
  const filtered = [];
27846
+ console.log(`[arc:modules] Filtering ${manifest.modules.length} modules with ${tokenPayloads.length} token(s):`, tokenPayloads.map((t) => `${t.tokenType}(${JSON.stringify(t.params)})`).join(", ") || "none");
27847
+ console.log(`[arc:modules] Protected modules:`, [...moduleAccessMap.keys()].join(", ") || "none");
27790
27848
  for (const mod of manifest.modules) {
27791
27849
  const access = moduleAccessMap.get(mod.name);
27792
27850
  if (!access) {
27793
27851
  filtered.push(mod);
27794
27852
  continue;
27795
27853
  }
27796
- if (!tokenPayload)
27854
+ if (tokenPayloads.length === 0) {
27855
+ console.log(`[arc:modules] ${mod.name}: SKIP (no tokens)`);
27797
27856
  continue;
27857
+ }
27798
27858
  let granted = false;
27799
27859
  for (const rule of access.rules) {
27800
- if (tokenPayload.tokenType === rule.token.name) {
27801
- granted = rule.check ? await rule.check(tokenPayload) : true;
27860
+ const matching = tokenPayloads.find((t) => t.tokenType === rule.token.name);
27861
+ if (matching) {
27862
+ granted = rule.check ? await rule.check(matching) : true;
27863
+ console.log(`[arc:modules] ${mod.name}: rule ${rule.token.name} matched token, check=${granted}`);
27802
27864
  if (granted)
27803
27865
  break;
27866
+ } else {
27867
+ console.log(`[arc:modules] ${mod.name}: rule needs "${rule.token.name}", no matching token (have: ${tokenPayloads.map((t) => t.tokenType).join(",")})`);
27804
27868
  }
27805
27869
  }
27806
27870
  if (granted) {
27807
27871
  filtered.push({ ...mod, url: signModuleUrl(mod.file) });
27808
27872
  }
27809
27873
  }
27874
+ console.log(`[arc:modules] Result: ${filtered.map((m2) => m2.name).join(", ")}`);
27810
27875
  return { modules: filtered, buildTime: manifest.buildTime };
27811
27876
  }
27812
27877
  function staticFilesHandler(ws, devMode, moduleAccessMap) {
@@ -27848,9 +27913,14 @@ function staticFilesHandler(ws, devMode, moduleAccessMap) {
27848
27913
  };
27849
27914
  }
27850
27915
  function apiEndpointsHandler(ws, getManifest, cm, moduleAccessMap) {
27851
- return (_req, url, ctx) => {
27916
+ return (req, url, ctx) => {
27852
27917
  if (url.pathname === "/api/modules") {
27853
- return filterManifestForToken(getManifest(), moduleAccessMap, ctx.tokenPayload).then((filtered) => Response.json(filtered, { headers: ctx.corsHeaders }));
27918
+ const arcTokensHeader = req.headers.get("X-Arc-Tokens");
27919
+ let payloads = parseArcTokensHeader(arcTokensHeader);
27920
+ if (payloads.length === 0 && ctx.tokenPayload) {
27921
+ payloads = [ctx.tokenPayload];
27922
+ }
27923
+ return filterManifestForTokens(getManifest(), moduleAccessMap, payloads).then((filtered) => Response.json(filtered, { headers: ctx.corsHeaders }));
27854
27924
  }
27855
27925
  if (url.pathname === "/api/translations") {
27856
27926
  const config = readTranslationsConfig(ws.rootDir);
@@ -27905,13 +27975,13 @@ async function startPlatformServer(opts) {
27905
27975
  const moduleAccessMap = opts.moduleAccess ?? new Map;
27906
27976
  let manifest = opts.manifest;
27907
27977
  const getManifest = () => manifest;
27908
- const shellHtml = generateShellHtml(ws.appName, ws.manifest);
27978
+ const shellHtml = generateShellHtml(ws.appName, ws.manifest, opts.arcEntries);
27909
27979
  const sseClients = new Set;
27910
27980
  if (!context) {
27911
27981
  const cors = {
27912
27982
  "Access-Control-Allow-Origin": "*",
27913
27983
  "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, PATCH, OPTIONS",
27914
- "Access-Control-Allow-Headers": "Content-Type, Authorization"
27984
+ "Access-Control-Allow-Headers": "Content-Type, Authorization, X-Arc-Tokens"
27915
27985
  };
27916
27986
  const server = Bun.serve({
27917
27987
  port,
@@ -28014,6 +28084,7 @@ async function platformDev() {
28014
28084
  } else {
28015
28085
  log2("No context \u2014 server endpoints skipped");
28016
28086
  }
28087
+ const arcEntries = collectArcPeerDeps(ws.packages);
28017
28088
  const platform3 = await startPlatformServer({
28018
28089
  ws,
28019
28090
  port,
@@ -28021,7 +28092,8 @@ async function platformDev() {
28021
28092
  context,
28022
28093
  moduleAccess,
28023
28094
  dbPath: join9(ws.rootDir, ".arc", "data", "dev.db"),
28024
- devMode: true
28095
+ devMode: true,
28096
+ arcEntries
28025
28097
  });
28026
28098
  ok(`Server on http://localhost:${port}`);
28027
28099
  if (platform3.contextHandler)
@@ -28105,6 +28177,7 @@ async function platformStart() {
28105
28177
  } else {
28106
28178
  log2("No context \u2014 server endpoints skipped");
28107
28179
  }
28180
+ const arcEntries = collectArcPeerDeps(ws.packages);
28108
28181
  const platform3 = await startPlatformServer({
28109
28182
  ws,
28110
28183
  port,
@@ -28112,7 +28185,8 @@ async function platformStart() {
28112
28185
  context,
28113
28186
  moduleAccess,
28114
28187
  dbPath: join10(ws.rootDir, ".arc", "data", "prod.db"),
28115
- devMode: false
28188
+ devMode: false,
28189
+ arcEntries
28116
28190
  });
28117
28191
  ok(`Server on http://localhost:${port}`);
28118
28192
  if (platform3.contextHandler)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arcote.tech/arc-cli",
3
- "version": "0.4.7",
3
+ "version": "0.4.9",
4
4
  "description": "CLI tool for Arc framework",
5
5
  "module": "index.ts",
6
6
  "main": "dist/index.js",
@@ -6,6 +6,7 @@ import {
6
6
  buildAll,
7
7
  buildPackages,
8
8
  buildStyles,
9
+ collectArcPeerDeps,
9
10
  loadServerContext,
10
11
  log,
11
12
  ok,
@@ -29,6 +30,7 @@ export async function platformDev(): Promise<void> {
29
30
  }
30
31
 
31
32
  // Start server (dev mode = SSE reload + no-cache)
33
+ const arcEntries = collectArcPeerDeps(ws.packages);
32
34
  const platform = await startPlatformServer({
33
35
  ws,
34
36
  port,
@@ -37,6 +39,7 @@ export async function platformDev(): Promise<void> {
37
39
  moduleAccess,
38
40
  dbPath: join(ws.rootDir, ".arc", "data", "dev.db"),
39
41
  devMode: true,
42
+ arcEntries,
40
43
  });
41
44
 
42
45
  ok(`Server on http://localhost:${port}`);
@@ -2,6 +2,7 @@ import { existsSync, readFileSync } from "fs";
2
2
  import { join } from "path";
3
3
  import { startPlatformServer } from "../platform/server";
4
4
  import {
5
+ collectArcPeerDeps,
5
6
  err,
6
7
  loadServerContext,
7
8
  log,
@@ -34,6 +35,7 @@ export async function platformStart(): Promise<void> {
34
35
  }
35
36
 
36
37
  // Start server (production mode = no SSE reload, aggressive caching)
38
+ const arcEntries = collectArcPeerDeps(ws.packages);
37
39
  const platform = await startPlatformServer({
38
40
  ws,
39
41
  port,
@@ -42,6 +44,7 @@ export async function platformStart(): Promise<void> {
42
44
  moduleAccess,
43
45
  dbPath: join(ws.rootDir, ".arc", "data", "prod.db"),
44
46
  devMode: false,
47
+ arcEntries,
45
48
  });
46
49
 
47
50
  ok(`Server on http://localhost:${port}`);
@@ -30,6 +30,8 @@ export interface PlatformServerOptions {
30
30
  dbPath?: string;
31
31
  /** If true, enables SSE reload stream + mutable manifest (dev mode) */
32
32
  devMode?: boolean;
33
+ /** Arc shell entries [shortName, fullPkgName][] for import map. Auto-detected if omitted. */
34
+ arcEntries?: [string, string][];
33
35
  }
34
36
 
35
37
  export interface PlatformServer {
@@ -69,7 +71,17 @@ export async function initContextHandler(
69
71
  // Shell HTML
70
72
  // ---------------------------------------------------------------------------
71
73
 
72
- export function generateShellHtml(appName: string, manifest?: { title: string; favicon?: string }): string {
74
+ export function generateShellHtml(
75
+ appName: string,
76
+ manifest?: { title: string; favicon?: string },
77
+ arcEntries?: [string, string][],
78
+ ): string {
79
+ const arcImports: Record<string, string> = {};
80
+ if (arcEntries) {
81
+ for (const [short, pkg] of arcEntries) {
82
+ arcImports[pkg] = `/shell/${short}.js`;
83
+ }
84
+ }
73
85
  const importMap = {
74
86
  imports: {
75
87
  react: "/shell/react.js",
@@ -77,13 +89,7 @@ export function generateShellHtml(appName: string, manifest?: { title: string; f
77
89
  "react/jsx-dev-runtime": "/shell/jsx-dev-runtime.js",
78
90
  "react-dom": "/shell/react-dom.js",
79
91
  "react-dom/client": "/shell/react-dom-client.js",
80
- "@arcote.tech/arc": "/shell/arc.js",
81
- "@arcote.tech/arc-ds": "/shell/arc-ds.js",
82
- "@arcote.tech/arc-react": "/shell/arc-react.js",
83
- "@arcote.tech/arc-auth": "/shell/arc-auth.js",
84
- "@arcote.tech/arc-utils": "/shell/arc-utils.js",
85
- "@arcote.tech/arc-workspace": "/shell/arc-workspace.js",
86
- "@arcote.tech/platform": "/shell/platform.js",
92
+ ...arcImports,
87
93
  },
88
94
  };
89
95
 
@@ -170,30 +176,73 @@ function verifyModuleSignature(filename: string, sig: string | null, exp: string
170
176
  return hasher.digest("hex").slice(0, 16) === sig;
171
177
  }
172
178
 
173
- async function filterManifestForToken(
179
+ /** Decode JWT payload without verification (for module manifest filtering).
180
+ * Normalizes tokenName → tokenType to match TokenPayload interface. */
181
+ function decodeTokenPayload(jwt: string): any | null {
182
+ try {
183
+ const parts = jwt.split(".");
184
+ if (parts.length !== 3) return null;
185
+ const payload = JSON.parse(atob(parts[1]));
186
+ return {
187
+ tokenType: payload.tokenName ?? payload.tokenType,
188
+ params: payload.params || {},
189
+ iat: payload.iat,
190
+ exp: payload.exp,
191
+ };
192
+ } catch {
193
+ return null;
194
+ }
195
+ }
196
+
197
+ /** Parse X-Arc-Tokens header: "scope1:jwt1,scope2:jwt2" → decoded payloads array */
198
+ function parseArcTokensHeader(header: string | null): any[] {
199
+ if (!header) return [];
200
+ const payloads: any[] = [];
201
+ for (const entry of header.split(",")) {
202
+ const colonIdx = entry.indexOf(":");
203
+ if (colonIdx < 0) continue;
204
+ const jwt = entry.slice(colonIdx + 1);
205
+ const payload = decodeTokenPayload(jwt);
206
+ if (payload) payloads.push(payload);
207
+ }
208
+ return payloads;
209
+ }
210
+
211
+ async function filterManifestForTokens(
174
212
  manifest: BuildManifest,
175
213
  moduleAccessMap: Map<string, ModuleAccess>,
176
- tokenPayload: any,
214
+ tokenPayloads: any[],
177
215
  ): Promise<BuildManifest> {
178
216
  const filtered: ModuleEntry[] = [];
179
217
 
218
+ console.log(`[arc:modules] Filtering ${manifest.modules.length} modules with ${tokenPayloads.length} token(s):`,
219
+ tokenPayloads.map(t => `${t.tokenType}(${JSON.stringify(t.params)})`).join(", ") || "none");
220
+ console.log(`[arc:modules] Protected modules:`, [...moduleAccessMap.keys()].join(", ") || "none");
221
+
180
222
  for (const mod of manifest.modules) {
181
223
  const access = moduleAccessMap.get(mod.name);
182
224
 
183
225
  if (!access) {
184
- // Public module — always include
185
226
  filtered.push(mod);
186
227
  continue;
187
228
  }
188
229
 
189
- // Protected module check if token grants access
190
- if (!tokenPayload) continue;
230
+ if (tokenPayloads.length === 0) {
231
+ console.log(`[arc:modules] ${mod.name}: SKIP (no tokens)`);
232
+ continue;
233
+ }
191
234
 
192
235
  let granted = false;
193
236
  for (const rule of access.rules) {
194
- if (tokenPayload.tokenType === rule.token.name) {
195
- granted = rule.check ? await rule.check(tokenPayload) : true;
237
+ const matching = tokenPayloads.find(
238
+ (t) => t.tokenType === rule.token.name,
239
+ );
240
+ if (matching) {
241
+ granted = rule.check ? await rule.check(matching) : true;
242
+ console.log(`[arc:modules] ${mod.name}: rule ${rule.token.name} matched token, check=${granted}`);
196
243
  if (granted) break;
244
+ } else {
245
+ console.log(`[arc:modules] ${mod.name}: rule needs "${rule.token.name}", no matching token (have: ${tokenPayloads.map(t => t.tokenType).join(",")})`);
197
246
  }
198
247
  }
199
248
 
@@ -202,6 +251,7 @@ async function filterManifestForToken(
202
251
  }
203
252
  }
204
253
 
254
+ console.log(`[arc:modules] Result: ${filtered.map(m => m.name).join(", ")}`);
205
255
  return { modules: filtered, buildTime: manifest.buildTime };
206
256
  }
207
257
 
@@ -266,10 +316,16 @@ function apiEndpointsHandler(
266
316
  cm: ConnectionManager | null,
267
317
  moduleAccessMap: Map<string, ModuleAccess>,
268
318
  ): ArcHttpHandler {
269
- return (_req, url, ctx) => {
319
+ return (req, url, ctx) => {
270
320
  if (url.pathname === "/api/modules") {
271
- // Filter manifest based on token protected modules only for authorized users
272
- return filterManifestForToken(getManifest(), moduleAccessMap, ctx.tokenPayload)
321
+ // Parse all tokens from X-Arc-Tokens header + Authorization fallback
322
+ const arcTokensHeader = req.headers.get("X-Arc-Tokens");
323
+ let payloads = parseArcTokensHeader(arcTokensHeader);
324
+ // Fallback: if no X-Arc-Tokens, use the single token from Authorization
325
+ if (payloads.length === 0 && ctx.tokenPayload) {
326
+ payloads = [ctx.tokenPayload];
327
+ }
328
+ return filterManifestForTokens(getManifest(), moduleAccessMap, payloads)
273
329
  .then((filtered) => Response.json(filtered, { headers: ctx.corsHeaders }));
274
330
  }
275
331
 
@@ -342,7 +398,7 @@ export async function startPlatformServer(
342
398
  let manifest = opts.manifest;
343
399
  const getManifest = () => manifest;
344
400
 
345
- const shellHtml = generateShellHtml(ws.appName, ws.manifest);
401
+ const shellHtml = generateShellHtml(ws.appName, ws.manifest, opts.arcEntries);
346
402
  const sseClients = new Set<ReadableStreamDefaultController>();
347
403
 
348
404
  if (!context) {
@@ -351,7 +407,7 @@ export async function startPlatformServer(
351
407
  "Access-Control-Allow-Origin": "*",
352
408
  "Access-Control-Allow-Methods":
353
409
  "GET, POST, PUT, DELETE, PATCH, OPTIONS",
354
- "Access-Control-Allow-Headers": "Content-Type, Authorization",
410
+ "Access-Control-Allow-Headers": "Content-Type, Authorization, X-Arc-Tokens",
355
411
  };
356
412
 
357
413
  const server = Bun.serve({
@@ -135,7 +135,7 @@ export async function buildAll(ws: WorkspaceInfo): Promise<BuildManifest> {
135
135
  }
136
136
 
137
137
  log("Building shell...");
138
- await buildShell(ws.shellDir);
138
+ await buildShell(ws.shellDir, ws.packages);
139
139
  ok("Shell built");
140
140
 
141
141
  return manifest;
@@ -145,7 +145,30 @@ export async function buildAll(ws: WorkspaceInfo): Promise<BuildManifest> {
145
145
  // Shell builder — framework packages for import map
146
146
  // ---------------------------------------------------------------------------
147
147
 
148
- async function buildShell(outDir: string): Promise<void> {
148
+ /** Collect all @arcote.tech/* peerDependencies from workspace packages. */
149
+ export function collectArcPeerDeps(packages: WorkspacePackage[]): [string, string][] {
150
+ const seen = new Set<string>();
151
+ // Always include core framework packages
152
+ for (const pkg of ["@arcote.tech/arc", "@arcote.tech/arc-ds", "@arcote.tech/arc-react", "@arcote.tech/platform"]) {
153
+ seen.add(pkg);
154
+ }
155
+ // Scan all workspace packages for @arcote.tech/* peerDeps
156
+ for (const wp of packages) {
157
+ const peerDeps = wp.packageJson.peerDependencies ?? {};
158
+ for (const dep of Object.keys(peerDeps)) {
159
+ if (dep.startsWith("@arcote.tech/")) seen.add(dep);
160
+ }
161
+ }
162
+ // Convert to [shortName, fullName] entries
163
+ return [...seen].map((pkg) => {
164
+ const short = pkg === "@arcote.tech/platform"
165
+ ? "platform"
166
+ : pkg.replace("@arcote.tech/", "");
167
+ return [short, pkg];
168
+ });
169
+ }
170
+
171
+ async function buildShell(outDir: string, packages?: WorkspacePackage[]): Promise<void> {
149
172
  mkdirSync(outDir, { recursive: true });
150
173
  const tmpDir = join(outDir, "_tmp");
151
174
  mkdirSync(tmpDir, { recursive: true });
@@ -203,15 +226,16 @@ export const { createPortal, flushSync } = ReactDOM;`,
203
226
  }
204
227
 
205
228
  // Step 2: Build Arc layer (react is EXTERNAL — resolved via import map)
206
- const arcEntries: [string, string][] = [
207
- ["arc", "@arcote.tech/arc"],
208
- ["arc-ds", "@arcote.tech/arc-ds"],
209
- ["arc-react", "@arcote.tech/arc-react"],
210
- ["arc-auth", "@arcote.tech/arc-auth"],
211
- ["arc-utils", "@arcote.tech/arc-utils"],
212
- ["arc-workspace", "@arcote.tech/arc-workspace"],
213
- ["platform", "@arcote.tech/platform"],
214
- ];
229
+ // Dynamically collect only @arcote.tech/* packages actually used by workspace
230
+ const arcEntries = packages
231
+ ? collectArcPeerDeps(packages)
232
+ : [
233
+ // Fallback: core packages only (no workspace packages available)
234
+ ["arc", "@arcote.tech/arc"],
235
+ ["arc-ds", "@arcote.tech/arc-ds"],
236
+ ["arc-react", "@arcote.tech/arc-react"],
237
+ ["platform", "@arcote.tech/platform"],
238
+ ] as [string, string][];
215
239
 
216
240
  const baseExternal = [
217
241
  "react",