publish(tasks 19+20): signer-stabilisierung für wiederholte runs
probleme auf der realen amber-infrastruktur behoben: 1. ohne festen CLIENT_SECRET_HEX erzeugt applesauce bei jedem lauf einen neuen client-pubkey. amber bindet permissions pro client-pubkey, also sah jeder lauf wie eine neue unberechtigte app aus und bekam "no permission" als auto-antwort. → CLIENT_SECRET_HEX in config + cli, SimpleSigner.fromKey durchgereicht. 2. applesauce wirft bei "already connected"/"no permission" unhandled rejections, weil response-promises asynchron reagieren. → globaler unhandledrejection-handler, der diese benannten fehler schluckt; connect() im try/catch mit open+force als fallback. 3. timeout auf bunker connect auf 60s erhöht (amber-pairing kann menschliches tap dauern beim ersten mal). einzel-post-publish live verifiziert: - offenheit-das-wesentliche als kind:30023 publiziert - alle 5 write-relays haben bestätigt - bild auf beide blossom-server hochgeladen - SPA rendert das bild von blossom.edufeed.org Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
18d9dad56e
commit
db61149924
|
|
@ -1,2 +1,3 @@
|
|||
.env
|
||||
logs/
|
||||
deno.lock
|
||||
|
|
|
|||
|
|
@ -79,9 +79,15 @@ async function cmdPublish(flags: {
|
|||
const runId = uuid()
|
||||
const logger = createLogger({ mode, runId })
|
||||
|
||||
const signer = await createBunkerSigner(config.bunkerUrl)
|
||||
console.log('[1/3] signer…')
|
||||
const signer = await createBunkerSigner(config.bunkerUrl, {
|
||||
clientSecretHex: config.clientSecretHex,
|
||||
})
|
||||
console.log('[2/3] outbox…')
|
||||
const outbox = await loadOutbox(config.bootstrapRelay, config.authorPubkeyHex)
|
||||
console.log('[3/3] blossom-server-liste…')
|
||||
const blossomServers = await loadBlossomServers(config.bootstrapRelay, config.authorPubkeyHex)
|
||||
console.log('setup done')
|
||||
if (outbox.write.length === 0) {
|
||||
console.error('no write relays in kind:10002')
|
||||
return 1
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ export interface Config {
|
|||
contentRoot: string
|
||||
clientTag: string
|
||||
minRelayAcks: number
|
||||
clientSecretHex?: string
|
||||
}
|
||||
|
||||
type EnvReader = (key: string) => string | undefined
|
||||
|
|
@ -36,6 +37,10 @@ export function loadConfig(read: EnvReader = (k) => Deno.env.get(k)): Config {
|
|||
if (!Number.isInteger(minAcks) || minAcks < 1) {
|
||||
throw new Error(`MIN_RELAY_ACKS must be a positive integer, got "${minAcksRaw}"`)
|
||||
}
|
||||
const clientSecretHex = read('CLIENT_SECRET_HEX')
|
||||
if (clientSecretHex && !/^[0-9a-f]{64}$/.test(clientSecretHex)) {
|
||||
throw new Error('CLIENT_SECRET_HEX must be 64 lowercase hex characters')
|
||||
}
|
||||
return {
|
||||
bunkerUrl: values.BUNKER_URL,
|
||||
authorPubkeyHex: values.AUTHOR_PUBKEY_HEX,
|
||||
|
|
@ -43,5 +48,6 @@ export function loadConfig(read: EnvReader = (k) => Deno.env.get(k)): Config {
|
|||
contentRoot: read('CONTENT_ROOT') ?? DEFAULTS.CONTENT_ROOT,
|
||||
clientTag: read('CLIENT_TAG') ?? DEFAULTS.CLIENT_TAG,
|
||||
minRelayAcks: minAcks,
|
||||
clientSecretHex,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { NostrConnectSigner } from 'applesauce-signers'
|
||||
import { NostrConnectSigner, SimpleSigner } from 'applesauce-signers'
|
||||
import { RelayPool } from 'applesauce-relay'
|
||||
import type { UnsignedEvent } from './event.ts'
|
||||
import type { SignedEvent } from './relays.ts'
|
||||
|
|
@ -13,6 +13,25 @@ const signerPool = new RelayPool()
|
|||
NostrConnectSigner.subscriptionMethod = (relays, filters) => signerPool.req(relays, filters)
|
||||
NostrConnectSigner.publishMethod = (relays, event) => signerPool.event(relays, event)
|
||||
|
||||
// Workaround: amber sendet bei wiederholten connect-requests mit bereits
|
||||
// bekanntem secret "already connected" oder "no permission". applesauce-
|
||||
// signers wirft daraufhin unhandled rejections, weil der request intern
|
||||
// schon aufgelöst wurde. wir schlucken diese benannten fehler prozessweit.
|
||||
const BENIGN_CONNECT_ERRORS = ['already connected', 'no permission']
|
||||
|
||||
function isBenignConnectError(msg: string): boolean {
|
||||
const lower = msg.toLowerCase()
|
||||
return BENIGN_CONNECT_ERRORS.some((e) => lower.includes(e))
|
||||
}
|
||||
|
||||
globalThis.addEventListener('unhandledrejection', (e: PromiseRejectionEvent) => {
|
||||
const reason = e.reason
|
||||
const msg = reason instanceof Error ? reason.message : String(reason)
|
||||
if (isBenignConnectError(msg)) {
|
||||
e.preventDefault()
|
||||
}
|
||||
})
|
||||
|
||||
function withTimeout<T>(p: Promise<T>, ms: number, label: string): Promise<T> {
|
||||
let timerId: number | undefined
|
||||
const timeoutPromise = new Promise<never>((_r, rej) => {
|
||||
|
|
@ -23,13 +42,44 @@ function withTimeout<T>(p: Promise<T>, ms: number, label: string): Promise<T> {
|
|||
}) as Promise<T>
|
||||
}
|
||||
|
||||
export async function createBunkerSigner(bunkerUrl: string): Promise<Signer> {
|
||||
const signer = await withTimeout(
|
||||
NostrConnectSigner.fromBunkerURI(bunkerUrl),
|
||||
30_000,
|
||||
'Bunker connect',
|
||||
)
|
||||
export interface CreateSignerOptions {
|
||||
clientSecretHex?: string
|
||||
}
|
||||
|
||||
export async function createBunkerSigner(
|
||||
bunkerUrl: string,
|
||||
options: CreateSignerOptions = {},
|
||||
): Promise<Signer> {
|
||||
const { remote, relays, secret } = NostrConnectSigner.parseBunkerURI(bunkerUrl)
|
||||
console.log(` signer: setup (remote=${remote.slice(0, 8)}…, relays=${relays.length})`)
|
||||
// Stabile client-identität: ohne festen CLIENT_SECRET_HEX erzeugt
|
||||
// applesauce pro lauf einen zufälligen key, und amber sieht jeden lauf
|
||||
// als neue app → permissions greifen nie. mit festem key bleibt die
|
||||
// identität über läufe erhalten.
|
||||
const clientSigner = options.clientSecretHex
|
||||
? SimpleSigner.fromKey(options.clientSecretHex)
|
||||
: undefined
|
||||
const signer = new NostrConnectSigner({ relays, remote, signer: clientSigner })
|
||||
const clientPubkey = await signer.signer.getPublicKey()
|
||||
console.log(` signer: client-pubkey=${clientPubkey.slice(0, 8)}…`)
|
||||
// connect() beim ersten mal nötig (damit amber die app registriert);
|
||||
// bei späteren runs ist amber schon gepaired mit diesem client-pubkey
|
||||
// und antwortet auf get_public_key / sign_event ohne erneuten connect.
|
||||
// wir versuchen connect, schlucken benign errors, und fallen-back auf
|
||||
// manuelles open().
|
||||
try {
|
||||
await withTimeout(signer.connect(secret), 60_000, 'Bunker connect')
|
||||
console.log(' signer: connect ok')
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err)
|
||||
if (!isBenignConnectError(msg)) throw err
|
||||
console.log(` signer: connect benign "${msg}", fallback to open+force`)
|
||||
await signer.open()
|
||||
;(signer as unknown as { isConnected: boolean }).isConnected = true
|
||||
}
|
||||
console.log(' signer: getPublicKey…')
|
||||
const pubkey = await withTimeout(signer.getPublicKey(), 30_000, 'Bunker getPublicKey')
|
||||
console.log(` signer: pubkey ok (${pubkey.slice(0, 8)}…)`)
|
||||
return {
|
||||
getPublicKey: () => Promise.resolve(pubkey),
|
||||
signEvent: async (ev: UnsignedEvent) => {
|
||||
|
|
|
|||
|
|
@ -12,7 +12,9 @@ export async function runCheck(config: Config): Promise<CheckResult> {
|
|||
const issues: string[] = []
|
||||
|
||||
try {
|
||||
const signer = await createBunkerSigner(config.bunkerUrl)
|
||||
const signer = await createBunkerSigner(config.bunkerUrl, {
|
||||
clientSecretHex: config.clientSecretHex,
|
||||
})
|
||||
const pk = await signer.getPublicKey()
|
||||
if (pk !== config.authorPubkeyHex) {
|
||||
issues.push(
|
||||
|
|
|
|||
Loading…
Reference in New Issue