Skip to content

fix(widget): both write paths POST to deleted SahajCloud endpoints #214

Closed as duplicate

Description

@sydevs-bot

Summary

sydevs/SahajCloud#801 replaced the widget's two write endpoints with one unified intake, and this repo was never updated. createRegistration still posts to POST /api/events/{id}/register, deleted with src/collections/Events/endpoints/registerForEvent.ts. sendUserMessage still posts to POST /api/user-messages, whose collection was deleted in the same PR. Event registration and the issue-report form both fail against cloud.sydevelopers.com.

Approach

What replaced them. One collection takes every public write, discriminated by type — contact, subscribe, registration, proposal. Registration is type: 'registration'. An issue report is type: 'contact'. The Turnstile header does not change: the write guard runs above the collection, before anything reads type.

Both call sites are in src/config/api/mutate.ts:

line sends now send instead
138 POST /events/{id}/register POST /user-submissions, type: 'registration'
288 POST /user-messages POST /user-submissions, type: 'contact'

⚠ The two halves are not equally ready — the report half is blocked

A contact submission must name a form, and this widget has no way to obtain one. needsForm (SahajCloud src/collections/UserSubmissions/fields.ts:175) returns true for every contact row — the one exemption covers subscribe only — and perTypeForm marks the relationship required under that condition, which Payload evaluates server-side on the REST path. A contact create with no form is refused by plain required validation, with no machine-readable code to map.

The widget cannot supply one:

  • The sahaj-atlas project grants regions, events, images, files (SahajCloud src/plugins/access/config/projects.ts:83). forms sits in wemeditate-web only, so the atlas key cannot read or list it.
  • sy-atlas-config carries canonicalFallbackClient, defaultMapCenter, defaultZoomLevel — no form reference.
  • prepareUserSubmission then cross-checks form.actionType against type and refuses submission_type_mismatch, so any id would also have to be a contact form.

So the registration half is mechanical and shippable now; the report half needs a SahajCloud decision first. See ## Open questions. Do not open a PR that half-lands the report path.

Field mapping — columns vs submissionData pairs

submissionData is a [{ field, value }] array, not a flat body. Real columns stay columns. SahajCloud src/collections/UserSubmissions/submissionData.ts is the authority.

Registration (mutate.ts:136):

widget value goes to
eventId event relationship column
data.email senderEmail column
data.startingAt startingAt column (timezone-aware date)
data.name submissionData pair, key name
activeLocale() submissionData pair, key locale
data.questions one pair per entry — Object.entries

The question keys already line up. RegistrationQuestionNameSchema (src/types/event.ts) and SahajCloud's EVENT_REGISTRATION_QUESTIONS name the same four: experience, referral, aspirations, questions. No renaming needed — TYPE_SUBMISSION_KEYS.registration is built from that same list.

Report (mutate.ts:259): message and subject become pairs; senderEmail stays a column; the four context keys flatten to pairs under their existing names.

⚠ Those key names are load-bearing. URL_EXEMPT_KEYS is exactly path, hostUrl, error, userAgent, locale. The write guard's URL scan walks every other pair, so renaming one — or sending pageUrl instead of hostUrl — starts refusing ordinary bug reports with urls_not_allowed. mutate.ts:267 already does this rename; keep it.

New bounds to respect

checkSubmissionData adds bounds the flat body never had:

  • At most 40 pairs per submission (MAX_SUBMISSION_DATA_ENTRIES).
  • Key length ≤ 100 (MAX_SUBMISSION_DATA_KEY_LENGTH).
  • Value ≤ 2000 by default, but 5000 for message, note and error (VALUE_MAX_LENGTHS).
  • A duplicate key, a non-string value, or a key the type does not accept → submission_data_invalid (400). The message names the offending key.

The widget's uniform CONTEXT_MAX = 2000 (mutate.ts:235) stays correct and is now merely stricter than the server for error. Leave it — a truncated error string is a better trade than a refused report.

The success shape changes — but nothing reads it

RegistrationResponseSchema (mutate.ts:12) expects { ok: true, registration: { id, uuid } }. The intake returns Payload's 201 create envelope. No consumer reads any of it: RegistrationForm.tsx:190 is onSuccess: () => setSubmitted(true). So narrow the schema to { doc: { id: z.number() } }, matching UserMessageResponseSchema (mutate.ts:178) and for the same stated reason — id is the one field a CMS rename cannot touch.

The report path's schema needs no change at all.

Refusal codes

gateRegistration.ts throws a Payload APIError carrying code at errors[].data.code. asRefusal (mutate.ts:95) already reads both positions, so it keeps working untouched.

