@jskit-ai/auth-web 0.1.159 → 0.1.161

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.
Files changed (28) hide show
  1. package/package.json +29 -135
  2. package/patterns/auth-surface/PATTERN.md +84 -0
  3. package/patterns/auth-surface/example/.vibe64/bin/preview-identity +5 -0
  4. package/src/client/index.js +1 -4
  5. package/src/client/providers/AuthWebClientProvider.js +32 -60
  6. package/src/client/runtime/authClient.js +79 -0
  7. package/src/client/runtime/authGuardRuntime.js +28 -7
  8. package/src/server/AuthWebFeature.js +41 -0
  9. package/src/server/managedPreviewIdentity.js +344 -0
  10. package/src/server/services/AuthWebService.js +4 -16
  11. package/test/clientBoot.test.js +3 -3
  12. package/test/clientSurface.test.js +6 -16
  13. package/test/managedPreviewIdentity.test.js +89 -0
  14. package/test/packageMetadataOwnership.test.js +3 -15
  15. package/test/provider.test.js +30 -220
  16. package/test/providerRuntime.test.js +92 -307
  17. package/src/client/providers/bootAuthClientProvider.js +0 -49
  18. package/src/server/providers/AuthRouteServiceProvider.js +0 -31
  19. package/src/server/providers/AuthWebServiceProvider.js +0 -38
  20. /package/{templates → patterns/auth-surface/example}/src/pages/auth/login.vue +0 -0
  21. /package/{templates → patterns/auth-surface/example}/src/pages/auth/reset-password.vue +0 -0
  22. /package/{templates → patterns/auth-surface/example}/src/pages/auth/signout.vue +0 -0
  23. /package/{templates → patterns/auth-surface/example}/src/runtime/authGuardRuntime.js +0 -0
  24. /package/{templates → patterns/auth-surface/example}/src/runtime/authHttpClient.js +0 -0
  25. /package/{templates → patterns/auth-surface/example}/src/runtime/useSignOut.js +0 -0
  26. /package/{templates → patterns/auth-surface/example}/src/views/auth/LoginView.vue +0 -0
  27. /package/{templates → patterns/auth-surface/example}/src/views/auth/ResetPasswordView.vue +0 -0
  28. /package/{templates → patterns/auth-surface/example}/src/views/auth/SignOutView.vue +0 -0
@@ -1,21 +1,21 @@
1
1
  import assert from "node:assert/strict";
2
2
  import test from "node:test";
3
- import { createApplication, createHttpRuntime } from "@jskit-ai/kernel/_testable";
4
- import { AuthRouteServiceProvider } from "../src/server/providers/AuthRouteServiceProvider.js";
5
- import { AuthWebServiceProvider } from "../src/server/providers/AuthWebServiceProvider.js";
3
+ import { createActionProvider } from "@jskit-ai/kernel/server/actions";
4
+ import { createCapabilityRuntime } from "@jskit-ai/kernel/shared/capabilities";
5
+ import { AuthFeature } from "@jskit-ai/auth-core/server/providers/AuthFeature";
6
+ import { HttpProvider } from "@jskit-ai/kernel/server/http";
7
+ import { AuthWebFeature } from "../src/server/AuthWebFeature.js";
6
8
 
