@selvajs/supabase-provider 0.20.0-beta.4 → 0.20.0-beta.6

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.
Files changed (64) hide show
  1. package/README.md +3 -1
  2. package/dist/auth/SupabaseAuthProvider.d.ts.map +1 -1
  3. package/dist/auth/SupabaseAuthProvider.js +18 -39
  4. package/dist/auth/SupabaseAuthProvider.js.map +1 -1
  5. package/dist/config.d.ts +5 -0
  6. package/dist/config.d.ts.map +1 -0
  7. package/dist/config.js +126 -0
  8. package/dist/config.js.map +1 -0
  9. package/dist/data/SupabaseAuditQuery.js +1 -1
  10. package/dist/data/SupabaseAuditQuery.js.map +1 -1
  11. package/dist/data/SupabaseComputeServerStore.d.ts +14 -5
  12. package/dist/data/SupabaseComputeServerStore.d.ts.map +1 -1
  13. package/dist/data/SupabaseComputeServerStore.js +103 -80
  14. package/dist/data/SupabaseComputeServerStore.js.map +1 -1
  15. package/dist/data/SupabaseDataProvider.d.ts.map +1 -1
  16. package/dist/data/SupabaseDataProvider.js +5 -3
  17. package/dist/data/SupabaseDataProvider.js.map +1 -1
  18. package/dist/data/SupabaseDefinitionStore.js +7 -6
  19. package/dist/data/SupabaseDefinitionStore.js.map +1 -1
  20. package/dist/data/SupabaseInviteStore.d.ts +1 -1
  21. package/dist/data/SupabaseInviteStore.d.ts.map +1 -1
  22. package/dist/data/SupabaseInviteStore.js +3 -2
  23. package/dist/data/SupabaseInviteStore.js.map +1 -1
  24. package/dist/data/SupabaseOrgStore.d.ts.map +1 -1
  25. package/dist/data/SupabaseOrgStore.js +10 -34
  26. package/dist/data/SupabaseOrgStore.js.map +1 -1
  27. package/dist/data/SupabaseProjectStore.d.ts.map +1 -1
  28. package/dist/data/SupabaseProjectStore.js +15 -12
  29. package/dist/data/SupabaseProjectStore.js.map +1 -1
  30. package/dist/data/SupabaseShareLinkStore.d.ts.map +1 -1
  31. package/dist/data/SupabaseShareLinkStore.js +34 -29
  32. package/dist/data/SupabaseShareLinkStore.js.map +1 -1
  33. package/dist/data/SupabaseSolveMetricSink.d.ts +11 -3
  34. package/dist/data/SupabaseSolveMetricSink.d.ts.map +1 -1
  35. package/dist/data/SupabaseSolveMetricSink.js +38 -14
  36. package/dist/data/SupabaseSolveMetricSink.js.map +1 -1
  37. package/dist/data/errors.d.ts.map +1 -1
  38. package/dist/data/errors.js +3 -0
  39. package/dist/data/errors.js.map +1 -1
  40. package/dist/data/migrationHead.d.ts +1 -1
  41. package/dist/data/migrationHead.js +1 -1
  42. package/dist/data/pagination.d.ts +2 -3
  43. package/dist/data/pagination.d.ts.map +1 -1
  44. package/dist/data/pagination.js +6 -15
  45. package/dist/data/pagination.js.map +1 -1
  46. package/dist/permissions/SupabasePlatformPermissionStore.d.ts +2 -2
  47. package/dist/permissions/SupabasePlatformPermissionStore.d.ts.map +1 -1
  48. package/dist/permissions/SupabasePlatformPermissionStore.js +10 -49
  49. package/dist/permissions/SupabasePlatformPermissionStore.js.map +1 -1
  50. package/dist/storage/SupabaseStorageProvider.d.ts +3 -2
  51. package/dist/storage/SupabaseStorageProvider.d.ts.map +1 -1
  52. package/dist/storage/SupabaseStorageProvider.js +30 -19
  53. package/dist/storage/SupabaseStorageProvider.js.map +1 -1
  54. package/dist/userProfile/SupabaseUserProfileProvider.d.ts +7 -0
  55. package/dist/userProfile/SupabaseUserProfileProvider.d.ts.map +1 -1
  56. package/dist/userProfile/SupabaseUserProfileProvider.js +9 -19
  57. package/dist/userProfile/SupabaseUserProfileProvider.js.map +1 -1
  58. package/package.json +9 -3
  59. package/supabase/migrations/20261101152228_selva_revoke_privileged_functions.sql +44 -0
  60. package/supabase/migrations/20261101160000_selva_rls_is_the_boundary.sql +527 -0
  61. package/supabase/migrations/20261101170000_selva_compute_api_key_private.sql +14 -0
  62. package/supabase/migrations/20261101180000_selva_erasure_keeps_tenant_data.sql +28 -0
  63. package/supabase/migrations/20261101190000_selva_atomic_compute_save.sql +136 -0
  64. package/supabase/migrations/20261101200000_selva_last_admin_lock_order.sql +62 -0
