@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.
- package/dist/access-log/access-log.interceptor.d.ts +10 -0
- package/dist/access-log/access-log.interceptor.d.ts.map +1 -1
- package/dist/access-log/access-log.interceptor.js +15 -1
- package/dist/access-log/access-log.interceptor.js.map +1 -1
- package/dist/ai/ai.controller.d.ts +2 -0
- package/dist/ai/ai.controller.d.ts.map +1 -1
- package/dist/ai/ai.service.d.ts +86 -2
- package/dist/ai/ai.service.d.ts.map +1 -1
- package/dist/ai/ai.service.js +304 -23
- package/dist/ai/ai.service.js.map +1 -1
- package/dist/ai-instruction/ai-prompt-resolver.service.d.ts +12 -0
- package/dist/ai-instruction/ai-prompt-resolver.service.d.ts.map +1 -1
- package/dist/ai-instruction/ai-prompt-resolver.service.js +26 -0
- package/dist/ai-instruction/ai-prompt-resolver.service.js.map +1 -1
- package/dist/core.module.d.ts.map +1 -1
- package/dist/core.module.js +3 -0
- package/dist/core.module.js.map +1 -1
- package/dist/document-kit/document-kit.module.d.ts +3 -0
- package/dist/document-kit/document-kit.module.d.ts.map +1 -0
- package/dist/document-kit/document-kit.module.js +24 -0
- package/dist/document-kit/document-kit.module.js.map +1 -0
- package/dist/document-kit/document-kit.registry.d.ts +55 -0
- package/dist/document-kit/document-kit.registry.d.ts.map +1 -0
- package/dist/document-kit/document-kit.registry.js +91 -0
- package/dist/document-kit/document-kit.registry.js.map +1 -0
- package/dist/document-kit/document-kit.service.d.ts +49 -0
- package/dist/document-kit/document-kit.service.d.ts.map +1 -0
- package/dist/document-kit/document-kit.service.js +135 -0
- package/dist/document-kit/document-kit.service.js.map +1 -0
- package/dist/file/file-upload-ticket.service.d.ts +1 -1
- package/dist/file/file.controller.d.ts +3 -3
- package/dist/file/file.service.d.ts +136 -13
- package/dist/file/file.service.d.ts.map +1 -1
- package/dist/file/file.service.js +297 -27
- package/dist/file/file.service.js.map +1 -1
- package/dist/file/mcp-file-source.schema.d.ts.map +1 -1
- package/dist/file/mcp-file-source.schema.js +8 -5
- package/dist/file/mcp-file-source.schema.js.map +1 -1
- package/dist/file/provider/abstract.provider.d.ts +33 -0
- package/dist/file/provider/abstract.provider.d.ts.map +1 -1
- package/dist/file/provider/abstract.provider.js +9 -0
- package/dist/file/provider/abstract.provider.js.map +1 -1
- package/dist/file/provider/s3-multipart.util.d.ts +17 -0
- package/dist/file/provider/s3-multipart.util.d.ts.map +1 -0
- package/dist/file/provider/s3-multipart.util.js +88 -0
- package/dist/file/provider/s3-multipart.util.js.map +1 -0
- package/dist/file/provider/s3.provider.d.ts +9 -1
- package/dist/file/provider/s3.provider.d.ts.map +1 -1
- package/dist/file/provider/s3.provider.js +16 -0
- package/dist/file/provider/s3.provider.js.map +1 -1
- package/dist/file/provider/spaces.provider.d.ts +10 -1
- package/dist/file/provider/spaces.provider.d.ts.map +1 -1
- package/dist/file/provider/spaces.provider.js +17 -0
- package/dist/file/provider/spaces.provider.js.map +1 -1
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/integration-profile/integration-profile.controller.d.ts +1 -0
- package/dist/integration-profile/integration-profile.controller.d.ts.map +1 -1
- package/dist/integration-profile/integration-profile.secrets.d.ts.map +1 -1
- package/dist/integration-profile/integration-profile.secrets.js +26 -0
- package/dist/integration-profile/integration-profile.secrets.js.map +1 -1
- package/dist/integration-profile/integration-profile.service.d.ts +1 -0
- package/dist/integration-profile/integration-profile.service.d.ts.map +1 -1
- package/dist/integration-profile/integration-profile.service.js +3 -3
- package/dist/integration-profile/integration-profile.service.js.map +1 -1
- package/dist/integration-profile/testers/ai.testers.d.ts.map +1 -1
- package/dist/integration-profile/testers/ai.testers.js +46 -8
- package/dist/integration-profile/testers/ai.testers.js.map +1 -1
- package/dist/integration-profile/testers/http-probe.d.ts.map +1 -1
- package/dist/integration-profile/testers/http-probe.js +5 -3
- package/dist/integration-profile/testers/http-probe.js.map +1 -1
- package/dist/integration-profile/testers/integration-tester.types.d.ts +5 -0
- package/dist/integration-profile/testers/integration-tester.types.d.ts.map +1 -1
- package/dist/mail/mail.service.d.ts.map +1 -1
- package/dist/mail/mail.service.js +16 -1
- package/dist/mail/mail.service.js.map +1 -1
- package/dist/mcp/mcp-server.service.d.ts.map +1 -1
- package/dist/mcp/mcp-server.service.js +10 -1
- package/dist/mcp/mcp-server.service.js.map +1 -1
- package/dist/mcp/mcp.module.d.ts.map +1 -1
- package/dist/mcp/mcp.module.js +2 -0
- package/dist/mcp/mcp.module.js.map +1 -1
- package/dist/mcp/tools/core-access-log.mcp-tools.d.ts +17 -0
- package/dist/mcp/tools/core-access-log.mcp-tools.d.ts.map +1 -0
- package/dist/mcp/tools/core-access-log.mcp-tools.js +74 -0
- package/dist/mcp/tools/core-access-log.mcp-tools.js.map +1 -0
- package/dist/mcp/tools/core-files.mcp-tools.js +4 -4
- package/dist/mcp/tools/core-files.mcp-tools.js.map +1 -1
- package/dist/meeting/meeting-provider.registry.d.ts +72 -0
- package/dist/meeting/meeting-provider.registry.d.ts.map +1 -1
- package/dist/meeting/meeting-provider.registry.js.map +1 -1
- package/dist/oauth/oauth-token-broker.service.d.ts +50 -1
- package/dist/oauth/oauth-token-broker.service.d.ts.map +1 -1
- package/dist/oauth/oauth-token-broker.service.js +105 -3
- package/dist/oauth/oauth-token-broker.service.js.map +1 -1
- package/dist/oauth/providers/microsoft-entra-id.provider.d.ts +6 -0
- package/dist/oauth/providers/microsoft-entra-id.provider.d.ts.map +1 -1
- package/dist/oauth/providers/microsoft-entra-id.provider.js +30 -4
- package/dist/oauth/providers/microsoft-entra-id.provider.js.map +1 -1
- package/dist/pdf/document/charts/chart-bar.d.ts +24 -0
- package/dist/pdf/document/charts/chart-bar.d.ts.map +1 -0
- package/dist/pdf/document/charts/chart-bar.js +117 -0
- package/dist/pdf/document/charts/chart-bar.js.map +1 -0
- package/dist/pdf/document/charts/chart-donut.d.ts +16 -0
- package/dist/pdf/document/charts/chart-donut.d.ts.map +1 -0
- package/dist/pdf/document/charts/chart-donut.js +89 -0
- package/dist/pdf/document/charts/chart-donut.js.map +1 -0
- package/dist/pdf/document/charts/chart-gauge.d.ts +15 -0
- package/dist/pdf/document/charts/chart-gauge.d.ts.map +1 -0
- package/dist/pdf/document/charts/chart-gauge.js +60 -0
- package/dist/pdf/document/charts/chart-gauge.js.map +1 -0
- package/dist/pdf/document/charts/chart-line.d.ts +29 -0
- package/dist/pdf/document/charts/chart-line.d.ts.map +1 -0
- package/dist/pdf/document/charts/chart-line.js +89 -0
- package/dist/pdf/document/charts/chart-line.js.map +1 -0
- package/dist/pdf/document/charts/chart-primitives.d.ts +64 -0
- package/dist/pdf/document/charts/chart-primitives.d.ts.map +1 -0
- package/dist/pdf/document/charts/chart-primitives.js +110 -0
- package/dist/pdf/document/charts/chart-primitives.js.map +1 -0
- package/dist/pdf/document/charts/chart-radar.d.ts +13 -0
- package/dist/pdf/document/charts/chart-radar.d.ts.map +1 -0
- package/dist/pdf/document/charts/chart-radar.js +83 -0
- package/dist/pdf/document/charts/chart-radar.js.map +1 -0
- package/dist/pdf/document/charts/chart-scale.d.ts +39 -0
- package/dist/pdf/document/charts/chart-scale.d.ts.map +1 -0
- package/dist/pdf/document/charts/chart-scale.js +118 -0
- package/dist/pdf/document/charts/chart-scale.js.map +1 -0
- package/dist/pdf/document/charts/index.d.ts +8 -0
- package/dist/pdf/document/charts/index.d.ts.map +1 -0
- package/dist/pdf/document/charts/index.js +24 -0
- package/dist/pdf/document/charts/index.js.map +1 -0
- package/dist/pdf/document/index.d.ts +8 -0
- package/dist/pdf/document/index.d.ts.map +1 -0
- package/dist/pdf/document/index.js +24 -0
- package/dist/pdf/document/index.js.map +1 -0
- package/dist/pdf/document/pdf-document.chrome.d.ts +20 -0
- package/dist/pdf/document/pdf-document.chrome.d.ts.map +1 -0
- package/dist/pdf/document/pdf-document.chrome.js +36 -0
- package/dist/pdf/document/pdf-document.chrome.js.map +1 -0
- package/dist/pdf/document/pdf-document.css.d.ts +20 -0
- package/dist/pdf/document/pdf-document.css.d.ts.map +1 -0
- package/dist/pdf/document/pdf-document.css.js +327 -0
- package/dist/pdf/document/pdf-document.css.js.map +1 -0
- package/dist/pdf/document/pdf-document.renderer.d.ts +10 -0
- package/dist/pdf/document/pdf-document.renderer.d.ts.map +1 -0
- package/dist/pdf/document/pdf-document.renderer.js +226 -0
- package/dist/pdf/document/pdf-document.renderer.js.map +1 -0
- package/dist/pdf/document/pdf-document.service.d.ts +27 -0
- package/dist/pdf/document/pdf-document.service.d.ts.map +1 -0
- package/dist/pdf/document/pdf-document.service.js +81 -0
- package/dist/pdf/document/pdf-document.service.js.map +1 -0
- package/dist/pdf/document/pdf-document.theme.d.ts +41 -0
- package/dist/pdf/document/pdf-document.theme.d.ts.map +1 -0
- package/dist/pdf/document/pdf-document.theme.js +105 -0
- package/dist/pdf/document/pdf-document.theme.js.map +1 -0
- package/dist/pdf/document/pdf-document.types.d.ts +164 -0
- package/dist/pdf/document/pdf-document.types.d.ts.map +1 -0
- package/dist/pdf/document/pdf-document.types.js +18 -0
- package/dist/pdf/document/pdf-document.types.js.map +1 -0
- package/dist/pdf/pdf.module.d.ts.map +1 -1
- package/dist/pdf/pdf.module.js +3 -2
- package/dist/pdf/pdf.module.js.map +1 -1
- package/dist/pdf/pdf.service.d.ts +75 -1
- package/dist/pdf/pdf.service.d.ts.map +1 -1
- package/dist/pdf/pdf.service.js +167 -63
- package/dist/pdf/pdf.service.js.map +1 -1
- package/dist/pdf/templates/commercial-proposal.template.d.ts +96 -0
- package/dist/pdf/templates/commercial-proposal.template.d.ts.map +1 -0
- package/dist/pdf/templates/commercial-proposal.template.js +276 -0
- package/dist/pdf/templates/commercial-proposal.template.js.map +1 -0
- package/dist/setting/favicon.controller.d.ts +26 -0
- package/dist/setting/favicon.controller.d.ts.map +1 -0
- package/dist/setting/favicon.controller.js +130 -0
- package/dist/setting/favicon.controller.js.map +1 -0
- package/dist/setting/setting.module.d.ts.map +1 -1
- package/dist/setting/setting.module.js +2 -1
- package/dist/setting/setting.module.js.map +1 -1
- package/dist/setting/setting.service.d.ts.map +1 -1
- package/dist/setting/setting.service.js +3 -0
- package/dist/setting/setting.service.js.map +1 -1
- package/dist/task/task.service.d.ts +6 -0
- package/dist/task/task.service.d.ts.map +1 -1
- package/dist/task/task.service.js +44 -13
- package/dist/task/task.service.js.map +1 -1
- package/dist/whatsapp-sender/whatsapp-sender.service.d.ts +35 -2
- package/dist/whatsapp-sender/whatsapp-sender.service.d.ts.map +1 -1
- package/dist/whatsapp-sender/whatsapp-sender.service.js +107 -22
- package/dist/whatsapp-sender/whatsapp-sender.service.js.map +1 -1
- package/hedhog/data/integration_type.yaml +6 -0
- package/hedhog/data/route.yaml +8 -0
- package/hedhog/data/setting_group.yaml +52 -0
- package/hedhog/frontend/app/configurations/[slug]/components/setting-field.test.tsx.ejs +85 -2
- package/hedhog/frontend/app/configurations/[slug]/components/setting-field.tsx.ejs +576 -491
- package/hedhog/frontend/app/users/simulate-access-dialog.test.tsx.ejs +29 -50
- package/hedhog/frontend/app/users/simulate-access-dialog.tsx.ejs +146 -50
- package/hedhog/frontend/messages/en.json +81 -2
- package/hedhog/frontend/messages/pt.json +81 -2
- package/hedhog/table/file.yaml +3 -1
- package/package.json +5 -5
- package/src/access-log/access-log.interceptor.spec.ts +50 -0
- package/src/access-log/access-log.interceptor.ts +18 -0
- package/src/ai/ai-attachment.fragments.spec.ts +56 -3
- package/src/ai/ai.service.responses.spec.ts +429 -0
- package/src/ai/ai.service.ts +409 -27
- package/src/ai-instruction/ai-prompt-resolver.service.spec.ts +44 -0
- package/src/ai-instruction/ai-prompt-resolver.service.ts +29 -0
- package/src/core.module.ts +3 -0
- package/src/document-kit/document-kit.module.ts +12 -0
- package/src/document-kit/document-kit.registry.ts +125 -0
- package/src/document-kit/document-kit.service.spec.ts +156 -0
- package/src/document-kit/document-kit.service.ts +173 -0
- package/src/file/file-direct-upload.spec.ts +344 -0
- package/src/file/file.service.spec.ts +168 -1
- package/src/file/file.service.ts +435 -36
- package/src/file/mcp-file-source.schema.ts +8 -5
- package/src/file/provider/abstract.provider.ts +57 -0
- package/src/file/provider/s3-multipart.spec.ts +123 -0
- package/src/file/provider/s3-multipart.util.ts +126 -0
- package/src/file/provider/s3.provider.ts +65 -1
- package/src/file/provider/spaces.provider.ts +67 -1
- package/src/index.ts +5 -0
- package/src/integration-profile/integration-profile.secrets.ts +26 -0
- package/src/integration-profile/integration-profile.service.ts +1 -0
- package/src/integration-profile/testers/ai.testers.ts +70 -5
- package/src/integration-profile/testers/http-probe.ts +5 -3
- package/src/integration-profile/testers/integration-tester.types.ts +5 -0
- package/src/integration-profile/testers/oauth.testers.spec.ts +13 -0
- package/src/integration-profile/testers/remaining.testers.spec.ts +65 -1
- package/src/integration-profile/testers/tester-coverage.spec.ts +12 -0
- package/src/mail/mail.service.spec.ts +25 -0
- package/src/mail/mail.service.ts +18 -1
- package/src/mcp/mcp-server.service.spec.ts +16 -0
- package/src/mcp/mcp-server.service.ts +10 -1
- package/src/mcp/mcp.module.ts +2 -0
- package/src/mcp/tools/core-access-log.mcp-tools.ts +66 -0
- package/src/mcp/tools/core-files.mcp-tools.ts +4 -4
- package/src/meeting/meeting-provider.registry.ts +80 -0
- package/src/oauth/oauth-token-broker.service.spec.ts +108 -0
- package/src/oauth/oauth-token-broker.service.ts +129 -2
- package/src/oauth/providers/microsoft-entra-id.provider.spec.ts +133 -0
- package/src/oauth/providers/microsoft-entra-id.provider.ts +29 -2
- package/src/pdf/document/charts/chart-bar.ts +217 -0
- package/src/pdf/document/charts/chart-donut.ts +163 -0
- package/src/pdf/document/charts/chart-gauge.ts +94 -0
- package/src/pdf/document/charts/chart-line.ts +159 -0
- package/src/pdf/document/charts/chart-primitives.ts +188 -0
- package/src/pdf/document/charts/chart-radar.ts +136 -0
- package/src/pdf/document/charts/chart-scale.spec.ts +93 -0
- package/src/pdf/document/charts/chart-scale.ts +141 -0
- package/src/pdf/document/charts/charts.spec.ts +157 -0
- package/src/pdf/document/charts/index.ts +7 -0
- package/src/pdf/document/index.ts +7 -0
- package/src/pdf/document/pdf-document.chrome.ts +38 -0
- package/src/pdf/document/pdf-document.css.ts +326 -0
- package/src/pdf/document/pdf-document.renderer.spec.ts +172 -0
- package/src/pdf/document/pdf-document.renderer.ts +317 -0
- package/src/pdf/document/pdf-document.service.ts +77 -0
- package/src/pdf/document/pdf-document.theme.ts +146 -0
- package/src/pdf/document/pdf-document.types.ts +149 -0
- package/src/pdf/pdf.module.ts +3 -2
- package/src/pdf/pdf.service.spec.ts +131 -0
- package/src/pdf/pdf.service.ts +256 -82
- package/src/pdf/templates/commercial-proposal.template.spec.ts +177 -0
- package/src/pdf/templates/commercial-proposal.template.ts +400 -0
- package/src/setting/favicon.controller.spec.ts +92 -0
- package/src/setting/favicon.controller.ts +135 -0
- package/src/setting/setting.module.ts +2 -1
- package/src/setting/setting.service.ts +3 -0
- package/src/task/task.service.spec.ts +77 -1
- package/src/task/task.service.ts +58 -14
- package/src/whatsapp-sender/whatsapp-sender.service.spec.ts +263 -0
- 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)
|
|
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(
|
|
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)
|
|
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)
|
|
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(
|
|
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
|
|
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 =
|
|
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. */
|