pakstr 0.13.3 ā 0.15.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/commands/publish.js +40 -11
- package/dist/core/nostr.js +30 -0
- package/dist/core/zapStore.js +101 -1
- package/dist/core/zapstoreConfig.js +125 -2
- package/package.json +1 -1
package/dist/commands/publish.js
CHANGED
|
@@ -26,10 +26,19 @@ const zapstoreConfig_1 = require("../core/zapstoreConfig");
|
|
|
26
26
|
async function publishCommand(configPath, options = {}) {
|
|
27
27
|
const config = (0, pakstrConfig_1.loadPakstrConfig)(configPath);
|
|
28
28
|
const apkPath = path_1.default.resolve(config.build.out);
|
|
29
|
+
const zapstoreConfig = options.dryRun
|
|
30
|
+
? undefined
|
|
31
|
+
: (0, zapstoreConfig_1.loadZapstorePublisherConfig)(config.configDir);
|
|
29
32
|
const publishNsec = (0, zapStore_1.resolvePublishSecret)(config.publish.publishKey, process.env);
|
|
33
|
+
const pubkeyHex = await (0, nostr_1.getNpubHex)(publishNsec.bytes);
|
|
34
|
+
const npub = (0, nostr_1.pubkeyHexToNpub)(pubkeyHex);
|
|
35
|
+
if (zapstoreConfig && zapstoreConfig.pubkeyHex !== pubkeyHex) {
|
|
36
|
+
throw new Error(`Publisher npub mismatch: ${zapstoreConfig_1.ZAPSTORE_CONFIG_FILENAME} configures ${zapstoreConfig.pubkey}, but the active Pakstr signer is ${npub}`);
|
|
37
|
+
}
|
|
30
38
|
console.log("\nš¤ pakstr publish" + (options.dryRun ? " (dry-run)" : ""));
|
|
31
39
|
console.log("App:", config.app.appName, `(${config.app.appId})`);
|
|
32
40
|
console.log("Publishing as:", publishNsec.envVar);
|
|
41
|
+
console.log("Publisher npub:", npub);
|
|
33
42
|
console.log("Relay:", config.publish.relay);
|
|
34
43
|
console.log("Blossom:", config.publish.blossom);
|
|
35
44
|
let blossomUrl;
|
|
@@ -39,13 +48,18 @@ async function publishCommand(configPath, options = {}) {
|
|
|
39
48
|
let filename = "";
|
|
40
49
|
let identityProof;
|
|
41
50
|
const transport = options.transport ?? (options.dryRun ? new zapStore_1.MockRelayTransport() : new zapStore_1.WebsocketRelayTransport());
|
|
42
|
-
const repository = (0, zapstoreConfig_1.loadZapstoreRepository)(config.configDir);
|
|
51
|
+
const repository = zapstoreConfig?.repository ?? (0, zapstoreConfig_1.loadZapstoreRepository)(config.configDir);
|
|
43
52
|
const app = {
|
|
44
53
|
appId: config.app.appId,
|
|
45
|
-
appName: config.app.appName,
|
|
54
|
+
appName: zapstoreConfig?.name ?? config.app.appName,
|
|
46
55
|
versionName: config.app.versionName,
|
|
47
56
|
versionCode: config.app.versionCode,
|
|
48
|
-
description: config.app.description,
|
|
57
|
+
description: zapstoreConfig?.description ?? config.app.description,
|
|
58
|
+
summary: zapstoreConfig?.summary,
|
|
59
|
+
tags: zapstoreConfig?.tags,
|
|
60
|
+
license: zapstoreConfig?.license,
|
|
61
|
+
website: zapstoreConfig?.website,
|
|
62
|
+
releaseNotes: zapstoreConfig?.releaseNotes,
|
|
49
63
|
repository,
|
|
50
64
|
};
|
|
51
65
|
const target = { relayUrl: config.publish.relay };
|
|
@@ -79,10 +93,25 @@ async function publishCommand(configPath, options = {}) {
|
|
|
79
93
|
console.log(" Re-run `pakstr sign` with a publish nsec available to generate it.");
|
|
80
94
|
}
|
|
81
95
|
}
|
|
82
|
-
// Publish the application event first
|
|
83
|
-
//
|
|
96
|
+
// Publish the application event first when Zapstore does not already have
|
|
97
|
+
// the current metadata, preserving first-publish repository authorization.
|
|
84
98
|
const appPublish = await (0, zapStore_1.buildAppMetadataEvent)({ app, publishNsec, target });
|
|
85
|
-
|
|
99
|
+
const existingAppMetadata = options.dryRun
|
|
100
|
+
? undefined
|
|
101
|
+
: await (0, zapStore_1.findAppMetadataEvent)(transport, {
|
|
102
|
+
appId: app.appId,
|
|
103
|
+
pubkeyHex,
|
|
104
|
+
relayUrl: config.publish.relay,
|
|
105
|
+
});
|
|
106
|
+
const appMetadata = existingAppMetadata && (0, zapStore_1.isCurrentAppMetadataEvent)(existingAppMetadata, appPublish.appMetadata)
|
|
107
|
+
? existingAppMetadata
|
|
108
|
+
: appPublish.appMetadata;
|
|
109
|
+
if (appMetadata === appPublish.appMetadata) {
|
|
110
|
+
await transport.publish(appMetadata, config.publish.relay);
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
console.log("š± Existing Zapstore application metadata is current; skipping kind 32267 publish.");
|
|
114
|
+
}
|
|
86
115
|
if (options.dryRun) {
|
|
87
116
|
// No network, no APK required. Synthesize a stub descriptor so the events
|
|
88
117
|
// can still be built and printed for preview.
|
|
@@ -97,7 +126,7 @@ async function publishCommand(configPath, options = {}) {
|
|
|
97
126
|
signature: "dry-run-stub==",
|
|
98
127
|
createdAt: Math.floor(Date.now() / 1000),
|
|
99
128
|
expiry: Math.floor(Date.now() / 1000) + 365 * 24 * 3600,
|
|
100
|
-
pubkeyHex
|
|
129
|
+
pubkeyHex,
|
|
101
130
|
};
|
|
102
131
|
console.log("š¦ (dry-run) Blossom URL:", blossomUrl);
|
|
103
132
|
}
|
|
@@ -133,14 +162,14 @@ async function publishCommand(configPath, options = {}) {
|
|
|
133
162
|
await transport.publish(releasePublish.identityProof, config.publish.relay);
|
|
134
163
|
}
|
|
135
164
|
const result = {
|
|
136
|
-
npub
|
|
137
|
-
pubkeyHex
|
|
138
|
-
appMetadataEventId:
|
|
165
|
+
npub,
|
|
166
|
+
pubkeyHex,
|
|
167
|
+
appMetadataEventId: appMetadata.id,
|
|
139
168
|
releaseEventId: releasePublish.release.id,
|
|
140
169
|
assetEventId: releasePublish.asset.id,
|
|
141
170
|
identityProofEventId: releasePublish.identityProof?.id,
|
|
142
171
|
events: {
|
|
143
|
-
appMetadata
|
|
172
|
+
appMetadata,
|
|
144
173
|
release: releasePublish.release,
|
|
145
174
|
asset: releasePublish.asset,
|
|
146
175
|
identityProof: releasePublish.identityProof,
|
package/dist/core/nostr.js
CHANGED
|
@@ -3,7 +3,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.NsecError = exports.NPUB_HRP = exports.NSEC_HRP = exports.PAKSTR_PUBLISH_NSEC_ENV = exports.PAKSTR_NSEC_ENV = void 0;
|
|
4
4
|
exports.decodeNsec = decodeNsec;
|
|
5
5
|
exports.encodeNsec = encodeNsec;
|
|
6
|
+
exports.decodeNpub = decodeNpub;
|
|
6
7
|
exports.encodeNpub = encodeNpub;
|
|
8
|
+
exports.npubToPubkeyHex = npubToPubkeyHex;
|
|
7
9
|
exports.requireSigningNsec = requireSigningNsec;
|
|
8
10
|
exports.resolvePublishNsec = resolvePublishNsec;
|
|
9
11
|
exports.isPublishNsecPresent = isPublishNsecPresent;
|
|
@@ -23,6 +25,7 @@ exports.PAKSTR_PUBLISH_NSEC_ENV = "PAKSTR_PUBLISH_NSEC";
|
|
|
23
25
|
exports.NSEC_HRP = "nsec";
|
|
24
26
|
exports.NPUB_HRP = "npub";
|
|
25
27
|
const NSEC_PAYLOAD_BYTES = 32;
|
|
28
|
+
const NPUB_PAYLOAD_BYTES = 32;
|
|
26
29
|
class NsecError extends Error {
|
|
27
30
|
envVar;
|
|
28
31
|
constructor(message, envVar) {
|
|
@@ -77,10 +80,37 @@ function encodeNsec(bytes) {
|
|
|
77
80
|
}
|
|
78
81
|
return base_1.bech32.encodeFromBytes(exports.NSEC_HRP, bytes);
|
|
79
82
|
}
|
|
83
|
+
/** Decode a Bech32 `npub1ā¦` string into 32 raw public-key bytes. */
|
|
84
|
+
function decodeNpub(value) {
|
|
85
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
86
|
+
throw new NsecError("npub is missing or empty");
|
|
87
|
+
}
|
|
88
|
+
if (value !== value.toLowerCase()) {
|
|
89
|
+
throw new NsecError("npub must be lowercase bech32");
|
|
90
|
+
}
|
|
91
|
+
let decoded;
|
|
92
|
+
try {
|
|
93
|
+
decoded = base_1.bech32.decodeToBytes(value);
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
throw new NsecError("npub is not valid bech32");
|
|
97
|
+
}
|
|
98
|
+
if (decoded.prefix !== exports.NPUB_HRP) {
|
|
99
|
+
throw new NsecError(`npub must use the "${exports.NPUB_HRP}" human-readable part`);
|
|
100
|
+
}
|
|
101
|
+
if (decoded.bytes.length !== NPUB_PAYLOAD_BYTES) {
|
|
102
|
+
throw new NsecError("npub payload must be exactly 32 bytes");
|
|
103
|
+
}
|
|
104
|
+
return decoded.bytes;
|
|
105
|
+
}
|
|
80
106
|
/** Encode 32 raw pubkey bytes as `npub1ā¦`. */
|
|
81
107
|
function encodeNpub(pubkey) {
|
|
82
108
|
return base_1.bech32.encodeFromBytes(exports.NPUB_HRP, pubkey);
|
|
83
109
|
}
|
|
110
|
+
/** Convert a bech32 npub to a lowercase hex public key. */
|
|
111
|
+
function npubToPubkeyHex(npub) {
|
|
112
|
+
return Buffer.from(decodeNpub(npub)).toString("hex");
|
|
113
|
+
}
|
|
84
114
|
/**
|
|
85
115
|
* Read and decode the signing nsec from `PAKSTR_NSEC`. Fails fast with a
|
|
86
116
|
* clear message if unset/empty/malformed. Never echoes the nsec value.
|
package/dist/core/zapStore.js
CHANGED
|
@@ -2,12 +2,15 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.MockRelayTransport = exports.WebsocketRelayTransport = exports.IDENTITY_PROOF_MESSAGE = exports.KIND_IDENTITY_PROOF = exports.KIND_ASSET = exports.KIND_RELEASE = exports.KIND_APP_METADATA = exports.DEFAULT_COMMUNITY = void 0;
|
|
4
4
|
exports.formatIdentityProofMessage = formatIdentityProofMessage;
|
|
5
|
+
exports.findAppMetadataEvent = findAppMetadataEvent;
|
|
6
|
+
exports.isCurrentAppMetadataEvent = isCurrentAppMetadataEvent;
|
|
5
7
|
exports.buildAppMetadataEvent = buildAppMetadataEvent;
|
|
6
8
|
exports.buildReleaseEvents = buildReleaseEvents;
|
|
7
9
|
exports.buildSignedPublishEvents = buildSignedPublishEvents;
|
|
8
10
|
exports.publishToZapStore = publishToZapStore;
|
|
9
11
|
exports.resolvePublishSecret = resolvePublishSecret;
|
|
10
12
|
exports.publishEventToRelay = publishEventToRelay;
|
|
13
|
+
exports.queryRelayForFilter = queryRelayForFilter;
|
|
11
14
|
exports.verifyPublish = verifyPublish;
|
|
12
15
|
exports.queryRelayForEvent = queryRelayForEvent;
|
|
13
16
|
const nostr_1 = require("./nostr");
|
|
@@ -38,11 +41,45 @@ function formatIdentityProofMessage(createdAt, expiry, pubkeyHex) {
|
|
|
38
41
|
const PLATFORM = "android-arm64-v8a";
|
|
39
42
|
const MIN_SDK = 28;
|
|
40
43
|
const TARGET_SDK = 36;
|
|
44
|
+
/** Query for this publisher's replaceable application event. */
|
|
45
|
+
async function findAppMetadataEvent(transport, input) {
|
|
46
|
+
if (!transport.query)
|
|
47
|
+
return undefined;
|
|
48
|
+
const event = await transport.query({
|
|
49
|
+
kinds: [exports.KIND_APP_METADATA],
|
|
50
|
+
authors: [input.pubkeyHex],
|
|
51
|
+
"#d": [input.appId],
|
|
52
|
+
limit: 1,
|
|
53
|
+
}, input.relayUrl);
|
|
54
|
+
if (!event)
|
|
55
|
+
return undefined;
|
|
56
|
+
const dTag = event.tags.find(tag => tag[0] === "d");
|
|
57
|
+
if (event.kind !== exports.KIND_APP_METADATA
|
|
58
|
+
|| event.pubkey !== input.pubkeyHex
|
|
59
|
+
|| dTag?.[1] !== input.appId) {
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
return event;
|
|
63
|
+
}
|
|
64
|
+
/** Whether an existing replaceable application event contains the current metadata. */
|
|
65
|
+
function isCurrentAppMetadataEvent(existing, expected) {
|
|
66
|
+
if (existing.kind !== expected.kind
|
|
67
|
+
|| existing.pubkey !== expected.pubkey
|
|
68
|
+
|| existing.content !== expected.content) {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
return expected.tags.every(expectedTag => existing.tags.some(existingTag => existingTag.length === expectedTag.length
|
|
72
|
+
&& existingTag.every((value, index) => value === expectedTag[index])));
|
|
73
|
+
}
|
|
41
74
|
/** Build the kind-32267 application event independently of the APK upload. */
|
|
42
75
|
async function buildAppMetadataEvent(input) {
|
|
43
76
|
const pubkeyHex = await (0, nostr_1.getNpubHex)(input.publishNsec.bytes);
|
|
44
77
|
const npub = (0, nostr_1.pubkeyHexToNpub)(pubkeyHex);
|
|
45
78
|
const communities = input.communities?.length ? input.communities : [exports.DEFAULT_COMMUNITY];
|
|
79
|
+
const summaryTags = input.app.summary !== undefined ? [["summary", input.app.summary]] : [];
|
|
80
|
+
const topicTags = (input.app.tags ?? []).map(tag => ["t", tag]);
|
|
81
|
+
const licenseTags = input.app.license !== undefined ? [["license", input.app.license]] : [];
|
|
82
|
+
const websiteTags = input.app.website !== undefined ? [["url", input.app.website]] : [];
|
|
46
83
|
const repositoryTags = input.app.repository ? [["repository", input.app.repository]] : [];
|
|
47
84
|
const appMetadata = await (0, nostr_1.signNostrEvent)({
|
|
48
85
|
kind: exports.KIND_APP_METADATA,
|
|
@@ -50,8 +87,12 @@ async function buildAppMetadataEvent(input) {
|
|
|
50
87
|
tags: [
|
|
51
88
|
["d", input.app.appId],
|
|
52
89
|
["name", input.app.appName],
|
|
90
|
+
...summaryTags,
|
|
53
91
|
["f", PLATFORM],
|
|
54
92
|
...communities.map(c => ["h", c]),
|
|
93
|
+
...topicTags,
|
|
94
|
+
...licenseTags,
|
|
95
|
+
...websiteTags,
|
|
55
96
|
...repositoryTags,
|
|
56
97
|
],
|
|
57
98
|
content: input.app.description ?? "",
|
|
@@ -95,7 +136,7 @@ async function buildReleaseEvents(input) {
|
|
|
95
136
|
["f", PLATFORM],
|
|
96
137
|
["e", asset.id, input.target.relayUrl],
|
|
97
138
|
],
|
|
98
|
-
content: "",
|
|
139
|
+
content: input.app.releaseNotes ?? "",
|
|
99
140
|
}, input.publishNsec.bytes);
|
|
100
141
|
// kind 30509 ā NIP-C1 Identity Proof (optional, links the APK signing
|
|
101
142
|
// certificate to the publisher Nostr identity). The signature is produced
|
|
@@ -171,6 +212,9 @@ class WebsocketRelayTransport {
|
|
|
171
212
|
constructor(webSocketCtor) {
|
|
172
213
|
this.WebSocketCtor = (webSocketCtor ?? WebSocket);
|
|
173
214
|
}
|
|
215
|
+
async query(filter, relayUrl) {
|
|
216
|
+
return await queryRelayForFilter(this.WebSocketCtor, relayUrl, filter);
|
|
217
|
+
}
|
|
174
218
|
async publish(event, relayUrl) {
|
|
175
219
|
await publishEventToRelay(this.WebSocketCtor, event, relayUrl);
|
|
176
220
|
}
|
|
@@ -179,6 +223,9 @@ exports.WebsocketRelayTransport = WebsocketRelayTransport;
|
|
|
179
223
|
/** A no-op transport for tests / dry-runs (no network). */
|
|
180
224
|
class MockRelayTransport {
|
|
181
225
|
published = [];
|
|
226
|
+
async query(_filter, _relayUrl) {
|
|
227
|
+
return undefined;
|
|
228
|
+
}
|
|
182
229
|
async publish(event, _relayUrl) {
|
|
183
230
|
this.published.push(event);
|
|
184
231
|
}
|
|
@@ -232,6 +279,59 @@ function publishEventToRelay(WebSocketCtor, event, relayUrl, timeoutMs = 30000)
|
|
|
232
279
|
};
|
|
233
280
|
});
|
|
234
281
|
}
|
|
282
|
+
/** Query a relay for the first event matching a Nostr filter. */
|
|
283
|
+
function queryRelayForFilter(WebSocketCtor, relayUrl, filter, timeoutMs = 30000) {
|
|
284
|
+
return new Promise((resolve, reject) => {
|
|
285
|
+
let settled = false;
|
|
286
|
+
const subscriptionId = `pakstr-app-${Date.now().toString(36)}`;
|
|
287
|
+
const ws = new WebSocketCtor(relayUrl);
|
|
288
|
+
const finish = (event, error) => {
|
|
289
|
+
if (settled)
|
|
290
|
+
return;
|
|
291
|
+
settled = true;
|
|
292
|
+
clearTimeout(timer);
|
|
293
|
+
try {
|
|
294
|
+
ws.send(JSON.stringify(["CLOSE", subscriptionId]));
|
|
295
|
+
}
|
|
296
|
+
catch { /* ignore */ }
|
|
297
|
+
try {
|
|
298
|
+
ws.close();
|
|
299
|
+
}
|
|
300
|
+
catch { /* ignore */ }
|
|
301
|
+
if (error)
|
|
302
|
+
reject(error);
|
|
303
|
+
else
|
|
304
|
+
resolve(event);
|
|
305
|
+
};
|
|
306
|
+
const timer = setTimeout(() => {
|
|
307
|
+
finish(undefined, new Error(`Relay query timed out: ${relayUrl}`));
|
|
308
|
+
}, timeoutMs);
|
|
309
|
+
ws.onopen = () => {
|
|
310
|
+
ws.send(JSON.stringify(["REQ", subscriptionId, filter]));
|
|
311
|
+
};
|
|
312
|
+
ws.onmessage = (msg) => {
|
|
313
|
+
let data;
|
|
314
|
+
try {
|
|
315
|
+
data = JSON.parse(typeof msg.data === "string" ? msg.data : msg.data.toString());
|
|
316
|
+
}
|
|
317
|
+
catch {
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
if (!Array.isArray(data) || data[1] !== subscriptionId)
|
|
321
|
+
return;
|
|
322
|
+
if (data[0] === "EVENT" && data[2])
|
|
323
|
+
finish(data[2]);
|
|
324
|
+
else if (data[0] === "EOSE")
|
|
325
|
+
finish(undefined);
|
|
326
|
+
else if (data[0] === "CLOSED") {
|
|
327
|
+
finish(undefined, new Error(`Relay closed query: ${relayUrl} ${data[2] ?? "unknown"}`));
|
|
328
|
+
}
|
|
329
|
+
};
|
|
330
|
+
ws.onerror = (error) => {
|
|
331
|
+
finish(undefined, new Error(`Relay connection error: ${relayUrl} ${error?.message ?? ""}`));
|
|
332
|
+
};
|
|
333
|
+
});
|
|
334
|
+
}
|
|
235
335
|
/**
|
|
236
336
|
* Verify a publish by querying the relay for the asset event and confirming
|
|
237
337
|
* the APK is retrievable from Blossom. Returns true if both check out.
|
|
@@ -3,12 +3,23 @@ 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.ZAPSTORE_CONFIG_FILENAME = void 0;
|
|
6
|
+
exports.ZapstoreConfigError = exports.ZAPSTORE_CONFIG_FILENAME = void 0;
|
|
7
7
|
exports.loadZapstoreRepository = loadZapstoreRepository;
|
|
8
|
+
exports.loadZapstorePublisherConfig = loadZapstorePublisherConfig;
|
|
8
9
|
const fs_1 = __importDefault(require("fs"));
|
|
9
10
|
const path_1 = __importDefault(require("path"));
|
|
10
11
|
const js_yaml_1 = __importDefault(require("js-yaml"));
|
|
12
|
+
const nostr_1 = require("./nostr");
|
|
11
13
|
exports.ZAPSTORE_CONFIG_FILENAME = "zapstore.yaml";
|
|
14
|
+
class ZapstoreConfigError extends Error {
|
|
15
|
+
configPath;
|
|
16
|
+
constructor(message, configPath) {
|
|
17
|
+
super(message);
|
|
18
|
+
this.configPath = configPath;
|
|
19
|
+
this.name = "ZapstoreConfigError";
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
exports.ZapstoreConfigError = ZapstoreConfigError;
|
|
12
23
|
/** Read the repository URL from the zapstore.yaml beside pakstr.yaml. */
|
|
13
24
|
function loadZapstoreRepository(configDir) {
|
|
14
25
|
const configPath = path_1.default.join(configDir, exports.ZAPSTORE_CONFIG_FILENAME);
|
|
@@ -18,5 +29,117 @@ function loadZapstoreRepository(configDir) {
|
|
|
18
29
|
const repository = parsed?.repository;
|
|
19
30
|
if (typeof repository !== "string" || repository.trim().length === 0)
|
|
20
31
|
return undefined;
|
|
21
|
-
return repository.trim();
|
|
32
|
+
return normalizeRepositoryUrl(repository.trim());
|
|
33
|
+
}
|
|
34
|
+
/** Load and validate the Zapstore publisher configuration used for publication. */
|
|
35
|
+
function loadZapstorePublisherConfig(configDir) {
|
|
36
|
+
const configPath = path_1.default.join(configDir, exports.ZAPSTORE_CONFIG_FILENAME);
|
|
37
|
+
if (!fs_1.default.existsSync(configPath)) {
|
|
38
|
+
throw new ZapstoreConfigError(`Zapstore configuration not found: ${configPath}`, configPath);
|
|
39
|
+
}
|
|
40
|
+
let parsed;
|
|
41
|
+
try {
|
|
42
|
+
parsed = js_yaml_1.default.load(fs_1.default.readFileSync(configPath, "utf8"), { filename: configPath });
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
throw new ZapstoreConfigError(`Invalid ${exports.ZAPSTORE_CONFIG_FILENAME}: ${error instanceof Error ? error.message : String(error)}`, configPath);
|
|
46
|
+
}
|
|
47
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
48
|
+
throw new ZapstoreConfigError(`${exports.ZAPSTORE_CONFIG_FILENAME} must contain a YAML mapping`, configPath);
|
|
49
|
+
}
|
|
50
|
+
const config = parsed;
|
|
51
|
+
const rawRepository = requireString(config.repository, "repository", configPath);
|
|
52
|
+
validateRepositoryUrl(rawRepository, configPath);
|
|
53
|
+
const repository = normalizeRepositoryUrl(rawRepository);
|
|
54
|
+
const pubkey = requireString(config.pubkey, "pubkey", configPath);
|
|
55
|
+
const name = optionalString(config.name, "name", configPath);
|
|
56
|
+
const summary = optionalString(config.summary, "summary", configPath);
|
|
57
|
+
const description = optionalString(config.description, "description", configPath, false);
|
|
58
|
+
const tags = optionalTags(config.tags, configPath);
|
|
59
|
+
const license = optionalString(config.license, "license", configPath);
|
|
60
|
+
const website = optionalString(config.website, "website", configPath);
|
|
61
|
+
const releaseNotesPath = optionalString(config.release_notes, "release_notes", configPath);
|
|
62
|
+
const releaseNotes = releaseNotesPath === undefined
|
|
63
|
+
? undefined
|
|
64
|
+
: readReleaseNotes(releaseNotesPath, configDir, configPath);
|
|
65
|
+
let pubkeyHex;
|
|
66
|
+
try {
|
|
67
|
+
pubkeyHex = (0, nostr_1.npubToPubkeyHex)(pubkey);
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
throw new ZapstoreConfigError(`Invalid publisher npub in ${exports.ZAPSTORE_CONFIG_FILENAME}: ${error instanceof Error ? error.message : String(error)}`, configPath);
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
repository,
|
|
74
|
+
pubkey,
|
|
75
|
+
pubkeyHex,
|
|
76
|
+
name,
|
|
77
|
+
summary,
|
|
78
|
+
description,
|
|
79
|
+
tags,
|
|
80
|
+
license,
|
|
81
|
+
website,
|
|
82
|
+
releaseNotes,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
function requireString(value, field, configPath) {
|
|
86
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
87
|
+
throw new ZapstoreConfigError(`${exports.ZAPSTORE_CONFIG_FILENAME} is missing required field: ${field}`, configPath);
|
|
88
|
+
}
|
|
89
|
+
return value.trim();
|
|
90
|
+
}
|
|
91
|
+
function optionalString(value, field, configPath, trim = true) {
|
|
92
|
+
if (value === undefined)
|
|
93
|
+
return undefined;
|
|
94
|
+
if (typeof value !== "string") {
|
|
95
|
+
throw new ZapstoreConfigError(`${exports.ZAPSTORE_CONFIG_FILENAME} field ${field} must be a string`, configPath);
|
|
96
|
+
}
|
|
97
|
+
if (value.trim().length === 0 && field === "release_notes") {
|
|
98
|
+
throw new ZapstoreConfigError(`${exports.ZAPSTORE_CONFIG_FILENAME} field ${field} must not be empty`, configPath);
|
|
99
|
+
}
|
|
100
|
+
return trim ? value.trim() : value;
|
|
101
|
+
}
|
|
102
|
+
function optionalTags(value, configPath) {
|
|
103
|
+
if (value === undefined)
|
|
104
|
+
return undefined;
|
|
105
|
+
if (!Array.isArray(value)) {
|
|
106
|
+
throw new ZapstoreConfigError(`${exports.ZAPSTORE_CONFIG_FILENAME} field tags must be an array of non-empty strings`, configPath);
|
|
107
|
+
}
|
|
108
|
+
return value.map(tag => {
|
|
109
|
+
if (typeof tag !== "string" || tag.trim().length === 0) {
|
|
110
|
+
throw new ZapstoreConfigError(`${exports.ZAPSTORE_CONFIG_FILENAME} field tags must be an array of non-empty strings`, configPath);
|
|
111
|
+
}
|
|
112
|
+
return tag.trim();
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
function readReleaseNotes(releaseNotesPath, configDir, configPath) {
|
|
116
|
+
const resolved = path_1.default.resolve(configDir, releaseNotesPath);
|
|
117
|
+
if (!fs_1.default.existsSync(resolved) || !fs_1.default.lstatSync(resolved).isFile()) {
|
|
118
|
+
throw new ZapstoreConfigError(`${exports.ZAPSTORE_CONFIG_FILENAME} release_notes file not found: ${resolved}`, configPath);
|
|
119
|
+
}
|
|
120
|
+
try {
|
|
121
|
+
return fs_1.default.readFileSync(resolved, "utf8");
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
throw new ZapstoreConfigError(`Could not read ${exports.ZAPSTORE_CONFIG_FILENAME} release_notes file ${resolved}: ${error instanceof Error ? error.message : String(error)}`, configPath);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
function normalizeRepositoryUrl(repository) {
|
|
128
|
+
const queryIndex = repository.indexOf("?");
|
|
129
|
+
const fragmentIndex = repository.indexOf("#");
|
|
130
|
+
if (queryIndex !== -1 || fragmentIndex !== -1)
|
|
131
|
+
return repository;
|
|
132
|
+
return repository.endsWith(".git") ? repository.slice(0, -4) : repository;
|
|
133
|
+
}
|
|
134
|
+
function validateRepositoryUrl(repository, configPath) {
|
|
135
|
+
let parsed;
|
|
136
|
+
try {
|
|
137
|
+
parsed = new URL(repository);
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
throw new ZapstoreConfigError(`${exports.ZAPSTORE_CONFIG_FILENAME} repository must be a valid absolute URL`, configPath);
|
|
141
|
+
}
|
|
142
|
+
if (!["https:", "http:", "ssh:"].includes(parsed.protocol) || !parsed.hostname) {
|
|
143
|
+
throw new ZapstoreConfigError(`${exports.ZAPSTORE_CONFIG_FILENAME} repository must use an http, https, or ssh URL with a host`, configPath);
|
|
144
|
+
}
|
|
22
145
|
}
|