@openhi/constructs 0.0.160 → 0.0.161

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 (121) hide show
  1. package/lib/{chunk-HQ67J7BP.mjs → chunk-5S6VFBLT.mjs} +12 -70
  2. package/lib/chunk-5S6VFBLT.mjs.map +1 -0
  3. package/lib/{chunk-MVQWAIMC.mjs → chunk-6BB4CRSS.mjs} +3 -312
  4. package/lib/chunk-6BB4CRSS.mjs.map +1 -0
  5. package/lib/{chunk-WPCBVDFZ.mjs → chunk-76UM2LQ5.mjs} +2 -2
  6. package/lib/{chunk-QFHYTCVY.mjs → chunk-7TRO2STL.mjs} +7 -7
  7. package/lib/chunk-BUAYVN3C.mjs +87 -0
  8. package/lib/chunk-BUAYVN3C.mjs.map +1 -0
  9. package/lib/{chunk-23PUSHBV.mjs → chunk-D2Y6DDOC.mjs} +2 -2
  10. package/lib/chunk-DWSWCUZR.mjs +123 -0
  11. package/lib/chunk-DWSWCUZR.mjs.map +1 -0
  12. package/lib/{chunk-VZCPGQXA.mjs → chunk-EUIP2U5F.mjs} +69 -1
  13. package/lib/{chunk-VZCPGQXA.mjs.map → chunk-EUIP2U5F.mjs.map} +1 -1
  14. package/lib/chunk-GJTPXJKD.mjs +46 -0
  15. package/lib/chunk-GJTPXJKD.mjs.map +1 -0
  16. package/lib/chunk-I6LUPJUY.mjs +61 -0
  17. package/lib/chunk-I6LUPJUY.mjs.map +1 -0
  18. package/lib/{chunk-KR2Y2CVQ.mjs → chunk-KA3OMP3X.mjs} +2 -2
  19. package/lib/{chunk-ZM4GDHHC.mjs → chunk-KMEWULMX.mjs} +51 -3
  20. package/lib/chunk-KMEWULMX.mjs.map +1 -0
  21. package/lib/chunk-LKKLO66E.mjs +25 -0
  22. package/lib/chunk-LKKLO66E.mjs.map +1 -0
  23. package/lib/{chunk-CFJDATDK.mjs → chunk-MLFMW5IF.mjs} +43 -9
  24. package/lib/chunk-MLFMW5IF.mjs.map +1 -0
  25. package/lib/chunk-O5VQWB6U.mjs +315 -0
  26. package/lib/chunk-O5VQWB6U.mjs.map +1 -0
  27. package/lib/{chunk-7BQHLC7U.mjs → chunk-P3CTZWC2.mjs} +8 -40
  28. package/lib/chunk-P3CTZWC2.mjs.map +1 -0
  29. package/lib/{chunk-EFB5OFM7.mjs → chunk-P3NFCKTZ.mjs} +6 -4
  30. package/lib/{chunk-EFB5OFM7.mjs.map → chunk-P3NFCKTZ.mjs.map} +1 -1
  31. package/lib/{chunk-M7Y3BOQW.mjs → chunk-Q3MKITPY.mjs} +5 -5
  32. package/lib/chunk-Q64MOYJ7.mjs +218 -0
  33. package/lib/chunk-Q64MOYJ7.mjs.map +1 -0
  34. package/lib/chunk-RQKJNMX5.mjs +89 -0
  35. package/lib/chunk-RQKJNMX5.mjs.map +1 -0
  36. package/lib/{chunk-ZWSGM6PZ.mjs → chunk-SD7J3N3C.mjs} +2 -2
  37. package/lib/{chunk-7RZHFI77.mjs → chunk-VESULYQQ.mjs} +2 -2
  38. package/lib/{chunk-AOSEKL7U.mjs → chunk-WOTU36P3.mjs} +6 -103
  39. package/lib/chunk-WOTU36P3.mjs.map +1 -0
  40. package/lib/{chunk-X5E4YJGZ.mjs → chunk-YPTJJ35S.mjs} +2 -2
  41. package/lib/counter-apply-operation-DZM3MIDm.d.mts +63 -0
  42. package/lib/counter-apply-operation-DZM3MIDm.d.ts +63 -0
  43. package/lib/counter-maintenance.handler.d.mts +38 -0
  44. package/lib/counter-maintenance.handler.d.ts +38 -0
  45. package/lib/counter-maintenance.handler.js +2885 -0
  46. package/lib/counter-maintenance.handler.js.map +1 -0
  47. package/lib/counter-maintenance.handler.mjs +180 -0
  48. package/lib/counter-maintenance.handler.mjs.map +1 -0
  49. package/lib/counter-reconciliation.handler.d.mts +116 -0
  50. package/lib/counter-reconciliation.handler.d.ts +116 -0
  51. package/lib/counter-reconciliation.handler.js +3324 -0
  52. package/lib/counter-reconciliation.handler.js.map +1 -0
  53. package/lib/counter-reconciliation.handler.mjs +295 -0
  54. package/lib/counter-reconciliation.handler.mjs.map +1 -0
  55. package/lib/data-store-postgres-replication.handler.js +50 -2
  56. package/lib/data-store-postgres-replication.handler.js.map +1 -1
  57. package/lib/data-store-postgres-replication.handler.mjs +2 -2
  58. package/lib/delete-chunk.handler.js +118 -2
  59. package/lib/delete-chunk.handler.js.map +1 -1
  60. package/lib/delete-chunk.handler.mjs +3 -3
  61. package/lib/finalize.handler.js +50 -2
  62. package/lib/finalize.handler.js.map +1 -1
  63. package/lib/finalize.handler.mjs +4 -4
  64. package/lib/firehose-archive-transform.handler.js +50 -2
  65. package/lib/firehose-archive-transform.handler.js.map +1 -1
  66. package/lib/firehose-archive-transform.handler.mjs +2 -2
  67. package/lib/index.d.mts +140 -2
  68. package/lib/index.d.ts +143 -5
  69. package/lib/index.js +493 -196
  70. package/lib/index.js.map +1 -1
  71. package/lib/index.mjs +360 -193
  72. package/lib/index.mjs.map +1 -1
  73. package/lib/list-chunks.handler.js +118 -2
  74. package/lib/list-chunks.handler.js.map +1 -1
  75. package/lib/list-chunks.handler.mjs +3 -3
  76. package/lib/platform-deploy-bridge.handler.js +50 -2
  77. package/lib/platform-deploy-bridge.handler.js.map +1 -1
  78. package/lib/platform-deploy-bridge.handler.mjs +1 -1
  79. package/lib/pre-token-generation.handler.js +68 -0
  80. package/lib/pre-token-generation.handler.js.map +1 -1
  81. package/lib/pre-token-generation.handler.mjs +9 -5
  82. package/lib/pre-token-generation.handler.mjs.map +1 -1
  83. package/lib/provision-default-workspace.handler.js +883 -0
  84. package/lib/provision-default-workspace.handler.js.map +1 -1
  85. package/lib/provision-default-workspace.handler.mjs +10 -5
  86. package/lib/provision-default-workspace.handler.mjs.map +1 -1
  87. package/lib/rename-finalize.handler.js +50 -2
  88. package/lib/rename-finalize.handler.js.map +1 -1
  89. package/lib/rename-finalize.handler.mjs +2 -2
  90. package/lib/rename-list-targets.handler.js +118 -2
  91. package/lib/rename-list-targets.handler.js.map +1 -1
  92. package/lib/rename-list-targets.handler.mjs +11 -9
  93. package/lib/rename-list-targets.handler.mjs.map +1 -1
  94. package/lib/rename-rewrite-chunk.handler.js +68 -0
  95. package/lib/rename-rewrite-chunk.handler.js.map +1 -1
  96. package/lib/rename-rewrite-chunk.handler.mjs +2 -2
  97. package/lib/rest-api-lambda.handler.js +1454 -251
  98. package/lib/rest-api-lambda.handler.js.map +1 -1
  99. package/lib/rest-api-lambda.handler.mjs +415 -291
  100. package/lib/rest-api-lambda.handler.mjs.map +1 -1
  101. package/lib/seed-demo-data.handler.js +205 -8
  102. package/lib/seed-demo-data.handler.js.map +1 -1
  103. package/lib/seed-demo-data.handler.mjs +10 -7
  104. package/lib/seed-system-data.handler.js +118 -2
  105. package/lib/seed-system-data.handler.js.map +1 -1
  106. package/lib/seed-system-data.handler.mjs +5 -5
  107. package/package.json +1 -1
  108. package/lib/chunk-7BQHLC7U.mjs.map +0 -1
  109. package/lib/chunk-AOSEKL7U.mjs.map +0 -1
  110. package/lib/chunk-CFJDATDK.mjs.map +0 -1
  111. package/lib/chunk-HQ67J7BP.mjs.map +0 -1
  112. package/lib/chunk-MVQWAIMC.mjs.map +0 -1
  113. package/lib/chunk-ZM4GDHHC.mjs.map +0 -1
  114. /package/lib/{chunk-WPCBVDFZ.mjs.map → chunk-76UM2LQ5.mjs.map} +0 -0
  115. /package/lib/{chunk-QFHYTCVY.mjs.map → chunk-7TRO2STL.mjs.map} +0 -0
  116. /package/lib/{chunk-23PUSHBV.mjs.map → chunk-D2Y6DDOC.mjs.map} +0 -0
  117. /package/lib/{chunk-KR2Y2CVQ.mjs.map → chunk-KA3OMP3X.mjs.map} +0 -0
  118. /package/lib/{chunk-M7Y3BOQW.mjs.map → chunk-Q3MKITPY.mjs.map} +0 -0
  119. /package/lib/{chunk-ZWSGM6PZ.mjs.map → chunk-SD7J3N3C.mjs.map} +0 -0
  120. /package/lib/{chunk-7RZHFI77.mjs.map → chunk-VESULYQQ.mjs.map} +0 -0
  121. /package/lib/{chunk-X5E4YJGZ.mjs.map → chunk-YPTJJ35S.mjs.map} +0 -0
