@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
@@ -40,6 +40,47 @@ export function supportsServerSideCopy(
40
40
  return typeof (provider as Partial<ProviderServerSideCopy>).copy === 'function';
41
41
  }
42
42
 
43
+ /** Parte ja enviada de um upload multipart: numero (1-based) e o ETag que o storage devolveu. */
44
+ export type MultipartUploadPart = { partNumber: number; etag: string };
45
+
46
+ /**
47
+ * Upload direto do cliente para o storage, em partes com URL assinada: os bytes nao
48
+ * atravessam a API (nem o teto de 100 MB por requisicao do Cloudflare na frente dela).
49
+ *
50
+ * Opcional como o `copy`: so S3 e Spaces implementam, e quem chama confere com
51
+ * `supportsDirectUpload` antes de oferecer o caminho.
52
+ */
53
+ export interface ProviderDirectUpload {
54
+ createMultipartUpload(key: string, contentType: string): Promise<string>;
55
+ signUploadParts(
56
+ key: string,
57
+ uploadId: string,
58
+ partNumbers: number[],
59
+ ttlSeconds: number,
60
+ ): Promise<Array<{ partNumber: number; url: string }>>;
61
+ completeMultipartUpload(
62
+ key: string,
63
+ uploadId: string,
64
+ parts: MultipartUploadPart[],
65
+ ): Promise<void>;
66
+ abortMultipartUpload(key: string, uploadId: string): Promise<void>;
67
+ /** Os primeiros `bytes` do objeto (Range GET), para conferir o tipo real. */
68
+ readHead(key: string, bytes: number): Promise<Buffer>;
69
+ }
70
+
71
+ export function supportsDirectUpload(
72
+ provider: AbstractProvider,
73
+ ): provider is AbstractProvider & ProviderDirectUpload {
74
+ const candidate = provider as Partial<ProviderDirectUpload>;
75
+ return (
76
+ typeof candidate.createMultipartUpload === 'function' &&
77
+ typeof candidate.signUploadParts === 'function' &&
78
+ typeof candidate.completeMultipartUpload === 'function' &&
79
+ typeof candidate.abortMultipartUpload === 'function' &&
80
+ typeof candidate.readHead === 'function'
81
+ );
82
+ }
83
+
43
84
  export abstract class AbstractProvider implements IProvider {
44
85
  abstract uploadFromString(
45
86
  destination: string,
@@ -67,6 +108,22 @@ export abstract class AbstractProvider implements IProvider {
67
108
  */
68
109
  copy?(sourcePath: string, destination: string, filename: string): Promise<string>;
69
110
 
111
+ /** Upload direto em partes; ver `ProviderDirectUpload` e `supportsDirectUpload`. */
112
+ createMultipartUpload?(key: string, contentType: string): Promise<string>;
113
+ signUploadParts?(
114
+ key: string,
115
+ uploadId: string,
116
+ partNumbers: number[],
117
+ ttlSeconds: number,
118
+ ): Promise<Array<{ partNumber: number; url: string }>>;
119
+ completeMultipartUpload?(
120
+ key: string,
121
+ uploadId: string,
122
+ parts: MultipartUploadPart[],
123
+ ): Promise<void>;
124
+ abortMultipartUpload?(key: string, uploadId: string): Promise<void>;
125
+ readHead?(key: string, bytes: number): Promise<Buffer>;
126
+
70
127
  getExtension(filename: string) {
71
128
  return filename.split('.').pop();
72
129
  }
@@ -0,0 +1,123 @@
1
+ import { afterEach, describe, expect, it, jest } from '@jest/globals';
2
+ import {
3
+ AbortMultipartUploadCommand,
4
+ CompleteMultipartUploadCommand,
5
+ CreateMultipartUploadCommand,
6
+ GetObjectCommand,
7
+ S3Client,
8
+ } from '@aws-sdk/client-s3';
9
+ import { Readable } from 'stream';
10
+ import { supportsDirectUpload } from './abstract.provider';
11
+ import { S3Provider } from './s3.provider';
12
+ import { SpacesProvider } from './spaces.provider';
13
+
14
+ /**
15
+ * Upload multipart assinado no S3 e no Spaces, com o S3Client mockado: o que vai em cada
16
+ * comando, a URL de parte assinada de verdade (o presigner roda offline) e o Range GET.
17
+ */
18
+ const s3Settings = {
19
+ 'storage-s3-key': 'AKIAEXAMPLE',
20
+ 'storage-s3-secret': 'secret',
21
+ 'storage-s3-region': 'us-east-1',
22
+ 'storage-s3-bucket': 'bucket-s3',
23
+ };
24
+
25
+ const spacesSettings = {
26
+ 'storage-spaces-key': 'DOEXAMPLE',
27
+ 'storage-spaces-secret': 'secret',
28
+ 'storage-spaces-region': 'nyc3',
29
+ 'storage-spaces-bucket': 'spaces-hub',
30
+ };
31
+
32
+ function mockSend(impl: (command: any) => any) {
33
+ return jest.spyOn(S3Client.prototype, 'send').mockImplementation(async (command: any) => impl(command));
34
+ }
35
+
36
+ type ProviderCase = [string, () => any, string, string | undefined, string];
37
+
38
+ describe.each<ProviderCase>([
39
+ ['S3', () => new S3Provider(s3Settings), 'bucket-s3', undefined, 'amazonaws.com'],
40
+ ['Spaces', () => new SpacesProvider(spacesSettings), 'spaces-hub', 'public-read', 'digitaloceanspaces.com'],
41
+ ])('%s provider — upload direto', (_name, build, bucket, acl, host) => {
42
+ afterEach(() => jest.restoreAllMocks());
43
+
44
+ it('implementa o contrato de upload direto', () => {
45
+ expect(supportsDirectUpload(build())).toBe(true);
46
+ });
47
+
48
+ it('createMultipartUpload manda bucket, chave e tipo e devolve o UploadId', async () => {
49
+ const send = mockSend(() => ({ UploadId: 'up-1' }));
50
+
51
+ await expect(build().createMultipartUpload('social/brands/1/a.mp4', 'video/mp4')).resolves.toBe('up-1');
52
+
53
+ const command = send.mock.calls[0][0] as any;
54
+ expect(command).toBeInstanceOf(CreateMultipartUploadCommand);
55
+ expect(command.input).toEqual({
56
+ Bucket: bucket,
57
+ Key: 'social/brands/1/a.mp4',
58
+ ContentType: 'video/mp4',
59
+ ...(acl ? { ACL: acl } : {}),
60
+ });
61
+ });
62
+
63
+ it('createMultipartUpload sem UploadId é erro', async () => {
64
+ mockSend(() => ({}));
65
+ await expect(build().createMultipartUpload('k', 'video/mp4')).rejects.toThrow('multipart upload id');
66
+ });
67
+
68
+ it('signUploadParts devolve uma URL assinada de PUT por parte', async () => {
69
+ const parts = await build().signUploadParts('social/brands/1/a.mp4', 'up-1', [1, 2], 3600);
70
+
71
+ expect(parts.map((p) => p.partNumber)).toEqual([1, 2]);
72
+ for (const part of parts) {
73
+ const url = new URL(part.url);
74
+ expect(url.host).toContain(host);
75
+ expect(url.searchParams.get('uploadId')).toBe('up-1');
76
+ expect(url.searchParams.get('partNumber')).toBe(String(part.partNumber));
77
+ expect(url.searchParams.get('X-Amz-Expires')).toBe('3600');
78
+ expect(url.searchParams.get('X-Amz-Signature')).toBeTruthy();
79
+ }
80
+ });
81
+
82
+ it('completeMultipartUpload ordena as partes pelo número', async () => {
83
+ const send = mockSend(() => ({}));
84
+
85
+ await build().completeMultipartUpload('k', 'up-1', [
86
+ { partNumber: 2, etag: '"b"' },
87
+ { partNumber: 1, etag: '"a"' },
88
+ ]);
89
+
90
+ const command = send.mock.calls[0][0] as any;
91
+ expect(command).toBeInstanceOf(CompleteMultipartUploadCommand);
92
+ expect(command.input).toEqual({
93
+ Bucket: bucket,
94
+ Key: 'k',
95
+ UploadId: 'up-1',
96
+ MultipartUpload: {
97
+ Parts: [
98
+ { PartNumber: 1, ETag: '"a"' },
99
+ { PartNumber: 2, ETag: '"b"' },
100
+ ],
101
+ },
102
+ });
103
+ });
104
+
105
+ it('abortMultipartUpload aborta o upload da chave', async () => {
106
+ const send = mockSend(() => ({}));
107
+ await build().abortMultipartUpload('k', 'up-1');
108
+ const command = send.mock.calls[0][0] as any;
109
+ expect(command).toBeInstanceOf(AbortMultipartUploadCommand);
110
+ expect(command.input).toEqual({ Bucket: bucket, Key: 'k', UploadId: 'up-1' });
111
+ });
112
+
113
+ it('readHead pede só os primeiros bytes e corta o que vier a mais', async () => {
114
+ const send = mockSend(() => ({ Body: Readable.from([Buffer.from('0123456789'), Buffer.from('abc')]) }));
115
+
116
+ const head = await build().readHead('k', 4);
117
+
118
+ const command = send.mock.calls[0][0] as any;
119
+ expect(command).toBeInstanceOf(GetObjectCommand);
120
+ expect(command.input).toEqual({ Bucket: bucket, Key: 'k', Range: 'bytes=0-3' });
121
+ expect(head.toString()).toBe('0123');
122
+ });
123
+ });
@@ -0,0 +1,126 @@
1
+ import {
2
+ AbortMultipartUploadCommand,
3
+ CompleteMultipartUploadCommand,
4
+ CreateMultipartUploadCommand,
5
+ GetObjectCommand,
6
+ ObjectCannedACL,
7
+ S3Client,
8
+ UploadPartCommand,
9
+ } from '@aws-sdk/client-s3';
10
+ import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
11
+ import { Readable } from 'stream';
12
+ import { MultipartUploadPart } from './abstract.provider';
13
+
14
+ /**
15
+ * Upload multipart assinado, comum ao S3 e ao Spaces (mesma API): o servidor abre,
16
+ * assina e fecha; o cliente so recebe URLs que valem para uma parte e expiram.
17
+ * Mesmo desenho de libraries/studio/src/common/studio-s3-compatible.client.ts.
18
+ */
19
+ export async function s3CreateMultipartUpload(
20
+ client: S3Client,
21
+ bucket: string,
22
+ key: string,
23
+ contentType: string,
24
+ acl?: ObjectCannedACL,
25
+ ): Promise<string> {
26
+ const result = await client.send(
27
+ new CreateMultipartUploadCommand({
28
+ Bucket: bucket,
29
+ Key: key,
30
+ ContentType: contentType,
31
+ ...(acl ? { ACL: acl } : {}),
32
+ }),
33
+ );
34
+
35
+ if (!result.UploadId) {
36
+ throw new Error('The storage did not return a multipart upload id.');
37
+ }
38
+
39
+ return result.UploadId;
40
+ }
41
+
42
+ export async function s3SignUploadParts(
43
+ client: S3Client,
44
+ bucket: string,
45
+ key: string,
46
+ uploadId: string,
47
+ partNumbers: number[],
48
+ ttlSeconds: number,
49
+ ): Promise<Array<{ partNumber: number; url: string }>> {
50
+ return Promise.all(
51
+ partNumbers.map(async (partNumber) => ({
52
+ partNumber,
53
+ url: await getSignedUrl(
54
+ client as any,
55
+ new UploadPartCommand({
56
+ Bucket: bucket,
57
+ Key: key,
58
+ UploadId: uploadId,
59
+ PartNumber: partNumber,
60
+ }),
61
+ { expiresIn: ttlSeconds },
62
+ ),
63
+ })),
64
+ );
65
+ }
66
+
67
+ export async function s3CompleteMultipartUpload(
68
+ client: S3Client,
69
+ bucket: string,
70
+ key: string,
71
+ uploadId: string,
72
+ parts: MultipartUploadPart[],
73
+ ): Promise<void> {
74
+ await client.send(
75
+ new CompleteMultipartUploadCommand({
76
+ Bucket: bucket,
77
+ Key: key,
78
+ UploadId: uploadId,
79
+ MultipartUpload: {
80
+ Parts: [...parts]
81
+ .sort((a, b) => a.partNumber - b.partNumber)
82
+ .map((part) => ({ PartNumber: part.partNumber, ETag: part.etag })),
83
+ },
84
+ }),
85
+ );
86
+ }
87
+
88
+ export async function s3AbortMultipartUpload(
89
+ client: S3Client,
90
+ bucket: string,
91
+ key: string,
92
+ uploadId: string,
93
+ ): Promise<void> {
94
+ await client.send(
95
+ new AbortMultipartUploadCommand({ Bucket: bucket, Key: key, UploadId: uploadId }),
96
+ );
97
+ }
98
+
99
+ /** Range GET dos primeiros `bytes`: basta para o sniff do tipo, sem baixar o objeto. */
100
+ export async function s3ReadHead(
101
+ client: S3Client,
102
+ bucket: string,
103
+ key: string,
104
+ bytes: number,
105
+ ): Promise<Buffer> {
106
+ const length = Math.max(1, Math.floor(bytes));
107
+ const result = await client.send(
108
+ new GetObjectCommand({ Bucket: bucket, Key: key, Range: `bytes=0-${length - 1}` }),
109
+ );
110
+
111
+ const stream = result.Body as Readable;
112
+ const chunks: Buffer[] = [];
113
+ let total = 0;
114
+ for await (const chunk of stream) {
115
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
116
+ chunks.push(buffer);
117
+ total += buffer.length;
118
+ // Storage que ignora o Range devolveria o objeto inteiro: para no que basta.
119
+ if (total >= length) {
120
+ stream.destroy?.();
121
+ break;
122
+ }
123
+ }
124
+
125
+ return Buffer.concat(chunks).subarray(0, length);
126
+ }
@@ -14,7 +14,14 @@ import { BadRequestException } from '@nestjs/common';
14
14
  import { createReadStream } from 'fs';
15
15
  import { basename } from 'path';
16
16
  import { Readable } from 'stream';
17
- import { AbstractProvider } from './abstract.provider';
17
+ import { AbstractProvider, MultipartUploadPart } from './abstract.provider';
18
+ import {
19
+ s3AbortMultipartUpload,
20
+ s3CompleteMultipartUpload,
21
+ s3CreateMultipartUpload,
22
+ s3ReadHead,
23
+ s3SignUploadParts,
24
+ } from './s3-multipart.util';
18
25
 
19
26
  export class S3Provider extends AbstractProvider {
20
27
  private S3: S3 | undefined;
@@ -253,4 +260,61 @@ export class S3Provider extends AbstractProvider {
253
260
  // Use S3Client instance for getSignedUrl
254
261
  return await getSignedUrl(s3Client as any, command, { expiresIn: expires });
255
262
  }
263
+
264
+ async createMultipartUpload(key: string, contentType: string): Promise<string> {
265
+ return s3CreateMultipartUpload(
266
+ await this.getS3Client(),
267
+ this.setting['storage-s3-bucket'],
268
+ this.resolveS3Key(key),
269
+ contentType,
270
+ );
271
+ }
272
+
273
+ async signUploadParts(
274
+ key: string,
275
+ uploadId: string,
276
+ partNumbers: number[],
277
+ ttlSeconds: number,
278
+ ) {
279
+ return s3SignUploadParts(
280
+ await this.getS3Client(),
281
+ this.setting['storage-s3-bucket'],
282
+ this.resolveS3Key(key),
283
+ uploadId,
284
+ partNumbers,
285
+ ttlSeconds,
286
+ );
287
+ }
288
+
289
+ async completeMultipartUpload(
290
+ key: string,
291
+ uploadId: string,
292
+ parts: MultipartUploadPart[],
293
+ ): Promise<void> {
294
+ await s3CompleteMultipartUpload(
295
+ await this.getS3Client(),
296
+ this.setting['storage-s3-bucket'],
297
+ this.resolveS3Key(key),
298
+ uploadId,
299
+ parts,
300
+ );
301
+ }
302
+
303
+ async abortMultipartUpload(key: string, uploadId: string): Promise<void> {
304
+ await s3AbortMultipartUpload(
305
+ await this.getS3Client(),
306
+ this.setting['storage-s3-bucket'],
307
+ this.resolveS3Key(key),
308
+ uploadId,
309
+ );
310
+ }
311
+
312
+ async readHead(key: string, bytes: number): Promise<Buffer> {
313
+ return s3ReadHead(
314
+ await this.getS3Client(),
315
+ this.setting['storage-s3-bucket'],
316
+ this.resolveS3Key(key),
317
+ bytes,
318
+ );
319
+ }
256
320
  }
@@ -14,7 +14,14 @@ import { BadRequestException } from '@nestjs/common';
14
14
  import { createReadStream } from 'fs';
15
15
  import { basename } from 'path';
16
16
  import { Readable } from 'stream';
17
- import { AbstractProvider } from './abstract.provider';
17
+ import { AbstractProvider, MultipartUploadPart } from './abstract.provider';
18
+ import {
19
+ s3AbortMultipartUpload,
20
+ s3CompleteMultipartUpload,
21
+ s3CreateMultipartUpload,
22
+ s3ReadHead,
23
+ s3SignUploadParts,
24
+ } from './s3-multipart.util';
18
25
 
19
26
  export class SpacesProvider extends AbstractProvider {
20
27
  private S3: S3 | undefined;
@@ -280,4 +287,63 @@ export class SpacesProvider extends AbstractProvider {
280
287
  });
281
288
  return await getSignedUrl(s3Client as any, command, { expiresIn: expires });
282
289
  }
290
+
291
+ /** Mesmo ACL dos demais uploads deste provider (o objeto nasce com ele no create). */
292
+ async createMultipartUpload(key: string, contentType: string): Promise<string> {
293
+ return s3CreateMultipartUpload(
294
+ await this.getS3Client(),
295
+ this.setting['storage-spaces-bucket'],
296
+ this.resolveKey(key),
297
+ contentType,
298
+ 'public-read',
299
+ );
300
+ }
301
+
302
+ async signUploadParts(
303
+ key: string,
304
+ uploadId: string,
305
+ partNumbers: number[],
306
+ ttlSeconds: number,
307
+ ) {
308
+ return s3SignUploadParts(
309
+ await this.getS3Client(),
310
+ this.setting['storage-spaces-bucket'],
311
+ this.resolveKey(key),
312
+ uploadId,
313
+ partNumbers,
314
+ ttlSeconds,
315
+ );
316
+ }
317
+
318
+ async completeMultipartUpload(
319
+ key: string,
320
+ uploadId: string,
321
+ parts: MultipartUploadPart[],
322
+ ): Promise<void> {
323
+ await s3CompleteMultipartUpload(
324
+ await this.getS3Client(),
325
+ this.setting['storage-spaces-bucket'],
326
+ this.resolveKey(key),
327
+ uploadId,
328
+ parts,
329
+ );
330
+ }
331
+
332
+ async abortMultipartUpload(key: string, uploadId: string): Promise<void> {
333
+ await s3AbortMultipartUpload(
334
+ await this.getS3Client(),
335
+ this.setting['storage-spaces-bucket'],
336
+ this.resolveKey(key),
337
+ uploadId,
338
+ );
339
+ }
340
+
341
+ async readHead(key: string, bytes: number): Promise<Buffer> {
342
+ return s3ReadHead(
343
+ await this.getS3Client(),
344
+ this.setting['storage-spaces-bucket'],
345
+ this.resolveKey(key),
346
+ bytes,
347
+ );
348
+ }
283
349
  }
package/src/index.ts CHANGED
@@ -33,6 +33,9 @@ export * from './mail-layout/mail-layout.service';
33
33
  export * from './message-template/message-template.module';
34
34
  export * from './message-template/message-template.registry';
35
35
  export * from './message-template/message-template.service';
36
+ export * from './document-kit/document-kit.module';
37
+ export * from './document-kit/document-kit.registry';
38
+ export * from './document-kit/document-kit.service';
36
39
  export * from './mail/mail.module';
37
40
  export * from './mail/mail.service';
38
41
 
@@ -57,8 +60,10 @@ export * from './file/mcp-file-source.schema';
57
60
  export * from './file/mimetype-sniff.util';
58
61
  export * from './file/remote-fetch.util';
59
62
 
63
+ export * from './pdf/document';
60
64
  export * from './pdf/pdf.module';
61
65
  export * from './pdf/pdf.service';
66
+ export * from './pdf/templates/commercial-proposal.template';
62
67
  export * from './setting/brand-asset.service';
63
68
  export * from './setting/setting.module';
64
69
  export * from './setting/setting.service';
@@ -76,6 +76,32 @@ export const INTEGRATION_SECRET_KEYS: Record<string, string[]> = {
76
76
  // uso, então ele é reescrito no perfil a cada renovação: se não estiver aqui,
77
77
  // cada renovação grava a credencial em texto puro no banco.
78
78
  'melhor-envio': ['access_token', 'client_secret', 'refresh_token'],
79
+ // Redes sociais
80
+ // O app_secret troca o code do OAuth por token de qualquer Pagina que autorize o
81
+ // app; o webhook_verify_token e comparado no handshake dos webhooks da Meta.
82
+ // Os tokens das contas conectadas NAO moram aqui (ficam cifrados em
83
+ // social_channel, uma linha por Pagina/conta).
84
+ 'meta-social': ['app_secret', 'webhook_verify_token'],
85
+ // App do Instagram (Instagram API with Instagram Login) — o segredo do app do
86
+ // INSTAGRAM, nao o do app da Meta. Troca o code do OAuth pelo token de
87
+ // qualquer conta que autorize o app; os tokens de 60 dias das contas ficam
88
+ // cifrados em social_channel. O webhook_verify_token e comparado no
89
+ // handshake do webhook do Instagram (inbox de comentarios e DMs).
90
+ 'instagram-social': ['app_secret', 'webhook_verify_token'],
91
+ // Apps do Google para o modulo social (um projeto por rede). O client_secret
92
+ // troca o code do OAuth pelo refresh token de qualquer canal/local que
93
+ // autorize o app; os refresh tokens ficam cifrados em social_channel.
94
+ 'youtube-social': ['client_secret'],
95
+ 'google-business-social': ['client_secret'],
96
+ // App do LinkedIn para paginas de empresa (Community Management API). O
97
+ // client_secret troca o code do OAuth e renova o access token de qualquer
98
+ // pagina que autorize o app; as credenciais das paginas (envelope lir1:/lia1:)
99
+ // ficam cifradas em social_channel.
100
+ 'linkedin-social': ['client_secret'],
101
+ // App do X (OAuth 2.0, cliente confidencial). O client_secret vai no header
102
+ // Basic da troca do code, da renovacao e da revogacao; os tokens de cada
103
+ // conta (que rotacionam) ficam cifrados em social_channel_oauth_token.
104
+ 'x-social': ['client_secret'],
79
105
  };
80
106
 
81
107
  /** Chaves sensíveis de um provider (vazio se desconhecido). */
@@ -544,6 +544,7 @@ export class IntegrationProfileService {
544
544
  provider: provider.slug,
545
545
  ...(outcome.info ? { info: outcome.info } : {}),
546
546
  ...(outcome.warning ? { warning: outcome.warning } : {}),
547
+ ...(outcome.warningCodes?.length ? { warning_codes: outcome.warningCodes } : {}),
547
548
  tested_at: new Date().toISOString(),
548
549
  persisted,
549
550
  };
@@ -3,11 +3,45 @@ import {
3
3
  fail,
4
4
  pass,
5
5
  probeJson,
6
+ redactSecrets,
6
7
  requireFields,
7
8
  str,
9
+ type ProbeResult,
8
10
  } from './http-probe';
9
11
  import { IntegrationTester } from './integration-tester.types';
10
12
 
13
+ /**
14
+ * A frase do veredito mais o que o provider de fato respondeu.
15
+ *
16
+ * Sem isso todo 401 virava a mesma sentenca fixa e a resposta do provider —
17
+ * que o `probeJson` ja captura — era jogada fora. Foi o que fez um header de
18
+ * organizacao errado ser reportado como "chave invalida", mandando conferir o
19
+ * campo que estava certo.
20
+ *
21
+ * O resultado de um tester NAO passa por `sanitizeTesterError` (esse so cobre o
22
+ * caminho de excecao), entao a redacao dos segredos acontece aqui.
23
+ */
24
+ function withProviderDetail(
25
+ base: string,
26
+ result: ProbeResult,
27
+ secretValues: string[],
28
+ ): string {
29
+ const raw = String(result.body?.error?.message ?? '').trim();
30
+ if (!raw) return base;
31
+
32
+ const detail = redactSecrets(raw, secretValues).trim();
33
+ return detail ? `${base} ${detail}` : base;
34
+ }
35
+
36
+ /** O 401 da OpenAI culpa a chave ou a organizacao? A resposta dela ja diz. */
37
+ function isOpenAiOrganizationProblem(result: ProbeResult): boolean {
38
+ const code = String(result.body?.error?.code ?? '');
39
+ const message = String(result.body?.error?.message ?? '');
40
+ return (
41
+ code === 'invalid_organization' || /no such organization/i.test(message)
42
+ );
43
+ }
44
+
11
45
  /**
12
46
  * Testadores dos providers de IA e geração de vídeo.
13
47
  *
@@ -33,7 +67,26 @@ const openAiTester: IntegrationTester = {
33
67
  ctx.signal,
34
68
  );
35
69
 
36
- if (result.status === 401) return fail('Invalid OpenAI API key.');
70
+ if (result.status === 401) {
71
+ // O header de organizacao e opcional e so este testador o envia: o chat
72
+ // real nunca manda. Apontar o campo certo evita trocar uma chave boa.
73
+ if (isOpenAiOrganizationProblem(result)) {
74
+ return fail(
75
+ withProviderDetail(
76
+ organization
77
+ ? `OpenAI rejected the organization "${organization}". ` +
78
+ 'Check the Organization ID field (it looks like "org-..."), ' +
79
+ 'or leave it empty.'
80
+ : 'OpenAI rejected the organization.',
81
+ result,
82
+ ctx.secretValues,
83
+ ),
84
+ );
85
+ }
86
+ return fail(
87
+ withProviderDetail('Invalid OpenAI API key.', result, ctx.secretValues),
88
+ );
89
+ }
37
90
  if (result.status === 403) {
38
91
  return fail('This API key has no permission to list models.');
39
92
  }
@@ -72,7 +125,9 @@ const geminiTester: IntegrationTester = {
72
125
  const result = await probeJson(url, {}, ctx.signal);
73
126
 
74
127
  if (result.status === 400 || result.status === 401 || result.status === 403) {
75
- return fail('Invalid Gemini API key.');
128
+ return fail(
129
+ withProviderDetail('Invalid Gemini API key.', result, ctx.secretValues),
130
+ );
76
131
  }
77
132
  if (!result.ok) {
78
133
  return fail(describeHttpError(result, 'Google rejected the credentials.'));
@@ -102,7 +157,11 @@ const claudeTester: IntegrationTester = {
102
157
  ctx.signal,
103
158
  );
104
159
 
105
- if (result.status === 401) return fail('Invalid Anthropic API key.');
160
+ if (result.status === 401) {
161
+ return fail(
162
+ withProviderDetail('Invalid Anthropic API key.', result, ctx.secretValues),
163
+ );
164
+ }
106
165
  if (!result.ok) {
107
166
  return fail(describeHttpError(result, 'Anthropic rejected the credentials.'));
108
167
  }
@@ -124,7 +183,11 @@ const deepseekTester: IntegrationTester = {
124
183
  ctx.signal,
125
184
  );
126
185
 
127
- if (result.status === 401) return fail('Invalid DeepSeek API key.');
186
+ if (result.status === 401) {
187
+ return fail(
188
+ withProviderDetail('Invalid DeepSeek API key.', result, ctx.secretValues),
189
+ );
190
+ }
128
191
  if (!result.ok) {
129
192
  return fail(describeHttpError(result, 'DeepSeek rejected the credentials.'));
130
193
  }
@@ -147,7 +210,9 @@ const lumaTester: IntegrationTester = {
147
210
  );
148
211
 
149
212
  if (result.status === 401 || result.status === 403) {
150
- return fail('Invalid Luma API key.');
213
+ return fail(
214
+ withProviderDetail('Invalid Luma API key.', result, ctx.secretValues),
215
+ );
151
216
  }
152
217
  if (!result.ok) {
153
218
  return fail(describeHttpError(result, 'Luma rejected the credentials.'));
@@ -81,16 +81,18 @@ export async function probeJson(
81
81
  signal: AbortSignal,
82
82
  ): Promise<ProbeResult> {
83
83
  const response = await fetch(url, { ...init, signal });
84
- const text = (await response.text().catch(() => '')).slice(0, 2_000);
84
+ const raw = await response.text().catch(() => '');
85
85
 
86
+ // O parse usa o corpo inteiro: um access token JWT (ex.: o do Graph com muitas
87
+ // permissões de aplicação) passa fácil de 2 KB, e o JSON truncado viraria `{}`.
86
88
  let body: any = {};
87
89
  try {
88
- body = text ? JSON.parse(text) : {};
90
+ body = raw ? JSON.parse(raw) : {};
89
91
  } catch {
90
92
  body = {};
91
93
  }
92
94
 
93
- return { ok: response.ok, status: response.status, body, text };
95
+ return { ok: response.ok, status: response.status, body, text: raw.slice(0, 2_000) };
94
96
  }
95
97
 
96
98
  /** Remove qualquer ocorrência dos segredos do texto. */