UNPKG

@analog-tools/auth

Version:

Authentication module for AnalogJS applications

1,625 lines (1,624 loc) 58.7 kB
import { createError as d, sendRedirect as b, getQuery as O, getRequestHeaders as Z, getRequestURL as q, getHeader as U } from "h3"; import { getSession as m, createUnstorageStore as X, useSession as Q, updateSession as y, destroySession as ee } from "@analog-tools/session"; import { TRPCError as te } from "@trpc/server"; var g = /* @__PURE__ */ ((r) => (r.SkyBlue = "\x1B[94m", r.OceanBlue = "\x1B[34m", r.MidnightBlue = "\x1B[38;5;17m", r.SkyBlueBg = "\x1B[104m", r.OceanBlueBg = "\x1B[44m", r.MidnightBlueBg = "\x1B[48;5;17m", r.MintGreen = "\x1B[92m", r.ForestGreen = "\x1B[32m", r.EmeraldGreen = "\x1B[38;5;28m", r.MintGreenBg = "\x1B[102m", r.ForestGreenBg = "\x1B[42m", r.EmeraldGreenBg = "\x1B[48;5;28m", r.LemonYellow = "\x1B[93m", r.SunflowerYellow = "\x1B[33m", r.GoldYellow = "\x1B[38;5;220m", r.LemonYellowBg = "\x1B[103m", r.SunflowerYellowBg = "\x1B[43m", r.GoldYellowBg = "\x1B[48;5;220m", r.RoseRed = "\x1B[91m", r.FireRed = "\x1B[31m", r.BurgundyRed = "\x1B[38;5;88m", r.RoseRedBg = "\x1B[101m", r.FireRedBg = "\x1B[41m", r.BurgundyRedBg = "\x1B[48;5;88m", r.LavenderPurple = "\x1B[95m", r.RoyalPurple = "\x1B[38;5;93m", r.DeepPurple = "\x1B[38;5;54m", r.LavenderPurpleBg = "\x1B[105m", r.RoyalPurpleBg = "\x1B[48;5;93m", r.DeepPurpleBg = "\x1B[48;5;54m", r.PeachOrange = "\x1B[38;5;215m", r.TangerineOrange = "\x1B[38;5;208m", r.AmberOrange = "\x1B[38;5;214m", r.PeachOrangeBg = "\x1B[48;5;215m", r.TangerineOrangeBg = "\x1B[48;5;208m", r.AmberOrangeBg = "\x1B[48;5;214m", r.SilverGray = "\x1B[37m", r.SlateGray = "\x1B[90m", r.CharcoalGray = "\x1B[38;5;238m", r.SilverGrayBg = "\x1B[47m", r.SlateGrayBg = "\x1B[100m", r.CharcoalGrayBg = "\x1B[48;5;238m", r.PureBlack = "\x1B[30m", r.PureWhite = "\x1B[97m", r.PureBlackBg = "\x1B[40m", r.PureWhiteBg = "\x1B[107m", r.Cyan = "\x1B[36m", r.Reset = "\x1B[0m", r.Bold = "\x1B[1m", r.Dim = "\x1B[2m", r.Underline = "\x1B[4m", r.Blink = "\x1B[5m", r.Reverse = "\x1B[7m", r.Hidden = "\x1B[8m", r))(g || {}), c = /* @__PURE__ */ ((r) => (r[r.trace = 0] = "trace", r[r.debug = 1] = "debug", r[r.info = 2] = "info", r[r.warn = 3] = "warn", r[r.error = 4] = "error", r[r.fatal = 5] = "fatal", r[r.silent = 6] = "silent", r))(c || {}); function D(r) { return ["trace", "debug", "info", "warn", "error", "fatal", "silent"].includes(r); } const C = class C { /** * Generate a cache key for memoization (includes serialization options) * @private */ static getCacheKey(e, t, s, i) { if (!e.stack) return null; const n = e.stack.split(` `)[0] || ""; return `${e.name}:${e.message}:${n}:${t}:${s}:${i}`; } /** * Add to cache with size limit enforcement * @private */ static addToCache(e, t) { if (this.serializationCache.size >= this.MAX_CACHE_SIZE) { const s = this.serializationCache.keys().next().value; s && this.serializationCache.delete(s); } this.serializationCache.set(e, t); } /** * Safely serialize any value (Error objects, plain objects, primitives) for logging * * @param error - The value to serialize (Error, object, string, etc.) * @param options - Configuration options for serialization behavior * @returns Serialized representation - StructuredError for Error objects, string for others * * @example * ```typescript * // Serialize an Error object * const error = new Error('Failed to connect'); * const result = ErrorSerializer.serialize(error); * // Returns: { message: 'Failed to connect', name: 'Error', stack: '...' } * * // Serialize a plain object * const obj = { userId: '123', action: 'login' }; * const result = ErrorSerializer.serialize(obj); * // Returns: '{\n "userId": "123",\n "action": "login"\n}' * * // Serialize with options * const result = ErrorSerializer.serialize(error, { includeStack: false }); * ``` */ static serialize(e, t = {}) { const { includeStack: s = !0, maxDepth: i = C.DEFAULT_MAX_DEPTH, includeNonEnumerable: n = !1 } = t; if (e instanceof Error) { const o = this.getCacheKey(e, s, i, n); if (o) { const h = this.serializationCache.get(o); if (h) return h; } const l = this.serializeError(e, s, i, n, /* @__PURE__ */ new WeakSet()); return o && this.addToCache(o, l), l; } if (typeof e == "string") return e; const a = this.safeStringify(e, i, /* @__PURE__ */ new WeakSet()); if (typeof a == "string") return a; try { return JSON.stringify(a, null, 2); } catch { return String(a); } } /** * Serialize a standard Error object * @private */ static serializeError(e, t, s, i, n = /* @__PURE__ */ new WeakSet()) { if (n.has(e)) return { message: e.message, name: e.name, [Symbol.for("circular")]: this.CIRCULAR_REF_PLACEHOLDER }; n.add(e); const a = { message: e.message, name: e.name }; return t && e.stack && (a.stack = e.stack), "cause" in e && e.cause !== void 0 && (e.cause instanceof Error ? a.cause = this.serializeError(e.cause, t, s - 1, i, n) : a.cause = this.safeStringify(e.cause, s - 1, n)), Object.keys(e).forEach((o) => { if (!(o in a)) try { const l = e; a[o] = this.safeStringify(l[o], s - 1, n); } catch { a[o] = this.UNABLE_TO_SERIALIZE; } }), i && Object.getOwnPropertyNames(e).forEach((o) => { if (!(o in a) && o !== "stack" && o !== "message" && o !== "name") try { const l = Object.getOwnPropertyDescriptor(e, o); if (l && l.enumerable === !1) { const h = e; a[o] = this.safeStringify(h[o], s - 1, n); } } catch { a[o] = this.UNABLE_TO_SERIALIZE; } }), a; } /** * Safely stringify any object with circular reference detection * @private */ static safeStringify(e, t, s = /* @__PURE__ */ new WeakSet()) { if (t <= 0) return this.MAX_DEPTH_PLACEHOLDER; if (e === null || typeof e != "object") return e; if (s.has(e)) return this.CIRCULAR_REF_PLACEHOLDER; s.add(e); try { if (Array.isArray(e)) { const n = e.map((a) => this.safeStringify(a, t - 1, s)); return s.delete(e), n; } const i = {}; for (const [n, a] of Object.entries(e)) try { i[n] = this.safeStringify(a, t - 1, s); } catch { i[n] = this.UNABLE_TO_SERIALIZE; } return s.delete(e), i; } catch { return s.delete(e), this.UNABLE_TO_SERIALIZE; } } }; C.DEFAULT_MAX_DEPTH = 10, C.CIRCULAR_REF_PLACEHOLDER = "[Circular Reference]", C.MAX_DEPTH_PLACEHOLDER = "[Max Depth Reached]", C.UNABLE_TO_SERIALIZE = "[Unable to serialize]", C.serializationCache = /* @__PURE__ */ new Map(), C.MAX_CACHE_SIZE = 100; let v = C; const H = { highlight: { color: g.LemonYellow, bold: !0 }, accent: { color: g.SkyBlue }, attention: { color: g.RoyalPurple, bold: !0 }, success: { color: g.ForestGreen }, warning: { color: g.TangerineOrange }, error: { color: g.FireRed }, info: { color: g.OceanBlue }, debug: { color: g.SlateGray } }, W = { success: "✅", warning: "⚠️", error: "❌", info: "ℹ️", debug: "🐞" }, se = { trace: g.SlateGray, // Gray for trace (lowest level) debug: g.Cyan, // Cyan for debug info info: g.ForestGreen, // Green for general info warn: g.SunflowerYellow, // Standard yellow for warnings error: g.FireRed, // Red for errors fatal: g.FireRed, // Bold + standard red for fatal errors silent: g.Reset // Reset for silent }, M = class M { /** * Create a new LoggerStyleEngine * @param config Configuration for the style engine */ constructor(e = {}) { this.styleCache = /* @__PURE__ */ new Map(); const t = process.env.NODE_ENV === "test" || process.env.VITEST === "true"; this.useColors = e.useColors !== void 0 ? e.useColors : !t, this.globalStyles = { ...H, ...e.styles }, this.globalIcons = { ...W, ...e.icons }; } /** * Public method to resolve a style definition and return the ANSI color code (with caching) * @param style Style definition (semantic name or custom config) * @returns ANSI color code or undefined if invalid */ resolveStyle(e, t = "test", s) { return this.applyStyle(e, t, s); } /** * Enable or disable colored output * @param enabled Whether colors should be enabled */ setUseColors(e) { this.useColors = e; } /** * Check if colors are enabled * @returns Whether colors are enabled */ getUseColors() { return this.useColors; } /** * Update style and icon configuration * @param styles Partial style configuration to merge * @param icons Partial icon configuration to merge */ updateStyleConfig(e, t) { this.globalStyles = { ...this.globalStyles, ...e }, this.globalIcons = { ...this.globalIcons, ...t }; } /** * Format a log message with color and proper prefix * @param level Log level for the message * @param message The message to format * @param loggerName The name of the logger * @param context Optional context for the logger * @param overrideColor Optional color override for the message * @returns Formatted message with color */ formatMessage(e, t, s, i, n) { const a = i ? `[${s}:${i}]` : `[${s}]`; if (this.useColors) { let o = this.getColorForLevel(e); return n && (o = n), `${o}${a} ${t}${g.Reset}`; } else return `${a} ${t}`; } /** * Format message with metadata-based styling and icons * @param level Log level * @param message Message to format * @param loggerName The name of the logger * @param styling Optional metadata for styling * @param context Optional context for the logger * @returns Formatted message with styling and icons */ formatMessageWithMetadata(e, t, s, i, n) { let a = t, o = this.getColorForLevel(e); if (i?.style) { const l = this.applyStyle(i.style, s, n); l && (o = l); } return i?.icon && (a = `${this.resolveIcon(i.icon, s, n)} ${t}`), this.formatMessage(e, a, s, n, o); } /** * Parse metadata parameter to separate metadata from additional data * @param metadataOrData Could be LogStyling or additional data * @param data Additional data parameters * @returns Parsed metadata and remaining data */ parseMetadataParameter(e, t = []) { if (e && typeof e == "object" && !Array.isArray(e) && ("style" in e || "icon" in e)) return { metadata: e, restData: t }; const s = e !== void 0 ? [e, ...t] : t; if (s.length > 0) { const i = s[s.length - 1]; if (i && typeof i == "object" && !Array.isArray(i) && ("style" in i || "icon" in i)) return { metadata: i, restData: s.slice(0, -1) // Return all but the last parameter }; } return { metadata: void 0, restData: s }; } /** * Get color for a specific log level * @param level The log level * @returns ANSI color code for the log level * @private */ getColorForLevel(e) { const t = c[e], s = se[t]; return e === c.fatal ? `${g.Bold}${s}` : s || g.Reset; } /** * Apply style configuration and return ANSI color code * @param style Style configuration (semantic name or custom config) * @param loggerName Logger name for error messages * @param context Optional context for error messages * @returns ANSI color code or undefined if invalid * @private */ applyStyle(e, t, s) { const i = this.getStyleCacheValue(e); if (i !== void 0) return i; if (typeof e == "string") { const n = this.getSemanticStyleColor(e, t, s); return this.setStyleCache(e, n), n; } if (typeof e == "object" && "color" in e) { if (!this.isValidColor(e.color)) { this.setStyleCache(e, void 0), this.logWarning("Invalid color provided. Only predefined ColorEnum values are allowed.", t, s); return; } const n = this.constructStyleCode(e); return this.setStyleCache(e, n), n; } this.setStyleCache(e, void 0), this.logWarning("Unknown style configuration provided. Only semantic style names or valid ColorEnum objects are allowed.", t, s); } // Helper: Validate color isValidColor(e) { return Object.values(g).includes(e); } // Helper: Validate icon isValidIcon(e) { return this.isEmojiIcon(e); } // Helper: Memoization get/set getStyleCacheValue(e) { return this.styleCache.has(e) ? this.styleCache.get(e) : void 0; } setStyleCache(e, t) { this.styleCache.set(e, t); } // Helper: Error logging logWarning(e, t, s) { const i = s ? `${t}:${s}` : t; console.warn(`[${i}] ${e}`); } // Helper: Style code construction constructStyleCode(e) { let t = e.color.toString(); return e.bold && (t += g.Bold), e.underline && (t += g.Underline), t; } /** * Get ANSI color code for semantic style names * @param styleName Semantic style name * @param loggerName Logger name for error messages * @param context Optional context for error messages * @returns ANSI color code or undefined if unknown * @private */ getSemanticStyleColor(e, t, s) { if (this.styleCache.has(e)) return this.styleCache.get(e); const i = this.globalStyles[e]; if (i) { let a = i.color.toString(); return i.bold && (a += g.Bold), i.underline && (a += g.Underline), this.styleCache.set(e, a), a; } const n = s ? `${t}:${s}` : t; console.warn( `[${n}] Unknown semantic style: ${e}. Falling back to default.` ), this.styleCache.set(e, void 0); } /** * Expose style cache for diagnostics/testing */ getStyleCache() { return this.styleCache; } /** * Resolve icon to emoji character * @param icon Icon name or custom string * @param loggerName Logger name for error messages * @param context Optional context for error messages * @returns Emoji character * @private */ resolveIcon(e, t, s) { if (this.isValidIcon(e)) return e; const i = [ "success", "warning", "error", "info", "debug" ], n = i.find((a) => a === e); return n && this.globalIcons[n] ? this.globalIcons[n] : (i.includes(e) ? this.logWarning(`Unknown icon: ${e}. Expected a valid emoji or semantic icon name.`, t, s) : this.logWarning(`Invalid icon: ${e}. Expected a valid emoji or semantic icon name.`, t, s), e); } /** * Check if the provided icon is a valid emoji from our Icon type * @param icon Icon to check * @returns True if it's a valid emoji icon * @private */ isEmojiIcon(e) { return [ "✅", "⚠️", "❌", "ℹ️", "🐞", "⭐️", "🚀", "🔥", "✔️", "✖️", "❓", "🔒", "🔓", "⏳", "🕒", "⬆️", "⬇️", "➡️", "⬅️", "📁", "📄", "👤", "👥", "✏️", "➕", "➖", "🔔", "⚡️", "🎁", "🐛", "🌟", "❤️", "👀", "⚙️", "🔧", "🔨", "🔑", "🎉", "📝", "🚨", "📅", "💡", "🔍", "🔗", "🔖", "📌", "📎", "✉️", "📞", "🌍", "☁️", "🌈", "🌙", "☀️", "❄️", "✨", "🎵", "📷", "🎥", "🎤", "🔊", "🔋", "🗑️", "💰", "💳", "🎂", "🏅", "🏆", "👑", "🛸", "🛡️", "🛑", "▶️", "⏸️", "⏺️", "⏪", "⏩", "🔁", "🔀", "🎲", "🎈", "🍪", "☕️", "🍵", "🍺", "🍷", "🍕", "🍔", "🍟", "🍎", "🍌", "🍒", "🍋", "🥕", "🌽", "🥦", "🥚", "🧀", "🍞", "🍰", "🍦", "🍫", "🍿", "🥓", "🍤", "🐟", "🦀", "🐙", "🐋", "🐬", "🐧", "🐸", "🐢", "🐍", "🐉", "🦄", "🐱", "🐶", "🐭", "🐰", "🐻", "🐼", "🐨", "🐯", "🦁", "🐒", "🐘", "🐎", "🐄", "🐖", "🐑", "🐔", "🦆", "🦢", "🦉", "🦅", "🦜", "🦚", "🦩", "🦋", "🐝", "🐜", "🐞", "🕷️", "🦂", "🐌", "🪱", "🐛", "🦗", "🦟", "🪰", "🪳", "🪲" ].includes(e); } }; M.INJECTABLE = !0; let L = M; class B extends Error { constructor(e) { super(e), this.name = "LoggerError"; } } const $ = { windowMs: 5e3, // 5 seconds flushOnCritical: !0 }, ie = [c.error, c.fatal]; class re { constructor(e, t) { this.config = e, this.formatMessage = t, this.entries = /* @__PURE__ */ new Map(); } /** * Add a message to the deduplicator */ addMessage(e, t, s = "") { if (!this.config.enabled || this.config.flushOnCritical && ie.includes(e)) return !0; const i = this.generateFingerprint(t, e, s), n = this.entries.get(i); return n ? n.count++ : this.entries.set(i, { message: t, level: e, context: s, firstSeen: Date.now(), count: 1 }), this.scheduleFlush(), !1; } /** * Flush all pending messages immediately */ flush() { if (this.entries.size !== 0) { this.flushTimer && (clearTimeout(this.flushTimer), this.flushTimer = void 0); for (const e of this.entries.values()) this.outputMessage(e); this.entries.clear(); } } /** * Destroy the deduplicator and clean up resources */ destroy() { this.flushTimer && (clearTimeout(this.flushTimer), this.flushTimer = void 0), this.entries.clear(); } /** * Generate a fingerprint for message deduplication */ generateFingerprint(e, t, s) { return `${t}:${s}:${e}`; } /** * Schedule a flush after the configured window */ scheduleFlush() { this.flushTimer || (this.flushTimer = setTimeout(() => { this.flush(); }, this.config.windowMs)); } /** * Output a batched message with count if needed */ outputMessage(e) { let t = e.message; e.count > 1 && (t = `${t} (×${e.count})`); const s = this.formatMessage(e.level, t, e.context); switch (e.level) { case c.trace: console.trace(s); break; case c.debug: console.debug(s); break; case c.info: console.info(s); break; case c.warn: console.warn(s); break; case c.error: console.error(s); break; case c.fatal: console.error(s); break; } } } const I = class I { /** * Create a new LoggerService * @param config The logger configuration * @param styleEngine Optional style engine (will be created if not provided) * @param context Optional context for child logger * @param parent Optional parent logger for child logger */ constructor(e = {}, t, s, i) { if (this.config = e, this.childLoggers = {}, this.disabledContexts = [], this.activeGroups = [], i) this.parentLogger = i, this.name = i.name, this.context = s, this.logLevel = i.getLogLevel(), this.styleEngine = i.styleEngine; else { if (typeof e.level == "string" && !Object.keys(c).includes(e.level)) throw new B(`Invalid log level: ${e.level}`); if (this.logLevel = this.castLoglevel( e.level || process.env.LOG_LEVEL || "info" ), this.name = e.name || "analog-tools", this.setDisabledContexts( e.disabledContexts ?? process.env.LOG_DISABLED_CONTEXTS?.split(",") ?? [] ), this.styleEngine = t || new L({ useColors: e.useColors, styles: { ...H, ...e.styles }, icons: { ...W, ...e.icons } }), e.deduplication?.enabled) { const n = { enabled: !0, windowMs: e.deduplication.windowMs ?? $.windowMs, flushOnCritical: e.deduplication.flushOnCritical ?? $.flushOnCritical }, a = (o, l, h) => this.styleEngine.formatMessage(o, l, this.name, h); this.deduplicator = new re(n, a); } } } /** * Check if this logger's context is enabled * Always returns true for root logger */ isContextEnabled() { return this.context ? !(this.parentLogger || this).disabledContexts.includes(this.context) : !0; } /** * Set disabled contexts for this logger * @param contexts Array of context names to disable */ setDisabledContexts(e) { this.disabledContexts = e; } /** * Get the current log level * @returns The current log level */ getLogLevel() { return this.logLevel; } /** * Get disabled contexts * @returns Array of disabled context names */ getDisabledContexts() { return (this.parentLogger || this).disabledContexts || []; } /** * Enable or disable colored output * @param enabled Whether colors should be enabled */ setUseColors(e) { (this.parentLogger || this).styleEngine.setUseColors(e); } /** * Check if colors are enabled * @returns Whether colors are enabled */ getUseColors() { return (this.parentLogger || this).styleEngine.getUseColors(); } /** * Create a child logger with a specific context * @param context The context for the child logger * @returns A new logger instance with the given context */ forContext(e) { return this.childLoggers[e] || (this.childLoggers[e] = new I({}, void 0, e, this)), this.childLoggers[e]; } /** * Start a new log group with the given name * All subsequent log messages will be indented * @param groupName The name of the group */ group(e) { if (!this.isContextEnabled()) return; (this.parentLogger?.activeGroups || this.activeGroups).push(e); const s = this.styleEngine.formatMessage( c.info, `Group: ${e}`, this.name, this.context ); console.group(`${s} ▼`); } /** * End a log group with the given name * Subsequent log messages will no longer be indented * @param groupName The name of the group (optional, will end the most recent group if not provided) */ groupEnd(e) { if (!this.isContextEnabled()) return; const t = this.parentLogger?.activeGroups || this.activeGroups; if (e) { const s = t.lastIndexOf(e); if (s !== -1) { const i = t.splice(s); for (let n = 0; n < i.length; n++) console.groupEnd(), console.log(""); } } else t.length > 0 && (t.pop(), console.groupEnd(), console.log("")); } trace(e, t, ...s) { if (!this.isContextEnabled() || this.logLevel > c.trace) return; const { metadata: i, restData: n } = this.styleEngine.parseMetadataParameter( t, s ); if (this.handleDeduplication(c.trace, e, i, n)) if (i) { const a = this.styleEngine.formatMessageWithMetadata( c.trace, e, this.name, i, this.context ); console.trace(a, ...n || []); } else { const a = this.styleEngine.formatMessage( c.trace, e, this.name, this.context ); console.trace(a, ...n || []); } } debug(e, t, ...s) { if (!this.isContextEnabled() || this.logLevel > c.debug) return; const { metadata: i, restData: n } = this.styleEngine.parseMetadataParameter( t, s ); if (this.handleDeduplication(c.debug, e, i, n)) if (i) { const a = this.styleEngine.formatMessageWithMetadata( c.debug, e, this.name, i, this.context ); console.debug(a, ...n || []); } else { const a = this.styleEngine.formatMessage( c.debug, e, this.name, this.context ); console.debug(a, ...n || []); } } info(e, t, ...s) { if (!this.isContextEnabled() || this.logLevel > c.info) return; const { metadata: i, restData: n } = this.styleEngine.parseMetadataParameter( t, s ); if (this.handleDeduplication(c.info, e, i, n)) if (i) { const a = this.styleEngine.formatMessageWithMetadata( c.info, e, this.name, i, this.context ); console.info(a, ...n || []); } else { const a = this.styleEngine.formatMessage( c.info, e, this.name, this.context ); console.info(a, ...n || []); } } warn(e, t, ...s) { if (!this.isContextEnabled() || this.logLevel > c.warn) return; const { metadata: i, restData: n } = this.styleEngine.parseMetadataParameter( t, s ); if (this.handleDeduplication(c.warn, e, i, n)) if (i) { const a = this.styleEngine.formatMessageWithMetadata( c.warn, e, this.name, i, this.context ); console.warn(a, ...n || []); } else { const a = this.styleEngine.formatMessage( c.warn, e, this.name, this.context ); console.warn(a, ...n || []); } } error(e, t, s, ...i) { if (!this.isContextEnabled() || this.logLevel > c.error) return; const { message: n, serializedError: a, context: o, data: l } = this.parseErrorParameters( e, t, s, i ), h = l || []; let u, S = h; if (h.length > 0) { const p = h[h.length - 1]; p && typeof p == "object" && !Array.isArray(p) && ("style" in p || "icon" in p) && (u = p, S = h.slice(0, -1)); } const k = [this.styleEngine.formatMessageWithMetadata( c.error, n, this.name, u, this.context )]; a && k.push(a), o && k.push(o), console.error(...k, ...S); } fatal(e, t, s, ...i) { if (!this.isContextEnabled() || this.logLevel > c.fatal) return; if (typeof e == "string" && t && typeof t == "object" && !Array.isArray(t) && ("style" in t || "icon" in t)) { const p = t, J = this.styleEngine.formatMessageWithMetadata( c.fatal, `FATAL: ${e}`, this.name, p, this.context ); console.error(J); return; } const { message: n, serializedError: a, context: o, data: l } = this.parseErrorParameters( e, t, s, i ), h = l || []; let u, S = h; if (h.length > 0) { const p = h[h.length - 1]; p && typeof p == "object" && !Array.isArray(p) && ("style" in p || "icon" in p) && (u = p, S = h.slice(0, -1)); } const k = [this.styleEngine.formatMessageWithMetadata( c.fatal, `FATAL: ${n}`, this.name, u, this.context )]; a && k.push(a), o && k.push(o), console.error(...k, ...S); } /** * Parse and normalize error method parameters to support multiple overloads * * Handles various parameter combinations: * - error(Error) -> extracts message and serializes error * - error(message) -> uses message as-is * - error(message, Error) -> uses message and serializes error * - error(message, LogContext) -> uses message and context * - error(message, Error, LogContext) -> uses all three with proper typing * - error(message, ...data) -> backwards compatibility mode * * @private * @param messageOrError - String message or Error object * @param errorOrContext - Error object, LogContext, or additional data * @param contextOrData - LogContext or additional data * @param additionalData - Extra data for backwards compatibility * @returns Parsed parameters with normalized structure */ parseErrorParameters(e, t, s, i = []) { if (e instanceof Error && t === void 0) return { message: e.message, serializedError: v.serialize(e), data: [] }; if (typeof e == "string" && t === void 0) return { message: e, data: [] }; if (typeof e == "string" && t instanceof Error && s === void 0) return { message: e, serializedError: v.serialize(t), data: [] }; if (typeof e == "string" && this.isLogContext(t) && s === void 0) return { message: e, context: t, data: [] }; if (typeof e == "string" && t instanceof Error && this.isLogContext(s)) return { message: e, serializedError: v.serialize(t), context: s, data: i }; const n = typeof e == "string" ? e : "Unknown error", a = t ? v.serialize(t) : void 0, o = s !== void 0 ? [s, ...i] : i; return { message: n, serializedError: a, data: o }; } /** * Type guard to check if an object is LogMetadata * * LogMetadata is defined as a plain object that is not: * - null or undefined * - An Array * - An Error instance * - A Date instance * - A RegExp instance * - A Function * * @private * @param obj - Object to check * @returns True if object matches LogMetadata interface */ isLogContext(e) { return typeof e == "object" && e !== null && !Array.isArray(e) && !(e instanceof Error) && !(e instanceof Date) && !(e instanceof RegExp) && typeof e != "function"; } /** * Cast a string to a LogLevel, with runtime validation and warning for invalid levels * @param level String representation of log level * @returns Numeric LogLevel * @private */ castLoglevel(e) { if (D(e)) switch (e) { case "trace": return c.trace; case "debug": return c.debug; case "info": return c.info; case "warn": return c.warn; case "error": return c.error; case "fatal": return c.fatal; case "silent": return c.silent; } const t = e.toLowerCase(); throw D(t) ? new B( `Invalid log level: ${e}. Log levels are case-sensitive. Valid levels: trace, debug, info, warn, error, fatal, silent.` ) : new B( `Invalid log level: ${e}. Valid levels: trace, debug, info, warn, error, fatal, silent.` ); } /** * Safely format a message using the style engine * Catches and logs any errors from the style engine * @param args Arguments for the style engine formatMessage method * @returns The formatted message * @private */ safeFormatMessage(e, t, s, i = "", n = "") { try { return this.styleEngine.formatMessage(e, t, s, i, n); } catch (a) { throw this.error("Style engine failure", a), new B("Style engine failure"); } } /** * Handle deduplication for a log message * @param level Log level * @param message Message text * @returns True if message should be logged immediately, false if batched * @private */ shouldLogImmediately(e, t) { const i = (this.parentLogger || this).deduplicator; if (!i) return !0; try { return i.addMessage(e, t, this.context); } catch (n) { return console.error("Logger deduplication error:", n), !0; } } /** * Deduplication handler for all log methods * Returns true if the message should be logged immediately, false if batched */ handleDeduplication(e, t, s, i) { return !s && i.length === 0 ? this.shouldLogImmediately(e, t) : !0; } }; I.INJECTABLE = !0; let A = I, E = null; function K() { return E || (E = new ne()), E; } class ne { constructor() { this.serviceMap = /* @__PURE__ */ new Map(); } /** * Register a service with a token * @param token - The injection token for the service * @param properties - The constructor parameters for the service class */ register(e, ...t) { if (this.isServiceInjectable(e) && (!this.hasService(e) || this.getService(e) === void 0)) { if (t === void 0 || t.length === 0) { this.serviceMap.set(this.getInjcectableName(e), new e()); return; } this.serviceMap.set( this.getInjcectableName(e), new e(...t) ); } } registerAsUndefined(e) { if (this.isServiceInjectable(e)) this.serviceMap.set(this.getInjcectableName(e), void 0); else throw new Error( `Service with token ${this.getInjcectableName( e )} is not injectable. Ensure it has the INJECTABLE static property set to true.` ); } registerCustomServiceInstance(e, t) { if (this.isServiceInjectable(e)) this.serviceMap.set(this.getInjcectableName(e), t); else throw new Error( `Service with token ${this.getInjcectableName( e )} is not injectable. Ensure it has the INJECTABLE static property set to true.` ); } /** * Get a service by its token * @param token - The injection token for the service * @returns The requested service or undefined if not found */ getService(e) { if (this.isServiceInjectable(e)) return this.hasService(e) || this.register(e), this.serviceMap.get(this.getInjcectableName(e)); } /** * Check if a service is registered * @param token - The injection token for the service * @returns True if the service is registered */ hasService(e) { return this.serviceMap.has(this.getInjcectableName(e)); } /** * Check if a service class is injectable * @param token - The injection token for the service * @returns True if the service has the INJECTABLE static property set to true */ isServiceInjectable(e) { const t = e.INJECTABLE; return t !== void 0 && t !== !1; } getInjcectableName(e) { if (this.isServiceInjectable(e)) { const t = e.INJECTABLE; return typeof t != "string" ? e.name : t; } throw new Error( `Service with token ${e.name} is not injectable. Ensure it has the INJECTABLE static property set to true.` ); } /** * Clean up all services when the game is being destroyed */ destroy() { this.serviceMap.clear(); } } function ae(r, e, t = {}) { const { required: s = !0 } = t, i = r.getService(e); if (!i && s) throw new Error( `Service with token ${e || "unknown"} not found in registry` ); return i; } function f(r, e = {}) { return ae(K(), r, e); } function Y(r, ...e) { K().register(r, ...e); } const _ = class _ { constructor(e) { this.storageConfig = e, this.logger = f(A).forContext("SessionService"); } async initSession(e) { m(e) ? this.logger.debug( "Session already exists, skipping initialization" ) : (this.logger.info("Creating new session"), this.store || (this.store = await X(this.storageConfig.driver)), await Q(e, { store: this.store, secret: this.storageConfig.sessionSecret || "change-me-in-production", name: "auth.session", maxAge: 3600 * 24, // 24 hours cookie: { httpOnly: !0, secure: process.env.NODE_ENV === "production", sameSite: "lax" // Always use 'lax' to allow OAuth redirects }, // Initialize default session structure with auth property generate: () => ({ auth: { isAuthenticated: !1 } }) })); } /** * Get a session by session ID * @param sessionId The session ID * @returns The session object or null if not found */ async getSession(e) { try { const t = await this.store.getItem(e); return t ? { id: e, data: t, save: async () => { await this.store.setItem(e, t); } } : null; } catch (t) { return this.logger.error("Error retrieving session", t, { sessionId: e }), null; } } /** * Get all active sessions from storage * @returns Array of session objects with update capability */ async getActiveSessions() { try { const e = await this.store.getKeys(); return (await Promise.all( e.map(async (s) => { const i = await this.store.getItem(s); return i ? { id: this.storageConfig.prefix && s.startsWith(`${this.storageConfig.prefix}:`) ? s.substring(`${this.storageConfig.prefix}:`.length) : s, data: i, update: (a) => { const o = a(i); Object.assign(i, o); }, save: async () => { await this.store.setItem(s, i); } } : null; }) )).filter((s) => s !== null); } catch (e) { return this.logger.error("Error retrieving active sessions", e), []; } } async destroyAuthSession(e) { try { await this.initSession(e), m(e)?.auth && await y(e, (s) => { const i = { ...s }; return delete i.auth, i; }), await ee(e); } catch (t) { throw this.logger.error("Session destruction failed", t), d({ statusCode: 500, message: "Session handling failed" }); } } async getSessionData(e, t) { await this.initSession(e); const s = m(e); return this.logger.debug( "Retrieved session data", s ), s?.[t] || null; } async setSessionData(e, t, s) { await this.initSession(e), await y(e, (i) => ({ ...i, [t]: s })); } async isValidSession(e) { return await this.initSession(e), !!m(e); } }; _.INJECTABLE = !0; let T = _; const R = class R { constructor(e) { this.openIDConfigCache = null, this.configLastFetched = null, this.CONFIG_CACHE_TTL = 36e5, this.TOKEN_REFRESH_SAFETY_MARGIN = 300, this.logger = f(A).forContext( "OAuthAuthenticationService" ), Y(T, e.sessionStorage), this.config = e; } // 5 minutes in seconds /** * Validate that the service has been properly initialized * @throws Error if mandatory configuration is missing */ validateConfiguration() { if (!this.config.issuer || !this.config.clientId || !this.config.clientSecret || !this.config.callbackUri) throw new Error( "OAuth Authentication Service not properly initialized. Make sure to call AnalogAuth() with valid configuration before using authentication features." ); } // Rest of the code converted to class methods... /** * Initialize session for the request */ async initSession(e) { return await f(T).initSession(e); } getConfig() { return this.validateConfiguration(), this.config; } /** * Safely access a configuration value * @param key The configuration key to retrieve * @param fallbackValue Optional fallback value if the config value doesn't exist * @returns The configuration value or fallback value * @throws Error if the configuration value doesn't exist and no fallback is provided */ getConfigValue(e, t) { const s = this.config[e]; if (s === void 0 || typeof s == "string" && s === "") { if (t !== void 0) return t; if (e === "userHandler" || e === "logoutUrl" || e === "audience") return; if (e === "unprotectedRoutes") return []; throw new Error(`Configuration value for '${e}' doesn't exist`); } return s; } /** * Check if the route is unprotected * @param path The request path * @returns True if the route is unprotected, false otherwise */ isUnprotectedRoute(e) { const t = this.getConfigValue( "unprotectedRoutes", [] ); return Array.isArray(t) ? t.some((s) => { if (s.endsWith("*")) { const a = s.slice(0, -1); if (!e.startsWith(a)) return !1; const o = e.slice(a.length); return o.length > 0 && o !== "/"; } const i = s.endsWith("/") ? s : s + "/", n = e.endsWith("/") ? e : e + "/"; return e === s || n === i; }) : !1; } /** * Get OAuth authorization URL for login */ async getAuthorizationUrl(e, t) { this.validateConfiguration(); const s = await this.getOpenIDConfiguration(), i = this.getConfigValue("audience", void 0), n = { response_type: "code", client_id: this.getConfigValue("clientId"), redirect_uri: t || this.getConfigValue("callbackUri"), scope: this.getConfigValue("scope"), state: e, ...i ? { audience: i } : {} }, a = new URLSearchParams(n); return `${s.authorization_endpoint}?${a.toString()}`; } /** * Exchange authorization code for tokens */ async exchangeCodeForTokens(e, t) { const s = await this.getOpenIDConfiguration(), i = await fetch(s.token_endpoint, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: new URLSearchParams({ grant_type: "authorization_code", client_id: this.getConfigValue("clientId"), client_secret: this.getConfigValue("clientSecret"), code: e, redirect_uri: t || this.getConfigValue("callbackUri") }).toString() }); if (!i.ok) { const n = await i.json(); throw this.logger.error("Error exchanging code for tokens", n), d({ statusCode: 401, message: "Failed to exchange authorization code" }); } return await i.json(); } /** * Refresh access token using refresh token */ async refreshTokens(e) { const t = await this.getOpenIDConfiguration(); try { const s = await fetch(t.token_endpoint, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: new URLSearchParams({ grant_type: "refresh_token", client_id: this.getConfigValue("clientId"), client_secret: this.getConfigValue("clientSecret"), refresh_token: e }).toString() }); if (!s.ok) { const i = await s.json().catch(() => ({ error: "Unknown error" })); throw this.logger.error("Error refreshing token", i), d({ statusCode: 401, message: "Failed to refresh token" }); } return await s.json(); } catch (s) { throw this.logger.error("Error during token refresh", s), d({ statusCode: 401, message: "Failed to refresh authentication token" }); } } /** * Get user info from OAuth provider with improved error handling and retry logic */ // eslint-disable-next-line @typescript-eslint/no-explicit-any async getUserInfo(e, t = 3) { const s = await this.getOpenIDConfiguration(); let i = null; for (let n = 1; n <= t; n++) try { const a = await fetch(s.userinfo_endpoint, { headers: { Authorization: `Bearer ${e}` }, // Add timeout to prevent hanging requests signal: AbortSignal.timeout(1e4) // 10 second timeout }); if (!a.ok) { const l = await a.json().catch(() => ({ error: "Unknown error" })); if (this.logger.error("Error getting user info", l, { attempt: n, maxRetries: t }), a.status === 401) throw d({ statusCode: 401, message: "Authentication token is invalid or expired" }); if (a.status === 429) { const h = parseInt( a.headers.get("Retry-After") || "5", 10 ); await new Promise( (u) => setTimeout(u, h * 1e3) ); continue; } else if (a.status >= 500) { await new Promise((h) => setTimeout(h, n * 1e3)); continue; } else throw d({ statusCode: a.status, message: `Failed to get user info: ${l.error || "Unknown error"}` }); } const o = await a.json(); if (!o || !o.sub && !o.id) throw d({ statusCode: 500, message: "Invalid user data received from provider" }); return o; } catch (a) { if (i = a, a instanceof Error && "statusCode" in a && a.statusCode === 401) throw a; if ((a instanceof TypeError || a instanceof Error && a.name === "AbortError") && (this.logger.error("Network error fetching user info", a, { attempt: n, maxRetries: t }), n < t)) { await new Promise( (o) => setTimeout(o, Math.pow(2, n) * 500) ); continue; } if (n === t) throw this.logger.error("Failed to get user info after multiple attempts", { maxRetries: t }), d({ statusCode: 500, message: "Failed to get user info after multiple attempts", cause: i }); } throw d({ statusCode: 500, message: "Unexpected error getting user info" }); } /** * Handle OAuth callback */ async handleCallback(e, t, s) { if (!s) throw d({ statusCode: 400, message: "Invalid state parameter" }); const i = await this.exchangeCodeForTokens(t), { access_token: n, id_token: a, refresh_token: o, expires_in: l } = i, h = await this.getUserInfo(n), u = this.getConfigValue("userHandler", void 0); let S = h; u && "createOrUpdateUser" in u && (S = await u.createOrUpdateUser?.(h)); const x = { isAuthenticated: !0, accessToken: n, idToken: a, refreshToken: o, expiresAt: Date.now() + l * 1e3, userInfo: h }; return await y(e, () => ({ user: S, auth: x })), this.logger.debug("Authentication session data saved successfully"), { user: S, tokens: i }; } /** * Calculate if token needs refresh based on safety margin * @param expiresAt Timestamp when token expires * @returns True if token should be refreshed */ shouldRefreshToken(e) { const t = Date.now(), s = this.TOKEN_REFRESH_SAFETY_MARGIN * 1e3; return t + s > e; } /** * Serverless-compatible method to refresh expiring tokens * This should be called by a scheduled function/CRON job * rather than using setInterval which doesn't work reliably in serverless */ async refreshExpiringTokens() { this.logger.debug("Starting bulk token refresh process"); try { const t = await f(T).getActiveSessions(); let s = 0, i = 0; const n = t.length; this.logger.debug(`Found ${n} active sessions to check`); for (const o of t) try { if (!o.data.auth?.isAuthenticated || !o.data.auth.refreshToken || !o.data.auth.expiresAt) { this.logger.debug(`Skipping session ${o.id} - no valid auth data`); continue; } if (!this.shouldRefreshToken(o.data.auth.expiresAt)) { this.logger.debug(`Skipping session ${o.id} - token not expiring soon`); continue; } this.logger.debug(`Refreshing token for session ${o.id}`); const l = await this.refreshTokens(o.data.auth.refreshToken); o.update((h) => { const u = h.auth; return u ? { ...h, auth: { ...u, accessToken: l.access_token, idToken: l.id_token || u.idToken, refreshToken: l.refresh_token || u.refreshToken, expiresAt: Date.now() + l.expires_in * 1e3 } } : h; }), await o.save(), s++, this.logger.debug(`Successfully refreshed token for session ${o.id}`); } catch (l) { i++, this.logger.error( "Failed to refresh token for session", l, { sessionId: o.id } ); try { o.update((h) => { const u = h.auth; return u ? { ...h, auth: { ...u, isAuthenticated: !1 } } : h; }), await o.save(); } catch (h) { this.logger.error( "Failed to update session after refresh failure", h, { sessionId: o.id } ); } } const a = { refreshed: s, failed: i, total: n }; return this.logger.info("Bulk token refresh completed", a), a; } catch (e) { throw this.logger.error("Error during bulk token refresh", e), d({ statusCode: 500, message: "Failed to refresh expiring tokens" }); } } /** * Check if user is authenticated */ async isAuthenticated(e) { await f(T).initSession(e); const t = await m(e); if (!t?.auth) return await y(e, () => ({ auth: { isAuthenticated: !1 } })), !1; if (!t.auth.isAuthenticated) return !1; if (t.auth.expiresAt && t.auth.expiresAt < Date.now()) if (t.auth.refreshToken) try { const s = await this.refreshTokens(t.auth.refreshToken); return await y(e, (i) => ({ auth: { ...i.auth, isAuthenticated: !0, accessToken: s.access_token, idToken: s.id_token || t?.auth?.idToken, refreshToken: s.refresh_token || t?.auth?.refreshToken, expiresAt: Date.now() + s.expires_in * 1e3 } })), !0; } catch (s) { return this.logger.error("Error refreshing token", s), await y(e, (i) => ({ auth: { ...i.auth, isAuthenticated: !1 } })), this.logger.info("error occurred while refreshing token"), this.logger.groupEnd( "OAuthAuthenticationService.isAuthenticated " + e.path ), !1; } else return this.logger.info(" No refresh token available"), this.logger.groupEnd( "OAuthAuthenticationService.isAuthenticated " + e.path ), !1; return t.auth.expiresAt && this.shouldRefreshToken(t.auth.expiresAt) && setTimeout(async () => { try {