@openhi/constructs 0.0.104 → 0.0.106

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 (83) hide show
  1. package/README.md +14 -0
  2. package/lib/chunk-2PM2NGXI.mjs +31 -0
  3. package/lib/chunk-2PM2NGXI.mjs.map +1 -0
  4. package/lib/chunk-AGF3RAAZ.mjs +20 -0
  5. package/lib/chunk-AGF3RAAZ.mjs.map +1 -0
  6. package/lib/chunk-AO3E22CS.mjs +108 -0
  7. package/lib/chunk-AO3E22CS.mjs.map +1 -0
  8. package/lib/chunk-CHPEQRXU.mjs +45 -0
  9. package/lib/chunk-CHPEQRXU.mjs.map +1 -0
  10. package/lib/chunk-JUNL76HF.mjs +428 -0
  11. package/lib/chunk-JUNL76HF.mjs.map +1 -0
  12. package/lib/chunk-L6UAP4KP.mjs +27 -0
  13. package/lib/chunk-L6UAP4KP.mjs.map +1 -0
  14. package/lib/{chunk-3QS3WKRC.mjs → chunk-LZOMFHX3.mjs} +9 -2
  15. package/lib/chunk-QMIOLLAS.mjs +531 -0
  16. package/lib/chunk-QMIOLLAS.mjs.map +1 -0
  17. package/lib/chunk-SYBADQXI.mjs +607 -0
  18. package/lib/chunk-SYBADQXI.mjs.map +1 -0
  19. package/lib/chunk-VXX4I3EF.mjs +19 -0
  20. package/lib/chunk-VXX4I3EF.mjs.map +1 -0
  21. package/lib/{chunk-MLTYFMSE.mjs → chunk-VYDIGFIX.mjs} +74 -29
  22. package/lib/chunk-VYDIGFIX.mjs.map +1 -0
  23. package/lib/chunk-YU2HRNUP.mjs +33 -0
  24. package/lib/chunk-YU2HRNUP.mjs.map +1 -0
  25. package/lib/chunk-YZZDUJHI.mjs +37 -0
  26. package/lib/chunk-YZZDUJHI.mjs.map +1 -0
  27. package/lib/cors-options-lambda.handler.mjs +1 -1
  28. package/lib/data-store-postgres-replication.handler.mjs +1 -1
  29. package/lib/events-BfrkMoBD.d.mts +44 -0
  30. package/lib/events-BfrkMoBD.d.ts +44 -0
  31. package/lib/events-CVA3_eEB.d.mts +23 -0
  32. package/lib/events-CVA3_eEB.d.ts +23 -0
  33. package/lib/events-DGep6C7w.d.mts +207 -0
  34. package/lib/events-DGep6C7w.d.ts +207 -0
  35. package/lib/firehose-archive-transform.handler.mjs +1 -1
  36. package/lib/index.d.mts +508 -29
  37. package/lib/index.d.ts +773 -30
  38. package/lib/index.js +2536 -105
  39. package/lib/index.js.map +1 -1
  40. package/lib/index.mjs +899 -106
  41. package/lib/index.mjs.map +1 -1
  42. package/lib/openhi-context-CaBH8SFo.d.mts +39 -0
  43. package/lib/openhi-context-CaBH8SFo.d.ts +39 -0
  44. package/lib/platform-deploy-bridge.handler.d.mts +14 -0
  45. package/lib/platform-deploy-bridge.handler.d.ts +14 -0
  46. package/lib/platform-deploy-bridge.handler.js +762 -0
  47. package/lib/platform-deploy-bridge.handler.js.map +1 -0
  48. package/lib/platform-deploy-bridge.handler.mjs +134 -0
  49. package/lib/platform-deploy-bridge.handler.mjs.map +1 -0
  50. package/lib/post-authentication.handler.mjs +1 -1
  51. package/lib/post-confirmation.handler.js +50 -904
  52. package/lib/post-confirmation.handler.js.map +1 -1
  53. package/lib/post-confirmation.handler.mjs +37 -112
  54. package/lib/post-confirmation.handler.mjs.map +1 -1
  55. package/lib/pre-token-generation.handler.js +135 -55
  56. package/lib/pre-token-generation.handler.js.map +1 -1
  57. package/lib/pre-token-generation.handler.mjs +25 -32
  58. package/lib/pre-token-generation.handler.mjs.map +1 -1
  59. package/lib/provision-default-workspace.handler.d.mts +13 -0
  60. package/lib/provision-default-workspace.handler.d.ts +13 -0
  61. package/lib/provision-default-workspace.handler.js +1172 -0
  62. package/lib/provision-default-workspace.handler.js.map +1 -0
  63. package/lib/provision-default-workspace.handler.mjs +175 -0
  64. package/lib/provision-default-workspace.handler.mjs.map +1 -0
  65. package/lib/rest-api-lambda.handler.js +114 -59
  66. package/lib/rest-api-lambda.handler.js.map +1 -1
  67. package/lib/rest-api-lambda.handler.mjs +60 -587
  68. package/lib/rest-api-lambda.handler.mjs.map +1 -1
  69. package/lib/seed-demo-data.handler.d.mts +107 -0
  70. package/lib/seed-demo-data.handler.d.ts +107 -0
  71. package/lib/seed-demo-data.handler.js +2037 -0
  72. package/lib/seed-demo-data.handler.js.map +1 -0
  73. package/lib/seed-demo-data.handler.mjs +23 -0
  74. package/lib/seed-demo-data.handler.mjs.map +1 -0
  75. package/lib/seed-system-data.handler.d.mts +64 -0
  76. package/lib/seed-system-data.handler.d.ts +64 -0
  77. package/lib/seed-system-data.handler.js +1631 -0
  78. package/lib/seed-system-data.handler.js.map +1 -0
  79. package/lib/seed-system-data.handler.mjs +135 -0
  80. package/lib/seed-system-data.handler.mjs.map +1 -0
  81. package/package.json +4 -2
  82. package/lib/chunk-MLTYFMSE.mjs.map +0 -1
  83. /package/lib/{chunk-3QS3WKRC.mjs.map → chunk-LZOMFHX3.mjs.map} +0 -0
package/lib/index.js CHANGED
@@ -90,23 +90,637 @@ var require_lib = __commonJS({
90
90
  }
91
91
  });
92
92
 
