@elisym/sdk 0.25.0 → 0.25.1

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
@@ -75,7 +75,9 @@ var DEFAULTS = {
75
75
  IROH_FETCH_TIMEOUT_MS: 3e5,
76
76
  // Ceiling for a single Blossom blob upload (PUT /upload). Large blobs (up to
77
77
  // LIMITS.MAX_FILE_SIZE) need far more than the 30s used for small media images.
78
- BLOSSOM_UPLOAD_TIMEOUT_MS: 3e5
78
+ BLOSSOM_UPLOAD_TIMEOUT_MS: 3e5,
79
+ // Ceiling for a single encrypted Blossom blob download (GET). Same budget as upload.
80
+ BLOSSOM_FETCH_TIMEOUT_MS: 3e5
79
81
  };
80
82
  var LIMITS = {
81
83
  MAX_INPUT_LENGTH: 1e5,
@@ -100,6 +102,12 @@ var LIMITS = {
100
102
  // providers may lower it per deployment.
101
103
  MAX_FILE_SIZE: 1073741824,
102
104
  // 1 GiB
105
+ // Cap for the ENCRYPTED Blossom path (web/SDK). The encrypt-then-upload flow is
106
+ // whole-buffer in WebCrypto + BlossomService (~3x file-size peak RAM), so this is
107
+ // deliberately far below MAX_FILE_SIZE to stay safe in a browser tab; larger files
108
+ // use iroh. The relay enforces a ~128 MiB server-side backstop.
109
+ MAX_BLOSSOM_ENCRYPTED_BYTES: 104857600,
110
+ // 100 MiB
103
111
  MAX_TIMEOUT_SECS: 600,
104
112
  // Upper bound for execution budgets (`max_execution_secs` / `execution_timeout_secs`).
105
113
  // Distinct from MAX_TIMEOUT_SECS (the result-wait cap): execution budgets may be
@@ -1237,6 +1245,62 @@ var BlossomService = class {
1237
1245
  clearTimeout(timer);
1238
1246
  }
1239
1247
  }
1248
+ /**
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.
1251
+ */
1252
+ async download(url, opts = {}) {
1253
+ const maxBytes = opts.maxBytes ?? LIMITS.MAX_FILE_SIZE;
1254
+ const controller = new AbortController();
1255
+ const timer = setTimeout(
1256
+ () => controller.abort(),
1257
+ opts.timeoutMs ?? DEFAULTS.BLOSSOM_FETCH_TIMEOUT_MS
1258
+ );
1259
+ try {
1260
+ const res = await fetch(url, { signal: controller.signal });
1261
+ if (!res.ok) {
1262
+ throw new Error(`Download failed: ${res.status} ${res.statusText}`);
1263
+ }
1264
+ const declared = Number(res.headers.get("content-length"));
1265
+ if (Number.isFinite(declared) && declared > maxBytes) {
1266
+ throw new Error(`Blob too large: ${declared} bytes exceeds limit of ${maxBytes}.`);
1267
+ }
1268
+ if (!res.body) {
1269
+ throw new Error("Download response has no body.");
1270
+ }
1271
+ const reader = res.body.getReader();
1272
+ const chunks = [];
1273
+ let total = 0;
1274
+ let chunk = await reader.read();
1275
+ while (!chunk.done) {
1276
+ total += chunk.value.byteLength;
1277
+ if (total > maxBytes) {
1278
+ await reader.cancel();
1279
+ throw new Error(`Blob exceeds limit of ${maxBytes} bytes.`);
1280
+ }
1281
+ chunks.push(chunk.value);
1282
+ chunk = await reader.read();
1283
+ }
1284
+ const bytes = new Uint8Array(total);
1285
+ let offset = 0;
1286
+ for (const c of chunks) {
1287
+ bytes.set(c, offset);
1288
+ offset += c.byteLength;
1289
+ }
1290
+ if (opts.expectedSha256 !== void 0) {
1291
+ const digest = await crypto.subtle.digest("SHA-256", bytes);
1292
+ const hashHex = [...new Uint8Array(digest)].map((b) => b.toString(16).padStart(2, "0")).join("");
1293
+ if (hashHex !== opts.expectedSha256) {
1294
+ throw new Error(
1295
+ `Download integrity check failed: got ${hashHex}, expected ${opts.expectedSha256}.`
1296
+ );
1297
+ }
1298
+ }
1299
+ return bytes;
1300
+ } finally {
1301
+ clearTimeout(timer);
1302
+ }
1303
+ }
1240
1304
  async uploadToBlossom(identity, bytes, hashHex, mime) {
1241
1305
  const contentType = mime || "application/octet-stream";
1242
1306
  const authHeader = this.authHeader(identity, "upload", hashHex);
@@ -1952,6 +2016,26 @@ var FileTransportSchema = z.discriminatedUnion("kind", [
1952
2016
  kind: z.literal("iroh"),
1953
2017
  /** Opaque iroh `BlobTicket` string. Parsed into a real ticket only at fetch time. */
1954
2018
  ticket: z.string().min(1).max(MAX_TICKET_LENGTH)
2019
+ }),
2020
+ z.object({
2021
+ kind: z.literal("blossom"),
2022
+ /** Public HTTP(S) URL of the CIPHERTEXT blob on a Blossom relay. */
2023
+ url: z.string().url().max(2048),
2024
+ /** sha256 (lowercase hex) of the ciphertext - what the relay stores and addresses. */
2025
+ sha256: z.string().regex(/^[0-9a-f]{64}$/),
2026
+ /**
2027
+ * Hybrid-encryption parameters. The file bytes are AES-256-GCM encrypted with a random
2028
+ * content key; that key is NIP-44-wrapped to the recipient. `name`/`mime`/`size` on the
2029
+ * attachment describe the PLAINTEXT and live only inside the (encrypted) envelope - never
2030
+ * sent to the relay (the relay only ever sees opaque ciphertext).
2031
+ */
2032
+ enc: z.object({
2033
+ alg: z.literal("AES-256-GCM"),
2034
+ /** base64 12-byte GCM IV (non-secret). */
2035
+ iv: z.string().min(1).max(64),
2036
+ /** NIP-44-wrapped content key. */
2037
+ key: z.string().min(1).max(2048)
2038
+ })
1955
2039
  })
1956
2040
  ]);