@@ -0,0 +1,527 @@
1
+ -- ============================================================================
2
+ -- RLS is the boundary, and it matches `rules.ts`
3
+ --
4
+ -- The anon key is publishable, so anyone holding a session JWT can reach
5
+ -- PostgREST without going through Selva. Every rule the app enforces on a write
6
+ -- has to hold here on its own.
7
+ --
8
+ -- `public` still ignores ALLOW_CROSS_ORG_PUBLIC: Postgres cannot read the app's
9
+ -- env, so the app stays authoritative for that flag.
10
+ -- ============================================================================
11
+
12
+ -- ----------------------------------------------------------------------------
13
+ -- 1. Membership helpers stay inside a delegated session's token org
14
+ -- ----------------------------------------------------------------------------
15
+
16
+ -- A token for org A runs under its owner's JWT, so a helper that only looks at
17
+ -- `auth.uid()` also answers yes for every other org that user belongs to.
18
+ create or replace function selva.in_token_scope(o uuid)
19
+ returns boolean
20
+ language sql stable
21
+ as $$
22
+ select selva.delegated_org() is null or selva.delegated_org() = o;
23
+ $$;
24
+
25
+ create or replace function selva.is_org_member(o uuid)
26
+ returns boolean
27
+ language sql stable security definer set search_path = public
28
+ as $$
29
+ select selva.in_token_scope(o) and exists (
30
+ select 1 from selva.org_members
31
+ where org_id = o and user_id = auth.uid() and deleted_at is null
32
+ );
33
+ $$;
34
+
35
+ create or replace function selva.is_org_admin(o uuid)
36
+ returns boolean
37
+ language sql stable security definer set search_path = public
38
+ as $$
39
+ select selva.in_token_scope(o) and exists (
40
+ select 1 from selva.org_members
41
+ where org_id = o and user_id = auth.uid() and deleted_at is null
42
+ and role in ('owner', 'admin')
43
+ );
44
+ $$;
45
+
46
+ create or replace function selva.is_org_owner(o uuid)
47
+ returns boolean
48
+ language sql stable security definer set search_path = public
49
+ as $$
50
+ select selva.in_token_scope(o) and exists (
51
+ select 1 from selva.org_members
52
+ where org_id = o and user_id = auth.uid() and deleted_at is null
53
+ and role = 'owner'
54
+ );
55
+ $$;
56
+
57
+ create or replace function selva.has_org_permission(o uuid, perm text)
58
+ returns boolean
59
+ language sql stable security definer set search_path = public
60
+ as $$
61
+ select selva.is_instance_admin() or (selva.in_token_scope(o) and exists (
62
+ select 1 from selva.org_members
63
+ where org_id = o and user_id = auth.uid() and deleted_at is null
64
+ and perm = any(permissions)
65
+ ));
66
+ $$;
67
+
68
+ create or replace function selva.is_project_member(p uuid)
69
+ returns boolean
70
+ language sql stable security definer set search_path = public
71
+ as $$
72
+ select exists (
73
+ select 1
74
+ from selva.project_members m
75
+ join selva.projects proj on proj.id = m.project_id
76
+ where m.project_id = p and m.user_id = auth.uid() and m.deleted_at is null
77
+ and selva.in_token_scope(proj.org_id)
78
+ );
79
+ $$;
80
+
81
+ create or replace function selva.is_project_owner(p uuid)
82
+ returns boolean
83
+ language sql stable security definer set search_path = public
84
+ as $$
85
+ select exists (
86
+ select 1
87
+ from selva.project_members m
88
+ join selva.projects proj on proj.id = m.project_id
89
+ where m.project_id = p and m.user_id = auth.uid() and m.deleted_at is null
90
+ and m.role = 'owner'
91
+ and selva.in_token_scope(proj.org_id)
92
+ );
93
+ $$;
94
+
95
+ create or replace function selva.is_project_editor(p uuid)
96
+ returns boolean
97
+ language sql stable security definer set search_path = public
98
+ as $$
99
+ select exists (
100
+ select 1
101
+ from selva.project_members m
102
+ join selva.projects proj on proj.id = m.project_id
103
+ where m.project_id = p and m.user_id = auth.uid() and m.deleted_at is null
104
+ and m.role in ('owner', 'editor')
105
+ and selva.in_token_scope(proj.org_id)
106
+ );
107
+ $$;
108
+
109
+ -- A creator who left the org keeps `owner_id`, which must not bring them back.
110
+ create or replace function selva.may_seed_project_owner(p uuid)
111
+ returns boolean
112
+ language sql stable security definer set search_path = public
113
+ as $$
114
+ select exists (
115
+ select 1 from selva.projects proj
116
+ where proj.id = p
117
+ and proj.deleted_at is null
118
+ and selva.is_org_member(proj.org_id)
119
+ and (proj.owner_id = auth.uid() or selva.is_org_admin(proj.org_id))
120
+ );
121
+ $$;
122
+
123
+ -- ----------------------------------------------------------------------------
124
+ -- 2. Commons authority follows `canEditDefinition`
125
+ -- ----------------------------------------------------------------------------
126
+
127
+ -- `ownerId` is stamped at upload and never revisited, so it only counts for as
128
+ -- long as the uploader belongs to the org.
129
+ create or replace function selva.is_commons_contributor(p uuid, definition_owner uuid)
130
+ returns boolean
131
+ language sql stable security definer set search_path = public
132
+ as $$
133
+ select definition_owner = auth.uid() and exists (
134
+ select 1 from selva.projects proj
135
+ where proj.id = p
136
+ and proj.deleted_at is null
137
+ and proj.auto_join_on_upload
138
+ and selva.is_org_member(proj.org_id)
139
+ );
140
+ $$;
141
+
142
+ -- An upload is several writes, and an outsider may make them on a public commons
143
+ -- without ever being a contributor, so this also carries the insert and the
144
+ -- first version.
145
+ create or replace function selva.may_upload_to_commons(p uuid, definition_owner uuid)
146
+ returns boolean
147
+ language sql stable security definer set search_path = public
148
+ as $$
149
+ select definition_owner = auth.uid() and selva.visible_project(p) and exists (
150
+ select 1 from selva.projects proj
151
+ where proj.id = p
152
+ and proj.deleted_at is null
153
+ and proj.auto_join_on_upload
154
+ and selva.in_token_scope(proj.org_id)
155
+ );
156
+ $$;
157
+
158
+ create or replace function selva.is_definition_editor(g uuid)
159
+ returns boolean
160
+ language sql stable security definer set search_path = public
161
+ as $$
162
+ select exists (
163
+ select 1 from selva.definitions d
164
+ where d.guid = g and d.deleted_at is null and selva.is_project_editor(d.project_id)
165
+ );
166
+ $$;
167
+
168
+ create or replace function selva.is_definition_contributor(g uuid)
169
+ returns boolean
170
+ language sql stable security definer set search_path = public
171
+ as $$
172
+ select exists (
173
+ select 1 from selva.definitions d
174
+ where d.guid = g and d.deleted_at is null
175
+ and selva.is_commons_contributor(d.project_id, d.owner_id)
176
+ );
177
+ $$;
178
+
179
+ -- The window closes when a version is attached.
180
+ create or replace function selva.is_unfinished_commons_upload(g uuid)
181
+ returns boolean
182
+ language sql stable security definer set search_path = public
183
+ as $$
184
+ select exists (
185
+ select 1 from selva.definitions d
186
+ where d.guid = g and d.deleted_at is null
187
+ and d.live_version_id is null and d.draft_version_id is null
188
+ and selva.may_upload_to_commons(d.project_id, d.owner_id)
189
+ );
190
+ $$;
191
+
192
+ drop policy if exists "definitions: editors can insert" on selva.definitions;
193
+ create policy "definitions: editors can insert"
194
+ on selva.definitions for insert
195
+ to authenticated
196
+ with check (
197
+ selva.is_instance_admin()
198
+ or selva.is_project_editor(project_id)
199
+ or selva.may_upload_to_commons(project_id, owner_id)
200
+ );
201
+
202
+ drop policy if exists "definitions: editors can update" on selva.definitions;
203
+ create policy "definitions: editors can update"
204
+ on selva.definitions for update
205
+ to authenticated
206
+ using (
207
+ selva.is_instance_admin()
208
+ or selva.is_project_editor(project_id)
209
+ or selva.is_commons_contributor(project_id, owner_id)
210
+ or selva.is_unfinished_commons_upload(guid)
211
+ )
212
+ with check (
213
+ selva.is_instance_admin()
214
+ or selva.is_project_editor(project_id)
215
+ or selva.is_commons_contributor(project_id, owner_id)
216
+ or selva.may_upload_to_commons(project_id, owner_id)
217
+ );
218
+
219
+ drop policy if exists "definitions: editors can delete" on selva.definitions;
220
+ create policy "definitions: editors can delete"
221
+ on selva.definitions for delete
222
+ to authenticated
223
+ using (
224
+ selva.is_instance_admin()
225
+ or selva.is_project_editor(project_id)
226
+ or selva.is_commons_contributor(project_id, owner_id)
227
+ or selva.is_unfinished_commons_upload(guid)
228
+ );
229
+
230
+ drop policy if exists "definition_versions: editors can insert" on selva.definition_versions;
231
+ create policy "definition_versions: editors can insert"
232
+ on selva.definition_versions for insert
233
+ to authenticated
234
+ with check (
235
+ selva.is_instance_admin()
236
+ or selva.is_definition_editor(definition_guid)
237
+ or (
238
+ uploaded_by = auth.uid()
239
+ and (
240
+ selva.is_definition_contributor(definition_guid)
241
+ or selva.is_unfinished_commons_upload(definition_guid)
242
+ )
243
+ )
244
+ );
245
+
246
+ drop policy if exists "definition_versions: editors can update" on selva.definition_versions;
247
+ create policy "definition_versions: editors can update"
248
+ on selva.definition_versions for update
249
+ to authenticated
250
+ using (selva.is_instance_admin() or selva.is_definition_editor(definition_guid))
251
+ with check (selva.is_instance_admin() or selva.is_definition_editor(definition_guid));
252
+
253
+ drop policy if exists "definition_versions: editors can delete" on selva.definition_versions;
254
+ create policy "definition_versions: editors can delete"
255
+ on selva.definition_versions for delete
256
+ to authenticated
257
+ using (
258
+ selva.is_instance_admin()
259
+ or selva.is_definition_editor(definition_guid)
260
+ or selva.is_definition_contributor(definition_guid)
261
+ or selva.is_unfinished_commons_upload(definition_guid)
262
+ );
263
+
264
+ drop policy if exists "share_links: editors can read" on selva.share_links;
265
+ create policy "share_links: editors can read"
266
+ on selva.share_links for select
267
+ to authenticated
268
+ using (
269
+ selva.is_instance_admin()
270
+ or selva.is_definition_editor(definition_guid)
271
+ or selva.is_definition_contributor(definition_guid)
272
+ );
273
+
274
+ drop policy if exists "share_links: editors can insert" on selva.share_links;
275
+ create policy "share_links: editors can insert"
276
+ on selva.share_links for insert
277
+ to authenticated
278
+ with check (
279
+ selva.is_instance_admin()
280
+ or selva.is_definition_editor(definition_guid)
281
+ or selva.is_definition_contributor(definition_guid)
282
+ );
283
+
284
+ drop policy if exists "share_links: editors can revoke" on selva.share_links;
285
+ create policy "share_links: editors can revoke"
286
+ on selva.share_links for update
287
+ to authenticated
288
+ using (
289
+ selva.is_instance_admin()
290
+ or selva.is_definition_editor(definition_guid)
291
+ or selva.is_definition_contributor(definition_guid)
292
+ )
293
+ with check (
294
+ selva.is_instance_admin()
295
+ or selva.is_definition_editor(definition_guid)
296
+ or selva.is_definition_contributor(definition_guid)
297
+ );
298
+
299
+ -- ----------------------------------------------------------------------------
300
+ -- 3. Org roles follow `canChangeOrgRole`
301
+ -- ----------------------------------------------------------------------------
302
+
303
+ -- `member` is the only standing an admin may hand out or take away, on the row
304
+ -- as it stands and on the row as it ends up.
305
+ create or replace function selva.may_set_org_role(o uuid, r text)
306
+ returns boolean
307
+ language sql stable security definer set search_path = public
308
+ as $$
309
+ select selva.is_instance_admin()
310
+ or selva.is_org_owner(o)
311
+ or (r = 'member' and selva.is_org_admin(o));
312
+ $$;
313
+
314
+ drop policy if exists "org_members: admins can insert" on selva.org_members;
315
+ create policy "org_members: admins can insert"
316
+ on selva.org_members for insert
317
+ to authenticated
318
+ with check (selva.may_set_org_role(org_id, role));
319
+
320
+ drop policy if exists "org_members: admins can update" on selva.org_members;
321
+ create policy "org_members: admins can update"
322
+ on selva.org_members for update
323
+ to authenticated
324
+ using (selva.may_set_org_role(org_id, role))
325
+ with check (selva.may_set_org_role(org_id, role));
326
+
327
+ drop policy if exists "org_members: admins can delete" on selva.org_members;
328
+ create policy "org_members: admins can delete"
329
+ on selva.org_members for delete
330
+ to authenticated
331
+ using (selva.may_set_org_role(org_id, role));
332
+
333
+ drop policy if exists "orgs: authenticated can create their own" on selva.orgs;
334
+ create policy "orgs: authenticated can create their own"
335
+ on selva.orgs for insert
336
+ to authenticated
337
+ with check (
338
+ selva.is_instance_admin()
339
+ or (owner_id = auth.uid() and selva.delegated_org() is null)
340
+ );
341
+
342
+ -- ----------------------------------------------------------------------------
343
+ -- 4. Invites carry authority, so they get the same checks as granting it
344
+ -- ----------------------------------------------------------------------------
345
+
346
+ -- accept-invite applies a row's role and platform permissions through the
347
+ -- service role, so whoever can shape the row picks what the invitee receives.
348
+ create or replace function selva.may_issue_invite(o uuid, r text, platform text[])
349
+ returns boolean
350
+ language sql stable security definer set search_path = public
351
+ as $$
352
+ select selva.has_org_permission(o, 'manage_org_members')
353
+ and selva.may_set_org_role(o, r)
354
+ and (cardinality(platform) = 0 or selva.is_instance_admin());
355
+ $$;
356
+
357
+ drop policy if exists "invites: manage_org_members can insert" on selva.invites;
358
+ create policy "invites: manage_org_members can insert"
359
+ on selva.invites for insert
360
+ to authenticated
361
+ with check (selva.may_issue_invite(org_id, org_role, platform_permissions));
362
+
363
+ drop policy if exists "invites: manage_org_members can delete" on selva.invites;
364
+ create policy "invites: manage_org_members can delete"
365
+ on selva.invites for delete
366
+ to authenticated
367
+ using (selva.may_issue_invite(org_id, org_role, platform_permissions));
368
+
369
+ -- A pending invite is immutable apart from being accepted.
370
+ revoke update on selva.invites from anon, authenticated;
371
+ grant update (accepted_at, accepted_by_user_id) on selva.invites to authenticated;
372
+
373
+ -- ----------------------------------------------------------------------------
374
+ -- 5. Profiles and tokens
375
+ -- ----------------------------------------------------------------------------
376
+
377
+ create or replace function selva.shares_org_with(u uuid)
378
+ returns boolean
379
+ language sql stable security definer set search_path = public
380
+ as $$
381
+ select exists (
382
+ select 1
383
+ from selva.org_members mine
384
+ join selva.org_members theirs on theirs.org_id = mine.org_id
385
+ where mine.user_id = auth.uid() and mine.deleted_at is null
386
+ and theirs.user_id = u and theirs.deleted_at is null
387
+ and selva.in_token_scope(mine.org_id)
388
+ );
389
+ $$;
390
+
391
+ -- The app reads other users' profiles with the service role, so this only limits
392
+ -- direct REST.
393
+ drop policy if exists "user_profiles: any authenticated can read" on selva.user_profiles;
394
+ create policy "user_profiles: self and shared orgs can read"
395
+ on selva.user_profiles for select
396
+ to authenticated
397
+ using (user_id = auth.uid() or selva.is_instance_admin() or selva.shares_org_with(user_id));
398
+
399
+ drop policy if exists "api_tokens: owner or org leadership can revoke" on selva.api_tokens;
400
+ create policy "api_tokens: owner or org leadership can revoke"
401
+ on selva.api_tokens for update
402
+ to authenticated
403
+ using (
404
+ (user_id = auth.uid() and selva.in_token_scope(org_id))
405
+ or selva.has_org_permission(org_id, 'manage_org_members')
406
+ )
407
+ with check (
408
+ revoked_at is not null
409
+ and (
410
+ (user_id = auth.uid() and selva.in_token_scope(org_id))
411
+ or selva.has_org_permission(org_id, 'manage_org_members')
412
+ )
413
+ );
414
+
415
+ -- ----------------------------------------------------------------------------
416
+ -- 6. Removing an org member reaches rows the acting admin cannot see
417
+ -- ----------------------------------------------------------------------------
418
+
419
+ -- RLS hides private projects from an org admin who is not in them, so a cascade
420
+ -- run as that admin would leave the leaver's project access intact and report
421
+ -- success. Definer rights reach those rows, and one transaction ends both
422
+ -- memberships together.
423
+ create or replace function selva.remove_org_member(o uuid, u uuid)
424
+ returns void
425
+ language plpgsql security definer set search_path = public
426
+ as $$
427
+ declare
428
+ target_role text;
429
+ begin
430
+ select role into target_role
431
+ from selva.org_members
432
+ where org_id = o and user_id = u and deleted_at is null
433
+ for update;
434
+ if not found then
435
+ raise exception 'Org member not found' using errcode = 'P0002';
436
+ end if;
437
+
438
+ if auth.jwt() ->> 'role' is distinct from 'service_role'
439
+ and not selva.may_set_org_role(o, target_role) then
440
+ raise exception 'No permission to remove this member' using errcode = '42501';
441
+ end if;
442
+
443
+ update selva.project_members m
444
+ set deleted_at = now(), updated_at = now(), updated_by = coalesce(auth.uid(), m.updated_by)
445
+ from selva.projects p
446
+ where p.id = m.project_id and p.org_id = o and m.user_id = u and m.deleted_at is null;
447
+
448
+ update selva.org_members m
449
+ set deleted_at = now(), updated_at = now(), updated_by = coalesce(auth.uid(), m.updated_by)
450
+ where m.org_id = o and m.user_id = u and m.deleted_at is null;
451
+ end;
452
+ $$;
453
+
454
+ revoke all on function selva.remove_org_member(uuid, uuid) from public, anon;
455
+ grant execute on function selva.remove_org_member(uuid, uuid) to authenticated, service_role;
456
+
457
+ -- ----------------------------------------------------------------------------
458
+ -- 7. The share-link roster reaches every project in the org
459
+ -- ----------------------------------------------------------------------------
460
+
461
+ create or replace function selva.leads_definition_org(g uuid)
462
+ returns boolean
463
+ language sql stable security definer set search_path = public
464
+ as $$
465
+ select exists (
466
+ select 1
467
+ from selva.definitions d
468
+ join selva.projects p on p.id = d.project_id
469
+ where d.guid = g and d.deleted_at is null and p.deleted_at is null
470
+ and selva.has_org_permission(p.org_id, 'manage_org_members')
471
+ );
472
+ $$;
473
+
474
+ drop policy if exists "share_links: org leadership can read the roster" on selva.share_links;
475
+ create policy "share_links: org leadership can read the roster"
476
+ on selva.share_links for select
477
+ to authenticated
478
+ using (selva.leads_definition_org(definition_guid));
479
+
480
+ -- A definer function, not a policy: the roster embeds `definitions` and
481
+ -- `projects`, whose own RLS hides private projects from the leadership reading
482
+ -- it, and those are the links offboarding has to find.
483
+ create or replace function selva.org_share_link_roster(o uuid, page_limit integer, page_offset integer)
484
+ returns table (
485
+ id uuid,
486
+ definition_guid uuid,
487
+ channel text,
488
+ token_hash text,
489
+ name text,
490
+ created_by uuid,
491
+ created_at timestamptz,
492
+ expires_at timestamptz,
493
+ revoked_at timestamptz,
494
+ allow_solve boolean,
495
+ max_solves integer,
496
+ solve_count integer,
497
+ definition_name text,
498
+ project_id uuid,
499
+ project_name text,
500
+ total_count bigint
501
+ )
502
+ language plpgsql stable security definer set search_path = public
503
+ as $$
504
+ begin
505
+ if auth.jwt() ->> 'role' is distinct from 'service_role'
506
+ and not selva.has_org_permission(o, 'manage_org_members') then
507
+ raise exception 'No permission to read this roster' using errcode = '42501';
508
+ end if;
509
+
510
+ return query
511
+ select l.id, l.definition_guid, l.channel, l.token_hash, l.name, l.created_by, l.created_at,
512
+ l.expires_at, l.revoked_at, l.allow_solve, l.max_solves, l.solve_count,
513
+ d.display_name, p.id, p.name, count(*) over ()
514
+ from selva.share_links l
515
+ join selva.definitions d on d.guid = l.definition_guid and d.deleted_at is null
516
+ join selva.projects p on p.id = d.project_id and p.deleted_at is null
517
+ where p.org_id = o
518
+ and l.revoked_at is null
519
+ and (l.expires_at is null or l.expires_at > now())
520
+ order by l.created_at desc
521
+ limit page_limit offset page_offset;
522
+ end;
523
+ $$;
524
+
525
+ revoke all on function selva.org_share_link_roster(uuid, integer, integer) from public, anon;
526
+ grant execute on function selva.org_share_link_roster(uuid, integer, integer)
527
+ to authenticated, service_role;
@@ -0,0 +1,14 @@
1
+ -- ============================================================================
2
+ -- Sessions cannot read compute_servers.api_key
3
+ --
4
+ -- RLS is per row, so anyone who could see a shared platform server could also
5
+ -- select its ciphertext and try to replay it. The store reads keys with the
6
+ -- service role, after a session read has said which servers the caller sees.
7
+ -- ============================================================================
8
+
9
+ revoke select on selva.compute_servers from anon, authenticated;
10
+
11
+ grant select (
12
+ id, scope, owner_org_id, shared_with_all, label, server_url,
13
+ timeout_ms, retry_count, created_at, has_api_key
14
+ ) on selva.compute_servers to authenticated;
@@ -0,0 +1,28 @@
1
+ -- ============================================================================
2
+ -- Erasing a user removes the person, not the tenant data they created
3
+ --
4
+ -- `owner_id` and `uploaded_by` record who created a row. Cascading on them took
5
+ -- whole orgs with a founder long gone, and a live version's restrict FK refused
6
+ -- to erase its uploader. Authority comes from membership rows, which still
7
+ -- cascade, so these columns go null and the stores read that as the erased actor.
8
+ -- ============================================================================
9
+
10
+ alter table selva.orgs alter column owner_id drop not null;
11
+ alter table selva.orgs drop constraint if exists orgs_owner_id_fkey;
12
+ alter table selva.orgs add constraint orgs_owner_id_fkey
13
+ foreign key (owner_id) references auth.users(id) on delete set null;
14
+
15
+ alter table selva.projects alter column owner_id drop not null;
16
+ alter table selva.projects drop constraint if exists projects_owner_id_fkey;
17
+ alter table selva.projects add constraint projects_owner_id_fkey
18
+ foreign key (owner_id) references auth.users(id) on delete set null;
19
+
20
+ alter table selva.definitions alter column owner_id drop not null;
21
+ alter table selva.definitions drop constraint if exists definitions_owner_id_fkey;
22
+ alter table selva.definitions add constraint definitions_owner_id_fkey
23
+ foreign key (owner_id) references auth.users(id) on delete set null;
24
+
25
+ alter table selva.definition_versions alter column uploaded_by drop not null;
26
+ alter table selva.definition_versions drop constraint if exists definition_versions_uploaded_by_fkey;
27
+ alter table selva.definition_versions add constraint definition_versions_uploaded_by_fkey
28
+ foreign key (uploaded_by) references auth.users(id) on delete set null;