93
+ // ../workflows/lib/envelope-version.js
94
+ var require_envelope_version = __commonJS({
95
+ "../workflows/lib/envelope-version.js"(exports2) {
96
+ "use strict";
97
+ Object.defineProperty(exports2, "__esModule", { value: true });
98
+ exports2.ENVELOPE_VERSION = void 0;
99
+ exports2.isSupportedEnvelopeVersion = isSupportedEnvelopeVersion;
100
+ exports2.ENVELOPE_VERSION = "1.0";
101
+ var ENVELOPE_VERSION_PATTERN = /^\d+\.\d+$/;
102
+ var MIN_SUPPORTED_MAJOR = 1;
103
+ var MAX_SUPPORTED_MAJOR = 1;
104
+ function isSupportedEnvelopeVersion(version) {
105
+ if (!ENVELOPE_VERSION_PATTERN.test(version)) {
106
+ return false;
107
+ }
108
+ const major = Number.parseInt(version.split(".")[0], 10);
109
+ return major >= MIN_SUPPORTED_MAJOR && major <= MAX_SUPPORTED_MAJOR;
110
+ }
111
+ }
112
+ });
113
+
114
+ // ../workflows/lib/envelope.js
115
+ var require_envelope = __commonJS({
116
+ "../workflows/lib/envelope.js"(exports2) {
117
+ "use strict";
118
+ Object.defineProperty(exports2, "__esModule", { value: true });
119
+ exports2.MissingActorContextError = void 0;
120
+ exports2.isWorkflowUserActor = isWorkflowUserActor;
121
+ exports2.isWorkflowSystemActor = isWorkflowSystemActor;
122
+ exports2.workflowUserActorFromClaims = workflowUserActorFromClaims;
123
+ function isWorkflowUserActor(actor) {
124
+ return actor.ohi_uid !== void 0;
125
+ }
126
+ function isWorkflowSystemActor(actor) {
127
+ return actor.system !== void 0;
128
+ }
129
+ function workflowUserActorFromClaims(claims) {
130
+ if (claims.ohi_tid === void 0 || claims.ohi_wid === void 0) {
131
+ 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.");
132
+ }
133
+ return {
134
+ ohi_tid: claims.ohi_tid,
135
+ ohi_wid: claims.ohi_wid,
136
+ ohi_uid: claims.ohi_uid,
137
+ ohi_uname: claims.ohi_uname
138
+ };
139
+ }
140
+ var MissingActorContextError = class extends Error {
141
+ /** @param message - human-readable description of the missing claim. */
142
+ constructor(message) {
143
+ super(message);
144
+ this.name = "MissingActorContextError";
145
+ }
146
+ };
147
+ exports2.MissingActorContextError = MissingActorContextError;
148
+ }
149
+ });
150
+
151
+ // ../workflows/lib/sources.js
152
+ var require_sources = __commonJS({
153
+ "../workflows/lib/sources.js"(exports2) {
154
+ "use strict";
155
+ Object.defineProperty(exports2, "__esModule", { value: true });
156
+ exports2.DEFAULT_BUS_NAME_BY_SOURCE = exports2.OPENHI_OPS_SOURCE = exports2.OPENHI_DATA_SOURCE = exports2.OPENHI_CONTROL_SOURCE = void 0;
157
+ exports2.OPENHI_CONTROL_SOURCE = "openhi.control";
158
+ exports2.OPENHI_DATA_SOURCE = "openhi.data";
159
+ exports2.OPENHI_OPS_SOURCE = "openhi.ops";
160
+ exports2.DEFAULT_BUS_NAME_BY_SOURCE = {
161
+ [exports2.OPENHI_CONTROL_SOURCE]: "openhi-control-event-bus",
162
+ [exports2.OPENHI_DATA_SOURCE]: "openhi-data-event-bus",
163
+ [exports2.OPENHI_OPS_SOURCE]: "openhi-ops-event-bus"
164
+ };
165
+ }
166
+ });
167
+
168
+ // ../workflows/lib/detail-types/registry.js
169
+ var require_registry = __commonJS({
170
+ "../workflows/lib/detail-types/registry.js"(exports2) {
171
+ "use strict";
172
+ Object.defineProperty(exports2, "__esModule", { value: true });
173
+ exports2.InvalidDetailTypeRegistrationError = void 0;
174
+ exports2.defineDetailType = defineDetailType;
175
+ exports2.isWellFormedDetailType = isWellFormedDetailType;
176
+ function defineDetailType(entry) {
177
+ if (!isWellFormedDetailType(entry.detailType)) {
178
+ throw new InvalidDetailTypeRegistrationError(`Detail-type "${entry.detailType}" does not match the platform-wide format <area>.<event>.v<integer>. See TR-016 \xA7Open Items #2.`);
179
+ }
180
+ return entry;
181
+ }
182
+ var DETAIL_TYPE_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*\.[a-z0-9]+(?:-[a-z0-9]+)*\.v\d+$/;
183
+ function isWellFormedDetailType(detailType) {
184
+ return DETAIL_TYPE_PATTERN.test(detailType);
185
+ }
186
+ var InvalidDetailTypeRegistrationError = class extends Error {
187
+ /** @param message - human-readable description of the violation. */
188
+ constructor(message) {
189
+ super(message);
190
+ this.name = "InvalidDetailTypeRegistrationError";
191
+ }
192
+ };
193
+ exports2.InvalidDetailTypeRegistrationError = InvalidDetailTypeRegistrationError;
194
+ }
195
+ });
196
+
197
+ // ../workflows/lib/detail-types/platform.js
198
+ var require_platform = __commonJS({
199
+ "../workflows/lib/detail-types/platform.js"(exports2) {
200
+ "use strict";
201
+ Object.defineProperty(exports2, "__esModule", { value: true });
202
+ exports2.PlatformSystemDataSeededV1 = exports2.PlatformDeploymentCompletedV1 = void 0;
203
+ var sources_1 = require_sources();
204
+ var registry_1 = require_registry();
205
+ exports2.PlatformDeploymentCompletedV1 = (0, registry_1.defineDetailType)({
206
+ detailType: "platform.deployment-completed.v1",
207
+ source: sources_1.OPENHI_CONTROL_SOURCE,
208
+ dedupRequired: true
209
+ });
210
+ exports2.PlatformSystemDataSeededV1 = (0, registry_1.defineDetailType)({
211
+ detailType: "platform.system-data-seeded.v1",
212
+ source: sources_1.OPENHI_CONTROL_SOURCE,
213
+ dedupRequired: true
214
+ });
215
+ }
216
+ });
217
+
218
+ // ../workflows/lib/detail-types/index.js
219
+ var require_detail_types = __commonJS({
220
+ "../workflows/lib/detail-types/index.js"(exports2) {
221
+ "use strict";
222
+ var __createBinding = exports2 && exports2.__createBinding || (Object.create ? (function(o, m, k, k2) {
223
+ if (k2 === void 0) k2 = k;
224
+ var desc = Object.getOwnPropertyDescriptor(m, k);
225
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
226
+ desc = { enumerable: true, get: function() {
227
+ return m[k];
228
+ } };
229
+ }
230
+ Object.defineProperty(o, k2, desc);
231
+ }) : (function(o, m, k, k2) {
232
+ if (k2 === void 0) k2 = k;
233
+ o[k2] = m[k];
234
+ }));
235
+ var __exportStar = exports2 && exports2.__exportStar || function(m, exports3) {
236
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports3, p)) __createBinding(exports3, m, p);
237
+ };
238
+ Object.defineProperty(exports2, "__esModule", { value: true });
239
+ __exportStar(require_platform(), exports2);
240
+ __exportStar(require_registry(), exports2);
241
+ }
242
+ });
243
+
244
+ // ../workflows/lib/publisher.js
245
+ var require_publisher = __commonJS({
246
+ "../workflows/lib/publisher.js"(exports2) {
247
+ "use strict";
248
+ Object.defineProperty(exports2, "__esModule", { value: true });
249
+ exports2.WorkflowPublishError = void 0;
250
+ exports2.workflowsClient = workflowsClient;
251
+ exports2.publishWorkflowEvent = publishWorkflowEvent;
252
+ var node_crypto_1 = require("crypto");
253
+ var client_eventbridge_1 = require("@aws-sdk/client-eventbridge");
254
+ var envelope_version_1 = require_envelope_version();
255
+ var sources_1 = require_sources();
256
+ function workflowsClient(bridge, options = {}) {
257
+ return {
258
+ publish: (entry, payload, ctx) => publishWorkflowEvent(bridge, entry, payload, ctx, options)
259
+ };
260
+ }
261
+ async function publishWorkflowEvent(bridge, entry, payload, ctx, options = {}) {
262
+ const eventIdGenerator = options.eventIdGenerator ?? (() => (0, node_crypto_1.randomUUID)());
263
+ const correlationIdGenerator = options.correlationIdGenerator ?? (() => (0, node_crypto_1.randomUUID)());
264
+ const now = options.now ?? (() => /* @__PURE__ */ new Date());
265
+ const envelope = {
266
+ eventId: eventIdGenerator(),
267
+ attempt: 1,
268
+ correlationId: ctx.correlationId ?? correlationIdGenerator(),
269
+ causationId: ctx.causationId ?? null,
270
+ actor: ctx.actor,
271
+ occurredAt: now().toISOString(),
272
+ envelopeVersion: envelope_version_1.ENVELOPE_VERSION,
273
+ payload
274
+ };
275
+ const busName = options.busNameByPlane?.[entry.source] ?? sources_1.DEFAULT_BUS_NAME_BY_SOURCE[entry.source];
276
+ const result = await bridge.send(new client_eventbridge_1.PutEventsCommand({
277
+ Entries: [
278
+ {
279
+ EventBusName: busName,
280
+ Source: entry.source,
281
+ DetailType: entry.detailType,
282
+ Detail: JSON.stringify(envelope)
283
+ }
284
+ ]
285
+ }));
286
+ if ((result.FailedEntryCount ?? 0) > 0) {
287
+ const first = result.Entries?.[0];
288
+ throw new WorkflowPublishError(`EventBridge rejected ${entry.detailType} publish on bus ${busName}: ${first?.ErrorCode ?? "unknown"} \u2014 ${first?.ErrorMessage ?? "no error message"}`);
289
+ }
290
+ return { eventId: envelope.eventId };
291
+ }
292
+ var WorkflowPublishError = class extends Error {
293
+ /** @param message - human-readable description of the failed publish. */
294
+ constructor(message) {
295
+ super(message);
296
+ this.name = "WorkflowPublishError";
297
+ }
298
+ };
299
+ exports2.WorkflowPublishError = WorkflowPublishError;
300
+ }
301
+ });
302
+
303
+ // ../workflows/lib/consumer.js
304
+ var require_consumer = __commonJS({
305
+ "../workflows/lib/consumer.js"(exports2) {
306
+ "use strict";
307
+ Object.defineProperty(exports2, "__esModule", { value: true });
308
+ exports2.UnsupportedEnvelopeVersionError = exports2.InvalidWorkflowEventError = void 0;
309
+ exports2.parseWorkflowEvent = parseWorkflowEvent2;
310
+ var envelope_version_1 = require_envelope_version();
311
+ function parseWorkflowEvent2(event, expected) {
312
+ if (event.source !== expected.source) {
313
+ throw new InvalidWorkflowEventError(`EventBridge source "${event.source}" does not match expected detail-type's source "${expected.source}".`);
314
+ }
315
+ if (event["detail-type"] !== expected.detailType) {
316
+ throw new InvalidWorkflowEventError(`EventBridge detail-type "${event["detail-type"]}" does not match expected "${expected.detailType}".`);
317
+ }
318
+ const candidate = asEnvelopeCandidate(event.detail);
319
+ if (!(0, envelope_version_1.isSupportedEnvelopeVersion)(candidate.envelopeVersion)) {
320
+ throw new UnsupportedEnvelopeVersionError(`Envelope version "${candidate.envelopeVersion}" is outside the SDK's supported range.`);
321
+ }
322
+ const envelope = {
323
+ eventId: candidate.eventId,
324
+ attempt: candidate.attempt,
325
+ correlationId: candidate.correlationId,
326
+ causationId: candidate.causationId,
327
+ actor: candidate.actor,
328
+ occurredAt: candidate.occurredAt,
329
+ envelopeVersion: candidate.envelopeVersion,
330
+ payload: candidate.payload
331
+ };
332
+ return {
333
+ envelope,
334
+ dedupKey: { eventId: envelope.eventId, attempt: envelope.attempt }
335
+ };
336
+ }
337
+ function asEnvelopeCandidate(detail) {
338
+ if (detail === null || typeof detail !== "object") {
339
+ throw new InvalidWorkflowEventError("EventBridge detail is not a non-null object.");
340
+ }
341
+ const obj = detail;
342
+ assertString(obj, "eventId");
343
+ assertPositiveInteger(obj, "attempt");
344
+ assertString(obj, "correlationId");
345
+ assertCausationId(obj);
346
+ assertActor(obj);
347
+ assertString(obj, "occurredAt");
348
+ assertString(obj, "envelopeVersion");
349
+ if (!("payload" in obj)) {
350
+ throw new InvalidWorkflowEventError("Envelope is missing required field: payload.");
351
+ }
352
+ return obj;
353
+ }
354
+ function assertString(obj, field) {
355
+ const value = obj[field];
356
+ if (typeof value !== "string" || value.length === 0) {
357
+ throw new InvalidWorkflowEventError(`Envelope field "${field}" must be a non-empty string.`);
358
+ }
359
+ }
360
+ function assertPositiveInteger(obj, field) {
361
+ const value = obj[field];
362
+ if (typeof value !== "number" || !Number.isInteger(value) || value < 1) {
363
+ throw new InvalidWorkflowEventError(`Envelope field "${field}" must be a 1-indexed integer.`);
364
+ }
365
+ }
366
+ function assertCausationId(obj) {
367
+ if (!("causationId" in obj)) {
368
+ throw new InvalidWorkflowEventError("Envelope is missing required field: causationId.");
369
+ }
370
+ const value = obj.causationId;
371
+ if (value !== null && (typeof value !== "string" || value.length === 0)) {
372
+ throw new InvalidWorkflowEventError('Envelope field "causationId" must be a non-empty string or null.');
373
+ }
374
+ }
375
+ function assertActor(obj) {
376
+ const actor = obj.actor;
377
+ if (actor === null || typeof actor !== "object") {
378
+ throw new InvalidWorkflowEventError('Envelope field "actor" must be an object.');
379
+ }
380
+ const actorObj = actor;
381
+ const isUserActor = typeof actorObj.ohi_uid === "string" && typeof actorObj.ohi_uname === "string" && typeof actorObj.ohi_tid === "string" && typeof actorObj.ohi_wid === "string";
382
+ const isSystemActor = typeof actorObj.system === "string";
383
+ if (!isUserActor && !isSystemActor) {
384
+ 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 }).');
385
+ }
386
+ }
387
+ var InvalidWorkflowEventError = class extends Error {
388
+ /** @param message - human-readable description of the validation failure. */
389
+ constructor(message) {
390
+ super(message);
391
+ this.name = "InvalidWorkflowEventError";
392
+ }
393
+ };
394
+ exports2.InvalidWorkflowEventError = InvalidWorkflowEventError;
395
+ var UnsupportedEnvelopeVersionError = class extends Error {
396
+ /** @param message - human-readable description of the unsupported version. */
397
+ constructor(message) {
398
+ super(message);
399
+ this.name = "UnsupportedEnvelopeVersionError";
400
+ }
401
+ };
402
+ exports2.UnsupportedEnvelopeVersionError = UnsupportedEnvelopeVersionError;
403
+ }
404
+ });
405
+
406
+ // ../workflows/lib/dedup/env.js
407
+ var require_env = __commonJS({
408
+ "../workflows/lib/dedup/env.js"(exports2) {
409
+ "use strict";
410
+ Object.defineProperty(exports2, "__esModule", { value: true });
411
+ exports2.WORKFLOW_DEDUP_MAX_CONSUMER_NAME_LENGTH = exports2.WORKFLOW_DEDUP_DEFAULT_TTL_SECONDS = exports2.WORKFLOW_DEDUP_TABLE_NAME_ENV_VAR = void 0;
412
+ exports2.WORKFLOW_DEDUP_TABLE_NAME_ENV_VAR = "OPENHI_WORKFLOW_DEDUP_TABLE_NAME";
413
+ exports2.WORKFLOW_DEDUP_DEFAULT_TTL_SECONDS = 14 * 24 * 60 * 60;
414
+ exports2.WORKFLOW_DEDUP_MAX_CONSUMER_NAME_LENGTH = 64;
415
+ }
416
+ });
417
+
418
+ // ../workflows/lib/dedup/workflow-dedup-client.js
419
+ var require_workflow_dedup_client = __commonJS({
420
+ "../workflows/lib/dedup/workflow-dedup-client.js"(exports2) {
421
+ "use strict";
422
+ Object.defineProperty(exports2, "__esModule", { value: true });
423
+ exports2.WorkflowDedupInvalidInputError = exports2.WorkflowDedupTableNameMissingError = void 0;
424
+ exports2.workflowDedupClient = workflowDedupClient2;
425
+ exports2.recordIfAbsent = recordIfAbsent;
426
+ exports2.markFailed = markFailed;
427
+ exports2.encodeSortKey = encodeSortKey;
428
+ var client_dynamodb_1 = require("@aws-sdk/client-dynamodb");
429
+ var env_1 = require_env();
430
+ function workflowDedupClient2(dynamodb, options = {}) {
431
+ return {
432
+ recordIfAbsent: (input) => recordIfAbsent(dynamodb, input, options),
433
+ markFailed: (input) => markFailed(dynamodb, input, options)
434
+ };
435
+ }
436
+ async function recordIfAbsent(dynamodb, input, options = {}) {
437
+ assertConsumerName(input.consumerName);
438
+ assertPositiveInteger(input.attempt, "attempt");
439
+ const ttlSeconds = input.ttlSeconds ?? options.defaultTtlSeconds ?? env_1.WORKFLOW_DEDUP_DEFAULT_TTL_SECONDS;
440
+ if (!Number.isInteger(ttlSeconds) || ttlSeconds <= 0) {
441
+ throw new WorkflowDedupInvalidInputError(`ttlSeconds must be a positive integer; got ${ttlSeconds}.`);
442
+ }
443
+ const tableName = resolveTableName(options.tableName);
444
+ const now = (options.now ?? defaultNow)();
445
+ const sk = encodeSortKey(input.eventId, input.attempt);
446
+ const expiresAt = Math.floor(now.getTime() / 1e3) + ttlSeconds;
447
+ try {
448
+ await dynamodb.send(new client_dynamodb_1.PutItemCommand({
449
+ TableName: tableName,
450
+ Item: {
451
+ consumerName: { S: input.consumerName },
452
+ sk: { S: sk },
453
+ eventId: { S: input.eventId },
454
+ attempt: { N: String(input.attempt) },
455
+ recordedAt: { S: now.toISOString() },
456
+ expiresAt: { N: String(expiresAt) }
457
+ },
458
+ ConditionExpression: "attribute_not_exists(consumerName) AND attribute_not_exists(sk)"
459
+ }));
460
+ return { recorded: true };
461
+ } catch (err) {
462
+ if (err instanceof client_dynamodb_1.ConditionalCheckFailedException) {
463
+ return { recorded: false, alreadyProcessed: true };
464
+ }
465
+ throw err;
466
+ }
467
+ }
468
+ async function markFailed(dynamodb, input, options = {}) {
469
+ assertConsumerName(input.consumerName);
470
+ assertPositiveInteger(input.attempt, "attempt");
471
+ if (input.reason.length === 0) {
472
+ throw new WorkflowDedupInvalidInputError("reason must be non-empty.");
473
+ }
474
+ const tableName = resolveTableName(options.tableName);
475
+ const now = (options.now ?? defaultNow)();
476
+ const sk = encodeSortKey(input.eventId, input.attempt);
477
+ await dynamodb.send(new client_dynamodb_1.UpdateItemCommand({
478
+ TableName: tableName,
479
+ Key: {
480
+ consumerName: { S: input.consumerName },
481
+ sk: { S: sk }
482
+ },
483
+ UpdateExpression: "SET #failed = :failed, #failureReason = :reason, #failedAt = :failedAt",
484
+ ExpressionAttributeNames: {
485
+ "#failed": "failed",
486
+ "#failureReason": "failureReason",
487
+ "#failedAt": "failedAt"
488
+ },
489
+ ExpressionAttributeValues: {
490
+ ":failed": { BOOL: true },
491
+ ":reason": { S: input.reason },
492
+ ":failedAt": { S: now.toISOString() }
493
+ }
494
+ }));
495
+ }
496
+ function encodeSortKey(eventId, attempt) {
497
+ if (eventId.length === 0) {
498
+ throw new WorkflowDedupInvalidInputError("eventId must be non-empty.");
499
+ }
500
+ return `${eventId}#${attempt}`;
501
+ }
502
+ function resolveTableName(explicit) {
503
+ const name = explicit ?? process.env[env_1.WORKFLOW_DEDUP_TABLE_NAME_ENV_VAR];
504
+ if (!name) {
505
+ throw new WorkflowDedupTableNameMissingError(`Workflow dedup table name not set. Pass options.tableName or set ${env_1.WORKFLOW_DEDUP_TABLE_NAME_ENV_VAR}.`);
506
+ }
507
+ return name;
508
+ }
509
+ function assertConsumerName(consumerName) {
510
+ if (consumerName.length === 0) {
511
+ throw new WorkflowDedupInvalidInputError("consumerName must be non-empty.");
512
+ }
513
+ if (consumerName.length > env_1.WORKFLOW_DEDUP_MAX_CONSUMER_NAME_LENGTH) {
514
+ throw new WorkflowDedupInvalidInputError(`consumerName must be \u2264${env_1.WORKFLOW_DEDUP_MAX_CONSUMER_NAME_LENGTH} chars; got ${consumerName.length}.`);
515
+ }
516
+ if (/\s/.test(consumerName)) {
517
+ throw new WorkflowDedupInvalidInputError("consumerName must not contain whitespace.");
518
+ }
519
+ }
520
+ function assertPositiveInteger(value, field) {
521
+ if (!Number.isInteger(value) || value < 1) {
522
+ throw new WorkflowDedupInvalidInputError(`${field} must be a 1-indexed integer; got ${value}.`);
523
+ }
524
+ }
525
+ function defaultNow() {
526
+ return /* @__PURE__ */ new Date();
527
+ }
528
+ var WorkflowDedupTableNameMissingError = class extends Error {
529
+ /** @param message - human-readable description. */
530
+ constructor(message) {
531
+ super(message);
532
+ this.name = "WorkflowDedupTableNameMissingError";
533
+ }
534
+ };
535
+ exports2.WorkflowDedupTableNameMissingError = WorkflowDedupTableNameMissingError;
536
+ var WorkflowDedupInvalidInputError = class extends Error {
537
+ /** @param message - human-readable description. */
538
+ constructor(message) {
539
+ super(message);
540
+ this.name = "WorkflowDedupInvalidInputError";
541
+ }
542
+ };
543
+ exports2.WorkflowDedupInvalidInputError = WorkflowDedupInvalidInputError;
544
+ }
545
+ });
546
+
547
+ // ../workflows/lib/dedup/index.js
548
+ var require_dedup = __commonJS({
549
+ "../workflows/lib/dedup/index.js"(exports2) {
550
+ "use strict";
551
+ Object.defineProperty(exports2, "__esModule", { value: true });
552
+ 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;
553
+ var env_1 = require_env();
554
+ Object.defineProperty(exports2, "WORKFLOW_DEDUP_DEFAULT_TTL_SECONDS", { enumerable: true, get: function() {
555
+ return env_1.WORKFLOW_DEDUP_DEFAULT_TTL_SECONDS;
556
+ } });
557
+ Object.defineProperty(exports2, "WORKFLOW_DEDUP_MAX_CONSUMER_NAME_LENGTH", { enumerable: true, get: function() {
558
+ return env_1.WORKFLOW_DEDUP_MAX_CONSUMER_NAME_LENGTH;
559
+ } });
560
+ Object.defineProperty(exports2, "WORKFLOW_DEDUP_TABLE_NAME_ENV_VAR", { enumerable: true, get: function() {
561
+ return env_1.WORKFLOW_DEDUP_TABLE_NAME_ENV_VAR;
562
+ } });
563
+ var workflow_dedup_client_1 = require_workflow_dedup_client();
564
+ Object.defineProperty(exports2, "WorkflowDedupInvalidInputError", { enumerable: true, get: function() {
565
+ return workflow_dedup_client_1.WorkflowDedupInvalidInputError;
566
+ } });
567
+ Object.defineProperty(exports2, "WorkflowDedupTableNameMissingError", { enumerable: true, get: function() {
568
+ return workflow_dedup_client_1.WorkflowDedupTableNameMissingError;
569
+ } });
570
+ Object.defineProperty(exports2, "encodeSortKey", { enumerable: true, get: function() {
571
+ return workflow_dedup_client_1.encodeSortKey;
572
+ } });
573
+ Object.defineProperty(exports2, "markFailed", { enumerable: true, get: function() {
574
+ return workflow_dedup_client_1.markFailed;
575
+ } });
576
+ Object.defineProperty(exports2, "recordIfAbsent", { enumerable: true, get: function() {
577
+ return workflow_dedup_client_1.recordIfAbsent;
578
+ } });
579
+ Object.defineProperty(exports2, "workflowDedupClient", { enumerable: true, get: function() {
580
+ return workflow_dedup_client_1.workflowDedupClient;
581
+ } });
582
+ }
583
+ });
584
+
585
+ // ../workflows/lib/index.js
586
+ var require_lib2 = __commonJS({
587
+ "../workflows/lib/index.js"(exports2) {
588
+ "use strict";
589
+ Object.defineProperty(exports2, "__esModule", { value: true });
590
+ exports2.workflowDedupClient = exports2.recordIfAbsent = exports2.markFailed = exports2.encodeSortKey = exports2.WorkflowDedupTableNameMissingError = exports2.WorkflowDedupInvalidInputError = exports2.WORKFLOW_DEDUP_TABLE_NAME_ENV_VAR = exports2.WORKFLOW_DEDUP_MAX_CONSUMER_NAME_LENGTH = exports2.WORKFLOW_DEDUP_DEFAULT_TTL_SECONDS = exports2.parseWorkflowEvent = exports2.UnsupportedEnvelopeVersionError = exports2.InvalidWorkflowEventError = exports2.workflowsClient = exports2.publishWorkflowEvent = exports2.WorkflowPublishError = exports2.isWellFormedDetailType = exports2.defineDetailType = exports2.PlatformSystemDataSeededV1 = exports2.PlatformDeploymentCompletedV1 = exports2.InvalidDetailTypeRegistrationError = exports2.OPENHI_OPS_SOURCE = exports2.OPENHI_DATA_SOURCE = exports2.OPENHI_CONTROL_SOURCE = exports2.DEFAULT_BUS_NAME_BY_SOURCE = exports2.workflowUserActorFromClaims = exports2.isWorkflowUserActor = exports2.isWorkflowSystemActor = exports2.MissingActorContextError = exports2.isSupportedEnvelopeVersion = exports2.ENVELOPE_VERSION = void 0;
591
+ var envelope_version_1 = require_envelope_version();
592
+ Object.defineProperty(exports2, "ENVELOPE_VERSION", { enumerable: true, get: function() {
593
+ return envelope_version_1.ENVELOPE_VERSION;
594
+ } });
595
+ Object.defineProperty(exports2, "isSupportedEnvelopeVersion", { enumerable: true, get: function() {
596
+ return envelope_version_1.isSupportedEnvelopeVersion;
597
+ } });
598
+ var envelope_1 = require_envelope();
599
+ Object.defineProperty(exports2, "MissingActorContextError", { enumerable: true, get: function() {
600
+ return envelope_1.MissingActorContextError;
601
+ } });
602
+ Object.defineProperty(exports2, "isWorkflowSystemActor", { enumerable: true, get: function() {
603
+ return envelope_1.isWorkflowSystemActor;
604
+ } });
605
+ Object.defineProperty(exports2, "isWorkflowUserActor", { enumerable: true, get: function() {
606
+ return envelope_1.isWorkflowUserActor;
607
+ } });
608
+ Object.defineProperty(exports2, "workflowUserActorFromClaims", { enumerable: true, get: function() {
609
+ return envelope_1.workflowUserActorFromClaims;
610
+ } });
611
+ var sources_1 = require_sources();
612
+ Object.defineProperty(exports2, "DEFAULT_BUS_NAME_BY_SOURCE", { enumerable: true, get: function() {
613
+ return sources_1.DEFAULT_BUS_NAME_BY_SOURCE;
614
+ } });
615
+ Object.defineProperty(exports2, "OPENHI_CONTROL_SOURCE", { enumerable: true, get: function() {
616
+ return sources_1.OPENHI_CONTROL_SOURCE;
617
+ } });
618
+ Object.defineProperty(exports2, "OPENHI_DATA_SOURCE", { enumerable: true, get: function() {
619
+ return sources_1.OPENHI_DATA_SOURCE;
620
+ } });
621
+ Object.defineProperty(exports2, "OPENHI_OPS_SOURCE", { enumerable: true, get: function() {
622
+ return sources_1.OPENHI_OPS_SOURCE;
623
+ } });
624
+ var detail_types_1 = require_detail_types();
625
+ Object.defineProperty(exports2, "InvalidDetailTypeRegistrationError", { enumerable: true, get: function() {
626
+ return detail_types_1.InvalidDetailTypeRegistrationError;
627
+ } });
628
+ Object.defineProperty(exports2, "PlatformDeploymentCompletedV1", { enumerable: true, get: function() {
629
+ return detail_types_1.PlatformDeploymentCompletedV1;
630
+ } });
631
+ Object.defineProperty(exports2, "PlatformSystemDataSeededV1", { enumerable: true, get: function() {
632
+ return detail_types_1.PlatformSystemDataSeededV1;
633
+ } });
634
+ Object.defineProperty(exports2, "defineDetailType", { enumerable: true, get: function() {
635
+ return detail_types_1.defineDetailType;
636
+ } });
637
+ Object.defineProperty(exports2, "isWellFormedDetailType", { enumerable: true, get: function() {
638
+ return detail_types_1.isWellFormedDetailType;
639
+ } });
640
+ var publisher_1 = require_publisher();
641
+ Object.defineProperty(exports2, "WorkflowPublishError", { enumerable: true, get: function() {
642
+ return publisher_1.WorkflowPublishError;
643
+ } });
644
+ Object.defineProperty(exports2, "publishWorkflowEvent", { enumerable: true, get: function() {
645
+ return publisher_1.publishWorkflowEvent;
646
+ } });
647
+ Object.defineProperty(exports2, "workflowsClient", { enumerable: true, get: function() {
648
+ return publisher_1.workflowsClient;
649
+ } });
650
+ var consumer_1 = require_consumer();
651
+ Object.defineProperty(exports2, "InvalidWorkflowEventError", { enumerable: true, get: function() {
652
+ return consumer_1.InvalidWorkflowEventError;
653
+ } });
654
+ Object.defineProperty(exports2, "UnsupportedEnvelopeVersionError", { enumerable: true, get: function() {
655
+ return consumer_1.UnsupportedEnvelopeVersionError;
656
+ } });
657
+ Object.defineProperty(exports2, "parseWorkflowEvent", { enumerable: true, get: function() {
658
+ return consumer_1.parseWorkflowEvent;
659
+ } });
660
+ var dedup_1 = require_dedup();
661
+ Object.defineProperty(exports2, "WORKFLOW_DEDUP_DEFAULT_TTL_SECONDS", { enumerable: true, get: function() {
662
+ return dedup_1.WORKFLOW_DEDUP_DEFAULT_TTL_SECONDS;
663
+ } });
664
+ Object.defineProperty(exports2, "WORKFLOW_DEDUP_MAX_CONSUMER_NAME_LENGTH", { enumerable: true, get: function() {
665
+ return dedup_1.WORKFLOW_DEDUP_MAX_CONSUMER_NAME_LENGTH;
666
+ } });
667
+ Object.defineProperty(exports2, "WORKFLOW_DEDUP_TABLE_NAME_ENV_VAR", { enumerable: true, get: function() {
668
+ return dedup_1.WORKFLOW_DEDUP_TABLE_NAME_ENV_VAR;
669
+ } });
670
+ Object.defineProperty(exports2, "WorkflowDedupInvalidInputError", { enumerable: true, get: function() {
671
+ return dedup_1.WorkflowDedupInvalidInputError;
672
+ } });
673
+ Object.defineProperty(exports2, "WorkflowDedupTableNameMissingError", { enumerable: true, get: function() {
674
+ return dedup_1.WorkflowDedupTableNameMissingError;
675
+ } });
676
+ Object.defineProperty(exports2, "encodeSortKey", { enumerable: true, get: function() {
677
+ return dedup_1.encodeSortKey;
678
+ } });
679
+ Object.defineProperty(exports2, "markFailed", { enumerable: true, get: function() {
680
+ return dedup_1.markFailed;
681
+ } });
682
+ Object.defineProperty(exports2, "recordIfAbsent", { enumerable: true, get: function() {
683
+ return dedup_1.recordIfAbsent;
684
+ } });
685
+ Object.defineProperty(exports2, "workflowDedupClient", { enumerable: true, get: function() {
686
+ return dedup_1.workflowDedupClient;
687
+ } });
688
+ }
689
+ });
690
+
93
691
  // src/index.ts
94
692
  var src_exports = {};
