From 6397c7988dcca21464cb505673d435177194ab03 Mon Sep 17 00:00:00 2001 From: Padreug Date: Fri, 19 Jun 2026 15:00:33 +0200 Subject: [PATCH] feat(schema)(#25): Request.keyUserId + SigningCondition lifecycle for live grant eval Additive, non-breaking schema prep for the Option D live-evaluation ACL: - Request gains keyUserId (FK) + @@index([keyUserId, method]) so token usage caps can be derived live by COUNTing allowed Requests, replacing the never-enforced mutable PolicyRule.currentUsageCount (derive-don't-count, per lnbits/nostr_bunker prior art). - SigningCondition gains createdAt/expiresAt/revokedAt so the manual-override layer carries its own lifecycle and runs through the same grantIsLive(now) predicate as token grants (D1: two typed sources, one shared rule). No behavior change yet; the ACL hot path and applyToken de-materialization follow in subsequent commits. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../migration.sql | 37 +++++++++++++++++++ prisma/schema.prisma | 23 +++++++++++- 2 files changed, 58 insertions(+), 2 deletions(-) create mode 100644 prisma/migrations/20260619125847_live_grant_lifecycle_schema/migration.sql diff --git a/prisma/migrations/20260619125847_live_grant_lifecycle_schema/migration.sql b/prisma/migrations/20260619125847_live_grant_lifecycle_schema/migration.sql new file mode 100644 index 0000000..7608e6e --- /dev/null +++ b/prisma/migrations/20260619125847_live_grant_lifecycle_schema/migration.sql @@ -0,0 +1,37 @@ +-- RedefineTables +PRAGMA defer_foreign_keys=ON; +PRAGMA foreign_keys=OFF; +CREATE TABLE "new_Request" ( + "id" TEXT NOT NULL PRIMARY KEY, + "keyName" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "requestId" TEXT NOT NULL, + "remotePubkey" TEXT NOT NULL, + "method" TEXT NOT NULL, + "params" TEXT, + "allowed" BOOLEAN, + "keyUserId" INTEGER, + CONSTRAINT "Request_keyUserId_fkey" FOREIGN KEY ("keyUserId") REFERENCES "KeyUser" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); +INSERT INTO "new_Request" ("allowed", "createdAt", "id", "keyName", "method", "params", "remotePubkey", "requestId") SELECT "allowed", "createdAt", "id", "keyName", "method", "params", "remotePubkey", "requestId" FROM "Request"; +DROP TABLE "Request"; +ALTER TABLE "new_Request" RENAME TO "Request"; +CREATE INDEX "Request_keyUserId_method_idx" ON "Request"("keyUserId", "method"); +CREATE TABLE "new_SigningCondition" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "method" TEXT, + "kind" TEXT, + "content" TEXT, + "keyUserKeyName" TEXT, + "allowed" BOOLEAN, + "keyUserId" INTEGER, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "expiresAt" DATETIME, + "revokedAt" DATETIME, + CONSTRAINT "SigningCondition_keyUserId_fkey" FOREIGN KEY ("keyUserId") REFERENCES "KeyUser" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); +INSERT INTO "new_SigningCondition" ("allowed", "content", "id", "keyUserId", "keyUserKeyName", "kind", "method") SELECT "allowed", "content", "id", "keyUserId", "keyUserKeyName", "kind", "method" FROM "SigningCondition"; +DROP TABLE "SigningCondition"; +ALTER TABLE "new_SigningCondition" RENAME TO "SigningCondition"; +PRAGMA foreign_keys=ON; +PRAGMA defer_foreign_keys=OFF; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index f0072ea..73f54df 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -17,6 +17,14 @@ model Request { method String params String? allowed Boolean? + // Bind each request to the KeyUser it was evaluated against so usage + // caps can be derived live by COUNTing allowed Requests, instead of + // maintaining a mutable PolicyRule.currentUsageCount that drifts. + // See aiolabs/nsecbunkerd#25 (Option D, derive-don't-count). + keyUserId Int? + KeyUser KeyUser? @relation(fields: [keyUserId], references: [id]) + + @@index([keyUserId, method]) } model KeyUser { @@ -31,6 +39,7 @@ model KeyUser { logs Log[] signingConditions SigningCondition[] Token Token[] + requests Request[] @@unique([keyName, userPubkey], name: "unique_key_user") } @@ -56,15 +65,25 @@ model User { pubkey String } +// The SigningCondition layer is the MANUAL-OVERRIDE source of truth +// (web-approval / add_signing_condition / create_account bootstrap) — it is +// no longer materialized from token policies (see aiolabs/nsecbunkerd#25: +// applyToken stopped photocopying; token grants are evaluated live off +// Token -> Policy -> PolicyRule). Under D1 the override layer carries its +// own lifecycle so it runs through the same grantIsLive(now) predicate as +// token grants. model SigningCondition { - id Int @id @default(autoincrement()) + id Int @id @default(autoincrement()) method String? kind String? content String? keyUserKeyName String? allowed Boolean? keyUserId Int? - KeyUser KeyUser? @relation(fields: [keyUserId], references: [id]) + createdAt DateTime @default(now()) + expiresAt DateTime? + revokedAt DateTime? + KeyUser KeyUser? @relation(fields: [keyUserId], references: [id]) } model Log {