@hed-hog/core 0.0.400 → 0.0.402

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 (274) hide show
  1. package/dist/access-log/access-log.interceptor.d.ts +10 -0
  2. package/dist/access-log/access-log.interceptor.d.ts.map +1 -1
  3. package/dist/access-log/access-log.interceptor.js +15 -1
  4. package/dist/access-log/access-log.interceptor.js.map +1 -1
  5. package/dist/ai/ai.controller.d.ts +2 -0
  6. package/dist/ai/ai.controller.d.ts.map +1 -1
  7. package/dist/ai/ai.service.d.ts +86 -2
  8. package/dist/ai/ai.service.d.ts.map +1 -1
  9. package/dist/ai/ai.service.js +304 -23
  10. package/dist/ai/ai.service.js.map +1 -1
  11. package/dist/ai-instruction/ai-prompt-resolver.service.d.ts +12 -0
  12. package/dist/ai-instruction/ai-prompt-resolver.service.d.ts.map +1 -1
  13. package/dist/ai-instruction/ai-prompt-resolver.service.js +26 -0
  14. package/dist/ai-instruction/ai-prompt-resolver.service.js.map +1 -1
  15. package/dist/core.module.d.ts.map +1 -1
  16. package/dist/core.module.js +3 -0
  17. package/dist/core.module.js.map +1 -1
  18. package/dist/document-kit/document-kit.module.d.ts +3 -0
  19. package/dist/document-kit/document-kit.module.d.ts.map +1 -0
  20. package/dist/document-kit/document-kit.module.js +24 -0
  21. package/dist/document-kit/document-kit.module.js.map +1 -0
  22. package/dist/document-kit/document-kit.registry.d.ts +55 -0
  23. package/dist/document-kit/document-kit.registry.d.ts.map +1 -0
  24. package/dist/document-kit/document-kit.registry.js +91 -0
  25. package/dist/document-kit/document-kit.registry.js.map +1 -0
  26. package/dist/document-kit/document-kit.service.d.ts +49 -0
  27. package/dist/document-kit/document-kit.service.d.ts.map +1 -0
  28. package/dist/document-kit/document-kit.service.js +135 -0
  29. package/dist/document-kit/document-kit.service.js.map +1 -0
  30. package/dist/file/file-upload-ticket.service.d.ts +1 -1
  31. package/dist/file/file.controller.d.ts +3 -3
  32. package/dist/file/file.service.d.ts +136 -13
  33. package/dist/file/file.service.d.ts.map +1 -1
  34. package/dist/file/file.service.js +297 -27
  35. package/dist/file/file.service.js.map +1 -1
  36. package/dist/file/mcp-file-source.schema.d.ts.map +1 -1
  37. package/dist/file/mcp-file-source.schema.js +8 -5
  38. package/dist/file/mcp-file-source.schema.js.map +1 -1
  39. package/dist/file/provider/abstract.provider.d.ts +33 -0
  40. package/dist/file/provider/abstract.provider.d.ts.map +1 -1
  41. package/dist/file/provider/abstract.provider.js +9 -0
  42. package/dist/file/provider/abstract.provider.js.map +1 -1
  43. package/dist/file/provider/s3-multipart.util.d.ts +17 -0
  44. package/dist/file/provider/s3-multipart.util.d.ts.map +1 -0
  45. package/dist/file/provider/s3-multipart.util.js +88 -0
  46. package/dist/file/provider/s3-multipart.util.js.map +1 -0
  47. package/dist/file/provider/s3.provider.d.ts +9 -1
  48. package/dist/file/provider/s3.provider.d.ts.map +1 -1
  49. package/dist/file/provider/s3.provider.js +16 -0
  50. package/dist/file/provider/s3.provider.js.map +1 -1
  51. package/dist/file/provider/spaces.provider.d.ts +10 -1
  52. package/dist/file/provider/spaces.provider.d.ts.map +1 -1
  53. package/dist/file/provider/spaces.provider.js +17 -0
  54. package/dist/file/provider/spaces.provider.js.map +1 -1
  55. package/dist/index.d.ts +5 -0
  56. package/dist/index.d.ts.map +1 -1
  57. package/dist/index.js +5 -0
  58. package/dist/index.js.map +1 -1
  59. package/dist/integration-profile/integration-profile.controller.d.ts +1 -0
  60. package/dist/integration-profile/integration-profile.controller.d.ts.map +1 -1
  61. package/dist/integration-profile/integration-profile.secrets.d.ts.map +1 -1
  62. package/dist/integration-profile/integration-profile.secrets.js +26 -0
  63. package/dist/integration-profile/integration-profile.secrets.js.map +1 -1
  64. package/dist/integration-profile/integration-profile.service.d.ts +1 -0
  65. package/dist/integration-profile/integration-profile.service.d.ts.map +1 -1
  66. package/dist/integration-profile/integration-profile.service.js +3 -3
  67. package/dist/integration-profile/integration-profile.service.js.map +1 -1
  68. package/dist/integration-profile/testers/ai.testers.d.ts.map +1 -1
  69. package/dist/integration-profile/testers/ai.testers.js +46 -8
  70. package/dist/integration-profile/testers/ai.testers.js.map +1 -1
  71. package/dist/integration-profile/testers/http-probe.d.ts.map +1 -1
  72. package/dist/integration-profile/testers/http-probe.js +5 -3
  73. package/dist/integration-profile/testers/http-probe.js.map +1 -1
  74. package/dist/integration-profile/testers/integration-tester.types.d.ts +5 -0
  75. package/dist/integration-profile/testers/integration-tester.types.d.ts.map +1 -1
  76. package/dist/mail/mail.service.d.ts.map +1 -1
  77. package/dist/mail/mail.service.js +16 -1
  78. package/dist/mail/mail.service.js.map +1 -1
  79. package/dist/mcp/mcp-server.service.d.ts.map +1 -1
  80. package/dist/mcp/mcp-server.service.js +10 -1
  81. package/dist/mcp/mcp-server.service.js.map +1 -1
  82. package/dist/mcp/mcp.module.d.ts.map +1 -1
  83. package/dist/mcp/mcp.module.js +2 -0
  84. package/dist/mcp/mcp.module.js.map +1 -1
  85. package/dist/mcp/tools/core-access-log.mcp-tools.d.ts +17 -0
  86. package/dist/mcp/tools/core-access-log.mcp-tools.d.ts.map +1 -0
  87. package/dist/mcp/tools/core-access-log.mcp-tools.js +74 -0
  88. package/dist/mcp/tools/core-access-log.mcp-tools.js.map +1 -0
  89. package/dist/mcp/tools/core-files.mcp-tools.js +4 -4
  90. package/dist/mcp/tools/core-files.mcp-tools.js.map +1 -1
  91. package/dist/meeting/meeting-provider.registry.d.ts +72 -0
  92. package/dist/meeting/meeting-provider.registry.d.ts.map +1 -1
  93. package/dist/meeting/meeting-provider.registry.js.map +1 -1
  94. package/dist/oauth/oauth-token-broker.service.d.ts +50 -1
  95. package/dist/oauth/oauth-token-broker.service.d.ts.map +1 -1
  96. package/dist/oauth/oauth-token-broker.service.js +105 -3
  97. package/dist/oauth/oauth-token-broker.service.js.map +1 -1
  98. package/dist/oauth/providers/microsoft-entra-id.provider.d.ts +6 -0
  99. package/dist/oauth/providers/microsoft-entra-id.provider.d.ts.map +1 -1
  100. package/dist/oauth/providers/microsoft-entra-id.provider.js +30 -4
  101. package/dist/oauth/providers/microsoft-entra-id.provider.js.map +1 -1
  102. package/dist/pdf/document/charts/chart-bar.d.ts +24 -0
  103. package/dist/pdf/document/charts/chart-bar.d.ts.map +1 -0
  104. package/dist/pdf/document/charts/chart-bar.js +117 -0
  105. package/dist/pdf/document/charts/chart-bar.js.map +1 -0
  106. package/dist/pdf/document/charts/chart-donut.d.ts +16 -0
  107. package/dist/pdf/document/charts/chart-donut.d.ts.map +1 -0
  108. package/dist/pdf/document/charts/chart-donut.js +89 -0
  109. package/dist/pdf/document/charts/chart-donut.js.map +1 -0
  110. package/dist/pdf/document/charts/chart-gauge.d.ts +15 -0
  111. package/dist/pdf/document/charts/chart-gauge.d.ts.map +1 -0
  112. package/dist/pdf/document/charts/chart-gauge.js +60 -0
  113. package/dist/pdf/document/charts/chart-gauge.js.map +1 -0
  114. package/dist/pdf/document/charts/chart-line.d.ts +29 -0
  115. package/dist/pdf/document/charts/chart-line.d.ts.map +1 -0
  116. package/dist/pdf/document/charts/chart-line.js +89 -0
  117. package/dist/pdf/document/charts/chart-line.js.map +1 -0
  118. package/dist/pdf/document/charts/chart-primitives.d.ts +64 -0
  119. package/dist/pdf/document/charts/chart-primitives.d.ts.map +1 -0
  120. package/dist/pdf/document/charts/chart-primitives.js +110 -0
  121. package/dist/pdf/document/charts/chart-primitives.js.map +1 -0
  122. package/dist/pdf/document/charts/chart-radar.d.ts +13 -0
  123. package/dist/pdf/document/charts/chart-radar.d.ts.map +1 -0
  124. package/dist/pdf/document/charts/chart-radar.js +83 -0
  125. package/dist/pdf/document/charts/chart-radar.js.map +1 -0
  126. package/dist/pdf/document/charts/chart-scale.d.ts +39 -0
  127. package/dist/pdf/document/charts/chart-scale.d.ts.map +1 -0
  128. package/dist/pdf/document/charts/chart-scale.js +118 -0
  129. package/dist/pdf/document/charts/chart-scale.js.map +1 -0
  130. package/dist/pdf/document/charts/index.d.ts +8 -0
  131. package/dist/pdf/document/charts/index.d.ts.map +1 -0
  132. package/dist/pdf/document/charts/index.js +24 -0
  133. package/dist/pdf/document/charts/index.js.map +1 -0
  134. package/dist/pdf/document/index.d.ts +8 -0
  135. package/dist/pdf/document/index.d.ts.map +1 -0
  136. package/dist/pdf/document/index.js +24 -0
  137. package/dist/pdf/document/index.js.map +1 -0
  138. package/dist/pdf/document/pdf-document.chrome.d.ts +20 -0
  139. package/dist/pdf/document/pdf-document.chrome.d.ts.map +1 -0
  140. package/dist/pdf/document/pdf-document.chrome.js +36 -0
  141. package/dist/pdf/document/pdf-document.chrome.js.map +1 -0
  142. package/dist/pdf/document/pdf-document.css.d.ts +20 -0
  143. package/dist/pdf/document/pdf-document.css.d.ts.map +1 -0
  144. package/dist/pdf/document/pdf-document.css.js +327 -0
  145. package/dist/pdf/document/pdf-document.css.js.map +1 -0
  146. package/dist/pdf/document/pdf-document.renderer.d.ts +10 -0
  147. package/dist/pdf/document/pdf-document.renderer.d.ts.map +1 -0
  148. package/dist/pdf/document/pdf-document.renderer.js +226 -0
  149. package/dist/pdf/document/pdf-document.renderer.js.map +1 -0
  150. package/dist/pdf/document/pdf-document.service.d.ts +27 -0
  151. package/dist/pdf/document/pdf-document.service.d.ts.map +1 -0
  152. package/dist/pdf/document/pdf-document.service.js +81 -0
  153. package/dist/pdf/document/pdf-document.service.js.map +1 -0
  154. package/dist/pdf/document/pdf-document.theme.d.ts +41 -0
  155. package/dist/pdf/document/pdf-document.theme.d.ts.map +1 -0
  156. package/dist/pdf/document/pdf-document.theme.js +105 -0
  157. package/dist/pdf/document/pdf-document.theme.js.map +1 -0
  158. package/dist/pdf/document/pdf-document.types.d.ts +164 -0
  159. package/dist/pdf/document/pdf-document.types.d.ts.map +1 -0
  160. package/dist/pdf/document/pdf-document.types.js +18 -0
  161. package/dist/pdf/document/pdf-document.types.js.map +1 -0
  162. package/dist/pdf/pdf.module.d.ts.map +1 -1
  163. package/dist/pdf/pdf.module.js +3 -2
  164. package/dist/pdf/pdf.module.js.map +1 -1
  165. package/dist/pdf/pdf.service.d.ts +75 -1
  166. package/dist/pdf/pdf.service.d.ts.map +1 -1
  167. package/dist/pdf/pdf.service.js +167 -63
  168. package/dist/pdf/pdf.service.js.map +1 -1
  169. package/dist/pdf/templates/commercial-proposal.template.d.ts +96 -0
  170. package/dist/pdf/templates/commercial-proposal.template.d.ts.map +1 -0
  171. package/dist/pdf/templates/commercial-proposal.template.js +276 -0
  172. package/dist/pdf/templates/commercial-proposal.template.js.map +1 -0
  173. package/dist/setting/favicon.controller.d.ts +26 -0
  174. package/dist/setting/favicon.controller.d.ts.map +1 -0
  175. package/dist/setting/favicon.controller.js +130 -0
  176. package/dist/setting/favicon.controller.js.map +1 -0
  177. package/dist/setting/setting.module.d.ts.map +1 -1
  178. package/dist/setting/setting.module.js +2 -1
  179. package/dist/setting/setting.module.js.map +1 -1
  180. package/dist/setting/setting.service.d.ts.map +1 -1
  181. package/dist/setting/setting.service.js +3 -0
  182. package/dist/setting/setting.service.js.map +1 -1
  183. package/dist/task/task.service.d.ts +6 -0
  184. package/dist/task/task.service.d.ts.map +1 -1
  185. package/dist/task/task.service.js +44 -13
  186. package/dist/task/task.service.js.map +1 -1
  187. package/dist/whatsapp-sender/whatsapp-sender.service.d.ts +35 -2
  188. package/dist/whatsapp-sender/whatsapp-sender.service.d.ts.map +1 -1
  189. package/dist/whatsapp-sender/whatsapp-sender.service.js +107 -22
  190. package/dist/whatsapp-sender/whatsapp-sender.service.js.map +1 -1
  191. package/hedhog/data/integration_type.yaml +6 -0
  192. package/hedhog/data/route.yaml +8 -0
  193. package/hedhog/data/setting_group.yaml +52 -0
  194. package/hedhog/frontend/app/configurations/[slug]/components/setting-field.test.tsx.ejs +85 -2
  195. package/hedhog/frontend/app/configurations/[slug]/components/setting-field.tsx.ejs +576 -491
  196. package/hedhog/frontend/app/users/simulate-access-dialog.test.tsx.ejs +29 -50
  197. package/hedhog/frontend/app/users/simulate-access-dialog.tsx.ejs +146 -50
  198. package/hedhog/frontend/messages/en.json +81 -2
  199. package/hedhog/frontend/messages/pt.json +81 -2
  200. package/hedhog/table/file.yaml +3 -1
  201. package/package.json +5 -5
  202. package/src/access-log/access-log.interceptor.spec.ts +50 -0
  203. package/src/access-log/access-log.interceptor.ts +18 -0
  204. package/src/ai/ai-attachment.fragments.spec.ts +56 -3
  205. package/src/ai/ai.service.responses.spec.ts +429 -0
  206. package/src/ai/ai.service.ts +409 -27
  207. package/src/ai-instruction/ai-prompt-resolver.service.spec.ts +44 -0
  208. package/src/ai-instruction/ai-prompt-resolver.service.ts +29 -0
  209. package/src/core.module.ts +3 -0
  210. package/src/document-kit/document-kit.module.ts +12 -0
  211. package/src/document-kit/document-kit.registry.ts +125 -0
  212. package/src/document-kit/document-kit.service.spec.ts +156 -0
  213. package/src/document-kit/document-kit.service.ts +173 -0
  214. package/src/file/file-direct-upload.spec.ts +344 -0
  215. package/src/file/file.service.spec.ts +168 -1
  216. package/src/file/file.service.ts +435 -36
  217. package/src/file/mcp-file-source.schema.ts +8 -5
  218. package/src/file/provider/abstract.provider.ts +57 -0
  219. package/src/file/provider/s3-multipart.spec.ts +123 -0
  220. package/src/file/provider/s3-multipart.util.ts +126 -0
  221. package/src/file/provider/s3.provider.ts +65 -1
  222. package/src/file/provider/spaces.provider.ts +67 -1
  223. package/src/index.ts +5 -0
  224. package/src/integration-profile/integration-profile.secrets.ts +26 -0
  225. package/src/integration-profile/integration-profile.service.ts +1 -0
  226. package/src/integration-profile/testers/ai.testers.ts +70 -5
  227. package/src/integration-profile/testers/http-probe.ts +5 -3
  228. package/src/integration-profile/testers/integration-tester.types.ts +5 -0
  229. package/src/integration-profile/testers/oauth.testers.spec.ts +13 -0
  230. package/src/integration-profile/testers/remaining.testers.spec.ts +65 -1
  231. package/src/integration-profile/testers/tester-coverage.spec.ts +12 -0
  232. package/src/mail/mail.service.spec.ts +25 -0
  233. package/src/mail/mail.service.ts +18 -1
  234. package/src/mcp/mcp-server.service.spec.ts +16 -0
  235. package/src/mcp/mcp-server.service.ts +10 -1
  236. package/src/mcp/mcp.module.ts +2 -0
  237. package/src/mcp/tools/core-access-log.mcp-tools.ts +66 -0
  238. package/src/mcp/tools/core-files.mcp-tools.ts +4 -4
  239. package/src/meeting/meeting-provider.registry.ts +80 -0
  240. package/src/oauth/oauth-token-broker.service.spec.ts +108 -0
  241. package/src/oauth/oauth-token-broker.service.ts +129 -2
  242. package/src/oauth/providers/microsoft-entra-id.provider.spec.ts +133 -0
  243. package/src/oauth/providers/microsoft-entra-id.provider.ts +29 -2
  244. package/src/pdf/document/charts/chart-bar.ts +217 -0
  245. package/src/pdf/document/charts/chart-donut.ts +163 -0
  246. package/src/pdf/document/charts/chart-gauge.ts +94 -0
  247. package/src/pdf/document/charts/chart-line.ts +159 -0
  248. package/src/pdf/document/charts/chart-primitives.ts +188 -0
  249. package/src/pdf/document/charts/chart-radar.ts +136 -0
  250. package/src/pdf/document/charts/chart-scale.spec.ts +93 -0
  251. package/src/pdf/document/charts/chart-scale.ts +141 -0
  252. package/src/pdf/document/charts/charts.spec.ts +157 -0
  253. package/src/pdf/document/charts/index.ts +7 -0
  254. package/src/pdf/document/index.ts +7 -0
  255. package/src/pdf/document/pdf-document.chrome.ts +38 -0
  256. package/src/pdf/document/pdf-document.css.ts +326 -0
  257. package/src/pdf/document/pdf-document.renderer.spec.ts +172 -0
  258. package/src/pdf/document/pdf-document.renderer.ts +317 -0
  259. package/src/pdf/document/pdf-document.service.ts +77 -0
  260. package/src/pdf/document/pdf-document.theme.ts +146 -0
  261. package/src/pdf/document/pdf-document.types.ts +149 -0
  262. package/src/pdf/pdf.module.ts +3 -2
  263. package/src/pdf/pdf.service.spec.ts +131 -0
  264. package/src/pdf/pdf.service.ts +256 -82
  265. package/src/pdf/templates/commercial-proposal.template.spec.ts +177 -0
  266. package/src/pdf/templates/commercial-proposal.template.ts +400 -0
  267. package/src/setting/favicon.controller.spec.ts +92 -0
  268. package/src/setting/favicon.controller.ts +135 -0
  269. package/src/setting/setting.module.ts +2 -1
  270. package/src/setting/setting.service.ts +3 -0
  271. package/src/task/task.service.spec.ts +77 -1
  272. package/src/task/task.service.ts +58 -14
  273. package/src/whatsapp-sender/whatsapp-sender.service.spec.ts +263 -0
  274. package/src/whatsapp-sender/whatsapp-sender.service.ts +154 -16
