@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/agent-store.cjs.map +1 -1
- package/dist/agent-store.js.map +1 -1
- package/dist/index.cjs +369 -12
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +378 -41
- package/dist/index.d.ts +378 -41
- package/dist/index.js +360 -13
- package/dist/index.js.map +1 -1
- package/dist/node.cjs +83 -22
- package/dist/node.cjs.map +1 -1
- package/dist/node.d.cts +66 -0
- package/dist/node.d.ts +66 -0
- package/dist/node.js +83 -22
- package/dist/node.js.map +1 -1
- package/dist/skills.cjs +57 -1
- package/dist/skills.cjs.map +1 -1
- package/dist/skills.d.cts +23 -3
- package/dist/skills.d.ts +23 -3
- package/dist/skills.js +58 -2
- package/dist/skills.js.map +1 -1
- package/package.json +1 -1
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
|
-
/**
|
|
1989
|
-
|
|
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
|
|
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.
|
|
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 {
|
|
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,
|
|
2355
|
-
const hasAttachment =
|
|
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,
|
|
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,
|
|
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,
|
|
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;
|