@analog-tools/auth
Version:
Authentication module for AnalogJS applications
1,625 lines (1,624 loc) • 58.7 kB
JavaScript
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 {