@comfyorg/sdk 0.1.1 → 0.1.2

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/README.md CHANGED
@@ -44,11 +44,11 @@ await job.getOutputs("13")[0].toFile("out.png");
44
44
 
45
45
  ## Auth, per surface
46
46
 
47
- | Surface | Auth |
48
- | ------------------------------------------------------------------ | ------------------------------------------ |
49
- | Self-hosted proxy (`comfy-api-proxy` in front of your own ComfyUI) | none — do **not** pass `apiKey` |
50
- | Comfy Cloud | `new Comfy(baseUrl, { apiKey: "ck_..." })` |
51
- | Serverless | `new Comfy(baseUrl, { apiKey: "ck_..." })` |
47
+ | Surface | Auth |
48
+ | ------------------------------------------------------------------ | ----------------------------------------------- |
49
+ | Self-hosted proxy (`comfy-api-proxy` in front of your own ComfyUI) | none — do **not** pass `apiKey` |
50
+ | Comfy Cloud | `new Comfy(baseUrl, { apiKey: "comfyui-..." })` |
51
+ | Serverless | `new Comfy(baseUrl, { apiKey: "comfyui-..." })` |
52
52
 
53
53
  The client only attaches the `Authorization` header to requests aimed at its
54
54
  own `baseUrl`'s origin. If the server hands back an absolute URL on a
@@ -56,6 +56,11 @@ different host (for example a job's `events`/`cancel` link, or a redirect on
56
56
  an asset download), the key is not sent there — see "Typed errors" below for
57
57
  the exception classes this surface can raise.
58
58
 
59
+ The SDK identifies itself via `User-Agent` (for support + usage analytics);
60
+ no other data is collected. Pass `clientInfo` to `new Comfy(baseUrl, { ... })`
61
+ to append your own app's name to it, for example when attributing traffic
62
+ from a Worker built on top of this SDK.
63
+
59
64
  ## Quickstart
60
65
 
61
66
  ```ts
@@ -173,6 +178,32 @@ managing an `AbortController` yourself:
173
178
  await client.run(wf, { timeoutMs: 60_000 });
174
179
  ```
175
180
 
181
+ ## Downloading outputs
182
+
183
+ A finished job exposes its results as output handles — `job.outputs`, or
184
+ `job.getOutputs(nodeId)` to filter to one node. Each is an asset you can pull
185
+ down whichever way suits the caller:
186
+
187
+ ```ts
188
+ const out = job.getOutputs("13")[0];
189
+ await out.toFile("result.png"); // stream to disk
190
+ const data = await out.toBytes(); // buffer into memory
191
+ await out.toFile("head.png", { range: [0, 1023] }); // range-aware: first 1 KiB only
192
+ ```
193
+
194
+ `getDownloadUrl()` hands back a fetchable URL instead of streaming the bytes
195
+ through your process — give it to a browser, a CDN, or another service:
196
+
197
+ ```ts
198
+ const { url, expiresAt } = await out.getDownloadUrl();
199
+ ```
200
+
201
+ On Comfy Cloud / serverless it's a short-lived, **self-authorizing** signed
202
+ storage URL: whoever holds it can read the asset until `expiresAt` with no API
203
+ key of their own. On a self-hosted proxy it's the content endpoint (normal auth
204
+ still applies) and `expiresAt` is `null`. It works on every backend and never
205
+ downloads the bytes first.
206
+
176
207
  ## Typed errors
177
208
 
178
209
  Protocol-level failures are raised as one exception class per error code, so
@@ -66,6 +66,11 @@ const BY_CODE = {
66
66
  queue_full: QueueFull,
67
67
  insufficient_credits: InsufficientCredits,
68
68
  not_found: NotFound,
69
+ // public-api currently returns entity-specific 404 codes even though the spec
70
+ // documents the generic `not_found`; map them so callers still get a typed
71
+ // NotFound. (Server/spec reconciliation is a separate follow-up.)
72
+ job_not_found: NotFound,
73
+ asset_not_found: NotFound,
69
74
  unauthorized: Unauthorized,
70
75
  forbidden: Forbidden,
71
76
  };
@@ -372,7 +372,7 @@ export type PostJobsData = {
372
372
  [key: string]: unknown;
373
373
  };
