@mbc-cqrs-serverless/core 1.3.3 → 1.3.5

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 (101) hide show
  1. package/README.md +37 -0
  2. package/dist/app.module.js +2 -0
  3. package/dist/app.module.js.map +1 -1
  4. package/dist/auth/auth.module.d.ts +2 -0
  5. package/dist/auth/auth.module.js +30 -0
  6. package/dist/auth/auth.module.js.map +1 -0
  7. package/dist/auth/authz-bootstrap.service.d.ts +11 -0
  8. package/dist/auth/authz-bootstrap.service.js +47 -0
  9. package/dist/auth/authz-bootstrap.service.js.map +1 -0
  10. package/dist/auth/group-role-resolver.interface.d.ts +13 -0
  11. package/dist/auth/group-role-resolver.interface.js +3 -0
  12. package/dist/auth/group-role-resolver.interface.js.map +1 -0
  13. package/dist/auth/group-role-resolver.registry.d.ts +6 -0
  14. package/dist/auth/group-role-resolver.registry.js +26 -0
  15. package/dist/auth/group-role-resolver.registry.js.map +1 -0
  16. package/dist/auth/index.d.ts +4 -0
  17. package/dist/auth/index.js +21 -0
  18. package/dist/auth/index.js.map +1 -0
  19. package/dist/command-events/data-sync.new.event.handler.js +25 -1
  20. package/dist/command-events/data-sync.new.event.handler.js.map +1 -1
  21. package/dist/commands/command.event.handler.d.ts +26 -1
  22. package/dist/commands/command.event.handler.js +165 -6
  23. package/dist/commands/command.event.handler.js.map +1 -1
  24. package/dist/commands/command.service.d.ts +3 -1
  25. package/dist/commands/command.service.js +40 -12
  26. package/dist/commands/command.service.js.map +1 -1
  27. package/dist/context/invoke.d.ts +1 -0
  28. package/dist/context/invoke.js +1 -1
  29. package/dist/context/invoke.js.map +1 -1
  30. package/dist/context/user.d.ts +4 -0
  31. package/dist/context/user.js +50 -0
  32. package/dist/context/user.js.map +1 -1
  33. package/dist/data-store/dynamodb.service.d.ts +3 -1
  34. package/dist/data-store/dynamodb.service.js +2 -1
  35. package/dist/data-store/dynamodb.service.js.map +1 -1
  36. package/dist/decorators/constants.d.ts +2 -0
  37. package/dist/decorators/constants.js +3 -1
  38. package/dist/decorators/constants.js.map +1 -1
  39. package/dist/decorators/data-sync-handler.decorator.d.ts +19 -0
  40. package/dist/decorators/data-sync-handler.decorator.js +19 -0
  41. package/dist/decorators/data-sync-handler.decorator.js.map +1 -1
  42. package/dist/decorators/group-role-resolver.decorator.d.ts +9 -0
  43. package/dist/decorators/group-role-resolver.decorator.js +17 -0
  44. package/dist/decorators/group-role-resolver.decorator.js.map +1 -0
  45. package/dist/decorators/index.d.ts +2 -0
  46. package/dist/decorators/index.js +2 -0
  47. package/dist/decorators/index.js.map +1 -1
  48. package/dist/decorators/notification-transport.decorator.d.ts +6 -0
  49. package/dist/decorators/notification-transport.decorator.js +14 -0
  50. package/dist/decorators/notification-transport.decorator.js.map +1 -0
  51. package/dist/env.validation.d.ts +19 -0
  52. package/dist/env.validation.js +45 -2
  53. package/dist/env.validation.js.map +1 -1
  54. package/dist/guard/roles.guard.d.ts +11 -1
  55. package/dist/guard/roles.guard.js +44 -8
  56. package/dist/guard/roles.guard.js.map +1 -1
  57. package/dist/helpers/serializer.d.ts +1 -2
  58. package/dist/helpers/serializer.js +34 -7
  59. package/dist/helpers/serializer.js.map +1 -1
  60. package/dist/index.d.ts +1 -0
  61. package/dist/index.js +1 -0
  62. package/dist/index.js.map +1 -1
  63. package/dist/interfaces/command-module-options.interface.d.ts +11 -1
  64. package/dist/notification-env.validation.d.ts +8 -0
  65. package/dist/notification-env.validation.js +36 -0
  66. package/dist/notification-env.validation.js.map +1 -0
  67. package/dist/notifications/appsync-events.service.d.ts +44 -0
  68. package/dist/notifications/appsync-events.service.js +147 -0
  69. package/dist/notifications/appsync-events.service.js.map +1 -0
  70. package/dist/notifications/appsync.service.d.ts +3 -2
  71. package/dist/notifications/appsync.service.js +20 -12
  72. package/dist/notifications/appsync.service.js.map +1 -1
  73. package/dist/notifications/enums/index.d.ts +1 -0
  74. package/dist/notifications/enums/index.js +18 -0
  75. package/dist/notifications/enums/index.js.map +1 -0
  76. package/dist/notifications/enums/notification-transport.enum.d.ts +4 -0
  77. package/dist/notifications/enums/notification-transport.enum.js +9 -0
  78. package/dist/notifications/enums/notification-transport.enum.js.map +1 -0
  79. package/dist/notifications/event/notification.event.handler.d.ts +13 -5
  80. package/dist/notifications/event/notification.event.handler.js +31 -7
  81. package/dist/notifications/event/notification.event.handler.js.map +1 -1
  82. package/dist/notifications/index.d.ts +3 -0
  83. package/dist/notifications/index.js +3 -0
  84. package/dist/notifications/index.js.map +1 -1
  85. package/dist/notifications/interfaces/index.d.ts +1 -0
  86. package/dist/notifications/interfaces/index.js +18 -0
  87. package/dist/notifications/interfaces/index.js.map +1 -0
  88. package/dist/notifications/interfaces/notification-transport.interface.d.ts +7 -0
  89. package/dist/notifications/interfaces/notification-transport.interface.js +5 -0
  90. package/dist/notifications/interfaces/notification-transport.interface.js.map +1 -0
  91. package/dist/notifications/notification.module.js +12 -2
  92. package/dist/notifications/notification.module.js.map +1 -1
  93. package/dist/services/explorer.service.d.ts +5 -0
  94. package/dist/services/explorer.service.js +12 -1
  95. package/dist/services/explorer.service.js.map +1 -1
  96. package/dist/services/mocks/group-role-resolver.mock.d.ts +4 -0
  97. package/dist/services/mocks/group-role-resolver.mock.js +20 -0
  98. package/dist/services/mocks/group-role-resolver.mock.js.map +1 -0
  99. package/dist/step-func/step-function.service.js +6 -1
  100. package/dist/step-func/step-function.service.js.map +1 -1
  101. package/package.json +4 -4
