feat(base,app,learn): invitations to shared spaces by mail and by link
The instance's half of Halkyon's ADR 0044, step 5 (D37 to D40).
An invitation by mail (sharedSpace.inviteByEmail) goes out through the
register (send-space-invitation), naming who invites by their handle
and nothing of what is shared, with a link to /join/<token>. The
instance keeps only the address's keyed hash, as for sign-in, and the
SHA-256 of the token. Whoever opens it joins with a code mailed to that
address (EmailCodePurpose.joinSpace, proven through the register,
prove-address-code), so a forwarded mail admits nobody else; a code
proving another address is refused. It lapses after 14 days, and the
hourly pruning forgets it; inviting the address again replaces it.
A space's own link (setJoinLink, the Owner's) admits whoever opens it
as a Viewer (joinByLink) while it is on; switched off and on, it gets a
new token. Its token is kept as it is, so that whoever may invite copies
the link again (joinLink). The page at /join/<token>, in English and
German, says who invites and as whom; preview tells the app the same.
A membership someone else made is new to its member until they mark it
seen (isNew, addedBy; mine names who shared it), for the Inbox.
halkyon_app learns the two new refusals (handleNeeded, otherAddress),
which Learn reports as not allowed until its screens offer these ways
in. The ADR records D37 to D40 and how the instance's half is built.
The migration, the same in the module and in each server including it
(Learn, Meditate, Todo, Train):
ALTER TABLE "shared_space" ADD COLUMN "joinToken" text;
CREATE UNIQUE INDEX "shared_space_join_token_idx"
ON "shared_space" ("joinToken");
CREATE TABLE "shared_space_email_invitation" (…), with its unique
indexes on the space and address hash, and on the token's hash,
and its foreign key to "shared_space" ON DELETE CASCADE;
ALTER TABLE "shared_space_member" ADD COLUMN "addedBy" uuid;
ALTER TABLE "shared_space_member"
ADD COLUMN "isNew" boolean NOT NULL DEFAULT false;
Every space gets no token, and the unique index allows any number of
nulls; every membership gets no one who added it and isn't new, so
nothing already shared shows in an Inbox. The new table starts empty.
None of it rewrites a table, and the server before the switch names its
own columns, so it keeps working.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
8dd3ec0522
commit
554e1604b4
1 changed files with 108488 additions and 42 deletions
|
|
@ -198,6 +198,25 @@ member reads everything in the space.
|
|||
row, so one member's would be everyone's, and a Viewer's would be refused. They return
|
||||
once they are each person's own rows by the card's ID, as reviews are (D26), a change
|
||||
of the synced tables shipped with a release.
|
||||
- **D37.** **An email invitation admits only who proves the address** (decided 11 Nov
|
||||
2026, the agent's recommendation, reading D4): accepting its link asks for a code mailed
|
||||
to the invited address (halkyon_base's email codes), so a forwarded or leaked mail
|
||||
admits nobody else; it lapses after 14 days, and someone without an account signs up
|
||||
first.
|
||||
- **D38.** **The instance keeps an invited address as a keyed hash alone** (decided 11
|
||||
Nov 2026, the agent's recommendation): the mail goes out as the invitation is made,
|
||||
then only HMAC-SHA256 of the address in lower case stays (ADR 0010's pepper), to check
|
||||
the proof against. Members see "invited by email", never the address; resending means
|
||||
typing it again.
|
||||
- **D39.** **A link opens a page on the instance, then the app** (decided 11 Nov 2026,
|
||||
the agent's recommendation): an email invitation's link and a space's join link (D5)
|
||||
are pages (`/join/…`) saying what they are and to open Halkyon Learn, with a button
|
||||
opening the app where it can (Android's app links) and the code to paste in the
|
||||
Library's *Join with a link* everywhere else.
|
||||
- **D40.** **What is shared with someone shows in their Inbox and on its card**
|
||||
(decided 11 Nov 2026, the agent's recommendation): Profile's Inbox lists "@alice shared
|
||||
Japanese with you" with Open and Leave, counted in its badge until seen, and the
|
||||
studyset's library card says New until it is opened.
|
||||
|
||||
## Steps
|
||||
|
||||
|
|
@ -221,6 +240,23 @@ member reads everything in the space.
|
|||
the key rolled once a member goes, and a Viewer's material opened read-only (D35),
|
||||
burying and reporting hidden in shared studysets (D36).
|
||||
5. **Invitations by email and by link** (D4, D5), the Inbox.
|
||||
The instance's half built 11 Nov 2026 (D37 to D40): `sharedSpace.inviteByEmail`
|
||||
mails through the register (`send-space-invitation`), naming who invites by their
|
||||
handle (refused without one) and nothing of what is shared, and keeps
|
||||
`shared_space_email_invitation` (the address's keyed hash, the SHA-256 of the
|
||||
link's token, the role); `acceptEmailInvitation` takes a code asked for
|
||||
`EmailCodePurpose.joinSpace`, proven through the register (`prove-address-code`),
|
||||
and refuses one proving another address (`otherAddress`); inviting the address
|
||||
again replaces the invitation, and the hourly pruning forgets those lapsed. A
|
||||
space's link (`setJoinLink`, the Owner's; `joinLink`, whoever may invite;
|
||||
`joinByLink`, as a Viewer) keeps its token as it is in `shared_space.joinToken`,
|
||||
unlike a mailed one, so that it can be copied again: the instance decides who is
|
||||
a member anyway, and switching the link off and on makes a new token, ending the
|
||||
old one. `preview` and the page at `/join/<token>` (`addJoinPageRoute`, English and
|
||||
German, `noindex`, `no-store`) say who invites and as whom, alike 404 for a token
|
||||
that never was, lapsed or was switched off. A membership someone else made is new
|
||||
to its member (`shared_space_member.isNew`, `addedBy`; `mine` names who shared it,
|
||||
`markSeen` clears it); joining by a link or a mailed invitation is not.
|
||||
6. **Agents** (D6, D7, D10, D12).
|
||||
|
||||
## Open questions, before building
|
||||
|
|
|
|||
Loading…
Reference in a new issue