@elisym/sdk 0.25.0 → 0.25.3

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
@@ -73,9 +73,16 @@ var DEFAULTS = {
73
73
  // default, not a protocol constant - the transfer is resumable and its own
74
74
  // budget, decoupled from the result-wait window.
75
75
  IROH_FETCH_TIMEOUT_MS: 3e5,
76
+ // Ceiling for a single iroh SEED (addFromPath/addBytes/share). Seeding is local
77
+ // (hash + store-copy + ticket mint), so this is a generous backstop: it bounds
78
+ // the JS await so a wedged native call surfaces as a thrown error (and triggers a
79
+ // node reset) instead of an indefinite hang that stalls file delivery.
80
+ IROH_SEED_TIMEOUT_MS: 12e4,
76
81
  // Ceiling for a single Blossom blob upload (PUT /upload). Large blobs (up to
77
82
  // LIMITS.MAX_FILE_SIZE) need far more than the 30s used for small media images.
78
- BLOSSOM_UPLOAD_TIMEOUT_MS: 3e5
83
+ BLOSSOM_UPLOAD_TIMEOUT_MS: 3e5,
84
+ // Ceiling for a single encrypted Blossom blob download (GET). Same budget as upload.
85
+ BLOSSOM_FETCH_TIMEOUT_MS: 3e5
79
86
  };
