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.
Files changed (2) hide show
  1. package/index.mjs +78 -5
  2. 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
- const envelope = JSON.stringify({ kv: 1, kind: 'save', label, body })
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
- const opts = {}
273
- if (args.includes('--last')) opts.last = true
274
- if (args.includes('--search')) opts.search = args[args.indexOf('--search') + 1]
275
- await cmdLoad(opts)
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "keychat-save",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
4
  "description": "Save and load files to BSV blockchain via KeyChat",
5
5
  "type": "module",
6
6
  "bin": {