@openhi/constructs 0.0.135 → 0.0.137

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.
package/lib/index.mjs CHANGED
@@ -358,12 +358,14 @@ import {
358
358
  } from "@codedrifters/utils";
359
359
  import { RemovalPolicy, Stack, Tags } from "aws-cdk-lib";
360
360
  import { paramCase } from "change-case";
361
+ var DEFAULT_RELEASE_BRANCH = "main";
362
+ var CHILD_ZONE_PREFIX_MAX_LENGTH = 56;
361
363
  var OPENHI_TAG_SUFFIX_REPO_NAME = "repo-name";
362
364
  var OPENHI_TAG_SUFFIX_BRANCH_NAME = "branch-name";
363
365
  var OPENHI_TAG_SUFFIX_SERVICE_TYPE = "service-type";
364
366
  var OPENHI_TAG_SUFFIX_STAGE_TYPE = "stage-type";
365
367
  var openHiTagKey = (appName, suffix) => `${appName}:${suffix}`;
366
- var OpenHiService = class extends Stack {
368
+ var OpenHiService = class _OpenHiService extends Stack {
367
369
  /**
368
370
  * Creates a new OpenHI service stack.
369
371
  *
@@ -383,8 +385,12 @@ var OpenHiService = class extends Stack {
383
385
  }
384
386
  const appName = props.appName ?? ohEnv.ohStage.ohApp.appName ?? "openhi";
385
387
  const repoName = props.repoName ?? findGitRepoName();
386
- const defaultReleaseBranch = props.defaultReleaseBranch ?? "main";
387
- const branchName = props.branchName ?? (process.env.JEST_WORKER_ID ? "test-branch" : process.env.GIT_BRANCH_NAME?.trim() || (ohEnv.ohStage.stageType === import_config3.OPEN_HI_STAGE.DEV ? findGitBranch() : defaultReleaseBranch));
388
+ const { branchName, defaultReleaseBranch } = _OpenHiService.resolveBranchContext(ohEnv, {
389
+ ...props.branchName !== void 0 && { branchName: props.branchName },
390
+ ...props.defaultReleaseBranch !== void 0 && {
391
+ defaultReleaseBranch: props.defaultReleaseBranch
392
+ }
393
+ });
388
394
  const environmentHash = hashString2(
389
395
  [appName, ohEnv.deploymentTargetRole, account, region].join("-"),
390
396
  6
@@ -459,6 +465,50 @@ var OpenHiService = class extends Stack {
459
465
  ohEnv.ohStage.stageType.slice(0, 255)
460
466
  );
461
467
  }
468
+ /**
469
+ * Compose the full per-deploy domain for an OpenHI service.
470
+ *
471
+ * On the release branch (`branchName === defaultReleaseBranch`), the
472
+ * full domain is `<domainPrefix>.<zoneName>`. On every other branch
473
+ * the per-PR preview hostname is
474
+ * `<domainPrefix>-<childZonePrefix>.<zoneName>`.
475
+ *
476
+ * Pure helper — reads no environment state. Subclasses expose thin
477
+ * statics (`composeFullDomain`) that fill in `domainPrefix` from their
478
+ * own service constant and delegate here.
479
+ */
480
+ static composeServiceDomain(opts) {
481
+ const isRelease = opts.branchName === opts.defaultReleaseBranch;
482
+ const subDomain = isRelease ? opts.domainPrefix : `${opts.domainPrefix}-${opts.childZonePrefix}`;
483
+ return `${subDomain}.${opts.zoneName}`;
484
+ }
485
+ /**
486
+ * Compute the `childZonePrefix` segment for a given branch — kebab-cased
487
+ * and truncated to {@link CHILD_ZONE_PREFIX_MAX_LENGTH}. Matches the
488
+ * per-instance {@link OpenHiService.childZonePrefix} getter so consumers
489
+ * can compose hostnames identical to the service's own without
490
+ * instantiating it.
491
+ */
492
+ static computeChildZonePrefix(branchName) {
493
+ return paramCase(branchName).slice(0, CHILD_ZONE_PREFIX_MAX_LENGTH);
494
+ }
495
+ /**
496
+ * Resolve the branch context the service would compute internally given
497
+ * an environment and optional overrides. Mirrors the same defaulting
498
+ * (props override → JEST sentinel → `GIT_BRANCH_NAME` env → git
499
+ * detection on DEV → release branch on stage/prod) the
500
+ * {@link OpenHiService} constructor uses.
501
+ *
502
+ * Consumers (e.g. sibling stack entries) call this to predict the
503
+ * branch values a service will see at synth time so they can compose
504
+ * hostnames against the same inputs.
505
+ */
506
+ static resolveBranchContext(ohEnv, overrides = {}) {
507
+ const defaultReleaseBranch = overrides.defaultReleaseBranch ?? DEFAULT_RELEASE_BRANCH;
508
+ const branchName = overrides.branchName ?? (process.env.JEST_WORKER_ID ? "test-branch" : process.env.GIT_BRANCH_NAME?.trim() || (ohEnv.ohStage.stageType === import_config3.OPEN_HI_STAGE.DEV ? findGitBranch() : defaultReleaseBranch));
509
+ const childZonePrefix = _OpenHiService.computeChildZonePrefix(branchName);
510
+ return { branchName, defaultReleaseBranch, childZonePrefix };
511
+ }
462
512
  /**
463
513
  * DNS prefix for this branche's child zone. Capped at 56 chars so
464
514
  * that a `<service>-<prefix>` hostname segment stays under the 63-byte
@@ -467,7 +517,7 @@ var OpenHiService = class extends Stack {
467
517
  * headroom on the longer side.
468
518
  */
469
519
  get childZonePrefix() {
470
- return paramCase(this.branchName).slice(0, 56);
520
+ return _OpenHiService.computeChildZonePrefix(this.branchName);
471
521
  }
472
522
  };
473
523
 
@@ -1419,10 +1469,11 @@ var DataStorePostgresReplica = class extends Construct6 {
1419
1469
  bundling: {
1420
1470
  minify: true,
1421
1471
  sourceMap: false,
1422
- // pg has conditional/optional deps (pg-native, pg-cloudflare) that
1423
- // historically misbehave when bundled by esbuild; keep it as a real
1424
- // node_module in the Lambda zip instead.
1425
- nodeModules: ["pg"]
1472
+ // pg's conditional optional deps (pg-native, pg-cloudflare) are
1473
+ // marked external so esbuild does not try to resolve them — pg's
1474
+ // runtime code wraps the requires in try/catch and falls back to
1475
+ // the pure-JS client when they are not present.
1476
+ externalModules: ["pg-native", "pg-cloudflare"]
1426
1477
  }
1427
1478
  });
