@jskit-ai/auth-web 0.1.38 → 0.1.40

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.38",
4
+ "version": "0.1.40",
5
5
  "kind": "runtime",
6
6
  "description": "Auth web module: Fastify auth routes plus web login/sign-out scaffolds.",
7
7
  "dependsOn": [
@@ -220,10 +220,10 @@ export default Object.freeze({
220
220
  "@tanstack/vue-query": "5.92.12",
221
221
  "@mdi/js": "^7.4.47",
222
222
  "@fastify/type-provider-typebox": "^6.1.0",
223
- "@jskit-ai/auth-core": "0.1.36",
224
- "@jskit-ai/http-runtime": "0.1.36",
225
- "@jskit-ai/kernel": "0.1.37",
226
- "@jskit-ai/shell-web": "0.1.36",
223
+ "@jskit-ai/auth-core": "0.1.38",
224
+ "@jskit-ai/http-runtime": "0.1.38",
225
+ "@jskit-ai/kernel": "0.1.39",
226
+ "@jskit-ai/shell-web": "0.1.38",
227
227
  "vuetify": "^4.0.0"
228
228
  },
229
229
  "dev": {}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jskit-ai/auth-web",
3
- "version": "0.1.38",
3
+ "version": "0.1.40",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "test": "node --test"
@@ -18,12 +18,13 @@
18
18
  },
19
19
  "dependencies": {
20
20
  "@tanstack/vue-query": "^5.90.5",
21
- "@jskit-ai/auth-core": "0.1.36",
21
+ "@jskit-ai/auth-core": "0.1.38",
22
22
  "@mdi/js": "^7.4.47",
23
23
  "@fastify/type-provider-typebox": "^6.1.0",
24
- "@jskit-ai/kernel": "0.1.37",
25
- "@jskit-ai/shell-web": "0.1.36",
24
+ "@jskit-ai/kernel": "0.1.39",
25
+ "@jskit-ai/shell-web": "0.1.38",
26
+ "pinia": "^3.0.4",
26
27
  "vuetify": "^4.0.0",
27
- "@jskit-ai/http-runtime": "0.1.36"
28
+ "@jskit-ai/http-runtime": "0.1.38"
28
29
  }
29
30
  }
@@ -5,7 +5,7 @@ const OTP_MODE = "otp";
5
5
  const EMAIL_CONFIRMATION_MODE = "confirm-email";
6
6
 
