pakstr 0.8.6 → 0.9.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.
Files changed (38) hide show
  1. package/android-template/app/build.gradle.kts +11 -3
  2. package/android-template/app/src/main/java/com/pakstr/app/MainActivity.kt +2 -0
  3. package/android-template/app/src/main/java/com/pakstr/app/debug/DebugReportExporter.kt +1 -1
  4. package/android-template/app/src/main/java/com/pakstr/app/debug/DeveloperToolsActivity.kt +1 -1
  5. package/android-template/app/src/main/java/com/pakstr/app/debug/DeviceDeveloperState.kt +23 -0
  6. package/android-template/app/src/main/java/com/pakstr/app/debug/WebViewDebugManager.kt +31 -4
  7. package/android-template/app/src/test/java/com/pakstr/app/debug/DeviceDeveloperStateTest.kt +21 -0
  8. package/android-template/app/src/test/java/com/pakstr/app/debug/WebViewDebugManagerTest.kt +84 -0
  9. package/bin/cli.js +11 -6
  10. package/dist/cli.js +53 -16
  11. package/dist/commands/build.js +42 -114
  12. package/dist/commands/init.js +191 -0
  13. package/dist/commands/publish.js +137 -0
  14. package/dist/commands/run.js +95 -0
  15. package/dist/commands/sign.js +41 -0
  16. package/dist/core/androidProject.js +70 -0
  17. package/dist/core/appIdentity.js +7 -0
  18. package/dist/core/blossom.js +122 -0
  19. package/dist/core/dotenv.js +51 -0
  20. package/dist/core/nostr.js +153 -0
  21. package/dist/core/pakstrConfig.js +206 -0
  22. package/dist/core/zapStore.js +221 -0
  23. package/dist/runners/DockerGradleRunner.js +38 -128
  24. package/dist/runners/DockerSignRunner.js +182 -0
  25. package/dist/runners/LocalGradleRunner.js +26 -129
  26. package/dist/runners/createRunner.js +4 -4
  27. package/package.json +7 -1
  28. package/dist/android/releaseVerification.js +0 -164
  29. package/dist/config.js +0 -4
  30. package/dist/core/buildContext.js +0 -102
  31. package/dist/core/config.js +0 -24
  32. package/dist/core/copyFile.js +0 -17
  33. package/dist/core/manifest.js +0 -47
  34. package/dist/core/releaseSigning.js +0 -134
  35. package/dist/core/resolveProjectInputs.js +0 -20
  36. package/dist/core/validateProject.js +0 -21
  37. package/dist/core/zeroConfig.js +0 -97
  38. package/dist/runtime/runtimeConfig.js +0 -22
