fix(db): renumber board key scope migration

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Dotta 2026-08-15 19:20:37 +00:00 committed by cryppadotta
parent f78d1b4249
commit ad32d2270d
5 changed files with 242 additions and 172 deletions

View File

@ -6,7 +6,7 @@ import {
startEmbeddedPostgresTestDatabase,
} from "./test-embedded-postgres.js";
const MIGRATION_FILE = "0212_cultured_george_stacy.sql";
const MIGRATION_FILE = "0277_omniscient_darwin.sql";
const cleanups: Array<() => Promise<void>> = [];
const embeddedPostgresSupport = await getEmbeddedPostgresTestSupport();
const describeEmbeddedPostgres = embeddedPostgresSupport.supported ? describe : describe.skip;

View File

@ -1,54 +0,0 @@
ALTER TABLE "board_api_keys" ADD COLUMN IF NOT EXISTS "scope_config" jsonb;--> statement-breakpoint
ALTER TABLE "board_api_keys" ADD COLUMN IF NOT EXISTS "token_prefix" text;--> statement-breakpoint
ALTER TABLE "board_api_keys" ADD COLUMN IF NOT EXISTS "legacy_unrestricted" boolean DEFAULT false NOT NULL;--> statement-breakpoint
-- Only rows present before this migration can enter the legacy-unrestricted state.
-- Malformed non-null scope JSON is deliberately preserved so authentication fails closed.
UPDATE "board_api_keys"
SET "legacy_unrestricted" = true
WHERE "scope_config" IS NULL;--> statement-breakpoint
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_constraint
WHERE conname = 'board_api_keys_scope_legacy_check'
AND conrelid = 'board_api_keys'::regclass
) THEN
ALTER TABLE "board_api_keys"
ADD CONSTRAINT "board_api_keys_scope_legacy_check"
CHECK (
("legacy_unrestricted" = true AND "scope_config" IS NULL)
OR
("legacy_unrestricted" = false AND "scope_config" IS NOT NULL)
);
END IF;
END $$;--> statement-breakpoint
-- Pending CLI auth challenges created before this migration remain nullable and
-- cannot mint a key; every new challenge must persist a validated scope.
ALTER TABLE "cli_auth_challenges" ADD COLUMN IF NOT EXISTS "requested_scope_config" jsonb;
--> statement-breakpoint
ALTER TABLE "cli_auth_challenges" ADD COLUMN IF NOT EXISTS "pending_key_prefix" text;
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "board_api_key_authorization_events" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"board_api_key_id" uuid NOT NULL,
"owner_user_id" text NOT NULL,
"token_prefix" text,
"action" text NOT NULL,
"classification" text NOT NULL,
"authoritative_company_id" uuid,
"authoritative_resource_type" text,
"authoritative_resource_id" text,
"decision" text NOT NULL,
"reason" text NOT NULL,
"request_id" text,
"run_id" uuid,
"details" jsonb DEFAULT '{}'::jsonb NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "board_api_key_authorization_events_key_created_idx"
ON "board_api_key_authorization_events" USING btree ("board_api_key_id", "created_at" DESC);--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "board_api_key_authorization_events_company_created_idx"
ON "board_api_key_authorization_events" USING btree ("authoritative_company_id", "created_at" DESC);

View File

@ -0,0 +1,52 @@
CREATE TABLE IF NOT EXISTS "board_api_key_authorization_events" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"board_api_key_id" uuid NOT NULL,
"owner_user_id" text NOT NULL,
"token_prefix" text,
"action" text NOT NULL,
"classification" text NOT NULL,
"authoritative_company_id" uuid,
"authoritative_resource_type" text,
"authoritative_resource_id" text,
"decision" text NOT NULL,
"reason" text NOT NULL,
"request_id" text,
"run_id" uuid,
"details" jsonb DEFAULT '{}'::jsonb NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);--> statement-breakpoint
ALTER TABLE "board_api_keys" ADD COLUMN IF NOT EXISTS "scope_config" jsonb;--> statement-breakpoint
ALTER TABLE "board_api_keys" ADD COLUMN IF NOT EXISTS "token_prefix" text;--> statement-breakpoint
ALTER TABLE "board_api_keys" ADD COLUMN IF NOT EXISTS "legacy_unrestricted" boolean DEFAULT false NOT NULL;--> statement-breakpoint
-- Only rows present before this migration can enter the legacy-unrestricted
-- state. Malformed non-null scope JSON is deliberately preserved so
-- authentication fails closed.
UPDATE "board_api_keys"
SET "legacy_unrestricted" = true
WHERE "scope_config" IS NULL;--> statement-breakpoint
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'board_api_keys_scope_legacy_check'
AND conrelid = 'board_api_keys'::regclass
) THEN
ALTER TABLE "board_api_keys"
ADD CONSTRAINT "board_api_keys_scope_legacy_check"
CHECK (
("legacy_unrestricted" = true AND "scope_config" IS NULL)
OR
("legacy_unrestricted" = false AND "scope_config" IS NOT NULL)
);
END IF;
END $$;--> statement-breakpoint
-- Pending CLI auth challenges created before this migration remain nullable
-- and cannot mint a key; every new challenge must persist a validated scope.
ALTER TABLE "cli_auth_challenges" ADD COLUMN IF NOT EXISTS "requested_scope_config" jsonb;--> statement-breakpoint
ALTER TABLE "cli_auth_challenges" ADD COLUMN IF NOT EXISTS "pending_key_prefix" text;--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "board_api_key_authorization_events_key_created_idx" ON "board_api_key_authorization_events" USING btree ("board_api_key_id","created_at" DESC NULLS LAST);--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "board_api_key_authorization_events_company_created_idx" ON "board_api_key_authorization_events" USING btree ("authoritative_company_id","created_at" DESC NULLS LAST);

