@automagik/omni 2.260723.1 → 2.260727.1

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.
@@ -0,0 +1,435 @@
1
+ -- Multitenancy control plane — additive foundation (wish: omni-full-multitenancy, Group G1).
2
+ --
3
+ -- Establishes first-class tenant identity, principals/memberships, a bounded
4
+ -- fixed-role registry, an ISOLATED authentication credential index, separate
5
+ -- platform credentials, tenant key lineage/delegation foundations, and split
6
+ -- tenant/platform audit stores. Frozen G0 contract: ADR-0001/0003/0005/0006/0010
7
+ -- and OWNERSHIP_MATRIX "New ownership/control tables".
8
+ --
9
+ -- This migration is purely additive:
10
+ -- * It does NOT touch legacy `api_keys` or any existing business table.
11
+ -- * It adds NO tenant_id to existing tables (that is Group G2).
12
+ -- * It adds NO RLS policies or runtime roles (that is Group G3).
13
+ --
14
+ -- No hard tenant delete: every foreign key referencing `tenants` uses
15
+ -- ON DELETE RESTRICT (or NO ACTION for deferred/self references) so nothing can
16
+ -- cascade-delete a tenant or erase security/audit lineage. Tenant lifecycle
17
+ -- ends at `archived`.
18
+ --
19
+ -- Hand-written per the established convention (see 0032_genie_hosts.sql):
20
+ -- CREATE TABLE IF NOT EXISTS + explicit constraints, journal entry only, no
21
+ -- drizzle snapshot. Idempotent so a partial apply can be safely re-run.
22
+
23
+ -- ---------------------------------------------------------------------------
24
+ -- tenants — platform control plane
25
+ -- ---------------------------------------------------------------------------
26
+ CREATE TABLE IF NOT EXISTS "tenants" (
27
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
28
+ "slug" varchar(63) NOT NULL,
29
+ "display_name" varchar(255) NOT NULL,
30
+ "status" varchar(20) DEFAULT 'active' NOT NULL,
31
+ "policy_version" integer DEFAULT 1 NOT NULL,
32
+ "revocation_epoch" integer DEFAULT 0 NOT NULL,
33
+ "max_key_ttl_seconds" integer NOT NULL,
34
+ "max_key_rate_limit" integer NOT NULL,
35
+ "max_key_budget" integer NOT NULL,
36
+ "created_by_principal_id" uuid,
37
+ "created_at" timestamp with time zone DEFAULT now() NOT NULL,
38
+ "updated_at" timestamp with time zone DEFAULT now() NOT NULL,
39
+ "suspended_at" timestamp with time zone,
40
+ "archived_at" timestamp with time zone,
41
+ CONSTRAINT "tenants_status_check" CHECK ("status" IN ('active', 'suspended', 'archived')),
42
+ CONSTRAINT "tenants_slug_format_check" CHECK ("slug" ~ '^[a-z0-9][a-z0-9-]*$'),
43
+ CONSTRAINT "tenants_epochs_check" CHECK ("policy_version" >= 1 AND "revocation_epoch" >= 0),
44
+ CONSTRAINT "tenants_key_policy_check" CHECK ("max_key_ttl_seconds" > 0 AND "max_key_rate_limit" > 0 AND "max_key_budget" > 0)
45
+ );
46
+ --> statement-breakpoint
47
+ CREATE UNIQUE INDEX IF NOT EXISTS "tenants_slug_uq" ON "tenants" ("slug");
48
+ --> statement-breakpoint
49
+ CREATE INDEX IF NOT EXISTS "tenants_status_idx" ON "tenants" ("status");
50
+ --> statement-breakpoint
51
+
52
+ -- Hard deletion is forbidden even for an otherwise-unreferenced tenant. The
53
+ -- lifecycle terminates at archived; future purge workflows require a separate,
54
+ -- explicitly approved model rather than DELETE.
55
+ CREATE OR REPLACE FUNCTION "reject_tenant_hard_delete"()
56
+ RETURNS trigger
57
+ LANGUAGE plpgsql
58
+ AS $$
59
+ BEGIN
60
+ RAISE EXCEPTION 'tenant hard delete is forbidden';
61
+ END;
62
+ $$;
63
+ --> statement-breakpoint
64
+ DROP TRIGGER IF EXISTS "tenants_no_hard_delete" ON "tenants";
65
+ --> statement-breakpoint
66
+ CREATE TRIGGER "tenants_no_hard_delete"
67
+ BEFORE DELETE ON "tenants"
68
+ FOR EACH ROW EXECUTE FUNCTION "reject_tenant_hard_delete"();
69
+ --> statement-breakpoint
70
+
71
+ -- ---------------------------------------------------------------------------
72
+ -- principals — platform identity plane
73
+ -- ---------------------------------------------------------------------------
74
+ CREATE TABLE IF NOT EXISTS "principals" (
75
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
76
+ "type" varchar(20) NOT NULL,
77
+ "subject" varchar(255) NOT NULL,
78
+ "display_name" varchar(255),
79
+ "status" varchar(20) DEFAULT 'active' NOT NULL,
80
+ "created_at" timestamp with time zone DEFAULT now() NOT NULL,
81
+ "updated_at" timestamp with time zone DEFAULT now() NOT NULL,
82
+ "disabled_at" timestamp with time zone,
83
+ CONSTRAINT "principals_type_check" CHECK ("type" IN ('human', 'service')),
84
+ CONSTRAINT "principals_status_check" CHECK ("status" IN ('active', 'disabled'))
85
+ );
86
+ --> statement-breakpoint
87
+ CREATE UNIQUE INDEX IF NOT EXISTS "principals_subject_uq" ON "principals" ("subject");
88
+ --> statement-breakpoint
89
+ CREATE INDEX IF NOT EXISTS "principals_status_idx" ON "principals" ("status");
90
+ --> statement-breakpoint
91
+
92
+ -- Deferred FK: tenants.created_by_principal_id -> principals.id (RESTRICT).
93
+ DO $$ BEGIN
94
+ ALTER TABLE "tenants" ADD CONSTRAINT "tenants_created_by_principal_id_fk"
95
+ FOREIGN KEY ("created_by_principal_id") REFERENCES "principals" ("id") ON DELETE RESTRICT;
96
+ EXCEPTION WHEN duplicate_object THEN NULL;
97
+ END $$;
98
+ --> statement-breakpoint
99
+
100
+ -- ---------------------------------------------------------------------------
101
+ -- tenant_memberships — principal <-> tenant role relation
102
+ -- ---------------------------------------------------------------------------
103
+ CREATE TABLE IF NOT EXISTS "tenant_memberships" (
104
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
105
+ "tenant_id" uuid NOT NULL,
106
+ "principal_id" uuid NOT NULL,
107
+ "role" varchar(32) NOT NULL,
108
+ "status" varchar(20) DEFAULT 'active' NOT NULL,
109
+ "invited_by_principal_id" uuid,
110
+ "created_at" timestamp with time zone DEFAULT now() NOT NULL,
111
+ "updated_at" timestamp with time zone DEFAULT now() NOT NULL,
112
+ "disabled_at" timestamp with time zone,
113
+ CONSTRAINT "tenant_memberships_role_check" CHECK ("role" IN ('tenant-owner', 'tenant-admin', 'tenant-operator', 'tenant-viewer')),
114
+ CONSTRAINT "tenant_memberships_status_check" CHECK ("status" IN ('active', 'disabled')),
115
+ CONSTRAINT "tenant_memberships_tenant_id_fk" FOREIGN KEY ("tenant_id") REFERENCES "tenants" ("id") ON DELETE RESTRICT,
116
+ CONSTRAINT "tenant_memberships_principal_id_fk" FOREIGN KEY ("principal_id") REFERENCES "principals" ("id") ON DELETE RESTRICT,
117
+ CONSTRAINT "tenant_memberships_invited_by_principal_id_fk" FOREIGN KEY ("invited_by_principal_id") REFERENCES "principals" ("id") ON DELETE RESTRICT
118
+ );
119
+ --> statement-breakpoint
120
+ CREATE UNIQUE INDEX IF NOT EXISTS "tenant_memberships_tenant_principal_uq" ON "tenant_memberships" ("tenant_id", "principal_id");
121
+ --> statement-breakpoint
122
+ CREATE UNIQUE INDEX IF NOT EXISTS "tenant_memberships_tenant_id_uq" ON "tenant_memberships" ("tenant_id", "id");
123
+ --> statement-breakpoint
124
+ CREATE UNIQUE INDEX IF NOT EXISTS "tenant_memberships_tenant_principal_id_uq" ON "tenant_memberships" ("tenant_id", "principal_id", "id");
125
+ --> statement-breakpoint
126
+ CREATE INDEX IF NOT EXISTS "tenant_memberships_tenant_idx" ON "tenant_memberships" ("tenant_id");
127
+ --> statement-breakpoint
128
+ CREATE INDEX IF NOT EXISTS "tenant_memberships_principal_idx" ON "tenant_memberships" ("principal_id");
129
+ --> statement-breakpoint
130
+
131
+ -- ---------------------------------------------------------------------------
132
+ -- tenant_role_policies — fixed, bounded role ceiling registry (seed data)
133
+ -- ---------------------------------------------------------------------------
134
+ CREATE TABLE IF NOT EXISTS "tenant_role_policies" (
135
+ "role" varchar(32) PRIMARY KEY NOT NULL,
136
+ "description" text NOT NULL,
137
+ "max_scopes" text[] NOT NULL,
138
+ "can_manage_memberships" boolean NOT NULL,
139
+ "can_delegate_keys" boolean NOT NULL,
140
+ "max_delegation_depth" integer DEFAULT 1 NOT NULL,
141
+ "created_at" timestamp with time zone DEFAULT now() NOT NULL,
142
+ CONSTRAINT "tenant_role_policies_role_check" CHECK ("role" IN ('tenant-owner', 'tenant-admin', 'tenant-operator', 'tenant-viewer')),
143
+ CONSTRAINT "tenant_role_policies_depth_check" CHECK ("max_delegation_depth" >= 0),
144
+ CONSTRAINT "tenant_role_policies_no_platform_authority_check" CHECK (
145
+ cardinality("max_scopes") > 0
146
+ AND array_position("max_scopes", NULL) IS NULL
147
+ AND NOT ('*' = ANY("max_scopes"))
148
+ AND array_to_string("max_scopes", ',') !~ '(^|,)platform:'
149
+ ),
150
+ CONSTRAINT "tenant_role_policies_fixed_ceiling_check" CHECK (
151
+ ("role" = 'tenant-owner' AND "max_scopes" = ARRAY['tenant:*', 'keys:delegate']::text[] AND "can_manage_memberships" AND "can_delegate_keys" AND "max_delegation_depth" = 1)
152
+ OR ("role" = 'tenant-admin' AND "max_scopes" = ARRAY['tenant:*', 'keys:delegate']::text[] AND "can_manage_memberships" AND "can_delegate_keys" AND "max_delegation_depth" = 1)
153
+ OR ("role" = 'tenant-operator' AND "max_scopes" = ARRAY['tenant:read', 'tenant:write']::text[] AND NOT "can_manage_memberships" AND NOT "can_delegate_keys" AND "max_delegation_depth" = 0)
154
+ OR ("role" = 'tenant-viewer' AND "max_scopes" = ARRAY['tenant:read']::text[] AND NOT "can_manage_memberships" AND NOT "can_delegate_keys" AND "max_delegation_depth" = 0)
155
+ )
156
+ );
157
+ --> statement-breakpoint
158
+
159
+ -- Seed the four fixed roles with bounded ceilings. No role carries '*'.
160
+ INSERT INTO "tenant_role_policies" ("role", "description", "max_scopes", "can_manage_memberships", "can_delegate_keys", "max_delegation_depth")
161
+ VALUES
162
+ ('tenant-owner', 'Membership/lifecycle authority inside the tenant; cannot create platform authority.', ARRAY['tenant:*', 'keys:delegate']::text[], true, true, 1),
163
+ ('tenant-admin', 'Full tenant resource administration and bounded delegation.', ARRAY['tenant:*', 'keys:delegate']::text[], true, true, 1),
164
+ ('tenant-operator', 'Operational write access without membership/key-policy administration.', ARRAY['tenant:read', 'tenant:write']::text[], false, false, 0),
165
+ ('tenant-viewer', 'Read-only tenant access.', ARRAY['tenant:read']::text[], false, false, 0)
166
+ ON CONFLICT ("role") DO UPDATE SET
167
+ "description" = EXCLUDED."description",
168
+ "max_scopes" = EXCLUDED."max_scopes",
169
+ "can_manage_memberships" = EXCLUDED."can_manage_memberships",
170
+ "can_delegate_keys" = EXCLUDED."can_delegate_keys",
171
+ "max_delegation_depth" = EXCLUDED."max_delegation_depth";
172
+ --> statement-breakpoint
173
+
174
+ -- ---------------------------------------------------------------------------
175
+ -- platform_api_keys — platform credential class (break-glass / automation)
176
+ -- ---------------------------------------------------------------------------
177
+ CREATE TABLE IF NOT EXISTS "platform_api_keys" (
178
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
179
+ "name" varchar(255) NOT NULL,
180
+ "description" text,
181
+ "key_prefix" varchar(12) NOT NULL,
182
+ "key_hash" varchar(64) NOT NULL,
183
+ "scopes" text[] NOT NULL,
184
+ "status" varchar(20) DEFAULT 'active' NOT NULL,
185
+ "principal_id" uuid NOT NULL,
186
+ "created_by_principal_id" uuid,
187
+ "expires_at" timestamp with time zone,
188
+ "last_used_at" timestamp with time zone,
189
+ "revoked_at" timestamp with time zone,
190
+ "revoked_by" varchar(255),
191
+ "revoke_reason" text,
192
+ "created_at" timestamp with time zone DEFAULT now() NOT NULL,
193
+ "updated_at" timestamp with time zone DEFAULT now() NOT NULL,
194
+ CONSTRAINT "platform_api_keys_status_check" CHECK ("status" IN ('active', 'revoked')),
195
+ CONSTRAINT "platform_api_keys_principal_id_fk" FOREIGN KEY ("principal_id") REFERENCES "principals" ("id") ON DELETE RESTRICT,
196
+ CONSTRAINT "platform_api_keys_created_by_principal_id_fk" FOREIGN KEY ("created_by_principal_id") REFERENCES "principals" ("id") ON DELETE RESTRICT
197
+ );
198
+ --> statement-breakpoint
199
+ CREATE UNIQUE INDEX IF NOT EXISTS "platform_api_keys_name_uq" ON "platform_api_keys" ("name");
200
+ --> statement-breakpoint
201
+ CREATE UNIQUE INDEX IF NOT EXISTS "platform_api_keys_key_hash_uq" ON "platform_api_keys" ("key_hash");
202
+ --> statement-breakpoint
203
+ CREATE UNIQUE INDEX IF NOT EXISTS "platform_api_keys_id_principal_uq" ON "platform_api_keys" ("id", "principal_id");
204
+ --> statement-breakpoint
205
+ CREATE INDEX IF NOT EXISTS "platform_api_keys_key_prefix_idx" ON "platform_api_keys" ("key_prefix");
206
+ --> statement-breakpoint
207
+ CREATE INDEX IF NOT EXISTS "platform_api_keys_status_idx" ON "platform_api_keys" ("status");
208
+ --> statement-breakpoint
209
+
210
+ -- ---------------------------------------------------------------------------
211
+ -- tenant_key_lineage — tenant-visible key metadata + delegation lineage
212
+ -- ---------------------------------------------------------------------------
213
+ CREATE TABLE IF NOT EXISTS "tenant_key_lineage" (
214
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
215
+ "tenant_id" uuid NOT NULL,
216
+ "principal_id" uuid NOT NULL,
217
+ "membership_id" uuid NOT NULL,
218
+ "actor_role" varchar(32) NOT NULL,
219
+ "name" varchar(255) NOT NULL,
220
+ "key_prefix" varchar(12) NOT NULL,
221
+ "scopes" text[] NOT NULL,
222
+ "resource_constraints" jsonb DEFAULT '{}'::jsonb NOT NULL,
223
+ "status" varchar(20) DEFAULT 'active' NOT NULL,
224
+ "parent_key_id" uuid,
225
+ "root_key_id" uuid NOT NULL,
226
+ "depth" integer DEFAULT 0 NOT NULL,
227
+ "created_by_principal_id" uuid,
228
+ "expires_at" timestamp with time zone,
229
+ "rate_limit" integer,
230
+ "budget" integer,
231
+ "revoked_at" timestamp with time zone,
232
+ "revoke_reason" text,
233
+ "revocation_epoch" integer DEFAULT 0 NOT NULL,
234
+ "ancestor_revoked" boolean DEFAULT false NOT NULL,
235
+ "ceiling_snapshot" jsonb DEFAULT '{}'::jsonb NOT NULL,
236
+ "created_at" timestamp with time zone DEFAULT now() NOT NULL,
237
+ "updated_at" timestamp with time zone DEFAULT now() NOT NULL,
238
+ CONSTRAINT "tenant_key_lineage_role_check" CHECK ("actor_role" IN ('tenant-owner', 'tenant-admin', 'tenant-operator', 'tenant-viewer')),
239
+ CONSTRAINT "tenant_key_lineage_status_check" CHECK ("status" IN ('active', 'revoked', 'expired')),
240
+ CONSTRAINT "tenant_key_lineage_depth_check" CHECK ("depth" >= 0),
241
+ CONSTRAINT "tenant_key_lineage_positive_limits_check" CHECK (("rate_limit" IS NULL OR "rate_limit" > 0) AND ("budget" IS NULL OR "budget" > 0)),
242
+ CONSTRAINT "tenant_key_lineage_principal_membership_pair_check" CHECK (("principal_id" IS NULL AND "membership_id" IS NULL) OR ("principal_id" IS NOT NULL AND "membership_id" IS NOT NULL)),
243
+ CONSTRAINT "tenant_key_lineage_depth_shape_check" CHECK (("depth" = 0 AND "parent_key_id" IS NULL AND "root_key_id" = "id") OR ("depth" = 1 AND "parent_key_id" IS NOT NULL)),
244
+ CONSTRAINT "tenant_key_lineage_no_platform_authority_check" CHECK (
245
+ cardinality("scopes") > 0
246
+ AND array_position("scopes", NULL) IS NULL
247
+ AND NOT ('*' = ANY("scopes"))
248
+ AND array_to_string("scopes", ',') !~ '(^|,)platform:'
249
+ ),
250
+ CONSTRAINT "tenant_key_lineage_tenant_id_fk" FOREIGN KEY ("tenant_id") REFERENCES "tenants" ("id") ON DELETE RESTRICT,
251
+ CONSTRAINT "tenant_key_lineage_principal_id_fk" FOREIGN KEY ("principal_id") REFERENCES "principals" ("id") ON DELETE RESTRICT,
252
+ CONSTRAINT "tenant_key_lineage_membership_id_fk" FOREIGN KEY ("membership_id") REFERENCES "tenant_memberships" ("id") ON DELETE RESTRICT,
253
+ CONSTRAINT "tenant_key_lineage_membership_principal_fk" FOREIGN KEY ("tenant_id", "principal_id", "membership_id") REFERENCES "tenant_memberships" ("tenant_id", "principal_id", "id") ON DELETE RESTRICT,
254
+ CONSTRAINT "tenant_key_lineage_parent_key_id_fk" FOREIGN KEY ("parent_key_id") REFERENCES "tenant_key_lineage" ("id") ON DELETE RESTRICT,
255
+ CONSTRAINT "tenant_key_lineage_created_by_principal_id_fk" FOREIGN KEY ("created_by_principal_id") REFERENCES "principals" ("id") ON DELETE RESTRICT
256
+ );
257
+ --> statement-breakpoint
258
+ CREATE UNIQUE INDEX IF NOT EXISTS "tenant_key_lineage_tenant_id_uq" ON "tenant_key_lineage" ("tenant_id", "id");
259
+ --> statement-breakpoint
260
+ CREATE UNIQUE INDEX IF NOT EXISTS "tenant_key_lineage_auth_binding_uq" ON "tenant_key_lineage" ("tenant_id", "id", "principal_id", "membership_id", "actor_role");
261
+ --> statement-breakpoint
262
+ CREATE INDEX IF NOT EXISTS "tenant_key_lineage_tenant_idx" ON "tenant_key_lineage" ("tenant_id");
263
+ --> statement-breakpoint
264
+ CREATE INDEX IF NOT EXISTS "tenant_key_lineage_parent_idx" ON "tenant_key_lineage" ("parent_key_id");
265
+ --> statement-breakpoint
266
+ CREATE INDEX IF NOT EXISTS "tenant_key_lineage_root_idx" ON "tenant_key_lineage" ("root_key_id");
267
+ --> statement-breakpoint
268
+ CREATE INDEX IF NOT EXISTS "tenant_key_lineage_status_idx" ON "tenant_key_lineage" ("status");
269
+ --> statement-breakpoint
270
+
271
+ DO $$ BEGIN
272
+ ALTER TABLE "tenant_key_lineage" ADD CONSTRAINT "tenant_key_lineage_parent_tenant_fk"
273
+ FOREIGN KEY ("tenant_id", "parent_key_id") REFERENCES "tenant_key_lineage" ("tenant_id", "id") ON DELETE RESTRICT;
274
+ EXCEPTION WHEN duplicate_object THEN NULL;
275
+ END $$;
276
+ --> statement-breakpoint
277
+ DO $$ BEGIN
278
+ ALTER TABLE "tenant_key_lineage" ADD CONSTRAINT "tenant_key_lineage_root_tenant_fk"
279
+ FOREIGN KEY ("tenant_id", "root_key_id") REFERENCES "tenant_key_lineage" ("tenant_id", "id") ON DELETE RESTRICT;
280
+ EXCEPTION WHEN duplicate_object THEN NULL;
281
+ END $$;
282
+ --> statement-breakpoint
283
+
284
+ -- ---------------------------------------------------------------------------
285
+ -- auth_credentials — ISOLATED authentication index (platform-owned)
286
+ -- ---------------------------------------------------------------------------
287
+ CREATE TABLE IF NOT EXISTS "auth_credentials" (
288
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
289
+ "credential_class" varchar(20) NOT NULL,
290
+ "key_hash" varchar(64) NOT NULL,
291
+ "key_prefix" varchar(12) NOT NULL,
292
+ "tenant_id" uuid,
293
+ "principal_id" uuid,
294
+ "membership_id" uuid,
295
+ "actor_role" varchar(32),
296
+ "scopes" text[] NOT NULL,
297
+ "status" varchar(20) DEFAULT 'active' NOT NULL,
298
+ "tenant_key_lineage_id" uuid,
299
+ "platform_api_key_id" uuid,
300
+ "policy_snapshot_version" integer DEFAULT 1 NOT NULL,
301
+ "revocation_epoch_snapshot" integer DEFAULT 0 NOT NULL,
302
+ "expires_at" timestamp with time zone,
303
+ "revoked_at" timestamp with time zone,
304
+ "created_at" timestamp with time zone DEFAULT now() NOT NULL,
305
+ "updated_at" timestamp with time zone DEFAULT now() NOT NULL,
306
+ CONSTRAINT "auth_credentials_class_check" CHECK ("credential_class" IN ('tenant', 'platform')),
307
+ CONSTRAINT "auth_credentials_status_check" CHECK ("status" IN ('active', 'revoked', 'expired')),
308
+ CONSTRAINT "auth_credentials_class_separation_check" CHECK (
309
+ (
310
+ "credential_class" = 'tenant'
311
+ AND "tenant_id" IS NOT NULL
312
+ AND "tenant_key_lineage_id" IS NOT NULL
313
+ AND "actor_role" IS NOT NULL
314
+ AND "principal_id" IS NOT NULL
315
+ AND "membership_id" IS NOT NULL
316
+ AND "platform_api_key_id" IS NULL
317
+ ) OR (
318
+ "credential_class" = 'platform'
319
+ AND "tenant_id" IS NULL
320
+ AND "platform_api_key_id" IS NOT NULL
321
+ AND "principal_id" IS NOT NULL
322
+ AND "membership_id" IS NULL
323
+ AND "tenant_key_lineage_id" IS NULL
324
+ AND "actor_role" IS NULL
325
+ )
326
+ ),
327
+ CONSTRAINT "auth_credentials_principal_membership_pair_check" CHECK ("credential_class" <> 'tenant' OR (("principal_id" IS NULL AND "membership_id" IS NULL) OR ("principal_id" IS NOT NULL AND "membership_id" IS NOT NULL))),
328
+ CONSTRAINT "auth_credentials_tenant_no_wildcard_check" CHECK (
329
+ "credential_class" <> 'tenant' OR (
330
+ cardinality("scopes") > 0
331
+ AND array_position("scopes", NULL) IS NULL
332
+ AND NOT ('*' = ANY("scopes"))
333
+ AND array_to_string("scopes", ',') !~ '(^|,)platform:'
334
+ )
335
+ ),
336
+ CONSTRAINT "auth_credentials_tenant_id_fk" FOREIGN KEY ("tenant_id") REFERENCES "tenants" ("id") ON DELETE RESTRICT,
337
+ CONSTRAINT "auth_credentials_principal_id_fk" FOREIGN KEY ("principal_id") REFERENCES "principals" ("id") ON DELETE RESTRICT,
338
+ CONSTRAINT "auth_credentials_membership_id_fk" FOREIGN KEY ("membership_id") REFERENCES "tenant_memberships" ("id") ON DELETE RESTRICT,
339
+ CONSTRAINT "auth_credentials_tenant_key_lineage_id_fk" FOREIGN KEY ("tenant_key_lineage_id") REFERENCES "tenant_key_lineage" ("id") ON DELETE RESTRICT,
340
+ CONSTRAINT "auth_credentials_platform_api_key_id_fk" FOREIGN KEY ("platform_api_key_id") REFERENCES "platform_api_keys" ("id") ON DELETE RESTRICT,
341
+ CONSTRAINT "auth_credentials_tenant_lineage_fk" FOREIGN KEY ("tenant_id", "tenant_key_lineage_id") REFERENCES "tenant_key_lineage" ("tenant_id", "id") ON DELETE RESTRICT,
342
+ CONSTRAINT "auth_credentials_tenant_lineage_binding_fk" FOREIGN KEY ("tenant_id", "tenant_key_lineage_id", "principal_id", "membership_id", "actor_role") REFERENCES "tenant_key_lineage" ("tenant_id", "id", "principal_id", "membership_id", "actor_role") ON DELETE RESTRICT,
343
+ CONSTRAINT "auth_credentials_membership_principal_fk" FOREIGN KEY ("tenant_id", "principal_id", "membership_id") REFERENCES "tenant_memberships" ("tenant_id", "principal_id", "id") ON DELETE RESTRICT,
344
+ CONSTRAINT "auth_credentials_platform_source_principal_fk" FOREIGN KEY ("platform_api_key_id", "principal_id") REFERENCES "platform_api_keys" ("id", "principal_id") ON DELETE RESTRICT
345
+ );
346
+ --> statement-breakpoint
347
+ CREATE UNIQUE INDEX IF NOT EXISTS "auth_credentials_key_hash_uq" ON "auth_credentials" ("key_hash");
348
+ --> statement-breakpoint
349
+ CREATE UNIQUE INDEX IF NOT EXISTS "auth_credentials_tenant_lineage_uq" ON "auth_credentials" ("tenant_key_lineage_id");
350
+ --> statement-breakpoint
351
+ CREATE UNIQUE INDEX IF NOT EXISTS "auth_credentials_platform_source_uq" ON "auth_credentials" ("platform_api_key_id");
352
+ --> statement-breakpoint
353
+ CREATE INDEX IF NOT EXISTS "auth_credentials_class_tenant_idx" ON "auth_credentials" ("credential_class", "tenant_id");
354
+ --> statement-breakpoint
355
+
356
+ -- ---------------------------------------------------------------------------
357
+ -- tenant_audit_logs — append-only, tenant-scoped audit store
358
+ -- ---------------------------------------------------------------------------
359
+ CREATE TABLE IF NOT EXISTS "tenant_audit_logs" (
360
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
361
+ "tenant_id" uuid NOT NULL,
362
+ "actor_principal_id" uuid,
363
+ "actor_credential_id" uuid NOT NULL,
364
+ "action" varchar(100) NOT NULL,
365
+ "target_type" varchar(100),
366
+ "target_id" varchar(255),
367
+ "request_id" varchar(100) NOT NULL,
368
+ "metadata" jsonb DEFAULT '{}'::jsonb NOT NULL,
369
+ "created_at" timestamp with time zone DEFAULT now() NOT NULL,
370
+ CONSTRAINT "tenant_audit_logs_tenant_id_fk" FOREIGN KEY ("tenant_id") REFERENCES "tenants" ("id") ON DELETE RESTRICT,
371
+ CONSTRAINT "tenant_audit_logs_actor_principal_id_fk" FOREIGN KEY ("actor_principal_id") REFERENCES "principals" ("id") ON DELETE RESTRICT
372
+ );
373
+ --> statement-breakpoint
374
+ CREATE INDEX IF NOT EXISTS "tenant_audit_logs_tenant_idx" ON "tenant_audit_logs" ("tenant_id");
375
+ --> statement-breakpoint
376
+ CREATE INDEX IF NOT EXISTS "tenant_audit_logs_created_at_idx" ON "tenant_audit_logs" ("created_at");
377
+ --> statement-breakpoint
378
+
379
+ -- ---------------------------------------------------------------------------
380
+ -- platform_audit_logs — append-only platform-admin audit store
381
+ -- ---------------------------------------------------------------------------
382
+ CREATE TABLE IF NOT EXISTS "platform_audit_logs" (
383
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
384
+ "actor_principal_id" uuid,
385
+ "actor_credential_id" uuid NOT NULL,
386
+ "action" varchar(100) NOT NULL,
387
+ "target_tenant_id" uuid,
388
+ "reason" text NOT NULL,
389
+ "request_id" varchar(100) NOT NULL,
390
+ "before_metadata" jsonb,
391
+ "after_metadata" jsonb,
392
+ "created_at" timestamp with time zone DEFAULT now() NOT NULL,
393
+ CONSTRAINT "platform_audit_logs_target_shape_check" CHECK ("target_tenant_id" IS NOT NULL OR "action" = 'tenant.list'),
394
+ CONSTRAINT "platform_audit_logs_actor_principal_id_fk" FOREIGN KEY ("actor_principal_id") REFERENCES "principals" ("id") ON DELETE RESTRICT,
395
+ CONSTRAINT "platform_audit_logs_target_tenant_id_fk" FOREIGN KEY ("target_tenant_id") REFERENCES "tenants" ("id") ON DELETE RESTRICT
396
+ );
397
+ --> statement-breakpoint
398
+ CREATE INDEX IF NOT EXISTS "platform_audit_logs_target_tenant_idx" ON "platform_audit_logs" ("target_tenant_id");
399
+ --> statement-breakpoint
400
+ CREATE INDEX IF NOT EXISTS "platform_audit_logs_created_at_idx" ON "platform_audit_logs" ("created_at");
401
+ --> statement-breakpoint
402
+
403
+ -- Enforce append-only audit semantics in the database, independent of application code.
404
+ CREATE OR REPLACE FUNCTION "reject_audit_log_mutation"()
405
+ RETURNS trigger
406
+ LANGUAGE plpgsql
407
+ AS $$
408
+ BEGIN
409
+ RAISE EXCEPTION 'multitenancy audit logs are append-only';
410
+ END;
411
+ $$;
412
+ --> statement-breakpoint
413
+ DROP TRIGGER IF EXISTS "tenant_audit_logs_append_only" ON "tenant_audit_logs";
414
+ --> statement-breakpoint
415
+ CREATE TRIGGER "tenant_audit_logs_append_only"
416
+ BEFORE UPDATE OR DELETE ON "tenant_audit_logs"
417
+ FOR EACH ROW EXECUTE FUNCTION "reject_audit_log_mutation"();
418
+ --> statement-breakpoint
419
+ DROP TRIGGER IF EXISTS "tenant_audit_logs_no_truncate" ON "tenant_audit_logs";
420
+ --> statement-breakpoint
421
+ CREATE TRIGGER "tenant_audit_logs_no_truncate"
422
+ BEFORE TRUNCATE ON "tenant_audit_logs"
423
+ FOR EACH STATEMENT EXECUTE FUNCTION "reject_audit_log_mutation"();
424
+ --> statement-breakpoint
425
+ DROP TRIGGER IF EXISTS "platform_audit_logs_append_only" ON "platform_audit_logs";
426
+ --> statement-breakpoint
427
+ CREATE TRIGGER "platform_audit_logs_append_only"
428
+ BEFORE UPDATE OR DELETE ON "platform_audit_logs"
429
+ FOR EACH ROW EXECUTE FUNCTION "reject_audit_log_mutation"();
430
+ --> statement-breakpoint
431
+ DROP TRIGGER IF EXISTS "platform_audit_logs_no_truncate" ON "platform_audit_logs";
432
+ --> statement-breakpoint
433
+ CREATE TRIGGER "platform_audit_logs_no_truncate"
434
+ BEFORE TRUNCATE ON "platform_audit_logs"
435
+ FOR EACH STATEMENT EXECUTE FUNCTION "reject_audit_log_mutation"();