@sanlam-fintech-digital/mfe-platform-cli 0.0.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/README.md +271 -0
- package/dist/auth/constants.d.ts +1 -0
- package/dist/auth/constants.js +4 -0
- package/dist/auth/device-flow.d.ts +41 -0
- package/dist/auth/device-flow.js +122 -0
- package/dist/auth/orchestrator.d.ts +6 -0
- package/dist/auth/orchestrator.js +76 -0
- package/dist/auth/pkce-flow.d.ts +9 -0
- package/dist/auth/pkce-flow.js +40 -0
- package/dist/auth/pkce.d.ts +36 -0
- package/dist/auth/pkce.js +286 -0
- package/dist/auth/providers/azure-devops.d.ts +15 -0
- package/dist/auth/providers/azure-devops.js +130 -0
- package/dist/auth/providers/github.d.ts +6 -0
- package/dist/auth/providers/github.js +38 -0
- package/dist/commands/init.d.ts +10 -0
- package/dist/commands/init.js +214 -0
- package/dist/commands/update.d.ts +5 -0
- package/dist/commands/update.js +199 -0
- package/dist/config/auth-token-cache.d.ts +10 -0
- package/dist/config/auth-token-cache.js +11 -0
- package/dist/config/loader.d.ts +9 -0
- package/dist/config/loader.js +214 -0
- package/dist/feed/router.d.ts +13 -0
- package/dist/feed/router.js +58 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +49 -0
- package/dist/npmrc/manager.d.ts +19 -0
- package/dist/npmrc/manager.js +142 -0
- package/dist/nuget/manager.d.ts +40 -0
- package/dist/nuget/manager.js +145 -0
- package/dist/types/config.d.ts +150 -0
- package/dist/types/config.js +63 -0
- package/package.json +41 -0
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.buildAuthorizeUrl = buildAuthorizeUrl;
|
|
7
|
+
exports.generateCodeVerifier = generateCodeVerifier;
|
|
8
|
+
exports.generateCodeChallenge = generateCodeChallenge;
|
|
9
|
+
exports.waitForAuthCode = waitForAuthCode;
|
|
10
|
+
exports.findFreePort = findFreePort;
|
|
11
|
+
exports.exchangeCodeForTokenWithEndpoint = exchangeCodeForTokenWithEndpoint;
|
|
12
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
13
|
+
const http_1 = __importDefault(require("http"));
|
|
14
|
+
const url_1 = require("url");
|
|
15
|
+
function buildAuthorizeUrl(request, codeChallenge, state) {
|
|
16
|
+
const params = new URLSearchParams({
|
|
17
|
+
client_id: request.clientId,
|
|
18
|
+
response_type: 'code',
|
|
19
|
+
redirect_uri: request.redirectUri,
|
|
20
|
+
response_mode: 'query',
|
|
21
|
+
scope: request.scope,
|
|
22
|
+
code_challenge: codeChallenge,
|
|
23
|
+
code_challenge_method: 'S256',
|
|
24
|
+
state,
|
|
25
|
+
});
|
|
26
|
+
return `${request.authorizeEndpoint}?${params.toString()}`;
|
|
27
|
+
}
|
|
28
|
+
function generateCodeVerifier() {
|
|
29
|
+
return base64Url(crypto_1.default.randomBytes(32));
|
|
30
|
+
}
|
|
31
|
+
async function generateCodeChallenge(codeVerifier) {
|
|
32
|
+
const hash = crypto_1.default.createHash('sha256').update(codeVerifier).digest();
|
|
33
|
+
return base64Url(hash);
|
|
34
|
+
}
|
|
35
|
+
async function waitForAuthCode(redirectUri, state, listenPort, expectedPath, timeoutMs = 120000, maxRetries = 3) {
|
|
36
|
+
const redirectUrl = new url_1.URL(redirectUri);
|
|
37
|
+
// If listenPort is not provided, try to extract from redirectUri (legacy behavior).
|
|
38
|
+
// Previously this defaulted to port 80, which requires elevated privileges on most systems.
|
|
39
|
+
// We now require an explicit, non-privileged port (>= 1024) either via listenPort or redirectUri.
|
|
40
|
+
const inferredPort = listenPort ?? (redirectUrl.port ? Number(redirectUrl.port) : undefined);
|
|
41
|
+
const port = inferredPort !== undefined &&
|
|
42
|
+
Number.isFinite(inferredPort) &&
|
|
43
|
+
Number.isInteger(inferredPort) &&
|
|
44
|
+
inferredPort >= 1024 &&
|
|
45
|
+
inferredPort <= 65535
|
|
46
|
+
? inferredPort
|
|
47
|
+
: undefined;
|
|
48
|
+
if (port === undefined) {
|
|
49
|
+
throw new Error('Unable to determine a valid listen port for PKCE redirect. ' +
|
|
50
|
+
'Please provide a non-privileged port (>= 1024) via the listenPort parameter or include it in the redirectUri.');
|
|
51
|
+
}
|
|
52
|
+
const path = expectedPath || redirectUrl.pathname;
|
|
53
|
+
// Retry mechanism to handle race condition where port might be taken between
|
|
54
|
+
// findFreePort() returning and server.listen() attempting to bind
|
|
55
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
56
|
+
try {
|
|
57
|
+
return await attemptListenForAuthCode(port, path, state, timeoutMs);
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
const isAddressInUse = err instanceof Error &&
|
|
61
|
+
(err.message.includes('EADDRINUSE') ||
|
|
62
|
+
err.code === 'EADDRINUSE');
|
|
63
|
+
if (isAddressInUse && attempt < maxRetries) {
|
|
64
|
+
// Exponential backoff: 100ms, 200ms, 400ms
|
|
65
|
+
const delayMs = 100 * Math.pow(2, attempt - 1);
|
|
66
|
+
await new Promise(resolve => setTimeout(resolve, delayMs));
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
throw err;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// TypeScript requires a return statement here, but this code is unreachable
|
|
73
|
+
// because the loop either returns successfully or throws an error on the last attempt
|
|
74
|
+
throw new Error('Unexpected: Failed to bind to port after retries');
|
|
75
|
+
}
|
|
76
|
+
function attemptListenForAuthCode(port, path, state, timeoutMs) {
|
|
77
|
+
return new Promise((resolve, reject) => {
|
|
78
|
+
const sockets = new Set();
|
|
79
|
+
const server = http_1.default.createServer((req, res) => {
|
|
80
|
+
try {
|
|
81
|
+
if (!req.url) {
|
|
82
|
+
res.writeHead(400, { 'Content-Type': 'text/plain' });
|
|
83
|
+
res.end('Invalid request');
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
// We construct a dummy base to parse the URL relative to localhost
|
|
87
|
+
const requestUrl = new url_1.URL(req.url, `http://localhost:${port}`);
|
|
88
|
+
if (requestUrl.pathname !== path) {
|
|
89
|
+
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
90
|
+
res.end('Not found');
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const code = requestUrl.searchParams.get('code');
|
|
94
|
+
const returnedState = requestUrl.searchParams.get('state');
|
|
95
|
+
const error = requestUrl.searchParams.get('error');
|
|
96
|
+
const errorDescription = requestUrl.searchParams.get('error_description');
|
|
97
|
+
if (error) {
|
|
98
|
+
res.writeHead(400, { 'Content-Type': 'text/plain' });
|
|
99
|
+
res.end(`Authorization error: ${error}`);
|
|
100
|
+
cleanup();
|
|
101
|
+
reject(new Error(errorDescription || error));
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
// Check state for CSRF protection.
|
|
105
|
+
// The caller (pkce-flow) constructs the state as a JSON string, e.g.:
|
|
106
|
+
// {"id":"pkce-...","port":12345}
|
|
107
|
+
// The provider (or relay service) should send back exactly what we sent.
|
|
108
|
+
// We validate both exact match and JSON structure to help diagnose relay service issues.
|
|
109
|
+
if (!code) {
|
|
110
|
+
res.writeHead(400, { 'Content-Type': 'text/plain' });
|
|
111
|
+
res.end('Missing authorization code');
|
|
112
|
+
cleanup();
|
|
113
|
+
reject(new Error('Missing authorization code in response'));
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (!returnedState) {
|
|
117
|
+
res.writeHead(400, { 'Content-Type': 'text/plain' });
|
|
118
|
+
res.end('Missing state parameter');
|
|
119
|
+
cleanup();
|
|
120
|
+
reject(new Error('Missing state parameter in response. This may indicate a relay service issue.'));
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
// First check for exact match (the happy path)
|
|
124
|
+
if (returnedState !== state) {
|
|
125
|
+
// State mismatch - try to provide helpful diagnostics
|
|
126
|
+
let errorMessage = 'State parameter mismatch';
|
|
127
|
+
let errorDetails = '';
|
|
128
|
+
// Try to parse expected state to see its structure
|
|
129
|
+
try {
|
|
130
|
+
JSON.parse(state);
|
|
131
|
+
// Expected state is valid JSON
|
|
132
|
+
// Try to parse returned state
|
|
133
|
+
try {
|
|
134
|
+
JSON.parse(returnedState);
|
|
135
|
+
// Both are valid JSON but don't match - could be modified by relay
|
|
136
|
+
errorDetails = ' (both states are valid JSON but differ - possible relay service modification)';
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
// Returned state is not valid JSON - likely corrupted by relay
|
|
140
|
+
errorDetails = ' (returned state is not valid JSON - likely corrupted by relay service)';
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
// Expected state is not JSON - shouldn't happen with current implementation
|
|
145
|
+
errorDetails = ' (unexpected state format)';
|
|
146
|
+
}
|
|
147
|
+
res.writeHead(400, { 'Content-Type': 'text/plain' });
|
|
148
|
+
res.end('Invalid authorization response: state mismatch');
|
|
149
|
+
cleanup();
|
|
150
|
+
reject(new Error(`${errorMessage}${errorDetails}. Expected: ${state}, Received: ${returnedState}`));
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
|
154
|
+
res.end('Authorization complete. You can close this window.');
|
|
155
|
+
cleanup();
|
|
156
|
+
resolve(code);
|
|
157
|
+
}
|
|
158
|
+
catch (err) {
|
|
159
|
+
cleanup();
|
|
160
|
+
reject(err instanceof Error ? err : new Error(String(err)));
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
server.on('connection', (socket) => {
|
|
164
|
+
sockets.add(socket);
|
|
165
|
+
socket.on('close', () => sockets.delete(socket));
|
|
166
|
+
});
|
|
167
|
+
// Handle server errors, including EADDRINUSE
|
|
168
|
+
server.on('error', (err) => {
|
|
169
|
+
cleanup();
|
|
170
|
+
reject(err);
|
|
171
|
+
});
|
|
172
|
+
const timeout = setTimeout(() => {
|
|
173
|
+
cleanup();
|
|
174
|
+
reject(new Error('Authorization timed out'));
|
|
175
|
+
}, timeoutMs);
|
|
176
|
+
const cleanup = () => {
|
|
177
|
+
clearTimeout(timeout);
|
|
178
|
+
for (const socket of sockets) {
|
|
179
|
+
socket.destroy();
|
|
180
|
+
}
|
|
181
|
+
server.close();
|
|
182
|
+
};
|
|
183
|
+
server.listen(port, 'localhost');
|
|
184
|
+
server.unref();
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Finds a free port by binding to port 0 and returning the OS-assigned port.
|
|
189
|
+
*
|
|
190
|
+
* **Race Condition Warning**: There is a small window between when this function
|
|
191
|
+
* closes the temporary server and when the caller attempts to bind to the returned port.
|
|
192
|
+
* During this window, another process could potentially claim the port.
|
|
193
|
+
*
|
|
194
|
+
* To handle this race condition, callers should implement retry logic with exponential
|
|
195
|
+
* backoff when binding fails with EADDRINUSE. The `waitForAuthCode` function already
|
|
196
|
+
* implements this retry mechanism.
|
|
197
|
+
*
|
|
198
|
+
* @returns A promise that resolves to a free port number
|
|
199
|
+
* @throws Error if unable to find a free port within the timeout period
|
|
200
|
+
*/
|
|
201
|
+
async function findFreePort() {
|
|
202
|
+
return new Promise((resolve, reject) => {
|
|
203
|
+
const server = http_1.default.createServer();
|
|
204
|
+
let settled = false;
|
|
205
|
+
const timeout = setTimeout(() => {
|
|
206
|
+
if (settled) {
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
settled = true;
|
|
210
|
+
try {
|
|
211
|
+
server.close();
|
|
212
|
+
}
|
|
213
|
+
catch {
|
|
214
|
+
// Ignore errors while closing on timeout
|
|
215
|
+
}
|
|
216
|
+
reject(new Error('Timed out while trying to find a free port'));
|
|
217
|
+
}, 5000);
|
|
218
|
+
const safeResolve = (port) => {
|
|
219
|
+
if (settled) {
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
settled = true;
|
|
223
|
+
clearTimeout(timeout);
|
|
224
|
+
resolve(port);
|
|
225
|
+
};
|
|
226
|
+
const safeReject = (err) => {
|
|
227
|
+
if (settled) {
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
settled = true;
|
|
231
|
+
clearTimeout(timeout);
|
|
232
|
+
reject(err);
|
|
233
|
+
};
|
|
234
|
+
server.on('error', (err) => {
|
|
235
|
+
safeReject(err);
|
|
236
|
+
});
|
|
237
|
+
server.listen(0, () => {
|
|
238
|
+
const address = server.address();
|
|
239
|
+
const port = typeof address === 'object' && address ? address.port : 0;
|
|
240
|
+
try {
|
|
241
|
+
server.close(() => {
|
|
242
|
+
if (port > 0) {
|
|
243
|
+
safeResolve(port);
|
|
244
|
+
}
|
|
245
|
+
else {
|
|
246
|
+
safeReject(new Error('Failed to obtain a free port'));
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
catch (err) {
|
|
251
|
+
safeReject(err instanceof Error
|
|
252
|
+
? err
|
|
253
|
+
: new Error('Failed to close server while finding a free port'));
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
async function exchangeCodeForTokenWithEndpoint(tokenEndpoint, clientId, redirectUri, code, codeVerifier, scope) {
|
|
259
|
+
const response = await fetch(tokenEndpoint, {
|
|
260
|
+
method: 'POST',
|
|
261
|
+
headers: {
|
|
262
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
263
|
+
Accept: 'application/json',
|
|
264
|
+
},
|
|
265
|
+
body: new URLSearchParams({
|
|
266
|
+
client_id: clientId,
|
|
267
|
+
grant_type: 'authorization_code',
|
|
268
|
+
code,
|
|
269
|
+
redirect_uri: redirectUri,
|
|
270
|
+
code_verifier: codeVerifier,
|
|
271
|
+
scope,
|
|
272
|
+
}),
|
|
273
|
+
});
|
|
274
|
+
const data = (await response.json());
|
|
275
|
+
if (!response.ok) {
|
|
276
|
+
throw new Error(`Token request failed: ${data.error || response.statusText} - ${data.error_description || ''}`);
|
|
277
|
+
}
|
|
278
|
+
return data;
|
|
279
|
+
}
|
|
280
|
+
function base64Url(buffer) {
|
|
281
|
+
return buffer
|
|
282
|
+
.toString('base64')
|
|
283
|
+
.replace(/\+/g, '-')
|
|
284
|
+
.replace(/\//g, '_')
|
|
285
|
+
.replace(/=+$/g, '');
|
|
286
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Registry } from '../../types/config.js';
|
|
2
|
+
/**
|
|
3
|
+
* OAuth scope for Azure DevOps device flow authentication
|
|
4
|
+
* Uses the Azure DevOps application ID with .default scope
|
|
5
|
+
*/
|
|
6
|
+
export declare const AZURE_DEVOPS_OAUTH_SCOPE = "499b84ac-1321-427f-aa17-267ca6975798/.default";
|
|
7
|
+
/**
|
|
8
|
+
* Authenticate Azure DevOps registries using OAuth device flow
|
|
9
|
+
* Returns map of registry URL ā PAT token
|
|
10
|
+
*/
|
|
11
|
+
export declare function authenticateAzureDevOps(clientId: string, tenantId: string, registries: Registry[], redirectUri?: string): Promise<Map<string, string>>;
|
|
12
|
+
/**
|
|
13
|
+
* Generate PATs using a provided OAuth token (skips auth flow)
|
|
14
|
+
*/
|
|
15
|
+
export declare function authenticateAzureDevOpsWithOAuthToken(oauthToken: string, registries: Registry[]): Promise<Map<string, string>>;
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.AZURE_DEVOPS_OAUTH_SCOPE = void 0;
|
|
7
|
+
exports.authenticateAzureDevOps = authenticateAzureDevOps;
|
|
8
|
+
exports.authenticateAzureDevOpsWithOAuthToken = authenticateAzureDevOpsWithOAuthToken;
|
|
9
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
10
|
+
const pkce_flow_js_1 = require("../pkce-flow.js");
|
|
11
|
+
const constants_1 = require("../constants");
|
|
12
|
+
/**
|
|
13
|
+
* Entra ID (Azure AD) OAuth endpoint templates
|
|
14
|
+
* Tenant ID will be substituted at runtime
|
|
15
|
+
*/
|
|
16
|
+
const AUTHORIZE_ENDPOINT_TEMPLATE = 'https://login.microsoftonline.com/{tenantId}/oauth2/v2.0/authorize';
|
|
17
|
+
const TOKEN_ENDPOINT_TEMPLATE = 'https://login.microsoftonline.com/{tenantId}/oauth2/v2.0/token';
|
|
18
|
+
/**
|
|
19
|
+
* OAuth scope for Azure DevOps device flow authentication
|
|
20
|
+
* Uses the Azure DevOps application ID with .default scope
|
|
21
|
+
*/
|
|
22
|
+
exports.AZURE_DEVOPS_OAUTH_SCOPE = '499b84ac-1321-427f-aa17-267ca6975798/.default';
|
|
23
|
+
/**
|
|
24
|
+
* PAT scope for npm package access (read-only)
|
|
25
|
+
* vso.packaging is the read-only scope for accessing Azure DevOps package feeds
|
|
26
|
+
*/
|
|
27
|
+
const PAT_SCOPE = 'vso.packaging';
|
|
28
|
+
/**
|
|
29
|
+
* PAT API endpoint pattern
|
|
30
|
+
*/
|
|
31
|
+
const PAT_API_ENDPOINT = 'https://vssps.dev.azure.com/{organization}/_apis/tokens/pats?api-version=7.1-preview.1';
|
|
32
|
+
/**
|
|
33
|
+
* Authenticate Azure DevOps registries using OAuth device flow
|
|
34
|
+
* Returns map of registry URL ā PAT token
|
|
35
|
+
*/
|
|
36
|
+
async function authenticateAzureDevOps(clientId, tenantId, registries, redirectUri) {
|
|
37
|
+
console.log(chalk_1.default.bold('\nš Azure DevOps Authentication\n'));
|
|
38
|
+
// Step 1: PKCE auth to get OAuth token
|
|
39
|
+
// If redirectUri is provided (e.g. from CLI or config), use it (Relay mode if non-localhost).
|
|
40
|
+
// Otherwise fallback to legacy fixed port localhost URI.
|
|
41
|
+
const effectiveRedirectUri = redirectUri || constants_1.DEFAULT_REDIRECT_URI;
|
|
42
|
+
const tokenResponse = await (0, pkce_flow_js_1.runPkceFlow)({
|
|
43
|
+
authorizeEndpoint: AUTHORIZE_ENDPOINT_TEMPLATE.replace('{tenantId}', tenantId),
|
|
44
|
+
tokenEndpoint: TOKEN_ENDPOINT_TEMPLATE.replace('{tenantId}', tenantId),
|
|
45
|
+
clientId,
|
|
46
|
+
redirectUri: effectiveRedirectUri,
|
|
47
|
+
scope: exports.AZURE_DEVOPS_OAUTH_SCOPE,
|
|
48
|
+
providerName: 'Azure DevOps',
|
|
49
|
+
});
|
|
50
|
+
if (!tokenResponse.access_token) {
|
|
51
|
+
throw new Error('OAuth access token not returned from Entra ID');
|
|
52
|
+
}
|
|
53
|
+
return buildTokenMap(registries, tokenResponse.access_token);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Generate PATs using a provided OAuth token (skips auth flow)
|
|
57
|
+
*/
|
|
58
|
+
async function authenticateAzureDevOpsWithOAuthToken(oauthToken, registries) {
|
|
59
|
+
return buildTokenMap(registries, oauthToken);
|
|
60
|
+
}
|
|
61
|
+
async function buildTokenMap(registries, oauthToken) {
|
|
62
|
+
const organizations = extractOrganizations(registries);
|
|
63
|
+
console.log(chalk_1.default.gray(`\nFound ${organizations.size} unique organization(s): ${Array.from(organizations).join(', ')}\n`));
|
|
64
|
+
const tokenMap = new Map();
|
|
65
|
+
for (const org of organizations) {
|
|
66
|
+
const pat = await generatePat(org, oauthToken);
|
|
67
|
+
for (const registry of registries) {
|
|
68
|
+
const registryOrg = extractOrgFromUrl(registry.url);
|
|
69
|
+
if (registryOrg === org) {
|
|
70
|
+
tokenMap.set(registry.url, pat);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return tokenMap;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Extract unique organizations from registry URLs
|
|
78
|
+
*/
|
|
79
|
+
function extractOrganizations(registries) {
|
|
80
|
+
const orgs = new Set();
|
|
81
|
+
for (const registry of registries) {
|
|
82
|
+
const org = extractOrgFromUrl(registry.url);
|
|
83
|
+
if (org) {
|
|
84
|
+
orgs.add(org);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return orgs;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Extract organization from Azure DevOps registry URL
|
|
91
|
+
* Format: https://pkgs.dev.azure.com/{organization}/_packaging/...
|
|
92
|
+
*/
|
|
93
|
+
function extractOrgFromUrl(url) {
|
|
94
|
+
const match = url.match(/pkgs\.dev\.azure\.com\/([^/]+)\//);
|
|
95
|
+
return match ? match[1] : null;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Generate a Personal Access Token using the OAuth token
|
|
99
|
+
*/
|
|
100
|
+
async function generatePat(organization, oauthToken) {
|
|
101
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
102
|
+
const displayName = `mfe-platform-cli-${organization}-${timestamp}`;
|
|
103
|
+
// Set PAT to expire in 1 year
|
|
104
|
+
const validTo = new Date();
|
|
105
|
+
validTo.setFullYear(validTo.getFullYear() + 1);
|
|
106
|
+
const patRequest = {
|
|
107
|
+
displayName,
|
|
108
|
+
scope: PAT_SCOPE,
|
|
109
|
+
validTo: validTo.toISOString(),
|
|
110
|
+
allOrgs: false,
|
|
111
|
+
};
|
|
112
|
+
const endpoint = PAT_API_ENDPOINT.replace('{organization}', organization);
|
|
113
|
+
console.log(chalk_1.default.gray(`Generating PAT for organization: ${organization}...`));
|
|
114
|
+
const response = await fetch(endpoint, {
|
|
115
|
+
method: 'POST',
|
|
116
|
+
headers: {
|
|
117
|
+
'Content-Type': 'application/json',
|
|
118
|
+
Accept: 'application/json',
|
|
119
|
+
Authorization: `Bearer ${oauthToken}`,
|
|
120
|
+
},
|
|
121
|
+
body: JSON.stringify(patRequest),
|
|
122
|
+
});
|
|
123
|
+
if (!response.ok) {
|
|
124
|
+
const errorText = await response.text();
|
|
125
|
+
throw new Error(`Failed to generate PAT for ${organization}: ${response.status} ${response.statusText}\n${errorText}`);
|
|
126
|
+
}
|
|
127
|
+
const patResponse = (await response.json());
|
|
128
|
+
console.log(chalk_1.default.green(`ā PAT generated: ${displayName}`));
|
|
129
|
+
return patResponse.patToken.token;
|
|
130
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { Registry } from '../../types/config.js';
|
|
2
|
+
/**
|
|
3
|
+
* Authenticate GitHub registries using OAuth device flow
|
|
4
|
+
* Returns map of registry URL ā access token (same token for all registries in group)
|
|
5
|
+
*/
|
|
6
|
+
export declare function authenticateGitHub(clientId: string, registries: Registry[]): Promise<Map<string, string>>;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.authenticateGitHub = authenticateGitHub;
|
|
7
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
8
|
+
const device_flow_js_1 = require("../device-flow.js");
|
|
9
|
+
/**
|
|
10
|
+
* GitHub OAuth endpoints
|
|
11
|
+
*/
|
|
12
|
+
const DEVICE_AUTH_ENDPOINT = 'https://github.com/login/device/code';
|
|
13
|
+
const TOKEN_ENDPOINT = 'https://github.com/login/oauth/access_token';
|
|
14
|
+
/**
|
|
15
|
+
* Required scopes for npm package access (read-only)
|
|
16
|
+
*/
|
|
17
|
+
const REQUIRED_SCOPES = 'read:packages';
|
|
18
|
+
/**
|
|
19
|
+
* Authenticate GitHub registries using OAuth device flow
|
|
20
|
+
* Returns map of registry URL ā access token (same token for all registries in group)
|
|
21
|
+
*/
|
|
22
|
+
async function authenticateGitHub(clientId, registries) {
|
|
23
|
+
console.log(chalk_1.default.bold('\nš GitHub Authentication\n'));
|
|
24
|
+
// Step 1: Device flow to get access token
|
|
25
|
+
const deviceAuth = await (0, device_flow_js_1.initiateDeviceFlow)(DEVICE_AUTH_ENDPOINT, clientId, REQUIRED_SCOPES);
|
|
26
|
+
(0, device_flow_js_1.displayUserInstructions)(deviceAuth.user_code, deviceAuth.verification_uri, 'GitHub');
|
|
27
|
+
const tokenResponse = await (0, device_flow_js_1.pollForToken)(TOKEN_ENDPOINT, clientId, deviceAuth.device_code, deviceAuth.interval, deviceAuth.expires_in);
|
|
28
|
+
if (!tokenResponse.access_token) {
|
|
29
|
+
throw new Error('GitHub access token not returned from OAuth device flow');
|
|
30
|
+
}
|
|
31
|
+
console.log(chalk_1.default.green('ā GitHub access token acquired\n'));
|
|
32
|
+
// Step 2: Map all registries to the same token
|
|
33
|
+
const tokenMap = new Map();
|
|
34
|
+
for (const registry of registries) {
|
|
35
|
+
tokenMap.set(registry.url, tokenResponse.access_token);
|
|
36
|
+
}
|
|
37
|
+
return tokenMap;
|
|
38
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { ConfigSource } from '../types/config.js';
|
|
3
|
+
/**
|
|
4
|
+
* Get stored config source and auth options for update command
|
|
5
|
+
*/
|
|
6
|
+
export declare function getStoredConfigSource(): Promise<ConfigSource | null>;
|
|
7
|
+
/**
|
|
8
|
+
* Create and configure the init command
|
|
9
|
+
*/
|
|
10
|
+
export declare function createInitCommand(): Command;
|