@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.
- package/LICENSE +1 -1
- package/README.md +2 -0
- package/dist/auth/SupabaseAuthProvider.d.ts.map +1 -1
- package/dist/auth/SupabaseAuthProvider.js +7 -14
- package/dist/auth/SupabaseAuthProvider.js.map +1 -1
- package/dist/data/SupabaseComputeServerStore.d.ts +14 -3
- package/dist/data/SupabaseComputeServerStore.d.ts.map +1 -1
- package/dist/data/SupabaseComputeServerStore.js +67 -16
- package/dist/data/SupabaseComputeServerStore.js.map +1 -1
- package/dist/data/SupabaseDataProvider.d.ts +36 -8
- package/dist/data/SupabaseDataProvider.d.ts.map +1 -1
- package/dist/data/SupabaseDataProvider.js +119 -15
- package/dist/data/SupabaseDataProvider.js.map +1 -1
- package/dist/data/SupabaseDefinitionStore.d.ts +1 -0
- package/dist/data/SupabaseDefinitionStore.d.ts.map +1 -1
- package/dist/data/SupabaseDefinitionStore.js +41 -4
- package/dist/data/SupabaseDefinitionStore.js.map +1 -1
- package/dist/data/SupabaseEventSink.d.ts +5 -2
- package/dist/data/SupabaseEventSink.d.ts.map +1 -1
- package/dist/data/SupabaseEventSink.js +20 -3
- package/dist/data/SupabaseEventSink.js.map +1 -1
- package/dist/data/SupabaseSolveMetricSink.d.ts +49 -4
- package/dist/data/SupabaseSolveMetricSink.d.ts.map +1 -1
- package/dist/data/SupabaseSolveMetricSink.js +109 -19
- package/dist/data/SupabaseSolveMetricSink.js.map +1 -1
- package/dist/data/index.d.ts +1 -0
- package/dist/data/index.d.ts.map +1 -1
- package/dist/data/index.js +1 -0
- package/dist/data/index.js.map +1 -1
- package/dist/data/migrationHead.d.ts +14 -0
- package/dist/data/migrationHead.d.ts.map +1 -0
- package/dist/data/migrationHead.js +14 -0
- package/dist/data/migrationHead.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/permissions/SupabasePlatformPermissionStore.d.ts +5 -3
- package/dist/permissions/SupabasePlatformPermissionStore.d.ts.map +1 -1
- package/dist/permissions/SupabasePlatformPermissionStore.js +17 -30
- package/dist/permissions/SupabasePlatformPermissionStore.js.map +1 -1
- package/dist/storage/SupabaseStorageProvider.d.ts.map +1 -1
- package/dist/storage/SupabaseStorageProvider.js +3 -0
- package/dist/storage/SupabaseStorageProvider.js.map +1 -1
- package/package.json +23 -14
- package/supabase/migrations/20260712120000_user_profiles_disabled_and_gin.sql +95 -0
- package/supabase/migrations/20260712123000_selva_migration_head_fn.sql +38 -0
- package/supabase/migrations/20260713120000_selva_next_version_number.sql +59 -0
- package/supabase/migrations/20260713140000_selva_solve_cache_limit.sql +15 -0
- package/supabase/migrations/20260716120000_definition_status_and_audit_version.sql +36 -0
- package/supabase/migrations/20260716130000_erase_user_audit_email.sql +36 -0
- package/supabase/migrations/20260717120000_compute_server_has_api_key.sql +20 -0
- package/dist/auth/__tests__/conformance.test.d.ts +0 -2
- package/dist/auth/__tests__/conformance.test.d.ts.map +0 -1
- package/dist/auth/__tests__/conformance.test.js +0 -76
- package/dist/auth/__tests__/conformance.test.js.map +0 -1
- package/dist/auth/__tests__/token-verification.test.d.ts +0 -13
- package/dist/auth/__tests__/token-verification.test.d.ts.map +0 -1
- package/dist/auth/__tests__/token-verification.test.js +0 -135
- package/dist/auth/__tests__/token-verification.test.js.map +0 -1
- package/dist/data/__tests__/compute-server-conformance.test.d.ts +0 -2
- package/dist/data/__tests__/compute-server-conformance.test.d.ts.map +0 -1
- package/dist/data/__tests__/compute-server-conformance.test.js +0 -45
- package/dist/data/__tests__/compute-server-conformance.test.js.map +0 -1
- package/dist/data/__tests__/compute-server-encryption.test.d.ts +0 -12
- package/dist/data/__tests__/compute-server-encryption.test.d.ts.map +0 -1
- package/dist/data/__tests__/compute-server-encryption.test.js +0 -143
- package/dist/data/__tests__/compute-server-encryption.test.js.map +0 -1
- package/dist/data/__tests__/definition-conformance.test.d.ts +0 -2
- package/dist/data/__tests__/definition-conformance.test.d.ts.map +0 -1
- package/dist/data/__tests__/definition-conformance.test.js +0 -85
- package/dist/data/__tests__/definition-conformance.test.js.map +0 -1
- package/dist/data/__tests__/event-sink-conformance.test.d.ts +0 -2
- package/dist/data/__tests__/event-sink-conformance.test.d.ts.map +0 -1
- package/dist/data/__tests__/event-sink-conformance.test.js +0 -27
- package/dist/data/__tests__/event-sink-conformance.test.js.map +0 -1
- package/dist/data/__tests__/invite-conformance.test.d.ts +0 -2
- package/dist/data/__tests__/invite-conformance.test.d.ts.map +0 -1
- package/dist/data/__tests__/invite-conformance.test.js +0 -45
- package/dist/data/__tests__/invite-conformance.test.js.map +0 -1
- package/dist/data/__tests__/org-conformance.test.d.ts +0 -2
- package/dist/data/__tests__/org-conformance.test.d.ts.map +0 -1
- package/dist/data/__tests__/org-conformance.test.js +0 -29
- package/dist/data/__tests__/org-conformance.test.js.map +0 -1
- package/dist/data/__tests__/project-conformance.test.d.ts +0 -2
- package/dist/data/__tests__/project-conformance.test.d.ts.map +0 -1
- package/dist/data/__tests__/project-conformance.test.js +0 -57
- package/dist/data/__tests__/project-conformance.test.js.map +0 -1
- package/dist/data/__tests__/share-link-concurrency.test.d.ts +0 -22
- package/dist/data/__tests__/share-link-concurrency.test.d.ts.map +0 -1
- package/dist/data/__tests__/share-link-concurrency.test.js +0 -115
- package/dist/data/__tests__/share-link-concurrency.test.js.map +0 -1
- package/dist/data/__tests__/share-link-conformance.test.d.ts +0 -2
- package/dist/data/__tests__/share-link-conformance.test.d.ts.map +0 -1
- package/dist/data/__tests__/share-link-conformance.test.js +0 -88
- package/dist/data/__tests__/share-link-conformance.test.js.map +0 -1
- package/dist/data/__tests__/solve-metric-sink-conformance.test.d.ts +0 -2
- package/dist/data/__tests__/solve-metric-sink-conformance.test.d.ts.map +0 -1
- package/dist/data/__tests__/solve-metric-sink-conformance.test.js +0 -52
- package/dist/data/__tests__/solve-metric-sink-conformance.test.js.map +0 -1
- package/dist/data/__tests__/test-helpers.d.ts +0 -52
- package/dist/data/__tests__/test-helpers.d.ts.map +0 -1
- package/dist/data/__tests__/test-helpers.js +0 -177
- package/dist/data/__tests__/test-helpers.js.map +0 -1
- package/dist/permissions/__tests__/conformance.test.d.ts +0 -2
- package/dist/permissions/__tests__/conformance.test.d.ts.map +0 -1
- package/dist/permissions/__tests__/conformance.test.js +0 -27
- package/dist/permissions/__tests__/conformance.test.js.map +0 -1
- package/dist/storage/__tests__/conformance.test.d.ts +0 -2
- package/dist/storage/__tests__/conformance.test.d.ts.map +0 -1
- package/dist/storage/__tests__/conformance.test.js +0 -149
- package/dist/storage/__tests__/conformance.test.js.map +0 -1
- package/dist/userProfile/__tests__/conformance.test.d.ts +0 -2
- package/dist/userProfile/__tests__/conformance.test.d.ts.map +0 -1
- package/dist/userProfile/__tests__/conformance.test.js +0 -38
- package/dist/userProfile/__tests__/conformance.test.js.map +0 -1
- package/src/auth/SupabaseAuthProvider.ts +0 -600
- package/src/auth/__tests__/conformance.test.ts +0 -78
- package/src/auth/__tests__/token-verification.test.ts +0 -158
- package/src/auth/index.ts +0 -2
- package/src/cli/sync-migrations.ts +0 -133
- package/src/data/SupabaseAuditQuery.ts +0 -103
- package/src/data/SupabaseComputeServerStore.ts +0 -382
- package/src/data/SupabaseDataProvider.ts +0 -153
- package/src/data/SupabaseDefinitionStore.ts +0 -527
- package/src/data/SupabaseEventSink.ts +0 -30
- package/src/data/SupabaseInviteStore.ts +0 -171
- package/src/data/SupabaseOrgStore.ts +0 -508
- package/src/data/SupabaseProjectStore.ts +0 -431
- package/src/data/SupabaseShareLinkStore.ts +0 -176
- package/src/data/SupabaseSolveMetricSink.ts +0 -38
- package/src/data/__tests__/compute-server-conformance.test.ts +0 -46
- package/src/data/__tests__/compute-server-encryption.test.ts +0 -173
- package/src/data/__tests__/definition-conformance.test.ts +0 -89
- package/src/data/__tests__/event-sink-conformance.test.ts +0 -32
- package/src/data/__tests__/invite-conformance.test.ts +0 -46
- package/src/data/__tests__/org-conformance.test.ts +0 -30
- package/src/data/__tests__/project-conformance.test.ts +0 -57
- package/src/data/__tests__/share-link-concurrency.test.ts +0 -124
- package/src/data/__tests__/share-link-conformance.test.ts +0 -87
- package/src/data/__tests__/solve-metric-sink-conformance.test.ts +0 -59
- package/src/data/__tests__/test-helpers.ts +0 -188
- package/src/data/client.ts +0 -173
- package/src/data/errors.ts +0 -45
- package/src/data/index.ts +0 -29
- package/src/data/pagination.ts +0 -68
- package/src/data/query.ts +0 -22
- package/src/data/rowStamp.ts +0 -48
- package/src/index.ts +0 -42
- package/src/permissions/SupabasePlatformPermissionStore.ts +0 -161
- package/src/permissions/__tests__/conformance.test.ts +0 -28
- package/src/permissions/index.ts +0 -1
- package/src/storage/SupabaseStorageProvider.ts +0 -182
- package/src/storage/__tests__/conformance.test.ts +0 -152
- package/src/storage/index.ts +0 -2
- package/src/userProfile/SupabaseUserProfileProvider.ts +0 -177
- package/src/userProfile/__tests__/conformance.test.ts +0 -39
- 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
|
-
}
|