@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.cjs CHANGED
@@ -98,9 +98,16 @@ var DEFAULTS = {
98
98
  // default, not a protocol constant - the transfer is resumable and its own
99
99
  // budget, decoupled from the result-wait window.
100
100
  IROH_FETCH_TIMEOUT_MS: 3e5,
101
+ // Ceiling for a single iroh SEED (addFromPath/addBytes/share). Seeding is local
102
+ // (hash + store-copy + ticket mint), so this is a generous backstop: it bounds
103
+ // the JS await so a wedged native call surfaces as a thrown error (and triggers a
104
+ // node reset) instead of an indefinite hang that stalls file delivery.
105
+ IROH_SEED_TIMEOUT_MS: 12e4,
101
106
  // Ceiling for a single Blossom blob upload (PUT /upload). Large blobs (up to
102
107
  // LIMITS.MAX_FILE_SIZE) need far more than the 30s used for small media images.
103
- BLOSSOM_UPLOAD_TIMEOUT_MS: 3e5
108
+ BLOSSOM_UPLOAD_TIMEOUT_MS: 3e5,
109
+ // Ceiling for a single encrypted Blossom blob download (GET). Same budget as upload.
110
+ BLOSSOM_FETCH_TIMEOUT_MS: 3e5
104
111
  };
