pakstr 0.19.3 → 0.20.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/dist/cli.js CHANGED
@@ -9,6 +9,7 @@ const sign_1 = require("./commands/sign");
9
9
  const publish_1 = require("./commands/publish");
10
10
  const run_1 = require("./commands/run");
11
11
  const init_1 = require("./commands/init");
12
+ const nsite_1 = require("./commands/nsite");
12
13
  const dotenv_1 = require("./core/dotenv");
13
14
  const package_json_1 = __importDefault(require("../package.json"));
14
15
  async function runCLI(argv) {
@@ -40,6 +41,12 @@ async function runCLI(argv) {
40
41
  case "sign":
41
42
  await (0, sign_1.signCommand)(configFlag ?? undefined);
42
43
  return;
44
+ case "nsite":
45
+ await (0, nsite_1.nsiteCommand)(configFlag ?? undefined, {
46
+ dir: extractFlag(args, "--dir"),
47
+ dryRun: args.includes("--dry-run"),
48
+ });
49
+ return;
43
50
  case "publish":
44
51
  await (0, publish_1.publishCommand)(configFlag ?? undefined, {
45
52
  dryRun: args.includes("--dry-run"),
@@ -75,6 +82,7 @@ Usage:
75
82
  pakstr init [--force] [--config <path>]
76
83
  pakstr build [--config <path>]
77
84
  pakstr sign [--config <path>]
85
+ pakstr nsite [--config <path>] [--dir <path>] [--dry-run]
78
86
  pakstr publish [--config <path>] [--dry-run] [--verify] [--out-json <path>]
79
87
  pakstr run [--config <path>] [--dry-run] [--verify-publish] [--publish-out-json <path>]
80
88
 
@@ -196,6 +196,22 @@ publish:
196
196
  source: upload
197
197
  # source: https://downloads.example.com/my-app.apk # Or an absolute public APK URL.
198
198
 
199
+ nsite:
200
+ enabled: true
201
+ relays:
202
+ - wss://nostr.cercatrova.me
203
+ - wss://relay.primal.net
204
+ - wss://nos.lol
205
+ - wss://relay.damus.io
206
+ servers:
207
+ - https://cdn.hzrd149.com
208
+ - https://cdn.sovbit.host
209
+ - https://cdn.nostrcheck.me
210
+ - https://nostr.download
211
+ publishProfile: true # Publishes only when explicit profile data is added.
212
+ publishRelayList: true
213
+ publishServerList: true
214
+
199
215
  relay: wss://relay.zapstore.dev
200
216
  # publishKey: PAKSTR_PUBLISH_NSEC # OPTIONAL. Omit to reuse ${nostr_1.PAKSTR_NSEC_ENV} for publishing.
201
217
 
@@ -0,0 +1,38 @@
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.nsiteCommand = nsiteCommand;
7
+ const path_1 = __importDefault(require("path"));
8
+ const nsite_1 = require("../core/nsite");
9
+ const nostr_1 = require("../core/nostr");
10
+ const pakstrConfig_1 = require("../core/pakstrConfig");
11
+ const zapStore_1 = require("../core/zapStore");
12
+ async function nsiteCommand(configPath, options = {}) {
13
+ const directoryOverride = options.dir ? path_1.default.resolve(options.dir) : undefined;
14
+ const config = (0, pakstrConfig_1.loadPakstrConfig)(configPath, { webOverride: directoryOverride });
15
+ const directory = directoryOverride ?? config.build.web;
16
+ const publishNsec = (0, nostr_1.resolvePublishNsec)(config.publish.publishKey, process.env);
17
+ const transport = options.transport ?? (options.dryRun ? new zapStore_1.MockRelayTransport() : new zapStore_1.WebsocketRelayTransport());
18
+ console.log("\n🌐 pakstr nsite" + (options.dryRun ? " (dry-run)" : ""));
19
+ console.log("Directory:", directory);
20
+ console.log("Publishing as:", publishNsec.envVar);
21
+ console.log("Blossom servers:", config.publish.nsite.servers.join(", "));
22
+ console.log("Relays:", config.publish.nsite.relays.join(", "));
23
+ const result = await (0, nsite_1.publishNsite)({
24
+ directory,
25
+ config: config.publish.nsite,
26
+ secret: publishNsec.bytes,
27
+ transport,
28
+ dryRun: options.dryRun,
29
+ fetchImpl: options.fetchImpl,
30
+ });
31
+ console.log(`Files: ${result.files.length} (${result.totalBytes} bytes)`);
32
+ console.log(`Events: ${result.events.map(item => item.event.kind).join(", ")}`);
33
+ if (result.partialFailures > 0)
34
+ console.log(`āš ļø Nsite published with ${result.partialFailures} target failure(s)`);
35
+ else
36
+ console.log(options.dryRun ? "āœ… Nsite dry run complete" : "āœ… Nsite published");
37
+ return result;
38
+ }
@@ -12,6 +12,7 @@ const giteaRelease_1 = require("../core/giteaRelease");
12
12
  const identityProof_1 = require("../core/identityProof");
13
13
  const nostr_1 = require("../core/nostr");
14
14
  const pakstrConfig_1 = require("../core/pakstrConfig");
15
+ const nsite_1 = require("../core/nsite");
15
16
  const zapStore_1 = require("../core/zapStore");
16
17
  const zapstoreConfig_1 = require("../core/zapstoreConfig");
17
18
  /** `pakstr publish` — optionally upload the signed APK and publish its Zapstore events. */
@@ -23,9 +24,40 @@ async function publishCommand(configPath, options = {}) {
23
24
  const apkPath = path_1.default.resolve(config.build.out);
24
25
  const uploadEnabled = config.publish.upload.enabled;
25
26
  const zapstoreEnabled = config.publish.zapstoreEnabled;
27
+ const nsiteEnabled = config.publish.nsite.enabled;
26
28
  const fetchImpl = options.fetchImpl ?? options.blossomFetch;
29
+ if (!uploadEnabled && !zapstoreEnabled && !nsiteEnabled) {
30
+ throw new Error("Nothing to publish: publish.upload, publish.zapstore, and publish.nsite are disabled");
31
+ }
32
+ const transport = options.transport ?? (options.dryRun ? new zapStore_1.MockRelayTransport() : new zapStore_1.WebsocketRelayTransport());
27
33
  if (!uploadEnabled && !zapstoreEnabled) {
28
- throw new Error("Nothing to publish: both publish.upload and publish.zapstore are disabled");
34
+ console.log("\nšŸ“¤ pakstr publish" + (options.dryRun ? " (dry-run)" : ""));
35
+ console.log("App:", config.app.appName, `(${config.app.appId})`);
36
+ console.log("Upload: disabled");
37
+ console.log("Zapstore: disabled");
38
+ const publishNsec = (0, zapStore_1.resolvePublishSecret)(config.publish.publishKey, process.env);
39
+ const nsite = await (0, nsite_1.publishNsite)({
40
+ directory: config.build.web,
41
+ config: config.publish.nsite,
42
+ secret: publishNsec.bytes,
43
+ transport,
44
+ dryRun: options.dryRun,
45
+ fetchImpl,
46
+ });
47
+ const result = { artifactUrl: "", uploadProvider: null, nsite };
48
+ if (options.outJson) {
49
+ fs_1.default.writeFileSync(options.outJson, JSON.stringify({
50
+ appId: config.app.appId,
51
+ appName: config.app.appName,
52
+ versionName: config.app.versionName,
53
+ versionCode: config.app.versionCode,
54
+ nsite,
55
+ publishedAt: new Date().toISOString(),
56
+ dryRun: !!options.dryRun,
57
+ }, null, 2) + "\n", "utf8");
58
+ console.log("šŸ“ Publish summary written to:", options.outJson);
59
+ }
60
+ return result;
29
61
  }
30
62
  console.log("\nšŸ“¤ pakstr publish" + (options.dryRun ? " (dry-run)" : ""));
31
63
  console.log("App:", config.app.appName, `(${config.app.appId})`);
@@ -59,7 +91,6 @@ async function publishCommand(configPath, options = {}) {
59
91
  console.log("āš ļø No .identity-proof sidecar found — kind 30509 will not be published.");
60
92
  }
61
93
  }
62
- const transport = options.transport ?? (options.dryRun ? new zapStore_1.MockRelayTransport() : new zapStore_1.WebsocketRelayTransport());
63
94
  let zapstoreConfig;
64
95
  let publishNsec;
65
96
  let npub;
@@ -236,11 +267,27 @@ async function publishCommand(configPath, options = {}) {
236
267
  throw new Error(`Artifact verification failed: HTTP ${response.status}`);
237
268
  }
238
269
  }
270
+ let nsiteResult;
271
+ if (nsiteEnabled) {
272
+ const nsiteSecret = publishNsec ?? (0, zapStore_1.resolvePublishSecret)(config.publish.publishKey, process.env);
273
+ nsiteResult = await (0, nsite_1.publishNsite)({
274
+ directory: config.build.web,
275
+ config: config.publish.nsite,
276
+ secret: nsiteSecret.bytes,
277
+ transport,
278
+ dryRun: options.dryRun,
279
+ fetchImpl,
280
+ });
281
+ if (nsiteResult.partialFailures > 0) {
282
+ console.log(`āš ļø Nsite published with ${nsiteResult.partialFailures} target failure(s)`);
283
+ }
284
+ }
239
285
  const result = {
240
286
  artifactUrl,
241
287
  uploadProvider,
242
288
  zapstore: zapstoreResult,
243
289
  ...(zapstoreResult ?? {}),
290
+ ...(nsiteResult ? { nsite: nsiteResult } : {}),
244
291
  };
245
292
  console.log("šŸ”— Artifact URL:", artifactUrl);
246
293
  if (options.outJson) {
@@ -267,6 +314,7 @@ async function publishCommand(configPath, options = {}) {
267
314
  identityProofEventId: zapstoreResult?.identityProofEventId,
268
315
  publishedAt: new Date().toISOString(),
269
316
  dryRun: !!options.dryRun,
317
+ ...(nsiteResult ? { nsite: nsiteResult } : {}),
270
318
  };
271
319
  fs_1.default.writeFileSync(options.outJson, JSON.stringify(summary, null, 2) + "\n", "utf8");
272
320
  console.log("šŸ“ Publish summary written to:", options.outJson);
@@ -27,16 +27,16 @@ async function runCommand(configPath, options = {}) {
27
27
  const config = (0, pakstrConfig_1.loadPakstrConfig)(configPath);
28
28
  // Verify PAKSTR_NSEC present (fail fast before any work).
29
29
  (0, nostr_1.requireSigningNsec)(process.env);
30
- // Pre-validate the Nostr publish nsec when Zapstore or Blossom needs it.
31
- if (config.publish.zapstoreEnabled || (config.publish.upload.enabled && config.publish.upload.provider === "blossom")) {
30
+ // Pre-validate the Nostr publish nsec when Zapstore, Blossom, or nsite needs it.
31
+ if (config.publish.zapstoreEnabled || config.publish.nsite.enabled || (config.publish.upload.enabled && config.publish.upload.provider === "blossom")) {
32
32
  if (!(0, nostr_1.isPublishNsecPresent)(config.publish.publishKey, process.env)) {
33
33
  const envVar = config.publish.publishKey === undefined
34
34
  ? "PAKSTR_NSEC"
35
35
  : config.publish.publishKey === null
36
36
  ? "PAKSTR_PUBLISH_NSEC"
37
37
  : config.publish.publishKey;
38
- throw new Error(`${envVar} is required for publishing and must not be empty ` +
39
- `(publish.zapstore.enabled is true). Pre-validation failed before any build work.`);
38
+ throw new Error(`${envVar} is required for publishing and must not be empty. ` +
39
+ `Pre-validation failed before any build work.`);
40
40
  }
41
41
  }
42
42
  console.log("\nšŸš€ pakstr run");
@@ -44,7 +44,7 @@ async function runCommand(configPath, options = {}) {
44
44
  console.log("Version:", config.app.versionName, `(${config.app.versionCode})`);
45
45
  console.log("Web:", config.build.web);
46
46
  console.log("Out:", path_1.default.resolve(config.build.out));
47
- const publishingEnabled = config.publish.zapstoreEnabled || config.publish.upload.enabled;
47
+ const publishingEnabled = config.publish.zapstoreEnabled || config.publish.upload.enabled || config.publish.nsite.enabled;
48
48
  console.log("Publish:", publishingEnabled ? "enabled" : "disabled");
49
49
  if (options.dryRun) {
50
50
  // Skip build + sign — no Docker, no APK. Only exercise the publish
@@ -95,10 +95,10 @@ async function uploadToBlossom(opts) {
95
95
  try {
96
96
  const descriptor = JSON.parse(text);
97
97
  if (descriptor.sha256 && descriptor.sha256 !== sha256) {
98
- throw new BlossomError("Blossom response SHA-256 does not match the uploaded APK");
98
+ throw new BlossomError("Blossom response SHA-256 does not match the uploaded file");
99
99
  }
100
100
  if (descriptor.size !== undefined && descriptor.size !== size) {
101
- throw new BlossomError("Blossom response size does not match the uploaded APK");
101
+ throw new BlossomError("Blossom response size does not match the uploaded file");
102
102
  }
103
103
  if (!descriptor.url)
104
104
  descriptor.url = `${serverUrl}/${sha256}`;
@@ -0,0 +1,176 @@
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.NSITE_MANIFEST_KIND = void 0;
7
+ exports.publishNsite = publishNsite;
8
+ exports.inspectNsiteDirectory = inspectNsiteDirectory;
9
+ exports.buildNsiteEventTemplates = buildNsiteEventTemplates;
10
+ const crypto_1 = require("crypto");
11
+ const fs_1 = __importDefault(require("fs"));
12
+ const path_1 = __importDefault(require("path"));
13
+ const blossom_1 = require("./blossom");
14
+ const nostr_1 = require("./nostr");
15
+ exports.NSITE_MANIFEST_KIND = 15128;
16
+ async function publishNsite(options) {
17
+ const files = inspectNsiteDirectory(options.directory);
18
+ const fileResults = [];
19
+ let partialFailures = 0;
20
+ for (const file of files) {
21
+ const servers = [];
22
+ if (options.dryRun) {
23
+ servers.push(...options.config.servers.map(target => ({ target, success: true, skipped: true })));
24
+ }
25
+ else {
26
+ const artifact = {
27
+ filePath: file.filePath,
28
+ filename: path_1.default.basename(file.filePath),
29
+ contentType: file.contentType,
30
+ sha256: file.sha256,
31
+ size: file.size,
32
+ };
33
+ for (const serverUrl of options.config.servers) {
34
+ try {
35
+ await (0, blossom_1.uploadToBlossom)({
36
+ serverUrl,
37
+ artifact,
38
+ secret: options.secret,
39
+ contentType: file.contentType,
40
+ fetchImpl: options.fetchImpl,
41
+ });
42
+ servers.push({ target: serverUrl, success: true });
43
+ }
44
+ catch (error) {
45
+ partialFailures++;
46
+ const status = typeof error === "object" && error !== null && "status" in error
47
+ ? Number(error.status)
48
+ : undefined;
49
+ servers.push({ target: serverUrl, success: false, ...(Number.isFinite(status) ? { status } : {}) });
50
+ }
51
+ }
52
+ if (!servers.some(result => result.success)) {
53
+ throw new Error(`Nsite upload failed for ${file.path} on every configured Blossom server`);
54
+ }
55
+ }
56
+ fileResults.push({ ...file, servers });
57
+ }
58
+ const createdAt = Math.floor((options.now?.() ?? Date.now()) / 1000);
59
+ const eventTemplates = buildNsiteEventTemplates(files, options.config, createdAt);
60
+ const events = [];
61
+ for (const template of eventTemplates) {
62
+ const event = await (0, nostr_1.signNostrEvent)(template, options.secret);
63
+ const relays = [];
64
+ if (options.dryRun) {
65
+ relays.push(...options.config.relays.map(target => ({ target, success: true, skipped: true })));
66
+ }
67
+ else {
68
+ for (const relayUrl of options.config.relays) {
69
+ try {
70
+ await options.transport.publish(event, relayUrl);
71
+ relays.push({ target: relayUrl, success: true });
72
+ }
73
+ catch {
74
+ partialFailures++;
75
+ relays.push({ target: relayUrl, success: false });
76
+ }
77
+ }
78
+ if (event.kind === exports.NSITE_MANIFEST_KIND && !relays.some(result => result.success)) {
79
+ throw new Error("Nsite manifest was rejected by every configured relay");
80
+ }
81
+ }
82
+ events.push({ event, relays });
83
+ }
84
+ return {
85
+ dryRun: !!options.dryRun,
86
+ directory: path_1.default.resolve(options.directory),
87
+ totalBytes: files.reduce((sum, file) => sum + file.size, 0),
88
+ files: fileResults,
89
+ events,
90
+ partialFailures,
91
+ };
92
+ }
93
+ function inspectNsiteDirectory(directory) {
94
+ const root = path_1.default.resolve(directory);
95
+ if (!fs_1.default.existsSync(root) || !fs_1.default.lstatSync(root).isDirectory()) {
96
+ throw new Error(`Nsite directory does not exist or is not a directory: ${root}`);
97
+ }
98
+ const indexPath = path_1.default.join(root, "index.html");
99
+ if (!fs_1.default.existsSync(indexPath) || !fs_1.default.lstatSync(indexPath).isFile()) {
100
+ throw new Error(`Nsite directory must contain index.html: ${root}`);
101
+ }
102
+ const files = [];
103
+ const walk = (current) => {
104
+ const entries = fs_1.default.readdirSync(current, { withFileTypes: true })
105
+ .sort((a, b) => a.name.localeCompare(b.name));
106
+ for (const entry of entries) {
107
+ const filePath = path_1.default.join(current, entry.name);
108
+ if (entry.isSymbolicLink())
109
+ continue;
110
+ if (entry.isDirectory()) {
111
+ walk(filePath);
112
+ }
113
+ else if (entry.isFile()) {
114
+ const bytes = fs_1.default.readFileSync(filePath);
115
+ const relative = path_1.default.relative(root, filePath).split(path_1.default.sep).join("/");
116
+ files.push({
117
+ filePath,
118
+ path: `/${relative}`,
119
+ sha256: (0, crypto_1.createHash)("sha256").update(bytes).digest("hex"),
120
+ size: bytes.length,
121
+ contentType: contentTypeFor(filePath),
122
+ });
123
+ }
124
+ }
125
+ };
126
+ walk(root);
127
+ return files.sort((a, b) => a.path.localeCompare(b.path));
128
+ }
129
+ function buildNsiteEventTemplates(files, config, createdAt) {
130
+ const templates = [{
131
+ kind: exports.NSITE_MANIFEST_KIND,
132
+ created_at: createdAt,
133
+ tags: [
134
+ ...files.map(file => ["path", file.path, file.sha256]),
135
+ ...config.servers.map(server => ["server", server]),
136
+ ...config.relays.map(relay => ["relay", relay]),
137
+ ["client", "pakstr"],
138
+ ],
139
+ content: "",
140
+ }];
141
+ if (config.publishRelayList) {
142
+ templates.push({ kind: 10002, created_at: createdAt, tags: config.relays.map(relay => ["r", relay, "write"]), content: "" });
143
+ }
144
+ if (config.publishServerList) {
145
+ templates.push({ kind: 10063, created_at: createdAt, tags: config.servers.map(server => ["server", server]), content: "" });
146
+ }
147
+ if (config.publishProfile && config.profile && Object.keys(config.profile).length > 0) {
148
+ templates.push({ kind: 0, created_at: createdAt, tags: [], content: JSON.stringify(config.profile) });
149
+ }
150
+ return templates;
151
+ }
152
+ function contentTypeFor(filePath) {
153
+ const types = {
154
+ ".css": "text/css; charset=utf-8",
155
+ ".gif": "image/gif",
156
+ ".html": "text/html; charset=utf-8",
157
+ ".ico": "image/x-icon",
158
+ ".jpeg": "image/jpeg",
159
+ ".jpg": "image/jpeg",
160
+ ".js": "text/javascript; charset=utf-8",
161
+ ".json": "application/json; charset=utf-8",
162
+ ".map": "application/json; charset=utf-8",
163
+ ".mjs": "text/javascript; charset=utf-8",
164
+ ".otf": "font/otf",
165
+ ".png": "image/png",
166
+ ".svg": "image/svg+xml",
167
+ ".txt": "text/plain; charset=utf-8",
168
+ ".wasm": "application/wasm",
169
+ ".webmanifest": "application/manifest+json",
170
+ ".webp": "image/webp",
171
+ ".woff": "font/woff",
172
+ ".woff2": "font/woff2",
173
+ ".xml": "application/xml",
174
+ };
175
+ return types[path_1.default.extname(filePath).toLowerCase()] ?? "application/octet-stream";
176
+ }
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.ConfigError = exports.APP_ID_PATTERN = exports.PERMISSION_ALIASES = exports.PAKSTR_CONFIG_FILENAME = void 0;
6
+ exports.ConfigError = exports.DEFAULT_NSITE_SERVERS = exports.DEFAULT_NSITE_RELAYS = exports.APP_ID_PATTERN = exports.PERMISSION_ALIASES = exports.PAKSTR_CONFIG_FILENAME = void 0;
7
7
  exports.sanitizeAppIdSegment = sanitizeAppIdSegment;
8
8
  exports.loadPakstrConfig = loadPakstrConfig;
9
9
  exports.validatePakstrConfig = validatePakstrConfig;
@@ -43,6 +43,18 @@ function sanitizeAppIdSegment(input) {
43
43
  s = s.replace(/^[^a-z]+/, ""); // drop leading non-letters (digits, underscores)
44
44
  return s.length > 0 ? s : null;
45
45
  }
46
+ exports.DEFAULT_NSITE_RELAYS = [
47
+ "wss://nostr.cercatrova.me",
48
+ "wss://relay.primal.net",
49
+ "wss://nos.lol",
50
+ "wss://relay.damus.io",
51
+ ];
52
+ exports.DEFAULT_NSITE_SERVERS = [
53
+ "https://cdn.hzrd149.com",
54
+ "https://cdn.sovbit.host",
55
+ "https://cdn.nostrcheck.me",
56
+ "https://nostr.download",
57
+ ];
46
58
  class ConfigError extends Error {
47
59
  configPath;
48
60
  constructor(message, configPath) {
@@ -59,7 +71,7 @@ function fail(message, configPath) {
59
71
  * Load and fully validate `pakstr.yaml` from `configPath` (defaults to
60
72
  * `pakstr.yaml` in `cwd`). Returns paths resolved relative to the yaml file.
61
73
  */
62
- function loadPakstrConfig(configPath) {
74
+ function loadPakstrConfig(configPath, options = {}) {
63
75
  const resolvedPath = path_1.default.resolve(configPath ?? path_1.default.join(process.cwd(), exports.PAKSTR_CONFIG_FILENAME));
64
76
  if (!fs_1.default.existsSync(resolvedPath) || !fs_1.default.lstatSync(resolvedPath).isFile()) {
65
77
  fail(`pakstr.yaml not found: ${resolvedPath}`);
@@ -76,7 +88,7 @@ function loadPakstrConfig(configPath) {
76
88
  if (raw === null || raw === undefined || typeof raw !== "object" || Array.isArray(raw)) {
77
89
  fail("pakstr.yaml must be a mapping with `app`, `build`, and optionally `publish` sections", resolvedPath);
78
90
  }
79
- return resolveAndValidate(raw, resolvedPath);
91
+ return resolveAndValidate(raw, resolvedPath, options);
80
92
  }
81
93
  /**
82
94
  * Validate an in-memory Pakstr configuration through `resolveAndValidate`.
@@ -86,7 +98,7 @@ function loadPakstrConfig(configPath) {
86
98
  function validatePakstrConfig(config, configPath) {
87
99
  resolveAndValidate(config, configPath ?? "");
88
100
  }
89
- function resolveAndValidate(config, configPath) {
101
+ function resolveAndValidate(config, configPath, options = {}) {
90
102
  const configDir = configPath ? path_1.default.dirname(configPath) : process.cwd();
91
103
  const where = (field) => configPath ? `${field} (in ${configPath})` : field;
92
104
  const app = config.app;
@@ -160,7 +172,7 @@ function resolveAndValidate(config, configPath) {
160
172
  if (!build || typeof build !== "object")
161
173
  fail(`Missing required section: build`, configPath);
162
174
  requireString(build.web, "build.web", configPath);
163
- const webAbs = path_1.default.resolve(configDir, build.web);
175
+ const webAbs = options.webOverride ? path_1.default.resolve(options.webOverride) : path_1.default.resolve(configDir, build.web);
164
176
  if (!fs_1.default.existsSync(webAbs) || !fs_1.default.lstatSync(webAbs).isDirectory()) {
165
177
  fail(`${where("build.web")} does not exist or is not a directory: ${webAbs}`, configPath);
166
178
  }
@@ -226,6 +238,31 @@ function resolveAndValidate(config, configPath) {
226
238
  }
227
239
  }
228
240
  const relay = publish?.relay ?? "wss://relay.zapstore.dev";
241
+ const rawNsite = publish?.nsite;
242
+ if (rawNsite !== undefined && (rawNsite === null || typeof rawNsite !== "object" || Array.isArray(rawNsite))) {
243
+ fail(`${where("publish.nsite")} must be a mapping`, configPath);
244
+ }
245
+ if (rawNsite?.enabled !== undefined && typeof rawNsite.enabled !== "boolean") {
246
+ fail(`${where("publish.nsite.enabled")} must be a boolean`, configPath);
247
+ }
248
+ const booleanNsiteFields = ["publishProfile", "publishRelayList", "publishServerList"];
249
+ for (const field of booleanNsiteFields) {
250
+ if (rawNsite?.[field] !== undefined && typeof rawNsite[field] !== "boolean") {
251
+ fail(`${where(`publish.nsite.${field}`)} must be a boolean`, configPath);
252
+ }
253
+ }
254
+ const nsiteRelays = resolveUrlList(rawNsite?.relays, exports.DEFAULT_NSITE_RELAYS, "publish.nsite.relays", ["ws:", "wss:"], configPath);
255
+ const nsiteServers = resolveUrlList(rawNsite?.servers, exports.DEFAULT_NSITE_SERVERS, "publish.nsite.servers", ["http:", "https:"], configPath).map(value => value.replace(/\/$/, ""));
256
+ const nsiteProfile = resolveNsiteProfile(rawNsite?.profile, configPath);
257
+ const nsite = {
258
+ enabled: rawNsite === undefined ? false : rawNsite.enabled ?? true,
259
+ relays: nsiteRelays,
260
+ servers: nsiteServers,
261
+ publishProfile: rawNsite?.publishProfile ?? true,
262
+ publishRelayList: rawNsite?.publishRelayList ?? true,
263
+ publishServerList: rawNsite?.publishServerList ?? true,
264
+ ...(nsiteProfile ? { profile: nsiteProfile } : {}),
265
+ };
229
266
  if (publish?.blossom !== undefined && publish.upload !== undefined) {
230
267
  fail(`${where("publish.blossom")} cannot be combined with publish.upload`, configPath);
231
268
  }
@@ -292,9 +329,51 @@ function resolveAndValidate(config, configPath) {
292
329
  app: resolvedApp,
293
330
  build: { web: webAbs, out, builder },
294
331
  runtime: resolvedRuntime,
295
- publish: { zapstoreEnabled, zapstoreSource, upload, publishKey, relay, blossom },
332
+ publish: { zapstoreEnabled, zapstoreSource, upload, nsite, publishKey, relay, blossom },
296
333
  };
297
334
  }
335
+ function resolveUrlList(value, defaults, field, protocols, configPath) {
336
+ const values = value === undefined ? defaults : value;
337
+ if (!Array.isArray(values) || values.length === 0 || values.some(item => typeof item !== "string" || item.length === 0)) {
338
+ fail(`${field} must be a non-empty list of URLs`, configPath);
339
+ }
340
+ const resolved = [];
341
+ for (const item of values) {
342
+ let parsed;
343
+ try {
344
+ parsed = new URL(item);
345
+ }
346
+ catch {
347
+ fail(`${field} entries must be absolute ${protocols.join("/")} URLs`, configPath);
348
+ }
349
+ if (!protocols.includes(parsed.protocol) || !parsed.hostname || parsed.username || parsed.password) {
350
+ fail(`${field} entries must be absolute ${protocols.join("/")} URLs without credentials`, configPath);
351
+ }
352
+ const normalized = parsed.toString().replace(/\/$/, "");
353
+ if (!resolved.includes(normalized))
354
+ resolved.push(normalized);
355
+ }
356
+ return resolved;
357
+ }
358
+ function resolveNsiteProfile(value, configPath) {
359
+ if (value === undefined)
360
+ return undefined;
361
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
362
+ fail("publish.nsite.profile must be a mapping", configPath);
363
+ }
364
+ const fields = ["name", "display_name", "about", "picture", "banner", "website", "nip05", "lud16", "lud06"];
365
+ const entries = [];
366
+ for (const field of fields) {
367
+ const fieldValue = value[field];
368
+ if (fieldValue !== undefined) {
369
+ if (typeof fieldValue !== "string" || fieldValue.length === 0) {
370
+ fail(`publish.nsite.profile.${field} must be a non-empty string`, configPath);
371
+ }
372
+ entries.push([field, fieldValue]);
373
+ }
374
+ }
375
+ return entries.length > 0 ? Object.fromEntries(entries) : undefined;
376
+ }
298
377
  function requireString(value, field, configPath) {
299
378
  if (typeof value !== "string" || value.length === 0) {
300
379
  fail(`Missing required field: ${field}`, configPath);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pakstr",
3
- "version": "0.19.3",
3
+ "version": "0.20.0",
4
4
  "description": "CLI for packaging Nostr web apps into Android APKs",
5
5
  "type": "commonjs",
6
6
  "main": "dist/index.js",