@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.
Files changed (173) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +271 -0
  3. package/dist/auth/SupabaseAuthProvider.d.ts +81 -0
  4. package/dist/auth/SupabaseAuthProvider.d.ts.map +1 -0
  5. package/dist/auth/SupabaseAuthProvider.js +383 -0
  6. package/dist/auth/SupabaseAuthProvider.js.map +1 -0
  7. package/dist/auth/__tests__/conformance.test.d.ts +2 -0
  8. package/dist/auth/__tests__/conformance.test.d.ts.map +1 -0
  9. package/dist/auth/__tests__/conformance.test.js +76 -0
  10. package/dist/auth/__tests__/conformance.test.js.map +1 -0
  11. package/dist/auth/index.d.ts +3 -0
  12. package/dist/auth/index.d.ts.map +1 -0
  13. package/dist/auth/index.js +2 -0
  14. package/dist/auth/index.js.map +1 -0
  15. package/dist/data/SupabaseAuditQuery.d.ts +16 -0
  16. package/dist/data/SupabaseAuditQuery.d.ts.map +1 -0
  17. package/dist/data/SupabaseAuditQuery.js +83 -0
  18. package/dist/data/SupabaseAuditQuery.js.map +1 -0
  19. package/dist/data/SupabaseComputeServerStore.d.ts +26 -0
  20. package/dist/data/SupabaseComputeServerStore.d.ts.map +1 -0
  21. package/dist/data/SupabaseComputeServerStore.js +227 -0
  22. package/dist/data/SupabaseComputeServerStore.js.map +1 -0
  23. package/dist/data/SupabaseDataProvider.d.ts +55 -0
  24. package/dist/data/SupabaseDataProvider.d.ts.map +1 -0
  25. package/dist/data/SupabaseDataProvider.js +114 -0
  26. package/dist/data/SupabaseDataProvider.js.map +1 -0
  27. package/dist/data/SupabaseDefinitionStore.d.ts +39 -0
  28. package/dist/data/SupabaseDefinitionStore.d.ts.map +1 -0
  29. package/dist/data/SupabaseDefinitionStore.js +419 -0
  30. package/dist/data/SupabaseDefinitionStore.js.map +1 -0
  31. package/dist/data/SupabaseEventSink.d.ts +17 -0
  32. package/dist/data/SupabaseEventSink.d.ts.map +1 -0
  33. package/dist/data/SupabaseEventSink.js +31 -0
  34. package/dist/data/SupabaseEventSink.js.map +1 -0
  35. package/dist/data/SupabaseInviteStore.d.ts +22 -0
  36. package/dist/data/SupabaseInviteStore.d.ts.map +1 -0
  37. package/dist/data/SupabaseInviteStore.js +159 -0
  38. package/dist/data/SupabaseInviteStore.js.map +1 -0
  39. package/dist/data/SupabaseOrgStore.d.ts +33 -0
  40. package/dist/data/SupabaseOrgStore.d.ts.map +1 -0
  41. package/dist/data/SupabaseOrgStore.js +455 -0
  42. package/dist/data/SupabaseOrgStore.js.map +1 -0
  43. package/dist/data/SupabaseProjectStore.d.ts +29 -0
  44. package/dist/data/SupabaseProjectStore.d.ts.map +1 -0
  45. package/dist/data/SupabaseProjectStore.js +328 -0
  46. package/dist/data/SupabaseProjectStore.js.map +1 -0
  47. package/dist/data/SupabaseShareLinkStore.d.ts +26 -0
  48. package/dist/data/SupabaseShareLinkStore.d.ts.map +1 -0
  49. package/dist/data/SupabaseShareLinkStore.js +150 -0
  50. package/dist/data/SupabaseShareLinkStore.js.map +1 -0
  51. package/dist/data/__tests__/compute-server-conformance.test.d.ts +2 -0
  52. package/dist/data/__tests__/compute-server-conformance.test.d.ts.map +1 -0
  53. package/dist/data/__tests__/compute-server-conformance.test.js +40 -0
  54. package/dist/data/__tests__/compute-server-conformance.test.js.map +1 -0
  55. package/dist/data/__tests__/definition-conformance.test.d.ts +2 -0
  56. package/dist/data/__tests__/definition-conformance.test.d.ts.map +1 -0
  57. package/dist/data/__tests__/definition-conformance.test.js +85 -0
  58. package/dist/data/__tests__/definition-conformance.test.js.map +1 -0
  59. package/dist/data/__tests__/event-sink-conformance.test.d.ts +2 -0
  60. package/dist/data/__tests__/event-sink-conformance.test.d.ts.map +1 -0
  61. package/dist/data/__tests__/event-sink-conformance.test.js +27 -0
  62. package/dist/data/__tests__/event-sink-conformance.test.js.map +1 -0
  63. package/dist/data/__tests__/invite-conformance.test.d.ts +2 -0
  64. package/dist/data/__tests__/invite-conformance.test.d.ts.map +1 -0
  65. package/dist/data/__tests__/invite-conformance.test.js +45 -0
  66. package/dist/data/__tests__/invite-conformance.test.js.map +1 -0
  67. package/dist/data/__tests__/org-conformance.test.d.ts +2 -0
  68. package/dist/data/__tests__/org-conformance.test.d.ts.map +1 -0
  69. package/dist/data/__tests__/org-conformance.test.js +29 -0
  70. package/dist/data/__tests__/org-conformance.test.js.map +1 -0
  71. package/dist/data/__tests__/project-conformance.test.d.ts +2 -0
  72. package/dist/data/__tests__/project-conformance.test.d.ts.map +1 -0
  73. package/dist/data/__tests__/project-conformance.test.js +57 -0
  74. package/dist/data/__tests__/project-conformance.test.js.map +1 -0
  75. package/dist/data/__tests__/share-link-conformance.test.d.ts +2 -0
  76. package/dist/data/__tests__/share-link-conformance.test.d.ts.map +1 -0
  77. package/dist/data/__tests__/share-link-conformance.test.js +88 -0
  78. package/dist/data/__tests__/share-link-conformance.test.js.map +1 -0
  79. package/dist/data/__tests__/test-helpers.d.ts +53 -0
  80. package/dist/data/__tests__/test-helpers.d.ts.map +1 -0
  81. package/dist/data/__tests__/test-helpers.js +175 -0
  82. package/dist/data/__tests__/test-helpers.js.map +1 -0
  83. package/dist/data/client.d.ts +44 -0
  84. package/dist/data/client.d.ts.map +1 -0
  85. package/dist/data/client.js +40 -0
  86. package/dist/data/client.js.map +1 -0
  87. package/dist/data/index.d.ts +12 -0
  88. package/dist/data/index.d.ts.map +1 -0
  89. package/dist/data/index.js +11 -0
  90. package/dist/data/index.js.map +1 -0
  91. package/dist/data/pagination.d.ts +30 -0
  92. package/dist/data/pagination.d.ts.map +1 -0
  93. package/dist/data/pagination.js +53 -0
  94. package/dist/data/pagination.js.map +1 -0
  95. package/dist/index.d.ts +9 -0
  96. package/dist/index.d.ts.map +1 -0
  97. package/dist/index.js +6 -0
  98. package/dist/index.js.map +1 -0
  99. package/dist/permissions/SupabasePlatformPermissionStore.d.ts +25 -0
  100. package/dist/permissions/SupabasePlatformPermissionStore.d.ts.map +1 -0
  101. package/dist/permissions/SupabasePlatformPermissionStore.js +153 -0
  102. package/dist/permissions/SupabasePlatformPermissionStore.js.map +1 -0
  103. package/dist/permissions/__tests__/conformance.test.d.ts +2 -0
  104. package/dist/permissions/__tests__/conformance.test.d.ts.map +1 -0
  105. package/dist/permissions/__tests__/conformance.test.js +27 -0
  106. package/dist/permissions/__tests__/conformance.test.js.map +1 -0
  107. package/dist/permissions/index.d.ts +2 -0
  108. package/dist/permissions/index.d.ts.map +1 -0
  109. package/dist/permissions/index.js +2 -0
  110. package/dist/permissions/index.js.map +1 -0
  111. package/dist/storage/SupabaseStorageProvider.d.ts +67 -0
  112. package/dist/storage/SupabaseStorageProvider.d.ts.map +1 -0
  113. package/dist/storage/SupabaseStorageProvider.js +136 -0
  114. package/dist/storage/SupabaseStorageProvider.js.map +1 -0
  115. package/dist/storage/__tests__/conformance.test.d.ts +2 -0
  116. package/dist/storage/__tests__/conformance.test.d.ts.map +1 -0
  117. package/dist/storage/__tests__/conformance.test.js +149 -0
  118. package/dist/storage/__tests__/conformance.test.js.map +1 -0
  119. package/dist/storage/index.d.ts +3 -0
  120. package/dist/storage/index.d.ts.map +1 -0
  121. package/dist/storage/index.js +2 -0
  122. package/dist/storage/index.js.map +1 -0
  123. package/dist/userProfile/SupabaseUserProfileProvider.d.ts +16 -0
  124. package/dist/userProfile/SupabaseUserProfileProvider.d.ts.map +1 -0
  125. package/dist/userProfile/SupabaseUserProfileProvider.js +148 -0
  126. package/dist/userProfile/SupabaseUserProfileProvider.js.map +1 -0
  127. package/dist/userProfile/__tests__/conformance.test.d.ts +2 -0
  128. package/dist/userProfile/__tests__/conformance.test.d.ts.map +1 -0
  129. package/dist/userProfile/__tests__/conformance.test.js +38 -0
  130. package/dist/userProfile/__tests__/conformance.test.js.map +1 -0
  131. package/dist/userProfile/index.d.ts +2 -0
  132. package/dist/userProfile/index.d.ts.map +1 -0
  133. package/dist/userProfile/index.js +2 -0
  134. package/dist/userProfile/index.js.map +1 -0
  135. package/package.json +75 -0
  136. package/src/auth/SupabaseAuthProvider.ts +464 -0
  137. package/src/auth/__tests__/conformance.test.ts +78 -0
  138. package/src/auth/index.ts +2 -0
  139. package/src/data/SupabaseAuditQuery.ts +103 -0
  140. package/src/data/SupabaseComputeServerStore.ts +262 -0
  141. package/src/data/SupabaseDataProvider.ts +125 -0
  142. package/src/data/SupabaseDefinitionStore.ts +504 -0
  143. package/src/data/SupabaseEventSink.ts +30 -0
  144. package/src/data/SupabaseInviteStore.ts +183 -0
  145. package/src/data/SupabaseOrgStore.ts +547 -0
  146. package/src/data/SupabaseProjectStore.ts +414 -0
  147. package/src/data/SupabaseShareLinkStore.ts +191 -0
  148. package/src/data/__tests__/compute-server-conformance.test.ts +40 -0
  149. package/src/data/__tests__/definition-conformance.test.ts +89 -0
  150. package/src/data/__tests__/event-sink-conformance.test.ts +32 -0
  151. package/src/data/__tests__/invite-conformance.test.ts +46 -0
  152. package/src/data/__tests__/org-conformance.test.ts +30 -0
  153. package/src/data/__tests__/project-conformance.test.ts +57 -0
  154. package/src/data/__tests__/share-link-conformance.test.ts +87 -0
  155. package/src/data/__tests__/test-helpers.ts +186 -0
  156. package/src/data/client.ts +86 -0
  157. package/src/data/index.ts +11 -0
  158. package/src/data/pagination.ts +68 -0
  159. package/src/index.ts +23 -0
  160. package/src/permissions/SupabasePlatformPermissionStore.ts +161 -0
  161. package/src/permissions/__tests__/conformance.test.ts +28 -0
  162. package/src/permissions/index.ts +1 -0
  163. package/src/storage/SupabaseStorageProvider.ts +177 -0
  164. package/src/storage/__tests__/conformance.test.ts +152 -0
  165. package/src/storage/index.ts +2 -0
  166. package/src/userProfile/SupabaseUserProfileProvider.ts +177 -0
  167. package/src/userProfile/__tests__/conformance.test.ts +39 -0
  168. package/src/userProfile/index.ts +1 -0
  169. package/supabase/.branches/_current_branch +1 -0
  170. package/supabase/.temp/cli-latest +1 -0
  171. package/supabase/config.toml +68 -0
  172. package/supabase/migrations/0001_initial.sql +1084 -0
  173. package/supabase/seed.sql +13 -0
