@foru-ms/sdk 1.1.1 → 1.2.1

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.
Files changed (47) hide show
  1. package/README.md +293 -11
  2. package/dist/Client.d.ts +63 -4
  3. package/dist/Client.js +124 -22
  4. package/dist/errors.d.ts +52 -0
  5. package/dist/errors.js +95 -0
  6. package/dist/index.d.ts +3 -0
  7. package/dist/index.js +3 -0
  8. package/dist/resources/Integrations.d.ts +7 -0
  9. package/dist/resources/Integrations.js +6 -0
  10. package/dist/resources/Posts.d.ts +12 -0
  11. package/dist/resources/Posts.js +44 -0
  12. package/dist/resources/PrivateMessages.d.ts +4 -0
  13. package/dist/resources/PrivateMessages.js +6 -0
  14. package/dist/resources/SSO.d.ts +10 -0
  15. package/dist/resources/SSO.js +11 -0
  16. package/dist/resources/Tags.d.ts +8 -0
  17. package/dist/resources/Tags.js +24 -0
  18. package/dist/resources/Threads.d.ts +20 -0
  19. package/dist/resources/Threads.js +85 -0
  20. package/dist/resources/Users.d.ts +10 -0
  21. package/dist/resources/Users.js +26 -0
  22. package/dist/resources/Webhooks.d.ts +69 -0
  23. package/dist/resources/Webhooks.js +115 -0
  24. package/dist/response-types.d.ts +105 -0
  25. package/dist/response-types.js +2 -0
  26. package/dist/utils.d.ts +80 -0
  27. package/dist/utils.js +138 -0
  28. package/examples/README.md +38 -0
  29. package/examples/authentication.ts +79 -0
  30. package/examples/error-handling.ts +133 -0
  31. package/examples/managing-threads.ts +130 -0
  32. package/examples/pagination.ts +81 -0
  33. package/examples/webhooks.ts +176 -0
  34. package/package.json +1 -1
  35. package/src/Client.ts +165 -25
  36. package/src/errors.ts +95 -0
  37. package/src/index.ts +3 -0
  38. package/src/resources/Integrations.ts +11 -0
  39. package/src/resources/Posts.ts +56 -0
  40. package/src/resources/PrivateMessages.ts +10 -0
  41. package/src/resources/SSO.ts +17 -0
  42. package/src/resources/Tags.ts +32 -0
  43. package/src/resources/Threads.ts +109 -0
  44. package/src/resources/Users.ts +36 -0
  45. package/src/resources/Webhooks.ts +131 -0
  46. package/src/response-types.ts +113 -0
  47. package/src/utils.ts +182 -0
@@ -75,6 +75,42 @@ export class UsersResource {
75
75
  });
76
76
  }
77
77
 
