@vormio/plugin-sdk 0.0.0-stage → 0.0.5

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.
@@ -0,0 +1,240 @@
1
+ import { createHash, createPrivateKey, createPublicKey, generateKeyPairSync, sign, verify, } from 'node:crypto';
2
+ import { z } from 'zod';
3
+ import { parseManifest, pluginApiVersion, pluginIdSchema, versionSchema, } from "../manifest.js";
4
+ import { packageLimits } from "../package-rules.js";
5
+ /** Limits for a signed plugin catalog. */
6
+ export const catalogLimits = {
7
+ maxCatalogBytes: 1024 * 1024,
8
+ maxPlugins: 256,
9
+ maxReleasesPerPlugin: 8,
10
+ };
11
+ export class CatalogError extends Error {
12
+ code;
13
+ problems;
14
+ constructor(code, problems) {
15
+ super(problems.join('\n'));
16
+ this.code = code;
17
+ this.problems = problems;
18
+ this.name = 'CatalogError';
19
+ }
20
+ }
21
+ /**
22
+ * The manifest fields every host reads, whatever the release's apiVersion. The rest of the manifest
23
+ * is kept as is: a host parses it fully only for releases of its own apiVersion (`releaseManifest`).
24
+ */
25
+ const releaseManifestSchema = z.looseObject({
26
+ apiVersion: z.number().int().min(1),
27
+ id: pluginIdSchema,
28
+ version: versionSchema,
29
+ name: z.string().trim().min(1).max(80),
30
+ description: z.string().max(400).optional(),
31
+ });
32
+ const releaseSchema = z.strictObject({
33
+ /** The package's `manifest.json`. */
34
+ manifest: releaseManifestSchema,
35
+ /** The package file, relative to the catalog: always directly inside `packages/`. */
36
+ file: z
37
+ .string()
38
+ .regex(/^packages\/[A-Za-z0-9][A-Za-z0-9._-]{0,199}\.vormioplugin$/, 'Use packages/<name>.vormioplugin'),
39
+ bytes: z.number().int().min(1).max(packageLimits.maxArchiveBytes),
40
+ sha256: z.string().regex(/^[0-9a-f]{64}$/, 'Use lower-case hexadecimal'),
41
+ });
42
+ const payloadSchema = z
43
+ .strictObject({
44
+ /** Grows with every publication; hosts refuse a catalog older than one they have seen. */
45
+ sequence: z.number().int().min(1).max(Number.MAX_SAFE_INTEGER),
46
+ publishedAt: z.iso.datetime(),
47
+ plugins: z
48
+ .array(z.strictObject({
49
+ id: pluginIdSchema,
50
+ releases: z.array(releaseSchema).min(1).max(catalogLimits.maxReleasesPerPlugin),
51
+ }))
52
+ .max(catalogLimits.maxPlugins),
53
+ })
54
+ .superRefine((payload, context) => {
55
+ const ids = new Set();
56
+ const files = new Set();
57
+ payload.plugins.forEach((plugin, index) => {
58
+ const issue = (path, message) => context.addIssue({ code: 'custom', path: ['plugins', index, ...path], message });
59
+ if (ids.has(plugin.id))
60
+ issue(['id'], 'Plugin IDs must not repeat');
61
+ ids.add(plugin.id);
62
+ const versions = new Set();
63
+ plugin.releases.forEach((release, at) => {
64
+ if (release.manifest.id !== plugin.id)
65
+ issue(['releases', at, 'manifest', 'id'], 'The manifest ID must match the plugin ID');
66
+ if (versions.has(release.manifest.version))
67
+ issue(['releases', at, 'manifest', 'version'], 'Versions must not repeat');
68
+ versions.add(release.manifest.version);
69
+ const file = release.file.toLowerCase();
70
+ if (files.has(file))
71
+ issue(['releases', at, 'file'], 'Files must not repeat');
72
+ files.add(file);
73
+ });
74
+ });
75
+ });
76
+ const envelopeSchema = z.strictObject({
77
+ format: z.literal(1),
78
+ keyId: z.string().regex(/^[0-9a-f]{16}$/),
79
+ /** The payload's UTF-8 JSON, base64; the signature covers exactly these bytes. */
80
+ payload: z.string().min(1),
81
+ signature: z.string().min(1),
82
+ });
83
+ function problemsOf(error) {
84
+ return error.issues.map((issue) => `${issue.path.length ? issue.path.join('.') : 'catalog'}: ${issue.message}`);
85
+ }
86
+ /** Decodes canonical base64 only, so that one envelope has exactly one meaning. */
87
+ function decodeBase64(text) {
88
+ const bytes = Buffer.from(text, 'base64');
89
+ return bytes.length > 0 && bytes.toString('base64') === text ? bytes : undefined;
90
+ }
91
+ const utf8 = new TextDecoder('utf-8', { fatal: true });
92
+ function parseJson(bytes, what) {
93
+ try {
94
+ return JSON.parse(utf8.decode(bytes));
95
+ }
96
+ catch {
97
+ throw new CatalogError('format', [`${what} is not valid UTF-8 JSON`]);
98
+ }
99
+ }
100
+ // --- Keys ------------------------------------------------------------------------------------
101
+ // A public key is written as the base64 of its 32 raw Ed25519 bytes, short enough to build into the
102
+ // app. Private keys are PKCS#8 PEM files that never leave the publisher's machine.
103
+ function rawPublicKey(publicKey) {
104
+ const bytes = decodeBase64(publicKey.trim());
105
+ if (bytes?.length !== 32)
106
+ throw new CatalogError('key', ['A public key is 32 raw Ed25519 bytes in base64']);
107
+ return bytes;
108
+ }
109
+ function publicKeyObject(publicKey) {
110
+ const x = rawPublicKey(publicKey).toString('base64url');
111
+ return createPublicKey({ key: { kty: 'OKP', crv: 'Ed25519', x }, format: 'jwk' });
112
+ }
113
+ function publicKeyText(key) {
114
+ return Buffer.from(key.export({ format: 'jwk' }).x, 'base64url').toString('base64');
115
+ }
116
+ function privateKeyObject(privateKey) {
117
+ let key;
118
+ try {
119
+ key = createPrivateKey(privateKey);
120
+ }
121
+ catch {
122
+ throw new CatalogError('key', ['The private key is not a PEM private key']);
123
+ }
124
+ if (key.asymmetricKeyType !== 'ed25519')
125
+ throw new CatalogError('key', ['The private key is not an Ed25519 key']);
126
+ return key;
127
+ }
128
+ /** The first 16 hexadecimal digits of the public key's SHA-256; catalogs name their key by it. */
129
+ export function catalogKeyId(publicKey) {
130
+ return createHash('sha256').update(rawPublicKey(publicKey)).digest('hex').slice(0, 16);
131
+ }
132
+ /** The public key that belongs to a PEM private key. */
133
+ export function catalogPublicKey(privateKey) {
134
+ return publicKeyText(createPublicKey(privateKeyObject(privateKey)));
135
+ }
136
+ export function generateCatalogKeys() {
137
+ const { publicKey, privateKey } = generateKeyPairSync('ed25519');
138
+ const text = publicKeyText(publicKey);
139
+ return {
140
+ publicKey: text,
141
+ privateKey: privateKey.export({ format: 'pem', type: 'pkcs8' }).toString(),
142
+ keyId: catalogKeyId(text),
143
+ };
144
+ }
145
+ // --- Signing and verification ----------------------------------------------------------------
146
+ /** Validates a payload and returns the signed `catalog.json` bytes. */
147
+ export function signCatalog(payload, privateKey) {
148
+ const result = payloadSchema.safeParse(payload);
149
+ if (!result.success)
150
+ throw new CatalogError('format', problemsOf(result.error));
151
+ const key = privateKeyObject(privateKey);
152
+ const bytes = Buffer.from(JSON.stringify(payload));
153
+ const envelope = {
154
+ format: 1,
155
+ keyId: catalogKeyId(publicKeyText(createPublicKey(key))),
156
+ payload: bytes.toString('base64'),
157
+ signature: sign(null, bytes, key).toString('base64'),
158
+ };
159
+ const text = Buffer.from(`${JSON.stringify(envelope, null, 2)}\n`);
160
+ if (text.length > catalogLimits.maxCatalogBytes)
161
+ throw new CatalogError('format', ['The catalog is larger than 1 MiB']);
162
+ return text;
163
+ }
164
+ /**
165
+ * Checks `catalog.json` against a public key: shape of the envelope, key ID, signature, and only
166
+ * then the payload. Throws `CatalogError` at the first check that fails.
167
+ */
168
+ export function verifyCatalog(bytes, publicKey) {
169
+ if (bytes.length > catalogLimits.maxCatalogBytes)
170
+ throw new CatalogError('format', ['The catalog is larger than 1 MiB']);
171
+ const envelope = envelopeSchema.safeParse(parseJson(bytes, 'The catalog'));
172
+ if (!envelope.success)
173
+ throw new CatalogError('format', problemsOf(envelope.error));
174
+ const keyId = catalogKeyId(publicKey);
175
+ if (envelope.data.keyId !== keyId)
176
+ throw new CatalogError('key', [
177
+ `The catalog is signed with key ${envelope.data.keyId}, not ${keyId}`,
178
+ ]);
179
+ const payload = decodeBase64(envelope.data.payload);
180
+ const signature = decodeBase64(envelope.data.signature);
181
+ if (!payload || signature?.length !== 64)
182
+ throw new CatalogError('format', ['The payload or signature is not canonical base64']);
183
+ if (!verify(null, payload, publicKeyObject(publicKey), signature))
184
+ throw new CatalogError('signature', ['The signature does not match the catalog']);
185
+ const result = payloadSchema.safeParse(parseJson(payload, 'The payload'));
186
+ if (!result.success)
187
+ throw new CatalogError('format', problemsOf(result.error));
188
+ return { keyId, ...result.data };
189
+ }
190
+ /**
191
+ * The release's manifest when this SDK's hosts can install it: same apiVersion, and valid under
192
+ * this SDK's manifest schema. Otherwise the reasons, typically that the release needs a newer host.
193
+ */
194
+ export function releaseManifest(release) {
195
+ if (release.manifest.apiVersion !== pluginApiVersion)
196
+ return {
197
+ ok: false,
198
+ problems: [
199
+ `apiVersion ${release.manifest.apiVersion}; this version supports ${pluginApiVersion}`,
200
+ ],
201
+ };
202
+ return parseManifest(release.manifest);
203
+ }
204
+ // --- Versions --------------------------------------------------------------------------------
205
+ function versionParts(version) {
206
+ const dash = version.indexOf('-');
207
+ const core = dash < 0 ? version : version.slice(0, dash);
208
+ return {
209
+ core: core.split('.').map(Number),
210
+ prerelease: dash < 0 ? [] : version.slice(dash + 1).split('.'),
211
+ };
212
+ }
213
+ /** Orders two semantic versions by precedence: negative, zero or positive, like a sort callback. */
214
+ export function compareVersions(a, b) {
215
+ const left = versionParts(a);
216
+ const right = versionParts(b);
217
+ for (let at = 0; at < 3; at++) {
218
+ const difference = (left.core[at] ?? 0) - (right.core[at] ?? 0);
219
+ if (difference)
220
+ return Math.sign(difference);
221
+ }
222
+ // A version without a pre-release ranks above any pre-release of it.
223
+ if (!left.prerelease.length || !right.prerelease.length)
224
+ return Math.sign(right.prerelease.length - left.prerelease.length);
225
+ const count = Math.min(left.prerelease.length, right.prerelease.length);
226
+ for (let at = 0; at < count; at++) {
227
+ const x = left.prerelease[at];
228
+ const y = right.prerelease[at];
229
+ if (x === y)
230
+ continue;
231
+ const xNumeric = /^\d+$/.test(x);
232
+ const yNumeric = /^\d+$/.test(y);
233
+ if (xNumeric && yNumeric)
234
+ return Math.sign(Number(x) - Number(y));
235
+ if (xNumeric !== yNumeric)
236
+ return xNumeric ? -1 : 1;
237
+ return x < y ? -1 : 1;
238
+ }
239
+ return Math.sign(left.prerelease.length - right.prerelease.length);
240
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,99 @@
1
+ #!/usr/bin/env node
2
+ import { existsSync } from 'node:fs';
3
+ import { writeFile } from 'node:fs/promises';
4
+ import { resolve } from 'node:path';
5
+ import { CatalogError } from "./catalog.js";
6
+ import { checkPackage, PackageError, readPackageDirectory } from "./package.js";
7
+ import { PublishError, writeCatalog, writeCatalogKeys } from "./publish.js";
8
+ import { writeZip, ZipError } from "./zip.js";
9
+ const usage = [
10
+ 'Usage:',
11
+ ' vormio-plugin pack <plugin directory> <output.vormioplugin>',
12
+ ' vormio-plugin keygen <output path prefix>',
13
+ ' vormio-plugin catalog <release config.json> --key <private key.pem> --out <new folder>',
14
+ ].join('\n');
15
+ /** `vormio-plugin pack`: validates a built plugin directory and writes a `.vormioplugin` file. */
16
+ async function pack(source, output) {
17
+ if (!output.endsWith('.vormioplugin')) {
18
+ console.error('The output file must end with .vormioplugin');
19
+ return 2;
20
+ }
21
+ const target = resolve(output);
22
+ if (existsSync(target)) {
23
+ console.error(`${target} already exists; choose another name or remove it first`);
24
+ return 1;
25
+ }
26
+ const contents = await readPackageDirectory(resolve(source));
27
+ const checked = checkPackage(contents);
28
+ const entries = checked.files.map((file) => ({
29
+ path: file.path,
30
+ data: contents.get(file.path),
31
+ }));
32
+ await writeFile(target, writeZip(entries), { flag: 'wx' });
33
+ console.log(`${checked.manifest.id} ${checked.manifest.version}: ${entries.length} files → ${target}`);
34
+ console.log(`digest ${checked.digest}`);
35
+ return 0;
36
+ }
37
+ /** `vormio-plugin keygen`: creates the key pair a catalog is signed with. */
38
+ async function keygen(prefix) {
39
+ const keys = await writeCatalogKeys(prefix);
40
+ console.log(`key ${keys.keyId}`);
41
+ console.log(`public key ${keys.publicKey}`);
42
+ console.log(`private key → ${keys.privateKeyFile}`);
43
+ console.log(`public key → ${keys.publicKeyFile}`);
44
+ console.log('Keep the private key offline. Never commit, upload or share it.');
45
+ return 0;
46
+ }
47
+ /** `vormio-plugin catalog`: publishes a signed catalog and its packages into a new folder. */
48
+ async function catalog(args) {
49
+ let config;
50
+ let key;
51
+ let out;
52
+ for (let at = 0; at < args.length; at++) {
53
+ const arg = args[at];
54
+ if (arg === '--key')
55
+ key = args[++at];
56
+ else if (arg === '--out')
57
+ out = args[++at];
58
+ else if (config === undefined && arg && !arg.startsWith('--'))
59
+ config = arg;
60
+ else
61
+ return usageError();
62
+ }
63
+ if (!config || !key || !out)
64
+ return usageError();
65
+ const written = await writeCatalog({ config, privateKey: key, out });
66
+ console.log(`catalog.json: sequence ${written.sequence}, key ${written.keyId}`);
67
+ for (const release of written.releases)
68
+ console.log(` ${release.id} ${release.version} → ${release.file}`);
69
+ console.log(`Upload the packages folder of ${resolve(out)} first, then catalog.json.`);
70
+ return 0;
71
+ }
72
+ function usageError() {
73
+ console.error(usage);
74
+ return 2;
75
+ }
76
+ async function main(args) {
77
+ const [command, ...rest] = args;
78
+ try {
79
+ if (command === 'pack' && rest.length === 2)
80
+ return await pack(rest[0], rest[1]);
81
+ if (command === 'keygen' && rest.length === 1)
82
+ return await keygen(rest[0]);
83
+ if (command === 'catalog')
84
+ return await catalog(rest);
85
+ return usageError();
86
+ }
87
+ catch (error) {
88
+ if (error instanceof PackageError || error instanceof ZipError) {
89
+ console.error(`The plugin is not valid:\n${error.message}`);
90
+ return 1;
91
+ }
92
+ if (error instanceof PublishError || error instanceof CatalogError) {
93
+ console.error(error.message);
94
+ return 1;
95
+ }
96
+ throw error;
97
+ }
98
+ }
99
+ process.exitCode = await main(process.argv.slice(2));
@@ -0,0 +1,8 @@
1
+ export { checkPackage, PackageError, readPackageArchive, readPackageDirectory, type CheckedPackage, } from './package.ts';
2
+ export { readZip, writeZip, ZipError, type ZipEntry } from './zip.ts';
3
+ export { CatalogError, catalogKeyId, catalogLimits, catalogPublicKey, compareVersions, generateCatalogKeys, releaseManifest, signCatalog, verifyCatalog, type Catalog, type CatalogErrorCode, type CatalogKeys, type CatalogPayload, type CatalogPlugin, type CatalogRelease, } from './catalog.ts';
4
+ export { PublishError, writeCatalog, writeCatalogKeys, type WrittenCatalog, type WrittenKeys, } from './publish.ts';
5
+ export { contributionsOf, manifestSchema, missingEntries, parseManifest, permissionSummary, pluginApiVersion, type Contribution, type ContributionKind, type PluginManifest, } from '../manifest.ts';
6
+ export { checkPackageFiles, checkPackagePath, packageLimits } from '../package-rules.ts';
7
+ export { compositionReportSchema, parseReport, reportLimits } from '../report.ts';
8
+ export type { CompositionReport } from '../bridge.ts';
@@ -0,0 +1,8 @@
1
+ export { checkPackage, PackageError, readPackageArchive, readPackageDirectory, } from "./package.js";
2
+ export { readZip, writeZip, ZipError } from "./zip.js";
3
+ export { CatalogError, catalogKeyId, catalogLimits, catalogPublicKey, compareVersions, generateCatalogKeys, releaseManifest, signCatalog, verifyCatalog, } from "./catalog.js";
4
+ export { PublishError, writeCatalog, writeCatalogKeys, } from "./publish.js";
5
+ // The manifest and package rules again, so Node code needs no DOM types from the runtime entry.
6
+ export { contributionsOf, manifestSchema, missingEntries, parseManifest, permissionSummary, pluginApiVersion, } from "../manifest.js";
7
+ export { checkPackageFiles, checkPackagePath, packageLimits } from "../package-rules.js";
8
+ export { compositionReportSchema, parseReport, reportLimits } from "../report.js";
@@ -0,0 +1,25 @@
1
+ import { type PluginManifest } from '../manifest.ts';
2
+ import { type PackageFile } from '../package-rules.ts';
3
+ export declare class PackageError extends Error {
4
+ readonly problems: readonly string[];
5
+ constructor(problems: readonly string[]);
6
+ }
7
+ /** A validated plugin: its manifest, file list and content digest. */
8
+ export interface CheckedPackage {
9
+ manifest: PluginManifest;
10
+ files: PackageFile[];
11
+ /** SHA-256 over every file's path and content, in path order. */
12
+ digest: string;
13
+ }
14
+ /** Validates file contents given as path → bytes (from a zip or a directory). */
15
+ export declare function checkPackage(contents: ReadonlyMap<string, Buffer>): CheckedPackage;
16
+ /**
17
+ * Reads a plugin directory. Links and junctions are refused rather than followed, and reading stops
18
+ * as soon as the package limits are exceeded.
19
+ */
20
+ export declare function readPackageDirectory(directory: string): Promise<Map<string, Buffer>>;
21
+ /** Reads and validates a `.vormioplugin` file. */
22
+ export declare function readPackageArchive(zip: Buffer): {
23
+ checked: CheckedPackage;
24
+ contents: Map<string, Buffer>;
25
+ };
@@ -0,0 +1,82 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { lstat, readdir, readFile } from 'node:fs/promises';
3
+ import { join } from 'node:path';
4
+ import { missingEntries, parseManifest } from "../manifest.js";
5
+ import { checkPackageFiles, packageLimits } from "../package-rules.js";
6
+ import { readZip } from "./zip.js";
7
+ export class PackageError extends Error {
8
+ problems;
9
+ constructor(problems) {
10
+ super(problems.join('\n'));
11
+ this.problems = problems;
12
+ this.name = 'PackageError';
13
+ }
14
+ }
15
+ /** Validates file contents given as path → bytes (from a zip or a directory). */
16
+ export function checkPackage(contents) {
17
+ const files = [...contents].map(([path, data]) => ({ path, size: data.length }));
18
+ const problems = checkPackageFiles(files);
19
+ const manifestBytes = contents.get('manifest.json');
20
+ if (problems.length || !manifestBytes)
21
+ throw new PackageError(problems);
22
+ let json;
23
+ try {
24
+ json = JSON.parse(manifestBytes.toString('utf8'));
25
+ }
26
+ catch {
27
+ throw new PackageError(['manifest.json: not valid JSON']);
28
+ }
29
+ const parsed = parseManifest(json);
30
+ if (!parsed.ok)
31
+ throw new PackageError(parsed.problems);
32
+ const missing = missingEntries(parsed.manifest, new Set(contents.keys()));
33
+ if (missing.length)
34
+ throw new PackageError(missing.map((entry) => `${entry}: entry not found`));
35
+ const digest = createHash('sha256');
36
+ for (const path of [...contents.keys()].sort()) {
37
+ digest.update(path).update('\0');
38
+ digest.update(createHash('sha256').update(contents.get(path)).digest()).update('\0');
39
+ }
40
+ return {
41
+ manifest: parsed.manifest,
42
+ files: files.sort((a, b) => (a.path < b.path ? -1 : 1)),
43
+ digest: digest.digest('hex'),
44
+ };
45
+ }
46
+ /**
47
+ * Reads a plugin directory. Links and junctions are refused rather than followed, and reading stops
48
+ * as soon as the package limits are exceeded.
49
+ */
50
+ export async function readPackageDirectory(directory) {
51
+ const contents = new Map();
52
+ let total = 0;
53
+ const visit = async (relative) => {
54
+ for (const name of (await readdir(join(directory, relative))).sort()) {
55
+ const path = relative ? `${relative}/${name}` : name;
56
+ const info = await lstat(join(directory, path));
57
+ if (info.isSymbolicLink())
58
+ throw new PackageError([`${path}: links are not allowed`]);
59
+ if (info.isDirectory()) {
60
+ await visit(path);
61
+ continue;
62
+ }
63
+ if (!info.isFile())
64
+ throw new PackageError([`${path}: not a regular file`]);
65
+ if (contents.size >= packageLimits.maxFiles)
66
+ throw new PackageError([`more than ${packageLimits.maxFiles} files`]);
67
+ if (info.size > packageLimits.maxFileBytes)
68
+ throw new PackageError([`${path}: file too large`]);
69
+ total += info.size;
70
+ if (total > packageLimits.maxTotalBytes)
71
+ throw new PackageError(['package too large']);
72
+ contents.set(path, await readFile(join(directory, path)));
73
+ }
74
+ };
75
+ await visit('');
76
+ return contents;
77
+ }
78
+ /** Reads and validates a `.vormioplugin` file. */
79
+ export function readPackageArchive(zip) {
80
+ const contents = new Map(readZip(zip).map((entry) => [entry.path, entry.data]));
81
+ return { checked: checkPackage(contents), contents };
82
+ }
@@ -0,0 +1,42 @@
1
+ /** A publication that cannot go ahead; nothing has been written. */
2
+ export declare class PublishError extends Error {
3
+ readonly problems: readonly string[];
4
+ constructor(problems: readonly string[]);
5
+ }
6
+ export interface WrittenKeys {
7
+ keyId: string;
8
+ publicKey: string;
9
+ privateKeyFile: string;
10
+ publicKeyFile: string;
11
+ }
12
+ /**
13
+ * Generates a catalog key pair into `<prefix>.private.pem` and `<prefix>.public.txt`. Refuses to
14
+ * write inside a Git working tree, where the private key could end up in a commit, and never
15
+ * overwrites a file.
16
+ */
17
+ export declare function writeCatalogKeys(prefix: string): Promise<WrittenKeys>;
18
+ export interface WrittenCatalog {
19
+ keyId: string;
20
+ sequence: number;
21
+ releases: {
22
+ id: string;
23
+ version: string;
24
+ file: string;
25
+ bytes: number;
26
+ sha256: string;
27
+ }[];
28
+ }
29
+ /**
30
+ * Publishes a catalog into a new, empty folder: validates every package the way a host does before
31
+ * installing it, copies each under a name derived from its content, signs `catalog.json` and checks
32
+ * the result with the public key. Every check runs before anything is written. Upload the
33
+ * `packages` folder first and `catalog.json` last.
34
+ */
35
+ export declare function writeCatalog(options: {
36
+ /** Release configuration; relative paths in it are resolved against its folder. */
37
+ config: string;
38
+ /** PKCS#8 PEM file of the catalog's private key. */
39
+ privateKey: string;
40
+ out: string;
41
+ now?: Date;
42
+ }): Promise<WrittenCatalog>;