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.
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/show.ts ADDED
@@ -0,0 +1,328 @@
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 semver from 'semver';
15
+ import { Extension, Registry, VersionReference } from './registry';
16
+ import { formatCount, formatNumber, printRows } from './table';
17
+ import { ShowOptions } from './show-options';
18
+ import { addEnvOptions, matchExtensionId } from './util';
19
+
20
+ /**
21
+ * How many versions the history table prints unless `--all-versions` is given. Matches what
22
+ * `vsce show` lists; the remainder is reported as a count rather than dropped silently.
23
+ */
24
+ const VERSION_HISTORY_SIZE = 6;
25
+
26
+ /**
27
+ * Tags starting with `__` are internal bookkeeping (e.g. `__web_extension`) rather than anything
28
+ * the publisher wrote, and the web UI hides them too.
29
+ */
30
+ const INTERNAL_TAG_PREFIX = '__';
31
+
32
+ /** One row of the version history table. */
33
+ interface VersionSummary {
34
+ version: string;
35
+ targetPlatforms: string[];
36
+ }
37
+
38
+ /**
39
+ * Page size for the version-reference listing. Versions come back newest first, and each version
40
+ * contributes one entry per target platform, so a page has to be comfortably larger than the
41
+ * number of versions shown for the default table to be filled from a single request.
42
+ */
43
+ const VERSION_PAGE_SIZE = 100;
44
+
45
+ /**
46
+ * Prints an extension's metadata.
47
+ */
48
+ export async function show(options: ShowOptions): Promise<void> {
49
+ addEnvOptions(options);
50
+ const { id, version } = splitVersion(options.extensionId);
51
+ const match = matchExtensionId(id);
52
+ if (!match) {
53
+ throw new Error('The extension identifier must have the form `namespace.extension` or `namespace/extension`.');
54
+ }
55
+
56
+ const [, namespace, name] = match;
57
+ const registry = new Registry(options);
58
+ const extension = await registry.getMetadata(namespace, name, options.target, version);
59
+ if (extension.error) {
60
+ throw new Error(extension.error);
61
+ }
62
+
63
+ if (options.json) {
64
+ console.log(JSON.stringify(extension, null, 4));
65
+ return;
66
+ }
67
+
68
+ const allVersions = options.allVersions === true;
69
+ const versions = await getVersions(registry, namespace, name, options.target, allVersions);
70
+ printSummary(extension, versions, allVersions);
71
+ }
72
+
73
+ /**
74
+ * Splits a trailing `@<version>` off the identifier, the way `vsce show` accepts it. Only the last
75
+ * `@` is considered, so it stays out of the way of the identifier itself.
76
+ *
77
+ * An `@` with nothing after it is rejected rather than read as "no version given". It usually means a
78
+ * shell variable that did not expand - `ovsx show redhat.java@$VERSION` with `VERSION` unset - and
79
+ * reporting the latest version for that would answer a question the caller did not ask.
80
+ */
81
+ function splitVersion(extensionId: string): { id: string; version?: string } {
82
+ const at = extensionId.lastIndexOf('@');
83
+ if (at <= 0) {
84
+ return { id: extensionId };
85
+ }
86
+ const version = extensionId.substring(at + 1);
87
+ if (version.length === 0) {
88
+ throw new Error('A version must follow `@`, as in `namespace.extension@1.2.3`.');
89
+ }
90
+ return { id: extensionId.substring(0, at), version };
91
+ }
92
+
93
+ /**
94
+ * Collects the version history, collapsing the one-entry-per-target-platform rows the registry
95
+ * returns into a single row per version. Fetches one page unless every version is wanted, in which
96
+ * case it pages to the end - `totalSize` counts version/target-platform pairs, not versions, so
97
+ * paging has to run on what has actually been returned rather than on a version count.
98
+ *
99
+ * Best-effort: a registry that doesn't serve this endpoint still gets everything else, just
100
+ * without the history table.
101
+ */
102
+ async function getVersions(
103
+ registry: Registry,
104
+ namespace: string,
105
+ name: string,
106
+ target: string | undefined,
107
+ allVersions: boolean
108
+ ): Promise<VersionSummary[]> {
109
+ const references: VersionReference[] = [];
110
+ let offset = 0;
111
+ try {
112
+ for (;;) {
113
+ const page = await registry.getVersionReferences(namespace, name, target, VERSION_PAGE_SIZE, offset);
114
+ if (page.error) {
115
+ return [];
116
+ }
117
+ const versions = page.versions ?? [];
118
+ references.push(...versions);
119
+ offset += versions.length;
120
+ if (!allVersions || versions.length === 0 || offset >= page.totalSize) {
121
+ break;
122
+ }
123
+ }
124
+ } catch {
125
+ return [];
126
+ }
127
+
128
+ const byVersion = new Map<string, VersionSummary>();
129
+ for (const reference of references) {
130
+ let summary = byVersion.get(reference.version);
131
+ if (!summary) {
132
+ summary = { version: reference.version, targetPlatforms: [] };
133
+ byVersion.set(reference.version, summary);
134
+ }
135
+ if (reference.targetPlatform && !summary.targetPlatforms.includes(reference.targetPlatform)) {
136
+ summary.targetPlatforms.push(reference.targetPlatform);
137
+ }
138
+ }
139
+
140
+ return [...byVersion.values()].sort(byNewestFirst);
141
+ }
142
+
143
+ /**
144
+ * Newest version first. Anything that isn't valid semver (the registry accepts such versions from
145
+ * a mirror) sorts after everything that is, rather than being compared incomparably.
146
+ *
147
+ * Deliberately `valid` and not `coerce`: coerce drops prerelease identifiers, so `1.2.0-alpha.1`
148
+ * and `1.2.0` would compare equal and order unstably, and it accepts inputs semver itself rejects
149
+ * (`v1.2` becomes `1.2.0`), which would defeat the fallback below.
150
+ */
151
+ function byNewestFirst(a: VersionSummary, b: VersionSummary): number {
152
+ const left = semver.valid(a.version);
153
+ const right = semver.valid(b.version);
154
+ if (left && right) {
155
+ return semver.rcompare(left, right);
156
+ }
157
+ if (left) {
158
+ return -1;
159
+ }
160
+ if (right) {
161
+ return 1;
162
+ }
163
+ return b.version.localeCompare(a.version);
164
+ }
165
+
166
+ function printSummary(extension: Extension, versions: VersionSummary[], allVersions: boolean): void {
167
+ const publisher = extension.namespaceDisplayName || extension.namespace;
168
+ console.log(extension.displayName || extension.name);
169
+ console.log(` ${publisher}${extension.verified ? ' (verified publisher)' : ''}`);
170
+ console.log(` ${formatCount(extension.downloadCount, 'download')} ${formatRating(extension)}`);
171
+ if (extension.description) {
172
+ console.log();
173
+ console.log(` ${extension.description}`);
174
+ }
175
+
176
+ printNotices(extension);
177
+ printVersionHistory(versions, allVersions);
178
+ printList('Categories', extension.categories);
179
+ printList('Tags', extension.tags?.filter(tag => !tag.startsWith(INTERNAL_TAG_PREFIX)));
180
+ printTable('More Info', moreInfo(extension));
181
+ printTable('Registry', registryInfo(extension));
182
+ printTable('Statistics', statistics(extension));
183
+ }
184
+
185
+ /**
186
+ * Anything a consumer of this extension should see before its metadata: deprecation first, since
187
+ * it changes whether they should install it at all.
188
+ */
189
+ function printNotices(extension: Extension): void {
190
+ const notices: string[] = [];
191
+ if (extension.deprecated) {
192
+ const replacement = extension.replacement;
193
+ notices.push(
194
+ replacement
195
+ ? `Deprecated - superseded by ${replacement.displayName ?? replacement.url}`
196
+ : 'Deprecated'
197
+ );
198
+ }
199
+ if (extension.downloadable === false) {
200
+ notices.push('Not downloadable from this registry');
201
+ }
202
+ if (extension.preview) {
203
+ notices.push('Preview');
204
+ }
205
+ if (extension.namespaceOwnershipConflict) {
206
+ notices.push('The namespace ownership of this extension is disputed');
207
+ }
208
+
209
+ if (notices.length > 0) {
210
+ console.log();
211
+ for (const notice of notices) {
212
+ console.log(` ! ${notice}`);
213
+ }
214
+ }
215
+ }
216
+
217
+ function printVersionHistory(versions: VersionSummary[], allVersions: boolean): void {
218
+ if (versions.length === 0) {
219
+ return;
220
+ }
221
+
222
+ const shown = allVersions ? versions : versions.slice(0, VERSION_HISTORY_SIZE);
223
+ const rows = shown.map(v => [v.version, v.targetPlatforms.join(', ')]);
224
+
225
+ console.log();
226
+ console.log('Version History:');
227
+ printRows([['Version', 'Target Platforms'], ...rows]);
228
+ const remaining = versions.length - shown.length;
229
+ if (remaining > 0) {
230
+ console.log(` ... and ${remaining} more (pass --all-versions to list them)`);
231
+ }
232
+ }
233
+
234
+ function moreInfo(extension: Extension): string[][] {
235
+ const rows: string[][] = [
236
+ ['Unique Identifier', `${extension.namespace}.${extension.name}`],
237
+ ['Version', extension.version]
238
+ ];
239
+ if (extension.versionAlias?.length > 0) {
240
+ rows.push(['Version Aliases', extension.versionAlias.join(', ')]);
241
+ }
242
+ if (extension.targetPlatform) {
243
+ rows.push(['Target Platform', extension.targetPlatform]);
244
+ }
245
+ rows.push(['Last Updated', extension.timestamp]);
246
+ rows.push(['Published By', extension.publishedBy?.loginName ?? '']);
247
+ addIfPresent(rows, 'License', extension.license);
248
+ addIfPresent(rows, 'Homepage', extension.homepage);
249
+ addIfPresent(rows, 'Repository', extension.repository);
250
+ addIfPresent(rows, 'Bugs', extension.bugs);
251
+ addIfPresent(rows, 'Q&A', extension.qna);
252
+ addIfPresent(rows, 'Sponsor', extension.sponsorLink);
253
+ if (extension.engines) {
254
+ rows.push(['Engines', Object.entries(extension.engines).map(([e, v]) => `${e} ${v}`).join(', ')]);
255
+ }
256
+ return rows;
257
+ }
258
+
259
+ /**
260
+ * The parts an Open VSX compatible registry reports that the Marketplace has no equivalent for.
261
+ * Kept in its own block rather than mixed into "More Info" so it's obvious what is registry
262
+ * specific, and omitted entirely when none of it applies.
263
+ */
264
+ function registryInfo(extension: Extension): string[][] {
265
+ const rows: string[][] = [];
266
+ if (extension.verified !== undefined) {
267
+ rows.push(['Verified Publisher', yesNo(extension.verified)]);
268
+ }
269
+ if (extension.publishedWithTrustedPublishing) {
270
+ rows.push(['Trusted Publishing', 'yes']);
271
+ }
272
+ if (extension.preRelease !== undefined) {
273
+ rows.push(['Pre-Release', yesNo(extension.preRelease)]);
274
+ }
275
+ addIfPresent(rows, 'Extension Kind', extension.extensionKind?.join(', '));
276
+ addIfPresent(rows, 'Localized', extension.localizedLanguages?.join(', '));
277
+ addIfPresent(rows, 'Dependencies', extension.dependencies?.map(referenceId).join(', '));
278
+ addIfPresent(rows, 'Bundled Extensions', extension.bundledExtensions?.map(referenceId).join(', '));
279
+ return rows;
280
+ }
281
+
282
+ function statistics(extension: Extension): string[][] {
283
+ const rows: string[][] = [['Downloads', formatNumber(extension.downloadCount)]];
284
+ if (extension.averageRating !== undefined) {
285
+ rows.push(['Average Rating', `${extension.averageRating.toFixed(1)}/5`]);
286
+ }
287
+ rows.push(['Reviews', formatNumber(Number(extension.reviewCount ?? 0))]);
288
+ return rows;
289
+ }
290
+
291
+ function referenceId(reference: { namespace: string; extension: string }): string {
292
+ return `${reference.namespace}.${reference.extension}`;
293
+ }
294
+
295
+ function addIfPresent(rows: string[][], label: string, value?: string): void {
296
+ if (value) {
297
+ rows.push([label, value]);
298
+ }
299
+ }
300
+
301
+ function printList(title: string, values?: string[]): void {
302
+ if (!values || values.length === 0) {
303
+ return;
304
+ }
305
+ console.log();
306
+ console.log(`${title}:`);
307
+ console.log(` ${values.join(', ')}`);
308
+ }
309
+
310
+ function printTable(title: string, rows: string[][]): void {
311
+ if (rows.length === 0) {
312
+ return;
313
+ }
314
+ console.log();
315
+ console.log(`${title}:`);
316
+ printRows(rows);
317
+ }
318
+
319
+ function formatRating(extension: Extension): string {
320
+ if (extension.averageRating === undefined) {
321
+ return 'no ratings';
322
+ }
323
+ return `${extension.averageRating.toFixed(1)}/5 from ${formatCount(extension.reviewCount, 'review')}`;
324
+ }
325
+
326
+ function yesNo(value: boolean): string {
327
+ return value ? 'yes' : 'no';
328
+ }
package/src/table.ts ADDED
@@ -0,0 +1,53 @@
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
+ /**
15
+ * Prints rows as an aligned table, indented by two spaces. Every column but the last is padded,
16
+ * so a trailing empty cell doesn't leave ragged whitespace behind.
17
+ */
18
+ export function printRows(rows: string[][]): void {
19
+ const widths: number[] = [];
20
+ for (const row of rows) {
21
+ row.forEach((cell, i) => widths[i] = Math.max(widths[i] ?? 0, cell.length));
22
+ }
23
+ for (const row of rows) {
24
+ const line = row
25
+ .map((cell, i) => (i === row.length - 1 ? cell : cell.padEnd(widths[i])))
26
+ .join(' ')
27
+ .trimEnd();
28
+ console.log(` ${line}`);
29
+ }
30
+ }
31
+
32
+ /** Formats a count with thousands separators, and the noun pluralised to match. */
33
+ export function formatCount(count: number | undefined, noun: string): string {
34
+ const value = count ?? 0;
35
+ return `${formatNumber(value)} ${value === 1 ? noun : noun + 's'}`;
36
+ }
37
+
38
+ export function formatNumber(value: number | undefined): string {
39
+ return (value ?? 0).toLocaleString('en-US');
40
+ }
41
+
42
+ /**
43
+ * Shortens `text` to at most `max` characters, marking that it was cut. A `max` of zero or less
44
+ * leaves no room for the marker either, so it yields nothing rather than an ellipsis wider than the
45
+ * budget it was given.
46
+ */
47
+ export function truncate(text: string, max: number): string {
48
+ if (max <= 0) {
49
+ return '';
50
+ }
51
+ const collapsed = text.replace(/\s+/g, ' ').trim();
52
+ return collapsed.length <= max ? collapsed : `${collapsed.substring(0, max - 1)}…`;
53
+ }
@@ -0,0 +1,30 @@
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
+ export interface TrustedPublishingOptions {
15
+ /**
16
+ * Obtain a short-lived publishing token by exchanging an OIDC ID token. If unset, trusted
17
+ * publishing is used whenever an ID token source is detected and no access token is available.
18
+ */
19
+ trustedPublishing?: boolean;
20
+ /**
21
+ * The OIDC ID token to exchange. Only needed for CI systems that expose the token directly,
22
+ * such as GitLab CI; on GitHub Actions the token is requested from the workflow runtime.
23
+ */
24
+ idToken?: string;
25
+ /**
26
+ * The audience to request for the OIDC ID token. Defaults to the registry URL and must match
27
+ * the audience the registry expects.
28
+ */
29
+ oidcAudience?: string;
30
+ }
@@ -0,0 +1,148 @@
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 { getIdToken, hasIdTokenSource } from './oidc';
15
+ import { AccessToken, Registry } from './registry';
16
+ import { StatusError } from './util';
17
+ import { TrustedPublishingOptions } from './trusted-publishing-options';
18
+
19
+ const tokens = new Map<string, Promise<string>>();
20
+
21
+ // The value each cached exchange resolved to, so a token a publish was refused with can be told from one
22
+ // another publish has already replaced. Cleared while an exchange is in flight.
23
+ const issued = new Map<string, string>();
24
+
25
+ /**
26
+ * Whether the given options ask for trusted publishing. If the user did not decide explicitly,
27
+ * it is used whenever the surrounding CI system can provide an OIDC ID token.
28
+ */
29
+ export function useTrustedPublishing(options: TrustedPublishingOptions): boolean {
30
+ return options.trustedPublishing ?? hasIdTokenSource(options);
31
+ }
32
+
33
+ /**
34
+ * Exchanges an OIDC ID token for a short-lived access token that can publish the given extension.
35
+ * The token is not stored, it is only valid for a few minutes.
36
+ */
37
+ export function getTrustedPublishingToken(
38
+ registry: Registry,
39
+ namespace: string,
40
+ extension: string,
41
+ options: TrustedPublishingOptions
42
+ ): Promise<string> {
43
+ // publishing fans out over targets and package paths, but one token is enough per extension
44
+ const key = `${namespace}.${extension}`;
45
+ return tokens.get(key) ?? exchange(key, registry, namespace, extension, options);
46
+ }
47
+
48
+ /**
49
+ * Exchanges the ID token again after the registry refused the one this extension was publishing with.
50
+ * The issued token is short-lived and shared by every target platform of a release, so a slow fan-out
51
+ * can outlive it and be refused partway through.
52
+ *
53
+ * `stale` is the token that was refused. If the cache no longer holds it, another target has already
54
+ * replaced it and that exchange is awaited instead, so one expiry costs one exchange however many
55
+ * targets hit it at once.
56
+ */
57
+ export function refreshTrustedPublishingToken(
58
+ registry: Registry,
59
+ namespace: string,
60
+ extension: string,
61
+ options: TrustedPublishingOptions,
62
+ stale: string
63
+ ): Promise<string> {
64
+ const key = `${namespace}.${extension}`;
65
+ const current = tokens.get(key);
66
+ if (current && issued.get(key) !== stale) {
67
+ return current;
68
+ }
69
+
70
+ return exchange(key, registry, namespace, extension, options);
71
+ }
72
+
73
+ function exchange(
74
+ key: string,
75
+ registry: Registry,
76
+ namespace: string,
77
+ extension: string,
78
+ options: TrustedPublishingOptions
79
+ ): Promise<string> {
80
+ // Dropped up front, so a refresh that is still in flight does not look like the stale token to the
81
+ // next caller and start a second exchange of its own.
82
+ issued.delete(key);
83
+ const token = requestToken(registry, namespace, extension, options).then(value => {
84
+ issued.set(key, value);
85
+ return value;
86
+ });
87
+ tokens.set(key, token);
88
+ return token;
89
+ }
90
+
91
+ // The registry answers 503 when it could not reach the identity provider to check the token, as opposed
92
+ // to checking it and refusing. That is worth another go: publishing runs unattended on CI, and a blip
93
+ // reaching GitHub or GitLab should not fail a release build. A refusal is never retried.
94
+ const RETRYABLE_STATUS = [502, 503, 504];
95
+ const RETRY_DELAYS_MS = [1000, 4000];
96
+
97
+ async function requestToken(
98
+ registry: Registry,
99
+ namespace: string,
100
+ extension: string,
101
+ options: TrustedPublishingOptions
102
+ ): Promise<string> {
103
+ const audience = options.oidcAudience ?? registry.url;
104
+ const idToken = await getIdToken(audience, options);
105
+
106
+ let result: AccessToken;
107
+ try {
108
+ result = await exchangeWithRetry(registry, namespace, extension, idToken);
109
+ } catch (err) {
110
+ throw new Error(`${err.message}\n${registrationHint(registry, namespace, extension)}`);
111
+ }
112
+ if (result.error) {
113
+ throw new Error(`${result.error}\n${registrationHint(registry, namespace, extension)}`);
114
+ }
115
+ if (!result.value) {
116
+ throw new Error('The registry did not return a publishing token.');
117
+ }
118
+
119
+ const expires = result.expiresTimestamp ? `, expires at ${result.expiresTimestamp}` : '';
120
+ console.log(`\ud83d\udd10 Trusted publishing token issued for ${namespace}.${extension}${expires}`);
121
+ return result.value;
122
+ }
123
+
124
+ async function exchangeWithRetry(
125
+ registry: Registry,
126
+ namespace: string,
127
+ extension: string,
128
+ idToken: string
129
+ ): Promise<AccessToken> {
130
+ for (let attempt = 0; ; attempt++) {
131
+ try {
132
+ return await registry.requestTrustedPublishingToken(namespace, extension, idToken);
133
+ } catch (err) {
134
+ const status = (err as StatusError)?.status;
135
+ if (attempt >= RETRY_DELAYS_MS.length || status === undefined || !RETRYABLE_STATUS.includes(status)) {
136
+ throw err;
137
+ }
138
+ const delay = RETRY_DELAYS_MS[attempt];
139
+ console.log(`The registry could not verify the token (${status}), retrying in ${delay / 1000}s`);
140
+ await new Promise(resolve => setTimeout(resolve, delay));
141
+ }
142
+ }
143
+ }
144
+
145
+ function registrationHint(registry: Registry, namespace: string, extension: string): string {
146
+ return `Check the trusted publishers registered for '${namespace}.${extension}' at `
147
+ + `${registry.url}/user-settings/trusted-publishers.`;
148
+ }
@@ -0,0 +1,35 @@
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 { RegistryOptions } from './registry-options';
15
+
16
+ export interface UnpublishOptions extends RegistryOptions {
17
+ /**
18
+ * Identifier in the form `namespace.extension` or `namespace/extension`.
19
+ * Read from the `package.json` in the current directory if omitted.
20
+ */
21
+ extensionId?: string;
22
+ /**
23
+ * Versions to delete. All versions are deleted if omitted.
24
+ */
25
+ versions?: string[];
26
+ /**
27
+ * Target platforms to delete. All target platforms of the selected versions
28
+ * are deleted if omitted. Can only be used together with `versions`.
29
+ */
30
+ targets?: string[];
31
+ /**
32
+ * Delete without asking for confirmation.
33
+ */
34
+ force?: boolean;
35
+ }