| index 58c868a..7e1ab5b 100644 |
| --- a/docs/superpowers/specs/2026-08-30-sheets-design.md |
| +++ b/docs/superpowers/specs/2026-08-30-sheets-design.md |
| @@ -389,6 +389,27 @@ enroll passkeys and never mount `Gate`. |
| - **`§9.1` call 1 is therefore unambiguous:** the home mints the session, |
| the instance trusts a signed assertion. The first draft left both plausible. |
| |
| +**Assertions carry identity, never authorization** (docs-e6, 2026-08-30 — |
| +stated positively because a reviewer will otherwise propose the opposite as |
| +an improvement). |
| + |
| +A guest's assertion is the same shape as an owner's. The assertion says *who |
| +you are*; the instance decides *what you may do* from its own `grants` |
| +table. Narrowing a comment-only guest's assertion is tempting and belongs on |
| +the wrong side of the boundary: encoding capability requires the home to |
| +know capability, which puts an authorization model inside the identity |
| +origin and cuts against §9's design, where the home knows you have access to |
| +a team in an app and nothing else. |
| + |
| +The security argument does not survive either — a leaked assertion buys |
| +exactly the grants of the person it names, which is what a stolen session |
| +buys anyway. Narrowing reduces no blast radius while adding a second |
| +assertion type, its key handling, and a second audience-binding surface. |
| + |
| +**Assertion expiry is seconds, not minutes.** It is exchanged for a session |
| +immediately; the long-lived thing is the session, which is `sessions`' |
| +problem and already solved. |
| + |
| **Forward, and recorded so this is a staging post rather than a ceiling |
| (Paul, 2026-08-30):** CARLOS is expected to grow support for per-instance |
| auth. One RP at the home is the right answer for Sheets now, and it is |
| @@ -825,7 +846,16 @@ formatting (a second expression evaluator plus a rules UI). Naming charts as |
| the sole slip candidate understated the risk. |
| |
| So the slip order is written down rather than discovered: **charts first, |
| -conditional formatting second.** Version history is not a slip candidate |
| +conditional formatting second.** |
| + |
| +**The criterion is "its absence leaves a coherent product", not "it has no |
| +dependents"** (docs-e6, 2026-08-30). The two usually agree, and where they |
| +disagree the first is the one that matters. Charts passes on the stronger |
| +reading: a spreadsheet without charts is still a spreadsheet. Conditional |
| +formatting is second because a spreadsheet without it is also still a |
| +spreadsheet, merely a duller one — whereas a spreadsheet without version |
| +history is one nobody trusts with real data, which is why history is not on |
| +this list at all. Version history is not a slip candidate |
| because §4.3's log design is load-bearing for live sync — but **its minimal |
| form is named now**: view history and restore a *cell* or a *range*. |
| Restore-a-whole-sheet is the extension, not the baseline. |
| @@ -872,3 +902,82 @@ it is a fallback rather than a scramble. |
| - **Language neutrality upstream.** A discussion proposing CARLOS picks the |
| language per app rather than assuming Go. Nothing here depends on the |
| outcome; Go stays right for this app either way. |
| + |
| +--- |
| + |
| +## Appendix A — assertion verification, filing draft (awaiting Paul's nod) |
| + |
| +**Destination: `github.com/rastrilloorg/rastrillo`.** Rastrillo has no |
| +amadan remote, so this is a GitHub issue rather than an `amadan discuss`, |
| +and it needs Paul's go-ahead. Drafted jointly with docs-e6; both consumer |
| +paragraphs are below. |
| + |
| +**Title: An instance needs to verify a signed identity assertion** |
| + |
| +Instance-per-account makes single sign-in a day-one problem — `decisions.md` |
| +says it directly: *"three instances must not mean three credentials."* Two |
| +apps have now independently arrived at the same answer, which is why this is |
| +a framework ask rather than an app one. |
| + |
| +The shape both reached: **one identity origin holds the credentials and runs |
| +the ceremony; instances receive identity as a signed assertion and verify a |
| +signature.** Instances never run WebAuthn, never hold a passkey RP, never |
| +see a password. |
| + |
| +This is Tito's recorded pattern: *identity lives beside the router, outside |
| +the per-account blast radius, and reaches instances only as **signed** |
| +assertions — headers are stripped-and-re-minted, and signing (not stripping |
| +alone) is what stops a peer socket from forging them.* |
| + |
| +**What rastrillo has:** `sessions`, `auth`, `password`, `passkey` — every |
| +one authenticating *at* the app. **What is missing:** anything letting an |
| +app say "I trust assertions minted by this identity origin; verify this one |
| +and mint a session from it." |
| + |
| +**Why the framework rather than two apps.** The parenthetical in Tito's rule |
| +is the point — *signing, not stripping alone* — and the failure modes do not |
| +announce themselves: |
| + |
| +- **Audience binding.** An assertion must name the instance it is for, or |
| + one minted for team A replays against team B. Both apps are multi-tenant |
| + by construction, so every deployment has thousands of sibling instances to |
| + replay against. This is the one most likely to differ subtly between two |
| + independent implementations. |
| +- **Replay.** Single-use identifier plus short expiry — seconds, not |
| + minutes; the long-lived thing is the session minted afterwards. |
| +- **Key distribution and rotation**, without a flag day. |
| +- **Algorithm confusion.** |
| + |
| +That is squarely "supplies what is hard to get right twice", which is how |
| +rastrillo describes its own scope — and it is literally twice: two apps, |
| +same week, same pattern, neither having seen the other's code. |
| + |
| +**Rough shape**, not a design: verify against pinned or fetched public keys, |
| +check audience against this instance's own identity, enforce single-use and |
| +expiry, return a Subject plus claims, hand off to `sessions.SignIn` — which |
| +is the whole contract the existing identity plugins have with the core. A |
| +fourth plugin, not a new seam. |
| + |
| +**One thing to state positively, because a reviewer will otherwise propose |
| +it as an improvement: assertions carry identity, never authorization.** A |
| +guest's assertion is the same shape as an owner's; the instance decides |
| +capability from its own grants. Encoding capability would require the |
| +identity origin to hold an authorization model, and a leaked assertion buys |
| +exactly what a stolen session buys anyway — so narrowing reduces no blast |
| +radius while adding a second assertion type and a second audience-binding |
| +surface. |
| + |
| +**Named consumers:** |
| + |
| +- **Sheets (`oficina/sheets`)** — the home's credential origin is the sole |
| + RP, confirmed by Paul 2026-08-30; instances verify assertions and never |
| + enroll passkeys. |
| +- **Docs (`oficina/docs`)** — same shape, same week, independently reached. |
| + Docs adds one operational requirement Sheets does not surface: its guest |
| + population is dominated by single-purpose visitors who signed in once to |
| + comment and never returned, so the single-use replay store churns against |
| + a population far larger than the membership list and unbounded in time. |
| + **The store must be self-pruning, sized by assertion lifetime rather than |
| + by user count** — a naive implementation is correct on day one and a |
| + growing table for the life of the deployment. That belongs in the helper, |
| + because "single-use" is the helper's promise to keep. |