keychat-save 1.0.0 → 1.2.0
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/index.mjs +78 -5
- package/package.json +1 -1
package/index.mjs
CHANGED
|
@@ -21,10 +21,12 @@ import { join } from 'node:path'
|
|
|
21
21
|
|
|
22
22
|
const CONFIG_DIR = join(homedir(), '.keychat')
|
|
23
23
|
const WIF_PATH = join(CONFIG_DIR, 'identity.wif')
|
|
24
|
+
const INDEX_PATH = join(CONFIG_DIR, 'saves-index.md')
|
|
24
25
|
const BRIDGE = 'https://www.keychat.pro/api/bridge'
|
|
25
26
|
const SAVE_PREFIX = '4b534156' // "KSAV"
|
|
26
27
|
const VERSION = '01'
|
|
27
28
|
const SUB_UNIT_SATS = 125
|
|
29
|
+
const INDEX_MARKER = '\n\n## __SAVES_INDEX__\n'
|
|
28
30
|
|
|
29
31
|
// ── crypto ───────────────────────────────────────────────────────
|
|
30
32
|
function bytesToHex (bytes) {
|
|
@@ -147,7 +149,13 @@ async function cmdSave (label, body) {
|
|
|
147
149
|
const address = key.toPublicKey().toAddress()
|
|
148
150
|
const pubHex = key.toPublicKey().toString()
|
|
149
151
|
|
|
150
|
-
|
|
152
|
+
// Bundle the local saves index into every save body so any single save
|
|
153
|
+
// bootstraps the full chain on a fresh device.
|
|
154
|
+
let index = ''
|
|
155
|
+
try { index = readFileSync(INDEX_PATH, 'utf8') } catch {}
|
|
156
|
+
const fullBody = body + INDEX_MARKER + index
|
|
157
|
+
|
|
158
|
+
const envelope = JSON.stringify({ kv: 1, kind: 'save', label, body: fullBody })
|
|
151
159
|
const encrypted = encrypt(envelope, key, pubHex)
|
|
152
160
|
const cipherHex = bytesToHex(encrypted)
|
|
153
161
|
const timestamp = Math.floor(Date.now() / 1000).toString(16).padStart(8, '0')
|
|
@@ -187,12 +195,70 @@ async function cmdSave (label, body) {
|
|
|
187
195
|
await tx.sign()
|
|
188
196
|
|
|
189
197
|
const txid = await broadcast(tx.toHex())
|
|
198
|
+
|
|
199
|
+
// Prepend new entry to local saves index
|
|
200
|
+
const date = new Date().toISOString().slice(0, 10)
|
|
201
|
+
const computeCost = (totalCost / 500).toString()
|
|
202
|
+
const newRow = `| ${date} | ${label} | ${txid} | ${computeCost}C |\n`
|
|
203
|
+
let header = '# KeyChat saves index\n\n| Date | Label | TxID | Cost |\n|------|-------|------|------|\n'
|
|
204
|
+
let existing = ''
|
|
205
|
+
try {
|
|
206
|
+
const cur = readFileSync(INDEX_PATH, 'utf8')
|
|
207
|
+
// Strip header if present, keep existing rows
|
|
208
|
+
const tableStart = cur.indexOf('|------|-------|------|------|')
|
|
209
|
+
existing = tableStart >= 0 ? cur.slice(tableStart + '|------|-------|------|------|'.length).trim() + '\n' : cur
|
|
210
|
+
} catch {}
|
|
211
|
+
writeFileSync(INDEX_PATH, header + newRow + existing, { mode: 0o600 })
|
|
212
|
+
|
|
190
213
|
console.log(`Saved to chain!`)
|
|
191
214
|
console.log(` Label: ${label}`)
|
|
192
215
|
console.log(` Size: ${tx.toHex().length / 2} bytes`)
|
|
193
216
|
console.log(` Tx: ${txid}`)
|
|
194
217
|
}
|
|
195
218
|
|
|
219
|
+
async function cmdLoadByTxid (txid) {
|
|
220
|
+
if (!existsSync(WIF_PATH)) { console.error('Run: keychat-save init'); process.exit(1) }
|
|
221
|
+
const key = PrivateKey.fromWif(readFileSync(WIF_PATH, 'utf8').trim())
|
|
222
|
+
const tx = await getTx(txid)
|
|
223
|
+
let opReturnHex = null
|
|
224
|
+
for (const o of (tx.outputs || [])) {
|
|
225
|
+
const s = o.script || ''
|
|
226
|
+
if (s.startsWith('006a') || s.startsWith('6a') || o.satoshis === 0) { opReturnHex = s; break }
|
|
227
|
+
}
|
|
228
|
+
if (!opReturnHex) { console.error('No OP_RETURN in tx.'); process.exit(1) }
|
|
229
|
+
const parts = extractParts(opReturnHex)
|
|
230
|
+
if (!parts || parts.length < 5 || parts[0] !== SAVE_PREFIX || parts[1] !== VERSION) {
|
|
231
|
+
console.error('Not a KSAV save tx.'); process.exit(1)
|
|
232
|
+
}
|
|
233
|
+
const cipherHex = parts[3]
|
|
234
|
+
const tsHex = parts[4]
|
|
235
|
+
const decrypted = decrypt(Array.from(hexToBytes(cipherHex)), key)
|
|
236
|
+
let label = 'Untitled', body = decrypted
|
|
237
|
+
try {
|
|
238
|
+
const env = JSON.parse(decrypted)
|
|
239
|
+
if (env.kind === 'save') { label = env.label || 'Untitled'; body = env.body || '' }
|
|
240
|
+
} catch {}
|
|
241
|
+
|
|
242
|
+
// Extract embedded saves index. Only write to local file if local is
|
|
243
|
+
// missing (fresh device bootstrap). Existing local index is left alone
|
|
244
|
+
// so loading an older save can't wipe out newer entries.
|
|
245
|
+
const idx = body.indexOf(INDEX_MARKER)
|
|
246
|
+
let userBody = body
|
|
247
|
+
if (idx >= 0) {
|
|
248
|
+
userBody = body.slice(0, idx)
|
|
249
|
+
const embeddedIndex = body.slice(idx + INDEX_MARKER.length).trim()
|
|
250
|
+
if (embeddedIndex && !existsSync(INDEX_PATH)) {
|
|
251
|
+
try {
|
|
252
|
+
writeFileSync(INDEX_PATH, embeddedIndex + '\n', { mode: 0o600 })
|
|
253
|
+
console.error(`(restored saves index to ${INDEX_PATH})`)
|
|
254
|
+
} catch {}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
console.log(`[${label}] — ${new Date(parseInt(tsHex, 16) * 1000).toLocaleString()}`)
|
|
259
|
+
console.log(userBody)
|
|
260
|
+
}
|
|
261
|
+
|
|
196
262
|
async function cmdLoad (opts = {}) {
|
|
197
263
|
if (!existsSync(WIF_PATH)) { console.error('Run: keychat-save init'); process.exit(1) }
|
|
198
264
|
const key = PrivateKey.fromWif(readFileSync(WIF_PATH, 'utf8').trim())
|
|
@@ -269,10 +335,16 @@ if (cmd === 'init') {
|
|
|
269
335
|
if (!body) { console.error('Usage: keychat-save save "label" "content"'); process.exit(1) }
|
|
270
336
|
await cmdSave(label, body)
|
|
271
337
|
} else if (cmd === 'load') {
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
if (
|
|
275
|
-
|
|
338
|
+
// Direct txid lookup — instant, no history scan
|
|
339
|
+
const txidIdx = args.indexOf('--txid')
|
|
340
|
+
if (txidIdx >= 0 && args[txidIdx + 1]) {
|
|
341
|
+
await cmdLoadByTxid(args[txidIdx + 1])
|
|
342
|
+
} else {
|
|
343
|
+
const opts = {}
|
|
344
|
+
if (args.includes('--last')) opts.last = true
|
|
345
|
+
if (args.includes('--search')) opts.search = args[args.indexOf('--search') + 1]
|
|
346
|
+
await cmdLoad(opts)
|
|
347
|
+
}
|
|
276
348
|
} else {
|
|
277
349
|
console.log(`keychat-save — Save to BSV blockchain via KeyChat (keychat.pro)
|
|
278
350
|
|
|
@@ -284,6 +356,7 @@ Commands:
|
|
|
284
356
|
load List all saves
|
|
285
357
|
load --last Most recent save
|
|
286
358
|
load --search "keyword" Search saves
|
|
359
|
+
load --txid <txid> Load a specific save by txid (instant, no scan)
|
|
287
360
|
|
|
288
361
|
Your data. Your key. Permanent on BSV.`)
|
|
289
362
|
}
|