@ldraney/mcp-linkedin 0.1.2 → 0.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.
- package/.claude/settings.local.json +2 -1
- package/.mcpbignore +33 -0
- package/README.md +91 -111
- package/manifest.json +4 -25
- package/oauth-relay/Dockerfile +12 -0
- package/oauth-relay/fly.toml +17 -0
- package/oauth-relay/index.js +151 -0
- package/oauth-relay/package-lock.json +830 -0
- package/oauth-relay/package.json +15 -0
- package/package.json +1 -2
- package/src/database.js +126 -123
- package/src/index.js +43 -1
- package/src/tools.js +81 -13
package/.mcpbignore
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Development/test files
|
|
2
|
+
__tests__/
|
|
3
|
+
*.test.js
|
|
4
|
+
jest.config.js
|
|
5
|
+
.env
|
|
6
|
+
.env.example
|
|
7
|
+
|
|
8
|
+
# Documentation
|
|
9
|
+
*.md
|
|
10
|
+
!README.md
|
|
11
|
+
|
|
12
|
+
# Git
|
|
13
|
+
.git/
|
|
14
|
+
.gitignore
|
|
15
|
+
|
|
16
|
+
# IDE
|
|
17
|
+
.vscode/
|
|
18
|
+
.idea/
|
|
19
|
+
|
|
20
|
+
# Claude dev files
|
|
21
|
+
.claude/
|
|
22
|
+
|
|
23
|
+
# OAuth relay (separate deployment)
|
|
24
|
+
oauth-relay/
|
|
25
|
+
|
|
26
|
+
# Old database files
|
|
27
|
+
*.db
|
|
28
|
+
scheduled_posts.db
|
|
29
|
+
|
|
30
|
+
# Other
|
|
31
|
+
test_post.js
|
|
32
|
+
todos.db
|
|
33
|
+
*.html
|
package/README.md
CHANGED
|
@@ -1,135 +1,115 @@
|
|
|
1
1
|
# mcp-linkedin
|
|
2
2
|
|
|
3
|
-
MCP server for managing LinkedIn posts - create,
|
|
3
|
+
MCP server for managing LinkedIn posts - create text, image, video, document, poll, and multi-image posts. Schedule posts, add comments and reactions.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Installation
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
### MCPB Bundle (One-Click)
|
|
8
|
+
Download from [latest release](https://github.com/intelligent-staffing-systems/mcp-linkedin/releases/latest) and open `mcp-linkedin.mcpb` with Claude Desktop.
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
As a technical professional, I want to:
|
|
12
|
-
- Create LinkedIn posts that link to my GitHub projects
|
|
13
|
-
- Update existing posts to reflect project changes
|
|
14
|
-
- Manage my LinkedIn content programmatically
|
|
15
|
-
- Share my expertise and educational content with my professional network
|
|
16
|
-
|
|
17
|
-
This tool is designed to help educate and demonstrate expertise to a LinkedIn audience through consistent, high-quality content sharing.
|
|
18
|
-
|
|
19
|
-
## Status
|
|
20
|
-
|
|
21
|
-
✅ **MVP Complete** - All core tools implemented and tested!
|
|
22
|
-
|
|
23
|
-
**Milestones:**
|
|
24
|
-
- ✅ LinkedIn Developer App with OAuth 2.0
|
|
25
|
-
- ✅ 7 MCP tools implemented (create, get, delete posts + auth)
|
|
26
|
-
- ✅ 24 automated tests passing
|
|
27
|
-
- ✅ Type-safe with JSDoc + Zod validation
|
|
28
|
-
- ✅ MCP server with stdio transport
|
|
29
|
-
- ✅ Ready for Claude Desktop integration
|
|
30
|
-
|
|
31
|
-
## Features
|
|
32
|
-
|
|
33
|
-
### ✅ Implemented (MVP)
|
|
34
|
-
|
|
35
|
-
- **linkedin_create_post** - Create simple text posts with hashtags and mentions
|
|
36
|
-
- **linkedin_create_post_with_link** - Create posts with link previews (GitHub, blogs, etc.)
|
|
37
|
-
- **linkedin_get_my_posts** - Retrieve your recent posts with pagination
|
|
38
|
-
- **linkedin_delete_post** - Delete posts by URN
|
|
39
|
-
- **linkedin_get_auth_url** - Generate OAuth authorization URL
|
|
40
|
-
- **linkedin_exchange_code** - Exchange auth code for access token
|
|
41
|
-
- **linkedin_get_user_info** - Get your LinkedIn profile information
|
|
42
|
-
|
|
43
|
-
### 🚧 Planned (Next Phase)
|
|
44
|
-
|
|
45
|
-
- **Post scheduling** - Schedule posts for future publication (HIGH PRIORITY)
|
|
46
|
-
- **Draft management** - Save and manage draft posts locally
|
|
47
|
-
- **Image uploads** - Create posts with images
|
|
48
|
-
- **Post updates** - Edit existing posts
|
|
49
|
-
- **Analytics** - View post engagement metrics (if API supports)
|
|
50
|
-
|
|
51
|
-
## LinkedIn API
|
|
52
|
-
|
|
53
|
-
This MCP will utilize LinkedIn's REST API:
|
|
54
|
-
- Base URL: `https://api.linkedin.com/rest/posts`
|
|
55
|
-
- Authentication: OAuth 2.0
|
|
56
|
-
- Required permissions: `w_member_social` scope
|
|
57
|
-
- API Documentation: [LinkedIn Posts API](https://learn.microsoft.com/en-us/linkedin/marketing/community-management/shares/posts-api)
|
|
58
|
-
|
|
59
|
-
## Development Philosophy
|
|
60
|
-
|
|
61
|
-
1. Start with clear user stories based on LinkedIn API capabilities
|
|
62
|
-
2. Refine requirements through discussion and iteration
|
|
63
|
-
3. Build incrementally with well-defined issues and branches
|
|
64
|
-
4. Test thoroughly with real-world use cases
|
|
65
|
-
|
|
66
|
-
## Getting Started
|
|
67
|
-
|
|
68
|
-
### Prerequisites
|
|
69
|
-
|
|
70
|
-
1. Create a LinkedIn Developer App at https://www.linkedin.com/developers/apps
|
|
71
|
-
2. Add these products to your app:
|
|
72
|
-
- "Sign In with LinkedIn using OpenID Connect"
|
|
73
|
-
- "Share on LinkedIn"
|
|
74
|
-
3. Configure OAuth redirect URI (e.g., `https://localhost:3000/callback`)
|
|
75
|
-
|
|
76
|
-
### Setup
|
|
77
|
-
|
|
78
|
-
1. Clone the repository:
|
|
10
|
+
### npm / npx
|
|
79
11
|
```bash
|
|
80
|
-
|
|
81
|
-
cd mcp-linkedin
|
|
12
|
+
npx @ldraney/mcp-linkedin
|
|
82
13
|
```
|
|
83
14
|
|
|
84
|
-
|
|
85
|
-
```
|
|
86
|
-
|
|
15
|
+
### Manual MCP Config
|
|
16
|
+
```json
|
|
17
|
+
{
|
|
18
|
+
"mcpServers": {
|
|
19
|
+
"linkedin": {
|
|
20
|
+
"command": "npx",
|
|
21
|
+
"args": ["@ldraney/mcp-linkedin"],
|
|
22
|
+
"env": {
|
|
23
|
+
"LINKEDIN_CLIENT_ID": "your-client-id",
|
|
24
|
+
"LINKEDIN_CLIENT_SECRET": "your-client-secret",
|
|
25
|
+
"LINKEDIN_REDIRECT_URI": "https://localhost:8888/callback",
|
|
26
|
+
"LINKEDIN_PERSON_ID": "your-person-id",
|
|
27
|
+
"LINKEDIN_ACCESS_TOKEN": "your-access-token"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
87
32
|
```
|
|
88
33
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
34
|
+
## Tools (20 Total)
|
|
35
|
+
|
|
36
|
+
### Content Creation
|
|
37
|
+
| Tool | Description |
|
|
38
|
+
|------|-------------|
|
|
39
|
+
| `linkedin_create_post` | Create text posts |
|
|
40
|
+
| `linkedin_create_post_with_link` | Posts with article/link preview |
|
|
41
|
+
| `linkedin_create_post_with_image` | Upload image + create post |
|
|
42
|
+
| `linkedin_create_post_with_video` | Upload video + create post |
|
|
43
|
+
| `linkedin_create_post_with_document` | Upload PDF/PPT/DOC + create post |
|
|
44
|
+
| `linkedin_create_post_with_multi_images` | Upload 2-20 images + create post |
|
|
45
|
+
| `linkedin_create_poll` | Create poll posts (2-4 options) |
|
|
46
|
+
|
|
47
|
+
### Content Management
|
|
48
|
+
| Tool | Description |
|
|
49
|
+
|------|-------------|
|
|
50
|
+
| `linkedin_get_my_posts` | Retrieve recent posts (paginated) |
|
|
51
|
+
| `linkedin_update_post` | Edit existing posts |
|
|
52
|
+
| `linkedin_delete_post` | Delete by URN |
|
|
53
|
+
|
|
54
|
+
### Social Interactions
|
|
55
|
+
| Tool | Description |
|
|
56
|
+
|------|-------------|
|
|
57
|
+
| `linkedin_add_comment` | Add comment to a post |
|
|
58
|
+
| `linkedin_add_reaction` | React to a post (LIKE, PRAISE, etc.) |
|
|
59
|
+
|
|
60
|
+
### Scheduling
|
|
61
|
+
| Tool | Description |
|
|
62
|
+
|------|-------------|
|
|
63
|
+
| `linkedin_schedule_post` | Schedule for future publication |
|
|
64
|
+
| `linkedin_list_scheduled_posts` | List by status |
|
|
65
|
+
| `linkedin_cancel_scheduled_post` | Cancel pending post |
|
|
66
|
+
| `linkedin_get_scheduled_post` | Get scheduled post details |
|
|
67
|
+
|
|
68
|
+
### Authentication
|
|
69
|
+
| Tool | Description |
|
|
70
|
+
|------|-------------|
|
|
71
|
+
| `linkedin_get_auth_url` | Start OAuth flow |
|
|
72
|
+
| `linkedin_exchange_code` | Complete OAuth |
|
|
73
|
+
| `linkedin_refresh_token` | Refresh expired token |
|
|
74
|
+
| `linkedin_get_user_info` | Get profile info |
|
|
75
|
+
|
|
76
|
+
## Setup
|
|
77
|
+
|
|
78
|
+
### Option 1: Quick Setup (Recommended)
|
|
79
|
+
1. Install the extension
|
|
80
|
+
2. Ask Claude: "Help me set up LinkedIn authentication"
|
|
81
|
+
3. Follow the guided OAuth flow
|
|
82
|
+
|
|
83
|
+
### Option 2: Manual Setup
|
|
84
|
+
1. Create a LinkedIn Developer App at https://www.linkedin.com/developers/apps
|
|
85
|
+
2. Add products: "Sign In with LinkedIn using OpenID Connect" + "Share on LinkedIn"
|
|
86
|
+
3. Configure redirect URI: `https://localhost:8888/callback`
|
|
87
|
+
4. Set environment variables (see Installation above)
|
|
93
88
|
|
|
94
|
-
|
|
89
|
+
## Running the Scheduler
|
|
90
|
+
|
|
91
|
+
For scheduled posts to publish automatically:
|
|
95
92
|
```bash
|
|
96
|
-
npm
|
|
93
|
+
npm run scheduler
|
|
97
94
|
```
|
|
98
95
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
See [MCP_SETUP.md](./MCP_SETUP.md) for detailed Claude Desktop configuration instructions.
|
|
96
|
+
This runs a daemon that checks every minute for posts due to publish.
|
|
102
97
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
See `.env.example` for required configuration:
|
|
106
|
-
- `LINKEDIN_CLIENT_ID` - Your app's client ID
|
|
107
|
-
- `LINKEDIN_CLIENT_SECRET` - Your app's client secret
|
|
108
|
-
- `LINKEDIN_REDIRECT_URI` - OAuth callback URL
|
|
109
|
-
- `LINKEDIN_PERSON_ID` - Your LinkedIn person URN (from userinfo endpoint)
|
|
110
|
-
- `LINKEDIN_ACCESS_TOKEN` - OAuth access token (60-day expiry)
|
|
111
|
-
|
|
112
|
-
## Documentation
|
|
113
|
-
|
|
114
|
-
- **[MCP_SETUP.md](./MCP_SETUP.md)** - Claude Desktop configuration and usage guide
|
|
115
|
-
- **[USER_STORY.md](./USER_STORY.md)** - Complete user stories and feature roadmap
|
|
116
|
-
- **[LINKEDIN_API_REFERENCE.md](./LINKEDIN_API_REFERENCE.md)** - Comprehensive API documentation
|
|
117
|
-
- **[src/types.js](./src/types.js)** - JSDoc type definitions
|
|
118
|
-
- **[src/schemas.js](./src/schemas.js)** - Zod validation schemas
|
|
119
|
-
|
|
120
|
-
## Testing
|
|
98
|
+
## Development
|
|
121
99
|
|
|
122
100
|
```bash
|
|
123
|
-
npm
|
|
124
|
-
npm
|
|
101
|
+
npm install
|
|
102
|
+
npm test # 118 tests
|
|
125
103
|
npm run test:coverage # Coverage report
|
|
104
|
+
npm start # Start MCP server
|
|
105
|
+
npm run scheduler # Start scheduler daemon
|
|
126
106
|
```
|
|
127
107
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
## Contributing
|
|
108
|
+
## Documentation
|
|
131
109
|
|
|
132
|
-
|
|
110
|
+
- [CLAUDE.md](./CLAUDE.md) - Project structure and API details
|
|
111
|
+
- [MCP_SETUP.md](./MCP_SETUP.md) - Claude Desktop configuration
|
|
112
|
+
- [LINKEDIN_API_REFERENCE.md](./LINKEDIN_API_REFERENCE.md) - API documentation
|
|
133
113
|
|
|
134
114
|
## License
|
|
135
115
|
|
package/manifest.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"manifest_version": "0.2",
|
|
3
3
|
"name": "@ldraney/mcp-linkedin",
|
|
4
|
-
"version": "0.1
|
|
5
|
-
"description": "MCP server for
|
|
4
|
+
"version": "0.2.1",
|
|
5
|
+
"description": "MCP server for LinkedIn - create posts, images, videos, documents, polls. One-click OAuth via built-in relay.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Lucas Draney"
|
|
8
8
|
},
|
|
@@ -19,43 +19,22 @@
|
|
|
19
19
|
"${__dirname}/src/index.js"
|
|
20
20
|
],
|
|
21
21
|
"env": {
|
|
22
|
-
"LINKEDIN_CLIENT_ID": "${user_config.linkedin_client_id}",
|
|
23
|
-
"LINKEDIN_CLIENT_SECRET": "${user_config.linkedin_client_secret}",
|
|
24
|
-
"LINKEDIN_REDIRECT_URI": "${user_config.linkedin_redirect_uri}",
|
|
25
22
|
"LINKEDIN_PERSON_ID": "${user_config.linkedin_person_id}",
|
|
26
23
|
"LINKEDIN_ACCESS_TOKEN": "${user_config.linkedin_access_token}"
|
|
27
24
|
}
|
|
28
25
|
}
|
|
29
26
|
},
|
|
30
27
|
"user_config": {
|
|
31
|
-
"linkedin_client_id": {
|
|
32
|
-
"type": "string",
|
|
33
|
-
"title": "Client ID (optional)",
|
|
34
|
-
"description": "LinkedIn OAuth app client ID - leave blank to set up via chat",
|
|
35
|
-
"required": false
|
|
36
|
-
},
|
|
37
|
-
"linkedin_client_secret": {
|
|
38
|
-
"type": "string",
|
|
39
|
-
"title": "Client Secret (optional)",
|
|
40
|
-
"description": "LinkedIn OAuth app client secret - leave blank to set up via chat",
|
|
41
|
-
"required": false
|
|
42
|
-
},
|
|
43
|
-
"linkedin_redirect_uri": {
|
|
44
|
-
"type": "string",
|
|
45
|
-
"title": "Redirect URI (optional)",
|
|
46
|
-
"description": "OAuth callback URL - defaults to https://localhost:8888/callback",
|
|
47
|
-
"required": false
|
|
48
|
-
},
|
|
49
28
|
"linkedin_person_id": {
|
|
50
29
|
"type": "string",
|
|
51
30
|
"title": "Person ID (optional)",
|
|
52
|
-
"description": "Your LinkedIn person ID - obtained
|
|
31
|
+
"description": "Your LinkedIn person ID - obtained via OAuth flow in chat",
|
|
53
32
|
"required": false
|
|
54
33
|
},
|
|
55
34
|
"linkedin_access_token": {
|
|
56
35
|
"type": "string",
|
|
57
36
|
"title": "Access Token (optional)",
|
|
58
|
-
"description": "OAuth access token - obtained via
|
|
37
|
+
"description": "OAuth access token - obtained via OAuth flow in chat",
|
|
59
38
|
"required": false
|
|
60
39
|
}
|
|
61
40
|
},
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
app = 'iss-linkedin-oauth'
|
|
2
|
+
primary_region = 'sjc'
|
|
3
|
+
|
|
4
|
+
[build]
|
|
5
|
+
|
|
6
|
+
[http_service]
|
|
7
|
+
internal_port = 3000
|
|
8
|
+
force_https = true
|
|
9
|
+
auto_stop_machines = 'stop'
|
|
10
|
+
auto_start_machines = true
|
|
11
|
+
min_machines_running = 0
|
|
12
|
+
processes = ['app']
|
|
13
|
+
|
|
14
|
+
[[vm]]
|
|
15
|
+
memory = '256mb'
|
|
16
|
+
cpu_kind = 'shared'
|
|
17
|
+
cpus = 1
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
const express = require('express');
|
|
2
|
+
const https = require('https');
|
|
3
|
+
|
|
4
|
+
const app = express();
|
|
5
|
+
const PORT = process.env.PORT || 3000;
|
|
6
|
+
|
|
7
|
+
const CLIENT_ID = process.env.LINKEDIN_CLIENT_ID;
|
|
8
|
+
const CLIENT_SECRET = process.env.LINKEDIN_CLIENT_SECRET;
|
|
9
|
+
const REDIRECT_URI = process.env.REDIRECT_URI || `https://${process.env.FLY_APP_NAME}.fly.dev/auth/callback`;
|
|
10
|
+
const LOCALHOST_CALLBACK = 'http://localhost:8888/callback';
|
|
11
|
+
|
|
12
|
+
// Health check
|
|
13
|
+
app.get('/', (req, res) => {
|
|
14
|
+
res.json({ status: 'ok', service: 'mcp-linkedin-oauth-relay' });
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
// Step 1: Redirect user to LinkedIn OAuth
|
|
18
|
+
app.get('/auth/linkedin', (req, res) => {
|
|
19
|
+
if (!CLIENT_ID) {
|
|
20
|
+
return res.status(500).json({ error: 'LINKEDIN_CLIENT_ID not configured' });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const scope = 'openid profile w_member_social';
|
|
24
|
+
const state = Math.random().toString(36).substring(7);
|
|
25
|
+
|
|
26
|
+
const authUrl = new URL('https://www.linkedin.com/oauth/v2/authorization');
|
|
27
|
+
authUrl.searchParams.set('response_type', 'code');
|
|
28
|
+
authUrl.searchParams.set('client_id', CLIENT_ID);
|
|
29
|
+
authUrl.searchParams.set('redirect_uri', REDIRECT_URI);
|
|
30
|
+
authUrl.searchParams.set('scope', scope);
|
|
31
|
+
authUrl.searchParams.set('state', state);
|
|
32
|
+
|
|
33
|
+
res.redirect(authUrl.toString());
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// Step 2: Handle callback from LinkedIn, exchange code for token
|
|
37
|
+
app.get('/auth/callback', async (req, res) => {
|
|
38
|
+
const { code, error, error_description } = req.query;
|
|
39
|
+
|
|
40
|
+
if (error) {
|
|
41
|
+
return redirectToLocalhost(res, { error, error_description });
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (!code) {
|
|
45
|
+
return redirectToLocalhost(res, { error: 'missing_code', error_description: 'No authorization code received' });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
// Exchange code for access token
|
|
50
|
+
const tokenData = await exchangeCodeForToken(code);
|
|
51
|
+
|
|
52
|
+
// Get user info to get person ID
|
|
53
|
+
const userInfo = await getUserInfo(tokenData.access_token);
|
|
54
|
+
|
|
55
|
+
// Redirect to localhost with token and person ID
|
|
56
|
+
redirectToLocalhost(res, {
|
|
57
|
+
access_token: tokenData.access_token,
|
|
58
|
+
expires_in: tokenData.expires_in,
|
|
59
|
+
person_id: userInfo.sub,
|
|
60
|
+
name: userInfo.name
|
|
61
|
+
});
|
|
62
|
+
} catch (err) {
|
|
63
|
+
console.error('OAuth error:', err);
|
|
64
|
+
redirectToLocalhost(res, { error: 'token_exchange_failed', error_description: err.message });
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
function redirectToLocalhost(res, params) {
|
|
69
|
+
const url = new URL(LOCALHOST_CALLBACK);
|
|
70
|
+
Object.entries(params).forEach(([key, value]) => {
|
|
71
|
+
if (value) url.searchParams.set(key, value);
|
|
72
|
+
});
|
|
73
|
+
res.redirect(url.toString());
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function exchangeCodeForToken(code) {
|
|
77
|
+
return new Promise((resolve, reject) => {
|
|
78
|
+
const postData = new URLSearchParams({
|
|
79
|
+
grant_type: 'authorization_code',
|
|
80
|
+
code,
|
|
81
|
+
client_id: CLIENT_ID,
|
|
82
|
+
client_secret: CLIENT_SECRET,
|
|
83
|
+
redirect_uri: REDIRECT_URI
|
|
84
|
+
}).toString();
|
|
85
|
+
|
|
86
|
+
const options = {
|
|
87
|
+
hostname: 'www.linkedin.com',
|
|
88
|
+
path: '/oauth/v2/accessToken',
|
|
89
|
+
method: 'POST',
|
|
90
|
+
headers: {
|
|
91
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
92
|
+
'Content-Length': Buffer.byteLength(postData)
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const req = https.request(options, (res) => {
|
|
97
|
+
let data = '';
|
|
98
|
+
res.on('data', chunk => data += chunk);
|
|
99
|
+
res.on('end', () => {
|
|
100
|
+
try {
|
|
101
|
+
const parsed = JSON.parse(data);
|
|
102
|
+
if (parsed.error) {
|
|
103
|
+
reject(new Error(parsed.error_description || parsed.error));
|
|
104
|
+
} else {
|
|
105
|
+
resolve(parsed);
|
|
106
|
+
}
|
|
107
|
+
} catch (e) {
|
|
108
|
+
reject(new Error('Failed to parse token response'));
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
req.on('error', reject);
|
|
114
|
+
req.write(postData);
|
|
115
|
+
req.end();
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function getUserInfo(accessToken) {
|
|
120
|
+
return new Promise((resolve, reject) => {
|
|
121
|
+
const options = {
|
|
122
|
+
hostname: 'api.linkedin.com',
|
|
123
|
+
path: '/v2/userinfo',
|
|
124
|
+
method: 'GET',
|
|
125
|
+
headers: {
|
|
126
|
+
'Authorization': `Bearer ${accessToken}`
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const req = https.request(options, (res) => {
|
|
131
|
+
let data = '';
|
|
132
|
+
res.on('data', chunk => data += chunk);
|
|
133
|
+
res.on('end', () => {
|
|
134
|
+
try {
|
|
135
|
+
resolve(JSON.parse(data));
|
|
136
|
+
} catch (e) {
|
|
137
|
+
reject(new Error('Failed to parse user info'));
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
req.on('error', reject);
|
|
143
|
+
req.end();
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
app.listen(PORT, () => {
|
|
148
|
+
console.log(`OAuth relay running on port ${PORT}`);
|
|
149
|
+
console.log(`Auth URL: /auth/linkedin`);
|
|
150
|
+
console.log(`Callback: ${REDIRECT_URI}`);
|
|
151
|
+
});
|