@arcote.tech/arc-cli 0.7.2 → 0.7.3
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 +27 -19
- package/package.json +7 -7
- package/src/platform/server.ts +52 -26
package/dist/index.js
CHANGED
|
@@ -31486,11 +31486,12 @@ async function createArcServer(config) {
|
|
|
31486
31486
|
init_i18n();
|
|
31487
31487
|
import { existsSync as existsSync18, mkdirSync as mkdirSync14 } from "fs";
|
|
31488
31488
|
import { join as join20 } from "path";
|
|
31489
|
-
function generateShellHtml(appName, manifest, initial) {
|
|
31489
|
+
function generateShellHtml(appName, manifest, initial, stylesHash) {
|
|
31490
31490
|
const initialUrl = initial ? `/browser/${initial.file}` : null;
|
|
31491
31491
|
if (!initialUrl) {
|
|
31492
31492
|
throw new Error("generateShellHtml: initial bundle missing from manifest");
|
|
31493
31493
|
}
|
|
31494
|
+
const stylesQs = stylesHash ? `?v=${stylesHash.slice(0, 16)}` : "";
|
|
31494
31495
|
return `<!doctype html>
|
|
31495
31496
|
<html lang="en">
|
|
31496
31497
|
<head>
|
|
@@ -31499,8 +31500,8 @@ function generateShellHtml(appName, manifest, initial) {
|
|
|
31499
31500
|
<title>${manifest?.title ?? appName}</title>${manifest?.favicon ? `
|
|
31500
31501
|
<link rel="icon" href="${manifest.favicon}">` : ""}${manifest ? `
|
|
31501
31502
|
<link rel="manifest" href="/manifest.json">` : ""}
|
|
31502
|
-
<link rel="stylesheet" href="/styles.css" />
|
|
31503
|
-
<link rel="stylesheet" href="/theme.css" />
|
|
31503
|
+
<link rel="stylesheet" href="/styles.css${stylesQs}" />
|
|
31504
|
+
<link rel="stylesheet" href="/theme.css${stylesQs}" />
|
|
31504
31505
|
<link rel="modulepreload" href="${initialUrl}" />
|
|
31505
31506
|
</head>
|
|
31506
31507
|
<body>
|
|
@@ -31539,7 +31540,6 @@ function serveFile(filePath, headers = {}) {
|
|
|
31539
31540
|
});
|
|
31540
31541
|
}
|
|
31541
31542
|
var MODULE_SIG_SECRET = process.env.ARC_MODULE_SECRET ?? "";
|
|
31542
|
-
var MODULE_SIG_TTL = 3600;
|
|
31543
31543
|
function ensureModuleSigSecret(ws, devMode) {
|
|
31544
31544
|
if (MODULE_SIG_SECRET)
|
|
31545
31545
|
return;
|
|
@@ -31552,19 +31552,16 @@ function ensureModuleSigSecret(ws, devMode) {
|
|
|
31552
31552
|
}
|
|
31553
31553
|
}
|
|
31554
31554
|
function signGroupUrl(file) {
|
|
31555
|
-
const exp = Math.floor(Date.now() / 1000) + MODULE_SIG_TTL;
|
|
31556
31555
|
const hasher = new Bun.CryptoHasher("sha256");
|
|
31557
|
-
hasher.update(`${file}:${
|
|
31556
|
+
hasher.update(`${file}:${MODULE_SIG_SECRET}`);
|
|
31558
31557
|
const sig = hasher.digest("hex").slice(0, 16);
|
|
31559
|
-
return `/browser/${file}?sig=${sig}
|
|
31558
|
+
return `/browser/${file}?sig=${sig}`;
|
|
31560
31559
|
}
|
|
31561
|
-
function verifyGroupSignature(file, sig
|
|
31562
|
-
if (!sig
|
|
31563
|
-
return false;
|
|
31564
|
-
if (Number(exp) < Date.now() / 1000)
|
|
31560
|
+
function verifyGroupSignature(file, sig) {
|
|
31561
|
+
if (!sig)
|
|
31565
31562
|
return false;
|
|
31566
31563
|
const hasher = new Bun.CryptoHasher("sha256");
|
|
31567
|
-
hasher.update(`${file}:${
|
|
31564
|
+
hasher.update(`${file}:${MODULE_SIG_SECRET}`);
|
|
31568
31565
|
return hasher.digest("hex").slice(0, 16) === sig;
|
|
31569
31566
|
}
|
|
31570
31567
|
function decodeTokenPayload(jwt2) {
|
|
@@ -31654,8 +31651,7 @@ function staticFilesHandler(ws, devMode, getManifest) {
|
|
|
31654
31651
|
const isGroupEntry = Object.values(manifest.groups).some((g3) => g3.file === file);
|
|
31655
31652
|
if (isGroupEntry) {
|
|
31656
31653
|
const sig = url.searchParams.get("sig");
|
|
31657
|
-
|
|
31658
|
-
if (!verifyGroupSignature(file, sig, exp)) {
|
|
31654
|
+
if (!verifyGroupSignature(file, sig)) {
|
|
31659
31655
|
return new Response("Forbidden", { status: 403, headers: ctx.corsHeaders });
|
|
31660
31656
|
}
|
|
31661
31657
|
}
|
|
@@ -31665,13 +31661,25 @@ function staticFilesHandler(ws, devMode, getManifest) {
|
|
|
31665
31661
|
});
|
|
31666
31662
|
}
|
|
31667
31663
|
if (path4.startsWith("/locales/"))
|
|
31668
|
-
return serveFile(join20(ws.arcDir, path4.slice(1)),
|
|
31664
|
+
return serveFile(join20(ws.arcDir, path4.slice(1)), {
|
|
31665
|
+
...ctx.corsHeaders,
|
|
31666
|
+
"Cache-Control": devMode ? "no-cache" : "max-age=300,stale-while-revalidate=3600"
|
|
31667
|
+
});
|
|
31669
31668
|
if (path4.startsWith("/assets/"))
|
|
31670
|
-
return serveFile(join20(ws.assetsDir, path4.slice(8)),
|
|
31669
|
+
return serveFile(join20(ws.assetsDir, path4.slice(8)), {
|
|
31670
|
+
...ctx.corsHeaders,
|
|
31671
|
+
"Cache-Control": devMode ? "no-cache" : "max-age=31536000,immutable"
|
|
31672
|
+
});
|
|
31671
31673
|
if (path4 === "/styles.css")
|
|
31672
|
-
return serveFile(join20(ws.arcDir, "styles.css"),
|
|
31674
|
+
return serveFile(join20(ws.arcDir, "styles.css"), {
|
|
31675
|
+
...ctx.corsHeaders,
|
|
31676
|
+
"Cache-Control": devMode ? "no-cache" : "max-age=31536000,immutable"
|
|
31677
|
+
});
|
|
31673
31678
|
if (path4 === "/theme.css")
|
|
31674
|
-
return serveFile(join20(ws.arcDir, "theme.css"),
|
|
31679
|
+
return serveFile(join20(ws.arcDir, "theme.css"), {
|
|
31680
|
+
...ctx.corsHeaders,
|
|
31681
|
+
"Cache-Control": devMode ? "no-cache" : "max-age=31536000,immutable"
|
|
31682
|
+
});
|
|
31675
31683
|
if ((path4 === "/manifest.json" || path4 === "/manifest.webmanifest") && ws.manifest) {
|
|
31676
31684
|
return serveFile(ws.manifest.path, ctx.corsHeaders);
|
|
31677
31685
|
}
|
|
@@ -31750,7 +31758,7 @@ async function startPlatformServer(opts) {
|
|
|
31750
31758
|
const setManifest = (m4) => {
|
|
31751
31759
|
manifest = m4;
|
|
31752
31760
|
};
|
|
31753
|
-
const getShellHtml = () => generateShellHtml(ws.appName, ws.manifest, manifest?.initial);
|
|
31761
|
+
const getShellHtml = () => generateShellHtml(ws.appName, ws.manifest, manifest?.initial, manifest?.stylesHash);
|
|
31754
31762
|
const sseClients = new Set;
|
|
31755
31763
|
const notifyReload = (m4) => {
|
|
31756
31764
|
const data = JSON.stringify(m4);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arcote.tech/arc-cli",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.3",
|
|
4
4
|
"description": "CLI tool for Arc framework",
|
|
5
5
|
"module": "index.ts",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -12,12 +12,12 @@
|
|
|
12
12
|
"build": "bun build --target=bun ./src/index.ts --outdir=dist --external @arcote.tech/arc --external @arcote.tech/arc-ds --external @arcote.tech/arc-react --external @arcote.tech/platform && chmod +x dist/index.js"
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"@arcote.tech/arc": "^0.7.
|
|
16
|
-
"@arcote.tech/arc-ds": "^0.7.
|
|
17
|
-
"@arcote.tech/arc-react": "^0.7.
|
|
18
|
-
"@arcote.tech/arc-host": "^0.7.
|
|
19
|
-
"@arcote.tech/arc-adapter-db-sqlite": "^0.7.
|
|
20
|
-
"@arcote.tech/platform": "^0.7.
|
|
15
|
+
"@arcote.tech/arc": "^0.7.3",
|
|
16
|
+
"@arcote.tech/arc-ds": "^0.7.3",
|
|
17
|
+
"@arcote.tech/arc-react": "^0.7.3",
|
|
18
|
+
"@arcote.tech/arc-host": "^0.7.3",
|
|
19
|
+
"@arcote.tech/arc-adapter-db-sqlite": "^0.7.3",
|
|
20
|
+
"@arcote.tech/platform": "^0.7.3",
|
|
21
21
|
"@clack/prompts": "^0.9.0",
|
|
22
22
|
"commander": "^11.1.0",
|
|
23
23
|
"chokidar": "^3.5.3",
|
package/src/platform/server.ts
CHANGED
|
@@ -73,6 +73,7 @@ export function generateShellHtml(
|
|
|
73
73
|
appName: string,
|
|
74
74
|
manifest?: { title: string; favicon?: string },
|
|
75
75
|
initial?: { file: string; hash: string },
|
|
76
|
+
stylesHash?: string,
|
|
76
77
|
): string {
|
|
77
78
|
// Initial bundle carries framework, public modules, and PlatformApp re-export.
|
|
78
79
|
// No importmap — single Bun.build with splitting:true inlines + dedups everything
|
|
@@ -81,14 +82,19 @@ export function generateShellHtml(
|
|
|
81
82
|
if (!initialUrl) {
|
|
82
83
|
throw new Error("generateShellHtml: initial bundle missing from manifest");
|
|
83
84
|
}
|
|
85
|
+
// Append the styles content hash as a query string. Filenames are stable
|
|
86
|
+
// (/styles.css, /theme.css) but their content changes between builds; the
|
|
87
|
+
// hash invalidates the browser cache exactly when the content changes,
|
|
88
|
+
// letting us serve them with `Cache-Control: immutable`.
|
|
89
|
+
const stylesQs = stylesHash ? `?v=${stylesHash.slice(0, 16)}` : "";
|
|
84
90
|
return `<!doctype html>
|
|
85
91
|
<html lang="en">
|
|
86
92
|
<head>
|
|
87
93
|
<meta charset="UTF-8" />
|
|
88
94
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
89
95
|
<title>${manifest?.title ?? appName}</title>${manifest?.favicon ? `\n <link rel="icon" href="${manifest.favicon}">` : ""}${manifest ? `\n <link rel="manifest" href="/manifest.json">` : ""}
|
|
90
|
-
<link rel="stylesheet" href="/styles.css" />
|
|
91
|
-
<link rel="stylesheet" href="/theme.css" />
|
|
96
|
+
<link rel="stylesheet" href="/styles.css${stylesQs}" />
|
|
97
|
+
<link rel="stylesheet" href="/theme.css${stylesQs}" />
|
|
92
98
|
<link rel="modulepreload" href="${initialUrl}" />
|
|
93
99
|
</head>
|
|
94
100
|
<body>
|
|
@@ -148,7 +154,6 @@ function serveFile(
|
|
|
148
154
|
// without this, every restart invalidates every signed URL in the open page).
|
|
149
155
|
// Prod uses ARC_MODULE_SECRET if set, otherwise a random UUID per process.
|
|
150
156
|
let MODULE_SIG_SECRET: string = process.env.ARC_MODULE_SECRET ?? "";
|
|
151
|
-
const MODULE_SIG_TTL = 3600; // 1 hour
|
|
152
157
|
|
|
153
158
|
function ensureModuleSigSecret(ws: WorkspaceInfo, devMode: boolean): void {
|
|
154
159
|
if (MODULE_SIG_SECRET) return;
|
|
@@ -163,29 +168,31 @@ function ensureModuleSigSecret(ws: WorkspaceInfo, devMode: boolean): void {
|
|
|
163
168
|
}
|
|
164
169
|
|
|
165
170
|
/**
|
|
166
|
-
* Signed URL for a token-group bundle. HMAC
|
|
167
|
-
*
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
-
*
|
|
171
|
+
* Signed URL for a token-group bundle. HMAC binds the filename so a sig minted
|
|
172
|
+
* for `/browser/admin.<hash>.js` cannot be replayed for any other file. Shared
|
|
173
|
+
* chunks (`chunk-<hash>.js`) are NEVER signed — their names are content-hashed
|
|
174
|
+
* and they don't carry private code on their own (group entries side-effect-
|
|
175
|
+
* register the modules).
|
|
176
|
+
*
|
|
177
|
+
* The signature is intentionally NOT time-bound. A per-request TTL would
|
|
178
|
+
* mutate the URL on every `/api/modules` poll, blowing the browser cache for
|
|
179
|
+
* the (potentially multi-megabyte) protected bundle. Without `exp`:
|
|
180
|
+
* - Content hash in the filename invalidates URLs on deploy.
|
|
181
|
+
* - In-process secret rotation (new UUID on restart) rotates all sigs.
|
|
182
|
+
* - Threat model: a stolen sig only buys access to the JS code itself —
|
|
183
|
+
* all runtime API calls re-validate the JWT independently.
|
|
171
184
|
*/
|
|
172
185
|
function signGroupUrl(file: string): string {
|
|
173
|
-
const exp = Math.floor(Date.now() / 1000) + MODULE_SIG_TTL;
|
|
174
186
|
const hasher = new Bun.CryptoHasher("sha256");
|
|
175
|
-
hasher.update(`${file}:${
|
|
187
|
+
hasher.update(`${file}:${MODULE_SIG_SECRET}`);
|
|
176
188
|
const sig = hasher.digest("hex").slice(0, 16);
|
|
177
|
-
return `/browser/${file}?sig=${sig}
|
|
189
|
+
return `/browser/${file}?sig=${sig}`;
|
|
178
190
|
}
|
|
179
191
|
|
|
180
|
-
function verifyGroupSignature(
|
|
181
|
-
|
|
182
|
-
sig: string | null,
|
|
183
|
-
exp: string | null,
|
|
184
|
-
): boolean {
|
|
185
|
-
if (!sig || !exp) return false;
|
|
186
|
-
if (Number(exp) < Date.now() / 1000) return false;
|
|
192
|
+
function verifyGroupSignature(file: string, sig: string | null): boolean {
|
|
193
|
+
if (!sig) return false;
|
|
187
194
|
const hasher = new Bun.CryptoHasher("sha256");
|
|
188
|
-
hasher.update(`${file}:${
|
|
195
|
+
hasher.update(`${file}:${MODULE_SIG_SECRET}`);
|
|
189
196
|
return hasher.digest("hex").slice(0, 16) === sig;
|
|
190
197
|
}
|
|
191
198
|
|
|
@@ -311,8 +318,7 @@ function staticFilesHandler(
|
|
|
311
318
|
);
|
|
312
319
|
if (isGroupEntry) {
|
|
313
320
|
const sig = url.searchParams.get("sig");
|
|
314
|
-
|
|
315
|
-
if (!verifyGroupSignature(file, sig, exp)) {
|
|
321
|
+
if (!verifyGroupSignature(file, sig)) {
|
|
316
322
|
return new Response("Forbidden", { status: 403, headers: ctx.corsHeaders });
|
|
317
323
|
}
|
|
318
324
|
}
|
|
@@ -322,14 +328,34 @@ function staticFilesHandler(
|
|
|
322
328
|
"Cache-Control": devMode ? "no-cache" : "max-age=31536000,immutable",
|
|
323
329
|
});
|
|
324
330
|
}
|
|
331
|
+
// Locales (compiled .po → .json) — short TTL with SWR; catalogs change
|
|
332
|
+
// build-to-build but rarely within a session, and they're tiny.
|
|
325
333
|
if (path.startsWith("/locales/"))
|
|
326
|
-
return serveFile(join(ws.arcDir, path.slice(1)),
|
|
334
|
+
return serveFile(join(ws.arcDir, path.slice(1)), {
|
|
335
|
+
...ctx.corsHeaders,
|
|
336
|
+
"Cache-Control": devMode ? "no-cache" : "max-age=300,stale-while-revalidate=3600",
|
|
337
|
+
});
|
|
338
|
+
// Browser assets (SQLite WASM worker, etc.) — content-addressed in their
|
|
339
|
+
// source paths, safe to cache forever in prod.
|
|
327
340
|
if (path.startsWith("/assets/"))
|
|
328
|
-
return serveFile(join(ws.assetsDir, path.slice(8)),
|
|
341
|
+
return serveFile(join(ws.assetsDir, path.slice(8)), {
|
|
342
|
+
...ctx.corsHeaders,
|
|
343
|
+
"Cache-Control": devMode ? "no-cache" : "max-age=31536000,immutable",
|
|
344
|
+
});
|
|
345
|
+
// /styles.css and /theme.css have stable URLs but their CONTENT changes
|
|
346
|
+
// between builds. HTML references them with `?v=<stylesHash>` (see
|
|
347
|
+
// generateShellHtml), so the URL changes on rebuild and immutable caching
|
|
348
|
+
// is safe — the handler ignores the query string itself.
|
|
329
349
|
if (path === "/styles.css")
|
|
330
|
-
return serveFile(join(ws.arcDir, "styles.css"),
|
|
350
|
+
return serveFile(join(ws.arcDir, "styles.css"), {
|
|
351
|
+
...ctx.corsHeaders,
|
|
352
|
+
"Cache-Control": devMode ? "no-cache" : "max-age=31536000,immutable",
|
|
353
|
+
});
|
|
331
354
|
if (path === "/theme.css")
|
|
332
|
-
return serveFile(join(ws.arcDir, "theme.css"),
|
|
355
|
+
return serveFile(join(ws.arcDir, "theme.css"), {
|
|
356
|
+
...ctx.corsHeaders,
|
|
357
|
+
"Cache-Control": devMode ? "no-cache" : "max-age=31536000,immutable",
|
|
358
|
+
});
|
|
333
359
|
|
|
334
360
|
// Serve manifest.json from root dir
|
|
335
361
|
if ((path === "/manifest.json" || path === "/manifest.webmanifest") && ws.manifest) {
|
|
@@ -442,7 +468,7 @@ export async function startPlatformServer(
|
|
|
442
468
|
// Recompute on every request — manifest.initial.hash changes when public
|
|
443
469
|
// modules are rebuilt in dev, and we want the new URL in the HTML.
|
|
444
470
|
const getShellHtml = (): string =>
|
|
445
|
-
generateShellHtml(ws.appName, ws.manifest, manifest?.initial);
|
|
471
|
+
generateShellHtml(ws.appName, ws.manifest, manifest?.initial, manifest?.stylesHash);
|
|
446
472
|
const sseClients = new Set<ReadableStreamDefaultController>();
|
|
447
473
|
|
|
448
474
|
const notifyReload = (m: BuildManifest) => {
|