@jskit-ai/auth-provider-local-core 0.1.60 → 0.1.61

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.
package/package.json CHANGED
@@ -1,18 +1,17 @@
1
1
  {
2
2
  "name": "@jskit-ai/auth-provider-local-core",
3
- "version": "0.1.60",
3
+ "version": "0.1.61",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "test": "node --test"
7
7
  },
8
8
  "exports": {
9
- "./server/providers/AuthLocalServiceProvider": "./src/server/providers/AuthLocalServiceProvider.js",
10
- "./server/providers/AuthProviderServiceProvider": "./src/server/providers/AuthProviderServiceProvider.js",
9
+ "./server/providers/AuthLocalProvider": "./src/server/providers/AuthLocalProvider.js",
11
10
  "./server/lib/index": "./src/server/lib/index.js"
12
11
  },
13
12
  "dependencies": {
14
- "@jskit-ai/auth-core": "0.1.157",
15
- "@jskit-ai/kernel": "0.1.159",
13
+ "@jskit-ai/auth-core": "0.1.158",
14
+ "@jskit-ai/kernel": "0.1.160",
16
15
  "nodemailer": "^9.0.3"
17
16
  },
18
17
  "description": "Local auth provider with a file backend default and no database requirement.",
@@ -20,24 +19,22 @@
20
19
  "kind": "runtime",
21
20
  "capabilities": {
22
21
  "provides": [
23
- "auth.provider.local",
24
- "auth.provider"
22
+ "auth.service"
25
23
  ],
26
24
  "requires": [
27
- "auth.access"
25
+ "auth.extensions",
26
+ "runtime.app-root",
27
+ "runtime.env",
28
+ "runtime.logger"
28
29
  ]
29
30
  },
30
31
  "runtime": {
31
32
  "server": {
32
- "providerEntrypoint": "src/server/providers/AuthLocalServiceProvider.js",
33
+ "providerEntrypoint": "src/server/providers/AuthLocalProvider.js",
33
34
  "providers": [
34
35
  {
35
- "entrypoint": "src/server/providers/AuthLocalServiceProvider.js",
36
- "export": "AuthLocalServiceProvider"
37
- },
38
- {
39
- "entrypoint": "src/server/providers/AuthProviderServiceProvider.js",
40
- "export": "AuthProviderServiceProvider"
36
+ "entrypoint": "src/server/providers/AuthLocalProvider.js",
37
+ "export": "AuthLocalProvider"
41
38
  }
42
39
  ]
43
40
  }
@@ -46,76 +43,21 @@
46
43
  "apiSummary": {
47
44
  "surfaces": [
48
45
  {
49
- "subpath": "./server/providers/AuthLocalServiceProvider",
50
- "summary": "Exports the local auth provider service provider."
46
+ "subpath": "./server/providers/AuthLocalProvider",
47
+ "summary": "Provides the selected local authentication service through explicit runtime capabilities."
51
48
  },
52
49
  {
53
50
  "subpath": "./server/lib/index",
54
51
  "summary": "Exports local auth service, file backend helpers, password strategy helpers, and local auth register hook decorators."
55
52
  }
56
53
  ],
57
- "containerTokens": {
58
- "server": [
59
- "authService",
60
- "auth.local.backend",
61
- "auth.local.passwordStrategy"
62
- ],
63
- "client": []
64
- }
54
+ "extensionCapabilities": [
55
+ "auth.invitation-context",
56
+ "auth.local.backend",
57
+ "auth.local.password-strategy",
58
+ "auth.profile.projector"
59
+ ]
65
60
  }
66
- },
67
- "ci": {
68
- "environment": {
69
- "AUTH_PROVIDER": "local"
70
- },
71
- "services": [],
72
- "steps": []
73
- },
74
- "mutations": {
75
- "dependencies": {
76
- "runtime": {
77
- "@jskit-ai/auth-core": "0.1.157",
78
- "@jskit-ai/kernel": "0.1.159",
79
- "nodemailer": "^9.0.3",
80
- "dotenv": "^16.4.5"
81
- },
82
- "dev": {}
83
- },
84
- "packageJson": {
85
- "scripts": {}
86
- },
87
- "procfile": {},
88
- "files": [],
89
- "text": [
90
- {
91
- "file": ".env",
92
- "op": "upsert-env",
93
- "key": "AUTH_PROVIDER",
94
- "value": "local",
95
- "reason": "Select local auth as the auth provider.",
96
- "category": "runtime-config",
97
- "id": "auth-provider"
98
- },
99
- {
100
- "file": ".env",
101
- "op": "upsert-env",
102
- "key": "AUTH_LOCAL_BACKEND",
103
- "value": "file",
104
- "reason": "Use the built-in local file auth backend.",
105
- "category": "runtime-config",
106
- "id": "auth-local-backend"
107
- },
108
- {
109
- "file": ".gitignore",
110
- "op": "append-text",
111
- "position": "bottom",
112
- "skipIfContains": ".jskit/auth/",
113
- "value": "\n.jskit/auth/\n",
114
- "reason": "Ignore local auth runtime state.",
115
- "category": "runtime-state",
116
- "id": "auth-local-store-gitignore"
117
- }
118
- ]
119
61
  }
120
62
  }
121
63
  }
