ovsx 1.2.0 → 1.3.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/src/main.ts CHANGED
@@ -9,12 +9,13 @@
9
9
  ********************************************************************************/
10
10
 
11
11
  import * as commander from 'commander';
12
- import * as leven from 'leven';
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
- import { LIB_VERSION } from './version';
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(program.debug));
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(program.debug));
53
+ .catch(handleError(debug));
50
54
  });
51
55
 
52
56
  const publishCmd = program.command('publish [extension.vsix]');
@@ -86,7 +90,7 @@ module.exports = function (argv: string[]): void {
86
90
  console.warn("Ignoring option '--packageVersion' for prepackaged extension.");
87
91
  if (extensionFile !== undefined && allowMissingRepository !== undefined)
88
92
  console.warn("Ignoring option '--allow-missing-repository' for prepackaged extension.");
89
- const { registryUrl, pat } = program.opts();
93
+ const { registryUrl, pat, debug } = program.opts();
90
94
  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 })
91
95
  .then(results => {
92
96
  const reasons = results.filter(result => result.status === 'rejected')
@@ -95,7 +99,7 @@ module.exports = function (argv: string[]): void {
95
99
  if (reasons.length > 0) {
96
100
  const message = 'See the documentation for more information:\n'
97
101
  + 'https://github.com/eclipse/openvsx/wiki/Publishing-Extensions';
98
- const errorHandler = handleError(program.debug, message, false);
102
+ const errorHandler = handleError(debug, message, false);
99
103
  for (const reason of reasons) {
100
104
  errorHandler(reason);
101
105
  }
@@ -111,7 +115,7 @@ module.exports = function (argv: string[]): void {
111
115
  .option('-t, --target <targets...>', 'Only delete the given target architectures of the given versions.')
112
116
  .option('-f, --force', 'Skip the confirmation prompt.')
113
117
  .action((extensionId: string | undefined, { versions, target, force }) => {
114
- const { registryUrl, pat } = program.opts();
118
+ const { registryUrl, pat, debug } = program.opts();
115
119
  unpublish({
116
120
  extensionId,
117
121
  versions: typeof versions === 'string' ? [versions] : versions,
@@ -119,7 +123,7 @@ module.exports = function (argv: string[]): void {
119
123
  force,
120
124
  registryUrl,
121
125
  pat
122
- }).catch(handleError(program.debug));
126
+ }).catch(handleError(debug));
123
127
  });
124
128
 
125
129
  const searchCmd = program.command('search [text]');
@@ -136,17 +140,17 @@ module.exports = function (argv: string[]): void {
136
140
  .option('--sort-order <order>', `Sort order: ${SORT_ORDERS.join(', ')}.`)
137
141
  .option('--json', 'Print the raw results as JSON.')
138
142
  .action((text: string | undefined, { category, target, size, offset, sortBy, sortOrder, json }) => {
139
- const { registryUrl } = program.opts();
143
+ const { registryUrl, debug } = program.opts();
140
144
  search({ text, category, target, size, offset, sortBy, sortOrder, json, registryUrl })
141
- .catch(handleError(program.debug));
145
+ .catch(handleError(debug));
142
146
  });
143
147
 
144
148
  const listCmd = program.command('list <namespace>');
145
149
  listCmd.description('List the extensions published in a namespace.')
146
150
  .option('--json', 'Print the raw namespace metadata as JSON.')
147
151
  .action((namespace: string, { json }) => {
148
- const { registryUrl } = program.opts();
149
- list({ namespace, json, registryUrl }).catch(handleError(program.debug));
152
+ const { registryUrl, debug } = program.opts();
153
+ list({ namespace, json, registryUrl }).catch(handleError(debug));
150
154
  });
151
155
 
152
156
  const showCmd = program.command('show <namespace.extension[@version]>');
@@ -155,8 +159,8 @@ module.exports = function (argv: string[]): void {
155
159
  .option('--all-versions', 'List every published version instead of the most recent few.')
156
160
  .option('--json', 'Print the raw metadata as JSON.')
157
161
  .action((extensionId: string, { target, allVersions, json }) => {
158
- const { registryUrl } = program.opts();
159
- show({ extensionId, target, allVersions, json, registryUrl }).catch(handleError(program.debug));
162
+ const { registryUrl, debug } = program.opts();
163
+ show({ extensionId, target, allVersions, json, registryUrl }).catch(handleError(debug));
160
164
  });
161
165
 
162
166
  const getCmd = program.command('get <namespace.extension>');
@@ -166,18 +170,18 @@ module.exports = function (argv: string[]): void {
166
170
  .option('-o, --output <path>', 'Save the output in the specified file or directory.')
167
171
  .option('--metadata', 'Print the extension\'s metadata instead of downloading it.')
168
172
  .action((extensionId: string, { target, versionRange, output, metadata }) => {
169
- const { registryUrl } = program.opts();
173
+ const { registryUrl, debug } = program.opts();
170
174
  getExtension({ extensionId, target: target, version: versionRange, registryUrl, output, metadata })
171
- .catch(handleError(program.debug));
175
+ .catch(handleError(debug));
172
176
  });
173
177
 
174
178
  const verifyCmd = program.command('verify <extension.vsix>');
175
179
  verifyCmd.description('Verify a downloaded package\'s signature against the registry\'s public key.')
176
180
  .option('-t, --target <target>', 'Target architecture')
177
181
  .action((packagePath: string, { target }) => {
178
- const { registryUrl } = program.opts();
182
+ const { registryUrl, debug } = program.opts();
179
183
  verify({ packagePath, target, registryUrl })
180
- .catch(handleError(program.debug));
184
+ .catch(handleError(debug));
181
185
  });
182
186
 
183
187
  const verifySignatureCmd = program.command('verify-signature');
@@ -187,43 +191,44 @@ module.exports = function (argv: string[]): void {
187
191
  .requiredOption('-s, --signaturePath <path>', 'Path to the signature file.')
188
192
  .requiredOption('-k, --publicKeyPath <path>', 'Path to the registry\'s public key file.')
189
193
  .action(({ packagePath, manifestPath, signaturePath, publicKeyPath }) => {
194
+ const { debug } = program.opts();
190
195
  verifySignature({ packagePath, manifestPath, signaturePath, publicKeyPath })
191
- .catch(handleError(program.debug));
196
+ .catch(handleError(debug));
192
197
  });
193
198
 
194
199
  const loginCmd = program.command('login <namespace>');
195
200
  loginCmd.description('Adds a namespace to the list of known namespaces')
196
201
  .action((namespace: string) => {
197
- const { registryUrl, pat } = program.opts();
198
- login({ namespace, registryUrl, pat }).catch(handleError(program.debug));
202
+ const { registryUrl, pat, debug } = program.opts();
203
+ login({ namespace, registryUrl, pat }).catch(handleError(debug));
199
204
  });
200
205
 
201
206
  const logoutCmd = program.command('logout <namespace>');
202
207
  logoutCmd.description('Removes a namespace from the list of known namespaces')
203
208
  .action((namespace: string) => {
204
- logout(namespace).catch(handleError(program.debug));
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();
209
+ const { debug } = program.opts();
210
+ logout(namespace).catch(handleError(debug));
222
211
  });
223
212
 
224
- program.parse(argv);
213
+ // 'command:*' is deprecated in favor of catching the commander.unknownCommand error, but that
214
+ // error is reported (message written, process exited) by commander itself before a catch runs,
215
+ // so it can't reproduce this suggestion wording. Still supported for backwards compatibility.
216
+ program.on('command:*', (operands: string[]) => {
217
+ const availableCommands = program.commands.map(c => c.name());
218
+ const actualCommand = operands[0];
219
+ const suggestion = availableCommands.find(c => levenshtein(c, actualCommand) < c.length * 0.4);
220
+ if (suggestion)
221
+ console.error(`Unknown command '${actualCommand}', did you mean '${suggestion}'?\n`);
222
+ else
223
+ console.error(`Unknown command '${actualCommand}'.\n`);
224
+ process.exitCode = 1;
225
+ program.help();
226
+ });
225
227
 
226
228
  if (process.argv.length <= 2) {
229
+ // Commander 14 would otherwise print this to stderr and exit 1 during program.parse(argv) below.
227
230
  program.help();
228
231
  }
232
+
233
+ program.parse(argv);
229
234
  };
package/src/oidc.ts CHANGED
@@ -11,10 +11,9 @@
11
11
  * SPDX-License-Identifier: EPL-2.0
12
12
  *****************************************************************************/
13
13
 
14
- import * as http from 'http';
15
- import * as followRedirects from 'follow-redirects';
14
+ import { getJson } from './http';
16
15
  import { TrustedPublishingOptions } from './trusted-publishing-options';
17
- import { configuredTimeout, redactUrl, statusError } from './util';
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/publish.ts CHANGED
@@ -8,15 +8,26 @@
8
8
  * SPDX-License-Identifier: EPL-2.0
9
9
  ********************************************************************************/
10
10
  import * as fs from 'fs';
11
+ import * as semver from 'semver';
11
12
  import { createVSIX, IPackageOptions } from '@vscode/vsce';
12
13
  import { getPAT } from './pat';
13
- import { createTempFile, addEnvOptions, addTrustedPublishingEnvOptions, formatBytes, StatusError } from './util';
14
+ import {
15
+ getTempFilePath,
16
+ addEnvOptions,
17
+ addTrustedPublishingEnvOptions,
18
+ formatBytes,
19
+ Manifest,
20
+ StatusError
21
+ } from './util';
14
22
  import { Extension, Registry } from './registry';
15
23
  import { checkLicense } from './check-license';
16
24
  import { readVSIXPackage } from './zip';
17
25
  import { PublishOptions, PublishCommonOptions } from './publish-options';
18
26
  import { getTrustedPublishingToken, refreshTrustedPublishingToken, useTrustedPublishing } from './trusted-publishing';
19
27
 
28
+ /** Registries older than this cannot report the limit that applies to a namespace. */
29
+ const MIN_SIZE_LIMIT_REGISTRY_VERSION = '1.3.0';
30
+
20
31
  /**
21
32
  * Publishes an extension.
22
33
  */
@@ -24,8 +35,11 @@ export async function publish(options: PublishOptions = {}): Promise<PromiseSett
24
35
  addEnvOptions(options);
25
36
  addTrustedPublishingEnvOptions(options);
26
37
 
27
- // Looked up once and shared by every target/package below, rather than once per artifact.
28
- const maxExtensionSize = await getMaxExtensionSize(new Registry(options));
38
+ // Shared by every target/package below, rather than one per artifact: besides saving the extra
39
+ // `/api/version` round trips, it's also where the registry's reported version is cached (see
40
+ // Registry.tokenQuery), so a wide fan-out doesn't repeat that lookup once per target either.
41
+ const registry = new Registry(options);
42
+ const maxExtensionSize = await getMaxExtensionSize(registry);
29
43
 
30
44
  const internalPublishOptions: InternalPublishOptions[] = [];
31
45
  const packagePaths = options.packagePath || [undefined];
@@ -36,15 +50,15 @@ export async function publish(options: PublishOptions = {}): Promise<PromiseSett
36
50
  }
37
51
  }
38
52
 
39
- return Promise.allSettled(internalPublishOptions.map(publishOptions => doPublish(publishOptions)));
53
+ return Promise.allSettled(internalPublishOptions.map(publishOptions => doPublish(registry, publishOptions)));
40
54
  }
41
55
 
42
56
  /**
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.
57
+ * Looks up the registry's default extension size limit, so an oversized package can be flagged
58
+ * before the upload instead of after transferring the whole payload.
45
59
  *
46
60
  * 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.
61
+ * `undefined` here, and nothing is said about the size.
48
62
  */
49
63
  async function getMaxExtensionSize(registry: Registry): Promise<number | undefined> {
50
64
  try {
@@ -54,28 +68,43 @@ async function getMaxExtensionSize(registry: Registry): Promise<number | undefin
54
68
  }
55
69
  }
56
70
 
57
- async function doPublish(options: InternalPublishOptions = {}): Promise<void> {
71
+ async function doPublish(registry: Registry, options: InternalPublishOptions = {}): Promise<void> {
58
72
  // if the packagePath is a link to a vsix, don't need to package it
59
73
  if (options.packagePath?.endsWith('.vsix')) {
60
74
  options.extensionFile = options.packagePath;
61
75
  delete options.packagePath;
62
76
  delete options.target;
63
77
  }
64
- const registry = new Registry(options);
65
- if (!options.extensionFile) {
66
- await packageExtension(options, registry);
67
- console.log(); // new line
68
- } else if (options.preRelease) {
69
- console.warn("Ignoring option '--pre-release' for prepackaged extension.");
78
+ // Only a package created here is a temp file; one the user supplied must never be deleted.
79
+ const packaged = !options.extensionFile;
80
+ try {
81
+ if (packaged) {
82
+ await packageExtension(options, registry);
83
+ console.log(); // new line
84
+ } else if (options.preRelease) {
85
+ console.warn("Ignoring option '--pre-release' for prepackaged extension.");
86
+ }
87
+ await publishExtensionFile(registry, options);
88
+ } finally {
89
+ if (packaged && options.extensionFile) {
90
+ // Caught rather than awaited plainly: a failure here (e.g. the temp dir is locked or
91
+ // read-only) must not replace whatever error publishExtensionFile already threw above.
92
+ await fs.promises.rm(options.extensionFile, { force: true }).catch(err => {
93
+ console.warn(`Could not delete temporary file ${options.extensionFile}: ${err.message}`);
94
+ });
95
+ }
70
96
  }
97
+ }
71
98
 
72
- await ensureWithinSizeLimit(options.extensionFile!, options.maxExtensionSize, registry.url);
99
+ async function publishExtensionFile(registry: Registry, options: InternalPublishOptions): Promise<void> {
100
+ // Read up front rather than only when a token has to be obtained: the size limit is looked up per
101
+ // namespace, and the namespace lives in the manifest.
102
+ const manifest = await readVSIXPackage(options.extensionFile!);
73
103
 
74
104
  // Set only when this publish obtained the token itself through trusted publishing, which is the one
75
105
  // case where a refusal can be answered by asking for a new token.
76
106
  let exchanged: { namespace: string; extension: string } | undefined;
77
107
  if (!options.pat) {
78
- const manifest = await readVSIXPackage(options.extensionFile!);
79
108
  if (useTrustedPublishing(options)) {
80
109
  exchanged = { namespace: manifest.publisher, extension: manifest.name };
81
110
  options.pat = await getTrustedPublishingToken(registry, manifest.publisher, manifest.name, options);
@@ -84,9 +113,13 @@ async function doPublish(options: InternalPublishOptions = {}): Promise<void> {
84
113
  }
85
114
  }
86
115
 
116
+ // After the token is resolved, because the lookup is authenticated - and by here a token always
117
+ // exists, whether supplied, fetched, or exchanged through trusted publishing.
118
+ const sizeLimit = await ensureWithinSizeLimit(registry, options, manifest);
119
+
87
120
  let extension: Extension | undefined;
88
121
  try {
89
- extension = await doRegistryPublish(registry, options, exchanged);
122
+ extension = await doRegistryPublish(registry, options, exchanged, sizeLimit);
90
123
  } catch (err) {
91
124
  if (options.skipDuplicate && err.message.endsWith('is already published.')) {
92
125
  console.log(err.message + ' Skipping publish.');
@@ -123,10 +156,11 @@ async function doPublish(options: InternalPublishOptions = {}): Promise<void> {
123
156
  async function doRegistryPublish(
124
157
  registry: Registry,
125
158
  options: InternalPublishOptions,
126
- exchanged: { namespace: string; extension: string } | undefined
159
+ exchanged: { namespace: string; extension: string } | undefined,
160
+ sizeLimit?: number
127
161
  ): Promise<Extension> {
128
162
  try {
129
- return await registry.publish(options.extensionFile!, options.pat!);
163
+ return await registry.publish(options.extensionFile!, options.pat!, sizeLimit);
130
164
  } catch (err) {
131
165
  if (!exchanged || (err as StatusError)?.status !== 401) {
132
166
  throw err;
@@ -140,28 +174,95 @@ async function doRegistryPublish(
140
174
  options,
141
175
  options.pat!
142
176
  );
143
- return registry.publish(options.extensionFile!, options.pat);
177
+ return registry.publish(options.extensionFile!, options.pat, sizeLimit);
144
178
  }
145
179
  }
146
180
 
147
181
  /**
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.
182
+ * Refuses a package the registry would reject anyway, before uploading it, and reports how many
183
+ * bytes it has concluded may be sent - the caller raises the transport cap to that, so nothing we
184
+ * decided to upload is then refused locally.
185
+ *
186
+ * The limit is the one that applies to this package's namespace and extension, which a size override
187
+ * can raise above the registry default. Registries older than
188
+ * {@link MIN_SIZE_LIMIT_REGISTRY_VERSION} cannot report it, so there the check stays advisory: it
189
+ * warns against the default and publishes, rather than refusing an upload those registries would
190
+ * have accepted.
152
191
  */
153
- async function ensureWithinSizeLimit(extensionFile: string, maxSize: number | undefined, registryUrl: string): Promise<void> {
154
- if (!maxSize) {
155
- return;
192
+ async function ensureWithinSizeLimit(
193
+ registry: Registry,
194
+ options: InternalPublishOptions,
195
+ manifest: Manifest
196
+ ): Promise<number | undefined> {
197
+ const { size } = await fs.promises.stat(options.extensionFile!);
198
+ const limit = await resolveSizeLimit(registry, options, manifest);
199
+
200
+ if (limit.kind === 'failed') {
201
+ // Always said, however small the package: a size override can lower a limit as well as raise
202
+ // it, so there is no size from which it follows that this upload is fine. Staying quiet here
203
+ // left a registry whose /api/-/* routes never arrive indistinguishable from a working one.
204
+ console.warn(
205
+ `Could not check the size limit for ${manifest.publisher}.${manifest.name} at ${registry.url} `
206
+ + `(${limit.reason}). Publishing anyway: the registry enforces its own limit, so an oversized `
207
+ + `package is refused after the upload rather than before it.`
208
+ );
209
+ } else if (limit.kind === 'unsupported') {
210
+ const fallback = options.maxExtensionSize;
211
+ if (fallback && size > fallback) {
212
+ console.warn(
213
+ `The extension package (${formatBytes(size)}) exceeds the default size limit of ${formatBytes(fallback)} `
214
+ + `reported by the registry at ${registry.url}, which is too old to report the limit for a `
215
+ + `single namespace. Publishing anyway: the namespace may have a different limit configured, `
216
+ + `and the registry decides.`
217
+ );
218
+ }
156
219
  }
157
220
 
158
- const { size } = await fs.promises.stat(extensionFile);
159
- if (size > maxSize) {
221
+ if (limit.kind !== 'resolved') {
222
+ // The allowance is this package, not the built-in publish size: having decided to send it and
223
+ // let the registry answer, the transport cap must not be what refuses it instead. An older
224
+ // registry whose default is above 512 MiB would otherwise reject a package it would accept.
225
+ return size;
226
+ }
227
+
228
+ if (size > limit.limit) {
160
229
  throw new Error(
161
- `The extension package (${formatBytes(size)}) exceeds the size limit of ${formatBytes(maxSize)} `
162
- + `accepted by the registry at ${registryUrl}.`
230
+ `The extension package (${formatBytes(size)}) exceeds the size limit of ${formatBytes(limit.limit)} `
231
+ + `for ${manifest.publisher}.${manifest.name} at ${registry.url}.`
163
232
  );
164
233
  }
234
+ return limit.limit;
235
+ }
236
+
237
+ /**
238
+ * The limit for this namespace/extension. A registry too old to report one and a registry that could
239
+ * not answer are told apart: both publish anyway, but only the second one means a check the user is
240
+ * entitled to expect did not happen.
241
+ */
242
+ type SizeLimitLookup =
243
+ | { kind: 'resolved'; limit: number }
244
+ | { kind: 'unsupported' }
245
+ | { kind: 'failed'; reason: string };
246
+
247
+ async function resolveSizeLimit(
248
+ registry: Registry,
249
+ options: InternalPublishOptions,
250
+ manifest: Manifest
251
+ ): Promise<SizeLimitLookup> {
252
+ const reported = await registry.getRegistryVersion().catch(() => undefined);
253
+ const version = reported?.version ? semver.coerce(reported.version) : undefined;
254
+ if (!version || semver.lt(version, MIN_SIZE_LIMIT_REGISTRY_VERSION)) {
255
+ return { kind: 'unsupported' };
256
+ }
257
+
258
+ try {
259
+ const { maxSize } = await registry.getSizeLimit(manifest.publisher, manifest.name, options.pat!);
260
+ return { kind: 'resolved', limit: maxSize };
261
+ } catch (err) {
262
+ // The registry claims to be new enough but could not answer. It enforces the limit itself
263
+ // regardless, so publish rather than refusing on a failed lookup.
264
+ return { kind: 'failed', reason: err instanceof Error ? err.message : String(err) };
265
+ }
165
266
  }
166
267
 
167
268
  async function packageExtension(options: InternalPublishOptions, registry: Registry): Promise<void> {
@@ -169,7 +270,7 @@ async function packageExtension(options: InternalPublishOptions, registry: Regis
169
270
  await checkLicense(options.packagePath!);
170
271
  }
171
272
 
172
- options.extensionFile = await createTempFile({ postfix: '.vsix' });
273
+ options.extensionFile = getTempFilePath('.vsix');
173
274
  const packageOptions: IPackageOptions = {
174
275
  packagePath: options.extensionFile,
175
276
  target: options.target,
@@ -208,8 +309,9 @@ interface InternalPublishOptions extends PublishCommonOptions {
208
309
  dependencies?: boolean;
209
310
 
210
311
  /**
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.
312
+ * The registry's default extension size limit in bytes, looked up once via `/api/version` and
313
+ * shared across every target/package being published. Advisory: a namespace override can allow
314
+ * more. `undefined` when it couldn't be determined.
213
315
  */
214
316
  maxExtensionSize?: number;
215
317
  }