Skip to content
Draft
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
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -352,7 +352,9 @@ Bluesky doesn't require an application for automated posts, only your identifier
1. Name your app password and click "Next".
1. Copy the generated password and click "Done".

**Important:** Do not use your login password with the API.
**Important:**
- Do not use your login password with the API.
- The identifier should be your human-readable username without the `@` symbol (e.g., `username.bsky.social`), not your DID (Decentralized Identifier).
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- The identifier should be your human-readable username without the `@` symbol (e.g., `username.bsky.social`), not your DID (Decentralized Identifier).
- The identifier should be your human-readable username without the `@` symbol and without the host (e.g., `username`), not your DID (Decentralized Identifier).


### LinkedIn

Expand Down
7 changes: 1 addition & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

153 changes: 153 additions & 0 deletions src/bin.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ const options = {
file: { type: stringType },
image: { type: stringType },
"image-alt": { type: stringType },
verbose: { type: booleanType },
help: { type: booleanType, short: "h" },
version: { type: booleanType, short: "v" },
};
Expand Down Expand Up @@ -122,6 +123,7 @@ if (
console.log("--file The file to read the message from.");
console.log("--image The image file to upload with the message.");
console.log("--image-alt Alt text for the image (default: filename).");
console.log("--verbose Show detailed HTTP request and response information.");
console.log("--help, -h Show this message.");
console.log("--version, -v Show version number.");
process.exit(1);
Expand All @@ -142,6 +144,157 @@ if (process.env.CROSSPOST_DOTENV) {

const env = new Env();

//-----------------------------------------------------------------------------
// Setup verbose logging if requested
//-----------------------------------------------------------------------------

if (flags.verbose) {
const originalFetch = globalThis.fetch;
const sensitiveHeaders = new Set([
"authorization",
"cookie",
"set-cookie",
"x-api-key",
"api-key",
]);
const MAX_BODY_LENGTH = 5000; // Maximum characters to log

globalThis.fetch = async function verboseFetch(url, options) {
console.log("\n--- HTTP Request ---");
console.log(`URL: ${url}`);
console.log(`Method: ${options?.method || "GET"}`);

// Filter sensitive headers
if (options?.headers) {
/** @type {Record<string, string>} */
const filteredHeaders = {};
let headers = options.headers;

// Convert Headers object to plain object if needed
if (
typeof headers === "object" &&
headers !== null &&
typeof headers.entries === "function"
) {
try {
headers = Object.fromEntries(headers.entries());
} catch {
// If entries() fails, just use the object as-is
}
}

// Now iterate over the headers object
if (typeof headers === "object" && headers !== null) {
for (const [key, value] of Object.entries(headers)) {
if (sensitiveHeaders.has(key.toLowerCase())) {
filteredHeaders[key] = "[REDACTED]";
} else {
filteredHeaders[key] = value;
}
}
}
console.log("Headers:", JSON.stringify(filteredHeaders, null, 2));
}

if (options?.body) {
let bodyStr;
if (typeof options.body === "string") {
bodyStr = options.body;
} else if (
typeof options.body === "object" &&
options.body !== null
) {
// Try to parse as JSON and redact sensitive fields
try {
const bodyObj = JSON.parse(
typeof options.body === "string"
? options.body
: JSON.stringify(options.body),
);
// Redact known sensitive fields
if (bodyObj.password) {
bodyObj.password = "[REDACTED]";
}
if (bodyObj.access_token) {
bodyObj.access_token = "[REDACTED]";
}
if (bodyObj.api_key) {
bodyObj.api_key = "[REDACTED]";
}
bodyStr = JSON.stringify(bodyObj);
} catch {
// If we can't parse it, just use the string representation
bodyStr = String(options.body);
}
} else {
bodyStr = String(options.body);
}

if (bodyStr.length > MAX_BODY_LENGTH) {
console.log(
"Body:",
bodyStr.substring(0, MAX_BODY_LENGTH) + "... (truncated)",
);
} else {
console.log("Body:", bodyStr);
}
}

const response = await originalFetch(url, options);

// Clone the response so we can read it for logging and still return it
const responseClone = response.clone();

console.log("\n--- HTTP Response ---");
console.log(`Status: ${response.status} ${response.statusText}`);

// Filter sensitive response headers
/** @type {Record<string, string>} */
const filteredResponseHeaders = {};
for (const [key, value] of response.headers.entries()) {
if (sensitiveHeaders.has(key.toLowerCase())) {
filteredResponseHeaders[key] = "[REDACTED]";
} else {
filteredResponseHeaders[key] = value;
}
}
console.log("Headers:", JSON.stringify(filteredResponseHeaders, null, 2));

// Try to read the response body for logging
try {
const contentType = response.headers.get("content-type");
if (contentType?.includes("application/json")) {
const json = await responseClone.json();
const jsonStr = JSON.stringify(json, null, 2);
if (jsonStr.length > MAX_BODY_LENGTH) {
console.log(
"Body:",
jsonStr.substring(0, MAX_BODY_LENGTH) + "... (truncated)",
);
} else {
console.log("Body:", jsonStr);
}
} else {
const text = await responseClone.text();
if (text.length > MAX_BODY_LENGTH) {
console.log(
"Body:",
text.substring(0, MAX_BODY_LENGTH) + "... (truncated)",
);
} else {
console.log("Body:", text);
}
}
} catch {
console.log("Body: (could not read response body)");
}

console.log("--- End Response ---\n");

return response;
};
}

//-----------------------------------------------------------------------------
// Determine which strategies to use
//-----------------------------------------------------------------------------
Expand Down
27 changes: 27 additions & 0 deletions tests/bin.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -118,4 +118,31 @@ describe("bin", function () {
});
});
});

describe("verbose flag", function () {
it("should include HTTP request/response details when --verbose is set", done => {
// Test that the verbose flag is recognized and shown in help
const child = fork(
builtExecutablePath,
["--verbose", "--help"],
{
stdio: "pipe",
},
);

let output = "";

child.stdout.on("data", data => {
output += data.toString();
});

child.on("exit", code => {
// Help should exit with code 1
assert.strictEqual(code, 1);
// Verify verbose flag is shown in help text
assert.match(output, /--verbose/);
done();
});
});
});
});