pakstr 0.6.0 ā 0.8.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/android-template/app/build.gradle.kts +23 -45
- package/android-template/app/src/main/java/com/pakstr/app/MainActivity.kt +72 -6
- package/android-template/app/src/main/java/com/pakstr/app/debug/DeveloperToolsAccessManager.kt +14 -0
- package/android-template/app/src/main/java/com/pakstr/app/debug/SupportUnlock.kt +44 -0
- package/android-template/app/src/main/java/com/pakstr/app/debug/SupportUnlockGestureLayout.kt +87 -0
- package/android-template/app/src/main/res/layout/activity_layout.xml +2 -2
- package/android-template/app/src/main/res/values/strings.xml +1 -0
- package/android-template/gradle.properties +1 -2
- package/dist/android/releaseVerification.js +83 -0
- package/dist/android/releaseWorkspace.js +44 -0
- package/dist/android/template.js +7 -2
- package/dist/commands/build.js +144 -108
- package/dist/core/manifest.js +10 -0
- package/dist/core/releaseSigning.js +134 -0
- package/dist/runners/DockerGradleRunner.js +291 -37
- package/dist/runners/LocalGradleRunner.js +91 -30
- package/dist/runners/process.js +42 -0
- package/package.json +2 -1
package/dist/commands/build.js
CHANGED
|
@@ -8,140 +8,177 @@ const fs_1 = __importDefault(require("fs"));
|
|
|
8
8
|
const path_1 = __importDefault(require("path"));
|
|
9
9
|
const child_process_1 = require("child_process");
|
|
10
10
|
const gradle_1 = require("../android/gradle");
|
|
11
|
-
const validateProject_1 = require("../core/validateProject");
|
|
12
|
-
const buildContext_1 = require("../core/buildContext");
|
|
13
|
-
const template_1 = require("../android/template");
|
|
14
|
-
const createRunner_1 = require("../runners/createRunner");
|
|
15
|
-
const config_1 = require("../core/config");
|
|
16
11
|
const branding_1 = require("../android/branding");
|
|
17
|
-
const version_1 = require("../android/version");
|
|
18
12
|
const icon_1 = require("../android/icon");
|
|
19
|
-
const splash_1 = require("../android/splash");
|
|
20
13
|
const permissions_1 = require("../android/permissions");
|
|
14
|
+
const releaseWorkspace_1 = require("../android/releaseWorkspace");
|
|
15
|
+
const splash_1 = require("../android/splash");
|
|
16
|
+
const template_1 = require("../android/template");
|
|
17
|
+
const version_1 = require("../android/version");
|
|
18
|
+
const buildContext_1 = require("../core/buildContext");
|
|
19
|
+
const config_1 = require("../core/config");
|
|
20
|
+
const releaseSigning_1 = require("../core/releaseSigning");
|
|
21
|
+
const validateProject_1 = require("../core/validateProject");
|
|
22
|
+
const createRunner_1 = require("../runners/createRunner");
|
|
21
23
|
const runtimeConfig_1 = require("../runtime/runtimeConfig");
|
|
22
24
|
async function buildCommand(args) {
|
|
23
25
|
const ctx = (0, buildContext_1.createBuildContext)(args);
|
|
24
26
|
const config = (0, config_1.loadConfig)(process.cwd());
|
|
25
27
|
const manifest = ctx.manifest;
|
|
26
|
-
console.log("š Manifest loaded");
|
|
27
|
-
const distPath = ctx.dist;
|
|
28
|
-
const androidRoot = await (0, template_1.ensureTemplate)();
|
|
29
28
|
const mode = ctx.mode;
|
|
30
|
-
const apkOutput = ctx.out;
|
|
31
|
-
const assetsTarget = path_1.default.join(androidRoot, "app/src/main/assets/www");
|
|
32
29
|
const appName = manifest?.app?.name || manifest?.appName || "Pakstr App";
|
|
30
|
+
const finalPath = path_1.default.resolve(ctx.out);
|
|
33
31
|
console.log("\nš Pakstr CLI");
|
|
34
32
|
console.log("š¦ App:", appName);
|
|
35
|
-
console.log("š Web:",
|
|
36
|
-
console.log("š± Out:",
|
|
37
|
-
console.log("š± Android:", androidRoot);
|
|
33
|
+
console.log("š Web:", ctx.dist);
|
|
34
|
+
console.log("š± Out:", finalPath);
|
|
38
35
|
console.log("šļø Builder:", config.builder);
|
|
39
36
|
console.log("š BUILD MODE:", mode.toUpperCase());
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
37
|
+
(0, validateProject_1.validateProject)(ctx.dist, manifest);
|
|
38
|
+
validateOutputParent(finalPath);
|
|
39
|
+
if (mode === "release") {
|
|
40
|
+
removeExistingOutput(finalPath);
|
|
41
|
+
}
|
|
42
|
+
const signingInput = mode === "release" ? (0, releaseSigning_1.readReleaseSigningInput)() : undefined;
|
|
43
|
+
const templateRoot = await (0, template_1.ensureTemplate)();
|
|
44
|
+
let workspace;
|
|
45
|
+
let signing;
|
|
46
|
+
let stagedPath;
|
|
47
|
+
let primaryError;
|
|
48
|
+
let cleanupError;
|
|
49
|
+
try {
|
|
50
|
+
const androidRoot = mode === "release"
|
|
51
|
+
? (workspace = (0, releaseWorkspace_1.createReleaseWorkspace)(templateRoot)).androidRoot
|
|
52
|
+
: templateRoot;
|
|
53
|
+
console.log("š± Android:", androidRoot);
|
|
54
|
+
await prepareAndroidProject(androidRoot, ctx.dist, manifest, appName);
|
|
55
|
+
if (signingInput) {
|
|
56
|
+
signing = (0, releaseSigning_1.createReleaseSigningContext)(signingInput);
|
|
57
|
+
signingInput.keystoreBase64 = "";
|
|
58
|
+
}
|
|
59
|
+
console.log("\nāļø Running build runner...");
|
|
60
|
+
const result = await (0, createRunner_1.createRunner)(config).build({
|
|
61
|
+
androidRoot,
|
|
62
|
+
mode,
|
|
63
|
+
expectedApplicationId: manifest.appId,
|
|
64
|
+
signing,
|
|
65
|
+
});
|
|
66
|
+
if (mode === "release" && !result.verification) {
|
|
67
|
+
throw new Error("Release runner returned no verification evidence");
|
|
68
|
+
}
|
|
69
|
+
stagedPath = stageArtifact(result.artifactPath, finalPath);
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
primaryError = error;
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
signing?.cleanup();
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
cleanupError = error;
|
|
79
|
+
}
|
|
80
|
+
try {
|
|
81
|
+
workspace?.cleanup();
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
cleanupError ??= error;
|
|
85
|
+
}
|
|
86
|
+
if (primaryError || cleanupError) {
|
|
87
|
+
removeStagedArtifact(stagedPath);
|
|
88
|
+
removeExistingOutput(finalPath);
|
|
89
|
+
if (primaryError) {
|
|
90
|
+
if (cleanupError) {
|
|
91
|
+
console.error("Cleanup also failed:", safeErrorMessage(cleanupError));
|
|
92
|
+
}
|
|
93
|
+
throw primaryError;
|
|
94
|
+
}
|
|
95
|
+
throw cleanupError;
|
|
96
|
+
}
|
|
97
|
+
if (!stagedPath) {
|
|
98
|
+
throw new Error("Build completed without a staged APK");
|
|
99
|
+
}
|
|
100
|
+
publishArtifact(stagedPath, finalPath);
|
|
101
|
+
console.log("\nš BUILD COMPLETE");
|
|
102
|
+
console.log("App:", appName);
|
|
103
|
+
console.log("Mode:", mode.toUpperCase());
|
|
104
|
+
console.log("š¦ Output:", finalPath);
|
|
105
|
+
openFolder(finalPath);
|
|
106
|
+
}
|
|
107
|
+
async function prepareAndroidProject(androidRoot, distPath, manifest, appName) {
|
|
108
|
+
const assetsTarget = path_1.default.join(androidRoot, "app/src/main/assets/www");
|
|
44
109
|
if (!fs_1.default.existsSync(distPath)) {
|
|
45
|
-
throw new Error("
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
// -----------------------------
|
|
50
|
-
console.log("\nš¦ Copying web ā Android assets...");
|
|
51
|
-
fs_1.default.rmSync(assetsTarget, {
|
|
52
|
-
recursive: true,
|
|
53
|
-
force: true,
|
|
54
|
-
});
|
|
55
|
-
fs_1.default.mkdirSync(assetsTarget, {
|
|
56
|
-
recursive: true,
|
|
57
|
-
});
|
|
110
|
+
throw new Error("Web assets not found");
|
|
111
|
+
}
|
|
112
|
+
fs_1.default.rmSync(assetsTarget, { recursive: true, force: true });
|
|
113
|
+
fs_1.default.mkdirSync(assetsTarget, { recursive: true });
|
|
58
114
|
copyFolder(distPath, assetsTarget);
|
|
59
|
-
console.log("š¦ Web assets copied successfully");
|
|
60
115
|
(0, runtimeConfig_1.generateRuntimeConfig)(assetsTarget, manifest);
|
|
61
|
-
console.log("runtime files:", fs_1.default.readdirSync(assetsTarget)
|
|
62
|
-
.filter(f => f.includes("pakstr")));
|
|
63
|
-
console.log(fs_1.default.existsSync(path_1.default.join(assetsTarget, "pakstr-runtime.json")));
|
|
64
|
-
console.log(fs_1.default.readdirSync(assetsTarget));
|
|
65
|
-
// -----------------------------
|
|
66
|
-
// 3. PATCH ANDROID PROJECT
|
|
67
|
-
// -----------------------------
|
|
68
116
|
(0, gradle_1.patchGradle)(androidRoot, manifest);
|
|
69
|
-
console.log("āļø Gradle patched");
|
|
70
|
-
// -----------------------------
|
|
71
|
-
// 3.1 PATCH BRANDING
|
|
72
|
-
// -----------------------------
|
|
73
117
|
(0, branding_1.patchAppName)(androidRoot, appName);
|
|
74
118
|
(0, branding_1.patchPackageName)(androidRoot, manifest.appId);
|
|
75
119
|
(0, version_1.patchVersion)(androidRoot, manifest.versionCode, manifest.versionName);
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
await (0, icon_1.patchIcon)(androidRoot, iconPath, manifest.backgroundColor ?? "#FFFFFF");
|
|
120
|
+
if (manifest.ui?.icon) {
|
|
121
|
+
await (0, icon_1.patchIcon)(androidRoot, manifest.ui.icon, manifest.backgroundColor ?? "#FFFFFF");
|
|
79
122
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
console.log("SPLASH IMAGE:", splash.image);
|
|
83
|
-
console.log("SPLASH BACKGROUND:", splash.background);
|
|
84
|
-
await (0, splash_1.patchSplash)(androidRoot, splash.image, splash.background ?? "#FFFFFF");
|
|
123
|
+
if (manifest.ui?.splash) {
|
|
124
|
+
await (0, splash_1.patchSplash)(androidRoot, manifest.ui.splash.image, manifest.ui.splash.background ?? "#FFFFFF");
|
|
85
125
|
}
|
|
86
126
|
(0, permissions_1.patchPermissions)(androidRoot, manifest);
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
console.log("\nš¦ APK GENERATED:");
|
|
97
|
-
console.log(apkPath);
|
|
98
|
-
}
|
|
99
|
-
catch (err) {
|
|
100
|
-
console.error("ā Build failed");
|
|
101
|
-
console.error(err);
|
|
102
|
-
throw err;
|
|
103
|
-
}
|
|
104
|
-
// -----------------------------
|
|
105
|
-
// 5. COPY APK TO OUTPUT
|
|
106
|
-
// -----------------------------
|
|
107
|
-
const finalPath = path_1.default.resolve(apkOutput);
|
|
108
|
-
fs_1.default.mkdirSync(path_1.default.dirname(finalPath), {
|
|
109
|
-
recursive: true,
|
|
110
|
-
});
|
|
127
|
+
}
|
|
128
|
+
function validateOutputParent(finalPath) {
|
|
129
|
+
const parent = path_1.default.dirname(finalPath);
|
|
130
|
+
if (!fs_1.default.existsSync(parent) || !fs_1.default.lstatSync(parent).isDirectory()) {
|
|
131
|
+
throw new Error(`Output directory must already exist: ${parent}`);
|
|
132
|
+
}
|
|
133
|
+
if (fs_1.default.lstatSync(parent).isSymbolicLink()) {
|
|
134
|
+
throw new Error(`Output directory must not be a symbolic link: ${parent}`);
|
|
135
|
+
}
|
|
111
136
|
if (fs_1.default.existsSync(finalPath)) {
|
|
112
137
|
const stat = fs_1.default.lstatSync(finalPath);
|
|
113
|
-
if (stat.
|
|
114
|
-
|
|
115
|
-
recursive: true,
|
|
116
|
-
force: true,
|
|
117
|
-
});
|
|
138
|
+
if (!stat.isFile() || stat.isSymbolicLink()) {
|
|
139
|
+
throw new Error(`Output path must be a regular file: ${finalPath}`);
|
|
118
140
|
}
|
|
119
141
|
}
|
|
120
|
-
fs_1.default.copyFileSync(apkPath, finalPath);
|
|
121
|
-
console.log("\nš¦ CLEAN OUTPUT:");
|
|
122
|
-
console.log(finalPath);
|
|
123
|
-
openFolder(finalPath);
|
|
124
|
-
// -----------------------------
|
|
125
|
-
// 6. FINAL OUTPUT
|
|
126
|
-
// -----------------------------
|
|
127
|
-
console.log("\nš BUILD COMPLETE");
|
|
128
|
-
console.log("App:", appName);
|
|
129
|
-
console.log("Mode:", mode.toUpperCase());
|
|
130
142
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
143
|
+
function stageArtifact(artifactPath, finalPath) {
|
|
144
|
+
const parent = path_1.default.dirname(finalPath);
|
|
145
|
+
const stageDir = fs_1.default.mkdtempSync(path_1.default.join(parent, ".pakstr-stage-"));
|
|
146
|
+
const stagedPath = path_1.default.join(stageDir, "app.apk");
|
|
147
|
+
fs_1.default.copyFileSync(artifactPath, stagedPath, fs_1.default.constants.COPYFILE_EXCL);
|
|
148
|
+
if (fs_1.default.statSync(stagedPath).size !== fs_1.default.statSync(artifactPath).size) {
|
|
149
|
+
fs_1.default.rmSync(stageDir, { recursive: true, force: true });
|
|
150
|
+
throw new Error("Staged APK size does not match verified artifact");
|
|
151
|
+
}
|
|
152
|
+
return stagedPath;
|
|
153
|
+
}
|
|
154
|
+
function publishArtifact(stagedPath, finalPath) {
|
|
155
|
+
removeExistingOutput(finalPath);
|
|
156
|
+
fs_1.default.renameSync(stagedPath, finalPath);
|
|
157
|
+
fs_1.default.rmSync(path_1.default.dirname(stagedPath), { recursive: true, force: true });
|
|
158
|
+
if (!fs_1.default.existsSync(finalPath) || !fs_1.default.lstatSync(finalPath).isFile()) {
|
|
159
|
+
throw new Error("Final APK publication failed");
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
function removeExistingOutput(finalPath) {
|
|
163
|
+
if (!fs_1.default.existsSync(finalPath))
|
|
164
|
+
return;
|
|
165
|
+
const stat = fs_1.default.lstatSync(finalPath);
|
|
166
|
+
if (!stat.isFile() || stat.isSymbolicLink()) {
|
|
167
|
+
throw new Error(`Refusing to remove non-regular output path: ${finalPath}`);
|
|
168
|
+
}
|
|
169
|
+
fs_1.default.unlinkSync(finalPath);
|
|
170
|
+
}
|
|
171
|
+
function removeStagedArtifact(stagedPath) {
|
|
172
|
+
if (!stagedPath)
|
|
173
|
+
return;
|
|
174
|
+
fs_1.default.rmSync(path_1.default.dirname(stagedPath), { recursive: true, force: true });
|
|
175
|
+
}
|
|
134
176
|
function copyFolder(src, dest) {
|
|
135
|
-
const
|
|
136
|
-
withFileTypes: true,
|
|
137
|
-
});
|
|
138
|
-
for (const entry of entries) {
|
|
177
|
+
for (const entry of fs_1.default.readdirSync(src, { withFileTypes: true })) {
|
|
139
178
|
const srcPath = path_1.default.join(src, entry.name);
|
|
140
179
|
const destPath = path_1.default.join(dest, entry.name);
|
|
141
180
|
if (entry.isDirectory()) {
|
|
142
|
-
fs_1.default.mkdirSync(destPath, {
|
|
143
|
-
recursive: true,
|
|
144
|
-
});
|
|
181
|
+
fs_1.default.mkdirSync(destPath, { recursive: true });
|
|
145
182
|
copyFolder(srcPath, destPath);
|
|
146
183
|
}
|
|
147
184
|
else {
|
|
@@ -149,17 +186,16 @@ function copyFolder(src, dest) {
|
|
|
149
186
|
}
|
|
150
187
|
}
|
|
151
188
|
}
|
|
152
|
-
// -----------------------------
|
|
153
|
-
// OPEN FOLDER (macOS)
|
|
154
|
-
// -----------------------------
|
|
155
189
|
function openFolder(filePath) {
|
|
156
|
-
if (process.platform !== "darwin")
|
|
190
|
+
if (process.platform !== "darwin" || process.env.CI)
|
|
157
191
|
return;
|
|
158
|
-
}
|
|
159
192
|
try {
|
|
160
|
-
(0, child_process_1.
|
|
193
|
+
(0, child_process_1.execFileSync)("open", [path_1.default.dirname(filePath)], { stdio: "ignore" });
|
|
161
194
|
}
|
|
162
195
|
catch {
|
|
163
|
-
//
|
|
196
|
+
// Opening the output folder is optional.
|
|
164
197
|
}
|
|
165
198
|
}
|
|
199
|
+
function safeErrorMessage(error) {
|
|
200
|
+
return error instanceof Error ? error.message : "unknown cleanup error";
|
|
201
|
+
}
|
package/dist/core/manifest.js
CHANGED
|
@@ -16,6 +16,16 @@ function loadManifest(path) {
|
|
|
16
16
|
function validateManifest(m) {
|
|
17
17
|
if (!m.appId)
|
|
18
18
|
throw new Error("ā appId is required");
|
|
19
|
+
const appIdPattern = /^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)+$/;
|
|
20
|
+
if (m.appId.length < 3 ||
|
|
21
|
+
m.appId.length > 255 ||
|
|
22
|
+
!appIdPattern.test(m.appId)) {
|
|
23
|
+
throw new Error("ā appId must be a lowercase reverse-domain Android Application ID");
|
|
24
|
+
}
|
|
25
|
+
const debugAppId = `${m.appId}.debug`;
|
|
26
|
+
if (debugAppId.length > 255 || !appIdPattern.test(debugAppId)) {
|
|
27
|
+
throw new Error("ā appId is too long to derive the debug Application ID");
|
|
28
|
+
}
|
|
19
29
|
if (!m.appName)
|
|
20
30
|
throw new Error("ā appName is required");
|
|
21
31
|
if (!m.versionName)
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.INTERNAL_KEYSTORE_PATH_ENV = exports.PUBLIC_SIGNING_ENV = void 0;
|
|
7
|
+
exports.readReleaseSigningInput = readReleaseSigningInput;
|
|
8
|
+
exports.createReleaseSigningContext = createReleaseSigningContext;
|
|
9
|
+
exports.removeSigningVariables = removeSigningVariables;
|
|
10
|
+
exports.createGradleSigningEnvironment = createGradleSigningEnvironment;
|
|
11
|
+
const fs_1 = __importDefault(require("fs"));
|
|
12
|
+
const os_1 = __importDefault(require("os"));
|
|
13
|
+
const path_1 = __importDefault(require("path"));
|
|
14
|
+
exports.PUBLIC_SIGNING_ENV = {
|
|
15
|
+
keystoreBase64: "PAKSTR_ANDROID_KEYSTORE_BASE64",
|
|
16
|
+
keystorePassword: "PAKSTR_ANDROID_KEYSTORE_PASSWORD",
|
|
17
|
+
keyAlias: "PAKSTR_ANDROID_KEY_ALIAS",
|
|
18
|
+
keyPassword: "PAKSTR_ANDROID_KEY_PASSWORD",
|
|
19
|
+
};
|
|
20
|
+
exports.INTERNAL_KEYSTORE_PATH_ENV = "PAKSTR_ANDROID_KEYSTORE_PATH";
|
|
21
|
+
const MAX_KEYSTORE_BYTES = 16 * 1024 * 1024;
|
|
22
|
+
const CANONICAL_BASE64 = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
|
|
23
|
+
function readReleaseSigningInput(env = process.env) {
|
|
24
|
+
const keystoreBase64 = requireValue(env, exports.PUBLIC_SIGNING_ENV.keystoreBase64);
|
|
25
|
+
const storePassword = requireValue(env, exports.PUBLIC_SIGNING_ENV.keystorePassword);
|
|
26
|
+
const keyAlias = requireValue(env, exports.PUBLIC_SIGNING_ENV.keyAlias);
|
|
27
|
+
const keyPassword = requireValue(env, exports.PUBLIC_SIGNING_ENV.keyPassword);
|
|
28
|
+
validateCanonicalBase64(keystoreBase64);
|
|
29
|
+
return {
|
|
30
|
+
keystoreBase64,
|
|
31
|
+
storePassword,
|
|
32
|
+
keyAlias,
|
|
33
|
+
keyPassword,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
function createReleaseSigningContext(input, temporaryRoot = os_1.default.tmpdir()) {
|
|
37
|
+
let decoded = decodeKeystore(input.keystoreBase64);
|
|
38
|
+
fs_1.default.mkdirSync(temporaryRoot, { recursive: true, mode: 0o700 });
|
|
39
|
+
const signingDir = fs_1.default.mkdtempSync(path_1.default.join(temporaryRoot, "pakstr-signing-"));
|
|
40
|
+
const keystorePath = path_1.default.join(signingDir, "release.keystore");
|
|
41
|
+
let cleaned = false;
|
|
42
|
+
try {
|
|
43
|
+
fs_1.default.chmodSync(signingDir, 0o700);
|
|
44
|
+
fs_1.default.writeFileSync(keystorePath, decoded, {
|
|
45
|
+
flag: "wx",
|
|
46
|
+
mode: 0o600,
|
|
47
|
+
});
|
|
48
|
+
verifyProtectedKeystore(signingDir, keystorePath, decoded.length);
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
fs_1.default.rmSync(signingDir, { recursive: true, force: true });
|
|
52
|
+
throw error;
|
|
53
|
+
}
|
|
54
|
+
finally {
|
|
55
|
+
decoded.fill(0);
|
|
56
|
+
decoded = Buffer.alloc(0);
|
|
57
|
+
}
|
|
58
|
+
return Object.freeze({
|
|
59
|
+
keystorePath,
|
|
60
|
+
storePassword: input.storePassword,
|
|
61
|
+
keyAlias: input.keyAlias,
|
|
62
|
+
keyPassword: input.keyPassword,
|
|
63
|
+
cleanup() {
|
|
64
|
+
if (cleaned)
|
|
65
|
+
return;
|
|
66
|
+
fs_1.default.rmSync(signingDir, { recursive: true, force: true, maxRetries: 2 });
|
|
67
|
+
if (fs_1.default.existsSync(signingDir)) {
|
|
68
|
+
throw new Error(`Failed to remove temporary signing directory: ${signingDir}`);
|
|
69
|
+
}
|
|
70
|
+
cleaned = true;
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
function removeSigningVariables(env) {
|
|
75
|
+
const sanitized = { ...env };
|
|
76
|
+
delete sanitized[exports.PUBLIC_SIGNING_ENV.keystoreBase64];
|
|
77
|
+
delete sanitized[exports.PUBLIC_SIGNING_ENV.keystorePassword];
|
|
78
|
+
delete sanitized[exports.PUBLIC_SIGNING_ENV.keyAlias];
|
|
79
|
+
delete sanitized[exports.PUBLIC_SIGNING_ENV.keyPassword];
|
|
80
|
+
delete sanitized[exports.INTERNAL_KEYSTORE_PATH_ENV];
|
|
81
|
+
return sanitized;
|
|
82
|
+
}
|
|
83
|
+
function createGradleSigningEnvironment(context, env = process.env, keystorePath = context.keystorePath) {
|
|
84
|
+
return {
|
|
85
|
+
...removeSigningVariables(env),
|
|
86
|
+
[exports.INTERNAL_KEYSTORE_PATH_ENV]: keystorePath,
|
|
87
|
+
[exports.PUBLIC_SIGNING_ENV.keystorePassword]: context.storePassword,
|
|
88
|
+
[exports.PUBLIC_SIGNING_ENV.keyAlias]: context.keyAlias,
|
|
89
|
+
[exports.PUBLIC_SIGNING_ENV.keyPassword]: context.keyPassword,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
function requireValue(env, name) {
|
|
93
|
+
const value = env[name];
|
|
94
|
+
if (value === undefined || value.length === 0) {
|
|
95
|
+
throw new Error(`${name} is required and must not be empty for release builds`);
|
|
96
|
+
}
|
|
97
|
+
return value;
|
|
98
|
+
}
|
|
99
|
+
function validateCanonicalBase64(value) {
|
|
100
|
+
if (!CANONICAL_BASE64.test(value)) {
|
|
101
|
+
throw new Error(`${exports.PUBLIC_SIGNING_ENV.keystoreBase64} must be canonical single-line RFC 4648 base64`);
|
|
102
|
+
}
|
|
103
|
+
const decodedLength = Buffer.byteLength(value, "base64");
|
|
104
|
+
if (decodedLength === 0) {
|
|
105
|
+
throw new Error(`${exports.PUBLIC_SIGNING_ENV.keystoreBase64} decodes to an empty keystore`);
|
|
106
|
+
}
|
|
107
|
+
if (decodedLength > MAX_KEYSTORE_BYTES) {
|
|
108
|
+
throw new Error(`${exports.PUBLIC_SIGNING_ENV.keystoreBase64} exceeds the 16 MiB decoded size limit`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
function decodeKeystore(value) {
|
|
112
|
+
const decoded = Buffer.from(value, "base64");
|
|
113
|
+
if (decoded.toString("base64") !== value) {
|
|
114
|
+
decoded.fill(0);
|
|
115
|
+
throw new Error(`${exports.PUBLIC_SIGNING_ENV.keystoreBase64} is not canonical base64`);
|
|
116
|
+
}
|
|
117
|
+
return decoded;
|
|
118
|
+
}
|
|
119
|
+
function verifyProtectedKeystore(signingDir, keystorePath, expectedSize) {
|
|
120
|
+
const dirStat = fs_1.default.lstatSync(signingDir);
|
|
121
|
+
const fileStat = fs_1.default.lstatSync(keystorePath);
|
|
122
|
+
if (!dirStat.isDirectory() || dirStat.isSymbolicLink()) {
|
|
123
|
+
throw new Error("Temporary signing directory is not a real directory");
|
|
124
|
+
}
|
|
125
|
+
if (!fileStat.isFile() || fileStat.isSymbolicLink()) {
|
|
126
|
+
throw new Error("Temporary keystore is not a regular file");
|
|
127
|
+
}
|
|
128
|
+
if ((dirStat.mode & 0o777) !== 0o700 || (fileStat.mode & 0o777) !== 0o600) {
|
|
129
|
+
throw new Error("Temporary signing storage does not have owner-only permissions");
|
|
130
|
+
}
|
|
131
|
+
if (fileStat.size !== expectedSize) {
|
|
132
|
+
throw new Error("Temporary keystore size does not match decoded input");
|
|
133
|
+
}
|
|
134
|
+
}
|