1428
1479
  this.cluster.secret.grantRead(this.replicationFunction);
@@ -2880,6 +2931,7 @@ _OpenHiAuthService.SERVICE_TYPE = "auth";
2880
2931
  var OpenHiAuthService = _OpenHiAuthService;
2881
2932
 
2882
2933
  // src/services/open-hi-rest-api-service.ts
2934
+ var import_config5 = __toESM(require_lib2());
2883
2935
  import {
2884
2936
  CorsHttpMethod,
2885
2937
  DomainName,
@@ -2970,7 +3022,31 @@ var RestApiLambda = class extends Construct20 {
2970
3022
  // src/services/open-hi-rest-api-service.ts
2971
3023
  var REST_API_BASE_URL_SSM_NAME = "REST_API_BASE_URL";
2972
3024
  var REST_API_DOMAIN_NAME_SSM_NAME = "REST_API_DOMAIN_NAME";
3025
+ var DEV_CORS_ALLOW_ORIGINS = [
3026
+ "http://localhost:3000",
3027
+ "https://localhost:3000",
3028
+ "http://localhost:5173",
3029
+ "https://localhost:5173",
3030
+ "http://127.0.0.1:3000",
3031
+ "https://127.0.0.1:3000",
3032
+ "http://127.0.0.1:5173",
3033
+ "https://127.0.0.1:5173"
3034
+ ];
2973
3035
  var _OpenHiRestApiService = class _OpenHiRestApiService extends OpenHiService {
3036
+ /**
3037
+ * Compose the REST API's full per-deploy domain. Thin wrapper over
3038
+ * {@link OpenHiService.composeServiceDomain} that pins `domainPrefix`
3039
+ * to {@link API_DOMAIN_PREFIX}.
3040
+ *
3041
+ * Use from sibling stacks that need to predict the API's hostname
3042
+ * before the REST API stack is synthesised.
3043
+ */
3044
+ static composeFullDomain(opts) {
3045
+ return OpenHiService.composeServiceDomain({
3046
+ ...opts,
3047
+ domainPrefix: _OpenHiRestApiService.API_DOMAIN_PREFIX
3048
+ });
3049
+ }
2974
3050
  /**
2975
3051
  * Returns an IHttpApi by looking up the REST API stack's HTTP API ID from SSM.
2976
3052
  */
@@ -3054,11 +3130,18 @@ var _OpenHiRestApiService = class _OpenHiRestApiService extends OpenHiService {
3054
3130
  }
3055
3131
  /**
3056
3132
  * Returns the API domain name string (e.g. api.example.com or api-\{prefix\}.example.com).
3133
+ * Delegates to {@link OpenHiRestApiService.composeFullDomain} so the
3134
+ * release-vs-feature composition stays in one place; picks up
3135
+ * `this.defaultReleaseBranch` (not a hard-coded `"main"`).
3057
3136
  * Override to customize.
3058
3137
  */
3059
3138
  createApiDomainNameString(hostedZone) {
3060
- const apiPrefix = this.branchName === "main" ? `api` : `api-${this.childZonePrefix}`;
3061
- return [apiPrefix, hostedZone.zoneName].join(".");
3139
+ return _OpenHiRestApiService.composeFullDomain({
3140
+ branchName: this.branchName,
3141
+ defaultReleaseBranch: this.defaultReleaseBranch,
3142
+ childZonePrefix: this.childZonePrefix,
3143
+ zoneName: hostedZone.zoneName
3144
+ });
3062
3145
  }
3063
3146
  /**
3064
3147
  * Creates the SSM parameter for the REST API base URL.
@@ -3117,7 +3200,7 @@ var _OpenHiRestApiService = class _OpenHiRestApiService extends OpenHiService {
3117
3200
  postgresSecretArn,
3118
3201
  postgresDatabase,
3119
3202
  postgresSchema,
3120
- ...extraEnvironment !== void 0 && { extraEnvironment }
3203
+ extraEnvironment
3121
3204
  });
3122
3205
  lambda.addToRolePolicy(
3123
3206
  new PolicyStatement7({
@@ -3207,7 +3290,10 @@ var _OpenHiRestApiService = class _OpenHiRestApiService extends OpenHiService {
3207
3290
  routeKey: HttpRouteKey.with("/{proxy+}", HttpMethod.ANY),
3208
3291
  integration
3209
3292
  });
3210
- const apiPrefix = this.branchName === "main" ? `api` : `api-${this.childZonePrefix}`;
3293
+ const apiPrefix = this.apiDomainName.slice(
3294
+ 0,
3295
+ -(hostedZone.zoneName.length + 1)
3296
+ );
3211
3297
  new ARecord3(this, `api-a-record-${apiPrefix}`, {
3212
3298
  zone: hostedZone,
3213
3299
  recordName: apiPrefix,
@@ -3233,27 +3319,10 @@ var _OpenHiRestApiService = class _OpenHiRestApiService extends OpenHiService {
3233
3319
  { userPoolClients: [userPoolClient] }
3234
3320
  );
3235
3321
  const { corsPreflight: cors, ...restRootHttpApiProps } = this.props.rootHttpApiProps ?? {};
3236
- const corsPreflight = cors !== void 0 ? {
3237
- allowOrigins: cors.allowOrigins,
3238
- allowMethods: cors.allowMethods ?? [
3239
- CorsHttpMethod.GET,
3240
- CorsHttpMethod.HEAD,
3241
- CorsHttpMethod.POST,
3242
- CorsHttpMethod.PUT,
3243
- CorsHttpMethod.PATCH,
3244
- CorsHttpMethod.DELETE,
3245
- CorsHttpMethod.OPTIONS
3246
- ],
3247
- allowHeaders: cors.allowHeaders ?? [
3248
- "Content-Type",
3249
- "Authorization"
3250
- ],
3251
- allowCredentials: cors.allowCredentials ?? true,
3252
- maxAge: cors.maxAge ?? Duration10.days(1),
3253
- ...cors.exposeHeaders !== void 0 && {
3254
- exposeHeaders: cors.exposeHeaders
3255
- }
3256
- } : void 0;
3322
+ const isNonProd = this.ohEnv.ohStage.stageType !== import_config5.OPEN_HI_STAGE.PROD;
3323
+ const callerOrigins = cors?.allowOrigins ?? [];
3324
+ const mergedOrigins = isNonProd ? Array.from(/* @__PURE__ */ new Set([...callerOrigins, ...DEV_CORS_ALLOW_ORIGINS])) : callerOrigins;
3325
+ const corsPreflight = cors !== void 0 || isNonProd ? this.buildCorsPreflightOptions(mergedOrigins, cors) : void 0;
3257
3326
  const rootHttpApi = new RootHttpApi(this, {
3258
3327
  ...restRootHttpApiProps,
3259
3328
  ...corsPreflight !== void 0 && { corsPreflight },
@@ -3270,21 +3339,43 @@ var _OpenHiRestApiService = class _OpenHiRestApiService extends OpenHiService {
3270
3339
  });
3271
3340
  return rootHttpApi;
3272
3341
  }
3342
+ /**
3343
+ * Builds the full `CorsPreflightOptions` from a merged origins array,
3344
+ * filling defaults for `allowMethods`/`allowHeaders`/`allowCredentials`/
3345
+ * `maxAge` from the caller-supplied block when present.
3346
+ */
3347
+ buildCorsPreflightOptions(allowOrigins, cors) {
3348
+ return {
3349
+ allowOrigins: [...allowOrigins],
3350
+ allowMethods: cors?.allowMethods ?? [
3351
+ CorsHttpMethod.GET,
3352
+ CorsHttpMethod.HEAD,
3353
+ CorsHttpMethod.POST,
3354
+ CorsHttpMethod.PUT,
3355
+ CorsHttpMethod.PATCH,
3356
+ CorsHttpMethod.DELETE,
3357
+ CorsHttpMethod.OPTIONS
3358
+ ],
3359
+ allowHeaders: cors?.allowHeaders ?? ["Content-Type", "Authorization"],
3360
+ allowCredentials: cors?.allowCredentials ?? true,
3361
+ maxAge: cors?.maxAge ?? Duration10.days(1),
3362
+ ...cors?.exposeHeaders !== void 0 && {
3363
+ exposeHeaders: cors.exposeHeaders
3364
+ }
3365
+ };
3366
+ }
3273
3367
  /**
3274
3368
  * Builds the `OPENHI_RUNTIME_CONFIG_*` env-var map the REST API Lambda
3275
- * exposes through `GET /control/runtime-config`. Returns `undefined` when
3276
- * the `runtimeConfig` prop is omitted so no env vars are set.
3277
- *
3278
- * The three Cognito IDs are resolved via SSM lookups against the auth
3279
- * stack from a dedicated sub-scope (`runtime-config`) so they don't
3280
- * collide with the user-pool / user-pool-client constructs already
3281
- * created in {@link createRootHttpApi}. `apiBaseUrl` is derived from
3282
- * this stack's own custom domain so callers don't have to hardcode it.
3369
+ * exposes through `GET /control/runtime-config`. The four values are
3370
+ * always populated the three Cognito IDs are resolved via SSM lookups
3371
+ * against the auth stack from a dedicated sub-scope (`runtime-config`)
3372
+ * so they don't collide with the user-pool / user-pool-client constructs
3373
+ * already created in {@link createRootHttpApi}, and `apiBaseUrl` is
3374
+ * derived from this stack's own custom domain. The OAuth callback URL
3375
+ * is no longer plumbed through the API — the admin-console derives it
3376
+ * client-side from `window.location.origin`.
3283
3377
  */
3284
3378
  resolveRuntimeConfigEnvVars() {
3285
- if (this.props.runtimeConfig === void 0) {
3286
- return void 0;
3287
- }
3288
3379
  const cognitoScope = new Construct21(this, "runtime-config");
3289
3380
  const userPool = OpenHiAuthService.userPoolFromConstruct(cognitoScope);
3290
3381
  const userPoolClient = OpenHiAuthService.userPoolClientFromConstruct(cognitoScope);
@@ -3293,12 +3384,16 @@ var _OpenHiRestApiService = class _OpenHiRestApiService extends OpenHiService {
3293
3384
  OPENHI_RUNTIME_CONFIG_COGNITO_USER_POOL_ID: userPool.userPoolId,
3294
3385
  OPENHI_RUNTIME_CONFIG_COGNITO_USER_POOL_CLIENT_ID: userPoolClient.userPoolClientId,
3295
3386
  OPENHI_RUNTIME_CONFIG_COGNITO_DOMAIN_URL: cognitoDomainUrl,
3296
- OPENHI_RUNTIME_CONFIG_COGNITO_REDIRECT_URI: this.props.runtimeConfig.cognitoRedirectUri,
3297
3387
  OPENHI_RUNTIME_CONFIG_API_BASE_URL: `https://${this.apiDomainName}`
3298
3388
  };
3299
3389
  }
3300
3390
  };
3301
3391
  _OpenHiRestApiService.SERVICE_TYPE = "rest-api";
3392
+ /**
3393
+ * Sub-domain prefix used by the REST API. Release-branch hostname is
3394
+ * `api.<zone>`; per-PR preview hostname is `api-<childZonePrefix>.<zone>`.
3395
+ */
3396
+ _OpenHiRestApiService.API_DOMAIN_PREFIX = "api";
3302
3397
  var OpenHiRestApiService = _OpenHiRestApiService;
3303
3398
 
3304
3399
  // src/services/open-hi-graphql-service.ts
@@ -3342,10 +3437,26 @@ _OpenHiGraphqlService.SERVICE_TYPE = "graphql-api";
3342
3437
  var OpenHiGraphqlService = _OpenHiGraphqlService;
3343
3438
 
3344
3439
  // src/services/open-hi-website-service.ts
3345
- var import_config5 = __toESM(require_lib2());
3440
+ var import_config6 = __toESM(require_lib2());
3346
3441
  import { Bucket as Bucket3 } from "aws-cdk-lib/aws-s3";
3347
3442
  var SSM_PARAM_NAME_FULL_DOMAIN = "WEBSITE_FULL_DOMAIN";
3443
+ var ADMIN_DOMAIN_PREFIX = "admin";
3348
3444
  var _OpenHiWebsiteService = class _OpenHiWebsiteService extends OpenHiService {
3445
+ /**
3446
+ * Compose the website's full per-deploy domain. Thin wrapper over
3447
+ * {@link OpenHiService.composeServiceDomain} that fills in
3448
+ * {@link DEFAULT_DOMAIN_PREFIX} when `domainPrefix` is omitted.
3449
+ *
3450
+ * Use from sibling stacks that need to predict the website's hostname
3451
+ * before the website stack is synthesised — e.g. the REST API stack
3452
+ * computing its CORS `allowOrigins` for the admin-console.
3453
+ */
3454
+ static composeFullDomain(opts) {
3455
+ return OpenHiService.composeServiceDomain({
3456
+ ...opts,
3457
+ domainPrefix: opts.domainPrefix ?? _OpenHiWebsiteService.DEFAULT_DOMAIN_PREFIX
3458
+ });
3459
+ }
3349
3460
  /**
3350
3461
  * Looks up the static-hosting bucket ARN published by the release-branch
3351
3462
  * deploy of this service.
@@ -3464,16 +3575,24 @@ var _OpenHiWebsiteService = class _OpenHiWebsiteService extends OpenHiService {
3464
3575
  * every other deploy serves a per-PR preview at
3465
3576
  * `\<domainPrefix\>-\<childZonePrefix\>.\<zone\>`
3466
3577
  * (e.g. `admin-feat-1093-patient-migration.dev.openhi.org`).
3578
+ *
3579
+ * Delegates to {@link OpenHiWebsiteService.composeFullDomain} so the
3580
+ * release-vs-feature composition stays in one place.
3467
3581
  */
3468
3582
  computeFullDomain(hostedZone) {
3469
- const subDomain = this.computeSubDomain();
3470
- return [subDomain, hostedZone.zoneName].join(".");
3583
+ return _OpenHiWebsiteService.composeFullDomain({
3584
+ domainPrefix: this.props.domainPrefix,
3585
+ branchName: this.branchName,
3586
+ defaultReleaseBranch: this.defaultReleaseBranch,
3587
+ childZonePrefix: this.childZonePrefix,
3588
+ zoneName: hostedZone.zoneName
3589
+ });
3471
3590
  }
3472
3591
  /**
3473
3592
  * Returns the sub-domain label (left of the zone) for the current
3474
- * deploy. Used both for {@link fullDomain} and for the per-branch S3
3475
- * key prefix passed to {@link StaticContent} so the upload prefix
3476
- * always matches the served hostname.
3593
+ * deploy. Used for the per-branch S3 key prefix passed to
3594
+ * {@link StaticContent} so the upload prefix always matches the
3595
+ * served hostname.
3477
3596
  *
3478
3597
  * Non-release deploys compose the per-PR slug as
3479
3598
  * `\<domainPrefix\>-\<childZonePrefix\>`, mirroring the REST API's
@@ -3484,7 +3603,7 @@ var _OpenHiWebsiteService = class _OpenHiWebsiteService extends OpenHiService {
3484
3603
  */
3485
3604
  computeSubDomain() {
3486
3605
  const isReleaseBranch = this.branchName === this.defaultReleaseBranch;
3487
- const domainPrefix = this.props.domainPrefix ?? "www";
3606
+ const domainPrefix = this.props.domainPrefix ?? _OpenHiWebsiteService.DEFAULT_DOMAIN_PREFIX;
3488
3607
  if (isReleaseBranch) {
3489
3608
  return domainPrefix;
3490
3609
  }
@@ -3511,7 +3630,7 @@ var _OpenHiWebsiteService = class _OpenHiWebsiteService extends OpenHiService {
3511
3630
  domainNames: [this.fullDomain, wildcardSan],
3512
3631
  description: `OpenHI website (${this.fullDomain})`,
3513
3632
  prefixPattern: PER_BRANCH_PREVIEW_PREFIX,
3514
- enablePreviewLifecycle: this.ohEnv.ohStage.stageType !== import_config5.OPEN_HI_STAGE.PROD,
3633
+ enablePreviewLifecycle: this.ohEnv.ohStage.stageType !== import_config6.OPEN_HI_STAGE.PROD,
3515
3634
  ...restApi !== void 0 && { restApi }
3516
3635
  });
3517
3636
  }
@@ -3597,6 +3716,12 @@ var _OpenHiWebsiteService = class _OpenHiWebsiteService extends OpenHiService {
3597
3716
  }
3598
3717
  };
