@kehto/services 0.6.0 → 0.8.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
@@ -231,156 +231,6 @@ function createNotificationService(options) {
231
231
 
232
232
  // src/identity-service.ts
233
233
  var IDENTITY_SERVICE_VERSION = "1.0.0";
234
- var DECRYPT_ERROR_CODES = [
235
- "class-forbidden",
236
- "signer-denied",
237
- "signer-unavailable",
238
- "decrypt-failed",
239
- "malformed-wrap",
240
- "impersonation",
241
- "unsupported-encryption",
242
- "policy-denied"
243
- ];
244
- var DECRYPT_ERROR_CODE_SET = new Set(DECRYPT_ERROR_CODES);
245
- function isDecryptErrorCode(value) {
246
- return typeof value === "string" && DECRYPT_ERROR_CODE_SET.has(value);
247
- }
248
- function normalizeDecryptError(error) {
249
- if (isDecryptErrorCode(error)) return error;
250
- if (typeof error === "object" && error !== null) {
251
- const candidate = error;
252
- if (isDecryptErrorCode(candidate.code)) return candidate.code;
253
- if (isDecryptErrorCode(candidate.error)) return candidate.error;
254
- if (isDecryptErrorCode(candidate.message)) return candidate.message;
255
- }
256
- return "decrypt-failed";
257
- }
258
- function sendDecryptError(id, error, send) {
259
- const result = {
260
- type: "identity.decrypt.error",
261
- id,
262
- error
263
- };
264
- send(result);
265
- }
266
- function isStringArrayArray(value) {
267
- return Array.isArray(value) && value.every(
268
- (tag) => Array.isArray(tag) && tag.every((part) => typeof part === "string")
269
- );
270
- }
271
- function isNostrEvent(value) {
272
- const event = value;
273
- return typeof event === "object" && event !== null && typeof event.id === "string" && typeof event.pubkey === "string" && typeof event.created_at === "number" && typeof event.kind === "number" && isStringArrayArray(event.tags) && typeof event.content === "string" && typeof event.sig === "string";
274
- }
275
- function isRumor(value) {
276
- const rumor = value;
277
- return typeof rumor === "object" && rumor !== null && typeof rumor.id === "string" && typeof rumor.pubkey === "string" && typeof rumor.created_at === "number" && typeof rumor.kind === "number" && isStringArrayArray(rumor.tags) && typeof rumor.content === "string";
278
- }
279
- function isGiftWrapDecryptResult(value) {
280
- const result = value;
281
- return typeof result === "object" && result !== null && isNostrEvent(result.seal) && isRumor(result.rumor);
282
- }
283
- function firstDecodedByte(content) {
284
- const trimmed = content.trim();
285
- if (trimmed.length === 0) return null;
286
- const normalized = trimmed.replace(/-/g, "+").replace(/_/g, "/");
287
- const padded = normalized.padEnd(Math.ceil(normalized.length / 4) * 4, "=");
288
- try {
289
- const decoded = atob(padded);
290
- return decoded.length > 0 ? decoded.charCodeAt(0) : null;
291
- } catch {
292
- return null;
293
- }
294
- }
295
- function detectEncryptionMode(event) {
296
- if (event.kind === 4) return "nip04";
297
- if (event.kind === 1059) return "nip17";
298
- if (event.kind === 14 || firstDecodedByte(event.content) === 2) {
299
- return "nip44-direct";
300
- }
301
- return null;
302
- }
303
- function rumorFromSignedEvent(event, content) {
304
- return {
305
- id: event.id,
306
- pubkey: event.pubkey,
307
- kind: event.kind,
308
- tags: event.tags,
309
- created_at: event.created_at,
310
- content
311
- };
312
- }
313
- async function handleDecrypt(id, message, send, options) {
314
- const event = message.event;
315
- if (!isNostrEvent(event)) {
316
- sendDecryptError(id, "malformed-wrap", send);
317
- return;
318
- }
319
- const verifyEvent = options.verifyEvent ?? (() => true);
320
- let verified;
321
- try {
322
- verified = await Promise.resolve(verifyEvent(event));
323
- } catch {
324
- sendDecryptError(id, "malformed-wrap", send);
325
- return;
326
- }
327
- if (!verified) {
328
- sendDecryptError(id, "malformed-wrap", send);
329
- return;
330
- }
331
- const mode = detectEncryptionMode(event);
332
- if (!mode) {
333
- sendDecryptError(id, "unsupported-encryption", send);
334
- return;
335
- }
336
- const decryptor = options.getDecryptor?.() ?? null;
337
- if (!decryptor) {
338
- sendDecryptError(id, "signer-unavailable", send);
339
- return;
340
- }
341
- try {
342
- if (mode === "nip04") {
343
- const plaintext = await decryptor.nip04Decrypt(event.pubkey, event.content);
344
- const result2 = {
345
- type: "identity.decrypt.result",
346
- id,
347
- rumor: rumorFromSignedEvent(event, plaintext),
348
- sender: event.pubkey
349
- };
350
- send(result2);
351
- return;
352
- }
353
- if (mode === "nip44-direct") {
354
- const plaintext = await decryptor.nip44Decrypt(event.pubkey, event.content);
355
- const result2 = {
356
- type: "identity.decrypt.result",
357
- id,
358
- rumor: rumorFromSignedEvent(event, plaintext),
359
- sender: event.pubkey
360
- };
361
- send(result2);
362
- return;
363
- }
364
- const unwrapped = await decryptor.unwrapGiftWrap(event);
365
- if (!isGiftWrapDecryptResult(unwrapped)) {
366
- sendDecryptError(id, "malformed-wrap", send);
367
- return;
368
- }
369
- if (unwrapped.seal.pubkey !== unwrapped.rumor.pubkey) {
370
- sendDecryptError(id, "impersonation", send);
371
- return;
372
- }
373
- const result = {
374
- type: "identity.decrypt.result",
375
- id,
376
- rumor: unwrapped.rumor,
377
- sender: unwrapped.seal.pubkey
378
- };
379
- send(result);
380
- } catch (error) {
381
- sendDecryptError(id, normalizeDecryptError(error), send);
382
- }
383
- }
384
234
  function createIdentityService(options) {
385
235
  return {
386
236
  descriptor: {
@@ -496,10 +346,6 @@ function createIdentityService(options) {
496
346
  send(result);
497
347
  return;
498
348
  }
499
- case "identity.decrypt": {
500
- void handleDecrypt(id, message, send, options);
501
- return;
502
- }
503
349
  default:
504
350
  sendError(message.type, `Unknown identity method: ${message.type}`);
505
351
  }
@@ -2278,6 +2124,278 @@ function normalizePublishResult(res, relayUrls) {
2278
2124
  return out;
2279
2125
  }
2280
2126
 
2127
+ // src/upload-service.ts
2128
+ var UPLOAD_SERVICE_VERSION = "1.0.0";
2129
+ var UPLOAD_DESCRIPTOR = {
2130
+ name: "upload",
2131
+ version: UPLOAD_SERVICE_VERSION,
2132
+ description: "NAP-UPLOAD shell-mediated file/blob upload \u2014 upload/status with progress pushes"
2133
+ };
2134
+ function createUploadService(options) {
2135
+ if (!options || typeof options.uploader !== "object" || options.uploader === null) {
2136
+ throw new Error("createUploadService: options.uploader is required");
2137
+ }
2138
+ const { uploader } = options;
2139
+ const generateId2 = options.generateId ?? (() => crypto.randomUUID());
2140
+ const now = options.now ?? (() => Date.now());
2141
+ const entries = /* @__PURE__ */ new Map();
2142
+ function handleUpload(windowId, msg, send) {
2143
+ const m = msg;
2144
+ const id = m.id ?? "";
2145
+ const request = m.request;
2146
+ if (!request || typeof request !== "object" || request.data == null) {
2147
+ send({ type: "upload.upload.result", id, error: "invalid request" });
2148
+ return;
2149
+ }
2150
+ const uploadId = generateId2();
2151
+ const key = `${windowId}:${uploadId}`;
2152
+ entries.set(key, { uploadId });
2153
+ const ctx = {
2154
+ uploadId,
2155
+ windowId,
2156
+ onStatus: (status) => {
2157
+ const stamped = { ...status, uploadId, updatedAt: status.updatedAt || now() };
2158
+ const entry = entries.get(key);
2159
+ if (entry) entry.status = stamped;
2160
+ send({ type: "upload.status.changed", status: stamped });
2161
+ }
2162
+ };
2163
+ void uploader.upload(request, ctx).then((result) => {
2164
+ const stamped = { ...result, uploadId };
2165
+ const entry = entries.get(key);
2166
+ if (entry) entry.status = { ...stamped, updatedAt: now() };
2167
+ send({ type: "upload.upload.result", id, result: stamped });
2168
+ }).catch((err) => {
2169
+ entries.delete(key);
2170
+ send({ type: "upload.upload.result", id, error: toErrorMessage2(err) });
2171
+ });
2172
+ }
2173
+ function handleStatus(windowId, msg, send) {
2174
+ const m = msg;
2175
+ const id = m.id ?? "";
2176
+ const uploadId = m.uploadId;
2177
+ if (typeof uploadId !== "string" || uploadId.length === 0) {
2178
+ send({ type: "upload.status.result", id, error: "invalid uploadId" });
2179
+ return;
2180
+ }
2181
+ const tracked = entries.get(`${windowId}:${uploadId}`)?.status;
2182
+ if (tracked) {
2183
+ send({ type: "upload.status.result", id, status: tracked });
2184
+ return;
2185
+ }
2186
+ if (uploader.status) {
2187
+ void uploader.status(uploadId).then(
2188
+ (status) => send(
2189
+ status ? { type: "upload.status.result", id, status } : { type: "upload.status.result", id, error: "unknown upload" }
2190
+ )
2191
+ ).catch(
2192
+ (err) => send({ type: "upload.status.result", id, error: toErrorMessage2(err) })
2193
+ );
2194
+ return;
2195
+ }
2196
+ send({ type: "upload.status.result", id, error: "unknown upload" });
2197
+ }
2198
+ return {
2199
+ descriptor: UPLOAD_DESCRIPTOR,
2200
+ handleMessage(windowId, message, send) {
2201
+ switch (message.type) {
2202
+ case "upload.upload":
2203
+ handleUpload(windowId, message, send);
2204
+ return;
2205
+ case "upload.status":
2206
+ handleStatus(windowId, message, send);
2207
+ return;
2208
+ default:
2209
+ return;
2210
+ }
2211
+ },
2212
+ onWindowDestroyed(windowId) {
2213
+ const prefix = `${windowId}:`;
2214
+ for (const [key, entry] of entries) {
2215
+ if (key.startsWith(prefix)) {
2216
+ uploader.cancel?.(entry.uploadId);
2217
+ entries.delete(key);
2218
+ }
2219
+ }
2220
+ }
2221
+ };
2222
+ }
2223
+ function toErrorMessage2(err) {
2224
+ if (err instanceof Error) return err.message;
2225
+ if (typeof err === "string") return err;
2226
+ return "upload request failed";
2227
+ }
2228
+
2229
+ // src/http-uploader.ts
2230
+ var KIND_NIP98 = 27235;
2231
+ var KIND_BLOSSOM_AUTH = 24242;
2232
+ var BLOSSOM_AUTH_TTL_S = 3600;
2233
+ function createHttpUploader(options) {
2234
+ if (!options || typeof options.signEvent !== "function") {
2235
+ throw new Error("createHttpUploader: options.signEvent is required");
2236
+ }
2237
+ const rails = options.rails ?? {};
2238
+ const signEvent = options.signEvent;
2239
+ const fetchFn = options.fetch ?? fetch;
2240
+ const digest = options.digestSha256 ?? defaultDigestSha256;
2241
+ const nowS = options.now ?? (() => Math.floor(Date.now() / 1e3));
2242
+ const defaultRail = options.defaultRail ?? firstConfiguredRail(rails);
2243
+ async function upload(request, ctx) {
2244
+ const rail = request.rail ?? defaultRail;
2245
+ const config = rail === "nip96" ? rails.nip96 : rail === "blossom" ? rails.blossom : void 0;
2246
+ if (rail !== "nip96" && rail !== "blossom") {
2247
+ return failed(ctx.uploadId, rail ?? "unknown", "unsupported rail");
2248
+ }
2249
+ const server = config?.servers?.[0];
2250
+ if (!server) {
2251
+ return failed(ctx.uploadId, rail, "no server configured");
2252
+ }
2253
+ const bytes = await toBytes(request.data);
2254
+ const sha256 = await digest(bytes);
2255
+ try {
2256
+ return rail === "nip96" ? await uploadNip96({ request, ctx, server, bytes, sha256, signEvent, fetchFn, nowS }) : await uploadBlossom({ request, ctx, server, bytes, sha256, signEvent, fetchFn, nowS });
2257
+ } catch (err) {
2258
+ return failed(ctx.uploadId, rail, toErrorMessage3(err), sha256);
2259
+ }
2260
+ }
2261
+ return { upload };
2262
+ }
2263
+ async function uploadNip96(args) {
2264
+ const { request, ctx, server, bytes, sha256, signEvent, fetchFn, nowS } = args;
2265
+ const auth = await signEvent({
2266
+ kind: KIND_NIP98,
2267
+ created_at: nowS(),
2268
+ content: "",
2269
+ tags: [
2270
+ ["u", server],
2271
+ ["method", "POST"],
2272
+ ["payload", sha256]
2273
+ ]
2274
+ });
2275
+ const form = new FormData();
2276
+ form.append("file", new Blob([bytesToArrayBuffer(bytes)], { type: request.mimeType }), request.filename ?? "file");
2277
+ if (request.caption !== void 0) form.append("caption", request.caption);
2278
+ if (request.mimeType !== void 0) form.append("content_type", request.mimeType);
2279
+ if (request.noTransform) form.append("no_transform", "true");
2280
+ const res = await fetchFn(server, {
2281
+ method: "POST",
2282
+ headers: { Authorization: nostrAuthHeader(auth) },
2283
+ body: form
2284
+ });
2285
+ if (!res.ok) {
2286
+ return failed(ctx.uploadId, "nip96", `server rejected (HTTP ${res.status})`, sha256);
2287
+ }
2288
+ const body = await res.json();
2289
+ if (body.status === "error") {
2290
+ return failed(ctx.uploadId, "nip96", body.message ?? "upload failed", sha256);
2291
+ }
2292
+ const tags = body.nip94_event?.tags ?? [];
2293
+ return fromNip94Tags(ctx.uploadId, "nip96", tags, bytes.byteLength, sha256);
2294
+ }
2295
+ function fromNip94Tags(uploadId, rail, tags, fallbackSize, fallbackSha) {
2296
+ const get = (name) => tags.find((t) => t[0] === name)?.[1];
2297
+ const url = get("url");
2298
+ const result = {
2299
+ ok: Boolean(url),
2300
+ uploadId,
2301
+ status: url ? "complete" : "failed",
2302
+ rail,
2303
+ sha256: get("x") ?? fallbackSha,
2304
+ nip94: tags
2305
+ };
2306
+ if (url) result.url = url;
2307
+ const ox = get("ox");
2308
+ if (ox) result.originalSha256 = ox;
2309
+ const size = get("size");
2310
+ result.size = size ? Number(size) : fallbackSize;
2311
+ const m = get("m");
2312
+ if (m) result.mimeType = m;
2313
+ const dim = parseDimensions(get("dim"));
2314
+ if (dim) result.dimensions = dim;
2315
+ const blurhash = get("blurhash");
2316
+ if (blurhash) result.blurhash = blurhash;
2317
+ if (!url) result.error = "server returned no url";
2318
+ return result;
2319
+ }
2320
+ async function uploadBlossom(args) {
2321
+ const { request, ctx, server, bytes, sha256, signEvent, fetchFn, nowS } = args;
2322
+ const auth = await signEvent({
2323
+ kind: KIND_BLOSSOM_AUTH,
2324
+ created_at: nowS(),
2325
+ content: `Upload ${request.filename ?? "file"}`,
2326
+ tags: [
2327
+ ["t", "upload"],
2328
+ ["x", sha256],
2329
+ ["expiration", String(nowS() + BLOSSOM_AUTH_TTL_S)]
2330
+ ]
2331
+ });
2332
+ const endpoint = `${trimTrailingSlash(server)}/upload`;
2333
+ const headers = { Authorization: nostrAuthHeader(auth) };
2334
+ if (request.mimeType) headers["Content-Type"] = request.mimeType;
2335
+ const res = await fetchFn(endpoint, {
2336
+ method: "PUT",
2337
+ headers,
2338
+ body: bytesToArrayBuffer(bytes)
2339
+ });
2340
+ if (!res.ok) {
2341
+ return failed(ctx.uploadId, "blossom", `server rejected (HTTP ${res.status})`, sha256);
2342
+ }
2343
+ const blob = await res.json();
2344
+ if (!blob.url) {
2345
+ return failed(ctx.uploadId, "blossom", "server returned no url", sha256);
2346
+ }
2347
+ const result = {
2348
+ ok: true,
2349
+ uploadId: ctx.uploadId,
2350
+ status: "complete",
2351
+ rail: "blossom",
2352
+ url: blob.url,
2353
+ sha256: blob.sha256 ?? sha256,
2354
+ size: blob.size ?? bytes.byteLength
2355
+ };
2356
+ if (blob.type) result.mimeType = blob.type;
2357
+ return result;
2358
+ }
2359
+ function failed(uploadId, rail, error, sha256) {
2360
+ return { ok: false, uploadId, status: "failed", rail, error, ...sha256 ? { sha256 } : {} };
2361
+ }
2362
+ function firstConfiguredRail(rails) {
2363
+ if (rails.nip96?.servers?.length) return "nip96";
2364
+ if (rails.blossom?.servers?.length) return "blossom";
2365
+ return void 0;
2366
+ }
2367
+ function nostrAuthHeader(event) {
2368
+ return `Nostr ${base64Utf8(JSON.stringify(event))}`;
2369
+ }
2370
+ function base64Utf8(s) {
2371
+ return btoa(String.fromCharCode(...new TextEncoder().encode(s)));
2372
+ }
2373
+ function parseDimensions(dim) {
2374
+ if (!dim) return void 0;
2375
+ const m = /^(\d+)x(\d+)$/.exec(dim);
2376
+ if (!m) return void 0;
2377
+ return { width: Number(m[1]), height: Number(m[2]) };
2378
+ }
2379
+ async function toBytes(data) {
2380
+ if (data instanceof ArrayBuffer) return new Uint8Array(data);
2381
+ return new Uint8Array(await data.arrayBuffer());
2382
+ }
2383
+ function bytesToArrayBuffer(bytes) {
2384
+ return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
2385
+ }
2386
+ function trimTrailingSlash(url) {
2387
+ return url.endsWith("/") ? url.slice(0, -1) : url;
2388
+ }
2389
+ async function defaultDigestSha256(bytes) {
2390
+ const buf = await crypto.subtle.digest("SHA-256", bytesToArrayBuffer(bytes));
2391
+ return Array.from(new Uint8Array(buf)).map((b) => b.toString(16).padStart(2, "0")).join("");
2392
+ }
2393
+ function toErrorMessage3(err) {
2394
+ if (err instanceof Error) return err.message;
2395
+ if (typeof err === "string") return err;
2396
+ return "upload failed";
2397
+ }
2398
+
2281
2399
  // src/cvm-service.ts
2282
2400
  var CVM_SERVICE_VERSION = "1.0.0";
2283
2401
  var CVM_DESCRIPTOR = {
@@ -2320,7 +2438,7 @@ function createCvmService(options) {
2320
2438
  const m = msg;
2321
2439
  const id = m.id ?? "";
2322
2440
  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: toErrorMessage2(err) })
2441
+ (err) => send({ type: "cvm.discover.result", id, servers: [], error: toErrorMessage4(err) })
2324
2442
  );
2325
2443
  }
2326
2444
  function handleRequest(windowId, msg, send) {
@@ -2336,7 +2454,7 @@ function createCvmService(options) {
2336
2454
  }
2337
2455
  openSession(windowId, m.server, send);
2338
2456
  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: toErrorMessage2(err) })
