@openhi/constructs 0.0.111 → 0.0.112

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 (111) hide show
  1. package/lib/chunk-23PUSHBV.mjs +24 -0
  2. package/lib/chunk-23PUSHBV.mjs.map +1 -0
  3. package/lib/{chunk-7FUAMZOF.mjs → chunk-53OHXLIL.mjs} +3 -3
  4. package/lib/chunk-6NBGYGFL.mjs +1803 -0
  5. package/lib/chunk-6NBGYGFL.mjs.map +1 -0
  6. package/lib/chunk-7RZHFI77.mjs +22 -0
  7. package/lib/chunk-7RZHFI77.mjs.map +1 -0
  8. package/lib/{chunk-7Q2IJ2J5.mjs → chunk-CUUKXDB2.mjs} +6 -6
  9. package/lib/chunk-FYHBHHWK.mjs +47 -0
  10. package/lib/chunk-FYHBHHWK.mjs.map +1 -0
  11. package/lib/{chunk-MULKGFIJ.mjs → chunk-GBDIGTNV.mjs} +165 -10
  12. package/lib/chunk-GBDIGTNV.mjs.map +1 -0
  13. package/lib/chunk-HQ67J7BP.mjs +199 -0
  14. package/lib/chunk-HQ67J7BP.mjs.map +1 -0
  15. package/lib/{chunk-AJ3G3THO.mjs → chunk-KO64HPWQ.mjs} +2 -2
  16. package/lib/{chunk-BB5MK4L3.mjs → chunk-KSFC72TT.mjs} +3 -3
  17. package/lib/{chunk-2TPJ6HOF.mjs → chunk-NZRW7ROK.mjs} +72 -54
  18. package/lib/chunk-NZRW7ROK.mjs.map +1 -0
  19. package/lib/chunk-QJDHVMKT.mjs +117 -0
  20. package/lib/chunk-QJDHVMKT.mjs.map +1 -0
  21. package/lib/{chunk-IS4VQRI4.mjs → chunk-QMBJ4VHC.mjs} +12 -47
  22. package/lib/chunk-QMBJ4VHC.mjs.map +1 -0
  23. package/lib/chunk-TRY7JGWO.mjs +16 -0
  24. package/lib/chunk-TRY7JGWO.mjs.map +1 -0
  25. package/lib/chunk-W4KR4CSL.mjs +236 -0
  26. package/lib/chunk-W4KR4CSL.mjs.map +1 -0
  27. package/lib/{chunk-AGF3RAAZ.mjs → chunk-WPCBVDFZ.mjs} +2 -2
  28. package/lib/chunk-WQWFVEVX.mjs +66 -0
  29. package/lib/chunk-WQWFVEVX.mjs.map +1 -0
  30. package/lib/{chunk-SYBADQXI.mjs → chunk-ZM4GDHHC.mjs} +77 -2
  31. package/lib/chunk-ZM4GDHHC.mjs.map +1 -0
  32. package/lib/delete-chunk.handler.d.mts +29 -0
  33. package/lib/delete-chunk.handler.d.ts +29 -0
  34. package/lib/delete-chunk.handler.js +2716 -0
  35. package/lib/delete-chunk.handler.js.map +1 -0
  36. package/lib/delete-chunk.handler.mjs +47 -0
  37. package/lib/delete-chunk.handler.mjs.map +1 -0
  38. package/lib/events-CjS-sm0W.d.mts +107 -0
  39. package/lib/events-CjS-sm0W.d.ts +107 -0
  40. package/lib/events-Da_cFgtc.d.mts +208 -0
  41. package/lib/events-Da_cFgtc.d.ts +208 -0
  42. package/lib/finalize.handler.d.mts +35 -0
  43. package/lib/finalize.handler.d.ts +35 -0
  44. package/lib/finalize.handler.js +875 -0
  45. package/lib/finalize.handler.js.map +1 -0
  46. package/lib/finalize.handler.mjs +166 -0
  47. package/lib/finalize.handler.mjs.map +1 -0
  48. package/lib/index.d.mts +189 -2
  49. package/lib/index.d.ts +500 -3
  50. package/lib/index.js +1753 -174
  51. package/lib/index.js.map +1 -1
  52. package/lib/index.mjs +571 -17
  53. package/lib/index.mjs.map +1 -1
  54. package/lib/list-chunks.handler.d.mts +28 -0
  55. package/lib/list-chunks.handler.d.ts +28 -0
  56. package/lib/list-chunks.handler.js +2746 -0
  57. package/lib/list-chunks.handler.js.map +1 -0
  58. package/lib/list-chunks.handler.mjs +54 -0
  59. package/lib/list-chunks.handler.mjs.map +1 -0
  60. package/lib/platform-deploy-bridge.handler.js +76 -1
  61. package/lib/platform-deploy-bridge.handler.js.map +1 -1
  62. package/lib/platform-deploy-bridge.handler.mjs +1 -1
  63. package/lib/pre-token-generation.handler.js +1106 -155
  64. package/lib/pre-token-generation.handler.js.map +1 -1
  65. package/lib/pre-token-generation.handler.mjs +6 -4
  66. package/lib/pre-token-generation.handler.mjs.map +1 -1
  67. package/lib/provision-default-workspace.handler.js +1529 -142
  68. package/lib/provision-default-workspace.handler.js.map +1 -1
  69. package/lib/provision-default-workspace.handler.mjs +8 -4
  70. package/lib/provision-default-workspace.handler.mjs.map +1 -1
  71. package/lib/rename-finalize.handler.d.mts +30 -0
  72. package/lib/rename-finalize.handler.d.ts +30 -0
  73. package/lib/rename-finalize.handler.js +795 -0
  74. package/lib/rename-finalize.handler.js.map +1 -0
  75. package/lib/rename-finalize.handler.mjs +90 -0
  76. package/lib/rename-finalize.handler.mjs.map +1 -0
  77. package/lib/rename-list-targets.handler.d.mts +26 -0
  78. package/lib/rename-list-targets.handler.d.ts +26 -0
  79. package/lib/rename-list-targets.handler.js +2985 -0
  80. package/lib/rename-list-targets.handler.js.map +1 -0
  81. package/lib/rename-list-targets.handler.mjs +431 -0
  82. package/lib/rename-list-targets.handler.mjs.map +1 -0
  83. package/lib/rename-rewrite-chunk.handler.d.mts +35 -0
  84. package/lib/rename-rewrite-chunk.handler.d.ts +35 -0
  85. package/lib/rename-rewrite-chunk.handler.js +2021 -0
  86. package/lib/rename-rewrite-chunk.handler.js.map +1 -0
  87. package/lib/rename-rewrite-chunk.handler.mjs +27 -0
  88. package/lib/rename-rewrite-chunk.handler.mjs.map +1 -0
  89. package/lib/rest-api-lambda.handler.js +4021 -932
  90. package/lib/rest-api-lambda.handler.js.map +1 -1
  91. package/lib/rest-api-lambda.handler.mjs +1786 -80
  92. package/lib/rest-api-lambda.handler.mjs.map +1 -1
  93. package/lib/seed-demo-data.handler.js +1588 -124
  94. package/lib/seed-demo-data.handler.js.map +1 -1
  95. package/lib/seed-demo-data.handler.mjs +10 -6
  96. package/lib/seed-system-data.handler.js +1179 -155
  97. package/lib/seed-system-data.handler.js.map +1 -1
  98. package/lib/seed-system-data.handler.mjs +5 -4
  99. package/lib/seed-system-data.handler.mjs.map +1 -1
  100. package/package.json +3 -3
  101. package/lib/chunk-2TPJ6HOF.mjs.map +0 -1
  102. package/lib/chunk-IS4VQRI4.mjs.map +0 -1
  103. package/lib/chunk-MULKGFIJ.mjs.map +0 -1
  104. package/lib/chunk-QR5JVSCF.mjs +0 -862
  105. package/lib/chunk-QR5JVSCF.mjs.map +0 -1
  106. package/lib/chunk-SYBADQXI.mjs.map +0 -1
  107. /package/lib/{chunk-7FUAMZOF.mjs.map → chunk-53OHXLIL.mjs.map} +0 -0
  108. /package/lib/{chunk-7Q2IJ2J5.mjs.map → chunk-CUUKXDB2.mjs.map} +0 -0
  109. /package/lib/{chunk-AJ3G3THO.mjs.map → chunk-KO64HPWQ.mjs.map} +0 -0
  110. /package/lib/{chunk-BB5MK4L3.mjs.map → chunk-KSFC72TT.mjs.map} +0 -0
  111. /package/lib/{chunk-AGF3RAAZ.mjs.map → chunk-WPCBVDFZ.mjs.map} +0 -0
package/lib/index.js CHANGED
@@ -194,6 +194,56 @@ var require_registry = __commonJS({
194
194
  }
195
195
  });
196
196
 
197
+ // ../workflows/lib/detail-types/control-plane.js
198
+ var require_control_plane = __commonJS({
199
+ "../workflows/lib/detail-types/control-plane.js"(exports2) {
200
+ "use strict";
201
+ Object.defineProperty(exports2, "__esModule", { value: true });
202
+ exports2.ControlPlaneRenameFailedV1 = exports2.ControlPlaneRenameCompleteV1 = exports2.ControlPlaneRenameV1 = exports2.RENAMABLE_ENTITY_TYPE = exports2.ControlPlaneOwningDeleteFailedV1 = exports2.ControlPlaneOwningDeleteCompleteV1 = exports2.ControlPlaneOwningDeleteV1 = exports2.OWNING_ENTITY_TYPE = void 0;
203
+ var sources_1 = require_sources();
204
+ var registry_1 = require_registry();
205
+ exports2.OWNING_ENTITY_TYPE = {
206
+ Workspace: "Workspace",
207
+ User: "User"
208
+ };
209
+ exports2.ControlPlaneOwningDeleteV1 = (0, registry_1.defineDetailType)({
210
+ detailType: "control-plane.owning-delete.v1",
211
+ source: sources_1.OPENHI_DATA_SOURCE,
212
+ dedupRequired: true
213
+ });
214
+ exports2.ControlPlaneOwningDeleteCompleteV1 = (0, registry_1.defineDetailType)({
215
+ detailType: "control-plane.owning-delete-complete.v1",
216
+ source: sources_1.OPENHI_OPS_SOURCE,
217
+ dedupRequired: true
218
+ });
219
+ exports2.ControlPlaneOwningDeleteFailedV1 = (0, registry_1.defineDetailType)({
220
+ detailType: "control-plane.owning-delete-failed.v1",
221
+ source: sources_1.OPENHI_OPS_SOURCE,
222
+ dedupRequired: true
223
+ });
224
+ exports2.RENAMABLE_ENTITY_TYPE = {
225
+ Tenant: "Tenant",
226
+ User: "User",
227
+ Role: "Role"
228
+ };
229
+ exports2.ControlPlaneRenameV1 = (0, registry_1.defineDetailType)({
230
+ detailType: "control-plane.rename.v1",
231
+ source: sources_1.OPENHI_DATA_SOURCE,
232
+ dedupRequired: true
233
+ });
234
+ exports2.ControlPlaneRenameCompleteV1 = (0, registry_1.defineDetailType)({
235
+ detailType: "control-plane.rename-complete.v1",
236
+ source: sources_1.OPENHI_OPS_SOURCE,
237
+ dedupRequired: true
238
+ });
239
+ exports2.ControlPlaneRenameFailedV1 = (0, registry_1.defineDetailType)({
240
+ detailType: "control-plane.rename-failed.v1",
241
+ source: sources_1.OPENHI_OPS_SOURCE,
242
+ dedupRequired: true
243
+ });
244
+ }
245
+ });
246
+
197
247
  // ../workflows/lib/detail-types/platform.js
