@cogcoin/client 0.5.4 → 0.5.5

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.
Files changed (35) hide show
  1. package/README.md +1 -1
  2. package/dist/app-paths.d.ts +2 -0
  3. package/dist/app-paths.js +4 -0
  4. package/dist/art/wallet.txt +9 -9
  5. package/dist/bitcoind/bootstrap/chunk-manifest.d.ts +14 -0
  6. package/dist/bitcoind/bootstrap/chunk-manifest.js +85 -0
  7. package/dist/bitcoind/bootstrap/chunk-recovery.d.ts +4 -0
  8. package/dist/bitcoind/bootstrap/chunk-recovery.js +122 -0
  9. package/dist/bitcoind/bootstrap/constants.d.ts +3 -1
  10. package/dist/bitcoind/bootstrap/constants.js +3 -1
  11. package/dist/bitcoind/bootstrap/controller.d.ts +6 -1
  12. package/dist/bitcoind/bootstrap/controller.js +14 -7
  13. package/dist/bitcoind/bootstrap/default-snapshot-chunk-manifest.d.ts +2 -0
  14. package/dist/bitcoind/bootstrap/default-snapshot-chunk-manifest.js +2309 -0
  15. package/dist/bitcoind/bootstrap/download.js +177 -83
  16. package/dist/bitcoind/bootstrap/headers.d.ts +4 -2
  17. package/dist/bitcoind/bootstrap/headers.js +29 -4
  18. package/dist/bitcoind/bootstrap/state.d.ts +11 -1
  19. package/dist/bitcoind/bootstrap/state.js +50 -23
  20. package/dist/bitcoind/bootstrap/types.d.ts +12 -1
  21. package/dist/bitcoind/client/internal-types.d.ts +1 -0
  22. package/dist/bitcoind/client/managed-client.js +27 -13
  23. package/dist/bitcoind/client/sync-engine.js +42 -5
  24. package/dist/bitcoind/errors.js +9 -0
  25. package/dist/bitcoind/types.d.ts +9 -0
  26. package/dist/cli/output.js +1 -1
  27. package/dist/wallet/lifecycle.js +64 -5
  28. package/dist/wallet/runtime.d.ts +2 -0
  29. package/dist/wallet/runtime.js +2 -0
  30. package/dist/wallet/state/pending-init.d.ts +24 -0
  31. package/dist/wallet/state/pending-init.js +59 -0
  32. package/dist/wallet/state/provider.d.ts +1 -0
  33. package/dist/wallet/state/provider.js +7 -1
  34. package/dist/wallet/types.d.ts +8 -0
  35. package/package.json +4 -2
@@ -2,9 +2,39 @@ import { formatManagedSyncErrorMessage } from "../errors.js";
2
2
  import { normalizeRpcBlock } from "../normalize.js";
3
3
  import { estimateEtaSeconds } from "./rate-tracker.js";
4
4
  const DEFAULT_SYNC_CATCH_UP_POLL_MS = 2_000;
