fix(core): token writes keep their actor in history but are user-owned in field_sources

fieldOwner(actor) maps every non-plugin actor to "user" wherever field_sources is written
(entries, engagements, list ownership from history), so syncs never overwrite a field an API
token edited. authenticateAppToken also returns the token name; revokeAppToken reports whether
it revoked; listSuggestions queries the canonical work id.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Lucas Winther
2026-10-30 21:29:38 +01:00
co-authored by Claude Opus 5.5
parent e3a759d768
commit aa8e4db070
6 changed files with 110 additions and 18 deletions
+12 -4
View File
@@ -170,11 +170,17 @@ export async function createAppToken(
}
export async function verifyAppToken(db: Db, token: string, scope: TokenScope): Promise<User | null> {
return (await authenticateAppToken(db, token, scope))?.user ?? null;
}
/** Like `verifyAppToken`, also returning the token's name (API writes record actor `token:<name>`). */
export async function authenticateAppToken(db: Db, token: string, scope: TokenScope): Promise<{ user: User; tokenName: string } | null> {
const row = await db.k
.selectFrom("app_tokens")
.innerJoin("users", "users.id", "app_tokens.user_id")
.select([
"app_tokens.id as token_id",
"app_tokens.name as token_name",
"app_tokens.scopes",
"users.id",
"users.username",
@@ -186,17 +192,19 @@ export async function verifyAppToken(db: Db, token: string, scope: TokenScope):
.executeTakeFirst();
if (!row || !fromJson<string[]>(row.scopes, []).includes(scope)) return null;
await db.k.updateTable("app_tokens").set({ last_used_at: nowIso() }).where("id", "=", row.token_id).execute();
return toUser(row);
return { user: toUser(row), tokenName: row.token_name };
}
export async function revokeAppToken(db: Db, userId: string, id: string): Promise<void> {
await db.k
/** Revoke one of the user's live tokens. Returns false when no such live token exists (unknown, foreign or revoked). */
export async function revokeAppToken(db: Db, userId: string, id: string): Promise<boolean> {
const r = await db.k
.updateTable("app_tokens")
.set({ revoked_at: nowIso() })
.where("id", "=", id)
.where("user_id", "=", userId)
.where("revoked_at", "is", null)
.execute();
.executeTakeFirst();
return Number(r.numUpdatedRows) === 1;
}
export interface AppTokenInfo {