@wtfalch/authz-store 0.1.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.
@@ -0,0 +1,571 @@
1
+ -- The store's baseline: every table @wtfalch/authz-store defines, as manage's
2
+ -- live schema has it on 2026-09-22 (manage drizzle/0000-0015 plus scripts/authz-v2).
3
+ -- Left to each app: the offered-permission CHECKs on tenants (they list that
4
+ -- app's catalogue) and break_glass_sessions.operator_id -> profiles (a table the
5
+ -- app owns). authz_events_action_check lists @wtfalch/authz's core events.
6
+ -- Generated once from pg_dump. Never edit it: a change is a new numbered file.
7
+ CREATE OR REPLACE FUNCTION access_team_tree_guard()
8
+ RETURNS trigger
9
+ LANGUAGE plpgsql
10
+ AS $function$
11
+ DECLARE current_id uuid; next_id uuid; depth integer := 0; subtree_depth integer := 0;
12
+ BEGIN
13
+ IF TG_OP = 'UPDATE' AND NEW.tenant_id <> OLD.tenant_id THEN
14
+ RAISE EXCEPTION 'a team cannot move between organisations';
15
+ END IF;
16
+ PERFORM 1 FROM tenants WHERE id = NEW.tenant_id FOR UPDATE;
17
+ current_id := NEW.parent_id;
18
+ WHILE current_id IS NOT NULL LOOP
19
+ IF current_id = NEW.id OR depth >= 63 THEN RAISE EXCEPTION 'invalid team hierarchy'; END IF;
20
+ SELECT parent_id INTO next_id FROM access_teams WHERE tenant_id = NEW.tenant_id AND id = current_id;
21
+ IF NOT FOUND THEN RAISE EXCEPTION 'unknown team parent'; END IF;
22
+ current_id := next_id;
23
+ depth := depth + 1;
24
+ END LOOP;
25
+ IF TG_OP = 'UPDATE' AND NEW.parent_id IS DISTINCT FROM OLD.parent_id THEN
26
+ WITH RECURSIVE children AS (
27
+ SELECT id, 0 AS level FROM access_teams WHERE id = NEW.id AND tenant_id = NEW.tenant_id
28
+ UNION ALL
29
+ SELECT t.id, c.level + 1 FROM access_teams t JOIN children c ON t.parent_id = c.id
30
+ WHERE t.tenant_id = NEW.tenant_id AND c.level < 64
31
+ ) SELECT coalesce(max(level), 0) INTO subtree_depth FROM children;
32
+ IF depth + subtree_depth >= 64 THEN RAISE EXCEPTION 'invalid team hierarchy'; END IF;
33
+ END IF;
34
+ RETURN NEW;
35
+ END $function$;
36
+ --> statement-breakpoint
37
+ CREATE OR REPLACE FUNCTION authz_event_resource_guard()
38
+ RETURNS trigger
39
+ LANGUAGE plpgsql
40
+ AS $function$
41
+ BEGIN
42
+ IF NEW.team_id IS DISTINCT FROM OLD.team_id OR NEW.subject_id IS DISTINCT FROM OLD.subject_id
43
+ OR NEW.subject_class IS DISTINCT FROM OLD.subject_class THEN
44
+ RAISE EXCEPTION 'audit resource metadata is immutable';
45
+ END IF;
46
+ RETURN NEW;
47
+ END $function$;
48
+ --> statement-breakpoint
49
+ CREATE OR REPLACE FUNCTION authz_events_guard()
50
+ RETURNS trigger
51
+ LANGUAGE plpgsql
52
+ AS $function$
53
+ BEGIN
54
+ IF TG_OP = 'DELETE' THEN
55
+ RAISE EXCEPTION 'authz_events is append-only: delete refused';
56
+ ELSIF TG_OP = 'TRUNCATE' THEN
57
+ RAISE EXCEPTION 'authz_events is append-only: truncate refused';
58
+ ELSIF TG_OP = 'UPDATE' THEN
59
+ IF NEW."id" IS DISTINCT FROM OLD."id"
60
+ OR NEW."occurred_at" IS DISTINCT FROM OLD."occurred_at"
61
+ OR NEW."tenant_id" IS DISTINCT FROM OLD."tenant_id"
62
+ OR NEW."actor_class" IS DISTINCT FROM OLD."actor_class"
63
+ OR NEW."actor_id" IS DISTINCT FROM OLD."actor_id"
64
+ OR NEW."action" IS DISTINCT FROM OLD."action"
65
+ OR NEW."target_type" IS DISTINCT FROM OLD."target_type"
66
+ OR NEW."target_id" IS DISTINCT FROM OLD."target_id"
67
+ OR NEW."outcome" IS DISTINCT FROM OLD."outcome"
68
+ OR NEW."context" IS DISTINCT FROM OLD."context"
69
+ OR NEW."session_id" IS DISTINCT FROM OLD."session_id"
70
+ OR NEW."reason" IS DISTINCT FROM OLD."reason"
71
+ OR NEW."reference" IS DISTINCT FROM OLD."reference"
72
+ OR NEW."request_id" IS DISTINCT FROM OLD."request_id"
73
+ OR NEW."ip" IS DISTINCT FROM OLD."ip"
74
+ OR NEW."user_agent" IS DISTINCT FROM OLD."user_agent"
75
+ OR NEW."tenant_visible" IS DISTINCT FROM OLD."tenant_visible"
76
+ OR NEW."schema_version" IS DISTINCT FROM OLD."schema_version"
77
+ THEN
78
+ RAISE EXCEPTION 'authz_events is append-only: only actor_display, before, after and erased_at may change';
79
+ END IF;
80
+ END IF;
81
+ RETURN NEW;
82
+ END
83
+ $function$;
84
+ --> statement-breakpoint
85
+ CREATE TABLE access_team_members (
86
+ tenant_id uuid NOT NULL,
87
+ team_id uuid NOT NULL,
88
+ principal_id text NOT NULL,
89
+ principal_class text DEFAULT 'human'::text NOT NULL,
90
+ CONSTRAINT access_team_members_principal_class_check CHECK ((principal_class = ANY (ARRAY['human'::text, 'api_key'::text, 'agent'::text, 'service'::text])))
91
+ );
92
+ --> statement-breakpoint
93
+ CREATE TABLE access_teams (
94
+ id uuid DEFAULT gen_random_uuid() NOT NULL,
95
+ tenant_id uuid NOT NULL,
96
+ parent_id uuid,
97
+ name text NOT NULL,
98
+ archived_at timestamp with time zone,
99
+ created_at timestamp with time zone DEFAULT now() NOT NULL,
100
+ CONSTRAINT access_teams_check CHECK ((parent_id IS DISTINCT FROM id)),
101
+ CONSTRAINT access_teams_name_check CHECK (((length(btrim(name)) >= 1) AND (length(btrim(name)) <= 100)))
102
+ );
103
+ --> statement-breakpoint
104
+ CREATE TABLE attach_proposals (
105
+ id uuid DEFAULT gen_random_uuid() NOT NULL,
106
+ parent_id uuid NOT NULL,
107
+ child_id uuid NOT NULL,
108
+ status text DEFAULT 'pending'::text NOT NULL,
109
+ reason text NOT NULL,
110
+ reference text NOT NULL,
111
+ proposed_by text NOT NULL,
112
+ parent_accepted_by text,
113
+ parent_accepted_at timestamp with time zone,
114
+ decided_by text,
115
+ decided_at timestamp with time zone,
116
+ created_at timestamp with time zone DEFAULT now() NOT NULL,
117
+ CONSTRAINT attach_proposals_decided_check CHECK (((status = ANY (ARRAY['pending_parent'::text, 'pending'::text])) = (decided_at IS NULL))),
118
+ CONSTRAINT attach_proposals_not_self_check CHECK ((parent_id <> child_id)),
119
+ CONSTRAINT attach_proposals_parent_accepted_check CHECK (((status <> 'pending_parent'::text) OR (parent_accepted_at IS NULL))),
120
+ CONSTRAINT attach_proposals_reason_check CHECK (((length(reason) >= 1) AND (length(reason) <= 512))),
121
+ CONSTRAINT attach_proposals_reference_check CHECK (((length(reference) >= 1) AND (length(reference) <= 512))),
122
+ CONSTRAINT attach_proposals_status_check CHECK ((status = ANY (ARRAY['pending_parent'::text, 'pending'::text, 'accepted'::text, 'declined'::text])))
123
+ );
124
+ --> statement-breakpoint
125
+ CREATE TABLE authz_assignments (
126
+ id uuid DEFAULT gen_random_uuid() NOT NULL,
127
+ tenant_id uuid NOT NULL,
128
+ application_id text NOT NULL,
129
+ platform_id text NOT NULL,
130
+ role_id uuid NOT NULL,
131
+ principal_id text,
132
+ principal_class text,
133
+ team_id uuid,
134
+ recipient_descendants boolean DEFAULT false NOT NULL,
135
+ boundary jsonb NOT NULL,
136
+ scope jsonb NOT NULL,
137
+ source text DEFAULT 'direct'::text NOT NULL,
138
+ via_tenant_id uuid,
139
+ primary_assignment boolean DEFAULT false NOT NULL,
140
+ expires_at timestamp with time zone,
141
+ created_by text,
142
+ created_at timestamp with time zone DEFAULT now() NOT NULL,
143
+ CONSTRAINT authz_assignments_boundary_check CHECK (((jsonb_typeof(boundary) = 'object'::text) AND ((boundary ->> 'kind'::text) = ANY (ARRAY['organisation'::text, 'organisations'::text, 'platform'::text])))),
144
+ CONSTRAINT authz_assignments_check CHECK (((principal_id IS NOT NULL) = (principal_class IS NOT NULL))),
145
+ CONSTRAINT authz_assignments_check1 CHECK (((((principal_id IS NOT NULL))::integer + ((team_id IS NOT NULL))::integer) = 1)),
146
+ CONSTRAINT authz_assignments_check2 CHECK (((NOT recipient_descendants) OR (team_id IS NOT NULL))),
147
+ CONSTRAINT authz_assignments_check3 CHECK (((NOT primary_assignment) OR (principal_id IS NOT NULL))),
148
+ CONSTRAINT authz_assignments_check4 CHECK (((source = 'inherited'::text) = (via_tenant_id IS NOT NULL))),
149
+ CONSTRAINT authz_assignments_principal_class_check CHECK ((principal_class = ANY (ARRAY['human'::text, 'api_key'::text, 'agent'::text, 'service'::text]))),
150
+ CONSTRAINT authz_assignments_scope_check CHECK (((jsonb_typeof(scope) = 'object'::text) AND ((scope ->> 'kind'::text) = ANY (ARRAY['organisation'::text, 'team'::text, 'own_teams'::text, 'resource'::text])))),
151
+ CONSTRAINT authz_assignments_source_check CHECK ((source = ANY (ARRAY['direct'::text, 'inherited'::text, 'activation'::text])))
152
+ );
153
+ --> statement-breakpoint
154
+ CREATE TABLE authz_events (
155
+ id bigint NOT NULL,
156
+ occurred_at timestamp with time zone DEFAULT now() NOT NULL,
157
+ tenant_id uuid,
158
+ actor_class text NOT NULL,
159
+ actor_id text NOT NULL,
160
+ actor_display text NOT NULL,
161
+ action text NOT NULL,
162
+ target_type text NOT NULL,
163
+ target_id text NOT NULL,
164
+ outcome text NOT NULL,
165
+ context text NOT NULL,
166
+ session_id text,
167
+ reason text,
168
+ reference text,
169
+ request_id text,
170
+ ip text,
171
+ user_agent text,
172
+ tenant_visible boolean NOT NULL,
173
+ before jsonb,
174
+ after jsonb,
175
+ erased_at timestamp with time zone,
176
+ schema_version smallint DEFAULT 1 NOT NULL,
177
+ team_id uuid,
178
+ subject_id text,
179
+ subject_class text,
180
+ CONSTRAINT authz_events_actor_class_check CHECK ((actor_class = ANY (ARRAY['human'::text, 'api_key'::text, 'agent'::text, 'service'::text]))),
181
+ CONSTRAINT authz_events_actor_display_check CHECK (((length(actor_display) >= 1) AND (length(actor_display) <= 256))),
182
+ CONSTRAINT authz_events_actor_id_check CHECK (((length(actor_id) >= 1) AND (length(actor_id) <= 256))),
183
+ CONSTRAINT authz_events_after_check CHECK (((after IS NULL) OR (octet_length((after)::text) <= 65536))),
184
+ CONSTRAINT authz_events_before_check CHECK (((before IS NULL) OR (octet_length((before)::text) <= 65536))),
185
+ CONSTRAINT authz_events_break_glass_check CHECK (((context <> 'break_glass'::text) OR ((session_id IS NOT NULL) AND (reason IS NOT NULL) AND (reference IS NOT NULL) AND (reason = ANY (ARRAY['customer_support'::text, 'incident'::text, 'review'::text, 'migration'::text, 'other'::text]))))),
186
+ CONSTRAINT authz_events_context_check CHECK ((context = ANY (ARRAY['standard'::text, 'operator'::text, 'break_glass'::text]))),
187
+ CONSTRAINT authz_events_ip_check CHECK (((ip IS NULL) OR (length(ip) <= 64))),
188
+ CONSTRAINT authz_events_outcome_check CHECK ((outcome = ANY (ARRAY['success'::text, 'denied'::text, 'error'::text]))),
189
+ CONSTRAINT authz_events_reason_check CHECK (((reason IS NULL) OR (length(reason) <= 512))),
190
+ CONSTRAINT authz_events_reference_check CHECK (((reference IS NULL) OR (length(reference) <= 512))),
191
+ CONSTRAINT authz_events_request_id_check CHECK (((request_id IS NULL) OR (length(request_id) <= 128))),
192
+ CONSTRAINT authz_events_session_id_check CHECK (((session_id IS NULL) OR (length(session_id) <= 256))),
193
+ CONSTRAINT authz_events_subject_class_check CHECK ((subject_class = ANY (ARRAY['human'::text, 'api_key'::text, 'agent'::text, 'service'::text]))),
194
+ CONSTRAINT authz_events_subject_pair_check CHECK (((subject_id IS NULL) = (subject_class IS NULL))),
195
+ CONSTRAINT authz_events_target_id_check CHECK (((length(target_id) >= 1) AND (length(target_id) <= 256))),
196
+ CONSTRAINT authz_events_target_type_check CHECK (((length(target_type) >= 1) AND (length(target_type) <= 64))),
197
+ CONSTRAINT authz_events_team_requires_tenant_check CHECK (((team_id IS NULL) OR (tenant_id IS NOT NULL))),
198
+ CONSTRAINT authz_events_user_agent_check CHECK (((user_agent IS NULL) OR (length(user_agent) <= 1024)))
199
+ );
200
+ --> statement-breakpoint
201
+ ALTER TABLE authz_events ALTER COLUMN id ADD GENERATED ALWAYS AS IDENTITY (
202
+ SEQUENCE NAME authz_events_id_seq
203
+ START WITH 1
204
+ INCREMENT BY 1
205
+ NO MINVALUE
206
+ NO MAXVALUE
207
+ CACHE 1
208
+ );
209
+ --> statement-breakpoint
210
+ CREATE TABLE authz_policy_version (
211
+ id integer NOT NULL,
212
+ version integer NOT NULL,
213
+ application_id text NOT NULL,
214
+ platform_id text NOT NULL,
215
+ mapping_revision text NOT NULL,
216
+ source_digest text NOT NULL,
217
+ migrated_at timestamp with time zone DEFAULT now() NOT NULL,
218
+ CONSTRAINT authz_policy_version_id_check CHECK ((id = 1)),
219
+ CONSTRAINT authz_policy_version_version_check CHECK ((version = 2))
220
+ );
221
+ --> statement-breakpoint
222
+ CREATE TABLE authz_roles (
223
+ id uuid DEFAULT gen_random_uuid() NOT NULL,
224
+ tenant_id uuid NOT NULL,
225
+ application_id text NOT NULL,
226
+ platform_id text NOT NULL,
227
+ key text NOT NULL,
228
+ name text NOT NULL,
229
+ description text DEFAULT ''::text NOT NULL,
230
+ built_in boolean DEFAULT false NOT NULL,
231
+ revision integer DEFAULT 1 NOT NULL,
232
+ entries jsonb NOT NULL,
233
+ guards text[] DEFAULT '{}'::text[] NOT NULL,
234
+ created_by text,
235
+ created_at timestamp with time zone DEFAULT now() NOT NULL,
236
+ updated_at timestamp with time zone DEFAULT now() NOT NULL,
237
+ updated_by text,
238
+ CONSTRAINT authz_roles_entries_check CHECK (((jsonb_typeof(entries) = 'array'::text) AND (jsonb_array_length(entries) <= 1000))),
239
+ CONSTRAINT authz_roles_key_check CHECK ((key ~ '^[a-z][a-z0-9_-]{0,63}$'::text)),
240
+ CONSTRAINT authz_roles_name_check CHECK (((length(btrim(name)) >= 1) AND (length(btrim(name)) <= 100))),
241
+ CONSTRAINT authz_roles_revision_check CHECK ((revision > 0))
242
+ );
243
+ --> statement-breakpoint
244
+ CREATE TABLE break_glass_sessions (
245
+ id uuid DEFAULT gen_random_uuid() NOT NULL,
246
+ operator_id text NOT NULL,
247
+ tenant_id uuid NOT NULL,
248
+ read_only boolean DEFAULT true NOT NULL,
249
+ reason_code text NOT NULL,
250
+ reference text NOT NULL,
251
+ started_at timestamp with time zone DEFAULT now() NOT NULL,
252
+ expires_at timestamp with time zone NOT NULL,
253
+ ended_at timestamp with time zone,
254
+ ended_by text,
255
+ role_id uuid NOT NULL,
256
+ CONSTRAINT bg_ended_check CHECK (((ended_at IS NULL) OR (ended_at >= started_at))),
257
+ CONSTRAINT bg_reason_check CHECK ((reason_code = ANY (ARRAY['customer_support'::text, 'incident'::text, 'review'::text, 'migration'::text, 'other'::text]))),
258
+ CONSTRAINT bg_reference_check CHECK (((length(reference) >= 1) AND (length(reference) <= 512))),
259
+ CONSTRAINT bg_ttl_check CHECK (((expires_at > started_at) AND (expires_at <= (started_at + '01:00:00'::interval))))
260
+ );
261
+ --> statement-breakpoint
262
+ CREATE TABLE credentials (
263
+ id uuid DEFAULT gen_random_uuid() NOT NULL,
264
+ tenant_id uuid NOT NULL,
265
+ kind text NOT NULL,
266
+ name text NOT NULL,
267
+ secret_hash text NOT NULL,
268
+ created_by text,
269
+ created_at timestamp with time zone DEFAULT now() NOT NULL,
270
+ expires_at timestamp with time zone,
271
+ revoked_at timestamp with time zone,
272
+ last_used_at timestamp with time zone,
273
+ secret_prefix text,
274
+ issuer_id text NOT NULL,
275
+ issuer_class text NOT NULL,
276
+ CONSTRAINT credentials_expiry_check CHECK (((expires_at IS NULL) OR (expires_at > created_at))),
277
+ CONSTRAINT credentials_issuer_class_check CHECK ((issuer_class = ANY (ARRAY['human'::text, 'api_key'::text, 'agent'::text, 'service'::text]))),
278
+ CONSTRAINT credentials_kind_check CHECK ((kind = ANY (ARRAY['api_key'::text, 'agent'::text, 'service'::text]))),
279
+ CONSTRAINT credentials_name_check CHECK (((length(name) >= 1) AND (length(name) <= 120))),
280
+ CONSTRAINT credentials_secret_prefix_check CHECK (((secret_prefix IS NULL) OR ((length(secret_prefix) >= 1) AND (length(secret_prefix) <= 16))))
281
+ );
282
+ --> statement-breakpoint
283
+ CREATE TABLE invitations (
284
+ id uuid DEFAULT gen_random_uuid() NOT NULL,
285
+ tenant_id uuid NOT NULL,
286
+ email text NOT NULL,
287
+ token_hash text NOT NULL,
288
+ status text DEFAULT 'pending'::text NOT NULL,
289
+ mail_state text DEFAULT 'pending'::text NOT NULL,
290
+ invited_by text,
291
+ expires_at timestamp with time zone NOT NULL,
292
+ accepted_by text,
293
+ accepted_at timestamp with time zone,
294
+ revoked_by text,
295
+ revoked_at timestamp with time zone,
296
+ created_at timestamp with time zone DEFAULT now() NOT NULL,
297
+ attach_parent_id uuid,
298
+ mail_state_at timestamp with time zone,
299
+ role_id uuid NOT NULL,
300
+ invited_by_class text,
301
+ CONSTRAINT invitations_accepted_check CHECK (((status = 'accepted'::text) = (accepted_at IS NOT NULL))),
302
+ CONSTRAINT invitations_email_lower_check CHECK ((email = lower(email))),
303
+ CONSTRAINT invitations_expiry_check CHECK ((expires_at > created_at)),
304
+ CONSTRAINT invitations_invited_by_class_check CHECK ((invited_by_class = ANY (ARRAY['human'::text, 'api_key'::text, 'agent'::text, 'service'::text]))),
305
+ CONSTRAINT invitations_mail_state_check CHECK ((mail_state = ANY (ARRAY['pending'::text, 'sent'::text, 'failed'::text]))),
306
+ CONSTRAINT invitations_revoked_check CHECK (((status = 'revoked'::text) = (revoked_at IS NOT NULL))),
307
+ CONSTRAINT invitations_status_check CHECK ((status = ANY (ARRAY['pending'::text, 'accepted'::text, 'revoked'::text, 'expired'::text]))),
308
+ CONSTRAINT invitations_typed_inviter CHECK (((status <> 'pending'::text) OR (invited_by IS NULL) OR (invited_by_class IS NOT NULL)))
309
+ );
310
+ --> statement-breakpoint
311
+ CREATE TABLE memberships (
312
+ tenant_id uuid NOT NULL,
313
+ principal_id text NOT NULL,
314
+ principal_class text NOT NULL,
315
+ source text DEFAULT 'direct'::text NOT NULL,
316
+ via_tenant_id uuid,
317
+ tenant_name text NOT NULL,
318
+ granted_by text,
319
+ created_at timestamp with time zone DEFAULT now() NOT NULL,
320
+ updated_at timestamp with time zone DEFAULT now() NOT NULL,
321
+ CONSTRAINT memberships_principal_class_check CHECK ((principal_class = ANY (ARRAY['human'::text, 'api_key'::text, 'agent'::text, 'service'::text]))),
322
+ CONSTRAINT memberships_source_check CHECK ((source = ANY (ARRAY['direct'::text, 'inherited'::text]))),
323
+ CONSTRAINT memberships_source_via_check CHECK (((source = 'inherited'::text) = (via_tenant_id IS NOT NULL)))
324
+ );
325
+ --> statement-breakpoint
326
+ CREATE TABLE tenants (
327
+ id uuid DEFAULT gen_random_uuid() NOT NULL,
328
+ kind text DEFAULT 'customer'::text NOT NULL,
329
+ slug text NOT NULL,
330
+ name text NOT NULL,
331
+ state text DEFAULT 'active'::text NOT NULL,
332
+ parent_id uuid,
333
+ ceiling text[] NOT NULL,
334
+ self_denied text[] DEFAULT '{}'::text[] NOT NULL,
335
+ created_by text,
336
+ created_at timestamp with time zone DEFAULT now() NOT NULL,
337
+ updated_at timestamp with time zone DEFAULT now() NOT NULL,
338
+ frozen boolean DEFAULT false NOT NULL,
339
+ CONSTRAINT tenants_frozen_operator_check CHECK (((kind = 'operator'::text) OR (NOT frozen))),
340
+ CONSTRAINT tenants_kind_check CHECK ((kind = ANY (ARRAY['customer'::text, 'operator'::text]))),
341
+ CONSTRAINT tenants_name_check CHECK (((length(name) >= 1) AND (length(name) <= 120))),
342
+ CONSTRAINT tenants_operator_active_check CHECK (((kind <> 'operator'::text) OR (state = 'active'::text))),
343
+ CONSTRAINT tenants_operator_no_parent_check CHECK (((kind <> 'operator'::text) OR (parent_id IS NULL))),
344
+ CONSTRAINT tenants_parent_not_self_check CHECK (((parent_id IS NULL) OR (parent_id <> id))),
345
+ CONSTRAINT tenants_slug_check CHECK ((slug ~ '^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$'::text)),
346
+ CONSTRAINT tenants_state_check CHECK ((state = ANY (ARRAY['active'::text, 'read_only'::text, 'suspended'::text, 'archived'::text])))
347
+ );
348
+ --> statement-breakpoint
349
+ ALTER TABLE ONLY access_team_members
350
+ ADD CONSTRAINT access_team_members_pkey PRIMARY KEY (team_id, principal_class, principal_id);
351
+ --> statement-breakpoint
352
+ ALTER TABLE ONLY access_teams
353
+ ADD CONSTRAINT access_teams_pkey PRIMARY KEY (id);
354
+ --> statement-breakpoint
355
+ ALTER TABLE ONLY access_teams
356
+ ADD CONSTRAINT access_teams_tenant_id_id_key UNIQUE (tenant_id, id);
357
+ --> statement-breakpoint
358
+ ALTER TABLE ONLY attach_proposals
359
+ ADD CONSTRAINT attach_proposals_pkey PRIMARY KEY (id);
360
+ --> statement-breakpoint
361
+ ALTER TABLE ONLY authz_assignments
362
+ ADD CONSTRAINT authz_assignments_pkey PRIMARY KEY (id);
363
+ --> statement-breakpoint
364
+ ALTER TABLE ONLY authz_events
365
+ ADD CONSTRAINT authz_events_pkey PRIMARY KEY (id);
366
+ --> statement-breakpoint
367
+ ALTER TABLE ONLY authz_policy_version
368
+ ADD CONSTRAINT authz_policy_version_pkey PRIMARY KEY (id);
369
+ --> statement-breakpoint
370
+ ALTER TABLE ONLY authz_roles
371
+ ADD CONSTRAINT authz_roles_boundary_id UNIQUE (tenant_id, application_id, platform_id, id);
372
+ --> statement-breakpoint
373
+ ALTER TABLE ONLY authz_roles
374
+ ADD CONSTRAINT authz_roles_pkey PRIMARY KEY (id);
375
+ --> statement-breakpoint
376
+ ALTER TABLE ONLY authz_roles
377
+ ADD CONSTRAINT authz_roles_tenant_app_key UNIQUE (tenant_id, application_id, key);
378
+ --> statement-breakpoint
379
+ ALTER TABLE ONLY authz_roles
380
+ ADD CONSTRAINT authz_roles_tenant_id_id_key UNIQUE (tenant_id, id);
381
+ --> statement-breakpoint
382
+ ALTER TABLE ONLY break_glass_sessions
383
+ ADD CONSTRAINT break_glass_sessions_pkey PRIMARY KEY (id);
384
+ --> statement-breakpoint
385
+ ALTER TABLE ONLY credentials
386
+ ADD CONSTRAINT credentials_pkey PRIMARY KEY (id);
387
+ --> statement-breakpoint
388
+ ALTER TABLE ONLY credentials
389
+ ADD CONSTRAINT credentials_secret_hash_key UNIQUE (secret_hash);
390
+ --> statement-breakpoint
391
+ ALTER TABLE ONLY invitations
392
+ ADD CONSTRAINT invitations_pkey PRIMARY KEY (id);
393
+ --> statement-breakpoint
394
+ ALTER TABLE ONLY invitations
395
+ ADD CONSTRAINT invitations_token_hash_key UNIQUE (token_hash);
396
+ --> statement-breakpoint
397
+ ALTER TABLE ONLY memberships
398
+ ADD CONSTRAINT memberships_pkey PRIMARY KEY (tenant_id, principal_class, principal_id);
399
+ --> statement-breakpoint
400
+ ALTER TABLE ONLY memberships
401
+ ADD CONSTRAINT memberships_typed_identity UNIQUE (tenant_id, principal_class, principal_id);
402
+ --> statement-breakpoint
403
+ ALTER TABLE ONLY tenants
404
+ ADD CONSTRAINT tenants_pkey PRIMARY KEY (id);
405
+ --> statement-breakpoint
406
+ ALTER TABLE ONLY tenants
407
+ ADD CONSTRAINT tenants_slug_unique UNIQUE (slug);
408
+ --> statement-breakpoint
409
+ CREATE INDEX access_team_members_principal_idx ON access_team_members USING btree (tenant_id, principal_id);
410
+ --> statement-breakpoint
411
+ CREATE INDEX access_teams_parent_idx ON access_teams USING btree (tenant_id, parent_id);
412
+ --> statement-breakpoint
413
+ CREATE INDEX attach_proposals_child_idx ON attach_proposals USING btree (child_id, created_at DESC);
414
+ --> statement-breakpoint
415
+ CREATE INDEX attach_proposals_parent_idx ON attach_proposals USING btree (parent_id, created_at DESC);
416
+ --> statement-breakpoint
417
+ CREATE UNIQUE INDEX attach_proposals_pending_idx ON attach_proposals USING btree (parent_id, child_id) WHERE (status = ANY (ARRAY['pending_parent'::text, 'pending'::text]));
418
+ --> statement-breakpoint
419
+ CREATE UNIQUE INDEX authz_assignments_primary_idx ON authz_assignments USING btree (tenant_id, application_id, principal_class, principal_id) WHERE primary_assignment;
420
+ --> statement-breakpoint
421
+ CREATE INDEX authz_assignments_principal_idx ON authz_assignments USING btree (tenant_id, principal_class, principal_id);
422
+ --> statement-breakpoint
423
+ CREATE INDEX authz_assignments_role_idx ON authz_assignments USING btree (tenant_id, role_id);
424
+ --> statement-breakpoint
425
+ CREATE INDEX authz_assignments_team_idx ON authz_assignments USING btree (tenant_id, team_id);
426
+ --> statement-breakpoint
427
+ CREATE INDEX authz_events_action_time_idx ON authz_events USING btree (action, occurred_at DESC);
428
+ --> statement-breakpoint
429
+ CREATE INDEX authz_events_actor_time_idx ON authz_events USING btree (actor_id, occurred_at DESC);
430
+ --> statement-breakpoint
431
+ CREATE INDEX authz_events_subject_time_idx ON authz_events USING btree (tenant_id, subject_class, subject_id, occurred_at DESC, id DESC);
432
+ --> statement-breakpoint
433
+ CREATE INDEX authz_events_team_time_idx ON authz_events USING btree (tenant_id, team_id, occurred_at DESC, id DESC);
434
+ --> statement-breakpoint
435
+ CREATE INDEX authz_events_tenant_time_idx ON authz_events USING btree (tenant_id, occurred_at DESC, id DESC);
436
+ --> statement-breakpoint
437
+ CREATE INDEX bg_active_idx ON break_glass_sessions USING btree (operator_id, tenant_id) WHERE (ended_at IS NULL);
438
+ --> statement-breakpoint
439
+ CREATE INDEX bg_tenant_idx ON break_glass_sessions USING btree (tenant_id, started_at DESC);
440
+ --> statement-breakpoint
441
+ CREATE INDEX credentials_live_idx ON credentials USING btree (tenant_id, created_at DESC) WHERE (revoked_at IS NULL);
442
+ --> statement-breakpoint
443
+ CREATE INDEX credentials_tenant_idx ON credentials USING btree (tenant_id);
444
+ --> statement-breakpoint
445
+ CREATE UNIQUE INDEX invitations_pending_idx ON invitations USING btree (tenant_id, email) WHERE (status = 'pending'::text);
446
+ --> statement-breakpoint
447
+ CREATE INDEX invitations_tenant_idx ON invitations USING btree (tenant_id, created_at DESC);
448
+ --> statement-breakpoint
449
+ CREATE INDEX memberships_principal_idx ON memberships USING btree (principal_class, principal_id, tenant_name, tenant_id);
450
+ --> statement-breakpoint
451
+ CREATE INDEX memberships_via_idx ON memberships USING btree (via_tenant_id);
452
+ --> statement-breakpoint
453
+ CREATE UNIQUE INDEX tenants_one_operator_idx ON tenants USING btree (kind) WHERE (kind = 'operator'::text);
454
+ --> statement-breakpoint
455
+ CREATE INDEX tenants_parent_idx ON tenants USING btree (parent_id);
456
+ --> statement-breakpoint
457
+ CREATE TRIGGER access_team_tree_guard BEFORE INSERT OR UPDATE ON access_teams FOR EACH ROW EXECUTE FUNCTION access_team_tree_guard();
458
+ --> statement-breakpoint
459
+ CREATE TRIGGER authz_event_resource_guard BEFORE UPDATE ON authz_events FOR EACH ROW EXECUTE FUNCTION authz_event_resource_guard();
460
+ --> statement-breakpoint
461
+ CREATE TRIGGER authz_events_guarded_update BEFORE UPDATE ON authz_events FOR EACH ROW EXECUTE FUNCTION authz_events_guard();
462
+ --> statement-breakpoint
463
+ CREATE TRIGGER authz_events_no_delete BEFORE DELETE ON authz_events FOR EACH ROW EXECUTE FUNCTION authz_events_guard();
464
+ --> statement-breakpoint
465
+ CREATE TRIGGER authz_events_no_truncate BEFORE TRUNCATE ON authz_events FOR EACH STATEMENT EXECUTE FUNCTION authz_events_guard();
466
+ --> statement-breakpoint
467
+ ALTER TABLE ONLY access_team_members
468
+ ADD CONSTRAINT access_team_members_tenant_id_principal_class_principal_id_fkey FOREIGN KEY (tenant_id, principal_class, principal_id) REFERENCES memberships(tenant_id, principal_class, principal_id) ON DELETE CASCADE;
469
+ --> statement-breakpoint
470
+ ALTER TABLE ONLY access_team_members
471
+ ADD CONSTRAINT access_team_members_tenant_id_team_id_fkey FOREIGN KEY (tenant_id, team_id) REFERENCES access_teams(tenant_id, id) ON DELETE CASCADE;
472
+ --> statement-breakpoint
473
+ ALTER TABLE ONLY access_teams
474
+ ADD CONSTRAINT access_teams_tenant_id_fkey FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE;
475
+ --> statement-breakpoint
476
+ ALTER TABLE ONLY access_teams
477
+ ADD CONSTRAINT access_teams_tenant_id_parent_id_fkey FOREIGN KEY (tenant_id, parent_id) REFERENCES access_teams(tenant_id, id);
478
+ --> statement-breakpoint
479
+ ALTER TABLE ONLY attach_proposals
480
+ ADD CONSTRAINT attach_proposals_child_id_fkey FOREIGN KEY (child_id) REFERENCES tenants(id) ON DELETE CASCADE;
481
+ --> statement-breakpoint
482
+ ALTER TABLE ONLY attach_proposals
483
+ ADD CONSTRAINT attach_proposals_parent_id_fkey FOREIGN KEY (parent_id) REFERENCES tenants(id) ON DELETE CASCADE;
484
+ --> statement-breakpoint
485
+ ALTER TABLE ONLY authz_assignments
486
+ ADD CONSTRAINT authz_assignments_tenant_id_application_id_platform_id_rol_fkey FOREIGN KEY (tenant_id, application_id, platform_id, role_id) REFERENCES authz_roles(tenant_id, application_id, platform_id, id);
487
+ --> statement-breakpoint
488
+ ALTER TABLE ONLY authz_assignments
489
+ ADD CONSTRAINT authz_assignments_tenant_id_fkey FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE;
490
+ --> statement-breakpoint
491
+ ALTER TABLE ONLY authz_assignments
492
+ ADD CONSTRAINT authz_assignments_tenant_id_principal_class_principal_id_fkey FOREIGN KEY (tenant_id, principal_class, principal_id) REFERENCES memberships(tenant_id, principal_class, principal_id) ON DELETE CASCADE;
493
+ --> statement-breakpoint
494
+ ALTER TABLE ONLY authz_assignments
495
+ ADD CONSTRAINT authz_assignments_tenant_id_team_id_fkey FOREIGN KEY (tenant_id, team_id) REFERENCES access_teams(tenant_id, id) ON DELETE CASCADE;
496
+ --> statement-breakpoint
497
+ ALTER TABLE ONLY authz_assignments
498
+ ADD CONSTRAINT authz_assignments_via_tenant_id_fkey FOREIGN KEY (via_tenant_id) REFERENCES tenants(id) ON DELETE CASCADE;
499
+ --> statement-breakpoint
500
+ ALTER TABLE ONLY authz_events
501
+ ADD CONSTRAINT authz_events_team_tenant_fk FOREIGN KEY (tenant_id, team_id) REFERENCES access_teams(tenant_id, id);
502
+ --> statement-breakpoint
503
+ ALTER TABLE ONLY authz_roles
504
+ ADD CONSTRAINT authz_roles_tenant_id_fkey FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE;
505
+ --> statement-breakpoint
506
+ ALTER TABLE ONLY break_glass_sessions
507
+ ADD CONSTRAINT break_glass_sessions_tenant_id_fkey FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE RESTRICT;
508
+ --> statement-breakpoint
509
+ ALTER TABLE ONLY credentials
510
+ ADD CONSTRAINT credentials_tenant_id_fkey FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE;
511
+ --> statement-breakpoint
512
+ ALTER TABLE ONLY invitations
513
+ ADD CONSTRAINT invitations_attach_parent_id_fkey FOREIGN KEY (attach_parent_id) REFERENCES tenants(id) ON DELETE SET NULL;
514
+ --> statement-breakpoint
515
+ ALTER TABLE ONLY invitations
516
+ ADD CONSTRAINT invitations_policy_role_fk FOREIGN KEY (tenant_id, role_id) REFERENCES authz_roles(tenant_id, id);
517
+ --> statement-breakpoint
518
+ ALTER TABLE ONLY invitations
519
+ ADD CONSTRAINT invitations_tenant_id_fkey FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE;
520
+ --> statement-breakpoint
521
+ ALTER TABLE ONLY memberships
522
+ ADD CONSTRAINT memberships_tenant_id_fkey FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE;
523
+ --> statement-breakpoint
524
+ ALTER TABLE ONLY memberships
525
+ ADD CONSTRAINT memberships_via_tenant_id_fkey FOREIGN KEY (via_tenant_id) REFERENCES tenants(id) ON DELETE CASCADE;
526
+ --> statement-breakpoint
527
+ ALTER TABLE ONLY break_glass_sessions
528
+ ADD CONSTRAINT support_policy_role_fk FOREIGN KEY (tenant_id, role_id) REFERENCES authz_roles(tenant_id, id);
529
+ --> statement-breakpoint
530
+ ALTER TABLE ONLY tenants
531
+ ADD CONSTRAINT tenants_parent_id_fkey FOREIGN KEY (parent_id) REFERENCES tenants(id) ON DELETE RESTRICT;
532
+ --> statement-breakpoint
533
+ ALTER TABLE "authz_events" ADD CONSTRAINT "authz_events_action_check" CHECK ("action" IN (
534
+ 'activation.approved',
535
+ 'activation.denied',
536
+ 'activation.expired',
537
+ 'activation.requested',
538
+ 'activation.revoked',
539
+ 'activation.started',
540
+ 'audit_log.viewed',
541
+ 'break_glass.ended',
542
+ 'break_glass.started',
543
+ 'break_glass.viewed',
544
+ 'credential.minted',
545
+ 'credential.revoked',
546
+ 'credential.rotated',
547
+ 'invitation.accepted',
548
+ 'invitation.expired',
549
+ 'invitation.revoked',
550
+ 'invitation.sent',
551
+ 'membership.created',
552
+ 'membership.ended',
553
+ 'membership.role_changed',
554
+ 'operator.bootstrapped',
555
+ 'owner.installed',
556
+ 'person.erased',
557
+ 'platform.frozen',
558
+ 'platform.unfrozen',
559
+ 'role.created',
560
+ 'role.deleted',
561
+ 'role.updated',
562
+ 'tenant.attach_declined',
563
+ 'tenant.attach_proposed',
564
+ 'tenant.ceiling_changed',
565
+ 'tenant.created',
566
+ 'tenant.exported',
567
+ 'tenant.parent_attached',
568
+ 'tenant.parent_detached',
569
+ 'tenant.settings_changed',
570
+ 'tenant.state_changed'
571
+ ));
@@ -0,0 +1,27 @@
1
+ -- Grants to one person, stored rather than compiled from a role. Guests live
2
+ -- here; see src/grants.ts and https://github.com/wtfalch/authz/issues/42.
3
+ CREATE TABLE "authz_grants" (
4
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
5
+ "tenant_id" uuid NOT NULL,
6
+ "application_id" text NOT NULL,
7
+ "platform_id" text NOT NULL,
8
+ "permission" text NOT NULL,
9
+ "principal_id" text NOT NULL,
10
+ "principal_class" text NOT NULL,
11
+ "boundary" jsonb NOT NULL,
12
+ "scope" jsonb NOT NULL,
13
+ "constraints" jsonb,
14
+ "relation" text DEFAULT 'any' NOT NULL,
15
+ "guest" boolean DEFAULT false NOT NULL,
16
+ "reason" text,
17
+ "expires_at" timestamp with time zone,
18
+ "revoked_at" timestamp with time zone,
19
+ "created_by" text,
20
+ "created_at" timestamp with time zone DEFAULT now() NOT NULL,
21
+ -- The engine refuses a guest grant with no end date, so the table does too.
22
+ CONSTRAINT "authz_grants_guest_expires" CHECK (NOT "guest" OR "expires_at" IS NOT NULL)
23
+ );
24
+ --> statement-breakpoint
25
+ CREATE INDEX "authz_grants_principal_idx" ON "authz_grants" ("tenant_id", "principal_class", "principal_id");
26
+ --> statement-breakpoint
27
+ CREATE INDEX "authz_grants_guest_idx" ON "authz_grants" ("tenant_id", "guest");
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@wtfalch/authz-store",
3
+ "version": "0.1.0",
4
+ "description": "Persistence and lifecycle for @wtfalch/authz: the storage a host would otherwise write itself.",
5
+ "type": "module",
6
+ "files": [
7
+ "dist",
8
+ "migrations",
9
+ "LICENSE"
10
+ ],
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/index.d.ts",
14
+ "default": "./dist/index.js"
15
+ },
16
+ "./package.json": "./package.json"
17
+ },
18
+ "peerDependencies": {
19
+ "@wtfalch/authz": "^0.12.0",
20
+ "drizzle-orm": "^0.39.0"
21
+ },
22
+ "engines": {
23
+ "node": ">=22.0.0"
24
+ },
25
+ "license": "MIT",
26
+ "publishConfig": {
27
+ "access": "public"
28
+ },
29
+ "devDependencies": {
30
+ "@electric-sql/pglite": "^0.5.8",
31
+ "@types/node": "^22",
32
+ "drizzle-kit": "^0.31.10",
33
+ "drizzle-orm": "^0.39.0",
34
+ "fast-check": "^4.9.0",
35
+ "postgres": "^3.4.5",
36
+ "typescript": "^5.9.0",
37
+ "vitest": "^4.1.6"
38
+ },
39
+ "scripts": {
40
+ "build": "rm -rf dist && tsc -p tsconfig.build.json",
41
+ "typecheck": "tsc --noEmit",
42
+ "test": "vitest run"
43
+ }
44
+ }