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,51 @@
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.loadDotEnv = loadDotEnv;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const path_1 = __importDefault(require("path"));
9
+ /**
10
+ * Minimal `.env` loader for local development convenience.
11
+ *
12
+ * Parses a `.env` file (simple `KEY=VALUE` lines) and merges values into
13
+ * `process.env` WITHOUT overriding variables that are already set, so CI
14
+ * secret-manager values and explicit exports always win.
15
+ *
16
+ * This is purely a local-dev ergonomic: pakstr still reads secrets only from
17
+ * `process.env`. The `.env` file is gitignored (see `pakstr init`).
18
+ *
19
+ * Silently no-ops when the file is missing.
20
+ */
21
+ function loadDotEnv(dir = process.cwd()) {
22
+ const envPath = path_1.default.join(dir, ".env");
23
+ if (!fs_1.default.existsSync(envPath) || !fs_1.default.lstatSync(envPath).isFile()) {
24
+ return [];
25
+ }
26
+ const loaded = [];
27
+ const text = fs_1.default.readFileSync(envPath, "utf8");
28
+ for (const rawLine of text.split(/\r?\n/)) {
29
+ const line = rawLine.trim();
30
+ if (line.length === 0 || line.startsWith("#"))
31
+ continue;
32
+ const eq = line.indexOf("=");
33
+ if (eq <= 0)
34
+ continue;
35
+ const key = line.slice(0, eq).trim();
36
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key))
37
+ continue;
38
+ let value = line.slice(eq + 1).trim();
39
+ // Strip surrounding matching quotes.
40
+ if ((value.startsWith('"') && value.endsWith('"')) ||
41
+ (value.startsWith("'") && value.endsWith("'"))) {
42
+ value = value.slice(1, -1);
43
+ }
44
+ // Never override an existing env var (CI secrets / explicit exports win).
45
+ if (process.env[key] === undefined) {
46
+ process.env[key] = value;
47
+ loaded.push(key);
48
+ }
49
+ }
50
+ return loaded;
51
+ }
@@ -0,0 +1,153 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.NsecError = exports.NPUB_HRP = exports.NSEC_HRP = exports.PAKSTR_PUBLISH_NSEC_ENV = exports.PAKSTR_NSEC_ENV = void 0;
4
+ exports.decodeNsec = decodeNsec;
5
+ exports.encodeNsec = encodeNsec;
6
+ exports.encodeNpub = encodeNpub;
7
+ exports.requireSigningNsec = requireSigningNsec;
8
+ exports.resolvePublishNsec = resolvePublishNsec;
9
+ exports.isPublishNsecPresent = isPublishNsecPresent;
10
+ exports.signNostrEvent = signNostrEvent;
11
+ exports.getNpubHex = getNpubHex;
12
+ exports.pubkeyHexToNpub = pubkeyHexToNpub;
13
+ const base_1 = require("@scure/base");
14
+ const applesauce_signers_1 = require("applesauce-signers");
15
+ /**
16
+ * nsec + Nostr publish helpers.
17
+ *
18
+ * Uses applesauce (`PrivateKeySigner`) for secp256k1 event signing and
19
+ * `@scure/base` for Bech32 (nsec/npub) encode/decode. Never nostr-tools.
20
+ */
21
+ exports.PAKSTR_NSEC_ENV = "PAKSTR_NSEC";
22
+ exports.PAKSTR_PUBLISH_NSEC_ENV = "PAKSTR_PUBLISH_NSEC";
23
+ exports.NSEC_HRP = "nsec";
24
+ exports.NPUB_HRP = "npub";
25
+ const NSEC_PAYLOAD_BYTES = 32;
26
+ class NsecError extends Error {
27
+ envVar;
28
+ constructor(message, envVar) {
29
+ super(message);
30
+ this.envVar = envVar;
31
+ this.name = "NsecError";
32
+ }
33
+ }
34
+ exports.NsecError = NsecError;
35
+ /**
36
+ * Decode a Bech32 `nsec1…` string into 32 raw bytes.
37
+ * Validates: lowercase, HRP `nsec`, 32-byte payload, nonzero secp256k1 scalar.
38
+ */
39
+ function decodeNsec(value) {
40
+ if (typeof value !== "string" || value.length === 0) {
41
+ throw new NsecError("nsec is missing or empty");
42
+ }
43
+ if (value !== value.toLowerCase()) {
44
+ throw new NsecError("nsec must be lowercase bech32");
45
+ }
46
+ let decoded;
47
+ try {
48
+ decoded = base_1.bech32.decodeToBytes(value);
49
+ }
50
+ catch {
51
+ throw new NsecError("nsec is not valid bech32");
52
+ }
53
+ if (decoded.prefix !== exports.NSEC_HRP) {
54
+ throw new NsecError(`nsec must use the "${exports.NSEC_HRP}" human-readable part`);
55
+ }
56
+ const bytes = decoded.bytes;
57
+ if (bytes.length !== NSEC_PAYLOAD_BYTES) {
58
+ throw new NsecError("nsec payload must be exactly 32 bytes");
59
+ }
60
+ if (bytes.every(b => b === 0)) {
61
+ throw new NsecError("nsec must be a nonzero secret key");
62
+ }
63
+ // secp256k1 group order
64
+ const order = BigInt("0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141");
65
+ let scalar = 0n;
66
+ for (const b of bytes)
67
+ scalar = (scalar << 8n) | BigInt(b);
68
+ if (scalar === 0n || scalar >= order) {
69
+ throw new NsecError("nsec is not a valid secp256k1 private scalar");
70
+ }
71
+ return bytes;
72
+ }
73
+ /** Encode 32 raw bytes as a lowercase bech32 `nsec1…` string. */
74
+ function encodeNsec(bytes) {
75
+ if (bytes.length !== NSEC_PAYLOAD_BYTES) {
76
+ throw new NsecError("secret key must be exactly 32 bytes");
77
+ }
78
+ return base_1.bech32.encodeFromBytes(exports.NSEC_HRP, bytes);
79
+ }
80
+ /** Encode 32 raw pubkey bytes as `npub1…`. */
81
+ function encodeNpub(pubkey) {
82
+ return base_1.bech32.encodeFromBytes(exports.NPUB_HRP, pubkey);
83
+ }
84
+ /**
85
+ * Read and decode the signing nsec from `PAKSTR_NSEC`. Fails fast with a
86
+ * clear message if unset/empty/malformed. Never echoes the nsec value.
87
+ */
88
+ function requireSigningNsec(env = process.env) {
89
+ const value = env[exports.PAKSTR_NSEC_ENV];
90
+ if (value === undefined || value.length === 0) {
91
+ throw new NsecError(`${exports.PAKSTR_NSEC_ENV} is required and must not be empty`, exports.PAKSTR_NSEC_ENV);
92
+ }
93
+ const bytes = decodeNsec(value);
94
+ return { bytes, envVar: exports.PAKSTR_NSEC_ENV };
95
+ }
96
+ /**
97
+ * Resolve the publish nsec per SPEC §4.1.
98
+ *
99
+ * @param publishKey - value of `publish.publishKey` from pakstr.yaml:
100
+ * undefined → omitted entirely → reuse PAKSTR_NSEC;
101
+ * string (explicit) → read env var named by that string;
102
+ * null (bare key) → read PAKSTR_PUBLISH_NSEC.
103
+ * @param env - environment (defaults to process.env).
104
+ *
105
+ * Returns the resolved env-var name and decoded secret. Throws NsecError
106
+ * (fail fast) if the resolved env var is unset/empty/malformed.
107
+ */
108
+ function resolvePublishNsec(publishKey, env = process.env) {
109
+ if (publishKey === undefined) {
110
+ return requireSigningNsec(env);
111
+ }
112
+ const envVar = publishKey === null ? exports.PAKSTR_PUBLISH_NSEC_ENV : publishKey;
113
+ const value = env[envVar];
114
+ if (value === undefined || value.length === 0) {
115
+ throw new NsecError(`${envVar} is required for publishing and must not be empty`, envVar);
116
+ }
117
+ const bytes = decodeNsec(value);
118
+ return { bytes, envVar };
119
+ }
120
+ /** Only checks whether the resolved publish nsec env var is present & non-empty. */
121
+ function isPublishNsecPresent(publishKey, env = process.env) {
122
+ if (publishKey === undefined) {
123
+ const v = env[exports.PAKSTR_NSEC_ENV];
124
+ return v !== undefined && v.length > 0;
125
+ }
126
+ const envVar = publishKey === null ? exports.PAKSTR_PUBLISH_NSEC_ENV : publishKey;
127
+ const v = env[envVar];
128
+ return v !== undefined && v.length > 0;
129
+ }
130
+ /** Sign a Nostr event with a secret key (32 raw bytes). Uses applesauce. */
131
+ async function signNostrEvent(template, secret) {
132
+ const signer = new applesauce_signers_1.PrivateKeySigner(secret);
133
+ const event = await signer.signEvent({
134
+ kind: template.kind,
135
+ created_at: template.created_at,
136
+ tags: template.tags,
137
+ content: template.content,
138
+ });
139
+ return event;
140
+ }
141
+ /** Derive the hex npub (publisher identity) for a secret key. */
142
+ async function getNpubHex(secret) {
143
+ const signer = new applesauce_signers_1.PrivateKeySigner(secret);
144
+ return await signer.getPublicKey();
145
+ }
146
+ /** Convert a hex pubkey to bech32 npub. */
147
+ function pubkeyHexToNpub(hexPubkey) {
148
+ if (!/^[0-9a-f]{64}$/.test(hexPubkey)) {
149
+ throw new NsecError("pubkey must be 64 hex characters");
150
+ }
151
+ const bytes = Buffer.from(hexPubkey, "hex");
152
+ return encodeNpub(bytes);
153
+ }
@@ -0,0 +1,206 @@
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.ConfigError = exports.APP_ID_PATTERN = exports.PERMISSION_ALIASES = exports.PAKSTR_CONFIG_FILENAME = void 0;
7
+ exports.sanitizeAppIdSegment = sanitizeAppIdSegment;
8
+ exports.loadPakstrConfig = loadPakstrConfig;
9
+ exports.validatePakstrConfig = validatePakstrConfig;
10
+ const fs_1 = __importDefault(require("fs"));
11
+ const path_1 = __importDefault(require("path"));
12
+ const js_yaml_1 = __importDefault(require("js-yaml"));
13
+ /**
14
+ * Pakstr configuration model — the single `pakstr.yaml` file.
15
+ * Spec: docs/SPEC.md §3. Secrets are NEVER placed in this file.
16
+ */
17
+ exports.PAKSTR_CONFIG_FILENAME = "pakstr.yaml";
18
+ exports.PERMISSION_ALIASES = [
19
+ "INTERNET",
20
+ "CAMERA",
21
+ "NOTIFICATIONS",
22
+ "VIBRATE",
23
+ ];
24
+ const HEX_COLOR_RE = /^#[0-9A-Fa-f]{6}$/;
25
+ /**
26
+ * A valid Android / Java package name: dot-separated segments, each starting
27
+ * with a lower-case letter and containing only lower-case letters, digits, and
28
+ * underscores. Hyphens are NOT allowed (AAPT rejects them in `<manifest
29
+ * package>`). Used by both `pakstr init` (to sanitize inferred names) and
30
+ * config validation (to reject bad values).
31
+ */
32
+ exports.APP_ID_PATTERN = /^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)*$/;
33
+ /**
34
+ * Sanitize an arbitrary string (e.g. an npm package name like `my-pakstr-app`)
35
+ * into a valid Android package-name segment: lower-case, hyphens → underscores,
36
+ * strip anything other than [a-z0-9_], and prefix with `x` if it starts with a
37
+ * digit. Returns `null` if nothing usable remains.
38
+ */
39
+ function sanitizeAppIdSegment(input) {
40
+ let s = input.toLowerCase().replace(/[^a-z0-9_-]/g, "");
41
+ s = s.replace(/-/g, "_");
42
+ s = s.replace(/^[^a-z]+/, ""); // drop leading non-letters (digits, underscores)
43
+ return s.length > 0 ? s : null;
44
+ }
45
+ class ConfigError extends Error {
46
+ configPath;
47
+ constructor(message, configPath) {
48
+ super(message);
49
+ this.configPath = configPath;
50
+ this.name = "ConfigError";
51
+ }
52
+ }
53
+ exports.ConfigError = ConfigError;
54
+ function fail(message, configPath) {
55
+ throw new ConfigError(message, configPath);
56
+ }
57
+ /**
58
+ * Load and fully validate `pakstr.yaml` from `configPath` (defaults to
59
+ * `pakstr.yaml` in `cwd`). Returns paths resolved relative to the yaml file.
60
+ */
61
+ function loadPakstrConfig(configPath) {
62
+ const resolvedPath = path_1.default.resolve(configPath ?? path_1.default.join(process.cwd(), exports.PAKSTR_CONFIG_FILENAME));
63
+ if (!fs_1.default.existsSync(resolvedPath) || !fs_1.default.lstatSync(resolvedPath).isFile()) {
64
+ fail(`pakstr.yaml not found: ${resolvedPath}`);
65
+ }
66
+ let raw;
67
+ try {
68
+ raw = js_yaml_1.default.load(fs_1.default.readFileSync(resolvedPath, "utf8"), {
69
+ filename: resolvedPath,
70
+ });
71
+ }
72
+ catch (error) {
73
+ fail(`pakstr.yaml is not valid YAML: ${error instanceof Error ? error.message : "parse error"}`, resolvedPath);
74
+ }
75
+ if (raw === null || raw === undefined || typeof raw !== "object" || Array.isArray(raw)) {
76
+ fail("pakstr.yaml must be a mapping with `app`, `build`, and optionally `publish` sections", resolvedPath);
77
+ }
78
+ return resolveAndValidate(raw, resolvedPath);
79
+ }
80
+ function validatePakstrConfig(config, configPath) {
81
+ resolveAndValidate(config, configPath ?? "");
82
+ }
83
+ function resolveAndValidate(config, configPath) {
84
+ const configDir = path_1.default.dirname(configPath);
85
+ const where = (field) => configPath ? `${field} (in ${configPath})` : field;
86
+ const app = config.app;
87
+ if (!app || typeof app !== "object")
88
+ fail(`Missing required section: app`, configPath);
89
+ // Required app fields
90
+ requireString(app.appId, "app.appId", configPath);
91
+ requireString(app.appName, "app.appName", configPath);
92
+ requireString(app.versionName, "app.versionName", configPath);
93
+ requirePositiveInt(app.versionCode, "app.versionCode", configPath);
94
+ // appId rules (§3.3): non-empty, ≤255 UTF-8 bytes, equals trimmed form,
95
+ // and must be a valid Android/Java package name (segments of lower-case
96
+ // alphanumerics + underscores separated by dots; no hyphens). AAPT rejects
97
+ // hyphens in the `package` attribute with "not a valid Android package name".
98
+ if (app.appId !== app.appId.trim()) {
99
+ fail(`${where("app.appId")} must not have leading or trailing whitespace`, configPath);
100
+ }
101
+ if (Buffer.byteLength(app.appId, "utf8") > 255) {
102
+ fail(`${where("app.appId")} must be ≤ 255 UTF-8 bytes`, configPath);
103
+ }
104
+ if (!exports.APP_ID_PATTERN.test(app.appId)) {
105
+ fail(`${where("app.appId")} must be a valid Android package name: dot-separated segments of` +
106
+ ` lower-case letters, digits, and underscores (no hyphens). Got: "${app.appId}"`, configPath);
107
+ }
108
+ // appName / versionName basic sanity
109
+ if (/[\r\n]/.test(app.appName)) {
110
+ fail(`${where("app.appName")} must not contain line breaks`, configPath);
111
+ }
112
+ if (/[\r\n"\\]/.test(app.versionName)) {
113
+ fail(`${where("app.versionName")} must not contain line breaks, quotes, or backslashes`, configPath);
114
+ }
115
+ if (app.description !== undefined && typeof app.description !== "string") {
116
+ fail(`${where("app.description")} must be a string`, configPath);
117
+ }
118
+ // Optional color fields
119
+ if (app.backgroundColor !== undefined) {
120
+ assertHexColor(app.backgroundColor, "app.backgroundColor", configPath);
121
+ }
122
+ if (app.splash?.background !== undefined) {
123
+ assertHexColor(app.splash.background, "app.splash.background", configPath);
124
+ }
125
+ if (app.splash && !app.splash.image) {
126
+ fail(`${where("app.splash.image")} is required when splash is configured`, configPath);
127
+ }
128
+ // Permissions
129
+ if (app.permissions !== undefined) {
130
+ if (!Array.isArray(app.permissions) || app.permissions.some(p => typeof p !== "string")) {
131
+ fail(`${where("app.permissions")} must be a list of strings`, configPath);
132
+ }
133
+ for (const perm of app.permissions) {
134
+ const normalized = perm.trim().toUpperCase();
135
+ if (!exports.PERMISSION_ALIASES.includes(normalized)) {
136
+ fail(`${where("app.permissions")} has unknown entry "${perm}"; allowed: ${exports.PERMISSION_ALIASES.join(", ")}`, configPath);
137
+ }
138
+ }
139
+ }
140
+ // build section
141
+ const build = config.build;
142
+ if (!build || typeof build !== "object")
143
+ fail(`Missing required section: build`, configPath);
144
+ requireString(build.web, "build.web", configPath);
145
+ const webAbs = path_1.default.resolve(configDir, build.web);
146
+ if (!fs_1.default.existsSync(webAbs) || !fs_1.default.lstatSync(webAbs).isDirectory()) {
147
+ fail(`${where("build.web")} does not exist or is not a directory: ${webAbs}`, configPath);
148
+ }
149
+ if (!fs_1.default.existsSync(path_1.default.join(webAbs, "index.html"))) {
150
+ fail(`${where("build.web")} must contain index.html: ${webAbs}`, configPath);
151
+ }
152
+ if (build.builder !== undefined && build.builder !== "docker") {
153
+ fail(`${where("build.builder")} must be "docker" (only the docker builder is specified)`, configPath);
154
+ }
155
+ const builder = build.builder ?? "docker";
156
+ const outDefault = path_1.default.join(configDir, "build", `${app.appId}.apk`);
157
+ const out = build.out ? path_1.default.resolve(configDir, build.out) : outDefault;
158
+ // publish section
159
+ const publish = config.publish;
160
+ let zapstoreEnabled = true;
161
+ let publishKey = undefined;
162
+ if (publish !== undefined) {
163
+ if (publish.zapstore !== undefined) {
164
+ if (publish.zapstore.enabled !== undefined && typeof publish.zapstore.enabled !== "boolean") {
165
+ fail(`${where("publish.zapstore.enabled")} must be a boolean`, configPath);
166
+ }
167
+ zapstoreEnabled = publish.zapstore.enabled ?? true;
168
+ }
169
+ if (publish.publishKey !== undefined) {
170
+ if (publish.publishKey !== null && typeof publish.publishKey !== "string") {
171
+ fail(`${where("publish.publishKey")} must be a string or empty (bare key)`, configPath);
172
+ }
173
+ if (typeof publish.publishKey === "string" && publish.publishKey.trim().length === 0) {
174
+ // Treat empty string like a bare key.
175
+ publishKey = null;
176
+ }
177
+ else {
178
+ publishKey = publish.publishKey;
179
+ }
180
+ }
181
+ }
182
+ const relay = publish?.relay ?? "wss://relay.zapstore.dev";
183
+ const blossom = publish?.blossom ?? "https://cdn.zapstore.dev";
184
+ return {
185
+ configDir,
186
+ configPath,
187
+ app,
188
+ build: { web: webAbs, out, builder },
189
+ publish: { zapstoreEnabled, publishKey, relay, blossom },
190
+ };
191
+ }
192
+ function requireString(value, field, configPath) {
193
+ if (typeof value !== "string" || value.length === 0) {
194
+ fail(`Missing required field: ${field}`, configPath);
195
+ }
196
+ }
197
+ function requirePositiveInt(value, field, configPath) {
198
+ if (typeof value !== "number" || !Number.isInteger(value) || value <= 0) {
199
+ fail(`${field} must be a positive integer`, configPath);
200
+ }
201
+ }
202
+ function assertHexColor(value, field, configPath) {
203
+ if (typeof value !== "string" || !HEX_COLOR_RE.test(value)) {
204
+ fail(`${field} must be a hex color of the form #RRGGBB`, configPath);
205
+ }
206
+ }
@@ -0,0 +1,221 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MockRelayTransport = exports.WebsocketRelayTransport = exports.KIND_ASSET = exports.KIND_RELEASE = exports.KIND_APP_METADATA = exports.DEFAULT_COMMUNITY = void 0;
4
+ exports.buildSignedPublishEvents = buildSignedPublishEvents;
5
+ exports.publishToZapStore = publishToZapStore;
6
+ exports.resolvePublishSecret = resolvePublishSecret;
7
+ exports.publishEventToRelay = publishEventToRelay;
8
+ exports.verifyPublish = verifyPublish;
9
+ exports.queryRelayForEvent = queryRelayForEvent;
10
+ const nostr_1 = require("./nostr");
11
+ /** Default Zap Store catalog community (h tag). */
12
+ exports.DEFAULT_COMMUNITY = "acfeaea6e51420e8068fac446ca9d17d7a9ef6a5d20d93894e50fee3d4902a84";
13
+ /** NIP-82 event kinds. */
14
+ exports.KIND_APP_METADATA = 32267;
15
+ exports.KIND_RELEASE = 30063;
16
+ exports.KIND_ASSET = 3063;
17
+ /** Android platform identifier for the template's generic APK. */
18
+ const PLATFORM = "android-arm64-v8a";
19
+ const MIN_SDK = 28;
20
+ const TARGET_SDK = 36;
21
+ /** Build the three NIP-82 events (unsigned → signed) for an app release. */
22
+ async function buildSignedPublishEvents(input) {
23
+ const pubkeyHex = await (0, nostr_1.getNpubHex)(input.publishNsec.bytes);
24
+ const npub = (0, nostr_1.pubkeyHexToNpub)(pubkeyHex);
25
+ const now = Math.floor(Date.now() / 1000);
26
+ const communities = input.communities?.length ? input.communities : [exports.DEFAULT_COMMUNITY];
27
+ // kind 32267 — Software Application (app metadata)
28
+ const appMetadata = await (0, nostr_1.signNostrEvent)({
29
+ kind: exports.KIND_APP_METADATA,
30
+ created_at: now,
31
+ tags: [
32
+ ["d", input.app.appId],
33
+ ["name", input.app.appName],
34
+ ["f", PLATFORM],
35
+ ...communities.map(c => ["h", c]),
36
+ ],
37
+ content: input.app.description ?? "",
38
+ }, input.publishNsec.bytes);
39
+ // kind 3063 — Software Asset (the APK)
40
+ const asset = await (0, nostr_1.signNostrEvent)({
41
+ kind: exports.KIND_ASSET,
42
+ created_at: now,
43
+ tags: [
44
+ ["i", input.app.appId],
45
+ ["x", input.apk.apkSha256],
46
+ ["version", input.app.versionName],
47
+ ["url", input.apk.blossomUrl],
48
+ ["m", "application/vnd.android.package-archive"],
49
+ ["size", String(input.apk.apkSize)],
50
+ ["f", PLATFORM],
51
+ ["min_platform_version", String(MIN_SDK)],
52
+ ["target_platform_version", String(TARGET_SDK)],
53
+ ["filename", input.apk.filename],
54
+ ["version_code", String(input.app.versionCode)],
55
+ ["apk_certificate_hash", input.apk.signerCertificateSha256],
56
+ ],
57
+ content: "",
58
+ }, input.publishNsec.bytes);
59
+ // kind 30063 — Software Release (references the asset event)
60
+ const release = await (0, nostr_1.signNostrEvent)({
61
+ kind: exports.KIND_RELEASE,
62
+ created_at: now,
63
+ tags: [
64
+ ["i", input.app.appId],
65
+ ["version", input.app.versionName],
66
+ ["d", `${input.app.appId}@${input.app.versionName}`],
67
+ ["c", "main"],
68
+ ["f", PLATFORM],
69
+ ["e", asset.id, input.target.relayUrl],
70
+ ],
71
+ content: "",
72
+ }, input.publishNsec.bytes);
73
+ return { npub, pubkeyHex, appMetadata, release, asset };
74
+ }
75
+ /** Publish all three events to the relay via the given transport. */
76
+ async function publishToZapStore(input, transport) {
77
+ const built = await buildSignedPublishEvents(input);
78
+ await transport.publish(built.asset, input.target.relayUrl);
79
+ await transport.publish(built.release, input.target.relayUrl);
80
+ await transport.publish(built.appMetadata, input.target.relayUrl);
81
+ return {
82
+ npub: built.npub,
83
+ pubkeyHex: built.pubkeyHex,
84
+ appMetadataEventId: built.appMetadata.id,
85
+ releaseEventId: built.release.id,
86
+ assetEventId: built.asset.id,
87
+ events: {
88
+ appMetadata: built.appMetadata,
89
+ release: built.release,
90
+ asset: built.asset,
91
+ },
92
+ relayUrl: input.target.relayUrl,
93
+ };
94
+ }
95
+ /** Resolve the publish nsec for a loaded pakstr config per §4.1. */
96
+ function resolvePublishSecret(publishKey, env = process.env) {
97
+ return (0, nostr_1.resolvePublishNsec)(publishKey, env);
98
+ }
99
+ /** Real websocket transport: sends ["EVENT", e] and expects ["OK", id, true, …]. */
100
+ class WebsocketRelayTransport {
101
+ WebSocketCtor;
102
+ constructor(webSocketCtor) {
103
+ this.WebSocketCtor = (webSocketCtor ?? WebSocket);
104
+ }
105
+ async publish(event, relayUrl) {
106
+ await publishEventToRelay(this.WebSocketCtor, event, relayUrl);
107
+ }
108
+ }
109
+ exports.WebsocketRelayTransport = WebsocketRelayTransport;
110
+ /** A no-op transport for tests / dry-runs (no network). */
111
+ class MockRelayTransport {
112
+ published = [];
113
+ async publish(event, _relayUrl) {
114
+ this.published.push(event);
115
+ }
116
+ }
117
+ exports.MockRelayTransport = MockRelayTransport;
118
+ /** Low-level: publish one event to a relay over websocket, awaiting OK. */
119
+ function publishEventToRelay(WebSocketCtor, event, relayUrl, timeoutMs = 30000) {
120
+ return new Promise((resolve, reject) => {
121
+ let settled = false;
122
+ const ws = new WebSocketCtor(relayUrl);
123
+ const timer = setTimeout(() => {
124
+ if (settled)
125
+ return;
126
+ settled = true;
127
+ try {
128
+ ws.close();
129
+ }
130
+ catch { /* ignore */ }
131
+ reject(new Error(`Relay publish timed out: ${relayUrl}`));
132
+ }, timeoutMs);
133
+ ws.onopen = () => {
134
+ ws.send(JSON.stringify(["EVENT", event]));
135
+ };
136
+ ws.onmessage = (msg) => {
137
+ let data;
138
+ try {
139
+ data = JSON.parse(typeof msg.data === "string" ? msg.data : msg.data.toString());
140
+ }
141
+ catch {
142
+ return;
143
+ }
144
+ if (Array.isArray(data) && data[0] === "OK" && data[1] === event.id) {
145
+ settled = true;
146
+ clearTimeout(timer);
147
+ try {
148
+ ws.close();
149
+ }
150
+ catch { /* ignore */ }
151
+ if (data[2] === true)
152
+ resolve();
153
+ else
154
+ reject(new Error(`Relay rejected event ${event.id}: ${data[3] ?? "unknown"}`));
155
+ }
156
+ };
157
+ ws.onerror = (err) => {
158
+ if (settled)
159
+ return;
160
+ settled = true;
161
+ clearTimeout(timer);
162
+ reject(new Error(`Relay connection error: ${relayUrl} ${err?.message ?? ""}`));
163
+ };
164
+ });
165
+ }
166
+ /**
167
+ * Verify a publish by querying the relay for the asset event and confirming
168
+ * the APK is retrievable from Blossom. Returns true if both check out.
169
+ */
170
+ async function verifyPublish(input) {
171
+ const WebSocketCtor = input.WebSocketCtor ?? WebSocket;
172
+ const fetchFn = input.fetchImpl ?? fetch;
173
+ const eventFound = await queryRelayForEvent(WebSocketCtor, input.relayUrl, input.assetEventId);
174
+ const head = await fetchFn(input.blossomUrl, { method: "HEAD" });
175
+ return { eventFound, apkDownloadable: head.status === 200 };
176
+ }
177
+ /** Open a relay, send a REQ for a single event id, wait for it. */
178
+ function queryRelayForEvent(WebSocketCtor, relayUrl, eventId, timeoutMs = 30000) {
179
+ return new Promise((resolve) => {
180
+ let settled = false;
181
+ const ws = new WebSocketCtor(relayUrl);
182
+ const timer = setTimeout(() => {
183
+ if (settled)
184
+ return;
185
+ settled = true;
186
+ try {
187
+ ws.close();
188
+ }
189
+ catch { /* ignore */ }
190
+ resolve(false);
191
+ }, timeoutMs);
192
+ ws.onopen = () => {
193
+ ws.send(JSON.stringify(["REQ", "verify-" + eventId.slice(0, 8), { ids: [eventId] }]));
194
+ };
195
+ ws.onmessage = (msg) => {
196
+ let data;
197
+ try {
198
+ data = JSON.parse(typeof msg.data === "string" ? msg.data : msg.data.toString());
199
+ }
200
+ catch {
201
+ return;
202
+ }
203
+ if (Array.isArray(data) && data[0] === "EVENT" && data[2]?.id === eventId) {
204
+ settled = true;
205
+ clearTimeout(timer);
206
+ try {
207
+ ws.close();
208
+ }
209
+ catch { /* ignore */ }
210
+ resolve(true);
211
+ }
212
+ };
213
+ ws.onerror = () => {
214
+ if (settled)
215
+ return;
216
+ settled = true;
217
+ clearTimeout(timer);
218
+ resolve(false);
219
+ };
220
+ });
221
+ }