@kehto/services 0.5.0 → 0.7.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/index.js CHANGED
@@ -1854,6 +1854,702 @@ function createResourceService(options) {
1854
1854
  return handler;
1855
1855
  }
1856
1856
 
1857
+ // src/outbox-service.ts
1858
+ var OUTBOX_SERVICE_VERSION = "1.0.0";
1859
+ var OUTBOX_DESCRIPTOR = {
1860
+ name: "outbox",
1861
+ version: OUTBOX_SERVICE_VERSION,
1862
+ description: "NAP-OUTBOX outbox-aware relay routing \u2014 query/subscribe/publish/resolveRelays"
1863
+ };
1864
+ function normalizeFilters(raw) {
1865
+ if (Array.isArray(raw)) {
1866
+ const filters = raw.filter((f) => typeof f === "object" && f !== null);
1867
+ return filters.length > 0 ? filters : null;
1868
+ }
1869
+ if (typeof raw === "object" && raw !== null) return [raw];
1870
+ return null;
1871
+ }
1872
+ function createOutboxService(options) {
1873
+ if (!options || typeof options.router !== "object" || options.router === null) {
1874
+ throw new Error("createOutboxService: options.router is required");
1875
+ }
1876
+ const { router } = options;
1877
+ const subscriptions = /* @__PURE__ */ new Map();
1878
+ function handleQuery(msg, send) {
1879
+ const m = msg;
1880
+ const id = m.id ?? "";
1881
+ const filters = normalizeFilters(m.filters);
1882
+ if (!filters) {
1883
+ send({ type: "outbox.query.result", id, events: [], relays: {}, error: "invalid filter" });
1884
+ return;
1885
+ }
1886
+ void router.query(filters, m.options).then(
1887
+ (result) => send({
1888
+ type: "outbox.query.result",
1889
+ id,
1890
+ events: result.events,
1891
+ relays: result.relays,
1892
+ ...result.incomplete === void 0 ? {} : { incomplete: result.incomplete },
1893
+ ...result.error === void 0 ? {} : { error: result.error }
1894
+ })
1895
+ ).catch(
1896
+ (err) => send({ type: "outbox.query.result", id, events: [], relays: {}, error: toErrorMessage(err) })
1897
+ );
1898
+ }
1899
+ function handleSubscribe(windowId, msg, send) {
1900
+ const m = msg;
1901
+ const subId = m.subId;
1902
+ if (typeof subId !== "string" || subId.length === 0) return;
1903
+ const subKey = `${windowId}:${subId}`;
1904
+ subscriptions.get(subKey)?.close();
1905
+ subscriptions.delete(subKey);
1906
+ const filters = normalizeFilters(m.filters);
1907
+ if (!filters) {
1908
+ send({ type: "outbox.closed", subId, reason: "invalid filter" });
1909
+ return;
1910
+ }
1911
+ const sink = {
1912
+ event: (event, relay) => send({ type: "outbox.event", subId, event, ...relay === void 0 ? {} : { relay } }),
1913
+ eose: () => send({ type: "outbox.eose", subId }),
1914
+ closed: (reason) => {
1915
+ subscriptions.delete(subKey);
1916
+ send({ type: "outbox.closed", subId, ...reason === void 0 ? {} : { reason } });
1917
+ }
1918
+ };
1919
+ subscriptions.set(subKey, router.subscribe(filters, m.options, sink));
1920
+ }
1921
+ function handleClose(windowId, msg, send) {
1922
+ const m = msg;
1923
+ const subId = m.subId;
1924
+ if (typeof subId !== "string") return;
1925
+ const subKey = `${windowId}:${subId}`;
1926
+ subscriptions.get(subKey)?.close();
1927
+ subscriptions.delete(subKey);
1928
+ send({ type: "outbox.closed", subId });
1929
+ }
1930
+ function handlePublish(msg, send) {
1931
+ const m = msg;
1932
+ const id = m.id ?? "";
1933
+ if (!m.event || typeof m.event !== "object") {
1934
+ send({ type: "outbox.publish.result", id, ok: false, error: "invalid filter" });
1935
+ return;
1936
+ }
1937
+ void router.publish(m.event, m.options).then(
1938
+ (result) => send({
1939
+ type: "outbox.publish.result",
1940
+ id,
1941
+ ok: result.ok,
1942
+ ...result.event === void 0 ? {} : { event: result.event },
1943
+ ...result.eventId === void 0 ? {} : { eventId: result.eventId },
1944
+ ...result.relays === void 0 ? {} : { relays: result.relays },
1945
+ ...result.error === void 0 ? {} : { error: result.error }
1946
+ })
1947
+ ).catch(
1948
+ (err) => send({ type: "outbox.publish.result", id, ok: false, error: toErrorMessage(err) })
1949
+ );
1950
+ }
1951
+ function handleResolveRelays(msg, send) {
1952
+ const m = msg;
1953
+ const id = m.id ?? "";
1954
+ if (!m.target || typeof m.target !== "object") {
1955
+ send({ type: "outbox.resolveRelays.result", id, error: "invalid filter" });
1956
+ return;
1957
+ }
1958
+ void router.resolveRelays(m.target).then((plan) => send({ type: "outbox.resolveRelays.result", id, plan })).catch(
1959
+ (err) => send({ type: "outbox.resolveRelays.result", id, error: toErrorMessage(err) })
1960
+ );
1961
+ }
1962
+ return {
1963
+ descriptor: OUTBOX_DESCRIPTOR,
1964
+ handleMessage(windowId, message, send) {
1965
+ switch (message.type) {
1966
+ case "outbox.query":
1967
+ handleQuery(message, send);
1968
+ return;
1969
+ case "outbox.subscribe":
1970
+ handleSubscribe(windowId, message, send);
1971
+ return;
1972
+ case "outbox.close":
1973
+ handleClose(windowId, message, send);
1974
+ return;
1975
+ case "outbox.publish":
1976
+ handlePublish(message, send);
1977
+ return;
1978
+ case "outbox.resolveRelays":
1979
+ handleResolveRelays(message, send);
1980
+ return;
1981
+ default:
1982
+ return;
1983
+ }
1984
+ },
1985
+ onWindowDestroyed(windowId) {
1986
+ const prefix = `${windowId}:`;
1987
+ for (const [key, sub] of subscriptions) {
1988
+ if (key.startsWith(prefix)) {
1989
+ sub.close();
1990
+ subscriptions.delete(key);
1991
+ }
1992
+ }
1993
+ }
1994
+ };
1995
+ }
1996
+ function toErrorMessage(err) {
1997
+ if (err instanceof Error) return err.message;
1998
+ if (typeof err === "string") return err;
1999
+ return "outbox request failed";
2000
+ }
2001
+
2002
+ // src/relay-pool-outbox-router.ts
2003
+ var DEFAULT_QUERY_TIMEOUT_MS = 4e3;
2004
+ function defaultRelayAllowed(url) {
2005
+ return typeof url === "string" && (url.startsWith("wss://") || url.startsWith("ws://"));
2006
+ }
2007
+ function deriveAuthors(filters, optionAuthors) {
2008
+ const authors = /* @__PURE__ */ new Set();
2009
+ for (const filter of filters) {
2010
+ for (const author of filter.authors ?? []) authors.add(author);
2011
+ }
2012
+ for (const author of optionAuthors ?? []) authors.add(author);
2013
+ return [...authors];
2014
+ }
2015
+ function wantsWriteRelays(direction, strategy) {
2016
+ if (strategy === "outbox") return true;
2017
+ if (strategy === "inbox") return false;
2018
+ return direction === "read";
2019
+ }
2020
+ function allowed(ctx, urls) {
2021
+ const out = /* @__PURE__ */ new Set();
2022
+ for (const url of urls) {
2023
+ if (ctx.isRelayAllowed(url)) out.add(url);
2024
+ }
2025
+ return [...out];
2026
+ }
2027
+ async function resolvePlan(ctx, pubkeys, direction, strategy, relayHints) {
2028
+ const useWrite = wantsWriteRelays(direction, strategy);
2029
+ const collected = /* @__PURE__ */ new Set();
2030
+ const missingAuthors = [];
2031
+ let sawNip65 = false;
2032
+ if (pubkeys.length > 0) {
2033
+ const lists = await ctx.loadRelayLists(pubkeys);
2034
+ for (const pubkey of pubkeys) {
2035
+ const entry = lists.get(pubkey);
2036
+ const relays2 = entry ? useWrite ? entry.write : entry.read : void 0;
2037
+ if (relays2 && relays2.length > 0) {
2038
+ sawNip65 = true;
2039
+ for (const url of relays2) collected.add(url);
2040
+ } else {
2041
+ missingAuthors.push(pubkey);
2042
+ }
2043
+ }
2044
+ }
2045
+ for (const url of relayHints ?? []) collected.add(url);
2046
+ let relays = allowed(ctx, collected);
2047
+ let source;
2048
+ if (relays.length === 0) {
2049
+ relays = allowed(ctx, ctx.fallbackRelays);
2050
+ source = "fallback";
2051
+ } else {
2052
+ source = sawNip65 ? "nip65" : "policy";
2053
+ }
2054
+ const plan = { relays, source };
2055
+ if (missingAuthors.length > 0) plan.missingAuthors = missingAuthors;
2056
+ return plan;
2057
+ }
2058
+ function recordRelay(collector, id, relayUrl) {
2059
+ let set = collector.relayMap.get(id);
2060
+ if (!set) {
2061
+ set = /* @__PURE__ */ new Set();
2062
+ collector.relayMap.set(id, set);
2063
+ }
2064
+ set.add(relayUrl);
2065
+ }
2066
+ function admitEvent(ctx, collector, event) {
2067
+ if (collector.seen.has(event.id)) return;
2068
+ collector.verifications.push(
2069
+ ctx.verify(event).then((ok) => {
2070
+ if (ok && !collector.seen.has(event.id)) collector.seen.set(event.id, event);
2071
+ else if (!ok) collector.relayMap.delete(event.id);
2072
+ })
2073
+ );
2074
+ }
2075
+ function buildCollectResult(collector, timedOut) {
2076
+ const events = [...collector.seen.values()];
2077
+ const relayObj = {};
2078
+ for (const event of events) relayObj[event.id] = [...collector.relayMap.get(event.id) ?? []];
2079
+ return { events, relayMap: relayObj, incomplete: timedOut };
2080
+ }
2081
+ function collectFromRelays(ctx, filters, relayUrls, timeoutMs) {
2082
+ return new Promise((resolve) => {
2083
+ const collector = { seen: /* @__PURE__ */ new Map(), relayMap: /* @__PURE__ */ new Map(), verifications: [] };
2084
+ const handles = [];
2085
+ let eoseCount = 0;
2086
+ let finished = false;
2087
+ let timedOut = false;
2088
+ function finalize() {
2089
+ if (finished) return;
2090
+ finished = true;
2091
+ clearTimeout(timer);
2092
+ for (const handle of handles) {
2093
+ try {
2094
+ handle.unsubscribe();
2095
+ } catch {
2096
+ }
2097
+ }
2098
+ void Promise.all(collector.verifications).then(() => resolve(buildCollectResult(collector, timedOut)));
2099
+ }
2100
+ const timer = setTimeout(() => {
2101
+ timedOut = true;
2102
+ finalize();
2103
+ }, timeoutMs);
2104
+ for (const relayUrl of relayUrls) {
2105
+ handles.push(relayPoolSubscribe(ctx, filters, relayUrl, (item) => {
2106
+ if (finished) return;
2107
+ if (item === "EOSE") {
2108
+ eoseCount += 1;
2109
+ if (eoseCount >= relayUrls.length) finalize();
2110
+ return;
2111
+ }
2112
+ recordRelay(collector, item.id, relayUrl);
2113
+ admitEvent(ctx, collector, item);
2114
+ }));
2115
+ }
2116
+ if (relayUrls.length === 0) finalize();
2117
+ });
2118
+ }
2119
+ function relayPoolSubscribe(ctx, filters, relayUrl, cb) {
2120
+ return ctx.relayPool.subscribe(filters, [relayUrl], cb);
2121
+ }
2122
+ async function queryImpl(ctx, filters, options) {
2123
+ if (!ctx.relayPool.isAvailable()) {
2124
+ return { events: [], relays: {}, incomplete: true, error: "relay list unavailable" };
2125
+ }
2126
+ const strategy = options?.strategy ?? "auto";
2127
+ const authors = deriveAuthors(filters, options?.authors);
2128
+ const plan = await resolvePlan(ctx, authors, "read", strategy, options?.relays);
2129
+ if (plan.relays.length === 0) {
2130
+ return { events: [], relays: {}, incomplete: true, error: "relay list unavailable" };
2131
+ }
2132
+ const timeoutMs = options?.timeoutMs ?? ctx.defaultTimeoutMs;
2133
+ const collected = await collectFromRelays(ctx, filters, plan.relays, timeoutMs);
2134
+ const incomplete = collected.incomplete || (plan.missingAuthors?.length ?? 0) > 0;
2135
+ let events = collected.events;
2136
+ if (options?.limit !== void 0 && events.length > options.limit) {
2137
+ events = [...events].sort((a, b) => b.created_at - a.created_at).slice(0, options.limit);
2138
+ }
2139
+ const result = { events, relays: collected.relayMap };
2140
+ if (incomplete) result.incomplete = true;
2141
+ return result;
2142
+ }
2143
+ function closeLiveSub(sub) {
2144
+ for (const handle of sub.handles) {
2145
+ try {
2146
+ handle.unsubscribe();
2147
+ } catch {
2148
+ }
2149
+ }
2150
+ sub.handles.length = 0;
2151
+ }
2152
+ function attachLiveRelay(ctx, sub, filters, relayUrl, live, sink) {
2153
+ sub.handles.push(relayPoolSubscribe(ctx, filters, relayUrl, (item) => {
2154
+ if (sub.closed) return;
2155
+ if (item === "EOSE") {
2156
+ sub.eoseCount += 1;
2157
+ if (!sub.eoseSent && sub.eoseCount >= sub.relayCount) {
2158
+ sub.eoseSent = true;
2159
+ sink.eose();
2160
+ if (!live) {
2161
+ sub.closed = true;
2162
+ closeLiveSub(sub);
2163
+ sink.closed();
2164
+ }
2165
+ }
2166
+ return;
2167
+ }
2168
+ if (sub.seen.has(item.id)) return;
2169
+ void ctx.verify(item).then((ok) => {
2170
+ if (!ok || sub.closed || sub.seen.has(item.id)) return;
2171
+ sub.seen.add(item.id);
2172
+ sink.event(item, relayUrl);
2173
+ });
2174
+ }));
2175
+ }
2176
+ function startSubscription(ctx, filters, options, sink) {
2177
+ const live = options?.live ?? true;
2178
+ const strategy = options?.strategy ?? "auto";
2179
+ const authors = deriveAuthors(filters, options?.authors);
2180
+ const sub = { handles: [], seen: /* @__PURE__ */ new Set(), closed: false, eoseCount: 0, relayCount: 0, eoseSent: false };
2181
+ void resolvePlan(ctx, authors, "read", strategy, options?.relays).then((plan) => {
2182
+ if (sub.closed) return;
2183
+ if (plan.relays.length === 0) {
2184
+ sink.closed("relay list unavailable");
2185
+ return;
2186
+ }
2187
+ sub.relayCount = plan.relays.length;
2188
+ for (const relayUrl of plan.relays) attachLiveRelay(ctx, sub, filters, relayUrl, live, sink);
2189
+ }).catch((err) => {
2190
+ if (!sub.closed) sink.closed(err instanceof Error ? err.message : "subscribe failed");
2191
+ });
2192
+ return {
2193
+ close() {
2194
+ if (sub.closed) return;
2195
+ sub.closed = true;
2196
+ closeLiveSub(sub);
2197
+ }
2198
+ };
2199
+ }
2200
+ async function resolvePublishTargets(ctx, signed, options) {
2201
+ const strategy = options?.strategy ?? "auto";
2202
+ const targets = /* @__PURE__ */ new Set();
2203
+ const authorPlan = await resolvePlan(ctx, [signed.pubkey], "read", strategy === "inbox" ? "auto" : "outbox");
2204
+ for (const url of authorPlan.relays) targets.add(url);
2205
+ if (options?.targetAuthors && options.targetAuthors.length > 0) {
2206
+ const inboxPlan = await resolvePlan(ctx, options.targetAuthors, "write", "inbox");
2207
+ for (const url of inboxPlan.relays) targets.add(url);
2208
+ }
2209
+ for (const url of options?.relays ?? []) targets.add(url);
2210
+ return allowed(ctx, targets);
2211
+ }
2212
+ async function publishImpl(ctx, template, options) {
2213
+ if (!ctx.signEvent) return { ok: false, error: "publish denied" };
2214
+ if (!ctx.relayPool.isAvailable()) return { ok: false, error: "relay list unavailable" };
2215
+ let signed;
2216
+ try {
2217
+ signed = await ctx.signEvent(template);
2218
+ } catch (err) {
2219
+ return { ok: false, error: err instanceof Error ? err.message : "sign failed" };
2220
+ }
2221
+ const relayUrls = await resolvePublishTargets(ctx, signed, options);
2222
+ if (relayUrls.length === 0) return { ok: false, event: signed, eventId: signed.id, error: "relay list unavailable" };
2223
+ let relays;
2224
+ try {
2225
+ relays = normalizePublishResult(await ctx.relayPool.publish(signed, relayUrls), relayUrls);
2226
+ } catch (err) {
2227
+ return { ok: false, event: signed, eventId: signed.id, error: err instanceof Error ? err.message : "publish failed" };
2228
+ }
2229
+ const ok = Object.values(relays).some(Boolean);
2230
+ const result = { ok, event: signed, eventId: signed.id, relays };
2231
+ if (!ok) result.error = "publish denied";
2232
+ return result;
2233
+ }
2234
+ function createRelayPoolOutboxRouter(options) {
2235
+ if (!options || typeof options.relayPool !== "object" || options.relayPool === null) {
2236
+ throw new Error("createRelayPoolOutboxRouter: options.relayPool is required");
2237
+ }
2238
+ if (typeof options.loadRelayLists !== "function") {
2239
+ throw new Error("createRelayPoolOutboxRouter: options.loadRelayLists is required");
2240
+ }
2241
+ if (!Array.isArray(options.fallbackRelays)) {
2242
+ throw new Error("createRelayPoolOutboxRouter: options.fallbackRelays is required");
2243
+ }
2244
+ const verifyEvent = options.verifyEvent;
2245
+ const ctx = {
2246
+ relayPool: options.relayPool,
2247
+ loadRelayLists: options.loadRelayLists,
2248
+ fallbackRelays: options.fallbackRelays,
2249
+ signEvent: options.signEvent,
2250
+ isRelayAllowed: options.isRelayAllowed ?? defaultRelayAllowed,
2251
+ defaultTimeoutMs: options.defaultTimeoutMs ?? DEFAULT_QUERY_TIMEOUT_MS,
2252
+ async verify(event) {
2253
+ if (!verifyEvent) return true;
2254
+ try {
2255
+ return await verifyEvent(event);
2256
+ } catch {
2257
+ return false;
2258
+ }
2259
+ }
2260
+ };
2261
+ return {
2262
+ query: (filters, queryOptions) => queryImpl(ctx, filters, queryOptions),
2263
+ subscribe: (filters, subscribeOptions, sink) => startSubscription(ctx, filters, subscribeOptions, sink),
2264
+ publish: (template, publishOptions) => publishImpl(ctx, template, publishOptions),
2265
+ resolveRelays: (target) => {
2266
+ const pubkeys = target.authors ?? (target.pubkey ? [target.pubkey] : []);
2267
+ return resolvePlan(ctx, pubkeys, target.direction ?? "read", target.strategy ?? "auto");
2268
+ }
2269
+ };
2270
+ }
2271
+ function normalizePublishResult(res, relayUrls) {
2272
+ const out = {};
2273
+ if (res && typeof res === "object") {
2274
+ for (const url of relayUrls) out[url] = res[url] ?? false;
2275
+ } else {
2276
+ for (const url of relayUrls) out[url] = true;
2277
+ }
2278
+ return out;
2279
+ }
2280
+
2281
+ // src/upload-service.ts
2282
+ var UPLOAD_SERVICE_VERSION = "1.0.0";
2283
+ var UPLOAD_DESCRIPTOR = {
2284
+ name: "upload",
2285
+ version: UPLOAD_SERVICE_VERSION,
2286
+ description: "NAP-UPLOAD shell-mediated file/blob upload \u2014 upload/status with progress pushes"
2287
+ };
2288
+ function createUploadService(options) {
2289
+ if (!options || typeof options.uploader !== "object" || options.uploader === null) {
2290
+ throw new Error("createUploadService: options.uploader is required");
2291
+ }
2292
+ const { uploader } = options;
2293
+ const generateId2 = options.generateId ?? (() => crypto.randomUUID());
2294
+ const now = options.now ?? (() => Date.now());
2295
+ const entries = /* @__PURE__ */ new Map();
2296
+ function handleUpload(windowId, msg, send) {
2297
+ const m = msg;
2298
+ const id = m.id ?? "";
2299
+ const request = m.request;
2300
+ if (!request || typeof request !== "object" || request.data == null) {
2301
+ send({ type: "upload.upload.result", id, error: "invalid request" });
2302
+ return;
2303
+ }
2304
+ const uploadId = generateId2();
2305
+ const key = `${windowId}:${uploadId}`;
2306
+ entries.set(key, { uploadId });
2307
+ const ctx = {
2308
+ uploadId,
2309
+ windowId,
2310
+ onStatus: (status) => {
2311
+ const stamped = { ...status, uploadId, updatedAt: status.updatedAt || now() };
2312
+ const entry = entries.get(key);
2313
+ if (entry) entry.status = stamped;
2314
+ send({ type: "upload.status.changed", status: stamped });
2315
+ }
2316
+ };
2317
+ void uploader.upload(request, ctx).then((result) => {
2318
+ const stamped = { ...result, uploadId };
2319
+ const entry = entries.get(key);
2320
+ if (entry) entry.status = { ...stamped, updatedAt: now() };
2321
+ send({ type: "upload.upload.result", id, result: stamped });
2322
+ }).catch((err) => {
2323
+ entries.delete(key);
2324
+ send({ type: "upload.upload.result", id, error: toErrorMessage2(err) });
2325
+ });
2326
+ }
2327
+ function handleStatus(windowId, msg, send) {
2328
+ const m = msg;
2329
+ const id = m.id ?? "";
2330
+ const uploadId = m.uploadId;
2331
+ if (typeof uploadId !== "string" || uploadId.length === 0) {
2332
+ send({ type: "upload.status.result", id, error: "invalid uploadId" });
2333
+ return;
2334
+ }
2335
+ const tracked = entries.get(`${windowId}:${uploadId}`)?.status;
2336
+ if (tracked) {
2337
+ send({ type: "upload.status.result", id, status: tracked });
2338
+ return;
2339
+ }
2340
+ if (uploader.status) {
2341
+ void uploader.status(uploadId).then(
2342
+ (status) => send(
2343
+ status ? { type: "upload.status.result", id, status } : { type: "upload.status.result", id, error: "unknown upload" }
2344
+ )
2345
+ ).catch(
2346
+ (err) => send({ type: "upload.status.result", id, error: toErrorMessage2(err) })
2347
+ );
2348
+ return;
2349
+ }
2350
+ send({ type: "upload.status.result", id, error: "unknown upload" });
2351
+ }
2352
+ return {
2353
+ descriptor: UPLOAD_DESCRIPTOR,
2354
+ handleMessage(windowId, message, send) {
2355
+ switch (message.type) {
2356
+ case "upload.upload":
2357
+ handleUpload(windowId, message, send);
2358
+ return;
2359
+ case "upload.status":
2360
+ handleStatus(windowId, message, send);
2361
+ return;
2362
+ default:
2363
+ return;
2364
+ }
2365
+ },
2366
+ onWindowDestroyed(windowId) {
2367
+ const prefix = `${windowId}:`;
2368
+ for (const [key, entry] of entries) {
2369
+ if (key.startsWith(prefix)) {
2370
+ uploader.cancel?.(entry.uploadId);
2371
+ entries.delete(key);
2372
+ }
2373
+ }
2374
+ }
2375
+ };
2376
+ }
2377
+ function toErrorMessage2(err) {
2378
+ if (err instanceof Error) return err.message;
2379
+ if (typeof err === "string") return err;
2380
+ return "upload request failed";
2381
+ }
2382
+
2383
+ // src/http-uploader.ts
2384
+ var KIND_NIP98 = 27235;
2385
+ var KIND_BLOSSOM_AUTH = 24242;
2386
+ var BLOSSOM_AUTH_TTL_S = 3600;
2387
+ function createHttpUploader(options) {
2388
+ if (!options || typeof options.signEvent !== "function") {
2389
+ throw new Error("createHttpUploader: options.signEvent is required");
2390
+ }
2391
+ const rails = options.rails ?? {};
2392
+ const signEvent = options.signEvent;
2393
+ const fetchFn = options.fetch ?? fetch;
2394
+ const digest = options.digestSha256 ?? defaultDigestSha256;
2395
+ const nowS = options.now ?? (() => Math.floor(Date.now() / 1e3));
2396
+ const defaultRail = options.defaultRail ?? firstConfiguredRail(rails);
2397
+ async function upload(request, ctx) {
2398
+ const rail = request.rail ?? defaultRail;
2399
+ const config = rail === "nip96" ? rails.nip96 : rail === "blossom" ? rails.blossom : void 0;
2400
+ if (rail !== "nip96" && rail !== "blossom") {
2401
+ return failed(ctx.uploadId, rail ?? "unknown", "unsupported rail");
2402
+ }
2403
+ const server = config?.servers?.[0];
2404
+ if (!server) {
2405
+ return failed(ctx.uploadId, rail, "no server configured");
2406
+ }
2407
+ const bytes = await toBytes(request.data);
2408
+ const sha256 = await digest(bytes);
2409
+ try {
2410
+ return rail === "nip96" ? await uploadNip96({ request, ctx, server, bytes, sha256, signEvent, fetchFn, nowS }) : await uploadBlossom({ request, ctx, server, bytes, sha256, signEvent, fetchFn, nowS });
2411
+ } catch (err) {
2412
+ return failed(ctx.uploadId, rail, toErrorMessage3(err), sha256);
2413
+ }
2414
+ }
2415
+ return { upload };
2416
+ }
2417
+ async function uploadNip96(args) {
2418
+ const { request, ctx, server, bytes, sha256, signEvent, fetchFn, nowS } = args;
2419
+ const auth = await signEvent({
2420
+ kind: KIND_NIP98,
2421
+ created_at: nowS(),
2422
+ content: "",
2423
+ tags: [
2424
+ ["u", server],
2425
+ ["method", "POST"],
2426
+ ["payload", sha256]
2427
+ ]
2428
+ });
2429
+ const form = new FormData();
2430
+ form.append("file", new Blob([bytesToArrayBuffer(bytes)], { type: request.mimeType }), request.filename ?? "file");
2431
+ if (request.caption !== void 0) form.append("caption", request.caption);
2432
+ if (request.mimeType !== void 0) form.append("content_type", request.mimeType);
2433
+ if (request.noTransform) form.append("no_transform", "true");
2434
+ const res = await fetchFn(server, {
2435
+ method: "POST",
2436
+ headers: { Authorization: nostrAuthHeader(auth) },
2437
+ body: form
2438
+ });
2439
+ if (!res.ok) {
2440
+ return failed(ctx.uploadId, "nip96", `server rejected (HTTP ${res.status})`, sha256);
2441
+ }
2442
+ const body = await res.json();
2443
+ if (body.status === "error") {
2444
+ return failed(ctx.uploadId, "nip96", body.message ?? "upload failed", sha256);
2445
+ }
2446
+ const tags = body.nip94_event?.tags ?? [];
2447
+ return fromNip94Tags(ctx.uploadId, "nip96", tags, bytes.byteLength, sha256);
2448
+ }
2449
+ function fromNip94Tags(uploadId, rail, tags, fallbackSize, fallbackSha) {
2450
+ const get = (name) => tags.find((t) => t[0] === name)?.[1];
2451
+ const url = get("url");
2452
+ const result = {
2453
+ ok: Boolean(url),
2454
+ uploadId,
2455
+ status: url ? "complete" : "failed",
2456
+ rail,
2457
+ sha256: get("x") ?? fallbackSha,
2458
+ nip94: tags
2459
+ };
2460
+ if (url) result.url = url;
2461
+ const ox = get("ox");
2462
+ if (ox) result.originalSha256 = ox;
2463
+ const size = get("size");
2464
+ result.size = size ? Number(size) : fallbackSize;
2465
+ const m = get("m");
2466
+ if (m) result.mimeType = m;
2467
+ const dim = parseDimensions(get("dim"));
2468
+ if (dim) result.dimensions = dim;
2469
+ const blurhash = get("blurhash");
2470
+ if (blurhash) result.blurhash = blurhash;
2471
+ if (!url) result.error = "server returned no url";
2472
+ return result;
2473
+ }
2474
+ async function uploadBlossom(args) {
2475
+ const { request, ctx, server, bytes, sha256, signEvent, fetchFn, nowS } = args;
2476
+ const auth = await signEvent({
2477
+ kind: KIND_BLOSSOM_AUTH,
2478
+ created_at: nowS(),
2479
+ content: `Upload ${request.filename ?? "file"}`,
2480
+ tags: [
2481
+ ["t", "upload"],
2482
+ ["x", sha256],
2483
+ ["expiration", String(nowS() + BLOSSOM_AUTH_TTL_S)]
2484
+ ]
2485
+ });
2486
+ const endpoint = `${trimTrailingSlash(server)}/upload`;
2487
+ const headers = { Authorization: nostrAuthHeader(auth) };
2488
+ if (request.mimeType) headers["Content-Type"] = request.mimeType;
2489
+ const res = await fetchFn(endpoint, {
2490
+ method: "PUT",
2491
+ headers,
2492
+ body: bytesToArrayBuffer(bytes)
2493
+ });
2494
+ if (!res.ok) {
2495
+ return failed(ctx.uploadId, "blossom", `server rejected (HTTP ${res.status})`, sha256);
2496
+ }
2497
+ const blob = await res.json();
2498
+ if (!blob.url) {
2499
+ return failed(ctx.uploadId, "blossom", "server returned no url", sha256);
2500
+ }
2501
+ const result = {
2502
+ ok: true,
2503
+ uploadId: ctx.uploadId,
2504
+ status: "complete",
2505
+ rail: "blossom",
2506
+ url: blob.url,
2507
+ sha256: blob.sha256 ?? sha256,
2508
+ size: blob.size ?? bytes.byteLength
2509
+ };
2510
+ if (blob.type) result.mimeType = blob.type;
2511
+ return result;
2512
+ }
2513
+ function failed(uploadId, rail, error, sha256) {
2514
+ return { ok: false, uploadId, status: "failed", rail, error, ...sha256 ? { sha256 } : {} };
2515
+ }
2516
+ function firstConfiguredRail(rails) {
2517
+ if (rails.nip96?.servers?.length) return "nip96";
2518
+ if (rails.blossom?.servers?.length) return "blossom";
2519
+ return void 0;
2520
+ }
2521
+ function nostrAuthHeader(event) {
2522
+ return `Nostr ${base64Utf8(JSON.stringify(event))}`;
2523
+ }
2524
+ function base64Utf8(s) {
2525
+ return btoa(String.fromCharCode(...new TextEncoder().encode(s)));
2526
+ }
2527
+ function parseDimensions(dim) {
2528
+ if (!dim) return void 0;
2529
+ const m = /^(\d+)x(\d+)$/.exec(dim);
2530
+ if (!m) return void 0;
2531
+ return { width: Number(m[1]), height: Number(m[2]) };
2532
+ }
2533
+ async function toBytes(data) {
2534
+ if (data instanceof ArrayBuffer) return new Uint8Array(data);
2535
+ return new Uint8Array(await data.arrayBuffer());
2536
+ }
2537
+ function bytesToArrayBuffer(bytes) {
2538
+ return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
2539
+ }
2540
+ function trimTrailingSlash(url) {
2541
+ return url.endsWith("/") ? url.slice(0, -1) : url;
2542
+ }
2543
+ async function defaultDigestSha256(bytes) {
2544
+ const buf = await crypto.subtle.digest("SHA-256", bytesToArrayBuffer(bytes));
2545
+ return Array.from(new Uint8Array(buf)).map((b) => b.toString(16).padStart(2, "0")).join("");
2546
+ }
2547
+ function toErrorMessage3(err) {
2548
+ if (err instanceof Error) return err.message;
2549
+ if (typeof err === "string") return err;
2550
+ return "upload failed";
2551
+ }
2552
+
1857
2553
  // src/cvm-service.ts
