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 +8 -0
- package/dist/commands/init.js +16 -0
- package/dist/commands/nsite.js +38 -0
- package/dist/commands/publish.js +50 -2
- package/dist/commands/run.js +5 -5
- package/dist/core/blossom.js +2 -2
- package/dist/core/nsite.js +176 -0
- package/dist/core/pakstrConfig.js +85 -6
- package/package.json +1 -1
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
|
|
package/dist/commands/init.js
CHANGED
|
@@ -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
|
+
}
|
package/dist/commands/publish.js
CHANGED
|
@@ -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
|
-
|
|
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);
|
package/dist/commands/run.js
CHANGED
|
@@ -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
|
|
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
|
-
`
|
|
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
|
package/dist/core/blossom.js
CHANGED
|
@@ -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
|
|
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
|
|
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);
|