Files
bitvid/scripts/build-bitlogin-widget.mjs
T
thePR0M3TH3ANandClaude Opus 5 fbac54dd48 Re-vendor BitLogin at 205b067, and record its provenance
Updates the vendored widget to BitLogin 205b067, which includes the
second-audit hardening the previous bytes predated -- phrase-gated sudo key,
deterministic migration, the pinned derivation chain -- plus the entropy
work: the closed nonce/IV surface, fail-closed RNG tests, and the
package-wide Math.random ban.

Also ports the provenance recording from BitUnlock's vendoring script. The
manifest now carries repository, commit, commit date and toolchain, and the
script refuses to vendor from a BitLogin checkout with uncommitted changes,
resolved before vendorDir is deleted so the refusal leaves the committed
files intact. Hashes prove the bytes have not changed since they were
committed; only the commit says which BitLogin produced them, and version
"0.1.0" has covered many substantive changes.

EXISTING SIGN-INS ARE UNAFFECTED, which matters most here since this is the
one consumer with real users. normalize.ts, argon2id.ts, hkdf.ts and
scalarExpand.ts are byte-identical between the previously vendored build and
205b067, so login name + password derive the same locator, signing scalar
and capsule key as before. Accounts created with a hand-typed password still
sign in: the manual-password removal took away the creation, recovery and
rotation CONTROLS, not the acceptance, and nothing on the login path
inspects the password's shape. BitLogin's legacyPassword.e2e.test.ts pins
that, including the migration path of signing in with an old typed password
and rotating onto a generated one.

What does change for users: creating a new account, recovering, or rotating
now hands out a generated six-word passphrase instead of accepting a typed
one.

2354 unit tests pass, including the 10 bitlogin-specific ones in
auth-providers-bitlogin and bitlogin-modal-integration.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01HtNMsKzzFTPURemX7tT19u
2026-07-31 22:17:08 -04:00

106 lines
4.9 KiB
JavaScript

// Vendor the sibling @bitlogin/widget build into a pinned, same-origin bundle.
//
// bitvid's CSP is script-src 'self' (plus a couple of pinned CDN fallbacks for
// nostr-tools) -- the same reasoning as the existing bitcoin-connect/blossom-sdk
// vendor steps. Unlike those, BitLogin ships its own Vite-built ESM output
// directly (bitlogin.js + cryptoWorker.js + a shared chunk they both import),
// so this script copies that output verbatim instead of re-bundling it with
// esbuild -- the worker's relative self-URL resolution depends on the three
// files staying exactly as built and sitting side by side (see BitRoad's and
// BitUnlock's identical vendor steps for this same reasoning).
//
// node scripts/build-bitlogin-widget.mjs
// BITLOGIN_ENTRY=/path/to/bitlogin node scripts/build-bitlogin-widget.mjs
import { createHash } from "node:crypto";
import { copyFile, mkdir, readdir, readFile, rm, writeFile } from "node:fs/promises";
import { existsSync } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { execSync } from "node:child_process";
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const bitloginRoot = process.env.BITLOGIN_ENTRY
? path.resolve(process.env.BITLOGIN_ENTRY)
: path.resolve(repoRoot, "../bitlogin");
const widgetDir = path.join(bitloginRoot, "packages/widget");
const widgetDist = path.join(widgetDir, "dist");
const vendorDir = path.join(repoRoot, "vendor/bitlogin");
const integrityFile = path.join(repoRoot, "vendor/bitlogin.integrity.json");
const packageJson = JSON.parse(await readFile(path.join(widgetDir, "package.json"), "utf8"));
if (packageJson.name !== "@bitlogin/widget") {
throw new Error("BITLOGIN_ENTRY must point to a BitLogin checkout (packages/widget/package.json not found there).");
}
// Provenance (BitUnlock entropy audit M1, ported). Hashes prove the vendored
// bytes have not changed since they were committed; they cannot say WHICH
// BitLogin commit produced them. @bitlogin/widget is a private package pinned
// at 0.1.0 across many substantive changes, so the version string distinguishes
// nothing. Resolved BEFORE the dist build and before vendorDir is deleted, so
// refusing a dirty checkout leaves the committed vendored files untouched.
const git = (args) => execSync(`git ${args}`, { cwd: bitloginRoot, encoding: "utf8" }).trim();
let source;
try {
const dirty = git("status --porcelain") !== "";
if (dirty && process.env.BITLOGIN_ALLOW_DIRTY !== "1") {
throw new Error(
"DIRTY_CHECKOUT: the BitLogin checkout has uncommitted changes, so the artifact this "
+ "would produce corresponds to no commit and cannot be reproduced. Commit there first, "
+ "or set BITLOGIN_ALLOW_DIRTY=1 for a throwaway local build.",
);
}
source = {
repository: git("remote get-url origin"),
commit: git("rev-parse HEAD"),
commitDate: git("log -1 --format=%cI"),
dirty,
provenance: dirty ? "dirty" : "attributed",
toolchain: { node: process.version, vite: packageJson.devDependencies?.vite },
};
} catch (error) {
if (error.message.startsWith("DIRTY_CHECKOUT:")) {
throw new Error(error.message.replace("DIRTY_CHECKOUT: ", ""));
}
throw new Error(
`Could not read BitLogin git provenance from ${bitloginRoot}: ${error.message}. `
+ "Vendoring from a non-git source leaves the artifact unattributable; refusing.",
);
}
console.log(`[vendor-bitlogin] vendoring @bitlogin/widget from ${source.commit.slice(0, 12)}`);
if (!existsSync(widgetDist)) {
console.log("[build-bitlogin-widget] dist missing, building @bitlogin/core then @bitlogin/widget first...");
execSync("npm run build -w @bitlogin/core", { cwd: bitloginRoot, stdio: "inherit" });
execSync("npm run build -w @bitlogin/widget", { cwd: bitloginRoot, stdio: "inherit" });
}
await rm(vendorDir, { recursive: true, force: true });
await mkdir(vendorDir, { recursive: true });
const distEntries = (await readdir(widgetDist)).filter((name) => name.endsWith(".js"));
if (!distEntries.includes("bitlogin.js") || !distEntries.includes("cryptoWorker.js")) {
throw new Error("Expected bitlogin.js and cryptoWorker.js in @bitlogin/widget dist -- did the build layout change?");
}
const files = {};
for (const name of distEntries) {
await copyFile(path.join(widgetDist, name), path.join(vendorDir, name));
const bytes = await readFile(path.join(widgetDist, name));
files[name] = { sha256: createHash("sha256").update(bytes).digest("hex"), bytes: bytes.byteLength };
}
await writeFile(
integrityFile,
`${JSON.stringify({ package: packageJson.name, version: packageJson.version, source, files }, null, 2)}\n`,
);
try {
await copyFile(path.join(bitloginRoot, "LICENSE"), path.join(repoRoot, "vendor/bitlogin.LICENSE"));
} catch (error) {
console.warn("[build-bitlogin-widget] could not copy LICENSE:", error.message);
}
console.log("[build-bitlogin-widget] wrote", path.relative(repoRoot, vendorDir), `(${distEntries.length} files)`);