ovsx 1.1.0 → 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.
Files changed (121) hide show
  1. package/README.md +71 -1
  2. package/lib/get.js +1 -1
  3. package/lib/get.js.map +1 -1
  4. package/lib/index.d.ts +4 -0
  5. package/lib/index.d.ts.map +1 -1
  6. package/lib/index.js +4 -0
  7. package/lib/index.js.map +1 -1
  8. package/lib/list-options.d.ts +24 -0
  9. package/lib/list-options.d.ts.map +1 -0
  10. package/lib/list-options.js +15 -0
  11. package/lib/list-options.js.map +1 -0
  12. package/lib/list.d.ts +18 -0
  13. package/lib/list.d.ts.map +1 -0
  14. package/lib/list.js +46 -0
  15. package/lib/list.js.map +1 -0
  16. package/lib/main.js +78 -2
  17. package/lib/main.js.map +1 -1
  18. package/lib/oidc.d.ts +22 -0
  19. package/lib/oidc.d.ts.map +1 -0
  20. package/lib/oidc.js +101 -0
  21. package/lib/oidc.js.map +1 -0
  22. package/lib/publish-options.d.ts +8 -1
  23. package/lib/publish-options.d.ts.map +1 -1
  24. package/lib/publish.d.ts.map +1 -1
  25. package/lib/publish.js +73 -4
  26. package/lib/publish.js.map +1 -1
  27. package/lib/registry-options.d.ts +8 -0
  28. package/lib/registry-options.d.ts.map +1 -1
  29. package/lib/registry.d.ts +119 -1
  30. package/lib/registry.d.ts.map +1 -1
  31. package/lib/registry.js +187 -19
  32. package/lib/registry.js.map +1 -1
  33. package/lib/search-options.d.ts +48 -0
  34. package/lib/search-options.d.ts.map +1 -0
  35. package/lib/search-options.js +15 -0
  36. package/lib/search-options.js.map +1 -0
  37. package/lib/search.d.ts +22 -0
  38. package/lib/search.d.ts.map +1 -0
  39. package/lib/search.js +86 -0
  40. package/lib/search.js.map +1 -0
  41. package/lib/show-options.d.ts +33 -0
  42. package/lib/show-options.d.ts.map +1 -0
  43. package/lib/show-options.js +15 -0
  44. package/lib/show-options.js.map +1 -0
  45. package/lib/show.d.ts +18 -0
  46. package/lib/show.d.ts.map +1 -0
  47. package/lib/show.js +289 -0
  48. package/lib/show.js.map +1 -0
  49. package/lib/table.d.ts +27 -0
  50. package/lib/table.d.ts.map +1 -0
  51. package/lib/table.js +56 -0
  52. package/lib/table.js.map +1 -0
  53. package/lib/trusted-publishing-options.d.ts +30 -0
  54. package/lib/trusted-publishing-options.d.ts.map +1 -0
  55. package/lib/trusted-publishing-options.js +15 -0
  56. package/lib/trusted-publishing-options.js.map +1 -0
  57. package/lib/trusted-publishing.d.ts +35 -0
  58. package/lib/trusted-publishing.d.ts.map +1 -0
  59. package/lib/trusted-publishing.js +112 -0
  60. package/lib/trusted-publishing.js.map +1 -0
  61. package/lib/unpublish-options.d.ts +34 -0
  62. package/lib/unpublish-options.d.ts.map +1 -0
  63. package/lib/unpublish-options.js +15 -0
  64. package/lib/unpublish-options.js.map +1 -0
  65. package/lib/unpublish.d.ts +22 -0
  66. package/lib/unpublish.d.ts.map +1 -0
  67. package/lib/unpublish.js +137 -0
  68. package/lib/unpublish.js.map +1 -0
  69. package/lib/util.d.ts +39 -1
  70. package/lib/util.d.ts.map +1 -1
  71. package/lib/util.js +91 -5
  72. package/lib/util.js.map +1 -1
  73. package/lib/verify-options.d.ts +24 -0
  74. package/lib/verify-options.d.ts.map +1 -0
  75. package/lib/verify-options.js +15 -0
  76. package/lib/verify-options.js.map +1 -0
  77. package/lib/verify-signature-options.d.ts +32 -0
  78. package/lib/verify-signature-options.d.ts.map +1 -0
  79. package/lib/verify-signature-options.js +15 -0
  80. package/lib/verify-signature-options.js.map +1 -0
  81. package/lib/verify-signature.d.ts +28 -0
  82. package/lib/verify-signature.d.ts.map +1 -0
  83. package/lib/verify-signature.js +105 -0
  84. package/lib/verify-signature.js.map +1 -0
  85. package/lib/verify.d.ts +21 -0
  86. package/lib/verify.d.ts.map +1 -0
  87. package/lib/verify.js +97 -0
  88. package/lib/verify.js.map +1 -0
  89. package/lib/version.d.ts +1 -1
  90. package/lib/version.js +1 -1
  91. package/lib/zip.d.ts +10 -0
  92. package/lib/zip.d.ts.map +1 -1
  93. package/lib/zip.js +31 -0
  94. package/lib/zip.js.map +1 -1
  95. package/package.json +9 -5
  96. package/src/get.ts +1 -1
  97. package/src/index.ts +4 -0
  98. package/src/list-options.ts +25 -0
  99. package/src/list.ts +48 -0
  100. package/src/main.ts +89 -3
  101. package/src/oidc.ts +107 -0
  102. package/src/publish-options.ts +8 -1
  103. package/src/publish.ts +97 -5
  104. package/src/registry-options.ts +8 -0
  105. package/src/registry.ts +292 -19
  106. package/src/search-options.ts +49 -0
  107. package/src/search.ts +95 -0
  108. package/src/show-options.ts +34 -0
  109. package/src/show.ts +328 -0
  110. package/src/table.ts +53 -0
  111. package/src/trusted-publishing-options.ts +30 -0
  112. package/src/trusted-publishing.ts +148 -0
  113. package/src/unpublish-options.ts +35 -0
  114. package/src/unpublish.ts +160 -0
  115. package/src/util.ts +102 -5
  116. package/src/verify-options.ts +25 -0
  117. package/src/verify-signature-options.ts +32 -0
  118. package/src/verify-signature.ts +124 -0
  119. package/src/verify.ts +107 -0
  120. package/src/version.ts +1 -1
  121. 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
+ }
@@ -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 namespace = (await readVSIXPackage(options.extensionFile!)).publisher;
52
- options.pat = await getPAT(namespace, options);
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.publish(options.extensionFile!, options.pat);
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
  }
@@ -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
  }