@capgo/capacitor-ssl-pinning 8.1.6 → 8.1.8
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 +3 -2
- package/scripts/generate-ai-changelog.mjs +135 -0
- package/scripts/prepare-certs.mjs +29 -2
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.8",
|
|
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,7 +55,8 @@
|
|
|
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"
|
|
58
|
+
"example:build": "bun run build && cd example-app && bun install --frozen-lockfile && bun run build",
|
|
59
|
+
"changelog:ai": "node scripts/generate-ai-changelog.mjs"
|
|
59
60
|
},
|
|
60
61
|
"devDependencies": {
|
|
61
62
|
"@capacitor/android": "^8.0.0",
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Generate a release changelog with Cloudflare Workers AI in CI.
|
|
4
|
+
* Invoked from build.yml on tag releases via `bun run changelog:ai`.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { execFileSync } from 'node:child_process';
|
|
8
|
+
import { appendFileSync } from 'node:fs';
|
|
9
|
+
|
|
10
|
+
const DEFAULT_MODEL = '@cf/moonshotai/kimi-k2.7-code';
|
|
11
|
+
|
|
12
|
+
function parseArgs(argv) {
|
|
13
|
+
const args = { fromTag: process.env.FROM_TAG, toTag: process.env.TO_TAG };
|
|
14
|
+
for (let i = 0; i < argv.length; i++) {
|
|
15
|
+
if (argv[i] === '--from-tag') args.fromTag = argv[++i];
|
|
16
|
+
if (argv[i] === '--to-tag') args.toTag = argv[++i];
|
|
17
|
+
if (argv[i] === '--model') args.model = argv[++i];
|
|
18
|
+
}
|
|
19
|
+
return args;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function git(args) {
|
|
23
|
+
return execFileSync('/usr/bin/git', args, { encoding: 'utf8' }).trim();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function resolveTags({ fromTag, toTag }) {
|
|
27
|
+
const currentTag =
|
|
28
|
+
toTag ??
|
|
29
|
+
(process.env.GITHUB_REF?.startsWith('refs/tags/') ? process.env.GITHUB_REF.replace('refs/tags/', '') : null);
|
|
30
|
+
|
|
31
|
+
if (!currentTag) {
|
|
32
|
+
throw new Error('Missing target tag. Set GITHUB_REF to refs/tags/<tag> or pass --to-tag.');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const previousTag = fromTag ?? git(['describe', '--tags', '--abbrev=0', `${currentTag}^`]);
|
|
36
|
+
return { previousTag, currentTag };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function buildPrompt(previousTag, currentTag) {
|
|
40
|
+
const commits = git(['log', `${previousTag}..${currentTag}`, '--pretty=format:%s']);
|
|
41
|
+
const diffLines = git(['diff', '--stat', `${previousTag}..${currentTag}`]).split('\n');
|
|
42
|
+
const diffStat = diffLines.at(-1)?.trim() ?? '';
|
|
43
|
+
|
|
44
|
+
return `You are a technical writer creating a changelog for a software project.
|
|
45
|
+
|
|
46
|
+
There are changes in the git repository between two points:
|
|
47
|
+
- From tag: ${previousTag}
|
|
48
|
+
- To tag: ${currentTag}
|
|
49
|
+
- Commits:
|
|
50
|
+
${commits || '(none)'}
|
|
51
|
+
- Diff summary: ${diffStat || '(no file changes)'}
|
|
52
|
+
|
|
53
|
+
## Instructions:
|
|
54
|
+
1. **Categorize changes** using standard changelog categories:
|
|
55
|
+
- **Added**: New features
|
|
56
|
+
- **Changed**: Changes in existing functionality
|
|
57
|
+
- **Deprecated**: Soon-to-be removed features
|
|
58
|
+
- **Removed**: Removed features
|
|
59
|
+
- **Fixed**: Bug fixes
|
|
60
|
+
- **Security**: Security improvements
|
|
61
|
+
|
|
62
|
+
2. **Write clear, user-focused descriptions** that explain:
|
|
63
|
+
- What changed from a user's perspective
|
|
64
|
+
- Why it matters
|
|
65
|
+
- Any breaking changes or migration notes
|
|
66
|
+
|
|
67
|
+
3. **Use consistent formatting**:
|
|
68
|
+
- Each item should be a concise bullet point
|
|
69
|
+
- Start with an action verb when possible
|
|
70
|
+
- Group related changes together
|
|
71
|
+
|
|
72
|
+
4. **Focus on semantic meaning** rather than technical implementation details
|
|
73
|
+
|
|
74
|
+
Output ONLY the changelog content in markdown format. Do NOT include any explanatory text, metadata, introductory phrases like "Based on the git analysis" or "Here's the changelog", or concluding remarks. Start IMMEDIATELY with the changelog categories and items (e.g., "## Added"). Do not include version numbers or dates. Do not preface your response with any text whatsoever.`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function generateChangelog(prompt, model) {
|
|
78
|
+
const accountId = process.env.CLOUDFLARE_ACCOUNT_ID;
|
|
79
|
+
const apiToken = process.env.CLOUDFLARE_API_TOKEN;
|
|
80
|
+
|
|
81
|
+
if (!accountId || !apiToken) {
|
|
82
|
+
throw new Error('CLOUDFLARE_ACCOUNT_ID and CLOUDFLARE_API_TOKEN are required.');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const url = `https://api.cloudflare.com/client/v4/accounts/${accountId}/ai/run/${model}`;
|
|
86
|
+
const response = await fetch(url, {
|
|
87
|
+
method: 'POST',
|
|
88
|
+
headers: {
|
|
89
|
+
Authorization: `Bearer ${apiToken}`,
|
|
90
|
+
'Content-Type': 'application/json',
|
|
91
|
+
},
|
|
92
|
+
body: JSON.stringify({
|
|
93
|
+
messages: [{ role: 'user', content: prompt }],
|
|
94
|
+
}),
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const payload = await response.json();
|
|
98
|
+
if (!response.ok || !payload.success) {
|
|
99
|
+
throw new Error(`Cloudflare AI request failed with status ${response.status}`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const content = payload.result?.choices?.[0]?.message?.content ?? payload.result?.response;
|
|
103
|
+
if (!content || typeof content !== 'string') {
|
|
104
|
+
throw new Error('Cloudflare AI returned an empty changelog response');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return content.trim();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function writeGithubOutput({ result, fromTag, toTag }) {
|
|
111
|
+
const outputFile = process.env.GITHUB_OUTPUT;
|
|
112
|
+
if (!outputFile) return;
|
|
113
|
+
|
|
114
|
+
const delimiter = `changelog_${Date.now()}`;
|
|
115
|
+
appendFileSync(outputFile, `result<<${delimiter}\n${result}\n${delimiter}\n`);
|
|
116
|
+
appendFileSync(outputFile, `from_tag=${fromTag}\n`);
|
|
117
|
+
appendFileSync(outputFile, `to_tag=${toTag}\n`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const args = parseArgs(process.argv.slice(2));
|
|
121
|
+
const model = args.model ?? process.env.CLOUDFLARE_AI_MODEL ?? DEFAULT_MODEL;
|
|
122
|
+
const { previousTag, currentTag } = resolveTags(args);
|
|
123
|
+
const prompt = buildPrompt(previousTag, currentTag);
|
|
124
|
+
|
|
125
|
+
console.error(`Generating changelog with ${model}`);
|
|
126
|
+
console.error(`Range: ${previousTag}..${currentTag}`);
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
const result = await generateChangelog(prompt, model);
|
|
130
|
+
writeGithubOutput({ result, fromTag: previousTag, toTag: currentTag });
|
|
131
|
+
} catch (error) {
|
|
132
|
+
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
133
|
+
console.error(`Changelog generation failed: ${message}`);
|
|
134
|
+
process.exit(1);
|
|
135
|
+
}
|
|
@@ -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
|
}
|