Skip to content

Decentralize Subcommand#181

Open
shawntabrizi wants to merge 5 commits into
paritytech:mainfrom
shawntabrizi:feat/decentralize
Open

Decentralize Subcommand#181
shawntabrizi wants to merge 5 commits into
paritytech:mainfrom
shawntabrizi:feat/decentralize

Conversation

@shawntabrizi
Copy link
Copy Markdown
Member

@shawntabrizi shawntabrizi commented May 19, 2026

Summary

Adds dot decentralize — point at a live static site, get back a .dot URL. Wraps the existing runStorageDeploy (Bulletin chunked upload + DotNS register) behind a wget --mirror of the source site.

dot decentralize --site=shawntabrizi.com                    # signs with your `dot init` session account
dot decentralize --site=shawntabrizi.com --suri //Bob       # signs with the shared //Bob testnet account
dot decentralize --site=shawntabrizi.com --suri <mnemonic>  # signs with a BIP-39 mnemonic
dot decentralize --site=shawntabrizi.com --dot=mysite42     # explicit name (must be NoStatus-compatible if signer has no PoP)

Default signs with the user's dot init session account so the registered .dot name is owned by them. --suri overrides it; --suri //Bob is the explicit zero-setup path used by the dot-decentralize demo service.

Mechanics:

  • Mirrorwget --mirror --convert-links --adjust-extension --page-requisites --no-parent --no-host-directories into a fresh tmp dir. URL passed as a separate execve argument, never spliced into a shell string. Validates http(s) scheme only.
  • Upload + register — delegates straight to the existing runStorageDeploy (the same path dot deploy uses), so any improvements to chunked-storage / retries / DotNS commit-reveal flow into this command for free.
  • Auto-generated names — derived from the source URL's hostname so the resulting .dot.li URL is recognisable. shawntabrizi.comshawntabrizi-com-byhq57.dot, example.comexample-com-uslj17.dot, shawntabrizi.github.ioshawntabrizi-github-io-<rand>NN. The transformation is intentionally dumb: lowercase, dots → hyphens, sanitise to [a-z0-9-], cap host segment at 30 chars, pad with random letters so the base length lands in DotNS's NoStatus classifier branch (baseLength >= 9 + exactly 2 trailing digits → no PoP required). No TLD or www. stripping — that requires the Public Suffix List, which we don't want as a dep, and users who want a clean name pass --dot=<name> explicitly. Falls back to decent-<rand>NN for unparseable inputs.
  • Latent bug fixed — the previous hex-based suffix produced labels with >2 trailing digits ~62 % of the time (when randomBytes.toString("hex") happened to end in a digit), silently masked by the retry loop as RESERVED. The new generator uses lowercase-only letters for the variable middle so the trailing-digit invariant always holds.
  • Net new prereqwget. Added to dot init (src/utils/toolchain.ts) so a fresh dot init provisions it alongside git/ipfs/foundry. Manual hint: brew install wget.

Surface added:

  • src/commands/decentralize/index.ts — CLI handler (telemetry spans for signer / availability / random-name / mirror / storage)
  • src/utils/decentralize/mirror.ts — wget wrapper + WgetMissingError, InvalidSiteUrlError
  • src/utils/decentralize/randomName.tsgenerateLabel(siteUrl?) + findAvailableRandomName({ siteUrl, ... }) with 20-attempt cap
  • src/utils/decentralize/randomName.test.ts — 15 invariant tests (hostname incorporation incl. www. and multi-segment public suffixes preserved verbatim, path-ignore, fallback, exactly 2 trailing digits across 200 iterations, NoStatus base length, length cap, normalizeDomain compatibility)
  • src/index.ts — wires the command
  • src/telemetry-config.ts — adds "decentralize" to the command tag union
  • src/utils/toolchain.ts — adds wget to TOOL_STEPS

Test plan

End-to-end verified manually on paseo-next-v2:

  • dot decentralize --site=example.com --suri //Bobexample-com-uslj17.dot → CID bafybeicpx… → live at https://example-com-uslj17.dot.li, generalised "Owned by a development account" footer
  • dot decentralize --site=shawntabrizi.com --suri //Bobshawntabrizi-com-byhq57.dot (hostname incorporated verbatim incl. -com, NoStatus shape, 4 random letters + 2 digits suffix)
  • dot decentralize --site=shawntabrizi.com --dot=shawntabrizi42 --suri //Bobshawntabrizi42.dot.li (custom NoStatus-compatible name, 12-char base + 2 trailing digits — bypasses URL-derived naming)
  • dot decentralize --site=shawntabrizi.com --dot=shawntabrizi --suri //Bob → refuses with the chain's "requires ProofOfPersonhoodFull" error (expected — availability.ts returns available + note for PoP-gated names; deploy fails at chain register)
  • dot decentralize --site=example.com (no --suri, no session) → no //Bob fallback; goes through getSessionSigner
  • pnpm format:check, pnpm lint:license, pnpm test src/commands/decentralize src/utils/decentralize src/utils/signer clean — 15 new tests for the label generator pass

