@foru-ms/sdk 1.1.1 → 1.2.0

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