@coinbase/cdp-sdk
Version:
SDK for interacting with the Coinbase Developer Platform Wallet API
307 lines • 10.8 kB
JavaScript
import { toJson } from "../json.js";
import { createLogger } from "../logging/logger.js";
import { createRequestUrl } from "./createRequestUrl.js";
import { EndpointSupplier } from "./EndpointSupplier.js";
import { getErrorResponseBody } from "./getErrorResponseBody.js";
import { getFetchFn } from "./getFetchFn.js";
import { getRequestBody } from "./getRequestBody.js";
import { getResponseBody } from "./getResponseBody.js";
import { Headers } from "./Headers.js";
import { makeRequest } from "./makeRequest.js";
import { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js";
import { requestWithRetries } from "./requestWithRetries.js";
const SENSITIVE_HEADERS = new Set([
"authorization",
"www-authenticate",
"x-api-key",
"api-key",
"apikey",
"x-api-token",
"x-auth-token",
"auth-token",
"cookie",
"set-cookie",
"proxy-authorization",
"proxy-authenticate",
"x-csrf-token",
"x-xsrf-token",
"x-session-token",
"x-access-token",
]);
function redactHeaders(headers) {
const filtered = {};
for (const [key, value] of headers instanceof Headers ? headers.entries() : Object.entries(headers)) {
if (SENSITIVE_HEADERS.has(key.toLowerCase())) {
filtered[key] = "[REDACTED]";
}
else {
filtered[key] = value;
}
}
return filtered;
}
const SENSITIVE_QUERY_PARAMS = new Set([
"api_key",
"api-key",
"apikey",
"token",
"access_token",
"access-token",
"auth_token",
"auth-token",
"password",
"passwd",
"secret",
"api_secret",
"api-secret",
"apisecret",
"key",
"session",
"session_id",
"session-id",
]);
function redactQueryParameters(queryParameters) {
if (queryParameters == null) {
return undefined;
}
const redacted = {};
for (const [key, value] of Object.entries(queryParameters)) {
redacted[key] = SENSITIVE_QUERY_PARAMS.has(key.toLowerCase()) ? "[REDACTED]" : value;
}
return redacted;
}
function redactUrl(url) {
const protocolIndex = url.indexOf("://");
if (protocolIndex === -1)
return url;
const afterProtocol = protocolIndex + 3;
// Find the first delimiter that marks the end of the authority section
const pathStart = url.indexOf("/", afterProtocol);
let queryStart = url.indexOf("?", afterProtocol);
let fragmentStart = url.indexOf("#", afterProtocol);
const firstDelimiter = Math.min(pathStart === -1 ? url.length : pathStart, queryStart === -1 ? url.length : queryStart, fragmentStart === -1 ? url.length : fragmentStart);
// Find the LAST @ before the delimiter (handles multiple @ in credentials)
let atIndex = -1;
for (let i = afterProtocol; i < firstDelimiter; i++) {
if (url[i] === "@") {
atIndex = i;
}
}
if (atIndex !== -1) {
url = `${url.slice(0, afterProtocol)}[REDACTED]@${url.slice(atIndex + 1)}`;
}
// Recalculate queryStart since url might have changed
queryStart = url.indexOf("?");
if (queryStart === -1)
return url;
fragmentStart = url.indexOf("#", queryStart);
const queryEnd = fragmentStart !== -1 ? fragmentStart : url.length;
const queryString = url.slice(queryStart + 1, queryEnd);
if (queryString.length === 0)
return url;
// FAST PATH: Quick check if any sensitive keywords present
// Using indexOf is faster than regex for simple substring matching
const lower = queryString.toLowerCase();
const hasSensitive = lower.includes("token") ||
lower.includes("key") ||
lower.includes("password") ||
lower.includes("passwd") ||
lower.includes("secret") ||
lower.includes("session") ||
lower.includes("auth");
if (!hasSensitive) {
return url;
}
// SLOW PATH: Parse and redact
const redactedParams = [];
const params = queryString.split("&");
for (const param of params) {
const equalIndex = param.indexOf("=");
if (equalIndex === -1) {
redactedParams.push(param);
continue;
}
const key = param.slice(0, equalIndex);
let shouldRedact = SENSITIVE_QUERY_PARAMS.has(key.toLowerCase());
if (!shouldRedact && key.includes("%")) {
try {
const decodedKey = decodeURIComponent(key);
shouldRedact = SENSITIVE_QUERY_PARAMS.has(decodedKey.toLowerCase());
}
catch { }
}
redactedParams.push(shouldRedact ? `${key}=[REDACTED]` : param);
}
return url.slice(0, queryStart + 1) + redactedParams.join("&") + url.slice(queryEnd);
}
async function getHeaders(args) {
const newHeaders = new Headers();
newHeaders.set("Accept", args.responseType === "json"
? "application/json"
: args.responseType === "text"
? "text/plain"
: args.responseType === "sse"
? "text/event-stream"
: "*/*");
if (args.body !== undefined && args.contentType != null) {
newHeaders.set("Content-Type", args.contentType);
}
if (args.headers == null) {
return newHeaders;
}
for (const [key, value] of Object.entries(args.headers)) {
const result = await EndpointSupplier.get(value, { endpointMetadata: args.endpointMetadata ?? {} });
if (typeof result === "string") {
newHeaders.set(key, result);
continue;
}
if (result == null) {
continue;
}
newHeaders.set(key, `${result}`);
}
return newHeaders;
}
export async function fetcherImpl(args) {
let url = args.url;
if (args.queryString != null && args.queryString.length > 0) {
url = `${url}?${args.queryString}`;
}
else {
url = createRequestUrl(args.url, args.queryParameters);
}
const requestBody = await getRequestBody({
body: args.body,
type: args.requestType ?? "other",
});
const fetchFn = args.fetchFn ?? (await getFetchFn());
const headers = await getHeaders(args);
const logger = createLogger(args.logging);
if (logger.isDebug()) {
const metadata = {
method: args.method,
url: redactUrl(url),
headers: redactHeaders(headers),
queryParameters: redactQueryParameters(args.queryParameters),
hasBody: requestBody != null,
};
logger.debug("Making HTTP request", metadata);
}
try {
const response = await requestWithRetries(async () => makeRequest(fetchFn, url, args.method, headers, requestBody, args.timeoutMs, args.abortSignal, args.withCredentials, args.duplex, args.responseType === "streaming" || args.responseType === "sse"), args.maxRetries);
if (response.status >= 200 && response.status < 400) {
if (logger.isDebug()) {
const metadata = {
method: args.method,
url: redactUrl(url),
statusCode: response.status,
responseHeaders: redactHeaders(response.headers),
};
logger.debug("HTTP request succeeded", metadata);
}
const body = await getResponseBody(response, args.responseType);
return {
ok: true,
body: body,
headers: response.headers,
rawResponse: toRawResponse(response),
};
}
else {
if (logger.isError()) {
const metadata = {
method: args.method,
url: redactUrl(url),
statusCode: response.status,
responseHeaders: redactHeaders(Object.fromEntries(response.headers.entries())),
};
logger.error("HTTP request failed with error status", metadata);
}
return {
ok: false,
error: {
reason: "status-code",
statusCode: response.status,
body: await getErrorResponseBody(response),
},
rawResponse: toRawResponse(response),
};
}
}
catch (error) {
if (args.abortSignal?.aborted) {
if (logger.isError()) {
const metadata = {
method: args.method,
url: redactUrl(url),
};
logger.error("HTTP request was aborted", metadata);
}
return {
ok: false,
error: {
reason: "unknown",
errorMessage: "The user aborted a request",
cause: error,
},
rawResponse: abortRawResponse,
};
}
else if (error instanceof Error && error.name === "AbortError") {
if (logger.isError()) {
const metadata = {
method: args.method,
url: redactUrl(url),
timeoutMs: args.timeoutMs,
};
logger.error("HTTP request timed out", metadata);
}
return {
ok: false,
error: {
reason: "timeout",
cause: error,
},
rawResponse: abortRawResponse,
};
}
else if (error instanceof Error) {
if (logger.isError()) {
const metadata = {
method: args.method,
url: redactUrl(url),
errorMessage: error.message,
};
logger.error("HTTP request failed with error", metadata);
}
return {
ok: false,
error: {
reason: "unknown",
errorMessage: error.message,
cause: error,
},
rawResponse: unknownRawResponse,
};
}
if (logger.isError()) {
const metadata = {
method: args.method,
url: redactUrl(url),
error: toJson(error),
};
logger.error("HTTP request failed with unknown error", metadata);
}
return {
ok: false,
error: {
reason: "unknown",
errorMessage: toJson(error),
cause: error,
},
rawResponse: unknownRawResponse,
};
}
}
export const fetcher = fetcherImpl;
//# sourceMappingURL=Fetcher.js.map