eoas 3.0.3 → 3.0.4
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.
|
@@ -7,6 +7,7 @@ const fs_extra_1 = require("fs-extra");
|
|
|
7
7
|
const path_1 = tslib_1.__importDefault(require("path"));
|
|
8
8
|
const log_1 = tslib_1.__importDefault(require("../lib/log"));
|
|
9
9
|
const prompts_1 = require("../lib/prompts");
|
|
10
|
+
const utils_1 = require("../lib/utils");
|
|
10
11
|
class GenerateCerts extends core_1.Command {
|
|
11
12
|
static args = {};
|
|
12
13
|
static description = 'Generate private & public certificates for code signing';
|
|
@@ -78,6 +79,9 @@ class GenerateCerts extends core_1.Command {
|
|
|
78
79
|
});
|
|
79
80
|
const keyPairPEM = (0, code_signing_certificates_1.convertKeyPairToPEM)(keyPair);
|
|
80
81
|
const certificatePEM = (0, code_signing_certificates_1.convertCertificateToCertificatePEM)(certificate);
|
|
82
|
+
// Before the key touches the disk, so there is no window where it exists
|
|
83
|
+
// uncovered by the ignore rule.
|
|
84
|
+
(0, utils_1.ensurePrivateKeyIgnored)(process.cwd());
|
|
81
85
|
await Promise.all([
|
|
82
86
|
(0, fs_extra_1.writeFile)(path_1.default.join(keyOutput, 'public-key.pem'), keyPairPEM.publicKeyPEM),
|
|
83
87
|
(0, fs_extra_1.writeFile)(path_1.default.join(keyOutput, 'private-key.pem'), keyPairPEM.privateKeyPEM),
|
|
@@ -85,6 +89,8 @@ class GenerateCerts extends core_1.Command {
|
|
|
85
89
|
]);
|
|
86
90
|
log_1.default.succeed(`Generated public and private keys output in ${keyOutputDir}. Please follow the documentation to securely store them and do not commit them to your repository.`);
|
|
87
91
|
log_1.default.succeed(`Generated code signing certificate output in ${certificateOutputDir}.`);
|
|
92
|
+
log_1.default.warn('⚠️ private-key.pem is used by your OTA server to sign updates. Never commit it and do not keep it inside your app project: configure it on your server (or in a secret store), then remove it from this machine.');
|
|
93
|
+
log_1.default.warn('Your team does not need this key for local development: run the dev server with DISABLE_CODE_SIGNING=true. See the "Local development" section of the documentation.');
|
|
88
94
|
}
|
|
89
95
|
}
|
|
90
96
|
exports.default = GenerateCerts;
|
package/dist/commands/init.js
CHANGED
|
@@ -41,7 +41,7 @@ class Init extends core_1.Command {
|
|
|
41
41
|
message: 'Enter the URL of your update server (ex: https://customota.com)',
|
|
42
42
|
name: 'updateUrl',
|
|
43
43
|
type: 'text',
|
|
44
|
-
initial: (0, expoConfig_1.getExpoConfigUpdateUrl)(config),
|
|
44
|
+
initial: ((0, expoConfig_1.getExpoConfigUpdateUrl)(config) || '').replace(/\/manifest$/, ''),
|
|
45
45
|
validate: v => {
|
|
46
46
|
return !!v && (0, utils_1.isValidUpdateUrl)(v);
|
|
47
47
|
},
|
|
@@ -95,13 +95,15 @@ class Init extends core_1.Command {
|
|
|
95
95
|
}
|
|
96
96
|
},
|
|
97
97
|
});
|
|
98
|
+
// The code signing fields are guarded so the dev server can run without the
|
|
99
|
+
// private key: DISABLE_CODE_SIGNING=true expo start --dev-client. The strings
|
|
100
|
+
// are emitted as raw expressions by createOrModifyExpoConfigAsync.
|
|
98
101
|
const newUpdateConfig = {
|
|
99
102
|
url: manifestEndpoint,
|
|
100
|
-
codeSigningMetadata: {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
codeSigningCertificate: codeSigningCertificatePath,
|
|
103
|
+
codeSigningMetadata: "process.env.DISABLE_CODE_SIGNING ? undefined : { keyid: 'main', alg: 'rsa-v1_5-sha256' }",
|
|
104
|
+
codeSigningCertificate: `process.env.DISABLE_CODE_SIGNING ? undefined : '${codeSigningCertificatePath
|
|
105
|
+
.replace(/\\/g, '\\\\')
|
|
106
|
+
.replace(/'/g, "\\'")}'`,
|
|
105
107
|
enabled: true,
|
|
106
108
|
requestHeaders: {
|
|
107
109
|
'expo-channel-name': 'process.env.RELEASE_CHANNEL',
|
|
@@ -119,6 +121,7 @@ class Init extends core_1.Command {
|
|
|
119
121
|
updateConfigSpinner.fail('Failed to update Expo config');
|
|
120
122
|
log_1.default.error(e);
|
|
121
123
|
}
|
|
124
|
+
(0, utils_1.ensurePrivateKeyIgnored)(projectDir);
|
|
122
125
|
}
|
|
123
126
|
}
|
|
124
127
|
exports.default = Init;
|
package/dist/lib/expoConfig.d.ts
CHANGED
|
@@ -23,5 +23,5 @@ export declare function getPublicExpoConfigAsync(projectDir: string, opts?: Expo
|
|
|
23
23
|
export declare function getExpoConfigUpdateUrl(config: ExpoConfig): string | undefined;
|
|
24
24
|
export declare function getExpoAppId(config: ExpoConfig): string | undefined;
|
|
25
25
|
export declare function requireExpoAppId(config: ExpoConfig): string;
|
|
26
|
-
export declare function createOrModifyExpoConfigAsync(projectDir: string, exp:
|
|
26
|
+
export declare function createOrModifyExpoConfigAsync(projectDir: string, exp: Record<string, any>): Promise<void>;
|
|
27
27
|
export declare function resolveServerUrl(config: ExpoConfig): Promise<string>;
|
package/dist/lib/expoConfig.js
CHANGED
|
@@ -31,7 +31,13 @@ async function getExpoConfigInternalAsync(projectDir, opts = {}) {
|
|
|
31
31
|
const runner = (0, packageRunner_1.resolvePackageRunner)(opts.packageRunner, projectDir);
|
|
32
32
|
const [runnerCommand, runnerArgs] = (0, packageRunner_1.splitPackageRunner)(runner);
|
|
33
33
|
try {
|
|
34
|
-
const { stdout } = await (0, spawn_async_1.default)(runnerCommand, [
|
|
34
|
+
const { stdout } = await (0, spawn_async_1.default)(runnerCommand, [
|
|
35
|
+
...runnerArgs,
|
|
36
|
+
'expo',
|
|
37
|
+
'config',
|
|
38
|
+
'--json',
|
|
39
|
+
...(opts.isPublicConfig ? ['--type', 'public'] : []),
|
|
40
|
+
], {
|
|
35
41
|
cwd: projectDir,
|
|
36
42
|
env: {
|
|
37
43
|
...process.env,
|
|
@@ -132,6 +138,10 @@ function requireExpoAppId(config) {
|
|
|
132
138
|
return appId;
|
|
133
139
|
}
|
|
134
140
|
exports.requireExpoAppId = requireExpoAppId;
|
|
141
|
+
// exp is a config fragment. String values starting with 'process.env.' are
|
|
142
|
+
// emitted as raw JavaScript expressions rather than string literals, so callers
|
|
143
|
+
// can write env-dependent values like
|
|
144
|
+
// "process.env.DISABLE_CODE_SIGNING ? undefined : './certs/certificate.pem'".
|
|
135
145
|
async function createOrModifyExpoConfigAsync(projectDir, exp) {
|
|
136
146
|
try {
|
|
137
147
|
ensureExpoConfigExists(projectDir);
|
|
@@ -148,37 +158,22 @@ async function createOrModifyExpoConfigAsync(projectDir, exp) {
|
|
|
148
158
|
// eslint-disable-next-line node/no-sync
|
|
149
159
|
fs_extra_1.default.writeFileSync(configPathJS, newConfigContent);
|
|
150
160
|
}
|
|
151
|
-
else
|
|
161
|
+
else {
|
|
162
|
+
const configPath = hasJsConfig ? configPathJS : configPathTS;
|
|
152
163
|
// eslint-disable-next-line node/no-sync
|
|
153
|
-
const existingCode = fs_extra_1.default.readFileSync(
|
|
154
|
-
const j = jscodeshift_1.default;
|
|
164
|
+
const existingCode = fs_extra_1.default.readFileSync(configPath, 'utf8');
|
|
165
|
+
const j = configPath.endsWith('.ts') ? jscodeshift_1.default.withParser('ts') : jscodeshift_1.default;
|
|
155
166
|
const ast = j(existingCode);
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
path.value.body.body.length > 0) {
|
|
160
|
-
const returnStatement = path.value.body.body.find(node => j.ReturnStatement.check(node));
|
|
161
|
-
if (returnStatement &&
|
|
162
|
-
j.ReturnStatement.check(returnStatement) &&
|
|
163
|
-
returnStatement.argument) {
|
|
164
|
-
const configObject = returnStatement.argument;
|
|
165
|
-
if (j.ObjectExpression.check(configObject)) {
|
|
166
|
-
updateObjectExpression(j, configObject, exp);
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
});
|
|
167
|
+
if (!updateExportedConfigObject(j, ast, exp)) {
|
|
168
|
+
throw new Error(`Could not find the exported config object in ${path_1.default.basename(configPath)}.`);
|
|
169
|
+
}
|
|
171
170
|
const updatedCode = ast.toSource({
|
|
172
171
|
quote: 'auto',
|
|
173
172
|
trailingComma: true,
|
|
174
173
|
reuseWhitespace: true,
|
|
175
174
|
});
|
|
176
175
|
// eslint-disable-next-line node/no-sync
|
|
177
|
-
fs_extra_1.default.writeFileSync(
|
|
178
|
-
}
|
|
179
|
-
else if (configPathTS) {
|
|
180
|
-
log_1.default.warn('TypeScript support is not yet implemented.');
|
|
181
|
-
throw new Error('TypeScript support is not yet implemented.');
|
|
176
|
+
fs_extra_1.default.writeFileSync(configPath, updatedCode);
|
|
182
177
|
}
|
|
183
178
|
}
|
|
184
179
|
catch (e) {
|
|
@@ -192,12 +187,65 @@ async function createOrModifyExpoConfigAsync(projectDir, exp) {
|
|
|
192
187
|
}
|
|
193
188
|
}
|
|
194
189
|
exports.createOrModifyExpoConfigAsync = createOrModifyExpoConfigAsync;
|
|
190
|
+
// Finds the object literal the dynamic config exports (export default or
|
|
191
|
+
// module.exports; a function returning it, with expression or block body;
|
|
192
|
+
// optional 'as' casts) and merges exp into it. Returns false when no
|
|
193
|
+
// recognizable shape is found, so the caller can fail loudly instead of
|
|
194
|
+
// writing the file back unchanged.
|
|
195
|
+
function updateExportedConfigObject(j, ast, exp) {
|
|
196
|
+
const exportedNodes = [];
|
|
197
|
+
ast.find(j.ExportDefaultDeclaration).forEach(p => exportedNodes.push(p.value.declaration));
|
|
198
|
+
ast
|
|
199
|
+
.find(j.AssignmentExpression, {
|
|
200
|
+
left: { object: { name: 'module' }, property: { name: 'exports' } },
|
|
201
|
+
})
|
|
202
|
+
.forEach(p => exportedNodes.push(p.value.right));
|
|
203
|
+
for (const exportedNode of exportedNodes) {
|
|
204
|
+
const configObject = resolveConfigObject(j, exportedNode);
|
|
205
|
+
if (configObject) {
|
|
206
|
+
updateObjectExpression(j, configObject, exp);
|
|
207
|
+
return true;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
function resolveConfigObject(j, exportedNode) {
|
|
213
|
+
let node = unwrapExpression(exportedNode);
|
|
214
|
+
if (j.ArrowFunctionExpression.check(node) ||
|
|
215
|
+
j.FunctionExpression.check(node) ||
|
|
216
|
+
j.FunctionDeclaration.check(node)) {
|
|
217
|
+
if (j.BlockStatement.check(node.body)) {
|
|
218
|
+
const returnStatement = node.body.body.find((statement) => j.ReturnStatement.check(statement));
|
|
219
|
+
node = returnStatement?.argument ?? null;
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
222
|
+
node = node.body;
|
|
223
|
+
}
|
|
224
|
+
node = node && unwrapExpression(node);
|
|
225
|
+
}
|
|
226
|
+
return node && j.ObjectExpression.check(node) ? node : null;
|
|
227
|
+
}
|
|
228
|
+
// Strips TS casts and parentheses: `({ ... } as ExpoConfig)` -> the object.
|
|
229
|
+
function unwrapExpression(node) {
|
|
230
|
+
let current = node;
|
|
231
|
+
while (current &&
|
|
232
|
+
(current.type === 'TSAsExpression' ||
|
|
233
|
+
current.type === 'TSSatisfiesExpression' ||
|
|
234
|
+
current.type === 'ParenthesizedExpression')) {
|
|
235
|
+
current = current.expression;
|
|
236
|
+
}
|
|
237
|
+
return current;
|
|
238
|
+
}
|
|
195
239
|
function updateObjectExpression(j, configObject, updates) {
|
|
196
240
|
Object.entries(updates).forEach(([key, value]) => {
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
241
|
+
// The default parser produces 'Property' nodes, the ts parser 'ObjectProperty'.
|
|
242
|
+
const existingProperty = configObject.properties.find((prop) => {
|
|
243
|
+
if (prop.type !== 'Property' && prop.type !== 'ObjectProperty') {
|
|
244
|
+
return false;
|
|
245
|
+
}
|
|
246
|
+
return ((prop.key.type === 'Identifier' && prop.key.name === key) ||
|
|
247
|
+
((prop.key.type === 'StringLiteral' || prop.key.type === 'Literal') &&
|
|
248
|
+
prop.key.value === key));
|
|
201
249
|
});
|
|
202
250
|
if (existingProperty) {
|
|
203
251
|
configObject.properties = configObject.properties.filter(prop => prop !== existingProperty);
|
|
@@ -208,7 +256,10 @@ function updateObjectExpression(j, configObject, updates) {
|
|
|
208
256
|
}
|
|
209
257
|
function createValueNode(j, value) {
|
|
210
258
|
if (typeof value === 'string' && value.startsWith('process.env.')) {
|
|
211
|
-
|
|
259
|
+
if (/^process\.env\.\w+$/.test(value)) {
|
|
260
|
+
return j.memberExpression(j.memberExpression(j.identifier('process'), j.identifier('env')), j.identifier(value.split('.')[2]));
|
|
261
|
+
}
|
|
262
|
+
return parseExpressionNode(j, value);
|
|
212
263
|
}
|
|
213
264
|
if (typeof value === 'object' && value !== null) {
|
|
214
265
|
return j.objectExpression(Object.entries(value).map(([key, val]) => j.objectProperty(j.stringLiteral(key), createValueNode(j, val)) // Force stringLiteral pour garder les guillemets
|
|
@@ -216,8 +267,19 @@ function createValueNode(j, value) {
|
|
|
216
267
|
}
|
|
217
268
|
return j.literal(value);
|
|
218
269
|
}
|
|
270
|
+
function parseExpressionNode(j, code) {
|
|
271
|
+
const statement = j(`(${code});`).find(j.ExpressionStatement).nodes()[0];
|
|
272
|
+
return statement.expression;
|
|
273
|
+
}
|
|
274
|
+
// Raw expressions are swapped for placeholders before JSON.stringify and
|
|
275
|
+
// spliced back verbatim afterwards, so JSON escaping never mangles their
|
|
276
|
+
// contents (backslashes in Windows paths, quotes, ...).
|
|
219
277
|
function stringifyWithEnv(obj) {
|
|
220
|
-
|
|
278
|
+
const rawExpressions = [];
|
|
279
|
+
const json = JSON.stringify(obj, (_key, value) => typeof value === 'string' && value.startsWith('process.env.')
|
|
280
|
+
? `__RAW_EXPR_${rawExpressions.push(value) - 1}__`
|
|
281
|
+
: value, 2);
|
|
282
|
+
return json.replace(/"__RAW_EXPR_(\d+)__"/g, (_match, index) => rawExpressions[Number(index)]);
|
|
221
283
|
}
|
|
222
284
|
async function resolveServerUrl(config) {
|
|
223
285
|
const updateUrl = config.updates?.url;
|
|
@@ -58,7 +58,9 @@ function detectRunnerFromPackageJson(startDir) {
|
|
|
58
58
|
while (dir !== root) {
|
|
59
59
|
const pkgPath = path_1.default.join(dir, 'package.json');
|
|
60
60
|
try {
|
|
61
|
+
// eslint-disable-next-line node/no-sync
|
|
61
62
|
if (fs_extra_1.default.existsSync(pkgPath)) {
|
|
63
|
+
// eslint-disable-next-line node/no-sync
|
|
62
64
|
const pkg = fs_extra_1.default.readJsonSync(pkgPath);
|
|
63
65
|
if (pkg.packageManager) {
|
|
64
66
|
const name = pkg.packageManager.split('@')[0];
|
package/dist/lib/utils.d.ts
CHANGED
package/dist/lib/utils.js
CHANGED
|
@@ -1,7 +1,38 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.isValidUpdateUrl = void 0;
|
|
3
|
+
exports.ensurePrivateKeyIgnored = exports.isValidUpdateUrl = void 0;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const fs_extra_1 = tslib_1.__importDefault(require("fs-extra"));
|
|
6
|
+
const path_1 = tslib_1.__importDefault(require("path"));
|
|
7
|
+
const log_1 = tslib_1.__importDefault(require("./log"));
|
|
4
8
|
function isValidUpdateUrl(updateUrl) {
|
|
5
9
|
return updateUrl.match(/^https?:\/\/[^/]+$/) !== null;
|
|
6
10
|
}
|
|
7
11
|
exports.isValidUpdateUrl = isValidUpdateUrl;
|
|
12
|
+
// Keeps the code signing private key out of the app repository: appends a bare
|
|
13
|
+
// 'private-key.pem' pattern to the project .gitignore (a pattern without a
|
|
14
|
+
// slash matches at every directory level). Only an existing bare rule counts:
|
|
15
|
+
// comments, negated entries and path-specific rules like certs/private-key.pem
|
|
16
|
+
// do not guarantee project-wide protection. Appending at the end also wins over
|
|
17
|
+
// an earlier negated entry, since the last matching gitignore rule prevails.
|
|
18
|
+
function ensurePrivateKeyIgnored(projectDir) {
|
|
19
|
+
const gitignorePath = path_1.default.join(projectDir, '.gitignore');
|
|
20
|
+
try {
|
|
21
|
+
// eslint-disable-next-line node/no-sync
|
|
22
|
+
const gitignore = fs_extra_1.default.existsSync(gitignorePath) ? fs_extra_1.default.readFileSync(gitignorePath, 'utf8') : '';
|
|
23
|
+
const lines = gitignore.split(/\r?\n/).map(line => line.trim());
|
|
24
|
+
const lastBareRule = lines.lastIndexOf('private-key.pem');
|
|
25
|
+
const lastNegation = lines.lastIndexOf('!private-key.pem');
|
|
26
|
+
if (lastBareRule !== -1 && lastBareRule > lastNegation) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const separator = gitignore === '' ? '' : gitignore.endsWith('\n') ? '\n' : '\n\n';
|
|
30
|
+
// eslint-disable-next-line node/no-sync
|
|
31
|
+
fs_extra_1.default.appendFileSync(gitignorePath, `${separator}# Code signing private key (server-side secret, never commit it)\nprivate-key.pem\n`);
|
|
32
|
+
log_1.default.succeed('Added private-key.pem to .gitignore');
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
log_1.default.warn('Could not update .gitignore. Make sure private-key.pem is never committed to your repository.');
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
exports.ensurePrivateKeyIgnored = ensurePrivateKeyIgnored;
|
package/package.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eoas",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.4",
|
|
4
4
|
"main": "index.js",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"build": "tsc --project tsconfig.json",
|
|
7
7
|
"watch": "tsc --project tsconfig.json --watch",
|
|
8
|
-
"lint": "eslint ."
|
|
8
|
+
"lint": "eslint .",
|
|
9
|
+
"test": "vitest run"
|
|
9
10
|
},
|
|
10
11
|
"engines": {
|
|
11
12
|
"node": ">=18.0.0"
|
|
@@ -82,7 +83,8 @@
|
|
|
82
83
|
"eslint-plugin-async-protect": "^3.1.0",
|
|
83
84
|
"eslint-plugin-node": "^11.1.0",
|
|
84
85
|
"ts-node": "10.9.2",
|
|
85
|
-
"typescript": "5.3.3"
|
|
86
|
+
"typescript": "5.3.3",
|
|
87
|
+
"vitest": "^2.1.9"
|
|
86
88
|
},
|
|
87
89
|
"bin": {
|
|
88
90
|
"eoas": "bin/run.js"
|