@zephkelly/nuxt-authkit 0.0.1

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 (75) hide show
  1. package/README.md +170 -0
  2. package/dist/module.d.mts +6 -0
  3. package/dist/module.json +9 -0
  4. package/dist/module.mjs +80 -0
  5. package/dist/runtime/composables/useAuthkitToken.d.ts +25 -0
  6. package/dist/runtime/composables/useAuthkitToken.js +31 -0
  7. package/dist/runtime/helpers/request-middleware.d.ts +22 -0
  8. package/dist/runtime/helpers/request-middleware.js +82 -0
  9. package/dist/runtime/index.d.ts +8 -0
  10. package/dist/runtime/index.js +2 -0
  11. package/dist/runtime/plugins/auth-fetch.client.d.ts +13 -0
  12. package/dist/runtime/plugins/auth-fetch.client.js +91 -0
  13. package/dist/runtime/plugins/internal/request-target.d.ts +16 -0
  14. package/dist/runtime/plugins/internal/request-target.js +17 -0
  15. package/dist/runtime/roles/has-role.d.ts +21 -0
  16. package/dist/runtime/roles/has-role.js +51 -0
  17. package/dist/runtime/server/api/_auth/refresh-handler.d.ts +6 -0
  18. package/dist/runtime/server/api/_auth/refresh-handler.js +44 -0
  19. package/dist/runtime/server/api/_auth/refresh.post.d.ts +6 -0
  20. package/dist/runtime/server/api/_auth/refresh.post.js +5 -0
  21. package/dist/runtime/server/tsconfig.json +3 -0
  22. package/dist/runtime/service/index.d.ts +119 -0
  23. package/dist/runtime/service/index.js +170 -0
  24. package/dist/runtime/service/new-index.d.ts +24 -0
  25. package/dist/runtime/service/new-index.js +0 -0
  26. package/dist/runtime/service/types/async-service.d.ts +59 -0
  27. package/dist/runtime/service/types/async-service.js +0 -0
  28. package/dist/runtime/service/types/sync-service.d.ts +28 -0
  29. package/dist/runtime/service/types/sync-service.js +0 -0
  30. package/dist/runtime/service/utils/getJwtAsyncService.d.ts +11 -0
  31. package/dist/runtime/service/utils/getJwtAsyncService.js +16 -0
  32. package/dist/runtime/storage/nitro.d.ts +61 -0
  33. package/dist/runtime/storage/nitro.js +124 -0
  34. package/dist/runtime/storage/types.d.ts +27 -0
  35. package/dist/runtime/storage/types.js +0 -0
  36. package/dist/runtime/strategy/base.d.ts +40 -0
  37. package/dist/runtime/strategy/base.js +88 -0
  38. package/dist/runtime/strategy/jwt/callback.d.ts +60 -0
  39. package/dist/runtime/strategy/jwt/callback.js +0 -0
  40. package/dist/runtime/strategy/jwt/index.d.ts +137 -0
  41. package/dist/runtime/strategy/jwt/index.js +748 -0
  42. package/dist/runtime/strategy/jwt/jwt-crypto.d.ts +122 -0
  43. package/dist/runtime/strategy/jwt/jwt-crypto.js +298 -0
  44. package/dist/runtime/strategy/jwt/types.d.ts +68 -0
  45. package/dist/runtime/strategy/jwt/types.js +0 -0
  46. package/dist/runtime/strategy/types.d.ts +68 -0
  47. package/dist/runtime/strategy/types.js +0 -0
  48. package/dist/runtime/types/2fac.d.ts +32 -0
  49. package/dist/runtime/types/2fac.js +0 -0
  50. package/dist/runtime/types/authkit-types.d.ts +6 -0
  51. package/dist/runtime/types/authkit-types.js +0 -0
  52. package/dist/runtime/types/callbacks.d.ts +133 -0
  53. package/dist/runtime/types/callbacks.js +0 -0
  54. package/dist/runtime/types/cookie.d.ts +47 -0
  55. package/dist/runtime/types/cookie.js +0 -0
  56. package/dist/runtime/types/expand.d.ts +3 -0
  57. package/dist/runtime/types/expand.js +0 -0
  58. package/dist/runtime/types/index.d.ts +52 -0
  59. package/dist/runtime/types/index.js +0 -0
  60. package/dist/runtime/types/module-options.d.ts +33 -0
  61. package/dist/runtime/types/module-options.js +0 -0
  62. package/dist/runtime/types/runtime-config.d.ts +2 -0
  63. package/dist/runtime/types/runtime-config.js +47 -0
  64. package/dist/runtime/types/token.d.ts +42 -0
  65. package/dist/runtime/types/token.js +12 -0
  66. package/dist/runtime/utils/context-helpers.d.ts +4 -0
  67. package/dist/runtime/utils/context-helpers.js +10 -0
  68. package/dist/runtime/utils/get-module-options.d.ts +2 -0
  69. package/dist/runtime/utils/get-module-options.js +18 -0
  70. package/dist/runtime/utils/password.d.ts +23 -0
  71. package/dist/runtime/utils/password.js +18 -0
  72. package/dist/runtime/utils/uuid.d.ts +187 -0
  73. package/dist/runtime/utils/uuid.js +345 -0
  74. package/dist/types.d.mts +7 -0
  75. package/package.json +65 -0
