@makefully/adaptfully 4.0.0 → 4.2.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.
@@ -0,0 +1,197 @@
1
+ import crypto from 'node:crypto';
2
+ import fs from 'node:fs';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import {Readable, PassThrough} from 'node:stream';
6
+ import {pipeline} from 'node:stream/promises';
7
+ import archiver from 'archiver';
8
+ import unzipper from 'unzip-stream';
9
+ import {
10
+ WRAPFULLY_DEFAULT_KID,
11
+ WRAPFULLY_DEFAULT_PUBLIC_KEY,
12
+ } from './wrapfullyDefaultPublicKey.js';
13
+
14
+ const
15
+ ENVELOPE_DATA = 'wrapfully.enc',
16
+ ENVELOPE_KEY = 'wrapfully.key.enc',
17
+ ENVELOPE_META = 'wrapfully-crypto.json',
18
+ RESULT_DATA = 'result.enc',
19
+ RESULT_KEY = 'result.key.enc',
20
+ ALG = 'RSA-OAEP-SHA256+AES-256-GCM';
21
+
22
+ function aesEncrypt (plaintext) {
23
+ const key = crypto.randomBytes(32);
24
+ const iv = crypto.randomBytes(12);
25
+ const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
26
+ const enc = Buffer.concat([cipher.update(plaintext), cipher.final()]);
27
+ const tag = cipher.getAuthTag();
28
+
29
+ return {key, ciphertext: Buffer.concat([iv, tag, enc])};
30
+ }
31
+
32
+ function aesDecrypt (key, blob) {
33
+ const iv = blob.subarray(0, 12);
34
+ const tag = blob.subarray(12, 28);
35
+ const data = blob.subarray(28);
36
+ const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
37
+
38
+ decipher.setAuthTag(tag);
39
+
40
+ return Buffer.concat([decipher.update(data), decipher.final()]);
41
+ }
42
+
43
+ function wrapKey (aesKey, publicKeyPem) {
44
+ return crypto.publicEncrypt(
45
+ {
46
+ key: publicKeyPem,
47
+ padding: crypto.constants.RSA_PKCS1_OAEP_PADDING,
48
+ oaepHash: 'sha256',
49
+ },
50
+ aesKey,
51
+ );
52
+ }
53
+
54
+ function unwrapKey (wrapped, privateKeyPem) {
55
+ return crypto.privateDecrypt(
56
+ {
57
+ key: privateKeyPem,
58
+ padding: crypto.constants.RSA_PKCS1_OAEP_PADDING,
59
+ oaepHash: 'sha256',
60
+ },
61
+ wrapped,
62
+ );
63
+ }
64
+
65
+ export function generateResultKeyPair () {
66
+ const {publicKey, privateKey} = crypto.generateKeyPairSync('rsa', {
67
+ modulusLength: 2048,
68
+ publicKeyEncoding: {type: 'spki', format: 'pem'},
69
+ privateKeyEncoding: {type: 'pkcs8', format: 'pem'},
70
+ });
71
+
72
+ return {publicKey, privateKey};
73
+ }
74
+
75
+ export function resolveEncryptPublicKey (wrapfullyConfig = {}, cliPublicKey) {
76
+ if (cliPublicKey) {
77
+ if (cliPublicKey.includes('BEGIN PUBLIC KEY')) {
78
+ return {pem: cliPublicKey.trim(), kid: wrapfullyConfig.encryptKid || 'override'};
79
+ }
80
+ return {
81
+ pem: fs.readFileSync(cliPublicKey, 'utf8').trim(),
82
+ kid: wrapfullyConfig.encryptKid || 'override',
83
+ };
84
+ }
85
+ if (wrapfullyConfig.encryptPublicKey) {
86
+ const value = wrapfullyConfig.encryptPublicKey;
87
+
88
+ if (String(value).includes('BEGIN PUBLIC KEY')) {
89
+ return {pem: String(value).trim(), kid: wrapfullyConfig.encryptKid || 'override'};
90
+ }
91
+ return {
92
+ pem: fs.readFileSync(value, 'utf8').trim(),
93
+ kid: wrapfullyConfig.encryptKid || 'override',
94
+ };
95
+ }
96
+
97
+ return {pem: WRAPFULLY_DEFAULT_PUBLIC_KEY.trim(), kid: WRAPFULLY_DEFAULT_KID};
98
+ }
99
+
100
+ async function bufferFromStream (stream) {
101
+ const chunks = [];
102
+
103
+ for await (const chunk of stream) {
104
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
105
+ }
106
+
107
+ return Buffer.concat(chunks);
108
+ }
109
+
110
+ async function zipToBuffer (appendFn) {
111
+ const archive = archiver('zip', {zlib: {level: 0}});
112
+ const pass = new PassThrough();
113
+ const chunks = [];
114
+
115
+ archive.pipe(pass);
116
+ pass.on('data', (c) => chunks.push(c));
117
+
118
+ const done = new Promise((resolve, reject) => {
119
+ pass.on('end', resolve);
120
+ pass.on('error', reject);
121
+ archive.on('error', reject);
122
+ });
123
+
124
+ await appendFn(archive);
125
+ await archive.finalize();
126
+ await done;
127
+
128
+ return Buffer.concat(chunks);
129
+ }
130
+
131
+ /**
132
+ * Wrap an inner zip stream/buffer in a Yap-safe envelope for Wrapfully workers.
133
+ */
134
+ export async function createEnvelopeFromInnerZip (innerZip, {publicKeyPem, kid, resultKeyPair}) {
135
+ const innerBuf = Buffer.isBuffer(innerZip) ? innerZip : await bufferFromStream(innerZip);
136
+ const {key, ciphertext} = aesEncrypt(innerBuf);
137
+ const wrapped = wrapKey(key, publicKeyPem);
138
+ const meta = {
139
+ kid,
140
+ alg: ALG,
141
+ resultPub: resultKeyPair.publicKey,
142
+ };
143
+
144
+ const zip = await zipToBuffer(async (archive) => {
145
+ archive.append(ciphertext, {name: ENVELOPE_DATA});
146
+ archive.append(wrapped, {name: ENVELOPE_KEY});
147
+ archive.append(JSON.stringify(meta, null, 2), {name: ENVELOPE_META});
148
+ archive.append(resultKeyPair.publicKey, {name: 'resultPub.pem'});
149
+ });
150
+
151
+ return {zip, resultPriv: resultKeyPair.privateKey, kid};
152
+ }
153
+
154
+ /**
155
+ * Decrypt a completed Yap result envelope into a plaintext zip Buffer.
156
+ */
157
+ export async function decryptResultEnvelope (resultZipBuf, resultPrivPem) {
158
+ if (!resultPrivPem) {
159
+ throw new Error('Missing resultPriv for encrypted Wrapfully chore download.');
160
+ }
161
+
162
+ const tmp = path.join(os.tmpdir(), `adaptfully-result-${Date.now().toString(16)}`);
163
+
164
+ fs.mkdirSync(tmp, {recursive: true});
165
+ try {
166
+ await pipeline(Readable.from(resultZipBuf), unzipper.Extract({path: tmp}));
167
+ if (!fs.existsSync(path.join(tmp, RESULT_DATA))) {
168
+ // Not an envelope — return as-is
169
+ return resultZipBuf;
170
+ }
171
+ const ciphertext = fs.readFileSync(path.join(tmp, RESULT_DATA));
172
+ const wrapped = fs.readFileSync(path.join(tmp, RESULT_KEY));
173
+ const aesKey = unwrapKey(wrapped, resultPrivPem);
174
+
175
+ return aesDecrypt(aesKey, ciphertext);
176
+ } finally {
177
+ fs.rmSync(tmp, {recursive: true, force: true});
178
+ }
179
+ }
180
+
181
+ export function isResultEnvelopeBuffer (buf) {
182
+ // Weak check — PK zip; decryptResultEnvelope falls back if members missing
183
+ return Buffer.isBuffer(buf) && buf.length > 4 && buf[0] === 0x50 && buf[1] === 0x4b;
184
+ }
185
+
186
+ export {
187
+ ENVELOPE_DATA,
188
+ ENVELOPE_KEY,
189
+ ENVELOPE_META,
190
+ RESULT_DATA,
191
+ RESULT_KEY,
192
+ ALG,
193
+ aesEncrypt,
194
+ aesDecrypt,
195
+ wrapKey,
196
+ unwrapKey,
197
+ };
@@ -1,6 +1,8 @@
1
1
  import fs from 'node:fs/promises';
