@wpelevator/wp-release 0.3.0 → 0.4.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.
@@ -1,13 +1,240 @@
1
- import { resolve } from 'node:path';
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 { checkDirectory, getSourceDirArg, ignoreOwnFile, sourceDirArgs, } from "./args.js";
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
+ '.' === 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
+ * Compare the copies of the manifest and its signature inside the checked
109
+ * directory with the given ones. A release that ships the manifest and its
110
+ * signature with the plugin files must ship the same bytes that were
111
+ * signed, otherwise a verifier on the site would check something else. The
112
+ * copies are not part of the manifest, since a manifest can't list itself,
113
+ * so they're skipped when comparing the files.
114
+ *
115
+ * Without an explicit path, the copies are expected at the root with the
116
+ * name of the manifest, and are checked only when they exist. With an
117
+ * explicit path they must exist.
118
+ *
119
+ * @param input Directory, manifest and embedded path.
120
+ * @param input.dir Directory to check.
121
+ * @param input.checksumsPath Manifest path.
122
+ * @param input.embedded Explicit path of the embedded manifest.
123
+ * @return The copies that were found to differ or be missing, and their paths.
124
+ */
125
+ async function checkEmbeddedCopies(input) {
126
+ const { dir, checksumsPath, embedded } = input;
127
+ const path = getEmbeddedPath(checksumsPath, embedded);
128
+ const pairs = path
129
+ ? [
130
+ { original: checksumsPath, copy: path },
131
+ {
132
+ original: checksumsPath + SIGNATURE_EXTENSION,
133
+ copy: path + SIGNATURE_EXTENSION,
134
+ },
135
+ ]
136
+ : [];
137
+ const results = await Promise.all(pairs.map(async ({ original, copy }) => {
138
+ const file = join(dir, copy);
139
+ const hasCopy = existsSync(file);
140
+ const hasOriginal = existsSync(original);
141
+ const isSame = !hasCopy ||
142
+ !hasOriginal ||
143
+ resolve(original) === resolve(file) ||
144
+ (await readFile(original)).equals(await readFile(file));
145
+ return {
146
+ copy,
147
+ missing: Boolean(embedded) && hasOriginal && !hasCopy,
148
+ differing: !isSame,
149
+ };
150
+ }));
151
+ return {
152
+ paths: pairs.map(({ copy }) => copy),
153
+ differing: results
154
+ .filter((result) => result.differing)
155
+ .map((result) => result.copy),
156
+ missing: results
157
+ .filter((result) => result.missing)
158
+ .map((result) => result.copy),
159
+ };
160
+ }
161
+ /**
162
+ * Compare the version in the manifest with the Version header of the
163
+ * checked files, so that an older signed release can't pass as a newer one.
164
+ *
165
+ * @param dir Directory to check.
166
+ * @param manifest Checksums manifest.
167
+ * @return The two versions when they differ, otherwise null.
168
+ */
169
+ async function findVersionMismatch(dir, manifest) {
170
+ const meta = await readPackageMeta({
171
+ dir,
172
+ slug: manifest.plugin ?? manifest.theme,
173
+ });
174
+ const files = meta?.version;
175
+ return manifest.version && files && manifest.version !== files
176
+ ? { manifest: manifest.version, files }
177
+ : null;
178
+ }
179
+ /**
180
+ * Compare the files of a directory with the manifest and log the result.
181
+ *
182
+ * @param input Directory, manifest and options.
183
+ * @param input.target
184
+ * @param input.manifest
185
+ * @param input.checksumsPath
186
+ * @param input.exclude
187
+ * @param input.embedded
188
+ * @param input.logger
189
+ * @return Exit code and result.
190
+ */
191
+ async function verifyTarget(input) {
192
+ const { target, manifest, checksumsPath, exclude, embedded, logger } = input;
193
+ const { dir } = target;
194
+ const copies = await checkEmbeddedCopies({
195
+ dir,
196
+ checksumsPath,
197
+ embedded,
198
+ });
199
+ const rules = ignoreOwnSignature(ignoreOwnFile(await loadFileRules({ exclude: parseExclude(exclude) }), dir, checksumsPath), dir, checksumsPath);
200
+ copies.paths.forEach((copy) => ignoreOwnFile(rules, dir, join(dir, copy)));
201
+ const comparison = await verifyChecksums({
202
+ sourceDir: dir,
203
+ manifest,
204
+ rules,
205
+ });
206
+ const { matched, extra } = comparison;
207
+ const missing = [...comparison.missing, ...copies.missing];
208
+ const changed = [...comparison.changed, ...copies.differing];
209
+ const versionMismatch = await findVersionMismatch(dir, manifest);
210
+ const valid = comparison.valid &&
211
+ !copies.differing.length &&
212
+ !copies.missing.length &&
213
+ !versionMismatch;
214
+ changed.forEach((file) => logger.error(`Changed: ${file}`));
215
+ missing.forEach((file) => logger.error(`Missing: ${file}`));
216
+ extra.forEach((file) => logger.error(`Not in the checksums: ${file}`));
217
+ if (versionMismatch) {
218
+ logger.error(`The checksums are for version ${versionMismatch.manifest}, but the files are version ${versionMismatch.files}.`);
219
+ }
220
+ logger.info(`${matched.length} files match, ${changed.length} changed, ${missing.length} missing and ${extra.length} extra.`);
221
+ return {
222
+ exitCode: valid ? 0 : 2,
223
+ result: {
224
+ valid,
225
+ checksumsPath,
226
+ matched: matched.length,
227
+ changed,
228
+ missing,
229
+ extra,
230
+ versionMismatch,
231
+ },
232
+ };
233
+ }
7
234
  export default {
8
235
  name: 'verify',
9
236
  usage: 'verify [<source-dir>] --checksums <file> [options]',
10
- summary: 'Check the files of a plugin or theme directory against a checksums manifest.',
237
+ summary: 'Check the files of a plugin or theme directory or ZIP against a checksums manifest.',
11
238
  help: `Arguments:
12
239
  <source-dir> Plugin or theme directory (default: .), also
13
240
  accepted as --source-dir <dir>
@@ -15,46 +242,81 @@ export default {
15
242
  Options:
16
243
  --checksums <file> Checksums manifest from "wp-release checksums" or the
17
244
  WordPress.org checksums API (required)
245
+ --zip <file|url> Check the files of this ZIP, a local file or an
246
+ http(s) URL such as the ZIP that WordPress.org
247
+ builds for a release, instead of <source-dir>
248
+ --root-folder <name> Folder in the ZIP (default: the one folder that
249
+ every entry is in)
250
+ --embedded <path> Where the manifest and its .sig are shipped inside the
251
+ files, like includes/checksums.json (default: the
252
+ name of the --checksums file at the root, when it
253
+ exists). They're skipped when comparing files and
254
+ must be identical to the --checksums file and its
255
+ .sig, and with this option they must exist
256
+ --public-key <key> Base64 Ed25519 public key. Checks <checksums>.sig, as
257
+ written by "wp-release sign", before comparing any
258
+ file, and fails when it's missing or invalid
18
259
  --exclude <patterns> Gitignore-style patterns to skip in both the directory
19
260
  and the manifest, comma-separated or repeated
20
261
 
21
262
  Reports every changed, missing and extra file, and exits with 2 unless all
22
- files match. Unlike "checksums" and "zip", verify doesn't read .distignore,
23
- so that an ignore file inside the checked directory can't hide added files.
24
- Use --exclude to skip files that aren't part of the release. A file matches when its SHA-256 equals the manifest, or its MD5
25
- for manifests without SHA-256.`,
263
+ files match. A file matches when its SHA-256 equals the manifest, or its MD5
264
+ for manifests without SHA-256. Exits with 2 too when the manifest version
265
+ differs from the Version header of the files.
266
+
267
+ Unlike "checksums" and "zip", verify doesn't read .distignore, so that an
268
+ ignore file inside the checked directory can't hide added files. Use
269
+ --exclude to skip files that aren't part of the release.
270
+
271
+ A manifest can't list itself. A copy of the manifest and of its .sig inside
272
+ the checked files is therefore skipped, but it must be identical to the
273
+ --checksums file. See --embedded for where it's looked for.`,
26
274
  options: {
27
275
  checksums: { type: 'string' },
276
+ zip: { type: 'string' },
277
+ 'root-folder': { type: 'string' },
278
+ embedded: { type: 'string' },
279
+ 'public-key': { type: 'string' },
28
280
  ...sourceDirArgs,
29
281
  exclude: { type: 'string', multiple: true },
30
282
  },
31
283
  async run({ values, positionals, cwd, logger }) {
32
- const sourceDir = resolve(cwd, getSourceDirArg(values, positionals));
284
+ const zip = getString(values, 'zip');
285
+ const sourceArg = positionals.length
286
+ ? getSourceDirArg(values, positionals)
287
+ : getString(values, 'source-dir');
33
288
  const checksums = getString(values, 'checksums');
34
289
  if (!checksums) {
35
290
  throw new ValidationError('Pass the checksums manifest with --checksums <file>.');
36
291
  }
37
- await checkDirectory(sourceDir);
292
+ if (zip && sourceArg) {
293
+ throw new ValidationError('Pass either --zip or a source directory, not both.');
294
+ }
38
295
  const checksumsPath = resolve(cwd, checksums);
296
+ const publicKey = getString(values, 'public-key');
297
+ if (publicKey) {
298
+ await checkSignature(checksumsPath, publicKey);
299
+ logger.info(`Signature of ${checksumsPath} is valid.`);
300
+ }
39
301
  const manifest = await readChecksums(checksumsPath);
40
- const rules = ignoreOwnFile(await loadFileRules({
41
- exclude: parseExclude(getStrings(values, 'exclude')),
42
- }), sourceDir, checksumsPath);
43
- const { valid, matched, changed, missing, extra } = await verifyChecksums({ sourceDir, manifest, rules });
44
- changed.forEach((file) => logger.error(`Changed: ${file}`));
45
- missing.forEach((file) => logger.error(`Missing: ${file}`));
46
- extra.forEach((file) => logger.error(`Not in the checksums: ${file}`));
47
- logger.info(`${matched.length} files match, ${changed.length} changed, ${missing.length} missing and ${extra.length} extra in ${sourceDir}.`);
48
- return {
49
- exitCode: valid ? 0 : 2,
50
- result: {
51
- valid,
302
+ const target = await resolveTarget({
303
+ sourceDir: resolve(cwd, sourceArg ?? '.'),
304
+ zip,
305
+ cwd,
306
+ rootFolder: getString(values, 'root-folder'),
307
+ });
308
+ try {
309
+ return await verifyTarget({
310
+ target,
311
+ manifest,
52
312
  checksumsPath,
53
- matched: matched.length,
54
- changed,
55
- missing,
56
- extra,
57
- },
58
- };
313
+ exclude: getStrings(values, 'exclude'),
314
+ embedded: getString(values, 'embedded'),
315
+ logger,
316
+ });
317
+ }
318
+ finally {
319
+ await target.cleanup();
320
+ }
59
321
  },
60
322
  };
@@ -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;
@@ -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>;
@@ -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>;