capsite/docs: references to DOs in Discord messages
How a Discord message could carry references to our DOs, after @endo/chat: names in the text, references beside it in a mailbox DO, an Adopt button answered ephemerally, and per-user petstores. Webkeys in markdown work but disclose the capability to every channel reader and to Discord itself; ephemeral replies still pass through Discord, so references should never cross it at all. Generated-by: Claude Opus 5.5
This commit is contained in:
1 parent
93d92d0207
commit
b4ab820486
1 file changed
+98
@@ -0,0 +1,98 @@
|
||||
# references to DOs in Discord messages
|
||||
|
||||
*In Smalltalk, JavaScript, and Unix sockets, a message can carry
|
||||
references the receiver can then send messages on. Discord messages
|
||||
can refer to messages, threads, channels, and servers, but not to our
|
||||
Durable Objects. This note works out how they could, after
|
||||
[@endo/chat](https://github.com/endojs/endo-but-for-bots/tree/llm/packages/chat).
|
||||
Written 2026-10-08.*
|
||||
|
||||
## webkeys in markdown: works, but discloses
|
||||
|
||||
The cheapest option is a [webkey](webkeys.md) as a markdown link,
|
||||
`[cookie store](https://webapp.example/store/#<key>)`. It gets the
|
||||
basics right: following it in a browser works, and the key sits in the
|
||||
fragment, so Discord's link-preview fetcher never sends it anywhere.
|
||||
|
||||
But the message body itself is the leak. Every reader of the channel
|
||||
now holds the capability, and so does Discord: its servers, its logs,
|
||||
its search index, its moderation tooling, and whoever reads those.
|
||||
That is fine for capabilities meant to be public (a public cache page)
|
||||
and wrong for anything like webapp's store id, where holding it means
|
||||
choosing whose session the cache uses.
|
||||
|
||||
## what @endo/chat does instead
|
||||
|
||||
In @endo/chat a message is not a string with secrets in it. Its parser
|
||||
(`message-parse.js`) splits `Here is @my-file for you` into
|
||||
|
||||
- `strings`: the text between references,
|
||||
- `edgeNames`: the names the *sender* put in the text (`my-file`),
|
||||
- `petNames`: the sender's own names for those references,
|
||||
|
||||
and the daemon delivers the references themselves beside the text,
|
||||
over CapTP. The receiver sees the edge name and **adopts** the
|
||||
reference under a petname of their own. Three things follow:
|
||||
|
||||
- **References never appear in text.** Only names do, and names are
|
||||
safe to show anyone.
|
||||
- **Names are per-person.** My `cookie-store` and your `alice's store`
|
||||
can be the same object, and neither name has authority by itself.
|
||||
- **Adopting is an act.** Receiving a message gives you the option to
|
||||
hold a reference, not the reference itself, until you choose a name
|
||||
for it.
|
||||
|
||||
## the endo-shaped version for Discord
|
||||
|
||||
A capsite bot (a Worker serving Discord's HTTP interactions endpoint)
|
||||
plays the daemon's part:
|
||||
|
||||
- **Petstores.** Each Discord user has a petstore DO: petname →
|
||||
DO reference. Minting files results there directly: webapp's mint
|
||||
could file the store id under `diigo cookie store` in Alice's
|
||||
petstore instead of handing it to her password manager.
|
||||
- **Sending.** A slash command, e.g.
|
||||
`/send to:@bob text:"here is @cookie-store"`. The bot parses the text
|
||||
like `parseMessage`, looks up each `@name` in the sender's petstore,
|
||||
posts the text (names only) to the channel, and files
|
||||
`{ discord message id → recipients, edgeName → reference }` in a
|
||||
mailbox DO.
|
||||
- **Adopting.** The posted message carries an **Adopt** button. On a
|
||||
click, Discord tells the bot who clicked (in a signed interaction);
|
||||
if they are a recipient, the bot answers with an *ephemeral* message,
|
||||
shown only to them, asking for a petname, then files the reference
|
||||
in *their* petstore.
|
||||
- **Using.** References are exercised through the bot by petname
|
||||
(`/call cookie-store set cookie …`, or a link to a page the bot
|
||||
serves). The reference stays inside our Workers.
|
||||
|
||||
### what this does and doesn't keep from Discord
|
||||
|
||||
Ephemeral replies hide content from other channel readers, but **not
|
||||
from Discord**: they still pass through its servers. So the design
|
||||
above never sends a reference through Discord at all, ephemeral or
|
||||
not; only petnames, edge names, and message ids cross it. Handing over
|
||||
a webkey in an ephemeral reply would be a weaker variant: hidden from
|
||||
the channel, still disclosed to the infrastructure.
|
||||
|
||||
What Discord *does* hold is identity: the bot believes Discord about
|
||||
who clicked **Adopt**. A compromised Discord account, or Discord
|
||||
itself, could therefore adopt references meant for that user. That is
|
||||
the same trust users already put in Discord for their messages, but it
|
||||
should be stated where references matter more than chat does.
|
||||
|
||||
## open questions
|
||||
|
||||
- **Durable references across RPC.** A petstore entry is a DO id plus
|
||||
enough to say what kind of thing it is (webapp instance, header
|
||||
store, cache). Getting from that back to a live RpcTarget is the
|
||||
marshalling-layer question from the cookie-rotation work
|
||||
(`docs/cookie-rotation-wiring.html` on the `webapp-cookie-rotation`
|
||||
branch).
|
||||
- **Petstore ids.** `idFromName(discordUserId)` is convenient but
|
||||
ambient: any worker with the namespace binding can recompute it.
|
||||
Acceptable if only the bot has the binding; otherwise mint with
|
||||
`newUniqueId` and keep the mapping in one place.
|
||||
- **Leaving Discord.** The same mailbox and petstore could back a web
|
||||
page or @endo/chat itself; Discord would then be one front end of
|
||||
several.
|
||||
Reference in new issue
Block a user