Open registration to guild members #27

Closed
opened 2026-08-08 06:06:30 +07:00 by sulthan · 1 comment
Owner

Parent

#19

What to build

The gate opens. Any member of the configured Discord guild becomes a Reader on their first login, with an empty library and their own install links waiting.

The second Reader is the first real test of everything the schema promised: isolation between libraries, and a shared Series polled once rather than twice. Also cleans up the two pieces of scaffolding left over from the cutover.

The riskiest thing here fails silently — a leak between Readers does not throw, does not log, and looks like working software until someone notices a series they never bookmarked. Isolation tests use two Readers and assert from both directions.

Acceptance criteria

  • Any member of the configured guild becomes a Reader on first successful login. No signup form, no invite code, no approval step.
  • First login and every later login are one code path; a second login reuses the existing Reader rather than creating another.
  • A non-member is refused clearly, and no Reader is created as a side effect of the refusal.
  • The owner-only restriction no longer gates login.
  • A brand-new Reader sees a deliberate empty library offering both userscript install links — not an error, not a blank page. It follows Cinder: no cards, corners or shadows, one measure column, tokens only, both colour branches touched together, and --ember is not borrowed for an empty state.
  • A new Reader's rendered userscript carries their own token, not the owner's.
  • Two Readers cannot read, modify or delete each other's Bookmarks, asserted from both directions rather than by counting one Reader's rows.
  • Two Readers bookmarking the same Series produce one Series and two Bookmarks with independent Progress; the second Reader's position starts at zero regardless of the first's.
  • A Series held by several Readers is fetched exactly once per due cycle.
  • One Reader deleting their Bookmark leaves the other Reader's Bookmark intact and the Series still polled.
  • The owner can revoke a specific Reader's session and the next request from it is rejected.
  • The expired grace path and its configuration are deleted, along with their tests. No unauthenticated-by-Reader route survives.
  • PRODUCT.md is rewritten: its claim of being single user with no accounts and no multi-user planned is false in all three clauses.

Blocked by

#26

## Parent #19 ## What to build The gate opens. Any member of the configured Discord guild becomes a Reader on their first login, with an empty library and their own install links waiting. The second Reader is the first real test of everything the schema promised: isolation between libraries, and a shared Series polled once rather than twice. Also cleans up the two pieces of scaffolding left over from the cutover. The riskiest thing here fails silently — a leak between Readers does not throw, does not log, and looks like working software until someone notices a series they never bookmarked. Isolation tests use two Readers and assert from both directions. ## Acceptance criteria - [x] Any member of the configured guild becomes a Reader on first successful login. No signup form, no invite code, no approval step. - [x] First login and every later login are one code path; a second login reuses the existing Reader rather than creating another. - [x] A non-member is refused clearly, and no Reader is created as a side effect of the refusal. - [x] The owner-only restriction no longer gates login. - [x] A brand-new Reader sees a deliberate empty library offering both userscript install links — not an error, not a blank page. It follows Cinder: no cards, corners or shadows, one measure column, tokens only, both colour branches touched together, and `--ember` is not borrowed for an empty state. - [x] A new Reader's rendered userscript carries their own token, not the owner's. - [x] Two Readers cannot read, modify or delete each other's Bookmarks, asserted from both directions rather than by counting one Reader's rows. - [x] Two Readers bookmarking the same Series produce one Series and two Bookmarks with independent Progress; the second Reader's position starts at zero regardless of the first's. - [x] A Series held by several Readers is fetched exactly once per due cycle. - [x] One Reader deleting their Bookmark leaves the other Reader's Bookmark intact and the Series still polled. - [x] The owner can revoke a specific Reader's session and the next request from it is rejected. - [x] The expired grace path and its configuration are deleted, along with their tests. No unauthenticated-by-Reader route survives. - [x] PRODUCT.md is rewritten: its claim of being single user with no accounts and no multi-user planned is false in all three clauses. ## Blocked by #26
sulthan added the ready-for-agent label 2026-08-08 06:06:30 +07:00
Author
Owner

Implemented on feat/open-registration-guild-members, PR #36 (b0bf6fe + f4f6c9c). Every criterion is ticked; how each was met, in the issue's order:

