@devcoffee/nuxt-core 1.0.2 → 1.1.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 (64) hide show
  1. package/CHANGELOG.md +82 -0
  2. package/GUIDELINE.md +351 -0
  3. package/README.md +405 -84
  4. package/dist/module.d.mts +295 -156
  5. package/dist/module.d.ts +300 -0
  6. package/dist/module.json +1 -1
  7. package/dist/module.mjs +191 -36
  8. package/dist/runtime/app/composables/useAuthContext.d.ts +26 -0
  9. package/dist/runtime/app/composables/useAuthContext.js +111 -0
  10. package/dist/runtime/app/composables/useLogger.d.ts +3 -3
  11. package/dist/runtime/app/composables/useSessionContext.d.ts +22 -0
  12. package/dist/runtime/app/composables/useSessionContext.js +5 -0
  13. package/dist/runtime/app/middleware/authts.d.ts +12 -0
  14. package/dist/runtime/app/middleware/authts.js +101 -0
  15. package/dist/runtime/app/pages/authorize.d.vue.ts +3 -0
  16. package/dist/runtime/app/pages/authorize.vue +32 -0
  17. package/dist/runtime/app/pages/authorize.vue.d.ts +3 -0
  18. package/dist/runtime/app/plugins/authts.d.ts +82 -0
  19. package/dist/runtime/app/plugins/authts.js +91 -0
  20. package/dist/runtime/app/plugins/formatters.d.ts +29 -0
  21. package/dist/runtime/app/plugins/formatters.js +101 -0
  22. package/dist/runtime/app/plugins/locale.d.ts +37 -0
  23. package/dist/runtime/app/plugins/locale.js +39 -0
  24. package/dist/runtime/app/plugins/logging.d.ts +24 -16
  25. package/dist/runtime/app/plugins/logging.js +0 -1
  26. package/dist/runtime/app/utils/hashing.d.ts +1 -0
  27. package/dist/runtime/app/utils/hashing.js +3 -0
  28. package/dist/runtime/server/adapters/http.d.ts +5 -0
  29. package/dist/runtime/server/adapters/http.js +15 -0
  30. package/dist/runtime/server/adapters/oidc.d.ts +58 -0
  31. package/dist/runtime/server/adapters/oidc.js +21 -0
  32. package/dist/runtime/server/adapters/storage.d.ts +39 -0
  33. package/dist/runtime/server/adapters/storage.js +14 -0
  34. package/dist/runtime/server/adapters/utils.d.ts +31 -0
  35. package/dist/runtime/server/adapters/utils.js +28 -0
  36. package/dist/runtime/server/composables/useServerLogger.d.ts +3 -2
  37. package/dist/runtime/server/composables/useServerLogger.js +4 -4
  38. package/dist/runtime/server/core/crypto.d.ts +70 -0
  39. package/dist/runtime/server/core/crypto.js +55 -0
  40. package/dist/runtime/server/core/helpers.d.ts +194 -0
  41. package/dist/runtime/server/core/helpers.js +350 -0
  42. package/dist/runtime/server/core/index.d.ts +1 -0
  43. package/dist/runtime/server/core/index.js +1 -0
  44. package/dist/runtime/server/core/mutex.d.ts +19 -0
  45. package/dist/runtime/server/core/mutex.js +39 -0
  46. package/dist/runtime/server/core/nuxtAuthtsHandler.d.ts +26 -0
  47. package/dist/runtime/server/core/nuxtAuthtsHandler.js +238 -0
  48. package/dist/runtime/server/core/nuxtForwardHandler.d.ts +18 -0
  49. package/dist/runtime/server/core/nuxtForwardHandler.js +60 -0
  50. package/dist/runtime/server/dev/route/session.d.ts +2 -0
  51. package/dist/runtime/server/dev/route/session.js +8 -0
  52. package/dist/runtime/server/plugins/authts.d.ts +11 -0
  53. package/dist/runtime/server/plugins/authts.js +55 -0
  54. package/dist/runtime/server/plugins/logging.js +7 -2
  55. package/dist/runtime/server/tsconfig.json +3 -3
  56. package/dist/runtime/types/global.env.d.ts +21 -7
  57. package/dist/runtime/types/nitro.d.ts +7 -2
  58. package/dist/runtime/types/nuxt.d.ts +28 -8
  59. package/dist/runtime/utils.d.ts +31 -0
  60. package/dist/runtime/utils.js +28 -0
  61. package/dist/types.d.mts +6 -4
  62. package/package.json +45 -17
  63. package/dist/runtime/plugin.d.ts +0 -2
  64. package/dist/runtime/plugin.js +0 -4
