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:
- 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?
- 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.
- 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.)
- Server-side secret management. Where does the HMAC signing key live? (Cloudflare Workers env var, same pattern as
NOTIFICATION_PRIVATE_KEY.)
- 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.
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: truebranch to/api/nourishthat let any client submit a score blob + coordinates and have the server sign + publish it underNOURISH_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:"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:
(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?/api/nourishresponse at compute time. Persisted in localStorage alongside the score blob. Sent on republish. Server verifies HMAC against a server-side secret.NOTIFICATION_PRIVATE_KEY.)Scope
scoreResolver.tswhen winner is L2/L3 and pantry confirmed miss)OPENAI_API_KEYcheck below theskipRecomputebranch forkpromptVersionbefore validation{ success: false }on exceptions (not 200{ published: false })References
224a411on thefeat/nourish-stability-drift-fixesbranch pre-rebasePriority
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.