@arcote.tech/arc-cli 0.4.7 → 0.4.8
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 +104 -30
- package/package.json +1 -1
- package/src/commands/platform-dev.ts +3 -0
- package/src/commands/platform-start.ts +3 -0
- package/src/platform/server.ts +76 -20
- package/src/platform/shared.ts +35 -11
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
|
-
//
|
|
17515
|
+
// node_modules/find-up/index.js
|
|
17505
17516
|
import path2 from "node:path";
|
|
17506
17517
|
|
|
17507
|
-
//
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
27801
|
-
|
|
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 (
|
|
27916
|
+
return (req, url, ctx) => {
|
|
27852
27917
|
if (url.pathname === "/api/modules") {
|
|
27853
|
-
|
|
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
|
@@ -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}`);
|
package/src/platform/server.ts
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
190
|
-
|
|
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
|
-
|
|
195
|
-
|
|
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 (
|
|
319
|
+
return (req, url, ctx) => {
|
|
270
320
|
if (url.pathname === "/api/modules") {
|
|
271
|
-
//
|
|
272
|
-
|
|
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({
|
package/src/platform/shared.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
[
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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",
|