@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,173 +0,0 @@
1
- /**
2
- * CI-runnable (no live stack) unit tests for at-rest encryption of
3
- * `compute_servers.api_key`. Uses a minimal fake of the Supabase query builder
4
- * that records `insert` payloads and serves canned `select` rows, so we can
5
- * assert the exact bytes that would reach the DB without a real Postgres.
6
- *
7
- * The security property under test: a plaintext apiKey handed to the store is
8
- * NEVER written verbatim — the DB only ever sees an `enc:v1:` envelope — and it
9
- * round-trips back to plaintext on read.
10
- */
11
-
12
- import { describe, it, expect, vi, beforeEach } from 'vitest';
13
- import { randomBytes } from 'node:crypto';
14
- import { isEncryptedSecret, decryptSecret } from '@selvajs/platform/computeServer';
15
- import type { RequestContext } from '@selvajs/platform';
16
- import { SupabaseComputeServerStore } from '../SupabaseComputeServerStore.js';
17
- import type { ClientBundle } from '../client.js';
18
-
19
- const SYSTEM_CTX = { userId: '', system: true } as unknown as RequestContext;
20
- const KEY = randomBytes(32);
21
-
22
- /**
23
- * Records every `insert` payload and returns canned rows for each table's
24
- * `select`. Only the fragments the store actually chains are implemented; the
25
- * builder is thenable so `await client.from(...).select(...)` resolves.
26
- */
27
- function fakeBundle(selectRows: Record<string, unknown[]> = {}) {
28
- const inserted: Record<string, unknown[]> = {};
29
-
30
- function builder(table: string) {
31
- const result = { data: selectRows[table] ?? [], error: null };
32
- const chain: Record<string, unknown> = {
33
- select: () => ({
34
- ...chain,
35
- eq: () => ({ ...chain, maybeSingle: async () => ({ data: null, error: null }) }),
36
- then: (resolve: (v: unknown) => unknown) => resolve(result)
37
- }),
38
- insert: (rows: unknown) => {
39
- inserted[table] = (inserted[table] ?? []).concat(rows as unknown[]);
40
- return { error: null };
41
- },
42
- delete: () => ({ eq: () => ({ eq: () => ({ error: null }), error: null }) }),
43
- update: () => ({ eq: () => ({ error: null }) }),
44
- upsert: () => ({ error: null })
45
- };
46
- return chain;
47
- }
48
-
49
- const client = { from: (t: string) => builder(t) };
50
- const bundle = {
51
- serviceClient: client,
52
- forRequest: () => client
53
- } as unknown as ClientBundle;
54
- return { bundle, inserted };
55
- }
56
-
57
- describe('SupabaseComputeServerStore — apiKey encryption at rest', () => {
58
- beforeEach(() => vi.restoreAllMocks());
59
-
60
- it('encrypts apiKey before insert — DB never sees plaintext', async () => {
61
- const { bundle, inserted } = fakeBundle();
62
- const store = new SupabaseComputeServerStore(bundle, KEY);
63
-
64
- await store.savePlatformServers(
65
- SYSTEM_CTX,
66
- [
67
- {
68
- id: 's1',
69
- label: 'prod',
70
- serverUrl: 'https://compute.example.com',
71
- apiKey: 'super-secret-key',
72
- scope: 'platform',
73
- sharedWith: 'all'
74
- }
75
- ],
76
- 's1'
77
- );
78
-
79
- const rows = inserted['compute_servers'] as Array<{ api_key: string }>;
80
- expect(rows).toHaveLength(1);
81
- expect(rows[0].api_key).not.toBe('super-secret-key');
82
- expect(isEncryptedSecret(rows[0].api_key)).toBe(true);
83
- // And it's genuinely our plaintext under the envelope.
84
- expect(decryptSecret(rows[0].api_key, KEY)).toBe('super-secret-key');
85
- });
86
-
87
- it('decrypts apiKey on read — callers see plaintext', async () => {
88
- // Seed the "DB" with an already-encrypted row.
89
- const { encryptSecret } = await import('@selvajs/platform/computeServer');
90
- const envelope = encryptSecret('round-trip-key', KEY);
91
- const { bundle } = fakeBundle({
92
- compute_servers: [
93
- {
94
- id: 's1',
95
- scope: 'platform',
96
- owner_org_id: null,
97
- shared_with_all: true,
98
- label: 'prod',
99
- server_url: 'https://compute.example.com',
100
- api_key: envelope,
101
- timeout_ms: null,
102
- retry_count: null
103
- }
104
- ]
105
- });
106
- const store = new SupabaseComputeServerStore(bundle, KEY);
107
-
108
- const config = await store.getConfig(SYSTEM_CTX);
109
- expect(config.servers).toHaveLength(1);
110
- expect(config.servers[0].apiKey).toBe('round-trip-key');
111
- });
112
-
113
- it('throws rather than persist plaintext when no key is configured', async () => {
114
- const { bundle } = fakeBundle();
115
- const store = new SupabaseComputeServerStore(bundle); // no key
116
-
117
- await expect(
118
- store.savePlatformServers(
119
- SYSTEM_CTX,
120
- [
121
- {
122
- id: 's1',
123
- label: 'prod',
124
- serverUrl: 'https://compute.example.com',
125
- apiKey: 'secret',
126
- scope: 'platform',
127
- sharedWith: 'all'
128
- }
129
- ],
130
- 's1'
131
- )
132
- ).rejects.toThrow(/SELVA_AT_REST_KEY/);
133
- });
134
-
135
- it('verifySecrets flags a plaintext row', async () => {
136
- const { bundle } = fakeBundle({
137
- compute_servers: [{ id: 's1', label: 'legacy', api_key: 'raw-plaintext' }]
138
- });
139
- const store = new SupabaseComputeServerStore(bundle, KEY);
140
-
141
- const report = await store.verifySecrets();
142
- expect(report.ok).toBe(false);
143
- expect(report.plaintextFound).toBe(true);
144
- expect(report.failures).toEqual([
145
- { serverId: 's1', serverLabel: 'legacy', reason: 'plaintext_on_disk' }
146
- ]);
147
- });
148
-
149
- it('verifySecrets reports key_mismatch when the at-rest key rotated', async () => {
150
- const { encryptSecret } = await import('@selvajs/platform/computeServer');
151
- const envelope = encryptSecret('secret', randomBytes(32)); // different key
152
- const { bundle } = fakeBundle({
153
- compute_servers: [{ id: 's1', label: 'prod', api_key: envelope }]
154
- });
155
- const store = new SupabaseComputeServerStore(bundle, KEY);
156
-
157
- const report = await store.verifySecrets();
158
- expect(report.ok).toBe(false);
159
- expect(report.plaintextFound).toBe(false);
160
- expect(report.failures[0]).toMatchObject({ serverId: 's1', reason: 'key_mismatch' });
161
- });
162
-
163
- it('verifySecrets returns ok when every apiKey decrypts', async () => {
164
- const { encryptSecret } = await import('@selvajs/platform/computeServer');
165
- const { bundle } = fakeBundle({
166
- compute_servers: [{ id: 's1', label: 'prod', api_key: encryptSecret('k', KEY) }]
167
- });
168
- const store = new SupabaseComputeServerStore(bundle, KEY);
169
-
170
- const report = await store.verifySecrets();
171
- expect(report).toEqual({ ok: true, failures: [], plaintextFound: false });
172
- });
173
- });
@@ -1,89 +0,0 @@
1
- import { describe, beforeEach, it } from 'vitest';
2
- import { runDefinitionStoreConformance } from '@selvajs/platform/testing';
3
- import { DEFAULT_ORG_PERMISSIONS } from '@selvajs/platform';
4
- import { SupabaseDefinitionStore } from '../SupabaseDefinitionStore.js';
5
- import { readEnv, resetAllData, seedUser } from './test-helpers.js';
6
-
7
- const envCtx = readEnv();
8
-
9
- if (!envCtx) {
10
- describe.skip('SupabaseDefinitionStore (skipped: no live stack)', () => {
11
- it('populate packages/providers/supabase/.env.test with Supabase creds to run these tests', () => {});
12
- });
13
- } else {
14
- describe('SupabaseDefinitionStore', () => {
15
- beforeEach(async () => {
16
- await resetAllData(envCtx);
17
- });
18
-
19
- runDefinitionStoreConformance({
20
- name: 'SupabaseDefinitionStore',
21
- createStore: () => new SupabaseDefinitionStore(envCtx.bundle),
22
- createScope: async () => {
23
- // Seed: 1 owner + 1 secondary user + 1 org + 2 projects. Definitions
24
- // created in the suite FK to one of those two projects.
25
- const { userId: ownerId, sessionToken: ownerSessionToken } = await seedUser(envCtx, '');
26
- const { userId: secondaryUserId, sessionToken: secondaryUserSessionToken } = await seedUser(
27
- envCtx,
28
- ''
29
- );
30
- const orgId = crypto.randomUUID();
31
- const projectId = crypto.randomUUID();
32
- const secondaryProjectId = crypto.randomUUID();
33
- const now = new Date().toISOString();
34
- const admin = envCtx.adminClient;
35
-
36
- const { error: orgError } = await admin.from('orgs').insert({
37
- id: orgId,
38
- name: 'Definition Test Org',
39
- slug: `def-test-${orgId.slice(0, 8)}`,
40
- owner_id: ownerId,
41
- created_at: now,
42
- updated_at: now
43
- });
44
- if (orgError) throw orgError;
45
- const { error: memberError } = await admin.from('org_members').insert({
46
- org_id: orgId,
47
- user_id: ownerId,
48
- role: 'owner',
49
- permissions: [...DEFAULT_ORG_PERMISSIONS.owner],
50
- joined_at: now
51
- });
52
- if (memberError) throw memberError;
53
-
54
- const { error: p1Error } = await admin.from('projects').insert([
55
- {
56
- id: projectId,
57
- org_id: orgId,
58
- name: 'Project A',
59
- slug: `proj-a-${projectId.slice(0, 8)}`,
60
- visibility: 'public',
61
- owner_id: ownerId,
62
- created_at: now,
63
- updated_at: now
64
- },
65
- {
66
- id: secondaryProjectId,
67
- org_id: orgId,
68
- name: 'Project B',
69
- slug: `proj-b-${secondaryProjectId.slice(0, 8)}`,
70
- visibility: 'public',
71
- owner_id: ownerId,
72
- created_at: now,
73
- updated_at: now
74
- }
75
- ]);
76
- if (p1Error) throw p1Error;
77
-
78
- return {
79
- ownerId,
80
- ownerSessionToken,
81
- projectId,
82
- secondaryProjectId,
83
- secondaryUserId,
84
- secondaryUserSessionToken
85
- };
86
- }
87
- });
88
- });
89
- }
@@ -1,32 +0,0 @@
1
- import { describe, beforeEach, it } from 'vitest';
2
- import { runEventSinkConformance, type RecordingEventSink } from '@selvajs/platform/testing';
3
- import { SupabaseDataProvider } from '../SupabaseDataProvider.js';
4
- import { readEnv, resetAllData, seedUser } from './test-helpers.js';
5
-
6
- const envCtx = readEnv();
7
-
8
- if (!envCtx) {
9
- describe.skip('SupabaseDataProvider event-sink (skipped: no live stack)', () => {
10
- it('populate packages/providers/supabase/.env.test with Supabase creds to run these tests', () => {});
11
- });
12
- } else {
13
- describe('SupabaseDataProvider event-sink', () => {
14
- beforeEach(async () => {
15
- await resetAllData(envCtx);
16
- });
17
-
18
- runEventSinkConformance({
19
- name: 'SupabaseDataProvider',
20
- createProvider: (sink: RecordingEventSink) =>
21
- SupabaseDataProvider.create(
22
- {
23
- supabaseUrl: envCtx.url,
24
- anonKey: envCtx.anonKey,
25
- serviceRoleKey: envCtx.serviceRoleKey
26
- },
27
- sink
28
- ),
29
- createActorId: () => seedUser(envCtx, '')
30
- });
31
- });
32
- }
@@ -1,46 +0,0 @@
1
- import { describe, beforeEach, it } from 'vitest';
2
- import { runInviteStoreConformance } from '@selvajs/platform/testing';
3
- import { DEFAULT_ORG_PERMISSIONS } from '@selvajs/platform';
4
- import { SupabaseInviteStore } from '../SupabaseInviteStore.js';
5
- import { readEnv, resetAllData, seedUser } from './test-helpers.js';
6
-
7
- const envCtx = readEnv();
8
-
9
- if (!envCtx) {
10
- describe.skip('SupabaseInviteStore (skipped: no live stack)', () => {
11
- it('populate packages/providers/supabase/.env.test with Supabase creds to run these tests', () => {});
12
- });
13
- } else {
14
- describe('SupabaseInviteStore', () => {
15
- beforeEach(async () => {
16
- await resetAllData(envCtx);
17
- });
18
-
19
- runInviteStoreConformance({
20
- name: 'SupabaseInviteStore',
21
- createStore: () => new SupabaseInviteStore(envCtx.bundle),
22
- createScope: async () => {
23
- const { userId: adminId, sessionToken: adminSessionToken } = await seedUser(envCtx, '');
24
- const orgId = crypto.randomUUID();
25
- const now = new Date().toISOString();
26
- await envCtx.adminClient.from('orgs').insert({
27
- id: orgId,
28
- name: 'Invite Test Org',
29
- slug: `invite-test-${orgId.slice(0, 8)}`,
30
- owner_id: adminId,
31
- created_at: now,
32
- updated_at: now
33
- });
34
- await envCtx.adminClient.from('org_members').insert({
35
- org_id: orgId,
36
- user_id: adminId,
37
- role: 'owner',
38
- permissions: [...DEFAULT_ORG_PERMISSIONS.owner],
39
- joined_at: now
40
- });
41
- return { adminId, adminSessionToken, orgId };
42
- },
43
- seedUser: (id) => seedUser(envCtx, id)
44
- });
45
- });
46
- }
@@ -1,30 +0,0 @@
1
- import { describe, beforeEach, it } from 'vitest';
2
- import { runOrgStoreConformance } from '@selvajs/platform/testing';
3
- import { SupabaseOrgStore } from '../SupabaseOrgStore.js';
4
- import { SupabaseInviteStore } from '../SupabaseInviteStore.js';
5
- import { SupabaseComputeServerStore } from '../SupabaseComputeServerStore.js';
6
- import { readEnv, resetAllData, seedUser } from './test-helpers.js';
7
-
8
- const envCtx = readEnv();
9
-
10
- if (!envCtx) {
11
- describe.skip('SupabaseOrgStore (skipped: no live stack)', () => {
12
- it('populate packages/providers/supabase/.env.test with Supabase creds to run these tests', () => {});
13
- });
14
- } else {
15
- describe('SupabaseOrgStore', () => {
16
- beforeEach(async () => {
17
- await resetAllData(envCtx);
18
- });
19
-
20
- runOrgStoreConformance({
21
- name: 'SupabaseOrgStore',
22
- createStore: () => new SupabaseOrgStore(envCtx.bundle),
23
- seedUser: (id) => seedUser(envCtx, id),
24
- createCompanionStores: () => ({
25
- invites: new SupabaseInviteStore(envCtx.bundle),
26
- computeServer: new SupabaseComputeServerStore(envCtx.bundle)
27
- })
28
- });
29
- });
30
- }
@@ -1,57 +0,0 @@
1
- import { describe, beforeEach, it } from 'vitest';
2
- import { runProjectStoreConformance } from '@selvajs/platform/testing';
3
- import { DEFAULT_ORG_PERMISSIONS } from '@selvajs/platform';
4
- import { SupabaseProjectStore } from '../SupabaseProjectStore.js';
5
- import { readEnv, resetAllData, seedUser } from './test-helpers.js';
6
-
7
- const envCtx = readEnv();
8
-
9
- if (!envCtx) {
10
- describe.skip('SupabaseProjectStore (skipped: no live stack)', () => {
11
- it('populate packages/providers/supabase/.env.test with Supabase creds to run these tests', () => {});
12
- });
13
- } else {
14
- describe('SupabaseProjectStore', () => {
15
- beforeEach(async () => {
16
- await resetAllData(envCtx);
17
- });
18
-
19
- runProjectStoreConformance({
20
- name: 'SupabaseProjectStore',
21
- createStore: async () => {
22
- // Seed an owner + an org so the project tests have an anchor.
23
- const { userId: ownerId, sessionToken: ownerSessionToken } = await seedUser(envCtx, '');
24
- const orgId = crypto.randomUUID();
25
- const now = new Date().toISOString();
26
- const admin = envCtx.adminClient;
27
- const { error: orgError } = await admin.from('orgs').insert({
28
- id: orgId,
29
- name: 'Conformance Org',
30
- slug: `conformance-${orgId.slice(0, 8)}`,
31
- owner_id: ownerId,
32
- created_at: now,
33
- updated_at: now
34
- });
35
- if (orgError) throw orgError;
36
- // And an owner membership with `manage_projects` so project
37
- // creates pass the `has_org_permission` policy under RLS.
38
- const { error: memberError } = await admin.from('org_members').insert({
39
- org_id: orgId,
40
- user_id: ownerId,
41
- role: 'owner',
42
- permissions: [...DEFAULT_ORG_PERMISSIONS.owner],
43
- joined_at: now
44
- });
45
- if (memberError) throw memberError;
46
-
47
- return {
48
- store: new SupabaseProjectStore(envCtx.bundle),
49
- orgId,
50
- ownerId,
51
- ownerSessionToken
52
- };
53
- },
54
- seedUser: (id) => seedUser(envCtx, id)
55
- });
56
- });
57
- }
@@ -1,124 +0,0 @@
1
- /**
2
- * Audit Q4/Q5 — the concurrent-solve guarantee for share-link caps.
3
- *
4
- * `tryIncrementSolveCount` is the load-bearing race-sensitive method: the
5
- * `/api/compute` endpoint calls it BEFORE solving and turns a `null` return
6
- * (cap hit) into HTTP 429. Under concurrent solves against one capped link, the
7
- * cap must hold exactly — no overshoot, no lost update handing two solves the
8
- * same slot. Supabase provides this via the SECURITY DEFINER
9
- * `try_increment_share_link_solve_count` RPC, which does the check-and-increment
10
- * in a single statement.
11
- *
12
- * This lives in the Supabase suite (not the shared conformance suite or the app
13
- * route tests) because it is the ONLY backend that actually provides the
14
- * guarantee: the local provider's whole-file read-modify-write is a documented
15
- * dev-scale tradeoff and is not concurrency-safe. Skipped without a live stack,
16
- * matching the other conformance tests here.
17
- *
18
- * Complements the sequential cap/count tests at
19
- * `packages/selva/src/routes/api/compute/__tests__/solve-cap-and-count.test.ts`.
20
- */
21
-
22
- import { describe, it, expect, beforeEach } from 'vitest';
23
- import type { RequestContext, ShareLink } from '@selvajs/platform';
24
- import { SupabaseShareLinkStore } from '../SupabaseShareLinkStore.js';
25
- import { readEnv, resetAllData, seedUser } from './test-helpers.js';
26
-
27
- const envCtx = readEnv();
28
-
29
- if (!envCtx) {
30
- describe.skip('SupabaseShareLinkStore concurrency (skipped: no live stack)', () => {
31
- it('populate packages/providers/supabase/.env.test with Supabase creds to run these tests', () => {});
32
- });
33
- } else {
34
- const ctx: RequestContext = { userId: '', system: true } as RequestContext;
35
-
36
- /** Seed org → project → definition → share link, all via service-role. */
37
- async function seedCappedLink(maxSolves: number): Promise<{ linkId: string }> {
38
- const { userId: ownerId } = await seedUser(envCtx!, '');
39
- const admin = envCtx!.adminClient;
40
- const orgId = crypto.randomUUID();
41
- const projectId = crypto.randomUUID();
42
- const definitionId = crypto.randomUUID();
43
- const linkId = crypto.randomUUID();
44
- const now = new Date().toISOString();
45
-
46
- const { error: orgErr } = await admin.from('orgs').insert({
47
- id: orgId,
48
- name: 'Concurrency Org',
49
- slug: `conc-${orgId.slice(0, 8)}`,
50
- owner_id: ownerId,
51
- created_at: now,
52
- updated_at: now
53
- });
54
- if (orgErr) throw orgErr;
55
- const { error: projErr } = await admin.from('projects').insert({
56
- id: projectId,
57
- org_id: orgId,
58
- name: 'Project',
59
- slug: `proj-${projectId.slice(0, 8)}`,
60
- visibility: 'public',
61
- owner_id: ownerId,
62
- created_at: now,
63
- updated_at: now
64
- });
65
- if (projErr) throw projErr;
66
- const { error: defErr } = await admin.from('definitions').insert({
67
- guid: definitionId,
68
- project_id: projectId,
69
- owner_id: ownerId,
70
- display_name: 'Def',
71
- status: 'published',
72
- created_at: now,
73
- updated_at: now
74
- });
75
- if (defErr) throw defErr;
76
-
77
- const link: ShareLink = {
78
- id: linkId,
79
- definitionId,
80
- channel: 'live',
81
- tokenHash: crypto.randomUUID().replace(/-/g, ''),
82
- createdBy: ownerId,
83
- createdAt: now,
84
- expiresAt: null,
85
- revokedAt: null,
86
- allowSolve: true,
87
- maxSolves,
88
- solveCount: 0
89
- };
90
- const store = new SupabaseShareLinkStore(envCtx!.bundle);
91
- await store.create(ctx, link);
92
- return { linkId };
93
- }
94
-
95
- describe('SupabaseShareLinkStore — concurrent solve-cap enforcement', () => {
96
- beforeEach(async () => {
97
- await resetAllData(envCtx!);
98
- });
99
-
100
- it('N concurrent solves against a cap-K link admit exactly K, refuse the rest', async () => {
101
- const CAP = 5;
102
- const ATTEMPTS = 25;
103
- const { linkId } = await seedCappedLink(CAP);
104
- const store = new SupabaseShareLinkStore(envCtx!.bundle);
105
-
106
- const outcomes = await Promise.all(
107
- Array.from({ length: ATTEMPTS }, () => store.tryIncrementSolveCount(ctx, linkId))
108
- );
109
-
110
- const admitted = outcomes.filter((n): n is number => n !== null);
111
- const refused = outcomes.filter((n) => n === null);
112
- // Exactly CAP admitted — the atomic RPC never lets a concurrent burst
113
- // overshoot the cap.
114
- expect(admitted).toHaveLength(CAP);
115
- expect(refused).toHaveLength(ATTEMPTS - CAP);
116
- // Each admitted solve got a distinct slot 1..CAP — no lost update.
117
- expect([...admitted].sort((a, b) => a - b)).toEqual([1, 2, 3, 4, 5]);
118
-
119
- // The persisted count settles at exactly the cap.
120
- const finalLink = await store.getById(ctx, linkId);
121
- expect(finalLink?.solveCount).toBe(CAP);
122
- });
123
- });
124
- }
@@ -1,87 +0,0 @@
1
- import { describe, beforeEach, it } from 'vitest';
2
- import { runShareLinkStoreConformance } from '@selvajs/platform/testing';
3
- import { DEFAULT_ORG_PERMISSIONS } from '@selvajs/platform';
4
- import { SupabaseShareLinkStore } from '../SupabaseShareLinkStore.js';
5
- import { readEnv, resetAllData, seedUser } from './test-helpers.js';
6
-
7
- const envCtx = readEnv();
8
-
9
- if (!envCtx) {
10
- describe.skip('SupabaseShareLinkStore (skipped: no live stack)', () => {
11
- it('populate packages/providers/supabase/.env.test with Supabase creds to run these tests', () => {});
12
- });
13
- } else {
14
- describe('SupabaseShareLinkStore', () => {
15
- beforeEach(async () => {
16
- await resetAllData(envCtx);
17
- });
18
-
19
- runShareLinkStoreConformance({
20
- name: 'SupabaseShareLinkStore',
21
- createStore: () => new SupabaseShareLinkStore(envCtx.bundle),
22
- createScope: async () => {
23
- // Seed: 1 owner + 1 org + 1 project + 2 definitions. Share links
24
- // FK to definitions, definitions FK to projects, projects FK to orgs.
25
- const { userId: ownerId, sessionToken: ownerSessionToken } = await seedUser(envCtx, '');
26
- const orgId = crypto.randomUUID();
27
- const projectId = crypto.randomUUID();
28
- const definitionId = crypto.randomUUID();
29
- const otherDefinitionId = crypto.randomUUID();
30
- const now = new Date().toISOString();
31
- const admin = envCtx.adminClient;
32
-
33
- const { error: orgError } = await admin.from('orgs').insert({
34
- id: orgId,
35
- name: 'Share Link Test Org',
36
- slug: `sl-test-${orgId.slice(0, 8)}`,
37
- owner_id: ownerId,
38
- created_at: now,
39
- updated_at: now
40
- });
41
- if (orgError) throw orgError;
42
- const { error: memberError } = await admin.from('org_members').insert({
43
- org_id: orgId,
44
- user_id: ownerId,
45
- role: 'owner',
46
- permissions: [...DEFAULT_ORG_PERMISSIONS.owner],
47
- joined_at: now
48
- });
49
- if (memberError) throw memberError;
50
- const { error: projError } = await admin.from('projects').insert({
51
- id: projectId,
52
- org_id: orgId,
53
- name: 'Project',
54
- slug: `proj-${projectId.slice(0, 8)}`,
55
- visibility: 'public',
56
- owner_id: ownerId,
57
- created_at: now,
58
- updated_at: now
59
- });
60
- if (projError) throw projError;
61
- const { error: defError } = await admin.from('definitions').insert([
62
- {
63
- guid: definitionId,
64
- project_id: projectId,
65
- owner_id: ownerId,
66
- display_name: 'Def A',
67
- status: 'published',
68
- created_at: now,
69
- updated_at: now
70
- },
71
- {
72
- guid: otherDefinitionId,
73
- project_id: projectId,
74
- owner_id: ownerId,
75
- display_name: 'Def B',
76
- status: 'published',
77
- created_at: now,
78
- updated_at: now
79
- }
80
- ]);
81
- if (defError) throw defError;
82
-
83
- return { ownerId, ownerSessionToken, definitionId, otherDefinitionId };
84
- }
85
- });
86
- });
87
- }