@selvajs/supabase-provider 0.14.3 → 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 (160) hide show
  1. package/LICENSE +1 -1
  2. package/dist/auth/SupabaseAuthProvider.d.ts +57 -0
  3. package/dist/auth/SupabaseAuthProvider.d.ts.map +1 -1
  4. package/dist/auth/SupabaseAuthProvider.js +123 -18
  5. package/dist/auth/SupabaseAuthProvider.js.map +1 -1
  6. package/dist/data/SupabaseComputeServerStore.d.ts +34 -2
  7. package/dist/data/SupabaseComputeServerStore.d.ts.map +1 -1
  8. package/dist/data/SupabaseComputeServerStore.js +146 -39
  9. package/dist/data/SupabaseComputeServerStore.js.map +1 -1
  10. package/dist/data/SupabaseDataProvider.d.ts +18 -3
  11. package/dist/data/SupabaseDataProvider.d.ts.map +1 -1
  12. package/dist/data/SupabaseDataProvider.js +83 -18
  13. package/dist/data/SupabaseDataProvider.js.map +1 -1
  14. package/dist/data/SupabaseDefinitionStore.d.ts +3 -1
  15. package/dist/data/SupabaseDefinitionStore.d.ts.map +1 -1
  16. package/dist/data/SupabaseDefinitionStore.js +94 -67
  17. package/dist/data/SupabaseDefinitionStore.js.map +1 -1
  18. package/dist/data/SupabaseInviteStore.d.ts.map +1 -1
  19. package/dist/data/SupabaseInviteStore.js +11 -22
  20. package/dist/data/SupabaseInviteStore.js.map +1 -1
  21. package/dist/data/SupabaseOrgStore.d.ts.map +1 -1
  22. package/dist/data/SupabaseOrgStore.js +49 -86
  23. package/dist/data/SupabaseOrgStore.js.map +1 -1
  24. package/dist/data/SupabaseProjectStore.d.ts.map +1 -1
  25. package/dist/data/SupabaseProjectStore.js +51 -70
  26. package/dist/data/SupabaseProjectStore.js.map +1 -1
  27. package/dist/data/SupabaseShareLinkStore.d.ts.map +1 -1
  28. package/dist/data/SupabaseShareLinkStore.js +13 -27
  29. package/dist/data/SupabaseShareLinkStore.js.map +1 -1
  30. package/dist/data/client.d.ts +35 -3
  31. package/dist/data/client.d.ts.map +1 -1
  32. package/dist/data/client.js +67 -16
  33. package/dist/data/client.js.map +1 -1
  34. package/dist/data/errors.d.ts +2 -0
  35. package/dist/data/errors.d.ts.map +1 -0
  36. package/dist/data/errors.js +23 -0
  37. package/dist/data/errors.js.map +1 -0
  38. package/dist/data/index.d.ts +8 -2
  39. package/dist/data/index.d.ts.map +1 -1
  40. package/dist/data/index.js +6 -1
  41. package/dist/data/index.js.map +1 -1
  42. package/dist/data/migrationHead.d.ts +14 -0
  43. package/dist/data/migrationHead.d.ts.map +1 -0
  44. package/dist/data/migrationHead.js +14 -0
  45. package/dist/data/migrationHead.js.map +1 -0
  46. package/dist/data/query.d.ts +20 -0
  47. package/dist/data/query.d.ts.map +1 -0
  48. package/dist/data/query.js +13 -0
  49. package/dist/data/query.js.map +1 -0
  50. package/dist/data/rowStamp.d.ts +38 -0
  51. package/dist/data/rowStamp.d.ts.map +1 -0
  52. package/dist/data/rowStamp.js +43 -0
  53. package/dist/data/rowStamp.js.map +1 -0
  54. package/dist/index.d.ts +2 -2
  55. package/dist/index.d.ts.map +1 -1
  56. package/dist/index.js +1 -1
  57. package/dist/index.js.map +1 -1
  58. package/dist/permissions/SupabasePlatformPermissionStore.d.ts +5 -3
  59. package/dist/permissions/SupabasePlatformPermissionStore.d.ts.map +1 -1
  60. package/dist/permissions/SupabasePlatformPermissionStore.js +17 -30
  61. package/dist/permissions/SupabasePlatformPermissionStore.js.map +1 -1
  62. package/dist/storage/SupabaseStorageProvider.d.ts.map +1 -1
  63. package/dist/storage/SupabaseStorageProvider.js +3 -0
  64. package/dist/storage/SupabaseStorageProvider.js.map +1 -1
  65. package/package.json +13 -14
  66. package/supabase/.branches/_current_branch +1 -0
  67. package/supabase/.temp/cli-latest +1 -0
  68. package/supabase/migrations/20260711120000_selva_definition_schema_version.sql +18 -0
  69. package/supabase/migrations/20260712120000_user_profiles_disabled_and_gin.sql +95 -0
  70. package/supabase/migrations/20260712123000_selva_migration_head_fn.sql +38 -0
  71. package/supabase/migrations/20260713120000_selva_next_version_number.sql +59 -0
  72. package/supabase/migrations/20260713140000_selva_solve_cache_limit.sql +15 -0
  73. package/dist/auth/__tests__/conformance.test.d.ts +0 -2
  74. package/dist/auth/__tests__/conformance.test.d.ts.map +0 -1
  75. package/dist/auth/__tests__/conformance.test.js +0 -76
  76. package/dist/auth/__tests__/conformance.test.js.map +0 -1
  77. package/dist/data/__tests__/compute-server-conformance.test.d.ts +0 -2
  78. package/dist/data/__tests__/compute-server-conformance.test.d.ts.map +0 -1
  79. package/dist/data/__tests__/compute-server-conformance.test.js +0 -40
  80. package/dist/data/__tests__/compute-server-conformance.test.js.map +0 -1
  81. package/dist/data/__tests__/definition-conformance.test.d.ts +0 -2
  82. package/dist/data/__tests__/definition-conformance.test.d.ts.map +0 -1
  83. package/dist/data/__tests__/definition-conformance.test.js +0 -85
  84. package/dist/data/__tests__/definition-conformance.test.js.map +0 -1
  85. package/dist/data/__tests__/event-sink-conformance.test.d.ts +0 -2
  86. package/dist/data/__tests__/event-sink-conformance.test.d.ts.map +0 -1
  87. package/dist/data/__tests__/event-sink-conformance.test.js +0 -27
  88. package/dist/data/__tests__/event-sink-conformance.test.js.map +0 -1
  89. package/dist/data/__tests__/invite-conformance.test.d.ts +0 -2
  90. package/dist/data/__tests__/invite-conformance.test.d.ts.map +0 -1
  91. package/dist/data/__tests__/invite-conformance.test.js +0 -45
  92. package/dist/data/__tests__/invite-conformance.test.js.map +0 -1
  93. package/dist/data/__tests__/org-conformance.test.d.ts +0 -2
  94. package/dist/data/__tests__/org-conformance.test.d.ts.map +0 -1
  95. package/dist/data/__tests__/org-conformance.test.js +0 -29
  96. package/dist/data/__tests__/org-conformance.test.js.map +0 -1
  97. package/dist/data/__tests__/project-conformance.test.d.ts +0 -2
  98. package/dist/data/__tests__/project-conformance.test.d.ts.map +0 -1
  99. package/dist/data/__tests__/project-conformance.test.js +0 -57
  100. package/dist/data/__tests__/project-conformance.test.js.map +0 -1
  101. package/dist/data/__tests__/share-link-conformance.test.d.ts +0 -2
  102. package/dist/data/__tests__/share-link-conformance.test.d.ts.map +0 -1
  103. package/dist/data/__tests__/share-link-conformance.test.js +0 -88
  104. package/dist/data/__tests__/share-link-conformance.test.js.map +0 -1
  105. package/dist/data/__tests__/solve-metric-sink-conformance.test.d.ts +0 -2
  106. package/dist/data/__tests__/solve-metric-sink-conformance.test.d.ts.map +0 -1
  107. package/dist/data/__tests__/solve-metric-sink-conformance.test.js +0 -52
  108. package/dist/data/__tests__/solve-metric-sink-conformance.test.js.map +0 -1
  109. package/dist/data/__tests__/test-helpers.d.ts +0 -52
  110. package/dist/data/__tests__/test-helpers.d.ts.map +0 -1
  111. package/dist/data/__tests__/test-helpers.js +0 -177
  112. package/dist/data/__tests__/test-helpers.js.map +0 -1
  113. package/dist/permissions/__tests__/conformance.test.d.ts +0 -2
  114. package/dist/permissions/__tests__/conformance.test.d.ts.map +0 -1
  115. package/dist/permissions/__tests__/conformance.test.js +0 -27
  116. package/dist/permissions/__tests__/conformance.test.js.map +0 -1
  117. package/dist/storage/__tests__/conformance.test.d.ts +0 -2
  118. package/dist/storage/__tests__/conformance.test.d.ts.map +0 -1
  119. package/dist/storage/__tests__/conformance.test.js +0 -149
  120. package/dist/storage/__tests__/conformance.test.js.map +0 -1
  121. package/dist/userProfile/__tests__/conformance.test.d.ts +0 -2
  122. package/dist/userProfile/__tests__/conformance.test.d.ts.map +0 -1
  123. package/dist/userProfile/__tests__/conformance.test.js +0 -38
  124. package/dist/userProfile/__tests__/conformance.test.js.map +0 -1
  125. package/src/auth/SupabaseAuthProvider.ts +0 -464
  126. package/src/auth/__tests__/conformance.test.ts +0 -78
  127. package/src/auth/index.ts +0 -2
  128. package/src/cli/sync-migrations.ts +0 -133
  129. package/src/data/SupabaseAuditQuery.ts +0 -103
  130. package/src/data/SupabaseComputeServerStore.ts +0 -262
  131. package/src/data/SupabaseDataProvider.ts +0 -133
  132. package/src/data/SupabaseDefinitionStore.ts +0 -520
  133. package/src/data/SupabaseEventSink.ts +0 -30
  134. package/src/data/SupabaseInviteStore.ts +0 -183
  135. package/src/data/SupabaseOrgStore.ts +0 -552
  136. package/src/data/SupabaseProjectStore.ts +0 -454
  137. package/src/data/SupabaseShareLinkStore.ts +0 -191
  138. package/src/data/SupabaseSolveMetricSink.ts +0 -38
  139. package/src/data/__tests__/compute-server-conformance.test.ts +0 -40
  140. package/src/data/__tests__/definition-conformance.test.ts +0 -89
  141. package/src/data/__tests__/event-sink-conformance.test.ts +0 -32
  142. package/src/data/__tests__/invite-conformance.test.ts +0 -46
  143. package/src/data/__tests__/org-conformance.test.ts +0 -30
  144. package/src/data/__tests__/project-conformance.test.ts +0 -57
  145. package/src/data/__tests__/share-link-conformance.test.ts +0 -87
  146. package/src/data/__tests__/solve-metric-sink-conformance.test.ts +0 -59
  147. package/src/data/__tests__/test-helpers.ts +0 -188
  148. package/src/data/client.ts +0 -98
  149. package/src/data/index.ts +0 -12
  150. package/src/data/pagination.ts +0 -68
  151. package/src/index.ts +0 -24
  152. package/src/permissions/SupabasePlatformPermissionStore.ts +0 -161
  153. package/src/permissions/__tests__/conformance.test.ts +0 -28
  154. package/src/permissions/index.ts +0 -1
  155. package/src/storage/SupabaseStorageProvider.ts +0 -182
  156. package/src/storage/__tests__/conformance.test.ts +0 -152
  157. package/src/storage/index.ts +0 -2
  158. package/src/userProfile/SupabaseUserProfileProvider.ts +0 -177
  159. package/src/userProfile/__tests__/conformance.test.ts +0 -39
  160. package/src/userProfile/index.ts +0 -1