3599
3718
  _OpenHiWebsiteService.SERVICE_TYPE = "website";
3719
+ /**
3720
+ * Default `domainPrefix` for this service when none is supplied.
3721
+ * Release-branch hostname is `www.<zone>`; per-PR preview hostname is
3722
+ * `www-<childZonePrefix>.<zone>`.
3723
+ */
3724
+ _OpenHiWebsiteService.DEFAULT_DOMAIN_PREFIX = "www";
3600
3725
  var OpenHiWebsiteService = _OpenHiWebsiteService;
3601
3726
 
3602
3727
  // src/workflows/control-plane/owning-delete-cascade/owning-delete-cascade-lambdas.ts
@@ -4114,6 +4239,7 @@ var export_OWNING_ENTITY_TYPE = import_workflows3.OWNING_ENTITY_TYPE;
4114
4239
  var export_PlatformDeploymentCompletedV1 = import_workflows2.PlatformDeploymentCompletedV1;
4115
4240
  var export_RENAMABLE_ENTITY_TYPE = import_workflows4.RENAMABLE_ENTITY_TYPE;
4116
4241
  export {
4242
+ ADMIN_DOMAIN_PREFIX,
4117
4243
  BRIDGED_STATUSES,
4118
4244
  CLOUDFORMATION_EVENT_SOURCE,
4119
4245
  CLOUDFORMATION_STACK_STATUS_CHANGE_DETAIL_TYPE,
@@ -4138,6 +4264,7 @@ export {
4138
4264
  DEMO_PERIOD,
4139
4265
  DEMO_TENANT_SPECS,
4140
4266
  DEMO_URN_SYSTEM,
4267
+ DEV_CORS_ALLOW_ORIGINS,
4141
4268
  DEV_USERS,
4142
4269
  DataEventBus,
4143
4270
  DataStoreHistoricalArchive,