80
87
  var LIMITS = {
81
88
  MAX_INPUT_LENGTH: 1e5,
@@ -100,6 +107,12 @@ var LIMITS = {
100
107
  // providers may lower it per deployment.
101
108
  MAX_FILE_SIZE: 1073741824,
102
109
  // 1 GiB
110
+ // Cap for the ENCRYPTED Blossom path (web/SDK). The encrypt-then-upload flow is
111
+ // whole-buffer in WebCrypto + BlossomService (~3x file-size peak RAM), so this is
112
+ // deliberately far below MAX_FILE_SIZE to stay safe in a browser tab; larger files
113
+ // use iroh. The relay enforces a ~128 MiB server-side backstop.
114
+ MAX_BLOSSOM_ENCRYPTED_BYTES: 104857600,
115
+ // 100 MiB
103
116
  MAX_TIMEOUT_SECS: 600,
104
117
  // Upper bound for execution budgets (`max_execution_secs` / `execution_timeout_secs`).
105
118
  // Distinct from MAX_TIMEOUT_SECS (the result-wait cap): execution budgets may be
@@ -1186,6 +1199,17 @@ var BlossomService = class {
1186
1199
  this.serverUrl = serverUrl;
1187
1200
  this.fallback = fallback;
1188
1201
  }
1202
+ /**
1203
+ * The content-addressed GET URL for a blob, derivable from its sha256 BEFORE
1204
+ * upload (BUD-01: `<serverUrl>/<sha256>`, no extension for our octet-stream
1205
+ * ciphertext uploads - same form `delete` addresses by). Lets a caller build a
1206
+ * complete attachment descriptor and defer the actual byte upload (the descriptor
1207
+ * is submitted first, the bytes PUT later). `upload()` re-verifies the server
1208
+ * returns this exact url.
1209
+ */
1210
+ contentUrl(sha256) {
1211
+ return `${this.serverUrl}/${sha256}`;
1212
+ }
1189
1213
  /**
1190
1214
  * Upload a file to the Blossom server, returning its descriptor. On any failure, falls
1191
1215
  * back to the configured uploader (if any) and returns a normalized descriptor with
@@ -1237,6 +1261,62 @@ var BlossomService = class {
1237
1261
  clearTimeout(timer);
1238
1262
  }
1239
1263
  }
1264
+ /**
1265
+ * Download a public blob (BUD-01 GET, no auth). Bounds memory on the ACTUAL streamed bytes (never
1266
+ * the declared Content-Length) and verifies the sha256 when `expectedSha256` is given. Browser-safe.
1267
+ */
1268
+ async download(url, opts = {}) {
1269
+ const maxBytes = opts.maxBytes ?? LIMITS.MAX_FILE_SIZE;
1270
+ const controller = new AbortController();
1271
+ const timer = setTimeout(
1272
+ () => controller.abort(),
1273
+ opts.timeoutMs ?? DEFAULTS.BLOSSOM_FETCH_TIMEOUT_MS
1274
+ );
1275
+ try {
1276
+ const res = await fetch(url, { signal: controller.signal });
1277
+ if (!res.ok) {
1278
+ throw new Error(`Download failed: ${res.status} ${res.statusText}`);
1279
+ }
1280
+ const declared = Number(res.headers.get("content-length"));
1281
+ if (Number.isFinite(declared) && declared > maxBytes) {
1282
+ throw new Error(`Blob too large: ${declared} bytes exceeds limit of ${maxBytes}.`);
1283
+ }
1284
+ if (!res.body) {
1285
+ throw new Error("Download response has no body.");
1286
+ }
1287
+ const reader = res.body.getReader();
1288
+ const chunks = [];
1289
+ let total = 0;
1290
+ let chunk = await reader.read();
1291
+ while (!chunk.done) {
1292
+ total += chunk.value.byteLength;
1293
+ if (total > maxBytes) {
1294
+ await reader.cancel();
1295
+ throw new Error(`Blob exceeds limit of ${maxBytes} bytes.`);
1296
+ }
1297
+ chunks.push(chunk.value);
1298
+ chunk = await reader.read();
1299
+ }
1300
+ const bytes = new Uint8Array(total);
1301
+ let offset = 0;
1302
+ for (const c of chunks) {
1303
+ bytes.set(c, offset);
1304
+ offset += c.byteLength;
1305
+ }
1306
+ if (opts.expectedSha256 !== void 0) {
1307
+ const digest = await crypto.subtle.digest("SHA-256", bytes);
1308
+ const hashHex = [...new Uint8Array(digest)].map((b) => b.toString(16).padStart(2, "0")).join("");
1309
+ if (hashHex !== opts.expectedSha256) {
1310
+ throw new Error(
1311
+ `Download integrity check failed: got ${hashHex}, expected ${opts.expectedSha256}.`
1312
+ );
1313
+ }
1314
+ }
1315
+ return bytes;
1316
+ } finally {
1317
+ clearTimeout(timer);
1318
+ }
1319
+ }
1240
1320
  async uploadToBlossom(identity, bytes, hashHex, mime) {
1241
1321
  const contentType = mime || "application/octet-stream";
1242
1322
  const authHeader = this.authHeader(identity, "upload", hashHex);
@@ -1368,6 +1448,9 @@ function parseCapabilityEvent(event, network) {
1368
1448
  if (card.inputMime !== void 0 && (typeof card.inputMime !== "string" || card.inputMime.length > 255) || card.outputMime !== void 0 && (typeof card.outputMime !== "string" || card.outputMime.length > 255)) {
1369
1449
  return null;
1370
1450
  }
1451
+ if (card.inputText !== void 0 && !["required", "optional", "none"].includes(card.inputText)) {
1452
+ card.inputText = void 0;
1453
+ }
1371
1454
  if (card.payment?.job_price !== null && card.payment?.job_price !== void 0 && (!Number.isInteger(card.payment.job_price) || card.payment.job_price < 0)) {
1372
1455
  return null;
1373
1456
  }
@@ -1952,6 +2035,26 @@ var FileTransportSchema = z.discriminatedUnion("kind", [
1952
2035
  kind: z.literal("iroh"),
1953
2036
  /** Opaque iroh `BlobTicket` string. Parsed into a real ticket only at fetch time. */
1954
2037
  ticket: z.string().min(1).max(MAX_TICKET_LENGTH)
2038
+ }),
2039
+ z.object({
2040
+ kind: z.literal("blossom"),
2041
+ /** Public HTTP(S) URL of the CIPHERTEXT blob on a Blossom relay. */
2042
+ url: z.string().url().max(2048),
2043
+ /** sha256 (lowercase hex) of the ciphertext - what the relay stores and addresses. */
2044
+ sha256: z.string().regex(/^[0-9a-f]{64}$/),
2045
+ /**
2046
+ * Hybrid-encryption parameters. The file bytes are AES-256-GCM encrypted with a random
2047
+ * content key; that key is NIP-44-wrapped to the recipient. `name`/`mime`/`size` on the
2048
+ * attachment describe the PLAINTEXT and live only inside the (encrypted) envelope - never
2049
+ * sent to the relay (the relay only ever sees opaque ciphertext).
2050
+ */
2051
+ enc: z.object({
2052
+ alg: z.literal("AES-256-GCM"),
2053
+ /** base64 12-byte GCM IV (non-secret). */
2054
+ iv: z.string().min(1).max(64),
2055
+ /** NIP-44-wrapped content key. */
2056
+ key: z.string().min(1).max(2048)
2057
+ })
1955
2058
  })
1956
2059
  ]);
1957
2060
  var FileAttachmentSchema = z.object({
@@ -1960,22 +2063,76 @@ var FileAttachmentSchema = z.object({
1960
2063
  /** Declared size in bytes (display/hint only; enforcement is on actual streamed bytes). */
1961
2064
  size: z.number().int().nonnegative(),
1962
2065
  mime: z.string().min(1).max(255),
1963
- /** Ordered by sender preference; at least one. */
1964
- transports: z.array(FileTransportSchema).min(1),
2066
+ /**
2067
+ * Ordered by sender preference; at least one KNOWN transport. Parsed leniently: unknown
2068
+ * transport `kind`s are dropped (not rejected) so adding a new transport never makes an older
2069
+ * decoder throw away the whole envelope - it just ignores the kinds it doesn't know and uses
2070
+ * the ones it does. At least one known transport must survive, else the attachment is invalid.
2071
+ */
2072
+ transports: z.array(z.unknown()).transform(
2073
+ (arr) => arr.flatMap((t) => {
2074
+ const parsed = FileTransportSchema.safeParse(t);
2075
+ return parsed.success ? [parsed.data] : [];
2076
+ })
2077
+ ).refine((arr) => arr.length >= 1, { message: "attachment has no known transport" }),
1965
2078
  /** Optional provider hint (unix seconds) for when seeding may stop. */
1966
2079
  seedingExpiresAt: z.number().int().nonnegative().optional()
1967
2080
  });
1968
2081
  var JobPayloadEnvelopeSchema = z.object({
1969
2082
  v: z.literal(ENVELOPE_VERSION),
1970
2083
  text: z.string().optional(),
1971
- attachment: FileAttachmentSchema.optional()
2084
+ // Legacy single attachment - kept (and mirrored from `attachments[0]`) so an old
2085
+ // decoder that doesn't know `attachments` still gets the first file.
2086
+ attachment: FileAttachmentSchema.optional(),
2087
+ // Multiple result/input files. Additive; old decoders strip this unknown key.
2088
+ attachments: z.array(FileAttachmentSchema).optional()
1972
2089
  });
2090
+ var ACCEPT_TRANSPORTS_TAG = "accept";
2091
+ var KNOWN_TRANSPORT_KINDS = ["iroh", "blossom"];
2092
+ function isKnownTransportKind(value) {
2093
+ return KNOWN_TRANSPORT_KINDS.includes(value);
2094
+ }
2095
+ function buildAcceptTransportsTag(kinds) {
2096
+ const seen = /* @__PURE__ */ new Set();
2097
+ const out = [ACCEPT_TRANSPORTS_TAG];
2098
+ for (const kind of kinds) {
2099
+ if (isKnownTransportKind(kind) && !seen.has(kind)) {
2100
+ seen.add(kind);
2101
+ out.push(kind);
2102
+ }
2103
+ }
2104
+ return out;
2105
+ }
2106
+ function readAcceptedTransports(tags) {
2107
+ const tag = tags.find((t) => t[0] === ACCEPT_TRANSPORTS_TAG);
2108
+ if (tag === void 0) {
2109
+ return void 0;
2110
+ }
2111
+ const seen = /* @__PURE__ */ new Set();
2112
+ const out = [];
2113
+ for (const value of tag.slice(1)) {
2114
+ if (isKnownTransportKind(value) && !seen.has(value)) {
2115
+ seen.add(value);
2116
+ out.push(value);
2117
+ }
2118
+ }
2119
+ return out.length > 0 ? out : void 0;
2120
+ }
2121
+ function attachmentsOf(decoded) {
2122
+ if (decoded.attachments !== void 0 && decoded.attachments.length > 0) {
2123
+ return decoded.attachments;
2124
+ }
2125
+ return decoded.attachment !== void 0 ? [decoded.attachment] : [];
2126
+ }
1973
2127
  function encodeJobPayload(payload) {
1974
2128
  const envelope = { v: ENVELOPE_VERSION };
1975
2129
  if (payload.text !== void 0) {
1976
2130
  envelope.text = payload.text;
1977
2131
  }
1978
- if (payload.attachment !== void 0) {
2132
+ if (payload.attachments !== void 0 && payload.attachments.length > 0) {
2133
+ envelope.attachments = payload.attachments;
2134
+ envelope.attachment = payload.attachments[0];
2135
+ } else if (payload.attachment !== void 0) {
1979
2136
  envelope.attachment = payload.attachment;
1980
2137
  }
1981
2138
  return JSON.stringify(envelope);
@@ -2003,7 +2160,11 @@ function decodeJobPayload(content) {
2003
2160
  `Invalid elisym job payload (v=${JSON.stringify(version)}): ${result.error.message}`
2004
2161
  );
2005
2162
  }
2006
- return { text: result.data.text, attachment: result.data.attachment };
2163
+ return {
2164
+ text: result.data.text,
2165
+ attachment: result.data.attachment,
2166
+ attachments: result.data.attachments
2167
+ };
2007
2168
  }
2008
2169
 
2009
2170
  // src/services/marketplace.ts
@@ -2072,6 +2233,12 @@ var MarketplaceService = class {
2072
2233
  tags.push(["p", options.providerPubkey]);
2073
2234
  tags.push(["encrypted", "nip44"]);
2074
2235
  }
2236
+ if (options.acceptTransports && options.acceptTransports.length > 0) {
2237
+ const acceptTag = buildAcceptTransportsTag(options.acceptTransports);
2238
+ if (acceptTag.length > 1) {
2239
+ tags.push(acceptTag);
2240
+ }
2241
+ }
2075
2242
  const kind = jobRequestKind(options.kindOffset ?? DEFAULT_KIND_OFFSET);
2076
2243
  const event = finalizeEvent(
2077
2244
  {
@@ -2162,7 +2329,7 @@ var MarketplaceService = class {
2162
2329
  }
2163
2330
  resultDelivered = true;
2164
2331
  try {
2165
- cb.onResult?.(decoded.text ?? "", ev.id, decoded.attachment);
2332
+ cb.onResult?.(decoded.text ?? "", ev.id, decoded.attachment, attachmentsOf(decoded));
2166
2333
  } catch {
2167
2334
  } finally {
2168
2335
  done();
@@ -2326,8 +2493,8 @@ var MarketplaceService = class {
2326
2493
  );
2327
2494
  }
2328
2495
  /** Submit a job result with NIP-44 encrypted content. Result kind is derived from the request kind. */
2329
- async submitJobResult(identity, requestEvent, content, amount, attachment) {
2330
- const hasAttachment = attachment !== void 0;
2496
+ async submitJobResult(identity, requestEvent, content, amount, attachments) {
2497
+ const hasAttachment = attachments !== void 0 && attachments.length > 0;
2331
2498
  if (!content && !hasAttachment) {
2332
2499
  throw new Error("Job result content must not be empty.");
2333
2500
  }
@@ -2341,7 +2508,7 @@ var MarketplaceService = class {
2341
2508
  );
2342
2509
  }
2343
2510
  const shouldEncrypt = isEncrypted(requestEvent);
2344
- const payload = hasAttachment ? encodeJobPayload({ text: content || void 0, attachment }) : content;
2511
+ const payload = hasAttachment ? encodeJobPayload({ text: content || void 0, attachments }) : content;
2345
2512
  if (shouldEncrypt) {
2346
2513
  const payloadBytes = utf8ByteLength(payload);
2347
2514
  if (payloadBytes > LIMITS.NIP44_MAX_PLAINTEXT_BYTES) {
@@ -2382,11 +2549,11 @@ var MarketplaceService = class {
2382
2549
  * With maxAttempts=3: try, ~1s, try, ~2s, try, throw.
2383
2550
  * Jitter: 0.5x-1.0x of calculated delay.
2384
2551
  */
2385
- async submitJobResultWithRetry(identity, requestEvent, content, amount, maxAttempts = DEFAULTS.RESULT_RETRY_COUNT, baseDelayMs = DEFAULTS.RESULT_RETRY_BASE_MS, attachment) {
2552
+ async submitJobResultWithRetry(identity, requestEvent, content, amount, maxAttempts = DEFAULTS.RESULT_RETRY_COUNT, baseDelayMs = DEFAULTS.RESULT_RETRY_BASE_MS, attachments) {
2386
2553
  const attempts = Math.max(1, maxAttempts);
2387
2554
  for (let attempt = 0; attempt < attempts; attempt++) {
2388
2555
  try {
2389
- return await this.submitJobResult(identity, requestEvent, content, amount, attachment);
2556
+ return await this.submitJobResult(identity, requestEvent, content, amount, attachments);
2390
2557
  } catch (e) {
2391
2558
  if (attempt >= attempts - 1) {
2392
2559
  throw e;
@@ -3480,6 +3647,186 @@ var ElisymClient = class {
3480
3647
  }
3481
3648
  };
3482
3649
 
3650
+ // src/primitives/file-crypto.ts
3651
+ var KEY_BYTES = 32;
3652
+ var IV_BYTES = 12;
3653
+ function bytesToBase64(bytes) {
3654
+ let bin = "";
3655
+ for (const b of bytes) {
3656
+ bin += String.fromCharCode(b);
3657
+ }
3658
+ return btoa(bin);
3659
+ }
3660
+ function base64ToBytes(b64) {
3661
+ const bin = atob(b64);
3662
+ const out = new Uint8Array(bin.length);
3663
+ for (let i = 0; i < bin.length; i += 1) {
3664
+ out[i] = bin.charCodeAt(i);
3665
+ }
3666
+ return out;
3667
+ }
3668
+ async function encryptBytesForRecipient(bytes, senderSk, recipientPubkey) {
3669
+ const rawKey = crypto.getRandomValues(new Uint8Array(KEY_BYTES));
3670
+ const iv = crypto.getRandomValues(new Uint8Array(IV_BYTES));
3671
+ const key = await crypto.subtle.importKey("raw", rawKey, "AES-GCM", false, ["encrypt"]);
3672
+ const ct = await crypto.subtle.encrypt({ name: "AES-GCM", iv, tagLength: 128 }, key, bytes);
3673
+ const wrappedKey = nip44Encrypt(bytesToBase64(rawKey), senderSk, recipientPubkey);
3674
+ return { ciphertext: new Uint8Array(ct), wrappedKey, iv: bytesToBase64(iv) };
3675
+ }
3676
+ async function decryptBytesFromSender(ciphertext, wrappedKey, iv, receiverSk, senderPubkey) {
3677
+ const rawKey = base64ToBytes(nip44Decrypt(wrappedKey, receiverSk, senderPubkey));
3678
+ const key = await crypto.subtle.importKey("raw", rawKey, "AES-GCM", false, ["decrypt"]);
3679
+ const pt = await crypto.subtle.decrypt(
3680
+ { name: "AES-GCM", iv: base64ToBytes(iv), tagLength: 128 },
3681
+ key,
3682
+ ciphertext
3683
+ );
3684
+ return new Uint8Array(pt);
3685
+ }
3686
+
3687
+ // src/transport/blossom-transport.ts
3688
+ var AES_GCM_TAG_BYTES = 16;
3689
+ function createBlossomTransport(opts) {
3690
+ const { blossom, identity } = opts;
3691
+ return {
3692
+ async seedBytes({ bytes, recipientPubkey }) {
3693
+ if (bytes.byteLength > LIMITS.MAX_BLOSSOM_ENCRYPTED_BYTES) {
3694
+ throw new Error(
3695
+ `File too large for encrypted Blossom: ${bytes.byteLength} bytes exceeds ${LIMITS.MAX_BLOSSOM_ENCRYPTED_BYTES}.`
3696
+ );
3697
+ }
3698
+ const { ciphertext, wrappedKey, iv } = await encryptBytesForRecipient(
3699
+ bytes,
3700
+ identity.secretKey,
3701
+ recipientPubkey
3702
+ );
3703
+ const blob = new Blob([ciphertext], { type: "application/octet-stream" });
3704
+ const descriptor = await blossom.upload(identity, blob);
3705
+ if (descriptor.provider !== "blossom") {
3706
+ throw new Error("Blossom upload fell back to a non-content-addressed provider.");
3707
+ }
3708
+ return {
3709
+ kind: "blossom",
3710
+ url: descriptor.url,
3711
+ sha256: descriptor.sha256,
3712
+ enc: { alg: "AES-256-GCM", iv, key: wrappedKey }
3713
+ };
3714
+ },
3715
+ async fetchToBytes({ transport, senderPubkey, maxBytes }) {
3716
+ const plaintextCap = maxBytes ?? LIMITS.MAX_BLOSSOM_ENCRYPTED_BYTES;
3717
+ const ciphertext = await blossom.download(transport.url, {
3718
+ maxBytes: plaintextCap + AES_GCM_TAG_BYTES,
3719
+ expectedSha256: transport.sha256
3720
+ });
3721
+ return decryptBytesFromSender(
3722
+ ciphertext,
3723
+ transport.enc.key,
3724
+ transport.enc.iv,
3725
+ identity.secretKey,
3726
+ senderPubkey
3727
+ );
3728
+ }
3729
+ };
3730
+ }
3731
+
3732
+ // src/transport/file-jobs.ts
3733
+ async function sha256Hex(bytes) {
3734
+ const digest = await crypto.subtle.digest("SHA-256", bytes);
3735
+ return [...new Uint8Array(digest)].map((b) => b.toString(16).padStart(2, "0")).join("");
3736
+ }
3737
+ var EXECUTABLE_EXTENSIONS = /* @__PURE__ */ new Set([
3738
+ ".exe",
3739
+ ".dll",
3740
+ ".bat",
3741
+ ".cmd",
3742
+ ".com",
3743
+ ".msi",
3744
+ ".sh",
3745
+ ".app",
3746
+ ".scr",
3747
+ ".ps1"
3748
+ ]);
3749
+ var EXECUTABLE_MIMES = /* @__PURE__ */ new Set([
3750
+ "application/x-msdownload",
3751
+ "application/x-msdos-program",
3752
+ "application/x-sh",
3753
+ "application/x-executable",
3754
+ "application/vnd.microsoft.portable-executable",
3755
+ "application/x-mach-binary"
3756
+ ]);
3757
+ function looksExecutable(name, type) {
3758
+ const dot = name.lastIndexOf(".");
3759
+ const ext = dot >= 0 ? name.slice(dot).toLowerCase() : "";
3760
+ return EXECUTABLE_EXTENSIONS.has(ext) || EXECUTABLE_MIMES.has(type);
3761
+ }
3762
+ async function prepareEncryptedFileInput(args) {
3763
+ const { file, providerPubkey, identity, blossom } = args;
3764
+ const name = file.name ?? "upload";
3765
+ if (looksExecutable(name, file.type)) {
3766
+ throw new Error("Refusing to upload an executable file.");
3767
+ }
3768
+ const bytes = new Uint8Array(await file.arrayBuffer());
3769
+ if (bytes.byteLength > LIMITS.MAX_BLOSSOM_ENCRYPTED_BYTES) {
3770
+ throw new Error(
3771
+ `File too large for the encrypted-Blossom transport: ${bytes.byteLength} bytes (max ${LIMITS.MAX_BLOSSOM_ENCRYPTED_BYTES}).`
3772
+ );
3773
+ }
3774
+ const { ciphertext, wrappedKey, iv } = await encryptBytesForRecipient(
3775
+ bytes,
3776
+ identity.secretKey,
3777
+ providerPubkey
3778
+ );
3779
+ const sha256 = await sha256Hex(ciphertext);
3780
+ const member = {
3781
+ kind: "blossom",
3782
+ url: blossom.contentUrl(sha256),
3783
+ sha256,
3784
+ enc: { alg: "AES-256-GCM", iv, key: wrappedKey }
3785
+ };
3786
+ const attachment = {
3787
+ name,
3788
+ size: bytes.byteLength,
3789
+ mime: file.type || "application/octet-stream",
3790
+ transports: [member]
3791
+ };
3792
+ const upload = async () => {
3793
+ const descriptor = await blossom.upload(
3794
+ identity,
3795
+ new Blob([ciphertext], { type: "application/octet-stream" })
3796
+ );
3797
+ if (descriptor.provider !== "blossom") {
3798
+ throw new Error("Blossom upload fell back to a non-content-addressed provider.");
3799
+ }
3800
+ if (descriptor.sha256 !== sha256 || descriptor.url !== member.url) {
3801
+ throw new Error(
3802
+ `Blossom upload descriptor mismatch (expected ${member.url} / ${sha256}, got ${descriptor.url} / ${descriptor.sha256}).`
3803
+ );
3804
+ }
3805
+ };
3806
+ return { attachment, upload };
3807
+ }
3808
+ async function buildEncryptedFileInput(args) {
3809
+ const prepared = await prepareEncryptedFileInput(args);
3810
+ await prepared.upload();
3811
+ return prepared.attachment;
3812
+ }
3813
+ async function fetchEncryptedFileOutput(args) {
3814
+ const { attachment, providerPubkey, identity, blossom, maxBytes } = args;
3815
+ const member = attachment.transports.find(
3816
+ (t) => t.kind === "blossom"
3817
+ );
3818
+ if (member === void 0) {
3819
+ throw new Error("Attachment has no blossom transport.");
3820
+ }
3821
+ const transport = createBlossomTransport({ blossom, identity });
3822
+ const bytes = await transport.fetchToBytes({
3823
+ transport: member,
3824
+ senderPubkey: providerPubkey,
3825
+ maxBytes
3826
+ });
3827
+ return { bytes, name: attachment.name, mime: attachment.mime };
3828
+ }
3829
+
3483
3830
  // src/services/jobErrors.ts
3484
3831
  var AGENT_UNAVAILABLE_MARKERS = [
3485
3832
  "agent temporarily unavailable",
@@ -4051,6 +4398,6 @@ function makeCensor() {
4051
4398
  };
4052
4399
  }
4053
4400
 
4054
- export { BlossomService, BoundedSet, DEFAULTS, DEFAULT_KIND_OFFSET, DEFAULT_REDACT_PATHS, DiscoveryService, ELISYM_PROTOCOL_TAG, ENVELOPE_VERSION, ElisymClient, ElisymIdentity, GlobalConfigSchema, INPUT_REDACT_PATHS, JobWaitTimeoutError, KIND_APP_HANDLER, KIND_JOB_FEEDBACK, KIND_JOB_REQUEST, KIND_JOB_REQUEST_BASE, KIND_JOB_RESULT, KIND_JOB_RESULT_BASE, KIND_LONG_FORM_ARTICLE, KIND_PING, KIND_PONG, KNOWN_ASSETS, LAMPORTS_PER_SOL, LIMITS, MarketplaceService, MediaService, NATIVE_SOL, NostrPool, POLICY_D_TAG_PREFIX, POLICY_TYPE_REGEX, POLICY_T_TAG, PROTOCOL_PROGRAM_ID_DEVNET, PaymentRequestSchema, PingService, PoliciesService, RELAYS, SECRET_REDACT_PATHS, SessionSpendLimitEntrySchema, SolanaPaymentStrategy, USDC_SOLANA_DEVNET, aggregateNetworkStats, assertExpiry, assertLamports, assetByKey, assetKey, buildPaymentInstructions, calculateProtocolFee, classifyJobError, clearPriorityFeeCache, clearProtocolConfigCache, clearQuickVerifyCache, compareAgentsByRank, computeRankKey, createPaymentRequestWithOnchainConfig, createSlidingWindowLimiter, decodeJobPayload, encodeJobPayload, estimateNetworkBaseline, estimatePriorityFeeMicroLamports, estimateSolFeeLamports, formatAssetAmount, formatFeeBreakdown, formatNetworkBaseline, formatSol, getNetworkStats, getProtocolConfig, getProtocolProgramId, jobRequestKind, jobResultKind, makeCensor, nip44Decrypt, nip44Encrypt, parseAssetAmount, parsePaymentRequest, pickPercentileFee, resolveAssetFromPaymentRequest, resolveKnownAsset, timeAgo, toDTag, truncateKey, utf8ByteLength, validateAgentName, validateExpiry, verifyJobPaymentQuick };
4401
+ export { ACCEPT_TRANSPORTS_TAG, BlossomService, BoundedSet, DEFAULTS, DEFAULT_KIND_OFFSET, DEFAULT_REDACT_PATHS, DiscoveryService, ELISYM_PROTOCOL_TAG, ENVELOPE_VERSION, ElisymClient, ElisymIdentity, GlobalConfigSchema, INPUT_REDACT_PATHS, JobWaitTimeoutError, KIND_APP_HANDLER, KIND_JOB_FEEDBACK, KIND_JOB_REQUEST, KIND_JOB_REQUEST_BASE, KIND_JOB_RESULT, KIND_JOB_RESULT_BASE, KIND_LONG_FORM_ARTICLE, KIND_PING, KIND_PONG, KNOWN_ASSETS, LAMPORTS_PER_SOL, LIMITS, MarketplaceService, MediaService, NATIVE_SOL, NostrPool, POLICY_D_TAG_PREFIX, POLICY_TYPE_REGEX, POLICY_T_TAG, PROTOCOL_PROGRAM_ID_DEVNET, PaymentRequestSchema, PingService, PoliciesService, RELAYS, SECRET_REDACT_PATHS, SessionSpendLimitEntrySchema, SolanaPaymentStrategy, USDC_SOLANA_DEVNET, aggregateNetworkStats, assertExpiry, assertLamports, assetByKey, assetKey, attachmentsOf, buildAcceptTransportsTag, buildEncryptedFileInput, buildPaymentInstructions, calculateProtocolFee, classifyJobError, clearPriorityFeeCache, clearProtocolConfigCache, clearQuickVerifyCache, compareAgentsByRank, computeRankKey, createBlossomTransport, createPaymentRequestWithOnchainConfig, createSlidingWindowLimiter, decodeJobPayload, decryptBytesFromSender, encodeJobPayload, encryptBytesForRecipient, estimateNetworkBaseline, estimatePriorityFeeMicroLamports, estimateSolFeeLamports, fetchEncryptedFileOutput, formatAssetAmount, formatFeeBreakdown, formatNetworkBaseline, formatSol, getNetworkStats, getProtocolConfig, getProtocolProgramId, jobRequestKind, jobResultKind, makeCensor, nip44Decrypt, nip44Encrypt, parseAssetAmount, parsePaymentRequest, pickPercentileFee, prepareEncryptedFileInput, readAcceptedTransports, resolveAssetFromPaymentRequest, resolveKnownAsset, timeAgo, toDTag, truncateKey, utf8ByteLength, validateAgentName, validateExpiry, verifyJobPaymentQuick };
4055
4402
  //# sourceMappingURL=index.js.map
4056
4403
  //# sourceMappingURL=index.js.map