import { PolkadotSigner } from 'polkadot-api'; import { DeployOptions } from '../deploy.js'; import { OwnershipResult } from '../dotns.js'; import { LoadedProductConfig } from './config-load.js'; import 'polkadot-api/ws'; import 'multiformats/cid'; import '../manifest.js'; import '../dotns-protocol.js'; import '../environments.js'; import '../errors.js'; import '../mnemonic.js'; import '../personhood/bootstrap.js'; import '../personhood/bind-personal-id.js'; import '../personhood/claim-pgas.js'; import '../personhood/bind-paid-alias.js'; import '../personhood/chain-prereqs.js'; import './types.js'; /** * Manifest publish orchestrator for RFC paritytech/triangle-js-sdks #0001 Steps 4 through 7. * * Wires [`storeFile`](../deploy.ts) and [`storeDirectory`](../deploy.ts) for * Bulletin uploads with [`DotNS`](../dotns.ts) for the on-chain text-record * writes. Each executable's contenthash and execution manifest are committed * atomically with `Utility.batch_all`, because either record without its * matching peer is not a valid launch contract. Root metadata and independent * executable kinds remain separate transactions; snapshot/rollback and the * Step 8 round-trip verification remain tracked follow-ups. */ interface PublishManifestOptions { /** Loaded + validated product config (call loadProductConfig first). */ loaded: LoadedProductConfig; /** Domain the legacy deploy targeted. Must match config.domain. */ domain: string; /** * Build-dir argument passed to the CLI plus the CID it produced. When the * resolved path of an executable in the config matches buildDir, we reuse * this CID instead of re-uploading the same bytes. */ buildDirCid?: { absPath: string; cid: string; }; /** * Env id (e.g. "paseo-next-v2"). Drives DotNS RPC + contract resolution * AND (#1094) the Bulletin endpoint the icon/executable uploads target — * both use the same resolved env, matching the legacy deploy. */ env?: string; /** Optional bulletin RPC override — same precedence as the legacy deploy's `--rpc` (#1094). */ rpc?: string; /** Required: signer mnemonic. */ mnemonic?: string; /** Optional Substrate-style derivation path. */ derivationPath?: string; /** Sign as the RFC-0022 product account, the same key deploy() registers the name with. */ productName?: string; /** * The Bulletin allowance-slot signer deploy() resolved for the content upload, handed over * in-process via DeployResult. Set when the deploy ran on a login session's own slot rather * than a mnemonic: without it this step would store the manifest from the shared dev pool * while the content it belongs to went to the user's slot, and on a chain whose pool holds * no quota the manifest would be the only part that fails. */ storageSigner?: PolkadotSigner; /** SS58 address of the slot account. Required when storageSigner is set. */ storageSignerAddress?: string; } interface PublishManifestResult { iconCid: string; executableCids: Record; textRecordsWritten: number; } /** * Publish a product manifest on top of an already-completed legacy deploy. * * Uploads the icon and any executables that aren't covered by `buildDirCid`, * then writes the root + per-executable text records on dotNS. Subnames * (`app|widget|funding|worker.`) are created on demand and pointed at the * content resolver before any `setText`. * * #1187: the whole body (icon/executable upload + DotNS writes) runs inside * `withManifestSpan`, which gives this phase its own Sentry root span. Without * it, `setDeployAttribute` calls here (and #1158's * `deploy.dotns.nonce_contention_retries`, which fires from inside * setContenthash/setTextRecord's shared-signer retry path) silently no-op — * `deployRootSpan` is null once `deploy()` has already returned, which it has * by the time bin/bulletin-deploy calls publishManifest. */ declare function publishManifest(opts: PublishManifestOptions): Promise; /** * #1187 perf win: `registerSubdomain` already sets the fresh subname's * resolver to the content resolver atomically (`setSubnodeOwner` + * `setResolver`, batched via `Utility.batch_all` — see dotns.ts * `registerSubdomain`). Calling `ensureContentResolver` again right after a * fresh register was therefore a wasted chain read on every executable of * every fresh deploy. Only the already-owned path still needs it — a * pre-existing subname can have a stale or unset resolver. * * Takes a minimal injectable `dotns`-like interface (rather than the * concrete `DotNS` class) purely so this branch can be unit-tested without a * live chain connection; `publishManifestBody` always calls it with a real * `DotNS` instance. */ interface RegisterOrEnsureResolverDeps { registerSubdomain(sublabel: string, parentLabel: string): Promise; ensureContentResolver(domainName: string): Promise<{ changed: boolean; }>; } declare function registerOrEnsureResolver(dotns: RegisterOrEnsureResolverDeps, ownership: OwnershipResult, execKind: string, baseLabel: string, domain: string): Promise<{ registered: boolean; }>; /** * The identity this step signs DotNS with AND stores its Bulletin bytes from — one resolution, * deliberately, because the two must never diverge: the icon and executables are paid for out of * the storage account's quota, and the manifest records are written by the name's owner. * * Mirrors what deploy() feeds selectStorageReconnect / resolveDotnsConnectOptions, so both * halves of a deploy resolve the same account: * - `productName` -> the RFC-0022 product account (see resolveProductSigner), which is what * deploy() stores content from too, since it swaps the signer before selecting storage. * - `storageSigner` -> the login session's Bulletin allowance slot, carried over from the * deploy that just ran. Kept alongside the signer fields rather than replacing them, * exactly as deploy() holds both, so selectStorageReconnect applies its own precedence. * - otherwise the mnemonic passthrough, unchanged. * * Call this ONCE per publish: resolveProductSigner logs "Product deployer: …" on every call. */ declare function manifestSignerOptions(opts: Pick): Pick; declare function domainMatchesEnvTld(domain: string, tld: string): boolean; /** * Reconcile a product config's `domain` against the domain the deploy is * actually targeting, before any chain or storage work runs. * * #1156: the only prior guard (`config.domain !== opts.domain`, a byte * compare) was toothless — bin/bulletin-deploy derived `opts.domain` FROM * the config, so the comparison was against a copy of itself. A config whose * `domain` field doesn't match the CLI's actual deploy target (e.g. a * hardcoded `bulletin-deploy.config.*` left over from a different PR/env) * slipped through untouched and only surfaced ~10 minutes later, mid- * manifest-publish, as an opaque `NotAuthorised()` revert from * `ensureContentResolver` — the signer doesn't own the OTHER name the config * still names (see the issue's dimspa evidence). * * `deployDomainArg` is documented as a BARE label (no TLD) since #1240, while * `configDomain` always carries a TLD (schema requires it), so the two * cannot be compared as raw strings even when they name the same target. * Both sides are normalized the same way, through `parseDomainName(x, * envTld).fullName`, which accepts either a bare label or a `.`-suffixed * one and independently rejects a DIFFERENT known TLD. `domainMatchesEnvTld` * (above) still runs first, against `configDomain` only, so a config * carrying the wrong environment's own suffix (e.g. "myapp.dot" against a * ".paseo" env) still fails — this does not weaken that check. * * Lowercased BEFORE normalizing, not after: `parseDomainName` delegates to * `validateDomainLabel`, whose charset is strictly lowercase-only (unlike the * config schema's case-insensitive `DOMAIN_RE`), so a case-insensitive * compare means folding case first — comparing post-throw is not an option. */ declare function reconcileManifestDomain(configDomain: string, deployDomainArg: string, envTld: string, sourcePath: string): void; export { type PublishManifestOptions, type PublishManifestResult, type RegisterOrEnsureResolverDeps, domainMatchesEnvTld, manifestSignerOptions, publishManifest, reconcileManifestDomain, registerOrEnsureResolver };