@wpelevator/wp-release 0.14.0 → 0.16.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 (58) hide show
  1. package/README.md +256 -36
  2. package/dist/changeset-config.d.ts +3 -0
  3. package/dist/changeset-release.d.ts +9 -3
  4. package/dist/changeset-release.js +23 -14
  5. package/dist/cli.d.ts +17 -7
  6. package/dist/cli.js +27 -2
  7. package/dist/commands/args.d.ts +11 -0
  8. package/dist/commands/args.js +23 -0
  9. package/dist/commands/artifact-signing.d.ts +20 -0
  10. package/dist/commands/artifact-signing.js +42 -0
  11. package/dist/commands/build.d.ts +32 -0
  12. package/dist/commands/build.js +91 -0
  13. package/dist/commands/changeset-add.d.ts +55 -6
  14. package/dist/commands/changeset-add.js +12 -3
  15. package/dist/commands/changeset-args.js +2 -2
  16. package/dist/commands/changeset-project-args.d.ts +63 -0
  17. package/dist/commands/changeset-project-args.js +114 -0
  18. package/dist/commands/changeset-status.d.ts +50 -1
  19. package/dist/commands/changeset-status.js +4 -2
  20. package/dist/commands/changeset-tag.d.ts +55 -6
  21. package/dist/commands/changeset-tag.js +7 -4
  22. package/dist/commands/changeset-version.d.ts +55 -6
  23. package/dist/commands/changeset-version.js +4 -2
  24. package/dist/commands/checksums.d.ts +4 -1
  25. package/dist/commands/checksums.js +27 -6
  26. package/dist/commands/command.d.ts +3 -0
  27. package/dist/commands/deploy-svn.d.ts +11 -15
  28. package/dist/commands/deploy-svn.js +25 -5
  29. package/dist/commands/package-source.d.ts +43 -0
  30. package/dist/commands/package-source.js +101 -0
  31. package/dist/commands/sign.d.ts +0 -9
  32. package/dist/commands/sign.js +10 -47
  33. package/dist/commands/verify.d.ts +7 -1
  34. package/dist/commands/verify.js +53 -17
  35. package/dist/commands/zip.d.ts +25 -2
  36. package/dist/commands/zip.js +136 -110
  37. package/dist/index.d.ts +9 -2
  38. package/dist/index.js +3 -1
  39. package/dist/integrity.d.ts +33 -0
  40. package/dist/integrity.js +115 -0
  41. package/dist/output-files.d.ts +7 -0
  42. package/dist/output-files.js +25 -0
  43. package/dist/package-build.d.ts +33 -0
  44. package/dist/package-build.js +112 -0
  45. package/dist/package-files.d.ts +25 -0
  46. package/dist/package-files.js +24 -0
  47. package/dist/package-zip.d.ts +14 -2
  48. package/dist/package-zip.js +129 -38
  49. package/dist/release-tags.js +6 -0
  50. package/dist/signing.d.ts +36 -0
  51. package/dist/signing.js +52 -1
  52. package/dist/single-project.d.ts +32 -0
  53. package/dist/single-project.js +144 -0
  54. package/dist/summary.d.ts +9 -0
  55. package/dist/summary.js +25 -0
  56. package/dist/zip.d.ts +3 -0
  57. package/dist/zip.js +16 -6
  58. package/package.json +6 -1
@@ -1,107 +1,15 @@
1
- import { basename, resolve } from 'node:path';
1
+ import { checksumSigningArgs, getArtifactSigning } from "./artifact-signing.js";
2
2
  import { ValidationError } from "../errors.js";
3
- import { resolveFileSelection } from "../file-selection.js";
3
+ import { basename, dirname, resolve, sep } from 'node:path';
4
+ import { loadChangesetConfig } from "../changeset-config.js";
5
+ import { resolveManifestCodec, checkChecksumsFile } from "../integrity.js";
6
+ import { getPackageArg, getPackageSource, getDirectorySource, } from "./package-source.js";
4
7
  import { FILENAME_FORMAT, formatFilename } from "../filename.js";