⚠ event_not_found will NOT arrive from pnpm types:cms. SahajCloud's responseTypes.ts:71 lists four codes and its own JSDoc states "the plain not-found 404 carries no code" — while gateRegistration.ts:61 throws { code: 'event_not_found' } on that very 404. The gate is what actually runs. Add the code to the local union the way captcha_failed already is, with a comment saying why, and raise the contradiction upstream.

Two new 400 codes to map: submission_data_invalid and submission_type_mismatch. Both are integrator errors, not viewer-actionable, so they belong in the generic-failure path rather than getting their own copy.

The synced types

src/types/payload/payload-types.ts still declares registrations, event-submissions, user-messages and form-submissions, and has no user-submissions. Run pnpm types:cms in the same PR.

⚠ It also drops EventRegistrationResponse from response-types.ts — that export is gone upstream. Nothing here imports it (only EventRegistrationErrorCode, in mutate.ts:1 and RegistrationForm.tsx:3), so this is safe. Confirm with a grep rather than assuming; docs/rules/data-layer.md warns that a curled URL can keep resolving while serving something else.

Docs and specs in the same PR

  • docs/rules/data-layer.md names both endpoints twice — in the fetchers preamble ("SahajCloud exposes only raw collection reads and a few custom endpoints … POST /api/events/:id/register") and in the whole Mutations section. Both are now wrong.
  • src/config/api/mutate.test.ts pins the old paths (createRegistration describe block, from line 47). Update its assertions to the new path, type, and pair shape.
  • Two comments name deleted paths: mutate.ts:230 cites SahajCloud's src/collections/UserMessages/UserMessages.ts for the 2000-character bound, which now lives in src/collections/UserSubmissions/submissionData.ts (and is per-key, not uniform); ReportIssueForm.tsx:83 names the old endpoint.
  • docs/embedding.md needs no change — the origin is unchanged, so the host CSP table is unaffected.

Acceptance criteria

  • No request in src/ targets /events/{id}/register or /user-messages.
  • A registration submits as a type: 'registration' create on /api/user-submissions and reaches its confirmation screen.
  • event, senderEmail and startingAt are sent as top-level fields, and name, locale and each enabled question as submissionData pairs.
  • The report's context pairs keep the keys path, hostUrl, userAgent, locale, error — the set URL_EXEMPT_KEYS names.
  • Both requests still send x-turnstile-token.
  • external_registration, event_ended, registration_closed, event_full and event_not_found each render refusal copy.
  • submission_data_invalid and submission_type_mismatch fall through to the generic failure message, not a blank screen.
  • src/types/payload/payload-types.ts matches SahajCloud main, and nothing imports a type the re-sync removed.
  • docs/rules/data-layer.md's Mutations section describes the intake, not the two deleted endpoints.
  • The comments at mutate.ts:230 and ReportIssueForm.tsx:83 name paths that exist.

Verification checklist

  • pnpm types:cms && git diff --stat src/types/payload/ shows the re-sync.
  • grep -rn "EventRegistrationResponse" src/ returns nothing outside src/types/payload/.
  • grep -rnE "events/.+/register|'/user-messages'" src/ docs/ returns nothing.
  • pnpm lint && pnpm typecheck && pnpm test:run all clean.
  • pnpm test:run src/config/api/mutate.test.ts — the registration spec asserts the new path and the pair shape, and fails when reverted to the old body.
  • Against the deployed Cloudflare preview, submit the registration form on an open event. Confirm a 201, and a row at /admin/collections/user-submissions with type: registration, the four question answers as submissionData pairs, and senderEmail set.
  • Register for an event whose last date has passed. Confirm the 409 renders the "this event has ended" copy.
  • Register for an event id that does not exist. Confirm the 404 renders refusal copy, not the generic error.
  • Submit a registration with a 3000-character answer. Confirm the 400 does not blank the form.

Open questions

  • Where does the report form get its contact form id? Three options, and the choice is a SahajCloud change either way: (a) add a form reference to the sy-atlas-config global, which the widget already reads; (b) exempt a client-originated contact row from needsForm and fall back to the default recipient; (c) publish the id as a build-time VITE_ variable, which drifts silently and is the weakest of the three. (a) looks right — the widget already boots off that global and the id is not a secret.
  • Does the registration confirmation copy still hold? The deleted endpoint composed and sent the confirmation inline. The intake returns status: 'pending' and delivery is a queued job with no callback, so the widget is never told the outcome — the same narrowing report.sent was reworded for in fix(event): deliver report-issue submissions instead of alerting #103. If the copy must change, it is CMS-owned: that needs an issue on sydevs/SahajCloud against sy-atlas-translations, not an edit here.

