@sogni-ai/sogni-client 4.2.0-alpha.21 → 4.2.0-alpha.23

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.
Files changed (178) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/CLAUDE.md +7 -0
  3. package/README.md +45 -25
  4. package/dist/Chat/ChatTools.js +30 -16
  5. package/dist/Chat/ChatTools.js.map +1 -1
  6. package/dist/Chat/hostedToolValidation.generated.d.ts +46 -0
  7. package/dist/Chat/hostedToolValidation.generated.js +306 -0
  8. package/dist/Chat/hostedToolValidation.generated.js.map +1 -0
  9. package/dist/Chat/index.d.ts +41 -1
  10. package/dist/Chat/index.js +285 -26
  11. package/dist/Chat/index.js.map +1 -1
  12. package/dist/Chat/modelRouting.d.ts +8 -26
  13. package/dist/Chat/modelRouting.js +11 -106
  14. package/dist/Chat/modelRouting.js.map +1 -1
  15. package/dist/Chat/sogniHostedTools.generated.json +1596 -304
  16. package/dist/Chat/tools.d.ts +52 -3
  17. package/dist/Chat/tools.js +80 -27
  18. package/dist/Chat/tools.js.map +1 -1
  19. package/dist/Chat/types.d.ts +119 -3
  20. package/dist/CreativeWorkflows/index.d.ts +1 -7
  21. package/dist/CreativeWorkflows/index.js +60 -21
  22. package/dist/CreativeWorkflows/index.js.map +1 -1
  23. package/dist/CreativeWorkflows/types.d.ts +28 -33
  24. package/dist/Projects/index.d.ts +49 -8
  25. package/dist/Projects/index.js +49 -8
  26. package/dist/Projects/index.js.map +1 -1
  27. package/dist/index.d.ts +19 -4
  28. package/dist/index.js +1 -1
  29. package/dist/index.js.map +1 -1
  30. package/dist/lib/Cache.js +6 -1
  31. package/dist/lib/Cache.js.map +1 -1
  32. package/dist/lib/mediaValidation.d.ts +3 -0
  33. package/dist/lib/mediaValidation.js +154 -0
  34. package/dist/lib/mediaValidation.js.map +1 -1
  35. package/dist-esm/Account/CurrentAccount.js +59 -0
  36. package/dist-esm/Account/CurrentAccount.js.map +1 -0
  37. package/dist-esm/Account/index.js +543 -0
  38. package/dist-esm/Account/index.js.map +1 -0
  39. package/dist-esm/Account/types.js +2 -0
  40. package/dist-esm/Account/types.js.map +1 -0
  41. package/dist-esm/ApiClient/WebSocketClient/BrowserWebSocketClient/ChannelCoordinator.js +327 -0
  42. package/dist-esm/ApiClient/WebSocketClient/BrowserWebSocketClient/ChannelCoordinator.js.map +1 -0
  43. package/dist-esm/ApiClient/WebSocketClient/BrowserWebSocketClient/index.js +222 -0
  44. package/dist-esm/ApiClient/WebSocketClient/BrowserWebSocketClient/index.js.map +1 -0
  45. package/dist-esm/ApiClient/WebSocketClient/ErrorCode.js +15 -0
  46. package/dist-esm/ApiClient/WebSocketClient/ErrorCode.js.map +1 -0
  47. package/dist-esm/ApiClient/WebSocketClient/eventSubscriptions.js +35 -0
  48. package/dist-esm/ApiClient/WebSocketClient/eventSubscriptions.js.map +1 -0
  49. package/dist-esm/ApiClient/WebSocketClient/events.js +2 -0
  50. package/dist-esm/ApiClient/WebSocketClient/events.js.map +1 -0
  51. package/dist-esm/ApiClient/WebSocketClient/index.js +221 -0
  52. package/dist-esm/ApiClient/WebSocketClient/index.js.map +1 -0
  53. package/dist-esm/ApiClient/WebSocketClient/messages.js +2 -0
  54. package/dist-esm/ApiClient/WebSocketClient/messages.js.map +1 -0
  55. package/dist-esm/ApiClient/WebSocketClient/types.js +2 -0
  56. package/dist-esm/ApiClient/WebSocketClient/types.js.map +1 -0
  57. package/dist-esm/ApiClient/events.js +2 -0
  58. package/dist-esm/ApiClient/events.js.map +1 -0
  59. package/dist-esm/ApiClient/index.js +120 -0
  60. package/dist-esm/ApiClient/index.js.map +1 -0
  61. package/dist-esm/ApiGroup.js +10 -0
  62. package/dist-esm/ApiGroup.js.map +1 -0
  63. package/dist-esm/Chat/ChatStream.js +181 -0
  64. package/dist-esm/Chat/ChatStream.js.map +1 -0
  65. package/dist-esm/Chat/ChatTools.js +562 -0
  66. package/dist-esm/Chat/ChatTools.js.map +1 -0
  67. package/dist-esm/Chat/hostedToolValidation.generated.js +301 -0
  68. package/dist-esm/Chat/hostedToolValidation.generated.js.map +1 -0
  69. package/dist-esm/Chat/index.js +693 -0
  70. package/dist-esm/Chat/index.js.map +1 -0
  71. package/dist-esm/Chat/modelRouting.js +326 -0
  72. package/dist-esm/Chat/modelRouting.js.map +1 -0
  73. package/dist-esm/Chat/sogniHostedTools.generated.json +1821 -0
  74. package/dist-esm/Chat/tools.js +149 -0
  75. package/dist-esm/Chat/tools.js.map +1 -0
  76. package/dist-esm/Chat/types.js +2 -0
  77. package/dist-esm/Chat/types.js.map +1 -0
  78. package/dist-esm/CreativeWorkflows/index.js +293 -0
  79. package/dist-esm/CreativeWorkflows/index.js.map +1 -0
  80. package/dist-esm/CreativeWorkflows/types.js +2 -0
  81. package/dist-esm/CreativeWorkflows/types.js.map +1 -0
  82. package/dist-esm/Projects/Job.js +391 -0
  83. package/dist-esm/Projects/Job.js.map +1 -0
  84. package/dist-esm/Projects/Project.js +295 -0
  85. package/dist-esm/Projects/Project.js.map +1 -0
  86. package/dist-esm/Projects/createJobRequestMessage.js +528 -0
  87. package/dist-esm/Projects/createJobRequestMessage.js.map +1 -0
  88. package/dist-esm/Projects/index.js +1350 -0
  89. package/dist-esm/Projects/index.js.map +1 -0
  90. package/dist-esm/Projects/types/ComfySamplerParams.js +2 -0
  91. package/dist-esm/Projects/types/ComfySamplerParams.js.map +1 -0
  92. package/dist-esm/Projects/types/ControlNetParams.js +2 -0
  93. package/dist-esm/Projects/types/ControlNetParams.js.map +1 -0
  94. package/dist-esm/Projects/types/EstimationResponse.js +2 -0
  95. package/dist-esm/Projects/types/EstimationResponse.js.map +1 -0
  96. package/dist-esm/Projects/types/ModelOptions.js +89 -0
  97. package/dist-esm/Projects/types/ModelOptions.js.map +1 -0
  98. package/dist-esm/Projects/types/ModelTiersRaw.js +13 -0
  99. package/dist-esm/Projects/types/ModelTiersRaw.js.map +1 -0
  100. package/dist-esm/Projects/types/RawProject.js +2 -0
  101. package/dist-esm/Projects/types/RawProject.js.map +1 -0
  102. package/dist-esm/Projects/types/events.js +2 -0
  103. package/dist-esm/Projects/types/events.js.map +1 -0
  104. package/dist-esm/Projects/types/index.js +10 -0
  105. package/dist-esm/Projects/types/index.js.map +1 -0
  106. package/dist-esm/Projects/utils/index.js +229 -0
  107. package/dist-esm/Projects/utils/index.js.map +1 -0
  108. package/dist-esm/Projects/utils/samplers.js +36 -0
  109. package/dist-esm/Projects/utils/samplers.js.map +1 -0
  110. package/dist-esm/Projects/utils/scheduler.js +27 -0
  111. package/dist-esm/Projects/utils/scheduler.js.map +1 -0
  112. package/dist-esm/Stats/index.js +20 -0
  113. package/dist-esm/Stats/index.js.map +1 -0
  114. package/dist-esm/Stats/types.js +2 -0
  115. package/dist-esm/Stats/types.js.map +1 -0
  116. package/dist-esm/events.js +2 -0
  117. package/dist-esm/events.js.map +1 -0
  118. package/dist-esm/index.js +144 -0
  119. package/dist-esm/index.js.map +1 -0
  120. package/dist-esm/lib/AuthManager/ApiKeyAuthManager.js +58 -0
  121. package/dist-esm/lib/AuthManager/ApiKeyAuthManager.js.map +1 -0
  122. package/dist-esm/lib/AuthManager/AuthManagerBase.js +9 -0
  123. package/dist-esm/lib/AuthManager/AuthManagerBase.js.map +1 -0
  124. package/dist-esm/lib/AuthManager/CookieAuthManager.js +48 -0
  125. package/dist-esm/lib/AuthManager/CookieAuthManager.js.map +1 -0
  126. package/dist-esm/lib/AuthManager/TokenAuthManager.js +186 -0
  127. package/dist-esm/lib/AuthManager/TokenAuthManager.js.map +1 -0
  128. package/dist-esm/lib/AuthManager/index.js +5 -0
  129. package/dist-esm/lib/AuthManager/index.js.map +1 -0
  130. package/dist-esm/lib/Cache.js +32 -0
  131. package/dist-esm/lib/Cache.js.map +1 -0
  132. package/dist-esm/lib/DataEntity.js +30 -0
  133. package/dist-esm/lib/DataEntity.js.map +1 -0
  134. package/dist-esm/lib/DefaultLogger.js +32 -0
  135. package/dist-esm/lib/DefaultLogger.js.map +1 -0
  136. package/dist-esm/lib/EIP712Helper.js +37 -0
  137. package/dist-esm/lib/EIP712Helper.js.map +1 -0
  138. package/dist-esm/lib/RestClient.js +90 -0
  139. package/dist-esm/lib/RestClient.js.map +1 -0
  140. package/dist-esm/lib/TypedEventEmitter.js +68 -0
  141. package/dist-esm/lib/TypedEventEmitter.js.map +1 -0
  142. package/dist-esm/lib/base64.js +18 -0
  143. package/dist-esm/lib/base64.js.map +1 -0
  144. package/dist-esm/lib/getUUID.js +7 -0
  145. package/dist-esm/lib/getUUID.js.map +1 -0
  146. package/dist-esm/lib/isNodejs.js +3 -0
  147. package/dist-esm/lib/isNodejs.js.map +1 -0
  148. package/dist-esm/lib/mediaValidation.js +430 -0
  149. package/dist-esm/lib/mediaValidation.js.map +1 -0
  150. package/dist-esm/lib/utils/index.js +30 -0
  151. package/dist-esm/lib/utils/index.js.map +1 -0
  152. package/dist-esm/lib/validation.js +138 -0
  153. package/dist-esm/lib/validation.js.map +1 -0
  154. package/dist-esm/package.json +3 -0
  155. package/dist-esm/types/ErrorData.js +2 -0
  156. package/dist-esm/types/ErrorData.js.map +1 -0
  157. package/dist-esm/types/json.js +2 -0
  158. package/dist-esm/types/json.js.map +1 -0
  159. package/dist-esm/types/token.js +2 -0
  160. package/dist-esm/types/token.js.map +1 -0
  161. package/dist-esm/version.js +3 -0
  162. package/dist-esm/version.js.map +1 -0
  163. package/llms-full.txt +11 -20
  164. package/llms.txt +9 -18
  165. package/package.json +21 -5
  166. package/src/Chat/ChatTools.ts +36 -16
  167. package/src/Chat/hostedToolValidation.generated.ts +453 -0
  168. package/src/Chat/index.ts +322 -37
  169. package/src/Chat/modelRouting.ts +26 -150
  170. package/src/Chat/sogniHostedTools.generated.json +1596 -304
  171. package/src/Chat/tools.ts +106 -41
  172. package/src/Chat/types.ts +131 -3
  173. package/src/CreativeWorkflows/index.ts +55 -41
  174. package/src/CreativeWorkflows/types.ts +70 -42
  175. package/src/Projects/index.ts +49 -8
  176. package/src/index.ts +33 -12
  177. package/src/lib/Cache.ts +8 -1
  178. package/src/lib/mediaValidation.ts +155 -0
