diff --git a/README.md b/README.md index 5ff8927..a028a00 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Take a folder (or a single HTML file), turn it into a correct single-page-app archive, and deploy it to the [Polkadot Bulletin Chain](https://github.com/paritytech/polkadot-bulletin-chain) -under a `.dot` name. +under a DotNS name. > [!WARNING] > Prototype / reference implementation. Not audited, actively experimental, and @@ -35,7 +35,7 @@ decentralize ./dist --dot my-app -- --env summit --password hunter2 | Option | Meaning | | --- | --- | -| `--dot ` | DotNS name, with or without `.dot`. Required. | +| `--dot ` | DotNS name. Required. The TLD comes from the target environment (`--env`) — e.g. paseo-next-v2 registers under `.paseo`. | | `--path ` | Explicit alternative to the bare positional source. | | `--entry ` | Entry file to use as `index.html`, skipping auto-detection. | | `--fallback` | Also write `404.html` + `_redirects` (off by default — see below). | @@ -47,6 +47,19 @@ verbatim (`--env`, `--password`, `--publish`, `--mnemonic`, `--js-merkle`, …). Use `--` to end this tool's own parsing when a forwarded flag would otherwise be ambiguous. +**The TLD comes from the target environment, not from `--dot`.** `--dot` only +carries the label — `my-app`, not `my-app.paseo`. This tool forwards that +label to `bulletin-deploy`, which applies whichever suffix the environment you +deploy to (`--env`, itself forwarded — see above) actually uses: +`paseo-next-v2` (the default) registers under `.paseo`; `preview` still uses +`.dot`; most other environments carry no fixed suffix at all. A trailing +`.dot` typed on `--dot` is stripped for backward compatibility (every example +before bulletin-deploy 0.15.0 spelled it out), but this tool does not append +one — guessing the right suffix per environment would mean copying +bulletin-deploy's environment table here, and that copy would rot the moment +a new network is added upstream. Pass the bare label and let bulletin-deploy +pick the suffix. + ## What it actually does 1. **Copies** your source into a temp staging directory. Your build output is @@ -61,7 +74,8 @@ ambiguous. root. Ambiguity is an error naming the candidates, not a guess. 4. **Writes nothing else by default.** `404.html` + `_redirects` are only written with `--fallback` (see below for why that's the default now). -5. **Runs `bulletin-deploy`** against the staged directory with your `.dot` name. +5. **Runs `bulletin-deploy`** against the staged directory with your DotNS + label — bare, so bulletin-deploy applies the target environment's own TLD. ## Why `index.html` at the root is the whole point @@ -121,14 +135,17 @@ DotNS (PopRules) accepts a label with **exactly zero or two trailing digits**; anything else reverts on-chain. bulletin-deploy responds by *rewriting* such labels — and on 0.13.x it did so on the registration path, silently retargeting the deploy at a different name ([its issue #1189](https://www.npmjs.com/package/bulletin-deploy)). -Observed live: `--dot my-app3` became `my-app.dot`, an already-owned live name, -and the deploy went on to offer to overwrite its content. +Observed live: `--dot my-app3` became `my-app.dot` (`.dot` was the only TLD +that existed at the time), an already-owned live name, and the deploy went on +to offer to overwrite its content. The hazard — silently landing on a +different, already-owned name — is the same regardless of which TLD the +target environment uses today. This tool refuses such labels up front and tells you what they would have become: ``` ✖ --dot "my-app3" has 1 trailing digit; DotNS (PopRules) accepts exactly 0 or 2. - bulletin-deploy would rewrite it to "my-app.dot" instead of failing … + bulletin-deploy would rewrite it to "my-app" instead of failing … ``` Use a label ending in a letter, or in exactly two digits (`my-app01`). diff --git a/e2e/BOOTSTRAP.md b/e2e/BOOTSTRAP.md index 0a6e465..d1546e8 100644 --- a/e2e/BOOTSTRAP.md +++ b/e2e/BOOTSTRAP.md @@ -17,12 +17,29 @@ here is audited or hardened for production use. ```sh e2e/bootstrap.sh # check-only, read-only, never touches the chain e2e/bootstrap.sh --fix # additionally runs `ipfs init` if the repo is missing -e2e/bootstrap.sh --register # the one real deploy that (re)claims decentralize-ci.dot +e2e/bootstrap.sh --register # the one real deploy that (re)claims decentralize-ci.paseo ``` -## 1. `decentralize-ci.dot` - -**What it is:** a fixed DotNS label on Paseo Next v2, registered 2026-08-07. +## 1. `decentralize-ci.paseo` + +> **Update, 2026-08-18:** the label's TLD changed. bulletin-deploy 0.15.0 made +> the TLD per-environment, and paseo-next-v2 (this suite's `ENV_ID`) registers +> under `.paseo`, not `.dot` — the name below was re-registered as +> `decentralize-ci.paseo` (see item 4's update for the full re-genesis story). +> Everywhere below that still says `.dot` in a command example is preserved +> as-written for history; the live, current name is `decentralize-ci.paseo`, +> and `decentralize` itself now forwards the bare label `decentralize-ci` and +> lets bulletin-deploy apply that suffix (see the main README's "Naming" +> section) — do not hand it `decentralize-ci.dot` directly, that now fails +> with `Domain "decentralize-ci.dot" ends in ".dot", but this environment +> uses ".paseo" names.` + +**What it is:** a fixed DotNS label on Paseo Next v2, originally registered +2026-08-07 as `decentralize-ci.dot`, then re-registered 2026-08-18 as +`decentralize-ci.paseo` after the chain re-genesis described in item 4's +update (a re-genesis resets on-chain state, so the original registration did +not carry forward — this is the "How to recreate it if lost" path below, +exercised for real). **Why it's needed:** the suite re-deploys over the same name every run rather than minting a fresh one per run. That keeps on-chain state bounded (one @@ -35,8 +52,9 @@ built-in worker `5DfhGyQdFobKM8NsWvEeAKk5EQQgYe9AydgJ7rMB6E1EqRzV` — the pool-fallback signer bulletin-deploy uses when no session and no `--mnemonic` are supplied. -**Why it classifies as free (NoStatus):** from `classifyLabelStatus` in the -pinned bulletin-deploy: +**Why it classifies as NoStatus (open to any account — see item 4's update +for why "NoStatus" no longer also means "free"):** from `classifyLabelStatus` +in the pinned bulletin-deploy: | Label shape | Required status | | --- | --- | @@ -55,16 +73,18 @@ failure. **How to recreate it if lost:** simply run a deploy against it. Any deploy — the e2e suite's own run, `e2e/bootstrap.sh --register`, or a manual -`bulletin-deploy decentralize-ci.dot` — re-registers/overwrites it. There -is no separate "provisioning" step; deploying *is* the recovery mechanism. +`bulletin-deploy decentralize-ci --env paseo-next-v2` (bare label — see +the 2026-08-18 update above) — re-registers/overwrites it. There is no +separate "provisioning" step; deploying *is* the recovery mechanism. **The sharp edge (accepted risk, not hidden):** ownership sits with bulletin-deploy's **shared default dev worker**, not an account this project controls. That worker is derived deterministically for *anyone* who runs bulletin-deploy with no session and no `--mnemonic` against Paseo Next v2 — it is not scoped to this repo or this CI. Concretely: anyone, anywhere, -running plain `bulletin-deploy decentralize-ci.dot` with no session -would be the *same* signer/owner and could silently overwrite our content. +running plain `bulletin-deploy decentralize-ci --env paseo-next-v2` with +no session would be the *same* signer/owner and could silently overwrite our +content. There is no ownership check we can add on our side to prevent this — the worker's identity is bulletin-deploy's, not ours. This is a real, accepted risk given the suite's scope (one disposable testnet fixture, not a @@ -86,15 +106,15 @@ worker and then **transfer the name to the signed-in account** with zero mobile signatures (`--no-transfer-to-signedin-user` is the opt-out — see `bulletin-deploy --help`). A developer who is signed in on their own machine and runs `e2e/bootstrap.sh --register` — or any bare `bulletin-deploy -decentralize-ci.dot` — without realizing it would silently move the name off -the pool-fallback worker and onto their personal account. That's worse than -the stranger case above: it's not recoverable by re-running `--register` -from CI, because CI's pool worker no longer owns the name to overwrite. -`e2e/bootstrap.sh --register` checks `bulletin-deploy whoami` first and -refuses to run while a session is signed in, naming `bulletin-deploy logout` -as the remedy — but this check only covers this script's own `--register` -path, not a developer running bulletin-deploy directly by hand. Log out -before touching `decentralize-ci.dot` directly. +decentralize-ci --env paseo-next-v2` — without realizing it would silently +move the name off the pool-fallback worker and onto their personal account. +That's worse than the stranger case above: it's not recoverable by re-running +`--register` from CI, because CI's pool worker no longer owns the name to +overwrite. `e2e/bootstrap.sh --register` checks `bulletin-deploy whoami` first +and refuses to run while a session is signed in, naming `bulletin-deploy +logout` as the remedy — but this check only covers this script's own +`--register` path, not a developer running bulletin-deploy directly by hand. +Log out before touching `decentralize-ci.paseo` directly. ## 2. Kubo (`ipfs` binary + initialized repo) @@ -193,8 +213,47 @@ reporting section. ## 4. The worker's `ProofOfPersonhoodFull` status -**What it is:** the pool-fallback signer (`5DfhGyQdFobKM8NsWvEeAKk5EQQgYe9AydgJ7rMB6E1EqRzV`) -currently carries `ProofOfPersonhoodFull`. +> **Update, 2026-08-18 — this predicted failure mode happened.** Paseo Next +> v2 was re-genesised, and the pool-fallback worker +> (`5DfhGyQdFobKM8NsWvEeAKk5EQQgYe9AydgJ7rMB6E1EqRzV`) came back with **no PoP +> status at all**. `bulletin-deploy`'s preflight now reports `Your PoP: +> NoStatus` for it — exactly the "single most likely way the design silently +> stops working" scenario called out below, before it had actually happened. +> The section below is left as originally written (it is still an accurate +> description of *why* the suite used to cost nothing); read it as history, +> then read this update for the current state: +> +> - **Deploys are no longer free.** A live registration against +> `decentralize-ci.paseo` reported `Oracle price: 10 PAS / Paying: 11 PAS` +> — `registerDepositWei` now takes the `NoStatus` branch (`startingPriceWei`) +> exactly as predicted. Separately, bulletin-deploy's preflight also prints +> a balance-floor figure (observed: **211.1 PAS**) that the signer must hold +> to proceed — that figure is a *minimum balance requirement*, not the +> price of this deploy; don't confuse the two when reading its output. +> - **The worker was funded** to cover this: topped up via the public faucet +> () to **~5005 PAS**, at +> `5DfhGyQdFobKM8NsWvEeAKk5EQQgYe9AydgJ7rMB6E1EqRzV` — the same address as +> above. **That is the address to top up** if this suite ever starts +> failing on a balance error again. +> - **The subtle trap: bulletin-deploy's own auto-top-up cannot rescue this +> worker.** bulletin-deploy has a dev-convenience path that auto-tops-up a +> low-balance signer from "Alice". That "Alice" is **the root account of +> the dev mnemonic** (`//` with no derivation path) — and the pool-fallback +> worker *is* that same root account, not `//Alice` (the well-known +> `5GrwvaEF…` test account derived from it). The auto-top-up code compares +> the source and recipient addresses and **skips the transfer whenever they +> are equal** — so when the worker itself is the signer, "Alice" funding +> the worker is a no-op by construction: the source and destination are the +> same account. Funding the derived `//Alice` (`5GrwvaEF…`) does **nothing** +> for this worker — do not waste a faucet request on it. Fund +> `5DfhGyQdFobKM8NsWvEeAKk5EQQgYe9AydgJ7rMB6E1EqRzV` directly. +> - **Current registration:** `decentralize-ci.paseo` is registered and owned +> by `0x35Cdb23fF7fc86E8DCcd577CA309bFEA9c978D20` (the same worker, EVM-mapped +> — matches item 1's "Current owner"). +> +> **What it was (history, before the re-genesis):** the pool-fallback signer +> (`5DfhGyQdFobKM8NsWvEeAKk5EQQgYe9AydgJ7rMB6E1EqRzV`) carried +> `ProofOfPersonhoodFull`. **Why this is the whole reason the suite costs nothing:** the price rule lives in the pinned bulletin-deploy and keys on the **signer's** status, not @@ -230,7 +289,17 @@ If nightly runs start failing on a price/balance error where they previously didn't, check this first, before assuming a `decentralize` or `bulletin-deploy` regression. -## 5. Pinned `bulletin-deploy@0.14.2` +## 5. Pinned `bulletin-deploy@0.15.0` + +> **Update, 2026-08-18:** bumped from `0.14.2` to `0.15.0` +> (`deps/bulletin-deploy-0.15.0`). The section below still says `0.14.2` in +> places where it is describing that specific version's behaviour verbatim +> ("verified by hand against 0.14.2" etc.) — left as-written, since it is +> accurate history, not updated to imply it was re-verified against 0.15.0 +> line-by-line. The one thing this bump changed that matters everywhere in +> this document: `assets/environments.json` gained a per-environment `tld` +> field, and `paseo-next-v2` (this suite's `ENV_ID`) now uses `.paseo` instead +> of `.dot` — see item 1's update and the main README's "Naming" section. **What it is:** `decentralize`'s own pinned dependency (`package.json` → `dependencies.bulletin-deploy`), and the two public exports the e2e suite @@ -270,8 +339,11 @@ a mismatch by the check above): import paths with compatible signatures — the check above catches this directly. 3. The price rule (`userStatus === NoStatus ? startingPriceWei : 0n`) is - still keyed the same way — a change here could reintroduce a cost where - today there is none. + still keyed the same way. (This branch is no longer hypothetical: as of + the 2026-08-18 update in item 4, the worker IS `NoStatus` and every run + now pays `startingPriceWei` — observed as `Oracle price: 10 PAS / Paying: + 11 PAS`. Re-verify this rule's shape on the next bump precisely because a + change here changes what the suite pays, not whether it pays at all.) 4. The CAR-vs-manifest relationship in the design doc ("The on-chain contenthash is the CAR, not the UnixFS directory") still holds — this is what the whole assertion chain in the design depends on. @@ -292,12 +364,19 @@ confirming whether the free-deploy premise still holds at all — a secret papering over a broken premise just hides the regression instead of surfacing it. +> **Update, 2026-08-18:** the trigger above fired — the worker's PoP status +> lapsed (item 4) and the suite now pays `~11 PAS` per run. The rule in this +> section still holds regardless: the fix was to **fund the worker directly** +> (item 4's faucet top-up to ~5005 PAS), not to add a `MNEMONIC` secret. "No +> secrets" was never a claim that deploys are free forever — it's a claim +> that this suite doesn't authenticate as anyone. It still doesn't. + ## Known unknowns Stated honestly rather than assumed away: - **Whether DotNS registrations expire.** Not established. If they do, the - window before `decentralize-ci.dot` needs a fresh registration (as opposed + window before `decentralize-ci.paseo` needs a fresh registration (as opposed to an overwrite of an existing one) is unknown. Both paths are handled by bulletin-deploy either way (see item 1's "how to recreate"), so this doesn't block the suite — it's flagged so nobody mistakes a @@ -318,5 +397,11 @@ Stated honestly rather than assumed away: bulletin-deploy ever exposes a genuinely lightweight read-only query for this, revisit. - **Whether the worker's `ProofOfPersonhoodFull` status can lapse, and on - what schedule.** Not established — see item 4. Worth monitoring, not - currently monitorable from outside a full deploy attempt. + what schedule.** Partially resolved, 2026-08-18: it can, and did — see item + 4's update. What's still unknown is the *schedule*: this instance was + triggered by a chain re-genesis, not a natural expiry, so whether + `ProofOfPersonhoodFull` also lapses on its own over time (independent of a + re-genesis event) remains unestablished. Still not monitorable from outside + a full deploy attempt (or a preflight-only run — see item 4's `Your PoP: + NoStatus` observation, which came from bulletin-deploy's own preflight + output, not a purpose-built check on our side). diff --git a/e2e/bootstrap.sh b/e2e/bootstrap.sh index ca774cb..991864f 100755 --- a/e2e/bootstrap.sh +++ b/e2e/bootstrap.sh @@ -30,7 +30,7 @@ # if the IPFS repo is missing. Nothing else. Does NOT # register a name and does NOT deploy. # --register Performs the ONE real on-chain deploy that (re)claims -# decentralize-ci.dot on Paseo Next v2. Prints exactly what +# decentralize-ci.paseo on Paseo Next v2. Prints exactly what # it is about to do, then asks for confirmation unless --yes # is also given. Refuses to run if any required check above # it is failing. @@ -73,7 +73,7 @@ Usage: e2e/bootstrap.sh [--fix] [--register [--yes]] [-h|--help] --fix Run `ipfs init` if the IPFS repo is missing. The only remediation this script performs automatically. --register Perform the one real deploy that (re)claims - decentralize-ci.dot on Paseo Next v2. Confirms interactively + decentralize-ci.paseo on Paseo Next v2. Confirms interactively unless --yes is also passed. --yes Skip the --register confirmation prompt. @@ -254,7 +254,7 @@ check_dotns_status() { # deploy path itself and can fail independently of the thing we're # trying to check. Fabricating a check here would be worse than # admitting we don't have one. See e2e/BOOTSTRAP.md. - record_result "decentralize-ci.dot on-chain contenthash" "UNKNOWN" "cannot check without deploying — no supported read-only query found (see e2e/BOOTSTRAP.md). Use --register if you believe it needs (re)claiming." + record_result "decentralize-ci.paseo on-chain contenthash" "UNKNOWN" "cannot check without deploying — no supported read-only query found (see e2e/BOOTSTRAP.md). Use --register if you believe it needs (re)claiming." } # --- table + summary ----------------------------------------------------- @@ -294,7 +294,7 @@ do_register() { # e2e/BOOTSTRAP.md item 1: bulletin-deploy's *default* behaviour when a # mobile session is signed in is to register with the local worker and # then transfer the name to that signed-in account. Running --register - # while signed in would silently move decentralize-ci.dot off the + # while signed in would silently move decentralize-ci.paseo off the # pool-fallback worker and onto a human's personal account, permanently # breaking the overwrite-path design (the CI worker would no longer own # it, and no `--register` re-run from CI could get it back). Refuse @@ -304,24 +304,28 @@ do_register() { if ! printf '%s' "$whoami_out" | grep -qi "not logged in"; then echo "Refusing --register: bulletin-deploy reports an active signed-in session:" >&2 echo " ${whoami_out//$'\n'/$'\n '}" >&2 - echo "By default, registering while signed in hands ${DOT_LABEL}.dot to that" >&2 + echo "By default, registering while signed in hands ${DOT_LABEL}.paseo to that" >&2 echo "signed-in account instead of the pool-fallback worker — this would break" >&2 echo "the overwrite-path design permanently (see e2e/BOOTSTRAP.md item 1)." >&2 echo "Run 'node node_modules/bulletin-deploy/bin/bulletin-deploy logout' first, then re-run --register." >&2 exit 1 fi - echo "== --register: (re)claim ${DOT_LABEL}.dot on ${ENV_ID} ==" + echo "== --register: (re)claim ${DOT_LABEL}.paseo on ${ENV_ID} ==" echo "This will:" echo " 1. Stage a minimal placeholder index.html in a throwaway temp directory." echo " 2. Run a REAL on-chain deploy:" - echo " node node_modules/bulletin-deploy/bin/bulletin-deploy ${DOT_LABEL}.dot --env ${ENV_ID}" - echo " 3. Use the pool-fallback worker (no session, no --mnemonic, no secret) —" - echo " confirmed above: no bulletin-deploy session is signed in." - echo " Expected cost: 0 PAS — the worker holds ProofOfPersonhoodFull, and" - echo " ${DOT_LABEL} classifies as a NoStatus label (free to any status)." - echo " If bulletin-deploy reports a nonzero price, STOP: that means the" - echo " worker's PoP status lapsed. See e2e/BOOTSTRAP.md before proceeding." + echo " node node_modules/bulletin-deploy/bin/bulletin-deploy ${DOT_LABEL} --env ${ENV_ID}" + echo " (bare label — bulletin-deploy applies ${ENV_ID}'s own TLD, .paseo; see the" + echo " main README's Naming section for why this script does not append one)." + echo " 3. Use the pool-fallback worker (no session, no --mnemonic, no secret)." + echo " Expected cost: ~11 PAS (oracle price 10 PAS + margin) — the worker no" + echo " longer holds ProofOfPersonhoodFull (see e2e/BOOTSTRAP.md item 4, updated" + echo " 2026-08-18), so registerDepositWei charges the NoStatus price. The worker" + echo " is funded (~5005 PAS as of 2026-08-18) specifically to cover this — see" + echo " e2e/BOOTSTRAP.md item 4. If bulletin-deploy reports a balance failure," + echo " the worker needs topping up again; see BOOTSTRAP.md item 4 for exactly" + echo " which address that is (the auto-top-up \"Alice\" cannot rescue it)." echo if [ "$ASSUME_YES" -ne 1 ]; then @@ -348,7 +352,11 @@ do_register() { echo "generated $(date -u +%Y-%m-%dT%H:%M:%SZ)" } >"$REGISTER_TMPDIR/index.html" - (cd "$REPO_ROOT" && node node_modules/bulletin-deploy/bin/bulletin-deploy "$REGISTER_TMPDIR" "${DOT_LABEL}.dot" --env "${ENV_ID}") + # Bare label — bulletin-deploy applies ${ENV_ID}'s own TLD (.paseo). Do + # not append ".dot" here: bulletin-deploy 0.15.0 made the TLD + # per-environment, and appending the wrong one fails outright with + # 'Domain "…" ends in ".dot", but this environment uses ".paseo" names.' + (cd "$REPO_ROOT" && node node_modules/bulletin-deploy/bin/bulletin-deploy "$REGISTER_TMPDIR" "${DOT_LABEL}" --env "${ENV_ID}") } # --- main ------------------------------------------------------------------ diff --git a/e2e/deploy.e2e.test.ts b/e2e/deploy.e2e.test.ts index fcb25f1..96d4d54 100644 --- a/e2e/deploy.e2e.test.ts +++ b/e2e/deploy.e2e.test.ts @@ -27,7 +27,7 @@ * `exit 0` would sail straight through the bug we most need to catch. * * This suite makes ONE real deploy per case to Paseo Next v2, against the - * fixed name `decentralize-ci.dot` (owned by bulletin-deploy's pool-fallback + * fixed name `decentralize-ci.paseo` (owned by bulletin-deploy's pool-fallback * worker — see e2e/BOOTSTRAP.md item 1 for the full rationale and its sharp * edges), and round-trips the on-chain contenthash back to the exact bytes * that were staged. Exit code alone is never the assertion. @@ -150,14 +150,14 @@ interface DeployRun { /** * Run the BUILT CLI (`dist/cli.js`, matching what users install — not the - * TypeScript source) against `decentralize-ci.dot`, with the child's session + * TypeScript source) against `decentralize-ci.paseo`, with the child's session * state isolated. * * WHY THE HOME OVERRIDE EXISTS — bulletin-deploy's default behaviour, when a * `bulletin-deploy login` session is signed in, is to register with a local * worker and then TRANSFER THE NAME to the signed-in account (zero mobile * signatures required — see e2e/BOOTSTRAP.md item 1's "mirror-image sharp - * edge"). This suite depends on `decentralize-ci.dot` staying owned by the + * edge"). This suite depends on `decentralize-ci.paseo` staying owned by the * shared pool-fallback worker (`0x35Cdb23fF7fc86E8DCcd577CA309bFEA9c978D20`) * FOREVER, so every future run can overwrite it. If this suite ever ran on a * machine with an active session — a developer's laptop, most likely — the @@ -353,7 +353,7 @@ async function deployAndVerify(caseLabel: string, ownFlags: string[]): Promise { +describe("deploy.e2e — decentralize-ci.paseo on Paseo Next v2", () => { it( "deploys the default archive, and the deployed manifest matches the staged bytes exactly (no --fallback)", async () => { diff --git a/package-lock.json b/package-lock.json index b9ddb45..e425bdf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "0.1.0", "license": "GPL-3.0-or-later", "dependencies": { - "bulletin-deploy": "0.14.2" + "bulletin-deploy": "0.15.0" }, "bin": { "decentralize": "dist/cli.js" @@ -3117,9 +3117,9 @@ } }, "node_modules/bulletin-deploy": { - "version": "0.14.2", - "resolved": "https://registry.security.parity.io/npm/bulletin-deploy/-/bulletin-deploy-0.14.2.tgz", - "integrity": "sha512-BOzsS7z/WL8JZFjuqySk9yNDaje5+Ya71QIq2z8Kiu13WgR8IBk1mT+xrzpArjBBGKtJgWx5JfOeWeUo8tPOGg==", + "version": "0.15.0", + "resolved": "https://registry.security.parity.io/npm/bulletin-deploy/-/bulletin-deploy-0.15.0.tgz", + "integrity": "sha512-UFfj2HX+20aWwhulueqfm6uOGmeHf+Ee6+v0Enw/8BzKkEW6j4lbOo8VlpPsDTBc+CAW+/rxNaxtJ2AdrnIPXQ==", "hasInstallScript": true, "license": "GPL-3.0-or-later", "dependencies": { diff --git a/package.json b/package.json index a15dbca..743fb8a 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@paritytech/decentralize", "version": "0.1.0", - "description": "Stage a file or folder as a single-page-app archive and deploy it to the Polkadot Bulletin Chain under a .dot name", + "description": "Stage a file or folder as a single-page-app archive and deploy it to the Polkadot Bulletin Chain under a DotNS name", "license": "GPL-3.0-or-later", "type": "module", "engines": { @@ -39,7 +39,7 @@ "prepublishOnly": "npm run build && npm test" }, "dependencies": { - "bulletin-deploy": "0.14.2" + "bulletin-deploy": "0.15.0" }, "devDependencies": { "@types/node": "^22.10.2", diff --git a/src/cli.test.ts b/src/cli.test.ts index d136a72..df62bc4 100644 --- a/src/cli.test.ts +++ b/src/cli.test.ts @@ -200,9 +200,22 @@ describe("the handoff to bulletin-deploy", () => { expect(out.record).not.toBeNull(); const argv = out.record!.argv; // Positional order is the contract: root, then name, then passthrough. - expect(argv.slice(1)).toEqual(["myapp.dot", "--env", "paseo-next-v2"]); + expect(argv.slice(1)).toEqual(["myapp", "--env", "paseo-next-v2"]); expect(argv[0]).toContain("decentralize-"); - expect(out.stdout).toContain("✔ Deployed myapp.dot"); + expect(out.stdout).toContain("✔ Deployed myapp"); + }); + + it("strips a trailing .dot but forwards a bare label unchanged", () => { + // bulletin-deploy 0.15.0 made the TLD per-environment (paseo-next-v2 + // -> .paseo; preview -> .dot; most others carry none at all), so this + // tool no longer appends a suffix — it forwards the bare label and + // lets bulletin-deploy apply whichever TLD the target --env uses. + // Both spellings of the same label must reach the child identically. + const withSuffix = run([app(), "--dot", "my-app.dot", "--", "--env", "paseo-next-v2"]); + const bare = run([app(), "--dot", "my-app", "--", "--env", "paseo-next-v2"]); + + expect(withSuffix.record!.argv.slice(1)).toEqual(["my-app", "--env", "paseo-next-v2"]); + expect(bare.record!.argv.slice(1)).toEqual(["my-app", "--env", "paseo-next-v2"]); }); it("hands the child a directory that still holds the staged archive", () => { @@ -301,6 +314,6 @@ describe("preflight, before anything is staged", () => { }); expect(out.status).toBe(0); - expect(out.record!.argv.slice(1)).toEqual(["myapp.dot", "--js-merkle"]); + expect(out.record!.argv.slice(1)).toEqual(["myapp", "--js-merkle"]); }); }); diff --git a/src/cli.ts b/src/cli.ts index d6c3fe0..2fde51e 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -134,7 +134,9 @@ function printUsage(): void { "Usage:\n" + " decentralize --dot [options] [-- ]\n\n" + "Options:\n" + - " --dot DotNS name, with or without `.dot` (required)\n" + + " --dot DotNS name (required). The TLD comes from the target\n" + + " environment (--env), not from this tool — e.g.\n" + + " paseo-next-v2 registers under `.paseo`.\n" + " --path Explicit alternative to the bare positional source\n" + " --entry Entry file to use as index.html (skips auto-detection)\n" + " --fallback Also write 404.html + _redirects (off by default; see README)\n" + diff --git a/src/index.test.ts b/src/index.test.ts index 9ff175a..6324620 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -55,17 +55,29 @@ afterEach(() => { }); describe("normaliseDomain", () => { - it("appends .dot to a bare label", () => { - expect(normaliseDomain("my-app")).toBe("my-app.dot"); + // bulletin-deploy 0.15.0 made the TLD per-environment (paseo-next-v2 -> + // .paseo; preview -> .dot; most others carry none), so this tool no + // longer appends a suffix of its own — it forwards the bare label and + // lets bulletin-deploy apply whichever TLD the target --env uses. + it("strips a trailing .dot to the bare label", () => { + expect(normaliseDomain("my-app.dot")).toBe("my-app"); }); - it("leaves an already-suffixed name alone rather than doubling it", () => { - expect(normaliseDomain("my-app.dot")).toBe("my-app.dot"); + it("leaves a bare label unchanged", () => { + expect(normaliseDomain("my-app")).toBe("my-app"); + }); + + it("forwards a non-.dot suffix unchanged, since the caller typed it deliberately", () => { + expect(normaliseDomain("my-app.paseo")).toBe("my-app.paseo"); + }); + + it("--dot my-app.dot and --dot my-app produce the same bare label", () => { + expect(normaliseDomain("my-app.dot")).toBe(normaliseDomain("my-app")); }); it("tolerates surrounding whitespace and a trailing dot", () => { - expect(normaliseDomain(" my-app.dot. ")).toBe("my-app.dot"); - expect(normaliseDomain(" my-app ")).toBe("my-app.dot"); + expect(normaliseDomain(" my-app.dot. ")).toBe("my-app"); + expect(normaliseDomain(" my-app ")).toBe("my-app"); }); it("rejects values that would normalise to nothing", () => { @@ -78,7 +90,7 @@ describe("parseArgs", () => { it("takes the source positionally and the domain as a named parameter", () => { const args = parseArgs(["./dist", "--dot", "my-app"]); expect(args.source).toBe("./dist"); - expect(args.domain).toBe("my-app.dot"); + expect(args.domain).toBe("my-app"); expect(args.passthrough).toEqual([]); }); @@ -86,7 +98,7 @@ describe("parseArgs", () => { expect(parseArgs(["--path", "./dist", "--domain", "x"]).source).toBe("./dist"); expect(parseArgs(["--path=./dist", "--dot=x"])).toMatchObject({ source: "./dist", - domain: "x.dot", + domain: "x", }); }); @@ -238,23 +250,26 @@ describe("resolveSpaRoot", () => { describe("assertLabelIsPopRulesSafe (via normaliseDomain)", () => { it("accepts a label ending in a letter", () => { - expect(normaliseDomain("my-app")).toBe("my-app.dot"); + expect(normaliseDomain("my-app")).toBe("my-app"); }); it("accepts exactly two trailing digits", () => { - expect(normaliseDomain("my-app01")).toBe("my-app01.dot"); + expect(normaliseDomain("my-app01")).toBe("my-app01"); }); it("rejects one trailing digit, naming what it would silently become", () => { - // The live near-miss: spa-route-test3 → spa-route-test.dot (#1189). - expect(() => normaliseDomain("spa-route-test3")).toThrow(/spa-route-test\.dot/); + // The live near-miss: spa-route-test3 → spa-route-test (#1189). No + // TLD is asserted here: which suffix bulletin-deploy would have + // rewritten onto is environment-dependent since 0.15.0, but the + // silent-retarget hazard is the same regardless. + expect(() => normaliseDomain("spa-route-test3")).toThrow(/"spa-route-test"/); expect(() => normaliseDomain("spa-route-test3")).toThrow(/1 trailing digit;/); }); it("rejects three or more trailing digits", () => { expect(() => normaliseDomain("my-app123")).toThrow(/3 trailing digits/); // >2 keeps the last two, mirroring sanitizeDomainLabel. - expect(() => normaliseDomain("my-app123")).toThrow(/my-app23\.dot/); + expect(() => normaliseDomain("my-app123")).toThrow(/"my-app23"/); }); it("strips a dangling hyphen when suggesting alternatives", () => { diff --git a/src/index.ts b/src/index.ts index 0b28344..baccc68 100644 --- a/src/index.ts +++ b/src/index.ts @@ -106,7 +106,10 @@ export class UsageError extends Error { export interface Args { /** File or directory to deploy. */ source: string; - /** DotNS label, normalised to end in `.dot`. */ + /** DotNS label. A trailing `.dot` typed by the user is stripped to the + * bare label; everything else is forwarded unchanged. bulletin-deploy + * applies its own environment's TLD (e.g. `.paseo` for the default + * paseo-next-v2 environment) — see `normaliseDomain`. */ domain: string; /** Explicit entry file relative to `source` (skips auto-detection). */ entry: string | null; @@ -243,15 +246,40 @@ export function parseArgs(argv: string[]): Args { }; } -/** `my-app` and `my-app.dot` both normalise to `my-app.dot`. */ +/** + * `my-app` and `my-app.dot` both normalise to the bare label `my-app`. + * + * bulletin-deploy 0.15.0 made the TLD per-environment — `paseo-next-v2` (the + * default) registers under `.paseo`; only `preview` still uses `.dot`; most + * others carry no TLD at all in `assets/environments.json`. This tool has no + * business reading that table and applying it itself: which environment is + * even in play is decided by a passthrough `--env` flag this tool + * deliberately does not parse (see `parseArgs`'s doc comment), so duplicating + * bulletin-deploy's environment table here would just rot the moment a new + * network is added upstream. Forwarding the bare label instead makes this + * tool TLD-agnostic: bulletin-deploy resolves the correct suffix for whatever + * `--env` was actually passed. Verified live: a bare `decentralize-ci` against + * `paseo-next-v2` resolved to `decentralize-ci.paseo`, registered, and set the + * contenthash — exactly what bulletin-deploy's own error message instructs + * when handed a name with the wrong suffix. + * + * A trailing `.dot` is still stripped rather than forwarded literally, purely + * for backward compatibility: every existing `--dot my-app.dot` invocation and + * every example predating 0.15.0 spelled the suffix out, and stripping it + * keeps all of those working unchanged. Anything else the caller types (e.g. + * `my-app.paseo`, deliberately spelling out a different environment's suffix) + * is forwarded byte-for-byte — this function does not know or guess what a + * `.paseo` or any other suffix means, so it never invents or removes one on + * your behalf. + */ export function normaliseDomain(input: string): string { const trimmed = input.trim().replace(/\.$/, ""); - if (trimmed === "" || trimmed === ".dot") { + const label = trimmed.endsWith(".dot") ? trimmed.slice(0, -".dot".length) : trimmed; + if (label === "") { throw new UsageError(`invalid --dot value: "${input}"`); } - const domain = trimmed.endsWith(".dot") ? trimmed : `${trimmed}.dot`; - assertLabelIsPopRulesSafe(domain.slice(0, -".dot".length)); - return domain; + assertLabelIsPopRulesSafe(label); + return label; } export function countTrailingDigits(label: string): number { @@ -265,9 +293,11 @@ export function countTrailingDigits(label: string): number { * on-chain. bulletin-deploy's `sanitizeDomainLabel` therefore rewrites such * labels — and up to and including 0.13.x it did so on the registration path, * which SILENTLY RETARGETS the deploy at a different name (its issue #1189). - * Observed live: `--dot spa-route-test3` became `spa-route-test.dot`, an - * already-owned live name, and the deploy went on to offer to overwrite its - * content. + * Observed live: `--dot spa-route-test3` became `spa-route-test`, an + * already-owned live name (registered under `.dot`, the only TLD that + * existed at the time), and the deploy went on to offer to overwrite its + * content. The rewrite-onto-a-different-name hazard is independent of which + * TLD is in play today. * * Newer bulletin-deploy refuses non-compliant labels outright, so erroring here * matches where upstream landed while also protecting anyone on an older @@ -292,7 +322,7 @@ export function assertLabelIsPopRulesSafe(label: string): void { throw new UsageError( `--dot "${label}" has ${trailing} trailing digit${trailing === 1 ? "" : "s"}; DotNS ` + `(PopRules) accepts exactly 0 or 2. bulletin-deploy would rewrite it to ` + - `"${becomes}.dot" instead of failing — on 0.13.x that silently retargets the ` + + `"${becomes}" instead of failing — on 0.13.x that silently retargets the ` + `deploy at a DIFFERENT name, overwriting it if you own it (its issue #1189). ` + `Use a label ending in a letter, or in exactly two digits (e.g. "${stripped}" ` + `or "${stripped}01").`,