@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,78 +0,0 @@
1
- import { describe, beforeEach, it } from 'vitest';
2
- import { runAuthProviderConformance, runEmailLinkAuthConformance } from '@selvajs/platform/testing';
3
- import { SupabaseAuthProvider } from '../SupabaseAuthProvider.js';
4
- import { readEnv, resetAllData } from '../../data/__tests__/test-helpers.js';
5
-
6
- const envCtx = readEnv();
7
-
8
- if (!envCtx) {
9
- describe.skip('SupabaseAuthProvider (skipped: no live stack)', () => {
10
- it('populate packages/providers/supabase/.env.test with Supabase creds to run these tests', () => {});
11
- });
12
- } else {
13
- const ctx = envCtx; // non-null in this branch
14
- // The auth conformance suite expects `verifyLogin('', adminPassword)` to
15
- // succeed. Supabase requires a non-empty email, so we seed a deterministic
16
- // admin email and have the suite use that via the `adminEmail` option.
17
- const ADMIN_EMAIL = 'admin@conformance.test';
18
- const ADMIN_PASSWORD = 'conformance-admin-password-1234';
19
-
20
- async function ensureAdmin() {
21
- const { data: existing } = await ctx.adminClient.auth.admin.listUsers({ perPage: 1000 });
22
- const match = existing.users.find((u) => u.email === ADMIN_EMAIL);
23
- if (match) return match.id;
24
- const { data, error } = await ctx.adminClient.auth.admin.createUser({
25
- email: ADMIN_EMAIL,
26
- password: ADMIN_PASSWORD,
27
- email_confirm: true
28
- });
29
- if (error) throw error;
30
- return data.user.id;
31
- }
32
-
33
- async function makeProvider() {
34
- const provider = new SupabaseAuthProvider({
35
- supabaseUrl: ctx.url,
36
- anonKey: ctx.anonKey,
37
- serviceRoleKey: ctx.serviceRoleKey
38
- });
39
- // Ensure the admin account exists + has instance_admin.
40
- const id = await ensureAdmin();
41
- const { error: upError } = await ctx.adminClient
42
- .from('user_profiles')
43
- .update({ platform_permissions: ['instance_admin'] })
44
- .eq('user_id', id);
45
- if (upError) throw upError;
46
- return { provider, adminPassword: ADMIN_PASSWORD };
47
- }
48
-
49
- describe('SupabaseAuthProvider', () => {
50
- beforeEach(async () => {
51
- await resetAllData(ctx);
52
- });
53
-
54
- runAuthProviderConformance({
55
- name: 'SupabaseAuthProvider',
56
- createProvider: async () => {
57
- const { provider, adminPassword } = await makeProvider();
58
- return { provider, adminPassword, adminEmail: ADMIN_EMAIL };
59
- },
60
- userManagement: true
61
- });
62
-
63
- runEmailLinkAuthConformance({
64
- name: 'SupabaseEmailLinkAuth',
65
- createAdapter: async () => {
66
- const { provider } = await makeProvider();
67
- return {
68
- adapter: provider.emailLink,
69
- // `validEmail` isn't used by the suite for live mail delivery;
70
- // it just needs to look like a real address. The malformed
71
- // case is what we actually assert on.
72
- validEmail: 'real-looking@conformance.test',
73
- invalidEmail: 'not-an-email-address'
74
- };
75
- }
76
- });
77
- });
78
- }
@@ -1,158 +0,0 @@
1
- /**
2
- * CI-runnable (no live stack) tests for §1b hybrid token verification. Stubs
3
- * the internal anon client's `getClaims` (local JWT verify) and `getUser`
4
- * (network GoTrue verify) so we can assert the network-call behavior directly.
5
- *
6
- * Contract:
7
- * - hybrid (default): verify locally via getClaims on every request; hit
8
- * getUser at most once per `revalidateMs` per session; a getUser rejection
9
- * during that recheck (sign-out / disabled) denies immediately.
10
- * - strict: getUser on every request.
11
- */
12
-
13
- import { describe, it, expect, vi, beforeEach } from 'vitest';
14
- import { SupabaseAuthProvider } from '../SupabaseAuthProvider.js';
15
-
16
- const URL = 'https://example.supabase.co';
17
- const ANON = 'anon-key';
18
- const SERVICE = 'service-key';
19
-
20
- interface AuthStub {
21
- getClaims: ReturnType<typeof vi.fn>;
22
- getUser: ReturnType<typeof vi.fn>;
23
- }
24
-
25
- /** Reach into the provider's private anon client and replace its auth surface. */
26
- function stubAnonAuth(provider: SupabaseAuthProvider): AuthStub {
27
- const stub: AuthStub = { getClaims: vi.fn(), getUser: vi.fn() };
28
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
29
- (provider as any).anon.auth.getClaims = stub.getClaims;
30
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
31
- (provider as any).anon.auth.getUser = stub.getUser;
32
- return stub;
33
- }
34
-
35
- function okClaims(overrides: Record<string, unknown> = {}) {
36
- return {
37
- data: {
38
- claims: {
39
- sub: 'user-1',
40
- session_id: 'sess-1',
41
- email: 'u@example.com',
42
- exp: Math.floor(Date.now() / 1000) + 3600,
43
- ...overrides
44
- },
45
- header: {},
46
- signature: new Uint8Array()
47
- },
48
- error: null
49
- };
50
- }
51
-
52
- function okUser() {
53
- return {
54
- data: { user: { id: 'user-1', email: 'u@example.com', user_metadata: {} } },
55
- error: null
56
- };
57
- }
58
-
59
- describe('SupabaseAuthProvider — hybrid token verification', () => {
60
- beforeEach(() => vi.restoreAllMocks());
61
-
62
- it('verifies locally via getClaims and rechecks getUser once per window', async () => {
63
- const provider = new SupabaseAuthProvider({
64
- supabaseUrl: URL,
65
- anonKey: ANON,
66
- serviceRoleKey: SERVICE,
67
- revalidateMs: 60_000
68
- });
69
- const stub = stubAnonAuth(provider);
70
- stub.getClaims.mockResolvedValue(okClaims());
71
- stub.getUser.mockResolvedValue(okUser());
72
-
73
- // First call: local verify + one recheck (never seen this session).
74
- const a = await provider.verifyToken('tok');
75
- expect(a?.id).toBe('user-1');
76
- expect(stub.getClaims).toHaveBeenCalledTimes(1);
77
- expect(stub.getUser).toHaveBeenCalledTimes(1);
78
-
79
- // Subsequent calls within the window: local only, NO more getUser.
80
- await provider.verifyToken('tok');
81
- await provider.verifyToken('tok');
82
- expect(stub.getClaims).toHaveBeenCalledTimes(3);
83
- expect(stub.getUser).toHaveBeenCalledTimes(1); // still just the first
84
- });
85
-
86
- it('denies immediately when the periodic getUser recheck rejects (signed out / revoked)', async () => {
87
- const provider = new SupabaseAuthProvider({
88
- supabaseUrl: URL,
89
- anonKey: ANON,
90
- serviceRoleKey: SERVICE
91
- });
92
- const stub = stubAnonAuth(provider);
93
- stub.getClaims.mockResolvedValue(okClaims());
94
- stub.getUser.mockResolvedValue({ data: { user: null }, error: { message: 'signed out' } });
95
-
96
- const result = await provider.verifyToken('tok');
97
- expect(result).toBeNull();
98
- });
99
-
100
- it('denies when the claims already mark the user disabled (no getUser needed)', async () => {
101
- const provider = new SupabaseAuthProvider({
102
- supabaseUrl: URL,
103
- anonKey: ANON,
104
- serviceRoleKey: SERVICE
105
- });
106
- const stub = stubAnonAuth(provider);
107
- stub.getClaims.mockResolvedValue(okClaims({ user_metadata: { disabled: true } }));
108
-
109
- const result = await provider.verifyToken('tok');
110
- expect(result).toBeNull();
111
- expect(stub.getUser).not.toHaveBeenCalled();
112
- });
113
-
114
- it('denies when getClaims fails (bad signature / expired)', async () => {
115
- const provider = new SupabaseAuthProvider({
116
- supabaseUrl: URL,
117
- anonKey: ANON,
118
- serviceRoleKey: SERVICE
119
- });
120
- const stub = stubAnonAuth(provider);
121
- stub.getClaims.mockResolvedValue({ data: null, error: { message: 'invalid JWT' } });
122
-
123
- expect(await provider.verifyToken('tok')).toBeNull();
124
- expect(stub.getUser).not.toHaveBeenCalled();
125
- });
126
-
127
- it('returns null for an empty token without any network call', async () => {
128
- const provider = new SupabaseAuthProvider({
129
- supabaseUrl: URL,
130
- anonKey: ANON,
131
- serviceRoleKey: SERVICE
132
- });
133
- const stub = stubAnonAuth(provider);
134
- expect(await provider.verifyToken('')).toBeNull();
135
- expect(stub.getClaims).not.toHaveBeenCalled();
136
- expect(stub.getUser).not.toHaveBeenCalled();
137
- });
138
- });
139
-
140
- describe('SupabaseAuthProvider — strict token verification', () => {
141
- beforeEach(() => vi.restoreAllMocks());
142
-
143
- it('calls getUser on every request and never getClaims', async () => {
144
- const provider = new SupabaseAuthProvider({
145
- supabaseUrl: URL,
146
- anonKey: ANON,
147
- serviceRoleKey: SERVICE,
148
- tokenVerification: 'strict'
149
- });
150
- const stub = stubAnonAuth(provider);
151
- stub.getUser.mockResolvedValue(okUser());
152
-
153
- await provider.verifyToken('tok');
154
- await provider.verifyToken('tok');
155
- expect(stub.getUser).toHaveBeenCalledTimes(2);
156
- expect(stub.getClaims).not.toHaveBeenCalled();
157
- });
158
- });
package/src/auth/index.ts DELETED
@@ -1,2 +0,0 @@
1
- export { SupabaseAuthProvider } from './SupabaseAuthProvider.js';
2
- export type { SupabaseAuthProviderConfig } from './SupabaseAuthProvider.js';
@@ -1,133 +0,0 @@
1
- #!/usr/bin/env node
2
- // ============================================================================
3
- // sync-migrations — copy this package's Supabase migrations into a consuming
4
- // app's supabase/migrations/ directory.
5
- //
6
- // Migrations ship with deterministic timestamp-prefixed filenames
7
- // (<UTCtimestamp>_selva_<name>.sql) so they slot into a consuming app's own
8
- // migration timeline without colliding, and so every machine/CI produces a
9
- // byte-identical migration history. We copy verbatim — no rename at copy time,
10
- // which is what keeps the history stable.
11
- //
12
- // Usage:
13
- // npx @selvajs/supabase-provider sync-migrations [--dir <path>] [--force]
14
- //
15
- // --dir <path> Target migrations dir (default: ./supabase/migrations)
16
- // --force Overwrite files that exist with differing content
17
- // ============================================================================
18
-
19
- import { fileURLToPath } from 'node:url';
20
- import { dirname, join, resolve } from 'node:path';
21
- import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
22
-
23
- const here = dirname(fileURLToPath(import.meta.url));
24
-
25
- // dist/cli/ -> package root is two levels up; migrations ship alongside src/dist.
26
- const packageRoot = resolve(here, '..', '..');
27
- const sourceDir = join(packageRoot, 'supabase', 'migrations');
28
-
29
- interface Options {
30
- targetDir: string;
31
- force: boolean;
32
- }
33
-
34
- function parseArgs(argv: string[]): Options {
35
- let targetDir = resolve(process.cwd(), 'supabase', 'migrations');
36
- let force = false;
37
-
38
- for (let i = 0; i < argv.length; i++) {
39
- const arg = argv[i];
40
- if (arg === '--dir') {
41
- const value = argv[++i];
42
- if (!value) fail('--dir requires a path argument');
43
- targetDir = resolve(process.cwd(), value);
44
- } else if (arg === '--force') {
45
- force = true;
46
- } else if (arg === '--help' || arg === '-h') {
47
- printHelp();
48
- process.exit(0);
49
- } else {
50
- fail(`unknown argument: ${arg}`);
51
- }
52
- }
53
-
54
- return { targetDir, force };
55
- }
56
-
57
- function printHelp(): void {
58
- console.log(
59
- [
60
- 'sync-migrations — copy @selvajs/supabase-provider migrations into your app.',
61
- '',
62
- 'Usage:',
63
- ' npx @selvajs/supabase-provider sync-migrations [--dir <path>] [--force]',
64
- '',
65
- ' --dir <path> Target migrations dir (default: ./supabase/migrations)',
66
- ' --force Overwrite files that exist with differing content',
67
- '',
68
- 'After syncing, apply with: npx supabase db push'
69
- ].join('\n')
70
- );
71
- }
72
-
73
- function fail(message: string): never {
74
- console.error(`error: ${message}`);
75
- process.exit(1);
76
- }
77
-
78
- function main(): void {
79
- const { targetDir, force } = parseArgs(process.argv.slice(2));
80
-
81
- if (!existsSync(sourceDir)) {
82
- fail(`could not locate packaged migrations at ${sourceDir}`);
83
- }
84
-
85
- const files = readdirSync(sourceDir)
86
- .filter((name) => name.endsWith('.sql'))
87
- .sort();
88
-
89
- if (files.length === 0) {
90
- fail(`no .sql migrations found in ${sourceDir}`);
91
- }
92
-
93
- mkdirSync(targetDir, { recursive: true });
94
-
95
- let copied = 0;
96
- let skipped = 0;
97
- let conflicts = 0;
98
-
99
- for (const name of files) {
100
- const from = join(sourceDir, name);
101
- const to = join(targetDir, name);
102
- const contents = readFileSync(from);
103
-
104
- if (existsSync(to)) {
105
- const existing = readFileSync(to);
106
- if (existing.equals(contents)) {
107
- skipped++;
108
- continue;
109
- }
110
- if (!force) {
111
- console.warn(
112
- ` conflict ${name} exists with different content (use --force to overwrite)`
113
- );
114
- conflicts++;
115
- continue;
116
- }
117
- }
118
-
119
- writeFileSync(to, contents);
120
- console.log(` copied ${name}`);
121
- copied++;
122
- }
123
-
124
- console.log(`\n${copied} copied, ${skipped} unchanged, ${conflicts} conflict(s) -> ${targetDir}`);
125
-
126
- if (conflicts > 0) {
127
- process.exit(1);
128
- }
129
-
130
- console.log('\nNext: review the files, then run npx supabase db push');
131
- }
132
-
133
- main();
@@ -1,103 +0,0 @@
1
- import type {
2
- AuditEventRow,
3
- AuditQueryFilters,
4
- AuditQueryResult,
5
- DomainEvent,
6
- IAuditQuery,
7
- RequestContext
8
- } from '@selvajs/platform';
9
- import type { ClientBundle } from './client.js';
10
-
11
- const MAX_LIMIT = 200;
12
- const DEFAULT_LIMIT = 100;
13
-
14
- // Cursor values are interpolated into PostgREST filter strings — both formats
15
- // must be validated before any string concatenation reaches the query.
16
- const ISO_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2})$/;
17
- const UUID_RE = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
18
-
19
- /**
20
- * Read-side for `audit_events`. Always uses the service-role client — RLS
21
- * blocks all read access by design, so the route layer is the single trust
22
- * boundary (callers must gate on `instance_admin`).
23
- *
24
- * Pagination is keyset on `(occurred_at desc, id desc)`. The `id` tie-breaker
25
- * prevents skipped rows when multiple events land in the same millisecond.
26
- */
27
- export class SupabaseAuditQuery implements IAuditQuery {
28
- constructor(private readonly clients: ClientBundle) {}
29
-
30
- async list(_ctx: RequestContext, filters: AuditQueryFilters): Promise<AuditQueryResult> {
31
- const limit = clampLimit(filters.limit);
32
-
33
- let q = this.clients.serviceClient
34
- .from('audit_events')
35
- .select('id, type, actor_id, occurred_at, data')
36
- .order('occurred_at', { ascending: false })
37
- .order('id', { ascending: false })
38
- // Fetch one extra row to detect "has next page" without a count query.
39
- .limit(limit + 1);
40
-
41
- if (filters.types && filters.types.length > 0) {
42
- q = q.in('type', [...filters.types]);
43
- }
44
- if (filters.actorId) {
45
- q = q.eq('actor_id', filters.actorId);
46
- }
47
- if (filters.sinceIso) {
48
- q = q.gte('occurred_at', filters.sinceIso);
49
- }
50
- if (filters.untilIso) {
51
- q = q.lte('occurred_at', filters.untilIso);
52
- }
53
- if (filters.cursor) {
54
- // `(occurred_at, id) < (cursorTs, cursorId)` expressed for PostgREST:
55
- // strictly older OR same timestamp with a strictly smaller id.
56
- //
57
- // Both values are interpolated into a `.or()` filter string. Validate
58
- // shape before interpolation — without these guards a malformed cursor
59
- // (or a hand-crafted URL by a route caller) could break out of the
60
- // filter and combine with arbitrary PostgREST operators.
61
- const ts = filters.cursor.occurredAt;
62
- const id = filters.cursor.id;
63
- if (!ISO_RE.test(ts) || !UUID_RE.test(id)) {
64
- throw new Error('audit_events query: malformed cursor');
65
- }
66
- q = q.or(`occurred_at.lt.${ts},and(occurred_at.eq.${ts},id.lt.${id})`);
67
- }
68
-
69
- const { data, error } = await q;
70
- if (error) {
71
- throw new Error(`audit_events query failed: ${error.message}`);
72
- }
73
-
74
- const fetched = (data ?? []) as Array<{
75
- id: string;
76
- type: string;
77
- actor_id: string;
78
- occurred_at: string;
79
- data: DomainEvent;
80
- }>;
81
-
82
- const hasMore = fetched.length > limit;
83
- const page = hasMore ? fetched.slice(0, limit) : fetched;
84
- const last = page[page.length - 1];
85
-
86
- return {
87
- rows: page.map((r) => ({
88
- id: r.id,
89
- type: r.type as DomainEvent['type'],
90
- actorId: r.actor_id,
91
- occurredAt: r.occurred_at,
92
- data: r.data
93
- })) satisfies AuditEventRow[],
94
- nextCursor: hasMore && last ? { occurredAt: last.occurred_at, id: last.id } : null
95
- };
96
- }
97
- }
98
-
99
- function clampLimit(requested: number | undefined): number {
100
- if (!requested || requested <= 0) return DEFAULT_LIMIT;
101
- if (requested > MAX_LIMIT) return MAX_LIMIT;
102
- return Math.floor(requested);
103
- }