UNPKG

@analog-tools/auth

Version:

Authentication module for AnalogJS applications

1,624 lines 60 kB
import { createError as d, sendRedirect as b, getQuery as H, getRequestHeaders as q, getRequestURL as X, getHeader as U } from "h3"; import { getSession as C, createRedisStore as $, createMemoryStore as Q, useSession as ee, updateSession as m, destroySession as te } from "@analog-tools/session"; import { TRPCError as se } 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 || {}), h = /* @__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))(h || {}); function F(r) { return ["trace", "debug", "info", "warn", "error", "fatal", "silent"].includes(r); } const A = class A { /** * Generate a cache key for memoization (includes serialization options) * @private */ static getCacheKey(e, t, s, i) { if (!e.stack) return null; const a = e.stack.split(` `)[0] || ""; return `${e.name}:${e.message}:${a}:${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 = A.DEFAULT_MAX_DEPTH, includeNonEnumerable: a = !1 } = t; if (e instanceof Error) { const o = this.getCacheKey(e, s, i, a); if (o) { const l = this.serializationCache.get(o); if (l) return l; } const c = this.serializeError(e, s, i, a, /* @__PURE__ */ new WeakSet()); return o && this.addToCache(o, c), c; } if (typeof e == "string") return e; const n = this.safeStringify(e, i, /* @__PURE__ */ new WeakSet()); if (typeof n == "string") return n; try { return JSON.stringify(n, null, 2); } catch { return String(n); } } /** * Serialize a standard Error object * @private */ static serializeError(e, t, s, i, a = /* @__PURE__ */ new WeakSet()) { if (a.has(e)) return { message: e.message, name: e.name, [Symbol.for("circular")]: this.CIRCULAR_REF_PLACEHOLDER }; a.add(e); const n = { message: e.message, name: e.name }; return t && e.stack && (n.stack = e.stack), "cause" in e && e.cause !== void 0 && (e.cause instanceof Error ? n.cause = this.serializeError(e.cause, t, s - 1, i, a) : n.cause = this.safeStringify(e.cause, s - 1, a)), Object.keys(e).forEach((o) => { if (!(o in n)) try { const c = e; n[o] = this.safeStringify(c[o], s - 1, a); } catch { n[o] = this.UNABLE_TO_SERIALIZE; } }), i && Object.getOwnPropertyNames(e).forEach((o) => { if (!(o in n) && o !== "stack" && o !== "message" && o !== "name") try { const c = Object.getOwnPropertyDescriptor(e, o); if (c && c.enumerable === !1) { const l = e; n[o] = this.safeStringify(l[o], s - 1, a); } } catch { n[o] = this.UNABLE_TO_SERIALIZE; } }), n; } /** * 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 a = e.map((n) => this.safeStringify(n, t - 1, s)); return s.delete(e), a; } const i = {}; for (const [a, n] of Object.entries(e)) try { i[a] = this.safeStringify(n, t - 1, s); } catch { i[a] = this.UNABLE_TO_SERIALIZE; } return s.delete(e), i; } catch { return s.delete(e), this.UNABLE_TO_SERIALIZE; } } }; A.DEFAULT_MAX_DEPTH = 10, A.CIRCULAR_REF_PLACEHOLDER = "[Circular Reference]", A.MAX_DEPTH_PLACEHOLDER = "[Max Depth Reached]", A.UNABLE_TO_SERIALIZE = "[Unable to serialize]", A.serializationCache = /* @__PURE__ */ new Map(), A.MAX_CACHE_SIZE = 100; let v = A; const W = { 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 } }, K = { success: "✅", warning: "⚠️", error: "❌", info: "ℹ️", debug: "🐞" }, ie = { 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 = { ...W, ...e.styles }, this.globalIcons = { ...K, ...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, a) { const n = i ? `[${s}:${i}]` : `[${s}]`; if (this.useColors) { let o = this.getColorForLevel(e); return a && (o = a), `${o}${n} ${t}${g.Reset}`; } else return `${n} ${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, a) { let n = t, o = this.getColorForLevel(e); if (i != null && i.style) { const c = this.applyStyle(i.style, s, a); c && (o = c); } return i != null && i.icon && (n = `${this.resolveIcon(i.icon, s, a)} ${t}`), this.formatMessage(e, n, s, a, 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 = h[e], s = ie[t]; return e === h.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 a = this.getSemanticStyleColor(e, t, s); return this.setStyleCache(e, a), a; } 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 a = this.constructStyleCode(e); return this.setStyleCache(e, a), a; } 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 n = i.color.toString(); return i.bold && (n += g.Bold), i.underline && (n += g.Underline), this.styleCache.set(e, n), n; } const a = s ? `${t}:${s}` : t; console.warn( `[${a}] 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" ], a = i.find((n) => n === e); return a && this.globalIcons[a] ? this.globalIcons[a] : (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 I extends Error { constructor(e) { super(e), this.name = "LoggerError"; } } const D = { windowMs: 5e3, // 5 seconds flushOnCritical: !0 }, re = [h.error, h.fatal]; class ae { 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 && re.includes(e)) return !0; const i = this.generateFingerprint(t, e, s), a = this.entries.get(i); return a ? a.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 h.trace: console.trace(s); break; case h.debug: console.debug(s); break; case h.info: console.info(s); break; case h.warn: console.warn(s); break; case h.error: console.error(s); break; case h.fatal: console.error(s); break; } } } const B = class B { /** * 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) { var a, n; 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(h).includes(e.level)) throw new I(`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 ?? ((a = process.env.LOG_DISABLED_CONTEXTS) == null ? void 0 : a.split(",")) ?? [] ), this.styleEngine = t || new L({ useColors: e.useColors, styles: { ...W, ...e.styles }, icons: { ...K, ...e.icons } }), (n = e.deduplication) != null && n.enabled) { const o = { enabled: !0, windowMs: e.deduplication.windowMs ?? D.windowMs, flushOnCritical: e.deduplication.flushOnCritical ?? D.flushOnCritical }, c = (l, u, p) => this.styleEngine.formatMessage(l, u, this.name, p); this.deduplicator = new ae(o, c); } } } /** * 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 B({}, 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) { var i; if (!this.isContextEnabled()) return; (((i = this.parentLogger) == null ? void 0 : i.activeGroups) || this.activeGroups).push(e); const s = this.styleEngine.formatMessage( h.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) { var s; if (!this.isContextEnabled()) return; const t = ((s = this.parentLogger) == null ? void 0 : s.activeGroups) || this.activeGroups; if (e) { const i = t.lastIndexOf(e); if (i !== -1) { const a = t.splice(i); for (let n = 0; n < a.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 > h.trace) return; const { metadata: i, restData: a } = this.styleEngine.parseMetadataParameter( t, s ); if (this.handleDeduplication(h.trace, e, i, a)) if (i) { const n = this.styleEngine.formatMessageWithMetadata( h.trace, e, this.name, i, this.context ); console.trace(n, ...a || []); } else { const n = this.styleEngine.formatMessage( h.trace, e, this.name, this.context ); console.trace(n, ...a || []); } } debug(e, t, ...s) { if (!this.isContextEnabled() || this.logLevel > h.debug) return; const { metadata: i, restData: a } = this.styleEngine.parseMetadataParameter( t, s ); if (this.handleDeduplication(h.debug, e, i, a)) if (i) { const n = this.styleEngine.formatMessageWithMetadata( h.debug, e, this.name, i, this.context ); console.debug(n, ...a || []); } else { const n = this.styleEngine.formatMessage( h.debug, e, this.name, this.context ); console.debug(n, ...a || []); } } info(e, t, ...s) { if (!this.isContextEnabled() || this.logLevel > h.info) return; const { metadata: i, restData: a } = this.styleEngine.parseMetadataParameter( t, s ); if (this.handleDeduplication(h.info, e, i, a)) if (i) { const n = this.styleEngine.formatMessageWithMetadata( h.info, e, this.name, i, this.context ); console.info(n, ...a || []); } else { const n = this.styleEngine.formatMessage( h.info, e, this.name, this.context ); console.info(n, ...a || []); } } warn(e, t, ...s) { if (!this.isContextEnabled() || this.logLevel > h.warn) return; const { metadata: i, restData: a } = this.styleEngine.parseMetadataParameter( t, s ); if (this.handleDeduplication(h.warn, e, i, a)) if (i) { const n = this.styleEngine.formatMessageWithMetadata( h.warn, e, this.name, i, this.context ); console.warn(n, ...a || []); } else { const n = this.styleEngine.formatMessage( h.warn, e, this.name, this.context ); console.warn(n, ...a || []); } } error(e, t, s, ...i) { if (!this.isContextEnabled() || this.logLevel > h.error) return; const { message: a, serializedError: n, context: o, data: c } = this.parseErrorParameters( e, t, s, i ), l = c || []; let u, p = l; if (l.length > 0) { const w = l[l.length - 1]; w && typeof w == "object" && !Array.isArray(w) && ("style" in w || "icon" in w) && (u = w, p = l.slice(0, -1)); } const S = [this.styleEngine.formatMessageWithMetadata( h.error, a, this.name, u, this.context )]; n && S.push(n), o && S.push(o), console.error(...S, ...p); } fatal(e, t, s, ...i) { if (!this.isContextEnabled() || this.logLevel > h.fatal) return; if (typeof e == "string" && t && typeof t == "object" && !Array.isArray(t) && ("style" in t || "icon" in t)) { const w = t, Z = this.styleEngine.formatMessageWithMetadata( h.fatal, `FATAL: ${e}`, this.name, w, this.context ); console.error(Z); return; } const { message: a, serializedError: n, context: o, data: c } = this.parseErrorParameters( e, t, s, i ), l = c || []; let u, p = l; if (l.length > 0) { const w = l[l.length - 1]; w && typeof w == "object" && !Array.isArray(w) && ("style" in w || "icon" in w) && (u = w, p = l.slice(0, -1)); } const S = [this.styleEngine.formatMessageWithMetadata( h.fatal, `FATAL: ${a}`, this.name, u, this.context )]; n && S.push(n), o && S.push(o), console.error(...S, ...p); } /** * 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 a = typeof e == "string" ? e : "Unknown error", n = t ? v.serialize(t) : void 0, o = s !== void 0 ? [s, ...i] : i; return { message: a, serializedError: n, 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 (F(e)) switch (e) { case "trace": return h.trace; case "debug": return h.debug; case "info": return h.info; case "warn": return h.warn; case "error": return h.error; case "fatal": return h.fatal; case "silent": return h.silent; } const t = e.toLowerCase(); throw F(t) ? new I( `Invalid log level: ${e}. Log levels are case-sensitive. Valid levels: trace, debug, info, warn, error, fatal, silent.` ) : new I( `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 = "", a = "") { try { return this.styleEngine.formatMessage(e, t, s, i, a); } catch (n) { throw this.error("Style engine failure", n), new I("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 (a) { return console.error("Logger deduplication error:", a), !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; } }; B.INJECTABLE = !0; let k = B, E = null; function Y() { 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 oe(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 oe(Y(), r, e); } function J(r, ...e) { Y().register(r, ...e); } const R = class R { constructor(e) { this.storageConfig = e, this.logger = f(k).forContext("SessionService"); } async initSession(e) { if (C(e)) this.logger.debug( "Session already exists, skipping initialization" ); else { if (this.logger.info("Creating new session"), !this.store) if (this.storageConfig.type === "redis") { const s = { ...this.storageConfig.config }; if ("url" in s) this.store = $(s); else if ("host" in s) { const i = { ...s, port: typeof s.port == "string" ? parseInt(s.port, 10) : s.port }; this.store = $(i); } else throw new Error("Invalid Redis configuration: must provide either url or host/port"); } else this.store = Q(); await ee(e, { store: this.store, secret: this.storageConfig.config.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.config.prefix && s.startsWith(`${this.storageConfig.config.prefix}:`) ? s.substring(`${this.storageConfig.config.prefix}:`.length) : s, data: i, update: (n) => { const o = n(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); const t = C(e); t != null && t.auth && await m(e, (s) => { const i = { ...s }; return delete i.auth, i; }), await te(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 = C(e); return this.logger.debug( "Retrieved session data", s ), (s == null ? void 0 : s[t]) || null; } async setSessionData(e, t, s) { await this.initSession(e), await m(e, (i) => ({ ...i, [t]: s })); } async isValidSession(e) { return await this.initSession(e), !!C(e); } }; R.INJECTABLE = !0; let T = R; const _ = class _ { constructor(e) { this.openIDConfigCache = null, this.configLastFetched = null, this.CONFIG_CACHE_TTL = 36e5, this.TOKEN_REFRESH_SAFETY_MARGIN = 300, this.logger = f(k).forContext( "OAuthAuthenticationService" ), J(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) => e.startsWith(s)); } /** * Get OAuth authorization URL for login */ async getAuthorizationUrl(e, t) { this.validateConfiguration(); const s = await this.getOpenIDConfiguration(), i = this.getConfigValue("audience", void 0), a = { response_type: "code", client_id: this.getConfigValue("clientId"), redirect_uri: t || this.getConfigValue("callbackUri"), scope: this.getConfigValue("scope"), state: e, ...i ? { audience: i } : {} }, n = new URLSearchParams(a); return `${s.authorization_endpoint}?${n.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 a = await i.json(); throw this.logger.error("Error exchanging code for tokens", a), 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 a = 1; a <= t; a++) try { const n = await fetch(s.userinfo_endpoint, { headers: { Authorization: `Bearer ${e}` }, // Add timeout to prevent hanging requests signal: AbortSignal.timeout(1e4) // 10 second timeout }); if (!n.ok) { const c = await n.json().catch(() => ({ error: "Unknown error" })); if (this.logger.error("Error getting user info", c, { attempt: a, maxRetries: t }), n.status === 401) throw d({ statusCode: 401, message: "Authentication token is invalid or expired" }); if (n.status === 429) { const l = parseInt( n.headers.get("Retry-After") || "5", 10 ); await new Promise( (u) => setTimeout(u, l * 1e3) ); continue; } else if (n.status >= 500) { await new Promise((l) => setTimeout(l, a * 1e3)); continue; } else throw d({ statusCode: n.status, message: `Failed to get user info: ${c.error || "Unknown error"}` }); } const o = await n.json(); if (!o || !o.sub && !o.id) throw d({ statusCode: 500, message: "Invalid user data received from provider" }); return o; } catch (n) { if (i = n, n instanceof Error && "statusCode" in n && n.statusCode === 401) throw n; if ((n instanceof TypeError || n instanceof Error && n.name === "AbortError") && (this.logger.error("Network error fetching user info", n, { attempt: a, maxRetries: t }), a < t)) { await new Promise( (o) => setTimeout(o, Math.pow(2, a) * 500) ); continue; } if (a === 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) { var S; if (!s) throw d({ statusCode: 400, message: "Invalid state parameter" }); const i = await this.exchangeCodeForTokens(t), { access_token: a, id_token: n, refresh_token: o, expires_in: c } = i, l = await this.getUserInfo(a), u = this.getConfigValue("userHandler", void 0); let p = l; u && "createOrUpdateUser" in u && (p = await ((S = u.createOrUpdateUser) == null ? void 0 : S.call(u, l))); const x = { isAuthenticated: !0, accessToken: a, idToken: n, refreshToken: o, expiresAt: Date.now() + c * 1e3, userInfo: l }; return await m(e, () => ({ user: p, auth: x })), this.logger.debug("Authentication session data saved successfully"), { user: p, 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() { var e; this.logger.debug("Starting bulk token refresh process"); try { const s = await f(T).getActiveSessions(); let i = 0, a = 0; const n = s.length; this.logger.debug(`Found ${n} active sessions to check`); for (const c of s) try { if (!((e = c.data.auth) != null && e.isAuthenticated) || !c.data.auth.refreshToken || !c.data.auth.expiresAt) { this.logger.debug(`Skipping session ${c.id} - no valid auth data`); continue; } if (!this.shouldRefreshToken(c.data.auth.expiresAt)) { this.logger.debug(`Skipping session ${c.id} - token not expiring soon`); continue; } this.logger.debug(`Refreshing token for session ${c.id}`); const l = await this.refreshTokens(c.data.auth.refreshToken); c.update((u) => { const p = u.auth; return p ? { ...u, auth: { ...p, accessToken: l.access_token, idToken: l.id_token || p.idToken, refreshToken: l.refresh_token || p.refreshToken, expiresAt: Date.now() + l.expires_in * 1e3 } } : u; }), await c.save(), i++, this.logger.debug(`Successfully refreshed token for session ${c.id}`); } catch (l) { a++, this.logger.error( "Failed to refresh token for session", l, { sessionId: c.id } ); try { c.update((u) => { const p = u.auth; return p ? { ...u, auth: { ...p, isAuthenticated: !1 } } : u; }), await c.save(); } catch (u) { this.logger.error( "Failed to update session after refresh failure", u, { sessionId: c.id } ); } } const o = { refreshed: i, failed: a, total: n }; return this.logger.info("Bulk token refresh completed", o), o; } catch (t) { throw this.logger.error("Error during bulk token refresh", t), 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 C(e); if (!(t != null && t.auth)) return await m(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 m(e, (i) => { var a, n; return { auth: { ...i.auth, isAuthenticated: !0, accessToken: s.access_token, idToken: s.id_token || ((a = t == null ? void 0 : t.auth) == null ? void 0 : a.idToken), refreshToken: s.refresh_token || ((n = t == null ? void 0 : t.auth) == null ? void 0 : n.refreshToken), expiresAt: