@openhi/constructs 0.0.151 → 0.0.153

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.
@@ -707,27 +707,34 @@ var require_lib = __commonJS({
707
707
  var seed_demo_data_handler_exports = {};
708
708
  __export(seed_demo_data_handler_exports, {
709
709
  SEED_DEMO_DATA_USER_POOL_ID_ENV_VAR: () => SEED_DEMO_DATA_USER_POOL_ID_ENV_VAR,
710
- devPasswordForEmail: () => devPasswordForEmail,
710
+ SEED_USER_PASSWORD_PARAMETER_PREFIX: () => SEED_USER_PASSWORD_PARAMETER_PREFIX,
711
+ __resetSsmClientForTests: () => __resetSsmClientForTests,
712
+ emailToSsmPath: () => emailToSsmPath,
713
+ fetchSeedUserPassword: () => fetchSeedUserPassword,
711
714
  handler: () => handler,
712
715
  productionCognitoProvisioner: () => productionCognitoProvisioner,
713
716
  runSeedDemoData: () => runSeedDemoData,
714
717
  seedDemoGraph: () => seedDemoGraph
715
718
  });
716
719
  module.exports = __toCommonJS(seed_demo_data_handler_exports);
717
- var import_node_crypto = require("crypto");
718
720
  var import_client_cognito_identity_provider = require("@aws-sdk/client-cognito-identity-provider");
719
721
  var import_client_dynamodb2 = require("@aws-sdk/client-dynamodb");
722
+ var import_client_ssm = require("@aws-sdk/client-ssm");
720
723
  var import_types12 = require("@openhi/types");
721
724
  var import_workflows2 = __toESM(require_lib());
722
725
 
723
726
  // src/workflows/control-plane/seed-demo-data/events.ts
724
727
  var import_types = require("@openhi/types");
725
728
  var import_workflows = __toESM(require_lib());
729
+
730
+ // src/data/operations/control/membership-constraints/platform-scope-tenant-id.ts
731
+ var PLATFORM_SCOPE_TENANT_ID = "platform";
732
+
733
+ // src/workflows/control-plane/seed-demo-data/events.ts
726
734
  var SEED_DEMO_DATA_CONSUMER_NAME = "seed-demo-data";
727
735
  var DEMO_URN_SYSTEM = "urn:openhi:demo";
728
736
  var OPENHI_RESOURCE_URN_SYSTEM = "http://openhi.org/";
729
737
  var DEMO_PERIOD = { start: "2026-01-01T00:00:00Z" };
730
- var PLATFORM_SCOPE_TENANT_ID = "platform";
731
738
  var PLACEHOLDER_TENANT_ID = "placeholder-tenant-id";
732
739
  var PLACEHOLDER_WORKSPACE_ID = "placeholder-workspace-id";
733
740
  var DEV_USERS = [
@@ -3510,6 +3517,9 @@ function buildRoleAssignmentWorkspaceProjectionItem(input) {
3510
3517
  var TENANT_LANE_SK_PREFIX = "MEMBERSHIP#TENANT#";
3511
3518
  async function assertUserHasTenantMembershipOperation(params) {
3512
3519
  const { userId, tenantId, tableName } = params;
3520
+ if (tenantId === PLATFORM_SCOPE_TENANT_ID) {
3521
+ return;
3522
+ }
3513
3523
  const service = getDynamoControlService(tableName);
3514
3524
  const result = await service.entities.membershipUserProjection.query.record({ userId }).begins({ sk: TENANT_LANE_SK_PREFIX }).go();
3515
3525
  const matched = (result.data ?? []).some((row) => row.tenantId === tenantId);
@@ -5017,6 +5027,25 @@ var errorMessage = (err) => {
5017
5027
  }
5018
5028
  return String(err);
5019
5029
  };
5030
+ var tryRun = async (failures, phase, scope, resourceType, resourceId, fn) => {
5031
+ try {
5032
+ await fn();
5033
+ return true;
5034
+ } catch (err) {
5035
+ failures.push({ phase, scope, resourceType, resourceId, error: err });
5036
+ return false;
5037
+ }
5038
+ };
5039
+ var aggregateFailureError = (failures) => {
5040
+ const summary = failures.map(
5041
+ (f) => `${f.phase} ${f.scope}/${f.resourceType}/${f.resourceId}: ${errorMessage(
5042
+ f.error
5043
+ )}`
5044
+ ).join("; ");
5045
+ return new Error(
5046
+ `seed-demo-data: ${failures.length} item(s) failed across phases: ${summary}`
5047
+ );
5048
+ };
5020
5049
  var idForRoleCode = (code) => {
5021
5050
  for (const key of Object.keys(import_types12.PLATFORM_ROLE_IDS)) {
5022
5051
  if (import_types12.PLATFORM_ROLE_CONCEPTS[key].code === code) {
@@ -5126,95 +5155,180 @@ var upsertUser = async (context, user, cognitoSub) => {
5126
5155
  lastUpdated: context.date ?? (/* @__PURE__ */ new Date()).toISOString()
5127
5156
  }).go();
5128
5157
  };
5129
- var seedWorkspaceDataPlane = async (baseContext, group) => {
5158
+ var seedWorkspaceDataPlane = async (baseContext, group, failures) => {
5130
5159
  const workspaceContext = {
5131
5160
  ...baseContext,
5132
5161
  tenantId: group.tenantId,
5133
5162
  workspaceId: group.workspaceId
5134
5163
  };
5164
+ const scope = `${group.tenantId}/${group.workspaceId}`;
5135
5165
  for (const patient of group.patients) {
5136
- await createPatientOperation({
5137
- context: workspaceContext,
5138
- body: patient
5139
- });
5166
+ await tryRun(
5167
+ failures,
5168
+ "phase-3",
5169
+ scope,
5170
+ "Patient",
5171
+ patient.id ?? "",
5172
+ () => createPatientOperation({
5173
+ context: workspaceContext,
5174
+ body: patient
5175
+ })
5176
+ );
5140
5177
  }
5141
5178
  for (const practitioner of group.practitioners) {
5142
- await createPractitionerOperation({
5143
- context: workspaceContext,
5144
- body: practitioner
5145
- });
5179
+ await tryRun(
5180
+ failures,
5181
+ "phase-3",
5182
+ scope,
5183
+ "Practitioner",
5184
+ practitioner.id ?? "",
5185
+ () => createPractitionerOperation({
5186
+ context: workspaceContext,
5187
+ body: practitioner
5188
+ })
5189
+ );
5146
5190
  }
5147
5191
  for (const observation of group.observations) {
5148
- await createObservationOperation({
5149
- context: workspaceContext,
5150
- body: observation
5151
- });
5192
+ await tryRun(
5193
+ failures,
5194
+ "phase-3",
5195
+ scope,
5196
+ "Observation",
5197
+ observation.id ?? "",
5198
+ () => createObservationOperation({
5199
+ context: workspaceContext,
5200
+ body: observation
5201
+ })
5202
+ );
5152
5203
  }
5153
5204
  for (const encounter of group.encounters) {
5154
- await createEncounterOperation({
5155
- context: workspaceContext,
5156
- body: encounter
5157
- });
5205
+ await tryRun(
5206
+ failures,
5207
+ "phase-3",
5208
+ scope,
5209
+ "Encounter",
5210
+ encounter.id ?? "",
5211
+ () => createEncounterOperation({
5212
+ context: workspaceContext,
5213
+ body: encounter
5214
+ })
5215
+ );
5158
5216
  }
5159
5217
  for (const account of group.accounts) {
5160
- await createAccountOperation({
5161
- context: workspaceContext,
5162
- body: account
5163
- });
5218
+ await tryRun(
5219
+ failures,
5220
+ "phase-3",
5221
+ scope,
5222
+ "Account",
5223
+ account.id ?? "",
5224
+ () => createAccountOperation({
5225
+ context: workspaceContext,
5226
+ body: account
5227
+ })
5228
+ );
5164
5229
  }
5165
5230
  };
5166
5231
  var seedDemoGraph = async (params) => {
5167
5232
  const { baseContext, devUsers, cognito } = params;
5233
+ const failures = [];
5168
5234
  for (const spec of DEMO_TENANT_SPECS) {
5169
5235
  const tenantContext = {
5170
5236
  ...baseContext,
5171
5237
  tenantId: spec.tenantId
5172
5238
  };
5173
- await createTenantOperation({
5174
- context: tenantContext,
5175
- body: { id: spec.tenantId, resource: tenantResourceBody(spec) }
5176
- });
5177
- for (const workspace of spec.workspaces) {
5178
- await createWorkspaceOperation({
5239
+ await tryRun(
5240
+ failures,
5241
+ "phase-1",
5242
+ spec.tenantId,
5243
+ "Tenant",
5244
+ spec.tenantId,
5245
+ () => createTenantOperation({
5179
5246
  context: tenantContext,
5180
- body: {
5181
- id: workspace.id,
5182
- resource: workspaceResourceBody(spec, workspace)
5183
- }
5184
- });
5247
+ body: { id: spec.tenantId, resource: tenantResourceBody(spec) }
5248
+ })
5249
+ );
5250
+ for (const workspace of spec.workspaces) {
5251
+ await tryRun(
5252
+ failures,
5253
+ "phase-1",
5254
+ spec.tenantId,
5255
+ "Workspace",
5256
+ workspace.id,
5257
+ () => createWorkspaceOperation({
5258
+ context: tenantContext,
5259
+ body: {
5260
+ id: workspace.id,
5261
+ resource: workspaceResourceBody(spec, workspace)
5262
+ }
5263
+ })
5264
+ );
5185
5265
  }
5186
5266
  }
5187
5267
  for (const user of devUsers) {
5188
- const cognitoSub = await cognito.ensureUser(user.email);
5189
- await upsertUser(baseContext, user, cognitoSub);
5268
+ let cognitoSub;
5269
+ try {
5270
+ cognitoSub = await cognito.ensureUser(user.email);
5271
+ } catch (err) {
5272
+ failures.push({
5273
+ phase: "phase-2",
5274
+ scope: user.id,
5275
+ resourceType: "CognitoUser",
5276
+ resourceId: user.email,
5277
+ error: err
5278
+ });
5279
+ continue;
5280
+ }
5281
+ await tryRun(
5282
+ failures,
5283
+ "phase-2",
5284
+ user.id,
5285
+ "User",
5286
+ user.id,
5287
+ () => upsertUser(baseContext, user, cognitoSub)
5288
+ );
5190
5289
  for (const spec of DEMO_TENANT_SPECS) {
5191
5290
  const tenantContext = {
5192
5291
  ...baseContext,
5193
5292
  tenantId: spec.tenantId
5194
5293
  };
5294
+ const userScope = `${user.id}@${spec.tenantId}`;
5195
5295
  const membershipId = demoMembershipId(user.id, spec.tenantId);
5196
- await createMembershipOperation({
5197
- context: tenantContext,
5198
- body: {
5199
- id: membershipId,
5200
- resource: membershipResourceBody(spec, user, membershipId)
5201
- }
5202
- });
5203
- for (const roleCode of demoRolesForUserInTenant(user, spec.tenantId)) {
5204
- const raId = demoRoleAssignmentId(user.id, spec.tenantId, roleCode);
5205
- await createRoleAssignmentOperation({
5296
+ await tryRun(
5297
+ failures,
5298
+ "phase-2",
5299
+ userScope,
5300
+ "Membership",
5301
+ membershipId,
5302
+ () => createMembershipOperation({
5206
5303
  context: tenantContext,
5207
5304
  body: {
5208
- id: raId,
5209
- resource: roleAssignmentResourceBody(
5210
- spec.scenario,
5211
- spec.tenantId,
5212
- user,
5213
- roleCode,
5214
- raId
5215
- )
5305
+ id: membershipId,
5306
+ resource: membershipResourceBody(spec, user, membershipId)
5216
5307
  }
5217
- });
5308
+ })
5309
+ );
5310
+ for (const roleCode of demoRolesForUserInTenant(user, spec.tenantId)) {
5311
+ const raId = demoRoleAssignmentId(user.id, spec.tenantId, roleCode);
5312
+ await tryRun(
5313
+ failures,
5314
+ "phase-2",
5315
+ userScope,
5316
+ "RoleAssignment",
5317
+ raId,
5318
+ () => createRoleAssignmentOperation({
5319
+ context: tenantContext,
5320
+ body: {
5321
+ id: raId,
5322
+ resource: roleAssignmentResourceBody(
5323
+ spec.scenario,
5324
+ spec.tenantId,
5325
+ user,
5326
+ roleCode,
5327
+ raId
5328
+ )
5329
+ }
5330
+ })
5331
+ );
5218
5332
  }
5219
5333
  }
5220
5334
  const platformContext = {
@@ -5227,22 +5341,42 @@ var seedDemoGraph = async (params) => {
5227
5341
  PLATFORM_SCOPE_TENANT_ID,
5228
5342
  platformRoleCode
5229
5343
  );
5230
- await createRoleAssignmentOperation({
5231
- context: platformContext,
5232
- body: {
5233
- id: platformRaId,
5234
- resource: roleAssignmentResourceBody(
5235
- "platform",
5236
- PLATFORM_SCOPE_TENANT_ID,
5237
- user,
5238
- platformRoleCode,
5239
- platformRaId
5240
- )
5241
- }
5242
- });
5344
+ await tryRun(
5345
+ failures,
5346
+ "phase-2",
5347
+ `${user.id}@${PLATFORM_SCOPE_TENANT_ID}`,
5348
+ "RoleAssignment",
5349
+ platformRaId,
5350
+ () => createRoleAssignmentOperation({
5351
+ context: platformContext,
5352
+ body: {
5353
+ id: platformRaId,
5354
+ resource: roleAssignmentResourceBody(
5355
+ "platform",
5356
+ PLATFORM_SCOPE_TENANT_ID,
5357
+ user,
5358
+ platformRoleCode,
5359
+ platformRaId
5360
+ )
5361
+ }
5362
+ })
5363
+ );
5243
5364
  }
5244
5365
  for (const group of DEMO_DATA_PLANE_FIXTURES) {
5245
- await seedWorkspaceDataPlane(baseContext, group);
5366
+ try {
5367
+ await seedWorkspaceDataPlane(baseContext, group, failures);
5368
+ } catch (err) {
5369
+ failures.push({
5370
+ phase: "phase-3",
5371
+ scope: `${group.tenantId}/${group.workspaceId}`,
5372
+ resourceType: "Workspace",
5373
+ resourceId: group.workspaceId,
5374
+ error: err
5375
+ });
5376
+ }
5377
+ }
5378
+ if (failures.length > 0) {
5379
+ throw aggregateFailureError(failures);
5246
5380
  }
5247
5381
  };
5248
5382
  var runSeedDemoData = async (event, deps, devUsers) => {
@@ -5281,10 +5415,66 @@ var runSeedDemoData = async (event, deps, devUsers) => {
5281
5415
  throw err;
5282
5416
  }
5283
5417
  };
5284
- var devPasswordForEmail = (email) => {
5285
- const digest = (0, import_node_crypto.createHash)("sha256").update(email).digest();
5286
- const base64url = digest.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
5287
- return `Dev-${base64url.slice(0, 20)}!1`;
5418
+ var SEED_USER_PASSWORD_PARAMETER_PREFIX = "/openhi/seed/users/";
5419
+ var SSM_PATH_SEGMENT = /^[A-Za-z0-9_.-]+$/;
5420
+ var emailToSsmPath = (email) => {
5421
+ if (typeof email !== "string" || email.length === 0) {
5422
+ throw new Error(
5423
+ `emailToSsmPath: email must be a non-empty string (received "${String(email)}").`
5424
+ );
5425
+ }
5426
+ const atIdx = email.indexOf("@");
5427
+ if (atIdx === -1 || atIdx !== email.lastIndexOf("@")) {
5428
+ throw new Error(
5429
+ `emailToSsmPath: email "${email}" must contain exactly one "@" character.`
5430
+ );
5431
+ }
5432
+ const localPart = email.slice(0, atIdx);
5433
+ const domainPart = email.slice(atIdx + 1);
5434
+ if (localPart.length === 0 || domainPart.length === 0) {
5435
+ throw new Error(
5436
+ `emailToSsmPath: email "${email}" must have a non-empty local-part and domain.`
5437
+ );
5438
+ }
5439
+ if (!SSM_PATH_SEGMENT.test(localPart) || !SSM_PATH_SEGMENT.test(domainPart)) {
5440
+ throw new Error(
5441
+ `emailToSsmPath: email "${email}" contains characters that would produce an invalid SSM parameter path (only A-Z, a-z, 0-9, '.', '-', and '_' are allowed).`
5442
+ );
5443
+ }
5444
+ return `${SEED_USER_PASSWORD_PARAMETER_PREFIX}${localPart}_at_${domainPart}/password`;
5445
+ };
5446
+ var cachedSsmClient;
5447
+ var getSsmClient = () => {
5448
+ if (!cachedSsmClient) {
5449
+ cachedSsmClient = new import_client_ssm.SSMClient({});
5450
+ }
5451
+ return cachedSsmClient;
5452
+ };
5453
+ var __resetSsmClientForTests = () => {
5454
+ cachedSsmClient = void 0;
5455
+ };
5456
+ var fetchSeedUserPassword = async (email) => {
5457
+ const path = emailToSsmPath(email);
5458
+ const client = getSsmClient();
5459
+ try {
5460
+ const result = await client.send(
5461
+ new import_client_ssm.GetParameterCommand({ Name: path, WithDecryption: true })
5462
+ );
5463
+ const value = result.Parameter?.Value;
5464
+ if (typeof value !== "string" || value.length === 0) {
5465
+ throw new Error(
5466
+ `fetchSeedUserPassword: SSM parameter "${path}" returned an empty value.`
5467
+ );
5468
+ }
5469
+ return value;
5470
+ } catch (err) {
5471
+ if (err instanceof import_client_ssm.ParameterNotFound) {
5472
+ throw new Error(
5473
+ `fetchSeedUserPassword: SSM parameter "${path}" not found. Provision a SecureString at "${path}" with the dev user's password before re-running seed-demo-data.`
5474
+ );
5475
+ }
5476
+ throw err;
5477
+ }
5288
5478
  };
5289
5479
  var productionCognitoProvisioner = () => {
5290
5480
  const client = new import_client_cognito_identity_provider.CognitoIdentityProviderClient({});
@@ -5302,8 +5492,19 @@ var productionCognitoProvisioner = () => {
5302
5492
  }
5303
5493
  return void 0;
5304
5494
  };
5495
+ const setPassword = async (email, password) => {
5496
+ await client.send(
5497
+ new import_client_cognito_identity_provider.AdminSetUserPasswordCommand({
5498
+ UserPoolId: userPoolId,
5499
+ Username: email,
5500
+ Password: password,
5501
+ Permanent: true
5502
+ })
5503
+ );
5504
+ };
5305
5505
  return {
5306
5506
  ensureUser: async (email) => {
5507
+ const password = await fetchSeedUserPassword(email);
5307
5508
  try {
5308
5509
  const created = await client.send(
5309
5510
  new import_client_cognito_identity_provider.AdminCreateUserCommand({
@@ -5316,14 +5517,7 @@ var productionCognitoProvisioner = () => {
5316
5517
  ]
5317
5518
  })
5318
5519
  );
5319
- await client.send(
5320
- new import_client_cognito_identity_provider.AdminSetUserPasswordCommand({
5321
- UserPoolId: userPoolId,
5322
- Username: email,
5323
- Password: devPasswordForEmail(email),
5324
- Permanent: true
5325
- })
5326
- );
5520
+ await setPassword(email, password);
5327
5521
  const sub = subFromAttributes(created.User?.Attributes);
5328
5522
  if (!sub) {
5329
5523
  throw new Error(
@@ -5333,6 +5527,7 @@ var productionCognitoProvisioner = () => {
5333
5527
  return sub;
5334
5528
  } catch (err) {
5335
5529
  if (err instanceof import_client_cognito_identity_provider.UsernameExistsException) {
5530
+ await setPassword(email, password);
5336
5531
  const got = await client.send(
5337
5532
  new import_client_cognito_identity_provider.AdminGetUserCommand({
5338
5533
  UserPoolId: userPoolId,
@@ -5366,7 +5561,10 @@ var handler = async (event) => runSeedDemoData(event, productionDependencies(),
5366
5561
  // Annotate the CommonJS export names for ESM import in node:
5367
5562
  0 && (module.exports = {
5368
5563
  SEED_DEMO_DATA_USER_POOL_ID_ENV_VAR,
5369
- devPasswordForEmail,
5564
+ SEED_USER_PASSWORD_PARAMETER_PREFIX,
5565
+ __resetSsmClientForTests,
5566
+ emailToSsmPath,
5567
+ fetchSeedUserPassword,
5370
5568
  handler,
5371
5569
  productionCognitoProvisioner,
5372
5570
  runSeedDemoData,