@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.
- package/CHANGELOG.md +20 -0
- package/README.md +29 -10
- package/docs/credentials/README.md +50 -0
- package/docs/credentials/android.md +124 -0
- package/docs/credentials/ios.md +182 -0
- package/docs/credentials/steam.md +99 -0
- package/lib/node/android-keystore.js +243 -0
- package/lib/node/apple-signing.js +392 -0
- package/lib/node/choreCrypto.js +197 -0
- package/lib/node/config.js +45 -4
- package/lib/node/deploy.js +239 -42
- package/lib/node/index.js +20 -0
- package/lib/node/pipeline.js +51 -14
- package/lib/node/publish-credentials.js +47 -1
- package/lib/node/wrapfullyDefaultPublicKey.js +17 -0
- package/package.json +2 -1
|
@@ -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
|
+
};
|
package/lib/node/config.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
2
|
|
|
3
|
-
const
|
|
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
|
-
*
|
|
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.
|
|
41
|
+
|| process.env.SHOWFULLY_SERVER
|
|
42
|
+
|| wrapfullyConfig.showfullyServer
|
|
39
43
|
|| wrapfullyConfig.server
|
|
40
|
-
||
|
|
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
|
+
}
|
package/lib/node/deploy.js
CHANGED
|
@@ -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 {
|
|
4
|
+
import {Readable} from 'node:stream';
|
|
5
|
+
import {pipeline} from 'node:stream/promises';
|
|
5
6
|
import unzipper from 'unzip-stream';
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
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
|
-
*
|
|
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
|
|
28
|
-
|
|
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
|
-
|
|
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
|
|
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 {{
|
|
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(
|
|
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, {
|
|
205
|
+
archiveStream = createDeployArchive(options.artifactPath, options.publishDir, contents, {log});
|
|
71
206
|
} else if (stage === 'release') {
|
|
72
|
-
archiveStream = createReleaseArchive(deployFolder, contents, options.deploymentDirs ?? [], {
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
|
95
|
-
|
|
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(
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
}
|
|
105
|
-
|
|
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';
|