Skip to content

Commit 3a37b8e

Browse files
authored
Merge pull request Junirezz#996 from KayMuna/feat/858
issue
2 parents 46a58ee + ae385fb commit 3a37b8e

6 files changed

Lines changed: 1106 additions & 382 deletions

File tree

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
-- Migration: Add ScopedAdminToken and ScopedAdminTokenRotationEvent tables
2+
-- Issue #858: Prisma storage layer for scoped admin tokens with rotation audit trail
3+
4+
CREATE TABLE "ScopedAdminToken" (
5+
"id" TEXT NOT NULL PRIMARY KEY,
6+
"keyId" TEXT NOT NULL UNIQUE,
7+
"hashedSecret" TEXT NOT NULL,
8+
"permissions" TEXT NOT NULL,
9+
"label" TEXT NOT NULL,
10+
"createdBy" TEXT NOT NULL,
11+
"revoked" BOOLEAN NOT NULL DEFAULT false,
12+
"revokedBy" TEXT,
13+
"revokedAt" DATETIME,
14+
"expiresAt" DATETIME,
15+
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
16+
"rotatedAt" DATETIME
17+
);
18+
19+
CREATE INDEX "ScopedAdminToken_keyId_idx" ON "ScopedAdminToken"("keyId");
20+
CREATE INDEX "ScopedAdminToken_revoked_idx" ON "ScopedAdminToken"("revoked");
21+
CREATE INDEX "ScopedAdminToken_createdAt_idx" ON "ScopedAdminToken"("createdAt");
22+
CREATE INDEX "ScopedAdminToken_expiresAt_idx" ON "ScopedAdminToken"("expiresAt");
23+
CREATE INDEX "ScopedAdminToken_createdBy_idx" ON "ScopedAdminToken"("createdBy");
24+
25+
CREATE TABLE "ScopedAdminTokenRotationEvent" (
26+
"id" TEXT NOT NULL PRIMARY KEY,
27+
"keyId" TEXT NOT NULL,
28+
"keyFingerprint" TEXT NOT NULL,
29+
"rotatedBy" TEXT NOT NULL,
30+
"rotatedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
31+
CONSTRAINT "ScopedAdminTokenRotationEvent_keyId_fkey"
32+
FOREIGN KEY ("keyId") REFERENCES "ScopedAdminToken"("keyId") ON DELETE CASCADE ON UPDATE CASCADE
33+
);
34+
35+
CREATE INDEX "ScopedAdminTokenRotationEvent_keyId_idx" ON "ScopedAdminTokenRotationEvent"("keyId");
36+
CREATE INDEX "ScopedAdminTokenRotationEvent_rotatedAt_idx" ON "ScopedAdminTokenRotationEvent"("rotatedAt");
37+
CREATE INDEX "ScopedAdminTokenRotationEvent_rotatedBy_idx" ON "ScopedAdminTokenRotationEvent"("rotatedBy");

backend/prisma/schema.prisma

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -369,3 +369,50 @@ model TransactionBackfillJob {
369369
@@index([createdAt])
370370
@@index([dryRun])
371371
}
372+
373+
// ─── Scoped Admin Tokens (Issue #858) ────────────────────────────────────────
374+
375+
/// Permission-scoped admin token. Secrets are stored as SHA-256 hashes only;
376+
/// plaintext values are never persisted.
377+
model ScopedAdminToken {
378+
id String @id @default(uuid())
379+
/// Human-readable identifier prefix `yv_` + 16 hex chars.
380+
keyId String @unique
381+
/// SHA-256(secret) – used for timing-safe authentication.
382+
hashedSecret String
383+
/// JSON-serialised AdminPermission[].
384+
permissions String
385+
label String
386+
createdBy String
387+
revoked Boolean @default(false)
388+
revokedBy String?
389+
revokedAt DateTime?
390+
expiresAt DateTime?
391+
createdAt DateTime @default(now())
392+
rotatedAt DateTime?
393+
394+
rotationEvents ScopedAdminTokenRotationEvent[]
395+
396+
@@index([keyId])
397+
@@index([revoked])
398+
@@index([createdAt])
399+
@@index([expiresAt])
400+
@@index([createdBy])
401+
}
402+
403+
/// Immutable audit record written on every successful secret rotation.
404+
/// Old hashed secrets are NOT stored; only the timing and actor are retained.
405+
model ScopedAdminTokenRotationEvent {
406+
id String @id @default(uuid())
407+
keyId String
408+
/// Fingerprint of the key that was rotated (sha256:<first 16 hex chars>).
409+
keyFingerprint String
410+
rotatedBy String
411+
rotatedAt DateTime @default(now())
412+
413+
token ScopedAdminToken @relation(fields: [keyId], references: [keyId], onDelete: Cascade)
414+
415+
@@index([keyId])
416+
@@index([rotatedAt])
417+
@@index([rotatedBy])
418+
}

0 commit comments

Comments
 (0)