@playrunner/github 0.1.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.
- package/LICENSE +134 -0
- package/README.md +30 -0
- package/assets/github.svg +5 -0
- package/package.json +101 -0
- package/src/api/index.ts +80 -0
- package/src/frontend/GithubIcon.tsx +13 -0
- package/src/frontend/GithubSettingsModal.tsx +475 -0
- package/src/frontend/icon.ts +2 -0
- package/src/frontend/index.tsx +23 -0
- package/src/frontend/tokenRefresh.ts +79 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
Playrunner Sustainable Use License v1.0
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Concept AI PTY LTD
|
|
4
|
+
|
|
5
|
+
This Playrunner Sustainable Use License v1.0 (the "License") applies to the
|
|
6
|
+
source code and related repository materials for Playrunner that are owned by
|
|
7
|
+
Concept AI PTY LTD, unless a file or directory states a different license.
|
|
8
|
+
|
|
9
|
+
1. Definitions
|
|
10
|
+
|
|
11
|
+
"Licensor" means Concept AI PTY LTD.
|
|
12
|
+
|
|
13
|
+
"Licensed Work" means the Playrunner source code and related repository
|
|
14
|
+
materials made available by Licensor under this License, excluding any material
|
|
15
|
+
that is expressly made available under a different license or other third-party
|
|
16
|
+
terms.
|
|
17
|
+
|
|
18
|
+
"You" means the individual or legal entity exercising rights under this
|
|
19
|
+
License.
|
|
20
|
+
|
|
21
|
+
"Internal Business Purposes" means use solely to support Your own internal
|
|
22
|
+
operations. It does not include making the Licensed Work, or a material part of
|
|
23
|
+
its functionality, available to third parties as a product or service.
|
|
24
|
+
|
|
25
|
+
2. License Grant
|
|
26
|
+
|
|
27
|
+
Subject to this License, Licensor grants You a worldwide, non-exclusive,
|
|
28
|
+
royalty-free, non-transferable, non-sublicensable license to use,
|
|
29
|
+
copy, run, modify, create derivative works of, and distribute the Licensed
|
|
30
|
+
Work.
|
|
31
|
+
|
|
32
|
+
3. Permitted Uses
|
|
33
|
+
|
|
34
|
+
You may exercise the rights granted in Section 2 only for:
|
|
35
|
+
|
|
36
|
+
a. Your Internal Business Purposes; or
|
|
37
|
+
b. personal, educational, evaluation, or other non-commercial purposes.
|
|
38
|
+
|
|
39
|
+
For clarity, use by Your employees, contractors, and controlled affiliates on
|
|
40
|
+
Your behalf for Your Internal Business Purposes is permitted under this
|
|
41
|
+
License.
|
|
42
|
+
|
|
43
|
+
4. Redistribution Conditions
|
|
44
|
+
|
|
45
|
+
If You distribute the Licensed Work or a modified version, You must:
|
|
46
|
+
|
|
47
|
+
a. include a copy of this License;
|
|
48
|
+
b. preserve all copyright, attribution, and license notices;
|
|
49
|
+
c. clearly identify that You changed the Licensed Work; and
|
|
50
|
+
d. distribute it only free of charge and only for uses otherwise permitted by
|
|
51
|
+
Section 3.
|
|
52
|
+
|
|
53
|
+
5. Prohibited Uses
|
|
54
|
+
|
|
55
|
+
Unless Licensor gives You separate written permission, You may not:
|
|
56
|
+
|
|
57
|
+
a. sell, license, sublicense, rent, lease, or otherwise monetize the Licensed
|
|
58
|
+
Work or access to it;
|
|
59
|
+
b. provide the Licensed Work, or a service substantially based on it, to third
|
|
60
|
+
parties on a hosted, managed, white-label, or software-as-a-service basis;
|
|
61
|
+
c. embed, bundle, or integrate the Licensed Work into a commercial product or
|
|
62
|
+
service where a material part of the value delivered to third parties comes
|
|
63
|
+
from the Licensed Work or its functionality, except where the Licensed Work
|
|
64
|
+
is used only as an internal tool supporting Your own operations;
|
|
65
|
+
d. remove, obscure, or alter any ownership, attribution, or license notices; or
|
|
66
|
+
e. use Licensor's names, logos, or trademarks except for reasonable nominative
|
|
67
|
+
reference or as separately authorized in writing.
|
|
68
|
+
|
|
69
|
+
6. Patent License
|
|
70
|
+
|
|
71
|
+
Subject to this License, Licensor grants You a worldwide, non-exclusive,
|
|
72
|
+
royalty-free patent license under patent claims that Licensor can license and
|
|
73
|
+
that are necessarily infringed by the unmodified Licensed Work, solely to make,
|
|
74
|
+
use, run, and import the Licensed Work for the permitted uses in this License.
|
|
75
|
+
|
|
76
|
+
This patent license does not apply to:
|
|
77
|
+
|
|
78
|
+
a. Your modifications to the Licensed Work;
|
|
79
|
+
b. combinations of the Licensed Work with other software, products, or services
|
|
80
|
+
unless such combination is part of the unmodified Licensed Work as released
|
|
81
|
+
by Licensor; or
|
|
82
|
+
c. any activity that is prohibited by this License.
|
|
83
|
+
|
|
84
|
+
If You bring or assist in bringing a patent claim against Licensor or any
|
|
85
|
+
recipient of the Licensed Work alleging that the Licensed Work infringes a
|
|
86
|
+
patent, Your rights under this License terminate immediately.
|
|
87
|
+
|
|
88
|
+
7. Contributions
|
|
89
|
+
|
|
90
|
+
Unless Licensor agrees otherwise in writing, any contribution intentionally
|
|
91
|
+
submitted by You for inclusion in the Licensed Work is provided under this
|
|
92
|
+
License, and You represent that You have the right to submit it on those terms.
|
|
93
|
+
|
|
94
|
+
8. Third-Party Materials
|
|
95
|
+
|
|
96
|
+
The Licensed Work may include or depend on third-party software or other
|
|
97
|
+
materials that are licensed under separate terms. Those third-party components
|
|
98
|
+
remain subject to their own licenses, notices, and obligations.
|
|
99
|
+
|
|
100
|
+
9. Termination
|
|
101
|
+
|
|
102
|
+
If You violate this License, Your rights under it terminate automatically.
|
|
103
|
+
Licensor may reinstate those rights if You fully cure the violation within 30
|
|
104
|
+
days after receiving written notice from Licensor. Termination does not limit
|
|
105
|
+
any other remedies available to Licensor.
|
|
106
|
+
|
|
107
|
+
10. No Warranty
|
|
108
|
+
|
|
109
|
+
TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS
|
|
110
|
+
PROVIDED "AS IS" AND "AS AVAILABLE", WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
111
|
+
KIND, WHETHER EXPRESS, IMPLIED, STATUTORY, OR OTHERWISE, INCLUDING ANY IMPLIED
|
|
112
|
+
WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE, AND
|
|
113
|
+
NON-INFRINGEMENT.
|
|
114
|
+
|
|
115
|
+
11. Limitation of Liability
|
|
116
|
+
|
|
117
|
+
TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW, IN NO EVENT WILL LICENSOR OR
|
|
118
|
+
ITS AFFILIATES, DIRECTORS, OFFICERS, EMPLOYEES, OR CONTRIBUTORS BE LIABLE FOR
|
|
119
|
+
ANY INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, PUNITIVE, OR CONSEQUENTIAL
|
|
120
|
+
DAMAGES, OR FOR ANY LOSS OF DATA, PROFITS, REVENUE, GOODWILL, OR BUSINESS
|
|
121
|
+
INTERRUPTION, ARISING OUT OF OR RELATING TO THIS LICENSE OR THE LICENSED WORK,
|
|
122
|
+
EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
|
|
123
|
+
|
|
124
|
+
TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW, LICENSOR'S TOTAL LIABILITY
|
|
125
|
+
ARISING OUT OF OR RELATING TO THIS LICENSE OR THE LICENSED WORK WILL NOT EXCEED
|
|
126
|
+
ONE HUNDRED AUSTRALIAN DOLLARS (AUD $100).
|
|
127
|
+
|
|
128
|
+
12. General
|
|
129
|
+
|
|
130
|
+
This License is the complete agreement between You and Licensor regarding the
|
|
131
|
+
Licensed Work. If any provision of this License is held unenforceable, the
|
|
132
|
+
remaining provisions will remain in effect to the fullest extent permitted by
|
|
133
|
+
law. A waiver of any breach or default under this License is effective only if
|
|
134
|
+
made in writing by Licensor.
|
package/README.md
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# @playrunner/github
|
|
2
|
+
|
|
3
|
+
GitHub authentication and repository access for Playrunner workflows.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @playrunner/github
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Add the package as a direct production dependency of the Playrunner frontend and API. Playwright installations also use this package for repository authentication.
|
|
12
|
+
|
|
13
|
+
## Package surfaces
|
|
14
|
+
|
|
15
|
+
- `@playrunner/github` exports the GitHub integration, icon, and settings UI.
|
|
16
|
+
- `@playrunner/github/api` exports token exchange and refresh API routes.
|
|
17
|
+
- `@playrunner/github/assets/github.svg` exports the package-owned icon.
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
import githubIntegration, { GithubSettingsModal } from '@playrunner/github';
|
|
21
|
+
import githubApiContribution from '@playrunner/github/api';
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Documentation
|
|
25
|
+
|
|
26
|
+
See the [GitHub integration documentation](https://playrunner.dev/docs/integration-packages/github/) for GitHub App setup, exports, and Playrunner build integration.
|
|
27
|
+
|
|
28
|
+
## License
|
|
29
|
+
|
|
30
|
+
Licensed under the [Playrunner Sustainable Use License](https://github.com/playrunner/playrunner/blob/main/LICENSE).
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
|
2
|
+
<title>GitHub</title>
|
|
3
|
+
<path d="M12,2A10,10 0 0,0 2,12c0 4.42 2.87 8.17 6.84 9.49.5.09.68-.22.68-.49 0-.24-.01-.87-.01-1.71-2.78.62-3.37-1.23-3.37-1.23-.45-1.18-1.11-1.49-1.11-1.49-.91-.63.07-.62.07-.62 1 .07 1.53 1.07 1.53 1.07.9 1.56 2.36 1.11 2.94.85.09-.66.35-1.11.63-1.37-2.22-.26-4.55-1.14-4.55-5.07 0-1.12.39-2.03 1.03-2.75-.1-.26-.45-1.31.1-2.73 0 0 .84-.27 2.75 1.05a9.38 9.38 0 0 1 5 0c1.91-1.32 2.75-1.05 2.75-1.05.55 1.42.2 2.47.1 2.73.64.72 1.03 1.63 1.03 2.75 0 3.94-2.34 4.81-4.57 5.06.36.31.68.92.68 1.86 0 1.34-.01 2.41-.01 2.74 0 .27.18.59.69.49C19.14 20.41 22 16.66 22 12.24A10.24 10.24 0 0 0 12,2z"/>
|
|
4
|
+
</svg>
|
|
5
|
+
|
package/package.json
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@playrunner/github",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "GitHub integration package for Playrunner.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"playrunner",
|
|
7
|
+
"github",
|
|
8
|
+
"integration"
|
|
9
|
+
],
|
|
10
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
11
|
+
"author": "Concept AI PTY LTD",
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "git+https://github.com/playrunner/playrunner.git",
|
|
15
|
+
"directory": "packages/github"
|
|
16
|
+
},
|
|
17
|
+
"homepage": "https://playrunner.dev/docs/integration-packages/github/",
|
|
18
|
+
"bugs": {
|
|
19
|
+
"url": "https://github.com/playrunner/playrunner/issues"
|
|
20
|
+
},
|
|
21
|
+
"type": "module",
|
|
22
|
+
"publishConfig": {
|
|
23
|
+
"registry": "https://registry.npmjs.org",
|
|
24
|
+
"access": "public"
|
|
25
|
+
},
|
|
26
|
+
"playrunner": {
|
|
27
|
+
"integration": {
|
|
28
|
+
"id": "github",
|
|
29
|
+
"frontend": ".",
|
|
30
|
+
"api": "./api"
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"exports": {
|
|
34
|
+
".": {
|
|
35
|
+
"types": "./src/frontend/index.tsx",
|
|
36
|
+
"import": "./src/frontend/index.tsx",
|
|
37
|
+
"require": "./src/frontend/index.tsx",
|
|
38
|
+
"default": "./src/frontend/index.tsx"
|
|
39
|
+
},
|
|
40
|
+
"./frontend": {
|
|
41
|
+
"types": "./src/frontend/index.tsx",
|
|
42
|
+
"import": "./src/frontend/index.tsx",
|
|
43
|
+
"require": "./src/frontend/index.tsx",
|
|
44
|
+
"default": "./src/frontend/index.tsx"
|
|
45
|
+
},
|
|
46
|
+
"./api": {
|
|
47
|
+
"types": "./src/api/index.ts",
|
|
48
|
+
"import": "./src/api/index.ts",
|
|
49
|
+
"require": "./src/api/index.ts",
|
|
50
|
+
"default": "./src/api/index.ts"
|
|
51
|
+
},
|
|
52
|
+
"./assets/github.svg": "./assets/github.svg"
|
|
53
|
+
},
|
|
54
|
+
"files": [
|
|
55
|
+
"LICENSE",
|
|
56
|
+
"README.md",
|
|
57
|
+
"assets",
|
|
58
|
+
"src"
|
|
59
|
+
],
|
|
60
|
+
"scripts": {
|
|
61
|
+
"lint": "eslint .",
|
|
62
|
+
"lint:fix": "eslint . --fix",
|
|
63
|
+
"format": "prettier --write .",
|
|
64
|
+
"format:check": "prettier --check .",
|
|
65
|
+
"typecheck": "tsc --noEmit"
|
|
66
|
+
},
|
|
67
|
+
"dependencies": {
|
|
68
|
+
"express": "^5.2.1"
|
|
69
|
+
},
|
|
70
|
+
"peerDependencies": {
|
|
71
|
+
"@playrunner/integration-sdk": "^0.1.0",
|
|
72
|
+
"lucide-react": "^0.546.0",
|
|
73
|
+
"react": "^19.0.0"
|
|
74
|
+
},
|
|
75
|
+
"peerDependenciesMeta": {
|
|
76
|
+
"@playrunner/integration-sdk": {
|
|
77
|
+
"optional": true
|
|
78
|
+
},
|
|
79
|
+
"lucide-react": {
|
|
80
|
+
"optional": true
|
|
81
|
+
},
|
|
82
|
+
"react": {
|
|
83
|
+
"optional": true
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
"devDependencies": {
|
|
87
|
+
"@eslint/js": "^9.39.4",
|
|
88
|
+
"@types/express": "^5.0.6",
|
|
89
|
+
"@types/react": "^19.2.14",
|
|
90
|
+
"eslint": "^9.39.4",
|
|
91
|
+
"eslint-config-prettier": "10.1.8",
|
|
92
|
+
"eslint-plugin-react": "^7.37.5",
|
|
93
|
+
"eslint-plugin-react-hooks": "^7.1.1",
|
|
94
|
+
"eslint-plugin-react-refresh": "^0.5.3",
|
|
95
|
+
"globals": "^17.6.0",
|
|
96
|
+
"lucide-react": "^0.546.0",
|
|
97
|
+
"prettier": "3.8.4",
|
|
98
|
+
"typescript": "~5.8.2",
|
|
99
|
+
"typescript-eslint": "^8.61.1"
|
|
100
|
+
}
|
|
101
|
+
}
|
package/src/api/index.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
|
|
3
|
+
export const githubRouter = Router();
|
|
4
|
+
|
|
5
|
+
export const githubApiContribution = {
|
|
6
|
+
id: 'github',
|
|
7
|
+
mountPath: '/api/github',
|
|
8
|
+
router: githubRouter,
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export default githubApiContribution;
|
|
12
|
+
|
|
13
|
+
// Proxy endpoint to exchange GitHub OAuth code for an access token to bypass CORS
|
|
14
|
+
githubRouter.post('/token', async (req, res) => {
|
|
15
|
+
const { code, client_id, client_secret } = req.body;
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
const gRes = await fetch('https://github.com/login/oauth/access_token', {
|
|
19
|
+
method: 'POST',
|
|
20
|
+
headers: {
|
|
21
|
+
'Content-Type': 'application/json',
|
|
22
|
+
Accept: 'application/json',
|
|
23
|
+
'User-Agent': 'Playrunner-App',
|
|
24
|
+
},
|
|
25
|
+
body: JSON.stringify({
|
|
26
|
+
client_id,
|
|
27
|
+
client_secret,
|
|
28
|
+
code,
|
|
29
|
+
}),
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const text = await gRes.text();
|
|
33
|
+
try {
|
|
34
|
+
const data = JSON.parse(text);
|
|
35
|
+
res.json(data);
|
|
36
|
+
} catch {
|
|
37
|
+
console.error('Token exchange failed. GitHub returned non-JSON:', text);
|
|
38
|
+
res
|
|
39
|
+
.status(500)
|
|
40
|
+
.json({ error: 'Failed to exchange token', details: text });
|
|
41
|
+
}
|
|
42
|
+
} catch (err) {
|
|
43
|
+
console.error('Token exchange error:', err);
|
|
44
|
+
res.status(500).json({ error: 'Failed to exchange token' });
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// Proxy endpoint to refresh GitHub OAuth token
|
|
49
|
+
githubRouter.post('/refresh', async (req, res) => {
|
|
50
|
+
const { refresh_token, client_id, client_secret } = req.body;
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
const gRes = await fetch('https://github.com/login/oauth/access_token', {
|
|
54
|
+
method: 'POST',
|
|
55
|
+
headers: {
|
|
56
|
+
'Content-Type': 'application/json',
|
|
57
|
+
Accept: 'application/json',
|
|
58
|
+
'User-Agent': 'Playrunner-App',
|
|
59
|
+
},
|
|
60
|
+
body: JSON.stringify({
|
|
61
|
+
client_id,
|
|
62
|
+
client_secret,
|
|
63
|
+
refresh_token,
|
|
64
|
+
grant_type: 'refresh_token',
|
|
65
|
+
}),
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const text = await gRes.text();
|
|
69
|
+
try {
|
|
70
|
+
const data = JSON.parse(text);
|
|
71
|
+
res.json(data);
|
|
72
|
+
} catch {
|
|
73
|
+
console.error('Token refresh failed. GitHub returned non-JSON:', text);
|
|
74
|
+
res.status(500).json({ error: 'Failed to refresh token', details: text });
|
|
75
|
+
}
|
|
76
|
+
} catch (err) {
|
|
77
|
+
console.error('Token refresh error:', err);
|
|
78
|
+
res.status(500).json({ error: 'Failed to refresh token' });
|
|
79
|
+
}
|
|
80
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export function GithubIcon({ className }: { className?: string }) {
|
|
2
|
+
return (
|
|
3
|
+
<svg
|
|
4
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
5
|
+
viewBox="0 0 24 24"
|
|
6
|
+
fill="currentColor"
|
|
7
|
+
aria-hidden="true"
|
|
8
|
+
className={className}
|
|
9
|
+
>
|
|
10
|
+
<path d="M12,2A10,10 0 0,0 2,12c0 4.42 2.87 8.17 6.84 9.49.5.09.68-.22.68-.49 0-.24-.01-.87-.01-1.71-2.78.62-3.37-1.23-3.37-1.23-.45-1.18-1.11-1.49-1.11-1.49-.91-.63.07-.62.07-.62 1 .07 1.53 1.07 1.53 1.07.9 1.56 2.36 1.11 2.94.85.09-.66.35-1.11.63-1.37-2.22-.26-4.55-1.14-4.55-5.07 0-1.12.39-2.03 1.03-2.75-.1-.26-.45-1.31.1-2.73 0 0 .84-.27 2.75 1.05a9.38 9.38 0 0 1 5 0c1.91-1.32 2.75-1.05 2.75-1.05.55 1.42.2 2.47.1 2.73.64.72 1.03 1.63 1.03 2.75 0 3.94-2.34 4.81-4.57 5.06.36.31.68.92.68 1.86 0 1.34-.01 2.41-.01 2.74 0 .27.18.59.69.49C19.14 20.41 22 16.66 22 12.24A10.24 10.24 0 0 0 12,2z" />
|
|
11
|
+
</svg>
|
|
12
|
+
);
|
|
13
|
+
}
|
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
BookOpen,
|
|
4
|
+
Check,
|
|
5
|
+
ChevronRight,
|
|
6
|
+
Copy,
|
|
7
|
+
ExternalLink,
|
|
8
|
+
Loader2,
|
|
9
|
+
} from 'lucide-react';
|
|
10
|
+
import {
|
|
11
|
+
IntegrationConnectionAutofillGuard,
|
|
12
|
+
IntegrationConnectionInput,
|
|
13
|
+
useIntegrationHost,
|
|
14
|
+
} from '@playrunner/integration-sdk';
|
|
15
|
+
import { GithubIcon } from './GithubIcon';
|
|
16
|
+
|
|
17
|
+
interface GithubSettingsModalProps {
|
|
18
|
+
isOpen: boolean;
|
|
19
|
+
onClose: () => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const DEFAULT_DOCS_URL = 'https://playrunner.dev';
|
|
23
|
+
const GITHUB_SETUP_DOCS_URL = getDocsUrl('docs/tutorials/connect-github');
|
|
24
|
+
|
|
25
|
+
type DocsImportMeta = ImportMeta & {
|
|
26
|
+
env?: {
|
|
27
|
+
VITE_DOCS_URL?: string;
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
function getDocsUrl(path = '') {
|
|
32
|
+
const baseUrl = (
|
|
33
|
+
(import.meta as DocsImportMeta).env?.VITE_DOCS_URL || DEFAULT_DOCS_URL
|
|
34
|
+
)
|
|
35
|
+
.trim()
|
|
36
|
+
.replace(/\/+$/, '');
|
|
37
|
+
const normalizedPath = path.trim().replace(/^\/+/, '');
|
|
38
|
+
|
|
39
|
+
return normalizedPath ? `${baseUrl}/${normalizedPath}` : baseUrl;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function GithubSettingsModal({
|
|
43
|
+
isOpen,
|
|
44
|
+
onClose,
|
|
45
|
+
}: GithubSettingsModalProps) {
|
|
46
|
+
const { auth, store, ui } = useIntegrationHost();
|
|
47
|
+
const Modal = ui.Modal;
|
|
48
|
+
const [githubAppName, setGithubAppName] = useState('');
|
|
49
|
+
const [githubClientId, setGithubClientId] = useState('');
|
|
50
|
+
const [githubClientSecret, setGithubClientSecret] = useState('');
|
|
51
|
+
const [githubAppSlug, setGithubAppSlug] = useState<string | null>(null);
|
|
52
|
+
const [isAuthenticating, setIsAuthenticating] = useState(false);
|
|
53
|
+
const [authSuccess, setAuthSuccess] = useState(false);
|
|
54
|
+
const [copiedUrl, setCopiedUrl] = useState(false);
|
|
55
|
+
const popupRef = React.useRef<Window | null>(null);
|
|
56
|
+
|
|
57
|
+
const callbackUrl = `${window.location.origin}/oauth/callback/github`;
|
|
58
|
+
|
|
59
|
+
const handleCopyUrl = () => {
|
|
60
|
+
navigator.clipboard.writeText(callbackUrl);
|
|
61
|
+
setCopiedUrl(true);
|
|
62
|
+
setTimeout(() => setCopiedUrl(false), 2000);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
React.useEffect(() => {
|
|
66
|
+
let isMounted = true;
|
|
67
|
+
const fetchCredentials = async () => {
|
|
68
|
+
if (isOpen && auth.currentUser) {
|
|
69
|
+
try {
|
|
70
|
+
const data = await store.getIntegration(
|
|
71
|
+
auth.currentUser.uid,
|
|
72
|
+
'github',
|
|
73
|
+
);
|
|
74
|
+
if (data && isMounted) {
|
|
75
|
+
if (data.clientId && data.accessToken) {
|
|
76
|
+
setGithubAppName(data.appName || data.appSlug || '');
|
|
77
|
+
setGithubClientId(data.clientId);
|
|
78
|
+
setGithubClientSecret(data.clientSecret || '');
|
|
79
|
+
if (data.appSlug) setGithubAppSlug(data.appSlug);
|
|
80
|
+
setAuthSuccess(true);
|
|
81
|
+
} else if (data.clientId) {
|
|
82
|
+
setGithubAppName(data.appName || data.appSlug || '');
|
|
83
|
+
setGithubClientId(data.clientId);
|
|
84
|
+
setGithubClientSecret(data.clientSecret || '');
|
|
85
|
+
if (data.appSlug) setGithubAppSlug(data.appSlug);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
} catch (error) {
|
|
89
|
+
console.error('Failed to fetch Github credentials', error);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
if (isOpen) {
|
|
95
|
+
fetchCredentials();
|
|
96
|
+
} else {
|
|
97
|
+
setAuthSuccess(false);
|
|
98
|
+
setIsAuthenticating(false);
|
|
99
|
+
setGithubAppName('');
|
|
100
|
+
setGithubClientId('');
|
|
101
|
+
setGithubClientSecret('');
|
|
102
|
+
setGithubAppSlug(null);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return () => {
|
|
106
|
+
isMounted = false;
|
|
107
|
+
};
|
|
108
|
+
}, [auth, isOpen, store]);
|
|
109
|
+
|
|
110
|
+
const handleAuthenticateGithub = async () => {
|
|
111
|
+
try {
|
|
112
|
+
setIsAuthenticating(true);
|
|
113
|
+
|
|
114
|
+
const currentUser = auth.currentUser;
|
|
115
|
+
|
|
116
|
+
if (currentUser) {
|
|
117
|
+
await store.saveIntegration(currentUser.uid, 'github', {
|
|
118
|
+
appName: githubAppName,
|
|
119
|
+
clientId: githubClientId,
|
|
120
|
+
clientSecret: githubClientSecret,
|
|
121
|
+
updatedAt: new Date().toISOString(),
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
let isProcessing = false;
|
|
126
|
+
const messageListener = async (event: MessageEvent) => {
|
|
127
|
+
if (event.origin !== window.location.origin) return;
|
|
128
|
+
if (event.data?.type === 'oauth_callback' && event.data?.success) {
|
|
129
|
+
if (isProcessing) return;
|
|
130
|
+
isProcessing = true;
|
|
131
|
+
|
|
132
|
+
if (auth.currentUser) {
|
|
133
|
+
if (event.data?.params?.code) {
|
|
134
|
+
try {
|
|
135
|
+
// Exchange the code for an access token
|
|
136
|
+
const token = await auth.currentUser.getIdToken();
|
|
137
|
+
const tokenRes = await fetch('/api/github/token', {
|
|
138
|
+
method: 'POST',
|
|
139
|
+
headers: {
|
|
140
|
+
'Content-Type': 'application/json',
|
|
141
|
+
Authorization: `Bearer ${token}`,
|
|
142
|
+
},
|
|
143
|
+
body: JSON.stringify({
|
|
144
|
+
code: event.data.params.code,
|
|
145
|
+
client_id: githubClientId,
|
|
146
|
+
client_secret: githubClientSecret,
|
|
147
|
+
}),
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
const tokenData = await tokenRes.json();
|
|
151
|
+
|
|
152
|
+
if (!tokenData.access_token) {
|
|
153
|
+
throw new Error(
|
|
154
|
+
`Failed to retrieve access token: ${JSON.stringify(tokenData)}`,
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const integrationData: any = {
|
|
159
|
+
appName: githubAppName,
|
|
160
|
+
clientId: githubClientId,
|
|
161
|
+
clientSecret: githubClientSecret,
|
|
162
|
+
code: event.data.params.code,
|
|
163
|
+
accessToken: tokenData.access_token,
|
|
164
|
+
refreshToken: tokenData.refresh_token,
|
|
165
|
+
expiresAt: tokenData.expires_in
|
|
166
|
+
? Date.now() + tokenData.expires_in * 1000
|
|
167
|
+
: undefined,
|
|
168
|
+
refreshTokenExpiresAt: tokenData.refresh_token_expires_in
|
|
169
|
+
? Date.now() + tokenData.refresh_token_expires_in * 1000
|
|
170
|
+
: undefined,
|
|
171
|
+
appSlug: githubAppName,
|
|
172
|
+
updatedAt: new Date().toISOString(),
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
// Store installation_id if it was provided
|
|
176
|
+
if (event.data.params.installation_id) {
|
|
177
|
+
integrationData.installationId =
|
|
178
|
+
event.data.params.installation_id;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
await store.saveIntegration(
|
|
182
|
+
auth.currentUser.uid,
|
|
183
|
+
'github',
|
|
184
|
+
integrationData,
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
setGithubAppSlug(githubAppName);
|
|
188
|
+
if (popupRef.current)
|
|
189
|
+
popupRef.current.postMessage(
|
|
190
|
+
{ type: 'oauth_close' },
|
|
191
|
+
window.location.origin,
|
|
192
|
+
);
|
|
193
|
+
} catch (err) {
|
|
194
|
+
console.error('Failed to save auth code:', err);
|
|
195
|
+
if (popupRef.current)
|
|
196
|
+
popupRef.current.postMessage(
|
|
197
|
+
{ type: 'oauth_close' },
|
|
198
|
+
window.location.origin,
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
} else if (event.data?.params?.installation_id) {
|
|
202
|
+
if (event.data?.params?.setup_action === 'update') {
|
|
203
|
+
// Just an update to the installation (e.g. adding repositories)
|
|
204
|
+
if (popupRef.current)
|
|
205
|
+
popupRef.current.postMessage(
|
|
206
|
+
{ type: 'oauth_close' },
|
|
207
|
+
window.location.origin,
|
|
208
|
+
);
|
|
209
|
+
} else {
|
|
210
|
+
// If there is an installation ID but no code, they probably forgot to check "Request user authorization (OAuth) during installation"
|
|
211
|
+
alert(
|
|
212
|
+
"Installation successful, but no OAuth code found. Please ensure you checked 'Request user authorization (OAuth) during installation' in your GitHub App settings.",
|
|
213
|
+
);
|
|
214
|
+
if (popupRef.current)
|
|
215
|
+
popupRef.current.postMessage(
|
|
216
|
+
{ type: 'oauth_close' },
|
|
217
|
+
window.location.origin,
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
setIsAuthenticating(false);
|
|
224
|
+
setAuthSuccess(true);
|
|
225
|
+
window.removeEventListener('message', messageListener);
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
window.addEventListener('message', messageListener);
|
|
230
|
+
|
|
231
|
+
const authUrl = `https://github.com/apps/${githubAppName}/installations/new`;
|
|
232
|
+
|
|
233
|
+
const width = 800;
|
|
234
|
+
const height = 700;
|
|
235
|
+
const left = window.screen.width / 2 - width / 2;
|
|
236
|
+
const top = window.screen.height / 2 - height / 2;
|
|
237
|
+
popupRef.current = window.open(
|
|
238
|
+
authUrl,
|
|
239
|
+
'GithubOAuth',
|
|
240
|
+
`toolbar=no, location=no, directories=no, status=no, menubar=no, scrollbars=no, resizable=no, copyhistory=no, width=${width}, height=${height}, top=${top}, left=${left}`,
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
const checkPopup = setInterval(() => {
|
|
244
|
+
if (!popupRef.current || popupRef.current.closed) {
|
|
245
|
+
clearInterval(checkPopup);
|
|
246
|
+
setIsAuthenticating(false);
|
|
247
|
+
window.removeEventListener('message', messageListener);
|
|
248
|
+
}
|
|
249
|
+
}, 500);
|
|
250
|
+
} catch (error) {
|
|
251
|
+
console.error('Failed to save credentials', error);
|
|
252
|
+
setIsAuthenticating(false);
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
const handleDisconnect = async () => {
|
|
257
|
+
if (!auth.currentUser) return;
|
|
258
|
+
try {
|
|
259
|
+
await store.deleteIntegration(auth.currentUser.uid, 'github');
|
|
260
|
+
setAuthSuccess(false);
|
|
261
|
+
setGithubAppName('');
|
|
262
|
+
setGithubClientId('');
|
|
263
|
+
setGithubClientSecret('');
|
|
264
|
+
setGithubAppSlug(null);
|
|
265
|
+
} catch (error) {
|
|
266
|
+
console.error('Failed to disconnect Github', error);
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
return (
|
|
271
|
+
<Modal
|
|
272
|
+
isOpen={isOpen}
|
|
273
|
+
onClose={onClose}
|
|
274
|
+
zIndex={70}
|
|
275
|
+
title="Connect to GitHub"
|
|
276
|
+
icon={<GithubIcon className="w-5 h-5 text-[var(--foreground)]" />}
|
|
277
|
+
footer={
|
|
278
|
+
!authSuccess ? (
|
|
279
|
+
<button
|
|
280
|
+
onClick={handleAuthenticateGithub}
|
|
281
|
+
disabled={
|
|
282
|
+
!githubAppName ||
|
|
283
|
+
!githubClientId ||
|
|
284
|
+
!githubClientSecret ||
|
|
285
|
+
isAuthenticating
|
|
286
|
+
}
|
|
287
|
+
className="flex items-center gap-2 px-4 py-2 rounded-lg bg-[var(--accent)] hover:bg-[var(--accent)]/90 text-[var(--accent-foreground)] font-medium text-sm transition-colors shadow-sm disabled:opacity-50 disabled:cursor-not-allowed"
|
|
288
|
+
>
|
|
289
|
+
{isAuthenticating ? (
|
|
290
|
+
<>
|
|
291
|
+
<Loader2 className="w-4 h-4 animate-spin" />
|
|
292
|
+
Authenticating...
|
|
293
|
+
</>
|
|
294
|
+
) : (
|
|
295
|
+
<>
|
|
296
|
+
Authenticate
|
|
297
|
+
<ChevronRight className="w-4 h-4" />
|
|
298
|
+
</>
|
|
299
|
+
)}
|
|
300
|
+
</button>
|
|
301
|
+
) : null
|
|
302
|
+
}
|
|
303
|
+
>
|
|
304
|
+
{authSuccess ? (
|
|
305
|
+
<div className="flex flex-col items-center justify-center text-center gap-4 py-8">
|
|
306
|
+
<div className="w-16 h-16 rounded-full bg-green-500/20 flex items-center justify-center animate-in zoom-in duration-300">
|
|
307
|
+
<Check className="w-8 h-8 text-green-500" />
|
|
308
|
+
</div>
|
|
309
|
+
<div>
|
|
310
|
+
<h3 className="text-xl font-semibold text-[var(--foreground)] mb-2">
|
|
311
|
+
GitHub Connected Successfully
|
|
312
|
+
</h3>
|
|
313
|
+
<p className="text-muted text-sm max-w-[320px] mx-auto mb-4">
|
|
314
|
+
Your GitHub App is connected. You can now use it to manage your
|
|
315
|
+
repositories.
|
|
316
|
+
</p>
|
|
317
|
+
|
|
318
|
+
<div className="bg-[var(--control-bg)] border border-[var(--border)] rounded-lg p-4 text-left max-w-sm mx-auto mb-6">
|
|
319
|
+
<h4 className="text-sm font-medium text-[var(--foreground)] mb-2">
|
|
320
|
+
Manage Repositories
|
|
321
|
+
</h4>
|
|
322
|
+
|
|
323
|
+
<div className="space-y-3 text-xs text-muted">
|
|
324
|
+
<p>
|
|
325
|
+
You can change which repositories the app has access to at any
|
|
326
|
+
time.
|
|
327
|
+
</p>
|
|
328
|
+
{githubAppSlug && (
|
|
329
|
+
<a
|
|
330
|
+
href={`https://github.com/apps/${githubAppSlug}/installations/new`}
|
|
331
|
+
target="_blank"
|
|
332
|
+
rel="noopener noreferrer"
|
|
333
|
+
className="flex justify-center items-center gap-2 px-3 py-2 mt-2 w-full rounded-md bg-[var(--foreground)] text-[var(--background)] hover:bg-[var(--foreground)]/90 font-medium transition-colors"
|
|
334
|
+
>
|
|
335
|
+
Configure App Repositories
|
|
336
|
+
</a>
|
|
337
|
+
)}
|
|
338
|
+
</div>
|
|
339
|
+
</div>
|
|
340
|
+
|
|
341
|
+
<div className="flex items-center justify-center gap-4">
|
|
342
|
+
<button
|
|
343
|
+
onClick={() => setAuthSuccess(false)}
|
|
344
|
+
className="text-xs text-muted hover:text-[var(--foreground)] underline transition-colors"
|
|
345
|
+
>
|
|
346
|
+
Change Credentials
|
|
347
|
+
</button>
|
|
348
|
+
<span className="text-muted text-xs">/</span>
|
|
349
|
+
<button
|
|
350
|
+
onClick={handleDisconnect}
|
|
351
|
+
className="text-xs text-red-500 hover:text-red-400 underline transition-colors"
|
|
352
|
+
>
|
|
353
|
+
Disconnect
|
|
354
|
+
</button>
|
|
355
|
+
</div>
|
|
356
|
+
</div>
|
|
357
|
+
<button
|
|
358
|
+
onClick={onClose}
|
|
359
|
+
className="mt-4 flex items-center gap-2 px-4 py-2 rounded-lg bg-surface border border-subtle hover:bg-surface-hover text-[var(--foreground)] font-medium text-sm transition-colors shadow-sm"
|
|
360
|
+
>
|
|
361
|
+
Close Menu
|
|
362
|
+
</button>
|
|
363
|
+
</div>
|
|
364
|
+
) : (
|
|
365
|
+
<div className="flex flex-col gap-6">
|
|
366
|
+
<IntegrationConnectionAutofillGuard connectionId="github" />
|
|
367
|
+
|
|
368
|
+
<div className="rounded-xl border border-[var(--border)] bg-[var(--surface-hover)] p-4 text-left">
|
|
369
|
+
<div className="flex items-start gap-3">
|
|
370
|
+
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg border border-[var(--border)] bg-[var(--background)]">
|
|
371
|
+
<BookOpen className="h-4 w-4 text-muted" />
|
|
372
|
+
</div>
|
|
373
|
+
<div className="min-w-0 flex-1">
|
|
374
|
+
<p className="text-sm font-medium text-[var(--foreground)]">
|
|
375
|
+
GitHub App setup
|
|
376
|
+
</p>
|
|
377
|
+
<p className="mt-1 text-xs leading-relaxed text-muted">
|
|
378
|
+
Use the setup guide to create the GitHub App, configure
|
|
379
|
+
repository permissions, and find the app slug, client ID, and
|
|
380
|
+
client secret.
|
|
381
|
+
</p>
|
|
382
|
+
<a
|
|
383
|
+
href={GITHUB_SETUP_DOCS_URL}
|
|
384
|
+
target="_blank"
|
|
385
|
+
rel="noopener noreferrer"
|
|
386
|
+
className="mt-3 inline-flex items-center gap-1.5 text-xs font-medium text-[var(--foreground)] underline underline-offset-4 hover:text-muted"
|
|
387
|
+
>
|
|
388
|
+
Open GitHub setup guide
|
|
389
|
+
<ExternalLink className="h-3.5 w-3.5" />
|
|
390
|
+
</a>
|
|
391
|
+
</div>
|
|
392
|
+
</div>
|
|
393
|
+
</div>
|
|
394
|
+
|
|
395
|
+
<div className="space-y-2">
|
|
396
|
+
<p className="text-xs font-medium text-muted">Callback URL</p>
|
|
397
|
+
<p className="text-xs leading-relaxed text-muted">
|
|
398
|
+
Use this value when the setup guide asks for the callback URL and
|
|
399
|
+
setup URL.
|
|
400
|
+
</p>
|
|
401
|
+
<div className="relative">
|
|
402
|
+
<code className="block p-3 pr-10 bg-[var(--background)] border border-subtle rounded text-xs select-all font-mono text-blue-400 overflow-x-auto whitespace-nowrap">
|
|
403
|
+
{callbackUrl}
|
|
404
|
+
</code>
|
|
405
|
+
<button
|
|
406
|
+
type="button"
|
|
407
|
+
onClick={handleCopyUrl}
|
|
408
|
+
className="absolute top-2 right-2 p-1 rounded bg-[var(--background)] text-muted hover:text-[var(--foreground)] hover:bg-surface-hover transition-colors"
|
|
409
|
+
title="Copy URL"
|
|
410
|
+
>
|
|
411
|
+
{copiedUrl ? (
|
|
412
|
+
<Check className="w-3.5 h-3.5 text-green-500" />
|
|
413
|
+
) : (
|
|
414
|
+
<Copy className="w-3.5 h-3.5" />
|
|
415
|
+
)}
|
|
416
|
+
</button>
|
|
417
|
+
</div>
|
|
418
|
+
</div>
|
|
419
|
+
|
|
420
|
+
<div className="space-y-4 pt-2 border-t border-subtle">
|
|
421
|
+
<div>
|
|
422
|
+
<label
|
|
423
|
+
htmlFor="github-connection-field-a"
|
|
424
|
+
className="block text-xs font-medium text-muted mb-1.5"
|
|
425
|
+
>
|
|
426
|
+
GitHub App Name (URL Slug)
|
|
427
|
+
</label>
|
|
428
|
+
<IntegrationConnectionInput
|
|
429
|
+
id="github-connection-field-a"
|
|
430
|
+
connectionId="github"
|
|
431
|
+
fieldSlot="a"
|
|
432
|
+
placeholder="e.g. my-awesome-app"
|
|
433
|
+
value={githubAppName}
|
|
434
|
+
onChange={(e) => setGithubAppName(e.target.value)}
|
|
435
|
+
/>
|
|
436
|
+
</div>
|
|
437
|
+
<div>
|
|
438
|
+
<label
|
|
439
|
+
htmlFor="github-connection-field-b"
|
|
440
|
+
className="block text-xs font-medium text-muted mb-1.5"
|
|
441
|
+
>
|
|
442
|
+
Client ID
|
|
443
|
+
</label>
|
|
444
|
+
<IntegrationConnectionInput
|
|
445
|
+
id="github-connection-field-b"
|
|
446
|
+
connectionId="github"
|
|
447
|
+
fieldSlot="b"
|
|
448
|
+
placeholder="Paste value from GitHub app"
|
|
449
|
+
value={githubClientId}
|
|
450
|
+
onChange={(e) => setGithubClientId(e.target.value)}
|
|
451
|
+
/>
|
|
452
|
+
</div>
|
|
453
|
+
<div>
|
|
454
|
+
<label
|
|
455
|
+
htmlFor="github-connection-field-c"
|
|
456
|
+
className="block text-xs font-medium text-muted mb-1.5"
|
|
457
|
+
>
|
|
458
|
+
Client Secret
|
|
459
|
+
</label>
|
|
460
|
+
<IntegrationConnectionInput
|
|
461
|
+
id="github-connection-field-c"
|
|
462
|
+
connectionId="github"
|
|
463
|
+
fieldSlot="c"
|
|
464
|
+
mode="secret"
|
|
465
|
+
placeholder="Paste value from GitHub app"
|
|
466
|
+
value={githubClientSecret}
|
|
467
|
+
onChange={(e) => setGithubClientSecret(e.target.value)}
|
|
468
|
+
/>
|
|
469
|
+
</div>
|
|
470
|
+
</div>
|
|
471
|
+
</div>
|
|
472
|
+
)}
|
|
473
|
+
</Modal>
|
|
474
|
+
);
|
|
475
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Integration } from '@playrunner/integration-sdk';
|
|
2
|
+
import { GithubSettingsModal } from './GithubSettingsModal';
|
|
3
|
+
import { GithubIcon } from './GithubIcon';
|
|
4
|
+
import { refreshGithubTokenIfNeeded } from './tokenRefresh';
|
|
5
|
+
|
|
6
|
+
export const githubIntegration: Integration = {
|
|
7
|
+
id: 'github',
|
|
8
|
+
name: 'GitHub',
|
|
9
|
+
category: 'Version Control',
|
|
10
|
+
description: 'Sync repositories and trigger actions',
|
|
11
|
+
icon: GithubIcon,
|
|
12
|
+
nodeType: 'trigger',
|
|
13
|
+
iconRenderMode: 'mask',
|
|
14
|
+
getAuthPath: (uid) => `users/${uid}/integrations/github`,
|
|
15
|
+
SettingsModal: GithubSettingsModal,
|
|
16
|
+
refreshStoredIntegration: refreshGithubTokenIfNeeded,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export default githubIntegration;
|
|
20
|
+
|
|
21
|
+
export { GithubIcon } from './GithubIcon';
|
|
22
|
+
export { GithubSettingsModal } from './GithubSettingsModal';
|
|
23
|
+
export { githubIconUrl } from './icon';
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { IntegrationRefreshContext } from '@playrunner/integration-sdk';
|
|
2
|
+
|
|
3
|
+
function isBadGithubRefreshTokenError(tokenData: any) {
|
|
4
|
+
return tokenData?.error === 'bad_refresh_token';
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export async function refreshGithubTokenIfNeeded({
|
|
8
|
+
integrationData,
|
|
9
|
+
getApiHeaders,
|
|
10
|
+
saveIntegration,
|
|
11
|
+
deleteIntegration,
|
|
12
|
+
}: IntegrationRefreshContext) {
|
|
13
|
+
if (
|
|
14
|
+
!integrationData ||
|
|
15
|
+
!integrationData.accessToken ||
|
|
16
|
+
!integrationData.refreshToken ||
|
|
17
|
+
!integrationData.expiresAt
|
|
18
|
+
) {
|
|
19
|
+
return integrationData;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (Date.now() + 5 * 60 * 1000 <= integrationData.expiresAt) {
|
|
23
|
+
return integrationData;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
console.log('GitHub token is expired or expiring soon, refreshing...');
|
|
28
|
+
const res = await fetch('/api/github/refresh', {
|
|
29
|
+
method: 'POST',
|
|
30
|
+
headers: {
|
|
31
|
+
'Content-Type': 'application/json',
|
|
32
|
+
...(await getApiHeaders()),
|
|
33
|
+
},
|
|
34
|
+
body: JSON.stringify({
|
|
35
|
+
refresh_token: integrationData.refreshToken,
|
|
36
|
+
client_id: integrationData.clientId,
|
|
37
|
+
client_secret: integrationData.clientSecret,
|
|
38
|
+
}),
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const tokenData = await res.json();
|
|
42
|
+
|
|
43
|
+
if (tokenData.access_token) {
|
|
44
|
+
integrationData.accessToken = tokenData.access_token;
|
|
45
|
+
if (tokenData.refresh_token) {
|
|
46
|
+
integrationData.refreshToken = tokenData.refresh_token;
|
|
47
|
+
}
|
|
48
|
+
if (tokenData.expires_in) {
|
|
49
|
+
integrationData.expiresAt = Date.now() + tokenData.expires_in * 1000;
|
|
50
|
+
}
|
|
51
|
+
if (tokenData.refresh_token_expires_in) {
|
|
52
|
+
integrationData.refreshTokenExpiresAt =
|
|
53
|
+
Date.now() + tokenData.refresh_token_expires_in * 1000;
|
|
54
|
+
}
|
|
55
|
+
integrationData.updatedAt = new Date().toISOString();
|
|
56
|
+
|
|
57
|
+
await saveIntegration(integrationData);
|
|
58
|
+
console.log('GitHub token refreshed successfully.');
|
|
59
|
+
return integrationData;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (isBadGithubRefreshTokenError(tokenData)) {
|
|
63
|
+
console.warn(
|
|
64
|
+
'GitHub refresh token is invalid or expired. Clearing the saved GitHub connection; reconnect GitHub to continue.',
|
|
65
|
+
);
|
|
66
|
+
await deleteIntegration();
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
console.error(
|
|
71
|
+
'Failed to refresh GitHub token, no access_token returned:',
|
|
72
|
+
tokenData,
|
|
73
|
+
);
|
|
74
|
+
} catch (error) {
|
|
75
|
+
console.error('Failed to refresh GitHub token:', error);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return integrationData;
|
|
79
|
+
}
|