feat(api): separate API keys from MCP tokens, add travellers

An MCP token drives every assistant tool; an API key reads trips over HTTP.
Handing the same credential to a third-party integration gives it the whole
MCP surface, so the two are now separate kinds on `mcp_tokens` and the kind
sits in the WHERE clause — a token of the wrong kind is indistinguishable
from one that does not exist.

Also adds `travellers` to the trip payload: display name and an owner flag,
no ids and no email addresses. An integration key reads its owner's
itinerary; it is not a way to enumerate the people around them.

Documents the surface in the wiki, since integrators need something to read
that is not the source.
This commit is contained in:
Maurice
2026-08-28 01:29:04 +02:00
committed by Maurice
parent 9cddf683a5
commit 4b120d39c8
9 changed files with 381 additions and 20 deletions
+22
View File
@@ -4154,6 +4154,28 @@ function runMigrations(db: Database.Database): void {
db.exec("ALTER TABLE reservations ADD COLUMN ingest_state TEXT NOT NULL DEFAULT 'live'");
}
},
/*
* Separate an integration key from an MCP token (#2089).
*
* Both live in mcp_tokens and until now both opened everything a token can
* open. That was fine while /mcp was the only consumer; with a public REST
* surface it means a key somebody minted for a chat client also reads their
* trips over HTTP, and a key minted for an integration can drive every MCP
* tool. One credential, two very different blast radii.
*
* `kind` splits them, and every existing row becomes 'mcp' — that is what
* they were issued for, and silently widening a key that is already in
* somebody's config would be the opposite of what this migration is for.
*
* Appended LAST: the array is index-addressed against schema_version.
*/
() => {
const cols = db.prepare("SELECT name FROM pragma_table_info('mcp_tokens')").all() as Array<{ name: string }>;
if (!cols.some(c => c.name === 'kind')) {
db.exec("ALTER TABLE mcp_tokens ADD COLUMN kind TEXT NOT NULL DEFAULT 'mcp'");
}
},
];
if (currentVersion < migrations.length) {