Feature: NIP-17 order intake transport (gift-wrapped DMs) #9

Open
opened 2026-05-11 06:57:55 +00:00 by padreug · 0 comments
Owner

Background

Order placement today is REST: customer webapp posts to
POST /restaurant/api/v1/orders, restaurant LNbits responds with a
bolt11. The longer-term direction (per
/etc/nixos/modules/dev-env/docs/stack-overview.md's "Future
direction: Nostr-native transport" section) is to move
inter-service communication onto signed Nostr events as RPC.

For ordering, that means NIP-17 gift-wrapped DMs (kind 1059 →
kind 14 unwrap via NIP-44 v2). References:
~/dev/refs/repos/nostr-protocol/nips/17.md,
~/dev/refs/repos/nostr-protocol/nips/44.md.

The extension already has the dispatcher scaffolded —
nostr_sync._place_order_from_dm is complete and wired behind a
settings flag (settings.nostr_orders_enabled). What's missing is
(a) the NIP-44 v2 unwrap on the extension side and (b) the NIP-44
wrap + signing on the webapp side.

The customer webapp's v1 ships REST-only to keep that path
fast; this issue tracks the additive Nostr-native transport for v2.

Goals

Extension side

  • Implement NIP-44 v2 ChaCha20 + HMAC-SHA256 unwrap of the seal
    (kind 13) and re-unwrap of the rumor (kind 14) inside
    nostr_sync._handle_gift_wrapped_dm.
  • Decoded payload feeds the existing _place_order_from_dm helper
    unchanged.
  • Status updates back to the customer pubkey go out as further
    NIP-17 DMs (paid → accepted → ready → delivered).
  • settings.nostr_orders_enabled gates the listener; flag
    documented in docs/nostr-layer.md.
  • Replay protection: dedupe by inbound event.id to prevent the
    same DM from re-creating an order if a relay re-delivers.

Webapp side

  • NIP-07 (browser extension) signer integration in
    AuthService so we can sign customer events without holding the
    user's nsec in webapp memory. Detect window.nostr, prompt on
    first use, sign via the extension.
  • Order placement composable gains a transport: 'rest' | 'nostr'
    option, default rest.
  • When nostr, the cart's CreateOrder payload is gift-wrapped to
    the restaurant's nostr_pubkey and published; the webapp
    subscribes for the response DM carrying the bolt11; payment then
    flows through the same WalletService.payInvoice as the REST
    path.

Open design questions

  • Key custody. Where does the customer's secret key live?
    NIP-07 (browser extension), NIP-46 (remote signer / nsec.app),
    or webapp-generated and stored encrypted in localStorage? Today
    the webapp's AuthService caches account.prvkey from the
    LNbits account record — that is not acceptable as a
    long-term solution; v1 of NIP-17 should require NIP-07.
  • Bolt11 delivery channel. Same gift-wrapped DM stream, or a
    separate request/response convention? NIP-17 is one-way DM
    semantically; restaurants need to write back. Probably a matched
    DM with ["e", <customer_request_event_id>] as a thread tag.
  • Latency. REST is sub-second locally; relay roundtrip can be
    several seconds. UX needs an interim "Submitting…" state with a
    reasonable timeout.
  • Backwards compatibility. REST stays the supported path —
    this is additive. Operators may want it for non-Nostr customers
    (kiosks, web fallback).

Acceptance criteria

Extension

  • nostr_sync._handle_gift_wrapped_dm performs full NIP-44 v2
    unwrap and dispatches to _place_order_from_dm.
  • Order status transitions emit NIP-17 status DMs to the
    customer pubkey (paid → accepted → ready → delivered).
  • settings.nostr_orders_enabled gates the listener; flag
    documented in docs/nostr-layer.md.
  • Replay dedupe by inbound event.id.
  • Tests covering wrap → unwrap round-trip, replay dedup, and
    status DM emission.

Webapp

  • NIP-07 signer support in AuthService (detect
    window.nostr, prompt, sign).
  • useCheckout() gains a transport: 'rest' | 'nostr'
    option; default rest.
  • When nostr, the cart's CreateOrder is gift-wrapped to
    the restaurant pubkey and published; webapp subscribes for
    the response DM carrying the bolt11; payment then flows
    through the same WalletService.payInvoice.
  • docs/webapp-integration.md and the extension's
    docs/nostr-layer.md both gain the live (not-stubbed)
    NIP-17 flow description.

Out of scope

  • NIP-46 remote signer (separate issue if we want nsec.app
    support).
  • Removing REST. REST stays — operators may want it for non-Nostr
    customers (kiosks, web fallback).
  • Encrypted ordering analytics or any non-DM data flows.

Files / surfaces

Extension:

  • ~/dev/shared/extensions/restaurant/nostr_sync.py
  • ~/dev/shared/extensions/restaurant/nostr_publisher.py
  • ~/dev/shared/extensions/restaurant/nostr/event.py
  • ~/dev/shared/extensions/restaurant/services.py (status-
    transition hook for outbound DMs)
  • ~/dev/shared/extensions/restaurant/docs/nostr-layer.md

Webapp:

  • ~/dev/webapp/src/modules/base/auth/auth-service.ts
  • ~/dev/webapp/src/modules/base/nostr/relay-hub.ts (if it needs
    publishing helpers it doesn't already expose)
  • ~/dev/webapp/src/modules/restaurant/composables/useCheckout.ts
  • ~/dev/webapp/src/modules/restaurant/services/RestaurantNostrSync.ts

See also

  • #8 — festival aggregator (different concern but related Nostr-
    native trajectory)
  • #2 — tiered operator modes (operator may toggle whether their
    venue accepts Nostr-DM orders at all)

