@jskit-ai/auth-web 0.1.56 → 0.1.58

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-web",
4
- "version": "0.1.56",
4
+ "version": "0.1.58",
5
5
  "kind": "runtime",
6
6
  "description": "Auth web module: Fastify auth routes plus web login/sign-out scaffolds.",
7
7
  "dependsOn": [
@@ -219,11 +219,10 @@ export default Object.freeze({
219
219
  "runtime": {
220
220
  "@tanstack/vue-query": "5.92.12",
221
221
  "@mdi/js": "^7.4.47",
222
- "@fastify/type-provider-typebox": "^6.1.0",
223
- "@jskit-ai/auth-core": "0.1.54",
224
- "@jskit-ai/http-runtime": "0.1.54",
225
- "@jskit-ai/kernel": "0.1.55",
226
- "@jskit-ai/shell-web": "0.1.54",
222
+ "@jskit-ai/auth-core": "0.1.56",
223
+ "@jskit-ai/http-runtime": "0.1.56",
224
+ "@jskit-ai/kernel": "0.1.57",
225
+ "@jskit-ai/shell-web": "0.1.56",
227
226
  "vuetify": "^4.0.0"
228
227
  },
229
228
  "dev": {}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jskit-ai/auth-web",
3
- "version": "0.1.56",
3
+ "version": "0.1.58",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "test": "node --test"
@@ -18,13 +18,16 @@
18
18
  },
19
19
  "dependencies": {
20
20
  "@tanstack/vue-query": "^5.90.5",
21
- "@jskit-ai/auth-core": "0.1.54",
21
+ "@jskit-ai/auth-core": "0.1.56",
22
22
  "@mdi/js": "^7.4.47",
23
- "@fastify/type-provider-typebox": "^6.1.0",
24
- "@jskit-ai/kernel": "0.1.55",
25
- "@jskit-ai/shell-web": "0.1.54",
23
+ "@jskit-ai/kernel": "0.1.57",
24
+ "@jskit-ai/shell-web": "0.1.56",
26
25
  "pinia": "^3.0.4",
27
26
  "vuetify": "^4.0.0",
28
- "@jskit-ai/http-runtime": "0.1.54"
27
+ "@jskit-ai/http-runtime": "0.1.56"
28
+ },
29
+ "peerDependencies": {
30
+ "vue": "^3.5.13",
31
+ "vue-router": "^5.0.4"
29
32
  }
30
33
  }
@@ -150,7 +150,7 @@ export function useLoginViewActions({
150
150
  email: normalizedEmail,
151
151
  password: String(state.password.value || "")
152
152
  };
153
- ensureCommandSectionValid(authRegisterCommand, "bodyValidator", registerPayload, "Unable to register.");
153
+ ensureCommandSectionValid(authRegisterCommand, "body", registerPayload, "Unable to register.");
154
154
 
155
155
  const registerResult = await request(AUTH_PATHS.REGISTER, {
156
156
  method: "POST",
@@ -178,7 +178,7 @@ export function useLoginViewActions({
178
178
  const forgotPayload = { email: normalizedEmail };
179
179
  ensureCommandSectionValid(
180
180
  authPasswordResetRequestCommand,
181
- "bodyValidator",
181
+ "body",
182
182
  forgotPayload,
183
183
  "Unable to request password reset."
184
184
  );
@@ -196,7 +196,7 @@ export function useLoginViewActions({
196
196
  token: String(state.otpCode.value || "").trim(),
197
197
  type: "email"
198
198
  };
199
- ensureCommandSectionValid(authLoginOtpVerifyCommand, "bodyValidator", otpPayload, "Unable to verify one-time code.");
199
+ ensureCommandSectionValid(authLoginOtpVerifyCommand, "body", otpPayload, "Unable to verify one-time code.");
200
200
 
201
201
  const otpResult = await request(AUTH_PATHS.LOGIN_OTP_VERIFY, {
202
202
  method: "POST",
@@ -215,7 +215,7 @@ export function useLoginViewActions({
215
215
  email: normalizedEmail,
216
216
  password: String(state.password.value || "")
217
217
  };
218
- ensureCommandSectionValid(authLoginPasswordCommand, "bodyValidator", loginPayload, "Unable to sign in.");
218
+ ensureCommandSectionValid(authLoginPasswordCommand, "body", loginPayload, "Unable to sign in.");
219
219
 
220
220
  const loginResult = await request(AUTH_PATHS.LOGIN, {
221
221
  method: "POST",
@@ -284,7 +284,7 @@ export function useLoginViewActions({
284
284
  });
285
285
  ensureCommandSectionValid(
286
286
  authLoginOAuthCompleteCommand,
287
- "bodyValidator",
287
+ "body",
288
288
  payload,
289
289
  "Invalid OAuth callback payload."
290
290
  );
@@ -358,7 +358,7 @@ export function useLoginViewActions({
358
358
  };
359
359
  ensureCommandSectionValid(
360
360
  authLoginOtpRequestCommand,
361
- "bodyValidator",
361
+ "body",
362
362
  otpRequestPayload,
363
363
  "Unable to request one-time code."
364
364
  );
@@ -389,7 +389,7 @@ export function useLoginViewActions({
389
389
  };
390
390
  ensureCommandSectionValid(
391
391
  authRegisterConfirmationResendCommand,
392
- "bodyValidator",
392
+ "body",
393
393
  resendPayload,
394
394
  "Unable to resend confirmation email."
395
395
  );
@@ -441,13 +441,13 @@ export function useLoginViewActions({
441
441
  try {
442
442
  ensureCommandSectionValid(
443
443
  authLoginOAuthStartCommand,
444
- "paramsValidator",
444
+ "params",
445
445
  paramsPayload,
446
446
  "OAuth provider id is invalid."
447
447
  );
448
448
  ensureCommandSectionValid(
449
449
  authLoginOAuthStartCommand,
450
- "queryValidator",
450
+ "query",
451
451
  queryPayload,
452
452
  "OAuth return path is invalid."
453
453
  );
@@ -13,7 +13,7 @@ export function useLoginViewValidation({ state } = {}) {
13
13
  function resolveFieldErrorMessages({
14
14
  shouldValidate,
15
15
  commandResource,
16
- section = "bodyValidator",
16
+ section = "body",
17
17
  payload,
18
18
  fieldName
19
19
  } = {}) {
@@ -2,8 +2,8 @@ import { AUTH_GUARD_RUNTIME_INJECTION_KEY } from "../runtime/inject.js";
2
2
  import { useAuthStore } from "../stores/useAuthStore.js";
3
3
 
4
4
  async function bootAuthClientProvider(app) {
5
- if (!app || typeof app.make !== "function") {
6
- throw new Error("AuthWebClientProvider requires application make().");
5
+ if (!app || typeof app.make !== "function" || typeof app.has !== "function") {
6
+ throw new Error("AuthWebClientProvider requires application make()/has().");
7
7
  }
8
8
 
9
9
  const authGuardRuntime = app.make("runtime.auth-guard.client");
@@ -15,6 +15,15 @@ async function bootAuthClientProvider(app) {
15
15
  authStore.attachRuntime(authGuardRuntime);
16
16
  await authStore.initialize();
17
17
 
18
+ if (app.has("runtime.web-bootstrap.client")) {
19
+ const bootstrapRuntime = app.make("runtime.web-bootstrap.client");
20
+ if (bootstrapRuntime && typeof bootstrapRuntime.refresh === "function") {
21
+ authStore.subscribe(() => {
22
+ void bootstrapRuntime.refresh("auth.state");
23
+ });
24
+ }
25
+ }
26
+
18
27
  if (!app.has("jskit.client.vue.app")) {
19
28
  return;
20
29
  }
@@ -1,5 +1,29 @@
1
1
  import { AuthWebService } from "../services/AuthWebService.js";
2
2
 
3
+ function parseBoolean(value, fallback = false) {
4
+ const raw = String(value || "").trim().toLowerCase();
5
+ if (!raw) {
6
+ return fallback;
7
+ }
8
+ if (["1", "true", "yes", "on"].includes(raw)) {
9
+ return true;
10
+ }
11
+ if (["0", "false", "no", "off"].includes(raw)) {
12
+ return false;
13
+ }
14
+ return fallback;
15
+ }
16
+
17
+ function resolveDevAuthBootstrapEnabled(scope) {
18
+ const env =
19
+ scope && typeof scope.has === "function" && scope.has("jskit.env")
20
+ ? scope.make("jskit.env")
21
+ : {};
22
+
23
+ return parseBoolean(env?.AUTH_DEV_BYPASS_ENABLED, false) &&
24
+ String(env?.NODE_ENV || "development").trim().toLowerCase() !== "production";
25
+ }
26
+
3
27
  class AuthWebServiceProvider {
4
28
  static id = "auth.web";
5
29
 
@@ -14,8 +38,12 @@ class AuthWebServiceProvider {
14
38
  }
15
39
 
16
40
  app.singleton("auth.web.service", (scope) => {
17
- const authService = scope.make("authService");
18
- return new AuthWebService({ authService });
41
+ return new AuthWebService({
42
+ getAuthService() {
43
+ return scope.make("authService");
44
+ },
45
+ devAuthBootstrapEnabled: resolveDevAuthBootstrapEnabled(scope)
46
+ });
19
47
  });
20
48
  }
21
49
  }
@@ -32,10 +32,10 @@ function buildRoutes(controller, { includeDevLoginAs = false } = {}) {
32
32
  tags: ["auth"],
33
33
  summary: "Register a new user"
34
34
  },
35
- bodyValidator: authRegisterCommand.operation.bodyValidator,
36
- responseValidators: withStandardErrorResponses(
35
+ body: authRegisterCommand.operation.body,
36
+ responses: withStandardErrorResponses(
37
37
  {
38
- 201: authRegisterCommand.operation.responseValidator
38
+ 201: authRegisterCommand.operation.response
39
39
  },
40
40
  { includeValidation400: true }
41
41
  ),
@@ -53,10 +53,10 @@ function buildRoutes(controller, { includeDevLoginAs = false } = {}) {
53
53
  tags: ["auth"],
54
54
  summary: "Resend sign-up email confirmation"
55
55
  },
56
- bodyValidator: authRegisterConfirmationResendCommand.operation.bodyValidator,
57
- responseValidators: withStandardErrorResponses(
56
+ body: authRegisterConfirmationResendCommand.operation.body,
57
+ responses: withStandardErrorResponses(
58
58
  {
59
- 200: authRegisterConfirmationResendCommand.operation.responseValidator
59
+ 200: authRegisterConfirmationResendCommand.operation.response
60
60
  },
61
61
  { includeValidation400: true }
62
62
  ),
@@ -74,10 +74,10 @@ function buildRoutes(controller, { includeDevLoginAs = false } = {}) {
74
74
  tags: ["auth"],
75
75
  summary: "Log in with configured credentials"
76
76
  },
77
- bodyValidator: authLoginPasswordCommand.operation.bodyValidator,
78
- responseValidators: withStandardErrorResponses(
77
+ body: authLoginPasswordCommand.operation.body,
78
+ responses: withStandardErrorResponses(
79
79
  {
80
- 200: authLoginPasswordCommand.operation.responseValidator
80
+ 200: authLoginPasswordCommand.operation.response
81
81
  },
82
82
  { includeValidation400: true }
83
83
  ),
@@ -95,10 +95,10 @@ function buildRoutes(controller, { includeDevLoginAs = false } = {}) {
95
95
  tags: ["auth"],
96
96
  summary: "Request one-time email login code"
97
97
  },
98
- bodyValidator: authLoginOtpRequestCommand.operation.bodyValidator,
99
- responseValidators: withStandardErrorResponses(
98
+ body: authLoginOtpRequestCommand.operation.body,
99
+ responses: withStandardErrorResponses(
100
100
  {
101
- 200: authLoginOtpRequestCommand.operation.responseValidator
101
+ 200: authLoginOtpRequestCommand.operation.response
102
102
  },
103
103
  { includeValidation400: true }
104
104
  ),
@@ -116,10 +116,10 @@ function buildRoutes(controller, { includeDevLoginAs = false } = {}) {
116
116
  tags: ["auth"],
117
117
  summary: "Verify one-time email login code and create session"
118
118
  },
119
- bodyValidator: authLoginOtpVerifyCommand.operation.bodyValidator,
120
- responseValidators: withStandardErrorResponses(
119
+ body: authLoginOtpVerifyCommand.operation.body,
120
+ responses: withStandardErrorResponses(
121
121
  {
122
- 200: authLoginOtpVerifyCommand.operation.responseValidator
122
+ 200: authLoginOtpVerifyCommand.operation.response
123
123
  },
124
124
  { includeValidation400: true }
125
125
  ),
@@ -138,11 +138,11 @@ function buildRoutes(controller, { includeDevLoginAs = false } = {}) {
138
138
  tags: ["auth"],
139
139
  summary: "Start OAuth login with configured provider"
140
140
  },
141
- paramsValidator: authLoginOAuthStartCommand.operation.paramsValidator,
142
- queryValidator: authLoginOAuthStartCommand.operation.queryValidator,
143
- responseValidators: withStandardErrorResponses(
141
+ params: authLoginOAuthStartCommand.operation.params,
142
+ query: authLoginOAuthStartCommand.operation.query,
143
+ responses: withStandardErrorResponses(
144
144
  {
145
- 302: authLoginOAuthStartCommand.operation.responseValidator
145
+ 302: authLoginOAuthStartCommand.operation.response
146
146
  },
147
147
  { includeValidation400: true }
148
148
  ),
@@ -160,10 +160,10 @@ function buildRoutes(controller, { includeDevLoginAs = false } = {}) {
160
160
  tags: ["auth"],
161
161
  summary: "Complete OAuth code exchange and set session cookies"
162
162
  },
163
- bodyValidator: authLoginOAuthCompleteCommand.operation.bodyValidator,
164
- responseValidators: withStandardErrorResponses(
163
+ body: authLoginOAuthCompleteCommand.operation.body,
164
+ responses: withStandardErrorResponses(
165
165
  {
166
- 200: authLoginOAuthCompleteCommand.operation.responseValidator
166
+ 200: authLoginOAuthCompleteCommand.operation.response
167
167
  },
168
168
  { includeValidation400: true }
169
169
  ),
@@ -181,10 +181,10 @@ function buildRoutes(controller, { includeDevLoginAs = false } = {}) {
181
181
  tags: ["auth"],
182
182
  summary: "Dev-only: create a local session for an existing user"
183
183
  },
184
- bodyValidator: authDevLoginAsCommand.operation.bodyValidator,
185
- responseValidators: withStandardErrorResponses(
184
+ body: authDevLoginAsCommand.operation.body,
185
+ responses: withStandardErrorResponses(
186
186
  {
187
- 200: authDevLoginAsCommand.operation.responseValidator
187
+ 200: authDevLoginAsCommand.operation.response
188
188
  },
189
189
  { includeValidation400: true }
190
190
  ),
@@ -202,10 +202,10 @@ function buildRoutes(controller, { includeDevLoginAs = false } = {}) {
202
202
  tags: ["auth"],
203
203
  summary: "Request a password reset email"
204
204
  },
205
- bodyValidator: authPasswordResetRequestCommand.operation.bodyValidator,
206
- responseValidators: withStandardErrorResponses(
205
+ body: authPasswordResetRequestCommand.operation.body,
206
+ responses: withStandardErrorResponses(
207
207
  {
208
- 200: authPasswordResetRequestCommand.operation.responseValidator
208
+ 200: authPasswordResetRequestCommand.operation.response
209
209
  },
210
210
  { includeValidation400: true }
211
211
  ),
@@ -223,10 +223,10 @@ function buildRoutes(controller, { includeDevLoginAs = false } = {}) {
223
223
  tags: ["auth"],
224
224
  summary: "Complete password recovery link exchange"
225
225
  },
226
- bodyValidator: authPasswordRecoveryCompleteCommand.operation.bodyValidator,
227
- responseValidators: withStandardErrorResponses(
226
+ body: authPasswordRecoveryCompleteCommand.operation.body,
227
+ responses: withStandardErrorResponses(
228
228
  {
229
- 200: authPasswordRecoveryCompleteCommand.operation.responseValidator
229
+ 200: authPasswordRecoveryCompleteCommand.operation.response
230
230
  },
231
231
  { includeValidation400: true }
232
232
  ),
@@ -244,10 +244,10 @@ function buildRoutes(controller, { includeDevLoginAs = false } = {}) {
244
244
  tags: ["auth"],
245
245
  summary: "Set a new password for authenticated recovery session"
246
246
  },
247
- bodyValidator: authPasswordResetCommand.operation.bodyValidator,
248
- responseValidators: withStandardErrorResponses(
247
+ body: authPasswordResetCommand.operation.body,
248
+ responses: withStandardErrorResponses(
249
249
  {
250
- 200: authPasswordResetCommand.operation.responseValidator
250
+ 200: authPasswordResetCommand.operation.response
251
251
  },
252
252
  { includeValidation400: true }
253
253
  ),
@@ -265,8 +265,8 @@ function buildRoutes(controller, { includeDevLoginAs = false } = {}) {
265
265
  tags: ["auth"],
266
266
  summary: "Log out and clear session cookies"
267
267
  },
268
- responseValidators: withStandardErrorResponses({
269
- 200: authLogoutCommand.operation.responseValidator
268
+ responses: withStandardErrorResponses({
269
+ 200: authLogoutCommand.operation.response
270
270
  }),
271
271
  handler: handler("logout")
272
272
  },
@@ -278,9 +278,9 @@ function buildRoutes(controller, { includeDevLoginAs = false } = {}) {
278
278
  tags: ["auth"],
279
279
  summary: "Get current session status and CSRF token"
280
280
  },
281
- responseValidators: withStandardErrorResponses({
282
- 200: authSessionReadCommand.operation.responseValidator,
283
- 503: authSessionReadCommand.operation.unavailableResponseValidator
281
+ responses: withStandardErrorResponses({
282
+ 200: authSessionReadCommand.operation.response,
283
+ 503: authSessionReadCommand.operation.unavailableResponse
284
284
  }),
285
285
  handler: handler("session")
286
286
  }
@@ -1,11 +1,14 @@
1
1
  import { AUTH_ACTION_IDS } from "../constants/authActionIds.js";
2
2
 
3
3
  class AuthWebService {
4
- constructor({ authService } = {}) {
5
- if (!authService) {
6
- throw new Error("authService is required.");
4
+ constructor({ authService, getAuthService, devAuthBootstrapEnabled } = {}) {
5
+ if (!authService && typeof getAuthService !== "function") {
6
+ throw new Error("authService or getAuthService is required.");
7
7
  }
8
- this.authService = authService;
8
+ this.authService = authService || null;
9
+ this.getAuthService = typeof getAuthService === "function" ? getAuthService : null;
10
+ this.devAuthBootstrapEnabled =
11
+ typeof devAuthBootstrapEnabled === "boolean" ? devAuthBootstrapEnabled : null;
9
12
  }
10
13
 
11
14
  static get actionIds() {
@@ -68,9 +71,28 @@ class AuthWebService {
68
71
  });
69
72
  }
70
73
 
74
+ resolveAuthService() {
75
+ if (this.authService) {
76
+ return this.authService;
77
+ }
78
+ if (typeof this.getAuthService !== "function") {
79
+ throw new Error("authService is required.");
80
+ }
81
+
82
+ this.authService = this.getAuthService();
83
+ if (!this.authService) {
84
+ throw new Error("authService is required.");
85
+ }
86
+ return this.authService;
87
+ }
88
+
71
89
  isDevLoginAsAvailable() {
72
- return typeof this.authService?.isDevAuthBootstrapEnabled === "function"
73
- ? this.authService.isDevAuthBootstrapEnabled()
90
+ if (this.devAuthBootstrapEnabled != null) {
91
+ return this.devAuthBootstrapEnabled === true;
92
+ }
93
+ const authService = this.resolveAuthService();
94
+ return typeof authService?.isDevAuthBootstrapEnabled === "function"
95
+ ? authService.isDevAuthBootstrapEnabled()
74
96
  : false;
75
97
  }
76
98
 
@@ -109,20 +131,21 @@ class AuthWebService {
109
131
 
110
132
  writeSessionCookies(reply, session) {
111
133
  if (session && reply) {
112
- this.authService.writeSessionCookies(reply, session);
134
+ this.resolveAuthService().writeSessionCookies(reply, session);
113
135
  }
114
136
  }
115
137
 
116
138
  clearSessionCookies(reply) {
117
139
  if (reply) {
118
- this.authService.clearSessionCookies(reply);
140
+ this.resolveAuthService().clearSessionCookies(reply);
119
141
  }
120
142
  }
121
143
 
122
144
  getOAuthProviderCatalogPayload() {
145
+ const authService = this.resolveAuthService();
123
146
  const catalog =
124
- typeof this.authService.getOAuthProviderCatalog === "function"
125
- ? this.authService.getOAuthProviderCatalog()
147
+ typeof authService.getOAuthProviderCatalog === "function"
148
+ ? authService.getOAuthProviderCatalog()
126
149
  : null;
127
150
  const providers = Array.isArray(catalog?.providers)
128
151
  ? catalog.providers
@@ -37,15 +37,15 @@ test("auth-web exports runtime signout helpers directly", () => {
37
37
  assert.equal(typeof fromRuntimePerformSignOutRequest, "function");
38
38
  });
39
39
 
40
- test("auth-web removes legacy client wrapper modules", () => {
41
- const legacyFiles = [
40
+ test("auth-web removes copied client wrapper modules", () => {
41
+ const removedFiles = [
42
42
  "../src/client/composables/authHttpClient.js",
43
43
  "../src/client/composables/authGuardRuntime.js",
44
44
  "../src/client/composables/useSignOut.js",
45
45
  "../src/client/api/AuthHttpClient.js"
46
46
  ];
47
47
 
48
- for (const relativePath of legacyFiles) {
48
+ for (const relativePath of removedFiles) {
49
49
  const absolutePath = fileURLToPath(new URL(relativePath, import.meta.url));
50
50
  assert.equal(existsSync(absolutePath), false, `${relativePath} must not exist.`);
51
51
  }
@@ -10,12 +10,12 @@ import { authLoginPasswordCommand } from "@jskit-ai/auth-core/shared/commands/au
10
10
  test("auth fastify adapter exports controller/routes backed by shared command validators", () => {
11
11
  assert.equal(typeof AuthController, "function");
12
12
  assert.equal(typeof buildRoutes, "function");
13
- assert.ok(authLoginPasswordCommand.operation.bodyValidator.schema);
13
+ assert.ok(authLoginPasswordCommand.operation.body.schema);
14
14
  });
15
15
 
16
- test("auth-web no longer contains legacy server/schema directory", () => {
16
+ test("auth-web does not contain src/server/schema", () => {
17
17
  const testFilePath = fileURLToPath(import.meta.url);
18
18
  const packageRoot = path.resolve(path.dirname(testFilePath), "..");
19
- const legacySchemaDir = path.join(packageRoot, "src", "server", "schema");
20
- assert.equal(existsSync(legacySchemaDir), false, "src/server/schema must not exist.");
19
+ const serverSchemaDirPath = path.join(packageRoot, "src", "server", "schema");
20
+ assert.equal(existsSync(serverSchemaDirPath), false, "src/server/schema must not exist.");
21
21
  });
@@ -56,7 +56,7 @@ function createAuthRuntimeStub(initialState = {}) {
56
56
  };
57
57
  }
58
58
 
59
- function createAppDouble({ authGuardRuntime } = {}) {
59
+ function createAppDouble({ authGuardRuntime, bootstrapRuntime = null } = {}) {
60
60
  const singletons = new Map();
61
61
  const singletonInstances = new Map();
62
62
  const provided = [];
@@ -88,6 +88,9 @@ function createAppDouble({ authGuardRuntime } = {}) {
88
88
  if (token === "runtime.auth-guard.client") {
89
89
  return true;
90
90
  }
91
+ if (token === "runtime.web-bootstrap.client") {
92
+ return Boolean(bootstrapRuntime);
93
+ }
91
94
  return singletons.has(token) || singletonInstances.has(token);
92
95
  },
93
96
  make(token) {
@@ -124,6 +127,9 @@ function createAppDouble({ authGuardRuntime } = {}) {
124
127
  if (token === "runtime.auth-guard.client") {
125
128
  return authGuardRuntime;
126
129
  }
130
+ if (token === "runtime.web-bootstrap.client") {
131
+ return bootstrapRuntime;
132
+ }
127
133
  if (singletonInstances.has(token)) {
128
134
  return singletonInstances.get(token);
129
135
  }
@@ -163,3 +169,30 @@ test("auth web client boot binds explicit Pinia store state and raw runtime inje
163
169
  const providedByKey = new Map(app.provided.map((entry) => [entry.key, entry.value]));
164
170
  assert.equal(providedByKey.get(AUTH_GUARD_RUNTIME_INJECTION_KEY), authGuardRuntime);
165
171
  });
172
+
173
+ test("auth web client boot refreshes shared bootstrap runtime on auth changes", async () => {
174
+ const authGuardRuntime = createAuthRuntimeStub({
175
+ authenticated: false,
176
+ username: ""
177
+ });
178
+ const refreshCalls = [];
179
+ const app = createAppDouble({
180
+ authGuardRuntime,
181
+ bootstrapRuntime: {
182
+ async refresh(reason) {
183
+ refreshCalls.push(String(reason || ""));
184
+ return null;
185
+ }
186
+ }
187
+ });
188
+
189
+ await bootAuthClientProvider(app);
190
+ assert.deepEqual(refreshCalls, []);
191
+
192
+ authGuardRuntime.push({
193
+ authenticated: true,
194
+ username: "ada"
195
+ });
196
+
197
+ assert.deepEqual(refreshCalls, ["auth.state"]);
198
+ });
@@ -123,15 +123,12 @@ test("auth route provider registers routes and executes login/logout handlers",
123
123
  assert.equal(events.some((entry) => entry.type === "clearSession"), true);
124
124
  });
125
125
 
126
- test("auth route provider registers dev login route only when auth service enables it", async () => {
126
+ test("auth route provider registers dev login route only when dev auth bypass is enabled in env", async () => {
127
127
  const fastify = createFastifyStub();
128
128
  const app = createApplication();
129
129
  const httpRuntime = createHttpRuntime({ app, fastify });
130
130
 
131
131
  const authService = {
132
- isDevAuthBootstrapEnabled() {
133
- return true;
134
- },
135
132
  writeSessionCookies() {},
136
133
  clearSessionCookies() {},
137
134
  getOAuthProviderCatalog() {
@@ -139,6 +136,10 @@ test("auth route provider registers dev login route only when auth service enabl
139
136
  }
140
137
  };
141
138
 
139
+ app.instance("jskit.env", {
140
+ NODE_ENV: "development",
141
+ AUTH_DEV_BYPASS_ENABLED: "true"
142
+ });
142
143
  app.instance("authService", authService);
143
144
  app.instance("actionExecutor", {
144
145
  async execute({ actionId }) {
@@ -169,3 +170,51 @@ test("auth route provider registers dev login route only when auth service enabl
169
170
  assert.equal(devLoginReply.payload.userId, "7");
170
171
  assert.equal(devLoginReply.payload.username, "Dev Ada");
171
172
  });
173
+
174
+ test("auth route provider does not resolve authService during boot", async () => {
175
+ const fastify = createFastifyStub();
176
+ const app = createApplication();
177
+ const httpRuntime = createHttpRuntime({ app, fastify });
178
+ let authServiceResolutions = 0;
179
+
180
+ app.singleton("authService", () => {
181
+ authServiceResolutions += 1;
182
+ return {
183
+ writeSessionCookies() {},
184
+ clearSessionCookies() {},
185
+ getOAuthProviderCatalog() {
186
+ return { providers: [], defaultProvider: "" };
187
+ }
188
+ };
189
+ });
190
+
191
+ app.instance("actionExecutor", {
192
+ async execute({ actionId }) {
193
+ if (actionId === "auth.login.password") {
194
+ return {
195
+ session: { access_token: "a", refresh_token: "r" },
196
+ profile: { displayName: "Ada" }
197
+ };
198
+ }
199
+ return {};
200
+ }
201
+ });
202
+
203
+ class MockAuthProvider {
204
+ static id = "auth.provider";
205
+ }
206
+
207
+ await app.start({ providers: [MockAuthProvider, AuthWebServiceProvider, AuthRouteServiceProvider] });
208
+ assert.equal(authServiceResolutions, 0);
209
+
210
+ const registration = httpRuntime.registerRoutes();
211
+ assert.equal(registration.routeCount > 0, true);
212
+ assert.equal(authServiceResolutions, 0);
213
+
214
+ const loginRoute = fastify.routes.find((route) => route.method === "POST" && route.url === "/api/login");
215
+ assert.ok(loginRoute);
216
+ const loginReply = createReplyStub();
217
+ await loginRoute.handler({ body: { email: "ada@example.com", password: "pass" } }, loginReply);
218
+ assert.equal(loginReply.statusCode, 200);
219
+ assert.equal(authServiceResolutions, 1);
220
+ });