View File

@ -1,5 +1,5 @@
{
"id": "5f80a2a4-dcf6-48a0-be4b-f32dbb243e28",
"id": "582904aa-9302-4986-b589-2e9cfa7d7b03",
"prevId": "9c7c9e05-e663-4a29-8b46-63733f62da9b",
"version": "7",
"dialect": "postgresql",
@ -2402,119 +2402,6 @@
},
"isRLSEnabled": false
},
"public.ai_provider_defaults": {
"name": "ai_provider_defaults",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"company_id": {
"name": "company_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"provider": {
"name": "provider",
"type": "text",
"primaryKey": false,
"notNull": true
},
"grant_id": {
"name": "grant_id",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"updated_at": {
"name": "updated_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {
"ai_provider_defaults_owner_provider_uq": {
"name": "ai_provider_defaults_owner_provider_uq",
"columns": [
{
"expression": "company_id",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "user_id",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "provider",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"ai_provider_defaults_company_id_companies_id_fk": {
"name": "ai_provider_defaults_company_id_companies_id_fk",
"tableFrom": "ai_provider_defaults",
"tableTo": "companies",
"columnsFrom": [
"company_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
},
"ai_provider_defaults_company_grant_fk": {
"name": "ai_provider_defaults_company_grant_fk",
"tableFrom": "ai_provider_defaults",
"tableTo": "connection_grants",
"columnsFrom": [
"company_id",
"grant_id"
],
"columnsTo": [
"company_id",
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {
"ai_provider_defaults_provider_check": {
"name": "ai_provider_defaults_provider_check",
"value": "\"ai_provider_defaults\".\"provider\" in ('anthropic','openai','openrouter','xai')"
}
},
"isRLSEnabled": false
},
"public.approval_comments": {
"name": "approval_comments",
"schema": "",
@ -3307,6 +3194,155 @@
"checkConstraints": {},
"isRLSEnabled": false
},
"public.board_api_key_authorization_events": {
"name": "board_api_key_authorization_events",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"board_api_key_id": {
"name": "board_api_key_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"owner_user_id": {
"name": "owner_user_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"token_prefix": {
"name": "token_prefix",
"type": "text",
"primaryKey": false,
"notNull": false
},
"action": {
"name": "action",
"type": "text",
"primaryKey": false,
"notNull": true
},
"classification": {
"name": "classification",
"type": "text",
"primaryKey": false,
"notNull": true
},
"authoritative_company_id": {
"name": "authoritative_company_id",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"authoritative_resource_type": {
"name": "authoritative_resource_type",
"type": "text",
"primaryKey": false,
"notNull": false
},
"authoritative_resource_id": {
"name": "authoritative_resource_id",
"type": "text",
"primaryKey": false,
"notNull": false
},
"decision": {
"name": "decision",
"type": "text",
"primaryKey": false,
"notNull": true
},
"reason": {
"name": "reason",
"type": "text",
"primaryKey": false,
"notNull": true
},
"request_id": {
"name": "request_id",
"type": "text",
"primaryKey": false,
"notNull": false
},
"run_id": {
"name": "run_id",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"details": {
"name": "details",
"type": "jsonb",
"primaryKey": false,
"notNull": true,
"default": "'{}'::jsonb"
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {
"board_api_key_authorization_events_key_created_idx": {
"name": "board_api_key_authorization_events_key_created_idx",
"columns": [
{
"expression": "board_api_key_id",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "created_at",
"isExpression": false,
"asc": false,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
},
"board_api_key_authorization_events_company_created_idx": {
"name": "board_api_key_authorization_events_company_created_idx",
"columns": [
{
"expression": "authoritative_company_id",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "created_at",
"isExpression": false,
"asc": false,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.board_api_keys": {
"name": "board_api_keys",
"schema": "",
@ -3336,6 +3372,25 @@
"primaryKey": false,
"notNull": true
},
"scope_config": {
"name": "scope_config",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"token_prefix": {
"name": "token_prefix",
"type": "text",
"primaryKey": false,
"notNull": false
},
"legacy_unrestricted": {
"name": "legacy_unrestricted",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"last_used_at": {
"name": "last_used_at",
"type": "timestamp with time zone",
@ -3412,7 +3467,12 @@
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"checkConstraints": {
"board_api_keys_scope_legacy_check": {
"name": "board_api_keys_scope_legacy_check",
"value": "(\"board_api_keys\".\"legacy_unrestricted\" = true and \"board_api_keys\".\"scope_config\" is null) or (\"board_api_keys\".\"legacy_unrestricted\" = false and \"board_api_keys\".\"scope_config\" is not null)"
}
},
"isRLSEnabled": false
},
"public.budget_incidents": {
@ -8487,12 +8547,24 @@
"primaryKey": false,
"notNull": false
},
"requested_scope_config": {
"name": "requested_scope_config",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"pending_key_hash": {
"name": "pending_key_hash",
"type": "text",
"primaryKey": false,
"notNull": true
},
"pending_key_prefix": {
"name": "pending_key_prefix",
"type": "text",
"primaryKey": false,
"notNull": false
},
"pending_key_name": {
"name": "pending_key_name",
"type": "text",

View File

@ -1930,8 +1930,8 @@
{
"idx": 277,
"version": "7",
"when": 1789260211664,
"tag": "0277_uneven_lady_deathstrike",
"when": 1789258667522,
"tag": "0277_omniscient_darwin",
"breakpoints": true
}
]