78
+ async getThreads(id: string, params?: {
79
+ query?: string;
80
+ cursor?: string;
81
+ filter?: 'newest' | 'oldest';
82
+ }): Promise<import('../types').ThreadListResponse> {
83
+ const searchParams = new URLSearchParams();
84
+ if (params) {
85
+ Object.entries(params).forEach(([key, value]) => {
86
+ if (value !== undefined) {
87
+ searchParams.append(key, value as string);
88
+ }
89
+ });
90
+ }
91
+ return this.client.request<import('../types').ThreadListResponse>(`/user/${id}/threads?${searchParams.toString()}`, {
92
+ method: 'GET',
93
+ });
94
+ }
95
+
96
+ async getPosts(id: string, params?: {
97
+ query?: string;
98
+ cursor?: string;
99
+ filter?: 'newest' | 'oldest';
100
+ }): Promise<import('../types').PostListResponse> {
101
+ const searchParams = new URLSearchParams();
102
+ if (params) {
103
+ Object.entries(params).forEach(([key, value]) => {
104
+ if (value !== undefined) {
105
+ searchParams.append(key, value as string);
106
+ }
107
+ });
108
+ }
109
+ return this.client.request<import('../types').PostListResponse>(`/user/${id}/posts?${searchParams.toString()}`, {
110
+ method: 'GET',
111
+ });
112
+ }
113
+
78
114
  async getFollowers(id: string, params?: {
79
115
  query?: string;
80
116
  cursor?: string;
@@ -1,6 +1,9 @@
1
1
  import { ForumClient } from '../Client';
2
2
  import { Webhook, WebhookListResponse } from '../types';
3
3
 
4
+ /**
5
+ * Resource for managing webhooks
6
+ */
4
7
  export class WebhooksResource {
5
8
  private client: ForumClient;
6
9
 
@@ -8,12 +11,21 @@ export class WebhooksResource {
8
11
  this.client = client;
9
12
  }
10
13
 
14
+ /**
15
+ * List all webhooks
16
+ * @returns Promise resolving to list of webhooks
17
+ */
11
18
  async list(): Promise<WebhookListResponse> {
12
19
  return this.client.request<WebhookListResponse>('/webhooks', {
13
20
  method: 'GET',
14
21
  });
15
22
  }
16
23
 
24
+ /**
25
+ * Create a new webhook
26
+ * @param payload - Webhook configuration
27
+ * @returns Promise resolving to created webhook
28
+ */
17
29
  async create(payload: {
18
30
  name: string;
19
31
  url: string;
@@ -25,12 +37,23 @@ export class WebhooksResource {
25
37
  });
26
38
  }
27
39
 
40
+ /**
41
+ * Get a webhook by ID
42
+ * @param id - Webhook ID
43
+ * @returns Promise resolving to webhook details
44
+ */
28
45
  async retrieve(id: string): Promise<{ webhook: Webhook }> {
29
46
  return this.client.request<{ webhook: Webhook }>(`/webhooks/${id}`, {
30
47
  method: 'GET',
31
48
  });
32
49
  }
33
50
 
51
+ /**
52
+ * Update a webhook
53
+ * @param id - Webhook ID
54
+ * @param payload - Updated webhook data
55
+ * @returns Promise resolving to updated webhook
56
+ */
34
57
  async update(id: string, payload: {
35
58
  name?: string;
36
59
  url?: string;
@@ -43,12 +66,23 @@ export class WebhooksResource {
43
66
  });
44
67
  }
45
68
 
69
+ /**
70
+ * Delete a webhook
71
+ * @param id - Webhook ID
72
+ * @returns Promise resolving to deletion confirmation
73
+ */
46
74
  async delete(id: string): Promise<{ success: boolean }> {
47
75
  return this.client.request<{ success: boolean }>(`/webhooks/${id}`, {
48
76
  method: 'DELETE',
49
77
  });
50
78
  }
51
79
 
80
+ /**
81
+ * Get webhook delivery history
82
+ * @param id - Webhook ID
83
+ * @param params - Query parameters
84
+ * @returns Promise resolving to delivery history
85
+ */
52
86
  async getDeliveries(id: string, params?: { cursor?: string }): Promise<{ deliveries: any[]; total: number; nextCursor?: string }> {
53
87
  const searchParams = new URLSearchParams();
54
88
  if (params?.cursor) searchParams.append('cursor', params.cursor);
@@ -57,4 +91,101 @@ export class WebhooksResource {
57
91
  method: 'GET',
58
92
  });
59
93
  }
94
+
95
+ /**
96
+ * Verify webhook signature for security
97
+ * @param payload - Request body (should be the original object, not stringified)
98
+ * @param signature - Signature from X-Webhook-Signature header
99
+ * @param timestamp - Timestamp from X-Webhook-Timestamp header (in milliseconds)
100
+ * @param secret - Webhook secret from creation
101
+ * @param maxAge - Maximum age of webhook in milliseconds (default: 5 minutes)
102
+ * @returns True if signature is valid and timestamp is recent
103
+ * @example
104
+ * ```typescript
105
+ * // In your webhook handler
106
+ * app.post('/webhook', (req, res) => {
107
+ * const signature = req.headers['x-webhook-signature'];
108
+ * const timestamp = req.headers['x-webhook-timestamp'];
109
+ * const event = req.headers['x-webhook-event'];
110
+ *
111
+ * // Verify signature and timestamp
112
+ * const isValid = client.webhooks.verifySignature(
113
+ * req.body,
114
+ * signature,
115
+ * timestamp,
116
+ * 'your_webhook_secret'
117
+ * );
118
+ *
119
+ * if (!isValid) {
120
+ * return res.status(401).send('Invalid signature');
121
+ * }
122
+ *
123
+ * // Process webhook...
124
+ * console.log('Event:', event);
125
+ * res.sendStatus(200);
126
+ * });
127
+ * ```
128
+ */
129
+ verifySignature(
130
+ payload: any,
131
+ signature: string,
132
+ timestamp: string,
133
+ secret: string,
134
+ maxAge: number = 5 * 60 * 1000 // 5 minutes default
135
+ ): boolean {
136
+ try {
137
+ // Verify timestamp (prevent replay attacks)
138
+ const webhookTimestamp = parseInt(timestamp, 10);
139
+ if (isNaN(webhookTimestamp)) {
140
+ console.error('Invalid webhook timestamp format');
141
+ return false;
142
+ }
143
+
144
+ const age = Date.now() - webhookTimestamp;
145
+ if (age > maxAge) {
146
+ console.error('Webhook timestamp too old:', age, 'ms');
147
+ return false;
148
+ }
149
+
150
+ // Use Node.js crypto if available
151
+ if (typeof require !== 'undefined') {
152
+ try {
153
+ const crypto = require('crypto');
154
+
155
+ // Create the signed data: timestamp.payload
156
+ const data = `${timestamp}.${JSON.stringify(payload)}`;
157
+
158
+ // Calculate expected signature
159
+ const expectedSignature = crypto
160
+ .createHmac('sha256', secret)
161
+ .update(data)
162
+ .digest('hex');
163
+
164
+ // Use timing-safe comparison
165
+ if (crypto.timingSafeEqual) {
166
+ return crypto.timingSafeEqual(
167
+ Buffer.from(signature),
168
+ Buffer.from(expectedSignature)
169
+ );
170
+ }
171
+
172
+ // Fallback to simple comparison (less secure)
173
+ return signature === expectedSignature;
174
+
175
+ } catch (error) {
176
+ console.error('Error using Node.js crypto:', error);
177
+ // Fall through to browser implementation
178
+ }
179
+ }
180
+
181
+ // For browser environments or when Node crypto is not available
182
+ console.warn('Webhook signature verification not fully supported in this environment');
183
+ console.warn('Please use Node.js for secure webhook verification');
184
+ return false;
185
+
186
+ } catch (error) {
187
+ console.error('Error verifying webhook signature:', error);
188
+ return false;
189
+ }
190
+ }
60
191
  }
@@ -0,0 +1,113 @@
1
+ import { User } from './types';
2
+
3
+ /**
4
+ * Rate limit information from API responses
5
+ */
6
+ export interface RateLimitInfo {
7
+ /** Maximum number of requests allowed in the time window */
8
+ limit: number;
9
+ /** Number of requests remaining in the current window */
10
+ remaining: number;
11
+ /** Unix timestamp when the rate limit resets */
12
+ reset: number;
13
+ }
14
+
15
+ /**
16
+ * Metadata included with API responses
17
+ */
18
+ export interface ResponseMetadata {
19
+ /** Rate limit information, if available */
20
+ rateLimit?: RateLimitInfo;
21
+ /** Request ID for debugging */
22
+ requestId?: string;
23
+ }
24
+
25
+ /**
26
+ * Generic API response wrapper
27
+ */
28
+ export interface APIResponse<T> {
29
+ /** Response data */
30
+ data: T;
31
+ /** Response metadata */
32
+ meta?: ResponseMetadata;
33
+ }
34
+
35
+ /**
36
+ * Response for interaction lists (likes, upvotes, etc.)
37
+ */
38
+ export interface InteractionListResponse {
39
+ /** List of users who performed the interaction */
40
+ users: User[];
41
+ /** Cursor for next page */
42
+ nextCursor?: string;
43
+ /** Total count of interactions */
44
+ count: number;
45
+ }
46
+
47
+ /**
48
+ * Response for poll data
49
+ */
50
+ export interface PollResponse {
51
+ /** Poll ID */
52
+ id: string;
53
+ /** Poll title */
54
+ title: string;
55
+ /** Poll options */
56
+ options: PollOption[];
57
+ /** Total number of votes */
58
+ totalVotes: number;
59
+ /** Whether the poll allows multiple votes */
60
+ allowMultiple?: boolean;
61
+ /** Poll expiration date */
62
+ expiresAt?: string;
63
+ /** User's vote, if userId was provided */
64
+ userVote?: {
65
+ optionId: string;
66
+ votedAt: string;
67
+ };
68
+ }
69
+
70
+ /**
71
+ * Poll option data
72
+ */
73
+ export interface PollOption {
74
+ /** Option ID */
75
+ id: string;
76
+ /** Option title */
77
+ title: string;
78
+ /** Option color */
79
+ color?: string;
80
+ /** Number of votes for this option */
81
+ votes: number;
82
+ /** Percentage of total votes */
83
+ percentage: number;
84
+ /** Extended data */
85
+ extendedData?: Record<string, any>;
86
+ }
87
+
88
+ /**
89
+ * Response for batch operations
90
+ */
91
+ export interface BatchOperationResponse {
92
+ /** Number of items successfully processed */
93
+ success: number;
94
+ /** Number of items that failed */
95
+ failed: number;
96
+ /** Error details for failed items */
97
+ errors?: Array<{
98
+ id: string;
99
+ error: string;
100
+ }>;
101
+ }
102
+
103
+ /**
104
+ * Pagination cursor response
105
+ */
106
+ export interface CursorPagination<T> {
107
+ /** Array of items */
108
+ items: T[];
109
+ /** Cursor for next page */
110
+ nextCursor?: string;
111
+ /** Whether there are more items */
112
+ hasMore: boolean;
113
+ }
package/src/utils.ts ADDED
@@ -0,0 +1,182 @@
1
+ import { CursorPagination } from './response-types';
2
+
3
+ /**
4
+ * Pagination utilities for working with cursor-based pagination
5
+ */
6
+ export class PaginationHelper {
7
+ /**
8
+ * Async generator to auto-paginate through all results
9
+ * @param fetchPage - Function to fetch a page of results
10
+ * @yields Individual items from each page
11
+ * @example
12
+ * ```typescript
13
+ * const helper = new PaginationHelper();
14
+ * for await (const thread of helper.paginateAll((cursor) =>
15
+ * client.threads.list({ cursor })
16
+ * )) {
17
+ * console.log(thread);
18
+ * }
19
+ * ```
20
+ */
21
+ async *paginateAll<T>(
22
+ fetchPage: (cursor?: string) => Promise<{
23
+ threads?: T[];
24
+ posts?: T[];
25
+ users?: T[];
26
+ tags?: T[];
27
+ nextThreadCursor?: string;
28
+ nextPostCursor?: string;
29
+ nextUserCursor?: string;
30
+ nextTagCursor?: string;
31
+ }>
32
+ ): AsyncIterableIterator<T> {
33
+ let cursor: string | undefined;
34
+ let hasMore = true;
35
+
36
+ while (hasMore) {
37
+ const response = await fetchPage(cursor);
38
+
39
+ // Determine which array and cursor to use
40
+ const items = response.threads || response.posts || response.users || response.tags || [];
41
+ const nextCursor = response.nextThreadCursor || response.nextPostCursor ||
42
+ response.nextUserCursor || response.nextTagCursor;
43
+
44
+ for (const item of items) {
45
+ yield item;
46
+ }
47
+
48
+ cursor = nextCursor;
49
+ hasMore = !!nextCursor;
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Fetch all items from a paginated endpoint
55
+ * @param fetchPage - Function to fetch a page of results
56
+ * @param maxPages - Maximum number of pages to fetch (default: Infinity)
57
+ * @returns Array of all items
58
+ */
59
+ async fetchAllPages<T>(
60
+ fetchPage: (cursor?: string) => Promise<{
61
+ threads?: T[];
62
+ posts?: T[];
63
+ users?: T[];
64
+ tags?: T[];
65
+ nextThreadCursor?: string;
66
+ nextPostCursor?: string;
67
+ nextUserCursor?: string;
68
+ nextTagCursor?: string;
69
+ }>,
70
+ maxPages: number = Infinity
71
+ ): Promise<T[]> {
72
+ const allItems: T[] = [];
73
+ let cursor: string | undefined;
74
+ let pageCount = 0;
75
+
76
+ while (pageCount < maxPages) {
77
+ const response = await fetchPage(cursor);
78
+
79
+ const items = response.threads || response.posts || response.users || response.tags || [];
80
+ const nextCursor = response.nextThreadCursor || response.nextPostCursor ||
81
+ response.nextUserCursor || response.nextTagCursor;
82
+
83
+ allItems.push(...items);
84
+
85
+ cursor = nextCursor;
86
+ pageCount++;
87
+
88
+ if (!nextCursor) break;
89
+ }
90
+
91
+ return allItems;
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Input validation utilities
97
+ */
98
+ export class Validator {
99
+ /**
100
+ * Validate that a string is not empty
101
+ */
102
+ static isNonEmptyString(value: any, fieldName: string): void {
103
+ if (typeof value !== 'string' || value.trim().length === 0) {
104
+ throw new Error(`${fieldName} must be a non-empty string`);
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Validate email format
110
+ */
111
+ static isValidEmail(email: string): boolean {
112
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
113
+ return emailRegex.test(email);
114
+ }
115
+
116
+ /**
117
+ * Validate that a value is one of the allowed options
118
+ */
119
+ static isOneOf<T>(value: T, options: T[], fieldName: string): void {
120
+ if (!options.includes(value)) {
121
+ throw new Error(`${fieldName} must be one of: ${options.join(', ')}`);
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Validate cursor format (if needed)
127
+ */
128
+ static isValidCursor(cursor: string): boolean {
129
+ return typeof cursor === 'string' && cursor.length > 0;
130
+ }
131
+ }
132
+
133
+ /**
134
+ * Retry utilities for handling transient failures
135
+ */
136
+ export class RetryHelper {
137
+ /**
138
+ * Execute a function with exponential backoff retry
139
+ * @param fn - Function to execute
140
+ * @param maxRetries - Maximum number of retry attempts
141
+ * @param initialDelay - Initial delay in milliseconds
142
+ * @returns Result of the function
143
+ */
144
+ static async withRetry<T>(
145
+ fn: () => Promise<T>,
146
+ maxRetries: number = 3,
147
+ initialDelay: number = 1000
148
+ ): Promise<T> {
149
+ let lastError: Error;
150
+
151
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
152
+ try {
153
+ return await fn();
154
+ } catch (error: any) {
155
+ lastError = error;
156
+
157
+ // Don't retry on client errors (4xx) except 429
158
+ if (error.statusCode >= 400 && error.statusCode < 500 && error.statusCode !== 429) {
159
+ throw error;
160
+ }
161
+
162
+ // Don't retry on last attempt
163
+ if (attempt === maxRetries) {
164
+ break;
165
+ }
166
+
167
+ // Calculate delay with exponential backoff
168
+ const delay = error.retryAfter
169
+ ? error.retryAfter * 1000
170
+ : initialDelay * Math.pow(2, attempt);
171
+
172
+ await this.sleep(delay);
173
+ }
174
+ }
175
+
176
+ throw lastError!;
177
+ }
178
+
179
+ private static sleep(ms: number): Promise<void> {
180
+ return new Promise(resolve => setTimeout(resolve, ms));
181
+ }
182
+ }