5
- function sleep(ms) {
6
- return new Promise((resolve) => {
7
- setTimeout(resolve, ms);
5
+ function createAbortError(signal) {
6
+ const reason = signal?.reason;
7
+ if (reason instanceof Error) {
8
+ return reason;
9
+ }
10
+ const error = new Error("managed_sync_aborted");
11
+ error.name = "AbortError";
12
+ return error;
13
+ }
14
+ function isAbortError(error, signal) {
15
+ if (signal?.aborted) {
16
+ return true;
17
+ }
18
+ return error instanceof Error
19
+ && (error.name === "AbortError" || error.message === "managed_sync_aborted");
20
+ }
21
+ function throwIfAborted(signal) {
22
+ if (signal?.aborted) {
23
+ throw createAbortError(signal);
24
+ }
25
+ }
26
+ function sleep(ms, signal) {
27
+ return new Promise((resolve, reject) => {
28
+ const timer = setTimeout(() => {
29
+ signal?.removeEventListener("abort", onAbort);
30
+ resolve();
31
+ }, ms);
32
+ const onAbort = () => {
33
+ clearTimeout(timer);
34
+ signal?.removeEventListener("abort", onAbort);
35
+ reject(createAbortError(signal));
36
+ };
37
+ signal?.addEventListener("abort", onAbort, { once: true });
8
38
  });
9
39
  }
10
40
  async function setBitcoinSyncProgress(dependencies, info) {
@@ -81,9 +111,12 @@ async function syncAgainstBestHeight(dependencies, bestHeight) {
81
111
  }
82
112
  export async function syncToTip(dependencies) {
83
113
  try {
114
+ throwIfAborted(dependencies.abortSignal);
84
115
  await dependencies.node.validate();
85
116
  const indexedTipBeforeBootstrap = await dependencies.client.getTip();
86
- await dependencies.bootstrap.ensureReady(indexedTipBeforeBootstrap, dependencies.node.expectedChain);
117
+ await dependencies.bootstrap.ensureReady(indexedTipBeforeBootstrap, dependencies.node.expectedChain, {
118
+ signal: dependencies.abortSignal,
119
+ });
87
120
  const startTip = await dependencies.client.getTip();
88
121
  const aggregate = {
89
122
  appliedBlocks: 0,
@@ -95,6 +128,7 @@ export async function syncToTip(dependencies) {
95
128
  bestHashHex: "",
96
129
  };
97
130
  while (true) {
131
+ throwIfAborted(dependencies.abortSignal);
98
132
  const startInfo = await dependencies.rpc.getBlockchainInfo();
99
133
  await setBitcoinSyncProgress(dependencies, startInfo);
100
134
  const pass = await syncAgainstBestHeight(dependencies, startInfo.blocks);
@@ -129,10 +163,13 @@ export async function syncToTip(dependencies) {
129
163
  if (endInfo.blocks >= dependencies.startHeight && finalTip?.height !== endInfo.blocks) {
130
164
  continue;
131
165
  }
132
- await sleep(DEFAULT_SYNC_CATCH_UP_POLL_MS);
166
+ await sleep(DEFAULT_SYNC_CATCH_UP_POLL_MS, dependencies.abortSignal);
133
167
  }
134
168
  }
135
169
  catch (error) {
170
+ if (isAbortError(error, dependencies.abortSignal)) {
171
+ throw createAbortError(dependencies.abortSignal);
172
+ }
136
173
  const message = formatManagedSyncErrorMessage(error instanceof Error ? error.message : String(error));
137
174
  await dependencies.progress.setPhase("error", {
138
175
  lastError: message,
@@ -18,6 +18,15 @@ export function formatManagedSyncErrorMessage(message) {
18
18
  if (message === "snapshot_response_body_missing") {
19
19
  return appendNextStep("Snapshot server returned an empty response body.", "Wait a moment, confirm the snapshot host is reachable, then rerun sync.");
20
20
  }
21
+ if (message === "snapshot_resume_requires_partial_content") {
22
+ return appendNextStep("Snapshot server ignored the resume request for a partial download.", "Wait a moment and rerun sync. If this keeps happening, confirm the snapshot host supports HTTP range requests.");
23
+ }
24
+ if (message.startsWith("snapshot_chunk_sha256_mismatch_")) {
25
+ return appendNextStep("A downloaded snapshot chunk was corrupted and was rolled back to the last verified checkpoint.", "Wait a moment and rerun sync. If this keeps happening, check local disk health and the stability of the snapshot connection.");
26
+ }
27
+ if (message.startsWith("snapshot_download_incomplete_")) {
28
+ return appendNextStep("Snapshot download ended before the expected file size was reached.", "Wait a moment and rerun sync. The downloader will resume from the last verified checkpoint.");
29
+ }
21
30
  if (message === "bitcoind_cookie_timeout") {
22
31
  return appendNextStep("The managed Bitcoin node did not finish starting in time.", "Check the node logs and local permissions for the Bitcoin data directory, then rerun sync.");
23
32
  }
@@ -8,6 +8,15 @@ export interface SnapshotMetadata {
8
8
  sha256: string;
9
9
  sizeBytes: number;
10
10
  }
11
+ export interface SnapshotChunkManifest {
12
+ formatVersion: number;
13
+ chunkSizeBytes: number;
14
+ snapshotFilename: string;
15
+ snapshotHeight: number;
16
+ snapshotSizeBytes: number;
17
+ snapshotSha256: string;
18
+ chunkSha256s: string[];
19
+ }
11
20
  export interface WritingQuote {
12
21
  quote: string;
13
22
  author: string;
@@ -335,7 +335,7 @@ export function createCliErrorPresentation(errorCode, fallbackMessage) {
335
335
  return {
336
336
  what: "Mnemonic confirmation failed.",
337
337
  why: "The requested recovery-phrase confirmation word did not match, so wallet initialization was canceled before it could finish.",
338
- next: "Run `cogcoin init` again and re-enter the requested confirmation words carefully.",
338
+ next: "Run `cogcoin init` again and re-enter the requested confirmation words carefully. The same recovery phrase will be shown until confirmation succeeds.",
339
339
  };
340
340
  }
341
341
  if (errorCode === "wallet_restore_mnemonic_invalid") {
@@ -19,8 +19,9 @@ import { loadMiningRuntimeStatus, saveMiningRuntimeStatus } from "./mining/runti
19
19
  import { normalizeMiningStateRecord } from "./mining/state.js";
20
20
  import { renderWalletMnemonicRevealArt } from "./mnemonic-art.js";
21
21
  import { clearWalletExplicitLock, loadWalletExplicitLock, saveWalletExplicitLock, } from "./state/explicit-lock.js";
22
+ import { clearWalletPendingInitializationState, loadWalletPendingInitializationStateOrNull, saveWalletPendingInitializationState, } from "./state/pending-init.js";
22
23
  import { clearUnlockSession, loadUnlockSession, saveUnlockSession } from "./state/session.js";
23
- import { createDefaultWalletSecretProvider, createWalletRootId, createWalletSecretReference, } from "./state/provider.js";
24
+ import { createDefaultWalletSecretProvider, createWalletPendingInitSecretReference, createWalletRootId, createWalletSecretReference, } from "./state/provider.js";
24
25
  import { loadWalletState, saveWalletState } from "./state/storage.js";
25
26
  export const DEFAULT_UNLOCK_DURATION_MS = 15 * 60 * 1000;
26
27
  export { previewResetWallet, resetWallet, } from "./reset.js";
@@ -62,6 +63,53 @@ function extractWalletRootIdFromEnvelope(envelope) {
62
63
  }
63
64
  return keyId.slice(prefix.length);
64
65
  }
66
+ function resolvePendingInitializationStoragePaths(paths) {
67
+ return {
68
+ primaryPath: paths.walletInitPendingPath,
69
+ backupPath: paths.walletInitPendingBackupPath,
70
+ };
71
+ }
72
+ async function clearPendingInitialization(paths, provider) {
73
+ await clearWalletPendingInitializationState(resolvePendingInitializationStoragePaths(paths), {
74
+ provider,
75
+ secretReference: createWalletPendingInitSecretReference(paths.stateRoot),
76
+ });
77
+ }
78
+ async function loadOrCreatePendingInitializationMaterial(options) {
79
+ try {
80
+ const loaded = await loadWalletPendingInitializationStateOrNull(resolvePendingInitializationStoragePaths(options.paths), {
81
+ provider: options.provider,
82
+ });
83
+ if (loaded !== null) {
84
+ return deriveWalletMaterialFromMnemonic(loaded.state.mnemonic.phrase);
85
+ }
86
+ }
87
+ catch {
88
+ await clearPendingInitialization(options.paths, options.provider);
89
+ }
90
+ const material = generateWalletMaterial();
91
+ const secretReference = createWalletPendingInitSecretReference(options.paths.stateRoot);
92
+ const pendingState = {
93
+ schemaVersion: 1,
94
+ createdAtUnixMs: options.nowUnixMs,
95
+ mnemonic: {
96
+ phrase: material.mnemonic.phrase,
97
+ language: material.mnemonic.language,
98
+ },
99
+ };
100
+ await options.provider.storeSecret(secretReference.keyId, randomBytes(32));
101
+ try {
102
+ await saveWalletPendingInitializationState(resolvePendingInitializationStoragePaths(options.paths), pendingState, {
103
+ provider: options.provider,
104
+ secretReference,
105
+ });
106
+ }
107
+ catch (error) {
108
+ await options.provider.deleteSecret(secretReference.keyId).catch(() => undefined);
109
+ throw error;
110
+ }
111
+ return material;
112
+ }
65
113
  function createInitialWalletState(options) {
66
114
  return {
67
115
  schemaVersion: 1,
@@ -780,8 +828,9 @@ export function parseUnlockDurationToMs(raw) {
780
828
  }
781
829
  return duration;
782
830
  }
783
- async function ensureWalletNotInitialized(paths) {
831
+ async function ensureWalletNotInitialized(paths, provider) {
784
832
  if (await pathExists(paths.walletStatePath) || await pathExists(paths.walletStateBackupPath)) {
833
+ await clearPendingInitialization(paths, provider);
785
834
  throw new Error("wallet_already_initialized");
786
835
  }
787
836
  }
@@ -1079,11 +1128,16 @@ export async function initializeWallet(options) {
1079
1128
  walletRootId: null,
1080
1129
  });
1081
1130
  try {
1082
- await ensureWalletNotInitialized(paths);
1083
- const material = generateWalletMaterial();
1131
+ await ensureWalletNotInitialized(paths, provider);
1132
+ const material = await loadOrCreatePendingInitializationMaterial({
1133
+ provider,
1134
+ paths,
1135
+ nowUnixMs,
1136
+ });
1084
1137
  let mnemonicRevealed = false;
1085
1138
  options.prompter.writeLine("Cogcoin Wallet Initialization");
1086
- options.prompter.writeLine("Write down this 24-word recovery phrase. It will only be shown once:");
1139
+ options.prompter.writeLine("Write down this 24-word recovery phrase.");
1140
+ options.prompter.writeLine("The same phrase will be shown again until confirmation succeeds:");
1087
1141
  options.prompter.writeLine("");
1088
1142
  for (const line of renderWalletMnemonicRevealArt(material.mnemonic.words)) {
1089
1143
  options.prompter.writeLine(line);
@@ -1126,6 +1180,7 @@ export async function initializeWallet(options) {
1126
1180
  provider,
1127
1181
  secretReference,
1128
1182
  });
1183
+ await clearPendingInitialization(paths, provider);
1129
1184
  return {
1130
1185
  walletRootId,
1131
1186
  fundingAddress: verifiedState.funding.address,
@@ -1290,6 +1345,7 @@ export async function importWallet(options) {
1290
1345
  const replacementStateExists = await pathExists(paths.walletStatePath) || await pathExists(paths.walletStateBackupPath);
1291
1346
  const importedWalletDir = join(options.dataDir, "wallets", sanitizeWalletName(payload.walletRootId));
1292
1347
  const replacementCoreWalletExists = await pathExists(importedWalletDir);
1348
+ await clearPendingInitialization(paths, provider);
1293
1349
  if (replacementStateExists || replacementCoreWalletExists) {
1294
1350
  await confirmTypedAcknowledgement(options.prompter, "IMPORT", "Type IMPORT to replace the existing local wallet state and managed Core wallet replica: ");
1295
1351
  }
@@ -1333,6 +1389,7 @@ export async function importWallet(options) {
1333
1389
  provider,
1334
1390
  secretReference,
1335
1391
  });
1392
+ await clearPendingInitialization(paths, provider);
1336
1393
  if (previousWalletRootId !== null && previousWalletRootId !== payload.walletRootId) {
1337
1394
  await provider.deleteSecret(createWalletSecretReference(previousWalletRootId).keyId).catch(() => undefined);
1338
1395
  }
@@ -1372,6 +1429,7 @@ export async function restoreWalletFromMnemonic(options) {
1372
1429
  || await pathExists(paths.walletStateBackupPath);
1373
1430
  const replacementCoreWalletExists = await detectExistingManagedWalletReplica(options.dataDir);
1374
1431
  const mnemonicPhrase = await promptForRestoreMnemonic(options.prompter);
1432
+ await clearPendingInitialization(paths, provider);
1375
1433
  if (replacementStateExists || replacementCoreWalletExists) {
1376
1434
  await confirmRestoreReplacement(options.prompter);
1377
1435
  }
@@ -1425,6 +1483,7 @@ export async function restoreWalletFromMnemonic(options) {
1425
1483
  provider,
1426
1484
  secretReference,
1427
1485
  });
1486
+ await clearPendingInitialization(paths, provider);
1428
1487
  if (previousWalletRootId !== null && previousWalletRootId !== walletRootId) {
1429
1488
  try {
1430
1489
  await clearPreviousManagedWalletRuntime({
@@ -9,6 +9,8 @@ export interface WalletRuntimePaths {
9
9
  indexerRoot: string;
10
10
  walletStatePath: string;
11
11
  walletStateBackupPath: string;
12
+ walletInitPendingPath: string;
13
+ walletInitPendingBackupPath: string;
12
14
  walletUnlockSessionPath: string;
13
15
  walletExplicitLockPath: string;
14
16
  walletControlLockPath: string;
@@ -11,6 +11,8 @@ export function resolveWalletRuntimePathsForTesting(resolution = {}) {
11
11
  indexerRoot: paths.indexerRoot,
12
12
  walletStatePath: paths.walletStatePath,
13
13
  walletStateBackupPath: paths.walletStateBackupPath,
14
+ walletInitPendingPath: paths.walletInitPendingPath,
15
+ walletInitPendingBackupPath: paths.walletInitPendingBackupPath,
14
16
  walletUnlockSessionPath: paths.walletUnlockSessionPath,
15
17
  walletExplicitLockPath: paths.walletExplicitLockPath,
16
18
  walletControlLockPath: paths.walletControlLockPath,
@@ -0,0 +1,24 @@
1
+ import type { WalletPendingInitializationStateV1 } from "../types.js";
2
+ import type { WalletSecretProvider, WalletSecretReference } from "./provider.js";
3
+ export interface WalletPendingInitializationStoragePaths {
4
+ primaryPath: string;
5
+ backupPath: string;
6
+ }
7
+ export interface LoadedWalletPendingInitializationState {
8
+ source: "primary" | "backup";
9
+ state: WalletPendingInitializationStateV1;
10
+ }
11
+ export declare function saveWalletPendingInitializationState(paths: WalletPendingInitializationStoragePaths, state: WalletPendingInitializationStateV1, access: {
12
+ provider: WalletSecretProvider;
13
+ secretReference: WalletSecretReference;
14
+ }): Promise<void>;
15
+ export declare function loadWalletPendingInitializationState(paths: WalletPendingInitializationStoragePaths, access: {
16
+ provider: WalletSecretProvider;
17
+ }): Promise<LoadedWalletPendingInitializationState>;
18
+ export declare function loadWalletPendingInitializationStateOrNull(paths: WalletPendingInitializationStoragePaths, access: {
19
+ provider: WalletSecretProvider;
20
+ }): Promise<LoadedWalletPendingInitializationState | null>;
21
+ export declare function clearWalletPendingInitializationState(paths: WalletPendingInitializationStoragePaths, access?: {
22
+ provider?: WalletSecretProvider;
23
+ secretReference?: WalletSecretReference;
24
+ }): Promise<void>;
@@ -0,0 +1,59 @@
1
+ import { readFile, rm } from "node:fs/promises";
2
+ import { writeJsonFileAtomic } from "../fs/atomic.js";
3
+ import { decryptJsonWithSecretProvider, encryptJsonWithSecretProvider, } from "./crypto.js";
4
+ function isMissingFileError(error) {
5
+ return error instanceof Error
6
+ && "code" in error
7
+ && error.code === "ENOENT";
8
+ }
9
+ async function readEnvelope(path) {
10
+ const raw = await readFile(path, "utf8");
11
+ return JSON.parse(raw);
12
+ }
13
+ async function loadFromPath(path, source, provider) {
14
+ return {
15
+ source,
16
+ state: await decryptJsonWithSecretProvider(await readEnvelope(path), provider),
17
+ };
18
+ }
19
+ export async function saveWalletPendingInitializationState(paths, state, access) {
20
+ const envelope = await encryptJsonWithSecretProvider(state, access.provider, access.secretReference, {
21
+ format: "cogcoin-wallet-init-pending-state",
22
+ });
23
+ await writeJsonFileAtomic(paths.primaryPath, envelope, { mode: 0o600 });
24
+ await writeJsonFileAtomic(paths.backupPath, envelope, { mode: 0o600 });
25
+ }
26
+ export async function loadWalletPendingInitializationState(paths, access) {
27
+ try {
28
+ return await loadFromPath(paths.primaryPath, "primary", access.provider);
29
+ }
30
+ catch (primaryError) {
31
+ try {
32
+ return await loadFromPath(paths.backupPath, "backup", access.provider);
33
+ }
34
+ catch (backupError) {
35
+ if (isMissingFileError(primaryError)) {
36
+ throw backupError;
37
+ }
38
+ throw primaryError;
39
+ }
40
+ }
41
+ }
42
+ export async function loadWalletPendingInitializationStateOrNull(paths, access) {
43
+ try {
44
+ return await loadWalletPendingInitializationState(paths, access);
45
+ }
46
+ catch (error) {
47
+ if (isMissingFileError(error)) {
48
+ return null;
49
+ }
50
+ throw error;
51
+ }
52
+ }
53
+ export async function clearWalletPendingInitializationState(paths, access) {
54
+ await rm(paths.primaryPath, { force: true }).catch(() => undefined);
55
+ await rm(paths.backupPath, { force: true }).catch(() => undefined);
56
+ if (access?.provider != null && access.secretReference != null) {
57
+ await access.provider.deleteSecret(access.secretReference.keyId).catch(() => undefined);
58
+ }
59
+ }
@@ -23,6 +23,7 @@ export interface WalletSecretProvider {
23
23
  deleteSecret(keyId: string): Promise<void>;
24
24
  }
25
25
  export declare function createWalletSecretReference(walletRootId: string): WalletSecretReference;
26
+ export declare function createWalletPendingInitSecretReference(stateRoot: string): WalletSecretReference;
26
27
  export declare class MemoryWalletSecretProvider implements WalletSecretProvider {
27
28
  #private;
28
29
  readonly kind = "memory-test";
@@ -1,4 +1,4 @@
1
- import { randomUUID } from "node:crypto";
1
+ import { createHash, randomUUID } from "node:crypto";
2
2
  import { execFile, spawn } from "node:child_process";
3
3
  import { mkdir, readFile, rm } from "node:fs/promises";
4
4
  import { dirname, join } from "node:path";
@@ -17,6 +17,12 @@ export function createWalletSecretReference(walletRootId) {
17
17
  keyId: `wallet-state:${walletRootId}`,
18
18
  };
19
19
  }
20
+ export function createWalletPendingInitSecretReference(stateRoot) {
21
+ return {
22
+ kind: "wallet-init-pending-key",
23
+ keyId: `wallet-init-pending:${createHash("sha256").update(stateRoot).digest("hex")}`,
24
+ };
25
+ }
20
26
  function bytesToBase64(secret) {
21
27
  return Buffer.from(secret).toString("base64");
22
28
  }
@@ -252,3 +252,11 @@ export interface WalletExplicitLockStateV1 {
252
252
  walletRootId: string;
253
253
  lockedAtUnixMs: number;
254
254
  }
255
+ export interface WalletPendingInitializationStateV1 {
256
+ schemaVersion: 1;
257
+ createdAtUnixMs: number;
258
+ mnemonic: {
259
+ phrase: string;
260
+ language: WalletMnemonicLanguage;
261
+ };
262
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cogcoin/client",
3
- "version": "0.5.4",
3
+ "version": "0.5.5",
4
4
  "description": "Store-backed Cogcoin client with wallet flows, SQLite persistence, and managed Bitcoin Core integration.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -53,10 +53,12 @@
53
53
  "files": [
54
54
  "LICENSE",
55
55
  "README.md",
56
- "dist/**/*"
56
+ "dist"
57
57
  ],
58
58
  "scripts": {
59
59
  "build": "rm -rf dist && node ./node_modules/typescript/bin/tsc -p tsconfig.json && node -e \"import('node:fs/promises').then(async (fs) => { await fs.mkdir('dist/art', { recursive: true }); await Promise.all([fs.copyFile('src/writing_quotes.json', 'dist/writing_quotes.json'), fs.copyFile('src/art/banner.txt', 'dist/art/banner.txt'), fs.copyFile('src/art/scroll.txt', 'dist/art/scroll.txt'), fs.copyFile('src/art/train-smoke.txt', 'dist/art/train-smoke.txt'), fs.copyFile('src/art/train.txt', 'dist/art/train.txt'), fs.copyFile('src/art/train-car.txt', 'dist/art/train-car.txt'), fs.copyFile('src/art/wallet.txt', 'dist/art/wallet.txt')]); })\"",
60
+ "generate:default-snapshot-chunk-manifest": "node scripts/generate-default-snapshot-chunk-manifest.mjs",
61
+ "verify:default-snapshot-chunk-manifest": "node scripts/generate-default-snapshot-chunk-manifest.mjs --check",
60
62
  "test": "rm -rf .test-dist && node ./node_modules/typescript/bin/tsc -p tsconfig.test.json && node -e \"import('node:fs/promises').then(async (fs) => { await fs.mkdir('.test-dist/src/art', { recursive: true }); await Promise.all([fs.copyFile('src/writing_quotes.json', '.test-dist/src/writing_quotes.json'), fs.copyFile('src/art/banner.txt', '.test-dist/src/art/banner.txt'), fs.copyFile('src/art/scroll.txt', '.test-dist/src/art/scroll.txt'), fs.copyFile('src/art/train-smoke.txt', '.test-dist/src/art/train-smoke.txt'), fs.copyFile('src/art/train.txt', '.test-dist/src/art/train.txt'), fs.copyFile('src/art/train-car.txt', '.test-dist/src/art/train-car.txt'), fs.copyFile('src/art/wallet.txt', '.test-dist/src/art/wallet.txt')]); })\" && node --test .test-dist/test/*.test.js"
61
63
  },
62
64
  "dependencies": {