Skip to content

Nourish Stability — opportunistic pantry republish (drift #4) with authenticated payload #314

Description

@spe1020

Placeholder for PR 3.5 in the Nourish Stability feature. Closes drift source #4 (silent pantry publish failure) — deferred from #313 after Copilot review surfaced a pantry-spoofing vector in the original blob-trust design.

Background

The original commit 5 in #313 added a skipRecompute: true branch to /api/nourish that let any client submit a score blob + coordinates and have the server sign + publish it under NOURISH_SERVICE_PUBKEY. Membership gate was bypassed on the rationale that "every localStorage entry originated server-side, so the blob can be trusted."

Copilot caught the flaw: the endpoint is reachable via direct HTTP (curl), bypassing the entire client-side trust chain. The "no existing code path writes arbitrary JSON" argument only covers application-originated writes — it says nothing about an attacker calling the endpoint directly with fabricated input. Server endpoints need server-side trust verification, not inferred trust from application-layer analysis.

See the Copilot review thread on #313 for the full diagnosis.

Damage bound (why drift #4 can wait one more PR cycle)

Kind 30078 is NIP-33 addressable by d-tag. A fabricated event at coordinates (NOURISH_SERVICE_PUBKEY, nourish:30023:pubkey:dTag) is overwritten by the next legitimate rescore of that recipe. So the damage is bounded but not zero:

  • A malicious actor can fabricate a score for any recipe
  • That score propagates to pantry under the service pubkey (appears legitimate to clients)
  • It stays there until the next legitimate rescore overwrites it
  • No recipe can carry two fabricated scores simultaneously (d-tag collision)

"Every recipe on zap.cooking shows a fabricated Nourish score until we manually rescore" is a real bad day if scripted. We don't want to ship the endpoint without auth.

What PR 3.5 Phase 1 needs to investigate

Before implementation, answer these five questions:

  1. Auth token shape. HMAC bound to (recipePubkey, recipeDTag, contentHash, promptVersion, scoresHash) issued at compute time, verified at republish time? NIP-98 HTTP auth (user signs a request token with their pubkey)? Something else?
  2. Token lifecycle. Issued in the /api/nourish response at compute time. Persisted in localStorage alongside the score blob. Sent on republish. Server verifies HMAC against a server-side secret.
  3. Token expiry. Does it expire? If yes, how long? If no, is that a concern? (Lean: no expiry — the underlying score doesn't expire either, and token rotation adds complexity without clear threat model benefit.)
  4. Server-side secret management. Where does the HMAC signing key live? (Cloudflare Workers env var, same pattern as NOTIFICATION_PRIVATE_KEY.)
  5. Rotation. If the HMAC key is ever rotated, existing tokens become invalid and all republish healing stops working for cached scores. Is that acceptable? (Probably yes — rotation is rare, and users' next compute gets a fresh valid token.)

Scope

References

Priority

Not urgent — MODERATE severity, damage bound by replaceable-event semantics. Queue behind PR 4 (admin rescore loop) if PR 4 is ready to ship first; otherwise can slot in after PR 3 merges.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions