pakstr 0.7.0 → 0.8.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.
@@ -7,141 +7,187 @@ exports.buildCommand = buildCommand;
7
7
  const fs_1 = __importDefault(require("fs"));
8
8
  const path_1 = __importDefault(require("path"));
9
9
  const child_process_1 = require("child_process");
10
+ const generatedProjectVerification_1 = require("../android/generatedProjectVerification");
10
11
  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
12
  const branding_1 = require("../android/branding");
17
- const version_1 = require("../android/version");
18
13
  const icon_1 = require("../android/icon");
19
- const splash_1 = require("../android/splash");
20
14
  const permissions_1 = require("../android/permissions");
15
+ const releaseWorkspace_1 = require("../android/releaseWorkspace");
16
+ const splash_1 = require("../android/splash");
17
+ const template_1 = require("../android/template");
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
- const appName = manifest?.app?.name || manifest?.appName || "Pakstr App";
29
+ const appName = manifest.appName;
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
+ expectedMetadata: {
64
+ applicationId: manifest.appId,
65
+ versionCode: manifest.versionCode,
66
+ versionName: manifest.versionName,
67
+ appLabel: appName,
68
+ },
69
+ signing,
70
+ });
71
+ if (mode === "release" && !result.verification) {
72
+ throw new Error("Release runner returned no verification evidence");
73
+ }
74
+ stagedPath = stageArtifact(result.artifactPath, finalPath);
75
+ }
76
+ catch (error) {
77
+ primaryError = error;
78
+ }
79
+ try {
80
+ signing?.cleanup();
81
+ }
82
+ catch (error) {
83
+ cleanupError = error;
84
+ }
85
+ try {
86
+ workspace?.cleanup();
87
+ }
88
+ catch (error) {
89
+ cleanupError ??= error;
90
+ }
91
+ if (primaryError || cleanupError) {
92
+ removeStagedArtifact(stagedPath);
93
+ removeExistingOutput(finalPath);
94
+ if (primaryError) {
95
+ if (cleanupError) {
96
+ console.error("Cleanup also failed:", safeErrorMessage(cleanupError));
97
+ }
98
+ throw primaryError;
99
+ }
100
+ throw cleanupError;
101
+ }
102
+ if (!stagedPath) {
103
+ throw new Error("Build completed without a staged APK");
104
+ }
105
+ publishArtifact(stagedPath, finalPath);
106
+ console.log("\nšŸš€ BUILD COMPLETE");
107
+ console.log("App:", appName);
108
+ console.log("Mode:", mode.toUpperCase());
109
+ console.log("šŸ“¦ Output:", finalPath);
110
+ openFolder(finalPath);
111
+ }
112
+ async function prepareAndroidProject(androidRoot, distPath, manifest, appName) {
113
+ const assetsTarget = path_1.default.join(androidRoot, "app/src/main/assets/www");
44
114
  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
- });
115
+ throw new Error("Web assets not found");
116
+ }
117
+ fs_1.default.rmSync(assetsTarget, { recursive: true, force: true });
118
+ fs_1.default.mkdirSync(assetsTarget, { recursive: true });
58
119
  copyFolder(distPath, assetsTarget);
59
- console.log("šŸ“¦ Web assets copied successfully");
60
120
  (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
121
  (0, gradle_1.patchGradle)(androidRoot, manifest);
69
- console.log("āš™ļø Gradle patched");
70
- // -----------------------------
71
- // 3.1 PATCH BRANDING
72
- // -----------------------------
73
122
  (0, branding_1.patchAppName)(androidRoot, appName);
74
- (0, branding_1.patchPackageName)(androidRoot, manifest.appId);
75
- (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");
79
- }
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?.icon) {
124
+ await (0, icon_1.patchIcon)(androidRoot, manifest.ui.icon, manifest.backgroundColor ?? "#FFFFFF");
125
+ }
126
+ if (manifest.ui?.splash) {
127
+ await (0, splash_1.patchSplash)(androidRoot, manifest.ui.splash.image, manifest.ui.splash.background ?? "#FFFFFF");
85
128
  }