5
- import { getPackageFiles } from "../package-deploy.js";
6
- import { readPackageMeta } from "../package-meta.js";
7
- import { zipPackage } from "../package-zip.js";
8
- import { getZipSummary } from "../summary.js";
9
- import { checkDirectory, fileSelectionArgs, getFileSelectionInput, getSourceDirArg, resolveOutput, sourceDirArgs, } from "./args.js";
8
+ import { getChecksumsPath, zipPackage } from "../package-zip.js";
9
+ import { SIGNING_KEY_ENV, signFiles, getSignaturePath } from "../signing.js";
10
+ import { getSignSummary, getZipSummary } from "../summary.js";
11
+ import { fileSelectionArgs, parseSignatureFormats, resolveOutput, sourceDirArgs, } from "./args.js";
10
12
  import { getBoolean, getString, getStrings } from "./command.js";
11
- /** Options of a source directory and its files, which --package takes from the config. */
12
- const SOURCE_FLAGS = [
13
- 'source-dir',
14
- 'distignore',
15
- 'no-distignore',
16
- 'exclude',
17
- 'exclude-gitattributes',
18
- ];
19
- /**
20
- * Get the configured package to zip, failing for the options that set the
21
- * source directory and the files instead.
22
- *
23
- * @param values Parsed CLI values.
24
- * @param positionals Positional arguments.
25
- * @return The package identifier or path, or undefined without --package.
26
- * @throws {ValidationError} For an empty --package, or --package with a source directory or file selection option.
27
- */
28
- function getPackageArg(values, positionals) {
29
- const name = getString(values, 'package')?.trim();
30
- const conflicts = SOURCE_FLAGS.filter((flag) => undefined !== getString(values, flag) ||
31
- getBoolean(values, flag) ||
32
- getStrings(values, flag).length);
33
- if ('' === name) {
34
- throw new ValidationError('--package <name> must not be empty.');
35
- }
36
- if (name && (positionals.length || conflicts.length)) {
37
- throw new ValidationError(`--package takes the directory and files of the package from the config, so it can't be combined with ${positionals.length
38
- ? 'a source directory'
39
- : conflicts.map((flag) => `--${flag}`).join(', ')}.`);
40
- }
41
- return name;
42
- }
43
- /**
44
- * Describe the files of a directory with the slug and the version from the
45
- * options, or from the plugin or theme headers.
46
- *
47
- * @param values Parsed CLI values.
48
- * @param sourceDir Directory to zip.
49
- * @param selection File selection.
50
- * @param slug Default slug.
51
- * @return Source directory, file selection, slug, version and type.
52
- */
53
- async function describeSource(values, sourceDir, selection, slug) {
54
- const name = getString(values, 'slug') ?? slug;
55
- const meta = await readPackageMeta({ dir: sourceDir, slug: name });
56
- return {
57
- sourceDir,
58
- selection,
59
- slug: name,
60
- version: getString(values, 'version') ?? meta?.version ?? null,
61
- ...(meta ? { type: meta.type } : {}),
62
- };
63
- }
64
- /**
65
- * Get the files of a source directory from the CLI options.
66
- *
67
- * @param values Parsed CLI values.
68
- * @param positionals Positional arguments.
69
- * @param cwd Working directory.
70
- * @return Source directory, file selection, slug and version.
71
- */
72
- async function getDirectorySource(values, positionals, cwd) {
73
- const sourceDir = resolve(cwd, getSourceDirArg(values, positionals));
74
- await checkDirectory(sourceDir);
75
- return describeSource(values, sourceDir, resolveFileSelection({
76
- cwd,
77
- sourceDir,
78
- ...getFileSelectionInput(values),
79
- }), basename(cwd));
80
- }
81
- /**
82
- * Get the directory of a configured package with the files config of its
83
- * package group, which its deploys use too.
84
- *
85
- * @param values Parsed CLI values.
86
- * @param cwd Repository root with the wp-release config.
87
- * @param name Package identifier or path under the configured root.
88
- * @param configFile Config file, instead of the conventional one.
89
- * @return Source directory, file selection, slug, version and package.
90
- * @throws {ValidationError} Without a config or for an unknown package.
91
- */
92
- async function getPackageSource(values, cwd, name, configFile) {
93
- const pkg = await getPackageFiles({
94
- cwd,
95
- package: name,
96
- ...(configFile ? { configFile } : {}),
97
- });
98
- const source = await describeSource(values, pkg.directory, resolveFileSelection({
99
- cwd,
100
- sourceDir: pkg.directory,
101
- ...pkg.files,
102
- }), basename(pkg.directory));
103
- return { ...source, package: pkg.name };
104
- }
105
13
  export default {
106
14
  name: 'zip',
107
15
  usage: 'zip [<source-dir>] [options] | zip --package <name> [options]',
@@ -134,10 +42,24 @@ Options:
134
42
  --checksums Also write the checksums manifest of the archived
135
43
  files next to the ZIP, with .checksums.json
136
44
  instead of .zip
45
+ --checksums-file <file> Relative manifest filename; implies checksums.
46
+ Beside the ZIP, or inside its package root with
47
+ --embed-checksums. No absolute paths or dot segments.
48
+ --checksums-format <format> wordpress-json (default) or sha256sum
49
+ --checksums-algorithms <names> md5,sha256 (default), comma-separated or repeated
50
+ --embed-checksums Embed manifest instead of writing it beside ZIP
51
+ --no-embed-checksums Override embedding configured in integrity
52
+ --sign-checksums Sign the manifest only, before embedding it
53
+ --no-sign-checksums Override configured manifest signing
54
+ --sign Sign only the ZIP with the ${SIGNING_KEY_ENV} key
55
+ --sign-format <names> Signature formats of --sign, comma-separated or
56
+ repeated, like wordpress,minisign,ssh, as the
57
+ --format of "wp-release sign" (default:
58
+ wordpress); requires --sign or --sign-checksums
137
59
  --force Overwrite existing output files
138
60
 
139
- Entries are sorted and use a fixed timestamp and file mode, so the same files
140
- always give the same ZIP.
61
+ Entries are sorted and use a fixed timestamp and file mode. Embedded minisign
62
+ signatures include their signing time; other built-in metadata is deterministic.
141
63
 
142
64
  Examples:
143
65
  wp-release zip dist --slug my-plugin --checksums
@@ -154,7 +76,14 @@ Examples:
154
76
  ...sourceDirArgs,
155
77
  ...fileSelectionArgs,
156
78
  package: { type: 'string' },
157
- checksums: { type: 'boolean' },
79
+ ...checksumSigningArgs,
80
+ 'checksums-file': { type: 'string' },
81
+ 'checksums-format': { type: 'string' },
82
+ 'checksums-algorithms': { type: 'string', multiple: true },
83
+ 'embed-checksums': { type: 'boolean' },
84
+ 'no-embed-checksums': { type: 'boolean' },
85
+ 'no-sign-checksums': { type: 'boolean' },
86
+ sign: { type: 'boolean' },
158
87
  force: { type: 'boolean' },
159
88
  },