7
7
  const AUTH_TITLE_BY_MODE = Object.freeze({
8
- [LOGIN_MODE]: "Welcome back",
8
+ [LOGIN_MODE]: "Welcome",
9
9
  [REGISTER_MODE]: "Create your account",
10
10
  [FORGOT_MODE]: "Reset your password",
11
11
  [OTP_MODE]: "Use one-time code",
@@ -7,6 +7,8 @@ export { default as DefaultLoginView } from "./views/DefaultLoginView.vue";
7
7
  export { default as DefaultSignOutView } from "./views/DefaultSignOutView.vue";
8
8
  export { default as AuthProfileWidget } from "./views/AuthProfileWidget.vue";
9
9
  export { default as AuthProfileMenuLinkItem } from "./views/AuthProfileMenuLinkItem.vue";
10
+ export { useAuthStore } from "./stores/useAuthStore.js";
11
+ export { useAuthGuardRuntime } from "./runtime/inject.js";
10
12
 
11
13
  const routeComponents = Object.freeze({
12
14
  "auth-login": DefaultLoginView,
@@ -4,10 +4,10 @@ function resolveSurfaceLinkTarget({
4
4
  context = null,
5
5
  surface = "",
6
6
  explicitTo = "",
7
- workspaceSuffix = "",
8
- nonWorkspaceSuffix = ""
7
+ scopedSuffix = "",
8
+ unscopedSuffix = ""
9
9
  } = {}) {
10
- const fallbackPath = String(nonWorkspaceSuffix || "").trim() || String(workspaceSuffix || "").trim() || "/";
10
+ const fallbackPath = String(unscopedSuffix || "").trim() || String(scopedSuffix || "").trim() || "/";
11
11
  return resolveShellLinkPath({
12
12
  context,
13
13
  surface,
@@ -3,6 +3,7 @@ import AuthProfileWidget from "../views/AuthProfileWidget.vue";
3
3
  import AuthProfileMenuLinkItem from "../views/AuthProfileMenuLinkItem.vue";
4
4
  import { createAuthGuardRuntime } from "../runtime/authGuardRuntime.js";
5
5
  import { useLoginView } from "../runtime/useLoginView.js";
6
+ import { bootAuthClientProvider } from "./bootAuthClientProvider.js";
6
7
  import { resolveSurfaceNavigationTargetFromPlacementContext } from "@jskit-ai/shell-web/client/placement";
7
8
 
8
9
  class AuthWebClientProvider {
@@ -37,23 +38,7 @@ class AuthWebClientProvider {
37
38
  }
38
39
 
39
40
  async boot(app) {
40
- if (!app || typeof app.make !== "function") {
41
- throw new Error("AuthWebClientProvider requires application make().");
42
- }
43
-
44
- const authGuardRuntime = app.make("runtime.auth-guard.client");
45
- await authGuardRuntime.initialize();
46
-
47
- if (!app.has("jskit.client.vue.app")) {
48
- return;
49
- }
50
-
51
- const vueApp = app.make("jskit.client.vue.app");
52
- if (!vueApp || typeof vueApp.provide !== "function") {
53
- return;
54
- }
55
-
56
- vueApp.provide("jskit.auth-web.runtime.auth-guard.client", authGuardRuntime);
41
+ await bootAuthClientProvider(app);
57
42
  }
58
43
  }
59
44
 
@@ -0,0 +1,30 @@
1
+ import { AUTH_GUARD_RUNTIME_INJECTION_KEY } from "../runtime/inject.js";
2
+ import { useAuthStore } from "../stores/useAuthStore.js";
3
+
4
+ async function bootAuthClientProvider(app) {
5
+ if (!app || typeof app.make !== "function") {
6
+ throw new Error("AuthWebClientProvider requires application make().");
7
+ }
8
+
9
+ const authGuardRuntime = app.make("runtime.auth-guard.client");
10
+ const pinia = app.make("jskit.client.pinia");
11
+ if (!pinia) {
12
+ throw new Error("AuthWebClientProvider requires Pinia installed in the client app.");
13
+ }
14
+ const authStore = useAuthStore(pinia);
15
+ authStore.attachRuntime(authGuardRuntime);
16
+ await authStore.initialize();
17
+
18
+ if (!app.has("jskit.client.vue.app")) {
19
+ return;
20
+ }
21
+
22
+ const vueApp = app.make("jskit.client.vue.app");
23
+ if (!vueApp || typeof vueApp.provide !== "function") {
24
+ return;
25
+ }
26
+
27
+ vueApp.provide(AUTH_GUARD_RUNTIME_INJECTION_KEY, authGuardRuntime);
28
+ }
29
+
30
+ export { bootAuthClientProvider };
@@ -1,6 +1,8 @@
1
1
  import { inject } from "vue";
2
2
  import { isAuthGuardRuntime } from "./authGuardRuntime.js";
3
3
 
4
+ const AUTH_GUARD_RUNTIME_INJECTION_KEY = "jskit.auth-web.runtime.auth-guard.client";
5
+
4
6
  const EMPTY_AUTH_GUARD_STATE = Object.freeze({
5
7
  authenticated: false,
6
8
  username: "",
@@ -24,7 +26,7 @@ const EMPTY_AUTH_GUARD_RUNTIME = Object.freeze({
24
26
  });
25
27
 
26
28
  function useAuthGuardRuntime({ required = false } = {}) {
27
- const runtime = inject("jskit.auth-web.runtime.auth-guard.client", null);
29
+ const runtime = inject(AUTH_GUARD_RUNTIME_INJECTION_KEY, null);
28
30
  if (isAuthGuardRuntime(runtime)) {
29
31
  return runtime;
30
32
  }
@@ -37,6 +39,8 @@ function useAuthGuardRuntime({ required = false } = {}) {
37
39
  }
38
40
 
39
41
  export {
42
+ AUTH_GUARD_RUNTIME_INJECTION_KEY,
43
+ EMPTY_AUTH_GUARD_STATE,
40
44
  EMPTY_AUTH_GUARD_RUNTIME,
41
45
  useAuthGuardRuntime
42
46
  };
@@ -0,0 +1,85 @@
1
+ import { computed, markRaw, shallowRef } from "vue";
2
+ import { defineStore } from "pinia";
3
+ import { isAuthGuardRuntime } from "../runtime/authGuardRuntime.js";
4
+ import { EMPTY_AUTH_GUARD_RUNTIME, EMPTY_AUTH_GUARD_STATE } from "../runtime/inject.js";
5
+
6
+ function normalizeAuthStateValue(nextState) {
7
+ if (!nextState || typeof nextState !== "object") {
8
+ return EMPTY_AUTH_GUARD_STATE;
9
+ }
10
+
11
+ return nextState;
12
+ }
13
+
14
+ export const useAuthStore = defineStore("jskit.auth-web.auth", () => {
15
+ const runtime = shallowRef(markRaw(EMPTY_AUTH_GUARD_RUNTIME));
16
+ const authState = shallowRef(EMPTY_AUTH_GUARD_STATE);
17
+ let unsubscribe = null;
18
+
19
+ function setAuthState(nextState) {
20
+ authState.value = normalizeAuthStateValue(nextState);
21
+ return authState.value;
22
+ }
23
+
24
+ function detachRuntimeSubscription() {
25
+ if (typeof unsubscribe === "function") {
26
+ unsubscribe();
27
+ unsubscribe = null;
28
+ }
29
+ }
30
+
31
+ function attachRuntime(nextRuntime = EMPTY_AUTH_GUARD_RUNTIME) {
32
+ if (!isAuthGuardRuntime(nextRuntime) || typeof nextRuntime.subscribe !== "function") {
33
+ throw new TypeError("useAuthStore.attachRuntime requires an auth guard runtime with subscribe().");
34
+ }
35
+
36
+ if (runtime.value === nextRuntime) {
37
+ setAuthState(nextRuntime.getState());
38
+ return runtime.value;
39
+ }
40
+
41
+ detachRuntimeSubscription();
42
+ runtime.value = markRaw(nextRuntime);
43
+ setAuthState(nextRuntime.getState());
44
+ unsubscribe = nextRuntime.subscribe((nextState) => {
45
+ setAuthState(nextState);
46
+ });
47
+
48
+ return runtime.value;
49
+ }
50
+
51
+ async function initialize(options = {}) {
52
+ return setAuthState(await runtime.value.initialize(options));
53
+ }
54
+
55
+ async function refresh(options = {}) {
56
+ return setAuthState(await runtime.value.refresh(options));
57
+ }
58
+
59
+ function getState() {
60
+ return authState.value;
61
+ }
62
+
63
+ function subscribe(listener) {
64
+ return runtime.value.subscribe(listener);
65
+ }
66
+
67
+ const authenticated = computed(() => authState.value.authenticated === true);
68
+ const username = computed(() => String(authState.value.username || ""));
69
+ const oauthProviders = computed(() => authState.value.oauthProviders || EMPTY_AUTH_GUARD_STATE.oauthProviders);
70
+ const oauthDefaultProvider = computed(() => String(authState.value.oauthDefaultProvider || ""));
71
+
72
+ return {
73
+ runtime,
74
+ authState,
75
+ authenticated,
76
+ username,
77
+ oauthProviders,
78
+ oauthDefaultProvider,
79
+ attachRuntime,
80
+ initialize,
81
+ refresh,
82
+ getState,
83
+ subscribe
84
+ };
85
+ });
@@ -1,15 +1,11 @@
1
1
  <script setup>
2
- import { computed, onBeforeUnmount, onMounted, ref } from "vue";
2
+ import { computed } from "vue";
3
3
  import ShellOutlet from "@jskit-ai/shell-web/client/components/ShellOutlet";
4
4
  import { useWebPlacementContext } from "@jskit-ai/shell-web/client/placement";
5
- import { useAuthGuardRuntime } from "../runtime/inject.js";
5
+ import { useAuthStore } from "../stores/useAuthStore.js";
6
6
 
7
- const authGuardRuntime = useAuthGuardRuntime({
8
- required: true
9
- });
10
- const authState = ref(authGuardRuntime.getState());
7
+ const auth = useAuthStore();
11
8
  const { context: shellPlacementContext } = useWebPlacementContext();
12
- let unsubscribe = null;
13
9
 
14
10
  const shellUser = computed(() => {
15
11
  const user = shellPlacementContext.value?.user;
@@ -25,7 +21,7 @@ const displayName = computed(() => {
25
21
  return fromContext;
26
22
  }
27
23
 
28
- const username = String(authState.value?.username || "").trim();
24
+ const username = String(auth.username || "").trim();
29
25
  if (username) {
30
26
  return username;
31
27
  }
@@ -58,23 +54,10 @@ const initials = computed(() => {
58
54
 
59
55
  const placementContext = computed(() => {
60
56
  return {
61
- auth: authState.value,
57
+ auth: auth.authState,
62
58
  user: shellUser.value
63
59
  };
64
60
  });
65
-
66
- onMounted(() => {
67
- unsubscribe = authGuardRuntime.subscribe((nextState) => {
68
- authState.value = nextState;
69
- });
70
- });
71
-
72
- onBeforeUnmount(() => {
73
- if (typeof unsubscribe === "function") {
74
- unsubscribe();
75
- unsubscribe = null;
76
- }
77
- });
78
61
  </script>
79
62
 
80
63
  <template>
@@ -6,6 +6,9 @@ import test from "node:test";
6
6
  test("auth-web client index defines provider-based client routes surface", () => {
7
7
  const source = readFileSync(fileURLToPath(new URL("../src/client/index.js", import.meta.url)), "utf8");
8
8
 
9
+ assert.equal(source.includes('export { useAuthStore } from "./stores/useAuthStore.js";'), true);
10
+ assert.equal(source.includes('export { useAuthGuardRuntime } from "./runtime/inject.js";'), true);
11
+ assert.equal(source.includes('export { useAuth } from "./composables/useAuth.js";'), false);
9
12
  assert.equal(source.includes("const routeComponents = Object.freeze({"), true);
10
13
  assert.equal(source.includes('"auth-login": DefaultLoginView'), true);
11
14
  assert.equal(source.includes('"auth-signout": DefaultSignOutView'), true);
@@ -0,0 +1,165 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { createPinia } from "pinia";
4
+ import { AUTH_GUARD_RUNTIME_INJECTION_KEY } from "../src/client/runtime/inject.js";
5
+ import { bootAuthClientProvider } from "../src/client/providers/bootAuthClientProvider.js";
6
+ import { useAuthStore } from "../src/client/stores/useAuthStore.js";
7
+
8
+ function createAuthRuntimeStub(initialState = {}) {
9
+ let state = Object.freeze({
10
+ authenticated: Boolean(initialState.authenticated),
11
+ username: String(initialState.username || ""),
12
+ oauthDefaultProvider: String(initialState.oauthDefaultProvider || ""),
13
+ oauthProviders: Array.isArray(initialState.oauthProviders)
14
+ ? Object.freeze([...initialState.oauthProviders])
15
+ : Object.freeze([])
16
+ });
17
+ let initializeCalls = 0;
18
+ const listeners = new Set();
19
+
20
+ return {
21
+ async initialize() {
22
+ initializeCalls += 1;
23
+ return state;
24
+ },
25
+ async refresh() {
26
+ return state;
27
+ },
28
+ getState() {
29
+ return state;
30
+ },
31
+ subscribe(listener) {
32
+ if (typeof listener === "function") {
33
+ listeners.add(listener);
34
+ }
35
+ return () => {
36
+ listeners.delete(listener);
37
+ };
38
+ },
39
+ push(nextState = {}) {
40
+ state = Object.freeze({
41
+ authenticated: Boolean(nextState.authenticated),
42
+ username: String(nextState.username || ""),
43
+ oauthDefaultProvider: String(nextState.oauthDefaultProvider || ""),
44
+ oauthProviders: Array.isArray(nextState.oauthProviders)
45
+ ? Object.freeze([...nextState.oauthProviders])
46
+ : Object.freeze([])
47
+ });
48
+
49
+ for (const listener of listeners) {
50
+ listener(state);
51
+ }
52
+ },
53
+ get initializeCalls() {
54
+ return initializeCalls;
55
+ }
56
+ };
57
+ }
58
+
59
+ function createAppDouble({ authGuardRuntime } = {}) {
60
+ const singletons = new Map();
61
+ const singletonInstances = new Map();
62
+ const provided = [];
63
+ const pinia = createPinia();
64
+ const vueApp = {
65
+ provide(key, value) {
66
+ provided.push({ key, value });
67
+ }
68
+ };
69
+
70
+ return {
71
+ singletons,
72
+ provided,
73
+ pinia,
74
+ vueApp,
75
+ singleton(token, factory) {
76
+ singletons.set(token, factory);
77
+ },
78
+ has(token) {
79
+ if (token === "jskit.client.vue.app") {
80
+ return true;
81
+ }
82
+ if (token === "jskit.client.pinia") {
83
+ return true;
84
+ }
85
+ if (token === "runtime.web-placement.client") {
86
+ return true;
87
+ }
88
+ if (token === "runtime.auth-guard.client") {
89
+ return true;
90
+ }
91
+ return singletons.has(token) || singletonInstances.has(token);
92
+ },
93
+ make(token) {
94
+ if (token === "jskit.client.vue.app") {
95
+ return vueApp;
96
+ }
97
+ if (token === "jskit.client.pinia") {
98
+ return pinia;
99
+ }
100
+ if (token === "runtime.web-placement.client") {
101
+ return {
102
+ getContext() {
103
+ return Object.freeze({
104
+ surfaceConfig: Object.freeze({
105
+ enabledSurfaceIds: Object.freeze(["home"]),
106
+ defaultSurfaceId: "home",
107
+ surfaces: Object.freeze({
108
+ home: Object.freeze({
109
+ id: "home",
110
+ origin: "",
111
+ pagesRoot: "home"
112
+ }),
113
+ auth: Object.freeze({
114
+ id: "auth",
115
+ origin: "",
116
+ pagesRoot: "auth"
117
+ })
118
+ })
119
+ })
120
+ });
121
+ }
122
+ };
123
+ }
124
+ if (token === "runtime.auth-guard.client") {
125
+ return authGuardRuntime;
126
+ }
127
+ if (singletonInstances.has(token)) {
128
+ return singletonInstances.get(token);
129
+ }
130
+ const factory = singletons.get(token);
131
+ if (!factory) {
132
+ throw new Error(`Unknown token ${String(token)}`);
133
+ }
134
+ const instance = factory(this);
135
+ singletonInstances.set(token, instance);
136
+ return instance;
137
+ }
138
+ };
139
+ }
140
+
141
+ test("auth web client boot binds explicit Pinia store state and raw runtime injection together", async () => {
142
+ const authGuardRuntime = createAuthRuntimeStub({
143
+ authenticated: true,
144
+ username: "ada"
145
+ });
146
+ const app = createAppDouble({ authGuardRuntime });
147
+
148
+ await bootAuthClientProvider(app);
149
+
150
+ const authStore = useAuthStore(app.pinia);
151
+ assert.equal(authStore.runtime, authGuardRuntime);
152
+ assert.equal(authStore.authenticated, true);
153
+ assert.equal(authStore.username, "ada");
154
+ assert.equal(authGuardRuntime.initializeCalls, 1);
155
+
156
+ authGuardRuntime.push({
157
+ authenticated: true,
158
+ username: "grace"
159
+ });
160
+
161
+ assert.equal(authStore.username, "grace");
162
+
163
+ const providedByKey = new Map(app.provided.map((entry) => [entry.key, entry.value]));
164
+ assert.equal(providedByKey.get(AUTH_GUARD_RUNTIME_INJECTION_KEY), authGuardRuntime);
165
+ });
@@ -39,8 +39,8 @@ test("resolveSurfaceLinkTarget builds surface-scoped path for target surfaces",
39
39
  const to = resolveSurfaceLinkTarget({
40
40
  context: createPlacementContext(),
41
41
  surface: "admin",
42
- workspaceSuffix: "/projects",
43
- nonWorkspaceSuffix: "/projects"
42
+ scopedSuffix: "/projects",
43
+ unscopedSuffix: "/projects"
44
44
  });
45
45
 
46
46
  assert.equal(to, "/admin/projects");
@@ -50,8 +50,8 @@ test("resolveSurfaceLinkTarget builds non-workspace path for non-workspace surfa
50
50
  const to = resolveSurfaceLinkTarget({
51
51
  context: createPlacementContext(),
52
52
  surface: "console",
53
- workspaceSuffix: "/projects",
54
- nonWorkspaceSuffix: "/projects"
53
+ scopedSuffix: "/projects",
54
+ unscopedSuffix: "/projects"
55
55
  });
56
56
 
57
57
  assert.equal(to, "/console/projects");
@@ -73,7 +73,7 @@ test("resolveSurfaceLinkTarget no longer requires workspace slug for surface lin
73
73
  surfaceConfig: createPlacementContext().surfaceConfig
74
74
  },
75
75
  surface: "admin",
76
- workspaceSuffix: "/projects"
76
+ scopedSuffix: "/projects"
77
77
  });
78
78
 
79
79
  assert.equal(to, "/admin/projects");
@@ -0,0 +1,95 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { createPinia } from "pinia";
4
+ import { useAuthStore } from "../src/client/stores/useAuthStore.js";
5
+
6
+ function createAuthRuntimeStub(initialState = {}) {
7
+ let state = Object.freeze({
8
+ authenticated: Boolean(initialState.authenticated),
9
+ username: String(initialState.username || ""),
10
+ oauthDefaultProvider: String(initialState.oauthDefaultProvider || ""),
11
+ oauthProviders: Array.isArray(initialState.oauthProviders)
12
+ ? Object.freeze([...initialState.oauthProviders])
13
+ : Object.freeze([])
14
+ });
15
+ const listeners = new Set();
16
+
17
+ return {
18
+ async initialize() {
19
+ return state;
20
+ },
21
+ async refresh() {
22
+ return state;
23
+ },
24
+ getState() {
25
+ return state;
26
+ },
27
+ subscribe(listener) {
28
+ if (typeof listener === "function") {
29
+ listeners.add(listener);
30
+ }
31
+ return () => {
32
+ listeners.delete(listener);
33
+ };
34
+ },
35
+ push(nextState = {}) {
36
+ state = Object.freeze({
37
+ authenticated: Boolean(nextState.authenticated),
38
+ username: String(nextState.username || ""),
39
+ oauthDefaultProvider: String(nextState.oauthDefaultProvider || ""),
40
+ oauthProviders: Array.isArray(nextState.oauthProviders)
41
+ ? Object.freeze([...nextState.oauthProviders])
42
+ : Object.freeze([])
43
+ });
44
+
45
+ for (const listener of listeners) {
46
+ listener(state);
47
+ }
48
+ }
49
+ };
50
+ }
51
+
52
+ test("auth store exposes reactive state and direct runtime methods", async () => {
53
+ const pinia = createPinia();
54
+ const runtime = createAuthRuntimeStub({
55
+ authenticated: false,
56
+ username: ""
57
+ });
58
+ const auth = useAuthStore(pinia);
59
+
60
+ auth.attachRuntime(runtime);
61
+
62
+ assert.equal(auth.runtime, runtime);
63
+ assert.equal(auth.authenticated, false);
64
+ assert.equal(auth.username, "");
65
+ assert.deepEqual(auth.oauthProviders, []);
66
+ assert.equal(auth.oauthDefaultProvider, "");
67
+ assert.equal(auth.getState().authenticated, false);
68
+ assert.equal(await auth.refresh(), runtime.getState());
69
+
70
+ runtime.push({
71
+ authenticated: true,
72
+ username: "ada",
73
+ oauthProviders: [{ id: "google", label: "Google" }],
74
+ oauthDefaultProvider: "google"
75
+ });
76
+
77
+ assert.equal(auth.authenticated, true);
78
+ assert.equal(auth.username, "ada");
79
+ assert.deepEqual(auth.oauthProviders, [{ id: "google", label: "Google" }]);
80
+ assert.equal(auth.oauthDefaultProvider, "google");
81
+ assert.equal(auth.getState().authenticated, true);
82
+
83
+ let observedState = null;
84
+ const unsubscribe = auth.subscribe((nextState) => {
85
+ observedState = nextState;
86
+ });
87
+
88
+ runtime.push({
89
+ authenticated: true,
90
+ username: "grace"
91
+ });
92
+
93
+ assert.equal(observedState?.username, "grace");
94
+ unsubscribe();
95
+ });