2
2
 
3
- const DEFAULT_SERVER = 'http://localhost:9633/';
3
+ const DEFAULT_SHOWFULLY_SERVER = process.env.SHOWFULLY_DEV
4
+ ? 'http://localhost:9630/'
5
+ : 'https://make.makefullystudios.com/';
4
6
 
5
7
  /**
6
8
  * @param {string} [projectRoot='.']
@@ -29,14 +31,53 @@ export async function loadProjectConfig(projectRoot = '.') {
29
31
  }
30
32
 
31
33
  /**
32
- * @param {{ server?: string }} wrapfullyConfig
34
+ * Showfully Yap base URL (not Wrapfully HTTP).
35
+ * @param {{ server?: string, showfullyServer?: string }} wrapfullyConfig
33
36
  * @param {string} [cliServer]
34
37
  */
35
38
  export function resolveServerUrl(wrapfullyConfig, cliServer) {
36
39
  return (
37
40
  cliServer
38
- || process.env.WRAPFULLY_SERVER
41
+ || process.env.SHOWFULLY_SERVER
42
+ || wrapfullyConfig.showfullyServer
39
43
  || wrapfullyConfig.server
40
- || DEFAULT_SERVER
44
+ || process.env.WRAPFULLY_SERVER
45
+ || DEFAULT_SHOWFULLY_SERVER
41
46
  ).replace(/\/?$/, '/');
42
47
  }