@@ -0,0 +1,159 @@
1
+ import { NoopEventSink, ProviderError, actorFrom } from '@selvajs/platform';
2
+ import { nextCursorFromRange, toRange } from './pagination.js';
3
+ /**
4
+ * Invite store. `getByTokenHash` routes through a SECURITY DEFINER RPC so
5
+ * unauthenticated / just-signed-up callers can validate and consume the
6
+ * invite without RLS blocking them. The raw token is hashed at the route
7
+ * layer (the selva app's `invites/token.server.ts`); the store sees only the
8
+ * HMAC digest. Other operations (create, list, revoke) go through the normal
9
+ * REST API with RLS enforcing `manage_org_members`.
10
+ */
11
+ export class SupabaseInviteStore {
12
+ clients;
13
+ events;
14
+ constructor(clients, events = new NoopEventSink()) {
15
+ this.clients = clients;
16
+ this.events = events;
17
+ }
18
+ async create(ctx, invite) {
19
+ const { error } = await this.clients
20
+ .forRequest(ctx)
21
+ .from('invites')
22
+ .insert(inviteToRow(invite));
23
+ if (error)
24
+ throw mapError(error);
25
+ await this.events.emit({
26
+ type: 'invite.created',
27
+ inviteId: invite.id,
28
+ orgId: invite.orgId,
29
+ email: invite.email,
30
+ actorId: actorFrom(ctx)
31
+ });
32
+ }
33
+ async getByTokenHash(ctx, tokenHash) {
34
+ // Use the SECURITY DEFINER RPC so the token itself is the capability —
35
+ // unauthenticated callers visiting /accept-invite can resolve it. The
36
+ // caller has already hashed the raw URL token; the RPC just looks up
37
+ // the row by digest.
38
+ const { data, error } = await this.clients
39
+ .forRequest(ctx)
40
+ .rpc('get_invite_by_token_hash', { h: tokenHash });
41
+ if (error)
42
+ throw mapError(error);
43
+ // rpc returns the matching row (or null). PostgREST may wrap it in
44
+ // an array depending on the signature — normalize both shapes.
45
+ const row = Array.isArray(data) ? data[0] : data;
46
+ return row ? rowToInvite(row) : null;
47
+ }
48
+ async listByOrg(ctx, orgId, opts) {
49
+ const range = toRange(opts);
50
+ const { data, error, count } = await this.clients
51
+ .forRequest(ctx)
52
+ .from('invites')
53
+ .select('*', { count: 'exact' })
54
+ .eq('org_id', orgId)
55
+ .order('created_at', { ascending: (opts?.orderDir ?? 'desc') === 'asc' })
56
+ .range(range.from, range.to);
57
+ if (error)
58
+ throw mapError(error);
59
+ const items = (data ?? []).map(rowToInvite);
60
+ return { items, nextCursor: nextCursorFromRange(range, items.length, count) };
61
+ }
62
+ async markAccepted(ctx, id, userId) {
63
+ // Direct UPDATE — the caller is authenticated (they just signed up).
64
+ // `markAccepted` is idempotent so we don't error if no row matches.
65
+ const { data, error } = await this.clients
66
+ .forRequest(ctx)
67
+ .from('invites')
68
+ .update({ accepted_at: new Date().toISOString(), accepted_by_user_id: userId })
69
+ .eq('id', id)
70
+ .is('accepted_at', null)
71
+ .select('org_id');
72
+ if (error)
73
+ throw mapError(error);
74
+ const row = data?.[0];
75
+ if (!row)
76
+ return;
77
+ await this.events.emit({
78
+ type: 'invite.accepted',
79
+ inviteId: id,
80
+ orgId: row.org_id,
81
+ userId,
82
+ actorId: actorFrom(ctx)
83
+ });
84
+ }
85
+ async revoke(ctx, id) {
86
+ const { data, error } = await this.clients
87
+ .forRequest(ctx)
88
+ .from('invites')
89
+ .delete()
90
+ .eq('id', id)
91
+ .is('accepted_at', null)
92
+ .select('org_id');
93
+ if (error)
94
+ throw mapError(error);
95
+ const row = data?.[0];
96
+ if (!row)
97
+ return;
98
+ await this.events.emit({
99
+ type: 'invite.revoked',
100
+ inviteId: id,
101
+ orgId: row.org_id,
102
+ actorId: actorFrom(ctx)
103
+ });
104
+ }
105
+ async deleteByOrg(ctx, orgId) {
106
+ const { error } = await this.clients
107
+ .forRequest(ctx)
108
+ .from('invites')
109
+ .delete()
110
+ .eq('org_id', orgId);
111
+ if (error)
112
+ throw mapError(error);
113
+ }
114
+ }
115
+ function rowToInvite(row) {
116
+ return {
117
+ id: row.id,
118
+ tokenHash: row.token_hash,
119
+ email: row.email,
120
+ orgId: row.org_id,
121
+ orgRole: row.org_role,
122
+ orgPermissions: row.org_permissions,
123
+ invitedBy: row.invited_by,
124
+ createdAt: row.created_at,
125
+ expiresAt: row.expires_at,
126
+ acceptedAt: row.accepted_at ?? undefined,
127
+ acceptedByUserId: row.accepted_by_user_id ?? undefined
128
+ };
129
+ }
130
+ function inviteToRow(i) {
131
+ return {
132
+ id: i.id,
133
+ token_hash: i.tokenHash,
134
+ email: i.email,
135
+ org_id: i.orgId,
136
+ org_role: i.orgRole,
137
+ org_permissions: i.orgPermissions,
138
+ invited_by: i.invitedBy,
139
+ created_at: i.createdAt,
140
+ expires_at: i.expiresAt,
141
+ accepted_at: i.acceptedAt ?? null,
142
+ accepted_by_user_id: i.acceptedByUserId ?? null
143
+ };
144
+ }
145
+ function mapError(e) {
146
+ const pg = e;
147
+ if (pg?.code === '23505')
148
+ return new ProviderError(pg.message ?? 'Duplicate record', 409);
149
+ if (pg?.code === '23503')
150
+ return new ProviderError(pg.message ?? 'Foreign key violation', 409);
151
+ if (e instanceof Error)
152
+ return e;
153
+ if (e && typeof e === 'object') {
154
+ const obj = e;
155
+ return new Error(obj.code ? `[${obj.code}] ${obj.message ?? ''}` : (obj.message ?? String(e)));
156
+ }
157
+ return new Error(String(e));
158
+ }
159
+ //# sourceMappingURL=SupabaseInviteStore.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SupabaseInviteStore.js","sourceRoot":"","sources":["../../src/data/SupabaseInviteStore.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,33 @@
1
+ import type { IOrgStore, IEventSink, Organization, OrgMember, OrgRole, OrgPermission, RequestContext, ListOptions, Page } from '@selvajs/platform';
2
+ import type { ClientBundle } from './client.js';
3
+ /**
4
+ * Org + org-membership store backed by Postgres. The queries rely on RLS
5
+ * for per-user visibility; the service-role client bypasses those policies
6
+ * for admin paths (setup, janitor, tests).
7
+ *
8
+ * `createOrg` atomically seeds the creator as `owner` with every
9
+ * `OrgPermission` — otherwise under user-scoped RLS the creator would
10
+ * immediately lose visibility of their own org.
11
+ */
12
+ export declare class SupabaseOrgStore implements IOrgStore {
13
+ private readonly clients;
14
+ private readonly events;
15
+ constructor(clients: ClientBundle, events?: IEventSink);
16
+ listOrgs(ctx: RequestContext, opts?: ListOptions): Promise<Page<Organization>>;
17
+ getOrg(ctx: RequestContext, id: string): Promise<Organization | null>;
18
+ getOrgBySlug(ctx: RequestContext, slug: string): Promise<Organization | null>;
19
+ createOrg(ctx: RequestContext, org: Organization): Promise<void>;
20
+ updateOrg(ctx: RequestContext, id: string, patch: Partial<Pick<Organization, 'name' | 'slug'>>): Promise<void>;
21
+ deleteOrg(ctx: RequestContext, id: string): Promise<void>;
22
+ listOrgMembers(ctx: RequestContext, orgId: string, opts?: ListOptions): Promise<Page<OrgMember>>;
23
+ getOrgMember(ctx: RequestContext, orgId: string, userId: string): Promise<OrgMember | null>;
24
+ findUserMembership(ctx: RequestContext, userId: string): Promise<{
25
+ org: Organization;
26
+ member: OrgMember;
27
+ } | null>;
28
+ addOrgMember(ctx: RequestContext, member: OrgMember): Promise<void>;
29
+ updateOrgMemberRole(ctx: RequestContext, orgId: string, userId: string, role: OrgRole): Promise<void>;
30
+ updateOrgMemberPermissions(ctx: RequestContext, orgId: string, userId: string, permissions: readonly OrgPermission[]): Promise<void>;
31
+ removeOrgMember(ctx: RequestContext, orgId: string, userId: string): Promise<void>;
32
+ }
33
+ //# sourceMappingURL=SupabaseOrgStore.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SupabaseOrgStore.d.ts","sourceRoot":"","sources":["../../src/data/SupabaseOrgStore.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACX,SAAS,EACT,UAAU,EACV,YAAY,EACZ,SAAS,EACT,OAAO,EACP,aAAa,EACb,cAAc,EACd,WAAW,EACX,IAAI,EACJ,MAAM,mBAAmB,CAAC;AAQ3B,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAGhD;;;;;;;;GAQG;AACH,qBAAa,gBAAiB,YAAW,SAAS;IAIhD,OAAO,CAAC,QAAQ,CAAC,OAAO;IAHzB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAa;gBAGlB,OAAO,EAAE,YAAY,EACtC,MAAM,GAAE,UAAgC;IAKnC,QAAQ,CAAC,GAAG,EAAE,cAAc,EAAE,IAAI,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;IAe9E,MAAM,CAAC,GAAG,EAAE,cAAc,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC;IAYrE,YAAY,CAAC,GAAG,EAAE,cAAc,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC;IAY7E,SAAS,CAAC,GAAG,EAAE,cAAc,EAAE,GAAG,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC;IAyBhE,SAAS,CACd,GAAG,EAAE,cAAc,EACnB,EAAE,EAAE,MAAM,EACV,KAAK,EAAE,OAAO,CAAC,IAAI,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAAC,CAAC,GACjD,OAAO,CAAC,IAAI,CAAC;IAsBV,SAAS,CAAC,GAAG,EAAE,cAAc,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAyFzD,cAAc,CACnB,GAAG,EAAE,cAAc,EACnB,KAAK,EAAE,MAAM,EACb,IAAI,CAAC,EAAE,WAAW,GAChB,OAAO,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IAerB,YAAY,CACjB,GAAG,EAAE,cAAc,EACnB,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,GACZ,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC;IAatB,kBAAkB,CACvB,GAAG,EAAE,cAAc,EACnB,MAAM,EAAE,MAAM,GACZ,OAAO,CAAC;QAAE,GAAG,EAAE,YAAY,CAAC;QAAC,MAAM,EAAE,SAAS,CAAA;KAAE,GAAG,IAAI,CAAC;IA8BrD,YAAY,CAAC,GAAG,EAAE,cAAc,EAAE,MAAM,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC;IAqBnE,mBAAmB,CACxB,GAAG,EAAE,cAAc,EACnB,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,OAAO,GACX,OAAO,CAAC,IAAI,CAAC;IA2BV,0BAA0B,CAC/B,GAAG,EAAE,cAAc,EACnB,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,EACd,WAAW,EAAE,SAAS,aAAa,EAAE,GACnC,OAAO,CAAC,IAAI,CAAC;IA0BV,eAAe,CAAC,GAAG,EAAE,cAAc,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CA0CxF"}
@@ -0,0 +1,455 @@
1
+ import { DEFAULT_ORG_PERMISSIONS, ProviderError, auditSoftDelete, actorFrom, NoopEventSink } from '@selvajs/platform';
2
+ import { nextCursorFromRange, orderColumn, toRange } from './pagination.js';
3
+ /**
4
+ * Org + org-membership store backed by Postgres. The queries rely on RLS
5
+ * for per-user visibility; the service-role client bypasses those policies
6
+ * for admin paths (setup, janitor, tests).
7
+ *
8
+ * `createOrg` atomically seeds the creator as `owner` with every
9
+ * `OrgPermission` — otherwise under user-scoped RLS the creator would
10
+ * immediately lose visibility of their own org.
11
+ */
12
+ export class SupabaseOrgStore {
13
+ clients;
14
+ events;
15
+ constructor(clients, events = new NoopEventSink()) {
16
+ this.clients = clients;
17
+ this.events = events;
18
+ }
19
+ async listOrgs(ctx, opts) {
20
+ const range = toRange(opts);
21
+ const direction = opts?.orderDir ?? 'desc';
22
+ const { data, error, count } = await this.clients
23
+ .forRequest(ctx)
24
+ .from('orgs')
25
+ .select('*', { count: 'exact' })
26
+ .is('deleted_at', null)
27
+ .order(orderColumn(opts?.orderBy), { ascending: direction === 'asc' })
28
+ .range(range.from, range.to);
29
+ if (error)
30
+ throw mapError(error);
31
+ const items = (data ?? []).map(rowToOrg);
32
+ return { items, nextCursor: nextCursorFromRange(range, items.length, count) };
33
+ }
34
+ async getOrg(ctx, id) {
35
+ const { data, error } = await this.clients
36
+ .forRequest(ctx)
37
+ .from('orgs')
38
+ .select('*')
39
+ .eq('id', id)
40
+ .is('deleted_at', null)
41
+ .maybeSingle();
42
+ if (error)
43
+ throw mapError(error);
44
+ return data ? rowToOrg(data) : null;
45
+ }
46
+ async getOrgBySlug(ctx, slug) {
47
+ const { data, error } = await this.clients
48
+ .forRequest(ctx)
49
+ .from('orgs')
50
+ .select('*')
51
+ .eq('slug', slug)
52
+ .is('deleted_at', null)
53
+ .maybeSingle();
54
+ if (error)
55
+ throw mapError(error);
56
+ return data ? rowToOrg(data) : null;
57
+ }
58
+ async createOrg(ctx, org) {
59
+ const client = this.clients.forRequest(ctx);
60
+ const { error } = await client.from('orgs').insert(orgToRow(org));
61
+ if (error)
62
+ throw mapError(error);
63
+ // Seed owner membership so user-scoped queries can see the org post-create.
64
+ // Service-role bypasses RLS, but the same code path runs for user-scoped
65
+ // callers — the membership row is what makes them visible to themselves.
66
+ // Upsert so a stale soft-deleted row from a prior org with the same id
67
+ // is reactivated rather than blocking creation.
68
+ const { error: memberError } = await client.from('org_members').upsert({
69
+ org_id: org.id,
70
+ user_id: org.ownerId,
71
+ role: 'owner',
72
+ permissions: [...DEFAULT_ORG_PERMISSIONS.owner],
73
+ joined_at: new Date().toISOString(),
74
+ deleted_at: null
75
+ }, { onConflict: 'org_id,user_id' });
76
+ if (memberError)
77
+ throw mapError(memberError);
78
+ await this.events.emit({ type: 'org.created', orgId: org.id, actorId: actorFrom(ctx) });
79
+ }
80
+ async updateOrg(ctx, id, patch) {
81
+ const row = {};
82
+ if (patch.name !== undefined)
83
+ row.name = patch.name;
84
+ if (patch.slug !== undefined)
85
+ row.slug = patch.slug;
86
+ if (Object.keys(row).length === 0)
87
+ return;
88
+ // `updated_at` is set by the trg_orgs_updated_at trigger; `updated_by`
89
+ // is not, so stamp it here. ctx.userId can be empty in system contexts —
90
+ // the DB FK is `on delete set null` so passing empty would fail; let it
91
+ // fall back to whatever's already on the row by omitting in that case.
92
+ if (ctx.userId)
93
+ row.updated_by = ctx.userId;
94
+ const { error, data } = await this.clients
95
+ .forRequest(ctx)
96
+ .from('orgs')
97
+ .update(row)
98
+ .eq('id', id)
99
+ .is('deleted_at', null)
100
+ .select('id');
101
+ if (error)
102
+ throw mapError(error);
103
+ if (!data || data.length === 0)
104
+ throw new ProviderError(`Org '${id}' not found`, 404);
105
+ }
106
+ async deleteOrg(ctx, id) {
107
+ // §9 soft-delete with cascade. Mirrors LocalOrgStore: org → org_members,
108
+ // projects → project_members, definitions. Hard delete is reserved for
109
+ // the background janitor; user-facing deletes preserve the audit trail.
110
+ const client = this.clients.forRequest(ctx);
111
+ const stamp = auditSoftDelete(ctx, ctx.userId);
112
+ const stampRow = stampToRow(stamp);
113
+ // Org itself.
114
+ const { error: orgErr, data: orgData } = await client
115
+ .from('orgs')
116
+ .update(stampRow)
117
+ .eq('id', id)
118
+ .is('deleted_at', null)
119
+ .select('id');
120
+ if (orgErr)
121
+ throw mapError(orgErr);
122
+ if (!orgData || orgData.length === 0)
123
+ throw new ProviderError(`Org '${id}' not found`, 404);
124
+ // Cascade: org_members.
125
+ const { error: omErr } = await client
126
+ .from('org_members')
127
+ .update(stampRow)
128
+ .eq('org_id', id)
129
+ .is('deleted_at', null);
130
+ if (omErr)
131
+ throw mapError(omErr);
132
+ // Cascade: projects in this org. Fetch IDs first so we can cascade to
133
+ // project_members and definitions in app code (Postgres FK CASCADE only
134
+ // fires on hard DELETE, which we're not doing here).
135
+ const { data: orgProjects, error: projFetchErr } = await client
136
+ .from('projects')
137
+ .select('id')
138
+ .eq('org_id', id)
139
+ .is('deleted_at', null);
140
+ if (projFetchErr)
141
+ throw mapError(projFetchErr);
142
+ const projectIds = (orgProjects ?? []).map((p) => p.id);
143
+ const { error: projErr } = await client
144
+ .from('projects')
145
+ .update(stampRow)
146
+ .eq('org_id', id)
147
+ .is('deleted_at', null);
148
+ if (projErr)
149
+ throw mapError(projErr);
150
+ if (projectIds.length > 0) {
151
+ const { error: pmErr } = await client
152
+ .from('project_members')
153
+ .update(stampRow)
154
+ .in('project_id', projectIds)
155
+ .is('deleted_at', null);
156
+ if (pmErr)
157
+ throw mapError(pmErr);
158
+ const { error: defErr } = await client
159
+ .from('definitions')
160
+ .update(stampRow)
161
+ .in('project_id', projectIds)
162
+ .is('deleted_at', null);
163
+ if (defErr)
164
+ throw mapError(defErr);
165
+ }
166
+ // Hard-delete tables that have no `deleted_at` column. SQL CASCADE on
167
+ // the org FK only fires for hard org deletes; we soft-delete, so we
168
+ // clean these up here. Pending invites to a dead org are unredeemable;
169
+ // stale compute config is operational state with no audit need.
170
+ const { error: invErr } = await client.from('invites').delete().eq('org_id', id);
171
+ if (invErr)
172
+ throw mapError(invErr);
173
+ const { error: cdErr } = await client
174
+ .from('compute_server_org_defaults')
175
+ .delete()
176
+ .eq('org_id', id);
177
+ if (cdErr)
178
+ throw mapError(cdErr);
179
+ const { error: shErr } = await client.from('compute_server_shares').delete().eq('org_id', id);
180
+ if (shErr)
181
+ throw mapError(shErr);
182
+ const { error: csErr } = await client
183
+ .from('compute_servers')
184
+ .delete()
185
+ .eq('scope', 'org')
186
+ .eq('owner_org_id', id);
187
+ if (csErr)
188
+ throw mapError(csErr);
189
+ await this.events.emit({ type: 'org.deleted', orgId: id, actorId: actorFrom(ctx) });
190
+ }
191
+ // ============================================================================
192
+ // Org members
193
+ // ============================================================================
194
+ async listOrgMembers(ctx, orgId, opts) {
195
+ const range = toRange(opts);
196
+ const { data, error, count } = await this.clients
197
+ .forRequest(ctx)
198
+ .from('org_members')
199
+ .select('*', { count: 'exact' })
200
+ .eq('org_id', orgId)
201
+ .is('deleted_at', null)
202
+ .order('joined_at', { ascending: (opts?.orderDir ?? 'desc') === 'asc' })
203
+ .range(range.from, range.to);
204
+ if (error)
205
+ throw mapError(error);
206
+ const items = (data ?? []).map(rowToOrgMember);
207
+ return { items, nextCursor: nextCursorFromRange(range, items.length, count) };
208
+ }
209
+ async getOrgMember(ctx, orgId, userId) {
210
+ const { data, error } = await this.clients
211
+ .forRequest(ctx)
212
+ .from('org_members')
213
+ .select('*')
214
+ .eq('org_id', orgId)
215
+ .eq('user_id', userId)
216
+ .is('deleted_at', null)
217
+ .maybeSingle();
218
+ if (error)
219
+ throw mapError(error);
220
+ return data ? rowToOrgMember(data) : null;
221
+ }
222
+ async findUserMembership(ctx, userId) {
223
+ // One round-trip: join org_members → orgs and filter live rows on
224
+ // both sides. PostgREST `!inner` on the join keeps it a single SELECT,
225
+ // avoiding the listOrgs + getOrgMember-per-org N+1 the bootstrap path
226
+ // used to do. RLS still scopes by ctx; service-role sees all.
227
+ const { data, error } = await this.clients
228
+ .forRequest(ctx)
229
+ .from('org_members')
230
+ .select('*, org:orgs!inner(*)')
231
+ .eq('user_id', userId)
232
+ .is('deleted_at', null)
233
+ .is('org.deleted_at', null)
234
+ .order('joined_at', { ascending: true })
235
+ .limit(1)
236
+ .maybeSingle();
237
+ if (error)
238
+ throw mapError(error);
239
+ if (!data)
240
+ return null;
241
+ // PostgREST embeds the related row as `org` per the alias above. Strip
242
+ // it off the member row before mapping so OrgMember stays clean.
243
+ const row = data;
244
+ const orgRow = Array.isArray(row.org) ? row.org[0] : row.org;
245
+ if (!orgRow)
246
+ return null;
247
+ const { org: _omit, ...memberRow } = row;
248
+ return {
249
+ org: rowToOrg(orgRow),
250
+ member: rowToOrgMember(memberRow)
251
+ };
252
+ }
253
+ async addOrgMember(ctx, member) {
254
+ // Upsert so a prior soft-deleted row is reactivated rather than throwing
255
+ // a duplicate-key error. Mirrors LocalOrgStore.addOrgMember.
256
+ const row = {
257
+ ...memberToRow(member),
258
+ deleted_at: null
259
+ };
260
+ if (ctx.userId)
261
+ row.updated_by = ctx.userId;
262
+ const { error } = await this.clients
263
+ .forRequest(ctx)
264
+ .from('org_members')
265
+ .upsert(row, { onConflict: 'org_id,user_id' });
266
+ if (error)
267
+ throw mapError(error);
268
+ await this.events.emit({
269
+ type: 'org_member.added',
270
+ orgId: member.orgId,
271
+ userId: member.userId,
272
+ actorId: actorFrom(ctx)
273
+ });
274
+ }
275
+ async updateOrgMemberRole(ctx, orgId, userId, role) {
276
+ // Role change re-seeds default permissions. Matches LocalOrgStore.
277
+ const row = {
278
+ role,
279
+ permissions: [...DEFAULT_ORG_PERMISSIONS[role]]
280
+ };
281
+ if (ctx.userId)
282
+ row.updated_by = ctx.userId;
283
+ const { data, error } = await this.clients
284
+ .forRequest(ctx)
285
+ .from('org_members')
286
+ .update(row)
287
+ .eq('org_id', orgId)
288
+ .eq('user_id', userId)
289
+ .is('deleted_at', null)
290
+ .select('user_id');
291
+ if (error)
292
+ throw mapError(error);
293
+ if (!data || data.length === 0)
294
+ throw new ProviderError(`Org member '${userId}' not found`, 404);
295
+ await this.events.emit({
296
+ type: 'org_member.role_changed',
297
+ orgId,
298
+ userId,
299
+ role,
300
+ actorId: actorFrom(ctx)
301
+ });
302
+ }
303
+ async updateOrgMemberPermissions(ctx, orgId, userId, permissions) {
304
+ // Replace permissions only. Matches LocalOrgStore.updateOrgMemberPermissions —
305
+ // distinct from role change so callers can grant a finer-grained set without
306
+ // re-seeding defaults from the role.
307
+ const row = { permissions: [...permissions] };
308
+ if (ctx.userId)
309
+ row.updated_by = ctx.userId;
310
+ const { data, error } = await this.clients
311
+ .forRequest(ctx)
312
+ .from('org_members')
313
+ .update(row)
314
+ .eq('org_id', orgId)
315
+ .eq('user_id', userId)
316
+ .is('deleted_at', null)
317
+ .select('user_id');
318
+ if (error)
319
+ throw mapError(error);
320
+ if (!data || data.length === 0)
321
+ throw new ProviderError(`Org member '${userId}' not found`, 404);
322
+ await this.events.emit({
323
+ type: 'org_member.permissions_changed',
324
+ orgId,
325
+ userId,
326
+ permissions: [...permissions],
327
+ actorId: actorFrom(ctx)
328
+ });
329
+ }
330
+ async removeOrgMember(ctx, orgId, userId) {
331
+ // §9 cascade — losing org membership ends every project membership scoped
332
+ // to that tenant. Soft-delete the org_member row, then cascade-soft-delete
333
+ // the user's project_members for any project in this org. Run the project
334
+ // cascade BEFORE the org_member update so RLS policies that gate
335
+ // project_members on org_member existence still pass for the duration.
336
+ const client = this.clients.forRequest(ctx);
337
+ const stamp = auditSoftDelete(ctx, ctx.userId);
338
+ const stampRow = stampToRow(stamp);
339
+ const { data: orgProjects, error: projError } = await client
340
+ .from('projects')
341
+ .select('id')
342
+ .eq('org_id', orgId)
343
+ .is('deleted_at', null);
344
+ if (projError)
345
+ throw mapError(projError);
346
+ const projectIds = (orgProjects ?? []).map((p) => p.id);
347
+ if (projectIds.length > 0) {
348
+ const { error: pmError } = await client
349
+ .from('project_members')
350
+ .update(stampRow)
351
+ .in('project_id', projectIds)
352
+ .eq('user_id', userId)
353
+ .is('deleted_at', null);
354
+ if (pmError)
355
+ throw mapError(pmError);
356
+ }
357
+ const { error } = await client
358
+ .from('org_members')
359
+ .update(stampRow)
360
+ .eq('org_id', orgId)
361
+ .eq('user_id', userId)
362
+ .is('deleted_at', null);
363
+ if (error)
364
+ throw mapError(error);
365
+ await this.events.emit({
366
+ type: 'org_member.removed',
367
+ orgId,
368
+ userId,
369
+ actorId: actorFrom(ctx)
370
+ });
371
+ }
372
+ }
373
+ /**
374
+ * Build a row payload from an audit-soft-delete stamp. `updated_by` only
375
+ * goes into the row when ctx.userId is set — passing `''` would violate the
376
+ * `references auth.users(id)` FK on system contexts. The DB trigger sets
377
+ * `updated_at`, but for soft-delete we set it explicitly so it matches
378
+ * `deleted_at` (single timestamp for the deletion event).
379
+ */
380
+ function stampToRow(stamp) {
381
+ const row = {
382
+ deleted_at: stamp.deletedAt,
383
+ updated_at: stamp.updatedAt
384
+ };
385
+ if (stamp.updatedBy)
386
+ row.updated_by = stamp.updatedBy;
387
+ return row;
388
+ }
389
+ function rowToOrg(row) {
390
+ return {
391
+ id: row.id,
392
+ name: row.name,
393
+ slug: row.slug,
394
+ ownerId: row.owner_id,
395
+ createdBy: row.created_by ?? row.owner_id,
396
+ updatedBy: row.updated_by ?? row.owner_id,
397
+ createdAt: row.created_at,
398
+ updatedAt: row.updated_at,
399
+ deletedAt: row.deleted_at ?? null
400
+ };
401
+ }
402
+ function orgToRow(org) {
403
+ return {
404
+ id: org.id,
405
+ name: org.name,
406
+ slug: org.slug,
407
+ owner_id: org.ownerId,
408
+ created_by: org.createdBy,
409
+ updated_by: org.updatedBy,
410
+ created_at: org.createdAt,
411
+ updated_at: org.updatedAt,
412
+ deleted_at: org.deletedAt ?? null
413
+ };
414
+ }
415
+ function rowToOrgMember(row) {
416
+ return {
417
+ orgId: row.org_id,
418
+ userId: row.user_id,
419
+ role: row.role,
420
+ permissions: (row.permissions ?? []),
421
+ joinedAt: row.joined_at,
422
+ updatedAt: row.updated_at ?? row.joined_at,
423
+ updatedBy: row.updated_by ?? row.user_id,
424
+ deletedAt: row.deleted_at ?? null
425
+ };
426
+ }
427
+ function memberToRow(m) {
428
+ return {
429
+ org_id: m.orgId,
430
+ user_id: m.userId,
431
+ role: m.role,
432
+ permissions: m.permissions ?? [],
433
+ joined_at: m.joinedAt
434
+ };
435
+ }
436
+ function mapError(e) {
437
+ const pg = e;
438
+ if (pg?.code === '23505') {
439
+ return new ProviderError(pg.message ?? 'Duplicate record', 409);
440
+ }
441
+ if (pg?.code === '23503') {
442
+ return new ProviderError(pg.message ?? 'Foreign key violation', 409);
443
+ }
444
+ if (e instanceof Error)
445
+ return e;
446
+ if (e && typeof e === 'object') {
447
+ const obj = e;
448
+ const msg = obj.message ?? obj.details ?? obj.hint ?? 'Unknown Postgres error';
449
+ const err = new Error(obj.code ? `[${obj.code}] ${msg}` : msg);
450
+ Object.assign(err, obj);
451
+ return err;
452
+ }
453
+ return new Error(String(e));
454
+ }
455
+ //# sourceMappingURL=SupabaseOrgStore.js.map