@dalcontak/blogger-mcp-server 1.0.1 → 1.0.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/.github/workflows/publish.yml +30 -1
- package/README.md +40 -34
- package/RELEASE.md +64 -32
- package/dist/bloggerService.d.ts +7 -100
- package/dist/bloggerService.js +17 -146
- package/dist/config.d.ts +3 -0
- package/dist/config.js +12 -12
- package/dist/index.d.ts +1 -0
- package/dist/index.js +81 -154
- package/dist/server.d.ts +0 -11
- package/dist/server.js +59 -339
- package/dist/types.d.ts +15 -44
- package/dist/ui-manager.js +8 -16
- package/package.json +4 -1
- package/src/bloggerService.test.ts +5 -1
- package/src/bloggerService.ts +26 -161
- package/src/config.test.ts +34 -20
- package/src/config.ts +17 -16
- package/src/index.ts +117 -194
- package/src/server.test.ts +128 -0
- package/src/server.ts +63 -332
- package/src/types.ts +12 -60
- package/src/ui-manager.ts +17 -26
- package/Dockerfile +0 -64
|
@@ -6,7 +6,7 @@ on:
|
|
|
6
6
|
- 'release/**'
|
|
7
7
|
|
|
8
8
|
permissions:
|
|
9
|
-
contents:
|
|
9
|
+
contents: write # Needed for creating GitHub Releases and Tags
|
|
10
10
|
id-token: write
|
|
11
11
|
|
|
12
12
|
jobs:
|
|
@@ -35,3 +35,32 @@ jobs:
|
|
|
35
35
|
run: npm publish --provenance --access public
|
|
36
36
|
env:
|
|
37
37
|
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
38
|
+
|
|
39
|
+
- name: Extract version from package.json
|
|
40
|
+
id: package-version
|
|
41
|
+
run: echo "version=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT
|
|
42
|
+
|
|
43
|
+
- name: Create GitHub Release
|
|
44
|
+
env:
|
|
45
|
+
GH_TOKEN: ${{ github.token }}
|
|
46
|
+
run: |
|
|
47
|
+
VERSION="v${{ steps.package-version.outputs.version }}"
|
|
48
|
+
|
|
49
|
+
# gh release create will automatically create the tag if it doesn't exist
|
|
50
|
+
# --generate-notes builds an automated changelog from commit history
|
|
51
|
+
gh release create "$VERSION" \
|
|
52
|
+
--title "Release $VERSION" \
|
|
53
|
+
--generate-notes \
|
|
54
|
+
--notes "### 📦 Installation
|
|
55
|
+
Available on npm: [npmjs.com/package/@dalcontak/blogger-mcp-server](https://www.npmjs.com/package/@dalcontak/blogger-mcp-server)
|
|
56
|
+
|
|
57
|
+
You can run the server directly without installing it using \`npx\`:
|
|
58
|
+
\`\`\`bash
|
|
59
|
+
npx -y @dalcontak/blogger-mcp-server
|
|
60
|
+
\`\`\`
|
|
61
|
+
|
|
62
|
+
Or install it globally:
|
|
63
|
+
\`\`\`bash
|
|
64
|
+
npm install -g @dalcontak/blogger-mcp-server
|
|
65
|
+
\`\`\`" \
|
|
66
|
+
--target $GITHUB_SHA
|
package/README.md
CHANGED
|
@@ -37,48 +37,54 @@ npm run build
|
|
|
37
37
|
|
|
38
38
|
### Option 1: API Key (Read-only)
|
|
39
39
|
|
|
40
|
-
Access public blogs only.
|
|
40
|
+
Access public blogs only. Useful if you only need to read data.
|
|
41
41
|
|
|
42
42
|
1. Go to [Google Cloud Console](https://console.cloud.google.com/)
|
|
43
|
-
2. Create/select a project
|
|
44
|
-
3.
|
|
45
|
-
4.
|
|
46
|
-
5. Set the environment variable:
|
|
43
|
+
2. Create/select a project, then enable the **Blogger API v3**.
|
|
44
|
+
3. Create an **API Key** under Credentials.
|
|
45
|
+
4. Set the environment variable: `export BLOGGER_API_KEY=your_api_key_here`
|
|
47
46
|
|
|
48
|
-
|
|
49
|
-
export BLOGGER_API_KEY=your_api_key_here
|
|
50
|
-
```
|
|
47
|
+
### Option 2: OAuth2 (Full Access)
|
|
51
48
|
|
|
52
|
-
|
|
49
|
+
Required to create, update, delete posts, and list your own blogs.
|
|
53
50
|
|
|
54
|
-
|
|
51
|
+
**Need a step-by-step visual guide?**
|
|
52
|
+
🔗 [**Read the complete tutorial on setting up OAuth2 for Blogger MCP here**](https://dalcontk.blogspot.com/2026/03/guia-paso-paso-como-configurar.html)
|
|
53
|
+
*(Note: This guide is written in Spanish. Feel free to use Google Translate if you need it in another language).*
|
|
55
54
|
|
|
56
|
-
|
|
55
|
+
**Step 1: Configure OAuth Consent**
|
|
56
|
+
1. In [Google Cloud Console](https://console.cloud.google.com/), go to **Google Auth Platform** > **Overview**.
|
|
57
|
+
2. Under **Audience**, add your Google account as a Test User.
|
|
58
|
+
3. Under **Data Access** (Scopes), add: `https://www.googleapis.com/auth/blogger`
|
|
57
59
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
7. Scopes: Select **`https://www.googleapis.com/auth/blogger`**
|
|
65
|
-
8. Create credentials and note the **Client ID** and **Client Secret**
|
|
66
|
-
|
|
67
|
-
To obtain a refresh token (one-time setup):
|
|
68
|
-
- Use the [OAuth Playground](https://developers.google.com/oauthplayground/)
|
|
69
|
-
- Select **Blogger API v3**
|
|
70
|
-
- Choose `https://www.googleapis.com/auth/blogger` scope
|
|
71
|
-
- Authorize and copy the **refresh token**
|
|
72
|
-
|
|
73
|
-
Set environment variables:
|
|
60
|
+
**Step 2: Create Web Credentials**
|
|
61
|
+
1. Go to **Credentials** > **Create Credentials** > **OAuth client ID**.
|
|
62
|
+
2. Application type: Select **Web application** *(do not use Desktop app)*.
|
|
63
|
+
3. Name: Your app name.
|
|
64
|
+
4. Authorized redirect URIs: Add exactly `https://developers.google.com/oauthplayground`
|
|
65
|
+
5. Click Create and copy your **Client ID** and **Client Secret**.
|
|
74
66
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
67
|
+
**Step 3: Get a Refresh Token**
|
|
68
|
+
1. Go to the [Google OAuth 2.0 Playground](https://developers.google.com/oauthplayground/).
|
|
69
|
+
2. Click the **Gear icon** (top right) ⚙️ > check **Use your own OAuth credentials**.
|
|
70
|
+
3. Paste your **Client ID** and **Client Secret**, then close the settings panel.
|
|
71
|
+
4. In Step 1 (left panel), scroll to **Blogger API v3**, select `https://www.googleapis.com/auth/blogger`, and click **Authorize APIs**.
|
|
72
|
+
5. Log in with your test Google account and grant permissions.
|
|
73
|
+
6. In Step 2, click **Exchange authorization code for tokens**.
|
|
74
|
+
7. Copy the generated **Refresh token**.
|
|
75
|
+
|
|
76
|
+
**Step 4: Set Environment Variables**
|
|
77
|
+
Configure your MCP client (like Claude Desktop or OpenCode) with:
|
|
78
|
+
|
|
79
|
+
```json
|
|
80
|
+
"env": {
|
|
81
|
+
"GOOGLE_CLIENT_ID": "your_client_id_here",
|
|
82
|
+
"GOOGLE_CLIENT_SECRET": "your_client_secret_here",
|
|
83
|
+
"GOOGLE_REFRESH_TOKEN": "1//your_refresh_token_here"
|
|
84
|
+
}
|
|
79
85
|
```
|
|
80
86
|
|
|
81
|
-
> **Note:** If both
|
|
87
|
+
> **Note:** If both API Key and OAuth2 are configured, OAuth2 is used.
|
|
82
88
|
|
|
83
89
|
## Usage
|
|
84
90
|
|
|
@@ -108,7 +114,7 @@ Create or edit your Claude Desktop config file:
|
|
|
108
114
|
"mcpServers": {
|
|
109
115
|
"blogger": {
|
|
110
116
|
"command": "node",
|
|
111
|
-
"args": ["
|
|
117
|
+
"args": ["./dist/index.js"],
|
|
112
118
|
"env": {
|
|
113
119
|
"BLOGGER_API_KEY": "your_api_key_here"
|
|
114
120
|
}
|
|
@@ -197,7 +203,7 @@ src/
|
|
|
197
203
|
config.ts # Environment-based configuration object
|
|
198
204
|
types.ts # Shared interfaces and type definitions
|
|
199
205
|
ui-manager.ts # Express + Socket.IO web dashboard
|
|
200
|
-
|
|
206
|
+
*.test.ts # Unit tests (Jest) alongside source files
|
|
201
207
|
.github/workflows/ # GitHub Actions CI/CD
|
|
202
208
|
public/ # Static web UI assets (HTML/JS/CSS)
|
|
203
209
|
dist/ # Compiled output
|
package/RELEASE.md
CHANGED
|
@@ -4,28 +4,55 @@ This document explains how to create releases and publish to npm.
|
|
|
4
4
|
|
|
5
5
|
## Prerequisites
|
|
6
6
|
|
|
7
|
-
### Setup Trusted Publishing (
|
|
7
|
+
### Setup GitHub Actions Token + Trusted Publishing (Provenance)
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
We use a **hybrid approach** combining a classic automation token for authentication with OIDC (OpenID Connect) for cryptographic provenance signing.
|
|
10
10
|
|
|
11
|
-
#### Step 1:
|
|
11
|
+
#### Step 1: Create Fine-grained Automation Token
|
|
12
|
+
|
|
13
|
+
1. Log in to npm: `npm login`
|
|
14
|
+
2. Generate an automation token (90-day lifetime):
|
|
15
|
+
```bash
|
|
16
|
+
npm token create --name="github-actions" --scopes="dalcontak" --packages-and-scopes-permission="read-write" --bypass-2fa=true
|
|
17
|
+
```
|
|
18
|
+
3. Copy the token (starts with `npm_`)
|
|
19
|
+
|
|
20
|
+
#### Step 2: Add Token to GitHub Secrets
|
|
21
|
+
|
|
22
|
+
1. Go to your repository: https://github.com/dalcontak/blogger-mcp-server
|
|
23
|
+
2. Navigate to **Settings** > **Secrets and variables** > **Actions**
|
|
24
|
+
3. Click **"New repository secret"**
|
|
25
|
+
4. Name: `NPM_TOKEN`
|
|
26
|
+
5. Value: Paste the token from Step 1
|
|
27
|
+
6. Click **"Add secret"**
|
|
28
|
+
|
|
29
|
+
#### Step 3: Configure npm Package Settings for Automation Tokens
|
|
30
|
+
|
|
31
|
+
1. Go to https://www.npmjs.com/package/@dalcontak/blogger-mcp-server
|
|
32
|
+
2. Click on **"Settings"** tab
|
|
33
|
+
3. In **Publishing access**, change to: **"Require two-factor authentication or a token"** (or similar option that allows automation tokens)
|
|
34
|
+
4. Save the change
|
|
35
|
+
|
|
36
|
+
⚠️ **Important:** If set to "Require 2FA" (without "or a token"), automation tokens will be rejected with error 403.
|
|
37
|
+
|
|
38
|
+
#### Step 4: Configure GitHub Actions as Trusted Publisher (for Provenance)
|
|
39
|
+
|
|
40
|
+
This enables cryptographic signing of published packages (npm provenance).
|
|
12
41
|
|
|
13
|
-
The correct navigation is:
|
|
14
42
|
1. Go to https://www.npmjs.com/package/@dalcontak/blogger-mcp-server
|
|
15
43
|
2. Click on **"Settings"** tab
|
|
16
44
|
3. Find the **"Trusted Publishers"** section
|
|
17
|
-
4. Click on **"
|
|
45
|
+
4. Click on **"Set up connection"**
|
|
18
46
|
5. Fill in:
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
47
|
+
- **Organization or user**: `dalcontak`
|
|
48
|
+
- **Repository**: `dalcontak/blogger-mcp-server`
|
|
49
|
+
- **Workflow filename**: `publish.yml`
|
|
22
50
|
6. Click **"Set up connection"**
|
|
23
51
|
|
|
24
|
-
|
|
25
|
-
-
|
|
26
|
-
-
|
|
27
|
-
|
|
28
|
-
That's it! No token needed — OIDC generates short-lived, job-specific credentials automatically.
|
|
52
|
+
That's it! The workflow will now:
|
|
53
|
+
- Authenticate with your `NPM_TOKEN` secret
|
|
54
|
+
- Sign packages with GitHub Actions provenance
|
|
55
|
+
- Publish to npm securely
|
|
29
56
|
|
|
30
57
|
## Version Naming
|
|
31
58
|
|
|
@@ -76,7 +103,7 @@ GitHub Actions will automatically:
|
|
|
76
103
|
3. ✅ Install dependencies
|
|
77
104
|
4. ✅ Run tests (npm test)
|
|
78
105
|
5. ✅ Build (npm run build)
|
|
79
|
-
6. ✅ Publish to npm using
|
|
106
|
+
6. ✅ Publish to npm using `NPM_TOKEN` + provenance signing
|
|
80
107
|
|
|
81
108
|
The workflow triggers on any push to branches matching `release/**`.
|
|
82
109
|
|
|
@@ -97,29 +124,34 @@ npm install @dalcontak/blogger-mcp-server
|
|
|
97
124
|
- The branch name **must** contain version after "release-" (e.g., `release-1.1.7`)
|
|
98
125
|
- Version format: **3 digits separated by dots** (X.Y.Z)
|
|
99
126
|
- Only pushes to `release/**` branches trigger the build/publish workflow
|
|
100
|
-
- **
|
|
101
|
-
-
|
|
127
|
+
- **NPM_TOKEN secret is required** in GitHub Actions for authentication (automation token)
|
|
128
|
+
- Token lifetime is 90 days maximum — remember to recreate and update secret before expiration
|
|
129
|
+
- Requires Node.js >= 20 (used in workflow)
|
|
102
130
|
|
|
103
131
|
## Troubleshooting
|
|
104
132
|
|
|
105
|
-
### Error: "
|
|
133
|
+
### Error: "Two-factor authentication is required but an automation token was specified"
|
|
134
|
+
|
|
135
|
+
**Solution:** Make sure npm package settings allow automation tokens:
|
|
136
|
+
1. Go to https://www.npmjs.com/package/@dalcontak/blogger-mcp-server > Settings
|
|
137
|
+
2. Change **Publishing access** to: "Require two-factor authentication or a token"
|
|
138
|
+
3. Save and try the workflow again
|
|
139
|
+
|
|
140
|
+
### Error: "E403: You cannot publish over previously published versions"
|
|
106
141
|
|
|
107
|
-
|
|
108
|
-
1. GitHub Actions is configured as **Trusted Publisher** in npm settings
|
|
109
|
-
2. Workflow filename matches: `publish.yml`
|
|
110
|
-
3. Repository is correct: `dalcontak/blogger-mcp-server`
|
|
111
|
-
4. Workflow has `permissions: id-token: write`
|
|
142
|
+
**Solution:** This is normal. You cannot republish an existing version. Bump the version in `package.json` and create a new release branch.
|
|
112
143
|
|
|
113
|
-
### Error: "
|
|
144
|
+
### Error: "E404: Package not in this registry"
|
|
114
145
|
|
|
115
|
-
|
|
146
|
+
**Solution:** Make sure `NPM_TOKEN` is set as a GitHub secret and that the token has write permissions for `@dalcontak` scope.
|
|
116
147
|
|
|
117
|
-
## Comparison:
|
|
148
|
+
## Comparison: Token vs OIDC (Provenance)
|
|
118
149
|
|
|
119
|
-
| Aspect |
|
|
120
|
-
|
|
121
|
-
| **
|
|
122
|
-
| **
|
|
123
|
-
| **
|
|
124
|
-
| **
|
|
125
|
-
| **
|
|
150
|
+
| Aspect | Fine-grained Token | OIDC (Provenance) | Our Hybrid Approach |
|
|
151
|
+
|---------|--------------------|---------------------|-------------------|
|
|
152
|
+
| **Purpose** | Authentication (publish permission) | Cryptographic signing (supply chain security) | Both combined |
|
|
153
|
+
| **Status** | ⚠️ Limited to 90 days | ✅ Standard feature | ✅ Best practice |
|
|
154
|
+
| **Rotation** | Manual (recreate every 90 days) | Automatic (job-specific) | Token manual, OIDC automatic |
|
|
155
|
+
| **Security** | Stored in GitHub secrets | Job-specific, short-lived | Multi-layer security |
|
|
156
|
+
| **GitHub Secret** | Required (NPM_TOKEN) | ❌ Not needed | Required (NPM_TOKEN) |
|
|
157
|
+
| **Setup** | Create via CLI, add to secret | Configure in npmjs.com | Both steps required |
|
package/dist/bloggerService.d.ts
CHANGED
|
@@ -1,121 +1,28 @@
|
|
|
1
1
|
import { blogger_v3 } from 'googleapis';
|
|
2
|
-
import {
|
|
3
|
-
/**
|
|
4
|
-
* Custom types to compensate for Blogger API limitations
|
|
5
|
-
*/
|
|
2
|
+
import { BloggerPost } from './types';
|
|
6
3
|
interface BloggerLabelList {
|
|
7
4
|
kind?: string;
|
|
8
|
-
items?:
|
|
5
|
+
items?: Array<{
|
|
6
|
+
name: string;
|
|
7
|
+
}>;
|
|
9
8
|
}
|
|
10
|
-
/**
|
|
11
|
-
* Google Blogger API interaction service
|
|
12
|
-
*
|
|
13
|
-
* Supports two authentication modes:
|
|
14
|
-
* - OAuth2 (GOOGLE_CLIENT_ID + GOOGLE_CLIENT_SECRET + GOOGLE_REFRESH_TOKEN):
|
|
15
|
-
* full access (read + write). Required for listBlogs, createPost, updatePost, deletePost.
|
|
16
|
-
* - API Key (BLOGGER_API_KEY): read-only access to public blogs.
|
|
17
|
-
* Works for getBlog, listPosts, getPost, searchPosts, listLabels, getLabel.
|
|
18
|
-
*
|
|
19
|
-
* If both are configured, OAuth2 is used (it covers all operations).
|
|
20
|
-
*/
|
|
21
9
|
export declare class BloggerService {
|
|
22
10
|
private blogger;
|
|
23
11
|
private readonly isOAuth2;
|
|
24
|
-
/**
|
|
25
|
-
* Initializes the Blogger service with OAuth2 or API key
|
|
26
|
-
*/
|
|
27
12
|
constructor();
|
|
28
|
-
/**
|
|
29
|
-
* Checks that OAuth2 authentication is available.
|
|
30
|
-
* Throws an explicit error if the operation requires OAuth2 and we are in API key mode.
|
|
31
|
-
*/
|
|
32
13
|
private requireOAuth2;
|
|
33
|
-
/**
|
|
34
|
-
* Lists all blogs for the authenticated user.
|
|
35
|
-
* Requires OAuth2 (blogs.listByUser with userId: 'self').
|
|
36
|
-
* @returns Blog list
|
|
37
|
-
*/
|
|
38
14
|
listBlogs(): Promise<blogger_v3.Schema$BlogList>;
|
|
39
|
-
/**
|
|
40
|
-
* Retrieves details of a specific blog
|
|
41
|
-
* @param blogId ID of the blog to retrieve
|
|
42
|
-
* @returns Blog details
|
|
43
|
-
*/
|
|
44
15
|
getBlog(blogId: string): Promise<blogger_v3.Schema$Blog>;
|
|
45
|
-
/**
|
|
46
|
-
* Retrieves a blog by its URL
|
|
47
|
-
* @param url Blog URL
|
|
48
|
-
* @returns Blog details
|
|
49
|
-
*/
|
|
50
16
|
getBlogByUrl(url: string): Promise<blogger_v3.Schema$Blog>;
|
|
51
|
-
/**
|
|
52
|
-
* Simulates blog creation.
|
|
53
|
-
* Note: The Blogger API does not actually allow creating a blog via API.
|
|
54
|
-
* This method simulates the functionality and returns an explanatory error message.
|
|
55
|
-
*
|
|
56
|
-
* @param blogData Blog data to create
|
|
57
|
-
* @returns Explanatory error message
|
|
58
|
-
*/
|
|
59
|
-
createBlog(blogData: Partial<BloggerBlog>): Promise<any>;
|
|
60
|
-
/**
|
|
61
|
-
* Lists posts from a blog
|
|
62
|
-
* @param blogId Blog ID
|
|
63
|
-
* @param maxResults Maximum number of results to return
|
|
64
|
-
* @returns Post list
|
|
65
|
-
*/
|
|
66
17
|
listPosts(blogId: string, maxResults?: number): Promise<blogger_v3.Schema$PostList>;
|
|
67
|
-
/**
|
|
68
|
-
* Searches posts in a blog using the native posts.search endpoint of the Blogger API
|
|
69
|
-
* @param blogId Blog ID
|
|
70
|
-
* @param query Search term
|
|
71
|
-
* @param maxResults Maximum number of results to return
|
|
72
|
-
* @returns List of matching posts
|
|
73
|
-
*/
|
|
74
18
|
searchPosts(blogId: string, query: string, maxResults?: number): Promise<blogger_v3.Schema$PostList>;
|
|
75
|
-
/**
|
|
76
|
-
* Retrieves a specific post
|
|
77
|
-
* @param blogId Blog ID
|
|
78
|
-
* @param postId Post ID
|
|
79
|
-
* @returns Post details
|
|
80
|
-
*/
|
|
81
19
|
getPost(blogId: string, postId: string): Promise<blogger_v3.Schema$Post>;
|
|
82
|
-
/**
|
|
83
|
-
* Creates a new post in a blog.
|
|
84
|
-
* Requires OAuth2.
|
|
85
|
-
* @param blogId Blog ID
|
|
86
|
-
* @param postData Post data to create
|
|
87
|
-
* @returns Created post
|
|
88
|
-
*/
|
|
89
20
|
createPost(blogId: string, postData: Partial<BloggerPost>): Promise<blogger_v3.Schema$Post>;
|
|
90
|
-
/**
|
|
91
|
-
* Updates an existing post.
|
|
92
|
-
* Requires OAuth2.
|
|
93
|
-
* @param blogId Blog ID
|
|
94
|
-
* @param postId Post ID
|
|
95
|
-
* @param postData Post data to update
|
|
96
|
-
* @returns Updated post
|
|
97
|
-
*/
|
|
98
21
|
updatePost(blogId: string, postId: string, postData: Partial<BloggerPost>): Promise<blogger_v3.Schema$Post>;
|
|
99
|
-
/**
|
|
100
|
-
* Deletes a post.
|
|
101
|
-
* Requires OAuth2.
|
|
102
|
-
* @param blogId Blog ID
|
|
103
|
-
* @param postId Post ID
|
|
104
|
-
* @returns Deletion status
|
|
105
|
-
*/
|
|
106
22
|
deletePost(blogId: string, postId: string): Promise<void>;
|
|
107
|
-
/**
|
|
108
|
-
* Lists labels from a blog
|
|
109
|
-
* @param blogId Blog ID
|
|
110
|
-
* @returns Label list
|
|
111
|
-
*/
|
|
112
23
|
listLabels(blogId: string): Promise<BloggerLabelList>;
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
* @param labelName Label name
|
|
117
|
-
* @returns Label details
|
|
118
|
-
*/
|
|
119
|
-
getLabel(blogId: string, labelName: string): Promise<BloggerLabel>;
|
|
24
|
+
getLabel(blogId: string, labelName: string): Promise<{
|
|
25
|
+
name: string;
|
|
26
|
+
}>;
|
|
120
27
|
}
|
|
121
28
|
export {};
|