Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions packages/common/base/src/logger/SdkLogger.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { ISdkLogger } from "./interfaces";
import type { ConsoleLogLevel, LogContext, LogEntry, LogLevel, LogSink } from "./types";
import { generateExecutionId, mergeContext, serializeLogArgs } from "./utils";
import { generateExecutionId, mergeContext, redactSensitiveFields, serializeLogArgs } from "./utils";
import { ConsoleSink, detectPlatform, type Platform } from "./index";
import { sinkManager } from "./sink-manager";
import type { APIKeyEnvironmentPrefix } from "../apiKey/types";
Expand Down Expand Up @@ -410,11 +410,16 @@ export class SdkLogger implements ISdkLogger {
// getExecutionContext() returns context from AsyncLocalStorage (Node.js) or instance field (browser/RN)
const mergedContext = mergeContext(this.globalContext, this.getExecutionContext() ?? {}, argsContext);

// Redact sensitive fields (JWTs, API keys, auth tokens, etc.) before passing to sinks.
// This is a defense-in-depth measure — callers should avoid logging sensitive data,
// but this catches any that slip through.
const redactedContext = redactSensitiveFields(mergedContext) as LogContext;

// Create log entry
const entry: LogEntry = {
level,
message: serializedMessage,
context: mergedContext,
context: redactedContext,
timestamp: Date.now(),
};

Expand Down
22 changes: 7 additions & 15 deletions packages/common/base/src/logger/decorators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,34 +64,26 @@ export function WithLoggerContext<TThis = unknown>(options: WithLoggerContextOpt
const wrapped = function (this: TThis, ...args: unknown[]): unknown {
const ctx = options.buildContext ? options.buildContext(this, args) : {};
// Delegate execution lifecycle to logger.withExecutionContext
// Note: We intentionally do NOT log method arguments or return values here.
// Method args/results may contain sensitive data (JWTs, API keys, auth tokens).
// Each method is responsible for its own curated log statements with safe fields.
return options.logger.withExecutionContext(options.methodName, ctx, () => {
// Log the parameters before calling the function
options.logger.info(`${options.methodName} called`, { args });

let result: unknown;
try {
result = original.apply(this, args);
} catch (error) {
// Log the error before throwing
options.logger.error(`${options.methodName} threw an error`, { error });
throw error;
}

// Handle async functions
if (result instanceof Promise) {
return result
.then((value) => {
options.logger.info(`${options.methodName} completed successfully`, { result: value });
return value;
})
.catch((error) => {
options.logger.error(`${options.methodName} threw an error`, { error });
throw error;
});
return result.catch((error) => {
options.logger.error(`${options.methodName} threw an error`, { error });
throw error;
});
}

// Log success for sync functions
options.logger.info(`${options.methodName} completed successfully`, { result });
return result;
});
};
Expand Down
83 changes: 83 additions & 0 deletions packages/common/base/src/logger/utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,88 @@
import type { LogContext } from "./types";

/**
* Set of keys whose values should be redacted from log output.
* Keys are matched case-insensitively against object property names.
*/
const SENSITIVE_KEYS = new Set([
"jwt",
"apikey",
"api_key",
"authorization",
"x-api-key",
"accesstoken",
"access_token",
"authtoken",
"auth_token",
"bearertoken",
"bearer_token",
"idtoken",
"id_token",
"refreshtoken",
"refresh_token",
"secret",
"client_secret",
"password",
"privatekey",
"private_key",
"credential",
"authdata",
"bearer",
]);

const MAX_REDACTION_DEPTH = 10;

/**
* Truncates a string value for redacted output, showing first and last 4 chars.
* For short strings (<=8 chars), returns '[REDACTED]'.
*/
function redactValue(value: unknown): string {
if (typeof value === "string" && value.length > 8) {
return `${value.slice(0, 4)}...${value.slice(-4)}`;
}
return "[REDACTED]";
}

/**
* Recursively redacts known-sensitive fields from an object before logging.
* This is a defense-in-depth measure to prevent credentials (JWTs, API keys, etc.)
* from appearing in console output or external log sinks.
*
* @param obj - The value to redact
* @param depth - Current recursion depth (capped at MAX_REDACTION_DEPTH)
* @returns A new object with sensitive fields replaced by redacted placeholders
*/
export function redactSensitiveFields(obj: unknown, depth = 0): unknown {
if (depth > MAX_REDACTION_DEPTH || obj == null || typeof obj !== "object") {
return obj;
}

if (Array.isArray(obj)) {
return obj.map((item) => redactSensitiveFields(item, depth + 1));
}

// Error instances have non-enumerable properties (name, message, stack);
// preserve them explicitly so error observability is not lost.
if (obj instanceof Error) {
return {
name: obj.name,
message: obj.message,
stack: obj.stack,
...(redactSensitiveFields(Object.fromEntries(Object.entries(obj)), depth + 1) as object),
};
}

const result: Record<string, unknown> = {};
for (const [key, value] of Object.entries(obj as Record<string, unknown>)) {
if (SENSITIVE_KEYS.has(key.toLowerCase())) {
result[key] = redactValue(value);
} else {
result[key] = redactSensitiveFields(value, depth + 1);
}
}
return result;
}

/**
* Generates a unique execution ID for tracing function execution
* Uses a combination of timestamp and random characters for uniqueness
Expand Down
5 changes: 4 additions & 1 deletion packages/wallets/src/api/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,10 @@ class ApiClient extends CrossmintApiClient {
error: result.error,
});
} else if ("address" in result) {
walletsLogger.info("wallets.api.getWallet.success", result);
walletsLogger.info("wallets.api.getWallet.success", {
address: result.address,
locator,
});
}
return result;
}
Expand Down
6 changes: 4 additions & 2 deletions packages/wallets/src/wallets/wallet-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,11 @@ export class WalletFactory {
methodName: "walletFactory.getWallet",
buildContext(_thisArg: WalletFactory, args: unknown[]) {
if (typeof args[0] === "string") {
return { walletLocator: args[0] as string, args: args[1] as WalletArgsFor<Chain> };
const walletArgs = args[1] as WalletArgsFor<Chain> | undefined;
return { walletLocator: args[0] as string, chain: walletArgs?.chain };
}
return { args: args[0] as WalletArgsFor<Chain> };
const walletArgs = args[0] as WalletArgsFor<Chain>;
return { chain: walletArgs?.chain };
},
})
public async getWallet<C extends Chain>(
Expand Down
Loading