86
129
  (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,
130
+ (0, generatedProjectVerification_1.verifyGeneratedAndroidProject)(androidRoot, {
131
+ applicationId: manifest.appId,
132
+ versionCode: manifest.versionCode,
133
+ versionName: manifest.versionName,
134
+ appLabel: appName,
110
135
  });
136
+ }
137
+ function validateOutputParent(finalPath) {
138
+ const parent = path_1.default.dirname(finalPath);
139
+ if (!fs_1.default.existsSync(parent) || !fs_1.default.lstatSync(parent).isDirectory()) {
140
+ throw new Error(`Output directory must already exist: ${parent}`);
141
+ }
142
+ if (fs_1.default.lstatSync(parent).isSymbolicLink()) {
143
+ throw new Error(`Output directory must not be a symbolic link: ${parent}`);
144
+ }
111
145
  if (fs_1.default.existsSync(finalPath)) {
112
146
  const stat = fs_1.default.lstatSync(finalPath);
113
- if (stat.isDirectory()) {
114
- fs_1.default.rmSync(finalPath, {
115
- recursive: true,
116
- force: true,
117
- });
147
+ if (!stat.isFile() || stat.isSymbolicLink()) {
148
+ throw new Error(`Output path must be a regular file: ${finalPath}`);
118
149
  }
119
150
  }
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
151
  }
131
- // -----------------------------
132
- // COPY FOLDER
133
- // -----------------------------
152
+ function stageArtifact(artifactPath, finalPath) {
153
+ const parent = path_1.default.dirname(finalPath);
154
+ const stageDir = fs_1.default.mkdtempSync(path_1.default.join(parent, ".pakstr-stage-"));
155
+ const stagedPath = path_1.default.join(stageDir, "app.apk");
156
+ fs_1.default.copyFileSync(artifactPath, stagedPath, fs_1.default.constants.COPYFILE_EXCL);
157
+ if (fs_1.default.statSync(stagedPath).size !== fs_1.default.statSync(artifactPath).size) {
158
+ fs_1.default.rmSync(stageDir, { recursive: true, force: true });
159
+ throw new Error("Staged APK size does not match verified artifact");
160
+ }
161
+ return stagedPath;
162
+ }
163
+ function publishArtifact(stagedPath, finalPath) {
164
+ removeExistingOutput(finalPath);
165
+ fs_1.default.renameSync(stagedPath, finalPath);
166
+ fs_1.default.rmSync(path_1.default.dirname(stagedPath), { recursive: true, force: true });
167
+ if (!fs_1.default.existsSync(finalPath) || !fs_1.default.lstatSync(finalPath).isFile()) {
168
+ throw new Error("Final APK publication failed");
169
+ }
170
+ }
171
+ function removeExistingOutput(finalPath) {
172
+ if (!fs_1.default.existsSync(finalPath))
173
+ return;
174
+ const stat = fs_1.default.lstatSync(finalPath);
175
+ if (!stat.isFile() || stat.isSymbolicLink()) {
176
+ throw new Error(`Refusing to remove non-regular output path: ${finalPath}`);
177
+ }
178
+ fs_1.default.unlinkSync(finalPath);
179
+ }
180
+ function removeStagedArtifact(stagedPath) {
181
+ if (!stagedPath)
182
+ return;
183
+ fs_1.default.rmSync(path_1.default.dirname(stagedPath), { recursive: true, force: true });
184
+ }
134
185
  function copyFolder(src, dest) {
135
- const entries = fs_1.default.readdirSync(src, {
136
- withFileTypes: true,
137
- });
138
- for (const entry of entries) {
186
+ for (const entry of fs_1.default.readdirSync(src, { withFileTypes: true })) {
139
187
  const srcPath = path_1.default.join(src, entry.name);
140
188
  const destPath = path_1.default.join(dest, entry.name);
141
189
  if (entry.isDirectory()) {
142
- fs_1.default.mkdirSync(destPath, {
143
- recursive: true,
144
- });
190
+ fs_1.default.mkdirSync(destPath, { recursive: true });
145
191
  copyFolder(srcPath, destPath);
146
192
  }
147
193
  else {
@@ -149,17 +195,16 @@ function copyFolder(src, dest) {
149
195
  }
150
196
  }
151
197
  }
152
- // -----------------------------
153
- // OPEN FOLDER (macOS)
154
- // -----------------------------
155
198
  function openFolder(filePath) {
156
- if (process.platform !== "darwin") {
199
+ if (process.platform !== "darwin" || process.env.CI)
157
200
  return;
158
- }
159
201
  try {
160
- (0, child_process_1.execSync)(`open "${path_1.default.dirname(filePath)}"`);
202
+ (0, child_process_1.execFileSync)("open", [path_1.default.dirname(filePath)], { stdio: "ignore" });
161
203
  }
162
204
  catch {
163
- // ignore
205
+ // Opening the output folder is optional.
164
206
  }
165
207
  }
208
+ function safeErrorMessage(error) {
209
+ return error instanceof Error ? error.message : "unknown cleanup error";
210
+ }
@@ -16,12 +16,31 @@ 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");
31
+ if (/[\r\n]/.test(m.appName)) {
32
+ throw new Error("āŒ appName must not contain line breaks");
33
+ }
21
34
  if (!m.versionName)
22
35
  throw new Error("āŒ versionName is required");
36
+ if (/[\r\n"\\]/.test(m.versionName)) {
37
+ throw new Error("āŒ versionName must not contain line breaks, quotes, or backslashes");
38
+ }
23
39
  if (m.versionCode === undefined)
24
40
  throw new Error("āŒ versionCode is required");
41
+ if (!Number.isInteger(m.versionCode) || m.versionCode <= 0) {
42
+ throw new Error("āŒ versionCode must be a positive integer");
43
+ }
25
44
  if (m.ui?.splash && !m.ui.splash.image) {
26
45
  throw new Error("āŒ splash.image is required");
27
46
  }
@@ -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
+ }