wk-recovery: set or clear the recovery key on our own names (signs in Actions)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UozKbNTNSfntAfsaaFuLuM
This commit is contained in:
teahubandClaude Opus 5.5 committed 2026-10-25 20:03:57 -07:00
1 parent e6107d6946
commit d8f7ad2920
1 file changed
+50
+50
View File
@@ -0,0 +1,50 @@
name: wk-recovery
# Sets (or clears, with recovery=none) the recovery key on our own well-known names.
# Signs with the name's main key inside Actions: factory = SNAIL_KNOWN, well-known = WK_NSEC. Keys never leave the runner.
on:
workflow_dispatch:
inputs:
key: { description: "which main key signs: factory or well-known", required: true, default: factory }
names: { description: comma-separated names, required: true }
recovery: { description: "recovery pubkey (64 hex) or none", required: true }
jobs:
set:
runs-on: ubuntu-latest
steps:
- name: run
env:
K_FACTORY: ${{ secrets.SNAIL_KNOWN }}
K_WK: ${{ secrets.WK_NSEC }}
WHICH: ${{ inputs.key }}
NAMES: ${{ inputs.names }}
REC: ${{ inputs.recovery }}
run: |
mkdir -p /tmp/rk && cd /tmp/rk
npm init -y >/dev/null && npm i --silent @noble/curves@1.9.7 >/dev/null
cat > t.mjs <<'JS'
import { schnorr } from '@noble/curves/secp256k1';
import { createHash } from 'node:crypto';
const CH = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l';
const toHex = (s) => { s = s.trim().toLowerCase(); if (/^[0-9a-f]{64}$/.test(s)) return s;
const d = [...s.slice(s.lastIndexOf('1') + 1)].map((c) => CH.indexOf(c)).slice(0, -6); let a = 0, b = 0; const o = [];
for (const v of d) { a = (a << 5) | v; b += 5; while (b >= 8) { b -= 8; o.push((a >> b) & 255); } } return Buffer.from(o).toString('hex'); };
const raw = process.env.WHICH === 'well-known' ? process.env.K_WK : process.env.WHICH === 'factory' ? process.env.K_FACTORY : '';
if (!raw) { console.log('key must be factory or well-known'); process.exit(2); }
const rec = process.env.REC.trim().toLowerCase();
if (rec !== 'none' && !/^[0-9a-f]{64}$/.test(rec)) { console.log('recovery must be 64 hex or none'); process.exit(2); }
const sk = toHex(raw), pk = Buffer.from(schnorr.getPublicKey(sk)).toString('hex');
let fails = 0, n = 0;
for (const name of process.env.NAMES.split(',').map((s) => s.trim().toLowerCase()).filter(Boolean)) {
const ev = { pubkey: pk, created_at: Math.floor(Date.now() / 1000), kind: 27235, content: String(n++),
tags: [['u', 'https://well-known.online/recovery'], ['method', 'POST'], ['name', name], ...(rec === 'none' ? [] : [['p', rec]])] };
ev.id = createHash('sha256').update(JSON.stringify([0, ev.pubkey, ev.created_at, ev.kind, ev.tags, ev.content])).digest('hex');
ev.sig = Buffer.from(schnorr.sign(ev.id, sk)).toString('hex');
const r = await fetch('https://well-known.online/recovery', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ event: ev }) });
const j = await r.json().catch(() => ({}));
const ok = r.status === 200 && (rec === 'none' ? j.recovery === null : j.recovery === rec);
console.log(`${ok ? 'PASS' : 'FAIL'} ${name} ${r.status} ${JSON.stringify(j)}`);
if (!ok) fails++;
}
process.exit(fails ? 1 : 0);
JS
node t.mjs