95
693
  __export(src_exports, {
694
+ BRIDGED_STATUSES: () => BRIDGED_STATUSES,
695
+ CLOUDFORMATION_EVENT_SOURCE: () => CLOUDFORMATION_EVENT_SOURCE,
696
+ CLOUDFORMATION_STACK_STATUS_CHANGE_DETAIL_TYPE: () => CLOUDFORMATION_STACK_STATUS_CHANGE_DETAIL_TYPE,
697
+ CONTROL_EVENT_BUS_NAME_ENV_VAR: () => CONTROL_EVENT_BUS_NAME_ENV_VAR,
96
698
  ChildHostedZone: () => ChildHostedZone,
97
699
  CognitoFixtureSeederClient: () => CognitoFixtureSeederClient,
98
700
  CognitoUserPool: () => CognitoUserPool,
99
701
  CognitoUserPoolClient: () => CognitoUserPoolClient,
100
702
  CognitoUserPoolDomain: () => CognitoUserPoolDomain,
101
703
  CognitoUserPoolKmsKey: () => CognitoUserPoolKmsKey,
704
+ ControlEventBus: () => ControlEventBus,
102
705
  DATA_STORE_CHANGE_DETAIL_MAX_UTF8_BYTES: () => DATA_STORE_CHANGE_DETAIL_MAX_UTF8_BYTES,
103
706
  DATA_STORE_CHANGE_DETAIL_TYPE: () => DATA_STORE_CHANGE_DETAIL_TYPE,
104
707
  DATA_STORE_CHANGE_EVENT_SOURCE: () => DATA_STORE_CHANGE_EVENT_SOURCE,
708
+ DEMO_PERIOD: () => DEMO_PERIOD,
709
+ DEMO_TENANT_SPECS: () => DEMO_TENANT_SPECS,
710
+ DEMO_URN_SYSTEM: () => DEMO_URN_SYSTEM,
711
+ DEV_USERS: () => DEV_USERS,
105
712
  DataEventBus: () => DataEventBus,
106
713
  DataStoreHistoricalArchive: () => DataStoreHistoricalArchive,
107
714
  DataStorePostgresReplica: () => DataStorePostgresReplica,
108
715
  DiscoverableStringParameter: () => DiscoverableStringParameter,
109
716
  DynamoDbDataStore: () => DynamoDbDataStore,
717
+ OPENHI_REPO_TAG_KEY_ENV_VAR: () => OPENHI_REPO_TAG_KEY_ENV_VAR,
718
+ OPENHI_RESOURCE_URN_SYSTEM: () => OPENHI_RESOURCE_URN_SYSTEM,
719
+ OPENHI_TAG_KEY_PREFIX_ENV_VAR: () => OPENHI_TAG_KEY_PREFIX_ENV_VAR,
720
+ OPENHI_TAG_SUFFIX_BRANCH_NAME: () => OPENHI_TAG_SUFFIX_BRANCH_NAME,
721
+ OPENHI_TAG_SUFFIX_REPO_NAME: () => OPENHI_TAG_SUFFIX_REPO_NAME,
722
+ OPENHI_TAG_SUFFIX_SERVICE_TYPE: () => OPENHI_TAG_SUFFIX_SERVICE_TYPE,
723
+ OPENHI_TAG_SUFFIX_STAGE_TYPE: () => OPENHI_TAG_SUFFIX_STAGE_TYPE,
110
724
  OpenHiApp: () => OpenHiApp,
111
725
  OpenHiAuthService: () => OpenHiAuthService,
112
726
  OpenHiDataService: () => OpenHiDataService,
@@ -117,22 +731,60 @@ __export(src_exports, {
117
731
  OpenHiService: () => OpenHiService,
118
732
  OpenHiStage: () => OpenHiStage,
119
733
  OpsEventBus: () => OpsEventBus,
734
+ PLACEHOLDER_TENANT_ID: () => PLACEHOLDER_TENANT_ID,
735
+ PLACEHOLDER_WORKSPACE_ID: () => PLACEHOLDER_WORKSPACE_ID,
736
+ PLATFORM_DEPLOY_BRIDGE_ACTOR_SYSTEM: () => PLATFORM_DEPLOY_BRIDGE_ACTOR_SYSTEM,
737
+ PLATFORM_SCOPE_TENANT_ID: () => PLATFORM_SCOPE_TENANT_ID,
120
738
  POSTGRES_REPLICA_CLUSTER_ARN_SSM_NAME: () => POSTGRES_REPLICA_CLUSTER_ARN_SSM_NAME,
121
739
  POSTGRES_REPLICA_DATABASE_NAME_SSM_NAME: () => POSTGRES_REPLICA_DATABASE_NAME_SSM_NAME,
122
740
  POSTGRES_REPLICA_SECRET_ARN_SSM_NAME: () => POSTGRES_REPLICA_SECRET_ARN_SSM_NAME,
741
+ PROVISION_DEFAULT_WORKSPACE_DETAIL_TYPE: () => PROVISION_DEFAULT_WORKSPACE_DETAIL_TYPE,
742
+ PlatformDeployBridge: () => PlatformDeployBridge,
743
+ PlatformDeployBridgeLambda: () => PlatformDeployBridgeLambda,
744
+ PlatformDeploymentCompletedV1: () => import_workflows4.PlatformDeploymentCompletedV1,
123
745
  PostAuthenticationLambda: () => PostAuthenticationLambda,
124
746
  PostConfirmationLambda: () => PostConfirmationLambda,
125
747
  PreTokenGenerationLambda: () => PreTokenGenerationLambda,
748
+ ProvisionDefaultWorkspaceLambda: () => ProvisionDefaultWorkspaceLambda,
126
749
  REST_API_BASE_URL_SSM_NAME: () => REST_API_BASE_URL_SSM_NAME,
127
750
  RootGraphqlApi: () => RootGraphqlApi,
128
751
  RootHostedZone: () => RootHostedZone,
129
752
  RootHttpApi: () => RootHttpApi,
130
753
  RootWildcardCertificate: () => RootWildcardCertificate,
754
+ SEED_DEMO_DATA_CONSUMER_NAME: () => SEED_DEMO_DATA_CONSUMER_NAME,
755
+ SEED_SYSTEM_DATA_ACTOR_SYSTEM: () => SEED_SYSTEM_DATA_ACTOR_SYSTEM,
756
+ SEED_SYSTEM_DATA_CONSUMER_NAME: () => SEED_SYSTEM_DATA_CONSUMER_NAME,
757
+ SEED_SYSTEM_DATA_CONTROL_BUS_ENV_VAR: () => SEED_SYSTEM_DATA_CONTROL_BUS_ENV_VAR,
131
758
  STATIC_HOSTING_SERVICE_TYPE: () => STATIC_HOSTING_SERVICE_TYPE,
759
+ SeedDemoDataLambda: () => SeedDemoDataLambda,
760
+ SeedDemoDataWorkflow: () => SeedDemoDataWorkflow,
761
+ SeedSystemDataLambda: () => SeedSystemDataLambda,
762
+ SeedSystemDataWorkflow: () => SeedSystemDataWorkflow,
132
763
  StaticHosting: () => StaticHosting,
764
+ USER_ONBOARDING_EVENT_SOURCE: () => USER_ONBOARDING_EVENT_SOURCE,
765
+ UserOnboardingWorkflow: () => UserOnboardingWorkflow,
766
+ WorkflowDedupConsumerNameInvalidError: () => WorkflowDedupConsumerNameInvalidError,
767
+ WorkflowDedupTable: () => WorkflowDedupTable,
768
+ WorkflowDedupTableDuplicateError: () => WorkflowDedupTableDuplicateError,
133
769
  buildFhirCurrentResourceChangeDetail: () => buildFhirCurrentResourceChangeDetail,
770
+ buildProvisionDefaultWorkspaceRequestedDetail: () => buildProvisionDefaultWorkspaceRequestedDetail,
771
+ demoBasePartitionKeys: () => demoBasePartitionKeys,
772
+ demoDevUserPartitionKeys: () => demoDevUserPartitionKeys,
773
+ demoMembershipId: () => demoMembershipId,
774
+ demoMembershipPartitionKey: () => demoMembershipPartitionKey,
775
+ demoRoleAssignmentId: () => demoRoleAssignmentId,
776
+ demoRoleAssignmentPartitionKey: () => demoRoleAssignmentPartitionKey,
777
+ demoRolesForUserInTenant: () => demoRolesForUserInTenant,
778
+ demoScenarioIdentifier: () => demoScenarioIdentifier,
779
+ demoTenantPartitionKey: () => demoTenantPartitionKey,
780
+ demoUserPartitionKey: () => demoUserPartitionKey,
781
+ demoWorkspacePartitionKey: () => demoWorkspacePartitionKey,
134
782
  getDynamoDbDataStoreTableName: () => getDynamoDbDataStoreTableName,
135
- getPostgresReplicaSchemaName: () => getPostgresReplicaSchemaName
783
+ getPostgresReplicaSchemaName: () => getPostgresReplicaSchemaName,
784
+ getWorkflowDedupTableName: () => getWorkflowDedupTableName,
785
+ openHiTagKey: () => openHiTagKey,
786
+ openhiResourceIdentifier: () => openhiResourceIdentifier,
787
+ rolePartitionKey: () => rolePartitionKey
136
788
  });
137
789
  module.exports = __toCommonJS(src_exports);
138
790
 
@@ -347,6 +999,11 @@ var import_utils = require("@codedrifters/utils");
347
999
  var import_config3 = __toESM(require_lib());
348
1000
  var import_aws_cdk_lib4 = require("aws-cdk-lib");
349
1001
  var import_change_case = require("change-case");
1002
+ var OPENHI_TAG_SUFFIX_REPO_NAME = "repo-name";
1003
+ var OPENHI_TAG_SUFFIX_BRANCH_NAME = "branch-name";
1004
+ var OPENHI_TAG_SUFFIX_SERVICE_TYPE = "service-type";
1005
+ var OPENHI_TAG_SUFFIX_STAGE_TYPE = "stage-type";
1006
+ var openHiTagKey = (appName, suffix) => `${appName}:${suffix}`;
350
1007
  var OpenHiService = class extends import_aws_cdk_lib4.Stack {
351
1008
  /**
352
1009
  * Creates a new OpenHI service stack.
@@ -414,11 +1071,20 @@ var OpenHiService = class extends import_aws_cdk_lib4.Stack {
414
1071
  `availability-zones:account=${account}:region=${region}`,
415
1072
  [`${region}a`, `${region}b`, `${region}c`]
416
1073
  );
417
- import_aws_cdk_lib4.Tags.of(this).add(`${appName}:repo-name`, repoName.slice(0, 255));
418
- import_aws_cdk_lib4.Tags.of(this).add(`${appName}:branch-name`, branchName.slice(0, 255));
419
- import_aws_cdk_lib4.Tags.of(this).add(`${appName}:service-type`, id.slice(0, 255));
420
1074
  import_aws_cdk_lib4.Tags.of(this).add(
421
- `${appName}:stage-type`,
1075
+ openHiTagKey(appName, OPENHI_TAG_SUFFIX_REPO_NAME),
1076
+ repoName.slice(0, 255)
1077
+ );
1078
+ import_aws_cdk_lib4.Tags.of(this).add(
1079
+ openHiTagKey(appName, OPENHI_TAG_SUFFIX_BRANCH_NAME),
1080
+ branchName.slice(0, 255)
1081
+ );
1082
+ import_aws_cdk_lib4.Tags.of(this).add(
1083
+ openHiTagKey(appName, OPENHI_TAG_SUFFIX_SERVICE_TYPE),
1084
+ id.slice(0, 255)
1085
+ );
1086
+ import_aws_cdk_lib4.Tags.of(this).add(
1087
+ openHiTagKey(appName, OPENHI_TAG_SUFFIX_STAGE_TYPE),
422
1088
  ohEnv.ohStage.stageType.slice(0, 255)
423
1089
  );
424
1090
  }
@@ -761,14 +1427,13 @@ var import_aws_lambda2 = require("aws-cdk-lib/aws-lambda");
761
1427
  var import_aws_lambda_nodejs2 = require("aws-cdk-lib/aws-lambda-nodejs");
762
1428
  var import_constructs2 = require("constructs");
763
1429
  var HANDLER_NAME2 = "post-confirmation.handler.js";
764
- function resolveHandlerEntry2(dirname) {
1430
+ var resolveHandlerEntry2 = (dirname) => {
765
1431
  const sameDir = import_node_path2.default.join(dirname, HANDLER_NAME2);
766
1432
  if (import_node_fs2.default.existsSync(sameDir)) {
767
1433
  return sameDir;
768
1434
  }
769
- const fromLib = import_node_path2.default.join(dirname, "..", "..", "..", "lib", HANDLER_NAME2);
770
- return fromLib;
771
- }
1435
+ return import_node_path2.default.join(dirname, "..", "..", "..", "lib", HANDLER_NAME2);
1436
+ };
772
1437
  var PostConfirmationLambda = class extends import_constructs2.Construct {
773
1438
  constructor(scope, props) {
774
1439
  super(scope, "post-confirmation-lambda");
@@ -777,7 +1442,7 @@ var PostConfirmationLambda = class extends import_constructs2.Construct {
777
1442
  runtime: import_aws_lambda2.Runtime.NODEJS_LATEST,
778
1443
  memorySize: 1024,
779
1444
  environment: {
780
- DYNAMO_TABLE_NAME: props.dynamoTableName
1445
+ CONTROL_EVENT_BUS_NAME: props.controlEventBusName
781
1446
  }
782
1447
  });
783
1448
  }
@@ -1105,6 +1770,204 @@ var DynamoDbDataStore = class extends import_aws_dynamodb.Table {
1105
1770
  }
1106
1771
  };
1107
1772
 
1773
+ // src/components/dynamodb/workflow-dedup-table.ts
1774
+ var import_workflows = __toESM(require_lib2());
1775
+ var import_aws_cdk_lib8 = require("aws-cdk-lib");
1776
+ var import_aws_dynamodb2 = require("aws-cdk-lib/aws-dynamodb");
1777
+ var import_aws_iam = require("aws-cdk-lib/aws-iam");
1778
+ var import_constructs5 = require("constructs");
1779
+ function getWorkflowDedupTableName(scope) {
1780
+ const stack = OpenHiService.of(scope);
1781
+ return `workflow-dedup-${stack.branchHash}`;
1782
+ }
1783
+ var _WorkflowDedupTable = class _WorkflowDedupTable extends import_constructs5.Construct {
1784
+ constructor(scope, id, props = {}) {
1785
+ super(scope, id);
1786
+ this.registeredConsumers = /* @__PURE__ */ new Set();
1787
+ const service = OpenHiService.of(scope);
1788
+ const others = service.node.findAll().filter(
1789
+ (c) => c instanceof _WorkflowDedupTable && c !== this
1790
+ );
1791
+ if (others.length > 0) {
1792
+ throw new WorkflowDedupTableDuplicateError(
1793
+ `WorkflowDedupTable already exists at ${others[0].node.path}; only one shared dedup table is allowed per service stack (TR-015).`
1794
+ );
1795
+ }
1796
+ this.table = new import_aws_dynamodb2.Table(this, "Table", {
1797
+ tableName: getWorkflowDedupTableName(scope),
1798
+ partitionKey: {
1799
+ name: "consumerName",
1800
+ type: import_aws_dynamodb2.AttributeType.STRING
1801
+ },
1802
+ sortKey: {
1803
+ name: "sk",
1804
+ type: import_aws_dynamodb2.AttributeType.STRING
1805
+ },
1806
+ billingMode: import_aws_dynamodb2.BillingMode.PAY_PER_REQUEST,
1807
+ timeToLiveAttribute: "expiresAt",
1808
+ removalPolicy: props.removalPolicy ?? service.removalPolicy
1809
+ });
1810
+ new DiscoverableStringParameter(this, "table-name-param", {
1811
+ ssmParamName: _WorkflowDedupTable.TABLE_NAME_SSM_PARAM_NAME,
1812
+ stringValue: this.table.tableName
1813
+ });
1814
+ new DiscoverableStringParameter(this, "table-arn-param", {
1815
+ ssmParamName: _WorkflowDedupTable.TABLE_ARN_SSM_PARAM_NAME,
1816
+ stringValue: this.table.tableArn
1817
+ });
1818
+ }
1819
+ /** Cross-stack lookup for the table name. */
1820
+ static tableNameFromLookup(scope) {
1821
+ return DiscoverableStringParameter.valueForLookupName(scope, {
1822
+ ssmParamName: _WorkflowDedupTable.TABLE_NAME_SSM_PARAM_NAME,
1823
+ serviceType: _WorkflowDedupTable.PUBLISHER_SERVICE_TYPE
1824
+ });
1825
+ }
1826
+ /** Cross-stack lookup for the table ARN. */
1827
+ static tableArnFromLookup(scope) {
1828
+ return DiscoverableStringParameter.valueForLookupName(scope, {
1829
+ ssmParamName: _WorkflowDedupTable.TABLE_ARN_SSM_PARAM_NAME,
1830
+ serviceType: _WorkflowDedupTable.PUBLISHER_SERVICE_TYPE
1831
+ });
1832
+ }
1833
+ /**
1834
+ * Cross-stack equivalent of {@link grantConsumer}. Use when the dedup
1835
+ * table is on a different stack than the consumer Lambda — the
1836
+ * grant resolves the table name + ARN via SSM at synth time, so the
1837
+ * consumer stack does not pick up a CloudFormation export dependency
1838
+ * on the global stack.
1839
+ *
1840
+ * Inverts the singleton-guard semantics of `grantConsumer`: there is
1841
+ * no synth-time check that the same `consumerName` was registered
1842
+ * twice across stacks. Consumer names are agreed by convention
1843
+ * (see TR-015); double-registration is operator error caught at
1844
+ * design time, not synth time.
1845
+ */
1846
+ static grantConsumerFromLookup(scope, fn, consumerName, options = {}) {
1847
+ _WorkflowDedupTable.assertConsumerNameStatic(consumerName);
1848
+ const tableName = _WorkflowDedupTable.tableNameFromLookup(scope);
1849
+ const tableArn = _WorkflowDedupTable.tableArnFromLookup(scope);
1850
+ fn.addEnvironment(import_workflows.WORKFLOW_DEDUP_TABLE_NAME_ENV_VAR, tableName);
1851
+ if (options.defaultTtlSeconds !== void 0) {
1852
+ fn.addEnvironment(
1853
+ "OPENHI_WORKFLOW_DEDUP_DEFAULT_TTL_SECONDS",
1854
+ String(options.defaultTtlSeconds)
1855
+ );
1856
+ }
1857
+ fn.addToRolePolicy(
1858
+ new import_aws_iam.PolicyStatement({
1859
+ effect: import_aws_iam.Effect.ALLOW,
1860
+ actions: [
1861
+ "dynamodb:PutItem",
1862
+ "dynamodb:UpdateItem",
1863
+ "dynamodb:GetItem",
1864
+ "dynamodb:Query"
1865
+ ],
1866
+ resources: [tableArn],
1867
+ conditions: {
1868
+ "ForAllValues:StringEquals": {
1869
+ "dynamodb:LeadingKeys": [consumerName]
1870
+ }
1871
+ }
1872
+ })
1873
+ );
1874
+ }
1875
+ /**
1876
+ * Standalone consumer-name validator shared by the instance method
1877
+ * and `grantConsumerFromLookup` so the two grants enforce identical
1878
+ * invariants.
1879
+ */
1880
+ static assertConsumerNameStatic(consumerName) {
1881
+ if (consumerName.length === 0) {
1882
+ throw new WorkflowDedupConsumerNameInvalidError(
1883
+ "consumerName must be non-empty."
1884
+ );
1885
+ }
1886
+ if (consumerName.length > import_workflows.WORKFLOW_DEDUP_MAX_CONSUMER_NAME_LENGTH) {
1887
+ throw new WorkflowDedupConsumerNameInvalidError(
1888
+ `consumerName must be at most ${import_workflows.WORKFLOW_DEDUP_MAX_CONSUMER_NAME_LENGTH} chars; got ${consumerName.length}.`
1889
+ );
1890
+ }
1891
+ if (/\s/.test(consumerName)) {
1892
+ throw new WorkflowDedupConsumerNameInvalidError(
1893
+ "consumerName must not contain whitespace."
1894
+ );
1895
+ }
1896
+ }
1897
+ /**
1898
+ * Wire a Lambda consumer to this table. Injects the table-name env var
1899
+ * so the runtime `WorkflowDedupClient` can resolve it, then attaches a
1900
+ * per-consumer IAM grant scoped by `dynamodb:LeadingKeys` so the
1901
+ * consumer can only read/write its own partition.
1902
+ */
1903
+ grantConsumer(fn, consumerName, options = {}) {
1904
+ this.assertConsumerName(consumerName);
1905
+ if (this.registeredConsumers.has(consumerName)) {
1906
+ import_aws_cdk_lib8.Annotations.of(this).addWarning(
1907
+ `WorkflowDedupTable: consumerName "${consumerName}" registered more than once; subsequent grantConsumer calls add policy statements but do not re-inject the env var.`
1908
+ );
1909
+ }
1910
+ this.registeredConsumers.add(consumerName);
1911
+ fn.addEnvironment(import_workflows.WORKFLOW_DEDUP_TABLE_NAME_ENV_VAR, this.table.tableName);
1912
+ if (options.defaultTtlSeconds !== void 0) {
1913
+ fn.addEnvironment(
1914
+ "OPENHI_WORKFLOW_DEDUP_DEFAULT_TTL_SECONDS",
1915
+ String(options.defaultTtlSeconds)
1916
+ );
1917
+ }
1918
+ fn.addToRolePolicy(
1919
+ new import_aws_iam.PolicyStatement({
1920
+ effect: import_aws_iam.Effect.ALLOW,
1921
+ actions: [
1922
+ "dynamodb:PutItem",
1923
+ "dynamodb:UpdateItem",
1924
+ "dynamodb:GetItem",
1925
+ "dynamodb:Query"
1926
+ ],
1927
+ resources: [this.table.tableArn],
1928
+ conditions: {
1929
+ "ForAllValues:StringEquals": {
1930
+ "dynamodb:LeadingKeys": [consumerName]
1931
+ }
1932
+ }
1933
+ })
1934
+ );
1935
+ }
1936
+ assertConsumerName(consumerName) {
1937
+ _WorkflowDedupTable.assertConsumerNameStatic(consumerName);
1938
+ }
1939
+ };
1940
+ /** SSM param name (short) used by `DiscoverableStringParameter` for the table name lookup. */
1941
+ _WorkflowDedupTable.TABLE_NAME_SSM_PARAM_NAME = "workflow-dedup-table-name";
1942
+ /** SSM param name (short) used by `DiscoverableStringParameter` for the table ARN lookup. */
1943
+ _WorkflowDedupTable.TABLE_ARN_SSM_PARAM_NAME = "workflow-dedup-table-arn";
1944
+ /**
1945
+ * Service-type the publishing stack runs under. The cross-stack lookups
1946
+ * pin to this value so consumer stacks on a different service-type
1947
+ * (e.g. `data`, `auth`) resolve the parameter at the publisher's SSM
1948
+ * path instead of their own. Typed against `OpenHiServiceType` so a
1949
+ * future rename of the literal triggers a compile error; not pulled
1950
+ * from `OpenHiGlobalService.SERVICE_TYPE` because
1951
+ * `OpenHiGlobalService` already imports `WorkflowDedupTable` — a
1952
+ * back-import would create a circular dependency.
1953
+ */
1954
+ _WorkflowDedupTable.PUBLISHER_SERVICE_TYPE = "global";
1955
+ var WorkflowDedupTable = _WorkflowDedupTable;
1956
+ var WorkflowDedupTableDuplicateError = class extends Error {
1957
+ /** @param message - human-readable description of the duplicate. */
1958
+ constructor(message) {
1959
+ super(message);
1960
+ this.name = "WorkflowDedupTableDuplicateError";
1961
+ }
1962
+ };
1963
+ var WorkflowDedupConsumerNameInvalidError = class extends Error {
1964
+ /** @param message - human-readable description of the invariant violation. */
1965
+ constructor(message) {
1966
+ super(message);
1967
+ this.name = "WorkflowDedupConsumerNameInvalidError";
1968
+ }
1969
+ };
1970
+
1108
1971
  // src/components/event-bridge/data-event-bus.ts
1109
1972
  var import_aws_events = require("aws-cdk-lib/aws-events");
1110
1973
  var DataEventBus = class _DataEventBus extends import_aws_events.EventBus {
@@ -1149,16 +2012,38 @@ var OpsEventBus = class _OpsEventBus extends import_aws_events2.EventBus {
1149
2012
  }
1150
2013
  };
1151
2014
 
2015
+ // src/components/event-bridge/control-event-bus.ts
2016
+ var import_aws_events3 = require("aws-cdk-lib/aws-events");
2017
+ var ControlEventBus = class _ControlEventBus extends import_aws_events3.EventBus {
2018
+ /*****************************************************************************
2019
+ *
2020
+ * Return a name for this EventBus based on the stack environment hash. This
2021
+ * name is common across all stacks since it's using the environment hash in
2022
+ * its name.
2023
+ *
2024
+ ****************************************************************************/
2025
+ static getEventBusName(scope) {
2026
+ const stack = OpenHiService.of(scope);
2027
+ return `controlv1${stack.branchHash}`;
2028
+ }
2029
+ constructor(scope, props) {
2030
+ super(scope, "control-event-bus-v1", {
2031
+ ...props,
2032
+ eventBusName: _ControlEventBus.getEventBusName(scope)
2033
+ });
2034
+ }
2035
+ };
2036
+
1152
2037
  // src/components/postgres/data-store-postgres-replica.ts
1153
2038
  var import_node_fs5 = __toESM(require("fs"));
1154
2039
  var import_node_path5 = __toESM(require("path"));
1155
- var import_aws_cdk_lib8 = require("aws-cdk-lib");
2040
+ var import_aws_cdk_lib9 = require("aws-cdk-lib");
1156
2041
  var ec2 = __toESM(require("aws-cdk-lib/aws-ec2"));
1157
2042
  var import_aws_lambda5 = require("aws-cdk-lib/aws-lambda");
1158
2043
  var import_aws_lambda_event_sources = require("aws-cdk-lib/aws-lambda-event-sources");
1159
2044
  var import_aws_lambda_nodejs5 = require("aws-cdk-lib/aws-lambda-nodejs");
1160
2045
  var rds = __toESM(require("aws-cdk-lib/aws-rds"));
1161
- var import_constructs5 = require("constructs");
2046
+ var import_constructs6 = require("constructs");
1162
2047
  var HANDLER_NAME5 = "data-store-postgres-replication.handler.js";
1163
2048
  var DEFAULT_DATABASE_NAME = "openhi";
1164
2049
  var SCHEMA_NAME_PATTERN = /^[a-z_][a-z0-9_]{0,62}$/;
@@ -1181,7 +2066,7 @@ function getPostgresReplicaSchemaName(branchHash) {
1181
2066
  }
1182
2067
  return candidate;
1183
2068
  }
