@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.
- package/README.md +37 -0
- package/dist/app.module.js +2 -0
- package/dist/app.module.js.map +1 -1
- package/dist/auth/auth.module.d.ts +2 -0
- package/dist/auth/auth.module.js +30 -0
- package/dist/auth/auth.module.js.map +1 -0
- package/dist/auth/authz-bootstrap.service.d.ts +11 -0
- package/dist/auth/authz-bootstrap.service.js +47 -0
- package/dist/auth/authz-bootstrap.service.js.map +1 -0
- package/dist/auth/group-role-resolver.interface.d.ts +13 -0
- package/dist/auth/group-role-resolver.interface.js +3 -0
- package/dist/auth/group-role-resolver.interface.js.map +1 -0
- package/dist/auth/group-role-resolver.registry.d.ts +6 -0
- package/dist/auth/group-role-resolver.registry.js +26 -0
- package/dist/auth/group-role-resolver.registry.js.map +1 -0
- package/dist/auth/index.d.ts +4 -0
- package/dist/auth/index.js +21 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/command-events/data-sync.new.event.handler.js +25 -1
- package/dist/command-events/data-sync.new.event.handler.js.map +1 -1
- package/dist/commands/command.event.handler.d.ts +26 -1
- package/dist/commands/command.event.handler.js +165 -6
- package/dist/commands/command.event.handler.js.map +1 -1
- package/dist/commands/command.service.d.ts +3 -1
- package/dist/commands/command.service.js +40 -12
- package/dist/commands/command.service.js.map +1 -1
- package/dist/context/invoke.d.ts +1 -0
- package/dist/context/invoke.js +1 -1
- package/dist/context/invoke.js.map +1 -1
- package/dist/context/user.d.ts +4 -0
- package/dist/context/user.js +50 -0
- package/dist/context/user.js.map +1 -1
- package/dist/data-store/dynamodb.service.d.ts +3 -1
- package/dist/data-store/dynamodb.service.js +2 -1
- package/dist/data-store/dynamodb.service.js.map +1 -1
- package/dist/decorators/constants.d.ts +2 -0
- package/dist/decorators/constants.js +3 -1
- package/dist/decorators/constants.js.map +1 -1
- package/dist/decorators/data-sync-handler.decorator.d.ts +19 -0
- package/dist/decorators/data-sync-handler.decorator.js +19 -0
- package/dist/decorators/data-sync-handler.decorator.js.map +1 -1
- package/dist/decorators/group-role-resolver.decorator.d.ts +9 -0
- package/dist/decorators/group-role-resolver.decorator.js +17 -0
- package/dist/decorators/group-role-resolver.decorator.js.map +1 -0
- package/dist/decorators/index.d.ts +2 -0
- package/dist/decorators/index.js +2 -0
- package/dist/decorators/index.js.map +1 -1
- package/dist/decorators/notification-transport.decorator.d.ts +6 -0
- package/dist/decorators/notification-transport.decorator.js +14 -0
- package/dist/decorators/notification-transport.decorator.js.map +1 -0
- package/dist/env.validation.d.ts +19 -0
- package/dist/env.validation.js +45 -2
- package/dist/env.validation.js.map +1 -1
- package/dist/guard/roles.guard.d.ts +11 -1
- package/dist/guard/roles.guard.js +44 -8
- package/dist/guard/roles.guard.js.map +1 -1
- package/dist/helpers/serializer.d.ts +1 -2
- package/dist/helpers/serializer.js +34 -7
- package/dist/helpers/serializer.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/interfaces/command-module-options.interface.d.ts +11 -1
- package/dist/notification-env.validation.d.ts +8 -0
- package/dist/notification-env.validation.js +36 -0
- package/dist/notification-env.validation.js.map +1 -0
- package/dist/notifications/appsync-events.service.d.ts +44 -0
- package/dist/notifications/appsync-events.service.js +147 -0
- package/dist/notifications/appsync-events.service.js.map +1 -0
- package/dist/notifications/appsync.service.d.ts +3 -2
- package/dist/notifications/appsync.service.js +20 -12
- package/dist/notifications/appsync.service.js.map +1 -1
- package/dist/notifications/enums/index.d.ts +1 -0
- package/dist/notifications/enums/index.js +18 -0
- package/dist/notifications/enums/index.js.map +1 -0
- package/dist/notifications/enums/notification-transport.enum.d.ts +4 -0
- package/dist/notifications/enums/notification-transport.enum.js +9 -0
- package/dist/notifications/enums/notification-transport.enum.js.map +1 -0
- package/dist/notifications/event/notification.event.handler.d.ts +13 -5
- package/dist/notifications/event/notification.event.handler.js +31 -7
- package/dist/notifications/event/notification.event.handler.js.map +1 -1
- package/dist/notifications/index.d.ts +3 -0
- package/dist/notifications/index.js +3 -0
- package/dist/notifications/index.js.map +1 -1
- package/dist/notifications/interfaces/index.d.ts +1 -0
- package/dist/notifications/interfaces/index.js +18 -0
- package/dist/notifications/interfaces/index.js.map +1 -0
- package/dist/notifications/interfaces/notification-transport.interface.d.ts +7 -0
- package/dist/notifications/interfaces/notification-transport.interface.js +5 -0
- package/dist/notifications/interfaces/notification-transport.interface.js.map +1 -0
- package/dist/notifications/notification.module.js +12 -2
- package/dist/notifications/notification.module.js.map +1 -1
- package/dist/services/explorer.service.d.ts +5 -0
- package/dist/services/explorer.service.js +12 -1
- package/dist/services/explorer.service.js.map +1 -1
- package/dist/services/mocks/group-role-resolver.mock.d.ts +4 -0
- package/dist/services/mocks/group-role-resolver.mock.js +20 -0
- package/dist/services/mocks/group-role-resolver.mock.js.map +1 -0
- package/dist/step-func/step-function.service.js +6 -1
- package/dist/step-func/step-function.service.js.map +1 -1
- 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 |
|
package/dist/app.module.js
CHANGED
|
@@ -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,
|
package/dist/app.module.js.map
CHANGED
|
@@ -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;
|
|
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,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 @@
|
|
|
1
|
+
{"version":3,"file":"group-role-resolver.interface.js","sourceRoot":"","sources":["../../src/auth/group-role-resolver.interface.ts"],"names":[],"mappings":""}
|
|
@@ -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,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
|
-
|
|
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,
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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 = {
|