198
248
  var require_platform = __commonJS({
199
249
  "../workflows/lib/detail-types/platform.js"(exports2) {
@@ -236,6 +286,7 @@ var require_detail_types = __commonJS({
236
286
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports3, p)) __createBinding(exports3, m, p);
237
287
  };
238
288
  Object.defineProperty(exports2, "__esModule", { value: true });
289
+ __exportStar(require_control_plane(), exports2);
239
290
  __exportStar(require_platform(), exports2);
240
291
  __exportStar(require_registry(), exports2);
241
292
  }
@@ -587,7 +638,7 @@ var require_lib2 = __commonJS({
587
638
  "../workflows/lib/index.js"(exports2) {
588
639
  "use strict";
589
640
  Object.defineProperty(exports2, "__esModule", { value: true });
590
- exports2.workflowDedupClient = exports2.recordIfAbsent = exports2.markFailed = exports2.encodeSortKey = exports2.WorkflowDedupTableNameMissingError = exports2.WorkflowDedupInvalidInputError = exports2.WORKFLOW_DEDUP_TABLE_NAME_ENV_VAR = exports2.WORKFLOW_DEDUP_MAX_CONSUMER_NAME_LENGTH = exports2.WORKFLOW_DEDUP_DEFAULT_TTL_SECONDS = exports2.parseWorkflowEvent = exports2.UnsupportedEnvelopeVersionError = exports2.InvalidWorkflowEventError = exports2.workflowsClient = exports2.publishWorkflowEvent = exports2.WorkflowPublishError = exports2.isWellFormedDetailType = exports2.defineDetailType = exports2.PlatformSystemDataSeededV1 = exports2.PlatformDeploymentCompletedV1 = exports2.InvalidDetailTypeRegistrationError = exports2.OPENHI_OPS_SOURCE = exports2.OPENHI_DATA_SOURCE = exports2.OPENHI_CONTROL_SOURCE = exports2.DEFAULT_BUS_NAME_BY_SOURCE = exports2.workflowUserActorFromClaims = exports2.isWorkflowUserActor = exports2.isWorkflowSystemActor = exports2.MissingActorContextError = exports2.isSupportedEnvelopeVersion = exports2.ENVELOPE_VERSION = void 0;
641
+ exports2.workflowDedupClient = exports2.recordIfAbsent = exports2.markFailed = exports2.encodeSortKey = exports2.WorkflowDedupTableNameMissingError = exports2.WorkflowDedupInvalidInputError = exports2.WORKFLOW_DEDUP_TABLE_NAME_ENV_VAR = exports2.WORKFLOW_DEDUP_MAX_CONSUMER_NAME_LENGTH = exports2.WORKFLOW_DEDUP_DEFAULT_TTL_SECONDS = exports2.parseWorkflowEvent = exports2.UnsupportedEnvelopeVersionError = exports2.InvalidWorkflowEventError = exports2.workflowsClient = exports2.publishWorkflowEvent = exports2.WorkflowPublishError = exports2.isWellFormedDetailType = exports2.defineDetailType = exports2.RENAMABLE_ENTITY_TYPE = exports2.PlatformSystemDataSeededV1 = exports2.PlatformDeploymentCompletedV1 = exports2.OWNING_ENTITY_TYPE = exports2.InvalidDetailTypeRegistrationError = exports2.ControlPlaneRenameV1 = exports2.ControlPlaneRenameFailedV1 = exports2.ControlPlaneRenameCompleteV1 = exports2.ControlPlaneOwningDeleteV1 = exports2.ControlPlaneOwningDeleteFailedV1 = exports2.ControlPlaneOwningDeleteCompleteV1 = exports2.OPENHI_OPS_SOURCE = exports2.OPENHI_DATA_SOURCE = exports2.OPENHI_CONTROL_SOURCE = exports2.DEFAULT_BUS_NAME_BY_SOURCE = exports2.workflowUserActorFromClaims = exports2.isWorkflowUserActor = exports2.isWorkflowSystemActor = exports2.MissingActorContextError = exports2.isSupportedEnvelopeVersion = exports2.ENVELOPE_VERSION = void 0;
591
642
  var envelope_version_1 = require_envelope_version();
592
643
  Object.defineProperty(exports2, "ENVELOPE_VERSION", { enumerable: true, get: function() {
593
644
  return envelope_version_1.ENVELOPE_VERSION;
@@ -622,15 +673,39 @@ var require_lib2 = __commonJS({
622
673
  return sources_1.OPENHI_OPS_SOURCE;
623
674
  } });
624
675
  var detail_types_1 = require_detail_types();
676
+ Object.defineProperty(exports2, "ControlPlaneOwningDeleteCompleteV1", { enumerable: true, get: function() {
677
+ return detail_types_1.ControlPlaneOwningDeleteCompleteV1;
678
+ } });
679
+ Object.defineProperty(exports2, "ControlPlaneOwningDeleteFailedV1", { enumerable: true, get: function() {
680
+ return detail_types_1.ControlPlaneOwningDeleteFailedV1;
681
+ } });
682
+ Object.defineProperty(exports2, "ControlPlaneOwningDeleteV1", { enumerable: true, get: function() {
683
+ return detail_types_1.ControlPlaneOwningDeleteV1;
684
+ } });
685
+ Object.defineProperty(exports2, "ControlPlaneRenameCompleteV1", { enumerable: true, get: function() {
686
+ return detail_types_1.ControlPlaneRenameCompleteV1;
687
+ } });
688
+ Object.defineProperty(exports2, "ControlPlaneRenameFailedV1", { enumerable: true, get: function() {
689
+ return detail_types_1.ControlPlaneRenameFailedV1;
690
+ } });
691
+ Object.defineProperty(exports2, "ControlPlaneRenameV1", { enumerable: true, get: function() {
692
+ return detail_types_1.ControlPlaneRenameV1;
693
+ } });
625
694
  Object.defineProperty(exports2, "InvalidDetailTypeRegistrationError", { enumerable: true, get: function() {
626
695
  return detail_types_1.InvalidDetailTypeRegistrationError;
627
696
  } });
697
+ Object.defineProperty(exports2, "OWNING_ENTITY_TYPE", { enumerable: true, get: function() {
698
+ return detail_types_1.OWNING_ENTITY_TYPE;
699
+ } });
628
700
  Object.defineProperty(exports2, "PlatformDeploymentCompletedV1", { enumerable: true, get: function() {
629
701
  return detail_types_1.PlatformDeploymentCompletedV1;
630
702
  } });
631
703
  Object.defineProperty(exports2, "PlatformSystemDataSeededV1", { enumerable: true, get: function() {
632
704
  return detail_types_1.PlatformSystemDataSeededV1;
633
705
  } });
706
+ Object.defineProperty(exports2, "RENAMABLE_ENTITY_TYPE", { enumerable: true, get: function() {
707
+ return detail_types_1.RENAMABLE_ENTITY_TYPE;
708
+ } });
634
709
  Object.defineProperty(exports2, "defineDetailType", { enumerable: true, get: function() {
635
710
  return detail_types_1.defineDetailType;
636
711
  } });
@@ -702,6 +777,12 @@ __export(src_exports, {
702
777
  CognitoUserPoolDomain: () => CognitoUserPoolDomain,
703
778
  CognitoUserPoolKmsKey: () => CognitoUserPoolKmsKey,
704
779
  ControlEventBus: () => ControlEventBus,
780
+ ControlPlaneOwningDeleteCompleteV1: () => import_workflows5.ControlPlaneOwningDeleteCompleteV1,
781
+ ControlPlaneOwningDeleteFailedV1: () => import_workflows5.ControlPlaneOwningDeleteFailedV1,
782
+ ControlPlaneOwningDeleteV1: () => import_workflows5.ControlPlaneOwningDeleteV1,
783
+ ControlPlaneRenameCompleteV1: () => import_workflows6.ControlPlaneRenameCompleteV1,
784
+ ControlPlaneRenameFailedV1: () => import_workflows6.ControlPlaneRenameFailedV1,
785
+ ControlPlaneRenameV1: () => import_workflows6.ControlPlaneRenameV1,
705
786
  DATA_STORE_CHANGE_DETAIL_MAX_UTF8_BYTES: () => DATA_STORE_CHANGE_DETAIL_MAX_UTF8_BYTES,
706
787
  DATA_STORE_CHANGE_DETAIL_TYPE: () => DATA_STORE_CHANGE_DETAIL_TYPE,
707
788
  DATA_STORE_CHANGE_EVENT_SOURCE: () => DATA_STORE_CHANGE_EVENT_SOURCE,
@@ -721,6 +802,11 @@ __export(src_exports, {
721
802
  OPENHI_TAG_SUFFIX_REPO_NAME: () => OPENHI_TAG_SUFFIX_REPO_NAME,
722
803
  OPENHI_TAG_SUFFIX_SERVICE_TYPE: () => OPENHI_TAG_SUFFIX_SERVICE_TYPE,
723
804
  OPENHI_TAG_SUFFIX_STAGE_TYPE: () => OPENHI_TAG_SUFFIX_STAGE_TYPE,
805
+ OWNING_DELETE_CASCADE_CONSUMER_NAME: () => OWNING_DELETE_CASCADE_CONSUMER_NAME,
806
+ OWNING_DELETE_CASCADE_DEFAULT_CONCURRENCY: () => OWNING_DELETE_CASCADE_DEFAULT_CONCURRENCY,
807
+ OWNING_DELETE_CASCADE_STUCK_THRESHOLD_MINUTES: () => OWNING_DELETE_CASCADE_STUCK_THRESHOLD_MINUTES,
808
+ OWNING_DELETE_OPS_EVENT_BUS_ENV_VAR: () => OWNING_DELETE_OPS_EVENT_BUS_ENV_VAR,
809
+ OWNING_ENTITY_TYPE: () => import_workflows5.OWNING_ENTITY_TYPE,
724
810
  OpenHiApp: () => OpenHiApp,
725
811
  OpenHiAuthService: () => OpenHiAuthService,
726
812
  OpenHiDataService: () => OpenHiDataService,
@@ -731,6 +817,8 @@ __export(src_exports, {
731
817
  OpenHiService: () => OpenHiService,
732
818
  OpenHiStage: () => OpenHiStage,
733
819
  OpsEventBus: () => OpsEventBus,
820
+ OwningDeleteCascadeLambdas: () => OwningDeleteCascadeLambdas,
821
+ OwningDeleteCascadeWorkflow: () => OwningDeleteCascadeWorkflow,
734
822
  PLACEHOLDER_TENANT_ID: () => PLACEHOLDER_TENANT_ID,
735
823
  PLACEHOLDER_WORKSPACE_ID: () => PLACEHOLDER_WORKSPACE_ID,
736
824
  PLATFORM_DEPLOY_BRIDGE_ACTOR_SYSTEM: () => PLATFORM_DEPLOY_BRIDGE_ACTOR_SYSTEM,
@@ -746,7 +834,15 @@ __export(src_exports, {
746
834
  PostConfirmationLambda: () => PostConfirmationLambda,
747
835
  PreTokenGenerationLambda: () => PreTokenGenerationLambda,
748
836
  ProvisionDefaultWorkspaceLambda: () => ProvisionDefaultWorkspaceLambda,
837
+ RENAMABLE_ENTITY_TYPE: () => import_workflows6.RENAMABLE_ENTITY_TYPE,
838
+ RENAME_CASCADE_CONSUMER_NAME: () => RENAME_CASCADE_CONSUMER_NAME,
839
+ RENAME_CASCADE_DEFAULT_CONCURRENCY: () => RENAME_CASCADE_DEFAULT_CONCURRENCY,
840
+ RENAME_CASCADE_FAILED_THRESHOLD: () => RENAME_CASCADE_FAILED_THRESHOLD,
841
+ RENAME_CASCADE_OPS_EVENT_BUS_ENV_VAR: () => RENAME_CASCADE_OPS_EVENT_BUS_ENV_VAR,
842
+ RENAME_CASCADE_SLOW_THRESHOLD_SECONDS: () => RENAME_CASCADE_SLOW_THRESHOLD_SECONDS,
749
843
  REST_API_BASE_URL_SSM_NAME: () => REST_API_BASE_URL_SSM_NAME,
844
+ RenameCascadeLambdas: () => RenameCascadeLambdas,
845
+ RenameCascadeWorkflow: () => RenameCascadeWorkflow,
750
846
  RootGraphqlApi: () => RootGraphqlApi,
751
847
  RootHostedZone: () => RootHostedZone,
752
848
  RootHttpApi: () => RootHttpApi,
@@ -2721,7 +2817,7 @@ var demoDevUserPartitionKeys = (devUsers) => {
2721
2817
  // src/workflows/control-plane/seed-demo-data/seed-demo-data-lambda.ts
2722
2818
  var import_node_fs7 = __toESM(require("fs"));
2723
2819
  var import_node_path7 = __toESM(require("path"));
2724
- var import_types9 = require("@openhi/types");
2820
+ var import_types13 = require("@openhi/types");
2725
2821
  var import_aws_cdk_lib12 = require("aws-cdk-lib");
2726
2822
  var import_aws_events6 = require("aws-cdk-lib/aws-events");
2727
2823
  var import_aws_events_targets2 = require("aws-cdk-lib/aws-events-targets");
@@ -2734,11 +2830,11 @@ var import_constructs11 = require("constructs");
2734
2830
  var import_node_crypto = require("crypto");
2735
2831
  var import_client_cognito_identity_provider = require("@aws-sdk/client-cognito-identity-provider");
2736
2832
  var import_client_dynamodb2 = require("@aws-sdk/client-dynamodb");
2737
- var import_types8 = require("@openhi/types");
2833
+ var import_types12 = require("@openhi/types");
2738
2834
  var import_workflows3 = __toESM(require_lib2());
2739
2835
 
2740
2836
  // src/data/dynamo/dynamo-control-service.ts
2741
- var import_electrodb8 = require("electrodb");
2837
+ var import_electrodb14 = require("electrodb");
2742
2838
 
2743
2839
  // src/data/dynamo/dynamo-client.ts
2744
2840
  var import_client_dynamodb = require("@aws-sdk/client-dynamodb");
@@ -2802,6 +2898,60 @@ var gsi1skAttribute = {
2802
2898
  return label !== void 0 ? `${label}#${id}` : fallback;
2803
2899
  }
2804
2900
  };
2901
+ function extractRoleId(resource) {
2902
+ const flat = resource.roleId;
2903
+ if (typeof flat === "string" && flat.length > 0) return flat;
2904
+ const role = resource.role;
2905
+ if (role && typeof role === "object") {
2906
+ const reference = role.reference;
2907
+ if (typeof reference === "string" && reference.length > 0) {
2908
+ const slash = reference.lastIndexOf("/");
2909
+ const tail = slash >= 0 ? reference.slice(slash + 1) : reference;
2910
+ if (tail.length > 0) return tail;
2911
+ }
2912
+ }
2913
+ return void 0;
2914
+ }
2915
+ var roleAssignmentGsi1skAttribute = {
2916
+ type: "string",
2917
+ watch: ["resource", "denormalizedUserName", "lastUpdated", "id"],
2918
+ set: (_val, item) => {
2919
+ const id = typeof item?.id === "string" ? item.id : "";
2920
+ const lastUpdated = typeof item?.lastUpdated === "string" ? item.lastUpdated : "";
2921
+ const fallback = `${lastUpdated}#${id}`;
2922
+ if (typeof item?.resource !== "string" || item.resource.length === 0) {
2923
+ return fallback;
2924
+ }
2925
+ let parsed;
2926
+ try {
2927
+ parsed = JSON.parse(item.resource);
2928
+ } catch {
2929
+ return fallback;
2930
+ }
2931
+ if (!parsed || typeof parsed !== "object") return fallback;
2932
+ const roleId = extractRoleId(parsed);
2933
+ if (roleId === void 0) return fallback;
2934
+ const denormalizedUserName = typeof item.denormalizedUserName === "string" ? item.denormalizedUserName : "";
2935
+ const normalizedUserName = denormalizedUserName.length > 0 ? (0, import_types2.normalizeLabel)(denormalizedUserName) : "";
2936
+ if (normalizedUserName.length === 0) return fallback;
2937
+ return `${roleId}#${normalizedUserName}#${id}`;
2938
+ }
2939
+ };
2940
+ var membershipGsi1skAttribute = {
2941
+ type: "string",
2942
+ watch: ["denormalizedUserName", "lastUpdated", "id"],
2943
+ set: (_val, item) => {
2944
+ const id = typeof item?.id === "string" ? item.id : "";
2945
+ const lastUpdated = typeof item?.lastUpdated === "string" ? item.lastUpdated : "";
2946
+ const fallback = `${lastUpdated}#${id}`;
2947
+ const denormalizedUserName = typeof item?.denormalizedUserName === "string" ? item.denormalizedUserName : "";
2948
+ const normalizedUserName = denormalizedUserName.length > 0 ? (0, import_types2.normalizeLabel)(denormalizedUserName) : "";
2949
+ if (normalizedUserName.length === 0) {
2950
+ return fallback;
2951
+ }
2952
+ return `${normalizedUserName}#${id}`;
2953
+ }
2954
+ };
2805
2955
 
2806
2956
  // src/data/dynamo/entities/control/configuration-entity.ts
2807
2957
  var ConfigurationEntity = new import_electrodb.Entity({
@@ -2928,218 +3078,241 @@ var ConfigurationEntity = new import_electrodb.Entity({
2928
3078
  }
2929
3079
  });
2930
3080
 
2931
- // src/data/dynamo/entities/control/membership-entity.ts
3081
+ // src/data/dynamo/entities/control/configuration-user-projection-entity.ts
2932
3082
  var import_electrodb2 = require("electrodb");
2933
- var MembershipEntity = new import_electrodb2.Entity({
3083
+ var ConfigurationUserProjectionEntity = new import_electrodb2.Entity({
2934
3084
  model: {
2935
- entity: "membership",
3085
+ entity: "configurationUserProjection",
2936
3086
  service: "control",
2937
3087
  version: "01"
2938
3088
  },
2939
3089
  attributes: {
2940
- /** Sort key sentinel. Always "CURRENT". */
2941
- sk: {
2942
- type: "string",
2943
- required: true,
2944
- default: "CURRENT"
2945
- },
2946
- /** Tenant in which the user has membership (required). */
2947
- tenantId: {
3090
+ /**
3091
+ * User partition discriminator. Renders as `USER#ID#<userId>` on the
3092
+ * base-table PK. Always required — the projection has no meaning
3093
+ * outside a user partition.
3094
+ */
3095
+ userId: {
2948
3096
  type: "string",
2949
3097
  required: true
2950
3098
  },
2951
- /** FHIR Resource.id; membership id. */
2952
- id: {
3099
+ /**
3100
+ * Pre-composed sort key — built by the operations-layer projection
3101
+ * writer via `buildConfigurationUserProjectionSk`. The entity stores
3102
+ * the value verbatim so the SK grammar (pattern #10 user-scope) is
3103
+ * owned by the operations layer, not duplicated here.
3104
+ */
3105
+ sk: {
2953
3106
  type: "string",
2954
3107
  required: true
2955
3108
  },
2956
- /** Full Membership resource serialized as JSON string. */
2957
- resource: {
3109
+ /**
3110
+ * Configuration canonical-record id. Stored as a discriminating
3111
+ * field so consumers can hydrate the canonical row via the
3112
+ * Configuration get-by-id operation when the projection's `summary`
3113
+ * is insufficient.
3114
+ */
3115
+ configurationId: {
2958
3116
  type: "string",
2959
3117
  required: true
2960
3118
  },
2961
3119
  /**
2962
- * Summary projection (key display fields as JSON string: id, displayName, status).
2963
- * Populated on every write via extractSummary(resource); GSI1 INCLUDE surfaces it on lists.
3120
+ * Tenant the Configuration is associated with. The canonical row
3121
+ * keys off `(tenantId, workspaceId, userId, roleId)`; the projection
3122
+ * carries `tenantId` so consumers reconstructing the canonical PK
3123
+ * have the tenant segment without a hop.
2964
3124
  */
2965
- summary: {
3125
+ tenantId: {
2966
3126
  type: "string",
2967
3127
  required: true
2968
3128
  },
2969
- /** Version id (e.g. ULID). */
2970
- vid: {
3129
+ /**
3130
+ * Scope marker. Always `"user"` on this projection — recorded
3131
+ * explicitly so future scope-bearing projections (workspace,
3132
+ * tenant, role) can share filter semantics in a unified
3133
+ * cross-projection list query if one ever lands.
3134
+ */
3135
+ scope: {
2971
3136
  type: "string",
2972
- required: true
3137
+ required: true,
3138
+ default: "user"
2973
3139
  },
2974
- lastUpdated: {
3140
+ /**
3141
+ * Configuration's `key` attribute (config category, e.g. endpoints,
3142
+ * branding, display). Mirrored from the canonical row so consumers
3143
+ * reading the projection get the natural display label without a
3144
+ * BatchGet hop. Doubles as the source of `<normalizedConfigName>` in
3145
+ * the SK.
3146
+ */
3147
+ displayName: {
2975
3148
  type: "string",
2976
- required: true
2977
- },
2978
- gsi1Shard: gsi1ShardAttribute,
2979
- /** Derived GSI1 sort key — name-based when extractable; else `<lastUpdated>#<id>`. */
2980
- gsi1sk: gsi1skAttribute,
2981
- deleted: {
2982
- type: "boolean",
2983
3149
  required: false
2984
3150
  },
2985
- bundleId: {
3151
+ /**
3152
+ * Summary projection (key display fields as JSON string) — mirrored
3153
+ * from the canonical Configuration row so user-partition queries do
3154
+ * not need a BatchGet hop.
3155
+ */
3156
+ summary: {
2986
3157
  type: "string",
2987
- required: false
3158
+ required: true
2988
3159
  },
2989
- msgId: {
3160
+ /** Version id mirrored from the canonical Configuration row. */
3161
+ vid: {
2990
3162
  type: "string",
2991
- required: false
3163
+ required: true
2992
3164
  },
2993
- /**
2994
- * Denormalized `linked-data-identity` Reference (e.g. `Practitioner/abc`).
2995
- * Populated from the FHIR extension on the Membership resource at write
2996
- * time so future GSIs can index data-plane identity lookups without
2997
- * deserializing the full resource JSON. See ADR 2026-03-13-02 §6.
2998
- */
2999
- linkedDataIdentityRef: {
3165
+ /** Last-updated timestamp mirrored from the canonical Configuration row. */
3166
+ lastUpdated: {
3000
3167
  type: "string",
3001
- required: false
3168
+ required: true
3002
3169
  }
3003
3170
  },
3004
3171
  indexes: {
3005
- /** Base table: PK = TID#<tenantId>#MEMBERSHIP#ID#<id>, SK = CURRENT. Do not supply PK or SK from outside. */
3172
+ /**
3173
+ * Base table: PK = USER#ID#<userId>, SK = operation-supplied. A
3174
+ * single `Query(PK = USER#ID#<userId>, SK begins_with
3175
+ * 'CONFIGURATION#')` returns the user's user-scoped Configurations
3176
+ * sorted by `<normalizedConfigName>` (then `<configurationId>` as
3177
+ * the tiebreaker).
3178
+ */
3006
3179
  record: {
3007
3180
  pk: {
3008
3181
  field: "PK",
3009
- composite: ["tenantId", "id"],
3010
- template: "TID#${tenantId}#MEMBERSHIP#ID#${id}"
3182
+ composite: ["userId"],
3183
+ template: "USER#ID#${userId}"
3011
3184
  },
3012
3185
  sk: {
3013
3186
  field: "SK",
3187
+ casing: "none",
3014
3188
  composite: ["sk"],
3015
3189
  template: "${sk}"
3016
3190
  }
3017
- },
3018
- /**
3019
- * GSI1 — Unified Sharded List per ADR-011: list all Memberships for a tenant across the
3020
- * four shards. Membership is tenant-scoped only, so `WID#-` is a sentinel.
3021
- * SK is derived via `gsi1skAttribute` — uses the resource's natural label when
3022
- * extractable, else `<lastUpdated>#<id>` (DR-004). `casing: "none"` preserves the
3023
- * normalized label and ISO-8601 `T`/`Z`.
3024
- */
3025
- gsi1: {
3026
- index: "GSI1",
3027
- pk: {
3028
- field: "GSI1PK",
3029
- composite: ["tenantId", "gsi1Shard"],
3030
- template: "TID#${tenantId}#WID#-#RT#Membership#SHARD#${gsi1Shard}"
3031
- },
3032
- sk: {
3033
- field: "GSI1SK",
3034
- casing: "none",
3035
- composite: ["gsi1sk"],
3036
- template: "${gsi1sk}"
3037
- }
3038
3191
  }
3039
3192
  }
3040
3193
  });
3041
3194
 
3042
- // src/data/dynamo/entities/control/role-entity.ts
3195
+ // src/data/dynamo/entities/control/configuration-workspace-projection-entity.ts
3043
3196
  var import_electrodb3 = require("electrodb");
3044
- var RoleEntity = new import_electrodb3.Entity({
3197
+ var ConfigurationWorkspaceProjectionEntity = new import_electrodb3.Entity({
3045
3198
  model: {
3046
- entity: "role",
3199
+ entity: "configurationWorkspaceProjection",
3047
3200
  service: "control",
3048
3201
  version: "01"
3049
3202
  },
3050
3203
  attributes: {
3051
- /** Sort key sentinel. Always "CURRENT". */
3052
- sk: {
3053
- type: "string",
3054
- required: true,
3055
- default: "CURRENT"
3056
- },
3057
- /** FHIR Resource.id; role id. */
3058
- id: {
3204
+ /**
3205
+ * Tenant the workspace belongs to. Renders as the leading segment
3206
+ * of the base-table PK. Always required — the workspace partition
3207
+ * is tenant-scoped per ADR-011.
3208
+ */
3209
+ tenantId: {
3059
3210
  type: "string",
3060
3211
  required: true
3061
3212
  },
3062
- /** Full Role resource serialized as JSON string. */
3063
- resource: {
3213
+ /**
3214
+ * Workspace partition discriminator. Renders as the trailing
3215
+ * segment of the base-table PK
3216
+ * (`TID#<tenantId>#WORKSPACE#ID#<workspaceId>`). Always required —
3217
+ * the projection has no meaning outside a workspace partition.
3218
+ */
3219
+ workspaceId: {
3064
3220
  type: "string",
3065
3221
  required: true
3066
3222
  },
3067
3223
  /**
3068
- * Summary projection (key display fields as JSON string: id, displayName, status).
3069
- * Populated on every write via extractSummary(resource); GSI1 INCLUDE surfaces it on lists.
3224
+ * Pre-composed sort key built by the operations-layer projection
3225
+ * writer via `buildConfigurationWorkspaceProjectionSk`. The entity
3226
+ * stores the value verbatim so the SK grammar (pattern #10
3227
+ * workspace-scope) is owned by the operations layer, not
3228
+ * duplicated here.
3070
3229
  */
3071
- summary: {
3230
+ sk: {
3072
3231
  type: "string",
3073
3232
  required: true
3074
3233
  },
3075
- /** Version id (e.g. ULID). */
3076
- vid: {
3234
+ /**
3235
+ * Configuration canonical-record id. Stored as a discriminating
3236
+ * field so consumers can hydrate the canonical row via the
3237
+ * Configuration get-by-id operation when the projection's `summary`
3238
+ * is insufficient.
3239
+ */
3240
+ configurationId: {
3077
3241
  type: "string",
3078
3242
  required: true
3079
3243
  },
3080
- lastUpdated: {
3244
+ /**
3245
+ * Scope marker. Always `"workspace"` on this projection — recorded
3246
+ * explicitly so future scope-bearing projections (user, tenant,
3247
+ * role) can share filter semantics in a unified cross-projection
3248
+ * list query if one ever lands.
3249
+ */
3250
+ scope: {
3081
3251
  type: "string",
3082
- required: true
3252
+ required: true,
3253
+ default: "workspace"
3083
3254
  },
3084
- gsi1Shard: gsi1ShardAttribute,
3085
- /** Derived GSI1 sort key name-based when extractable; else `<lastUpdated>#<id>`. */
3086
- gsi1sk: gsi1skAttribute,
3087
- deleted: {
3088
- type: "boolean",
3255
+ /**
3256
+ * Configuration's `key` attribute (config category, e.g. endpoints,
3257
+ * branding, display). Mirrored from the canonical row so consumers
3258
+ * reading the projection get the natural display label without a
3259
+ * BatchGet hop. Doubles as the source of `<normalizedConfigName>`
3260
+ * in the SK.
3261
+ */
3262
+ displayName: {
3263
+ type: "string",
3089
3264
  required: false
3090
3265
  },
3091
- bundleId: {
3266
+ /**
3267
+ * Summary projection (key display fields as JSON string) — mirrored
3268
+ * from the canonical Configuration row so workspace-partition
3269
+ * queries do not need a BatchGet hop.
3270
+ */
3271
+ summary: {
3092
3272
  type: "string",
3093
- required: false
3273
+ required: true
3094
3274
  },
3095
- msgId: {
3275
+ /** Version id mirrored from the canonical Configuration row. */
3276
+ vid: {
3096
3277
  type: "string",
3097
- required: false
3278
+ required: true
3279
+ },
3280
+ /** Last-updated timestamp mirrored from the canonical Configuration row. */
3281
+ lastUpdated: {
3282
+ type: "string",
3283
+ required: true
3098
3284
  }
3099
3285
  },
3100
3286
  indexes: {
3101
- /** Base table: PK = ROLE#ID#<id>, SK = CURRENT. Do not supply PK or SK from outside. */
3287
+ /**
3288
+ * Base table: PK = TID#<tenantId>#WORKSPACE#ID#<workspaceId>,
3289
+ * SK = operation-supplied. A single
3290
+ * `Query(PK = TID#<tenantId>#WORKSPACE#ID#<workspaceId>, SK begins_with 'CONFIGURATION#')`
3291
+ * returns the workspace's workspace-scoped Configurations sorted by
3292
+ * `<normalizedConfigName>` (then `<configurationId>` as the
3293
+ * tiebreaker).
3294
+ */
3102
3295
  record: {
3103
3296
  pk: {
3104
3297
  field: "PK",
3105
- composite: ["id"],
3106
- template: "ROLE#ID#${id}"
3298
+ composite: ["tenantId", "workspaceId"],
3299
+ template: "TID#${tenantId}#WORKSPACE#ID#${workspaceId}"
3107
3300
  },
3108
3301
  sk: {
3109
3302
  field: "SK",
3303
+ casing: "none",
3110
3304
  composite: ["sk"],
3111
3305
  template: "${sk}"
3112
3306
  }
3113
- },
3114
- /**
3115
- * GSI1 — Unified Sharded List per ADR-011: list all Roles across the four shards.
3116
- * Non-tenant-isolated, so `TID#-#WID#-` sentinels precede `RT#Role#SHARD#<n>`.
3117
- * SK is derived via `gsi1skAttribute` — uses the resource's natural label when
3118
- * extractable, else `<lastUpdated>#<id>` (DR-004). `casing: "none"` preserves the
3119
- * normalized label and ISO-8601 `T`/`Z`.
3120
- */
3121
- gsi1: {
3122
- index: "GSI1",
3123
- pk: {
3124
- field: "GSI1PK",
3125
- composite: ["gsi1Shard"],
3126
- template: "TID#-#WID#-#RT#Role#SHARD#${gsi1Shard}"
3127
- },
3128
- sk: {
3129
- field: "GSI1SK",
3130
- casing: "none",
3131
- composite: ["gsi1sk"],
3132
- template: "${gsi1sk}"
3133
- }
3134
3307
  }
3135
3308
  }
3136
3309
  });
3137
3310
 
3138
- // src/data/dynamo/entities/control/roleassignment-entity.ts
3311
+ // src/data/dynamo/entities/control/membership-entity.ts
3139
3312
  var import_electrodb4 = require("electrodb");
3140
- var RoleAssignmentEntity = new import_electrodb4.Entity({
3313
+ var MembershipEntity = new import_electrodb4.Entity({
3141
3314
  model: {
3142
- entity: "roleassignment",
3315
+ entity: "membership",
3143
3316
  service: "control",
3144
3317
  version: "01"
3145
3318
  },
@@ -3150,17 +3323,17 @@ var RoleAssignmentEntity = new import_electrodb4.Entity({
3150
3323
  required: true,
3151
3324
  default: "CURRENT"
3152
3325
  },
3153
- /** Tenant in which the role assignment applies (required). */
3326
+ /** Tenant in which the user has membership (required). */
3154
3327
  tenantId: {
3155
3328
  type: "string",
3156
3329
  required: true
3157
3330
  },
3158
- /** FHIR Resource.id; role assignment id. */
3331
+ /** FHIR Resource.id; membership id. */
3159
3332
  id: {
3160
3333
  type: "string",
3161
3334
  required: true
3162
3335
  },
3163
- /** Full RoleAssignment resource serialized as JSON string. */
3336
+ /** Full Membership resource serialized as JSON string. */
3164
3337
  resource: {
3165
3338
  type: "string",
3166
3339
  required: true
@@ -3183,8 +3356,14 @@ var RoleAssignmentEntity = new import_electrodb4.Entity({
3183
3356
  required: true
3184
3357
  },
3185
3358
  gsi1Shard: gsi1ShardAttribute,
3186
- /** Derived GSI1 sort key — name-based when extractable; else `<lastUpdated>#<id>`. */
3187
- gsi1sk: gsi1skAttribute,
3359
+ /**
3360
+ * Derived GSI1 sort key — `<normalizedUserName>#<id>` per ADR-018
3361
+ * pattern #1 so a GSI1 query partitioned on the tenant range-scans
3362
+ * by user-name prefix and returns memberships sorted by user name.
3363
+ * Falls back to `<lastUpdated>#<id>` when `denormalizedUserName`
3364
+ * is missing.
3365
+ */
3366
+ gsi1sk: membershipGsi1skAttribute,
3188
3367
  deleted: {
3189
3368
  type: "boolean",
3190
3369
  required: false
@@ -3196,49 +3375,863 @@ var RoleAssignmentEntity = new import_electrodb4.Entity({
3196
3375
  msgId: {
3197
3376
  type: "string",
3198
3377
  required: false
3199
- }
3200
- },
3201
- indexes: {
3202
- /** Base table: PK = TID#<tenantId>#ROLEASSIGNMENT#ID#<id>, SK = CURRENT. Do not supply PK or SK from outside. */
3203
- record: {
3204
- pk: {
3205
- field: "PK",
3206
- composite: ["tenantId", "id"],
3207
- template: "TID#${tenantId}#ROLEASSIGNMENT#ID#${id}"
3208
- },
3209
- sk: {
3378
+ },
3379
+ /**
3380
+ * Denormalized `linked-data-identity` Reference (e.g. `Practitioner/abc`).
3381
+ * Populated from the FHIR extension on the Membership resource at write
3382
+ * time so future GSIs can index data-plane identity lookups without
3383
+ * deserializing the full resource JSON. See ADR 2026-03-13-02 §6.
3384
+ */
3385
+ linkedDataIdentityRef: {
3386
+ type: "string",
3387
+ required: false
3388
+ },
3389
+ /**
3390
+ * Denormalized display name of the linked Tenant, captured at row
3391
+ * last-write time. Promoted to a top-level attribute so the ADR-018
3392
+ * adjacency-list projection SKs (pattern #3 — `MEMBERSHIP#TENANT#<normalizedTenantName>#…`)
3393
+ * can be composed from a top-level field instead of digging into the
3394
+ * `resource` JSON. Optional on the schema so pre-TR-024 rows do not
3395
+ * break; the operations-layer multi-write helper (#1010) makes the
3396
+ * field load-bearing at write time per TR-024 rule 2 (write-time
3397
+ * source = canonical Tenant.displayName).
3398
+ * @see TR-024 — Denormalized display-name attributes
3399
+ */
3400
+ denormalizedTenantName: {
3401
+ type: "string",
3402
+ required: false
3403
+ },
3404
+ /**
3405
+ * Denormalized display name of the linked User, captured at row
3406
+ * last-write time. Promoted to a top-level attribute so the ADR-018
3407
+ * adjacency-list canonical-record GSI1SK (pattern #1 —
3408
+ * `<normalizedUserName>#<id>`) and workspace-projection SK (pattern #2)
3409
+ * can be composed from a top-level field. Optional on the schema so
3410
+ * pre-TR-024 rows do not break; the operations-layer multi-write helper
3411
+ * (#1010) makes the field load-bearing at write time per TR-024 rule 2
3412
+ * (write-time source = canonical User.displayName).
3413
+ * @see TR-024 — Denormalized display-name attributes
3414
+ */
3415
+ denormalizedUserName: {
3416
+ type: "string",
3417
+ required: false
3418
+ }
3419
+ },
3420
+ indexes: {
3421
+ /** Base table: PK = TID#<tenantId>#MEMBERSHIP#ID#<id>, SK = CURRENT. Do not supply PK or SK from outside. */
3422
+ record: {
3423
+ pk: {
3424
+ field: "PK",
3425
+ composite: ["tenantId", "id"],
3426
+ template: "TID#${tenantId}#MEMBERSHIP#ID#${id}"
3427
+ },
3428
+ sk: {
3429
+ field: "SK",
3430
+ composite: ["sk"],
3431
+ template: "${sk}"
3432
+ }
3433
+ },
3434
+ /**
3435
+ * GSI1 — Unified Sharded List per ADR-011: list all Memberships for a tenant across the
3436
+ * four shards. Membership is tenant-scoped only, so `WID#-` is a sentinel.
3437
+ * SK is derived via `membershipGsi1skAttribute` — composes
3438
+ * `<normalizedUserName>#<id>` per ADR-018 pattern #1 (users in a
3439
+ * tenant, sorted by user name); falls back to `<lastUpdated>#<id>`
3440
+ * when `denormalizedUserName` is missing. `casing: "none"` preserves
3441
+ * the normalized label and ISO-8601 `T`/`Z`.
3442
+ */
3443
+ gsi1: {
3444
+ index: "GSI1",
3445
+ pk: {
3446
+ field: "GSI1PK",
3447
+ composite: ["tenantId", "gsi1Shard"],
3448
+ template: "TID#${tenantId}#WID#-#RT#Membership#SHARD#${gsi1Shard}"
3449
+ },
3450
+ sk: {
3451
+ field: "GSI1SK",
3452
+ casing: "none",
3453
+ composite: ["gsi1sk"],
3454
+ template: "${gsi1sk}"
3455
+ }
3456
+ }
3457
+ }
3458
+ });
3459
+
3460
+ // src/data/dynamo/entities/control/membership-user-projection-entity.ts
3461
+ var import_electrodb5 = require("electrodb");
3462
+ var MembershipUserProjectionEntity = new import_electrodb5.Entity({
3463
+ model: {
3464
+ entity: "membershipUserProjection",
3465
+ service: "control",
3466
+ version: "01"
3467
+ },
3468
+ attributes: {
3469
+ /**
3470
+ * User partition discriminator. Renders as `USER#ID#<userId>` on the
3471
+ * base-table PK. Always required — the projection has no meaning
3472
+ * outside a user partition.
3473
+ */
3474
+ userId: {
3475
+ type: "string",
3476
+ required: true
3477
+ },
3478
+ /**
3479
+ * Pre-composed sort key — built by the operations-layer projection
3480
+ * writer via `buildMembershipUserProjectionSk*` helpers. The entity
3481
+ * stores the value verbatim so the SK grammar (patterns #3 and #4)
3482
+ * is owned by the operations layer, not duplicated here.
3483
+ */
3484
+ sk: {
3485
+ type: "string",
3486
+ required: true
3487
+ },
3488
+ /** Tenant in which the membership applies. Always required. */
3489
+ tenantId: {
3490
+ type: "string",
3491
+ required: true
3492
+ },
3493
+ /**
3494
+ * Workspace the membership scopes to. Present iff the projection
3495
+ * row is a pattern-#4 workspace sub-lane row; absent for pattern-#3
3496
+ * tenant sub-lane rows.
3497
+ */
3498
+ workspaceId: {
3499
+ type: "string",
3500
+ required: false
3501
+ },
3502
+ /**
3503
+ * Membership canonical-record id. Stored as a discriminating field
3504
+ * so consumers can hydrate the canonical row via
3505
+ * `MembershipEntity.get({ tenantId, id: membershipId })` when the
3506
+ * projection's `summary` is insufficient.
3507
+ */
3508
+ membershipId: {
3509
+ type: "string",
3510
+ required: true
3511
+ },
3512
+ /**
3513
+ * Summary projection (key display fields as JSON string: id,
3514
+ * displayName, status) — mirrored from the canonical Membership row
3515
+ * so user-partition queries do not need a BatchGet hop.
3516
+ */
3517
+ summary: {
3518
+ type: "string",
3519
+ required: true
3520
+ },
3521
+ /** Version id mirrored from the canonical Membership row. */
3522
+ vid: {
3523
+ type: "string",
3524
+ required: true
3525
+ },
3526
+ /** Last-updated timestamp mirrored from the canonical Membership row. */
3527
+ lastUpdated: {
3528
+ type: "string",
3529
+ required: true
3530
+ },
3531
+ /**
3532
+ * Denormalized Tenant display name — required to compose pattern-#3
3533
+ * SK (`MEMBERSHIP#TENANT#<normalizedTenantName>#…`). Optional on the
3534
+ * schema because pre-TR-024 rows may not carry a display name; the
3535
+ * operations layer falls back gracefully when missing.
3536
+ */
3537
+ denormalizedTenantName: {
3538
+ type: "string",
3539
+ required: false
3540
+ },
3541
+ /**
3542
+ * Denormalized User display name — mirrored from the canonical
3543
+ * Membership row per TR-024 rule 3 (canonical-record symmetry).
3544
+ * Carried on the projection so consumers can render the user's
3545
+ * display name without a hop to the User record.
3546
+ */
3547
+ denormalizedUserName: {
3548
+ type: "string",
3549
+ required: false
3550
+ },
3551
+ /**
3552
+ * Denormalized Workspace display name — required to compose
3553
+ * pattern-#4 SK (`MEMBERSHIP#WORKSPACE#TID#<tenantId>#<normalizedWorkspaceName>#…`).
3554
+ * Optional on the schema (TR-024 § Open Item #4 defers a formal
3555
+ * Workspace-rename cascade); the operations layer falls back to a
3556
+ * sentinel when missing so the SK still has a valid shape.
3557
+ */
3558
+ denormalizedWorkspaceName: {
3559
+ type: "string",
3560
+ required: false
3561
+ }
3562
+ },
3563
+ indexes: {
3564
+ /**
3565
+ * Base table: PK = USER#ID#<userId>, SK = operation-supplied.
3566
+ * Both pattern #3 and pattern #4 use this same index — the SK string
3567
+ * encodes the lane discriminator (`MEMBERSHIP#TENANT#…` vs
3568
+ * `MEMBERSHIP#WORKSPACE#…`) so a single `Query(PK = USER#ID#<userId>,
3569
+ * SK begins_with 'MEMBERSHIP#')` returns both lanes interleaved.
3570
+ */
3571
+ record: {
3572
+ pk: {
3573
+ field: "PK",
3574
+ composite: ["userId"],
3575
+ template: "USER#ID#${userId}"
3576
+ },
3577
+ sk: {
3578
+ field: "SK",
3579
+ casing: "none",
3580
+ composite: ["sk"],
3581
+ template: "${sk}"
3582
+ }
3583
+ }
3584
+ }
3585
+ });
3586
+
3587
+ // src/data/dynamo/entities/control/membership-workspace-projection-entity.ts
3588
+ var import_electrodb6 = require("electrodb");
3589
+ var MembershipWorkspaceProjectionEntity = new import_electrodb6.Entity({
3590
+ model: {
3591
+ entity: "membershipWorkspaceProjection",
3592
+ service: "control",
3593
+ version: "01"
3594
+ },
3595
+ attributes: {
3596
+ /**
3597
+ * Tenant the workspace belongs to. Renders as the leading segment
3598
+ * of the base-table PK. Always required — the workspace partition
3599
+ * is tenant-scoped per ADR-011.
3600
+ */
3601
+ tenantId: {
3602
+ type: "string",
3603
+ required: true
3604
+ },
3605
+ /**
3606
+ * Workspace partition discriminator. Renders as the trailing
3607
+ * segment of the base-table PK
3608
+ * (`TID#<tenantId>#WORKSPACE#ID#<workspaceId>`). Always required —
3609
+ * the projection has no meaning outside a workspace partition.
3610
+ */
3611
+ workspaceId: {
3612
+ type: "string",
3613
+ required: true
3614
+ },
3615
+ /**
3616
+ * Pre-composed sort key — built by the operations-layer projection
3617
+ * writer via `buildMembershipWorkspaceProjectionSk`. The entity
3618
+ * stores the value verbatim so the SK grammar (pattern #2) is
3619
+ * owned by the operations layer, not duplicated here.
3620
+ */
3621
+ sk: {
3622
+ type: "string",
3623
+ required: true
3624
+ },
3625
+ /**
3626
+ * User the membership links. Stored as a discriminating field so
3627
+ * consumers can hydrate the canonical User row via
3628
+ * `UserEntity.get({ id: userId, sk: "CURRENT" })` when the
3629
+ * projection's `summary` is insufficient.
3630
+ */
3631
+ userId: {
3632
+ type: "string",
3633
+ required: true
3634
+ },
3635
+ /**
3636
+ * Membership canonical-record id. Stored as a discriminating field
3637
+ * so consumers can hydrate the canonical row via
3638
+ * `MembershipEntity.get({ tenantId, id: membershipId })` when the
3639
+ * projection's `summary` is insufficient.
3640
+ */
3641
+ membershipId: {
3642
+ type: "string",
3643
+ required: true
3644
+ },
3645
+ /**
3646
+ * Summary projection (key display fields as JSON string: id,
3647
+ * displayName, status) — mirrored from the canonical Membership row
3648
+ * so workspace-partition queries do not need a BatchGet hop.
3649
+ */
3650
+ summary: {
3651
+ type: "string",
3652
+ required: true
3653
+ },
3654
+ /** Version id mirrored from the canonical Membership row. */
3655
+ vid: {
3656
+ type: "string",
3657
+ required: true
3658
+ },
3659
+ /** Last-updated timestamp mirrored from the canonical Membership row. */
3660
+ lastUpdated: {
3661
+ type: "string",
3662
+ required: true
3663
+ },
3664
+ /**
3665
+ * Denormalized User display name — required to compose the
3666
+ * pattern-#2 SK (`MEMBERSHIP#<normalizedUserName>#…`). Optional on
3667
+ * the schema because pre-TR-024 rows may not carry a display name;
3668
+ * the operations layer falls back to a sentinel when missing so
3669
+ * the SK still has a valid shape. The TR-023 rename-cascade
3670
+ * pipeline rewrites the SK on a User rename.
3671
+ */
3672
+ denormalizedUserName: {
3673
+ type: "string",
3674
+ required: false
3675
+ }
3676
+ },
3677
+ indexes: {
3678
+ /**
3679
+ * Base table: PK = TID#<tenantId>#WORKSPACE#ID#<workspaceId>,
3680
+ * SK = operation-supplied. Pattern #2 uses this index — the SK
3681
+ * encodes the entity-type prefix (`MEMBERSHIP#…`) so a
3682
+ * `Query(PK = TID#<tenantId>#WORKSPACE#ID#<workspaceId>, SK begins_with 'MEMBERSHIP#')`
3683
+ * returns every member projection for the workspace in normalized-
3684
+ * user-name sort order.
3685
+ */
3686
+ record: {
3687
+ pk: {
3688
+ field: "PK",
3689
+ composite: ["tenantId", "workspaceId"],
3690
+ template: "TID#${tenantId}#WORKSPACE#ID#${workspaceId}"
3691
+ },
3692
+ sk: {
3693
+ field: "SK",
3694
+ casing: "none",
3695
+ composite: ["sk"],
3696
+ template: "${sk}"
3697
+ }
3698
+ }
3699
+ }
3700
+ });
3701
+
3702
+ // src/data/dynamo/entities/control/role-entity.ts
3703
+ var import_electrodb7 = require("electrodb");
3704
+ var RoleEntity = new import_electrodb7.Entity({
3705
+ model: {
3706
+ entity: "role",
3707
+ service: "control",
3708
+ version: "01"
3709
+ },
3710
+ attributes: {
3711
+ /** Sort key sentinel. Always "CURRENT". */
3712
+ sk: {
3713
+ type: "string",
3714
+ required: true,
3715
+ default: "CURRENT"
3716
+ },
3717
+ /** FHIR Resource.id; role id. */
3718
+ id: {
3719
+ type: "string",
3720
+ required: true
3721
+ },
3722
+ /** Full Role resource serialized as JSON string. */
3723
+ resource: {
3724
+ type: "string",
3725
+ required: true
3726
+ },
3727
+ /**
3728
+ * Summary projection (key display fields as JSON string: id, displayName, status).
3729
+ * Populated on every write via extractSummary(resource); GSI1 INCLUDE surfaces it on lists.
3730
+ */
3731
+ summary: {
3732
+ type: "string",
3733
+ required: true
3734
+ },
3735
+ /** Version id (e.g. ULID). */
3736
+ vid: {
3737
+ type: "string",
3738
+ required: true
3739
+ },
3740
+ lastUpdated: {
3741
+ type: "string",
3742
+ required: true
3743
+ },
3744
+ gsi1Shard: gsi1ShardAttribute,
3745
+ /** Derived GSI1 sort key — name-based when extractable; else `<lastUpdated>#<id>`. */
3746
+ gsi1sk: gsi1skAttribute,
3747
+ deleted: {
3748
+ type: "boolean",
3749
+ required: false
3750
+ },
3751
+ bundleId: {
3752
+ type: "string",
3753
+ required: false
3754
+ },
3755
+ msgId: {
3756
+ type: "string",
3757
+ required: false
3758
+ }
3759
+ },
3760
+ indexes: {
3761
+ /** Base table: PK = ROLE#ID#<id>, SK = CURRENT. Do not supply PK or SK from outside. */
3762
+ record: {
3763
+ pk: {
3764
+ field: "PK",
3765
+ composite: ["id"],
3766
+ template: "ROLE#ID#${id}"
3767
+ },
3768
+ sk: {
3769
+ field: "SK",
3770
+ composite: ["sk"],
3771
+ template: "${sk}"
3772
+ }
3773
+ },
3774
+ /**
3775
+ * GSI1 — Unified Sharded List per ADR-011: list all Roles across the four shards.
3776
+ * Non-tenant-isolated, so `TID#-#WID#-` sentinels precede `RT#Role#SHARD#<n>`.
3777
+ * SK is derived via `gsi1skAttribute` — uses the resource's natural label when
3778
+ * extractable, else `<lastUpdated>#<id>` (DR-004). `casing: "none"` preserves the
3779
+ * normalized label and ISO-8601 `T`/`Z`.
3780
+ */
3781
+ gsi1: {
3782
+ index: "GSI1",
3783
+ pk: {
3784
+ field: "GSI1PK",
3785
+ composite: ["gsi1Shard"],
3786
+ template: "TID#-#WID#-#RT#Role#SHARD#${gsi1Shard}"
3787
+ },
3788
+ sk: {
3789
+ field: "GSI1SK",
3790
+ casing: "none",
3791
+ composite: ["gsi1sk"],
3792
+ template: "${gsi1sk}"
3793
+ }
3794
+ }
3795
+ }
3796
+ });
3797
+
3798
+ // src/data/dynamo/entities/control/roleassignment-entity.ts
3799
+ var import_electrodb8 = require("electrodb");
3800
+ var RoleAssignmentEntity = new import_electrodb8.Entity({
3801
+ model: {
3802
+ entity: "roleassignment",
3803
+ service: "control",
3804
+ version: "01"
3805
+ },
3806
+ attributes: {
3807
+ /** Sort key sentinel. Always "CURRENT". */
3808
+ sk: {
3809
+ type: "string",
3810
+ required: true,
3811
+ default: "CURRENT"
3812
+ },
3813
+ /** Tenant in which the role assignment applies (required). */
3814
+ tenantId: {
3815
+ type: "string",
3816
+ required: true
3817
+ },
3818
+ /** FHIR Resource.id; role assignment id. */
3819
+ id: {
3820
+ type: "string",
3821
+ required: true
3822
+ },
3823
+ /** Full RoleAssignment resource serialized as JSON string. */
3824
+ resource: {
3825
+ type: "string",
3826
+ required: true
3827
+ },
3828
+ /**
3829
+ * Summary projection (key display fields as JSON string: id, displayName, status).
3830
+ * Populated on every write via extractSummary(resource); GSI1 INCLUDE surfaces it on lists.
3831
+ */
3832
+ summary: {
3833
+ type: "string",
3834
+ required: true
3835
+ },
3836
+ /** Version id (e.g. ULID). */
3837
+ vid: {
3838
+ type: "string",
3839
+ required: true
3840
+ },
3841
+ lastUpdated: {
3842
+ type: "string",
3843
+ required: true
3844
+ },
3845
+ gsi1Shard: gsi1ShardAttribute,
3846
+ /**
3847
+ * Derived GSI1 sort key — discriminator-first
3848
+ * `<roleId>#<normalizedUserName>#<id>` per ADR-018 pattern #8 so a
3849
+ * GSI1 query partitioned on the tenant can `begins_with('<roleId>#')`
3850
+ * to enumerate every user assigned to a given role, sorted by user
3851
+ * name. Falls back to `<lastUpdated>#<id>` when either component is
3852
+ * missing.
3853
+ */
3854
+ gsi1sk: roleAssignmentGsi1skAttribute,
3855
+ deleted: {
3856
+ type: "boolean",
3857
+ required: false
3858
+ },
3859
+ bundleId: {
3860
+ type: "string",
3861
+ required: false
3862
+ },
3863
+ msgId: {
3864
+ type: "string",
3865
+ required: false
3866
+ },
3867
+ /**
3868
+ * Denormalized display name of the linked Tenant, captured at row
3869
+ * last-write time. Promoted to a top-level attribute so the ADR-018
3870
+ * adjacency-list user-projection SK (pattern #5 —
3871
+ * `ROLEASSIGNMENT#TENANT#<normalizedRoleName>#<roleId>#TID#<tenantId>#<id>`)
3872
+ * can be composed from a top-level field instead of digging into the
3873
+ * `resource` JSON. Optional on the schema so pre-TR-024 rows do not
3874
+ * break; the operations-layer multi-write helper (#1010) makes the
3875
+ * field load-bearing at write time per TR-024 rule 2 (write-time
3876
+ * source = canonical Tenant.displayName).
3877
+ * @see TR-024 — Denormalized display-name attributes
3878
+ */
3879
+ denormalizedTenantName: {
3880
+ type: "string",
3881
+ required: false
3882
+ },
3883
+ /**
3884
+ * Denormalized display name of the linked User, captured at row
3885
+ * last-write time. Promoted to a top-level attribute so the ADR-018
3886
+ * adjacency-list canonical-record GSI1SK (pattern #8 —
3887
+ * `<roleId>#<normalizedUserName>#<id>`) and workspace-projection SK
3888
+ * (pattern #9) can be composed from a top-level field. Optional on
3889
+ * the schema so pre-TR-024 rows do not break; the operations-layer
3890
+ * multi-write helper (#1010) makes the field load-bearing at write
3891
+ * time per TR-024 rule 2 (write-time source = canonical
3892
+ * User.displayName).
3893
+ * @see TR-024 — Denormalized display-name attributes
3894
+ */
3895
+ denormalizedUserName: {
3896
+ type: "string",
3897
+ required: false
3898
+ },
3899
+ /**
3900
+ * Denormalized display name of the linked Role, captured at row
3901
+ * last-write time. Promoted to a top-level attribute so the ADR-018
3902
+ * adjacency-list user-projection SK (pattern #5 —
3903
+ * `ROLEASSIGNMENT#TENANT#<normalizedRoleName>#…`) can be composed from
3904
+ * a top-level field. Optional on the schema so pre-TR-024 rows do not
3905
+ * break; the operations-layer multi-write helper (#1010) makes the
3906
+ * field load-bearing at write time per TR-024 rule 2 (write-time
3907
+ * source = canonical Role.displayName).
3908
+ * @see TR-024 — Denormalized display-name attributes
3909
+ */
3910
+ denormalizedRoleName: {
3911
+ type: "string",
3912
+ required: false
3913
+ }
3914
+ },
3915
+ indexes: {
3916
+ /** Base table: PK = TID#<tenantId>#ROLEASSIGNMENT#ID#<id>, SK = CURRENT. Do not supply PK or SK from outside. */
3917
+ record: {
3918
+ pk: {
3919
+ field: "PK",
3920
+ composite: ["tenantId", "id"],
3921
+ template: "TID#${tenantId}#ROLEASSIGNMENT#ID#${id}"
3922
+ },
3923
+ sk: {
3924
+ field: "SK",
3925
+ composite: ["sk"],
3926
+ template: "${sk}"
3927
+ }
3928
+ },
3929
+ /**
3930
+ * GSI1 — Unified Sharded List per ADR-011: list all RoleAssignments for a tenant across the
3931
+ * four shards. Tenant-scoped only, so `WID#-` is a sentinel.
3932
+ * SK is derived via `roleAssignmentGsi1skAttribute` — composes the
3933
+ * discriminator-first `<roleId>#<normalizedUserName>#<id>` shape per
3934
+ * ADR-018 pattern #8 (users with a specific role in a tenant, sorted
3935
+ * by user name); falls back to `<lastUpdated>#<id>` when either
3936
+ * component is missing. `casing: "none"` preserves the normalized
3937
+ * label and ISO-8601 `T`/`Z`.
3938
+ */
3939
+ gsi1: {
3940
+ index: "GSI1",
3941
+ pk: {
3942
+ field: "GSI1PK",
3943
+ composite: ["tenantId", "gsi1Shard"],
3944
+ template: "TID#${tenantId}#WID#-#RT#RoleAssignment#SHARD#${gsi1Shard}"
3945
+ },
3946
+ sk: {
3947
+ field: "GSI1SK",
3948
+ casing: "none",
3949
+ composite: ["gsi1sk"],
3950
+ template: "${gsi1sk}"
3951
+ }
3952
+ }
3953
+ }
3954
+ });
3955
+
3956
+ // src/data/dynamo/entities/control/roleassignment-user-projection-entity.ts
3957
+ var import_electrodb9 = require("electrodb");
3958
+ var RoleAssignmentUserProjectionEntity = new import_electrodb9.Entity({
3959
+ model: {
3960
+ entity: "roleAssignmentUserProjection",
3961
+ service: "control",
3962
+ version: "01"
3963
+ },
3964
+ attributes: {
3965
+ /**
3966
+ * User partition discriminator. Renders as `USER#ID#<userId>` on the
3967
+ * base-table PK. Always required — the projection has no meaning
3968
+ * outside a user partition.
3969
+ */
3970
+ userId: {
3971
+ type: "string",
3972
+ required: true
3973
+ },
3974
+ /**
3975
+ * Pre-composed sort key — built by the operations-layer projection
3976
+ * writer via `buildRoleAssignmentUserProjectionSk*` helpers. The
3977
+ * entity stores the value verbatim so the SK grammar (tenant-lane
3978
+ * vs workspace-lane) is owned by the operations layer, not
3979
+ * duplicated here.
3980
+ */
3981
+ sk: {
3982
+ type: "string",
3983
+ required: true
3984
+ },
3985
+ /** Tenant in which the role assignment applies. Always required. */
3986
+ tenantId: {
3987
+ type: "string",
3988
+ required: true
3989
+ },
3990
+ /**
3991
+ * Workspace the role assignment scopes to. Present iff the
3992
+ * projection row is the workspace-level sub-lane; absent for
3993
+ * tenant-level sub-lane rows.
3994
+ */
3995
+ workspaceId: {
3996
+ type: "string",
3997
+ required: false
3998
+ },
3999
+ /**
4000
+ * Role the assignment grants. Stored as a discriminating field so
4001
+ * `Query(PK = USER#ID#<userId>, SK begins_with 'ROLEASSIGNMENT#…')`
4002
+ * results carry the role id without a hop to the canonical row.
4003
+ */
4004
+ roleId: {
4005
+ type: "string",
4006
+ required: true
4007
+ },
4008
+ /**
4009
+ * RoleAssignment canonical-record id. Stored as a discriminating
4010
+ * field so consumers can hydrate the canonical row via
4011
+ * `RoleAssignmentEntity.get({ tenantId, id: roleAssignmentId })`
4012
+ * when the projection's `summary` is insufficient.
4013
+ */
4014
+ roleAssignmentId: {
4015
+ type: "string",
4016
+ required: true
4017
+ },
4018
+ /**
4019
+ * Summary projection (key display fields as JSON string: id,
4020
+ * displayName, status) — mirrored from the canonical RoleAssignment
4021
+ * row so user-partition queries do not need a BatchGet hop.
4022
+ */
4023
+ summary: {
4024
+ type: "string",
4025
+ required: true
4026
+ },
4027
+ /** Version id mirrored from the canonical RoleAssignment row. */
4028
+ vid: {
4029
+ type: "string",
4030
+ required: true
4031
+ },
4032
+ /** Last-updated timestamp mirrored from the canonical RoleAssignment row. */
4033
+ lastUpdated: {
4034
+ type: "string",
4035
+ required: true
4036
+ },
4037
+ /**
4038
+ * Denormalized Tenant display name — mirrored from the canonical
4039
+ * RoleAssignment row per TR-024 rule 3 (canonical-record symmetry).
4040
+ * Optional on the schema because pre-TR-024 rows may not carry a
4041
+ * display name; the operations layer falls back gracefully when
4042
+ * missing.
4043
+ */
4044
+ denormalizedTenantName: {
4045
+ type: "string",
4046
+ required: false
4047
+ },
4048
+ /**
4049
+ * Denormalized User display name — mirrored from the canonical
4050
+ * RoleAssignment row per TR-024 rule 3 (canonical-record symmetry).
4051
+ * Carried on the projection so consumers can render the user's
4052
+ * display name without a hop to the User record.
4053
+ */
4054
+ denormalizedUserName: {
4055
+ type: "string",
4056
+ required: false
4057
+ },
4058
+ /**
4059
+ * Denormalized Role display name — required to compose the SK's
4060
+ * `<normalizedRoleName>` segment. Optional on the schema (pre-TR-024
4061
+ * rows fall back to a sentinel) but expected to be present at write
4062
+ * time per TR-024 rule 2 (write-time source =
4063
+ * canonical Role.displayName).
4064
+ */
4065
+ denormalizedRoleName: {
4066
+ type: "string",
4067
+ required: false
4068
+ }
4069
+ },
4070
+ indexes: {
4071
+ /**
4072
+ * Base table: PK = USER#ID#<userId>, SK = operation-supplied. Both
4073
+ * sub-lanes (tenant-level and workspace-level) use this same index —
4074
+ * the SK string encodes the lane discriminator
4075
+ * (`ROLEASSIGNMENT#TENANT#…` vs `ROLEASSIGNMENT#WORKSPACE#…`) so a
4076
+ * single `Query(PK = USER#ID#<userId>, SK begins_with
4077
+ * 'ROLEASSIGNMENT#')` returns both lanes interleaved.
4078
+ */
4079
+ record: {
4080
+ pk: {
4081
+ field: "PK",
4082
+ composite: ["userId"],
4083
+ template: "USER#ID#${userId}"
4084
+ },
4085
+ sk: {
3210
4086
  field: "SK",
4087
+ casing: "none",
3211
4088
  composite: ["sk"],
3212
4089
  template: "${sk}"
3213
4090
  }
4091
+ }
4092
+ }
4093
+ });
4094
+
4095
+ // src/data/dynamo/entities/control/roleassignment-workspace-projection-entity.ts
4096
+ var import_electrodb10 = require("electrodb");
4097
+ var RoleAssignmentWorkspaceProjectionEntity = new import_electrodb10.Entity({
4098
+ model: {
4099
+ entity: "roleAssignmentWorkspaceProjection",
4100
+ service: "control",
4101
+ version: "01"
4102
+ },
4103
+ attributes: {
4104
+ /**
4105
+ * Tenant the workspace belongs to. Renders as the leading segment
4106
+ * of the base-table PK. Always required — the workspace partition
4107
+ * is tenant-scoped per ADR-011.
4108
+ */
4109
+ tenantId: {
4110
+ type: "string",
4111
+ required: true
3214
4112
  },
3215
4113
  /**
3216
- * GSI1 Unified Sharded List per ADR-011: list all RoleAssignments for a tenant across the
3217
- * four shards. Tenant-scoped only, so `WID#-` is a sentinel.
3218
- * SK is derived via `gsi1skAttribute` uses the resource's natural label when
3219
- * extractable, else `<lastUpdated>#<id>` (DR-004). `casing: "none"` preserves the
3220
- * normalized label and ISO-8601 `T`/`Z`.
4114
+ * Workspace partition discriminator. Renders as the trailing
4115
+ * segment of the base-table PK
4116
+ * (`TID#<tenantId>#WORKSPACE#ID#<workspaceId>`). Always required
4117
+ * the projection has no meaning outside a workspace partition.
3221
4118
  */
3222
- gsi1: {
3223
- index: "GSI1",
4119
+ workspaceId: {
4120
+ type: "string",
4121
+ required: true
4122
+ },
4123
+ /**
4124
+ * Pre-composed sort key — built by the operations-layer projection
4125
+ * writer via `buildRoleAssignmentWorkspaceProjectionSk`. The entity
4126
+ * stores the value verbatim so the SK grammar (pattern #9) is
4127
+ * owned by the operations layer, not duplicated here.
4128
+ */
4129
+ sk: {
4130
+ type: "string",
4131
+ required: true
4132
+ },
4133
+ /**
4134
+ * User the role assignment grants the role to. Stored as a
4135
+ * discriminating field so consumers can hydrate the canonical User
4136
+ * row via `UserEntity.get({ id: userId, sk: "CURRENT" })` when the
4137
+ * projection's `summary` is insufficient.
4138
+ */
4139
+ userId: {
4140
+ type: "string",
4141
+ required: true
4142
+ },
4143
+ /**
4144
+ * Role the assignment grants. Stored as a discriminating field —
4145
+ * also rendered into the SK as the discriminator-first segment so
4146
+ * `begins_with('ROLEASSIGNMENT#<roleId>#')` filters one role.
4147
+ */
4148
+ roleId: {
4149
+ type: "string",
4150
+ required: true
4151
+ },
4152
+ /**
4153
+ * RoleAssignment canonical-record id. Stored as a discriminating
4154
+ * field so consumers can hydrate the canonical row via
4155
+ * `RoleAssignmentEntity.get({ tenantId, id: roleAssignmentId })`
4156
+ * when the projection's `summary` is insufficient.
4157
+ */
4158
+ roleAssignmentId: {
4159
+ type: "string",
4160
+ required: true
4161
+ },
4162
+ /**
4163
+ * Summary projection (key display fields as JSON string: id,
4164
+ * displayName, status) — mirrored from the canonical RoleAssignment
4165
+ * row so workspace-partition queries do not need a BatchGet hop.
4166
+ */
4167
+ summary: {
4168
+ type: "string",
4169
+ required: true
4170
+ },
4171
+ /** Version id mirrored from the canonical RoleAssignment row. */
4172
+ vid: {
4173
+ type: "string",
4174
+ required: true
4175
+ },
4176
+ /** Last-updated timestamp mirrored from the canonical RoleAssignment row. */
4177
+ lastUpdated: {
4178
+ type: "string",
4179
+ required: true
4180
+ },
4181
+ /**
4182
+ * Denormalized User display name — required to compose the
4183
+ * pattern-#9 SK (`ROLEASSIGNMENT#<roleId>#<normalizedUserName>#…`).
4184
+ * Optional on the schema because pre-TR-024 rows may not carry a
4185
+ * display name; the operations layer falls back to a sentinel when
4186
+ * missing so the SK still has a valid shape. The TR-023 rename-
4187
+ * cascade pipeline rewrites the SK on a User rename.
4188
+ */
4189
+ denormalizedUserName: {
4190
+ type: "string",
4191
+ required: false
4192
+ },
4193
+ /**
4194
+ * Denormalized Role display name — mirrored from the canonical
4195
+ * RoleAssignment row per TR-024 rule 3 (canonical-record symmetry).
4196
+ * Carried on the projection so consumers can render the role's
4197
+ * display name without a hop to the Role record. Not part of the
4198
+ * SK (pattern #9 sorts on `<normalizedUserName>`, not role name) —
4199
+ * a Role rename does NOT rewrite this SK.
4200
+ */
4201
+ denormalizedRoleName: {
4202
+ type: "string",
4203
+ required: false
4204
+ }
4205
+ },
4206
+ indexes: {
4207
+ /**
4208
+ * Base table: PK = TID#<tenantId>#WORKSPACE#ID#<workspaceId>,
4209
+ * SK = operation-supplied. Pattern #9 uses this index — the SK
4210
+ * encodes the entity-type prefix and discriminator-first roleId
4211
+ * (`ROLEASSIGNMENT#<roleId>#…`) so
4212
+ * `Query(PK = TID#<tenantId>#WORKSPACE#ID#<workspaceId>, SK begins_with 'ROLEASSIGNMENT#<roleId>#')`
4213
+ * returns every user-assignment for that role in the workspace, sorted
4214
+ * by normalized user name.
4215
+ */
4216
+ record: {
3224
4217
  pk: {
3225
- field: "GSI1PK",
3226
- composite: ["tenantId", "gsi1Shard"],
3227
- template: "TID#${tenantId}#WID#-#RT#RoleAssignment#SHARD#${gsi1Shard}"
4218
+ field: "PK",
4219
+ composite: ["tenantId", "workspaceId"],
4220
+ template: "TID#${tenantId}#WORKSPACE#ID#${workspaceId}"
3228
4221
  },
3229
4222
  sk: {
3230
- field: "GSI1SK",
4223
+ field: "SK",
3231
4224
  casing: "none",
3232
- composite: ["gsi1sk"],
3233
- template: "${gsi1sk}"
4225
+ composite: ["sk"],
4226
+ template: "${sk}"
3234
4227
  }
3235
4228
  }
3236
4229
  }
3237
4230
  });
3238
4231
 
3239
4232
  // src/data/dynamo/entities/control/tenant-entity.ts
3240
- var import_electrodb5 = require("electrodb");
3241
- var TenantEntity = new import_electrodb5.Entity({
4233
+ var import_electrodb11 = require("electrodb");
4234
+ var TenantEntity = new import_electrodb11.Entity({
3242
4235
  model: {
3243
4236
  entity: "tenant",
3244
4237
  service: "control",
@@ -3338,8 +4331,8 @@ var TenantEntity = new import_electrodb5.Entity({
3338
4331
  });
3339
4332
 
3340
4333
  // src/data/dynamo/entities/control/user-entity.ts
3341
- var import_electrodb6 = require("electrodb");
3342
- var UserEntity = new import_electrodb6.Entity({
4334
+ var import_electrodb12 = require("electrodb");
4335
+ var UserEntity = new import_electrodb12.Entity({
3343
4336
  model: {
3344
4337
  entity: "user",
3345
4338
  service: "control",
@@ -3394,6 +4387,28 @@ var UserEntity = new import_electrodb6.Entity({
3394
4387
  type: "boolean",
3395
4388
  required: false
3396
4389
  },
4390
+ /**
4391
+ * TR-022 / ADR-018 lifecycle state for the cascade pipeline.
4392
+ *
4393
+ * - `active` (or undefined) — normal, readable state.
4394
+ * - `deleting` — intermediate state set synchronously by the
4395
+ * hard-delete API entry point. The owning-delete cascade state
4396
+ * machine fans out from this transition (DynamoDB stream →
4397
+ * `control-plane.owning-delete.v1` → Step Functions). Readers MUST
4398
+ * short-circuit on `deleting` so partial cascades stay invisible.
4399
+ * - `deleted-failed` — terminal failure state set by the cascade
4400
+ * finalize Lambda when the cascade run fails irrecoverably.
4401
+ * Operators recover by re-running the cascade or by direct
4402
+ * intervention.
4403
+ *
4404
+ * The cascade finalize step deletes the canonical record conditional
4405
+ * on `lifecycleState = "deleting"`; on replay the conditional check
4406
+ * fails and the finalize step treats that as a no-op success.
4407
+ */
4408
+ lifecycleState: {
4409
+ type: ["active", "deleting", "deleted-failed"],
4410
+ required: false
4411
+ },
3397
4412
  bundleId: {
3398
4413
  type: "string",
3399
4414
  required: false
@@ -3463,8 +4478,8 @@ var UserEntity = new import_electrodb6.Entity({
3463
4478
  });
3464
4479
 
3465
4480
  // src/data/dynamo/entities/control/workspace-entity.ts
3466
- var import_electrodb7 = require("electrodb");
3467
- var WorkspaceEntity = new import_electrodb7.Entity({
4481
+ var import_electrodb13 = require("electrodb");
4482
+ var WorkspaceEntity = new import_electrodb13.Entity({
3468
4483
  model: {
3469
4484
  entity: "workspace",
3470
4485
  service: "control",
@@ -3516,6 +4531,28 @@ var WorkspaceEntity = new import_electrodb7.Entity({
3516
4531
  type: "boolean",
3517
4532
  required: false
3518
4533
  },
4534
+ /**
4535
+ * TR-022 / ADR-018 lifecycle state for the cascade pipeline.
4536
+ *
4537
+ * - `active` (or undefined) — normal, readable state.
4538
+ * - `deleting` — intermediate state set synchronously by the
4539
+ * hard-delete API entry point. The owning-delete cascade state
4540
+ * machine fans out from this transition (DynamoDB stream →
4541
+ * `control-plane.owning-delete.v1` → Step Functions). Readers MUST
4542
+ * short-circuit on `deleting` so partial cascades stay invisible.
4543
+ * - `deleted-failed` — terminal failure state set by the cascade
4544
+ * finalize Lambda when the cascade run fails irrecoverably.
4545
+ * Operators recover by re-running the cascade or by direct
4546
+ * intervention.
4547
+ *
4548
+ * The cascade finalize step deletes the canonical record conditional
4549
+ * on `lifecycleState = "deleting"`; on replay the conditional check
4550
+ * fails and the finalize step treats that as a no-op success.
4551
+ */
4552
+ lifecycleState: {
4553
+ type: ["active", "deleting", "deleted-failed"],
4554
+ required: false
4555
+ },
3519
4556
  bundleId: {
3520
4557
  type: "string",
3521
4558
  required: false
@@ -3566,38 +4603,57 @@ var WorkspaceEntity = new import_electrodb7.Entity({
3566
4603
  // src/data/dynamo/dynamo-control-service.ts
3567
4604
  var controlPlaneEntities = {
3568
4605
  configuration: ConfigurationEntity,
4606
+ configurationUserProjection: ConfigurationUserProjectionEntity,
4607
+ configurationWorkspaceProjection: ConfigurationWorkspaceProjectionEntity,
3569
4608
  membership: MembershipEntity,
4609
+ membershipUserProjection: MembershipUserProjectionEntity,
4610
+ membershipWorkspaceProjection: MembershipWorkspaceProjectionEntity,
3570
4611
  role: RoleEntity,
3571
4612
  roleAssignment: RoleAssignmentEntity,
4613
+ roleAssignmentUserProjection: RoleAssignmentUserProjectionEntity,
4614
+ roleAssignmentWorkspaceProjection: RoleAssignmentWorkspaceProjectionEntity,
3572
4615
  tenant: TenantEntity,
3573
4616
  user: UserEntity,
3574
4617
  workspace: WorkspaceEntity
3575
4618
  };
3576
- var controlPlaneService = new import_electrodb8.Service(controlPlaneEntities, {
4619
+ var controlPlaneService = new import_electrodb14.Service(controlPlaneEntities, {
3577
4620
  table: defaultTableName,
3578
4621
  client: dynamoClient
3579
4622
  });
3580
4623
  var DynamoControlService = {
3581
- entities: controlPlaneService.entities
4624
+ entities: controlPlaneService.entities,
4625
+ transaction: controlPlaneService.transaction
3582
4626
  };
3583
4627
 
3584
4628
  // src/data/operations/control/membership/membership-create-operation.ts
4629
+ var import_types5 = require("@openhi/types");
4630
+
4631
+ // src/data/operations/control/membership/membership-user-projection.ts
3585
4632
  var import_types3 = require("@openhi/types");
3586
4633
 
3587
- // src/data/operations/control/roleassignment/roleassignment-create-operation.ts
4634
+ // src/data/operations/control/membership/membership-workspace-projection.ts
3588
4635
  var import_types4 = require("@openhi/types");
3589
4636
 
4637
+ // src/data/operations/control/roleassignment/roleassignment-create-operation.ts
4638
+ var import_types8 = require("@openhi/types");
4639
+
4640
+ // src/data/operations/control/roleassignment/roleassignment-user-projection.ts
4641
+ var import_types6 = require("@openhi/types");
4642
+
4643
+ // src/data/operations/control/roleassignment/roleassignment-workspace-projection.ts
4644
+ var import_types7 = require("@openhi/types");
4645
+
3590
4646
  // src/data/operations/control/tenant/tenant-create-operation.ts
3591
- var import_types5 = require("@openhi/types");
4647
+ var import_types9 = require("@openhi/types");
3592
4648
 
3593
4649
  // src/data/operations/control/workspace/workspace-create-operation.ts
3594
- var import_types7 = require("@openhi/types");
4650
+ var import_types11 = require("@openhi/types");
3595
4651
 
3596
4652
  // src/data/dynamo/dynamo-data-service.ts
3597
- var import_electrodb10 = require("electrodb");
4653
+ var import_electrodb16 = require("electrodb");
3598
4654
 
3599
4655
  // src/data/dynamo/entities/data-entity-common.ts
3600
- var import_electrodb9 = require("electrodb");
4656
+ var import_electrodb15 = require("electrodb");
3601
4657
  var dataEntityAttributes = {
3602
4658
  /** Sort key. "CURRENT" for current version; version history in S3. */
3603
4659
  sk: {
@@ -3693,7 +4749,7 @@ var dataEntityAttributes = {
3693
4749
  }
3694
4750
  };
3695
4751
  function createDataEntity(entity, resourceTypeLabel) {
3696
- return new import_electrodb9.Entity({
4752
+ return new import_electrodb15.Entity({
3697
4753
  model: {
3698
4754
  entity,
3699
4755
  service: "data",
@@ -4607,16 +5663,17 @@ var dataPlaneEntities = {
4607
5663
  visionprescription: VisionPrescriptionEntity,
4608
5664
  verificationresult: VerificationResultEntity
4609
5665
  };
4610
- var dataPlaneService = new import_electrodb10.Service(dataPlaneEntities, {
5666
+ var dataPlaneService = new import_electrodb16.Service(dataPlaneEntities, {
4611
5667
  table: defaultTableName,
4612
5668
  client: dynamoClient
4613
5669
  });
4614
5670
  var DynamoDataService = {
4615
- entities: dataPlaneService.entities
5671
+ entities: dataPlaneService.entities,
5672
+ transaction: dataPlaneService.transaction
4616
5673
  };
4617
5674
 
4618
5675
  // src/data/operations/data-operations-common.ts
4619
- var import_types6 = require("@openhi/types");
5676
+ var import_types10 = require("@openhi/types");
4620
5677
 
4621
5678
  // src/lib/compression.ts
4622
5679
  var import_node_zlib = require("zlib");
@@ -4646,7 +5703,7 @@ var SeedDemoDataLambda = class extends import_constructs11.Construct {
4646
5703
  [SEED_DEMO_DATA_USER_POOL_ID_ENV_VAR]: props.userPool.userPoolId
4647
5704
  }
4648
5705
  });
4649
- const roleReadKeys = Object.values(import_types9.PLATFORM_ROLE_IDS).map(rolePartitionKey);
5706
+ const roleReadKeys = Object.values(import_types13.PLATFORM_ROLE_IDS).map(rolePartitionKey);
4650
5707
  this.lambda.addToRolePolicy(
4651
5708
  new import_aws_iam3.PolicyStatement({
4652
5709
  effect: import_aws_iam3.Effect.ALLOW,
@@ -4735,7 +5792,7 @@ var SEED_SYSTEM_DATA_CONTROL_BUS_ENV_VAR = "CONTROL_EVENT_BUS_NAME";
4735
5792
  // src/workflows/control-plane/seed-system-data/seed-system-data-lambda.ts
4736
5793
  var import_node_fs8 = __toESM(require("fs"));
4737
5794
  var import_node_path8 = __toESM(require("path"));
4738
- var import_types10 = require("@openhi/types");
5795
+ var import_types14 = require("@openhi/types");
4739
5796
  var import_aws_cdk_lib13 = require("aws-cdk-lib");
4740
5797
  var import_aws_events7 = require("aws-cdk-lib/aws-events");
4741
5798
  var import_aws_events_targets3 = require("aws-cdk-lib/aws-events-targets");
@@ -4764,7 +5821,7 @@ var SeedSystemDataLambda = class extends import_constructs13.Construct {
4764
5821
  [SEED_SYSTEM_DATA_CONTROL_BUS_ENV_VAR]: props.controlEventBus.eventBusName
4765
5822
  }
4766
5823
  });
4767
- const roleArns = Object.values(import_types10.PLATFORM_ROLE_IDS).map(
5824
+ const roleArns = Object.values(import_types14.PLATFORM_ROLE_IDS).map(
4768
5825
  (id) => `role#id#${id}`
4769
5826
  );
4770
5827
  this.lambda.addToRolePolicy(
@@ -5715,6 +6772,507 @@ var _OpenHiGraphqlService = class _OpenHiGraphqlService extends OpenHiService {
5715
6772
  };
5716
6773
  _OpenHiGraphqlService.SERVICE_TYPE = "graphql-api";
5717
6774
  var OpenHiGraphqlService = _OpenHiGraphqlService;
6775
+
6776
+ // src/workflows/control-plane/owning-delete-cascade/events.ts
6777
+ var import_workflows5 = __toESM(require_lib2());
6778
+ var OWNING_DELETE_CASCADE_CONSUMER_NAME = "owning-delete-cascade";
6779
+ var OWNING_DELETE_CASCADE_DEFAULT_CONCURRENCY = 8;
6780
+ var OWNING_DELETE_CASCADE_STUCK_THRESHOLD_MINUTES = 15;
6781
+ var OWNING_DELETE_OPS_EVENT_BUS_ENV_VAR = "OWNING_DELETE_OPS_EVENT_BUS_NAME";
6782
+
6783
+ // src/workflows/control-plane/owning-delete-cascade/owning-delete-cascade-lambdas.ts
6784
+ var import_node_fs12 = __toESM(require("fs"));
6785
+ var import_node_path12 = __toESM(require("path"));
6786
+ var import_aws_cdk_lib15 = require("aws-cdk-lib");
6787
+ var import_aws_iam8 = require("aws-cdk-lib/aws-iam");
6788
+ var import_aws_lambda12 = require("aws-cdk-lib/aws-lambda");
6789
+ var import_aws_lambda_nodejs12 = require("aws-cdk-lib/aws-lambda-nodejs");
6790
+ var import_constructs19 = require("constructs");
6791
+ function resolveHandlerEntry12(dirname, handlerName) {
6792
+ const sameDir = import_node_path12.default.join(dirname, handlerName);
6793
+ if (import_node_fs12.default.existsSync(sameDir)) {
6794
+ return { entry: sameDir, handler: "handler" };
6795
+ }
6796
+ const libDir = import_node_path12.default.join(dirname, "..", "..", "..", "..", "lib", handlerName);
6797
+ return { entry: libDir, handler: "handler" };
6798
+ }
6799
+ var OwningDeleteCascadeLambdas = class extends import_constructs19.Construct {
6800
+ constructor(scope, props) {
6801
+ super(scope, "owning-delete-cascade-lambdas");
6802
+ const listResolved = resolveHandlerEntry12(
6803
+ __dirname,
6804
+ "list-chunks.handler.js"
6805
+ );
6806
+ this.listChunks = new import_aws_lambda_nodejs12.NodejsFunction(this, "list-chunks-handler", {
6807
+ entry: listResolved.entry,
6808
+ runtime: import_aws_lambda12.Runtime.NODEJS_LATEST,
6809
+ memorySize: 512,
6810
+ timeout: import_aws_cdk_lib15.Duration.minutes(1),
6811
+ environment: {
6812
+ DYNAMO_TABLE_NAME: props.dataStoreTable.tableName
6813
+ }
6814
+ });
6815
+ props.dataStoreTable.grant(this.listChunks, "dynamodb:Query");
6816
+ const deleteResolved = resolveHandlerEntry12(
6817
+ __dirname,
6818
+ "delete-chunk.handler.js"
6819
+ );
6820
+ this.deleteChunk = new import_aws_lambda_nodejs12.NodejsFunction(this, "delete-chunk-handler", {
6821
+ entry: deleteResolved.entry,
6822
+ runtime: import_aws_lambda12.Runtime.NODEJS_LATEST,
6823
+ memorySize: 512,
6824
+ timeout: import_aws_cdk_lib15.Duration.minutes(1),
6825
+ environment: {
6826
+ DYNAMO_TABLE_NAME: props.dataStoreTable.tableName
6827
+ }
6828
+ });
6829
+ props.dataStoreTable.grant(
6830
+ this.deleteChunk,
6831
+ "dynamodb:DeleteItem",
6832
+ "dynamodb:UpdateItem",
6833
+ "dynamodb:PutItem"
6834
+ );
6835
+ const finalizeResolved = resolveHandlerEntry12(
6836
+ __dirname,
6837
+ "finalize.handler.js"
6838
+ );
6839
+ this.finalize = new import_aws_lambda_nodejs12.NodejsFunction(this, "finalize-handler", {
6840
+ entry: finalizeResolved.entry,
6841
+ runtime: import_aws_lambda12.Runtime.NODEJS_LATEST,
6842
+ memorySize: 512,
6843
+ timeout: import_aws_cdk_lib15.Duration.minutes(1),
6844
+ environment: {
6845
+ DYNAMO_TABLE_NAME: props.dataStoreTable.tableName,
6846
+ [OWNING_DELETE_OPS_EVENT_BUS_ENV_VAR]: props.opsEventBus.eventBusName
6847
+ }
6848
+ });
6849
+ props.dataStoreTable.grant(this.finalize, "dynamodb:DeleteItem");
6850
+ this.finalize.addToRolePolicy(
6851
+ new import_aws_iam8.PolicyStatement({
6852
+ effect: import_aws_iam8.Effect.ALLOW,
6853
+ actions: ["events:PutEvents"],
6854
+ resources: [props.opsEventBus.eventBusArn]
6855
+ })
6856
+ );
6857
+ }
6858
+ };
6859
+
6860
+ // src/workflows/control-plane/owning-delete-cascade/owning-delete-cascade-workflow.ts
6861
+ var import_aws_cdk_lib16 = require("aws-cdk-lib");
6862
+ var import_aws_events9 = require("aws-cdk-lib/aws-events");
6863
+ var import_aws_events_targets5 = require("aws-cdk-lib/aws-events-targets");
6864
+ var import_aws_stepfunctions = require("aws-cdk-lib/aws-stepfunctions");
6865
+ var import_aws_stepfunctions_tasks = require("aws-cdk-lib/aws-stepfunctions-tasks");
6866
+ var import_constructs20 = require("constructs");
6867
+ var OwningDeleteCascadeWorkflow = class extends import_constructs20.Construct {
6868
+ constructor(scope, props) {
6869
+ super(scope, "owning-delete-cascade-workflow");
6870
+ this.lambdas = new OwningDeleteCascadeLambdas(this, {
6871
+ dataStoreTable: props.dataStoreTable,
6872
+ opsEventBus: props.opsEventBus
6873
+ });
6874
+ const concurrency = props.cascadeMapConcurrency ?? OWNING_DELETE_CASCADE_DEFAULT_CONCURRENCY;
6875
+ const initState = new import_aws_stepfunctions.Pass(this, "init-state", {
6876
+ parameters: {
6877
+ "ownerType.$": "$.detail.payload.ownerType",
6878
+ "ownerId.$": "$.detail.payload.ownerId",
6879
+ "tenantId.$": "$.detail.payload.tenantId",
6880
+ cursors: {},
6881
+ projectionsRemoved: 0,
6882
+ chunkCount: 0,
6883
+ // Used by the finalize step to compute `durationMs`.
6884
+ "startedAt.$": "$$.Execution.StartTime",
6885
+ // Propagate envelope identity for ADR-016 causation chaining.
6886
+ "eventId.$": "$.detail.eventId",
6887
+ "correlationId.$": "$.detail.correlationId",
6888
+ "causationId.$": "$.detail.eventId"
6889
+ }
6890
+ });
6891
+ const listChunks = new import_aws_stepfunctions_tasks.LambdaInvoke(this, "list-chunks", {
6892
+ lambdaFunction: this.lambdas.listChunks,
6893
+ resultPath: "$.listResult",
6894
+ retryOnServiceExceptions: true
6895
+ });
6896
+ const updateAfterList = new import_aws_stepfunctions.Pass(this, "update-after-list", {
6897
+ parameters: {
6898
+ "ownerType.$": "$.ownerType",
6899
+ "ownerId.$": "$.ownerId",
6900
+ "tenantId.$": "$.tenantId",
6901
+ "cursors.$": "$.listResult.Payload.cursors",
6902
+ "projectionsRemoved.$": "$.listResult.Payload.projectionsRemoved",
6903
+ "chunkCount.$": "$.listResult.Payload.chunkCount",
6904
+ "exhausted.$": "$.listResult.Payload.exhausted",
6905
+ "chunks.$": "$.listResult.Payload.chunks",
6906
+ "startedAt.$": "$.startedAt",
6907
+ "eventId.$": "$.eventId",
6908
+ "correlationId.$": "$.correlationId",
6909
+ "causationId.$": "$.causationId"
6910
+ }
6911
+ });
6912
+ const rewriteChunks = new import_aws_stepfunctions.CustomState(this, "rewrite-chunks", {
6913
+ stateJson: {
6914
+ Type: "Map",
6915
+ ItemsPath: "$.chunks",
6916
+ MaxConcurrency: concurrency,
6917
+ ResultPath: "$.rewriteResults",
6918
+ ItemSelector: {
6919
+ // The handler receives `CascadeChunkInput` verbatim from the
6920
+ // `listChunks` output, no additional wrapping.
6921
+ "ownerType.$": "$$.Map.Item.Value.ownerType",
6922
+ "ownerId.$": "$$.Map.Item.Value.ownerId",
6923
+ "tenantId.$": "$$.Map.Item.Value.tenantId",
6924
+ "rows.$": "$$.Map.Item.Value.rows",
6925
+ "chunkToken.$": "$$.Map.Item.Value.chunkToken"
6926
+ },
6927
+ ItemProcessor: {
6928
+ ProcessorConfig: {
6929
+ // Inline mode (NOT Distributed). Per TR-022 Choice 2A.
6930
+ Mode: "INLINE"
6931
+ },
6932
+ StartAt: "DeleteChunk",
6933
+ States: {
6934
+ DeleteChunk: {
6935
+ Type: "Task",
6936
+ Resource: "arn:aws:states:::lambda:invoke",
6937
+ Parameters: {
6938
+ FunctionName: this.lambdas.deleteChunk.functionArn,
6939
+ "Payload.$": "$"
6940
+ },
6941
+ Retry: [
6942
+ {
6943
+ ErrorEquals: [
6944
+ "DynamoDB.ProvisionedThroughputExceededException",
6945
+ "DynamoDB.ThrottlingException",
6946
+ "DynamoDB.TransactionConflictException",
6947
+ "Lambda.ServiceException",
6948
+ "Lambda.AWSLambdaException",
6949
+ "Lambda.SdkClientException"
6950
+ ],
6951
+ IntervalSeconds: 1,
6952
+ MaxAttempts: 3,
6953
+ BackoffRate: 2,
6954
+ MaxDelaySeconds: 30
6955
+ }
6956
+ ],
6957
+ Catch: [
6958
+ {
6959
+ // Replay path: the rows in this chunk are already
6960
+ // gone. Treat as a no-op success so the outer loop
6961
+ // keeps draining the partition.
6962
+ ErrorEquals: ["DynamoDB.TransactionCanceledException"],
6963
+ Next: "ChunkAlreadyDeleted"
6964
+ }
6965
+ ],
6966
+ End: true
6967
+ },
6968
+ ChunkAlreadyDeleted: {
6969
+ Type: "Succeed"
6970
+ }
6971
+ }
6972
+ }
6973
+ }
6974
+ });
6975
+ const interPageWait = new import_aws_stepfunctions.Wait(this, "inter-page-wait", {
6976
+ time: import_aws_stepfunctions.WaitTime.duration(import_aws_cdk_lib16.Duration.seconds(0))
6977
+ });
6978
+ const isExhausted = new import_aws_stepfunctions.Choice(this, "is-exhausted");
6979
+ const finalize = new import_aws_stepfunctions_tasks.LambdaInvoke(this, "finalize", {
6980
+ lambdaFunction: this.lambdas.finalize,
6981
+ payload: import_aws_stepfunctions.TaskInput.fromObject({
6982
+ "ownerType.$": "$.ownerType",
6983
+ "ownerId.$": "$.ownerId",
6984
+ "tenantId.$": "$.tenantId",
6985
+ "projectionsRemoved.$": "$.projectionsRemoved",
6986
+ "chunkCount.$": "$.chunkCount",
6987
+ "startedAt.$": "$.startedAt",
6988
+ "eventId.$": "$.eventId",
6989
+ "correlationId.$": "$.correlationId",
6990
+ "causationId.$": "$.causationId"
6991
+ }),
6992
+ resultPath: "$.finalizeResult",
6993
+ retryOnServiceExceptions: true
6994
+ });
6995
+ const success = new import_aws_stepfunctions.Succeed(this, "success");
6996
+ const definition = initState.next(listChunks).next(updateAfterList).next(rewriteChunks).next(interPageWait).next(
6997
+ isExhausted.when(
6998
+ import_aws_stepfunctions.Condition.booleanEquals("$.exhausted", true),
6999
+ finalize.next(success)
7000
+ ).otherwise(listChunks)
7001
+ );
7002
+ this.stateMachine = new import_aws_stepfunctions.StateMachine(this, "state-machine", {
7003
+ definitionBody: import_aws_stepfunctions.DefinitionBody.fromChainable(definition),
7004
+ // Long timeout because real-world cascades can run minutes when
7005
+ // a workspace has thousands of members. The stuck-cascade alarm
7006
+ // fires at 15 minutes; the state machine itself does not abort.
7007
+ timeout: import_aws_cdk_lib16.Duration.hours(2)
7008
+ });
7009
+ this.rule = new import_aws_events9.Rule(this, "rule", {
7010
+ eventBus: props.dataEventBus,
7011
+ eventPattern: {
7012
+ source: [import_workflows5.OPENHI_DATA_SOURCE],
7013
+ detailType: [import_workflows5.ControlPlaneOwningDeleteV1.detailType]
7014
+ },
7015
+ targets: [
7016
+ new import_aws_events_targets5.SfnStateMachine(this.stateMachine, {
7017
+ retryAttempts: 2,
7018
+ maxEventAge: import_aws_cdk_lib16.Duration.hours(2)
7019
+ })
7020
+ ]
7021
+ });
7022
+ }
7023
+ };
7024
+
7025
+ // src/workflows/control-plane/rename-cascade/events.ts
7026
+ var import_workflows6 = __toESM(require_lib2());
7027
+ var RENAME_CASCADE_CONSUMER_NAME = "rename-cascade";
7028
+ var RENAME_CASCADE_DEFAULT_CONCURRENCY = 10;
7029
+ var RENAME_CASCADE_FAILED_THRESHOLD = 0;
7030
+ var RENAME_CASCADE_SLOW_THRESHOLD_SECONDS = 300;
7031
+ var RENAME_CASCADE_OPS_EVENT_BUS_ENV_VAR = "RENAME_CASCADE_OPS_EVENT_BUS_NAME";
7032
+
7033
+ // src/workflows/control-plane/rename-cascade/rename-cascade-lambdas.ts
7034
+ var import_node_fs13 = __toESM(require("fs"));
7035
+ var import_node_path13 = __toESM(require("path"));
7036
+ var import_aws_cdk_lib17 = require("aws-cdk-lib");
7037
+ var import_aws_iam9 = require("aws-cdk-lib/aws-iam");
7038
+ var import_aws_lambda13 = require("aws-cdk-lib/aws-lambda");
7039
+ var import_aws_lambda_nodejs13 = require("aws-cdk-lib/aws-lambda-nodejs");
7040
+ var import_constructs21 = require("constructs");
7041
+ function resolveHandlerEntry13(dirname, handlerName) {
7042
+ const sameDir = import_node_path13.default.join(dirname, handlerName);
7043
+ if (import_node_fs13.default.existsSync(sameDir)) {
7044
+ return { entry: sameDir, handler: "handler" };
7045
+ }
7046
+ const libDir = import_node_path13.default.join(dirname, "..", "..", "..", "..", "lib", handlerName);
7047
+ return { entry: libDir, handler: "handler" };
7048
+ }
7049
+ var RenameCascadeLambdas = class extends import_constructs21.Construct {
7050
+ constructor(scope, props) {
7051
+ super(scope, "rename-cascade-lambdas");
7052
+ const listResolved = resolveHandlerEntry13(
7053
+ __dirname,
7054
+ "rename-list-targets.handler.js"
7055
+ );
7056
+ this.listTargets = new import_aws_lambda_nodejs13.NodejsFunction(this, "list-targets-handler", {
7057
+ entry: listResolved.entry,
7058
+ runtime: import_aws_lambda13.Runtime.NODEJS_LATEST,
7059
+ memorySize: 512,
7060
+ timeout: import_aws_cdk_lib17.Duration.minutes(1),
7061
+ environment: {
7062
+ DYNAMO_TABLE_NAME: props.dataStoreTable.tableName
7063
+ }
7064
+ });
7065
+ props.dataStoreTable.grant(this.listTargets, "dynamodb:Query");
7066
+ const rewriteResolved = resolveHandlerEntry13(
7067
+ __dirname,
7068
+ "rename-rewrite-chunk.handler.js"
7069
+ );
7070
+ this.rewriteChunk = new import_aws_lambda_nodejs13.NodejsFunction(this, "rewrite-chunk-handler", {
7071
+ entry: rewriteResolved.entry,
7072
+ runtime: import_aws_lambda13.Runtime.NODEJS_LATEST,
7073
+ memorySize: 512,
7074
+ timeout: import_aws_cdk_lib17.Duration.minutes(1),
7075
+ environment: {
7076
+ DYNAMO_TABLE_NAME: props.dataStoreTable.tableName
7077
+ }
7078
+ });
7079
+ props.dataStoreTable.grant(
7080
+ this.rewriteChunk,
7081
+ "dynamodb:DeleteItem",
7082
+ "dynamodb:PutItem",
7083
+ "dynamodb:UpdateItem"
7084
+ );
7085
+ const finalizeResolved = resolveHandlerEntry13(
7086
+ __dirname,
7087
+ "rename-finalize.handler.js"
7088
+ );
7089
+ this.finalize = new import_aws_lambda_nodejs13.NodejsFunction(this, "finalize-handler", {
7090
+ entry: finalizeResolved.entry,
7091
+ runtime: import_aws_lambda13.Runtime.NODEJS_LATEST,
7092
+ memorySize: 512,
7093
+ timeout: import_aws_cdk_lib17.Duration.minutes(1),
7094
+ environment: {
7095
+ [RENAME_CASCADE_OPS_EVENT_BUS_ENV_VAR]: props.opsEventBus.eventBusName
7096
+ }
7097
+ });
7098
+ this.finalize.addToRolePolicy(
7099
+ new import_aws_iam9.PolicyStatement({
7100
+ effect: import_aws_iam9.Effect.ALLOW,
7101
+ actions: ["events:PutEvents"],
7102
+ resources: [props.opsEventBus.eventBusArn]
7103
+ })
7104
+ );
7105
+ }
7106
+ };
7107
+
7108
+ // src/workflows/control-plane/rename-cascade/rename-cascade-workflow.ts
7109
+ var import_aws_cdk_lib18 = require("aws-cdk-lib");
7110
+ var import_aws_events10 = require("aws-cdk-lib/aws-events");
7111
+ var import_aws_events_targets6 = require("aws-cdk-lib/aws-events-targets");
7112
+ var import_aws_stepfunctions2 = require("aws-cdk-lib/aws-stepfunctions");
7113
+ var import_aws_stepfunctions_tasks2 = require("aws-cdk-lib/aws-stepfunctions-tasks");
7114
+ var import_constructs22 = require("constructs");
7115
+ var RenameCascadeWorkflow = class extends import_constructs22.Construct {
7116
+ constructor(scope, props) {
7117
+ super(scope, "rename-cascade-workflow");
7118
+ this.lambdas = new RenameCascadeLambdas(this, {
7119
+ dataStoreTable: props.dataStoreTable,
7120
+ opsEventBus: props.opsEventBus
7121
+ });
7122
+ const concurrency = props.cascadeMapConcurrency ?? RENAME_CASCADE_DEFAULT_CONCURRENCY;
7123
+ const initState = new import_aws_stepfunctions2.Pass(this, "init-state", {
7124
+ parameters: {
7125
+ "entityType.$": "$.detail.payload.entityType",
7126
+ "entityId.$": "$.detail.payload.entityId",
7127
+ "tenantId.$": "$.detail.payload.tenantId",
7128
+ "oldName.$": "$.detail.payload.oldName",
7129
+ "newName.$": "$.detail.payload.newName",
7130
+ "oldNormalizedName.$": "$.detail.payload.oldNormalizedName",
7131
+ "newNormalizedName.$": "$.detail.payload.newNormalizedName",
7132
+ cursors: {},
7133
+ itemsRewritten: 0,
7134
+ chunkCount: 0,
7135
+ "startedAt.$": "$$.Execution.StartTime",
7136
+ "eventId.$": "$.detail.eventId",
7137
+ "correlationId.$": "$.detail.correlationId",
7138
+ "causationId.$": "$.detail.eventId"
7139
+ }
7140
+ });
7141
+ const listTargets = new import_aws_stepfunctions_tasks2.LambdaInvoke(this, "list-targets", {
7142
+ lambdaFunction: this.lambdas.listTargets,
7143
+ resultPath: "$.listResult",
7144
+ retryOnServiceExceptions: true
7145
+ });
7146
+ const updateAfterList = new import_aws_stepfunctions2.Pass(this, "update-after-list", {
7147
+ parameters: {
7148
+ "entityType.$": "$.entityType",
7149
+ "entityId.$": "$.entityId",
7150
+ "tenantId.$": "$.tenantId",
7151
+ "oldName.$": "$.oldName",
7152
+ "newName.$": "$.newName",
7153
+ "oldNormalizedName.$": "$.oldNormalizedName",
7154
+ "newNormalizedName.$": "$.newNormalizedName",
7155
+ "cursors.$": "$.listResult.Payload.cursors",
7156
+ "itemsRewritten.$": "$.listResult.Payload.itemsRewritten",
7157
+ "chunkCount.$": "$.listResult.Payload.chunkCount",
7158
+ "exhausted.$": "$.listResult.Payload.exhausted",
7159
+ "chunks.$": "$.listResult.Payload.chunks",
7160
+ "startedAt.$": "$.startedAt",
7161
+ "eventId.$": "$.eventId",
7162
+ "correlationId.$": "$.correlationId",
7163
+ "causationId.$": "$.causationId"
7164
+ }
7165
+ });
7166
+ const rewriteChunks = new import_aws_stepfunctions2.CustomState(this, "rewrite-chunks", {
7167
+ stateJson: {
7168
+ Type: "Map",
7169
+ ItemsPath: "$.chunks",
7170
+ MaxConcurrency: concurrency,
7171
+ ResultPath: "$.rewriteResults",
7172
+ ItemSelector: {
7173
+ "entityType.$": "$$.Map.Item.Value.entityType",
7174
+ "entityId.$": "$$.Map.Item.Value.entityId",
7175
+ "tenantId.$": "$$.Map.Item.Value.tenantId",
7176
+ "targets.$": "$$.Map.Item.Value.targets",
7177
+ "chunkToken.$": "$$.Map.Item.Value.chunkToken"
7178
+ },
7179
+ ItemProcessor: {
7180
+ ProcessorConfig: {
7181
+ // DISTRIBUTED mode — per TR-023 (NOT INLINE; that is
7182
+ // TR-022's territory).
7183
+ Mode: "DISTRIBUTED",
7184
+ ExecutionType: "STANDARD"
7185
+ },
7186
+ StartAt: "RewriteChunk",
7187
+ States: {
7188
+ RewriteChunk: {
7189
+ Type: "Task",
7190
+ Resource: "arn:aws:states:::lambda:invoke",
7191
+ Parameters: {
7192
+ FunctionName: this.lambdas.rewriteChunk.functionArn,
7193
+ "Payload.$": "$"
7194
+ },
7195
+ Retry: [
7196
+ {
7197
+ ErrorEquals: [
7198
+ "DynamoDB.ProvisionedThroughputExceededException",
7199
+ "DynamoDB.ThrottlingException",
7200
+ "DynamoDB.TransactionConflictException",
7201
+ "Lambda.ServiceException",
7202
+ "Lambda.AWSLambdaException",
7203
+ "Lambda.SdkClientException"
7204
+ ],
7205
+ IntervalSeconds: 1,
7206
+ MaxAttempts: 5,
7207
+ BackoffRate: 2,
7208
+ MaxDelaySeconds: 30
7209
+ }
7210
+ ],
7211
+ Catch: [
7212
+ {
7213
+ // Replay path: the rewrite race "lost to a later
7214
+ // write" per TR-023 idempotency. Treat as a no-op
7215
+ // success so the outer loop keeps draining the page.
7216
+ ErrorEquals: ["DynamoDB.TransactionCanceledException"],
7217
+ Next: "ChunkAlreadyRewritten"
7218
+ }
7219
+ ],
7220
+ End: true
7221
+ },
7222
+ ChunkAlreadyRewritten: {
7223
+ Type: "Succeed"
7224
+ }
7225
+ }
7226
+ }
7227
+ }
7228
+ });
7229
+ const isExhausted = new import_aws_stepfunctions2.Choice(this, "is-exhausted");
7230
+ const finalize = new import_aws_stepfunctions_tasks2.LambdaInvoke(this, "finalize", {
7231
+ lambdaFunction: this.lambdas.finalize,
7232
+ payload: import_aws_stepfunctions2.TaskInput.fromObject({
7233
+ "entityType.$": "$.entityType",
7234
+ "entityId.$": "$.entityId",
7235
+ "tenantId.$": "$.tenantId",
7236
+ "newName.$": "$.newName",
7237
+ "itemsRewritten.$": "$.itemsRewritten",
7238
+ "chunkCount.$": "$.chunkCount",
7239
+ "startedAt.$": "$.startedAt",
7240
+ "eventId.$": "$.eventId",
7241
+ "correlationId.$": "$.correlationId",
7242
+ "causationId.$": "$.causationId"
7243
+ }),
7244
+ resultPath: "$.finalizeResult",
7245
+ retryOnServiceExceptions: true
7246
+ });
7247
+ const success = new import_aws_stepfunctions2.Succeed(this, "success");
7248
+ const definition = initState.next(listTargets).next(updateAfterList).next(rewriteChunks).next(
7249
+ isExhausted.when(
7250
+ import_aws_stepfunctions2.Condition.booleanEquals("$.exhausted", true),
7251
+ finalize.next(success)
7252
+ ).otherwise(listTargets)
7253
+ );
7254
+ this.stateMachine = new import_aws_stepfunctions2.StateMachine(this, "state-machine", {
7255
+ definitionBody: import_aws_stepfunctions2.DefinitionBody.fromChainable(definition),
7256
+ // Long timeout — large renames may rewrite thousands of rows;
7257
+ // the `CascadeSlow` alarm fires at 300s p99 but the state
7258
+ // machine itself does not abort.
7259
+ timeout: import_aws_cdk_lib18.Duration.hours(2)
7260
+ });
7261
+ this.rule = new import_aws_events10.Rule(this, "rule", {
7262
+ eventBus: props.dataEventBus,
7263
+ eventPattern: {
7264
+ source: [import_workflows6.OPENHI_DATA_SOURCE],
7265
+ detailType: [import_workflows6.ControlPlaneRenameV1.detailType]
7266
+ },
7267
+ targets: [
7268
+ new import_aws_events_targets6.SfnStateMachine(this.stateMachine, {
7269
+ retryAttempts: 2,
7270
+ maxEventAge: import_aws_cdk_lib18.Duration.hours(2)
7271
+ })
7272
+ ]
7273
+ });
7274
+ }
7275
+ };
5718
7276
  // Annotate the CommonJS export names for ESM import in node:
5719
7277
  0 && (module.exports = {
5720
7278
  BRIDGED_STATUSES,
@@ -5728,6 +7286,12 @@ var OpenHiGraphqlService = _OpenHiGraphqlService;
5728
7286
  CognitoUserPoolDomain,
5729
7287
  CognitoUserPoolKmsKey,
5730
7288
  ControlEventBus,
7289
+ ControlPlaneOwningDeleteCompleteV1,
7290
+ ControlPlaneOwningDeleteFailedV1,
7291
+ ControlPlaneOwningDeleteV1,
7292
+ ControlPlaneRenameCompleteV1,
7293
+ ControlPlaneRenameFailedV1,
7294
+ ControlPlaneRenameV1,
5731
7295
  DATA_STORE_CHANGE_DETAIL_MAX_UTF8_BYTES,
5732
7296
  DATA_STORE_CHANGE_DETAIL_TYPE,
5733
7297
  DATA_STORE_CHANGE_EVENT_SOURCE,
@@ -5747,6 +7311,11 @@ var OpenHiGraphqlService = _OpenHiGraphqlService;
5747
7311
  OPENHI_TAG_SUFFIX_REPO_NAME,
5748
7312
  OPENHI_TAG_SUFFIX_SERVICE_TYPE,
5749
7313
  OPENHI_TAG_SUFFIX_STAGE_TYPE,
7314
+ OWNING_DELETE_CASCADE_CONSUMER_NAME,
7315
+ OWNING_DELETE_CASCADE_DEFAULT_CONCURRENCY,
7316
+ OWNING_DELETE_CASCADE_STUCK_THRESHOLD_MINUTES,
7317
+ OWNING_DELETE_OPS_EVENT_BUS_ENV_VAR,
7318
+ OWNING_ENTITY_TYPE,
5750
7319
  OpenHiApp,
5751
7320
  OpenHiAuthService,
5752
7321
  OpenHiDataService,
@@ -5757,6 +7326,8 @@ var OpenHiGraphqlService = _OpenHiGraphqlService;
5757
7326
  OpenHiService,
5758
7327
  OpenHiStage,
5759
7328
  OpsEventBus,
7329
+ OwningDeleteCascadeLambdas,
7330
+ OwningDeleteCascadeWorkflow,
5760
7331
  PLACEHOLDER_TENANT_ID,
5761
7332
  PLACEHOLDER_WORKSPACE_ID,
5762
7333
  PLATFORM_DEPLOY_BRIDGE_ACTOR_SYSTEM,
@@ -5772,7 +7343,15 @@ var OpenHiGraphqlService = _OpenHiGraphqlService;
5772
7343
  PostConfirmationLambda,
5773
7344
  PreTokenGenerationLambda,
5774
7345
  ProvisionDefaultWorkspaceLambda,
7346
+ RENAMABLE_ENTITY_TYPE,
7347
+ RENAME_CASCADE_CONSUMER_NAME,
7348
+ RENAME_CASCADE_DEFAULT_CONCURRENCY,
7349
+ RENAME_CASCADE_FAILED_THRESHOLD,
7350
+ RENAME_CASCADE_OPS_EVENT_BUS_ENV_VAR,
7351
+ RENAME_CASCADE_SLOW_THRESHOLD_SECONDS,
5775
7352
  REST_API_BASE_URL_SSM_NAME,
7353
+ RenameCascadeLambdas,
7354
+ RenameCascadeWorkflow,
5776
7355
  RootGraphqlApi,
5777
7356
  RootHostedZone,
5778
7357
  RootHttpApi,