@alchemy/common 0.0.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (212) hide show
  1. package/LICENSE +21 -0
  2. package/dist/esm/actions/addBreadCrumb.d.ts +14 -0
  3. package/dist/esm/actions/addBreadCrumb.js +27 -0
  4. package/dist/esm/actions/addBreadCrumb.js.map +1 -0
  5. package/dist/esm/chains.d.ts +234 -0
  6. package/dist/esm/chains.js +113 -0
  7. package/dist/esm/chains.js.map +1 -0
  8. package/dist/esm/errors/AccountNotFoundError.d.ts +10 -0
  9. package/dist/esm/errors/AccountNotFoundError.js +19 -0
  10. package/dist/esm/errors/AccountNotFoundError.js.map +1 -0
  11. package/dist/esm/errors/BaseError.d.ts +23 -0
  12. package/dist/esm/errors/BaseError.js +40 -0
  13. package/dist/esm/errors/BaseError.js.map +1 -0
  14. package/dist/esm/errors/ChainNotFoundError.d.ts +11 -0
  15. package/dist/esm/errors/ChainNotFoundError.js +19 -0
  16. package/dist/esm/errors/ChainNotFoundError.js.map +1 -0
  17. package/dist/esm/errors/ConnectionConfigError.d.ts +13 -0
  18. package/dist/esm/errors/ConnectionConfigError.js +25 -0
  19. package/dist/esm/errors/ConnectionConfigError.js.map +1 -0
  20. package/dist/esm/errors/FetchError.d.ts +15 -0
  21. package/dist/esm/errors/FetchError.js +25 -0
  22. package/dist/esm/errors/FetchError.js.map +1 -0
  23. package/dist/esm/errors/InvalidRequestError.d.ts +13 -0
  24. package/dist/esm/errors/InvalidRequestError.js +22 -0
  25. package/dist/esm/errors/InvalidRequestError.js.map +1 -0
  26. package/dist/esm/errors/MethodUnsupportedError.d.ts +13 -0
  27. package/dist/esm/errors/MethodUnsupportedError.js +21 -0
  28. package/dist/esm/errors/MethodUnsupportedError.js.map +1 -0
  29. package/dist/esm/errors/ServerError.d.ts +15 -0
  30. package/dist/esm/errors/ServerError.js +25 -0
  31. package/dist/esm/errors/ServerError.js.map +1 -0
  32. package/dist/esm/index.d.ts +29 -0
  33. package/dist/esm/index.js +27 -0
  34. package/dist/esm/index.js.map +1 -0
  35. package/dist/esm/logging/config.d.ts +190 -0
  36. package/dist/esm/logging/config.js +279 -0
  37. package/dist/esm/logging/config.js.map +1 -0
  38. package/dist/esm/logging/index.d.ts +6 -0
  39. package/dist/esm/logging/index.js +5 -0
  40. package/dist/esm/logging/index.js.map +1 -0
  41. package/dist/esm/logging/local.d.ts +10 -0
  42. package/dist/esm/logging/local.js +35 -0
  43. package/dist/esm/logging/local.js.map +1 -0
  44. package/dist/esm/logging/logger.d.ts +80 -0
  45. package/dist/esm/logging/logger.js +111 -0
  46. package/dist/esm/logging/logger.js.map +1 -0
  47. package/dist/esm/logging/noop.d.ts +6 -0
  48. package/dist/esm/logging/noop.js +12 -0
  49. package/dist/esm/logging/noop.js.map +1 -0
  50. package/dist/esm/logging/sinks.d.ts +90 -0
  51. package/dist/esm/logging/sinks.js +111 -0
  52. package/dist/esm/logging/sinks.js.map +1 -0
  53. package/dist/esm/logging/types.d.ts +96 -0
  54. package/dist/esm/logging/types.js +2 -0
  55. package/dist/esm/logging/types.js.map +1 -0
  56. package/dist/esm/logging/utils.d.ts +7 -0
  57. package/dist/esm/logging/utils.js +21 -0
  58. package/dist/esm/logging/utils.js.map +1 -0
  59. package/dist/esm/rest/restClient.d.ts +34 -0
  60. package/dist/esm/rest/restClient.js +55 -0
  61. package/dist/esm/rest/restClient.js.map +1 -0
  62. package/dist/esm/rest/types.d.ts +24 -0
  63. package/dist/esm/rest/types.js +2 -0
  64. package/dist/esm/rest/types.js.map +1 -0
  65. package/dist/esm/tracing/traceHeader.d.ts +82 -0
  66. package/dist/esm/tracing/traceHeader.js +145 -0
  67. package/dist/esm/tracing/traceHeader.js.map +1 -0
  68. package/dist/esm/tracing/updateHeaders.d.ts +24 -0
  69. package/dist/esm/tracing/updateHeaders.js +61 -0
  70. package/dist/esm/tracing/updateHeaders.js.map +1 -0
  71. package/dist/esm/transport/alchemy.d.ts +110 -0
  72. package/dist/esm/transport/alchemy.js +164 -0
  73. package/dist/esm/transport/alchemy.js.map +1 -0
  74. package/dist/esm/transport/chainRegistry.d.ts +31 -0
  75. package/dist/esm/transport/chainRegistry.js +95 -0
  76. package/dist/esm/transport/chainRegistry.js.map +1 -0
  77. package/dist/esm/transport/connection.d.ts +20 -0
  78. package/dist/esm/transport/connection.js +2 -0
  79. package/dist/esm/transport/connection.js.map +1 -0
  80. package/dist/esm/transport/connectionSchema.d.ts +124 -0
  81. package/dist/esm/transport/connectionSchema.js +121 -0
  82. package/dist/esm/transport/connectionSchema.js.map +1 -0
  83. package/dist/esm/utils/assertNever.d.ts +8 -0
  84. package/dist/esm/utils/assertNever.js +12 -0
  85. package/dist/esm/utils/assertNever.js.map +1 -0
  86. package/dist/esm/utils/bigint.d.ts +24 -0
  87. package/dist/esm/utils/bigint.js +37 -0
  88. package/dist/esm/utils/bigint.js.map +1 -0
  89. package/dist/esm/utils/createEip1193HandlerFactory.d.ts +18 -0
  90. package/dist/esm/utils/createEip1193HandlerFactory.js +11 -0
  91. package/dist/esm/utils/createEip1193HandlerFactory.js.map +1 -0
  92. package/dist/esm/utils/headers.d.ts +7 -0
  93. package/dist/esm/utils/headers.js +29 -0
  94. package/dist/esm/utils/headers.js.map +1 -0
  95. package/dist/esm/utils/lowerAddress.d.ts +8 -0
  96. package/dist/esm/utils/lowerAddress.js +9 -0
  97. package/dist/esm/utils/lowerAddress.js.map +1 -0
  98. package/dist/esm/utils/raise.d.ts +8 -0
  99. package/dist/esm/utils/raise.js +14 -0
  100. package/dist/esm/utils/raise.js.map +1 -0
  101. package/dist/esm/utils/types.d.ts +10 -0
  102. package/dist/esm/utils/types.js +2 -0
  103. package/dist/esm/utils/types.js.map +1 -0
  104. package/dist/esm/version.d.ts +1 -0
  105. package/dist/esm/version.js +4 -0
  106. package/dist/esm/version.js.map +1 -0
  107. package/dist/types/actions/addBreadCrumb.d.ts +15 -0
  108. package/dist/types/actions/addBreadCrumb.d.ts.map +1 -0
  109. package/dist/types/chains.d.ts +235 -0
  110. package/dist/types/chains.d.ts.map +1 -0
  111. package/dist/types/errors/AccountNotFoundError.d.ts +11 -0
  112. package/dist/types/errors/AccountNotFoundError.d.ts.map +1 -0
  113. package/dist/types/errors/BaseError.d.ts +24 -0
  114. package/dist/types/errors/BaseError.d.ts.map +1 -0
  115. package/dist/types/errors/ChainNotFoundError.d.ts +12 -0
  116. package/dist/types/errors/ChainNotFoundError.d.ts.map +1 -0
  117. package/dist/types/errors/ConnectionConfigError.d.ts +14 -0
  118. package/dist/types/errors/ConnectionConfigError.d.ts.map +1 -0
  119. package/dist/types/errors/FetchError.d.ts +16 -0
  120. package/dist/types/errors/FetchError.d.ts.map +1 -0
  121. package/dist/types/errors/InvalidRequestError.d.ts +14 -0
  122. package/dist/types/errors/InvalidRequestError.d.ts.map +1 -0
  123. package/dist/types/errors/MethodUnsupportedError.d.ts +14 -0
  124. package/dist/types/errors/MethodUnsupportedError.d.ts.map +1 -0
  125. package/dist/types/errors/ServerError.d.ts +16 -0
  126. package/dist/types/errors/ServerError.d.ts.map +1 -0
  127. package/dist/types/index.d.ts +30 -0
  128. package/dist/types/index.d.ts.map +1 -0
  129. package/dist/types/logging/config.d.ts +191 -0
  130. package/dist/types/logging/config.d.ts.map +1 -0
  131. package/dist/types/logging/index.d.ts +7 -0
  132. package/dist/types/logging/index.d.ts.map +1 -0
  133. package/dist/types/logging/local.d.ts +11 -0
  134. package/dist/types/logging/local.d.ts.map +1 -0
  135. package/dist/types/logging/logger.d.ts +81 -0
  136. package/dist/types/logging/logger.d.ts.map +1 -0
  137. package/dist/types/logging/noop.d.ts +7 -0
  138. package/dist/types/logging/noop.d.ts.map +1 -0
  139. package/dist/types/logging/sinks.d.ts +91 -0
  140. package/dist/types/logging/sinks.d.ts.map +1 -0
  141. package/dist/types/logging/types.d.ts +97 -0
  142. package/dist/types/logging/types.d.ts.map +1 -0
  143. package/dist/types/logging/utils.d.ts +8 -0
  144. package/dist/types/logging/utils.d.ts.map +1 -0
  145. package/dist/types/rest/restClient.d.ts +35 -0
  146. package/dist/types/rest/restClient.d.ts.map +1 -0
  147. package/dist/types/rest/types.d.ts +25 -0
  148. package/dist/types/rest/types.d.ts.map +1 -0
  149. package/dist/types/tracing/traceHeader.d.ts +83 -0
  150. package/dist/types/tracing/traceHeader.d.ts.map +1 -0
  151. package/dist/types/tracing/updateHeaders.d.ts +25 -0
  152. package/dist/types/tracing/updateHeaders.d.ts.map +1 -0
  153. package/dist/types/transport/alchemy.d.ts +111 -0
  154. package/dist/types/transport/alchemy.d.ts.map +1 -0
  155. package/dist/types/transport/chainRegistry.d.ts +32 -0
  156. package/dist/types/transport/chainRegistry.d.ts.map +1 -0
  157. package/dist/types/transport/connection.d.ts +21 -0
  158. package/dist/types/transport/connection.d.ts.map +1 -0
  159. package/dist/types/transport/connectionSchema.d.ts +125 -0
  160. package/dist/types/transport/connectionSchema.d.ts.map +1 -0
  161. package/dist/types/utils/assertNever.d.ts +9 -0
  162. package/dist/types/utils/assertNever.d.ts.map +1 -0
  163. package/dist/types/utils/bigint.d.ts +25 -0
  164. package/dist/types/utils/bigint.d.ts.map +1 -0
  165. package/dist/types/utils/createEip1193HandlerFactory.d.ts +19 -0
  166. package/dist/types/utils/createEip1193HandlerFactory.d.ts.map +1 -0
  167. package/dist/types/utils/headers.d.ts +8 -0
  168. package/dist/types/utils/headers.d.ts.map +1 -0
  169. package/dist/types/utils/lowerAddress.d.ts +9 -0
  170. package/dist/types/utils/lowerAddress.d.ts.map +1 -0
  171. package/dist/types/utils/raise.d.ts +9 -0
  172. package/dist/types/utils/raise.d.ts.map +1 -0
  173. package/dist/types/utils/types.d.ts +11 -0
  174. package/dist/types/utils/types.d.ts.map +1 -0
  175. package/dist/types/version.d.ts +2 -0
  176. package/dist/types/version.d.ts.map +1 -0
  177. package/package.json +67 -0
  178. package/src/actions/addBreadCrumb.ts +38 -0
  179. package/src/chains.ts +118 -0
  180. package/src/errors/AccountNotFoundError.ts +16 -0
  181. package/src/errors/BaseError.ts +51 -0
  182. package/src/errors/ChainNotFoundError.ts +15 -0
  183. package/src/errors/ConnectionConfigError.ts +22 -0
  184. package/src/errors/FetchError.ts +21 -0
  185. package/src/errors/InvalidRequestError.ts +19 -0
  186. package/src/errors/MethodUnsupportedError.ts +17 -0
  187. package/src/errors/ServerError.ts +21 -0
  188. package/src/index.ts +60 -0
  189. package/src/logging/config.ts +365 -0
  190. package/src/logging/index.ts +20 -0
  191. package/src/logging/local.ts +39 -0
  192. package/src/logging/logger.ts +194 -0
  193. package/src/logging/noop.ts +13 -0
  194. package/src/logging/sinks.ts +115 -0
  195. package/src/logging/types.ts +111 -0
  196. package/src/logging/utils.ts +31 -0
  197. package/src/rest/restClient.ts +64 -0
  198. package/src/rest/types.ts +42 -0
  199. package/src/tracing/traceHeader.ts +154 -0
  200. package/src/tracing/updateHeaders.ts +66 -0
  201. package/src/transport/alchemy.ts +242 -0
  202. package/src/transport/chainRegistry.ts +115 -0
  203. package/src/transport/connection.ts +19 -0
  204. package/src/transport/connectionSchema.ts +145 -0
  205. package/src/utils/assertNever.ts +12 -0
  206. package/src/utils/bigint.ts +58 -0
  207. package/src/utils/createEip1193HandlerFactory.ts +25 -0
  208. package/src/utils/headers.ts +48 -0
  209. package/src/utils/lowerAddress.ts +10 -0
  210. package/src/utils/raise.ts +14 -0
  211. package/src/utils/types.ts +14 -0
  212. package/src/version.ts +3 -0
