pakstr 0.13.2 → 0.14.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 +90 -37
- package/dist/core/nostr.js +30 -0
- package/dist/core/zapStore.js +121 -7
- package/dist/core/zapstoreConfig.js +80 -0
- package/package.json +1 -1
package/dist/commands/publish.js
CHANGED
|
@@ -11,9 +11,10 @@ const zapStore_1 = require("../core/zapStore");
|
|
|
11
11
|
const blossom_1 = require("../core/blossom");
|
|
12
12
|
const nostr_1 = require("../core/nostr");
|
|
13
13
|
const identityProof_1 = require("../core/identityProof");
|
|
14
|
+
const zapstoreConfig_1 = require("../core/zapstoreConfig");
|
|
14
15
|
/**
|
|
15
|
-
* `pakstr publish` —
|
|
16
|
-
*
|
|
16
|
+
* `pakstr publish` — publish the NIP-82 application event, upload the signed APK
|
|
17
|
+
* to Blossom, then publish the APK-dependent events. Spec §5.4 / §8.
|
|
17
18
|
*
|
|
18
19
|
* Requires that `pakstr build` + `pakstr sign` have already run (the signed
|
|
19
20
|
* APK at build.out, plus its `.signer-sha256` sidecar).
|
|
@@ -25,7 +26,15 @@ const identityProof_1 = require("../core/identityProof");
|
|
|
25
26
|
async function publishCommand(configPath, options = {}) {
|
|
26
27
|
const config = (0, pakstrConfig_1.loadPakstrConfig)(configPath);
|
|
27
28
|
const apkPath = path_1.default.resolve(config.build.out);
|
|
29
|
+
const zapstoreConfig = options.dryRun
|
|
30
|
+
? undefined
|
|
31
|
+
: (0, zapstoreConfig_1.loadZapstorePublisherConfig)(config.configDir);
|
|
28
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
|
+
}
|
|
29
38
|
console.log("\n📤 pakstr publish" + (options.dryRun ? " (dry-run)" : ""));
|
|
30
39
|
console.log("App:", config.app.appName, `(${config.app.appId})`);
|
|
31
40
|
console.log("Publishing as:", publishNsec.envVar);
|
|
@@ -34,28 +43,21 @@ async function publishCommand(configPath, options = {}) {
|
|
|
34
43
|
let blossomUrl;
|
|
35
44
|
let apkSha256;
|
|
36
45
|
let apkSize;
|
|
37
|
-
let signerCertificateSha256;
|
|
38
|
-
let filename;
|
|
46
|
+
let signerCertificateSha256 = "";
|
|
47
|
+
let filename = "";
|
|
39
48
|
let identityProof;
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
createdAt: Math.floor(Date.now() / 1000),
|
|
53
|
-
expiry: Math.floor(Date.now() / 1000) + 365 * 24 * 3600,
|
|
54
|
-
pubkeyHex: await (0, nostr_1.getNpubHex)(publishNsec.bytes),
|
|
55
|
-
};
|
|
56
|
-
console.log("📦 (dry-run) Blossom URL:", blossomUrl);
|
|
57
|
-
}
|
|
58
|
-
else {
|
|
49
|
+
const transport = options.transport ?? (options.dryRun ? new zapStore_1.MockRelayTransport() : new zapStore_1.WebsocketRelayTransport());
|
|
50
|
+
const repository = zapstoreConfig?.repository ?? (0, zapstoreConfig_1.loadZapstoreRepository)(config.configDir);
|
|
51
|
+
const app = {
|
|
52
|
+
appId: config.app.appId,
|
|
53
|
+
appName: config.app.appName,
|
|
54
|
+
versionName: config.app.versionName,
|
|
55
|
+
versionCode: config.app.versionCode,
|
|
56
|
+
description: config.app.description,
|
|
57
|
+
repository,
|
|
58
|
+
};
|
|
59
|
+
const target = { relayUrl: config.publish.relay };
|
|
60
|
+
if (!options.dryRun) {
|
|
59
61
|
if (!fs_1.default.existsSync(apkPath) || !fs_1.default.lstatSync(apkPath).isFile()) {
|
|
60
62
|
throw new Error(`Signed APK not found at ${apkPath}. Run \`pakstr build\` and \`pakstr sign\` first.`);
|
|
61
63
|
}
|
|
@@ -64,6 +66,7 @@ async function publishCommand(configPath, options = {}) {
|
|
|
64
66
|
throw new Error(`Signer certificate sidecar not found: ${certShaPath}. Run \`pakstr sign\` first.`);
|
|
65
67
|
}
|
|
66
68
|
signerCertificateSha256 = fs_1.default.readFileSync(certShaPath, "utf8").trim();
|
|
69
|
+
filename = path_1.default.basename(apkPath);
|
|
67
70
|
// Read the NIP-C1 identity proof sidecar (optional; generated by
|
|
68
71
|
// `pakstr sign` when a publish nsec is available).
|
|
69
72
|
const proofPath = `${apkPath}.identity-proof`;
|
|
@@ -81,8 +84,47 @@ async function publishCommand(configPath, options = {}) {
|
|
|
81
84
|
// No sidecar found — warn so users can spot a missing 30509 proof
|
|
82
85
|
// in CI output (the sign step may have run without a publish nsec).
|
|
83
86
|
console.log("⚠️ No .identity-proof sidecar found — the kind 30509 identity proof will not be published.");
|
|
84
|
-
console.log(" Re-run
|
|
87
|
+
console.log(" Re-run `pakstr sign` with a publish nsec available to generate it.");
|
|
85
88
|
}
|
|
89
|
+
}
|
|
90
|
+
// Publish the application event first when Zapstore does not already have
|
|
91
|
+
// the current metadata, preserving first-publish repository authorization.
|
|
92
|
+
const appPublish = await (0, zapStore_1.buildAppMetadataEvent)({ app, publishNsec, target });
|
|
93
|
+
const existingAppMetadata = options.dryRun
|
|
94
|
+
? undefined
|
|
95
|
+
: await (0, zapStore_1.findAppMetadataEvent)(transport, {
|
|
96
|
+
appId: app.appId,
|
|
97
|
+
pubkeyHex,
|
|
98
|
+
relayUrl: config.publish.relay,
|
|
99
|
+
});
|
|
100
|
+
const appMetadata = existingAppMetadata && (0, zapStore_1.isCurrentAppMetadataEvent)(existingAppMetadata, appPublish.appMetadata)
|
|
101
|
+
? existingAppMetadata
|
|
102
|
+
: appPublish.appMetadata;
|
|
103
|
+
if (appMetadata === appPublish.appMetadata) {
|
|
104
|
+
await transport.publish(appMetadata, config.publish.relay);
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
console.log("📱 Existing Zapstore application metadata is current; skipping kind 32267 publish.");
|
|
108
|
+
}
|
|
109
|
+
if (options.dryRun) {
|
|
110
|
+
// No network, no APK required. Synthesize a stub descriptor so the events
|
|
111
|
+
// can still be built and printed for preview.
|
|
112
|
+
apkSha256 = "0".repeat(64);
|
|
113
|
+
apkSize = 0;
|
|
114
|
+
blossomUrl = `${config.publish.blossom.replace(/\/$/, "")}/${apkSha256}`;
|
|
115
|
+
signerCertificateSha256 = "0".repeat(64);
|
|
116
|
+
filename = path_1.default.basename(apkPath);
|
|
117
|
+
// Stub identity proof so the 30509 event path is exercised end-to-end.
|
|
118
|
+
identityProof = {
|
|
119
|
+
certHash: signerCertificateSha256,
|
|
120
|
+
signature: "dry-run-stub==",
|
|
121
|
+
createdAt: Math.floor(Date.now() / 1000),
|
|
122
|
+
expiry: Math.floor(Date.now() / 1000) + 365 * 24 * 3600,
|
|
123
|
+
pubkeyHex,
|
|
124
|
+
};
|
|
125
|
+
console.log("📦 (dry-run) Blossom URL:", blossomUrl);
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
86
128
|
const blossom = await (0, blossom_1.uploadToBlossom)({
|
|
87
129
|
serverUrl: config.publish.blossom,
|
|
88
130
|
filePath: apkPath,
|
|
@@ -92,20 +134,11 @@ async function publishCommand(configPath, options = {}) {
|
|
|
92
134
|
blossomUrl = blossom.url;
|
|
93
135
|
apkSha256 = blossom.sha256;
|
|
94
136
|
apkSize = blossom.size;
|
|
95
|
-
filename = path_1.default.basename(apkPath);
|
|
96
137
|
console.log("📦 Uploaded to Blossom:", blossom.url);
|
|
97
138
|
console.log(" APK SHA-256:", blossom.sha256, "| size:", blossom.size);
|
|
98
139
|
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
const result = await (0, zapStore_1.publishToZapStore)({
|
|
102
|
-
app: {
|
|
103
|
-
appId: config.app.appId,
|
|
104
|
-
appName: config.app.appName,
|
|
105
|
-
versionName: config.app.versionName,
|
|
106
|
-
versionCode: config.app.versionCode,
|
|
107
|
-
description: config.app.description,
|
|
108
|
-
},
|
|
140
|
+
const releasePublish = await (0, zapStore_1.buildReleaseEvents)({
|
|
141
|
+
app,
|
|
109
142
|
apk: {
|
|
110
143
|
apkSha256,
|
|
111
144
|
apkSize,
|
|
@@ -114,9 +147,29 @@ async function publishCommand(configPath, options = {}) {
|
|
|
114
147
|
filename,
|
|
115
148
|
},
|
|
116
149
|
publishNsec,
|
|
117
|
-
target
|
|
150
|
+
target,
|
|
118
151
|
identityProof,
|
|
119
|
-
}
|
|
152
|
+
});
|
|
153
|
+
await transport.publish(releasePublish.asset, config.publish.relay);
|
|
154
|
+
await transport.publish(releasePublish.release, config.publish.relay);
|
|
155
|
+
if (releasePublish.identityProof) {
|
|
156
|
+
await transport.publish(releasePublish.identityProof, config.publish.relay);
|
|
157
|
+
}
|
|
158
|
+
const result = {
|
|
159
|
+
npub,
|
|
160
|
+
pubkeyHex,
|
|
161
|
+
appMetadataEventId: appMetadata.id,
|
|
162
|
+
releaseEventId: releasePublish.release.id,
|
|
163
|
+
assetEventId: releasePublish.asset.id,
|
|
164
|
+
identityProofEventId: releasePublish.identityProof?.id,
|
|
165
|
+
events: {
|
|
166
|
+
appMetadata,
|
|
167
|
+
release: releasePublish.release,
|
|
168
|
+
asset: releasePublish.asset,
|
|
169
|
+
identityProof: releasePublish.identityProof,
|
|
170
|
+
},
|
|
171
|
+
relayUrl: config.publish.relay,
|
|
172
|
+
};
|
|
120
173
|
console.log(options.dryRun ? "✅ PUBLISHED (dry-run — no network)" : "✅ PUBLISHED");
|
|
121
174
|
console.log("🪪 Publisher npub:", result.npub);
|
|
122
175
|
console.log("🧾 Asset event:", result.assetEventId);
|
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,10 +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;
|
|
7
|
+
exports.buildAppMetadataEvent = buildAppMetadataEvent;
|
|
8
|
+
exports.buildReleaseEvents = buildReleaseEvents;
|
|
5
9
|
exports.buildSignedPublishEvents = buildSignedPublishEvents;
|
|
6
10
|
exports.publishToZapStore = publishToZapStore;
|
|
7
11
|
exports.resolvePublishSecret = resolvePublishSecret;
|
|
8
12
|
exports.publishEventToRelay = publishEventToRelay;
|
|
13
|
+
exports.queryRelayForFilter = queryRelayForFilter;
|
|
9
14
|
exports.verifyPublish = verifyPublish;
|
|
10
15
|
exports.queryRelayForEvent = queryRelayForEvent;
|
|
11
16
|
const nostr_1 = require("./nostr");
|
|
@@ -36,25 +41,61 @@ function formatIdentityProofMessage(createdAt, expiry, pubkeyHex) {
|
|
|
36
41
|
const PLATFORM = "android-arm64-v8a";
|
|
37
42
|
const MIN_SDK = 28;
|
|
38
43
|
const TARGET_SDK = 36;
|
|
39
|
-
/**
|
|
40
|
-
|
|
41
|
-
|
|
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
|
+
}
|
|
74
|
+
/** Build the kind-32267 application event independently of the APK upload. */
|
|
75
|
+
async function buildAppMetadataEvent(input) {
|
|
42
76
|
const pubkeyHex = await (0, nostr_1.getNpubHex)(input.publishNsec.bytes);
|
|
43
77
|
const npub = (0, nostr_1.pubkeyHexToNpub)(pubkeyHex);
|
|
44
|
-
const now = Math.floor(Date.now() / 1000);
|
|
45
78
|
const communities = input.communities?.length ? input.communities : [exports.DEFAULT_COMMUNITY];
|
|
46
|
-
|
|
79
|
+
const repositoryTags = input.app.repository ? [["repository", input.app.repository]] : [];
|
|
47
80
|
const appMetadata = await (0, nostr_1.signNostrEvent)({
|
|
48
81
|
kind: exports.KIND_APP_METADATA,
|
|
49
|
-
created_at: now,
|
|
82
|
+
created_at: Math.floor(Date.now() / 1000),
|
|
50
83
|
tags: [
|
|
51
84
|
["d", input.app.appId],
|
|
52
85
|
["name", input.app.appName],
|
|
53
86
|
["f", PLATFORM],
|
|
54
87
|
...communities.map(c => ["h", c]),
|
|
88
|
+
...repositoryTags,
|
|
55
89
|
],
|
|
56
90
|
content: input.app.description ?? "",
|
|
57
91
|
}, input.publishNsec.bytes);
|
|
92
|
+
return { npub, pubkeyHex, appMetadata };
|
|
93
|
+
}
|
|
94
|
+
/** Build the APK-dependent NIP-82 events after the Blossom upload. */
|
|
95
|
+
async function buildReleaseEvents(input) {
|
|
96
|
+
const pubkeyHex = await (0, nostr_1.getNpubHex)(input.publishNsec.bytes);
|
|
97
|
+
const npub = (0, nostr_1.pubkeyHexToNpub)(pubkeyHex);
|
|
98
|
+
const now = Math.floor(Date.now() / 1000);
|
|
58
99
|
// kind 3063 — Software Asset (the APK)
|
|
59
100
|
const asset = await (0, nostr_1.signNostrEvent)({
|
|
60
101
|
kind: exports.KIND_ASSET,
|
|
@@ -112,7 +153,21 @@ async function buildSignedPublishEvents(input) {
|
|
|
112
153
|
content: formatIdentityProofMessage(input.identityProof.createdAt, input.identityProof.expiry, input.identityProof.pubkeyHex),
|
|
113
154
|
}, input.publishNsec.bytes);
|
|
114
155
|
}
|
|
115
|
-
return { npub, pubkeyHex,
|
|
156
|
+
return { npub, pubkeyHex, release, asset, identityProof: identityProofEvent ?? undefined };
|
|
157
|
+
}
|
|
158
|
+
/** Build the NIP-82 events (unsigned → signed) for an app release.
|
|
159
|
+
* Includes a NIP-C1 identity proof (kind 30509) if `identityProof` is set. */
|
|
160
|
+
async function buildSignedPublishEvents(input) {
|
|
161
|
+
const app = await buildAppMetadataEvent(input);
|
|
162
|
+
const release = await buildReleaseEvents(input);
|
|
163
|
+
return {
|
|
164
|
+
npub: app.npub,
|
|
165
|
+
pubkeyHex: app.pubkeyHex,
|
|
166
|
+
appMetadata: app.appMetadata,
|
|
167
|
+
release: release.release,
|
|
168
|
+
asset: release.asset,
|
|
169
|
+
identityProof: release.identityProof,
|
|
170
|
+
};
|
|
116
171
|
}
|
|
117
172
|
/** Publish all three events to the relay via the given transport. */
|
|
118
173
|
async function publishToZapStore(input, transport) {
|
|
@@ -149,6 +204,9 @@ class WebsocketRelayTransport {
|
|
|
149
204
|
constructor(webSocketCtor) {
|
|
150
205
|
this.WebSocketCtor = (webSocketCtor ?? WebSocket);
|
|
151
206
|
}
|
|
207
|
+
async query(filter, relayUrl) {
|
|
208
|
+
return await queryRelayForFilter(this.WebSocketCtor, relayUrl, filter);
|
|
209
|
+
}
|
|
152
210
|
async publish(event, relayUrl) {
|
|
153
211
|
await publishEventToRelay(this.WebSocketCtor, event, relayUrl);
|
|
154
212
|
}
|
|
@@ -157,6 +215,9 @@ exports.WebsocketRelayTransport = WebsocketRelayTransport;
|
|
|
157
215
|
/** A no-op transport for tests / dry-runs (no network). */
|
|
158
216
|
class MockRelayTransport {
|
|
159
217
|
published = [];
|
|
218
|
+
async query(_filter, _relayUrl) {
|
|
219
|
+
return undefined;
|
|
220
|
+
}
|
|
160
221
|
async publish(event, _relayUrl) {
|
|
161
222
|
this.published.push(event);
|
|
162
223
|
}
|
|
@@ -210,6 +271,59 @@ function publishEventToRelay(WebSocketCtor, event, relayUrl, timeoutMs = 30000)
|
|
|
210
271
|
};
|
|
211
272
|
});
|
|
212
273
|
}
|
|
274
|
+
/** Query a relay for the first event matching a Nostr filter. */
|
|
275
|
+
function queryRelayForFilter(WebSocketCtor, relayUrl, filter, timeoutMs = 30000) {
|
|
276
|
+
return new Promise((resolve, reject) => {
|
|
277
|
+
let settled = false;
|
|
278
|
+
const subscriptionId = `pakstr-app-${Date.now().toString(36)}`;
|
|
279
|
+
const ws = new WebSocketCtor(relayUrl);
|
|
280
|
+
const finish = (event, error) => {
|
|
281
|
+
if (settled)
|
|
282
|
+
return;
|
|
283
|
+
settled = true;
|
|
284
|
+
clearTimeout(timer);
|
|
285
|
+
try {
|
|
286
|
+
ws.send(JSON.stringify(["CLOSE", subscriptionId]));
|
|
287
|
+
}
|
|
288
|
+
catch { /* ignore */ }
|
|
289
|
+
try {
|
|
290
|
+
ws.close();
|
|
291
|
+
}
|
|
292
|
+
catch { /* ignore */ }
|
|
293
|
+
if (error)
|
|
294
|
+
reject(error);
|
|
295
|
+
else
|
|
296
|
+
resolve(event);
|
|
297
|
+
};
|
|
298
|
+
const timer = setTimeout(() => {
|
|
299
|
+
finish(undefined, new Error(`Relay query timed out: ${relayUrl}`));
|
|
300
|
+
}, timeoutMs);
|
|
301
|
+
ws.onopen = () => {
|
|
302
|
+
ws.send(JSON.stringify(["REQ", subscriptionId, filter]));
|
|
303
|
+
};
|
|
304
|
+
ws.onmessage = (msg) => {
|
|
305
|
+
let data;
|
|
306
|
+
try {
|
|
307
|
+
data = JSON.parse(typeof msg.data === "string" ? msg.data : msg.data.toString());
|
|
308
|
+
}
|
|
309
|
+
catch {
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
if (!Array.isArray(data) || data[1] !== subscriptionId)
|
|
313
|
+
return;
|
|
314
|
+
if (data[0] === "EVENT" && data[2])
|
|
315
|
+
finish(data[2]);
|
|
316
|
+
else if (data[0] === "EOSE")
|
|
317
|
+
finish(undefined);
|
|
318
|
+
else if (data[0] === "CLOSED") {
|
|
319
|
+
finish(undefined, new Error(`Relay closed query: ${relayUrl} ${data[2] ?? "unknown"}`));
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
ws.onerror = (error) => {
|
|
323
|
+
finish(undefined, new Error(`Relay connection error: ${relayUrl} ${error?.message ?? ""}`));
|
|
324
|
+
};
|
|
325
|
+
});
|
|
326
|
+
}
|
|
213
327
|
/**
|
|
214
328
|
* Verify a publish by querying the relay for the asset event and confirming
|
|
215
329
|
* the APK is retrievable from Blossom. Returns true if both check out.
|
|
@@ -0,0 +1,80 @@
|
|
|
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.ZapstoreConfigError = exports.ZAPSTORE_CONFIG_FILENAME = void 0;
|
|
7
|
+
exports.loadZapstoreRepository = loadZapstoreRepository;
|
|
8
|
+
exports.loadZapstorePublisherConfig = loadZapstorePublisherConfig;
|
|
9
|
+
const fs_1 = __importDefault(require("fs"));
|
|
10
|
+
const path_1 = __importDefault(require("path"));
|
|
11
|
+
const js_yaml_1 = __importDefault(require("js-yaml"));
|
|
12
|
+
const nostr_1 = require("./nostr");
|
|
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;
|
|
23
|
+
/** Read the repository URL from the zapstore.yaml beside pakstr.yaml. */
|
|
24
|
+
function loadZapstoreRepository(configDir) {
|
|
25
|
+
const configPath = path_1.default.join(configDir, exports.ZAPSTORE_CONFIG_FILENAME);
|
|
26
|
+
if (!fs_1.default.existsSync(configPath))
|
|
27
|
+
return undefined;
|
|
28
|
+
const parsed = js_yaml_1.default.load(fs_1.default.readFileSync(configPath, "utf8"));
|
|
29
|
+
const repository = parsed?.repository;
|
|
30
|
+
if (typeof repository !== "string" || repository.trim().length === 0)
|
|
31
|
+
return undefined;
|
|
32
|
+
return 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 repository = requireString(config.repository, "repository", configPath);
|
|
52
|
+
validateRepositoryUrl(repository, configPath);
|
|
53
|
+
const pubkey = requireString(config.pubkey, "pubkey", configPath);
|
|
54
|
+
let pubkeyHex;
|
|
55
|
+
try {
|
|
56
|
+
pubkeyHex = (0, nostr_1.npubToPubkeyHex)(pubkey);
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
throw new ZapstoreConfigError(`Invalid publisher npub in ${exports.ZAPSTORE_CONFIG_FILENAME}: ${error instanceof Error ? error.message : String(error)}`, configPath);
|
|
60
|
+
}
|
|
61
|
+
return { repository, pubkey, pubkeyHex };
|
|
62
|
+
}
|
|
63
|
+
function requireString(value, field, configPath) {
|
|
64
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
65
|
+
throw new ZapstoreConfigError(`${exports.ZAPSTORE_CONFIG_FILENAME} is missing required field: ${field}`, configPath);
|
|
66
|
+
}
|
|
67
|
+
return value.trim();
|
|
68
|
+
}
|
|
69
|
+
function validateRepositoryUrl(repository, configPath) {
|
|
70
|
+
let parsed;
|
|
71
|
+
try {
|
|
72
|
+
parsed = new URL(repository);
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
throw new ZapstoreConfigError(`${exports.ZAPSTORE_CONFIG_FILENAME} repository must be a valid absolute URL`, configPath);
|
|
76
|
+
}
|
|
77
|
+
if (!["https:", "http:", "ssh:"].includes(parsed.protocol) || !parsed.hostname) {
|
|
78
|
+
throw new ZapstoreConfigError(`${exports.ZAPSTORE_CONFIG_FILENAME} repository must use an http, https, or ssh URL with a host`, configPath);
|
|
79
|
+
}
|
|
80
|
+
}
|