1-4. discordCallback checks guild membership (and DISCORD_REQUIRED_ROLE), then Store.EnsureReader — one code path, idempotent on discord_id. The refusal returns before EnsureReader, so nothing is created for a non-member; asserted by a reader-count check after a refused sign-in. OWNER_DISCORD_ID seeds the owner but no longer gates login.
5. Empty library offers both install links, placed behind the tab-specific empty states so "No favourites yet" is not shadowed. Tokens only, --danger for destruction, --ember untouched; both colour branches shot.
6. /install/* and /u/{cred}/* render with the acting Reader's derived credential — asserted to differ from the owner's.
7-10. Isolation asserted in both directions for read, modify and delete. Shared series: one series row, two independent progresses (second starts at zero), DueForLatestCheck returns it once, and one Reader's delete leaves the other's bookmark and the poll intact.
11. Owner-only POST /readers/{id}/revoke (404 for anyone else) plus a Readers panel. The owner's own row is deliberately not revocable — that is what logout is for — so the endpoint 404s on the owner id too.
12. API_TOKEN, API_TOKEN_GRACE_UNTIL, the legacy branch in httpmw.ResolveReader and their tests are gone, along with the references in .env.example, compose, README, DEPLOY, CUTOVER and REDEPLOY. A credential now authenticates exactly one Reader or nothing.
13. PRODUCT.md rewritten: guild members with their own libraries, accounts created by signing in, one owner capability.

Verification: go test ./... green (needs Docker). Live smoke against a throwaway Postgres — empty-library state in dark and light, roster render, a real revoke through the panel (target's next request 401s, owner untouched), owner self-revoke refused 404, per-Reader userscript path and bearer auth both 200 with 404/401 for an unknown credential.

Not in scope here and still pending: deploying it. OWNER_DISCORD_ID, DISCORD_GUILD_ID and the OAuth pair must be set before the next redeploy — the backend refuses to start without them, and API_TOKEN/API_TOKEN_GRACE_UNTIL should be dropped from .env at the same time.

Implemented on `feat/open-registration-guild-members`, PR #36 (b0bf6fe + f4f6c9c). Every criterion is ticked; how each was met, in the issue's order: 1-4. `discordCallback` checks guild membership (and `DISCORD_REQUIRED_ROLE`), then `Store.EnsureReader` — one code path, idempotent on `discord_id`. The refusal returns before `EnsureReader`, so nothing is created for a non-member; asserted by a reader-count check after a refused sign-in. `OWNER_DISCORD_ID` seeds the owner but no longer gates login. 5. Empty library offers both install links, placed behind the tab-specific empty states so "No favourites yet" is not shadowed. Tokens only, `--danger` for destruction, `--ember` untouched; both colour branches shot. 6. `/install/*` and `/u/{cred}/*` render with the acting Reader's derived credential — asserted to differ from the owner's. 7-10. Isolation asserted in both directions for read, modify and delete. Shared series: one `series` row, two independent progresses (second starts at zero), `DueForLatestCheck` returns it once, and one Reader's delete leaves the other's bookmark and the poll intact. 11. Owner-only `POST /readers/{id}/revoke` (404 for anyone else) plus a Readers panel. The owner's own row is deliberately not revocable — that is what logout is for — so the endpoint 404s on the owner id too. 12. `API_TOKEN`, `API_TOKEN_GRACE_UNTIL`, the legacy branch in `httpmw.ResolveReader` and their tests are gone, along with the references in `.env.example`, compose, README, DEPLOY, CUTOVER and REDEPLOY. A credential now authenticates exactly one Reader or nothing. 13. PRODUCT.md rewritten: guild members with their own libraries, accounts created by signing in, one owner capability. Verification: `go test ./...` green (needs Docker). Live smoke against a throwaway Postgres — empty-library state in dark and light, roster render, a real revoke through the panel (target's next request 401s, owner untouched), owner self-revoke refused 404, per-Reader userscript path and bearer auth both 200 with 404/401 for an unknown credential. Not in scope here and still pending: deploying it. `OWNER_DISCORD_ID`, `DISCORD_GUILD_ID` and the OAuth pair must be set before the next redeploy — the backend refuses to start without them, and `API_TOKEN`/`API_TOKEN_GRACE_UNTIL` should be dropped from `.env` at the same time.
sulthan added ready-for-human and removed ready-for-agent labels 2026-08-08 20:16:11 +07:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: sulthan/mangaBookmark#27