nuxt-huia-oidc 1.0.0-alpha.10

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 (46) hide show
  1. package/README.md +69 -0
  2. package/dist/module.d.mts +63 -0
  3. package/dist/module.json +12 -0
  4. package/dist/module.mjs +123 -0
  5. package/dist/runtime/app/composables/useHuia.d.ts +41 -0
  6. package/dist/runtime/app/composables/useHuia.js +25 -0
  7. package/dist/runtime/app/composables/useUserSession.d.ts +45 -0
  8. package/dist/runtime/app/composables/useUserSession.js +36 -0
  9. package/dist/runtime/app/middleware/auth.d.ts +3 -0
  10. package/dist/runtime/app/middleware/auth.js +14 -0
  11. package/dist/runtime/app/plugins/session.server.d.ts +8 -0
  12. package/dist/runtime/app/plugins/session.server.js +8 -0
  13. package/dist/runtime/app/utils/route-match.d.ts +8 -0
  14. package/dist/runtime/app/utils/route-match.js +9 -0
  15. package/dist/runtime/server/middleware/session.context.d.ts +7 -0
  16. package/dist/runtime/server/middleware/session.context.js +6 -0
  17. package/dist/runtime/server/plugins/oidc.discovery.d.ts +2 -0
  18. package/dist/runtime/server/plugins/oidc.discovery.js +5 -0
  19. package/dist/runtime/server/routes/auth/oidc/callback.get.d.ts +2 -0
  20. package/dist/runtime/server/routes/auth/oidc/callback.get.js +16 -0
  21. package/dist/runtime/server/routes/auth/oidc/login.get.d.ts +2 -0
  22. package/dist/runtime/server/routes/auth/oidc/login.get.js +21 -0
  23. package/dist/runtime/server/routes/auth/oidc/logout.get.d.ts +2 -0
  24. package/dist/runtime/server/routes/auth/oidc/logout.get.js +22 -0
  25. package/dist/runtime/server/routes/auth/session.get.d.ts +2 -0
  26. package/dist/runtime/server/routes/auth/session.get.js +6 -0
  27. package/dist/runtime/server/utils/config.d.ts +58 -0
  28. package/dist/runtime/server/utils/config.js +60 -0
  29. package/dist/runtime/server/utils/cookie.d.ts +9 -0
  30. package/dist/runtime/server/utils/cookie.js +90 -0
  31. package/dist/runtime/server/utils/internal-types.d.ts +84 -0
  32. package/dist/runtime/server/utils/internal-types.js +0 -0
  33. package/dist/runtime/server/utils/oidc.d.ts +24 -0
  34. package/dist/runtime/server/utils/oidc.js +171 -0
  35. package/dist/runtime/server/utils/refresh.d.ts +7 -0
  36. package/dist/runtime/server/utils/refresh.js +107 -0
  37. package/dist/runtime/server/utils/session.d.ts +15 -0
  38. package/dist/runtime/server/utils/session.js +165 -0
  39. package/dist/runtime/server/utils/storage.d.ts +17 -0
  40. package/dist/runtime/server/utils/storage.js +46 -0
  41. package/dist/runtime/server/utils/tokens.d.ts +1 -0
  42. package/dist/runtime/server/utils/tokens.js +6 -0
  43. package/dist/runtime/types.d.ts +57 -0
  44. package/dist/runtime/types.js +0 -0
  45. package/dist/types.d.mts +3 -0
  46. package/package.json +60 -0