package/README.md ADDED
@@ -0,0 +1,170 @@
1
+ # @zephkelly/nuxt-authkit
2
+
3
+ An authentication toolkit for Nuxt applications. JWT access/refresh tokens with
4
+ rotating refresh tokens, HttpOnly cookie storage, role guards, and scrypt password
5
+ hashing.
6
+
7
+ > **Upgrading from 0.0.x?** Read [MIGRATION.md](./MIGRATION.md) first. This release
8
+ > **logs every user out once** — tokens minted by 0.0.x carry no `typ` claim and are
9
+ > now rejected — and three changes need action in consuming apps.
10
+
11
+ ## Setup
12
+
13
+ ```ts
14
+ // nuxt.config.ts
15
+ export default defineNuxtConfig({
16
+ modules: ['@zephkelly/nuxt-authkit'],
17
+
18
+ nuxtAuthkit: {
19
+ strategies: ['jwt'],
20
+ strategy: {
21
+ name: 'jwt',
22
+ tokens: {
23
+ access: { expiresIn: 900 }, // 15 minutes
24
+ refresh: {
25
+ expiresIn: 604800, // 7 days
26
+ rotate: true, // rotate on every refresh (default)
27
+ cookie: {
28
+ name: 'refreshToken',
29
+ secure: true,
30
+ httpOnly: true, // forced on regardless
31
+ sameSite: 'strict'
32
+ }
33
+ }
34
+ },
35
+ jwt: {
36
+ algorithm: 'RS256',
37
+ privateKey: process.env.JWT_PRIVATE_KEY!,
38
+ publicKey: process.env.JWT_PUBLIC_KEY!
39
+ }
40
+ }
41
+ }
42
+ });
43
+ ```
44
+
45
+ Generate an RSA key pair:
46
+
47
+ ```bash
48
+ openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:3072 -out private.pem
49
+ openssl rsa -pubout -in private.pem -out public.pem
50
+ ```
51
+
52
+ Keep the private key in `JWT_PRIVATE_KEY` (or `NUXT_NUXTAUTHKIT_STRATEGY_JWT_PRIVATE_KEY`)
53
+ — it belongs in the environment, never in a committed config or a published build
54
+ output.
55
+
56
+ ## Wiring it up
57
+
58
+ Register the strategy in a Nitro plugin. The callbacks are where your app plugs in
59
+ its own storage and user directory.
60
+
61
+ ```ts
62
+ // server/plugins/auth.ts
63
+ export default defineNitroPlugin(() => {
64
+ createNuxtAuthkit(
65
+ createJWTStrategy({
66
+ // Required: verify credentials.
67
+ async onAuthenticate({ email, password }) {
68
+ const user = await db.users.findByEmail(email);
69
+ if (!user || !await verifyPassword(user.passwordHash, password)) {
70
+ return null;
71
+ }
72
+ return { id: user.id, roles: user.roles };
73
+ },
74
+
75
+ // Required: persist a refresh token. Store a HASH, not the token —
76
+ // then a leaked database snapshot is not a set of live sessions.
77
+ async onStoreRefreshToken(token, userId) {
78
+ await db.refreshTokens.insert({ userId, hash: sha256(token) });
79
+ },
80
+
81
+ // Required: look one up.
82
+ async onGetRefreshToken(token, userId) {
83
+ const record = await db.refreshTokens.find(sha256(token));
84
+ if (!record) return { valid: false };
85
+
86
+ // Rotated away already but presented again → the token was stolen.
87
+ // authkit responds by revoking the whole family via onRevokeTokenFamily.
88
+ // Allow a short grace period, or concurrent refreshes log users out.
89
+ if (record.consumedAt) {
90
+ const age = Date.now() - record.consumedAt;
91
+ return age < 10_000 ? { valid: true } : { valid: false, reused: true };
92
+ }
93
+
94
+ // Roles read fresh from the DB, so a ban applies on the next refresh
95
+ // rather than whenever the user next logs in.
96
+ return { valid: true, roles: await db.users.rolesOf(userId) };
97
+ },
98
+
99
+ // Strongly recommended: without this, logout cannot terminate a session
100
+ // server-side and rotation never invalidates the old token.
101
+ async onRemoveRefreshToken(token) {
102
+ await db.refreshTokens.markConsumed(sha256(token));
103
+ },
104
+
105
+ // Recommended: theft response.
106
+ async onRevokeTokenFamily(family) {
107
+ await db.refreshTokens.revokeFamily(family);
108
+ },
109
+
110
+ // Recommended: admin force-logout. revokeServer() throws without it.
111
+ async onRevokeServer({ userId }) {
112
+ await db.refreshTokens.deleteAllForUser(userId);
113
+ }
114
+ })
115
+ );
116
+ });
117
+ ```
118
+
119
+ The library warns at startup about any recommended callback you have not
120
+ implemented, and what the consequence is.
121
+
122
+ ## Protecting routes
123
+
124
+ ```ts
125
+ export default defineEventHandler({
126
+ onRequest: [
127
+ () => hasAuthkitSession(), // 401 without a valid session
128
+ () => hasAuthkitRole('admin'), // 401 unauthenticated, 403 without the role
129
+ () => rejectAuthkitRole('banned') // deny-filter; also 401 unauthenticated
130
+ ],
131
+ handler: async () => { /* ... */ }
132
+ });
133
+ ```
134
+
135
+ `rejectAuthkitRole` is a deny-filter, **not** an authentication gate. It requires a
136
+ valid session: proving a caller does *not* hold a role first requires knowing who
137
+ they are.
138
+
139
+ ## Tokens
140
+
141
+ `authenticate()` sets the refresh token as an HttpOnly cookie and returns the
142
+ access token. **Never put the refresh token in a response body** — from there it
143
+ reaches JavaScript, and it is a multi-day credential.
144
+
145
+ Access and refresh tokens carry a signed `typ` claim, so a refresh token cannot be
146
+ replayed as a Bearer access token.
147
+
148
+ On the client, the shipped plugin attaches the access token to same-origin requests
149
+ and transparently refreshes + retries once on a 401:
150
+
151
+ ```ts
152
+ const { accessToken, setAccessToken, clearAccessToken } = useAuthkitToken();
153
+ ```
154
+
155
+ ## Passwords
156
+
157
+ ```ts
158
+ const hash = await hashPassword(plain);
159
+ const ok = await verifyPassword(hash, plain);
160
+ ```
161
+
162
+ scrypt via `@adonisjs/hash`. Cost parameters are configurable under
163
+ `nuxtAuthkit.scrypt`; parameters are embedded in each stored hash, so raising them
164
+ later does not invalidate existing passwords.
165
+
166
+ ## Security
167
+
168
+ See [SECURITY-AUDIT-JWT.md](./SECURITY-AUDIT-JWT.md) for the audit this library's
169
+ current design responds to, and [MIGRATION.md](./MIGRATION.md) for the resulting
170
+ changes.
@@ -0,0 +1,6 @@
1
+ import * as _nuxt_schema from '@nuxt/schema';
2
+ import { ModuleOptions } from '../dist/runtime/types/module-options.js';
3
+
4
+ declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, false>;
5
+
6
+ export { _default as default };
@@ -0,0 +1,9 @@
1
+ {
2
+ "name": "nuxt-authkit",
3
+ "configKey": "nuxtAuthkit",
4
+ "version": "0.0.1",
5
+ "builder": {
6
+ "@nuxt/module-builder": "1.0.2",
7
+ "unbuild": "3.6.1"
8
+ }
9
+ }
@@ -0,0 +1,80 @@
1
+ import { defineNuxtModule, createResolver, updateRuntimeConfig, addServerImports, addServerHandler, addPlugin } from '@nuxt/kit';
2
+ import { mergeRuntimeConfig } from '../dist/runtime/utils/get-module-options.js';
3
+
4
+ const module = defineNuxtModule({
5
+ meta: {
6
+ name: "nuxt-authkit",
7
+ configKey: "nuxtAuthkit"
8
+ },
9
+ defaults: {
10
+ strategies: ["jwt"],
11
+ scrypt: {},
12
+ clientPlugin: true
13
+ },
14
+ setup(_options, _nuxt) {
15
+ const resolver = createResolver(import.meta.url);
16
+ _nuxt.options.alias = _nuxt.options.alias || {};
17
+ _nuxt.options.alias["#nuxt-authkit"] = resolver.resolve("./runtime/index");
18
+ _nuxt.hook("nitro:config", async (nitroConfig) => {
19
+ nitroConfig.experimental = nitroConfig.experimental || {};
20
+ nitroConfig.experimental.asyncContext = true;
21
+ });
22
+ const mergedRuntimeConfig = mergeRuntimeConfig(_options);
23
+ updateRuntimeConfig(mergedRuntimeConfig);
24
+ let getAuthkitUtil = null;
25
+ if (mergedRuntimeConfig?.nuxtAuthkit.strategies.includes("jwt")) {
26
+ getAuthkitUtil = {
27
+ name: "getAuthkit",
28
+ from: resolver.resolve("./runtime/service/utils/getJwtAsyncService")
29
+ };
30
+ }
31
+ addServerImports([
32
+ {
33
+ name: "createNuxtAuthkit",
34
+ from: resolver.resolve("./runtime/index")
35
+ },
36
+ {
37
+ name: "createJWTStrategy",
38
+ from: resolver.resolve("./runtime/strategy/jwt")
39
+ },
40
+ ...getAuthkitUtil ? [getAuthkitUtil] : []
41
+ ]);
42
+ addServerImports([
43
+ {
44
+ name: "hasAuthkitRole",
45
+ from: resolver.resolve("./runtime/helpers/request-middleware")
46
+ },
47
+ {
48
+ name: "hasAuthkitSession",
49
+ from: resolver.resolve("./runtime/helpers/request-middleware")
50
+ },
51
+ {
52
+ name: "rejectAuthkitRole",
53
+ from: resolver.resolve("./runtime/helpers/request-middleware")
54
+ }
55
+ ]);
56
+ addServerImports([
57
+ {
58
+ name: "hashPassword",
59
+ from: resolver.resolve("./runtime/utils/password")
60
+ },
61
+ {
62
+ name: "verifyPassword",
63
+ from: resolver.resolve("./runtime/utils/password")
64
+ }
65
+ ]);
66
+ addServerHandler({
67
+ route: "/api/_authkit/refresh",
68
+ method: "post",
69
+ handler: resolver.resolve("./runtime/server/api/_auth/refresh.post")
70
+ });
71
+ if (_options.clientPlugin !== false) {
72
+ addPlugin({
73
+ src: resolver.resolve("./runtime/plugins/auth-fetch.client"),
74
+ mode: "client"
75
+ });
76
+ }
77
+ }
78
+ });
79
+
80
+ export { module as default };
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Access to the current access token.
3
+ *
4
+ * The refresh token deliberately has no presence here. It lives in an HttpOnly
5
+ * cookie, unreadable from JavaScript, and the server reads it from there and
6
+ * nowhere else. Mirroring it into `useState` would put a multi-day credential
7
+ * within reach of any XSS on the page (and serialise it into `window.__NUXT__`
8
+ * during SSR) in exchange for nothing.
9
+ */
10
+ export declare function useAuthkitToken(): {
11
+ accessToken: Readonly<import("vue").Ref<string | null, string | null>>;
12
+ setAccessToken: (token: string | null) => void;
13
+ clearAccessToken: () => void;
14
+ getAuthHeader: () => {
15
+ Authorization: string;
16
+ } | {
17
+ Authorization?: undefined;
18
+ };
19
+ /**
20
+ * @deprecated The refresh token is HttpOnly and is never held in client
21
+ * state. Clearing the access token is what "log out locally" means here.
22
+ * This alias exists so 0.0.x callers keep compiling; it will be removed.
23
+ */
24
+ clearRefreshToken: () => void;
25
+ };
@@ -0,0 +1,31 @@
1
+ import { useState } from "#app";
2
+ import { readonly } from "vue";
3
+ export function useAuthkitToken() {
4
+ const accessToken = useState("authkit-access-token", () => null);
5
+ function setAccessToken(token) {
6
+ accessToken.value = token;
7
+ }
8
+ function clearAccessToken() {
9
+ accessToken.value = null;
10
+ }
11
+ function getAuthHeader() {
12
+ if (accessToken.value) {
13
+ return {
14
+ Authorization: `Bearer ${accessToken.value}`
15
+ };
16
+ }
17
+ return {};
18
+ }
19
+ return {
20
+ accessToken: readonly(accessToken),
21
+ setAccessToken,
22
+ clearAccessToken,
23
+ getAuthHeader,
24
+ /**
25
+ * @deprecated The refresh token is HttpOnly and is never held in client
26
+ * state. Clearing the access token is what "log out locally" means here.
27
+ * This alias exists so 0.0.x callers keep compiling; it will be removed.
28
+ */
29
+ clearRefreshToken: clearAccessToken
30
+ };
31
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Require a valid session. Throws 401 otherwise.
3
+ */
4
+ export declare function hasAuthkitSession(): Promise<void>;
5
+ /**
6
+ * Require the caller to hold at least one of `roles` (or all of them when
7
+ * `requireAll`). Throws 401 without a valid session, 403 without the role.
8
+ */
9
+ export declare function hasAuthkitRole(roles: string | string[], requireAll?: boolean): Promise<void>;
10
+ /**
11
+ * Deny-filter: reject callers who hold any of `roles` (or all of them when
12
+ * `requireAll`).
13
+ *
14
+ * This is NOT an authentication gate — it is the inverse of one, and it requires
15
+ * a valid session to mean anything. Without the session check below, an
16
+ * unauthenticated request has no roles, "has no roles" reads as "does not hold
17
+ * the rejected role", and the guard waves it straight through: a banned user
18
+ * evades `rejectAuthkitRole('banned')` by simply dropping their Authorization
19
+ * header. Proving a caller does *not* hold a role first requires knowing who
20
+ * they are.
21
+ */
22
+ export declare function rejectAuthkitRole(roles: string | string[], requireAll?: boolean): Promise<void>;
@@ -0,0 +1,82 @@
1
+ import { createError } from "h3";
2
+ import { getAuthkit } from "../service/utils/getJwtAsyncService.js";
3
+ import { hasRole } from "../roles/has-role.js";
4
+ function isHttpError(error) {
5
+ return !!error && typeof error === "object" && "statusCode" in error;
6
+ }
7
+ export async function hasAuthkitSession() {
8
+ try {
9
+ const authkit = getAuthkit();
10
+ const tokenSet = await authkit.verify();
11
+ if (!tokenSet.valid) {
12
+ throw createError({
13
+ statusCode: 401,
14
+ message: "Unauthorized"
15
+ });
16
+ }
17
+ } catch (error) {
18
+ if (isHttpError(error)) {
19
+ throw error;
20
+ }
21
+ throw createError({
22
+ statusCode: 401,
23
+ message: "Unauthorized"
24
+ });
25
+ }
26
+ }
27
+ export async function hasAuthkitRole(roles, requireAll = false) {
28
+ try {
29
+ const authkit = getAuthkit();
30
+ const verificationResult = await authkit.verify();
31
+ if (!verificationResult.valid) {
32
+ throw createError({
33
+ statusCode: 401,
34
+ message: "Unauthorized"
35
+ });
36
+ }
37
+ const payloadRoles = verificationResult.payload?.roles || [];
38
+ const allowedRoles = Array.isArray(roles) ? roles : [roles];
39
+ if (!hasRole(payloadRoles, allowedRoles, requireAll)) {
40
+ throw createError({
41
+ statusCode: 403,
42
+ message: "Forbidden"
43
+ });
44
+ }
45
+ } catch (error) {
46
+ if (isHttpError(error)) {
47
+ throw error;
48
+ }
49
+ throw createError({
50
+ statusCode: 403,
51
+ message: "Forbidden"
52
+ });
53
+ }
54
+ }
55
+ export async function rejectAuthkitRole(roles, requireAll = false) {
56
+ try {
57
+ const authkit = getAuthkit();
58
+ const verificationResult = await authkit.verify();
59
+ if (!verificationResult.valid) {
60
+ throw createError({
61
+ statusCode: 401,
62
+ message: "Unauthorized"
63
+ });
64
+ }
65
+ const payloadRoles = verificationResult.payload?.roles || [];
66
+ const rejectedRoles = Array.isArray(roles) ? roles : [roles];
67
+ if (hasRole(payloadRoles, rejectedRoles, requireAll)) {
68
+ throw createError({
69
+ statusCode: 403,
70
+ message: "Forbidden"
71
+ });
72
+ }
73
+ } catch (error) {
74
+ if (isHttpError(error)) {
75
+ throw error;
76
+ }
77
+ throw createError({
78
+ statusCode: 403,
79
+ message: "Forbidden"
80
+ });
81
+ }
82
+ }
@@ -0,0 +1,8 @@
1
+ export { createNuxtAuthkit } from './service/index.js';
2
+ export { createJWTStrategy } from './strategy/jwt/index.js';
3
+ export type { AuthCallbacks, RefreshTokenLookup, RefreshTokenLookupResult } from './types/callbacks.js';
4
+ export type { AuthkitCredentials, AuthkitUser } from './types/authkit-types.js';
5
+ export type { TokenPayload, TokenSet, TokenMetadata, TokenUse } from './types/token.js';
6
+ export type { AuthResult, VerifyAuthResult, AuthError, AuthErrorCode } from './types/index.js';
7
+ export type { JWTAuthCallbacks } from './strategy/jwt/callback.js';
8
+ export type { JwtStrategyConfig, JWTVerifyOptions, JWTRefreshOptions, JWTServerRevokeOptions } from './strategy/jwt/types.js';
@@ -0,0 +1,2 @@
1
+ export { createNuxtAuthkit } from "./service/index.js";
2
+ export { createJWTStrategy } from "./strategy/jwt/index.js";
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Client-only. This plugin assigns `globalThis.$fetch`, and on the server that is
3
+ * a single process-wide object shared by every concurrent SSR request — one
4
+ * request's setup would overwrite another's, handing request A the token store
5
+ * closed over by request B. In the browser `globalThis` is the tab, so exactly
6
+ * one user's session is ever in scope.
7
+ *
8
+ * The `.client` suffix is what enforces that. Nuxt matches plugin mode on
9
+ * `/\.(server|client)(\.\w+)*$/` — a *dot*, not a hyphen. A file named
10
+ * `auth-fetch-client.ts` does not match, registers as mode 'all', and runs on SSR.
11
+ */
12
+ declare const _default: import("nuxt/app").Plugin<Record<string, unknown>> & import("nuxt/app").ObjectPlugin<Record<string, unknown>>;
13
+ export default _default;
@@ -0,0 +1,91 @@
1
+ import { defineNuxtPlugin } from "#app";
2
+ import { useAuthkitToken } from "../composables/useAuthkitToken.js";
3
+ import { REFRESH_ENDPOINT, isSameOrigin, isRefreshRequest } from "./internal/request-target.js";
4
+ export default defineNuxtPlugin(() => {
5
+ const tokenStore = useAuthkitToken();
6
+ let refreshPromise = null;
7
+ const origin = () => window.location.origin;
8
+ const performRefresh = async () => {
9
+ try {
10
+ const response = await $fetch.raw(REFRESH_ENDPOINT, {
11
+ method: "POST"
12
+ });
13
+ const newToken = response?._data?.data?.tokens?.accessToken;
14
+ if (newToken) {
15
+ tokenStore.setAccessToken(newToken);
16
+ return newToken;
17
+ }
18
+ return null;
19
+ } catch (error) {
20
+ tokenStore.clearAccessToken();
21
+ throw error;
22
+ } finally {
23
+ refreshPromise = null;
24
+ }
25
+ };
26
+ globalThis.$fetch = $fetch.create({
27
+ async onRequest({ options, request }) {
28
+ if (isRefreshRequest(request, origin())) {
29
+ return;
30
+ }
31
+ if (refreshPromise) {
32
+ await refreshPromise.catch(() => {
33
+ });
34
+ }
35
+ if (!isSameOrigin(request, origin())) {
36
+ return;
37
+ }
38
+ const token = tokenStore.accessToken.value;
39
+ if (token) {
40
+ options.headers = {
41
+ ...options.headers,
42
+ ...tokenStore.getAuthHeader()
43
+ };
44
+ }
45
+ },
46
+ async onResponse({ response, request }) {
47
+ if (!isRefreshRequest(request, origin())) {
48
+ return;
49
+ }
50
+ const newToken = response._data?.data?.tokens?.accessToken;
51
+ if (newToken) {
52
+ tokenStore.setAccessToken(newToken);
53
+ }
54
+ },
55
+ async onResponseError({ response, options, request }) {
56
+ if (isRefreshRequest(request, origin())) {
57
+ return;
58
+ }
59
+ if (response.status !== 401) return;
60
+ if (options.headers?.["x-retried"]) {
61
+ tokenStore.clearAccessToken();
62
+ return;
63
+ }
64
+ if (response._data?.error?.code === "REFRESH_TOKEN_EXPIRED" || response._data?.error?.recoverable === false) {
65
+ tokenStore.clearAccessToken();
66
+ return;
67
+ }
68
+ if (!refreshPromise) {
69
+ refreshPromise = performRefresh();
70
+ }
71
+ try {
72
+ const newToken = await refreshPromise;
73
+ if (!newToken) {
74
+ tokenStore.clearAccessToken();
75
+ return;
76
+ }
77
+ return await $fetch(request, {
78
+ ...options,
79
+ headers: {
80
+ ...options.headers,
81
+ "Authorization": `Bearer ${newToken}`,
82
+ "x-retried": "true"
83
+ }
84
+ });
85
+ } catch {
86
+ tokenStore.clearAccessToken();
87
+ return;
88
+ }
89
+ }
90
+ });
91
+ });
@@ -0,0 +1,16 @@
1
+ export declare const REFRESH_ENDPOINT = "/api/_authkit/refresh";
2
+ /**
3
+ * Whether a request is going to our own origin.
4
+ *
5
+ * The interceptor patches the global `$fetch`, so it sees every request the app
6
+ * makes — including third-party ones (payments, maps, analytics) and any URL an
7
+ * attacker managed to influence. Attaching the session token to those hands the
8
+ * user's credential to whoever is on the other end, and a permissive CORS policy
9
+ * on their side is all it takes for the call to succeed.
10
+ */
11
+ export declare function isSameOrigin(request: unknown, origin: string): boolean;
12
+ /**
13
+ * Whether a request targets our own refresh endpoint — the only response allowed
14
+ * to hand us a new session token.
15
+ */
16
+ export declare function isRefreshRequest(request: unknown, origin: string): boolean;
@@ -0,0 +1,17 @@
1
+ export const REFRESH_ENDPOINT = "/api/_authkit/refresh";
2
+ function toUrl(request, origin) {
3
+ const raw = typeof request === "string" ? request : typeof Request !== "undefined" && request instanceof Request ? request.url : String(request);
4
+ try {
5
+ return new URL(raw, origin);
6
+ } catch {
7
+ return null;
8
+ }
9
+ }
10
+ export function isSameOrigin(request, origin) {
11
+ return toUrl(request, origin)?.origin === origin;
12
+ }
13
+ export function isRefreshRequest(request, origin) {
14
+ const url = toUrl(request, origin);
15
+ if (!url) return false;
16
+ return url.origin === origin && url.pathname === REFRESH_ENDPOINT;
17
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Core function to check if a user has the specified role(s)
3
+ * Works with both arrays of roles and role objects with primary_role/roles properties
4
+ *
5
+ * @param {string[] | { primary_role?: string, roles?: string[] } | null | undefined} userRoles - User roles to check
6
+ * @param {string[]} requiredRoles - Role or array of roles to check against
7
+ * @param {boolean} requireAll - If true, user must have ALL specified roles; if false, user only needs ONE of the roles
8
+ * @returns {boolean} - True if user has the required role(s), false otherwise
9
+ */
10
+ export declare function checkRole(userRoles: (string)[] | {
11
+ primary_role?: string;
12
+ roles?: (string)[];
13
+ } | null | undefined, requiredRoles: string | string[], requireAll?: boolean): boolean;
14
+ /**
15
+ * Server-side role checking function
16
+ *
17
+ * @param string[] payloadRoles - Roles from the token payload
18
+ * @param string[] roles - Role or array of roles to check against
19
+ * @returns {boolean} - True if user has the role, false otherwise
20
+ */
21
+ export declare function hasRole(payloadRoles: string[] | undefined, roles: string[], requireAll?: boolean): boolean;
@@ -0,0 +1,51 @@
1
+ import { createError } from "h3";
2
+ import { consola } from "consola";
3
+ export function checkRole(userRoles, requiredRoles, requireAll = false) {
4
+ if (!userRoles) {
5
+ return false;
6
+ }
7
+ if (!requiredRoles) {
8
+ return false;
9
+ }
10
+ const rolesToCheck = Array.isArray(requiredRoles) ? requiredRoles : [requiredRoles];
11
+ if (rolesToCheck.length === 0) {
12
+ return false;
13
+ }
14
+ if (Array.isArray(userRoles)) {
15
+ if (requireAll) {
16
+ return rolesToCheck.every((role) => userRoles.includes(role));
17
+ } else {
18
+ return rolesToCheck.some((role) => userRoles.includes(role));
19
+ }
20
+ }
21
+ const allUserRoles = [];
22
+ if (userRoles.primary_role) {
23
+ allUserRoles.push(userRoles.primary_role);
24
+ }
25
+ if (userRoles.roles && Array.isArray(userRoles.roles)) {
26
+ allUserRoles.push(...userRoles.roles);
27
+ }
28
+ if (requireAll) {
29
+ return rolesToCheck.every((role) => allUserRoles.includes(role));
30
+ } else {
31
+ return rolesToCheck.some((role) => allUserRoles.includes(role));
32
+ }
33
+ }
34
+ export function hasRole(payloadRoles, roles, requireAll = false) {
35
+ if (!roles) {
36
+ if (import.meta.server) {
37
+ throw createError({
38
+ statusCode: 500,
39
+ message: "Validation rules not provided"
40
+ });
41
+ } else if (import.meta.client || import.meta.browser) {
42
+ consola.error("[\x1B[31m authkit \x1B[0m] hasRole called without roles to check against");
43
+ return false;
44
+ }
45
+ return false;
46
+ }
47
+ if (!payloadRoles) {
48
+ return false;
49
+ }
50
+ return checkRole(payloadRoles, roles, requireAll);
51
+ }
@@ -0,0 +1,6 @@
1
+ import { type H3Event } from 'h3';
2
+ export declare function handleRefreshRequest(event: H3Event): Promise<{
3
+ status: string;
4
+ message: string;
5
+ data: import("../../../index.js").AuthResult;
6
+ }>;