2457
+ (err) => send({ type: "cvm.request.result", id, error: toErrorMessage4(err) })
2340
2458
  );
2341
2459
  }
2342
2460
  function handleClose(windowId, msg, send) {
@@ -2347,7 +2465,7 @@ function createCvmService(options) {
2347
2465
  return;
2348
2466
  }
2349
2467
  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: toErrorMessage2(err) }));
2468
+ void transport.close(m.server).then(() => send({ type: "cvm.close.result", id })).catch((err) => send({ type: "cvm.close.result", id, error: toErrorMessage4(err) }));
2351
2469
  }
2352
2470
  return {
2353
2471
  descriptor: CVM_DESCRIPTOR,
@@ -2378,7 +2496,7 @@ function createCvmService(options) {
2378
2496
  }
2379
2497
  };
2380
2498
  }
2381
- function toErrorMessage2(err) {
2499
+ function toErrorMessage4(err) {
2382
2500
  if (err instanceof Error) return err.message;
2383
2501
  if (typeof err === "string") return err;
2384
2502
  return "cvm request failed";
@@ -2390,6 +2508,7 @@ export {
2390
2508
  createConfigService,
2391
2509
  createCoordinatedRelay,
2392
2510
  createCvmService,
2511
+ createHttpUploader,
2393
2512
  createIdentityService,
2394
2513
  createKeysService,
2395
2514
  createMediaService,
@@ -2399,6 +2518,7 @@ export {
2399
2518
  createRelayPoolOutboxRouter,
2400
2519
  createRelayPoolService,
2401
2520
  createResourceService,
2402
- createThemeService
2521
+ createThemeService,
2522
+ createUploadService
2403
2523
  };
2404
2524
  //# sourceMappingURL=index.js.map