@openhi/constructs 0.0.159 → 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 (129) 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-7TRO2STL.mjs +4616 -0
  7. package/lib/chunk-7TRO2STL.mjs.map +1 -0
  8. package/lib/chunk-BUAYVN3C.mjs +87 -0
  9. package/lib/chunk-BUAYVN3C.mjs.map +1 -0
  10. package/lib/{chunk-23PUSHBV.mjs → chunk-D2Y6DDOC.mjs} +2 -2
  11. package/lib/chunk-DWSWCUZR.mjs +123 -0
  12. package/lib/chunk-DWSWCUZR.mjs.map +1 -0
  13. package/lib/{chunk-VZCPGQXA.mjs → chunk-EUIP2U5F.mjs} +69 -1
  14. package/lib/{chunk-VZCPGQXA.mjs.map → chunk-EUIP2U5F.mjs.map} +1 -1
  15. package/lib/chunk-GJTPXJKD.mjs +46 -0
  16. package/lib/chunk-GJTPXJKD.mjs.map +1 -0
  17. package/lib/chunk-I6LUPJUY.mjs +61 -0
  18. package/lib/chunk-I6LUPJUY.mjs.map +1 -0
  19. package/lib/{chunk-KR2Y2CVQ.mjs → chunk-KA3OMP3X.mjs} +2 -2
  20. package/lib/{chunk-ZM4GDHHC.mjs → chunk-KMEWULMX.mjs} +51 -3
  21. package/lib/chunk-KMEWULMX.mjs.map +1 -0
  22. package/lib/chunk-LKKLO66E.mjs +25 -0
  23. package/lib/chunk-LKKLO66E.mjs.map +1 -0
  24. package/lib/{chunk-CFJDATDK.mjs → chunk-MLFMW5IF.mjs} +43 -9
  25. package/lib/chunk-MLFMW5IF.mjs.map +1 -0
  26. package/lib/chunk-O5VQWB6U.mjs +315 -0
  27. package/lib/chunk-O5VQWB6U.mjs.map +1 -0
  28. package/lib/{chunk-7BQHLC7U.mjs → chunk-P3CTZWC2.mjs} +8 -40
  29. package/lib/chunk-P3CTZWC2.mjs.map +1 -0
  30. package/lib/chunk-P3NFCKTZ.mjs +502 -0
  31. package/lib/chunk-P3NFCKTZ.mjs.map +1 -0
  32. package/lib/{chunk-M7Y3BOQW.mjs → chunk-Q3MKITPY.mjs} +5 -5
  33. package/lib/chunk-Q64MOYJ7.mjs +218 -0
  34. package/lib/chunk-Q64MOYJ7.mjs.map +1 -0
  35. package/lib/chunk-RQKJNMX5.mjs +89 -0
  36. package/lib/chunk-RQKJNMX5.mjs.map +1 -0
  37. package/lib/{chunk-ZWSGM6PZ.mjs → chunk-SD7J3N3C.mjs} +2 -2
  38. package/lib/{chunk-7RZHFI77.mjs → chunk-VESULYQQ.mjs} +2 -2
  39. package/lib/{chunk-AOSEKL7U.mjs → chunk-WOTU36P3.mjs} +6 -103
  40. package/lib/chunk-WOTU36P3.mjs.map +1 -0
  41. package/lib/{chunk-X5E4YJGZ.mjs → chunk-YPTJJ35S.mjs} +2 -2
  42. package/lib/counter-apply-operation-DZM3MIDm.d.mts +63 -0
  43. package/lib/counter-apply-operation-DZM3MIDm.d.ts +63 -0
  44. package/lib/counter-maintenance.handler.d.mts +38 -0
  45. package/lib/counter-maintenance.handler.d.ts +38 -0
  46. package/lib/counter-maintenance.handler.js +2885 -0
  47. package/lib/counter-maintenance.handler.js.map +1 -0
  48. package/lib/counter-maintenance.handler.mjs +180 -0
  49. package/lib/counter-maintenance.handler.mjs.map +1 -0
  50. package/lib/counter-reconciliation.handler.d.mts +116 -0
  51. package/lib/counter-reconciliation.handler.d.ts +116 -0
  52. package/lib/counter-reconciliation.handler.js +3324 -0
  53. package/lib/counter-reconciliation.handler.js.map +1 -0
  54. package/lib/counter-reconciliation.handler.mjs +295 -0
  55. package/lib/counter-reconciliation.handler.mjs.map +1 -0
  56. package/lib/data-store-postgres-replication.handler.js +50 -2
  57. package/lib/data-store-postgres-replication.handler.js.map +1 -1
  58. package/lib/data-store-postgres-replication.handler.mjs +2 -2
  59. package/lib/delete-chunk.handler.js +118 -2
  60. package/lib/delete-chunk.handler.js.map +1 -1
  61. package/lib/delete-chunk.handler.mjs +3 -3
  62. package/lib/{events-DTgo2dcW.d.mts → events-TG654e7L.d.mts} +68 -19
  63. package/lib/{events-DTgo2dcW.d.ts → events-TG654e7L.d.ts} +68 -19
  64. package/lib/finalize.handler.js +50 -2
  65. package/lib/finalize.handler.js.map +1 -1
  66. package/lib/finalize.handler.mjs +4 -4
  67. package/lib/firehose-archive-transform.handler.js +50 -2
  68. package/lib/firehose-archive-transform.handler.js.map +1 -1
  69. package/lib/firehose-archive-transform.handler.mjs +2 -2
  70. package/lib/index.d.mts +1283 -4
  71. package/lib/index.d.ts +1389 -24
  72. package/lib/index.js +4113 -320
  73. package/lib/index.js.map +1 -1
  74. package/lib/index.mjs +602 -195
  75. package/lib/index.mjs.map +1 -1
  76. package/lib/list-chunks.handler.js +118 -2
  77. package/lib/list-chunks.handler.js.map +1 -1
  78. package/lib/list-chunks.handler.mjs +3 -3
  79. package/lib/platform-deploy-bridge.handler.js +50 -2
  80. package/lib/platform-deploy-bridge.handler.js.map +1 -1
  81. package/lib/platform-deploy-bridge.handler.mjs +1 -1
  82. package/lib/pre-token-generation.handler.js +68 -0
  83. package/lib/pre-token-generation.handler.js.map +1 -1
  84. package/lib/pre-token-generation.handler.mjs +9 -5
  85. package/lib/pre-token-generation.handler.mjs.map +1 -1
  86. package/lib/provision-default-workspace.handler.js +887 -4
  87. package/lib/provision-default-workspace.handler.js.map +1 -1
  88. package/lib/provision-default-workspace.handler.mjs +14 -9
  89. package/lib/provision-default-workspace.handler.mjs.map +1 -1
  90. package/lib/rename-finalize.handler.js +50 -2
  91. package/lib/rename-finalize.handler.js.map +1 -1
  92. package/lib/rename-finalize.handler.mjs +2 -2
  93. package/lib/rename-list-targets.handler.js +118 -2
  94. package/lib/rename-list-targets.handler.js.map +1 -1
  95. package/lib/rename-list-targets.handler.mjs +11 -9
  96. package/lib/rename-list-targets.handler.mjs.map +1 -1
  97. package/lib/rename-rewrite-chunk.handler.js +68 -0
  98. package/lib/rename-rewrite-chunk.handler.js.map +1 -1
  99. package/lib/rename-rewrite-chunk.handler.mjs +2 -2
  100. package/lib/rest-api-lambda.handler.js +1454 -251
  101. package/lib/rest-api-lambda.handler.js.map +1 -1
  102. package/lib/rest-api-lambda.handler.mjs +673 -821
  103. package/lib/rest-api-lambda.handler.mjs.map +1 -1
  104. package/lib/seed-demo-data.handler.d.mts +1 -1
  105. package/lib/seed-demo-data.handler.d.ts +1 -1
  106. package/lib/seed-demo-data.handler.js +4004 -201
  107. package/lib/seed-demo-data.handler.js.map +1 -1
  108. package/lib/seed-demo-data.handler.mjs +10 -7
  109. package/lib/seed-system-data.handler.js +118 -2
  110. package/lib/seed-system-data.handler.js.map +1 -1
  111. package/lib/seed-system-data.handler.mjs +5 -5
  112. package/package.json +1 -1
  113. package/lib/chunk-7BQHLC7U.mjs.map +0 -1
  114. package/lib/chunk-AOSEKL7U.mjs.map +0 -1
  115. package/lib/chunk-BQMJSDOD.mjs +0 -1136
  116. package/lib/chunk-BQMJSDOD.mjs.map +0 -1
  117. package/lib/chunk-CFJDATDK.mjs.map +0 -1
  118. package/lib/chunk-E6MCKJVS.mjs +0 -212
  119. package/lib/chunk-E6MCKJVS.mjs.map +0 -1
  120. package/lib/chunk-HQ67J7BP.mjs.map +0 -1
  121. package/lib/chunk-MVQWAIMC.mjs.map +0 -1
  122. package/lib/chunk-ZM4GDHHC.mjs.map +0 -1
  123. /package/lib/{chunk-WPCBVDFZ.mjs.map → chunk-76UM2LQ5.mjs.map} +0 -0
  124. /package/lib/{chunk-23PUSHBV.mjs.map → chunk-D2Y6DDOC.mjs.map} +0 -0
  125. /package/lib/{chunk-KR2Y2CVQ.mjs.map → chunk-KA3OMP3X.mjs.map} +0 -0
  126. /package/lib/{chunk-M7Y3BOQW.mjs.map → chunk-Q3MKITPY.mjs.map} +0 -0
  127. /package/lib/{chunk-ZWSGM6PZ.mjs.map → chunk-SD7J3N3C.mjs.map} +0 -0
  128. /package/lib/{chunk-7RZHFI77.mjs.map → chunk-VESULYQQ.mjs.map} +0 -0
  129. /package/lib/{chunk-X5E4YJGZ.mjs.map → chunk-YPTJJ35S.mjs.map} +0 -0
