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.
- 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
- Guardrails for agents in productionA catalogue of the guards I actually ship: typed confirmations, blast-radius escalation, pay-to-play gating, ordered workflows, and read-only by default.Musing