@kehto/services 0.6.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.d.ts +233 -1
- package/dist/index.js +279 -5
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -1796,4 +1796,236 @@ interface RelayPoolOutboxRouterOptions {
|
|
|
1796
1796
|
*/
|
|
1797
1797
|
declare function createRelayPoolOutboxRouter(options: RelayPoolOutboxRouterOptions): OutboxRouter;
|
|
1798
1798
|
|
|
1799
|
-
|
|
1799
|
+
/**
|
|
1800
|
+
* upload-service.ts — NAP-UPLOAD (shell-mediated file/blob upload) reference service.
|
|
1801
|
+
*
|
|
1802
|
+
* Shell-side handler for the NAP-UPLOAD wire protocol. It is a pure envelope
|
|
1803
|
+
* router: it validates `upload.*` envelopes, delegates the actual byte transfer
|
|
1804
|
+
* (server selection, rail authorization signing, the HTTP upload) to an injected
|
|
1805
|
+
* {@link Uploader}, and posts the correlated result / status messages back to the
|
|
1806
|
+
* napplet.
|
|
1807
|
+
*
|
|
1808
|
+
* The uploader is injected (options-as-bridge) so this service has no transport
|
|
1809
|
+
* or Nostr dependency and is fully unit-testable. NAP-UPLOAD is deliberately
|
|
1810
|
+
* abstract over the backend — the runtime decides *how* it uploads (NIP-96,
|
|
1811
|
+
* Blossom, …). A concrete HTTP-backed uploader ships alongside as
|
|
1812
|
+
* {@link createHttpUploader}.
|
|
1813
|
+
*
|
|
1814
|
+
* ──────────────────────────── Responsibilities ────────────────────────────
|
|
1815
|
+
* Inbound: upload.upload, upload.status
|
|
1816
|
+
* Outbound: upload.upload.result, upload.status.result, upload.status.changed
|
|
1817
|
+
*
|
|
1818
|
+
* The service owns the `uploadId` (generated per request, scoped to the
|
|
1819
|
+
* requesting napplet), tracks the latest {@link UploadStatus} per upload for
|
|
1820
|
+
* `upload.status` queries, and cleans up on window teardown. The shell owns
|
|
1821
|
+
* consent, policy, server selection, signing, and the HTTP upload — all behind
|
|
1822
|
+
* the {@link Uploader}.
|
|
1823
|
+
*
|
|
1824
|
+
* @example
|
|
1825
|
+
* ```ts
|
|
1826
|
+
* import { createUploadService, createHttpUploader } from '@kehto/services';
|
|
1827
|
+
*
|
|
1828
|
+
* const uploader = createHttpUploader({ rails: { nip96: { servers } }, signEvent });
|
|
1829
|
+
* runtime.registerService('upload', createUploadService({ uploader }));
|
|
1830
|
+
* ```
|
|
1831
|
+
*
|
|
1832
|
+
* @packageDocumentation
|
|
1833
|
+
*/
|
|
1834
|
+
|
|
1835
|
+
/**
|
|
1836
|
+
* Storage rail. `nip96` (NIP-96 HTTP file storage) and `blossom` (Blossom blob
|
|
1837
|
+
* storage) are the first concrete backends; the open string keeps the API
|
|
1838
|
+
* stable as shells add rails (torrents, usenet, …).
|
|
1839
|
+
*/
|
|
1840
|
+
type UploadRail = 'nip96' | 'blossom' | (string & {});
|
|
1841
|
+
/** Lifecycle state of an upload. */
|
|
1842
|
+
type UploadState = 'pending' | 'uploading' | 'complete' | 'failed' | 'cancelled';
|
|
1843
|
+
/** Pixel dimensions of an uploaded image/video. */
|
|
1844
|
+
interface UploadDimensions {
|
|
1845
|
+
width: number;
|
|
1846
|
+
height: number;
|
|
1847
|
+
}
|
|
1848
|
+
/**
|
|
1849
|
+
* A napplet's upload request. `data` crosses the postMessage boundary by
|
|
1850
|
+
* structured clone — shells never require base64 encoding.
|
|
1851
|
+
*/
|
|
1852
|
+
interface UploadRequest {
|
|
1853
|
+
/** Storage rail; omit to let the shell pick a configured default. */
|
|
1854
|
+
rail?: UploadRail;
|
|
1855
|
+
/** The bytes to upload. */
|
|
1856
|
+
data: ArrayBuffer | Blob;
|
|
1857
|
+
/** MIME type; inferred from `data` when omitted. */
|
|
1858
|
+
mimeType?: string;
|
|
1859
|
+
/** Suggested filename. */
|
|
1860
|
+
filename?: string;
|
|
1861
|
+
/** Alt text / description for the file event. */
|
|
1862
|
+
caption?: string;
|
|
1863
|
+
/** Request the server not re-encode the file (NIP-96 `no_transform`). */
|
|
1864
|
+
noTransform?: boolean;
|
|
1865
|
+
/** Rail-specific or shell-specific extra metadata. */
|
|
1866
|
+
metadata?: Record<string, unknown>;
|
|
1867
|
+
}
|
|
1868
|
+
/** A single Nostr tag (NIP-94 / imeta entries are arrays of strings). */
|
|
1869
|
+
type NostrTag = string[];
|
|
1870
|
+
/** The result of an upload. */
|
|
1871
|
+
interface UploadResult {
|
|
1872
|
+
/** Whether the upload succeeded (or is progressing) vs failed/cancelled. */
|
|
1873
|
+
ok: boolean;
|
|
1874
|
+
/** Shell-generated id, scoped to the requesting napplet. */
|
|
1875
|
+
uploadId: string;
|
|
1876
|
+
/** Current lifecycle state. */
|
|
1877
|
+
status: UploadState;
|
|
1878
|
+
/** The rail the shell used. */
|
|
1879
|
+
rail: UploadRail;
|
|
1880
|
+
/** Primary download URL. */
|
|
1881
|
+
url?: string;
|
|
1882
|
+
/** Mirrors / alternative server URLs. */
|
|
1883
|
+
fallbackUrls?: string[];
|
|
1884
|
+
/** Hash of the stored blob (NIP-94 `x`). */
|
|
1885
|
+
sha256?: string;
|
|
1886
|
+
/** Hash before server transforms (NIP-94 `ox`). */
|
|
1887
|
+
originalSha256?: string;
|
|
1888
|
+
/** Size in bytes. */
|
|
1889
|
+
size?: number;
|
|
1890
|
+
/** Stored MIME type. */
|
|
1891
|
+
mimeType?: string;
|
|
1892
|
+
/** Image/video dimensions when known. */
|
|
1893
|
+
dimensions?: UploadDimensions;
|
|
1894
|
+
/** Blurhash placeholder when known. */
|
|
1895
|
+
blurhash?: string;
|
|
1896
|
+
/** Ready-to-attach NIP-94 / imeta tags. */
|
|
1897
|
+
nip94?: NostrTag[];
|
|
1898
|
+
/** Error reason when the upload failed or was cancelled. */
|
|
1899
|
+
error?: string;
|
|
1900
|
+
}
|
|
1901
|
+
/** A status snapshot for an upload, including progress counters. */
|
|
1902
|
+
interface UploadStatus extends UploadResult {
|
|
1903
|
+
/** Bytes sent so far (while uploading). */
|
|
1904
|
+
bytesSent?: number;
|
|
1905
|
+
/** Total bytes to send. */
|
|
1906
|
+
bytesTotal?: number;
|
|
1907
|
+
/** Unix ms timestamp of this status. */
|
|
1908
|
+
updatedAt: number;
|
|
1909
|
+
}
|
|
1910
|
+
/**
|
|
1911
|
+
* Context handed to an {@link Uploader} for a single upload. Carries the
|
|
1912
|
+
* service-owned `uploadId` and a sink for streaming progress / state changes.
|
|
1913
|
+
*/
|
|
1914
|
+
interface UploaderContext {
|
|
1915
|
+
/** The service-generated upload id (authoritative; scoped to the napplet). */
|
|
1916
|
+
uploadId: string;
|
|
1917
|
+
/** The napplet window that requested the upload. */
|
|
1918
|
+
windowId: string;
|
|
1919
|
+
/**
|
|
1920
|
+
* Push a status update (progress, or a transition to complete/failed). The
|
|
1921
|
+
* service stamps `uploadId` and `updatedAt` before forwarding to the napplet
|
|
1922
|
+
* as `upload.status.changed`, and records it as the latest tracked status.
|
|
1923
|
+
*/
|
|
1924
|
+
onStatus(status: UploadStatus): void;
|
|
1925
|
+
}
|
|
1926
|
+
/**
|
|
1927
|
+
* Abstract upload backend. Implementors own server selection, rail
|
|
1928
|
+
* authorization signing (NIP-98 for NIP-96, kind 24242 for Blossom), the HTTP
|
|
1929
|
+
* upload, and integrity-hash reporting. The service translates wire envelopes
|
|
1930
|
+
* into these calls and back. A concrete reference implementation ships as
|
|
1931
|
+
* {@link createHttpUploader}.
|
|
1932
|
+
*/
|
|
1933
|
+
interface Uploader {
|
|
1934
|
+
/** Upload `request.data`, streaming progress through `ctx.onStatus`. */
|
|
1935
|
+
upload(request: UploadRequest, ctx: UploaderContext): Promise<UploadResult>;
|
|
1936
|
+
/** Optional: resolve the latest status for an upload the service is not tracking. */
|
|
1937
|
+
status?(uploadId: string): Promise<UploadStatus | undefined>;
|
|
1938
|
+
/** Optional: abort an in-flight upload (called on window teardown). */
|
|
1939
|
+
cancel?(uploadId: string): void;
|
|
1940
|
+
}
|
|
1941
|
+
/** Options for {@link createUploadService}. */
|
|
1942
|
+
interface UploadServiceOptions {
|
|
1943
|
+
/** The upload backend the shell uses. Required. */
|
|
1944
|
+
uploader: Uploader;
|
|
1945
|
+
/** Generate an upload id; defaults to `crypto.randomUUID()`. */
|
|
1946
|
+
generateId?: () => string;
|
|
1947
|
+
/** Current time in unix ms; defaults to `Date.now()`. */
|
|
1948
|
+
now?: () => number;
|
|
1949
|
+
}
|
|
1950
|
+
/**
|
|
1951
|
+
* Create the NAP-UPLOAD service handler.
|
|
1952
|
+
*
|
|
1953
|
+
* @param options - Must provide an {@link Uploader}.
|
|
1954
|
+
* @returns A `ServiceHandler` ready for `runtime.registerService('upload', handler)`.
|
|
1955
|
+
* @throws If `options.uploader` is missing.
|
|
1956
|
+
*/
|
|
1957
|
+
declare function createUploadService(options: UploadServiceOptions): ServiceHandler;
|
|
1958
|
+
|
|
1959
|
+
/**
|
|
1960
|
+
* http-uploader.ts — NAP-UPLOAD concrete HTTP-backed {@link Uploader}.
|
|
1961
|
+
*
|
|
1962
|
+
* The reference upload backend for {@link createUploadService}. Implements two
|
|
1963
|
+
* storage rails over HTTP:
|
|
1964
|
+
*
|
|
1965
|
+
* - **NIP-96** — signs a NIP-98 (kind 27235) HTTP-auth event, POSTs the file as
|
|
1966
|
+
* `multipart/form-data`, and maps the returned NIP-94 event tags into an
|
|
1967
|
+
* {@link UploadResult}.
|
|
1968
|
+
* - **Blossom** — signs a kind 24242 authorization event, PUTs the raw bytes to
|
|
1969
|
+
* `<server>/upload`, and maps the returned blob descriptor.
|
|
1970
|
+
*
|
|
1971
|
+
* Signing (`signEvent`) and transport (`fetch`) are injected so the uploader
|
|
1972
|
+
* carries no Nostr or network dependency and is fully unit-testable. The shell
|
|
1973
|
+
* holds the signing key and never exposes it to napplets — the uploader only
|
|
1974
|
+
* receives a signing callback. Server URLs are shell configuration, not napplet
|
|
1975
|
+
* input: a napplet may *hint* a rail, but never a server.
|
|
1976
|
+
*
|
|
1977
|
+
* The configured server URL is used directly as the upload endpoint (the
|
|
1978
|
+
* NIP-96 `api_url` / Blossom base). Hosts that need `.well-known` discovery can
|
|
1979
|
+
* resolve it before constructing the uploader.
|
|
1980
|
+
*
|
|
1981
|
+
* @example
|
|
1982
|
+
* ```ts
|
|
1983
|
+
* const uploader = createHttpUploader({
|
|
1984
|
+
* rails: { nip96: { servers: ['https://nostr.build/api/v2/nip96/upload'] } },
|
|
1985
|
+
* signEvent: (tmpl) => signer.signEvent(tmpl),
|
|
1986
|
+
* });
|
|
1987
|
+
* runtime.registerService('upload', createUploadService({ uploader }));
|
|
1988
|
+
* ```
|
|
1989
|
+
*
|
|
1990
|
+
* @packageDocumentation
|
|
1991
|
+
*/
|
|
1992
|
+
|
|
1993
|
+
/** Per-rail server configuration. The first server is the primary endpoint. */
|
|
1994
|
+
interface RailServerConfig {
|
|
1995
|
+
/** Ordered server endpoint URLs; index 0 is primary. */
|
|
1996
|
+
servers: string[];
|
|
1997
|
+
}
|
|
1998
|
+
/** Storage rails this uploader can serve. */
|
|
1999
|
+
interface HttpUploaderRails {
|
|
2000
|
+
/** NIP-96 HTTP file storage. */
|
|
2001
|
+
nip96?: RailServerConfig;
|
|
2002
|
+
/** Blossom blob storage. */
|
|
2003
|
+
blossom?: RailServerConfig;
|
|
2004
|
+
}
|
|
2005
|
+
/** Signs an event template on the user's behalf (shell holds the key). */
|
|
2006
|
+
type SignEvent = (template: EventTemplate) => Promise<NostrEvent>;
|
|
2007
|
+
/** Options for {@link createHttpUploader}. */
|
|
2008
|
+
interface HttpUploaderOptions {
|
|
2009
|
+
/** Configured rails + their servers. */
|
|
2010
|
+
rails: HttpUploaderRails;
|
|
2011
|
+
/** Rail to use when a request omits one; defaults to the first configured rail. */
|
|
2012
|
+
defaultRail?: UploadRail;
|
|
2013
|
+
/** Signs NIP-98 / Blossom auth events. Required. */
|
|
2014
|
+
signEvent: SignEvent;
|
|
2015
|
+
/** Fetch implementation; defaults to the global `fetch`. */
|
|
2016
|
+
fetch?: typeof fetch;
|
|
2017
|
+
/** Hex SHA-256 of the payload bytes; defaults to Web Crypto. */
|
|
2018
|
+
digestSha256?: (bytes: Uint8Array) => Promise<string>;
|
|
2019
|
+
/** Unix *seconds* clock for event timestamps; defaults to `Date.now()/1000`. */
|
|
2020
|
+
now?: () => number;
|
|
2021
|
+
}
|
|
2022
|
+
/**
|
|
2023
|
+
* Create the reference HTTP {@link Uploader} (NIP-96 + Blossom rails).
|
|
2024
|
+
*
|
|
2025
|
+
* @param options - Rails, server config, and the injected `signEvent`.
|
|
2026
|
+
* @returns An {@link Uploader} for `createUploadService({ uploader })`.
|
|
2027
|
+
* @throws If `options.signEvent` is missing.
|
|
2028
|
+
*/
|
|
2029
|
+
declare function createHttpUploader(options: HttpUploaderOptions): Uploader;
|
|
2030
|
+
|
|
2031
|
+
export { type AudioServiceOptions, type AudioSource, type CacheServiceOptions, type ConfigSchemaValidation, type ConfigService, type ConfigServiceOptions, type CoordinatedRelayOptions, type GiftWrapDecryptResult, type HostCacheBridge, type HostDecryptBridge, type HostKeyEvent, type HostKeysBridge, type HostMediaBridge, type HttpUploaderOptions, type HttpUploaderRails, type IdentityDecryptErrorCode, type IdentityDecryptErrorMessage, type IdentityDecryptMessage, type IdentityDecryptResultMessage, type IdentityServiceOptions, type KeysServiceOptions, type MediaMetadataLike, type MediaPlaybackOwner, type MediaServiceOptions, type MediaSessionCreateOptions, type MediaSessionTarget, type MediaSourceRef, type NostrTag, type Notification, type NotificationServiceOptions, type NotifyServiceOptions, type OutboxPublishOptions, type OutboxPublishResult, type OutboxQueryOptions, type OutboxRelayPlan, type OutboxRelayPool, type OutboxResult, type OutboxRouter, type OutboxRouterSubscription, type OutboxServiceOptions, type OutboxStrategy, type OutboxSubscribeOptions, type OutboxSubscriptionSink, type OutboxTarget, type RailServerConfig, type RelayListEntry, type RelayPoolOutboxRouterOptions, type RelayPoolServiceOptions, type ResourceService, type ResourceServiceOptions, type Rumor, type SignEvent, type ThemeService, type ThemeServiceOptions, type UploadDimensions, type UploadRail, type UploadRequest, type UploadResult, type UploadServiceOptions, type UploadState, type UploadStatus, type Uploader, type UploaderContext, type VerifyEvent, createAudioService, createBrowserMediaBridge, createCacheService, createConfigService, createCoordinatedRelay, createHttpUploader, createIdentityService, createKeysService, createMediaService, createNotificationService, createNotifyService, createOutboxService, createRelayPoolOutboxRouter, createRelayPoolService, createResourceService, createThemeService, createUploadService };
|
package/dist/index.js
CHANGED
|
@@ -2278,6 +2278,278 @@ function normalizePublishResult(res, relayUrls) {
|
|
|
2278
2278
|
return out;
|
|
2279
2279
|
}
|
|
2280
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
|
+
|
|
2281
2553
|
// src/cvm-service.ts
|
|
2282
2554
|
var CVM_SERVICE_VERSION = "1.0.0";
|
|
2283
2555
|
var CVM_DESCRIPTOR = {
|
|
@@ -2320,7 +2592,7 @@ function createCvmService(options) {
|
|
|
2320
2592
|
const m = msg;
|
|
2321
2593
|
const id = m.id ?? "";
|
|
2322
2594
|
void transport.discover(m.query).then((servers) => send({ type: "cvm.discover.result", id, servers })).catch(
|
|
2323
|
-
(err) => send({ type: "cvm.discover.result", id, servers: [], error:
|
|
2595
|
+
(err) => send({ type: "cvm.discover.result", id, servers: [], error: toErrorMessage4(err) })
|
|
2324
2596
|
);
|
|
2325
2597
|
}
|
|
2326
2598
|
function handleRequest(windowId, msg, send) {
|
|
@@ -2336,7 +2608,7 @@ function createCvmService(options) {
|
|
|
2336
2608
|
}
|
|
2337
2609
|
openSession(windowId, m.server, send);
|
|
2338
2610
|
void transport.request(m.server, m.message, m.options).then((message) => send({ type: "cvm.request.result", id, message })).catch(
|
|
2339
|
-
(err) => send({ type: "cvm.request.result", id, error:
|
|
2611
|
+
(err) => send({ type: "cvm.request.result", id, error: toErrorMessage4(err) })
|
|
2340
2612
|
);
|
|
2341
2613
|
}
|
|
2342
2614
|
function handleClose(windowId, msg, send) {
|
|
@@ -2347,7 +2619,7 @@ function createCvmService(options) {
|
|
|
2347
2619
|
return;
|
|
2348
2620
|
}
|
|
2349
2621
|
closeSession(windowId, m.server.pubkey);
|
|
2350
|
-
void transport.close(m.server).then(() => send({ type: "cvm.close.result", id })).catch((err) => send({ type: "cvm.close.result", id, error:
|
|
2622
|
+
void transport.close(m.server).then(() => send({ type: "cvm.close.result", id })).catch((err) => send({ type: "cvm.close.result", id, error: toErrorMessage4(err) }));
|
|
2351
2623
|
}
|
|
2352
2624
|
return {
|
|
2353
2625
|
descriptor: CVM_DESCRIPTOR,
|
|
@@ -2378,7 +2650,7 @@ function createCvmService(options) {
|
|
|
2378
2650
|
}
|
|
2379
2651
|
};
|
|
2380
2652
|
}
|
|
2381
|
-
function
|
|
2653
|
+
function toErrorMessage4(err) {
|
|
2382
2654
|
if (err instanceof Error) return err.message;
|
|
2383
2655
|
if (typeof err === "string") return err;
|
|
2384
2656
|
return "cvm request failed";
|
|
@@ -2390,6 +2662,7 @@ export {
|
|
|
2390
2662
|
createConfigService,
|
|
2391
2663
|
createCoordinatedRelay,
|
|
2392
2664
|
createCvmService,
|
|
2665
|
+
createHttpUploader,
|
|
2393
2666
|
createIdentityService,
|
|
2394
2667
|
createKeysService,
|
|
2395
2668
|
createMediaService,
|
|
@@ -2399,6 +2672,7 @@ export {
|
|
|
2399
2672
|
createRelayPoolOutboxRouter,
|
|
2400
2673
|
createRelayPoolService,
|
|
2401
2674
|
createResourceService,
|
|
2402
|
-
createThemeService
|
|
2675
|
+
createThemeService,
|
|
2676
|
+
createUploadService
|
|
2403
2677
|
};
|
|
2404
2678
|
//# sourceMappingURL=index.js.map
|