160
89
  async run({ values, positionals, cwd, configFile, logger, }) {
@@ -163,21 +92,89 @@ Examples:
163
92
  ? await getPackageSource(values, cwd, name, configFile)
164
93
  : await getDirectorySource(values, positionals, cwd);
165
94
  const filename = formatFilename(getString(values, 'filename-format') ?? FILENAME_FORMAT, { slug: source.slug, version: source.version });
95
+ const zipPath = resolveOutput({
96
+ cwd,
97
+ output: getString(values, 'output'),
98
+ filename: `${filename}.zip`,
99
+ });
100
+ const loaded = await loadChangesetConfig({ cwd, configFile });
101
+ const configured = loaded?.config.integrity;
102
+ const algorithms = getStrings(values, 'checksums-algorithms').flatMap((value) => value.split(','));
103
+ const integrity = {
104
+ ...configured,
105
+ codec: getString(values, 'checksums-format') ?? configured?.codec,
106
+ ...(algorithms.length
107
+ ? { algorithms: algorithms }
108
+ : {}),
109
+ };
110
+ const codec = resolveManifestCodec(integrity.codec);
111
+ const embed = !getBoolean(values, 'no-embed-checksums') &&
112
+ (getBoolean(values, 'embed-checksums') ||
113
+ Boolean(configured?.embed));
114
+ const checksumsFile = checkChecksumsFile(getString(values, 'checksums-file') ??
115
+ configured?.file ??
116
+ (codec.id === 'wordpress-json'
117
+ ? basename(getChecksumsPath(zipPath))
118
+ : codec.filename));
119
+ const manifestSign = !getBoolean(values, 'no-sign-checksums') &&
120
+ (getBoolean(values, 'sign-checksums') ||
121
+ Boolean(configured?.sign));
122
+ const checksums = getBoolean(values, 'checksums') ||
123
+ embed ||
124
+ manifestSign ||
125
+ Boolean(getString(values, 'checksums-file') ?? configured?.file);
126
+ const signFormats = getStrings(values, 'sign-format');
127
+ const configuredFormats = configured?.sign
128
+ ? configured.sign.formats ?? []
129
+ : [];
130
+ const formats = parseSignatureFormats(signFormats.length ? signFormats : configuredFormats);
131
+ if (signFormats.length &&
132
+ !manifestSign &&
133
+ !getBoolean(values, 'sign')) {
134
+ throw new ValidationError('--sign-format requires --sign or --sign-checksums; it does not enable signing.');
135
+ }
136
+ const signing = getBoolean(values, 'sign')
137
+ ? getArtifactSigning({
138
+ ...values,
139
+ 'sign-checksums': false,
140
+ 'sign-format': formats,
141
+ }, {
142
+ artifact: zipPath,
143
+ checksums: resolve(dirname(zipPath), checksumsFile),
144
+ })
145
+ : undefined;
146
+ const externalManifest = checksums && !embed
147
+ ? resolve(dirname(zipPath), checksumsFile)
148
+ : undefined;
149
+ const artifacts = [
150
+ zipPath,
151
+ ...(externalManifest ? [externalManifest] : []),
152
+ ...(signing
153
+ ? formats.map((format) => getSignaturePath(zipPath, format))
154
+ : []),
155
+ ...(externalManifest && manifestSign
156
+ ? formats.map((format) => getSignaturePath(externalManifest, format))
157
+ : []),
158
+ ];
159
+ if (new Set(artifacts).size !== artifacts.length ||
160
+ artifacts.some((path, index) => artifacts.some((other, otherIndex) => index !== otherIndex && other.startsWith(path + sep)))) {
161
+ throw new ValidationError('ZIP, manifest and signature destinations must be distinct.');
162
+ }
166
163
  const zipped = await zipPackage({
167
164
  sourceDir: source.sourceDir,
168
165
  selection: source.selection,
169
- zipPath: resolveOutput({
170
- cwd,
171
- output: getString(values, 'output'),
172
- filename: `${filename}.zip`,
173
- }),
166
+ zipPath,
174
167
  rootFolder: getBoolean(values, 'no-root-folder')
175
168
  ? null
176
169
  : getString(values, 'root-folder') ?? source.slug,
177
170
  slug: source.slug,
178
171
  version: source.version,
179
172
  ...(source.type ? { type: source.type } : {}),
180
- checksums: getBoolean(values, 'checksums'),
173
+ checksums,
174
+ checksumsFile: checksums ? checksumsFile : undefined,
175
+ embedChecksums: embed,
176
+ integrity,
177
+ signChecksums: manifestSign ? formats : undefined,
181
178
  force: getBoolean(values, 'force'),
182
179
  });
183
180
  logger.debug(`Archived ${zipped.files} files from ${source.sourceDir}.`);
@@ -185,13 +182,42 @@ Examples:
185
182
  if (zipped.checksumsPath) {
186
183
  logger.info(zipped.checksumsPath);
187
184
  }
185
+ const signed = signing
186
+ ? { ...(await signFiles(signing)), formats: signing.formats }
187
+ : undefined;
188
+ signed?.signatures.forEach((path) => logger.info(path));
188
189
  const result = {
189
190
  ...zipped,
191
+ ...(zipped.checksumsSigning
192
+ ? {
193
+ signatures: zipped.checksumsSignatures,
194
+ publicKey: zipped.checksumsSigning.publicKey,
195
+ }
196
+ : {}),
197
+ ...(signed
198
+ ? {
199
+ signatures: [
200
+ ...signed.signatures,
201
+ ...(zipped.checksumsSignatures ?? []),
202
+ ],
203
+ publicKey: signed.publicKey,
204
+ }
205
+ : {}),
190
206
  slug: source.slug,
191
207
  version: source.version,
192
208
  ...(source.package ? { package: source.package } : {}),
193
209
  };
194
210
  logger.summary((context) => getZipSummary(context, result));
211
+ const checksumsSigning = zipped.checksumsSigning;
212
+ if (checksumsSigning) {
213
+ logger.summary((context) => getSignSummary(context, {
214
+ ...checksumsSigning,
215
+ formats,
216
+ }));
217
+ }
218
+ if (signed) {
219
+ logger.summary((context) => getSignSummary(context, signed));
220
+ }
195
221
  return { exitCode: 0, result };
196
222
  },
