pakstr 0.23.0 → 0.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -76,6 +76,21 @@ npm run build
76
76
  npx pakstr run
77
77
  ```
78
78
 
79
+ **If your CI runs the job inside a sandbox with its own kernel** - a Kata microVM, or any
80
+ runner where the job container is given no host Docker socket - the job has to start its
81
+ own Docker daemon, and that daemon's default bridge cannot be brought up inside the
82
+ sandbox. Set `PAKSTR_DOCKER_NETWORK=host` so every container pakstr creates joins the
83
+ sandbox's own network namespace instead:
84
+
85
+ ```bash
86
+ PAKSTR_DOCKER_NETWORK=host npx pakstr run
87
+ ```
88
+
89
+ It applies to the build container, the signing container and the short-lived copy
90
+ containers alike, because all of them are created the same way. Leave it unset on a
91
+ workstation, where Docker's default network is what you want - host networking is not
92
+ available on Docker Desktop for macOS.
93
+
79
94
  ---
80
95
 
81
96
  ## Documentation & License
@@ -15,6 +15,18 @@ const pakstrConfig_1 = require("../core/pakstrConfig");
15
15
  const nsite_1 = require("../core/nsite");
16
16
  const zapStore_1 = require("../core/zapStore");
17
17
  const zapstoreConfig_1 = require("../core/zapstoreConfig");
18
+ function imageContentType(filePath) {
19
+ switch (path_1.default.extname(filePath).toLowerCase()) {
20
+ case ".png": return "image/png";
21
+ case ".jpg":
22
+ case ".jpeg": return "image/jpeg";
23
+ case ".gif": return "image/gif";
24
+ case ".webp": return "image/webp";
25
+ case ".svg": return "image/svg+xml";
26
+ case ".ico": return "image/x-icon";
27
+ default: return "application/octet-stream";
28
+ }
29
+ }
18
30
  /** `pakstr publish` — optionally upload the signed APK and publish its Zapstore events. */
19
31
  async function publishCommand(configPath, options = {}) {
20
32
  if (options.blossomFetch && options.fetchImpl) {
@@ -108,6 +120,15 @@ async function publishCommand(configPath, options = {}) {
108
120
  console.log("Publishing as:", publishNsec.envVar);
109
121
  console.log("Publisher npub:", npub);
110
122
  console.log("Relay:", config.publish.relay);
123
+ const iconUrl = zapstoreConfig?.icon
124
+ ? (await (0, blossom_1.uploadToBlossom)({
125
+ serverUrl: config.publish.blossom,
126
+ filePath: zapstoreConfig.icon,
127
+ secret: publishNsec.bytes,
128
+ contentType: imageContentType(zapstoreConfig.icon),
129
+ fetchImpl,
130
+ })).url
131
+ : undefined;
111
132
  app = {
112
133
  appId: config.app.appId,
113
134
  appName: zapstoreConfig?.name ?? config.app.appName,
@@ -118,6 +139,7 @@ async function publishCommand(configPath, options = {}) {
118
139
  tags: zapstoreConfig?.tags,
119
140
  license: zapstoreConfig?.license,
120
141
  website: zapstoreConfig?.website,
142
+ icon: iconUrl,
121
143
  releaseNotes: zapstoreConfig?.releaseNotes,
122
144
  repository: zapstoreConfig?.repository ?? (0, zapstoreConfig_1.loadZapstoreRepository)(config.configDir),
123
145
  };
@@ -77,6 +77,7 @@ async function buildAppMetadataEvent(input) {
77
77
  const npub = (0, nostr_1.pubkeyHexToNpub)(pubkeyHex);
78
78
  const communities = input.communities?.length ? input.communities : [exports.DEFAULT_COMMUNITY];
79
79
  const summaryTags = input.app.summary !== undefined ? [["summary", input.app.summary]] : [];
80
+ const iconTags = input.app.icon !== undefined ? [["icon", input.app.icon]] : [];
80
81
  const topicTags = (input.app.tags ?? []).map(tag => ["t", tag]);
81
82
  const licenseTags = input.app.license !== undefined ? [["license", input.app.license]] : [];
82
83
  const websiteTags = input.app.website !== undefined ? [["url", input.app.website]] : [];
@@ -88,6 +89,7 @@ async function buildAppMetadataEvent(input) {
88
89
  ["d", input.app.appId],
89
90
  ["name", input.app.appName],
90
91
  ...summaryTags,
92
+ ...iconTags,
91
93
  ["f", PLATFORM],
92
94
  ...communities.map(c => ["h", c]),
93
95
  ...topicTags,
@@ -58,6 +58,10 @@ function loadZapstorePublisherConfig(configDir) {
58
58
  const tags = optionalTags(config.tags, configPath);
59
59
  const license = optionalString(config.license, "license", configPath);
60
60
  const website = optionalString(config.website, "website", configPath);
61
+ const iconPath = optionalString(config.icon, "icon", configPath);
62
+ const icon = iconPath === undefined
63
+ ? undefined
64
+ : resolveIconPath(iconPath, configDir, configPath);
61
65
  const releaseNotesPath = optionalString(config.release_notes, "release_notes", configPath);
62
66
  const releaseNotes = releaseNotesPath === undefined
63
67
  ? undefined
@@ -79,6 +83,7 @@ function loadZapstorePublisherConfig(configDir) {
79
83
  tags,
80
84
  license,
81
85
  website,
86
+ icon,
82
87
  releaseNotes,
83
88
  };
84
89
  }
@@ -94,7 +99,7 @@ function optionalString(value, field, configPath, trim = true) {
94
99
  if (typeof value !== "string") {
95
100
  throw new ZapstoreConfigError(`${exports.ZAPSTORE_CONFIG_FILENAME} field ${field} must be a string`, configPath);
96
101
  }
97
- if (value.trim().length === 0 && field === "release_notes") {
102
+ if (value.trim().length === 0 && (field === "release_notes" || field === "icon")) {
98
103
  throw new ZapstoreConfigError(`${exports.ZAPSTORE_CONFIG_FILENAME} field ${field} must not be empty`, configPath);
99
104
  }
100
105
  return trim ? value.trim() : value;
@@ -112,6 +117,19 @@ function optionalTags(value, configPath) {
112
117
  return tag.trim();
113
118
  });
114
119
  }
120
+ function resolveIconPath(iconPath, configDir, configPath) {
121
+ const resolved = path_1.default.resolve(configDir, iconPath);
122
+ if (!fs_1.default.existsSync(resolved) || !fs_1.default.lstatSync(resolved).isFile()) {
123
+ throw new ZapstoreConfigError(`${exports.ZAPSTORE_CONFIG_FILENAME} icon file not found: ${resolved}`, configPath);
124
+ }
125
+ try {
126
+ fs_1.default.accessSync(resolved, fs_1.default.constants.R_OK);
127
+ }
128
+ catch (error) {
129
+ throw new ZapstoreConfigError(`Could not read ${exports.ZAPSTORE_CONFIG_FILENAME} icon file ${resolved}: ${error instanceof Error ? error.message : String(error)}`, configPath);
130
+ }
131
+ return resolved;
132
+ }
115
133
  function readReleaseNotes(releaseNotesPath, configDir, configPath) {
116
134
  const resolved = path_1.default.resolve(configDir, releaseNotesPath);
117
135
  if (!fs_1.default.existsSync(resolved) || !fs_1.default.lstatSync(resolved).isFile()) {
@@ -8,6 +8,7 @@ const fs_1 = __importDefault(require("fs"));
8
8
  const path_1 = __importDefault(require("path"));
9
9
  const crypto_1 = require("crypto");
10
10
  const process_1 = require("./process");
11
+ const dockerNetwork_1 = require("./dockerNetwork");
11
12
  const CONTAINER_RELEASE_APK_SIGNED = "/workspace/app/build/outputs/apk/release/app-release.apk";
12
13
  const CONTAINER_RELEASE_APK_UNSIGNED = "/workspace/app/build/outputs/apk/release/app-release-unsigned.apk";
13
14
  const CONTAINER_DEBUG_APK = "/workspace/app/build/outputs/apk/debug/app-debug.apk";
@@ -57,6 +58,7 @@ class DockerGradleRunner {
57
58
  this.execute("docker", [
58
59
  "run",
59
60
  "--rm",
61
+ ...(0, dockerNetwork_1.dockerNetworkArgs)(),
60
62
  "-v",
61
63
  `${storage.workspaceVolume}:/workspace`,
62
64
  this.image,
@@ -74,6 +76,7 @@ class DockerGradleRunner {
74
76
  this.execute("docker", [
75
77
  "run",
76
78
  "--rm",
79
+ ...(0, dockerNetwork_1.dockerNetworkArgs)(),
77
80
  "-v",
78
81
  `${storage.workspaceVolume}:/workspace`,
79
82
  this.image,
@@ -112,6 +115,7 @@ class DockerGradleRunner {
112
115
  try {
113
116
  this.execute("docker", [
114
117
  "create",
118
+ ...(0, dockerNetwork_1.dockerNetworkArgs)(),
115
119
  "--name",
116
120
  container,
117
121
  "-v",
@@ -132,6 +136,7 @@ class DockerGradleRunner {
132
136
  try {
133
137
  this.execute("docker", [
134
138
  "create",
139
+ ...(0, dockerNetwork_1.dockerNetworkArgs)(),
135
140
  "--name",
136
141
  container,
137
142
  "-v",
@@ -10,6 +10,7 @@ const fs_1 = __importDefault(require("fs"));
10
10
  const os_1 = __importDefault(require("os"));
11
11
  const path_1 = __importDefault(require("path"));
12
12
  const process_1 = require("./process");
13
+ const dockerNetwork_1 = require("./dockerNetwork");
13
14
  class DockerSignRunner {
14
15
  image;
15
16
  execute;
@@ -53,6 +54,7 @@ class DockerSignRunner {
53
54
  const stdout = this.execute("docker", [
54
55
  "run",
55
56
  "--rm",
57
+ ...(0, dockerNetwork_1.dockerNetworkArgs)(),
56
58
  "--tmpfs",
57
59
  "/signing:mode=0700,uid=0",
58
60
  "-v",
@@ -122,6 +124,7 @@ class DockerSignRunner {
122
124
  try {
123
125
  this.execute("docker", [
124
126
  "create",
127
+ ...(0, dockerNetwork_1.dockerNetworkArgs)(),
125
128
  "--name",
126
129
  container,
127
130
  "-v",
@@ -145,6 +148,7 @@ class DockerSignRunner {
145
148
  try {
146
149
  this.execute("docker", [
147
150
  "create",
151
+ ...(0, dockerNetwork_1.dockerNetworkArgs)(),
148
152
  "--name",
149
153
  container,
150
154
  "-v",
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.dockerNetworkArgs = dockerNetworkArgs;
4
+ /**
5
+ * The `--network` value passed to every container pakstr creates, read from
6
+ * PAKSTR_DOCKER_NETWORK.
7
+ *
8
+ * Unset means Docker's own default, which is what a workstation wants. A CI
9
+ * runner that sandboxes the job inside its own kernel needs the override: there
10
+ * the daemon's default bridge cannot be brought up - attaching a container to it
11
+ * writes to /proc/sys, which the sandbox exposes read-only - so `--network=host`
12
+ * puts the container in the sandbox's own network namespace instead, and the
13
+ * container inherits the resolvers the daemon itself uses.
14
+ *
15
+ * It is applied to `docker create` as well as `docker run`, because the workspace
16
+ * and artifact copy containers are created the same way and meet the same wall.
17
+ */
18
+ function dockerNetworkArgs() {
19
+ const network = process.env.PAKSTR_DOCKER_NETWORK?.trim();
20
+ return network ? ["--network", network] : [];
21
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pakstr",
3
- "version": "0.23.0",
3
+ "version": "0.25.0",
4
4
  "description": "CLI for packaging Nostr web apps into Android APKs",
5
5
  "type": "commonjs",
6
6
  "main": "dist/index.js",