@hed-hog/core 0.0.360 → 0.0.362

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 (153) hide show
  1. package/dist/access-log/access-log.controller.d.ts +1 -1
  2. package/dist/access-log/access-log.service.d.ts +1 -1
  3. package/dist/access-log/access-log.service.d.ts.map +1 -1
  4. package/dist/access-log/access-log.service.js +43 -1
  5. package/dist/access-log/access-log.service.js.map +1 -1
  6. package/dist/ai/ai.service.d.ts +1 -1
  7. package/dist/auth/auth.controller.d.ts +27 -26
  8. package/dist/auth/auth.controller.d.ts.map +1 -1
  9. package/dist/auth/auth.controller.js +6 -2
  10. package/dist/auth/auth.controller.js.map +1 -1
  11. package/dist/auth/auth.service.d.ts +35 -34
  12. package/dist/auth/auth.service.d.ts.map +1 -1
  13. package/dist/auth/auth.service.js +15 -5
  14. package/dist/auth/auth.service.js.map +1 -1
  15. package/dist/auth/guards/auth.guard.d.ts +1 -0
  16. package/dist/auth/guards/auth.guard.d.ts.map +1 -1
  17. package/dist/auth/guards/auth.guard.js +11 -4
  18. package/dist/auth/guards/auth.guard.js.map +1 -1
  19. package/dist/challenge/challenge.service.d.ts +12 -12
  20. package/dist/core/system.controller.d.ts +2 -2
  21. package/dist/core/system.service.d.ts +2 -2
  22. package/dist/dashboard/dashboard/dashboard.controller.d.ts +66 -66
  23. package/dist/dashboard/dashboard/dashboard.service.d.ts +66 -66
  24. package/dist/dashboard/dashboard-component/dashboard-component.controller.d.ts +26 -26
  25. package/dist/dashboard/dashboard-component/dashboard-component.service.d.ts +26 -26
  26. package/dist/dashboard/dashboard-component-role/dashboard-component-role.controller.d.ts +34 -34
  27. package/dist/dashboard/dashboard-component-role/dashboard-component-role.service.d.ts +34 -34
  28. package/dist/dashboard/dashboard-core/dashboard-core.controller.d.ts +8 -8
  29. package/dist/dashboard/dashboard-core/dashboard-core.service.d.ts +8 -8
  30. package/dist/dashboard/dashboard-item/dashboard-item.controller.d.ts +5 -5
  31. package/dist/dashboard/dashboard-item/dashboard-item.service.d.ts +5 -5
  32. package/dist/dashboard/dashboard-role/dashboard-role.controller.d.ts +25 -25
  33. package/dist/dashboard/dashboard-role/dashboard-role.service.d.ts +25 -25
  34. package/dist/file/file.controller.d.ts +6 -6
  35. package/dist/file/file.module.d.ts.map +1 -1
  36. package/dist/file/file.module.js +3 -2
  37. package/dist/file/file.module.js.map +1 -1
  38. package/dist/file/file.service.d.ts +121 -25
  39. package/dist/file/file.service.d.ts.map +1 -1
  40. package/dist/file/file.service.js +217 -32
  41. package/dist/file/file.service.js.map +1 -1
  42. package/dist/file/image-optimization.service.d.ts +20 -0
  43. package/dist/file/image-optimization.service.d.ts.map +1 -0
  44. package/dist/file/image-optimization.service.js +126 -0
  45. package/dist/file/image-optimization.service.js.map +1 -0
  46. package/dist/file/provider/local.provider.d.ts +1 -1
  47. package/dist/file/provider/local.provider.d.ts.map +1 -1
  48. package/dist/file/provider/local.provider.js +2 -9
  49. package/dist/file/provider/local.provider.js.map +1 -1
  50. package/dist/index.d.ts +1 -0
  51. package/dist/index.d.ts.map +1 -1
  52. package/dist/index.js +1 -0
  53. package/dist/index.js.map +1 -1
  54. package/dist/integration-profile/integration-profile.controller.d.ts +15 -15
  55. package/dist/integration-profile/integration-profile.service.d.ts +15 -15
  56. package/dist/locale/locale.controller.d.ts +4 -4
  57. package/dist/locale/locale.service.d.ts +4 -4
  58. package/dist/mail/mail.controller.d.ts +7 -7
  59. package/dist/mail/mail.service.d.ts +7 -7
  60. package/dist/mail-sent/mail-sent.controller.d.ts +12 -12
  61. package/dist/mail-sent/mail-sent.service.d.ts +12 -12
  62. package/dist/mcp/mcp-api-key.controller.d.ts +2 -2
  63. package/dist/mcp/mcp-api-key.service.d.ts +2 -2
  64. package/dist/mcp/tools/core-users.mcp-tools.d.ts +1 -1
  65. package/dist/mcp/tools/core-users.mcp-tools.d.ts.map +1 -1
  66. package/dist/mcp/tools/core-users.mcp-tools.js +2 -2
  67. package/dist/mcp/tools/core-users.mcp-tools.js.map +1 -1
  68. package/dist/mcp-chat/mcp-chat.controller.d.ts +8 -8
  69. package/dist/mcp-chat/mcp-chat.service.d.ts +8 -8
  70. package/dist/menu/menu.controller.d.ts +10 -10
  71. package/dist/menu/menu.service.d.ts +10 -10
  72. package/dist/notification/notification.controller.d.ts +36 -36
  73. package/dist/notification/notification.service.d.ts +36 -36
  74. package/dist/profile/profile.controller.d.ts +16 -16
  75. package/dist/profile/profile.service.d.ts +16 -16
  76. package/dist/profile/profile.service.d.ts.map +1 -1
  77. package/dist/profile/profile.service.js +44 -23
  78. package/dist/profile/profile.service.js.map +1 -1
  79. package/dist/role/role.controller.d.ts +6 -6
  80. package/dist/role/role.service.d.ts +6 -6
  81. package/dist/screen/screen.controller.d.ts +3 -3
  82. package/dist/screen/screen.service.d.ts +3 -3
  83. package/dist/session/session.controller.d.ts +2 -2
  84. package/dist/session/session.service.d.ts +6 -6
  85. package/dist/session/session.service.d.ts.map +1 -1
  86. package/dist/session/session.service.js +7 -2
  87. package/dist/session/session.service.js.map +1 -1
  88. package/dist/setting/setting.controller.d.ts +18 -18
  89. package/dist/setting/setting.service.d.ts +18 -18
  90. package/dist/setting/setting.service.d.ts.map +1 -1
  91. package/dist/setting/setting.service.js +0 -1
  92. package/dist/setting/setting.service.js.map +1 -1
  93. package/dist/token/token.service.d.ts +1 -0
  94. package/dist/token/token.service.d.ts.map +1 -1
  95. package/dist/token/token.service.js +23 -6
  96. package/dist/token/token.service.js.map +1 -1
  97. package/dist/user/user.controller.d.ts +30 -30
  98. package/dist/user/user.controller.d.ts.map +1 -1
  99. package/dist/user/user.controller.js +4 -3
  100. package/dist/user/user.controller.js.map +1 -1
  101. package/dist/user/user.service.d.ts +68 -68
  102. package/dist/user/user.service.d.ts.map +1 -1
  103. package/dist/user/user.service.js +6 -6
  104. package/dist/user/user.service.js.map +1 -1
  105. package/dist/webhook-integration/webhook-integration.controller.d.ts +2 -2
  106. package/dist/webhook-integration/webhook-integration.service.d.ts +2 -2
  107. package/hedhog/data/route.yaml +2 -0
  108. package/hedhog/data/setting_group.yaml +117 -26
  109. package/hedhog/data/setting_subgroup.yaml +8 -0
  110. package/hedhog/frontend/app/access-log/page.tsx.ejs +170 -11
  111. package/hedhog/frontend/app/account/components/remove-mfa-dialog.tsx.ejs +1 -1
  112. package/hedhog/frontend/app/account/components/verify-before-add-dialog.tsx.ejs +1 -1
  113. package/hedhog/frontend/app/configurations/[slug]/components/setting-field.tsx.ejs +113 -38
  114. package/hedhog/frontend/app/users/page.tsx.ejs +7 -7
  115. package/hedhog/frontend/app/users/permissions.tsx.ejs +137 -83
  116. package/hedhog/frontend/app/users/user-access-sheet.tsx.ejs +28 -26
  117. package/hedhog/frontend/app/users/user-sheet.tsx.ejs +207 -75
  118. package/hedhog/frontend/messages/en.json +9 -2
  119. package/hedhog/frontend/messages/pt.json +9 -2
  120. package/hedhog/table/setting_runtime_state.yaml +9 -0
  121. package/package.json +12 -6
  122. package/src/access-log/access-log.service.ts +19 -1
  123. package/src/auth/auth.controller.ts +5 -0
  124. package/src/auth/auth.service.ts +14 -4
  125. package/src/auth/guards/auth.guard.ts +10 -3
  126. package/src/file/file.module.ts +3 -2
  127. package/src/file/file.service.ts +448 -46
  128. package/src/file/image-optimization.service.ts +123 -0
  129. package/src/file/provider/local.provider.ts +2 -9
  130. package/src/index.ts +1 -0
  131. package/src/mcp/tools/core-users.mcp-tools.ts +267 -267
  132. package/src/profile/profile.service.ts +40 -22
  133. package/src/session/session.service.ts +10 -2
  134. package/src/setting/setting.service.ts +0 -1
  135. package/src/token/token.service.ts +26 -6
  136. package/src/user/user.controller.ts +5 -2
  137. package/src/user/user.service.ts +11 -8
  138. package/dist/menu/menu.service.spec.d.ts +0 -1
  139. package/dist/menu/menu.service.spec.d.ts.map +0 -1
  140. package/dist/menu/menu.service.spec.js +0 -208
  141. package/dist/menu/menu.service.spec.js.map +0 -1
  142. package/dist/route/route.service.spec.d.ts +0 -1
  143. package/dist/route/route.service.spec.d.ts.map +0 -1
  144. package/dist/route/route.service.spec.js +0 -262
  145. package/dist/route/route.service.spec.js.map +0 -1
  146. package/dist/screen/screen.service.spec.d.ts +0 -1
  147. package/dist/screen/screen.service.spec.d.ts.map +0 -1
  148. package/dist/screen/screen.service.spec.js +0 -259
  149. package/dist/screen/screen.service.spec.js.map +0 -1
  150. package/dist/user/user.service.spec.d.ts +0 -1
  151. package/dist/user/user.service.spec.d.ts.map +0 -1
  152. package/dist/user/user.service.spec.js +0 -261
  153. package/dist/user/user.service.spec.js.map +0 -1
