@openhi/constructs 0.0.152 → 0.0.154

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.
@@ -97,13 +97,40 @@ declare const seedDemoGraph: (params: {
97
97
  */
98
98
  declare const runSeedDemoData: (event: SeedDemoDataEvent, deps: SeedDemoDataDependencies, devUsers: ReadonlyArray<DemoDevUser>) => Promise<void>;
99
99
  /**
100
- * Deterministic password derived from the user's email. Re-running
101
- * the algorithm with the same email reproduces the password, so devs
102
- * can recover their own credentials from the docs page without the
103
- * workflow ever surfacing them. The shape satisfies the default
104
- * Cognito password policy (≥8 chars, upper + lower + number + symbol).
100
+ * SSM parameter-path prefix that hosts every seeded dev user's
101
+ * password. The full path for a single user is
102
+ * `${SEED_USER_PASSWORD_PARAMETER_PREFIX}<email_safe>/password`
103
+ * where `<email_safe>` is `<user>_at_<domain>` (see
104
+ * {@link emailToSsmPath}). The trailing `/password` segment leaves
105
+ * room for additional per-user parameters under the same email
106
+ * prefix in future phases without renaming the existing entries.
105
107
  */
106
- declare const devPasswordForEmail: (email: string) => string;
108
+ declare const SEED_USER_PASSWORD_PARAMETER_PREFIX = "/openhi/seed/users/";
109
+ /**
110
+ * Map an email address to its canonical SSM parameter path. The
111
+ * `@` separator is replaced with the literal sentinel `_at_` so
112
+ * the resulting path is a single SSM-legal name. Throws when the
113
+ * email is empty, missing exactly one `@`, or contains characters
114
+ * that would produce an invalid SSM path.
115
+ *
116
+ * Example: `alice@codedrifters.com` →
117
+ * `/openhi/seed/users/alice_at_codedrifters.com/password`.
118
+ */
119
+ declare const emailToSsmPath: (email: string) => string;
120
+ /**
121
+ * Test seam: reset the module-scoped SSM client cache. Unit tests
122
+ * that swap in different mocks across `it` blocks call this in
123
+ * `beforeEach` so each test gets a fresh client instance.
124
+ */
125
+ declare const __resetSsmClientForTests: () => void;
126
+ /**
127
+ * Read a seeded dev user's password from SSM Parameter Store. The
128
+ * parameter is expected to be a `SecureString` provisioned out of
129
+ * band by an operator (see the runbook in phase 3 of #1249). On
130
+ * `ParameterNotFound`, the error message includes the exact
131
+ * expected path so the operator can copy-paste-fix.
132
+ */
133
+ declare const fetchSeedUserPassword: (email: string) => Promise<string>;
107
134
  /**
108
135
  * Production Cognito provisioner backed by the AWS SDK. Reads the
109
136
  * user-pool id from the env var the lambda construct injects.
@@ -111,13 +138,21 @@ declare const devPasswordForEmail: (email: string) => string;
111
138
  * Idempotency contract:
112
139
  * - On first invocation, calls `AdminCreateUser` (with
113
140
  * `MessageAction: SUPPRESS` so no invitation email fires) then
114
- * `AdminSetUserPassword` (permanent). Returns the new user's sub.
141
+ * `AdminSetUserPassword` (permanent, from the SSM-sourced
142
+ * value). Returns the new user's sub.
115
143
  * - On subsequent invocations, `AdminCreateUser` throws
116
144
  * `UsernameExistsException`; the provisioner catches it, calls
117
- * `AdminGetUser` to read the existing user's sub, and returns
118
- * **without** touching the password or any attribute.
145
+ * `AdminGetUser` to read the existing user's sub, and **also
146
+ * re-applies the current SSM-sourced password** via
147
+ * `AdminSetUserPassword`. This is the rotation seam: operators
148
+ * update the SSM SecureString value and re-publish the seed
149
+ * event to push a fresh password down to Cognito.
150
+ *
151
+ * The SSM password is fetched once per email up-front so the new-
152
+ * user and rotation branches share the same value and at most one
153
+ * `GetParameter` call lands per `ensureUser` invocation.
119
154
  */
120
155
  declare const productionCognitoProvisioner: () => CognitoProvisioner;
121
156
  declare const handler: (event: SeedDemoDataEvent) => Promise<void>;
122
157
 
123
- export { type CognitoProvisioner, SEED_DEMO_DATA_USER_POOL_ID_ENV_VAR, type SeedDemoDataDependencies, devPasswordForEmail, handler, productionCognitoProvisioner, runSeedDemoData, seedDemoGraph };
158
+ export { type CognitoProvisioner, SEED_DEMO_DATA_USER_POOL_ID_ENV_VAR, SEED_USER_PASSWORD_PARAMETER_PREFIX, type SeedDemoDataDependencies, __resetSsmClientForTests, emailToSsmPath, fetchSeedUserPassword, handler, productionCognitoProvisioner, runSeedDemoData, seedDemoGraph };
@@ -97,13 +97,40 @@ declare const seedDemoGraph: (params: {
97
97
  */
98
98
  declare const runSeedDemoData: (event: SeedDemoDataEvent, deps: SeedDemoDataDependencies, devUsers: ReadonlyArray<DemoDevUser>) => Promise<void>;
99
99
  /**
100
- * Deterministic password derived from the user's email. Re-running
101
- * the algorithm with the same email reproduces the password, so devs
102
- * can recover their own credentials from the docs page without the
103
- * workflow ever surfacing them. The shape satisfies the default
104
- * Cognito password policy (≥8 chars, upper + lower + number + symbol).
100
+ * SSM parameter-path prefix that hosts every seeded dev user's
101
+ * password. The full path for a single user is
102
+ * `${SEED_USER_PASSWORD_PARAMETER_PREFIX}<email_safe>/password`
103
+ * where `<email_safe>` is `<user>_at_<domain>` (see
104
+ * {@link emailToSsmPath}). The trailing `/password` segment leaves
105
+ * room for additional per-user parameters under the same email
106
+ * prefix in future phases without renaming the existing entries.
105
107
  */
106
- declare const devPasswordForEmail: (email: string) => string;
108
+ declare const SEED_USER_PASSWORD_PARAMETER_PREFIX = "/openhi/seed/users/";
109
+ /**
110
+ * Map an email address to its canonical SSM parameter path. The
111
+ * `@` separator is replaced with the literal sentinel `_at_` so
112
+ * the resulting path is a single SSM-legal name. Throws when the
113
+ * email is empty, missing exactly one `@`, or contains characters
114
+ * that would produce an invalid SSM path.
115
+ *
116
+ * Example: `alice@codedrifters.com` →
117
+ * `/openhi/seed/users/alice_at_codedrifters.com/password`.
118
+ */
119
+ declare const emailToSsmPath: (email: string) => string;
120
+ /**
121
+ * Test seam: reset the module-scoped SSM client cache. Unit tests
122
+ * that swap in different mocks across `it` blocks call this in
123
+ * `beforeEach` so each test gets a fresh client instance.
124
+ */
125
+ declare const __resetSsmClientForTests: () => void;
126
+ /**
127
+ * Read a seeded dev user's password from SSM Parameter Store. The
128
+ * parameter is expected to be a `SecureString` provisioned out of
129
+ * band by an operator (see the runbook in phase 3 of #1249). On
130
+ * `ParameterNotFound`, the error message includes the exact
131
+ * expected path so the operator can copy-paste-fix.
132
+ */
133
+ declare const fetchSeedUserPassword: (email: string) => Promise<string>;
107
134
  /**
108
135
  * Production Cognito provisioner backed by the AWS SDK. Reads the
109
136
  * user-pool id from the env var the lambda construct injects.
@@ -111,13 +138,21 @@ declare const devPasswordForEmail: (email: string) => string;
111
138
  * Idempotency contract:
112
139
  * - On first invocation, calls `AdminCreateUser` (with
113
140
  * `MessageAction: SUPPRESS` so no invitation email fires) then
114
- * `AdminSetUserPassword` (permanent). Returns the new user's sub.
141
+ * `AdminSetUserPassword` (permanent, from the SSM-sourced
142
+ * value). Returns the new user's sub.
115
143
  * - On subsequent invocations, `AdminCreateUser` throws
116
144
  * `UsernameExistsException`; the provisioner catches it, calls
117
- * `AdminGetUser` to read the existing user's sub, and returns
118
- * **without** touching the password or any attribute.
145
+ * `AdminGetUser` to read the existing user's sub, and **also
146
+ * re-applies the current SSM-sourced password** via
147
+ * `AdminSetUserPassword`. This is the rotation seam: operators
148
+ * update the SSM SecureString value and re-publish the seed
149
+ * event to push a fresh password down to Cognito.
150
+ *
151
+ * The SSM password is fetched once per email up-front so the new-
152
+ * user and rotation branches share the same value and at most one
153
+ * `GetParameter` call lands per `ensureUser` invocation.
119
154
  */
120
155
  declare const productionCognitoProvisioner: () => CognitoProvisioner;
121
156
  declare const handler: (event: SeedDemoDataEvent) => Promise<void>;
122
157
 
123
- export { type CognitoProvisioner, SEED_DEMO_DATA_USER_POOL_ID_ENV_VAR, type SeedDemoDataDependencies, devPasswordForEmail, handler, productionCognitoProvisioner, runSeedDemoData, seedDemoGraph };
158
+ export { type CognitoProvisioner, SEED_DEMO_DATA_USER_POOL_ID_ENV_VAR, SEED_USER_PASSWORD_PARAMETER_PREFIX, type SeedDemoDataDependencies, __resetSsmClientForTests, emailToSsmPath, fetchSeedUserPassword, handler, productionCognitoProvisioner, runSeedDemoData, seedDemoGraph };
@@ -707,16 +707,19 @@ 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
 
@@ -5412,10 +5415,66 @@ var runSeedDemoData = async (event, deps, devUsers) => {
5412
5415
  throw err;
5413
5416
  }
5414
5417
  };
5415
- var devPasswordForEmail = (email) => {
5416
- const digest = (0, import_node_crypto.createHash)("sha256").update(email).digest();
5417
- const base64url = digest.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
5418
- 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
+ }
5419
5478
  };
5420
5479
  var productionCognitoProvisioner = () => {
5421
5480
  const client = new import_client_cognito_identity_provider.CognitoIdentityProviderClient({});
@@ -5433,8 +5492,19 @@ var productionCognitoProvisioner = () => {
5433
5492
  }
5434
5493
  return void 0;
5435
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
+ };
5436
5505
  return {
5437
5506
  ensureUser: async (email) => {
5507
+ const password = await fetchSeedUserPassword(email);
5438
5508
  try {
5439
5509
  const created = await client.send(
5440
5510
  new import_client_cognito_identity_provider.AdminCreateUserCommand({
@@ -5447,14 +5517,7 @@ var productionCognitoProvisioner = () => {
5447
5517
  ]
5448
5518
  })
5449
5519
  );
5450
- await client.send(
5451
- new import_client_cognito_identity_provider.AdminSetUserPasswordCommand({
5452
- UserPoolId: userPoolId,
5453
- Username: email,
5454
- Password: devPasswordForEmail(email),
5455
- Permanent: true
5456
- })
5457
- );
5520
+ await setPassword(email, password);
5458
5521
  const sub = subFromAttributes(created.User?.Attributes);
5459
5522
  if (!sub) {
5460
5523
  throw new Error(
@@ -5464,6 +5527,7 @@ var productionCognitoProvisioner = () => {
5464
5527
  return sub;
5465
5528
  } catch (err) {
5466
5529
  if (err instanceof import_client_cognito_identity_provider.UsernameExistsException) {
5530
+ await setPassword(email, password);
5467
5531
  const got = await client.send(
5468
5532
  new import_client_cognito_identity_provider.AdminGetUserCommand({
5469
5533
  UserPoolId: userPoolId,
@@ -5497,7 +5561,10 @@ var handler = async (event) => runSeedDemoData(event, productionDependencies(),
5497
5561
  // Annotate the CommonJS export names for ESM import in node:
5498
5562
  0 && (module.exports = {
5499
5563
  SEED_DEMO_DATA_USER_POOL_ID_ENV_VAR,
5500
- devPasswordForEmail,
5564
+ SEED_USER_PASSWORD_PARAMETER_PREFIX,
5565
+ __resetSsmClientForTests,
5566
+ emailToSsmPath,
5567
+ fetchSeedUserPassword,
5501
5568
  handler,
5502
5569
  productionCognitoProvisioner,
5503
5570
  runSeedDemoData,