mirror of
https://github.com/Routstr/routstr-core.git
synced 2026-10-30 07:37:39 +00:00
A fresh provider node currently has no Nostr identity and stays invisible until an operator pastes an nsec in the dashboard. This adds an opt-in boot-time provisioning path so a deployment can announce itself with no manual setup. - Add AUTO_GENERATE_NSEC (default false). When enabled and no legacy nsec/NSEC is configured, bootstrap_secrets generates an identity, stores it Fernet-encrypted, and sets the live nsec/npub. - The generated nsec is printed once at startup (like the admin password) so the operator can back it up; it never hits the log files. - The empty legacy slot is claimed with an atomic UPDATE so concurrent workers don't mint competing identities. - A provided NSEC always wins, an existing stored identity is reused, and an intentionally cleared identity is never resurrected. - Enable AUTO_GENERATE_NSEC=true in compose.yml so the bundled stack self-provisions; document the flag across README/docs/.env.example. - Add integration coverage for generation, idempotency, opt-in default, provided-nsec precedence, and cleared-identity behavior. Tests: 2329 unit passed; test_secret_bootstrap 21 passed; ruff + mypy clean.
106 lines
2.9 KiB
Python
106 lines
2.9 KiB
Python
"""Small adapter around the maintained ``nostr-sdk`` package."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from datetime import timedelta
|
|
from typing import Any, cast
|
|
|
|
from nostr_sdk import (
|
|
AckPolicy,
|
|
Client,
|
|
Event,
|
|
EventBuilder,
|
|
Filter,
|
|
Keys,
|
|
Kind,
|
|
PublicKey,
|
|
RelayUrl,
|
|
ReqExitPolicy,
|
|
ReqTarget,
|
|
SendEventTarget,
|
|
Tag,
|
|
)
|
|
|
|
|
|
def parse_keypair(secret_key: str) -> tuple[str, str]:
|
|
keys = Keys.parse(secret_key)
|
|
return keys.secret_key().to_hex(), keys.public_key().to_hex()
|
|
|
|
|
|
def generate_keypair() -> tuple[str, str]:
|
|
"""Generate a fresh Nostr identity as ``(nsec, npub)`` (both bech32).
|
|
|
|
Used to self-provision a node identity on first boot so a fresh deployment
|
|
can announce itself without an operator pasting an nsec. The caller owns
|
|
persisting the secret; this function never logs or stores it.
|
|
"""
|
|
keys = Keys.generate()
|
|
return keys.secret_key().to_bech32(), keys.public_key().to_bech32()
|
|
|
|
|
|
def create_signed_event(
|
|
secret_key_hex: str,
|
|
*,
|
|
kind: int,
|
|
content: str,
|
|
tags: list[list[str]],
|
|
) -> dict[str, Any]:
|
|
keys = Keys.parse(secret_key_hex)
|
|
event = (
|
|
EventBuilder(Kind(kind), content)
|
|
.tags([Tag.parse(tag) for tag in tags])
|
|
.finalize(keys)
|
|
)
|
|
return cast(dict[str, Any], json.loads(event.as_json()))
|
|
|
|
|
|
async def fetch_events(
|
|
relay_url: str,
|
|
*,
|
|
kind: int,
|
|
author: str,
|
|
limit: int,
|
|
timeout: int,
|
|
) -> list[dict[str, Any]]:
|
|
relay = RelayUrl.parse(relay_url)
|
|
client = Client()
|
|
await client.add_relay(relay)
|
|
try:
|
|
await client.connect()
|
|
event_filter = (
|
|
Filter().kinds([Kind(kind)]).authors([PublicKey.parse(author)]).limit(limit)
|
|
)
|
|
events = await client.fetch_events(
|
|
ReqTarget.single(relay, [event_filter]),
|
|
timeout=timedelta(seconds=timeout),
|
|
policy=ReqExitPolicy.WAIT_DURATION_AFTER_EOSE(timedelta(seconds=2.5)),
|
|
max_events=limit,
|
|
)
|
|
return [cast(dict[str, Any], json.loads(event.as_json())) for event in events]
|
|
finally:
|
|
await client.shutdown()
|
|
|
|
|
|
async def send_event(relay_url: str, event: dict[str, Any], *, timeout: int) -> None:
|
|
relay = RelayUrl.parse(relay_url)
|
|
client = Client()
|
|
await client.add_relay(relay)
|
|
try:
|
|
await client.connect()
|
|
output = await client.send_event(
|
|
Event.from_json(json.dumps(event)),
|
|
target=SendEventTarget.to([relay]),
|
|
ack_policy=AckPolicy.all(),
|
|
ok_timeout=timedelta(seconds=timeout),
|
|
)
|
|
if output.failed or relay not in output.success:
|
|
reasons = ", ".join(
|
|
f"{url}: {reason}" for url, reason in output.failed.items()
|
|
)
|
|
raise RuntimeError(
|
|
f"Relay did not accept event: {reasons or 'no OK from relay'}"
|
|
)
|
|
finally:
|
|
await client.shutdown()
|