@@ -0,0 +1,365 @@
1
+ /**
2
+ * Log level constants for controlling diagnostics output.
3
+ * Lower numeric values indicate higher priority (more restrictive filtering).
4
+ *
5
+ * @example
6
+ * ```ts
7
+ * import { LogLevel, setGlobalLoggerConfig } from "@alchemy/common";
8
+ *
9
+ * setGlobalLoggerConfig({ level: LogLevel.DEBUG });
10
+ * ```
11
+ */
12
+ export const LogLevel = {
13
+ /** Critical errors only */
14
+ ERROR: 0,
15
+ /** Warnings and errors */
16
+ WARN: 1,
17
+ /** Informational messages, warnings, and errors */
18
+ INFO: 2,
19
+ /** Debug messages and all above */
20
+ DEBUG: 3,
21
+ /** All messages including verbose details */
22
+ VERBOSE: 4,
23
+ } as const;
24
+
25
+ // eslint-disable-next-line @typescript-eslint/no-redeclare
26
+ export type LogLevel = (typeof LogLevel)[keyof typeof LogLevel];
27
+
28
+ /**
29
+ * Configuration for redacting sensitive data in log output.
30
+ */
31
+ export type RedactConfig = {
32
+ /** Predicate function to test if a key should be redacted. Returns true if the key should be redacted. */
33
+ keys?: (key: string) => boolean;
34
+ /** Function to replace redacted values. Defaults to returning "[REDACTED]". */
35
+ replacer?: (value: unknown, key?: string) => unknown;
36
+ };
37
+
38
+ /**
39
+ * Configuration for the diagnostics logging system.
40
+ * All properties are optional and will use sensible defaults if not provided.
41
+ */
42
+ export type DiagnosticsConfig = {
43
+ /** Minimum log level to emit. Defaults to INFO in development, ERROR in production. */
44
+ level?: LogLevel;
45
+ /** Configuration for redacting sensitive data in logs. */
46
+ redact?: RedactConfig;
47
+ /** Array of sink functions to receive log entries. Defaults to console sink. */
48
+ sinks?: Array<(entry: LogEntry) => void>;
49
+ /** Disable telemetry headers in requests. Defaults to false. */
50
+ disableTelemetryHeaders?: boolean;
51
+ /** Array of exact namespace strings to allow. If undefined or empty, all namespaces are enabled. Example: ["aa-infra", "wallet-apis"] */
52
+ enabledNamespaces?: string[];
53
+ };
54
+
55
+ /**
56
+ * Base context attached to all log entries from a logger instance.
57
+ */
58
+ export type LoggerContextBase = {
59
+ /** Package name (e.g., "@alchemy/aa-infra") */
60
+ package: string;
61
+ /** Package version (e.g., "1.0.0") */
62
+ version: string;
63
+ };
64
+
65
+ /**
66
+ * A single log entry emitted by the diagnostics logger.
67
+ */
68
+ export type LogEntry = {
69
+ /** Timestamp in milliseconds since epoch (Date.now()) */
70
+ ts: number;
71
+ /** Log level of this entry */
72
+ level: LogLevel;
73
+ /** Optional namespace for filtering/grouping (e.g., "aa-infra", "wallet-apis") */
74
+ namespace: string | undefined;
75
+ /** The log message */
76
+ message: string;
77
+ /** Optional structured data attached to this log entry */
78
+ data?: Record<string, unknown>;
79
+ /** Package context (name and version) */
80
+ context: LoggerContextBase;
81
+ };
82
+
83
+ let globalConfig: Required<DiagnosticsConfig> | undefined;
84
+
85
+ function defaultConfig(): Required<DiagnosticsConfig> {
86
+ const env = (typeof process !== "undefined" && process.env) || {};
87
+
88
+ // Default level: INFO in dev, ERROR in prod
89
+ const isDev = env.NODE_ENV === "development";
90
+ const defaultLevel = isDev ? LogLevel.INFO : LogLevel.ERROR;
91
+
92
+ let level: LogLevel = defaultLevel;
93
+ const envLevel = env.ALCHEMY_LOG_LEVEL;
94
+ if (envLevel) {
95
+ const normalized = envLevel.toUpperCase() as keyof typeof LogLevel;
96
+ if (normalized in LogLevel) {
97
+ level = LogLevel[normalized];
98
+ } else {
99
+ console.warn(
100
+ `[alchemy/common] Invalid ALCHEMY_LOG_LEVEL value: "${envLevel}". Expected one of: ERROR, WARN, INFO, DEBUG, VERBOSE. Using default: ${isDev ? "INFO" : "ERROR"}`,
101
+ );
102
+ }
103
+ }
104
+
105
+ return {
106
+ level,
107
+ redact: {
108
+ keys: (key: string) =>
109
+ /^(authorization|apiKey|jwt|privateKey|secret|password)$/i.test(key),
110
+ replacer: () => "[REDACTED]",
111
+ },
112
+ sinks: [consoleSink],
113
+ disableTelemetryHeaders: false,
114
+ enabledNamespaces: [],
115
+ };
116
+ }
117
+
118
+ /**
119
+ * Gets the current global logger configuration.
120
+ * If not yet initialized, returns default configuration based on environment.
121
+ *
122
+ * @returns {Required<DiagnosticsConfig>} The current global configuration with all fields populated
123
+ * @example
124
+ * ```ts
125
+ * import { getGlobalLoggerConfig } from "@alchemy/common";
126
+ *
127
+ * const config = getGlobalLoggerConfig();
128
+ * console.log("Current log level:", config.level);
129
+ * ```
130
+ */
131
+ export function getGlobalLoggerConfig(): Required<DiagnosticsConfig> {
132
+ globalConfig ??= defaultConfig();
133
+ return globalConfig;
134
+ }
135
+
136
+ /**
137
+ * Sets the global logger configuration.
138
+ * Partial configuration is supported - unspecified fields retain their current values.
139
+ *
140
+ * Configuration precedence (highest to lowest):
141
+ * 1. Explicit setGlobalLoggerConfig calls
142
+ * 2. ALCHEMY_LOG_LEVEL environment variable
143
+ * 3. Defaults (INFO in dev, ERROR in prod)
144
+ *
145
+ * @param {DiagnosticsConfig} cfg - Partial configuration to apply
146
+ * @example
147
+ * ```ts
148
+ * import { setGlobalLoggerConfig, LogLevel } from "@alchemy/common";
149
+ *
150
+ * // Set log level only
151
+ * setGlobalLoggerConfig({ level: LogLevel.DEBUG });
152
+ *
153
+ * // Add custom sink
154
+ * setGlobalLoggerConfig({
155
+ * sinks: [(entry) => console.log(JSON.stringify(entry))]
156
+ * });
157
+ *
158
+ * // Filter to specific namespaces
159
+ * setGlobalLoggerConfig({
160
+ * enabledNamespaces: ["aa-infra", "wallet-apis"]
161
+ * });
162
+ * ```
163
+ */
164
+ export function setGlobalLoggerConfig(cfg: DiagnosticsConfig): void {
165
+ const current = getGlobalLoggerConfig();
166
+ globalConfig = {
167
+ level: cfg.level ?? current.level,
168
+ redact: cfg.redact ?? current.redact,
169
+ sinks: cfg.sinks ?? current.sinks,
170
+ disableTelemetryHeaders:
171
+ cfg.disableTelemetryHeaders ?? current.disableTelemetryHeaders,
172
+ enabledNamespaces:
173
+ "enabledNamespaces" in cfg
174
+ ? (cfg.enabledNamespaces ?? [])
175
+ : current.enabledNamespaces,
176
+ };
177
+ }
178
+
179
+ /**
180
+ * Checks if a given log level is enabled based on current global configuration.
181
+ * Used internally by logger to short-circuit disabled log statements.
182
+ *
183
+ * @param {LogLevel} level - The log level to check
184
+ * @returns {boolean} True if the level is enabled (will be logged), false otherwise
185
+ * @example
186
+ * ```ts
187
+ * import { isLevelEnabled, LogLevel } from "@alchemy/common";
188
+ *
189
+ * if (isLevelEnabled(LogLevel.DEBUG)) {
190
+ * // Perform expensive debug computation
191
+ * }
192
+ * ```
193
+ */
194
+ export function isLevelEnabled(level: LogLevel): boolean {
195
+ return level <= getGlobalLoggerConfig().level;
196
+ }
197
+
198
+ /**
199
+ * Checks if a given namespace is enabled based on current global configuration.
200
+ * Used internally by logger to short-circuit logs from disabled namespaces.
201
+ *
202
+ * @param {string | undefined} namespace - The namespace to check
203
+ * @returns {boolean} True if the namespace is enabled (will be logged), false otherwise
204
+ * @example
205
+ * ```ts
206
+ * import { isNamespaceEnabled } from "@alchemy/common";
207
+ *
208
+ * if (isNamespaceEnabled("aa-infra")) {
209
+ * // This namespace is enabled
210
+ * }
211
+ * ```
212
+ */
213
+ export function isNamespaceEnabled(namespace: string | undefined): boolean {
214
+ const { enabledNamespaces } = getGlobalLoggerConfig();
215
+
216
+ // If no filter is set or it's an empty array, all namespaces are enabled
217
+ if (!enabledNamespaces || enabledNamespaces.length === 0) {
218
+ return true;
219
+ }
220
+
221
+ // If namespace is undefined, disable it (only named namespaces can be filtered)
222
+ if (namespace === undefined) {
223
+ return false;
224
+ }
225
+
226
+ // Check if the namespace is in the enabled list
227
+ return enabledNamespaces.includes(namespace);
228
+ }
229
+
230
+ /**
231
+ * Redacts sensitive keys in an object based on global redaction configuration.
232
+ * Performs deep redaction by recursively processing nested objects and arrays.
233
+ * Default redaction includes: authorization, apiKey, jwt, privateKey, secret, password.
234
+ *
235
+ * @param {Record<string, unknown> | undefined} obj - The object to redact
236
+ * @returns {Record<string, unknown> | undefined} A new object with sensitive values redacted, or undefined if input was undefined
237
+ * @example
238
+ * ```ts
239
+ * import { redactObject } from "@alchemy/common";
240
+ *
241
+ * const data = {
242
+ * apiKey: "secret123",
243
+ * userId: "user-456",
244
+ * nested: { secret: "hidden" }
245
+ * };
246
+ *
247
+ * const redacted = redactObject(data);
248
+ * // { apiKey: "[REDACTED]", userId: "user-456", nested: { secret: "[REDACTED]" } }
249
+ * ```
250
+ */
251
+ export function redactObject(
252
+ obj: Record<string, unknown> | undefined,
253
+ ): Record<string, unknown> | undefined {
254
+ if (!obj) return obj;
255
+ const { keys, replacer } = getGlobalLoggerConfig().redact;
256
+ const out: Record<string, unknown> = {};
257
+ for (const [k, v] of Object.entries(obj)) {
258
+ if (keys && keys(k)) {
259
+ out[k] = replacer ? replacer(v, k) : "[REDACTED]";
260
+ } else if (Array.isArray(v)) {
261
+ // Recursively redact array elements
262
+ out[k] = v.map((item) =>
263
+ item != null && typeof item === "object"
264
+ ? redactObject(item as Record<string, unknown>)
265
+ : item,
266
+ );
267
+ } else if (v != null && typeof v === "object") {
268
+ // Recursively redact nested objects
269
+ out[k] = redactObject(v as Record<string, unknown>);
270
+ } else {
271
+ out[k] = v;
272
+ }
273
+ }
274
+ return out;
275
+ }
276
+
277
+ /**
278
+ * Format timestamp as HH:MM:SS.mmm
279
+ *
280
+ * @param {number} ts - Timestamp in milliseconds since epoch
281
+ * @returns {string} Formatted timestamp string
282
+ */
283
+ function formatTimestamp(ts: number): string {
284
+ const date = new Date(ts);
285
+ const hours = String(date.getHours()).padStart(2, "0");
286
+ const minutes = String(date.getMinutes()).padStart(2, "0");
287
+ const seconds = String(date.getSeconds()).padStart(2, "0");
288
+ const milliseconds = String(date.getMilliseconds()).padStart(3, "0");
289
+ return `${hours}:${minutes}:${seconds}.${milliseconds}`;
290
+ }
291
+
292
+ /**
293
+ * Format data as a JSON object for console output.
294
+ * Filters out context fields (package, version) to avoid duplication.
295
+ * Handles BigInt values by converting them to strings.
296
+ *
297
+ * @param {Record<string, unknown> | undefined} data - The data object to format
298
+ * @returns {string} Formatted JSON string with leading space, or empty string if no data
299
+ */
300
+ function formatData(data: Record<string, unknown> | undefined): string {
301
+ if (!data || Object.keys(data).length === 0) return "";
302
+
303
+ // Filter out context fields (package, version) and create clean object
304
+ const filtered = Object.entries(data)
305
+ .filter(([key]) => key !== "package" && key !== "version")
306
+ .reduce(
307
+ (acc, [key, value]) => {
308
+ acc[key] = value;
309
+ return acc;
310
+ },
311
+ {} as Record<string, unknown>,
312
+ );
313
+
314
+ if (Object.keys(filtered).length === 0) return "";
315
+
316
+ return (
317
+ " " +
318
+ JSON.stringify(filtered, (_key, value) =>
319
+ typeof value === "bigint" ? value.toString() : value,
320
+ )
321
+ );
322
+ }
323
+
324
+ /**
325
+ * Default console sink for diagnostics logging.
326
+ * Routes log entries to appropriate console methods based on log level.
327
+ *
328
+ * Format: [HH:MM:SS.mmm] [package@version] message {data}
329
+ *
330
+ * @param {LogEntry} entry - The log entry to output
331
+ * @example
332
+ * ```ts
333
+ * import { consoleSink, setGlobalLoggerConfig } from "@alchemy/common";
334
+ *
335
+ * // Console sink is used by default, but can be explicitly configured
336
+ * setGlobalLoggerConfig({
337
+ * sinks: [consoleSink]
338
+ * });
339
+ * ```
340
+ */
341
+ export function consoleSink(entry: LogEntry): void {
342
+ const { ts, level, message, data, context } = entry;
343
+ const timestamp = formatTimestamp(ts);
344
+ const prefix = `[${timestamp}] [${context.package}@${context.version}]`;
345
+ const dataStr = formatData(data);
346
+ const output = `${prefix} ${message}${dataStr}`;
347
+
348
+ switch (level) {
349
+ case LogLevel.ERROR:
350
+ console.error(output);
351
+ break;
352
+ case LogLevel.WARN:
353
+ console.warn(output);
354
+ break;
355
+ case LogLevel.INFO:
356
+ console.info(output);
357
+ break;
358
+ case LogLevel.DEBUG:
359
+ console.debug(output);
360
+ break;
361
+ case LogLevel.VERBOSE:
362
+ console.log(output);
363
+ break;
364
+ }
365
+ }
@@ -0,0 +1,20 @@
1
+ export { createLogger, LogLevel, consoleSink } from "./logger.js";
2
+ export type { DiagnosticsLogger } from "./logger.js";
3
+ export {
4
+ setGlobalLoggerConfig,
5
+ getGlobalLoggerConfig,
6
+ isLevelEnabled,
7
+ isNamespaceEnabled,
8
+ redactObject,
9
+ } from "./config.js";
10
+ export type {
11
+ LogEntry,
12
+ DiagnosticsConfig,
13
+ RedactConfig,
14
+ LoggerContextBase,
15
+ } from "./config.js";
16
+
17
+ // Testing utilities
18
+ export { InMemorySink } from "./sinks.js";
19
+
20
+ export type * from "./types.js";
@@ -0,0 +1,39 @@
1
+ import type { EventsSchema, InnerLogger, LoggerContext } from "./types.js";
2
+ import { isClientDevMode } from "./utils.js";
3
+
4
+ /**
5
+ * Creates a local-only logger that outputs events to the console in development mode.
6
+ * This logger does not send data to external services and is safe for all environments.
7
+ *
8
+ * @template Schema - The events schema defining allowed events and their data structures
9
+ * @param {LoggerContext} context - Context information to attach to all events
10
+ * @returns {InnerLogger<Schema>} A logger instance that logs to console in dev mode
11
+ */
12
+ export function createLocalLogger<Schema extends EventsSchema = []>(
13
+ context: LoggerContext,
14
+ ): InnerLogger<Schema> {
15
+ const isDev = isClientDevMode();
16
+
17
+ // Generate a simple anonymous ID for local logging
18
+ const anonId = `local-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
19
+
20
+ return {
21
+ _internal: {
22
+ ready: Promise.resolve(),
23
+ anonId,
24
+ },
25
+ trackEvent: async ({ name, data }) => {
26
+ if (isDev) {
27
+ try {
28
+ console.log(`[${context.package}] Event: ${name}`, {
29
+ ...data,
30
+ ...context,
31
+ timestamp: new Date().toISOString(),
32
+ });
33
+ } catch {
34
+ // Silently ignore console logging errors
35
+ }
36
+ }
37
+ },
38
+ };
39
+ }
@@ -0,0 +1,194 @@
1
+ import {
2
+ LogLevel,
3
+ consoleSink,
4
+ getGlobalLoggerConfig,
5
+ isLevelEnabled,
6
+ isNamespaceEnabled,
7
+ redactObject,
8
+ type LoggerContextBase,
9
+ type LogEntry,
10
+ } from "./config.js";
11
+
12
+ /**
13
+ * Lazy message supplier function for expensive log computations.
14
+ * Only evaluated when the log level is enabled.
15
+ *
16
+ * @returns {[string, Record<string, unknown>?]} Tuple of [message, optional data]
17
+ */
18
+ type MessageSupplier = () => [string, Record<string, unknown>?];
19
+
20
+ /**
21
+ * Diagnostics logger instance for developer-facing debugging output.
22
+ * Provides level-based logging (error, warn, info, debug, verbose) with structured data.
23
+ */
24
+ export type DiagnosticsLogger = {
25
+ /** Log debug messages for development and troubleshooting */
26
+ debug: (
27
+ msg: string | MessageSupplier,
28
+ data?: Record<string, unknown>,
29
+ ) => void;
30
+ /** Log informational messages for key operations */
31
+ info: (msg: string | MessageSupplier, data?: Record<string, unknown>) => void;
32
+ /** Log warnings for recoverable issues */
33
+ warn: (msg: string | MessageSupplier, data?: Record<string, unknown>) => void;
34
+ /** Log errors for failures */
35
+ error: (
36
+ msg: string | MessageSupplier,
37
+ data?: Record<string, unknown>,
38
+ ) => void;
39
+ /** Log verbose details for very detailed tracing */
40
+ verbose: (
41
+ msg: string | MessageSupplier,
42
+ data?: Record<string, unknown>,
43
+ ) => void;
44
+ /** Create a child logger with additional context merged into all log entries */
45
+ withContext: (extra: Record<string, unknown>) => DiagnosticsLogger;
46
+ /**
47
+ * Wrap a function to measure and log its execution time at DEBUG level.
48
+ * Supports both synchronous and asynchronous functions.
49
+ *
50
+ * @template TArgs - Function argument types
51
+ * @template TRet - Function return type
52
+ * @param name - Name for profiling logs
53
+ * @param func - Function to profile
54
+ * @returns Wrapped function that logs execution time
55
+ */
56
+ profiled<TArgs extends any[], TRet>(
57
+ name: string,
58
+ func: (...args: TArgs) => TRet,
59
+ ): (...args: TArgs) => TRet;
60
+ };
61
+
62
+ /**
63
+ * Parameters for creating a diagnostics logger instance.
64
+ */
65
+ export type CreateLoggerParams = LoggerContextBase & {
66
+ /** Optional namespace for filtering/grouping (e.g., "aa-infra", "wallet-apis") */
67
+ namespace?: string;
68
+ /** Optional base context merged into all log entries from this logger */
69
+ baseContext?: Record<string, unknown>;
70
+ };
71
+
72
+ /**
73
+ * Creates a diagnostics logger instance for a package.
74
+ * Loggers emit structured log entries to configured sinks (default: console).
75
+ *
76
+ * All log methods support:
77
+ * - String messages: `logger.info("message", { data })`
78
+ * - Lazy suppliers: `logger.debug(() => ["expensive", computeData()])` (only evaluated when level enabled)
79
+ *
80
+ * @param {CreateLoggerParams} params - Logger configuration
81
+ * @returns {DiagnosticsLogger} A logger instance
82
+ * @example
83
+ * ```ts
84
+ * import { createLogger } from "@alchemy/common";
85
+ *
86
+ * const logger = createLogger({
87
+ * package: "@alchemy/aa-infra",
88
+ * version: "1.0.0",
89
+ * namespace: "aa-infra"
90
+ * });
91
+ *
92
+ * logger.info("processing request", { chainId: 1 });
93
+ * logger.debug("detailed state", { userOp: op });
94
+ *
95
+ * // Child logger with additional context
96
+ * const childLogger = logger.withContext({ requestId: "123" });
97
+ * childLogger.info("step complete"); // includes requestId in all logs
98
+ *
99
+ * // Profile a function
100
+ * const sendWithProfiling = logger.profiled("sendUserOp", sendUserOp);
101
+ * await sendWithProfiling(op); // logs execution time at DEBUG level
102
+ * ```
103
+ */
104
+ export function createLogger(params: CreateLoggerParams): DiagnosticsLogger {
105
+ const { namespace, baseContext, ...ctx } = params;
106
+
107
+ function emit(
108
+ level: LogLevel,
109
+ msgOrFn: string | MessageSupplier,
110
+ data?: Record<string, unknown>,
111
+ ) {
112
+ if (!isLevelEnabled(level)) return;
113
+ if (!isNamespaceEnabled(namespace)) return;
114
+
115
+ let message: string;
116
+ let payload: Record<string, unknown> | undefined = data;
117
+
118
+ if (typeof msgOrFn === "function") {
119
+ const [m, d] = msgOrFn();
120
+ message = m;
121
+ payload = d ?? data;
122
+ } else {
123
+ message = msgOrFn;
124
+ }
125
+
126
+ const entry: LogEntry = {
127
+ ts: Date.now(),
128
+ level,
129
+ namespace,
130
+ message,
131
+ data: redactObject({ ...(baseContext || {}), ...(payload || {}) }),
132
+ context: ctx,
133
+ };
134
+
135
+ for (const sink of getGlobalLoggerConfig().sinks) {
136
+ try {
137
+ sink(entry);
138
+ } catch (e) {
139
+ // Silently continue if a sink throws - don't break other sinks
140
+ }
141
+ }
142
+ }
143
+
144
+ const logger: DiagnosticsLogger = {
145
+ debug: (m, d) => emit(LogLevel.DEBUG, m, d),
146
+ info: (m, d) => emit(LogLevel.INFO, m, d),
147
+ warn: (m, d) => emit(LogLevel.WARN, m, d),
148
+ error: (m, d) => emit(LogLevel.ERROR, m, d),
149
+ verbose: (m, d) => emit(LogLevel.VERBOSE, m, d),
150
+ withContext(extra) {
151
+ return createLogger({
152
+ ...params,
153
+ baseContext: { ...(baseContext || {}), ...extra },
154
+ });
155
+ },
156
+ profiled<TArgs extends any[], TRet>(
157
+ name: string,
158
+ func: (...args: TArgs) => TRet,
159
+ ) {
160
+ return function profiledWrapper(this: any, ...args: TArgs): TRet {
161
+ const start = Date.now();
162
+ const result = func.apply(this, args) as TRet;
163
+ const finish = (ok: boolean) => {
164
+ const dur = Date.now() - start;
165
+ const msg = ok ? `profiled ${name}` : `profiled ${name} (failed)`;
166
+ // Use DEBUG for timing
167
+ emit(LogLevel.DEBUG, msg, {
168
+ executionTimeMs: dur,
169
+ functionName: name,
170
+ });
171
+ };
172
+ const maybePromise = result as unknown as { then?: Function };
173
+ if (maybePromise && typeof maybePromise.then === "function") {
174
+ return (result as unknown as Promise<unknown>).then(
175
+ (r) => {
176
+ finish(true);
177
+ return r as TRet;
178
+ },
179
+ (e) => {
180
+ finish(false);
181
+ throw e;
182
+ },
183
+ ) as unknown as TRet;
184
+ }
185
+ finish(true);
186
+ return result as TRet;
187
+ };
188
+ },
189
+ };
190
+
191
+ return logger;
192
+ }
193
+
194
+ export { LogLevel, consoleSink };
@@ -0,0 +1,13 @@
1
+ import type { InnerLogger } from "./types.js";
2
+
3
+ /**
4
+ * No-operation logger that discards all events.
5
+ * Used as a fallback when logger initialization fails or in disabled states.
6
+ */
7
+ export const noopLogger: InnerLogger<any> = {
8
+ trackEvent: async () => {},
9
+ _internal: {
10
+ ready: Promise.resolve(),
11
+ anonId: "",
12
+ },
13
+ };