@@ -6,6 +6,7 @@ import {
6
6
  forwardRef,
7
7
  Inject,
8
8
  Injectable,
9
+ Logger,
9
10
  NotFoundException,
10
11
  UnauthorizedException,
11
12
  } from '@nestjs/common';
@@ -21,6 +22,7 @@ import { LoginDTO } from './dto/login.dto';
21
22
 
22
23
  @Injectable()
23
24
  export class AuthService {
25
+ private readonly logger = new Logger(AuthService.name);
24
26
 
25
27
  constructor(
26
28
  @Inject(forwardRef(() => PrismaService))
@@ -46,7 +48,8 @@ export class AuthService {
46
48
  const payload = { sub: userId, sessionId: session.id };
47
49
  const accessToken = await this.token.createAccessToken(payload);
48
50
  await this.prisma.user.update({ where: { id: userId }, data: { last_login_at: new Date() } });
49
-
51
+ this.logger.debug(`[AuthService] Session created userId=${userId} sessionId=${session.id}`);
52
+
50
53
  return {
51
54
  accessToken,
52
55
  refreshToken,
@@ -474,9 +477,12 @@ export class AuthService {
474
477
  async refreshAccessToken(locale: string, refreshToken: string, ipAddress: string, userAgent: string) {
475
478
 
476
479
  if (!refreshToken) {
480
+ this.logger.warn(`[AuthService] refreshAccessToken called without token ip=${ipAddress}`);
477
481
  throw new UnauthorizedException(getLocaleText('accessDenied', locale, 'Access denied.'));
478
482
  }
479
483
 
484
+ this.logger.debug(`[AuthService] refreshAccessToken ip=${ipAddress} ua=${userAgent?.slice(0, 40)}`);
485
+
480
486
  const { session, token } = await this.session.refresh(
481
487
  locale,
482
488
  refreshToken,
@@ -488,6 +494,8 @@ export class AuthService {
488
494
  const payload = { sub: user.id, sessionId: session.id };
489
495
  const accessToken = await this.token.createAccessToken(payload);
490
496
 
497
+ this.logger.debug(`[AuthService] refreshAccessToken success userId=${user.id} newSessionId=${session.id}`);
498
+
491
499
  return { accessToken, refreshToken: token, session };
492
500
 
493
501
  }
@@ -780,7 +788,8 @@ export class AuthService {
780
788
  throw new BadRequestException(getLocaleText('webauthnNotConfigured', locale, 'WebAuthn not configured.'));
781
789
  }
782
790
 
783
- const rpID = new URL(process.env.FRONTEND_URL || 'http://localhost:3200').hostname;
791
+ const primaryOrigin = (process.env.CORS_ALLOWED_ORIGINS ?? '').split(/[\n,;]+/).map(u => u.trim()).filter(Boolean)[0] ?? 'http://localhost:3200';
792
+ const rpID = new URL(primaryOrigin).hostname;
784
793
 
785
794
  const allowCredentials = user.user_mfa
786
795
  .filter(mfa => mfa.user_mfa_webauthn && mfa.user_mfa_webauthn.length > 0)
@@ -834,8 +843,9 @@ export class AuthService {
834
843
  }
835
844
 
836
845
  const authenticator = challengeRecord.user_mfa.user_mfa_webauthn[0];
837
- const rpID = new URL(process.env.FRONTEND_URL || 'http://localhost:3200').hostname;
838
- const expectedOrigin = process.env.FRONTEND_URL || 'http://localhost:3200';
846
+ const primaryOrigin = (process.env.CORS_ALLOWED_ORIGINS ?? '').split(/[\n,;]+/).map(u => u.trim()).filter(Boolean)[0] ?? 'http://localhost:3200';
847
+ const rpID = new URL(primaryOrigin).hostname;
848
+ const expectedOrigin = primaryOrigin;
839
849
 
840
850
  const verification = await verifyAuthenticationResponse({
841
851
  response: assertionResponse,
@@ -5,6 +5,7 @@ import {
5
5
  CanActivate,
6
6
  ExecutionContext,
7
7
  Injectable,
8
+ Logger,
8
9
  UnauthorizedException,
9
10
  } from '@nestjs/common';
10
11
  import { Reflector } from '@nestjs/core';
@@ -14,6 +15,8 @@ import { TokenService } from '../../token/token.service';
14
15
 
15
16
  @Injectable()
16
17
  export class AuthGuard implements CanActivate {
18
+ private readonly logger = new Logger(AuthGuard.name);
19
+
17
20
  constructor(
18
21
  private token: TokenService,
19
22
  private reflector: Reflector,
@@ -50,23 +53,26 @@ export class AuthGuard implements CanActivate {
50
53
  if (isPublic) {
51
54
  return true;
52
55
  } else if (withNoRole) {
56
+ this.logger.debug(`[AuthGuard] No token: ${request.method} ${request.url}`);
53
57
  // @NoRole() requires authentication but no specific role — token is mandatory
54
58
  throw new UnauthorizedException(
55
59
  getLocaleText('accessDenied', locale, 'Access denied.'),
56
60
  );
57
61
  } else if (withRole) {
62
+ this.logger.debug(`[AuthGuard] No token: ${request.method} ${request.url}`);
58
63
  // @Role() decorator requires authentication
59
64
  throw new UnauthorizedException(
60
65
  getLocaleText('accessDenied', locale, 'Access denied.'),
61
66
  );
62
67
  } else {
68
+ this.logger.debug(`[AuthGuard] No token: ${request.method} ${request.url}`);
63
69
  // No explicit decorator, default behavior (deny)
64
70
  throw new UnauthorizedException(
65
71
  getLocaleText('accessDenied', locale, 'Access denied.'),
66
72
  );
67
73
  }
68
74
  }
69
-
75
+
70
76
  try {
71
77
  const payload = await this.token.verify(locale, token);
72
78
 
@@ -75,8 +81,9 @@ export class AuthGuard implements CanActivate {
75
81
  if (isPublic) {
76
82
  return true;
77
83
  } else {
78
- // Return proper 401 status with clear message for JWT errors
79
- const message = (error as any)?.message || (error as any)?.name || 'Unauthorized';
84
+ const errName = (error as any)?.name ?? 'UnknownError';
85
+ const message = (error as any)?.message || errName || 'Unauthorized';
86
+ this.logger.warn(`[AuthGuard] Token rejected ${request.method} ${request.url} error=${errName}: ${message}`);
80
87
  throw new UnauthorizedException(message);
81
88
  }
82
89
  }
@@ -5,6 +5,7 @@ import { JwtModule } from '@nestjs/jwt';
5
5
  import { SettingModule } from '../setting/setting.module';
6
6
  import { FileController } from './file.controller';
7
7
  import { FileService } from './file.service';
8
+ import { ImageOptimizationService } from './image-optimization.service';
8
9
 
9
10
  @Module({
10
11
  imports: [
@@ -15,8 +16,8 @@ import { FileService } from './file.service';
15
16
  JwtModule,
16
17
  ),
17
18
  ],
18
- providers: [FileService],
19
- exports: [FileService],
19
+ providers: [FileService, ImageOptimizationService],
20
+ exports: [FileService, ImageOptimizationService],
20
21
  controllers: [FileController],
21
22
  })
22
23
  export class FileModule { }
@@ -1,26 +1,46 @@
1
+ import {
2
+ GetBucketNotificationConfigurationCommand,
3
+ HeadBucketCommand,
4
+ HeadObjectCommand,
5
+ PutBucketNotificationConfigurationCommand,
6
+ S3Client,
7
+ } from '@aws-sdk/client-s3';
8
+ import {
9
+ AddPermissionCommand,
10
+ CreateFunctionCommand,
11
+ GetFunctionConfigurationCommand,
12
+ LambdaClient,
13
+ UpdateFunctionConfigurationCommand,
14
+ } from '@aws-sdk/client-lambda';
15
+ import {
16
+ AssumeRoleCommand,
17
+ GetSessionTokenCommand,
18
+ STSClient,
19
+ } from '@aws-sdk/client-sts';
1
20
  import { getLocaleText } from '@hed-hog/api-locale';
2
21
  import { PaginationDTO, PaginationService } from '@hed-hog/api-pagination';
3
22
  import { PrismaService } from '@hed-hog/api-prisma';
4
23
  import {
5
- BadRequestException,
6
- forwardRef,
7
- Inject,
8
- Injectable,
9
- Logger,
10
- NotFoundException,
11
- OnModuleInit
24
+ BadRequestException,
25
+ forwardRef,
26
+ Inject,
27
+ Injectable,
28
+ Logger,
29
+ NotFoundException,
30
+ OnModuleInit
12
31
  } from '@nestjs/common';
13
32
  import { JwtService } from '@nestjs/jwt';
14
33
  import { createWriteStream, promises as fs } from 'fs';
15
34
  import { basename, dirname } from 'path';
16
35
  import { pipeline } from 'stream/promises';
17
36
  import {
18
- buildStorageConfigFromIntegration,
19
- resolveFileProviderSlug,
20
- resolveStorageEnumProvider,
37
+ buildStorageConfigFromIntegration,
38
+ resolveFileProviderSlug,
39
+ resolveStorageEnumProvider,
21
40
  } from '../integration-profile/integration-profile.utils';
22
41
  import { SettingService } from '../setting/setting.service';
23
42
  import { DeleteDTO } from './dto/delete.dto';
43
+ import { ImageOptimizationService } from './image-optimization.service';
24
44
  import { AbstractProvider } from './provider/abstract.provider';
25
45
  import { ProviderFactory } from './provider/provider.factory';
26
46
 
@@ -65,6 +85,8 @@ export class FileService implements OnModuleInit {
65
85
  private readonly settingService: SettingService,
66
86
  @Inject(forwardRef(() => JwtService))
67
87
  private readonly jwtService: JwtService,
88
+ @Inject(forwardRef(() => ImageOptimizationService))
89
+ private readonly imageOptimizationService: ImageOptimizationService,
68
90
  ) { }
69
91
 
70
92
  async onModuleInit() {
@@ -391,21 +413,30 @@ export class FileService implements OnModuleInit {
391
413
  fileBuffer.originalname,
392
414
  );
393
415
 
416
+ const optimized = await this.imageOptimizationService.optimizeBuffer(
417
+ fileBuffer.buffer,
418
+ fileBuffer.mimetype,
419
+ normalizedFilename,
420
+ );
421
+
394
422
  const normalizedFile = {
395
423
  ...fileBuffer,
396
- originalname: normalizedFilename,
424
+ buffer: optimized.buffer,
425
+ mimetype: optimized.mimetype,
426
+ originalname: optimized.originalname,
427
+ size: optimized.buffer.length,
397
428
  };
398
429
 
399
430
  const url = await provider.upload(destination, normalizedFile);
400
431
 
401
432
  const file = await this.prismaService.file.create({
402
433
  data: {
403
- filename: normalizedFilename,
434
+ filename: optimized.originalname,
404
435
  path: url,
405
436
  provider_id: this.providerId,
406
437
  location: destination,
407
- mimetype_id: await this.getMimeType(fileBuffer.mimetype),
408
- size: fileBuffer.size,
438
+ mimetype_id: await this.getMimeType(optimized.mimetype),
439
+ size: optimized.buffer.length,
409
440
  },
410
441
  });
411
442
 
@@ -450,45 +481,35 @@ export class FileService implements OnModuleInit {
450
481
  throw new BadRequestException(`File too large: ${size} bytes`);
451
482
  }
452
483
 
453
- const provider = await this.getProvider();
454
- const providerWithPathUpload = provider as AbstractProvider & {
455
- uploadFromPath?: (
456
- destination: string,
457
- filePath: string,
458
- options?: { filename?: string; mimetype?: string },
459
- ) => Promise<string>;
460
- };
484
+ // Read buffer to allow image optimization regardless of provider support
485
+ const rawBuffer = await fs.readFile(filePath);
486
+ const optimized = await this.imageOptimizationService.optimizeBuffer(
487
+ rawBuffer,
488
+ mimetype,
489
+ originalname,
490
+ );
461
491
 
462
- let url: string;
463
- if (typeof providerWithPathUpload.uploadFromPath === 'function') {
464
- url = await providerWithPathUpload.uploadFromPath(destination, filePath, {
465
- filename: originalname,
466
- mimetype,
467
- });
468
- } else {
469
- // Fallback explícito para providers legados sem suporte nativo por path.
470
- const buffer = await fs.readFile(filePath);
471
- url = await provider.upload(destination, {
472
- fieldname: 'file',
473
- originalname,
474
- encoding: metadata?.encoding ?? '7bit',
475
- mimetype,
476
- size,
477
- destination: '',
478
- filename: originalname,
479
- path: filePath,
480
- buffer,
481
- });
482
- }
492
+ const provider = await this.getProvider();
493
+ const url = await provider.upload(destination, {
494
+ fieldname: 'file',
495
+ originalname: optimized.originalname,
496
+ encoding: metadata?.encoding ?? '7bit',
497
+ mimetype: optimized.mimetype,
498
+ size: optimized.buffer.length,
499
+ destination: '',
500
+ filename: optimized.originalname,
501
+ path: filePath,
502
+ buffer: optimized.buffer,
503
+ });
483
504
 
484
505
  const file = await this.prismaService.file.create({
485
506
  data: {
486
- filename: originalname,
507
+ filename: optimized.originalname,
487
508
  path: url,
488
509
  provider_id: this.providerId,
489
510
  location: destination,
490
- mimetype_id: await this.getMimeType(mimetype),
491
- size,
511
+ mimetype_id: await this.getMimeType(optimized.mimetype),
512
+ size: optimized.buffer.length,
492
513
  },
493
514
  });
494
515
 
@@ -666,6 +687,387 @@ export class FileService implements OnModuleInit {
666
687
  return url;
667
688
  }
668
689
 
690
+ async getTemporaryCredentials(params: {
691
+ accessKeyId: string;
692
+ secretAccessKey: string;
693
+ sessionToken?: string;
694
+ region: string;
695
+ roleArn?: string;
696
+ externalId?: string;
697
+ durationSeconds: number;
698
+ sessionName: string;
699
+ }): Promise<{
700
+ AccessKeyId: string;
701
+ SecretAccessKey: string;
702
+ SessionToken: string;
703
+ Expiration?: Date;
704
+ }> {
705
+ const stsClient = new STSClient({
706
+ region: params.region,
707
+ credentials: {
708
+ accessKeyId: params.accessKeyId,
709
+ secretAccessKey: params.secretAccessKey,
710
+ ...(params.sessionToken ? { sessionToken: params.sessionToken } : {}),
711
+ },
712
+ });
713
+
714
+ let creds: { AccessKeyId?: string; SecretAccessKey?: string; SessionToken?: string; Expiration?: Date } | undefined;
715
+
716
+ if (params.roleArn) {
717
+ const result = await stsClient.send(
718
+ new AssumeRoleCommand({
719
+ RoleArn: params.roleArn,
720
+ RoleSessionName: params.sessionName,
721
+ DurationSeconds: params.durationSeconds,
722
+ ...(params.externalId ? { ExternalId: params.externalId } : {}),
723
+ }),
724
+ );
725
+ creds = result.Credentials;
726
+ } else {
727
+ const result = await stsClient.send(
728
+ new GetSessionTokenCommand({ DurationSeconds: params.durationSeconds }),
729
+ );
730
+ creds = result.Credentials;
731
+ }
732
+
733
+ if (!creds?.AccessKeyId || !creds.SecretAccessKey || !creds.SessionToken) {
734
+ throw new BadRequestException('Could not generate temporary AWS credentials.');
735
+ }
736
+
737
+ return {
738
+ AccessKeyId: creds.AccessKeyId,
739
+ SecretAccessKey: creds.SecretAccessKey,
740
+ SessionToken: creds.SessionToken,
741
+ Expiration: creds.Expiration,
742
+ };
743
+ }
744
+
745
+ async headS3Object(params: {
746
+ accessKeyId: string;
747
+ secretAccessKey: string;
748
+ sessionToken?: string;
749
+ region: string;
750
+ bucket: string;
751
+ key: string;
752
+ }): Promise<{ ContentLength?: number; ETag?: string }> {
753
+ const s3 = new S3Client({
754
+ region: params.region,
755
+ credentials: {
756
+ accessKeyId: params.accessKeyId,
757
+ secretAccessKey: params.secretAccessKey,
758
+ ...(params.sessionToken ? { sessionToken: params.sessionToken } : {}),
759
+ },
760
+ });
761
+
762
+ const result = await s3.send(
763
+ new HeadObjectCommand({ Bucket: params.bucket, Key: params.key }),
764
+ );
765
+
766
+ return {
767
+ ContentLength: result.ContentLength,
768
+ ETag: result.ETag,
769
+ };
770
+ }
771
+
772
+ async headS3Bucket(params: {
773
+ accessKeyId: string;
774
+ secretAccessKey: string;
775
+ sessionToken?: string;
776
+ region: string;
777
+ bucket: string;
778
+ }): Promise<void> {
779
+ const s3 = new S3Client({
780
+ region: params.region,
781
+ credentials: {
782
+ accessKeyId: params.accessKeyId,
783
+ secretAccessKey: params.secretAccessKey,
784
+ ...(params.sessionToken ? { sessionToken: params.sessionToken } : {}),
785
+ },
786
+ });
787
+
788
+ await s3.send(new HeadBucketCommand({ Bucket: params.bucket }));
789
+ }
790
+
791
+ async getLambdaFunctionConfiguration(params: {
792
+ functionName: string;
793
+ region: string;
794
+ accessKeyId: string;
795
+ secretAccessKey: string;
796
+ sessionToken?: string;
797
+ }) {
798
+ const lambda = new LambdaClient({
799
+ region: params.region,
800
+ credentials: {
801
+ accessKeyId: params.accessKeyId,
802
+ secretAccessKey: params.secretAccessKey,
803
+ ...(params.sessionToken ? { sessionToken: params.sessionToken } : {}),
804
+ },
805
+ });
806
+
807
+ return lambda.send(
808
+ new GetFunctionConfigurationCommand({
809
+ FunctionName: params.functionName,
810
+ }),
811
+ );
812
+ }
813
+
814
+ async updateLambdaEnvironmentVariables(params: {
815
+ functionName: string;
816
+ region: string;
817
+ accessKeyId: string;
818
+ secretAccessKey: string;
819
+ sessionToken?: string;
820
+ environmentVariables: Record<string, string>;
821
+ }) {
822
+ const lambda = new LambdaClient({
823
+ region: params.region,
824
+ credentials: {
825
+ accessKeyId: params.accessKeyId,
826
+ secretAccessKey: params.secretAccessKey,
827
+ ...(params.sessionToken ? { sessionToken: params.sessionToken } : {}),
828
+ },
829
+ });
830
+
831
+ const current = await lambda.send(
832
+ new GetFunctionConfigurationCommand({
833
+ FunctionName: params.functionName,
834
+ }),
835
+ );
836
+
837
+ const merged = {
838
+ ...(current.Environment?.Variables ?? {}),
839
+ ...params.environmentVariables,
840
+ };
841
+
842
+ await lambda.send(
843
+ new UpdateFunctionConfigurationCommand({
844
+ FunctionName: params.functionName,
845
+ Environment: {
846
+ Variables: merged,
847
+ },
848
+ }),
849
+ );
850
+
851
+ return {
852
+ functionName: current.FunctionName ?? params.functionName,
853
+ functionArn: current.FunctionArn ?? null,
854
+ };
855
+ }
856
+
857
+ async createLambdaFunctionIfMissing(params: {
858
+ functionName: string;
859
+ region: string;
860
+ accessKeyId: string;
861
+ secretAccessKey: string;
862
+ sessionToken?: string;
863
+ roleArn: string;
864
+ packageType: 'Image' | 'Zip';
865
+ imageUri?: string;
866
+ zipFile?: Uint8Array;
867
+ s3Bucket?: string;
868
+ s3Key?: string;
869
+ s3ObjectVersion?: string;
870
+ handler?: string;
871
+ runtime?: string;
872
+ timeout?: number;
873
+ memorySize?: number;
874
+ environmentVariables?: Record<string, string>;
875
+ }) {
876
+ const lambda = new LambdaClient({
877
+ region: params.region,
878
+ credentials: {
879
+ accessKeyId: params.accessKeyId,
880
+ secretAccessKey: params.secretAccessKey,
881
+ ...(params.sessionToken ? { sessionToken: params.sessionToken } : {}),
882
+ },
883
+ });
884
+
885
+ try {
886
+ const existing = await lambda.send(
887
+ new GetFunctionConfigurationCommand({
888
+ FunctionName: params.functionName,
889
+ }),
890
+ );
891
+
892
+ return {
893
+ created: false,
894
+ functionName: existing.FunctionName ?? params.functionName,
895
+ functionArn: existing.FunctionArn ?? null,
896
+ };
897
+ } catch (error: any) {
898
+ if (String(error?.name ?? '') !== 'ResourceNotFoundException') {
899
+ throw error;
900
+ }
901
+ }
902
+
903
+ const baseInput = {
904
+ FunctionName: params.functionName,
905
+ Role: params.roleArn,
906
+ PackageType: params.packageType,
907
+ Timeout: params.timeout,
908
+ MemorySize: params.memorySize,
909
+ Environment: params.environmentVariables
910
+ ? {
911
+ Variables: params.environmentVariables,
912
+ }
913
+ : undefined,
914
+ } as const;
915
+
916
+ const created =
917
+ params.packageType === 'Image'
918
+ ? await lambda.send(
919
+ new CreateFunctionCommand({
920
+ ...baseInput,
921
+ Code: {
922
+ ImageUri: params.imageUri,
923
+ },
924
+ }),
925
+ )
926
+ : await lambda.send(
927
+ new CreateFunctionCommand({
928
+ ...baseInput,
929
+ Handler: params.handler,
930
+ Runtime: params.runtime as any,
931
+ Code: {
932
+ ...(params.zipFile
933
+ ? {
934
+ ZipFile: params.zipFile,
935
+ }
936
+ : {
937
+ S3Bucket: params.s3Bucket,
938
+ S3Key: params.s3Key,
939
+ ...(params.s3ObjectVersion
940
+ ? {
941
+ S3ObjectVersion: params.s3ObjectVersion,
942
+ }
943
+ : {}),
944
+ }),
945
+ },
946
+ }),
947
+ );
948
+
949
+ return {
950
+ created: true,
951
+ functionName: created.FunctionName ?? params.functionName,
952
+ functionArn: created.FunctionArn ?? null,
953
+ };
954
+ }
955
+
956
+ async allowS3InvokeLambda(params: {
957
+ functionName: string;
958
+ region: string;
959
+ accessKeyId: string;
960
+ secretAccessKey: string;
961
+ sessionToken?: string;
962
+ bucketName: string;
963
+ sourceAccount?: string;
964
+ statementId?: string;
965
+ }) {
966
+ const lambda = new LambdaClient({
967
+ region: params.region,
968
+ credentials: {
969
+ accessKeyId: params.accessKeyId,
970
+ secretAccessKey: params.secretAccessKey,
971
+ ...(params.sessionToken ? { sessionToken: params.sessionToken } : {}),
972
+ },
973
+ });
974
+
975
+ const statementId =
976
+ params.statementId ?? `lms-bulk-upload-${params.bucketName.replace(/[^a-zA-Z0-9-_]/g, '-')}`;
977
+
978
+ try {
979
+ await lambda.send(
980
+ new AddPermissionCommand({
981
+ FunctionName: params.functionName,
982
+ StatementId: statementId.slice(0, 100),
983
+ Action: 'lambda:InvokeFunction',
984
+ Principal: 's3.amazonaws.com',
985
+ SourceArn: `arn:aws:s3:::${params.bucketName}`,
986
+ ...(params.sourceAccount
987
+ ? {
988
+ SourceAccount: params.sourceAccount,
989
+ }
990
+ : {}),
991
+ }),
992
+ );
993
+ } catch (error: any) {
994
+ if (String(error?.name ?? '') === 'ResourceConflictException') {
995
+ return;
996
+ }
997
+
998
+ throw error;
999
+ }
1000
+ }
1001
+
1002
+ async configureS3BucketLambdaNotification(params: {
1003
+ bucketName: string;
1004
+ lambdaArn: string;
1005
+ region: string;
1006
+ accessKeyId: string;
1007
+ secretAccessKey: string;
1008
+ sessionToken?: string;
1009
+ configurationId?: string;
1010
+ prefix?: string;
1011
+ }) {
1012
+ const s3 = new S3Client({
1013
+ region: params.region,
1014
+ credentials: {
1015
+ accessKeyId: params.accessKeyId,
1016
+ secretAccessKey: params.secretAccessKey,
1017
+ ...(params.sessionToken ? { sessionToken: params.sessionToken } : {}),
1018
+ },
1019
+ });
1020
+
1021
+ const configurationId =
1022
+ params.configurationId ?? 'lms-bulk-upload-object-created';
1023
+
1024
+ const existing = await s3.send(
1025
+ new GetBucketNotificationConfigurationCommand({
1026
+ Bucket: params.bucketName,
1027
+ }),
1028
+ );
1029
+
1030
+ const lambdaConfigurations = Array.isArray(existing?.LambdaFunctionConfigurations)
1031
+ ? existing.LambdaFunctionConfigurations.filter((cfg: any) => cfg?.Id !== configurationId)
1032
+ : [];
1033
+
1034
+ lambdaConfigurations.push({
1035
+ Id: configurationId,
1036
+ LambdaFunctionArn: params.lambdaArn,
1037
+ Events: ['s3:ObjectCreated:*'],
1038
+ ...(params.prefix
1039
+ ? {
1040
+ Filter: {
1041
+ Key: {
1042
+ FilterRules: [
1043
+ {
1044
+ Name: 'prefix',
1045
+ Value: params.prefix,
1046
+ },
1047
+ ],
1048
+ },
1049
+ },
1050
+ }
1051
+ : {}),
1052
+ });
1053
+
1054
+ await s3.send(
1055
+ new PutBucketNotificationConfigurationCommand({
1056
+ Bucket: params.bucketName,
1057
+ NotificationConfiguration: {
1058
+ TopicConfigurations: Array.isArray(existing?.TopicConfigurations)
1059
+ ? existing.TopicConfigurations
1060
+ : [],
1061
+ QueueConfigurations: Array.isArray(existing?.QueueConfigurations)
1062
+ ? existing.QueueConfigurations
1063
+ : [],
1064
+ EventBridgeConfiguration: existing?.EventBridgeConfiguration,
1065
+ LambdaFunctionConfigurations: lambdaConfigurations,
1066
+ },
1067
+ }),
1068
+ );
1069
+ }
1070
+
669
1071
  createDownloadToken(filepath: string, expires = 3600) {
670
1072
  if (!filepath) {
671
1073
  throw new BadRequestException(`No file path provided`);