eoas 1.0.1

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/.eslintignore ADDED
@@ -0,0 +1 @@
1
+ node_modules
package/.eslintrc.js ADDED
@@ -0,0 +1,73 @@
1
+ module.exports = {
2
+ root: true,
3
+ extends: ['universe/node'],
4
+ plugins: ['node'],
5
+ ignorePatterns: ['bin/'],
6
+ rules: {
7
+ 'no-console': 'warn',
8
+ 'no-constant-condition': ['warn', { checkLoops: false }],
9
+ 'sort-imports': [
10
+ 'warn',
11
+ {
12
+ ignoreDeclarationSort: true,
13
+ },
14
+ ],
15
+ curly: 'warn',
16
+ 'import/no-cycle': 'error',
17
+ 'import/no-extraneous-dependencies': [
18
+ 'error',
19
+ { devDependencies: ['**/__tests__/**/*', '**/__mocks__/**/*'] },
20
+ ],
21
+ 'import/no-relative-packages': 'error',
22
+ 'no-restricted-imports': [
23
+ 'error',
24
+ {
25
+ paths: [
26
+ {
27
+ name: 'lodash',
28
+ message: "Don't use lodash, it's heavy!",
29
+ },
30
+ ],
31
+ },
32
+ ],
33
+ 'no-underscore-dangle': ['error', { allow: ['__typename'] }],
34
+ 'node/no-sync': 'error',
35
+ },
36
+ overrides: [
37
+ {
38
+ files: ['*.ts', '*.d.ts'],
39
+ parserOptions: {
40
+ project: './tsconfig.json',
41
+ },
42
+ rules: {
43
+ '@typescript-eslint/explicit-function-return-type': [
44
+ 'warn',
45
+ {
46
+ allowExpressions: true,
47
+ },
48
+ ],
49
+ '@typescript-eslint/prefer-nullish-coalescing': ['warn', { ignorePrimitives: true }],
50
+ '@typescript-eslint/no-confusing-void-expression': 'warn',
51
+ '@typescript-eslint/await-thenable': 'error',
52
+ '@typescript-eslint/no-misused-promises': [
53
+ 'error',
54
+ {
55
+ checksVoidReturn: false,
56
+ },
57
+ ],
58
+ '@typescript-eslint/no-floating-promises': 'error',
59
+ 'no-void': ['warn', { allowAsStatement: true }],
60
+ 'no-return-await': 'off',
61
+ '@typescript-eslint/return-await': ['error', 'always'],
62
+ '@typescript-eslint/no-confusing-non-null-assertion': 'warn',
63
+ '@typescript-eslint/no-extra-non-null-assertion': 'warn',
64
+ '@typescript-eslint/prefer-as-const': 'warn',
65
+ '@typescript-eslint/prefer-includes': 'warn',
66
+ '@typescript-eslint/prefer-readonly': 'warn',
67
+ '@typescript-eslint/prefer-string-starts-ends-with': 'warn',
68
+ '@typescript-eslint/prefer-ts-expect-error': 'warn',
69
+ '@typescript-eslint/no-unnecessary-type-assertion': 'warn',
70
+ },
71
+ },
72
+ ],
73
+ };
package/.prettierrc ADDED
@@ -0,0 +1,9 @@
1
+ {
2
+ "printWidth": 100,
3
+ "tabWidth": 2,
4
+ "singleQuote": true,
5
+ "bracketSameLine": true,
6
+ "trailingComma": "es5",
7
+ "arrowParens": "avoid",
8
+ "endOfLine": "auto"
9
+ }
package/README.md ADDED
@@ -0,0 +1,17 @@
1
+ # EOAS (Expo-Open-OTA Support)
2
+
3
+ EOAS (Expo-Open-OTA Support) is a powerful helper package designed to simplify the setup and update publication process for the [expo-open-ota](https://github.com/axelmarciano/expo-open-ota) project.
4
+
5
+ ## Quick Start
6
+
7
+ To get started with EOAS, check out the official documentation:
8
+ [EOAS Official Documentation](https://axelmarciano.github.io/expo-open-ota/)
9
+
10
+ ## Learn More
11
+ For detailed information and to explore the core functionalities of expo-open-ota, visit the main repository:
12
+ [expo-open-ota on GitHub](https://github.com/axelmarciano/expo-open-ota)
13
+
14
+ ---
15
+
16
+ Feel free to contribute, raise issues, or share feedback to help us improve EOAS!
17
+
package/bin/dev.cmd ADDED
@@ -0,0 +1,3 @@
1
+ @echo off
2
+
3
+ node "%~dp0\dev" %*
package/bin/dev.js ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env node
2
+ // eslint-disable-next-line node/shebang, unicorn/prefer-top-level-await
3
+ (async () => {
4
+ const oclif = await import('@oclif/core');
5
+ await oclif.execute({ development: true, dir: __dirname });
6
+ })();
package/bin/run.cmd ADDED
@@ -0,0 +1,3 @@
1
+ @echo off
2
+
3
+ node "%~dp0\run" %*
package/bin/run.js ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+
3
+ // eslint-disable-next-line unicorn/prefer-top-level-await
4
+ (async () => {
5
+ const oclif = await import('@oclif/core')
6
+ await oclif.execute({dir: __dirname})
7
+ })()
package/package.json ADDED
@@ -0,0 +1,92 @@
1
+ {
2
+ "name": "eoas",
3
+ "version": "1.0.1",
4
+ "main": "index.js",
5
+ "scripts": {
6
+ "build": "tsc --project tsconfig.json",
7
+ "watch": "tsc --project tsconfig.json --watch",
8
+ "lint": "eslint ."
9
+ },
10
+ "engines": {
11
+ "node": ">=18.0.0"
12
+ },
13
+ "homepage": "https://github.com/axelmarciano/expo-open-ota/tree/main/eoas",
14
+ "keywords": [
15
+ "expo-open-ota",
16
+ "expo",
17
+ "eas",
18
+ "cli"
19
+ ],
20
+ "author": "Axel Marciano",
21
+ "license": "MIT",
22
+ "description": "A CLI tool to manage publishing and OTA updates for expo-open-OTA self-hosted server. This is not an official tool from Expo but an open-source project (https://github.com/axelmarciano/expo-open-ota)",
23
+ "repository": "axelmarciano/expo-open-ota",
24
+ "dependencies": {
25
+ "@expo/code-signing-certificates": "^0.0.5",
26
+ "@expo/config": "10.0.6",
27
+ "@expo/config-plugins": "9.0.12",
28
+ "@expo/eas-build-job": "1.0.165",
29
+ "@expo/fingerprint": "^0.11.7",
30
+ "@expo/package-manager": "1.7.0",
31
+ "@expo/spawn-async": "1.7.2",
32
+ "@oclif/core": "^4.2.4",
33
+ "@types/node-fetch": "^2.6.12",
34
+ "@urql/core": "4.0.11",
35
+ "@urql/exchange-retry": "1.2.0",
36
+ "better-opn": "3.0.2",
37
+ "chalk": "4.1.2",
38
+ "fast-glob": "3.3.2",
39
+ "figures": "3.2.0",
40
+ "file-type": "^20.0.0",
41
+ "form-data": "^4.0.1",
42
+ "fs-extra": "11.2.0",
43
+ "getenv": "1.0.0",
44
+ "graphql": "16.8.1",
45
+ "graphql-tag": "^2.12.6",
46
+ "https-proxy-agent": "5.0.1",
47
+ "ignore": "5.3.0",
48
+ "joi": "17.11.0",
49
+ "jscodeshift": "^17.1.2",
50
+ "mime": "3.0.0",
51
+ "node-fetch": "^2.6.7",
52
+ "ora": "^5.1.0",
53
+ "prompts": "^2.4.2",
54
+ "recast": "^0.23.9",
55
+ "resolve-from": "5.0.0",
56
+ "semver": "7.5.4",
57
+ "tar": "6.2.1",
58
+ "terminal-link": "2.1.1",
59
+ "uuid": "9.0.1",
60
+ "eslint": "^8.57.1",
61
+ "prettier": "3.1.1",
62
+ "log-symbols": "^4.0.0"
63
+ },
64
+ "devDependencies": {
65
+ "@babel/parser": "^7.26.7",
66
+ "@babel/types": "^7.26.7",
67
+ "@tsconfig/node18": "^18.2.4",
68
+ "@types/fs-extra": "11.0.4",
69
+ "@types/getenv": "^1.0.0",
70
+ "@types/jscodeshift": "^0.12.0",
71
+ "@types/mime": "^3.0.4",
72
+ "@types/node": "^18.19.74",
73
+ "@types/prompts": "^2.4.9",
74
+ "@types/semver": "7.5.6",
75
+ "@types/tar": "6.1.10",
76
+ "@types/uuid": "9.0.7",
77
+ "eslint-config-universe": "^14.0.0",
78
+ "eslint-plugin-async-protect": "^3.1.0",
79
+ "eslint-plugin-node": "^11.1.0",
80
+ "ts-node": "10.9.2",
81
+ "typescript": "5.3.3"
82
+ },
83
+ "bin": {
84
+ "eoas": "./bin/run.js"
85
+ },
86
+ "oclif": {
87
+ "bin": "eoas",
88
+ "commands": "./dist/commands",
89
+ "dirname": "eoas",
90
+ "topicSeparator": ":"
91
+ }
92
+ }
@@ -0,0 +1,95 @@
1
+ import {
2
+ convertCertificateToCertificatePEM,
3
+ convertKeyPairToPEM,
4
+ generateKeyPair,
5
+ generateSelfSignedCodeSigningCertificate,
6
+ } from '@expo/code-signing-certificates';
7
+ import { Command } from '@oclif/core';
8
+ import { ensureDirSync, writeFile } from 'fs-extra';
9
+ import path from 'path';
10
+
11
+ import Log from '../lib/log';
12
+ import { promptAsync } from '../lib/prompts';
13
+
14
+ export default class GenerateCerts extends Command {
15
+ static override args = {};
16
+ static override description = 'Generate private & public certificates for code signing';
17
+ static override examples = ['<%= config.bin %> <%= command.id %>'];
18
+ static override flags = {};
19
+ public async run(): Promise<void> {
20
+ const { certificateOutputDir } = await promptAsync({
21
+ message:
22
+ 'In which directory would you like to store your code signing certificate (used by your expo app)?',
23
+ name: 'certificateOutputDir',
24
+ type: 'text',
25
+ initial: './keysStore',
26
+ validate: v => {
27
+ try {
28
+ // eslint-disable-next-line
29
+ ensureDirSync(path.join(process.cwd(), v));
30
+ return true;
31
+ } catch {
32
+ return false;
33
+ }
34
+ },
35
+ });
36
+ const { keyOutputDir } = await promptAsync({
37
+ message:
38
+ 'In which directory would you like to store your key pair (used by your OTA Server) ?. ⚠️ Those keysStore are sensitive and should be kept private.',
39
+ name: 'keyOutputDir',
40
+ type: 'text',
41
+ initial: './keysStore',
42
+ validate: v => {
43
+ try {
44
+ // eslint-disable-next-line
45
+ ensureDirSync(path.join(process.cwd(), v));
46
+ return true;
47
+ } catch {
48
+ return false;
49
+ }
50
+ },
51
+ });
52
+ const { certificateCommonName } = await promptAsync({
53
+ message: 'Please enter your Organization name',
54
+ name: 'certificateCommonName',
55
+ type: 'text',
56
+ initial: 'Your Organization Name',
57
+ validate: v => {
58
+ return !!v;
59
+ },
60
+ });
61
+ const { certificateValidityDurationYears } = await promptAsync({
62
+ message: 'How many years should the certificate be valid for?',
63
+ name: 'certificateValidityDurationYears',
64
+ type: 'number',
65
+ initial: 10,
66
+ validate: v => {
67
+ return v > 0 && Number.isInteger(v);
68
+ },
69
+ });
70
+ const validityDurationYears = Math.floor(Number(certificateValidityDurationYears));
71
+ const certificateOutput = path.resolve(process.cwd(), certificateOutputDir);
72
+ const keyOutput = path.resolve(process.cwd(), keyOutputDir);
73
+ const validityNotBefore = new Date();
74
+ const validityNotAfter = new Date();
75
+ validityNotAfter.setFullYear(validityNotAfter.getFullYear() + validityDurationYears);
76
+ const keyPair = generateKeyPair();
77
+ const certificate = generateSelfSignedCodeSigningCertificate({
78
+ keyPair,
79
+ validityNotBefore,
80
+ validityNotAfter,
81
+ commonName: certificateCommonName,
82
+ });
83
+ const keyPairPEM = convertKeyPairToPEM(keyPair);
84
+ const certificatePEM = convertCertificateToCertificatePEM(certificate);
85
+ await Promise.all([
86
+ writeFile(path.join(keyOutput, 'public-key.pem'), keyPairPEM.publicKeyPEM),
87
+ writeFile(path.join(keyOutput, 'private-key.pem'), keyPairPEM.privateKeyPEM),
88
+ writeFile(path.join(certificateOutput, 'certificate.pem'), certificatePEM),
89
+ ]);
90
+ Log.succeed(
91
+ `Generated public and private keys output in ${keyOutputDir}. Please follow the documentation to securely store them and do not commit them to your repository.`
92
+ );
93
+ Log.succeed(`Generated code signing certificate output in ${certificateOutputDir}.`);
94
+ }
95
+ }
@@ -0,0 +1,117 @@
1
+ import { Command } from '@oclif/core';
2
+ import fs from 'fs-extra';
3
+ import path from 'path';
4
+
5
+ import {
6
+ createOrModifyExpoConfigAsync,
7
+ getExpoConfigUpdateUrl,
8
+ getPrivateExpoConfigAsync,
9
+ } from '../lib/expoConfig';
10
+ import Log from '../lib/log';
11
+ import { ora } from '../lib/ora';
12
+ import { isExpoInstalled } from '../lib/package';
13
+ import { confirmAsync, promptAsync } from '../lib/prompts';
14
+ import { isValidUpdateUrl } from '../lib/utils';
15
+
16
+ export default class Init extends Command {
17
+ static override args = {};
18
+ static override description = 'Configure your existing expo project with Expo Open OTA';
19
+ static override examples = ['<%= config.bin %> <%= command.id %>'];
20
+ static override flags = {};
21
+ public async run(): Promise<void> {
22
+ const projectDir = process.cwd();
23
+ const hasExpo = isExpoInstalled(projectDir);
24
+ if (!hasExpo) {
25
+ Log.error('Expo is not installed in this project. Please install Expo first.');
26
+ return;
27
+ }
28
+ const config = await getPrivateExpoConfigAsync(projectDir);
29
+ if (!config) {
30
+ Log.error(
31
+ 'Could not find Expo config in this project. Please make sure you have an Expo config.'
32
+ );
33
+ return;
34
+ }
35
+ const { updateUrl: promptedUrl } = await promptAsync({
36
+ message: 'Enter the URL of your update server (ex: https://customota.com)',
37
+ name: 'updateUrl',
38
+ type: 'text',
39
+ initial: getExpoConfigUpdateUrl(config),
40
+ validate: v => {
41
+ return !!v && isValidUpdateUrl(v);
42
+ },
43
+ });
44
+ let manifestEndpoint = `${promptedUrl}/manifest`;
45
+ const updateUrl = getExpoConfigUpdateUrl(config);
46
+ if (updateUrl && !updateUrl.includes('expo.dev')) {
47
+ const confirmed = await confirmAsync({
48
+ message: `Expo config already has an update URL set to ${updateUrl}. Do you want to replace it?`,
49
+ name: 'replace',
50
+ type: 'confirm',
51
+ });
52
+ if (!confirmed) {
53
+ manifestEndpoint = updateUrl;
54
+ }
55
+ }
56
+ const confirmed = await confirmAsync({
57
+ message: 'Do you have already generated your certificates and keysStore for code signing?',
58
+ name: 'certificates',
59
+ type: 'confirm',
60
+ });
61
+ if (!confirmed) {
62
+ Log.fail('You need to generate your certificates first by using npx eoas generate-keysStore');
63
+ return;
64
+ }
65
+ const { codeSigningCertificatePath } = await promptAsync({
66
+ message: 'Enter the path to your code signing certificate (ex: ./keysStore/certificate.pem)',
67
+ name: 'codeSigningCertificatePath',
68
+ type: 'text',
69
+ initial: './keysStore/certificate.pem',
70
+ validate: v => {
71
+ try {
72
+ const fullPath = path.resolve(projectDir, v);
73
+ // eslint-disable-next-line
74
+ const fileExists = fs.existsSync(fullPath);
75
+ if (!fileExists) {
76
+ Log.newLine();
77
+ Log.error('File does not exist');
78
+ return false;
79
+ }
80
+ // eslint-disable-next-line
81
+ const key = fs.readFileSync(fullPath, 'utf8');
82
+ if (!key) {
83
+ Log.error('Empty key');
84
+ return false;
85
+ }
86
+ return true;
87
+ } catch {
88
+ return false;
89
+ }
90
+ },
91
+ });
92
+ const newUpdateConfig = {
93
+ url: manifestEndpoint,
94
+ codeSigningMetadata: {
95
+ keyid: 'main',
96
+ alg: 'rsa-v1_5-sha256' as const,
97
+ },
98
+ codeSigningCertificate: codeSigningCertificatePath,
99
+ enabled: true,
100
+ requestHeaders: {
101
+ 'expo-channel-name': 'process.env.RELEASE_CHANNEL',
102
+ },
103
+ };
104
+ const updateConfigSpinner = ora('Updating Expo config').start();
105
+ try {
106
+ await createOrModifyExpoConfigAsync(projectDir, {
107
+ updates: newUpdateConfig,
108
+ });
109
+ updateConfigSpinner.succeed(
110
+ 'Expo config successfully updated do not forget to format the file with prettier or eslint'
111
+ );
112
+ } catch (e) {
113
+ updateConfigSpinner.fail('Failed to update Expo config');
114
+ Log.error(e);
115
+ }
116
+ }
117
+ }