@@ -0,0 +1,2885 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __commonJS = (cb, mod) => function __require() {
9
+ return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
10
+ };
11
+ var __export = (target, all) => {
12
+ for (var name in all)
13
+ __defProp(target, name, { get: all[name], enumerable: true });
14
+ };
15
+ var __copyProps = (to, from, except, desc) => {
16
+ if (from && typeof from === "object" || typeof from === "function") {
17
+ for (let key of __getOwnPropNames(from))
18
+ if (!__hasOwnProp.call(to, key) && key !== except)
19
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
20
+ }
21
+ return to;
22
+ };
23
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
24
+ // If the importer is in node compatibility mode or this is not an ESM
25
+ // file that has been converted to a CommonJS file using a Babel-
26
+ // compatible transform (i.e. "__esModule" has not been set), then set
27
+ // "default" to the CommonJS "module.exports" for node compatibility.
28
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
29
+ mod
30
+ ));
31
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
32
+
33
+ // ../workflows/lib/envelope-version.js
34
+ var require_envelope_version = __commonJS({
35
+ "../workflows/lib/envelope-version.js"(exports2) {
36
+ "use strict";
37
+ Object.defineProperty(exports2, "__esModule", { value: true });
38
+ exports2.ENVELOPE_VERSION = void 0;
39
+ exports2.isSupportedEnvelopeVersion = isSupportedEnvelopeVersion;
40
+ exports2.ENVELOPE_VERSION = "1.0";
41
+ var ENVELOPE_VERSION_PATTERN = /^\d+\.\d+$/;
42
+ var MIN_SUPPORTED_MAJOR = 1;
43
+ var MAX_SUPPORTED_MAJOR = 1;
44
+ function isSupportedEnvelopeVersion(version) {
45
+ if (!ENVELOPE_VERSION_PATTERN.test(version)) {
46
+ return false;
47
+ }
48
+ const major = Number.parseInt(version.split(".")[0], 10);
49
+ return major >= MIN_SUPPORTED_MAJOR && major <= MAX_SUPPORTED_MAJOR;
50
+ }
51
+ }
52
+ });
53
+
54
+ // ../workflows/lib/envelope.js
55
+ var require_envelope = __commonJS({
56
+ "../workflows/lib/envelope.js"(exports2) {
57
+ "use strict";
58
+ Object.defineProperty(exports2, "__esModule", { value: true });
59
+ exports2.MissingActorContextError = void 0;
60
+ exports2.isWorkflowUserActor = isWorkflowUserActor;
61
+ exports2.isWorkflowSystemActor = isWorkflowSystemActor;
62
+ exports2.workflowUserActorFromClaims = workflowUserActorFromClaims;
63
+ function isWorkflowUserActor(actor) {
64
+ return actor.ohi_uid !== void 0;
65
+ }
66
+ function isWorkflowSystemActor(actor) {
67
+ return actor.system !== void 0;
68
+ }
69
+ function workflowUserActorFromClaims(claims) {
70
+ if (claims.ohi_tid === void 0 || claims.ohi_wid === void 0) {
71
+ throw new MissingActorContextError("workflowUserActorFromClaims: ohi_tid and ohi_wid are required on the workflow user-actor; the caller's JWT is missing one or both. Use a system-actor for pre-provisioning bootstrap workflows.");
72
+ }
73
+ return {
74
+ ohi_tid: claims.ohi_tid,
75
+ ohi_wid: claims.ohi_wid,
76
+ ohi_uid: claims.ohi_uid,
77
+ ohi_uname: claims.ohi_uname
78
+ };
79
+ }
80
+ var MissingActorContextError = class extends Error {
81
+ /** @param message - human-readable description of the missing claim. */
82
+ constructor(message) {
83
+ super(message);
84
+ this.name = "MissingActorContextError";
85
+ }
86
+ };
87
+ exports2.MissingActorContextError = MissingActorContextError;
88
+ }
89
+ });
90
+
91
+ // ../workflows/lib/sources.js
92
+ var require_sources = __commonJS({
93
+ "../workflows/lib/sources.js"(exports2) {
94
+ "use strict";
95
+ Object.defineProperty(exports2, "__esModule", { value: true });
96
+ exports2.DEFAULT_BUS_NAME_BY_SOURCE = exports2.OPENHI_OPS_SOURCE = exports2.OPENHI_DATA_SOURCE = exports2.OPENHI_CONTROL_SOURCE = void 0;
97
+ exports2.OPENHI_CONTROL_SOURCE = "openhi.control";
98
+ exports2.OPENHI_DATA_SOURCE = "openhi.data";
99
+ exports2.OPENHI_OPS_SOURCE = "openhi.ops";
100
+ exports2.DEFAULT_BUS_NAME_BY_SOURCE = {
101
+ [exports2.OPENHI_CONTROL_SOURCE]: "openhi-control-event-bus",
102
+ [exports2.OPENHI_DATA_SOURCE]: "openhi-data-event-bus",
103
+ [exports2.OPENHI_OPS_SOURCE]: "openhi-ops-event-bus"
104
+ };
105
+ }
106
+ });
107
+
108
+ // ../workflows/lib/detail-types/registry.js
109
+ var require_registry = __commonJS({
110
+ "../workflows/lib/detail-types/registry.js"(exports2) {
111
+ "use strict";
112
+ Object.defineProperty(exports2, "__esModule", { value: true });
113
+ exports2.InvalidDetailTypeRegistrationError = void 0;
114
+ exports2.defineDetailType = defineDetailType;
115
+ exports2.isWellFormedDetailType = isWellFormedDetailType;
116
+ function defineDetailType(entry) {
117
+ if (!isWellFormedDetailType(entry.detailType)) {
118
+ throw new InvalidDetailTypeRegistrationError(`Detail-type "${entry.detailType}" does not match the platform-wide format <area>.<event>.v<integer>. See TR-016 \xA7Open Items #2.`);
119
+ }
120
+ return entry;
121
+ }
122
+ var DETAIL_TYPE_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*\.[a-z0-9]+(?:-[a-z0-9]+)*\.v\d+$/;
123
+ function isWellFormedDetailType(detailType) {
124
+ return DETAIL_TYPE_PATTERN.test(detailType);
125
+ }
126
+ var InvalidDetailTypeRegistrationError = class extends Error {
127
+ /** @param message - human-readable description of the violation. */
128
+ constructor(message) {
129
+ super(message);
130
+ this.name = "InvalidDetailTypeRegistrationError";
131
+ }
132
+ };
133
+ exports2.InvalidDetailTypeRegistrationError = InvalidDetailTypeRegistrationError;
134
+ }
135
+ });
136
+
137
+ // ../workflows/lib/detail-types/control-plane.js
138
+ var require_control_plane = __commonJS({
139
+ "../workflows/lib/detail-types/control-plane.js"(exports2) {
140
+ "use strict";
141
+ Object.defineProperty(exports2, "__esModule", { value: true });
142
+ exports2.ControlPlaneWorkspaceDeletedV1 = exports2.ControlPlaneWorkspaceCreatedV1 = exports2.ControlPlaneRoleAssignmentDeletedV1 = exports2.ControlPlaneRoleAssignmentCreatedV1 = exports2.ControlPlaneMembershipDeletedV1 = exports2.ControlPlaneMembershipCreatedV1 = exports2.ControlPlaneRenameFailedV1 = exports2.ControlPlaneRenameCompleteV1 = exports2.ControlPlaneRenameV1 = exports2.RENAMABLE_ENTITY_TYPE = exports2.ControlPlaneOwningDeleteFailedV1 = exports2.ControlPlaneOwningDeleteCompleteV1 = exports2.ControlPlaneOwningDeleteV1 = exports2.OWNING_ENTITY_TYPE = void 0;
143
+ var sources_1 = require_sources();
144
+ var registry_1 = require_registry();
145
+ exports2.OWNING_ENTITY_TYPE = {
146
+ Workspace: "Workspace",
147
+ User: "User"
148
+ };
149
+ exports2.ControlPlaneOwningDeleteV1 = (0, registry_1.defineDetailType)({
150
+ detailType: "control-plane.owning-delete.v1",
151
+ source: sources_1.OPENHI_DATA_SOURCE,
152
+ dedupRequired: true
153
+ });
154
+ exports2.ControlPlaneOwningDeleteCompleteV1 = (0, registry_1.defineDetailType)({
155
+ detailType: "control-plane.owning-delete-complete.v1",
156
+ source: sources_1.OPENHI_OPS_SOURCE,
157
+ dedupRequired: true
158
+ });
159
+ exports2.ControlPlaneOwningDeleteFailedV1 = (0, registry_1.defineDetailType)({
160
+ detailType: "control-plane.owning-delete-failed.v1",
161
+ source: sources_1.OPENHI_OPS_SOURCE,
162
+ dedupRequired: true
163
+ });
164
+ exports2.RENAMABLE_ENTITY_TYPE = {
165
+ Tenant: "Tenant",
166
+ User: "User",
167
+ Role: "Role"
168
+ };
169
+ exports2.ControlPlaneRenameV1 = (0, registry_1.defineDetailType)({
170
+ detailType: "control-plane.rename.v1",
171
+ source: sources_1.OPENHI_DATA_SOURCE,
172
+ dedupRequired: true
173
+ });
174
+ exports2.ControlPlaneRenameCompleteV1 = (0, registry_1.defineDetailType)({
175
+ detailType: "control-plane.rename-complete.v1",
176
+ source: sources_1.OPENHI_OPS_SOURCE,
177
+ dedupRequired: true
178
+ });
179
+ exports2.ControlPlaneRenameFailedV1 = (0, registry_1.defineDetailType)({
180
+ detailType: "control-plane.rename-failed.v1",
181
+ source: sources_1.OPENHI_OPS_SOURCE,
182
+ dedupRequired: true
183
+ });
184
+ exports2.ControlPlaneMembershipCreatedV1 = (0, registry_1.defineDetailType)({
185
+ detailType: "control-plane.membership-created.v1",
186
+ source: sources_1.OPENHI_CONTROL_SOURCE,
187
+ dedupRequired: true
188
+ });
189
+ exports2.ControlPlaneMembershipDeletedV1 = (0, registry_1.defineDetailType)({
190
+ detailType: "control-plane.membership-deleted.v1",
191
+ source: sources_1.OPENHI_CONTROL_SOURCE,
192
+ dedupRequired: true
193
+ });
194
+ exports2.ControlPlaneRoleAssignmentCreatedV1 = (0, registry_1.defineDetailType)({
195
+ detailType: "control-plane.role-assignment-created.v1",
196
+ source: sources_1.OPENHI_CONTROL_SOURCE,
197
+ dedupRequired: true
198
+ });
199
+ exports2.ControlPlaneRoleAssignmentDeletedV1 = (0, registry_1.defineDetailType)({
200
+ detailType: "control-plane.role-assignment-deleted.v1",
201
+ source: sources_1.OPENHI_CONTROL_SOURCE,
202
+ dedupRequired: true
203
+ });
204
+ exports2.ControlPlaneWorkspaceCreatedV1 = (0, registry_1.defineDetailType)({
205
+ detailType: "control-plane.workspace-created.v1",
206
+ source: sources_1.OPENHI_CONTROL_SOURCE,
207
+ dedupRequired: true
208
+ });
209
+ exports2.ControlPlaneWorkspaceDeletedV1 = (0, registry_1.defineDetailType)({
210
+ detailType: "control-plane.workspace-deleted.v1",
211
+ source: sources_1.OPENHI_CONTROL_SOURCE,
212
+ dedupRequired: true
213
+ });
214
+ }
215
+ });
216
+
217
+ // ../workflows/lib/detail-types/platform.js
218
+ var require_platform = __commonJS({
219
+ "../workflows/lib/detail-types/platform.js"(exports2) {
220
+ "use strict";
221
+ Object.defineProperty(exports2, "__esModule", { value: true });
222
+ exports2.PlatformSystemDataSeededV1 = exports2.PlatformDeploymentCompletedV1 = void 0;
223
+ var sources_1 = require_sources();
224
+ var registry_1 = require_registry();
225
+ exports2.PlatformDeploymentCompletedV1 = (0, registry_1.defineDetailType)({
226
+ detailType: "platform.deployment-completed.v1",
227
+ source: sources_1.OPENHI_CONTROL_SOURCE,
228
+ dedupRequired: true
229
+ });
230
+ exports2.PlatformSystemDataSeededV1 = (0, registry_1.defineDetailType)({
231
+ detailType: "platform.system-data-seeded.v1",
232
+ source: sources_1.OPENHI_CONTROL_SOURCE,
233
+ dedupRequired: true
234
+ });
235
+ }
236
+ });
237
+
238
+ // ../workflows/lib/detail-types/index.js
239
+ var require_detail_types = __commonJS({
240
+ "../workflows/lib/detail-types/index.js"(exports2) {
241
+ "use strict";
242
+ var __createBinding = exports2 && exports2.__createBinding || (Object.create ? (function(o, m, k, k2) {
243
+ if (k2 === void 0) k2 = k;
244
+ var desc = Object.getOwnPropertyDescriptor(m, k);
245
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
246
+ desc = { enumerable: true, get: function() {
247
+ return m[k];
248
+ } };
249
+ }
250
+ Object.defineProperty(o, k2, desc);
251
+ }) : (function(o, m, k, k2) {
252
+ if (k2 === void 0) k2 = k;
253
+ o[k2] = m[k];
254
+ }));
255
+ var __exportStar = exports2 && exports2.__exportStar || function(m, exports3) {
256
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports3, p)) __createBinding(exports3, m, p);
257
+ };
258
+ Object.defineProperty(exports2, "__esModule", { value: true });
259
+ __exportStar(require_control_plane(), exports2);
260
+ __exportStar(require_platform(), exports2);
261
+ __exportStar(require_registry(), exports2);
262
+ }
263
+ });
264
+
265
+ // ../workflows/lib/publisher.js
266
+ var require_publisher = __commonJS({
267
+ "../workflows/lib/publisher.js"(exports2) {
268
+ "use strict";
269
+ Object.defineProperty(exports2, "__esModule", { value: true });
270
+ exports2.WorkflowPublishError = void 0;
271
+ exports2.workflowsClient = workflowsClient;
272
+ exports2.publishWorkflowEvent = publishWorkflowEvent;
273
+ var node_crypto_1 = require("crypto");
274
+ var client_eventbridge_1 = require("@aws-sdk/client-eventbridge");
275
+ var envelope_version_1 = require_envelope_version();
276
+ var sources_1 = require_sources();
277
+ function workflowsClient(bridge, options = {}) {
278
+ return {
279
+ publish: (entry, payload, ctx) => publishWorkflowEvent(bridge, entry, payload, ctx, options)
280
+ };
281
+ }
282
+ async function publishWorkflowEvent(bridge, entry, payload, ctx, options = {}) {
283
+ const eventIdGenerator = options.eventIdGenerator ?? (() => (0, node_crypto_1.randomUUID)());
284
+ const correlationIdGenerator = options.correlationIdGenerator ?? (() => (0, node_crypto_1.randomUUID)());
285
+ const now = options.now ?? (() => /* @__PURE__ */ new Date());
286
+ const envelope = {
287
+ eventId: eventIdGenerator(),
288
+ attempt: 1,
289
+ correlationId: ctx.correlationId ?? correlationIdGenerator(),
290
+ causationId: ctx.causationId ?? null,
291
+ actor: ctx.actor,
292
+ occurredAt: now().toISOString(),
293
+ envelopeVersion: envelope_version_1.ENVELOPE_VERSION,
294
+ payload
295
+ };
296
+ const busName = options.busNameByPlane?.[entry.source] ?? sources_1.DEFAULT_BUS_NAME_BY_SOURCE[entry.source];
297
+ const result = await bridge.send(new client_eventbridge_1.PutEventsCommand({
298
+ Entries: [
299
+ {
300
+ EventBusName: busName,
301
+ Source: entry.source,
302
+ DetailType: entry.detailType,
303
+ Detail: JSON.stringify(envelope)
304
+ }
305
+ ]
306
+ }));
307
+ if ((result.FailedEntryCount ?? 0) > 0) {
308
+ const first = result.Entries?.[0];
309
+ throw new WorkflowPublishError(`EventBridge rejected ${entry.detailType} publish on bus ${busName}: ${first?.ErrorCode ?? "unknown"} \u2014 ${first?.ErrorMessage ?? "no error message"}`);
310
+ }
311
+ return { eventId: envelope.eventId };
312
+ }
313
+ var WorkflowPublishError = class extends Error {
314
+ /** @param message - human-readable description of the failed publish. */
315
+ constructor(message) {
316
+ super(message);
317
+ this.name = "WorkflowPublishError";
318
+ }
319
+ };
320
+ exports2.WorkflowPublishError = WorkflowPublishError;
321
+ }
322
+ });
323
+
324
+ // ../workflows/lib/consumer.js
325
+ var require_consumer = __commonJS({
326
+ "../workflows/lib/consumer.js"(exports2) {
327
+ "use strict";
328
+ Object.defineProperty(exports2, "__esModule", { value: true });
329
+ exports2.UnsupportedEnvelopeVersionError = exports2.InvalidWorkflowEventError = void 0;
330
+ exports2.parseWorkflowEvent = parseWorkflowEvent2;
331
+ var envelope_version_1 = require_envelope_version();
332
+ function parseWorkflowEvent2(event, expected) {
333
+ if (event.source !== expected.source) {
334
+ throw new InvalidWorkflowEventError(`EventBridge source "${event.source}" does not match expected detail-type's source "${expected.source}".`);
335
+ }
336
+ if (event["detail-type"] !== expected.detailType) {
337
+ throw new InvalidWorkflowEventError(`EventBridge detail-type "${event["detail-type"]}" does not match expected "${expected.detailType}".`);
338
+ }
339
+ const candidate = asEnvelopeCandidate(event.detail);
340
+ if (!(0, envelope_version_1.isSupportedEnvelopeVersion)(candidate.envelopeVersion)) {
341
+ throw new UnsupportedEnvelopeVersionError(`Envelope version "${candidate.envelopeVersion}" is outside the SDK's supported range.`);
342
+ }
343
+ const envelope = {
344
+ eventId: candidate.eventId,
345
+ attempt: candidate.attempt,
346
+ correlationId: candidate.correlationId,
347
+ causationId: candidate.causationId,
348
+ actor: candidate.actor,
349
+ occurredAt: candidate.occurredAt,
350
+ envelopeVersion: candidate.envelopeVersion,
351
+ payload: candidate.payload
352
+ };
353
+ return {
354
+ envelope,
355
+ dedupKey: { eventId: envelope.eventId, attempt: envelope.attempt }
356
+ };
357
+ }
358
+ function asEnvelopeCandidate(detail) {
359
+ if (detail === null || typeof detail !== "object") {
360
+ throw new InvalidWorkflowEventError("EventBridge detail is not a non-null object.");
361
+ }
362
+ const obj = detail;
363
+ assertString(obj, "eventId");
364
+ assertPositiveInteger(obj, "attempt");
365
+ assertString(obj, "correlationId");
366
+ assertCausationId(obj);
367
+ assertActor(obj);
368
+ assertString(obj, "occurredAt");
369
+ assertString(obj, "envelopeVersion");
370
+ if (!("payload" in obj)) {
371
+ throw new InvalidWorkflowEventError("Envelope is missing required field: payload.");
372
+ }
373
+ return obj;
374
+ }
375
+ function assertString(obj, field) {
376
+ const value = obj[field];
377
+ if (typeof value !== "string" || value.length === 0) {
378
+ throw new InvalidWorkflowEventError(`Envelope field "${field}" must be a non-empty string.`);
379
+ }
380
+ }
381
+ function assertPositiveInteger(obj, field) {
382
+ const value = obj[field];
383
+ if (typeof value !== "number" || !Number.isInteger(value) || value < 1) {
384
+ throw new InvalidWorkflowEventError(`Envelope field "${field}" must be a 1-indexed integer.`);
385
+ }
386
+ }
387
+ function assertCausationId(obj) {
388
+ if (!("causationId" in obj)) {
389
+ throw new InvalidWorkflowEventError("Envelope is missing required field: causationId.");
390
+ }
391
+ const value = obj.causationId;
392
+ if (value !== null && (typeof value !== "string" || value.length === 0)) {
393
+ throw new InvalidWorkflowEventError('Envelope field "causationId" must be a non-empty string or null.');
394
+ }
395
+ }
396
+ function assertActor(obj) {
397
+ const actor = obj.actor;
398
+ if (actor === null || typeof actor !== "object") {
399
+ throw new InvalidWorkflowEventError('Envelope field "actor" must be an object.');
400
+ }
401
+ const actorObj = actor;
402
+ const isUserActor = typeof actorObj.ohi_uid === "string" && typeof actorObj.ohi_uname === "string" && typeof actorObj.ohi_tid === "string" && typeof actorObj.ohi_wid === "string";
403
+ const isSystemActor = typeof actorObj.system === "string";
404
+ if (!isUserActor && !isSystemActor) {
405
+ throw new InvalidWorkflowEventError('Envelope field "actor" must be either a user-actor (ohi_tid, ohi_wid, ohi_uid, ohi_uname) or a system-actor ({ system: string }).');
406
+ }
407
+ }
408
+ var InvalidWorkflowEventError = class extends Error {
409
+ /** @param message - human-readable description of the validation failure. */
410
+ constructor(message) {
411
+ super(message);
412
+ this.name = "InvalidWorkflowEventError";
413
+ }
414
+ };
415
+ exports2.InvalidWorkflowEventError = InvalidWorkflowEventError;
416
+ var UnsupportedEnvelopeVersionError = class extends Error {
417
+ /** @param message - human-readable description of the unsupported version. */
418
+ constructor(message) {
419
+ super(message);
420
+ this.name = "UnsupportedEnvelopeVersionError";
421
+ }
422
+ };
423
+ exports2.UnsupportedEnvelopeVersionError = UnsupportedEnvelopeVersionError;
424
+ }
425
+ });
426
+
427
+ // ../workflows/lib/dedup/env.js
428
+ var require_env = __commonJS({
429
+ "../workflows/lib/dedup/env.js"(exports2) {
430
+ "use strict";
431
+ Object.defineProperty(exports2, "__esModule", { value: true });
432
+ exports2.WORKFLOW_DEDUP_MAX_CONSUMER_NAME_LENGTH = exports2.WORKFLOW_DEDUP_DEFAULT_TTL_SECONDS = exports2.WORKFLOW_DEDUP_TABLE_NAME_ENV_VAR = void 0;
433
+ exports2.WORKFLOW_DEDUP_TABLE_NAME_ENV_VAR = "OPENHI_WORKFLOW_DEDUP_TABLE_NAME";
434
+ exports2.WORKFLOW_DEDUP_DEFAULT_TTL_SECONDS = 14 * 24 * 60 * 60;
435
+ exports2.WORKFLOW_DEDUP_MAX_CONSUMER_NAME_LENGTH = 64;
436
+ }
437
+ });
438
+
439
+ // ../workflows/lib/dedup/workflow-dedup-client.js
440
+ var require_workflow_dedup_client = __commonJS({
441
+ "../workflows/lib/dedup/workflow-dedup-client.js"(exports2) {
442
+ "use strict";
443
+ Object.defineProperty(exports2, "__esModule", { value: true });
444
+ exports2.WorkflowDedupInvalidInputError = exports2.WorkflowDedupTableNameMissingError = void 0;
445
+ exports2.workflowDedupClient = workflowDedupClient2;
446
+ exports2.recordIfAbsent = recordIfAbsent;
447
+ exports2.markFailed = markFailed;
448
+ exports2.encodeSortKey = encodeSortKey;
449
+ var client_dynamodb_1 = require("@aws-sdk/client-dynamodb");
450
+ var env_1 = require_env();
451
+ function workflowDedupClient2(dynamodb, options = {}) {
452
+ return {
453
+ recordIfAbsent: (input) => recordIfAbsent(dynamodb, input, options),
454
+ markFailed: (input) => markFailed(dynamodb, input, options)
455
+ };
456
+ }
457
+ async function recordIfAbsent(dynamodb, input, options = {}) {
458
+ assertConsumerName(input.consumerName);
459
+ assertPositiveInteger(input.attempt, "attempt");
460
+ const ttlSeconds = input.ttlSeconds ?? options.defaultTtlSeconds ?? env_1.WORKFLOW_DEDUP_DEFAULT_TTL_SECONDS;
461
+ if (!Number.isInteger(ttlSeconds) || ttlSeconds <= 0) {
462
+ throw new WorkflowDedupInvalidInputError(`ttlSeconds must be a positive integer; got ${ttlSeconds}.`);
463
+ }
464
+ const tableName = resolveTableName(options.tableName);
465
+ const now = (options.now ?? defaultNow)();
466
+ const sk = encodeSortKey(input.eventId, input.attempt);
467
+ const expiresAt = Math.floor(now.getTime() / 1e3) + ttlSeconds;
468
+ try {
469
+ await dynamodb.send(new client_dynamodb_1.PutItemCommand({
470
+ TableName: tableName,
471
+ Item: {
472
+ consumerName: { S: input.consumerName },
473
+ sk: { S: sk },
474
+ eventId: { S: input.eventId },
475
+ attempt: { N: String(input.attempt) },
476
+ recordedAt: { S: now.toISOString() },
477
+ expiresAt: { N: String(expiresAt) }
478
+ },
479
+ ConditionExpression: "attribute_not_exists(consumerName) AND attribute_not_exists(sk)"
480
+ }));
481
+ return { recorded: true };
482
+ } catch (err) {
483
+ if (err instanceof client_dynamodb_1.ConditionalCheckFailedException) {
484
+ return { recorded: false, alreadyProcessed: true };
485
+ }
486
+ throw err;
487
+ }
488
+ }
489
+ async function markFailed(dynamodb, input, options = {}) {
490
+ assertConsumerName(input.consumerName);
491
+ assertPositiveInteger(input.attempt, "attempt");
492
+ if (input.reason.length === 0) {
493
+ throw new WorkflowDedupInvalidInputError("reason must be non-empty.");
494
+ }
495
+ const tableName = resolveTableName(options.tableName);
496
+ const now = (options.now ?? defaultNow)();
497
+ const sk = encodeSortKey(input.eventId, input.attempt);
498
+ await dynamodb.send(new client_dynamodb_1.UpdateItemCommand({
499
+ TableName: tableName,
500
+ Key: {
501
+ consumerName: { S: input.consumerName },
502
+ sk: { S: sk }
503
+ },
504
+ UpdateExpression: "SET #failed = :failed, #failureReason = :reason, #failedAt = :failedAt",
505
+ ExpressionAttributeNames: {
506
+ "#failed": "failed",
507
+ "#failureReason": "failureReason",
508
+ "#failedAt": "failedAt"
509
+ },
510
+ ExpressionAttributeValues: {
511
+ ":failed": { BOOL: true },
512
+ ":reason": { S: input.reason },
513
+ ":failedAt": { S: now.toISOString() }
514
+ }
515
+ }));
516
+ }
517
+ function encodeSortKey(eventId, attempt) {
518
+ if (eventId.length === 0) {
519
+ throw new WorkflowDedupInvalidInputError("eventId must be non-empty.");
520
+ }
521
+ return `${eventId}#${attempt}`;
522
+ }
523
+ function resolveTableName(explicit) {
524
+ const name = explicit ?? process.env[env_1.WORKFLOW_DEDUP_TABLE_NAME_ENV_VAR];
525
+ if (!name) {
526
+ throw new WorkflowDedupTableNameMissingError(`Workflow dedup table name not set. Pass options.tableName or set ${env_1.WORKFLOW_DEDUP_TABLE_NAME_ENV_VAR}.`);
527
+ }
528
+ return name;
529
+ }
530
+ function assertConsumerName(consumerName) {
531
+ if (consumerName.length === 0) {
532
+ throw new WorkflowDedupInvalidInputError("consumerName must be non-empty.");
533
+ }
534
+ if (consumerName.length > env_1.WORKFLOW_DEDUP_MAX_CONSUMER_NAME_LENGTH) {
535
+ throw new WorkflowDedupInvalidInputError(`consumerName must be \u2264${env_1.WORKFLOW_DEDUP_MAX_CONSUMER_NAME_LENGTH} chars; got ${consumerName.length}.`);
536
+ }
537
+ if (/\s/.test(consumerName)) {
538
+ throw new WorkflowDedupInvalidInputError("consumerName must not contain whitespace.");
539
+ }
540
+ }
541
+ function assertPositiveInteger(value, field) {
542
+ if (!Number.isInteger(value) || value < 1) {
543
+ throw new WorkflowDedupInvalidInputError(`${field} must be a 1-indexed integer; got ${value}.`);
544
+ }
545
+ }
546
+ function defaultNow() {
547
+ return /* @__PURE__ */ new Date();
548
+ }
549
+ var WorkflowDedupTableNameMissingError = class extends Error {
550
+ /** @param message - human-readable description. */
551
+ constructor(message) {
552
+ super(message);
553
+ this.name = "WorkflowDedupTableNameMissingError";
554
+ }
555
+ };
556
+ exports2.WorkflowDedupTableNameMissingError = WorkflowDedupTableNameMissingError;
557
+ var WorkflowDedupInvalidInputError = class extends Error {
558
+ /** @param message - human-readable description. */
559
+ constructor(message) {
560
+ super(message);
561
+ this.name = "WorkflowDedupInvalidInputError";
562
+ }
563
+ };
564
+ exports2.WorkflowDedupInvalidInputError = WorkflowDedupInvalidInputError;
565
+ }
566
+ });
567
+
568
+ // ../workflows/lib/dedup/index.js
569
+ var require_dedup = __commonJS({
570
+ "../workflows/lib/dedup/index.js"(exports2) {
571
+ "use strict";
572
+ Object.defineProperty(exports2, "__esModule", { value: true });
573
+ 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 = void 0;
574
+ var env_1 = require_env();
575
+ Object.defineProperty(exports2, "WORKFLOW_DEDUP_DEFAULT_TTL_SECONDS", { enumerable: true, get: function() {
576
+ return env_1.WORKFLOW_DEDUP_DEFAULT_TTL_SECONDS;
577
+ } });
578
+ Object.defineProperty(exports2, "WORKFLOW_DEDUP_MAX_CONSUMER_NAME_LENGTH", { enumerable: true, get: function() {
579
+ return env_1.WORKFLOW_DEDUP_MAX_CONSUMER_NAME_LENGTH;
580
+ } });
581
+ Object.defineProperty(exports2, "WORKFLOW_DEDUP_TABLE_NAME_ENV_VAR", { enumerable: true, get: function() {
582
+ return env_1.WORKFLOW_DEDUP_TABLE_NAME_ENV_VAR;
583
+ } });
584
+ var workflow_dedup_client_1 = require_workflow_dedup_client();
585
+ Object.defineProperty(exports2, "WorkflowDedupInvalidInputError", { enumerable: true, get: function() {
586
+ return workflow_dedup_client_1.WorkflowDedupInvalidInputError;
587
+ } });
588
+ Object.defineProperty(exports2, "WorkflowDedupTableNameMissingError", { enumerable: true, get: function() {
589
+ return workflow_dedup_client_1.WorkflowDedupTableNameMissingError;
590
+ } });
591
+ Object.defineProperty(exports2, "encodeSortKey", { enumerable: true, get: function() {
592
+ return workflow_dedup_client_1.encodeSortKey;
593
+ } });
594
+ Object.defineProperty(exports2, "markFailed", { enumerable: true, get: function() {
595
+ return workflow_dedup_client_1.markFailed;
596
+ } });
597
+ Object.defineProperty(exports2, "recordIfAbsent", { enumerable: true, get: function() {
598
+ return workflow_dedup_client_1.recordIfAbsent;
599
+ } });
600
+ Object.defineProperty(exports2, "workflowDedupClient", { enumerable: true, get: function() {
601
+ return workflow_dedup_client_1.workflowDedupClient;
602
+ } });
603
+ }
604
+ });
605
+
606
+ // ../workflows/lib/index.js
607
+ var require_lib = __commonJS({
608
+ "../workflows/lib/index.js"(exports2) {
609
+ "use strict";
610
+ Object.defineProperty(exports2, "__esModule", { value: true });
611
+ 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.ControlPlaneWorkspaceDeletedV1 = exports2.ControlPlaneWorkspaceCreatedV1 = exports2.ControlPlaneRoleAssignmentDeletedV1 = exports2.ControlPlaneRoleAssignmentCreatedV1 = exports2.ControlPlaneRenameV1 = exports2.ControlPlaneRenameFailedV1 = exports2.ControlPlaneRenameCompleteV1 = exports2.ControlPlaneOwningDeleteV1 = exports2.ControlPlaneOwningDeleteFailedV1 = exports2.ControlPlaneOwningDeleteCompleteV1 = exports2.ControlPlaneMembershipDeletedV1 = exports2.ControlPlaneMembershipCreatedV1 = 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;
612
+ var envelope_version_1 = require_envelope_version();
613
+ Object.defineProperty(exports2, "ENVELOPE_VERSION", { enumerable: true, get: function() {
614
+ return envelope_version_1.ENVELOPE_VERSION;
615
+ } });
616
+ Object.defineProperty(exports2, "isSupportedEnvelopeVersion", { enumerable: true, get: function() {
617
+ return envelope_version_1.isSupportedEnvelopeVersion;
618
+ } });
619
+ var envelope_1 = require_envelope();
620
+ Object.defineProperty(exports2, "MissingActorContextError", { enumerable: true, get: function() {
621
+ return envelope_1.MissingActorContextError;
622
+ } });
623
+ Object.defineProperty(exports2, "isWorkflowSystemActor", { enumerable: true, get: function() {
624
+ return envelope_1.isWorkflowSystemActor;
625
+ } });
626
+ Object.defineProperty(exports2, "isWorkflowUserActor", { enumerable: true, get: function() {
627
+ return envelope_1.isWorkflowUserActor;
628
+ } });
629
+ Object.defineProperty(exports2, "workflowUserActorFromClaims", { enumerable: true, get: function() {
630
+ return envelope_1.workflowUserActorFromClaims;
631
+ } });
632
+ var sources_1 = require_sources();
633
+ Object.defineProperty(exports2, "DEFAULT_BUS_NAME_BY_SOURCE", { enumerable: true, get: function() {
634
+ return sources_1.DEFAULT_BUS_NAME_BY_SOURCE;
635
+ } });
636
+ Object.defineProperty(exports2, "OPENHI_CONTROL_SOURCE", { enumerable: true, get: function() {
637
+ return sources_1.OPENHI_CONTROL_SOURCE;
638
+ } });
639
+ Object.defineProperty(exports2, "OPENHI_DATA_SOURCE", { enumerable: true, get: function() {
640
+ return sources_1.OPENHI_DATA_SOURCE;
641
+ } });
642
+ Object.defineProperty(exports2, "OPENHI_OPS_SOURCE", { enumerable: true, get: function() {
643
+ return sources_1.OPENHI_OPS_SOURCE;
644
+ } });
645
+ var detail_types_1 = require_detail_types();
646
+ Object.defineProperty(exports2, "ControlPlaneMembershipCreatedV1", { enumerable: true, get: function() {
647
+ return detail_types_1.ControlPlaneMembershipCreatedV1;
648
+ } });
649
+ Object.defineProperty(exports2, "ControlPlaneMembershipDeletedV1", { enumerable: true, get: function() {
650
+ return detail_types_1.ControlPlaneMembershipDeletedV1;
651
+ } });
652
+ Object.defineProperty(exports2, "ControlPlaneOwningDeleteCompleteV1", { enumerable: true, get: function() {
653
+ return detail_types_1.ControlPlaneOwningDeleteCompleteV1;
654
+ } });
655
+ Object.defineProperty(exports2, "ControlPlaneOwningDeleteFailedV1", { enumerable: true, get: function() {
656
+ return detail_types_1.ControlPlaneOwningDeleteFailedV1;
657
+ } });
658
+ Object.defineProperty(exports2, "ControlPlaneOwningDeleteV1", { enumerable: true, get: function() {
659
+ return detail_types_1.ControlPlaneOwningDeleteV1;
660
+ } });
661
+ Object.defineProperty(exports2, "ControlPlaneRenameCompleteV1", { enumerable: true, get: function() {
662
+ return detail_types_1.ControlPlaneRenameCompleteV1;
663
+ } });
664
+ Object.defineProperty(exports2, "ControlPlaneRenameFailedV1", { enumerable: true, get: function() {
665
+ return detail_types_1.ControlPlaneRenameFailedV1;
666
+ } });
667
+ Object.defineProperty(exports2, "ControlPlaneRenameV1", { enumerable: true, get: function() {
668
+ return detail_types_1.ControlPlaneRenameV1;
669
+ } });
670
+ Object.defineProperty(exports2, "ControlPlaneRoleAssignmentCreatedV1", { enumerable: true, get: function() {
671
+ return detail_types_1.ControlPlaneRoleAssignmentCreatedV1;
672
+ } });
673
+ Object.defineProperty(exports2, "ControlPlaneRoleAssignmentDeletedV1", { enumerable: true, get: function() {
674
+ return detail_types_1.ControlPlaneRoleAssignmentDeletedV1;
675
+ } });
676
+ Object.defineProperty(exports2, "ControlPlaneWorkspaceCreatedV1", { enumerable: true, get: function() {
677
+ return detail_types_1.ControlPlaneWorkspaceCreatedV1;
678
+ } });
679
+ Object.defineProperty(exports2, "ControlPlaneWorkspaceDeletedV1", { enumerable: true, get: function() {
680
+ return detail_types_1.ControlPlaneWorkspaceDeletedV1;
681
+ } });
682
+ Object.defineProperty(exports2, "InvalidDetailTypeRegistrationError", { enumerable: true, get: function() {
683
+ return detail_types_1.InvalidDetailTypeRegistrationError;
684
+ } });
685
+ Object.defineProperty(exports2, "OWNING_ENTITY_TYPE", { enumerable: true, get: function() {
686
+ return detail_types_1.OWNING_ENTITY_TYPE;
687
+ } });
688
+ Object.defineProperty(exports2, "PlatformDeploymentCompletedV1", { enumerable: true, get: function() {
689
+ return detail_types_1.PlatformDeploymentCompletedV1;
690
+ } });
691
+ Object.defineProperty(exports2, "PlatformSystemDataSeededV1", { enumerable: true, get: function() {
692
+ return detail_types_1.PlatformSystemDataSeededV1;
693
+ } });
694
+ Object.defineProperty(exports2, "RENAMABLE_ENTITY_TYPE", { enumerable: true, get: function() {
695
+ return detail_types_1.RENAMABLE_ENTITY_TYPE;
696
+ } });
697
+ Object.defineProperty(exports2, "defineDetailType", { enumerable: true, get: function() {
698
+ return detail_types_1.defineDetailType;
699
+ } });
700
+ Object.defineProperty(exports2, "isWellFormedDetailType", { enumerable: true, get: function() {
701
+ return detail_types_1.isWellFormedDetailType;
702
+ } });
703
+ var publisher_1 = require_publisher();
704
+ Object.defineProperty(exports2, "WorkflowPublishError", { enumerable: true, get: function() {
705
+ return publisher_1.WorkflowPublishError;
706
+ } });
707
+ Object.defineProperty(exports2, "publishWorkflowEvent", { enumerable: true, get: function() {
708
+ return publisher_1.publishWorkflowEvent;
709
+ } });
710
+ Object.defineProperty(exports2, "workflowsClient", { enumerable: true, get: function() {
711
+ return publisher_1.workflowsClient;
712
+ } });
713
+ var consumer_1 = require_consumer();
714
+ Object.defineProperty(exports2, "InvalidWorkflowEventError", { enumerable: true, get: function() {
715
+ return consumer_1.InvalidWorkflowEventError;
716
+ } });
717
+ Object.defineProperty(exports2, "UnsupportedEnvelopeVersionError", { enumerable: true, get: function() {
718
+ return consumer_1.UnsupportedEnvelopeVersionError;
719
+ } });
720
+ Object.defineProperty(exports2, "parseWorkflowEvent", { enumerable: true, get: function() {
721
+ return consumer_1.parseWorkflowEvent;
722
+ } });
723
+ var dedup_1 = require_dedup();
724
+ Object.defineProperty(exports2, "WORKFLOW_DEDUP_DEFAULT_TTL_SECONDS", { enumerable: true, get: function() {
725
+ return dedup_1.WORKFLOW_DEDUP_DEFAULT_TTL_SECONDS;
726
+ } });
727
+ Object.defineProperty(exports2, "WORKFLOW_DEDUP_MAX_CONSUMER_NAME_LENGTH", { enumerable: true, get: function() {
728
+ return dedup_1.WORKFLOW_DEDUP_MAX_CONSUMER_NAME_LENGTH;
729
+ } });
730
+ Object.defineProperty(exports2, "WORKFLOW_DEDUP_TABLE_NAME_ENV_VAR", { enumerable: true, get: function() {
731
+ return dedup_1.WORKFLOW_DEDUP_TABLE_NAME_ENV_VAR;
732
+ } });
733
+ Object.defineProperty(exports2, "WorkflowDedupInvalidInputError", { enumerable: true, get: function() {
734
+ return dedup_1.WorkflowDedupInvalidInputError;
735
+ } });
736
+ Object.defineProperty(exports2, "WorkflowDedupTableNameMissingError", { enumerable: true, get: function() {
737
+ return dedup_1.WorkflowDedupTableNameMissingError;
738
+ } });
739
+ Object.defineProperty(exports2, "encodeSortKey", { enumerable: true, get: function() {
740
+ return dedup_1.encodeSortKey;
741
+ } });
742
+ Object.defineProperty(exports2, "markFailed", { enumerable: true, get: function() {
743
+ return dedup_1.markFailed;
744
+ } });
745
+ Object.defineProperty(exports2, "recordIfAbsent", { enumerable: true, get: function() {
746
+ return dedup_1.recordIfAbsent;
747
+ } });
748
+ Object.defineProperty(exports2, "workflowDedupClient", { enumerable: true, get: function() {
749
+ return dedup_1.workflowDedupClient;
750
+ } });
751
+ }
752
+ });
753
+
754
+ // src/workflows/control-plane/counter-maintenance/counter-maintenance.handler.ts
755
+ var counter_maintenance_handler_exports = {};
756
+ __export(counter_maintenance_handler_exports, {
757
+ handler: () => handler,
758
+ runCounterMaintenance: () => runCounterMaintenance
759
+ });
760
+ module.exports = __toCommonJS(counter_maintenance_handler_exports);
761
+ var import_client_dynamodb2 = require("@aws-sdk/client-dynamodb");
762
+ var import_workflows2 = __toESM(require_lib());
763
+
764
+ // src/workflows/control-plane/counter-maintenance/events.ts
765
+ var import_workflows = __toESM(require_lib());
766
+ var COUNTER_MAINTENANCE_CONSUMER_NAME = "counter-maintenance";
767
+ var COUNTER_MAINTENANCE_DETAIL_TYPES = [
768
+ import_workflows.ControlPlaneMembershipCreatedV1.detailType,
769
+ import_workflows.ControlPlaneMembershipDeletedV1.detailType,
770
+ import_workflows.ControlPlaneRoleAssignmentCreatedV1.detailType,
771
+ import_workflows.ControlPlaneRoleAssignmentDeletedV1.detailType,
772
+ import_workflows.ControlPlaneWorkspaceCreatedV1.detailType,
773
+ import_workflows.ControlPlaneWorkspaceDeletedV1.detailType
774
+ ];
775
+
776
+ // src/data/dynamo/dynamo-control-service.ts
777
+ var import_electrodb14 = require("electrodb");
778
+
779
+ // src/data/dynamo/dynamo-client.ts
780
+ var import_client_dynamodb = require("@aws-sdk/client-dynamodb");
781
+ var defaultTableName = process.env.DYNAMO_TABLE_NAME ?? "jesttesttable";
782
+ var dynamoClient = new import_client_dynamodb.DynamoDBClient({
783
+ ...process.env.MOCK_DYNAMODB_ENDPOINT && {
784
+ endpoint: process.env.MOCK_DYNAMODB_ENDPOINT,
785
+ sslEnabled: false,
786
+ region: "local"
787
+ }
788
+ });
789
+
790
+ // src/data/dynamo/entities/control/configuration-entity.ts
791
+ var import_electrodb = require("electrodb");
792
+
793
+ // src/data/dynamo/entities/control/control-entity-common.ts
794
+ var import_types = require("@openhi/types");
795
+
796
+ // src/data/dynamo/shard.ts
797
+ var SHARD_COUNT = 4;
798
+ function computeShard(id) {
799
+ let hash = 2166136261;
800
+ for (let i = 0; i < id.length; i++) {
801
+ hash ^= id.charCodeAt(i);
802
+ hash = Math.imul(hash, 16777619);
803
+ }
804
+ return (hash >>> 0) % SHARD_COUNT;
805
+ }
806
+
807
+ // src/data/dynamo/entities/control/control-entity-common.ts
808
+ var gsi1ShardAttribute = {
809
+ type: "string",
810
+ watch: ["id"],
811
+ set: (_val, item) => {
812
+ if (typeof item?.id !== "string" || item.id.length === 0) {
813
+ return void 0;
814
+ }
815
+ return String(computeShard(item.id));
816
+ }
817
+ };
818
+ var gsi1skAttribute = {
819
+ type: "string",
820
+ watch: ["resource", "lastUpdated", "id"],
821
+ set: (_val, item) => {
822
+ const id = typeof item?.id === "string" ? item.id : "";
823
+ const lastUpdated = typeof item?.lastUpdated === "string" ? item.lastUpdated : "";
824
+ const fallback = `${lastUpdated}#${id}`;
825
+ if (typeof item?.resource !== "string" || item.resource.length === 0) {
826
+ return fallback;
827
+ }
828
+ let parsed;
829
+ try {
830
+ parsed = JSON.parse(item.resource);
831
+ } catch {
832
+ return fallback;
833
+ }
834
+ if (!parsed || typeof parsed !== "object") return fallback;
835
+ const resourceType = parsed.resourceType;
836
+ if (typeof resourceType !== "string") return fallback;
837
+ const label = (0, import_types.extractLabel)(parsed);
838
+ return label !== void 0 ? `${label}#${id}` : fallback;
839
+ }
840
+ };
841
+ function extractRoleId(resource) {
842
+ const flat = resource.roleId;
843
+ if (typeof flat === "string" && flat.length > 0) return flat;
844
+ const role = resource.role;
845
+ if (role && typeof role === "object") {
846
+ const reference = role.reference;
847
+ if (typeof reference === "string" && reference.length > 0) {
848
+ const slash = reference.lastIndexOf("/");
849
+ const tail = slash >= 0 ? reference.slice(slash + 1) : reference;
850
+ if (tail.length > 0) return tail;
851
+ }
852
+ }
853
+ return void 0;
854
+ }
855
+ var roleAssignmentGsi1skAttribute = {
856
+ type: "string",
857
+ watch: ["resource", "denormalizedUserName", "lastUpdated", "id"],
858
+ set: (_val, item) => {
859
+ const id = typeof item?.id === "string" ? item.id : "";
860
+ const lastUpdated = typeof item?.lastUpdated === "string" ? item.lastUpdated : "";
861
+ const fallback = `${lastUpdated}#${id}`;
862
+ if (typeof item?.resource !== "string" || item.resource.length === 0) {
863
+ return fallback;
864
+ }
865
+ let parsed;
866
+ try {
867
+ parsed = JSON.parse(item.resource);
868
+ } catch {
869
+ return fallback;
870
+ }
871
+ if (!parsed || typeof parsed !== "object") return fallback;
872
+ const roleId = extractRoleId(parsed);
873
+ if (roleId === void 0) return fallback;
874
+ const denormalizedUserName = typeof item.denormalizedUserName === "string" ? item.denormalizedUserName : "";
875
+ const normalizedUserName = denormalizedUserName.length > 0 ? (0, import_types.normalizeLabel)(denormalizedUserName) : "";
876
+ if (normalizedUserName.length === 0) return fallback;
877
+ return `${roleId}#${normalizedUserName}#${id}`;
878
+ }
879
+ };
880
+ var membershipGsi1skAttribute = {
881
+ type: "string",
882
+ watch: ["denormalizedUserName", "lastUpdated", "id"],
883
+ set: (_val, item) => {
884
+ const id = typeof item?.id === "string" ? item.id : "";
885
+ const lastUpdated = typeof item?.lastUpdated === "string" ? item.lastUpdated : "";
886
+ const fallback = `${lastUpdated}#${id}`;
887
+ const denormalizedUserName = typeof item?.denormalizedUserName === "string" ? item.denormalizedUserName : "";
888
+ const normalizedUserName = denormalizedUserName.length > 0 ? (0, import_types.normalizeLabel)(denormalizedUserName) : "";
889
+ if (normalizedUserName.length === 0) {
890
+ return fallback;
891
+ }
892
+ return `${normalizedUserName}#${id}`;
893
+ }
894
+ };
895
+
896
+ // src/data/dynamo/entities/control/configuration-entity.ts
897
+ var ConfigurationEntity = new import_electrodb.Entity({
898
+ model: {
899
+ entity: "configuration",
900
+ service: "control",
901
+ version: "01"
902
+ },
903
+ attributes: {
904
+ /** Sort key. "CURRENT" for current version; version history in S3. */
905
+ sk: {
906
+ type: "string",
907
+ required: true,
908
+ default: "CURRENT"
909
+ },
910
+ /** Tenant scope. Use "BASELINE" when the config is baseline default (no tenant). */
911
+ tenantId: {
912
+ type: "string",
913
+ required: true,
914
+ default: "BASELINE"
915
+ },
916
+ /** Workspace scope. Use "-" when absent. */
917
+ workspaceId: {
918
+ type: "string",
919
+ required: true,
920
+ default: "-"
921
+ },
922
+ /** User scope. Use "-" when absent. */
923
+ userId: {
924
+ type: "string",
925
+ required: true,
926
+ default: "-"
927
+ },
928
+ /** Role scope. Use "-" when absent. */
929
+ roleId: {
930
+ type: "string",
931
+ required: true,
932
+ default: "-"
933
+ },
934
+ /** Config type (category), e.g. endpoints, branding, display. */
935
+ key: {
936
+ type: "string",
937
+ required: true
938
+ },
939
+ /** FHIR Resource.id; logical id in URL and for the Configuration resource. */
940
+ id: {
941
+ type: "string",
942
+ required: true
943
+ },
944
+ /** Payload as JSON string. JSON.stringify(resource) on write; JSON.parse(item.resource) on read. */
945
+ resource: {
946
+ type: "string",
947
+ required: true
948
+ },
949
+ /**
950
+ * Summary projection (key display fields as JSON string: id, key, status).
951
+ * Populated on every write via extractSummary(resource); GSI1 INCLUDE surfaces it on lists.
952
+ */
953
+ summary: {
954
+ type: "string",
955
+ required: true
956
+ },
957
+ /** Version id (e.g. ULID). Tracks current version; S3 history key. */
958
+ vid: {
959
+ type: "string",
960
+ required: true
961
+ },
962
+ lastUpdated: {
963
+ type: "string",
964
+ required: true
965
+ },
966
+ gsi1Shard: gsi1ShardAttribute,
967
+ deleted: {
968
+ type: "boolean",
969
+ required: false
970
+ },
971
+ bundleId: {
972
+ type: "string",
973
+ required: false
974
+ },
975
+ msgId: {
976
+ type: "string",
977
+ required: false
978
+ }
979
+ },
980
+ indexes: {
981
+ /** Base table: PK, SK (data store key names). PK is built from tenantId, workspaceId, userId, roleId; SK is built from key and sk. Do not supply PK or SK from outside. */
982
+ record: {
983
+ pk: {
984
+ field: "PK",
985
+ composite: ["tenantId", "workspaceId", "userId", "roleId"],
986
+ template: "CONFIG#TID#${tenantId}#WID#${workspaceId}#UID#${userId}#RID#${roleId}"
987
+ },
988
+ sk: {
989
+ field: "SK",
990
+ composite: ["key", "sk"],
991
+ template: "KEY#${key}#SK#${sk}"
992
+ }
993
+ },
994
+ /**
995
+ * GSI1 — Unified Sharded List per ADR-011: list all Configuration entries for a
996
+ * (tenant, workspace) across the four shards. Use for "list configs scoped to this tenant"
997
+ * (workspaceId = "-") or "list configs scoped to this workspace". Does not support
998
+ * hierarchical resolution in one query; use base table GetItem in fallback order
999
+ * (user → workspace → tenant → baseline) for that.
1000
+ * SK is `<key>#<id>` — Configuration's `key` is a required entity attribute (the
1001
+ * config category: endpoints, branding, display, …) and the natural sort/lookup
1002
+ * dimension. `casing: "none"` preserves the literal key value.
1003
+ */
1004
+ gsi1: {
1005
+ index: "GSI1",
1006
+ pk: {
1007
+ field: "GSI1PK",
1008
+ composite: ["tenantId", "workspaceId", "gsi1Shard"],
1009
+ template: "TID#${tenantId}#WID#${workspaceId}#RT#Configuration#SHARD#${gsi1Shard}"
1010
+ },
1011
+ sk: {
1012
+ field: "GSI1SK",
1013
+ casing: "none",
1014
+ composite: ["key", "id"],
1015
+ template: "${key}#${id}"
1016
+ }
1017
+ }
1018
+ }
1019
+ });
1020
+
1021
+ // src/data/dynamo/entities/control/configuration-user-projection-entity.ts
1022
+ var import_electrodb2 = require("electrodb");
1023
+ var ConfigurationUserProjectionEntity = new import_electrodb2.Entity({
1024
+ model: {
1025
+ entity: "configurationUserProjection",
1026
+ service: "control",
1027
+ version: "01"
1028
+ },
1029
+ attributes: {
1030
+ /**
1031
+ * User partition discriminator. Renders as `USER#ID#<userId>` on the
1032
+ * base-table PK. Always required — the projection has no meaning
1033
+ * outside a user partition.
1034
+ */
1035
+ userId: {
1036
+ type: "string",
1037
+ required: true
1038
+ },
1039
+ /**
1040
+ * Pre-composed sort key — built by the operations-layer projection
1041
+ * writer via `buildConfigurationUserProjectionSk`. The entity stores
1042
+ * the value verbatim so the SK grammar (pattern #10 user-scope) is
1043
+ * owned by the operations layer, not duplicated here.
1044
+ */
1045
+ sk: {
1046
+ type: "string",
1047
+ required: true
1048
+ },
1049
+ /**
1050
+ * Configuration canonical-record id. Stored as a discriminating
1051
+ * field so consumers can hydrate the canonical row via the
1052
+ * Configuration get-by-id operation when the projection's `summary`
1053
+ * is insufficient.
1054
+ */
1055
+ configurationId: {
1056
+ type: "string",
1057
+ required: true
1058
+ },
1059
+ /**
1060
+ * Tenant the Configuration is associated with. The canonical row
1061
+ * keys off `(tenantId, workspaceId, userId, roleId)`; the projection
1062
+ * carries `tenantId` so consumers reconstructing the canonical PK
1063
+ * have the tenant segment without a hop.
1064
+ */
1065
+ tenantId: {
1066
+ type: "string",
1067
+ required: true
1068
+ },
1069
+ /**
1070
+ * Scope marker. Always `"user"` on this projection — recorded
1071
+ * explicitly so future scope-bearing projections (workspace,
1072
+ * tenant, role) can share filter semantics in a unified
1073
+ * cross-projection list query if one ever lands.
1074
+ */
1075
+ scope: {
1076
+ type: "string",
1077
+ required: true,
1078
+ default: "user"
1079
+ },
1080
+ /**
1081
+ * Configuration's `key` attribute (config category, e.g. endpoints,
1082
+ * branding, display). Mirrored from the canonical row so consumers
1083
+ * reading the projection get the natural display label without a
1084
+ * BatchGet hop. Doubles as the source of `<normalizedConfigName>` in
1085
+ * the SK.
1086
+ */
1087
+ displayName: {
1088
+ type: "string",
1089
+ required: false
1090
+ },
1091
+ /**
1092
+ * Summary projection (key display fields as JSON string) — mirrored
1093
+ * from the canonical Configuration row so user-partition queries do
1094
+ * not need a BatchGet hop.
1095
+ */
1096
+ summary: {
1097
+ type: "string",
1098
+ required: true
1099
+ },
1100
+ /** Version id mirrored from the canonical Configuration row. */
1101
+ vid: {
1102
+ type: "string",
1103
+ required: true
1104
+ },
1105
+ /** Last-updated timestamp mirrored from the canonical Configuration row. */
1106
+ lastUpdated: {
1107
+ type: "string",
1108
+ required: true
1109
+ }
1110
+ },
1111
+ indexes: {
1112
+ /**
1113
+ * Base table: PK = USER#ID#\<userId\>, SK = operation-supplied. A
1114
+ * single `Query(PK = USER#ID#<userId>, SK begins_with 'CONFIGURATION#')`
1115
+ * returns the user's user-scoped Configurations sorted by
1116
+ * `<normalizedConfigName>` (then `<configurationId>` as the
1117
+ * tiebreaker).
1118
+ */
1119
+ record: {
1120
+ pk: {
1121
+ field: "PK",
1122
+ composite: ["userId"],
1123
+ template: "USER#ID#${userId}"
1124
+ },
1125
+ sk: {
1126
+ field: "SK",
1127
+ casing: "none",
1128
+ composite: ["sk"],
1129
+ template: "${sk}"
1130
+ }
1131
+ }
1132
+ }
1133
+ });
1134
+
1135
+ // src/data/dynamo/entities/control/configuration-workspace-projection-entity.ts
1136
+ var import_electrodb3 = require("electrodb");
1137
+ var ConfigurationWorkspaceProjectionEntity = new import_electrodb3.Entity({
1138
+ model: {
1139
+ entity: "configurationWorkspaceProjection",
1140
+ service: "control",
1141
+ version: "01"
1142
+ },
1143
+ attributes: {
1144
+ /**
1145
+ * Tenant the workspace belongs to. Renders as the leading segment
1146
+ * of the base-table PK. Always required — the workspace partition
1147
+ * is tenant-scoped per ADR-011.
1148
+ */
1149
+ tenantId: {
1150
+ type: "string",
1151
+ required: true
1152
+ },
1153
+ /**
1154
+ * Workspace partition discriminator. Renders as the trailing
1155
+ * segment of the base-table PK
1156
+ * (`TID#<tenantId>#WORKSPACE#ID#<workspaceId>`). Always required —
1157
+ * the projection has no meaning outside a workspace partition.
1158
+ */
1159
+ workspaceId: {
1160
+ type: "string",
1161
+ required: true
1162
+ },
1163
+ /**
1164
+ * Pre-composed sort key — built by the operations-layer projection
1165
+ * writer via `buildConfigurationWorkspaceProjectionSk`. The entity
1166
+ * stores the value verbatim so the SK grammar (pattern #10
1167
+ * workspace-scope) is owned by the operations layer, not
1168
+ * duplicated here.
1169
+ */
1170
+ sk: {
1171
+ type: "string",
1172
+ required: true
1173
+ },
1174
+ /**
1175
+ * Configuration canonical-record id. Stored as a discriminating
1176
+ * field so consumers can hydrate the canonical row via the
1177
+ * Configuration get-by-id operation when the projection's `summary`
1178
+ * is insufficient.
1179
+ */
1180
+ configurationId: {
1181
+ type: "string",
1182
+ required: true
1183
+ },
1184
+ /**
1185
+ * Scope marker. Always `"workspace"` on this projection — recorded
1186
+ * explicitly so future scope-bearing projections (user, tenant,
1187
+ * role) can share filter semantics in a unified cross-projection
1188
+ * list query if one ever lands.
1189
+ */
1190
+ scope: {
1191
+ type: "string",
1192
+ required: true,
1193
+ default: "workspace"
1194
+ },
1195
+ /**
1196
+ * Configuration's `key` attribute (config category, e.g. endpoints,
1197
+ * branding, display). Mirrored from the canonical row so consumers
1198
+ * reading the projection get the natural display label without a
1199
+ * BatchGet hop. Doubles as the source of `<normalizedConfigName>`
1200
+ * in the SK.
1201
+ */
1202
+ displayName: {
1203
+ type: "string",
1204
+ required: false
1205
+ },
1206
+ /**
1207
+ * Summary projection (key display fields as JSON string) — mirrored
1208
+ * from the canonical Configuration row so workspace-partition
1209
+ * queries do not need a BatchGet hop.
1210
+ */
1211
+ summary: {
1212
+ type: "string",
1213
+ required: true
1214
+ },
1215
+ /** Version id mirrored from the canonical Configuration row. */
1216
+ vid: {
1217
+ type: "string",
1218
+ required: true
1219
+ },
1220
+ /** Last-updated timestamp mirrored from the canonical Configuration row. */
1221
+ lastUpdated: {
1222
+ type: "string",
1223
+ required: true
1224
+ }
1225
+ },
1226
+ indexes: {
1227
+ /**
1228
+ * Base table: PK = TID#<tenantId>#WORKSPACE#ID#<workspaceId>,
1229
+ * SK = operation-supplied. A single
1230
+ * `Query(PK = TID#<tenantId>#WORKSPACE#ID#<workspaceId>, SK begins_with 'CONFIGURATION#')`
1231
+ * returns the workspace's workspace-scoped Configurations sorted by
1232
+ * `<normalizedConfigName>` (then `<configurationId>` as the
1233
+ * tiebreaker).
1234
+ */
1235
+ record: {
1236
+ pk: {
1237
+ field: "PK",
1238
+ composite: ["tenantId", "workspaceId"],
1239
+ template: "TID#${tenantId}#WORKSPACE#ID#${workspaceId}"
1240
+ },
1241
+ sk: {
1242
+ field: "SK",
1243
+ casing: "none",
1244
+ composite: ["sk"],
1245
+ template: "${sk}"
1246
+ }
1247
+ }
1248
+ }
1249
+ });
1250
+
1251
+ // src/data/dynamo/entities/control/membership-entity.ts
1252
+ var import_electrodb4 = require("electrodb");
1253
+ var MembershipEntity = new import_electrodb4.Entity({
1254
+ model: {
1255
+ entity: "membership",
1256
+ service: "control",
1257
+ version: "01"
1258
+ },
1259
+ attributes: {
1260
+ /** Sort key sentinel. Always "CURRENT". */
1261
+ sk: {
1262
+ type: "string",
1263
+ required: true,
1264
+ default: "CURRENT"
1265
+ },
1266
+ /** Tenant in which the user has membership (required). */
1267
+ tenantId: {
1268
+ type: "string",
1269
+ required: true
1270
+ },
1271
+ /** FHIR Resource.id; membership id. */
1272
+ id: {
1273
+ type: "string",
1274
+ required: true
1275
+ },
1276
+ /** Full Membership resource serialized as JSON string. */
1277
+ resource: {
1278
+ type: "string",
1279
+ required: true
1280
+ },
1281
+ /**
1282
+ * Summary projection (key display fields as JSON string: id, displayName, status).
1283
+ * Populated on every write via extractSummary(resource); GSI1 INCLUDE surfaces it on lists.
1284
+ */
1285
+ summary: {
1286
+ type: "string",
1287
+ required: true
1288
+ },
1289
+ /** Version id (e.g. ULID). */
1290
+ vid: {
1291
+ type: "string",
1292
+ required: true
1293
+ },
1294
+ lastUpdated: {
1295
+ type: "string",
1296
+ required: true
1297
+ },
1298
+ gsi1Shard: gsi1ShardAttribute,
1299
+ /**
1300
+ * Derived GSI1 sort key — `<normalizedUserName>#<id>` per ADR-018
1301
+ * pattern #1 so a GSI1 query partitioned on the tenant range-scans
1302
+ * by user-name prefix and returns memberships sorted by user name.
1303
+ * Falls back to `<lastUpdated>#<id>` when `denormalizedUserName`
1304
+ * is missing.
1305
+ */
1306
+ gsi1sk: membershipGsi1skAttribute,
1307
+ deleted: {
1308
+ type: "boolean",
1309
+ required: false
1310
+ },
1311
+ bundleId: {
1312
+ type: "string",
1313
+ required: false
1314
+ },
1315
+ msgId: {
1316
+ type: "string",
1317
+ required: false
1318
+ },
1319
+ /**
1320
+ * Denormalized `linked-data-identity` Reference (e.g. `Practitioner/abc`).
1321
+ * Populated from the FHIR extension on the Membership resource at write
1322
+ * time so future GSIs can index data-plane identity lookups without
1323
+ * deserializing the full resource JSON. See ADR 2026-03-13-02 §6.
1324
+ */
1325
+ linkedDataIdentityRef: {
1326
+ type: "string",
1327
+ required: false
1328
+ },
1329
+ /**
1330
+ * Denormalized display name of the linked Tenant, captured at row
1331
+ * last-write time. Promoted to a top-level attribute so the ADR-018
1332
+ * adjacency-list projection SKs (pattern #3 — `MEMBERSHIP#TENANT#<normalizedTenantName>#…`)
1333
+ * can be composed from a top-level field instead of digging into the
1334
+ * `resource` JSON. Optional on the schema so pre-TR-024 rows do not
1335
+ * break; the operations-layer multi-write helper (#1010) makes the
1336
+ * field load-bearing at write time per TR-024 rule 2 (write-time
1337
+ * source = canonical Tenant.displayName).
1338
+ * @see TR-024 — Denormalized display-name attributes
1339
+ */
1340
+ denormalizedTenantName: {
1341
+ type: "string",
1342
+ required: false
1343
+ },
1344
+ /**
1345
+ * Denormalized display name of the linked User, captured at row
1346
+ * last-write time. Promoted to a top-level attribute so the ADR-018
1347
+ * adjacency-list canonical-record GSI1SK (pattern #1 —
1348
+ * `<normalizedUserName>#<id>`) and workspace-projection SK (pattern #2)
1349
+ * can be composed from a top-level field. Optional on the schema so
1350
+ * pre-TR-024 rows do not break; the operations-layer multi-write helper
1351
+ * (#1010) makes the field load-bearing at write time per TR-024 rule 2
1352
+ * (write-time source = canonical User.displayName).
1353
+ * @see TR-024 — Denormalized display-name attributes
1354
+ */
1355
+ denormalizedUserName: {
1356
+ type: "string",
1357
+ required: false
1358
+ }
1359
+ },
1360
+ indexes: {
1361
+ /** Base table: PK = TID#<tenantId>#MEMBERSHIP#ID#<id>, SK = CURRENT. Do not supply PK or SK from outside. */
1362
+ record: {
1363
+ pk: {
1364
+ field: "PK",
1365
+ composite: ["tenantId", "id"],
1366
+ template: "TID#${tenantId}#MEMBERSHIP#ID#${id}"
1367
+ },
1368
+ sk: {
1369
+ field: "SK",
1370
+ composite: ["sk"],
1371
+ template: "${sk}"
1372
+ }
1373
+ },
1374
+ /**
1375
+ * GSI1 — Unified Sharded List per ADR-011: list all Memberships for a tenant across the
1376
+ * four shards. Membership is tenant-scoped only, so `WID#-` is a sentinel.
1377
+ * SK is derived via `membershipGsi1skAttribute` — composes
1378
+ * `<normalizedUserName>#<id>` per ADR-018 pattern #1 (users in a
1379
+ * tenant, sorted by user name); falls back to `<lastUpdated>#<id>`
1380
+ * when `denormalizedUserName` is missing. `casing: "none"` preserves
1381
+ * the normalized label and ISO-8601 `T`/`Z`.
1382
+ */
1383
+ gsi1: {
1384
+ index: "GSI1",
1385
+ pk: {
1386
+ field: "GSI1PK",
1387
+ composite: ["tenantId", "gsi1Shard"],
1388
+ template: "TID#${tenantId}#WID#-#RT#Membership#SHARD#${gsi1Shard}"
1389
+ },
1390
+ sk: {
1391
+ field: "GSI1SK",
1392
+ casing: "none",
1393
+ composite: ["gsi1sk"],
1394
+ template: "${gsi1sk}"
1395
+ }
1396
+ }
1397
+ }
1398
+ });
1399
+
1400
+ // src/data/dynamo/entities/control/membership-user-projection-entity.ts
1401
+ var import_electrodb5 = require("electrodb");
1402
+ var MembershipUserProjectionEntity = new import_electrodb5.Entity({
1403
+ model: {
1404
+ entity: "membershipUserProjection",
1405
+ service: "control",
1406
+ version: "01"
1407
+ },
1408
+ attributes: {
1409
+ /**
1410
+ * User partition discriminator. Renders as `USER#ID#<userId>` on the
1411
+ * base-table PK. Always required — the projection has no meaning
1412
+ * outside a user partition.
1413
+ */
1414
+ userId: {
1415
+ type: "string",
1416
+ required: true
1417
+ },
1418
+ /**
1419
+ * Pre-composed sort key — built by the operations-layer projection
1420
+ * writer via `buildMembershipUserProjectionSk*` helpers. The entity
1421
+ * stores the value verbatim so the SK grammar (patterns #3 and #4)
1422
+ * is owned by the operations layer, not duplicated here.
1423
+ */
1424
+ sk: {
1425
+ type: "string",
1426
+ required: true
1427
+ },
1428
+ /** Tenant in which the membership applies. Always required. */
1429
+ tenantId: {
1430
+ type: "string",
1431
+ required: true
1432
+ },
1433
+ /**
1434
+ * Workspace the membership scopes to. Present iff the projection
1435
+ * row is a pattern-#4 workspace sub-lane row; absent for pattern-#3
1436
+ * tenant sub-lane rows.
1437
+ */
1438
+ workspaceId: {
1439
+ type: "string",
1440
+ required: false
1441
+ },
1442
+ /**
1443
+ * Membership canonical-record id. Stored as a discriminating field
1444
+ * so consumers can hydrate the canonical row via
1445
+ * `MembershipEntity.get({ tenantId, id: membershipId })` when the
1446
+ * projection's `summary` is insufficient.
1447
+ */
1448
+ membershipId: {
1449
+ type: "string",
1450
+ required: true
1451
+ },
1452
+ /**
1453
+ * Summary projection (key display fields as JSON string: id,
1454
+ * displayName, status) — mirrored from the canonical Membership row
1455
+ * so user-partition queries do not need a BatchGet hop.
1456
+ */
1457
+ summary: {
1458
+ type: "string",
1459
+ required: true
1460
+ },
1461
+ /** Version id mirrored from the canonical Membership row. */
1462
+ vid: {
1463
+ type: "string",
1464
+ required: true
1465
+ },
1466
+ /** Last-updated timestamp mirrored from the canonical Membership row. */
1467
+ lastUpdated: {
1468
+ type: "string",
1469
+ required: true
1470
+ },
1471
+ /**
1472
+ * Denormalized Tenant display name — required to compose pattern-#3
1473
+ * SK (`MEMBERSHIP#TENANT#<normalizedTenantName>#…`). Optional on the
1474
+ * schema because pre-TR-024 rows may not carry a display name; the
1475
+ * operations layer falls back gracefully when missing.
1476
+ */
1477
+ denormalizedTenantName: {
1478
+ type: "string",
1479
+ required: false
1480
+ },
1481
+ /**
1482
+ * Denormalized User display name — mirrored from the canonical
1483
+ * Membership row per TR-024 rule 3 (canonical-record symmetry).
1484
+ * Carried on the projection so consumers can render the user's
1485
+ * display name without a hop to the User record.
1486
+ */
1487
+ denormalizedUserName: {
1488
+ type: "string",
1489
+ required: false
1490
+ },
1491
+ /**
1492
+ * Denormalized Workspace display name — required to compose
1493
+ * pattern-#4 SK (`MEMBERSHIP#WORKSPACE#TID#<tenantId>#<normalizedWorkspaceName>#…`).
1494
+ * Optional on the schema (TR-024 § Open Item #4 defers a formal
1495
+ * Workspace-rename cascade); the operations layer falls back to a
1496
+ * sentinel when missing so the SK still has a valid shape.
1497
+ */
1498
+ denormalizedWorkspaceName: {
1499
+ type: "string",
1500
+ required: false
1501
+ }
1502
+ },
1503
+ indexes: {
1504
+ /**
1505
+ * Base table: PK = USER#ID#\<userId\>, SK = operation-supplied.
1506
+ * Both pattern #3 and pattern #4 use this same index — the SK string
1507
+ * encodes the lane discriminator (`MEMBERSHIP#TENANT#…` vs
1508
+ * `MEMBERSHIP#WORKSPACE#…`) so a single
1509
+ * `Query(PK = USER#ID#<userId>, SK begins_with 'MEMBERSHIP#')`
1510
+ * returns both lanes interleaved.
1511
+ */
1512
+ record: {
1513
+ pk: {
1514
+ field: "PK",
1515
+ composite: ["userId"],
1516
+ template: "USER#ID#${userId}"
1517
+ },
1518
+ sk: {
1519
+ field: "SK",
1520
+ casing: "none",
1521
+ composite: ["sk"],
1522
+ template: "${sk}"
1523
+ }
1524
+ }
1525
+ }
1526
+ });
1527
+
1528
+ // src/data/dynamo/entities/control/membership-workspace-projection-entity.ts
1529
+ var import_electrodb6 = require("electrodb");
1530
+ var MembershipWorkspaceProjectionEntity = new import_electrodb6.Entity({
1531
+ model: {
1532
+ entity: "membershipWorkspaceProjection",
1533
+ service: "control",
1534
+ version: "01"
1535
+ },
1536
+ attributes: {
1537
+ /**
1538
+ * Tenant the workspace belongs to. Renders as the leading segment
1539
+ * of the base-table PK. Always required — the workspace partition
1540
+ * is tenant-scoped per ADR-011.
1541
+ */
1542
+ tenantId: {
1543
+ type: "string",
1544
+ required: true
1545
+ },
1546
+ /**
1547
+ * Workspace partition discriminator. Renders as the trailing
1548
+ * segment of the base-table PK
1549
+ * (`TID#<tenantId>#WORKSPACE#ID#<workspaceId>`). Always required —
1550
+ * the projection has no meaning outside a workspace partition.
1551
+ */
1552
+ workspaceId: {
1553
+ type: "string",
1554
+ required: true
1555
+ },
1556
+ /**
1557
+ * Pre-composed sort key — built by the operations-layer projection
1558
+ * writer via `buildMembershipWorkspaceProjectionSk`. The entity
1559
+ * stores the value verbatim so the SK grammar (pattern #2) is
1560
+ * owned by the operations layer, not duplicated here.
1561
+ */
1562
+ sk: {
1563
+ type: "string",
1564
+ required: true
1565
+ },
1566
+ /**
1567
+ * User the membership links. Stored as a discriminating field so
1568
+ * consumers can hydrate the canonical User row via
1569
+ * `UserEntity.get({ id: userId, sk: "CURRENT" })` when the
1570
+ * projection's `summary` is insufficient.
1571
+ */
1572
+ userId: {
1573
+ type: "string",
1574
+ required: true
1575
+ },
1576
+ /**
1577
+ * Membership canonical-record id. Stored as a discriminating field
1578
+ * so consumers can hydrate the canonical row via
1579
+ * `MembershipEntity.get({ tenantId, id: membershipId })` when the
1580
+ * projection's `summary` is insufficient.
1581
+ */
1582
+ membershipId: {
1583
+ type: "string",
1584
+ required: true
1585
+ },
1586
+ /**
1587
+ * Summary projection (key display fields as JSON string: id,
1588
+ * displayName, status) — mirrored from the canonical Membership row
1589
+ * so workspace-partition queries do not need a BatchGet hop.
1590
+ */
1591
+ summary: {
1592
+ type: "string",
1593
+ required: true
1594
+ },
1595
+ /** Version id mirrored from the canonical Membership row. */
1596
+ vid: {
1597
+ type: "string",
1598
+ required: true
1599
+ },
1600
+ /** Last-updated timestamp mirrored from the canonical Membership row. */
1601
+ lastUpdated: {
1602
+ type: "string",
1603
+ required: true
1604
+ },
1605
+ /**
1606
+ * Denormalized User display name — required to compose the
1607
+ * pattern-#2 SK (`MEMBERSHIP#<normalizedUserName>#…`). Optional on
1608
+ * the schema because pre-TR-024 rows may not carry a display name;
1609
+ * the operations layer falls back to a sentinel when missing so
1610
+ * the SK still has a valid shape. The TR-023 rename-cascade
1611
+ * pipeline rewrites the SK on a User rename.
1612
+ */
1613
+ denormalizedUserName: {
1614
+ type: "string",
1615
+ required: false
1616
+ }
1617
+ },
1618
+ indexes: {
1619
+ /**
1620
+ * Base table: PK = TID#<tenantId>#WORKSPACE#ID#<workspaceId>,
1621
+ * SK = operation-supplied. Pattern #2 uses this index — the SK
1622
+ * encodes the entity-type prefix (`MEMBERSHIP#…`) so a
1623
+ * `Query(PK = TID#<tenantId>#WORKSPACE#ID#<workspaceId>, SK begins_with 'MEMBERSHIP#')`
1624
+ * returns every member projection for the workspace in normalized-
1625
+ * user-name sort order.
1626
+ */
1627
+ record: {
1628
+ pk: {
1629
+ field: "PK",
1630
+ composite: ["tenantId", "workspaceId"],
1631
+ template: "TID#${tenantId}#WORKSPACE#ID#${workspaceId}"
1632
+ },
1633
+ sk: {
1634
+ field: "SK",
1635
+ casing: "none",
1636
+ composite: ["sk"],
1637
+ template: "${sk}"
1638
+ }
1639
+ }
1640
+ }
1641
+ });
1642
+
1643
+ // src/data/dynamo/entities/control/role-entity.ts
1644
+ var import_electrodb7 = require("electrodb");
1645
+ var RoleEntity = new import_electrodb7.Entity({
1646
+ model: {
1647
+ entity: "role",
1648
+ service: "control",
1649
+ version: "01"
1650
+ },
1651
+ attributes: {
1652
+ /** Sort key sentinel. Always "CURRENT". */
1653
+ sk: {
1654
+ type: "string",
1655
+ required: true,
1656
+ default: "CURRENT"
1657
+ },
1658
+ /** FHIR Resource.id; role id. */
1659
+ id: {
1660
+ type: "string",
1661
+ required: true
1662
+ },
1663
+ /** Full Role resource serialized as JSON string. */
1664
+ resource: {
1665
+ type: "string",
1666
+ required: true
1667
+ },
1668
+ /**
1669
+ * Summary projection (key display fields as JSON string: id, displayName, status).
1670
+ * Populated on every write via extractSummary(resource); GSI1 INCLUDE surfaces it on lists.
1671
+ */
1672
+ summary: {
1673
+ type: "string",
1674
+ required: true
1675
+ },
1676
+ /** Version id (e.g. ULID). */
1677
+ vid: {
1678
+ type: "string",
1679
+ required: true
1680
+ },
1681
+ lastUpdated: {
1682
+ type: "string",
1683
+ required: true
1684
+ },
1685
+ gsi1Shard: gsi1ShardAttribute,
1686
+ /** Derived GSI1 sort key — name-based when extractable; else `<lastUpdated>#<id>`. */
1687
+ gsi1sk: gsi1skAttribute,
1688
+ deleted: {
1689
+ type: "boolean",
1690
+ required: false
1691
+ },
1692
+ bundleId: {
1693
+ type: "string",
1694
+ required: false
1695
+ },
1696
+ msgId: {
1697
+ type: "string",
1698
+ required: false
1699
+ }
1700
+ },
1701
+ indexes: {
1702
+ /** Base table: PK = ROLE#ID#<id>, SK = CURRENT. Do not supply PK or SK from outside. */
1703
+ record: {
1704
+ pk: {
1705
+ field: "PK",
1706
+ composite: ["id"],
1707
+ template: "ROLE#ID#${id}"
1708
+ },
1709
+ sk: {
1710
+ field: "SK",
1711
+ composite: ["sk"],
1712
+ template: "${sk}"
1713
+ }
1714
+ },
1715
+ /**
1716
+ * GSI1 — Unified Sharded List per ADR-011: list all Roles across the four shards.
1717
+ * Non-tenant-isolated, so `TID#-#WID#-` sentinels precede `RT#Role#SHARD#<n>`.
1718
+ * SK is derived via `gsi1skAttribute` — uses the resource's natural label when
1719
+ * extractable, else `<lastUpdated>#<id>` (DR-004). `casing: "none"` preserves the
1720
+ * normalized label and ISO-8601 `T`/`Z`.
1721
+ */
1722
+ gsi1: {
1723
+ index: "GSI1",
1724
+ pk: {
1725
+ field: "GSI1PK",
1726
+ composite: ["gsi1Shard"],
1727
+ template: "TID#-#WID#-#RT#Role#SHARD#${gsi1Shard}"
1728
+ },
1729
+ sk: {
1730
+ field: "GSI1SK",
1731
+ casing: "none",
1732
+ composite: ["gsi1sk"],
1733
+ template: "${gsi1sk}"
1734
+ }
1735
+ }
1736
+ }
1737
+ });
1738
+
1739
+ // src/data/dynamo/entities/control/roleassignment-entity.ts
1740
+ var import_electrodb8 = require("electrodb");
1741
+ var RoleAssignmentEntity = new import_electrodb8.Entity({
1742
+ model: {
1743
+ entity: "roleassignment",
1744
+ service: "control",
1745
+ version: "01"
1746
+ },
1747
+ attributes: {
1748
+ /** Sort key sentinel. Always "CURRENT". */
1749
+ sk: {
1750
+ type: "string",
1751
+ required: true,
1752
+ default: "CURRENT"
1753
+ },
1754
+ /** Tenant in which the role assignment applies (required). */
1755
+ tenantId: {
1756
+ type: "string",
1757
+ required: true
1758
+ },
1759
+ /** FHIR Resource.id; role assignment id. */
1760
+ id: {
1761
+ type: "string",
1762
+ required: true
1763
+ },
1764
+ /** Full RoleAssignment resource serialized as JSON string. */
1765
+ resource: {
1766
+ type: "string",
1767
+ required: true
1768
+ },
1769
+ /**
1770
+ * Summary projection (key display fields as JSON string: id, displayName, status).
1771
+ * Populated on every write via extractSummary(resource); GSI1 INCLUDE surfaces it on lists.
1772
+ */
1773
+ summary: {
1774
+ type: "string",
1775
+ required: true
1776
+ },
1777
+ /** Version id (e.g. ULID). */
1778
+ vid: {
1779
+ type: "string",
1780
+ required: true
1781
+ },
1782
+ lastUpdated: {
1783
+ type: "string",
1784
+ required: true
1785
+ },
1786
+ gsi1Shard: gsi1ShardAttribute,
1787
+ /**
1788
+ * Derived GSI1 sort key — discriminator-first
1789
+ * `<roleId>#<normalizedUserName>#<id>` per ADR-018 pattern #8 so a
1790
+ * GSI1 query partitioned on the tenant can `begins_with('<roleId>#')`
1791
+ * to enumerate every user assigned to a given role, sorted by user
1792
+ * name. Falls back to `<lastUpdated>#<id>` when either component is
1793
+ * missing.
1794
+ */
1795
+ gsi1sk: roleAssignmentGsi1skAttribute,
1796
+ deleted: {
1797
+ type: "boolean",
1798
+ required: false
1799
+ },
1800
+ bundleId: {
1801
+ type: "string",
1802
+ required: false
1803
+ },
1804
+ msgId: {
1805
+ type: "string",
1806
+ required: false
1807
+ },
1808
+ /**
1809
+ * Denormalized display name of the linked Tenant, captured at row
1810
+ * last-write time. Promoted to a top-level attribute so the ADR-018
1811
+ * adjacency-list user-projection SK (pattern #5 —
1812
+ * `ROLEASSIGNMENT#TENANT#<normalizedRoleName>#<roleId>#TID#<tenantId>#<id>`)
1813
+ * can be composed from a top-level field instead of digging into the
1814
+ * `resource` JSON. Optional on the schema so pre-TR-024 rows do not
1815
+ * break; the operations-layer multi-write helper (#1010) makes the
1816
+ * field load-bearing at write time per TR-024 rule 2 (write-time
1817
+ * source = canonical Tenant.displayName).
1818
+ * @see TR-024 — Denormalized display-name attributes
1819
+ */
1820
+ denormalizedTenantName: {
1821
+ type: "string",
1822
+ required: false
1823
+ },
1824
+ /**
1825
+ * Denormalized display name of the linked User, captured at row
1826
+ * last-write time. Promoted to a top-level attribute so the ADR-018
1827
+ * adjacency-list canonical-record GSI1SK (pattern #8 —
1828
+ * `<roleId>#<normalizedUserName>#<id>`) and workspace-projection SK
1829
+ * (pattern #9) can be composed from a top-level field. Optional on
1830
+ * the schema so pre-TR-024 rows do not break; the operations-layer
1831
+ * multi-write helper (#1010) makes the field load-bearing at write
1832
+ * time per TR-024 rule 2 (write-time source = canonical
1833
+ * User.displayName).
1834
+ * @see TR-024 — Denormalized display-name attributes
1835
+ */
1836
+ denormalizedUserName: {
1837
+ type: "string",
1838
+ required: false
1839
+ },
1840
+ /**
1841
+ * Denormalized display name of the linked Role, captured at row
1842
+ * last-write time. Promoted to a top-level attribute so the ADR-018
1843
+ * adjacency-list user-projection SK (pattern #5 —
1844
+ * `ROLEASSIGNMENT#TENANT#<normalizedRoleName>#…`) can be composed from
1845
+ * a top-level field. Optional on the schema so pre-TR-024 rows do not
1846
+ * break; the operations-layer multi-write helper (#1010) makes the
1847
+ * field load-bearing at write time per TR-024 rule 2 (write-time
1848
+ * source = canonical Role.displayName).
1849
+ * @see TR-024 — Denormalized display-name attributes
1850
+ */
1851
+ denormalizedRoleName: {
1852
+ type: "string",
1853
+ required: false
1854
+ }
1855
+ },
1856
+ indexes: {
1857
+ /** Base table: PK = TID#<tenantId>#ROLEASSIGNMENT#ID#<id>, SK = CURRENT. Do not supply PK or SK from outside. */
1858
+ record: {
1859
+ pk: {
1860
+ field: "PK",
1861
+ composite: ["tenantId", "id"],
1862
+ template: "TID#${tenantId}#ROLEASSIGNMENT#ID#${id}"
1863
+ },
1864
+ sk: {
1865
+ field: "SK",
1866
+ composite: ["sk"],
1867
+ template: "${sk}"
1868
+ }
1869
+ },
1870
+ /**
1871
+ * GSI1 — Unified Sharded List per ADR-011: list all RoleAssignments for a tenant across the
1872
+ * four shards. Tenant-scoped only, so `WID#-` is a sentinel.
1873
+ * SK is derived via `roleAssignmentGsi1skAttribute` — composes the
1874
+ * discriminator-first `<roleId>#<normalizedUserName>#<id>` shape per
1875
+ * ADR-018 pattern #8 (users with a specific role in a tenant, sorted
1876
+ * by user name); falls back to `<lastUpdated>#<id>` when either
1877
+ * component is missing. `casing: "none"` preserves the normalized
1878
+ * label and ISO-8601 `T`/`Z`.
1879
+ */
1880
+ gsi1: {
1881
+ index: "GSI1",
1882
+ pk: {
1883
+ field: "GSI1PK",
1884
+ composite: ["tenantId", "gsi1Shard"],
1885
+ template: "TID#${tenantId}#WID#-#RT#RoleAssignment#SHARD#${gsi1Shard}"
1886
+ },
1887
+ sk: {
1888
+ field: "GSI1SK",
1889
+ casing: "none",
1890
+ composite: ["gsi1sk"],
1891
+ template: "${gsi1sk}"
1892
+ }
1893
+ }
1894
+ }
1895
+ });
1896
+
1897
+ // src/data/dynamo/entities/control/roleassignment-user-projection-entity.ts
1898
+ var import_electrodb9 = require("electrodb");
1899
+ var RoleAssignmentUserProjectionEntity = new import_electrodb9.Entity({
1900
+ model: {
1901
+ entity: "roleAssignmentUserProjection",
1902
+ service: "control",
1903
+ version: "01"
1904
+ },
1905
+ attributes: {
1906
+ /**
1907
+ * User partition discriminator. Renders as `USER#ID#<userId>` on the
1908
+ * base-table PK. Always required — the projection has no meaning
1909
+ * outside a user partition.
1910
+ */
1911
+ userId: {
1912
+ type: "string",
1913
+ required: true
1914
+ },
1915
+ /**
1916
+ * Pre-composed sort key — built by the operations-layer projection
1917
+ * writer via `buildRoleAssignmentUserProjectionSk*` helpers. The
1918
+ * entity stores the value verbatim so the SK grammar (tenant-lane
1919
+ * vs workspace-lane) is owned by the operations layer, not
1920
+ * duplicated here.
1921
+ */
1922
+ sk: {
1923
+ type: "string",
1924
+ required: true
1925
+ },
1926
+ /** Tenant in which the role assignment applies. Always required. */
1927
+ tenantId: {
1928
+ type: "string",
1929
+ required: true
1930
+ },
1931
+ /**
1932
+ * Workspace the role assignment scopes to. Present iff the
1933
+ * projection row is the workspace-level sub-lane; absent for
1934
+ * tenant-level sub-lane rows.
1935
+ */
1936
+ workspaceId: {
1937
+ type: "string",
1938
+ required: false
1939
+ },
1940
+ /**
1941
+ * Role the assignment grants. Stored as a discriminating field so
1942
+ * `Query(PK = USER#ID#<userId>, SK begins_with 'ROLEASSIGNMENT#…')`
1943
+ * results carry the role id without a hop to the canonical row.
1944
+ */
1945
+ roleId: {
1946
+ type: "string",
1947
+ required: true
1948
+ },
1949
+ /**
1950
+ * RoleAssignment canonical-record id. Stored as a discriminating
1951
+ * field so consumers can hydrate the canonical row via
1952
+ * `RoleAssignmentEntity.get({ tenantId, id: roleAssignmentId })`
1953
+ * when the projection's `summary` is insufficient.
1954
+ */
1955
+ roleAssignmentId: {
1956
+ type: "string",
1957
+ required: true
1958
+ },
1959
+ /**
1960
+ * Summary projection (key display fields as JSON string: id,
1961
+ * displayName, status) — mirrored from the canonical RoleAssignment
1962
+ * row so user-partition queries do not need a BatchGet hop.
1963
+ */
1964
+ summary: {
1965
+ type: "string",
1966
+ required: true
1967
+ },
1968
+ /** Version id mirrored from the canonical RoleAssignment row. */
1969
+ vid: {
1970
+ type: "string",
1971
+ required: true
1972
+ },
1973
+ /** Last-updated timestamp mirrored from the canonical RoleAssignment row. */
1974
+ lastUpdated: {
1975
+ type: "string",
1976
+ required: true
1977
+ },
1978
+ /**
1979
+ * Denormalized Tenant display name — mirrored from the canonical
1980
+ * RoleAssignment row per TR-024 rule 3 (canonical-record symmetry).
1981
+ * Optional on the schema because pre-TR-024 rows may not carry a
1982
+ * display name; the operations layer falls back gracefully when
1983
+ * missing.
1984
+ */
1985
+ denormalizedTenantName: {
1986
+ type: "string",
1987
+ required: false
1988
+ },
1989
+ /**
1990
+ * Denormalized User display name — mirrored from the canonical
1991
+ * RoleAssignment row per TR-024 rule 3 (canonical-record symmetry).
1992
+ * Carried on the projection so consumers can render the user's
1993
+ * display name without a hop to the User record.
1994
+ */
1995
+ denormalizedUserName: {
1996
+ type: "string",
1997
+ required: false
1998
+ },
1999
+ /**
2000
+ * Denormalized Role display name — required to compose the SK's
2001
+ * `<normalizedRoleName>` segment. Optional on the schema (pre-TR-024
2002
+ * rows fall back to a sentinel) but expected to be present at write
2003
+ * time per TR-024 rule 2 (write-time source =
2004
+ * canonical Role.displayName).
2005
+ */
2006
+ denormalizedRoleName: {
2007
+ type: "string",
2008
+ required: false
2009
+ }
2010
+ },
2011
+ indexes: {
2012
+ /**
2013
+ * Base table: PK = USER#ID#\<userId\>, SK = operation-supplied. Both
2014
+ * sub-lanes (tenant-level and workspace-level) use this same index —
2015
+ * the SK string encodes the lane discriminator
2016
+ * (`ROLEASSIGNMENT#TENANT#…` vs `ROLEASSIGNMENT#WORKSPACE#…`) so a
2017
+ * single `Query(PK = USER#ID#<userId>, SK begins_with 'ROLEASSIGNMENT#')`
2018
+ * returns both lanes interleaved.
2019
+ */
2020
+ record: {
2021
+ pk: {
2022
+ field: "PK",
2023
+ composite: ["userId"],
2024
+ template: "USER#ID#${userId}"
2025
+ },
2026
+ sk: {
2027
+ field: "SK",
2028
+ casing: "none",
2029
+ composite: ["sk"],
2030
+ template: "${sk}"
2031
+ }
2032
+ }
2033
+ }
2034
+ });
2035
+
2036
+ // src/data/dynamo/entities/control/roleassignment-workspace-projection-entity.ts
2037
+ var import_electrodb10 = require("electrodb");
2038
+ var RoleAssignmentWorkspaceProjectionEntity = new import_electrodb10.Entity({
2039
+ model: {
2040
+ entity: "roleAssignmentWorkspaceProjection",
2041
+ service: "control",
2042
+ version: "01"
2043
+ },
2044
+ attributes: {
2045
+ /**
2046
+ * Tenant the workspace belongs to. Renders as the leading segment
2047
+ * of the base-table PK. Always required — the workspace partition
2048
+ * is tenant-scoped per ADR-011.
2049
+ */
2050
+ tenantId: {
2051
+ type: "string",
2052
+ required: true
2053
+ },
2054
+ /**
2055
+ * Workspace partition discriminator. Renders as the trailing
2056
+ * segment of the base-table PK
2057
+ * (`TID#<tenantId>#WORKSPACE#ID#<workspaceId>`). Always required —
2058
+ * the projection has no meaning outside a workspace partition.
2059
+ */
2060
+ workspaceId: {
2061
+ type: "string",
2062
+ required: true
2063
+ },
2064
+ /**
2065
+ * Pre-composed sort key — built by the operations-layer projection
2066
+ * writer via `buildRoleAssignmentWorkspaceProjectionSk`. The entity
2067
+ * stores the value verbatim so the SK grammar (pattern #9) is
2068
+ * owned by the operations layer, not duplicated here.
2069
+ */
2070
+ sk: {
2071
+ type: "string",
2072
+ required: true
2073
+ },
2074
+ /**
2075
+ * User the role assignment grants the role to. Stored as a
2076
+ * discriminating field so consumers can hydrate the canonical User
2077
+ * row via `UserEntity.get({ id: userId, sk: "CURRENT" })` when the
2078
+ * projection's `summary` is insufficient.
2079
+ */
2080
+ userId: {
2081
+ type: "string",
2082
+ required: true
2083
+ },
2084
+ /**
2085
+ * Role the assignment grants. Stored as a discriminating field —
2086
+ * also rendered into the SK as the discriminator-first segment so
2087
+ * `begins_with('ROLEASSIGNMENT#<roleId>#')` filters one role.
2088
+ */
2089
+ roleId: {
2090
+ type: "string",
2091
+ required: true
2092
+ },
2093
+ /**
2094
+ * RoleAssignment canonical-record id. Stored as a discriminating
2095
+ * field so consumers can hydrate the canonical row via
2096
+ * `RoleAssignmentEntity.get({ tenantId, id: roleAssignmentId })`
2097
+ * when the projection's `summary` is insufficient.
2098
+ */
2099
+ roleAssignmentId: {
2100
+ type: "string",
2101
+ required: true
2102
+ },
2103
+ /**
2104
+ * Summary projection (key display fields as JSON string: id,
2105
+ * displayName, status) — mirrored from the canonical RoleAssignment
2106
+ * row so workspace-partition queries do not need a BatchGet hop.
2107
+ */
2108
+ summary: {
2109
+ type: "string",
2110
+ required: true
2111
+ },
2112
+ /** Version id mirrored from the canonical RoleAssignment row. */
2113
+ vid: {
2114
+ type: "string",
2115
+ required: true
2116
+ },
2117
+ /** Last-updated timestamp mirrored from the canonical RoleAssignment row. */
2118
+ lastUpdated: {
2119
+ type: "string",
2120
+ required: true
2121
+ },
2122
+ /**
2123
+ * Denormalized User display name — required to compose the
2124
+ * pattern-#9 SK (`ROLEASSIGNMENT#<roleId>#<normalizedUserName>#…`).
2125
+ * Optional on the schema because pre-TR-024 rows may not carry a
2126
+ * display name; the operations layer falls back to a sentinel when
2127
+ * missing so the SK still has a valid shape. The TR-023 rename-
2128
+ * cascade pipeline rewrites the SK on a User rename.
2129
+ */
2130
+ denormalizedUserName: {
2131
+ type: "string",
2132
+ required: false
2133
+ },
2134
+ /**
2135
+ * Denormalized Role display name — mirrored from the canonical
2136
+ * RoleAssignment row per TR-024 rule 3 (canonical-record symmetry).
2137
+ * Carried on the projection so consumers can render the role's
2138
+ * display name without a hop to the Role record. Not part of the
2139
+ * SK (pattern #9 sorts on `<normalizedUserName>`, not role name) —
2140
+ * a Role rename does NOT rewrite this SK.
2141
+ */
2142
+ denormalizedRoleName: {
2143
+ type: "string",
2144
+ required: false
2145
+ }
2146
+ },
2147
+ indexes: {
2148
+ /**
2149
+ * Base table: PK = TID#<tenantId>#WORKSPACE#ID#<workspaceId>,
2150
+ * SK = operation-supplied. Pattern #9 uses this index — the SK
2151
+ * encodes the entity-type prefix and discriminator-first roleId
2152
+ * (`ROLEASSIGNMENT#<roleId>#…`) so
2153
+ * `Query(PK = TID#<tenantId>#WORKSPACE#ID#<workspaceId>, SK begins_with 'ROLEASSIGNMENT#<roleId>#')`
2154
+ * returns every user-assignment for that role in the workspace, sorted
2155
+ * by normalized user name.
2156
+ */
2157
+ record: {
2158
+ pk: {
2159
+ field: "PK",
2160
+ composite: ["tenantId", "workspaceId"],
2161
+ template: "TID#${tenantId}#WORKSPACE#ID#${workspaceId}"
2162
+ },
2163
+ sk: {
2164
+ field: "SK",
2165
+ casing: "none",
2166
+ composite: ["sk"],
2167
+ template: "${sk}"
2168
+ }
2169
+ }
2170
+ }
2171
+ });
2172
+
2173
+ // src/data/dynamo/entities/control/tenant-entity.ts
2174
+ var import_electrodb11 = require("electrodb");
2175
+ var TenantEntity = new import_electrodb11.Entity({
2176
+ model: {
2177
+ entity: "tenant",
2178
+ service: "control",
2179
+ version: "01"
2180
+ },
2181
+ attributes: {
2182
+ /** Sort key sentinel. Always "CURRENT". */
2183
+ sk: {
2184
+ type: "string",
2185
+ required: true,
2186
+ default: "CURRENT"
2187
+ },
2188
+ /** The tenant's own id (= resource id). Drives the partition key. */
2189
+ tenantId: {
2190
+ type: "string",
2191
+ required: true
2192
+ },
2193
+ /** FHIR Resource.id; logical id in URL. Equals tenantId. */
2194
+ id: {
2195
+ type: "string",
2196
+ required: true
2197
+ },
2198
+ /** Full Tenant resource serialized as JSON string. */
2199
+ resource: {
2200
+ type: "string",
2201
+ required: true
2202
+ },
2203
+ /**
2204
+ * Summary projection (key display fields as JSON string: id, displayName, status).
2205
+ * Populated on every write via extractSummary(resource); GSI1 INCLUDE surfaces it on lists.
2206
+ */
2207
+ summary: {
2208
+ type: "string",
2209
+ required: true
2210
+ },
2211
+ /** Version id (e.g. ULID). */
2212
+ vid: {
2213
+ type: "string",
2214
+ required: true
2215
+ },
2216
+ lastUpdated: {
2217
+ type: "string",
2218
+ required: true
2219
+ },
2220
+ /**
2221
+ * ADR-028 denormalized counter — number of tenant-scoped Memberships
2222
+ * (users) in this tenant. Maintained by the counter-maintenance
2223
+ * consumer via atomic ADD; absent/0 until first event or reconciliation.
2224
+ */
2225
+ usersInTenant: {
2226
+ type: "number",
2227
+ required: false
2228
+ },
2229
+ /**
2230
+ * ADR-028 denormalized counter — number of Workspaces in this tenant.
2231
+ * Maintained by the counter-maintenance consumer via atomic ADD;
2232
+ * absent/0 until first event or reconciliation.
2233
+ */
2234
+ workspacesInTenant: {
2235
+ type: "number",
2236
+ required: false
2237
+ },
2238
+ gsi1Shard: gsi1ShardAttribute,
2239
+ /** Derived GSI1 sort key — name-based when extractable; else `<lastUpdated>#<id>`. */
2240
+ gsi1sk: gsi1skAttribute,
2241
+ deleted: {
2242
+ type: "boolean",
2243
+ required: false
2244
+ },
2245
+ bundleId: {
2246
+ type: "string",
2247
+ required: false
2248
+ },
2249
+ msgId: {
2250
+ type: "string",
2251
+ required: false
2252
+ }
2253
+ },
2254
+ indexes: {
2255
+ /** Base table: PK = TENANT#ID#<tenantId>, SK = CURRENT. Do not supply PK or SK from outside. */
2256
+ record: {
2257
+ pk: {
2258
+ field: "PK",
2259
+ composite: ["tenantId"],
2260
+ template: "TENANT#ID#${tenantId}"
2261
+ },
2262
+ sk: {
2263
+ field: "SK",
2264
+ composite: ["sk"],
2265
+ template: "${sk}"
2266
+ }
2267
+ },
2268
+ /**
2269
+ * GSI1 — Unified Sharded List per ADR-011: list all Tenants across the four shards.
2270
+ * Tenant lives at the platform tier (no parent tenant or workspace), so `TID#-#WID#-`
2271
+ * sentinels precede `RT#Tenant#SHARD#<n>`. SK is derived via `gsi1skAttribute` —
2272
+ * `<normalizedName>#<id>` when the resource carries a `name`, else `<lastUpdated>#<id>`
2273
+ * (DR-004). `casing: "none"` preserves the normalized label and ISO-8601 `T`/`Z`.
2274
+ */
2275
+ gsi1: {
2276
+ index: "GSI1",
2277
+ pk: {
2278
+ field: "GSI1PK",
2279
+ composite: ["gsi1Shard"],
2280
+ template: "TID#-#WID#-#RT#Tenant#SHARD#${gsi1Shard}"
2281
+ },
2282
+ sk: {
2283
+ field: "GSI1SK",
2284
+ casing: "none",
2285
+ composite: ["gsi1sk"],
2286
+ template: "${gsi1sk}"
2287
+ }
2288
+ }
2289
+ }
2290
+ });
2291
+
2292
+ // src/data/dynamo/entities/control/user-entity.ts
2293
+ var import_electrodb12 = require("electrodb");
2294
+ var UserEntity = new import_electrodb12.Entity({
2295
+ model: {
2296
+ entity: "user",
2297
+ service: "control",
2298
+ version: "01"
2299
+ },
2300
+ attributes: {
2301
+ /** Sort key sentinel. Always "CURRENT". */
2302
+ sk: {
2303
+ type: "string",
2304
+ required: true,
2305
+ default: "CURRENT"
2306
+ },
2307
+ /** FHIR Resource.id; platform user id (ohi_uid). */
2308
+ id: {
2309
+ type: "string",
2310
+ required: true
2311
+ },
2312
+ /** Full User resource serialized as JSON string. */
2313
+ resource: {
2314
+ type: "string",
2315
+ required: true
2316
+ },
2317
+ /**
2318
+ * Summary projection (key display fields as JSON string: id, displayName, status).
2319
+ * Populated on every write via extractSummary(resource); GSI1 INCLUDE surfaces it on lists.
2320
+ */
2321
+ summary: {
2322
+ type: "string",
2323
+ required: true
2324
+ },
2325
+ /**
2326
+ * Immutable Cognito-issued `sub` claim. Drives GSI2 (sub-lookup). Optional until the
2327
+ * Post Confirmation Lambda (#770) lands; required thereafter.
2328
+ */
2329
+ cognitoSub: {
2330
+ type: "string",
2331
+ required: false
2332
+ },
2333
+ /** Version id (e.g. ULID). */
2334
+ vid: {
2335
+ type: "string",
2336
+ required: true
2337
+ },
2338
+ lastUpdated: {
2339
+ type: "string",
2340
+ required: true
2341
+ },
2342
+ /**
2343
+ * ADR-028 denormalized counter — number of tenant-scoped Memberships
2344
+ * (tenants) this user belongs to. Maintained by the
2345
+ * counter-maintenance consumer via atomic ADD; absent/0 until first
2346
+ * event or reconciliation.
2347
+ */
2348
+ tenantsForUser: {
2349
+ type: "number",
2350
+ required: false
2351
+ },
2352
+ /**
2353
+ * ADR-028 denormalized counter — number of workspace-scoped
2354
+ * Memberships (workspaces) this user belongs to. Maintained by the
2355
+ * counter-maintenance consumer via atomic ADD; absent/0 until first
2356
+ * event or reconciliation.
2357
+ */
2358
+ workspacesForUser: {
2359
+ type: "number",
2360
+ required: false
2361
+ },
2362
+ gsi1Shard: gsi1ShardAttribute,
2363
+ /** Derived GSI1 sort key — name-based when extractable; else `<lastUpdated>#<id>`. */
2364
+ gsi1sk: gsi1skAttribute,
2365
+ deleted: {
2366
+ type: "boolean",
2367
+ required: false
2368
+ },
2369
+ /**
2370
+ * TR-022 / ADR-018 lifecycle state for the cascade pipeline.
2371
+ *
2372
+ * - `active` (or undefined) — normal, readable state.
2373
+ * - `deleting` — intermediate state set synchronously by the
2374
+ * hard-delete API entry point. The owning-delete cascade state
2375
+ * machine fans out from this transition (DynamoDB stream →
2376
+ * `control-plane.owning-delete.v1` → Step Functions). Readers MUST
2377
+ * short-circuit on `deleting` so partial cascades stay invisible.
2378
+ * - `deleted-failed` — terminal failure state set by the cascade
2379
+ * finalize Lambda when the cascade run fails irrecoverably.
2380
+ * Operators recover by re-running the cascade or by direct
2381
+ * intervention.
2382
+ *
2383
+ * The cascade finalize step deletes the canonical record conditional
2384
+ * on `lifecycleState = "deleting"`; on replay the conditional check
2385
+ * fails and the finalize step treats that as a no-op success.
2386
+ */
2387
+ lifecycleState: {
2388
+ type: ["active", "deleting", "deleted-failed"],
2389
+ required: false
2390
+ },
2391
+ bundleId: {
2392
+ type: "string",
2393
+ required: false
2394
+ },
2395
+ msgId: {
2396
+ type: "string",
2397
+ required: false
2398
+ }
2399
+ },
2400
+ indexes: {
2401
+ /** Base table: PK = USER#ID#<id>, SK = CURRENT. Do not supply PK or SK from outside. */
2402
+ record: {
2403
+ pk: {
2404
+ field: "PK",
2405
+ composite: ["id"],
2406
+ template: "USER#ID#${id}"
2407
+ },
2408
+ sk: {
2409
+ field: "SK",
2410
+ composite: ["sk"],
2411
+ template: "${sk}"
2412
+ }
2413
+ },
2414
+ /**
2415
+ * GSI1 — Unified Sharded List per ADR-011: list all Users across the four shards.
2416
+ * Non-tenant-isolated, so `TID#-#WID#-` sentinels precede `RT#User#SHARD#<n>`.
2417
+ * SK is derived via `gsi1skAttribute` — uses the resource's natural label when
2418
+ * extractable (string `name`/`title` via introspection), else `<lastUpdated>#<id>`
2419
+ * (DR-004). `casing: "none"` preserves the normalized label and ISO-8601 `T`/`Z`.
2420
+ */
2421
+ gsi1: {
2422
+ index: "GSI1",
2423
+ pk: {
2424
+ field: "GSI1PK",
2425
+ composite: ["gsi1Shard"],
2426
+ template: "TID#-#WID#-#RT#User#SHARD#${gsi1Shard}"
2427
+ },
2428
+ sk: {
2429
+ field: "GSI1SK",
2430
+ casing: "none",
2431
+ composite: ["gsi1sk"],
2432
+ template: "${gsi1sk}"
2433
+ }
2434
+ },
2435
+ /**
2436
+ * GSI2 — Cognito sub-lookup per ADR-011: resolves the UserEntity from a Cognito `sub` claim.
2437
+ * `condition` skips the index when `cognitoSub` is missing so legacy items without a sub are
2438
+ * not indexed.
2439
+ */
2440
+ gsi2: {
2441
+ index: "GSI2",
2442
+ condition: (attrs) => typeof attrs.cognitoSub === "string" && attrs.cognitoSub.length > 0,
2443
+ pk: {
2444
+ field: "GSI2PK",
2445
+ casing: "none",
2446
+ composite: ["cognitoSub"],
2447
+ template: "USER#SUB#${cognitoSub}"
2448
+ },
2449
+ sk: {
2450
+ field: "GSI2SK",
2451
+ casing: "none",
2452
+ composite: [],
2453
+ template: "CURRENT"
2454
+ }
2455
+ }
2456
+ }
2457
+ });
2458
+
2459
+ // src/data/dynamo/entities/control/workspace-entity.ts
2460
+ var import_electrodb13 = require("electrodb");
2461
+ var WorkspaceEntity = new import_electrodb13.Entity({
2462
+ model: {
2463
+ entity: "workspace",
2464
+ service: "control",
2465
+ version: "01"
2466
+ },
2467
+ attributes: {
2468
+ /** Sort key sentinel. Always "CURRENT". */
2469
+ sk: {
2470
+ type: "string",
2471
+ required: true,
2472
+ default: "CURRENT"
2473
+ },
2474
+ /** Tenant that contains this workspace (required). */
2475
+ tenantId: {
2476
+ type: "string",
2477
+ required: true
2478
+ },
2479
+ /** FHIR Resource.id; logical id in URL. */
2480
+ id: {
2481
+ type: "string",
2482
+ required: true
2483
+ },
2484
+ /** Full Workspace resource serialized as JSON string. */
2485
+ resource: {
2486
+ type: "string",
2487
+ required: true
2488
+ },
2489
+ /**
2490
+ * Summary projection (key display fields as JSON string: id, displayName, status).
2491
+ * Populated on every write via extractSummary(resource); GSI1 INCLUDE surfaces it on lists.
2492
+ */
2493
+ summary: {
2494
+ type: "string",
2495
+ required: true
2496
+ },
2497
+ /** Version id (e.g. ULID). */
2498
+ vid: {
2499
+ type: "string",
2500
+ required: true
2501
+ },
2502
+ lastUpdated: {
2503
+ type: "string",
2504
+ required: true
2505
+ },
2506
+ /**
2507
+ * ADR-028 denormalized counter — number of workspace-scoped
2508
+ * Memberships (users) in this workspace. Maintained by the
2509
+ * counter-maintenance consumer via atomic ADD; absent/0 until first
2510
+ * event or reconciliation.
2511
+ */
2512
+ usersInWorkspace: {
2513
+ type: "number",
2514
+ required: false
2515
+ },
2516
+ /**
2517
+ * ADR-028 denormalized counter — number of workspace-scoped
2518
+ * RoleAssignments classified as admin-tier in this workspace.
2519
+ * Maintained by the counter-maintenance consumer via atomic ADD;
2520
+ * absent/0 until first event or reconciliation.
2521
+ */
2522
+ adminUsersInWorkspace: {
2523
+ type: "number",
2524
+ required: false
2525
+ },
2526
+ /**
2527
+ * ADR-028 denormalized counter — number of workspace-scoped
2528
+ * RoleAssignments classified as non-admin in this workspace.
2529
+ * Maintained by the counter-maintenance consumer via atomic ADD;
2530
+ * absent/0 until first event or reconciliation.
2531
+ */
2532
+ normalUsersInWorkspace: {
2533
+ type: "number",
2534
+ required: false
2535
+ },
2536
+ gsi1Shard: gsi1ShardAttribute,
2537
+ /** Derived GSI1 sort key — name-based when extractable; else `<lastUpdated>#<id>`. */
2538
+ gsi1sk: gsi1skAttribute,
2539
+ deleted: {
2540
+ type: "boolean",
2541
+ required: false
2542
+ },
2543
+ /**
2544
+ * TR-022 / ADR-018 lifecycle state for the cascade pipeline.
2545
+ *
2546
+ * - `active` (or undefined) — normal, readable state.
2547
+ * - `deleting` — intermediate state set synchronously by the
2548
+ * hard-delete API entry point. The owning-delete cascade state
2549
+ * machine fans out from this transition (DynamoDB stream →
2550
+ * `control-plane.owning-delete.v1` → Step Functions). Readers MUST
2551
+ * short-circuit on `deleting` so partial cascades stay invisible.
2552
+ * - `deleted-failed` — terminal failure state set by the cascade
2553
+ * finalize Lambda when the cascade run fails irrecoverably.
2554
+ * Operators recover by re-running the cascade or by direct
2555
+ * intervention.
2556
+ *
2557
+ * The cascade finalize step deletes the canonical record conditional
2558
+ * on `lifecycleState = "deleting"`; on replay the conditional check
2559
+ * fails and the finalize step treats that as a no-op success.
2560
+ */
2561
+ lifecycleState: {
2562
+ type: ["active", "deleting", "deleted-failed"],
2563
+ required: false
2564
+ },
2565
+ bundleId: {
2566
+ type: "string",
2567
+ required: false
2568
+ },
2569
+ msgId: {
2570
+ type: "string",
2571
+ required: false
2572
+ }
2573
+ },
2574
+ indexes: {
2575
+ /** Base table: PK = TID#<tenantId>#WORKSPACE#ID#<id>, SK = CURRENT. Do not supply PK or SK from outside. */
2576
+ record: {
2577
+ pk: {
2578
+ field: "PK",
2579
+ composite: ["tenantId", "id"],
2580
+ template: "TID#${tenantId}#WORKSPACE#ID#${id}"
2581
+ },
2582
+ sk: {
2583
+ field: "SK",
2584
+ composite: ["sk"],
2585
+ template: "${sk}"
2586
+ }
2587
+ },
2588
+ /**
2589
+ * GSI1 — Unified Sharded List per ADR-011: list all Workspaces for a tenant across the
2590
+ * four shards. Workspace is itself the workspace identity, so `WID#-` is a sentinel.
2591
+ * SK is derived via `gsi1skAttribute` — `<normalizedName>#<id>` when the resource
2592
+ * carries a `name`, else `<lastUpdated>#<id>` (DR-004). `casing: "none"` preserves
2593
+ * the normalized label and ISO-8601 `T`/`Z`.
2594
+ */
2595
+ gsi1: {
2596
+ index: "GSI1",
2597
+ pk: {
2598
+ field: "GSI1PK",
2599
+ composite: ["tenantId", "gsi1Shard"],
2600
+ template: "TID#${tenantId}#WID#-#RT#Workspace#SHARD#${gsi1Shard}"
2601
+ },
2602
+ sk: {
2603
+ field: "GSI1SK",
2604
+ casing: "none",
2605
+ composite: ["gsi1sk"],
2606
+ template: "${gsi1sk}"
2607
+ }
2608
+ }
2609
+ }
2610
+ });
2611
+
2612
+ // src/data/dynamo/dynamo-control-service.ts
2613
+ var controlPlaneEntities = {
2614
+ configuration: ConfigurationEntity,
2615
+ configurationUserProjection: ConfigurationUserProjectionEntity,
2616
+ configurationWorkspaceProjection: ConfigurationWorkspaceProjectionEntity,
2617
+ membership: MembershipEntity,
2618
+ membershipUserProjection: MembershipUserProjectionEntity,
2619
+ membershipWorkspaceProjection: MembershipWorkspaceProjectionEntity,
2620
+ role: RoleEntity,
2621
+ roleAssignment: RoleAssignmentEntity,
2622
+ roleAssignmentUserProjection: RoleAssignmentUserProjectionEntity,
2623
+ roleAssignmentWorkspaceProjection: RoleAssignmentWorkspaceProjectionEntity,
2624
+ tenant: TenantEntity,
2625
+ user: UserEntity,
2626
+ workspace: WorkspaceEntity
2627
+ };
2628
+ var controlPlaneService = new import_electrodb14.Service(controlPlaneEntities, {
2629
+ table: defaultTableName,
2630
+ client: dynamoClient
2631
+ });
2632
+ var DynamoControlService = {
2633
+ entities: controlPlaneService.entities,
2634
+ transaction: controlPlaneService.transaction
2635
+ };
2636
+ function getDynamoControlService(tableName) {
2637
+ const resolved = tableName ?? defaultTableName;
2638
+ const service = new import_electrodb14.Service(controlPlaneEntities, {
2639
+ table: resolved,
2640
+ client: dynamoClient
2641
+ });
2642
+ return {
2643
+ entities: service.entities,
2644
+ transaction: service.transaction
2645
+ };
2646
+ }
2647
+
2648
+ // src/data/operations/control/counters/counter-apply-operation.ts
2649
+ var COUNTER_TARGET = {
2650
+ Tenant: "Tenant",
2651
+ Workspace: "Workspace",
2652
+ User: "User"
2653
+ };
2654
+ async function applyCounterDeltaOperation(params) {
2655
+ const { mutation, tableName } = params;
2656
+ const service = getDynamoControlService(tableName);
2657
+ const patch = buildPatch(service, mutation);
2658
+ try {
2659
+ if (mutation.delta > 0) {
2660
+ await patch.add({ [mutation.attribute]: mutation.delta }).go();
2661
+ return true;
2662
+ }
2663
+ await patch.add({ [mutation.attribute]: mutation.delta }).where((attr, { gt }) => gt(attr[mutation.attribute], 0)).go();
2664
+ return true;
2665
+ } catch (err) {
2666
+ if (isConditionalCheckFailure(err)) {
2667
+ return false;
2668
+ }
2669
+ throw err;
2670
+ }
2671
+ }
2672
+ function buildPatch(service, mutation) {
2673
+ switch (mutation.target) {
2674
+ case COUNTER_TARGET.Tenant:
2675
+ return service.entities.tenant.patch({
2676
+ tenantId: mutation.tenantId,
2677
+ sk: "CURRENT"
2678
+ });
2679
+ case COUNTER_TARGET.Workspace:
2680
+ return service.entities.workspace.patch({
2681
+ tenantId: mutation.tenantId,
2682
+ id: mutation.workspaceId,
2683
+ sk: "CURRENT"
2684
+ });
2685
+ case COUNTER_TARGET.User:
2686
+ return service.entities.user.patch({
2687
+ id: mutation.userId,
2688
+ sk: "CURRENT"
2689
+ });
2690
+ }
2691
+ }
2692
+ function isConditionalCheckFailure(err) {
2693
+ if (typeof err !== "object" || err === null) {
2694
+ return false;
2695
+ }
2696
+ const e = err;
2697
+ if (e.name === "ConditionalCheckFailedException" || e.code === "ConditionalCheckFailedException" || e.cause?.name === "ConditionalCheckFailedException") {
2698
+ return true;
2699
+ }
2700
+ return typeof e.message === "string" ? e.message.includes("ConditionalCheckFailed") : false;
2701
+ }
2702
+
2703
+ // src/data/operations/control/counters/role-admin-classification.ts
2704
+ function isAdminRoleAssignment(input) {
2705
+ if (codeIsAdminTier(input.roleLevel)) {
2706
+ return true;
2707
+ }
2708
+ if (idMatchesAdmin(input.roleId)) {
2709
+ return true;
2710
+ }
2711
+ return false;
2712
+ }
2713
+ function codeIsAdminTier(roleLevel) {
2714
+ if (typeof roleLevel !== "string" || roleLevel.length === 0) {
2715
+ return false;
2716
+ }
2717
+ const lower = roleLevel.toLowerCase();
2718
+ return lower === "admin" || lower.endsWith("admin");
2719
+ }
2720
+ function idMatchesAdmin(roleId) {
2721
+ if (typeof roleId !== "string" || roleId.length === 0) {
2722
+ return false;
2723
+ }
2724
+ return roleId.toLowerCase().includes("admin");
2725
+ }
2726
+
2727
+ // src/data/operations/control/counters/counter-event-router.ts
2728
+ function membershipMutations(payload, delta) {
2729
+ const mutations = [];
2730
+ if (payload.workspaceId !== void 0) {
2731
+ mutations.push({
2732
+ target: COUNTER_TARGET.Workspace,
2733
+ tenantId: payload.tenantId,
2734
+ workspaceId: payload.workspaceId,
2735
+ attribute: "usersInWorkspace",
2736
+ delta
2737
+ });
2738
+ if (payload.userId !== void 0) {
2739
+ mutations.push({
2740
+ target: COUNTER_TARGET.User,
2741
+ userId: payload.userId,
2742
+ attribute: "workspacesForUser",
2743
+ delta
2744
+ });
2745
+ }
2746
+ return mutations;
2747
+ }
2748
+ mutations.push({
2749
+ target: COUNTER_TARGET.Tenant,
2750
+ tenantId: payload.tenantId,
2751
+ attribute: "usersInTenant",
2752
+ delta
2753
+ });
2754
+ if (payload.userId !== void 0) {
2755
+ mutations.push({
2756
+ target: COUNTER_TARGET.User,
2757
+ userId: payload.userId,
2758
+ attribute: "tenantsForUser",
2759
+ delta
2760
+ });
2761
+ }
2762
+ return mutations;
2763
+ }
2764
+ function roleAssignmentMutations(payload, delta) {
2765
+ if (payload.workspaceId === void 0) {
2766
+ return [];
2767
+ }
2768
+ const attribute = isAdminRoleAssignment({
2769
+ roleLevel: payload.roleLevel,
2770
+ roleId: payload.roleId
2771
+ }) ? "adminUsersInWorkspace" : "normalUsersInWorkspace";
2772
+ return [
2773
+ {
2774
+ target: COUNTER_TARGET.Workspace,
2775
+ tenantId: payload.tenantId,
2776
+ workspaceId: payload.workspaceId,
2777
+ attribute,
2778
+ delta
2779
+ }
2780
+ ];
2781
+ }
2782
+ function workspaceMutations(payload, delta) {
2783
+ return [
2784
+ {
2785
+ target: COUNTER_TARGET.Tenant,
2786
+ tenantId: payload.tenantId,
2787
+ attribute: "workspacesInTenant",
2788
+ delta
2789
+ }
2790
+ ];
2791
+ }
2792
+
2793
+ // src/workflows/control-plane/counter-maintenance/counter-maintenance.handler.ts
2794
+ var errorMessage = (err) => err instanceof Error ? err.message : String(err);
2795
+ function resolveEvent(event) {
2796
+ const detailType = event["detail-type"];
2797
+ switch (detailType) {
2798
+ case import_workflows.ControlPlaneMembershipCreatedV1.detailType:
2799
+ return resolveMembership(event, import_workflows.ControlPlaneMembershipCreatedV1, 1);
2800
+ case import_workflows.ControlPlaneMembershipDeletedV1.detailType:
2801
+ return resolveMembership(event, import_workflows.ControlPlaneMembershipDeletedV1, -1);
2802
+ case import_workflows.ControlPlaneRoleAssignmentCreatedV1.detailType:
2803
+ return resolveRoleAssignment(
2804
+ event,
2805
+ import_workflows.ControlPlaneRoleAssignmentCreatedV1,
2806
+ 1
2807
+ );
2808
+ case import_workflows.ControlPlaneRoleAssignmentDeletedV1.detailType:
2809
+ return resolveRoleAssignment(
2810
+ event,
2811
+ import_workflows.ControlPlaneRoleAssignmentDeletedV1,
2812
+ -1
2813
+ );
2814
+ case import_workflows.ControlPlaneWorkspaceCreatedV1.detailType:
2815
+ return resolveWorkspace(event, import_workflows.ControlPlaneWorkspaceCreatedV1, 1);
2816
+ case import_workflows.ControlPlaneWorkspaceDeletedV1.detailType:
2817
+ return resolveWorkspace(event, import_workflows.ControlPlaneWorkspaceDeletedV1, -1);
2818
+ default:
2819
+ return null;
2820
+ }
2821
+ }
2822
+ function resolveMembership(event, entry, delta) {
2823
+ const parsed = (0, import_workflows2.parseWorkflowEvent)(event, entry);
2824
+ return {
2825
+ mutations: membershipMutations(parsed.envelope.payload, delta),
2826
+ dedupKey: parsed.dedupKey
2827
+ };
2828
+ }
2829
+ function resolveRoleAssignment(event, entry, delta) {
2830
+ const parsed = (0, import_workflows2.parseWorkflowEvent)(event, entry);
2831
+ return {
2832
+ mutations: roleAssignmentMutations(parsed.envelope.payload, delta),
2833
+ dedupKey: parsed.dedupKey
2834
+ };
2835
+ }
2836
+ function resolveWorkspace(event, entry, delta) {
2837
+ const parsed = (0, import_workflows2.parseWorkflowEvent)(event, entry);
2838
+ return {
2839
+ mutations: workspaceMutations(parsed.envelope.payload, delta),
2840
+ dedupKey: parsed.dedupKey
2841
+ };
2842
+ }
2843
+ var runCounterMaintenance = async (event, deps) => {
2844
+ const resolved = resolveEvent(event);
2845
+ if (resolved === null) {
2846
+ return;
2847
+ }
2848
+ const recordResult = await deps.dedupClient.recordIfAbsent({
2849
+ consumerName: COUNTER_MAINTENANCE_CONSUMER_NAME,
2850
+ eventId: resolved.dedupKey.eventId,
2851
+ attempt: resolved.dedupKey.attempt
2852
+ });
2853
+ if (!recordResult.recorded) {
2854
+ return;
2855
+ }
2856
+ try {
2857
+ for (const mutation of resolved.mutations) {
2858
+ await deps.applyMutation(mutation);
2859
+ }
2860
+ } catch (err) {
2861
+ await deps.dedupClient.markFailed({
2862
+ consumerName: COUNTER_MAINTENANCE_CONSUMER_NAME,
2863
+ eventId: resolved.dedupKey.eventId,
2864
+ attempt: resolved.dedupKey.attempt,
2865
+ reason: errorMessage(err)
2866
+ });
2867
+ throw err;
2868
+ }
2869
+ };
2870
+ var productionDependencies = () => {
2871
+ const dynamodb = new import_client_dynamodb2.DynamoDBClient({});
2872
+ return {
2873
+ dedupClient: (0, import_workflows2.workflowDedupClient)(dynamodb),
2874
+ applyMutation: async (mutation) => {
2875
+ await applyCounterDeltaOperation({ mutation });
2876
+ }
2877
+ };
2878
+ };
2879
+ var handler = async (event) => runCounterMaintenance(event, productionDependencies());
2880
+ // Annotate the CommonJS export names for ESM import in node:
2881
+ 0 && (module.exports = {
2882
+ handler,
2883
+ runCounterMaintenance
2884
+ });
2885
+ //# sourceMappingURL=counter-maintenance.handler.js.map