@elisym/sdk 0.25.3 → 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.cjs CHANGED
@@ -1287,18 +1287,38 @@ var BlossomService = class {
1287
1287
  }
1288
1288
  }
1289
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.
1290
+ * Download a content-addressed blob from THIS Blossom server (BUD-01 GET, no auth). Bounds memory
1291
+ * on the ACTUAL streamed bytes (never the declared Content-Length) and verifies the sha256 when
1292
+ * `expectedSha256` is given. Browser-safe.
1293
+ *
1294
+ * SSRF guard: `url` typically arrives inside a remote counterparty's encrypted job envelope, so it
1295
+ * is untrusted. elisym blobs are content-addressed on the single configured server (`seedBytes`
1296
+ * refuses non-content-addressed fallbacks), so a legitimate URL is always `<serverUrl>/<sha256>`.
1297
+ * The origin is pinned to `serverUrl` and redirects are refused, so a crafted url (or a 30x from
1298
+ * the host) can't coerce a fetch to loopback, cloud-metadata, or internal addresses. Federation
1299
+ * across Blossom servers would replace this single-origin pin with an explicit allowlist.
1292
1300
  */
1293
1301
  async download(url, opts = {}) {
1302
+ if (new URL(url).origin !== new URL(this.serverUrl).origin) {
1303
+ throw new Error(`Refusing to download from a non-Blossom origin: ${url}`);
1304
+ }
1294
1305
  const maxBytes = opts.maxBytes ?? LIMITS.MAX_FILE_SIZE;
1295
1306
  const controller = new AbortController();
1296
1307
  const timer = setTimeout(
1297
1308
  () => controller.abort(),
1298
1309
  opts.timeoutMs ?? DEFAULTS.BLOSSOM_FETCH_TIMEOUT_MS
1299
1310
  );
1311
+ const externalSignal = opts.signal;
1312
+ const onExternalAbort = () => controller.abort();
1313
+ if (externalSignal !== void 0) {
1314
+ if (externalSignal.aborted) {
1315
+ controller.abort();
1316
+ } else {
1317
+ externalSignal.addEventListener("abort", onExternalAbort, { once: true });
1318
+ }
1319
+ }
1300
1320
  try {
1301
- const res = await fetch(url, { signal: controller.signal });
1321
+ const res = await fetch(url, { signal: controller.signal, redirect: "error" });
1302
1322
  if (!res.ok) {
1303
1323
  throw new Error(`Download failed: ${res.status} ${res.statusText}`);
1304
1324
  }
@@ -1340,6 +1360,9 @@ var BlossomService = class {
1340
1360
  return bytes;
1341
1361
  } finally {
1342
1362
  clearTimeout(timer);
1363
+ if (externalSignal !== void 0) {
1364
+ externalSignal.removeEventListener("abort", onExternalAbort);
1365
+ }
1343
1366
  }
1344
1367
  }
1345
1368
  async uploadToBlossom(identity, bytes, hashHex, mime) {
@@ -3737,11 +3760,12 @@ function createBlossomTransport(opts) {
3737
3760
  enc: { alg: "AES-256-GCM", iv, key: wrappedKey }
3738
3761
  };
3739
3762
  },
3740
- async fetchToBytes({ transport, senderPubkey, maxBytes }) {
3763
+ async fetchToBytes({ transport, senderPubkey, maxBytes, signal }) {
3741
3764
  const plaintextCap = maxBytes ?? LIMITS.MAX_BLOSSOM_ENCRYPTED_BYTES;
3742
3765
  const ciphertext = await blossom.download(transport.url, {
3743
3766
  maxBytes: plaintextCap + AES_GCM_TAG_BYTES,
3744
- expectedSha256: transport.sha256
3767
+ expectedSha256: transport.sha256,
3768
+ signal
3745
3769
  });
3746
3770
  return decryptBytesFromSender(
3747
3771
  ciphertext,