Closesaiolabs/tasks#3. Pre-cascade prerequisite for aiolabs/lnbits#17
(signer abstraction phase 1), which lands an m002 startup job that
NULLs the legacy `accounts.prvkey` column. After this migration, the
tasks extension reads no plaintext nsec and works with any
NostrSigner backend (LocalSigner / RemoteBunkerSigner / ClientSideOnlySigner).
## What changed
### nostr_hooks.py — three publisher entry points
Was: `_account_keys(wallet_id)` helper pulled `(account.pubkey,
account.prvkey)` from the wallet's owning account, returned None when
prvkey was missing, then passed both to the publishers.
Now: each of `publish_or_delete_task_event`,
`publish_task_completion`, and `publish_completion_delete` calls
`await resolve_for_wallet(...)` (the DRY helper from
aiolabs/lnbits#23 — wallet → account → signer → can_sign-check in
one call, returns None on any soft-fail). The resolved `NostrSigner`
is passed to the publisher. Soft-skip on None (wallet missing,
account unclassified, or ClientSideOnlySigner where the server has
no signing authority).
Removed the `_account_keys` helper entirely.
### nostr_publisher.py — three publishers
Was: `publish_task_to_nostr`, `publish_completion_to_nostr`, and
`publish_completion_delete_to_nostr` each accepted
`(account_pubkey: str, account_prvkey: str)` and signed via a local
`sign_nostr_event` helper that called `coincurve.PrivateKey
.sign_schnorr` directly on the plaintext nsec.
Now: each publisher accepts `signer: NostrSigner`. Signing is
factored into a shared `_sign_and_publish` helper that builds the
unsigned event dict (`kind`/`created_at`/`tags`/`content`), hands it
to `await signer.sign_event(...)`, and writes `id`/`pubkey`/`sig`
back onto the local `NostrEvent` model before publishing. The signer
backend (LocalSigner / RemoteBunkerSigner) is transparent.
Removed the `sign_nostr_event` helper entirely — the signer
abstraction handles all signing now.
Dropped the `coincurve` import; no direct crypto in this extension.
## Acceptance
- [x] `_account_keys` helper removed (nostr_hooks no longer touches account.prvkey)
- [x] all three publishers accept NostrSigner instead of (pubkey, prvkey)
- [x] extension-local Schnorr code removed (sign_nostr_event gone)
- [x] coincurve import dropped
- [x] re-grep `tasks/`: zero `account.prvkey` references
- [x] version bumped: 0.0.1 → 0.0.2 (catalog entry deferred until cascade lands)
Manual smoke testing + tag + catalog entry follow the migration
landing; will run against the regtest stack with lnbits on
`issue-18-phase-2.3` (which validates both LocalSigner and
RemoteBunkerSigner signing paths end-to-end).
## Cross-references
- aiolabs/tasks#3 — issue this commit closes
- aiolabs/lnbits#17 — the cascading signer-abstraction PR
- aiolabs/lnbits#23 — the resolve_for_wallet helper this uses
- aiolabs/lnbits#21 — umbrella audit (5 affected extensions)
- aiolabs/events#23 — sister migration (already on signer-abstraction branch)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Generated 256x256 checkmark on indigo. Fixes the 404 on
/tasks/static/image/tasks.png observed in lnbits logs. Real branding
to come.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
m001 was leaving partial state on first boot: CREATE TABLE succeeded
but `CREATE UNIQUE INDEX ... ON tasks.tasks (...)` failed silently in
sqlite (schema prefix on the target table, not the index name, is not
what sqlite expects for ATTACHed databases). Because LNbits never
recorded the migration version, every restart retried m001 and crashed
on `CREATE TABLE tasks already exists`.
Two fixes:
- Use IF NOT EXISTS on every CREATE so a partial run is safe to re-run.
- Schema-prefix the index *name*, not the target table:
`CREATE INDEX tasks.idx ON tbl (...)` instead of `CREATE INDEX idx ON tasks.tbl (...)`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
views_api wires the full task lifecycle: create / update / delete /
list (per-wallet, public, admin-all) and the completion flow (claim /
start / complete via POST, unclaim via DELETE, plus a "mine" lookup
for the current user's claim on a task or specific occurrence).
Auth model: tasks are owned by an LNbits wallet but signed with the
wallet owner's account.pubkey — _wallet_pubkey resolves that pubkey at
create time and refuses to create tasks for accounts that haven't
generated a keypair yet, so we never publish a task we can't sign.
Completions optimistically insert with a local hash, publish to Nostr,
then re-insert under the actual event id so a later delete can find it.
Static SPA: Quasar-UMD index.vue / index.js mirroring the events
extension layout — wallet picker, task table, create/edit dialog with
optional daily/weekly recurrence, plus an admin-only public_listing
toggle. /tasks/:id and display.vue intentionally left out until the
public task page lands.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- nostr/ vendors NostrEvent + the nostrclient WebSocket bridge from
the events extension, retagged [TASKS] / subscription-id "tasks-*".
- nostr_publisher builds kind 31922 with the `event-type: task` tag
(per aiolabs/webapp#25 — disambiguates from kind-31922 activities on
shared relays), kind 31925 with task-status / occurrence /
completed_at, and kind 5 deletions for both.
- nostr_hooks bridges task/completion mutations to the publisher and
persists the resulting nostr_event_id back onto the local row.
- nostr_sync subscribes to {31922, 31925, 5/#k} and filters 31922
client-side on `event-type: task` because most relays don't index
custom single-letter tags.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Schema mirrors the webapp tasks module's ScheduledEvent + EventCompletion
shape:
- tasks.tasks (NIP-52 kind 31922 cache): (pubkey, d_tag) is the
parameterized-replaceable key. JSON-encoded participants / categories
/ recurrence columns are decoded on read via _parse_task_row so each
model can keep clean field validators.
- tasks.completions (kind 31925 cache): unique on
(task_address, pubkey, occurrence). occurrence is NULL for one-shot
tasks; create_completion deletes any prior claim for the same triple
so the latest event wins.
- tasks.settings: singleton row with public_listing toggle.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Empty skeleton mirroring the events extension layout: config/manifest,
pyproject + Makefile + ruff/mypy config, FastAPI routers wired into
tasks_ext, NostrClient bootstrap stubs, and an empty static/routes.json.
Models, migrations, CRUD, Nostr publisher/sync, and the API/template
layers land in follow-up commits.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>