@wpelevator/wp-release 0.3.0 → 0.4.1
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 +55 -8
- package/dist/checksums.js +3 -2
- package/dist/cli.d.ts +30 -1
- package/dist/cli.js +32 -0
- package/dist/commands/args.d.ts +10 -0
- package/dist/commands/args.js +13 -0
- package/dist/commands/checksums.d.ts +9 -7
- package/dist/commands/checksums.js +3 -3
- package/dist/commands/command.d.ts +2 -2
- package/dist/commands/deploy-svn.js +5 -2
- package/dist/commands/sign.d.ts +21 -0
- package/dist/commands/sign.js +55 -0
- package/dist/commands/verify.d.ts +14 -11
- package/dist/commands/verify.js +335 -27
- package/dist/commands/zip.d.ts +11 -8
- package/dist/commands/zip.js +4 -4
- package/dist/download.d.ts +15 -0
- package/dist/download.js +27 -0
- package/dist/index.d.ts +7 -2
- package/dist/index.js +3 -1
- package/dist/signing.d.ts +69 -0
- package/dist/signing.js +140 -0
- package/dist/svn-deploy.d.ts +12 -14
- package/dist/svn-deploy.js +44 -27
- package/package.json +5 -3
package/dist/commands/verify.js
CHANGED
|
@@ -1,13 +1,282 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { mkdir, mkdtemp, readFile, rm } from 'node:fs/promises';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { basename, isAbsolute, join, posix, resolve } from 'node:path';
|
|
2
5
|
import { readChecksums, verifyChecksums } from "../checksums.js";
|
|
6
|
+
import { downloadFile, isUrl } from "../download.js";
|
|
3
7
|
import { loadFileRules, parseExclude } from "../file-selection.js";
|
|
4
8
|
import { ValidationError } from "../errors.js";
|
|
5
|
-
import {
|
|
9
|
+
import { readPackageMeta } from "../package-meta.js";
|
|
10
|
+
import { extractZip, readZipEntries } from "../unzip.js";
|
|
11
|
+
import { SIGNATURE_EXTENSION, parsePublicKey, verifyFileSignature, } from "../signing.js";
|
|
12
|
+
import { checkDirectory, getSourceDirArg, ignoreOwnFile, ignoreOwnSignature, sourceDirArgs, } from "./args.js";
|
|
6
13
|
import { getString, getStrings } from "./command.js";
|
|
14
|
+
/**
|
|
15
|
+
* Fail unless the manifest has a valid signature file.
|
|
16
|
+
*
|
|
17
|
+
* @param checksumsPath Manifest path.
|
|
18
|
+
* @param publicKey Base64 public key.
|
|
19
|
+
* @throws {ValidationError} If the signature is missing or doesn't match.
|
|
20
|
+
*/
|
|
21
|
+
async function checkSignature(checksumsPath, publicKey) {
|
|
22
|
+
const signaturePath = checksumsPath + SIGNATURE_EXTENSION;
|
|
23
|
+
if (!existsSync(signaturePath)) {
|
|
24
|
+
throw new ValidationError(`${signaturePath} doesn't exist.`);
|
|
25
|
+
}
|
|
26
|
+
if (!(await verifyFileSignature(checksumsPath, signaturePath, parsePublicKey(publicKey)))) {
|
|
27
|
+
throw new ValidationError(`${signaturePath} isn't a valid signature of ${checksumsPath} for the public key.`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Find the single root folder that every archive entry is in.
|
|
32
|
+
*
|
|
33
|
+
* @param zipPath Archive path.
|
|
34
|
+
* @return Root folder name.
|
|
35
|
+
* @throws {ValidationError} If the entries don't share one root folder.
|
|
36
|
+
*/
|
|
37
|
+
async function findRootFolder(zipPath) {
|
|
38
|
+
const roots = new Set((await readZipEntries(zipPath)).map(({ path }) => path.split('/')[0] ?? ''));
|
|
39
|
+
const [root] = [...roots];
|
|
40
|
+
if (1 !== roots.size || !root) {
|
|
41
|
+
throw new ValidationError(`${zipPath} must have all files in one root folder, or pass the folder with --root-folder.`);
|
|
42
|
+
}
|
|
43
|
+
return root;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Get the directory to check: the source directory, or the files of an
|
|
47
|
+
* archive extracted into a temporary directory. Archives can be local files
|
|
48
|
+
* or http(s) URLs, such as the ZIP that WordPress.org builds for a release.
|
|
49
|
+
*
|
|
50
|
+
* @param input Source directory, archive and root folder.
|
|
51
|
+
* @param input.sourceDir
|
|
52
|
+
* @param input.zip
|
|
53
|
+
* @param input.cwd
|
|
54
|
+
* @param input.rootFolder
|
|
55
|
+
* @return Directory and cleanup function.
|
|
56
|
+
*/
|
|
57
|
+
async function resolveTarget(input) {
|
|
58
|
+
const { sourceDir, zip, cwd, rootFolder } = input;
|
|
59
|
+
if (!zip) {
|
|
60
|
+
await checkDirectory(sourceDir);
|
|
61
|
+
return { dir: sourceDir, cleanup: async () => undefined };
|
|
62
|
+
}
|
|
63
|
+
const tempDir = await mkdtemp(join(tmpdir(), 'wp-release-verify-'));
|
|
64
|
+
const cleanup = () => rm(tempDir, { recursive: true, force: true });
|
|
65
|
+
try {
|
|
66
|
+
const zipPath = isUrl(zip)
|
|
67
|
+
? join(tempDir, 'download.zip')
|
|
68
|
+
: resolve(cwd, zip);
|
|
69
|
+
const dir = join(tempDir, 'files');
|
|
70
|
+
if (isUrl(zip)) {
|
|
71
|
+
await downloadFile(zip, zipPath);
|
|
72
|
+
}
|
|
73
|
+
await mkdir(dir);
|
|
74
|
+
await extractZip({
|
|
75
|
+
zipPath,
|
|
76
|
+
targetDir: dir,
|
|
77
|
+
rootFolder: rootFolder ?? (await findRootFolder(zipPath)),
|
|
78
|
+
});
|
|
79
|
+
return { dir, cleanup };
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
await cleanup();
|
|
83
|
+
throw error;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Get the path of the embedded manifest inside the checked files: the
|
|
88
|
+
* explicit path, or the name of the manifest at the root when it's a JSON
|
|
89
|
+
* file.
|
|
90
|
+
*
|
|
91
|
+
* @param checksumsPath Manifest path.
|
|
92
|
+
* @param embedded Explicit path from --embedded.
|
|
93
|
+
* @return Relative path, or null when there's no embedded manifest to look for.
|
|
94
|
+
* @throws {ValidationError} If the explicit path isn't relative.
|
|
95
|
+
*/
|
|
96
|
+
function getEmbeddedPath(checksumsPath, embedded) {
|
|
97
|
+
const path = posix.normalize(embedded ?? basename(checksumsPath));
|
|
98
|
+
if (embedded &&
|
|
99
|
+
(isAbsolute(embedded) ||
|
|
100
|
+
embedded.includes('\\') ||
|
|
101
|
+
path.startsWith('../') ||
|
|
102
|
+
['.', '..'].includes(path))) {
|
|
103
|
+
throw new ValidationError(`--embedded must be a path inside the plugin with forward slashes, got "${embedded}".`);
|
|
104
|
+
}
|
|
105
|
+
return embedded || path.endsWith('.json') ? path : null;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Get the manifest and signature pairs to look for inside the checked files.
|
|
109
|
+
*
|
|
110
|
+
* @param checksumsPath Manifest path.
|
|
111
|
+
* @param path Relative path of the embedded manifest, if any.
|
|
112
|
+
* @return Pairs of the originals and their copies.
|
|
113
|
+
*/
|
|
114
|
+
function getCopyPairs(checksumsPath, path) {
|
|
115
|
+
return path
|
|
116
|
+
? [
|
|
117
|
+
{ original: checksumsPath, copy: path },
|
|
118
|
+
{
|
|
119
|
+
original: checksumsPath + SIGNATURE_EXTENSION,
|
|
120
|
+
copy: path + SIGNATURE_EXTENSION,
|
|
121
|
+
},
|
|
122
|
+
]
|
|
123
|
+
: [];
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Check that an explicit --embedded path can be verified: the manifest can't
|
|
127
|
+
* list its own copies, and the originals to compare with must exist.
|
|
128
|
+
*
|
|
129
|
+
* @param pairs Pairs of the originals and their copies.
|
|
130
|
+
* @param manifest Checksums manifest.
|
|
131
|
+
* @throws {ValidationError} If a copy is listed or an original is missing.
|
|
132
|
+
*/
|
|
133
|
+
function checkExplicitPairs(pairs, manifest) {
|
|
134
|
+
pairs.forEach(({ original, copy }) => {
|
|
135
|
+
if (Object.hasOwn(manifest.files, copy)) {
|
|
136
|
+
throw new ValidationError(`The checksums list ${copy}, but a manifest can't list its own embedded copy.`);
|
|
137
|
+
}
|
|
138
|
+
if (!existsSync(original)) {
|
|
139
|
+
throw new ValidationError(`--embedded needs ${original} to compare ${copy} with, but it doesn't exist.`);
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Check that two paths are the same file or have the same bytes.
|
|
145
|
+
*
|
|
146
|
+
* @param original Path of the original.
|
|
147
|
+
* @param file Path of the copy.
|
|
148
|
+
* @return Whether the contents are identical.
|
|
149
|
+
*/
|
|
150
|
+
async function hasSameContents(original, file) {
|
|
151
|
+
return (resolve(original) === resolve(file) ||
|
|
152
|
+
(await readFile(original)).equals(await readFile(file)));
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Compare the copies of the manifest and its signature inside the checked
|
|
156
|
+
* directory with the given ones. A release that ships the manifest and its
|
|
157
|
+
* signature with the plugin files must ship the same bytes that were
|
|
158
|
+
* signed, otherwise a verifier on the site would check something else. The
|
|
159
|
+
* copies are not part of the manifest, since a manifest can't list itself,
|
|
160
|
+
* so they're skipped when comparing the files.
|
|
161
|
+
*
|
|
162
|
+
* With an explicit path both originals and both copies must exist. Without
|
|
163
|
+
* one, the copies are expected at the root with the name of the manifest,
|
|
164
|
+
* and a file is treated as a copy only when it exists, the matching
|
|
165
|
+
* original exists and the manifest doesn't list it. Any other file is
|
|
166
|
+
* checked like the rest of the files.
|
|
167
|
+
*
|
|
168
|
+
* @param input Directory, manifest and embedded path.
|
|
169
|
+
* @param input.dir Directory to check.
|
|
170
|
+
* @param input.manifest Checksums manifest.
|
|
171
|
+
* @param input.checksumsPath Manifest path.
|
|
172
|
+
* @param input.embedded Explicit path of the embedded manifest.
|
|
173
|
+
* @return The copies that were found to differ or be missing, and the paths of the existing copies.
|
|
174
|
+
* @throws {ValidationError} If an explicit path can't be verified.
|
|
175
|
+
*/
|
|
176
|
+
async function checkEmbeddedCopies(input) {
|
|
177
|
+
const { dir, manifest, checksumsPath, embedded } = input;
|
|
178
|
+
const pairs = getCopyPairs(checksumsPath, getEmbeddedPath(checksumsPath, embedded));
|
|
179
|
+
if (embedded) {
|
|
180
|
+
checkExplicitPairs(pairs, manifest);
|
|
181
|
+
}
|
|
182
|
+
const found = pairs.filter(({ original, copy }) => existsSync(join(dir, copy)) &&
|
|
183
|
+
(embedded ||
|
|
184
|
+
(existsSync(original) &&
|
|
185
|
+
!Object.hasOwn(manifest.files, copy))));
|
|
186
|
+
const results = await Promise.all(found.map(async ({ original, copy }) => ({
|
|
187
|
+
copy,
|
|
188
|
+
isSame: await hasSameContents(original, join(dir, copy)),
|
|
189
|
+
})));
|
|
190
|
+
return {
|
|
191
|
+
paths: found.map(({ copy }) => copy),
|
|
192
|
+
differing: results
|
|
193
|
+
.filter(({ isSame }) => !isSame)
|
|
194
|
+
.map(({ copy }) => copy),
|
|
195
|
+
missing: embedded
|
|
196
|
+
? pairs
|
|
197
|
+
.filter(({ copy }) => !existsSync(join(dir, copy)))
|
|
198
|
+
.map(({ copy }) => copy)
|
|
199
|
+
: [],
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Compare the version in the manifest with the Version header of the
|
|
204
|
+
* checked files, so that an older signed release can't pass as a newer one.
|
|
205
|
+
*
|
|
206
|
+
* @param dir Directory to check.
|
|
207
|
+
* @param manifest Checksums manifest.
|
|
208
|
+
* @return The two versions when they differ, otherwise null.
|
|
209
|
+
*/
|
|
210
|
+
async function findVersionMismatch(dir, manifest) {
|
|
211
|
+
const meta = await readPackageMeta({
|
|
212
|
+
dir,
|
|
213
|
+
slug: manifest.plugin ?? manifest.theme,
|
|
214
|
+
});
|
|
215
|
+
const files = meta?.version;
|
|
216
|
+
return manifest.version && files && manifest.version !== files
|
|
217
|
+
? { manifest: manifest.version, files }
|
|
218
|
+
: null;
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Compare the files of a directory with the manifest and log the result.
|
|
222
|
+
*
|
|
223
|
+
* @param input Directory, manifest and options.
|
|
224
|
+
* @param input.target
|
|
225
|
+
* @param input.manifest
|
|
226
|
+
* @param input.checksumsPath
|
|
227
|
+
* @param input.exclude
|
|
228
|
+
* @param input.embedded
|
|
229
|
+
* @param input.logger
|
|
230
|
+
* @return Exit code and result.
|
|
231
|
+
*/
|
|
232
|
+
async function verifyTarget(input) {
|
|
233
|
+
const { target, manifest, checksumsPath, exclude, embedded, logger } = input;
|
|
234
|
+
const { dir } = target;
|
|
235
|
+
const copies = await checkEmbeddedCopies({
|
|
236
|
+
dir,
|
|
237
|
+
manifest,
|
|
238
|
+
checksumsPath,
|
|
239
|
+
embedded,
|
|
240
|
+
});
|
|
241
|
+
const rules = ignoreOwnSignature(ignoreOwnFile(await loadFileRules({ exclude: parseExclude(exclude) }), dir, checksumsPath), dir, checksumsPath);
|
|
242
|
+
copies.paths.forEach((copy) => ignoreOwnFile(rules, dir, join(dir, copy)));
|
|
243
|
+
const comparison = await verifyChecksums({
|
|
244
|
+
sourceDir: dir,
|
|
245
|
+
manifest,
|
|
246
|
+
rules,
|
|
247
|
+
});
|
|
248
|
+
const { matched, extra } = comparison;
|
|
249
|
+
const missing = [...comparison.missing, ...copies.missing];
|
|
250
|
+
const changed = [...comparison.changed, ...copies.differing];
|
|
251
|
+
const versionMismatch = await findVersionMismatch(dir, manifest);
|
|
252
|
+
const valid = comparison.valid &&
|
|
253
|
+
!copies.differing.length &&
|
|
254
|
+
!copies.missing.length &&
|
|
255
|
+
!versionMismatch;
|
|
256
|
+
changed.forEach((file) => logger.error(`Changed: ${file}`));
|
|
257
|
+
missing.forEach((file) => logger.error(`Missing: ${file}`));
|
|
258
|
+
extra.forEach((file) => logger.error(`Not in the checksums: ${file}`));
|
|
259
|
+
if (versionMismatch) {
|
|
260
|
+
logger.error(`The checksums are for version ${versionMismatch.manifest}, but the files are version ${versionMismatch.files}.`);
|
|
261
|
+
}
|
|
262
|
+
logger.info(`${matched.length} files match, ${changed.length} changed, ${missing.length} missing and ${extra.length} extra.`);
|
|
263
|
+
return {
|
|
264
|
+
exitCode: valid ? 0 : 2,
|
|
265
|
+
result: {
|
|
266
|
+
valid,
|
|
267
|
+
checksumsPath,
|
|
268
|
+
matched: matched.length,
|
|
269
|
+
changed,
|
|
270
|
+
missing,
|
|
271
|
+
extra,
|
|
272
|
+
versionMismatch,
|
|
273
|
+
},
|
|
274
|
+
};
|
|
275
|
+
}
|
|
7
276
|
export default {
|
|
8
277
|
name: 'verify',
|
|
9
278
|
usage: 'verify [<source-dir>] --checksums <file> [options]',
|
|
10
|
-
summary: 'Check the files of a plugin or theme directory against a checksums manifest.',
|
|
279
|
+
summary: 'Check the files of a plugin or theme directory or ZIP against a checksums manifest.',
|
|
11
280
|
help: `Arguments:
|
|
12
281
|
<source-dir> Plugin or theme directory (default: .), also
|
|
13
282
|
accepted as --source-dir <dir>
|
|
@@ -15,46 +284,85 @@ export default {
|
|
|
15
284
|
Options:
|
|
16
285
|
--checksums <file> Checksums manifest from "wp-release checksums" or the
|
|
17
286
|
WordPress.org checksums API (required)
|
|
287
|
+
--zip <file|url> Check the files of this ZIP, a local file or an
|
|
288
|
+
http(s) URL such as the ZIP that WordPress.org
|
|
289
|
+
builds for a release, instead of <source-dir>
|
|
290
|
+
--root-folder <name> Folder in the ZIP (default: the one folder that
|
|
291
|
+
every entry is in)
|
|
292
|
+
--embedded <path> Where the manifest and its .sig are shipped inside the
|
|
293
|
+
files, like includes/checksums.json. Both the
|
|
294
|
+
--checksums file and its .sig must exist, both
|
|
295
|
+
copies must be shipped, and they must be identical
|
|
296
|
+
to them. Without this option, a file with the name
|
|
297
|
+
of the --checksums file at the root (and its .sig)
|
|
298
|
+
is a copy only when it exists, the matching original
|
|
299
|
+
exists and the manifest doesn't list it
|
|
300
|
+
--public-key <key> Base64 Ed25519 public key. Checks <checksums>.sig, as
|
|
301
|
+
written by "wp-release sign", before comparing any
|
|
302
|
+
file, and fails when it's missing or invalid
|
|
18
303
|
--exclude <patterns> Gitignore-style patterns to skip in both the directory
|
|
19
304
|
and the manifest, comma-separated or repeated
|
|
20
305
|
|
|
21
306
|
Reports every changed, missing and extra file, and exits with 2 unless all
|
|
22
|
-
files match.
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
307
|
+
files match. A file matches when its SHA-256 equals the manifest, or its MD5
|
|
308
|
+
for manifests without SHA-256. Exits with 2 too when the manifest version
|
|
309
|
+
differs from the Version header of the files.
|
|
310
|
+
|
|
311
|
+
Unlike "checksums" and "zip", verify doesn't read .distignore, so that an
|
|
312
|
+
ignore file inside the checked directory can't hide added files. Use
|
|
313
|
+
--exclude to skip files that aren't part of the release.
|
|
314
|
+
|
|
315
|
+
A manifest can't list itself. A copy of the manifest and of its .sig inside
|
|
316
|
+
the checked files is therefore skipped, but it must be identical to the
|
|
317
|
+
--checksums file. A path that the manifest lists is never skipped, and a
|
|
318
|
+
file that isn't a matching copy is reported as extra. See --embedded for
|
|
319
|
+
where the copies are looked for.`,
|
|
26
320
|
options: {
|
|
27
321
|
checksums: { type: 'string' },
|
|
322
|
+
zip: { type: 'string' },
|
|
323
|
+
'root-folder': { type: 'string' },
|
|
324
|
+
embedded: { type: 'string' },
|
|
325
|
+
'public-key': { type: 'string' },
|
|
28
326
|
...sourceDirArgs,
|
|
29
327
|
exclude: { type: 'string', multiple: true },
|
|
30
328
|
},
|
|
31
329
|
async run({ values, positionals, cwd, logger }) {
|
|
32
|
-
const
|
|
330
|
+
const zip = getString(values, 'zip');
|
|
331
|
+
const sourceArg = positionals.length
|
|
332
|
+
? getSourceDirArg(values, positionals)
|
|
333
|
+
: getString(values, 'source-dir');
|
|
33
334
|
const checksums = getString(values, 'checksums');
|
|
34
335
|
if (!checksums) {
|
|
35
336
|
throw new ValidationError('Pass the checksums manifest with --checksums <file>.');
|
|
36
337
|
}
|
|
37
|
-
|
|
338
|
+
if (zip && sourceArg) {
|
|
339
|
+
throw new ValidationError('Pass either --zip or a source directory, not both.');
|
|
340
|
+
}
|
|
38
341
|
const checksumsPath = resolve(cwd, checksums);
|
|
342
|
+
const publicKey = getString(values, 'public-key');
|
|
343
|
+
if (publicKey) {
|
|
344
|
+
await checkSignature(checksumsPath, publicKey);
|
|
345
|
+
logger.info(`Signature of ${checksumsPath} is valid.`);
|
|
346
|
+
}
|
|
39
347
|
const manifest = await readChecksums(checksumsPath);
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
result: {
|
|
51
|
-
valid,
|
|
348
|
+
const target = await resolveTarget({
|
|
349
|
+
sourceDir: resolve(cwd, sourceArg ?? '.'),
|
|
350
|
+
zip,
|
|
351
|
+
cwd,
|
|
352
|
+
rootFolder: getString(values, 'root-folder'),
|
|
353
|
+
});
|
|
354
|
+
try {
|
|
355
|
+
return await verifyTarget({
|
|
356
|
+
target,
|
|
357
|
+
manifest,
|
|
52
358
|
checksumsPath,
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
359
|
+
exclude: getStrings(values, 'exclude'),
|
|
360
|
+
embedded: getString(values, 'embedded'),
|
|
361
|
+
logger,
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
finally {
|
|
365
|
+
await target.cleanup();
|
|
366
|
+
}
|
|
59
367
|
},
|
|
60
368
|
};
|
package/dist/commands/zip.d.ts
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
import type { CommandOutcome } from './command.ts';
|
|
2
|
+
/** Result of the zip command. */
|
|
3
|
+
export interface ZipResult {
|
|
4
|
+
/** Absolute path of the ZIP. */
|
|
5
|
+
zipPath: string;
|
|
6
|
+
/** Absolute path of the checksums manifest, set with --checksums. */
|
|
7
|
+
checksumsPath?: string;
|
|
8
|
+
/** Number of archived files. */
|
|
9
|
+
files: number;
|
|
10
|
+
}
|
|
1
11
|
declare const _default: {
|
|
2
12
|
name: string;
|
|
3
13
|
usage: string;
|
|
@@ -29,13 +39,6 @@ declare const _default: {
|
|
|
29
39
|
type: "string";
|
|
30
40
|
};
|
|
31
41
|
};
|
|
32
|
-
run({ values, positionals, cwd, logger }: import("./command.ts").CommandContext): Promise<
|
|
33
|
-
exitCode: number;
|
|
34
|
-
result: {
|
|
35
|
-
files: number;
|
|
36
|
-
checksumsPath?: string | undefined;
|
|
37
|
-
zipPath: string;
|
|
38
|
-
};
|
|
39
|
-
}>;
|
|
42
|
+
run({ values, positionals, cwd, logger, }: import("./command.ts").CommandContext): Promise<CommandOutcome<ZipResult>>;
|
|
40
43
|
};
|
|
41
44
|
export default _default;
|
package/dist/commands/zip.js
CHANGED
|
@@ -6,7 +6,7 @@ import { loadFileRules, resolveFileSelection, selectFiles, } from "../file-selec
|
|
|
6
6
|
import { FILENAME_FORMAT, formatFilename } from "../filename.js";
|
|
7
7
|
import { readPackageMeta } from "../package-meta.js";
|
|
8
8
|
import { createZip } from "../zip.js";
|
|
9
|
-
import { checkDirectory, fileSelectionArgs, getFileSelectionInput, getSourceDirArg, ignoreOwnFile, resolveOutput, sourceDirArgs, } from "./args.js";
|
|
9
|
+
import { checkDirectory, fileSelectionArgs, getFileSelectionInput, getSourceDirArg, ignoreOwnFile, ignoreOwnSignature, resolveOutput, sourceDirArgs, } from "./args.js";
|
|
10
10
|
import { getBoolean, getString } from "./command.js";
|
|
11
11
|
const ZIP_EXTENSION = '.zip';
|
|
12
12
|
/**
|
|
@@ -74,7 +74,7 @@ always give the same ZIP.`,
|
|
|
74
74
|
checksums: { type: 'boolean' },
|
|
75
75
|
force: { type: 'boolean' },
|
|
76
76
|
},
|
|
77
|
-
async run({ values, positionals, cwd, logger }) {
|
|
77
|
+
async run({ values, positionals, cwd, logger, }) {
|
|
78
78
|
const sourceDir = resolve(cwd, getSourceDirArg(values, positionals));
|
|
79
79
|
const slug = getString(values, 'slug') ?? basename(cwd);
|
|
80
80
|
const force = getBoolean(values, 'force');
|
|
@@ -95,9 +95,9 @@ always give the same ZIP.`,
|
|
|
95
95
|
sourceDir,
|
|
96
96
|
...getFileSelectionInput(values),
|
|
97
97
|
});
|
|
98
|
-
const rules = ignoreOwnFile(await loadFileRules(selection), sourceDir, zipPath);
|
|
98
|
+
const rules = ignoreOwnSignature(ignoreOwnFile(await loadFileRules(selection), sourceDir, zipPath), sourceDir, zipPath);
|
|
99
99
|
if (checksumsPath) {
|
|
100
|
-
ignoreOwnFile(rules, sourceDir, checksumsPath);
|
|
100
|
+
ignoreOwnSignature(ignoreOwnFile(rules, sourceDir, checksumsPath), sourceDir, checksumsPath);
|
|
101
101
|
}
|
|
102
102
|
const files = await selectFiles(sourceDir, rules);
|
|
103
103
|
if (!files.length) {
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Check if a location is an http(s) URL instead of a file path.
|
|
3
|
+
*
|
|
4
|
+
* @param location File path or URL.
|
|
5
|
+
* @return Whether it's a URL.
|
|
6
|
+
*/
|
|
7
|
+
export declare function isUrl(location: string): boolean;
|
|
8
|
+
/**
|
|
9
|
+
* Download a URL to a file. Redirects are followed.
|
|
10
|
+
*
|
|
11
|
+
* @param url http or https URL.
|
|
12
|
+
* @param path Destination file path.
|
|
13
|
+
* @throws {ValidationError} If the server doesn't answer with a body.
|
|
14
|
+
*/
|
|
15
|
+
export declare function downloadFile(url: string, path: string): Promise<void>;
|
package/dist/download.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { createWriteStream } from 'node:fs';
|
|
2
|
+
import { Readable } from 'node:stream';
|
|
3
|
+
import { pipeline } from 'node:stream/promises';
|
|
4
|
+
import { ValidationError } from "./errors.js";
|
|
5
|
+
/**
|
|
6
|
+
* Check if a location is an http(s) URL instead of a file path.
|
|
7
|
+
*
|
|
8
|
+
* @param location File path or URL.
|
|
9
|
+
* @return Whether it's a URL.
|
|
10
|
+
*/
|
|
11
|
+
export function isUrl(location) {
|
|
12
|
+
return /^https?:\/\//i.test(location);
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Download a URL to a file. Redirects are followed.
|
|
16
|
+
*
|
|
17
|
+
* @param url http or https URL.
|
|
18
|
+
* @param path Destination file path.
|
|
19
|
+
* @throws {ValidationError} If the server doesn't answer with a body.
|
|
20
|
+
*/
|
|
21
|
+
export async function downloadFile(url, path) {
|
|
22
|
+
const response = await fetch(url);
|
|
23
|
+
if (!response.ok || !response.body) {
|
|
24
|
+
throw new ValidationError(`Couldn't download ${url}: ${response.status} ${response.statusText}`.trim());
|
|
25
|
+
}
|
|
26
|
+
await pipeline(Readable.fromWeb(response.body), createWriteStream(path));
|
|
27
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { run, commands, getHelp, getCommandHelp } from './cli.ts';
|
|
1
|
+
export { run, execute, commands, getHelp, getCommandHelp } from './cli.ts';
|
|
2
2
|
export { getDeployInputFromArgs } from './commands/deploy-svn.ts';
|
|
3
3
|
export { ValidationError, CredentialsError } from './errors.ts';
|
|
4
4
|
export { Logger } from './logger.ts';
|
|
@@ -7,13 +7,18 @@ export { DISTIGNORE_FILENAME, parseExclude, resolveFileSelection, loadFileRules,
|
|
|
7
7
|
export { FILENAME_FORMAT, formatFilename } from './filename.ts';
|
|
8
8
|
export { CHECKSUMS_EXTENSION, CHECKSUM_ALGORITHMS, hashFile, hashFiles, createChecksums, writeChecksums, readChecksums, parseChecksums, compareChecksums, verifyChecksums, } from './checksums.ts';
|
|
9
9
|
export { HEADER_BYTES, PLUGIN_HEADERS, THEME_HEADERS, README_HEADERS, README_FILENAMES, parseFileHeaders, parseReadmeHeaders, readFileStart, findMainPluginFile, readPackageMeta, } from './package-meta.ts';
|
|
10
|
+
export { SIGNATURE_EXTENSION, SIGNING_KEY_ENV, parseSigningKey, parsePublicKey, getPublicKey, getFileDigest, signFile, writeSignature, verifyFileSignature, } from './signing.ts';
|
|
10
11
|
export { ZIP_MTIME, ZIP_FILE_MODE, createZip } from './zip.ts';
|
|
12
|
+
export { isUrl, downloadFile } from './download.ts';
|
|
11
13
|
export { readZipEntries, getZipEntryErrors, extractZip } from './unzip.ts';
|
|
12
14
|
export { GIT_TAG_FORMAT, getGitTagVersion, findConstantValues, findVersionMismatches, } from './version-check.ts';
|
|
13
15
|
export { Svn, SvnError, createSvnRunner, parseStatus, toSvnPath, } from './svn.ts';
|
|
14
16
|
export { syncDirectory, copyFileIfChanged } from './sync.ts';
|
|
15
17
|
export { ASSET_MIME_TYPES, SvnDeploy } from './svn-deploy.ts';
|
|
16
|
-
export type { CliIo } from './cli.ts';
|
|
18
|
+
export type { CliIo, CommandResults } from './cli.ts';
|
|
19
|
+
export type { ChecksumsResult } from './commands/checksums.ts';
|
|
20
|
+
export type { SignResult } from './commands/sign.ts';
|
|
21
|
+
export type { ZipResult } from './commands/zip.ts';
|
|
17
22
|
export type { Command, CommandContext, CommandOutcome, ParsedValues, } from './commands/command.ts';
|
|
18
23
|
export type { DeployArgsInput } from './commands/deploy-svn.ts';
|
|
19
24
|
export type { LoggerOptions, OutputStream } from './logger.ts';
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { run, commands, getHelp, getCommandHelp } from "./cli.js";
|
|
1
|
+
export { run, execute, commands, getHelp, getCommandHelp } from "./cli.js";
|
|
2
2
|
export { getDeployInputFromArgs } from "./commands/deploy-svn.js";
|
|
3
3
|
export { ValidationError, CredentialsError } from "./errors.js";
|
|
4
4
|
export { Logger } from "./logger.js";
|
|
@@ -7,7 +7,9 @@ export { DISTIGNORE_FILENAME, parseExclude, resolveFileSelection, loadFileRules,
|
|
|
7
7
|
export { FILENAME_FORMAT, formatFilename } from "./filename.js";
|
|
8
8
|
export { CHECKSUMS_EXTENSION, CHECKSUM_ALGORITHMS, hashFile, hashFiles, createChecksums, writeChecksums, readChecksums, parseChecksums, compareChecksums, verifyChecksums, } from "./checksums.js";
|
|
9
9
|
export { HEADER_BYTES, PLUGIN_HEADERS, THEME_HEADERS, README_HEADERS, README_FILENAMES, parseFileHeaders, parseReadmeHeaders, readFileStart, findMainPluginFile, readPackageMeta, } from "./package-meta.js";
|
|
10
|
+
export { SIGNATURE_EXTENSION, SIGNING_KEY_ENV, parseSigningKey, parsePublicKey, getPublicKey, getFileDigest, signFile, writeSignature, verifyFileSignature, } from "./signing.js";
|
|
10
11
|
export { ZIP_MTIME, ZIP_FILE_MODE, createZip } from "./zip.js";
|
|
12
|
+
export { isUrl, downloadFile } from "./download.js";
|
|
11
13
|
export { readZipEntries, getZipEntryErrors, extractZip } from "./unzip.js";
|
|
12
14
|
export { GIT_TAG_FORMAT, getGitTagVersion, findConstantValues, findVersionMismatches, } from "./version-check.js";
|
|
13
15
|
export { Svn, SvnError, createSvnRunner, parseStatus, toSvnPath, } from "./svn.js";
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { KeyObject } from 'node:crypto';
|
|
2
|
+
export declare const SIGNATURE_EXTENSION = ".sig";
|
|
3
|
+
/**
|
|
4
|
+
* Environment variable with the base64 Ed25519 signing key.
|
|
5
|
+
*/
|
|
6
|
+
export declare const SIGNING_KEY_ENV = "WP_RELEASE_SIGNING_KEY";
|
|
7
|
+
/**
|
|
8
|
+
* Get an Ed25519 private key from the base64 format that sodium and the
|
|
9
|
+
* update-client signing_key setting use: a 32-byte seed or a 64-byte
|
|
10
|
+
* libsodium secret key, which is the seed followed by the public key.
|
|
11
|
+
*
|
|
12
|
+
* @param encoded Base64 key.
|
|
13
|
+
* @return Private key.
|
|
14
|
+
* @throws {ValidationError} If the key has an unsupported length.
|
|
15
|
+
*/
|
|
16
|
+
export declare function parseSigningKey(encoded: string): KeyObject;
|
|
17
|
+
/**
|
|
18
|
+
* Get an Ed25519 public key from its base64 form.
|
|
19
|
+
*
|
|
20
|
+
* @param encoded Base64 32-byte public key.
|
|
21
|
+
* @return Public key.
|
|
22
|
+
* @throws {ValidationError} If the key has an unsupported length.
|
|
23
|
+
*/
|
|
24
|
+
export declare function parsePublicKey(encoded: string): KeyObject;
|
|
25
|
+
/**
|
|
26
|
+
* Get the base64 public key for a private key, in the format that
|
|
27
|
+
* verify_file_signature() callers and update-client take.
|
|
28
|
+
*
|
|
29
|
+
* @param privateKey Private key from parseSigningKey().
|
|
30
|
+
* @return Base64 public key.
|
|
31
|
+
*/
|
|
32
|
+
export declare function getPublicKey(privateKey: KeyObject): string;
|
|
33
|
+
/**
|
|
34
|
+
* Get the raw SHA-384 digest of a file, which is what WordPress core's
|
|
35
|
+
* verify_file_signature() verifies the signature against.
|
|
36
|
+
*
|
|
37
|
+
* @param path File path.
|
|
38
|
+
* @return Digest bytes.
|
|
39
|
+
*/
|
|
40
|
+
export declare function getFileDigest(path: string): Promise<Buffer>;
|
|
41
|
+
/**
|
|
42
|
+
* Sign a file with a detached Ed25519 signature of its raw SHA-384 digest.
|
|
43
|
+
*
|
|
44
|
+
* @param path File path.
|
|
45
|
+
* @param privateKey Private key from parseSigningKey().
|
|
46
|
+
* @return Base64 signature.
|
|
47
|
+
*/
|
|
48
|
+
export declare function signFile(path: string, privateKey: KeyObject): Promise<string>;
|
|
49
|
+
/**
|
|
50
|
+
* Write the signature of a file to a .sig file next to it.
|
|
51
|
+
*
|
|
52
|
+
* @param path File path.
|
|
53
|
+
* @param privateKey Private key from parseSigningKey().
|
|
54
|
+
* @param force Whether to overwrite an existing signature file.
|
|
55
|
+
* @return Signature file path.
|
|
56
|
+
* @throws {ValidationError} If the signature file exists and force isn't set.
|
|
57
|
+
*/
|
|
58
|
+
export declare function writeSignature(path: string, privateKey: KeyObject, force?: boolean): Promise<string>;
|
|
59
|
+
/**
|
|
60
|
+
* Check a file against a .sig file, which holds one base64 signature per
|
|
61
|
+
* line. The file is valid when any of the signatures match, so that a file
|
|
62
|
+
* can carry the signatures of a key rotation window.
|
|
63
|
+
*
|
|
64
|
+
* @param path File path.
|
|
65
|
+
* @param signaturePath Signature file path.
|
|
66
|
+
* @param publicKey Public key from parsePublicKey().
|
|
67
|
+
* @return Whether a signature matches.
|
|
68
|
+
*/
|
|
69
|
+
export declare function verifyFileSignature(path: string, signaturePath: string, publicKey: KeyObject): Promise<boolean>;
|