@arcote.tech/arc-cli 0.7.2 → 0.7.4
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 +59 -40
- package/package.json +7 -7
- package/src/builder/build-cache.ts +27 -8
- package/src/platform/server.ts +52 -26
package/dist/index.js
CHANGED
|
@@ -26625,7 +26625,7 @@ ${colors3.yellow}Type declaration errors:${colors3.reset}`);
|
|
|
26625
26625
|
|
|
26626
26626
|
// src/platform/shared.ts
|
|
26627
26627
|
import { copyFileSync, existsSync as existsSync10, mkdirSync as mkdirSync9, readdirSync as readdirSync5, readFileSync as readFileSync10, writeFileSync as writeFileSync9 } from "fs";
|
|
26628
|
-
import { dirname as
|
|
26628
|
+
import { dirname as dirname7, join as join11 } from "path";
|
|
26629
26629
|
|
|
26630
26630
|
// src/builder/module-builder.ts
|
|
26631
26631
|
import { execSync } from "child_process";
|
|
@@ -26637,17 +26637,28 @@ import {
|
|
|
26637
26637
|
rmSync,
|
|
26638
26638
|
writeFileSync as writeFileSync6
|
|
26639
26639
|
} from "fs";
|
|
26640
|
-
import { basename as basename2, dirname as
|
|
26640
|
+
import { basename as basename2, dirname as dirname6, join as join8, relative as relative3 } from "path";
|
|
26641
26641
|
init_i18n();
|
|
26642
26642
|
init_compile();
|
|
26643
26643
|
|
|
26644
26644
|
// src/builder/build-cache.ts
|
|
26645
26645
|
import { existsSync as existsSync5, mkdirSync as mkdirSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync5 } from "fs";
|
|
26646
|
-
import { join as join6 } from "path";
|
|
26647
|
-
|
|
26646
|
+
import { join as join6, dirname as dirname5 } from "path";
|
|
26647
|
+
import { fileURLToPath as fileURLToPath5 } from "url";
|
|
26648
|
+
var CACHE_SCHEMA_VERSION = 3;
|
|
26648
26649
|
var CACHE_FILE = ".build-cache.json";
|
|
26650
|
+
function readCliVersion() {
|
|
26651
|
+
try {
|
|
26652
|
+
const here = dirname5(fileURLToPath5(import.meta.url));
|
|
26653
|
+
const pkg = JSON.parse(readFileSync5(join6(here, "..", "package.json"), "utf-8"));
|
|
26654
|
+
return typeof pkg.version === "string" ? pkg.version : "unknown";
|
|
26655
|
+
} catch {
|
|
26656
|
+
return "unknown";
|
|
26657
|
+
}
|
|
26658
|
+
}
|
|
26659
|
+
var CACHE_FINGERPRINT = `${CACHE_SCHEMA_VERSION}:${readCliVersion()}`;
|
|
26649
26660
|
function emptyCache() {
|
|
26650
|
-
return { version:
|
|
26661
|
+
return { version: CACHE_FINGERPRINT, units: {} };
|
|
26651
26662
|
}
|
|
26652
26663
|
function loadBuildCache(arcDir) {
|
|
26653
26664
|
const path4 = join6(arcDir, CACHE_FILE);
|
|
@@ -26655,7 +26666,7 @@ function loadBuildCache(arcDir) {
|
|
|
26655
26666
|
return emptyCache();
|
|
26656
26667
|
try {
|
|
26657
26668
|
const raw = JSON.parse(readFileSync5(path4, "utf-8"));
|
|
26658
|
-
if (raw?.version !==
|
|
26669
|
+
if (raw?.version !== CACHE_FINGERPRINT || typeof raw.units !== "object") {
|
|
26659
26670
|
return emptyCache();
|
|
26660
26671
|
}
|
|
26661
26672
|
return raw;
|
|
@@ -26963,7 +26974,7 @@ async function buildContextClient(pkg, rootDir, client, cache, noCache) {
|
|
|
26963
26974
|
}
|
|
26964
26975
|
const globalsContent = Object.entries(client.defines).map(([k, v]) => `declare const ${k}: ${v};`).join(`
|
|
26965
26976
|
`);
|
|
26966
|
-
const declResult = await buildTypeDeclarations([pkg.entrypoint], outDir,
|
|
26977
|
+
const declResult = await buildTypeDeclarations([pkg.entrypoint], outDir, dirname6(pkg.entrypoint), globalsContent);
|
|
26967
26978
|
const declarationErrors = !declResult.success && declResult.errors.length > 0 ? declResult.errors.map((e) => `[${pkg.name}/${client.name}] ${e}`) : [];
|
|
26968
26979
|
const outputHash = sha256OfDir(outDir);
|
|
26969
26980
|
updateCache(cache, unitId, inputHash, { outputHash });
|
|
@@ -27527,7 +27538,7 @@ function resolveWorkspace() {
|
|
|
27527
27538
|
err("No package.json found");
|
|
27528
27539
|
process.exit(1);
|
|
27529
27540
|
}
|
|
27530
|
-
const rootDir =
|
|
27541
|
+
const rootDir = dirname7(packageJsonPath);
|
|
27531
27542
|
const rootPkg = JSON.parse(readFileSync10(packageJsonPath, "utf-8"));
|
|
27532
27543
|
const appName = rootPkg.name ?? "Arc App";
|
|
27533
27544
|
const arcDir = join11(rootDir, ".arc", "platform");
|
|
@@ -27704,7 +27715,7 @@ async function copyBrowserAssets(ws, cache, noCache) {
|
|
|
27704
27715
|
const outputHashes = {};
|
|
27705
27716
|
for (const asset of assets) {
|
|
27706
27717
|
const dest = join11(ws.assetsDir, asset.to);
|
|
27707
|
-
mkdirSync9(
|
|
27718
|
+
mkdirSync9(dirname7(dest), { recursive: true });
|
|
27708
27719
|
copyFileSync(asset.src, dest);
|
|
27709
27720
|
outputHashes[asset.to] = sha256Hex(readFileSync10(dest));
|
|
27710
27721
|
}
|
|
@@ -27763,8 +27774,8 @@ async function platformBuild(opts = {}) {
|
|
|
27763
27774
|
|
|
27764
27775
|
// src/commands/platform-deploy.ts
|
|
27765
27776
|
import { existsSync as existsSync17, readFileSync as readFileSync14 } from "fs";
|
|
27766
|
-
import { dirname as
|
|
27767
|
-
import { fileURLToPath as
|
|
27777
|
+
import { dirname as dirname9, join as join19 } from "path";
|
|
27778
|
+
import { fileURLToPath as fileURLToPath7 } from "url";
|
|
27768
27779
|
|
|
27769
27780
|
// src/deploy/bootstrap.ts
|
|
27770
27781
|
var {spawn: spawn4 } = globalThis.Bun;
|
|
@@ -28968,8 +28979,8 @@ import {
|
|
|
28968
28979
|
writeFileSync as writeFileSync14
|
|
28969
28980
|
} from "fs";
|
|
28970
28981
|
import { tmpdir as tmpdir3 } from "os";
|
|
28971
|
-
import { dirname as
|
|
28972
|
-
import { fileURLToPath as
|
|
28982
|
+
import { dirname as dirname8, join as join18 } from "path";
|
|
28983
|
+
import { fileURLToPath as fileURLToPath6 } from "url";
|
|
28973
28984
|
|
|
28974
28985
|
// src/deploy/image-template.ts
|
|
28975
28986
|
function generateDockerfile(inputs) {
|
|
@@ -29061,8 +29072,8 @@ function embedCliBundle(ws) {
|
|
|
29061
29072
|
copyFileSync2(source, target);
|
|
29062
29073
|
}
|
|
29063
29074
|
function locateCliBundle() {
|
|
29064
|
-
const here =
|
|
29065
|
-
let cur =
|
|
29075
|
+
const here = fileURLToPath6(import.meta.url);
|
|
29076
|
+
let cur = dirname8(here);
|
|
29066
29077
|
while (cur !== "/" && cur !== "") {
|
|
29067
29078
|
const candidate = join18(cur, "package.json");
|
|
29068
29079
|
if (existsSync16(candidate)) {
|
|
@@ -29080,7 +29091,7 @@ function locateCliBundle() {
|
|
|
29080
29091
|
throw e;
|
|
29081
29092
|
}
|
|
29082
29093
|
}
|
|
29083
|
-
const parent =
|
|
29094
|
+
const parent = dirname8(cur);
|
|
29084
29095
|
if (parent === cur)
|
|
29085
29096
|
break;
|
|
29086
29097
|
cur = parent;
|
|
@@ -29969,7 +29980,7 @@ async function platformDeploy(envArg, options = {}) {
|
|
|
29969
29980
|
log2("Inspecting remote server...");
|
|
29970
29981
|
const state = await detectRemoteState(cfg);
|
|
29971
29982
|
log2(`Remote state: ${state.kind}`);
|
|
29972
|
-
const cliVersion =
|
|
29983
|
+
const cliVersion = readCliVersion2();
|
|
29973
29984
|
const configHash = await hashDeployConfig(ws.rootDir);
|
|
29974
29985
|
await bootstrap({
|
|
29975
29986
|
cfg,
|
|
@@ -30001,10 +30012,10 @@ async function platformDeploy(envArg, options = {}) {
|
|
|
30001
30012
|
}
|
|
30002
30013
|
}
|
|
30003
30014
|
}
|
|
30004
|
-
function
|
|
30015
|
+
function readCliVersion2() {
|
|
30005
30016
|
try {
|
|
30006
|
-
let cur =
|
|
30007
|
-
const root =
|
|
30017
|
+
let cur = dirname9(fileURLToPath7(import.meta.url));
|
|
30018
|
+
const root = dirname9(cur).startsWith("/") ? "/" : ".";
|
|
30008
30019
|
while (cur !== root && cur !== "") {
|
|
30009
30020
|
const candidate = join19(cur, "package.json");
|
|
30010
30021
|
if (existsSync17(candidate)) {
|
|
@@ -30013,7 +30024,7 @@ function readCliVersion() {
|
|
|
30013
30024
|
return pkg.version ?? "unknown";
|
|
30014
30025
|
}
|
|
30015
30026
|
}
|
|
30016
|
-
const parent =
|
|
30027
|
+
const parent = dirname9(cur);
|
|
30017
30028
|
if (parent === cur)
|
|
30018
30029
|
break;
|
|
30019
30030
|
cur = parent;
|
|
@@ -31486,11 +31497,12 @@ async function createArcServer(config) {
|
|
|
31486
31497
|
init_i18n();
|
|
31487
31498
|
import { existsSync as existsSync18, mkdirSync as mkdirSync14 } from "fs";
|
|
31488
31499
|
import { join as join20 } from "path";
|
|
31489
|
-
function generateShellHtml(appName, manifest, initial) {
|
|
31500
|
+
function generateShellHtml(appName, manifest, initial, stylesHash) {
|
|
31490
31501
|
const initialUrl = initial ? `/browser/${initial.file}` : null;
|
|
31491
31502
|
if (!initialUrl) {
|
|
31492
31503
|
throw new Error("generateShellHtml: initial bundle missing from manifest");
|
|
31493
31504
|
}
|
|
31505
|
+
const stylesQs = stylesHash ? `?v=${stylesHash.slice(0, 16)}` : "";
|
|
31494
31506
|
return `<!doctype html>
|
|
31495
31507
|
<html lang="en">
|
|
31496
31508
|
<head>
|
|
@@ -31499,8 +31511,8 @@ function generateShellHtml(appName, manifest, initial) {
|
|
|
31499
31511
|
<title>${manifest?.title ?? appName}</title>${manifest?.favicon ? `
|
|
31500
31512
|
<link rel="icon" href="${manifest.favicon}">` : ""}${manifest ? `
|
|
31501
31513
|
<link rel="manifest" href="/manifest.json">` : ""}
|
|
31502
|
-
<link rel="stylesheet" href="/styles.css" />
|
|
31503
|
-
<link rel="stylesheet" href="/theme.css" />
|
|
31514
|
+
<link rel="stylesheet" href="/styles.css${stylesQs}" />
|
|
31515
|
+
<link rel="stylesheet" href="/theme.css${stylesQs}" />
|
|
31504
31516
|
<link rel="modulepreload" href="${initialUrl}" />
|
|
31505
31517
|
</head>
|
|
31506
31518
|
<body>
|
|
@@ -31539,7 +31551,6 @@ function serveFile(filePath, headers = {}) {
|
|
|
31539
31551
|
});
|
|
31540
31552
|
}
|
|
31541
31553
|
var MODULE_SIG_SECRET = process.env.ARC_MODULE_SECRET ?? "";
|
|
31542
|
-
var MODULE_SIG_TTL = 3600;
|
|
31543
31554
|
function ensureModuleSigSecret(ws, devMode) {
|
|
31544
31555
|
if (MODULE_SIG_SECRET)
|
|
31545
31556
|
return;
|
|
@@ -31552,19 +31563,16 @@ function ensureModuleSigSecret(ws, devMode) {
|
|
|
31552
31563
|
}
|
|
31553
31564
|
}
|
|
31554
31565
|
function signGroupUrl(file) {
|
|
31555
|
-
const exp = Math.floor(Date.now() / 1000) + MODULE_SIG_TTL;
|
|
31556
31566
|
const hasher = new Bun.CryptoHasher("sha256");
|
|
31557
|
-
hasher.update(`${file}:${
|
|
31567
|
+
hasher.update(`${file}:${MODULE_SIG_SECRET}`);
|
|
31558
31568
|
const sig = hasher.digest("hex").slice(0, 16);
|
|
31559
|
-
return `/browser/${file}?sig=${sig}
|
|
31569
|
+
return `/browser/${file}?sig=${sig}`;
|
|
31560
31570
|
}
|
|
31561
|
-
function verifyGroupSignature(file, sig
|
|
31562
|
-
if (!sig
|
|
31563
|
-
return false;
|
|
31564
|
-
if (Number(exp) < Date.now() / 1000)
|
|
31571
|
+
function verifyGroupSignature(file, sig) {
|
|
31572
|
+
if (!sig)
|
|
31565
31573
|
return false;
|
|
31566
31574
|
const hasher = new Bun.CryptoHasher("sha256");
|
|
31567
|
-
hasher.update(`${file}:${
|
|
31575
|
+
hasher.update(`${file}:${MODULE_SIG_SECRET}`);
|
|
31568
31576
|
return hasher.digest("hex").slice(0, 16) === sig;
|
|
31569
31577
|
}
|
|
31570
31578
|
function decodeTokenPayload(jwt2) {
|
|
@@ -31654,8 +31662,7 @@ function staticFilesHandler(ws, devMode, getManifest) {
|
|
|
31654
31662
|
const isGroupEntry = Object.values(manifest.groups).some((g3) => g3.file === file);
|
|
31655
31663
|
if (isGroupEntry) {
|
|
31656
31664
|
const sig = url.searchParams.get("sig");
|
|
31657
|
-
|
|
31658
|
-
if (!verifyGroupSignature(file, sig, exp)) {
|
|
31665
|
+
if (!verifyGroupSignature(file, sig)) {
|
|
31659
31666
|
return new Response("Forbidden", { status: 403, headers: ctx.corsHeaders });
|
|
31660
31667
|
}
|
|
31661
31668
|
}
|
|
@@ -31665,13 +31672,25 @@ function staticFilesHandler(ws, devMode, getManifest) {
|
|
|
31665
31672
|
});
|
|
31666
31673
|
}
|
|
31667
31674
|
if (path4.startsWith("/locales/"))
|
|
31668
|
-
return serveFile(join20(ws.arcDir, path4.slice(1)),
|
|
31675
|
+
return serveFile(join20(ws.arcDir, path4.slice(1)), {
|
|
31676
|
+
...ctx.corsHeaders,
|
|
31677
|
+
"Cache-Control": devMode ? "no-cache" : "max-age=300,stale-while-revalidate=3600"
|
|
31678
|
+
});
|
|
31669
31679
|
if (path4.startsWith("/assets/"))
|
|
31670
|
-
return serveFile(join20(ws.assetsDir, path4.slice(8)),
|
|
31680
|
+
return serveFile(join20(ws.assetsDir, path4.slice(8)), {
|
|
31681
|
+
...ctx.corsHeaders,
|
|
31682
|
+
"Cache-Control": devMode ? "no-cache" : "max-age=31536000,immutable"
|
|
31683
|
+
});
|
|
31671
31684
|
if (path4 === "/styles.css")
|
|
31672
|
-
return serveFile(join20(ws.arcDir, "styles.css"),
|
|
31685
|
+
return serveFile(join20(ws.arcDir, "styles.css"), {
|
|
31686
|
+
...ctx.corsHeaders,
|
|
31687
|
+
"Cache-Control": devMode ? "no-cache" : "max-age=31536000,immutable"
|
|
31688
|
+
});
|
|
31673
31689
|
if (path4 === "/theme.css")
|
|
31674
|
-
return serveFile(join20(ws.arcDir, "theme.css"),
|
|
31690
|
+
return serveFile(join20(ws.arcDir, "theme.css"), {
|
|
31691
|
+
...ctx.corsHeaders,
|
|
31692
|
+
"Cache-Control": devMode ? "no-cache" : "max-age=31536000,immutable"
|
|
31693
|
+
});
|
|
31675
31694
|
if ((path4 === "/manifest.json" || path4 === "/manifest.webmanifest") && ws.manifest) {
|
|
31676
31695
|
return serveFile(ws.manifest.path, ctx.corsHeaders);
|
|
31677
31696
|
}
|
|
@@ -31750,7 +31769,7 @@ async function startPlatformServer(opts) {
|
|
|
31750
31769
|
const setManifest = (m4) => {
|
|
31751
31770
|
manifest = m4;
|
|
31752
31771
|
};
|
|
31753
|
-
const getShellHtml = () => generateShellHtml(ws.appName, ws.manifest, manifest?.initial);
|
|
31772
|
+
const getShellHtml = () => generateShellHtml(ws.appName, ws.manifest, manifest?.initial, manifest?.stylesHash);
|
|
31754
31773
|
const sseClients = new Set;
|
|
31755
31774
|
const notifyReload = (m4) => {
|
|
31756
31775
|
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.4",
|
|
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.4",
|
|
16
|
+
"@arcote.tech/arc-ds": "^0.7.4",
|
|
17
|
+
"@arcote.tech/arc-react": "^0.7.4",
|
|
18
|
+
"@arcote.tech/arc-host": "^0.7.4",
|
|
19
|
+
"@arcote.tech/arc-adapter-db-sqlite": "^0.7.4",
|
|
20
|
+
"@arcote.tech/platform": "^0.7.4",
|
|
21
21
|
"@clack/prompts": "^0.9.0",
|
|
22
22
|
"commander": "^11.1.0",
|
|
23
23
|
"chokidar": "^3.5.3",
|
|
@@ -1,11 +1,30 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
2
|
-
import { join } from "path";
|
|
2
|
+
import { join, dirname } from "path";
|
|
3
|
+
import { fileURLToPath } from "url";
|
|
3
4
|
|
|
4
|
-
//
|
|
5
|
-
|
|
6
|
-
const CACHE_VERSION = 2;
|
|
5
|
+
// Schema version — bump if cache entry shape changes.
|
|
6
|
+
const CACHE_SCHEMA_VERSION = 3;
|
|
7
7
|
const CACHE_FILE = ".build-cache.json";
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Combined cache fingerprint: schema version + CLI version. Lets a newer CLI
|
|
11
|
+
* automatically invalidate dist/ produced by an older CLI even when source
|
|
12
|
+
* hashes match. Without this, a build-logic change in the CLI (e.g.
|
|
13
|
+
* externalization rules in buildContextClient) silently kept stale dist/
|
|
14
|
+
* around — see the v0.7.3 "creatorWorkspaces not found in context" incident.
|
|
15
|
+
*/
|
|
16
|
+
function readCliVersion(): string {
|
|
17
|
+
try {
|
|
18
|
+
// Bundled CLI lives at `<pkg>/dist/index.js`; package.json is one level up.
|
|
19
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
20
|
+
const pkg = JSON.parse(readFileSync(join(here, "..", "package.json"), "utf-8"));
|
|
21
|
+
return typeof pkg.version === "string" ? pkg.version : "unknown";
|
|
22
|
+
} catch {
|
|
23
|
+
return "unknown";
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
const CACHE_FINGERPRINT = `${CACHE_SCHEMA_VERSION}:${readCliVersion()}`;
|
|
27
|
+
|
|
9
28
|
export interface CacheEntry {
|
|
10
29
|
inputHash: string;
|
|
11
30
|
outputHash?: string;
|
|
@@ -13,22 +32,22 @@ export interface CacheEntry {
|
|
|
13
32
|
}
|
|
14
33
|
|
|
15
34
|
export interface BuildCache {
|
|
16
|
-
version:
|
|
35
|
+
version: string;
|
|
17
36
|
units: Record<string, CacheEntry>;
|
|
18
37
|
}
|
|
19
38
|
|
|
20
39
|
function emptyCache(): BuildCache {
|
|
21
|
-
return { version:
|
|
40
|
+
return { version: CACHE_FINGERPRINT, units: {} };
|
|
22
41
|
}
|
|
23
42
|
|
|
24
43
|
/** Loads cache from .arc/platform/.build-cache.json. Returns empty cache on
|
|
25
|
-
* missing file, parse error, or
|
|
44
|
+
* missing file, parse error, or fingerprint mismatch (schema OR CLI version). */
|
|
26
45
|
export function loadBuildCache(arcDir: string): BuildCache {
|
|
27
46
|
const path = join(arcDir, CACHE_FILE);
|
|
28
47
|
if (!existsSync(path)) return emptyCache();
|
|
29
48
|
try {
|
|
30
49
|
const raw = JSON.parse(readFileSync(path, "utf-8"));
|
|
31
|
-
if (raw?.version !==
|
|
50
|
+
if (raw?.version !== CACHE_FINGERPRINT || typeof raw.units !== "object") {
|
|
32
51
|
return emptyCache();
|
|
33
52
|
}
|
|
34
53
|
return raw as BuildCache;
|
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) => {
|