Zum Inhalt springen

Zugriff und Identität

Der Worker verifiziert kurzlebige, signierte Workspace-Claims. Deine Anwendung implementiert den Identity-Adapter, der das angemeldete Subject und seine Workspace-Rolle auflöst. Produktion lehnt die Development-Claim-Route ab.

Jede Aktion eines Modells nimmt eine von drei Entscheidungen:

Regel Zweck
role(...) Verlangt eine Mindestrolle, bevor SQL läuft.
filter(...) Wendet eine Leseregel innerhalb von SQL-Queries und Counts an.
check(...) Bewertet eine Zeile oder einen Schreibvorgang mit Anwendungslogik.
board: model({
name: t.string(),
ownerId: t.string().index(),
}).access({
read: filter(({ auth }) => ({ ownerId: auth.sub })),
update: check(({ auth, row }) => row?.ownerId === auth.sub),
delete: role("moderator"),
}),

Defaults: Lesen member, Create/Update editor, Delete moderator.

  • filter(({ auth }) => where) ist nur für read. Es wird in das SQL jeder Query kompiliert, die das Modell berührt, nutzt also Indizes, füllt jede Seite und zählt korrekt. Ein Array ist OR; der Filter des Aufrufers ANDet damit.
  • check() funktioniert für jede Aktion. Es erhält { auth, row, data, changed } und gibt einen Boolean zurück. Schreibchecks laufen innerhalb der Transaktion neben dem Schreibvorgang, den sie bewachen. Bei read scannen sie passende Zeilen, bis die Seite voll oder die Ergebnismenge erschöpft ist. Counts wenden dieselbe Sichtbarkeitsregel an.
  • Eine Regel, die einen Fehler wirft, verweigert den Zugriff.

Bevorzuge SQL-Filter für Lesesichtbarkeit, weil ein funktionsbasierter Lesecheck nach dem Abruf läuft.

Dateien haben eigene Regeln im selben Schema: siehe Dateizugriff.

Die Engine verifiziert einen Claim und stellt nie einen aus. Implementiere zwei Methoden, dann liefert die Library die Claim-Route, standardmäßig mit Rate-Limit:

import { defineTapeDB, WORKSPACE_ROLE_LEVELS } from "tapedb/worker";
import schema from "./tapedb.schema";
import { migrations } from "./tapedb/migrations.gen";
const tapedb = defineTapeDB({
schema,
migrations,
identity: {
async getSubject(headers) {
const session = await auth.api.getSession({ headers });
return session ? { sub: session.user.id } : null;
},
async getRole(workspaceId, sub) {
const member = await memberships.get(workspaceId, sub);
return member ? WORKSPACE_ROLE_LEVELS[member.role] : null;
},
// Optional; das sind die Standardwerte.
limits: {
ip: { requests: 300, perSeconds: 60 },
subject: { requests: 30, perSeconds: 60 },
},
},
});
export const WorkspaceRoom = tapedb.WorkspaceRoom;
export default tapedb.worker;

POST /_tapedb/claim gibt { token, workspaceId, roleLevel, expiresAt, expiresIn } zurück. Die Route führt einen Token-Bucket pro IP des Aufrufers, geprüft vor dem Nachschlagen der Session, und einen pro Subject, geprüft danach. Die Buckets liegen in Durable Objects, daher ist eine Zählung exakt und global, und sie speichern HMACs von IP und Subject, nie die Werte selbst. Über einem Limit antwortet die Route mit 429 rate_limited und Retry-After; setze ein Limit auf false oder limits: false, um es abzuschalten, und ergänze allowRequest(key) für eine eigene Prüfung, die nach dem IP-Limit läuft, mit key = claim:<IP des Aufrufers>. Ist der Limiter nicht erreichbar, gehen Claims durch und der Worker protokolliert claim_limit_unavailable. Der Client sendet ihn als Authorization: Bearer <token> und erneuert ihn beim Reconnect. In Development aktiviert tapedb dev eine Dev-Claim-Route, sodass der Client überhaupt keine Session braucht; ENVIRONMENT=production lehnt diese Route ab, und tapedb deploy weigert sich, das Flag auszuliefern.

Schemaänderungen und ihre SQL-Migrationen stehen in Migrationen.