1858
2554
  var CVM_SERVICE_VERSION = "1.0.0";
1859
2555
  var CVM_DESCRIPTOR = {
@@ -1896,7 +2592,7 @@ function createCvmService(options) {
1896
2592
  const m = msg;
1897
2593
  const id = m.id ?? "";
1898
2594
  void transport.discover(m.query).then((servers) => send({ type: "cvm.discover.result", id, servers })).catch(
1899
- (err) => send({ type: "cvm.discover.result", id, servers: [], error: toErrorMessage(err) })
2595
+ (err) => send({ type: "cvm.discover.result", id, servers: [], error: toErrorMessage4(err) })
1900
2596
  );
1901
2597
  }
1902
2598
  function handleRequest(windowId, msg, send) {
@@ -1912,7 +2608,7 @@ function createCvmService(options) {
1912
2608
  }
1913
2609
  openSession(windowId, m.server, send);
1914
2610
  void transport.request(m.server, m.message, m.options).then((message) => send({ type: "cvm.request.result", id, message })).catch(
1915
- (err) => send({ type: "cvm.request.result", id, error: toErrorMessage(err) })
2611
+ (err) => send({ type: "cvm.request.result", id, error: toErrorMessage4(err) })
1916
2612
  );
1917
2613
  }
1918
2614
  function handleClose(windowId, msg, send) {
@@ -1923,7 +2619,7 @@ function createCvmService(options) {
1923
2619
  return;
1924
2620
  }
1925
2621
  closeSession(windowId, m.server.pubkey);
1926
- void transport.close(m.server).then(() => send({ type: "cvm.close.result", id })).catch((err) => send({ type: "cvm.close.result", id, error: toErrorMessage(err) }));
2622
+ void transport.close(m.server).then(() => send({ type: "cvm.close.result", id })).catch((err) => send({ type: "cvm.close.result", id, error: toErrorMessage4(err) }));
1927
2623
  }
1928
2624
  return {
1929
2625
  descriptor: CVM_DESCRIPTOR,
@@ -1954,7 +2650,7 @@ function createCvmService(options) {
1954
2650
  }
1955
2651
  };
1956
2652
  }
1957
- function toErrorMessage(err) {
2653
+ function toErrorMessage4(err) {
1958
2654
  if (err instanceof Error) return err.message;
1959
2655
  if (typeof err === "string") return err;
1960
2656
  return "cvm request failed";
@@ -1966,13 +2662,17 @@ export {
1966
2662
  createConfigService,
1967
2663
  createCoordinatedRelay,
1968
2664
  createCvmService,
2665
+ createHttpUploader,
1969
2666
  createIdentityService,
1970
2667
  createKeysService,
1971
2668
  createMediaService,
1972
2669
  createNotificationService,
1973
2670
  createNotifyService,
2671
+ createOutboxService,
2672
+ createRelayPoolOutboxRouter,
1974
2673
  createRelayPoolService,
1975
2674
  createResourceService,
1976
- createThemeService
2675
+ createThemeService,
2676
+ createUploadService
1977
2677
  };
1978
2678
  //# sourceMappingURL=index.js.map