1957
2041
  var FileAttachmentSchema = z.object({
@@ -1960,8 +2044,18 @@ var FileAttachmentSchema = z.object({
1960
2044
  /** Declared size in bytes (display/hint only; enforcement is on actual streamed bytes). */
1961
2045
  size: z.number().int().nonnegative(),
1962
2046
  mime: z.string().min(1).max(255),
1963
- /** Ordered by sender preference; at least one. */
1964
- transports: z.array(FileTransportSchema).min(1),
2047
+ /**
2048
+ * Ordered by sender preference; at least one KNOWN transport. Parsed leniently: unknown
2049
+ * transport `kind`s are dropped (not rejected) so adding a new transport never makes an older
2050
+ * decoder throw away the whole envelope - it just ignores the kinds it doesn't know and uses
2051
+ * the ones it does. At least one known transport must survive, else the attachment is invalid.
2052
+ */
2053
+ transports: z.array(z.unknown()).transform(
2054
+ (arr) => arr.flatMap((t) => {
2055
+ const parsed = FileTransportSchema.safeParse(t);
2056
+ return parsed.success ? [parsed.data] : [];
2057
+ })
2058
+ ).refine((arr) => arr.length >= 1, { message: "attachment has no known transport" }),
1965
2059
  /** Optional provider hint (unix seconds) for when seeding may stop. */
1966
2060
  seedingExpiresAt: z.number().int().nonnegative().optional()
1967
2061
  });
@@ -1970,6 +2064,37 @@ var JobPayloadEnvelopeSchema = z.object({
1970
2064
  text: z.string().optional(),
1971
2065
  attachment: FileAttachmentSchema.optional()
1972
2066
  });
2067
+ var ACCEPT_TRANSPORTS_TAG = "accept";
2068
+ var KNOWN_TRANSPORT_KINDS = ["iroh", "blossom"];
2069
+ function isKnownTransportKind(value) {
2070
+ return KNOWN_TRANSPORT_KINDS.includes(value);
2071
+ }
2072
+ function buildAcceptTransportsTag(kinds) {
2073
+ const seen = /* @__PURE__ */ new Set();
2074
+ const out = [ACCEPT_TRANSPORTS_TAG];
2075
+ for (const kind of kinds) {
2076
+ if (isKnownTransportKind(kind) && !seen.has(kind)) {
2077
+ seen.add(kind);
2078
+ out.push(kind);
2079
+ }
2080
+ }
2081
+ return out;
2082
+ }
2083
+ function readAcceptedTransports(tags) {
2084
+ const tag = tags.find((t) => t[0] === ACCEPT_TRANSPORTS_TAG);
2085
+ if (tag === void 0) {
2086
+ return void 0;
2087
+ }
2088
+ const seen = /* @__PURE__ */ new Set();
2089
+ const out = [];
2090
+ for (const value of tag.slice(1)) {
2091
+ if (isKnownTransportKind(value) && !seen.has(value)) {
2092
+ seen.add(value);
2093
+ out.push(value);
2094
+ }
2095
+ }
2096
+ return out.length > 0 ? out : void 0;
2097
+ }
1973
2098
  function encodeJobPayload(payload) {
1974
2099
  const envelope = { v: ENVELOPE_VERSION };
1975
2100
  if (payload.text !== void 0) {
@@ -2072,6 +2197,12 @@ var MarketplaceService = class {
2072
2197
  tags.push(["p", options.providerPubkey]);
2073
2198
  tags.push(["encrypted", "nip44"]);
2074
2199
  }
2200
+ if (options.acceptTransports && options.acceptTransports.length > 0) {
2201
+ const acceptTag = buildAcceptTransportsTag(options.acceptTransports);
2202
+ if (acceptTag.length > 1) {
2203
+ tags.push(acceptTag);
2204
+ }
2205
+ }
2075
2206
  const kind = jobRequestKind(options.kindOffset ?? DEFAULT_KIND_OFFSET);
2076
2207
  const event = finalizeEvent(
2077
2208
  {
@@ -3480,6 +3611,147 @@ var ElisymClient = class {
3480
3611
  }
3481
3612
  };
3482
3613
 
3614
+ // src/primitives/file-crypto.ts
3615
+ var KEY_BYTES = 32;
3616
+ var IV_BYTES = 12;
3617
+ function bytesToBase64(bytes) {
3618
+ let bin = "";
3619
+ for (const b of bytes) {
3620
+ bin += String.fromCharCode(b);
3621
+ }
3622
+ return btoa(bin);
3623
+ }
3624
+ function base64ToBytes(b64) {
3625
+ const bin = atob(b64);
3626
+ const out = new Uint8Array(bin.length);
3627
+ for (let i = 0; i < bin.length; i += 1) {
3628
+ out[i] = bin.charCodeAt(i);
3629
+ }
3630
+ return out;
3631
+ }
3632
+ async function encryptBytesForRecipient(bytes, senderSk, recipientPubkey) {
3633
+ const rawKey = crypto.getRandomValues(new Uint8Array(KEY_BYTES));
3634
+ const iv = crypto.getRandomValues(new Uint8Array(IV_BYTES));
3635
+ const key = await crypto.subtle.importKey("raw", rawKey, "AES-GCM", false, ["encrypt"]);
3636
+ const ct = await crypto.subtle.encrypt({ name: "AES-GCM", iv, tagLength: 128 }, key, bytes);
3637
+ const wrappedKey = nip44Encrypt(bytesToBase64(rawKey), senderSk, recipientPubkey);
3638
+ return { ciphertext: new Uint8Array(ct), wrappedKey, iv: bytesToBase64(iv) };
3639
+ }
3640
+ async function decryptBytesFromSender(ciphertext, wrappedKey, iv, receiverSk, senderPubkey) {
3641
+ const rawKey = base64ToBytes(nip44Decrypt(wrappedKey, receiverSk, senderPubkey));
3642
+ const key = await crypto.subtle.importKey("raw", rawKey, "AES-GCM", false, ["decrypt"]);
3643
+ const pt = await crypto.subtle.decrypt(
3644
+ { name: "AES-GCM", iv: base64ToBytes(iv), tagLength: 128 },
3645
+ key,
3646
+ ciphertext
3647
+ );
3648
+ return new Uint8Array(pt);
3649
+ }
3650
+
3651
+ // src/transport/blossom-transport.ts
3652
+ var AES_GCM_TAG_BYTES = 16;
3653
+ function createBlossomTransport(opts) {
3654
+ const { blossom, identity } = opts;
3655
+ return {
3656
+ async seedBytes({ bytes, recipientPubkey }) {
3657
+ if (bytes.byteLength > LIMITS.MAX_BLOSSOM_ENCRYPTED_BYTES) {
3658
+ throw new Error(
3659
+ `File too large for encrypted Blossom: ${bytes.byteLength} bytes exceeds ${LIMITS.MAX_BLOSSOM_ENCRYPTED_BYTES}.`
3660
+ );
3661
+ }
3662
+ const { ciphertext, wrappedKey, iv } = await encryptBytesForRecipient(
3663
+ bytes,
3664
+ identity.secretKey,
3665
+ recipientPubkey
3666
+ );
3667
+ const blob = new Blob([ciphertext], { type: "application/octet-stream" });
3668
+ const descriptor = await blossom.upload(identity, blob);
3669
+ if (descriptor.provider !== "blossom") {
3670
+ throw new Error("Blossom upload fell back to a non-content-addressed provider.");
3671
+ }
3672
+ return {
3673
+ kind: "blossom",
3674
+ url: descriptor.url,
3675
+ sha256: descriptor.sha256,
3676
+ enc: { alg: "AES-256-GCM", iv, key: wrappedKey }
3677
+ };
3678
+ },
3679
+ async fetchToBytes({ transport, senderPubkey, maxBytes }) {
3680
+ const plaintextCap = maxBytes ?? LIMITS.MAX_BLOSSOM_ENCRYPTED_BYTES;
3681
+ const ciphertext = await blossom.download(transport.url, {
3682
+ maxBytes: plaintextCap + AES_GCM_TAG_BYTES,
3683
+ expectedSha256: transport.sha256
3684
+ });
3685
+ return decryptBytesFromSender(
3686
+ ciphertext,
3687
+ transport.enc.key,
3688
+ transport.enc.iv,
3689
+ identity.secretKey,
3690
+ senderPubkey
3691
+ );
3692
+ }
3693
+ };
3694
+ }
3695
+
3696
+ // src/transport/file-jobs.ts
3697
+ var EXECUTABLE_EXTENSIONS = /* @__PURE__ */ new Set([
3698
+ ".exe",
3699
+ ".dll",
3700
+ ".bat",
3701
+ ".cmd",
3702
+ ".com",
3703
+ ".msi",
3704
+ ".sh",
3705
+ ".app",
3706
+ ".scr",
3707
+ ".ps1"
3708
+ ]);
3709
+ var EXECUTABLE_MIMES = /* @__PURE__ */ new Set([
3710
+ "application/x-msdownload",
3711
+ "application/x-msdos-program",
3712
+ "application/x-sh",
3713
+ "application/x-executable",
3714
+ "application/vnd.microsoft.portable-executable",
3715
+ "application/x-mach-binary"
3716
+ ]);
3717
+ function looksExecutable(name, type) {
3718
+ const dot = name.lastIndexOf(".");
3719
+ const ext = dot >= 0 ? name.slice(dot).toLowerCase() : "";
3720
+ return EXECUTABLE_EXTENSIONS.has(ext) || EXECUTABLE_MIMES.has(type);
3721
+ }
3722
+ async function buildEncryptedFileInput(args) {
3723
+ const { file, providerPubkey, identity, blossom } = args;
3724
+ const name = file.name ?? "upload";
3725
+ if (looksExecutable(name, file.type)) {
3726
+ throw new Error("Refusing to upload an executable file.");
3727
+ }
3728
+ const transport = createBlossomTransport({ blossom, identity });
3729
+ const bytes = new Uint8Array(await file.arrayBuffer());
3730
+ const member = await transport.seedBytes({ bytes, recipientPubkey: providerPubkey });
3731
+ return {
3732
+ name,
3733
+ size: bytes.byteLength,
3734
+ mime: file.type || "application/octet-stream",
3735
+ transports: [member]
3736
+ };
3737
+ }
3738
+ async function fetchEncryptedFileOutput(args) {
3739
+ const { attachment, providerPubkey, identity, blossom, maxBytes } = args;
3740
+ const member = attachment.transports.find(
3741
+ (t) => t.kind === "blossom"
3742
+ );
3743
+ if (member === void 0) {
3744
+ throw new Error("Attachment has no blossom transport.");
3745
+ }
3746
+ const transport = createBlossomTransport({ blossom, identity });
3747
+ const bytes = await transport.fetchToBytes({
3748
+ transport: member,
3749
+ senderPubkey: providerPubkey,
3750
+ maxBytes
3751
+ });
3752
+ return { bytes, name: attachment.name, mime: attachment.mime };
3753
+ }
3754
+
3483
3755
  // src/services/jobErrors.ts
3484
3756
  var AGENT_UNAVAILABLE_MARKERS = [
3485
3757
  "agent temporarily unavailable",
@@ -4051,6 +4323,6 @@ function makeCensor() {
4051
4323
  };
4052
4324
  }
4053
4325
 
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 };
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 };
4055
4327
  //# sourceMappingURL=index.js.map
4056
4328
  //# sourceMappingURL=index.js.map