oficina / suite Public

Revision 9: a link token is stored hashed, and the rule that never left Sheets

Sheets settled this in its own spec §8.3 — "stored plaintext, the
replicated database becomes a bundle of live read capabilities" — and the
suite never lifted it. So Docs built link sharing with a plaintext token
column and a share page that re-displayed the URL on every visit, which
is precisely the convenience the rule costs and precisely the thing the
rule says you do not get.

Found by reading Sheets' schema while looking for something else. Not by
review, and not by any test Docs had: a plaintext token is not a
behaviour a test can observe, which is why it needs to be a rule rather
than a gate.

This is §2's failure mode with the roles reversed from revision 3's. That
one restated a sibling's rule and let the copy drift. This one is a rule
that lived correctly in one app and was never lifted into the doctrine,
so the next app could not inherit it and re-decided it worse, in
ignorance. Both are arguments for the same thing: the doctrine holds the
rule once, and apps cite it.

Ruling 16 also binds apps whose content is not sealed, which is the
tempting exception. A leaked database of a plaintext app already yields
the documents, so the token looks free. It is not: a token grants ongoing
access to a live system including every future revision, which a snapshot
does not, and an app that is plaintext today is one migration from being
sealed tomorrow — at which point the plaintext token is the single row
that defeats the sealing. Docs is exactly that app.

The accepted cost is stated rather than hidden: an owner who loses the
URL cannot ask for it back, only replace it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RVcEQ8NgrPCpRTb6JtBz1Q
Paul Campbell pushed by paul@keymail.dev 75063e69fddd7d7423e494ee856123573184b5cf parent 60b01a8
1 file changed, +58 −1
  • docs/superpowers/specs/2026-09-05-suite-sharing-doctrine.md +58 −1

Intent

Attributed to this commit: 72.8 g CO₂e (6805436 tokens). This is an attribution, not a measurement — usage counted from this commit's own marker forward to the next one on worktree-worktree-token-at-rest, never backward from it. A turn that produced more than one commit books all of its tokens to the last one, and a concurrent session on this branch can add to the same figure. Some of it was priced using the shared default rate, because no model-specific factor row exists yet for at least one model involved. The Carbon view has the repo total, which is never derived from this attribution.

diff --git a/docs/superpowers/specs/2026-09-05-suite-sharing-doctrine.md b/docs/superpowers/specs/2026-09-05-suite-sharing-doctrine.md
index 47622e3..4b6301f 100644
--- a/docs/superpowers/specs/2026-09-05-suite-sharing-doctrine.md
+++ b/docs/superpowers/specs/2026-09-05-suite-sharing-doctrine.md
@@ -3,7 +3,16 @@
How the apps of the suite share identity, membership and content with
each other, and where the line is that nothing crosses.
-**Status:** design, **revision 8**, 2026-09-05.
+**Status:** design, **revision 9**, 2026-09-06.
+
+**Revision 9 adds ruling 16: a link token is stored hashed, never in
+plaintext.** Sheets settled this in its own spec §8.3 and the suite never
+adopted it, so Docs built link sharing with a plaintext token column and a
+share page that re-displayed the URL — re-deciding, worse, a question a
+sibling had already answered. Found by reading Sheets' schema while looking
+for something else. This is the §2 failure mode with the roles reversed
+from revision 3's: not a rule restated and drifted, but a rule that lived
+in one app and was never lifted, so the next app could not inherit it.
**Revision 8 records that the contract module cannot be imported**, which
makes §12 step 2 built rather than done. `amadan.net/oficina/suite` does not
@@ -307,6 +316,39 @@ somebody read this", it is "does team standing alone reach it". A link
share is a capability held by whoever has the URL, and it is deliberately
not a grant to anybody in particular.
+**A link token is stored hashed, never in plaintext** (ruling 16). What an
+app keeps is a SHA-256 of the token — enough to recognise one coming back,
+never enough to produce one. The token is shown to its maker once, at
+creation, and the app cannot show it again.
+
+Sheets settled this first (its spec §8.3) and its sentence is the one to
+keep: *"stored plaintext, the replicated database becomes a bundle of live
+read capabilities."* A backup, a replica, a support export and a stray
+`SELECT` are all places a database goes where a live URL should not follow.
+
+The hash is fast (SHA-256), not a password hash, and the difference is
+deliberate rather than an oversight to correct later. A password is
+low-entropy and needs a slow hash to survive guessing; a token is 128 bits
+or more from `crypto/rand`, so guessing is not in the threat model and
+key-stretching would buy nothing while making every public page load do
+work. The property that matters — *the stored form does not yield the
+token* — a fast hash gives just as completely.
+
+The cost is real and is accepted: an owner who loses the URL cannot ask the
+app for it, only replace it. That is the honest trade, and it is smaller
+than it looks — replacing a link the owner has lost track of is usually the
+right action anyway.
+
+**This binds an app whether or not its content is sealed.** The tempting
+exception is an app storing plaintext content, where a leaked database
+already yields the documents and the token appears to add nothing. It adds
+two things. A token grants *ongoing* access to a live system, including
+every future revision, which a stolen snapshot does not; and an app that is
+plaintext today is one migration from being sealed tomorrow, at which point
+the plaintext token is the single row that defeats the sealing. Docs is
+exactly this case — its own spec §2 has titles due to be sealed — and Docs
+is where the rule was broken, not where it was foreseen.
+
Getting this wrong is a live leak, not a technicality. Ana keeps a
document private and link-shares it with one contractor. If the link made
it team-visible, memoria would ingest it, and the first teammate to search
@@ -1033,6 +1075,12 @@ apps are a suite, and every one is a consequence of ruling 1 or 4.
URL reads or writes the item, the app cannot name them, and a
forwarded link is a grant nobody recorded. This is what "anyone with
the link" means and there is no version of it that is not this.
+13. **A link token in a database column is a leak waiting for a backup**
+ (ruling 16). It is the one credential in these apps that arrives in a
+ URL, which means it is also in browser history, in a referer header if
+ an app is careless, and in whatever chat it was pasted into. The
+ database is the one of those places the app controls, so it is the one
+ the app is required to get right. Store the hash; show the token once.
---
@@ -1394,6 +1442,15 @@ link can edit" share is a link to sign-in that mints a guest grant, which
is the shape Docs already uses for its commenter link; the writer is then
a named guest and §7's envelope can carry them.
+Docs stores its link token **hashed** (suite ruling 16). Its first
+implementation stored the token in plaintext and re-displayed it on the
+share page on every visit, which is the convenience the rule costs. The
+defect was found by reading Sheets' schema while looking for something
+else — not by review, and not by any test Docs had, because a plaintext
+token is not a behaviour a test can observe. That is the argument for the
+doctrine holding storage rules at all: this one was already settled in a
+sibling and Docs re-decided it, worse, in ignorance.
+
Docs is the first receiver of the hand-off, so its create endpoint takes
suite §7.1 whole: the `Origin` check against the `consumers` allowlist at
receipt, pending state, and a commit from Docs' own origin. Its pinned