@prodbeam/mcp 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +29 -0
- package/README.md +626 -67
- package/dist/auth/app-config.d.ts +8 -0
- package/dist/auth/app-config.d.ts.map +1 -0
- package/dist/auth/app-config.js +32 -0
- package/dist/auth/app-config.js.map +1 -0
- package/dist/auth/auth-provider.d.ts +9 -0
- package/dist/auth/auth-provider.d.ts.map +1 -0
- package/dist/auth/auth-provider.js +173 -0
- package/dist/auth/auth-provider.js.map +1 -0
- package/dist/auth/github-device-flow.d.ts +5 -0
- package/dist/auth/github-device-flow.d.ts.map +1 -0
- package/dist/auth/github-device-flow.js +139 -0
- package/dist/auth/github-device-flow.js.map +1 -0
- package/dist/auth/jira-oauth-flow.d.ts +19 -0
- package/dist/auth/jira-oauth-flow.d.ts.map +1 -0
- package/dist/auth/jira-oauth-flow.js +210 -0
- package/dist/auth/jira-oauth-flow.js.map +1 -0
- package/dist/auth/token-store.d.ts +7 -0
- package/dist/auth/token-store.d.ts.map +1 -0
- package/dist/auth/token-store.js +74 -0
- package/dist/auth/token-store.js.map +1 -0
- package/dist/auth/types.d.ts +51 -0
- package/dist/auth/types.d.ts.map +1 -0
- package/dist/auth/types.js +2 -0
- package/dist/auth/types.js.map +1 -0
- package/dist/cli.js +403 -64
- package/dist/cli.js.map +1 -1
- package/dist/clients/github-client.d.ts +2 -2
- package/dist/clients/github-client.d.ts.map +1 -1
- package/dist/clients/github-client.js +14 -4
- package/dist/clients/github-client.js.map +1 -1
- package/dist/clients/jira-client.d.ts +9 -3
- package/dist/clients/jira-client.d.ts.map +1 -1
- package/dist/clients/jira-client.js +53 -10
- package/dist/clients/jira-client.js.map +1 -1
- package/dist/clients/types.d.ts +21 -0
- package/dist/clients/types.d.ts.map +1 -1
- package/dist/commands/auth.d.ts +4 -0
- package/dist/commands/auth.d.ts.map +1 -0
- package/dist/commands/auth.js +254 -0
- package/dist/commands/auth.js.map +1 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +45 -1
- package/dist/commands/init.js.map +1 -1
- package/dist/config/credentials.d.ts +2 -0
- package/dist/config/credentials.d.ts.map +1 -1
- package/dist/config/credentials.js +6 -0
- package/dist/config/credentials.js.map +1 -1
- package/dist/generators/metrics-calculator.d.ts.map +1 -1
- package/dist/generators/metrics-calculator.js +28 -0
- package/dist/generators/metrics-calculator.js.map +1 -1
- package/dist/generators/report-generator.d.ts +2 -1
- package/dist/generators/report-generator.d.ts.map +1 -1
- package/dist/generators/report-generator.js +565 -131
- package/dist/generators/report-generator.js.map +1 -1
- package/dist/index.js +275 -89
- package/dist/index.js.map +1 -1
- package/dist/insights/content-insights.d.ts +46 -0
- package/dist/insights/content-insights.d.ts.map +1 -0
- package/dist/insights/content-insights.js +193 -0
- package/dist/insights/content-insights.js.map +1 -0
- package/dist/orchestrator/data-fetcher.d.ts +3 -1
- package/dist/orchestrator/data-fetcher.d.ts.map +1 -1
- package/dist/orchestrator/data-fetcher.js +15 -0
- package/dist/orchestrator/data-fetcher.js.map +1 -1
- package/dist/types/github.d.ts +3 -0
- package/dist/types/github.d.ts.map +1 -1
- package/dist/types/jira.d.ts +9 -0
- package/dist/types/jira.d.ts.map +1 -1
- package/dist/types/retrospective.d.ts +15 -0
- package/dist/types/retrospective.d.ts.map +1 -1
- package/dist/types/weekly.d.ts +7 -0
- package/dist/types/weekly.d.ts.map +1 -1
- package/dist/validators.d.ts +6 -6
- package/package.json +2 -1
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare const GITHUB_CLIENT_ID = "Iv23liR2346KoRqEUAyK";
|
|
2
|
+
export declare const GITHUB_SCOPES: string[];
|
|
3
|
+
export declare const JIRA_CLIENT_ID = "CpFTSfXTqJc5JYuMYxsf0KXgbCs8Aeg6";
|
|
4
|
+
export declare const JIRA_CLIENT_SECRET = "ATOAnpwKOU-Nkq8jOd3TKEbYRSnXbB8pIuHNKc15ouBfmuimsywEvWIiGNdo4zbQQElI9ABDB926";
|
|
5
|
+
export declare const JIRA_SCOPES: string[];
|
|
6
|
+
export declare const JIRA_CALLBACK_PORT = 19274;
|
|
7
|
+
export declare const JIRA_REDIRECT_URI = "http://localhost:19274/callback";
|
|
8
|
+
//# sourceMappingURL=app-config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"app-config.d.ts","sourceRoot":"","sources":["../../src/auth/app-config.ts"],"names":[],"mappings":"AAmBA,eAAO,MAAM,gBAAgB,yBAAyB,CAAC;AAGvD,eAAO,MAAM,aAAa,UAAoC,CAAC;AAK/D,eAAO,MAAM,cAAc,qCAAqC,CAAC;AAGjE,eAAO,MAAM,kBAAkB,iFACiD,CAAC;AAGjF,eAAO,MAAM,WAAW,UA2BvB,CAAC;AAGF,eAAO,MAAM,kBAAkB,QAAQ,CAAC;AAGxC,eAAO,MAAM,iBAAiB,oCAAoD,CAAC"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export const GITHUB_CLIENT_ID = 'Iv23liR2346KoRqEUAyK';
|
|
2
|
+
export const GITHUB_SCOPES = ['repo', 'read:org', 'read:user'];
|
|
3
|
+
export const JIRA_CLIENT_ID = 'CpFTSfXTqJc5JYuMYxsf0KXgbCs8Aeg6';
|
|
4
|
+
export const JIRA_CLIENT_SECRET = 'ATOAnpwKOU-Nkq8jOd3TKEbYRSnXbB8pIuHNKc15ouBfmuimsywEvWIiGNdo4zbQQElI9ABDB926';
|
|
5
|
+
export const JIRA_SCOPES = [
|
|
6
|
+
'read:jira-work',
|
|
7
|
+
'read:jira-user',
|
|
8
|
+
'offline_access',
|
|
9
|
+
'read:issue:jira',
|
|
10
|
+
'read:issue-details:jira',
|
|
11
|
+
'read:issue-status:jira',
|
|
12
|
+
'read:issue-type:jira',
|
|
13
|
+
'read:comment:jira',
|
|
14
|
+
'read:user:jira',
|
|
15
|
+
'read:email-address:jira',
|
|
16
|
+
'read:label:jira',
|
|
17
|
+
'read:project:jira',
|
|
18
|
+
'read:project.component:jira',
|
|
19
|
+
'read:project-type:jira',
|
|
20
|
+
'read:jql:jira',
|
|
21
|
+
'read:dashboard:jira',
|
|
22
|
+
'read:deployment-info:jira',
|
|
23
|
+
'read:dev-info:jira',
|
|
24
|
+
'read:board-scope:jira-software',
|
|
25
|
+
'read:sprint:jira-software',
|
|
26
|
+
'read:epic:jira-software',
|
|
27
|
+
'read:issue:jira-software',
|
|
28
|
+
'read:deployment:jira-software',
|
|
29
|
+
];
|
|
30
|
+
export const JIRA_CALLBACK_PORT = 19274;
|
|
31
|
+
export const JIRA_REDIRECT_URI = `http://localhost:${JIRA_CALLBACK_PORT}/callback`;
|
|
32
|
+
//# sourceMappingURL=app-config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"app-config.js","sourceRoot":"","sources":["../../src/auth/app-config.ts"],"names":[],"mappings":"AAmBA,MAAM,CAAC,MAAM,gBAAgB,GAAG,sBAAsB,CAAC;AAGvD,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,MAAM,EAAE,UAAU,EAAE,WAAW,CAAC,CAAC;AAK/D,MAAM,CAAC,MAAM,cAAc,GAAG,kCAAkC,CAAC;AAGjE,MAAM,CAAC,MAAM,kBAAkB,GAC7B,8EAA8E,CAAC;AAGjF,MAAM,CAAC,MAAM,WAAW,GAAG;IAEzB,gBAAgB;IAChB,gBAAgB;IAChB,gBAAgB;IAEhB,iBAAiB;IACjB,yBAAyB;IACzB,wBAAwB;IACxB,sBAAsB;IACtB,mBAAmB;IACnB,gBAAgB;IAChB,yBAAyB;IACzB,iBAAiB;IACjB,mBAAmB;IACnB,6BAA6B;IAC7B,wBAAwB;IACxB,eAAe;IACf,qBAAqB;IACrB,2BAA2B;IAC3B,oBAAoB;IAEpB,gCAAgC;IAChC,2BAA2B;IAC3B,yBAAyB;IACzB,0BAA0B;IAC1B,+BAA+B;CAChC,CAAC;AAGF,MAAM,CAAC,MAAM,kBAAkB,GAAG,KAAK,CAAC;AAGxC,MAAM,CAAC,MAAM,iBAAiB,GAAG,oBAAoB,kBAAkB,WAAW,CAAC"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ResolvedGitHubAuth, ResolvedJiraAuth, AuthStatus } from './types.js';
|
|
2
|
+
export declare class AuthExpiredError extends Error {
|
|
3
|
+
service: 'github' | 'jira';
|
|
4
|
+
constructor(service: 'github' | 'jira');
|
|
5
|
+
}
|
|
6
|
+
export declare function resolveGitHubAuth(): Promise<ResolvedGitHubAuth | null>;
|
|
7
|
+
export declare function resolveJiraAuth(): Promise<ResolvedJiraAuth | null>;
|
|
8
|
+
export declare function getAuthStatuses(): AuthStatus[];
|
|
9
|
+
//# sourceMappingURL=auth-provider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth-provider.d.ts","sourceRoot":"","sources":["../../src/auth/auth-provider.ts"],"names":[],"mappings":"AAiCA,OAAO,KAAK,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAOnF,qBAAa,gBAAiB,SAAQ,KAAK;IACtB,OAAO,EAAE,QAAQ,GAAG,MAAM;gBAA1B,OAAO,EAAE,QAAQ,GAAG,MAAM;CAO9C;AASD,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC,CA2C5E;AASD,wBAAsB,eAAe,IAAI,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAqExE;AAQD,wBAAgB,eAAe,IAAI,UAAU,EAAE,CAoE9C"}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { resolveGitHubCredentials, resolveJiraCredentials, isGitHubFromEnv, isJiraFromEnv, } from '../config/credentials.js';
|
|
2
|
+
import { readGitHubOAuthTokens, readJiraOAuthTokens, writeGitHubOAuthTokens, writeJiraOAuthTokens, } from './token-store.js';
|
|
3
|
+
import { refreshGitHubToken } from './github-device-flow.js';
|
|
4
|
+
import { refreshJiraToken } from './jira-oauth-flow.js';
|
|
5
|
+
import { GITHUB_CLIENT_ID, JIRA_CLIENT_ID, JIRA_CLIENT_SECRET } from './app-config.js';
|
|
6
|
+
const REFRESH_BUFFER_MS = 5 * 60 * 1000;
|
|
7
|
+
export class AuthExpiredError extends Error {
|
|
8
|
+
service;
|
|
9
|
+
constructor(service) {
|
|
10
|
+
super(`${service === 'github' ? 'GitHub' : 'Jira'} OAuth session expired. ` +
|
|
11
|
+
'Run "prodbeam auth login" to re-authenticate.');
|
|
12
|
+
this.service = service;
|
|
13
|
+
this.name = 'AuthExpiredError';
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export async function resolveGitHubAuth() {
|
|
17
|
+
if (isGitHubFromEnv()) {
|
|
18
|
+
const creds = resolveGitHubCredentials();
|
|
19
|
+
if (creds) {
|
|
20
|
+
return { token: creds.token, method: 'pat' };
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
const oauthTokens = readGitHubOAuthTokens();
|
|
24
|
+
if (oauthTokens) {
|
|
25
|
+
const now = Date.now();
|
|
26
|
+
const accessExpiry = new Date(oauthTokens.accessTokenExpiresAt).getTime();
|
|
27
|
+
const refreshExpiry = new Date(oauthTokens.refreshTokenExpiresAt).getTime();
|
|
28
|
+
if (accessExpiry - now > REFRESH_BUFFER_MS) {
|
|
29
|
+
return { token: oauthTokens.accessToken, method: 'oauth' };
|
|
30
|
+
}
|
|
31
|
+
if (refreshExpiry > now) {
|
|
32
|
+
try {
|
|
33
|
+
const refreshed = await refreshGitHubToken(GITHUB_CLIENT_ID, oauthTokens.refreshToken);
|
|
34
|
+
writeGitHubOAuthTokens(refreshed);
|
|
35
|
+
return { token: refreshed.accessToken, method: 'oauth' };
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
throw new AuthExpiredError('github');
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
throw new AuthExpiredError('github');
|
|
42
|
+
}
|
|
43
|
+
const patCreds = resolveGitHubCredentials();
|
|
44
|
+
if (patCreds) {
|
|
45
|
+
return { token: patCreds.token, method: 'pat' };
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
export async function resolveJiraAuth() {
|
|
50
|
+
if (isJiraFromEnv()) {
|
|
51
|
+
const creds = resolveJiraCredentials();
|
|
52
|
+
if (creds) {
|
|
53
|
+
const baseUrl = creds.host.startsWith('https://') ? creds.host : `https://${creds.host}`;
|
|
54
|
+
const authHeader = `Basic ${Buffer.from(`${creds.email}:${creds.apiToken}`).toString('base64')}`;
|
|
55
|
+
return { baseUrl: baseUrl.replace(/\/$/, ''), authHeader, method: 'pat' };
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const oauthTokens = readJiraOAuthTokens();
|
|
59
|
+
if (oauthTokens) {
|
|
60
|
+
const now = Date.now();
|
|
61
|
+
const accessExpiry = new Date(oauthTokens.accessTokenExpiresAt).getTime();
|
|
62
|
+
const refreshExpiry = new Date(oauthTokens.refreshTokenExpiresAt).getTime();
|
|
63
|
+
if (accessExpiry - now > REFRESH_BUFFER_MS) {
|
|
64
|
+
return {
|
|
65
|
+
baseUrl: oauthTokens.cloudUrl,
|
|
66
|
+
authHeader: `Bearer ${oauthTokens.accessToken}`,
|
|
67
|
+
method: 'oauth',
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
if (refreshExpiry > now) {
|
|
71
|
+
try {
|
|
72
|
+
const result = await refreshJiraToken(JIRA_CLIENT_ID, JIRA_CLIENT_SECRET, oauthTokens.refreshToken);
|
|
73
|
+
const refreshed = {
|
|
74
|
+
...oauthTokens,
|
|
75
|
+
accessToken: result.accessToken,
|
|
76
|
+
refreshToken: result.refreshToken,
|
|
77
|
+
accessTokenExpiresAt: new Date(Date.now() + result.expiresIn * 1000).toISOString(),
|
|
78
|
+
refreshTokenExpiresAt: new Date(Date.now() + 90 * 24 * 60 * 60 * 1000).toISOString(),
|
|
79
|
+
};
|
|
80
|
+
writeJiraOAuthTokens(refreshed);
|
|
81
|
+
return {
|
|
82
|
+
baseUrl: refreshed.cloudUrl,
|
|
83
|
+
authHeader: `Bearer ${refreshed.accessToken}`,
|
|
84
|
+
method: 'oauth',
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
throw new AuthExpiredError('jira');
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
throw new AuthExpiredError('jira');
|
|
92
|
+
}
|
|
93
|
+
const patCreds = resolveJiraCredentials();
|
|
94
|
+
if (patCreds) {
|
|
95
|
+
const baseUrl = patCreds.host.startsWith('https://')
|
|
96
|
+
? patCreds.host
|
|
97
|
+
: `https://${patCreds.host}`;
|
|
98
|
+
const authHeader = `Basic ${Buffer.from(`${patCreds.email}:${patCreds.apiToken}`).toString('base64')}`;
|
|
99
|
+
return { baseUrl: baseUrl.replace(/\/$/, ''), authHeader, method: 'pat' };
|
|
100
|
+
}
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
export function getAuthStatuses() {
|
|
104
|
+
const statuses = [];
|
|
105
|
+
if (isGitHubFromEnv()) {
|
|
106
|
+
statuses.push({ service: 'github', method: 'pat', valid: true });
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
const oauthTokens = readGitHubOAuthTokens();
|
|
110
|
+
if (oauthTokens) {
|
|
111
|
+
const now = Date.now();
|
|
112
|
+
const refreshExpiry = new Date(oauthTokens.refreshTokenExpiresAt).getTime();
|
|
113
|
+
const valid = refreshExpiry > now;
|
|
114
|
+
statuses.push({
|
|
115
|
+
service: 'github',
|
|
116
|
+
method: 'oauth',
|
|
117
|
+
expiresAt: oauthTokens.accessTokenExpiresAt,
|
|
118
|
+
refreshExpiresAt: oauthTokens.refreshTokenExpiresAt,
|
|
119
|
+
valid,
|
|
120
|
+
error: valid ? undefined : 'Refresh token expired',
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
const patCreds = resolveGitHubCredentials();
|
|
125
|
+
if (patCreds) {
|
|
126
|
+
statuses.push({ service: 'github', method: 'pat', valid: true });
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
statuses.push({
|
|
130
|
+
service: 'github',
|
|
131
|
+
method: 'pat',
|
|
132
|
+
valid: false,
|
|
133
|
+
error: 'Not configured',
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (isJiraFromEnv()) {
|
|
139
|
+
statuses.push({ service: 'jira', method: 'pat', valid: true });
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
const oauthTokens = readJiraOAuthTokens();
|
|
143
|
+
if (oauthTokens) {
|
|
144
|
+
const now = Date.now();
|
|
145
|
+
const refreshExpiry = new Date(oauthTokens.refreshTokenExpiresAt).getTime();
|
|
146
|
+
const valid = refreshExpiry > now;
|
|
147
|
+
statuses.push({
|
|
148
|
+
service: 'jira',
|
|
149
|
+
method: 'oauth',
|
|
150
|
+
expiresAt: oauthTokens.accessTokenExpiresAt,
|
|
151
|
+
refreshExpiresAt: oauthTokens.refreshTokenExpiresAt,
|
|
152
|
+
valid,
|
|
153
|
+
error: valid ? undefined : 'Refresh token expired',
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
const patCreds = resolveJiraCredentials();
|
|
158
|
+
if (patCreds) {
|
|
159
|
+
statuses.push({ service: 'jira', method: 'pat', valid: true });
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
statuses.push({
|
|
163
|
+
service: 'jira',
|
|
164
|
+
method: 'pat',
|
|
165
|
+
valid: false,
|
|
166
|
+
error: 'Not configured (optional)',
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return statuses;
|
|
172
|
+
}
|
|
173
|
+
//# sourceMappingURL=auth-provider.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth-provider.js","sourceRoot":"","sources":["../../src/auth/auth-provider.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { GitHubDeviceCodeResponse, GitHubOAuthTokens } from './types.js';
|
|
2
|
+
export declare function requestDeviceCode(clientId: string, scopes: string[]): Promise<GitHubDeviceCodeResponse>;
|
|
3
|
+
export declare function pollForToken(clientId: string, deviceCode: string, interval: number, expiresIn: number): Promise<GitHubOAuthTokens>;
|
|
4
|
+
export declare function refreshGitHubToken(clientId: string, refreshToken: string): Promise<GitHubOAuthTokens>;
|
|
5
|
+
//# sourceMappingURL=github-device-flow.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"github-device-flow.d.ts","sourceRoot":"","sources":["../../src/auth/github-device-flow.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,wBAAwB,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAY9E,wBAAsB,iBAAiB,CACrC,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,EAAE,GACf,OAAO,CAAC,wBAAwB,CAAC,CAoCnC;AAUD,wBAAsB,YAAY,CAChC,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,iBAAiB,CAAC,CAwE5B;AAQD,wBAAsB,kBAAkB,CACtC,QAAQ,EAAE,MAAM,EAChB,YAAY,EAAE,MAAM,GACnB,OAAO,CAAC,iBAAiB,CAAC,CA6C5B"}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
const DEVICE_CODE_URL = 'https://github.com/login/device/code';
|
|
2
|
+
const ACCESS_TOKEN_URL = 'https://github.com/login/oauth/access_token';
|
|
3
|
+
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
4
|
+
export async function requestDeviceCode(clientId, scopes) {
|
|
5
|
+
const controller = new AbortController();
|
|
6
|
+
const timeout = setTimeout(() => controller.abort(), DEFAULT_TIMEOUT_MS);
|
|
7
|
+
try {
|
|
8
|
+
const response = await fetch(DEVICE_CODE_URL, {
|
|
9
|
+
method: 'POST',
|
|
10
|
+
headers: {
|
|
11
|
+
Accept: 'application/json',
|
|
12
|
+
'Content-Type': 'application/json',
|
|
13
|
+
},
|
|
14
|
+
body: JSON.stringify({
|
|
15
|
+
client_id: clientId,
|
|
16
|
+
scope: scopes.join(' '),
|
|
17
|
+
}),
|
|
18
|
+
signal: controller.signal,
|
|
19
|
+
});
|
|
20
|
+
if (!response.ok) {
|
|
21
|
+
throw new Error(`GitHub device code request failed: ${response.status} ${response.statusText}`);
|
|
22
|
+
}
|
|
23
|
+
const data = (await response.json());
|
|
24
|
+
return {
|
|
25
|
+
deviceCode: data['device_code'],
|
|
26
|
+
userCode: data['user_code'],
|
|
27
|
+
verificationUri: data['verification_uri'],
|
|
28
|
+
interval: data['interval'] ?? 5,
|
|
29
|
+
expiresIn: data['expires_in'] ?? 900,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
finally {
|
|
33
|
+
clearTimeout(timeout);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
export async function pollForToken(clientId, deviceCode, interval, expiresIn) {
|
|
37
|
+
const deadline = Date.now() + expiresIn * 1000;
|
|
38
|
+
let pollInterval = interval;
|
|
39
|
+
while (Date.now() < deadline) {
|
|
40
|
+
await sleep(pollInterval * 1000);
|
|
41
|
+
const controller = new AbortController();
|
|
42
|
+
const timeout = setTimeout(() => controller.abort(), DEFAULT_TIMEOUT_MS);
|
|
43
|
+
try {
|
|
44
|
+
const response = await fetch(ACCESS_TOKEN_URL, {
|
|
45
|
+
method: 'POST',
|
|
46
|
+
headers: {
|
|
47
|
+
Accept: 'application/json',
|
|
48
|
+
'Content-Type': 'application/json',
|
|
49
|
+
},
|
|
50
|
+
body: JSON.stringify({
|
|
51
|
+
client_id: clientId,
|
|
52
|
+
device_code: deviceCode,
|
|
53
|
+
grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
|
|
54
|
+
}),
|
|
55
|
+
signal: controller.signal,
|
|
56
|
+
});
|
|
57
|
+
const data = (await response.json());
|
|
58
|
+
const error = data['error'];
|
|
59
|
+
if (error === 'authorization_pending') {
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (error === 'slow_down') {
|
|
63
|
+
pollInterval += 5;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (error === 'expired_token') {
|
|
67
|
+
throw new Error('Device code expired. Please restart the authentication flow.');
|
|
68
|
+
}
|
|
69
|
+
if (error === 'access_denied') {
|
|
70
|
+
throw new Error('User denied the authorization request.');
|
|
71
|
+
}
|
|
72
|
+
if (error) {
|
|
73
|
+
throw new Error(`GitHub OAuth error: ${error}`);
|
|
74
|
+
}
|
|
75
|
+
const accessToken = data['access_token'];
|
|
76
|
+
const refreshToken = data['refresh_token'];
|
|
77
|
+
const expiresInSec = data['expires_in'] ?? 28800;
|
|
78
|
+
const refreshExpiresInSec = data['refresh_token_expires_in'] ?? 15811200;
|
|
79
|
+
const now = new Date();
|
|
80
|
+
return {
|
|
81
|
+
method: 'oauth',
|
|
82
|
+
accessToken,
|
|
83
|
+
refreshToken,
|
|
84
|
+
accessTokenExpiresAt: new Date(now.getTime() + expiresInSec * 1000).toISOString(),
|
|
85
|
+
refreshTokenExpiresAt: new Date(now.getTime() + refreshExpiresInSec * 1000).toISOString(),
|
|
86
|
+
scopes: (data['scope'] ?? '').split(',').filter(Boolean),
|
|
87
|
+
tokenType: 'bearer',
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
finally {
|
|
91
|
+
clearTimeout(timeout);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
throw new Error('Device code expired. Please restart the authentication flow.');
|
|
95
|
+
}
|
|
96
|
+
export async function refreshGitHubToken(clientId, refreshToken) {
|
|
97
|
+
const controller = new AbortController();
|
|
98
|
+
const timeout = setTimeout(() => controller.abort(), DEFAULT_TIMEOUT_MS);
|
|
99
|
+
try {
|
|
100
|
+
const response = await fetch(ACCESS_TOKEN_URL, {
|
|
101
|
+
method: 'POST',
|
|
102
|
+
headers: {
|
|
103
|
+
Accept: 'application/json',
|
|
104
|
+
'Content-Type': 'application/json',
|
|
105
|
+
},
|
|
106
|
+
body: JSON.stringify({
|
|
107
|
+
client_id: clientId,
|
|
108
|
+
grant_type: 'refresh_token',
|
|
109
|
+
refresh_token: refreshToken,
|
|
110
|
+
}),
|
|
111
|
+
signal: controller.signal,
|
|
112
|
+
});
|
|
113
|
+
const data = (await response.json());
|
|
114
|
+
if (data['error']) {
|
|
115
|
+
throw new Error(`GitHub token refresh failed: ${String(data['error'])} — ${String(data['error_description'] ?? '')}`);
|
|
116
|
+
}
|
|
117
|
+
const accessToken = data['access_token'];
|
|
118
|
+
const newRefreshToken = data['refresh_token'];
|
|
119
|
+
const expiresInSec = data['expires_in'] ?? 28800;
|
|
120
|
+
const refreshExpiresInSec = data['refresh_token_expires_in'] ?? 15811200;
|
|
121
|
+
const now = new Date();
|
|
122
|
+
return {
|
|
123
|
+
method: 'oauth',
|
|
124
|
+
accessToken,
|
|
125
|
+
refreshToken: newRefreshToken,
|
|
126
|
+
accessTokenExpiresAt: new Date(now.getTime() + expiresInSec * 1000).toISOString(),
|
|
127
|
+
refreshTokenExpiresAt: new Date(now.getTime() + refreshExpiresInSec * 1000).toISOString(),
|
|
128
|
+
scopes: (data['scope'] ?? '').split(',').filter(Boolean),
|
|
129
|
+
tokenType: 'bearer',
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
finally {
|
|
133
|
+
clearTimeout(timeout);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
function sleep(ms) {
|
|
137
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
138
|
+
}
|
|
139
|
+
//# sourceMappingURL=github-device-flow.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"github-device-flow.js","sourceRoot":"","sources":["../../src/auth/github-device-flow.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { JiraOAuthTokens, JiraCloudResource } from './types.js';
|
|
2
|
+
export declare function buildAuthorizationUrl(clientId: string, redirectUri: string, state: string, scopes: string[]): string;
|
|
3
|
+
export declare function generateState(): string;
|
|
4
|
+
export declare function waitForAuthCode(port: number, state: string, timeoutMs?: number): Promise<string>;
|
|
5
|
+
export declare function exchangeCodeForTokens(clientId: string, clientSecret: string, code: string, redirectUri: string): Promise<{
|
|
6
|
+
accessToken: string;
|
|
7
|
+
refreshToken: string;
|
|
8
|
+
expiresIn: number;
|
|
9
|
+
scope: string;
|
|
10
|
+
}>;
|
|
11
|
+
export declare function discoverCloudResources(accessToken: string): Promise<JiraCloudResource[]>;
|
|
12
|
+
export declare function refreshJiraToken(clientId: string, clientSecret: string, refreshToken: string): Promise<{
|
|
13
|
+
accessToken: string;
|
|
14
|
+
refreshToken: string;
|
|
15
|
+
expiresIn: number;
|
|
16
|
+
scope: string;
|
|
17
|
+
}>;
|
|
18
|
+
export declare function completeJiraOAuthFlow(clientId: string, clientSecret: string, code: string, redirectUri: string, scopes: string[]): Promise<JiraOAuthTokens>;
|
|
19
|
+
//# sourceMappingURL=jira-oauth-flow.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"jira-oauth-flow.d.ts","sourceRoot":"","sources":["../../src/auth/jira-oauth-flow.ts"],"names":[],"mappings":"AAmBA,OAAO,KAAK,EAAE,eAAe,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAarE,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,MAAM,EACnB,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,EAAE,GACf,MAAM,CAYR;AAKD,wBAAgB,aAAa,IAAI,MAAM,CAEtC;AAaD,wBAAsB,eAAe,CACnC,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,EACb,SAAS,SAAU,GAClB,OAAO,CAAC,MAAM,CAAC,CAoEjB;AAOD,wBAAsB,qBAAqB,CACzC,QAAQ,EAAE,MAAM,EAChB,YAAY,EAAE,MAAM,EACpB,IAAI,EAAE,MAAM,EACZ,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,CAiC1F;AAQD,wBAAsB,sBAAsB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,EAAE,CAAC,CA0B9F;AAQD,wBAAsB,gBAAgB,CACpC,QAAQ,EAAE,MAAM,EAChB,YAAY,EAAE,MAAM,EACpB,YAAY,EAAE,MAAM,GACnB,OAAO,CAAC;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,CAgC1F;AASD,wBAAsB,qBAAqB,CACzC,QAAQ,EAAE,MAAM,EAChB,YAAY,EAAE,MAAM,EACpB,IAAI,EAAE,MAAM,EACZ,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,MAAM,EAAE,GACf,OAAO,CAAC,eAAe,CAAC,CA0B1B"}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { createServer } from 'node:http';
|
|
2
|
+
import { randomBytes } from 'node:crypto';
|
|
3
|
+
const AUTH_URL = 'https://auth.atlassian.com/authorize';
|
|
4
|
+
const TOKEN_URL = 'https://auth.atlassian.com/oauth/token';
|
|
5
|
+
const RESOURCES_URL = 'https://api.atlassian.com/oauth/token/accessible-resources';
|
|
6
|
+
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
7
|
+
export function buildAuthorizationUrl(clientId, redirectUri, state, scopes) {
|
|
8
|
+
const params = new URLSearchParams({
|
|
9
|
+
audience: 'api.atlassian.com',
|
|
10
|
+
client_id: clientId,
|
|
11
|
+
scope: scopes.join(' '),
|
|
12
|
+
redirect_uri: redirectUri,
|
|
13
|
+
state,
|
|
14
|
+
response_type: 'code',
|
|
15
|
+
prompt: 'consent',
|
|
16
|
+
});
|
|
17
|
+
return `${AUTH_URL}?${params.toString()}`;
|
|
18
|
+
}
|
|
19
|
+
export function generateState() {
|
|
20
|
+
return randomBytes(16).toString('hex');
|
|
21
|
+
}
|
|
22
|
+
export async function waitForAuthCode(port, state, timeoutMs = 120_000) {
|
|
23
|
+
return new Promise((resolve, reject) => {
|
|
24
|
+
let server;
|
|
25
|
+
const timer = setTimeout(() => {
|
|
26
|
+
server?.close();
|
|
27
|
+
reject(new Error('OAuth callback timed out. Please try again.'));
|
|
28
|
+
}, timeoutMs);
|
|
29
|
+
server = createServer((req, res) => {
|
|
30
|
+
const url = new URL(req.url ?? '/', `http://localhost:${port}`);
|
|
31
|
+
if (url.pathname !== '/callback') {
|
|
32
|
+
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
33
|
+
res.end('Not found');
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const code = url.searchParams.get('code');
|
|
37
|
+
const returnedState = url.searchParams.get('state');
|
|
38
|
+
const error = url.searchParams.get('error');
|
|
39
|
+
clearTimeout(timer);
|
|
40
|
+
if (error) {
|
|
41
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
42
|
+
res.end(errorHtml(error));
|
|
43
|
+
server.close();
|
|
44
|
+
reject(new Error(`Jira OAuth error: ${error}`));
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
if (returnedState !== state) {
|
|
48
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
49
|
+
res.end(errorHtml('State mismatch — possible CSRF attack. Please try again.'));
|
|
50
|
+
server.close();
|
|
51
|
+
reject(new Error('OAuth state mismatch — possible CSRF attack.'));
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (!code) {
|
|
55
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
56
|
+
res.end(errorHtml('No authorization code received.'));
|
|
57
|
+
server.close();
|
|
58
|
+
reject(new Error('No authorization code received from Jira.'));
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
62
|
+
res.end(successHtml());
|
|
63
|
+
server.close();
|
|
64
|
+
resolve(code);
|
|
65
|
+
});
|
|
66
|
+
server.on('error', (err) => {
|
|
67
|
+
clearTimeout(timer);
|
|
68
|
+
if (err.code === 'EADDRINUSE') {
|
|
69
|
+
reject(new Error(`Port ${port} is already in use. Close the process using it and try again.`));
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
reject(new Error(`Failed to start OAuth callback server: ${err.message}`));
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
server.listen(port, '127.0.0.1');
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
export async function exchangeCodeForTokens(clientId, clientSecret, code, redirectUri) {
|
|
79
|
+
const controller = new AbortController();
|
|
80
|
+
const timeout = setTimeout(() => controller.abort(), DEFAULT_TIMEOUT_MS);
|
|
81
|
+
try {
|
|
82
|
+
const response = await fetch(TOKEN_URL, {
|
|
83
|
+
method: 'POST',
|
|
84
|
+
headers: { 'Content-Type': 'application/json' },
|
|
85
|
+
body: JSON.stringify({
|
|
86
|
+
grant_type: 'authorization_code',
|
|
87
|
+
client_id: clientId,
|
|
88
|
+
client_secret: clientSecret,
|
|
89
|
+
code,
|
|
90
|
+
redirect_uri: redirectUri,
|
|
91
|
+
}),
|
|
92
|
+
signal: controller.signal,
|
|
93
|
+
});
|
|
94
|
+
if (!response.ok) {
|
|
95
|
+
const body = await response.text();
|
|
96
|
+
throw new Error(`Jira token exchange failed: ${response.status} — ${body}`);
|
|
97
|
+
}
|
|
98
|
+
const data = (await response.json());
|
|
99
|
+
return {
|
|
100
|
+
accessToken: data['access_token'],
|
|
101
|
+
refreshToken: data['refresh_token'],
|
|
102
|
+
expiresIn: data['expires_in'] ?? 3600,
|
|
103
|
+
scope: data['scope'] ?? '',
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
finally {
|
|
107
|
+
clearTimeout(timeout);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
export async function discoverCloudResources(accessToken) {
|
|
111
|
+
const controller = new AbortController();
|
|
112
|
+
const timeout = setTimeout(() => controller.abort(), DEFAULT_TIMEOUT_MS);
|
|
113
|
+
try {
|
|
114
|
+
const response = await fetch(RESOURCES_URL, {
|
|
115
|
+
headers: {
|
|
116
|
+
Authorization: `Bearer ${accessToken}`,
|
|
117
|
+
Accept: 'application/json',
|
|
118
|
+
},
|
|
119
|
+
signal: controller.signal,
|
|
120
|
+
});
|
|
121
|
+
if (!response.ok) {
|
|
122
|
+
throw new Error(`Cloud resource discovery failed: ${response.status} ${response.statusText}`);
|
|
123
|
+
}
|
|
124
|
+
const data = (await response.json());
|
|
125
|
+
return data.map((resource) => ({
|
|
126
|
+
id: resource['id'],
|
|
127
|
+
url: resource['url'],
|
|
128
|
+
name: resource['name'],
|
|
129
|
+
}));
|
|
130
|
+
}
|
|
131
|
+
finally {
|
|
132
|
+
clearTimeout(timeout);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
export async function refreshJiraToken(clientId, clientSecret, refreshToken) {
|
|
136
|
+
const controller = new AbortController();
|
|
137
|
+
const timeout = setTimeout(() => controller.abort(), DEFAULT_TIMEOUT_MS);
|
|
138
|
+
try {
|
|
139
|
+
const response = await fetch(TOKEN_URL, {
|
|
140
|
+
method: 'POST',
|
|
141
|
+
headers: { 'Content-Type': 'application/json' },
|
|
142
|
+
body: JSON.stringify({
|
|
143
|
+
grant_type: 'refresh_token',
|
|
144
|
+
client_id: clientId,
|
|
145
|
+
client_secret: clientSecret,
|
|
146
|
+
refresh_token: refreshToken,
|
|
147
|
+
}),
|
|
148
|
+
signal: controller.signal,
|
|
149
|
+
});
|
|
150
|
+
if (!response.ok) {
|
|
151
|
+
const body = await response.text();
|
|
152
|
+
throw new Error(`Jira token refresh failed: ${response.status} — ${body}`);
|
|
153
|
+
}
|
|
154
|
+
const data = (await response.json());
|
|
155
|
+
return {
|
|
156
|
+
accessToken: data['access_token'],
|
|
157
|
+
refreshToken: data['refresh_token'],
|
|
158
|
+
expiresIn: data['expires_in'] ?? 3600,
|
|
159
|
+
scope: data['scope'] ?? '',
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
finally {
|
|
163
|
+
clearTimeout(timeout);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
export async function completeJiraOAuthFlow(clientId, clientSecret, code, redirectUri, scopes) {
|
|
167
|
+
const tokenResult = await exchangeCodeForTokens(clientId, clientSecret, code, redirectUri);
|
|
168
|
+
const resources = await discoverCloudResources(tokenResult.accessToken);
|
|
169
|
+
if (resources.length === 0) {
|
|
170
|
+
throw new Error('No accessible Jira Cloud sites found. Ensure you have access to at least one site.');
|
|
171
|
+
}
|
|
172
|
+
const cloud = resources[0];
|
|
173
|
+
const now = new Date();
|
|
174
|
+
return {
|
|
175
|
+
method: 'oauth',
|
|
176
|
+
accessToken: tokenResult.accessToken,
|
|
177
|
+
refreshToken: tokenResult.refreshToken,
|
|
178
|
+
accessTokenExpiresAt: new Date(now.getTime() + tokenResult.expiresIn * 1000).toISOString(),
|
|
179
|
+
refreshTokenExpiresAt: new Date(now.getTime() + 90 * 24 * 60 * 60 * 1000).toISOString(),
|
|
180
|
+
cloudId: cloud.id,
|
|
181
|
+
cloudUrl: `https://api.atlassian.com/ex/jira/${cloud.id}`,
|
|
182
|
+
scopes,
|
|
183
|
+
tokenType: 'bearer',
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
function successHtml() {
|
|
187
|
+
return `<!DOCTYPE html>
|
|
188
|
+
<html><head><title>Prodbeam — Authorized</title></head>
|
|
189
|
+
<body style="font-family:system-ui;text-align:center;padding:40px">
|
|
190
|
+
<h2>Authorized</h2>
|
|
191
|
+
<p>You can close this tab and return to the terminal.</p>
|
|
192
|
+
</body></html>`;
|
|
193
|
+
}
|
|
194
|
+
function errorHtml(message) {
|
|
195
|
+
return `<!DOCTYPE html>
|
|
196
|
+
<html><head><title>Prodbeam — Error</title></head>
|
|
197
|
+
<body style="font-family:system-ui;text-align:center;padding:40px">
|
|
198
|
+
<h2>Authorization Failed</h2>
|
|
199
|
+
<p>${escapeHtml(message)}</p>
|
|
200
|
+
<p>Please return to the terminal and try again.</p>
|
|
201
|
+
</body></html>`;
|
|
202
|
+
}
|
|
203
|
+
function escapeHtml(str) {
|
|
204
|
+
return str
|
|
205
|
+
.replace(/&/g, '&')
|
|
206
|
+
.replace(/</g, '<')
|
|
207
|
+
.replace(/>/g, '>')
|
|
208
|
+
.replace(/"/g, '"');
|
|
209
|
+
}
|
|
210
|
+
//# sourceMappingURL=jira-oauth-flow.js.map
|