197
223
  };
package/dist/index.d.ts CHANGED
@@ -9,7 +9,7 @@ export { DISTIGNORE_FILENAME, parseExclude, resolveFileSelection, loadFileRules,
9
9
  export { FILENAME_FORMAT, formatFilename } from './filename.ts';
10
10
  export { CHECKSUMS_EXTENSION, CHECKSUM_ALGORITHMS, hashFile, hashFiles, createChecksums, writeChecksums, readChecksums, parseChecksums, compareChecksums, verifyChecksums, } from './checksums.ts';
11
11
  export { HEADER_BYTES, PLUGIN_HEADERS, THEME_HEADERS, README_HEADERS, README_FILENAMES, parseFileHeaders, parseReadmeHeaders, readFileStart, findFileHeaderLine, findReadmeHeaderLine, findMainPluginFile, readPackageMeta, } from './package-meta.ts';
12
- export { DEFAULT_SIGNATURE_FORMAT, SIGNATURE_EXTENSION, SIGNATURE_FORMATS, SIGNING_KEY_ENV, parseSigningKey, parsePublicKey, generateSigningKeyPair, getPublicKey, getPublicKeys, getFileDigest, getSignaturePath, isSignatureFormat, signFile, writeSignature, verifyFileSignature, } from './signing.ts';
12
+ export { DEFAULT_SIGNATURE_FORMAT, SIGNATURE_EXTENSION, SIGNATURE_FORMATS, SIGNING_KEY_ENV, parseSigningKey, parsePublicKey, generateSigningKeyPair, getPublicKey, getPublicKeys, getFileDigest, getSignaturePath, isSignatureFormat, signFile, writeSignature, signFiles, verifyFileSignature, } from './signing.ts';
13
13
  export type { SignatureFormat, SignatureFormatHandler } from './signing.ts';
14
14
  export { MINISIGN_EXTENSION, createMinisignSignature, getMinisignPublicKey, parseMinisignPublicKey, verifyMinisignSignature, } from './minisign.ts';
15
15
  export { SSH_SIGNATURE_EXTENSION, SSH_SIGNATURE_NAMESPACE, createSshSignature, getSshPublicKey, parseSshPublicKey, verifySshSignature, } from './ssh-signature.ts';
@@ -34,10 +34,11 @@ export type { PackageDeployOutcome } from './commands/package-deploys.ts';
34
34
  export type { CliIo, CommandResults } from './cli.ts';
35
35
  export type { ChecksumsResult } from './commands/checksums.ts';
36
36
  export type { SignResult } from './commands/sign.ts';
37
+ export type { SignFilesInput, SignFilesResult } from './signing.ts';
37
38
  export type { KeygenResult } from './commands/keygen.ts';
38
39
  export type { ZipResult } from './commands/zip.ts';
39
40
  export type { Command, CommandContext, CommandOutcome, ParsedValues, } from './commands/command.ts';
40
- export type { DeployArgsInput } from './commands/deploy-svn.ts';
41
+ export type { DeployArgsInput, DeploySvnResult, } from './commands/deploy-svn.ts';
41
42
  export type { Annotation, LogEntry, LogLevel, LogWriter, LoggerOptions, OutputStream, SummaryBuilder, TextWriterOptions, } from './logger.ts';
42
43
  export type { GitHubActionsWriterOptions } from './github-actions-writer.ts';
43
44
  export type { Problem } from './errors.ts';
@@ -58,3 +59,9 @@ export type { SummaryContext } from './summary.ts';
58
59
  export type { CheckoutPath, DeployResult } from './svn-deploy.ts';
59
60
  export type { GitCommandInput, GitCommandOutput, GitRunner, RepositoryLocation, } from './git.ts';
60
61
  export type { GitDeployInput, GitDeployOptions, GitDeployResult, } from './git-deploy.ts';
62
+ export type { SingleProjectOptions } from './single-project.ts';
63
+ export { buildPackage } from './package-build.ts';
64
+ export type { BuildPackageInput, BuildPackageResult } from './package-build.ts';
65
+ export type { BuildResult } from './commands/build.ts';
66
+ export { wordpressCodec, sha256sumCodec, resolveManifestCodec, readIntegrity, } from './integrity.ts';
67
+ export type { ManifestCodec, IntegrityConfig } from './integrity.ts';
package/dist/index.js CHANGED
@@ -9,7 +9,7 @@ export { DISTIGNORE_FILENAME, parseExclude, resolveFileSelection, loadFileRules,
9
9
  export { FILENAME_FORMAT, formatFilename } from "./filename.js";
10
10
  export { CHECKSUMS_EXTENSION, CHECKSUM_ALGORITHMS, hashFile, hashFiles, createChecksums, writeChecksums, readChecksums, parseChecksums, compareChecksums, verifyChecksums, } from "./checksums.js";
11
11
  export { HEADER_BYTES, PLUGIN_HEADERS, THEME_HEADERS, README_HEADERS, README_FILENAMES, parseFileHeaders, parseReadmeHeaders, readFileStart, findFileHeaderLine, findReadmeHeaderLine, findMainPluginFile, readPackageMeta, } from "./package-meta.js";
12
- export { DEFAULT_SIGNATURE_FORMAT, SIGNATURE_EXTENSION, SIGNATURE_FORMATS, SIGNING_KEY_ENV, parseSigningKey, parsePublicKey, generateSigningKeyPair, getPublicKey, getPublicKeys, getFileDigest, getSignaturePath, isSignatureFormat, signFile, writeSignature, verifyFileSignature, } from "./signing.js";
12
+ export { DEFAULT_SIGNATURE_FORMAT, SIGNATURE_EXTENSION, SIGNATURE_FORMATS, SIGNING_KEY_ENV, parseSigningKey, parsePublicKey, generateSigningKeyPair, getPublicKey, getPublicKeys, getFileDigest, getSignaturePath, isSignatureFormat, signFile, writeSignature, signFiles, verifyFileSignature, } from "./signing.js";
13
13
  export { MINISIGN_EXTENSION, createMinisignSignature, getMinisignPublicKey, parseMinisignPublicKey, verifyMinisignSignature, } from "./minisign.js";
14
14
  export { SSH_SIGNATURE_EXTENSION, SSH_SIGNATURE_NAMESPACE, createSshSignature, getSshPublicKey, parseSshPublicKey, verifySshSignature, } from "./ssh-signature.js";
15
15
  export { ZIP_MTIME, ZIP_FILE_MODE, createZip } from "./zip.js";
@@ -27,3 +27,5 @@ export { GitDeploy, getDefaultToken, getGitAuthEnv, isValidRefName, resolveGitDe
27
27
  export { getGitDeployInputFromArgs } from "./commands/deploy-git.js";
28
28
  export { getDeployPackages, getGitDeploys, getPackageFiles, getSvnDeploys, } from "./package-deploy.js";
29
29
  export { zipPackage } from "./package-zip.js";
30
+ export { buildPackage } from "./package-build.js";
31
+ export { wordpressCodec, sha256sumCodec, resolveManifestCodec, readIntegrity, } from "./integrity.js";
@@ -0,0 +1,33 @@
1
+ import type { ChecksumsManifest, ChecksumAlgorithm } from './checksums.ts';
2
+ import type { SignatureFormat } from './signing.ts';
3
+ /** Custom codecs receive the validated WordPress-compatible integrity model. */
4
+ export interface ManifestCodec {
5
+ id: string;
6
+ filename: string;
7
+ encode: (manifest: ChecksumsManifest) => Uint8Array | Promise<Uint8Array>;
8
+ decode: (bytes: Uint8Array) => ChecksumsManifest | Promise<ChecksumsManifest>;
9
+ }
10
+ export interface IntegrityConfig {
11
+ codec?: 'wordpress-json' | 'sha256sum' | ManifestCodec;
12
+ algorithms?: ChecksumAlgorithm[];
13
+ file?: string;
14
+ embed?: boolean;
15
+ sign?: false | {
16
+ formats?: SignatureFormat[];
17
+ };
18
+ }
19
+ export declare const wordpressCodec: ManifestCodec;
20
+ export declare const sha256sumCodec: ManifestCodec;
21
+ export declare function resolveManifestCodec(value?: IntegrityConfig['codec']): ManifestCodec;
22
+ /**
23
+ * Relative artifact paths cannot escape their destination or use Windows paths.
24
+ * @param file
25
+ */
26
+ export declare function checkChecksumsFile(file: string): string;
27
+ export declare function readIntegrity(path: string, codec?: IntegrityConfig['codec']): Promise<ChecksumsManifest>;
28
+ /**
29
+ * Encode once and ensure the format preserves the selected payload hashes.
30
+ * @param codec
31
+ * @param manifest
32
+ */
33
+ export declare function encodeIntegrity(codec: ManifestCodec, manifest: ChecksumsManifest): Promise<Buffer>;
@@ -0,0 +1,115 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { basename } from 'node:path';
3
+ import { parseChecksums, readChecksums } from "./checksums.js";
4
+ import { ValidationError } from "./errors.js";
5
+ export const wordpressCodec = {
6
+ id: 'wordpress-json',
7
+ filename: 'checksums.json',
8
+ encode: (manifest) => Buffer.from(JSON.stringify(manifest, null, '\t') + '\n'),
9
+ decode: (bytes) => parseChecksums(Buffer.from(bytes).toString('utf8')),
10
+ };
11
+ export const sha256sumCodec = {
12
+ id: 'sha256sum',
13
+ filename: 'SHA256SUMS',
14
+ encode(manifest) {
15
+ const lines = Object.entries(manifest.files)
16
+ .sort()
17
+ .map(([path, hashes]) => {
18
+ if ('string' !== typeof hashes.sha256) {
19
+ throw new ValidationError('sha256sum requires one SHA-256 digest per file.');
20
+ }
21
+ const escaped = /[\\\n\r]/.test(path);
22
+ const name = path
23
+ .replaceAll('\\', '\\\\')
24
+ .replaceAll('\n', '\\n')
25
+ .replaceAll('\r', '\\r');
26
+ return `${escaped ? '\\' : ''}${hashes.sha256} ${name}`;
27
+ });
28
+ return Buffer.from(lines.join('\n') + '\n');
29
+ },
30
+ decode(bytes) {
31
+ const files = Object.create(null);
32
+ for (const line of Buffer.from(bytes)
33
+ .toString('utf8')
34
+ .split('\n')
35
+ .filter(Boolean)) {
36
+ const match = /^(\\?)([a-fA-F0-9]{64}) [ *](.+)$/.exec(line);
37
+ if (!match) {
38
+ throw new ValidationError('Invalid SHA256SUMS entry.');
39
+ }
40
+ const path = match[1]
41
+ ? (match[3] ?? '').replace(/\\([\\nr])/g, (_, char) => ({ n: '\n', r: '\r', '\\': '\\' })[char] ?? '')
42
+ : match[3] ?? '';
43
+ if (Object.hasOwn(files, path)) {
44
+ throw new ValidationError(`Duplicate checksum path: ${path}`);
45
+ }
46
+ files[path] = { sha256: (match[2] ?? '').toLowerCase() };
47
+ }
48
+ return parseChecksums(JSON.stringify({ files }));
49
+ },
50
+ };
51
+ export function resolveManifestCodec(value = 'wordpress-json') {
52
+ if (typeof value === 'string') {
53
+ if (!['wordpress-json', 'sha256sum'].includes(value)) {
54
+ throw new ValidationError(`Unknown checksums format: ${value}.`);
55
+ }
56
+ return value === 'sha256sum' ? sha256sumCodec : wordpressCodec;
57
+ }
58
+ if (!value ||
59
+ typeof value.encode !== 'function' ||
60
+ typeof value.decode !== 'function' ||
61
+ !value.id ||
62
+ !value.filename) {
63
+ throw new ValidationError('A checksums codec needs id, filename, encode and decode.');
64
+ }
65
+ return value;
66
+ }
67
+ /**
68
+ * Relative artifact paths cannot escape their destination or use Windows paths.
69
+ * @param file
70
+ */
71
+ export function checkChecksumsFile(file) {
72
+ if (!file ||
73
+ /[\\\x00-\x1f:]/.test(file) ||
74
+ file
75
+ .split('/')
76
+ .some((part) => !part || part === '.' || part === '..')) {
77
+ throw new ValidationError('--checksums-file must be a relative file path with forward slashes and no dot segments.');
78
+ }
79
+ return file;
80
+ }
81
+ export async function readIntegrity(path, codec) {
82
+ const selected = resolveManifestCodec(codec ??
83
+ (basename(path) === 'SHA256SUMS'
84
+ ? 'sha256sum'
85
+ : 'wordpress-json'));
86
+ const manifest = selected === wordpressCodec
87
+ ? await readChecksums(path)
88
+ : await selected.decode(await readFile(path));
89
+ return parseChecksums(JSON.stringify(manifest), path);
90
+ }
91
+ /**
92
+ * Encode once and ensure the format preserves the selected payload hashes.
93
+ * @param codec
94
+ * @param manifest
95
+ */
96
+ export async function encodeIntegrity(codec, manifest) {
97
+ const bytes = Buffer.from(await codec.encode(manifest));
98
+ const decoded = parseChecksums(JSON.stringify(await codec.decode(bytes)));
99
+ const expected = Object.keys(manifest.files);
100
+ const actual = Object.keys(decoded.files);
101
+ const preserved = expected.length === actual.length &&
102
+ actual.every((path) => {
103
+ const hashes = decoded.files[path];
104
+ return (Boolean(manifest.files[path]) &&
105
+ Object.entries(hashes ?? {}).every(([algorithm, digest]) => digest ===
106
+ manifest.files[path]?.[algorithm]));
107
+ });
108
+ if (!preserved ||
109
+ ['plugin', 'theme', 'version'].some((key) => decoded[key] !== undefined &&
110
+ decoded[key] !==
111
+ manifest[key])) {
112
+ throw new ValidationError('The checksums codec must preserve payload paths, digests and any represented package identity.');
113
+ }
114
+ return bytes;
115
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Validate generated file destinations before writing release artifacts.
3
+ *
4
+ * @param paths Output file paths.
5
+ * @param force Whether existing regular files can be overwritten.
6
+ */
7
+ export declare function checkOutputFiles(paths: string[], force: boolean): void;
@@ -0,0 +1,25 @@
1
+ import { lstatSync } from 'node:fs';
2
+ import { ValidationError } from "./errors.js";
3
+ import { isErrorCode } from "./file-selection.js";
4
+ /**
5
+ * Validate generated file destinations before writing release artifacts.
6
+ *
7
+ * @param paths Output file paths.
8
+ * @param force Whether existing regular files can be overwritten.
9
+ */
10
+ export function checkOutputFiles(paths, force) {
11
+ for (const path of paths) {
12
+ let existing;
13
+ try {
14
+ existing = lstatSync(path);
15
+ }
16
+ catch (error) {
17
+ if (!isErrorCode(error, 'ENOENT')) {
18
+ throw error;
19
+ }
20
+ }
21
+ if (existing && (!force || !existing.isFile())) {
22
+ throw new ValidationError(`${path} already exists, pass --force to overwrite a regular file.`);
23
+ }
24
+ }
25
+ }
@@ -0,0 +1,33 @@
1
+ import type { PackageType } from './checksums.ts';
2
+ import type { FileSelection } from './file-selection.ts';
3
+ export interface BuildPackageInput {
4
+ sourceDir: string;
5
+ selection: FileSelection;
6
+ outputDir: string;
7
+ force?: boolean;
8
+ /** Write a checksums manifest alongside the build directory. */
9
+ checksums?: boolean;
10
+ /** Slug and version for the manifest. */
11
+ slug?: string;
12
+ version?: string | null;
13
+ type?: PackageType;
14
+ }
15
+ export interface BuildPackageResult {
16
+ outputDir: string;
17
+ files: number;
18
+ checksumsPath?: string;
19
+ }
20
+ /**
21
+ * Get the manifest path alongside a build directory.
22
+ *
23
+ * @param outputDir Build directory path.
24
+ * @return Absolute manifest path.
25
+ */
26
+ export declare function getBuildChecksumsPath(outputDir: string): string;
27
+ /**
28
+ * Copy the release selection into a directory, replacing an existing build only with force.
29
+ *
30
+ * @param input Source selection, destination and overwrite option.
31
+ * @return Destination and number of copied files.
32
+ */
33
+ export declare function buildPackage(input: BuildPackageInput): Promise<BuildPackageResult>;