1184
- var DataStorePostgresReplica = class extends import_constructs5.Construct {
2069
+ var DataStorePostgresReplica = class extends import_constructs6.Construct {
1185
2070
  /**
1186
2071
  * Resolve the cluster ARN published by an upstream {@link DataStorePostgresReplica}.
1187
2072
  * Use from any stack that needs to grant `rds-data:ExecuteStatement` against
@@ -1218,7 +2103,7 @@ var DataStorePostgresReplica = class extends import_constructs5.Construct {
1218
2103
  super(scope, id);
1219
2104
  this.databaseName = props.databaseName ?? DEFAULT_DATABASE_NAME;
1220
2105
  this.schemaName = getPostgresReplicaSchemaName(props.branchHash);
1221
- const region = import_aws_cdk_lib8.Stack.of(this).region;
2106
+ const region = import_aws_cdk_lib9.Stack.of(this).region;
1222
2107
  this.vpc = props.vpc ?? new ec2.Vpc(this, "Vpc", {
1223
2108
  availabilityZones: [`${region}a`, `${region}b`],
1224
2109
  natGateways: 0,
@@ -1254,7 +2139,7 @@ var DataStorePostgresReplica = class extends import_constructs5.Construct {
1254
2139
  entry: resolveHandlerEntry5(__dirname),
1255
2140
  runtime: import_aws_lambda5.Runtime.NODEJS_LATEST,
1256
2141
  memorySize: 512,
1257
- timeout: import_aws_cdk_lib8.Duration.minutes(1),
2142
+ timeout: import_aws_cdk_lib9.Duration.minutes(1),
1258
2143
  vpc: this.vpc,
1259
2144
  vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE_ISOLATED },
1260
2145
  description: "Replicates DynamoDB current-resource changes into the Postgres `resources` JSONB table (ADR 2026-04-17-01).",
@@ -1281,7 +2166,7 @@ var DataStorePostgresReplica = class extends import_constructs5.Construct {
1281
2166
  new import_aws_lambda_event_sources.KinesisEventSource(props.kinesisStream, {
1282
2167
  startingPosition: import_aws_lambda5.StartingPosition.LATEST,
1283
2168
  batchSize: 100,
1284
- maxBatchingWindow: import_aws_cdk_lib8.Duration.seconds(5),
2169
+ maxBatchingWindow: import_aws_cdk_lib9.Duration.seconds(5),
1285
2170
  retryAttempts: 10,
1286
2171
  bisectBatchOnError: true,
1287
2172
  parallelizationFactor: 2,
@@ -1314,7 +2199,7 @@ var DataStorePostgresReplica = class extends import_constructs5.Construct {
1314
2199
  };
1315
2200
 
1316
2201
  // src/components/route-53/child-hosted-zone.ts
1317
- var import_aws_cdk_lib9 = require("aws-cdk-lib");
2202
+ var import_aws_cdk_lib10 = require("aws-cdk-lib");
1318
2203
  var import_aws_route53 = require("aws-cdk-lib/aws-route53");
1319
2204
  var ChildHostedZone = class extends import_aws_route53.HostedZone {
1320
2205
  constructor(scope, id, props) {
@@ -1323,7 +2208,7 @@ var ChildHostedZone = class extends import_aws_route53.HostedZone {
1323
2208
  zone: props.parentHostedZone,
1324
2209
  recordName: this.zoneName,
1325
2210
  values: this.hostedZoneNameServers || [],
1326
- ttl: import_aws_cdk_lib9.Duration.minutes(5)
2211
+ ttl: import_aws_cdk_lib10.Duration.minutes(5)
1327
2212
  });
1328
2213
  }
1329
2214
  };
@@ -1333,8 +2218,8 @@ var ChildHostedZone = class extends import_aws_route53.HostedZone {
1333
2218
  ChildHostedZone.SSM_PARAM_NAME = "CHILDHOSTEDZONE";
1334
2219
 
1335
2220
  // src/components/route-53/root-hosted-zone.ts
1336
- var import_constructs6 = require("constructs");
1337
- var RootHostedZone = class extends import_constructs6.Construct {
2221
+ var import_constructs7 = require("constructs");
2222
+ var RootHostedZone = class extends import_constructs7.Construct {
1338
2223
  };
1339
2224
 
1340
2225
  // src/components/static-hosting/static-hosting.ts
@@ -1342,9 +2227,9 @@ var import_aws_cloudfront = require("aws-cdk-lib/aws-cloudfront");
1342
2227
  var import_aws_cloudfront_origins = require("aws-cdk-lib/aws-cloudfront-origins");
1343
2228
  var import_aws_s3 = require("aws-cdk-lib/aws-s3");
1344
2229
  var import_core = require("aws-cdk-lib/core");
1345
- var import_constructs7 = require("constructs");
2230
+ var import_constructs8 = require("constructs");
1346
2231
  var STATIC_HOSTING_SERVICE_TYPE = "website";
1347
- var _StaticHosting = class _StaticHosting extends import_constructs7.Construct {
2232
+ var _StaticHosting = class _StaticHosting extends import_constructs8.Construct {
1348
2233
  constructor(scope, id, props = {}) {
1349
2234
  super(scope, id);
1350
2235
  const stack = OpenHiService.of(scope);
@@ -1396,21 +2281,119 @@ _StaticHosting.SSM_PARAM_NAME_DISTRIBUTION_ARN = "STATIC_HOSTING_DISTRIBUTION_AR
1396
2281
  var StaticHosting = _StaticHosting;
1397
2282
 
1398
2283
  // src/services/open-hi-auth-service.ts
1399
- var import_config4 = __toESM(require_lib());
2284
+ var import_config5 = __toESM(require_lib());
1400
2285
  var import_aws_cognito5 = require("aws-cdk-lib/aws-cognito");
1401
- var import_aws_iam = require("aws-cdk-lib/aws-iam");
2286
+ var import_aws_iam6 = require("aws-cdk-lib/aws-iam");
1402
2287
  var import_aws_kms2 = require("aws-cdk-lib/aws-kms");
1403
2288
  var import_core2 = require("aws-cdk-lib/core");
1404
2289
 
1405
2290
  // src/services/open-hi-data-service.ts
1406
- var import_aws_dynamodb2 = require("aws-cdk-lib/aws-dynamodb");
2291
+ var import_config4 = __toESM(require_lib());
2292
+ var import_aws_dynamodb3 = require("aws-cdk-lib/aws-dynamodb");
1407
2293
  var kinesis = __toESM(require("aws-cdk-lib/aws-kinesis"));
1408
2294
 
1409
2295
  // src/services/open-hi-global-service.ts
1410
2296
  var import_aws_certificatemanager2 = require("aws-cdk-lib/aws-certificatemanager");
1411
- var import_aws_events3 = require("aws-cdk-lib/aws-events");
2297
+ var import_aws_events5 = require("aws-cdk-lib/aws-events");
1412
2298
  var import_aws_route532 = require("aws-cdk-lib/aws-route53");
1413
2299
  var import_aws_ssm3 = require("aws-cdk-lib/aws-ssm");
2300
+
2301
+ // src/workflows/control-plane/platform-deploy-bridge/events.ts
2302
+ var CLOUDFORMATION_EVENT_SOURCE = "aws.cloudformation";
2303
+ var CLOUDFORMATION_STACK_STATUS_CHANGE_DETAIL_TYPE = "CloudFormation Stack Status Change";
2304
+ var BRIDGED_STATUSES = ["CREATE_COMPLETE", "UPDATE_COMPLETE"];
2305
+ var CONTROL_EVENT_BUS_NAME_ENV_VAR = "CONTROL_EVENT_BUS_NAME";
2306
+ var OPENHI_REPO_TAG_KEY_ENV_VAR = "OPENHI_REPO_TAG_KEY";
2307
+ var OPENHI_TAG_KEY_PREFIX_ENV_VAR = "OPENHI_TAG_KEY_PREFIX";
2308
+ var PLATFORM_DEPLOY_BRIDGE_ACTOR_SYSTEM = "platform-deploy-bridge";
2309
+
2310
+ // src/workflows/control-plane/platform-deploy-bridge/platform-deploy-bridge.ts
2311
+ var import_constructs10 = require("constructs");
2312
+
2313
+ // src/workflows/control-plane/platform-deploy-bridge/platform-deploy-bridge-lambda.ts
2314
+ var import_node_fs6 = __toESM(require("fs"));
2315
+ var import_node_path6 = __toESM(require("path"));
2316
+ var import_aws_cdk_lib11 = require("aws-cdk-lib");
2317
+ var import_aws_events4 = require("aws-cdk-lib/aws-events");
2318
+ var import_aws_events_targets = require("aws-cdk-lib/aws-events-targets");
2319
+ var import_aws_iam2 = require("aws-cdk-lib/aws-iam");
2320
+ var import_aws_lambda6 = require("aws-cdk-lib/aws-lambda");
2321
+ var import_aws_lambda_nodejs6 = require("aws-cdk-lib/aws-lambda-nodejs");
2322
+ var import_constructs9 = require("constructs");
2323
+ var HANDLER_NAME6 = "platform-deploy-bridge.handler.js";
2324
+ function resolveHandlerEntry6(dirname) {
2325
+ const sameDir = import_node_path6.default.join(dirname, HANDLER_NAME6);
2326
+ if (import_node_fs6.default.existsSync(sameDir)) {
2327
+ return sameDir;
2328
+ }
2329
+ return import_node_path6.default.join(dirname, "..", "..", "..", "..", "lib", HANDLER_NAME6);
2330
+ }
2331
+ var PlatformDeployBridgeLambda = class extends import_constructs9.Construct {
2332
+ constructor(scope, props) {
2333
+ super(scope, "platform-deploy-bridge-lambda");
2334
+ const service = OpenHiService.of(this);
2335
+ const repoTagKey = openHiTagKey(
2336
+ service.appName,
2337
+ OPENHI_TAG_SUFFIX_REPO_NAME
2338
+ );
2339
+ const tagKeyPrefix = `${service.appName}:`;
2340
+ const ownStackName = import_aws_cdk_lib11.Stack.of(this).stackName;
2341
+ const ownSuffix = `-${service.serviceId}-${import_aws_cdk_lib11.Stack.of(this).account}-${import_aws_cdk_lib11.Stack.of(this).region}`;
2342
+ const sharedPrefix = ownStackName.endsWith(ownSuffix) ? ownStackName.slice(0, -ownSuffix.length) : service.branchHash;
2343
+ const stackIdPrefix = `arn:aws:cloudformation:${import_aws_cdk_lib11.Stack.of(this).region}:${import_aws_cdk_lib11.Stack.of(this).account}:stack/${sharedPrefix}-`;
2344
+ this.lambda = new import_aws_lambda_nodejs6.NodejsFunction(this, "handler", {
2345
+ entry: resolveHandlerEntry6(__dirname),
2346
+ runtime: import_aws_lambda6.Runtime.NODEJS_LATEST,
2347
+ memorySize: 256,
2348
+ timeout: import_aws_cdk_lib11.Duration.seconds(30),
2349
+ environment: {
2350
+ [CONTROL_EVENT_BUS_NAME_ENV_VAR]: props.controlEventBus.eventBusName,
2351
+ [OPENHI_REPO_TAG_KEY_ENV_VAR]: repoTagKey,
2352
+ [OPENHI_TAG_KEY_PREFIX_ENV_VAR]: tagKeyPrefix
2353
+ }
2354
+ });
2355
+ this.lambda.addToRolePolicy(
2356
+ new import_aws_iam2.PolicyStatement({
2357
+ effect: import_aws_iam2.Effect.ALLOW,
2358
+ actions: ["cloudformation:DescribeStacks"],
2359
+ resources: [
2360
+ `arn:aws:cloudformation:${import_aws_cdk_lib11.Stack.of(this).region}:${import_aws_cdk_lib11.Stack.of(this).account}:stack/*`
2361
+ ]
2362
+ })
2363
+ );
2364
+ props.controlEventBus.grantPutEventsTo(this.lambda);
2365
+ this.rule = new import_aws_events4.Rule(this, "rule", {
2366
+ eventPattern: {
2367
+ source: [CLOUDFORMATION_EVENT_SOURCE],
2368
+ detailType: [CLOUDFORMATION_STACK_STATUS_CHANGE_DETAIL_TYPE],
2369
+ detail: {
2370
+ "stack-id": [{ prefix: stackIdPrefix }],
2371
+ "status-details": {
2372
+ status: [...BRIDGED_STATUSES]
2373
+ }
2374
+ }
2375
+ },
2376
+ targets: [
2377
+ new import_aws_events_targets.LambdaFunction(this.lambda, {
2378
+ retryAttempts: 2,
2379
+ maxEventAge: import_aws_cdk_lib11.Duration.hours(2)
2380
+ })
2381
+ ]
2382
+ });
2383
+ }
2384
+ };
2385
+
2386
+ // src/workflows/control-plane/platform-deploy-bridge/platform-deploy-bridge.ts
2387
+ var PlatformDeployBridge = class extends import_constructs10.Construct {
2388
+ constructor(scope, props) {
2389
+ super(scope, "platform-deploy-bridge");
2390
+ this.bridgeLambda = new PlatformDeployBridgeLambda(this, {
2391
+ controlEventBus: props.controlEventBus
2392
+ });
2393
+ }
2394
+ };
2395
+
2396
+ // src/services/open-hi-global-service.ts
1414
2397
  var _OpenHiGlobalService = class _OpenHiGlobalService extends OpenHiService {
1415
2398
  /**
1416
2399
  * Returns an IHostedZone from the given attributes (no SSM). Use when the zone is imported from config.
@@ -1449,7 +2432,7 @@ var _OpenHiGlobalService = class _OpenHiGlobalService extends OpenHiService {
1449
2432
  * Returns the data event bus by name (deterministic per branch). Use from other stacks to obtain an IEventBus reference.
1450
2433
  */
1451
2434
  static dataEventBusFromConstruct(scope) {
1452
- return import_aws_events3.EventBus.fromEventBusName(
2435
+ return import_aws_events5.EventBus.fromEventBusName(
1453
2436
  scope,
1454
2437
  "data-event-bus",
1455
2438
  DataEventBus.getEventBusName(scope)
@@ -1459,12 +2442,33 @@ var _OpenHiGlobalService = class _OpenHiGlobalService extends OpenHiService {
1459
2442
  * Returns the ops event bus by name (deterministic per branch). Use from other stacks to obtain an IEventBus reference.
1460
2443
  */
1461
2444
  static opsEventBusFromConstruct(scope) {
1462
- return import_aws_events3.EventBus.fromEventBusName(
2445
+ return import_aws_events5.EventBus.fromEventBusName(
1463
2446
  scope,
1464
2447
  "ops-event-bus",
1465
2448
  OpsEventBus.getEventBusName(scope)
1466
2449
  );
1467
2450
  }
2451
+ /**
2452
+ * Returns the control-plane event bus by name (deterministic per branch). Use from other stacks to obtain an IEventBus reference.
2453
+ */
2454
+ static controlEventBusFromConstruct(scope) {
2455
+ return import_aws_events5.EventBus.fromEventBusName(
2456
+ scope,
2457
+ "control-event-bus",
2458
+ ControlEventBus.getEventBusName(scope)
2459
+ );
2460
+ }
2461
+ /**
2462
+ * Returns the workflow dedup table by name (deterministic per branch).
2463
+ * Use from other stacks to obtain an ITable reference. Consumer Lambdas
2464
+ * are typically wired via `WorkflowDedupTable.grantConsumer(fn, name)`
2465
+ * on the owning service's `workflowDedupTable` reference; the
2466
+ * `tableNameFromLookup` / `tableArnFromLookup` SSM helpers on the
2467
+ * construct cover cross-stack consumers that need only the name/ARN.
2468
+ */
2469
+ static workflowDedupTableNameFromLookup(scope) {
2470
+ return WorkflowDedupTable.tableNameFromLookup(scope);
2471
+ }
1468
2472
  get serviceType() {
1469
2473
  return _OpenHiGlobalService.SERVICE_TYPE;
1470
2474
  }
@@ -1477,6 +2481,9 @@ var _OpenHiGlobalService = class _OpenHiGlobalService extends OpenHiService {
1477
2481
  this.rootWildcardCertificate = this.createRootWildcardCertificate();
1478
2482
  this.dataEventBus = this.createDataEventBus();
1479
2483
  this.opsEventBus = this.createOpsEventBus();
2484
+ this.controlEventBus = this.createControlEventBus();
2485
+ this.workflowDedupTable = this.createWorkflowDedupTable();
2486
+ this.platformDeployBridge = this.createPlatformDeployBridge();
1480
2487
  }
1481
2488
  /**
1482
2489
  * Validates that config required for the Global stack is present.
@@ -1541,23 +2548,1255 @@ var _OpenHiGlobalService = class _OpenHiGlobalService extends OpenHiService {
1541
2548
  createOpsEventBus() {
1542
2549
  return new OpsEventBus(this);
1543
2550
  }
2551
+ /**
2552
+ * Creates the control-plane event bus.
2553
+ * Override to customize.
2554
+ */
2555
+ createControlEventBus() {
2556
+ return new ControlEventBus(this);
2557
+ }
2558
+ /**
2559
+ * Creates the platform deploy bridge that republishes CloudFormation
2560
+ * Stack Status Change events onto the control event bus.
2561
+ * Override to customize.
2562
+ */
2563
+ createPlatformDeployBridge() {
2564
+ return new PlatformDeployBridge(this, {
2565
+ controlEventBus: this.controlEventBus
2566
+ });
2567
+ }
2568
+ /**
2569
+ * Creates the shared workflow dedup table (TR-015 singleton).
2570
+ * Override to customize.
2571
+ */
2572
+ createWorkflowDedupTable() {
2573
+ return new WorkflowDedupTable(this, "workflow-dedup-table");
2574
+ }
1544
2575
  };
1545
2576
  _OpenHiGlobalService.SERVICE_TYPE = "global";
1546
2577
  var OpenHiGlobalService = _OpenHiGlobalService;
1547
2578
 
1548
- // src/services/open-hi-data-service.ts
1549
- var _OpenHiDataService = class _OpenHiDataService extends OpenHiService {
1550
- /**
1551
- * Returns the data store table by name. Use from other stacks (e.g. REST API Lambda) to obtain an ITable reference.
1552
- */
1553
- static dynamoDbDataStoreFromConstruct(scope, id = "dynamo-db-data-store") {
1554
- return import_aws_dynamodb2.Table.fromTableName(scope, id, getDynamoDbDataStoreTableName(scope));
2579
+ // src/workflows/control-plane/seed-demo-data/events.ts
2580
+ var import_types = require("@openhi/types");
2581
+ var import_workflows2 = __toESM(require_lib2());
2582
+ var SEED_DEMO_DATA_CONSUMER_NAME = "seed-demo-data";
2583
+ var DEMO_URN_SYSTEM = "urn:openhi:demo";
2584
+ var OPENHI_RESOURCE_URN_SYSTEM = "http://openhi.org/";
2585
+ var DEMO_PERIOD = { start: "2026-01-01T00:00:00Z" };
2586
+ var PLATFORM_SCOPE_TENANT_ID = "platform";
2587
+ var PLACEHOLDER_TENANT_ID = "placeholder-tenant-id";
2588
+ var PLACEHOLDER_WORKSPACE_ID = "placeholder-workspace-id";
2589
+ var DEV_USERS = [
2590
+ { id: "dev-russell", email: "russell@codedrifters.com" },
2591
+ { id: "dev-cameron", email: "cameron@codedrifters.com" },
2592
+ { id: "dev-neelima", email: "neelima@codedrifters.com" },
2593
+ { id: "dev-garon", email: "garon@codedrifters.com" },
2594
+ { id: "dev-dave", email: "dave@codedrifters.com" },
2595
+ { id: "dev-drew", email: "drew@codedrifters.com" },
2596
+ { id: "dev-jessica", email: "jessica@codedrifters.com" },
2597
+ { id: "dev-jared", email: "jared@codedrifters.com" },
2598
+ { id: "dev-goddess", email: "goddess@codedrifters.com" }
2599
+ ];
2600
+ var DEMO_TENANT_SPECS = [
2601
+ {
2602
+ scenario: "placeholder",
2603
+ tenantId: PLACEHOLDER_TENANT_ID,
2604
+ tenantName: "OpenHI Placeholder Tenant",
2605
+ workspaces: [
2606
+ {
2607
+ id: PLACEHOLDER_WORKSPACE_ID,
2608
+ name: "OpenHI Placeholder Workspace",
2609
+ roleSuffix: "workspace"
2610
+ }
2611
+ ]
2612
+ },
2613
+ {
2614
+ scenario: "demo-wound-care",
2615
+ tenantId: "demo-wound-care-tenant",
2616
+ tenantName: "Cedarbrook Wound Healing Institute",
2617
+ workspaces: [
2618
+ {
2619
+ id: "demo-wound-care-workspace",
2620
+ name: "Cedarbrook Outpatient Wound Clinic",
2621
+ roleSuffix: "workspace"
2622
+ }
2623
+ ]
2624
+ },
2625
+ {
2626
+ scenario: "demo-primary-care",
2627
+ tenantId: "demo-primary-care-tenant",
2628
+ tenantName: "Maple Ridge Family Medicine",
2629
+ workspaces: [
2630
+ {
2631
+ id: "demo-primary-care-workspace",
2632
+ name: "Maple Ridge Main Street Office",
2633
+ roleSuffix: "workspace"
2634
+ }
2635
+ ]
2636
+ },
2637
+ {
2638
+ scenario: "demo-mixed",
2639
+ tenantId: "demo-mixed-tenant",
2640
+ tenantName: "Northbridge Health Network",
2641
+ workspaces: [
2642
+ {
2643
+ id: "demo-mixed-workspace-wound-care",
2644
+ name: "Northbridge Wound Care Center",
2645
+ roleSuffix: "workspace-wound-care"
2646
+ },
2647
+ {
2648
+ id: "demo-mixed-workspace-primary-care",
2649
+ name: "Northbridge Family Practice",
2650
+ roleSuffix: "workspace-primary-care"
2651
+ }
2652
+ ]
2653
+ }
2654
+ ];
2655
+ var demoMembershipId = (devUserId, tenantId) => `demo-membership-${devUserId}-${tenantId}`;
2656
+ var demoRoleAssignmentId = (devUserId, tenantId, roleCode) => `demo-roleassignment-${devUserId}-${tenantId}-${roleCode}`;
2657
+ var demoScenarioIdentifier = (scenario, roleSuffix) => ({
2658
+ system: DEMO_URN_SYSTEM,
2659
+ value: `${scenario}:${roleSuffix}`
2660
+ });
2661
+ var openhiResourceIdentifier = (params) => ({
2662
+ use: "unversioned",
2663
+ system: OPENHI_RESOURCE_URN_SYSTEM,
2664
+ value: `urn:ohi:${params.tenantId}:${params.workspaceId}:${params.resourceType}:${params.id}`
2665
+ });
2666
+ var demoRolesForUserInTenant = (_user, _tenantId) => {
2667
+ void _user;
2668
+ void _tenantId;
2669
+ return [import_types.CONTROL_PLANE_ROLE_CODE.TENANT_ADMIN];
2670
+ };
2671
+ var rolePartitionKey = (roleId) => `role#id#${roleId}`;
2672
+ var demoTenantPartitionKey = (tenantId) => `tenant#id#${tenantId}`;
2673
+ var demoWorkspacePartitionKey = (tenantId, workspaceId) => `tid#${tenantId}#workspace#id#${workspaceId}`;
2674
+ var demoMembershipPartitionKey = (tenantId, membershipId) => `tid#${tenantId}#membership#id#${membershipId}`;
2675
+ var demoRoleAssignmentPartitionKey = (tenantId, roleAssignmentId) => `tid#${tenantId}#roleassignment#id#${roleAssignmentId}`;
2676
+ var demoUserPartitionKey = (userId) => `user#id#${userId}`;
2677
+ var demoBasePartitionKeys = () => {
2678
+ const keys = [];
2679
+ for (const spec of DEMO_TENANT_SPECS) {
2680
+ keys.push(demoTenantPartitionKey(spec.tenantId));
2681
+ for (const workspace of spec.workspaces) {
2682
+ keys.push(demoWorkspacePartitionKey(spec.tenantId, workspace.id));
2683
+ }
1555
2684
  }
1556
- get serviceType() {
1557
- return _OpenHiDataService.SERVICE_TYPE;
2685
+ return keys;
2686
+ };
2687
+ var demoDevUserPartitionKeys = (devUsers) => {
2688
+ const keys = [];
2689
+ for (const user of devUsers) {
2690
+ keys.push(demoUserPartitionKey(user.id));
2691
+ for (const spec of DEMO_TENANT_SPECS) {
2692
+ keys.push(
2693
+ demoMembershipPartitionKey(
2694
+ spec.tenantId,
2695
+ demoMembershipId(user.id, spec.tenantId)
2696
+ )
2697
+ );
2698
+ for (const roleCode of demoRolesForUserInTenant(user, spec.tenantId)) {
2699
+ keys.push(
2700
+ demoRoleAssignmentPartitionKey(
2701
+ spec.tenantId,
2702
+ demoRoleAssignmentId(user.id, spec.tenantId, roleCode)
2703
+ )
2704
+ );
2705
+ }
2706
+ }
2707
+ keys.push(
2708
+ demoRoleAssignmentPartitionKey(
2709
+ PLATFORM_SCOPE_TENANT_ID,
2710
+ demoRoleAssignmentId(
2711
+ user.id,
2712
+ PLATFORM_SCOPE_TENANT_ID,
2713
+ import_types.CONTROL_PLANE_ROLE_CODE.SYSTEM_ADMIN
2714
+ )
2715
+ )
2716
+ );
1558
2717
  }
2718
+ return keys;
2719
+ };
2720
+
2721
+ // src/workflows/control-plane/seed-demo-data/seed-demo-data-lambda.ts
2722
+ var import_node_fs7 = __toESM(require("fs"));
2723
+ var import_node_path7 = __toESM(require("path"));
2724
+ var import_types8 = require("@openhi/types");
2725
+ var import_aws_cdk_lib12 = require("aws-cdk-lib");
2726
+ var import_aws_events6 = require("aws-cdk-lib/aws-events");
2727
+ var import_aws_events_targets2 = require("aws-cdk-lib/aws-events-targets");
2728
+ var import_aws_iam3 = require("aws-cdk-lib/aws-iam");
2729
+ var import_aws_lambda7 = require("aws-cdk-lib/aws-lambda");
2730
+ var import_aws_lambda_nodejs7 = require("aws-cdk-lib/aws-lambda-nodejs");
2731
+ var import_constructs11 = require("constructs");
2732
+
2733
+ // src/workflows/control-plane/seed-demo-data/seed-demo-data.handler.ts
2734
+ var import_node_crypto = require("crypto");
2735
+ var import_client_cognito_identity_provider = require("@aws-sdk/client-cognito-identity-provider");
2736
+ var import_client_dynamodb2 = require("@aws-sdk/client-dynamodb");
2737
+ var import_types7 = require("@openhi/types");
2738
+ var import_workflows3 = __toESM(require_lib2());
2739
+
2740
+ // src/data/dynamo/dynamo-control-service.ts
2741
+ var import_electrodb8 = require("electrodb");
2742
+
2743
+ // src/data/dynamo/dynamo-client.ts
2744
+ var import_client_dynamodb = require("@aws-sdk/client-dynamodb");
2745
+ var defaultTableName = process.env.DYNAMO_TABLE_NAME ?? "jesttesttable";
2746
+ var dynamoClient = new import_client_dynamodb.DynamoDBClient({
2747
+ ...process.env.MOCK_DYNAMODB_ENDPOINT && {
2748
+ endpoint: process.env.MOCK_DYNAMODB_ENDPOINT,
2749
+ sslEnabled: false,
2750
+ region: "local"
2751
+ }
2752
+ });
2753
+
2754
+ // src/data/dynamo/entities/control/configuration-entity.ts
2755
+ var import_electrodb = require("electrodb");
2756
+
2757
+ // src/data/dynamo/entities/control/control-entity-common.ts
2758
+ var import_types2 = require("@openhi/types");
2759
+
2760
+ // src/data/dynamo/shard.ts
2761
+ var SHARD_COUNT = 4;
2762
+ function computeShard(id) {
2763
+ let hash = 2166136261;
2764
+ for (let i = 0; i < id.length; i++) {
2765
+ hash ^= id.charCodeAt(i);
2766
+ hash = Math.imul(hash, 16777619);
2767
+ }
2768
+ return (hash >>> 0) % SHARD_COUNT;
2769
+ }
2770
+
2771
+ // src/data/dynamo/entities/control/control-entity-common.ts
2772
+ var gsi1ShardAttribute = {
2773
+ type: "string",
2774
+ watch: ["id"],
2775
+ set: (_val, item) => {
2776
+ if (typeof item?.id !== "string" || item.id.length === 0) {
2777
+ return void 0;
2778
+ }
2779
+ return String(computeShard(item.id));
2780
+ }
2781
+ };
2782
+ var gsi1skAttribute = {
2783
+ type: "string",
2784
+ watch: ["resource", "lastUpdated", "id"],
2785
+ set: (_val, item) => {
2786
+ const id = typeof item?.id === "string" ? item.id : "";
2787
+ const lastUpdated = typeof item?.lastUpdated === "string" ? item.lastUpdated : "";
2788
+ const fallback = `${lastUpdated}#${id}`;
2789
+ if (typeof item?.resource !== "string" || item.resource.length === 0) {
2790
+ return fallback;
2791
+ }
2792
+ let parsed;
2793
+ try {
2794
+ parsed = JSON.parse(item.resource);
2795
+ } catch {
2796
+ return fallback;
2797
+ }
2798
+ if (!parsed || typeof parsed !== "object") return fallback;
2799
+ const resourceType = parsed.resourceType;
2800
+ if (typeof resourceType !== "string") return fallback;
2801
+ const label = (0, import_types2.extractLabel)(parsed);
2802
+ return label !== void 0 ? `${label}#${id}` : fallback;
2803
+ }
2804
+ };
2805
+
2806
+ // src/data/dynamo/entities/control/configuration-entity.ts
2807
+ var ConfigurationEntity = new import_electrodb.Entity({
2808
+ model: {
2809
+ entity: "configuration",
2810
+ service: "control",
2811
+ version: "01"
2812
+ },
2813
+ attributes: {
2814
+ /** Sort key. "CURRENT" for current version; version history in S3. */
2815
+ sk: {
2816
+ type: "string",
2817
+ required: true,
2818
+ default: "CURRENT"
2819
+ },
2820
+ /** Tenant scope. Use "BASELINE" when the config is baseline default (no tenant). */
2821
+ tenantId: {
2822
+ type: "string",
2823
+ required: true,
2824
+ default: "BASELINE"
2825
+ },
2826
+ /** Workspace scope. Use "-" when absent. */
2827
+ workspaceId: {
2828
+ type: "string",
2829
+ required: true,
2830
+ default: "-"
2831
+ },
2832
+ /** User scope. Use "-" when absent. */
2833
+ userId: {
2834
+ type: "string",
2835
+ required: true,
2836
+ default: "-"
2837
+ },
2838
+ /** Role scope. Use "-" when absent. */
2839
+ roleId: {
2840
+ type: "string",
2841
+ required: true,
2842
+ default: "-"
2843
+ },
2844
+ /** Config type (category), e.g. endpoints, branding, display. */
2845
+ key: {
2846
+ type: "string",
2847
+ required: true
2848
+ },
2849
+ /** FHIR Resource.id; logical id in URL and for the Configuration resource. */
2850
+ id: {
2851
+ type: "string",
2852
+ required: true
2853
+ },
2854
+ /** Payload as JSON string. JSON.stringify(resource) on write; JSON.parse(item.resource) on read. */
2855
+ resource: {
2856
+ type: "string",
2857
+ required: true
2858
+ },
2859
+ /**
2860
+ * Summary projection (key display fields as JSON string: id, key, status).
2861
+ * Populated on every write via extractSummary(resource); GSI1 INCLUDE surfaces it on lists.
2862
+ */
2863
+ summary: {
2864
+ type: "string",
2865
+ required: true
2866
+ },
2867
+ /** Version id (e.g. ULID). Tracks current version; S3 history key. */
2868
+ vid: {
2869
+ type: "string",
2870
+ required: true
2871
+ },
2872
+ lastUpdated: {
2873
+ type: "string",
2874
+ required: true
2875
+ },
2876
+ gsi1Shard: gsi1ShardAttribute,
2877
+ deleted: {
2878
+ type: "boolean",
2879
+ required: false
2880
+ },
2881
+ bundleId: {
2882
+ type: "string",
2883
+ required: false
2884
+ },
2885
+ msgId: {
2886
+ type: "string",
2887
+ required: false
2888
+ }
2889
+ },
2890
+ indexes: {
2891
+ /** 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. */
2892
+ record: {
2893
+ pk: {
2894
+ field: "PK",
2895
+ composite: ["tenantId", "workspaceId", "userId", "roleId"],
2896
+ template: "CONFIG#TID#${tenantId}#WID#${workspaceId}#UID#${userId}#RID#${roleId}"
2897
+ },
2898
+ sk: {
2899
+ field: "SK",
2900
+ composite: ["key", "sk"],
2901
+ template: "KEY#${key}#SK#${sk}"
2902
+ }
2903
+ },
2904
+ /**
2905
+ * GSI1 — Unified Sharded List per ADR-011: list all Configuration entries for a
2906
+ * (tenant, workspace) across the four shards. Use for "list configs scoped to this tenant"
2907
+ * (workspaceId = "-") or "list configs scoped to this workspace". Does not support
2908
+ * hierarchical resolution in one query; use base table GetItem in fallback order
2909
+ * (user → workspace → tenant → baseline) for that.
2910
+ * SK is `<key>#<id>` — Configuration's `key` is a required entity attribute (the
2911
+ * config category: endpoints, branding, display, …) and the natural sort/lookup
2912
+ * dimension. `casing: "none"` preserves the literal key value.
2913
+ */
2914
+ gsi1: {
2915
+ index: "GSI1",
2916
+ pk: {
2917
+ field: "GSI1PK",
2918
+ composite: ["tenantId", "workspaceId", "gsi1Shard"],
2919
+ template: "TID#${tenantId}#WID#${workspaceId}#RT#Configuration#SHARD#${gsi1Shard}"
2920
+ },
2921
+ sk: {
2922
+ field: "GSI1SK",
2923
+ casing: "none",
2924
+ composite: ["key", "id"],
2925
+ template: "${key}#${id}"
2926
+ }
2927
+ }
2928
+ }
2929
+ });
2930
+
2931
+ // src/data/dynamo/entities/control/membership-entity.ts
2932
+ var import_electrodb2 = require("electrodb");
2933
+ var MembershipEntity = new import_electrodb2.Entity({
2934
+ model: {
2935
+ entity: "membership",
2936
+ service: "control",
2937
+ version: "01"
2938
+ },
2939
+ attributes: {
2940
+ /** Sort key sentinel. Always "CURRENT". */
2941
+ sk: {
2942
+ type: "string",
2943
+ required: true,
2944
+ default: "CURRENT"
2945
+ },
2946
+ /** Tenant in which the user has membership (required). */
2947
+ tenantId: {
2948
+ type: "string",
2949
+ required: true
2950
+ },
2951
+ /** FHIR Resource.id; membership id. */
2952
+ id: {
2953
+ type: "string",
2954
+ required: true
2955
+ },
2956
+ /** Full Membership resource serialized as JSON string. */
2957
+ resource: {
2958
+ type: "string",
2959
+ required: true
2960
+ },
2961
+ /**
2962
+ * Summary projection (key display fields as JSON string: id, displayName, status).
2963
+ * Populated on every write via extractSummary(resource); GSI1 INCLUDE surfaces it on lists.
2964
+ */
2965
+ summary: {
2966
+ type: "string",
2967
+ required: true
2968
+ },
2969
+ /** Version id (e.g. ULID). */
2970
+ vid: {
2971
+ type: "string",
2972
+ required: true
2973
+ },
2974
+ lastUpdated: {
2975
+ type: "string",
2976
+ required: true
2977
+ },
2978
+ gsi1Shard: gsi1ShardAttribute,
2979
+ /** Derived GSI1 sort key — name-based when extractable; else `<lastUpdated>#<id>`. */
2980
+ gsi1sk: gsi1skAttribute,
2981
+ deleted: {
2982
+ type: "boolean",
2983
+ required: false
2984
+ },
2985
+ bundleId: {
2986
+ type: "string",
2987
+ required: false
2988
+ },
2989
+ msgId: {
2990
+ type: "string",
2991
+ required: false
2992
+ }
2993
+ },
2994
+ indexes: {
2995
+ /** Base table: PK = TID#<tenantId>#MEMBERSHIP#ID#<id>, SK = CURRENT. Do not supply PK or SK from outside. */
2996
+ record: {
2997
+ pk: {
2998
+ field: "PK",
2999
+ composite: ["tenantId", "id"],
3000
+ template: "TID#${tenantId}#MEMBERSHIP#ID#${id}"
3001
+ },
3002
+ sk: {
3003
+ field: "SK",
3004
+ composite: ["sk"],
3005
+ template: "${sk}"
3006
+ }
3007
+ },
3008
+ /**
3009
+ * GSI1 — Unified Sharded List per ADR-011: list all Memberships for a tenant across the
3010
+ * four shards. Membership is tenant-scoped only, so `WID#-` is a sentinel.
3011
+ * SK is derived via `gsi1skAttribute` — uses the resource's natural label when
3012
+ * extractable, else `<lastUpdated>#<id>` (DR-004). `casing: "none"` preserves the
3013
+ * normalized label and ISO-8601 `T`/`Z`.
3014
+ */
3015
+ gsi1: {
3016
+ index: "GSI1",
3017
+ pk: {
3018
+ field: "GSI1PK",
3019
+ composite: ["tenantId", "gsi1Shard"],
3020
+ template: "TID#${tenantId}#WID#-#RT#Membership#SHARD#${gsi1Shard}"
3021
+ },
3022
+ sk: {
3023
+ field: "GSI1SK",
3024
+ casing: "none",
3025
+ composite: ["gsi1sk"],
3026
+ template: "${gsi1sk}"
3027
+ }
3028
+ }
3029
+ }
3030
+ });
3031
+
3032
+ // src/data/dynamo/entities/control/role-entity.ts
3033
+ var import_electrodb3 = require("electrodb");
3034
+ var RoleEntity = new import_electrodb3.Entity({
3035
+ model: {
3036
+ entity: "role",
3037
+ service: "control",
3038
+ version: "01"
3039
+ },
3040
+ attributes: {
3041
+ /** Sort key sentinel. Always "CURRENT". */
3042
+ sk: {
3043
+ type: "string",
3044
+ required: true,
3045
+ default: "CURRENT"
3046
+ },
3047
+ /** FHIR Resource.id; role id. */
3048
+ id: {
3049
+ type: "string",
3050
+ required: true
3051
+ },
3052
+ /** Full Role resource serialized as JSON string. */
3053
+ resource: {
3054
+ type: "string",
3055
+ required: true
3056
+ },
3057
+ /**
3058
+ * Summary projection (key display fields as JSON string: id, displayName, status).
3059
+ * Populated on every write via extractSummary(resource); GSI1 INCLUDE surfaces it on lists.
3060
+ */
3061
+ summary: {
3062
+ type: "string",
3063
+ required: true
3064
+ },
3065
+ /** Version id (e.g. ULID). */
3066
+ vid: {
3067
+ type: "string",
3068
+ required: true
3069
+ },
3070
+ lastUpdated: {
3071
+ type: "string",
3072
+ required: true
3073
+ },
3074
+ gsi1Shard: gsi1ShardAttribute,
3075
+ /** Derived GSI1 sort key — name-based when extractable; else `<lastUpdated>#<id>`. */
3076
+ gsi1sk: gsi1skAttribute,
3077
+ deleted: {
3078
+ type: "boolean",
3079
+ required: false
3080
+ },
3081
+ bundleId: {
3082
+ type: "string",
3083
+ required: false
3084
+ },
3085
+ msgId: {
3086
+ type: "string",
3087
+ required: false
3088
+ }
3089
+ },
3090
+ indexes: {
3091
+ /** Base table: PK = ROLE#ID#<id>, SK = CURRENT. Do not supply PK or SK from outside. */
3092
+ record: {
3093
+ pk: {
3094
+ field: "PK",
3095
+ composite: ["id"],
3096
+ template: "ROLE#ID#${id}"
3097
+ },
3098
+ sk: {
3099
+ field: "SK",
3100
+ composite: ["sk"],
3101
+ template: "${sk}"
3102
+ }
3103
+ },
3104
+ /**
3105
+ * GSI1 — Unified Sharded List per ADR-011: list all Roles across the four shards.
3106
+ * Non-tenant-isolated, so `TID#-#WID#-` sentinels precede `RT#Role#SHARD#<n>`.
3107
+ * SK is derived via `gsi1skAttribute` — uses the resource's natural label when
3108
+ * extractable, else `<lastUpdated>#<id>` (DR-004). `casing: "none"` preserves the
3109
+ * normalized label and ISO-8601 `T`/`Z`.
3110
+ */
3111
+ gsi1: {
3112
+ index: "GSI1",
3113
+ pk: {
3114
+ field: "GSI1PK",
3115
+ composite: ["gsi1Shard"],
3116
+ template: "TID#-#WID#-#RT#Role#SHARD#${gsi1Shard}"
3117
+ },
3118
+ sk: {
3119
+ field: "GSI1SK",
3120
+ casing: "none",
3121
+ composite: ["gsi1sk"],
3122
+ template: "${gsi1sk}"
3123
+ }
3124
+ }
3125
+ }
3126
+ });
3127
+
3128
+ // src/data/dynamo/entities/control/roleassignment-entity.ts
3129
+ var import_electrodb4 = require("electrodb");
3130
+ var RoleAssignmentEntity = new import_electrodb4.Entity({
3131
+ model: {
3132
+ entity: "roleassignment",
3133
+ service: "control",
3134
+ version: "01"
3135
+ },
3136
+ attributes: {
3137
+ /** Sort key sentinel. Always "CURRENT". */
3138
+ sk: {
3139
+ type: "string",
3140
+ required: true,
3141
+ default: "CURRENT"
3142
+ },
3143
+ /** Tenant in which the role assignment applies (required). */
3144
+ tenantId: {
3145
+ type: "string",
3146
+ required: true
3147
+ },
3148
+ /** FHIR Resource.id; role assignment id. */
3149
+ id: {
3150
+ type: "string",
3151
+ required: true
3152
+ },
3153
+ /** Full RoleAssignment resource serialized as JSON string. */
3154
+ resource: {
3155
+ type: "string",
3156
+ required: true
3157
+ },
3158
+ /**
3159
+ * Summary projection (key display fields as JSON string: id, displayName, status).
3160
+ * Populated on every write via extractSummary(resource); GSI1 INCLUDE surfaces it on lists.
3161
+ */
3162
+ summary: {
3163
+ type: "string",
3164
+ required: true
3165
+ },
3166
+ /** Version id (e.g. ULID). */
3167
+ vid: {
3168
+ type: "string",
3169
+ required: true
3170
+ },
3171
+ lastUpdated: {
3172
+ type: "string",
3173
+ required: true
3174
+ },
3175
+ gsi1Shard: gsi1ShardAttribute,
3176
+ /** Derived GSI1 sort key — name-based when extractable; else `<lastUpdated>#<id>`. */
3177
+ gsi1sk: gsi1skAttribute,
3178
+ deleted: {
3179
+ type: "boolean",
3180
+ required: false
3181
+ },
3182
+ bundleId: {
3183
+ type: "string",
3184
+ required: false
3185
+ },
3186
+ msgId: {
3187
+ type: "string",
3188
+ required: false
3189
+ }
3190
+ },
3191
+ indexes: {
3192
+ /** Base table: PK = TID#<tenantId>#ROLEASSIGNMENT#ID#<id>, SK = CURRENT. Do not supply PK or SK from outside. */
3193
+ record: {
3194
+ pk: {
3195
+ field: "PK",
3196
+ composite: ["tenantId", "id"],
3197
+ template: "TID#${tenantId}#ROLEASSIGNMENT#ID#${id}"
3198
+ },
3199
+ sk: {
3200
+ field: "SK",
3201
+ composite: ["sk"],
3202
+ template: "${sk}"
3203
+ }
3204
+ },
3205
+ /**
3206
+ * GSI1 — Unified Sharded List per ADR-011: list all RoleAssignments for a tenant across the
3207
+ * four shards. Tenant-scoped only, so `WID#-` is a sentinel.
3208
+ * SK is derived via `gsi1skAttribute` — uses the resource's natural label when
3209
+ * extractable, else `<lastUpdated>#<id>` (DR-004). `casing: "none"` preserves the
3210
+ * normalized label and ISO-8601 `T`/`Z`.
3211
+ */
3212
+ gsi1: {
3213
+ index: "GSI1",
3214
+ pk: {
3215
+ field: "GSI1PK",
3216
+ composite: ["tenantId", "gsi1Shard"],
3217
+ template: "TID#${tenantId}#WID#-#RT#RoleAssignment#SHARD#${gsi1Shard}"
3218
+ },
3219
+ sk: {
3220
+ field: "GSI1SK",
3221
+ casing: "none",
3222
+ composite: ["gsi1sk"],
3223
+ template: "${gsi1sk}"
3224
+ }
3225
+ }
3226
+ }
3227
+ });
3228
+
3229
+ // src/data/dynamo/entities/control/tenant-entity.ts
3230
+ var import_electrodb5 = require("electrodb");
3231
+ var TenantEntity = new import_electrodb5.Entity({
3232
+ model: {
3233
+ entity: "tenant",
3234
+ service: "control",
3235
+ version: "01"
3236
+ },
3237
+ attributes: {
3238
+ /** Sort key sentinel. Always "CURRENT". */
3239
+ sk: {
3240
+ type: "string",
3241
+ required: true,
3242
+ default: "CURRENT"
3243
+ },
3244
+ /** The tenant's own id (= resource id). Drives the partition key. */
3245
+ tenantId: {
3246
+ type: "string",
3247
+ required: true
3248
+ },
3249
+ /** FHIR Resource.id; logical id in URL. Equals tenantId. */
3250
+ id: {
3251
+ type: "string",
3252
+ required: true
3253
+ },
3254
+ /** Full Tenant resource serialized as JSON string. */
3255
+ resource: {
3256
+ type: "string",
3257
+ required: true
3258
+ },
3259
+ /**
3260
+ * Summary projection (key display fields as JSON string: id, displayName, status).
3261
+ * Populated on every write via extractSummary(resource); GSI1 INCLUDE surfaces it on lists.
3262
+ */
3263
+ summary: {
3264
+ type: "string",
3265
+ required: true
3266
+ },
3267
+ /** Version id (e.g. ULID). */
3268
+ vid: {
3269
+ type: "string",
3270
+ required: true
3271
+ },
3272
+ lastUpdated: {
3273
+ type: "string",
3274
+ required: true
3275
+ },
3276
+ gsi1Shard: gsi1ShardAttribute,
3277
+ /** Derived GSI1 sort key — name-based when extractable; else `<lastUpdated>#<id>`. */
3278
+ gsi1sk: gsi1skAttribute,
3279
+ deleted: {
3280
+ type: "boolean",
3281
+ required: false
3282
+ },
3283
+ bundleId: {
3284
+ type: "string",
3285
+ required: false
3286
+ },
3287
+ msgId: {
3288
+ type: "string",
3289
+ required: false
3290
+ }
3291
+ },
3292
+ indexes: {
3293
+ /** Base table: PK = TENANT#ID#<tenantId>, SK = CURRENT. Do not supply PK or SK from outside. */
3294
+ record: {
3295
+ pk: {
3296
+ field: "PK",
3297
+ composite: ["tenantId"],
3298
+ template: "TENANT#ID#${tenantId}"
3299
+ },
3300
+ sk: {
3301
+ field: "SK",
3302
+ composite: ["sk"],
3303
+ template: "${sk}"
3304
+ }
3305
+ },
3306
+ /**
3307
+ * GSI1 — Unified Sharded List per ADR-011: list all Tenants across the four shards.
3308
+ * Tenant lives at the platform tier (no parent tenant or workspace), so `TID#-#WID#-`
3309
+ * sentinels precede `RT#Tenant#SHARD#<n>`. SK is derived via `gsi1skAttribute` —
3310
+ * `<normalizedName>#<id>` when the resource carries a `name`, else `<lastUpdated>#<id>`
3311
+ * (DR-004). `casing: "none"` preserves the normalized label and ISO-8601 `T`/`Z`.
3312
+ */
3313
+ gsi1: {
3314
+ index: "GSI1",
3315
+ pk: {
3316
+ field: "GSI1PK",
3317
+ composite: ["gsi1Shard"],
3318
+ template: "TID#-#WID#-#RT#Tenant#SHARD#${gsi1Shard}"
3319
+ },
3320
+ sk: {
3321
+ field: "GSI1SK",
3322
+ casing: "none",
3323
+ composite: ["gsi1sk"],
3324
+ template: "${gsi1sk}"
3325
+ }
3326
+ }
3327
+ }
3328
+ });
3329
+
3330
+ // src/data/dynamo/entities/control/user-entity.ts
3331
+ var import_electrodb6 = require("electrodb");
3332
+ var UserEntity = new import_electrodb6.Entity({
3333
+ model: {
3334
+ entity: "user",
3335
+ service: "control",
3336
+ version: "01"
3337
+ },
3338
+ attributes: {
3339
+ /** Sort key sentinel. Always "CURRENT". */
3340
+ sk: {
3341
+ type: "string",
3342
+ required: true,
3343
+ default: "CURRENT"
3344
+ },
3345
+ /** FHIR Resource.id; platform user id (ohi_uid). */
3346
+ id: {
3347
+ type: "string",
3348
+ required: true
3349
+ },
3350
+ /** Full User resource serialized as JSON string. */
3351
+ resource: {
3352
+ type: "string",
3353
+ required: true
3354
+ },
3355
+ /**
3356
+ * Summary projection (key display fields as JSON string: id, displayName, status).
3357
+ * Populated on every write via extractSummary(resource); GSI1 INCLUDE surfaces it on lists.
3358
+ */
3359
+ summary: {
3360
+ type: "string",
3361
+ required: true
3362
+ },
3363
+ /**
3364
+ * Immutable Cognito-issued `sub` claim. Drives GSI2 (sub-lookup). Optional until the
3365
+ * Post Confirmation Lambda (#770) lands; required thereafter.
3366
+ */
3367
+ cognitoSub: {
3368
+ type: "string",
3369
+ required: false
3370
+ },
3371
+ /** Version id (e.g. ULID). */
3372
+ vid: {
3373
+ type: "string",
3374
+ required: true
3375
+ },
3376
+ lastUpdated: {
3377
+ type: "string",
3378
+ required: true
3379
+ },
3380
+ gsi1Shard: gsi1ShardAttribute,
3381
+ /** Derived GSI1 sort key — name-based when extractable; else `<lastUpdated>#<id>`. */
3382
+ gsi1sk: gsi1skAttribute,
3383
+ deleted: {
3384
+ type: "boolean",
3385
+ required: false
3386
+ },
3387
+ bundleId: {
3388
+ type: "string",
3389
+ required: false
3390
+ },
3391
+ msgId: {
3392
+ type: "string",
3393
+ required: false
3394
+ }
3395
+ },
3396
+ indexes: {
3397
+ /** Base table: PK = USER#ID#<id>, SK = CURRENT. Do not supply PK or SK from outside. */
3398
+ record: {
3399
+ pk: {
3400
+ field: "PK",
3401
+ composite: ["id"],
3402
+ template: "USER#ID#${id}"
3403
+ },
3404
+ sk: {
3405
+ field: "SK",
3406
+ composite: ["sk"],
3407
+ template: "${sk}"
3408
+ }
3409
+ },
3410
+ /**
3411
+ * GSI1 — Unified Sharded List per ADR-011: list all Users across the four shards.
3412
+ * Non-tenant-isolated, so `TID#-#WID#-` sentinels precede `RT#User#SHARD#<n>`.
3413
+ * SK is derived via `gsi1skAttribute` — uses the resource's natural label when
3414
+ * extractable (string `name`/`title` via introspection), else `<lastUpdated>#<id>`
3415
+ * (DR-004). `casing: "none"` preserves the normalized label and ISO-8601 `T`/`Z`.
3416
+ */
3417
+ gsi1: {
3418
+ index: "GSI1",
3419
+ pk: {
3420
+ field: "GSI1PK",
3421
+ composite: ["gsi1Shard"],
3422
+ template: "TID#-#WID#-#RT#User#SHARD#${gsi1Shard}"
3423
+ },
3424
+ sk: {
3425
+ field: "GSI1SK",
3426
+ casing: "none",
3427
+ composite: ["gsi1sk"],
3428
+ template: "${gsi1sk}"
3429
+ }
3430
+ },
3431
+ /**
3432
+ * GSI2 — Cognito sub-lookup per ADR-011: resolves the UserEntity from a Cognito `sub` claim.
3433
+ * `condition` skips the index when `cognitoSub` is missing so legacy items without a sub are
3434
+ * not indexed.
3435
+ */
3436
+ gsi2: {
3437
+ index: "GSI2",
3438
+ condition: (attrs) => typeof attrs.cognitoSub === "string" && attrs.cognitoSub.length > 0,
3439
+ pk: {
3440
+ field: "GSI2PK",
3441
+ casing: "none",
3442
+ composite: ["cognitoSub"],
3443
+ template: "USER#SUB#${cognitoSub}"
3444
+ },
3445
+ sk: {
3446
+ field: "GSI2SK",
3447
+ casing: "none",
3448
+ composite: [],
3449
+ template: "CURRENT"
3450
+ }
3451
+ }
3452
+ }
3453
+ });
3454
+
3455
+ // src/data/dynamo/entities/control/workspace-entity.ts
3456
+ var import_electrodb7 = require("electrodb");
3457
+ var WorkspaceEntity = new import_electrodb7.Entity({
3458
+ model: {
3459
+ entity: "workspace",
3460
+ service: "control",
3461
+ version: "01"
3462
+ },
3463
+ attributes: {
3464
+ /** Sort key sentinel. Always "CURRENT". */
3465
+ sk: {
3466
+ type: "string",
3467
+ required: true,
3468
+ default: "CURRENT"
3469
+ },
3470
+ /** Tenant that contains this workspace (required). */
3471
+ tenantId: {
3472
+ type: "string",
3473
+ required: true
3474
+ },
3475
+ /** FHIR Resource.id; logical id in URL. */
3476
+ id: {
3477
+ type: "string",
3478
+ required: true
3479
+ },
3480
+ /** Full Workspace resource serialized as JSON string. */
3481
+ resource: {
3482
+ type: "string",
3483
+ required: true
3484
+ },
3485
+ /**
3486
+ * Summary projection (key display fields as JSON string: id, displayName, status).
3487
+ * Populated on every write via extractSummary(resource); GSI1 INCLUDE surfaces it on lists.
3488
+ */
3489
+ summary: {
3490
+ type: "string",
3491
+ required: true
3492
+ },
3493
+ /** Version id (e.g. ULID). */
3494
+ vid: {
3495
+ type: "string",
3496
+ required: true
3497
+ },
3498
+ lastUpdated: {
3499
+ type: "string",
3500
+ required: true
3501
+ },
3502
+ gsi1Shard: gsi1ShardAttribute,
3503
+ /** Derived GSI1 sort key — name-based when extractable; else `<lastUpdated>#<id>`. */
3504
+ gsi1sk: gsi1skAttribute,
3505
+ deleted: {
3506
+ type: "boolean",
3507
+ required: false
3508
+ },
3509
+ bundleId: {
3510
+ type: "string",
3511
+ required: false
3512
+ },
3513
+ msgId: {
3514
+ type: "string",
3515
+ required: false
3516
+ }
3517
+ },
3518
+ indexes: {
3519
+ /** Base table: PK = TID#<tenantId>#WORKSPACE#ID#<id>, SK = CURRENT. Do not supply PK or SK from outside. */
3520
+ record: {
3521
+ pk: {
3522
+ field: "PK",
3523
+ composite: ["tenantId", "id"],
3524
+ template: "TID#${tenantId}#WORKSPACE#ID#${id}"
3525
+ },
3526
+ sk: {
3527
+ field: "SK",
3528
+ composite: ["sk"],
3529
+ template: "${sk}"
3530
+ }
3531
+ },
3532
+ /**
3533
+ * GSI1 — Unified Sharded List per ADR-011: list all Workspaces for a tenant across the
3534
+ * four shards. Workspace is itself the workspace identity, so `WID#-` is a sentinel.
3535
+ * SK is derived via `gsi1skAttribute` — `<normalizedName>#<id>` when the resource
3536
+ * carries a `name`, else `<lastUpdated>#<id>` (DR-004). `casing: "none"` preserves
3537
+ * the normalized label and ISO-8601 `T`/`Z`.
3538
+ */
3539
+ gsi1: {
3540
+ index: "GSI1",
3541
+ pk: {
3542
+ field: "GSI1PK",
3543
+ composite: ["tenantId", "gsi1Shard"],
3544
+ template: "TID#${tenantId}#WID#-#RT#Workspace#SHARD#${gsi1Shard}"
3545
+ },
3546
+ sk: {
3547
+ field: "GSI1SK",
3548
+ casing: "none",
3549
+ composite: ["gsi1sk"],
3550
+ template: "${gsi1sk}"
3551
+ }
3552
+ }
3553
+ }
3554
+ });
3555
+
3556
+ // src/data/dynamo/dynamo-control-service.ts
3557
+ var controlPlaneEntities = {
3558
+ configuration: ConfigurationEntity,
3559
+ membership: MembershipEntity,
3560
+ role: RoleEntity,
3561
+ roleAssignment: RoleAssignmentEntity,
3562
+ tenant: TenantEntity,
3563
+ user: UserEntity,
3564
+ workspace: WorkspaceEntity
3565
+ };
3566
+ var controlPlaneService = new import_electrodb8.Service(controlPlaneEntities, {
3567
+ table: defaultTableName,
3568
+ client: dynamoClient
3569
+ });
3570
+ var DynamoControlService = {
3571
+ entities: controlPlaneService.entities
3572
+ };
3573
+
3574
+ // src/data/operations/control/membership/membership-create-operation.ts
3575
+ var import_types3 = require("@openhi/types");
3576
+
3577
+ // src/data/operations/control/roleassignment/roleassignment-create-operation.ts
3578
+ var import_types4 = require("@openhi/types");
3579
+
3580
+ // src/data/operations/control/tenant/tenant-create-operation.ts
3581
+ var import_types5 = require("@openhi/types");
3582
+
3583
+ // src/data/operations/control/workspace/workspace-create-operation.ts
3584
+ var import_types6 = require("@openhi/types");
3585
+
3586
+ // src/workflows/control-plane/seed-demo-data/seed-demo-data.handler.ts
3587
+ var SEED_DEMO_DATA_USER_POOL_ID_ENV_VAR = "SEED_DEMO_DATA_USER_POOL_ID";
3588
+
3589
+ // src/workflows/control-plane/seed-demo-data/seed-demo-data-lambda.ts
3590
+ var HANDLER_NAME7 = "seed-demo-data.handler.js";
3591
+ function resolveHandlerEntry7(dirname) {
3592
+ const sameDir = import_node_path7.default.join(dirname, HANDLER_NAME7);
3593
+ if (import_node_fs7.default.existsSync(sameDir)) {
3594
+ return sameDir;
3595
+ }
3596
+ return import_node_path7.default.join(dirname, "..", "..", "..", "..", "lib", HANDLER_NAME7);
3597
+ }
3598
+ var SeedDemoDataLambda = class extends import_constructs11.Construct {
3599
+ constructor(scope, props) {
3600
+ super(scope, "seed-demo-data-lambda");
3601
+ this.lambda = new import_aws_lambda_nodejs7.NodejsFunction(this, "handler", {
3602
+ entry: resolveHandlerEntry7(__dirname),
3603
+ runtime: import_aws_lambda7.Runtime.NODEJS_LATEST,
3604
+ memorySize: 512,
3605
+ timeout: import_aws_cdk_lib12.Duration.minutes(2),
3606
+ environment: {
3607
+ DYNAMO_TABLE_NAME: props.dataStoreTable.tableName,
3608
+ [SEED_DEMO_DATA_USER_POOL_ID_ENV_VAR]: props.userPool.userPoolId
3609
+ }
3610
+ });
3611
+ const roleReadKeys = Object.values(import_types8.CONTROL_PLANE_ROLE_IDS).map(
3612
+ rolePartitionKey
3613
+ );
3614
+ this.lambda.addToRolePolicy(
3615
+ new import_aws_iam3.PolicyStatement({
3616
+ effect: import_aws_iam3.Effect.ALLOW,
3617
+ actions: ["dynamodb:GetItem"],
3618
+ resources: [props.dataStoreTable.tableArn],
3619
+ conditions: {
3620
+ "ForAllValues:StringEquals": {
3621
+ "dynamodb:LeadingKeys": roleReadKeys
3622
+ }
3623
+ }
3624
+ })
3625
+ );
3626
+ const writeKeys = [
3627
+ ...demoBasePartitionKeys(),
3628
+ ...demoDevUserPartitionKeys(DEV_USERS)
3629
+ ];
3630
+ this.lambda.addToRolePolicy(
3631
+ new import_aws_iam3.PolicyStatement({
3632
+ effect: import_aws_iam3.Effect.ALLOW,
3633
+ actions: ["dynamodb:PutItem", "dynamodb:UpdateItem"],
3634
+ resources: [props.dataStoreTable.tableArn],
3635
+ conditions: {
3636
+ "ForAllValues:StringEquals": {
3637
+ "dynamodb:LeadingKeys": writeKeys
3638
+ }
3639
+ }
3640
+ })
3641
+ );
3642
+ this.lambda.addToRolePolicy(
3643
+ new import_aws_iam3.PolicyStatement({
3644
+ effect: import_aws_iam3.Effect.ALLOW,
3645
+ actions: [
3646
+ "cognito-idp:AdminCreateUser",
3647
+ "cognito-idp:AdminGetUser",
3648
+ "cognito-idp:AdminSetUserPassword"
3649
+ ],
3650
+ resources: [
3651
+ import_aws_cdk_lib12.Stack.of(this).formatArn({
3652
+ service: "cognito-idp",
3653
+ resource: "userpool",
3654
+ resourceName: props.userPool.userPoolId
3655
+ })
3656
+ ]
3657
+ })
3658
+ );
3659
+ this.rule = new import_aws_events6.Rule(this, "rule", {
3660
+ eventBus: props.controlEventBus,
3661
+ eventPattern: {
3662
+ source: [import_workflows2.PlatformSystemDataSeededV1.source],
3663
+ detailType: [import_workflows2.PlatformSystemDataSeededV1.detailType]
3664
+ },
3665
+ targets: [
3666
+ new import_aws_events_targets2.LambdaFunction(this.lambda, {
3667
+ retryAttempts: 2,
3668
+ maxEventAge: import_aws_cdk_lib12.Duration.hours(2)
3669
+ })
3670
+ ]
3671
+ });
3672
+ }
3673
+ };
3674
+
3675
+ // src/workflows/control-plane/seed-demo-data/seed-demo-data-workflow.ts
3676
+ var import_constructs12 = require("constructs");
3677
+ var SeedDemoDataWorkflow = class extends import_constructs12.Construct {
3678
+ constructor(scope, props) {
3679
+ super(scope, "seed-demo-data-workflow");
3680
+ this.seedDemoData = new SeedDemoDataLambda(this, {
3681
+ controlEventBus: props.controlEventBus,
3682
+ dataStoreTable: props.dataStoreTable,
3683
+ userPool: props.userPool
3684
+ });
3685
+ WorkflowDedupTable.grantConsumerFromLookup(
3686
+ this,
3687
+ this.seedDemoData.lambda,
3688
+ SEED_DEMO_DATA_CONSUMER_NAME
3689
+ );
3690
+ }
3691
+ };
3692
+
3693
+ // src/workflows/control-plane/seed-system-data/events.ts
3694
+ var import_workflows4 = __toESM(require_lib2());
3695
+ var SEED_SYSTEM_DATA_CONSUMER_NAME = "seed-system-data";
3696
+ var SEED_SYSTEM_DATA_ACTOR_SYSTEM = "seed-system-data";
3697
+ var SEED_SYSTEM_DATA_CONTROL_BUS_ENV_VAR = "CONTROL_EVENT_BUS_NAME";
3698
+
3699
+ // src/workflows/control-plane/seed-system-data/seed-system-data-lambda.ts
3700
+ var import_node_fs8 = __toESM(require("fs"));
3701
+ var import_node_path8 = __toESM(require("path"));
3702
+ var import_types9 = require("@openhi/types");
3703
+ var import_aws_cdk_lib13 = require("aws-cdk-lib");
3704
+ var import_aws_events7 = require("aws-cdk-lib/aws-events");
3705
+ var import_aws_events_targets3 = require("aws-cdk-lib/aws-events-targets");
3706
+ var import_aws_iam4 = require("aws-cdk-lib/aws-iam");
3707
+ var import_aws_lambda8 = require("aws-cdk-lib/aws-lambda");
3708
+ var import_aws_lambda_nodejs8 = require("aws-cdk-lib/aws-lambda-nodejs");
3709
+ var import_constructs13 = require("constructs");
3710
+ var HANDLER_NAME8 = "seed-system-data.handler.js";
3711
+ function resolveHandlerEntry8(dirname) {
3712
+ const sameDir = import_node_path8.default.join(dirname, HANDLER_NAME8);
3713
+ if (import_node_fs8.default.existsSync(sameDir)) {
3714
+ return sameDir;
3715
+ }
3716
+ return import_node_path8.default.join(dirname, "..", "..", "..", "..", "lib", HANDLER_NAME8);
3717
+ }
3718
+ var SeedSystemDataLambda = class extends import_constructs13.Construct {
3719
+ constructor(scope, props) {
3720
+ super(scope, "seed-system-data-lambda");
3721
+ this.lambda = new import_aws_lambda_nodejs8.NodejsFunction(this, "handler", {
3722
+ entry: resolveHandlerEntry8(__dirname),
3723
+ runtime: import_aws_lambda8.Runtime.NODEJS_LATEST,
3724
+ memorySize: 512,
3725
+ timeout: import_aws_cdk_lib13.Duration.minutes(1),
3726
+ environment: {
3727
+ DYNAMO_TABLE_NAME: props.dataStoreTable.tableName,
3728
+ [SEED_SYSTEM_DATA_CONTROL_BUS_ENV_VAR]: props.controlEventBus.eventBusName
3729
+ }
3730
+ });
3731
+ const roleArns = Object.values(import_types9.CONTROL_PLANE_ROLE_IDS).map(
3732
+ (id) => `role#id#${id}`
3733
+ );
3734
+ this.lambda.addToRolePolicy(
3735
+ new import_aws_iam4.PolicyStatement({
3736
+ effect: import_aws_iam4.Effect.ALLOW,
3737
+ actions: ["dynamodb:PutItem", "dynamodb:UpdateItem"],
3738
+ resources: [props.dataStoreTable.tableArn],
3739
+ conditions: {
3740
+ "ForAllValues:StringEquals": {
3741
+ "dynamodb:LeadingKeys": roleArns
3742
+ }
3743
+ }
3744
+ })
3745
+ );
3746
+ props.controlEventBus.grantPutEventsTo(this.lambda);
3747
+ const hostStackName = import_aws_cdk_lib13.Stack.of(this).stackName;
3748
+ this.rule = new import_aws_events7.Rule(this, "rule", {
3749
+ eventBus: props.controlEventBus,
3750
+ eventPattern: {
3751
+ source: [import_workflows4.PlatformDeploymentCompletedV1.source],
3752
+ detailType: [import_workflows4.PlatformDeploymentCompletedV1.detailType],
3753
+ detail: {
3754
+ payload: {
3755
+ stackName: [hostStackName]
3756
+ }
3757
+ }
3758
+ },
3759
+ targets: [
3760
+ new import_aws_events_targets3.LambdaFunction(this.lambda, {
3761
+ retryAttempts: 2,
3762
+ maxEventAge: import_aws_cdk_lib13.Duration.hours(2)
3763
+ })
3764
+ ]
3765
+ });
3766
+ }
3767
+ };
3768
+
3769
+ // src/workflows/control-plane/seed-system-data/seed-system-data-workflow.ts
3770
+ var import_constructs14 = require("constructs");
3771
+ var SeedSystemDataWorkflow = class extends import_constructs14.Construct {
3772
+ constructor(scope, props) {
3773
+ super(scope, "seed-system-data-workflow");
3774
+ this.seedSystemData = new SeedSystemDataLambda(this, {
3775
+ controlEventBus: props.controlEventBus,
3776
+ dataStoreTable: props.dataStoreTable
3777
+ });
3778
+ WorkflowDedupTable.grantConsumerFromLookup(
3779
+ this,
3780
+ this.seedSystemData.lambda,
3781
+ SEED_SYSTEM_DATA_CONSUMER_NAME
3782
+ );
3783
+ }
3784
+ };
3785
+
3786
+ // src/services/open-hi-data-service.ts
3787
+ var _OpenHiDataService = class _OpenHiDataService extends OpenHiService {
1559
3788
  constructor(ohEnv, props = {}) {
1560
3789
  super(ohEnv, _OpenHiDataService.SERVICE_TYPE, props);
3790
+ /**
3791
+ * Cached control-event-bus lookup. `OpenHiGlobalService.controlEventBusFromConstruct`
3792
+ * registers a child `EventBus.fromEventBusName` construct with a
3793
+ * fixed id under the scope it is passed, so calling it twice on the
3794
+ * same `OpenHiDataService` instance collides. The cache mirrors the
3795
+ * `private controlEventBus()` pattern already used in
3796
+ * `OpenHiAuthService`. Use {@link controlEventBus} from this class
3797
+ * — never call the static lookup from inside `OpenHiDataService`.
3798
+ */
3799
+ this._controlEventBus = null;
1561
3800
  this.props = props;
1562
3801
  this.dataStoreChangeStream = new kinesis.Stream(
1563
3802
  this,
@@ -1592,6 +3831,53 @@ var _OpenHiDataService = class _OpenHiDataService extends OpenHiService {
1592
3831
  branchHash: this.branchHash
1593
3832
  }
1594
3833
  );
3834
+ this.seedSystemDataWorkflow = this.createSeedSystemDataWorkflow();
3835
+ this.seedDemoDataWorkflow = this.createSeedDemoDataWorkflow();
3836
+ }
3837
+ /**
3838
+ * Returns the data store table by name. Use from other stacks (e.g. REST API Lambda) to obtain an ITable reference.
3839
+ */
3840
+ static dynamoDbDataStoreFromConstruct(scope, id = "dynamo-db-data-store") {
3841
+ return import_aws_dynamodb3.Table.fromTableName(scope, id, getDynamoDbDataStoreTableName(scope));
3842
+ }
3843
+ get serviceType() {
3844
+ return _OpenHiDataService.SERVICE_TYPE;
3845
+ }
3846
+ /**
3847
+ * Lazily looks up the control event bus exactly once per
3848
+ * `OpenHiDataService` instance and caches the reference. Every
3849
+ * workflow that consumes the bus must read it through this method
3850
+ * — see {@link _controlEventBus} for the underlying collision risk.
3851
+ */
3852
+ controlEventBus() {
3853
+ if (this._controlEventBus === null) {
3854
+ this._controlEventBus = OpenHiGlobalService.controlEventBusFromConstruct(this);
3855
+ }
3856
+ return this._controlEventBus;
3857
+ }
3858
+ /**
3859
+ * Creates the seed-system-data workflow. Override to customize.
3860
+ */
3861
+ createSeedSystemDataWorkflow() {
3862
+ return new SeedSystemDataWorkflow(this, {
3863
+ controlEventBus: this.controlEventBus(),
3864
+ dataStoreTable: this.dataStore
3865
+ });
3866
+ }
3867
+ /**
3868
+ * Creates the seed-demo-data workflow — but only on non-prod
3869
+ * stages. Returns `undefined` on prod so the workflow literally
3870
+ * does not exist in prod stacks. Override to customize.
3871
+ */
3872
+ createSeedDemoDataWorkflow() {
3873
+ if (this.ohEnv.ohStage.stageType === import_config4.OPEN_HI_STAGE.PROD) {
3874
+ return void 0;
3875
+ }
3876
+ return new SeedDemoDataWorkflow(this, {
3877
+ controlEventBus: this.controlEventBus(),
3878
+ dataStoreTable: this.dataStore,
3879
+ userPool: OpenHiAuthService.userPoolFromConstruct(this)
3880
+ });
1595
3881
  }
1596
3882
  /**
1597
3883
  * Creates the single-table DynamoDB data store.
@@ -1600,13 +3886,107 @@ var _OpenHiDataService = class _OpenHiDataService extends OpenHiService {
1600
3886
  createDataStore() {
1601
3887
  return new DynamoDbDataStore(this, "dynamo-db-data-store", {
1602
3888
  kinesisStream: this.dataStoreChangeStream,
1603
- stream: import_aws_dynamodb2.StreamViewType.NEW_AND_OLD_IMAGES
3889
+ stream: import_aws_dynamodb3.StreamViewType.NEW_AND_OLD_IMAGES
1604
3890
  });
1605
3891
  }
1606
3892
  };
1607
3893
  _OpenHiDataService.SERVICE_TYPE = "data";
1608
3894
  var OpenHiDataService = _OpenHiDataService;
1609
3895
 
3896
+ // src/workflows/control-plane/user-onboarding/events.ts
3897
+ var USER_ONBOARDING_EVENT_SOURCE = "openhi.control.user-onboarding";
3898
+ var PROVISION_DEFAULT_WORKSPACE_DETAIL_TYPE = "ProvisionDefaultWorkspaceRequested";
3899
+ var buildProvisionDefaultWorkspaceRequestedDetail = (event) => {
3900
+ const attrs = event.request?.userAttributes ?? {};
3901
+ const cognitoSub = attrs.sub?.trim();
3902
+ if (!cognitoSub) {
3903
+ return void 0;
3904
+ }
3905
+ const email = attrs.email?.trim();
3906
+ const displayName = email || event.userName || cognitoSub;
3907
+ return {
3908
+ cognitoSub,
3909
+ ...email ? { email } : {},
3910
+ displayName,
3911
+ trigger: {
3912
+ source: "cognito.post-confirmation",
3913
+ triggerSource: event.triggerSource,
3914
+ userPoolId: event.userPoolId,
3915
+ userName: event.userName,
3916
+ clientId: event.callerContext?.clientId
3917
+ }
3918
+ };
3919
+ };
3920
+
3921
+ // src/workflows/control-plane/user-onboarding/provision-default-workspace-lambda.ts
3922
+ var import_node_fs9 = __toESM(require("fs"));
3923
+ var import_node_path9 = __toESM(require("path"));
3924
+ var import_aws_cdk_lib14 = require("aws-cdk-lib");
3925
+ var import_aws_events8 = require("aws-cdk-lib/aws-events");
3926
+ var import_aws_events_targets4 = require("aws-cdk-lib/aws-events-targets");
3927
+ var import_aws_iam5 = require("aws-cdk-lib/aws-iam");
3928
+ var import_aws_lambda9 = require("aws-cdk-lib/aws-lambda");
3929
+ var import_aws_lambda_nodejs9 = require("aws-cdk-lib/aws-lambda-nodejs");
3930
+ var import_constructs15 = require("constructs");
3931
+ var HANDLER_NAME9 = "provision-default-workspace.handler.js";
3932
+ function resolveHandlerEntry9(dirname) {
3933
+ const sameDir = import_node_path9.default.join(dirname, HANDLER_NAME9);
3934
+ if (import_node_fs9.default.existsSync(sameDir)) {
3935
+ return sameDir;
3936
+ }
3937
+ return import_node_path9.default.join(dirname, "..", "..", "..", "..", "lib", HANDLER_NAME9);
3938
+ }
3939
+ var ProvisionDefaultWorkspaceLambda = class extends import_constructs15.Construct {
3940
+ constructor(scope, props) {
3941
+ super(scope, "provision-default-workspace-lambda");
3942
+ this.lambda = new import_aws_lambda_nodejs9.NodejsFunction(this, "handler", {
3943
+ entry: resolveHandlerEntry9(__dirname),
3944
+ runtime: import_aws_lambda9.Runtime.NODEJS_LATEST,
3945
+ memorySize: 1024,
3946
+ environment: {
3947
+ DYNAMO_TABLE_NAME: props.dataStoreTable.tableName
3948
+ }
3949
+ });
3950
+ props.dataStoreTable.grant(
3951
+ this.lambda,
3952
+ "dynamodb:PutItem",
3953
+ "dynamodb:UpdateItem"
3954
+ );
3955
+ this.lambda.addToRolePolicy(
3956
+ new import_aws_iam5.PolicyStatement({
3957
+ effect: import_aws_iam5.Effect.ALLOW,
3958
+ actions: ["dynamodb:Query"],
3959
+ resources: [`${props.dataStoreTable.tableArn}/index/*`]
3960
+ })
3961
+ );
3962
+ this.rule = new import_aws_events8.Rule(this, "rule", {
3963
+ eventBus: props.controlEventBus,
3964
+ eventPattern: {
3965
+ source: [USER_ONBOARDING_EVENT_SOURCE],
3966
+ detailType: [PROVISION_DEFAULT_WORKSPACE_DETAIL_TYPE]
3967
+ },
3968
+ targets: [
3969
+ new import_aws_events_targets4.LambdaFunction(this.lambda, {
3970
+ retryAttempts: 2,
3971
+ maxEventAge: import_aws_cdk_lib14.Duration.hours(2)
3972
+ })
3973
+ ]
3974
+ });
3975
+ }
3976
+ };
3977
+
3978
+ // src/workflows/control-plane/user-onboarding/user-onboarding-workflow.ts
3979
+ var import_constructs16 = require("constructs");
3980
+ var UserOnboardingWorkflow = class extends import_constructs16.Construct {
3981
+ constructor(scope, props) {
3982
+ super(scope, "user-onboarding-workflow");
3983
+ this.provisionDefaultWorkspace = new ProvisionDefaultWorkspaceLambda(this, {
3984
+ dataStoreTable: props.dataStoreTable,
3985
+ controlEventBus: props.controlEventBus
3986
+ });
3987
+ }
3988
+ };
3989
+
1610
3990
  // src/services/open-hi-auth-service.ts
1611
3991
  var _OpenHiAuthService = class _OpenHiAuthService extends OpenHiService {
1612
3992
  constructor(ohEnv, props = {}) {
@@ -1618,11 +3998,13 @@ var _OpenHiAuthService = class _OpenHiAuthService extends OpenHiService {
1618
3998
  * would collide.
1619
3999
  */
1620
4000
  this._dataStoreTable = null;
4001
+ this._controlEventBus = null;
1621
4002
  this.props = props;
1622
4003
  this.userPoolKmsKey = this.createUserPoolKmsKey();
1623
4004
  this.preTokenGenerationLambda = this.createPreTokenGenerationLambda();
1624
4005
  this.postAuthenticationLambda = this.createPostAuthenticationLambda();
1625
4006
  this.postConfirmationLambda = this.createPostConfirmationLambda();
4007
+ this.userOnboardingWorkflow = this.createUserOnboardingWorkflow();
1626
4008
  this.userPool = this.createUserPool();
1627
4009
  this.grantPreTokenGenerationPermissions();
1628
4010
  this.grantPostAuthenticationPermissions();
@@ -1739,23 +4121,33 @@ var _OpenHiAuthService = class _OpenHiAuthService extends OpenHiService {
1739
4121
  }
1740
4122
  /**
1741
4123
  * Creates the Post Confirmation Lambda (Cognito trigger). On sign-up
1742
- * confirmation, writes the new user's default Tenant, Workspace,
1743
- * Memberships, and `tenant-user` RoleAssignment, plus a User record
1744
- * carrying the Cognito `sub` and current tenant/workspace pointers
1745
- * (ADR 2026-03-17-01 invariants).
4124
+ * confirmation, publishes a control-plane workflow event; provisioning lives
4125
+ * behind EventBridge.
1746
4126
  */
1747
4127
  createPostConfirmationLambda() {
1748
4128
  const construct = new PostConfirmationLambda(this, {
1749
- dynamoTableName: this.dataStoreTable().tableName
4129
+ controlEventBusName: this.controlEventBus().eventBusName
1750
4130
  });
1751
4131
  return construct.lambda;
1752
4132
  }
4133
+ createUserOnboardingWorkflow() {
4134
+ return new UserOnboardingWorkflow(this, {
4135
+ controlEventBus: this.controlEventBus(),
4136
+ dataStoreTable: this.dataStoreTable()
4137
+ });
4138
+ }
1753
4139
  dataStoreTable() {
1754
4140
  if (this._dataStoreTable === null) {
1755
4141
  this._dataStoreTable = OpenHiDataService.dynamoDbDataStoreFromConstruct(this);
1756
4142
  }
1757
4143
  return this._dataStoreTable;
1758
4144
  }
4145
+ controlEventBus() {
4146
+ if (this._controlEventBus === null) {
4147
+ this._controlEventBus = OpenHiGlobalService.controlEventBusFromConstruct(this);
4148
+ }
4149
+ return this._controlEventBus;
4150
+ }
1759
4151
  /**
1760
4152
  * Creates the Cognito User Pool and exports its ID to SSM.
1761
4153
  * Look up via {@link OpenHiAuthService.userPoolFromConstruct}.
@@ -1800,8 +4192,8 @@ var _OpenHiAuthService = class _OpenHiAuthService extends OpenHiService {
1800
4192
  const dynamoActions = ["dynamodb:GetItem", "dynamodb:Query"];
1801
4193
  dataStoreTable.grant(this.preTokenGenerationLambda, ...dynamoActions);
1802
4194
  this.preTokenGenerationLambda.addToRolePolicy(
1803
- new import_aws_iam.PolicyStatement({
1804
- effect: import_aws_iam.Effect.ALLOW,
4195
+ new import_aws_iam6.PolicyStatement({
4196
+ effect: import_aws_iam6.Effect.ALLOW,
1805
4197
  actions: [...dynamoActions],
1806
4198
  resources: [`${dataStoreTable.tableArn}/index/*`]
1807
4199
  })
@@ -1822,7 +4214,7 @@ var _OpenHiAuthService = class _OpenHiAuthService extends OpenHiService {
1822
4214
  */
1823
4215
  grantPostAuthenticationPermissions() {
1824
4216
  this.postAuthenticationLambda.addToRolePolicy(
1825
- new import_aws_iam.PolicyStatement({
4217
+ new import_aws_iam6.PolicyStatement({
1826
4218
  actions: ["cognito-idp:AdminUserGlobalSignOut"],
1827
4219
  resources: [
1828
4220
  import_core2.Stack.of(this).formatArn({
@@ -1835,26 +4227,11 @@ var _OpenHiAuthService = class _OpenHiAuthService extends OpenHiService {
1835
4227
  );
1836
4228
  }
1837
4229
  /**
1838
- * Grants the Post Confirmation Lambda write access to the data store
1839
- * table (and its GSIs) so it can seed the new user's Tenant, Workspace,
1840
- * Memberships, RoleAssignment, and User records on sign-up confirmation.
4230
+ * Grants the Post Confirmation Lambda publish-only access to the
4231
+ * control-plane event bus. Workflow Lambdas own DynamoDB writes.
1841
4232
  */
1842
4233
  grantPostConfirmationPermissions() {
1843
- const dataStoreTable = this.dataStoreTable();
1844
- const dynamoActions = [
1845
- "dynamodb:PutItem",
1846
- "dynamodb:UpdateItem",
1847
- "dynamodb:BatchWriteItem",
1848
- "dynamodb:DescribeTable"
1849
- ];
1850
- dataStoreTable.grant(this.postConfirmationLambda, ...dynamoActions);
1851
- this.postConfirmationLambda.addToRolePolicy(
1852
- new import_aws_iam.PolicyStatement({
1853
- effect: import_aws_iam.Effect.ALLOW,
1854
- actions: [...dynamoActions],
1855
- resources: [`${dataStoreTable.tableArn}/index/*`]
1856
- })
1857
- );
4234
+ this.controlEventBus().grantPutEventsTo(this.postConfirmationLambda);
1858
4235
  }
1859
4236
  /**
1860
4237
  * Creates the User Pool Client and exports its ID to SSM (AUTH service type).
@@ -1884,7 +4261,7 @@ var _OpenHiAuthService = class _OpenHiAuthService extends OpenHiService {
1884
4261
  * via env vars to drive `InitiateAuth`.
1885
4262
  */
1886
4263
  createFixtureSeederClient() {
1887
- if (this.ohEnv.ohStage.stageType === import_config4.OPEN_HI_STAGE.PROD) {
4264
+ if (this.ohEnv.ohStage.stageType === import_config5.OPEN_HI_STAGE.PROD) {
1888
4265
  return void 0;
1889
4266
  }
1890
4267
  const client = new CognitoFixtureSeederClient(this, {
@@ -1921,62 +4298,62 @@ _OpenHiAuthService.SERVICE_TYPE = "auth";
1921
4298
  var OpenHiAuthService = _OpenHiAuthService;
1922
4299
 
1923
4300
  // src/services/open-hi-rest-api-service.ts
1924
- var import_config5 = __toESM(require_lib());
4301
+ var import_config6 = __toESM(require_lib());
1925
4302
  var import_aws_apigatewayv22 = require("aws-cdk-lib/aws-apigatewayv2");
1926
4303
  var import_aws_apigatewayv2_authorizers = require("aws-cdk-lib/aws-apigatewayv2-authorizers");
1927
4304
  var import_aws_apigatewayv2_integrations = require("aws-cdk-lib/aws-apigatewayv2-integrations");
1928
- var import_aws_iam2 = require("aws-cdk-lib/aws-iam");
4305
+ var import_aws_iam7 = require("aws-cdk-lib/aws-iam");
1929
4306
  var import_aws_route533 = require("aws-cdk-lib/aws-route53");
1930
4307
  var import_aws_route53_targets = require("aws-cdk-lib/aws-route53-targets");
1931
4308
  var import_core3 = require("aws-cdk-lib/core");
1932
4309
 
1933
4310
  // src/data/lambda/cors-options-lambda.ts
1934
- var import_node_fs6 = __toESM(require("fs"));
1935
- var import_node_path6 = __toESM(require("path"));
1936
- var import_aws_lambda6 = require("aws-cdk-lib/aws-lambda");
1937
- var import_aws_lambda_nodejs6 = require("aws-cdk-lib/aws-lambda-nodejs");
1938
- var import_constructs8 = require("constructs");
1939
- var HANDLER_NAME6 = "cors-options-lambda.handler.js";
1940
- function resolveHandlerEntry6(dirname) {
1941
- const sameDir = import_node_path6.default.join(dirname, HANDLER_NAME6);
1942
- if (import_node_fs6.default.existsSync(sameDir)) {
4311
+ var import_node_fs10 = __toESM(require("fs"));
4312
+ var import_node_path10 = __toESM(require("path"));
4313
+ var import_aws_lambda10 = require("aws-cdk-lib/aws-lambda");
4314
+ var import_aws_lambda_nodejs10 = require("aws-cdk-lib/aws-lambda-nodejs");
4315
+ var import_constructs17 = require("constructs");
4316
+ var HANDLER_NAME10 = "cors-options-lambda.handler.js";
4317
+ function resolveHandlerEntry10(dirname) {
4318
+ const sameDir = import_node_path10.default.join(dirname, HANDLER_NAME10);
4319
+ if (import_node_fs10.default.existsSync(sameDir)) {
1943
4320
  return sameDir;
1944
4321
  }
1945
- const fromLib = import_node_path6.default.join(dirname, "..", "..", "..", "lib", HANDLER_NAME6);
4322
+ const fromLib = import_node_path10.default.join(dirname, "..", "..", "..", "lib", HANDLER_NAME10);
1946
4323
  return fromLib;
1947
4324
  }
1948
- var CorsOptionsLambda = class extends import_constructs8.Construct {
4325
+ var CorsOptionsLambda = class extends import_constructs17.Construct {
1949
4326
  constructor(scope, id = "cors-options-lambda") {
1950
4327
  super(scope, id);
1951
- this.lambda = new import_aws_lambda_nodejs6.NodejsFunction(this, "handler", {
1952
- entry: resolveHandlerEntry6(__dirname),
1953
- runtime: import_aws_lambda6.Runtime.NODEJS_LATEST,
4328
+ this.lambda = new import_aws_lambda_nodejs10.NodejsFunction(this, "handler", {
4329
+ entry: resolveHandlerEntry10(__dirname),
4330
+ runtime: import_aws_lambda10.Runtime.NODEJS_LATEST,
1954
4331
  memorySize: 128
1955
4332
  });
1956
4333
  }
1957
4334
  };
1958
4335
 
1959
4336
  // src/data/lambda/rest-api-lambda.ts
1960
- var import_node_fs7 = __toESM(require("fs"));
1961
- var import_node_path7 = __toESM(require("path"));
1962
- var import_aws_lambda7 = require("aws-cdk-lib/aws-lambda");
1963
- var import_aws_lambda_nodejs7 = require("aws-cdk-lib/aws-lambda-nodejs");
1964
- var import_constructs9 = require("constructs");
1965
- var HANDLER_NAME7 = "rest-api-lambda.handler.js";
1966
- function resolveHandlerEntry7(dirname) {
1967
- const sameDir = import_node_path7.default.join(dirname, HANDLER_NAME7);
1968
- if (import_node_fs7.default.existsSync(sameDir)) {
4337
+ var import_node_fs11 = __toESM(require("fs"));
4338
+ var import_node_path11 = __toESM(require("path"));
4339
+ var import_aws_lambda11 = require("aws-cdk-lib/aws-lambda");
4340
+ var import_aws_lambda_nodejs11 = require("aws-cdk-lib/aws-lambda-nodejs");
4341
+ var import_constructs18 = require("constructs");
4342
+ var HANDLER_NAME11 = "rest-api-lambda.handler.js";
4343
+ function resolveHandlerEntry11(dirname) {
4344
+ const sameDir = import_node_path11.default.join(dirname, HANDLER_NAME11);
4345
+ if (import_node_fs11.default.existsSync(sameDir)) {
1969
4346
  return sameDir;
1970
4347
  }
1971
- const fromLib = import_node_path7.default.join(dirname, "..", "..", "..", "lib", HANDLER_NAME7);
4348
+ const fromLib = import_node_path11.default.join(dirname, "..", "..", "..", "lib", HANDLER_NAME11);
1972
4349
  return fromLib;
1973
4350
  }
1974
- var RestApiLambda = class extends import_constructs9.Construct {
4351
+ var RestApiLambda = class extends import_constructs18.Construct {
1975
4352
  constructor(scope, props) {
1976
4353
  super(scope, "rest-api-lambda");
1977
- this.lambda = new import_aws_lambda_nodejs7.NodejsFunction(this, "handler", {
1978
- entry: resolveHandlerEntry7(__dirname),
1979
- runtime: import_aws_lambda7.Runtime.NODEJS_LATEST,
4354
+ this.lambda = new import_aws_lambda_nodejs11.NodejsFunction(this, "handler", {
4355
+ entry: resolveHandlerEntry11(__dirname),
4356
+ runtime: import_aws_lambda11.Runtime.NODEJS_LATEST,
1980
4357
  memorySize: 1024,
1981
4358
  environment: {
1982
4359
  DYNAMO_TABLE_NAME: props.dynamoTableName,
@@ -2118,8 +4495,8 @@ var _OpenHiRestApiService = class _OpenHiRestApiService extends OpenHiService {
2118
4495
  postgresSchema
2119
4496
  });
2120
4497
  lambda.addToRolePolicy(
2121
- new import_aws_iam2.PolicyStatement({
2122
- effect: import_aws_iam2.Effect.ALLOW,
4498
+ new import_aws_iam7.PolicyStatement({
4499
+ effect: import_aws_iam7.Effect.ALLOW,
2123
4500
  actions: [
2124
4501
  "rds-data:ExecuteStatement",
2125
4502
  "rds-data:BatchExecuteStatement"
@@ -2128,8 +4505,8 @@ var _OpenHiRestApiService = class _OpenHiRestApiService extends OpenHiService {
2128
4505
  })
2129
4506
  );
2130
4507
  lambda.addToRolePolicy(
2131
- new import_aws_iam2.PolicyStatement({
2132
- effect: import_aws_iam2.Effect.ALLOW,
4508
+ new import_aws_iam7.PolicyStatement({
4509
+ effect: import_aws_iam7.Effect.ALLOW,
2133
4510
  actions: ["secretsmanager:GetSecretValue"],
2134
4511
  resources: [postgresSecretArn]
2135
4512
  })
@@ -2147,15 +4524,15 @@ var _OpenHiRestApiService = class _OpenHiRestApiService extends OpenHiService {
2147
4524
  ];
2148
4525
  dataStoreTable.grant(lambda, ...dynamoActions);
2149
4526
  lambda.addToRolePolicy(
2150
- new import_aws_iam2.PolicyStatement({
2151
- effect: import_aws_iam2.Effect.ALLOW,
4527
+ new import_aws_iam7.PolicyStatement({
4528
+ effect: import_aws_iam7.Effect.ALLOW,
2152
4529
  actions: [...dynamoActions],
2153
4530
  resources: [`${dataStoreTable.tableArn}/index/*`]
2154
4531
  })
2155
4532
  );
2156
4533
  lambda.addToRolePolicy(
2157
- new import_aws_iam2.PolicyStatement({
2158
- effect: import_aws_iam2.Effect.ALLOW,
4534
+ new import_aws_iam7.PolicyStatement({
4535
+ effect: import_aws_iam7.Effect.ALLOW,
2159
4536
  actions: [
2160
4537
  "ssm:GetParameter",
2161
4538
  "ssm:GetParameters",
@@ -2214,7 +4591,7 @@ var _OpenHiRestApiService = class _OpenHiRestApiService extends OpenHiService {
2214
4591
  const userPool = OpenHiAuthService.userPoolFromConstruct(this);
2215
4592
  const userPoolClient = OpenHiAuthService.userPoolClientFromConstruct(this);
2216
4593
  const userPoolClients = [userPoolClient];
2217
- if (this.ohEnv.ohStage.stageType !== import_config5.OPEN_HI_STAGE.PROD) {
4594
+ if (this.ohEnv.ohStage.stageType !== import_config6.OPEN_HI_STAGE.PROD) {
2218
4595
  userPoolClients.push(
2219
4596
  OpenHiAuthService.fixtureSeederClientFromConstruct(this)
2220
4597
  );
@@ -2304,20 +4681,36 @@ _OpenHiGraphqlService.SERVICE_TYPE = "graphql-api";
2304
4681
  var OpenHiGraphqlService = _OpenHiGraphqlService;
2305
4682
  // Annotate the CommonJS export names for ESM import in node:
2306
4683
  0 && (module.exports = {
4684
+ BRIDGED_STATUSES,
4685
+ CLOUDFORMATION_EVENT_SOURCE,
4686
+ CLOUDFORMATION_STACK_STATUS_CHANGE_DETAIL_TYPE,
4687
+ CONTROL_EVENT_BUS_NAME_ENV_VAR,
2307
4688
  ChildHostedZone,
2308
4689
  CognitoFixtureSeederClient,
2309
4690
  CognitoUserPool,
2310
4691
  CognitoUserPoolClient,
2311
4692
  CognitoUserPoolDomain,
2312
4693
  CognitoUserPoolKmsKey,
4694
+ ControlEventBus,
2313
4695
  DATA_STORE_CHANGE_DETAIL_MAX_UTF8_BYTES,
2314
4696
  DATA_STORE_CHANGE_DETAIL_TYPE,
2315
4697
  DATA_STORE_CHANGE_EVENT_SOURCE,
4698
+ DEMO_PERIOD,
4699
+ DEMO_TENANT_SPECS,
4700
+ DEMO_URN_SYSTEM,
4701
+ DEV_USERS,
2316
4702
  DataEventBus,
2317
4703
  DataStoreHistoricalArchive,
2318
4704
  DataStorePostgresReplica,
2319
4705
  DiscoverableStringParameter,
2320
4706
  DynamoDbDataStore,
4707
+ OPENHI_REPO_TAG_KEY_ENV_VAR,
4708
+ OPENHI_RESOURCE_URN_SYSTEM,
4709
+ OPENHI_TAG_KEY_PREFIX_ENV_VAR,
4710
+ OPENHI_TAG_SUFFIX_BRANCH_NAME,
4711
+ OPENHI_TAG_SUFFIX_REPO_NAME,
4712
+ OPENHI_TAG_SUFFIX_SERVICE_TYPE,
4713
+ OPENHI_TAG_SUFFIX_STAGE_TYPE,
2321
4714
  OpenHiApp,
2322
4715
  OpenHiAuthService,
2323
4716
  OpenHiDataService,
@@ -2328,21 +4721,59 @@ var OpenHiGraphqlService = _OpenHiGraphqlService;
2328
4721
  OpenHiService,
2329
4722
  OpenHiStage,
2330
4723
  OpsEventBus,
4724
+ PLACEHOLDER_TENANT_ID,
4725
+ PLACEHOLDER_WORKSPACE_ID,
4726
+ PLATFORM_DEPLOY_BRIDGE_ACTOR_SYSTEM,
4727
+ PLATFORM_SCOPE_TENANT_ID,
2331
4728
  POSTGRES_REPLICA_CLUSTER_ARN_SSM_NAME,
2332
4729
  POSTGRES_REPLICA_DATABASE_NAME_SSM_NAME,
2333
4730
  POSTGRES_REPLICA_SECRET_ARN_SSM_NAME,
4731
+ PROVISION_DEFAULT_WORKSPACE_DETAIL_TYPE,
4732
+ PlatformDeployBridge,
4733
+ PlatformDeployBridgeLambda,
4734
+ PlatformDeploymentCompletedV1,
2334
4735
  PostAuthenticationLambda,
2335
4736
  PostConfirmationLambda,
2336
4737
  PreTokenGenerationLambda,
4738
+ ProvisionDefaultWorkspaceLambda,
2337
4739
  REST_API_BASE_URL_SSM_NAME,
2338
4740
  RootGraphqlApi,
2339
4741
  RootHostedZone,
2340
4742
  RootHttpApi,
2341
4743
  RootWildcardCertificate,
4744
+ SEED_DEMO_DATA_CONSUMER_NAME,
4745
+ SEED_SYSTEM_DATA_ACTOR_SYSTEM,
4746
+ SEED_SYSTEM_DATA_CONSUMER_NAME,
4747
+ SEED_SYSTEM_DATA_CONTROL_BUS_ENV_VAR,
2342
4748
  STATIC_HOSTING_SERVICE_TYPE,
4749
+ SeedDemoDataLambda,
4750
+ SeedDemoDataWorkflow,
4751
+ SeedSystemDataLambda,
4752
+ SeedSystemDataWorkflow,
2343
4753
  StaticHosting,
4754
+ USER_ONBOARDING_EVENT_SOURCE,
4755
+ UserOnboardingWorkflow,
4756
+ WorkflowDedupConsumerNameInvalidError,
4757
+ WorkflowDedupTable,
4758
+ WorkflowDedupTableDuplicateError,
2344
4759
  buildFhirCurrentResourceChangeDetail,
4760
+ buildProvisionDefaultWorkspaceRequestedDetail,
4761
+ demoBasePartitionKeys,
4762
+ demoDevUserPartitionKeys,
4763
+ demoMembershipId,
4764
+ demoMembershipPartitionKey,
4765
+ demoRoleAssignmentId,
4766
+ demoRoleAssignmentPartitionKey,
4767
+ demoRolesForUserInTenant,
4768
+ demoScenarioIdentifier,
4769
+ demoTenantPartitionKey,
4770
+ demoUserPartitionKey,
4771
+ demoWorkspacePartitionKey,
2345
4772
  getDynamoDbDataStoreTableName,
2346
- getPostgresReplicaSchemaName
4773
+ getPostgresReplicaSchemaName,
4774
+ getWorkflowDedupTableName,
4775
+ openHiTagKey,
4776
+ openhiResourceIdentifier,
4777
+ rolePartitionKey
2347
4778
  });
2348
4779
  //# sourceMappingURL=index.js.map