374
374
  /**
375
- * Optional typed extras submitted alongside the workflow. Closed: only the enumerated properties are accepted.
375
+ * Per-prompt ComfyUI `extra_data`, same shape as Comfy Cloud and local ComfyUI. Closed object: only the enumerated keys are accepted, keeping the contract fully typed. Forwarded to the worker per-prompt, never persisted, and excluded from idempotency comparison.
376
376
  */
377
377
  extra_data?: {
378
378
  /**
@@ -446,6 +446,10 @@ export type GetJobErrors = {
446
446
  * `not_found`.
447
447
  */
448
448
  404: ErrorEnvelope;
449
+ /**
450
+ * `rate_limited` — the caller has exceeded the request rate limit for this account. Account/rate-scoped, not job-specific — this can be returned even for a job id the caller doesn't own or that doesn't exist, without revealing which.
451
+ */
452
+ 429: ErrorEnvelope;
449
453
  /**
450
454
  * `upstream_error` — an unexpected failure reaching or processing the request in this implementation's backing services. The message is always a generic, safe-to-display string; implementation detail (the specific upstream, its error text, transport failures) is never included here — see each implementation's own error-mapping notes. Every operation in this contract can fail this way.
451
455
  */
@@ -522,6 +526,10 @@ export type CancelJobErrors = {
522
526
  * `not_found`.
523
527
  */
524
528
  404: ErrorEnvelope;
529
+ /**
530
+ * `rate_limited` — the caller has exceeded the request rate limit for this account. Account/rate-scoped, not job-specific — this can be returned even for a job id the caller doesn't own or that doesn't exist, without revealing which.
531
+ */
532
+ 429: ErrorEnvelope;
525
533
  /**
526
534
  * `upstream_error` — an unexpected failure reaching or processing the request in this implementation's backing services. The message is always a generic, safe-to-display string; implementation detail (the specific upstream, its error text, transport failures) is never included here — see each implementation's own error-mapping notes. Every operation in this contract can fail this way.
527
535
  */
@@ -18,4 +18,4 @@ export * as schemas from "./generated/zod.gen.js";
18
18
  export * from "./models.js";
19
19
  export * from "./errors.js";
20
20
  export { type RawEvent, iterateSse } from "./sse.js";
21
- export { ComfyLow, OPERATION_IDS, OPERATION_METHODS, type ComfyLowOptions, type RequestOptions, } from "./transport.js";
21
+ export { ComfyLow, OPERATION_IDS, OPERATION_METHODS, type ComfyLowOptions, type RequestOptions, type AssetContentUrl, } from "./transport.js";
package/dist/low/sse.d.ts CHANGED
@@ -14,11 +14,24 @@ export interface RawEvent {
14
14
  event: string;
15
15
  data: Record<string, unknown>;
16
16
  }
17
+ /**
18
+ * Default read-idle timeout for the SSE byte stream. The server sends keepalive
19
+ * comments well within this window, so no *bytes at all* for this long means a
20
+ * silently-stalled ("zombie") connection — open but delivering nothing, not
21
+ * legitimately idle. We error the stream in that case so the caller's
22
+ * reconnect/poll fallback runs (see `Job.events`) instead of hanging forever.
23
+ * The timeout is at the byte level, not the parsed-message level, so keepalive
24
+ * comments (which `eventsource-parser` drops) still count as liveness.
25
+ */
26
+ export declare const SSE_IDLE_TIMEOUT_MS = 45000;
17
27
  /**
18
28
  * Decode a fetch response body into a stream of {@link RawEvent}s.
19
29
  *
20
30
  * No cursor/replay support by design — the contract carries no `id`/
21
31
  * `Last-Event-ID`; a blank `data:` frame (no payload) is dropped, matching
22
- * the Python decoder.
32
+ * the Python decoder. A read-idle timeout (default {@link SSE_IDLE_TIMEOUT_MS})
33
+ * errors a silently-stalled connection instead of blocking forever.
23
34
  */
24
- export declare function iterateSse(body: ReadableStream<Uint8Array>): AsyncGenerator<RawEvent, void, void>;
35
+ export declare function iterateSse(body: ReadableStream<Uint8Array>, options?: {
36
+ idleTimeoutMs?: number;
37
+ }): AsyncGenerator<RawEvent, void, void>;
package/dist/low/sse.js CHANGED
@@ -24,15 +24,48 @@ function parseData(raw) {
24
24
  }
25
25
  return { value: parsed };
26
26
  }
27
+ /**
28
+ * Default read-idle timeout for the SSE byte stream. The server sends keepalive
29
+ * comments well within this window, so no *bytes at all* for this long means a
30
+ * silently-stalled ("zombie") connection — open but delivering nothing, not
31
+ * legitimately idle. We error the stream in that case so the caller's
32
+ * reconnect/poll fallback runs (see `Job.events`) instead of hanging forever.
33
+ * The timeout is at the byte level, not the parsed-message level, so keepalive
34
+ * comments (which `eventsource-parser` drops) still count as liveness.
35
+ */
36
+ export const SSE_IDLE_TIMEOUT_MS = 45_000;
37
+ /** A pass-through stream that errors if no chunk arrives within `ms`. */
38
+ function idleTimeout(ms) {
39
+ let timer;
40
+ const clear = () => {
41
+ if (timer !== undefined)
42
+ clearTimeout(timer);
43
+ };
44
+ const arm = (controller) => {
45
+ clear();
46
+ timer = setTimeout(() => controller.error(new Error(`SSE idle timeout: no data for ${ms}ms`)), ms);
47
+ };
48
+ return new TransformStream({
49
+ start: arm,
50
+ transform(chunk, controller) {
51
+ arm(controller);
52
+ controller.enqueue(chunk);
53
+ },
54
+ flush: clear,
55
+ cancel: clear,
56
+ });
57
+ }
27
58
  /**
28
59
  * Decode a fetch response body into a stream of {@link RawEvent}s.
29
60
  *
30
61
  * No cursor/replay support by design — the contract carries no `id`/
31
62
  * `Last-Event-ID`; a blank `data:` frame (no payload) is dropped, matching
32
- * the Python decoder.
63
+ * the Python decoder. A read-idle timeout (default {@link SSE_IDLE_TIMEOUT_MS})
64
+ * errors a silently-stalled connection instead of blocking forever.
33
65
  */
34
- export async function* iterateSse(body) {
66
+ export async function* iterateSse(body, options = {}) {
35
67
  const stream = body
68
+ .pipeThrough(idleTimeout(options.idleTimeoutMs ?? SSE_IDLE_TIMEOUT_MS))
36
69
  .pipeThrough(new TextDecoderStream())
37
70
  .pipeThrough(new EventSourceParserStream());
38
71
  for await (const message of stream) {
@@ -38,10 +38,24 @@ export interface RequestOptions {
38
38
  * must not time out while idle mid-job).
39
39
  */
40
40
  timeoutMs?: number | null;
41
+ /** Defaults to `"follow"`; `getAssetContentUrl` passes `"manual"` so it can
42
+ * read a redirect's `Location` instead of following it. */
43
+ redirect?: "follow" | "manual" | "error";
41
44
  }
42
45
  export interface ComfyLowOptions {
43
46
  fetch?: typeof fetch;
44
47
  timeoutMs?: number;
48
+ /**
49
+ * Appended to the default `User-Agent` as `app/{clientInfo}` — lets an
50
+ * app built on this SDK attribute its own traffic in request logs.
51
+ * Opt-in; omitted by default.
52
+ */
53
+ clientInfo?: string;
54
+ }
55
+ /** Return shape of {@link ComfyLow.getAssetContentUrl}. */
56
+ export interface AssetContentUrl {
57
+ url: string;
58
+ expiresAt: Date | null;
45
59
  }
46
60
  /** Synchronous protocol bindings — async throughout (JS is async-native). */
47
61
  export declare class ComfyLow {
@@ -49,6 +63,7 @@ export declare class ComfyLow {
49
63
  private readonly apiKey?;
50
64
  private readonly fetchImpl;
51
65
  private readonly defaultTimeoutMs;
66
+ private readonly userAgent;
52
67
  constructor(baseUrl: string, apiKey?: string, options?: ComfyLowOptions);
53
68
  private urlFor;
54
69
  /**
@@ -100,6 +115,23 @@ export declare class ComfyLow {
100
115
  range?: readonly [number, number];
101
116
  signal?: AbortSignal;
102
117
  }): Promise<Response>;
118
+ /**
119
+ * `GET /api/v2/assets/{id}/content` with the redirect **not** followed —
120
+ * hands back a directly-fetchable URL instead of the bytes, so a caller
121
+ * (e.g. a serverless/Cloudflare Worker) can pass the URL to a downstream
122
+ * consumer without streaming the body through itself. On an object-storage
123
+ * backend (Cloud/serverless) the server redirects to a short-lived signed
124
+ * URL; that redirect is deliberately not followed (unlike
125
+ * {@link getAssetContent}), both so its `Location` can be read and so this
126
+ * client's bearer token is never attached to the object-storage host. On a
127
+ * self-hosted proxy (which serves the bytes inline, no redirect) this
128
+ * returns the endpoint's own absolute URL instead. Works on every backend
129
+ * and never throws for either shape — only a genuine failure maps to the
130
+ * usual typed error.
131
+ */
132
+ getAssetContentUrl(assetId: string, options?: {
133
+ signal?: AbortSignal;
134
+ }): Promise<AssetContentUrl>;
103
135
  /**
104
136
  * `POST /api/v2/jobs`. `extraData` (e.g. the partner-node API key) is a
105
137
  * sibling of `workflow` on the wire, per the spec's closed `extra_data`
@@ -29,9 +29,55 @@ import { errorFromEnvelope } from "./errors.js";
29
29
  import { iterateSse } from "./sse.js";
30
30
  const API_PREFIX = "/api/v2";
31
31
  const DEFAULT_TIMEOUT_MS = 30_000;
32
+ // Mirrors `version` in package.json. Hand-kept in sync: this package builds
33
+ // with plain `tsc` (no bundler/codegen step to inline it), and importing
34
+ // `../../package.json` directly would step outside `tsc`'s configured
35
+ // `rootDir`.
36
+ const SDK_VERSION = "0.1.0";
32
37
  function looksLikePath(value) {
33
38
  return value.startsWith("http") || value.startsWith("/");
34
39
  }
40
+ function buildUserAgent(clientInfo) {
41
+ const base = `comfy-sdk-typescript/${SDK_VERSION} (node ${process.version})`;
42
+ if (!clientInfo)
43
+ return base;
44
+ // A caller-set token goes verbatim into a header value; reject CR/LF so it
45
+ // can never split/inject headers (undici would reject it anyway, but fail
46
+ // fast with a clear message at construction).
47
+ if (/[\r\n]/.test(clientInfo)) {
48
+ throw new Error("clientInfo must not contain CR or LF characters");
49
+ }
50
+ return `${base} app/${clientInfo}`;
51
+ }
52
+ const GOOG_DATE_RE = /^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})Z$/;
53
+ /**
54
+ * Expiry off a GCS V4 signed URL's `X-Goog-Date` (`YYYYMMDDTHHMMSSZ`, UTC) +
55
+ * `X-Goog-Expires` (seconds) query params. `null` if either is missing or
56
+ * doesn't match — an unrecognized signed-URL flavor degrades to "unknown
57
+ * expiry" rather than throwing.
58
+ */
59
+ function parseExpiry(url) {
60
+ let parsed;
61
+ try {
62
+ parsed = new URL(url);
63
+ }
64
+ catch {
65
+ return null;
66
+ }
67
+ const dateParam = parsed.searchParams.get("X-Goog-Date");
68
+ const expiresParam = parsed.searchParams.get("X-Goog-Expires");
69
+ if (!dateParam || !expiresParam)
70
+ return null;
71
+ const match = GOOG_DATE_RE.exec(dateParam);
72
+ if (!match)
73
+ return null;
74
+ const expiresSeconds = Number.parseInt(expiresParam, 10);
75
+ if (Number.isNaN(expiresSeconds))
76
+ return null;
77
+ const [, year, month, day, hour, minute, second] = match;
78
+ const epochMs = Date.UTC(Number(year), Number(month) - 1, Number(day), Number(hour), Number(minute), Number(second));
79
+ return new Date(epochMs + expiresSeconds * 1000);
80
+ }
35
81
  function parseRetryAfter(response) {
36
82
  const raw = response.headers.get("Retry-After");
37
83
  if (raw === null)
@@ -45,11 +91,13 @@ export class ComfyLow {
45
91
  apiKey;
46
92
  fetchImpl;
47
93
  defaultTimeoutMs;
94
+ userAgent;
48
95
  constructor(baseUrl, apiKey, options = {}) {
49
96
  this.baseUrl = baseUrl.replace(/\/$/, "");
50
97
  this.apiKey = apiKey;
51
98
  this.fetchImpl = options.fetch ?? fetch;
52
99
  this.defaultTimeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
100
+ this.userAgent = buildUserAgent(options.clientInfo);
53
101
  }
54
102
  urlFor(path) {
55
103
  if (path.startsWith("http"))
@@ -90,6 +138,12 @@ export class ComfyLow {
90
138
  headers.set(key, value);
91
139
  }
92
140
  }
141
+ // Identifies this SDK's traffic in request logs (support + adoption
142
+ // metrics, no other data collected) — never overrides a caller-supplied
143
+ // header.
144
+ if (!headers.has("User-Agent")) {
145
+ headers.set("User-Agent", this.userAgent);
146
+ }
93
147
  return headers;
94
148
  }
95
149
  resolveSignal(callerSignal, timeoutMs) {
@@ -113,7 +167,13 @@ export class ComfyLow {
113
167
  body = JSON.stringify(options.json);
114
168
  }
115
169
  const signal = this.resolveSignal(options.signal, options.timeoutMs);
116
- return this.fetchImpl(url, { method, headers, body, signal, redirect: "follow" });
170
+ return this.fetchImpl(url, {
171
+ method,
172
+ headers,
173
+ body,
174
+ signal,
175
+ redirect: options.redirect ?? "follow",
176
+ });
117
177
  }
118
178
  async parseOrRaise(response, ok) {
119
179
  if (ok.includes(response.status)) {
@@ -137,8 +197,11 @@ export class ComfyLow {
137
197
  // -- assets -------------------------------------------------------------
138
198
  /** `POST /api/v2/assets` — streaming multipart upload. */
139
199
  async postAssets(file, contentType, filePath, options = {}) {
200
+ // public-api streams the multipart upload and requires the metadata fields
201
+ // BEFORE the file part (so it can route the file stream without buffering).
202
+ // The `file` part MUST be appended last, or the server rejects with 422
203
+ // "content_type is required and must be sent before the file field".
140
204
  const form = new FormData();
141
- form.append("file", file, filePath);
142
205
  form.append("content_type", contentType);
143
206
  form.append("file_path", filePath);
144
207
  if (options.expectedHash !== undefined) {
@@ -147,6 +210,7 @@ export class ComfyLow {
147
210
  for (const tag of options.tags ?? []) {
148
211
  form.append("tags", tag);
149
212
  }
213
+ form.append("file", file, filePath);
150
214
  const headers = {};
151
215
  if (options.idempotencyKey) {
152
216
  headers["Idempotency-Key"] = options.idempotencyKey;
@@ -209,6 +273,41 @@ export class ComfyLow {
209
273
  }
210
274
  return response;
211
275
  }
276
+ /**
277
+ * `GET /api/v2/assets/{id}/content` with the redirect **not** followed —
278
+ * hands back a directly-fetchable URL instead of the bytes, so a caller
279
+ * (e.g. a serverless/Cloudflare Worker) can pass the URL to a downstream
280
+ * consumer without streaming the body through itself. On an object-storage
281
+ * backend (Cloud/serverless) the server redirects to a short-lived signed
282
+ * URL; that redirect is deliberately not followed (unlike
283
+ * {@link getAssetContent}), both so its `Location` can be read and so this
284
+ * client's bearer token is never attached to the object-storage host. On a
285
+ * self-hosted proxy (which serves the bytes inline, no redirect) this
286
+ * returns the endpoint's own absolute URL instead. Works on every backend
287
+ * and never throws for either shape — only a genuine failure maps to the
288
+ * usual typed error.
289
+ */
290
+ async getAssetContentUrl(assetId, options = {}) {
291
+ const path = `/assets/${encodeURIComponent(assetId)}/content`;
292
+ const response = await this.request("GET", path, {
293
+ signal: options.signal,
294
+ redirect: "manual",
295
+ });
296
+ const location = response.headers.get("Location");
297
+ // Node/undici hands back the real 3xx status with a readable `Location`
298
+ // for a manual redirect; status 0 covers a browser's opaque redirect.
299
+ // We only want the URL, not the bytes — release the response body so
300
+ // undici can reuse the connection instead of pinning it until GC.
301
+ if (location && (response.status === 0 || (response.status >= 300 && response.status < 400))) {
302
+ await response.body?.cancel();
303
+ return { url: location, expiresAt: parseExpiry(location) };
304
+ }
305
+ if (response.status === 200 || response.status === 206) {
306
+ await response.body?.cancel();
307
+ return { url: this.urlFor(path), expiresAt: null };
308
+ }
309
+ return this.parseOrRaise(response, [200, 206]); // always throws here
310
+ }
212
311
  // -- jobs -----------------------------------------------------------------
213
312
  /**
214
313
  * `POST /api/v2/jobs`. `extraData` (e.g. the partner-node API key) is a
@@ -37,6 +37,9 @@ export interface ComfyOptions {
37
37
  apiKey?: string;
38
38
  timeoutMs?: number;
39
39
  fetch?: ComfyLowOptions["fetch"];
40
+ /** Appended to the SDK's default `User-Agent` as `app/{clientInfo}` — lets
41
+ * an app built on this SDK attribute its own traffic in request logs. */
42
+ clientInfo?: string;
40
43
  }
41
44
  export declare class Comfy {
42
45
  private readonly low;
@@ -52,6 +52,7 @@ export class Comfy {
52
52
  this.low = new ComfyLow(baseUrl, options.apiKey, {
53
53
  timeoutMs: options.timeoutMs,
54
54
  fetch: options.fetch,
55
+ clientInfo: options.clientInfo,
55
56
  });
56
57
  this.assets = new AssetFactory(this.low);
57
58
  this.workflows = new WorkflowFactory();
@@ -79,6 +79,11 @@ const BY_CODE = {
79
79
  idempotency_key_reuse: IdempotencyKeyReuse,
80
80
  insufficient_credits: InsufficientCredits,
81
81
  not_found: NotFound,
82
+ // public-api returns entity-specific 404 codes even though the spec documents
83
+ // the generic not_found; map them so a missing job/asset raises the typed
84
+ // NotFound. (Server/spec reconciliation of the code set is a separate follow-up.)
85
+ job_not_found: NotFound,
86
+ asset_not_found: NotFound,
82
87
  unauthorized: Unauthorized,
83
88
  forbidden: Forbidden,
84
89
  };
@@ -23,4 +23,18 @@ export declare class Output {
23
23
  toBytes(options?: {
24
24
  range?: readonly [number, number];
25
25
  }): Promise<Uint8Array>;
26
+ /**
27
+ * A directly-fetchable URL for this output's bytes — a short-lived,
28
+ * self-authorizing bearer credential (readable until `expiresAt`) — so a
29
+ * caller (e.g. a serverless/Cloudflare Worker) can hand the URL to a
30
+ * downstream consumer instead of streaming the bytes through itself. On an
31
+ * object-storage backend (Cloud/serverless) this is a signed URL and
32
+ * `expiresAt` is set; on a self-hosted proxy (which serves the bytes
33
+ * inline) it's this asset's own content URL and `expiresAt` is `null`.
34
+ * Works on every backend and never throws.
35
+ */
36
+ getDownloadUrl(): Promise<{
37
+ url: string;
38
+ expiresAt: Date | null;
39
+ }>;
26
40
  }
@@ -45,4 +45,17 @@ export class Output {
45
45
  const response = await this.low.getAssetContent(this.model.id, { range: options.range });
46
46
  return new Uint8Array(await response.arrayBuffer());
47
47
  }
48
+ /**
49
+ * A directly-fetchable URL for this output's bytes — a short-lived,
50
+ * self-authorizing bearer credential (readable until `expiresAt`) — so a
51
+ * caller (e.g. a serverless/Cloudflare Worker) can hand the URL to a
52
+ * downstream consumer instead of streaming the bytes through itself. On an
53
+ * object-storage backend (Cloud/serverless) this is a signed URL and
54
+ * `expiresAt` is set; on a self-hosted proxy (which serves the bytes
55
+ * inline) it's this asset's own content URL and `expiresAt` is `null`.
56
+ * Works on every backend and never throws.
57
+ */
58
+ async getDownloadUrl() {
59
+ return this.low.getAssetContentUrl(this.model.id);
60
+ }
48
61
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@comfyorg/sdk",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "private": false,
5
5
  "description": "TypeScript SDK for running ComfyUI workflows via the Comfy API v2 (self-hosted, Comfy Cloud, serverless).",
6
6
  "homepage": "https://github.com/Comfy-Org/ComfyTypeScriptSDK#readme",