@@ -0,0 +1,191 @@
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.initCommand = initCommand;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const crypto_1 = require("crypto");
10
+ const pakstrConfig_1 = require("../core/pakstrConfig");
11
+ const nostr_1 = require("../core/nostr");
12
+ /**
13
+ * `pakstr init` — generate `pakstr.yaml` in the current directory.
14
+ * Spec §5.1. Infers defaults from `package.json`. Never silently overwrites.
15
+ */
16
+ const HEADER = `# pakstr.yaml — one file, one command, one APK.
17
+ # Generated by \`pakstr init\`. Edit freely.
18
+ #
19
+ # ⚠️ Keep your nsec OUT of this file. \`pakstr init\` wrote a local dev nsec
20
+ # to .env (gitignored). For real releases, put ${nostr_1.PAKSTR_NSEC_ENV} in your CI
21
+ # secret manager — reuse the same nsec so updates install over prior APKs.
22
+ # WARNING: ${nostr_1.PAKSTR_NSEC_ENV} is a production release-signing root secret. Anyone who
23
+ # has it can derive every app identity for every known appId. Losing it breaks
24
+ # update continuity. It cannot rotate the signer of an already-published app.
25
+ `;
26
+ function initCommand(options = {}) {
27
+ const configPath = path_1.default.resolve(options.out ?? path_1.default.join(process.cwd(), pakstrConfig_1.PAKSTR_CONFIG_FILENAME));
28
+ if (fs_1.default.existsSync(configPath) && !options.force) {
29
+ throw new Error(`${configPath} already exists. Use --force to overwrite, or remove it first.`);
30
+ }
31
+ const pkg = readPackageJson();
32
+ const { appId, sanitized: appIdSanitized } = inferAppId(pkg);
33
+ const appName = inferAppName(pkg) ?? appNameFromAppId(appId);
34
+ const versionName = (pkg && typeof pkg.version === "string" ? pkg.version : "1.0.0");
35
+ const versionCode = 1;
36
+ const web = inferWebDir();
37
+ const out = `./build/${appId}.apk`;
38
+ const yaml = renderConfig({
39
+ appId,
40
+ appName,
41
+ versionName,
42
+ versionCode,
43
+ description: pkg && typeof pkg.description === "string" ? pkg.description : "",
44
+ web,
45
+ out,
46
+ });
47
+ fs_1.default.writeFileSync(configPath, yaml, "utf8");
48
+ console.log(`✅ Wrote ${configPath}`);
49
+ if (appIdSanitized) {
50
+ console.log(`⚠️ Your package.json name contains characters that aren't valid in an Android package`);
51
+ console.log(` name (e.g. hyphens). pakstr sanitized it to appId "${appId}". Edit`);
52
+ console.log(` ${pakstrConfig_1.PAKSTR_CONFIG_FILENAME} if you'd prefer a different value.`);
53
+ }
54
+ // Generate a fresh dev nsec into a gitignored `.env` so `pakstr run` works
55
+ // locally with no manual setup. Never overwrites an existing PAKSTR_NSEC.
56
+ const nsecStatus = ensureDevNsec();
57
+ ensureGitIgnoresEnv();
58
+ console.log("");
59
+ console.log("➡️ Next: run pakstr run");
60
+ if (nsecStatus === "generated") {
61
+ console.log(` A fresh ${nostr_1.PAKSTR_NSEC_ENV} was written to .env (gitignored) for local dev.`);
62
+ }
63
+ else if (nsecStatus === "present") {
64
+ console.log(` ${nostr_1.PAKSTR_NSEC_ENV} already in .env — left untouched.`);
65
+ }
66
+ console.log("");
67
+ console.log(` For real releases, put the SAME ${nostr_1.PAKSTR_NSEC_ENV} in your CI secret manager`);
68
+ console.log(" (reusing it is what lets an update install over the previous APK).");
69
+ console.log(` ${nostr_1.PAKSTR_NSEC_ENV} is a release-signing root secret: anyone who has it can`);
70
+ console.log(" sign updates for every app whose appId they know. Keep it safe.");
71
+ }
72
+ /** Generate a dev nsec into .env if none is present there. Returns what happened. */
73
+ function ensureDevNsec() {
74
+ const envPath = path_1.default.join(process.cwd(), ".env");
75
+ const existing = fs_1.default.existsSync(envPath)
76
+ ? fs_1.default.readFileSync(envPath, "utf8")
77
+ : "";
78
+ if (containsPakstrNsec(existing))
79
+ return "present";
80
+ const nsec = (0, nostr_1.encodeNsec)((0, crypto_1.randomBytes)(32));
81
+ const line = `${nostr_1.PAKSTR_NSEC_ENV}=${nsec}\n`;
82
+ const header = existing.length === 0 ? "# Local development secrets — never commit this file.\n" : "";
83
+ fs_1.default.writeFileSync(envPath, header + (existing.length ? "\n" + line : line), {
84
+ flag: existing.length === 0 ? "wx" : "a",
85
+ });
86
+ return "generated";
87
+ }
88
+ function containsPakstrNsec(envText) {
89
+ return new RegExp(`^${nostr_1.PAKSTR_NSEC_ENV}=`, "m").test(envText);
90
+ }
91
+ /** Ensure `.env` is listed in `.gitignore` so the nsec is never committed. */
92
+ function ensureGitIgnoresEnv() {
93
+ const gitignorePath = path_1.default.join(process.cwd(), ".gitignore");
94
+ const existing = fs_1.default.existsSync(gitignorePath)
95
+ ? fs_1.default.readFileSync(gitignorePath, "utf8")
96
+ : "";
97
+ if (containsEnvEntry(existing))
98
+ return;
99
+ const addition = existing.length === 0 || existing.endsWith("\n")
100
+ ? ".env\n"
101
+ : "\n.env\n";
102
+ fs_1.default.writeFileSync(gitignorePath, existing + addition, {
103
+ flag: existing.length === 0 ? "wx" : "a",
104
+ });
105
+ }
106
+ function containsEnvEntry(gitignoreText) {
107
+ return gitignoreText
108
+ .split(/\r?\n/)
109
+ .some(line => line.trim() === ".env" || line.trim().startsWith(".env"));
110
+ }
111
+ function renderConfig(c) {
112
+ const perms = ` [${pakstrConfig_1.PERMISSION_ALIASES.map(p => `"${p}"`).join(", ")}]`;
113
+ return `${HEADER}
114
+ app:
115
+ appId: ${c.appId} # REQUIRED. Java package id; also feeds key derivation.
116
+ appName: ${escapeYamlScalar(c.appName)} # REQUIRED. Launcher label.
117
+ versionName: "${c.versionName}" # REQUIRED. Human-readable version.
118
+ versionCode: ${c.versionCode} # REQUIRED. Monotonic integer.
119
+ description: ${c.description === "" ? '""' : escapeYamlScalar(c.description)} # OPTIONAL.
120
+ # icon: ./icon.png # OPTIONAL. Resolved relative to this file.
121
+ # splash: # OPTIONAL.
122
+ # image: ./splash.png
123
+ # background: "#0f0f17"
124
+ # backgroundColor: "#0f0f17" # OPTIONAL. Launcher icon background.
125
+ # permissions: ${perms} # OPTIONAL. Aliases (case-insensitive): ${pakstrConfig_1.PERMISSION_ALIASES.join(", ")}.
126
+
127
+ build:
128
+ web: ${c.web} # REQUIRED. Built web assets (must contain index.html).
129
+ out: ${c.out} # OPTIONAL. Default ./build/<appId>.apk
130
+ builder: docker # Only "docker" is specified.
131
+
132
+ publish:
133
+ zapstore:
134
+ enabled: true # Default true when publish is present.
135
+ # publishKey: PAKSTR_PUBLISH_NSEC # OPTIONAL. Omit to reuse ${nostr_1.PAKSTR_NSEC_ENV} for publishing.
136
+ `;
137
+ }
138
+ function escapeYamlScalar(value) {
139
+ // Keep it simple: quote if it contains anything that looks YAML-special.
140
+ if (value === "" || /[:#\-?*&!|>'"%@\`\[\]{}]/.test(value) || /^\s|\s$/.test(value)) {
141
+ return `"${value.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`;
142
+ }
143
+ return value;
144
+ }
145
+ function readPackageJson() {
146
+ const p = path_1.default.join(process.cwd(), "package.json");
147
+ if (!fs_1.default.existsSync(p))
148
+ return null;
149
+ try {
150
+ return JSON.parse(fs_1.default.readFileSync(p, "utf8"));
151
+ }
152
+ catch {
153
+ return null;
154
+ }
155
+ }
156
+ function inferAppId(pkg) {
157
+ const name = pkg && typeof pkg.name === "string" ? pkg.name : null;
158
+ if (!name)
159
+ return { appId: "dev.pakstr.myapp", sanitized: false };
160
+ // Split the npm name into segments (handling @scope/name), sanitize each,
161
+ // and track whether any segment changed so we can warn the user.
162
+ const raw = name.startsWith("@") ? name.slice(1).split("/") : [name];
163
+ const segments = raw.map(s => s.toLowerCase());
164
+ const sanitized = segments.map(s => (0, pakstrConfig_1.sanitizeAppIdSegment)(s) ?? "");
165
+ const changed = segments.some((orig, i) => sanitized[i] !== orig);
166
+ const valid = sanitized.filter(s => s.length > 0);
167
+ const appId = valid.length > 0 ? `dev.pakstr.${valid.join(".")}` : "dev.pakstr.myapp";
168
+ // Final guard: if it still doesn't match the pattern, fall back.
169
+ return pakstrConfig_1.APP_ID_PATTERN.test(appId)
170
+ ? { appId, sanitized: changed }
171
+ : { appId: "dev.pakstr.myapp", sanitized: true };
172
+ }
173
+ function inferAppName(pkg) {
174
+ const name = pkg && typeof pkg.name === "string" ? pkg.name : null;
175
+ if (!name)
176
+ return null;
177
+ const unscoped = name.replace(/^@[^/]+\//, "");
178
+ return unscoped || null;
179
+ }
180
+ function appNameFromAppId(appId) {
181
+ const last = appId.split(".").pop() ?? appId;
182
+ return last.charAt(0).toUpperCase() + last.slice(1);
183
+ }
184
+ function inferWebDir() {
185
+ const cwd = process.cwd();
186
+ if (fs_1.default.existsSync(path_1.default.join(cwd, "dist", "index.html")))
187
+ return "./dist";
188
+ if (fs_1.default.existsSync(path_1.default.join(cwd, "build", "index.html")))
189
+ return "./build";
190
+ return "./dist";
191
+ }
@@ -0,0 +1,137 @@
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.publishCommand = publishCommand;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const pakstrConfig_1 = require("../core/pakstrConfig");
10
+ const zapStore_1 = require("../core/zapStore");
11
+ const blossom_1 = require("../core/blossom");
12
+ /**
13
+ * `pakstr publish` — upload the signed APK to Blossom and publish NIP-82
14
+ * events (32267/30063/3063) to the configured relay. Spec §5.4 / §8.
15
+ *
16
+ * Requires that `pakstr build` + `pakstr sign` have already run (the signed
17
+ * APK at build.out, plus its `.signer-sha256` sidecar).
18
+ *
19
+ * `--dry-run` skips network + disk: it builds the events with a stub blossom
20
+ * descriptor and a mock relay transport, so you can preview exactly what would
21
+ * be published without an APK or network.
22
+ */
23
+ async function publishCommand(configPath, options = {}) {
24
+ const config = (0, pakstrConfig_1.loadPakstrConfig)(configPath);
25
+ const apkPath = path_1.default.resolve(config.build.out);
26
+ const publishNsec = (0, zapStore_1.resolvePublishSecret)(config.publish.publishKey, process.env);
27
+ console.log("\n📤 pakstr publish" + (options.dryRun ? " (dry-run)" : ""));
28
+ console.log("App:", config.app.appName, `(${config.app.appId})`);
29
+ console.log("Publishing as:", publishNsec.envVar);
30
+ console.log("Relay:", config.publish.relay);
31
+ console.log("Blossom:", config.publish.blossom);
32
+ let blossomUrl;
33
+ let apkSha256;
34
+ let apkSize;
35
+ let signerCertificateSha256;
36
+ let filename;
37
+ if (options.dryRun) {
38
+ // No network, no APK required. Synthesize a stub descriptor so the events
39
+ // can still be built and printed for preview.
40
+ apkSha256 = "0".repeat(64);
41
+ apkSize = 0;
42
+ blossomUrl = `${config.publish.blossom.replace(/\/$/, "")}/${apkSha256}`;
43
+ signerCertificateSha256 = "0".repeat(64);
44
+ filename = path_1.default.basename(apkPath);
45
+ console.log("📦 (dry-run) Blossom URL:", blossomUrl);
46
+ }
47
+ else {
48
+ if (!fs_1.default.existsSync(apkPath) || !fs_1.default.lstatSync(apkPath).isFile()) {
49
+ throw new Error(`Signed APK not found at ${apkPath}. Run \`pakstr build\` and \`pakstr sign\` first.`);
50
+ }
51
+ const certShaPath = `${apkPath}.signer-sha256`;
52
+ if (!fs_1.default.existsSync(certShaPath)) {
53
+ throw new Error(`Signer certificate sidecar not found: ${certShaPath}. Run \`pakstr sign\` first.`);
54
+ }
55
+ signerCertificateSha256 = fs_1.default.readFileSync(certShaPath, "utf8").trim();
56
+ const blossom = await (0, blossom_1.uploadToBlossom)({
57
+ serverUrl: config.publish.blossom,
58
+ filePath: apkPath,
59
+ secret: publishNsec.bytes,
60
+ fetchImpl: options.blossomFetch,
61
+ });
62
+ blossomUrl = blossom.url;
63
+ apkSha256 = blossom.sha256;
64
+ apkSize = blossom.size;
65
+ filename = path_1.default.basename(apkPath);
66
+ console.log("📦 Uploaded to Blossom:", blossom.url);
67
+ console.log(" APK SHA-256:", blossom.sha256, "| size:", blossom.size);
68
+ }
69
+ // Build + publish the NIP-82 events.
70
+ const transport = options.transport ?? (options.dryRun ? new zapStore_1.MockRelayTransport() : new zapStore_1.WebsocketRelayTransport());
71
+ const result = await (0, zapStore_1.publishToZapStore)({
72
+ app: {
73
+ appId: config.app.appId,
74
+ appName: config.app.appName,
75
+ versionName: config.app.versionName,
76
+ versionCode: config.app.versionCode,
77
+ description: config.app.description,
78
+ },
79
+ apk: {
80
+ apkSha256,
81
+ apkSize,
82
+ blossomUrl,
83
+ signerCertificateSha256,
84
+ filename,
85
+ },
86
+ publishNsec,
87
+ target: { relayUrl: config.publish.relay },
88
+ }, transport);
89
+ console.log(options.dryRun ? "✅ PUBLISHED (dry-run — no network)" : "✅ PUBLISHED");
90
+ console.log("🪪 Publisher npub:", result.npub);
91
+ console.log("🧾 Asset event:", result.assetEventId);
92
+ console.log("🧾 Release event:", result.releaseEventId);
93
+ console.log("🧾 App metadata event:", result.appMetadataEventId);
94
+ console.log("🔗 Blossom URL:", blossomUrl);
95
+ console.log("📡 Relay:", config.publish.relay);
96
+ if (options.verify && !options.dryRun) {
97
+ const { eventFound, apkDownloadable } = await (0, zapStore_1.verifyPublish)({
98
+ app: {
99
+ appId: config.app.appId,
100
+ appName: config.app.appName,
101
+ versionName: config.app.versionName,
102
+ versionCode: config.app.versionCode,
103
+ },
104
+ assetEventId: result.assetEventId,
105
+ relayUrl: config.publish.relay,
106
+ blossomUrl,
107
+ });
108
+ console.log(`🔎 Verify: event on relay=${eventFound}, apk on blossom=${apkDownloadable}`);
109
+ if (!eventFound || !apkDownloadable) {
110
+ throw new Error("Publish verification failed");
111
+ }
112
+ }
113
+ if (options.outJson) {
114
+ const summary = {
115
+ appId: config.app.appId,
116
+ appName: config.app.appName,
117
+ versionName: config.app.versionName,
118
+ versionCode: config.app.versionCode,
119
+ npub: result.npub,
120
+ pubkeyHex: result.pubkeyHex,
121
+ relayUrl: config.publish.relay,
122
+ blossomServer: config.publish.blossom,
123
+ blossomUrl,
124
+ apkSha256,
125
+ apkSize,
126
+ signerCertificateSha256,
127
+ assetEventId: result.assetEventId,
128
+ releaseEventId: result.releaseEventId,
129
+ appMetadataEventId: result.appMetadataEventId,
130
+ publishedAt: new Date().toISOString(),
131
+ dryRun: !!options.dryRun,
132
+ };
133
+ fs_1.default.writeFileSync(options.outJson, JSON.stringify(summary, null, 2) + "\n", "utf8");
134
+ console.log("📝 Publish summary written to:", options.outJson);
135
+ }
136
+ return result;
137
+ }
@@ -0,0 +1,95 @@
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.runCommand = runCommand;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const pakstrConfig_1 = require("../core/pakstrConfig");
10
+ const nostr_1 = require("../core/nostr");
11
+ const build_1 = require("./build");
12
+ const sign_1 = require("./sign");
13
+ const publish_1 = require("./publish");
14
+ /**
15
+ * `pakstr run` — build → sign → publish. Spec §5.5 / §6.
16
+ *
17
+ * Each step is atomic and fail-fast (§7). The signed APK at build.out is
18
+ * preserved on a publish failure; only ephemeral/derived material is cleaned
19
+ * up. `init` is NOT part of `run`.
20
+ */
21
+ async function runCommand(configPath, options = {}) {
22
+ // Step 1: load & validate config, pre-validate nsecs.
23
+ const config = (0, pakstrConfig_1.loadPakstrConfig)(configPath);
24
+ // Verify PAKSTR_NSEC present (fail fast before any work).
25
+ (0, nostr_1.requireSigningNsec)(process.env);
26
+ // Pre-validate the publish nsec if publishing is enabled (§6 step 1).
27
+ if (config.publish.zapstoreEnabled) {
28
+ if (!(0, nostr_1.isPublishNsecPresent)(config.publish.publishKey, process.env)) {
29
+ const envVar = config.publish.publishKey === undefined
30
+ ? "PAKSTR_NSEC"
31
+ : config.publish.publishKey === null
32
+ ? "PAKSTR_PUBLISH_NSEC"
33
+ : config.publish.publishKey;
34
+ throw new Error(`${envVar} is required for publishing and must not be empty ` +
35
+ `(publish.zapstore.enabled is true). Pre-validation failed before any build work.`);
36
+ }
37
+ }
38
+ console.log("\n🚀 pakstr run");
39
+ console.log("App:", config.app.appName, `(${config.app.appId})`);
40
+ console.log("Version:", config.app.versionName, `(${config.app.versionCode})`);
41
+ console.log("Web:", config.build.web);
42
+ console.log("Out:", path_1.default.resolve(config.build.out));
43
+ console.log("Publish:", config.publish.zapstoreEnabled ? "enabled" : "disabled");
44
+ // Step 3: build the unsigned APK.
45
+ console.log("\n— Step 1/3: build —");
46
+ await (0, build_1.buildCommand)(config.configPath || undefined);
47
+ // Step 4: sign the APK.
48
+ console.log("\n— Step 2/3: sign —");
49
+ await (0, sign_1.signCommand)(config.configPath || undefined);
50
+ // Step 5: publish to Zap Store (unless disabled).
51
+ if (config.publish.zapstoreEnabled) {
52
+ console.log("\n— Step 3/3: publish —");
53
+ await (0, publish_1.publishCommand)(config.configPath || undefined, {
54
+ verify: options.verifyPublish,
55
+ outJson: options.publishOutJson,
56
+ });
57
+ }
58
+ else {
59
+ console.log("\nPublish disabled (publish.zapstore.enabled: false). Stopping after sign.");
60
+ }
61
+ // Step 6: cleanup. The signed APK at build.out is the artifact and is kept.
62
+ // Derived signing material is cleaned up inside the sign runner (tmpfs).
63
+ verifyNoLeakedKeystores(config);
64
+ console.log("\n✅ pakstr run complete");
65
+ console.log("Signed APK:", path_1.default.resolve(config.build.out));
66
+ }
67
+ /** Best-effort: ensure no derived .p12 keystore leaked onto disk (§7). */
68
+ function verifyNoLeakedKeystores(config) {
69
+ const buildDir = path_1.default.join(config.configDir, "build");
70
+ if (!fs_1.default.existsSync(buildDir))
71
+ return;
72
+ const walk = (dir) => {
73
+ const found = [];
74
+ for (const entry of fs_1.default.readdirSync(dir, { withFileTypes: true })) {
75
+ const p = path_1.default.join(dir, entry.name);
76
+ if (entry.isDirectory())
77
+ found.push(...walk(p));
78
+ else if (entry.name.endsWith(".p12") || entry.name.endsWith(".keystore"))
79
+ found.push(p);
80
+ }
81
+ return found;
82
+ };
83
+ const leaked = walk(buildDir);
84
+ if (leaked.length > 0) {
85
+ for (const f of leaked) {
86
+ try {
87
+ fs_1.default.rmSync(f, { force: true });
88
+ }
89
+ catch {
90
+ /* ignore */
91
+ }
92
+ }
93
+ throw new Error(`Derived keystore material leaked onto disk: ${leaked.join(", ")}`);
94
+ }
95
+ }
@@ -0,0 +1,41 @@
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.signCommand = signCommand;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const pakstrConfig_1 = require("../core/pakstrConfig");
10
+ const nostr_1 = require("../core/nostr");
11
+ const DockerSignRunner_1 = require("../runners/DockerSignRunner");
12
+ /**
13
+ * `pakstr sign` — sign the unsigned APK at `build.out` with the key derived
14
+ * from `PAKSTR_NSEC` + `app.appId`. Overwrites `build.out` with the signed APK.
15
+ * Spec §5.3.
16
+ */
17
+ async function signCommand(configPath) {
18
+ const config = (0, pakstrConfig_1.loadPakstrConfig)(configPath);
19
+ const unsignedApkPath = path_1.default.resolve(config.build.out);
20
+ if (!fs_1.default.existsSync(unsignedApkPath) || !fs_1.default.lstatSync(unsignedApkPath).isFile()) {
21
+ throw new Error(`Unsigned APK not found at ${unsignedApkPath}. Run \`pakstr build\` first.`);
22
+ }
23
+ const nsec = (0, nostr_1.requireSigningNsec)(process.env);
24
+ console.log("\n🔐 pakstr sign");
25
+ console.log("📦 Unsigned:", unsignedApkPath);
26
+ console.log("🔑 Deriving signing key from PAKSTR_NSEC +", config.app.appId);
27
+ const runner = new DockerSignRunner_1.DockerSignRunner();
28
+ const result = await runner.sign({
29
+ unsignedApkPath,
30
+ signedApkPath: unsignedApkPath,
31
+ appId: config.app.appId,
32
+ nsec: nsec.bytes,
33
+ });
34
+ // Write the signer certificate SHA-256 to a sidecar so `pakstr publish`/`run`
35
+ // can include it in the Zap Store asset event (apk_certificate_hash).
36
+ fs_1.default.writeFileSync(`${unsignedApkPath}.signer-sha256`, result.signerSha256, "utf8");
37
+ console.log("✅ SIGNED");
38
+ console.log("📦 Signed APK:", result.signedApkPath);
39
+ console.log("🧾 Signer certificate SHA-256:", result.signerSha256);
40
+ return result;
41
+ }
@@ -0,0 +1,70 @@
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.prepareAndroidProject = prepareAndroidProject;
7
+ exports.generateRuntimeConfig = generateRuntimeConfig;
8
+ const fs_1 = __importDefault(require("fs"));
9
+ const path_1 = __importDefault(require("path"));
10
+ const generatedProjectVerification_1 = require("../android/generatedProjectVerification");
11
+ const gradle_1 = require("../android/gradle");
12
+ const branding_1 = require("../android/branding");
13
+ const icon_1 = require("../android/icon");
14
+ const permissions_1 = require("../android/permissions");
15
+ const splash_1 = require("../android/splash");
16
+ /**
17
+ * Copy built web assets into the Android template and patch identity/branding.
18
+ * Spec §5.2. Creates `assets/www/` from `webDir`, patches Gradle, app name,
19
+ * icon, splash, and permissions, then verifies the generated project.
20
+ */
21
+ async function prepareAndroidProject(opts) {
22
+ const { androidRoot, webDir, app, configDir } = opts;
23
+ const assetsTarget = path_1.default.join(androidRoot, "app/src/main/assets/www");
24
+ if (!fs_1.default.existsSync(webDir)) {
25
+ throw new Error(`Web assets not found: ${webDir}`);
26
+ }
27
+ fs_1.default.rmSync(assetsTarget, { recursive: true, force: true });
28
+ fs_1.default.mkdirSync(assetsTarget, { recursive: true });
29
+ copyFolder(webDir, assetsTarget);
30
+ generateRuntimeConfig(assetsTarget);
31
+ (0, gradle_1.patchGradle)(androidRoot, app);
32
+ (0, branding_1.patchAppName)(androidRoot, app.appName);
33
+ if (app.icon) {
34
+ await (0, icon_1.patchIcon)(androidRoot, app.icon, app.backgroundColor ?? "#FFFFFF", configDir);
35
+ }
36
+ if (app.splash?.image) {
37
+ await (0, splash_1.patchSplash)(androidRoot, app.splash.image, app.splash.background ?? "#FFFFFF", configDir);
38
+ }
39
+ (0, permissions_1.patchPermissions)(androidRoot, app);
40
+ (0, generatedProjectVerification_1.verifyGeneratedAndroidProject)(androidRoot, {
41
+ applicationId: app.appId,
42
+ versionCode: app.versionCode,
43
+ versionName: app.versionName,
44
+ appLabel: app.appName,
45
+ });
46
+ }
47
+ /**
48
+ * Generate the runtime config written into the packaged web assets.
49
+ * Pakstr's config model has no runtime/debug fields, so this emits an empty
50
+ * config (kept for template compatibility).
51
+ */
52
+ function generateRuntimeConfig(webRoot) {
53
+ const runtime = {};
54
+ const jsContent = `window.PAKSTR_CONFIG = ${JSON.stringify(runtime, null, 2)};\n`;
55
+ fs_1.default.writeFileSync(path_1.default.join(webRoot, "pakstr-config.js"), jsContent.trim());
56
+ fs_1.default.writeFileSync(path_1.default.join(webRoot, "pakstr-runtime.json"), JSON.stringify(runtime, null, 2));
57
+ }
58
+ function copyFolder(src, dest) {
59
+ for (const entry of fs_1.default.readdirSync(src, { withFileTypes: true })) {
60
+ const srcPath = path_1.default.join(src, entry.name);
61
+ const destPath = path_1.default.join(dest, entry.name);
62
+ if (entry.isDirectory()) {
63
+ fs_1.default.mkdirSync(destPath, { recursive: true });
64
+ copyFolder(srcPath, destPath);
65
+ }
66
+ else {
67
+ fs_1.default.copyFileSync(srcPath, destPath);
68
+ }
69
+ }
70
+ }
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.asAndroidAppIdentity = asAndroidAppIdentity;
4
+ /** A `PakstrApp` is structurally an `AndroidAppIdentity`. */
5
+ function asAndroidAppIdentity(app) {
6
+ return app;
7
+ }
@@ -0,0 +1,122 @@
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.BlossomError = void 0;
7
+ exports.hashFile = hashFile;
8
+ exports.uploadToBlossom = uploadToBlossom;
9
+ exports.npubHexToBech32 = npubHexToBech32;
10
+ const crypto_1 = require("crypto");
11
+ const fs_1 = __importDefault(require("fs"));
12
+ const nostr_1 = require("./nostr");
13
+ /**
14
+ * Blossom (BUD-02 / BUD-11) blob upload.
15
+ *
16
+ * Uploads a file to a Blossom server with a Nostr kind-24242 authorization
17
+ * event signed by the supplied secret key. Returns the blob descriptor
18
+ * (`url`, `sha256`, `size`, `type`).
19
+ *
20
+ * Reference: https://github.com/hzrd149/blossom (BUD-02 upload, BUD-11 auth).
21
+ */
22
+ const DEFAULT_CONTENT_TYPE = "application/vnd.android.package-archive";
23
+ const AUTH_TTL_SECONDS = 5 * 60;
24
+ class BlossomError extends Error {
25
+ status;
26
+ constructor(message, status) {
27
+ super(message);
28
+ this.status = status;
29
+ this.name = "BlossomError";
30
+ }
31
+ }
32
+ exports.BlossomError = BlossomError;
33
+ /** Compute the SHA-256 of a file (hex). */
34
+ function hashFile(filePath) {
35
+ const size = fs_1.default.statSync(filePath).size;
36
+ const hash = (0, crypto_1.createHash)("sha256");
37
+ const fd = fs_1.default.openSync(filePath, "r");
38
+ try {
39
+ const buf = Buffer.alloc(64 * 1024);
40
+ let bytes = 0;
41
+ while ((bytes = fs_1.default.readSync(fd, buf, 0, buf.length, null)) !== 0) {
42
+ hash.update(buf.subarray(0, bytes));
43
+ }
44
+ }
45
+ finally {
46
+ fs_1.default.closeSync(fd);
47
+ }
48
+ return { sha256: hash.digest("hex"), size };
49
+ }
50
+ async function uploadToBlossom(opts) {
51
+ const fetchFn = opts.fetchImpl ?? fetch;
52
+ const contentType = opts.contentType ?? DEFAULT_CONTENT_TYPE;
53
+ const serverUrl = opts.serverUrl.replace(/\/$/, "");
54
+ const { sha256, size } = hashFile(opts.filePath);
55
+ // 1. HEAD /<sha256> — skip upload if the blob already exists.
56
+ const head = await fetchFn(`${serverUrl}/${sha256}`, { method: "HEAD" });
57
+ if (head.status === 200) {
58
+ return {
59
+ url: `${serverUrl}/${sha256}`,
60
+ sha256,
61
+ size,
62
+ type: contentType,
63
+ };
64
+ }
65
+ // 2. Build + sign the kind-24242 Blossom auth event.
66
+ const npubHex = await (0, nostr_1.getNpubHex)(opts.secret);
67
+ const expiration = Math.floor(Date.now() / 1000) + AUTH_TTL_SECONDS;
68
+ const authEvent = await (0, nostr_1.signNostrEvent)({
69
+ kind: 24242,
70
+ created_at: Math.floor(Date.now() / 1000),
71
+ tags: [
72
+ ["t", "upload"],
73
+ ["x", sha256],
74
+ ["expiration", String(expiration)],
75
+ ],
76
+ content: `Upload ${sha256}`,
77
+ }, opts.secret);
78
+ // 3. PUT /upload with the signed auth event in the Authorization header.
79
+ const authHeader = "Nostr " + Buffer.from(JSON.stringify(authEvent)).toString("base64");
80
+ const body = fs_1.default.readFileSync(opts.filePath);
81
+ const res = await fetchFn(`${serverUrl}/upload`, {
82
+ method: "PUT",
83
+ headers: {
84
+ Authorization: authHeader,
85
+ "Content-Type": contentType,
86
+ "X-SHA-256": sha256,
87
+ "Content-Length": String(size),
88
+ },
89
+ body,
90
+ });
91
+ if (res.status !== 200 && res.status !== 201) {
92
+ let detail = "";
93
+ try {
94
+ detail = await res.text();
95
+ }
96
+ catch {
97
+ /* ignore */
98
+ }
99
+ throw new BlossomError(`Blossom upload failed: HTTP ${res.status}${detail ? ` — ${detail.slice(0, 200)}` : ""}`, res.status);
100
+ }
101
+ // 4. Parse the blob descriptor (fall back to a constructed one).
102
+ const text = await res.text();
103
+ try {
104
+ const descriptor = JSON.parse(text);
105
+ if (!descriptor.url)
106
+ descriptor.url = `${serverUrl}/${sha256}`;
107
+ if (!descriptor.sha256)
108
+ descriptor.sha256 = sha256;
109
+ if (!descriptor.size)
110
+ descriptor.size = size;
111
+ if (!descriptor.type)
112
+ descriptor.type = contentType;
113
+ return descriptor;
114
+ }
115
+ catch {
116
+ return { url: `${serverUrl}/${sha256}`, sha256, size, type: contentType };
117
+ }
118
+ }
119
+ /** Convenience: hex npub → bech32 npub string for display. */
120
+ function npubHexToBech32(npubHex) {
121
+ return (0, nostr_1.pubkeyHexToNpub)(npubHex);
122
+ }