@hed-hog/core 0.0.370 → 0.0.371
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/LICENSE.md +21 -0
- package/README.md +2 -0
- package/dist/ai/ai.service.d.ts.map +1 -1
- package/dist/ai/ai.service.js +10 -7
- package/dist/ai/ai.service.js.map +1 -1
- package/dist/app-command/app-command.module.d.ts +3 -0
- package/dist/app-command/app-command.module.d.ts.map +1 -0
- package/dist/app-command/app-command.module.js +22 -0
- package/dist/app-command/app-command.module.js.map +1 -0
- package/dist/app-command/app-command.registry.d.ts +22 -0
- package/dist/app-command/app-command.registry.d.ts.map +1 -0
- package/dist/{webhook-integration/webhook-command.registry.js → app-command/app-command.registry.js} +6 -6
- package/dist/app-command/app-command.registry.js.map +1 -0
- package/dist/auth/auth.controller.d.ts +2 -2
- package/dist/auth/auth.controller.d.ts.map +1 -1
- package/dist/auth/auth.controller.js +20 -7
- package/dist/auth/auth.controller.js.map +1 -1
- package/dist/auth/auth.service.d.ts +3 -3
- package/dist/auth/auth.service.d.ts.map +1 -1
- package/dist/auth/auth.service.js +10 -36
- package/dist/auth/auth.service.js.map +1 -1
- package/dist/auth/dto/forget.dto.d.ts +1 -0
- package/dist/auth/dto/forget.dto.d.ts.map +1 -1
- package/dist/auth/dto/forget.dto.js +5 -0
- package/dist/auth/dto/forget.dto.js.map +1 -1
- package/dist/auth/guards/auth.guard.d.ts.map +1 -1
- package/dist/auth/guards/auth.guard.js +4 -1
- package/dist/auth/guards/auth.guard.js.map +1 -1
- package/dist/challenge/challenge.service.d.ts +8 -7
- package/dist/challenge/challenge.service.d.ts.map +1 -1
- package/dist/challenge/challenge.service.js +27 -19
- package/dist/challenge/challenge.service.js.map +1 -1
- package/dist/file/dto/upload.dto.d.ts +3 -0
- package/dist/file/dto/upload.dto.d.ts.map +1 -1
- package/dist/file/dto/upload.dto.js +22 -0
- package/dist/file/dto/upload.dto.js.map +1 -1
- package/dist/file/file.controller.d.ts +2 -1
- package/dist/file/file.controller.d.ts.map +1 -1
- package/dist/file/file.controller.js +29 -16
- package/dist/file/file.controller.js.map +1 -1
- package/dist/file/file.service.d.ts +51 -1
- package/dist/file/file.service.d.ts.map +1 -1
- package/dist/file/file.service.js +250 -18
- package/dist/file/file.service.js.map +1 -1
- package/dist/file/image-optimization.service.d.ts +17 -4
- package/dist/file/image-optimization.service.d.ts.map +1 -1
- package/dist/file/image-optimization.service.js +101 -46
- package/dist/file/image-optimization.service.js.map +1 -1
- package/dist/file/provider/s3.provider.d.ts.map +1 -1
- package/dist/file/provider/s3.provider.js +15 -7
- package/dist/file/provider/s3.provider.js.map +1 -1
- package/dist/file/provider/spaces.provider.d.ts.map +1 -1
- package/dist/file/provider/spaces.provider.js +18 -8
- package/dist/file/provider/spaces.provider.js.map +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/integration/services/event-subscriber.registry.d.ts +5 -0
- package/dist/integration/services/event-subscriber.registry.d.ts.map +1 -1
- package/dist/integration/services/event-subscriber.registry.js +15 -0
- package/dist/integration/services/event-subscriber.registry.js.map +1 -1
- package/dist/integration/services/outbox.processor.d.ts.map +1 -1
- package/dist/integration/services/outbox.processor.js +2 -4
- package/dist/integration/services/outbox.processor.js.map +1 -1
- package/dist/integration/services/outbox.service.d.ts +4 -2
- package/dist/integration/services/outbox.service.d.ts.map +1 -1
- package/dist/integration/services/outbox.service.js +26 -15
- package/dist/integration/services/outbox.service.js.map +1 -1
- package/dist/integration-profile/integration-credential-crypto.service.d.ts +43 -0
- package/dist/integration-profile/integration-credential-crypto.service.d.ts.map +1 -0
- package/dist/integration-profile/integration-credential-crypto.service.js +142 -0
- package/dist/integration-profile/integration-credential-crypto.service.js.map +1 -0
- package/dist/integration-profile/integration-profile.controller.d.ts +6 -7
- package/dist/integration-profile/integration-profile.controller.d.ts.map +1 -1
- package/dist/integration-profile/integration-profile.controller.js +4 -3
- package/dist/integration-profile/integration-profile.controller.js.map +1 -1
- package/dist/integration-profile/integration-profile.module.d.ts.map +1 -1
- package/dist/integration-profile/integration-profile.module.js +5 -2
- package/dist/integration-profile/integration-profile.module.js.map +1 -1
- package/dist/integration-profile/integration-profile.secrets.d.ts +16 -0
- package/dist/integration-profile/integration-profile.secrets.d.ts.map +1 -0
- package/dist/integration-profile/integration-profile.secrets.js +56 -0
- package/dist/integration-profile/integration-profile.secrets.js.map +1 -0
- package/dist/integration-profile/integration-profile.service.d.ts +125 -8
- package/dist/integration-profile/integration-profile.service.d.ts.map +1 -1
- package/dist/integration-profile/integration-profile.service.js +257 -68
- package/dist/integration-profile/integration-profile.service.js.map +1 -1
- package/dist/integration-profile/integration-profile.utils.d.ts +17 -0
- package/dist/integration-profile/integration-profile.utils.d.ts.map +1 -1
- package/dist/integration-profile/integration-profile.utils.js +31 -0
- package/dist/integration-profile/integration-profile.utils.js.map +1 -1
- package/dist/language/en.json +178 -0
- package/dist/language/pt.json +178 -0
- package/dist/mail/mail.controller.d.ts.map +1 -1
- package/dist/mail/mail.controller.js +5 -28
- package/dist/mail/mail.controller.js.map +1 -1
- package/dist/mail/mail.module.js +18 -18
- package/dist/mail/mail.module.js.map +1 -1
- package/dist/mail/mail.service.d.ts +13 -2
- package/dist/mail/mail.service.d.ts.map +1 -1
- package/dist/mail/mail.service.js +25 -15
- package/dist/mail/mail.service.js.map +1 -1
- package/dist/mcp-chat/mcp-chat.service.js +6 -5
- package/dist/mcp-chat/mcp-chat.service.js.map +1 -1
- package/dist/oauth/providers/facebook.provider.js +5 -5
- package/dist/oauth/providers/facebook.provider.js.map +1 -1
- package/dist/oauth/providers/github.provider.js +5 -5
- package/dist/oauth/providers/github.provider.js.map +1 -1
- package/dist/oauth/providers/google.provider.js +5 -5
- package/dist/oauth/providers/google.provider.js.map +1 -1
- package/dist/oauth/providers/microsoft-entra-id.provider.js +5 -5
- package/dist/oauth/providers/microsoft-entra-id.provider.js.map +1 -1
- package/dist/oauth/providers/microsoft.provider.js +5 -5
- package/dist/oauth/providers/microsoft.provider.js.map +1 -1
- package/dist/pdf/pdf.service.d.ts +4 -0
- package/dist/pdf/pdf.service.d.ts.map +1 -1
- package/dist/pdf/pdf.service.js +16 -9
- package/dist/pdf/pdf.service.js.map +1 -1
- package/dist/profile/profile.controller.d.ts +7 -7
- package/dist/profile/profile.controller.d.ts.map +1 -1
- package/dist/profile/profile.controller.js +28 -21
- package/dist/profile/profile.controller.js.map +1 -1
- package/dist/profile/profile.service.d.ts +7 -7
- package/dist/profile/profile.service.d.ts.map +1 -1
- package/dist/profile/profile.service.js +21 -25
- package/dist/profile/profile.service.js.map +1 -1
- package/dist/setting/setting.service.d.ts +1 -0
- package/dist/setting/setting.service.d.ts.map +1 -1
- package/dist/setting/setting.service.js +15 -2
- package/dist/setting/setting.service.js.map +1 -1
- package/dist/token/token.service.d.ts.map +1 -1
- package/dist/token/token.service.js +2 -1
- package/dist/token/token.service.js.map +1 -1
- package/dist/user/user.controller.d.ts +9 -0
- package/dist/user/user.controller.d.ts.map +1 -1
- package/dist/user/user.controller.js +13 -0
- package/dist/user/user.controller.js.map +1 -1
- package/dist/user/user.service.d.ts +7 -0
- package/dist/user/user.service.d.ts.map +1 -1
- package/dist/user/user.service.js +19 -0
- package/dist/user/user.service.js.map +1 -1
- package/dist/utils/webauthn-origin.d.ts +11 -0
- package/dist/utils/webauthn-origin.d.ts.map +1 -0
- package/dist/utils/webauthn-origin.js +37 -0
- package/dist/utils/webauthn-origin.js.map +1 -0
- package/dist/webhook-integration/webhook-integration.controller.d.ts +13 -0
- package/dist/webhook-integration/webhook-integration.controller.d.ts.map +1 -1
- package/dist/webhook-integration/webhook-integration.controller.js +29 -0
- package/dist/webhook-integration/webhook-integration.controller.js.map +1 -1
- package/dist/webhook-integration/webhook-integration.module.js +3 -3
- package/dist/webhook-integration/webhook-integration.module.js.map +1 -1
- package/dist/webhook-integration/webhook-integration.service.d.ts +18 -2
- package/dist/webhook-integration/webhook-integration.service.d.ts.map +1 -1
- package/dist/webhook-integration/webhook-integration.service.js +110 -6
- package/dist/webhook-integration/webhook-integration.service.js.map +1 -1
- package/hedhog/data/integration_provider.yaml +9 -0
- package/hedhog/data/integration_type.yaml +6 -0
- package/hedhog/data/menu.yaml +6 -0
- package/hedhog/data/role.yaml +7 -0
- package/hedhog/data/route.yaml +36 -0
- package/hedhog/data/setting_group.yaml +119 -13
- package/hedhog/frontend/app/access-log/page.tsx.ejs +35 -3
- package/hedhog/frontend/app/account/components/active-sessions.tsx.ejs +36 -3
- package/hedhog/frontend/app/account/components/mcp-tokens.tsx.ejs +39 -4
- package/hedhog/frontend/app/account/components/tokens.tsx.ejs +39 -4
- package/hedhog/frontend/app/ai_agent/page.tsx.ejs +35 -2
- package/hedhog/frontend/app/configurations/[slug]/components/byte-size-input.tsx.ejs +160 -0
- package/hedhog/frontend/app/configurations/[slug]/components/duration-input.tsx.ejs +157 -0
- package/hedhog/frontend/app/configurations/[slug]/components/setting-field.tsx.ejs +244 -63
- package/hedhog/frontend/app/dashboard/[slug]/dashboard-content.tsx.ejs +33 -1
- package/hedhog/frontend/app/dashboard/management/tabs/component-roles-tab.tsx.ejs +35 -3
- package/hedhog/frontend/app/dashboard/management/tabs/components-tab.tsx.ejs +36 -4
- package/hedhog/frontend/app/dashboard/management/tabs/dashboard-roles-tab.tsx.ejs +35 -3
- package/hedhog/frontend/app/dashboard/management/tabs/dashboards-tab.tsx.ejs +36 -4
- package/hedhog/frontend/app/dashboard/management/tabs/items-tab.tsx.ejs +35 -3
- package/hedhog/frontend/app/integration/profiles/page.tsx.ejs +141 -29
- package/hedhog/frontend/app/integrations-webhooks/page.tsx.ejs +118 -11
- package/hedhog/frontend/app/mail/log/page.tsx.ejs +37 -4
- package/hedhog/frontend/app/menu/page.tsx.ejs +34 -2
- package/hedhog/frontend/app/preferences/page.tsx.ejs +77 -0
- package/hedhog/frontend/app/roles/menus.tsx.ejs +35 -3
- package/hedhog/frontend/app/roles/page.tsx.ejs +34 -2
- package/hedhog/frontend/app/roles/routes.tsx.ejs +36 -4
- package/hedhog/frontend/app/roles/users.tsx.ejs +36 -4
- package/hedhog/frontend/app/routes/page.tsx.ejs +34 -2
- package/hedhog/frontend/app/setting-list/page.tsx.ejs +34 -2
- package/hedhog/frontend/app/users/page.tsx.ejs +51 -21
- package/hedhog/frontend/messages/en.json +69 -4
- package/hedhog/frontend/messages/pt.json +69 -4
- package/hedhog/table/setting.yaml +2 -0
- package/hedhog/table/webhook_call_log.yaml +49 -46
- package/package.json +8 -3
- package/src/ai/ai.service.ts +9 -6
- package/src/app-command/app-command.module.ts +9 -0
- package/src/{webhook-integration/webhook-command.registry.ts → app-command/app-command.registry.ts} +9 -9
- package/src/auth/auth.controller.ts +20 -4
- package/src/auth/auth.service.ts +10 -36
- package/src/auth/dto/forget.dto.ts +5 -1
- package/src/auth/guards/auth.guard.ts +4 -1
- package/src/challenge/challenge.service.ts +30 -19
- package/src/file/dto/upload.dto.ts +25 -1
- package/src/file/file.controller.ts +31 -18
- package/src/file/file.service.ts +341 -16
- package/src/file/image-optimization.service.ts +111 -46
- package/src/file/provider/s3.provider.ts +16 -7
- package/src/file/provider/spaces.provider.ts +15 -5
- package/src/index.ts +4 -1
- package/src/integration/services/event-subscriber.registry.ts +15 -0
- package/src/integration/services/outbox.processor.ts +2 -5
- package/src/integration/services/outbox.service.ts +35 -17
- package/src/integration-profile/integration-credential-crypto.service.ts +146 -0
- package/src/integration-profile/integration-profile.controller.ts +2 -0
- package/src/integration-profile/integration-profile.module.ts +5 -2
- package/src/integration-profile/integration-profile.secrets.ts +51 -0
- package/src/integration-profile/integration-profile.service.ts +335 -54
- package/src/integration-profile/integration-profile.utils.ts +56 -0
- package/src/language/en.json +5 -1
- package/src/language/pt.json +5 -1
- package/src/mail/mail.controller.ts +5 -29
- package/src/mail/mail.module.ts +5 -5
- package/src/mail/mail.service.ts +37 -13
- package/src/mcp-chat/mcp-chat.service.ts +5 -5
- package/src/oauth/oauth-callback-coordinator.service.spec.ts +159 -0
- package/src/oauth/providers/facebook.provider.ts +3 -3
- package/src/oauth/providers/github.provider.ts +3 -3
- package/src/oauth/providers/google.provider.ts +3 -3
- package/src/oauth/providers/microsoft-entra-id.provider.ts +3 -3
- package/src/oauth/providers/microsoft.provider.ts +3 -3
- package/src/pdf/pdf.service.ts +19 -6
- package/src/profile/profile.controller.ts +21 -14
- package/src/profile/profile.service.ts +23 -20
- package/src/security/security.service.spec.ts +222 -0
- package/src/setting/setting.service.ts +15 -2
- package/src/token/token.service.ts +2 -1
- package/src/user/user.controller.ts +26 -16
- package/src/user/user.service.ts +25 -0
- package/src/utils/webauthn-origin.ts +38 -0
- package/src/webhook-integration/webhook-integration.controller.ts +23 -0
- package/src/webhook-integration/webhook-integration.module.ts +3 -3
- package/src/webhook-integration/webhook-integration.service.ts +115 -5
- package/dist/webhook-integration/webhook-command.registry.d.ts +0 -22
- package/dist/webhook-integration/webhook-command.registry.d.ts.map +0 -1
- package/dist/webhook-integration/webhook-command.registry.js.map +0 -1
- package/src/menu/menu.service.spec.ts +0 -247
- package/src/route/route.service.spec.ts +0 -300
- package/src/screen/screen.service.spec.ts +0 -298
- package/src/user/user.service.spec.ts +0 -294
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
S3,
|
|
8
8
|
S3Client,
|
|
9
9
|
} from '@aws-sdk/client-s3';
|
|
10
|
+
import { Upload } from '@aws-sdk/lib-storage';
|
|
10
11
|
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
|
|
11
12
|
import { BadRequestException } from '@nestjs/common';
|
|
12
13
|
import { createReadStream } from 'fs';
|
|
@@ -157,18 +158,26 @@ export class S3Provider extends AbstractProvider {
|
|
|
157
158
|
filePath: string,
|
|
158
159
|
options?: { filename?: string; mimetype?: string },
|
|
159
160
|
): Promise<any> {
|
|
160
|
-
const
|
|
161
|
+
const client = await this.getS3Client();
|
|
161
162
|
const originalName = options?.filename ?? basename(filePath);
|
|
162
163
|
const storedFilename = this.getFilename(originalName);
|
|
163
164
|
const key = [destination, storedFilename].join('/');
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
165
|
+
|
|
166
|
+
// Multipart streaming upload: nunca buffeia o arquivo em memória e suporta
|
|
167
|
+
// objetos acima do limite de 5GB do PutObject único (ex.: pacotes SCORM grandes).
|
|
168
|
+
const upload = new Upload({
|
|
169
|
+
client,
|
|
170
|
+
params: {
|
|
171
|
+
Bucket: this.setting['storage-s3-bucket'],
|
|
172
|
+
Key: key,
|
|
173
|
+
Body: createReadStream(filePath),
|
|
174
|
+
ContentType: options?.mimetype,
|
|
175
|
+
},
|
|
176
|
+
queueSize: 4,
|
|
177
|
+
partSize: 16 * 1024 * 1024,
|
|
169
178
|
});
|
|
170
179
|
|
|
171
|
-
await
|
|
180
|
+
await upload.done();
|
|
172
181
|
return storedFilename;
|
|
173
182
|
}
|
|
174
183
|
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
S3,
|
|
8
8
|
S3Client,
|
|
9
9
|
} from '@aws-sdk/client-s3';
|
|
10
|
+
import { Upload } from '@aws-sdk/lib-storage';
|
|
10
11
|
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
|
|
11
12
|
import { BadRequestException } from '@nestjs/common';
|
|
12
13
|
import { createReadStream } from 'fs';
|
|
@@ -181,19 +182,28 @@ export class SpacesProvider extends AbstractProvider {
|
|
|
181
182
|
filePath: string,
|
|
182
183
|
options?: { filename?: string; mimetype?: string },
|
|
183
184
|
): Promise<any> {
|
|
184
|
-
const
|
|
185
|
+
const client = await this.getS3Client();
|
|
185
186
|
const originalName = options?.filename ?? basename(filePath);
|
|
186
187
|
const storedFilename = this.getFilename(originalName);
|
|
187
188
|
const key = [destination, storedFilename].join('/');
|
|
188
|
-
|
|
189
|
-
|
|
189
|
+
|
|
190
|
+
// Upload por streaming do disco: o lib-storage envia um PutObject único abaixo
|
|
191
|
+
// do partSize e só ativa multipart acima dele, suportando objetos acima do
|
|
192
|
+
// limite de 5GB do PutObject (ex.: pacotes SCORM grandes) sem buffeiar na RAM.
|
|
193
|
+
const upload = new Upload({
|
|
194
|
+
client,
|
|
195
|
+
params: {
|
|
190
196
|
Bucket: this.setting['storage-spaces-bucket'],
|
|
191
197
|
Key: key,
|
|
192
198
|
Body: createReadStream(filePath),
|
|
193
199
|
ContentType: options?.mimetype,
|
|
194
200
|
ACL: 'public-read',
|
|
195
|
-
}
|
|
196
|
-
|
|
201
|
+
},
|
|
202
|
+
queueSize: 4,
|
|
203
|
+
partSize: 16 * 1024 * 1024,
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
await upload.done();
|
|
197
207
|
return storedFilename;
|
|
198
208
|
}
|
|
199
209
|
|
package/src/index.ts
CHANGED
|
@@ -25,6 +25,8 @@ export * from './mail/mail.service';
|
|
|
25
25
|
export * from './integration-profile/integration-profile.module';
|
|
26
26
|
export * from './integration-profile/integration-profile.service';
|
|
27
27
|
export * from './integration-profile/integration-profile.utils';
|
|
28
|
+
export * from './integration-profile/integration-profile.secrets';
|
|
29
|
+
export * from './integration-profile/integration-credential-crypto.service';
|
|
28
30
|
|
|
29
31
|
export * from './file/file.module';
|
|
30
32
|
export * from './file/file.service';
|
|
@@ -37,9 +39,10 @@ export * from './setting/setting.service';
|
|
|
37
39
|
export * from './ai/ai.module';
|
|
38
40
|
export * from './ai/ai.service';
|
|
39
41
|
|
|
42
|
+
export * from './app-command/app-command.module';
|
|
43
|
+
export * from './app-command/app-command.registry';
|
|
40
44
|
export * from './webhook-integration/event-webhook.service';
|
|
41
45
|
export * from './webhook-integration/integration-event-catalog.service';
|
|
42
|
-
export * from './webhook-integration/webhook-command.registry';
|
|
43
46
|
export * from './webhook-integration/webhook-integration.module';
|
|
44
47
|
export * from './webhook-integration/webhook-integration.service';
|
|
45
48
|
|
|
@@ -80,6 +80,21 @@ export class EventSubscriberRegistry {
|
|
|
80
80
|
return count;
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
/**
|
|
84
|
+
* Remove all handlers whose consumerName starts with the given prefix.
|
|
85
|
+
* Used to clean up subscriptions for a specific agent before re-registering.
|
|
86
|
+
*/
|
|
87
|
+
removeByConsumerPrefix(prefix: string): void {
|
|
88
|
+
for (const [eventName, handlers] of this.subscribers.entries()) {
|
|
89
|
+
const filtered = handlers.filter((h) => !h.consumerName.startsWith(prefix));
|
|
90
|
+
if (filtered.length === 0) {
|
|
91
|
+
this.subscribers.delete(eventName);
|
|
92
|
+
} else {
|
|
93
|
+
this.subscribers.set(eventName, filtered);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
83
98
|
/**
|
|
84
99
|
* Clear all handlers (useful for testing)
|
|
85
100
|
*/
|
|
@@ -27,8 +27,8 @@ export class OutboxProcessor {
|
|
|
27
27
|
const batchSize = options?.batchSizeOverride ?? settings.batchSize;
|
|
28
28
|
const leaseMs = settings.processingLeaseMs;
|
|
29
29
|
|
|
30
|
-
//
|
|
31
|
-
const events = await this.outboxService.
|
|
30
|
+
// Atomically claim pending events — prevents duplicate processing across replicas
|
|
31
|
+
const events = await this.outboxService.findPendingAndLock(batchSize, leaseMs);
|
|
32
32
|
|
|
33
33
|
if (events.length === 0) {
|
|
34
34
|
return 0;
|
|
@@ -42,9 +42,6 @@ export class OutboxProcessor {
|
|
|
42
42
|
|
|
43
43
|
for (const event of events) {
|
|
44
44
|
try {
|
|
45
|
-
// Mark as processing with lease
|
|
46
|
-
await this.outboxService.markProcessing(event.id, leaseMs);
|
|
47
|
-
|
|
48
45
|
// Get handlers for this event
|
|
49
46
|
const handlers = this.registry.getHandlers(event.eventName);
|
|
50
47
|
|
|
@@ -102,30 +102,48 @@ export class OutboxService {
|
|
|
102
102
|
}
|
|
103
103
|
|
|
104
104
|
/**
|
|
105
|
-
*
|
|
105
|
+
* Atomically claim a batch of pending events using SELECT FOR UPDATE SKIP LOCKED.
|
|
106
|
+
* Prevents duplicate processing across multiple API replicas: each replica gets
|
|
107
|
+
* its own non-overlapping set of rows with no blocking between them.
|
|
106
108
|
*/
|
|
107
|
-
async
|
|
109
|
+
async findPendingAndLock(
|
|
108
110
|
limit: number,
|
|
109
111
|
leaseMs: number,
|
|
110
112
|
): Promise<OutboxEvent[]> {
|
|
111
113
|
const now = new Date();
|
|
112
114
|
const leaseThreshold = new Date(now.getTime() - leaseMs);
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
115
|
+
const leaseUntil = new Date(now.getTime() + leaseMs);
|
|
116
|
+
|
|
117
|
+
return this.prisma.$transaction(async (tx) => {
|
|
118
|
+
const rows = await tx.$queryRaw<OutboxEventRecord[]>`
|
|
119
|
+
SELECT * FROM outbox_event
|
|
120
|
+
WHERE status = 'pending'::"outbox_event_status_e5d55ba10d_enum"
|
|
121
|
+
OR (status = 'processing'::"outbox_event_status_e5d55ba10d_enum"
|
|
122
|
+
AND available_at <= ${leaseThreshold})
|
|
123
|
+
ORDER BY created_at ASC
|
|
124
|
+
LIMIT ${limit}
|
|
125
|
+
FOR UPDATE SKIP LOCKED
|
|
126
|
+
`;
|
|
127
|
+
|
|
128
|
+
if (rows.length === 0) return [];
|
|
129
|
+
|
|
130
|
+
const ids = rows.map((r) => r.id);
|
|
131
|
+
await tx.$executeRaw`
|
|
132
|
+
UPDATE outbox_event
|
|
133
|
+
SET status = 'processing'::"outbox_event_status_e5d55ba10d_enum",
|
|
134
|
+
available_at = ${leaseUntil},
|
|
135
|
+
updated_at = now()
|
|
136
|
+
WHERE id = ANY(${ids}::int[])
|
|
137
|
+
`;
|
|
138
|
+
|
|
139
|
+
return rows.map((r) =>
|
|
140
|
+
this.toDomainEvent({
|
|
141
|
+
...r,
|
|
142
|
+
status: OutboxEventStatus.PROCESSING,
|
|
143
|
+
available_at: leaseUntil,
|
|
144
|
+
}),
|
|
145
|
+
);
|
|
126
146
|
});
|
|
127
|
-
|
|
128
|
-
return records.map((record) => this.toDomainEvent(record as OutboxEventRecord));
|
|
129
147
|
}
|
|
130
148
|
|
|
131
149
|
/**
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { registerIntegrationConfigTransformer } from '@hed-hog/api-prisma';
|
|
2
|
+
import {
|
|
3
|
+
Inject,
|
|
4
|
+
Injectable,
|
|
5
|
+
Logger,
|
|
6
|
+
OnModuleInit,
|
|
7
|
+
forwardRef,
|
|
8
|
+
} from '@nestjs/common';
|
|
9
|
+
import { SecurityService } from '../security/security.service';
|
|
10
|
+
import { getSensitiveConfigKeys } from './integration-profile.secrets';
|
|
11
|
+
|
|
12
|
+
/** Prefixo de envelope dos segredos cifrados em repouso. */
|
|
13
|
+
export const ENC_ENVELOPE_PREFIX = 'enc.v1.';
|
|
14
|
+
|
|
15
|
+
/** Placeholder devolvido à UI no lugar de um segredo armazenado. */
|
|
16
|
+
export const SECRET_MASK = '********';
|
|
17
|
+
|
|
18
|
+
type Config = Record<string, unknown> | null | undefined;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Cifra/decifra os segredos dos Perfis de Integração em repouso.
|
|
22
|
+
*
|
|
23
|
+
* - Escrita: `encryptConfig` cifra (envelope `enc.v1.`) as chaves sensíveis do provider.
|
|
24
|
+
* - Leitura: um middleware Prisma `$use` (registrado em onModuleInit) decifra
|
|
25
|
+
* `integration_profile.config` de forma transparente em toda leitura/resultado, então os
|
|
26
|
+
* ~25 consumidores que leem o perfil direto via Prisma não mudam.
|
|
27
|
+
* - UI: `maskConfig` esconde segredos; `mergeKeepingStoredSecrets` implementa
|
|
28
|
+
* "deixe em branco para manter".
|
|
29
|
+
*
|
|
30
|
+
* Valores legados sem o prefixo passam intactos (sem janela de quebra antes do backfill).
|
|
31
|
+
*/
|
|
32
|
+
@Injectable()
|
|
33
|
+
export class IntegrationCredentialCryptoService implements OnModuleInit {
|
|
34
|
+
private readonly logger = new Logger(IntegrationCredentialCryptoService.name);
|
|
35
|
+
|
|
36
|
+
constructor(
|
|
37
|
+
@Inject(forwardRef(() => SecurityService))
|
|
38
|
+
private readonly securityService: SecurityService,
|
|
39
|
+
) {}
|
|
40
|
+
|
|
41
|
+
onModuleInit() {
|
|
42
|
+
// Registra a decifra transparente de integration_profile.config (query extension
|
|
43
|
+
// no PrismaService). A partir daqui, toda leitura do perfil devolve config decifrado.
|
|
44
|
+
registerIntegrationConfigTransformer((config) => this.decryptConfig(config));
|
|
45
|
+
this.logger.log(
|
|
46
|
+
'Integration credential decryption registered (integration_profile.config).',
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
private isEnveloped(value: unknown): value is string {
|
|
51
|
+
return typeof value === 'string' && value.startsWith(ENC_ENVELOPE_PREFIX);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Cifra um valor único com envelope (idempotente). */
|
|
55
|
+
encryptValue(value: string): string {
|
|
56
|
+
if (this.isEnveloped(value)) return value;
|
|
57
|
+
return ENC_ENVELOPE_PREFIX + this.securityService.encrypt(value);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Decifra um valor com envelope; valores sem envelope passam intactos. */
|
|
61
|
+
decryptValue(value: unknown): unknown {
|
|
62
|
+
if (!this.isEnveloped(value)) return value;
|
|
63
|
+
try {
|
|
64
|
+
return this.securityService.decrypt(value.slice(ENC_ENVELOPE_PREFIX.length));
|
|
65
|
+
} catch (err) {
|
|
66
|
+
// Não derruba a query: loga e devolve como veio (ex.: ENCRYPTION_SECRET trocado).
|
|
67
|
+
this.logger.error(
|
|
68
|
+
`Failed to decrypt integration secret: ${err instanceof Error ? err.message : String(err)}`,
|
|
69
|
+
);
|
|
70
|
+
return value;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Cifra as chaves sensíveis do provider no config (idempotente, ignora vazios). */
|
|
75
|
+
encryptConfig(providerSlug: string, config: Config): Config {
|
|
76
|
+
if (!config) return config;
|
|
77
|
+
const keys = getSensitiveConfigKeys(providerSlug);
|
|
78
|
+
if (keys.length === 0) return config;
|
|
79
|
+
const out: Record<string, unknown> = { ...config };
|
|
80
|
+
for (const key of keys) {
|
|
81
|
+
const value = out[key];
|
|
82
|
+
if (typeof value === 'string' && value.length > 0) {
|
|
83
|
+
out[key] = this.encryptValue(value);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return out;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Decifra qualquer valor com envelope no config (não precisa do provider slug). */
|
|
90
|
+
decryptConfig(config: Config): Config {
|
|
91
|
+
if (!config) return config;
|
|
92
|
+
let changed = false;
|
|
93
|
+
const out: Record<string, unknown> = { ...config };
|
|
94
|
+
for (const [key, value] of Object.entries(out)) {
|
|
95
|
+
if (this.isEnveloped(value)) {
|
|
96
|
+
out[key] = this.decryptValue(value);
|
|
97
|
+
changed = true;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return changed ? out : config;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Mascara os segredos do provider para resposta à UI (não envia o valor real). */
|
|
104
|
+
maskConfig(
|
|
105
|
+
providerSlug: string,
|
|
106
|
+
config: Config,
|
|
107
|
+
placeholder = SECRET_MASK,
|
|
108
|
+
): Config {
|
|
109
|
+
if (!config) return config;
|
|
110
|
+
const keys = getSensitiveConfigKeys(providerSlug);
|
|
111
|
+
if (keys.length === 0) return config;
|
|
112
|
+
const out: Record<string, unknown> = { ...config };
|
|
113
|
+
for (const key of keys) {
|
|
114
|
+
const value = out[key];
|
|
115
|
+
if (typeof value === 'string' && value.length > 0) {
|
|
116
|
+
out[key] = placeholder;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return out;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* "Deixe em branco para manter": para cada chave sensível, se o valor recebido está
|
|
124
|
+
* vazio ou é o placeholder de máscara, herda o valor já armazenado (decifrado).
|
|
125
|
+
*/
|
|
126
|
+
mergeKeepingStoredSecrets(
|
|
127
|
+
providerSlug: string,
|
|
128
|
+
incoming: Config,
|
|
129
|
+
storedDecrypted: Config,
|
|
130
|
+
): Config {
|
|
131
|
+
const merged: Record<string, unknown> = { ...(incoming ?? {}) };
|
|
132
|
+
const stored = storedDecrypted ?? {};
|
|
133
|
+
for (const key of getSensitiveConfigKeys(providerSlug)) {
|
|
134
|
+
const value = merged[key];
|
|
135
|
+
const isBlank =
|
|
136
|
+
value === undefined ||
|
|
137
|
+
value === null ||
|
|
138
|
+
value === '' ||
|
|
139
|
+
value === SECRET_MASK;
|
|
140
|
+
if (isBlank && typeof stored[key] === 'string') {
|
|
141
|
+
merged[key] = stored[key];
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return merged;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
@@ -55,12 +55,14 @@ export class IntegrationProfileController {
|
|
|
55
55
|
@Query('typeId') typeId?: string,
|
|
56
56
|
@Query('typeSlug') typeSlug?: string,
|
|
57
57
|
@Query('providerSlug') providerSlug?: string,
|
|
58
|
+
@Query('slug') slug?: string,
|
|
58
59
|
) {
|
|
59
60
|
return this.integrationProfileService.list(
|
|
60
61
|
paginationParams,
|
|
61
62
|
typeId ? parseInt(typeId, 10) : undefined,
|
|
62
63
|
typeSlug,
|
|
63
64
|
providerSlug,
|
|
65
|
+
slug,
|
|
64
66
|
);
|
|
65
67
|
}
|
|
66
68
|
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { PaginationModule } from '@hed-hog/api-pagination';
|
|
2
2
|
import { PrismaModule } from '@hed-hog/api-prisma';
|
|
3
3
|
import { forwardRef, Module } from '@nestjs/common';
|
|
4
|
+
import { SecurityModule } from '../security/security.module';
|
|
5
|
+
import { IntegrationCredentialCryptoService } from './integration-credential-crypto.service';
|
|
4
6
|
import { IntegrationProfileController } from './integration-profile.controller';
|
|
5
7
|
import { IntegrationProfileService } from './integration-profile.service';
|
|
6
8
|
|
|
@@ -8,9 +10,10 @@ import { IntegrationProfileService } from './integration-profile.service';
|
|
|
8
10
|
imports: [
|
|
9
11
|
forwardRef(() => PrismaModule),
|
|
10
12
|
forwardRef(() => PaginationModule),
|
|
13
|
+
forwardRef(() => SecurityModule),
|
|
11
14
|
],
|
|
12
15
|
controllers: [IntegrationProfileController],
|
|
13
|
-
providers: [IntegrationProfileService],
|
|
14
|
-
exports: [IntegrationProfileService],
|
|
16
|
+
providers: [IntegrationProfileService, IntegrationCredentialCryptoService],
|
|
17
|
+
exports: [IntegrationProfileService, IntegrationCredentialCryptoService],
|
|
15
18
|
})
|
|
16
19
|
export class IntegrationProfileModule {}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mapa AUTORITATIVO das chaves sensíveis (segredos) por provider de integração.
|
|
3
|
+
*
|
|
4
|
+
* SEGURANÇA-CRÍTICO: este mapa é a fonte única de verdade do que é cifrado em repouso
|
|
5
|
+
* (integration-credential-crypto.service) e mascarado na exportação/UI. Qualquer chave
|
|
6
|
+
* esquecida aqui NÃO será cifrada. Deve ser mantido em sincronia com os campos
|
|
7
|
+
* `type: 'password'` do form do admin (PROVIDER_FIELDS em
|
|
8
|
+
* apps/admin/src/app/(app)/(libraries)/core/integration/profiles/page.tsx).
|
|
9
|
+
*
|
|
10
|
+
* Não usar heurística por nome de chave: campos públicos como `site_key`, `public_key` e
|
|
11
|
+
* `access_key_id` contêm "key" mas NÃO são segredos.
|
|
12
|
+
*/
|
|
13
|
+
export const INTEGRATION_SECRET_KEYS: Record<string, string[]> = {
|
|
14
|
+
// Email
|
|
15
|
+
smtp: ['password'],
|
|
16
|
+
gmail: ['client_secret', 'refresh_token'],
|
|
17
|
+
ses: ['secret_access_key'],
|
|
18
|
+
// Mensageria
|
|
19
|
+
'evolution-api': ['token'],
|
|
20
|
+
'whatsapp-official': ['access_token'],
|
|
21
|
+
// Storage
|
|
22
|
+
s3: ['secret_access_key'],
|
|
23
|
+
gcs: ['key_file_json'],
|
|
24
|
+
'azure-blob': ['key'],
|
|
25
|
+
's3-compatible': ['secret_access_key'],
|
|
26
|
+
// IA
|
|
27
|
+
openai: ['api_key'],
|
|
28
|
+
gemini: ['api_key'],
|
|
29
|
+
claude: ['api_key'],
|
|
30
|
+
// OAuth
|
|
31
|
+
'google-oauth': ['client_secret'],
|
|
32
|
+
'github-oauth': ['client_secret'],
|
|
33
|
+
'microsoft-oauth': ['client_secret'],
|
|
34
|
+
'facebook-oauth': ['client_secret'],
|
|
35
|
+
'microsoft-entra-id-oauth': ['client_secret'],
|
|
36
|
+
// Pagamento
|
|
37
|
+
stripe: ['secret_key', 'webhook_secret'],
|
|
38
|
+
mercado_pago: ['access_token', 'webhook_secret'],
|
|
39
|
+
// Captcha
|
|
40
|
+
recaptcha: ['secret_key'],
|
|
41
|
+
'cloudflare-turnstile': ['secret_key'],
|
|
42
|
+
altcha: ['hmac_key'],
|
|
43
|
+
// Infraestrutura (autoscaling)
|
|
44
|
+
digitalocean: ['api_token'],
|
|
45
|
+
kubernetes: ['token', 'ca_cert'],
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/** Chaves sensíveis de um provider (vazio se desconhecido). */
|
|
49
|
+
export function getSensitiveConfigKeys(providerSlug: string): string[] {
|
|
50
|
+
return INTEGRATION_SECRET_KEYS[providerSlug.toLowerCase()] ?? [];
|
|
51
|
+
}
|