TODOs / follow-ups

Required before merge:

  • Unit tests for mirror.ts — URL validation (http/https only, shorthand expansion, scheme rejection), WgetMissingError mapping from ENOENT, countFiles walks subdirs, empty-mirror failure path.
  • Changeset — per CLAUDE.md, every user-facing PR needs .changeset/<slug>.md or the release workflow no-ops. Patch-level bump is fine.
  • Pre-flight the PoP requirement before mirroring. Currently availability.ts returns available with an advisory note for PoP-gated names; dot decentralize proceeds to mirror the site, then the chain rejects at register(). Should short-circuit on the advisory note when the signer can't meet the requirement (e.g. NoStatus signer + Full-PoP name) so the user doesn't waste wget cycles. Only relevant when --dot is provided; URL-derived names always fit NoStatus by construction.

Nice-to-have:

  • Pass runStorageDeploy's phase events through to a real progress UI instead of dumping raw event.kind. Today the user sees phase-start, chunk-progress lines — readable, but coarser than dot deploy's Ink TUI.
  • --site smarts: auto-detect when the URL is a known SPA hosting host (Vercel/Netlify/Pages) and warn that client-side routing won't work without a 404 → index.html rewrite.
  • README usage example. Discoverable via dot --help, but a one-liner in the README's command table would help.
  • Better error ergonomics when no session exists. Today the no-signer path on a machine with partial ~/.polkadot/ state can surface publicKey: Uint8Array expected of length 32, not of length=0 from getSessionSigner instead of SignerNotAvailableError. Pre-existing — not caused by this PR — but more visible now that this command exposes the no-flag default. Worth a follow-up in auth.ts.

Out of scope but flagging:

  • Fresh-install build is broken on main. @polkadot-api/json-rpc-provider-proxy@0.4.0 (pulled in transitively via @novasamatech/product-sdk@0.7.9-4 per the pnpm.overrides pin) runtime-imports @polkadot-api/json-rpc-provider but declares it only as a devDependency. pnpm skips the install; bun build --compile and even bun run src/index.ts then fail with Cannot find module '@polkadot-api/json-rpc-provider'. One-line fix: add @polkadot-api/json-rpc-provider as a direct dep in package.json so it lands in the top-level node_modules/@polkadot-api/. First entered the lockfile in 582a15f (2026-04-15) — not introduced by this PR, but the PR's verification flow tripped over it.

Related

The prototype web service that wraps this CLI for a conference demo lives at shawntabrizi/dot-decentralize. It passes --suri=//Bob to every invocation so all served domains end up owned by the shared test account.

Before, `dot decentralize --site=example.com` picked a random
`decent-ffe0ab72.dot`. The label was unrecognisable in the resulting
`.dot.li` URL and gave the user no clue which site they were
mirroring.

After, the auto-name starts with a dumb transliteration of the URL's
hostname (lowercase, dots → hyphens, sanitised to `[a-z0-9-]`, capped
at 30 chars), followed by a 4-letter random tail + 2 digits. No TLD
or `www.` stripping — that requires the Public Suffix List, which we
don't want as a dep; users who want a clean name pass `--dot`
explicitly. Falls back to the legacy `decent-` shape for unparseable
inputs.

  example.com            → example-com-uslj17.dot
  shawntabrizi.com       → shawntabrizi-com-byhq57.dot
  shawntabrizi.github.io → shawntabrizi-github-io-<rand>NN.dot

Also fixes a latent classifier bug. The previous hex-based suffix
produced labels with >2 trailing digits ~62% of the time (whenever
the random hex happened to end in a digit), classified as RESERVED
and silently masked by the 20-attempt retry loop. The new generator
uses lowercase letters only in the variable middle so the trailing-
digit invariant holds for every call.

New test file covers the invariants across 200 iterations: exactly
2 trailing digits, base length ≥9 (NoStatus), normalizeDomain regex
compatibility, plus the hostname-incorporation cases.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant