bulletin-deploy 0.6.6 → 0.6.7

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
@@ -15,7 +15,7 @@ Your site is live at `https://my-app00.dot.li`
15
15
  ## Prerequisites
16
16
 
17
17
  - **Node.js 22+**
18
- - **IPFS Kubo** (for merkleizing directories)
18
+ - **IPFS Kubo** (for merkleizing directories — not needed with `--js-merkle`)
19
19
 
20
20
  ```bash
21
21
  # macOS
@@ -29,6 +29,8 @@ sudo bash kubo/install.sh
29
29
  ipfs init
30
30
  ```
31
31
 
32
+ > **No Kubo?** Use `--js-merkle` to skip the Kubo requirement entirely. See [Merkleization modes](#merkleization-modes) below.
33
+
32
34
  ## CLI Usage
33
35
 
34
36
  ```bash
@@ -55,6 +57,7 @@ Options:
55
57
  --rpc wss://... Bulletin RPC (or set BULLETIN_RPC env var)
56
58
  --mnemonic "..." DotNS owner mnemonic (or set MNEMONIC env var)
57
59
  --playground Publish to the Playground remix registry
60
+ --js-merkle Use pure-JS merkleization (no IPFS Kubo binary required)
58
61
  --pool-size N Number of pool accounts (default: 10)
59
62
  --help Show help
60
63
  ```
@@ -92,6 +95,20 @@ const result = await deploy("./dist", "my-app00.dot");
92
95
  console.log(result.cid, result.domainName);
93
96
  ```
94
97
 
98
+ ### JS merkleization and custom telemetry
99
+
100
+ For environments without Kubo (WebContainers, serverless), use `jsMerkle`. The optional `attributes` field lets you inject telemetry context when git is unavailable:
101
+
102
+ ```javascript
103
+ const result = await deploy("./dist", "my-app00.dot", {
104
+ jsMerkle: true,
105
+ attributes: {
106
+ "deploy.source": "revx",
107
+ "deploy.repo": "user/project",
108
+ },
109
+ });
110
+ ```
111
+
95
112
  ## Domain Names and Proof of Personhood
96
113
 
97
114
  DotNS domain names are classified by the PopOracle contract on Asset Hub. The classification determines what level of **Proof of Personhood (PoP)** is required to register:
@@ -124,18 +141,53 @@ If you see **"Requires Full Personhood verification"**, the deploy will fail ear
124
141
  ## How It Works
125
142
 
126
143
  ```
127
- Build output ──> IPFS merkleize ──> CAR file ──> Chunk upload ──> DotNS
128
- ./dist ipfs add .car Bulletin Asset Hub
129
- Storage Registry
144
+ Build output ──> Merkleize ──> CAR file ──> Chunk upload ──> DotNS
145
+ ./dist (Kubo or JS) .car Bulletin Asset Hub
146
+ Storage Registry
130
147
  ```
131
148
 
132
- 1. **Merkleize** your build directory with IPFS to produce a content-addressed CAR file
149
+ 1. **Merkleize** your build directory to produce a content-addressed CAR file
133
150
  2. **Chunk and upload** the CAR file to Bulletin's TransactionStorage (1MB chunks, 2 per batch)
134
151
  3. **Store the DAG root** that links all chunks together under a single CID
135
152
  4. **Register or update** your `.dot` domain on Asset Hub with the new contenthash
136
153
 
137
154
  Your site is immediately accessible at `https://your-domain.dot.li`
138
155
 
156
+ ## Merkleization modes
157
+
158
+ bulletin-deploy supports two ways to merkleize your build directory into a CAR file:
159
+
160
+ | Mode | Flag | Requires | Best for |
161
+ |---|---|---|---|
162
+ | **Kubo** (default) | _(none)_ | IPFS Kubo binary installed | CI pipelines, local development |
163
+ | **JS** | `--js-merkle` | Nothing beyond Node.js | WebContainers, serverless, environments without system binaries |
164
+
165
+ Both modes produce valid IPFS UnixFS DAGs. The CIDs may differ between modes for the same input (different chunking implementations), but this is fine — each deploy sets a fresh contenthash on DotNS regardless.
166
+
167
+ ### CLI
168
+
169
+ ```bash
170
+ # Default (Kubo)
171
+ bulletin-deploy ./dist my-app00.dot
172
+
173
+ # JS merkleization
174
+ bulletin-deploy --js-merkle ./dist my-app00.dot
175
+ ```
176
+
177
+ ### Programmatic
178
+
179
+ ```javascript
180
+ import { deploy } from "bulletin-deploy";
181
+
182
+ // Default (Kubo)
183
+ await deploy("./dist", "my-app00.dot");
184
+
185
+ // JS merkleization
186
+ await deploy("./dist", "my-app00.dot", { jsMerkle: true });
187
+ ```
188
+
189
+ The JS mode uses `ipfs-unixfs-importer` (the same chunker Kubo uses internally) and `@ipld/car` for CAR serialization. It runs entirely in-memory with no temp files.
190
+
139
191
  ## Resilience Features
140
192
 
141
193
  ### Chunk-level retry
@@ -161,7 +213,9 @@ What's tracked:
161
213
  - Pool account selection
162
214
  - Source metadata (repo, branch, PR number, CI vs local)
163
215
 
164
- Dashboard: https://polkadot-community-foundation.sentry.io/dashboards/92523/
216
+ Dashboard:
217
+ - Bulletin Deploy Health: https://paritytech.sentry.io/dashboard/1669817/
218
+ - Deploy Failures Detail: https://paritytech.sentry.io/dashboard/1669818/
165
219
 
166
220
  ## Troubleshooting
167
221
 
@@ -16,6 +16,7 @@ for (let i = 0; i < args.length; i++) {
16
16
  else if (args[i] === "--rpc") { flags.rpc = args[++i]; }
17
17
  else if (args[i] === "--password") { flags.password = args[++i]; }
18
18
  else if (args[i] === "--playground") { flags.playground = true; }
19
+ else if (args[i] === "--js-merkle") { flags.jsMerkle = true; }
19
20
  else if (args[i] === "--version" || args[i] === "-V") { flags.version = true; }
20
21
  else if (args[i] === "--help" || args[i] === "-h") { flags.help = true; }
21
22
  else { positional.push(args[i]); }
@@ -39,6 +40,7 @@ Options:
39
40
  --pool-size N Number of pool accounts (default: 10)
40
41
  --password "..." Encrypt SPA content (users will be prompted to decrypt)
41
42
  --playground Publish to the playground remix registry
43
+ --js-merkle Use pure-JS merkleization (no IPFS Kubo binary required)
42
44
  --version Show version
43
45
  --help Show this help`);
44
46
  process.exit(0);
@@ -61,6 +63,7 @@ try {
61
63
  rpc: flags.rpc,
62
64
  poolSize: flags.poolSize,
63
65
  password: flags.password,
66
+ jsMerkle: flags.jsMerkle,
64
67
  });
65
68
 
66
69
  const output = process.env.GITHUB_OUTPUT;
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  captureWarning,
3
3
  withSpan
4
- } from "./chunk-BYYVSN7E.js";
4
+ } from "./chunk-ZGP4DMFK.js";
5
5
 
6
6
  // src/dotns.ts
7
7
  import crypto from "crypto";
@@ -0,0 +1,77 @@
1
+ // src/merkle.ts
2
+ import * as fs from "fs";
3
+ import * as path from "path";
4
+ import { importer } from "ipfs-unixfs-importer";
5
+ import { MemoryBlockstore } from "blockstore-core/memory";
6
+ import { CarWriter } from "@ipld/car/writer";
7
+ function walkDirectory(dirPath, prefix = "") {
8
+ let dirents;
9
+ try {
10
+ dirents = fs.readdirSync(dirPath, { withFileTypes: true });
11
+ } catch (err) {
12
+ const code = err.code;
13
+ if (code === "ENOENT") throw new Error(`Directory not found: ${dirPath}`);
14
+ if (code === "ENOTDIR") throw new Error(`Not a directory: ${dirPath}`);
15
+ throw err;
16
+ }
17
+ const entries = [];
18
+ for (const entry of dirents) {
19
+ const fullPath = path.join(dirPath, entry.name);
20
+ const relativePath = prefix ? `${prefix}/${entry.name}` : entry.name;
21
+ if (entry.isDirectory()) {
22
+ entries.push(...walkDirectory(fullPath, relativePath));
23
+ } else if (entry.isFile()) {
24
+ entries.push({ path: relativePath, content: fs.readFileSync(fullPath) });
25
+ }
26
+ }
27
+ return entries;
28
+ }
29
+ async function collectBytes(iter) {
30
+ const parts = [];
31
+ for await (const chunk of iter) {
32
+ parts.push(chunk);
33
+ }
34
+ const totalLength = parts.reduce((sum, p) => sum + p.length, 0);
35
+ const result = new Uint8Array(totalLength);
36
+ let offset = 0;
37
+ for (const part of parts) {
38
+ result.set(part, offset);
39
+ offset += part.length;
40
+ }
41
+ return result;
42
+ }
43
+ async function merkleizeJS(directoryPath) {
44
+ console.log(` Merkleizing (JS): ${directoryPath}`);
45
+ const files = walkDirectory(directoryPath);
46
+ const blockstore = new MemoryBlockstore();
47
+ const source = files.map((file) => ({
48
+ path: file.path,
49
+ content: (async function* () {
50
+ yield file.content;
51
+ })()
52
+ }));
53
+ let rootCid;
54
+ for await (const entry of importer(source, blockstore, {
55
+ cidVersion: 1,
56
+ rawLeaves: true,
57
+ wrapWithDirectory: true
58
+ })) {
59
+ rootCid = entry.cid;
60
+ }
61
+ if (!rootCid) {
62
+ throw new Error("Merkleization produced no root CID");
63
+ }
64
+ const { writer, out } = CarWriter.create([rootCid]);
65
+ const collectPromise = collectBytes(out);
66
+ for await (const { cid, bytes } of blockstore.getAll()) {
67
+ await writer.put({ cid, bytes: await collectBytes(bytes) });
68
+ }
69
+ await writer.close();
70
+ const carBytes = await collectPromise;
71
+ console.log(` CAR (JS): ${(carBytes.length / 1024 / 1024).toFixed(2)} MB`);
72
+ return { carBytes, cid: rootCid.toString() };
73
+ }
74
+
75
+ export {
76
+ merkleizeJS
77
+ };
@@ -4,7 +4,10 @@ import {
4
4
  TX_TIMEOUT_MS,
5
5
  fetchNonce,
6
6
  validateDomainLabel
7
- } from "./chunk-7FNQBFFQ.js";
7
+ } from "./chunk-FBXG7YMT.js";
8
+ import {
9
+ merkleizeJS
10
+ } from "./chunk-GZ5UUECB.js";
8
11
  import {
9
12
  derivePoolAccounts,
10
13
  ensureAuthorized,
@@ -18,7 +21,7 @@ import {
18
21
  setDeployAttribute,
19
22
  withDeploySpan,
20
23
  withSpan
21
- } from "./chunk-BYYVSN7E.js";
24
+ } from "./chunk-ZGP4DMFK.js";
22
25
 
23
26
  // src/deploy.ts
24
27
  import { Buffer } from "buffer";
@@ -778,12 +781,23 @@ async function merkleize(directoryPath, outputCarPath) {
778
781
  console.log(` CAR: ${(size / 1024 / 1024).toFixed(2)} MB`);
779
782
  return { carPath: outputCarPath, cid };
780
783
  }
781
- async function storeDirectory(directoryPath, provider = {}, password) {
782
- const carPath = path.join(path.dirname(directoryPath), `${path.basename(directoryPath)}.car`);
783
- const { cid: ipfsCid } = await withSpan("deploy.merkleize", "1a. merkleize", { "deploy.directory": directoryPath }, async () => {
784
- return merkleize(directoryPath, carPath);
785
- });
786
- let carContent = new Uint8Array(fs.readFileSync(carPath));
784
+ async function storeDirectory(directoryPath, provider = {}, password, jsMerkle) {
785
+ let carContent;
786
+ let ipfsCid;
787
+ if (jsMerkle) {
788
+ const result = await withSpan("deploy.merkleize", "1a. merkleize (js)", { "deploy.directory": directoryPath }, async () => {
789
+ return merkleizeJS(directoryPath);
790
+ });
791
+ carContent = result.carBytes;
792
+ ipfsCid = result.cid;
793
+ } else {
794
+ const carPath = path.join(path.dirname(directoryPath), `${path.basename(directoryPath)}.car`);
795
+ const { cid } = await withSpan("deploy.merkleize", "1a. merkleize", { "deploy.directory": directoryPath }, async () => {
796
+ return merkleize(directoryPath, carPath);
797
+ });
798
+ ipfsCid = cid;
799
+ carContent = new Uint8Array(fs.readFileSync(carPath));
800
+ }
787
801
  if (password) {
788
802
  console.log(` Encrypting CAR file...`);
789
803
  carContent = await encryptContent(carContent, password);
@@ -867,7 +881,7 @@ Or deploy with the original account, or use a different domain name.`);
867
881
  console.log(`
868
882
  Mode: Directory`);
869
883
  console.log(` Path: ${contentPath}`);
870
- const dirResult = await storeDirectory(contentPath, providerWithReconnect, options.password);
884
+ const dirResult = await storeDirectory(contentPath, providerWithReconnect, options.password, options.jsMerkle);
871
885
  cid = dirResult.storageCid;
872
886
  ipfsCid = dirResult.ipfsCid;
873
887
  } else {
@@ -907,6 +921,11 @@ Or deploy with the original account, or use a different domain name.`);
907
921
  }
908
922
  });
909
923
  setDeployAttribute("deploy.cid", cid);
924
+ if (options.attributes) {
925
+ for (const [key, value] of Object.entries(options.attributes)) {
926
+ setDeployAttribute(key, value);
927
+ }
928
+ }
910
929
  console.log("\n" + "=".repeat(60));
911
930
  console.log("DotNS");
912
931
  console.log("=".repeat(60));
@@ -6,7 +6,7 @@ import * as path from "path";
6
6
  // package.json
7
7
  var package_default = {
8
8
  name: "bulletin-deploy",
9
- version: "0.6.6",
9
+ version: "0.6.7",
10
10
  private: false,
11
11
  repository: {
12
12
  type: "git",
@@ -34,12 +34,13 @@ var package_default = {
34
34
  "cdm.json"
35
35
  ],
36
36
  scripts: {
37
- build: "tsup src/index.ts src/deploy.ts src/dotns.ts src/pool.ts src/telemetry.ts --format esm --dts --clean --target node22",
37
+ build: "tsup src/index.ts src/deploy.ts src/dotns.ts src/pool.ts src/telemetry.ts src/merkle.ts --format esm --dts --clean --target node22",
38
38
  test: "npm run build && node --test test/test.js",
39
39
  benchmark: "npm run build && node benchmark.js"
40
40
  },
41
41
  dependencies: {
42
42
  "@dotdm/cdm": "^0.5.1",
43
+ "@ipld/car": "^5.4.3",
43
44
  "@ipld/dag-pb": "^4.1.3",
44
45
  "@noble/hashes": "^1.7.2",
45
46
  "@polkadot-api/substrate-bindings": "^0.16.5",
@@ -48,7 +49,9 @@ var package_default = {
48
49
  "@polkadot/keyring": "^13.0.0",
49
50
  "@polkadot/util-crypto": "^13.0.0",
50
51
  "@sentry/node": "^9.14.0",
52
+ "blockstore-core": "^6.1.3",
51
53
  "ipfs-unixfs": "^11.2.0",
54
+ "ipfs-unixfs-importer": "^16.1.4",
52
55
  multiformats: "^13.4.1",
53
56
  "polkadot-api": "^1.23.1",
54
57
  viem: "^2.30.5"
@@ -172,6 +175,8 @@ function captureWarning(message, context) {
172
175
  try {
173
176
  Sentry.addBreadcrumb({ level: "warning", message, data: context });
174
177
  Sentry.captureMessage(message, { level: "warning", extra: context });
178
+ const root = Sentry.getRootSpan(Sentry.getActiveSpan());
179
+ if (root) root.setAttribute("deploy.sad", true);
175
180
  } catch {
176
181
  }
177
182
  }
package/dist/deploy.d.ts CHANGED
@@ -49,7 +49,7 @@ declare function merkleize(directoryPath: string, outputCarPath: string): Promis
49
49
  carPath: string;
50
50
  cid: string;
51
51
  }>;
52
- declare function storeDirectory(directoryPath: string, provider?: ExistingProvider, password?: string): Promise<{
52
+ declare function storeDirectory(directoryPath: string, provider?: ExistingProvider, password?: string, jsMerkle?: boolean): Promise<{
53
53
  storageCid: string;
54
54
  ipfsCid: string;
55
55
  }>;
@@ -63,6 +63,10 @@ interface DeployOptions {
63
63
  rpc?: string;
64
64
  poolSize?: number;
65
65
  password?: string;
66
+ /** Use pure-JS merkleization instead of Kubo CLI. Required for WebContainer environments. */
67
+ jsMerkle?: boolean;
68
+ /** Custom telemetry attributes, merged into the deploy span. Overrides auto-detected values. */
69
+ attributes?: Record<string, string>;
66
70
  }
67
71
  declare function deploy(content: DeployContent, domainName?: string | null, options?: DeployOptions): Promise<DeployResult>;
68
72
 
package/dist/deploy.js CHANGED
@@ -21,10 +21,11 @@ import {
21
21
  storeChunkedContent,
22
22
  storeDirectory,
23
23
  storeFile
24
- } from "./chunk-YB4HKYPR.js";
25
- import "./chunk-7FNQBFFQ.js";
24
+ } from "./chunk-TD73EZ5I.js";
25
+ import "./chunk-FBXG7YMT.js";
26
+ import "./chunk-GZ5UUECB.js";
26
27
  import "./chunk-LGPTJYA3.js";
27
- import "./chunk-BYYVSN7E.js";
28
+ import "./chunk-ZGP4DMFK.js";
28
29
  import "./chunk-QGM4M3NI.js";
29
30
  export {
30
31
  DEFAULT_BULLETIN_RPC,
package/dist/dotns.js CHANGED
@@ -19,8 +19,8 @@ import {
19
19
  sanitizeDomainLabel,
20
20
  stripTrailingDigits,
21
21
  validateDomainLabel
22
- } from "./chunk-7FNQBFFQ.js";
23
- import "./chunk-BYYVSN7E.js";
22
+ } from "./chunk-FBXG7YMT.js";
23
+ import "./chunk-ZGP4DMFK.js";
24
24
  import "./chunk-QGM4M3NI.js";
25
25
  export {
26
26
  CONNECTION_TIMEOUT_MS,
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export { DeployContent, DeployOptions, DeployResult, deploy } from './deploy.js';
2
2
  export { PoolAccount, PoolAuthorization, bootstrapPool, derivePoolAccounts, ensureAuthorized, fetchPoolAuthorizations, selectAccount } from './pool.js';
3
3
  export { DotNS, DotNSConnectOptions, OwnershipResult, PriceValidationResult } from './dotns.js';
4
+ export { MerkleizeResult, merkleizeJS } from './merkle.js';
4
5
  import 'multiformats/cid';
5
6
  import 'polkadot-api';
package/dist/index.js CHANGED
@@ -1,9 +1,12 @@
1
1
  import {
2
2
  deploy
3
- } from "./chunk-YB4HKYPR.js";
3
+ } from "./chunk-TD73EZ5I.js";
4
4
  import {
5
5
  DotNS
6
- } from "./chunk-7FNQBFFQ.js";
6
+ } from "./chunk-FBXG7YMT.js";
7
+ import {
8
+ merkleizeJS
9
+ } from "./chunk-GZ5UUECB.js";
7
10
  import {
8
11
  bootstrapPool,
9
12
  derivePoolAccounts,
@@ -11,7 +14,7 @@ import {
11
14
  fetchPoolAuthorizations,
12
15
  selectAccount
13
16
  } from "./chunk-LGPTJYA3.js";
14
- import "./chunk-BYYVSN7E.js";
17
+ import "./chunk-ZGP4DMFK.js";
15
18
  import "./chunk-QGM4M3NI.js";
16
19
  export {
17
20
  DotNS,
@@ -20,5 +23,6 @@ export {
20
23
  derivePoolAccounts,
21
24
  ensureAuthorized,
22
25
  fetchPoolAuthorizations,
26
+ merkleizeJS,
23
27
  selectAccount
24
28
  };
@@ -0,0 +1,7 @@
1
+ interface MerkleizeResult {
2
+ carBytes: Uint8Array;
3
+ cid: string;
4
+ }
5
+ declare function merkleizeJS(directoryPath: string): Promise<MerkleizeResult>;
6
+
7
+ export { type MerkleizeResult, merkleizeJS };
package/dist/merkle.js ADDED
@@ -0,0 +1,7 @@
1
+ import {
2
+ merkleizeJS
3
+ } from "./chunk-GZ5UUECB.js";
4
+ import "./chunk-QGM4M3NI.js";
5
+ export {
6
+ merkleizeJS
7
+ };
package/dist/telemetry.js CHANGED
@@ -8,7 +8,7 @@ import {
8
8
  setDeployAttribute,
9
9
  withDeploySpan,
10
10
  withSpan
11
- } from "./chunk-BYYVSN7E.js";
11
+ } from "./chunk-ZGP4DMFK.js";
12
12
  import "./chunk-QGM4M3NI.js";
13
13
  export {
14
14
  VERSION,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bulletin-deploy",
3
- "version": "0.6.6",
3
+ "version": "0.6.7",
4
4
  "private": false,
5
5
  "repository": {
6
6
  "type": "git",
@@ -28,12 +28,13 @@
28
28
  "cdm.json"
29
29
  ],
30
30
  "scripts": {
31
- "build": "tsup src/index.ts src/deploy.ts src/dotns.ts src/pool.ts src/telemetry.ts --format esm --dts --clean --target node22",
31
+ "build": "tsup src/index.ts src/deploy.ts src/dotns.ts src/pool.ts src/telemetry.ts src/merkle.ts --format esm --dts --clean --target node22",
32
32
  "test": "npm run build && node --test test/test.js",
33
33
  "benchmark": "npm run build && node benchmark.js"
34
34
  },
35
35
  "dependencies": {
36
36
  "@dotdm/cdm": "^0.5.1",
37
+ "@ipld/car": "^5.4.3",
37
38
  "@ipld/dag-pb": "^4.1.3",
38
39
  "@noble/hashes": "^1.7.2",
39
40
  "@polkadot-api/substrate-bindings": "^0.16.5",
@@ -42,7 +43,9 @@
42
43
  "@polkadot/keyring": "^13.0.0",
43
44
  "@polkadot/util-crypto": "^13.0.0",
44
45
  "@sentry/node": "^9.14.0",
46
+ "blockstore-core": "^6.1.3",
45
47
  "ipfs-unixfs": "^11.2.0",
48
+ "ipfs-unixfs-importer": "^16.1.4",
46
49
  "multiformats": "^13.4.1",
47
50
  "polkadot-api": "^1.23.1",
48
51
  "viem": "^2.30.5"