@selvajs/supabase-provider 0.14.2 → 0.14.4-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/auth/SupabaseAuthProvider.d.ts +57 -0
- package/dist/auth/SupabaseAuthProvider.d.ts.map +1 -1
- package/dist/auth/SupabaseAuthProvider.js +116 -4
- package/dist/auth/SupabaseAuthProvider.js.map +1 -1
- package/dist/auth/__tests__/token-verification.test.d.ts +13 -0
- package/dist/auth/__tests__/token-verification.test.d.ts.map +1 -0
- package/dist/auth/__tests__/token-verification.test.js +135 -0
- package/dist/auth/__tests__/token-verification.test.js.map +1 -0
- package/dist/data/SupabaseComputeServerStore.d.ts +34 -2
- package/dist/data/SupabaseComputeServerStore.d.ts.map +1 -1
- package/dist/data/SupabaseComputeServerStore.js +146 -39
- package/dist/data/SupabaseComputeServerStore.js.map +1 -1
- package/dist/data/SupabaseDataProvider.d.ts +5 -2
- package/dist/data/SupabaseDataProvider.d.ts.map +1 -1
- package/dist/data/SupabaseDataProvider.js +24 -18
- package/dist/data/SupabaseDataProvider.js.map +1 -1
- package/dist/data/SupabaseDefinitionStore.d.ts +2 -1
- package/dist/data/SupabaseDefinitionStore.d.ts.map +1 -1
- package/dist/data/SupabaseDefinitionStore.js +68 -67
- package/dist/data/SupabaseDefinitionStore.js.map +1 -1
- package/dist/data/SupabaseInviteStore.d.ts.map +1 -1
- package/dist/data/SupabaseInviteStore.js +11 -22
- package/dist/data/SupabaseInviteStore.js.map +1 -1
- package/dist/data/SupabaseOrgStore.d.ts +1 -1
- package/dist/data/SupabaseOrgStore.d.ts.map +1 -1
- package/dist/data/SupabaseOrgStore.js +54 -86
- package/dist/data/SupabaseOrgStore.js.map +1 -1
- package/dist/data/SupabaseProjectStore.d.ts.map +1 -1
- package/dist/data/SupabaseProjectStore.js +51 -70
- package/dist/data/SupabaseProjectStore.js.map +1 -1
- package/dist/data/SupabaseShareLinkStore.d.ts.map +1 -1
- package/dist/data/SupabaseShareLinkStore.js +13 -27
- package/dist/data/SupabaseShareLinkStore.js.map +1 -1
- package/dist/data/__tests__/compute-server-conformance.test.js +6 -1
- package/dist/data/__tests__/compute-server-conformance.test.js.map +1 -1
- package/dist/data/__tests__/compute-server-encryption.test.d.ts +12 -0
- package/dist/data/__tests__/compute-server-encryption.test.d.ts.map +1 -0
- package/dist/data/__tests__/compute-server-encryption.test.js +143 -0
- package/dist/data/__tests__/compute-server-encryption.test.js.map +1 -0
- package/dist/data/__tests__/share-link-concurrency.test.d.ts +22 -0
- package/dist/data/__tests__/share-link-concurrency.test.d.ts.map +1 -0
- package/dist/data/__tests__/share-link-concurrency.test.js +115 -0
- package/dist/data/__tests__/share-link-concurrency.test.js.map +1 -0
- package/dist/data/client.d.ts +35 -3
- package/dist/data/client.d.ts.map +1 -1
- package/dist/data/client.js +67 -16
- package/dist/data/client.js.map +1 -1
- package/dist/data/errors.d.ts +2 -0
- package/dist/data/errors.d.ts.map +1 -0
- package/dist/data/errors.js +23 -0
- package/dist/data/errors.js.map +1 -0
- package/dist/data/index.d.ts +7 -2
- package/dist/data/index.d.ts.map +1 -1
- package/dist/data/index.js +5 -1
- package/dist/data/index.js.map +1 -1
- package/dist/data/query.d.ts +20 -0
- package/dist/data/query.d.ts.map +1 -0
- package/dist/data/query.js +13 -0
- package/dist/data/query.js.map +1 -0
- package/dist/data/rowStamp.d.ts +38 -0
- package/dist/data/rowStamp.d.ts.map +1 -0
- package/dist/data/rowStamp.js +43 -0
- package/dist/data/rowStamp.js.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/storage/SupabaseStorageProvider.d.ts +7 -5
- package/dist/storage/SupabaseStorageProvider.d.ts.map +1 -1
- package/dist/storage/SupabaseStorageProvider.js +14 -7
- package/dist/storage/SupabaseStorageProvider.js.map +1 -1
- package/package.json +2 -2
- package/src/auth/SupabaseAuthProvider.ts +140 -4
- package/src/auth/__tests__/token-verification.test.ts +158 -0
- package/src/data/SupabaseComputeServerStore.ts +163 -43
- package/src/data/SupabaseDataProvider.ts +35 -15
- package/src/data/SupabaseDefinitionStore.ts +73 -66
- package/src/data/SupabaseInviteStore.ts +13 -25
- package/src/data/SupabaseOrgStore.ts +57 -96
- package/src/data/SupabaseProjectStore.ts +55 -78
- package/src/data/SupabaseShareLinkStore.ts +15 -30
- package/src/data/__tests__/compute-server-conformance.test.ts +7 -1
- package/src/data/__tests__/compute-server-encryption.test.ts +173 -0
- package/src/data/__tests__/share-link-concurrency.test.ts +124 -0
- package/src/data/client.ts +95 -20
- package/src/data/errors.ts +45 -0
- package/src/data/index.ts +19 -2
- package/src/data/query.ts +22 -0
- package/src/data/rowStamp.ts +48 -0
- package/src/index.ts +20 -2
- package/src/storage/SupabaseStorageProvider.ts +12 -7
- package/supabase/config.toml +6 -6
- package/supabase/migrations/20260630120000_org_assets.sql +19 -0
- package/supabase/migrations/20260711120000_selva_definition_schema_version.sql +18 -0
|
@@ -4,10 +4,23 @@ import {
|
|
|
4
4
|
type IComputeServerStore,
|
|
5
5
|
type ComputeConfig,
|
|
6
6
|
type ComputeServerConfig,
|
|
7
|
-
type RequestContext
|
|
7
|
+
type RequestContext,
|
|
8
|
+
type SecretVerificationFailure,
|
|
9
|
+
type SecretVerificationReport
|
|
8
10
|
} from '@selvajs/platform';
|
|
9
|
-
|
|
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';
|
|
10
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';
|
|
11
24
|
|
|
12
25
|
/**
|
|
13
26
|
* Compute-server config backed by three tables:
|
|
@@ -24,15 +37,26 @@ import type { ClientBundle } from './client.js';
|
|
|
24
37
|
* callers (resolver, page loaders) using helpers in `@selvajs/platform`.
|
|
25
38
|
*/
|
|
26
39
|
export class SupabaseComputeServerStore implements IComputeServerStore {
|
|
27
|
-
|
|
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
|
+
) {}
|
|
28
52
|
|
|
29
53
|
async getConfig(ctx: RequestContext): Promise<ComputeConfig> {
|
|
30
54
|
const client = this.clients.forRequest(ctx);
|
|
31
55
|
|
|
32
56
|
const [serversRes, sharesRes, orgDefRes, platDefRes] = await Promise.all([
|
|
33
|
-
client.from('compute_servers').select(
|
|
34
|
-
client.from('compute_server_shares').select(
|
|
35
|
-
client.from('compute_server_org_defaults').select(
|
|
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),
|
|
36
60
|
client
|
|
37
61
|
.from('compute_server_platform_default')
|
|
38
62
|
.select('default_server_id')
|
|
@@ -40,10 +64,10 @@ export class SupabaseComputeServerStore implements IComputeServerStore {
|
|
|
40
64
|
.maybeSingle()
|
|
41
65
|
]);
|
|
42
66
|
|
|
43
|
-
if (serversRes.error) throw
|
|
44
|
-
if (sharesRes.error) throw
|
|
45
|
-
if (orgDefRes.error) throw
|
|
46
|
-
if (platDefRes.error) throw
|
|
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);
|
|
47
71
|
|
|
48
72
|
// Build per-server `sharedWith` allowlists from the join table.
|
|
49
73
|
const sharedByServer = new Map<string, string[]>();
|
|
@@ -53,7 +77,9 @@ export class SupabaseComputeServerStore implements IComputeServerStore {
|
|
|
53
77
|
sharedByServer.set(row.server_id, list);
|
|
54
78
|
}
|
|
55
79
|
|
|
56
|
-
const servers = (serversRes.data ?? []).map((row) =>
|
|
80
|
+
const servers = (serversRes.data ?? []).map((row) =>
|
|
81
|
+
rowToServer(this.decryptRowApiKey(row), sharedByServer)
|
|
82
|
+
);
|
|
57
83
|
|
|
58
84
|
const orgDefaults: Record<string, string> = {};
|
|
59
85
|
for (const row of orgDefRes.data ?? []) {
|
|
@@ -77,13 +103,13 @@ export class SupabaseComputeServerStore implements IComputeServerStore {
|
|
|
77
103
|
|
|
78
104
|
// Replace-all of platform rows.
|
|
79
105
|
const { error: delErr } = await client.from('compute_servers').delete().eq('scope', 'platform');
|
|
80
|
-
if (delErr) throw
|
|
106
|
+
if (delErr) throw mapPostgrestError(delErr);
|
|
81
107
|
|
|
82
108
|
if (platformOnly.length > 0) {
|
|
83
109
|
const { error: insErr } = await client
|
|
84
110
|
.from('compute_servers')
|
|
85
|
-
.insert(platformOnly.map(serverToRow));
|
|
86
|
-
if (insErr) throw
|
|
111
|
+
.insert(platformOnly.map((s) => serverToRow(this.encryptServerApiKey(s))));
|
|
112
|
+
if (insErr) throw mapPostgrestError(insErr);
|
|
87
113
|
|
|
88
114
|
// Rebuild the share rows for any non-`all` platform servers.
|
|
89
115
|
const shareRows = platformOnly.flatMap((s) =>
|
|
@@ -93,7 +119,7 @@ export class SupabaseComputeServerStore implements IComputeServerStore {
|
|
|
93
119
|
);
|
|
94
120
|
if (shareRows.length > 0) {
|
|
95
121
|
const { error: shErr } = await client.from('compute_server_shares').insert(shareRows);
|
|
96
|
-
if (shErr) throw
|
|
122
|
+
if (shErr) throw mapPostgrestError(shErr);
|
|
97
123
|
}
|
|
98
124
|
}
|
|
99
125
|
|
|
@@ -101,7 +127,7 @@ export class SupabaseComputeServerStore implements IComputeServerStore {
|
|
|
101
127
|
.from('compute_server_platform_default')
|
|
102
128
|
.update({ default_server_id: defaultServerId ?? null })
|
|
103
129
|
.eq('singleton', true);
|
|
104
|
-
if (defErr) throw
|
|
130
|
+
if (defErr) throw mapPostgrestError(defErr);
|
|
105
131
|
}
|
|
106
132
|
|
|
107
133
|
async saveOrgServers(
|
|
@@ -118,13 +144,13 @@ export class SupabaseComputeServerStore implements IComputeServerStore {
|
|
|
118
144
|
.delete()
|
|
119
145
|
.eq('scope', 'org')
|
|
120
146
|
.eq('owner_org_id', orgId);
|
|
121
|
-
if (delErr) throw
|
|
147
|
+
if (delErr) throw mapPostgrestError(delErr);
|
|
122
148
|
|
|
123
149
|
if (orgOnly.length > 0) {
|
|
124
150
|
const { error: insErr } = await client
|
|
125
151
|
.from('compute_servers')
|
|
126
|
-
.insert(orgOnly.map(serverToRow));
|
|
127
|
-
if (insErr) throw
|
|
152
|
+
.insert(orgOnly.map((s) => serverToRow(this.encryptServerApiKey(s))));
|
|
153
|
+
if (insErr) throw mapPostgrestError(insErr);
|
|
128
154
|
}
|
|
129
155
|
|
|
130
156
|
if (defaultServerId === null) {
|
|
@@ -132,12 +158,12 @@ export class SupabaseComputeServerStore implements IComputeServerStore {
|
|
|
132
158
|
.from('compute_server_org_defaults')
|
|
133
159
|
.delete()
|
|
134
160
|
.eq('org_id', orgId);
|
|
135
|
-
if (error) throw
|
|
161
|
+
if (error) throw mapPostgrestError(error);
|
|
136
162
|
} else if (typeof defaultServerId === 'string') {
|
|
137
163
|
const { error } = await client
|
|
138
164
|
.from('compute_server_org_defaults')
|
|
139
165
|
.upsert({ org_id: orgId, default_server_id: defaultServerId });
|
|
140
|
-
if (error) throw
|
|
166
|
+
if (error) throw mapPostgrestError(error);
|
|
141
167
|
}
|
|
142
168
|
}
|
|
143
169
|
|
|
@@ -148,13 +174,13 @@ export class SupabaseComputeServerStore implements IComputeServerStore {
|
|
|
148
174
|
.from('compute_server_org_defaults')
|
|
149
175
|
.delete()
|
|
150
176
|
.eq('org_id', orgId);
|
|
151
|
-
if (error) throw
|
|
177
|
+
if (error) throw mapPostgrestError(error);
|
|
152
178
|
return;
|
|
153
179
|
}
|
|
154
180
|
const { error } = await client
|
|
155
181
|
.from('compute_server_org_defaults')
|
|
156
182
|
.upsert({ org_id: orgId, default_server_id: serverId });
|
|
157
|
-
if (error) throw
|
|
183
|
+
if (error) throw mapPostgrestError(error);
|
|
158
184
|
}
|
|
159
185
|
|
|
160
186
|
async deleteByOrg(ctx: RequestContext, orgId: string): Promise<void> {
|
|
@@ -166,7 +192,7 @@ export class SupabaseComputeServerStore implements IComputeServerStore {
|
|
|
166
192
|
.from('compute_server_org_defaults')
|
|
167
193
|
.delete()
|
|
168
194
|
.eq('org_id', orgId);
|
|
169
|
-
if (defErr) throw
|
|
195
|
+
if (defErr) throw mapPostgrestError(defErr);
|
|
170
196
|
|
|
171
197
|
// Org-private servers owned by this org.
|
|
172
198
|
const { error: srvErr } = await client
|
|
@@ -174,14 +200,125 @@ export class SupabaseComputeServerStore implements IComputeServerStore {
|
|
|
174
200
|
.delete()
|
|
175
201
|
.eq('scope', 'org')
|
|
176
202
|
.eq('owner_org_id', orgId);
|
|
177
|
-
if (srvErr) throw
|
|
203
|
+
if (srvErr) throw mapPostgrestError(srvErr);
|
|
178
204
|
|
|
179
205
|
// Strip this org from any platform server share allowlist.
|
|
180
206
|
const { error: shErr } = await client
|
|
181
207
|
.from('compute_server_shares')
|
|
182
208
|
.delete()
|
|
183
209
|
.eq('org_id', orgId);
|
|
184
|
-
if (shErr) throw
|
|
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 };
|
|
185
322
|
}
|
|
186
323
|
}
|
|
187
324
|
|
|
@@ -243,20 +380,3 @@ function serverToRow(s: ComputeServerConfig): ServerRow {
|
|
|
243
380
|
shared_with_all: false
|
|
244
381
|
};
|
|
245
382
|
}
|
|
246
|
-
|
|
247
|
-
interface PostgrestError {
|
|
248
|
-
code?: string;
|
|
249
|
-
message?: string;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
function mapError(e: unknown): Error {
|
|
253
|
-
const pg = e as PostgrestError;
|
|
254
|
-
if (pg?.code === '23505') return new ProviderError(pg.message ?? 'Duplicate record', 409);
|
|
255
|
-
if (pg?.code === '23503') return new ProviderError(pg.message ?? 'Foreign key violation', 409);
|
|
256
|
-
if (e instanceof Error) return e;
|
|
257
|
-
if (e && typeof e === 'object') {
|
|
258
|
-
const obj = e as { message?: string; code?: string };
|
|
259
|
-
return new Error(obj.code ? `[${obj.code}] ${obj.message ?? ''}` : (obj.message ?? String(e)));
|
|
260
|
-
}
|
|
261
|
-
return new Error(String(e));
|
|
262
|
-
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { IDataProvider, IEventSink, IPlatformProjectGrantStore } from '@selvajs/platform';
|
|
2
2
|
import { ProviderError } from '@selvajs/platform';
|
|
3
|
+
import { decodeSecretKey } from '@selvajs/platform/computeServer';
|
|
3
4
|
import type { ClientBundle, BuildClientOptions } from './client.js';
|
|
4
|
-
import { buildClientBundle } from './client.js';
|
|
5
|
+
import { buildClientBundle, clientBundleFromEnv } from './client.js';
|
|
5
6
|
import { SupabaseEventSink } from './SupabaseEventSink.js';
|
|
6
7
|
import { SupabaseSolveMetricSink } from './SupabaseSolveMetricSink.js';
|
|
7
8
|
import { SupabaseAuditQuery } from './SupabaseAuditQuery.js';
|
|
@@ -63,14 +64,15 @@ export class SupabaseDataProvider implements IDataProvider {
|
|
|
63
64
|
|
|
64
65
|
private constructor(
|
|
65
66
|
private readonly clients: ClientBundle,
|
|
66
|
-
events: IEventSink
|
|
67
|
+
events: IEventSink,
|
|
68
|
+
secretKey?: Buffer
|
|
67
69
|
) {
|
|
68
70
|
this.solveMetrics = new SupabaseSolveMetricSink(clients);
|
|
69
71
|
this.orgs = new SupabaseOrgStore(clients, events);
|
|
70
72
|
this.projects = new SupabaseProjectStore(clients, events);
|
|
71
73
|
this.definitions = new SupabaseDefinitionStore(clients, events);
|
|
72
74
|
this.invites = new SupabaseInviteStore(clients, events);
|
|
73
|
-
this.computeServer = new SupabaseComputeServerStore(clients);
|
|
75
|
+
this.computeServer = new SupabaseComputeServerStore(clients, secretKey);
|
|
74
76
|
this.shareLinks = new SupabaseShareLinkStore(clients, events);
|
|
75
77
|
this.userProfile = new SupabaseUserProfileProvider(clients);
|
|
76
78
|
this.permissions = new SupabasePlatformPermissionStore(clients);
|
|
@@ -82,27 +84,45 @@ export class SupabaseDataProvider implements IDataProvider {
|
|
|
82
84
|
env: Record<string, string | undefined>,
|
|
83
85
|
events?: IEventSink
|
|
84
86
|
): SupabaseDataProvider {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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);
|
|
93
102
|
}
|
|
94
103
|
|
|
95
|
-
static create(
|
|
104
|
+
static create(
|
|
105
|
+
opts: BuildClientOptions,
|
|
106
|
+
events?: IEventSink,
|
|
107
|
+
secretKey?: Buffer
|
|
108
|
+
): SupabaseDataProvider {
|
|
96
109
|
const bundle = buildClientBundle(opts);
|
|
97
|
-
return new SupabaseDataProvider(bundle, events ?? new SupabaseEventSink(bundle));
|
|
110
|
+
return new SupabaseDataProvider(bundle, events ?? new SupabaseEventSink(bundle), secretKey);
|
|
98
111
|
}
|
|
99
112
|
|
|
100
113
|
/**
|
|
101
114
|
* Build from a pre-existing `ClientBundle`. Useful for tests or advanced
|
|
102
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).
|
|
103
119
|
*/
|
|
104
|
-
static fromBundle(
|
|
105
|
-
|
|
120
|
+
static fromBundle(
|
|
121
|
+
bundle: ClientBundle,
|
|
122
|
+
events?: IEventSink,
|
|
123
|
+
secretKey?: Buffer
|
|
124
|
+
): SupabaseDataProvider {
|
|
125
|
+
return new SupabaseDataProvider(bundle, events ?? new SupabaseEventSink(bundle), secretKey);
|
|
106
126
|
}
|
|
107
127
|
|
|
108
128
|
/**
|