@jskit-ai/auth-provider-supabase-core 0.1.114 → 0.1.116

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.114",
4
+ "version": "0.1.116",
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.114",
87
- "@jskit-ai/kernel": "0.1.116",
86
+ "@jskit-ai/auth-core": "0.1.116",
87
+ "@jskit-ai/kernel": "0.1.119",
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.114",
3
+ "version": "0.1.116",
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.114",
16
- "@jskit-ai/kernel": "0.1.116",
15
+ "@jskit-ai/auth-core": "0.1.116",
16
+ "@jskit-ai/kernel": "0.1.119",
17
17
  "json-rest-schema": "1.x.x",
18
18
  "jose": "^6.1.0",
19
19
  "@supabase/supabase-js": "^2.57.4",
@@ -1,5 +1,10 @@
1
1
  import { AppError } from "@jskit-ai/kernel/server/runtime/errors";
2
2
  import { normalizeRecordId } from "@jskit-ai/kernel/shared/support/normalize";
3
+ import {
4
+ assertDevAuthPolicy,
5
+ ensureDevAuthRuntimeAvailable,
6
+ resolveDevAuthPolicy
7
+ } from "@jskit-ai/auth-core/server/devAuth";
3
8
  import { normalizeEmail } from "@jskit-ai/auth-core/server/utils";
4
9
  import { loadJose, isExpiredJwtError } from "./authJwt.js";
5
10
 
@@ -9,20 +14,6 @@ const DEFAULT_DEV_AUTH_ACCESS_TTL_SECONDS = 60 * 60 * 12;
9
14
  const DEFAULT_DEV_AUTH_REFRESH_TTL_SECONDS = 60 * 60 * 24 * 30;
10
15
  const encoder = new TextEncoder();
11
16
 
