Files
ng-eventually/packages/ng-e2e-helpers/bin/mint-wallet.ts
Sylvain Duchesne b50591f5bd feat(e2e-helpers): fabriquer un portefeuille, pour un test comme pour un déploiement
Trois appelants veulent la même chose — un portefeuille neuf, ses octets et son
mot de passe : la suite du polyfill à son démarrage, la suite d'une application
au sien, et un humain une fois pour provisionner un déploiement. Rien ne
produisait ce fichier ; il fallait aller le chercher à la main sur nextgraph.eu.

Mêmes entrées, mêmes sorties, mais des appelants de nature différente : une
suite ne va pas lancer un sous-processus et analyser sa sortie, et un humain ne
va pas écrire un fichier jetable pour appeler une fonction. Donc une fonction,
et un script mince par-dessus.

mintWalletBytes(password, name) rend les OCTETS, pas un chemin. C'est une
correction de ce qui existait : exportWalletFile imposait le disque à tout le
monde, et la suite applicative écrivait un fichier temporaire pour le relire
aussitôt en mémoire, puis devait le nettoyer. Elle ne le fait plus — serveApp
prend les octets. Qui veut un fichier l'écrit ; personne n'y est forcé.

Le script exige --password et refuse d'en inventer un, et refuse d'écraser un
.ngw existant sans --force, vérifié AVANT de fabriquer quoi que ce soit.
Exécuté pour de vrai : 800 octets, et le mot de passe imprimé est celui passé en
entrée. C'est le seul des trois cas qu'aucune suite n'exerce, donc le seul qui
pouvait être livré cassé sans que rien ne le dise.

Ce qui ne change pas : les suites fabriquent un portefeuille par exécution et
n'en héritent jamais ; le provisionnement veut l'inverse, durable et conservé.
Ils partagent la fabrication et l'export, ils divergent sur la durée de vie.
Les identifiants en dur deviennent un paramètre — les suites passent toujours
les leurs, le script prend ceux qu'on lui donne, et il n'existe aucun mot de
passe par défaut.
2026-08-16 15:44:37 +02:00

107 lines
4.5 KiB
TypeScript
Executable File

#!/usr/bin/env bun
/**
* Mint a NextGraph wallet and write it as a `.ngw` — the one-off a person runs to provision a
* deployment.
*
* ── Why this exists next to the library function ─────────────────────────────
* An application that hands a wallet out serves a `.ngw` at the URL it passes to
* `configure({ sharedWallet: { fileUrl, password } })`, and nothing produces that file: minting
* one means driving the wallet application in a browser, which is exactly what
* `mintWalletBytes` already does for the suites. So this is not a second implementation — it is
* that call, a `writeFileSync`, and the two lines a human needs to fill the configuration in.
*
* ── What it does NOT do ──────────────────────────────────────────────────────
* It does not invent a password. The password is what opens the wallet for everyone the
* deployment lets in; one chosen here would be a secret the tool knows and the operator does
* not, printed to a terminal and never chosen by anybody. It is a required argument.
*
* It also refuses to overwrite an existing file unless told to. A `.ngw` is an identity, and
* the identities it holds exist nowhere else — a clobbered one is not recoverable from the
* broker or anywhere else.
*
* Usage:
* bun run packages/ng-e2e-helpers/bin/mint-wallet.ts --password <password> [--out <path.ngw>] [--name <wallet name>] [--force]
*/
import * as fs from "node:fs";
import * as path from "node:path";
import { DEFAULT_WALLET_NAME, mintWalletBytes } from "../src/wallet";
const USAGE =
"usage: mint-wallet --password <password> [--out <path.ngw>] [--name <wallet name>] [--force]";
interface Options {
readonly password: string;
readonly name: string;
readonly out: string;
readonly force: boolean;
}
/** `--k v` and `--k=v` both, because a person types whichever one they learnt first. */
function parseArguments(argv: readonly string[]): Options {
const values = new Map<string, string>();
let force = false;
for (let i = 0; i < argv.length; i++) {
const arg = argv[i]!;
if (arg === "--force") {
force = true;
continue;
}
if (!arg.startsWith("--")) throw new Error(`unexpected argument ${arg}\n${USAGE}`);
const equals = arg.indexOf("=");
const key = equals === -1 ? arg.slice(2) : arg.slice(2, equals);
let value: string | undefined;
if (equals !== -1) {
value = arg.slice(equals + 1);
} else {
value = argv[++i];
}
if (value === undefined) throw new Error(`--${key} needs a value\n${USAGE}`);
if (!["password", "out", "name"].includes(key)) {
throw new Error(`unknown option --${key}\n${USAGE}`);
}
values.set(key, value);
}
const password = values.get("password");
if (password === undefined || password === "") {
throw new Error(`--password is required — this tool does not invent one\n${USAGE}`);
}
const name = values.get("name") ?? DEFAULT_WALLET_NAME;
// Relative to where the person is standing, which is the only path they can predict. Any
// default landing inside a checkout is covered by the repository-wide `*.ngw` ignore.
const out = path.resolve(process.cwd(), values.get("out") ?? `${name}.ngw`);
return { password, name, out, force };
}
async function main(): Promise<void> {
const options = parseArguments(process.argv.slice(2));
if (!options.force && fs.existsSync(options.out)) {
throw new Error(
`${options.out} already exists — a .ngw is an identity, so this refuses to overwrite one.\n` +
"Pass --force if that file is genuinely disposable.",
);
}
console.log(`[mint-wallet] minting the wallet "${options.name}" (this drives a real browser)...`);
const bytes = await mintWalletBytes(options.password, options.name);
fs.mkdirSync(path.dirname(options.out), { recursive: true });
fs.writeFileSync(options.out, bytes);
console.log("");
console.log(`[mint-wallet] wrote ${bytes.length} bytes`);
console.log(` file: ${options.out}`);
console.log(` password: ${options.password}`);
console.log("");
console.log("Serve that file, and give the application its URL and this password:");
console.log(" configure({ sharedWallet: { fileUrl: <where you serve it>, password: <above> } })");
}
main().then(
() => process.exit(0),
(e: unknown) => {
console.error(`[mint-wallet] ${e instanceof Error ? e.message : String(e)}`);
process.exit(1);
},
);