7
9
  function createFastifyStub() {
8
- const routes = [];
9
- const hooks = [];
10
10
  return {
11
- hooks,
12
- routes,
11
+ hooks: [],
12
+ routes: [],
13
13
  errorHandler: null,
14
14
  addHook(name, handler) {
15
- hooks.push({ name, handler });
15
+ this.hooks.push({ name, handler });
16
16
  },
17
17
  route(definition) {
18
- routes.push(definition);
18
+ this.routes.push(definition);
19
19
  },
20
20
  setErrorHandler(handler) {
21
21
  this.errorHandler = handler;
@@ -43,329 +43,114 @@ function createReplyStub() {
43
43
  };
44
44
  }
45
45
 
46
- class MockActionRuntimeProvider {
47
- static id = "runtime.actions";
48
- }
49
-
50
- test("auth route provider registers routes and executes login/logout handlers", async () => {
51
- const events = [];
52
- const fastify = createFastifyStub();
53
- const app = createApplication();
54
- const httpRuntime = createHttpRuntime({ app, fastify });
55
-
56
- const authService = {
46
+ function createAuthService(events = []) {
47
+ return {
48
+ async register(input) {
49
+ return {
50
+ actor: { displayName: input.displayName, email: input.email },
51
+ session: { access_token: "register-a", refresh_token: "register-r" },
52
+ requiresEmailConfirmation: false
53
+ };
54
+ },
55
+ async resendRegisterConfirmation() {
56
+ return { ok: true, message: "Confirmation sent." };
57
+ },
58
+ async login() {
59
+ return {
60
+ session: { access_token: "a", refresh_token: "r" },
61
+ actor: { displayName: "Ada", email: "ada@example.com" }
62
+ };
63
+ },
64
+ async devLoginAs(_request, input) {
65
+ return {
66
+ session: { access_token: "dev-a", refresh_token: "dev-r" },
67
+ actor: { id: input.userId || "7", displayName: "Dev Ada", email: "ada@example.com" }
68
+ };
69
+ },
70
+ async logout() {
71
+ return { ok: true, clearSession: true };
72
+ },
73
+ async authenticateRequest() {
74
+ return { authenticated: false };
75
+ },
76
+ getCapabilities() {
77
+ return {
78
+ provider: { id: "local", label: "Local" },
79
+ features: {
80
+ password: { login: true, register: true, change: true, methodToggle: false },
81
+ passwordRecovery: { request: true, complete: true, delivery: "dev-log" },
82
+ otp: { login: false },
83
+ oauthLogin: { enabled: false, providers: [], defaultProvider: null },
84
+ emailConfirmation: false,
85
+ profileUpdate: true,
86
+ providerLinking: { start: false, unlink: false },
87
+ securityStatus: true,
88
+ signOutOtherSessions: true,
89
+ appProfileProjection: false,
90
+ devLoginAs: false
91
+ }
92
+ };
93
+ },
57
94
  writeSessionCookies(_reply, session) {
58
95
  events.push({ type: "writeSession", session });
59
96
  },
60
97
  clearSessionCookies() {
61
98
  events.push({ type: "clearSession" });
62
- },
63
- getOAuthProviderCatalog() {
64
- return { providers: [], defaultProvider: "" };
65
99
  }
66
100
  };
101
+ }
67
102
 
68
- app.instance("authService", authService);
69
- app.instance("actionExecutor", {
70
- async execute({ actionId }) {
71
- if (actionId === "auth.register.confirmation.resend") {
72
- return {
73
- ok: true,
74
- message: "If an account exists for that email, a confirmation email has been sent."
75
- };
76
- }
77
- if (actionId === "auth.login.password") {
78
- return {
79
- session: { access_token: "a", refresh_token: "r" },
80
- actor: { displayName: "Ada" }
81
- };
82
- }
83
- if (actionId === "auth.dev.loginAs") {
84
- return {
85
- session: { access_token: "dev-a", refresh_token: "dev-r" },
86
- actor: { id: "7", displayName: "Dev Ada", email: "ada@example.com" }
87
- };
88
- }
89
- if (actionId === "auth.logout") {
90
- return {
91
- ok: true,
92
- clearSession: true
93
- };
94
- }
95
- return {};
96
- }
103
+ async function startAuthWeb({ env = {}, authService = null } = {}) {
104
+ const fastify = createFastifyStub();
105
+ const runtime = createCapabilityRuntime({
106
+ inputs: {
107
+ "auth.service": authService || createAuthService(),
108
+ "runtime.env": { NODE_ENV: "test", ...env },
109
+ "runtime.fastify": fastify
110
+ },
111
+ providers: [createActionProvider(), HttpProvider, AuthFeature, AuthWebFeature]
97
112
  });
113
+ await runtime.start();
114
+ return { fastify, runtime };
115
+ }
98
116
 
99
- class MockAuthProvider {
100
- static id = "auth.provider";
101
- }
102
-
103
- await app.start({ providers: [MockActionRuntimeProvider, MockAuthProvider, AuthWebServiceProvider, AuthRouteServiceProvider] });
104
-
105
- const registration = httpRuntime.registerRoutes();
106
- assert.equal(registration.routeCount > 0, true);
107
-
117
+ test("AuthWebFeature registers direct action-backed login and logout routes", async () => {
118
+ const events = [];
119
+ const { fastify } = await startAuthWeb({ authService: createAuthService(events) });
108
120
  const loginRoute = fastify.routes.find((route) => route.method === "POST" && route.url === "/api/login");
109
- assert.ok(loginRoute);
110
121
  const loginReply = createReplyStub();
111
- await loginRoute.handler({ body: { email: "ada@example.com", password: "pass" } }, loginReply);
122
+ await loginRoute.handler({ body: { email: "ada@example.com", password: "password value" } }, loginReply);
112
123
  assert.equal(loginReply.statusCode, 200);
113
124
  assert.equal(loginReply.payload.username, "Ada");
114
125
 
115
- const resendConfirmationRoute = fastify.routes.find(
116
- (route) => route.method === "POST" && route.url === "/api/register/confirmation/resend"
117
- );
118
- assert.ok(resendConfirmationRoute);
119
- const resendConfirmationReply = createReplyStub();
120
- await resendConfirmationRoute.handler({ body: { email: "ada@example.com" } }, resendConfirmationReply);
121
- assert.equal(resendConfirmationReply.statusCode, 200);
122
- assert.equal(resendConfirmationReply.payload.ok, true);
123
-
124
- const devLoginRoute = fastify.routes.find((route) => route.method === "POST" && route.url === "/api/dev-auth/login-as");
125
- assert.equal(devLoginRoute, undefined);
126
-
127
126
  const logoutRoute = fastify.routes.find((route) => route.method === "POST" && route.url === "/api/logout");
128
- assert.ok(logoutRoute);
129
127
  const logoutReply = createReplyStub();
130
128
  await logoutRoute.handler({}, logoutReply);
131
- assert.equal(logoutReply.statusCode, 200);
132
- assert.equal(logoutReply.payload.ok, true);
133
-
129
+ assert.deepEqual(logoutReply.payload, { ok: true });
134
130
  assert.equal(events.some((entry) => entry.type === "writeSession"), true);
135
131
  assert.equal(events.some((entry) => entry.type === "clearSession"), true);
136
132
  });
137
133
 
138
- test("auth route provider registers dev login route only when dev auth bypass is enabled in env", async () => {
139
- const fastify = createFastifyStub();
140
- const app = createApplication();
141
- const httpRuntime = createHttpRuntime({ app, fastify });
142
-
143
- const authService = {
144
- writeSessionCookies() {},
145
- clearSessionCookies() {},
146
- getOAuthProviderCatalog() {
147
- return { providers: [], defaultProvider: "" };
148
- }
149
- };
150
-
151
- app.instance("jskit.env", {
152
- NODE_ENV: "development",
153
- AUTH_DEV_BYPASS_ENABLED: "true"
154
- });
155
- app.instance("authService", authService);
156
- app.instance("actionExecutor", {
157
- async execute({ actionId }) {
158
- if (actionId === "auth.dev.loginAs") {
159
- return {
160
- session: { access_token: "dev-a", refresh_token: "dev-r" },
161
- actor: { id: "7", displayName: "Dev Ada", email: "ada@example.com" }
162
- };
163
- }
164
- return {};
134
+ test("AuthWebFeature exposes dev login only when the explicit dev policy enables it", async () => {
135
+ const disabled = await startAuthWeb();
136
+ assert.equal(disabled.fastify.routes.some((route) => route.url === "/api/dev-auth/login-as"), false);
137
+ const enabled = await startAuthWeb({
138
+ env: {
139
+ NODE_ENV: "development",
140
+ AUTH_DEV_BYPASS_ENABLED: "true",
141
+ AUTH_DEV_BYPASS_SECRET: "test-secret"
165
142
  }
166
143
  });
167
-
168
- class MockAuthProvider {
169
- static id = "auth.provider";
170
- }
171
-
172
- await app.start({ providers: [MockActionRuntimeProvider, MockAuthProvider, AuthWebServiceProvider, AuthRouteServiceProvider] });
173
-
174
- const registration = httpRuntime.registerRoutes();
175
- assert.equal(registration.routeCount > 0, true);
176
-
177
- const devLoginRoute = fastify.routes.find((route) => route.method === "POST" && route.url === "/api/dev-auth/login-as");
178
- assert.ok(devLoginRoute);
179
- const devLoginReply = createReplyStub();
180
- await devLoginRoute.handler({ body: { userId: "7" } }, devLoginReply);
181
- assert.equal(devLoginReply.statusCode, 200);
182
- assert.equal(devLoginReply.payload.userId, "7");
183
- assert.equal(devLoginReply.payload.username, "Dev Ada");
144
+ assert.equal(enabled.fastify.routes.some((route) => route.url === "/api/dev-auth/login-as"), true);
184
145
  });
185
146
 
186
- test("auth route provider does not resolve authService during boot", async () => {
187
- const fastify = createFastifyStub();
188
- const app = createApplication();
189
- const httpRuntime = createHttpRuntime({ app, fastify });
190
- let authServiceResolutions = 0;
191
-
192
- app.singleton("authService", () => {
193
- authServiceResolutions += 1;
194
- return {
195
- writeSessionCookies() {},
196
- clearSessionCookies() {},
197
- getOAuthProviderCatalog() {
198
- return { providers: [], defaultProvider: "" };
199
- }
200
- };
201
- });
202
-
203
- app.instance("actionExecutor", {
204
- async execute({ actionId }) {
205
- if (actionId === "auth.login.password") {
206
- return {
207
- session: { access_token: "a", refresh_token: "r" },
208
- actor: { displayName: "Ada" }
209
- };
210
- }
211
- return {};
212
- }
213
- });
214
-
215
- class MockAuthProvider {
216
- static id = "auth.provider";
217
- }
218
-
219
- await app.start({ providers: [MockActionRuntimeProvider, MockAuthProvider, AuthWebServiceProvider, AuthRouteServiceProvider] });
220
- assert.equal(authServiceResolutions, 0);
221
-
222
- const registration = httpRuntime.registerRoutes();
223
- assert.equal(registration.routeCount > 0, true);
224
- assert.equal(authServiceResolutions, 0);
225
-
226
- const loginRoute = fastify.routes.find((route) => route.method === "POST" && route.url === "/api/login");
227
- assert.ok(loginRoute);
228
- const loginReply = createReplyStub();
229
- await loginRoute.handler({ body: { email: "ada@example.com", password: "pass" } }, loginReply);
230
- assert.equal(loginReply.statusCode, 200);
231
- assert.equal(authServiceResolutions, 1);
232
- });
233
-
234
- test("auth session route preserves provider capabilities through response validation", async () => {
235
- const fastify = createFastifyStub();
236
- const app = createApplication();
237
- const httpRuntime = createHttpRuntime({ app, fastify });
238
-
239
- app.instance("authService", {
240
- writeSessionCookies() {},
241
- clearSessionCookies() {},
242
- getCapabilities() {
243
- return {
244
- provider: {
245
- id: "local",
246
- label: "Local"
247
- },
248
- features: {
249
- password: {
250
- login: true,
251
- register: true,
252
- change: true,
253
- methodToggle: false
254
- },
255
- passwordRecovery: {
256
- request: true,
257
- complete: true,
258
- delivery: "dev-log"
259
- },
260
- otp: {
261
- login: false
262
- },
263
- oauthLogin: {
264
- enabled: false,
265
- providers: [],
266
- defaultProvider: null
267
- },
268
- emailConfirmation: false,
269
- profileUpdate: true,
270
- providerLinking: {
271
- start: false,
272
- unlink: false
273
- },
274
- securityStatus: true,
275
- signOutOtherSessions: true,
276
- appProfileProjection: false,
277
- devLoginAs: false
278
- }
279
- };
280
- }
281
- });
282
- app.instance("actionExecutor", {
283
- async execute({ actionId }) {
284
- if (actionId === "auth.session.read") {
285
- return {
286
- authenticated: false
287
- };
288
- }
289
- return {};
290
- }
291
- });
292
-
293
- class MockAuthProvider {
294
- static id = "auth.provider";
295
- }
296
-
297
- await app.start({ providers: [MockActionRuntimeProvider, MockAuthProvider, AuthWebServiceProvider, AuthRouteServiceProvider] });
298
-
299
- const registration = httpRuntime.registerRoutes();
300
- assert.equal(registration.routeCount > 0, true);
301
-
147
+ test("auth session route returns provider capabilities", async () => {
148
+ const { fastify } = await startAuthWeb();
302
149
  const sessionRoute = fastify.routes.find((route) => route.method === "GET" && route.url === "/api/session");
303
- assert.ok(sessionRoute);
304
- const sessionReply = createReplyStub();
305
- await sessionRoute.handler({}, sessionReply);
306
-
307
- assert.equal(sessionReply.statusCode, 200);
308
- assert.equal(sessionReply.payload.authenticated, false);
309
- assert.equal(sessionReply.payload.authCapabilities.provider.id, "local");
310
- assert.equal(sessionReply.payload.authCapabilities.features.password.login, true);
311
- assert.equal(sessionReply.payload.authCapabilities.features.password.register, true);
312
- });
313
-
314
- test("auth session route exposes denial reason when policy clears a rejected authenticated session", async () => {
315
- const events = [];
316
- const fastify = createFastifyStub();
317
- const app = createApplication();
318
- const httpRuntime = createHttpRuntime({ app, fastify });
319
-
320
- const authService = {
321
- writeSessionCookies() {
322
- events.push({ type: "writeSession" });
323
- },
324
- clearSessionCookies() {
325
- events.push({ type: "clearSession" });
326
- },
327
- getOAuthProviderCatalog() {
328
- return { providers: [], defaultProvider: "" };
329
- }
330
- };
331
-
332
- app.instance("authService", authService);
333
- app.instance("actionExecutor", {
334
- async execute({ actionId }) {
335
- if (actionId === "auth.session.read") {
336
- return {
337
- authenticated: false,
338
- clearSession: true,
339
- transientFailure: false,
340
- authDenied: {
341
- code: "not_allowlisted",
342
- message: "This account is not allowed to access this application."
343
- }
344
- };
345
- }
346
- return {};
347
- }
348
- });
349
-
350
- class MockAuthProvider {
351
- static id = "auth.provider";
352
- }
353
-
354
- await app.start({ providers: [MockActionRuntimeProvider, MockAuthProvider, AuthWebServiceProvider, AuthRouteServiceProvider] });
355
-
356
- const registration = httpRuntime.registerRoutes();
357
- assert.equal(registration.routeCount > 0, true);
358
-
359
- const sessionRoute = fastify.routes.find((route) => route.method === "GET" && route.url === "/api/session");
360
- assert.ok(sessionRoute);
361
- const sessionReply = createReplyStub();
362
- await sessionRoute.handler({}, sessionReply);
363
-
364
- assert.equal(sessionReply.statusCode, 200);
365
- assert.equal(sessionReply.payload.authenticated, false);
366
- assert.deepEqual(sessionReply.payload.authDenied, {
367
- code: "not_allowlisted",
368
- message: "This account is not allowed to access this application."
369
- });
370
- assert.deepEqual(events, [{ type: "clearSession" }]);
150
+ const reply = createReplyStub();
151
+ await sessionRoute.handler({}, reply);
152
+ assert.equal(reply.statusCode, 200);
153
+ assert.equal(reply.payload.authenticated, false);
154
+ assert.equal(reply.payload.authCapabilities.provider.id, "local");
155
+ assert.equal(reply.payload.authCapabilities.features.password.register, true);
371
156
  });
@@ -1,49 +0,0 @@
1
- import {
2
- AUTH_GUARD_RUNTIME_INJECTION_KEY,
3
- AUTH_OAUTH_LAUNCH_CLIENT_INJECTION_KEY
4
- } from "../runtime/inject.js";
5
- import { createBrowserOAuthLaunchClient } from "../runtime/oauthLaunchClient.js";
6
- import { useAuthStore } from "../stores/useAuthStore.js";
7
-
8
- const AUTH_OAUTH_LAUNCH_CLIENT_TOKEN = "auth.oauth-launch.client";
9
-
10
- async function bootAuthClientProvider(app) {
11
- if (!app || typeof app.make !== "function" || typeof app.has !== "function") {
12
- throw new Error("AuthWebClientProvider requires application make()/has().");
13
- }
14
-
15
- const authGuardRuntime = app.make("runtime.auth-guard.client");
16
- const pinia = app.make("jskit.client.pinia");
17
- if (!pinia) {
18
- throw new Error("AuthWebClientProvider requires Pinia installed in the client app.");
19
- }
20
- const authStore = useAuthStore(pinia);
21
- authStore.attachRuntime(authGuardRuntime);
22
- await authStore.initialize();
23
-
24
- if (app.has("runtime.web-bootstrap.client")) {
25
- const bootstrapRuntime = app.make("runtime.web-bootstrap.client");
26
- if (bootstrapRuntime && typeof bootstrapRuntime.refresh === "function") {
27
- authStore.subscribe(() => {
28
- void bootstrapRuntime.refresh("auth.state");
29
- });
30
- }
31
- }
32
-
33
- if (!app.has("jskit.client.vue.app")) {
34
- return;
35
- }
36
-
37
- const vueApp = app.make("jskit.client.vue.app");
38
- if (!vueApp || typeof vueApp.provide !== "function") {
39
- return;
40
- }
41
-
42
- const oauthLaunchClient = app.has(AUTH_OAUTH_LAUNCH_CLIENT_TOKEN)
43
- ? app.make(AUTH_OAUTH_LAUNCH_CLIENT_TOKEN)
44
- : createBrowserOAuthLaunchClient();
45
- vueApp.provide(AUTH_GUARD_RUNTIME_INJECTION_KEY, authGuardRuntime);
46
- vueApp.provide(AUTH_OAUTH_LAUNCH_CLIENT_INJECTION_KEY, oauthLaunchClient);
47
- }
48
-
49
- export { bootAuthClientProvider };
@@ -1,31 +0,0 @@
1
- import { AuthController } from "../controllers/AuthController.js";
2
- import { buildRoutes } from "../routes/authRoutes.js";
3
-
4
- class AuthRouteServiceProvider {
5
- static id = "auth.routes";
6
-
7
- register(app) {
8
- if (!app || typeof app.has !== "function") {
9
- throw new Error("AuthRouteServiceProvider requires application has().");
10
- }
11
- }
12
-
13
- boot(app) {
14
- if (!app || typeof app.make !== "function") {
15
- throw new Error("AuthRouteServiceProvider requires application make().");
16
- }
17
-
18
- const router = app.make("jskit.http.router");
19
- const authWebService = app.make("auth.web.service");
20
- const controller = new AuthController({ service: authWebService });
21
- const routes = buildRoutes(controller, {
22
- includeDevLoginAs:
23
- typeof authWebService?.isDevLoginAsAvailable === "function" ? authWebService.isDevLoginAsAvailable() : false
24
- });
25
- for (const route of routes) {
26
- router.register(route.method, route.path, route, route.handler);
27
- }
28
- }
29
- }
30
-
31
- export { AuthRouteServiceProvider };
@@ -1,38 +0,0 @@
1
- import { resolveDevAuthPolicyFromEnv } from "@jskit-ai/auth-core/server/devAuth";
2
- import { AuthWebService } from "../services/AuthWebService.js";
3
-
4
- function resolveDevAuthBootstrapEnabled(scope) {
5
- const env =
6
- scope && typeof scope.has === "function" && scope.has("jskit.env")
7
- ? scope.make("jskit.env")
8
- : {};
9
-
10
- const policy = resolveDevAuthPolicyFromEnv(env);
11
- return policy.enabled && !policy.isProduction;
12
- }
13
-
14
- class AuthWebServiceProvider {
15
- static id = "auth.web";
16
-
17
- static startsAfter = ["runtime.actions"];
18
-
19
- register(app) {
20
- if (!app || typeof app.singleton !== "function" || typeof app.has !== "function") {
21
- throw new Error("AuthWebServiceProvider requires application singleton()/has().");
22
- }
23
- if (!app.has("actionExecutor")) {
24
- throw new Error("AuthWebServiceProvider requires actionExecutor binding.");
25
- }
26
-
27
- app.singleton("auth.web.service", (scope) => {
28
- return new AuthWebService({
29
- getAuthService() {
30
- return scope.make("authService");
31
- },
32
- devAuthBootstrapEnabled: resolveDevAuthBootstrapEnabled(scope)
33
- });
34
- });
35
- }
36
- }
37
-
38
- export { AuthWebServiceProvider };