@arcote.tech/arc-cli 0.4.6 → 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 +577 -2234
- package/package.json +1 -1
- package/src/builder/module-builder.ts +17 -4
- package/src/commands/platform-dev.ts +5 -1
- package/src/commands/platform-start.ts +5 -1
- package/src/i18n/catalog.ts +43 -5
- package/src/i18n/compile.ts +6 -1
- package/src/platform/server.ts +161 -21
- package/src/platform/shared.ts +87 -41
package/package.json
CHANGED
|
@@ -97,8 +97,13 @@ export interface WorkspacePackage {
|
|
|
97
97
|
packageJson: Record<string, any>;
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
export interface ModuleEntry {
|
|
101
|
+
file: string;
|
|
102
|
+
name: string;
|
|
103
|
+
}
|
|
104
|
+
|
|
100
105
|
export interface BuildManifest {
|
|
101
|
-
modules:
|
|
106
|
+
modules: ModuleEntry[];
|
|
102
107
|
buildTime: string;
|
|
103
108
|
}
|
|
104
109
|
|
|
@@ -203,9 +208,13 @@ export async function buildPackages(
|
|
|
203
208
|
mkdirSync(tmpDir, { recursive: true });
|
|
204
209
|
|
|
205
210
|
const entrypoints: string[] = [];
|
|
211
|
+
const fileToName = new Map<string, string>(); // safeName → module name
|
|
206
212
|
|
|
207
213
|
for (const pkg of packages) {
|
|
208
214
|
const safeName = pkg.path.split("/").pop()!;
|
|
215
|
+
// Module name: strip scope (e.g. @ndt/admin → admin)
|
|
216
|
+
const moduleName = pkg.name.includes("/") ? pkg.name.split("/").pop()! : pkg.name;
|
|
217
|
+
fileToName.set(safeName, moduleName);
|
|
209
218
|
|
|
210
219
|
// All packages get a simple re-export wrapper.
|
|
211
220
|
// Context packages that use module().build() self-register via side effects.
|
|
@@ -262,12 +271,16 @@ export async function buildPackages(
|
|
|
262
271
|
rmSync(tmpDir, { recursive: true, force: true });
|
|
263
272
|
|
|
264
273
|
// Build manifest
|
|
265
|
-
const
|
|
274
|
+
const moduleEntries: ModuleEntry[] = result.outputs
|
|
266
275
|
.filter((o) => o.kind === "entry-point")
|
|
267
|
-
.map((o) =>
|
|
276
|
+
.map((o) => {
|
|
277
|
+
const file = o.path.split("/").pop()!;
|
|
278
|
+
const safeName = file.replace(/\.js$/, "");
|
|
279
|
+
return { file, name: fileToName.get(safeName) ?? safeName };
|
|
280
|
+
});
|
|
268
281
|
|
|
269
282
|
const manifest: BuildManifest = {
|
|
270
|
-
modules:
|
|
283
|
+
modules: moduleEntries,
|
|
271
284
|
buildTime: new Date().toISOString(),
|
|
272
285
|
};
|
|
273
286
|
|
|
@@ -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,
|
|
@@ -21,7 +22,7 @@ export async function platformDev(): Promise<void> {
|
|
|
21
22
|
|
|
22
23
|
// Load server context
|
|
23
24
|
log("Loading server context...");
|
|
24
|
-
const context = await loadServerContext(ws.packages);
|
|
25
|
+
const { context, moduleAccess } = await loadServerContext(ws.packages);
|
|
25
26
|
if (context) {
|
|
26
27
|
ok("Context loaded");
|
|
27
28
|
} else {
|
|
@@ -29,13 +30,16 @@ 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,
|
|
35
37
|
manifest,
|
|
36
38
|
context,
|
|
39
|
+
moduleAccess,
|
|
37
40
|
dbPath: join(ws.rootDir, ".arc", "data", "dev.db"),
|
|
38
41
|
devMode: true,
|
|
42
|
+
arcEntries,
|
|
39
43
|
});
|
|
40
44
|
|
|
41
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,
|
|
@@ -26,7 +27,7 @@ export async function platformStart(): Promise<void> {
|
|
|
26
27
|
|
|
27
28
|
// Load server context
|
|
28
29
|
log("Loading server context...");
|
|
29
|
-
const context = await loadServerContext(ws.packages);
|
|
30
|
+
const { context, moduleAccess } = await loadServerContext(ws.packages);
|
|
30
31
|
if (context) {
|
|
31
32
|
ok("Context loaded");
|
|
32
33
|
} else {
|
|
@@ -34,13 +35,16 @@ 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,
|
|
40
42
|
manifest,
|
|
41
43
|
context,
|
|
44
|
+
moduleAccess,
|
|
42
45
|
dbPath: join(ws.rootDir, ".arc", "data", "prod.db"),
|
|
43
46
|
devMode: false,
|
|
47
|
+
arcEntries,
|
|
44
48
|
});
|
|
45
49
|
|
|
46
50
|
ok(`Server on http://localhost:${port}`);
|
package/src/i18n/catalog.ts
CHANGED
|
@@ -18,6 +18,7 @@ export interface CatalogEntry {
|
|
|
18
18
|
locations: string[];
|
|
19
19
|
hash: string;
|
|
20
20
|
obsolete: boolean;
|
|
21
|
+
manual?: boolean;
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
/** Short hash from msgid for change tracking */
|
|
@@ -37,6 +38,7 @@ export function parsePo(content: string): CatalogEntry[] {
|
|
|
37
38
|
let msgid = "";
|
|
38
39
|
let msgstr = "";
|
|
39
40
|
let obsolete = false;
|
|
41
|
+
let inManual = false;
|
|
40
42
|
|
|
41
43
|
const flush = () => {
|
|
42
44
|
if (msgid) {
|
|
@@ -46,6 +48,7 @@ export function parsePo(content: string): CatalogEntry[] {
|
|
|
46
48
|
locations,
|
|
47
49
|
hash: hash || hashMsgid(msgid),
|
|
48
50
|
obsolete,
|
|
51
|
+
...(inManual ? { manual: true } : {}),
|
|
49
52
|
});
|
|
50
53
|
}
|
|
51
54
|
locations = [];
|
|
@@ -58,6 +61,17 @@ export function parsePo(content: string): CatalogEntry[] {
|
|
|
58
61
|
for (const line of lines) {
|
|
59
62
|
const trimmed = line.trim();
|
|
60
63
|
|
|
64
|
+
// Manual section markers
|
|
65
|
+
if (trimmed === "# manual") {
|
|
66
|
+
inManual = true;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (trimmed === "# end manual") {
|
|
70
|
+
if (msgid) flush();
|
|
71
|
+
inManual = false;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
|
|
61
75
|
if (trimmed === "" || trimmed.startsWith("#,")) {
|
|
62
76
|
// Empty line or flags — boundary between entries
|
|
63
77
|
if (msgid) flush();
|
|
@@ -110,10 +124,23 @@ export function parsePo(content: string): CatalogEntry[] {
|
|
|
110
124
|
export function writePo(entries: CatalogEntry[]): string {
|
|
111
125
|
const lines: string[] = [];
|
|
112
126
|
|
|
113
|
-
|
|
114
|
-
const active = entries.filter((e) => !e.obsolete);
|
|
127
|
+
const manual = entries.filter((e) => e.manual);
|
|
128
|
+
const active = entries.filter((e) => !e.obsolete && !e.manual);
|
|
115
129
|
const obsolete = entries.filter((e) => e.obsolete);
|
|
116
130
|
|
|
131
|
+
// Manual section
|
|
132
|
+
if (manual.length > 0) {
|
|
133
|
+
lines.push("# manual");
|
|
134
|
+
for (const entry of manual) {
|
|
135
|
+
lines.push(`msgid ${quoteString(entry.msgid)}`);
|
|
136
|
+
lines.push(`msgstr ${quoteString(entry.msgstr)}`);
|
|
137
|
+
lines.push("");
|
|
138
|
+
}
|
|
139
|
+
lines.push("# end manual");
|
|
140
|
+
lines.push("");
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Extracted entries
|
|
117
144
|
for (const entry of active) {
|
|
118
145
|
for (const loc of entry.locations) {
|
|
119
146
|
lines.push(`#: ${loc}`);
|
|
@@ -124,6 +151,7 @@ export function writePo(entries: CatalogEntry[]): string {
|
|
|
124
151
|
lines.push("");
|
|
125
152
|
}
|
|
126
153
|
|
|
154
|
+
// Obsolete entries
|
|
127
155
|
if (obsolete.length > 0) {
|
|
128
156
|
lines.push("# Obsolete entries");
|
|
129
157
|
lines.push("");
|
|
@@ -152,12 +180,18 @@ export function mergeCatalog(
|
|
|
152
180
|
existingMap.set(entry.msgid, entry);
|
|
153
181
|
}
|
|
154
182
|
|
|
183
|
+
// Preserve manual entries as-is
|
|
184
|
+
const manualEntries = existing.filter((e) => e.manual);
|
|
185
|
+
const manualIds = new Set(manualEntries.map((e) => e.msgid));
|
|
186
|
+
|
|
155
187
|
const result: CatalogEntry[] = [];
|
|
156
188
|
const seen = new Set<string>();
|
|
157
189
|
|
|
158
|
-
// Process all extracted messages
|
|
190
|
+
// Process all extracted messages (skip those in manual section)
|
|
159
191
|
for (const [msgid, locations] of extracted) {
|
|
160
192
|
seen.add(msgid);
|
|
193
|
+
if (manualIds.has(msgid)) continue;
|
|
194
|
+
|
|
161
195
|
const prev = existingMap.get(msgid);
|
|
162
196
|
|
|
163
197
|
result.push({
|
|
@@ -171,7 +205,7 @@ export function mergeCatalog(
|
|
|
171
205
|
|
|
172
206
|
// Mark removed entries as obsolete (keep their translations)
|
|
173
207
|
for (const entry of existing) {
|
|
174
|
-
if (!seen.has(entry.msgid) && !entry.obsolete && entry.msgstr) {
|
|
208
|
+
if (!seen.has(entry.msgid) && !entry.obsolete && !entry.manual && entry.msgstr) {
|
|
175
209
|
result.push({
|
|
176
210
|
...entry,
|
|
177
211
|
obsolete: true,
|
|
@@ -180,7 +214,11 @@ export function mergeCatalog(
|
|
|
180
214
|
}
|
|
181
215
|
}
|
|
182
216
|
|
|
183
|
-
|
|
217
|
+
// Sort extracted entries alphabetically for deterministic output
|
|
218
|
+
result.sort((a, b) => a.msgid.localeCompare(b.msgid));
|
|
219
|
+
|
|
220
|
+
// Manual entries first, then sorted extracted, then obsolete
|
|
221
|
+
return [...manualEntries, ...result];
|
|
184
222
|
}
|
|
185
223
|
|
|
186
224
|
function extractQuoted(s: string): string {
|
package/src/i18n/compile.ts
CHANGED
|
@@ -18,7 +18,12 @@ export function compileCatalog(poPath: string): Record<string, string> {
|
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
// Sort keys for deterministic JSON output
|
|
22
|
+
const sorted: Record<string, string> = {};
|
|
23
|
+
for (const key of Object.keys(result).sort()) {
|
|
24
|
+
sorted[key] = result[key];
|
|
25
|
+
}
|
|
26
|
+
return sorted;
|
|
22
27
|
}
|
|
23
28
|
|
|
24
29
|
/**
|
package/src/platform/server.ts
CHANGED
|
@@ -11,7 +11,8 @@ import {
|
|
|
11
11
|
import { existsSync, mkdirSync } from "fs";
|
|
12
12
|
import { join } from "path";
|
|
13
13
|
import { readTranslationsConfig } from "../i18n";
|
|
14
|
-
import type { BuildManifest, WorkspaceInfo } from "./shared";
|
|
14
|
+
import type { BuildManifest, ModuleEntry, WorkspaceInfo } from "./shared";
|
|
15
|
+
import type { ModuleAccess } from "@arcote.tech/platform";
|
|
15
16
|
|
|
16
17
|
// ---------------------------------------------------------------------------
|
|
17
18
|
// Types
|
|
@@ -23,10 +24,14 @@ export interface PlatformServerOptions {
|
|
|
23
24
|
manifest: BuildManifest;
|
|
24
25
|
/** Server context (from loadServerContext). If null, static-only mode. */
|
|
25
26
|
context?: any;
|
|
27
|
+
/** Module access rules (from registry after loadServerContext). */
|
|
28
|
+
moduleAccess?: Map<string, ModuleAccess>;
|
|
26
29
|
/** Path to SQLite database file */
|
|
27
30
|
dbPath?: string;
|
|
28
31
|
/** If true, enables SSE reload stream + mutable manifest (dev mode) */
|
|
29
32
|
devMode?: boolean;
|
|
33
|
+
/** Arc shell entries [shortName, fullPkgName][] for import map. Auto-detected if omitted. */
|
|
34
|
+
arcEntries?: [string, string][];
|
|
30
35
|
}
|
|
31
36
|
|
|
32
37
|
export interface PlatformServer {
|
|
@@ -66,7 +71,17 @@ export async function initContextHandler(
|
|
|
66
71
|
// Shell HTML
|
|
67
72
|
// ---------------------------------------------------------------------------
|
|
68
73
|
|
|
69
|
-
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
|
+
}
|
|
70
85
|
const importMap = {
|
|
71
86
|
imports: {
|
|
72
87
|
react: "/shell/react.js",
|
|
@@ -74,13 +89,7 @@ export function generateShellHtml(appName: string, manifest?: { title: string; f
|
|
|
74
89
|
"react/jsx-dev-runtime": "/shell/jsx-dev-runtime.js",
|
|
75
90
|
"react-dom": "/shell/react-dom.js",
|
|
76
91
|
"react-dom/client": "/shell/react-dom-client.js",
|
|
77
|
-
|
|
78
|
-
"@arcote.tech/arc-ds": "/shell/arc-ds.js",
|
|
79
|
-
"@arcote.tech/arc-react": "/shell/arc-react.js",
|
|
80
|
-
"@arcote.tech/arc-auth": "/shell/arc-auth.js",
|
|
81
|
-
"@arcote.tech/arc-utils": "/shell/arc-utils.js",
|
|
82
|
-
"@arcote.tech/arc-workspace": "/shell/arc-workspace.js",
|
|
83
|
-
"@arcote.tech/platform": "/shell/platform.js",
|
|
92
|
+
...arcImports,
|
|
84
93
|
},
|
|
85
94
|
};
|
|
86
95
|
|
|
@@ -144,20 +153,140 @@ function serveFile(
|
|
|
144
153
|
});
|
|
145
154
|
}
|
|
146
155
|
|
|
156
|
+
// ---------------------------------------------------------------------------
|
|
157
|
+
// Module access — signed URLs
|
|
158
|
+
// ---------------------------------------------------------------------------
|
|
159
|
+
|
|
160
|
+
const MODULE_SIG_SECRET = process.env.ARC_MODULE_SECRET ?? crypto.randomUUID();
|
|
161
|
+
const MODULE_SIG_TTL = 3600; // 1 hour
|
|
162
|
+
|
|
163
|
+
function signModuleUrl(filename: string): string {
|
|
164
|
+
const exp = Math.floor(Date.now() / 1000) + MODULE_SIG_TTL;
|
|
165
|
+
const hasher = new Bun.CryptoHasher("sha256");
|
|
166
|
+
hasher.update(`${filename}:${exp}:${MODULE_SIG_SECRET}`);
|
|
167
|
+
const sig = hasher.digest("hex").slice(0, 16);
|
|
168
|
+
return `/modules/${filename}?sig=${sig}&exp=${exp}`;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function verifyModuleSignature(filename: string, sig: string | null, exp: string | null): boolean {
|
|
172
|
+
if (!sig || !exp) return false;
|
|
173
|
+
if (Number(exp) < Date.now() / 1000) return false;
|
|
174
|
+
const hasher = new Bun.CryptoHasher("sha256");
|
|
175
|
+
hasher.update(`${filename}:${exp}:${MODULE_SIG_SECRET}`);
|
|
176
|
+
return hasher.digest("hex").slice(0, 16) === sig;
|
|
177
|
+
}
|
|
178
|
+
|
|
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(
|
|
212
|
+
manifest: BuildManifest,
|
|
213
|
+
moduleAccessMap: Map<string, ModuleAccess>,
|
|
214
|
+
tokenPayloads: any[],
|
|
215
|
+
): Promise<BuildManifest> {
|
|
216
|
+
const filtered: ModuleEntry[] = [];
|
|
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
|
+
|
|
222
|
+
for (const mod of manifest.modules) {
|
|
223
|
+
const access = moduleAccessMap.get(mod.name);
|
|
224
|
+
|
|
225
|
+
if (!access) {
|
|
226
|
+
filtered.push(mod);
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (tokenPayloads.length === 0) {
|
|
231
|
+
console.log(`[arc:modules] ${mod.name}: SKIP (no tokens)`);
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
let granted = false;
|
|
236
|
+
for (const rule of access.rules) {
|
|
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}`);
|
|
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(",")})`);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (granted) {
|
|
250
|
+
filtered.push({ ...mod, url: signModuleUrl(mod.file) } as any);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
console.log(`[arc:modules] Result: ${filtered.map(m => m.name).join(", ")}`);
|
|
255
|
+
return { modules: filtered, buildTime: manifest.buildTime };
|
|
256
|
+
}
|
|
257
|
+
|
|
147
258
|
// ---------------------------------------------------------------------------
|
|
148
259
|
// Platform-specific HTTP handlers
|
|
149
260
|
// ---------------------------------------------------------------------------
|
|
150
261
|
|
|
151
|
-
function staticFilesHandler(
|
|
262
|
+
function staticFilesHandler(
|
|
263
|
+
ws: WorkspaceInfo,
|
|
264
|
+
devMode: boolean,
|
|
265
|
+
moduleAccessMap: Map<string, ModuleAccess>,
|
|
266
|
+
): ArcHttpHandler {
|
|
152
267
|
return (_req, url, ctx) => {
|
|
153
268
|
const path = url.pathname;
|
|
154
269
|
if (path.startsWith("/shell/"))
|
|
155
270
|
return serveFile(join(ws.shellDir, path.slice(7)), ctx.corsHeaders);
|
|
156
|
-
if (path.startsWith("/modules/"))
|
|
157
|
-
|
|
271
|
+
if (path.startsWith("/modules/")) {
|
|
272
|
+
const fileWithParams = path.slice(9);
|
|
273
|
+
const filename = fileWithParams.split("?")[0];
|
|
274
|
+
const moduleName = filename.replace(/\.js$/, "");
|
|
275
|
+
|
|
276
|
+
// Check access for protected modules
|
|
277
|
+
if (moduleAccessMap.has(moduleName)) {
|
|
278
|
+
const sig = url.searchParams.get("sig");
|
|
279
|
+
const exp = url.searchParams.get("exp");
|
|
280
|
+
if (!verifyModuleSignature(filename, sig, exp)) {
|
|
281
|
+
return new Response("Forbidden", { status: 403, headers: ctx.corsHeaders });
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return serveFile(join(ws.modulesDir, filename), {
|
|
158
286
|
...ctx.corsHeaders,
|
|
159
287
|
"Cache-Control": devMode ? "no-cache" : "max-age=31536000,immutable",
|
|
160
288
|
});
|
|
289
|
+
}
|
|
161
290
|
if (path.startsWith("/locales/"))
|
|
162
291
|
return serveFile(join(ws.arcDir, path.slice(1)), ctx.corsHeaders);
|
|
163
292
|
if (path === "/styles.css")
|
|
@@ -185,10 +314,20 @@ function apiEndpointsHandler(
|
|
|
185
314
|
ws: WorkspaceInfo,
|
|
186
315
|
getManifest: () => BuildManifest,
|
|
187
316
|
cm: ConnectionManager | null,
|
|
317
|
+
moduleAccessMap: Map<string, ModuleAccess>,
|
|
188
318
|
): ArcHttpHandler {
|
|
189
|
-
return (
|
|
190
|
-
if (url.pathname === "/api/modules")
|
|
191
|
-
|
|
319
|
+
return (req, url, ctx) => {
|
|
320
|
+
if (url.pathname === "/api/modules") {
|
|
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)
|
|
329
|
+
.then((filtered) => Response.json(filtered, { headers: ctx.corsHeaders }));
|
|
330
|
+
}
|
|
192
331
|
|
|
193
332
|
if (url.pathname === "/api/translations") {
|
|
194
333
|
const config = readTranslationsConfig(ws.rootDir);
|
|
@@ -255,10 +394,11 @@ export async function startPlatformServer(
|
|
|
255
394
|
opts: PlatformServerOptions,
|
|
256
395
|
): Promise<PlatformServer> {
|
|
257
396
|
const { ws, port, devMode, context } = opts;
|
|
397
|
+
const moduleAccessMap = opts.moduleAccess ?? new Map();
|
|
258
398
|
let manifest = opts.manifest;
|
|
259
399
|
const getManifest = () => manifest;
|
|
260
400
|
|
|
261
|
-
const shellHtml = generateShellHtml(ws.appName, ws.manifest);
|
|
401
|
+
const shellHtml = generateShellHtml(ws.appName, ws.manifest, opts.arcEntries);
|
|
262
402
|
const sseClients = new Set<ReadableStreamDefaultController>();
|
|
263
403
|
|
|
264
404
|
if (!context) {
|
|
@@ -267,7 +407,7 @@ export async function startPlatformServer(
|
|
|
267
407
|
"Access-Control-Allow-Origin": "*",
|
|
268
408
|
"Access-Control-Allow-Methods":
|
|
269
409
|
"GET, POST, PUT, DELETE, PATCH, OPTIONS",
|
|
270
|
-
"Access-Control-Allow-Headers": "Content-Type, Authorization",
|
|
410
|
+
"Access-Control-Allow-Headers": "Content-Type, Authorization, X-Arc-Tokens",
|
|
271
411
|
};
|
|
272
412
|
|
|
273
413
|
const server = Bun.serve({
|
|
@@ -285,9 +425,9 @@ export async function startPlatformServer(
|
|
|
285
425
|
|
|
286
426
|
// Platform handlers only
|
|
287
427
|
const handlers: ArcHttpHandler[] = [
|
|
288
|
-
apiEndpointsHandler(ws, getManifest, null),
|
|
428
|
+
apiEndpointsHandler(ws, getManifest, null, moduleAccessMap),
|
|
289
429
|
...(devMode ? [devReloadHandler(sseClients)] : []),
|
|
290
|
-
staticFilesHandler(ws, !!devMode),
|
|
430
|
+
staticFilesHandler(ws, !!devMode, moduleAccessMap),
|
|
291
431
|
spaFallbackHandler(shellHtml),
|
|
292
432
|
];
|
|
293
433
|
|
|
@@ -331,9 +471,9 @@ export async function startPlatformServer(
|
|
|
331
471
|
port,
|
|
332
472
|
httpHandlers: [
|
|
333
473
|
// Platform-specific handlers (checked AFTER arc handlers)
|
|
334
|
-
apiEndpointsHandler(ws, getManifest, null),
|
|
474
|
+
apiEndpointsHandler(ws, getManifest, null, moduleAccessMap),
|
|
335
475
|
...(devMode ? [devReloadHandler(sseClients)] : []),
|
|
336
|
-
staticFilesHandler(ws, !!devMode),
|
|
476
|
+
staticFilesHandler(ws, !!devMode, moduleAccessMap),
|
|
337
477
|
spaFallbackHandler(shellHtml),
|
|
338
478
|
],
|
|
339
479
|
onWsClose: (clientId) => cleanupClientSubs(clientId),
|