@elisym/sdk 0.25.1 → 0.25.4

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,6 +73,11 @@ 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
83
  BLOSSOM_UPLOAD_TIMEOUT_MS: 3e5,
@@ -1194,6 +1199,17 @@ var BlossomService = class {
1194
1199
  this.serverUrl = serverUrl;
1195
1200
  this.fallback = fallback;
1196
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
+ }
1197
1213
  /**
1198
1214
  * Upload a file to the Blossom server, returning its descriptor. On any failure, falls
1199
1215
  * back to the configured uploader (if any) and returns a normalized descriptor with
@@ -1246,18 +1262,38 @@ var BlossomService = class {
1246
1262
  }
1247
1263
  }
1248
1264
  /**
1249
- * Download a public blob (BUD-01 GET, no auth). Bounds memory on the ACTUAL streamed bytes (never
1250
- * the declared Content-Length) and verifies the sha256 when `expectedSha256` is given. Browser-safe.
1265
+ * Download a content-addressed blob from THIS Blossom server (BUD-01 GET, no auth). Bounds memory
1266
+ * on the ACTUAL streamed bytes (never the declared Content-Length) and verifies the sha256 when
1267
+ * `expectedSha256` is given. Browser-safe.
1268
+ *
1269
+ * SSRF guard: `url` typically arrives inside a remote counterparty's encrypted job envelope, so it
1270
+ * is untrusted. elisym blobs are content-addressed on the single configured server (`seedBytes`
1271
+ * refuses non-content-addressed fallbacks), so a legitimate URL is always `<serverUrl>/<sha256>`.
1272
+ * The origin is pinned to `serverUrl` and redirects are refused, so a crafted url (or a 30x from
1273
+ * the host) can't coerce a fetch to loopback, cloud-metadata, or internal addresses. Federation
1274
+ * across Blossom servers would replace this single-origin pin with an explicit allowlist.
1251
1275
  */
