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
99 lines
4.7 KiB
Markdown
99 lines
4.7 KiB
Markdown
# 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.
|