@jskit-ai/auth-provider-supabase-core 0.1.95 → 0.1.97

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.
@@ -1,7 +1,7 @@
1
1
  export default Object.freeze({
2
2
  "packageVersion": 1,
3
3
  "packageId": "@jskit-ai/auth-provider-supabase-core",
4
- "version": "0.1.95",
4
+ "version": "0.1.97",
5
5
  "kind": "runtime",
6
6
  "options": {
7
7
  "auth-supabase-url": {
@@ -83,8 +83,8 @@ export default Object.freeze({
83
83
  "mutations": {
84
84
  "dependencies": {
85
85
  "runtime": {
86
- "@jskit-ai/auth-core": "0.1.95",
87
- "@jskit-ai/kernel": "0.1.96",
86
+ "@jskit-ai/auth-core": "0.1.97",
87
+ "@jskit-ai/kernel": "0.1.99",
88
88
  "dotenv": "^16.4.5",
89
89
  "@supabase/supabase-js": "^2.57.4",
90
90
  "jose": "^6.1.0"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jskit-ai/auth-provider-supabase-core",
3
- "version": "0.1.95",
3
+ "version": "0.1.97",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "test": "node --test"
@@ -12,8 +12,8 @@
12
12
  "./client": "./src/client/index.js"
13
13
  },
14
14
  "dependencies": {
15
- "@jskit-ai/auth-core": "0.1.95",
16
- "@jskit-ai/kernel": "0.1.96",
15
+ "@jskit-ai/auth-core": "0.1.97",
16
+ "@jskit-ai/kernel": "0.1.99",
17
17
  "json-rest-schema": "1.x.x",
18
18
  "jose": "^6.1.0",
19
19
  "@supabase/supabase-js": "^2.57.4",
@@ -240,14 +240,22 @@ const authActionsAfterDevLogin = Object.freeze([
240
240
  },
241
241
  observability: {},
242
242
  async execute(_input, context, deps) {
243
+ let logoutResult = {
244
+ ok: true,
245
+ clearSession: true
246
+ };
247
+ if (deps.authService && typeof deps.authService.logout === "function") {
248
+ logoutResult = await deps.authService.logout(requireRequestContext(context, "auth.logout"));
249
+ }
250
+
243
251
  if (deps.authSessionEventsService && typeof deps.authSessionEventsService.notifySessionChanged === "function") {
244
252
  await deps.authSessionEventsService.notifySessionChanged({
245
253
  context
246
254
  });
247
255
  }
248
256
  return {
249
- ok: true,
250
- clearSession: true
257
+ ok: logoutResult?.ok !== false,
258
+ clearSession: logoutResult?.clearSession !== false
251
259
  };
252
260
  }
253
261
  },
@@ -21,4 +21,14 @@ function cookieOptions(isProduction, maxAge) {
21
21
  return options;
22
22
  }
23
23
 
24
- export { safeRequestCookies, cookieOptions };
24
+ function cookieClearOptions(isProduction) {
25
+ return [
26
+ cookieOptions(isProduction, 0),
27
+ {
28
+ ...cookieOptions(isProduction, 0),
29
+ path: "/api"
30
+ }
31
+ ];
32
+ }
33
+
34
+ export { safeRequestCookies, cookieOptions, cookieClearOptions };
@@ -32,7 +32,8 @@ function createPasswordSecurityFlows(deps) {
32
32
  buildAuthMethodsStatusFromSupabaseUser,
33
33
  buildSecurityStatusFromAuthMethodsStatus,
34
34
  authMethodPasswordProvider,
35
- buildAuthMethodsStatusFromProviderIds
35
+ buildAuthMethodsStatusFromProviderIds,
36
+ resolveDevAuthSecurityStatus
36
37
  } = deps;
37
38
 
38
39
  async function requestPasswordReset(payload) {
@@ -316,6 +317,13 @@ function createPasswordSecurityFlows(deps) {
316
317
  return buildSecurityStatusFromAuthMethodsStatus(authMethodsStatus);
317
318
  }
318
319
 
320
+ if (typeof resolveDevAuthSecurityStatus === "function") {
321
+ const devAuthSecurityStatus = await resolveDevAuthSecurityStatus(request);
322
+ if (devAuthSecurityStatus) {
323
+ return devAuthSecurityStatus;
324
+ }
325
+ }
326
+
319
327
  const current = await resolveCurrentAuthContext(request);
320
328
  return buildSecurityStatusFromAuthMethodsStatus(current.authMethodsStatus);
321
329
  }
@@ -42,7 +42,7 @@ import {
42
42
  findAuthMethodById,
43
43
  findLinkedIdentityByProvider
44
44
  } from "./authMethodStatus.js";
45
- import { safeRequestCookies, cookieOptions } from "./authCookies.js";
45
+ import { safeRequestCookies, cookieOptions, cookieClearOptions } from "./authCookies.js";
46
46
  import { loadJose, isExpiredJwtError, classifyJwtVerifyError } from "./authJwt.js";
47
47
  import { buildDisabledPasswordSecret } from "./authSecrets.js";
48
48
  import { createAccountFlows } from "./accountFlows.js";
@@ -55,6 +55,7 @@ import {
55
55
  authenticateDevAuthRequest,
56
56
  createDevAuthSession,
57
57
  ensureDevAuthBootstrapAvailable,
58
+ isDevAuthToken,
58
59
  resolveDevAuthConfig,
59
60
  resolveDevAuthProfile
60
61
  } from "./devAuthBootstrap.js";
@@ -372,6 +373,39 @@ function createService(options) {
372
373
  };
373
374
  }
374
375
 
376
+ function resolveRequestSessionTokens(request) {
377
+ const cookies = safeRequestCookies(request);
378
+ return {
379
+ accessToken: String(cookies[ACCESS_TOKEN_COOKIE] || ""),
380
+ refreshToken: String(cookies[REFRESH_TOKEN_COOKIE] || "")
381
+ };
382
+ }
383
+
384
+ async function authenticateDevAuthRequestFromCookies(request, tokens = resolveRequestSessionTokens(request)) {
385
+ const devAuthResult = await authenticateDevAuthRequest(
386
+ {
387
+ request,
388
+ accessToken: tokens.accessToken,
389
+ refreshToken: tokens.refreshToken
390
+ },
391
+ {
392
+ config: devAuthConfig,
393
+ userProfilesRepository
394
+ }
395
+ );
396
+
397
+ if (!devAuthResult || devAuthResult.authenticated !== true) {
398
+ return devAuthResult;
399
+ }
400
+
401
+ return {
402
+ ...devAuthResult,
403
+ profile: requireAuthenticatedProfile(devAuthResult.profile, {
404
+ context: "dev auth profile"
405
+ })
406
+ };
407
+ }
408
+
375
409
  function writeSessionCookies(reply, session) {
376
410
  const accessToken = String(session?.access_token || "");
377
411
  const refreshToken = String(session?.refresh_token || "");
@@ -387,9 +421,49 @@ function createService(options) {
387
421
  }
388
422
 
389
423
  function clearSessionCookies(reply) {
390
- const clearOptions = cookieOptions(isProduction, 0);
391
- reply.clearCookie(ACCESS_TOKEN_COOKIE, clearOptions);
392
- reply.clearCookie(REFRESH_TOKEN_COOKIE, clearOptions);
424
+ for (const clearOptions of cookieClearOptions(isProduction)) {
425
+ reply.clearCookie(ACCESS_TOKEN_COOKIE, clearOptions);
426
+ reply.clearCookie(REFRESH_TOKEN_COOKIE, clearOptions);
427
+ }
428
+ }
429
+
430
+ async function logout(request) {
431
+ const cookies = safeRequestCookies(request);
432
+ const accessToken = String(cookies[ACCESS_TOKEN_COOKIE] || "").trim();
433
+ const refreshToken = String(cookies[REFRESH_TOKEN_COOKIE] || "").trim();
434
+
435
+ if (!accessToken && !refreshToken) {
436
+ return {
437
+ ok: true,
438
+ clearSession: true
439
+ };
440
+ }
441
+
442
+ if (isDevAuthToken(accessToken) || isDevAuthToken(refreshToken)) {
443
+ return {
444
+ ok: true,
445
+ clearSession: true
446
+ };
447
+ }
448
+
449
+ ensureConfigured();
450
+ const supabase = getSupabaseClient();
451
+ await setSessionFromRequestCookies(request, {
452
+ supabaseClient: supabase
453
+ });
454
+
455
+ const response = await supabase.auth.signOut({
456
+ scope: "local"
457
+ });
458
+
459
+ if (response.error) {
460
+ throw mapAuthError(response.error, Number(response.error?.status || 400));
461
+ }
462
+
463
+ return {
464
+ ok: true,
465
+ clearSession: true
466
+ };
393
467
  }
394
468
 
395
469
  function requireSynchronizedProfile(profile) {
@@ -571,6 +645,25 @@ function createService(options) {
571
645
  };
572
646
  }
573
647
 
648
+ async function resolveDevAuthSecurityStatus(request) {
649
+ const devAuthResult = await authenticateDevAuthRequestFromCookies(request);
650
+ if (!devAuthResult) {
651
+ return null;
652
+ }
653
+
654
+ if (devAuthResult.authenticated !== true) {
655
+ throw new AppError(401, "Authentication required.");
656
+ }
657
+
658
+ const passwordSignInPolicy = await resolvePasswordSignInPolicyForUserId(devAuthResult.profile.id);
659
+ const authMethodsStatus = buildAuthMethodsStatusFromProviderIds([AUTH_METHOD_PASSWORD_PROVIDER], {
660
+ ...passwordSignInPolicy,
661
+ oauthProviders: authOAuthProviders
662
+ });
663
+
664
+ return buildSecurityStatusFromAuthMethodsStatus(authMethodsStatus);
665
+ }
666
+
574
667
  const { register, resendRegisterConfirmation, login, requestOtpLogin, verifyOtpLogin, updateDisplayName } = createAccountFlows({
575
668
  ensureConfigured,
576
669
  validationError,
@@ -652,34 +745,15 @@ function createService(options) {
652
745
  buildAuthMethodsStatusFromProviderIds(providerIds, {
653
746
  ...statusOptions,
654
747
  oauthProviders: authOAuthProviders
655
- })
748
+ }),
749
+ resolveDevAuthSecurityStatus
656
750
  });
657
751
 
658
752
  async function authenticateRequest(request) {
659
- const cookies = safeRequestCookies(request);
660
- const accessToken = String(cookies[ACCESS_TOKEN_COOKIE] || "");
661
- const refreshToken = String(cookies[REFRESH_TOKEN_COOKIE] || "");
753
+ const { accessToken, refreshToken } = resolveRequestSessionTokens(request);
662
754
 
663
- const devAuthResult = await authenticateDevAuthRequest(
664
- {
665
- request,
666
- accessToken,
667
- refreshToken
668
- },
669
- {
670
- config: devAuthConfig,
671
- userProfilesRepository
672
- }
673
- );
755
+ const devAuthResult = await authenticateDevAuthRequestFromCookies(request, { accessToken, refreshToken });
674
756
  if (devAuthResult) {
675
- if (devAuthResult.authenticated === true) {
676
- return {
677
- ...devAuthResult,
678
- profile: requireAuthenticatedProfile(devAuthResult.profile, {
679
- context: "dev auth profile"
680
- })
681
- };
682
- }
683
757
  return devAuthResult;
684
758
  }
685
759
 
@@ -860,6 +934,7 @@ function createService(options) {
860
934
  setPasswordSignInEnabled,
861
935
  unlinkProvider,
862
936
  signOutOtherSessions,
937
+ logout,
863
938
  getSecurityStatus,
864
939
  getSettingsProfileAuthInfo,
865
940
  getOAuthProviderCatalog,
@@ -909,6 +984,7 @@ const __testables = {
909
984
  findLinkedIdentityByProvider,
910
985
  safeRequestCookies,
911
986
  cookieOptions,
987
+ cookieClearOptions,
912
988
  isExpiredJwtError,
913
989
  classifyJwtVerifyError
914
990
  };
@@ -5,3 +5,163 @@ import * as authProviderSupabase from "../src/server/lib/index.js";
5
5
  test("auth.provider.supabase exports required symbols", () => {
6
6
  assert.equal(typeof authProviderSupabase.createService, "function");
7
7
  });
8
+
9
+ function createServiceFixture(overrides = {}) {
10
+ return authProviderSupabase.createService({
11
+ authProvider: {
12
+ id: "supabase",
13
+ supabaseUrl: "",
14
+ supabasePublishableKey: ""
15
+ },
16
+ appPublicUrl: "http://localhost:5173",
17
+ nodeEnv: "development",
18
+ userProfileSyncService: {
19
+ async findByIdentity() {
20
+ return null;
21
+ },
22
+ async syncIdentityProfile(profile) {
23
+ return {
24
+ id: "1",
25
+ email: String(profile?.email || "ada@example.com"),
26
+ displayName: String(profile?.displayName || "Ada Example"),
27
+ authProvider: String(profile?.authProvider || "supabase"),
28
+ authProviderUserSid: String(profile?.authProviderUserSid || "supabase-user-1")
29
+ };
30
+ }
31
+ },
32
+ ...overrides
33
+ });
34
+ }
35
+
36
+ test("clearSessionCookies clears root and API path session cookie variants", () => {
37
+ const authService = createServiceFixture();
38
+ const clearCalls = [];
39
+ const reply = {
40
+ clearCookie(name, options) {
41
+ clearCalls.push({
42
+ name,
43
+ options
44
+ });
45
+ }
46
+ };
47
+
48
+ authService.clearSessionCookies(reply);
49
+
50
+ assert.deepEqual(
51
+ clearCalls.map((call) => ({ name: call.name, path: call.options.path, maxAge: call.options.maxAge })),
52
+ [
53
+ { name: "sb_access_token", path: "/", maxAge: 0 },
54
+ { name: "sb_refresh_token", path: "/", maxAge: 0 },
55
+ { name: "sb_access_token", path: "/api", maxAge: 0 },
56
+ { name: "sb_refresh_token", path: "/api", maxAge: 0 }
57
+ ]
58
+ );
59
+ });
60
+
61
+ test("clearSessionCookies preserves secure cookie clearing in production", () => {
62
+ const authService = createServiceFixture({
63
+ nodeEnv: "production"
64
+ });
65
+ const clearCalls = [];
66
+ const reply = {
67
+ clearCookie(name, options) {
68
+ clearCalls.push({
69
+ name,
70
+ options
71
+ });
72
+ }
73
+ };
74
+
75
+ authService.clearSessionCookies(reply);
76
+
77
+ assert.equal(clearCalls.length, 4);
78
+ assert.equal(clearCalls.every((call) => call.options.secure === true), true);
79
+ });
80
+
81
+ test("logout is local-only when no session cookies are present", async () => {
82
+ const authService = createServiceFixture();
83
+
84
+ const result = await authService.logout({
85
+ cookies: {}
86
+ });
87
+
88
+ assert.deepEqual(result, {
89
+ ok: true,
90
+ clearSession: true
91
+ });
92
+ });
93
+
94
+ test("logout is local-only for dev auth cookies", async () => {
95
+ const authService = createServiceFixture();
96
+
97
+ const result = await authService.logout({
98
+ cookies: {
99
+ sb_access_token: "jskit-dev.invalid"
100
+ }
101
+ });
102
+
103
+ assert.deepEqual(result, {
104
+ ok: true,
105
+ clearSession: true
106
+ });
107
+ });
108
+
109
+ test("auth logout action delegates to provider logout and notifies session changes", async () => {
110
+ const action = authProviderSupabase
111
+ .buildAuthActions()
112
+ .find((definition) => definition.id === "auth.logout");
113
+ const request = {
114
+ id: "request-1"
115
+ };
116
+ const calls = [];
117
+
118
+ const result = await action.execute(
119
+ {},
120
+ {
121
+ requestMeta: {
122
+ request
123
+ }
124
+ },
125
+ {
126
+ authService: {
127
+ async logout(receivedRequest) {
128
+ calls.push({
129
+ type: "logout",
130
+ request: receivedRequest
131
+ });
132
+ return {
133
+ ok: true,
134
+ clearSession: true
135
+ };
136
+ }
137
+ },
138
+ authSessionEventsService: {
139
+ async notifySessionChanged(payload) {
140
+ calls.push({
141
+ type: "notify",
142
+ context: payload.context
143
+ });
144
+ }
145
+ }
146
+ }
147
+ );
148
+
149
+ assert.deepEqual(result, {
150
+ ok: true,
151
+ clearSession: true
152
+ });
153
+ assert.deepEqual(calls, [
154
+ {
155
+ type: "logout",
156
+ request
157
+ },
158
+ {
159
+ type: "notify",
160
+ context: {
161
+ requestMeta: {
162
+ request
163
+ }
164
+ }
165
+ }
166
+ ]);
167
+ });
@@ -109,6 +109,47 @@ test("dev auth bootstrap can issue and authenticate a local session without Supa
109
109
  assert.equal(authResult.session, null);
110
110
  });
111
111
 
112
+ test("dev auth bootstrap resolves security status without Supabase", async () => {
113
+ const authService = createServiceFixture();
114
+ const loginResult = await authService.devLoginAs(createLocalRequest(), {
115
+ userId: "7"
116
+ });
117
+
118
+ const securityStatus = await authService.getSecurityStatus(
119
+ createLocalRequest({
120
+ cookies: {
121
+ sb_access_token: loginResult.session.access_token,
122
+ sb_refresh_token: loginResult.session.refresh_token
123
+ }
124
+ })
125
+ );
126
+
127
+ const passwordMethod = securityStatus.authMethods.find((method) => method.id === "password");
128
+ assert.equal(securityStatus.authPolicy.minimumEnabledMethods, 1);
129
+ assert.equal(passwordMethod?.configured, true);
130
+ assert.equal(passwordMethod?.enabled, true);
131
+ });
132
+
133
+ test("dev auth security status rejects invalid dev sessions without recovery-link errors", async () => {
134
+ const authService = createServiceFixture();
135
+
136
+ await assert.rejects(
137
+ () =>
138
+ authService.getSecurityStatus(
139
+ createLocalRequest({
140
+ cookies: {
141
+ sb_access_token: "jskit-dev.invalid"
142
+ }
143
+ })
144
+ ),
145
+ (error) => {
146
+ assert.equal(error.statusCode || error.status, 401);
147
+ assert.equal(error.message, "Authentication required.");
148
+ return true;
149
+ }
150
+ );
151
+ });
152
+
112
153
  test("dev auth bootstrap supports email lookup", async () => {
113
154
  const authService = createServiceFixture();
114
155