48
+
49
+ /**
50
+ * Required PAT for Yap submit/poll/download.
51
+ * @param {{ accessToken?: string, showfullyPat?: string }} wrapfullyConfig
52
+ * @param {string} [cliToken]
53
+ */
54
+ export function resolveAccessToken(wrapfullyConfig = {}, cliToken) {
55
+ const token = (
56
+ cliToken
57
+ || process.env.SHOWFULLY_PAT
58
+ || process.env.WRAPFULLY_ACCESS_TOKEN
59
+ || wrapfullyConfig.accessToken
60
+ || wrapfullyConfig.showfullyPat
61
+ || ''
62
+ ).trim();
63
+
64
+ if (!token) {
65
+ throw new Error(
66
+ 'Showfully PAT required. Set SHOWFULLY_PAT or wrapfully.json "accessToken" '
67
+ + '(Settings → API tokens on play.makefullystudios.com).',
68
+ );
69
+ }
70
+
71
+ return token;
72
+ }
73
+
74
+ /**
75
+ * @param {{ encrypt?: boolean }} wrapfullyConfig
76
+ * @param {boolean} [cliEncrypt]
77
+ */
78
+ export function resolveEncryptFlag(wrapfullyConfig = {}, cliEncrypt) {
79
+ if (typeof cliEncrypt === 'boolean') {
80
+ return cliEncrypt;
81
+ }
82
+ return !!wrapfullyConfig.encrypt;
83
+ }
@@ -1,37 +1,150 @@
1
1
  import axios from 'axios';
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
- import { pipeline } from 'node:stream/promises';
4
+ import {Readable} from 'node:stream';
5
+ import {pipeline} from 'node:stream/promises';
5
6
  import unzipper from 'unzip-stream';