@@ -1223,8 +1223,26 @@ class ProjectsApi extends ApiGroup<ProjectApiEvents> {
1223
1223
  // ============================================
1224
1224
 
1225
1225
  /**
1226
- * Get upload URL for image
1227
- * @internal
1226
+ * Request a presigned upload URL for an image asset (reference image,
1227
+ * starting image, ControlNet image, context image, etc.). The caller
1228
+ * uploads the image bytes via `PUT` to the returned URL before
1229
+ * starting a project or workflow that references the asset.
1230
+ *
1231
+ * @param {ImageUrlParams} params - Image asset coordinates:
1232
+ * - imageId: Stable identifier for the asset within the job
1233
+ * (e.g. `"media_ref_1"`). The same id is later used to reference
1234
+ * the asset in workflow inputs.
1235
+ * - jobId: Caller-generated job/correlation id (e.g.
1236
+ * `"sogni-agent-1735000000-1-abcdef"`). Ties the asset to a
1237
+ * specific request.
1238
+ * - type: Asset role. Supported values include `'referenceImage'`,
1239
+ * `'referenceImageEnd'`, `'startingImage'`, `'cnImage'`,
1240
+ * `'contextImage1'`..`'contextImage16'`, `'preview'`, `'complete'`.
1241
+ * - contentType: Optional MIME type the caller will `PUT` (e.g.
1242
+ * `"image/png"`). Forwarded so the storage layer can pin the
1243
+ * Content-Type on the presigned URL.
1244
+ * @return {Promise<string>} Presigned `PUT` URL the caller should
1245
+ * upload the image bytes to. Short-lived; use immediately.
1228
1246
  */
1229
1247
  async uploadUrl(params: ImageUrlParams) {
1230
1248
  const r = await this.client.rest.get<ApiResponse<{ uploadUrl: string }>>(
@@ -1235,8 +1253,13 @@ class ProjectsApi extends ApiGroup<ProjectApiEvents> {
1235
1253
  }
1236
1254
 
1237
1255
  /**
1238
- * Get download URL for image
1239
- * @internal
1256
+ * Request a presigned download URL for a stored image asset.
1257
+ *
1258
+ * @param {ImageUrlParams} params - Same shape as
1259
+ * {@link ProjectsApi.uploadUrl}; `imageId`, `jobId`, and `type`
1260
+ * must match the values used at upload time.
1261
+ * @return {Promise<string>} Presigned `GET` URL for the image.
1262
+ * Throws if the server response does not include a `downloadUrl`.
1240
1263
  */
1241
1264
  async downloadUrl(params: ImageUrlParams) {
1242
1265
  const r = await this.client.rest.get<ApiResponse<{ downloadUrl: string }>>(
@@ -1250,8 +1273,21 @@ class ProjectsApi extends ApiGroup<ProjectApiEvents> {
1250
1273
  }
1251
1274
 
1252
1275
  /**
1253
- * Get upload URL for media (video/audio)
1254
- * @internal
1276
+ * Request a presigned upload URL for an audio or video asset
1277
+ * (reference audio, reference video, finished media artifacts, etc.).
1278
+ * The caller uploads the media bytes via `PUT` to the returned URL
1279
+ * before starting a project or workflow that references the asset.
1280
+ *
1281
+ * @param {MediaUrlParams} params - Media asset coordinates:
1282
+ * - id: Stable identifier for the asset within the job
1283
+ * (e.g. `"media_ref_1"`). Optional for some asset roles.
1284
+ * - jobId: Caller-generated job/correlation id.
1285
+ * - type: Asset role. Supported values are `'referenceAudio'`,
1286
+ * `'referenceVideo'`, `'preview'`, `'complete'`.
1287
+ * - contentType: Optional MIME type the caller will `PUT`
1288
+ * (e.g. `"audio/mp4"` or `"video/mp4"`).
1289
+ * @return {Promise<string>} Presigned `PUT` URL the caller should
1290
+ * upload the media bytes to.
1255
1291
  */
1256
1292
  async mediaUploadUrl(params: MediaUrlParams) {
1257
1293
  const r = await this.client.rest.get<ApiResponse<{ uploadUrl: string }>>(
@@ -1262,8 +1298,13 @@ class ProjectsApi extends ApiGroup<ProjectApiEvents> {
1262
1298
  }
1263
1299
 
1264
1300
  /**
1265
- * Get download URL for media (video/audio)
1266
- * @internal
1301
+ * Request a presigned download URL for a stored audio or video asset.
1302
+ *
1303
+ * @param {MediaUrlParams} params - Same shape as
1304
+ * {@link ProjectsApi.mediaUploadUrl}; `id`, `jobId`, and `type`
1305
+ * must match the values used at upload time.
1306
+ * @return {Promise<string>} Presigned `GET` URL for the media.
1307
+ * Throws if the server response does not include a `downloadUrl`.
1267
1308
  */
1268
1309
  async mediaDownloadUrl(params: MediaUrlParams) {
1269
1310
  const r = await this.client.rest.get<ApiResponse<{ downloadUrl: string }>>(
package/src/index.ts CHANGED
@@ -53,6 +53,11 @@ import {
53
53
  ChatCompletionResult,
54
54
  ChatJobStateEvent,
55
55
  ChatResponseFormat,
56
+ ChatRunEvent,
57
+ ChatRunRecord,
58
+ ChatRunStatus,
59
+ StartChatRunParams,
60
+ StreamChatRunEventsOptions,
56
61
  ContentPart,
57
62
  HostedChatCompletionParams,
58
63
  HostedChatCompletionResult,
@@ -85,7 +90,6 @@ import CreativeWorkflowsApi, { parseCreativeWorkflowSseChunk } from './CreativeW
85
90
  import {
86
91
  CreativeWorkflowArtifact,
87
92
  CreativeWorkflowEvent,
88
- CreativeWorkflowKind,
89
93
  CreativeWorkflowRecord,
90
94
  CreativeWorkflowSseEvent,
91
95
  CreativeWorkflowStatus,
@@ -93,10 +97,9 @@ import {
93
97
  ListCreativeWorkflowOptions,
94
98
  StartCreativeWorkflowOptions,
95
99
  StartCreativeWorkflowParams,
96
- StartHostedToolSequenceWorkflowDependency,
97
- StartHostedToolSequenceWorkflowInput,
98
- StartHostedToolSequenceWorkflowStep,
99
- StartImageToVideoWorkflowInput,
100
+ StartCreativeWorkflowDependency,
101
+ StartCreativeWorkflowInput,
102
+ StartCreativeWorkflowStep,
100
103
  StreamCreativeWorkflowEventsOptions
101
104
  } from './CreativeWorkflows/types';
102
105
  // Stats API
@@ -123,7 +126,12 @@ export type {
123
126
  ChatJobStateEvent,
124
127
  ChatMessage,
125
128
  ChatResponseFormat,
129
+ ChatRunEvent,
130
+ ChatRunRecord,
131
+ ChatRunStatus,
126
132
  ChatTokenUsage,
133
+ StartChatRunParams,
134
+ StreamChatRunEventsOptions,
127
135
  ContentPart,
128
136
  HostedChatCompletionChoice,
129
137
  HostedChatCompletionMessage,
@@ -132,7 +140,6 @@ export type {
132
140
  HostedCreativeWorkflowReference,
133
141
  CreativeWorkflowArtifact,
134
142
  CreativeWorkflowEvent,
135
- CreativeWorkflowKind,
136
143
  CreativeWorkflowRecord,
137
144
  CreativeWorkflowSseEvent,
138
145
  CreativeWorkflowStatus,
@@ -158,10 +165,9 @@ export type {
158
165
  CreativeWorkflowHostedToolName,
159
166
  StartCreativeWorkflowOptions,
160
167
  StartCreativeWorkflowParams,
161
- StartHostedToolSequenceWorkflowDependency,
162
- StartHostedToolSequenceWorkflowInput,
163
- StartHostedToolSequenceWorkflowStep,
164
- StartImageToVideoWorkflowInput,
168
+ StartCreativeWorkflowDependency,
169
+ StartCreativeWorkflowInput,
170
+ StartCreativeWorkflowStep,
165
171
  StreamCreativeWorkflowEventsOptions,
166
172
  SupernetType,
167
173
  TokenType,
@@ -292,8 +298,23 @@ export class SogniClient {
292
298
  account: AccountApi;
293
299
  projects: ProjectsApi;
294
300
  stats: StatsApi;
301
+ /**
302
+ * Chat surfaces.
303
+ * - `chat.completions.create` — socket-native chat completion.
304
+ * - `chat.hosted.create` — hosted REST chat completion (synchronous).
305
+ * - `chat.runs.{create, get, cancel, streamEvents}` — durable hosted
306
+ * chat runs that survive client disconnect, browser close, network
307
+ * drop, and API worker restart. See `/v1/chat/runs` REST surface.
308
+ * - `chat.tools.execute*` — execute Sogni platform tools surfaced by
309
+ * the chat completion (image / video / music generation).
310
+ */
295
311
  chat: ChatApi;
296
- creativeWorkflows: CreativeWorkflowsApi;
312
+ /**
313
+ * Durable creative workflows (`/v1/creative-agent/workflows`). Submit
314
+ * an explicit step sequence and follow its progress without keeping
315
+ * the client connected.
316
+ */
317
+ workflows: CreativeWorkflowsApi;
297
318
 
298
319
  apiClient: ApiClient;
299
320
 
@@ -302,7 +323,7 @@ export class SogniClient {
302
323
  this.projects = new ProjectsApi(config);
303
324
  this.stats = new StatsApi(config);
304
325
  this.chat = new ChatApi(config, this.projects);
305
- this.creativeWorkflows = new CreativeWorkflowsApi(config);
326
+ this.workflows = new CreativeWorkflowsApi(config);
306
327
 
307
328
  this.apiClient = config.client;
308
329
  }
package/src/lib/Cache.ts CHANGED
@@ -10,7 +10,14 @@ export default class Cache<V = any> {
10
10
 
11
11
  constructor(defaultTTL: number) {
12
12
  this.ttl = defaultTTL;
13
- setInterval(() => this.cleanup(), 10000);
13
+ const timer = setInterval(() => this.cleanup(), 10000) as unknown as {
14
+ unref?: () => void;
15
+ };
16
+ // Node timers have unref() so a module-level Cache doesn't keep one-shot
17
+ // scripts alive. Browser timers return numbers and don't expose unref.
18
+ if (typeof timer.unref === 'function') {
19
+ timer.unref();
20
+ }
14
21
  }
15
22
 
16
23
  write(key: string, value: V, ttl?: number) {
@@ -8,6 +8,8 @@ export interface ImageDimensions {
8
8
  export interface InlineMediaValidationOptions {
9
9
  maxBytes?: number;
10
10
  maxImageLongestSide?: number;
11
+ remoteFetchTimeoutMs?: number;
12
+ signal?: AbortSignal;
11
13
  }
12
14
 
13
15
  export interface ParsedInlineMediaData {
@@ -320,6 +322,109 @@ function validateMagicBytes(
320
322
  return undefined;
321
323
  }
322
324
 
325
+ function mimeTypeForDetectedFormat(mediaType: MediaType, bytes: Uint8Array): string {
326
+ if (mediaType === 'image') {
327
+ const format = detectImageFormat(bytes);
328
+ if (format === 'png') return 'image/png';
329
+ if (format === 'jpeg') return 'image/jpeg';
330
+ } else if (mediaType === 'audio') {
331
+ const format = detectAudioFormat(bytes);
332
+ if (format === 'wav') return 'audio/wav';
333
+ if (format === 'mpeg') return 'audio/mpeg';
334
+ if (format === 'mp4') return 'audio/mp4';
335
+ } else {
336
+ const format = detectVideoFormat(bytes);
337
+ if (format === 'quicktime') return 'video/quicktime';
338
+ if (format === 'mp4') return 'video/mp4';
339
+ }
340
+ throw new Error(
341
+ `Remote ${mediaType} input is not a supported format. Allowed types: ${getAllowedMimeTypes(mediaType).join(', ')}`
342
+ );
343
+ }
344
+
345
+ function normalizeResponseMimeType(
346
+ mediaType: MediaType,
347
+ contentType: string | null,
348
+ bytes: Uint8Array
349
+ ): string {
350
+ const mimeType = contentType?.split(';')[0]?.trim().toLowerCase() || '';
351
+ if (getAllowedMimeTypes(mediaType).includes(mimeType)) return mimeType;
352
+ return mimeTypeForDetectedFormat(mediaType, bytes);
353
+ }
354
+
355
+ function throwMediaByteLimit(mediaType: MediaType, maxBytes: number): never {
356
+ throw new Error(`${mediaType} input exceeds ${Math.round(maxBytes / (1024 * 1024))}MB limit`);
357
+ }
358
+
359
+ async function readResponseBytes(
360
+ response: Response,
361
+ mediaType: MediaType,
362
+ maxBytes: number | undefined
363
+ ): Promise<Uint8Array> {
364
+ if (!response.body) {
365
+ const bytes = new Uint8Array(await response.arrayBuffer());
366
+ if (maxBytes !== undefined && bytes.length > maxBytes) {
367
+ throwMediaByteLimit(mediaType, maxBytes);
368
+ }
369
+ return bytes;
370
+ }
371
+
372
+ const reader = response.body.getReader();
373
+ const chunks: Uint8Array[] = [];
374
+ let totalBytes = 0;
375
+
376
+ try {
377
+ while (true) {
378
+ const { value, done } = await reader.read();
379
+ if (done) break;
380
+ if (!value) continue;
381
+
382
+ totalBytes += value.byteLength;
383
+ if (maxBytes !== undefined && totalBytes > maxBytes) {
384
+ try {
385
+ await reader.cancel();
386
+ } catch {
387
+ // Preserve the size-limit error below.
388
+ }
389
+ throwMediaByteLimit(mediaType, maxBytes);
390
+ }
391
+ chunks.push(value);
392
+ }
393
+ } finally {
394
+ reader.releaseLock();
395
+ }
396
+
397
+ const bytes = new Uint8Array(totalBytes);
398
+ let offset = 0;
399
+ for (const chunk of chunks) {
400
+ bytes.set(chunk, offset);
401
+ offset += chunk.byteLength;
402
+ }
403
+ return bytes;
404
+ }
405
+
406
+ const DEFAULT_REMOTE_MEDIA_FETCH_TIMEOUT_MS = 20_000;
407
+
408
+ function trustedRemoteMediaUrl(value: string): URL | null {
409
+ try {
410
+ const url = new URL(value.trim());
411
+ if (url.protocol !== 'https:') return null;
412
+ if (url.username || url.password) return null;
413
+ const host = url.hostname.toLowerCase();
414
+ const trusted =
415
+ host === 'cdn.sogni.ai' ||
416
+ host.endsWith('.sogni.ai') ||
417
+ host === 'complete-images-production.s3.amazonaws.com' ||
418
+ /^[a-z0-9.-]+\.s3\.amazonaws\.com$/i.test(host) ||
419
+ /^[a-z0-9.-]+\.s3\.[a-z0-9-]+\.amazonaws\.com$/i.test(host) ||
420
+ /^s3\.[a-z0-9-]+\.amazonaws\.com$/i.test(host) ||
421
+ host.endsWith('.cloudfront.net');
422
+ return trusted ? url : null;
423
+ } catch {
424
+ return null;
425
+ }
426
+ }
427
+
323
428
  function validateImageDimensions(dimensions: ImageDimensions, maxLongestSide: number): void {
324
429
  if (Math.max(dimensions.width, dimensions.height) > maxLongestSide) {
325
430
  throw new Error(
@@ -365,3 +470,53 @@ export function parseInlineMediaDataUri(
365
470
  imageDimensions
366
471
  };
367
472
  }
473
+
474
+ export async function mediaInputToInlineDataUri(
475
+ input: string,
476
+ mediaType: MediaType,
477
+ options: InlineMediaValidationOptions = {}
478
+ ): Promise<string> {
479
+ const trimmed = input.trim();
480
+ const remoteUrl = trustedRemoteMediaUrl(trimmed);
481
+ if (!remoteUrl) {
482
+ parseInlineMediaDataUri(trimmed, mediaType, options);
483
+ return trimmed;
484
+ }
485
+
486
+ const controller = new AbortController();
487
+ const timeoutMs = options.remoteFetchTimeoutMs ?? DEFAULT_REMOTE_MEDIA_FETCH_TIMEOUT_MS;
488
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
489
+ const abortFromParent = () => controller.abort();
490
+ options.signal?.addEventListener('abort', abortFromParent, { once: true });
491
+ try {
492
+ if (options.signal?.aborted) controller.abort();
493
+ const response = await fetch(remoteUrl.toString(), {
494
+ signal: controller.signal,
495
+ redirect: 'error'
496
+ });
497
+ if (!response.ok) {
498
+ throw new Error(`Remote ${mediaType} input fetch failed with HTTP ${response.status}`);
499
+ }
500
+ const contentLength = Number(response.headers.get('content-length') || '');
501
+ if (
502
+ options.maxBytes !== undefined &&
503
+ Number.isFinite(contentLength) &&
504
+ contentLength > options.maxBytes
505
+ ) {
506
+ throwMediaByteLimit(mediaType, options.maxBytes);
507
+ }
508
+
509
+ const bytes = await readResponseBytes(response, mediaType, options.maxBytes);
510
+ const mimeType = normalizeResponseMimeType(
511
+ mediaType,
512
+ response.headers.get('content-type'),
513
+ bytes
514
+ );
515
+ const dataUri = `data:${mimeType};base64,${encodeBase64(bytes)}`;
516
+ parseInlineMediaDataUri(dataUri, mediaType, options);
517
+ return dataUri;
518
+ } finally {
519
+ clearTimeout(timeoutId);
520
+ options.signal?.removeEventListener('abort', abortFromParent);
521
+ }
522
+ }