12
- function parseBoolean(value, fallback = false) {
13
- const raw = String(value || "").trim().toLowerCase();
14
- if (!raw) {
15
- return fallback;
16
- }
17
- if (["1", "true", "yes", "on"].includes(raw)) {
18
- return true;
19
- }
20
- if (["0", "false", "no", "off"].includes(raw)) {
21
- return false;
22
- }
23
- return fallback;
24
- }
25
-
26
17
  function normalizePositiveInteger(value, fallback) {
27
18
  const parsed = Number.parseInt(String(value || "").trim(), 10);
28
19
  if (Number.isInteger(parsed) && parsed > 0) {
@@ -31,66 +22,6 @@ function normalizePositiveInteger(value, fallback) {
31
22
  return fallback;
32
23
  }
33
24
 
34
- function normalizeRequestHostname(request) {
35
- const hostHeader = String(request?.headers?.host || "").trim();
36
- if (!hostHeader) {
37
- return "";
38
- }
39
-
40
- const firstHost = hostHeader.split(",")[0]?.trim();
41
- if (!firstHost) {
42
- return "";
43
- }
44
-
45
- try {
46
- return new URL(`http://${firstHost}`).hostname.trim().toLowerCase();
47
- } catch {
48
- return firstHost
49
- .replace(/^\[/, "")
50
- .replace(/\]$/, "")
51
- .split(":")[0]
52
- .trim()
53
- .toLowerCase();
54
- }
55
- }
56
-
57
- function resolveDirectRemoteAddress(request) {
58
- return String(request?.socket?.remoteAddress || request?.raw?.socket?.remoteAddress || "").trim();
59
- }
60
-
61
- function normalizeLoopbackIp(value) {
62
- return String(value || "")
63
- .trim()
64
- .toLowerCase()
65
- .replace(/^\[|\]$/g, "")
66
- .replace(/^::ffff:/, "");
67
- }
68
-
69
- function isLoopbackIp(value) {
70
- const normalized = normalizeLoopbackIp(value);
71
- return normalized === "::1" || normalized === "127.0.0.1" || normalized.startsWith("127.");
72
- }
73
-
74
- function isLoopbackHostname(value) {
75
- const normalized = String(value || "")
76
- .trim()
77
- .toLowerCase()
78
- .replace(/^\[|\]$/g, "");
79
- return (
80
- normalized === "localhost" ||
81
- normalized.endsWith(".localhost") ||
82
- normalized === "::1" ||
83
- normalized === "127.0.0.1"
84
- );
85
- }
86
-
87
- function isLocalDevAuthRequest(request) {
88
- return (
89
- isLoopbackIp(resolveDirectRemoteAddress(request)) &&
90
- isLoopbackHostname(normalizeRequestHostname(request))
91
- );
92
- }
93
-
94
25
  function isDevAuthToken(token) {
95
26
  return String(token || "").trim().startsWith(DEV_AUTH_TOKEN_PREFIX);
96
27
  }
@@ -107,10 +38,13 @@ function resolveDevAuthConfig({
107
38
  accessTtlSeconds = DEFAULT_DEV_AUTH_ACCESS_TTL_SECONDS,
108
39
  refreshTtlSeconds = DEFAULT_DEV_AUTH_REFRESH_TTL_SECONDS
109
40
  } = {}) {
41
+ const policy = resolveDevAuthPolicy({
42
+ enabled,
43
+ nodeEnv,
44
+ secret
45
+ });
110
46
  return Object.freeze({
111
- enabled: parseBoolean(enabled, false),
112
- secret: String(secret || "").trim(),
113
- isProduction: String(nodeEnv || "development").trim().toLowerCase() === "production",
47
+ ...policy,
114
48
  jwtAudience: String(jwtAudience || "authenticated").trim() || "authenticated",
115
49
  accessTtlSeconds: normalizePositiveInteger(accessTtlSeconds, DEFAULT_DEV_AUTH_ACCESS_TTL_SECONDS),
116
50
  refreshTtlSeconds: normalizePositiveInteger(refreshTtlSeconds, DEFAULT_DEV_AUTH_REFRESH_TTL_SECONDS)
@@ -118,16 +52,10 @@ function resolveDevAuthConfig({
118
52
  }
119
53
 
120
54
  function assertDevAuthBootstrapConfig(config, { userProfilesRepository = null } = {}) {
55
+ assertDevAuthPolicy(config);
121
56
  if (!config?.enabled) {
122
57
  return;
123
58
  }
124
-
125
- if (config.isProduction) {
126
- throw new Error("AUTH_DEV_BYPASS_ENABLED must not be enabled in production.");
127
- }
128
- if (!config.secret) {
129
- throw new Error("AUTH_DEV_BYPASS_SECRET is required when AUTH_DEV_BYPASS_ENABLED=true.");
130
- }
131
59
  if (
132
60
  !userProfilesRepository ||
133
61
  typeof userProfilesRepository.findById !== "function" ||
@@ -139,18 +67,6 @@ function assertDevAuthBootstrapConfig(config, { userProfilesRepository = null }
139
67
  }
140
68
  }
141
69
 
142
- function ensureDevAuthBootstrapAvailable(config, request) {
143
- if (!config?.enabled || config?.isProduction) {
144
- throw new AppError(404, "Not found.");
145
- }
146
- if (!config.secret) {
147
- throw new AppError(500, "AUTH_DEV_BYPASS_SECRET is required when AUTH_DEV_BYPASS_ENABLED=true.");
148
- }
149
- if (!isLocalDevAuthRequest(request)) {
150
- throw new AppError(403, "Dev auth bootstrap is only available from localhost.");
151
- }
152
- }
153
-
154
70
  function buildProfileFromTokenClaims(payload) {
155
71
  const id = normalizeRecordId(payload?.sub, { fallback: null });
156
72
  const email = normalizeEmail(payload?.email || "");
@@ -322,7 +238,9 @@ async function authenticateDevAuthRequest(
322
238
  return null;
323
239
  }
324
240
 
325
- if (!config?.enabled || config?.isProduction || !config?.secret || !isLocalDevAuthRequest(request)) {
241
+ try {
242
+ ensureDevAuthRuntimeAvailable(config, request);
243
+ } catch {
326
244
  return {
327
245
  authenticated: false,
328
246
  clearSession: true,
@@ -391,7 +309,6 @@ export {
391
309
  assertDevAuthBootstrapConfig,
392
310
  authenticateDevAuthRequest,
393
311
  createDevAuthSession,
394
- ensureDevAuthBootstrapAvailable,
395
312
  isDevAuthToken,
396
313
  resolveDevAuthConfig,
397
314
  resolveDevAuthProfile
@@ -1,2 +1 @@
1
1
  export { createService, __testables } from "./service.js";
2
- export { devLoginAsAction } from "./actions/auth.contributor.js";
@@ -1,6 +1,7 @@
1
1
  import { createClient } from "@supabase/supabase-js";
2
2
  import { AppError } from "@jskit-ai/kernel/server/runtime/errors";
3
3
  import { normalizeAuthCapabilities } from "@jskit-ai/auth-core/shared/authCapabilities";
4
+ import { ensureDevAuthExchangeAvailable } from "@jskit-ai/auth-core/server/devAuth";
4
5
  import { normalizeAuthActor, normalizeAuthResult } from "@jskit-ai/auth-core/server/authActor";
5
6
  import {
6
7
  AUTH_METHOD_PASSWORD_ID,
@@ -56,7 +57,6 @@ import {
56
57
  assertDevAuthBootstrapConfig,
57
58
  authenticateDevAuthRequest,
58
59
  createDevAuthSession,
59
- ensureDevAuthBootstrapAvailable,
60
60
  isDevAuthToken,
61
61
  resolveDevAuthConfig,
62
62
  resolveDevAuthProfile
@@ -1032,7 +1032,7 @@ function createService(options) {
1032
1032
  }
1033
1033
 
1034
1034
  async function devLoginAs(request, input = {}) {
1035
- ensureDevAuthBootstrapAvailable(devAuthConfig, request);
1035
+ ensureDevAuthExchangeAvailable(devAuthConfig, request);
1036
1036
  const rawProfile = await resolveDevAuthProfile(input, {
1037
1037
  userProfilesRepository,
1038
1038
  validationError
@@ -1,9 +1,8 @@
1
1
  import { resolveAllowedOriginsFromSurfaceDefinitions } from "@jskit-ai/kernel/shared/support/returnToPath";
2
- import { withActionDefaults } from "@jskit-ai/kernel/shared/actions";
3
2
  import { applyAuthServiceDecorators } from "@jskit-ai/auth-core/server/authServiceDecoratorRegistry";
4
3
  import { normalizeEmail } from "@jskit-ai/auth-core/server/utils";
5
4
  import { createService } from "../lib/service.js";
6
- import { devLoginAsAction } from "../lib/actions/auth.contributor.js";
5
+
7
6
  const PROFILE_MODE_PROVIDER = "provider";
8
7
  const PROFILE_MODE_STANDALONE = "standalone";
9
8
  const PROFILE_MODE_USERS = "users";
@@ -17,20 +16,6 @@ function splitCsv(value) {
17
16
  .filter(Boolean);
18
17
  }
19
18
 
20
- function parseBoolean(value, fallback = false) {
21
- const raw = String(value || "").trim().toLowerCase();
22
- if (!raw) {
23
- return fallback;
24
- }
25
- if (["1", "true", "yes", "on"].includes(raw)) {
26
- return true;
27
- }
28
- if (["0", "false", "no", "off"].includes(raw)) {
29
- return false;
30
- }
31
- return fallback;
32
- }
33
-
34
19
  function normalizeRecord(value) {
35
20
  return value && typeof value === "object" && !Array.isArray(value) ? value : {};
36
21
  }
@@ -103,18 +88,6 @@ function resolveAuthProfileMode(appConfig = {}) {
103
88
  );
104
89
  }
105
90
 
106
- function isDevAuthBypassEnabledForRegistration(env) {
107
- if (!isDevAuthBypassRequested(env)) {
108
- return false;
109
- }
110
-
111
- return String(env?.NODE_ENV || "development").trim().toLowerCase() !== "production";
112
- }
113
-
114
- function isDevAuthBypassRequested(env) {
115
- return parseBoolean(env?.AUTH_DEV_BYPASS_ENABLED, false);
116
- }
117
-
118
91
  function createProviderIdentityProfileSyncService({ authProviderId = "supabase" } = {}) {
119
92
  return Object.freeze({
120
93
  async findByIdentity() {
@@ -201,17 +174,13 @@ function isDeferredJsonRestBootGap(app, error) {
201
174
  class AuthSupabaseServiceProvider {
202
175
  static id = "auth.provider.supabase";
203
176
 
204
- static dependsOn = ["runtime.actions"];
205
-
206
177
  register(app) {
207
178
  if (
208
179
  !app ||
209
180
  typeof app.singleton !== "function" ||
210
- typeof app.has !== "function" ||
211
- typeof app.actions !== "function" ||
212
- typeof app.service !== "function"
181
+ typeof app.has !== "function"
213
182
  ) {
214
- throw new Error("AuthSupabaseServiceProvider requires application singleton()/has()/actions()/service().");
183
+ throw new Error("AuthSupabaseServiceProvider requires application singleton()/has().");
215
184
  }
216
185
 
217
186
  assertSelectedAuthProvider(resolveRuntimeEnv(app));
@@ -227,7 +196,6 @@ class AuthSupabaseServiceProvider {
227
196
  const authProvider = resolveAuthProviderConfig(env, appConfig);
228
197
  const repositories = resolveOptionalRepositories(scope);
229
198
  const userSettingsRepository = repositories.userSettingsRepository || null;
230
- const devAuthBypassEnabled = parseBoolean(env.AUTH_DEV_BYPASS_ENABLED, false);
231
199
  const authProfileMode = resolveAuthProfileMode(appConfig);
232
200
  let userProfileSyncService = createProviderIdentityProfileSyncService({
233
201
  authProviderId: authProvider.id
@@ -258,7 +226,7 @@ class AuthSupabaseServiceProvider {
258
226
  userProfileSyncService,
259
227
  profileProjectionEnabled,
260
228
  userProfilesRepository: repositories.userProfilesRepository || null,
261
- devAuthBypassEnabled,
229
+ devAuthBypassEnabled: env.AUTH_DEV_BYPASS_ENABLED,
262
230
  devAuthBypassSecret: String(env.AUTH_DEV_BYPASS_SECRET || "").trim(),
263
231
  devAuthAccessTtlSeconds: env.AUTH_DEV_ACCESS_TTL_SECONDS,
264
232
  devAuthRefreshTtlSeconds: env.AUTH_DEV_REFRESH_TTL_SECONDS
@@ -266,17 +234,6 @@ class AuthSupabaseServiceProvider {
266
234
 
267
235
  return applyAuthServiceDecorators(scope, authService);
268
236
  });
269
-
270
- if (isDevAuthBypassEnabledForRegistration(resolveRuntimeEnv(app))) {
271
- app.actions(
272
- withActionDefaults([devLoginAsAction], {
273
- domain: "auth",
274
- dependencies: {
275
- authService: "authService"
276
- }
277
- })
278
- );
279
- }
280
237
  }
281
238
 
282
239
  boot(app) {
@@ -2,6 +2,8 @@ import assert from "node:assert/strict";
2
2
  import test from "node:test";
3
3
  import { createService } from "../src/server/lib/index.js";
4
4
 
5
+ const DEV_AUTH_SECRET = "dev-bootstrap-secret";
6
+
5
7
  function createProfile(overrides = {}) {
6
8
  return {
7
9
  id: "7",
@@ -64,6 +66,17 @@ function createLocalRequest(overrides = {}) {
64
66
  };
65
67
  }
66
68
 
69
+ function createDevAuthExchangeRequest(overrides = {}) {
70
+ const request = createLocalRequest(overrides);
71
+ return {
72
+ ...request,
73
+ headers: {
74
+ ...request.headers,
75
+ "x-jskit-dev-auth-secret": DEV_AUTH_SECRET
76
+ }
77
+ };
78
+ }
79
+
67
80
  function createServiceFixture(overrides = {}) {
68
81
  return createService({
69
82
  authProvider: {
@@ -75,7 +88,7 @@ function createServiceFixture(overrides = {}) {
75
88
  appPublicUrl: "http://localhost:5173",
76
89
  nodeEnv: "development",
77
90
  devAuthBypassEnabled: true,
78
- devAuthBypassSecret: "dev-bootstrap-secret",
91
+ devAuthBypassSecret: DEV_AUTH_SECRET,
79
92
  userProfilesRepository: createUserProfilesRepository(),
80
93
  userProfileSyncService: createUserProfileSyncService(),
81
94
  ...overrides
@@ -84,7 +97,7 @@ function createServiceFixture(overrides = {}) {
84
97
 
85
98
  test("dev auth bootstrap can issue and authenticate a local session without Supabase", async () => {
86
99
  const authService = createServiceFixture();
87
- const loginRequest = createLocalRequest();
100
+ const loginRequest = createDevAuthExchangeRequest();
88
101
 
89
102
  const loginResult = await authService.devLoginAs(loginRequest, {
90
103
  userId: "7"
@@ -111,7 +124,7 @@ test("dev auth bootstrap can issue and authenticate a local session without Supa
111
124
 
112
125
  test("dev auth bootstrap resolves security status without Supabase", async () => {
113
126
  const authService = createServiceFixture();
114
- const loginResult = await authService.devLoginAs(createLocalRequest(), {
127
+ const loginResult = await authService.devLoginAs(createDevAuthExchangeRequest(), {
115
128
  userId: "7"
116
129
  });
117
130
 
@@ -153,7 +166,7 @@ test("dev auth security status rejects invalid dev sessions without recovery-lin
153
166
  test("dev auth bootstrap supports email lookup", async () => {
154
167
  const authService = createServiceFixture();
155
168
 
156
- const result = await authService.devLoginAs(createLocalRequest(), {
169
+ const result = await authService.devLoginAs(createDevAuthExchangeRequest(), {
157
170
  email: "ADA@EXAMPLE.COM"
158
171
  });
159
172
 
@@ -161,6 +174,24 @@ test("dev auth bootstrap supports email lookup", async () => {
161
174
  assert.equal(result.profile.email, "ada@example.com");
162
175
  });
163
176
 
177
+ test("dev auth bootstrap rejects a missing or incorrect exchange secret", async () => {
178
+ const authService = createServiceFixture();
179
+
180
+ await assert.rejects(
181
+ () => authService.devLoginAs(createLocalRequest(), { userId: "7" }),
182
+ /not authorized/
183
+ );
184
+ await assert.rejects(
185
+ () => authService.devLoginAs(createLocalRequest({
186
+ headers: {
187
+ host: "localhost:3000",
188
+ "x-jskit-dev-auth-secret": "wrong-secret"
189
+ }
190
+ }), { userId: "7" }),
191
+ /not authorized/
192
+ );
193
+ });
194
+
164
195
  test("dev auth bootstrap canonicalizes producer profiles before returning them", async () => {
165
196
  const rawProfile = createProfile({
166
197
  email: " ADA@EXAMPLE.COM ",
@@ -177,7 +208,7 @@ test("dev auth bootstrap canonicalizes producer profiles before returning them",
177
208
  userProfilesRepository: createUserProfilesRepository(rawProfile)
178
209
  });
179
210
 
180
- const loginResult = await authService.devLoginAs(createLocalRequest(), {
211
+ const loginResult = await authService.devLoginAs(createDevAuthExchangeRequest(), {
181
212
  userId: "7"
182
213
  });
183
214
 
@@ -195,14 +226,14 @@ test("dev auth bootstrap canonicalizes producer profiles before returning them",
195
226
 
196
227
  test("dev auth bootstrap rejects non-local requests and clears leaked dev sessions", async () => {
197
228
  const authService = createServiceFixture();
198
- const issued = await authService.devLoginAs(createLocalRequest(), {
229
+ const issued = await authService.devLoginAs(createDevAuthExchangeRequest(), {
199
230
  userId: "7"
200
231
  });
201
232
 
202
233
  await assert.rejects(
203
234
  () =>
204
235
  authService.devLoginAs(
205
- createLocalRequest({
236
+ createDevAuthExchangeRequest({
206
237
  ip: "203.0.113.10",
207
238
  hostname: "example.com",
208
239
  headers: { host: "example.com" }
@@ -232,7 +263,7 @@ test("dev auth bootstrap does not trust forwarded localhost headers", async () =
232
263
  await assert.rejects(
233
264
  () =>
234
265
  authService.devLoginAs(
235
- createLocalRequest({
266
+ createDevAuthExchangeRequest({
236
267
  ip: "203.0.113.10",
237
268
  socket: {
238
269
  remoteAddress: "203.0.113.10"
@@ -37,7 +37,7 @@ function isBootFailureWithCause(error, pattern) {
37
37
  );
38
38
  }
39
39
 
40
- test("auth supabase provider defaults to provider profile mode and contributes auth actions", async () => {
40
+ test("auth supabase provider defaults to provider profile mode and composes with shared auth actions", async () => {
41
41
  const app = createApplication();
42
42
  app.instance("appConfig", createAppConfigFixture());
43
43
  app.instance("jskit.env", {
@@ -126,7 +126,7 @@ test("auth supabase provider defaults to provider profile mode and contributes a
126
126
  assert.equal(Array.isArray(definitions), true);
127
127
  assert.equal(definitions.some((definition) => definition.id === "auth.login.password"), true);
128
128
  assert.equal(definitions.some((definition) => definition.id === "auth.register.confirmation.resend"), true);
129
- assert.equal(definitions.some((definition) => definition.id === "auth.dev.loginAs"), false);
129
+ assert.equal(definitions.some((definition) => definition.id === "auth.dev.loginAs"), true);
130
130
  const sessionRead = definitions.find((definition) => definition.id === "auth.session.read");
131
131
  assert.deepEqual(sessionRead?.surfaces, ["home", "console"]);
132
132
  });
@@ -204,7 +204,12 @@ test("auth supabase provider reports password method toggle only with persistent
204
204
  });
205
205
 
206
206
  await app.start({
207
- providers: [ActionRuntimeServiceProvider, AuthSupabaseServiceProvider]
207
+ providers: [
208
+ ActionRuntimeServiceProvider,
209
+ AuthSupabaseServiceProvider,
210
+ AuthProviderServiceProvider,
211
+ AuthActionsServiceProvider
212
+ ]
208
213
  });
209
214
 
210
215
  const authService = app.make("authService");
@@ -348,7 +353,12 @@ test("auth supabase provider can boot dev auth without Supabase credentials", as
348
353
  });
349
354
 
350
355
  await app.start({
351
- providers: [ActionRuntimeServiceProvider, AuthSupabaseServiceProvider]
356
+ providers: [
357
+ ActionRuntimeServiceProvider,
358
+ AuthSupabaseServiceProvider,
359
+ AuthProviderServiceProvider,
360
+ AuthActionsServiceProvider
361
+ ]
352
362
  });
353
363
 
354
364
  const authService = app.make("authService");
@@ -1,21 +0,0 @@
1
- import { authDevLoginAsCommand } from "@jskit-ai/auth-core/shared/commands";
2
- import { requireRequestContext } from "@jskit-ai/auth-core/server/actions/auth.contributor";
3
-
4
- const devLoginAsAction = Object.freeze({
5
- id: "auth.dev.loginAs",
6
- version: 1,
7
- kind: "command",
8
- channels: ["api", "internal"],
9
- surfacesFrom: "enabled",
10
- input: authDevLoginAsCommand.operation.body,
11
- idempotency: "none",
12
- audit: {
13
- actionName: "auth.dev.loginAs"
14
- },
15
- observability: {},
16
- async execute(input, context, deps) {
17
- return deps.authService.devLoginAs(requireRequestContext(context, "auth.dev.loginAs"), input);
18
- }
19
- });
20
-
21
- export { devLoginAsAction };