@@ -1,4 +1,5 @@
1
1
  import {
2
+ DeleteObjectCommand,
2
3
  GetBucketNotificationConfigurationCommand,
3
4
  GetObjectCommand,
4
5
  HeadBucketCommand,
@@ -25,13 +26,15 @@ import { PaginationDTO, PaginationService } from '@hed-hog/api-pagination';
25
26
  import { PrismaService } from '@hed-hog/api-prisma';
26
27
  import {
27
28
  BadRequestException,
29
+ ConflictException,
28
30
  ForbiddenException,
29
31
  forwardRef,
30
32
  Inject,
31
33
  Injectable,
32
34
  Logger,
33
35
  NotFoundException,
34
- OnModuleInit
36
+ OnModuleInit,
37
+ ServiceUnavailableException
35
38
  } from '@nestjs/common';
36
39
  import { JwtService } from '@nestjs/jwt';
37
40
  import { createHash } from 'crypto';
@@ -56,10 +59,15 @@ import {
56
59
  } from './image-optimization.service';
57
60
  import {
58
61
  AbstractProvider,
62
+ MultipartUploadPart,
63
+ supportsDirectUpload,
59
64
  supportsServerSideCopy,
60
65
  } from './provider/abstract.provider';
61
66
  import { ProviderFactory } from './provider/provider.factory';
62
67
 
68
+ /** Códigos de erro do S3/Spaces que indicam indisponibilidade passageira, não arquivo ruim. */
69
+ const STORAGE_UNAVAILABLE_CODES = ['SlowDown', 'ServiceUnavailable', 'InternalError', 'RequestTimeout'];
70
+
63
71
  type ResolvedProvider = {
64
72
  provider: AbstractProvider;
65
73
  providerId: number;
@@ -78,6 +86,49 @@ const PROVIDER_CACHE_TTL_MS = Math.max(
78
86
  Number(process.env.FILE_PROVIDER_CACHE_TTL_MS ?? 30_000),
79
87
  );
80
88
 
89
+ /** Upload direto em partes: mínimo de 16 MiB por parte e até 9000 partes (regra do Studio). */
90
+ export const DIRECT_UPLOAD_MIN_PART_BYTES = 16 * 1024 * 1024;
91
+ export const DIRECT_UPLOAD_MAX_PARTS = 9000;
92
+ /** Validade do token e das URLs de parte: um vídeo grande em conexão lenta leva horas. */
93
+ export const DIRECT_UPLOAD_DEFAULT_TTL_SECONDS = 86_400;
94
+ const DIRECT_UPLOAD_TOKEN_TYPE = 'file-direct-upload';
95
+ /**
96
+ * `file.size` é BIGINT no banco, e o PrismaService o devolve como number: o teto real é o
97
+ * maior inteiro exato de um number (~9 PB).
98
+ */
99
+ const FILE_SIZE_COLUMN_MAX = Number.MAX_SAFE_INTEGER;
100
+
101
+ export type DirectUploadFileInfo = {
102
+ originalname: string;
103
+ mimetype: string;
104
+ size: number;
105
+ isPrivate?: boolean;
106
+ createdByUserId?: number | null;
107
+ /** Mesmo sentido do `uploadFromPath`: o chamador já conferiu o tipo pelas regras dele. */
108
+ skipMimeValidation?: boolean;
109
+ /**
110
+ * O chamador já conferiu o tamanho pelas regras dele: pula a `storage-max-size`. O teto
111
+ * da coluna `file.size` continua valendo.
112
+ */
113
+ skipSizeValidation?: boolean;
114
+ };
115
+
116
+ export type CompleteDirectUploadInput = DirectUploadFileInfo & {
117
+ key: string;
118
+ uploadId: string;
119
+ destination: string;
120
+ parts: MultipartUploadPart[];
121
+ /**
122
+ * Confere os primeiros `bytes` do objeto gravado (ex.: sniff do tipo real). Se `check`
123
+ * lançar, o objeto é apagado e o erro sobe como veio; se devolver um mimetype, é ele
124
+ * que vai para a linha `file` (o tipo real, no lugar do declarado).
125
+ */
126
+ verifyHead?: {
127
+ bytes: number;
128
+ check: (head: Buffer) => string | void | Promise<string | void>;
129
+ };
130
+ };
131
+
81
132
  /** Raw `?w=`/`?f=`/`?q=` query values for the public image endpoint. */
82
133
  export type ImageVariantRequest = {
83
134
  width?: number | string;
@@ -304,16 +355,37 @@ export class FileService implements OnModuleInit {
304
355
  return file;
305
356
  }
306
357
 
307
- private mapStorageNotFoundError(error: any, fileId: number) {
308
- const missingInStorage =
309
- error?.name === 'NoSuchKey' ||
310
- error?.Code === 'NoSuchKey' ||
311
- error?.$metadata?.httpStatusCode === 404;
358
+ /**
359
+ * Traduz a falha do provider de storage para a resposta HTTP certa.
360
+ *
361
+ * 5xx e throttle do S3/Spaces são indisponibilidade passageira, não defeito:
362
+ * viram 503 com `code: STORAGE_UNAVAILABLE`, que o HttpExceptionFilter da API
363
+ * responde com `Retry-After` e reporta deduplicado. Sem isso, um 503 do Spaces
364
+ * com corpo que não é XML (ex.: `upstream connect error` do balanceador)
365
+ * estourava no parse do SDK como `char 'u' is not expected` e virava 500.
366
+ *
367
+ * O status vem de `$metadata.httpStatusCode`, que o SDK carimba mesmo quando a
368
+ * desserialização do erro falha; `$response.statusCode` cobre o resto.
369
+ */
370
+ private mapStorageError(error: any, fileId: number): never {
371
+ const status = error?.$metadata?.httpStatusCode ?? error?.$response?.statusCode;
372
+ const codes = [error?.name, error?.Code];
312
373
 
313
- if (missingInStorage) {
374
+ if (codes.includes('NoSuchKey') || status === 404) {
314
375
  throw new NotFoundException(`File not found in storage: ${fileId}`);
315
376
  }
316
377
 
378
+ const unavailable =
379
+ (typeof status === 'number' && status >= 500) ||
380
+ codes.some((c) => STORAGE_UNAVAILABLE_CODES.includes(c));
381
+
382
+ if (unavailable) {
383
+ throw new ServiceUnavailableException(
384
+ { message: 'Storage temporarily unavailable', code: 'STORAGE_UNAVAILABLE' },
385
+ { cause: error },
386
+ );
387
+ }
388
+
317
389
  throw error;
318
390
  }
319
391
 
@@ -338,11 +410,15 @@ export class FileService implements OnModuleInit {
338
410
  const provider = await this.getProvider();
339
411
  const providerPath = this.resolveProviderPath(file);
340
412
 
413
+ // O HEAD só serve para descobrir o tamanho quando a tabela não o tem. Feito
414
+ // sempre, dobrava as requisições ao storage em cada segmento HLS.
341
415
  let metadata: any;
342
- try {
343
- metadata = await provider.metaData(providerPath);
344
- } catch {
345
- metadata = undefined;
416
+ if (file.size == null) {
417
+ try {
418
+ metadata = await provider.metaData(providerPath);
419
+ } catch {
420
+ metadata = undefined;
421
+ }
346
422
  }
347
423
 
348
424
  try {
@@ -353,7 +429,7 @@ export class FileService implements OnModuleInit {
353
429
  size: file.size ?? this.getFileSizeFromMetadata(metadata),
354
430
  };
355
431
  } catch (error) {
356
- this.mapStorageNotFoundError(error, fileId);
432
+ this.mapStorageError(error, fileId);
357
433
  }
358
434
  }
359
435
 
@@ -369,16 +445,21 @@ export class FileService implements OnModuleInit {
369
445
 
370
446
  const provider = await this.getProvider();
371
447
  const providerPath = this.resolveProviderPath(file);
372
- const totalSize: number = file.size ?? 0;
448
+ const totalSize = Number(file.size ?? 0);
373
449
 
374
450
  const resolvedStart = start ?? 0;
375
451
  const resolvedEnd = end !== undefined ? end : totalSize > 0 ? totalSize - 1 : undefined;
376
452
  const hasRange = start !== undefined;
377
453
 
378
- const stream = await provider.readStream(
379
- providerPath,
380
- hasRange ? { start: resolvedStart, end: resolvedEnd } : undefined,
381
- );
454
+ let stream: Awaited<ReturnType<AbstractProvider['readStream']>>;
455
+ try {
456
+ stream = await provider.readStream(
457
+ providerPath,
458
+ hasRange ? { start: resolvedStart, end: resolvedEnd } : undefined,
459
+ );
460
+ } catch (error) {
461
+ this.mapStorageError(error, fileId);
462
+ }
382
463
 
383
464
  const contentLength =
384
465
  resolvedEnd !== undefined ? resolvedEnd - resolvedStart + 1 : totalSize;
@@ -461,16 +542,7 @@ export class FileService implements OnModuleInit {
461
542
  try {
462
543
  providerBuffer = await provider.buffer(providerPath);
463
544
  } catch (error: any) {
464
- const missingInStorage =
465
- error?.name === 'NoSuchKey' ||
466
- error?.Code === 'NoSuchKey' ||
467
- error?.$metadata?.httpStatusCode === 404;
468
-
469
- if (missingInStorage) {
470
- throw new NotFoundException(`File not found in storage: ${fileId}`);
471
- }
472
-
473
- throw error;
545
+ this.mapStorageError(error, fileId);
474
546
  }
475
547
 
476
548
  const buffer = Buffer.from(providerBuffer);
@@ -549,6 +621,10 @@ export class FileService implements OnModuleInit {
549
621
  // Quem enviou. É o que permite recusar um `fileId` de outro usuário nas tools MCP
550
622
  // (ver `assertFileUsable`); fica nulo nos caminhos internos sem usuário.
551
623
  createdByUserId?: number | null;
624
+ // Grava a imagem byte a byte, sem a conversão para WebP nem o redimensionamento.
625
+ // Para quem entrega o arquivo a um consumidor externo que exige o formato
626
+ // original — o Instagram recusa WebP. O chamador já normalizou o arquivo.
627
+ skipImageOptimization?: boolean;
552
628
  },
553
629
  ) {
554
630
 
@@ -585,12 +661,18 @@ export class FileService implements OnModuleInit {
585
661
  fileBuffer.buffer,
586
662
  );
587
663
 
588
- const optimized = await this.imageOptimizationService.optimizeBuffer(
589
- fileBuffer.buffer,
590
- effectiveMimetype,
591
- normalizedFilename,
592
- { maxWidth: resize?.maxWidth, maxHeight: resize?.maxHeight },
593
- );
664
+ const optimized = options?.skipImageOptimization
665
+ ? {
666
+ buffer: fileBuffer.buffer,
667
+ mimetype: effectiveMimetype,
668
+ originalname: normalizedFilename,
669
+ }
670
+ : await this.imageOptimizationService.optimizeBuffer(
671
+ fileBuffer.buffer,
672
+ effectiveMimetype,
673
+ normalizedFilename,
674
+ { maxWidth: resize?.maxWidth, maxHeight: resize?.maxHeight },
675
+ );
594
676
 
595
677
  const normalizedFile = {
596
678
  ...fileBuffer,
@@ -635,6 +717,8 @@ export class FileService implements OnModuleInit {
635
717
  createdByUserId?: number | null;
636
718
  // Mesmo par maxWidth/maxHeight que `upload` aceita — só tem efeito em image/*.
637
719
  resize?: { maxWidth?: number; maxHeight?: number };
720
+ // Mesma opção de `upload`: a imagem segue o caminho de streaming, intacta.
721
+ skipImageOptimization?: boolean;
638
722
  },
639
723
  ) {
640
724
  if (!filePath) {
@@ -684,8 +768,9 @@ export class FileService implements OnModuleInit {
684
768
  // streaming direto do disco via provider.uploadFromPath. Isso evita carregar
685
769
  // o arquivo inteiro na RAM (fs.readFile estoura o limite de Buffer / causa OOM)
686
770
  // e habilita upload multipart no S3. A otimização de imagem só se aplica a
687
- // image/*, então não há perda nesse caminho.
688
- if (!mimetype.startsWith('image/')) {
771
+ // image/*, então não há perda nesse caminho. `skipImageOptimization` manda a
772
+ // imagem pelo mesmo caminho: o chamador quer os bytes exatamente como estão.
773
+ if (!mimetype.startsWith('image/') || metadata?.skipImageOptimization) {
689
774
  const url = await provider.uploadFromPath(destination, filePath, {
690
775
  filename: originalname,
691
776
  mimetype,
@@ -745,6 +830,292 @@ export class FileService implements OnModuleInit {
745
830
  return file;
746
831
  }
747
832
 
833
+ /**
834
+ * O storage ativo aceita upload direto em partes assinadas (S3 e Spaces)? Sem provider
835
+ * configurado, ou com um que não implementa, é `false` — quem oferece o caminho cai no
836
+ * upload pela API.
837
+ */
838
+ async supportsDirectUpload(): Promise<boolean> {
839
+ try {
840
+ return supportsDirectUpload(await this.getProvider());
841
+ } catch {
842
+ return false;
843
+ }
844
+ }
845
+
846
+ private async getDirectUploadProvider() {
847
+ const provider = await this.getProvider();
848
+ if (!supportsDirectUpload(provider)) {
849
+ throw new ConflictException('The file storage does not support direct uploads');
850
+ }
851
+ return provider;
852
+ }
853
+
854
+ /** `key` gravada pelo start: `${destination}/${uuid.ext}`, sem subpasta extra. */
855
+ private assertDirectUploadKey(key: string, destination: string): string {
856
+ const prefix = `${destination}/`;
857
+ const storedFilename = typeof key === 'string' && key.startsWith(prefix)
858
+ ? key.slice(prefix.length)
859
+ : '';
860
+
861
+ if (!storedFilename || storedFilename.includes('/')) {
862
+ throw new BadRequestException('Invalid direct upload key');
863
+ }
864
+
865
+ return storedFilename;
866
+ }
867
+
868
+ /**
869
+ * Abre um upload multipart direto no storage. A chave segue o mesmo formato que o
870
+ * `uploadFromPath` grava (`file.location` = destination, `file.path` = uuid.ext do
871
+ * `getFilename` do provider), então o arquivo final é servido como qualquer outro.
872
+ *
873
+ * Nada vai para o banco aqui: o estado entre as chamadas fica com quem chamou (token
874
+ * assinado, ver `signDirectUploadToken`) e a linha `file` só nasce no complete.
875
+ * Tamanho da parte com a regra do Studio: no mínimo 16 MiB e no máximo 9000 partes.
876
+ */
877
+ async startDirectUpload(destination: string, info: DirectUploadFileInfo) {
878
+ if (!destination) {
879
+ throw new BadRequestException(`No destination provided`);
880
+ }
881
+
882
+ const size = Number(info?.size);
883
+ if (!Number.isInteger(size) || size <= 0) {
884
+ throw new BadRequestException(`No size provided`);
885
+ }
886
+
887
+ // Acima disso o tamanho não cabe exato num number, e a linha não teria como ser lida.
888
+ if (size > FILE_SIZE_COLUMN_MAX) {
889
+ throw new BadRequestException(`File too large: ${size} bytes`);
890
+ }
891
+
892
+ const mimetype = String(info?.mimetype ?? '').trim();
893
+ if (!info?.skipMimeValidation && !(await this.acceptMimetypes(mimetype))) {
894
+ throw new BadRequestException(`Invalid file type: ${mimetype}`);
895
+ }
896
+
897
+ if (!info?.skipSizeValidation && !(await this.maxFileSize(size))) {
898
+ throw new BadRequestException(`File too large: ${size} bytes`);
899
+ }
900
+
901
+ const originalname = this.normalizeUploadedFilename(
902
+ String(info?.originalname ?? '').trim() || 'file',
903
+ );
904
+ const provider = await this.getDirectUploadProvider();
905
+ const key = `${destination}/${provider.getFilename(originalname)}`;
906
+ const uploadId = await provider.createMultipartUpload(key, mimetype || 'application/octet-stream');
907
+
908
+ const partSize = Math.max(
909
+ DIRECT_UPLOAD_MIN_PART_BYTES,
910
+ Math.ceil(size / DIRECT_UPLOAD_MAX_PARTS),
911
+ );
912
+
913
+ return {
914
+ key,
915
+ uploadId,
916
+ partSize,
917
+ partCount: Math.max(1, Math.ceil(size / partSize)),
918
+ };
919
+ }
920
+
921
+ /** URLs assinadas de PUT para as partes pedidas (1-based, sem repetir). */
922
+ async signDirectUploadParts(
923
+ key: string,
924
+ uploadId: string,
925
+ partNumbers: number[],
926
+ ttlSeconds = DIRECT_UPLOAD_DEFAULT_TTL_SECONDS,
927
+ ) {
928
+ if (!key || !uploadId) {
929
+ throw new BadRequestException('Invalid direct upload');
930
+ }
931
+
932
+ const numbers = Array.isArray(partNumbers) ? partNumbers.map(Number) : [];
933
+ if (
934
+ numbers.length === 0 ||
935
+ numbers.some((n) => !Number.isInteger(n) || n < 1 || n > DIRECT_UPLOAD_MAX_PARTS)
936
+ ) {
937
+ throw new BadRequestException('Invalid part numbers');
938
+ }
939
+
940
+ const provider = await this.getDirectUploadProvider();
941
+ return provider.signUploadParts(
942
+ key,
943
+ uploadId,
944
+ [...new Set(numbers)],
945
+ Math.max(60, Math.floor(ttlSeconds)),
946
+ );
947
+ }
948
+
949
+ /** Aborta o multipart (as partes já enviadas somem do bucket). Upload já fechado não é erro. */
950
+ async abortDirectUpload(key: string, uploadId: string): Promise<void> {
951
+ if (!key || !uploadId) {
952
+ throw new BadRequestException('Invalid direct upload');
953
+ }
954
+
955
+ const provider = await this.getDirectUploadProvider();
956
+ try {
957
+ await provider.abortMultipartUpload(key, uploadId);
958
+ } catch (error: any) {
959
+ const status = error?.$metadata?.httpStatusCode;
960
+ if (error?.name === 'NoSuchUpload' || error?.Code === 'NoSuchUpload' || status === 404) {
961
+ return;
962
+ }
963
+ throw error;
964
+ }
965
+ }
966
+
967
+ /**
968
+ * Fecha o multipart e registra o arquivo. Confere o que o storage gravou antes de criar
969
+ * a linha — tamanho pelo HeadObject (tem que bater com o declarado no start), teto
970
+ * `storage-max-size`, allowlist `storage-accept-mimetype` e, quando pedido, os primeiros
971
+ * bytes (`verifyHead`) — e apaga o objeto quando algo não confere: um upload recusado
972
+ * não deixa blob órfão no bucket.
973
+ */
974
+ async completeDirectUpload(input: CompleteDirectUploadInput) {
975
+ const { key, uploadId, destination } = input ?? ({} as CompleteDirectUploadInput);
976
+
977
+ if (!destination) {
978
+ throw new BadRequestException(`No destination provided`);
979
+ }
980
+ if (!uploadId) {
981
+ throw new BadRequestException('Invalid direct upload');
982
+ }
983
+
984
+ const storedFilename = this.assertDirectUploadKey(key, destination);
985
+
986
+ const parts = Array.isArray(input.parts) ? input.parts : [];
987
+ if (
988
+ parts.length === 0 ||
989
+ parts.some(
990
+ (part) =>
991
+ !Number.isInteger(Number(part?.partNumber)) ||
992
+ Number(part.partNumber) < 1 ||
993
+ typeof part?.etag !== 'string' ||
994
+ !part.etag.trim(),
995
+ )
996
+ ) {
997
+ throw new BadRequestException('Invalid upload parts');
998
+ }
999
+
1000
+ const provider = await this.getDirectUploadProvider();
1001
+
1002
+ try {
1003
+ await provider.completeMultipartUpload(
1004
+ key,
1005
+ uploadId,
1006
+ parts.map((part) => ({ partNumber: Number(part.partNumber), etag: part.etag.trim() })),
1007
+ );
1008
+ } catch (error: any) {
1009
+ const status = error?.$metadata?.httpStatusCode;
1010
+ if (typeof status === 'number' && status >= 400 && status < 500) {
1011
+ // Parte faltando, ETag errado, parte pequena demais: o multipart não tem conserto.
1012
+ await this.abortDirectUpload(key, uploadId).catch(() => undefined);
1013
+ throw new BadRequestException('The uploaded parts could not be assembled');
1014
+ }
1015
+ throw error;
1016
+ }
1017
+
1018
+ const discard = async () => {
1019
+ await provider.delete(key).catch((error) =>
1020
+ this.logger.warn(`Failed to delete rejected direct upload ${key}: ${error?.message}`),
1021
+ );
1022
+ };
1023
+
1024
+ const declaredSize = Number(input.size);
1025
+ const storedSize = this.getFileSizeFromMetadata(await provider.metaData(key));
1026
+
1027
+ if (storedSize === undefined || storedSize !== declaredSize) {
1028
+ await discard();
1029
+ throw new BadRequestException(
1030
+ `Stored size (${storedSize ?? 'unknown'} bytes) differs from the declared size (${declaredSize} bytes)`,
1031
+ );
1032
+ }
1033
+
1034
+ if (
1035
+ storedSize > FILE_SIZE_COLUMN_MAX ||
1036
+ (!input.skipSizeValidation && !(await this.maxFileSize(storedSize)))
1037
+ ) {
1038
+ await discard();
1039
+ throw new BadRequestException(`File too large: ${storedSize} bytes`);
1040
+ }
1041
+
1042
+ let mimetype = String(input.mimetype ?? '').trim();
1043
+
1044
+ if (input.verifyHead) {
1045
+ try {
1046
+ const verified = await input.verifyHead.check(
1047
+ await provider.readHead(key, input.verifyHead.bytes),
1048
+ );
1049
+ if (typeof verified === 'string' && verified.trim()) {
1050
+ mimetype = verified.trim();
1051
+ }
1052
+ } catch (error) {
1053
+ await discard();
1054
+ throw error;
1055
+ }
1056
+ }
1057
+
1058
+ if (!input.skipMimeValidation && !(await this.acceptMimetypes(mimetype))) {
1059
+ await discard();
1060
+ throw new BadRequestException(`Invalid file type: ${mimetype}`);
1061
+ }
1062
+
1063
+ try {
1064
+ return await this.prismaService.file.create({
1065
+ data: {
1066
+ filename: this.normalizeUploadedFilename(
1067
+ String(input.originalname ?? '').trim() || storedFilename,
1068
+ ),
1069
+ path: storedFilename,
1070
+ provider_id: this.providerId,
1071
+ location: destination,
1072
+ mimetype_id: await this.getMimeType(mimetype),
1073
+ size: storedSize,
1074
+ is_private: input.isPrivate ?? false,
1075
+ created_by_user_id: input.createdByUserId ?? null,
1076
+ },
1077
+ });
1078
+ } catch (error) {
1079
+ await discard();
1080
+ throw error;
1081
+ }
1082
+ }
1083
+
1084
+ /**
1085
+ * Token opaco que carrega o estado de um upload direto entre as chamadas (sem tabela):
1086
+ * JWT HS256 com o JWT_SECRET da API e o tipo `file-direct-upload`, como o token do
1087
+ * `file-upload-ticket`. Quem chama põe nas claims o que precisa amarrar (marca, usuário).
1088
+ */
1089
+ signDirectUploadToken(
1090
+ claims: Record<string, unknown>,
1091
+ ttlSeconds = DIRECT_UPLOAD_DEFAULT_TTL_SECONDS,
1092
+ ): string {
1093
+ return this.jwtService.sign(
1094
+ { ...claims, t: DIRECT_UPLOAD_TOKEN_TYPE },
1095
+ {
1096
+ secret: process.env.JWT_SECRET,
1097
+ expiresIn: `${Math.max(1, Math.floor(ttlSeconds))}s`,
1098
+ },
1099
+ );
1100
+ }
1101
+
1102
+ /** Claims do token de upload direto; assinatura inválida, expirado ou de outro tipo = 400. */
1103
+ readDirectUploadToken<T extends Record<string, unknown>>(token: string): T {
1104
+ let payload: any;
1105
+
1106
+ try {
1107
+ payload = this.jwtService.verify(String(token ?? ''), { secret: process.env.JWT_SECRET });
1108
+ } catch {
1109
+ throw new BadRequestException('Invalid or expired upload token');
1110
+ }
1111
+
1112
+ if (payload?.t !== DIRECT_UPLOAD_TOKEN_TYPE) {
1113
+ throw new BadRequestException('Token is not a direct upload token');
1114
+ }
1115
+
1116
+ return payload as T;
1117
+ }
1118
+
748
1119
  /**
749
1120
  * Mimetypes de imagem que `optimizeBuffer` devolve intactos por decisão de projeto
750
1121
  * (vetor não vira bitmap, GIF animado não vira quadro único). Enfileirá-los para
@@ -843,7 +1214,7 @@ export class FileService implements OnModuleInit {
843
1214
  }> {
844
1215
  const { file, buffer } = await this.getBuffer(fileId);
845
1216
  const currentMimetype = file.file_mimetype.name;
846
- const oldSize = file.size ?? buffer.length;
1217
+ const oldSize = Number(file.size ?? buffer.length);
847
1218
 
848
1219
  if (!currentMimetype.startsWith('image/')) {
849
1220
  return { changed: false, oldSize, newSize: oldSize, mimetypeChanged: false };
@@ -1976,6 +2347,34 @@ export class FileService implements OnModuleInit {
1976
2347
  });
1977
2348
  }
1978
2349
 
2350
+ /**
2351
+ * Apaga um objeto de bucket avulso, com as credenciais de quem o configurou.
2352
+ *
2353
+ * Mesmo motivo do `headS3Object` e do `signS3Object`: o bucket e de outra
2354
+ * library (o Studio guarda os brutos no perfil de armazenamento dele), e nao o
2355
+ * destino ativo do core. O S3 responde 204 tambem para key inexistente, entao
2356
+ * apagar duas vezes nao e erro.
2357
+ */
2358
+ async deleteS3Object(params: {
2359
+ accessKeyId: string;
2360
+ secretAccessKey: string;
2361
+ sessionToken?: string;
2362
+ region: string;
2363
+ bucket: string;
2364
+ key: string;
2365
+ }): Promise<void> {
2366
+ const s3 = new S3Client({
2367
+ region: params.region,
2368
+ credentials: {
2369
+ accessKeyId: params.accessKeyId,
2370
+ secretAccessKey: params.secretAccessKey,
2371
+ ...(params.sessionToken ? { sessionToken: params.sessionToken } : {}),
2372
+ },
2373
+ });
2374
+
2375
+ await s3.send(new DeleteObjectCommand({ Bucket: params.bucket, Key: params.key }));
2376
+ }
2377
+
1979
2378
  async headS3Bucket(params: {
1980
2379
  accessKeyId: string;
1981
2380
  secretAccessKey: string;
@@ -35,7 +35,7 @@ export const FILE_SOURCE_SCHEMA_PROPERTIES: Record<string, any> = {
35
35
  uploadId: {
36
36
  type: 'number',
37
37
  description:
38
- 'ID returned by core.files.create-upload-url or core.files.upload.init, after the binary has been uploaded. Use this to send a file that is on disk without putting its bytes in the conversation.',
38
+ 'ID returned by core.files.create-upload-url (after the PUT) or core.files.upload.init (after every chunk was sent with core.files.upload.append). The upload is completed automatically, so core.files.upload.complete is optional here.',
39
39
  },
40
40
  fileUrl: {
41
41
  type: 'string',
@@ -45,7 +45,7 @@ export const FILE_SOURCE_SCHEMA_PROPERTIES: Record<string, any> = {
45
45
  fileBase64: {
46
46
  type: 'string',
47
47
  description:
48
- 'Base64-encoded content, sent inline. Only for small files (about 256kb): base64 of anything larger overflows the model output limit. Requires fileName and mimeType.',
48
+ 'Base64-encoded content, sent inline. Only for small files (about 256kb): base64 of anything larger overflows the model output limit; use core.files.upload.init for larger files instead of giving up. Requires fileName and mimeType.',
49
49
  },
50
50
  fileName: {
51
51
  type: 'string',
@@ -72,9 +72,12 @@ export const FILE_SOURCE_SCHEMA_PROPERTIES: Record<string, any> = {
72
72
  * `storage-max-size`, e é ele que aparece na mensagem de erro.
73
73
  */
74
74
  export const FILE_SOURCE_DESCRIPTION = [
75
- 'Exactly one file source is required: fileId, uploadId, fileUrl or fileBase64.',
76
- 'Prefer fileId; use fileBase64 only for files of a few hundred kilobytes, since the encoded content has to fit in a single tool call.',
77
- 'To send a local file without putting it in the conversation, call core.files.create-upload-url, PUT the binary to the URL it returns, call core.files.upload.complete, and pass the resulting fileId here.',
75
+ 'Exactly one file source is required: fileId, uploadId, fileUrl or fileBase64. Pick the first that fits:',
76
+ '(1) file already in storage: fileId.',
77
+ '(2) file publicly reachable on the internet: fileUrl.',
78
+ '(3) client can make HTTP requests to the Hub API host (e.g. a local CLI agent): core.files.create-upload-url, PUT the bytes to the returned url, then pass the uploadId (or the fileId from core.files.upload.complete). Fastest.',
79
+ '(4) client runs in a network-restricted sandbox (e.g. claude.ai), or the PUT fails with host_not_allowed, 403 or a network error: core.files.upload.init, then core.files.upload.append for each chunk (index 0..totalChunks-1, chunkBase64 of at most chunkBytes raw bytes as returned by init), then pass the uploadId (or the fileId from core.files.upload.complete). Nothing leaves the MCP connection, and it works up to the storage max size; generate the chunks programmatically in the sandbox (split the file, print the base64 of each chunk) instead of typing base64 by hand.',
80
+ '(5) small file (about 256kb or less): fileBase64 inline.',
78
81
  'Every path enforces the storage-accept-mimetype and storage-max-size settings (10mb by default).',
79
82
  ].join(' ');
80
83