ovsx 1.1.1 → 1.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/README.md +71 -1
- package/lib/get.js +1 -1
- package/lib/get.js.map +1 -1
- package/lib/index.d.ts +4 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +4 -0
- package/lib/index.js.map +1 -1
- package/lib/list-options.d.ts +24 -0
- package/lib/list-options.d.ts.map +1 -0
- package/lib/list-options.js +15 -0
- package/lib/list-options.js.map +1 -0
- package/lib/list.d.ts +18 -0
- package/lib/list.d.ts.map +1 -0
- package/lib/list.js +46 -0
- package/lib/list.js.map +1 -0
- package/lib/main.js +78 -2
- package/lib/main.js.map +1 -1
- package/lib/oidc.d.ts +22 -0
- package/lib/oidc.d.ts.map +1 -0
- package/lib/oidc.js +101 -0
- package/lib/oidc.js.map +1 -0
- package/lib/publish-options.d.ts +8 -1
- package/lib/publish-options.d.ts.map +1 -1
- package/lib/publish.d.ts.map +1 -1
- package/lib/publish.js +73 -4
- package/lib/publish.js.map +1 -1
- package/lib/registry-options.d.ts +8 -0
- package/lib/registry-options.d.ts.map +1 -1
- package/lib/registry.d.ts +119 -1
- package/lib/registry.d.ts.map +1 -1
- package/lib/registry.js +187 -19
- package/lib/registry.js.map +1 -1
- package/lib/search-options.d.ts +48 -0
- package/lib/search-options.d.ts.map +1 -0
- package/lib/search-options.js +15 -0
- package/lib/search-options.js.map +1 -0
- package/lib/search.d.ts +22 -0
- package/lib/search.d.ts.map +1 -0
- package/lib/search.js +86 -0
- package/lib/search.js.map +1 -0
- package/lib/show-options.d.ts +33 -0
- package/lib/show-options.d.ts.map +1 -0
- package/lib/show-options.js +15 -0
- package/lib/show-options.js.map +1 -0
- package/lib/show.d.ts +18 -0
- package/lib/show.d.ts.map +1 -0
- package/lib/show.js +289 -0
- package/lib/show.js.map +1 -0
- package/lib/table.d.ts +27 -0
- package/lib/table.d.ts.map +1 -0
- package/lib/table.js +56 -0
- package/lib/table.js.map +1 -0
- package/lib/trusted-publishing-options.d.ts +30 -0
- package/lib/trusted-publishing-options.d.ts.map +1 -0
- package/lib/trusted-publishing-options.js +15 -0
- package/lib/trusted-publishing-options.js.map +1 -0
- package/lib/trusted-publishing.d.ts +35 -0
- package/lib/trusted-publishing.d.ts.map +1 -0
- package/lib/trusted-publishing.js +112 -0
- package/lib/trusted-publishing.js.map +1 -0
- package/lib/unpublish-options.d.ts +34 -0
- package/lib/unpublish-options.d.ts.map +1 -0
- package/lib/unpublish-options.js +15 -0
- package/lib/unpublish-options.js.map +1 -0
- package/lib/unpublish.d.ts +22 -0
- package/lib/unpublish.d.ts.map +1 -0
- package/lib/unpublish.js +137 -0
- package/lib/unpublish.js.map +1 -0
- package/lib/util.d.ts +39 -1
- package/lib/util.d.ts.map +1 -1
- package/lib/util.js +91 -5
- package/lib/util.js.map +1 -1
- package/lib/verify-options.d.ts +24 -0
- package/lib/verify-options.d.ts.map +1 -0
- package/lib/verify-options.js +15 -0
- package/lib/verify-options.js.map +1 -0
- package/lib/verify-signature-options.d.ts +32 -0
- package/lib/verify-signature-options.d.ts.map +1 -0
- package/lib/verify-signature-options.js +15 -0
- package/lib/verify-signature-options.js.map +1 -0
- package/lib/verify-signature.d.ts +28 -0
- package/lib/verify-signature.d.ts.map +1 -0
- package/lib/verify-signature.js +105 -0
- package/lib/verify-signature.js.map +1 -0
- package/lib/verify.d.ts +21 -0
- package/lib/verify.d.ts.map +1 -0
- package/lib/verify.js +97 -0
- package/lib/verify.js.map +1 -0
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- package/lib/zip.d.ts +10 -0
- package/lib/zip.d.ts.map +1 -1
- package/lib/zip.js +31 -0
- package/lib/zip.js.map +1 -1
- package/package.json +9 -5
- package/src/get.ts +1 -1
- package/src/index.ts +4 -0
- package/src/list-options.ts +25 -0
- package/src/list.ts +48 -0
- package/src/main.ts +89 -3
- package/src/oidc.ts +107 -0
- package/src/publish-options.ts +8 -1
- package/src/publish.ts +97 -5
- package/src/registry-options.ts +8 -0
- package/src/registry.ts +292 -19
- package/src/search-options.ts +49 -0
- package/src/search.ts +95 -0
- package/src/show-options.ts +34 -0
- package/src/show.ts +328 -0
- package/src/table.ts +53 -0
- package/src/trusted-publishing-options.ts +30 -0
- package/src/trusted-publishing.ts +148 -0
- package/src/unpublish-options.ts +35 -0
- package/src/unpublish.ts +160 -0
- package/src/util.ts +102 -5
- package/src/verify-options.ts +25 -0
- package/src/verify-signature-options.ts +32 -0
- package/src/verify-signature.ts +124 -0
- package/src/verify.ts +107 -0
- package/src/version.ts +1 -1
- package/src/zip.ts +31 -1
package/src/oidc.ts
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
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 followRedirects from 'follow-redirects';
|
|
16
|
+
import { TrustedPublishingOptions } from './trusted-publishing-options';
|
|
17
|
+
import { configuredTimeout, redactUrl, statusError } from './util';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Whether an OIDC ID token can be obtained without user interaction.
|
|
21
|
+
*/
|
|
22
|
+
export function hasIdTokenSource(options: TrustedPublishingOptions): boolean {
|
|
23
|
+
return Boolean(options.idToken) || isGitHubActionsIdTokenAvailable();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Obtains an OIDC ID token for the given audience from the surrounding CI system.
|
|
28
|
+
*/
|
|
29
|
+
export async function getIdToken(audience: string, options: TrustedPublishingOptions): Promise<string> {
|
|
30
|
+
// CI systems such as GitLab CI provide the ID token directly as an environment variable
|
|
31
|
+
if (options.idToken) {
|
|
32
|
+
return options.idToken;
|
|
33
|
+
}
|
|
34
|
+
if (isGitHubActionsIdTokenAvailable()) {
|
|
35
|
+
return getGitHubActionsIdToken(audience);
|
|
36
|
+
}
|
|
37
|
+
throw new Error('No OIDC ID token available for trusted publishing.\n'
|
|
38
|
+
+ "On GitHub Actions, grant the job the 'id-token: write' permission.\n"
|
|
39
|
+
+ 'On other CI systems, pass the ID token via the --idToken argument '
|
|
40
|
+
+ 'or the OVSX_ID_TOKEN environment variable.');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function isGitHubActionsIdTokenAvailable(): boolean {
|
|
44
|
+
return Boolean(process.env.ACTIONS_ID_TOKEN_REQUEST_URL && process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function getGitHubActionsIdToken(audience: string): Promise<string> {
|
|
48
|
+
// the request URL already carries an api-version query parameter, so keep its query intact
|
|
49
|
+
const url = new URL(process.env.ACTIONS_ID_TOKEN_REQUEST_URL!);
|
|
50
|
+
url.searchParams.set('audience', audience);
|
|
51
|
+
const response = await getJson<GitHubIdTokenResponse>(url, {
|
|
52
|
+
'Authorization': `Bearer ${process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN}`,
|
|
53
|
+
'Accept': 'application/json'
|
|
54
|
+
});
|
|
55
|
+
if (!response.value) {
|
|
56
|
+
throw new Error('GitHub Actions did not return an OIDC ID token.');
|
|
57
|
+
}
|
|
58
|
+
return response.value;
|
|
59
|
+
}
|
|
60
|
+
|
|
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
|
+
interface GitHubIdTokenResponse {
|
|
105
|
+
count?: number;
|
|
106
|
+
value?: string;
|
|
107
|
+
}
|
package/src/publish-options.ts
CHANGED
|
@@ -8,8 +8,9 @@
|
|
|
8
8
|
* SPDX-License-Identifier: EPL-2.0
|
|
9
9
|
********************************************************************************/
|
|
10
10
|
import { RegistryOptions } from './registry-options';
|
|
11
|
+
import { TrustedPublishingOptions } from './trusted-publishing-options';
|
|
11
12
|
|
|
12
|
-
export interface PublishCommonOptions extends RegistryOptions {
|
|
13
|
+
export interface PublishCommonOptions extends RegistryOptions, TrustedPublishingOptions {
|
|
13
14
|
/**
|
|
14
15
|
* Path to the vsix file to be published. Cannot be used together with `packagePath`.
|
|
15
16
|
*/
|
|
@@ -26,6 +27,12 @@ export interface PublishCommonOptions extends RegistryOptions {
|
|
|
26
27
|
* Should use `yarn` instead of `npm`. Only valid with `packagePath`.
|
|
27
28
|
*/
|
|
28
29
|
yarn?: boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Recurse into symlinked directories when collecting the files to pack, instead of packing each
|
|
32
|
+
* symlink as a file. Needed for a `node_modules` built out of symlinks, as pnpm's is. Only valid
|
|
33
|
+
* with `packagePath`.
|
|
34
|
+
*/
|
|
35
|
+
followSymlinks?: boolean;
|
|
29
36
|
/**
|
|
30
37
|
* Mark this package as a pre-release. Only valid with `packagePath`.
|
|
31
38
|
*/
|
package/src/publish.ts
CHANGED
|
@@ -7,31 +7,53 @@
|
|
|
7
7
|
*
|
|
8
8
|
* SPDX-License-Identifier: EPL-2.0
|
|
9
9
|
********************************************************************************/
|
|
10
|
+
import * as fs from 'fs';
|
|
10
11
|
import { createVSIX, IPackageOptions } from '@vscode/vsce';
|
|
11
12
|
import { getPAT } from './pat';
|
|
12
|
-
import { createTempFile, addEnvOptions } from './util';
|
|
13
|
+
import { createTempFile, addEnvOptions, addTrustedPublishingEnvOptions, formatBytes, StatusError } from './util';
|
|
13
14
|
import { Extension, Registry } from './registry';
|
|
14
15
|
import { checkLicense } from './check-license';
|
|
15
16
|
import { readVSIXPackage } from './zip';
|
|
16
17
|
import { PublishOptions, PublishCommonOptions } from './publish-options';
|
|
18
|
+
import { getTrustedPublishingToken, refreshTrustedPublishingToken, useTrustedPublishing } from './trusted-publishing';
|
|
17
19
|
|
|
18
20
|
/**
|
|
19
21
|
* Publishes an extension.
|
|
20
22
|
*/
|
|
21
23
|
export async function publish(options: PublishOptions = {}): Promise<PromiseSettledResult<void>[]> {
|
|
22
24
|
addEnvOptions(options);
|
|
25
|
+
addTrustedPublishingEnvOptions(options);
|
|
26
|
+
|
|
27
|
+
// Looked up once and shared by every target/package below, rather than once per artifact.
|
|
28
|
+
const maxExtensionSize = await getMaxExtensionSize(new Registry(options));
|
|
29
|
+
|
|
23
30
|
const internalPublishOptions: InternalPublishOptions[] = [];
|
|
24
31
|
const packagePaths = options.packagePath || [undefined];
|
|
25
32
|
const targets = options.targets || [undefined];
|
|
26
33
|
for (const packagePath of packagePaths) {
|
|
27
34
|
for (const target of targets) {
|
|
28
|
-
internalPublishOptions.push({ ...options, packagePath: packagePath, target: target });
|
|
35
|
+
internalPublishOptions.push({ ...options, packagePath: packagePath, target: target, maxExtensionSize });
|
|
29
36
|
}
|
|
30
37
|
}
|
|
31
38
|
|
|
32
39
|
return Promise.allSettled(internalPublishOptions.map(publishOptions => doPublish(publishOptions)));
|
|
33
40
|
}
|
|
34
41
|
|
|
42
|
+
/**
|
|
43
|
+
* Looks up the registry's configured extension size limit, so an oversized package can be rejected
|
|
44
|
+
* locally before the upload instead of after transferring the whole payload.
|
|
45
|
+
*
|
|
46
|
+
* Best-effort: registries that don't expose `/api/version`, or don't report a limit, return
|
|
47
|
+
* `undefined` here, and the upload proceeds to let the server enforce its own limit as before.
|
|
48
|
+
*/
|
|
49
|
+
async function getMaxExtensionSize(registry: Registry): Promise<number | undefined> {
|
|
50
|
+
try {
|
|
51
|
+
return (await registry.getRegistryVersion()).maxExtensionSize || undefined;
|
|
52
|
+
} catch {
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
35
57
|
async function doPublish(options: InternalPublishOptions = {}): Promise<void> {
|
|
36
58
|
// if the packagePath is a link to a vsix, don't need to package it
|
|
37
59
|
if (options.packagePath?.endsWith('.vsix')) {
|
|
@@ -47,14 +69,24 @@ async function doPublish(options: InternalPublishOptions = {}): Promise<void> {
|
|
|
47
69
|
console.warn("Ignoring option '--pre-release' for prepackaged extension.");
|
|
48
70
|
}
|
|
49
71
|
|
|
72
|
+
await ensureWithinSizeLimit(options.extensionFile!, options.maxExtensionSize, registry.url);
|
|
73
|
+
|
|
74
|
+
// Set only when this publish obtained the token itself through trusted publishing, which is the one
|
|
75
|
+
// case where a refusal can be answered by asking for a new token.
|
|
76
|
+
let exchanged: { namespace: string; extension: string } | undefined;
|
|
50
77
|
if (!options.pat) {
|
|
51
|
-
const
|
|
52
|
-
|
|
78
|
+
const manifest = await readVSIXPackage(options.extensionFile!);
|
|
79
|
+
if (useTrustedPublishing(options)) {
|
|
80
|
+
exchanged = { namespace: manifest.publisher, extension: manifest.name };
|
|
81
|
+
options.pat = await getTrustedPublishingToken(registry, manifest.publisher, manifest.name, options);
|
|
82
|
+
} else {
|
|
83
|
+
options.pat = await getPAT(manifest.publisher, options);
|
|
84
|
+
}
|
|
53
85
|
}
|
|
54
86
|
|
|
55
87
|
let extension: Extension | undefined;
|
|
56
88
|
try {
|
|
57
|
-
extension = await registry
|
|
89
|
+
extension = await doRegistryPublish(registry, options, exchanged);
|
|
58
90
|
} catch (err) {
|
|
59
91
|
if (options.skipDuplicate && err.message.endsWith('is already published.')) {
|
|
60
92
|
console.log(err.message + ' Skipping publish.');
|
|
@@ -79,6 +111,59 @@ async function doPublish(options: InternalPublishOptions = {}): Promise<void> {
|
|
|
79
111
|
}
|
|
80
112
|
}
|
|
81
113
|
|
|
114
|
+
/**
|
|
115
|
+
* Publishes the package, exchanging the ID token again if the registry refuses the trusted publishing
|
|
116
|
+
* token this was using. The issued token is short-lived and shared by every target platform of a
|
|
117
|
+
* release, so publishing a wide fan-out of large packages can outlive it and be refused partway
|
|
118
|
+
* through - having authorised the release and then failing it over an expiry would be the wrong call.
|
|
119
|
+
*
|
|
120
|
+
* Only ever retried once, and only for a token obtained here: nothing this can do makes a token the
|
|
121
|
+
* user supplied valid, and a second refusal means the token is not the problem.
|
|
122
|
+
*/
|
|
123
|
+
async function doRegistryPublish(
|
|
124
|
+
registry: Registry,
|
|
125
|
+
options: InternalPublishOptions,
|
|
126
|
+
exchanged: { namespace: string; extension: string } | undefined
|
|
127
|
+
): Promise<Extension> {
|
|
128
|
+
try {
|
|
129
|
+
return await registry.publish(options.extensionFile!, options.pat!);
|
|
130
|
+
} catch (err) {
|
|
131
|
+
if (!exchanged || (err as StatusError)?.status !== 401) {
|
|
132
|
+
throw err;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
console.log('The registry refused the publishing token, requesting a new one');
|
|
136
|
+
options.pat = await refreshTrustedPublishingToken(
|
|
137
|
+
registry,
|
|
138
|
+
exchanged.namespace,
|
|
139
|
+
exchanged.extension,
|
|
140
|
+
options,
|
|
141
|
+
options.pat!
|
|
142
|
+
);
|
|
143
|
+
return registry.publish(options.extensionFile!, options.pat);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Fails fast with an actionable message when the packaged extension is already known to exceed the
|
|
149
|
+
* registry's configured size limit, rather than uploading the whole file only to have the server
|
|
150
|
+
* reject it. `maxSize` is `undefined` when the limit couldn't be determined, in which case the check
|
|
151
|
+
* is skipped and the upload is left to the server to accept or reject.
|
|
152
|
+
*/
|
|
153
|
+
async function ensureWithinSizeLimit(extensionFile: string, maxSize: number | undefined, registryUrl: string): Promise<void> {
|
|
154
|
+
if (!maxSize) {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const { size } = await fs.promises.stat(extensionFile);
|
|
159
|
+
if (size > maxSize) {
|
|
160
|
+
throw new Error(
|
|
161
|
+
`The extension package (${formatBytes(size)}) exceeds the size limit of ${formatBytes(maxSize)} `
|
|
162
|
+
+ `accepted by the registry at ${registryUrl}.`
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
82
167
|
async function packageExtension(options: InternalPublishOptions, registry: Registry): Promise<void> {
|
|
83
168
|
if (registry.requiresLicense) {
|
|
84
169
|
await checkLicense(options.packagePath!);
|
|
@@ -92,6 +177,7 @@ async function packageExtension(options: InternalPublishOptions, registry: Regis
|
|
|
92
177
|
baseContentUrl: options.baseContentUrl,
|
|
93
178
|
baseImagesUrl: options.baseImagesUrl,
|
|
94
179
|
useYarn: options.yarn,
|
|
180
|
+
followSymlinks: options.followSymlinks,
|
|
95
181
|
dependencies: options.dependencies,
|
|
96
182
|
preRelease: options.preRelease,
|
|
97
183
|
allowMissingRepository: options.allowMissingRepository,
|
|
@@ -120,4 +206,10 @@ interface InternalPublishOptions extends PublishCommonOptions {
|
|
|
120
206
|
* Whether to do dependency detection via npm or yarn
|
|
121
207
|
*/
|
|
122
208
|
dependencies?: boolean;
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* The registry's configured extension size limit in bytes, looked up once via `/api/version` and
|
|
212
|
+
* shared across every target/package being published. `undefined` when it couldn't be determined.
|
|
213
|
+
*/
|
|
214
|
+
maxExtensionSize?: number;
|
|
123
215
|
}
|
package/src/registry-options.ts
CHANGED
|
@@ -33,4 +33,12 @@ export interface RegistryOptions {
|
|
|
33
33
|
* Maximal request body size for publishing.
|
|
34
34
|
*/
|
|
35
35
|
maxPublishSize?: number;
|
|
36
|
+
/**
|
|
37
|
+
* How long to wait, in milliseconds, for a request to make progress before giving up.
|
|
38
|
+
*
|
|
39
|
+
* This is an inactivity timeout rather than a deadline: any byte sent or received resets it, so
|
|
40
|
+
* a large package downloading slowly is not cut off, while a connection that has gone quiet does
|
|
41
|
+
* not hold the command open indefinitely. Zero disables it.
|
|
42
|
+
*/
|
|
43
|
+
timeout?: number;
|
|
36
44
|
}
|