@ricardoqmd/auth-vue 0.1.0 → 0.2.0
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/README.md +58 -3
- package/dist/index.cjs +6 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +25 -9
- package/dist/index.d.ts +25 -9
- package/dist/index.js +7 -5
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -5,9 +5,13 @@
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
npm install @ricardoqmd/auth-core @ricardoqmd/auth-keycloak @ricardoqmd/auth-vue vue
|
|
8
|
+
npm install @ricardoqmd/auth-core @ricardoqmd/auth-keycloak @ricardoqmd/auth-vue vue keycloak-js xstate
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
+
> Install `xstate` even though you never import it: `@ricardoqmd/auth-core` and
|
|
12
|
+
> `@xstate/vue` declare it as a peer, and a single shared instance must resolve at
|
|
13
|
+
> the top level (a duplicate copy would break the actor's reactivity).
|
|
14
|
+
|
|
11
15
|
> Scope is SPA / client-only (ADR-012). The plugin eagerly initializes the auth
|
|
12
16
|
> flow on install, which assumes a browser. It is SSR-ready by construction (one
|
|
13
17
|
> actor per app instance, never a module-level singleton) but SSR is not a
|
|
@@ -30,9 +34,14 @@ export const provider = createKeycloakProvider({
|
|
|
30
34
|
realm: import.meta.env.VITE_KC_REALM,
|
|
31
35
|
clientId: import.meta.env.VITE_KC_CLIENT_ID,
|
|
32
36
|
},
|
|
37
|
+
onLoad: "check-sso",
|
|
33
38
|
});
|
|
34
39
|
```
|
|
35
40
|
|
|
41
|
+
> `check-sso` boots the app without forcing a login — anonymous users land on
|
|
42
|
+
> your UI and sign in on demand. Use `login-required` instead to redirect
|
|
43
|
+
> straight to Keycloak before the app renders.
|
|
44
|
+
|
|
36
45
|
### 2. Install the plugin
|
|
37
46
|
|
|
38
47
|
```ts
|
|
@@ -118,12 +127,54 @@ const { isLoading, error, isAuthenticated } = useAuth();
|
|
|
118
127
|
</template>
|
|
119
128
|
```
|
|
120
129
|
|
|
130
|
+
### Route guards (imperative)
|
|
131
|
+
|
|
132
|
+
`useAuth()` is for components — it is reactive and **throws outside `setup()`**.
|
|
133
|
+
For code that runs outside the render tree (a vue-router `beforeEach` guard, an
|
|
134
|
+
HTTP interceptor), use the value returned by `createAuth(...)`: it is BOTH a Vue
|
|
135
|
+
plugin AND an imperative `AuthHandle`. The same object you install with
|
|
136
|
+
`app.use()` exposes synchronous accessors.
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
// main.ts
|
|
140
|
+
import { createApp } from "vue";
|
|
141
|
+
import { createAuth } from "@ricardoqmd/auth-vue";
|
|
142
|
+
import { router } from "./router";
|
|
143
|
+
import App from "./App.vue";
|
|
144
|
+
import { provider } from "./auth";
|
|
145
|
+
|
|
146
|
+
const auth = createAuth({ provider });
|
|
147
|
+
const app = createApp(App);
|
|
148
|
+
app.use(auth);
|
|
149
|
+
app.use(router);
|
|
150
|
+
|
|
151
|
+
router.beforeEach(async (to) => {
|
|
152
|
+
await auth.whenReady(); // wait out the first-navigation init race
|
|
153
|
+
if (to.meta.requiresAuth && !auth.isAuthenticated()) return { name: "login" };
|
|
154
|
+
if (to.meta.roles && !auth.hasAnyRole(to.meta.roles as string[])) {
|
|
155
|
+
return { name: "forbidden" };
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
app.mount("#app");
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
`auth.whenReady()` resolves once `init()` settles (authenticated, unauthenticated,
|
|
163
|
+
or error), so the first navigation does not race a pending initialization. The
|
|
164
|
+
handle also exposes `isAuthenticated()`, `isLoading()`, `getToken()`, `getUser()`,
|
|
165
|
+
`getIdpClaims()`, `getError()`, `hasRole()`, `hasAnyRole()`, and `subscribe()`.
|
|
166
|
+
Use `useAuth()` in components (reactive); use the handle in guards/interceptors
|
|
167
|
+
(imperative).
|
|
168
|
+
|
|
121
169
|
## API
|
|
122
170
|
|
|
123
171
|
### `createAuth(options)`
|
|
124
172
|
|
|
125
|
-
Returns a Vue `Plugin
|
|
126
|
-
|
|
173
|
+
Returns a value that is BOTH a Vue `Plugin` AND an `AuthHandle<TIdpClaims>`.
|
|
174
|
+
Install it with `app.use(createAuth({ provider }))`; the same object is usable
|
|
175
|
+
imperatively outside components (see [Route guards](#route-guards-imperative)).
|
|
176
|
+
Creates one auth actor per call, starts it, sends `INIT`, and provides it
|
|
177
|
+
app-wide.
|
|
127
178
|
|
|
128
179
|
| Option | Type | Description |
|
|
129
180
|
|---|---|---|
|
|
@@ -161,6 +212,10 @@ currently emitted by `auth-keycloak` — a dead refresh token rejects as
|
|
|
161
212
|
**Pre-1.0.** The public API mirrors `@ricardoqmd/auth-nextjs` and shares the
|
|
162
213
|
`@ricardoqmd/auth-core` contract.
|
|
163
214
|
|
|
215
|
+
`@ricardoqmd/auth-vue` is `0.x` and versions **independently** from
|
|
216
|
+
`@ricardoqmd/auth-core` and `@ricardoqmd/auth-keycloak` (ADR-013): its version
|
|
217
|
+
does not track theirs, and the API is not frozen until `1.0`.
|
|
218
|
+
|
|
164
219
|
## License
|
|
165
220
|
|
|
166
221
|
MIT © [ricardoqmd](https://github.com/ricardoqmd)
|
package/dist/index.cjs
CHANGED
|
@@ -12,12 +12,14 @@ var AUTH_INJECTION_KEY = /* @__PURE__ */ Symbol("ricardoqmd-auth");
|
|
|
12
12
|
|
|
13
13
|
// src/plugin.ts
|
|
14
14
|
function createAuth(options) {
|
|
15
|
+
const machine = authCore.createAuthMachine(options.provider);
|
|
16
|
+
const actor = xstate.createActor(machine);
|
|
17
|
+
actor.start();
|
|
18
|
+
actor.send({ type: "INIT" });
|
|
19
|
+
const handle = authCore.createAuthHandle(actor);
|
|
15
20
|
return {
|
|
21
|
+
...handle,
|
|
16
22
|
install(app) {
|
|
17
|
-
const machine = authCore.createAuthMachine(options.provider);
|
|
18
|
-
const actor = xstate.createActor(machine);
|
|
19
|
-
actor.start();
|
|
20
|
-
actor.send({ type: "INIT" });
|
|
21
23
|
app.provide(AUTH_INJECTION_KEY, actor);
|
|
22
24
|
const unmountable = app;
|
|
23
25
|
if (typeof unmountable.onUnmount === "function") {
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/injection-key.ts","../src/plugin.ts","../src/composable.ts"],"names":["createAuthMachine","createActor","inject","useSelector","computed"],"mappings":";;;;;;;;;;AAUO,IAAM,kBAAA,0BAAqD,iBAAiB,CAAA;;;
|
|
1
|
+
{"version":3,"sources":["../src/injection-key.ts","../src/plugin.ts","../src/composable.ts"],"names":["createAuthMachine","createActor","createAuthHandle","inject","useSelector","computed"],"mappings":"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","file":"index.cjs","sourcesContent":["import type { InjectionKey } from \"vue\";\nimport type { Actor } from \"xstate\";\nimport type { createAuthMachine } from \"@ricardoqmd/auth-core\";\n\n/** The started auth actor type, derived from the core machine factory. */\nexport type AuthActor<TIdpClaims = unknown> = Actor<\n ReturnType<typeof createAuthMachine<TIdpClaims>>\n>;\n\n/** App-wide injection key for the auth actor created by the createAuth plugin. */\nexport const AUTH_INJECTION_KEY: InjectionKey<AuthActor> = Symbol(\"ricardoqmd-auth\");\n","import type { App, Plugin } from \"vue\";\nimport { createActor } from \"xstate\";\nimport {\n createAuthHandle,\n createAuthMachine,\n type AuthHandle,\n type AuthProvider,\n} from \"@ricardoqmd/auth-core\";\nimport { AUTH_INJECTION_KEY, type AuthActor } from \"./injection-key.js\";\n\nexport interface CreateAuthOptions<TIdpClaims = unknown> {\n /** Adapter instance (e.g. createKeycloakProvider()). IDP-agnostic. */\n provider: AuthProvider<TIdpClaims>;\n}\n\n/**\n * Wire @ricardoqmd/auth-core into a Vue app.\n *\n * Returns a value that is BOTH a Vue plugin and an imperative AuthHandle:\n * - `app.use(auth)` installs it (provides the actor for useAuth()).\n * - the same `auth` object exposes whenReady()/isAuthenticated()/hasRole()/... for\n * use OUTSIDE components — a vue-router `beforeEach` guard or an HTTP interceptor,\n * where useAuth() cannot run.\n *\n * Creates ONE actor per call (SSR-safe: never a module-level singleton). The actor\n * is started and initialized here, so the handle is usable before mount (guards run\n * before the app renders). Scope is SPA/client-only (ADR-012): the eager INIT\n * assumes a browser; SSR-ready by construction but not a supported target in 0.x.\n *\n * @example\n * const auth = createAuth({ provider });\n * app.use(auth);\n * router.beforeEach(async (to) => {\n * await auth.whenReady();\n * if (to.meta.requiresAuth && !auth.isAuthenticated()) return { name: \"login\" };\n * if (to.meta.roles && !auth.hasAnyRole(to.meta.roles as string[])) {\n * return { name: \"forbidden\" };\n * }\n * });\n */\nexport function createAuth<TIdpClaims = unknown>(\n options: CreateAuthOptions<TIdpClaims>,\n): Plugin & AuthHandle<TIdpClaims> {\n const machine = createAuthMachine<TIdpClaims>(options.provider);\n const actor = createActor(machine);\n actor.start();\n actor.send({ type: \"INIT\" });\n const handle = createAuthHandle<TIdpClaims>(actor);\n\n return {\n ...handle,\n install(app: App) {\n app.provide(AUTH_INJECTION_KEY, actor as AuthActor);\n // Stop the actor when the app unmounts (avoids leaks in tests/HMR).\n // app.onUnmount requires Vue 3.5+; guard so older runtimes still install.\n const unmountable = app as App & { onUnmount?: (cb: () => void) => void };\n if (typeof unmountable.onUnmount === \"function\") {\n unmountable.onUnmount(() => actor.stop());\n }\n },\n };\n}\n","import { computed, inject, type ComputedRef } from \"vue\";\nimport { useSelector } from \"@xstate/vue\";\nimport type { AuthError, AuthUserClaims } from \"@ricardoqmd/auth-core\";\nimport { AUTH_INJECTION_KEY } from \"./injection-key.js\";\n\n/** Reactive auth state + helpers returned by useAuth(). */\nexport interface AuthState<TIdpClaims = unknown> {\n isLoading: ComputedRef<boolean>;\n isAuthenticated: ComputedRef<boolean>;\n token: ComputedRef<string | null>;\n user: ComputedRef<AuthUserClaims | null>;\n idpClaims: ComputedRef<TIdpClaims | null>;\n error: ComputedRef<AuthError | null>;\n login: () => void;\n logout: () => void;\n hasRole: (role: string) => boolean;\n hasAnyRole: (roles: string[]) => boolean;\n}\n\n/**\n * Reactive authentication state for the current app.\n * Must be called inside a component of an app that installed createAuth().\n */\nexport function useAuth<TIdpClaims = unknown>(): AuthState<TIdpClaims> {\n const actor = inject(AUTH_INJECTION_KEY);\n if (!actor) {\n throw new Error(\n \"useAuth() must be called within an app that installed the auth plugin. \" +\n \"Did you forget app.use(createAuth({ provider }))?\",\n );\n }\n\n const stateValue = useSelector(actor, (s) => s.value);\n const context = useSelector(actor, (s) => s.context);\n\n const isAuthenticated = computed(\n () =>\n typeof stateValue.value === \"object\" &&\n stateValue.value !== null &&\n \"authenticated\" in stateValue.value,\n );\n\n const topState = computed(() =>\n typeof stateValue.value === \"string\"\n ? stateValue.value\n : Object.keys(stateValue.value)[0],\n );\n\n const isLoading = computed(\n () => topState.value === \"initializing\" || topState.value === \"loggingOut\",\n );\n\n const token = computed(() => context.value.token);\n const user = computed(() => context.value.user);\n const idpClaims = computed(() => context.value.idpClaims as TIdpClaims | null);\n const error = computed(() => context.value.error);\n\n const login = () => actor.send({ type: \"LOGIN\" });\n const logout = () => actor.send({ type: \"LOGOUT\" });\n\n const hasRole = (role: string) =>\n user.value?.roles?.includes(role) ?? false;\n const hasAnyRole = (roles: string[]) =>\n roles.some((r) => user.value?.roles?.includes(r) ?? false);\n\n return {\n isLoading,\n isAuthenticated,\n token,\n user,\n idpClaims,\n error,\n login,\n logout,\n hasRole,\n hasAnyRole,\n };\n}\n"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Plugin, ComputedRef } from 'vue';
|
|
2
|
-
import { AuthProvider, AuthUserClaims, AuthError, createAuthMachine } from '@ricardoqmd/auth-core';
|
|
3
|
-
export { AuthError, AuthInitResult, AuthProvider, AuthTokens, AuthUserClaims, LogoutOptions } from '@ricardoqmd/auth-core';
|
|
2
|
+
import { AuthProvider, AuthHandle, AuthUserClaims, AuthError, createAuthMachine } from '@ricardoqmd/auth-core';
|
|
3
|
+
export { AuthError, AuthHandle, AuthInitResult, AuthProvider, AuthTokens, AuthUserClaims, LogoutOptions } from '@ricardoqmd/auth-core';
|
|
4
4
|
import { Actor } from 'xstate';
|
|
5
5
|
|
|
6
6
|
interface CreateAuthOptions<TIdpClaims = unknown> {
|
|
@@ -8,15 +8,31 @@ interface CreateAuthOptions<TIdpClaims = unknown> {
|
|
|
8
8
|
provider: AuthProvider<TIdpClaims>;
|
|
9
9
|
}
|
|
10
10
|
/**
|
|
11
|
-
*
|
|
11
|
+
* Wire @ricardoqmd/auth-core into a Vue app.
|
|
12
12
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
13
|
+
* Returns a value that is BOTH a Vue plugin and an imperative AuthHandle:
|
|
14
|
+
* - `app.use(auth)` installs it (provides the actor for useAuth()).
|
|
15
|
+
* - the same `auth` object exposes whenReady()/isAuthenticated()/hasRole()/... for
|
|
16
|
+
* use OUTSIDE components — a vue-router `beforeEach` guard or an HTTP interceptor,
|
|
17
|
+
* where useAuth() cannot run.
|
|
18
|
+
*
|
|
19
|
+
* Creates ONE actor per call (SSR-safe: never a module-level singleton). The actor
|
|
20
|
+
* is started and initialized here, so the handle is usable before mount (guards run
|
|
21
|
+
* before the app renders). Scope is SPA/client-only (ADR-012): the eager INIT
|
|
22
|
+
* assumes a browser; SSR-ready by construction but not a supported target in 0.x.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* const auth = createAuth({ provider });
|
|
26
|
+
* app.use(auth);
|
|
27
|
+
* router.beforeEach(async (to) => {
|
|
28
|
+
* await auth.whenReady();
|
|
29
|
+
* if (to.meta.requiresAuth && !auth.isAuthenticated()) return { name: "login" };
|
|
30
|
+
* if (to.meta.roles && !auth.hasAnyRole(to.meta.roles as string[])) {
|
|
31
|
+
* return { name: "forbidden" };
|
|
32
|
+
* }
|
|
33
|
+
* });
|
|
18
34
|
*/
|
|
19
|
-
declare function createAuth<TIdpClaims = unknown>(options: CreateAuthOptions<TIdpClaims>): Plugin
|
|
35
|
+
declare function createAuth<TIdpClaims = unknown>(options: CreateAuthOptions<TIdpClaims>): Plugin & AuthHandle<TIdpClaims>;
|
|
20
36
|
|
|
21
37
|
/** Reactive auth state + helpers returned by useAuth(). */
|
|
22
38
|
interface AuthState<TIdpClaims = unknown> {
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Plugin, ComputedRef } from 'vue';
|
|
2
|
-
import { AuthProvider, AuthUserClaims, AuthError, createAuthMachine } from '@ricardoqmd/auth-core';
|
|
3
|
-
export { AuthError, AuthInitResult, AuthProvider, AuthTokens, AuthUserClaims, LogoutOptions } from '@ricardoqmd/auth-core';
|
|
2
|
+
import { AuthProvider, AuthHandle, AuthUserClaims, AuthError, createAuthMachine } from '@ricardoqmd/auth-core';
|
|
3
|
+
export { AuthError, AuthHandle, AuthInitResult, AuthProvider, AuthTokens, AuthUserClaims, LogoutOptions } from '@ricardoqmd/auth-core';
|
|
4
4
|
import { Actor } from 'xstate';
|
|
5
5
|
|
|
6
6
|
interface CreateAuthOptions<TIdpClaims = unknown> {
|
|
@@ -8,15 +8,31 @@ interface CreateAuthOptions<TIdpClaims = unknown> {
|
|
|
8
8
|
provider: AuthProvider<TIdpClaims>;
|
|
9
9
|
}
|
|
10
10
|
/**
|
|
11
|
-
*
|
|
11
|
+
* Wire @ricardoqmd/auth-core into a Vue app.
|
|
12
12
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
13
|
+
* Returns a value that is BOTH a Vue plugin and an imperative AuthHandle:
|
|
14
|
+
* - `app.use(auth)` installs it (provides the actor for useAuth()).
|
|
15
|
+
* - the same `auth` object exposes whenReady()/isAuthenticated()/hasRole()/... for
|
|
16
|
+
* use OUTSIDE components — a vue-router `beforeEach` guard or an HTTP interceptor,
|
|
17
|
+
* where useAuth() cannot run.
|
|
18
|
+
*
|
|
19
|
+
* Creates ONE actor per call (SSR-safe: never a module-level singleton). The actor
|
|
20
|
+
* is started and initialized here, so the handle is usable before mount (guards run
|
|
21
|
+
* before the app renders). Scope is SPA/client-only (ADR-012): the eager INIT
|
|
22
|
+
* assumes a browser; SSR-ready by construction but not a supported target in 0.x.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* const auth = createAuth({ provider });
|
|
26
|
+
* app.use(auth);
|
|
27
|
+
* router.beforeEach(async (to) => {
|
|
28
|
+
* await auth.whenReady();
|
|
29
|
+
* if (to.meta.requiresAuth && !auth.isAuthenticated()) return { name: "login" };
|
|
30
|
+
* if (to.meta.roles && !auth.hasAnyRole(to.meta.roles as string[])) {
|
|
31
|
+
* return { name: "forbidden" };
|
|
32
|
+
* }
|
|
33
|
+
* });
|
|
18
34
|
*/
|
|
19
|
-
declare function createAuth<TIdpClaims = unknown>(options: CreateAuthOptions<TIdpClaims>): Plugin
|
|
35
|
+
declare function createAuth<TIdpClaims = unknown>(options: CreateAuthOptions<TIdpClaims>): Plugin & AuthHandle<TIdpClaims>;
|
|
20
36
|
|
|
21
37
|
/** Reactive auth state + helpers returned by useAuth(). */
|
|
22
38
|
interface AuthState<TIdpClaims = unknown> {
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createActor } from 'xstate';
|
|
2
|
-
import { createAuthMachine } from '@ricardoqmd/auth-core';
|
|
2
|
+
import { createAuthMachine, createAuthHandle } from '@ricardoqmd/auth-core';
|
|
3
3
|
import { inject, computed } from 'vue';
|
|
4
4
|
import { useSelector } from '@xstate/vue';
|
|
5
5
|
|
|
@@ -10,12 +10,14 @@ var AUTH_INJECTION_KEY = /* @__PURE__ */ Symbol("ricardoqmd-auth");
|
|
|
10
10
|
|
|
11
11
|
// src/plugin.ts
|
|
12
12
|
function createAuth(options) {
|
|
13
|
+
const machine = createAuthMachine(options.provider);
|
|
14
|
+
const actor = createActor(machine);
|
|
15
|
+
actor.start();
|
|
16
|
+
actor.send({ type: "INIT" });
|
|
17
|
+
const handle = createAuthHandle(actor);
|
|
13
18
|
return {
|
|
19
|
+
...handle,
|
|
14
20
|
install(app) {
|
|
15
|
-
const machine = createAuthMachine(options.provider);
|
|
16
|
-
const actor = createActor(machine);
|
|
17
|
-
actor.start();
|
|
18
|
-
actor.send({ type: "INIT" });
|
|
19
21
|
app.provide(AUTH_INJECTION_KEY, actor);
|
|
20
22
|
const unmountable = app;
|
|
21
23
|
if (typeof unmountable.onUnmount === "function") {
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/injection-key.ts","../src/plugin.ts","../src/composable.ts"],"names":[],"mappings":";;;;;;;;AAUO,IAAM,kBAAA,0BAAqD,iBAAiB,CAAA;;;
|
|
1
|
+
{"version":3,"sources":["../src/injection-key.ts","../src/plugin.ts","../src/composable.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import type { InjectionKey } from \"vue\";\nimport type { Actor } from \"xstate\";\nimport type { createAuthMachine } from \"@ricardoqmd/auth-core\";\n\n/** The started auth actor type, derived from the core machine factory. */\nexport type AuthActor<TIdpClaims = unknown> = Actor<\n ReturnType<typeof createAuthMachine<TIdpClaims>>\n>;\n\n/** App-wide injection key for the auth actor created by the createAuth plugin. */\nexport const AUTH_INJECTION_KEY: InjectionKey<AuthActor> = Symbol(\"ricardoqmd-auth\");\n","import type { App, Plugin } from \"vue\";\nimport { createActor } from \"xstate\";\nimport {\n createAuthHandle,\n createAuthMachine,\n type AuthHandle,\n type AuthProvider,\n} from \"@ricardoqmd/auth-core\";\nimport { AUTH_INJECTION_KEY, type AuthActor } from \"./injection-key.js\";\n\nexport interface CreateAuthOptions<TIdpClaims = unknown> {\n /** Adapter instance (e.g. createKeycloakProvider()). IDP-agnostic. */\n provider: AuthProvider<TIdpClaims>;\n}\n\n/**\n * Wire @ricardoqmd/auth-core into a Vue app.\n *\n * Returns a value that is BOTH a Vue plugin and an imperative AuthHandle:\n * - `app.use(auth)` installs it (provides the actor for useAuth()).\n * - the same `auth` object exposes whenReady()/isAuthenticated()/hasRole()/... for\n * use OUTSIDE components — a vue-router `beforeEach` guard or an HTTP interceptor,\n * where useAuth() cannot run.\n *\n * Creates ONE actor per call (SSR-safe: never a module-level singleton). The actor\n * is started and initialized here, so the handle is usable before mount (guards run\n * before the app renders). Scope is SPA/client-only (ADR-012): the eager INIT\n * assumes a browser; SSR-ready by construction but not a supported target in 0.x.\n *\n * @example\n * const auth = createAuth({ provider });\n * app.use(auth);\n * router.beforeEach(async (to) => {\n * await auth.whenReady();\n * if (to.meta.requiresAuth && !auth.isAuthenticated()) return { name: \"login\" };\n * if (to.meta.roles && !auth.hasAnyRole(to.meta.roles as string[])) {\n * return { name: \"forbidden\" };\n * }\n * });\n */\nexport function createAuth<TIdpClaims = unknown>(\n options: CreateAuthOptions<TIdpClaims>,\n): Plugin & AuthHandle<TIdpClaims> {\n const machine = createAuthMachine<TIdpClaims>(options.provider);\n const actor = createActor(machine);\n actor.start();\n actor.send({ type: \"INIT\" });\n const handle = createAuthHandle<TIdpClaims>(actor);\n\n return {\n ...handle,\n install(app: App) {\n app.provide(AUTH_INJECTION_KEY, actor as AuthActor);\n // Stop the actor when the app unmounts (avoids leaks in tests/HMR).\n // app.onUnmount requires Vue 3.5+; guard so older runtimes still install.\n const unmountable = app as App & { onUnmount?: (cb: () => void) => void };\n if (typeof unmountable.onUnmount === \"function\") {\n unmountable.onUnmount(() => actor.stop());\n }\n },\n };\n}\n","import { computed, inject, type ComputedRef } from \"vue\";\nimport { useSelector } from \"@xstate/vue\";\nimport type { AuthError, AuthUserClaims } from \"@ricardoqmd/auth-core\";\nimport { AUTH_INJECTION_KEY } from \"./injection-key.js\";\n\n/** Reactive auth state + helpers returned by useAuth(). */\nexport interface AuthState<TIdpClaims = unknown> {\n isLoading: ComputedRef<boolean>;\n isAuthenticated: ComputedRef<boolean>;\n token: ComputedRef<string | null>;\n user: ComputedRef<AuthUserClaims | null>;\n idpClaims: ComputedRef<TIdpClaims | null>;\n error: ComputedRef<AuthError | null>;\n login: () => void;\n logout: () => void;\n hasRole: (role: string) => boolean;\n hasAnyRole: (roles: string[]) => boolean;\n}\n\n/**\n * Reactive authentication state for the current app.\n * Must be called inside a component of an app that installed createAuth().\n */\nexport function useAuth<TIdpClaims = unknown>(): AuthState<TIdpClaims> {\n const actor = inject(AUTH_INJECTION_KEY);\n if (!actor) {\n throw new Error(\n \"useAuth() must be called within an app that installed the auth plugin. \" +\n \"Did you forget app.use(createAuth({ provider }))?\",\n );\n }\n\n const stateValue = useSelector(actor, (s) => s.value);\n const context = useSelector(actor, (s) => s.context);\n\n const isAuthenticated = computed(\n () =>\n typeof stateValue.value === \"object\" &&\n stateValue.value !== null &&\n \"authenticated\" in stateValue.value,\n );\n\n const topState = computed(() =>\n typeof stateValue.value === \"string\"\n ? stateValue.value\n : Object.keys(stateValue.value)[0],\n );\n\n const isLoading = computed(\n () => topState.value === \"initializing\" || topState.value === \"loggingOut\",\n );\n\n const token = computed(() => context.value.token);\n const user = computed(() => context.value.user);\n const idpClaims = computed(() => context.value.idpClaims as TIdpClaims | null);\n const error = computed(() => context.value.error);\n\n const login = () => actor.send({ type: \"LOGIN\" });\n const logout = () => actor.send({ type: \"LOGOUT\" });\n\n const hasRole = (role: string) =>\n user.value?.roles?.includes(role) ?? false;\n const hasAnyRole = (roles: string[]) =>\n roles.some((r) => user.value?.roles?.includes(r) ?? false);\n\n return {\n isLoading,\n isAuthenticated,\n token,\n user,\n idpClaims,\n error,\n login,\n logout,\n hasRole,\n hasAnyRole,\n };\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ricardoqmd/auth-vue",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Vue 3 client-side bindings for @ricardoqmd/auth-core — createAuth plugin, useAuth composable, RBAC helpers",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"auth",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"@xstate/vue": "^5.0.0"
|
|
41
41
|
},
|
|
42
42
|
"peerDependencies": {
|
|
43
|
-
"@ricardoqmd/auth-core": "^1.
|
|
43
|
+
"@ricardoqmd/auth-core": "^1.1.0",
|
|
44
44
|
"vue": "^3.5.0",
|
|
45
45
|
"xstate": "^5.20.0"
|
|
46
46
|
},
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"tsup": "^8.3.5",
|
|
53
53
|
"typescript": "^5.6.3",
|
|
54
54
|
"vitest": "^4.1.5",
|
|
55
|
-
"@ricardoqmd/auth-core": "1.
|
|
55
|
+
"@ricardoqmd/auth-core": "^1.1.0"
|
|
56
56
|
},
|
|
57
57
|
"publishConfig": {
|
|
58
58
|
"access": "public"
|