6
- import { createDeployArchive, createReleaseArchive, createSourceArchive } from './archive.js';
7
- import { clearStaleBuildExtract } from './artifacts.js';
8
- import { listHtmlFilesRecursive } from './fs-utils.js';
9
- import { printBuildReport } from './report.js';
7
+ import {createDeployArchive, createReleaseArchive, createSourceArchive} from './archive.js';
8
+ import {clearStaleBuildExtract} from './artifacts.js';
9
+ import {listHtmlFilesRecursive} from './fs-utils.js';
10
+ import {printBuildReport} from './report.js';
11
+ import {
12
+ createEnvelopeFromInnerZip,
13
+ decryptResultEnvelope,
14
+ generateResultKeyPair,
15
+ resolveEncryptPublicKey,
16
+ } from './choreCrypto.js';
17
+
18
+ const POLL_MS = 5000;
10
19
 
11
20
  /**
12
21
  * @param {string} gameId
13
22
  * @param {string} platformKey
14
23
  */
15
- function buildInfoParam(gameId, platformKey) {
24
+ export function buildInfoParam (gameId, platformKey) {
16
25
  return platformKey && platformKey !== gameId ? `${gameId}_${platformKey}` : gameId;
17
26
  }
18
27
 
19
28
  /**
20
- * @param {string} server
21
- * @param {'build' | 'deploy' | 'release'} stage
22
- * @param {string} family
23
- * @param {string} gameId
24
- * @param {string} platformKey
25
- * @param {string} [deploymentKey]
29
+ * Build wrapfully.json routing config appended to the Yap zip.
26
30
  */
27
- function resolveWrapfullyUrl(server, stage, family, gameId, platformKey, deploymentKey) {
28
- const info = buildInfoParam(gameId, platformKey);
31
+ export function buildWrapfullyJobConfig (stage, family, gameId, platformKey, deploymentKey) {
32
+ /** @type {Record<string, string>} */
33
+ const job = {
34
+ stage,
35
+ route: family,
36
+ platformKey,
37
+ gameId,
38
+ };
29
39
 
30
40
  if (stage === 'deploy') {
31
- return `${server}deploy/${deploymentKey}/${info}`;
41
+ if (!deploymentKey) {
42
+ throw new Error('deploy stage requires deploymentKey');
43
+ }
44
+ job.deploymentKey = deploymentKey;
45
+ }
46
+
47
+ return job;
48
+ }
49
+
50
+ async function streamToBuffer (stream) {
51
+ const chunks = [];
52
+
53
+ for await (const chunk of stream) {
54
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
55
+ }
56
+
57
+ return Buffer.concat(chunks);
58
+ }
59
+
60
+ async function sleep (ms) {
61
+ await new Promise((resolve) => setTimeout(resolve, ms));
62
+ }
63
+
64
+ /**
65
+ * Submit zip to Showfully Yap, poll until complete, return result zip Buffer.
66
+ */
67
+ export async function submitWrapfullyChore ({
68
+ server,
69
+ accessToken,
70
+ zipBuffer,
71
+ log = console.log,
72
+ }) {
73
+ const base = server.replace(/\/?$/, '/');
74
+ const submitUrl = `${base}yap/wrapfully`;
75
+ const headers = {
76
+ Authorization: `Bearer ${accessToken}`,
77
+ 'Content-Type': 'application/zip',
78
+ };
79
+
80
+ log(`adaptfully: POST ${submitUrl}`);
81
+
82
+ let submitJson;
83
+
84
+ try {
85
+ const {data} = await axios.post(submitUrl, zipBuffer, {
86
+ maxRedirects: 0,
87
+ headers,
88
+ maxBodyLength: Infinity,
89
+ maxContentLength: Infinity,
90
+ validateStatus: () => true,
91
+ });
92
+
93
+ submitJson = data;
94
+ } catch (err) {
95
+ if (/** @type {NodeJS.ErrnoException} */ (err).code === 'ECONNREFUSED') {
96
+ throw new Error(`Cannot connect to Showfully server "${server}"`);
97
+ }
98
+ throw err;
99
+ }
100
+
101
+ if (submitJson?.errors?.length) {
102
+ throw new Error(submitJson.errors[0]);
103
+ }
104
+ if (!submitJson?.choreId) {
105
+ throw new Error('Showfully did not return a choreId');
106
+ }
107
+
108
+ const {choreId} = submitJson;
109
+
110
+ log(`adaptfully: chore ${choreId} submitted; waiting…`);
111
+
112
+ for (;;) {
113
+ const statusUrl = `${base}yap/wrapfully/${encodeURIComponent(choreId)}/status`;
114
+ const {data: status} = await axios.get(statusUrl, {
115
+ headers: {Authorization: `Bearer ${accessToken}`},
116
+ validateStatus: () => true,
117
+ });
118
+
119
+ if (status?.state === 'complete') {
120
+ break;
121
+ }
122
+ if (status?.state === 'error' || status?.errors?.length) {
123
+ throw new Error(status.errors?.[0] || `Chore ${choreId} failed`);
124
+ }
125
+
126
+ log(`adaptfully: ${status?.status || status?.state || 'waiting'}…`);
127
+ await sleep(POLL_MS);
128
+ }
129
+
130
+ log(`adaptfully: downloading chore ${choreId}`);
131
+ const downloadUrl = `${base}yap/wrapfully/${encodeURIComponent(choreId)}`;
132
+ const {data, headers: resHeaders} = await axios.get(downloadUrl, {
133
+ headers: {Authorization: `Bearer ${accessToken}`},
134
+ responseType: 'arraybuffer',
135
+ maxContentLength: Infinity,
136
+ validateStatus: () => true,
137
+ });
138
+
139
+ const ctype = resHeaders['content-type'] || '';
140
+
141
+ if (ctype.includes('application/json')) {
142
+ const json = JSON.parse(Buffer.from(data).toString('utf8'));
143
+
144
+ throw new Error(json.errors?.[0] || 'Chore download returned JSON');
32
145
  }
33
146
 
34
- return `${server}${family}/${stage}/${info}`;
147
+ return Buffer.from(data);
35
148
  }
36
149
 
37
150
  /**
@@ -41,23 +154,45 @@ function resolveWrapfullyUrl(server, stage, family, gameId, platformKey, deploym
41
154
  * @param {'build' | 'deploy' | 'release'} stage
42
155
  * @param {string} family
43
156
  * @param {string} deployFolder
44
- * @param {{ name: string, version: string }} pkg
157
+ * @param {{ name: string, version: string, config?: object }} pkg
45
158
  * @param {'extract' | string} mode
46
- * @param {{ log?: (message: string) => void, publishDir?: string, deploymentDirs?: string[], platformKey?: string, deploymentKey?: string, artifactPath?: string }} [options]
159
+ * @param {{
160
+ * log?: (message: string) => void,
161
+ * publishDir?: string,
162
+ * deploymentDirs?: string[],
163
+ * platformKey?: string,
164
+ * deploymentKey?: string,
165
+ * artifactPath?: string,
166
+ * accessToken?: string,
167
+ * encrypt?: boolean,
168
+ * encryptPublicKey?: string,
169
+ * wrapfullyConfig?: object,
170
+ * }} [options]
47
171
  */
48
- export async function send(gameId, contents, server, stage, family, deployFolder, pkg, mode = 'extract', options = {}) {
172
+ export async function send (
173
+ gameId,
174
+ contents,
175
+ server,
176
+ stage,
177
+ family,
178
+ deployFolder,
179
+ pkg,
180
+ mode = 'extract',
181
+ options = {},
182
+ ) {
49
183
  const log = options.log ?? console.log;
50
184
  const platformKey = options.platformKey ?? family;
51
185
  const outputRoot = pkg.config?.outputFolder || 'output';
186
+ const accessToken = options.accessToken;
187
+
188
+ if (!accessToken) {
189
+ throw new Error('accessToken (Showfully PAT) is required for Yap wrapfully chores');
190
+ }
52
191
 
53
192
  if (mode === 'extract') {
54
193
  clearStaleBuildExtract(outputRoot);
55
194
  }
56
195
 
57
- const destination = mode === 'extract'
58
- ? unzipper.Extract({ path: `${outputRoot}/`, concurrency: 1 })
59
- : fs.createWriteStream(`${outputRoot}/${pkg.name}-${pkg.version}-${stage}-${family}.zip`);
60
-
61
196
  let archiveStream;
62
197
 
63
198
  if (stage === 'deploy') {
@@ -67,9 +202,9 @@ export async function send(gameId, contents, server, stage, family, deployFolder
67
202
  if (!options.deploymentKey) {
68
203
  throw new Error('deploy stage requires options.deploymentKey');
69
204
  }
70
- archiveStream = createDeployArchive(options.artifactPath, options.publishDir, contents, { log });
205
+ archiveStream = createDeployArchive(options.artifactPath, options.publishDir, contents, {log});
71
206
  } else if (stage === 'release') {
72
- archiveStream = createReleaseArchive(deployFolder, contents, options.deploymentDirs ?? [], { log });
207
+ archiveStream = createReleaseArchive(deployFolder, contents, options.deploymentDirs ?? [], {log});
73
208
  } else {
74
209
  archiveStream = createSourceArchive(deployFolder, contents, {
75
210
  log,
@@ -78,33 +213,95 @@ export async function send(gameId, contents, server, stage, family, deployFolder
78
213
  });
79
214
  }
80
215
 
81
- const url = resolveWrapfullyUrl(server, stage, family, gameId, platformKey, options.deploymentKey);
82
- log(`adaptfully: POST ${url}`);
83
-
84
216
  if (stage !== 'deploy') {
85
217
  const htmlFiles = listHtmlFilesRecursive(deployFolder);
218
+
86
219
  log(`adaptfully: sending ${htmlFiles.length} HTML file(s) from ${path.resolve(deployFolder)}`);
87
220
  }
88
221
 
89
- const { data } = await axios.post(url, archiveStream, {
90
- maxRedirects: 0,
91
- responseType: 'stream',
92
- });
222
+ // Append wrapfully.json routing into the archive by rebuilding into a buffer.
223
+ const job = buildWrapfullyJobConfig(stage, family, gameId, platformKey, options.deploymentKey);
224
+ const innerChunks = [];
93
225
 
94
- archiveStream.on('close', () => {
95
- log('adaptfully: upload complete');
96
- });
226
+ // Re-pack: read archiveStream into zip, add wrapfully.json
227
+ // Archiver streams aren't easily mutable — collect then use yazl-free approach:
228
+ // pipe archive to buffer, unzip to tmp, add file, rezip is heavy.
229
+ // Simpler: createSourceArchive already finalized as stream — append via second archiver
230
+ // that copies entries is complex. Instead append wrapfully.json by concatenating
231
+ // a sidecar zip is wrong. Best: write wrapfully.json into deploy folder meta before
232
+ // archive... but that mutates disk.
233
+ //
234
+ // Practical approach matching Dutifully: build archive, buffer it, then use a new
235
+ // archiver that includes the buffer as... no that's nested.
236
+ //
237
+ // Use jszip-like: buffer the created archive by consuming stream, then use
238
+ // adm-zip equivalent — we have unzipper. Extract to tmp, write wrapfully.json, rezip.
239
+
240
+ const tmpRoot = path.join(outputRoot, `.adaptfully-yap-${Date.now().toString(16)}`);
97
241
 
242
+ fs.mkdirSync(tmpRoot, {recursive: true});
98
243
  try {
99
- await pipeline(data, destination);
100
- } catch (err) {
101
- if (/** @type {NodeJS.ErrnoException} */ (err).code === 'ECONNREFUSED') {
102
- console.error(`Cannot connect to Wrapfully server "${server}"`);
103
- process.exit(1);
104
- }
105
- throw err;
244
+ await pipeline(archiveStream, unzipper.Extract({path: tmpRoot, concurrency: 1}));
245
+ fs.writeFileSync(path.join(tmpRoot, 'wrapfully.json'), JSON.stringify(job, null, 2));
246
+
247
+ const {default: archiver} = await import('archiver');
248
+ const {PassThrough} = await import('node:stream');
249
+ const archive = archiver('zip', {zlib: {level: 0}});
250
+ const pass = new PassThrough();
251
+
252
+ archive.pipe(pass);
253
+ pass.on('data', (c) => innerChunks.push(c));
254
+
255
+ const done = new Promise((resolve, reject) => {
256
+ pass.on('end', resolve);
257
+ pass.on('error', reject);
258
+ archive.on('error', reject);
259
+ });
260
+
261
+ archive.directory(tmpRoot, false);
262
+ await archive.finalize();
263
+ await done;
264
+ } finally {
265
+ fs.rmSync(tmpRoot, {recursive: true, force: true});
266
+ }
267
+
268
+ let uploadBuffer = Buffer.concat(innerChunks);
269
+ let resultPriv = null;
270
+
271
+ if (options.encrypt) {
272
+ const {pem, kid} = resolveEncryptPublicKey(
273
+ options.wrapfullyConfig || {},
274
+ options.encryptPublicKey,
275
+ );
276
+ const resultKeyPair = generateResultKeyPair();
277
+ const envelope = await createEnvelopeFromInnerZip(uploadBuffer, {
278
+ publicKeyPem: pem,
279
+ kid,
280
+ resultKeyPair,
281
+ });
282
+
283
+ uploadBuffer = envelope.zip;
284
+ resultPriv = envelope.resultPriv;
285
+ log(`adaptfully: encrypting chore with kid "${kid}"`);
286
+ }
287
+
288
+ let resultBuffer = await submitWrapfullyChore({
289
+ server,
290
+ accessToken,
291
+ zipBuffer: uploadBuffer,
292
+ log,
293
+ });
294
+
295
+ if (resultPriv) {
296
+ resultBuffer = await decryptResultEnvelope(resultBuffer, resultPriv);
106
297
  }
107
298
 
299
+ const destination = mode === 'extract'
300
+ ? unzipper.Extract({path: `${outputRoot}/`, concurrency: 1})
301
+ : fs.createWriteStream(`${outputRoot}/${pkg.name}-${pkg.version}-${stage}-${family}.zip`);
302
+
303
+ await pipeline(Readable.from(resultBuffer), destination);
304
+
108
305
  if (mode === 'extract') {
109
306
  printBuildReport(`${stage}-${family}`, pkg);
110
307
  }
package/lib/node/index.js CHANGED
@@ -62,6 +62,7 @@ export {
62
62
  ensurePublishCredentialsGitignore,
63
63
  normalizeUserPath,
64
64
  parsePublishCredentialArgv,
65
+ promptConfirm,
65
66
  promptRequired,
66
67
  PUBLISH_CREDENTIAL_GITIGNORE_PATTERNS,
67
68
  resolveUserPath,
@@ -90,3 +91,22 @@ export {
90
91
  runApplePublish,
91
92
  validateApplePublishJson,
92
93
  } from './apple-publish.js';
94
+ export {
95
+ androidKeystoreFromCli,
96
+ buildAndroidBuildJson,
97
+ defaultAndroidDeploymentDir,
98
+ generateKeystore,
99
+ runAndroidKeystore,
100
+ runKeytool,
101
+ } from './android-keystore.js';
102
+ export {
103
+ appleSigningFilenames,
104
+ appleSigningFromCli,
105
+ defaultAppleSigningDir,
106
+ exportAppleP12,
107
+ generateAppleCsr,
108
+ mergeAppleP12PasswordBuildJson,
109
+ normalizeAppleSigningKind,
110
+ runAppleSigning,
111
+ runOpenssl,
112
+ } from './apple-signing.js';