@elisym/sdk 0.25.3 → 0.25.5

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.d.cts CHANGED
@@ -417,18 +417,20 @@ interface CapabilityCard {
417
417
  /**
418
418
  * MIME the capability expects as a file input (from a dynamic-script skill's
419
419
  * `input_mime`). Discovery hint only; the provider still content-sniffs the
420
- * actual file. Its presence means the capability needs a file input - which
421
- * the web app cannot send (no iroh transport), so the web app blocks the Buy
422
- * button. `*` = any file, `image/*` = any image, `image/png` = exact.
420
+ * actual file. Its presence means the capability accepts a file input (the web
421
+ * app sends it over encrypted Blossom; the MCP/CLI over iroh), so clients show a
422
+ * file picker. `*` = any file, `image/*` = any image, `image/png` = exact.
423
423
  */
424
424
  inputMime?: string;
425
425
  /** MIME of a file result the capability produces (from `output_mime`). */
426
426
  outputMime?: string;
427
427
  /**
428
- * Whether a file-input capability ALSO accepts a text prompt (from `input_text`):
429
- * `'none'` = file only, `'optional'` = file + optional note, `'required'` = both.
430
- * Discovery hint; the web app shows/hides its text box accordingly. Only meaningful
431
- * with `inputMime`. Untrusted - gate on it, never render the raw value.
428
+ * How a file-input capability treats the text prompt (from `input_text`):
429
+ * `'none'` = file only, `'optional'` = file optional + instruction required (a
430
+ * generate-or-edit skill), `'required'` = file + text both required. Omitted =
431
+ * file required + optional note. Discovery hint; the web app shows/hides/gates its
432
+ * text box and file picker accordingly. Only meaningful with `inputMime`.
433
+ * Untrusted - gate on it, never render the raw value.
432
434
  */
433
435
  inputText?: 'required' | 'optional' | 'none';
434
436
  }
@@ -801,13 +803,22 @@ declare class BlossomService {
801
803
  /** Delete a blob by sha256 (BUD-02). Blossom only - there is no fallback for deletes. */
802
804
  delete(identity: ElisymIdentity, sha256: string): Promise<void>;
803
805
  /**
804
- * Download a public blob (BUD-01 GET, no auth). Bounds memory on the ACTUAL streamed bytes (never
805
- * the declared Content-Length) and verifies the sha256 when `expectedSha256` is given. Browser-safe.
806
+ * Download a content-addressed blob from THIS Blossom server (BUD-01 GET, no auth). Bounds memory
807
+ * on the ACTUAL streamed bytes (never the declared Content-Length) and verifies the sha256 when
808
+ * `expectedSha256` is given. Browser-safe.
809
+ *
810
+ * SSRF guard: `url` typically arrives inside a remote counterparty's encrypted job envelope, so it
811
+ * is untrusted. elisym blobs are content-addressed on the single configured server (`seedBytes`
812
+ * refuses non-content-addressed fallbacks), so a legitimate URL is always `<serverUrl>/<sha256>`.
813
+ * The origin is pinned to `serverUrl` and redirects are refused, so a crafted url (or a 30x from
814
+ * the host) can't coerce a fetch to loopback, cloud-metadata, or internal addresses. Federation
815
+ * across Blossom servers would replace this single-origin pin with an explicit allowlist.
806
816
  */
807
817
  download(url: string, opts?: {
808
818
  maxBytes?: number;
809
819
  timeoutMs?: number;
810
820
  expectedSha256?: string;
821
+ signal?: AbortSignal;
811
822
  }): Promise<Uint8Array>;
812
823
  private uploadToBlossom;
813
824
  private authHeader;
@@ -1171,6 +1182,8 @@ interface BlossomBlobTransport {
1171
1182
  transport: BlossomTransport;
1172
1183
  senderPubkey: string;
1173
1184
  maxBytes?: number;
1185
+ /** Abort the in-flight download (e.g. job stop() / input-fetch budget). */
1186
+ signal?: AbortSignal;
1174
1187
  }): Promise<Uint8Array>;
1175
1188
  }
