@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,3324 @@
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 = publishWorkflowEvent2;
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) => publishWorkflowEvent2(bridge, entry, payload, ctx, options)
280
+ };
281
+ }
282
+ async function publishWorkflowEvent2(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 = parseWorkflowEvent;
331
+ var envelope_version_1 = require_envelope_version();
332
+ function parseWorkflowEvent(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 = workflowDedupClient;
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 workflowDedupClient(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-reconciliation/counter-reconciliation.handler.ts
755
+ var counter_reconciliation_handler_exports = {};
756
+ __export(counter_reconciliation_handler_exports, {
757
+ handler: () => handler,
758
+ runCounterReconciliation: () => runCounterReconciliation
759
+ });
760
+ module.exports = __toCommonJS(counter_reconciliation_handler_exports);
761
+
762
+ // src/data/dynamo/dynamo-control-service.ts
763
+ var import_electrodb14 = require("electrodb");
764
+
765
+ // src/data/dynamo/dynamo-client.ts
766
+ var import_client_dynamodb = require("@aws-sdk/client-dynamodb");
767
+ var defaultTableName = process.env.DYNAMO_TABLE_NAME ?? "jesttesttable";
768
+ var dynamoClient = new import_client_dynamodb.DynamoDBClient({
769
+ ...process.env.MOCK_DYNAMODB_ENDPOINT && {
770
+ endpoint: process.env.MOCK_DYNAMODB_ENDPOINT,
771
+ sslEnabled: false,
772
+ region: "local"
773
+ }
774
+ });
775
+
776
+ // src/data/dynamo/entities/control/configuration-entity.ts
777
+ var import_electrodb = require("electrodb");
778
+
779
+ // src/data/dynamo/entities/control/control-entity-common.ts
780
+ var import_types = require("@openhi/types");
781
+
782
+ // src/data/dynamo/shard.ts
783
+ var SHARD_COUNT = 4;
784
+ function computeShard(id) {
785
+ let hash = 2166136261;
786
+ for (let i = 0; i < id.length; i++) {
787
+ hash ^= id.charCodeAt(i);
788
+ hash = Math.imul(hash, 16777619);
789
+ }
790
+ return (hash >>> 0) % SHARD_COUNT;
791
+ }
792
+
793
+ // src/data/dynamo/entities/control/control-entity-common.ts
794
+ var gsi1ShardAttribute = {
795
+ type: "string",
796
+ watch: ["id"],
797
+ set: (_val, item) => {
798
+ if (typeof item?.id !== "string" || item.id.length === 0) {
799
+ return void 0;
800
+ }
801
+ return String(computeShard(item.id));
802
+ }
803
+ };
804
+ var gsi1skAttribute = {
805
+ type: "string",
806
+ watch: ["resource", "lastUpdated", "id"],
807
+ set: (_val, item) => {
808
+ const id = typeof item?.id === "string" ? item.id : "";
809
+ const lastUpdated = typeof item?.lastUpdated === "string" ? item.lastUpdated : "";
810
+ const fallback = `${lastUpdated}#${id}`;
811
+ if (typeof item?.resource !== "string" || item.resource.length === 0) {
812
+ return fallback;
813
+ }
814
+ let parsed;
815
+ try {
816
+ parsed = JSON.parse(item.resource);
817
+ } catch {
818
+ return fallback;
819
+ }
820
+ if (!parsed || typeof parsed !== "object") return fallback;
821
+ const resourceType = parsed.resourceType;
822
+ if (typeof resourceType !== "string") return fallback;
823
+ const label = (0, import_types.extractLabel)(parsed);
824
+ return label !== void 0 ? `${label}#${id}` : fallback;
825
+ }
826
+ };
827
+ function extractRoleId(resource) {
828
+ const flat = resource.roleId;
829
+ if (typeof flat === "string" && flat.length > 0) return flat;
830
+ const role = resource.role;
831
+ if (role && typeof role === "object") {
832
+ const reference = role.reference;
833
+ if (typeof reference === "string" && reference.length > 0) {
834
+ const slash = reference.lastIndexOf("/");
835
+ const tail = slash >= 0 ? reference.slice(slash + 1) : reference;
836
+ if (tail.length > 0) return tail;
837
+ }
838
+ }
839
+ return void 0;
840
+ }
841
+ var roleAssignmentGsi1skAttribute = {
842
+ type: "string",
843
+ watch: ["resource", "denormalizedUserName", "lastUpdated", "id"],
844
+ set: (_val, item) => {
845
+ const id = typeof item?.id === "string" ? item.id : "";
846
+ const lastUpdated = typeof item?.lastUpdated === "string" ? item.lastUpdated : "";
847
+ const fallback = `${lastUpdated}#${id}`;
848
+ if (typeof item?.resource !== "string" || item.resource.length === 0) {
849
+ return fallback;
850
+ }
851
+ let parsed;
852
+ try {
853
+ parsed = JSON.parse(item.resource);
854
+ } catch {
855
+ return fallback;
856
+ }
857
+ if (!parsed || typeof parsed !== "object") return fallback;
858
+ const roleId = extractRoleId(parsed);
859
+ if (roleId === void 0) return fallback;
860
+ const denormalizedUserName = typeof item.denormalizedUserName === "string" ? item.denormalizedUserName : "";
861
+ const normalizedUserName = denormalizedUserName.length > 0 ? (0, import_types.normalizeLabel)(denormalizedUserName) : "";
862
+ if (normalizedUserName.length === 0) return fallback;
863
+ return `${roleId}#${normalizedUserName}#${id}`;
864
+ }
865
+ };
866
+ var membershipGsi1skAttribute = {
867
+ type: "string",
868
+ watch: ["denormalizedUserName", "lastUpdated", "id"],
869
+ set: (_val, item) => {
870
+ const id = typeof item?.id === "string" ? item.id : "";
871
+ const lastUpdated = typeof item?.lastUpdated === "string" ? item.lastUpdated : "";
872
+ const fallback = `${lastUpdated}#${id}`;
873
+ const denormalizedUserName = typeof item?.denormalizedUserName === "string" ? item.denormalizedUserName : "";
874
+ const normalizedUserName = denormalizedUserName.length > 0 ? (0, import_types.normalizeLabel)(denormalizedUserName) : "";
875
+ if (normalizedUserName.length === 0) {
876
+ return fallback;
877
+ }
878
+ return `${normalizedUserName}#${id}`;
879
+ }
880
+ };
881
+
882
+ // src/data/dynamo/entities/control/configuration-entity.ts
883
+ var ConfigurationEntity = new import_electrodb.Entity({
884
+ model: {
885
+ entity: "configuration",
886
+ service: "control",
887
+ version: "01"
888
+ },
889
+ attributes: {
890
+ /** Sort key. "CURRENT" for current version; version history in S3. */
891
+ sk: {
892
+ type: "string",
893
+ required: true,
894
+ default: "CURRENT"
895
+ },
896
+ /** Tenant scope. Use "BASELINE" when the config is baseline default (no tenant). */
897
+ tenantId: {
898
+ type: "string",
899
+ required: true,
900
+ default: "BASELINE"
901
+ },
902
+ /** Workspace scope. Use "-" when absent. */
903
+ workspaceId: {
904
+ type: "string",
905
+ required: true,
906
+ default: "-"
907
+ },
908
+ /** User scope. Use "-" when absent. */
909
+ userId: {
910
+ type: "string",
911
+ required: true,
912
+ default: "-"
913
+ },
914
+ /** Role scope. Use "-" when absent. */
915
+ roleId: {
916
+ type: "string",
917
+ required: true,
918
+ default: "-"
919
+ },
920
+ /** Config type (category), e.g. endpoints, branding, display. */
921
+ key: {
922
+ type: "string",
923
+ required: true
924
+ },
925
+ /** FHIR Resource.id; logical id in URL and for the Configuration resource. */
926
+ id: {
927
+ type: "string",
928
+ required: true
929
+ },
930
+ /** Payload as JSON string. JSON.stringify(resource) on write; JSON.parse(item.resource) on read. */
931
+ resource: {
932
+ type: "string",
933
+ required: true
934
+ },
935
+ /**
936
+ * Summary projection (key display fields as JSON string: id, key, status).
937
+ * Populated on every write via extractSummary(resource); GSI1 INCLUDE surfaces it on lists.
938
+ */
939
+ summary: {
940
+ type: "string",
941
+ required: true
942
+ },
943
+ /** Version id (e.g. ULID). Tracks current version; S3 history key. */
944
+ vid: {
945
+ type: "string",
946
+ required: true
947
+ },
948
+ lastUpdated: {
949
+ type: "string",
950
+ required: true
951
+ },
952
+ gsi1Shard: gsi1ShardAttribute,
953
+ deleted: {
954
+ type: "boolean",
955
+ required: false
956
+ },
957
+ bundleId: {
958
+ type: "string",
959
+ required: false
960
+ },
961
+ msgId: {
962
+ type: "string",
963
+ required: false
964
+ }
965
+ },
966
+ indexes: {
967
+ /** 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. */
968
+ record: {
969
+ pk: {
970
+ field: "PK",
971
+ composite: ["tenantId", "workspaceId", "userId", "roleId"],
972
+ template: "CONFIG#TID#${tenantId}#WID#${workspaceId}#UID#${userId}#RID#${roleId}"
973
+ },
974
+ sk: {
975
+ field: "SK",
976
+ composite: ["key", "sk"],
977
+ template: "KEY#${key}#SK#${sk}"
978
+ }
979
+ },
980
+ /**
981
+ * GSI1 — Unified Sharded List per ADR-011: list all Configuration entries for a
982
+ * (tenant, workspace) across the four shards. Use for "list configs scoped to this tenant"
983
+ * (workspaceId = "-") or "list configs scoped to this workspace". Does not support
984
+ * hierarchical resolution in one query; use base table GetItem in fallback order
985
+ * (user → workspace → tenant → baseline) for that.
986
+ * SK is `<key>#<id>` — Configuration's `key` is a required entity attribute (the
987
+ * config category: endpoints, branding, display, …) and the natural sort/lookup
988
+ * dimension. `casing: "none"` preserves the literal key value.
989
+ */
990
+ gsi1: {
991
+ index: "GSI1",
992
+ pk: {
993
+ field: "GSI1PK",
994
+ composite: ["tenantId", "workspaceId", "gsi1Shard"],
995
+ template: "TID#${tenantId}#WID#${workspaceId}#RT#Configuration#SHARD#${gsi1Shard}"
996
+ },
997
+ sk: {
998
+ field: "GSI1SK",
999
+ casing: "none",
1000
+ composite: ["key", "id"],
1001
+ template: "${key}#${id}"
1002
+ }
1003
+ }
1004
+ }
1005
+ });
1006
+
1007
+ // src/data/dynamo/entities/control/configuration-user-projection-entity.ts
1008
+ var import_electrodb2 = require("electrodb");
1009
+ var ConfigurationUserProjectionEntity = new import_electrodb2.Entity({
1010
+ model: {
1011
+ entity: "configurationUserProjection",
1012
+ service: "control",
1013
+ version: "01"
1014
+ },
1015
+ attributes: {
1016
+ /**
1017
+ * User partition discriminator. Renders as `USER#ID#<userId>` on the
1018
+ * base-table PK. Always required — the projection has no meaning
1019
+ * outside a user partition.
1020
+ */
1021
+ userId: {
1022
+ type: "string",
1023
+ required: true
1024
+ },
1025
+ /**
1026
+ * Pre-composed sort key — built by the operations-layer projection
1027
+ * writer via `buildConfigurationUserProjectionSk`. The entity stores
1028
+ * the value verbatim so the SK grammar (pattern #10 user-scope) is
1029
+ * owned by the operations layer, not duplicated here.
1030
+ */
1031
+ sk: {
1032
+ type: "string",
1033
+ required: true
1034
+ },
1035
+ /**
1036
+ * Configuration canonical-record id. Stored as a discriminating
1037
+ * field so consumers can hydrate the canonical row via the
1038
+ * Configuration get-by-id operation when the projection's `summary`
1039
+ * is insufficient.
1040
+ */
1041
+ configurationId: {
1042
+ type: "string",
1043
+ required: true
1044
+ },
1045
+ /**
1046
+ * Tenant the Configuration is associated with. The canonical row
1047
+ * keys off `(tenantId, workspaceId, userId, roleId)`; the projection
1048
+ * carries `tenantId` so consumers reconstructing the canonical PK
1049
+ * have the tenant segment without a hop.
1050
+ */
1051
+ tenantId: {
1052
+ type: "string",
1053
+ required: true
1054
+ },
1055
+ /**
1056
+ * Scope marker. Always `"user"` on this projection — recorded
1057
+ * explicitly so future scope-bearing projections (workspace,
1058
+ * tenant, role) can share filter semantics in a unified
1059
+ * cross-projection list query if one ever lands.
1060
+ */
1061
+ scope: {
1062
+ type: "string",
1063
+ required: true,
1064
+ default: "user"
1065
+ },
1066
+ /**
1067
+ * Configuration's `key` attribute (config category, e.g. endpoints,
1068
+ * branding, display). Mirrored from the canonical row so consumers
1069
+ * reading the projection get the natural display label without a
1070
+ * BatchGet hop. Doubles as the source of `<normalizedConfigName>` in
1071
+ * the SK.
1072
+ */
1073
+ displayName: {
1074
+ type: "string",
1075
+ required: false
1076
+ },
1077
+ /**
1078
+ * Summary projection (key display fields as JSON string) — mirrored
1079
+ * from the canonical Configuration row so user-partition queries do
1080
+ * not need a BatchGet hop.
1081
+ */
1082
+ summary: {
1083
+ type: "string",
1084
+ required: true
1085
+ },
1086
+ /** Version id mirrored from the canonical Configuration row. */
1087
+ vid: {
1088
+ type: "string",
1089
+ required: true
1090
+ },
1091
+ /** Last-updated timestamp mirrored from the canonical Configuration row. */
1092
+ lastUpdated: {
1093
+ type: "string",
1094
+ required: true
1095
+ }
1096
+ },
1097
+ indexes: {
1098
+ /**
1099
+ * Base table: PK = USER#ID#\<userId\>, SK = operation-supplied. A
1100
+ * single `Query(PK = USER#ID#<userId>, SK begins_with 'CONFIGURATION#')`
1101
+ * returns the user's user-scoped Configurations sorted by
1102
+ * `<normalizedConfigName>` (then `<configurationId>` as the
1103
+ * tiebreaker).
1104
+ */
1105
+ record: {
1106
+ pk: {
1107
+ field: "PK",
1108
+ composite: ["userId"],
1109
+ template: "USER#ID#${userId}"
1110
+ },
1111
+ sk: {
1112
+ field: "SK",
1113
+ casing: "none",
1114
+ composite: ["sk"],
1115
+ template: "${sk}"
1116
+ }
1117
+ }
1118
+ }
1119
+ });
1120
+
1121
+ // src/data/dynamo/entities/control/configuration-workspace-projection-entity.ts
1122
+ var import_electrodb3 = require("electrodb");
1123
+ var ConfigurationWorkspaceProjectionEntity = new import_electrodb3.Entity({
1124
+ model: {
1125
+ entity: "configurationWorkspaceProjection",
1126
+ service: "control",
1127
+ version: "01"
1128
+ },
1129
+ attributes: {
1130
+ /**
1131
+ * Tenant the workspace belongs to. Renders as the leading segment
1132
+ * of the base-table PK. Always required — the workspace partition
1133
+ * is tenant-scoped per ADR-011.
1134
+ */
1135
+ tenantId: {
1136
+ type: "string",
1137
+ required: true
1138
+ },
1139
+ /**
1140
+ * Workspace partition discriminator. Renders as the trailing
1141
+ * segment of the base-table PK
1142
+ * (`TID#<tenantId>#WORKSPACE#ID#<workspaceId>`). Always required —
1143
+ * the projection has no meaning outside a workspace partition.
1144
+ */
1145
+ workspaceId: {
1146
+ type: "string",
1147
+ required: true
1148
+ },
1149
+ /**
1150
+ * Pre-composed sort key — built by the operations-layer projection
1151
+ * writer via `buildConfigurationWorkspaceProjectionSk`. The entity
1152
+ * stores the value verbatim so the SK grammar (pattern #10
1153
+ * workspace-scope) is owned by the operations layer, not
1154
+ * duplicated here.
1155
+ */
1156
+ sk: {
1157
+ type: "string",
1158
+ required: true
1159
+ },
1160
+ /**
1161
+ * Configuration canonical-record id. Stored as a discriminating
1162
+ * field so consumers can hydrate the canonical row via the
1163
+ * Configuration get-by-id operation when the projection's `summary`
1164
+ * is insufficient.
1165
+ */
1166
+ configurationId: {
1167
+ type: "string",
1168
+ required: true
1169
+ },
1170
+ /**
1171
+ * Scope marker. Always `"workspace"` on this projection — recorded
1172
+ * explicitly so future scope-bearing projections (user, tenant,
1173
+ * role) can share filter semantics in a unified cross-projection
1174
+ * list query if one ever lands.
1175
+ */
1176
+ scope: {
1177
+ type: "string",
1178
+ required: true,
1179
+ default: "workspace"
1180
+ },
1181
+ /**
1182
+ * Configuration's `key` attribute (config category, e.g. endpoints,
1183
+ * branding, display). Mirrored from the canonical row so consumers
1184
+ * reading the projection get the natural display label without a
1185
+ * BatchGet hop. Doubles as the source of `<normalizedConfigName>`
1186
+ * in the SK.
1187
+ */
1188
+ displayName: {
1189
+ type: "string",
1190
+ required: false
1191
+ },
1192
+ /**
1193
+ * Summary projection (key display fields as JSON string) — mirrored
1194
+ * from the canonical Configuration row so workspace-partition
1195
+ * queries do not need a BatchGet hop.
1196
+ */
1197
+ summary: {
1198
+ type: "string",
1199
+ required: true
1200
+ },
1201
+ /** Version id mirrored from the canonical Configuration row. */
1202
+ vid: {
1203
+ type: "string",
1204
+ required: true
1205
+ },
1206
+ /** Last-updated timestamp mirrored from the canonical Configuration row. */
1207
+ lastUpdated: {
1208
+ type: "string",
1209
+ required: true
1210
+ }
1211
+ },
1212
+ indexes: {
1213
+ /**
1214
+ * Base table: PK = TID#<tenantId>#WORKSPACE#ID#<workspaceId>,
1215
+ * SK = operation-supplied. A single
1216
+ * `Query(PK = TID#<tenantId>#WORKSPACE#ID#<workspaceId>, SK begins_with 'CONFIGURATION#')`
1217
+ * returns the workspace's workspace-scoped Configurations sorted by
1218
+ * `<normalizedConfigName>` (then `<configurationId>` as the
1219
+ * tiebreaker).
1220
+ */
1221
+ record: {
1222
+ pk: {
1223
+ field: "PK",
1224
+ composite: ["tenantId", "workspaceId"],
1225
+ template: "TID#${tenantId}#WORKSPACE#ID#${workspaceId}"
1226
+ },
1227
+ sk: {
1228
+ field: "SK",
1229
+ casing: "none",
1230
+ composite: ["sk"],
1231
+ template: "${sk}"
1232
+ }
1233
+ }
1234
+ }
1235
+ });
1236
+
1237
+ // src/data/dynamo/entities/control/membership-entity.ts
1238
+ var import_electrodb4 = require("electrodb");
1239
+ var MembershipEntity = new import_electrodb4.Entity({
1240
+ model: {
1241
+ entity: "membership",
1242
+ service: "control",
1243
+ version: "01"
1244
+ },
1245
+ attributes: {
1246
+ /** Sort key sentinel. Always "CURRENT". */
1247
+ sk: {
1248
+ type: "string",
1249
+ required: true,
1250
+ default: "CURRENT"
1251
+ },
1252
+ /** Tenant in which the user has membership (required). */
1253
+ tenantId: {
1254
+ type: "string",
1255
+ required: true
1256
+ },
1257
+ /** FHIR Resource.id; membership id. */
1258
+ id: {
1259
+ type: "string",
1260
+ required: true
1261
+ },
1262
+ /** Full Membership resource serialized as JSON string. */
1263
+ resource: {
1264
+ type: "string",
1265
+ required: true
1266
+ },
1267
+ /**
1268
+ * Summary projection (key display fields as JSON string: id, displayName, status).
1269
+ * Populated on every write via extractSummary(resource); GSI1 INCLUDE surfaces it on lists.
1270
+ */
1271
+ summary: {
1272
+ type: "string",
1273
+ required: true
1274
+ },
1275
+ /** Version id (e.g. ULID). */
1276
+ vid: {
1277
+ type: "string",
1278
+ required: true
1279
+ },
1280
+ lastUpdated: {
1281
+ type: "string",
1282
+ required: true
1283
+ },
1284
+ gsi1Shard: gsi1ShardAttribute,
1285
+ /**
1286
+ * Derived GSI1 sort key — `<normalizedUserName>#<id>` per ADR-018
1287
+ * pattern #1 so a GSI1 query partitioned on the tenant range-scans
1288
+ * by user-name prefix and returns memberships sorted by user name.
1289
+ * Falls back to `<lastUpdated>#<id>` when `denormalizedUserName`
1290
+ * is missing.
1291
+ */
1292
+ gsi1sk: membershipGsi1skAttribute,
1293
+ deleted: {
1294
+ type: "boolean",
1295
+ required: false
1296
+ },
1297
+ bundleId: {
1298
+ type: "string",
1299
+ required: false
1300
+ },
1301
+ msgId: {
1302
+ type: "string",
1303
+ required: false
1304
+ },
1305
+ /**
1306
+ * Denormalized `linked-data-identity` Reference (e.g. `Practitioner/abc`).
1307
+ * Populated from the FHIR extension on the Membership resource at write
1308
+ * time so future GSIs can index data-plane identity lookups without
1309
+ * deserializing the full resource JSON. See ADR 2026-03-13-02 §6.
1310
+ */
1311
+ linkedDataIdentityRef: {
1312
+ type: "string",
1313
+ required: false
1314
+ },
1315
+ /**
1316
+ * Denormalized display name of the linked Tenant, captured at row
1317
+ * last-write time. Promoted to a top-level attribute so the ADR-018
1318
+ * adjacency-list projection SKs (pattern #3 — `MEMBERSHIP#TENANT#<normalizedTenantName>#…`)
1319
+ * can be composed from a top-level field instead of digging into the
1320
+ * `resource` JSON. Optional on the schema so pre-TR-024 rows do not
1321
+ * break; the operations-layer multi-write helper (#1010) makes the
1322
+ * field load-bearing at write time per TR-024 rule 2 (write-time
1323
+ * source = canonical Tenant.displayName).
1324
+ * @see TR-024 — Denormalized display-name attributes
1325
+ */
1326
+ denormalizedTenantName: {
1327
+ type: "string",
1328
+ required: false
1329
+ },
1330
+ /**
1331
+ * Denormalized display name of the linked User, captured at row
1332
+ * last-write time. Promoted to a top-level attribute so the ADR-018
1333
+ * adjacency-list canonical-record GSI1SK (pattern #1 —
1334
+ * `<normalizedUserName>#<id>`) and workspace-projection SK (pattern #2)
1335
+ * can be composed from a top-level field. Optional on the schema so
1336
+ * pre-TR-024 rows do not break; the operations-layer multi-write helper
1337
+ * (#1010) makes the field load-bearing at write time per TR-024 rule 2
1338
+ * (write-time source = canonical User.displayName).
1339
+ * @see TR-024 — Denormalized display-name attributes
1340
+ */
1341
+ denormalizedUserName: {
1342
+ type: "string",
1343
+ required: false
1344
+ }
1345
+ },
1346
+ indexes: {
1347
+ /** Base table: PK = TID#<tenantId>#MEMBERSHIP#ID#<id>, SK = CURRENT. Do not supply PK or SK from outside. */
1348
+ record: {
1349
+ pk: {
1350
+ field: "PK",
1351
+ composite: ["tenantId", "id"],
1352
+ template: "TID#${tenantId}#MEMBERSHIP#ID#${id}"
1353
+ },
1354
+ sk: {
1355
+ field: "SK",
1356
+ composite: ["sk"],
1357
+ template: "${sk}"
1358
+ }
1359
+ },
1360
+ /**
1361
+ * GSI1 — Unified Sharded List per ADR-011: list all Memberships for a tenant across the
1362
+ * four shards. Membership is tenant-scoped only, so `WID#-` is a sentinel.
1363
+ * SK is derived via `membershipGsi1skAttribute` — composes
1364
+ * `<normalizedUserName>#<id>` per ADR-018 pattern #1 (users in a
1365
+ * tenant, sorted by user name); falls back to `<lastUpdated>#<id>`
1366
+ * when `denormalizedUserName` is missing. `casing: "none"` preserves
1367
+ * the normalized label and ISO-8601 `T`/`Z`.
1368
+ */
1369
+ gsi1: {
1370
+ index: "GSI1",
1371
+ pk: {
1372
+ field: "GSI1PK",
1373
+ composite: ["tenantId", "gsi1Shard"],
1374
+ template: "TID#${tenantId}#WID#-#RT#Membership#SHARD#${gsi1Shard}"
1375
+ },
1376
+ sk: {
1377
+ field: "GSI1SK",
1378
+ casing: "none",
1379
+ composite: ["gsi1sk"],
1380
+ template: "${gsi1sk}"
1381
+ }
1382
+ }
1383
+ }
1384
+ });
1385
+
1386
+ // src/data/dynamo/entities/control/membership-user-projection-entity.ts
1387
+ var import_electrodb5 = require("electrodb");
1388
+ var MembershipUserProjectionEntity = new import_electrodb5.Entity({
1389
+ model: {
1390
+ entity: "membershipUserProjection",
1391
+ service: "control",
1392
+ version: "01"
1393
+ },
1394
+ attributes: {
1395
+ /**
1396
+ * User partition discriminator. Renders as `USER#ID#<userId>` on the
1397
+ * base-table PK. Always required — the projection has no meaning
1398
+ * outside a user partition.
1399
+ */
1400
+ userId: {
1401
+ type: "string",
1402
+ required: true
1403
+ },
1404
+ /**
1405
+ * Pre-composed sort key — built by the operations-layer projection
1406
+ * writer via `buildMembershipUserProjectionSk*` helpers. The entity
1407
+ * stores the value verbatim so the SK grammar (patterns #3 and #4)
1408
+ * is owned by the operations layer, not duplicated here.
1409
+ */
1410
+ sk: {
1411
+ type: "string",
1412
+ required: true
1413
+ },
1414
+ /** Tenant in which the membership applies. Always required. */
1415
+ tenantId: {
1416
+ type: "string",
1417
+ required: true
1418
+ },
1419
+ /**
1420
+ * Workspace the membership scopes to. Present iff the projection
1421
+ * row is a pattern-#4 workspace sub-lane row; absent for pattern-#3
1422
+ * tenant sub-lane rows.
1423
+ */
1424
+ workspaceId: {
1425
+ type: "string",
1426
+ required: false
1427
+ },
1428
+ /**
1429
+ * Membership canonical-record id. Stored as a discriminating field
1430
+ * so consumers can hydrate the canonical row via
1431
+ * `MembershipEntity.get({ tenantId, id: membershipId })` when the
1432
+ * projection's `summary` is insufficient.
1433
+ */
1434
+ membershipId: {
1435
+ type: "string",
1436
+ required: true
1437
+ },
1438
+ /**
1439
+ * Summary projection (key display fields as JSON string: id,
1440
+ * displayName, status) — mirrored from the canonical Membership row
1441
+ * so user-partition queries do not need a BatchGet hop.
1442
+ */
1443
+ summary: {
1444
+ type: "string",
1445
+ required: true
1446
+ },
1447
+ /** Version id mirrored from the canonical Membership row. */
1448
+ vid: {
1449
+ type: "string",
1450
+ required: true
1451
+ },
1452
+ /** Last-updated timestamp mirrored from the canonical Membership row. */
1453
+ lastUpdated: {
1454
+ type: "string",
1455
+ required: true
1456
+ },
1457
+ /**
1458
+ * Denormalized Tenant display name — required to compose pattern-#3
1459
+ * SK (`MEMBERSHIP#TENANT#<normalizedTenantName>#…`). Optional on the
1460
+ * schema because pre-TR-024 rows may not carry a display name; the
1461
+ * operations layer falls back gracefully when missing.
1462
+ */
1463
+ denormalizedTenantName: {
1464
+ type: "string",
1465
+ required: false
1466
+ },
1467
+ /**
1468
+ * Denormalized User display name — mirrored from the canonical
1469
+ * Membership row per TR-024 rule 3 (canonical-record symmetry).
1470
+ * Carried on the projection so consumers can render the user's
1471
+ * display name without a hop to the User record.
1472
+ */
1473
+ denormalizedUserName: {
1474
+ type: "string",
1475
+ required: false
1476
+ },
1477
+ /**
1478
+ * Denormalized Workspace display name — required to compose
1479
+ * pattern-#4 SK (`MEMBERSHIP#WORKSPACE#TID#<tenantId>#<normalizedWorkspaceName>#…`).
1480
+ * Optional on the schema (TR-024 § Open Item #4 defers a formal
1481
+ * Workspace-rename cascade); the operations layer falls back to a
1482
+ * sentinel when missing so the SK still has a valid shape.
1483
+ */
1484
+ denormalizedWorkspaceName: {
1485
+ type: "string",
1486
+ required: false
1487
+ }
1488
+ },
1489
+ indexes: {
1490
+ /**
1491
+ * Base table: PK = USER#ID#\<userId\>, SK = operation-supplied.
1492
+ * Both pattern #3 and pattern #4 use this same index — the SK string
1493
+ * encodes the lane discriminator (`MEMBERSHIP#TENANT#…` vs
1494
+ * `MEMBERSHIP#WORKSPACE#…`) so a single
1495
+ * `Query(PK = USER#ID#<userId>, SK begins_with 'MEMBERSHIP#')`
1496
+ * returns both lanes interleaved.
1497
+ */
1498
+ record: {
1499
+ pk: {
1500
+ field: "PK",
1501
+ composite: ["userId"],
1502
+ template: "USER#ID#${userId}"
1503
+ },
1504
+ sk: {
1505
+ field: "SK",
1506
+ casing: "none",
1507
+ composite: ["sk"],
1508
+ template: "${sk}"
1509
+ }
1510
+ }
1511
+ }
1512
+ });
1513
+
1514
+ // src/data/dynamo/entities/control/membership-workspace-projection-entity.ts
1515
+ var import_electrodb6 = require("electrodb");
1516
+ var MembershipWorkspaceProjectionEntity = new import_electrodb6.Entity({
1517
+ model: {
1518
+ entity: "membershipWorkspaceProjection",
1519
+ service: "control",
1520
+ version: "01"
1521
+ },
1522
+ attributes: {
1523
+ /**
1524
+ * Tenant the workspace belongs to. Renders as the leading segment
1525
+ * of the base-table PK. Always required — the workspace partition
1526
+ * is tenant-scoped per ADR-011.
1527
+ */
1528
+ tenantId: {
1529
+ type: "string",
1530
+ required: true
1531
+ },
1532
+ /**
1533
+ * Workspace partition discriminator. Renders as the trailing
1534
+ * segment of the base-table PK
1535
+ * (`TID#<tenantId>#WORKSPACE#ID#<workspaceId>`). Always required —
1536
+ * the projection has no meaning outside a workspace partition.
1537
+ */
1538
+ workspaceId: {
1539
+ type: "string",
1540
+ required: true
1541
+ },
1542
+ /**
1543
+ * Pre-composed sort key — built by the operations-layer projection
1544
+ * writer via `buildMembershipWorkspaceProjectionSk`. The entity
1545
+ * stores the value verbatim so the SK grammar (pattern #2) is
1546
+ * owned by the operations layer, not duplicated here.
1547
+ */
1548
+ sk: {
1549
+ type: "string",
1550
+ required: true
1551
+ },
1552
+ /**
1553
+ * User the membership links. Stored as a discriminating field so
1554
+ * consumers can hydrate the canonical User row via
1555
+ * `UserEntity.get({ id: userId, sk: "CURRENT" })` when the
1556
+ * projection's `summary` is insufficient.
1557
+ */
1558
+ userId: {
1559
+ type: "string",
1560
+ required: true
1561
+ },
1562
+ /**
1563
+ * Membership canonical-record id. Stored as a discriminating field
1564
+ * so consumers can hydrate the canonical row via
1565
+ * `MembershipEntity.get({ tenantId, id: membershipId })` when the
1566
+ * projection's `summary` is insufficient.
1567
+ */
1568
+ membershipId: {
1569
+ type: "string",
1570
+ required: true
1571
+ },
1572
+ /**
1573
+ * Summary projection (key display fields as JSON string: id,
1574
+ * displayName, status) — mirrored from the canonical Membership row
1575
+ * so workspace-partition queries do not need a BatchGet hop.
1576
+ */
1577
+ summary: {
1578
+ type: "string",
1579
+ required: true
1580
+ },
1581
+ /** Version id mirrored from the canonical Membership row. */
1582
+ vid: {
1583
+ type: "string",
1584
+ required: true
1585
+ },
1586
+ /** Last-updated timestamp mirrored from the canonical Membership row. */
1587
+ lastUpdated: {
1588
+ type: "string",
1589
+ required: true
1590
+ },
1591
+ /**
1592
+ * Denormalized User display name — required to compose the
1593
+ * pattern-#2 SK (`MEMBERSHIP#<normalizedUserName>#…`). Optional on
1594
+ * the schema because pre-TR-024 rows may not carry a display name;
1595
+ * the operations layer falls back to a sentinel when missing so
1596
+ * the SK still has a valid shape. The TR-023 rename-cascade
1597
+ * pipeline rewrites the SK on a User rename.
1598
+ */
1599
+ denormalizedUserName: {
1600
+ type: "string",
1601
+ required: false
1602
+ }
1603
+ },
1604
+ indexes: {
1605
+ /**
1606
+ * Base table: PK = TID#<tenantId>#WORKSPACE#ID#<workspaceId>,
1607
+ * SK = operation-supplied. Pattern #2 uses this index — the SK
1608
+ * encodes the entity-type prefix (`MEMBERSHIP#…`) so a
1609
+ * `Query(PK = TID#<tenantId>#WORKSPACE#ID#<workspaceId>, SK begins_with 'MEMBERSHIP#')`
1610
+ * returns every member projection for the workspace in normalized-
1611
+ * user-name sort order.
1612
+ */
1613
+ record: {
1614
+ pk: {
1615
+ field: "PK",
1616
+ composite: ["tenantId", "workspaceId"],
1617
+ template: "TID#${tenantId}#WORKSPACE#ID#${workspaceId}"
1618
+ },
1619
+ sk: {
1620
+ field: "SK",
1621
+ casing: "none",
1622
+ composite: ["sk"],
1623
+ template: "${sk}"
1624
+ }
1625
+ }
1626
+ }
1627
+ });
1628
+
1629
+ // src/data/dynamo/entities/control/role-entity.ts
1630
+ var import_electrodb7 = require("electrodb");
1631
+ var RoleEntity = new import_electrodb7.Entity({
1632
+ model: {
1633
+ entity: "role",
1634
+ service: "control",
1635
+ version: "01"
1636
+ },
1637
+ attributes: {
1638
+ /** Sort key sentinel. Always "CURRENT". */
1639
+ sk: {
1640
+ type: "string",
1641
+ required: true,
1642
+ default: "CURRENT"
1643
+ },
1644
+ /** FHIR Resource.id; role id. */
1645
+ id: {
1646
+ type: "string",
1647
+ required: true
1648
+ },
1649
+ /** Full Role resource serialized as JSON string. */
1650
+ resource: {
1651
+ type: "string",
1652
+ required: true
1653
+ },
1654
+ /**
1655
+ * Summary projection (key display fields as JSON string: id, displayName, status).
1656
+ * Populated on every write via extractSummary(resource); GSI1 INCLUDE surfaces it on lists.
1657
+ */
1658
+ summary: {
1659
+ type: "string",
1660
+ required: true
1661
+ },
1662
+ /** Version id (e.g. ULID). */
1663
+ vid: {
1664
+ type: "string",
1665
+ required: true
1666
+ },
1667
+ lastUpdated: {
1668
+ type: "string",
1669
+ required: true
1670
+ },
1671
+ gsi1Shard: gsi1ShardAttribute,
1672
+ /** Derived GSI1 sort key — name-based when extractable; else `<lastUpdated>#<id>`. */
1673
+ gsi1sk: gsi1skAttribute,
1674
+ deleted: {
1675
+ type: "boolean",
1676
+ required: false
1677
+ },
1678
+ bundleId: {
1679
+ type: "string",
1680
+ required: false
1681
+ },
1682
+ msgId: {
1683
+ type: "string",
1684
+ required: false
1685
+ }
1686
+ },
1687
+ indexes: {
1688
+ /** Base table: PK = ROLE#ID#<id>, SK = CURRENT. Do not supply PK or SK from outside. */
1689
+ record: {
1690
+ pk: {
1691
+ field: "PK",
1692
+ composite: ["id"],
1693
+ template: "ROLE#ID#${id}"
1694
+ },
1695
+ sk: {
1696
+ field: "SK",
1697
+ composite: ["sk"],
1698
+ template: "${sk}"
1699
+ }
1700
+ },
1701
+ /**
1702
+ * GSI1 — Unified Sharded List per ADR-011: list all Roles across the four shards.
1703
+ * Non-tenant-isolated, so `TID#-#WID#-` sentinels precede `RT#Role#SHARD#<n>`.
1704
+ * SK is derived via `gsi1skAttribute` — uses the resource's natural label when
1705
+ * extractable, else `<lastUpdated>#<id>` (DR-004). `casing: "none"` preserves the
1706
+ * normalized label and ISO-8601 `T`/`Z`.
1707
+ */
1708
+ gsi1: {
1709
+ index: "GSI1",
1710
+ pk: {
1711
+ field: "GSI1PK",
1712
+ composite: ["gsi1Shard"],
1713
+ template: "TID#-#WID#-#RT#Role#SHARD#${gsi1Shard}"
1714
+ },
1715
+ sk: {
1716
+ field: "GSI1SK",
1717
+ casing: "none",
1718
+ composite: ["gsi1sk"],
1719
+ template: "${gsi1sk}"
1720
+ }
1721
+ }
1722
+ }
1723
+ });
1724
+
1725
+ // src/data/dynamo/entities/control/roleassignment-entity.ts
1726
+ var import_electrodb8 = require("electrodb");
1727
+ var RoleAssignmentEntity = new import_electrodb8.Entity({
1728
+ model: {
1729
+ entity: "roleassignment",
1730
+ service: "control",
1731
+ version: "01"
1732
+ },
1733
+ attributes: {
1734
+ /** Sort key sentinel. Always "CURRENT". */
1735
+ sk: {
1736
+ type: "string",
1737
+ required: true,
1738
+ default: "CURRENT"
1739
+ },
1740
+ /** Tenant in which the role assignment applies (required). */
1741
+ tenantId: {
1742
+ type: "string",
1743
+ required: true
1744
+ },
1745
+ /** FHIR Resource.id; role assignment id. */
1746
+ id: {
1747
+ type: "string",
1748
+ required: true
1749
+ },
1750
+ /** Full RoleAssignment resource serialized as JSON string. */
1751
+ resource: {
1752
+ type: "string",
1753
+ required: true
1754
+ },
1755
+ /**
1756
+ * Summary projection (key display fields as JSON string: id, displayName, status).
1757
+ * Populated on every write via extractSummary(resource); GSI1 INCLUDE surfaces it on lists.
1758
+ */
1759
+ summary: {
1760
+ type: "string",
1761
+ required: true
1762
+ },
1763
+ /** Version id (e.g. ULID). */
1764
+ vid: {
1765
+ type: "string",
1766
+ required: true
1767
+ },
1768
+ lastUpdated: {
1769
+ type: "string",
1770
+ required: true
1771
+ },
1772
+ gsi1Shard: gsi1ShardAttribute,
1773
+ /**
1774
+ * Derived GSI1 sort key — discriminator-first
1775
+ * `<roleId>#<normalizedUserName>#<id>` per ADR-018 pattern #8 so a
1776
+ * GSI1 query partitioned on the tenant can `begins_with('<roleId>#')`
1777
+ * to enumerate every user assigned to a given role, sorted by user
1778
+ * name. Falls back to `<lastUpdated>#<id>` when either component is
1779
+ * missing.
1780
+ */
1781
+ gsi1sk: roleAssignmentGsi1skAttribute,
1782
+ deleted: {
1783
+ type: "boolean",
1784
+ required: false
1785
+ },
1786
+ bundleId: {
1787
+ type: "string",
1788
+ required: false
1789
+ },
1790
+ msgId: {
1791
+ type: "string",
1792
+ required: false
1793
+ },
1794
+ /**
1795
+ * Denormalized display name of the linked Tenant, captured at row
1796
+ * last-write time. Promoted to a top-level attribute so the ADR-018
1797
+ * adjacency-list user-projection SK (pattern #5 —
1798
+ * `ROLEASSIGNMENT#TENANT#<normalizedRoleName>#<roleId>#TID#<tenantId>#<id>`)
1799
+ * can be composed from a top-level field instead of digging into the
1800
+ * `resource` JSON. Optional on the schema so pre-TR-024 rows do not
1801
+ * break; the operations-layer multi-write helper (#1010) makes the
1802
+ * field load-bearing at write time per TR-024 rule 2 (write-time
1803
+ * source = canonical Tenant.displayName).
1804
+ * @see TR-024 — Denormalized display-name attributes
1805
+ */
1806
+ denormalizedTenantName: {
1807
+ type: "string",
1808
+ required: false
1809
+ },
1810
+ /**
1811
+ * Denormalized display name of the linked User, captured at row
1812
+ * last-write time. Promoted to a top-level attribute so the ADR-018
1813
+ * adjacency-list canonical-record GSI1SK (pattern #8 —
1814
+ * `<roleId>#<normalizedUserName>#<id>`) and workspace-projection SK
1815
+ * (pattern #9) can be composed from a top-level field. Optional on
1816
+ * the schema so pre-TR-024 rows do not break; the operations-layer
1817
+ * multi-write helper (#1010) makes the field load-bearing at write
1818
+ * time per TR-024 rule 2 (write-time source = canonical
1819
+ * User.displayName).
1820
+ * @see TR-024 — Denormalized display-name attributes
1821
+ */
1822
+ denormalizedUserName: {
1823
+ type: "string",
1824
+ required: false
1825
+ },
1826
+ /**
1827
+ * Denormalized display name of the linked Role, captured at row
1828
+ * last-write time. Promoted to a top-level attribute so the ADR-018
1829
+ * adjacency-list user-projection SK (pattern #5 —
1830
+ * `ROLEASSIGNMENT#TENANT#<normalizedRoleName>#…`) can be composed from
1831
+ * a top-level field. Optional on the schema so pre-TR-024 rows do not
1832
+ * break; the operations-layer multi-write helper (#1010) makes the
1833
+ * field load-bearing at write time per TR-024 rule 2 (write-time
1834
+ * source = canonical Role.displayName).
1835
+ * @see TR-024 — Denormalized display-name attributes
1836
+ */
1837
+ denormalizedRoleName: {
1838
+ type: "string",
1839
+ required: false
1840
+ }
1841
+ },
1842
+ indexes: {
1843
+ /** Base table: PK = TID#<tenantId>#ROLEASSIGNMENT#ID#<id>, SK = CURRENT. Do not supply PK or SK from outside. */
1844
+ record: {
1845
+ pk: {
1846
+ field: "PK",
1847
+ composite: ["tenantId", "id"],
1848
+ template: "TID#${tenantId}#ROLEASSIGNMENT#ID#${id}"
1849
+ },
1850
+ sk: {
1851
+ field: "SK",
1852
+ composite: ["sk"],
1853
+ template: "${sk}"
1854
+ }
1855
+ },
1856
+ /**
1857
+ * GSI1 — Unified Sharded List per ADR-011: list all RoleAssignments for a tenant across the
1858
+ * four shards. Tenant-scoped only, so `WID#-` is a sentinel.
1859
+ * SK is derived via `roleAssignmentGsi1skAttribute` — composes the
1860
+ * discriminator-first `<roleId>#<normalizedUserName>#<id>` shape per
1861
+ * ADR-018 pattern #8 (users with a specific role in a tenant, sorted
1862
+ * by user name); falls back to `<lastUpdated>#<id>` when either
1863
+ * component is missing. `casing: "none"` preserves the normalized
1864
+ * label and ISO-8601 `T`/`Z`.
1865
+ */
1866
+ gsi1: {
1867
+ index: "GSI1",
1868
+ pk: {
1869
+ field: "GSI1PK",
1870
+ composite: ["tenantId", "gsi1Shard"],
1871
+ template: "TID#${tenantId}#WID#-#RT#RoleAssignment#SHARD#${gsi1Shard}"
1872
+ },
1873
+ sk: {
1874
+ field: "GSI1SK",
1875
+ casing: "none",
1876
+ composite: ["gsi1sk"],
1877
+ template: "${gsi1sk}"
1878
+ }
1879
+ }
1880
+ }
1881
+ });
1882
+
1883
+ // src/data/dynamo/entities/control/roleassignment-user-projection-entity.ts
1884
+ var import_electrodb9 = require("electrodb");
1885
+ var RoleAssignmentUserProjectionEntity = new import_electrodb9.Entity({
1886
+ model: {
1887
+ entity: "roleAssignmentUserProjection",
1888
+ service: "control",
1889
+ version: "01"
1890
+ },
1891
+ attributes: {
1892
+ /**
1893
+ * User partition discriminator. Renders as `USER#ID#<userId>` on the
1894
+ * base-table PK. Always required — the projection has no meaning
1895
+ * outside a user partition.
1896
+ */
1897
+ userId: {
1898
+ type: "string",
1899
+ required: true
1900
+ },
1901
+ /**
1902
+ * Pre-composed sort key — built by the operations-layer projection
1903
+ * writer via `buildRoleAssignmentUserProjectionSk*` helpers. The
1904
+ * entity stores the value verbatim so the SK grammar (tenant-lane
1905
+ * vs workspace-lane) is owned by the operations layer, not
1906
+ * duplicated here.
1907
+ */
1908
+ sk: {
1909
+ type: "string",
1910
+ required: true
1911
+ },
1912
+ /** Tenant in which the role assignment applies. Always required. */
1913
+ tenantId: {
1914
+ type: "string",
1915
+ required: true
1916
+ },
1917
+ /**
1918
+ * Workspace the role assignment scopes to. Present iff the
1919
+ * projection row is the workspace-level sub-lane; absent for
1920
+ * tenant-level sub-lane rows.
1921
+ */
1922
+ workspaceId: {
1923
+ type: "string",
1924
+ required: false
1925
+ },
1926
+ /**
1927
+ * Role the assignment grants. Stored as a discriminating field so
1928
+ * `Query(PK = USER#ID#<userId>, SK begins_with 'ROLEASSIGNMENT#…')`
1929
+ * results carry the role id without a hop to the canonical row.
1930
+ */
1931
+ roleId: {
1932
+ type: "string",
1933
+ required: true
1934
+ },
1935
+ /**
1936
+ * RoleAssignment canonical-record id. Stored as a discriminating
1937
+ * field so consumers can hydrate the canonical row via
1938
+ * `RoleAssignmentEntity.get({ tenantId, id: roleAssignmentId })`
1939
+ * when the projection's `summary` is insufficient.
1940
+ */
1941
+ roleAssignmentId: {
1942
+ type: "string",
1943
+ required: true
1944
+ },
1945
+ /**
1946
+ * Summary projection (key display fields as JSON string: id,
1947
+ * displayName, status) — mirrored from the canonical RoleAssignment
1948
+ * row so user-partition queries do not need a BatchGet hop.
1949
+ */
1950
+ summary: {
1951
+ type: "string",
1952
+ required: true
1953
+ },
1954
+ /** Version id mirrored from the canonical RoleAssignment row. */
1955
+ vid: {
1956
+ type: "string",
1957
+ required: true
1958
+ },
1959
+ /** Last-updated timestamp mirrored from the canonical RoleAssignment row. */
1960
+ lastUpdated: {
1961
+ type: "string",
1962
+ required: true
1963
+ },
1964
+ /**
1965
+ * Denormalized Tenant display name — mirrored from the canonical
1966
+ * RoleAssignment row per TR-024 rule 3 (canonical-record symmetry).
1967
+ * Optional on the schema because pre-TR-024 rows may not carry a
1968
+ * display name; the operations layer falls back gracefully when
1969
+ * missing.
1970
+ */
1971
+ denormalizedTenantName: {
1972
+ type: "string",
1973
+ required: false
1974
+ },
1975
+ /**
1976
+ * Denormalized User display name — mirrored from the canonical
1977
+ * RoleAssignment row per TR-024 rule 3 (canonical-record symmetry).
1978
+ * Carried on the projection so consumers can render the user's
1979
+ * display name without a hop to the User record.
1980
+ */
1981
+ denormalizedUserName: {
1982
+ type: "string",
1983
+ required: false
1984
+ },
1985
+ /**
1986
+ * Denormalized Role display name — required to compose the SK's
1987
+ * `<normalizedRoleName>` segment. Optional on the schema (pre-TR-024
1988
+ * rows fall back to a sentinel) but expected to be present at write
1989
+ * time per TR-024 rule 2 (write-time source =
1990
+ * canonical Role.displayName).
1991
+ */
1992
+ denormalizedRoleName: {
1993
+ type: "string",
1994
+ required: false
1995
+ }
1996
+ },
1997
+ indexes: {
1998
+ /**
1999
+ * Base table: PK = USER#ID#\<userId\>, SK = operation-supplied. Both
2000
+ * sub-lanes (tenant-level and workspace-level) use this same index —
2001
+ * the SK string encodes the lane discriminator
2002
+ * (`ROLEASSIGNMENT#TENANT#…` vs `ROLEASSIGNMENT#WORKSPACE#…`) so a
2003
+ * single `Query(PK = USER#ID#<userId>, SK begins_with 'ROLEASSIGNMENT#')`
2004
+ * returns both lanes interleaved.
2005
+ */
2006
+ record: {
2007
+ pk: {
2008
+ field: "PK",
2009
+ composite: ["userId"],
2010
+ template: "USER#ID#${userId}"
2011
+ },
2012
+ sk: {
2013
+ field: "SK",
2014
+ casing: "none",
2015
+ composite: ["sk"],
2016
+ template: "${sk}"
2017
+ }
2018
+ }
2019
+ }
2020
+ });
2021
+
2022
+ // src/data/dynamo/entities/control/roleassignment-workspace-projection-entity.ts
2023
+ var import_electrodb10 = require("electrodb");
2024
+ var RoleAssignmentWorkspaceProjectionEntity = new import_electrodb10.Entity({
2025
+ model: {
2026
+ entity: "roleAssignmentWorkspaceProjection",
2027
+ service: "control",
2028
+ version: "01"
2029
+ },
2030
+ attributes: {
2031
+ /**
2032
+ * Tenant the workspace belongs to. Renders as the leading segment
2033
+ * of the base-table PK. Always required — the workspace partition
2034
+ * is tenant-scoped per ADR-011.
2035
+ */
2036
+ tenantId: {
2037
+ type: "string",
2038
+ required: true
2039
+ },
2040
+ /**
2041
+ * Workspace partition discriminator. Renders as the trailing
2042
+ * segment of the base-table PK
2043
+ * (`TID#<tenantId>#WORKSPACE#ID#<workspaceId>`). Always required —
2044
+ * the projection has no meaning outside a workspace partition.
2045
+ */
2046
+ workspaceId: {
2047
+ type: "string",
2048
+ required: true
2049
+ },
2050
+ /**
2051
+ * Pre-composed sort key — built by the operations-layer projection
2052
+ * writer via `buildRoleAssignmentWorkspaceProjectionSk`. The entity
2053
+ * stores the value verbatim so the SK grammar (pattern #9) is
2054
+ * owned by the operations layer, not duplicated here.
2055
+ */
2056
+ sk: {
2057
+ type: "string",
2058
+ required: true
2059
+ },
2060
+ /**
2061
+ * User the role assignment grants the role to. Stored as a
2062
+ * discriminating field so consumers can hydrate the canonical User
2063
+ * row via `UserEntity.get({ id: userId, sk: "CURRENT" })` when the
2064
+ * projection's `summary` is insufficient.
2065
+ */
2066
+ userId: {
2067
+ type: "string",
2068
+ required: true
2069
+ },
2070
+ /**
2071
+ * Role the assignment grants. Stored as a discriminating field —
2072
+ * also rendered into the SK as the discriminator-first segment so
2073
+ * `begins_with('ROLEASSIGNMENT#<roleId>#')` filters one role.
2074
+ */
2075
+ roleId: {
2076
+ type: "string",
2077
+ required: true
2078
+ },
2079
+ /**
2080
+ * RoleAssignment canonical-record id. Stored as a discriminating
2081
+ * field so consumers can hydrate the canonical row via
2082
+ * `RoleAssignmentEntity.get({ tenantId, id: roleAssignmentId })`
2083
+ * when the projection's `summary` is insufficient.
2084
+ */
2085
+ roleAssignmentId: {
2086
+ type: "string",
2087
+ required: true
2088
+ },
2089
+ /**
2090
+ * Summary projection (key display fields as JSON string: id,
2091
+ * displayName, status) — mirrored from the canonical RoleAssignment
2092
+ * row so workspace-partition queries do not need a BatchGet hop.
2093
+ */
2094
+ summary: {
2095
+ type: "string",
2096
+ required: true
2097
+ },
2098
+ /** Version id mirrored from the canonical RoleAssignment row. */
2099
+ vid: {
2100
+ type: "string",
2101
+ required: true
2102
+ },
2103
+ /** Last-updated timestamp mirrored from the canonical RoleAssignment row. */
2104
+ lastUpdated: {
2105
+ type: "string",
2106
+ required: true
2107
+ },
2108
+ /**
2109
+ * Denormalized User display name — required to compose the
2110
+ * pattern-#9 SK (`ROLEASSIGNMENT#<roleId>#<normalizedUserName>#…`).
2111
+ * Optional on the schema because pre-TR-024 rows may not carry a
2112
+ * display name; the operations layer falls back to a sentinel when
2113
+ * missing so the SK still has a valid shape. The TR-023 rename-
2114
+ * cascade pipeline rewrites the SK on a User rename.
2115
+ */
2116
+ denormalizedUserName: {
2117
+ type: "string",
2118
+ required: false
2119
+ },
2120
+ /**
2121
+ * Denormalized Role display name — mirrored from the canonical
2122
+ * RoleAssignment row per TR-024 rule 3 (canonical-record symmetry).
2123
+ * Carried on the projection so consumers can render the role's
2124
+ * display name without a hop to the Role record. Not part of the
2125
+ * SK (pattern #9 sorts on `<normalizedUserName>`, not role name) —
2126
+ * a Role rename does NOT rewrite this SK.
2127
+ */
2128
+ denormalizedRoleName: {
2129
+ type: "string",
2130
+ required: false
2131
+ }
2132
+ },
2133
+ indexes: {
2134
+ /**
2135
+ * Base table: PK = TID#<tenantId>#WORKSPACE#ID#<workspaceId>,
2136
+ * SK = operation-supplied. Pattern #9 uses this index — the SK
2137
+ * encodes the entity-type prefix and discriminator-first roleId
2138
+ * (`ROLEASSIGNMENT#<roleId>#…`) so
2139
+ * `Query(PK = TID#<tenantId>#WORKSPACE#ID#<workspaceId>, SK begins_with 'ROLEASSIGNMENT#<roleId>#')`
2140
+ * returns every user-assignment for that role in the workspace, sorted
2141
+ * by normalized user name.
2142
+ */
2143
+ record: {
2144
+ pk: {
2145
+ field: "PK",
2146
+ composite: ["tenantId", "workspaceId"],
2147
+ template: "TID#${tenantId}#WORKSPACE#ID#${workspaceId}"
2148
+ },
2149
+ sk: {
2150
+ field: "SK",
2151
+ casing: "none",
2152
+ composite: ["sk"],
2153
+ template: "${sk}"
2154
+ }
2155
+ }
2156
+ }
2157
+ });
2158
+
2159
+ // src/data/dynamo/entities/control/tenant-entity.ts
2160
+ var import_electrodb11 = require("electrodb");
2161
+ var TenantEntity = new import_electrodb11.Entity({
2162
+ model: {
2163
+ entity: "tenant",
2164
+ service: "control",
2165
+ version: "01"
2166
+ },
2167
+ attributes: {
2168
+ /** Sort key sentinel. Always "CURRENT". */
2169
+ sk: {
2170
+ type: "string",
2171
+ required: true,
2172
+ default: "CURRENT"
2173
+ },
2174
+ /** The tenant's own id (= resource id). Drives the partition key. */
2175
+ tenantId: {
2176
+ type: "string",
2177
+ required: true
2178
+ },
2179
+ /** FHIR Resource.id; logical id in URL. Equals tenantId. */
2180
+ id: {
2181
+ type: "string",
2182
+ required: true
2183
+ },
2184
+ /** Full Tenant resource serialized as JSON string. */
2185
+ resource: {
2186
+ type: "string",
2187
+ required: true
2188
+ },
2189
+ /**
2190
+ * Summary projection (key display fields as JSON string: id, displayName, status).
2191
+ * Populated on every write via extractSummary(resource); GSI1 INCLUDE surfaces it on lists.
2192
+ */
2193
+ summary: {
2194
+ type: "string",
2195
+ required: true
2196
+ },
2197
+ /** Version id (e.g. ULID). */
2198
+ vid: {
2199
+ type: "string",
2200
+ required: true
2201
+ },
2202
+ lastUpdated: {
2203
+ type: "string",
2204
+ required: true
2205
+ },
2206
+ /**
2207
+ * ADR-028 denormalized counter — number of tenant-scoped Memberships
2208
+ * (users) in this tenant. Maintained by the counter-maintenance
2209
+ * consumer via atomic ADD; absent/0 until first event or reconciliation.
2210
+ */
2211
+ usersInTenant: {
2212
+ type: "number",
2213
+ required: false
2214
+ },
2215
+ /**
2216
+ * ADR-028 denormalized counter — number of Workspaces in this tenant.
2217
+ * Maintained by the counter-maintenance consumer via atomic ADD;
2218
+ * absent/0 until first event or reconciliation.
2219
+ */
2220
+ workspacesInTenant: {
2221
+ type: "number",
2222
+ required: false
2223
+ },
2224
+ gsi1Shard: gsi1ShardAttribute,
2225
+ /** Derived GSI1 sort key — name-based when extractable; else `<lastUpdated>#<id>`. */
2226
+ gsi1sk: gsi1skAttribute,
2227
+ deleted: {
2228
+ type: "boolean",
2229
+ required: false
2230
+ },
2231
+ bundleId: {
2232
+ type: "string",
2233
+ required: false
2234
+ },
2235
+ msgId: {
2236
+ type: "string",
2237
+ required: false
2238
+ }
2239
+ },
2240
+ indexes: {
2241
+ /** Base table: PK = TENANT#ID#<tenantId>, SK = CURRENT. Do not supply PK or SK from outside. */
2242
+ record: {
2243
+ pk: {
2244
+ field: "PK",
2245
+ composite: ["tenantId"],
2246
+ template: "TENANT#ID#${tenantId}"
2247
+ },
2248
+ sk: {
2249
+ field: "SK",
2250
+ composite: ["sk"],
2251
+ template: "${sk}"
2252
+ }
2253
+ },
2254
+ /**
2255
+ * GSI1 — Unified Sharded List per ADR-011: list all Tenants across the four shards.
2256
+ * Tenant lives at the platform tier (no parent tenant or workspace), so `TID#-#WID#-`
2257
+ * sentinels precede `RT#Tenant#SHARD#<n>`. SK is derived via `gsi1skAttribute` —
2258
+ * `<normalizedName>#<id>` when the resource carries a `name`, else `<lastUpdated>#<id>`
2259
+ * (DR-004). `casing: "none"` preserves the normalized label and ISO-8601 `T`/`Z`.
2260
+ */
2261
+ gsi1: {
2262
+ index: "GSI1",
2263
+ pk: {
2264
+ field: "GSI1PK",
2265
+ composite: ["gsi1Shard"],
2266
+ template: "TID#-#WID#-#RT#Tenant#SHARD#${gsi1Shard}"
2267
+ },
2268
+ sk: {
2269
+ field: "GSI1SK",
2270
+ casing: "none",
2271
+ composite: ["gsi1sk"],
2272
+ template: "${gsi1sk}"
2273
+ }
2274
+ }
2275
+ }
2276
+ });
2277
+
2278
+ // src/data/dynamo/entities/control/user-entity.ts
2279
+ var import_electrodb12 = require("electrodb");
2280
+ var UserEntity = new import_electrodb12.Entity({
2281
+ model: {
2282
+ entity: "user",
2283
+ service: "control",
2284
+ version: "01"
2285
+ },
2286
+ attributes: {
2287
+ /** Sort key sentinel. Always "CURRENT". */
2288
+ sk: {
2289
+ type: "string",
2290
+ required: true,
2291
+ default: "CURRENT"
2292
+ },
2293
+ /** FHIR Resource.id; platform user id (ohi_uid). */
2294
+ id: {
2295
+ type: "string",
2296
+ required: true
2297
+ },
2298
+ /** Full User resource serialized as JSON string. */
2299
+ resource: {
2300
+ type: "string",
2301
+ required: true
2302
+ },
2303
+ /**
2304
+ * Summary projection (key display fields as JSON string: id, displayName, status).
2305
+ * Populated on every write via extractSummary(resource); GSI1 INCLUDE surfaces it on lists.
2306
+ */
2307
+ summary: {
2308
+ type: "string",
2309
+ required: true
2310
+ },
2311
+ /**
2312
+ * Immutable Cognito-issued `sub` claim. Drives GSI2 (sub-lookup). Optional until the
2313
+ * Post Confirmation Lambda (#770) lands; required thereafter.
2314
+ */
2315
+ cognitoSub: {
2316
+ type: "string",
2317
+ required: false
2318
+ },
2319
+ /** Version id (e.g. ULID). */
2320
+ vid: {
2321
+ type: "string",
2322
+ required: true
2323
+ },
2324
+ lastUpdated: {
2325
+ type: "string",
2326
+ required: true
2327
+ },
2328
+ /**
2329
+ * ADR-028 denormalized counter — number of tenant-scoped Memberships
2330
+ * (tenants) this user belongs to. Maintained by the
2331
+ * counter-maintenance consumer via atomic ADD; absent/0 until first
2332
+ * event or reconciliation.
2333
+ */
2334
+ tenantsForUser: {
2335
+ type: "number",
2336
+ required: false
2337
+ },
2338
+ /**
2339
+ * ADR-028 denormalized counter — number of workspace-scoped
2340
+ * Memberships (workspaces) this user belongs to. Maintained by the
2341
+ * counter-maintenance consumer via atomic ADD; absent/0 until first
2342
+ * event or reconciliation.
2343
+ */
2344
+ workspacesForUser: {
2345
+ type: "number",
2346
+ required: false
2347
+ },
2348
+ gsi1Shard: gsi1ShardAttribute,
2349
+ /** Derived GSI1 sort key — name-based when extractable; else `<lastUpdated>#<id>`. */
2350
+ gsi1sk: gsi1skAttribute,
2351
+ deleted: {
2352
+ type: "boolean",
2353
+ required: false
2354
+ },
2355
+ /**
2356
+ * TR-022 / ADR-018 lifecycle state for the cascade pipeline.
2357
+ *
2358
+ * - `active` (or undefined) — normal, readable state.
2359
+ * - `deleting` — intermediate state set synchronously by the
2360
+ * hard-delete API entry point. The owning-delete cascade state
2361
+ * machine fans out from this transition (DynamoDB stream →
2362
+ * `control-plane.owning-delete.v1` → Step Functions). Readers MUST
2363
+ * short-circuit on `deleting` so partial cascades stay invisible.
2364
+ * - `deleted-failed` — terminal failure state set by the cascade
2365
+ * finalize Lambda when the cascade run fails irrecoverably.
2366
+ * Operators recover by re-running the cascade or by direct
2367
+ * intervention.
2368
+ *
2369
+ * The cascade finalize step deletes the canonical record conditional
2370
+ * on `lifecycleState = "deleting"`; on replay the conditional check
2371
+ * fails and the finalize step treats that as a no-op success.
2372
+ */
2373
+ lifecycleState: {
2374
+ type: ["active", "deleting", "deleted-failed"],
2375
+ required: false
2376
+ },
2377
+ bundleId: {
2378
+ type: "string",
2379
+ required: false
2380
+ },
2381
+ msgId: {
2382
+ type: "string",
2383
+ required: false
2384
+ }
2385
+ },
2386
+ indexes: {
2387
+ /** Base table: PK = USER#ID#<id>, SK = CURRENT. Do not supply PK or SK from outside. */
2388
+ record: {
2389
+ pk: {
2390
+ field: "PK",
2391
+ composite: ["id"],
2392
+ template: "USER#ID#${id}"
2393
+ },
2394
+ sk: {
2395
+ field: "SK",
2396
+ composite: ["sk"],
2397
+ template: "${sk}"
2398
+ }
2399
+ },
2400
+ /**
2401
+ * GSI1 — Unified Sharded List per ADR-011: list all Users across the four shards.
2402
+ * Non-tenant-isolated, so `TID#-#WID#-` sentinels precede `RT#User#SHARD#<n>`.
2403
+ * SK is derived via `gsi1skAttribute` — uses the resource's natural label when
2404
+ * extractable (string `name`/`title` via introspection), else `<lastUpdated>#<id>`
2405
+ * (DR-004). `casing: "none"` preserves the normalized label and ISO-8601 `T`/`Z`.
2406
+ */
2407
+ gsi1: {
2408
+ index: "GSI1",
2409
+ pk: {
2410
+ field: "GSI1PK",
2411
+ composite: ["gsi1Shard"],
2412
+ template: "TID#-#WID#-#RT#User#SHARD#${gsi1Shard}"
2413
+ },
2414
+ sk: {
2415
+ field: "GSI1SK",
2416
+ casing: "none",
2417
+ composite: ["gsi1sk"],
2418
+ template: "${gsi1sk}"
2419
+ }
2420
+ },
2421
+ /**
2422
+ * GSI2 — Cognito sub-lookup per ADR-011: resolves the UserEntity from a Cognito `sub` claim.
2423
+ * `condition` skips the index when `cognitoSub` is missing so legacy items without a sub are
2424
+ * not indexed.
2425
+ */
2426
+ gsi2: {
2427
+ index: "GSI2",
2428
+ condition: (attrs) => typeof attrs.cognitoSub === "string" && attrs.cognitoSub.length > 0,
2429
+ pk: {
2430
+ field: "GSI2PK",
2431
+ casing: "none",
2432
+ composite: ["cognitoSub"],
2433
+ template: "USER#SUB#${cognitoSub}"
2434
+ },
2435
+ sk: {
2436
+ field: "GSI2SK",
2437
+ casing: "none",
2438
+ composite: [],
2439
+ template: "CURRENT"
2440
+ }
2441
+ }
2442
+ }
2443
+ });
2444
+
2445
+ // src/data/dynamo/entities/control/workspace-entity.ts
2446
+ var import_electrodb13 = require("electrodb");
2447
+ var WorkspaceEntity = new import_electrodb13.Entity({
2448
+ model: {
2449
+ entity: "workspace",
2450
+ service: "control",
2451
+ version: "01"
2452
+ },
2453
+ attributes: {
2454
+ /** Sort key sentinel. Always "CURRENT". */
2455
+ sk: {
2456
+ type: "string",
2457
+ required: true,
2458
+ default: "CURRENT"
2459
+ },
2460
+ /** Tenant that contains this workspace (required). */
2461
+ tenantId: {
2462
+ type: "string",
2463
+ required: true
2464
+ },
2465
+ /** FHIR Resource.id; logical id in URL. */
2466
+ id: {
2467
+ type: "string",
2468
+ required: true
2469
+ },
2470
+ /** Full Workspace resource serialized as JSON string. */
2471
+ resource: {
2472
+ type: "string",
2473
+ required: true
2474
+ },
2475
+ /**
2476
+ * Summary projection (key display fields as JSON string: id, displayName, status).
2477
+ * Populated on every write via extractSummary(resource); GSI1 INCLUDE surfaces it on lists.
2478
+ */
2479
+ summary: {
2480
+ type: "string",
2481
+ required: true
2482
+ },
2483
+ /** Version id (e.g. ULID). */
2484
+ vid: {
2485
+ type: "string",
2486
+ required: true
2487
+ },
2488
+ lastUpdated: {
2489
+ type: "string",
2490
+ required: true
2491
+ },
2492
+ /**
2493
+ * ADR-028 denormalized counter — number of workspace-scoped
2494
+ * Memberships (users) in this workspace. Maintained by the
2495
+ * counter-maintenance consumer via atomic ADD; absent/0 until first
2496
+ * event or reconciliation.
2497
+ */
2498
+ usersInWorkspace: {
2499
+ type: "number",
2500
+ required: false
2501
+ },
2502
+ /**
2503
+ * ADR-028 denormalized counter — number of workspace-scoped
2504
+ * RoleAssignments classified as admin-tier in this workspace.
2505
+ * Maintained by the counter-maintenance consumer via atomic ADD;
2506
+ * absent/0 until first event or reconciliation.
2507
+ */
2508
+ adminUsersInWorkspace: {
2509
+ type: "number",
2510
+ required: false
2511
+ },
2512
+ /**
2513
+ * ADR-028 denormalized counter — number of workspace-scoped
2514
+ * RoleAssignments classified as non-admin in this workspace.
2515
+ * Maintained by the counter-maintenance consumer via atomic ADD;
2516
+ * absent/0 until first event or reconciliation.
2517
+ */
2518
+ normalUsersInWorkspace: {
2519
+ type: "number",
2520
+ required: false
2521
+ },
2522
+ gsi1Shard: gsi1ShardAttribute,
2523
+ /** Derived GSI1 sort key — name-based when extractable; else `<lastUpdated>#<id>`. */
2524
+ gsi1sk: gsi1skAttribute,
2525
+ deleted: {
2526
+ type: "boolean",
2527
+ required: false
2528
+ },
2529
+ /**
2530
+ * TR-022 / ADR-018 lifecycle state for the cascade pipeline.
2531
+ *
2532
+ * - `active` (or undefined) — normal, readable state.
2533
+ * - `deleting` — intermediate state set synchronously by the
2534
+ * hard-delete API entry point. The owning-delete cascade state
2535
+ * machine fans out from this transition (DynamoDB stream →
2536
+ * `control-plane.owning-delete.v1` → Step Functions). Readers MUST
2537
+ * short-circuit on `deleting` so partial cascades stay invisible.
2538
+ * - `deleted-failed` — terminal failure state set by the cascade
2539
+ * finalize Lambda when the cascade run fails irrecoverably.
2540
+ * Operators recover by re-running the cascade or by direct
2541
+ * intervention.
2542
+ *
2543
+ * The cascade finalize step deletes the canonical record conditional
2544
+ * on `lifecycleState = "deleting"`; on replay the conditional check
2545
+ * fails and the finalize step treats that as a no-op success.
2546
+ */
2547
+ lifecycleState: {
2548
+ type: ["active", "deleting", "deleted-failed"],
2549
+ required: false
2550
+ },
2551
+ bundleId: {
2552
+ type: "string",
2553
+ required: false
2554
+ },
2555
+ msgId: {
2556
+ type: "string",
2557
+ required: false
2558
+ }
2559
+ },
2560
+ indexes: {
2561
+ /** Base table: PK = TID#<tenantId>#WORKSPACE#ID#<id>, SK = CURRENT. Do not supply PK or SK from outside. */
2562
+ record: {
2563
+ pk: {
2564
+ field: "PK",
2565
+ composite: ["tenantId", "id"],
2566
+ template: "TID#${tenantId}#WORKSPACE#ID#${id}"
2567
+ },
2568
+ sk: {
2569
+ field: "SK",
2570
+ composite: ["sk"],
2571
+ template: "${sk}"
2572
+ }
2573
+ },
2574
+ /**
2575
+ * GSI1 — Unified Sharded List per ADR-011: list all Workspaces for a tenant across the
2576
+ * four shards. Workspace is itself the workspace identity, so `WID#-` is a sentinel.
2577
+ * SK is derived via `gsi1skAttribute` — `<normalizedName>#<id>` when the resource
2578
+ * carries a `name`, else `<lastUpdated>#<id>` (DR-004). `casing: "none"` preserves
2579
+ * the normalized label and ISO-8601 `T`/`Z`.
2580
+ */
2581
+ gsi1: {
2582
+ index: "GSI1",
2583
+ pk: {
2584
+ field: "GSI1PK",
2585
+ composite: ["tenantId", "gsi1Shard"],
2586
+ template: "TID#${tenantId}#WID#-#RT#Workspace#SHARD#${gsi1Shard}"
2587
+ },
2588
+ sk: {
2589
+ field: "GSI1SK",
2590
+ casing: "none",
2591
+ composite: ["gsi1sk"],
2592
+ template: "${gsi1sk}"
2593
+ }
2594
+ }
2595
+ }
2596
+ });
2597
+
2598
+ // src/data/dynamo/dynamo-control-service.ts
2599
+ var controlPlaneEntities = {
2600
+ configuration: ConfigurationEntity,
2601
+ configurationUserProjection: ConfigurationUserProjectionEntity,
2602
+ configurationWorkspaceProjection: ConfigurationWorkspaceProjectionEntity,
2603
+ membership: MembershipEntity,
2604
+ membershipUserProjection: MembershipUserProjectionEntity,
2605
+ membershipWorkspaceProjection: MembershipWorkspaceProjectionEntity,
2606
+ role: RoleEntity,
2607
+ roleAssignment: RoleAssignmentEntity,
2608
+ roleAssignmentUserProjection: RoleAssignmentUserProjectionEntity,
2609
+ roleAssignmentWorkspaceProjection: RoleAssignmentWorkspaceProjectionEntity,
2610
+ tenant: TenantEntity,
2611
+ user: UserEntity,
2612
+ workspace: WorkspaceEntity
2613
+ };
2614
+ var controlPlaneService = new import_electrodb14.Service(controlPlaneEntities, {
2615
+ table: defaultTableName,
2616
+ client: dynamoClient
2617
+ });
2618
+ var DynamoControlService = {
2619
+ entities: controlPlaneService.entities,
2620
+ transaction: controlPlaneService.transaction
2621
+ };
2622
+ function getDynamoControlService(tableName) {
2623
+ const resolved = tableName ?? defaultTableName;
2624
+ const service = new import_electrodb14.Service(controlPlaneEntities, {
2625
+ table: resolved,
2626
+ client: dynamoClient
2627
+ });
2628
+ return {
2629
+ entities: service.entities,
2630
+ transaction: service.transaction
2631
+ };
2632
+ }
2633
+
2634
+ // src/data/operations/control/counters/counter-apply-operation.ts
2635
+ var COUNTER_TARGET = {
2636
+ Tenant: "Tenant",
2637
+ Workspace: "Workspace",
2638
+ User: "User"
2639
+ };
2640
+
2641
+ // src/data/operations/control/counters/role-admin-classification.ts
2642
+ function isAdminRoleAssignment(input) {
2643
+ if (codeIsAdminTier(input.roleLevel)) {
2644
+ return true;
2645
+ }
2646
+ if (idMatchesAdmin(input.roleId)) {
2647
+ return true;
2648
+ }
2649
+ return false;
2650
+ }
2651
+ function codeIsAdminTier(roleLevel) {
2652
+ if (typeof roleLevel !== "string" || roleLevel.length === 0) {
2653
+ return false;
2654
+ }
2655
+ const lower = roleLevel.toLowerCase();
2656
+ return lower === "admin" || lower.endsWith("admin");
2657
+ }
2658
+ function idMatchesAdmin(roleId) {
2659
+ if (typeof roleId !== "string" || roleId.length === 0) {
2660
+ return false;
2661
+ }
2662
+ return roleId.toLowerCase().includes("admin");
2663
+ }
2664
+
2665
+ // src/data/operations/control/control-event-publisher.ts
2666
+ var import_client_eventbridge = require("@aws-sdk/client-eventbridge");
2667
+ var import_workflows = __toESM(require_lib());
2668
+ function extractRoleLevel(resource) {
2669
+ const code = resource?.code;
2670
+ const first = code?.coding?.[0]?.code;
2671
+ return typeof first === "string" && first.length > 0 ? first : void 0;
2672
+ }
2673
+
2674
+ // src/data/operations/control/membership/membership-list-by-user-operation.ts
2675
+ function buildSkPrefix(mode, tenantId) {
2676
+ switch (mode) {
2677
+ case "tenant":
2678
+ return "MEMBERSHIP#TENANT#";
2679
+ case "workspace":
2680
+ return "MEMBERSHIP#WORKSPACE#";
2681
+ case "workspaceInTenant":
2682
+ return `MEMBERSHIP#WORKSPACE#TID#${tenantId}#`;
2683
+ case "all":
2684
+ default:
2685
+ return "MEMBERSHIP#";
2686
+ }
2687
+ }
2688
+
2689
+ // src/data/operations/control/membership/membership-count-by-user-operation.ts
2690
+ async function countMembershipsByUserOperation(params) {
2691
+ const { userId, mode = "all", tenantId, tableName } = params;
2692
+ if (mode === "workspaceInTenant" && !tenantId) {
2693
+ throw new Error(
2694
+ 'countMembershipsByUserOperation: tenantId is required when mode === "workspaceInTenant"'
2695
+ );
2696
+ }
2697
+ const service = getDynamoControlService(tableName);
2698
+ const skPrefix = buildSkPrefix(mode, tenantId);
2699
+ const result = await service.entities.membershipUserProjection.query.record({ userId }).begins({ sk: skPrefix }).go({ pages: "all", attributes: ["membershipId"] });
2700
+ return (result.data ?? []).length;
2701
+ }
2702
+
2703
+ // src/data/operations/control/membership/membership-list-by-workspace-operation.ts
2704
+ async function membershipListByWorkspaceOperation(params) {
2705
+ const {
2706
+ tenantId,
2707
+ workspaceId,
2708
+ cursor = null,
2709
+ limit,
2710
+ order,
2711
+ tableName
2712
+ } = params;
2713
+ const service = getDynamoControlService(tableName);
2714
+ const goOptions = {
2715
+ cursor
2716
+ };
2717
+ if (limit !== void 0) {
2718
+ goOptions.limit = limit;
2719
+ }
2720
+ if (order !== void 0) {
2721
+ goOptions.order = order;
2722
+ }
2723
+ const result = await service.entities.membershipWorkspaceProjection.query.record({ tenantId, workspaceId }).begins({ sk: "MEMBERSHIP#" }).go(goOptions);
2724
+ const items = (result.data ?? []).map((row) => ({
2725
+ tenantId: row.tenantId,
2726
+ workspaceId: row.workspaceId,
2727
+ sk: row.sk,
2728
+ userId: row.userId,
2729
+ membershipId: row.membershipId,
2730
+ summary: row.summary,
2731
+ vid: row.vid,
2732
+ lastUpdated: row.lastUpdated,
2733
+ denormalizedUserName: row.denormalizedUserName
2734
+ }));
2735
+ return { items, cursor: result.cursor ?? null };
2736
+ }
2737
+
2738
+ // src/data/operations/data-operations-common.ts
2739
+ var import_types2 = require("@openhi/types");
2740
+
2741
+ // src/lib/compression.ts
2742
+ var import_node_zlib = require("zlib");
2743
+
2744
+ // src/data/operations/data-operations-common.ts
2745
+ var BATCH_GET_MAX_ATTEMPTS = 3;
2746
+ var BATCH_GET_BASE_BACKOFF_MS = 50;
2747
+ async function batchGetWithRetry(entity, keys) {
2748
+ if (keys.length === 0) return [];
2749
+ const collected = [];
2750
+ let pending = keys;
2751
+ let attempt = 0;
2752
+ while (pending.length > 0) {
2753
+ if (attempt > 0) {
2754
+ await new Promise(
2755
+ (resolve) => setTimeout(resolve, BATCH_GET_BASE_BACKOFF_MS * 2 ** (attempt - 1))
2756
+ );
2757
+ }
2758
+ attempt++;
2759
+ const result = await entity.get(pending).go();
2760
+ collected.push(...result.data);
2761
+ const unprocessed = result.unprocessed ?? [];
2762
+ if (unprocessed.length === 0) break;
2763
+ if (attempt >= BATCH_GET_MAX_ATTEMPTS) {
2764
+ throw new Error(
2765
+ `BatchGet exhausted retries: ${unprocessed.length} key(s) still unprocessed after ${BATCH_GET_MAX_ATTEMPTS} attempt(s)`
2766
+ );
2767
+ }
2768
+ pending = unprocessed;
2769
+ }
2770
+ return collected;
2771
+ }
2772
+ async function dispatchListMode(mode, shardResults, hooks) {
2773
+ if (mode === "count") {
2774
+ let total = 0;
2775
+ for (const shardResult of shardResults) {
2776
+ total += (shardResult.data ?? []).length;
2777
+ }
2778
+ return { entries: [], total };
2779
+ }
2780
+ if (mode === "summary") {
2781
+ const entries2 = [];
2782
+ for (const shardResult of shardResults) {
2783
+ for (const item of shardResult.data ?? []) {
2784
+ if (typeof item.summary !== "string") continue;
2785
+ let parsed;
2786
+ try {
2787
+ parsed = JSON.parse(item.summary);
2788
+ } catch {
2789
+ continue;
2790
+ }
2791
+ entries2.push(hooks.buildSummaryEntry(item.id, parsed));
2792
+ }
2793
+ }
2794
+ return { entries: entries2, total: entries2.length };
2795
+ }
2796
+ const orderedIds = [];
2797
+ for (const shardResult of shardResults) {
2798
+ for (const item of shardResult.data ?? []) {
2799
+ orderedIds.push(item.id);
2800
+ }
2801
+ }
2802
+ if (orderedIds.length === 0) return { entries: [], total: 0 };
2803
+ const items = await hooks.hydrate(orderedIds);
2804
+ const byId = new Map(items.map((item) => [hooks.getId(item), item]));
2805
+ const entries = [];
2806
+ for (const id of orderedIds) {
2807
+ const item = byId.get(id);
2808
+ if (!item) continue;
2809
+ entries.push(hooks.buildEntry(id, item));
2810
+ }
2811
+ return { entries, total: entries.length };
2812
+ }
2813
+
2814
+ // src/data/operations/control/membership/membership-list-operation.ts
2815
+ var SK = "CURRENT";
2816
+ async function listMembershipsOperation(params) {
2817
+ const { context, tableName, mode = "full" } = params;
2818
+ const tenantId = context.tenantId;
2819
+ const service = getDynamoControlService(tableName);
2820
+ const shardResults = await Promise.all(
2821
+ Array.from(
2822
+ { length: SHARD_COUNT },
2823
+ (_, shard) => service.entities.membership.query.gsi1({ tenantId, gsi1Shard: String(shard) }).go()
2824
+ )
2825
+ );
2826
+ return dispatchListMode(mode, shardResults, {
2827
+ hydrate: (orderedIds) => batchGetWithRetry(
2828
+ service.entities.membership,
2829
+ orderedIds.map((id) => ({ tenantId, id, sk: SK }))
2830
+ ),
2831
+ getId: (item) => item.id,
2832
+ buildEntry: (id, item) => ({
2833
+ id,
2834
+ resource: {
2835
+ resourceType: "Membership",
2836
+ id,
2837
+ ...JSON.parse(item.resource)
2838
+ }
2839
+ }),
2840
+ buildSummaryEntry: (id, parsed) => ({
2841
+ id,
2842
+ resource: { resourceType: "Membership", id, ...parsed }
2843
+ })
2844
+ });
2845
+ }
2846
+
2847
+ // src/data/operations/control/membership/membership-user-projection.ts
2848
+ var import_types3 = require("@openhi/types");
2849
+ function extractReferenceSlug(resource, fieldName) {
2850
+ const field = resource[fieldName];
2851
+ if (!field || typeof field !== "object") {
2852
+ return void 0;
2853
+ }
2854
+ const reference = field.reference;
2855
+ if (typeof reference !== "string" || reference.length === 0) {
2856
+ return void 0;
2857
+ }
2858
+ const slash = reference.lastIndexOf("/");
2859
+ const tail = slash >= 0 ? reference.slice(slash + 1) : reference;
2860
+ return tail.length > 0 ? tail : void 0;
2861
+ }
2862
+
2863
+ // src/data/operations/control/roleassignment/roleassignment-list-by-workspace-operation.ts
2864
+ function buildSkPrefix2(roleId) {
2865
+ if (roleId === void 0 || roleId.length === 0) {
2866
+ return "ROLEASSIGNMENT#";
2867
+ }
2868
+ return `ROLEASSIGNMENT#${roleId}#`;
2869
+ }
2870
+ async function roleAssignmentListByWorkspaceOperation(params) {
2871
+ const {
2872
+ tenantId,
2873
+ workspaceId,
2874
+ roleId,
2875
+ cursor = null,
2876
+ limit,
2877
+ order,
2878
+ tableName
2879
+ } = params;
2880
+ const service = getDynamoControlService(tableName);
2881
+ const skPrefix = buildSkPrefix2(roleId);
2882
+ const goOptions = {
2883
+ cursor
2884
+ };
2885
+ if (limit !== void 0) {
2886
+ goOptions.limit = limit;
2887
+ }
2888
+ if (order !== void 0) {
2889
+ goOptions.order = order;
2890
+ }
2891
+ const result = await service.entities.roleAssignmentWorkspaceProjection.query.record({ tenantId, workspaceId }).begins({ sk: skPrefix }).go(goOptions);
2892
+ const items = (result.data ?? []).map((row) => ({
2893
+ tenantId: row.tenantId,
2894
+ workspaceId: row.workspaceId,
2895
+ sk: row.sk,
2896
+ userId: row.userId,
2897
+ roleId: row.roleId,
2898
+ roleAssignmentId: row.roleAssignmentId,
2899
+ summary: row.summary,
2900
+ vid: row.vid,
2901
+ lastUpdated: row.lastUpdated,
2902
+ denormalizedUserName: row.denormalizedUserName,
2903
+ denormalizedRoleName: row.denormalizedRoleName
2904
+ }));
2905
+ return { items, cursor: result.cursor ?? null };
2906
+ }
2907
+
2908
+ // src/data/operations/control/workspace/workspace-list-operation.ts
2909
+ var SK2 = "CURRENT";
2910
+ function counterValue(value) {
2911
+ return typeof value === "number" && Number.isFinite(value) ? value : 0;
2912
+ }
2913
+ async function listWorkspacesOperation(params) {
2914
+ const { context, tableName, mode = "full" } = params;
2915
+ const { tenantId } = context;
2916
+ const service = getDynamoControlService(tableName);
2917
+ const shardResults = await Promise.all(
2918
+ Array.from(
2919
+ { length: SHARD_COUNT },
2920
+ (_, shard) => service.entities.workspace.query.gsi1({ tenantId, gsi1Shard: String(shard) }).go()
2921
+ )
2922
+ );
2923
+ return dispatchListMode(mode, shardResults, {
2924
+ hydrate: (orderedIds) => batchGetWithRetry(
2925
+ service.entities.workspace,
2926
+ orderedIds.map((id) => ({ tenantId, id, sk: SK2 }))
2927
+ ),
2928
+ getId: (item) => item.id,
2929
+ // FULL mode (admin list default): read the ADR-028 counters off the
2930
+ // canonical record hydrated by BatchGet and expose them as
2931
+ // `resource.counts`. Missing counters render as 0.
2932
+ buildEntry: (id, item) => ({
2933
+ id,
2934
+ resource: {
2935
+ resourceType: "Workspace",
2936
+ id,
2937
+ ...JSON.parse(item.resource),
2938
+ counts: {
2939
+ usersInWorkspace: counterValue(item.usersInWorkspace),
2940
+ adminUsersInWorkspace: counterValue(item.adminUsersInWorkspace),
2941
+ normalUsersInWorkspace: counterValue(item.normalUsersInWorkspace)
2942
+ }
2943
+ }
2944
+ }),
2945
+ // SUMMARY mode reads only the GSI1 `summary` projection (no
2946
+ // counters); surface zeros so the shape stays uniform.
2947
+ buildSummaryEntry: (id, parsed) => ({
2948
+ id,
2949
+ resource: {
2950
+ resourceType: "Workspace",
2951
+ id,
2952
+ ...parsed,
2953
+ counts: {
2954
+ usersInWorkspace: 0,
2955
+ adminUsersInWorkspace: 0,
2956
+ normalUsersInWorkspace: 0
2957
+ }
2958
+ }
2959
+ })
2960
+ });
2961
+ }
2962
+
2963
+ // src/data/operations/control/counters/counter-reconcile-operation.ts
2964
+ function counterValue2(value) {
2965
+ return typeof value === "number" && Number.isFinite(value) ? value : 0;
2966
+ }
2967
+ function reconcileContext(tenantId) {
2968
+ return {
2969
+ tenantId,
2970
+ workspaceId: "",
2971
+ date: (/* @__PURE__ */ new Date()).toISOString(),
2972
+ actorId: "counter-reconciliation",
2973
+ actorName: "Counter Reconciliation Job",
2974
+ actorType: "internal-system",
2975
+ source: "step-function"
2976
+ };
2977
+ }
2978
+ async function reconcileTenantCountersOperation(params) {
2979
+ const { tenantId, tableName } = params;
2980
+ const service = getDynamoControlService(tableName);
2981
+ const context = reconcileContext(tenantId);
2982
+ const workspacesResult = await listWorkspacesOperation({
2983
+ context,
2984
+ tableName,
2985
+ mode: "count"
2986
+ });
2987
+ const workspacesInTenant = workspacesResult.total;
2988
+ const memberships = await listMembershipsOperation({
2989
+ context,
2990
+ tableName,
2991
+ mode: "full"
2992
+ });
2993
+ let usersInTenant = 0;
2994
+ for (const entry of memberships.entries) {
2995
+ const workspaceSlug = extractReferenceSlug(entry.resource, "workspace");
2996
+ if (workspaceSlug === void 0) {
2997
+ usersInTenant += 1;
2998
+ }
2999
+ }
3000
+ const current = await service.entities.tenant.get({ tenantId, sk: "CURRENT" }).go();
3001
+ const drift = [];
3002
+ const recomputed = {
3003
+ usersInTenant,
3004
+ workspacesInTenant
3005
+ };
3006
+ for (const counter of Object.keys(recomputed)) {
3007
+ const oldValue = counterValue2(current.data?.[counter]);
3008
+ const newValue = recomputed[counter];
3009
+ if (oldValue !== newValue) {
3010
+ drift.push({
3011
+ target: COUNTER_TARGET.Tenant,
3012
+ id: tenantId,
3013
+ counter,
3014
+ old: oldValue,
3015
+ new: newValue
3016
+ });
3017
+ }
3018
+ }
3019
+ if (drift.length > 0) {
3020
+ await service.entities.tenant.patch({ tenantId, sk: "CURRENT" }).set(recomputed).go();
3021
+ }
3022
+ return { drift };
3023
+ }
3024
+ async function reconcileWorkspaceCountersOperation(params) {
3025
+ const { tenantId, workspaceId, tableName } = params;
3026
+ const service = getDynamoControlService(tableName);
3027
+ let usersInWorkspace = 0;
3028
+ let membershipCursor = null;
3029
+ do {
3030
+ const page = await membershipListByWorkspaceOperation({
3031
+ tenantId,
3032
+ workspaceId,
3033
+ cursor: membershipCursor,
3034
+ tableName
3035
+ });
3036
+ usersInWorkspace += page.items.length;
3037
+ membershipCursor = page.cursor;
3038
+ } while (membershipCursor !== null);
3039
+ let adminUsersInWorkspace = 0;
3040
+ let normalUsersInWorkspace = 0;
3041
+ let roleAssignmentCursor = null;
3042
+ do {
3043
+ const page = await roleAssignmentListByWorkspaceOperation({
3044
+ tenantId,
3045
+ workspaceId,
3046
+ cursor: roleAssignmentCursor,
3047
+ tableName
3048
+ });
3049
+ for (const item of page.items) {
3050
+ const roleLevel = await readRoleLevel(
3051
+ service,
3052
+ tenantId,
3053
+ item.roleAssignmentId
3054
+ );
3055
+ if (isAdminRoleAssignment({ roleLevel, roleId: item.roleId })) {
3056
+ adminUsersInWorkspace += 1;
3057
+ } else {
3058
+ normalUsersInWorkspace += 1;
3059
+ }
3060
+ }
3061
+ roleAssignmentCursor = page.cursor;
3062
+ } while (roleAssignmentCursor !== null);
3063
+ const current = await service.entities.workspace.get({ tenantId, id: workspaceId, sk: "CURRENT" }).go();
3064
+ const drift = [];
3065
+ const recomputed = {
3066
+ usersInWorkspace,
3067
+ adminUsersInWorkspace,
3068
+ normalUsersInWorkspace
3069
+ };
3070
+ for (const counter of Object.keys(recomputed)) {
3071
+ const oldValue = counterValue2(current.data?.[counter]);
3072
+ const newValue = recomputed[counter];
3073
+ if (oldValue !== newValue) {
3074
+ drift.push({
3075
+ target: COUNTER_TARGET.Workspace,
3076
+ id: workspaceId,
3077
+ tenantId,
3078
+ counter,
3079
+ old: oldValue,
3080
+ new: newValue
3081
+ });
3082
+ }
3083
+ }
3084
+ if (drift.length > 0) {
3085
+ await service.entities.workspace.patch({ tenantId, id: workspaceId, sk: "CURRENT" }).set(recomputed).go();
3086
+ }
3087
+ return { drift };
3088
+ }
3089
+ async function reconcileUserCountersOperation(params) {
3090
+ const { userId, tableName } = params;
3091
+ const service = getDynamoControlService(tableName);
3092
+ const tenantsForUser = await countMembershipsByUserOperation({
3093
+ userId,
3094
+ mode: "tenant",
3095
+ tableName
3096
+ });
3097
+ const workspacesForUser = await countMembershipsByUserOperation({
3098
+ userId,
3099
+ mode: "workspace",
3100
+ tableName
3101
+ });
3102
+ const current = await service.entities.user.get({ id: userId, sk: "CURRENT" }).go();
3103
+ const drift = [];
3104
+ const recomputed = {
3105
+ tenantsForUser,
3106
+ workspacesForUser
3107
+ };
3108
+ for (const counter of Object.keys(recomputed)) {
3109
+ const oldValue = counterValue2(current.data?.[counter]);
3110
+ const newValue = recomputed[counter];
3111
+ if (oldValue !== newValue) {
3112
+ drift.push({
3113
+ target: COUNTER_TARGET.User,
3114
+ id: userId,
3115
+ counter,
3116
+ old: oldValue,
3117
+ new: newValue
3118
+ });
3119
+ }
3120
+ }
3121
+ if (drift.length > 0) {
3122
+ await service.entities.user.patch({ id: userId, sk: "CURRENT" }).set(recomputed).go();
3123
+ }
3124
+ return { drift };
3125
+ }
3126
+ async function readRoleLevel(service, tenantId, roleAssignmentId) {
3127
+ const response = await service.entities.roleAssignment.get({ tenantId, id: roleAssignmentId, sk: "CURRENT" }).go();
3128
+ if (!response.data) {
3129
+ return void 0;
3130
+ }
3131
+ const resource = JSON.parse(response.data.resource);
3132
+ return extractRoleLevel(resource);
3133
+ }
3134
+
3135
+ // src/data/operations/control/tenant/tenant-list-operation.ts
3136
+ var SK3 = "CURRENT";
3137
+ function counterValue3(value) {
3138
+ return typeof value === "number" && Number.isFinite(value) ? value : 0;
3139
+ }
3140
+ async function listTenantsOperation(params) {
3141
+ const { tableName, mode = "full" } = params;
3142
+ const service = getDynamoControlService(tableName);
3143
+ const shardResults = await Promise.all(
3144
+ Array.from(
3145
+ { length: SHARD_COUNT },
3146
+ (_, shard) => service.entities.tenant.query.gsi1({ gsi1Shard: String(shard) }).go()
3147
+ )
3148
+ );
3149
+ return dispatchListMode(mode, shardResults, {
3150
+ hydrate: (orderedIds) => batchGetWithRetry(
3151
+ service.entities.tenant,
3152
+ orderedIds.map((id) => ({ tenantId: id, sk: SK3 }))
3153
+ ),
3154
+ getId: (item) => item.id,
3155
+ // FULL mode (admin list default): read the ADR-028 counters off the
3156
+ // canonical record hydrated by BatchGet and expose them as
3157
+ // `resource.counts`. Missing counters render as 0.
3158
+ buildEntry: (id, item) => ({
3159
+ id,
3160
+ resource: {
3161
+ resourceType: "Tenant",
3162
+ id,
3163
+ ...JSON.parse(item.resource),
3164
+ counts: {
3165
+ usersInTenant: counterValue3(item.usersInTenant),
3166
+ workspacesInTenant: counterValue3(item.workspacesInTenant)
3167
+ }
3168
+ }
3169
+ }),
3170
+ // SUMMARY mode reads only the GSI1 `summary` projection, which does
3171
+ // not carry the counters; surface zeros so the shape stays uniform.
3172
+ buildSummaryEntry: (id, parsed) => ({
3173
+ id,
3174
+ resource: {
3175
+ resourceType: "Tenant",
3176
+ id,
3177
+ ...parsed,
3178
+ counts: { usersInTenant: 0, workspacesInTenant: 0 }
3179
+ }
3180
+ })
3181
+ });
3182
+ }
3183
+
3184
+ // src/data/operations/control/user/user-list-operation.ts
3185
+ var SK4 = "CURRENT";
3186
+ function counterValue4(value) {
3187
+ return typeof value === "number" && Number.isFinite(value) ? value : 0;
3188
+ }
3189
+ async function listUsersOperation(params) {
3190
+ const { tableName, mode = "full" } = params;
3191
+ const service = getDynamoControlService(tableName);
3192
+ const shardResults = await Promise.all(
3193
+ Array.from(
3194
+ { length: SHARD_COUNT },
3195
+ (_, shard) => service.entities.user.query.gsi1({ gsi1Shard: String(shard) }).go()
3196
+ )
3197
+ );
3198
+ return dispatchListMode(mode, shardResults, {
3199
+ hydrate: (orderedIds) => batchGetWithRetry(
3200
+ service.entities.user,
3201
+ orderedIds.map((id) => ({ id, sk: SK4 }))
3202
+ ),
3203
+ getId: (item) => item.id,
3204
+ // FULL mode (admin list default): read the ADR-028 counters off the
3205
+ // canonical record hydrated by BatchGet and expose them as
3206
+ // `resource.counts`. Missing counters render as 0.
3207
+ buildEntry: (id, item) => ({
3208
+ id,
3209
+ resource: {
3210
+ resourceType: "User",
3211
+ id,
3212
+ ...JSON.parse(item.resource),
3213
+ counts: {
3214
+ tenantsForUser: counterValue4(item.tenantsForUser),
3215
+ workspacesForUser: counterValue4(item.workspacesForUser)
3216
+ }
3217
+ }
3218
+ }),
3219
+ // SUMMARY mode reads only the GSI1 `summary` projection (no
3220
+ // counters); surface zeros so the shape stays uniform.
3221
+ buildSummaryEntry: (id, parsed) => ({
3222
+ id,
3223
+ resource: {
3224
+ resourceType: "User",
3225
+ id,
3226
+ ...parsed,
3227
+ counts: { tenantsForUser: 0, workspacesForUser: 0 }
3228
+ }
3229
+ })
3230
+ });
3231
+ }
3232
+
3233
+ // src/data/operations/control/counters/counter-reconcile-driver.ts
3234
+ function driverContext(tenantId) {
3235
+ return {
3236
+ tenantId,
3237
+ workspaceId: "",
3238
+ date: (/* @__PURE__ */ new Date()).toISOString(),
3239
+ actorId: "counter-reconciliation",
3240
+ actorName: "Counter Reconciliation Job",
3241
+ actorType: "internal-system",
3242
+ source: "step-function"
3243
+ };
3244
+ }
3245
+ async function reconcileAllCountersOperation(params = {}) {
3246
+ const { tableName } = params;
3247
+ const drift = [];
3248
+ let tenantsScanned = 0;
3249
+ let workspacesScanned = 0;
3250
+ let usersScanned = 0;
3251
+ const tenants = await listTenantsOperation({
3252
+ context: driverContext(""),
3253
+ tableName,
3254
+ mode: "summary"
3255
+ });
3256
+ for (const tenant of tenants.entries) {
3257
+ tenantsScanned += 1;
3258
+ const tenantResult = await reconcileTenantCountersOperation({
3259
+ tenantId: tenant.id,
3260
+ tableName
3261
+ });
3262
+ drift.push(...tenantResult.drift);
3263
+ const workspaces = await listWorkspacesOperation({
3264
+ context: driverContext(tenant.id),
3265
+ tableName,
3266
+ mode: "summary"
3267
+ });
3268
+ for (const workspace of workspaces.entries) {
3269
+ workspacesScanned += 1;
3270
+ const workspaceResult = await reconcileWorkspaceCountersOperation({
3271
+ tenantId: tenant.id,
3272
+ workspaceId: workspace.id,
3273
+ tableName
3274
+ });
3275
+ drift.push(...workspaceResult.drift);
3276
+ }
3277
+ }
3278
+ const users = await listUsersOperation({
3279
+ context: driverContext(""),
3280
+ tableName,
3281
+ mode: "summary"
3282
+ });
3283
+ for (const user of users.entries) {
3284
+ usersScanned += 1;
3285
+ const userResult = await reconcileUserCountersOperation({
3286
+ userId: user.id,
3287
+ tableName
3288
+ });
3289
+ drift.push(...userResult.drift);
3290
+ }
3291
+ return {
3292
+ drift,
3293
+ scanned: {
3294
+ tenants: tenantsScanned,
3295
+ workspaces: workspacesScanned,
3296
+ users: usersScanned
3297
+ },
3298
+ countersCorrected: drift.length
3299
+ };
3300
+ }
3301
+
3302
+ // src/workflows/control-plane/counter-reconciliation/counter-reconciliation.handler.ts
3303
+ var runCounterReconciliation = async (deps) => {
3304
+ const report = await deps.reconcileAll();
3305
+ console.log(
3306
+ JSON.stringify({
3307
+ message: "counter-reconciliation complete",
3308
+ scanned: report.scanned,
3309
+ countersCorrected: report.countersCorrected,
3310
+ drift: report.drift
3311
+ })
3312
+ );
3313
+ return report;
3314
+ };
3315
+ var productionDependencies = () => ({
3316
+ reconcileAll: () => reconcileAllCountersOperation()
3317
+ });
3318
+ var handler = async () => runCounterReconciliation(productionDependencies());
3319
+ // Annotate the CommonJS export names for ESM import in node:
3320
+ 0 && (module.exports = {
3321
+ handler,
3322
+ runCounterReconciliation
3323
+ });
3324
+ //# sourceMappingURL=counter-reconciliation.handler.js.map