Downstream impact

None outbound. This repo is the consumer; SahajCloud already shipped its half in #801. The two open questions above are inbound asks on SahajCloud.

Notes

Found by Thursday's survey-contracts sweep.

Evidence: sydevs/SahajCloud#801 deletes src/collections/Events/endpoints/registerForEvent.ts and src/collections/UserMessages/UserMessages.ts. src/collections/Events/endpoints/ now holds only geojson.ts, responseTypes.ts and verifyEventAction.ts. grep -rn "user-submissions" src/ in this repo returns nothing.

Verified against SahajCloud main at 07e3e40, reading the collection source rather than the API. GET https://cloud.sydevelopers.com/api/openapi.json needs a client key and returned 401.

sydevs/SahajCloud#796 covered the server-side port only. No ticket tracks the consumers.

🤖 Written by the sydevs autonomous loop — see what this is

Activity

  1. added theissue type on Sep 17, 2026
  2. added
    proposalFiled by the loop. Removed on the first human verb.
    bot:workingLock: a cloud session is running on this item. Removed when it ends.
    and removed on Sep 17, 2026
  3. sydevs-bot commented on Sep 17, 2026

    @sydevs-bot
    ContributorAuthor

    🤖 no session running

    recent dispatches
    • 01:49Z answer · attempt 1 · session · done
    • 01:49Z answer · attempt 1 · session
    • 08:15Z revise · attempt 1 · session · done
    • 08:15Z revise · attempt 1 · session

    Written by the sydevs dispatcher. Sessions never edit this comment.

  4. sydevs-bot commented on Sep 17, 2026

    @sydevs-bot
    ContributorAuthor

    The report half of this ticket is blocked; the registration half is not. A contact submission must name a forms id, and the atlas key cannot read forms — its project grants regions, events, images, files only, and sy-atlas-config carries no form reference. Added as an open question with three options.

    Other corrections from reading the code:

    • The registration question keys already match SahajCloud's list. No renaming — only reshaping into submissionData pairs.
    • Nothing reads the registration response, so the shape change forces no copy decision.
    • event_not_found contradicts SahajCloud's responseTypes.ts, which says a 404 carries no code. The gate throws one anyway.
    • Added the URL_EXEMPT_KEYS constraint, the new submissionData bounds, docs/rules/data-layer.md, and mutate.test.ts.

    Effort stays Hard.

    This is arguably two tickets — registration now, report once the form question is answered. Comment @sydevs-bot split to separate them. @sydevs-bot implement starts work.

    🤖 Written by the sydevs autonomous loop — see what this is


    Generated by Claude Code

  5. added and removed
    bot:workingLock: a cloud session is running on this item. Removed when it ends.
    on Sep 17, 2026
  6. moved this from Proposed to Revising in SYDevelopers Technical Roadmapon Sep 17, 2026
  7. Ardnived commented on Sep 18, 2026

    @Ardnived
    Contributor

    @sydevs-bot This ticket appears to be redundant with an already existing ticket: #213

    If that's true, close this ticket and log this to the ops journal. We should make sure that the daily surveys don't duplicate existing tickets.

  8. added
    bot:workingLock: a cloud session is running on this item. Removed when it ends.
    and removed
    proposalFiled by the loop. Removed on the first human verb.
    on Sep 18, 2026
  9. sydevs-bot commented on Sep 18, 2026

    @sydevs-bot
    ContributorAuthor

    Confirmed redundant. Closing this as a duplicate of #195.

    #195 specifies the same migration and was filed 2026-09-04. PR #213 implements it and opened 2026-09-17 01:34Z — seven hours before this ticket was filed. That PR changes every file named here: src/config/api/mutate.ts, mutate.test.ts, docs/rules/data-layer.md, src/types/payload/.

    Nothing here is lost by closing:

    This ticket's claim "No ticket tracks the consumers" was wrong. The survey read SahajCloud, not this repo's open work.

    Logged the survey gap to the ops journal, sydevs/claude-workflow#111. A dedupe step for the survey skills needs a claude-workflow PR, which I have not opened.

    🤖 Written by the sydevs autonomous loop — see what this is


    Generated by Claude Code

  10. added and removed
    bot:workingLock: a cloud session is running on this item. Removed when it ends.
    on Sep 18, 2026
  11. moved this from Revising to Done in SYDevelopers Technical Roadmapon Sep 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Fields

    Priority

    Critical

    Hold Until

    None yet

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions