@crosspost/sdk 0.1.2 → 0.1.4

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Open Crosspost
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -2,273 +2,376 @@
2
2
 
3
3
  SDK for interacting with the Crosspost API.
4
4
 
5
- ## Overview
6
-
7
- This package provides a client for interacting with the Crosspost API, allowing you to easily
8
- integrate social media posting capabilities into your applications. The SDK is designed to be
9
- flexible and easy to use, with support for multiple authentication methods and platforms.
10
-
11
- ## Features
12
-
13
- - Unified client for all supported platforms
14
- - Flexible authentication:
15
- - Direct `nearAuthData` injection
16
- - Automatic cookie-based authentication (`__crosspost_auth`)
17
- - Explicit authentication via `setAuthentication` method
18
- - Platform-specific clients (Twitter, with more to come)
19
- - Type-safe request/response handling using `@crosspost/types`
20
- - Comprehensive error handling with specific `ApiError` and `PlatformError` types
21
- - CSRF protection via Double Submit Cookie pattern
22
-
23
5
  ## Installation
24
6
 
25
7
  ```bash
26
8
  bun install @crosspost/sdk
27
9
  ```
28
10
 
29
- ## Usage
30
-
31
- ### Initializing the SDK
32
-
33
- The SDK can be initialized in several ways depending on how you manage authentication:
34
-
35
- **1. Cookie-Based Authentication (Recommended for Browsers)**
36
-
37
- If the user has previously authenticated via the API, the SDK can automatically use the
38
- authentication data stored in the `__crosspost_auth` cookie.
11
+ ## Quick Start
39
12
 
40
13
  ```typescript
41
- import { CrosspostClient } from '@crosspost/sdk';
14
+ import { ApiError, CrosspostClient, isAuthError, PlatformError } from '@crosspost/sdk';
42
15
 
43
- // Client will automatically try to load auth from the cookie
16
+ // Initialize the client (authentication can be provided later)
44
17
  const client = new CrosspostClient({
45
18
  baseUrl: 'https://your-crosspost-api.com', // Optional: Defaults to official API
46
19
  });
47
20
 
48
- // If the cookie exists and is valid, requests will be authenticated.
49
- // If not, authenticated requests will throw an ApiError.
50
- ```
21
+ // Set authentication with fresh signature for the request
22
+ client.setAuthentication({
23
+ accountId: 'your-account.near',
24
+ publicKey: 'ed25519:...',
25
+ signature: '...',
26
+ message: '...',
27
+ });
28
+
29
+ // Check if client has authentication data set
30
+ if (client.isAuthenticated()) {
31
+ console.log('Client has authentication data');
32
+ }
51
33
 
52
- The cookie is stored with secure attributes:
34
+ // NEAR Account Authorization
35
+ async function authorizeNearAccount() {
36
+ try {
37
+ // Authorize with NEAR account
38
+ const authResponse = await client.auth.authorizeNearAccount();
39
+ console.log('NEAR authorization successful');
40
+ console.log('Account ID:', authResponse.accountId);
41
+ console.log('Status:', authResponse.status);
42
+ console.log('Connected platforms:', authResponse.connectedPlatforms);
43
+ return true;
44
+ } catch (error) {
45
+ console.error('NEAR authorization failed');
46
+ if (error instanceof ApiError) {
47
+ console.error('Error code:', error.code);
48
+ console.error('Status:', error.status);
49
+ console.error('Details:', error.details);
50
+ console.error('Recoverable:', error.recoverable);
51
+ }
52
+ return false;
53
+ }
54
+ }
55
+
56
+ // Unauthorize NEAR Account
57
+ async function unauthorizeNearAccount() {
58
+ try {
59
+ // Unauthorize NEAR account (removes all platform connections)
60
+ const response = await client.auth.unauthorizeNearAccount();
61
+ console.log('NEAR account unauthorized');
62
+ console.log('Status:', response.status);
63
+ console.log('Message:', response.message);
64
+ return true;
65
+ } catch (error) {
66
+ console.error('Failed to unauthorize NEAR account');
67
+ if (error instanceof ApiError) {
68
+ console.error('Error code:', error.code);
69
+ console.error('Status:', error.status);
70
+ console.error('Details:', error.details);
71
+ }
72
+ return false;
73
+ }
74
+ }
75
+
76
+ // Revoke Platform Authorization
77
+ async function revokePlatformAuth(platform) {
78
+ try {
79
+ // Revoke specific platform authorization
80
+ const response = await client.auth.revokeAuth(platform);
81
+ console.log(`${platform} authorization revoked`);
82
+ console.log('Status:', response.status);
83
+ console.log('Platform:', response.platform);
84
+ console.log('Message:', response.message);
85
+ return true;
86
+ } catch (error) {
87
+ console.error(`Failed to revoke ${platform} authorization`);
88
+ if (error instanceof ApiError) {
89
+ console.error('Error code:', error.code);
90
+ console.error('Status:', error.status);
91
+ console.error('Details:', error.details);
92
+ }
93
+ return false;
94
+ }
95
+ }
53
96
 
54
- - `secure: true` - Only sent over HTTPS
55
- - `sameSite: 'lax'` - Provides CSRF protection while allowing top-level navigation
56
- - `path: '/'` - Available across the entire domain
57
- - `expires: 30 days` - Persists for 30 days
97
+ // Example usage
98
+ async function createPost() {
99
+ try {
100
+ const response = await client.post.createPost({
101
+ targets: [{ platform: 'twitter', userId: 'your-twitter-id' }],
102
+ content: {
103
+ text: 'Hello from Crosspost SDK!',
104
+ },
105
+ });
106
+ console.log('Post created successfully');
107
+ console.log('Post ID:', response.id);
108
+ console.log('Platform:', response.platform);
109
+ console.log('URL:', response.url);
110
+ console.log('Created at:', response.createdAt);
111
+ } catch (error) {
112
+ // Check if it's an authentication error
113
+ if (isAuthError(error)) {
114
+ console.error('Authentication required. Attempting to authorize...');
115
+ const authorized = await authorizeNearAccount();
116
+ if (authorized) {
117
+ // Retry the operation
118
+ return createPost();
119
+ }
120
+ } else {
121
+ // Handle other error types
122
+ console.error('Error creating post:', error);
123
+ if (error instanceof ApiError) {
124
+ console.error('Error code:', error.code);
125
+ console.error('Status:', error.status);
126
+ console.error('Details:', error.details);
127
+ console.error('Recoverable:', error.recoverable);
128
+ } else if (error instanceof PlatformError) {
129
+ console.error('Platform:', error.platform);
130
+ console.error('Error code:', error.code);
131
+ console.error('Original error:', error.originalError);
132
+ }
133
+ }
134
+ }
135
+ }
136
+ ```
58
137
 
59
- **2. Direct Authentication**
138
+ ## API Reference
60
139
 
61
- Provide the `nearAuthData` object directly if you have obtained it through other means (e.g.,
62
- server-side flow, manual signing).
140
+ ### CrosspostClient
63
141
 
64
142
  ```typescript
65
- import { CrosspostClient } from '@crosspost/sdk';
66
- import type { NearAuthData } from 'near-sign-verify'; // Assuming this type exists
143
+ constructor(config?: {
144
+ baseUrl?: string;
145
+ nearAuthData?: NearAuthData;
146
+ timeout?: number;
147
+ retries?: number;
148
+ })
149
+ ```
67
150
 
68
- const nearAuthData: NearAuthData = {
69
- account_id: 'example.near',
70
- public_key: 'ed25519:...',
71
- signature: '...',
72
- message: '...',
73
- nonce: '...',
74
- recipient: 'crosspost-api.near',
75
- // callback_url and state are optional
76
- };
151
+ #### Methods
77
152
 
78
- const client = new CrosspostClient({
79
- baseUrl: 'https://your-crosspost-api.com',
80
- nearAuthData: nearAuthData,
81
- });
82
- ```
153
+ - `setAuthentication(nearAuthData: NearAuthData): Promise<void>` - Sets authentication data
154
+ - `isAuthenticated(): boolean` - Checks if client is authenticated
155
+
156
+ ### Auth API (client.auth)
157
+
158
+ - `authorizeNearAccount(): Promise<NearAuthorizationResponse>` - Authorizes NEAR account
159
+ - `unauthorizeNearAccount(): Promise<NearAuthorizationResponse>` - Unauthorizes NEAR account
160
+ - `getNearAuthorizationStatus(): Promise<NearAuthorizationResponse>` - Checks authorization status
161
+ - `loginToPlatform(platform, options?): Promise<EnhancedApiResponse<any>>` - Initiates OAuth flow
162
+ - `refreshToken(platform): Promise<EnhancedApiResponse<any>>` - Refreshes platform token
163
+ - `refreshProfile(platform): Promise<EnhancedApiResponse<any>>` - Refreshes user profile
164
+ - `getAuthStatus(platform): Promise<AuthStatusResponse>` - Gets authentication status
165
+ - `revokeAuth(platform): Promise<AuthRevokeResponse>` - Revokes platform access
166
+ - `getConnectedAccounts(): Promise<ConnectedAccountsResponse>` - Lists connected accounts
167
+
168
+ ### Post API (client.post)
169
+
170
+ Each post operation accepts a request object that includes:
171
+
172
+ - `targets`: Array of `{ platform: string, userId: string }` specifying where to perform the action
173
+ - Additional parameters specific to each operation
174
+
175
+ Available methods:
176
+
177
+ - `createPost(request: CreatePostRequest): Promise<CreatePostResponse>` - Creates posts on specified
178
+ platforms
179
+ - `repost(request: RepostRequest): Promise<RepostResponse>` - Reposts an existing post
180
+ - `quotePost(request: QuotePostRequest): Promise<QuotePostResponse>` - Quotes an existing post
181
+ - `replyToPost(request: ReplyToPostRequest): Promise<ReplyToPostResponse>` - Replies to a post
182
+ - `likePost(request: LikePostRequest): Promise<LikePostResponse>` - Likes a post
183
+ - `unlikePost(request: UnlikePostRequest): Promise<UnlikePostResponse>` - Unlikes a post
184
+ - `deletePost(request: DeletePostRequest): Promise<DeletePostResponse>` - Deletes posts
185
+
186
+ ### Activity API (client.activity)
83
187
 
84
- **3. Explicit Authentication**
188
+ - `getLeaderboard(options): Promise<LeaderboardResponse>` - Gets activity leaderboard
189
+ - `getAccountActivity(signerId, options): Promise<AccountActivityResponse>` - Gets account activity
190
+ - `getAccountPosts(signerId, options): Promise<AccountPostsResponse>` - Gets account posts
85
191
 
86
- Initialize the client without authentication and set it later, for example, after a user logs in via
87
- a NEAR wallet connection. This method also stores the authentication data in the `__crosspost_auth`
88
- cookie for future use.
192
+ ### System API (client.system)
193
+
194
+ - `getRateLimits(): Promise<RateLimitsResponse>` - Gets all rate limits
195
+ - `getEndpointRateLimit(endpoint): Promise<EndpointRateLimitResponse>` - Gets endpoint rate limit
196
+ - `getHealthStatus(): Promise<HealthStatusResponse>` - Gets API health status
197
+
198
+ ### Error Handling Utilities
89
199
 
90
200
  ```typescript
91
- import { CrosspostClient } from '@crosspost/sdk';
92
- import type { NearAuthData } from 'near-sign-verify';
201
+ import {
202
+ apiWrapper,
203
+ enrichErrorWithContext,
204
+ getErrorDetails,
205
+ getErrorMessage,
206
+ isAuthError,
207
+ isContentError,
208
+ isMediaError,
209
+ isNetworkError,
210
+ isPlatformError,
211
+ isPostError,
212
+ isRateLimitError,
213
+ isRecoverableError,
214
+ isValidationError,
215
+ } from '@crosspost/sdk';
216
+
217
+ // Check error types
218
+ if (isAuthError(error)) {
219
+ // Handle authentication errors
220
+ }
93
221
 
94
- const client = new CrosspostClient({
95
- baseUrl: 'https://your-crosspost-api.com',
222
+ // Get user-friendly error message
223
+ const message = getErrorMessage(error, 'Default message');
224
+
225
+ // Get error details
226
+ const details = getErrorDetails(error);
227
+
228
+ // Add context to errors
229
+ const enrichedError = enrichErrorWithContext(error, {
230
+ operation: 'createPost',
231
+ timestamp: Date.now(),
96
232
  });
97
233
 
98
- // Later, after obtaining the signature...
99
- async function handleAuthentication(nearAuthData: NearAuthData) {
100
- try {
101
- // This sets the auth data in the client and stores it in the cookie
102
- await client.setAuthentication(nearAuthData);
103
- console.log('Authentication successful and stored.');
104
- // Client is now ready for authenticated requests
105
- } catch (error) {
106
- console.error('Authentication failed:', error);
107
- }
108
- }
234
+ // Wrap API calls with error handling
235
+ const result = await apiWrapper(
236
+ async () => {
237
+ // API call implementation
238
+ return await fetch('/api/endpoint');
239
+ },
240
+ { operation: 'fetchData' }, // Optional context
241
+ );
109
242
  ```
110
243
 
111
- ### Making Authenticated Requests (Example: Twitter Client)
244
+ ## Usage Examples
245
+
246
+ ### Creating a Post
112
247
 
113
248
  ```typescript
114
- // Create a post
115
- const createPostResponse = await client.twitter.createPost({
116
- content: {
249
+ // Create a text post on Twitter
250
+ const textPostResponse = await client.post.createPost({
251
+ targets: [{
252
+ platform: 'twitter',
253
+ userId: 'your-twitter-id',
254
+ }],
255
+ content: [{
117
256
  text: 'Hello from Crosspost SDK!',
118
- },
257
+ }],
119
258
  });
120
259
 
121
- console.log(`Post created with ID: ${createPostResponse.id}`);
122
-
123
- // Create a post with media
124
- const createPostWithMediaResponse = await client.twitter.createPost({
125
- content: {
260
+ // Create a post with media on multiple platforms
261
+ const mediaPostResponse = await client.post.createPost({
262
+ targets: [
263
+ { platform: 'twitter', userId: 'your-twitter-id' },
264
+ { platform: 'facebook', userId: 'your-facebook-id' },
265
+ ],
266
+ content: [{
126
267
  text: 'Check out this image!',
127
- media: [
128
- {
129
- type: 'image',
130
- url: 'https://example.com/image.jpg',
131
- },
132
- ],
133
- },
268
+ media: [{
269
+ data: imageBlob,
270
+ mimeType: 'image/jpeg',
271
+ altText: 'A beautiful sunset',
272
+ }],
273
+ }],
134
274
  });
275
+ ```
135
276
 
136
- // Like a post
137
- await client.twitter.likePost({
277
+ ### Post Interactions
278
+
279
+ ```typescript
280
+ // Like a post on Twitter
281
+ await client.post.likePost({
282
+ targets: [{
283
+ platform: 'twitter',
284
+ userId: 'your-twitter-id',
285
+ }],
286
+ platform: 'twitter',
138
287
  postId: '1234567890',
139
288
  });
140
289
 
141
- // Repost a post
142
- await client.twitter.repost({
290
+ // Repost on multiple platforms
291
+ await client.post.repost({
292
+ targets: [
293
+ { platform: 'twitter', userId: 'your-twitter-id' },
294
+ { platform: 'facebook', userId: 'your-facebook-id' },
295
+ ],
296
+ platform: 'twitter',
143
297
  postId: '1234567890',
144
298
  });
145
299
 
146
300
  // Reply to a post
147
- await client.twitter.reply({
301
+ await client.post.replyToPost({
302
+ targets: [{
303
+ platform: 'twitter',
304
+ userId: 'your-twitter-id',
305
+ }],
306
+ platform: 'twitter',
148
307
  postId: '1234567890',
149
- content: {
308
+ content: [{
150
309
  text: 'This is a reply!',
151
- },
310
+ }],
152
311
  });
153
312
 
154
- // Delete a post
155
- await client.twitter.deletePost({
156
- postId: '1234567890',
313
+ // Delete posts
314
+ await client.post.deletePost({
315
+ targets: [{
316
+ platform: 'twitter',
317
+ userId: 'your-twitter-id',
318
+ }],
319
+ posts: [{
320
+ platform: 'twitter',
321
+ userId: 'your-twitter-id',
322
+ postId: '1234567890',
323
+ }],
157
324
  });
158
325
  ```
159
326
 
160
- ### Error Handling
327
+ ### Getting Activity Data
161
328
 
162
329
  ```typescript
163
- import { ApiError, ApiErrorCode, CrosspostClient, PlatformError } from '@crosspost/sdk';
164
-
165
- try {
166
- // Ensure client is authenticated (either via cookie or direct data)
167
- const response = await client.post.createPost({ // Assuming a generic post API exists
168
- targets: [{ platform: 'twitter', userId: 'twitter_user_id' }], // Example target
169
- content: {
170
- text: 'Hello from Crosspost SDK!',
171
- },
172
- });
173
-
174
- console.log(`Post created with ID: ${response.id}`);
175
- } catch (error) {
176
- if (error instanceof ApiError) {
177
- // Handle authentication errors
178
- if (error.code === ApiErrorCode.UNAUTHORIZED) {
179
- console.error('Authentication required. Please sign in with your NEAR wallet.');
180
- // Redirect to authentication flow or show login UI
181
- } else {
182
- console.error(`API Error: ${error.message}`);
183
- console.error(`Error Code: ${error.code}`); // e.g., RATE_LIMITED
184
- console.error(`Status: ${error.status}`); // HTTP status code
185
- console.error(`Details:`, error.details); // Additional context
186
- console.error(`Recoverable: ${error.recoverable}`);
187
- }
188
- } else if (error instanceof PlatformError) {
189
- // Handle errors specific to a platform (e.g., Twitter API error)
190
- console.error(`Platform Error (${error.platform}): ${error.message}`);
191
- console.error(`Original Error:`, error.originalError);
192
- } else {
193
- console.error(`Unexpected error:`, error);
194
- }
195
- }
196
- ```
197
-
198
- ## API Reference
199
-
200
- ### `CrosspostClient`
201
-
202
- The main client for interacting with the Crosspost API.
330
+ // Get leaderboard
331
+ const leaderboard = await client.activity.getLeaderboard({
332
+ timeframe: 'week',
333
+ limit: 10,
334
+ });
203
335
 
204
- #### Constructor
336
+ // Get account activity
337
+ const activity = await client.activity.getAccountActivity('user.near', {
338
+ timeframe: 'month',
339
+ });
205
340
 
206
- ```typescript
207
- constructor(config?: CrosspostClientConfig)
341
+ // Get account posts
342
+ const posts = await client.activity.getAccountPosts('user.near', {
343
+ limit: 20,
344
+ offset: 0,
345
+ });
208
346
  ```
209
347
 
210
- `CrosspostClientConfig` Options:
211
-
212
- - `baseUrl?: string`: Base URL of the Crosspost API. Defaults to the official endpoint.
213
- - `nearAuthData?: NearAuthData`: NEAR authentication data object. If not provided, the client
214
- attempts to load from the `__crosspost_auth` cookie.
215
- - `timeout?: number`: Request timeout in milliseconds (default: 30000).
216
- - `retries?: number`: Number of retries for failed requests (network/5xx errors) (default: 2).
217
-
218
- #### Methods
219
-
220
- - `setAuthentication(nearAuthData: NearAuthData): Promise<void>`: Sets the provided `NearAuthData`
221
- in the client and stores it in the `__crosspost_auth` cookie for future use.
222
-
223
- #### Properties
224
-
225
- - `auth`: Instance of `AuthApi` for authentication-related operations.
226
- - `post`: Instance of `PostApi` for post-related operations.
227
-
228
- ### API Modules
348
+ ### Checking Rate Limits
229
349
 
230
- #### `AuthApi` (`client.auth`)
231
-
232
- - `authorizeNearAccount(): Promise<NearAuthorizationResponse>`: Authorizes the current NEAR account
233
- (requires `nearAuthData` to be set).
234
- - `getNearAuthorizationStatus(): Promise<NearAuthorizationResponse>`: Checks if the current NEAR
235
- account is authorized.
236
- - `loginToPlatform(platform, options?): Promise<EnhancedApiResponse<any>>`: Initiates the OAuth
237
- login flow for a platform.
238
- - `refreshToken(platform): Promise<EnhancedApiResponse<any>>`: Refreshes the platform token.
239
- - `refreshProfile(platform): Promise<EnhancedApiResponse<any>>`: Refreshes the user's profile from
240
- the platform.
241
- - `getAuthStatus(platform): Promise<AuthStatusResponse>`: Gets the authentication status for a
242
- specific platform.
243
- - `revokeAuth(platform): Promise<AuthRevokeResponse>`: Revokes access for a specific platform.
244
- - `getConnectedAccounts(): Promise<ConnectedAccountsResponse>`: Lists all platform accounts
245
- connected to the NEAR account.
246
-
247
- #### `PostApi` (`client.post`)
248
-
249
- - `createPost(request: CreatePostRequest): Promise<CreatePostResponse>`: Creates a new post.
250
- - `repost(request: RepostRequest): Promise<RepostResponse>`: Reposts an existing post.
251
- - `quotePost(request: QuotePostRequest): Promise<QuotePostResponse>`: Quotes an existing post.
252
- - `replyToPost(request: ReplyToPostRequest): Promise<ReplyToPostResponse>`: Replies to an existing
253
- post.
254
- - `likePost(request: LikePostRequest): Promise<LikePostResponse>`: Likes a post.
255
- - `unlikePost(request: UnlikePostRequest): Promise<UnlikePostResponse>`: Unlikes a post.
256
- - `deletePost(request: DeletePostRequest): Promise<DeletePostResponse>`: Deletes one or more posts.
257
-
258
- ### CSRF Protection
350
+ ```typescript
351
+ // Get all rate limits
352
+ const rateLimits = await client.system.getRateLimits();
259
353
 
260
- The SDK supports the Double Submit Cookie pattern for CSRF protection:
354
+ // Get rate limit for a specific endpoint
355
+ const postRateLimit = await client.system.getEndpointRateLimit('post');
356
+ ```
261
357
 
262
- 1. The backend API sets a CSRF token in a non-HttpOnly cookie named `XSRF-TOKEN`
263
- 2. The SDK automatically reads this token and includes it in the `X-CSRF-Token` header for all
264
- state-changing requests (non-GET)
265
- 3. The backend API validates that the token in the header matches the token in the cookie
358
+ ## Authentication and Security
266
359
 
267
- This protection is automatically enabled when the backend API is configured to use CSRF tokens.
360
+ ### Authentication Strategy
268
361
 
269
- _(Note: Specific platform clients like `client.twitter` might be deprecated in favor of using the
270
- generic `client.post` API with platform targets specified in the request body.)_
362
+ The SDK uses direct authentication with per-request signatures:
271
363
 
272
- ## License
364
+ ```typescript
365
+ // Initialize the client
366
+ const client = new CrosspostClient({
367
+ baseUrl: 'https://your-crosspost-api.com',
368
+ });
273
369
 
274
- MIT
370
+ // Before making authenticated requests, set fresh signature
371
+ client.setAuthentication({
372
+ accountId: 'your-account.near',
373
+ publicKey: 'ed25519:...',
374
+ signature: '...',
375
+ message: '...',
376
+ });
377
+ ```