package/README.md CHANGED
@@ -215,6 +215,43 @@ export class TodoDataSyncHandler implements IDataSyncHandler {
215
215
  }
216
216
  ```
217
217
 
218
+ ## Group-based roles
219
+
220
+ `RolesGuard` checks direct roles from JWT `custom:roles` first, then roles from groups in `custom:groups` (tenant-scoped). Group → role mappings are **not** stored in the JWT; implement a resolver in your app.
221
+
222
+ **JWT example:**
223
+
224
+ ```json
225
+ {
226
+ "custom:roles": "[{\"tenant\":\"tenant-a\",\"role\":\"admin\"}]",
227
+ "custom:groups": "[{\"tenant\":\"tenant-a\",\"groups\":[\"sales-team\"]}]"
228
+ }
229
+ ```
230
+
231
+ **Resolver (one per app):**
232
+
233
+ ```typescript
234
+ import {
235
+ GroupRoleResolver,
236
+ IGroupRoleResolver,
237
+ } from '@mbc-cqrs-serverless/core';
238
+
239
+ // Do not add @Injectable() — @GroupRoleResolver() already registers the provider.
240
+ @GroupRoleResolver()
241
+ export class AppGroupRoleResolver implements IGroupRoleResolver {
242
+ async resolveRoles({ tenantCode, groupIds, claims }) {
243
+ // Load roles from DynamoDB, RDS, config, etc.
244
+ return ['viewer', 'reporter'];
245
+ }
246
+ }
247
+ ```
248
+
249
+ Register the class in your NestJS module `providers`. `AuthModule` is imported automatically via core `AppModule.forRoot()` and exports `GroupRoleResolverRegistry` globally for `@Auth()` / `RolesGuard`.
250
+
251
+ ### Extending authorization
252
+
253
+ `RolesGuard` evaluates `tenantRoles` from the JWT (not `getUserRole()`). The protected `getUserRole()` method is **deprecated** and not called by the default guard. Subclasses should override `verifyRole` or `resolveGroupRoles` for custom logic.
254
+
218
255
  ## Environment Variables
219
256
 
220
257
  | Variable | Description | Default |
@@ -13,6 +13,7 @@ const core_1 = require("@nestjs/core");
13
13
  const app_controller_1 = require("./app.controller");
14
14
  const app_module_definition_1 = require("./app.module-definition");
15
15
  const app_service_1 = require("./app.service");
16
+ const auth_1 = require("./auth");
16
17
  const data_sync_module_1 = require("./command-events/data-sync.module");
17
18
  const data_store_module_1 = require("./data-store/data-store.module");
18
19
  const env_validation_1 = require("./env.validation");
@@ -64,6 +65,7 @@ exports.AppModule = AppModule;
64
65
  exports.AppModule = AppModule = __decorate([
65
66
  (0, common_1.Module)({
66
67
  imports: [
68
+ auth_1.AuthModule,
67
69
  notification_module_1.NotificationModule,
68
70
  data_store_module_1.DataStoreModule,
69
71
  data_sync_module_1.DataSyncModule,
@@ -1 +1 @@
1
- {"version":3,"file":"app.module.js","sourceRoot":"","sources":["../src/app.module.ts"],"names":[],"mappings":";;;;;;;;;AAAA,2CAAsD;AACtD,2CAA6C;AAC7C,uCAAmD;AAEnD,qDAAgD;AAChD,mEAA+E;AAC/E,+CAA0C;AAC1C,wEAAkE;AAClE,sEAAgE;AAChE,qDAAoD;AACpD,qCAAsC;AACtC,6EAAwE;AACxE,uDAAkD;AAClD,kEAA6D;AAC7D,2EAAqE;AAa9D,IAAM,SAAS,GAAf,MAAM,SAAU,SAAQ,+CAAuB;IACpD,MAAM,CAAC,OAAO,CAAC,OAA4B;QACzC,MAAM,uBAAuB,GAAG,OAAO,CAAC,GAAG,CAAC,qBAAqB,KAAK,MAAM,CAAA;QAC5E,MAAM,OAAO,GAAW;YACtB;gBACE,IAAI,EAAE,KAAK;gBACX,MAAM,EAAE,OAAO,CAAC,UAAU;aAC3B;SACF,CAAA;QACD,iCAAiC;QACjC,IAAI,uBAAuB,EAAE,CAAC;YAC5B,OAAO,CAAC,IAAI,CAAC;gBACX,IAAI,EAAE,OAAO;gBACb,MAAM,EAAE,oBAAW;aACpB,CAAC,CAAA;QACJ,CAAC;QAED,MAAM,MAAM,GAAG,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,CAAA;QACrC,MAAM,OAAO,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC,CAAA;QAC3C,OAAO,CAAC,IAAI,CACV,qBAAY,CAAC,OAAO,CAAC;YACnB,QAAQ,EAAE,IAAI;YACd,KAAK,EAAE,IAAI;YACX,eAAe,EAAE,IAAI;YACrB,iBAAiB,EAAE;gBACjB,YAAY,EAAE,KAAK;gBACnB,UAAU,EAAE,IAAI;aACjB;YACD,QAAQ,EAAE,IAAA,kCAAiB,EAAC,OAAO,CAAC,MAAM,CAAC;SAC5C,CAAC,CACH,CAAA;QACD,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,CAAA;QAChC,IAAI,uBAAuB,EAAE,CAAC;YAC5B,OAAO,CAAC,IAAI,CAAC,oBAAW,CAAC,CAAA;QAC3B,CAAC;QACD,OAAO,CAAC,IAAI,CAAC,mBAAY,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAA;QAE5C,OAAO;YACL,GAAG,MAAM;YACT,OAAO;SACR,CAAA;IACH,CAAC;CACF,CAAA;AA1CY,8BAAS;oBAAT,SAAS;IAXrB,IAAA,eAAM,EAAC;QACN,OAAO,EAAE;YACP,wCAAkB;YAClB,mCAAe;YACf,iCAAc;YACd,yCAAkB;YAClB,0BAAW;SACZ;QACD,WAAW,EAAE,CAAC,8BAAa,CAAC;QAC5B,SAAS,EAAE,CAAC,wBAAU,EAAE,kCAAe,CAAC;KACzC,CAAC;GACW,SAAS,CA0CrB"}
1
+ {"version":3,"file":"app.module.js","sourceRoot":"","sources":["../src/app.module.ts"],"names":[],"mappings":";;;;;;;;;AAAA,2CAAsD;AACtD,2CAA6C;AAC7C,uCAAmD;AAEnD,qDAAgD;AAChD,mEAA+E;AAC/E,+CAA0C;AAC1C,iCAAmC;AACnC,wEAAkE;AAClE,sEAAgE;AAChE,qDAAoD;AACpD,qCAAsC;AACtC,6EAAwE;AACxE,uDAAkD;AAClD,kEAA6D;AAC7D,2EAAqE;AAc9D,IAAM,SAAS,GAAf,MAAM,SAAU,SAAQ,+CAAuB;IACpD,MAAM,CAAC,OAAO,CAAC,OAA4B;QACzC,MAAM,uBAAuB,GAAG,OAAO,CAAC,GAAG,CAAC,qBAAqB,KAAK,MAAM,CAAA;QAC5E,MAAM,OAAO,GAAW;YACtB;gBACE,IAAI,EAAE,KAAK;gBACX,MAAM,EAAE,OAAO,CAAC,UAAU;aAC3B;SACF,CAAA;QACD,iCAAiC;QACjC,IAAI,uBAAuB,EAAE,CAAC;YAC5B,OAAO,CAAC,IAAI,CAAC;gBACX,IAAI,EAAE,OAAO;gBACb,MAAM,EAAE,oBAAW;aACpB,CAAC,CAAA;QACJ,CAAC;QAED,MAAM,MAAM,GAAG,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,CAAA;QACrC,MAAM,OAAO,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC,CAAA;QAC3C,OAAO,CAAC,IAAI,CACV,qBAAY,CAAC,OAAO,CAAC;YACnB,QAAQ,EAAE,IAAI;YACd,KAAK,EAAE,IAAI;YACX,eAAe,EAAE,IAAI;YACrB,iBAAiB,EAAE;gBACjB,YAAY,EAAE,KAAK;gBACnB,UAAU,EAAE,IAAI;aACjB;YACD,QAAQ,EAAE,IAAA,kCAAiB,EAAC,OAAO,CAAC,MAAM,CAAC;SAC5C,CAAC,CACH,CAAA;QACD,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,CAAA;QAChC,IAAI,uBAAuB,EAAE,CAAC;YAC5B,OAAO,CAAC,IAAI,CAAC,oBAAW,CAAC,CAAA;QAC3B,CAAC;QACD,OAAO,CAAC,IAAI,CAAC,mBAAY,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAA;QAE5C,OAAO;YACL,GAAG,MAAM;YACT,OAAO;SACR,CAAA;IACH,CAAC;CACF,CAAA;AA1CY,8BAAS;oBAAT,SAAS;IAZrB,IAAA,eAAM,EAAC;QACN,OAAO,EAAE;YACP,iBAAU;YACV,wCAAkB;YAClB,mCAAe;YACf,iCAAc;YACd,yCAAkB;YAClB,0BAAW;SACZ;QACD,WAAW,EAAE,CAAC,8BAAa,CAAC;QAC5B,SAAS,EAAE,CAAC,wBAAU,EAAE,kCAAe,CAAC;KACzC,CAAC;GACW,SAAS,CA0CrB"}
@@ -0,0 +1,2 @@
1
+ export declare class AuthModule {
2
+ }
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.AuthModule = void 0;
10
+ const common_1 = require("@nestjs/common");
11
+ const roles_guard_1 = require("../guard/roles.guard");
12
+ const explorer_service_1 = require("../services/explorer.service");
13
+ const authz_bootstrap_service_1 = require("./authz-bootstrap.service");
14
+ const group_role_resolver_registry_1 = require("./group-role-resolver.registry");
15
+ let AuthModule = class AuthModule {
16
+ };
17
+ exports.AuthModule = AuthModule;
18
+ exports.AuthModule = AuthModule = __decorate([
19
+ (0, common_1.Global)(),
20
+ (0, common_1.Module)({
21
+ providers: [
22
+ explorer_service_1.ExplorerService,
23
+ group_role_resolver_registry_1.GroupRoleResolverRegistry,
24
+ authz_bootstrap_service_1.AuthzBootstrapService,
25
+ roles_guard_1.RolesGuard,
26
+ ],
27
+ exports: [group_role_resolver_registry_1.GroupRoleResolverRegistry, roles_guard_1.RolesGuard, explorer_service_1.ExplorerService],
28
+ })
29
+ ], AuthModule);
30
+ //# sourceMappingURL=auth.module.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth.module.js","sourceRoot":"","sources":["../../src/auth/auth.module.ts"],"names":[],"mappings":";;;;;;;;;AAAA,2CAA+C;AAE/C,sDAAiD;AACjD,mEAA8D;AAC9D,uEAAiE;AACjE,iFAA0E;AAYnE,IAAM,UAAU,GAAhB,MAAM,UAAU;CAAG,CAAA;AAAb,gCAAU;qBAAV,UAAU;IAVtB,IAAA,eAAM,GAAE;IACR,IAAA,eAAM,EAAC;QACN,SAAS,EAAE;YACT,kCAAe;YACf,wDAAyB;YACzB,+CAAqB;YACrB,wBAAU;SACX;QACD,OAAO,EAAE,CAAC,wDAAyB,EAAE,wBAAU,EAAE,kCAAe,CAAC;KAClE,CAAC;GACW,UAAU,CAAG"}
@@ -0,0 +1,11 @@
1
+ import { OnApplicationBootstrap } from '@nestjs/common';
2
+ import { ModuleRef } from '@nestjs/core';
3
+ import { ExplorerService } from '../services/explorer.service';
4
+ import { GroupRoleResolverRegistry } from './group-role-resolver.registry';
5
+ export declare class AuthzBootstrapService implements OnApplicationBootstrap {
6
+ private readonly explorerService;
7
+ private readonly moduleRef;
8
+ private readonly registry;
9
+ constructor(explorerService: ExplorerService, moduleRef: ModuleRef, registry: GroupRoleResolverRegistry);
10
+ onApplicationBootstrap(): void;
11
+ }
@@ -0,0 +1,47 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.AuthzBootstrapService = void 0;
13
+ const common_1 = require("@nestjs/common");
14
+ const core_1 = require("@nestjs/core");
15
+ const explorer_service_1 = require("../services/explorer.service");
16
+ const group_role_resolver_registry_1 = require("./group-role-resolver.registry");
17
+ let AuthzBootstrapService = class AuthzBootstrapService {
18
+ constructor(explorerService, moduleRef, registry) {
19
+ this.explorerService = explorerService;
20
+ this.moduleRef = moduleRef;
21
+ this.registry = registry;
22
+ }
23
+ onApplicationBootstrap() {
24
+ const types = this.explorerService.exploreGroupRoleResolvers();
25
+ if (types.length > 1) {
26
+ throw new Error('Only one @GroupRoleResolver() is allowed per application');
27
+ }
28
+ if (types.length === 0) {
29
+ return;
30
+ }
31
+ const instance = this.moduleRef.get(types[0], {
32
+ strict: false,
33
+ });
34
+ if (!instance) {
35
+ throw new Error(`GroupRoleResolver ${types[0].name} was discovered but could not be resolved from DI`);
36
+ }
37
+ this.registry.set(instance);
38
+ }
39
+ };
40
+ exports.AuthzBootstrapService = AuthzBootstrapService;
41
+ exports.AuthzBootstrapService = AuthzBootstrapService = __decorate([
42
+ (0, common_1.Injectable)(),
43
+ __metadata("design:paramtypes", [explorer_service_1.ExplorerService,
44
+ core_1.ModuleRef,
45
+ group_role_resolver_registry_1.GroupRoleResolverRegistry])
46
+ ], AuthzBootstrapService);
47
+ //# sourceMappingURL=authz-bootstrap.service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"authz-bootstrap.service.js","sourceRoot":"","sources":["../../src/auth/authz-bootstrap.service.ts"],"names":[],"mappings":";;;;;;;;;;;;AAAA,2CAAmE;AACnE,uCAAwC;AAExC,mEAA8D;AAE9D,iFAA0E;AAGnE,IAAM,qBAAqB,GAA3B,MAAM,qBAAqB;IAChC,YACmB,eAAgC,EAChC,SAAoB,EACpB,QAAmC;QAFnC,oBAAe,GAAf,eAAe,CAAiB;QAChC,cAAS,GAAT,SAAS,CAAW;QACpB,aAAQ,GAAR,QAAQ,CAA2B;IACnD,CAAC;IAEJ,sBAAsB;QACpB,MAAM,KAAK,GAAG,IAAI,CAAC,eAAe,CAAC,yBAAyB,EAAE,CAAA;QAE9D,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACrB,MAAM,IAAI,KAAK,CACb,0DAA0D,CAC3D,CAAA;QACH,CAAC;QAED,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACvB,OAAM;QACR,CAAC;QAED,MAAM,QAAQ,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,CAAqB,KAAK,CAAC,CAAC,CAAC,EAAE;YAChE,MAAM,EAAE,KAAK;SACd,CAAC,CAAA;QAEF,IAAI,CAAC,QAAQ,EAAE,CAAC;YACd,MAAM,IAAI,KAAK,CACb,qBAAqB,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,mDAAmD,CACtF,CAAA;QACH,CAAC;QAED,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAA;IAC7B,CAAC;CACF,CAAA;AAhCY,sDAAqB;gCAArB,qBAAqB;IADjC,IAAA,mBAAU,GAAE;qCAGyB,kCAAe;QACrB,gBAAS;QACV,wDAAyB;GAJ3C,qBAAqB,CAgCjC"}
@@ -0,0 +1,13 @@
1
+ import { JwtClaims } from '../context/invoke';
2
+ export interface TenantGroupMembership {
3
+ tenant: string;
4
+ groups: string[];
5
+ }
6
+ export interface ResolveGroupRolesInput {
7
+ tenantCode: string;
8
+ groupIds: string[];
9
+ claims?: JwtClaims;
10
+ }
11
+ export interface IGroupRoleResolver {
12
+ resolveRoles(input: ResolveGroupRolesInput): Promise<string[]>;
13
+ }
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ //# sourceMappingURL=group-role-resolver.interface.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"group-role-resolver.interface.js","sourceRoot":"","sources":["../../src/auth/group-role-resolver.interface.ts"],"names":[],"mappings":""}
@@ -0,0 +1,6 @@
1
+ import { IGroupRoleResolver } from './group-role-resolver.interface';
2
+ export declare class GroupRoleResolverRegistry {
3
+ private resolver?;
4
+ set(resolver: IGroupRoleResolver): void;
5
+ get(): IGroupRoleResolver | undefined;
6
+ }
@@ -0,0 +1,26 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.GroupRoleResolverRegistry = void 0;
10
+ const common_1 = require("@nestjs/common");
11
+ let GroupRoleResolverRegistry = class GroupRoleResolverRegistry {
12
+ set(resolver) {
13
+ if (this.resolver) {
14
+ throw new Error('Only one @GroupRoleResolver() is allowed per application');
15
+ }
16
+ this.resolver = resolver;
17
+ }
18
+ get() {
19
+ return this.resolver;
20
+ }
21
+ };
22
+ exports.GroupRoleResolverRegistry = GroupRoleResolverRegistry;
23
+ exports.GroupRoleResolverRegistry = GroupRoleResolverRegistry = __decorate([
24
+ (0, common_1.Injectable)()
25
+ ], GroupRoleResolverRegistry);
26
+ //# sourceMappingURL=group-role-resolver.registry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"group-role-resolver.registry.js","sourceRoot":"","sources":["../../src/auth/group-role-resolver.registry.ts"],"names":[],"mappings":";;;;;;;;;AAAA,2CAA2C;AAKpC,IAAM,yBAAyB,GAA/B,MAAM,yBAAyB;IAGpC,GAAG,CAAC,QAA4B;QAC9B,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;YAClB,MAAM,IAAI,KAAK,CACb,0DAA0D,CAC3D,CAAA;QACH,CAAC;QACD,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAA;IAC1B,CAAC;IAED,GAAG;QACD,OAAO,IAAI,CAAC,QAAQ,CAAA;IACtB,CAAC;CACF,CAAA;AAfY,8DAAyB;oCAAzB,yBAAyB;IADrC,IAAA,mBAAU,GAAE;GACA,yBAAyB,CAerC"}
@@ -0,0 +1,4 @@
1
+ export * from './auth.module';
2
+ export * from './authz-bootstrap.service';
3
+ export * from './group-role-resolver.interface';
4
+ export * from './group-role-resolver.registry';
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./auth.module"), exports);
18
+ __exportStar(require("./authz-bootstrap.service"), exports);
19
+ __exportStar(require("./group-role-resolver.interface"), exports);
20
+ __exportStar(require("./group-role-resolver.registry"), exports);
21
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/auth/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,gDAA6B;AAC7B,4DAAyC;AACzC,kEAA+C;AAC/C,iEAA8C"}
@@ -34,7 +34,31 @@ let DataSyncNewCommandEventHandler = DataSyncNewCommandEventHandler_1 = class Da
34
34
  .replace('@', '-v')
35
35
  .replace(/[^0-9A-Za-z_-]+/g, `__${Math.random().toString(36).substring(2, 4)}__`);
36
36
  const sfnExecName = `${moduleName}-${ddbRecordId}-${Date.now()}`;
37
- return await this.sfnService.startExecution(this.sfnArn, event, sfnExecName);
37
+ try {
38
+ return await this.sfnService.startExecution(this.sfnArn, event, sfnExecName);
39
+ }
40
+ catch (error) {
41
+ // Only tolerate a missing state machine when running under
42
+ // serverless-offline (local development), where DynamoDB Streams can fire
43
+ // before Step Functions Local has registered the state machine.
44
+ // serverless-offline sets IS_OFFLINE and it is never set in a deployed
45
+ // Lambda, so it is a reliable local-only signal (same check as
46
+ // EmailService). SFN_ENDPOINT is NOT used here because it is a generic
47
+ // endpoint override that may legitimately be set in production (VPC
48
+ // endpoint, PrivateLink). In any other environment a missing state
49
+ // machine is a real misconfiguration (wrong ARN, undeployed/deleted state
50
+ // machine, deploy race) and must surface as a failure — otherwise the
51
+ // command is silently written without its data-sync ever running, with no
52
+ // Lambda error metric / stream retry / alarm to detect it.
53
+ const isOffline = process.env.IS_OFFLINE === 'true' || process.env.IS_OFFLINE === '1';
54
+ if (error?.name === 'StateMachineDoesNotExist' && isOffline) {
55
+ this.logger.warn(`State machine not found (ARN: ${this.sfnArn}). ` +
56
+ 'This may happen during local development if the state machine is not yet registered. ' +
57
+ `Skipping execution for: ${sfnExecName}`);
58
+ return;
59
+ }
60
+ throw error;
61
+ }
38
62
  }
39
63
  };
40
64
  exports.DataSyncNewCommandEventHandler = DataSyncNewCommandEventHandler;
@@ -1 +1 @@
1
- {"version":3,"file":"data-sync.new.event.handler.js","sourceRoot":"","sources":["../../src/command-events/data-sync.new.event.handler.ts"],"names":[],"mappings":";;;;;;;;;;;;;AAAA,2CAAuC;AACvC,2CAA8C;AAE9C,8CAA+C;AAC/C,8CAA4C;AAE5C,8EAAwE;AACxE,+DAA+D;AAGxD,IAAM,8BAA8B,sCAApC,MAAM,8BAA8B;IAMzC,YACmB,MAAqB,EACrB,UAA+B,EAC/B,eAAgC;QAFhC,WAAM,GAAN,MAAM,CAAe;QACrB,eAAU,GAAV,UAAU,CAAqB;QAC/B,oBAAe,GAAf,eAAe,CAAiB;QANlC,WAAM,GAAG,IAAI,eAAM,CAAC,gCAA8B,CAAC,IAAI,CAAC,CAAA;QAQvE,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC,GAAG,CAAC,iBAAiB,CAAC,CAAA;IAC7C,CAAC;IAED,KAAK,CAAC,OAAO,CAAC,KAA8B;QAC1C,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,aAAa,EAAE,KAAK,CAAC,CAAA;QACvC,MAAM,UAAU,GAAG,IAAI,CAAC,eAAe,CAAC,aAAa,CAAC,KAAK,CAAC,SAAS,CAAC,CAAA;QACtE,MAAM,OAAO,GAAG,KAAK,CAAC,QAAQ,EAAE,IAAI,CAAA;QACpC,MAAM,WAAW,GAAG,GAAG,OAAO,CAAC,EAAE,EAAE,CAAC,IAAI,IAAI,IAAI,OAAO,CAAC,EAAE,EAAE,CAAC,IAAI,IAAI,EAAE;aACpE,UAAU,CAAC,GAAG,EAAE,GAAG,CAAC;aACpB,OAAO,CAAC,GAAG,EAAE,IAAI,CAAC;aAClB,OAAO,CACN,kBAAkB,EAClB,KAAK,IAAI,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,CACpD,CAAA;QAEH,MAAM,WAAW,GAAG,GAAG,UAAU,IAAI,WAAW,IAAI,IAAI,CAAC,GAAG,EAAE,EAAE,CAAA;QAChE,OAAO,MAAM,IAAI,CAAC,UAAU,CAAC,cAAc,CAAC,IAAI,CAAC,MAAM,EAAE,KAAK,EAAE,WAAW,CAAC,CAAA;IAC9E,CAAC;CACF,CAAA;AA7BY,wEAA8B;yCAA9B,8BAA8B;IAD1C,IAAA,yBAAY,EAAC,6CAAuB,CAAC;qCAQT,sBAAa;QACT,2CAAmB;QACd,4BAAe;GATxC,8BAA8B,CA6B1C"}
1
+ {"version":3,"file":"data-sync.new.event.handler.js","sourceRoot":"","sources":["../../src/command-events/data-sync.new.event.handler.ts"],"names":[],"mappings":";;;;;;;;;;;;;AAAA,2CAAuC;AACvC,2CAA8C;AAE9C,8CAA+C;AAC/C,8CAA4C;AAE5C,8EAAwE;AACxE,+DAA+D;AAGxD,IAAM,8BAA8B,sCAApC,MAAM,8BAA8B;IAMzC,YACmB,MAAqB,EACrB,UAA+B,EAC/B,eAAgC;QAFhC,WAAM,GAAN,MAAM,CAAe;QACrB,eAAU,GAAV,UAAU,CAAqB;QAC/B,oBAAe,GAAf,eAAe,CAAiB;QANlC,WAAM,GAAG,IAAI,eAAM,CAAC,gCAA8B,CAAC,IAAI,CAAC,CAAA;QAQvE,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC,GAAG,CAAC,iBAAiB,CAAC,CAAA;IAC7C,CAAC;IAED,KAAK,CAAC,OAAO,CAAC,KAA8B;QAC1C,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,aAAa,EAAE,KAAK,CAAC,CAAA;QACvC,MAAM,UAAU,GAAG,IAAI,CAAC,eAAe,CAAC,aAAa,CAAC,KAAK,CAAC,SAAS,CAAC,CAAA;QACtE,MAAM,OAAO,GAAG,KAAK,CAAC,QAAQ,EAAE,IAAI,CAAA;QACpC,MAAM,WAAW,GAAG,GAAG,OAAO,CAAC,EAAE,EAAE,CAAC,IAAI,IAAI,IAAI,OAAO,CAAC,EAAE,EAAE,CAAC,IAAI,IAAI,EAAE;aACpE,UAAU,CAAC,GAAG,EAAE,GAAG,CAAC;aACpB,OAAO,CAAC,GAAG,EAAE,IAAI,CAAC;aAClB,OAAO,CACN,kBAAkB,EAClB,KAAK,IAAI,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,CACpD,CAAA;QAEH,MAAM,WAAW,GAAG,GAAG,UAAU,IAAI,WAAW,IAAI,IAAI,CAAC,GAAG,EAAE,EAAE,CAAA;QAChE,IAAI,CAAC;YACH,OAAO,MAAM,IAAI,CAAC,UAAU,CAAC,cAAc,CACzC,IAAI,CAAC,MAAM,EACX,KAAK,EACL,WAAW,CACZ,CAAA;QACH,CAAC;QAAC,OAAO,KAAU,EAAE,CAAC;YACpB,2DAA2D;YAC3D,0EAA0E;YAC1E,gEAAgE;YAChE,uEAAuE;YACvE,+DAA+D;YAC/D,uEAAuE;YACvE,oEAAoE;YACpE,mEAAmE;YACnE,0EAA0E;YAC1E,sEAAsE;YACtE,0EAA0E;YAC1E,2DAA2D;YAC3D,MAAM,SAAS,GACb,OAAO,CAAC,GAAG,CAAC,UAAU,KAAK,MAAM,IAAI,OAAO,CAAC,GAAG,CAAC,UAAU,KAAK,GAAG,CAAA;YACrE,IAAI,KAAK,EAAE,IAAI,KAAK,0BAA0B,IAAI,SAAS,EAAE,CAAC;gBAC5D,IAAI,CAAC,MAAM,CAAC,IAAI,CACd,iCAAiC,IAAI,CAAC,MAAM,KAAK;oBAC/C,uFAAuF;oBACvF,2BAA2B,WAAW,EAAE,CAC3C,CAAA;gBACD,OAAM;YACR,CAAC;YACD,MAAM,KAAK,CAAA;QACb,CAAC;IACH,CAAC;CACF,CAAA;AA3DY,wEAA8B;yCAA9B,8BAA8B;IAD1C,IAAA,yBAAY,EAAC,6CAAuB,CAAC;qCAQT,sBAAa;QACT,2CAAmB;QACd,4BAAe;GATxC,8BAA8B,CA2D1C"}
@@ -1,7 +1,7 @@
1
1
  import { ConfigService } from '@nestjs/config';
2
2
  import { DataSyncCommandSfnEvent, StepFunctionStateInput } from '../command-events/data-sync.sfn.event';
3
3
  import { S3Service } from '../data-store';
4
- import { CommandModuleOptions } from '../interfaces';
4
+ import { CommandModel, CommandModuleOptions, DetailKey } from '../interfaces';
5
5
  import { SnsService } from '../queue';
6
6
  import { StepFunctionService } from '../step-func/step-function.service';
7
7
  import { CommandService } from './command.service';
@@ -22,11 +22,36 @@ export declare class CommandEventHandler {
22
22
  execute(event: DataSyncCommandSfnEvent): Promise<StepFunctionStateInput | StepFunctionStateInput[]>;
23
23
  protected handleStepState(event: DataSyncCommandSfnEvent): Promise<StepFunctionStateInput | StepFunctionStateInput[]>;
24
24
  protected waitConfirmToken(event: DataSyncCommandSfnEvent): Promise<StepFunctionStateInput>;
25
+ /**
26
+ * Retry wrapper around commandService.getItem for the cross-execution
27
+ * predecessor-status check in waitConfirmToken. Bounded and short —
28
+ * smooths a single transient DDB failure; does not wait out an outage.
29
+ */
30
+ protected getItemWithRetry(key: DetailKey, options: {
31
+ consistentRead: boolean;
32
+ }, maxAttempts: number, baseDelayMs: number): Promise<CommandModel>;
33
+ /**
34
+ * Bounded retry with exponential backoff (baseDelayMs * 2^(attempt - 1)),
35
+ * rethrowing the last error once every attempt is exhausted. Shared by both
36
+ * sides of the version handshake so the pull path (waitConfirmToken) and the
37
+ * push path (checkNextToken) tolerate transient DynamoDB failures equally.
38
+ */
39
+ protected withRetry<T>(fn: () => Promise<T>, maxAttempts: number, baseDelayMs: number): Promise<T>;
25
40
  protected checkVersion(event: DataSyncCommandSfnEvent): Promise<StepFunctionStateInput>;
26
41
  protected setTtlCommand(event: DataSyncCommandSfnEvent): Promise<StepFunctionStateInput>;
27
42
  protected historyCopy(event: DataSyncCommandSfnEvent): Promise<StepFunctionStateInput>;
28
43
  protected transformData(event: DataSyncCommandSfnEvent): Promise<StepFunctionStateInput[]>;
29
44
  protected syncData(event: DataSyncCommandSfnEvent): Promise<StepFunctionStateInput>;
30
45
  protected checkNextToken(event: DataSyncCommandSfnEvent): Promise<StepFunctionStateInput>;
46
+ protected handleResumeExecutionError(event: DataSyncCommandSfnEvent, e: unknown, options: {
47
+ benignNames: ReadonlySet<string>;
48
+ logContext: string;
49
+ alarmPayload: Record<string, unknown>;
50
+ }): Promise<void>;
51
+ /**
52
+ * Best-effort alarm publish — SNS failure must not fail the SFN step
53
+ * (e.g. after updateTaskToken already succeeded).
54
+ */
55
+ protected publishAlarmSafely(event: DataSyncCommandSfnEvent, errorDetails: any): Promise<void>;
31
56
  protected publishAlarm(event: DataSyncCommandSfnEvent, errorDetails: any): Promise<void>;
32
57
  }
@@ -49,7 +49,7 @@ let CommandEventHandler = CommandEventHandler_1 = class CommandEventHandler {
49
49
  }
50
50
  catch (error) {
51
51
  await this.commandService.updateStatus(event.commandKey, (0, status_enum_1.getCommandStatus)(event.stepStateName, status_enum_1.CommandStatus.STATUS_FAILED), event.commandRecord.requestId);
52
- await this.publishAlarm(event, error.stack);
52
+ await this.publishAlarmSafely(event, error.stack);
53
53
  throw error;
54
54
  }
55
55
  }
@@ -76,12 +76,101 @@ let CommandEventHandler = CommandEventHandler_1 = class CommandEventHandler {
76
76
  async waitConfirmToken(event) {
77
77
  this.logger.debug('waitConfirmToken::', event);
78
78
  await this.commandService.updateTaskToken(event.commandKey, event.taskToken);
79
+ if (event.commandRecord.version > 1) {
80
+ const prevSk = (0, key_1.addSortKeyVersion)((0, key_1.removeSortKeyVersion)(event.commandRecord.sk), event.commandRecord.version - 1);
81
+ let prevCommand;
82
+ let prevReadFailed = false;
83
+ try {
84
+ // consistentRead: true — predecessor status across independent SFN
85
+ // executions must not be a stale eventually-consistent read.
86
+ //
87
+ // Bounded retry (3 attempts, exponential backoff baseDelayMs * 2^(n-1)):
88
+ // best-effort check — updateTaskToken already succeeded; checkNextToken
89
+ // on the predecessor remains the primary resume path.
90
+ prevCommand = await this.getItemWithRetry({ pk: event.commandRecord.pk, sk: prevSk }, { consistentRead: true }, 3, 100);
91
+ }
92
+ catch (e) {
93
+ // After app + SDK retries, treat as persistent degradation of the
94
+ // self-resume backstop — do not fail the step (token already stored).
95
+ prevReadFailed = true;
96
+ this.logger.error(`[${event.commandKey.pk}] Could not read predecessor status for command v${event.commandRecord.version} after retries, self-resume backstop degraded: ` +
97
+ `${e instanceof Error ? e.message : 'Unknown error'}`, e instanceof Error ? e.stack : undefined);
98
+ await this.publishAlarmSafely(event, {
99
+ self_resume_predecessor_read_failed: true,
100
+ cause: e instanceof Error ? e.message : String(e),
101
+ });
102
+ }
103
+ // A successful read that returns no row is not the same as "predecessor
104
+ // has not reached finish yet" — the command chain is append-only, so for
105
+ // version > 1 the predecessor row must exist. Surface it instead of
106
+ // letting it fall through the same silent path as normal waiting.
107
+ if (!prevReadFailed && !prevCommand) {
108
+ this.logger.warn(`[${event.commandKey.pk}] Predecessor command v${event.commandRecord.version - 1} not found (sk: ${prevSk}) — ` +
109
+ `self-resume backstop cannot evaluate predecessor status`);
110
+ }
111
+ // Limitation: self-resume only when predecessor status is finish:STARTED
112
+ // or finish:FINISHED. Any predecessor exit before FINISH (wait_prev_command
113
+ // 24h timeout Pass→Fail with no Lambda/DDB update, version-mismatch fail,
114
+ // or *:FAILED mid-pipeline) leaves a non-finish status (often with a stale
115
+ // taskToken), so this check will not self-resume and this version may wait
116
+ // out its own 24h timeout (cascade).
117
+ const finishStarted = (0, status_enum_1.getCommandStatus)(sfn_name_enum_1.DataSyncCommandSfnName.FINISH, status_enum_1.CommandStatus.STATUS_STARTED);
118
+ const finishFinished = (0, status_enum_1.getCommandStatus)(sfn_name_enum_1.DataSyncCommandSfnName.FINISH, status_enum_1.CommandStatus.STATUS_FINISHED);
119
+ const prevEnteredFinish = prevCommand?.status === finishStarted ||
120
+ prevCommand?.status === finishFinished;
121
+ if (prevEnteredFinish) {
122
+ this.logger.log(`[${event.commandKey.pk}] Prev command already in finish step — self-resuming v${event.commandRecord.version}`);
123
+ try {
124
+ await this.sfnService.resumeExecution(event.taskToken, {
125
+ result: 'resumed_by_prev_version',
126
+ prevVersion: event.commandRecord.version - 1,
127
+ });
128
+ }
129
+ catch (e) {
130
+ await this.handleResumeExecutionError(event, e, {
131
+ benignNames: new Set(['TaskDoesNotExist', 'TaskTimedOut']),
132
+ logContext: `[${event.commandKey.pk}] Self-resume for v${event.commandRecord.version}`,
133
+ alarmPayload: { self_resume_failed: true },
134
+ });
135
+ }
136
+ }
137
+ }
79
138
  return {
80
139
  result: {
81
140
  token: event.taskToken,
82
141
  },
83
142
  };
84
143
  }
144
+ /**
145
+ * Retry wrapper around commandService.getItem for the cross-execution
146
+ * predecessor-status check in waitConfirmToken. Bounded and short —
147
+ * smooths a single transient DDB failure; does not wait out an outage.
148
+ */
149
+ async getItemWithRetry(key, options, maxAttempts, baseDelayMs) {
150
+ return await this.withRetry(() => this.commandService.getItem(key, options), maxAttempts, baseDelayMs);
151
+ }
152
+ /**
153
+ * Bounded retry with exponential backoff (baseDelayMs * 2^(attempt - 1)),
154
+ * rethrowing the last error once every attempt is exhausted. Shared by both
155
+ * sides of the version handshake so the pull path (waitConfirmToken) and the
156
+ * push path (checkNextToken) tolerate transient DynamoDB failures equally.
157
+ */
158
+ async withRetry(fn, maxAttempts, baseDelayMs) {
159
+ let lastError;
160
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
161
+ try {
162
+ return await fn();
163
+ }
164
+ catch (e) {
165
+ lastError = e;
166
+ if (attempt < maxAttempts) {
167
+ const delayMs = baseDelayMs * Math.pow(2, attempt - 1);
168
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
169
+ }
170
+ }
171
+ }
172
+ throw lastError;
173
+ }
85
174
  async checkVersion(event) {
86
175
  this.logger.debug('Checking version::', event.commandRecord);
87
176
  const sk = (0, key_1.removeSortKeyVersion)(event.commandRecord.sk);
@@ -92,10 +181,16 @@ let CommandEventHandler = CommandEventHandler_1 = class CommandEventHandler {
92
181
  this.logger.debug('Checking version for data::', data);
93
182
  const commandVersion = event.commandRecord.version;
94
183
  const nextVersion = 1 + (data?.version || 0);
184
+ // consistentRead: true — the predecessor row is written by an independent
185
+ // SFN execution. A stale eventually-consistent miss makes oldCommand look
186
+ // absent, which returns result: 0 and routes straight to set_ttl_command,
187
+ // skipping wait_prev_command and letting a later version's sync_data land
188
+ // out of order. Matches the predecessor reads in waitConfirmToken and
189
+ // getNextCommand.
95
190
  const oldCommand = await this.commandService.getItem({
96
191
  pk: event.commandRecord.pk,
97
192
  sk: (0, key_1.addSortKeyVersion)(sk, commandVersion - 1),
98
- });
193
+ }, { consistentRead: true });
99
194
  if (nextVersion === commandVersion) {
100
195
  return {
101
196
  result: 0,
@@ -117,7 +212,7 @@ let CommandEventHandler = CommandEventHandler_1 = class CommandEventHandler {
117
212
  error: 'version is not match',
118
213
  cause: 'next version must be ' + nextVersion + ' but got ' + commandVersion,
119
214
  };
120
- await this.publishAlarm(event, errorDetails);
215
+ await this.publishAlarmSafely(event, errorDetails);
121
216
  return errorDetails;
122
217
  }
123
218
  async setTtlCommand(event) {
@@ -142,7 +237,12 @@ let CommandEventHandler = CommandEventHandler_1 = class CommandEventHandler {
142
237
  }
143
238
  async transformData(event) {
144
239
  this.logger.debug('transformData:: ', event.commandRecord);
145
- return this.commandService.dataSyncHandlers.map((cls) => ({
240
+ const handlers = this.commandService.dataSyncHandlers;
241
+ if (handlers.length === 0) {
242
+ this.logger.warn(`[${this.options.tableName}] transformData: no DataSyncHandlers registered — ` +
243
+ `no sync will occur for ${this.options.tableName}`);
244
+ }
245
+ return handlers.map((cls) => ({
146
246
  prevStateName: event.stepStateName,
147
247
  result: cls.constructor.name,
148
248
  }));
@@ -162,7 +262,31 @@ let CommandEventHandler = CommandEventHandler_1 = class CommandEventHandler {
162
262
  }
163
263
  async checkNextToken(event) {
164
264
  this.logger.debug('checkNextToken:: ', event.commandRecord);
165
- const nextCommand = await this.commandService.getNextCommand(event.commandKey);
265
+ let nextCommand;
266
+ try {
267
+ // Same bounded retry as the pull-side predecessor read in
268
+ // waitConfirmToken — without it a transient DDB failure here takes out
269
+ // both halves of the handshake at once.
270
+ nextCommand = await this.withRetry(() => this.commandService.getNextCommand(event.commandKey), 3, 100);
271
+ }
272
+ catch (e) {
273
+ // Deliberately does not rethrow. Failing this step makes execute() write
274
+ // finish:FAILED, and the successor's self-resume backstop only reacts to
275
+ // finish:STARTED|FINISHED — so the push path and the pull path would
276
+ // collapse together and the chain would stall until the 24h timeout.
277
+ // Returning normally lets execute() write finish:FINISHED, keeping the
278
+ // successor's self-resume armed for its own token-store pass. The step is
279
+ // reported as finished even though the push resume never ran; the alarm
280
+ // below is what records that, and the successor is the recovery path.
281
+ this.logger.error(`[${event.commandKey.pk}] Could not read next command after retries, ` +
282
+ `push resume skipped (successor self-resume remains armed): ` +
283
+ `${e instanceof Error ? e.message : 'Unknown error'}`, e instanceof Error ? e.stack : undefined);
284
+ await this.publishAlarmSafely(event, {
285
+ next_command_read_failed: true,
286
+ cause: e instanceof Error ? e.message : String(e),
287
+ });
288
+ return null;
289
+ }
166
290
  if (!nextCommand) {
167
291
  this.logger.debug('No next command version found. Chain ends.');
168
292
  return null;
@@ -176,7 +300,15 @@ let CommandEventHandler = CommandEventHandler_1 = class CommandEventHandler {
176
300
  });
177
301
  }
178
302
  catch (e) {
179
- this.logger.warn(`Could not resume command v${nextCommand.version}: ${e instanceof Error ? e.message : 'Unknown error'}`);
303
+ await this.handleResumeExecutionError(event, e, {
304
+ benignNames: new Set(['TaskDoesNotExist']),
305
+ logContext: `[${event.commandKey.pk}] Resume for v${nextCommand.version} (sk: ${nextCommand.sk})`,
306
+ alarmPayload: {
307
+ push_resume_failed: true,
308
+ nextVersion: nextCommand.version,
309
+ nextSk: nextCommand.sk,
310
+ },
311
+ });
180
312
  }
181
313
  }
182
314
  else {
@@ -184,6 +316,33 @@ let CommandEventHandler = CommandEventHandler_1 = class CommandEventHandler {
184
316
  }
185
317
  return null;
186
318
  }
319
+ async handleResumeExecutionError(event, e, options) {
320
+ const name = e instanceof Error ? e.name : undefined;
321
+ if (name && options.benignNames.has(name)) {
322
+ this.logger.warn(`${options.logContext} already consumed (${name})`);
323
+ return;
324
+ }
325
+ this.logger.error(`${options.logContext} failed unexpectedly (${name ?? 'unknown'}): ` +
326
+ `${e instanceof Error ? e.message : 'Unknown error'}`, e instanceof Error ? e.stack : undefined);
327
+ await this.publishAlarmSafely(event, {
328
+ ...options.alarmPayload,
329
+ errorName: name ?? 'unknown',
330
+ cause: e instanceof Error ? e.message : String(e),
331
+ });
332
+ }
333
+ /**
334
+ * Best-effort alarm publish — SNS failure must not fail the SFN step
335
+ * (e.g. after updateTaskToken already succeeded).
336
+ */
337
+ async publishAlarmSafely(event, errorDetails) {
338
+ try {
339
+ await this.publishAlarm(event, errorDetails);
340
+ }
341
+ catch (alarmError) {
342
+ this.logger.error(`[${event.commandKey.pk}] publishAlarm failed: ` +
343
+ `${alarmError instanceof Error ? alarmError.message : 'Unknown error'}`, alarmError instanceof Error ? alarmError.stack : undefined);
344
+ }
345
+ }
187
346
  async publishAlarm(event, errorDetails) {
188
347
  this.logger.debug('event', event);
189
348
  const alarm = {