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.
@@ -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:", distPath);
36
- console.log("šŸ“± Out:", apkOutput);
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
- // 1. VALIDATE PROJECT
42
- // -----------------------------
43
- (0, validateProject_1.validateProject)(distPath, manifest);
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("āŒ Web assets not found");
46
- }
47
- // -----------------------------
48
- // 2. COPY WEB → ANDROID ASSETS
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
- const iconPath = manifest.ui?.icon;
77
- if (iconPath) {
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
- const splash = manifest.ui?.splash;
81
- if (splash) {
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
- // 4. RUN BUILD RUNNER
89
- // -----------------------------
90
- let apkPath;
91
- try {
92
- console.log("\nāš™ļø Running build runner...");
93
- const runner = (0, createRunner_1.createRunner)(config);
94
- apkPath = await runner.build(androidRoot, mode);
95
- console.log("āœ… APK build successful");
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.isDirectory()) {
114
- fs_1.default.rmSync(finalPath, {
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
- // COPY FOLDER
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 entries = fs_1.default.readdirSync(src, {
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.execSync)(`open "${path_1.default.dirname(filePath)}"`);
193
+ (0, child_process_1.execFileSync)("open", [path_1.default.dirname(filePath)], { stdio: "ignore" });
161
194
  }
162
195
  catch {
163
- // ignore
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
+ }
@@ -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
+ }