@@ -0,0 +1,129 @@
1
+ import { parseBooleanFlag } from "@jskit-ai/auth-core/server/booleanFlag";
2
+ import { assertDevAuthPolicy, resolveDevAuthPolicyFromEnv } from "@jskit-ai/auth-core/server/devAuth";
3
+ import {
4
+ attachDeferredAuthService,
5
+ createDeferredAuthService
6
+ } from "@jskit-ai/auth-core/server/deferredAuthService";
7
+ import { defineProvider } from "@jskit-ai/kernel/shared/capabilities";
8
+ import { randomBytes } from "node:crypto";
9
+ import fs from "node:fs";
10
+ import path from "node:path";
11
+ import { createLocalFileBackend } from "../lib/fileBackend.js";
12
+ import { createLocalAuthService } from "../lib/service.js";
13
+
14
+ const DEFAULT_STORE_DIR = ".jskit/auth";
15
+ function resolveStoreDir(env, appRoot) {
16
+ const configured = String(env.AUTH_LOCAL_STORE_DIR || DEFAULT_STORE_DIR).trim() || DEFAULT_STORE_DIR;
17
+ return path.resolve(appRoot, configured);
18
+ }
19
+
20
+ function resolveSessionSecret(env, { storeDir, isProduction }) {
21
+ const explicit = String(env.AUTH_LOCAL_SESSION_SECRET || "").trim();
22
+ if (explicit) return explicit;
23
+ if (isProduction) throw new Error("AUTH_LOCAL_SESSION_SECRET is required for local auth in production.");
24
+
25
+ const secretPath = path.join(storeDir, "session.secret");
26
+ try {
27
+ const existing = fs.readFileSync(secretPath, "utf8").trim();
28
+ if (existing) return existing;
29
+ } catch (error) {
30
+ if (!error || error.code !== "ENOENT") throw error;
31
+ }
32
+ fs.mkdirSync(storeDir, { recursive: true, mode: 0o700 });
33
+ const generated = randomBytes(32).toString("base64url");
34
+ fs.writeFileSync(secretPath, `${generated}\n`, { mode: 0o600 });
35
+ return generated;
36
+ }
37
+
38
+ function resolveSmtpConfig(env) {
39
+ const host = String(env.AUTH_LOCAL_SMTP_HOST || "").trim();
40
+ if (!host) return { smtpConfigured: false, smtp: null };
41
+ const from = String(env.AUTH_LOCAL_SMTP_FROM || "").trim();
42
+ if (!from) throw new Error("AUTH_LOCAL_SMTP_FROM is required when AUTH_LOCAL_SMTP_HOST is set.");
43
+ return {
44
+ smtpConfigured: true,
45
+ smtp: {
46
+ host,
47
+ port: Number(env.AUTH_LOCAL_SMTP_PORT || 587),
48
+ secure: parseBooleanFlag(env.AUTH_LOCAL_SMTP_SECURE, false),
49
+ user: String(env.AUTH_LOCAL_SMTP_USER || "").trim(),
50
+ password: String(env.AUTH_LOCAL_SMTP_PASSWORD || ""),
51
+ from,
52
+ replyTo: String(env.AUTH_LOCAL_SMTP_REPLY_TO || "").trim()
53
+ }
54
+ };
55
+ }
56
+
57
+ function resolveAppPublicUrl(env, { smtpConfigured }) {
58
+ const configured = String(env.APP_PUBLIC_URL || "").trim();
59
+ if (!configured) {
60
+ if (smtpConfigured) throw new Error("APP_PUBLIC_URL is required when local auth SMTP recovery is configured.");
61
+ return "http://localhost:5173";
62
+ }
63
+ let parsed;
64
+ try {
65
+ parsed = new URL(configured);
66
+ } catch {
67
+ throw new Error("APP_PUBLIC_URL must be a valid URL when local auth SMTP recovery is configured.");
68
+ }
69
+ if (!["http:", "https:"].includes(parsed.protocol)) {
70
+ throw new Error("APP_PUBLIC_URL must use http or https when local auth SMTP recovery is configured.");
71
+ }
72
+ return parsed.toString().replace(/\/$/u, "");
73
+ }
74
+
75
+ function resolveConfig({ appRoot, backend, env, logger }) {
76
+ const nodeEnv = String(env.NODE_ENV || "development").trim() || "development";
77
+ const isProduction = nodeEnv === "production";
78
+ const storeDir = resolveStoreDir(env, appRoot);
79
+ if (!backend && isProduction && !parseBooleanFlag(env.AUTH_LOCAL_FILE_PRODUCTION_ACK, false)) {
80
+ throw new Error("AUTH_LOCAL_FILE_PRODUCTION_ACK is required to use the local file auth backend in production.");
81
+ }
82
+ const smtp = resolveSmtpConfig(env);
83
+ const devAuth = resolveDevAuthPolicyFromEnv(env);
84
+ assertDevAuthPolicy(devAuth);
85
+ return {
86
+ storeDir,
87
+ nodeEnv,
88
+ logger,
89
+ appPublicUrl: resolveAppPublicUrl(env, smtp),
90
+ sessionSecret: resolveSessionSecret(env, { storeDir, isProduction }),
91
+ recoveryDevOutput: String(env.AUTH_LOCAL_RECOVERY_DEV_OUTPUT || "log").trim().toLowerCase() || "log",
92
+ devAuth,
93
+ ...smtp
94
+ };
95
+ }
96
+
97
+ const AuthLocalProvider = defineProvider({
98
+ id: "auth.local",
99
+ requires: {
100
+ appRoot: "runtime.app-root",
101
+ env: "runtime.env",
102
+ extensions: "auth.extensions",
103
+ logger: "runtime.logger"
104
+ },
105
+ optional: {
106
+ backend: "auth.local.backend",
107
+ passwordStrategy: "auth.local.password-strategy"
108
+ },
109
+ provides: {
110
+ authService: "auth.service"
111
+ },
112
+ setup() {
113
+ return { authService: createDeferredAuthService() };
114
+ },
115
+ boot(dependencies, { outputs }) {
116
+ const { appRoot, backend, env, extensions, logger, passwordStrategy } = dependencies;
117
+ const config = resolveConfig({ appRoot, backend, env, logger });
118
+ const baseService = createLocalAuthService({
119
+ backend: backend || createLocalFileBackend({ storeDir: config.storeDir }),
120
+ config,
121
+ profileProjector: extensions.resolveProfileProjector(),
122
+ passwordStrategy,
123
+ invitationContextResolver: extensions.resolveInvitationContextResolver()
124
+ });
125
+ attachDeferredAuthService(outputs.authService, extensions.decorateService(baseService));
126
+ }
127
+ });
128
+
129
+ export { AuthLocalProvider, resolveConfig };
@@ -1,25 +1,14 @@
1
1
  import assert from "node:assert/strict";
