@selvajs/supabase-provider 0.14.4-beta.0 → 0.14.4-beta.1

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 (144) hide show
  1. package/LICENSE +1 -1
  2. package/dist/auth/SupabaseAuthProvider.d.ts.map +1 -1
  3. package/dist/auth/SupabaseAuthProvider.js +7 -14
  4. package/dist/auth/SupabaseAuthProvider.js.map +1 -1
  5. package/dist/data/SupabaseDataProvider.d.ts +13 -1
  6. package/dist/data/SupabaseDataProvider.d.ts.map +1 -1
  7. package/dist/data/SupabaseDataProvider.js +59 -0
  8. package/dist/data/SupabaseDataProvider.js.map +1 -1
  9. package/dist/data/SupabaseDefinitionStore.d.ts +1 -0
  10. package/dist/data/SupabaseDefinitionStore.d.ts.map +1 -1
  11. package/dist/data/SupabaseDefinitionStore.js +27 -1
  12. package/dist/data/SupabaseDefinitionStore.js.map +1 -1
  13. package/dist/data/index.d.ts +1 -0
  14. package/dist/data/index.d.ts.map +1 -1
  15. package/dist/data/index.js +1 -0
  16. package/dist/data/index.js.map +1 -1
  17. package/dist/data/migrationHead.d.ts +14 -0
  18. package/dist/data/migrationHead.d.ts.map +1 -0
  19. package/dist/data/migrationHead.js +14 -0
  20. package/dist/data/migrationHead.js.map +1 -0
  21. package/dist/index.d.ts +1 -1
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +1 -1
  24. package/dist/index.js.map +1 -1
  25. package/dist/permissions/SupabasePlatformPermissionStore.d.ts +5 -3
  26. package/dist/permissions/SupabasePlatformPermissionStore.d.ts.map +1 -1
  27. package/dist/permissions/SupabasePlatformPermissionStore.js +17 -30
  28. package/dist/permissions/SupabasePlatformPermissionStore.js.map +1 -1
  29. package/dist/storage/SupabaseStorageProvider.d.ts.map +1 -1
  30. package/dist/storage/SupabaseStorageProvider.js +3 -0
  31. package/dist/storage/SupabaseStorageProvider.js.map +1 -1
  32. package/package.json +13 -14
  33. package/supabase/.branches/_current_branch +1 -0
  34. package/supabase/.temp/cli-latest +1 -0
  35. package/supabase/migrations/20260712120000_user_profiles_disabled_and_gin.sql +95 -0
  36. package/supabase/migrations/20260712123000_selva_migration_head_fn.sql +38 -0
  37. package/supabase/migrations/20260713120000_selva_next_version_number.sql +59 -0
  38. package/supabase/migrations/20260713140000_selva_solve_cache_limit.sql +15 -0
  39. package/dist/auth/__tests__/conformance.test.d.ts +0 -2
  40. package/dist/auth/__tests__/conformance.test.d.ts.map +0 -1
  41. package/dist/auth/__tests__/conformance.test.js +0 -76
  42. package/dist/auth/__tests__/conformance.test.js.map +0 -1
  43. package/dist/auth/__tests__/token-verification.test.d.ts +0 -13
  44. package/dist/auth/__tests__/token-verification.test.d.ts.map +0 -1
  45. package/dist/auth/__tests__/token-verification.test.js +0 -135
  46. package/dist/auth/__tests__/token-verification.test.js.map +0 -1
  47. package/dist/data/__tests__/compute-server-conformance.test.d.ts +0 -2
  48. package/dist/data/__tests__/compute-server-conformance.test.d.ts.map +0 -1
  49. package/dist/data/__tests__/compute-server-conformance.test.js +0 -45
  50. package/dist/data/__tests__/compute-server-conformance.test.js.map +0 -1
  51. package/dist/data/__tests__/compute-server-encryption.test.d.ts +0 -12
  52. package/dist/data/__tests__/compute-server-encryption.test.d.ts.map +0 -1
  53. package/dist/data/__tests__/compute-server-encryption.test.js +0 -143
  54. package/dist/data/__tests__/compute-server-encryption.test.js.map +0 -1
  55. package/dist/data/__tests__/definition-conformance.test.d.ts +0 -2
  56. package/dist/data/__tests__/definition-conformance.test.d.ts.map +0 -1
  57. package/dist/data/__tests__/definition-conformance.test.js +0 -85
  58. package/dist/data/__tests__/definition-conformance.test.js.map +0 -1
  59. package/dist/data/__tests__/event-sink-conformance.test.d.ts +0 -2
  60. package/dist/data/__tests__/event-sink-conformance.test.d.ts.map +0 -1
  61. package/dist/data/__tests__/event-sink-conformance.test.js +0 -27
  62. package/dist/data/__tests__/event-sink-conformance.test.js.map +0 -1
  63. package/dist/data/__tests__/invite-conformance.test.d.ts +0 -2
  64. package/dist/data/__tests__/invite-conformance.test.d.ts.map +0 -1
  65. package/dist/data/__tests__/invite-conformance.test.js +0 -45
  66. package/dist/data/__tests__/invite-conformance.test.js.map +0 -1
  67. package/dist/data/__tests__/org-conformance.test.d.ts +0 -2
  68. package/dist/data/__tests__/org-conformance.test.d.ts.map +0 -1
  69. package/dist/data/__tests__/org-conformance.test.js +0 -29
  70. package/dist/data/__tests__/org-conformance.test.js.map +0 -1
  71. package/dist/data/__tests__/project-conformance.test.d.ts +0 -2
  72. package/dist/data/__tests__/project-conformance.test.d.ts.map +0 -1
  73. package/dist/data/__tests__/project-conformance.test.js +0 -57
  74. package/dist/data/__tests__/project-conformance.test.js.map +0 -1
  75. package/dist/data/__tests__/share-link-concurrency.test.d.ts +0 -22
  76. package/dist/data/__tests__/share-link-concurrency.test.d.ts.map +0 -1
  77. package/dist/data/__tests__/share-link-concurrency.test.js +0 -115
  78. package/dist/data/__tests__/share-link-concurrency.test.js.map +0 -1
  79. package/dist/data/__tests__/share-link-conformance.test.d.ts +0 -2
  80. package/dist/data/__tests__/share-link-conformance.test.d.ts.map +0 -1
  81. package/dist/data/__tests__/share-link-conformance.test.js +0 -88
  82. package/dist/data/__tests__/share-link-conformance.test.js.map +0 -1
  83. package/dist/data/__tests__/solve-metric-sink-conformance.test.d.ts +0 -2
  84. package/dist/data/__tests__/solve-metric-sink-conformance.test.d.ts.map +0 -1
  85. package/dist/data/__tests__/solve-metric-sink-conformance.test.js +0 -52
  86. package/dist/data/__tests__/solve-metric-sink-conformance.test.js.map +0 -1
  87. package/dist/data/__tests__/test-helpers.d.ts +0 -52
  88. package/dist/data/__tests__/test-helpers.d.ts.map +0 -1
  89. package/dist/data/__tests__/test-helpers.js +0 -177
  90. package/dist/data/__tests__/test-helpers.js.map +0 -1
  91. package/dist/permissions/__tests__/conformance.test.d.ts +0 -2
  92. package/dist/permissions/__tests__/conformance.test.d.ts.map +0 -1
  93. package/dist/permissions/__tests__/conformance.test.js +0 -27
  94. package/dist/permissions/__tests__/conformance.test.js.map +0 -1
  95. package/dist/storage/__tests__/conformance.test.d.ts +0 -2
  96. package/dist/storage/__tests__/conformance.test.d.ts.map +0 -1
  97. package/dist/storage/__tests__/conformance.test.js +0 -149
  98. package/dist/storage/__tests__/conformance.test.js.map +0 -1
  99. package/dist/userProfile/__tests__/conformance.test.d.ts +0 -2
  100. package/dist/userProfile/__tests__/conformance.test.d.ts.map +0 -1
  101. package/dist/userProfile/__tests__/conformance.test.js +0 -38
  102. package/dist/userProfile/__tests__/conformance.test.js.map +0 -1
  103. package/src/auth/SupabaseAuthProvider.ts +0 -600
  104. package/src/auth/__tests__/conformance.test.ts +0 -78
  105. package/src/auth/__tests__/token-verification.test.ts +0 -158
  106. package/src/auth/index.ts +0 -2
  107. package/src/cli/sync-migrations.ts +0 -133
  108. package/src/data/SupabaseAuditQuery.ts +0 -103
  109. package/src/data/SupabaseComputeServerStore.ts +0 -382
  110. package/src/data/SupabaseDataProvider.ts +0 -153
  111. package/src/data/SupabaseDefinitionStore.ts +0 -527
  112. package/src/data/SupabaseEventSink.ts +0 -30
  113. package/src/data/SupabaseInviteStore.ts +0 -171
  114. package/src/data/SupabaseOrgStore.ts +0 -508
  115. package/src/data/SupabaseProjectStore.ts +0 -431
  116. package/src/data/SupabaseShareLinkStore.ts +0 -176
  117. package/src/data/SupabaseSolveMetricSink.ts +0 -38
  118. package/src/data/__tests__/compute-server-conformance.test.ts +0 -46
  119. package/src/data/__tests__/compute-server-encryption.test.ts +0 -173
  120. package/src/data/__tests__/definition-conformance.test.ts +0 -89
  121. package/src/data/__tests__/event-sink-conformance.test.ts +0 -32
  122. package/src/data/__tests__/invite-conformance.test.ts +0 -46
  123. package/src/data/__tests__/org-conformance.test.ts +0 -30
  124. package/src/data/__tests__/project-conformance.test.ts +0 -57
  125. package/src/data/__tests__/share-link-concurrency.test.ts +0 -124
  126. package/src/data/__tests__/share-link-conformance.test.ts +0 -87
  127. package/src/data/__tests__/solve-metric-sink-conformance.test.ts +0 -59
  128. package/src/data/__tests__/test-helpers.ts +0 -188
  129. package/src/data/client.ts +0 -173
  130. package/src/data/errors.ts +0 -45
  131. package/src/data/index.ts +0 -29
  132. package/src/data/pagination.ts +0 -68
  133. package/src/data/query.ts +0 -22
  134. package/src/data/rowStamp.ts +0 -48
  135. package/src/index.ts +0 -42
  136. package/src/permissions/SupabasePlatformPermissionStore.ts +0 -161
  137. package/src/permissions/__tests__/conformance.test.ts +0 -28
  138. package/src/permissions/index.ts +0 -1
  139. package/src/storage/SupabaseStorageProvider.ts +0 -182
  140. package/src/storage/__tests__/conformance.test.ts +0 -152
  141. package/src/storage/index.ts +0 -2
  142. package/src/userProfile/SupabaseUserProfileProvider.ts +0 -177
  143. package/src/userProfile/__tests__/conformance.test.ts +0 -39
  144. package/src/userProfile/index.ts +0 -1
@@ -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
- }