@selvajs/supabase-provider 0.14.4-beta.0 → 0.15.0-beta.2

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 (158) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +2 -0
  3. package/dist/auth/SupabaseAuthProvider.d.ts.map +1 -1
  4. package/dist/auth/SupabaseAuthProvider.js +7 -14
  5. package/dist/auth/SupabaseAuthProvider.js.map +1 -1
  6. package/dist/data/SupabaseComputeServerStore.d.ts +14 -3
  7. package/dist/data/SupabaseComputeServerStore.d.ts.map +1 -1
  8. package/dist/data/SupabaseComputeServerStore.js +67 -16
  9. package/dist/data/SupabaseComputeServerStore.js.map +1 -1
  10. package/dist/data/SupabaseDataProvider.d.ts +36 -8
  11. package/dist/data/SupabaseDataProvider.d.ts.map +1 -1
  12. package/dist/data/SupabaseDataProvider.js +119 -15
  13. package/dist/data/SupabaseDataProvider.js.map +1 -1
  14. package/dist/data/SupabaseDefinitionStore.d.ts +1 -0
  15. package/dist/data/SupabaseDefinitionStore.d.ts.map +1 -1
  16. package/dist/data/SupabaseDefinitionStore.js +41 -4
  17. package/dist/data/SupabaseDefinitionStore.js.map +1 -1
  18. package/dist/data/SupabaseEventSink.d.ts +5 -2
  19. package/dist/data/SupabaseEventSink.d.ts.map +1 -1
  20. package/dist/data/SupabaseEventSink.js +20 -3
  21. package/dist/data/SupabaseEventSink.js.map +1 -1
  22. package/dist/data/SupabaseSolveMetricSink.d.ts +49 -4
  23. package/dist/data/SupabaseSolveMetricSink.d.ts.map +1 -1
  24. package/dist/data/SupabaseSolveMetricSink.js +109 -19
  25. package/dist/data/SupabaseSolveMetricSink.js.map +1 -1
  26. package/dist/data/index.d.ts +1 -0
  27. package/dist/data/index.d.ts.map +1 -1
  28. package/dist/data/index.js +1 -0
  29. package/dist/data/index.js.map +1 -1
  30. package/dist/data/migrationHead.d.ts +14 -0
  31. package/dist/data/migrationHead.d.ts.map +1 -0
  32. package/dist/data/migrationHead.js +14 -0
  33. package/dist/data/migrationHead.js.map +1 -0
  34. package/dist/index.d.ts +1 -1
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +1 -1
  37. package/dist/index.js.map +1 -1
  38. package/dist/permissions/SupabasePlatformPermissionStore.d.ts +5 -3
  39. package/dist/permissions/SupabasePlatformPermissionStore.d.ts.map +1 -1
  40. package/dist/permissions/SupabasePlatformPermissionStore.js +17 -30
  41. package/dist/permissions/SupabasePlatformPermissionStore.js.map +1 -1
  42. package/dist/storage/SupabaseStorageProvider.d.ts.map +1 -1
  43. package/dist/storage/SupabaseStorageProvider.js +3 -0
  44. package/dist/storage/SupabaseStorageProvider.js.map +1 -1
  45. package/package.json +23 -14
  46. package/supabase/migrations/20260712120000_user_profiles_disabled_and_gin.sql +95 -0
  47. package/supabase/migrations/20260712123000_selva_migration_head_fn.sql +38 -0
  48. package/supabase/migrations/20260713120000_selva_next_version_number.sql +59 -0
  49. package/supabase/migrations/20260713140000_selva_solve_cache_limit.sql +15 -0
  50. package/supabase/migrations/20260716120000_definition_status_and_audit_version.sql +36 -0
  51. package/supabase/migrations/20260716130000_erase_user_audit_email.sql +36 -0
  52. package/supabase/migrations/20260717120000_compute_server_has_api_key.sql +20 -0
  53. package/dist/auth/__tests__/conformance.test.d.ts +0 -2
  54. package/dist/auth/__tests__/conformance.test.d.ts.map +0 -1
  55. package/dist/auth/__tests__/conformance.test.js +0 -76
  56. package/dist/auth/__tests__/conformance.test.js.map +0 -1
  57. package/dist/auth/__tests__/token-verification.test.d.ts +0 -13
  58. package/dist/auth/__tests__/token-verification.test.d.ts.map +0 -1
  59. package/dist/auth/__tests__/token-verification.test.js +0 -135
  60. package/dist/auth/__tests__/token-verification.test.js.map +0 -1
  61. package/dist/data/__tests__/compute-server-conformance.test.d.ts +0 -2
  62. package/dist/data/__tests__/compute-server-conformance.test.d.ts.map +0 -1
  63. package/dist/data/__tests__/compute-server-conformance.test.js +0 -45
  64. package/dist/data/__tests__/compute-server-conformance.test.js.map +0 -1
  65. package/dist/data/__tests__/compute-server-encryption.test.d.ts +0 -12
  66. package/dist/data/__tests__/compute-server-encryption.test.d.ts.map +0 -1
  67. package/dist/data/__tests__/compute-server-encryption.test.js +0 -143
  68. package/dist/data/__tests__/compute-server-encryption.test.js.map +0 -1
  69. package/dist/data/__tests__/definition-conformance.test.d.ts +0 -2
  70. package/dist/data/__tests__/definition-conformance.test.d.ts.map +0 -1
  71. package/dist/data/__tests__/definition-conformance.test.js +0 -85
  72. package/dist/data/__tests__/definition-conformance.test.js.map +0 -1
  73. package/dist/data/__tests__/event-sink-conformance.test.d.ts +0 -2
  74. package/dist/data/__tests__/event-sink-conformance.test.d.ts.map +0 -1
  75. package/dist/data/__tests__/event-sink-conformance.test.js +0 -27
  76. package/dist/data/__tests__/event-sink-conformance.test.js.map +0 -1
  77. package/dist/data/__tests__/invite-conformance.test.d.ts +0 -2
  78. package/dist/data/__tests__/invite-conformance.test.d.ts.map +0 -1
  79. package/dist/data/__tests__/invite-conformance.test.js +0 -45
  80. package/dist/data/__tests__/invite-conformance.test.js.map +0 -1
  81. package/dist/data/__tests__/org-conformance.test.d.ts +0 -2
  82. package/dist/data/__tests__/org-conformance.test.d.ts.map +0 -1
  83. package/dist/data/__tests__/org-conformance.test.js +0 -29
  84. package/dist/data/__tests__/org-conformance.test.js.map +0 -1
  85. package/dist/data/__tests__/project-conformance.test.d.ts +0 -2
  86. package/dist/data/__tests__/project-conformance.test.d.ts.map +0 -1
  87. package/dist/data/__tests__/project-conformance.test.js +0 -57
  88. package/dist/data/__tests__/project-conformance.test.js.map +0 -1
  89. package/dist/data/__tests__/share-link-concurrency.test.d.ts +0 -22
  90. package/dist/data/__tests__/share-link-concurrency.test.d.ts.map +0 -1
  91. package/dist/data/__tests__/share-link-concurrency.test.js +0 -115
  92. package/dist/data/__tests__/share-link-concurrency.test.js.map +0 -1
  93. package/dist/data/__tests__/share-link-conformance.test.d.ts +0 -2
  94. package/dist/data/__tests__/share-link-conformance.test.d.ts.map +0 -1
  95. package/dist/data/__tests__/share-link-conformance.test.js +0 -88
  96. package/dist/data/__tests__/share-link-conformance.test.js.map +0 -1
  97. package/dist/data/__tests__/solve-metric-sink-conformance.test.d.ts +0 -2
  98. package/dist/data/__tests__/solve-metric-sink-conformance.test.d.ts.map +0 -1
  99. package/dist/data/__tests__/solve-metric-sink-conformance.test.js +0 -52
  100. package/dist/data/__tests__/solve-metric-sink-conformance.test.js.map +0 -1
  101. package/dist/data/__tests__/test-helpers.d.ts +0 -52
  102. package/dist/data/__tests__/test-helpers.d.ts.map +0 -1
  103. package/dist/data/__tests__/test-helpers.js +0 -177
  104. package/dist/data/__tests__/test-helpers.js.map +0 -1
  105. package/dist/permissions/__tests__/conformance.test.d.ts +0 -2
  106. package/dist/permissions/__tests__/conformance.test.d.ts.map +0 -1
  107. package/dist/permissions/__tests__/conformance.test.js +0 -27
  108. package/dist/permissions/__tests__/conformance.test.js.map +0 -1
  109. package/dist/storage/__tests__/conformance.test.d.ts +0 -2
  110. package/dist/storage/__tests__/conformance.test.d.ts.map +0 -1
  111. package/dist/storage/__tests__/conformance.test.js +0 -149
  112. package/dist/storage/__tests__/conformance.test.js.map +0 -1
  113. package/dist/userProfile/__tests__/conformance.test.d.ts +0 -2
  114. package/dist/userProfile/__tests__/conformance.test.d.ts.map +0 -1
  115. package/dist/userProfile/__tests__/conformance.test.js +0 -38
  116. package/dist/userProfile/__tests__/conformance.test.js.map +0 -1
  117. package/src/auth/SupabaseAuthProvider.ts +0 -600
  118. package/src/auth/__tests__/conformance.test.ts +0 -78
  119. package/src/auth/__tests__/token-verification.test.ts +0 -158
  120. package/src/auth/index.ts +0 -2
  121. package/src/cli/sync-migrations.ts +0 -133
  122. package/src/data/SupabaseAuditQuery.ts +0 -103
  123. package/src/data/SupabaseComputeServerStore.ts +0 -382
  124. package/src/data/SupabaseDataProvider.ts +0 -153
  125. package/src/data/SupabaseDefinitionStore.ts +0 -527
  126. package/src/data/SupabaseEventSink.ts +0 -30
  127. package/src/data/SupabaseInviteStore.ts +0 -171
  128. package/src/data/SupabaseOrgStore.ts +0 -508
  129. package/src/data/SupabaseProjectStore.ts +0 -431
  130. package/src/data/SupabaseShareLinkStore.ts +0 -176
  131. package/src/data/SupabaseSolveMetricSink.ts +0 -38
  132. package/src/data/__tests__/compute-server-conformance.test.ts +0 -46
  133. package/src/data/__tests__/compute-server-encryption.test.ts +0 -173
  134. package/src/data/__tests__/definition-conformance.test.ts +0 -89
  135. package/src/data/__tests__/event-sink-conformance.test.ts +0 -32
  136. package/src/data/__tests__/invite-conformance.test.ts +0 -46
  137. package/src/data/__tests__/org-conformance.test.ts +0 -30
  138. package/src/data/__tests__/project-conformance.test.ts +0 -57
  139. package/src/data/__tests__/share-link-concurrency.test.ts +0 -124
  140. package/src/data/__tests__/share-link-conformance.test.ts +0 -87
  141. package/src/data/__tests__/solve-metric-sink-conformance.test.ts +0 -59
  142. package/src/data/__tests__/test-helpers.ts +0 -188
  143. package/src/data/client.ts +0 -173
  144. package/src/data/errors.ts +0 -45
  145. package/src/data/index.ts +0 -29
  146. package/src/data/pagination.ts +0 -68
  147. package/src/data/query.ts +0 -22
  148. package/src/data/rowStamp.ts +0 -48
  149. package/src/index.ts +0 -42
  150. package/src/permissions/SupabasePlatformPermissionStore.ts +0 -161
  151. package/src/permissions/__tests__/conformance.test.ts +0 -28
  152. package/src/permissions/index.ts +0 -1
  153. package/src/storage/SupabaseStorageProvider.ts +0 -182
  154. package/src/storage/__tests__/conformance.test.ts +0 -152
  155. package/src/storage/index.ts +0 -2
  156. package/src/userProfile/SupabaseUserProfileProvider.ts +0 -177
  157. package/src/userProfile/__tests__/conformance.test.ts +0 -39
  158. package/src/userProfile/index.ts +0 -1
