@ofauth/onlyfans-sdk 2.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.ts ADDED
@@ -0,0 +1,4 @@
1
+ /* eslint-disable */
2
+ export * from './runtime';
3
+ export * from './client';
4
+ export * from './webhooks';
package/src/runtime.ts ADDED
@@ -0,0 +1,183 @@
1
+ /* eslint-disable */
2
+ /**
3
+ * OFAuth API
4
+ * TypeScript SDK v2 - Nested client structure
5
+ */
6
+
7
+ export const BASE_PATH = "https://api-next.ofauth.com";
8
+
9
+ export interface OFAuthConfig {
10
+ apiKey: string;
11
+ basePath?: string;
12
+ connectionId?: string;
13
+ fetchApi?: typeof fetch;
14
+ }
15
+
16
+ export interface RequestConfig {
17
+ path: string;
18
+ method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
19
+ query?: Record<string, any>;
20
+ body?: any;
21
+ headers?: Record<string, string>;
22
+ connectionId?: string;
23
+ }
24
+
25
+ export interface PaginatedResponse<T> {
26
+ list: T[];
27
+ hasMore: boolean;
28
+ nextOffset?: number;
29
+ }
30
+
31
+ export interface MarkerPaginatedResponse<T> {
32
+ list: T[];
33
+ hasMore: boolean;
34
+ marker?: string;
35
+ }
36
+
37
+ export interface PaginationOptions {
38
+ maxItems?: number;
39
+ pageSize?: number;
40
+ }
41
+
42
+ export class OFAuthAPIError extends Error {
43
+ readonly status: number;
44
+ readonly code?: string;
45
+ readonly details?: any;
46
+ readonly response: Response;
47
+
48
+ constructor(response: Response, body?: { message?: string; code?: string; details?: any }) {
49
+ super(body?.message || `HTTP ${response.status}: ${response.statusText}`);
50
+ this.name = 'OFAuthAPIError';
51
+ this.status = response.status;
52
+ this.code = body?.code;
53
+ this.details = body?.details;
54
+ this.response = response;
55
+ }
56
+ }
57
+
58
+ function buildQueryString(params: Record<string, any>): string {
59
+ const entries: string[] = [];
60
+ for (const [key, value] of Object.entries(params)) {
61
+ if (value === undefined || value === null) continue;
62
+ if (Array.isArray(value)) {
63
+ for (const v of value) {
64
+ entries.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(v))}`);
65
+ }
66
+ } else {
67
+ entries.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`);
68
+ }
69
+ }
70
+ return entries.length > 0 ? `?${entries.join('&')}` : '';
71
+ }
72
+
73
+ export async function request<T>(config: OFAuthConfig, reqConfig: RequestConfig): Promise<T> {
74
+ const fetchFn = config.fetchApi || fetch;
75
+ const basePath = config.basePath || BASE_PATH;
76
+ const connectionId = reqConfig.connectionId || config.connectionId;
77
+
78
+ const url = basePath + reqConfig.path + (reqConfig.query ? buildQueryString(reqConfig.query) : '');
79
+
80
+ const headers: Record<string, string> = {
81
+ 'apiKey': config.apiKey,
82
+ ...reqConfig.headers,
83
+ };
84
+
85
+ if (connectionId) {
86
+ headers['x-connection-id'] = connectionId;
87
+ }
88
+
89
+ if (reqConfig.body && typeof reqConfig.body === 'object' &&
90
+ !(reqConfig.body instanceof FormData) && !(reqConfig.body instanceof Blob)) {
91
+ headers['Content-Type'] = 'application/json';
92
+ }
93
+
94
+ const response = await fetchFn(url, {
95
+ method: reqConfig.method,
96
+ headers,
97
+ body: reqConfig.body instanceof FormData || reqConfig.body instanceof Blob
98
+ ? reqConfig.body
99
+ : reqConfig.body ? JSON.stringify(reqConfig.body) : undefined,
100
+ });
101
+
102
+ if (!response.ok) {
103
+ let errorBody: any;
104
+ try { errorBody = await response.json(); } catch {}
105
+ throw new OFAuthAPIError(response, errorBody);
106
+ }
107
+
108
+ const contentType = response.headers.get('content-type');
109
+ if (contentType?.includes('application/json')) {
110
+ return response.json();
111
+ }
112
+
113
+ if (response.status === 204) return {} as T;
114
+ return response.text() as unknown as T;
115
+ }
116
+
117
+ export type ItemType<T> = T extends { list: Array<infer U> } ? U : never;
118
+
119
+ export interface ProxyRequestOptions {
120
+ path: string;
121
+ method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
122
+ query?: Record<string, any>;
123
+ body?: any;
124
+ connectionId?: string;
125
+ }
126
+
127
+ export async function proxy<T = any>(config: OFAuthConfig, opts: ProxyRequestOptions): Promise<T> {
128
+ const fetchFn = config.fetchApi || fetch;
129
+ const basePath = config.basePath || BASE_PATH;
130
+
131
+ let targetPath = opts.path;
132
+ if (targetPath.startsWith('/api2/v2')) {
133
+ targetPath = targetPath.slice(8);
134
+ } else if (targetPath.startsWith('api2/v2')) {
135
+ targetPath = '/' + targetPath.slice(7);
136
+ }
137
+
138
+ if (!targetPath.startsWith('/')) {
139
+ targetPath = '/' + targetPath;
140
+ }
141
+
142
+ let url = basePath + '/v2/access/proxy' + targetPath;
143
+
144
+ if (opts.query) {
145
+ url += buildQueryString(opts.query);
146
+ }
147
+
148
+ const headers: Record<string, string> = {
149
+ 'apiKey': config.apiKey,
150
+ };
151
+
152
+ const connectionId = opts.connectionId || config.connectionId;
153
+ if (connectionId) {
154
+ headers['x-connection-id'] = connectionId;
155
+ }
156
+
157
+ if (opts.body && typeof opts.body === 'object' &&
158
+ !(opts.body instanceof FormData) && !(opts.body instanceof Blob)) {
159
+ headers['Content-Type'] = 'application/json';
160
+ }
161
+
162
+ const response = await fetchFn(url, {
163
+ method: opts.method,
164
+ headers,
165
+ body: opts.body instanceof FormData || opts.body instanceof Blob
166
+ ? opts.body
167
+ : opts.body ? JSON.stringify(opts.body) : undefined,
168
+ });
169
+
170
+ if (!response.ok) {
171
+ let errorBody: any;
172
+ try { errorBody = await response.json(); } catch {}
173
+ throw new OFAuthAPIError(response, errorBody);
174
+ }
175
+
176
+ const contentType = response.headers.get('content-type');
177
+ if (contentType?.includes('application/json')) {
178
+ return response.json();
179
+ }
180
+
181
+ if (response.status === 204) return {} as T;
182
+ return response.text() as unknown as T;
183
+ }
@@ -0,0 +1,447 @@
1
+ /* eslint-disable */
2
+ /**
3
+ * OFAuth Webhook Verification & Routing
4
+ * Svix-compatible HMAC-SHA256 signature verification
5
+ */
6
+
7
+ import * as crypto from "node:crypto";
8
+
9
+ // ============================================================================
10
+ // Types
11
+ // ============================================================================
12
+
13
+ /** Svix webhook headers */
14
+ export interface WebhookHeaders {
15
+ "svix-id": string;
16
+ "svix-timestamp": string;
17
+ "svix-signature": string;
18
+ }
19
+
20
+ /** Options for webhook verification */
21
+ export interface WebhookVerificationOptions {
22
+ /** Webhook signing secret (with or without whsec_ prefix) */
23
+ secret: string;
24
+ /** Maximum allowed timestamp age in seconds (default: 300 = 5 minutes) */
25
+ tolerance?: number;
26
+ }
27
+
28
+ /** User data from OnlyFans */
29
+ export interface WebhookUserData {
30
+ userId: string;
31
+ name: string;
32
+ username: string;
33
+ avatar: string;
34
+ }
35
+
36
+ /** Connection object in webhook events */
37
+ export interface WebhookConnection {
38
+ id: string;
39
+ platformUserId: string;
40
+ status: "active" | "awaiting_2fa" | "expired";
41
+ userData?: WebhookUserData;
42
+ permissions?: string[];
43
+ }
44
+
45
+ /** Dynamic rules configuration */
46
+ export interface WebhookDynamicRules {
47
+ static_param: string;
48
+ format: string;
49
+ start: string;
50
+ end: string;
51
+ prefix: string;
52
+ suffix: string;
53
+ checksum_constant: number;
54
+ checksum_indexes: number[];
55
+ app_token: string;
56
+ revision: string;
57
+ }
58
+
59
+ /** Base webhook event */
60
+ export interface BaseWebhookEvent {
61
+ eventType: string;
62
+ live: boolean;
63
+ data: Record<string, any>;
64
+ }
65
+
66
+ /** connection.created event */
67
+ export interface ConnectionCreatedEvent extends BaseWebhookEvent {
68
+ eventType: "connection.created";
69
+ data: {
70
+ connection: WebhookConnection;
71
+ clientReferenceId: string | null;
72
+ };
73
+ }
74
+
75
+ /** connection.updated event */
76
+ export interface ConnectionUpdatedEvent extends BaseWebhookEvent {
77
+ eventType: "connection.updated";
78
+ data: {
79
+ connection: WebhookConnection;
80
+ clientReferenceId: string | null;
81
+ };
82
+ }
83
+
84
+ /** connection.expired event */
85
+ export interface ConnectionExpiredEvent extends BaseWebhookEvent {
86
+ eventType: "connection.expired";
87
+ data: {
88
+ connection: {
89
+ id: string;
90
+ platformUserId: string;
91
+ status: "expired";
92
+ };
93
+ clientReferenceId: string | null;
94
+ };
95
+ }
96
+
97
+ /** rules.updated event */
98
+ export interface RulesUpdatedEvent extends BaseWebhookEvent {
99
+ eventType: "rules.updated";
100
+ data: {
101
+ rules: WebhookDynamicRules;
102
+ revision: string;
103
+ };
104
+ }
105
+
106
+ /** Union of all webhook event types */
107
+ export type WebhookEvent =
108
+ | ConnectionCreatedEvent
109
+ | ConnectionUpdatedEvent
110
+ | ConnectionExpiredEvent
111
+ | RulesUpdatedEvent;
112
+
113
+ /** String literal union of event type names */
114
+ export type WebhookEventType = WebhookEvent["eventType"];
115
+
116
+ /** Extract the data shape for a given event type */
117
+ export type WebhookEventData<T extends WebhookEventType> = Extract<
118
+ WebhookEvent,
119
+ { eventType: T }
120
+ >["data"];
121
+
122
+ /** Handler function for a specific event type */
123
+ export type WebhookHandler<T extends WebhookEventType = WebhookEventType> = (
124
+ event: Extract<WebhookEvent, { eventType: T }>
125
+ ) => Promise<void> | void;
126
+
127
+ /** Configuration for WebhookRouter */
128
+ export interface WebhookRouterConfig {
129
+ secret: string;
130
+ tolerance?: number;
131
+ handlers?: Partial<Record<WebhookEventType, WebhookHandler>>;
132
+ defaultHandler?: WebhookHandler;
133
+ errorHandler?: (error: Error, event?: WebhookEvent) => void;
134
+ }
135
+
136
+ // ============================================================================
137
+ // Errors
138
+ // ============================================================================
139
+
140
+ /** Error thrown when webhook verification fails */
141
+ export class WebhookVerificationError extends Error {
142
+ constructor(message: string, public code: string) {
143
+ super(message);
144
+ this.name = "WebhookVerificationError";
145
+ }
146
+ }
147
+
148
+ /** Type guard for WebhookVerificationError */
149
+ export function isWebhookVerificationError(
150
+ error: unknown
151
+ ): error is WebhookVerificationError {
152
+ return error instanceof WebhookVerificationError;
153
+ }
154
+
155
+ // ============================================================================
156
+ // Verification
157
+ // ============================================================================
158
+
159
+ /**
160
+ * Extract and validate Svix webhook headers from a request.
161
+ * Accepts either a plain object or a `Headers` instance.
162
+ */
163
+ export function extractWebhookHeaders(
164
+ headers: Record<string, string> | Headers
165
+ ): WebhookHeaders {
166
+ const get = (name: string): string | null => {
167
+ if (headers instanceof Headers) return headers.get(name);
168
+ return headers[name] || headers[name.toLowerCase()] || null;
169
+ };
170
+
171
+ const svixId = get("svix-id");
172
+ const svixTimestamp = get("svix-timestamp");
173
+ const svixSignature = get("svix-signature");
174
+
175
+ if (!svixId || !svixTimestamp || !svixSignature) {
176
+ throw new WebhookVerificationError(
177
+ "Missing required webhook headers (svix-id, svix-timestamp, svix-signature)",
178
+ "MISSING_HEADERS"
179
+ );
180
+ }
181
+
182
+ return {
183
+ "svix-id": svixId,
184
+ "svix-timestamp": svixTimestamp,
185
+ "svix-signature": svixSignature,
186
+ };
187
+ }
188
+
189
+ /**
190
+ * Verify a webhook signature using the Svix HMAC-SHA256 protocol.
191
+ *
192
+ * @returns true if the signature is valid
193
+ * @throws WebhookVerificationError on failure
194
+ */
195
+ export function verifyWebhookSignature(
196
+ payload: string,
197
+ headers: WebhookHeaders,
198
+ options: WebhookVerificationOptions
199
+ ): boolean {
200
+ const { secret, tolerance = 300 } = options;
201
+
202
+ // --- timestamp check ---
203
+ const ts = parseInt(headers["svix-timestamp"], 10);
204
+ if (isNaN(ts)) {
205
+ throw new WebhookVerificationError("Invalid timestamp format", "INVALID_TIMESTAMP");
206
+ }
207
+ if (Math.abs(Math.floor(Date.now() / 1000) - ts) > tolerance) {
208
+ throw new WebhookVerificationError("Webhook timestamp too old", "TIMESTAMP_TOO_OLD");
209
+ }
210
+
211
+ // --- compute expected signature ---
212
+ const cleanSecret = secret.startsWith("whsec_") ? secret.slice(6) : secret;
213
+ const key = Buffer.from(cleanSecret, "base64");
214
+ const signed = `${headers["svix-id"]}.${headers["svix-timestamp"]}.${payload}`;
215
+ const expected = crypto.createHmac("sha256", key).update(signed).digest("base64");
216
+
217
+ // --- compare against provided signatures ---
218
+ const sigs = headers["svix-signature"]
219
+ .split(" ")
220
+ .map((s) => {
221
+ const [version, signature] = s.split(",");
222
+ return { version, signature };
223
+ })
224
+ .filter((s) => s.version === "v1");
225
+
226
+ if (sigs.length === 0) {
227
+ throw new WebhookVerificationError("No valid v1 signatures found", "NO_VALID_SIGNATURES");
228
+ }
229
+
230
+ const isValid = sigs.some((s) => {
231
+ try {
232
+ return crypto.timingSafeEqual(
233
+ Buffer.from(expected, "base64"),
234
+ Buffer.from(s.signature, "base64")
235
+ );
236
+ } catch {
237
+ return false;
238
+ }
239
+ });
240
+
241
+ if (!isValid) {
242
+ throw new WebhookVerificationError("Signature verification failed", "SIGNATURE_MISMATCH");
243
+ }
244
+
245
+ return true;
246
+ }
247
+
248
+ /**
249
+ * Verify a webhook from a `Request` object (Cloudflare Workers, Deno, etc.).
250
+ * Returns the parsed event payload.
251
+ */
252
+ export async function verifyWebhookRequest<T = WebhookEvent>(
253
+ request: Request,
254
+ secret: string,
255
+ tolerance?: number
256
+ ): Promise<T> {
257
+ const headers = extractWebhookHeaders(request.headers);
258
+ const payload = await request.text();
259
+ verifyWebhookSignature(payload, headers, { secret, tolerance });
260
+ try {
261
+ return JSON.parse(payload) as T;
262
+ } catch {
263
+ throw new WebhookVerificationError("Failed to parse webhook payload as JSON", "INVALID_JSON");
264
+ }
265
+ }
266
+
267
+ /**
268
+ * Verify a webhook from a raw payload + headers (Express.js, Node.js HTTP, etc.).
269
+ * Returns the parsed event payload.
270
+ */
271
+ export function verifyWebhookPayload<T = WebhookEvent>(
272
+ payload: string | Buffer,
273
+ headers: Record<string, string>,
274
+ secret: string,
275
+ tolerance?: number
276
+ ): T {
277
+ const payloadStr = typeof payload === "string" ? payload : payload.toString("utf8");
278
+ const webhookHeaders = extractWebhookHeaders(headers);
279
+ verifyWebhookSignature(payloadStr, webhookHeaders, { secret, tolerance });
280
+ try {
281
+ return JSON.parse(payloadStr) as T;
282
+ } catch {
283
+ throw new WebhookVerificationError("Failed to parse webhook payload as JSON", "INVALID_JSON");
284
+ }
285
+ }
286
+
287
+ // ============================================================================
288
+ // Router
289
+ // ============================================================================
290
+
291
+ /**
292
+ * Routes verified webhook events to registered handler functions.
293
+ *
294
+ * @example
295
+ * ```ts
296
+ * const router = createWebhookRouter({
297
+ * secret: "whsec_...",
298
+ * handlers: {
299
+ * "connection.created": async (event) => { ... },
300
+ * },
301
+ * });
302
+ * ```
303
+ */
304
+ export class WebhookRouter {
305
+ private handlers: Partial<Record<WebhookEventType, WebhookHandler>> = {};
306
+ private defaultHandler?: WebhookHandler;
307
+ private errorHandler?: (error: Error, event?: WebhookEvent) => void;
308
+ private secret: string;
309
+ private tolerance: number;
310
+
311
+ constructor(config: WebhookRouterConfig) {
312
+ this.secret = config.secret;
313
+ this.tolerance = config.tolerance ?? 300;
314
+ if (config.handlers) this.handlers = { ...config.handlers };
315
+ this.defaultHandler = config.defaultHandler;
316
+ this.errorHandler = config.errorHandler;
317
+ }
318
+
319
+ /** Register a handler for a specific event type */
320
+ on<T extends WebhookEventType>(eventType: T, handler: WebhookHandler<T>): this {
321
+ this.handlers[eventType] = handler as unknown as WebhookHandler;
322
+ return this;
323
+ }
324
+
325
+ /** Register a default handler for unmatched event types */
326
+ onDefault(handler: WebhookHandler): this {
327
+ this.defaultHandler = handler;
328
+ return this;
329
+ }
330
+
331
+ /** Register an error handler */
332
+ onError(handler: (error: Error, event?: WebhookEvent) => void): this {
333
+ this.errorHandler = handler;
334
+ return this;
335
+ }
336
+
337
+ /** Handle a `Request` object (Cloudflare Workers, Deno, Bun, Node 18+) */
338
+ async handleRequest(request: Request): Promise<void> {
339
+ try {
340
+ const event = await verifyWebhookRequest<WebhookEvent>(
341
+ request,
342
+ this.secret,
343
+ this.tolerance
344
+ );
345
+ await this.processEvent(event);
346
+ } catch (error) {
347
+ if (this.errorHandler) this.errorHandler(error as Error);
348
+ else throw error;
349
+ }
350
+ }
351
+
352
+ /** Handle a raw payload + headers (Express.js, Fastify, etc.) */
353
+ async handlePayload(
354
+ payload: string | Buffer,
355
+ headers: Record<string, string>
356
+ ): Promise<void> {
357
+ try {
358
+ const event = verifyWebhookPayload<WebhookEvent>(
359
+ payload,
360
+ headers,
361
+ this.secret,
362
+ this.tolerance
363
+ );
364
+ await this.processEvent(event);
365
+ } catch (error) {
366
+ if (this.errorHandler) this.errorHandler(error as Error);
367
+ else throw error;
368
+ }
369
+ }
370
+
371
+ /** Update the webhook secret at runtime */
372
+ updateSecret(secret: string): void {
373
+ this.secret = secret;
374
+ }
375
+
376
+ /** Update the timestamp tolerance at runtime */
377
+ updateTolerance(tolerance: number): void {
378
+ this.tolerance = tolerance;
379
+ }
380
+
381
+ private async processEvent(event: WebhookEvent): Promise<void> {
382
+ try {
383
+ const handler = this.handlers[event.eventType] || this.defaultHandler;
384
+ if (!handler) {
385
+ console.warn(`No handler registered for webhook event type: ${event.eventType}`);
386
+ return;
387
+ }
388
+ await handler(event);
389
+ } catch (error) {
390
+ if (this.errorHandler) this.errorHandler(error as Error, event);
391
+ else throw error;
392
+ }
393
+ }
394
+ }
395
+
396
+ /** Create a new WebhookRouter */
397
+ export function createWebhookRouter(config: WebhookRouterConfig): WebhookRouter {
398
+ return new WebhookRouter(config);
399
+ }
400
+
401
+ // ============================================================================
402
+ // Framework Helpers
403
+ // ============================================================================
404
+
405
+ /**
406
+ * Create a fetch-style handler (Cloudflare Workers, Deno, Bun).
407
+ * Returns a `Response` with appropriate status codes.
408
+ */
409
+ export function createFetchWebhookHandler(
410
+ router: WebhookRouter
411
+ ): (request: Request) => Promise<Response> {
412
+ return async (request: Request): Promise<Response> => {
413
+ try {
414
+ await router.handleRequest(request);
415
+ return new Response("OK", { status: 200 });
416
+ } catch (error) {
417
+ if (isWebhookVerificationError(error)) {
418
+ return new Response(
419
+ JSON.stringify({ error: error.message, code: error.code }),
420
+ { status: 401, headers: { "Content-Type": "application/json" } }
421
+ );
422
+ }
423
+ console.error("Webhook processing error:", error);
424
+ return new Response("Internal server error", { status: 500 });
425
+ }
426
+ };
427
+ }
428
+
429
+ /**
430
+ * Create an Express.js middleware for webhook handling.
431
+ * Expects `express.raw({ type: "application/json" })` to be applied first.
432
+ */
433
+ export function createExpressWebhookMiddleware(router: WebhookRouter): (req: any, res: any, next: any) => Promise<void> {
434
+ return async (req: any, res: any, next: any) => {
435
+ try {
436
+ await router.handlePayload(req.body, req.headers);
437
+ res.status(200).send("OK");
438
+ } catch (error) {
439
+ if (isWebhookVerificationError(error)) {
440
+ res.status(401).json({ error: error.message, code: error.code });
441
+ } else {
442
+ res.status(500).json({ error: "Internal server error" });
443
+ next(error);
444
+ }
445
+ }
446
+ };
447
+ }