← Public packages
@kentcdodds/x
X API v2 helpers for tweets, search, legacy DMs, and encrypted X Chat via a Fly XDK sidecar.
src/openapi-client.ts
329 lines · 11.9 KB · TypeScriptimport { createAuthenticatedFetch } from 'kody:runtime'
/**
* Scaffolded X OpenAPI client (dependency-free ESM).
*
* Source: openapi_client_scaffold against
* https://api.twitter.com/2/openapi.json (X API v2 ~815KB; under the 4MB fetch cap).
* Auth default: bearerSecret xBearerToken
*
* Unscoped {{secret:xBearerToken}} resolves for this user-scoped secret.
* OAuth user-context calls use rawXRequest with authMode: 'oauth' and
* createAuthenticatedFetch(integrationName) from kody:runtime.
*/
export type QueryValue = string | number | boolean | null | undefined
export type QueryInput = Record<string, QueryValue | QueryValue[]>
export type ScaffoldInput = {
params?: Record<string, unknown>
query?: QueryInput
headers?: Record<string, string>
body?: unknown
}
export type ScaffoldOptions = {
fetchImpl?: typeof fetch
}
const API_BASE_URL = 'https://api.x.com'
const BEARER_AUTH_HEADER = 'Bearer {{secret:xBearerToken}}'
export function buildUrl(pathTemplate: string, params: Record<string, unknown> = {}): string {
return (
API_BASE_URL +
pathTemplate.replace(/\{([^}]+)\}/g, (_match, name: string) => {
const value = params[name]
if (value === undefined || value === null) {
throw new Error(`Missing required path parameter: ${name}`)
}
return encodeURIComponent(String(value))
})
)
}
export function appendQuery(url: string, query: QueryInput = {}): string {
const search = new URLSearchParams()
for (const [key, value] of Object.entries(query)) {
if (value === undefined || value === null || value === '') continue
if (Array.isArray(value)) {
for (const item of value) {
if (item === undefined || item === null || item === '') continue
search.append(key, String(item))
}
continue
}
search.append(key, String(value))
}
const qs = search.toString()
return qs ? `${url}?${qs}` : url
}
function mergeHeaders(
userHeaders: Record<string, string> | undefined,
authHeaders: Record<string, string>,
): Record<string, string> {
const merged: Record<string, string> = { ...(userHeaders ?? {}) }
for (const [key, value] of Object.entries(authHeaders)) {
const lower = key.toLowerCase()
for (const existing of Object.keys(merged)) {
if (existing.toLowerCase() === lower) delete merged[existing]
}
merged[key] = value
}
return merged
}
function hasHeader(headers: Record<string, string>, name: string): boolean {
const lower = name.toLowerCase()
return Object.keys(headers).some((key) => key.toLowerCase() === lower)
}
async function resolveFetch(fetchImpl?: typeof fetch): Promise<typeof fetch> {
return fetchImpl ?? fetch
}
function bearerAuthHeaders(): Record<string, string> {
return { Authorization: BEARER_AUTH_HEADER }
}
function oauthAuthHeaders(
accessToken?: string,
accessTokenSecretName = 'xAccessToken',
): Record<string, string> {
return {
Authorization: accessToken
? `Bearer ${accessToken}`
: `Bearer {{secret:${accessTokenSecretName}}}`,
}
}
async function scaffoldFetch(
method: string,
pathTemplate: string,
input: ScaffoldInput = {},
options: ScaffoldOptions = {},
): Promise<Response> {
const params = input.params ?? {}
const url = appendQuery(buildUrl(pathTemplate, params), input.query)
const headers = mergeHeaders(input.headers, bearerAuthHeaders())
let body: string | undefined
if (input.body !== undefined) {
body = JSON.stringify(input.body)
if (!hasHeader(headers, 'content-type')) headers['content-type'] = 'application/json'
}
const fetchImpl = await resolveFetch(options.fetchImpl)
return fetchImpl(url, { method, headers, body })
}
/** GET /2/users/me — Get my User (app-only bearer; prefer oauth via rawXRequest for user context). */
export async function getusersme(input: ScaffoldInput = {}, options: ScaffoldOptions = {}): Promise<Response> {
return scaffoldFetch('GET', '/2/users/me', input, options)
}
/** GET /2/users/by/username/{username} — Get User by username */
export async function getusersbyusername(
input: ScaffoldInput = {},
options: ScaffoldOptions = {},
): Promise<Response> {
const params = input.params ?? {}
if (params.username === undefined || params.username === null) {
throw new Error('Missing required path parameter: username')
}
return scaffoldFetch('GET', '/2/users/by/username/{username}', input, options)
}
/** GET /2/tweets/search/recent — Search recent Posts */
export async function searchpostsrecent(
input: ScaffoldInput = {},
options: ScaffoldOptions = {},
): Promise<Response> {
return scaffoldFetch('GET', '/2/tweets/search/recent', input, options)
}
/** GET /2/users/{id}/tweets — Get Posts */
export async function getusersposts(input: ScaffoldInput = {}, options: ScaffoldOptions = {}): Promise<Response> {
const params = input.params ?? {}
if (params.id === undefined || params.id === null) {
throw new Error('Missing required path parameter: id')
}
return scaffoldFetch('GET', '/2/users/{id}/tweets', input, options)
}
/** GET /2/tweets/{id} — Get Post by ID */
export async function getpostsbyid(input: ScaffoldInput = {}, options: ScaffoldOptions = {}): Promise<Response> {
const params = input.params ?? {}
if (params.id === undefined || params.id === null) {
throw new Error('Missing required path parameter: id')
}
return scaffoldFetch('GET', '/2/tweets/{id}', input, options)
}
/** POST /2/tweets — Create or Edit Post */
export async function createposts(input: ScaffoldInput = {}, options: ScaffoldOptions = {}): Promise<Response> {
return scaffoldFetch('POST', '/2/tweets', input, options)
}
/** DELETE /2/tweets/{id} — Delete Post */
export async function deleteposts(input: ScaffoldInput = {}, options: ScaffoldOptions = {}): Promise<Response> {
const params = input.params ?? {}
if (params.id === undefined || params.id === null) {
throw new Error('Missing required path parameter: id')
}
return scaffoldFetch('DELETE', '/2/tweets/{id}', input, options)
}
/** POST /2/users/{id}/likes — Like Post */
export async function likepost(input: ScaffoldInput = {}, options: ScaffoldOptions = {}): Promise<Response> {
const params = input.params ?? {}
if (params.id === undefined || params.id === null) {
throw new Error('Missing required path parameter: id')
}
return scaffoldFetch('POST', '/2/users/{id}/likes', input, options)
}
/** DELETE /2/users/{id}/likes/{tweet_id} — Unlike Post */
export async function unlikepost(input: ScaffoldInput = {}, options: ScaffoldOptions = {}): Promise<Response> {
const params = input.params ?? {}
if (params.id === undefined || params.id === null) {
throw new Error('Missing required path parameter: id')
}
if (params.tweet_id === undefined || params.tweet_id === null) {
throw new Error('Missing required path parameter: tweet_id')
}
return scaffoldFetch('DELETE', '/2/users/{id}/likes/{tweet_id}', input, options)
}
/** POST /2/users/{id}/retweets — Repost Post */
export async function repostpost(input: ScaffoldInput = {}, options: ScaffoldOptions = {}): Promise<Response> {
const params = input.params ?? {}
if (params.id === undefined || params.id === null) {
throw new Error('Missing required path parameter: id')
}
return scaffoldFetch('POST', '/2/users/{id}/retweets', input, options)
}
/** DELETE /2/users/{id}/retweets/{source_tweet_id} — Unrepost Post */
export async function unrepostpost(input: ScaffoldInput = {}, options: ScaffoldOptions = {}): Promise<Response> {
const params = input.params ?? {}
if (params.id === undefined || params.id === null) {
throw new Error('Missing required path parameter: id')
}
if (params.source_tweet_id === undefined || params.source_tweet_id === null) {
throw new Error('Missing required path parameter: source_tweet_id')
}
return scaffoldFetch('DELETE', '/2/users/{id}/retweets/{source_tweet_id}', input, options)
}
/** POST /2/users/{id}/bookmarks — Create Bookmark */
export async function createusersbookmark(
input: ScaffoldInput = {},
options: ScaffoldOptions = {},
): Promise<Response> {
const params = input.params ?? {}
if (params.id === undefined || params.id === null) {
throw new Error('Missing required path parameter: id')
}
return scaffoldFetch('POST', '/2/users/{id}/bookmarks', input, options)
}
/** DELETE /2/users/{id}/bookmarks/{tweet_id} — Delete Bookmark */
export async function deleteusersbookmark(
input: ScaffoldInput = {},
options: ScaffoldOptions = {},
): Promise<Response> {
const params = input.params ?? {}
if (params.id === undefined || params.id === null) {
throw new Error('Missing required path parameter: id')
}
if (params.tweet_id === undefined || params.tweet_id === null) {
throw new Error('Missing required path parameter: tweet_id')
}
return scaffoldFetch('DELETE', '/2/users/{id}/bookmarks/{tweet_id}', input, options)
}
/** POST /2/users/{id}/following — Follow User */
export async function followuser(input: ScaffoldInput = {}, options: ScaffoldOptions = {}): Promise<Response> {
const params = input.params ?? {}
if (params.id === undefined || params.id === null) {
throw new Error('Missing required path parameter: id')
}
return scaffoldFetch('POST', '/2/users/{id}/following', input, options)
}
/** DELETE /2/users/{source_user_id}/following/{target_user_id} — Unfollow User */
export async function unfollowuser(input: ScaffoldInput = {}, options: ScaffoldOptions = {}): Promise<Response> {
const params = input.params ?? {}
if (params.source_user_id === undefined || params.source_user_id === null) {
throw new Error('Missing required path parameter: source_user_id')
}
if (params.target_user_id === undefined || params.target_user_id === null) {
throw new Error('Missing required path parameter: target_user_id')
}
return scaffoldFetch('DELETE', '/2/users/{source_user_id}/following/{target_user_id}', input, options)
}
export type XAuthMode = 'bearer' | 'oauth'
/**
* Escape-hatch / OAuth-capable fetch for paths relative to https://api.x.com/2.
* Use authMode: 'oauth' for user-context writes and /users/me.
*/
export async function rawXRequest(
path: string,
options: {
method?: string
query?: QueryInput
body?: unknown
headers?: Record<string, string>
authMode?: XAuthMode
accessToken?: string
/** OAuth access-token secret name for multi-account integrations. Defaults to xAccessToken. */
accessTokenSecretName?: string
/** Saved OAuth integration name. OAuth calls use createAuthenticatedFetch(name). */
integrationName?: string
fetchImpl?: typeof fetch
} = {},
): Promise<Response> {
const method = (options.method || (options.body !== undefined ? 'POST' : 'GET')).toUpperCase()
const authMode = options.authMode || (method === 'GET' ? 'bearer' : 'oauth')
const normalized = path.replace(/^\/+/, '').replace(/^2\//, '')
const url = appendQuery(`${API_BASE_URL}/2/${normalized}`, options.query)
let body: string | undefined
if (options.body !== undefined && options.body !== null) {
body = typeof options.body === 'string' ? options.body : JSON.stringify(options.body)
}
if (authMode === 'oauth' && !options.accessToken) {
const integrationName = options.integrationName
if (!integrationName) {
throw new Error(
'OAuth X requests need integrationName (or an explicit accessToken). Use createAuthenticatedFetch from kody:runtime.',
)
}
const headers: Record<string, string> = {
accept: 'application/json',
...(options.headers ?? {}),
}
if (body !== undefined && !hasHeader(headers, 'content-type')) {
headers['content-type'] = 'application/json'
}
const authenticatedFetch = await Promise.resolve(createAuthenticatedFetch(integrationName))
return authenticatedFetch(url, { method, headers, body })
}
const auth =
authMode === 'oauth'
? oauthAuthHeaders(options.accessToken, options.accessTokenSecretName)
: bearerAuthHeaders()
const headers = mergeHeaders(
{
accept: 'application/json',
...(options.headers ?? {}),
},
auth,
)
if (body !== undefined && !hasHeader(headers, 'content-type')) headers['content-type'] = 'application/json'
const fetchImpl = await resolveFetch(options.fetchImpl)
return fetchImpl(url, { method, headers, body })
}