ovsx 1.3.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 +5 -1
- package/lib/check-license.js +2 -2
- 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/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 +6 -20
- package/lib/main.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 +58 -19
- package/lib/publish.js.map +1 -1
- package/lib/registry.d.ts +2 -2
- package/lib/registry.d.ts.map +1 -1
- package/lib/registry.js +18 -4
- 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 +3 -3
- package/lib/util.d.ts.map +1 -1
- package/lib/util.js +7 -4
- 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/package.json +7 -5
- package/src/check-license.ts +1 -1
- package/src/create-namespace.ts +3 -1
- package/src/index.ts +0 -3
- package/src/main.ts +6 -20
- package/src/pat.ts +22 -9
- package/src/publish.ts +60 -19
- package/src/registry.ts +23 -4
- 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 +7 -4
- package/src/verify-pat.ts +3 -1
package/src/publish.ts
CHANGED
|
@@ -35,24 +35,68 @@ export async function publish(options: PublishOptions = {}): Promise<PromiseSett
|
|
|
35
35
|
addEnvOptions(options);
|
|
36
36
|
addTrustedPublishingEnvOptions(options);
|
|
37
37
|
|
|
38
|
+
const internalPublishOptions: InternalPublishOptions[] = [];
|
|
39
|
+
const prepackagedFiles: string[] = [];
|
|
40
|
+
const packagePaths = options.packagePath || [undefined];
|
|
41
|
+
const targets = options.targets || [undefined];
|
|
42
|
+
for (const packagePath of packagePaths) {
|
|
43
|
+
const extensionFile = options.extensionFile ?? (packagePath?.endsWith('.vsix') ? packagePath : undefined);
|
|
44
|
+
if (extensionFile !== undefined) {
|
|
45
|
+
prepackagedFiles.push(extensionFile);
|
|
46
|
+
// A package is already built for its target, so it is published once, not once per target.
|
|
47
|
+
internalPublishOptions.push({ ...options, extensionFile, packagePath: undefined, target: undefined });
|
|
48
|
+
} else {
|
|
49
|
+
for (const target of targets) {
|
|
50
|
+
internalPublishOptions.push({ ...options, packagePath, target });
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
// Before the registry is contacted, so a slow or failing lookup cannot keep these from being seen.
|
|
55
|
+
warnIgnoredPackagingOptions(options, prepackagedFiles);
|
|
56
|
+
|
|
38
57
|
// Shared by every target/package below, rather than one per artifact: besides saving the extra
|
|
39
58
|
// `/api/version` round trips, it's also where the registry's reported version is cached (see
|
|
40
59
|
// Registry.tokenQuery), so a wide fan-out doesn't repeat that lookup once per target either.
|
|
41
60
|
const registry = new Registry(options);
|
|
42
61
|
const maxExtensionSize = await getMaxExtensionSize(registry);
|
|
43
62
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
63
|
+
return Promise.allSettled(internalPublishOptions.map(publishOptions =>
|
|
64
|
+
doPublish(registry, { ...publishOptions, maxExtensionSize })));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Names the files in the warning: next to a directory being packaged, the options still apply to that.
|
|
69
|
+
*/
|
|
70
|
+
function warnIgnoredPackagingOptions(options: PublishOptions, prepackagedFiles: string[]): void {
|
|
71
|
+
const files = [...new Set(prepackagedFiles)];
|
|
72
|
+
if (files.length === 0) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const subject = `${files.length === 1 ? 'extension' : 'extensions'} ${files.map(file => `'${file}'`).join(', ')}`;
|
|
76
|
+
for (const [flag, key] of PACKAGING_OPTIONS) {
|
|
77
|
+
// An explicit `false` is not a flag that was given.
|
|
78
|
+
if (options[key] !== undefined && options[key] !== false) {
|
|
79
|
+
console.warn(`Ignoring option '${flag}' for prepackaged ${subject}.`);
|
|
50
80
|
}
|
|
51
81
|
}
|
|
52
|
-
|
|
53
|
-
return Promise.allSettled(internalPublishOptions.map(publishOptions => doPublish(registry, publishOptions)));
|
|
54
82
|
}
|
|
55
83
|
|
|
84
|
+
/**
|
|
85
|
+
* Options that only affect packaging, with the CLI flag that sets each, in the order they are reported
|
|
86
|
+
* as ignored for a prepackaged extension. `--no-dependencies` is left out: the CLI always sets
|
|
87
|
+
* `dependencies`, so whether it was given cannot be told apart.
|
|
88
|
+
*/
|
|
89
|
+
export const PACKAGING_OPTIONS: ReadonlyArray<readonly [string, keyof PublishOptions]> = [
|
|
90
|
+
['--target', 'targets'],
|
|
91
|
+
['--baseContentUrl', 'baseContentUrl'],
|
|
92
|
+
['--baseImagesUrl', 'baseImagesUrl'],
|
|
93
|
+
['--yarn', 'yarn'],
|
|
94
|
+
['--follow-symlinks', 'followSymlinks'],
|
|
95
|
+
['--packageVersion', 'packageVersion'],
|
|
96
|
+
['--allow-missing-repository', 'allowMissingRepository'],
|
|
97
|
+
['--pre-release', 'preRelease']
|
|
98
|
+
];
|
|
99
|
+
|
|
56
100
|
/**
|
|
57
101
|
* Looks up the registry's default extension size limit, so an oversized package can be flagged
|
|
58
102
|
* before the upload instead of after transferring the whole payload.
|
|
@@ -69,20 +113,12 @@ async function getMaxExtensionSize(registry: Registry): Promise<number | undefin
|
|
|
69
113
|
}
|
|
70
114
|
|
|
71
115
|
async function doPublish(registry: Registry, options: InternalPublishOptions = {}): Promise<void> {
|
|
72
|
-
// if the packagePath is a link to a vsix, don't need to package it
|
|
73
|
-
if (options.packagePath?.endsWith('.vsix')) {
|
|
74
|
-
options.extensionFile = options.packagePath;
|
|
75
|
-
delete options.packagePath;
|
|
76
|
-
delete options.target;
|
|
77
|
-
}
|
|
78
116
|
// Only a package created here is a temp file; one the user supplied must never be deleted.
|
|
79
117
|
const packaged = !options.extensionFile;
|
|
80
118
|
try {
|
|
81
119
|
if (packaged) {
|
|
82
120
|
await packageExtension(options, registry);
|
|
83
121
|
console.log(); // new line
|
|
84
|
-
} else if (options.preRelease) {
|
|
85
|
-
console.warn("Ignoring option '--pre-release' for prepackaged extension.");
|
|
86
122
|
}
|
|
87
123
|
await publishExtensionFile(registry, options);
|
|
88
124
|
} finally {
|
|
@@ -109,7 +145,10 @@ async function publishExtensionFile(registry: Registry, options: InternalPublish
|
|
|
109
145
|
exchanged = { namespace: manifest.publisher, extension: manifest.name };
|
|
110
146
|
options.pat = await getTrustedPublishingToken(registry, manifest.publisher, manifest.name, options);
|
|
111
147
|
} else {
|
|
112
|
-
|
|
148
|
+
// getPAT has verified a prompted token already, so it can be stored before publishing
|
|
149
|
+
const { pat, save } = await getPAT(manifest.publisher, options);
|
|
150
|
+
options.pat = pat;
|
|
151
|
+
await save();
|
|
113
152
|
}
|
|
114
153
|
}
|
|
115
154
|
|
|
@@ -210,7 +249,8 @@ async function ensureWithinSizeLimit(
|
|
|
210
249
|
const fallback = options.maxExtensionSize;
|
|
211
250
|
if (fallback && size > fallback) {
|
|
212
251
|
console.warn(
|
|
213
|
-
`The extension package (${formatBytes(size)}) exceeds the default size limit of
|
|
252
|
+
`The extension package (${formatBytes(size)}, ${size} bytes) exceeds the default size limit of `
|
|
253
|
+
+ `${formatBytes(fallback)} (${fallback} bytes) `
|
|
214
254
|
+ `reported by the registry at ${registry.url}, which is too old to report the limit for a `
|
|
215
255
|
+ `single namespace. Publishing anyway: the namespace may have a different limit configured, `
|
|
216
256
|
+ `and the registry decides.`
|
|
@@ -227,7 +267,8 @@ async function ensureWithinSizeLimit(
|
|
|
227
267
|
|
|
228
268
|
if (size > limit.limit) {
|
|
229
269
|
throw new Error(
|
|
230
|
-
`The extension package (${formatBytes(size)}) exceeds the size limit of
|
|
270
|
+
`The extension package (${formatBytes(size)}, ${size} bytes) exceeds the size limit of `
|
|
271
|
+
+ `${formatBytes(limit.limit)} (${limit.limit} bytes) `
|
|
231
272
|
+ `for ${manifest.publisher}.${manifest.name} at ${registry.url}.`
|
|
232
273
|
);
|
|
233
274
|
}
|
package/src/registry.ts
CHANGED
|
@@ -107,12 +107,23 @@ export class Registry {
|
|
|
107
107
|
}
|
|
108
108
|
|
|
109
109
|
/**
|
|
110
|
-
* Cached per `Registry` instance - callers like `tokenQuery` and `unpublish`'s own version check
|
|
110
|
+
* Cached per `Registry` instance once it has succeeded - callers like `tokenQuery` and `unpublish`'s own version check
|
|
111
111
|
* would otherwise each fetch it separately, doubling the round trip for a single command.
|
|
112
112
|
*/
|
|
113
113
|
getRegistryVersion(): Promise<RegistryVersion> {
|
|
114
114
|
try {
|
|
115
|
-
|
|
115
|
+
if (this.registryVersion) {
|
|
116
|
+
return this.registryVersion;
|
|
117
|
+
}
|
|
118
|
+
const pending: Promise<RegistryVersion> = this.getJson(this.getUrl(['api', 'version']));
|
|
119
|
+
this.registryVersion = pending;
|
|
120
|
+
// A failure is not remembered, so a later caller asks again rather than inheriting it.
|
|
121
|
+
pending.catch(() => {
|
|
122
|
+
if (this.registryVersion === pending) {
|
|
123
|
+
this.registryVersion = undefined;
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
return pending;
|
|
116
127
|
} catch (err) {
|
|
117
128
|
return rejectError(err);
|
|
118
129
|
}
|
|
@@ -144,10 +155,15 @@ export class Registry {
|
|
|
144
155
|
return Math.max(this.maxPublishSize, sizeLimit);
|
|
145
156
|
}
|
|
146
157
|
|
|
147
|
-
requestTrustedPublishingToken(
|
|
158
|
+
requestTrustedPublishingToken(
|
|
159
|
+
namespace: string,
|
|
160
|
+
extension: string,
|
|
161
|
+
idToken: string,
|
|
162
|
+
registrationCode?: string
|
|
163
|
+
): Promise<AccessToken> {
|
|
148
164
|
try {
|
|
149
165
|
const url = this.getUrl(['api', '-', 'trusted-publishing', 'token']);
|
|
150
|
-
const request = { namespace, extension, token: idToken };
|
|
166
|
+
const request = { namespace, extension, token: idToken, registrationCode };
|
|
151
167
|
return this.post(JSON.stringify(request), url, {
|
|
152
168
|
'Content-Type': 'application/json'
|
|
153
169
|
}, DEFAULT_TOKEN_REQUEST_SIZE);
|
|
@@ -291,6 +307,9 @@ export class Registry {
|
|
|
291
307
|
send(url, this.getRequestOptions(), undefined, response => {
|
|
292
308
|
if (response.statusCode !== undefined && (response.statusCode < 200 || response.statusCode > 299)) {
|
|
293
309
|
response.resume();
|
|
310
|
+
// Claimed here so a timeout or reset while the error body drains does not run fail().
|
|
311
|
+
// Nothing was written yet, so there is nothing to clean up.
|
|
312
|
+
failed = true;
|
|
294
313
|
reject(statusError(response));
|
|
295
314
|
return;
|
|
296
315
|
}
|
|
@@ -27,4 +27,9 @@ export interface TrustedPublishingOptions {
|
|
|
27
27
|
* the audience the registry expects.
|
|
28
28
|
*/
|
|
29
29
|
oidcAudience?: string;
|
|
30
|
+
/**
|
|
31
|
+
* The registration code shown when the trusted publisher of a private repository was registered.
|
|
32
|
+
* Only the first publish needs it: it completes the registration, and the code is used up.
|
|
33
|
+
*/
|
|
34
|
+
registrationCode?: string;
|
|
30
35
|
}
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
import { getIdToken, hasIdTokenSource } from './oidc';
|
|
15
15
|
import { AccessToken, Registry } from './registry';
|
|
16
|
-
import { StatusError } from './util';
|
|
16
|
+
import { StatusError, redactUrl } from './util';
|
|
17
17
|
import { TrustedPublishingOptions } from './trusted-publishing-options';
|
|
18
18
|
|
|
19
19
|
const tokens = new Map<string, Promise<string>>();
|
|
@@ -100,12 +100,13 @@ async function requestToken(
|
|
|
100
100
|
extension: string,
|
|
101
101
|
options: TrustedPublishingOptions
|
|
102
102
|
): Promise<string> {
|
|
103
|
-
|
|
103
|
+
requireSecureRegistry(registry);
|
|
104
|
+
const audience = options.oidcAudience ?? await expectedAudience(registry);
|
|
104
105
|
const idToken = await getIdToken(audience, options);
|
|
105
106
|
|
|
106
107
|
let result: AccessToken;
|
|
107
108
|
try {
|
|
108
|
-
result = await exchangeWithRetry(registry, namespace, extension, idToken);
|
|
109
|
+
result = await exchangeWithRetry(registry, namespace, extension, idToken, options.registrationCode);
|
|
109
110
|
} catch (err) {
|
|
110
111
|
throw new Error(`${err.message}\n${registrationHint(registry, namespace, extension)}`);
|
|
111
112
|
}
|
|
@@ -125,11 +126,12 @@ async function exchangeWithRetry(
|
|
|
125
126
|
registry: Registry,
|
|
126
127
|
namespace: string,
|
|
127
128
|
extension: string,
|
|
128
|
-
idToken: string
|
|
129
|
+
idToken: string,
|
|
130
|
+
registrationCode?: string
|
|
129
131
|
): Promise<AccessToken> {
|
|
130
132
|
for (let attempt = 0; ; attempt++) {
|
|
131
133
|
try {
|
|
132
|
-
return await registry.requestTrustedPublishingToken(namespace, extension, idToken);
|
|
134
|
+
return await registry.requestTrustedPublishingToken(namespace, extension, idToken, registrationCode);
|
|
133
135
|
} catch (err) {
|
|
134
136
|
const status = (err as StatusError)?.status;
|
|
135
137
|
if (attempt >= RETRY_DELAYS_MS.length || status === undefined || !RETRYABLE_STATUS.includes(status)) {
|
|
@@ -142,6 +144,32 @@ async function exchangeWithRetry(
|
|
|
142
144
|
}
|
|
143
145
|
}
|
|
144
146
|
|
|
147
|
+
/**
|
|
148
|
+
* The ID token is a bearer credential for publishing, and the registry also chooses the audience it
|
|
149
|
+
* is minted for, so both only travel over https. A loopback registry is exempt, as for a browser's
|
|
150
|
+
* secure contexts: nothing leaves the machine.
|
|
151
|
+
*/
|
|
152
|
+
function requireSecureRegistry(registry: Registry): void {
|
|
153
|
+
const { protocol, hostname } = new URL(registry.url);
|
|
154
|
+
if (protocol === 'https:' || hostname === 'localhost' || hostname === '[::1]' || /^127\./.test(hostname)) {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
throw new Error(
|
|
158
|
+
`Trusted publishing sends an ID token to the registry, so the registry URL must use https, `
|
|
159
|
+
+ `but it is ${redactUrl(registry.url)}. Use an https URL, or publish with an access token.`
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* The audience a registry validates is its own configuration, defaulting to the URL of its web UI
|
|
165
|
+
* rather than the API URL this CLI talks to, so it is read from where the registry publishes it.
|
|
166
|
+
* A registry that reports nothing there is asked for the registry URL.
|
|
167
|
+
*/
|
|
168
|
+
async function expectedAudience(registry: Registry): Promise<string> {
|
|
169
|
+
const reported = await registry.getRegistryVersion().catch(() => undefined);
|
|
170
|
+
return reported?.trustedPublishingAudience ?? registry.url;
|
|
171
|
+
}
|
|
172
|
+
|
|
145
173
|
function registrationHint(registry: Registry, namespace: string, extension: string): string {
|
|
146
174
|
return `Check the trusted publishers registered for '${namespace}.${extension}' at `
|
|
147
175
|
+ `${registry.url}/user-settings/trusted-publishers.`;
|
package/src/unpublish.ts
CHANGED
|
@@ -12,12 +12,7 @@
|
|
|
12
12
|
*****************************************************************************/
|
|
13
13
|
|
|
14
14
|
import { confirm } from '@inquirer/prompts';
|
|
15
|
-
|
|
16
|
-
// exports whenever the importer applies ES module interop (e.g. a bundler, or vitest's own
|
|
17
|
-
// transform), making the truthiness check below always true. `import = require` always binds
|
|
18
|
-
// directly to the exported value, regardless of how the importing tool handles interop.
|
|
19
|
-
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
20
|
-
import isCI = require('is-ci');
|
|
15
|
+
import { isCI } from 'ci-info';
|
|
21
16
|
import * as semver from 'semver';
|
|
22
17
|
import { getPAT } from './pat';
|
|
23
18
|
import { Registry, TargetPlatformVersion } from './registry';
|
|
@@ -48,7 +43,8 @@ export async function unpublish(options: UnpublishOptions = {}): Promise<void> {
|
|
|
48
43
|
const registry = new Registry(options);
|
|
49
44
|
await ensureUnpublishSupported(registry);
|
|
50
45
|
|
|
51
|
-
|
|
46
|
+
const { pat, save } = await getPAT(namespace, { ...options, namespace }, false);
|
|
47
|
+
options.pat = pat;
|
|
52
48
|
|
|
53
49
|
const targetVersions = getTargetVersions(options);
|
|
54
50
|
await confirmUnpublish(`${namespace}.${extension}`, targetVersions, registry.url, options.force);
|
|
@@ -57,6 +53,7 @@ export async function unpublish(options: UnpublishOptions = {}): Promise<void> {
|
|
|
57
53
|
if (result.error) {
|
|
58
54
|
throw new Error(result.error);
|
|
59
55
|
}
|
|
56
|
+
await save();
|
|
60
57
|
// the registry reports one line per deleted version, and nothing at all if there was nothing to delete
|
|
61
58
|
console.log(`🗑 ${result.success || `Nothing to delete for ${namespace}.${extension}`}`);
|
|
62
59
|
}
|
package/src/util.ts
CHANGED
|
@@ -13,6 +13,7 @@ import * as fs from 'fs';
|
|
|
13
13
|
import * as os from 'os';
|
|
14
14
|
import * as path from 'path';
|
|
15
15
|
import * as http from 'http';
|
|
16
|
+
import { InvalidArgumentError } from 'commander';
|
|
16
17
|
import { RegistryOptions } from './registry-options';
|
|
17
18
|
import { TrustedPublishingOptions } from './trusted-publishing-options';
|
|
18
19
|
|
|
@@ -28,6 +29,7 @@ export function addTrustedPublishingEnvOptions(options: TrustedPublishingOptions
|
|
|
28
29
|
options.trustedPublishing ??= parseBooleanEnv(process.env.OVSX_TRUSTED_PUBLISHING);
|
|
29
30
|
options.idToken ??= process.env.OVSX_ID_TOKEN;
|
|
30
31
|
options.oidcAudience ??= process.env.OVSX_OIDC_AUDIENCE;
|
|
32
|
+
options.registrationCode ??= process.env.OVSX_REGISTRATION_CODE;
|
|
31
33
|
}
|
|
32
34
|
|
|
33
35
|
/** How long a request may make no progress before it is abandoned; see RegistryOptions.timeout. */
|
|
@@ -66,7 +68,8 @@ function parseBooleanEnv(value?: string): boolean | undefined {
|
|
|
66
68
|
export function parseNonNegativeInt(value: string): number {
|
|
67
69
|
const parsed = Number(value);
|
|
68
70
|
if (!Number.isInteger(parsed) || parsed < 0) {
|
|
69
|
-
|
|
71
|
+
// Commander prints only this error type as a usage message; anything else escapes as a stack trace.
|
|
72
|
+
throw new InvalidArgumentError(`Expected a non-negative whole number, got '${value}'.`);
|
|
70
73
|
}
|
|
71
74
|
return parsed;
|
|
72
75
|
}
|
|
@@ -137,9 +140,9 @@ export function handleError(debug?: boolean, additionalMessage?: string, exit: b
|
|
|
137
140
|
}
|
|
138
141
|
|
|
139
142
|
/**
|
|
140
|
-
* Formats a byte count for display, e.g. `1536` -> `1
|
|
141
|
-
* `FileUtils.byteCountToDisplaySize` formatting so client- and
|
|
142
|
-
* the same way.
|
|
143
|
+
* Formats a byte count for display, rounding down, e.g. `1536` -> `1 KB` and `1048577` -> `1 MB`.
|
|
144
|
+
* Mirrors the registry's own `FileUtils.byteCountToDisplaySize` formatting so client- and
|
|
145
|
+
* server-side size limit messages read the same way.
|
|
143
146
|
*/
|
|
144
147
|
export function formatBytes(bytes: number): string {
|
|
145
148
|
const units = ['bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB'];
|
package/src/verify-pat.ts
CHANGED
|
@@ -33,6 +33,8 @@ export async function verifyPat(options: VerifyPatOptions): Promise<void> {
|
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
const { pat, save } = await getPAT(options.namespace, options, false);
|
|
37
|
+
options.pat = pat;
|
|
37
38
|
await doVerifyPat(options);
|
|
39
|
+
await save();
|
|
38
40
|
}
|