| index 8a093d4..160f45d 100644 |
| --- a/docs/superpowers/specs/2026-09-07-meet-design.md |
| +++ b/docs/superpowers/specs/2026-09-07-meet-design.md |
| @@ -1,9 +1,8 @@ |
| # 🤖 Meet — design |
| |
| Design, 2026-09-07. Direction approved by Paul in conversation; this is the |
| -written form. **Revision 3**, answering the twelve findings of the |
| -second Codex review (`CODEX-FINDINGS-MEET-R2.md`) on top of revision 2's |
| -answers to the first (`CODEX-FINDINGS-MEET.md`); §14 has both tables. Drafted by the Meet |
| +written form. **Revision 4**, answering the ten findings of the third |
| +Codex review (`CODEX-FINDINGS-MEET-R3.md`); §14 has all three rounds. Drafted by the Meet |
| session (Fable 5.1) from a brainstorm with Paul on 2026-09-06 and |
| 2026-09-07. Every ruling in §1 attributed to Paul was given in that |
| conversation or on the discussion it cites. |
| @@ -60,7 +59,10 @@ text, any SDP fragment, any candidate address and any link token. A |
| link token appears once in an HTTP response body and nowhere else. |
| |
| What the claim does not cover, said plainly: signalling passes through |
| -Meet's process in plaintext for the instant it is relayed, and a |
| +Meet's process in plaintext for the instant it is relayed, plus a |
| +replay ring of at most thirty-two targeted messages per participation |
| +held in memory for at most thirty seconds after its stream drops |
| +(§4.5), and a |
| capability URL is a bearer that the person who holds it may paste |
| anywhere. Ruling 16 covers the token at rest, not in flight. |
| |
| @@ -158,19 +160,21 @@ token, ruling 16), `created_by`, `created_at`, `revoked_at`. One meeting |
| may hold several links so a revoked one can be replaced. The token is |
| returned once, in the response that minted it, and never again. |
| |
| -**`pending_guests`** — `id`, `meeting_id`, `address_fingerprint` (keyed |
| +**`pending_joins`** — `id`, `meeting_id`, `link_id` (the |
| +`meeting_links` row the browser followed), `address_fingerprint` (keyed |
| HMAC of the address under the sealer's purpose key, so the table never |
| holds a plaintext outsider address, calendar's rule for attendees), |
| `created_at`, `expires_at` (fifteen minutes). Written at `POST /signin` |
| -when an outsider asks for a magic link through a live meeting link |
| -(§3.2), read by the sign-in authorize callback and by `/continue`, and |
| -deleted when a grant is minted from it or when it expires. A restart |
| -does not lose it, so a magic link clicked after a deploy still works. |
| +when anyone, member or outsider, asks for a magic link while holding a |
| +live meeting link (§3.2); read by the sign-in authorize callback (for |
| +outsiders) and by `/continue` (for everyone); deleted when consumed or |
| +expired. A restart does not lose it, so a magic link clicked after a |
| +deploy still works. |
| |
| **`guest_grants`** — `id`, `meeting_id`, `person_id`, `granted_at`, |
| `expires_at`, `admitted_at` (nullable), `admitted_by` (actor, |
| nullable). Minted at sign-in continuation when a non-member follows a |
| -live link (§3.2), or from a `pending_guests` row at `/continue`. One row |
| +live link (§3.2), or from a `pending_joins` row at `/continue`. One row |
| per person per meeting; following the link twice touches nothing. `admitted_at` and `admitted_by` are an audit of |
| the first admission and confer nothing: whether a guest is admitted |
| *now* is hub state (§4.2). |
| @@ -205,9 +209,8 @@ computed, never stored: |
| `expires_at` in the future. The row is the instance's own authority |
| under doctrine §5.2. |
| |
| -Standing is what `/m/{sqid}/join`, a fresh `GET /m/{sqid}/stream` with |
| -no `peer`, and every `/api/` request check. It is computed on each |
| -request and never cached. |
| +Standing is the entry authority. §2.5 says which endpoints check it, |
| +and it is computed on each request and never cached. |
| |
| ### 2.4 Participation: who may continue |
| |
| @@ -249,6 +252,32 @@ adds no cache of its own. |
| |
| --- |
| |
| +### 2.5 The authorization contract, per endpoint |
| + |
| +One rule per endpoint, so §4 and §5 do not each restate it: |
| + |
| +| Endpoint | Authority | Checked | |
| +|---|---|---| |
| +| `GET /` , `POST /meetings`, confirm pages and their POSTs, `/m/{sqid}/remove/{peer}` | **Current member standing** | On the request. | |
| +| `GET /m/{sqid}/join`, `GET /m/{sqid}` | Standing (`member` or `guest`) **or** a live participation for this session in this room | On the request. | |
| +| `GET /m/{sqid}/stream` with no `peer` | Standing | On the request. Creates an unjoined participation. | |
| +| `GET /m/{sqid}/stream?peer=` | Participation bound to this session | On the request. | |
| +| `POST /m/{sqid}/signal`, `join` | Participation, **and** standing at that moment, because `join` is the entry | On the request, under the hub lock. | |
| +| `POST /m/{sqid}/signal`, `admit` | Participation with **current** `member` kind, re-derived from standing on this request, not only on the tick; **and** the target's entry authority (`member` standing or an unexpired grant) at this moment, under the same lock as the transition | On the request. | |
| +| `POST /m/{sqid}/signal`, every other type; `GET /m/{sqid}/ice` | Participation, admitted | On the request. Standing is not consulted; a guest's expired grant does not matter here (ruling 14). | |
| +| `/api/v1/*` | Agent token unrevoked **and** operator's current member standing | On the request. | |
| + |
| +A request by a participation whose kind is `member` re-derives standing |
| +on every request as well as on the tick; a member who has just been |
| +removed is dropped by the next thing they do, not the next tick. The |
| +tick (§2.4) exists for the quiet case. Promotion of a waiting guest on |
| +the tick (§2.4) goes through the same admission check as `admit`, |
| +including the target's entry authority, so a guest whose grant has |
| +expired is never admitted by either path; only an already-admitted |
| +guest survives expiry. |
| + |
| +--- |
| + |
| ## 3. Joining |
| |
| ### 3.1 Making a meeting and its link |
| @@ -292,30 +321,36 @@ gives one 404 for all four. Live gives one of three outcomes: |
| /signin` on the allowlist before rastrillo's `Begin`. Meet keeps that |
| gate with one exception: an address not on the allowlist is let through |
| when, and only when, the request carries a valid pending-join cookie for |
| -a live link. In that case Meet first writes a `pending_guests` row |
| -(meeting, keyed fingerprint of the address, fifteen minutes) and only |
| -then calls `Begin`. Nobody can cause mail to an arbitrary address |
| -without holding a live meeting link, and holding the link is the team's |
| -own choice. |
| +a live link. Whenever the cookie is present and valid, for a member |
| +address or an outsider's, Meet first writes a `pending_joins` row |
| +(the **link** the cookie names, its meeting, the keyed fingerprint of |
| +the address, fifteen minutes) and only then calls `Begin`. Nobody can |
| +cause mail to an arbitrary address without holding a live meeting |
| +link, and holding the link is the team's own choice. |
| |
| **Admitting the emailed link.** rastrillo's `Authorize` callback |
| receives the verified address and nothing else, and Docs wires it to |
| the allowlist, which would 403 the outsider's click. Meet's `Authorize` |
| is: the address is on the allowlist, **or** its keyed fingerprint has an |
| -unexpired `pending_guests` row. This works in whichever browser the |
| -emailed link is opened, because the row was written before the mail |
| -went out and lives in the database, not in a cookie. |
| +unexpired `pending_joins` row whose link is still live. This works in |
| +whichever browser the emailed link is opened, because the row was |
| +written before the mail went out and lives in the database, not in a |
| +cookie. |
| |
| **Continuation.** rastrillo has one configured landing path for a fresh |
| session and no per-attempt return value, so Meet's `SignedInPath` is |
| `/continue`. `GET /continue` resolves the signed-in person, then: |
| |
| -1. If a `pending_guests` row matches their fingerprint and is unexpired: |
| - look the meeting up as though they had just followed a live link (so |
| - a link revoked during sign-in still 404s), mint the grant if they are |
| - a non-member, delete the row, redirect to the holding screen. The |
| - pending-join cookie, if present, is cleared; it is not consulted, |
| - because the row is the record. |
| +1. If one or more `pending_joins` rows match their fingerprint and are |
| + unexpired, take the **most recent**. In one transaction: re-check |
| + that its link is not revoked, its meeting not ended, and the row not |
| + expired, exactly as `GET /j/{token}` would (so a link replaced during |
| + sign-in still 404s even though the meeting has a new live link); |
| + mint the grant if they are a non-member and hold none; delete every |
| + pending row for this fingerprint; redirect to the holding screen. If |
| + the re-check fails, delete the rows and fall through to 2 or 3. The |
| + pending-join cookie, if present, is cleared and never consulted; the |
| + row is the record. |
| 2. Otherwise, if they are a member: redirect to `/`, the meeting list. |
| A member who opened `/signin` directly lands where they expect. |
| 3. Otherwise: redirect to `/welcome`, the not-a-member page: "You're |
| @@ -323,36 +358,46 @@ session and no per-attempt return value, so Meet's `SignedInPath` is |
| If someone sent you a meeting link, open it again now." with sign-out |
| and nothing else. Scriptless. |
| |
| -The other-browser case (magic link opened on a phone) is therefore not |
| -a fallback: step 1 finds the row regardless of cookies. Re-following the |
| -link also works, because a signed-in non-member takes the second bullet |
| -at the top of this section. |
| - |
| -**Mail budget.** The inherited limiter allows twenty attempts per client |
| -address and five per email address per fifteen minutes, keyed on |
| -`RemoteAddr`, which behind the platform edge is the edge, so that |
| -budget is shared by every visitor. Meet cannot separate guest and |
| -member traffic inside that limiter, so it separates it in front: Meet's |
| -gate counts outsider attempts, refused or not, **before** `Begin` is |
| -reached, at five per link per fifteen minutes and ten across all links |
| -per fifteen minutes. Outsider traffic can therefore consume at most ten |
| -of the shared twenty, leaving ten attempts per window for members. A |
| -team whose members need more than ten sign-ins in fifteen minutes while |
| -a link is under attack will see rate errors; that is the stated |
| -residue, and a trusted client address from the edge is the platform |
| -change that would remove it, filed as a question on the platform when |
| -slice 1 opens, not solved here. |
| +A member who followed a link while signed out therefore reaches the |
| +holding screen of that meeting, in whichever browser the emailed link |
| +is opened, by the same row an outsider uses. The rows differ only in |
| +what they authorize: for an outsider the row also admits the address at |
| +`Authorize`; for a member the allowlist does that and the row only |
| +carries the destination. |
| + |
| +**Mail budget, and what it does not buy.** The inherited limiter allows |
| +twenty attempts per client address and five per email address per |
| +fifteen minutes, keyed on `RemoteAddr`, which behind the platform edge |
| +is the edge, so that budget is shared by every visitor. Meet bounds |
| +what it can bound: attempts for a **non-allowlisted** address are |
| +counted before `Begin` is reached, at five per link per fifteen minutes |
| +and ten across all links per fifteen minutes. That stops a leaked link |
| +from being used to mail strangers at volume. |
| + |
| +It does not protect members, and revision 3's claim that it did was |
| +wrong: anyone can submit a **member's** address without any link, |
| +reach `Begin`, and consume the shared client-address allowance twenty |
| +times in fifteen minutes, or a single member's five-per-address |
| +allowance, after which that member cannot sign in for the rest of the |
| +window. The gate classifies an address, not the person typing it, and |
| +nothing in front of `Begin` can tell them apart. This is the residual |
| +denial of service for sign-in on every app that shares this edge and |
| +this limiter, not a Meet-specific one, and Meet records it rather than |
| +claiming a reserve. The fix is a trusted client address from the edge |
| +or a per-app limiter keyed on something better, and a question to that |
| +effect is filed on the platform when slice 1 opens. |
| |
| **Responses.** Meet's own refusals (allowlist without a link, link |
| budget) redirect to `/signin?sent=1`, the same destination as a |
| successful `Begin`. rastrillo's own outcomes are left as they are: |
| -`?err=address` for a malformed address and `?err=rate` for its limiter, |
| -both of which describe the requester's own input or behaviour. The |
| -residue, stated plainly: while the shared budget is exhausted, a member |
| -sees `?err=rate` where a non-member without a link would have seen |
| -`?sent=1`, which distinguishes them. That window exists only under the |
| -attack the budget above bounds, and it is recorded here rather than |
| -claimed away. |
| +`?err=address` for a malformed address and `?err=rate` for its limiter. |
| +The residue, stated plainly: while a budget is exhausted, a member |
| +address gets `?err=rate` where an unknown address without a link gets |
| +`?sent=1`, which distinguishes the two; and because a single member |
| +address can be rate-limited on purpose, that distinction is reachable |
| +by an attacker at will, not only under global exhaustion. Membership of |
| +a specific address is therefore not a secret Meet can keep at the |
| +sign-in form, and the README says so. |
| |
| ### 3.3 The holding screen |
| |
| @@ -406,8 +451,8 @@ message. |
| |
| ### 4.1 Transport |
| |
| -One SSE stream per peer, `GET /m/{sqid}/stream`, under the session and |
| -the standing check, with the Docs live-sync heartbeat (15 s) and |
| +One SSE stream per peer, `GET /m/{sqid}/stream`, authorized per §2.5, |
| +with the Docs live-sync heartbeat (15 s) and |
| connection TTL (5 min) so hibernation cannot be held off by a dead |
| stream. The first event on a new stream is `hello {peer}`, the server's |
| peer id for this stream. Upstream messages are `POST /m/{sqid}/signal` |
| @@ -433,14 +478,17 @@ not yet joined. A room holds at most **sixteen participations** of any |
| state (unjoined, waiting, admitted, or held for resume); a stream that |
| would be the seventeenth is refused with 503 before anything is |
| allocated. An unjoined participation that has not posted `join` within |
| -thirty seconds is dropped with its stream. It is a 16-byte random value, shown in |
| -rosters so browsers can address each other, and it is not a credential: |
| +thirty seconds is dropped with its stream. |
| + |
| +The peer id is a 16-byte random value, shown in rosters so browsers can |
| +address each other, and it is not a credential: |
| every `POST /signal` must carry a `peer` that the hub finds **in this |
| room, bound to this session**; anything else is refused with 404 and |
| -nothing is relayed. A session may hold one peer per room; a second |
| -stream from the same session for the same room resumes the existing |
| -peer if it is within `resume_until` (§4.5) and otherwise replaces it, |
| -closing the old stream with `lost {reason: "replaced"}`. |
| +nothing is relayed. A session may hold one peer per room; a stream |
| +opened with `?peer=` resumes that participation (§4.5); a stream opened |
| +without `peer` while the session already has a participation in the |
| +room replaces it, closing the old stream with `lost {reason: |
| +"replaced"}`, which is terminal for the old tab (§4.5). |
| |
| Every downstream envelope is built by the server: `type`, `from` (the |
| sender's peer id, from the binding, never from the body), the sender's |
| @@ -456,8 +504,8 @@ a multi-host fleet; that is noted, not solved. |
| |
| | Type | Who may send | Recipients | Server does | |
| |---|---|---|---| |
| -| `join` | any peer with standing | — | Member: admit (subject to cap), `roster` to the joiner, `peer` to admitted peers. Guest: mark waiting, `knock` to admitted **members**. Idempotent for a peer already present. | |
| -| `admit {peer}` | an admitted member | — | Target must be waiting in this room. Under the hub lock: if admitted count is four, `full` to the admitting member and nothing changes; else mark admitted, `roster` to the guest, `peer` to the other admitted peers, `knock-gone` to members. | |
| +| `join` | a participation whose person has standing now (§2.5) | — | Member: admit (subject to cap), `roster` to the joiner, `peer` to admitted peers. Guest: mark waiting, `knock` to admitted **members**. Idempotent for a peer already present. | |
| +| `admit {peer}` | an admitted participation with current `member` kind (§2.5) | — | Target must be waiting in this room and hold entry authority now (§2.5). Under the hub lock: if the target's grant has expired, `lost {reason: "expired"}` to it and `knock-gone` to members; if admitted count is four, `full` to the admitting member and nothing changes; else mark admitted, `roster` to the guest, `peer` to the other admitted peers, `knock-gone` to members. | |
| | `leave` | any peer | — | Remove the peer. Admitted: `gone` to admitted peers. Waiting: `knock-gone` to members. | |
| | `offer`, `answer`, `candidate` | an admitted peer | the one admitted peer named in `to` | Relay the payload unread. | |
| | `chat {text}` | an admitted peer | all admitted peers | Fan out with the sender's name. 2 KB. Not stored. | |
| @@ -475,10 +523,8 @@ member who joins a room where a guest is already waiting therefore sees |
| the knock in the roster, not in a `knock` that had no recipient; `knock` |
| is the incremental form for members already present. |
| |
| -**`lost`** carries a reason, and the reason decides the browser's |
| -response: `buffer` and `replaced` are transport outcomes and the |
| -browser resumes; `standing`, `expired`, `removed` and `ended` are |
| -terminal and the browser closes every peer connection and shows why. |
| +**`lost`** carries a reason, and §4.5 says what the browser does with |
| +each. |
| |
| **The cap** is enforced at every transition into the admitted state, |
| under the hub lock: a member's `join`, an `admit`, and never on a resume |
| @@ -488,8 +534,8 @@ Queued guests can exceed four while waiting; admission checks the count |
| at admission time, so four queued guests and one member yield three |
| admissions and one `full`. |
| |
| -The server validates type and shape and the sender's standing and |
| -admission, and reads nothing inside `offer`, `answer`, `candidate` or |
| +The server validates type and shape and the sender's authority per |
| +§2.5, and reads nothing inside `offer`, `answer`, `candidate` or |
| `chat` beyond the byte length. |
| |
| ### 4.4 Limits and logging |
| @@ -499,8 +545,9 @@ admission, and reads nothing inside `offer`, `answer`, `candidate` or |
| - `chat` text above 2 KB is refused. |
| - A peer may send thirty signal messages per second with a burst of |
| sixty; beyond that the hub replies 429 with `Retry-After`, and the |
| - browser's queue (§4.6) retries rather than drops. The client sends |
| - one message at a time per target, so 429 is reached only by abuse. |
| + browser's queue (§4.6) honours it and retries; a legitimate |
| + negotiation burst across three targets can touch the limit briefly |
| + and loses nothing. |
| - A room holds at most sixteen participations in any state (§4.2) and at |
| most eight waiting; a ninth waiting `join` is refused with `full`. |
| - A session holds one peer per room (§4.2); a person holds at most four |
| @@ -516,20 +563,46 @@ admission, and reads nothing inside `offer`, `answer`, `candidate` or |
| ### 4.5 Renewal, resume and restart |
| |
| The five-minute TTL and any dropped stream are handled by **resume**, |
| -not by rejoining. When a stream closes, the peer stays in the hub for a |
| -thirty-second `resume_until` with its admission intact. The browser |
| -reopens `GET /m/{sqid}/stream?peer=<id>`; the hub finds the peer bound |
| -to this session, attaches the new stream, and sends it a fresh `roster` |
| -(§4.3) as the first event after `hello`. The browser **reconciles** |
| -against that roster: a peer connection to an id not in the roster is |
| -closed, an id in the roster with no connection gets one. That is how a |
| -browser that missed a `gone` while its buffer overflowed still closes |
| -the removed peer's connection: the roster it receives on resume is the |
| -authority, not the events it missed. No event goes to anyone else; the |
| -other browsers' peer connections were never affected because media is |
| -P2P. A resume does not consume a cap slot. A peer that misses the |
| -window is removed as a leave. `lost {reason: "buffer"}` and `{reason: |
| -"replaced"}` are followed by the same resume. |
| +not by rejoining. When a stream closes, the participation stays in the |
| +hub for a thirty-second `resume_until` with its admission intact. The |
| +browser reopens `GET /m/{sqid}/stream?peer=<id>&last=<json>`, where |
| +`last` maps each sender's peer id to the highest `seq` this browser has |
| +received from it. The hub finds the participation bound to this session |
| +and attaches the new stream. What it sends next depends on admission: |
| + |
| +- **Admitted**: `hello`, then a fresh `roster` (§4.3), then a **replay** |
| + of every targeted message (`offer`, `answer`, `candidate`) held for |
| + this participation in its replay ring with `seq` above `last`. The |
| + ring holds the last thirty-two targeted messages sent to each |
| + participation, for the resume window only, in memory, and is dropped |
| + with the participation; that is the one place Meet's process holds |
| + signalling for longer than the instant of relay, bounded by thirty |
| + seconds and thirty-two messages, and it is stated in §0.1. If a |
| + sender's messages above `last` are no longer all in the ring, the hub |
| + sends `gap {from}` instead of a partial replay, and the browser |
| + performs a **pair reset** with that sender (§4.6). |
| +- **Waiting or unjoined**: `hello`, then `state {waiting: true}`, and |
| + nothing else. A waiting guest learns nothing about the room by |
| + resuming, which is ruling 9's isolation carried through the resume |
| + path. |
| + |
| +The browser **reconciles** against the roster it received: a peer |
| +connection to an id not in the roster is closed, an id in the roster |
| +with no connection gets one. That is how a browser that missed a `gone` |
| +while its buffer overflowed still closes the removed peer's connection: |
| +the roster on resume is the authority, not the events it missed. No |
| +event goes to anyone else; the other browsers' peer connections were |
| +never affected because media is P2P. A resume does not consume a cap |
| +slot. A participation that misses the window is removed as a leave. |
| + |
| +**`lost` reasons.** `buffer` is followed by a resume. `replaced` is |
| +**terminal for the displaced tab**: it closes every peer connection and |
| +shows "This meeting is open in another tab", with one button, "Use this |
| +tab instead", which opens a fresh stream without `peer` and so replaces |
| +the other tab in turn. A displaced tab never resumes on its own, so two |
| +tabs cannot fight; only a person's click moves the participation. |
| +`standing`, `expired`, `removed` and `ended` are terminal and the |
| +browser closes every peer connection and shows why. |
| |
| A **process restart** empties the hub. Every browser's stream errors |
| and its resume is refused with 404 (unknown peer). On that refusal the |
| @@ -541,19 +614,27 @@ connections on the refusal, no guest keeps media flowing from before |
| the restart. That is the honest cost of keeping admission in memory, |
| stated rather than hidden behind persistence. |
| |
| -### 4.6 Delivery: ordered, acknowledged, per pair |
| +### 4.6 Delivery: ordered, acknowledged, replayed |
| |
| -SSE plus POST is not a session; the browser makes it one. For each |
| -target peer the browser keeps an **outbound queue** and sends one |
| -message at a time, in order, waiting for the POST's 2xx before the |
| +SSE plus POST is not a session; the browser and the hub make it one. |
| +For each target peer the browser keeps an **outbound queue** and sends |
| +one message at a time, in order, waiting for the POST's 2xx before the |
| next. Each `offer`, `answer` and `candidate` carries a `seq`, per |
| (sender, target), starting at one and incrementing; the server relays |
| -`seq` unchanged. A POST that fails (network, 5xx, 429) is retried with |
| -backoff, same `seq`, up to thirty seconds, then the browser treats the |
| -target as unreachable, closes that connection, and re-creates it on the |
| -next `roster`. The receiver keeps the last `seq` seen per sender and |
| -drops anything at or below it, so a retried POST that had in fact |
| -landed is harmless. |
| +`seq` unchanged and keeps a copy in the target's replay ring (§4.5). A |
| +POST that fails (network, 5xx, 429) is retried with backoff, same |
| +`seq`, up to thirty seconds, then the browser treats the target as |
| +unreachable, closes that connection, and re-creates it on the next |
| +`roster`. The receiver keeps the last `seq` seen per sender and drops |
| +anything at or below it, so a retried POST that had in fact landed is |
| +harmless, and a replay on resume delivers only what was missed. |
| + |
| +A **pair reset** is the recovery when replay cannot fill a gap: the |
| +browser that received `gap {from}` closes its connection to that peer |
| +and posts `reset` to it; the peer closes its side too; both then create |
| +a fresh connection and negotiate as for a new `peer`. A reset costs the |
| +pair a second or two of media and happens only after a resume whose |
| +gap exceeded the ring, which is rare by construction. |
| |
| Candidates are applied only after a remote description is set; earlier |
| ones are held in a per-peer buffer, which the W3C pattern already |
| @@ -589,7 +670,7 @@ or parse is a startup refusal outside `-env development`, logged as one |
| line naming the variable and not its value. The decoded value is held |
| in memory and never logged. |
| |
| -Per call, `GET /m/{sqid}/ice` (under the session and standing) returns |
| +Per call, `GET /m/{sqid}/ice` (an admitted participation, §2.5) returns |
| the `RTCIceServer` list. For each element, every URL is passed through |
| as given; URLs whose scheme is `turn` or `turns` carry `username = |
| "<unix expiry>:" + username_suffix` and `credential = |
| @@ -609,15 +690,22 @@ Credential expiry is a hard deadline on the relay, checked on allocate |
| and on every refresh: the allocation ends at expiry whether or not the |
| browser sends anything. The client therefore renews at two and a half |
| hours, and **renewal is per connection and complete only when the new |
| -path works**: |
| +path is proven**: |
| |
| 1. Fetch `/ice`. On failure, retry every minute; the tile shows a |
| warning from the first failure. |
| 2. For each peer connection: `setConfiguration` with the new servers, |
| - `restartIce()`, and let perfect negotiation carry the offer. The |
| - connection is **renewed** when `iceConnectionState` reaches |
| - `connected` or `completed` after the restart, within thirty seconds. |
| - Otherwise restart again, up to three times. |
| + `restartIce()`, and let perfect negotiation carry the offer. An ICE |
| + restart changes the local ICE username fragment, so the restart has |
| + a **generation** the browser can name: the new `usernameFragment`. |
| + The connection is **renewed** when, within thirty seconds, (a) the |
| + restart's offer/answer exchange has completed and `signalingState` |
| + is `stable` with that answer applied, and (b) `getStats()` reports a |
| + selected candidate pair in state `succeeded` whose local candidate |
| + carries the new generation's `usernameFragment`. An |
| + `iceConnectionState` change on its own proves nothing, because a |
| + `completed` transport moves to `connected` merely by starting a |
| + restart. Otherwise restart again, up to three times. |
| 3. The old credential's deadline is kept as the **must-succeed-by** |
| time for every connection not yet renewed. A connection that is not |
| renewed by then shows "relay expired" with a retry, which runs step 2 |
| @@ -695,14 +783,16 @@ and one script file so a second app can adopt it. |
| |
| ### 7.1 Lone shape |
| |
| -Sign-in is Docs' `internal/docs/auth.go` adopted with the two changes in |
| -§3.2: the pending-join exception to the allowlist gate, and |
| -`SignedInPath` set to `/continue`. `MEET_ALLOWED_ADDRESSES`, |
| -`MEET_ORIGIN`, `MEET_INSTANCE_KEY`, `MEET_SMTP_*` and `MEET_DEK` are |
| -startup refusals outside `-env development`. The same 303 for every |
| -address. This file already lives in Docs, Calendar and Slides; Meet's is |
| -the fourth copy, and the extraction task Slides recorded stands. Meet |
| -adds none. |
| +Sign-in is Docs' `internal/docs/auth.go` adopted with the changes §3.2 |
| +specifies in full: the pending-join exception to the gate in front of |
| +`Begin`, the `pending_joins` row written before `Begin`, an `Authorize` |
| +that admits allowlist-or-pending, `SignedInPath` set to `/continue`, |
| +and the outsider budgets. §3.2 also states which responses remain |
| +distinguishable. `MEET_ALLOWED_ADDRESSES`, `MEET_ORIGIN`, |
| +`MEET_INSTANCE_KEY`, `MEET_SMTP_*` and `MEET_DEK` are startup refusals |
| +outside `-env development`. This file already lives in Docs, Calendar |
| +and Slides; Meet's is the fourth copy, and the extraction task Slides |
| +recorded stands. Meet adds none. |
| |
| ### 7.2 Actors |
| |
| @@ -826,15 +916,17 @@ own gates from §13; nothing is deferred to the deploy slice. |
| 1. **Meetings, links, sign-in.** `persons`, `meetings`, `meeting_links`, |
| `guest_grants`; sealing; the list; create; the link shown once; the |
| two confirm pages and their POSTs; the `/j/` route with all three |
| - outcomes and the one 404; the pending-join cookie, `pending_guests`, |
| - the `Authorize` callback, `/continue` with its three outcomes, the |
| - allowlist exception and the two outsider budgets; `/welcome`; |
| + outcomes and the one 404; the pending-join cookie, `pending_joins`, |
| + the `Authorize` callback, `/continue` with its three outcomes and |
| + the atomic re-check, the allowlist exception and the two outsider |
| + budgets; `/welcome`; the §2.5 contract as middleware; |
| standing (§2.3) as a package with tests; `make release` stamping. |
| Scriptless throughout. Joining lands on a holding page that says the |
| room is the next slice. |
| 2. **Holding screen and room, direct connectivity.** `devices.js`, the |
| hub with peer binding, participation, cap, limits, resume with |
| - roster reconciliation and the tick; the stream and `hello`; every |
| + admission-branched output, the replay ring, `gap` and pair reset, |
| + terminal `replaced`, and the tick; the stream and `hello`; every |
| message in §4.3 except `chat` and `share`; the removal confirm page; |
| `room.js` with perfect negotiation and the §4.6 queue; the |
| participation transitions; the `AGENTS.md` exception. `CARLOS_RELAY` absent. The browser gate: two |
| @@ -863,9 +955,11 @@ one-404 rule (1); the full emailed-link round trip for an outsider, |
| with the pending-join cookie present and absent at the click, the |
| `Authorize` callback admitting the pending address and refusing an |
| unknown one, revocation during sign-in, `/continue`'s three outcomes |
| -for member and non-member with present, absent and expired rows, and |
| -the two outsider budgets including outsider exhaustion followed by a |
| -member sign-in through the same client address (1); standing as a |
| +for member and non-member with present, absent and expired rows, a |
| +member's signed-out link journey completing in another browser, link A |
| +replaced by B between mail and click (404, no grant), and the outsider |
| +budgets plus the documented exhaustion of the shared allowance by |
| +repeated submission of one member address (1); standing as a |
| table test (1); hub tests for peer binding and refusal of a foreign or |
| cross-room `peer`, the sixteen-participation and unjoined-timeout |
| limits with streams opened and never joined, targeted relay only to |
| @@ -874,14 +968,19 @@ admitted peers in the room, a waiting guest receiving nothing but |
| members and not to guests, guest-first arrival then a member joining, |
| promotion of a waiting guest to member under and at the cap, the cap |
| at `join` and at `admit` including four queued guests and concurrent |
| -admissions, resume within and after the window with the roster sent, |
| +admissions, resume within and after the window with the roster sent to an admitted |
| +peer and only `state` to a waiting one, replay from the ring above |
| +`last`, `gap` when the ring is exceeded, `replaced` terminal for the |
| +displaced tab, `admit` of a guest whose grant expired since the last |
| +tick refused, |
| the 64-event buffer, the rate limit, an admitted guest continuing past |
| grant expiry on every endpoint and being refused fresh entry, a member |
| dropped on the tick (2); the §0.1 database and WAL tests (1) and the |
| captured-log test for join, admit, offer, candidate, a refusal and an |
| error (2), extended to accepted chat (4); the minting vector and the |
| renewal state machine including a successful fetch followed by a failed |
| -restart (3); the API reach check after an operator's removal (5); |
| +restart, and a `completed` transport that moves to `connected` on |
| +restart start without qualifying as renewed (3); the API reach check after an operator's removal (5); |
| `make ci` green on every slice. |
| |
| **Browser.** Calendar's chromedp drive is recorded as not running in |
| @@ -899,7 +998,10 @@ the TTL shortened to seconds leaves the call connected (2); a member |
| removed from the allowlist mid-call is dropped on the next tick and the |
| other context sees `gone` (2); a context whose stream is closed while a |
| peer leaves resumes, receives the roster, and has closed the connection |
| -to the departed peer (2); after a server restart both contexts have |
| +to the departed peer (2); an offer whose SSE delivery is cut after the |
| +POST's 2xx is replayed on resume and the pair connects (2); a second tab |
| +on the same session displaces the first, which shows the take-over |
| +button and does not reconnect on its own (2); after a server restart both contexts have |
| closed their old connections before rejoining (2); renewal with the schedule shortened to |
| seconds produces a second `/ice` fetch and an ICE restart that leaves |
| the track flowing, and a failed fetch shows the warning (3); the |
| @@ -948,7 +1050,7 @@ half-closed, twelve new, request changes. Dispositions in revision 3: |
| |
| | Finding | Disposition | |
| |---|---| |
| -| R2-1 authorize callback | `pending_guests` written before `Begin`; `Authorize` admits an allowlisted address or a pending fingerprint; works in any browser (§2.1, §3.2). Closes the R1-7 residue. | |
| +| R2-1 authorize callback | `pending_guests` (now `pending_joins`) written before `Begin`; `Authorize` admits an allowlisted address or a pending fingerprint; works in any browser (§2.1, §3.2). Closes the R1-7 residue. | |
| | R2-2 shared budget | Outsider attempts counted before `Begin`, five per link and ten overall per fifteen minutes, leaving ten of the shared twenty for members; residue and rastrillo's own `?err=` responses stated, not claimed away (§3.2). Closes the R1-8 residue. | |
| | R2-3 expiry versus authorization | Ruling 14: standing for entry, participation for continuing; `/signal`, resume and `/ice` authorized by participation; an admitted guest continues past expiry on every endpoint and cannot re-enter (§2.3, §2.4). Closes the R1-5 residue. | |
| | R2-4 missed `gone`, restart | Resume sends an authoritative `roster` and the browser reconciles; a restart's 404 makes the browser close every connection before rejoining; `lost` reasons split into resumable and terminal (§4.3, §4.5). Closes the R1-9 residue. | |
| @@ -961,6 +1063,25 @@ half-closed, twelve new, request changes. Dispositions in revision 3: |
| | R2-11 removal confirm | `remove` is no longer a signalling message; confirm page and guarded POST with authorization at execution (§3.5). | |
| | R2-12 the floor | Host-only gathering, ICE finds a direct path, peer-reflexive named; bilateral reachability dropped (§5.5, §0.3). Closes the R1-14 residue. | |
| |
| -Revision 3 awaits Paul's review of the written form and a third Codex |
| +**Round 3**, GPT-6 via Codex against revision 3 at 1bd7161: |
| +`CODEX-FINDINGS-MEET-R3.md`: six round-2 findings closed, five |
| +half-closed, one worse; ten new (three P1); request changes. |
| +Dispositions in revision 4: |
| + |
| +| Finding | Disposition | |
| +|---|---| |
| +| R3-1 waiting guest resumes into the roster | Resume output branches on admission: admitted gets `roster` and replay; waiting or unjoined gets `state {waiting}` only (§4.5). Closes the R2-4 regression. | |
| +| R3-2 revoked link indistinguishable | `pending_joins` binds the originating link; `/continue` re-checks that link, its meeting and the row atomically with grant creation (§2.1, §3.2). | |
| +| R3-3 admit after expiry, before the tick | `admit` and tick promotion check the target's entry authority under the same lock as the transition (§2.5). | |
| +| R3-4 endpoint contracts still on standing | §2.5, one authorization row per endpoint; `join` checks standing, everything after admission checks participation only; member kind re-derived on every request (§2.5, §4.1, §4.3, §5.2 point at it). | |
| +| R3-5 no member reserve | The reserve claim is withdrawn; the residual denial of service through a member's address is stated as the edge-wide limitation it is, with the platform question filed at slice 1 (§3.2). | |
| +| R3-6 signed-out member loses continuation | `pending_joins` is written for members too and carries the destination; `Authorize` needs it only for outsiders; most recent row wins (§3.2). | |
| +| R3-7 downstream loss after 2xx | Resume carries `last` per sender; the hub replays from a thirty-two-message ring per participation; `gap` triggers a pair reset (§4.5, §4.6). | |
| +| R3-8 `connected` proves nothing | Completion requires the restart generation's answer applied and a succeeded selected pair whose local candidate carries the new `usernameFragment` (§5.3). | |
| +| R3-9 displaced tab fights back | `replaced` is terminal for the displaced tab; only a person's click takes the participation back (§4.5). | |
| +| R3-10 stale claims | §7.1 now defers to §3.2; the 429 claim is removed (§4.4). | |
| + |
| +Revision 4 awaits Paul's review of the written form and a fourth Codex |
| round. Neither has happened; this line is the record. |
| |
| + |