1176
1189
  declare function createBlossomTransport(opts: {
package/dist/index.d.ts CHANGED
@@ -417,18 +417,20 @@ interface CapabilityCard {
417
417
  /**
418
418
  * MIME the capability expects as a file input (from a dynamic-script skill's
419
419
  * `input_mime`). Discovery hint only; the provider still content-sniffs the
420
- * actual file. Its presence means the capability needs a file input - which
421
- * the web app cannot send (no iroh transport), so the web app blocks the Buy
422
- * button. `*` = any file, `image/*` = any image, `image/png` = exact.
420
+ * actual file. Its presence means the capability accepts a file input (the web
421
+ * app sends it over encrypted Blossom; the MCP/CLI over iroh), so clients show a
422
+ * file picker. `*` = any file, `image/*` = any image, `image/png` = exact.
423
423
  */
424
424
  inputMime?: string;
425
425
  /** MIME of a file result the capability produces (from `output_mime`). */
426
426
  outputMime?: string;
427
427
  /**
428
- * Whether a file-input capability ALSO accepts a text prompt (from `input_text`):
429
- * `'none'` = file only, `'optional'` = file + optional note, `'required'` = both.
430
- * Discovery hint; the web app shows/hides its text box accordingly. Only meaningful
431
- * with `inputMime`. Untrusted - gate on it, never render the raw value.
428
+ * How a file-input capability treats the text prompt (from `input_text`):
429
+ * `'none'` = file only, `'optional'` = file optional + instruction required (a
430
+ * generate-or-edit skill), `'required'` = file + text both required. Omitted =
431
+ * file required + optional note. Discovery hint; the web app shows/hides/gates its
432
+ * text box and file picker accordingly. Only meaningful with `inputMime`.
433
+ * Untrusted - gate on it, never render the raw value.
432
434
  */
433
435
  inputText?: 'required' | 'optional' | 'none';
434
436
  }
@@ -801,13 +803,22 @@ declare class BlossomService {
801
803
  /** Delete a blob by sha256 (BUD-02). Blossom only - there is no fallback for deletes. */
802
804
  delete(identity: ElisymIdentity, sha256: string): Promise<void>;
803
805
  /**
804
- * Download a public blob (BUD-01 GET, no auth). Bounds memory on the ACTUAL streamed bytes (never
805
- * the declared Content-Length) and verifies the sha256 when `expectedSha256` is given. Browser-safe.
806
+ * Download a content-addressed blob from THIS Blossom server (BUD-01 GET, no auth). Bounds memory
807
+ * on the ACTUAL streamed bytes (never the declared Content-Length) and verifies the sha256 when
808
+ * `expectedSha256` is given. Browser-safe.
809
+ *
810
+ * SSRF guard: `url` typically arrives inside a remote counterparty's encrypted job envelope, so it
811
+ * is untrusted. elisym blobs are content-addressed on the single configured server (`seedBytes`
812
+ * refuses non-content-addressed fallbacks), so a legitimate URL is always `<serverUrl>/<sha256>`.
813
+ * The origin is pinned to `serverUrl` and redirects are refused, so a crafted url (or a 30x from
814
+ * the host) can't coerce a fetch to loopback, cloud-metadata, or internal addresses. Federation
815
+ * across Blossom servers would replace this single-origin pin with an explicit allowlist.
806
816
  */
807
817
  download(url: string, opts?: {
808
818
  maxBytes?: number;
809
819
  timeoutMs?: number;
810
820
  expectedSha256?: string;
821
+ signal?: AbortSignal;
811
822
  }): Promise<Uint8Array>;
812
823
  private uploadToBlossom;
813
824
  private authHeader;
@@ -1171,6 +1182,8 @@ interface BlossomBlobTransport {
1171
1182
  transport: BlossomTransport;
1172
1183
  senderPubkey: string;
1173
1184
  maxBytes?: number;
1185
+ /** Abort the in-flight download (e.g. job stop() / input-fetch budget). */
1186
+ signal?: AbortSignal;
1174
1187
  }): Promise<Uint8Array>;
1175
1188
  }
1176
1189
  declare function createBlossomTransport(opts: {
package/dist/index.js CHANGED
@@ -9,6 +9,8 @@ import * as nip44 from 'nostr-tools/nip44';
9
9
 
10
10
  // src/constants.ts
11
11
  var RELAYS = [
12
+ // Dedicated elisym relay (self-hosted) first, public relays as fallback.
13
+ "wss://relay.elisym.network",
12
14
  "wss://relay.damus.io",
13
15
  "wss://nos.lol",
14
16
  "wss://relay.nostr.band",
@@ -1262,18 +1264,38 @@ var BlossomService = class {
1262
1264
  }
1263
1265
  }
1264
1266
  /**
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
+ * Download a content-addressed blob from THIS Blossom server (BUD-01 GET, no auth). Bounds memory
1268
+ * on the ACTUAL streamed bytes (never the declared Content-Length) and verifies the sha256 when
1269
+ * `expectedSha256` is given. Browser-safe.
1270
+ *
1271
+ * SSRF guard: `url` typically arrives inside a remote counterparty's encrypted job envelope, so it
1272
+ * is untrusted. elisym blobs are content-addressed on the single configured server (`seedBytes`
1273
+ * refuses non-content-addressed fallbacks), so a legitimate URL is always `<serverUrl>/<sha256>`.
1274
+ * The origin is pinned to `serverUrl` and redirects are refused, so a crafted url (or a 30x from
1275
+ * the host) can't coerce a fetch to loopback, cloud-metadata, or internal addresses. Federation
1276
+ * across Blossom servers would replace this single-origin pin with an explicit allowlist.
1267
1277
  */
1268
1278
  async download(url, opts = {}) {
1279
+ if (new URL(url).origin !== new URL(this.serverUrl).origin) {
1280
+ throw new Error(`Refusing to download from a non-Blossom origin: ${url}`);
1281
+ }
1269
1282
  const maxBytes = opts.maxBytes ?? LIMITS.MAX_FILE_SIZE;
1270
1283
  const controller = new AbortController();
1271
1284
  const timer = setTimeout(
1272
1285
  () => controller.abort(),
1273
1286
  opts.timeoutMs ?? DEFAULTS.BLOSSOM_FETCH_TIMEOUT_MS
1274
1287
  );
1288
+ const externalSignal = opts.signal;
1289
+ const onExternalAbort = () => controller.abort();
1290
+ if (externalSignal !== void 0) {
1291
+ if (externalSignal.aborted) {
1292
+ controller.abort();
1293
+ } else {
1294
+ externalSignal.addEventListener("abort", onExternalAbort, { once: true });
1295
+ }
1296
+ }
1275
1297
  try {
1276
- const res = await fetch(url, { signal: controller.signal });
1298
+ const res = await fetch(url, { signal: controller.signal, redirect: "error" });
1277
1299
  if (!res.ok) {
1278
1300
  throw new Error(`Download failed: ${res.status} ${res.statusText}`);
1279
1301
  }
@@ -1315,6 +1337,9 @@ var BlossomService = class {
1315
1337
  return bytes;
1316
1338
  } finally {
1317
1339
  clearTimeout(timer);
1340
+ if (externalSignal !== void 0) {
1341
+ externalSignal.removeEventListener("abort", onExternalAbort);
1342
+ }
1318
1343
  }
1319
1344
  }
1320
1345
  async uploadToBlossom(identity, bytes, hashHex, mime) {
@@ -3712,11 +3737,12 @@ function createBlossomTransport(opts) {
3712
3737
  enc: { alg: "AES-256-GCM", iv, key: wrappedKey }
3713
3738
  };
3714
3739
  },
3715
- async fetchToBytes({ transport, senderPubkey, maxBytes }) {
3740
+ async fetchToBytes({ transport, senderPubkey, maxBytes, signal }) {
3716
3741
  const plaintextCap = maxBytes ?? LIMITS.MAX_BLOSSOM_ENCRYPTED_BYTES;
3717
3742
  const ciphertext = await blossom.download(transport.url, {
3718
3743
  maxBytes: plaintextCap + AES_GCM_TAG_BYTES,
3719
- expectedSha256: transport.sha256
3744
+ expectedSha256: transport.sha256,
3745
+ signal
3720
3746
  });
3721
3747
  return decryptBytesFromSender(
3722
3748
  ciphertext,