@@ -5,6 +5,9 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
6
  var __getProtoOf = Object.getPrototypeOf;
7
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
+ };
8
11
  var __export = (target, all) => {
9
12
  for (var name in all)
10
13
  __defProp(target, name, { get: all[name], enumerable: true });
@@ -27,6 +30,727 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
27
30
  ));
28
31
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
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
+
30
754
  // src/data/lambda/rest-api-lambda.handler.ts
31
755
  var rest_api_lambda_handler_exports = {};
32
756
  __export(rest_api_lambda_handler_exports, {
@@ -1675,6 +2399,24 @@ var TenantEntity = new import_electrodb11.Entity({
1675
2399
  type: "string",
1676
2400
  required: true
1677
2401
  },
2402
+ /**
2403
+ * ADR-028 denormalized counter — number of tenant-scoped Memberships
2404
+ * (users) in this tenant. Maintained by the counter-maintenance
2405
+ * consumer via atomic ADD; absent/0 until first event or reconciliation.
2406
+ */
2407
+ usersInTenant: {
2408
+ type: "number",
2409
+ required: false
2410
+ },
2411
+ /**
2412
+ * ADR-028 denormalized counter — number of Workspaces in this tenant.
2413
+ * Maintained by the counter-maintenance consumer via atomic ADD;
2414
+ * absent/0 until first event or reconciliation.
2415
+ */
2416
+ workspacesInTenant: {
2417
+ type: "number",
2418
+ required: false
2419
+ },
1678
2420
  gsi1Shard: gsi1ShardAttribute,
1679
2421
  /** Derived GSI1 sort key — name-based when extractable; else `<lastUpdated>#<id>`. */
1680
2422
  gsi1sk: gsi1skAttribute,
@@ -1779,6 +2521,26 @@ var UserEntity = new import_electrodb12.Entity({
1779
2521
  type: "string",
1780
2522
  required: true
1781
2523
  },
2524
+ /**
2525
+ * ADR-028 denormalized counter — number of tenant-scoped Memberships
2526
+ * (tenants) this user belongs to. Maintained by the
2527
+ * counter-maintenance consumer via atomic ADD; absent/0 until first
2528
+ * event or reconciliation.
2529
+ */
2530
+ tenantsForUser: {
2531
+ type: "number",
2532
+ required: false
2533
+ },
2534
+ /**
2535
+ * ADR-028 denormalized counter — number of workspace-scoped
2536
+ * Memberships (workspaces) this user belongs to. Maintained by the
2537
+ * counter-maintenance consumer via atomic ADD; absent/0 until first
2538
+ * event or reconciliation.
2539
+ */
2540
+ workspacesForUser: {
2541
+ type: "number",
2542
+ required: false
2543
+ },
1782
2544
  gsi1Shard: gsi1ShardAttribute,
1783
2545
  /** Derived GSI1 sort key — name-based when extractable; else `<lastUpdated>#<id>`. */
1784
2546
  gsi1sk: gsi1skAttribute,
@@ -1923,6 +2685,36 @@ var WorkspaceEntity = new import_electrodb13.Entity({
1923
2685
  type: "string",
1924
2686
  required: true
1925
2687
  },
2688
+ /**
2689
+ * ADR-028 denormalized counter — number of workspace-scoped
2690
+ * Memberships (users) in this workspace. Maintained by the
2691
+ * counter-maintenance consumer via atomic ADD; absent/0 until first
2692
+ * event or reconciliation.
2693
+ */
2694
+ usersInWorkspace: {
2695
+ type: "number",
2696
+ required: false
2697
+ },
2698
+ /**
2699
+ * ADR-028 denormalized counter — number of workspace-scoped
2700
+ * RoleAssignments classified as admin-tier in this workspace.
2701
+ * Maintained by the counter-maintenance consumer via atomic ADD;
2702
+ * absent/0 until first event or reconciliation.
2703
+ */
2704
+ adminUsersInWorkspace: {
2705
+ type: "number",
2706
+ required: false
2707
+ },
2708
+ /**
2709
+ * ADR-028 denormalized counter — number of workspace-scoped
2710
+ * RoleAssignments classified as non-admin in this workspace.
2711
+ * Maintained by the counter-maintenance consumer via atomic ADD;
2712
+ * absent/0 until first event or reconciliation.
2713
+ */
2714
+ normalUsersInWorkspace: {
2715
+ type: "number",
2716
+ required: false
2717
+ },
1926
2718
  gsi1Shard: gsi1ShardAttribute,
1927
2719
  /** Derived GSI1 sort key — name-based when extractable; else `<lastUpdated>#<id>`. */
1928
2720
  gsi1sk: gsi1skAttribute,
@@ -3240,7 +4032,7 @@ function sendInvalidSummary400(res, diagnostics) {
3240
4032
  }
3241
4033
  async function handleListRoute(opts) {
3242
4034
  const { req, res, basePath, listOperation, errorLogContext } = opts;
3243
- const ctx = req.openhiContext;
4035
+ const ctx = opts.context ?? req.openhiContext;
3244
4036
  const parsed = parseSearchSubsetting(req, res);
3245
4037
  if ("errorResponse" in parsed) return parsed.errorResponse;
3246
4038
  try {
@@ -4933,6 +5725,76 @@ function buildMembershipWorkspaceProjectionItem(input) {
4933
5725
  };
4934
5726
  }
4935
5727
 
5728
+ // src/data/operations/control/control-event-publisher.ts
5729
+ var import_client_eventbridge = require("@aws-sdk/client-eventbridge");
5730
+ var import_workflows = __toESM(require_lib());
5731
+ var CONTROL_EVENT_BUS_NAME_ENV_VAR = "CONTROL_EVENT_BUS_NAME";
5732
+ var cachedClient;
5733
+ function getClient() {
5734
+ if (!cachedClient) {
5735
+ cachedClient = new import_client_eventbridge.EventBridgeClient({
5736
+ region: process.env.AWS_REGION ?? "us-east-1"
5737
+ });
5738
+ }
5739
+ return cachedClient;
5740
+ }
5741
+ function actorFromContext(context) {
5742
+ return {
5743
+ ohi_tid: context.tenantId,
5744
+ ohi_wid: context.workspaceId,
5745
+ ohi_uid: context.actorId,
5746
+ ohi_uname: context.actorName
5747
+ };
5748
+ }
5749
+ async function publishControlEvent(entry, payload, context) {
5750
+ const busName = process.env[CONTROL_EVENT_BUS_NAME_ENV_VAR];
5751
+ if (!busName) {
5752
+ return;
5753
+ }
5754
+ try {
5755
+ await (0, import_workflows.publishWorkflowEvent)(
5756
+ getClient(),
5757
+ entry,
5758
+ payload,
5759
+ { actor: actorFromContext(context) },
5760
+ { busNameByPlane: { [import_workflows.OPENHI_CONTROL_SOURCE]: busName } }
5761
+ );
5762
+ } catch (err) {
5763
+ console.error(`control-event publish failed for ${entry.detailType}:`, err);
5764
+ }
5765
+ }
5766
+ async function publishMembershipCreated(context, detail) {
5767
+ await publishControlEvent(import_workflows.ControlPlaneMembershipCreatedV1, detail, context);
5768
+ }
5769
+ async function publishMembershipDeleted(context, detail) {
5770
+ await publishControlEvent(import_workflows.ControlPlaneMembershipDeletedV1, detail, context);
5771
+ }
5772
+ async function publishRoleAssignmentCreated(context, detail) {
5773
+ await publishControlEvent(
5774
+ import_workflows.ControlPlaneRoleAssignmentCreatedV1,
5775
+ detail,
5776
+ context
5777
+ );
5778
+ }
5779
+ async function publishRoleAssignmentDeleted(context, detail) {
5780
+ await publishControlEvent(
5781
+ import_workflows.ControlPlaneRoleAssignmentDeletedV1,
5782
+ detail,
5783
+ context
5784
+ );
5785
+ }
5786
+ async function publishWorkspaceCreated(context, detail) {
5787
+ await publishControlEvent(import_workflows.ControlPlaneWorkspaceCreatedV1, detail, context);
5788
+ }
5789
+ async function publishWorkspaceDeleted(context, detail) {
5790
+ await publishControlEvent(import_workflows.ControlPlaneWorkspaceDeletedV1, detail, context);
5791
+ }
5792
+ function extractRoleLevel(resource) {
5793
+ const code = resource?.code;
5794
+ const first = code?.coding?.[0]?.code;
5795
+ return typeof first === "string" && first.length > 0 ? first : void 0;
5796
+ }
5797
+
4936
5798
  // src/data/operations/control/denormalized-display-names.ts
4937
5799
  function extractDenormalizedReferenceDisplay(resource, fieldName) {
4938
5800
  const field = resource[fieldName];
@@ -5060,6 +5922,14 @@ async function createMembershipOperation(params) {
5060
5922
  });
5061
5923
  }
5062
5924
  await executeMultiWrite({ service, triples });
5925
+ await publishMembershipCreated(context, {
5926
+ membershipId: id,
5927
+ tenantId: context.tenantId,
5928
+ ...userIdFromResource !== void 0 && { userId: userIdFromResource },
5929
+ ...workspaceIdFromResource !== void 0 && {
5930
+ workspaceId: workspaceIdFromResource
5931
+ }
5932
+ });
5063
5933
  return {
5064
5934
  id,
5065
5935
  resource,
@@ -5186,6 +6056,14 @@ async function deleteMembershipOperation(params) {
5186
6056
  });
5187
6057
  }
5188
6058
  await executeMultiWrite({ service, triples });
6059
+ await publishMembershipDeleted(context, {
6060
+ membershipId: id,
6061
+ tenantId: context.tenantId,
6062
+ ...userIdFromResource !== void 0 && { userId: userIdFromResource },
6063
+ ...workspaceIdFromResource !== void 0 && {
6064
+ workspaceId: workspaceIdFromResource
6065
+ }
6066
+ });
5189
6067
  }
5190
6068
 
5191
6069
  // src/data/rest-api/routes/control/membership/membership-delete-route.ts
@@ -5270,69 +6148,209 @@ async function listMembershipsOperation(params) {
5270
6148
  });
5271
6149
  }
5272
6150
 
5273
- // src/data/rest-api/routes/control/membership/membership-list-route.ts
5274
- async function listMembershipsRoute(req, res) {
5275
- return handleListRoute({
5276
- req,
5277
- res,
5278
- basePath: BASE_PATH.MEMBERSHIP,
5279
- listOperation: listMembershipsOperation,
5280
- errorLogContext: "GET /Membership list error:"
6151
+ // src/data/rest-api/routes/control/cross-cutting-route-helpers.ts
6152
+ function sendInvalidQuery400(res, diagnostics) {
6153
+ return res.status(400).json({
6154
+ resourceType: "OperationOutcome",
6155
+ issue: [{ severity: "error", code: "invalid", diagnostics }]
5281
6156
  });
5282
6157
  }
5283
-
5284
- // src/data/operations/control/membership/membership-update-operation.ts
5285
- var import_types9 = require("@openhi/types");
5286
- async function updateMembershipOperation(params) {
5287
- const { context, id, body, tableName } = params;
5288
- const service = getDynamoControlService(tableName);
5289
- const existing = await service.entities.membership.get({ tenantId: context.tenantId, id, sk: "CURRENT" }).go();
5290
- if (!existing.data) {
5291
- throw new NotFoundError(`Membership not found: ${id}`);
6158
+ function resolveTenantScopeOverride(req, res, context) {
6159
+ const raw = (req.query ?? {}).tenant;
6160
+ if (raw === void 0) {
6161
+ return { ok: true, context };
5292
6162
  }
5293
- const parsedResource = typeof body.resource === "string" ? JSON.parse(body.resource) : body.resource ?? {};
5294
- const lastUpdated = context.date ?? (/* @__PURE__ */ new Date()).toISOString();
5295
- const vid = `${Date.now()}`;
5296
- const resource = { resourceType: "Membership", id, ...parsedResource };
5297
- let linkedDataIdentityRef;
5298
- try {
5299
- const ext = (0, import_types9.assertLinkedDataIdentityCardinality)(
5300
- resource
5301
- );
5302
- linkedDataIdentityRef = ext?.valueReference?.reference;
5303
- } catch (e) {
5304
- if (e instanceof import_types9.LinkedDataIdentityCardinalityError) {
5305
- throw new ValidationError(e.message, { cause: e });
5306
- }
5307
- throw e;
6163
+ if (typeof raw !== "string" || raw.length === 0) {
6164
+ return {
6165
+ ok: false,
6166
+ response: sendInvalidQuery400(
6167
+ res,
6168
+ "Query parameter `tenant` must be a non-empty string."
6169
+ )
6170
+ };
5308
6171
  }
5309
- const resourceRecord = resource;
5310
- const denormalizedTenantName = extractDenormalizedReferenceDisplay(
5311
- resourceRecord,
5312
- "tenant"
5313
- );
5314
- const denormalizedUserName = extractDenormalizedReferenceDisplay(
5315
- resourceRecord,
5316
- "user"
5317
- );
5318
- const denormalizedWorkspaceName = extractDenormalizedReferenceDisplay(
5319
- resourceRecord,
5320
- "workspace"
5321
- );
5322
- const summary = JSON.stringify((0, import_types9.extractSummary)(resource));
5323
- const userIdFromResource = extractReferenceSlug(resourceRecord, "user");
5324
- const workspaceIdFromResource = extractReferenceSlug(
5325
- resourceRecord,
5326
- "workspace"
5327
- );
5328
- const userProjectionItem = userIdFromResource !== void 0 ? buildMembershipUserProjectionItem({
5329
- tenantId: context.tenantId,
5330
- userId: userIdFromResource,
5331
- workspaceId: workspaceIdFromResource,
5332
- membershipId: id,
5333
- summary,
5334
- vid,
5335
- lastUpdated,
6172
+ return { ok: true, context: { ...context, tenantId: raw } };
6173
+ }
6174
+ function sendForbidden403(res, diagnostics) {
6175
+ return res.status(403).json({
6176
+ resourceType: "OperationOutcome",
6177
+ issue: [{ severity: "error", code: "forbidden", diagnostics }]
6178
+ });
6179
+ }
6180
+ var COMMON_KEYS = ["cursor", "limit", "order"];
6181
+ function parseCommonListQuery(req, res, options = {}) {
6182
+ const q = req.query ?? {};
6183
+ const allowedKeys = /* @__PURE__ */ new Set([
6184
+ ...COMMON_KEYS,
6185
+ ...options.extraKeys ?? []
6186
+ ]);
6187
+ const extra = Object.keys(q).filter((k) => !allowedKeys.has(k));
6188
+ if (extra.length > 0) {
6189
+ return {
6190
+ ok: false,
6191
+ response: sendInvalidQuery400(
6192
+ res,
6193
+ `Unsupported query parameter${extra.length === 1 ? "" : "s"}: ${extra.join(", ")}.`
6194
+ )
6195
+ };
6196
+ }
6197
+ const rawCursor = q.cursor;
6198
+ let cursor = null;
6199
+ if (rawCursor !== void 0) {
6200
+ if (typeof rawCursor !== "string") {
6201
+ return {
6202
+ ok: false,
6203
+ response: sendInvalidQuery400(
6204
+ res,
6205
+ "Query parameter `cursor` must be a string."
6206
+ )
6207
+ };
6208
+ }
6209
+ cursor = rawCursor;
6210
+ }
6211
+ const rawLimit = q.limit;
6212
+ let limit;
6213
+ if (rawLimit !== void 0) {
6214
+ if (typeof rawLimit !== "string" || rawLimit.length === 0) {
6215
+ return {
6216
+ ok: false,
6217
+ response: sendInvalidQuery400(
6218
+ res,
6219
+ "Query parameter `limit` must be a positive integer."
6220
+ )
6221
+ };
6222
+ }
6223
+ const parsed = Number(rawLimit);
6224
+ if (!Number.isInteger(parsed) || parsed <= 0) {
6225
+ return {
6226
+ ok: false,
6227
+ response: sendInvalidQuery400(
6228
+ res,
6229
+ "Query parameter `limit` must be a positive integer."
6230
+ )
6231
+ };
6232
+ }
6233
+ limit = parsed;
6234
+ }
6235
+ const rawOrder = q.order;
6236
+ let order;
6237
+ if (rawOrder !== void 0) {
6238
+ if (rawOrder !== "asc" && rawOrder !== "desc") {
6239
+ return {
6240
+ ok: false,
6241
+ response: sendInvalidQuery400(
6242
+ res,
6243
+ 'Query parameter `order` must be one of "asc", "desc".'
6244
+ )
6245
+ };
6246
+ }
6247
+ order = rawOrder;
6248
+ }
6249
+ const value = order === void 0 ? limit === void 0 ? { cursor } : { cursor, limit } : limit === void 0 ? { cursor, order } : { cursor, limit, order };
6250
+ return { ok: true, value };
6251
+ }
6252
+ function buildPaginationLinks(opts) {
6253
+ const links = [
6254
+ { relation: "self", url: composeUrl(opts.basePath, opts.query) }
6255
+ ];
6256
+ if (opts.nextCursor !== null && opts.nextCursor.length > 0) {
6257
+ const nextQuery = { ...opts.query };
6258
+ nextQuery.cursor = opts.nextCursor;
6259
+ links.push({
6260
+ relation: "next",
6261
+ url: composeUrl(opts.basePath, nextQuery)
6262
+ });
6263
+ }
6264
+ return links;
6265
+ }
6266
+ function composeUrl(basePath, query) {
6267
+ const usp = new URLSearchParams();
6268
+ for (const [key, value] of Object.entries(query)) {
6269
+ if (value === void 0 || value === null) {
6270
+ continue;
6271
+ }
6272
+ if (Array.isArray(value)) {
6273
+ for (const v of value) {
6274
+ if (v !== void 0 && v !== null) {
6275
+ usp.append(key, String(v));
6276
+ }
6277
+ }
6278
+ } else {
6279
+ usp.append(key, String(value));
6280
+ }
6281
+ }
6282
+ const qs = usp.toString();
6283
+ return qs.length === 0 ? basePath : `${basePath}?${qs}`;
6284
+ }
6285
+
6286
+ // src/data/rest-api/routes/control/membership/membership-list-route.ts
6287
+ async function listMembershipsRoute(req, res) {
6288
+ const scope = resolveTenantScopeOverride(req, res, req.openhiContext);
6289
+ if (!scope.ok) {
6290
+ return scope.response;
6291
+ }
6292
+ return handleListRoute({
6293
+ req,
6294
+ res,
6295
+ basePath: BASE_PATH.MEMBERSHIP,
6296
+ listOperation: listMembershipsOperation,
6297
+ errorLogContext: "GET /Membership list error:",
6298
+ context: scope.context
6299
+ });
6300
+ }
6301
+
6302
+ // src/data/operations/control/membership/membership-update-operation.ts
6303
+ var import_types9 = require("@openhi/types");
6304
+ async function updateMembershipOperation(params) {
6305
+ const { context, id, body, tableName } = params;
6306
+ const service = getDynamoControlService(tableName);
6307
+ const existing = await service.entities.membership.get({ tenantId: context.tenantId, id, sk: "CURRENT" }).go();
6308
+ if (!existing.data) {
6309
+ throw new NotFoundError(`Membership not found: ${id}`);
6310
+ }
6311
+ const parsedResource = typeof body.resource === "string" ? JSON.parse(body.resource) : body.resource ?? {};
6312
+ const lastUpdated = context.date ?? (/* @__PURE__ */ new Date()).toISOString();
6313
+ const vid = `${Date.now()}`;
6314
+ const resource = { resourceType: "Membership", id, ...parsedResource };
6315
+ let linkedDataIdentityRef;
6316
+ try {
6317
+ const ext = (0, import_types9.assertLinkedDataIdentityCardinality)(
6318
+ resource
6319
+ );
6320
+ linkedDataIdentityRef = ext?.valueReference?.reference;
6321
+ } catch (e) {
6322
+ if (e instanceof import_types9.LinkedDataIdentityCardinalityError) {
6323
+ throw new ValidationError(e.message, { cause: e });
6324
+ }
6325
+ throw e;
6326
+ }
6327
+ const resourceRecord = resource;
6328
+ const denormalizedTenantName = extractDenormalizedReferenceDisplay(
6329
+ resourceRecord,
6330
+ "tenant"
6331
+ );
6332
+ const denormalizedUserName = extractDenormalizedReferenceDisplay(
6333
+ resourceRecord,
6334
+ "user"
6335
+ );
6336
+ const denormalizedWorkspaceName = extractDenormalizedReferenceDisplay(
6337
+ resourceRecord,
6338
+ "workspace"
6339
+ );
6340
+ const summary = JSON.stringify((0, import_types9.extractSummary)(resource));
6341
+ const userIdFromResource = extractReferenceSlug(resourceRecord, "user");
6342
+ const workspaceIdFromResource = extractReferenceSlug(
6343
+ resourceRecord,
6344
+ "workspace"
6345
+ );
6346
+ const userProjectionItem = userIdFromResource !== void 0 ? buildMembershipUserProjectionItem({
6347
+ tenantId: context.tenantId,
6348
+ userId: userIdFromResource,
6349
+ workspaceId: workspaceIdFromResource,
6350
+ membershipId: id,
6351
+ summary,
6352
+ vid,
6353
+ lastUpdated,
5336
6354
  denormalizedTenantName,
5337
6355
  denormalizedUserName,
5338
6356
  denormalizedWorkspaceName
@@ -5902,6 +6920,18 @@ async function createRoleAssignmentOperation(params) {
5902
6920
  });
5903
6921
  }
5904
6922
  await executeMultiWrite({ service, triples });
6923
+ await publishRoleAssignmentCreated(context, {
6924
+ roleAssignmentId: id,
6925
+ tenantId: context.tenantId,
6926
+ ...userIdFromResource !== void 0 && { userId: userIdFromResource },
6927
+ ...workspaceIdFromResource !== void 0 && {
6928
+ workspaceId: workspaceIdFromResource
6929
+ },
6930
+ ...roleIdFromResource !== void 0 && { roleId: roleIdFromResource },
6931
+ ...extractRoleLevel(resourceRecord) !== void 0 && {
6932
+ roleLevel: extractRoleLevel(resourceRecord)
6933
+ }
6934
+ });
5905
6935
  return {
5906
6936
  id,
5907
6937
  resource,
@@ -6029,6 +7059,18 @@ async function deleteRoleAssignmentOperation(params) {
6029
7059
  });
6030
7060
  }
6031
7061
  await executeMultiWrite({ service, triples });
7062
+ await publishRoleAssignmentDeleted(context, {
7063
+ roleAssignmentId: id,
7064
+ tenantId: context.tenantId,
7065
+ ...userIdFromResource !== void 0 && { userId: userIdFromResource },
7066
+ ...workspaceIdFromResource !== void 0 && {
7067
+ workspaceId: workspaceIdFromResource
7068
+ },
7069
+ ...roleIdFromResource !== void 0 && { roleId: roleIdFromResource },
7070
+ ...extractRoleLevel(parsed) !== void 0 && {
7071
+ roleLevel: extractRoleLevel(parsed)
7072
+ }
7073
+ });
6032
7074
  }
6033
7075
 
6034
7076
  // src/data/rest-api/routes/control/roleassignment/roleassignment-delete-route.ts
@@ -6440,6 +7482,9 @@ async function getTenantByIdRoute(req, res) {
6440
7482
 
6441
7483
  // src/data/operations/control/tenant/tenant-list-operation.ts
6442
7484
  var SK9 = "CURRENT";
7485
+ function counterValue(value) {
7486
+ return typeof value === "number" && Number.isFinite(value) ? value : 0;
7487
+ }
6443
7488
  async function listTenantsOperation(params) {
6444
7489
  const { tableName, mode = "full" } = params;
6445
7490
  const service = getDynamoControlService(tableName);
@@ -6449,40 +7494,278 @@ async function listTenantsOperation(params) {
6449
7494
  (_, shard) => service.entities.tenant.query.gsi1({ gsi1Shard: String(shard) }).go()
6450
7495
  )
6451
7496
  );
6452
- return dispatchListMode(
6453
- mode,
6454
- shardResults,
6455
- {
6456
- hydrate: (orderedIds) => batchGetWithRetry(
6457
- service.entities.tenant,
6458
- orderedIds.map((id) => ({ tenantId: id, sk: SK9 }))
6459
- ),
6460
- getId: (item) => item.id,
6461
- buildEntry: (id, item) => ({
7497
+ return dispatchListMode(mode, shardResults, {
7498
+ hydrate: (orderedIds) => batchGetWithRetry(
7499
+ service.entities.tenant,
7500
+ orderedIds.map((id) => ({ tenantId: id, sk: SK9 }))
7501
+ ),
7502
+ getId: (item) => item.id,
7503
+ // FULL mode (admin list default): read the ADR-028 counters off the
7504
+ // canonical record hydrated by BatchGet and expose them as
7505
+ // `resource.counts`. Missing counters render as 0.
7506
+ buildEntry: (id, item) => ({
7507
+ id,
7508
+ resource: {
7509
+ resourceType: "Tenant",
6462
7510
  id,
6463
- resource: {
6464
- resourceType: "Tenant",
6465
- id,
6466
- ...JSON.parse(item.resource)
7511
+ ...JSON.parse(item.resource),
7512
+ counts: {
7513
+ usersInTenant: counterValue(item.usersInTenant),
7514
+ workspacesInTenant: counterValue(item.workspacesInTenant)
6467
7515
  }
6468
- }),
6469
- buildSummaryEntry: (id, parsed) => ({
7516
+ }
7517
+ }),
7518
+ // SUMMARY mode reads only the GSI1 `summary` projection, which does
7519
+ // not carry the counters; surface zeros so the shape stays uniform.
7520
+ buildSummaryEntry: (id, parsed) => ({
7521
+ id,
7522
+ resource: {
7523
+ resourceType: "Tenant",
6470
7524
  id,
6471
- resource: { resourceType: "Tenant", id, ...parsed }
6472
- })
7525
+ ...parsed,
7526
+ counts: { usersInTenant: 0, workspacesInTenant: 0 }
7527
+ }
7528
+ })
7529
+ });
7530
+ }
7531
+
7532
+ // src/data/rest-api/routes/control/tenant/tenant-list-route.ts
7533
+ async function listTenantsRoute(req, res) {
7534
+ return handleListRoute({
7535
+ req,
7536
+ res,
7537
+ basePath: BASE_PATH.TENANT,
7538
+ listOperation: listTenantsOperation,
7539
+ errorLogContext: "GET /Tenant list error:"
7540
+ });
7541
+ }
7542
+
7543
+ // src/data/operations/control/workspace/workspace-list-operation.ts
7544
+ var SK10 = "CURRENT";
7545
+ function counterValue2(value) {
7546
+ return typeof value === "number" && Number.isFinite(value) ? value : 0;
7547
+ }
7548
+ async function listWorkspacesOperation(params) {
7549
+ const { context, tableName, mode = "full" } = params;
7550
+ const { tenantId } = context;
7551
+ const service = getDynamoControlService(tableName);
7552
+ const shardResults = await Promise.all(
7553
+ Array.from(
7554
+ { length: SHARD_COUNT },
7555
+ (_, shard) => service.entities.workspace.query.gsi1({ tenantId, gsi1Shard: String(shard) }).go()
7556
+ )
7557
+ );
7558
+ return dispatchListMode(mode, shardResults, {
7559
+ hydrate: (orderedIds) => batchGetWithRetry(
7560
+ service.entities.workspace,
7561
+ orderedIds.map((id) => ({ tenantId, id, sk: SK10 }))
7562
+ ),
7563
+ getId: (item) => item.id,
7564
+ // FULL mode (admin list default): read the ADR-028 counters off the
7565
+ // canonical record hydrated by BatchGet and expose them as
7566
+ // `resource.counts`. Missing counters render as 0.
7567
+ buildEntry: (id, item) => ({
7568
+ id,
7569
+ resource: {
7570
+ resourceType: "Workspace",
7571
+ id,
7572
+ ...JSON.parse(item.resource),
7573
+ counts: {
7574
+ usersInWorkspace: counterValue2(item.usersInWorkspace),
7575
+ adminUsersInWorkspace: counterValue2(item.adminUsersInWorkspace),
7576
+ normalUsersInWorkspace: counterValue2(item.normalUsersInWorkspace)
7577
+ }
7578
+ }
7579
+ }),
7580
+ // SUMMARY mode reads only the GSI1 `summary` projection (no
7581
+ // counters); surface zeros so the shape stays uniform.
7582
+ buildSummaryEntry: (id, parsed) => ({
7583
+ id,
7584
+ resource: {
7585
+ resourceType: "Workspace",
7586
+ id,
7587
+ ...parsed,
7588
+ counts: {
7589
+ usersInWorkspace: 0,
7590
+ adminUsersInWorkspace: 0,
7591
+ normalUsersInWorkspace: 0
7592
+ }
7593
+ }
7594
+ })
7595
+ });
7596
+ }
7597
+
7598
+ // src/data/operations/control/tenant/tenant-users-operation.ts
7599
+ var USER_SK = "CURRENT";
7600
+ function extractEmail(user) {
7601
+ const telecom = user.telecom;
7602
+ if (!Array.isArray(telecom)) {
7603
+ return null;
7604
+ }
7605
+ for (const entry of telecom) {
7606
+ if (entry.system === "email" && typeof entry.value === "string" && entry.value.length > 0) {
7607
+ return entry.value;
7608
+ }
7609
+ }
7610
+ return null;
7611
+ }
7612
+ function extractDisplayName(user) {
7613
+ const name = user.name;
7614
+ if (!Array.isArray(name) || name.length === 0) {
7615
+ return null;
7616
+ }
7617
+ const first = name[0];
7618
+ if (typeof first.text === "string" && first.text.trim().length > 0) {
7619
+ return first.text.trim();
7620
+ }
7621
+ const given = Array.isArray(first.given) ? first.given.filter((g) => typeof g === "string").join(" ").trim() : "";
7622
+ const family = typeof first.family === "string" ? first.family : "";
7623
+ const composed = `${given} ${family}`.trim();
7624
+ return composed.length > 0 ? composed : null;
7625
+ }
7626
+ function extractReferenceDisplay(resource, fieldName) {
7627
+ const field = resource[fieldName];
7628
+ if (!field || typeof field !== "object") {
7629
+ return null;
7630
+ }
7631
+ const display = field.display;
7632
+ if (typeof display !== "string") {
7633
+ return null;
7634
+ }
7635
+ const trimmed = display.trim();
7636
+ return trimmed.length > 0 ? trimmed : null;
7637
+ }
7638
+ function ensureAccumulator(byUser, userId) {
7639
+ let acc = byUser.get(userId);
7640
+ if (!acc) {
7641
+ acc = { tenantRole: null, workspaces: /* @__PURE__ */ new Map() };
7642
+ byUser.set(userId, acc);
7643
+ }
7644
+ return acc;
7645
+ }
7646
+ async function tenantUsersOperation(params) {
7647
+ const { context, tableName } = params;
7648
+ const service = getDynamoControlService(tableName);
7649
+ const [memberships, roleAssignments, workspaces] = await Promise.all([
7650
+ listMembershipsOperation({ context, tableName }),
7651
+ listRoleAssignmentsOperation({ context, tableName }),
7652
+ listWorkspacesOperation({ context, tableName })
7653
+ ]);
7654
+ const workspaceNames = /* @__PURE__ */ new Map();
7655
+ for (const entry of workspaces.entries) {
7656
+ const resource = entry.resource;
7657
+ const name = typeof resource.name === "string" && resource.name.trim().length > 0 ? resource.name.trim() : null;
7658
+ workspaceNames.set(entry.id, name);
7659
+ }
7660
+ const byUser = /* @__PURE__ */ new Map();
7661
+ for (const entry of memberships.entries) {
7662
+ const resource = entry.resource;
7663
+ const userId = extractReferenceSlug(resource, "user");
7664
+ if (userId === void 0) {
7665
+ continue;
7666
+ }
7667
+ const acc = ensureAccumulator(byUser, userId);
7668
+ const workspaceId = extractReferenceSlug(resource, "workspace");
7669
+ if (workspaceId !== void 0 && !acc.workspaces.has(workspaceId)) {
7670
+ acc.workspaces.set(workspaceId, { workspaceId, role: null });
7671
+ }
7672
+ }
7673
+ for (const entry of roleAssignments.entries) {
7674
+ const resource = entry.resource;
7675
+ const userId = extractReferenceSlug(resource, "user");
7676
+ const roleId = extractReferenceSlug(resource, "role");
7677
+ if (userId === void 0 || roleId === void 0) {
7678
+ continue;
6473
7679
  }
7680
+ const acc = ensureAccumulator(byUser, userId);
7681
+ const role = {
7682
+ roleId,
7683
+ roleName: extractReferenceDisplay(resource, "role")
7684
+ };
7685
+ const workspaceId = extractReferenceSlug(resource, "workspace");
7686
+ if (workspaceId === void 0) {
7687
+ acc.tenantRole = role;
7688
+ } else {
7689
+ const existing = acc.workspaces.get(workspaceId);
7690
+ if (existing) {
7691
+ existing.role = role;
7692
+ } else {
7693
+ acc.workspaces.set(workspaceId, { workspaceId, role });
7694
+ }
7695
+ }
7696
+ }
7697
+ const userIds = Array.from(byUser.keys());
7698
+ const userRows = userIds.length === 0 ? [] : await batchGetWithRetry(
7699
+ service.entities.user,
7700
+ userIds.map((id) => ({ id, sk: USER_SK }))
6474
7701
  );
7702
+ const usersById = /* @__PURE__ */ new Map();
7703
+ for (const row of userRows) {
7704
+ try {
7705
+ usersById.set(
7706
+ row.id,
7707
+ JSON.parse(row.resource)
7708
+ );
7709
+ } catch {
7710
+ }
7711
+ }
7712
+ const entries = userIds.map((userId) => {
7713
+ const acc = byUser.get(userId);
7714
+ const user = usersById.get(userId);
7715
+ const workspaceEntries = Array.from(
7716
+ acc.workspaces.values()
7717
+ ).map((ws) => ({
7718
+ workspaceId: ws.workspaceId,
7719
+ workspaceName: workspaceNames.get(ws.workspaceId) ?? null,
7720
+ role: ws.role
7721
+ }));
7722
+ return {
7723
+ resourceType: "TenantUser",
7724
+ userId,
7725
+ displayName: user ? extractDisplayName(user) : null,
7726
+ email: user ? extractEmail(user) : null,
7727
+ tenantRole: acc.tenantRole,
7728
+ workspaces: workspaceEntries
7729
+ };
7730
+ });
7731
+ return { entries };
6475
7732
  }
6476
7733
 
6477
- // src/data/rest-api/routes/control/tenant/tenant-list-route.ts
6478
- async function listTenantsRoute(req, res) {
6479
- return handleListRoute({
6480
- req,
6481
- res,
6482
- basePath: BASE_PATH.TENANT,
6483
- listOperation: listTenantsOperation,
6484
- errorLogContext: "GET /Tenant list error:"
6485
- });
7734
+ // src/data/rest-api/routes/control/tenant/tenant-list-users-route.ts
7735
+ async function listTenantUsersRoute(req, res) {
7736
+ const ctx = req.openhiContext;
7737
+ if (!ctx) {
7738
+ return sendForbidden403(
7739
+ res,
7740
+ "Missing or invalid OpenHI JWT claims (tenant, workspace, or audit context)."
7741
+ );
7742
+ }
7743
+ const tenantId = String(req.params.id);
7744
+ if (tenantId.length === 0) {
7745
+ return sendForbidden403(res, "Tenant id is required.");
7746
+ }
7747
+ const parsed = parseCommonListQuery(req, res);
7748
+ if (!parsed.ok) {
7749
+ return parsed.response;
7750
+ }
7751
+ const scopedContext = { ...ctx, tenantId };
7752
+ try {
7753
+ const result = await tenantUsersOperation({ context: scopedContext });
7754
+ const basePath = `${BASE_PATH.TENANT}/${tenantId}/users`;
7755
+ const entries = result.entries.map((entry) => ({
7756
+ fullUrl: `${BASE_PATH.USER}/${entry.userId}`,
7757
+ resource: entry
7758
+ }));
7759
+ return res.json({
7760
+ resourceType: "Bundle",
7761
+ type: "searchset",
7762
+ total: entries.length,
7763
+ link: [{ relation: "self", url: basePath }],
7764
+ entry: entries
7765
+ });
7766
+ } catch (err) {
7767
+ return sendOperationOutcome500(res, err, "GET /Tenant/:id/users error:");
7768
+ }
6486
7769
  }
6487
7770
 
6488
7771
  // src/data/operations/control/tenant/tenant-update-operation.ts
@@ -6552,6 +7835,7 @@ async function updateTenantRoute(req, res) {
6552
7835
  // src/data/rest-api/routes/control/tenant/tenant.ts
6553
7836
  var router6 = import_express6.default.Router();
6554
7837
  router6.get("/", listTenantsRoute);
7838
+ router6.get("/:id/users", listTenantUsersRoute);
6555
7839
  router6.get("/:id", getTenantByIdRoute);
6556
7840
  router6.post("/", createTenantRoute);
6557
7841
  router6.put("/:id", updateTenantRoute);
@@ -6747,125 +8031,6 @@ async function configurationListByWorkspaceOperation(params) {
6747
8031
  return { items, cursor: result.cursor ?? null };
6748
8032
  }
6749
8033
 
6750
- // src/data/rest-api/routes/control/cross-cutting-route-helpers.ts
6751
- function sendInvalidQuery400(res, diagnostics) {
6752
- return res.status(400).json({
6753
- resourceType: "OperationOutcome",
6754
- issue: [{ severity: "error", code: "invalid", diagnostics }]
6755
- });
6756
- }
6757
- function sendForbidden403(res, diagnostics) {
6758
- return res.status(403).json({
6759
- resourceType: "OperationOutcome",
6760
- issue: [{ severity: "error", code: "forbidden", diagnostics }]
6761
- });
6762
- }
6763
- var COMMON_KEYS = ["cursor", "limit", "order"];
6764
- function parseCommonListQuery(req, res, options = {}) {
6765
- const q = req.query ?? {};
6766
- const allowedKeys = /* @__PURE__ */ new Set([
6767
- ...COMMON_KEYS,
6768
- ...options.extraKeys ?? []
6769
- ]);
6770
- const extra = Object.keys(q).filter((k) => !allowedKeys.has(k));
6771
- if (extra.length > 0) {
6772
- return {
6773
- ok: false,
6774
- response: sendInvalidQuery400(
6775
- res,
6776
- `Unsupported query parameter${extra.length === 1 ? "" : "s"}: ${extra.join(", ")}.`
6777
- )
6778
- };
6779
- }
6780
- const rawCursor = q.cursor;
6781
- let cursor = null;
6782
- if (rawCursor !== void 0) {
6783
- if (typeof rawCursor !== "string") {
6784
- return {
6785
- ok: false,
6786
- response: sendInvalidQuery400(
6787
- res,
6788
- "Query parameter `cursor` must be a string."
6789
- )
6790
- };
6791
- }
6792
- cursor = rawCursor;
6793
- }
6794
- const rawLimit = q.limit;
6795
- let limit;
6796
- if (rawLimit !== void 0) {
6797
- if (typeof rawLimit !== "string" || rawLimit.length === 0) {
6798
- return {
6799
- ok: false,
6800
- response: sendInvalidQuery400(
6801
- res,
6802
- "Query parameter `limit` must be a positive integer."
6803
- )
6804
- };
6805
- }
6806
- const parsed = Number(rawLimit);
6807
- if (!Number.isInteger(parsed) || parsed <= 0) {
6808
- return {
6809
- ok: false,
6810
- response: sendInvalidQuery400(
6811
- res,
6812
- "Query parameter `limit` must be a positive integer."
6813
- )
6814
- };
6815
- }
6816
- limit = parsed;
6817
- }
6818
- const rawOrder = q.order;
6819
- let order;
6820
- if (rawOrder !== void 0) {
6821
- if (rawOrder !== "asc" && rawOrder !== "desc") {
6822
- return {
6823
- ok: false,
6824
- response: sendInvalidQuery400(
6825
- res,
6826
- 'Query parameter `order` must be one of "asc", "desc".'
6827
- )
6828
- };
6829
- }
6830
- order = rawOrder;
6831
- }
6832
- const value = order === void 0 ? limit === void 0 ? { cursor } : { cursor, limit } : limit === void 0 ? { cursor, order } : { cursor, limit, order };
6833
- return { ok: true, value };
6834
- }
6835
- function buildPaginationLinks(opts) {
6836
- const links = [
6837
- { relation: "self", url: composeUrl(opts.basePath, opts.query) }
6838
- ];
6839
- if (opts.nextCursor !== null && opts.nextCursor.length > 0) {
6840
- const nextQuery = { ...opts.query };
6841
- nextQuery.cursor = opts.nextCursor;
6842
- links.push({
6843
- relation: "next",
6844
- url: composeUrl(opts.basePath, nextQuery)
6845
- });
6846
- }
6847
- return links;
6848
- }
6849
- function composeUrl(basePath, query) {
6850
- const usp = new URLSearchParams();
6851
- for (const [key, value] of Object.entries(query)) {
6852
- if (value === void 0 || value === null) {
6853
- continue;
6854
- }
6855
- if (Array.isArray(value)) {
6856
- for (const v of value) {
6857
- if (v !== void 0 && v !== null) {
6858
- usp.append(key, String(v));
6859
- }
6860
- }
6861
- } else {
6862
- usp.append(key, String(value));
6863
- }
6864
- }
6865
- const qs = usp.toString();
6866
- return qs.length === 0 ? basePath : `${basePath}?${qs}`;
6867
- }
6868
-
6869
8034
  // src/data/rest-api/routes/control/projection-bundle-helpers.ts
6870
8035
  var EXT_BASE = "https://openhi.org/fhir/StructureDefinition";
6871
8036
  function parseProjectionSummary(summary) {
@@ -7181,6 +8346,20 @@ async function membershipListByUserOperation(params) {
7181
8346
  return { items, cursor: result.cursor ?? null };
7182
8347
  }
7183
8348
 
8349
+ // src/data/operations/control/membership/membership-count-by-user-operation.ts
8350
+ async function countMembershipsByUserOperation(params) {
8351
+ const { userId, mode = "all", tenantId, tableName } = params;
8352
+ if (mode === "workspaceInTenant" && !tenantId) {
8353
+ throw new Error(
8354
+ 'countMembershipsByUserOperation: tenantId is required when mode === "workspaceInTenant"'
8355
+ );
8356
+ }
8357
+ const service = getDynamoControlService(tableName);
8358
+ const skPrefix = buildSkPrefix(mode, tenantId);
8359
+ const result = await service.entities.membershipUserProjection.query.record({ userId }).begins({ sk: skPrefix }).go({ pages: "all", attributes: ["membershipId"] });
8360
+ return (result.data ?? []).length;
8361
+ }
8362
+
7184
8363
  // src/data/operations/control/membership/membership-list-by-workspace-operation.ts
7185
8364
  async function membershipListByWorkspaceOperation(params) {
7186
8365
  const {
@@ -7239,7 +8418,7 @@ async function listUserMembershipsRoute(req, res) {
7239
8418
  );
7240
8419
  }
7241
8420
  const parsed = parseCommonListQuery(req, res, {
7242
- extraKeys: ["mode", "tenantId"]
8421
+ extraKeys: ["mode", "tenantId", "_summary"]
7243
8422
  });
7244
8423
  if (!parsed.ok) {
7245
8424
  return parsed.response;
@@ -7272,6 +8451,33 @@ async function listUserMembershipsRoute(req, res) {
7272
8451
  'Query parameter `tenantId` is required when `mode === "workspaceInTenant"`.'
7273
8452
  );
7274
8453
  }
8454
+ const rawSummary = req.query._summary;
8455
+ if (rawSummary !== void 0) {
8456
+ if (rawSummary !== "count") {
8457
+ return sendInvalidQuery400(
8458
+ res,
8459
+ 'Query parameter `_summary` must be "count" on this endpoint.'
8460
+ );
8461
+ }
8462
+ try {
8463
+ const total = await countMembershipsByUserOperation({
8464
+ userId,
8465
+ mode,
8466
+ tenantId
8467
+ });
8468
+ return res.json({
8469
+ resourceType: "Bundle",
8470
+ type: "searchset",
8471
+ total
8472
+ });
8473
+ } catch (err) {
8474
+ return sendOperationOutcome500(
8475
+ res,
8476
+ err,
8477
+ "GET /User/:id/Membership count error:"
8478
+ );
8479
+ }
8480
+ }
7275
8481
  try {
7276
8482
  const result = await membershipListByUserOperation({
7277
8483
  userId,
@@ -7504,7 +8710,10 @@ async function listUserRoleAssignmentsRoute(req, res) {
7504
8710
  }
7505
8711
 
7506
8712
  // src/data/operations/control/user/user-list-operation.ts
7507
- var SK10 = "CURRENT";
8713
+ var SK11 = "CURRENT";
8714
+ function counterValue3(value) {
8715
+ return typeof value === "number" && Number.isFinite(value) ? value : 0;
8716
+ }
7508
8717
  async function listUsersOperation(params) {
7509
8718
  const { tableName, mode = "full" } = params;
7510
8719
  const service = getDynamoControlService(tableName);
@@ -7517,20 +8726,34 @@ async function listUsersOperation(params) {
7517
8726
  return dispatchListMode(mode, shardResults, {
7518
8727
  hydrate: (orderedIds) => batchGetWithRetry(
7519
8728
  service.entities.user,
7520
- orderedIds.map((id) => ({ id, sk: SK10 }))
8729
+ orderedIds.map((id) => ({ id, sk: SK11 }))
7521
8730
  ),
7522
8731
  getId: (item) => item.id,
8732
+ // FULL mode (admin list default): read the ADR-028 counters off the
8733
+ // canonical record hydrated by BatchGet and expose them as
8734
+ // `resource.counts`. Missing counters render as 0.
7523
8735
  buildEntry: (id, item) => ({
7524
8736
  id,
7525
8737
  resource: {
7526
8738
  resourceType: "User",
7527
8739
  id,
7528
- ...JSON.parse(item.resource)
8740
+ ...JSON.parse(item.resource),
8741
+ counts: {
8742
+ tenantsForUser: counterValue3(item.tenantsForUser),
8743
+ workspacesForUser: counterValue3(item.workspacesForUser)
8744
+ }
7529
8745
  }
7530
8746
  }),
8747
+ // SUMMARY mode reads only the GSI1 `summary` projection (no
8748
+ // counters); surface zeros so the shape stays uniform.
7531
8749
  buildSummaryEntry: (id, parsed) => ({
7532
8750
  id,
7533
- resource: { resourceType: "User", id, ...parsed }
8751
+ resource: {
8752
+ resourceType: "User",
8753
+ id,
8754
+ ...parsed,
8755
+ counts: { tenantsForUser: 0, workspacesForUser: 0 }
8756
+ }
7534
8757
  })
7535
8758
  });
7536
8759
  }
@@ -7687,7 +8910,7 @@ function idFromReference(reference, prefix) {
7687
8910
  }
7688
8911
 
7689
8912
  // src/data/operations/control/user/user-switch-tenant-workspace-operation.ts
7690
- var SK11 = "CURRENT";
8913
+ var SK12 = "CURRENT";
7691
8914
  async function switchUserTenantWorkspaceOperation(params) {
7692
8915
  const { cognitoSub, tenantReference, workspaceReference, tableName } = params;
7693
8916
  const tenantId = idFromReference(tenantReference, "Tenant/");
@@ -7748,7 +8971,7 @@ async function switchUserTenantWorkspaceOperation(params) {
7748
8971
  (0, import_types20.extractSummary)(updatedResource)
7749
8972
  );
7750
8973
  const service = getDynamoControlService(tableName);
7751
- await service.entities.user.patch({ id: user.id, sk: SK11 }).set({
8974
+ await service.entities.user.patch({ id: user.id, sk: SK12 }).set({
7752
8975
  resource: JSON.stringify(updatedResource),
7753
8976
  summary,
7754
8977
  vid,
@@ -8227,6 +9450,10 @@ async function createWorkspaceOperation(params) {
8227
9450
  workspaceName,
8228
9451
  tableName
8229
9452
  });
9453
+ await publishWorkspaceCreated(context, {
9454
+ workspaceId: id,
9455
+ tenantId
9456
+ });
8230
9457
  return { id, resource, meta: { lastUpdated, versionId: vid } };
8231
9458
  }
8232
9459
 
@@ -8275,12 +9502,20 @@ async function deleteWorkspaceOperation(params) {
8275
9502
  const { context, id, tableName } = params;
8276
9503
  const { tenantId } = context;
8277
9504
  const service = getDynamoControlService(tableName);
9505
+ const existing = await service.entities.workspace.get({ tenantId, id, sk: "CURRENT" }).go();
9506
+ const workspaceExisted = existing.data !== null;
8278
9507
  await service.entities.workspace.delete({ tenantId, id, sk: "CURRENT" }).go();
8279
9508
  await deleteOrganizationOperation({
8280
9509
  context: { ...context, workspaceId: id },
8281
9510
  id,
8282
9511
  tableName
8283
9512
  });
9513
+ if (workspaceExisted) {
9514
+ await publishWorkspaceDeleted(context, {
9515
+ workspaceId: id,
9516
+ tenantId
9517
+ });
9518
+ }
8284
9519
  }
8285
9520
 
8286
9521
  // src/data/rest-api/routes/control/workspace/workspace-delete-route.ts
@@ -8546,51 +9781,19 @@ async function listWorkspaceRoleAssignmentsRoute(req, res) {
8546
9781
  }
8547
9782
  }
8548
9783
 
8549
- // src/data/operations/control/workspace/workspace-list-operation.ts
8550
- var SK12 = "CURRENT";
8551
- async function listWorkspacesOperation(params) {
8552
- const { context, tableName, mode = "full" } = params;
8553
- const { tenantId } = context;
8554
- const service = getDynamoControlService(tableName);
8555
- const shardResults = await Promise.all(
8556
- Array.from(
8557
- { length: SHARD_COUNT },
8558
- (_, shard) => service.entities.workspace.query.gsi1({ tenantId, gsi1Shard: String(shard) }).go()
8559
- )
8560
- );
8561
- return dispatchListMode(
8562
- mode,
8563
- shardResults,
8564
- {
8565
- hydrate: (orderedIds) => batchGetWithRetry(
8566
- service.entities.workspace,
8567
- orderedIds.map((id) => ({ tenantId, id, sk: SK12 }))
8568
- ),
8569
- getId: (item) => item.id,
8570
- buildEntry: (id, item) => ({
8571
- id,
8572
- resource: {
8573
- resourceType: "Workspace",
8574
- id,
8575
- ...JSON.parse(item.resource)
8576
- }
8577
- }),
8578
- buildSummaryEntry: (id, parsed) => ({
8579
- id,
8580
- resource: { resourceType: "Workspace", id, ...parsed }
8581
- })
8582
- }
8583
- );
8584
- }
8585
-
8586
9784
  // src/data/rest-api/routes/control/workspace/workspace-list-route.ts
8587
9785
  async function listWorkspacesRoute(req, res) {
9786
+ const scope = resolveTenantScopeOverride(req, res, req.openhiContext);
9787
+ if (!scope.ok) {
9788
+ return scope.response;
9789
+ }
8588
9790
  return handleListRoute({
8589
9791
  req,
8590
9792
  res,
8591
9793
  basePath: BASE_PATH.WORKSPACE,
8592
9794
  listOperation: listWorkspacesOperation,
8593
- errorLogContext: "GET /Workspace list error:"
9795
+ errorLogContext: "GET /Workspace list error:",
9796
+ context: scope.context
8594
9797
  });
8595
9798
  }
8596
9799