package/README.md ADDED
@@ -0,0 +1,69 @@
1
+ # nuxt-huia-oidc
2
+
3
+ **Nuxt 3+** authentication module for the [Huia.OpenId](https://github.com/Ayman-Elfaki/Huia)
4
+ identity provider. OIDC Authorization Code flow with PKCE, RFC 9126 Pushed Authorization Requests,
5
+ transparent server-side token refresh, and a dual-layer session that keeps **every token on the
6
+ server**.
7
+
8
+ Its sibling module, [`nuxt-huia-headless`](../nuxt-huia-headless), talks to `Huia.Headless` instead —
9
+ a JSON register/login API with no OIDC redirect — for apps that own their own login form. Use this
10
+ module when the app authenticates against a multi-tenant `Huia.OpenId` server via OIDC redirects.
11
+
12
+ - Built on [`openid-client`](https://github.com/panva/openid-client) v6 (ESM, Web Crypto)
13
+ - Requires Nuxt `>=3`, Nitro `>=2.10`, Node `>=20.11`
14
+ - Full spec: [`SPEC.md`](./SPEC.md) · docs: <https://github.com/Ayman-Elfaki/Huia/tree/main/docs/nuxt>
15
+
16
+ ## Install
17
+
18
+ ```bash
19
+ npm install nuxt-huia-oidc
20
+ ```
21
+
22
+ ```ts
23
+ // nuxt.config.ts
24
+ export default defineNuxtConfig({
25
+ modules: ['nuxt-huia-oidc'],
26
+ huia: {
27
+ baseUrl: 'https://id.example.com',
28
+ tenant: 'acme',
29
+ clientId: 'acme-web',
30
+ // clientSecret via NUXT_HUIA_CLIENT_SECRET
31
+ scopes: ['openid', 'profile', 'email', 'offline_access'],
32
+ par: { enabled: true },
33
+ // session.password via NUXT_HUIA_SESSION_PASSWORD (>= 32 chars)
34
+ },
35
+ })
36
+ ```
37
+
38
+ ## Usage
39
+
40
+ ```vue
41
+ <script setup lang="ts">
42
+ const { user, loggedIn } = useUserSession()
43
+ const { login, logout } = useHuia()
44
+ </script>
45
+ ```
46
+
47
+ ```ts
48
+ // server/api/me.get.ts
49
+ export default defineEventHandler(async (event) => {
50
+ const { user } = await requireUserSession(event) // 401 if anonymous
51
+ return { id: user.sub, name: user.name }
52
+ })
53
+ ```
54
+
55
+ The browser only ever holds a sealed, chunked `__Host-huia_sess` cookie carrying the session id and a
56
+ whitelisted claim subset. Access / refresh / id tokens live in Nitro Storage, keyed by that id, and
57
+ are refreshed transparently. See [`SPEC.md`](./SPEC.md) for the architecture, the cookie-chunking and
58
+ soft-lock algorithms, the security table, and the test strategy.
59
+
60
+ ## Develop
61
+
62
+ ```bash
63
+ npm install
64
+ npm run dev:prepare
65
+ npm test # Vitest unit + integration (mocked OP)
66
+ npm run dev # playground on http://localhost:3000
67
+ ```
68
+
69
+ `.npmrc` pins `legacy-peer-deps=true` (an npm 10 arborist crash on the nuxt peer graph).
@@ -0,0 +1,63 @@
1
+ import * as _nuxt_schema from '@nuxt/schema';
2
+
3
+ interface ModuleOptions {
4
+ baseUrl?: string;
5
+ tenant?: string;
6
+ issuer?: string;
7
+ clientId: string;
8
+ clientSecret?: string;
9
+ redirectUrl?: string;
10
+ scopes?: string[];
11
+ allowedAuthParams?: string[];
12
+ par?: {
13
+ enabled?: boolean;
14
+ required?: boolean;
15
+ };
16
+ session?: {
17
+ name?: string;
18
+ password?: string;
19
+ maxAge?: number;
20
+ cookie?: {
21
+ sameSite?: 'lax' | 'strict' | 'none';
22
+ secure?: boolean;
23
+ };
24
+ userClaims?: string[];
25
+ stateless?: boolean;
26
+ };
27
+ storage?: {
28
+ base?: string;
29
+ };
30
+ refresh?: {
31
+ enabled?: boolean;
32
+ earlyRefreshSeconds?: number;
33
+ lock?: {
34
+ ttlMs?: number;
35
+ waitMs?: number;
36
+ pollMs?: number;
37
+ };
38
+ };
39
+ cookie?: {
40
+ chunkSize?: number;
41
+ maxChunks?: number;
42
+ };
43
+ middleware?: {
44
+ global?: boolean;
45
+ exclude?: string[];
46
+ };
47
+ hydration?: 'useState' | 'asyncData';
48
+ routes?: {
49
+ login?: string;
50
+ callback?: string;
51
+ logout?: string;
52
+ session?: string;
53
+ error?: string;
54
+ };
55
+ logout?: {
56
+ rpInitiated?: boolean;
57
+ };
58
+ allowInsecureTls?: boolean;
59
+ }
60
+ declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, false>;
61
+
62
+ export { _default as default };
63
+ export type { ModuleOptions };
@@ -0,0 +1,12 @@
1
+ {
2
+ "name": "nuxt-huia-oidc",
3
+ "configKey": "huia",
4
+ "compatibility": {
5
+ "nuxt": ">=3.0.0"
6
+ },
7
+ "version": "1.0.0-alpha.10",
8
+ "builder": {
9
+ "@nuxt/module-builder": "1.0.3",
10
+ "unbuild": "3.6.1"
11
+ }
12
+ }
@@ -0,0 +1,123 @@
1
+ import { defineNuxtModule, createResolver, addServerImportsDir, addImportsDir, addPlugin, addRouteMiddleware, addServerHandler, addServerPlugin } from '@nuxt/kit';
2
+ import { defu } from 'defu';
3
+
4
+ const defaults = {
5
+ baseUrl: "",
6
+ tenant: "",
7
+ issuer: "",
8
+ clientId: "",
9
+ redirectUrl: "/auth/oidc/callback",
10
+ scopes: ["openid", "profile", "email", "offline_access"],
11
+ allowedAuthParams: ["ui_locales", "prompt", "login_hint"],
12
+ par: { enabled: true, required: false },
13
+ session: {
14
+ name: "__Host-huia_sess",
15
+ password: "",
16
+ maxAge: 60 * 60 * 24 * 7,
17
+ cookie: { sameSite: "lax" },
18
+ userClaims: ["sub", "name", "email", "preferred_username", "given_name", "family_name", "roles"],
19
+ stateless: false
20
+ },
21
+ storage: { base: "huia-auth" },
22
+ refresh: {
23
+ enabled: true,
24
+ earlyRefreshSeconds: 60,
25
+ lock: { ttlMs: 1e4, waitMs: 8e3, pollMs: 150 }
26
+ },
27
+ cookie: { chunkSize: 3800, maxChunks: 8 },
28
+ middleware: { global: false, exclude: [] },
29
+ hydration: "useState",
30
+ routes: {
31
+ login: "/auth/oidc/login",
32
+ callback: "/auth/oidc/callback",
33
+ logout: "/auth/oidc/logout",
34
+ session: "/auth/session",
35
+ error: "/"
36
+ },
37
+ logout: { rpInitiated: true },
38
+ allowInsecureTls: false
39
+ };
40
+ const module$1 = defineNuxtModule({
41
+ meta: {
42
+ name: "nuxt-huia-oidc",
43
+ configKey: "huia",
44
+ compatibility: { nuxt: ">=3.0.0" }
45
+ },
46
+ defaults,
47
+ setup(options, nuxt) {
48
+ const { resolve } = createResolver(import.meta.url);
49
+ const opts = defu(options, defaults);
50
+ if (!opts.session.password && !process.env.NUXT_HUIA_SESSION_PASSWORD) {
51
+ console.warn("[huia-auth] no session password set \u2014 set NUXT_HUIA_SESSION_PASSWORD (>= 32 chars)");
52
+ }
53
+ nuxt.options.runtimeConfig.huia = defu(
54
+ nuxt.options.runtimeConfig.huia,
55
+ {
56
+ clientId: opts.clientId,
57
+ clientSecret: opts.clientSecret ?? "",
58
+ issuer: opts.issuer ?? "",
59
+ baseUrl: opts.baseUrl ?? "",
60
+ tenant: opts.tenant ?? "",
61
+ redirectUrl: opts.redirectUrl,
62
+ scopes: opts.scopes,
63
+ allowedAuthParams: opts.allowedAuthParams,
64
+ par: opts.par,
65
+ session: { ...opts.session, password: opts.session.password ?? "" },
66
+ storage: opts.storage,
67
+ refresh: opts.refresh,
68
+ cookie: opts.cookie,
69
+ routes: opts.routes,
70
+ logout: opts.logout,
71
+ allowInsecureTls: opts.allowInsecureTls
72
+ }
73
+ );
74
+ nuxt.options.runtimeConfig.public.huia = defu(
75
+ nuxt.options.runtimeConfig.public.huia,
76
+ {
77
+ loginPath: opts.routes.login,
78
+ logoutPath: opts.routes.logout,
79
+ sessionPath: opts.routes.session,
80
+ // The module's own auth routes are never protected by its own middleware, regardless of
81
+ // `middleware.exclude` — applying "must be logged in" to the routes that exist precisely for
82
+ // the logged-out flow is never correct, and was the mechanism behind the infinite-redirect
83
+ // risk of turning on `middleware.global` with no exclusions configured.
84
+ middlewareExclude: [
85
+ ...opts.middleware.exclude,
86
+ opts.routes.login,
87
+ opts.routes.callback,
88
+ opts.routes.logout,
89
+ opts.routes.session
90
+ ]
91
+ }
92
+ );
93
+ nuxt.options.alias["#huia-auth"] = resolve("runtime/types");
94
+ nuxt.options.imports = nuxt.options.imports || {};
95
+ nuxt.options.imports.exclude = [...nuxt.options.imports.exclude || [], /[\\/]huia-auth-core[\\/]/];
96
+ nuxt.hook("nitro:config", (nitroConfig) => {
97
+ nitroConfig.imports = nitroConfig.imports || {};
98
+ nitroConfig.imports.exclude = nitroConfig.imports.exclude || [];
99
+ nitroConfig.imports.exclude.push(/[\\/]huia-auth-core[\\/]/);
100
+ });
101
+ addServerImportsDir(resolve("runtime/server/utils"));
102
+ addImportsDir(resolve("runtime/app/composables"));
103
+ if (opts.hydration === "useState") {
104
+ addPlugin({ src: resolve("runtime/app/plugins/session.server"), mode: "server" });
105
+ }
106
+ addRouteMiddleware({
107
+ name: "auth",
108
+ path: resolve("runtime/app/middleware/auth"),
109
+ global: opts.middleware.global
110
+ });
111
+ addServerHandler({ route: opts.routes.login, method: "get", handler: resolve("runtime/server/routes/auth/oidc/login.get") });
112
+ addServerHandler({ route: opts.routes.callback, method: "get", handler: resolve("runtime/server/routes/auth/oidc/callback.get") });
113
+ addServerHandler({ route: opts.routes.logout, method: "get", handler: resolve("runtime/server/routes/auth/oidc/logout.get") });
114
+ addServerHandler({ route: opts.routes.session, method: "get", handler: resolve("runtime/server/routes/auth/session.get") });
115
+ addServerHandler({ middleware: true, handler: resolve("runtime/server/middleware/session.context") });
116
+ addServerPlugin(resolve("runtime/server/plugins/oidc.discovery"));
117
+ nuxt.hook("prepare:types", ({ references }) => {
118
+ references.push({ path: resolve("runtime/types.d.ts") });
119
+ });
120
+ }
121
+ });
122
+
123
+ export { module$1 as default };
@@ -0,0 +1,41 @@
1
+ export declare function useHuia(): {
2
+ user: import("vue").ComputedRef<import("../../types.js").UserClaims | null>;
3
+ loggedIn: import("vue").ComputedRef<boolean>;
4
+ session: Readonly<import("vue").Ref<{
5
+ readonly user?: {
6
+ readonly [x: string]: Readonly<unknown>;
7
+ readonly sub: string;
8
+ readonly name?: string | undefined;
9
+ readonly email?: string | undefined;
10
+ readonly preferred_username?: string | undefined;
11
+ readonly given_name?: string | undefined;
12
+ readonly family_name?: string | undefined;
13
+ readonly roles?: readonly string[] | undefined;
14
+ } | undefined;
15
+ readonly loggedIn?: boolean | undefined;
16
+ readonly expiresAt?: number | undefined;
17
+ }, {
18
+ readonly user?: {
19
+ readonly [x: string]: Readonly<unknown>;
20
+ readonly sub: string;
21
+ readonly name?: string | undefined;
22
+ readonly email?: string | undefined;
23
+ readonly preferred_username?: string | undefined;
24
+ readonly given_name?: string | undefined;
25
+ readonly family_name?: string | undefined;
26
+ readonly roles?: readonly string[] | undefined;
27
+ } | undefined;
28
+ readonly loggedIn?: boolean | undefined;
29
+ readonly expiresAt?: number | undefined;
30
+ }>>;
31
+ hasRole: (role: string) => boolean;
32
+ hasAnyRole: (...roles: string[]) => boolean;
33
+ login(opts?: {
34
+ returnTo?: string;
35
+ locale?: string;
36
+ prompt?: string;
37
+ }): string | false | void | import("vue-router").RouteLocationAsRelativeGeneric | import("vue-router").RouteLocationAsPathGeneric | Promise<false | void | import("vue-router").NavigationFailure>;
38
+ logout(opts?: {
39
+ returnTo?: string;
40
+ }): string | false | void | import("vue-router").RouteLocationAsRelativeGeneric | import("vue-router").RouteLocationAsPathGeneric | Promise<false | void | import("vue-router").NavigationFailure>;
41
+ };
@@ -0,0 +1,25 @@
1
+ import { navigateTo, useRoute, useRuntimeConfig } from "#imports";
2
+ import { useUserSession } from "./useUserSession.js";
3
+ export function useHuia() {
4
+ const { user, loggedIn, session, hasRole, hasAnyRole } = useUserSession();
5
+ const paths = useRuntimeConfig().public.huia;
6
+ return {
7
+ user,
8
+ loggedIn,
9
+ session,
10
+ hasRole,
11
+ hasAnyRole,
12
+ login(opts = {}) {
13
+ const query = { returnTo: opts.returnTo ?? useRoute().fullPath };
14
+ if (opts.locale) query.ui_locales = opts.locale;
15
+ if (opts.prompt) query.prompt = opts.prompt;
16
+ return navigateTo({ path: paths.loginPath, query }, { external: true });
17
+ },
18
+ logout(opts = {}) {
19
+ return navigateTo(
20
+ { path: paths.logoutPath, query: { returnTo: opts.returnTo ?? "/" } },
21
+ { external: true }
22
+ );
23
+ }
24
+ };
25
+ }
@@ -0,0 +1,45 @@
1
+ import type { UserClaims } from '../../types.js';
2
+ export declare function useUserSession(): {
3
+ session: Readonly<import("vue").Ref<{
4
+ readonly user?: {
5
+ readonly [x: string]: Readonly<unknown>;
6
+ readonly sub: string;
7
+ readonly name?: string | undefined;
8
+ readonly email?: string | undefined;
9
+ readonly preferred_username?: string | undefined;
10
+ readonly given_name?: string | undefined;
11
+ readonly family_name?: string | undefined;
12
+ readonly roles?: readonly string[] | undefined;
13
+ } | undefined;
14
+ readonly loggedIn?: boolean | undefined;
15
+ readonly expiresAt?: number | undefined;
16
+ }, {
17
+ readonly user?: {
18
+ readonly [x: string]: Readonly<unknown>;
19
+ readonly sub: string;
20
+ readonly name?: string | undefined;
21
+ readonly email?: string | undefined;
22
+ readonly preferred_username?: string | undefined;
23
+ readonly given_name?: string | undefined;
24
+ readonly family_name?: string | undefined;
25
+ readonly roles?: readonly string[] | undefined;
26
+ } | undefined;
27
+ readonly loggedIn?: boolean | undefined;
28
+ readonly expiresAt?: number | undefined;
29
+ }>>;
30
+ user: import("vue").ComputedRef<UserClaims | null>;
31
+ loggedIn: import("vue").ComputedRef<boolean>;
32
+ expiresAt: import("vue").ComputedRef<number | undefined>;
33
+ /** Re-read the server session (after returning from login, on tab focus, …). */
34
+ fetch(): Promise<void>;
35
+ /** Full logout via the server route (clears storage + cookies, hits the OP end_session). */
36
+ clear(opts?: {
37
+ returnTo?: string;
38
+ }): Promise<void>;
39
+ /** Optimistic local reset only (UI); does not touch the server. */
40
+ clearLocal(): void;
41
+ /** Whether the signed-in user has `role` (from the `roles` claim — requires the `roles` scope). */
42
+ hasRole(role: string): boolean;
43
+ /** Whether the signed-in user has at least one of `roles`. */
44
+ hasAnyRole(...roles: string[]): boolean;
45
+ };
@@ -0,0 +1,36 @@
1
+ import { computed, readonly } from "vue";
2
+ import { useState, useRequestFetch, navigateTo, useRuntimeConfig } from "#imports";
3
+ export function useUserSession() {
4
+ const session = useState("huia-auth:session", () => ({}));
5
+ const paths = useRuntimeConfig().public.huia;
6
+ return {
7
+ session: readonly(session),
8
+ user: computed(() => session.value.user ?? null),
9
+ loggedIn: computed(() => !!session.value.user),
10
+ expiresAt: computed(() => session.value.expiresAt),
11
+ /** Re-read the server session (after returning from login, on tab focus, …). */
12
+ async fetch() {
13
+ session.value = await useRequestFetch()(paths.sessionPath);
14
+ },
15
+ /** Full logout via the server route (clears storage + cookies, hits the OP end_session). */
16
+ async clear(opts = {}) {
17
+ await navigateTo(
18
+ { path: paths.logoutPath, query: { returnTo: opts.returnTo ?? "/" } },
19
+ { external: true }
20
+ );
21
+ },
22
+ /** Optimistic local reset only (UI); does not touch the server. */
23
+ clearLocal() {
24
+ session.value = {};
25
+ },
26
+ /** Whether the signed-in user has `role` (from the `roles` claim — requires the `roles` scope). */
27
+ hasRole(role) {
28
+ return !!session.value.user?.roles?.includes(role);
29
+ },
30
+ /** Whether the signed-in user has at least one of `roles`. */
31
+ hasAnyRole(...roles) {
32
+ const userRoles = session.value.user?.roles;
33
+ return !!userRoles && roles.some((role) => userRoles.includes(role));
34
+ }
35
+ };
36
+ }
@@ -0,0 +1,3 @@
1
+ import type { RouteMiddleware } from 'nuxt/app';
2
+ declare const _default: RouteMiddleware;
3
+ export default _default;
@@ -0,0 +1,14 @@
1
+ import { defineNuxtRouteMiddleware, navigateTo, useRuntimeConfig } from "#imports";
2
+ import { useUserSession } from "../composables/useUserSession.js";
3
+ import { isExcluded } from "../utils/route-match.js";
4
+ const middleware = (to) => {
5
+ const { loginPath, middlewareExclude } = useRuntimeConfig().public.huia;
6
+ if (isExcluded(to.path, middlewareExclude)) return;
7
+ const { loggedIn } = useUserSession();
8
+ if (loggedIn.value) return;
9
+ return navigateTo(
10
+ { path: loginPath, query: { returnTo: to.fullPath } },
11
+ { external: true, replace: true }
12
+ );
13
+ };
14
+ export default defineNuxtRouteMiddleware(middleware);
@@ -0,0 +1,8 @@
1
+ import type { Plugin } from 'nuxt/app';
2
+ /**
3
+ * SSR only. Copies the session resolved by the Nitro middleware (`event.context.huia`) into
4
+ * `useState('huia-auth:session')`, which Nuxt serialises into the payload. The client then reads the
5
+ * identical value on first render — no fetch, no hydration mismatch, no flash.
6
+ */
7
+ declare const plugin: Plugin;
8
+ export default plugin;
@@ -0,0 +1,8 @@
1
+ import { defineNuxtPlugin, useState, useRequestEvent } from "#imports";
2
+ const plugin = defineNuxtPlugin(() => {
3
+ const event = useRequestEvent();
4
+ const state = useState("huia-auth:session", () => ({}));
5
+ const ctx = event?.context;
6
+ state.value = ctx?.huia ?? {};
7
+ });
8
+ export default plugin;
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Matches `path` against a glob-ish exclude pattern: `**` matches any depth (including zero
3
+ * segments), `*` matches exactly one path segment, everything else is a literal. `/auth/**` matches
4
+ * `/auth`, `/auth/oidc/login` and anything else under `/auth`; `/about` matches only `/about`.
5
+ */
6
+ export declare function matchesPattern(path: string, pattern: string): boolean;
7
+ /** Whether `path` matches any pattern in `exclude`. */
8
+ export declare function isExcluded(path: string, exclude: string[]): boolean;
@@ -0,0 +1,9 @@
1
+ export function matchesPattern(path, pattern) {
2
+ const regexSource = pattern.split("/").map(
3
+ (segment) => segment === "**" ? ".*" : segment.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, "[^/]*")
4
+ ).join("/").replace(/\/\.\*$/, "(?:/.*)?");
5
+ return new RegExp(`^${regexSource}$`).test(path);
6
+ }
7
+ export function isExcluded(path, exclude) {
8
+ return exclude.some((pattern) => matchesPattern(path, pattern));
9
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Runs on every request. Populates `event.context.huia` (memoised inside `getUserSession`) so the
3
+ * Nuxt server plugin can seed `useState('huia-auth:session')` synchronously during SSR. Errors are
4
+ * swallowed here so a transient storage/OP hiccup never 500s an otherwise-anonymous request.
5
+ */
6
+ declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<void>>;
7
+ export default _default;
@@ -0,0 +1,6 @@
1
+ import { defineEventHandler } from "h3";
2
+ import { getUserSession } from "../utils/session.js";
3
+ export default defineEventHandler(async (event) => {
4
+ await getUserSession(event).catch(() => {
5
+ });
6
+ });
@@ -0,0 +1,2 @@
1
+ declare const _default: import("nitropack").NitroAppPlugin;
2
+ export default _default;
@@ -0,0 +1,5 @@
1
+ import { defineNitroPlugin } from "nitropack/runtime";
2
+ import { warmDiscovery } from "../utils/oidc.js";
3
+ export default defineNitroPlugin(() => {
4
+ warmDiscovery();
5
+ });
@@ -0,0 +1,2 @@
1
+ declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<void>>;
2
+ export default _default;
@@ -0,0 +1,16 @@
1
+ import { defineEventHandler, sendRedirect } from "h3";
2
+ import { resolveAuthConfig } from "../../../utils/config.js";
3
+ import { completeAuthorization } from "../../../utils/oidc.js";
4
+ import { setUserSession } from "../../../utils/session.js";
5
+ export default defineEventHandler(async (event) => {
6
+ const cfg = resolveAuthConfig(event);
7
+ try {
8
+ const { tokens, claims, returnTo } = await completeAuthorization(event);
9
+ await setUserSession(event, { tokens, claims });
10
+ return sendRedirect(event, returnTo || "/", 302);
11
+ } catch (err) {
12
+ const message = err instanceof Error ? err.message : "";
13
+ const code = /state/i.test(message) ? "state_mismatch" : /issuer/i.test(message) ? "issuer_mismatch" : /par_expired|request_uri/i.test(message) ? "par_expired" : "callback_failed";
14
+ return sendRedirect(event, `${cfg.errorPath}?auth_error=${code}`, 302);
15
+ }
16
+ });
@@ -0,0 +1,2 @@
1
+ declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<void>>;
2
+ export default _default;
@@ -0,0 +1,21 @@
1
+ import { defineEventHandler, getQuery, sendRedirect } from "h3";
2
+ import { resolveAuthConfig } from "../../../utils/config.js";
3
+ import { beginAuthorization } from "../../../utils/oidc.js";
4
+ import { sanitizeReturnTo } from "../../../utils/tokens.js";
5
+ export default defineEventHandler(async (event) => {
6
+ const cfg = resolveAuthConfig(event);
7
+ const q = getQuery(event);
8
+ const returnTo = sanitizeReturnTo(typeof q.returnTo === "string" ? q.returnTo : void 0);
9
+ const extra = {};
10
+ for (const key of cfg.allowedAuthParams) {
11
+ const v = q[key];
12
+ if (typeof v === "string" && v.length > 0) extra[key] = v;
13
+ }
14
+ try {
15
+ const { redirectTo } = await beginAuthorization(event, { returnTo, extra });
16
+ return sendRedirect(event, redirectTo, 302);
17
+ } catch (err) {
18
+ const reason = err instanceof Error && err.message.startsWith("par_required") ? "par_required" : "login_failed";
19
+ return sendRedirect(event, `${cfg.errorPath}?auth_error=${reason}`, 302);
20
+ }
21
+ });
@@ -0,0 +1,2 @@
1
+ declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<void>>;
2
+ export default _default;
@@ -0,0 +1,22 @@
1
+ import { defineEventHandler, getQuery, getRequestURL, sendRedirect } from "h3";
2
+ import { resolveAuthConfig } from "../../../utils/config.js";
3
+ import { buildLogoutUrl } from "../../../utils/oidc.js";
4
+ import { clearUserSession, getSecureTokenRecord } from "../../../utils/session.js";
5
+ import { sanitizeReturnTo, isJwtExpired } from "../../../utils/tokens.js";
6
+ export default defineEventHandler(async (event) => {
7
+ const cfg = resolveAuthConfig(event);
8
+ const returnTo = sanitizeReturnTo(String(getQuery(event).returnTo ?? "/"));
9
+ const postLogoutRedirectUri = new URL(returnTo, getRequestURL(event).origin).href;
10
+ const record = await getSecureTokenRecord(event);
11
+ await clearUserSession(event);
12
+ if (cfg.logout.rpInitiated && record?.idToken && !isJwtExpired(record.idToken)) {
13
+ return sendRedirect(event, await buildLogoutUrl(event, {
14
+ idTokenHint: record.idToken,
15
+ postLogoutRedirectUri
16
+ }), 302);
17
+ }
18
+ if (cfg.logout.rpInitiated) {
19
+ return sendRedirect(event, await buildLogoutUrl(event, { postLogoutRedirectUri }), 302);
20
+ }
21
+ return sendRedirect(event, postLogoutRedirectUri, 302);
22
+ });
@@ -0,0 +1,2 @@
1
+ declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<import("../../../types.js").UserSession>>;
2
+ export default _default;
@@ -0,0 +1,6 @@
1
+ import { defineEventHandler, setResponseHeader } from "h3";
2
+ import { getUserSession } from "../../utils/session.js";
3
+ export default defineEventHandler(async (event) => {
4
+ setResponseHeader(event, "Cache-Control", "no-store");
5
+ return getUserSession(event);
6
+ });
@@ -0,0 +1,58 @@
1
+ import { type H3Event } from 'h3';
2
+ import type { ResolvedAuthConfig } from './internal-types.js';
3
+ interface RawHuiaAuth {
4
+ clientId: string;
5
+ clientSecret: string;
6
+ issuer: string;
7
+ baseUrl: string;
8
+ tenant: string;
9
+ redirectUrl: string;
10
+ scopes: string[];
11
+ allowedAuthParams: string[];
12
+ par: {
13
+ enabled: boolean;
14
+ required: boolean;
15
+ };
16
+ session: {
17
+ name: string;
18
+ password: string;
19
+ maxAge: number;
20
+ cookie: {
21
+ sameSite?: string;
22
+ secure?: boolean;
23
+ };
24
+ userClaims: string[];
25
+ };
26
+ storage: {
27
+ base: string;
28
+ };
29
+ refresh: {
30
+ enabled: boolean;
31
+ earlyRefreshSeconds: number;
32
+ lock: {
33
+ ttlMs: number;
34
+ waitMs: number;
35
+ pollMs: number;
36
+ };
37
+ };
38
+ cookie: {
39
+ chunkSize: number;
40
+ maxChunks: number;
41
+ };
42
+ routes: {
43
+ login: string;
44
+ callback: string;
45
+ logout: string;
46
+ session: string;
47
+ error: string;
48
+ };
49
+ allowInsecureTls: boolean;
50
+ logout?: {
51
+ rpInitiated?: boolean;
52
+ };
53
+ }
54
+ /** `{baseUrl}/{tenant}` (or an explicit `issuer`), with any trailing slash stripped. */
55
+ export declare function resolveIssuer(raw: Pick<RawHuiaAuth, 'issuer' | 'baseUrl' | 'tenant'>): string;
56
+ export declare function discoveryCacheKey(issuer: string): string;
57
+ export declare function resolveAuthConfig(event: H3Event): ResolvedAuthConfig;
58
+ export {};