TemplateResourceForm: create mode is unguarded against a same-second collision at a user-chosen address #74

Open
opened 2026-07-31 09:25:15 +00:00 by laoc · 0 comments
Owner

Follow-up from #72 / PR #73, measured by TestOER while verifying that fix. Not a regression from #73 — it is the boundary the #73 guard deliberately does not cover.

What happens

TemplateResourceForm.svelte guards edit mode only:

created_at: nextCreatedAt(isEditMode ? resourceEvent : null)

In create mode there is no predecessor in hand, so the timestamp is just "now". That is fine when the address is new — but the address is not guaranteed to be new. resolveResourceDTag (src/lib/helpers/educational/formReference.js:35-43) takes the create-mode d tag from emittedD, i.e. the form's amb:id field — a url the user types. So the same author can create at an address that already holds a resource, with no edit involved.

Measured as shipped (TestOER, local relay, 10cdb4f8)

relay before   EXISTING       @1785489627
create at      1785489627     <- exact tie, no error, navigated to /naddr…
IDB after      CREATE-COLLIDE @1785489627
relay after    EXISTING       @1785489627   <- unchanged

The client caches and renders its own version while the relay still serves the old one, and a cache hit ends the address loader before any relay is asked — so the divergence is not self-healing on reload.

Scope, stated honestly

  • The test relay tie-breaks keep-old. NIP-01 says lower id wins on a created_at tie, so on a conforming relay this diverges roughly half the time, not always.
  • It needs the same author to create at a url that already holds their resource and land in the same second.

Narrow. But it is #62's class of bug (client and relay disagree about which version is current, silently), at the one boundary #73 left open.

Why it was left open, and what a fix would cost

The edit-mode guard works because the predecessor event is already loaded. Create mode has nothing to compare against, so a real fix means looking the address up before publishing — an eventStore.replaceable(30142, pubkey, dTag) check plus, for correctness against a relay we have never queried, an addressLoader round trip. That turns a synchronous submit into an async pre-flight and adds a "this url already has a resource — edit it instead?" decision that is arguably a UX question, not a timestamp question.

Options

  1. Do nothing. Narrow, ~50% on a conforming relay, requires user-caused address reuse.
  2. Cheap partial: if eventStore already holds an event at (pubkey, dTag) in create mode, pass it to nextCreatedAt — same one-line shape as edit mode. Closes the case where the resource was loaded this session; silent no-op otherwise.
  3. Full: async pre-flight via addressLoader + surface "this address is taken" in the form.

Recommendation: 2, if anything. It is the same guard already in the file, costs nothing, and cannot make the current behaviour worse. 3 is a UX decision for @laoc_buzz.

Evidence and setup: RESEARCH/ISSUE_72_TEMPLATE_RESOURCE_FORM_GUARDS.md (TestOER).

Follow-up from #72 / PR #73, measured by TestOER while verifying that fix. Not a regression from #73 — it is the boundary the #73 guard deliberately does not cover. ## What happens `TemplateResourceForm.svelte` guards **edit** mode only: ```js created_at: nextCreatedAt(isEditMode ? resourceEvent : null) ``` In create mode there is no predecessor in hand, so the timestamp is just "now". That is fine when the address is new — but the address is **not** guaranteed to be new. `resolveResourceDTag` (`src/lib/helpers/educational/formReference.js:35-43`) takes the create-mode `d` tag from `emittedD`, i.e. the form's `amb:id` field — **a url the user types**. So the same author can create at an address that already holds a resource, with no edit involved. ## Measured as shipped (TestOER, local relay, `10cdb4f8`) ``` relay before EXISTING @1785489627 create at 1785489627 <- exact tie, no error, navigated to /naddr… IDB after CREATE-COLLIDE @1785489627 relay after EXISTING @1785489627 <- unchanged ``` The client caches and renders its own version while the relay still serves the old one, and a cache hit ends the address loader before any relay is asked — so the divergence is not self-healing on reload. ## Scope, stated honestly - The test relay tie-breaks keep-old. NIP-01 says **lower id wins** on a `created_at` tie, so on a conforming relay this diverges roughly **half** the time, not always. - It needs the same author to create at a url that already holds their resource **and** land in the same second. Narrow. But it is #62's class of bug (client and relay disagree about which version is current, silently), at the one boundary #73 left open. ## Why it was left open, and what a fix would cost The edit-mode guard works because the predecessor event is already loaded. Create mode has nothing to compare against, so a real fix means **looking the address up before publishing** — an `eventStore.replaceable(30142, pubkey, dTag)` check plus, for correctness against a relay we have never queried, an `addressLoader` round trip. That turns a synchronous submit into an async pre-flight and adds a "this url already has a resource — edit it instead?" decision that is arguably a UX question, not a timestamp question. ## Options 1. **Do nothing.** Narrow, ~50% on a conforming relay, requires user-caused address reuse. 2. **Cheap partial:** if `eventStore` already holds an event at `(pubkey, dTag)` in create mode, pass it to `nextCreatedAt` — same one-line shape as edit mode. Closes the case where the resource was loaded this session; silent no-op otherwise. 3. **Full:** async pre-flight via `addressLoader` + surface "this address is taken" in the form. Recommendation: **2**, if anything. It is the same guard already in the file, costs nothing, and cannot make the current behaviour worse. 3 is a UX decision for @laoc_buzz. Evidence and setup: `RESEARCH/ISSUE_72_TEMPLATE_RESOURCE_FORM_GUARDS.md` (TestOER).
Sign in to join this conversation.
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
edufeed/edufeed-app#74
No description provided.