2
- import { readFile } from "node:fs/promises";
3
2
  import test from "node:test";
4
3
  import packageJson from "../package.json" with { type: "json" };
5
4
 
6
5
  const packageMetadata = packageJson.jskit;
7
6
 
8
- test("local auth contributes only its provider selection to CI", () => {
9
- assert.deepEqual(packageMetadata.ci, {
10
- environment: {
11
- AUTH_PROVIDER: "local"
12
- },
13
- services: [],
14
- steps: []
15
- });
7
+ test("local auth has no package mutation or CI recipe", () => {
8
+ assert.equal(Object.hasOwn(packageMetadata, "ci"), false);
9
+ assert.equal(Object.hasOwn(packageMetadata, "mutations"), false);
16
10
  });
17
11
 
18
- test("local auth scaffolds the Nodemailer version used by the package", async () => {
19
- const packageJson = JSON.parse(await readFile(new URL("../package.json", import.meta.url), "utf8"));
20
-
21
- assert.equal(
22
- packageMetadata.mutations.dependencies.runtime.nodemailer,
23
- packageJson.dependencies.nodemailer
24
- );
12
+ test("local auth declares Nodemailer as its own runtime dependency", () => {
13
+ assert.equal(typeof packageJson.dependencies.nodemailer, "string");
25
14
  });
@@ -3,13 +3,12 @@ import fs from "node:fs/promises";
3
3
  import os from "node:os";
4
4
  import path from "node:path";
5
5
  import test from "node:test";
6
- import { createApplication } from "@jskit-ai/kernel/_testable";
7
- import { ActionRuntimeServiceProvider } from "@jskit-ai/kernel/server/actions";
8
- import { registerAuthServiceDecorator } from "@jskit-ai/auth-core/server/authServiceDecoratorRegistry";
6
+ import { createActionProvider } from "@jskit-ai/kernel/server/actions";
7
+ import { createCapabilityRuntime, defineProvider } from "@jskit-ai/kernel/shared/capabilities";
8
+ import { createAuthExtensions } from "@jskit-ai/auth-core/server/authExtensions";
9
9
  import { DEV_AUTH_SECRET_HEADER } from "@jskit-ai/auth-core/server/devAuth";
10
- import { AuthActionsServiceProvider } from "@jskit-ai/auth-core/server/providers/AuthActionsServiceProvider";
11
- import { AuthLocalServiceProvider } from "../src/server/providers/AuthLocalServiceProvider.js";
12
- import { AuthProviderServiceProvider } from "../src/server/providers/AuthProviderServiceProvider.js";
10
+ import { AuthFeature } from "@jskit-ai/auth-core/server/providers/AuthFeature";
11
+ import { AuthLocalProvider } from "../src/server/providers/AuthLocalProvider.js";
13
12
  import {
14
13
  createLocalAuthRegisterHookDecorator,
15
14
  createLocalAuthService,
@@ -21,23 +20,6 @@ import {
21
20
 
22
21
  const DEV_AUTH_SECRET = "local-preview-exchange-secret";
23
22
 
24
- function createAppConfigFixture() {
25
- return {
26
- surfaceModeAll: "all",
27
- surfaceDefaultId: "home",
28
- surfaceDefinitions: {
29
- home: { id: "home", pagesRoot: "", enabled: true, requiresAuth: false, requiresWorkspace: false },
30
- console: {
31
- id: "console",
32
- pagesRoot: "console",
33
- enabled: true,
34
- requiresAuth: true,
35
- requiresWorkspace: false
36
- }
37
- }
38
- };
39
- }
40
-
41
23
  function createReplyFixture() {
42
24
  const cookies = {};
43
25
  const cookieOptions = {};
@@ -94,67 +76,82 @@ function devAuthEnv() {
94
76
  async function createStartedApp({
95
77
  env = {},
96
78
  profileProjector = null,
97
- profileProjectorFactory = null,
98
79
  passwordStrategy = null,
99
80
  invitationContextResolver = null,
100
81
  logger = null,
101
- configureApp = null
82
+ configureExtensions = null
102
83
  } = {}) {
103
84
  const storeDir = await fs.mkdtemp(path.join(os.tmpdir(), "jskit-auth-local-"));
104
- const app = createApplication();
105
- app.instance("appConfig", createAppConfigFixture());
106
- app.instance("jskit.env", {
107
- AUTH_PROVIDER: "local",
85
+ const backend = createLocalFileBackend({ storeDir });
86
+ const extensions = createAuthExtensions();
87
+ const runtimeLogger = logger || {
88
+ info() {},
89
+ warn() {},
90
+ error() {},
91
+ debug() {}
92
+ };
93
+ if (typeof configureExtensions === "function") configureExtensions(extensions, runtimeLogger);
94
+ if (profileProjector) {
95
+ extensions.registerProfileProjector({
96
+ projectorId: "test.profile",
97
+ ...profileProjector
98
+ });
99
+ }
100
+ if (invitationContextResolver) {
101
+ extensions.registerInvitationContextResolver({
102
+ resolverId: "test.invitation",
103
+ ...invitationContextResolver
104
+ });
105
+ }
106
+ let authService = null;
107
+ let actionExecutor = null;
108
+ const probe = defineProvider({
109
+ id: "test.auth.probe",
110
+ requires: {
111
+ actions: "runtime.actions",
112
+ service: "auth.service"
113
+ },
114
+ setup({ actions, service }) {
115
+ actionExecutor = actions;
116
+ authService = service;
117
+ return {};
118
+ }
119
+ });
120
+ const inputs = {
121
+ "runtime.app-root": storeDir,
122
+ "runtime.env": {
108
123
  AUTH_LOCAL_STORE_DIR: storeDir,
109
124
  AUTH_LOCAL_RECOVERY_DEV_OUTPUT: "response",
110
125
  APP_PUBLIC_URL: "http://localhost:5173",
111
126
  NODE_ENV: "test",
112
127
  ...env
113
- });
114
- app.instance(
115
- "jskit.logger",
116
- logger || {
117
- info() {},
118
- warn() {},
119
- error() {},
120
- debug() {}
121
- }
122
- );
123
- app.instance("domainEvents", {
124
- async publish() {}
125
- });
126
- if (profileProjector) {
127
- app.instance("auth.profile.projector", profileProjector);
128
- }
129
- if (profileProjectorFactory) {
130
- app.singleton("auth.profile.projector", profileProjectorFactory);
131
- }
132
- if (passwordStrategy) {
133
- app.instance("auth.local.passwordStrategy", passwordStrategy);
134
- }
135
- if (invitationContextResolver) {
136
- app.instance("auth.invitationContextResolver", invitationContextResolver);
137
- }
138
- if (typeof configureApp === "function") {
139
- configureApp(app);
140
- }
141
- await app.start({
128
+ },
129
+ "runtime.logger": runtimeLogger,
130
+ "auth.extensions": extensions,
131
+ "auth.local.backend": backend,
132
+ ...(passwordStrategy ? { "auth.local.password-strategy": passwordStrategy } : {}),
133
+ };
134
+ const runtime = createCapabilityRuntime({
135
+ inputs,
142
136
  providers: [
143
- ActionRuntimeServiceProvider,
144
- AuthLocalServiceProvider,
145
- AuthProviderServiceProvider,
146
- AuthActionsServiceProvider
137
+ createActionProvider(),
138
+ AuthLocalProvider,
139
+ AuthFeature,
140
+ probe
147
141
  ]
148
142
  });
143
+ await runtime.start();
149
144
  return {
150
- app,
145
+ actionExecutor,
146
+ authService,
147
+ backend,
148
+ runtime,
151
149
  storeDir
152
150
  };
153
151
  }
154
152
 
155
153
  test("local auth provider registers, logs in, reads session, and logs out with file backend", async () => {
156
- const { app, storeDir } = await createStartedApp();
157
- const authService = app.make("authService");
154
+ const { actionExecutor, authService, storeDir } = await createStartedApp();
158
155
  const capabilities = authService.getCapabilities();
159
156
  assert.equal(capabilities.provider.id, "local");
160
157
  assert.equal(capabilities.features.password.login, true);
@@ -178,7 +175,6 @@ test("local auth provider registers, logs in, reads session, and logs out with f
178
175
  assert.equal(session.authenticated, true);
179
176
  assert.equal(session.actor.email, "ada@example.com");
180
177
 
181
- const actionExecutor = app.make("actionExecutor");
182
178
  const definitions = actionExecutor.listDefinitions();
183
179
  assert.equal(definitions.some((definition) => definition.id === "auth.login.password"), true);
184
180
  const actionSession = await actionExecutor.execute({
@@ -204,10 +200,9 @@ test("local auth provider registers, logs in, reads session, and logs out with f
204
200
  });
205
201
 
206
202
  test("local file auth login-as issues native cookies for an existing user", async () => {
207
- const { app } = await createStartedApp({
203
+ const { authService } = await createStartedApp({
208
204
  env: devAuthEnv()
209
205
  });
210
- const authService = app.make("authService");
211
206
  await authService.register({
212
207
  displayName: "Ada",
213
208
  email: "ada@example.com",
@@ -244,7 +239,7 @@ test("local login-as never invokes the mutating profile projector", async () =>
244
239
  };
245
240
  let findCalls = 0;
246
241
  let syncCalls = 0;
247
- const { app } = await createStartedApp({
242
+ const { authService } = await createStartedApp({
248
243
  env: devAuthEnv(),
249
244
  profileProjector: {
250
245
  async findByIdentity() {
@@ -260,7 +255,6 @@ test("local login-as never invokes the mutating profile projector", async () =>
260
255
  }
261
256
  }
262
257
  });
263
- const authService = app.make("authService");
264
258
  const registered = await authService.register({
265
259
  displayName: "Ada",
266
260
  email: "ada@example.com",
@@ -286,7 +280,7 @@ test("local login-as never invokes the mutating profile projector", async () =>
286
280
  });
287
281
 
288
282
  test("local login-as rejects missing, disabled, and unprojected users", async () => {
289
- const { app } = await createStartedApp({
283
+ const { authService, backend } = await createStartedApp({
290
284
  env: devAuthEnv(),
291
285
  profileProjector: {
292
286
  async findByIdentity() {
@@ -297,7 +291,6 @@ test("local login-as rejects missing, disabled, and unprojected users", async ()
297
291
  }
298
292
  }
299
293
  });
300
- const backend = app.make("auth.local.backend");
301
294
  const password = await hashPassword("stored password value");
302
295
  await backend.withTransaction(async (tx) => {
303
296
  await tx.users.create({
@@ -315,7 +308,6 @@ test("local login-as rejects missing, disabled, and unprojected users", async ()
315
308
  password
316
309
  });
317
310
  });
318
- const authService = app.make("authService");
319
311
 
320
312
  for (const email of [
321
313
  "missing@example.com",
@@ -330,10 +322,9 @@ test("local login-as rejects missing, disabled, and unprojected users", async ()
330
322
  });
331
323
 
332
324
  test("local login-as rejects missing, incorrect, and non-loopback exchange authorization", async () => {
333
- const { app } = await createStartedApp({
325
+ const { authService } = await createStartedApp({
334
326
  env: devAuthEnv()
335
327
  });
336
- const authService = app.make("authService");
337
328
  await authService.register({
338
329
  displayName: "Ada",
339
330
  email: "ada@example.com",
@@ -379,7 +370,7 @@ test("local auth provider rejects dev login-as enablement in production", async
379
370
  test("local auth provider applies auth service decorators for blocking and non-blocking hooks", async () => {
380
371
  const calls = [];
381
372
  const errors = [];
382
- const { app } = await createStartedApp({
373
+ const { authService } = await createStartedApp({
383
374
  logger: {
384
375
  info() {},
385
376
  warn(payload, message) {
@@ -388,12 +379,11 @@ test("local auth provider applies auth service decorators for blocking and non-b
388
379
  error() {},
389
380
  debug() {}
390
381
  },
391
- configureApp(app) {
392
- registerAuthServiceDecorator(app, "test.auth.local.nonBlockingHook", (scope) => ({
393
- ...createLocalAuthRegisterHookDecorator({
382
+ configureExtensions(extensions, runtimeLogger) {
383
+ extensions.registerServiceDecorator(createLocalAuthRegisterHookDecorator({
394
384
  decoratorId: "test.auth.local.nonBlockingHook",
395
385
  order: 20,
396
- logger: scope.make("jskit.logger"),
386
+ logger: runtimeLogger,
397
387
  hook: {
398
388
  hookId: "audit",
399
389
  blocking: false,
@@ -402,10 +392,8 @@ test("local auth provider applies auth service decorators for blocking and non-b
402
392
  throw new Error("audit unavailable");
403
393
  }
404
394
  }
405
- })
406
- }));
407
- registerAuthServiceDecorator(app, "test.auth.local.blockingHook", () =>
408
- createLocalAuthRegisterHookDecorator({
395
+ }));
396
+ extensions.registerServiceDecorator(createLocalAuthRegisterHookDecorator({
409
397
  decoratorId: "test.auth.local.blockingHook",
410
398
  order: 10,
411
399
  hook: {
@@ -418,11 +406,9 @@ test("local auth provider applies auth service decorators for blocking and non-b
418
406
  }
419
407
  }
420
408
  }
421
- })
422
- );
409
+ }));
423
410
  }
424
411
  });
425
- const authService = app.make("authService");
426
412
 
427
413
  const registered = await authService.register({
428
414
  email: "hooks@example.com",
@@ -472,7 +458,7 @@ test("local auth register hook decorators require explicit blocking mode", () =>
472
458
  );
473
459
  });
474
460
 
475
- test("local auth provider resolves a custom password strategy from the container", async () => {
461
+ test("local auth provider accepts a custom password strategy capability", async () => {
476
462
  const passwordStrategy = {
477
463
  prefix: "strategy",
478
464
  async hashPassword(password) {
@@ -487,8 +473,7 @@ test("local auth provider resolves a custom password strategy from the container
487
473
  return record?.algorithm === "test-password" && record?.hash === `${this.prefix}-${password}`;
488
474
  }
489
475
  };
490
- const { app } = await createStartedApp({ passwordStrategy });
491
- const authService = app.make("authService");
476
+ const { authService } = await createStartedApp({ passwordStrategy });
492
477
 
493
478
  await authService.register({
494
479
  email: "strategy@example.com",
@@ -751,8 +736,7 @@ test("local auth password strategy handles register, reset, login, and change pa
751
736
  );
752
737
  }
753
738
  };
754
- const { app } = await createStartedApp({ passwordStrategy });
755
- const authService = app.make("authService");
739
+ const { authService } = await createStartedApp({ passwordStrategy });
756
740
 
757
741
  await authService.register({
758
742
  email: "strategy-paths@example.com",
@@ -817,8 +801,7 @@ test("local auth password strategy handles register, reset, login, and change pa
817
801
  });
818
802
 
819
803
  test("local auth provider completes recovery through a recovery-scoped session", async () => {
820
- const { app } = await createStartedApp();
821
- const authService = app.make("authService");
804
+ const { authService } = await createStartedApp();
822
805
  await authService.register({
823
806
  email: "Recovery@example.com",
824
807
  password: "correct horse battery staple",
@@ -890,8 +873,7 @@ test("local auth provider completes recovery through a recovery-scoped session",
890
873
  });
891
874
 
892
875
  test("local auth reset invalidates other outstanding recovery tokens for the user", async () => {
893
- const { app } = await createStartedApp();
894
- const authService = app.make("authService");
876
+ const { authService } = await createStartedApp();
895
877
  await authService.register({
896
878
  email: "recovery-tokens@example.com",
897
879
  password: "correct horse battery staple",
@@ -933,8 +915,7 @@ test("local auth reset invalidates other outstanding recovery tokens for the use
933
915
  });
934
916
 
935
917
  test("local auth provider changes password through the account-security contract", async () => {
936
- const { app } = await createStartedApp();
937
- const authService = app.make("authService");
918
+ const { authService } = await createStartedApp();
938
919
  const registered = await authService.register({
939
920
  email: "Security@example.com",
940
921
  password: "correct horse battery staple",
@@ -982,36 +963,9 @@ test("local auth provider changes password through the account-security contract
982
963
  assert.equal(login.actor.email, "security@example.com");
983
964
  });
984
965
 
985
- test("local auth provider defers profile projector resolution until projection is needed", async () => {
986
- let projectorFactoryCalls = 0;
987
- const { app } = await createStartedApp({
988
- profileProjectorFactory() {
989
- projectorFactoryCalls += 1;
990
- return {
991
- async syncIdentityProfile(profile) {
992
- return {
993
- ...profile,
994
- id: "projected-user",
995
- profileSource: "users"
996
- };
997
- }
998
- };
999
- }
1000
- });
1001
- assert.equal(projectorFactoryCalls, 0);
1002
- const authService = app.make("authService");
1003
- assert.equal(projectorFactoryCalls, 0);
1004
- const registered = await authService.register({
1005
- email: "projected@example.com",
1006
- password: "correct horse battery staple",
1007
- displayName: "Projected User"
1008
- });
1009
- assert.equal(projectorFactoryCalls, 1);
1010
- assert.equal(registered.actor.appUserId, "projected-user");
1011
- });
1012
966
  test("local auth provider projects app profile when auth.profile.projector is installed", async () => {
1013
967
  const projectedProfiles = [];
1014
- const { app } = await createStartedApp({
968
+ const { authService } = await createStartedApp({
1015
969
  profileProjector: {
1016
970
  async syncIdentityProfile(profile) {
1017
971
  projectedProfiles.push(profile);
@@ -1023,7 +977,6 @@ test("local auth provider projects app profile when auth.profile.projector is in
1023
977
  }
1024
978
  }
1025
979
  });
1026
- const authService = app.make("authService");
1027
980
  assert.equal(authService.getCapabilities().features.appProfileProjection, true);
1028
981
 
1029
982
  const registered = await authService.register({
@@ -1041,7 +994,7 @@ test("local auth provider projects app profile when auth.profile.projector is in
1041
994
  test("local auth provider passes resolved invitation context into profile projection during registration", async () => {
1042
995
  const resolverCalls = [];
1043
996
  const projectionCalls = [];
1044
- const { app } = await createStartedApp({
997
+ const { authService } = await createStartedApp({
1045
998
  invitationContextResolver: {
1046
999
  async resolveInvitationContext(invitation) {
1047
1000
  resolverCalls.push(invitation);
@@ -1067,7 +1020,6 @@ test("local auth provider passes resolved invitation context into profile projec
1067
1020
  }
1068
1021
  }
1069
1022
  });
1070
- const authService = app.make("authService");
1071
1023
 
1072
1024
  const registered = await authService.register({
1073
1025
  email: "Invitee@Example.com",
@@ -1103,44 +1055,16 @@ test("local auth provider passes resolved invitation context into profile projec
1103
1055
  assert.equal(registered.actor.appUserId, "invited-app-user");
1104
1056
  });
1105
1057
 
1106
- test("local auth provider rejects AUTH_PROVIDER mismatches", async () => {
1107
- const app = createApplication();
1108
- app.instance("appConfig", createAppConfigFixture());
1109
- app.instance("jskit.env", {
1110
- AUTH_PROVIDER: "supabase",
1111
- NODE_ENV: "test"
1112
- });
1113
-
1114
- await assert.rejects(
1115
- () =>
1116
- app.start({
1117
- providers: [ActionRuntimeServiceProvider, AuthLocalServiceProvider]
1118
- }),
1119
- (error) => /AUTH_PROVIDER is "supabase"/.test(String(error.details?.cause?.message || error.message || ""))
1120
- );
1121
- });
1122
-
1123
1058
  test("local auth provider requires an explicit public URL for SMTP recovery", async () => {
1124
- const storeDir = await fs.mkdtemp(path.join(os.tmpdir(), "jskit-auth-local-"));
1125
- const app = createApplication();
1126
- app.instance("appConfig", createAppConfigFixture());
1127
- app.instance("jskit.env", {
1128
- AUTH_PROVIDER: "local",
1129
- AUTH_LOCAL_STORE_DIR: storeDir,
1130
- AUTH_LOCAL_SMTP_HOST: "smtp.example.com",
1131
- AUTH_LOCAL_SMTP_FROM: "support@example.com",
1132
- NODE_ENV: "test"
1133
- });
1134
-
1135
1059
  await assert.rejects(
1136
- () =>
1137
- app.start({
1138
- providers: [ActionRuntimeServiceProvider, AuthLocalServiceProvider]
1139
- }),
1140
- (error) =>
1141
- /APP_PUBLIC_URL is required when local auth SMTP recovery is configured/.test(
1142
- String(error.details?.cause?.message || error.message || "")
1143
- )
1060
+ () => createStartedApp({
1061
+ env: {
1062
+ APP_PUBLIC_URL: "",
1063
+ AUTH_LOCAL_SMTP_HOST: "smtp.example.com",
1064
+ AUTH_LOCAL_SMTP_FROM: "support@example.com"
1065
+ }
1066
+ }),
1067
+ /APP_PUBLIC_URL is required when local auth SMTP recovery is configured/
1144
1068
  );
1145
1069
  });
1146
1070
 
@@ -1,233 +0,0 @@
1
- import { applyAuthServiceDecorators } from "@jskit-ai/auth-core/server/authServiceDecoratorRegistry";
2
- import { parseBooleanFlag } from "@jskit-ai/auth-core/server/booleanFlag";
3
- import {
4
- assertDevAuthPolicy,
5
- resolveDevAuthPolicyFromEnv
6
- } from "@jskit-ai/auth-core/server/devAuth";
7
- import fs from "node:fs";
8
- import path from "node:path";
9
- import { randomBytes } from "node:crypto";
10
- import { createLocalAuthService } from "../lib/service.js";
11
- import { createLocalFileBackend } from "../lib/fileBackend.js";
12
-
13
- const DEFAULT_STORE_DIR = ".jskit/auth";
14
-
15
- function resolveLocalBackendMode(scope) {
16
- const env = resolveRuntimeEnv(scope);
17
- return String(env.AUTH_LOCAL_BACKEND || "file").trim().toLowerCase() || "file";
18
- }
19
-
20
- function resolveRuntimeEnv(scope) {
21
- const env = scope && typeof scope.has === "function" && scope.has("jskit.env") ? scope.make("jskit.env") : {};
22
- return {
23
- ...process.env,
24
- ...(env && typeof env === "object" ? env : {})
25
- };
26
- }
27
-
28
- function assertSelectedAuthProvider(env) {
29
- const selectedProvider = String(env?.AUTH_PROVIDER || "").trim().toLowerCase();
30
- if (selectedProvider && selectedProvider !== "local") {
31
- throw new Error(
32
- `AUTH_PROVIDER is "${selectedProvider}", but @jskit-ai/auth-provider-local-core is installed as the selected auth provider.`
33
- );
34
- }
35
- }
36
-
37
- function resolveStoreDir(env) {
38
- return path.resolve(String(env.AUTH_LOCAL_STORE_DIR || DEFAULT_STORE_DIR).trim() || DEFAULT_STORE_DIR);
39
- }
40
-
41
- function resolveSessionSecret(env, { storeDir, isProduction }) {
42
- const explicit = String(env.AUTH_LOCAL_SESSION_SECRET || "").trim();
43
- if (explicit) {
44
- return explicit;
45
- }
46
- if (isProduction) {
47
- throw new Error("AUTH_LOCAL_SESSION_SECRET is required for local auth in production.");
48
- }
49
-
50
- const secretPath = path.join(storeDir, "session.secret");
51
- try {
52
- const existing = fs.readFileSync(secretPath, "utf8").trim();
53
- if (existing) {
54
- return existing;
55
- }
56
- } catch (error) {
57
- if (!error || error.code !== "ENOENT") {
58
- throw error;
59
- }
60
- }
61
-
62
- fs.mkdirSync(storeDir, { recursive: true, mode: 0o700 });
63
- const generated = randomBytes(32).toString("base64url");
64
- fs.writeFileSync(secretPath, `${generated}\n`, { mode: 0o600 });
65
- return generated;
66
- }
67
-
68
- function resolveSmtpConfig(env) {
69
- const host = String(env.AUTH_LOCAL_SMTP_HOST || "").trim();
70
- if (!host) {
71
- return {
72
- smtpConfigured: false,
73
- smtp: null
74
- };
75
- }
76
- const from = String(env.AUTH_LOCAL_SMTP_FROM || "").trim();
77
- if (!from) {
78
- throw new Error("AUTH_LOCAL_SMTP_FROM is required when AUTH_LOCAL_SMTP_HOST is set.");
79
- }
80
- return {
81
- smtpConfigured: true,
82
- smtp: {
83
- host,
84
- port: Number(env.AUTH_LOCAL_SMTP_PORT || 587),
85
- secure: parseBooleanFlag(env.AUTH_LOCAL_SMTP_SECURE, false),
86
- user: String(env.AUTH_LOCAL_SMTP_USER || "").trim(),
87
- password: String(env.AUTH_LOCAL_SMTP_PASSWORD || ""),
88
- from,
89
- replyTo: String(env.AUTH_LOCAL_SMTP_REPLY_TO || "").trim()
90
- }
91
- };
92
- }
93
-
94
- function resolveAppPublicUrl(env, { smtpConfigured }) {
95
- const configured = String(env.APP_PUBLIC_URL || "").trim();
96
- if (!configured) {
97
- if (smtpConfigured) {
98
- throw new Error("APP_PUBLIC_URL is required when local auth SMTP recovery is configured.");
99
- }
100
- return "http://localhost:5173";
101
- }
102
-
103
- let parsed;
104
- try {
105
- parsed = new URL(configured);
106
- } catch {
107
- throw new Error("APP_PUBLIC_URL must be a valid URL when local auth SMTP recovery is configured.");
108
- }
109
-
110
- if (!["http:", "https:"].includes(parsed.protocol)) {
111
- throw new Error("APP_PUBLIC_URL must use http or https when local auth SMTP recovery is configured.");
112
- }
113
-
114
- return parsed.toString().replace(/\/$/, "");
115
- }
116
-
117
- function resolveConfig(scope) {
118
- const env = resolveRuntimeEnv(scope);
119
- assertSelectedAuthProvider(env);
120
- const nodeEnv = String(env.NODE_ENV || "development").trim() || "development";
121
- const isProduction = nodeEnv === "production";
122
- const backend = String(env.AUTH_LOCAL_BACKEND || "file").trim().toLowerCase() || "file";
123
- const storeDir = resolveStoreDir(env);
124
- if (backend === "file" && isProduction && !parseBooleanFlag(env.AUTH_LOCAL_FILE_PRODUCTION_ACK, false)) {
125
- throw new Error("AUTH_LOCAL_FILE_PRODUCTION_ACK is required to use the local file auth backend in production.");
126
- }
127
- const smtp = resolveSmtpConfig(env);
128
- const devAuth = resolveDevAuthPolicyFromEnv(env);
129
- assertDevAuthPolicy(devAuth);
130
- return {
131
- backend,
132
- storeDir,
133
- nodeEnv,
134
- logger: scope.has("jskit.logger") ? scope.make("jskit.logger") : console,
135
- appPublicUrl: resolveAppPublicUrl(env, smtp),
136
- sessionSecret: resolveSessionSecret(env, {
137
- storeDir,
138
- isProduction
139
- }),
140
- recoveryDevOutput: String(env.AUTH_LOCAL_RECOVERY_DEV_OUTPUT || "log").trim().toLowerCase() || "log",
141
- devAuth,
142
- ...smtp
143
- };
144
- }
145
-
146
- function createLazyProfileProjector(scope) {
147
- if (!scope.has("auth.profile.projector")) {
148
- return null;
149
- }
150
- const resolveMethod = (methodName) => {
151
- const projector = scope.make("auth.profile.projector");
152
- if (!projector || typeof projector[methodName] !== "function") {
153
- throw new Error(`auth.profile.projector.${methodName}() must be a function.`);
154
- }
155
- return projector[methodName].bind(projector);
156
- };
157
- return {
158
- findByIdentity(identity, options = {}) {
159
- return resolveMethod("findByIdentity")(identity, options);
160
- },
161
- syncIdentityProfile(profile, options = {}) {
162
- return resolveMethod("syncIdentityProfile")(profile, options);
163
- }
164
- };
165
- }
166
-
167
- class AuthLocalServiceProvider {
168
- static id = "auth.provider.local";
169
-
170
- register(app) {
171
- if (!app || typeof app.singleton !== "function" || typeof app.has !== "function") {
172
- throw new Error("AuthLocalServiceProvider requires application singleton()/has().");
173
- }
174
-
175
- assertSelectedAuthProvider(resolveRuntimeEnv(app));
176
-
177
- if (app.has("authService")) {
178
- throw new Error("AuthLocalServiceProvider cannot register authService because another auth provider already registered it.");
179
- }
180
-
181
- if (!app.has("auth.local.backend") && resolveLocalBackendMode(app) === "file") {
182
- app.singleton("auth.local.backend", (scope) => {
183
- const config = resolveConfig(scope);
184
- return createLocalFileBackend({
185
- storeDir: config.storeDir
186
- });
187
- });
188
- }
189
-
190
- app.singleton("authService", (scope) => {
191
- const config = resolveConfig(scope);
192
- if (!scope.has("auth.local.backend")) {
193
- throw new Error(`AUTH_LOCAL_BACKEND="${config.backend}" requires a package or app provider that registers auth.local.backend.`);
194
- }
195
- const backend = scope.make("auth.local.backend");
196
- const profileProjector = createLazyProfileProjector(scope);
197
- const invitationContextResolver = scope.has("auth.invitationContextResolver")
198
- ? {
199
- async resolveInvitationContext(invitation, options = {}) {
200
- const resolver = scope.make("auth.invitationContextResolver");
201
- if (typeof resolver === "function") {
202
- return resolver(invitation, options);
203
- }
204
- if (!resolver || typeof resolver.resolveInvitationContext !== "function") {
205
- throw new Error("auth.invitationContextResolver.resolveInvitationContext() must be a function.");
206
- }
207
- return resolver.resolveInvitationContext(invitation, options);
208
- }
209
- }
210
- : null;
211
- const passwordStrategy = scope.has("auth.local.passwordStrategy")
212
- ? scope.make("auth.local.passwordStrategy")
213
- : null;
214
- const authService = createLocalAuthService({
215
- backend,
216
- config,
217
- profileProjector,
218
- passwordStrategy,
219
- invitationContextResolver
220
- });
221
- return applyAuthServiceDecorators(scope, authService);
222
- });
223
- }
224
-
225
- boot(app) {
226
- if (!app || typeof app.make !== "function") {
227
- throw new Error("AuthLocalServiceProvider requires application make().");
228
- }
229
- app.make("authService");
230
- }
231
- }
232
-
233
- export { AuthLocalServiceProvider, resolveLocalBackendMode };
@@ -1,7 +0,0 @@
1
- class AuthProviderServiceProvider {
2
- static id = "auth.provider";
3
-
4
- register() {}
5
- }
6
-
7
- export { AuthProviderServiceProvider };