()
| 126 | * Derives the wallet, queries Arweave for shards, and reconstructs local state. |
| 127 | */ |
| 128 | export async function initExistingCommand(): Promise<void> { |
| 129 | if (existsSync(DB_PATH)) { |
| 130 | console.log("SharedContext is already initialized at ~/.sharedcontext/"); |
| 131 | console.log("To reinitialize, delete ~/.sharedcontext/ first."); |
| 132 | return; |
| 133 | } |
| 134 | |
| 135 | console.log(BANNER); |
| 136 | console.log("Restore SharedContext from recovery phrase.\n"); |
| 137 | |
| 138 | const input = await prompt(`Enter your ${PHRASE_WORD_COUNT}-word recovery phrase: `); |
| 139 | const words = input.split(/\s+/).map((w) => w.toLowerCase()); |
| 140 | |
| 141 | const validation = validatePhrase(words); |
| 142 | if (!validation.valid) { |
| 143 | console.error(`Invalid phrase: ${validation.error}`); |
| 144 | process.exit(1); |
| 145 | } |
| 146 | |
| 147 | const phrase = phraseToString(words); |
| 148 | |
| 149 | // Derive deterministic identity from phrase |
| 150 | console.log("Deriving identity..."); |
| 151 | const keypair = deriveKeypairFromPhrase(phrase); |
| 152 | console.log(`Wallet: ${keypair.address}`); |
| 153 | |
| 154 | mkdirSync(SHAREDCONTEXT_DIR, { recursive: true }); |
| 155 | |
| 156 | console.log("\nQuerying Arweave and reconstructing local state..."); |
| 157 | |
| 158 | try { |
| 159 | const result = await pullAndReconstruct(keypair.address, phrase, DB_PATH); |
| 160 | |
| 161 | // Persist identity material locally so normal CLI flows work after restore. |
| 162 | const identity = await fetchIdentity(keypair.address); |
| 163 | if (!identity) { |
| 164 | throw new Error( |
| 165 | "Recovery failed: identity transaction not found on Arweave." |
| 166 | ); |
| 167 | } |
| 168 | writeFileSync(SALT_PATH, Buffer.from(identity.salt), { mode: 0o600 }); |
| 169 | writeFileSync(IDENTITY_PATH, identity.encryptedPrivateKey, { mode: 0o600 }); |
| 170 | |
| 171 | // Sanity check: recovered encrypted key must decrypt to phrase-derived identity key. |
| 172 | const key = deriveKey(phrase, identity.salt); |
| 173 | const recoveredPrivateKey = decrypt(identity.encryptedPrivateKey, key); |
| 174 | if (Buffer.compare(Buffer.from(recoveredPrivateKey), Buffer.from(keypair.privateKey)) !== 0) { |
| 175 | throw new Error( |
| 176 | "Recovered identity does not match the provided recovery phrase." |
| 177 | ); |
| 178 | } |
| 179 | |
| 180 | console.log(`Recovered ${result.factCount} fact(s), version ${result.version}.`); |
| 181 | } catch (err) { |
| 182 | const { rmSync } = await import("fs"); |
| 183 | rmSync(SHAREDCONTEXT_DIR, { recursive: true, force: true }); |
| 184 | throw err; |
| 185 | } |
no test coverage detected