@jskit-ai/auth-provider-supabase-core 0.1.4

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.
@@ -0,0 +1,138 @@
1
+ export default Object.freeze({
2
+ "packageVersion": 1,
3
+ "packageId": "@jskit-ai/auth-provider-supabase-core",
4
+ "version": "0.1.4",
5
+ "options": {
6
+ "auth-supabase-url": {
7
+ "required": true,
8
+ "values": [],
9
+ "promptLabel": "Supabase URL",
10
+ "promptHint": "https://YOUR-PROJECT.supabase.co"
11
+ },
12
+ "auth-supabase-publishable-key": {
13
+ "required": true,
14
+ "values": [],
15
+ "promptLabel": "Supabase publishable key",
16
+ "promptHint": "sb_publishable_..."
17
+ },
18
+ "app-public-url": {
19
+ "required": true,
20
+ "values": [],
21
+ "defaultValue": "http://localhost:5173",
22
+ "promptLabel": "App public URL",
23
+ "promptHint": "Browser URL used for auth redirects"
24
+ }
25
+ },
26
+ "dependsOn": [
27
+ "@jskit-ai/auth-core",
28
+ "@jskit-ai/value-app-config-shared"
29
+ ],
30
+ "capabilities": {
31
+ "provides": [
32
+ "auth.provider.supabase",
33
+ "auth.provider"
34
+ ],
35
+ "requires": [
36
+ "auth.access"
37
+ ]
38
+ },
39
+ "runtime": {
40
+ "server": {
41
+ "providerEntrypoint": "src/server/providers/AuthSupabaseServiceProvider.js",
42
+ "providers": [
43
+ {
44
+ "entrypoint": "src/server/providers/AuthSupabaseServiceProvider.js",
45
+ "export": "AuthSupabaseServiceProvider"
46
+ },
47
+ {
48
+ "entrypoint": "src/server/providers/AuthProviderServiceProvider.js",
49
+ "export": "AuthProviderServiceProvider"
50
+ }
51
+ ]
52
+ }
53
+ },
54
+ "metadata": {
55
+ "apiSummary": {
56
+ "surfaces": [
57
+ {
58
+ "subpath": "./server/providers/AuthSupabaseServiceProvider",
59
+ "summary": "Exports the Supabase auth provider service provider."
60
+ },
61
+ {
62
+ "subpath": "./server/providers/AuthProviderServiceProvider",
63
+ "summary": "Exports the generic auth provider registration service provider."
64
+ },
65
+ {
66
+ "subpath": "./server/lib/index",
67
+ "summary": "Exports curated server-side Supabase auth service helpers."
68
+ },
69
+ {
70
+ "subpath": "./client",
71
+ "summary": "Exports no runtime API today (reserved client entrypoint)."
72
+ }
73
+ ],
74
+ "containerTokens": {
75
+ "server": [
76
+ "authService",
77
+ "auth.provider.supabase.actionContributor"
78
+ ],
79
+ "client": []
80
+ }
81
+ }
82
+ },
83
+ "mutations": {
84
+ "dependencies": {
85
+ "runtime": {
86
+ "@jskit-ai/auth-core": "0.1.4",
87
+ "@jskit-ai/kernel": "0.1.4",
88
+ "dotenv": "^16.4.5",
89
+ "@supabase/supabase-js": "^2.57.4",
90
+ "jose": "^6.1.0"
91
+ },
92
+ "dev": {}
93
+ },
94
+ "packageJson": {
95
+ "scripts": {}
96
+ },
97
+ "procfile": {},
98
+ "files": [],
99
+ "text": [
100
+ {
101
+ "file": ".env",
102
+ "op": "upsert-env",
103
+ "key": "AUTH_PROVIDER",
104
+ "value": "supabase",
105
+ "reason": "Select Supabase as the auth provider.",
106
+ "category": "runtime-config",
107
+ "id": "auth-provider"
108
+ },
109
+ {
110
+ "file": ".env",
111
+ "op": "upsert-env",
112
+ "key": "AUTH_SUPABASE_URL",
113
+ "value": "${option:auth-supabase-url}",
114
+ "reason": "Configure Supabase project URL for auth.",
115
+ "category": "runtime-config",
116
+ "id": "auth-supabase-url"
117
+ },
118
+ {
119
+ "file": ".env",
120
+ "op": "upsert-env",
121
+ "key": "AUTH_SUPABASE_PUBLISHABLE_KEY",
122
+ "value": "${option:auth-supabase-publishable-key}",
123
+ "reason": "Configure Supabase publishable key for auth.",
124
+ "category": "runtime-config",
125
+ "id": "auth-supabase-publishable-key"
126
+ },
127
+ {
128
+ "file": ".env",
129
+ "op": "upsert-env",
130
+ "key": "APP_PUBLIC_URL",
131
+ "value": "${option:app-public-url}",
132
+ "reason": "Configure application public URL for auth redirect flows.",
133
+ "category": "runtime-config",
134
+ "id": "auth-app-public-url"
135
+ }
136
+ ]
137
+ }
138
+ });
package/package.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "@jskit-ai/auth-provider-supabase-core",
3
+ "version": "0.1.4",
4
+ "type": "module",
5
+ "scripts": {
6
+ "test": "node --test"
7
+ },
8
+ "exports": {
9
+ "./server/providers/AuthSupabaseServiceProvider": "./src/server/providers/AuthSupabaseServiceProvider.js",
10
+ "./server/providers/AuthProviderServiceProvider": "./src/server/providers/AuthProviderServiceProvider.js",
11
+ "./server/lib/index": "./src/server/lib/index.js",
12
+ "./client": "./src/client/index.js"
13
+ },
14
+ "dependencies": {
15
+ "@jskit-ai/auth-core": "0.1.4",
16
+ "@jskit-ai/kernel": "0.1.4",
17
+ "jose": "^6.1.0",
18
+ "@supabase/supabase-js": "^2.57.4"
19
+ }
20
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,276 @@
1
+ import { AppError } from "@jskit-ai/kernel/server/runtime/errors";
2
+
3
+ function normalizeLocalReturnToPath(value, { fallback = "" } = {}) {
4
+ const normalized = String(value || "").trim();
5
+ if (!normalized) {
6
+ return fallback;
7
+ }
8
+
9
+ if (!normalized.startsWith("/") || normalized.startsWith("//")) {
10
+ return fallback;
11
+ }
12
+
13
+ return normalized;
14
+ }
15
+
16
+ function createAccountFlows(deps) {
17
+ const {
18
+ ensureConfigured,
19
+ validators,
20
+ validationError,
21
+ getSupabaseClient,
22
+ displayNameFromEmail,
23
+ mapAuthError,
24
+ syncProfileFromSupabaseUser,
25
+ resolvePasswordSignInPolicyForUserId,
26
+ otpLoginRedirectUrl,
27
+ buildOtpLoginRedirectUrl,
28
+ appPublicUrl,
29
+ isTransientSupabaseError,
30
+ isUserNotFoundLikeAuthError,
31
+ parseOtpLoginVerifyPayload,
32
+ mapOtpVerifyError,
33
+ setSessionFromRequestCookies,
34
+ mapProfileUpdateError,
35
+ normalizeReturnToPath = normalizeLocalReturnToPath
36
+ } = deps;
37
+
38
+ function resolveOtpEmailRedirectTo(returnToValue) {
39
+ const returnTo = normalizeReturnToPath(returnToValue, { fallback: "" });
40
+ if (!returnTo) {
41
+ return otpLoginRedirectUrl;
42
+ }
43
+
44
+ if (typeof buildOtpLoginRedirectUrl === "function") {
45
+ try {
46
+ return buildOtpLoginRedirectUrl({
47
+ appPublicUrl,
48
+ returnTo
49
+ });
50
+ } catch {
51
+ // Fall through to the pre-built URL fallback below.
52
+ }
53
+ }
54
+
55
+ try {
56
+ const redirectUrl = new URL(String(otpLoginRedirectUrl || ""));
57
+ redirectUrl.searchParams.set("returnTo", returnTo);
58
+ return redirectUrl.toString();
59
+ } catch {
60
+ return otpLoginRedirectUrl;
61
+ }
62
+ }
63
+
64
+ async function register(payload) {
65
+ ensureConfigured();
66
+
67
+ const parsed = validators.registerInput(payload);
68
+ if (Object.keys(parsed.fieldErrors).length > 0) {
69
+ throw validationError(parsed.fieldErrors);
70
+ }
71
+
72
+ const supabase = getSupabaseClient();
73
+ const response = await supabase.auth.signUp({
74
+ email: parsed.email,
75
+ password: parsed.password,
76
+ options: {
77
+ data: {
78
+ display_name: displayNameFromEmail(parsed.email)
79
+ }
80
+ }
81
+ });
82
+
83
+ if (response.error) {
84
+ throw mapAuthError(response.error, 400);
85
+ }
86
+
87
+ if (!response.data?.user) {
88
+ throw new AppError(500, "Supabase sign-up did not return a user.");
89
+ }
90
+
91
+ const profile = await syncProfileFromSupabaseUser(response.data.user, parsed.email);
92
+
93
+ if (!response.data.session) {
94
+ return {
95
+ requiresEmailConfirmation: true,
96
+ email: parsed.email,
97
+ profile,
98
+ session: null
99
+ };
100
+ }
101
+
102
+ return {
103
+ requiresEmailConfirmation: false,
104
+ profile,
105
+ session: response.data.session
106
+ };
107
+ }
108
+
109
+ async function login(payload) {
110
+ ensureConfigured();
111
+
112
+ const parsed = validators.loginInput(payload);
113
+ if (Object.keys(parsed.fieldErrors).length > 0) {
114
+ throw validationError(parsed.fieldErrors);
115
+ }
116
+
117
+ const supabase = getSupabaseClient();
118
+ const response = await supabase.auth.signInWithPassword({
119
+ email: parsed.email,
120
+ password: parsed.password
121
+ });
122
+
123
+ if (response.error || !response.data?.user || !response.data?.session) {
124
+ throw mapAuthError(response.error, 401);
125
+ }
126
+
127
+ const profile = await syncProfileFromSupabaseUser(response.data.user, parsed.email);
128
+ const passwordSignInPolicy = await resolvePasswordSignInPolicyForUserId(profile.id);
129
+ if (!passwordSignInPolicy.passwordSignInEnabled) {
130
+ throw new AppError(401, "Invalid email or password.");
131
+ }
132
+
133
+ return {
134
+ profile,
135
+ session: response.data.session
136
+ };
137
+ }
138
+
139
+ async function requestOtpLogin(payload) {
140
+ ensureConfigured();
141
+
142
+ const parsed = validators.forgotPasswordInput(payload);
143
+ if (Object.keys(parsed.fieldErrors).length > 0) {
144
+ throw validationError(parsed.fieldErrors);
145
+ }
146
+
147
+ const emailRedirectTo = resolveOtpEmailRedirectTo(payload?.returnTo);
148
+ const supabase = getSupabaseClient();
149
+ let response;
150
+ try {
151
+ response = await supabase.auth.signInWithOtp({
152
+ email: parsed.email,
153
+ options: {
154
+ shouldCreateUser: false,
155
+ emailRedirectTo
156
+ }
157
+ });
158
+ } catch (error) {
159
+ if (isTransientSupabaseError(error)) {
160
+ throw mapAuthError(error, 503);
161
+ }
162
+
163
+ return {
164
+ ok: true,
165
+ message: "If an account exists for that email, a one-time code has been sent."
166
+ };
167
+ }
168
+
169
+ if (response.error) {
170
+ if (isTransientSupabaseError(response.error)) {
171
+ throw mapAuthError(response.error, 503);
172
+ }
173
+
174
+ if (isUserNotFoundLikeAuthError(response.error)) {
175
+ return {
176
+ ok: true,
177
+ message: "If an account exists for that email, a one-time code has been sent."
178
+ };
179
+ }
180
+
181
+ throw mapAuthError(response.error, Number(response.error?.status || 400));
182
+ }
183
+
184
+ return {
185
+ ok: true,
186
+ message: "If an account exists for that email, a one-time code has been sent."
187
+ };
188
+ }
189
+
190
+ async function verifyOtpLogin(payload) {
191
+ ensureConfigured();
192
+
193
+ const parsed = parseOtpLoginVerifyPayload(payload);
194
+ if (Object.keys(parsed.fieldErrors).length > 0) {
195
+ throw validationError(parsed.fieldErrors);
196
+ }
197
+
198
+ const supabase = getSupabaseClient();
199
+ let response;
200
+ try {
201
+ if (parsed.tokenHash) {
202
+ response = await supabase.auth.verifyOtp({
203
+ token_hash: parsed.tokenHash,
204
+ type: parsed.type
205
+ });
206
+ } else {
207
+ response = await supabase.auth.verifyOtp({
208
+ email: parsed.email,
209
+ token: parsed.token,
210
+ type: parsed.type
211
+ });
212
+ }
213
+ } catch (error) {
214
+ throw mapOtpVerifyError(error);
215
+ }
216
+
217
+ if (response.error || !response.data?.session || !response.data?.user) {
218
+ throw mapOtpVerifyError(response.error);
219
+ }
220
+
221
+ const profile = await syncProfileFromSupabaseUser(response.data.user, response.data.user.email || parsed.email);
222
+
223
+ return {
224
+ profile,
225
+ session: response.data.session
226
+ };
227
+ }
228
+
229
+ async function updateDisplayName(request, displayName) {
230
+ ensureConfigured();
231
+
232
+ const normalizedDisplayName = String(displayName || "").trim();
233
+ if (!normalizedDisplayName) {
234
+ throw validationError({
235
+ displayName: "Display name is required."
236
+ });
237
+ }
238
+
239
+ const supabase = getSupabaseClient();
240
+ const sessionResponse = await setSessionFromRequestCookies(request, {
241
+ supabaseClient: supabase
242
+ });
243
+
244
+ let updateResponse;
245
+ try {
246
+ updateResponse = await supabase.auth.updateUser({
247
+ data: {
248
+ display_name: normalizedDisplayName
249
+ }
250
+ });
251
+ } catch (error) {
252
+ throw mapProfileUpdateError(error);
253
+ }
254
+
255
+ if (updateResponse.error || !updateResponse.data?.user) {
256
+ throw mapProfileUpdateError(updateResponse.error);
257
+ }
258
+
259
+ const profile = await syncProfileFromSupabaseUser(updateResponse.data.user, updateResponse.data.user.email);
260
+
261
+ return {
262
+ profile,
263
+ session: sessionResponse.data.session
264
+ };
265
+ }
266
+
267
+ return {
268
+ register,
269
+ login,
270
+ requestOtpLogin,
271
+ verifyOtpLogin,
272
+ updateDisplayName
273
+ };
274
+ }
275
+
276
+ export { createAccountFlows };
@@ -0,0 +1,225 @@
1
+ import {
2
+ EMPTY_INPUT_VALIDATOR
3
+ } from "@jskit-ai/kernel/shared/actions/actionContributorHelpers";
4
+ import { authRegisterCommand } from "@jskit-ai/auth-core/shared/commands/authRegisterCommand";
5
+ import { authLoginPasswordCommand } from "@jskit-ai/auth-core/shared/commands/authLoginPasswordCommand";
6
+ import { authLoginOtpRequestCommand } from "@jskit-ai/auth-core/shared/commands/authLoginOtpRequestCommand";
7
+ import { authLoginOtpVerifyCommand } from "@jskit-ai/auth-core/shared/commands/authLoginOtpVerifyCommand";
8
+ import { authLoginOAuthStartCommand } from "@jskit-ai/auth-core/shared/commands/authLoginOAuthStartCommand";
9
+ import { authLoginOAuthCompleteCommand } from "@jskit-ai/auth-core/shared/commands/authLoginOAuthCompleteCommand";
10
+ import { authPasswordResetRequestCommand } from "@jskit-ai/auth-core/shared/commands/authPasswordResetRequestCommand";
11
+ import { authPasswordRecoveryCompleteCommand } from "@jskit-ai/auth-core/shared/commands/authPasswordRecoveryCompleteCommand";
12
+ import { authPasswordResetCommand } from "@jskit-ai/auth-core/shared/commands/authPasswordResetCommand";
13
+
14
+ function requireRequestContext(context, actionId) {
15
+ const request = context?.requestMeta?.request || null;
16
+ if (request) {
17
+ return request;
18
+ }
19
+
20
+ throw new Error(`${actionId} requires request context.`);
21
+ }
22
+
23
+ const authActions = Object.freeze([
24
+ {
25
+ id: "auth.register",
26
+ version: 1,
27
+ kind: "command",
28
+ channels: ["api", "internal"],
29
+ surfacesFrom: "enabled",
30
+ inputValidator: authRegisterCommand.operation.bodyValidator,
31
+ idempotency: "none",
32
+ audit: {
33
+ actionName: "auth.register"
34
+ },
35
+ observability: {},
36
+ async execute(input, _context, deps) {
37
+ return deps.authService.register(input);
38
+ }
39
+ },
40
+ {
41
+ id: "auth.login.password",
42
+ version: 1,
43
+ kind: "command",
44
+ channels: ["api", "internal"],
45
+ surfacesFrom: "enabled",
46
+ inputValidator: authLoginPasswordCommand.operation.bodyValidator,
47
+ idempotency: "none",
48
+ audit: {
49
+ actionName: "auth.login.password"
50
+ },
51
+ observability: {},
52
+ async execute(input, _context, deps) {
53
+ return deps.authService.login(input);
54
+ }
55
+ },
56
+ {
57
+ id: "auth.login.otp.request",
58
+ version: 1,
59
+ kind: "command",
60
+ channels: ["api", "internal"],
61
+ surfacesFrom: "enabled",
62
+ inputValidator: authLoginOtpRequestCommand.operation.bodyValidator,
63
+ idempotency: "none",
64
+ audit: {
65
+ actionName: "auth.login.otp.request"
66
+ },
67
+ observability: {},
68
+ async execute(input, _context, deps) {
69
+ return deps.authService.requestOtpLogin(input);
70
+ }
71
+ },
72
+ {
73
+ id: "auth.login.otp.verify",
74
+ version: 1,
75
+ kind: "command",
76
+ channels: ["api", "internal"],
77
+ surfacesFrom: "enabled",
78
+ inputValidator: authLoginOtpVerifyCommand.operation.bodyValidator,
79
+ idempotency: "none",
80
+ audit: {
81
+ actionName: "auth.login.otp.verify"
82
+ },
83
+ observability: {},
84
+ async execute(input, _context, deps) {
85
+ return deps.authService.verifyOtpLogin(input);
86
+ }
87
+ },
88
+ {
89
+ id: "auth.login.oauth.start",
90
+ version: 1,
91
+ kind: "command",
92
+ channels: ["api", "internal"],
93
+ surfacesFrom: "enabled",
94
+ inputValidator: [authLoginOAuthStartCommand.operation.paramsValidator, authLoginOAuthStartCommand.operation.queryValidator],
95
+ idempotency: "none",
96
+ audit: {
97
+ actionName: "auth.login.oauth.start"
98
+ },
99
+ observability: {},
100
+ async execute(input, _context, deps) {
101
+ return deps.authService.oauthStart(input);
102
+ }
103
+ },
104
+ {
105
+ id: "auth.login.oauth.complete",
106
+ version: 1,
107
+ kind: "command",
108
+ channels: ["api", "internal"],
109
+ surfacesFrom: "enabled",
110
+ inputValidator: authLoginOAuthCompleteCommand.operation.bodyValidator,
111
+ idempotency: "none",
112
+ audit: {
113
+ actionName: "auth.login.oauth.complete"
114
+ },
115
+ observability: {},
116
+ async execute(input, _context, deps) {
117
+ return deps.authService.oauthComplete(input);
118
+ }
119
+ },
120
+ {
121
+ id: "auth.password.reset.request",
122
+ version: 1,
123
+ kind: "command",
124
+ channels: ["api", "internal"],
125
+ surfacesFrom: "enabled",
126
+ inputValidator: authPasswordResetRequestCommand.operation.bodyValidator,
127
+ idempotency: "none",
128
+ audit: {
129
+ actionName: "auth.password.reset.request"
130
+ },
131
+ observability: {},
132
+ async execute(input, _context, deps) {
133
+ return deps.authService.requestPasswordReset(input);
134
+ }
135
+ },
136
+ {
137
+ id: "auth.password.recovery.complete",
138
+ version: 1,
139
+ kind: "command",
140
+ channels: ["api", "internal"],
141
+ surfacesFrom: "enabled",
142
+ inputValidator: authPasswordRecoveryCompleteCommand.operation.bodyValidator,
143
+ idempotency: "none",
144
+ audit: {
145
+ actionName: "auth.password.recovery.complete"
146
+ },
147
+ observability: {},
148
+ async execute(input, _context, deps) {
149
+ return deps.authService.completePasswordRecovery(input);
150
+ }
151
+ },
152
+ {
153
+ id: "auth.password.reset",
154
+ version: 1,
155
+ kind: "command",
156
+ channels: ["api", "internal"],
157
+ surfacesFrom: "enabled",
158
+ inputValidator: authPasswordResetCommand.operation.bodyValidator,
159
+ idempotency: "none",
160
+ audit: {
161
+ actionName: "auth.password.reset"
162
+ },
163
+ observability: {},
164
+ async execute(input, context, deps) {
165
+ return deps.authService.resetPassword(requireRequestContext(context, "auth.password.reset"), input);
166
+ }
167
+ },
168
+ {
169
+ id: "auth.logout",
170
+ version: 1,
171
+ kind: "command",
172
+ channels: ["api", "automation", "internal"],
173
+ surfacesFrom: "enabled",
174
+ inputValidator: EMPTY_INPUT_VALIDATOR,
175
+ outputValidator: {
176
+ schema: {
177
+ type: "object",
178
+ properties: {
179
+ ok: {
180
+ type: "boolean"
181
+ },
182
+ clearSession: {
183
+ type: "boolean"
184
+ }
185
+ },
186
+ required: ["ok", "clearSession"],
187
+ additionalProperties: false
188
+ }
189
+ },
190
+ idempotency: "none",
191
+ audit: {
192
+ actionName: "auth.logout"
193
+ },
194
+ observability: {},
195
+ async execute(_input, context, deps) {
196
+ if (deps.authSessionEventsService && typeof deps.authSessionEventsService.notifySessionChanged === "function") {
197
+ await deps.authSessionEventsService.notifySessionChanged({
198
+ context
199
+ });
200
+ }
201
+ return {
202
+ ok: true,
203
+ clearSession: true
204
+ };
205
+ }
206
+ },
207
+ {
208
+ id: "auth.session.read",
209
+ version: 1,
210
+ kind: "query",
211
+ channels: ["api", "internal"],
212
+ surfacesFrom: "enabled",
213
+ inputValidator: EMPTY_INPUT_VALIDATOR,
214
+ idempotency: "none",
215
+ audit: {
216
+ actionName: "auth.session.read"
217
+ },
218
+ observability: {},
219
+ async execute(_input, context, deps) {
220
+ return deps.authService.authenticateRequest(requireRequestContext(context, "auth.session.read"));
221
+ }
222
+ }
223
+ ]);
224
+
225
+ export { authActions };
@@ -0,0 +1,24 @@
1
+ function safeRequestCookies(request) {
2
+ if (request?.cookies && typeof request.cookies === "object") {
3
+ return request.cookies;
4
+ }
5
+
6
+ return {};
7
+ }
8
+
9
+ function cookieOptions(isProduction, maxAge) {
10
+ const options = {
11
+ httpOnly: true,
12
+ sameSite: "lax",
13
+ secure: isProduction,
14
+ path: "/"
15
+ };
16
+
17
+ if (Number.isFinite(maxAge)) {
18
+ options.maxAge = Math.max(0, Math.floor(maxAge));
19
+ }
20
+
21
+ return options;
22
+ }
23
+
24
+ export { safeRequestCookies, cookieOptions };