@@ -0,0 +1,350 @@
1
+ import { createError } from "#devcoffee-core/server/adapters/http";
2
+ import {
3
+ allowInsecureRequests,
4
+ authorizationCodeGrant as authorizationCodeGrantOidc,
5
+ buildAuthorizationUrl as buildAuthorizationUrlOidc,
6
+ calculatePKCECodeChallenge,
7
+ Configuration,
8
+ discovery,
9
+ fetchUserInfo as fetchUserInfoFromOidc,
10
+ randomPKCECodeVerifier,
11
+ randomState,
12
+ refreshTokenGrant as refreshTokenGrantFromOidc,
13
+ revokeToken
14
+ } from "#devcoffee-core/server/adapters/oidc";
15
+ import {
16
+ getSessionData,
17
+ hasSessionData,
18
+ removeSessionData,
19
+ setSessionData
20
+ } from "#devcoffee-core/server/adapters/storage";
21
+ import { deepMerge, omit } from "#devcoffee-core/server/adapters/utils";
22
+ import useServerLogger from "#devcoffee-core/server/composables/useServerLogger";
23
+ import { useRuntimeConfig } from "#imports";
24
+ import { useStorage } from "nitropack/runtime";
25
+ import { decryptTokenSet, encryptTokenSet, generateSessionId, isValidSessionId, verifySessionId } from "./crypto.js";
26
+ import { tryAcquireLock } from "./mutex.js";
27
+ function getAnonymousUser(extras) {
28
+ const anonymous = useRuntimeConfig().nuxtCore.authts.auth.anonymousUser;
29
+ const { defaultLocale, defaultTimeZone, defaultLanguage } = useRuntimeConfig().nuxtCore;
30
+ return deepMerge(
31
+ {},
32
+ {
33
+ id: "anonymous",
34
+ email: "",
35
+ sub: "anonymous",
36
+ firstName: "Anonymous",
37
+ lastName: "User",
38
+ locale: defaultLocale,
39
+ language: defaultLanguage,
40
+ timezone: defaultTimeZone
41
+ },
42
+ anonymous,
43
+ extras || {}
44
+ );
45
+ }
46
+ function getSessionStorageKey(storagePrefix, sessionId) {
47
+ return storagePrefix ? `${storagePrefix}:${sessionId}` : sessionId;
48
+ }
49
+ export function isSameOrigin(redirectUrl, requestUrl) {
50
+ try {
51
+ return new URL(redirectUrl).origin === requestUrl.origin;
52
+ } catch {
53
+ return false;
54
+ }
55
+ }
56
+ export async function getSession(sessionId, opts) {
57
+ const sessingKey = getSessionStorageKey(opts.storagePrefix, sessionId);
58
+ if (!await hasSessionData(opts.storageName, sessingKey)) return null;
59
+ const session = await getSessionData(opts.storageName, sessingKey);
60
+ if (!session) return null;
61
+ if (session.auth?.tokenSet && session.auth.tokenSet.encrypted === true) {
62
+ if (opts.secret) {
63
+ const decrypted = decryptTokenSet(session.auth.tokenSet, opts.secret);
64
+ session.auth.tokenSet = decrypted ?? void 0;
65
+ } else {
66
+ const logger = useServerLogger({ tag: "authts-helper", level: 3 });
67
+ logger.warn(
68
+ "[getSession] tokenSet is encrypted but sessions.secret is not configured \u2014 tokenSet will be inaccessible"
69
+ );
70
+ session.auth.tokenSet = void 0;
71
+ }
72
+ }
73
+ return session;
74
+ }
75
+ function newSession(sessionId, expiresAt) {
76
+ return {
77
+ id: sessionId,
78
+ auth: { status: "unauthenticated" },
79
+ user: getAnonymousUser(),
80
+ data: {},
81
+ expiresAt,
82
+ issuedAt: Date.now()
83
+ };
84
+ }
85
+ export async function validateSession(sessionCookieId, opts) {
86
+ const now = Date.now();
87
+ const expiresAt = now + opts.expiresIn;
88
+ let sessionId = void 0;
89
+ let sessionKey = void 0;
90
+ let deleteSessionKey = void 0;
91
+ let session = null;
92
+ const rawSessionId = verifySessionId(sessionCookieId, opts.secret);
93
+ if (rawSessionId && isValidSessionId(rawSessionId)) {
94
+ sessionId = rawSessionId;
95
+ }
96
+ if (sessionId) {
97
+ sessionKey = getSessionStorageKey(opts.storagePrefix, sessionId);
98
+ if (await hasSessionData(opts.storageName, sessionKey)) {
99
+ session = await getSessionData(opts.storageName, sessionKey);
100
+ }
101
+ if (!session || session.expiresAt <= now) {
102
+ deleteSessionKey = session ? sessionKey : void 0;
103
+ session = newSession(generateSessionId(), expiresAt);
104
+ sessionKey = getSessionStorageKey(opts.storagePrefix, session.id);
105
+ } else {
106
+ session.expiresAt = expiresAt;
107
+ }
108
+ }
109
+ session = session || newSession(generateSessionId(), expiresAt);
110
+ sessionKey = sessionKey || getSessionStorageKey(opts.storagePrefix, session.id);
111
+ if (deleteSessionKey && deleteSessionKey !== sessionKey) {
112
+ await removeSessionData(opts.storageName, deleteSessionKey);
113
+ }
114
+ await setSessionData(opts.storageName, sessionKey, session, opts.expiresIn / 1e3 | 0);
115
+ return session;
116
+ }
117
+ export async function updateSession(sessionId, input, opts) {
118
+ const now = Date.now();
119
+ const serverKey = `${opts.storagePrefix}:${sessionId}`;
120
+ const normalizedInput = omit(
121
+ input,
122
+ ["id", "issuedAt", "expiresAt"]
123
+ );
124
+ let session = await getSessionData(opts.storageName, serverKey);
125
+ if (!session) {
126
+ throw createError({
127
+ status: 500,
128
+ fatal: true,
129
+ message: `session '${sessionId}' was not found.`
130
+ });
131
+ }
132
+ session = deepMerge({}, session, normalizedInput);
133
+ session.expiresAt = now + opts.expiresIn;
134
+ const sessionToStore = { ...session };
135
+ if (opts.secret && sessionToStore.auth?.tokenSet) {
136
+ sessionToStore.auth = {
137
+ ...sessionToStore.auth,
138
+ tokenSet: encryptTokenSet(
139
+ sessionToStore.auth.tokenSet,
140
+ opts.secret
141
+ )
142
+ };
143
+ }
144
+ await setSessionData(opts.storageName, serverKey, sessionToStore, opts.expiresIn / 1e3 | 0);
145
+ return session;
146
+ }
147
+ export async function renewSession(sessionId, opts) {
148
+ let sessionKey = getSessionStorageKey(opts.storagePrefix, sessionId);
149
+ if (await hasSessionData(opts.storageName, sessionKey)) {
150
+ await removeSessionData(opts.storageName, sessionKey);
151
+ }
152
+ const session = newSession(generateSessionId(), Date.now() + opts.expiresIn);
153
+ sessionKey = getSessionStorageKey(opts.storagePrefix, session.id);
154
+ await setSessionData(opts.storageName, sessionKey, session, opts.expiresIn / 1e3 | 0);
155
+ return session;
156
+ }
157
+ export async function deleteSession(sessionId, opts) {
158
+ const sessionKey = getSessionStorageKey(opts.storagePrefix, sessionId);
159
+ if (await hasSessionData(opts.storageName, sessionKey)) {
160
+ await removeSessionData(opts.storageName, sessionKey);
161
+ }
162
+ }
163
+ export async function discoveryOpendId(wellKnownUrl, opts) {
164
+ const logger = useServerLogger({ tag: "authts-helper", level: 3 });
165
+ const {
166
+ cache: { prefix: cachedPrefix, expires },
167
+ clientId,
168
+ clientSecret
169
+ } = opts;
170
+ const storage = useStorage("cache");
171
+ const cacheKey = `${cachedPrefix}:${clientId}`;
172
+ let meta = await storage.getItem(cacheKey);
173
+ if (!meta) {
174
+ const isHttpEndpoint = wellKnownUrl.startsWith("http://");
175
+ const config = await discovery(
176
+ new URL(wellKnownUrl),
177
+ clientId,
178
+ {
179
+ client_id: clientId,
180
+ client_secret: clientSecret
181
+ },
182
+ void 0,
183
+ isHttpEndpoint ? { execute: [allowInsecureRequests] } : void 0
184
+ );
185
+ meta = {
186
+ server: config.serverMetadata(),
187
+ client: config.clientMetadata()
188
+ };
189
+ await storage.setItem(cacheKey, meta, { ttl: expires / 1e3 | 0 });
190
+ logger.info('Fetching OpenID Connect metadata from "%s"', wellKnownUrl);
191
+ }
192
+ return meta;
193
+ }
194
+ export async function getOpenIdConfiguration(wellKnownUrl, opts) {
195
+ const meta = await discoveryOpendId(wellKnownUrl, opts);
196
+ const configuration = new Configuration(meta.server, opts.clientId, meta.client);
197
+ if (wellKnownUrl.startsWith("http://")) {
198
+ allowInsecureRequests(configuration);
199
+ }
200
+ return configuration;
201
+ }
202
+ function getOpenIdRedirectUrl(origin, redirectUri) {
203
+ return new URL(redirectUri, origin.origin);
204
+ }
205
+ export async function buildAuthorizationUrl(session, opt) {
206
+ const {
207
+ wellKnownUrl,
208
+ cache,
209
+ clientId,
210
+ clientSecret,
211
+ origin,
212
+ redirectUri,
213
+ scopes,
214
+ usePkce,
215
+ codeChallengeMethod,
216
+ sessionExpires,
217
+ sessionStorageName,
218
+ sessionStoragePrefix,
219
+ sessionSecret = ""
220
+ } = opt;
221
+ const config = await getOpenIdConfiguration(wellKnownUrl, { cache, clientId, clientSecret });
222
+ const serverMeta = config.serverMetadata();
223
+ const state = randomState();
224
+ const results = {
225
+ state
226
+ };
227
+ const parameters = {
228
+ state,
229
+ redirect_uri: getOpenIdRedirectUrl(origin, redirectUri).toString(),
230
+ scope: scopes.join(" ")
231
+ };
232
+ if (usePkce && serverMeta.supportsPKCE(codeChallengeMethod)) {
233
+ const codeVerifier = randomPKCECodeVerifier();
234
+ parameters.code_challenge_method = codeChallengeMethod;
235
+ parameters.code_challenge = await calculatePKCECodeChallenge(codeVerifier);
236
+ results.pkceCodeVerifier = codeVerifier;
237
+ }
238
+ await updateSession(
239
+ session.id,
240
+ { auth: { status: "unauthenticated" } },
241
+ {
242
+ storageName: sessionStorageName,
243
+ storagePrefix: sessionStoragePrefix,
244
+ expiresIn: sessionExpires,
245
+ secret: sessionSecret
246
+ }
247
+ );
248
+ results.authorizeUrl = buildAuthorizationUrlOidc(config, parameters).toString();
249
+ return results;
250
+ }
251
+ export async function authorizationCodeGrant(authorizeParams, opts) {
252
+ const { wellKnownUrl, cache, clientId, clientSecret, origin, redirectUri, usePkce, codeChallengeMethod } = opts;
253
+ const config = await getOpenIdConfiguration(wellKnownUrl, { cache, clientId, clientSecret });
254
+ const serverMeta = config.serverMetadata();
255
+ const redirectUrl = getOpenIdRedirectUrl(origin, redirectUri);
256
+ if (authorizeParams.code) {
257
+ redirectUrl.searchParams.set("code", authorizeParams.code);
258
+ }
259
+ if (authorizeParams.state) {
260
+ redirectUrl.searchParams.set("state", authorizeParams.state);
261
+ }
262
+ const checks = {
263
+ expectedState: authorizeParams.checks?.expectedState
264
+ };
265
+ if (usePkce && serverMeta.supportsPKCE(codeChallengeMethod)) {
266
+ checks.pkceCodeVerifier = authorizeParams.checks?.pkceCodeVerifier;
267
+ }
268
+ const parameters = {
269
+ grant_type: "authorization_code",
270
+ client_id: config.clientMetadata().client_id,
271
+ client_secret: config.clientMetadata().client_secret,
272
+ redirect_uri: redirectUrl.toString()
273
+ };
274
+ return authorizationCodeGrantOidc(config, redirectUrl, checks, parameters);
275
+ }
276
+ async function refreshTokenGrant(refreshToken, opts) {
277
+ const { wellKnownUrl, ...discoveryOption } = opts;
278
+ const config = await getOpenIdConfiguration(wellKnownUrl, discoveryOption);
279
+ return refreshTokenGrantFromOidc(config, refreshToken);
280
+ }
281
+ export function constructTokenSet(input) {
282
+ let tokenType = input.token_type;
283
+ if (tokenType.toUpperCase() === "BEARER") {
284
+ tokenType = "Bearer";
285
+ }
286
+ return {
287
+ tokenType,
288
+ idToken: input.id_token,
289
+ accessToken: input.access_token,
290
+ refreshToken: input.refresh_token,
291
+ scopes: (input.scope || "").split(" "),
292
+ expiresAt: Date.now() + (input.expires_in || 0) * 1e3
293
+ };
294
+ }
295
+ export async function refreshTokenIfNeeded(session, opts) {
296
+ const logger = useServerLogger({ tag: "authts-helper", level: 3 });
297
+ let updateSession2 = {
298
+ auth: { status: "unauthenticated", tokenSet: void 0 },
299
+ user: getAnonymousUser()
300
+ };
301
+ if (session?.auth?.status === "authenticated" && session?.auth?.tokenSet) {
302
+ const { accessToken, refreshToken, expiresAt } = session.auth.tokenSet;
303
+ const accessExpired = Boolean(accessToken && expiresAt - opts.tokenRefreshBufferMs < Date.now());
304
+ if (!accessExpired) {
305
+ updateSession2 = {
306
+ auth: {
307
+ status: session.auth.status,
308
+ tokenSet: session.auth.tokenSet
309
+ },
310
+ user: session.user
311
+ };
312
+ } else if (accessExpired && refreshToken) {
313
+ const lockStorage = useStorage("cache");
314
+ const lockKey = `${opts.cache.prefix}:refresh-lock:${session.id}`;
315
+ const acquired = await tryAcquireLock(lockStorage, lockKey, 10, opts.distributedLock ?? false);
316
+ if (!acquired) {
317
+ logger.debug('[refreshTokenIfNeeded] refresh lock held for session "%s" \u2014 skipping', session.id);
318
+ return { auth: { status: session.auth.status, tokenSet: session.auth.tokenSet }, user: session.user };
319
+ }
320
+ logger.info('Refreshing access token for session "%s"', session.id);
321
+ const tokenSet = await refreshTokenGrant(refreshToken, {
322
+ wellKnownUrl: opts.wellKnownUrl,
323
+ cache: opts.cache,
324
+ clientId: opts.clientId,
325
+ clientSecret: opts.clientSecret
326
+ });
327
+ await lockStorage.removeItem(`${opts.cache.prefix}:userinfo:${session.id}`);
328
+ updateSession2 = {
329
+ auth: {
330
+ status: session.auth.status,
331
+ tokenSet: constructTokenSet(tokenSet)
332
+ },
333
+ user: session.user
334
+ };
335
+ }
336
+ }
337
+ return updateSession2;
338
+ }
339
+ export async function fetchUserInfo(accessToken, sub, opts) {
340
+ const { wellKnownUrl, cache, clientId, clientSecret } = opts;
341
+ const config = await getOpenIdConfiguration(wellKnownUrl, { cache, clientId, clientSecret });
342
+ return fetchUserInfoFromOidc(config, accessToken, sub);
343
+ }
344
+ export async function revokeTokens(tokens, opts) {
345
+ const { wellKnownUrl, cache, clientId, clientSecret } = opts;
346
+ const config = await getOpenIdConfiguration(wellKnownUrl, { cache, clientId, clientSecret });
347
+ return Promise.allSettled(
348
+ tokens.map((token) => !token ? Promise.resolve() : revokeToken(config, token))
349
+ );
350
+ }
@@ -0,0 +1 @@
1
+ export * as helpers from './helpers.js';
@@ -0,0 +1 @@
1
+ export * as helpers from "./helpers.js";
@@ -0,0 +1,19 @@
1
+ import type { Storage } from 'unstorage';
2
+ /**
3
+ * Attempts to acquire a distributed or optimistic mutex lock in Nitro cache storage.
4
+ *
5
+ * Two-tier strategy (D-01):
6
+ * - Tier 1 (atomic, `useAtomic: true`): Uses IORedis `SET NX EX` via the unstorage Redis driver's
7
+ * `getInstance()` accessor. Only effective when the Nitro `cache` mount is backed by a Redis driver.
8
+ * Falls through to optimistic path if the driver accessor is unavailable.
9
+ * - Tier 2 (optimistic, `useAtomic: false`): `hasItem` check followed by `setItem` with TTL.
10
+ * This is the Phase 7 behavior and remains the default path.
11
+ *
12
+ * @param storage - Prefixed cache storage (`useStorage('cache')`). Used for the optimistic path.
13
+ * @param lockKey - The lock key (without the cache prefix). Must be unique per session.
14
+ * @param ttlSeconds - Lock TTL in seconds. Lock expires automatically; no explicit release needed.
15
+ * @param useAtomic - When `true`, attempt atomic NX acquisition via Redis native client.
16
+ * @returns `true` if the lock was acquired by this caller, `false` if the lock was already held.
17
+ * @since 1.0.0
18
+ */
19
+ export declare function tryAcquireLock(storage: Storage, lockKey: string, ttlSeconds: number, useAtomic: boolean): Promise<boolean>;
@@ -0,0 +1,39 @@
1
+ import useServerLogger from "#devcoffee-core/server/composables/useServerLogger";
2
+ import { useStorage } from "nitropack/runtime";
3
+ const logger = useServerLogger({ tag: "authts-mutex", level: 3 });
4
+ export async function tryAcquireLock(storage, lockKey, ttlSeconds, useAtomic) {
5
+ if (useAtomic) {
6
+ try {
7
+ const baseStorage = useStorage();
8
+ const { driver, base } = baseStorage.getMount("cache:");
9
+ const client = driver.getInstance?.();
10
+ if (client) {
11
+ const prefixedKey = base + lockKey;
12
+ const result = await client.set(
13
+ prefixedKey,
14
+ "1",
15
+ "NX",
16
+ "EX",
17
+ ttlSeconds
18
+ );
19
+ if (result === "OK") {
20
+ logger.debug('[mutex] atomic lock acquired for key "%s"', lockKey);
21
+ return true;
22
+ }
23
+ logger.debug('[mutex] atomic lock held for key "%s" \u2014 skipping', lockKey);
24
+ return false;
25
+ }
26
+ logger.warn("[mutex] atomic lock unavailable (getInstance not present on driver) \u2014 falling back to optimistic");
27
+ } catch (err) {
28
+ logger.warn("[mutex] atomic lock failed (%s) \u2014 falling back to optimistic", String(err));
29
+ }
30
+ }
31
+ logger.debug('[mutex] using optimistic lock for key "%s"', lockKey);
32
+ const lockExists = await storage.hasItem(lockKey);
33
+ if (lockExists) {
34
+ logger.debug('[mutex] optimistic lock held for key "%s" \u2014 skipping', lockKey);
35
+ return false;
36
+ }
37
+ await storage.setItem(lockKey, true, { ttl: ttlSeconds });
38
+ return true;
39
+ }
@@ -0,0 +1,26 @@
1
+ import type { NuxtAuthOptions } from '#devcoffee-core/server/adapters/http';
2
+ /**
3
+ * Creates a universal authentication handler for Nuxt, integrating with OpenID Connect.
4
+ *
5
+ * Handles three main endpoints:
6
+ * - `/api/_auth/session`: Returns the current normalized session.
7
+ * - `/api/_auth/authorize-url`: Builds and returns the OpenID authorization URL.
8
+ * - `/api/_auth/token`: Handles the authorization code exchange and updates the session.
9
+ *
10
+ * @param options - Optional custom overrides for session/user mapping callbacks.
11
+ * @returns An h3 event handler to be registered under the `/api/_auth/*` routes.
12
+ *
13
+ * @example
14
+ * ```ts
15
+ * export default NuxtAuthtsHandler({
16
+ * userInfo: async (user, tokenSet) => ({
17
+ * id: user.sub,
18
+ * email: user.email ?? '',
19
+ * name: user.name ?? '',
20
+ * }),
21
+ * })
22
+ * ```
23
+ *
24
+ * @since 1.0.0
25
+ */
26
+ export default function NuxtAuthtsHandler(options?: DeepPartial<NuxtAuthOptions>): any;
@@ -0,0 +1,238 @@
1
+ import {
2
+ createError,
3
+ deleteCookie,
4
+ eventHandler,
5
+ getCookie,
6
+ getQuery,
7
+ getRequestURL,
8
+ readFormData,
9
+ setCookie,
10
+ useRuntimeConfig
11
+ } from "#devcoffee-core/server/adapters/http";
12
+ import { deepMerge, omit } from "#devcoffee-core/server/adapters/utils";
13
+ import { useStorage } from "nitropack/runtime";
14
+ import {
15
+ authorizationCodeGrant,
16
+ buildAuthorizationUrl,
17
+ constructTokenSet,
18
+ deleteSession,
19
+ fetchUserInfo,
20
+ getSession,
21
+ isSameOrigin,
22
+ revokeTokens,
23
+ updateSession
24
+ } from "./helpers.js";
25
+ const AUTH_API_PREFIX = "/api/_auth";
26
+ var AuthSupportAction = /* @__PURE__ */ ((AuthSupportAction2) => {
27
+ AuthSupportAction2["GET_SESSION"] = "session";
28
+ AuthSupportAction2["AUTHORIZE_URL"] = "authorize-url";
29
+ AuthSupportAction2["TOKEN"] = "token";
30
+ AuthSupportAction2["LOGOUT"] = "logout";
31
+ return AuthSupportAction2;
32
+ })(AuthSupportAction || {});
33
+ function getAuthAction(requestUrl) {
34
+ const parts = requestUrl.pathname.toLocaleLowerCase().replace(AUTH_API_PREFIX, "").split("/").filter(Boolean);
35
+ if (!parts[0] || !Object.values(AuthSupportAction).includes(parts[0]))
36
+ throw createError({
37
+ status: 500,
38
+ fatal: true,
39
+ statusMessage: "auth action not support",
40
+ message: `AuthSupportAction does not support '${parts[0]}'`
41
+ });
42
+ return parts[0];
43
+ }
44
+ const defaultNuxtAuthOptions = {
45
+ /**
46
+ * Default Devcoffee Nuxt AuthTS option callbacks for session and user mapping.
47
+ * These can be overridden by passing custom callbacks to `NuxtAuthtsHandler()`.
48
+ * @since 1.0.0
49
+ */
50
+ session: async (session, auth) => {
51
+ return Promise.resolve(
52
+ deepMerge(omit(session, ["issuedAt", "expiresAt"]), {
53
+ isAuthenticated: auth.status === "authenticated" && Boolean(auth.tokenSet && session.user.id)
54
+ })
55
+ );
56
+ },
57
+ /**
58
+ * Maps the OpenID Connect user info response to your local user schema.
59
+ *
60
+ * @param user - The raw user info response from the OpenID provider.
61
+ * @param opts - The token response containing access and ID tokens.
62
+ * @returns A normalized user object.
63
+ * @since 1.0.0
64
+ */
65
+ userInfo: async (user, opts) => user
66
+ };
67
+ export default function NuxtAuthtsHandler(options) {
68
+ const { enabled: authEnabled, sessions: sessionConfig, openid, auth } = useRuntimeConfig().nuxtCore.authts;
69
+ const nuxtAuthOptions = deepMerge({ ...defaultNuxtAuthOptions }, options || {});
70
+ return eventHandler(async (event) => {
71
+ const requestUrl = getRequestURL(event);
72
+ const queryParams = getQuery(event);
73
+ const authAction = getAuthAction(requestUrl);
74
+ let session = await getSession(event.context.sessionId, {
75
+ storageName: sessionConfig.storage.name,
76
+ storagePrefix: sessionConfig.storage.prefix,
77
+ secret: sessionConfig.secret || ""
78
+ });
79
+ if (!session) {
80
+ throw createError({
81
+ status: 500,
82
+ fatal: true,
83
+ message: `session '${event.context.sessionId}' was not found!`
84
+ });
85
+ }
86
+ if (!authEnabled && !["session" /* GET_SESSION */].includes(authAction)) {
87
+ throw createError({
88
+ status: 500,
89
+ fatal: true,
90
+ message: `Action '${authAction}' is disabled!`
91
+ });
92
+ }
93
+ const authCookieOpts = omit(sessionConfig.cookieOpts, ["expires", "maxAge"]);
94
+ let redirectUrl;
95
+ let tokenSet;
96
+ switch (authAction) {
97
+ case "session" /* GET_SESSION */:
98
+ const refetchSessionData = {};
99
+ if (openid.autoFetchUser && session.auth?.status === "authenticated" && session.auth?.tokenSet) {
100
+ const cacheStorage2 = useStorage("cache");
101
+ const userInfoCacheKey = `${openid.cache.prefix}:userinfo:${session.id}`;
102
+ let cachedUser = await cacheStorage2.getItem(userInfoCacheKey);
103
+ if (!cachedUser) {
104
+ cachedUser = await nuxtAuthOptions.userInfo(session.user, { tokenSet: session.auth.tokenSet });
105
+ await cacheStorage2.setItem(userInfoCacheKey, cachedUser, { ttl: openid.autoFetchUserTtl });
106
+ }
107
+ refetchSessionData.user = cachedUser;
108
+ }
109
+ if (Object.keys(refetchSessionData).length > 0) {
110
+ session = await updateSession(session.id, refetchSessionData, {
111
+ storageName: sessionConfig.storage.name,
112
+ storagePrefix: sessionConfig.storage.prefix,
113
+ expiresIn: sessionConfig.expiresIn,
114
+ secret: sessionConfig.secret || ""
115
+ });
116
+ }
117
+ event.context.sessionId = session.id;
118
+ return nuxtAuthOptions.session(omit(session, ["auth"]), session.auth);
119
+ case "authorize-url" /* AUTHORIZE_URL */:
120
+ const { authorizeUrl, state, pkceCodeVerifier } = await buildAuthorizationUrl(session, {
121
+ codeChallengeMethod: openid.codeChallengeMethod,
122
+ sessionExpires: sessionConfig.expiresIn,
123
+ cache: openid.cache,
124
+ origin: requestUrl,
125
+ wellKnownUrl: openid.wellKnownUrl,
126
+ usePkce: openid.usePkce,
127
+ clientId: openid.clientId,
128
+ clientSecret: openid.clientSecret,
129
+ redirectUri: openid.redirectUri,
130
+ scopes: openid.scopes,
131
+ sessionStorageName: sessionConfig.storage.name,
132
+ sessionStoragePrefix: sessionConfig.storage.prefix
133
+ });
134
+ if (pkceCodeVerifier) {
135
+ setCookie(event, sessionConfig.names.pkce, pkceCodeVerifier, authCookieOpts);
136
+ }
137
+ if (queryParams.redirectUrl) {
138
+ const candidateUrl = String(queryParams.redirectUrl);
139
+ if (!isSameOrigin(candidateUrl, requestUrl)) {
140
+ throw createError({
141
+ status: 400,
142
+ statusMessage: "redirect URL must be same-origin",
143
+ message: `Redirect URL '${candidateUrl}' is not same-origin with '${requestUrl.origin}'`
144
+ });
145
+ }
146
+ setCookie(event, sessionConfig.names.redirectUrl, candidateUrl, authCookieOpts);
147
+ }
148
+ setCookie(event, sessionConfig.names.state, state, authCookieOpts);
149
+ event.context.sessionId = session.id;
150
+ return { redirectUrl: authorizeUrl };
151
+ case "token" /* TOKEN */:
152
+ const formData = await readFormData(event);
153
+ const openIdTokenSet = await authorizationCodeGrant(
154
+ {
155
+ code: formData.get("code") || void 0,
156
+ state: formData.get("state") || void 0,
157
+ checks: {
158
+ expectedState: getCookie(event, sessionConfig.names.state),
159
+ pkceCodeVerifier: getCookie(event, sessionConfig.names.pkce)
160
+ }
161
+ },
162
+ {
163
+ cache: openid.cache,
164
+ origin: requestUrl,
165
+ usePkce: openid.usePkce,
166
+ clientId: openid.clientId,
167
+ redirectUri: openid.redirectUri,
168
+ wellKnownUrl: openid.wellKnownUrl,
169
+ clientSecret: openid.clientSecret,
170
+ codeChallengeMethod: openid.codeChallengeMethod
171
+ }
172
+ );
173
+ const candidateRedirect = getCookie(event, sessionConfig.names.redirectUrl);
174
+ if (candidateRedirect && !isSameOrigin(candidateRedirect, requestUrl)) {
175
+ throw createError({
176
+ status: 400,
177
+ statusMessage: "redirect URL must be same-origin",
178
+ message: `Redirect URL '${candidateRedirect}' is not same-origin with '${requestUrl.origin}'`
179
+ });
180
+ }
181
+ redirectUrl = candidateRedirect || auth.defaultLoginRedirectUri;
182
+ Array.of(sessionConfig.names.state, sessionConfig.names.pkce, sessionConfig.names.redirectUrl).forEach(
183
+ (cookie) => deleteCookie(event, cookie, authCookieOpts)
184
+ );
185
+ tokenSet = constructTokenSet(openIdTokenSet);
186
+ const sessionData = {
187
+ auth: {
188
+ status: "authenticated",
189
+ tokenSet
190
+ }
191
+ };
192
+ if (openid.fetchUserOnLogin) {
193
+ const openidUser = await fetchUserInfo(tokenSet.accessToken, openIdTokenSet.claims()?.sub || "", {
194
+ cache: openid.cache,
195
+ clientId: openid.clientId,
196
+ clientSecret: openid.clientSecret,
197
+ wellKnownUrl: openid.wellKnownUrl
198
+ });
199
+ sessionData.user = await nuxtAuthOptions.userInfo(session.user, { openidUser, tokenSet });
200
+ }
201
+ await updateSession(session.id, sessionData, {
202
+ storageName: sessionConfig.storage.name,
203
+ storagePrefix: sessionConfig.storage.prefix,
204
+ expiresIn: sessionConfig.expiresIn,
205
+ secret: sessionConfig.secret || ""
206
+ });
207
+ event.context.sessionId = session.id;
208
+ return { redirectUrl };
209
+ case "logout" /* LOGOUT */:
210
+ redirectUrl = auth.defaultLogoutRedirectUri;
211
+ tokenSet = session.auth.tokenSet;
212
+ if (tokenSet) {
213
+ await revokeTokens([tokenSet.accessToken, tokenSet.refreshToken, tokenSet.idToken], {
214
+ cache: openid.cache,
215
+ clientId: openid.clientId,
216
+ clientSecret: openid.clientSecret,
217
+ wellKnownUrl: openid.wellKnownUrl
218
+ });
219
+ }
220
+ await deleteSession(session.id, {
221
+ storageName: sessionConfig.storage.name,
222
+ storagePrefix: sessionConfig.storage.prefix
223
+ });
224
+ const cacheStorage = useStorage("cache");
225
+ await cacheStorage.removeItem(`${openid.cache.prefix}:userinfo:${session.id}`);
226
+ deleteCookie(event, sessionConfig.names.sessionId, authCookieOpts);
227
+ event.context.sessionId = "";
228
+ return { redirectUrl };
229
+ default:
230
+ break;
231
+ }
232
+ throw createError({
233
+ status: 500,
234
+ fatal: true,
235
+ message: `Action '${authAction}' is disabled!`
236
+ });
237
+ });
238
+ }