References

  • NIP-17: ~/dev/refs/repos/nostr-protocol/nips/17.md
  • NIP-44: ~/dev/refs/repos/nostr-protocol/nips/44.md
  • OmniXY stack overview "Future direction: Nostr-native transport"
    at /etc/nixos/modules/dev-env/docs/stack-overview.md
## Background Order placement today is REST: customer webapp posts to `POST /restaurant/api/v1/orders`, restaurant LNbits responds with a bolt11. The longer-term direction (per `/etc/nixos/modules/dev-env/docs/stack-overview.md`'s "Future direction: Nostr-native transport" section) is to move inter-service communication onto **signed Nostr events as RPC**. For ordering, that means **NIP-17 gift-wrapped DMs** (kind 1059 → kind 14 unwrap via NIP-44 v2). References: `~/dev/refs/repos/nostr-protocol/nips/17.md`, `~/dev/refs/repos/nostr-protocol/nips/44.md`. The extension already has the **dispatcher** scaffolded — `nostr_sync._place_order_from_dm` is complete and wired behind a settings flag (`settings.nostr_orders_enabled`). What's missing is (a) the NIP-44 v2 unwrap on the extension side and (b) the NIP-44 wrap + signing on the webapp side. The customer webapp's v1 ships **REST-only** to keep that path fast; this issue tracks the additive Nostr-native transport for v2. ## Goals ### Extension side - Implement NIP-44 v2 ChaCha20 + HMAC-SHA256 unwrap of the seal (kind 13) and re-unwrap of the rumor (kind 14) inside `nostr_sync._handle_gift_wrapped_dm`. - Decoded payload feeds the existing `_place_order_from_dm` helper unchanged. - Status updates back to the customer pubkey go out as further NIP-17 DMs (`paid → accepted → ready → delivered`). - `settings.nostr_orders_enabled` gates the listener; flag documented in `docs/nostr-layer.md`. - Replay protection: dedupe by inbound `event.id` to prevent the same DM from re-creating an order if a relay re-delivers. ### Webapp side - **NIP-07** (browser extension) signer integration in `AuthService` so we can sign customer events without holding the user's nsec in webapp memory. Detect `window.nostr`, prompt on first use, sign via the extension. - Order placement composable gains a `transport: 'rest' | 'nostr'` option, default `rest`. - When `nostr`, the cart's `CreateOrder` payload is gift-wrapped to the restaurant's `nostr_pubkey` and published; the webapp subscribes for the response DM carrying the bolt11; payment then flows through the same `WalletService.payInvoice` as the REST path. ## Open design questions - **Key custody.** Where does the customer's secret key live? NIP-07 (browser extension), NIP-46 (remote signer / nsec.app), or webapp-generated and stored encrypted in localStorage? Today the webapp's `AuthService` caches `account.prvkey` from the LNbits account record — that is **not acceptable** as a long-term solution; v1 of NIP-17 should **require NIP-07**. - **Bolt11 delivery channel.** Same gift-wrapped DM stream, or a separate request/response convention? NIP-17 is one-way DM semantically; restaurants need to write back. Probably a matched DM with `["e", <customer_request_event_id>]` as a thread tag. - **Latency.** REST is sub-second locally; relay roundtrip can be several seconds. UX needs an interim "Submitting…" state with a reasonable timeout. - **Backwards compatibility.** REST stays the supported path — this is additive. Operators may want it for non-Nostr customers (kiosks, web fallback). ## Acceptance criteria ### Extension - [ ] `nostr_sync._handle_gift_wrapped_dm` performs full NIP-44 v2 unwrap and dispatches to `_place_order_from_dm`. - [ ] Order status transitions emit NIP-17 status DMs to the customer pubkey (`paid → accepted → ready → delivered`). - [ ] `settings.nostr_orders_enabled` gates the listener; flag documented in `docs/nostr-layer.md`. - [ ] Replay dedupe by inbound `event.id`. - [ ] Tests covering wrap → unwrap round-trip, replay dedup, and status DM emission. ### Webapp - [ ] NIP-07 signer support in `AuthService` (detect `window.nostr`, prompt, sign). - [ ] `useCheckout()` gains a `transport: 'rest' | 'nostr'` option; default `rest`. - [ ] When `nostr`, the cart's `CreateOrder` is gift-wrapped to the restaurant pubkey and published; webapp subscribes for the response DM carrying the bolt11; payment then flows through the same `WalletService.payInvoice`. - [ ] `docs/webapp-integration.md` and the extension's `docs/nostr-layer.md` both gain the live (not-stubbed) NIP-17 flow description. ## Out of scope - NIP-46 remote signer (separate issue if we want nsec.app support). - Removing REST. REST stays — operators may want it for non-Nostr customers (kiosks, web fallback). - Encrypted ordering analytics or any non-DM data flows. ## Files / surfaces **Extension:** - `~/dev/shared/extensions/restaurant/nostr_sync.py` - `~/dev/shared/extensions/restaurant/nostr_publisher.py` - `~/dev/shared/extensions/restaurant/nostr/event.py` - `~/dev/shared/extensions/restaurant/services.py` (status- transition hook for outbound DMs) - `~/dev/shared/extensions/restaurant/docs/nostr-layer.md` **Webapp:** - `~/dev/webapp/src/modules/base/auth/auth-service.ts` - `~/dev/webapp/src/modules/base/nostr/relay-hub.ts` (if it needs publishing helpers it doesn't already expose) - `~/dev/webapp/src/modules/restaurant/composables/useCheckout.ts` - `~/dev/webapp/src/modules/restaurant/services/RestaurantNostrSync.ts` ## See also - #8 — festival aggregator (different concern but related Nostr- native trajectory) - #2 — tiered operator modes (operator may toggle whether their venue accepts Nostr-DM orders at all) ## References - NIP-17: `~/dev/refs/repos/nostr-protocol/nips/17.md` - NIP-44: `~/dev/refs/repos/nostr-protocol/nips/44.md` - OmniXY stack overview "Future direction: Nostr-native transport" at `/etc/nixos/modules/dev-env/docs/stack-overview.md`
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
aiolabs/restaurant#9
No description provided.