105
112
  var LIMITS = {
106
113
  MAX_INPUT_LENGTH: 1e5,
@@ -125,6 +132,12 @@ var LIMITS = {
125
132
  // providers may lower it per deployment.
126
133
  MAX_FILE_SIZE: 1073741824,
127
134
  // 1 GiB
135
+ // Cap for the ENCRYPTED Blossom path (web/SDK). The encrypt-then-upload flow is
136
+ // whole-buffer in WebCrypto + BlossomService (~3x file-size peak RAM), so this is
137
+ // deliberately far below MAX_FILE_SIZE to stay safe in a browser tab; larger files
138
+ // use iroh. The relay enforces a ~128 MiB server-side backstop.
139
+ MAX_BLOSSOM_ENCRYPTED_BYTES: 104857600,
140
+ // 100 MiB
128
141
  MAX_TIMEOUT_SECS: 600,
129
142
  // Upper bound for execution budgets (`max_execution_secs` / `execution_timeout_secs`).
130
143
  // Distinct from MAX_TIMEOUT_SECS (the result-wait cap): execution budgets may be
@@ -1211,6 +1224,17 @@ var BlossomService = class {
1211
1224
  this.serverUrl = serverUrl;
1212
1225
  this.fallback = fallback;
1213
1226
  }
1227
+ /**
1228
+ * The content-addressed GET URL for a blob, derivable from its sha256 BEFORE
1229
+ * upload (BUD-01: `<serverUrl>/<sha256>`, no extension for our octet-stream
1230
+ * ciphertext uploads - same form `delete` addresses by). Lets a caller build a
1231
+ * complete attachment descriptor and defer the actual byte upload (the descriptor
1232
+ * is submitted first, the bytes PUT later). `upload()` re-verifies the server
1233
+ * returns this exact url.
1234
+ */
1235
+ contentUrl(sha256) {
1236
+ return `${this.serverUrl}/${sha256}`;
1237
+ }
1214
1238
  /**
1215
1239
  * Upload a file to the Blossom server, returning its descriptor. On any failure, falls
1216
1240
  * back to the configured uploader (if any) and returns a normalized descriptor with
@@ -1262,6 +1286,62 @@ var BlossomService = class {
1262
1286
  clearTimeout(timer);
1263
1287
  }
1264
1288
  }
1289
+ /**
1290
+ * Download a public blob (BUD-01 GET, no auth). Bounds memory on the ACTUAL streamed bytes (never
1291
+ * the declared Content-Length) and verifies the sha256 when `expectedSha256` is given. Browser-safe.
1292
+ */
1293
+ async download(url, opts = {}) {
1294
+ const maxBytes = opts.maxBytes ?? LIMITS.MAX_FILE_SIZE;
1295
+ const controller = new AbortController();
1296
+ const timer = setTimeout(
1297
+ () => controller.abort(),
1298
+ opts.timeoutMs ?? DEFAULTS.BLOSSOM_FETCH_TIMEOUT_MS
1299
+ );
1300
+ try {
1301
+ const res = await fetch(url, { signal: controller.signal });
1302
+ if (!res.ok) {
1303
+ throw new Error(`Download failed: ${res.status} ${res.statusText}`);
1304
+ }
1305
+ const declared = Number(res.headers.get("content-length"));
1306
+ if (Number.isFinite(declared) && declared > maxBytes) {
1307
+ throw new Error(`Blob too large: ${declared} bytes exceeds limit of ${maxBytes}.`);
1308
+ }
1309
+ if (!res.body) {
1310
+ throw new Error("Download response has no body.");
1311
+ }
1312
+ const reader = res.body.getReader();
1313
+ const chunks = [];
1314
+ let total = 0;
1315
+ let chunk = await reader.read();
1316
+ while (!chunk.done) {
1317
+ total += chunk.value.byteLength;
1318
+ if (total > maxBytes) {
1319
+ await reader.cancel();
1320
+ throw new Error(`Blob exceeds limit of ${maxBytes} bytes.`);
1321
+ }
1322
+ chunks.push(chunk.value);
1323
+ chunk = await reader.read();
1324
+ }
1325
+ const bytes = new Uint8Array(total);
1326
+ let offset = 0;
1327
+ for (const c of chunks) {
1328
+ bytes.set(c, offset);
1329
+ offset += c.byteLength;
1330
+ }
1331
+ if (opts.expectedSha256 !== void 0) {
1332
+ const digest = await crypto.subtle.digest("SHA-256", bytes);
1333
+ const hashHex = [...new Uint8Array(digest)].map((b) => b.toString(16).padStart(2, "0")).join("");
1334
+ if (hashHex !== opts.expectedSha256) {
1335
+ throw new Error(
1336
+ `Download integrity check failed: got ${hashHex}, expected ${opts.expectedSha256}.`
1337
+ );
1338
+ }
1339
+ }
1340
+ return bytes;
1341
+ } finally {
1342
+ clearTimeout(timer);
1343
+ }
1344
+ }
1265
1345
  async uploadToBlossom(identity, bytes, hashHex, mime) {
1266
1346
  const contentType = mime || "application/octet-stream";
1267
1347
  const authHeader = this.authHeader(identity, "upload", hashHex);
@@ -1393,6 +1473,9 @@ function parseCapabilityEvent(event, network) {
1393
1473
  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)) {
1394
1474
  return null;
1395
1475
  }
1476
+ if (card.inputText !== void 0 && !["required", "optional", "none"].includes(card.inputText)) {
1477
+ card.inputText = void 0;
1478
+ }
1396
1479
  if (card.payment?.job_price !== null && card.payment?.job_price !== void 0 && (!Number.isInteger(card.payment.job_price) || card.payment.job_price < 0)) {
1397
1480
  return null;
1398
1481
  }
@@ -1977,6 +2060,26 @@ var FileTransportSchema = zod.z.discriminatedUnion("kind", [
1977
2060
  kind: zod.z.literal("iroh"),
1978
2061
  /** Opaque iroh `BlobTicket` string. Parsed into a real ticket only at fetch time. */
1979
2062
  ticket: zod.z.string().min(1).max(MAX_TICKET_LENGTH)
2063
+ }),
2064
+ zod.z.object({
2065
+ kind: zod.z.literal("blossom"),
2066
+ /** Public HTTP(S) URL of the CIPHERTEXT blob on a Blossom relay. */
2067
+ url: zod.z.string().url().max(2048),
2068
+ /** sha256 (lowercase hex) of the ciphertext - what the relay stores and addresses. */
2069
+ sha256: zod.z.string().regex(/^[0-9a-f]{64}$/),
2070
+ /**
2071
+ * Hybrid-encryption parameters. The file bytes are AES-256-GCM encrypted with a random
2072
+ * content key; that key is NIP-44-wrapped to the recipient. `name`/`mime`/`size` on the
2073
+ * attachment describe the PLAINTEXT and live only inside the (encrypted) envelope - never
2074
+ * sent to the relay (the relay only ever sees opaque ciphertext).
2075
+ */
2076
+ enc: zod.z.object({
2077
+ alg: zod.z.literal("AES-256-GCM"),
2078
+ /** base64 12-byte GCM IV (non-secret). */
2079
+ iv: zod.z.string().min(1).max(64),
2080
+ /** NIP-44-wrapped content key. */
2081
+ key: zod.z.string().min(1).max(2048)
2082
+ })
1980
2083
  })
1981
2084
  ]);
