diff --git a/src/commands/apps/bundles/create.ts b/src/commands/apps/bundles/create.ts index 50971ab..4d26a19 100644 --- a/src/commands/apps/bundles/create.ts +++ b/src/commands/apps/bundles/create.ts @@ -9,7 +9,9 @@ import appsService from '../../../services/apps'; import appBundlesService from '../../../services/app-bundles'; import { getMessageFromUnknownError } from '../../../utils/error'; import { createHash } from '../../../utils/hash'; -import { createBuffer } from '../../../utils/buffer'; +import { createBuffer, createBufferFromPath } from '../../../utils/buffer'; +import { createSignature } from '../../../utils/signature'; +import { fileExistsAtPath } from '../../../utils/file'; export default defineCommand({ meta: { @@ -44,6 +46,10 @@ export default defineCommand({ type: 'string', description: 'Path to the bundle to upload. Must be a folder (e.g. `www` or `dist`) or a zip file.', }, + privateKey: { + type: 'string', + description: 'The path to the private key file to sign the bundle with.', + }, rollout: { type: 'string', description: 'The percentage of devices to deploy the bundle to. Must be a number between 0 and 1 (e.g. 0.5).', @@ -59,7 +65,7 @@ export default defineCommand({ return; } - const { androidMax, androidMin, rollout, iosMax, iosMin } = ctx.args; + const { androidMax, androidMin, privateKey, rollout, iosMax, iosMin } = ctx.args; let appId = ctx.args.appId; let channelName = ctx.args.channel; let path = ctx.args.path; @@ -92,22 +98,41 @@ export default defineCommand({ } } } + let privateKeyBuffer; + if (privateKey) { + if (privateKey.endsWith('.pem')) { + const fileExists = await fileExistsAtPath(privateKey); + if (fileExists) { + privateKeyBuffer = await createBufferFromPath(privateKey); + } else { + consola.error('Private key file not found.'); + return; + } + } else { + consola.error('Private key must be a path to a .pem file.'); + return; + } + } // Create form data const formData = new FormData(); if (path) { + let fileBuffer; if (zip.isZipped(path)) { const readStream = createReadStream(path); - const buffer = await createBuffer(readStream); - const hash = await createHash(buffer); - formData.append('file', buffer, { filename: 'bundle.zip' }); - formData.append('checksum', hash); + fileBuffer = await createBuffer(readStream); } else { consola.start('Zipping folder...'); - const zipBuffer = await zip.zipFolder(path); - const hash = await createHash(zipBuffer); - formData.append('file', zipBuffer, { filename: 'bundle.zip' }); - formData.append('checksum', hash); + fileBuffer = await zip.zipFolder(path); + } + consola.start('Generating checksum...'); + const hash = await createHash(fileBuffer); + formData.append('file', fileBuffer, { filename: 'bundle.zip' }); + formData.append('checksum', hash); + if (privateKeyBuffer) { + consola.start('Signing bundle...'); + const signature = await createSignature(privateKeyBuffer, fileBuffer); + formData.append('signature', signature); } } if (url) { diff --git a/src/utils/buffer.ts b/src/utils/buffer.ts index b296d81..0abc1ad 100644 --- a/src/utils/buffer.ts +++ b/src/utils/buffer.ts @@ -15,3 +15,9 @@ export const createBuffer = async (data: ReadStream): Promise => { data.on('error', reject); }); }; + +export const createBufferFromPath = async (path: string): Promise => { + const fs = await import('fs'); + const stream = fs.createReadStream(path); + return createBuffer(stream); +}; diff --git a/src/utils/file.ts b/src/utils/file.ts new file mode 100644 index 0000000..4f441a0 --- /dev/null +++ b/src/utils/file.ts @@ -0,0 +1,8 @@ +export const fileExistsAtPath = async (path: string): Promise => { + const fs = await import('fs'); + return new Promise((resolve) => { + fs.access(path, fs.constants.F_OK, (err) => { + resolve(!err); + }); + }); +}; diff --git a/src/utils/signature.ts b/src/utils/signature.ts new file mode 100644 index 0000000..1184bb2 --- /dev/null +++ b/src/utils/signature.ts @@ -0,0 +1,7 @@ +export const createSignature = async (privateKey: Buffer, data: Buffer): Promise => { + const crypto = await import('crypto'); + const sign = crypto.createSign('sha256'); + sign.update(data); + sign.end(); + return sign.sign(privateKey).toString('base64'); +};