1252
1276
  async download(url, opts = {}) {
1277
+ if (new URL(url).origin !== new URL(this.serverUrl).origin) {
1278
+ throw new Error(`Refusing to download from a non-Blossom origin: ${url}`);
1279
+ }
1253
1280
  const maxBytes = opts.maxBytes ?? LIMITS.MAX_FILE_SIZE;
1254
1281
  const controller = new AbortController();
1255
1282
  const timer = setTimeout(
1256
1283
  () => controller.abort(),
1257
1284
  opts.timeoutMs ?? DEFAULTS.BLOSSOM_FETCH_TIMEOUT_MS
1258
1285
  );
1286
+ const externalSignal = opts.signal;
1287
+ const onExternalAbort = () => controller.abort();
1288
+ if (externalSignal !== void 0) {
1289
+ if (externalSignal.aborted) {
1290
+ controller.abort();
1291
+ } else {
1292
+ externalSignal.addEventListener("abort", onExternalAbort, { once: true });
1293
+ }
1294
+ }
1259
1295
  try {
1260
- const res = await fetch(url, { signal: controller.signal });
1296
+ const res = await fetch(url, { signal: controller.signal, redirect: "error" });
1261
1297
  if (!res.ok) {
1262
1298
  throw new Error(`Download failed: ${res.status} ${res.statusText}`);
1263
1299
  }
@@ -1299,6 +1335,9 @@ var BlossomService = class {
1299
1335
  return bytes;
1300
1336
  } finally {
1301
1337
  clearTimeout(timer);
1338
+ if (externalSignal !== void 0) {
1339
+ externalSignal.removeEventListener("abort", onExternalAbort);
1340
+ }
1302
1341
  }
1303
1342
  }
1304
1343
  async uploadToBlossom(identity, bytes, hashHex, mime) {
@@ -1432,6 +1471,9 @@ function parseCapabilityEvent(event, network) {
1432
1471
  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)) {
1433
1472
  return null;
1434
1473
  }
1474
+ if (card.inputText !== void 0 && !["required", "optional", "none"].includes(card.inputText)) {
1475
+ card.inputText = void 0;
1476
+ }
1435
1477
  if (card.payment?.job_price !== null && card.payment?.job_price !== void 0 && (!Number.isInteger(card.payment.job_price) || card.payment.job_price < 0)) {
1436
1478
  return null;
1437
1479
  }
@@ -2062,7 +2104,11 @@ var FileAttachmentSchema = z.object({
2062
2104
  var JobPayloadEnvelopeSchema = z.object({
2063
2105
  v: z.literal(ENVELOPE_VERSION),
2064
2106
  text: z.string().optional(),
2065
- attachment: FileAttachmentSchema.optional()
2107
+ // Legacy single attachment - kept (and mirrored from `attachments[0]`) so an old
2108
+ // decoder that doesn't know `attachments` still gets the first file.
2109
+ attachment: FileAttachmentSchema.optional(),
2110
+ // Multiple result/input files. Additive; old decoders strip this unknown key.
2111
+ attachments: z.array(FileAttachmentSchema).optional()
2066
2112
  });
2067
2113
  var ACCEPT_TRANSPORTS_TAG = "accept";
2068
2114
  var KNOWN_TRANSPORT_KINDS = ["iroh", "blossom"];
@@ -2095,12 +2141,21 @@ function readAcceptedTransports(tags) {
2095
2141
  }
2096
2142
  return out.length > 0 ? out : void 0;
2097
2143
  }
2144
+ function attachmentsOf(decoded) {
2145
+ if (decoded.attachments !== void 0 && decoded.attachments.length > 0) {
2146
+ return decoded.attachments;
2147
+ }
2148
+ return decoded.attachment !== void 0 ? [decoded.attachment] : [];
2149
+ }
2098
2150
  function encodeJobPayload(payload) {
2099
2151
  const envelope = { v: ENVELOPE_VERSION };
2100
2152
  if (payload.text !== void 0) {
2101
2153
  envelope.text = payload.text;
2102
2154
  }
2103
- if (payload.attachment !== void 0) {
2155
+ if (payload.attachments !== void 0 && payload.attachments.length > 0) {
2156
+ envelope.attachments = payload.attachments;
2157
+ envelope.attachment = payload.attachments[0];
2158
+ } else if (payload.attachment !== void 0) {
2104
2159
  envelope.attachment = payload.attachment;
2105
2160
  }
2106
2161
  return JSON.stringify(envelope);
@@ -2128,7 +2183,11 @@ function decodeJobPayload(content) {
2128
2183
  `Invalid elisym job payload (v=${JSON.stringify(version)}): ${result.error.message}`
2129
2184
  );
2130
2185
  }
2131
- return { text: result.data.text, attachment: result.data.attachment };
2186
+ return {
2187
+ text: result.data.text,
2188
+ attachment: result.data.attachment,
2189
+ attachments: result.data.attachments
2190
+ };
2132
2191
  }
2133
2192
 
2134
2193
  // src/services/marketplace.ts
@@ -2293,7 +2352,7 @@ var MarketplaceService = class {
2293
2352
  }
2294
2353
  resultDelivered = true;
2295
2354
  try {
2296
- cb.onResult?.(decoded.text ?? "", ev.id, decoded.attachment);
2355
+ cb.onResult?.(decoded.text ?? "", ev.id, decoded.attachment, attachmentsOf(decoded));
2297
2356
  } catch {
2298
2357
  } finally {
2299
2358
  done();
@@ -2457,8 +2516,8 @@ var MarketplaceService = class {
2457
2516
  );
2458
2517
  }
2459
2518
  /** Submit a job result with NIP-44 encrypted content. Result kind is derived from the request kind. */
2460
- async submitJobResult(identity, requestEvent, content, amount, attachment) {
2461
- const hasAttachment = attachment !== void 0;
2519
+ async submitJobResult(identity, requestEvent, content, amount, attachments) {
2520
+ const hasAttachment = attachments !== void 0 && attachments.length > 0;
2462
2521
  if (!content && !hasAttachment) {
2463
2522
  throw new Error("Job result content must not be empty.");
2464
2523
  }
@@ -2472,7 +2531,7 @@ var MarketplaceService = class {
2472
2531
  );
2473
2532
  }
2474
2533
  const shouldEncrypt = isEncrypted(requestEvent);
2475
- const payload = hasAttachment ? encodeJobPayload({ text: content || void 0, attachment }) : content;
2534
+ const payload = hasAttachment ? encodeJobPayload({ text: content || void 0, attachments }) : content;
2476
2535
  if (shouldEncrypt) {
2477
2536
  const payloadBytes = utf8ByteLength(payload);
2478
2537
  if (payloadBytes > LIMITS.NIP44_MAX_PLAINTEXT_BYTES) {
@@ -2513,11 +2572,11 @@ var MarketplaceService = class {
2513
2572
  * With maxAttempts=3: try, ~1s, try, ~2s, try, throw.
2514
2573
  * Jitter: 0.5x-1.0x of calculated delay.
2515
2574
  */
2516
- async submitJobResultWithRetry(identity, requestEvent, content, amount, maxAttempts = DEFAULTS.RESULT_RETRY_COUNT, baseDelayMs = DEFAULTS.RESULT_RETRY_BASE_MS, attachment) {
2575
+ async submitJobResultWithRetry(identity, requestEvent, content, amount, maxAttempts = DEFAULTS.RESULT_RETRY_COUNT, baseDelayMs = DEFAULTS.RESULT_RETRY_BASE_MS, attachments) {
2517
2576
  const attempts = Math.max(1, maxAttempts);
2518
2577
  for (let attempt = 0; attempt < attempts; attempt++) {
2519
2578
  try {
2520
- return await this.submitJobResult(identity, requestEvent, content, amount, attachment);
2579
+ return await this.submitJobResult(identity, requestEvent, content, amount, attachments);
2521
2580
  } catch (e) {
2522
2581
  if (attempt >= attempts - 1) {
2523
2582
  throw e;
@@ -3676,11 +3735,12 @@ function createBlossomTransport(opts) {
3676
3735
  enc: { alg: "AES-256-GCM", iv, key: wrappedKey }
3677
3736
  };
3678
3737
  },
3679
- async fetchToBytes({ transport, senderPubkey, maxBytes }) {
3738
+ async fetchToBytes({ transport, senderPubkey, maxBytes, signal }) {
3680
3739
  const plaintextCap = maxBytes ?? LIMITS.MAX_BLOSSOM_ENCRYPTED_BYTES;
3681
3740
  const ciphertext = await blossom.download(transport.url, {
3682
3741
  maxBytes: plaintextCap + AES_GCM_TAG_BYTES,
3683
- expectedSha256: transport.sha256
3742
+ expectedSha256: transport.sha256,
3743
+ signal
3684
3744
  });
3685
3745
  return decryptBytesFromSender(
3686
3746
  ciphertext,
@@ -3694,6 +3754,10 @@ function createBlossomTransport(opts) {
3694
3754
  }
3695
3755
 
3696
3756
  // src/transport/file-jobs.ts
3757
+ async function sha256Hex(bytes) {
3758
+ const digest = await crypto.subtle.digest("SHA-256", bytes);
3759
+ return [...new Uint8Array(digest)].map((b) => b.toString(16).padStart(2, "0")).join("");
3760
+ }
3697
3761
  var EXECUTABLE_EXTENSIONS = /* @__PURE__ */ new Set([
3698
3762
  ".exe",
3699
3763
  ".dll",
@@ -3719,21 +3783,56 @@ function looksExecutable(name, type) {
3719
3783
  const ext = dot >= 0 ? name.slice(dot).toLowerCase() : "";
3720
3784
  return EXECUTABLE_EXTENSIONS.has(ext) || EXECUTABLE_MIMES.has(type);
3721
3785
  }
3722
- async function buildEncryptedFileInput(args) {
3786
+ async function prepareEncryptedFileInput(args) {
3723
3787
  const { file, providerPubkey, identity, blossom } = args;
3724
3788
  const name = file.name ?? "upload";
3725
3789
  if (looksExecutable(name, file.type)) {
3726
3790
  throw new Error("Refusing to upload an executable file.");
3727
3791
  }
3728
- const transport = createBlossomTransport({ blossom, identity });
3729
3792
  const bytes = new Uint8Array(await file.arrayBuffer());
3730
- const member = await transport.seedBytes({ bytes, recipientPubkey: providerPubkey });
3731
- return {
3793
+ if (bytes.byteLength > LIMITS.MAX_BLOSSOM_ENCRYPTED_BYTES) {
3794
+ throw new Error(
3795
+ `File too large for the encrypted-Blossom transport: ${bytes.byteLength} bytes (max ${LIMITS.MAX_BLOSSOM_ENCRYPTED_BYTES}).`
3796
+ );
3797
+ }
3798
+ const { ciphertext, wrappedKey, iv } = await encryptBytesForRecipient(
3799
+ bytes,
3800
+ identity.secretKey,
3801
+ providerPubkey
3802
+ );
3803
+ const sha256 = await sha256Hex(ciphertext);
3804
+ const member = {
3805
+ kind: "blossom",
3806
+ url: blossom.contentUrl(sha256),
3807
+ sha256,
3808
+ enc: { alg: "AES-256-GCM", iv, key: wrappedKey }
3809
+ };
3810
+ const attachment = {
3732
3811
  name,
3733
3812
  size: bytes.byteLength,
3734
3813
  mime: file.type || "application/octet-stream",
3735
3814
  transports: [member]
3736
3815
  };
3816
+ const upload = async () => {
3817
+ const descriptor = await blossom.upload(
3818
+ identity,
3819
+ new Blob([ciphertext], { type: "application/octet-stream" })
3820
+ );
3821
+ if (descriptor.provider !== "blossom") {
3822
+ throw new Error("Blossom upload fell back to a non-content-addressed provider.");
3823
+ }
3824
+ if (descriptor.sha256 !== sha256 || descriptor.url !== member.url) {
3825
+ throw new Error(
3826
+ `Blossom upload descriptor mismatch (expected ${member.url} / ${sha256}, got ${descriptor.url} / ${descriptor.sha256}).`
3827
+ );
3828
+ }
3829
+ };
3830
+ return { attachment, upload };
3831
+ }
3832
+ async function buildEncryptedFileInput(args) {
3833
+ const prepared = await prepareEncryptedFileInput(args);
3834
+ await prepared.upload();
3835
+ return prepared.attachment;
3737
3836
  }
3738
3837
  async function fetchEncryptedFileOutput(args) {
3739
3838
  const { attachment, providerPubkey, identity, blossom, maxBytes } = args;
@@ -4323,6 +4422,6 @@ function makeCensor() {
4323
4422
  };
4324
4423
  }
4325
4424
 
4326
- 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, 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, readAcceptedTransports, resolveAssetFromPaymentRequest, resolveKnownAsset, timeAgo, toDTag, truncateKey, utf8ByteLength, validateAgentName, validateExpiry, verifyJobPaymentQuick };
4425
+ 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 };
4327
4426
  //# sourceMappingURL=index.js.map
4328
4427
  //# sourceMappingURL=index.js.map