@marinoscar/kvox-eval 0.0.0-bootstrap → 0.1.0-next.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/LICENSE +21 -0
- package/README.md +166 -0
- package/dist/ask/index.d.ts +6 -0
- package/dist/ask/index.js +28 -0
- package/dist/ask/question-schema.d.ts +121 -0
- package/dist/ask/question-schema.js +167 -0
- package/dist/ask/report.d.ts +88 -0
- package/dist/ask/report.js +170 -0
- package/dist/ask/scorer.d.ts +104 -0
- package/dist/ask/scorer.js +164 -0
- package/dist/ask-run/ask-command.d.ts +62 -0
- package/dist/ask-run/ask-command.js +254 -0
- package/dist/ask-run/host/app.d.ts +65 -0
- package/dist/ask-run/host/app.js +146 -0
- package/dist/ask-run/host/db.d.ts +38 -0
- package/dist/ask-run/host/db.js +126 -0
- package/dist/ask-run/host/ports.d.ts +35 -0
- package/dist/ask-run/host/ports.js +113 -0
- package/dist/ask-run/index.d.ts +13 -0
- package/dist/ask-run/index.js +35 -0
- package/dist/ask-run/replay.d.ts +128 -0
- package/dist/ask-run/replay.js +212 -0
- package/dist/ask-run/run-ask-eval.d.ts +125 -0
- package/dist/ask-run/run-ask-eval.js +193 -0
- package/dist/ask-run/seed-golden-graph.d.ts +64 -0
- package/dist/ask-run/seed-golden-graph.js +495 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.js +12 -0
- package/dist/cli/kg-command.d.ts +66 -0
- package/dist/cli/kg-command.js +238 -0
- package/dist/cli/main.d.ts +18 -0
- package/dist/cli/main.js +69 -0
- package/dist/fixtures/domains.d.ts +16 -0
- package/dist/fixtures/domains.js +33 -0
- package/dist/fixtures/fixture-schema.d.ts +323 -0
- package/dist/fixtures/fixture-schema.js +199 -0
- package/dist/fixtures/index.d.ts +16 -0
- package/dist/fixtures/index.js +40 -0
- package/dist/fixtures/load.d.ts +73 -0
- package/dist/fixtures/load.js +137 -0
- package/dist/fixtures/questions.d.ts +15 -0
- package/dist/fixtures/questions.js +26 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +36 -0
- package/dist/kg/gold-runner.d.ts +13 -0
- package/dist/kg/gold-runner.js +57 -0
- package/dist/kg/index.d.ts +17 -0
- package/dist/kg/index.js +65 -0
- package/dist/kg/internals.d.ts +7 -0
- package/dist/kg/internals.js +17 -0
- package/dist/kg/prediction-schema.d.ts +245 -0
- package/dist/kg/prediction-schema.js +102 -0
- package/dist/kg/predictions.d.ts +11 -0
- package/dist/kg/predictions.js +49 -0
- package/dist/kg/report.d.ts +84 -0
- package/dist/kg/report.js +125 -0
- package/dist/kg/run-kg-eval.d.ts +77 -0
- package/dist/kg/run-kg-eval.js +137 -0
- package/dist/kg/runner.d.ts +72 -0
- package/dist/kg/runner.js +51 -0
- package/dist/kg/runners/extract-resolve-runner.d.ts +40 -0
- package/dist/kg/runners/extract-resolve-runner.js +199 -0
- package/dist/kg/runners/extract-runner.d.ts +65 -0
- package/dist/kg/runners/extract-runner.js +243 -0
- package/dist/kg/scorer.d.ts +226 -0
- package/dist/kg/scorer.js +463 -0
- package/dist/kg/targets.d.ts +17 -0
- package/dist/kg/targets.js +16 -0
- package/dist/kg/text.d.ts +45 -0
- package/dist/kg/text.js +100 -0
- package/dist/live/adapters.d.ts +16 -0
- package/dist/live/adapters.js +58 -0
- package/dist/live/env.d.ts +39 -0
- package/dist/live/env.js +56 -0
- package/dist/live/index.d.ts +6 -0
- package/dist/live/index.js +16 -0
- package/dist/live/live-ai.d.ts +104 -0
- package/dist/live/live-ai.js +157 -0
- package/dist/parity/constants.d.ts +79 -0
- package/dist/parity/constants.js +104 -0
- package/dist/parity/index.d.ts +20 -0
- package/dist/parity/index.js +38 -0
- package/dist/parity/internals.d.ts +8 -0
- package/dist/parity/internals.js +16 -0
- package/dist/parity/scenario.d.ts +45 -0
- package/dist/parity/scenario.js +245 -0
- package/dist/parity/script.d.ts +44 -0
- package/dist/parity/script.js +136 -0
- package/dist/parity/snapshot.d.ts +43 -0
- package/dist/parity/snapshot.js +71 -0
- package/dist/real-data/index.d.ts +2 -0
- package/dist/real-data/index.js +12 -0
- package/dist/real-data/real-data.d.ts +76 -0
- package/dist/real-data/real-data.js +156 -0
- package/fixtures/kg-golden/README.md +136 -0
- package/fixtures/kg-golden/ask-questions.json +458 -0
- package/fixtures/kg-golden/meetings/m01-northwind-pilot-kickoff.json +792 -0
- package/fixtures/kg-golden/meetings/m02-halden-migration-planning.json +814 -0
- package/fixtures/kg-golden/meetings/m03-larkspur-weekly-sync.json +601 -0
- package/fixtures/kg-golden/meetings/m04-corvid-intake-discovery.json +690 -0
- package/fixtures/kg-golden/meetings/m05-hosting-vendor-selection.json +631 -0
- package/fixtures/kg-golden/meetings/m06-northwind-floor-data-review.json +649 -0
- package/fixtures/kg-golden/meetings/m07-gullwing-intro-notes.json +428 -0
- package/fixtures/kg-golden/meetings/m08-halden-migration-checkpoint.json +672 -0
- package/fixtures/kg-golden/meetings/m09-northwind-platform-org-update.json +711 -0
- package/fixtures/kg-golden/meetings/m10-corvid-prototype-review.json +623 -0
- package/fixtures/kg-golden/meetings/m11-solvane-grid-dashboard-scoping.json +689 -0
- package/fixtures/kg-golden/meetings/m12-northwind-pilot-replan.json +681 -0
- package/fixtures/kg-golden/meetings/m13-halden-cutover-go-no-go.json +721 -0
- package/fixtures/kg-golden/meetings/m14-gullwing-discovery-readout.json +719 -0
- package/fixtures/kg-golden/meetings/m15-halden-handover-ravi-leaving.json +716 -0
- package/fixtures/kg-golden/meetings/m16-varden-claims-portal-intro.json +704 -0
- package/fixtures/kg-golden/meetings/m17-corvid-usability-debrief.json +572 -0
- package/fixtures/kg-golden/meetings/m18-hosting-decision-revisited.json +655 -0
- package/fixtures/kg-golden/meetings/m19-solvane-integration-kickoff.json +726 -0
- package/fixtures/kg-golden/meetings/m20-northwind-go-live-retro.json +673 -0
- package/fixtures/kg-golden/meetings/m21-quarterly-account-review.json +449 -0
- package/fixtures/kg-golden/meetings/m22-halden-intro-call-2020.json +558 -0
- package/fixtures/kg-golden/meetings/m23-gullwing-model-review.json +702 -0
- package/fixtures/kg-golden/meetings/m24-varden-discovery-midpoint.json +643 -0
- package/fixtures/kg-golden/meetings/m25-corvid-rollout-planning.json +642 -0
- package/fixtures/kg-golden/meetings/m26-larkspur-engineering-prep.json +695 -0
- package/fixtures/kg-golden/meetings/m27-solvane-phase-one-demo.json +649 -0
- package/fixtures/kg-golden/meetings/m28-northwind-building-5-scoping.json +715 -0
- package/fixtures/kg-golden/meetings/m29-halden-southern-cutover-review.json +752 -0
- package/fixtures/kg-golden/meetings/m30-varden-portal-build-kickoff.json +697 -0
- package/fixtures/kg-golden/meetings/m31-gullwing-rollout-renewal.json +681 -0
- package/fixtures/kg-golden/meetings/m32-brambleway-weekly-sync.json +472 -0
- package/fixtures/kg-golden/meetings/m33-nina-oliver-one-on-one.json +342 -0
- package/fixtures/kg-golden/meetings/m34-coffee-with-dana.json +249 -0
- package/fixtures/kg-golden/meetings/m35-brambleway-june-sync.json +553 -0
- package/fixtures/kg-golden/meetings/m36-copenhagen-conference-planning.json +296 -0
- package/host/generated/client.d.ts +1 -0
- package/host/generated/client.js +5 -0
- package/host/generated/default.d.ts +1 -0
- package/host/generated/default.js +5 -0
- package/host/generated/edge.d.ts +1 -0
- package/host/generated/edge.js +1658 -0
- package/host/generated/index-browser.js +1684 -0
- package/host/generated/index.d.ts +192335 -0
- package/host/generated/index.js +1658 -0
- package/host/generated/package.json +139 -0
- package/host/generated/query_compiler_fast_bg.js +2 -0
- package/host/generated/query_compiler_fast_bg.wasm +0 -0
- package/host/generated/query_compiler_fast_bg.wasm-base64.js +2 -0
- package/host/generated/runtime/client.d.ts +3433 -0
- package/host/generated/runtime/client.js +110 -0
- package/host/generated/runtime/index-browser.d.ts +90 -0
- package/host/generated/runtime/index-browser.js +6 -0
- package/host/generated/runtime/wasm-compiler-edge.js +100 -0
- package/host/generated/schema.prisma +6312 -0
- package/host/generated/wasm-edge-light-loader.mjs +5 -0
- package/host/generated/wasm-worker-loader.mjs +5 -0
- package/host/prisma/fragments/kvox.ask.prisma +126 -0
- package/host/prisma/fragments/kvox.graph.prisma +1073 -0
- package/host/prisma/fragments/kvox.notes.prisma +787 -0
- package/host/prisma/fragments/kvox.search.prisma +211 -0
- package/host/prisma/fragments/kvox.transcripts.prisma +790 -0
- package/host/prisma/kvox.lock +101 -0
- package/host/prisma/migrations/20240101000000_initial/migration.sql +297 -0
- package/host/prisma/migrations/20240101000001_add_personal_access_tokens/migration.sql +31 -0
- package/host/prisma/migrations/20240101000002_add_credentials/migration.sql +20 -0
- package/host/prisma/migrations/20240101000003_add_notification_deliveries/migration.sql +27 -0
- package/host/prisma/migrations/20240101000004_drop_stale_uuid_defaults/migration.sql +35 -0
- package/host/prisma/migrations/20240101000005_add_notifications/migration.sql +22 -0
- package/host/prisma/migrations/20240101000006_add_push_subscriptions/migration.sql +25 -0
- package/host/prisma/migrations/20240101000007_add_jobs/migration.sql +110 -0
- package/host/prisma/migrations/20240101000008_add_worker_nodes/migration.sql +90 -0
- package/host/prisma/migrations/20240101000009_add_database_backup_runs/migration.sql +104 -0
- package/host/prisma/migrations/20240101000010_add_notification_broadcasts/migration.sql +57 -0
- package/host/prisma/migrations/20240101000011_add_backup_run_job_link/migration.sql +87 -0
- package/host/prisma/migrations/20240101000012_add_job_node_secrets/migration.sql +61 -0
- package/host/prisma/migrations/20240101000013_add_backup_run_pg_dump_version/migration.sql +26 -0
- package/host/prisma/migrations/20240101000014_add_job_claim_token/migration.sql +92 -0
- package/host/prisma/migrations/20240101000015_add_ai_platform/migration.sql +108 -0
- package/host/prisma/migrations/20240101000016_revoke_viewer_ai_use/migration.sql +32 -0
- package/host/prisma/migrations/20240101000017_add_user_credentials/migration.sql +29 -0
- package/host/prisma/migrations/20240101000018_add_device_session_credential_link/migration.sql +52 -0
- package/host/prisma/migrations/20240101000019_add_worker_node_vitals/migration.sql +3 -0
- package/host/prisma/migrations/20240101000020_add_job_trace_context/migration.sql +6 -0
- package/host/prisma/migrations/20240101000021_add_retention_created_at_indexes/migration.sql +13 -0
- package/host/prisma/migrations/20240101000022_add_organizations/migration.sql +142 -0
- package/host/prisma/migrations/20240101000023_split_system_org_roles/migration.sql +131 -0
- package/host/prisma/migrations/20240101000024_org_scoped_rls/migration.sql +215 -0
- package/host/prisma/migrations/20240101000025_add_groups/migration.sql +165 -0
- package/host/prisma/migrations/20240101000026_add_grants/migration.sql +117 -0
- package/host/prisma/migrations/20240101000027_add_org_settings/migration.sql +49 -0
- package/host/prisma/migrations/20240101000028_add_org_credentials/migration.sql +54 -0
- package/host/prisma/migrations/20240101000029_add_jobs_org_id/migration.sql +18 -0
- package/host/prisma/migrations/20240101000030_add_jobs_org_id_status_index/migration.sql +6 -0
- package/host/prisma/migrations/20240101000031_add_broadcast_target_org/migration.sql +21 -0
- package/host/prisma/migrations/20240101000032_add_android_app/migration.sql +60 -0
- package/host/prisma/migrations/20240101000033_open_job_reason/migration.sql +16 -0
- package/host/prisma/migrations/20240101000034_ai_usage_feature/migration.sql +37 -0
- package/host/prisma/migrations/20240101000035_add_app_metadata/migration.sql +49 -0
- package/host/prisma/migrations/20240101000036_open_sharing_vocabularies/migration.sql +97 -0
- package/host/prisma/migrations/20260914130000_kvox_add_transcripts/migration.sql +322 -0
- package/host/prisma/migrations/20260914140000_kvox_add_notes/migration.sql +311 -0
- package/host/prisma/migrations/20260915120000_kvox_add_search_vectors/migration.sql +150 -0
- package/host/prisma/migrations/20260915130000_kvox_add_search_embeddings/migration.sql +362 -0
- package/host/prisma/migrations/20260915210000_kvox_add_note_title_source/migration.sql +30 -0
- package/host/prisma/migrations/20260923120000_kvox_add_note_generation_context_snapshot/migration.sql +24 -0
- package/host/prisma/migrations/20260923130000_kvox_add_user_hidden_note_templates/migration.sql +35 -0
- package/host/prisma/migrations/20260923140000_kvox_add_transcript_speaker_identities/migration.sql +6 -0
- package/host/prisma/migrations/20260923150000_kvox_transcript_name_checks/migration.sql +95 -0
- package/host/prisma/migrations/20260923160000_kvox_add_note_body_format/migration.sql +27 -0
- package/host/prisma/migrations/20260923170000_kvox_add_note_version_body_format/migration.sql +23 -0
- package/host/prisma/migrations/20260926012548_kvox_add_knowledge_graph/migration.sql +680 -0
- package/host/prisma/migrations/20260926030000_kvox_fix_kg_relations_valid_precision_chk/migration.sql +29 -0
- package/host/prisma/migrations/20260926030100_kvox_add_transcript_recorded_at/migration.sql +9 -0
- package/host/prisma/migrations/20260926030200_kvox_kg_evidence_invariant/migration.sql +60 -0
- package/host/prisma/migrations/20260926040000_kvox_add_kg_graph_layouts/migration.sql +30 -0
- package/host/prisma/migrations/20260926050000_kvox_add_ask_conversations/migration.sql +99 -0
- package/host/prisma/migrations/20260926231512_kvox_add_kg_exports/migration.sql +52 -0
- package/host/prisma/migrations/20261016120000_kvox_transcripts_org_id/migration.sql +49 -0
- package/host/prisma/platform.lock +319 -0
- package/host/prisma/schema/app.kvox.ask.prisma +110 -0
- package/host/prisma/schema/app.kvox.graph.prisma +1030 -0
- package/host/prisma/schema/app.kvox.notes.prisma +746 -0
- package/host/prisma/schema/app.kvox.search.prisma +208 -0
- package/host/prisma/schema/app.kvox.transcripts.prisma +726 -0
- package/host/prisma/schema/platform.ai.prisma +419 -0
- package/host/prisma/schema/platform.android-app.prisma +46 -0
- package/host/prisma/schema/platform.base.prisma +15 -0
- package/host/prisma/schema/platform.credentials.prisma +156 -0
- package/host/prisma/schema/platform.db-backup.prisma +359 -0
- package/host/prisma/schema/platform.identity.prisma +638 -0
- package/host/prisma/schema/platform.jobs.prisma +824 -0
- package/host/prisma/schema/platform.notifications.prisma +641 -0
- package/host/prisma/schema/platform.settings.prisma +53 -0
- package/host/prisma/schema/platform.sharing.prisma +208 -0
- package/host/prisma/schema/platform.storage.prisma +118 -0
- package/package.json +112 -1
- package/parity/requests.ask.json +2681 -0
- package/parity/requests.m01.json +1554 -0
- package/parity/requests.m02.json +1554 -0
- package/parity/requests.m03.json +1554 -0
- package/parity/requests.m04.json +1554 -0
- package/parity/requests.m05.json +1554 -0
- package/parity/requests.m06.json +1554 -0
- package/parity/requests.m07.json +1554 -0
- package/parity/requests.m08.json +1554 -0
- package/parity/requests.m09.json +1554 -0
- package/parity/requests.m10.json +1554 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
-- =============================================================================
|
|
2
|
+
-- Revoke `ai:use` from the Viewer role on existing deployments (issue #499)
|
|
3
|
+
-- =============================================================================
|
|
4
|
+
-- Viewer is the DEFAULT role every new user lands in (see `ROLES` in
|
|
5
|
+
-- `prisma/seed-data.ts` and the allowlist-driven signup path). Seeding
|
|
6
|
+
-- `ai:use` onto it (issue #423, epic #419) meant every fresh signup could
|
|
7
|
+
-- call AI with no explicit grant — harmless under the `byok` key policy (no
|
|
8
|
+
-- key, no calls succeed), but wrong under `byok_with_org_fallback`: a
|
|
9
|
+
-- brand-new Viewer would silently spend the deployment's own org key the
|
|
10
|
+
-- first time they touched an AI surface, with no administrator having
|
|
11
|
+
-- decided that person should be able to.
|
|
12
|
+
--
|
|
13
|
+
-- This migration is DATA-ONLY (no `schema.prisma` change — the shape of
|
|
14
|
+
-- `role_permissions` is unchanged) and undoes exactly that one grant on
|
|
15
|
+
-- deployments that already ran the old seed: it deletes the single
|
|
16
|
+
-- `role_permissions` row joining the role named 'viewer' and the permission
|
|
17
|
+
-- named 'ai:use', and touches nothing else — not Admin's or Contributor's
|
|
18
|
+
-- grants, not any other Viewer permission.
|
|
19
|
+
--
|
|
20
|
+
-- IDEMPOTENT: a `DELETE ... WHERE` naming both rows by subquery is a no-op if
|
|
21
|
+
-- the row is already gone, or if either the 'viewer' role or the 'ai:use'
|
|
22
|
+
-- permission does not exist at all (a fork that renamed or removed either).
|
|
23
|
+
-- Safe to run against a database that never had the row (a fresh install
|
|
24
|
+
-- seeded from the already-updated `seed-data.ts`) and safe to re-run.
|
|
25
|
+
--
|
|
26
|
+
-- An administrator who wants a specific Viewer (or all of them) to use AI
|
|
27
|
+
-- again grants it back explicitly — re-insert a `role_permissions` row for
|
|
28
|
+
-- `('viewer', 'ai:use')` — or promotes the account to Contributor, which
|
|
29
|
+
-- still carries the grant.
|
|
30
|
+
DELETE FROM "role_permissions"
|
|
31
|
+
WHERE "role_id" = (SELECT "id" FROM "roles" WHERE "name" = 'viewer')
|
|
32
|
+
AND "permission_id" = (SELECT "id" FROM "permissions" WHERE "name" = 'ai:use');
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
-- =============================================================================
|
|
2
|
+
-- Per-user encrypted credentials (issue #387)
|
|
3
|
+
-- =============================================================================
|
|
4
|
+
-- A sibling of "credentials", which is NOT modified. Rows are addressed by
|
|
5
|
+
-- (user_id, purpose, name); `secret` is ciphertext encrypted under the
|
|
6
|
+
-- owner-bound sub-key domain `user:<user_id>:<purpose>`. All three unique
|
|
7
|
+
-- columns are NOT NULL so the unique index is a real constraint. Rows cascade
|
|
8
|
+
-- with their owner.
|
|
9
|
+
|
|
10
|
+
-- CreateTable
|
|
11
|
+
CREATE TABLE "user_credentials" (
|
|
12
|
+
"id" UUID NOT NULL,
|
|
13
|
+
"user_id" UUID NOT NULL,
|
|
14
|
+
"purpose" TEXT NOT NULL,
|
|
15
|
+
"name" TEXT NOT NULL,
|
|
16
|
+
"secret" TEXT NOT NULL,
|
|
17
|
+
"hint" TEXT,
|
|
18
|
+
"label" TEXT,
|
|
19
|
+
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
20
|
+
"updated_at" TIMESTAMPTZ NOT NULL,
|
|
21
|
+
|
|
22
|
+
CONSTRAINT "user_credentials_pkey" PRIMARY KEY ("id")
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
-- CreateIndex
|
|
26
|
+
CREATE UNIQUE INDEX "user_credentials_user_id_purpose_name_key" ON "user_credentials"("user_id", "purpose", "name");
|
|
27
|
+
|
|
28
|
+
-- AddForeignKey
|
|
29
|
+
ALTER TABLE "user_credentials" ADD CONSTRAINT "user_credentials_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
package/host/prisma/migrations/20240101000018_add_device_session_credential_link/migration.sql
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
-- =============================================================================
|
|
2
|
+
-- Device session <-> issued credential link (issue #518)
|
|
3
|
+
-- =============================================================================
|
|
4
|
+
-- Today nothing links a `DeviceCode` row to what it issued: once the polling
|
|
5
|
+
-- client collects a PAT (and that PAT is used to mint refresh tokens),
|
|
6
|
+
-- revoking the `DeviceCode` "session" does nothing to the credential the
|
|
7
|
+
-- device is actually holding. This migration adds the columns that let a
|
|
8
|
+
-- device-session revocation reach both:
|
|
9
|
+
--
|
|
10
|
+
-- 1. `device_codes.pat_id`: the PAT minted for this device once collected.
|
|
11
|
+
-- Nullable until collection. `SetNull` on delete — a `DeviceCode` history
|
|
12
|
+
-- row must survive its PAT being deleted (or aging out) independently.
|
|
13
|
+
-- 2. `device_codes.collected_at` / `credential_expires_at`: collection state
|
|
14
|
+
-- copied from the PAT at collection time, so the sessions list can filter
|
|
15
|
+
-- "approved, not yet collected" vs. "collected, still valid" without a
|
|
16
|
+
-- join against `personal_access_tokens`.
|
|
17
|
+
-- 3. `device_codes.revoked_at`: marks the device session (and whatever it
|
|
18
|
+
-- issued) as revoked. Distinct from `PersonalAccessToken.revokedAt` /
|
|
19
|
+
-- `RefreshToken.revokedAt`, which the revocation service also sets on the
|
|
20
|
+
-- linked rows — this column is what the sessions list itself filters on.
|
|
21
|
+
-- 4. `refresh_tokens.device_code_id`: the device-authorization session that
|
|
22
|
+
-- minted this refresh token, if any, so that same revocation can reach
|
|
23
|
+
-- refresh tokens too. `SetNull` on delete for the same reason as above.
|
|
24
|
+
--
|
|
25
|
+
-- Additive only: every new column is nullable, no backfill, no data migration.
|
|
26
|
+
-- =============================================================================
|
|
27
|
+
|
|
28
|
+
-- AlterTable
|
|
29
|
+
ALTER TABLE "device_codes"
|
|
30
|
+
ADD COLUMN "pat_id" UUID,
|
|
31
|
+
ADD COLUMN "collected_at" TIMESTAMPTZ,
|
|
32
|
+
ADD COLUMN "credential_expires_at" TIMESTAMPTZ,
|
|
33
|
+
ADD COLUMN "revoked_at" TIMESTAMPTZ;
|
|
34
|
+
|
|
35
|
+
-- AlterTable
|
|
36
|
+
ALTER TABLE "refresh_tokens" ADD COLUMN "device_code_id" UUID;
|
|
37
|
+
|
|
38
|
+
-- CreateIndex
|
|
39
|
+
-- Serves the sessions list: "this user's device sessions that are not
|
|
40
|
+
-- revoked" (approved-uncollected and collected-unexpired both satisfy
|
|
41
|
+
-- `revoked_at IS NULL`); the remaining collected/expiry filtering is done in
|
|
42
|
+
-- the query predicate, not a second index.
|
|
43
|
+
CREATE INDEX "device_codes_user_id_revoked_at_idx" ON "device_codes"("user_id", "revoked_at");
|
|
44
|
+
|
|
45
|
+
-- CreateIndex
|
|
46
|
+
CREATE INDEX "refresh_tokens_device_code_id_idx" ON "refresh_tokens"("device_code_id");
|
|
47
|
+
|
|
48
|
+
-- AddForeignKey
|
|
49
|
+
ALTER TABLE "device_codes" ADD CONSTRAINT "device_codes_pat_id_fkey" FOREIGN KEY ("pat_id") REFERENCES "personal_access_tokens"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
|
50
|
+
|
|
51
|
+
-- AddForeignKey
|
|
52
|
+
ALTER TABLE "refresh_tokens" ADD CONSTRAINT "refresh_tokens_device_code_id_fkey" FOREIGN KEY ("device_code_id") REFERENCES "device_codes"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
-- Issue #607: carry the enqueuing request's W3C trace context on the job row.
|
|
2
|
+
-- Nullable, no default, no index: existing rows simply have no trace context.
|
|
3
|
+
-- The partial unique index jobs_active_dedup_uniq_idx is deliberately untouched.
|
|
4
|
+
|
|
5
|
+
-- AlterTable
|
|
6
|
+
ALTER TABLE "jobs" ADD COLUMN "trace_context" TEXT;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
-- Retention sweeps (#681): each purge reads "the oldest rows older than the
|
|
2
|
+
-- cutoff", across every user. Plain btree indexes on the timestamp, so a batch
|
|
3
|
+
-- is an index range scan rather than a sequential scan. `audit_events` already
|
|
4
|
+
-- has `audit_events_created_at_idx`.
|
|
5
|
+
|
|
6
|
+
-- CreateIndex
|
|
7
|
+
CREATE INDEX "notification_deliveries_created_at_idx" ON "notification_deliveries"("created_at");
|
|
8
|
+
|
|
9
|
+
-- CreateIndex
|
|
10
|
+
CREATE INDEX "notifications_created_at_idx" ON "notifications"("created_at");
|
|
11
|
+
|
|
12
|
+
-- CreateIndex
|
|
13
|
+
CREATE INDEX "ai_runs_created_at_idx" ON "ai_runs"("created_at");
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
-- CreateEnum
|
|
2
|
+
CREATE TYPE "MembershipStatus" AS ENUM ('active', 'suspended');
|
|
3
|
+
|
|
4
|
+
-- CreateEnum
|
|
5
|
+
CREATE TYPE "InviteStatus" AS ENUM ('pending', 'accepted', 'revoked', 'expired');
|
|
6
|
+
|
|
7
|
+
-- AlterTable
|
|
8
|
+
ALTER TABLE "device_codes" ADD COLUMN "org_id" UUID;
|
|
9
|
+
|
|
10
|
+
-- AlterTable
|
|
11
|
+
ALTER TABLE "personal_access_tokens" ADD COLUMN "org_id" UUID;
|
|
12
|
+
|
|
13
|
+
-- AlterTable
|
|
14
|
+
ALTER TABLE "refresh_tokens" ADD COLUMN "org_id" UUID;
|
|
15
|
+
|
|
16
|
+
-- CreateTable
|
|
17
|
+
CREATE TABLE "organizations" (
|
|
18
|
+
"id" UUID NOT NULL,
|
|
19
|
+
"name" TEXT NOT NULL,
|
|
20
|
+
"slug" TEXT NOT NULL,
|
|
21
|
+
"is_default" BOOLEAN NOT NULL DEFAULT false,
|
|
22
|
+
"created_by_id" UUID,
|
|
23
|
+
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
24
|
+
"updated_at" TIMESTAMPTZ NOT NULL,
|
|
25
|
+
|
|
26
|
+
CONSTRAINT "organizations_pkey" PRIMARY KEY ("id")
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
-- CreateTable
|
|
30
|
+
CREATE TABLE "memberships" (
|
|
31
|
+
"id" UUID NOT NULL,
|
|
32
|
+
"org_id" UUID NOT NULL,
|
|
33
|
+
"user_id" UUID NOT NULL,
|
|
34
|
+
"status" "MembershipStatus" NOT NULL DEFAULT 'active',
|
|
35
|
+
"last_active_at" TIMESTAMPTZ,
|
|
36
|
+
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
37
|
+
"updated_at" TIMESTAMPTZ NOT NULL,
|
|
38
|
+
|
|
39
|
+
CONSTRAINT "memberships_pkey" PRIMARY KEY ("id")
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
-- CreateTable
|
|
43
|
+
CREATE TABLE "org_invites" (
|
|
44
|
+
"id" UUID NOT NULL,
|
|
45
|
+
"org_id" UUID NOT NULL,
|
|
46
|
+
"email" TEXT NOT NULL,
|
|
47
|
+
"status" "InviteStatus" NOT NULL DEFAULT 'pending',
|
|
48
|
+
"token_hash" TEXT,
|
|
49
|
+
"expires_at" TIMESTAMPTZ,
|
|
50
|
+
"invited_by_id" UUID,
|
|
51
|
+
"accepted_by_id" UUID,
|
|
52
|
+
"accepted_at" TIMESTAMPTZ,
|
|
53
|
+
"notes" TEXT,
|
|
54
|
+
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
55
|
+
|
|
56
|
+
CONSTRAINT "org_invites_pkey" PRIMARY KEY ("id")
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
-- CreateIndex
|
|
60
|
+
CREATE UNIQUE INDEX "organizations_slug_key" ON "organizations"("slug");
|
|
61
|
+
|
|
62
|
+
-- CreateIndex
|
|
63
|
+
CREATE INDEX "memberships_user_id_idx" ON "memberships"("user_id");
|
|
64
|
+
|
|
65
|
+
-- CreateIndex
|
|
66
|
+
CREATE UNIQUE INDEX "memberships_org_id_user_id_key" ON "memberships"("org_id", "user_id");
|
|
67
|
+
|
|
68
|
+
-- CreateIndex
|
|
69
|
+
CREATE UNIQUE INDEX "org_invites_token_hash_key" ON "org_invites"("token_hash");
|
|
70
|
+
|
|
71
|
+
-- CreateIndex
|
|
72
|
+
CREATE INDEX "org_invites_email_idx" ON "org_invites"("email");
|
|
73
|
+
|
|
74
|
+
-- CreateIndex
|
|
75
|
+
CREATE UNIQUE INDEX "org_invites_org_id_email_key" ON "org_invites"("org_id", "email");
|
|
76
|
+
|
|
77
|
+
-- CreateIndex
|
|
78
|
+
CREATE INDEX "device_codes_org_id_idx" ON "device_codes"("org_id");
|
|
79
|
+
|
|
80
|
+
-- CreateIndex
|
|
81
|
+
CREATE INDEX "personal_access_tokens_org_id_idx" ON "personal_access_tokens"("org_id");
|
|
82
|
+
|
|
83
|
+
-- CreateIndex
|
|
84
|
+
CREATE INDEX "refresh_tokens_org_id_idx" ON "refresh_tokens"("org_id");
|
|
85
|
+
|
|
86
|
+
-- AddForeignKey
|
|
87
|
+
ALTER TABLE "refresh_tokens" ADD CONSTRAINT "refresh_tokens_org_id_fkey" FOREIGN KEY ("org_id") REFERENCES "organizations"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
88
|
+
|
|
89
|
+
-- AddForeignKey
|
|
90
|
+
ALTER TABLE "personal_access_tokens" ADD CONSTRAINT "personal_access_tokens_org_id_fkey" FOREIGN KEY ("org_id") REFERENCES "organizations"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
91
|
+
|
|
92
|
+
-- AddForeignKey
|
|
93
|
+
ALTER TABLE "device_codes" ADD CONSTRAINT "device_codes_org_id_fkey" FOREIGN KEY ("org_id") REFERENCES "organizations"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
94
|
+
|
|
95
|
+
-- AddForeignKey
|
|
96
|
+
ALTER TABLE "organizations" ADD CONSTRAINT "organizations_created_by_id_fkey" FOREIGN KEY ("created_by_id") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
|
97
|
+
|
|
98
|
+
-- AddForeignKey
|
|
99
|
+
ALTER TABLE "memberships" ADD CONSTRAINT "memberships_org_id_fkey" FOREIGN KEY ("org_id") REFERENCES "organizations"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
100
|
+
|
|
101
|
+
-- AddForeignKey
|
|
102
|
+
ALTER TABLE "memberships" ADD CONSTRAINT "memberships_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
103
|
+
|
|
104
|
+
-- AddForeignKey
|
|
105
|
+
ALTER TABLE "org_invites" ADD CONSTRAINT "org_invites_org_id_fkey" FOREIGN KEY ("org_id") REFERENCES "organizations"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
106
|
+
|
|
107
|
+
-- AddForeignKey
|
|
108
|
+
ALTER TABLE "org_invites" ADD CONSTRAINT "org_invites_invited_by_id_fkey" FOREIGN KEY ("invited_by_id") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
|
109
|
+
|
|
110
|
+
-- AddForeignKey
|
|
111
|
+
ALTER TABLE "org_invites" ADD CONSTRAINT "org_invites_accepted_by_id_fkey" FOREIGN KEY ("accepted_by_id") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
|
112
|
+
|
|
113
|
+
-- =============================================================================
|
|
114
|
+
-- Raw SQL (hand-written; Prisma cannot express a partial unique index)
|
|
115
|
+
-- =============================================================================
|
|
116
|
+
|
|
117
|
+
-- Exactly one default organization: a constant-expression partial unique index.
|
|
118
|
+
-- Intentional schema drift: never replace it with @@unique or a findFirst check.
|
|
119
|
+
CREATE UNIQUE INDEX "organizations_default_uniq_idx" ON "organizations" ((true)) WHERE "is_default";
|
|
120
|
+
|
|
121
|
+
-- =============================================================================
|
|
122
|
+
-- Backfill (forward-only). Every INSERT names its id: the server-side uuid
|
|
123
|
+
-- defaults were dropped (0005_drop_stale_uuid_defaults).
|
|
124
|
+
-- =============================================================================
|
|
125
|
+
|
|
126
|
+
-- 1. The default organization.
|
|
127
|
+
INSERT INTO "organizations" ("id", "name", "slug", "is_default", "created_at", "updated_at")
|
|
128
|
+
VALUES (gen_random_uuid(), 'Default organization', 'default', true, now(), now())
|
|
129
|
+
ON CONFLICT DO NOTHING;
|
|
130
|
+
|
|
131
|
+
-- 2. One active membership per existing user.
|
|
132
|
+
INSERT INTO "memberships" ("id", "org_id", "user_id", "status", "last_active_at", "created_at", "updated_at")
|
|
133
|
+
SELECT gen_random_uuid(), o."id", u."id", 'active', now(), now(), now()
|
|
134
|
+
FROM "users" u
|
|
135
|
+
CROSS JOIN "organizations" o
|
|
136
|
+
WHERE o."is_default"
|
|
137
|
+
ON CONFLICT ("org_id", "user_id") DO NOTHING;
|
|
138
|
+
|
|
139
|
+
-- 3. Every existing credential row acts in the default organization.
|
|
140
|
+
UPDATE "refresh_tokens" SET "org_id" = (SELECT "id" FROM "organizations" WHERE "is_default") WHERE "org_id" IS NULL;
|
|
141
|
+
UPDATE "personal_access_tokens" SET "org_id" = (SELECT "id" FROM "organizations" WHERE "is_default") WHERE "org_id" IS NULL;
|
|
142
|
+
UPDATE "device_codes" SET "org_id" = (SELECT "id" FROM "organizations" WHERE "is_default") WHERE "org_id" IS NULL;
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
-- =============================================================================
|
|
2
|
+
-- Split RBAC into system roles and org roles (PP-6.3, issue #723)
|
|
3
|
+
-- =============================================================================
|
|
4
|
+
--
|
|
5
|
+
-- System roles operate the deployment and stay in "user_roles" (admin). Org
|
|
6
|
+
-- roles operate one organization and move onto the membership
|
|
7
|
+
-- ("memberships"."role_id"): org_admin, contributor, viewer. Every role and
|
|
8
|
+
-- permission gains a "scope". Effective permissions become the system roles'
|
|
9
|
+
-- grants plus the grants of the current organization's membership role, so the
|
|
10
|
+
-- data below is moved so that every user keeps exactly what they had.
|
|
11
|
+
--
|
|
12
|
+
-- Forward-only and in one transaction: the expand (columns) and the data move
|
|
13
|
+
-- happen together, so no request between "migrate deploy" and the seed run
|
|
14
|
+
-- sees an administrator without a permission. Nothing is dropped: "user_roles"
|
|
15
|
+
-- keeps its shape (expand/contract); only rows pointing at org roles go.
|
|
16
|
+
-- Every INSERT names its id (0005_drop_stale_uuid_defaults).
|
|
17
|
+
-- =============================================================================
|
|
18
|
+
|
|
19
|
+
-- CreateEnum
|
|
20
|
+
CREATE TYPE "RoleScope" AS ENUM ('system', 'org');
|
|
21
|
+
|
|
22
|
+
-- AlterTable: nullable first, backfilled below, then NOT NULL.
|
|
23
|
+
ALTER TABLE "memberships" ADD COLUMN "role_id" UUID;
|
|
24
|
+
|
|
25
|
+
-- AlterTable: NULL means the default org role (viewer).
|
|
26
|
+
ALTER TABLE "org_invites" ADD COLUMN "role_id" UUID;
|
|
27
|
+
|
|
28
|
+
-- AlterTable
|
|
29
|
+
ALTER TABLE "permissions" ADD COLUMN "scope" "RoleScope" NOT NULL DEFAULT 'system';
|
|
30
|
+
|
|
31
|
+
-- AlterTable
|
|
32
|
+
ALTER TABLE "roles" ADD COLUMN "scope" "RoleScope" NOT NULL DEFAULT 'system';
|
|
33
|
+
|
|
34
|
+
-- -----------------------------------------------------------------------------
|
|
35
|
+
-- 1. The org roles exist. org_admin is new; viewer is the fallback role of
|
|
36
|
+
-- step 4 and may be missing on a database the seed has not reached yet.
|
|
37
|
+
-- The seed refreshes both descriptions.
|
|
38
|
+
-- -----------------------------------------------------------------------------
|
|
39
|
+
INSERT INTO "roles" ("id", "name", "description", "scope")
|
|
40
|
+
VALUES (gen_random_uuid(), 'org_admin', 'Organization administrator - everything a contributor can do, plus manage the organization members and invites', 'org')
|
|
41
|
+
ON CONFLICT ("name") DO NOTHING;
|
|
42
|
+
|
|
43
|
+
INSERT INTO "roles" ("id", "name", "description", "scope")
|
|
44
|
+
VALUES (gen_random_uuid(), 'viewer', 'Read-only organization member - view content and manage own settings', 'org')
|
|
45
|
+
ON CONFLICT ("name") DO NOTHING;
|
|
46
|
+
|
|
47
|
+
-- -----------------------------------------------------------------------------
|
|
48
|
+
-- 2. Scopes. admin stays 'system' (the column default), like every
|
|
49
|
+
-- operational permission. An app's own roles and permissions keep the
|
|
50
|
+
-- default until the seed writes the scope their declarations name.
|
|
51
|
+
-- -----------------------------------------------------------------------------
|
|
52
|
+
UPDATE "roles" SET "scope" = 'org' WHERE "name" IN ('org_admin', 'contributor', 'viewer');
|
|
53
|
+
|
|
54
|
+
UPDATE "permissions" SET "scope" = 'org'
|
|
55
|
+
WHERE "name" IN (
|
|
56
|
+
'user_settings:read', 'user_settings:write', 'storage:read', 'storage:write', 'ai:use',
|
|
57
|
+
'org_members:read', 'org_members:write', 'org_invites:read', 'org_invites:write'
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
-- -----------------------------------------------------------------------------
|
|
61
|
+
-- 3. admin's grants of org permissions move to org_admin: copy first, then
|
|
62
|
+
-- delete. Every admin gets org_admin on their membership in step 4, in this
|
|
63
|
+
-- same transaction, so no administrator loses a permission. Grants an
|
|
64
|
+
-- administrator made to contributor or viewer are left as they are.
|
|
65
|
+
-- -----------------------------------------------------------------------------
|
|
66
|
+
INSERT INTO "role_permissions" ("role_id", "permission_id")
|
|
67
|
+
SELECT oa."id", rp."permission_id"
|
|
68
|
+
FROM "role_permissions" rp
|
|
69
|
+
JOIN "roles" a ON a."id" = rp."role_id" AND a."name" = 'admin'
|
|
70
|
+
JOIN "permissions" p ON p."id" = rp."permission_id" AND p."scope" = 'org'
|
|
71
|
+
CROSS JOIN "roles" oa
|
|
72
|
+
WHERE oa."name" = 'org_admin'
|
|
73
|
+
ON CONFLICT DO NOTHING;
|
|
74
|
+
|
|
75
|
+
DELETE FROM "role_permissions" rp
|
|
76
|
+
USING "roles" a, "permissions" p
|
|
77
|
+
WHERE rp."role_id" = a."id" AND a."name" = 'admin'
|
|
78
|
+
AND rp."permission_id" = p."id" AND p."scope" = 'org';
|
|
79
|
+
|
|
80
|
+
-- -----------------------------------------------------------------------------
|
|
81
|
+
-- 4. Every user is a member of the default organization (0023 did this for
|
|
82
|
+
-- the users that existed then; this catches any created since through a
|
|
83
|
+
-- path that skipped it), and every membership gets its role from the
|
|
84
|
+
-- user's global roles: admin -> org_admin; otherwise the highest of
|
|
85
|
+
-- contributor > viewer; none -> viewer (the default org role).
|
|
86
|
+
-- -----------------------------------------------------------------------------
|
|
87
|
+
INSERT INTO "memberships" ("id", "org_id", "user_id", "status", "last_active_at", "created_at", "updated_at")
|
|
88
|
+
SELECT gen_random_uuid(), o."id", u."id", 'active', now(), now(), now()
|
|
89
|
+
FROM "users" u
|
|
90
|
+
CROSS JOIN "organizations" o
|
|
91
|
+
WHERE o."is_default"
|
|
92
|
+
ON CONFLICT ("org_id", "user_id") DO NOTHING;
|
|
93
|
+
|
|
94
|
+
UPDATE "memberships" m
|
|
95
|
+
SET "role_id" = (
|
|
96
|
+
SELECT r."id" FROM "roles" r
|
|
97
|
+
WHERE r."name" = CASE
|
|
98
|
+
WHEN EXISTS (
|
|
99
|
+
SELECT 1 FROM "user_roles" ur JOIN "roles" x ON x."id" = ur."role_id"
|
|
100
|
+
WHERE ur."user_id" = m."user_id" AND x."name" = 'admin'
|
|
101
|
+
) THEN 'org_admin'
|
|
102
|
+
WHEN EXISTS (
|
|
103
|
+
SELECT 1 FROM "user_roles" ur JOIN "roles" x ON x."id" = ur."role_id"
|
|
104
|
+
WHERE ur."user_id" = m."user_id" AND x."name" = 'contributor'
|
|
105
|
+
) THEN 'contributor'
|
|
106
|
+
ELSE 'viewer'
|
|
107
|
+
END
|
|
108
|
+
)
|
|
109
|
+
WHERE m."role_id" IS NULL;
|
|
110
|
+
|
|
111
|
+
ALTER TABLE "memberships" ALTER COLUMN "role_id" SET NOT NULL;
|
|
112
|
+
|
|
113
|
+
-- -----------------------------------------------------------------------------
|
|
114
|
+
-- 5. Contract, in data only: the global assignments of org roles are now
|
|
115
|
+
-- carried by the memberships. admin rows (system) stay.
|
|
116
|
+
-- -----------------------------------------------------------------------------
|
|
117
|
+
DELETE FROM "user_roles" ur
|
|
118
|
+
USING "roles" r
|
|
119
|
+
WHERE ur."role_id" = r."id" AND r."scope" = 'org';
|
|
120
|
+
|
|
121
|
+
-- CreateIndex
|
|
122
|
+
CREATE INDEX "memberships_role_id_idx" ON "memberships"("role_id");
|
|
123
|
+
|
|
124
|
+
-- CreateIndex
|
|
125
|
+
CREATE INDEX "org_invites_role_id_idx" ON "org_invites"("role_id");
|
|
126
|
+
|
|
127
|
+
-- AddForeignKey
|
|
128
|
+
ALTER TABLE "memberships" ADD CONSTRAINT "memberships_role_id_fkey" FOREIGN KEY ("role_id") REFERENCES "roles"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
|
129
|
+
|
|
130
|
+
-- AddForeignKey
|
|
131
|
+
ALTER TABLE "org_invites" ADD CONSTRAINT "org_invites_role_id_fkey" FOREIGN KEY ("role_id") REFERENCES "roles"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
-- =============================================================================
|
|
2
|
+
-- org_id on tenant-scoped tables and row-level security (PP-6.5, issue #725)
|
|
3
|
+
-- =============================================================================
|
|
4
|
+
--
|
|
5
|
+
-- The database enforces organization isolation (ADR 0002 D5). Four tables are
|
|
6
|
+
-- org-owned and get a NOT NULL (or, for usage events, nullable) org_id plus a
|
|
7
|
+
-- FORCEd policy keyed on transaction-local settings; audit_events gets a
|
|
8
|
+
-- nullable org_id and no policy in this release.
|
|
9
|
+
--
|
|
10
|
+
-- storage_objects org_id NOT NULL, FK RESTRICT, RLS forced
|
|
11
|
+
-- storage_object_chunks org_id NOT NULL, FK RESTRICT, RLS forced; its link to
|
|
12
|
+
-- the parent is now COMPOSITE (object_id, org_id) ->
|
|
13
|
+
-- storage_objects (id, org_id): a foreign-key check
|
|
14
|
+
-- bypasses row-level security, so a plain key would let
|
|
15
|
+
-- one organization store a reference to another's row
|
|
16
|
+
-- ai_runs org_id NOT NULL, FK RESTRICT, RLS forced. Restrict, not
|
|
17
|
+
-- SetNull: the column is NOT NULL, so history cannot
|
|
18
|
+
-- survive an organization's deletion by nulling it
|
|
19
|
+
-- ai_usage_events org_id nullable, FK SET NULL, RLS forced. Nullable so
|
|
20
|
+
-- a deployment-wide event (a catalogue sync) and the
|
|
21
|
+
-- history of a deleted organization survive; such a row
|
|
22
|
+
-- is visible to the system (bypass) client only
|
|
23
|
+
-- audit_events org_id nullable, FK SET NULL, NO row-level security
|
|
24
|
+
--
|
|
25
|
+
-- BACKFILL. Every existing row of the three NOT NULL tables, and of
|
|
26
|
+
-- ai_usage_events, goes to the default organization (created by
|
|
27
|
+
-- 0023_add_organizations on every database). Two exceptions keep NULL:
|
|
28
|
+
--
|
|
29
|
+
-- - ai_usage_events with operation = 'catalog': a catalogue sync is run by the
|
|
30
|
+
-- deployment, not by an organization, and new ones are written without one.
|
|
31
|
+
-- - audit_events whose action is a SYSTEM action. Everything else (including
|
|
32
|
+
-- an action this migration does not know) belongs to the default
|
|
33
|
+
-- organization. System action prefixes:
|
|
34
|
+
-- system_settings: email_settings: push_config: storage_config:
|
|
35
|
+
-- ai_config: ai_model: ai_catalog: telemetry: support_bundle:
|
|
36
|
+
-- maintenance: db_restore: db_backup: allowlist: users: nodes:
|
|
37
|
+
-- jobs. notification_broadcast. settings. retention.
|
|
38
|
+
--
|
|
39
|
+
-- The backfill runs in an explicit transaction that raises the bypass flag
|
|
40
|
+
-- (the pattern ADR 0002 prescribes for any migration that touches an RLS
|
|
41
|
+
-- table: under FORCE a bare UPDATE would silently change zero rows). The
|
|
42
|
+
-- policies are enabled AFTER it, so on a first apply the flag is belt and
|
|
43
|
+
-- braces; it keeps the pattern in front of the next reader.
|
|
44
|
+
--
|
|
45
|
+
-- THE APPLICATION ROLE MUST BE NOSUPERUSER NOBYPASSRLS, or every policy below is
|
|
46
|
+
-- inert (a superuser sees every row of a FORCEd table). The block at the end
|
|
47
|
+
-- warns when the role applying this migration is not an ordinary role. The
|
|
48
|
+
-- Doctor check `db.rls_role` fails while any FORCEd table exists and the
|
|
49
|
+
-- application role is a superuser or has BYPASSRLS.
|
|
50
|
+
--
|
|
51
|
+
-- DEPLOYMENT NOTE. The statements below take short locks and build the new
|
|
52
|
+
-- indexes inside the migration transaction; on a very large storage_objects or
|
|
53
|
+
-- ai_usage_events table pre-create the indexes with CREATE INDEX CONCURRENTLY
|
|
54
|
+
-- IF NOT EXISTS under the same names before applying.
|
|
55
|
+
-- =============================================================================
|
|
56
|
+
|
|
57
|
+
-- DropForeignKey
|
|
58
|
+
ALTER TABLE "storage_object_chunks" DROP CONSTRAINT "storage_object_chunks_object_id_fkey";
|
|
59
|
+
|
|
60
|
+
-- AlterTable (expand: nullable first, so existing rows can be backfilled)
|
|
61
|
+
ALTER TABLE "storage_objects" ADD COLUMN "org_id" UUID;
|
|
62
|
+
ALTER TABLE "storage_object_chunks" ADD COLUMN "org_id" UUID;
|
|
63
|
+
ALTER TABLE "ai_runs" ADD COLUMN "org_id" UUID;
|
|
64
|
+
ALTER TABLE "ai_usage_events" ADD COLUMN "org_id" UUID;
|
|
65
|
+
ALTER TABLE "audit_events" ADD COLUMN "org_id" UUID;
|
|
66
|
+
|
|
67
|
+
-- Backfill, as system work.
|
|
68
|
+
BEGIN;
|
|
69
|
+
SELECT set_config('app.rls_bypass', 'on', true);
|
|
70
|
+
|
|
71
|
+
UPDATE "storage_objects"
|
|
72
|
+
SET "org_id" = (SELECT "id" FROM "organizations" WHERE "is_default")
|
|
73
|
+
WHERE "org_id" IS NULL;
|
|
74
|
+
|
|
75
|
+
UPDATE "storage_object_chunks" AS c
|
|
76
|
+
SET "org_id" = o."org_id"
|
|
77
|
+
FROM "storage_objects" AS o
|
|
78
|
+
WHERE c."object_id" = o."id" AND c."org_id" IS NULL;
|
|
79
|
+
|
|
80
|
+
UPDATE "ai_runs"
|
|
81
|
+
SET "org_id" = (SELECT "id" FROM "organizations" WHERE "is_default")
|
|
82
|
+
WHERE "org_id" IS NULL;
|
|
83
|
+
|
|
84
|
+
UPDATE "ai_usage_events"
|
|
85
|
+
SET "org_id" = (SELECT "id" FROM "organizations" WHERE "is_default")
|
|
86
|
+
WHERE "org_id" IS NULL AND "operation" <> 'catalog';
|
|
87
|
+
|
|
88
|
+
UPDATE "audit_events"
|
|
89
|
+
SET "org_id" = (SELECT "id" FROM "organizations" WHERE "is_default")
|
|
90
|
+
WHERE "org_id" IS NULL
|
|
91
|
+
AND "action" NOT LIKE 'system\_settings:%'
|
|
92
|
+
AND "action" NOT LIKE 'email\_settings:%'
|
|
93
|
+
AND "action" NOT LIKE 'push\_config:%'
|
|
94
|
+
AND "action" NOT LIKE 'storage\_config:%'
|
|
95
|
+
AND "action" NOT LIKE 'ai\_config:%'
|
|
96
|
+
AND "action" NOT LIKE 'ai\_model:%'
|
|
97
|
+
AND "action" NOT LIKE 'ai\_catalog:%'
|
|
98
|
+
AND "action" NOT LIKE 'telemetry:%'
|
|
99
|
+
AND "action" NOT LIKE 'support\_bundle:%'
|
|
100
|
+
AND "action" NOT LIKE 'maintenance:%'
|
|
101
|
+
AND "action" NOT LIKE 'db\_restore:%'
|
|
102
|
+
AND "action" NOT LIKE 'db\_backup:%'
|
|
103
|
+
AND "action" NOT LIKE 'allowlist:%'
|
|
104
|
+
AND "action" NOT LIKE 'users:%'
|
|
105
|
+
AND "action" NOT LIKE 'nodes:%'
|
|
106
|
+
AND "action" NOT LIKE 'jobs.%'
|
|
107
|
+
AND "action" NOT LIKE 'notification\_broadcast.%'
|
|
108
|
+
AND "action" NOT LIKE 'settings.%'
|
|
109
|
+
AND "action" NOT LIKE 'retention.%';
|
|
110
|
+
COMMIT;
|
|
111
|
+
|
|
112
|
+
-- AlterTable (contract: the three tables whose column is NOT NULL)
|
|
113
|
+
ALTER TABLE "storage_objects" ALTER COLUMN "org_id" SET NOT NULL;
|
|
114
|
+
ALTER TABLE "storage_object_chunks" ALTER COLUMN "org_id" SET NOT NULL;
|
|
115
|
+
ALTER TABLE "ai_runs" ALTER COLUMN "org_id" SET NOT NULL;
|
|
116
|
+
|
|
117
|
+
-- CreateIndex
|
|
118
|
+
CREATE UNIQUE INDEX "storage_objects_id_org_id_key" ON "storage_objects"("id", "org_id");
|
|
119
|
+
|
|
120
|
+
-- CreateIndex
|
|
121
|
+
CREATE INDEX "storage_objects_org_id_uploaded_by_id_created_at_idx" ON "storage_objects"("org_id", "uploaded_by_id", "created_at");
|
|
122
|
+
|
|
123
|
+
-- CreateIndex
|
|
124
|
+
CREATE INDEX "storage_objects_org_id_status_idx" ON "storage_objects"("org_id", "status");
|
|
125
|
+
|
|
126
|
+
-- CreateIndex
|
|
127
|
+
CREATE INDEX "storage_object_chunks_org_id_object_id_idx" ON "storage_object_chunks"("org_id", "object_id");
|
|
128
|
+
|
|
129
|
+
-- CreateIndex
|
|
130
|
+
CREATE INDEX "ai_runs_org_id_user_id_created_at_idx" ON "ai_runs"("org_id", "user_id", "created_at");
|
|
131
|
+
|
|
132
|
+
-- CreateIndex
|
|
133
|
+
CREATE INDEX "ai_usage_events_org_id_user_id_created_at_idx" ON "ai_usage_events"("org_id", "user_id", "created_at");
|
|
134
|
+
|
|
135
|
+
-- CreateIndex
|
|
136
|
+
CREATE INDEX "ai_usage_events_org_id_created_at_idx" ON "ai_usage_events"("org_id", "created_at");
|
|
137
|
+
|
|
138
|
+
-- CreateIndex
|
|
139
|
+
CREATE INDEX "audit_events_org_id_created_at_idx" ON "audit_events"("org_id", "created_at");
|
|
140
|
+
|
|
141
|
+
-- AddForeignKey
|
|
142
|
+
ALTER TABLE "storage_objects" ADD CONSTRAINT "storage_objects_org_id_fkey" FOREIGN KEY ("org_id") REFERENCES "organizations"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
|
143
|
+
|
|
144
|
+
-- AddForeignKey (composite: object_id and org_id together)
|
|
145
|
+
ALTER TABLE "storage_object_chunks" ADD CONSTRAINT "storage_object_chunks_object_id_org_id_fkey" FOREIGN KEY ("object_id", "org_id") REFERENCES "storage_objects"("id", "org_id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
146
|
+
|
|
147
|
+
-- AddForeignKey
|
|
148
|
+
ALTER TABLE "storage_object_chunks" ADD CONSTRAINT "storage_object_chunks_org_id_fkey" FOREIGN KEY ("org_id") REFERENCES "organizations"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
|
149
|
+
|
|
150
|
+
-- AddForeignKey
|
|
151
|
+
ALTER TABLE "ai_runs" ADD CONSTRAINT "ai_runs_org_id_fkey" FOREIGN KEY ("org_id") REFERENCES "organizations"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
|
152
|
+
|
|
153
|
+
-- AddForeignKey
|
|
154
|
+
ALTER TABLE "ai_usage_events" ADD CONSTRAINT "ai_usage_events_org_id_fkey" FOREIGN KEY ("org_id") REFERENCES "organizations"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
|
155
|
+
|
|
156
|
+
-- AddForeignKey
|
|
157
|
+
ALTER TABLE "audit_events" ADD CONSTRAINT "audit_events_org_id_fkey" FOREIGN KEY ("org_id") REFERENCES "organizations"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
|
158
|
+
|
|
159
|
+
-- =============================================================================
|
|
160
|
+
-- Row-level security. Prisma's schema language cannot express any of this and
|
|
161
|
+
-- its diff ignores it; the policy names are listed in RLS_POLICIES
|
|
162
|
+
-- (packages/platform-db/src/drift/rls-policies.ts) and asserted against
|
|
163
|
+
-- pg_policies by the drift test.
|
|
164
|
+
--
|
|
165
|
+
-- NULLIF(..., '') is required: current_setting(name, true) is NULL on a fresh
|
|
166
|
+
-- connection but the EMPTY STRING on a connection that has ever held a
|
|
167
|
+
-- transaction-local value, and a bare ''::uuid cast errors.
|
|
168
|
+
-- FORCE applies the policy to the table owner, which is the application role:
|
|
169
|
+
-- unscoped code sees no rows and cannot insert (it fails closed).
|
|
170
|
+
-- =============================================================================
|
|
171
|
+
|
|
172
|
+
ALTER TABLE "storage_objects" ENABLE ROW LEVEL SECURITY;
|
|
173
|
+
ALTER TABLE "storage_objects" FORCE ROW LEVEL SECURITY;
|
|
174
|
+
CREATE POLICY "storage_objects_org_isolation" ON "storage_objects"
|
|
175
|
+
USING ("org_id" = NULLIF(current_setting('app.org_id', true), '')::uuid
|
|
176
|
+
OR current_setting('app.rls_bypass', true) = 'on')
|
|
177
|
+
WITH CHECK ("org_id" = NULLIF(current_setting('app.org_id', true), '')::uuid
|
|
178
|
+
OR current_setting('app.rls_bypass', true) = 'on');
|
|
179
|
+
|
|
180
|
+
ALTER TABLE "storage_object_chunks" ENABLE ROW LEVEL SECURITY;
|
|
181
|
+
ALTER TABLE "storage_object_chunks" FORCE ROW LEVEL SECURITY;
|
|
182
|
+
CREATE POLICY "storage_object_chunks_org_isolation" ON "storage_object_chunks"
|
|
183
|
+
USING ("org_id" = NULLIF(current_setting('app.org_id', true), '')::uuid
|
|
184
|
+
OR current_setting('app.rls_bypass', true) = 'on')
|
|
185
|
+
WITH CHECK ("org_id" = NULLIF(current_setting('app.org_id', true), '')::uuid
|
|
186
|
+
OR current_setting('app.rls_bypass', true) = 'on');
|
|
187
|
+
|
|
188
|
+
ALTER TABLE "ai_runs" ENABLE ROW LEVEL SECURITY;
|
|
189
|
+
ALTER TABLE "ai_runs" FORCE ROW LEVEL SECURITY;
|
|
190
|
+
CREATE POLICY "ai_runs_org_isolation" ON "ai_runs"
|
|
191
|
+
USING ("org_id" = NULLIF(current_setting('app.org_id', true), '')::uuid
|
|
192
|
+
OR current_setting('app.rls_bypass', true) = 'on')
|
|
193
|
+
WITH CHECK ("org_id" = NULLIF(current_setting('app.org_id', true), '')::uuid
|
|
194
|
+
OR current_setting('app.rls_bypass', true) = 'on');
|
|
195
|
+
|
|
196
|
+
ALTER TABLE "ai_usage_events" ENABLE ROW LEVEL SECURITY;
|
|
197
|
+
ALTER TABLE "ai_usage_events" FORCE ROW LEVEL SECURITY;
|
|
198
|
+
CREATE POLICY "ai_usage_events_org_isolation" ON "ai_usage_events"
|
|
199
|
+
USING ("org_id" = NULLIF(current_setting('app.org_id', true), '')::uuid
|
|
200
|
+
OR current_setting('app.rls_bypass', true) = 'on')
|
|
201
|
+
WITH CHECK ("org_id" = NULLIF(current_setting('app.org_id', true), '')::uuid
|
|
202
|
+
OR current_setting('app.rls_bypass', true) = 'on');
|
|
203
|
+
|
|
204
|
+
-- A policy is inert for a superuser or a BYPASSRLS role. Warn (do not fail:
|
|
205
|
+
-- the migration role may legitimately differ from the application role).
|
|
206
|
+
DO $$
|
|
207
|
+
DECLARE
|
|
208
|
+
inert boolean;
|
|
209
|
+
BEGIN
|
|
210
|
+
SELECT rolsuper OR rolbypassrls INTO inert FROM pg_roles WHERE rolname = current_user;
|
|
211
|
+
IF inert THEN
|
|
212
|
+
RAISE WARNING 'row-level security is INERT for role "%": it is a superuser or has BYPASSRLS. Run the application as an ordinary role (NOSUPERUSER NOBYPASSRLS) or tenant isolation is not enforced.', current_user;
|
|
213
|
+
END IF;
|
|
214
|
+
END
|
|
215
|
+
$$;
|