@capgo/capacitor-ssl-pinning 8.1.5 → 8.1.7
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/README.md +2 -2
- package/android/src/main/java/io/ionic/sslpinning/CertificateLoader.java +36 -1
- package/dist/esm/definitions.d.ts +4 -0
- package/dist/esm/definitions.js.map +1 -1
- package/ios/Sources/SSLPinningPlugin/SSLPinningSupport.swift +70 -2
- package/package.json +2 -3
- package/scripts/prepare-certs.mjs +29 -2
- package/scripts/deploy-example-capgo.mjs +0 -117
package/README.md
CHANGED
|
@@ -54,7 +54,7 @@ const config: CapacitorConfig = {
|
|
|
54
54
|
enabled: true,
|
|
55
55
|
},
|
|
56
56
|
SSLPinning: {
|
|
57
|
-
certs: ['sslCerts/production/primary.cer', 'sslCerts/production/backup.
|
|
57
|
+
certs: ['sslCerts/production/primary.cer', 'sslCerts/production/backup.der'],
|
|
58
58
|
excludedDomains: ['https://analytics.google.com'],
|
|
59
59
|
},
|
|
60
60
|
},
|
|
@@ -63,7 +63,7 @@ const config: CapacitorConfig = {
|
|
|
63
63
|
export default config;
|
|
64
64
|
```
|
|
65
65
|
|
|
66
|
-
During `bunx cap sync`, the plugin copies the configured certificates into `webDir/certs`, which lets both native implementations load them from the bundled app assets.
|
|
66
|
+
During `bunx cap sync`, the plugin copies the configured certificates into `webDir/certs`, which lets both native implementations load them from the bundled app assets. Both `.cer` and `.der` are supported; PEM `.cer` files are converted to DER automatically for iOS.
|
|
67
67
|
|
|
68
68
|
## Usage
|
|
69
69
|
|
|
@@ -3,6 +3,7 @@ package io.ionic.sslpinning;
|
|
|
3
3
|
import android.content.Context;
|
|
4
4
|
import com.getcapacitor.Bridge;
|
|
5
5
|
import com.getcapacitor.PluginConfig;
|
|
6
|
+
import java.io.FileNotFoundException;
|
|
6
7
|
import java.io.InputStream;
|
|
7
8
|
import java.net.URL;
|
|
8
9
|
import java.security.KeyStore;
|
|
@@ -91,9 +92,43 @@ final class CertificateLoader {
|
|
|
91
92
|
|
|
92
93
|
private static X509Certificate loadCertificate(final Context context, final String configuredCertPath) throws Exception {
|
|
93
94
|
final String fileName = configuredCertPath.substring(configuredCertPath.lastIndexOf('/') + 1);
|
|
95
|
+
final String[] candidateFileNames = candidateFileNames(fileName);
|
|
96
|
+
final CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
|
|
97
|
+
|
|
98
|
+
for (String candidateFileName : candidateFileNames) {
|
|
99
|
+
try (InputStream inputStream = context.getAssets().open(CERTS_DIR + "/" + candidateFileName)) {
|
|
100
|
+
return (X509Certificate) certificateFactory.generateCertificate(inputStream);
|
|
101
|
+
} catch (FileNotFoundException ignored) {
|
|
102
|
+
// Try alternate extension if available.
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Trigger the original exception message for easier debugging.
|
|
94
107
|
try (InputStream inputStream = context.getAssets().open(CERTS_DIR + "/" + fileName)) {
|
|
95
|
-
final CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
|
|
96
108
|
return (X509Certificate) certificateFactory.generateCertificate(inputStream);
|
|
97
109
|
}
|
|
98
110
|
}
|
|
111
|
+
|
|
112
|
+
private static String[] candidateFileNames(final String fileName) {
|
|
113
|
+
if (fileName == null) {
|
|
114
|
+
return new String[0];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
final int dotIndex = fileName.lastIndexOf('.');
|
|
118
|
+
if (dotIndex == -1) {
|
|
119
|
+
return new String[] { fileName };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
final String base = fileName.substring(0, dotIndex);
|
|
123
|
+
final String ext = fileName.substring(dotIndex + 1).toLowerCase();
|
|
124
|
+
|
|
125
|
+
if ("cer".equals(ext)) {
|
|
126
|
+
return new String[] { fileName, base + ".der" };
|
|
127
|
+
}
|
|
128
|
+
if ("der".equals(ext)) {
|
|
129
|
+
return new String[] { fileName, base + ".cer" };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return new String[] { fileName };
|
|
133
|
+
}
|
|
99
134
|
}
|
|
@@ -33,6 +33,10 @@ export interface SSLPinningPluginConfig {
|
|
|
33
33
|
*
|
|
34
34
|
* During `bunx cap sync`, this plugin copies them into `webDir/certs` so native runtimes can load them from
|
|
35
35
|
* the bundled web assets.
|
|
36
|
+
*
|
|
37
|
+
* Both `.cer` and `.der` filenames are supported. iOS requires DER-encoded X.509 certificates; when a
|
|
38
|
+
* certificate file is provided in PEM form (for example a `.cer` containing `-----BEGIN CERTIFICATE-----`),
|
|
39
|
+
* the sync hook converts it to DER while keeping the same filename.
|
|
36
40
|
*/
|
|
37
41
|
certs?: string[];
|
|
38
42
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"definitions.js","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":"","sourcesContent":["/**\n * Plugin version payload.\n */\nexport interface PluginVersionResult {\n /**\n * Version identifier returned by the platform implementation.\n */\n version: string;\n}\n\n/**\n * Static SSL pinning configuration currently visible to the plugin.\n */\nexport interface SSLPinningConfigurationState {\n /**\n * Whether at least one certificate is configured for native pinning.\n */\n configured: boolean;\n\n /**\n * Certificate paths from `capacitor.config.*` relative to the app root.\n */\n certs: string[];\n\n /**\n * Fully-qualified URLs that should bypass SSL pinning.\n */\n excludedDomains: string[];\n}\n\n/**\n * Plugin configuration added under `plugins.SSLPinning` in `capacitor.config.*`.\n */\nexport interface SSLPinningPluginConfig {\n /**\n * Certificate files relative to the application root.\n *\n * During `bunx cap sync`, this plugin copies them into `webDir/certs` so native runtimes can load them from\n * the bundled web assets.\n */\n certs?: string[];\n\n /**\n * Fully-qualified URLs that should bypass pinning.\n *\n * Match is done by origin, and by path-prefix when the configured URL contains a path.\n */\n excludedDomains?: string[];\n}\n\n/**\n * Capacitor API for inspecting SSL pinning configuration.\n *\n * Native enforcement is applied automatically to `CapacitorHttp` requests when:\n * 1. `plugins.CapacitorHttp.enabled` is `true`\n * 2. `plugins.SSLPinning.certs` contains at least one certificate\n */\nexport interface SSLPinningPlugin {\n /**\n * Returns the active native configuration visible to the plugin.\n */\n getConfiguration(): Promise<SSLPinningConfigurationState>;\n\n /**\n * Returns the native implementation version marker.\n */\n getPluginVersion(): Promise<PluginVersionResult>;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"definitions.js","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":"","sourcesContent":["/**\n * Plugin version payload.\n */\nexport interface PluginVersionResult {\n /**\n * Version identifier returned by the platform implementation.\n */\n version: string;\n}\n\n/**\n * Static SSL pinning configuration currently visible to the plugin.\n */\nexport interface SSLPinningConfigurationState {\n /**\n * Whether at least one certificate is configured for native pinning.\n */\n configured: boolean;\n\n /**\n * Certificate paths from `capacitor.config.*` relative to the app root.\n */\n certs: string[];\n\n /**\n * Fully-qualified URLs that should bypass SSL pinning.\n */\n excludedDomains: string[];\n}\n\n/**\n * Plugin configuration added under `plugins.SSLPinning` in `capacitor.config.*`.\n */\nexport interface SSLPinningPluginConfig {\n /**\n * Certificate files relative to the application root.\n *\n * During `bunx cap sync`, this plugin copies them into `webDir/certs` so native runtimes can load them from\n * the bundled web assets.\n *\n * Both `.cer` and `.der` filenames are supported. iOS requires DER-encoded X.509 certificates; when a\n * certificate file is provided in PEM form (for example a `.cer` containing `-----BEGIN CERTIFICATE-----`),\n * the sync hook converts it to DER while keeping the same filename.\n */\n certs?: string[];\n\n /**\n * Fully-qualified URLs that should bypass pinning.\n *\n * Match is done by origin, and by path-prefix when the configured URL contains a path.\n */\n excludedDomains?: string[];\n}\n\n/**\n * Capacitor API for inspecting SSL pinning configuration.\n *\n * Native enforcement is applied automatically to `CapacitorHttp` requests when:\n * 1. `plugins.CapacitorHttp.enabled` is `true`\n * 2. `plugins.SSLPinning.certs` contains at least one certificate\n */\nexport interface SSLPinningPlugin {\n /**\n * Returns the active native configuration visible to the plugin.\n */\n getConfiguration(): Promise<SSLPinningConfigurationState>;\n\n /**\n * Returns the native implementation version marker.\n */\n getPluginVersion(): Promise<PluginVersionResult>;\n}\n"]}
|
|
@@ -85,8 +85,9 @@ final class SSLPinningCertificateStore {
|
|
|
85
85
|
|
|
86
86
|
private func loadCertificate(from configuredPath: String) throws -> SecCertificate {
|
|
87
87
|
let fileName = URL(fileURLWithPath: configuredPath).lastPathComponent
|
|
88
|
-
|
|
89
|
-
|
|
88
|
+
let candidateFileNames = SSLPinningCertificateStore.candidateFileNames(for: fileName)
|
|
89
|
+
|
|
90
|
+
guard let certificateData = SSLPinningCertificateStore.loadBundledCertificateData(candidateFileNames: candidateFileNames),
|
|
90
91
|
let certificate = SecCertificateCreateWithData(nil, certificateData as CFData)
|
|
91
92
|
else {
|
|
92
93
|
throw NSError(
|
|
@@ -98,6 +99,73 @@ final class SSLPinningCertificateStore {
|
|
|
98
99
|
|
|
99
100
|
return certificate
|
|
100
101
|
}
|
|
102
|
+
|
|
103
|
+
private static func candidateFileNames(for fileName: String) -> [String] {
|
|
104
|
+
var candidates = [fileName]
|
|
105
|
+
let url = URL(fileURLWithPath: fileName)
|
|
106
|
+
let ext = url.pathExtension.lowercased()
|
|
107
|
+
let base = url.deletingPathExtension().lastPathComponent
|
|
108
|
+
|
|
109
|
+
if ext == "cer" {
|
|
110
|
+
candidates.append("\(base).der")
|
|
111
|
+
} else if ext == "der" {
|
|
112
|
+
candidates.append("\(base).cer")
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return candidates
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
private static func loadBundledCertificateData(candidateFileNames: [String]) -> Data? {
|
|
119
|
+
guard let resourceURL = Bundle.main.resourceURL else {
|
|
120
|
+
return nil
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
for fileName in candidateFileNames {
|
|
124
|
+
let candidateURL = resourceURL.appendingPathComponent("public/certs/\(fileName)")
|
|
125
|
+
guard let data = try? Data(contentsOf: candidateURL) else {
|
|
126
|
+
continue
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if let decoded = decodePEMCertificateIfNeeded(data) {
|
|
130
|
+
return decoded
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return data
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return nil
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
private static func decodePEMCertificateIfNeeded(_ data: Data) -> Data? {
|
|
140
|
+
guard let text = String(data: data, encoding: .utf8),
|
|
141
|
+
text.contains("-----BEGIN CERTIFICATE-----")
|
|
142
|
+
else {
|
|
143
|
+
return nil
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
let beginMarker = "-----BEGIN CERTIFICATE-----"
|
|
147
|
+
let endMarker = "-----END CERTIFICATE-----"
|
|
148
|
+
|
|
149
|
+
guard let beginRange = text.range(of: beginMarker),
|
|
150
|
+
let endRange = text.range(of: endMarker, range: beginRange.upperBound..<text.endIndex)
|
|
151
|
+
else {
|
|
152
|
+
return nil
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
let base64Body = text[beginRange.upperBound..<endRange.lowerBound]
|
|
156
|
+
.replacingOccurrences(of: "\r", with: "")
|
|
157
|
+
.replacingOccurrences(of: "\n", with: "")
|
|
158
|
+
.replacingOccurrences(of: " ", with: "")
|
|
159
|
+
.replacingOccurrences(of: "\t", with: "")
|
|
160
|
+
|
|
161
|
+
guard let decoded = Data(base64Encoded: String(base64Body), options: [.ignoreUnknownCharacters]),
|
|
162
|
+
!decoded.isEmpty
|
|
163
|
+
else {
|
|
164
|
+
return nil
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return decoded
|
|
168
|
+
}
|
|
101
169
|
}
|
|
102
170
|
|
|
103
171
|
enum SSLPinningTrustEvaluator {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@capgo/capacitor-ssl-pinning",
|
|
3
|
-
"version": "8.1.
|
|
3
|
+
"version": "8.1.7",
|
|
4
4
|
"description": "Capacitor SSL pinning plugin for Android and iOS that integrates with CapacitorHttp.",
|
|
5
5
|
"main": "dist/plugin.cjs.js",
|
|
6
6
|
"module": "dist/esm/index.js",
|
|
@@ -55,8 +55,7 @@
|
|
|
55
55
|
"capacitor:sync:before": "bun run prepare:certs",
|
|
56
56
|
"capacitor:update:before": "bun run prepare:certs",
|
|
57
57
|
"example:install": "cd example-app && bun install --frozen-lockfile",
|
|
58
|
-
"example:build": "bun run build && cd example-app && bun install --frozen-lockfile && bun run build"
|
|
59
|
-
"example:capgo:deploy": "bun run example:build && bun scripts/deploy-example-capgo.mjs"
|
|
58
|
+
"example:build": "bun run build && cd example-app && bun install --frozen-lockfile && bun run build"
|
|
60
59
|
},
|
|
61
60
|
"devDependencies": {
|
|
62
61
|
"@capacitor/android": "^8.0.0",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import { cp, mkdir, readdir, rm } from 'node:fs/promises';
|
|
3
|
+
import { cp, mkdir, readdir, rm, readFile, writeFile } from 'node:fs/promises';
|
|
4
4
|
import { basename, join, resolve } from 'node:path';
|
|
5
5
|
|
|
6
6
|
const rootDir = process.env.CAPACITOR_ROOT_DIR;
|
|
@@ -12,6 +12,20 @@ const fail = (message) => {
|
|
|
12
12
|
throw new Error(`[ssl-pinning] ${message}`);
|
|
13
13
|
};
|
|
14
14
|
|
|
15
|
+
const pemToDer = (pem) => {
|
|
16
|
+
const match = pem.match(/-----BEGIN CERTIFICATE-----([\s\S]*?)-----END CERTIFICATE-----/);
|
|
17
|
+
if (!match) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const base64 = match[1].replace(/[\r\n\s]/g, '');
|
|
22
|
+
if (!base64) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return Buffer.from(base64, 'base64');
|
|
27
|
+
};
|
|
28
|
+
|
|
15
29
|
if (!rootDir || !webDir || !configJson) {
|
|
16
30
|
log('Capacitor hook environment not detected, skipping certificate preparation.');
|
|
17
31
|
process.exit(0);
|
|
@@ -46,6 +60,19 @@ for (const existingEntry of await readdir(targetDir, { withFileTypes: true })) {
|
|
|
46
60
|
}
|
|
47
61
|
|
|
48
62
|
for (const { source, fileName } of sourceFiles) {
|
|
49
|
-
|
|
63
|
+
const targetPath = join(targetDir, fileName);
|
|
64
|
+
const buffer = await readFile(source);
|
|
65
|
+
const head = buffer.subarray(0, 2048).toString('utf8');
|
|
66
|
+
if (head.includes('-----BEGIN CERTIFICATE-----')) {
|
|
67
|
+
const der = pemToDer(buffer.toString('utf8'));
|
|
68
|
+
if (!der || der.length === 0) {
|
|
69
|
+
fail(`Failed to decode PEM certificate ${fileName}.`);
|
|
70
|
+
}
|
|
71
|
+
await writeFile(targetPath, der);
|
|
72
|
+
log(`Converted PEM ${fileName} into DER at ${targetDir}`);
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
await cp(source, targetPath, { force: true });
|
|
50
77
|
log(`Copied ${fileName} into ${targetDir}`);
|
|
51
78
|
}
|
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
3
|
-
import { dirname, resolve } from 'node:path';
|
|
4
|
-
import { fileURLToPath } from 'node:url';
|
|
5
|
-
import { spawnSync } from 'node:child_process';
|
|
6
|
-
|
|
7
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
|
-
const repoRoot = resolve(__dirname, '..');
|
|
9
|
-
const appDir = resolve(repoRoot, 'example-app');
|
|
10
|
-
const rootPackageJson = JSON.parse(readFileSync(resolve(repoRoot, 'package.json'), 'utf8'));
|
|
11
|
-
const examplePackageJson = JSON.parse(readFileSync(resolve(appDir, 'package.json'), 'utf8'));
|
|
12
|
-
const tsConfigPath = resolve(appDir, 'capacitor.config.ts');
|
|
13
|
-
const jsonConfigPath = resolve(appDir, 'capacitor.config.json');
|
|
14
|
-
const configPath = existsSync(tsConfigPath) ? tsConfigPath : existsSync(jsonConfigPath) ? jsonConfigPath : null;
|
|
15
|
-
|
|
16
|
-
if (!configPath) {
|
|
17
|
-
console.error(
|
|
18
|
-
'Missing Capacitor config. Expected example-app/capacitor.config.ts or example-app/capacitor.config.json.',
|
|
19
|
-
);
|
|
20
|
-
process.exit(1);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const configText = readFileSync(configPath, 'utf8');
|
|
24
|
-
|
|
25
|
-
function readConfigString(key, fallback) {
|
|
26
|
-
const match = configText.match(new RegExp(`['"]?${key}['"]?\\s*[:=]\\s*['"]([^'"]+)['"]`));
|
|
27
|
-
return match?.[1] ?? fallback;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function runCapgo(args, allowFailure = false) {
|
|
31
|
-
const token = process.env.CAPGO_TOKEN;
|
|
32
|
-
const fullArgs = ['@capgo/cli@8.5.3', ...args];
|
|
33
|
-
if (token) {
|
|
34
|
-
fullArgs.push('--apikey', token);
|
|
35
|
-
}
|
|
36
|
-
const result = spawnSync('bunx', fullArgs, {
|
|
37
|
-
cwd: appDir,
|
|
38
|
-
stdio: 'inherit',
|
|
39
|
-
env: process.env,
|
|
40
|
-
});
|
|
41
|
-
if (!allowFailure && result.status !== 0) {
|
|
42
|
-
process.exit(result.status ?? 1);
|
|
43
|
-
}
|
|
44
|
-
return result.status ?? 1;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const appId = process.env.CAPGO_APP_ID || readConfigString('appId');
|
|
48
|
-
const appName = process.env.CAPGO_APP_NAME || readConfigString('appName', rootPackageJson.name);
|
|
49
|
-
const webDir = process.env.CAPGO_WEB_DIR || readConfigString('webDir', 'dist');
|
|
50
|
-
const distDir = resolve(appDir, webDir);
|
|
51
|
-
const channel = process.env.CAPGO_CHANNEL || process.argv[2] || 'production';
|
|
52
|
-
function usableVersion(version) {
|
|
53
|
-
return version && version !== '0.0.0' ? version : undefined;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const bundle =
|
|
57
|
-
process.env.CAPGO_BUNDLE || usableVersion(examplePackageJson.version) || usableVersion(rootPackageJson.version);
|
|
58
|
-
const comment =
|
|
59
|
-
process.env.CAPGO_COMMENT ||
|
|
60
|
-
(process.env.GITHUB_RUN_NUMBER ? `${appName} run ${process.env.GITHUB_RUN_NUMBER}` : `${appName} ${bundle}`);
|
|
61
|
-
const iconPath = process.env.CAPGO_ICON || resolve(appDir, 'assets', 'capgo-icon.png');
|
|
62
|
-
|
|
63
|
-
if (!appId) {
|
|
64
|
-
console.error('Missing Capgo app id. Set CAPGO_APP_ID or example-app/capacitor.config.* appId.');
|
|
65
|
-
process.exit(1);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
if (!existsSync(distDir)) {
|
|
69
|
-
console.error(`Missing example app bundle at ${distDir}. Run bun run example:build first.`);
|
|
70
|
-
process.exit(1);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
if (!bundle) {
|
|
74
|
-
console.error(
|
|
75
|
-
'Missing Capgo bundle version. Set CAPGO_BUNDLE or add a version to example-app/package.json or package.json.',
|
|
76
|
-
);
|
|
77
|
-
process.exit(1);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const appArgs = [appId, '--name', appName];
|
|
81
|
-
if (existsSync(iconPath)) {
|
|
82
|
-
appArgs.push('--icon', iconPath);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
const setStatus = runCapgo(['app', 'set', ...appArgs], true);
|
|
86
|
-
if (setStatus !== 0) {
|
|
87
|
-
runCapgo(['app', 'add', ...appArgs]);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
console.log(`Deploying ${appId} to Capgo channel "${channel}"`);
|
|
91
|
-
const channelArgs = ['channel', 'add', channel, appId];
|
|
92
|
-
if (channel === 'production') {
|
|
93
|
-
channelArgs.push('--default');
|
|
94
|
-
}
|
|
95
|
-
runCapgo(channelArgs, true);
|
|
96
|
-
|
|
97
|
-
runCapgo([
|
|
98
|
-
'bundle',
|
|
99
|
-
'upload',
|
|
100
|
-
appId,
|
|
101
|
-
'--bundle',
|
|
102
|
-
bundle,
|
|
103
|
-
'--path',
|
|
104
|
-
webDir,
|
|
105
|
-
'--channel',
|
|
106
|
-
channel,
|
|
107
|
-
'--package-json',
|
|
108
|
-
'package.json,../package.json',
|
|
109
|
-
'--node-modules',
|
|
110
|
-
'node_modules,../node_modules',
|
|
111
|
-
'--delta',
|
|
112
|
-
'--no-key',
|
|
113
|
-
'--ignore-checksum-check',
|
|
114
|
-
'--version-exists-ok',
|
|
115
|
-
'--comment',
|
|
116
|
-
comment,
|
|
117
|
-
]);
|