@selvajs/supabase-provider 0.11.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/LICENSE +21 -0
- package/README.md +271 -0
- package/dist/auth/SupabaseAuthProvider.d.ts +81 -0
- package/dist/auth/SupabaseAuthProvider.d.ts.map +1 -0
- package/dist/auth/SupabaseAuthProvider.js +383 -0
- package/dist/auth/SupabaseAuthProvider.js.map +1 -0
- package/dist/auth/__tests__/conformance.test.d.ts +2 -0
- package/dist/auth/__tests__/conformance.test.d.ts.map +1 -0
- package/dist/auth/__tests__/conformance.test.js +76 -0
- package/dist/auth/__tests__/conformance.test.js.map +1 -0
- package/dist/auth/index.d.ts +3 -0
- package/dist/auth/index.d.ts.map +1 -0
- package/dist/auth/index.js +2 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/data/SupabaseAuditQuery.d.ts +16 -0
- package/dist/data/SupabaseAuditQuery.d.ts.map +1 -0
- package/dist/data/SupabaseAuditQuery.js +83 -0
- package/dist/data/SupabaseAuditQuery.js.map +1 -0
- package/dist/data/SupabaseComputeServerStore.d.ts +26 -0
- package/dist/data/SupabaseComputeServerStore.d.ts.map +1 -0
- package/dist/data/SupabaseComputeServerStore.js +227 -0
- package/dist/data/SupabaseComputeServerStore.js.map +1 -0
- package/dist/data/SupabaseDataProvider.d.ts +55 -0
- package/dist/data/SupabaseDataProvider.d.ts.map +1 -0
- package/dist/data/SupabaseDataProvider.js +114 -0
- package/dist/data/SupabaseDataProvider.js.map +1 -0
- package/dist/data/SupabaseDefinitionStore.d.ts +39 -0
- package/dist/data/SupabaseDefinitionStore.d.ts.map +1 -0
- package/dist/data/SupabaseDefinitionStore.js +419 -0
- package/dist/data/SupabaseDefinitionStore.js.map +1 -0
- package/dist/data/SupabaseEventSink.d.ts +17 -0
- package/dist/data/SupabaseEventSink.d.ts.map +1 -0
- package/dist/data/SupabaseEventSink.js +31 -0
- package/dist/data/SupabaseEventSink.js.map +1 -0
- package/dist/data/SupabaseInviteStore.d.ts +22 -0
- package/dist/data/SupabaseInviteStore.d.ts.map +1 -0
- package/dist/data/SupabaseInviteStore.js +159 -0
- package/dist/data/SupabaseInviteStore.js.map +1 -0
- package/dist/data/SupabaseOrgStore.d.ts +33 -0
- package/dist/data/SupabaseOrgStore.d.ts.map +1 -0
- package/dist/data/SupabaseOrgStore.js +455 -0
- package/dist/data/SupabaseOrgStore.js.map +1 -0
- package/dist/data/SupabaseProjectStore.d.ts +29 -0
- package/dist/data/SupabaseProjectStore.d.ts.map +1 -0
- package/dist/data/SupabaseProjectStore.js +328 -0
- package/dist/data/SupabaseProjectStore.js.map +1 -0
- package/dist/data/SupabaseShareLinkStore.d.ts +26 -0
- package/dist/data/SupabaseShareLinkStore.d.ts.map +1 -0
- package/dist/data/SupabaseShareLinkStore.js +150 -0
- package/dist/data/SupabaseShareLinkStore.js.map +1 -0
- package/dist/data/__tests__/compute-server-conformance.test.d.ts +2 -0
- package/dist/data/__tests__/compute-server-conformance.test.d.ts.map +1 -0
- package/dist/data/__tests__/compute-server-conformance.test.js +40 -0
- package/dist/data/__tests__/compute-server-conformance.test.js.map +1 -0
- package/dist/data/__tests__/definition-conformance.test.d.ts +2 -0
- package/dist/data/__tests__/definition-conformance.test.d.ts.map +1 -0
- package/dist/data/__tests__/definition-conformance.test.js +85 -0
- package/dist/data/__tests__/definition-conformance.test.js.map +1 -0
- package/dist/data/__tests__/event-sink-conformance.test.d.ts +2 -0
- package/dist/data/__tests__/event-sink-conformance.test.d.ts.map +1 -0
- package/dist/data/__tests__/event-sink-conformance.test.js +27 -0
- package/dist/data/__tests__/event-sink-conformance.test.js.map +1 -0
- package/dist/data/__tests__/invite-conformance.test.d.ts +2 -0
- package/dist/data/__tests__/invite-conformance.test.d.ts.map +1 -0
- package/dist/data/__tests__/invite-conformance.test.js +45 -0
- package/dist/data/__tests__/invite-conformance.test.js.map +1 -0
- package/dist/data/__tests__/org-conformance.test.d.ts +2 -0
- package/dist/data/__tests__/org-conformance.test.d.ts.map +1 -0
- package/dist/data/__tests__/org-conformance.test.js +29 -0
- package/dist/data/__tests__/org-conformance.test.js.map +1 -0
- package/dist/data/__tests__/project-conformance.test.d.ts +2 -0
- package/dist/data/__tests__/project-conformance.test.d.ts.map +1 -0
- package/dist/data/__tests__/project-conformance.test.js +57 -0
- package/dist/data/__tests__/project-conformance.test.js.map +1 -0
- package/dist/data/__tests__/share-link-conformance.test.d.ts +2 -0
- package/dist/data/__tests__/share-link-conformance.test.d.ts.map +1 -0
- package/dist/data/__tests__/share-link-conformance.test.js +88 -0
- package/dist/data/__tests__/share-link-conformance.test.js.map +1 -0
- package/dist/data/__tests__/test-helpers.d.ts +53 -0
- package/dist/data/__tests__/test-helpers.d.ts.map +1 -0
- package/dist/data/__tests__/test-helpers.js +175 -0
- package/dist/data/__tests__/test-helpers.js.map +1 -0
- package/dist/data/client.d.ts +44 -0
- package/dist/data/client.d.ts.map +1 -0
- package/dist/data/client.js +40 -0
- package/dist/data/client.js.map +1 -0
- package/dist/data/index.d.ts +12 -0
- package/dist/data/index.d.ts.map +1 -0
- package/dist/data/index.js +11 -0
- package/dist/data/index.js.map +1 -0
- package/dist/data/pagination.d.ts +30 -0
- package/dist/data/pagination.d.ts.map +1 -0
- package/dist/data/pagination.js +53 -0
- package/dist/data/pagination.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/permissions/SupabasePlatformPermissionStore.d.ts +25 -0
- package/dist/permissions/SupabasePlatformPermissionStore.d.ts.map +1 -0
- package/dist/permissions/SupabasePlatformPermissionStore.js +153 -0
- package/dist/permissions/SupabasePlatformPermissionStore.js.map +1 -0
- package/dist/permissions/__tests__/conformance.test.d.ts +2 -0
- package/dist/permissions/__tests__/conformance.test.d.ts.map +1 -0
- package/dist/permissions/__tests__/conformance.test.js +27 -0
- package/dist/permissions/__tests__/conformance.test.js.map +1 -0
- package/dist/permissions/index.d.ts +2 -0
- package/dist/permissions/index.d.ts.map +1 -0
- package/dist/permissions/index.js +2 -0
- package/dist/permissions/index.js.map +1 -0
- package/dist/storage/SupabaseStorageProvider.d.ts +67 -0
- package/dist/storage/SupabaseStorageProvider.d.ts.map +1 -0
- package/dist/storage/SupabaseStorageProvider.js +136 -0
- package/dist/storage/SupabaseStorageProvider.js.map +1 -0
- package/dist/storage/__tests__/conformance.test.d.ts +2 -0
- package/dist/storage/__tests__/conformance.test.d.ts.map +1 -0
- package/dist/storage/__tests__/conformance.test.js +149 -0
- package/dist/storage/__tests__/conformance.test.js.map +1 -0
- package/dist/storage/index.d.ts +3 -0
- package/dist/storage/index.d.ts.map +1 -0
- package/dist/storage/index.js +2 -0
- package/dist/storage/index.js.map +1 -0
- package/dist/userProfile/SupabaseUserProfileProvider.d.ts +16 -0
- package/dist/userProfile/SupabaseUserProfileProvider.d.ts.map +1 -0
- package/dist/userProfile/SupabaseUserProfileProvider.js +148 -0
- package/dist/userProfile/SupabaseUserProfileProvider.js.map +1 -0
- package/dist/userProfile/__tests__/conformance.test.d.ts +2 -0
- package/dist/userProfile/__tests__/conformance.test.d.ts.map +1 -0
- package/dist/userProfile/__tests__/conformance.test.js +38 -0
- package/dist/userProfile/__tests__/conformance.test.js.map +1 -0
- package/dist/userProfile/index.d.ts +2 -0
- package/dist/userProfile/index.d.ts.map +1 -0
- package/dist/userProfile/index.js +2 -0
- package/dist/userProfile/index.js.map +1 -0
- package/package.json +75 -0
- package/src/auth/SupabaseAuthProvider.ts +464 -0
- package/src/auth/__tests__/conformance.test.ts +78 -0
- package/src/auth/index.ts +2 -0
- package/src/data/SupabaseAuditQuery.ts +103 -0
- package/src/data/SupabaseComputeServerStore.ts +262 -0
- package/src/data/SupabaseDataProvider.ts +125 -0
- package/src/data/SupabaseDefinitionStore.ts +504 -0
- package/src/data/SupabaseEventSink.ts +30 -0
- package/src/data/SupabaseInviteStore.ts +183 -0
- package/src/data/SupabaseOrgStore.ts +547 -0
- package/src/data/SupabaseProjectStore.ts +414 -0
- package/src/data/SupabaseShareLinkStore.ts +191 -0
- package/src/data/__tests__/compute-server-conformance.test.ts +40 -0
- package/src/data/__tests__/definition-conformance.test.ts +89 -0
- package/src/data/__tests__/event-sink-conformance.test.ts +32 -0
- package/src/data/__tests__/invite-conformance.test.ts +46 -0
- package/src/data/__tests__/org-conformance.test.ts +30 -0
- package/src/data/__tests__/project-conformance.test.ts +57 -0
- package/src/data/__tests__/share-link-conformance.test.ts +87 -0
- package/src/data/__tests__/test-helpers.ts +186 -0
- package/src/data/client.ts +86 -0
- package/src/data/index.ts +11 -0
- package/src/data/pagination.ts +68 -0
- package/src/index.ts +23 -0
- package/src/permissions/SupabasePlatformPermissionStore.ts +161 -0
- package/src/permissions/__tests__/conformance.test.ts +28 -0
- package/src/permissions/index.ts +1 -0
- package/src/storage/SupabaseStorageProvider.ts +177 -0
- package/src/storage/__tests__/conformance.test.ts +152 -0
- package/src/storage/index.ts +2 -0
- package/src/userProfile/SupabaseUserProfileProvider.ts +177 -0
- package/src/userProfile/__tests__/conformance.test.ts +39 -0
- package/src/userProfile/index.ts +1 -0
- package/supabase/.branches/_current_branch +1 -0
- package/supabase/.temp/cli-latest +1 -0
- package/supabase/config.toml +68 -0
- package/supabase/migrations/0001_initial.sql +1084 -0
- package/supabase/seed.sql +13 -0
|
@@ -0,0 +1,1084 @@
|
|
|
1
|
+
-- ============================================================================
|
|
2
|
+
-- Selva — initial schema (consolidated bootstrap).
|
|
3
|
+
--
|
|
4
|
+
-- This is the only migration in the repository. It produces the same
|
|
5
|
+
-- end-state that 0001–0010 produced incrementally, with the dropped /
|
|
6
|
+
-- replaced columns and policies omitted entirely. Apply once on a fresh
|
|
7
|
+
-- DB; future schema changes go in numbered files (0002_…, 0003_…).
|
|
8
|
+
--
|
|
9
|
+
-- Layered top-to-bottom so dependencies always resolve:
|
|
10
|
+
-- 1. set_updated_at trigger function
|
|
11
|
+
-- 2. user_profiles + auto-seed trigger + RLS
|
|
12
|
+
-- 3. orgs, org_members, projects, project_members tables + indexes
|
|
13
|
+
-- 4. updated_at triggers + name uniqueness index
|
|
14
|
+
-- 5. RLS helper functions (depend on all of the above)
|
|
15
|
+
-- 6. Org/project RLS policies
|
|
16
|
+
-- 7. definitions + definition_versions + RPC + RLS
|
|
17
|
+
-- 8. invites + RPC + RLS
|
|
18
|
+
-- 9. compute_servers + per-org/instance defaults + RLS
|
|
19
|
+
-- 10. share_links (spec §7) + atomic-increment RPC + RLS
|
|
20
|
+
-- 11. Storage bucket policies (selva-public, selva-private)
|
|
21
|
+
-- 12. audit_events (spec §9 domain-event sink)
|
|
22
|
+
-- ============================================================================
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
-- ============================================================================
|
|
26
|
+
-- 1. set_updated_at trigger function
|
|
27
|
+
-- Maintains updated_at on every UPDATE without adapter involvement.
|
|
28
|
+
-- ============================================================================
|
|
29
|
+
|
|
30
|
+
create or replace function public.set_updated_at()
|
|
31
|
+
returns trigger
|
|
32
|
+
language plpgsql
|
|
33
|
+
as $$
|
|
34
|
+
begin
|
|
35
|
+
new.updated_at := now();
|
|
36
|
+
return new;
|
|
37
|
+
end;
|
|
38
|
+
$$;
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
-- ============================================================================
|
|
42
|
+
-- 2. user_profiles
|
|
43
|
+
-- Selva-specific extension of auth.users (display name, platform perms,
|
|
44
|
+
-- starred definitions, recent runs). Auto-seeded by trigger on signup.
|
|
45
|
+
-- ============================================================================
|
|
46
|
+
|
|
47
|
+
create table if not exists public.user_profiles (
|
|
48
|
+
user_id uuid primary key references auth.users(id) on delete cascade,
|
|
49
|
+
display_name text,
|
|
50
|
+
platform_permissions text[] not null default '{}',
|
|
51
|
+
starred_definitions uuid[] not null default '{}',
|
|
52
|
+
recent_runs jsonb not null default '[]',
|
|
53
|
+
created_at timestamptz not null default now(),
|
|
54
|
+
last_login_at timestamptz
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
create or replace function public.handle_new_auth_user()
|
|
58
|
+
returns trigger
|
|
59
|
+
language plpgsql
|
|
60
|
+
security definer
|
|
61
|
+
set search_path = public
|
|
62
|
+
as $$
|
|
63
|
+
begin
|
|
64
|
+
insert into public.user_profiles (user_id)
|
|
65
|
+
values (new.id)
|
|
66
|
+
on conflict (user_id) do nothing;
|
|
67
|
+
return new;
|
|
68
|
+
end;
|
|
69
|
+
$$;
|
|
70
|
+
|
|
71
|
+
drop trigger if exists trg_auth_user_created on auth.users;
|
|
72
|
+
create trigger trg_auth_user_created
|
|
73
|
+
after insert on auth.users
|
|
74
|
+
for each row execute function public.handle_new_auth_user();
|
|
75
|
+
|
|
76
|
+
alter table public.user_profiles enable row level security;
|
|
77
|
+
|
|
78
|
+
create policy "user_profiles: any authenticated can read"
|
|
79
|
+
on public.user_profiles for select
|
|
80
|
+
to authenticated
|
|
81
|
+
using (true);
|
|
82
|
+
|
|
83
|
+
create policy "user_profiles: users can update own"
|
|
84
|
+
on public.user_profiles for update
|
|
85
|
+
to authenticated
|
|
86
|
+
using (user_id = auth.uid())
|
|
87
|
+
with check (
|
|
88
|
+
user_id = auth.uid()
|
|
89
|
+
-- Users cannot escalate their own platform permissions; only service-role
|
|
90
|
+
-- writes (which bypass RLS) may change that column.
|
|
91
|
+
and platform_permissions is not distinct from (
|
|
92
|
+
select platform_permissions from public.user_profiles where user_id = auth.uid()
|
|
93
|
+
)
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
-- ============================================================================
|
|
98
|
+
-- 3. orgs, org_members, projects, project_members
|
|
99
|
+
-- Tenant-owned hierarchy. All carry created_by/updated_by/deleted_at;
|
|
100
|
+
-- reads filter deleted_at IS NULL.
|
|
101
|
+
-- ============================================================================
|
|
102
|
+
|
|
103
|
+
create table if not exists public.orgs (
|
|
104
|
+
id uuid primary key,
|
|
105
|
+
name text not null,
|
|
106
|
+
slug text not null unique,
|
|
107
|
+
owner_id uuid not null references auth.users(id) on delete cascade,
|
|
108
|
+
created_by uuid references auth.users(id) on delete set null,
|
|
109
|
+
updated_by uuid references auth.users(id) on delete set null,
|
|
110
|
+
created_at timestamptz not null default now(),
|
|
111
|
+
updated_at timestamptz not null default now(),
|
|
112
|
+
deleted_at timestamptz
|
|
113
|
+
);
|
|
114
|
+
create index if not exists idx_orgs_live on public.orgs(id) where deleted_at is null;
|
|
115
|
+
|
|
116
|
+
create table if not exists public.org_members (
|
|
117
|
+
org_id uuid not null references public.orgs(id) on delete cascade,
|
|
118
|
+
user_id uuid not null references auth.users(id) on delete cascade,
|
|
119
|
+
role text not null check (role in ('owner', 'admin', 'member')),
|
|
120
|
+
permissions text[] not null default '{}',
|
|
121
|
+
joined_at timestamptz not null default now(),
|
|
122
|
+
updated_at timestamptz not null default now(),
|
|
123
|
+
updated_by uuid references auth.users(id) on delete set null,
|
|
124
|
+
deleted_at timestamptz,
|
|
125
|
+
primary key (org_id, user_id)
|
|
126
|
+
);
|
|
127
|
+
create index if not exists idx_org_members_user
|
|
128
|
+
on public.org_members(user_id) where deleted_at is null;
|
|
129
|
+
|
|
130
|
+
create table if not exists public.projects (
|
|
131
|
+
id uuid primary key,
|
|
132
|
+
org_id uuid not null references public.orgs(id) on delete cascade,
|
|
133
|
+
name text not null,
|
|
134
|
+
slug text not null,
|
|
135
|
+
description text,
|
|
136
|
+
visibility text not null check (visibility in ('public', 'org', 'private')),
|
|
137
|
+
auto_join_on_upload boolean not null default false,
|
|
138
|
+
owner_id uuid not null references auth.users(id) on delete cascade,
|
|
139
|
+
created_by uuid references auth.users(id) on delete set null,
|
|
140
|
+
updated_by uuid references auth.users(id) on delete set null,
|
|
141
|
+
created_at timestamptz not null default now(),
|
|
142
|
+
updated_at timestamptz not null default now(),
|
|
143
|
+
deleted_at timestamptz,
|
|
144
|
+
unique (org_id, slug),
|
|
145
|
+
-- Spec §4: commons mode requires visibility=public.
|
|
146
|
+
constraint projects_commons_requires_public check (
|
|
147
|
+
auto_join_on_upload = false or visibility = 'public'
|
|
148
|
+
)
|
|
149
|
+
);
|
|
150
|
+
create index if not exists idx_projects_org
|
|
151
|
+
on public.projects(org_id) where deleted_at is null;
|
|
152
|
+
-- Case-insensitive unique name per org (defense beyond slug uniqueness).
|
|
153
|
+
create unique index if not exists projects_org_name_unique
|
|
154
|
+
on public.projects (org_id, lower(name));
|
|
155
|
+
|
|
156
|
+
create table if not exists public.project_members (
|
|
157
|
+
project_id uuid not null references public.projects(id) on delete cascade,
|
|
158
|
+
user_id uuid not null references auth.users(id) on delete cascade,
|
|
159
|
+
role text not null check (role in ('owner', 'editor', 'viewer')),
|
|
160
|
+
joined_at timestamptz not null default now(),
|
|
161
|
+
updated_at timestamptz not null default now(),
|
|
162
|
+
updated_by uuid references auth.users(id) on delete set null,
|
|
163
|
+
deleted_at timestamptz,
|
|
164
|
+
primary key (project_id, user_id)
|
|
165
|
+
);
|
|
166
|
+
create index if not exists idx_project_members_user
|
|
167
|
+
on public.project_members(user_id) where deleted_at is null;
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
-- ============================================================================
|
|
171
|
+
-- 4. updated_at triggers
|
|
172
|
+
-- ============================================================================
|
|
173
|
+
|
|
174
|
+
create trigger trg_orgs_updated_at before update on public.orgs
|
|
175
|
+
for each row execute function public.set_updated_at();
|
|
176
|
+
create trigger trg_projects_updated_at before update on public.projects
|
|
177
|
+
for each row execute function public.set_updated_at();
|
|
178
|
+
create trigger trg_org_members_updated_at before update on public.org_members
|
|
179
|
+
for each row execute function public.set_updated_at();
|
|
180
|
+
create trigger trg_project_members_updated_at before update on public.project_members
|
|
181
|
+
for each row execute function public.set_updated_at();
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
-- ============================================================================
|
|
185
|
+
-- 5. RLS helper functions
|
|
186
|
+
-- All SECURITY DEFINER so they bypass RLS on the tables they read (avoids
|
|
187
|
+
-- recursion). All filter deleted_at IS NULL.
|
|
188
|
+
-- ============================================================================
|
|
189
|
+
|
|
190
|
+
create or replace function public.is_instance_admin()
|
|
191
|
+
returns boolean
|
|
192
|
+
language sql stable security definer set search_path = public
|
|
193
|
+
as $$
|
|
194
|
+
select exists (
|
|
195
|
+
select 1 from public.user_profiles
|
|
196
|
+
where user_id = auth.uid() and 'instance_admin' = any(platform_permissions)
|
|
197
|
+
);
|
|
198
|
+
$$;
|
|
199
|
+
|
|
200
|
+
create or replace function public.is_org_member(o uuid)
|
|
201
|
+
returns boolean
|
|
202
|
+
language sql stable security definer set search_path = public
|
|
203
|
+
as $$
|
|
204
|
+
select exists (
|
|
205
|
+
select 1 from public.org_members
|
|
206
|
+
where org_id = o and user_id = auth.uid() and deleted_at is null
|
|
207
|
+
);
|
|
208
|
+
$$;
|
|
209
|
+
|
|
210
|
+
create or replace function public.is_org_admin(o uuid)
|
|
211
|
+
returns boolean
|
|
212
|
+
language sql stable security definer set search_path = public
|
|
213
|
+
as $$
|
|
214
|
+
select exists (
|
|
215
|
+
select 1 from public.org_members
|
|
216
|
+
where org_id = o and user_id = auth.uid() and deleted_at is null
|
|
217
|
+
and role in ('owner', 'admin')
|
|
218
|
+
);
|
|
219
|
+
$$;
|
|
220
|
+
|
|
221
|
+
create or replace function public.is_org_owner(o uuid)
|
|
222
|
+
returns boolean
|
|
223
|
+
language sql stable security definer set search_path = public
|
|
224
|
+
as $$
|
|
225
|
+
select exists (
|
|
226
|
+
select 1 from public.org_members
|
|
227
|
+
where org_id = o and user_id = auth.uid() and deleted_at is null
|
|
228
|
+
and role = 'owner'
|
|
229
|
+
);
|
|
230
|
+
$$;
|
|
231
|
+
|
|
232
|
+
create or replace function public.has_org_permission(o uuid, perm text)
|
|
233
|
+
returns boolean
|
|
234
|
+
language sql stable security definer set search_path = public
|
|
235
|
+
as $$
|
|
236
|
+
select public.is_instance_admin() or exists (
|
|
237
|
+
select 1 from public.org_members
|
|
238
|
+
where org_id = o and user_id = auth.uid() and deleted_at is null
|
|
239
|
+
and perm = any(permissions)
|
|
240
|
+
);
|
|
241
|
+
$$;
|
|
242
|
+
|
|
243
|
+
create or replace function public.is_project_member(p uuid)
|
|
244
|
+
returns boolean
|
|
245
|
+
language sql stable security definer set search_path = public
|
|
246
|
+
as $$
|
|
247
|
+
select exists (
|
|
248
|
+
select 1 from public.project_members
|
|
249
|
+
where project_id = p and user_id = auth.uid() and deleted_at is null
|
|
250
|
+
);
|
|
251
|
+
$$;
|
|
252
|
+
|
|
253
|
+
-- Spec §5 canView: private → member, org → org member, public → everyone.
|
|
254
|
+
create or replace function public.visible_project(p uuid)
|
|
255
|
+
returns boolean
|
|
256
|
+
language sql stable security definer set search_path = public
|
|
257
|
+
as $$
|
|
258
|
+
select public.is_instance_admin() or exists (
|
|
259
|
+
select 1
|
|
260
|
+
from public.projects
|
|
261
|
+
where id = p
|
|
262
|
+
and deleted_at is null
|
|
263
|
+
and (
|
|
264
|
+
visibility = 'public'
|
|
265
|
+
or (visibility = 'org' and public.is_org_member(org_id))
|
|
266
|
+
or public.is_project_member(id)
|
|
267
|
+
)
|
|
268
|
+
);
|
|
269
|
+
$$;
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
-- ============================================================================
|
|
273
|
+
-- 6. Org / project RLS policies
|
|
274
|
+
-- ============================================================================
|
|
275
|
+
|
|
276
|
+
alter table public.orgs enable row level security;
|
|
277
|
+
|
|
278
|
+
-- RLS gates "may this principal touch this row?", not "is the row in a state
|
|
279
|
+
-- worth showing?". Soft-delete filtering (`deleted_at is null`) belongs at the
|
|
280
|
+
-- query layer — every store already issues `.is('deleted_at', null)` on
|
|
281
|
+
-- user-facing reads. Putting the lifecycle check in the USING clause breaks
|
|
282
|
+
-- the soft-delete write itself: `UPDATE ... SET deleted_at = now() RETURNING`
|
|
283
|
+
-- triggers a PostgREST re-read against the SELECT policy, which then filters
|
|
284
|
+
-- out the just-tombstoned row and surfaces as "new row violates RLS". Same
|
|
285
|
+
-- trap for `revoked_at` on share_links below.
|
|
286
|
+
create policy "orgs: members and instance admins can read"
|
|
287
|
+
on public.orgs for select
|
|
288
|
+
to authenticated
|
|
289
|
+
using (public.is_instance_admin() or public.is_org_member(id));
|
|
290
|
+
|
|
291
|
+
create policy "orgs: authenticated can create their own"
|
|
292
|
+
on public.orgs for insert
|
|
293
|
+
to authenticated
|
|
294
|
+
with check (public.is_instance_admin() or owner_id = auth.uid());
|
|
295
|
+
|
|
296
|
+
create policy "orgs: owners and instance admins can update"
|
|
297
|
+
on public.orgs for update
|
|
298
|
+
to authenticated
|
|
299
|
+
using (public.is_instance_admin() or public.is_org_owner(id))
|
|
300
|
+
with check (public.is_instance_admin() or public.is_org_owner(id));
|
|
301
|
+
|
|
302
|
+
create policy "orgs: owners and instance admins can delete"
|
|
303
|
+
on public.orgs for delete
|
|
304
|
+
to authenticated
|
|
305
|
+
using (public.is_instance_admin() or public.is_org_owner(id));
|
|
306
|
+
|
|
307
|
+
alter table public.org_members enable row level security;
|
|
308
|
+
|
|
309
|
+
create policy "org_members: org members can read roster"
|
|
310
|
+
on public.org_members for select
|
|
311
|
+
to authenticated
|
|
312
|
+
using (public.is_instance_admin() or public.is_org_member(org_id));
|
|
313
|
+
|
|
314
|
+
create policy "org_members: admins can insert"
|
|
315
|
+
on public.org_members for insert
|
|
316
|
+
to authenticated
|
|
317
|
+
with check (public.is_instance_admin() or public.is_org_admin(org_id));
|
|
318
|
+
|
|
319
|
+
create policy "org_members: admins can update"
|
|
320
|
+
on public.org_members for update
|
|
321
|
+
to authenticated
|
|
322
|
+
using (public.is_instance_admin() or public.is_org_admin(org_id))
|
|
323
|
+
with check (public.is_instance_admin() or public.is_org_admin(org_id));
|
|
324
|
+
|
|
325
|
+
create policy "org_members: admins can delete"
|
|
326
|
+
on public.org_members for delete
|
|
327
|
+
to authenticated
|
|
328
|
+
using (public.is_instance_admin() or public.is_org_admin(org_id));
|
|
329
|
+
|
|
330
|
+
alter table public.projects enable row level security;
|
|
331
|
+
|
|
332
|
+
create policy "projects: visible to members"
|
|
333
|
+
on public.projects for select
|
|
334
|
+
to authenticated
|
|
335
|
+
using (public.visible_project(id));
|
|
336
|
+
|
|
337
|
+
create policy "projects: manage_projects can create"
|
|
338
|
+
on public.projects for insert
|
|
339
|
+
to authenticated
|
|
340
|
+
with check (public.has_org_permission(org_id, 'manage_projects'));
|
|
341
|
+
|
|
342
|
+
-- Project settings (name, slug, description, visibility, flags) are owner-only.
|
|
343
|
+
create policy "projects: owners can update"
|
|
344
|
+
on public.projects for update
|
|
345
|
+
to authenticated
|
|
346
|
+
using (public.is_instance_admin() or owner_id = auth.uid())
|
|
347
|
+
with check (public.is_instance_admin() or owner_id = auth.uid());
|
|
348
|
+
|
|
349
|
+
create policy "projects: owners can delete"
|
|
350
|
+
on public.projects for delete
|
|
351
|
+
to authenticated
|
|
352
|
+
using (public.is_instance_admin() or owner_id = auth.uid());
|
|
353
|
+
|
|
354
|
+
alter table public.project_members enable row level security;
|
|
355
|
+
|
|
356
|
+
create policy "project_members: visible to project members"
|
|
357
|
+
on public.project_members for select
|
|
358
|
+
to authenticated
|
|
359
|
+
using (public.is_instance_admin() or public.visible_project(project_id));
|
|
360
|
+
|
|
361
|
+
-- canManage (§5) collapses to owner-only; org admins no longer silently
|
|
362
|
+
-- manage project members.
|
|
363
|
+
create policy "project_members: managers can insert"
|
|
364
|
+
on public.project_members for insert
|
|
365
|
+
to authenticated
|
|
366
|
+
with check (
|
|
367
|
+
public.is_instance_admin()
|
|
368
|
+
or exists (
|
|
369
|
+
select 1 from public.projects p
|
|
370
|
+
where p.id = project_id and p.deleted_at is null and p.owner_id = auth.uid()
|
|
371
|
+
)
|
|
372
|
+
);
|
|
373
|
+
|
|
374
|
+
create policy "project_members: managers can update"
|
|
375
|
+
on public.project_members for update
|
|
376
|
+
to authenticated
|
|
377
|
+
using (
|
|
378
|
+
public.is_instance_admin()
|
|
379
|
+
or exists (
|
|
380
|
+
select 1 from public.projects p
|
|
381
|
+
where p.id = project_id and p.deleted_at is null and p.owner_id = auth.uid()
|
|
382
|
+
)
|
|
383
|
+
)
|
|
384
|
+
with check (
|
|
385
|
+
public.is_instance_admin()
|
|
386
|
+
or exists (
|
|
387
|
+
select 1 from public.projects p
|
|
388
|
+
where p.id = project_id and p.deleted_at is null and p.owner_id = auth.uid()
|
|
389
|
+
)
|
|
390
|
+
);
|
|
391
|
+
|
|
392
|
+
create policy "project_members: managers can delete"
|
|
393
|
+
on public.project_members for delete
|
|
394
|
+
to authenticated
|
|
395
|
+
using (
|
|
396
|
+
public.is_instance_admin()
|
|
397
|
+
or exists (
|
|
398
|
+
select 1 from public.projects p
|
|
399
|
+
where p.id = project_id and p.deleted_at is null and p.owner_id = auth.uid()
|
|
400
|
+
)
|
|
401
|
+
);
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
-- ============================================================================
|
|
405
|
+
-- 7. definitions + definition_versions (spec §6)
|
|
406
|
+
--
|
|
407
|
+
-- Versioning model: each upload creates an immutable definition_versions row.
|
|
408
|
+
-- definitions.live_version_id / draft_version_id point at versions; both are
|
|
409
|
+
-- ON DELETE RESTRICT so a referenced version cannot be deleted (deletion
|
|
410
|
+
-- protection enforced at the DB layer).
|
|
411
|
+
--
|
|
412
|
+
-- Per-version columns (file_ext, original_filename) live on definition_versions,
|
|
413
|
+
-- not on definitions — different versions can carry different uploaded shapes.
|
|
414
|
+
-- ============================================================================
|
|
415
|
+
|
|
416
|
+
create table if not exists public.definitions (
|
|
417
|
+
guid uuid primary key,
|
|
418
|
+
project_id uuid not null references public.projects(id) on delete cascade,
|
|
419
|
+
owner_id uuid not null references auth.users(id) on delete cascade,
|
|
420
|
+
created_by uuid references auth.users(id) on delete set null,
|
|
421
|
+
updated_by uuid references auth.users(id) on delete set null,
|
|
422
|
+
-- FKs to definition_versions added below once that table exists.
|
|
423
|
+
live_version_id uuid,
|
|
424
|
+
draft_version_id uuid,
|
|
425
|
+
compute_server_id uuid,
|
|
426
|
+
display_name text not null,
|
|
427
|
+
description text,
|
|
428
|
+
category text,
|
|
429
|
+
tags text[] not null default '{}',
|
|
430
|
+
cover_image text,
|
|
431
|
+
status text not null check (status in ('pending', 'draft', 'review', 'published', 'archived')),
|
|
432
|
+
run_count bigint not null default 0,
|
|
433
|
+
created_at timestamptz not null default now(),
|
|
434
|
+
updated_at timestamptz not null default now(),
|
|
435
|
+
deleted_at timestamptz
|
|
436
|
+
);
|
|
437
|
+
|
|
438
|
+
create index if not exists idx_definitions_project
|
|
439
|
+
on public.definitions(project_id) where deleted_at is null;
|
|
440
|
+
create index if not exists idx_definitions_status
|
|
441
|
+
on public.definitions(status) where deleted_at is null;
|
|
442
|
+
create index if not exists idx_definitions_pending_updated
|
|
443
|
+
on public.definitions(updated_at)
|
|
444
|
+
where status = 'pending' and deleted_at is null;
|
|
445
|
+
|
|
446
|
+
create trigger trg_definitions_updated_at before update on public.definitions
|
|
447
|
+
for each row execute function public.set_updated_at();
|
|
448
|
+
|
|
449
|
+
create table if not exists public.definition_versions (
|
|
450
|
+
id uuid primary key,
|
|
451
|
+
definition_guid uuid not null references public.definitions(guid) on delete cascade,
|
|
452
|
+
version_number integer not null check (version_number >= 1),
|
|
453
|
+
file_ext text not null check (file_ext in ('gh', 'ghx')),
|
|
454
|
+
file_key text not null,
|
|
455
|
+
original_filename text,
|
|
456
|
+
uploaded_by uuid not null references auth.users(id) on delete cascade,
|
|
457
|
+
uploaded_at timestamptz not null default now(),
|
|
458
|
+
unique (definition_guid, version_number)
|
|
459
|
+
);
|
|
460
|
+
|
|
461
|
+
create index if not exists idx_definition_versions_def
|
|
462
|
+
on public.definition_versions(definition_guid, version_number desc);
|
|
463
|
+
|
|
464
|
+
-- Spec §6 deletion protection: cannot delete a version while it's serving
|
|
465
|
+
-- either channel. Postgres raises 23503; the store maps that to 409.
|
|
466
|
+
alter table public.definitions
|
|
467
|
+
add constraint fk_definitions_live_version
|
|
468
|
+
foreign key (live_version_id) references public.definition_versions(id)
|
|
469
|
+
on delete restrict;
|
|
470
|
+
|
|
471
|
+
alter table public.definitions
|
|
472
|
+
add constraint fk_definitions_draft_version
|
|
473
|
+
foreign key (draft_version_id) references public.definition_versions(id)
|
|
474
|
+
on delete restrict;
|
|
475
|
+
|
|
476
|
+
-- Atomic run-count increment. Called after each successful solve. No-op on
|
|
477
|
+
-- missing or soft-deleted rows.
|
|
478
|
+
create or replace function public.increment_run_count(g uuid)
|
|
479
|
+
returns void
|
|
480
|
+
language sql security definer set search_path = public
|
|
481
|
+
as $$
|
|
482
|
+
update public.definitions
|
|
483
|
+
set run_count = run_count + 1
|
|
484
|
+
where guid = g and deleted_at is null;
|
|
485
|
+
$$;
|
|
486
|
+
grant execute on function public.increment_run_count(uuid) to authenticated, service_role;
|
|
487
|
+
|
|
488
|
+
alter table public.definitions enable row level security;
|
|
489
|
+
|
|
490
|
+
create policy "definitions: visible via project"
|
|
491
|
+
on public.definitions for select
|
|
492
|
+
to authenticated
|
|
493
|
+
using (public.visible_project(project_id));
|
|
494
|
+
|
|
495
|
+
create policy "definitions: editors can insert"
|
|
496
|
+
on public.definitions for insert
|
|
497
|
+
to authenticated
|
|
498
|
+
with check (
|
|
499
|
+
public.is_instance_admin()
|
|
500
|
+
-- Container mode: project editor/owner required.
|
|
501
|
+
or exists (
|
|
502
|
+
select 1 from public.project_members m
|
|
503
|
+
where m.project_id = definitions.project_id
|
|
504
|
+
and m.user_id = auth.uid()
|
|
505
|
+
and m.deleted_at is null
|
|
506
|
+
and m.role in ('owner', 'editor')
|
|
507
|
+
)
|
|
508
|
+
-- Commons mode: any authenticated user may create on a public + commons
|
|
509
|
+
-- project; they become its owner_id.
|
|
510
|
+
or exists (
|
|
511
|
+
select 1 from public.projects p
|
|
512
|
+
where p.id = definitions.project_id
|
|
513
|
+
and p.deleted_at is null
|
|
514
|
+
and p.visibility = 'public'
|
|
515
|
+
and p.auto_join_on_upload = true
|
|
516
|
+
and definitions.owner_id = auth.uid()
|
|
517
|
+
)
|
|
518
|
+
);
|
|
519
|
+
|
|
520
|
+
create policy "definitions: editors can update"
|
|
521
|
+
on public.definitions for update
|
|
522
|
+
to authenticated
|
|
523
|
+
using (
|
|
524
|
+
public.is_instance_admin()
|
|
525
|
+
or exists (
|
|
526
|
+
select 1 from public.project_members m
|
|
527
|
+
where m.project_id = definitions.project_id
|
|
528
|
+
and m.user_id = auth.uid()
|
|
529
|
+
and m.deleted_at is null
|
|
530
|
+
and m.role in ('owner', 'editor')
|
|
531
|
+
)
|
|
532
|
+
or exists (
|
|
533
|
+
select 1 from public.projects p
|
|
534
|
+
where p.id = definitions.project_id
|
|
535
|
+
and p.deleted_at is null
|
|
536
|
+
and p.auto_join_on_upload = true
|
|
537
|
+
and definitions.owner_id = auth.uid()
|
|
538
|
+
)
|
|
539
|
+
)
|
|
540
|
+
with check (
|
|
541
|
+
public.is_instance_admin()
|
|
542
|
+
or exists (
|
|
543
|
+
select 1 from public.project_members m
|
|
544
|
+
where m.project_id = definitions.project_id
|
|
545
|
+
and m.user_id = auth.uid()
|
|
546
|
+
and m.deleted_at is null
|
|
547
|
+
and m.role in ('owner', 'editor')
|
|
548
|
+
)
|
|
549
|
+
or exists (
|
|
550
|
+
select 1 from public.projects p
|
|
551
|
+
where p.id = definitions.project_id
|
|
552
|
+
and p.deleted_at is null
|
|
553
|
+
and p.auto_join_on_upload = true
|
|
554
|
+
and definitions.owner_id = auth.uid()
|
|
555
|
+
)
|
|
556
|
+
);
|
|
557
|
+
|
|
558
|
+
create policy "definitions: editors can delete"
|
|
559
|
+
on public.definitions for delete
|
|
560
|
+
to authenticated
|
|
561
|
+
using (
|
|
562
|
+
public.is_instance_admin()
|
|
563
|
+
or exists (
|
|
564
|
+
select 1 from public.project_members m
|
|
565
|
+
where m.project_id = definitions.project_id
|
|
566
|
+
and m.user_id = auth.uid()
|
|
567
|
+
and m.deleted_at is null
|
|
568
|
+
and m.role in ('owner', 'editor')
|
|
569
|
+
)
|
|
570
|
+
or exists (
|
|
571
|
+
select 1 from public.projects p
|
|
572
|
+
where p.id = definitions.project_id
|
|
573
|
+
and p.deleted_at is null
|
|
574
|
+
and p.auto_join_on_upload = true
|
|
575
|
+
and definitions.owner_id = auth.uid()
|
|
576
|
+
)
|
|
577
|
+
);
|
|
578
|
+
|
|
579
|
+
alter table public.definition_versions enable row level security;
|
|
580
|
+
|
|
581
|
+
-- Versions inherit visibility from the parent definition (which inherits
|
|
582
|
+
-- from the parent project). Versions are immutable — no UPDATE policy.
|
|
583
|
+
create policy "definition_versions: visible via parent"
|
|
584
|
+
on public.definition_versions for select
|
|
585
|
+
to authenticated
|
|
586
|
+
using (
|
|
587
|
+
exists (
|
|
588
|
+
select 1 from public.definitions d
|
|
589
|
+
where d.guid = definition_versions.definition_guid
|
|
590
|
+
and d.deleted_at is null
|
|
591
|
+
and public.visible_project(d.project_id)
|
|
592
|
+
)
|
|
593
|
+
);
|
|
594
|
+
|
|
595
|
+
create policy "definition_versions: editors can insert"
|
|
596
|
+
on public.definition_versions for insert
|
|
597
|
+
to authenticated
|
|
598
|
+
with check (
|
|
599
|
+
public.is_instance_admin()
|
|
600
|
+
or exists (
|
|
601
|
+
select 1
|
|
602
|
+
from public.definitions d
|
|
603
|
+
join public.project_members m on m.project_id = d.project_id
|
|
604
|
+
where d.guid = definition_guid
|
|
605
|
+
and d.deleted_at is null
|
|
606
|
+
and m.user_id = auth.uid()
|
|
607
|
+
and m.deleted_at is null
|
|
608
|
+
and m.role in ('owner', 'editor')
|
|
609
|
+
)
|
|
610
|
+
or exists (
|
|
611
|
+
select 1
|
|
612
|
+
from public.definitions d
|
|
613
|
+
join public.projects p on p.id = d.project_id
|
|
614
|
+
where d.guid = definition_guid
|
|
615
|
+
and d.deleted_at is null
|
|
616
|
+
and p.deleted_at is null
|
|
617
|
+
and p.auto_join_on_upload = true
|
|
618
|
+
and d.owner_id = auth.uid()
|
|
619
|
+
and definition_versions.uploaded_by = auth.uid()
|
|
620
|
+
)
|
|
621
|
+
);
|
|
622
|
+
|
|
623
|
+
-- Without an explicit DELETE policy, RLS silently affects 0 rows on every
|
|
624
|
+
-- `delete from definition_versions` — `SupabaseDefinitionStore.deleteVersion`
|
|
625
|
+
-- returns success without removing anything, and the FK ON DELETE RESTRICT on
|
|
626
|
+
-- `definitions.live_version_id` / `draft_version_id` never gets a chance to
|
|
627
|
+
-- raise the 23503 the store maps to a 409. Same authority as INSERT.
|
|
628
|
+
create policy "definition_versions: editors can delete"
|
|
629
|
+
on public.definition_versions for delete
|
|
630
|
+
to authenticated
|
|
631
|
+
using (
|
|
632
|
+
public.is_instance_admin()
|
|
633
|
+
or exists (
|
|
634
|
+
select 1
|
|
635
|
+
from public.definitions d
|
|
636
|
+
join public.project_members m on m.project_id = d.project_id
|
|
637
|
+
where d.guid = definition_versions.definition_guid
|
|
638
|
+
and d.deleted_at is null
|
|
639
|
+
and m.user_id = auth.uid()
|
|
640
|
+
and m.deleted_at is null
|
|
641
|
+
and m.role in ('owner', 'editor')
|
|
642
|
+
)
|
|
643
|
+
or exists (
|
|
644
|
+
select 1
|
|
645
|
+
from public.definitions d
|
|
646
|
+
join public.projects p on p.id = d.project_id
|
|
647
|
+
where d.guid = definition_versions.definition_guid
|
|
648
|
+
and d.deleted_at is null
|
|
649
|
+
and p.deleted_at is null
|
|
650
|
+
and p.auto_join_on_upload = true
|
|
651
|
+
and d.owner_id = auth.uid()
|
|
652
|
+
)
|
|
653
|
+
);
|
|
654
|
+
|
|
655
|
+
|
|
656
|
+
-- ============================================================================
|
|
657
|
+
-- 8. invites
|
|
658
|
+
-- ============================================================================
|
|
659
|
+
|
|
660
|
+
-- `org_id ... on delete cascade` only fires when the org is HARD-deleted.
|
|
661
|
+
-- The standard `deleteOrg` path is a soft-delete (UPDATE deleted_at), so the
|
|
662
|
+
-- SupabaseOrgStore.deleteOrg cascade explicitly DELETEs invites for the org;
|
|
663
|
+
-- see SupabaseOrgStore.ts. Same pattern for compute_servers below.
|
|
664
|
+
-- Invite tokens are HMAC-hashed at the route layer (the selva app's
|
|
665
|
+
-- `lib/server/invites/token.server.ts`) before reaching the store — the raw
|
|
666
|
+
-- token is shown to the admin once and never persisted. A DB-only leak
|
|
667
|
+
-- therefore can't be replayed without the instance's INVITE_TOKEN_SECRET.
|
|
668
|
+
-- Mirrors the share-link design (`share_links.token_hash`).
|
|
669
|
+
create table if not exists public.invites (
|
|
670
|
+
id uuid primary key,
|
|
671
|
+
token_hash text not null unique,
|
|
672
|
+
email text not null,
|
|
673
|
+
org_id uuid not null references public.orgs(id) on delete cascade,
|
|
674
|
+
org_role text not null check (org_role in ('owner', 'admin', 'member')),
|
|
675
|
+
org_permissions text[] not null default '{}',
|
|
676
|
+
invited_by uuid not null references auth.users(id) on delete cascade,
|
|
677
|
+
created_at timestamptz not null default now(),
|
|
678
|
+
expires_at timestamptz not null,
|
|
679
|
+
accepted_at timestamptz,
|
|
680
|
+
accepted_by_user_id uuid references auth.users(id) on delete set null
|
|
681
|
+
);
|
|
682
|
+
|
|
683
|
+
create index if not exists idx_invites_org on public.invites(org_id, created_at desc);
|
|
684
|
+
|
|
685
|
+
alter table public.invites enable row level security;
|
|
686
|
+
|
|
687
|
+
create policy "invites: manage_org_members can read org invites"
|
|
688
|
+
on public.invites for select
|
|
689
|
+
to authenticated
|
|
690
|
+
using (public.has_org_permission(org_id, 'manage_org_members'));
|
|
691
|
+
|
|
692
|
+
create policy "invites: manage_org_members can insert"
|
|
693
|
+
on public.invites for insert
|
|
694
|
+
to authenticated
|
|
695
|
+
with check (public.has_org_permission(org_id, 'manage_org_members'));
|
|
696
|
+
|
|
697
|
+
create policy "invites: manage_org_members can update"
|
|
698
|
+
on public.invites for update
|
|
699
|
+
to authenticated
|
|
700
|
+
using (public.has_org_permission(org_id, 'manage_org_members'))
|
|
701
|
+
with check (public.has_org_permission(org_id, 'manage_org_members'));
|
|
702
|
+
|
|
703
|
+
create policy "invites: manage_org_members can delete"
|
|
704
|
+
on public.invites for delete
|
|
705
|
+
to authenticated
|
|
706
|
+
using (public.has_org_permission(org_id, 'manage_org_members'));
|
|
707
|
+
|
|
708
|
+
-- Hash-gated read via SECURITY DEFINER so the token itself is the capability.
|
|
709
|
+
-- Caller hashes the raw URL token at the route layer and passes the digest;
|
|
710
|
+
-- the function just looks up the row. Returns SETOF so a missing match
|
|
711
|
+
-- becomes an empty array (not a composite of nulls).
|
|
712
|
+
create or replace function public.get_invite_by_token_hash(h text)
|
|
713
|
+
returns setof public.invites
|
|
714
|
+
language sql stable security definer set search_path = public
|
|
715
|
+
as $$
|
|
716
|
+
select *
|
|
717
|
+
from public.invites
|
|
718
|
+
where token_hash = h
|
|
719
|
+
and accepted_at is null
|
|
720
|
+
and expires_at > now()
|
|
721
|
+
limit 1;
|
|
722
|
+
$$;
|
|
723
|
+
grant execute on function public.get_invite_by_token_hash(text) to anon, authenticated, service_role;
|
|
724
|
+
|
|
725
|
+
|
|
726
|
+
-- ============================================================================
|
|
727
|
+
-- 9. compute_servers (spec §3 — platform vs. org-private)
|
|
728
|
+
--
|
|
729
|
+
-- Servers are discriminated by `scope`:
|
|
730
|
+
-- * scope = 'platform' → managed by `manage_compute`. `sharedWith = 'all'`
|
|
731
|
+
-- (db: shared_with_all = true) exposes to every org; otherwise the
|
|
732
|
+
-- per-org allowlist lives in `compute_server_shares`. The global
|
|
733
|
+
-- default is always visible regardless (the "baseline" floor).
|
|
734
|
+
-- * scope = 'org' → org-private. `owner_org_id` is required.
|
|
735
|
+
-- Visible only to members of that org. Gated by the platform flag
|
|
736
|
+
-- ALLOW_ORG_COMPUTE_OVERRIDE at the TS layer; DB policies still accept
|
|
737
|
+
-- writes so flipping the flag on doesn't require a policy change.
|
|
738
|
+
--
|
|
739
|
+
-- Defaults are layered:
|
|
740
|
+
-- * compute_server_platform_default (singleton) — global default. Must
|
|
741
|
+
-- reference a platform row.
|
|
742
|
+
-- * compute_server_org_defaults — per-org override. Must reference a
|
|
743
|
+
-- server visible to that org (validated at the TS layer).
|
|
744
|
+
--
|
|
745
|
+
-- Cascade note: the `owner_org_id` FK is `on delete cascade`, but
|
|
746
|
+
-- `deleteOrg` is a soft-delete and never triggers it. SupabaseOrgStore
|
|
747
|
+
-- explicitly DELETEs from compute_server_org_defaults, compute_server_shares,
|
|
748
|
+
-- and compute_servers (where owner_org_id matches) for the org. The FK
|
|
749
|
+
-- cascade only matters for the eventual hard-delete janitor.
|
|
750
|
+
-- ============================================================================
|
|
751
|
+
|
|
752
|
+
-- compute_servers — discriminated by `scope`.
|
|
753
|
+
-- * scope = 'platform': managed by `manage_compute`. `owner_org_id` must
|
|
754
|
+
-- be null. `shared_with_all = true` exposes the server to every org;
|
|
755
|
+
-- otherwise the per-org allowlist lives in `compute_server_shares`. The
|
|
756
|
+
-- global default in `compute_server_platform_default` is *always*
|
|
757
|
+
-- visible regardless of share state (spec §3 — baseline floor).
|
|
758
|
+
-- * scope = 'org': managed by an org member with `manage_org_compute`.
|
|
759
|
+
-- `owner_org_id` is required and visibility is implicit (only members
|
|
760
|
+
-- of `owner_org_id` can see it). Gated by the platform flag
|
|
761
|
+
-- ALLOW_ORG_COMPUTE_OVERRIDE at the TS layer.
|
|
762
|
+
create table if not exists public.compute_servers (
|
|
763
|
+
id uuid primary key,
|
|
764
|
+
scope text not null check (scope in ('platform', 'org')),
|
|
765
|
+
owner_org_id uuid references public.orgs(id) on delete cascade,
|
|
766
|
+
shared_with_all boolean not null default false,
|
|
767
|
+
label text not null,
|
|
768
|
+
server_url text not null,
|
|
769
|
+
api_key text,
|
|
770
|
+
timeout_ms integer,
|
|
771
|
+
retry_count integer,
|
|
772
|
+
created_at timestamptz not null default now(),
|
|
773
|
+
-- Scope-specific invariants.
|
|
774
|
+
check (
|
|
775
|
+
(scope = 'platform' and owner_org_id is null)
|
|
776
|
+
or (scope = 'org' and owner_org_id is not null and shared_with_all = false)
|
|
777
|
+
)
|
|
778
|
+
);
|
|
779
|
+
create index if not exists idx_compute_servers_owner_org on public.compute_servers(owner_org_id);
|
|
780
|
+
|
|
781
|
+
-- Per-org share allowlist for platform servers. Empty rows = dormant
|
|
782
|
+
-- platform server (admin-only) unless it's the global default.
|
|
783
|
+
create table if not exists public.compute_server_shares (
|
|
784
|
+
server_id uuid not null references public.compute_servers(id) on delete cascade,
|
|
785
|
+
org_id uuid not null references public.orgs(id) on delete cascade,
|
|
786
|
+
primary key (server_id, org_id)
|
|
787
|
+
);
|
|
788
|
+
create index if not exists idx_compute_server_shares_org on public.compute_server_shares(org_id);
|
|
789
|
+
|
|
790
|
+
-- Per-org default override (orgDefaults[orgId]).
|
|
791
|
+
create table if not exists public.compute_server_org_defaults (
|
|
792
|
+
org_id uuid primary key references public.orgs(id) on delete cascade,
|
|
793
|
+
default_server_id uuid references public.compute_servers(id) on delete set null
|
|
794
|
+
);
|
|
795
|
+
|
|
796
|
+
-- Single-row sentinel for the global default.
|
|
797
|
+
create table if not exists public.compute_server_platform_default (
|
|
798
|
+
singleton boolean primary key default true,
|
|
799
|
+
default_server_id uuid references public.compute_servers(id) on delete set null,
|
|
800
|
+
check (singleton)
|
|
801
|
+
);
|
|
802
|
+
insert into public.compute_server_platform_default (singleton, default_server_id)
|
|
803
|
+
values (true, null)
|
|
804
|
+
on conflict (singleton) do nothing;
|
|
805
|
+
|
|
806
|
+
alter table public.compute_servers enable row level security;
|
|
807
|
+
alter table public.compute_server_shares enable row level security;
|
|
808
|
+
alter table public.compute_server_org_defaults enable row level security;
|
|
809
|
+
alter table public.compute_server_platform_default enable row level security;
|
|
810
|
+
|
|
811
|
+
-- Read: instance_admin sees everything. Otherwise visibility follows §3:
|
|
812
|
+
-- * platform server with shared_with_all = true → all authenticated users
|
|
813
|
+
-- * platform server with a row in compute_server_shares for an org the
|
|
814
|
+
-- user belongs to → that user
|
|
815
|
+
-- * platform server that is the current global default → all (floor)
|
|
816
|
+
-- * org server → only members of owner_org_id
|
|
817
|
+
create policy "compute_servers: visibility-scoped read"
|
|
818
|
+
on public.compute_servers for select
|
|
819
|
+
to authenticated
|
|
820
|
+
using (
|
|
821
|
+
public.is_instance_admin()
|
|
822
|
+
or (scope = 'platform' and shared_with_all)
|
|
823
|
+
or (scope = 'platform' and id = (
|
|
824
|
+
select default_server_id from public.compute_server_platform_default where singleton
|
|
825
|
+
))
|
|
826
|
+
or (scope = 'platform' and exists (
|
|
827
|
+
select 1 from public.compute_server_shares s
|
|
828
|
+
where s.server_id = compute_servers.id and public.is_org_member(s.org_id)
|
|
829
|
+
))
|
|
830
|
+
or (scope = 'org' and public.is_org_member(owner_org_id))
|
|
831
|
+
);
|
|
832
|
+
|
|
833
|
+
-- Writes:
|
|
834
|
+
-- * scope = 'platform' → instance_admin only.
|
|
835
|
+
-- * scope = 'org' → manage_org_compute on owner_org_id.
|
|
836
|
+
create policy "compute_servers: scoped write"
|
|
837
|
+
on public.compute_servers for all
|
|
838
|
+
to authenticated
|
|
839
|
+
using (
|
|
840
|
+
public.is_instance_admin()
|
|
841
|
+
or (scope = 'org' and owner_org_id is not null
|
|
842
|
+
and public.has_org_permission(owner_org_id, 'manage_org_compute'))
|
|
843
|
+
)
|
|
844
|
+
with check (
|
|
845
|
+
public.is_instance_admin()
|
|
846
|
+
or (scope = 'org' and owner_org_id is not null
|
|
847
|
+
and public.has_org_permission(owner_org_id, 'manage_org_compute'))
|
|
848
|
+
);
|
|
849
|
+
|
|
850
|
+
create policy "compute_server_shares: read"
|
|
851
|
+
on public.compute_server_shares for select
|
|
852
|
+
to authenticated
|
|
853
|
+
using (public.is_instance_admin() or public.is_org_member(org_id));
|
|
854
|
+
|
|
855
|
+
create policy "compute_server_shares: admin write"
|
|
856
|
+
on public.compute_server_shares for all
|
|
857
|
+
to authenticated
|
|
858
|
+
using (public.is_instance_admin())
|
|
859
|
+
with check (public.is_instance_admin());
|
|
860
|
+
|
|
861
|
+
create policy "compute_server_org_defaults: members can read"
|
|
862
|
+
on public.compute_server_org_defaults for select
|
|
863
|
+
to authenticated
|
|
864
|
+
using (public.is_instance_admin() or public.is_org_member(org_id));
|
|
865
|
+
|
|
866
|
+
create policy "compute_server_org_defaults: manage_org_compute can write"
|
|
867
|
+
on public.compute_server_org_defaults for all
|
|
868
|
+
to authenticated
|
|
869
|
+
using (public.is_instance_admin() or public.has_org_permission(org_id, 'manage_org_compute'))
|
|
870
|
+
with check (public.is_instance_admin() or public.has_org_permission(org_id, 'manage_org_compute'));
|
|
871
|
+
|
|
872
|
+
create policy "compute_server_platform_default: read all"
|
|
873
|
+
on public.compute_server_platform_default for select
|
|
874
|
+
to authenticated
|
|
875
|
+
using (true);
|
|
876
|
+
|
|
877
|
+
create policy "compute_server_platform_default: admin write"
|
|
878
|
+
on public.compute_server_platform_default for all
|
|
879
|
+
to authenticated
|
|
880
|
+
using (public.is_instance_admin())
|
|
881
|
+
with check (public.is_instance_admin());
|
|
882
|
+
|
|
883
|
+
|
|
884
|
+
-- ============================================================================
|
|
885
|
+
-- 10. share_links (spec §7)
|
|
886
|
+
--
|
|
887
|
+
-- Per-definition tokens granting unauthenticated access to one
|
|
888
|
+
-- (definitionId, channel). token_hash is HMAC(SHARE_LINK_SECRET, raw); the
|
|
889
|
+
-- raw token is shown to the minter once at creation.
|
|
890
|
+
-- ============================================================================
|
|
891
|
+
|
|
892
|
+
create table if not exists public.share_links (
|
|
893
|
+
id uuid primary key,
|
|
894
|
+
definition_guid uuid not null references public.definitions(guid) on delete cascade,
|
|
895
|
+
channel text not null check (channel in ('live', 'draft')),
|
|
896
|
+
token_hash text not null unique,
|
|
897
|
+
name text,
|
|
898
|
+
created_by uuid not null references auth.users(id) on delete cascade,
|
|
899
|
+
created_at timestamptz not null default now(),
|
|
900
|
+
expires_at timestamptz,
|
|
901
|
+
revoked_at timestamptz,
|
|
902
|
+
allow_solve boolean not null default true,
|
|
903
|
+
max_solves integer check (max_solves is null or max_solves >= 1),
|
|
904
|
+
solve_count integer not null default 0
|
|
905
|
+
);
|
|
906
|
+
|
|
907
|
+
create index if not exists idx_share_links_definition
|
|
908
|
+
on public.share_links(definition_guid, created_at desc)
|
|
909
|
+
where revoked_at is null;
|
|
910
|
+
|
|
911
|
+
-- Atomic check-and-increment. Returns the new solve_count on success, NULL
|
|
912
|
+
-- when the cap was reached (no row updated) or the link is revoked /
|
|
913
|
+
-- expired / missing. Single-statement = no race even under concurrent solves.
|
|
914
|
+
create or replace function public.try_increment_share_link_solve_count(link_id uuid)
|
|
915
|
+
returns integer
|
|
916
|
+
language sql security definer set search_path = public
|
|
917
|
+
as $$
|
|
918
|
+
update public.share_links
|
|
919
|
+
set solve_count = solve_count + 1
|
|
920
|
+
where id = link_id
|
|
921
|
+
and revoked_at is null
|
|
922
|
+
and (expires_at is null or expires_at > now())
|
|
923
|
+
and (max_solves is null or solve_count < max_solves)
|
|
924
|
+
returning solve_count;
|
|
925
|
+
$$;
|
|
926
|
+
grant execute on function public.try_increment_share_link_solve_count(uuid)
|
|
927
|
+
to authenticated, anon, service_role;
|
|
928
|
+
|
|
929
|
+
alter table public.share_links enable row level security;
|
|
930
|
+
|
|
931
|
+
-- Read/insert/revoke: same authority that can edit the parent definition
|
|
932
|
+
-- (project owner/editor or commons-mode definition owner). Token resolution
|
|
933
|
+
-- uses the SECURITY DEFINER RPC + service-role and bypasses RLS — the token
|
|
934
|
+
-- IS the credential there.
|
|
935
|
+
|
|
936
|
+
create policy "share_links: editors can read"
|
|
937
|
+
on public.share_links for select
|
|
938
|
+
to authenticated
|
|
939
|
+
using (
|
|
940
|
+
public.is_instance_admin()
|
|
941
|
+
or exists (
|
|
942
|
+
select 1
|
|
943
|
+
from public.definitions d
|
|
944
|
+
join public.project_members m on m.project_id = d.project_id
|
|
945
|
+
where d.guid = share_links.definition_guid
|
|
946
|
+
and d.deleted_at is null
|
|
947
|
+
and m.user_id = auth.uid()
|
|
948
|
+
and m.deleted_at is null
|
|
949
|
+
and m.role in ('owner', 'editor')
|
|
950
|
+
)
|
|
951
|
+
or exists (
|
|
952
|
+
select 1
|
|
953
|
+
from public.definitions d
|
|
954
|
+
join public.projects p on p.id = d.project_id
|
|
955
|
+
where d.guid = share_links.definition_guid
|
|
956
|
+
and d.deleted_at is null
|
|
957
|
+
and p.deleted_at is null
|
|
958
|
+
and p.auto_join_on_upload = true
|
|
959
|
+
and d.owner_id = auth.uid()
|
|
960
|
+
)
|
|
961
|
+
);
|
|
962
|
+
|
|
963
|
+
create policy "share_links: editors can insert"
|
|
964
|
+
on public.share_links for insert
|
|
965
|
+
to authenticated
|
|
966
|
+
with check (
|
|
967
|
+
public.is_instance_admin()
|
|
968
|
+
or exists (
|
|
969
|
+
select 1
|
|
970
|
+
from public.definitions d
|
|
971
|
+
join public.project_members m on m.project_id = d.project_id
|
|
972
|
+
where d.guid = share_links.definition_guid
|
|
973
|
+
and d.deleted_at is null
|
|
974
|
+
and m.user_id = auth.uid()
|
|
975
|
+
and m.deleted_at is null
|
|
976
|
+
and m.role in ('owner', 'editor')
|
|
977
|
+
)
|
|
978
|
+
or exists (
|
|
979
|
+
select 1
|
|
980
|
+
from public.definitions d
|
|
981
|
+
join public.projects p on p.id = d.project_id
|
|
982
|
+
where d.guid = share_links.definition_guid
|
|
983
|
+
and d.deleted_at is null
|
|
984
|
+
and p.deleted_at is null
|
|
985
|
+
and p.auto_join_on_upload = true
|
|
986
|
+
and d.owner_id = auth.uid()
|
|
987
|
+
)
|
|
988
|
+
);
|
|
989
|
+
|
|
990
|
+
-- UPDATE policy covers `revoke` (set revoked_at). Same authority as insert.
|
|
991
|
+
create policy "share_links: editors can revoke"
|
|
992
|
+
on public.share_links for update
|
|
993
|
+
to authenticated
|
|
994
|
+
using (
|
|
995
|
+
public.is_instance_admin()
|
|
996
|
+
or exists (
|
|
997
|
+
select 1
|
|
998
|
+
from public.definitions d
|
|
999
|
+
join public.project_members m on m.project_id = d.project_id
|
|
1000
|
+
where d.guid = share_links.definition_guid
|
|
1001
|
+
and d.deleted_at is null
|
|
1002
|
+
and m.user_id = auth.uid()
|
|
1003
|
+
and m.deleted_at is null
|
|
1004
|
+
and m.role in ('owner', 'editor')
|
|
1005
|
+
)
|
|
1006
|
+
)
|
|
1007
|
+
with check (
|
|
1008
|
+
public.is_instance_admin()
|
|
1009
|
+
or exists (
|
|
1010
|
+
select 1
|
|
1011
|
+
from public.definitions d
|
|
1012
|
+
join public.project_members m on m.project_id = d.project_id
|
|
1013
|
+
where d.guid = share_links.definition_guid
|
|
1014
|
+
and d.deleted_at is null
|
|
1015
|
+
and m.user_id = auth.uid()
|
|
1016
|
+
and m.deleted_at is null
|
|
1017
|
+
and m.role in ('owner', 'editor')
|
|
1018
|
+
)
|
|
1019
|
+
);
|
|
1020
|
+
|
|
1021
|
+
|
|
1022
|
+
-- ============================================================================
|
|
1023
|
+
-- 11. Storage bucket policies
|
|
1024
|
+
--
|
|
1025
|
+
-- All mutations on both buckets go through the app server, which holds the
|
|
1026
|
+
-- service-role key (SupabaseStorageProvider) — service-role bypasses RLS.
|
|
1027
|
+
-- User-scoped JWTs (the `authenticated` role) get NO direct access to
|
|
1028
|
+
-- storage REST: a logged-in user could otherwise extract their cookie JWT,
|
|
1029
|
+
-- pair it with the public anon key, and call /storage/v1/object/... directly
|
|
1030
|
+
-- to read/overwrite/delete any tenant's objects, since storage.objects has
|
|
1031
|
+
-- no project/ownership column to filter on. Application-layer access is
|
|
1032
|
+
-- enforced server-side: private reads flow through /api/files (which uses
|
|
1033
|
+
-- the service-role client and runs project-membership checks), and public
|
|
1034
|
+
-- reads use the CDN endpoint via the anon role below.
|
|
1035
|
+
-- ============================================================================
|
|
1036
|
+
|
|
1037
|
+
-- selva-public is served directly from the CDN (cover images, brand assets);
|
|
1038
|
+
-- anonymous reads are required. Writes are service-role only.
|
|
1039
|
+
create policy "selva-public: anyone can read"
|
|
1040
|
+
on storage.objects for select
|
|
1041
|
+
to anon, authenticated
|
|
1042
|
+
using (bucket_id = 'selva-public');
|
|
1043
|
+
|
|
1044
|
+
-- selva-private is never accessed directly by users. No policies for the
|
|
1045
|
+
-- authenticated role are defined; service-role bypasses RLS for app writes
|
|
1046
|
+
-- and proxied reads through /api/files.
|
|
1047
|
+
|
|
1048
|
+
|
|
1049
|
+
-- ============================================================================
|
|
1050
|
+
-- 12. audit_events
|
|
1051
|
+
-- Persistent sink for the domain events defined in `@selvajs/platform/events`.
|
|
1052
|
+
-- Every successful mutation in a data store emits one row here via
|
|
1053
|
+
-- `SupabaseEventSink`. Read-only from the application's point of view —
|
|
1054
|
+
-- mutations come exclusively from the sink writer.
|
|
1055
|
+
--
|
|
1056
|
+
-- Schema is intentionally generic: type + actor + timestamp + JSONB payload
|
|
1057
|
+
-- (the entire DomainEvent serialized). New event variants don't require a
|
|
1058
|
+
-- column migration — only the union in events/interface.ts.
|
|
1059
|
+
--
|
|
1060
|
+
-- The UI for viewing this remains deferred (Permissions.md §12); the table
|
|
1061
|
+
-- exists today so audit data is captured from the moment the system goes live.
|
|
1062
|
+
-- ============================================================================
|
|
1063
|
+
|
|
1064
|
+
create table if not exists public.audit_events (
|
|
1065
|
+
id uuid primary key default gen_random_uuid(),
|
|
1066
|
+
type text not null,
|
|
1067
|
+
actor_id text not null,
|
|
1068
|
+
occurred_at timestamptz not null default now(),
|
|
1069
|
+
data jsonb not null
|
|
1070
|
+
);
|
|
1071
|
+
|
|
1072
|
+
create index if not exists audit_events_occurred_at_idx
|
|
1073
|
+
on public.audit_events (occurred_at desc);
|
|
1074
|
+
|
|
1075
|
+
create index if not exists audit_events_type_occurred_at_idx
|
|
1076
|
+
on public.audit_events (type, occurred_at desc);
|
|
1077
|
+
|
|
1078
|
+
create index if not exists audit_events_actor_occurred_at_idx
|
|
1079
|
+
on public.audit_events (actor_id, occurred_at desc);
|
|
1080
|
+
|
|
1081
|
+
-- RLS: writes always go through service-role (the sink uses the service
|
|
1082
|
+
-- client). Authenticated users have NO read access today — the audit-log
|
|
1083
|
+
-- viewer UI will land later with its own instance_admin-only policy.
|
|
1084
|
+
alter table public.audit_events enable row level security;
|