ovsx 1.1.1 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +71 -1
- package/lib/get.js +1 -1
- package/lib/get.js.map +1 -1
- package/lib/index.d.ts +4 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +4 -0
- package/lib/index.js.map +1 -1
- package/lib/list-options.d.ts +24 -0
- package/lib/list-options.d.ts.map +1 -0
- package/lib/list-options.js +15 -0
- package/lib/list-options.js.map +1 -0
- package/lib/list.d.ts +18 -0
- package/lib/list.d.ts.map +1 -0
- package/lib/list.js +46 -0
- package/lib/list.js.map +1 -0
- package/lib/main.js +78 -2
- package/lib/main.js.map +1 -1
- package/lib/oidc.d.ts +22 -0
- package/lib/oidc.d.ts.map +1 -0
- package/lib/oidc.js +101 -0
- package/lib/oidc.js.map +1 -0
- package/lib/publish-options.d.ts +8 -1
- package/lib/publish-options.d.ts.map +1 -1
- package/lib/publish.d.ts.map +1 -1
- package/lib/publish.js +73 -4
- package/lib/publish.js.map +1 -1
- package/lib/registry-options.d.ts +8 -0
- package/lib/registry-options.d.ts.map +1 -1
- package/lib/registry.d.ts +119 -1
- package/lib/registry.d.ts.map +1 -1
- package/lib/registry.js +187 -19
- package/lib/registry.js.map +1 -1
- package/lib/search-options.d.ts +48 -0
- package/lib/search-options.d.ts.map +1 -0
- package/lib/search-options.js +15 -0
- package/lib/search-options.js.map +1 -0
- package/lib/search.d.ts +22 -0
- package/lib/search.d.ts.map +1 -0
- package/lib/search.js +86 -0
- package/lib/search.js.map +1 -0
- package/lib/show-options.d.ts +33 -0
- package/lib/show-options.d.ts.map +1 -0
- package/lib/show-options.js +15 -0
- package/lib/show-options.js.map +1 -0
- package/lib/show.d.ts +18 -0
- package/lib/show.d.ts.map +1 -0
- package/lib/show.js +289 -0
- package/lib/show.js.map +1 -0
- package/lib/table.d.ts +27 -0
- package/lib/table.d.ts.map +1 -0
- package/lib/table.js +56 -0
- package/lib/table.js.map +1 -0
- package/lib/trusted-publishing-options.d.ts +30 -0
- package/lib/trusted-publishing-options.d.ts.map +1 -0
- package/lib/trusted-publishing-options.js +15 -0
- package/lib/trusted-publishing-options.js.map +1 -0
- package/lib/trusted-publishing.d.ts +35 -0
- package/lib/trusted-publishing.d.ts.map +1 -0
- package/lib/trusted-publishing.js +112 -0
- package/lib/trusted-publishing.js.map +1 -0
- package/lib/unpublish-options.d.ts +34 -0
- package/lib/unpublish-options.d.ts.map +1 -0
- package/lib/unpublish-options.js +15 -0
- package/lib/unpublish-options.js.map +1 -0
- package/lib/unpublish.d.ts +22 -0
- package/lib/unpublish.d.ts.map +1 -0
- package/lib/unpublish.js +137 -0
- package/lib/unpublish.js.map +1 -0
- package/lib/util.d.ts +39 -1
- package/lib/util.d.ts.map +1 -1
- package/lib/util.js +91 -5
- package/lib/util.js.map +1 -1
- package/lib/verify-options.d.ts +24 -0
- package/lib/verify-options.d.ts.map +1 -0
- package/lib/verify-options.js +15 -0
- package/lib/verify-options.js.map +1 -0
- package/lib/verify-signature-options.d.ts +32 -0
- package/lib/verify-signature-options.d.ts.map +1 -0
- package/lib/verify-signature-options.js +15 -0
- package/lib/verify-signature-options.js.map +1 -0
- package/lib/verify-signature.d.ts +28 -0
- package/lib/verify-signature.d.ts.map +1 -0
- package/lib/verify-signature.js +105 -0
- package/lib/verify-signature.js.map +1 -0
- package/lib/verify.d.ts +21 -0
- package/lib/verify.d.ts.map +1 -0
- package/lib/verify.js +97 -0
- package/lib/verify.js.map +1 -0
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- package/lib/zip.d.ts +10 -0
- package/lib/zip.d.ts.map +1 -1
- package/lib/zip.js +31 -0
- package/lib/zip.js.map +1 -1
- package/package.json +9 -5
- package/src/get.ts +1 -1
- package/src/index.ts +4 -0
- package/src/list-options.ts +25 -0
- package/src/list.ts +48 -0
- package/src/main.ts +89 -3
- package/src/oidc.ts +107 -0
- package/src/publish-options.ts +8 -1
- package/src/publish.ts +97 -5
- package/src/registry-options.ts +8 -0
- package/src/registry.ts +292 -19
- package/src/search-options.ts +49 -0
- package/src/search.ts +95 -0
- package/src/show-options.ts +34 -0
- package/src/show.ts +328 -0
- package/src/table.ts +53 -0
- package/src/trusted-publishing-options.ts +30 -0
- package/src/trusted-publishing.ts +148 -0
- package/src/unpublish-options.ts +35 -0
- package/src/unpublish.ts +160 -0
- package/src/util.ts +102 -5
- package/src/verify-options.ts +25 -0
- package/src/verify-signature-options.ts +32 -0
- package/src/verify-signature.ts +124 -0
- package/src/verify.ts +107 -0
- package/src/version.ts +1 -1
- package/src/zip.ts +31 -1
package/src/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
|
+
}
|