ovsx 1.2.0 → 1.4.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/README.md +7 -1
- package/lib/check-license.d.ts.map +1 -1
- package/lib/check-license.js +5 -4
- package/lib/check-license.js.map +1 -1
- package/lib/create-namespace.d.ts.map +1 -1
- package/lib/create-namespace.js +3 -1
- package/lib/create-namespace.js.map +1 -1
- package/lib/get.js +5 -5
- package/lib/get.js.map +1 -1
- package/lib/http.d.ts +32 -0
- package/lib/http.d.ts.map +1 -0
- package/lib/http.js +170 -0
- package/lib/http.js.map +1 -0
- package/lib/index.d.ts +0 -3
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +0 -3
- package/lib/index.js.map +1 -1
- package/lib/main.js +50 -61
- package/lib/main.js.map +1 -1
- package/lib/oidc.d.ts.map +1 -1
- package/lib/oidc.js +5 -47
- package/lib/oidc.js.map +1 -1
- package/lib/pat.d.ts +6 -1
- package/lib/pat.d.ts.map +1 -1
- package/lib/pat.js +16 -7
- package/lib/pat.js.map +1 -1
- package/lib/publish.d.ts +6 -0
- package/lib/publish.d.ts.map +1 -1
- package/lib/publish.js +147 -40
- package/lib/publish.js.map +1 -1
- package/lib/registry.d.ts +50 -12
- package/lib/registry.d.ts.map +1 -1
- package/lib/registry.js +131 -126
- package/lib/registry.js.map +1 -1
- package/lib/trusted-publishing-options.d.ts +5 -0
- package/lib/trusted-publishing-options.d.ts.map +1 -1
- package/lib/trusted-publishing.js +28 -4
- package/lib/trusted-publishing.js.map +1 -1
- package/lib/unpublish.d.ts.map +1 -1
- package/lib/unpublish.js +5 -8
- package/lib/unpublish.js.map +1 -1
- package/lib/util.d.ts +19 -15
- package/lib/util.d.ts.map +1 -1
- package/lib/util.js +51 -71
- package/lib/util.js.map +1 -1
- package/lib/verify-pat.d.ts.map +1 -1
- package/lib/verify-pat.js +3 -1
- package/lib/verify-pat.js.map +1 -1
- package/lib/verify.js +2 -2
- package/lib/verify.js.map +1 -1
- package/lib/zip.d.ts.map +1 -1
- package/lib/zip.js +2 -10
- package/lib/zip.js.map +1 -1
- package/package.json +19 -14
- package/src/check-license.ts +5 -4
- package/src/create-namespace.ts +3 -1
- package/src/get.ts +6 -6
- package/src/http.ts +176 -0
- package/src/index.ts +0 -3
- package/src/main.ts +54 -63
- package/src/oidc.ts +5 -47
- package/src/pat.ts +22 -9
- package/src/publish.ts +187 -44
- package/src/registry.ts +153 -126
- package/src/trusted-publishing-options.ts +5 -0
- package/src/trusted-publishing.ts +33 -5
- package/src/unpublish.ts +4 -7
- package/src/util.ts +47 -73
- package/src/verify-pat.ts +3 -1
- package/src/verify.ts +3 -3
- package/src/zip.ts +2 -12
- package/lib/version.d.ts +0 -2
- package/lib/version.d.ts.map +0 -1
- package/lib/version.js +0 -5
- package/lib/version.js.map +0 -1
- package/src/version.ts +0 -1
package/src/http.ts
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/******************************************************************************
|
|
2
|
+
* Copyright (c) 2026 Contributors to the Eclipse Foundation.
|
|
3
|
+
*
|
|
4
|
+
* See the NOTICE file(s) distributed with this work for additional
|
|
5
|
+
* information regarding copyright ownership.
|
|
6
|
+
*
|
|
7
|
+
* This program and the accompanying materials are made available under the
|
|
8
|
+
* terms of the Eclipse Public License 2.0 which is available at
|
|
9
|
+
* https://www.eclipse.org/legal/epl-2.0.
|
|
10
|
+
*
|
|
11
|
+
* SPDX-License-Identifier: EPL-2.0
|
|
12
|
+
*****************************************************************************/
|
|
13
|
+
|
|
14
|
+
import * as http from 'http';
|
|
15
|
+
import * as https from 'https';
|
|
16
|
+
import * as fs from 'fs';
|
|
17
|
+
import { redactUrl, statusError, withStatus } from './util';
|
|
18
|
+
|
|
19
|
+
// Hop limit the fetch standard uses.
|
|
20
|
+
const MAX_REDIRECTS = 20;
|
|
21
|
+
|
|
22
|
+
// Dropped on a redirect to another origin; X-OpenVSX-Token carries the PAT just like Authorization.
|
|
23
|
+
const CREDENTIAL_HEADERS = /^(?:authorization|cookie|proxy-authorization|x-openvsx-token)$/i;
|
|
24
|
+
|
|
25
|
+
export type RequestBody = () => string | Buffer | Uint8Array | fs.ReadStream;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Sends a request and follows redirects to `onResponse`'s final response, reporting any failure
|
|
29
|
+
* to `fail`. A GET follows every redirect; a request with a body follows only 307/308, sending
|
|
30
|
+
* the body again, because 301/302/303 turn it into a GET without one. Credentials are dropped
|
|
31
|
+
* once a redirect leaves the origin. Only the headers in `options` are sent, so a caller that
|
|
32
|
+
* passes none carries no credentials.
|
|
33
|
+
*/
|
|
34
|
+
export function send(
|
|
35
|
+
url: URL,
|
|
36
|
+
options: http.RequestOptions,
|
|
37
|
+
body: RequestBody | undefined,
|
|
38
|
+
onResponse: (response: http.IncomingMessage) => void,
|
|
39
|
+
fail: (err: Error) => void,
|
|
40
|
+
hops = 0
|
|
41
|
+
): void {
|
|
42
|
+
const protocol = url.protocol === 'https:' ? https : http;
|
|
43
|
+
const request = protocol.request(url, options, response => {
|
|
44
|
+
const status = response.statusCode ?? 0;
|
|
45
|
+
const location = response.headers.location;
|
|
46
|
+
if (status < 300 || status > 399 || !location) {
|
|
47
|
+
onResponse(response);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
response.resume();
|
|
51
|
+
|
|
52
|
+
let next: URL;
|
|
53
|
+
try {
|
|
54
|
+
next = new URL(location, url);
|
|
55
|
+
} catch (err) {
|
|
56
|
+
fail(err as Error);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
if (hops >= MAX_REDIRECTS) {
|
|
60
|
+
fail(new Error(`The request to ${redactUrl(url)} was redirected more than ${MAX_REDIRECTS} times.`));
|
|
61
|
+
} else if (next.protocol !== 'https:' && next.protocol !== 'http:') {
|
|
62
|
+
fail(new Error(`The request to ${redactUrl(url)} was redirected to unsupported URL ${redactUrl(next)}.`));
|
|
63
|
+
} else if (body && status !== 307 && status !== 308) {
|
|
64
|
+
fail(withStatus(new Error(`The ${options.method} request to ${redactUrl(url)} was redirected to ${redactUrl(next)} `
|
|
65
|
+
+ `with status ${status}, which drops the request body. Use the redirect target as the registry URL.`), status));
|
|
66
|
+
} else {
|
|
67
|
+
// The server may answer before reading the body: abandon this hop so its stream
|
|
68
|
+
// releases the file and a late reset on its socket can't fail the next hop.
|
|
69
|
+
request.removeListener('error', fail);
|
|
70
|
+
request.on('error', () => { /* abandoned */ });
|
|
71
|
+
if (content instanceof fs.ReadStream) {
|
|
72
|
+
content.unpipe(request);
|
|
73
|
+
content.destroy();
|
|
74
|
+
}
|
|
75
|
+
request.destroy();
|
|
76
|
+
const headers = next.origin === url.origin ? options.headers : withoutCredentials(options.headers as http.OutgoingHttpHeaders);
|
|
77
|
+
send(next, { ...options, headers }, body, onResponse, fail, hops + 1);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
request.on('error', fail);
|
|
81
|
+
failOnTimeout(request, url, options.timeout, fail);
|
|
82
|
+
|
|
83
|
+
const content = body?.();
|
|
84
|
+
if (content instanceof fs.ReadStream) {
|
|
85
|
+
content.on('error', (err: Error) => {
|
|
86
|
+
request.destroy();
|
|
87
|
+
fail(err);
|
|
88
|
+
});
|
|
89
|
+
request.on('error', () => content.close());
|
|
90
|
+
content.on('open', () => content.pipe(request));
|
|
91
|
+
} else {
|
|
92
|
+
request.end(content);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Sends a GET with only the given headers and resolves with the parsed JSON response. */
|
|
97
|
+
export function getJson<T>(url: URL, headers: http.OutgoingHttpHeaders, timeout: number): Promise<T> {
|
|
98
|
+
return new Promise((resolve, reject) => {
|
|
99
|
+
send(url, { method: 'GET', headers, timeout }, undefined, jsonResponse<T>(resolve, reject), reject);
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Reads a response as JSON. A non-2xx status rejects with the `message` or `error` of a JSON
|
|
105
|
+
* error body when there is one, and with the status otherwise. An HTML page rejects with an
|
|
106
|
+
* Error rather than its markup, since it usually means the URL points at a website, not an API.
|
|
107
|
+
*/
|
|
108
|
+
export function jsonResponse<T>(resolve: (value: T) => void, reject: (reason: any) => void): (res: http.IncomingMessage) => void {
|
|
109
|
+
return response => {
|
|
110
|
+
response.setEncoding('utf-8');
|
|
111
|
+
let json = '';
|
|
112
|
+
// A connection lost after the headers have arrived ends the response without 'end' ever
|
|
113
|
+
// firing, so without this the promise is never settled and the command waits on a body
|
|
114
|
+
// that is not coming. The configured timeout would eventually rescue it, but rejecting
|
|
115
|
+
// here reports what actually happened instead of thirty seconds of silence.
|
|
116
|
+
response.on('error', reject);
|
|
117
|
+
response.on('data', chunk => json += chunk);
|
|
118
|
+
response.on('end', () => {
|
|
119
|
+
if (response.statusCode !== undefined && (response.statusCode < 200 || response.statusCode > 299)) {
|
|
120
|
+
if (json.trimStart().startsWith('{')) {
|
|
121
|
+
try {
|
|
122
|
+
const parsed = JSON.parse(json) as { message?: string; error?: string };
|
|
123
|
+
const message = parsed.message || parsed.error;
|
|
124
|
+
if (message) {
|
|
125
|
+
// keep the status: the message alone cannot say whether retrying is worth it
|
|
126
|
+
reject(withStatus(new Error(message), response.statusCode));
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
} catch (err) {
|
|
130
|
+
// Ignore the error and reject with response status
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
reject(statusError(response));
|
|
134
|
+
} else if (isHtml(response, json)) {
|
|
135
|
+
reject(withStatus(new Error(`The server responded with status ${response.statusCode} and an HTML page instead of JSON.`),
|
|
136
|
+
response.statusCode));
|
|
137
|
+
} else {
|
|
138
|
+
try {
|
|
139
|
+
resolve(JSON.parse(json));
|
|
140
|
+
} catch (err) {
|
|
141
|
+
reject(err);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Node's `timeout` option only raises an event - the request stays open, which is why a server
|
|
150
|
+
* that accepts a connection and then says nothing used to hold a command open indefinitely.
|
|
151
|
+
* Destroying the request with an error routes through the error handling each caller already
|
|
152
|
+
* has, so a stalled request rejects the way any other failure does. With a timeout of zero the
|
|
153
|
+
* event never fires and this does nothing.
|
|
154
|
+
*/
|
|
155
|
+
function failOnTimeout(request: http.ClientRequest, url: URL, timeout: number | undefined, fail: (err: Error) => void): void {
|
|
156
|
+
request.on('timeout', () => {
|
|
157
|
+
// Reported before the request is torn down, rather than by destroying it with the error.
|
|
158
|
+
// Destroying raises errors of its own - ECONNRESET on the response, a premature close
|
|
159
|
+
// from pipeline - and node guarantees no order between those and the request's own. In
|
|
160
|
+
// practice the request's arrives first, so this is belt and braces rather than the thing
|
|
161
|
+
// that stops the wrong error being reported; that is the caller claiming the failure
|
|
162
|
+
// before it does any asynchronous cleanup.
|
|
163
|
+
fail(new Error(`No response from ${redactUrl(url)} for ${timeout} ms.`));
|
|
164
|
+
request.destroy();
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// The body check covers servers that send HTML without a text/html Content-Type.
|
|
169
|
+
function isHtml(response: http.IncomingMessage, body: string): boolean {
|
|
170
|
+
const contentType = response.headers['content-type']?.split(';')[0].trim().toLowerCase();
|
|
171
|
+
return contentType === 'text/html' || /^\s*<(?:!doctype\s+html|html)[\s>]/i.test(body);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function withoutCredentials(headers: http.OutgoingHttpHeaders = {}): http.OutgoingHttpHeaders {
|
|
175
|
+
return Object.fromEntries(Object.entries(headers).filter(([name]) => !CREDENTIAL_HEADERS.test(name)));
|
|
176
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -12,9 +12,6 @@ export * from './create-namespace';
|
|
|
12
12
|
export * from './create-namespace-options';
|
|
13
13
|
export * from './get';
|
|
14
14
|
export * from './get-options';
|
|
15
|
-
export * from './login';
|
|
16
|
-
export * from './login-options';
|
|
17
|
-
export * from './logout';
|
|
18
15
|
export * from './publish';
|
|
19
16
|
export * from './publish-options';
|
|
20
17
|
export * from './registry';
|
package/src/main.ts
CHANGED
|
@@ -9,12 +9,13 @@
|
|
|
9
9
|
********************************************************************************/
|
|
10
10
|
|
|
11
11
|
import * as commander from 'commander';
|
|
12
|
-
import
|
|
12
|
+
import { readFileSync } from 'fs';
|
|
13
|
+
import { join } from 'path';
|
|
13
14
|
import { createNamespace } from './create-namespace';
|
|
14
15
|
import { verifyPat } from './verify-pat';
|
|
15
16
|
import { publish } from './publish';
|
|
16
17
|
import { unpublish } from './unpublish';
|
|
17
|
-
import { handleError, parseNonNegativeInt } from './util';
|
|
18
|
+
import { handleError, levenshtein, parseNonNegativeInt } from './util';
|
|
18
19
|
import { getExtension } from './get';
|
|
19
20
|
import { list } from './list';
|
|
20
21
|
import { DEFAULT_SEARCH_SIZE, SORT_KEYS, SORT_ORDERS, search } from './search';
|
|
@@ -23,10 +24,13 @@ import { verify } from './verify';
|
|
|
23
24
|
import { verifySignature } from './verify-signature';
|
|
24
25
|
import login from './login';
|
|
25
26
|
import logout from './logout';
|
|
26
|
-
|
|
27
|
+
|
|
28
|
+
// Resolves from both src/ (tests) and lib/ (published build); npm always ships package.json.
|
|
29
|
+
const LIB_VERSION: string = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8')).version;
|
|
27
30
|
|
|
28
31
|
module.exports = function (argv: string[]): void {
|
|
29
32
|
const program = new commander.Command();
|
|
33
|
+
program.nameFromFilename(argv[1]);
|
|
30
34
|
program.usage('<command> [options]')
|
|
31
35
|
.option('-r, --registryUrl <url>', 'Use the registry API at this base URL.')
|
|
32
36
|
.option('-p, --pat <token>', 'Personal access token.')
|
|
@@ -36,17 +40,17 @@ module.exports = function (argv: string[]): void {
|
|
|
36
40
|
const createNamespaceCmd = program.command('create-namespace <name>');
|
|
37
41
|
createNamespaceCmd.description('Create a new namespace')
|
|
38
42
|
.action((name: string) => {
|
|
39
|
-
const { registryUrl, pat } = program.opts();
|
|
43
|
+
const { registryUrl, pat, debug } = program.opts();
|
|
40
44
|
createNamespace({ name, registryUrl, pat })
|
|
41
|
-
.catch(handleError(
|
|
45
|
+
.catch(handleError(debug));
|
|
42
46
|
});
|
|
43
47
|
|
|
44
48
|
const verifyTokenCmd = program.command('verify-pat [namespace]');
|
|
45
49
|
verifyTokenCmd.description('Verify that a personal access token can publish to a namespace')
|
|
46
50
|
.action((namespace?: string) => {
|
|
47
|
-
const { registryUrl, pat } = program.opts();
|
|
51
|
+
const { registryUrl, pat, debug } = program.opts();
|
|
48
52
|
verifyPat({ namespace, registryUrl, pat })
|
|
49
|
-
.catch(handleError(
|
|
53
|
+
.catch(handleError(debug));
|
|
50
54
|
});
|
|
51
55
|
|
|
52
56
|
const publishCmd = program.command('publish [extension.vsix]');
|
|
@@ -65,29 +69,14 @@ module.exports = function (argv: string[]): void {
|
|
|
65
69
|
.option('--trusted-publishing', 'Exchange an OIDC ID token for a short-lived publishing token. Enabled automatically when a CI system provides an ID token and no access token is given.')
|
|
66
70
|
.option('--idToken <token>', 'The OIDC ID token to exchange. Only needed on CI systems that provide the token directly, e.g. GitLab CI.')
|
|
67
71
|
.option('--oidcAudience <audience>', 'Audience to request for the OIDC ID token. Defaults to the registry URL.')
|
|
68
|
-
.
|
|
72
|
+
.option('--registrationCode <code>', 'The registration code of a pending trusted publisher (a private repository); completes its registration on the first publish.')
|
|
73
|
+
.action((extensionFile: string, { target, packagePath, baseContentUrl, baseImagesUrl, yarn, followSymlinks, preRelease, allowMissingRepository, dependencies, skipDuplicate, packageVersion, trustedPublishing, idToken, oidcAudience, registrationCode }) => {
|
|
69
74
|
if (extensionFile !== undefined && packagePath !== undefined) {
|
|
70
75
|
console.error('\u274c Please specify either a package file or a package path, but not both.\n');
|
|
71
76
|
publishCmd.help();
|
|
72
77
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
target = undefined;
|
|
76
|
-
}
|
|
77
|
-
if (extensionFile !== undefined && baseContentUrl !== undefined)
|
|
78
|
-
console.warn("Ignoring option '--baseContentUrl' for prepackaged extension.");
|
|
79
|
-
if (extensionFile !== undefined && baseImagesUrl !== undefined)
|
|
80
|
-
console.warn("Ignoring option '--baseImagesUrl' for prepackaged extension.");
|
|
81
|
-
if (extensionFile !== undefined && yarn !== undefined)
|
|
82
|
-
console.warn("Ignoring option '--yarn' for prepackaged extension.");
|
|
83
|
-
if (extensionFile !== undefined && followSymlinks !== undefined)
|
|
84
|
-
console.warn("Ignoring option '--follow-symlinks' for prepackaged extension.");
|
|
85
|
-
if (extensionFile !== undefined && packageVersion !== undefined)
|
|
86
|
-
console.warn("Ignoring option '--packageVersion' for prepackaged extension.");
|
|
87
|
-
if (extensionFile !== undefined && allowMissingRepository !== undefined)
|
|
88
|
-
console.warn("Ignoring option '--allow-missing-repository' for prepackaged extension.");
|
|
89
|
-
const { registryUrl, pat } = program.opts();
|
|
90
|
-
publish({ extensionFile, registryUrl, pat, targets: typeof target === 'string' ? [target] : target, packagePath: typeof packagePath === 'string' ? [packagePath] : packagePath, baseContentUrl, baseImagesUrl, yarn, followSymlinks, preRelease, allowMissingRepository, dependencies, skipDuplicate, packageVersion, trustedPublishing, idToken, oidcAudience })
|
|
78
|
+
const { registryUrl, pat, debug } = program.opts();
|
|
79
|
+
publish({ extensionFile, registryUrl, pat, targets: typeof target === 'string' ? [target] : target, packagePath: typeof packagePath === 'string' ? [packagePath] : packagePath, baseContentUrl, baseImagesUrl, yarn, followSymlinks, preRelease, allowMissingRepository, dependencies, skipDuplicate, packageVersion, trustedPublishing, idToken, oidcAudience, registrationCode })
|
|
91
80
|
.then(results => {
|
|
92
81
|
const reasons = results.filter(result => result.status === 'rejected')
|
|
93
82
|
.map(rejectedResult => rejectedResult.reason);
|
|
@@ -95,14 +84,15 @@ module.exports = function (argv: string[]): void {
|
|
|
95
84
|
if (reasons.length > 0) {
|
|
96
85
|
const message = 'See the documentation for more information:\n'
|
|
97
86
|
+ 'https://github.com/eclipse/openvsx/wiki/Publishing-Extensions';
|
|
98
|
-
const errorHandler = handleError(
|
|
87
|
+
const errorHandler = handleError(debug, message, false);
|
|
99
88
|
for (const reason of reasons) {
|
|
100
89
|
errorHandler(reason);
|
|
101
90
|
}
|
|
102
91
|
|
|
103
92
|
process.exit(1);
|
|
104
93
|
}
|
|
105
|
-
})
|
|
94
|
+
})
|
|
95
|
+
.catch(handleError(debug));
|
|
106
96
|
});
|
|
107
97
|
|
|
108
98
|
const unpublishCmd = program.command('unpublish [namespace.extension]');
|
|
@@ -111,7 +101,7 @@ module.exports = function (argv: string[]): void {
|
|
|
111
101
|
.option('-t, --target <targets...>', 'Only delete the given target architectures of the given versions.')
|
|
112
102
|
.option('-f, --force', 'Skip the confirmation prompt.')
|
|
113
103
|
.action((extensionId: string | undefined, { versions, target, force }) => {
|
|
114
|
-
const { registryUrl, pat } = program.opts();
|
|
104
|
+
const { registryUrl, pat, debug } = program.opts();
|
|
115
105
|
unpublish({
|
|
116
106
|
extensionId,
|
|
117
107
|
versions: typeof versions === 'string' ? [versions] : versions,
|
|
@@ -119,7 +109,7 @@ module.exports = function (argv: string[]): void {
|
|
|
119
109
|
force,
|
|
120
110
|
registryUrl,
|
|
121
111
|
pat
|
|
122
|
-
}).catch(handleError(
|
|
112
|
+
}).catch(handleError(debug));
|
|
123
113
|
});
|
|
124
114
|
|
|
125
115
|
const searchCmd = program.command('search [text]');
|
|
@@ -136,17 +126,17 @@ module.exports = function (argv: string[]): void {
|
|
|
136
126
|
.option('--sort-order <order>', `Sort order: ${SORT_ORDERS.join(', ')}.`)
|
|
137
127
|
.option('--json', 'Print the raw results as JSON.')
|
|
138
128
|
.action((text: string | undefined, { category, target, size, offset, sortBy, sortOrder, json }) => {
|
|
139
|
-
const { registryUrl } = program.opts();
|
|
129
|
+
const { registryUrl, debug } = program.opts();
|
|
140
130
|
search({ text, category, target, size, offset, sortBy, sortOrder, json, registryUrl })
|
|
141
|
-
.catch(handleError(
|
|
131
|
+
.catch(handleError(debug));
|
|
142
132
|
});
|
|
143
133
|
|
|
144
134
|
const listCmd = program.command('list <namespace>');
|
|
145
135
|
listCmd.description('List the extensions published in a namespace.')
|
|
146
136
|
.option('--json', 'Print the raw namespace metadata as JSON.')
|
|
147
137
|
.action((namespace: string, { json }) => {
|
|
148
|
-
const { registryUrl } = program.opts();
|
|
149
|
-
list({ namespace, json, registryUrl }).catch(handleError(
|
|
138
|
+
const { registryUrl, debug } = program.opts();
|
|
139
|
+
list({ namespace, json, registryUrl }).catch(handleError(debug));
|
|
150
140
|
});
|
|
151
141
|
|
|
152
142
|
const showCmd = program.command('show <namespace.extension[@version]>');
|
|
@@ -155,8 +145,8 @@ module.exports = function (argv: string[]): void {
|
|
|
155
145
|
.option('--all-versions', 'List every published version instead of the most recent few.')
|
|
156
146
|
.option('--json', 'Print the raw metadata as JSON.')
|
|
157
147
|
.action((extensionId: string, { target, allVersions, json }) => {
|
|
158
|
-
const { registryUrl } = program.opts();
|
|
159
|
-
show({ extensionId, target, allVersions, json, registryUrl }).catch(handleError(
|
|
148
|
+
const { registryUrl, debug } = program.opts();
|
|
149
|
+
show({ extensionId, target, allVersions, json, registryUrl }).catch(handleError(debug));
|
|
160
150
|
});
|
|
161
151
|
|
|
162
152
|
const getCmd = program.command('get <namespace.extension>');
|
|
@@ -166,18 +156,18 @@ module.exports = function (argv: string[]): void {
|
|
|
166
156
|
.option('-o, --output <path>', 'Save the output in the specified file or directory.')
|
|
167
157
|
.option('--metadata', 'Print the extension\'s metadata instead of downloading it.')
|
|
168
158
|
.action((extensionId: string, { target, versionRange, output, metadata }) => {
|
|
169
|
-
const { registryUrl } = program.opts();
|
|
159
|
+
const { registryUrl, debug } = program.opts();
|
|
170
160
|
getExtension({ extensionId, target: target, version: versionRange, registryUrl, output, metadata })
|
|
171
|
-
.catch(handleError(
|
|
161
|
+
.catch(handleError(debug));
|
|
172
162
|
});
|
|
173
163
|
|
|
174
164
|
const verifyCmd = program.command('verify <extension.vsix>');
|
|
175
165
|
verifyCmd.description('Verify a downloaded package\'s signature against the registry\'s public key.')
|
|
176
166
|
.option('-t, --target <target>', 'Target architecture')
|
|
177
167
|
.action((packagePath: string, { target }) => {
|
|
178
|
-
const { registryUrl } = program.opts();
|
|
168
|
+
const { registryUrl, debug } = program.opts();
|
|
179
169
|
verify({ packagePath, target, registryUrl })
|
|
180
|
-
.catch(handleError(
|
|
170
|
+
.catch(handleError(debug));
|
|
181
171
|
});
|
|
182
172
|
|
|
183
173
|
const verifySignatureCmd = program.command('verify-signature');
|
|
@@ -187,43 +177,44 @@ module.exports = function (argv: string[]): void {
|
|
|
187
177
|
.requiredOption('-s, --signaturePath <path>', 'Path to the signature file.')
|
|
188
178
|
.requiredOption('-k, --publicKeyPath <path>', 'Path to the registry\'s public key file.')
|
|
189
179
|
.action(({ packagePath, manifestPath, signaturePath, publicKeyPath }) => {
|
|
180
|
+
const { debug } = program.opts();
|
|
190
181
|
verifySignature({ packagePath, manifestPath, signaturePath, publicKeyPath })
|
|
191
|
-
.catch(handleError(
|
|
182
|
+
.catch(handleError(debug));
|
|
192
183
|
});
|
|
193
184
|
|
|
194
185
|
const loginCmd = program.command('login <namespace>');
|
|
195
186
|
loginCmd.description('Adds a namespace to the list of known namespaces')
|
|
196
187
|
.action((namespace: string) => {
|
|
197
|
-
const { registryUrl, pat } = program.opts();
|
|
198
|
-
login({ namespace, registryUrl, pat }).catch(handleError(
|
|
188
|
+
const { registryUrl, pat, debug } = program.opts();
|
|
189
|
+
login({ namespace, registryUrl, pat }).catch(handleError(debug));
|
|
199
190
|
});
|
|
200
191
|
|
|
201
192
|
const logoutCmd = program.command('logout <namespace>');
|
|
202
193
|
logoutCmd.description('Removes a namespace from the list of known namespaces')
|
|
203
194
|
.action((namespace: string) => {
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
program
|
|
208
|
-
.command('*', '', { noHelp: true })
|
|
209
|
-
.action((cmd: commander.Command) => {
|
|
210
|
-
const availableCommands = program.commands.map((c: any) => c._name) as string[];
|
|
211
|
-
const actualCommand = cmd.args[0];
|
|
212
|
-
if (actualCommand) {
|
|
213
|
-
const suggestion = availableCommands.find(c => leven(c, actualCommand) < c.length * 0.4);
|
|
214
|
-
if (suggestion)
|
|
215
|
-
console.error(`Unknown command '${actualCommand}', did you mean '${suggestion}'?\n`);
|
|
216
|
-
else
|
|
217
|
-
console.error(`Unknown command '${actualCommand}'.\n`);
|
|
218
|
-
} else {
|
|
219
|
-
console.error('Unknown command.');
|
|
220
|
-
}
|
|
221
|
-
program.help();
|
|
195
|
+
const { debug } = program.opts();
|
|
196
|
+
logout(namespace).catch(handleError(debug));
|
|
222
197
|
});
|
|
223
198
|
|
|
224
|
-
|
|
199
|
+
// 'command:*' is deprecated in favor of catching the commander.unknownCommand error, but that
|
|
200
|
+
// error is reported (message written, process exited) by commander itself before a catch runs,
|
|
201
|
+
// so it can't reproduce this suggestion wording. Still supported for backwards compatibility.
|
|
202
|
+
program.on('command:*', (operands: string[]) => {
|
|
203
|
+
const availableCommands = program.commands.map(c => c.name());
|
|
204
|
+
const actualCommand = operands[0];
|
|
205
|
+
const suggestion = availableCommands.find(c => levenshtein(c, actualCommand) < c.length * 0.4);
|
|
206
|
+
if (suggestion)
|
|
207
|
+
console.error(`Unknown command '${actualCommand}', did you mean '${suggestion}'?\n`);
|
|
208
|
+
else
|
|
209
|
+
console.error(`Unknown command '${actualCommand}'.\n`);
|
|
210
|
+
process.exitCode = 1;
|
|
211
|
+
program.help();
|
|
212
|
+
});
|
|
225
213
|
|
|
226
|
-
if (
|
|
214
|
+
if (argv.length <= 2) {
|
|
215
|
+
// Commander 14 would otherwise print this to stderr and exit 1 during program.parse(argv) below.
|
|
227
216
|
program.help();
|
|
228
217
|
}
|
|
218
|
+
|
|
219
|
+
program.parse(argv);
|
|
229
220
|
};
|
package/src/oidc.ts
CHANGED
|
@@ -11,10 +11,9 @@
|
|
|
11
11
|
* SPDX-License-Identifier: EPL-2.0
|
|
12
12
|
*****************************************************************************/
|
|
13
13
|
|
|
14
|
-
import
|
|
15
|
-
import * as followRedirects from 'follow-redirects';
|
|
14
|
+
import { getJson } from './http';
|
|
16
15
|
import { TrustedPublishingOptions } from './trusted-publishing-options';
|
|
17
|
-
import { configuredTimeout
|
|
16
|
+
import { configuredTimeout } from './util';
|
|
18
17
|
|
|
19
18
|
/**
|
|
20
19
|
* Whether an OIDC ID token can be obtained without user interaction.
|
|
@@ -48,59 +47,18 @@ async function getGitHubActionsIdToken(audience: string): Promise<string> {
|
|
|
48
47
|
// the request URL already carries an api-version query parameter, so keep its query intact
|
|
49
48
|
const url = new URL(process.env.ACTIONS_ID_TOKEN_REQUEST_URL!);
|
|
50
49
|
url.searchParams.set('audience', audience);
|
|
50
|
+
// Sent with only these headers, so none of the registry's credentials reach the CI system's
|
|
51
|
+
// token service. OVSX_TIMEOUT still applies, as there are no registry options to read it from.
|
|
51
52
|
const response = await getJson<GitHubIdTokenResponse>(url, {
|
|
52
53
|
'Authorization': `Bearer ${process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN}`,
|
|
53
54
|
'Accept': 'application/json'
|
|
54
|
-
});
|
|
55
|
+
}, configuredTimeout());
|
|
55
56
|
if (!response.value) {
|
|
56
57
|
throw new Error('GitHub Actions did not return an OIDC ID token.');
|
|
57
58
|
}
|
|
58
59
|
return response.value;
|
|
59
60
|
}
|
|
60
61
|
|
|
61
|
-
/**
|
|
62
|
-
* Minimal JSON GET that is not bound to the registry: the request must not carry any registry
|
|
63
|
-
* credentials, as it is sent to the CI system's token service.
|
|
64
|
-
*/
|
|
65
|
-
function getJson<T>(url: URL, headers: http.OutgoingHttpHeaders): Promise<T> {
|
|
66
|
-
return new Promise((resolve, reject) => {
|
|
67
|
-
const protocol = url.protocol === 'https:' ? followRedirects.https : followRedirects.http;
|
|
68
|
-
// OVSX_TIMEOUT covers this request too. Read here rather than taken from the registry's
|
|
69
|
-
// options, since this one carries none of the registry's configuration - only the clock is
|
|
70
|
-
// shared, and having two of those to explain would be worse.
|
|
71
|
-
const timeout = configuredTimeout();
|
|
72
|
-
const request = protocol.request(url, { method: 'GET', headers, timeout }, response => {
|
|
73
|
-
response.setEncoding('utf-8');
|
|
74
|
-
let json = '';
|
|
75
|
-
// See Registry.getJsonResponse: a connection lost mid-body never fires 'end', so the
|
|
76
|
-
// response's own error has to be what settles the promise.
|
|
77
|
-
response.on('error', reject);
|
|
78
|
-
response.on('data', chunk => json += chunk);
|
|
79
|
-
response.on('end', () => {
|
|
80
|
-
if (response.statusCode !== undefined && (response.statusCode < 200 || response.statusCode > 299)) {
|
|
81
|
-
reject(statusError(response));
|
|
82
|
-
} else {
|
|
83
|
-
try {
|
|
84
|
-
resolve(JSON.parse(json));
|
|
85
|
-
} catch (err) {
|
|
86
|
-
reject(err);
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
});
|
|
90
|
-
});
|
|
91
|
-
request.on('error', reject);
|
|
92
|
-
// The timeout option only raises an event, so the request has to be destroyed for it to mean
|
|
93
|
-
// anything; the error then arrives at the handler above.
|
|
94
|
-
request.on('timeout', () => {
|
|
95
|
-
// Rejected before the teardown, so the timeout is what the caller sees rather than
|
|
96
|
-
// whichever error destroying the request happens to raise first.
|
|
97
|
-
reject(new Error(`No response from ${redactUrl(url)} for ${timeout} ms.`));
|
|
98
|
-
request.destroy();
|
|
99
|
-
});
|
|
100
|
-
request.end();
|
|
101
|
-
});
|
|
102
|
-
}
|
|
103
|
-
|
|
104
62
|
interface GitHubIdTokenResponse {
|
|
105
63
|
count?: number;
|
|
106
64
|
value?: string;
|
package/src/pat.ts
CHANGED
|
@@ -38,19 +38,32 @@ export async function requestPAT(namespace: string, options: CreateNamespaceOpti
|
|
|
38
38
|
return pat;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
export
|
|
41
|
+
export interface ResolvedPAT {
|
|
42
|
+
pat: string;
|
|
43
|
+
/** Stores a prompted token, warning instead of throwing; call it only once the registry has accepted the token. */
|
|
44
|
+
save(): Promise<void>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function getPAT(namespace: string, options: CreateNamespaceOptions | PublishOptions | VerifyPatOptions, verify: boolean = true): Promise<ResolvedPAT> {
|
|
48
|
+
const nothingToSave = async () => undefined;
|
|
42
49
|
if (options?.pat) {
|
|
43
|
-
return options.pat;
|
|
50
|
+
return { pat: options.pat, save: nothingToSave };
|
|
44
51
|
}
|
|
45
52
|
|
|
46
53
|
const store = await openDefaultStore();
|
|
47
|
-
|
|
48
|
-
if (
|
|
49
|
-
return pat;
|
|
54
|
+
const stored = await store.get(namespace);
|
|
55
|
+
if (stored) {
|
|
56
|
+
return { pat: stored, save: nothingToSave };
|
|
50
57
|
}
|
|
51
58
|
|
|
52
|
-
pat = await requestPAT(namespace, options, verify);
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
59
|
+
const pat = await requestPAT(namespace, options, verify);
|
|
60
|
+
// a storage failure must not turn a completed registry operation into a reported failure
|
|
61
|
+
const save = async () => {
|
|
62
|
+
try {
|
|
63
|
+
await store.add(namespace, pat);
|
|
64
|
+
} catch (e) {
|
|
65
|
+
console.warn(`Could not store the token: ${e instanceof Error ? e.message : e}`);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
return { pat, save };
|
|
56
69
|
}
|