Merge remote-tracking branch 'origin/main' into feat/v140-fixes

# Conflicts:
#	docs/api/openapi.yaml
This commit is contained in:
Marc-André Bombeck committed 2026-10-03 20:06:16 +02:00
commit cd3c0b7de2
74 files changed
+4140 -508

No files matched your search

@@ -0,0 +1,20 @@
-- The encryption key backup step and the boot check that the configured key
-- opens this database.
--
-- Four nullable columns on the instance settings row record which key the
-- operator confirmed a backup of (id and fingerprint, never the key). The
-- canary table holds one known plaintext per key id, sealed under that key; a
-- process whose key cannot open it refuses to serve. Both are written at
-- runtime, so the migration only creates the shapes.
ALTER TABLE "app_settings" ADD COLUMN IF NOT EXISTS "encryption_key_backup_confirmed_at" TIMESTAMP(3);
ALTER TABLE "app_settings" ADD COLUMN IF NOT EXISTS "encryption_key_backup_confirmed_key_id" TEXT;
ALTER TABLE "app_settings" ADD COLUMN IF NOT EXISTS "encryption_key_backup_confirmed_fingerprint" TEXT;
ALTER TABLE "app_settings" ADD COLUMN IF NOT EXISTS "encryption_key_backup_confirmed_by_user_id" TEXT;
CREATE TABLE IF NOT EXISTS "encryption_key_canaries" (
"key_id" TEXT NOT NULL,
"ciphertext" TEXT NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "encryption_key_canaries_pkey" PRIMARY KEY ("key_id")
);
+31
View File
@@ -4880,9 +4880,40 @@ model AppSettings {
documentMaxFileBytes Int @default(26214400) @map("document_max_file_bytes")
documentQuotaBytes BigInt @default(1073741824) @map("document_quota_bytes")
// The operator's confirmation that the encryption key is backed up, bound to
// the key it was given for: the active key id and its fingerprint (the first
// twelve hex characters of SHA-256 over the raw key bytes). A rotation or a
// re-keyed install changes either one, and the step is due again. Never the
// key itself. Written by `POST /api/admin/encryption/key-backup/confirm`;
// consumer: `readKeyBackupStatus` (the admin step, the overview tile and the
// dashboard banner for admins).
encryptionKeyBackupConfirmedAt DateTime? @map("encryption_key_backup_confirmed_at")
encryptionKeyBackupConfirmedKeyId String? @map("encryption_key_backup_confirmed_key_id")
encryptionKeyBackupConfirmedFingerprint String? @map("encryption_key_backup_confirmed_fingerprint")
/// Plain id, not a relation: the instance row must not become user-scoped,
/// and a deleted admin leaves the confirmation standing. Consumer: the admin
/// step names who confirmed when the account still exists.
encryptionKeyBackupConfirmedByUserId String? @map("encryption_key_backup_confirmed_by_user_id")
@@map("app_settings")
}
/// One known plaintext per configured encryption key id, sealed under that
/// key. At boot every configured id is checked against its row: a row that no
/// longer opens means the key in the environment is not the key this database
/// was written with, and the process refuses to serve instead of answering
/// with random errors. A missing row is written (`ON CONFLICT DO NOTHING`).
/// Deliberately outside key rotation: a canary belongs to its key id, and
/// re-sealing it under the active key would make every id's check test the
/// active key. Written and read only by `src/lib/crypto/canary.ts`.
model EncryptionKeyCanary {
keyId String @id @map("key_id")
ciphertext String
createdAt DateTime @default(now()) @map("created_at")
@@map("encryption_key_canaries")
}
// ─── Audit Log ─────────────────────────────────────────────
model AuditLog {