@@ -1,149 +0,0 @@
1
- import { describe, beforeEach, it, expect } from 'vitest';
2
- import { createClient } from '@supabase/supabase-js';
3
- import { runStorageProviderConformance } from '@selvajs/platform/testing';
4
- import { definitionPaths } from '@selvajs/platform';
5
- import { SupabaseStorageProvider } from '../SupabaseStorageProvider.js';
6
- const SUPABASE_URL = process.env.SUPABASE_URL;
7
- const SERVICE_ROLE_KEY = process.env.SUPABASE_SERVICE_ROLE_KEY;
8
- const PUBLIC_BUCKET = process.env.SUPABASE_PUBLIC_BUCKET ?? 'selva-public';
9
- const PRIVATE_BUCKET = process.env.SUPABASE_PRIVATE_BUCKET ?? 'selva-private';
10
- // The conformance suite hits a live Supabase stack. If creds aren't present
11
- // (e.g. in CI without Docker, or a clean clone pre-`supabase start`), skip
12
- // the suite with a single explanatory test so the run surfaces the reason.
13
- // `createClient` transitively loads `@supabase/realtime-js`, which on Node < 22
14
- // throws at construction without native WebSocket — treat that as "no stack"
15
- // rather than crashing the suite during collection.
16
- function tryBuildAdminClient() {
17
- if (!SUPABASE_URL || !SERVICE_ROLE_KEY)
18
- return null;
19
- try {
20
- return createClient(SUPABASE_URL, SERVICE_ROLE_KEY, {
21
- auth: { persistSession: false, autoRefreshToken: false }
22
- });
23
- }
24
- catch (err) {
25
- const msg = err instanceof Error ? err.message : String(err);
26
- if (msg.includes('WebSocket'))
27
- return null;
28
- throw err;
29
- }
30
- }
31
- const _adminClient = tryBuildAdminClient();
32
- if (_adminClient === null) {
33
- describe.skip('SupabaseStorageProvider (skipped: no live stack)', () => {
34
- it('populate packages/providers/supabase/.env.test with SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY (from `npx supabase start`) to run these tests', () => { });
35
- });
36
- }
37
- else {
38
- // Re-bind in this branch so closures below see the non-null narrowed type.
39
- const adminClient = _adminClient;
40
- /**
41
- * Delete every object under a prefix in a bucket. The conformance suite
42
- * assumes fresh state per test; Supabase Storage is shared across runs,
43
- * so we reset both buckets before each test.
44
- */
45
- async function emptyBucket(bucket) {
46
- const keys = [];
47
- const stack = [''];
48
- while (stack.length > 0) {
49
- const dir = stack.pop();
50
- const { data, error } = await adminClient.storage.from(bucket).list(dir, {
51
- limit: 1000,
52
- sortBy: { column: 'name', order: 'asc' }
53
- });
54
- if (error)
55
- throw error;
56
- for (const entry of data ?? []) {
57
- const full = dir ? `${dir}/${entry.name}` : entry.name;
58
- if (entry.id === null)
59
- stack.push(full);
60
- else
61
- keys.push(full);
62
- }
63
- }
64
- if (keys.length === 0)
65
- return;
66
- for (let i = 0; i < keys.length; i += 1000) {
67
- const { error } = await adminClient.storage.from(bucket).remove(keys.slice(i, i + 1000));
68
- if (error)
69
- throw error;
70
- }
71
- }
72
- describe('SupabaseStorageProvider', () => {
73
- beforeEach(async () => {
74
- await Promise.all([emptyBucket(PUBLIC_BUCKET), emptyBucket(PRIVATE_BUCKET)]);
75
- });
76
- runStorageProviderConformance({
77
- name: 'SupabaseStorageProvider',
78
- createStorage: () => new SupabaseStorageProvider({
79
- supabaseUrl: SUPABASE_URL,
80
- serviceRoleKey: SERVICE_ROLE_KEY,
81
- publicBucket: PUBLIC_BUCKET,
82
- privateBucket: PRIVATE_BUCKET
83
- })
84
- });
85
- // ============================================================================
86
- // Bucket routing — Supabase-specific (the conformance suite is provider-
87
- // agnostic and can't see which bucket a file lands in). Without these tests
88
- // a regex regression in `bucketFor` could silently move .gh source files
89
- // into the public/CDN bucket.
90
- // ============================================================================
91
- describe('bucket routing', () => {
92
- const storage = new SupabaseStorageProvider({
93
- supabaseUrl: SUPABASE_URL,
94
- serviceRoleKey: SERVICE_ROLE_KEY,
95
- publicBucket: PUBLIC_BUCKET,
96
- privateBucket: PRIVATE_BUCKET
97
- });
98
- async function existsInBucket(bucket, path) {
99
- const { data, error } = await adminClient.storage.from(bucket).download(path);
100
- if (error)
101
- return false;
102
- return Boolean(data);
103
- }
104
- it('versioned .gh files land in the private bucket', async () => {
105
- const guid = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa';
106
- const path = definitionPaths.version(guid, 1, 'gh');
107
- await storage.put(path, new TextEncoder().encode('GH_BYTES'));
108
- expect(await existsInBucket(PRIVATE_BUCKET, path)).toBe(true);
109
- expect(await existsInBucket(PUBLIC_BUCKET, path)).toBe(false);
110
- });
111
- it('versioned .ghx files land in the private bucket', async () => {
112
- const guid = 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb';
113
- const path = definitionPaths.version(guid, 3, 'ghx');
114
- await storage.put(path, new TextEncoder().encode('GHX_BYTES'));
115
- expect(await existsInBucket(PRIVATE_BUCKET, path)).toBe(true);
116
- expect(await existsInBucket(PUBLIC_BUCKET, path)).toBe(false);
117
- });
118
- it('cover images land in the public bucket', async () => {
119
- const guid = 'cccccccc-cccc-cccc-cccc-cccccccccccc';
120
- const path = definitionPaths.image(guid);
121
- // Real 1×1 transparent PNG. The provider runs every image through
122
- // transcodeImageIfNeeded → sharp, so a hand-crafted byte stub
123
- // would fail the decode. Routing (the test's actual concern) is
124
- // independent of which format the input arrives in.
125
- const tinyPng = Uint8Array.from([
126
- 137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82, 0, 0, 0, 1, 0, 0, 0, 1, 8,
127
- 6, 0, 0, 0, 31, 21, 196, 137, 0, 0, 0, 9, 112, 72, 89, 115, 0, 0, 3, 232, 0, 0, 3, 232, 1,
128
- 181, 123, 82, 107, 0, 0, 0, 13, 73, 68, 65, 84, 120, 156, 99, 96, 96, 96, 96, 0, 0, 0, 5,
129
- 0, 1, 165, 246, 69, 64, 0, 0, 0, 0, 73, 69, 78, 68, 174, 66, 96, 130
130
- ]);
131
- await storage.put(path, tinyPng, 'image/png');
132
- expect(await existsInBucket(PUBLIC_BUCKET, path)).toBe(true);
133
- expect(await existsInBucket(PRIVATE_BUCKET, path)).toBe(false);
134
- });
135
- it('getPublicUrl returns the proxy prefix for private files', () => {
136
- const path = definitionPaths.version('guid-1', 1, 'gh');
137
- const url = storage.getPublicUrl(path);
138
- expect(url.startsWith('/api/files/')).toBe(true);
139
- expect(url).not.toContain('/storage/v1/object/public/');
140
- });
141
- it('getPublicUrl returns the CDN URL for public files', () => {
142
- const path = definitionPaths.image('guid-1');
143
- const url = storage.getPublicUrl(path);
144
- expect(url).toContain(`/storage/v1/object/public/${PUBLIC_BUCKET}/`);
145
- });
146
- });
147
- });
148
- }
149
- //# sourceMappingURL=conformance.test.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"conformance.test.js","sourceRoot":"","sources":["../../../src/storage/__tests__/conformance.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,UAAU,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAC1D,OAAO,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AACrD,OAAO,EAAE,6BAA6B,EAAE,MAAM,2BAA2B,CAAC;AAC1E,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,EAAE,uBAAuB,EAAE,MAAM,+BAA+B,CAAC;AAExE,MAAM,YAAY,GAAG,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC;AAC9C,MAAM,gBAAgB,GAAG,OAAO,CAAC,GAAG,CAAC,yBAAyB,CAAC;AAC/D,MAAM,aAAa,GAAG,OAAO,CAAC,GAAG,CAAC,sBAAsB,IAAI,cAAc,CAAC;AAC3E,MAAM,cAAc,GAAG,OAAO,CAAC,GAAG,CAAC,uBAAuB,IAAI,eAAe,CAAC;AAE9E,4EAA4E;AAC5E,2EAA2E;AAC3E,2EAA2E;AAC3E,gFAAgF;AAChF,6EAA6E;AAC7E,oDAAoD;AACpD,SAAS,mBAAmB;IAC3B,IAAI,CAAC,YAAY,IAAI,CAAC,gBAAgB;QAAE,OAAO,IAAI,CAAC;IACpD,IAAI,CAAC;QACJ,OAAO,YAAY,CAAC,YAAY,EAAE,gBAAgB,EAAE;YACnD,IAAI,EAAE,EAAE,cAAc,EAAE,KAAK,EAAE,gBAAgB,EAAE,KAAK,EAAE;SACxD,CAAC,CAAC;IACJ,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACd,MAAM,GAAG,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QAC7D,IAAI,GAAG,CAAC,QAAQ,CAAC,WAAW,CAAC;YAAE,OAAO,IAAI,CAAC;QAC3C,MAAM,GAAG,CAAC;IACX,CAAC;AACF,CAAC;AACD,MAAM,YAAY,GAAG,mBAAmB,EAAE,CAAC;AAE3C,IAAI,YAAY,KAAK,IAAI,EAAE,CAAC;IAC3B,QAAQ,CAAC,IAAI,CAAC,kDAAkD,EAAE,GAAG,EAAE;QACtE,EAAE,CAAC,+IAA+I,EAAE,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;IAC/J,CAAC,CAAC,CAAC;AACJ,CAAC;KAAM,CAAC;IACP,2EAA2E;IAC3E,MAAM,WAAW,GAAG,YAAY,CAAC;IAEjC;;;;OAIG;IACH,KAAK,UAAU,WAAW,CAAC,MAAc;QACxC,MAAM,IAAI,GAAa,EAAE,CAAC;QAC1B,MAAM,KAAK,GAAa,CAAC,EAAE,CAAC,CAAC;QAC7B,OAAO,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACzB,MAAM,GAAG,GAAG,KAAK,CAAC,GAAG,EAAG,CAAC;YACzB,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,MAAM,WAAW,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE;gBACxE,KAAK,EAAE,IAAI;gBACX,MAAM,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE;aACxC,CAAC,CAAC;YACH,IAAI,KAAK;gBAAE,MAAM,KAAK,CAAC;YACvB,KAAK,MAAM,KAAK,IAAI,IAAI,IAAI,EAAE,EAAE,CAAC;gBAChC,MAAM,IAAI,GAAG,GAAG,CAAC,CAAC,CAAC,GAAG,GAAG,IAAI,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC;gBACvD,IAAI,KAAK,CAAC,EAAE,KAAK,IAAI;oBAAE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;;oBACnC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACtB,CAAC;QACF,CAAC;QACD,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO;QAC9B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,IAAI,IAAI,EAAE,CAAC;YAC5C,MAAM,EAAE,KAAK,EAAE,GAAG,MAAM,WAAW,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC;YACzF,IAAI,KAAK;gBAAE,MAAM,KAAK,CAAC;QACxB,CAAC;IACF,CAAC;IAED,QAAQ,CAAC,yBAAyB,EAAE,GAAG,EAAE;QACxC,UAAU,CAAC,KAAK,IAAI,EAAE;YACrB,MAAM,OAAO,CAAC,GAAG,CAAC,CAAC,WAAW,CAAC,aAAa,CAAC,EAAE,WAAW,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC;QAC9E,CAAC,CAAC,CAAC;QAEH,6BAA6B,CAAC;YAC7B,IAAI,EAAE,yBAAyB;YAC/B,aAAa,EAAE,GAAG,EAAE,CACnB,IAAI,uBAAuB,CAAC;gBAC3B,WAAW,EAAE,YAAa;gBAC1B,cAAc,EAAE,gBAAiB;gBACjC,YAAY,EAAE,aAAa;gBAC3B,aAAa,EAAE,cAAc;aAC7B,CAAC;SACH,CAAC,CAAC;QAEH,+EAA+E;QAC/E,yEAAyE;QACzE,4EAA4E;QAC5E,yEAAyE;QACzE,8BAA8B;QAC9B,+EAA+E;QAC/E,QAAQ,CAAC,gBAAgB,EAAE,GAAG,EAAE;YAC/B,MAAM,OAAO,GAAG,IAAI,uBAAuB,CAAC;gBAC3C,WAAW,EAAE,YAAa;gBAC1B,cAAc,EAAE,gBAAiB;gBACjC,YAAY,EAAE,aAAa;gBAC3B,aAAa,EAAE,cAAc;aAC7B,CAAC,CAAC;YAEH,KAAK,UAAU,cAAc,CAAC,MAAc,EAAE,IAAY;gBACzD,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,MAAM,WAAW,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;gBAC9E,IAAI,KAAK;oBAAE,OAAO,KAAK,CAAC;gBACxB,OAAO,OAAO,CAAC,IAAI,CAAC,CAAC;YACtB,CAAC;YAED,EAAE,CAAC,gDAAgD,EAAE,KAAK,IAAI,EAAE;gBAC/D,MAAM,IAAI,GAAG,sCAAsC,CAAC;gBACpD,MAAM,IAAI,GAAG,eAAe,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,EAAE,IAAI,CAAC,CAAC;gBACpD,MAAM,OAAO,CAAC,GAAG,CAAC,IAAI,EAAE,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC;gBAC9D,MAAM,CAAC,MAAM,cAAc,CAAC,cAAc,EAAE,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;gBAC9D,MAAM,CAAC,MAAM,cAAc,CAAC,aAAa,EAAE,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YAC/D,CAAC,CAAC,CAAC;YAEH,EAAE,CAAC,iDAAiD,EAAE,KAAK,IAAI,EAAE;gBAChE,MAAM,IAAI,GAAG,sCAAsC,CAAC;gBACpD,MAAM,IAAI,GAAG,eAAe,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,EAAE,KAAK,CAAC,CAAC;gBACrD,MAAM,OAAO,CAAC,GAAG,CAAC,IAAI,EAAE,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC,CAAC;gBAC/D,MAAM,CAAC,MAAM,cAAc,CAAC,cAAc,EAAE,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;gBAC9D,MAAM,CAAC,MAAM,cAAc,CAAC,aAAa,EAAE,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YAC/D,CAAC,CAAC,CAAC;YAEH,EAAE,CAAC,wCAAwC,EAAE,KAAK,IAAI,EAAE;gBACvD,MAAM,IAAI,GAAG,sCAAsC,CAAC;gBACpD,MAAM,IAAI,GAAG,eAAe,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;gBACzC,kEAAkE;gBAClE,8DAA8D;gBAC9D,gEAAgE;gBAChE,oDAAoD;gBACpD,MAAM,OAAO,GAAG,UAAU,CAAC,IAAI,CAAC;oBAC/B,GAAG,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC;oBACvF,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,GAAG,EAAE,EAAE,EAAE,EAAE,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC;oBACzF,GAAG,EAAE,GAAG,EAAE,EAAE,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,GAAG,EAAE,GAAG,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC;oBACxF,CAAC,EAAE,CAAC,EAAE,GAAG,EAAE,GAAG,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,GAAG,EAAE,EAAE,EAAE,EAAE,EAAE,GAAG;iBACpE,CAAC,CAAC;gBACH,MAAM,OAAO,CAAC,GAAG,CAAC,IAAI,EAAE,OAAO,EAAE,WAAW,CAAC,CAAC;gBAC9C,MAAM,CAAC,MAAM,cAAc,CAAC,aAAa,EAAE,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;gBAC7D,MAAM,CAAC,MAAM,cAAc,CAAC,cAAc,EAAE,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YAChE,CAAC,CAAC,CAAC;YAEH,EAAE,CAAC,yDAAyD,EAAE,GAAG,EAAE;gBAClE,MAAM,IAAI,GAAG,eAAe,CAAC,OAAO,CAAC,QAAQ,EAAE,CAAC,EAAE,IAAI,CAAC,CAAC;gBACxD,MAAM,GAAG,GAAG,OAAO,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC;gBACvC,MAAM,CAAC,GAAG,CAAC,UAAU,CAAC,aAAa,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;gBACjD,MAAM,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,4BAA4B,CAAC,CAAC;YACzD,CAAC,CAAC,CAAC;YAEH,EAAE,CAAC,mDAAmD,EAAE,GAAG,EAAE;gBAC5D,MAAM,IAAI,GAAG,eAAe,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;gBAC7C,MAAM,GAAG,GAAG,OAAO,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC;gBACvC,MAAM,CAAC,GAAG,CAAC,CAAC,SAAS,CAAC,6BAA6B,aAAa,GAAG,CAAC,CAAC;YACtE,CAAC,CAAC,CAAC;QACJ,CAAC,CAAC,CAAC;IACJ,CAAC,CAAC,CAAC;AACJ,CAAC"}
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=conformance.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"conformance.test.d.ts","sourceRoot":"","sources":["../../../src/userProfile/__tests__/conformance.test.ts"],"names":[],"mappings":""}
@@ -1,38 +0,0 @@
1
- import { describe, beforeEach, it } from 'vitest';
2
- import { runUserProfileStoreConformance } from '@selvajs/platform/testing';
3
- import { SupabaseUserProfileProvider } from '../SupabaseUserProfileProvider.js';
4
- import { readEnv, resetAllData } from '../../data/__tests__/test-helpers.js';
5
- const envCtx = readEnv();
6
- if (!envCtx) {
7
- describe.skip('SupabaseUserProfileProvider (skipped: no live stack)', () => {
8
- it('populate packages/providers/supabase/.env.test with Supabase creds to run these tests', () => { });
9
- });
10
- }
11
- else {
12
- describe('SupabaseUserProfileProvider', () => {
13
- beforeEach(async () => {
14
- await resetAllData(envCtx);
15
- });
16
- runUserProfileStoreConformance({
17
- name: 'SupabaseUserProfileProvider',
18
- createStore: async () => {
19
- const store = new SupabaseUserProfileProvider(envCtx.bundle);
20
- const seedUser = async (email) => {
21
- const { data, error } = await envCtx.adminClient.auth.admin.createUser({
22
- email,
23
- password: 'conformance-test-password-1234',
24
- email_confirm: true
25
- });
26
- if (error)
27
- throw error;
28
- return {
29
- id: data.user.id,
30
- email: data.user.email
31
- };
32
- };
33
- return { store, seedUser };
34
- }
35
- });
36
- });
37
- }
38
- //# sourceMappingURL=conformance.test.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"conformance.test.js","sourceRoot":"","sources":["../../../src/userProfile/__tests__/conformance.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,UAAU,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAClD,OAAO,EAAE,8BAA8B,EAAE,MAAM,2BAA2B,CAAC;AAE3E,OAAO,EAAE,2BAA2B,EAAE,MAAM,mCAAmC,CAAC;AAChF,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,sCAAsC,CAAC;AAE7E,MAAM,MAAM,GAAG,OAAO,EAAE,CAAC;AAEzB,IAAI,CAAC,MAAM,EAAE,CAAC;IACb,QAAQ,CAAC,IAAI,CAAC,sDAAsD,EAAE,GAAG,EAAE;QAC1E,EAAE,CAAC,uFAAuF,EAAE,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;IACvG,CAAC,CAAC,CAAC;AACJ,CAAC;KAAM,CAAC;IACP,QAAQ,CAAC,6BAA6B,EAAE,GAAG,EAAE;QAC5C,UAAU,CAAC,KAAK,IAAI,EAAE;YACrB,MAAM,YAAY,CAAC,MAAM,CAAC,CAAC;QAC5B,CAAC,CAAC,CAAC;QAEH,8BAA8B,CAAC;YAC9B,IAAI,EAAE,6BAA6B;YACnC,WAAW,EAAE,KAAK,IAAI,EAAE;gBACvB,MAAM,KAAK,GAAG,IAAI,2BAA2B,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;gBAC7D,MAAM,QAAQ,GAAG,KAAK,EAAE,KAAa,EAAqB,EAAE;oBAC3D,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,MAAM,MAAM,CAAC,WAAW,CAAC,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC;wBACtE,KAAK;wBACL,QAAQ,EAAE,gCAAgC;wBAC1C,aAAa,EAAE,IAAI;qBACnB,CAAC,CAAC;oBACH,IAAI,KAAK;wBAAE,MAAM,KAAK,CAAC;oBACvB,OAAO;wBACN,EAAE,EAAE,IAAI,CAAC,IAAI,CAAC,EAAE;wBAChB,KAAK,EAAE,IAAI,CAAC,IAAI,CAAC,KAAK;qBACtB,CAAC;gBACH,CAAC,CAAC;gBACF,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC;YAC5B,CAAC;SACD,CAAC,CAAC;IACJ,CAAC,CAAC,CAAC;AACJ,CAAC"}
@@ -1,464 +0,0 @@
1
- import { createClient, type SupabaseClient, type User } from '@supabase/supabase-js';
2
- import type {
3
- IAuthProvider,
4
- IEmailLinkAuth,
5
- IOAuthAuth,
6
- IPasswordAuth,
7
- AuthUser,
8
- LoginResult,
9
- UserManagementResult,
10
- ListOptions,
11
- Page
12
- } from '@selvajs/platform';
13
-
14
- /**
15
- * Auth backed by Supabase Auth (GoTrue).
16
- *
17
- * Identity + session lifecycle are fully delegated:
18
- * - `verifyLogin` wraps `auth.signInWithPassword` — returns the access_token
19
- * straight from Supabase as the session token.
20
- * - `verifyToken` calls `auth.getUser(token)` against an anon client scoped
21
- * to that bearer; GoTrue validates the JWT for us.
22
- * - User management uses `auth.admin.*` with the service-role client.
23
- *
24
- * Identity-only — platform permissions live on `IPlatformPermissionStore`
25
- * (`SupabasePlatformPermissionStore` reads `user_profiles.platform_permissions`)
26
- * and profile state on `IUserProfileStore`.
27
- */
28
- export interface SupabaseAuthProviderConfig {
29
- supabaseUrl: string;
30
- anonKey: string;
31
- serviceRoleKey: string;
32
- /**
33
- * Whether self-service signup is allowed via `passwordAuth.registerUser`.
34
- * Default false — production deployments start with invite-only.
35
- */
36
- enableSelfSignup?: boolean;
37
- /**
38
- * OAuth providers enabled in the Supabase dashboard (lowercased:
39
- * "google", "github", …). Surfaced via `oauth.listProviders()` so the
40
- * driving layer doesn't read provider-specific env vars itself. Defaults
41
- * to an empty list.
42
- */
43
- oauthProviders?: readonly string[];
44
- /**
45
- * Allow `emailLink.sendMagicLink` to create new users on first request.
46
- * Default true — matches Supabase's `signInWithOtp` default and gives
47
- * fresh installs a working signup path. Set false for invite-only
48
- * deployments; `sendMagicLink` for an unknown email returns
49
- * `{ ok: false, reason: 'signup_disabled' }`.
50
- */
51
- allowEmailLinkSignup?: boolean;
52
- }
53
-
54
- export class SupabaseAuthProvider implements IAuthProvider {
55
- readonly name = 'Supabase Auth';
56
- readonly passwordAuth: IPasswordAuth;
57
- readonly oauth: IOAuthAuth;
58
- readonly emailLink: IEmailLinkAuth;
59
-
60
- private readonly admin: SupabaseClient;
61
- private readonly anon: SupabaseClient;
62
- private readonly anonKey: string;
63
- private readonly supabaseUrl: string;
64
-
65
- constructor(config: SupabaseAuthProviderConfig) {
66
- this.supabaseUrl = config.supabaseUrl;
67
- this.anonKey = config.anonKey;
68
- this.admin = createClient(config.supabaseUrl, config.serviceRoleKey, {
69
- auth: { persistSession: false, autoRefreshToken: false }
70
- });
71
- this.anon = createClient(config.supabaseUrl, config.anonKey, {
72
- auth: { persistSession: false, autoRefreshToken: false }
73
- });
74
- this.passwordAuth = new SupabasePasswordAuth(
75
- this.admin,
76
- this.anon,
77
- (email, password) => this.signIn(email, password),
78
- config.enableSelfSignup ?? false,
79
- (user) => this.hydrate(user)
80
- );
81
- this.oauth = new SupabaseOAuthAuth(
82
- this.anon,
83
- (user) => this.hydrate(user),
84
- config.oauthProviders ?? []
85
- );
86
- this.emailLink = new SupabaseEmailLinkAuth(
87
- this.anon,
88
- (user) => this.hydrate(user),
89
- config.allowEmailLinkSignup ?? true
90
- );
91
- }
92
-
93
- static fromEnv(env: Record<string, string | undefined>): SupabaseAuthProvider {
94
- const supabaseUrl = env.SUPABASE_URL;
95
- const anonKey = env.SUPABASE_ANON_KEY;
96
- const serviceRoleKey = env.SUPABASE_SERVICE_ROLE_KEY;
97
- if (!supabaseUrl) throw new Error('Missing required env var: SUPABASE_URL');
98
- if (!anonKey) throw new Error('Missing required env var: SUPABASE_ANON_KEY');
99
- if (!serviceRoleKey) throw new Error('Missing required env var: SUPABASE_SERVICE_ROLE_KEY');
100
- const oauthProviders = (env.SUPABASE_OAUTH_PROVIDERS ?? '')
101
- .split(',')
102
- .map((p) => p.trim().toLowerCase())
103
- .filter((p) => p.length > 0);
104
- return new SupabaseAuthProvider({
105
- supabaseUrl,
106
- anonKey,
107
- serviceRoleKey,
108
- enableSelfSignup: env.SUPABASE_ENABLE_SELF_SIGNUP === 'true',
109
- oauthProviders,
110
- // Default true; set SUPABASE_ALLOW_EMAIL_LINK_SIGNUP=false to lock down
111
- // to invite-only and reject magic-link signups for new addresses.
112
- allowEmailLinkSignup: env.SUPABASE_ALLOW_EMAIL_LINK_SIGNUP !== 'false'
113
- });
114
- }
115
-
116
- async verifyToken(token: string): Promise<AuthUser | null> {
117
- if (!token) return null;
118
- // Per-request anon client with the JWT attached. `getUser(token)` also
119
- // works, but routing through a scoped client keeps future sub-calls
120
- // (profile fetch, etc.) RLS-aware if we ever move them here.
121
- const { data, error } = await this.anon.auth.getUser(token);
122
- if (error || !data.user) return null;
123
- if (data.user.user_metadata?.disabled === true) return null;
124
- return this.hydrate(data.user);
125
- }
126
-
127
- async getUser(id: string): Promise<AuthUser | null> {
128
- const { data, error } = await this.admin.auth.admin.getUserById(id);
129
- if (error || !data.user) return null;
130
- return this.hydrate(data.user);
131
- }
132
-
133
- async listUsers(opts?: ListOptions): Promise<Page<AuthUser> | null> {
134
- // GoTrue's admin.listUsers uses its own pagination (page/perPage).
135
- // Translate our cursor (offset) into page number.
136
- const perPage = opts?.limit ?? 50;
137
- const page = opts?.cursor ? parseInt(opts.cursor, 10) : 1;
138
- const { data, error } = await this.admin.auth.admin.listUsers({
139
- page: Number.isFinite(page) && page >= 1 ? page : 1,
140
- perPage
141
- });
142
- if (error) throw error;
143
-
144
- const users = data.users;
145
- const hydrated = users.map((u) => this.hydrate(u));
146
- // If the page was full, assume there may be more.
147
- const nextCursor = users.length >= perPage ? String(page + 1) : undefined;
148
- return { items: hydrated, nextCursor };
149
- }
150
-
151
- async createUser(email: string): Promise<AuthUser> {
152
- // Allowlist variant: create without a password. The user receives a
153
- // magic-link / OIDC handoff from the consuming app. Identity-only;
154
- // platform permissions are granted via IPlatformPermissionStore.
155
- const { data, error } = await this.admin.auth.admin.createUser({
156
- email,
157
- email_confirm: true
158
- });
159
- if (error) throw error;
160
- if (!data.user) throw new Error('createUser returned no user');
161
- return this.hydrate(data.user);
162
- }
163
-
164
- async deleteUser(id: string): Promise<UserManagementResult> {
165
- // Identity-only delete. The §2 sole-`instance_admin` invariant lives
166
- // on IPlatformPermissionStore; callers consult it before calling here.
167
- const { error } = await this.admin.auth.admin.deleteUser(id);
168
- if (error) {
169
- // 404-shaped errors from GoTrue surface as { status: 404 } or a
170
- // specific message. Normalize to 'not_found' rather than throwing.
171
- const e = error as unknown as { status?: number; message?: string };
172
- if (e.status === 404 || /not.?found/i.test(e.message ?? '')) return 'not_found';
173
- throw error;
174
- }
175
- return 'ok';
176
- }
177
-
178
- async disableUser(id: string): Promise<UserManagementResult> {
179
- const { data: existing, error: fetchError } = await this.admin.auth.admin.getUserById(id);
180
- if (fetchError || !existing.user) return 'not_found';
181
- // GoTrue's user_metadata update merges shallowly — preserve existing
182
- // fields by spreading them in.
183
- const mergedMetadata = {
184
- ...(existing.user.user_metadata ?? {}),
185
- disabled: true
186
- };
187
- const { error } = await this.admin.auth.admin.updateUserById(id, {
188
- user_metadata: mergedMetadata
189
- });
190
- if (error) {
191
- const e = error as unknown as { status?: number; message?: string };
192
- if (e.status === 404 || /not.?found/i.test(e.message ?? '')) return 'not_found';
193
- throw error;
194
- }
195
- return 'ok';
196
- }
197
-
198
- async touchLastLogin(id: string): Promise<void> {
199
- // 60-second debounce. Matches the local provider.
200
- const { data, error } = await this.admin
201
- .from('user_profiles')
202
- .select('last_login_at')
203
- .eq('user_id', id)
204
- .maybeSingle();
205
- if (error) return;
206
-
207
- if (data?.last_login_at) {
208
- const prev = Date.parse(data.last_login_at);
209
- if (Number.isFinite(prev) && Date.now() - prev < 60_000) return;
210
- }
211
- await this.admin
212
- .from('user_profiles')
213
- .update({ last_login_at: new Date().toISOString() })
214
- .eq('user_id', id);
215
- }
216
-
217
- // OAuth lives on `this.oauth` (typed `IOAuthAuth`); see `SupabaseOAuthAuth`
218
- // below. Mirrors the `passwordAuth` capability split.
219
-
220
- // ============================================================================
221
- // Internals
222
- // ============================================================================
223
- /**
224
- * Sign in via GoTrue. Returns the access token string and the hydrated
225
- * user. Called by `SupabasePasswordAuth.verifyLogin` — kept on the
226
- * provider class so it has access to the hydrate helper without plumbing.
227
- */
228
- private async signIn(
229
- email: string,
230
- password: string
231
- ): Promise<{ user: AuthUser; sessionToken: string } | null> {
232
- const { data, error } = await this.anon.auth.signInWithPassword({ email, password });
233
- if (error || !data.user || !data.session) return null;
234
- if (data.user.user_metadata?.disabled === true) return null;
235
- return { user: this.hydrate(data.user), sessionToken: data.session.access_token };
236
- }
237
-
238
- /**
239
- * Map a GoTrue `User` to our identity-only `AuthUser`. Platform permissions
240
- * live on `IPlatformPermissionStore` and profile fields (displayName,
241
- * starred, recentRuns) on `IUserProfileStore` — neither is in AuthUser.
242
- */
243
- private hydrate(user: User): AuthUser {
244
- return {
245
- id: user.id,
246
- email: user.email ?? undefined,
247
- createdAt: user.created_at ?? undefined,
248
- lastLoginAt: user.last_sign_in_at ?? undefined,
249
- disabled: user.user_metadata?.disabled === true,
250
- metadata: user.user_metadata
251
- };
252
- }
253
-
254
- /**
255
- * Expose the anon URL + key so other code (e.g. the same process's
256
- * storage provider or per-request client factories) can build user-scoped
257
- * clients without requiring a second config read.
258
- */
259
- getAnonClientConfig(): { supabaseUrl: string; anonKey: string } {
260
- return { supabaseUrl: this.supabaseUrl, anonKey: this.anonKey };
261
- }
262
- }
263
-
264
- class SupabasePasswordAuth implements IPasswordAuth {
265
- constructor(
266
- private readonly admin: SupabaseClient,
267
- private readonly anon: SupabaseClient,
268
- private readonly signIn: (
269
- email: string,
270
- password: string
271
- ) => Promise<{ user: AuthUser; sessionToken: string } | null>,
272
- private readonly enableSelfSignup: boolean,
273
- private readonly hydrate: (user: User) => AuthUser
274
- ) {}
275
-
276
- async verifyLogin(email: string, password: string): Promise<LoginResult> {
277
- const result = await this.signIn(email, password);
278
- if (!result) return { kind: 'failed', reason: 'invalid_credentials' };
279
- return { kind: 'success', user: result.user, sessionToken: result.sessionToken };
280
- }
281
-
282
- async createUserWithPassword(email: string, password: string): Promise<AuthUser> {
283
- // Identity-only — platform permissions are granted separately via
284
- // IPlatformPermissionStore.set after creation.
285
- const { data, error } = await this.admin.auth.admin.createUser({
286
- email,
287
- password,
288
- email_confirm: true
289
- });
290
- if (error) throw error;
291
- if (!data.user) throw new Error('createUserWithPassword returned no user');
292
- return this.hydrate(data.user);
293
- }
294
-
295
- async registerUser(email: string, password: string): Promise<AuthUser | null> {
296
- if (!this.enableSelfSignup) return null;
297
- const { data, error } = await this.anon.auth.signUp({ email, password });
298
- if (error || !data.user) return null;
299
- return this.hydrate(data.user);
300
- }
301
- }
302
-
303
- /**
304
- * Supabase-backed `IOAuthAuth`. Thin wrappers over GoTrue's `signInWithOAuth`
305
- * / `exchangeCodeForSession` / `refreshSession` — the failure-shape mapping
306
- * (return null vs throw) is the only logic.
307
- */
308
- class SupabaseOAuthAuth implements IOAuthAuth {
309
- constructor(
310
- private readonly anon: SupabaseClient,
311
- private readonly hydrate: (user: User) => AuthUser,
312
- private readonly providers: readonly string[]
313
- ) {}
314
-
315
- listProviders(): readonly string[] {
316
- return this.providers;
317
- }
318
-
319
- async getOAuthAuthorizationUrl(
320
- provider: 'google' | 'github' | 'azure' | 'gitlab',
321
- redirectTo: string
322
- ): Promise<string> {
323
- const { data, error } = await this.anon.auth.signInWithOAuth({
324
- provider,
325
- options: { redirectTo, skipBrowserRedirect: true }
326
- });
327
- if (error || !data.url) throw error ?? new Error('signInWithOAuth returned no URL');
328
- return data.url;
329
- }
330
-
331
- async exchangeOAuthCode(code: string): Promise<{
332
- user: AuthUser;
333
- sessionToken: string;
334
- refreshToken: string;
335
- } | null> {
336
- const { data, error } = await this.anon.auth.exchangeCodeForSession(code);
337
- if (error || !data.user || !data.session) return null;
338
- if (data.user.user_metadata?.disabled === true) return null;
339
- return {
340
- user: this.hydrate(data.user),
341
- sessionToken: data.session.access_token,
342
- refreshToken: data.session.refresh_token
343
- };
344
- }
345
-
346
- async refreshSession(refreshToken: string): Promise<{
347
- sessionToken: string;
348
- refreshToken: string;
349
- } | null> {
350
- const { data, error } = await this.anon.auth.refreshSession({ refresh_token: refreshToken });
351
- if (error || !data.session) return null;
352
- return {
353
- sessionToken: data.session.access_token,
354
- refreshToken: data.session.refresh_token
355
- };
356
- }
357
- }
358
-
359
- /**
360
- * Supabase-backed `IEmailLinkAuth`. Wraps GoTrue's `signInWithOtp` (send the
361
- * link) and `verifyOtp` with `type: 'magiclink' | 'email' | 'signup'`
362
- * (verify on click).
363
- *
364
- * Token shape: Supabase emails the user `{callbackUrl}?token_hash=…&type=…`.
365
- * `verifyMagicLink` accepts either the full URL or just the raw `token_hash`
366
- * — adapters MAY support either to give the route layer flexibility. The
367
- * `type` query param decides which OTP variant `verifyOtp` runs (signup vs
368
- * magic-link returning user vs invite acceptance), so we pass it through.
369
- */
370
- class SupabaseEmailLinkAuth implements IEmailLinkAuth {
371
- constructor(
372
- private readonly anon: SupabaseClient,
373
- private readonly hydrate: (user: User) => AuthUser,
374
- private readonly allowSignup: boolean
375
- ) {}
376
-
377
- async sendMagicLink(
378
- email: string,
379
- callbackUrl: string
380
- ): Promise<
381
- { ok: true } | { ok: false; reason: 'rate_limited' | 'signup_disabled' | 'invalid_email' }
382
- > {
383
- const { error } = await this.anon.auth.signInWithOtp({
384
- email,
385
- options: {
386
- emailRedirectTo: callbackUrl,
387
- shouldCreateUser: this.allowSignup
388
- }
389
- });
390
- if (!error) return { ok: true };
391
-
392
- // Map GoTrue's classified errors to our coarse reasons. Anything we
393
- // don't recognize we throw — the route returns 500 and we get a stack
394
- // trace, instead of swallowing a real bug as "rate limited".
395
- const status = (error as { status?: number }).status;
396
- const code = (error as { code?: string }).code;
397
- const message = error.message ?? '';
398
- if (status === 429 || /rate.?limit/i.test(message))
399
- return { ok: false, reason: 'rate_limited' };
400
- if (
401
- code === 'otp_disabled' ||
402
- code === 'signup_disabled' ||
403
- /signup.*disabled/i.test(message)
404
- ) {
405
- return { ok: false, reason: 'signup_disabled' };
406
- }
407
- if (code === 'validation_failed' || /invalid.*email|email.*invalid/i.test(message)) {
408
- return { ok: false, reason: 'invalid_email' };
409
- }
410
- throw error;
411
- }
412
-
413
- async verifyMagicLink(rawCallbackUrl: string): Promise<{
414
- user: AuthUser;
415
- sessionToken: string;
416
- refreshToken?: string;
417
- } | null> {
418
- const params = parseCallbackParams(rawCallbackUrl);
419
- if (!params) return null;
420
- const { tokenHash, type } = params;
421
-
422
- const { data, error } = await this.anon.auth.verifyOtp({
423
- token_hash: tokenHash,
424
- type
425
- });
426
- if (error || !data.user || !data.session) return null;
427
- if (data.user.user_metadata?.disabled === true) return null;
428
- return {
429
- user: this.hydrate(data.user),
430
- sessionToken: data.session.access_token,
431
- refreshToken: data.session.refresh_token
432
- };
433
- }
434
- }
435
-
436
- /**
437
- * Pull `token_hash` + `type` from either a full callback URL or a bare
438
- * `token_hash=…&type=…` query string. Tolerant of both shapes so the route
439
- * layer can pass whichever form is convenient.
440
- *
441
- * Returns null when the inputs aren't recognizable — the caller treats null
442
- * as "send the user back to /login".
443
- */
444
- function parseCallbackParams(
445
- raw: string
446
- ): { tokenHash: string; type: 'magiclink' | 'email' | 'signup' | 'invite' | 'recovery' } | null {
447
- let search: URLSearchParams;
448
- try {
449
- // Full URL path
450
- search = new URL(raw).searchParams;
451
- } catch {
452
- // Query-string-only path — treat as bare params.
453
- search = new URLSearchParams(raw.startsWith('?') ? raw.slice(1) : raw);
454
- }
455
- const tokenHash = search.get('token_hash');
456
- const type = search.get('type');
457
- if (!tokenHash) return null;
458
- const allowed = new Set(['magiclink', 'email', 'signup', 'invite', 'recovery']);
459
- if (!type || !allowed.has(type)) return null;
460
- return {
461
- tokenHash,
462
- type: type as 'magiclink' | 'email' | 'signup' | 'invite' | 'recovery'
463
- };
464
- }