@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,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,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
|
-
}
|