TypeScript
Sign an OAuth 1.0a request in plain Node
Posting to X with user-context creds means signing the request yourself. Here is the HMAC-SHA1 signature, built by hand, no library.
21 Jun 2026
The signature base string is the whole game. Get the percent-encoding and the sort order right and the rest falls out.
import { createHmac, randomBytes } from "node:crypto";
// Stricter than encodeURIComponent: OAuth also escapes ! * ' ( )
function pctEncode(value: string): string {
return encodeURIComponent(value).replace(
/[!*'()]/g,
(c) => "%" + c.charCodeAt(0).toString(16).toUpperCase(),
);
}
interface Creds {
consumerKey: string;
consumerSecret: string;
token: string;
tokenSecret: string;
}
export function authHeader(
method: string,
baseUrl: string,
creds: Creds,
): string {
const oauth: Record<string, string> = {
oauth_consumer_key: creds.consumerKey,
oauth_nonce: randomBytes(16).toString("hex"),
oauth_signature_method: "HMAC-SHA1",
oauth_timestamp: Math.floor(Date.now() / 1000).toString(),
oauth_token: creds.token,
oauth_version: "1.0",
};
// Sort by encoded key, then join as k=v with &.
const sortedEncodedParams = Object.keys(oauth)
.map((k) => [pctEncode(k), pctEncode(oauth[k])])
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
.map(([k, v]) => `${k}=${v}`)
.join("&");
const baseString =
method.toUpperCase() +
"&" +
pctEncode(baseUrl) +
"&" +
pctEncode(sortedEncodedParams);
const signingKey =
pctEncode(creds.consumerSecret) + "&" + pctEncode(creds.tokenSecret);
oauth.oauth_signature = createHmac("sha1", signingKey)
.update(baseString)
.digest("base64");
return (
"OAuth " +
Object.keys(oauth)
.sort()
.map((k) => `${pctEncode(k)}="${pctEncode(oauth[k])}"`)
.join(", ")
);
}Gotchas
When you POST a JSON body, the body is not part of the signature. Only the oauth_* params (and any query-string params) go into the base string. People burn an afternoon hashing the body in and getting a 401; per OAuth 1.0a, a non-form-encoded body is correctly left out.
- I think software as you know it is disappearingThe apps go away. The AI builds your software on the fly, and you talk to it. From then on, everything runs on one question: whose model is smarter?Musing
- Giving agents real handsA fleet of thirteen small servers that give agents real hands. What they wrap, the two house styles I build them in, and why they all start read-only.Musing
- mcp-dokkuAn MCP server that drives a Dokku PaaS over SSH.Tool
- Small tool surfaces beat fat APIsA marketing API exposes 115 operations; my server hands the agent six tools. The boundary is set by token budget and model focus, not REST purity.Musing
- Infinite exercises, verifiedA model drafts maths questions against the component library, a verifier throws out the junk, and a clean one renders. Forever.Lab