@@ -1,382 +0,0 @@
1
- import {
2
- isOrgServer,
3
- isPlatformServer,
4
- type IComputeServerStore,
5
- type ComputeConfig,
6
- type ComputeServerConfig,
7
- type RequestContext,
8
- type SecretVerificationFailure,
9
- type SecretVerificationReport
10
- } from '@selvajs/platform';
11
- // Secret crypto functions come from the server-only subpath (they use
12
- // `node:crypto`, kept out of the root barrel so client bundles don't pull it in).
13
- import { decryptSecret, encryptSecret, isEncryptedSecret } from '@selvajs/platform/computeServer';
14
- import type { ClientBundle } from './client.js';
15
- import { mapPostgrestError } from './errors.js';
16
-
17
- /** Explicit column list for `compute_servers` — every field `rowToServer` consumes. */
18
- const SERVER_COLUMNS =
19
- 'id, scope, owner_org_id, shared_with_all, label, server_url, api_key, timeout_ms, retry_count';
20
- /** Columns from the share join table read in `getConfig`. */
21
- const SHARE_COLUMNS = 'server_id, org_id';
22
- /** Columns from the per-org default table read in `getConfig`. */
23
- const ORG_DEFAULT_COLUMNS = 'org_id, default_server_id';
24
-
25
- /**
26
- * Compute-server config backed by three tables:
27
- * - `compute_servers`: one row per server. `scope = 'platform' | 'org'`.
28
- * Platform rows carry a `shared_with_all` flag + `compute_server_shares`
29
- * allowlist; org rows carry `owner_org_id`.
30
- * - `compute_server_shares`: many-to-many for platform servers shared
31
- * with specific orgs (when `shared_with_all = false`).
32
- * - `compute_server_org_defaults`: per-org `orgDefaults[orgId]` choice.
33
- * - `compute_server_platform_default`: single-row sentinel for the global
34
- * `defaultServerId`.
35
- *
36
- * `getConfig` returns the full doc; the visibility predicate is applied by
37
- * callers (resolver, page loaders) using helpers in `@selvajs/platform`.
38
- */
39
- export class SupabaseComputeServerStore implements IComputeServerStore {
40
- /**
41
- * `secretKey` (from `SELVA_AT_REST_KEY`) encrypts `api_key` before it is
42
- * written to `compute_servers.api_key` and decrypts it on read, so the
43
- * Rhino.Compute credential never sits in the DB as plaintext — matching the
44
- * local provider's on-disk envelope. Optional only for legacy construction
45
- * paths (tests that never touch apiKeys); a store built without a key throws
46
- * if asked to write or read a secret. Wire it via `SupabaseDataProvider`.
47
- */
48
- constructor(
49
- private readonly clients: ClientBundle,
50
- private readonly secretKey?: Buffer
51
- ) {}
52
-
53
- async getConfig(ctx: RequestContext): Promise<ComputeConfig> {
54
- const client = this.clients.forRequest(ctx);
55
-
56
- const [serversRes, sharesRes, orgDefRes, platDefRes] = await Promise.all([
57
- client.from('compute_servers').select(SERVER_COLUMNS),
58
- client.from('compute_server_shares').select(SHARE_COLUMNS),
59
- client.from('compute_server_org_defaults').select(ORG_DEFAULT_COLUMNS),
60
- client
61
- .from('compute_server_platform_default')
62
- .select('default_server_id')
63
- .eq('singleton', true)
64
- .maybeSingle()
65
- ]);
66
-
67
- if (serversRes.error) throw mapPostgrestError(serversRes.error);
68
- if (sharesRes.error) throw mapPostgrestError(sharesRes.error);
69
- if (orgDefRes.error) throw mapPostgrestError(orgDefRes.error);
70
- if (platDefRes.error) throw mapPostgrestError(platDefRes.error);
71
-
72
- // Build per-server `sharedWith` allowlists from the join table.
73
- const sharedByServer = new Map<string, string[]>();
74
- for (const row of sharesRes.data ?? []) {
75
- const list = sharedByServer.get(row.server_id) ?? [];
76
- list.push(row.org_id);
77
- sharedByServer.set(row.server_id, list);
78
- }
79
-
80
- const servers = (serversRes.data ?? []).map((row) =>
81
- rowToServer(this.decryptRowApiKey(row), sharedByServer)
82
- );
83
-
84
- const orgDefaults: Record<string, string> = {};
85
- for (const row of orgDefRes.data ?? []) {
86
- if (row.default_server_id) orgDefaults[row.org_id] = row.default_server_id;
87
- }
88
-
89
- return {
90
- servers,
91
- defaultServerId: platDefRes.data?.default_server_id ?? undefined,
92
- orgDefaults
93
- };
94
- }
95
-
96
- async savePlatformServers(
97
- ctx: RequestContext,
98
- servers: ComputeServerConfig[],
99
- defaultServerId: string | undefined
100
- ): Promise<void> {
101
- const client = this.clients.forRequest(ctx);
102
- const platformOnly = servers.filter(isPlatformServer);
103
-
104
- // Replace-all of platform rows.
105
- const { error: delErr } = await client.from('compute_servers').delete().eq('scope', 'platform');
106
- if (delErr) throw mapPostgrestError(delErr);
107
-
108
- if (platformOnly.length > 0) {
109
- const { error: insErr } = await client
110
- .from('compute_servers')
111
- .insert(platformOnly.map((s) => serverToRow(this.encryptServerApiKey(s))));
112
- if (insErr) throw mapPostgrestError(insErr);
113
-
114
- // Rebuild the share rows for any non-`all` platform servers.
115
- const shareRows = platformOnly.flatMap((s) =>
116
- s.sharedWith === 'all'
117
- ? []
118
- : s.sharedWith.map((orgId) => ({ server_id: s.id, org_id: orgId }))
119
- );
120
- if (shareRows.length > 0) {
121
- const { error: shErr } = await client.from('compute_server_shares').insert(shareRows);
122
- if (shErr) throw mapPostgrestError(shErr);
123
- }
124
- }
125
-
126
- const { error: defErr } = await client
127
- .from('compute_server_platform_default')
128
- .update({ default_server_id: defaultServerId ?? null })
129
- .eq('singleton', true);
130
- if (defErr) throw mapPostgrestError(defErr);
131
- }
132
-
133
- async saveOrgServers(
134
- ctx: RequestContext,
135
- orgId: string,
136
- servers: ComputeServerConfig[],
137
- defaultServerId?: string | null
138
- ): Promise<void> {
139
- const client = this.clients.forRequest(ctx);
140
- const orgOnly = servers.filter(isOrgServer).map((s) => ({ ...s, ownerOrgId: orgId }));
141
-
142
- const { error: delErr } = await client
143
- .from('compute_servers')
144
- .delete()
145
- .eq('scope', 'org')
146
- .eq('owner_org_id', orgId);
147
- if (delErr) throw mapPostgrestError(delErr);
148
-
149
- if (orgOnly.length > 0) {
150
- const { error: insErr } = await client
151
- .from('compute_servers')
152
- .insert(orgOnly.map((s) => serverToRow(this.encryptServerApiKey(s))));
153
- if (insErr) throw mapPostgrestError(insErr);
154
- }
155
-
156
- if (defaultServerId === null) {
157
- const { error } = await client
158
- .from('compute_server_org_defaults')
159
- .delete()
160
- .eq('org_id', orgId);
161
- if (error) throw mapPostgrestError(error);
162
- } else if (typeof defaultServerId === 'string') {
163
- const { error } = await client
164
- .from('compute_server_org_defaults')
165
- .upsert({ org_id: orgId, default_server_id: defaultServerId });
166
- if (error) throw mapPostgrestError(error);
167
- }
168
- }
169
-
170
- async setOrgDefault(ctx: RequestContext, orgId: string, serverId: string | null): Promise<void> {
171
- const client = this.clients.forRequest(ctx);
172
- if (serverId === null) {
173
- const { error } = await client
174
- .from('compute_server_org_defaults')
175
- .delete()
176
- .eq('org_id', orgId);
177
- if (error) throw mapPostgrestError(error);
178
- return;
179
- }
180
- const { error } = await client
181
- .from('compute_server_org_defaults')
182
- .upsert({ org_id: orgId, default_server_id: serverId });
183
- if (error) throw mapPostgrestError(error);
184
- }
185
-
186
- async deleteByOrg(ctx: RequestContext, orgId: string): Promise<void> {
187
- const client = this.clients.forRequest(ctx);
188
-
189
- // orgDefaults entry — FK on org_id cascades from orgs(id), but org
190
- // soft-delete doesn't trigger the cascade, so do it explicitly.
191
- const { error: defErr } = await client
192
- .from('compute_server_org_defaults')
193
- .delete()
194
- .eq('org_id', orgId);
195
- if (defErr) throw mapPostgrestError(defErr);
196
-
197
- // Org-private servers owned by this org.
198
- const { error: srvErr } = await client
199
- .from('compute_servers')
200
- .delete()
201
- .eq('scope', 'org')
202
- .eq('owner_org_id', orgId);
203
- if (srvErr) throw mapPostgrestError(srvErr);
204
-
205
- // Strip this org from any platform server share allowlist.
206
- const { error: shErr } = await client
207
- .from('compute_server_shares')
208
- .delete()
209
- .eq('org_id', orgId);
210
- if (shErr) throw mapPostgrestError(shErr);
211
- }
212
-
213
- // ==========================================================================
214
- // At-rest secret handling
215
- // ==========================================================================
216
-
217
- /**
218
- * Encrypt a server's `apiKey` before it is written to the DB. Idempotent —
219
- * an already-enveloped value is passed through, so a round-trip (read →
220
- * save) never double-encrypts. Throws if the store was built without a key
221
- * but a real secret needs writing, so we never silently persist plaintext.
222
- */
223
- private encryptServerApiKey(s: ComputeServerConfig): ComputeServerConfig {
224
- if (!s.apiKey) return s;
225
- if (isEncryptedSecret(s.apiKey)) return s;
226
- if (!this.secretKey) {
227
- throw new Error(
228
- 'Cannot store a compute-server apiKey: SELVA_AT_REST_KEY is not configured. ' +
229
- 'Set it so secrets are encrypted at rest.'
230
- );
231
- }
232
- return { ...s, apiKey: encryptSecret(s.apiKey, this.secretKey) };
233
- }
234
-
235
- /**
236
- * Decrypt a row's `api_key` on read. Tolerant, mirroring the local provider:
237
- * a row whose ciphertext can't be authenticated under the current key is
238
- * returned with `api_key: null` and a warning logged, so the page keeps
239
- * rendering (solves against that server fail later at Rhino.Compute). A
240
- * plaintext value that predates encryption is passed through unchanged and
241
- * surfaced by `verifySecrets()` — never silently dropped.
242
- */
243
- private decryptRowApiKey(row: ServerRow): ServerRow {
244
- if (!row.api_key) return row;
245
- if (!isEncryptedSecret(row.api_key)) {
246
- // Legacy/hand-inserted plaintext. Pass through so an existing key
247
- // keeps working; verifySecrets() flags it for re-save via /admin/compute.
248
- console.warn(
249
- `[selva] compute_servers row "${row.label}" (${row.id}) has a plaintext api_key. ` +
250
- 'Re-save it via /admin/compute so it is stored encrypted.'
251
- );
252
- return row;
253
- }
254
- if (!this.secretKey) {
255
- console.warn(
256
- `[selva] Cannot decrypt api_key for compute server "${row.label}" (${row.id}): ` +
257
- 'SELVA_AT_REST_KEY is not configured. Returning without a key; solves will fail.'
258
- );
259
- return { ...row, api_key: null };
260
- }
261
- try {
262
- return { ...row, api_key: decryptSecret(row.api_key, this.secretKey) };
263
- } catch (cause) {
264
- console.warn(
265
- `[selva] Could not decrypt api_key for compute server "${row.label}" (${row.id}). ` +
266
- 'The stored ciphertext does not match the current SELVA_AT_REST_KEY. ' +
267
- 'This server will be returned without an apiKey; solves against it will fail. ' +
268
- 'Re-enter the key via /admin/compute, or restore the original SELVA_AT_REST_KEY.',
269
- cause
270
- );
271
- return { ...row, api_key: null };
272
- }
273
- }
274
-
275
- /**
276
- * Boot-time integrity check over every `compute_servers` row. Uses the
277
- * service client (no request context at boot) to read raw ciphertext, then
278
- * attempts to decrypt each `api_key`. Does NOT throw — returns a structured
279
- * report for boot health to drive `/api/health`.
280
- */
281
- async verifySecrets(): Promise<SecretVerificationReport> {
282
- const { data, error } = await this.clients.serviceClient
283
- .from('compute_servers')
284
- .select('id, label, api_key');
285
- if (error) throw mapPostgrestError(error);
286
-
287
- const failures: SecretVerificationFailure[] = [];
288
- let plaintextFound = false;
289
-
290
- for (const row of data ?? []) {
291
- const apiKey = (row as { api_key: string | null }).api_key;
292
- const id = (row as { id: string }).id;
293
- const label = (row as { label: string }).label;
294
- if (!apiKey) continue;
295
- if (!isEncryptedSecret(apiKey)) {
296
- plaintextFound = true;
297
- failures.push({ serverId: id, serverLabel: label, reason: 'plaintext_on_disk' });
298
- continue;
299
- }
300
- if (!this.secretKey) {
301
- failures.push({
302
- serverId: id,
303
- serverLabel: label,
304
- reason: 'key_mismatch',
305
- cause: 'SELVA_AT_REST_KEY is not configured'
306
- });
307
- continue;
308
- }
309
- try {
310
- decryptSecret(apiKey, this.secretKey);
311
- } catch (cause) {
312
- failures.push({
313
- serverId: id,
314
- serverLabel: label,
315
- reason: 'key_mismatch',
316
- cause: cause instanceof Error ? cause.message : String(cause)
317
- });
318
- }
319
- }
320
-
321
- return { ok: failures.length === 0, failures, plaintextFound };
322
- }
323
- }
324
-
325
- interface ServerRow {
326
- id: string;
327
- scope: 'platform' | 'org';
328
- owner_org_id: string | null;
329
- shared_with_all: boolean;
330
- label: string;
331
- server_url: string;
332
- api_key: string | null;
333
- timeout_ms: number | null;
334
- retry_count: number | null;
335
- }
336
-
337
- function rowToServer(row: ServerRow, sharedByServer: Map<string, string[]>): ComputeServerConfig {
338
- const common = {
339
- id: row.id,
340
- label: row.label,
341
- serverUrl: row.server_url,
342
- apiKey: row.api_key ?? undefined,
343
- timeoutMs: row.timeout_ms ?? undefined,
344
- retryCount: row.retry_count ?? undefined
345
- };
346
- if (row.scope === 'platform') {
347
- return {
348
- ...common,
349
- scope: 'platform',
350
- sharedWith: row.shared_with_all ? 'all' : (sharedByServer.get(row.id) ?? [])
351
- };
352
- }
353
- if (!row.owner_org_id) {
354
- throw new Error(`compute_servers row ${row.id} has scope='org' but null owner_org_id`);
355
- }
356
- return { ...common, scope: 'org', ownerOrgId: row.owner_org_id };
357
- }
358
-
359
- function serverToRow(s: ComputeServerConfig): ServerRow {
360
- const base = {
361
- id: s.id,
362
- label: s.label,
363
- server_url: s.serverUrl,
364
- api_key: s.apiKey ?? null,
365
- timeout_ms: s.timeoutMs ?? null,
366
- retry_count: s.retryCount ?? null
367
- };
368
- if (isPlatformServer(s)) {
369
- return {
370
- ...base,
371
- scope: 'platform',
372
- owner_org_id: null,
373
- shared_with_all: s.sharedWith === 'all'
374
- };
375
- }
376
- return {
377
- ...base,
378
- scope: 'org',
379
- owner_org_id: s.ownerOrgId,
380
- shared_with_all: false
381
- };
382
- }
@@ -1,153 +0,0 @@
1
- import type { IDataProvider, IEventSink, IPlatformProjectGrantStore } from '@selvajs/platform';
2
- import { ProviderError } from '@selvajs/platform';
3
- import { decodeSecretKey } from '@selvajs/platform/computeServer';
4
- import type { ClientBundle, BuildClientOptions } from './client.js';
5
- import { buildClientBundle, clientBundleFromEnv } from './client.js';
6
- import { SupabaseEventSink } from './SupabaseEventSink.js';
7
- import { SupabaseSolveMetricSink } from './SupabaseSolveMetricSink.js';
8
- import { SupabaseAuditQuery } from './SupabaseAuditQuery.js';
9
- import { SupabaseOrgStore } from './SupabaseOrgStore.js';
10
- import { SupabaseProjectStore } from './SupabaseProjectStore.js';
11
- import { SupabaseDefinitionStore } from './SupabaseDefinitionStore.js';
12
- import { SupabaseInviteStore } from './SupabaseInviteStore.js';
13
- import { SupabaseComputeServerStore } from './SupabaseComputeServerStore.js';
14
- import { SupabaseShareLinkStore } from './SupabaseShareLinkStore.js';
15
- import { SupabaseUserProfileProvider } from '../userProfile/SupabaseUserProfileProvider.js';
16
- import { SupabasePlatformPermissionStore } from '../permissions/SupabasePlatformPermissionStore.js';
17
-
18
- /**
19
- * Stub until the Supabase implementation lands. Reads and mutations surface a
20
- * clear 501; cascade hooks are silent no-ops so deleting an org or project on
21
- * a Supabase deployment without the feature wired does not blow up.
22
- */
23
- const NOT_IMPLEMENTED_MSG = 'Platform projects are not supported on this deployment yet.';
24
- const notImplementedGrantStore: IPlatformProjectGrantStore = {
25
- listByProject: async () => {
26
- throw new ProviderError(NOT_IMPLEMENTED_MSG, 501);
27
- },
28
- create: async () => {
29
- throw new ProviderError(NOT_IMPLEMENTED_MSG, 501);
30
- },
31
- delete: async () => {
32
- throw new ProviderError(NOT_IMPLEMENTED_MSG, 501);
33
- },
34
- deleteByProject: async () => {
35
- // No-op: nothing to clean up on a deployment that never accepted grants.
36
- },
37
- deleteByGranteeOrg: async () => {
38
- // No-op: same reasoning.
39
- }
40
- };
41
-
42
- /**
43
- * Composition of every data store for the Supabase backend. Instantiates one
44
- * `ClientBundle` (service-role + per-request factory) and wires each store
45
- * against it so every store in a single process shares the same pooled clients.
46
- */
47
- export class SupabaseDataProvider implements IDataProvider {
48
- readonly orgs: SupabaseOrgStore;
49
- readonly projects: SupabaseProjectStore;
50
- readonly definitions: SupabaseDefinitionStore;
51
- readonly invites: SupabaseInviteStore;
52
- readonly computeServer: SupabaseComputeServerStore;
53
- readonly shareLinks: SupabaseShareLinkStore;
54
- readonly userProfile: SupabaseUserProfileProvider;
55
- readonly permissions: SupabasePlatformPermissionStore;
56
- readonly platformProjectGrants: IPlatformProjectGrantStore;
57
- readonly auditQuery: SupabaseAuditQuery;
58
- /**
59
- * Per-solve timing sink, built from the same client bundle. Exposed so the
60
- * app's provider wiring can hand it to `SelvaConfig.solveMetrics` without
61
- * constructing a second Supabase client.
62
- */
63
- readonly solveMetrics: SupabaseSolveMetricSink;
64
-
65
- private constructor(
66
- private readonly clients: ClientBundle,
67
- events: IEventSink,
68
- secretKey?: Buffer
69
- ) {
70
- this.solveMetrics = new SupabaseSolveMetricSink(clients);
71
- this.orgs = new SupabaseOrgStore(clients, events);
72
- this.projects = new SupabaseProjectStore(clients, events);
73
- this.definitions = new SupabaseDefinitionStore(clients, events);
74
- this.invites = new SupabaseInviteStore(clients, events);
75
- this.computeServer = new SupabaseComputeServerStore(clients, secretKey);
76
- this.shareLinks = new SupabaseShareLinkStore(clients, events);
77
- this.userProfile = new SupabaseUserProfileProvider(clients);
78
- this.permissions = new SupabasePlatformPermissionStore(clients);
79
- this.platformProjectGrants = notImplementedGrantStore;
80
- this.auditQuery = new SupabaseAuditQuery(clients);
81
- }
82
-
83
- static fromEnv(
84
- env: Record<string, string | undefined>,
85
- events?: IEventSink
86
- ): SupabaseDataProvider {
87
- // Bundle-only env parse (URL + anon + service-role) is shared with
88
- // `clientBundleFromEnv`; the full data provider additionally requires the
89
- // at-rest key below.
90
- const bundle = clientBundleFromEnv(env);
91
- // Required so compute-server apiKeys are encrypted at rest in the DB —
92
- // same guarantee (and same key) as the local provider's on-disk envelope.
93
- if (!env.SELVA_AT_REST_KEY) {
94
- throw new Error(
95
- 'Missing required env var: SELVA_AT_REST_KEY (32-byte hex or base64). ' +
96
- 'Required to encrypt compute-server API keys at rest. ' +
97
- "Generate one with: node -e \"console.log(require('crypto').randomBytes(32).toString('hex'))\""
98
- );
99
- }
100
- const secretKey = decodeSecretKey(env.SELVA_AT_REST_KEY);
101
- return new SupabaseDataProvider(bundle, events ?? new SupabaseEventSink(bundle), secretKey);
102
- }
103
-
104
- static create(
105
- opts: BuildClientOptions,
106
- events?: IEventSink,
107
- secretKey?: Buffer
108
- ): SupabaseDataProvider {
109
- const bundle = buildClientBundle(opts);
110
- return new SupabaseDataProvider(bundle, events ?? new SupabaseEventSink(bundle), secretKey);
111
- }
112
-
113
- /**
114
- * Build from a pre-existing `ClientBundle`. Useful for tests or advanced
115
- * cases that need to inject a custom event sink or share a bundle externally.
116
- * `secretKey` is optional here — omit it only when the caller never touches
117
- * compute-server apiKeys (writes without a key throw rather than persist
118
- * plaintext).
119
- */
120
- static fromBundle(
121
- bundle: ClientBundle,
122
- events?: IEventSink,
123
- secretKey?: Buffer
124
- ): SupabaseDataProvider {
125
- return new SupabaseDataProvider(bundle, events ?? new SupabaseEventSink(bundle), secretKey);
126
- }
127
-
128
- /**
129
- * Expose the underlying bundle so other providers in the same package
130
- * (storage, user profile, auth) can share one set of clients per process.
131
- */
132
- getClientBundle(): ClientBundle {
133
- return this.clients;
134
- }
135
-
136
- /**
137
- * No-op: `public.user_profiles` is auto-seeded by the `handle_new_auth_user`
138
- * trigger on every `auth.users` insert (see `0001_initial.sql`). The data
139
- * layer always has a row for any authenticated user without per-request work.
140
- */
141
- async ensureUser(): Promise<void> {
142
- // Trigger handles it.
143
- }
144
-
145
- /**
146
- * No-op: `public.user_profiles.user_id` references `auth.users(id)` with
147
- * `on delete cascade`. Deleting the auth user removes the profile row
148
- * automatically.
149
- */
150
- async onUserDeleted(): Promise<void> {
151
- // FK cascade handles it.
152
- }
153
- }