bulletin-deploy 0.7.12 → 0.7.13-rc.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -53,21 +53,49 @@ bulletin-deploy <build-dir> <domain.dot>
53
53
  Examples:
54
54
 
55
55
  ```bash
56
- # Basic deploy
56
+ # Basic deploy (defaults to --env paseo-next)
57
57
  bulletin-deploy ./dist my-app00.dot
58
58
 
59
+ # Pick a different environment
60
+ bulletin-deploy ./dist my-app00.dot --env paseo-review
61
+
62
+ # List supported environments
63
+ bulletin-deploy --list-environments
64
+
65
+ # Refresh the environments cache before deploying
66
+ bulletin-deploy ./dist my-app00.dot --refresh-environments --env paseo-next
67
+
59
68
  # Direct signer deploy
60
69
  bulletin-deploy ./dist my-app00.dot --mnemonic "..."
61
70
 
62
- # Custom Bulletin RPC
71
+ # Custom Bulletin RPC override (asset-hub still comes from --env)
63
72
  bulletin-deploy ./dist my-app00.dot --rpc wss://custom-bulletin.example.com
64
73
  ```
65
74
 
75
+ ### Selecting an environment
76
+
77
+ `--env <id>` selects a target environment by id. The list of environments and their RPC endpoints is sourced dynamically from [`paritytech/bulletin-deploy/assets/environments.json`](./assets/environments.json), which mirrors [`paritytech/triangle-status/environments.json`](https://github.com/paritytech/triangle-status/blob/main/environments.json) (the latter is private; bulletin-deploy serves as the public mirror).
78
+
79
+ | Env id | Network | Bulletin available? |
80
+ |---|---|---|
81
+ | `paseo-next` (default) | testnet | yes |
82
+ | `paseo-review` | testnet | yes |
83
+ | `preview` | testnet | yes |
84
+ | `polkadot` | mainnet | not yet |
85
+ | `kusama` | mainnet | not yet |
86
+
87
+ A single env id drives both the bulletin RPC and the asset-hub RPC used internally for DotNS, so they cannot drift. When you pass `--rpc`, it overrides only the bulletin endpoint within the chosen env; the asset-hub endpoint still comes from `--env`.
88
+
89
+ The runtime uses a 24-hour cache at `${XDG_CACHE_HOME:-~/.cache}/bulletin-deploy/environments.json`; `--refresh-environments` busts it and re-fetches. The npm tarball ships a bundled snapshot that is used when the live URL is unreachable.
90
+
66
91
  ### Options
67
92
 
68
93
  | Flag | What it does |
69
94
  |---|---|
70
- | `--rpc wss://...` | Override the Bulletin RPC endpoint. Also readable from `BULLETIN_RPC`. |
95
+ | `--env <id>` | Target environment. Default: `paseo-next`. See `--list-environments` for valid ids. |
96
+ | `--list-environments` | Print the environments table and exit. |
97
+ | `--refresh-environments` | Bust the cache and re-fetch environments.json. Composes with `--env` (refresh-then-deploy) or runs solo. |
98
+ | `--rpc wss://...` | Override the Bulletin RPC endpoint within the chosen `--env`. Also readable from `BULLETIN_RPC`. |
71
99
  | `--mnemonic "..."` | Use a specific mnemonic as the direct signer for Bulletin uploads and DotNS updates. Also readable from `MNEMONIC`. |
72
100
  | `--derivation-path "..."` | Apply a Substrate derivation path to `--mnemonic`, for example `//deploy/3`. |
73
101
  | `--pool-size N` | Change the number of derived pool accounts available for pool-mode Bulletin uploads. Default: `10`. |
@@ -138,9 +166,11 @@ await deploy("./dist", "my-app00.dot", { jsMerkle: true });
138
166
 
139
167
  | Variable | Default | Description |
140
168
  |---|---|---|
141
- | `BULLETIN_RPC` | `wss://paseo-bulletin-rpc.polkadot.io` | Bulletin chain WebSocket RPC |
169
+ | `BULLETIN_RPC` | `wss://paseo-bulletin-rpc.polkadot.io` | Override the Bulletin chain WebSocket RPC for the chosen `--env`. |
170
+ | `BULLETIN_ENVIRONMENTS_URL` | bulletin-deploy public mirror | Override the runtime URL for environments.json. Internal teams point this at a fork or local proxy. |
142
171
  | `BULLETIN_DEPLOY_TELEMETRY` | off for external users, on for internal users | `1` to opt in, `0` to force off |
143
172
  | `BULLETIN_DEPLOY_UPDATE_CHECK` | `1` | Set to `0` to disable version checks on failure |
173
+ | `BULLETIN_DEPLOY_DOTNS_CLI` | bundled `@parity/dotns-cli`, then host dispatch | Set to `host` for `process.execPath dotns ...` dispatch in bundled host CLIs, or to a `dotns` command/path |
144
174
  | `DOTNS_STATUS` | `full` on testnet, `none` on mainnet | PoP level to self-grant before registration: `none`, `lite`, or `full` |
145
175
  | `IPFS_CID` | unset | Skip storage and reuse an existing CID |
146
176
  | `DEPLOY_TAG` | unset | Telemetry label equivalent to `--tag` |
@@ -0,0 +1,238 @@
1
+ {
2
+ "environments": [
3
+ {
4
+ "id": "preview",
5
+ "name": "Preview",
6
+ "network": "testnet",
7
+ "description": "Product Preview net, used by Product Teams",
8
+ "backend": "https://polkadot-app-stg.parity.io/",
9
+ "ipfs": "https://previewnet.substrate.dev/ipfs/"
10
+ },
11
+ {
12
+ "id": "paseo-next",
13
+ "name": "Paseo Next",
14
+ "network": "testnet",
15
+ "badge": "Testflight",
16
+ "description": "Production testnet for Polkadot App Testflight",
17
+ "backend": "https://identity-backend.parity-testnet.parity.io",
18
+ "ipfs": "https://paseo-ipfs.polkadot.io/ipfs",
19
+ "uptimeUrl": "https://stats.uptimerobot.com/LISM1LRA8m"
20
+ },
21
+ {
22
+ "id": "paseo-review",
23
+ "name": "Paseo Review",
24
+ "network": "testnet",
25
+ "description": "Used for App Store review builds",
26
+ "backend": "https://identity-backend-review.parity-testnet.parity.io",
27
+ "ipfs": "https://paseo-bulletin-review-ipfs.polkadot.io",
28
+ "uptimeUrl": "https://stats.uptimerobot.com/bckl1cu0AO"
29
+ },
30
+ {
31
+ "id": "polkadot",
32
+ "name": "Polkadot",
33
+ "network": "mainnet",
34
+ "description": "Polkadot mainnet"
35
+ },
36
+ {
37
+ "id": "kusama",
38
+ "name": "Kusama",
39
+ "network": "mainnet",
40
+ "description": "Kusama canary network"
41
+ }
42
+ ],
43
+ "chains": [
44
+ {
45
+ "id": "relay",
46
+ "name": "Relay",
47
+ "endpoints": {
48
+ "preview": {
49
+ "wss": "wss://previewnet.substrate.dev/relay/alice"
50
+ },
51
+ "paseo-next": {
52
+ "wss": "wss://paseo-rpc.n.dwellir.com",
53
+ "uptimeUrl": "https://stats.uptimerobot.com/UrEXbl6Xyt"
54
+ },
55
+ "paseo-review": {
56
+ "wss": "wss://paseo-rpc.n.dwellir.com",
57
+ "uptimeUrl": "https://stats.uptimerobot.com/UrEXbl6Xyt"
58
+ },
59
+ "polkadot": {
60
+ "wss": [
61
+ "wss://polkadot-rpc.n.dwellir.com",
62
+ "wss://polkadot.ibp.network",
63
+ "wss://polkadot.dotters.network",
64
+ "wss://rpc.polkadot.io"
65
+ ]
66
+ },
67
+ "kusama": {
68
+ "wss": [
69
+ "wss://kusama-rpc.n.dwellir.com",
70
+ "wss://kusama.ibp.network",
71
+ "wss://kusama.dotters.network",
72
+ "wss://kusama-rpc.polkadot.io"
73
+ ]
74
+ }
75
+ }
76
+ },
77
+ {
78
+ "id": "asset-hub",
79
+ "name": "Asset Hub",
80
+ "endpoints": {
81
+ "preview": {
82
+ "wss": "wss://previewnet.substrate.dev/asset-hub",
83
+ "parachainId": 1000
84
+ },
85
+ "paseo-next": {
86
+ "wss": "wss://asset-hub-paseo-rpc.n.dwellir.com",
87
+ "parachainId": 1000,
88
+ "uptimeUrl": "https://stats.uptimerobot.com/UrEXbl6Xyt"
89
+ },
90
+ "paseo-review": {
91
+ "wss": "wss://asset-hub-paseo-rpc.n.dwellir.com",
92
+ "parachainId": 1000,
93
+ "uptimeUrl": "https://stats.uptimerobot.com/UrEXbl6Xyt"
94
+ },
95
+ "polkadot": {
96
+ "wss": [
97
+ "wss://asset-hub-polkadot-rpc.n.dwellir.com",
98
+ "wss://asset-hub-polkadot.ibp.network",
99
+ "wss://asset-hub-polkadot.dotters.network",
100
+ "wss://polkadot-asset-hub-rpc.polkadot.io"
101
+ ],
102
+ "parachainId": 1000
103
+ },
104
+ "kusama": {
105
+ "wss": [
106
+ "wss://asset-hub-kusama-rpc.n.dwellir.com",
107
+ "wss://asset-hub-kusama.ibp.network",
108
+ "wss://asset-hub-kusama.dotters.network",
109
+ "wss://kusama-asset-hub-rpc.polkadot.io"
110
+ ],
111
+ "parachainId": 1000
112
+ }
113
+ }
114
+ },
115
+ {
116
+ "id": "bridge-hub",
117
+ "name": "Bridge Hub",
118
+ "endpoints": {
119
+ "polkadot": {
120
+ "wss": [
121
+ "wss://bridge-hub-polkadot-rpc.n.dwellir.com",
122
+ "wss://bridge-hub-polkadot.ibp.network",
123
+ "wss://bridge-hub-polkadot.dotters.network",
124
+ "wss://polkadot-bridge-hub-rpc.polkadot.io"
125
+ ],
126
+ "parachainId": 1002
127
+ },
128
+ "kusama": {
129
+ "wss": [
130
+ "wss://bridge-hub-kusama-rpc.n.dwellir.com",
131
+ "wss://bridge-hub-kusama.ibp.network",
132
+ "wss://bridge-hub-kusama.dotters.network",
133
+ "wss://kusama-bridge-hub-rpc.polkadot.io"
134
+ ],
135
+ "parachainId": 1002
136
+ }
137
+ }
138
+ },
139
+ {
140
+ "id": "collectives",
141
+ "name": "Collectives",
142
+ "endpoints": {
143
+ "polkadot": {
144
+ "wss": [
145
+ "wss://collectives-polkadot-rpc.n.dwellir.com",
146
+ "wss://collectives-polkadot.ibp.network",
147
+ "wss://collectives-polkadot.dotters.network",
148
+ "wss://polkadot-collectives-rpc.polkadot.io"
149
+ ],
150
+ "parachainId": 1001
151
+ }
152
+ }
153
+ },
154
+ {
155
+ "id": "coretime",
156
+ "name": "Coretime",
157
+ "endpoints": {
158
+ "polkadot": {
159
+ "wss": [
160
+ "wss://coretime-polkadot-rpc.n.dwellir.com",
161
+ "wss://coretime-polkadot.ibp.network",
162
+ "wss://coretime-polkadot.dotters.network",
163
+ "wss://polkadot-coretime-rpc.polkadot.io"
164
+ ],
165
+ "parachainId": 1005
166
+ },
167
+ "kusama": {
168
+ "wss": [
169
+ "wss://coretime-kusama-rpc.n.dwellir.com",
170
+ "wss://coretime-kusama.ibp.network",
171
+ "wss://coretime-kusama.dotters.network",
172
+ "wss://kusama-coretime-rpc.polkadot.io"
173
+ ],
174
+ "parachainId": 1005
175
+ }
176
+ }
177
+ },
178
+ {
179
+ "id": "people",
180
+ "name": "People",
181
+ "endpoints": {
182
+ "preview": {
183
+ "wss": "wss://previewnet.substrate.dev/people",
184
+ "parachainId": 1004
185
+ },
186
+ "paseo-next": {
187
+ "wss": "wss://paseo-people-next-rpc.polkadot.io",
188
+ "parachainId": 5140,
189
+ "uptimeUrl": "https://stats.uptimerobot.com/LISM1LRA8m"
190
+ },
191
+ "paseo-review": {
192
+ "wss": "wss://paseo-people-review-rpc.polkadot.io",
193
+ "parachainId": 5167,
194
+ "uptimeUrl": "https://stats.uptimerobot.com/bckl1cu0AO"
195
+ },
196
+ "polkadot": {
197
+ "wss": [
198
+ "wss://people-polkadot-rpc.n.dwellir.com",
199
+ "wss://people-polkadot.ibp.network",
200
+ "wss://people-polkadot.dotters.network",
201
+ "wss://polkadot-people-rpc.polkadot.io"
202
+ ],
203
+ "parachainId": 1004
204
+ },
205
+ "kusama": {
206
+ "wss": [
207
+ "wss://people-kusama-rpc.n.dwellir.com",
208
+ "wss://people-kusama.ibp.network",
209
+ "wss://people-kusama.dotters.network",
210
+ "wss://kusama-people-rpc.polkadot.io"
211
+ ],
212
+ "parachainId": 1004
213
+ }
214
+ }
215
+ },
216
+ {
217
+ "id": "bulletin",
218
+ "name": "Bulletin",
219
+ "endpoints": {
220
+ "preview": {
221
+ "wss": "wss://previewnet.substrate.dev/bulletin",
222
+ "parachainId": 2487
223
+ },
224
+ "paseo-next": {
225
+ "wss": "wss://paseo-bulletin-rpc.polkadot.io",
226
+ "parachainId": 5118,
227
+ "uptimeUrl": "https://stats.uptimerobot.com/LISM1LRA8m"
228
+ },
229
+ "paseo-review": {
230
+ "wss": "wss://paseo-bulletin-review-rpc.polkadot.io",
231
+ "parachainId": 5168,
232
+ "uptimeUrl": "https://stats.uptimerobot.com/bckl1cu0AO"
233
+ }
234
+ }
235
+ }
236
+ ],
237
+ "source": "https://docs.google.com/document/d/1xQoAmWDpbjhuXKT79DTNKFzv7ZkFr5npjo0BvrxXDIw"
238
+ }
@@ -5,6 +5,7 @@ import { VERSION, setDeployAttribute, captureWarning, closeTelemetry, setRunStat
5
5
  import { handleFailedDeploy, preReleaseWarning } from "../dist/version-check.js";
6
6
  import { setDeployContext, installLogCapture, buildCliFlagsSummary } from "../dist/bug-report.js";
7
7
  import { loadRunState, writeRunState, shouldSkipStaleWarning, shouldShowOomHint, probablyOomRssMb } from "../dist/run-state.js";
8
+ import { loadEnvironments, listEnvironments, formatEnvironmentTable, DEFAULT_ENV_ID, DEFAULT_ENVIRONMENTS_URL } from "../dist/environments.js";
8
9
  import * as fs from "fs";
9
10
 
10
11
  // Install early so anything printed during flag parsing / preflight is
@@ -21,6 +22,9 @@ for (let i = 0; i < args.length; i++) {
21
22
  else if (args[i] === "--mnemonic") { flags.mnemonic = args[++i]; }
22
23
  else if (args[i] === "--derivation-path") { flags.derivationPath = args[++i]; }
23
24
  else if (args[i] === "--rpc") { flags.rpc = args[++i]; }
25
+ else if (args[i] === "--env") { flags.env = args[++i]; }
26
+ else if (args[i] === "--list-environments") { flags.listEnvironments = true; }
27
+ else if (args[i] === "--refresh-environments") { flags.refreshEnvironments = true; }
24
28
  else if (args[i] === "--password") { flags.password = args[++i]; }
25
29
  else if (args[i] === "--js-merkle") { flags.jsMerkle = true; }
26
30
  else if (args[i] === "--tag") { flags.tag = args[++i]; }
@@ -42,6 +46,32 @@ if (flags.removedBootstrap) {
42
46
  process.exit(1);
43
47
  }
44
48
 
49
+ // --list-environments: print the environments table and exit. Composes with
50
+ // nothing else (deploy positional args are ignored).
51
+ if (flags.listEnvironments) {
52
+ try {
53
+ const { doc } = await loadEnvironments();
54
+ console.log(formatEnvironmentTable(listEnvironments(doc)));
55
+ process.exit(0);
56
+ } catch (e) {
57
+ console.error(`Error: failed to load environments: ${e?.message ?? e}`);
58
+ process.exit(1);
59
+ }
60
+ }
61
+
62
+ // --refresh-environments alone (no positional args): refresh and exit. With
63
+ // positional args, refresh first, then deploy.
64
+ if (flags.refreshEnvironments && positional.length === 0) {
65
+ try {
66
+ const { doc } = await loadEnvironments({ forceRefresh: true });
67
+ console.log(`Refreshed ${doc.environments.length} environments from ${process.env.BULLETIN_ENVIRONMENTS_URL ?? DEFAULT_ENVIRONMENTS_URL}`);
68
+ process.exit(0);
69
+ } catch (e) {
70
+ console.error(`Error: failed to refresh environments: ${e?.message ?? e}`);
71
+ process.exit(1);
72
+ }
73
+ }
74
+
45
75
  if (flags.help || positional.length === 0) {
46
76
  console.log(`bulletin-deploy v${VERSION}
47
77
 
@@ -49,9 +79,16 @@ Usage:
49
79
  bulletin-deploy <build-dir> <domain.dot> Deploy an app
50
80
 
51
81
  Options:
82
+ --env <id> Target environment from environments.json (default: paseo-next).
83
+ Drives both the bulletin RPC and the asset-hub RPC used
84
+ by DotNS. See --list-environments for valid ids.
85
+ --list-environments Print the environments table and exit.
86
+ --refresh-environments Bust the cache and re-fetch environments.json. Composes
87
+ with --env (refresh-then-deploy) or runs solo.
52
88
  --mnemonic "..." DotNS owner mnemonic (or set MNEMONIC env var)
53
89
  --derivation-path "..." Optional Substrate-style path applied to --mnemonic (e.g. //deploy/3)
54
- --rpc wss://... Bulletin RPC (or set BULLETIN_RPC env var)
90
+ --rpc wss://... Override the bulletin RPC for the chosen --env (or set BULLETIN_RPC).
91
+ Precedence: --rpc > BULLETIN_RPC > --env's bulletin endpoint.
55
92
  --pool-size N Number of pool accounts (default: 10)
56
93
  --password "..." Encrypt SPA content (users will be prompted to decrypt)
57
94
  --js-merkle Use pure-JS merkleization (no IPFS Kubo binary required)
@@ -159,6 +196,16 @@ try {
159
196
  if (!domain) { console.error("Error: domain required (e.g. my-app.dot)"); process.exit(1); }
160
197
  if (!fs.existsSync(buildDir)) { console.error(`Error: ${buildDir} does not exist`); process.exit(1); }
161
198
 
199
+ // --refresh-environments composed with a deploy: bust the cache before
200
+ // deploy() loads it, so deploy() picks up fresh data on the next loadEnvironments().
201
+ if (flags.refreshEnvironments) {
202
+ try {
203
+ await loadEnvironments({ forceRefresh: true });
204
+ } catch (e) {
205
+ console.error(`Error: failed to refresh environments: ${e?.message ?? e}`);
206
+ process.exit(1);
207
+ }
208
+ }
162
209
  const effectiveRpc = flags.rpc ?? process.env.BULLETIN_RPC ?? DEFAULT_BULLETIN_RPC;
163
210
  const deployTag = flags.tag ?? process.env.DEPLOY_TAG;
164
211
  const ci = process.env.GITHUB_ACTIONS === "true" ? {
@@ -184,6 +231,7 @@ try {
184
231
  mnemonic: flags.mnemonic,
185
232
  derivationPath: flags.derivationPath,
186
233
  rpc: flags.rpc,
234
+ env: flags.env,
187
235
  poolSize: flags.poolSize,
188
236
  password: flags.password,
189
237
  jsMerkle: flags.jsMerkle,
@@ -9,10 +9,10 @@ import {
9
9
  offerBugReport,
10
10
  scrubSecrets,
11
11
  setDeployContext
12
- } from "./chunk-N2OZLFJA.js";
13
- import "./chunk-RVCY2VEY.js";
14
- import "./chunk-LOZ5PSD3.js";
15
- import "./chunk-VQJLGE5J.js";
12
+ } from "./chunk-R6OJVSKS.js";
13
+ import "./chunk-C7TSB3W6.js";
14
+ import "./chunk-QCJSRJRK.js";
15
+ import "./chunk-HA3QKMVJ.js";
16
16
  import "./chunk-QGM4M3NI.js";
17
17
  export {
18
18
  buildCliFlagsSummary,
@@ -0,0 +1,266 @@
1
+ import {
2
+ NonRetryableError
3
+ } from "./chunk-ZOC4GITL.js";
4
+
5
+ // src/environments.ts
6
+ import * as fs from "fs/promises";
7
+ import * as path from "path";
8
+ import * as os from "os";
9
+ import { fileURLToPath } from "url";
10
+ var DEFAULT_ENVIRONMENTS_URL = "https://raw.githubusercontent.com/paritytech/bulletin-deploy/main/assets/environments.json";
11
+ var DEFAULT_ENV_ID = "paseo-next";
12
+ var CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
13
+ var FETCH_TIMEOUT_MS = 5e3;
14
+ var HARDCODED_FALLBACK = {
15
+ environments: [
16
+ {
17
+ id: "paseo-next",
18
+ name: "Paseo Next",
19
+ network: "testnet",
20
+ description: "Production testnet for Polkadot App Testflight (hardcoded fallback)"
21
+ }
22
+ ],
23
+ chains: [
24
+ {
25
+ id: "bulletin",
26
+ name: "Bulletin",
27
+ endpoints: {
28
+ "paseo-next": {
29
+ wss: "wss://paseo-bulletin-rpc.polkadot.io",
30
+ parachainId: 5118
31
+ }
32
+ }
33
+ },
34
+ {
35
+ id: "asset-hub",
36
+ name: "Asset Hub",
37
+ endpoints: {
38
+ "paseo-next": {
39
+ wss: [
40
+ "wss://asset-hub-paseo.dotters.network",
41
+ "wss://sys.ibp.network/asset-hub-paseo",
42
+ "wss://pas-rpc.stakeworld.io/assethub"
43
+ ],
44
+ parachainId: 1e3
45
+ }
46
+ }
47
+ }
48
+ ]
49
+ };
50
+ function defaultCacheDir() {
51
+ const xdg = process.env.XDG_CACHE_HOME;
52
+ const base = xdg && xdg.length > 0 ? xdg : path.join(os.homedir(), ".cache");
53
+ return path.join(base, "bulletin-deploy");
54
+ }
55
+ function defaultCachePath() {
56
+ return path.join(defaultCacheDir(), "environments.json");
57
+ }
58
+ function defaultBundledPath() {
59
+ return fileURLToPath(new URL("../assets/environments.json", import.meta.url));
60
+ }
61
+ function isValidDoc(value) {
62
+ if (!value || typeof value !== "object") return false;
63
+ const v = value;
64
+ return Array.isArray(v.environments) && Array.isArray(v.chains);
65
+ }
66
+ async function readCache(cachePath) {
67
+ try {
68
+ const raw = await fs.readFile(cachePath, "utf8");
69
+ const parsed = JSON.parse(raw);
70
+ if (parsed && typeof parsed === "object" && typeof parsed.fetchedAt === "string" && isValidDoc(parsed.doc)) {
71
+ return parsed;
72
+ }
73
+ return null;
74
+ } catch {
75
+ return null;
76
+ }
77
+ }
78
+ async function writeCache(cachePath, doc, now) {
79
+ try {
80
+ await fs.mkdir(path.dirname(cachePath), { recursive: true });
81
+ const payload = {
82
+ fetchedAt: new Date(now).toISOString(),
83
+ doc
84
+ };
85
+ await fs.writeFile(cachePath, JSON.stringify(payload, null, 2), "utf8");
86
+ } catch {
87
+ }
88
+ }
89
+ function isFresh(cache, now) {
90
+ const fetched = Date.parse(cache.fetchedAt);
91
+ if (Number.isNaN(fetched)) return false;
92
+ return now - fetched < CACHE_TTL_MS;
93
+ }
94
+ async function fetchLive(url, fetchImpl) {
95
+ const ctrl = new AbortController();
96
+ const timer = setTimeout(() => ctrl.abort(), FETCH_TIMEOUT_MS);
97
+ try {
98
+ const res = await fetchImpl(url, { signal: ctrl.signal });
99
+ if (!res.ok) {
100
+ throw new Error(`fetch ${url} returned HTTP ${res.status}`);
101
+ }
102
+ const json = await res.json();
103
+ if (!isValidDoc(json)) {
104
+ throw new Error(`fetch ${url} returned malformed environments doc`);
105
+ }
106
+ return json;
107
+ } finally {
108
+ clearTimeout(timer);
109
+ }
110
+ }
111
+ async function readBundled(bundledPath) {
112
+ try {
113
+ const raw = await fs.readFile(bundledPath, "utf8");
114
+ const parsed = JSON.parse(raw);
115
+ return isValidDoc(parsed) ? parsed : null;
116
+ } catch {
117
+ return null;
118
+ }
119
+ }
120
+ async function loadEnvironments(opts = {}) {
121
+ const url = opts.url ?? process.env.BULLETIN_ENVIRONMENTS_URL ?? DEFAULT_ENVIRONMENTS_URL;
122
+ const cachePath = path.join(opts.cacheDir ?? defaultCacheDir(), "environments.json");
123
+ const bundledPath = opts.bundledPath ?? defaultBundledPath();
124
+ const fetchImpl = opts.fetchImpl ?? fetch;
125
+ const nowFn = opts.now ?? Date.now;
126
+ const warn = opts.warn ?? ((msg) => console.error(msg));
127
+ const capture = opts.capture ?? (() => {
128
+ });
129
+ const now = nowFn();
130
+ if (opts.forceRefresh) {
131
+ const doc = await fetchLive(url, fetchImpl);
132
+ await writeCache(cachePath, doc, now);
133
+ return { doc, source: "live" };
134
+ }
135
+ const cache = await readCache(cachePath);
136
+ if (cache && isFresh(cache, now)) {
137
+ return { doc: cache.doc, source: "cache-fresh" };
138
+ }
139
+ try {
140
+ const doc = await fetchLive(url, fetchImpl);
141
+ await writeCache(cachePath, doc, now);
142
+ return { doc, source: "live" };
143
+ } catch (err) {
144
+ if (cache) {
145
+ warn(
146
+ `bulletin-deploy: live environments.json fetch failed; using cached copy from ${cache.fetchedAt} (${err.message ?? err})`
147
+ );
148
+ return { doc: cache.doc, source: "cache-stale" };
149
+ }
150
+ const bundled = await readBundled(bundledPath);
151
+ if (bundled) {
152
+ warn(
153
+ `bulletin-deploy: live environments.json fetch failed and no cache; using bundled snapshot (${err.message ?? err})`
154
+ );
155
+ return { doc: bundled, source: "bundled" };
156
+ }
157
+ capture(err);
158
+ warn(
159
+ `bulletin-deploy: live and bundled environments.json both unavailable; using hardcoded paseo-next fallback`
160
+ );
161
+ return { doc: HARDCODED_FALLBACK, source: "hardcoded-fallback" };
162
+ }
163
+ }
164
+ function normalizeWss(value) {
165
+ if (value === void 0) return [];
166
+ return Array.isArray(value) ? value.slice() : [value];
167
+ }
168
+ function levenshtein(a, b) {
169
+ if (a === b) return 0;
170
+ const m = a.length, n = b.length;
171
+ if (m === 0) return n;
172
+ if (n === 0) return m;
173
+ let prev = new Array(n + 1);
174
+ let curr = new Array(n + 1);
175
+ for (let j = 0; j <= n; j++) prev[j] = j;
176
+ for (let i = 1; i <= m; i++) {
177
+ curr[0] = i;
178
+ for (let j = 1; j <= n; j++) {
179
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
180
+ curr[j] = Math.min(curr[j - 1] + 1, prev[j] + 1, prev[j - 1] + cost);
181
+ }
182
+ [prev, curr] = [curr, prev];
183
+ }
184
+ return prev[n];
185
+ }
186
+ function suggestEnv(envId, doc) {
187
+ let best = null;
188
+ for (const env of doc.environments) {
189
+ const d = levenshtein(envId.toLowerCase(), env.id.toLowerCase());
190
+ if (best === null || d < best.dist) best = { id: env.id, dist: d };
191
+ }
192
+ if (best && best.dist <= Math.max(2, Math.floor(envId.length / 3))) {
193
+ return best.id;
194
+ }
195
+ return null;
196
+ }
197
+ function resolveEndpoints(doc, envId) {
198
+ const env = doc.environments.find((e) => e.id === envId);
199
+ if (!env) {
200
+ const valid = doc.environments.map((e) => e.id).join(", ");
201
+ const hint = suggestEnv(envId, doc);
202
+ const suffix = hint ? ` Did you mean '${hint}'?` : "";
203
+ throw new NonRetryableError(
204
+ `Unknown environment '${envId}'. Valid: ${valid}.${suffix}`
205
+ );
206
+ }
207
+ const bulletinChain = doc.chains.find((c) => c.id === "bulletin");
208
+ const assetHubChain = doc.chains.find((c) => c.id === "asset-hub");
209
+ const bulletin = normalizeWss(bulletinChain?.endpoints?.[envId]?.wss);
210
+ const assetHub = normalizeWss(assetHubChain?.endpoints?.[envId]?.wss);
211
+ if (bulletin.length === 0) {
212
+ throw new NonRetryableError(
213
+ `Bulletin chain not yet available on environment '${envId}'. The selected environment has no bulletin endpoint in https://github.com/paritytech/triangle-status/blob/main/environments.json. Pick a testnet (paseo-next, paseo-review, preview) or wait for the mainnet rollout.`
214
+ );
215
+ }
216
+ if (assetHub.length === 0) {
217
+ throw new NonRetryableError(
218
+ `Asset Hub endpoint missing for environment '${envId}'. Check https://github.com/paritytech/triangle-status/blob/main/environments.json.`
219
+ );
220
+ }
221
+ return { bulletin, assetHub, network: env.network, envName: env.name };
222
+ }
223
+ function listEnvironments(doc) {
224
+ const bulletinChain = doc.chains.find((c) => c.id === "bulletin");
225
+ return doc.environments.map((env) => {
226
+ const ep = bulletinChain?.endpoints?.[env.id];
227
+ const hasBulletin = !!ep && normalizeWss(ep.wss).length > 0;
228
+ return {
229
+ id: env.id,
230
+ name: env.name,
231
+ network: env.network,
232
+ hasBulletin,
233
+ description: env.description ?? ""
234
+ };
235
+ });
236
+ }
237
+ function formatEnvironmentTable(rows) {
238
+ const headers = ["ID", "Name", "Network", "Bulletin?", "Description"];
239
+ const data = rows.map((r) => [
240
+ r.id,
241
+ r.name,
242
+ r.network,
243
+ r.hasBulletin ? "yes" : "no",
244
+ r.description
245
+ ]);
246
+ const widths = headers.map(
247
+ (h, i) => Math.max(h.length, ...data.map((row) => row[i].length))
248
+ );
249
+ const fmtRow = (row) => row.map((cell, i) => cell.padEnd(widths[i])).join(" ");
250
+ const sep = widths.map((w) => "-".repeat(w)).join(" ");
251
+ return [fmtRow(headers), sep, ...data.map(fmtRow)].join("\n");
252
+ }
253
+
254
+ export {
255
+ DEFAULT_ENVIRONMENTS_URL,
256
+ DEFAULT_ENV_ID,
257
+ CACHE_TTL_MS,
258
+ FETCH_TIMEOUT_MS,
259
+ defaultCacheDir,
260
+ defaultCachePath,
261
+ defaultBundledPath,
262
+ loadEnvironments,
263
+ resolveEndpoints,
264
+ listEnvironments,
265
+ formatEnvironmentTable
266
+ };