1982
2085
  var FileAttachmentSchema = zod.z.object({
@@ -1985,22 +2088,76 @@ var FileAttachmentSchema = zod.z.object({
1985
2088
  /** Declared size in bytes (display/hint only; enforcement is on actual streamed bytes). */
1986
2089
  size: zod.z.number().int().nonnegative(),
1987
2090
  mime: zod.z.string().min(1).max(255),
1988
- /** Ordered by sender preference; at least one. */
1989
- transports: zod.z.array(FileTransportSchema).min(1),
2091
+ /**
2092
+ * Ordered by sender preference; at least one KNOWN transport. Parsed leniently: unknown
2093
+ * transport `kind`s are dropped (not rejected) so adding a new transport never makes an older
2094
+ * decoder throw away the whole envelope - it just ignores the kinds it doesn't know and uses
2095
+ * the ones it does. At least one known transport must survive, else the attachment is invalid.
2096
+ */
2097
+ transports: zod.z.array(zod.z.unknown()).transform(
2098
+ (arr) => arr.flatMap((t) => {
2099
+ const parsed = FileTransportSchema.safeParse(t);
2100
+ return parsed.success ? [parsed.data] : [];
2101
+ })
2102
+ ).refine((arr) => arr.length >= 1, { message: "attachment has no known transport" }),
1990
2103
  /** Optional provider hint (unix seconds) for when seeding may stop. */
1991
2104
  seedingExpiresAt: zod.z.number().int().nonnegative().optional()
1992
2105
  });
1993
2106
  var JobPayloadEnvelopeSchema = zod.z.object({
1994
2107
  v: zod.z.literal(ENVELOPE_VERSION),
1995
2108
  text: zod.z.string().optional(),
1996
- attachment: FileAttachmentSchema.optional()
2109
+ // Legacy single attachment - kept (and mirrored from `attachments[0]`) so an old
2110
+ // decoder that doesn't know `attachments` still gets the first file.
2111
+ attachment: FileAttachmentSchema.optional(),
2112
+ // Multiple result/input files. Additive; old decoders strip this unknown key.
2113
+ attachments: zod.z.array(FileAttachmentSchema).optional()
1997
2114
  });
2115
+ var ACCEPT_TRANSPORTS_TAG = "accept";
2116
+ var KNOWN_TRANSPORT_KINDS = ["iroh", "blossom"];
2117
+ function isKnownTransportKind(value) {
2118
+ return KNOWN_TRANSPORT_KINDS.includes(value);
2119
+ }
2120
+ function buildAcceptTransportsTag(kinds) {
2121
+ const seen = /* @__PURE__ */ new Set();
2122
+ const out = [ACCEPT_TRANSPORTS_TAG];
2123
+ for (const kind of kinds) {
2124
+ if (isKnownTransportKind(kind) && !seen.has(kind)) {
2125
+ seen.add(kind);
2126
+ out.push(kind);
2127
+ }
2128
+ }
2129
+ return out;
2130
+ }
2131
+ function readAcceptedTransports(tags) {
2132
+ const tag = tags.find((t) => t[0] === ACCEPT_TRANSPORTS_TAG);
2133
+ if (tag === void 0) {
2134
+ return void 0;
2135
+ }
2136
+ const seen = /* @__PURE__ */ new Set();
2137
+ const out = [];
2138
+ for (const value of tag.slice(1)) {
2139
+ if (isKnownTransportKind(value) && !seen.has(value)) {
2140
+ seen.add(value);
2141
+ out.push(value);
2142
+ }
2143
+ }
2144
+ return out.length > 0 ? out : void 0;
2145
+ }
2146
+ function attachmentsOf(decoded) {
2147
+ if (decoded.attachments !== void 0 && decoded.attachments.length > 0) {
2148
+ return decoded.attachments;
2149
+ }
2150
+ return decoded.attachment !== void 0 ? [decoded.attachment] : [];
2151
+ }
1998
2152
  function encodeJobPayload(payload) {
1999
2153
  const envelope = { v: ENVELOPE_VERSION };
2000
2154
  if (payload.text !== void 0) {
2001
2155
  envelope.text = payload.text;
2002
2156
  }
2003
- if (payload.attachment !== void 0) {
2157
+ if (payload.attachments !== void 0 && payload.attachments.length > 0) {
2158
+ envelope.attachments = payload.attachments;
2159
+ envelope.attachment = payload.attachments[0];
2160
+ } else if (payload.attachment !== void 0) {
2004
2161
  envelope.attachment = payload.attachment;
2005
2162
  }
2006
2163
  return JSON.stringify(envelope);
@@ -2028,7 +2185,11 @@ function decodeJobPayload(content) {
2028
2185
  `Invalid elisym job payload (v=${JSON.stringify(version)}): ${result.error.message}`
2029
2186
  );
2030
2187
  }
2031
- return { text: result.data.text, attachment: result.data.attachment };
2188
+ return {
2189
+ text: result.data.text,
2190
+ attachment: result.data.attachment,
2191
+ attachments: result.data.attachments
2192
+ };
2032
2193
  }
2033
2194
 
2034
2195
  // src/services/marketplace.ts
@@ -2097,6 +2258,12 @@ var MarketplaceService = class {
2097
2258
  tags.push(["p", options.providerPubkey]);
2098
2259
  tags.push(["encrypted", "nip44"]);
2099
2260
  }
2261
+ if (options.acceptTransports && options.acceptTransports.length > 0) {
2262
+ const acceptTag = buildAcceptTransportsTag(options.acceptTransports);
2263
+ if (acceptTag.length > 1) {
2264
+ tags.push(acceptTag);
2265
+ }
2266
+ }
2100
2267
  const kind = jobRequestKind(options.kindOffset ?? DEFAULT_KIND_OFFSET);
2101
2268
  const event = nostrTools.finalizeEvent(
2102
2269
  {
@@ -2187,7 +2354,7 @@ var MarketplaceService = class {
2187
2354
  }
2188
2355
  resultDelivered = true;
2189
2356
  try {
2190
- cb.onResult?.(decoded.text ?? "", ev.id, decoded.attachment);
2357
+ cb.onResult?.(decoded.text ?? "", ev.id, decoded.attachment, attachmentsOf(decoded));
2191
2358
  } catch {
2192
2359
  } finally {
2193
2360
  done();
@@ -2351,8 +2518,8 @@ var MarketplaceService = class {
2351
2518
  );
2352
2519
  }
2353
2520
  /** Submit a job result with NIP-44 encrypted content. Result kind is derived from the request kind. */
2354
- async submitJobResult(identity, requestEvent, content, amount, attachment) {
2355
- const hasAttachment = attachment !== void 0;
2521
+ async submitJobResult(identity, requestEvent, content, amount, attachments) {
2522
+ const hasAttachment = attachments !== void 0 && attachments.length > 0;
2356
2523
  if (!content && !hasAttachment) {
2357
2524
  throw new Error("Job result content must not be empty.");
2358
2525
  }
@@ -2366,7 +2533,7 @@ var MarketplaceService = class {
2366
2533
  );
2367
2534
  }
2368
2535
  const shouldEncrypt = isEncrypted(requestEvent);
2369
- const payload = hasAttachment ? encodeJobPayload({ text: content || void 0, attachment }) : content;
2536
+ const payload = hasAttachment ? encodeJobPayload({ text: content || void 0, attachments }) : content;
2370
2537
  if (shouldEncrypt) {
2371
2538
  const payloadBytes = utf8ByteLength(payload);
2372
2539
  if (payloadBytes > LIMITS.NIP44_MAX_PLAINTEXT_BYTES) {
@@ -2407,11 +2574,11 @@ var MarketplaceService = class {
2407
2574
  * With maxAttempts=3: try, ~1s, try, ~2s, try, throw.
2408
2575
  * Jitter: 0.5x-1.0x of calculated delay.
2409
2576
  */
2410
- async submitJobResultWithRetry(identity, requestEvent, content, amount, maxAttempts = DEFAULTS.RESULT_RETRY_COUNT, baseDelayMs = DEFAULTS.RESULT_RETRY_BASE_MS, attachment) {
2577
+ async submitJobResultWithRetry(identity, requestEvent, content, amount, maxAttempts = DEFAULTS.RESULT_RETRY_COUNT, baseDelayMs = DEFAULTS.RESULT_RETRY_BASE_MS, attachments) {
2411
2578
  const attempts = Math.max(1, maxAttempts);
2412
2579
  for (let attempt = 0; attempt < attempts; attempt++) {
2413
2580
  try {
2414
- return await this.submitJobResult(identity, requestEvent, content, amount, attachment);
2581
+ return await this.submitJobResult(identity, requestEvent, content, amount, attachments);
2415
2582
  } catch (e) {
2416
2583
  if (attempt >= attempts - 1) {
2417
2584
  throw e;
@@ -3505,6 +3672,186 @@ var ElisymClient = class {
3505
3672
  }
3506
3673
  };
3507
3674
 
3675
+ // src/primitives/file-crypto.ts
3676
+ var KEY_BYTES = 32;
3677
+ var IV_BYTES = 12;
3678
+ function bytesToBase64(bytes) {
3679
+ let bin = "";
3680
+ for (const b of bytes) {
3681
+ bin += String.fromCharCode(b);
3682
+ }
3683
+ return btoa(bin);
3684
+ }
3685
+ function base64ToBytes(b64) {
3686
+ const bin = atob(b64);
3687
+ const out = new Uint8Array(bin.length);
3688
+ for (let i = 0; i < bin.length; i += 1) {
3689
+ out[i] = bin.charCodeAt(i);
3690
+ }
3691
+ return out;
3692
+ }
3693
+ async function encryptBytesForRecipient(bytes, senderSk, recipientPubkey) {
3694
+ const rawKey = crypto.getRandomValues(new Uint8Array(KEY_BYTES));
3695
+ const iv = crypto.getRandomValues(new Uint8Array(IV_BYTES));
3696
+ const key = await crypto.subtle.importKey("raw", rawKey, "AES-GCM", false, ["encrypt"]);
3697
+ const ct = await crypto.subtle.encrypt({ name: "AES-GCM", iv, tagLength: 128 }, key, bytes);
3698
+ const wrappedKey = nip44Encrypt(bytesToBase64(rawKey), senderSk, recipientPubkey);
3699
+ return { ciphertext: new Uint8Array(ct), wrappedKey, iv: bytesToBase64(iv) };
3700
+ }
3701
+ async function decryptBytesFromSender(ciphertext, wrappedKey, iv, receiverSk, senderPubkey) {
3702
+ const rawKey = base64ToBytes(nip44Decrypt(wrappedKey, receiverSk, senderPubkey));
3703
+ const key = await crypto.subtle.importKey("raw", rawKey, "AES-GCM", false, ["decrypt"]);
3704
+ const pt = await crypto.subtle.decrypt(
3705
+ { name: "AES-GCM", iv: base64ToBytes(iv), tagLength: 128 },
3706
+ key,
3707
+ ciphertext
3708
+ );
3709
+ return new Uint8Array(pt);
3710
+ }
3711
+
3712
+ // src/transport/blossom-transport.ts
3713
+ var AES_GCM_TAG_BYTES = 16;
3714
+ function createBlossomTransport(opts) {
3715
+ const { blossom, identity } = opts;
3716
+ return {
3717
+ async seedBytes({ bytes, recipientPubkey }) {
3718
+ if (bytes.byteLength > LIMITS.MAX_BLOSSOM_ENCRYPTED_BYTES) {
3719
+ throw new Error(
3720
+ `File too large for encrypted Blossom: ${bytes.byteLength} bytes exceeds ${LIMITS.MAX_BLOSSOM_ENCRYPTED_BYTES}.`
3721
+ );
3722
+ }
3723
+ const { ciphertext, wrappedKey, iv } = await encryptBytesForRecipient(
3724
+ bytes,
3725
+ identity.secretKey,
3726
+ recipientPubkey
3727
+ );
3728
+ const blob = new Blob([ciphertext], { type: "application/octet-stream" });
3729
+ const descriptor = await blossom.upload(identity, blob);
3730
+ if (descriptor.provider !== "blossom") {
3731
+ throw new Error("Blossom upload fell back to a non-content-addressed provider.");
3732
+ }
3733
+ return {
3734
+ kind: "blossom",
3735
+ url: descriptor.url,
3736
+ sha256: descriptor.sha256,
3737
+ enc: { alg: "AES-256-GCM", iv, key: wrappedKey }
3738
+ };
3739
+ },
3740
+ async fetchToBytes({ transport, senderPubkey, maxBytes }) {
3741
+ const plaintextCap = maxBytes ?? LIMITS.MAX_BLOSSOM_ENCRYPTED_BYTES;
3742
+ const ciphertext = await blossom.download(transport.url, {
3743
+ maxBytes: plaintextCap + AES_GCM_TAG_BYTES,
3744
+ expectedSha256: transport.sha256
3745
+ });
3746
+ return decryptBytesFromSender(
3747
+ ciphertext,
3748
+ transport.enc.key,
3749
+ transport.enc.iv,
3750
+ identity.secretKey,
3751
+ senderPubkey
3752
+ );
3753
+ }
3754
+ };
3755
+ }
3756
+
3757
+ // src/transport/file-jobs.ts
3758
+ async function sha256Hex(bytes) {
3759
+ const digest = await crypto.subtle.digest("SHA-256", bytes);
3760
+ return [...new Uint8Array(digest)].map((b) => b.toString(16).padStart(2, "0")).join("");
3761
+ }
3762
+ var EXECUTABLE_EXTENSIONS = /* @__PURE__ */ new Set([
3763
+ ".exe",
3764
+ ".dll",
3765
+ ".bat",
3766
+ ".cmd",
3767
+ ".com",
3768
+ ".msi",
3769
+ ".sh",
3770
+ ".app",
3771
+ ".scr",
3772
+ ".ps1"
3773
+ ]);
3774
+ var EXECUTABLE_MIMES = /* @__PURE__ */ new Set([
3775
+ "application/x-msdownload",
3776
+ "application/x-msdos-program",
3777
+ "application/x-sh",
3778
+ "application/x-executable",
3779
+ "application/vnd.microsoft.portable-executable",
3780
+ "application/x-mach-binary"
3781
+ ]);
3782
+ function looksExecutable(name, type) {
3783
+ const dot = name.lastIndexOf(".");
3784
+ const ext = dot >= 0 ? name.slice(dot).toLowerCase() : "";
3785
+ return EXECUTABLE_EXTENSIONS.has(ext) || EXECUTABLE_MIMES.has(type);
3786
+ }
3787
+ async function prepareEncryptedFileInput(args) {
3788
+ const { file, providerPubkey, identity, blossom } = args;
3789
+ const name = file.name ?? "upload";
3790
+ if (looksExecutable(name, file.type)) {
3791
+ throw new Error("Refusing to upload an executable file.");
3792
+ }
3793
+ const bytes = new Uint8Array(await file.arrayBuffer());
3794
+ if (bytes.byteLength > LIMITS.MAX_BLOSSOM_ENCRYPTED_BYTES) {
3795
+ throw new Error(
3796
+ `File too large for the encrypted-Blossom transport: ${bytes.byteLength} bytes (max ${LIMITS.MAX_BLOSSOM_ENCRYPTED_BYTES}).`
3797
+ );
3798
+ }
3799
+ const { ciphertext, wrappedKey, iv } = await encryptBytesForRecipient(
3800
+ bytes,
3801
+ identity.secretKey,
3802
+ providerPubkey
3803
+ );
3804
+ const sha256 = await sha256Hex(ciphertext);
3805
+ const member = {
3806
+ kind: "blossom",
3807
+ url: blossom.contentUrl(sha256),
3808
+ sha256,
3809
+ enc: { alg: "AES-256-GCM", iv, key: wrappedKey }
3810
+ };
3811
+ const attachment = {
3812
+ name,
3813
+ size: bytes.byteLength,
3814
+ mime: file.type || "application/octet-stream",
3815
+ transports: [member]
3816
+ };
3817
+ const upload = async () => {
3818
+ const descriptor = await blossom.upload(
3819
+ identity,
3820
+ new Blob([ciphertext], { type: "application/octet-stream" })
3821
+ );
3822
+ if (descriptor.provider !== "blossom") {
3823
+ throw new Error("Blossom upload fell back to a non-content-addressed provider.");
3824
+ }
3825
+ if (descriptor.sha256 !== sha256 || descriptor.url !== member.url) {
3826
+ throw new Error(
3827
+ `Blossom upload descriptor mismatch (expected ${member.url} / ${sha256}, got ${descriptor.url} / ${descriptor.sha256}).`
3828
+ );
3829
+ }
3830
+ };
3831
+ return { attachment, upload };
3832
+ }
3833
+ async function buildEncryptedFileInput(args) {
3834
+ const prepared = await prepareEncryptedFileInput(args);
3835
+ await prepared.upload();
3836
+ return prepared.attachment;
3837
+ }
3838
+ async function fetchEncryptedFileOutput(args) {
3839
+ const { attachment, providerPubkey, identity, blossom, maxBytes } = args;
3840
+ const member = attachment.transports.find(
3841
+ (t) => t.kind === "blossom"
3842
+ );
3843
+ if (member === void 0) {
3844
+ throw new Error("Attachment has no blossom transport.");
3845
+ }
3846
+ const transport = createBlossomTransport({ blossom, identity });
3847
+ const bytes = await transport.fetchToBytes({
3848
+ transport: member,
3849
+ senderPubkey: providerPubkey,
3850
+ maxBytes
3851
+ });
3852
+ return { bytes, name: attachment.name, mime: attachment.mime };
3853
+ }
3854
+
3508
3855
  // src/services/jobErrors.ts
3509
3856
  var AGENT_UNAVAILABLE_MARKERS = [
3510
3857
  "agent temporarily unavailable",
@@ -4076,6 +4423,7 @@ function makeCensor() {
4076
4423
  };
4077
4424
  }
4078
4425
 
4426
+ exports.ACCEPT_TRANSPORTS_TAG = ACCEPT_TRANSPORTS_TAG;
4079
4427
  exports.BlossomService = BlossomService;
4080
4428
  exports.BoundedSet = BoundedSet;
4081
4429
  exports.DEFAULTS = DEFAULTS;
@@ -4122,6 +4470,9 @@ exports.assertExpiry = assertExpiry;
4122
4470
  exports.assertLamports = assertLamports;
4123
4471
  exports.assetByKey = assetByKey;
4124
4472
  exports.assetKey = assetKey;
4473
+ exports.attachmentsOf = attachmentsOf;
4474
+ exports.buildAcceptTransportsTag = buildAcceptTransportsTag;
4475
+ exports.buildEncryptedFileInput = buildEncryptedFileInput;
4125
4476
  exports.buildPaymentInstructions = buildPaymentInstructions;
4126
4477
  exports.calculateProtocolFee = calculateProtocolFee;
4127
4478
  exports.classifyJobError = classifyJobError;
@@ -4130,13 +4481,17 @@ exports.clearProtocolConfigCache = clearProtocolConfigCache;
4130
4481
  exports.clearQuickVerifyCache = clearQuickVerifyCache;
4131
4482
  exports.compareAgentsByRank = compareAgentsByRank;
4132
4483
  exports.computeRankKey = computeRankKey;
4484
+ exports.createBlossomTransport = createBlossomTransport;
4133
4485
  exports.createPaymentRequestWithOnchainConfig = createPaymentRequestWithOnchainConfig;
4134
4486
  exports.createSlidingWindowLimiter = createSlidingWindowLimiter;
4135
4487
  exports.decodeJobPayload = decodeJobPayload;
4488
+ exports.decryptBytesFromSender = decryptBytesFromSender;
4136
4489
  exports.encodeJobPayload = encodeJobPayload;
4490
+ exports.encryptBytesForRecipient = encryptBytesForRecipient;
4137
4491
  exports.estimateNetworkBaseline = estimateNetworkBaseline;
4138
4492
  exports.estimatePriorityFeeMicroLamports = estimatePriorityFeeMicroLamports;
4139
4493
  exports.estimateSolFeeLamports = estimateSolFeeLamports;
4494
+ exports.fetchEncryptedFileOutput = fetchEncryptedFileOutput;
4140
4495
  exports.formatAssetAmount = formatAssetAmount;
4141
4496
  exports.formatFeeBreakdown = formatFeeBreakdown;
4142
4497
  exports.formatNetworkBaseline = formatNetworkBaseline;
@@ -4152,6 +4507,8 @@ exports.nip44Encrypt = nip44Encrypt;
4152
4507
  exports.parseAssetAmount = parseAssetAmount;
4153
4508
  exports.parsePaymentRequest = parsePaymentRequest;
4154
4509
  exports.pickPercentileFee = pickPercentileFee;
4510
+ exports.prepareEncryptedFileInput = prepareEncryptedFileInput;
4511
+ exports.readAcceptedTransports = readAcceptedTransports;
4155
4512
  exports.resolveAssetFromPaymentRequest = resolveAssetFromPaymentRequest;
4156
4513
  exports.resolveKnownAsset = resolveKnownAsset;
4157
4514
  exports.timeAgo = timeAgo;