@openhi/constructs 0.0.136 → 0.0.138

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
@@ -736,23 +736,10 @@ import { UserPoolClient } from "aws-cdk-lib/aws-cognito";
736
736
  var CognitoUserPoolClient = class extends UserPoolClient {
737
737
  constructor(scope, props) {
738
738
  super(scope, "user-pool-client", {
739
- /**
740
- * Defaults
741
- */
739
+ // Default: SPA client (no secret). OAuth flow + callback/logout URL
740
+ // composition is the owning service's responsibility — pass via
741
+ // `props.oAuth` (see `OpenHiAuthService.resolveOAuthRedirectUrls`).
742
742
  generateSecret: false,
743
- oAuth: {
744
- flows: {
745
- authorizationCodeGrant: true,
746
- implicitCodeGrant: true
747
- },
748
- callbackUrls: [
749
- `http://localhost:3000/oauth/callback`,
750
- `https://localhost:3000/oauth/callback`
751
- ]
752
- },
753
- /**
754
- * Overrideable props
755
- */
756
743
  ...props
757
744
  });
758
745
  }
@@ -1469,10 +1456,11 @@ var DataStorePostgresReplica = class extends Construct6 {
1469
1456
  bundling: {
1470
1457
  minify: true,
1471
1458
  sourceMap: false,
1472
- // pg has conditional/optional deps (pg-native, pg-cloudflare) that
1473
- // historically misbehave when bundled by esbuild; keep it as a real
1474
- // node_module in the Lambda zip instead.
1475
- nodeModules: ["pg"]
1459
+ // pg's conditional optional deps (pg-native, pg-cloudflare) are
1460
+ // marked external so esbuild does not try to resolve them — pg's
1461
+ // runtime code wraps the requires in try/catch and falls back to
1462
+ // the pure-JS client when they are not present.
1463
+ externalModules: ["pg-native", "pg-cloudflare"]
1476
1464
  }
1477
1465
  });
1478
1466
  this.cluster.secret.grantRead(this.replicationFunction);
@@ -1988,6 +1976,7 @@ var StaticContent = class extends Construct10 {
1988
1976
  };
1989
1977
 
1990
1978
  // src/services/open-hi-auth-service.ts
1979
+ var import_config7 = __toESM(require_lib2());
1991
1980
  import {
1992
1981
  LambdaVersion,
1993
1982
  UserPool as UserPool2,
@@ -1995,7 +1984,7 @@ import {
1995
1984
  UserPoolDomain as UserPoolDomain2,
1996
1985
  UserPoolOperation
1997
1986
  } from "aws-cdk-lib/aws-cognito";
1998
- import { Effect as Effect6, PolicyStatement as PolicyStatement6 } from "aws-cdk-lib/aws-iam";
1987
+ import { Effect as Effect7, PolicyStatement as PolicyStatement7 } from "aws-cdk-lib/aws-iam";
1999
1988
  import { Key as Key2 } from "aws-cdk-lib/aws-kms";
2000
1989
  import { Stack as Stack7 } from "aws-cdk-lib/core";
2001
1990
 
@@ -2577,633 +2566,285 @@ var _OpenHiDataService = class _OpenHiDataService extends OpenHiService {
2577
2566
  _OpenHiDataService.SERVICE_TYPE = "data";
2578
2567
  var OpenHiDataService = _OpenHiDataService;
2579
2568
 
2580
- // src/workflows/control-plane/user-onboarding/provision-default-workspace-lambda.ts
2569
+ // src/services/open-hi-website-service.ts
2570
+ var import_config6 = __toESM(require_lib2());
2571
+ import { Bucket as Bucket3 } from "aws-cdk-lib/aws-s3";
2572
+
2573
+ // src/services/open-hi-rest-api-service.ts
2574
+ var import_config5 = __toESM(require_lib2());
2575
+ import {
2576
+ CorsHttpMethod,
2577
+ DomainName,
2578
+ HttpApi as HttpApi2,
2579
+ HttpMethod,
2580
+ HttpNoneAuthorizer,
2581
+ HttpRoute,
2582
+ HttpRouteKey
2583
+ } from "aws-cdk-lib/aws-apigatewayv2";
2584
+ import { HttpUserPoolAuthorizer } from "aws-cdk-lib/aws-apigatewayv2-authorizers";
2585
+ import { HttpLambdaIntegration } from "aws-cdk-lib/aws-apigatewayv2-integrations";
2586
+ import { Effect as Effect5, PolicyStatement as PolicyStatement5 } from "aws-cdk-lib/aws-iam";
2587
+ import {
2588
+ ARecord as ARecord3,
2589
+ HostedZone as HostedZone3,
2590
+ RecordTarget as RecordTarget3
2591
+ } from "aws-cdk-lib/aws-route53";
2592
+ import { ApiGatewayv2DomainProperties } from "aws-cdk-lib/aws-route53-targets";
2593
+ import { Duration as Duration9 } from "aws-cdk-lib/core";
2594
+ import { Construct as Construct19 } from "constructs";
2595
+
2596
+ // src/data/lambda/cors-options-lambda.ts
2581
2597
  import fs10 from "fs";
2582
2598
  import path10 from "path";
2583
- import { Duration as Duration9 } from "aws-cdk-lib";
2584
- import { Rule as Rule4 } from "aws-cdk-lib/aws-events";
2585
- import { LambdaFunction as LambdaFunction4 } from "aws-cdk-lib/aws-events-targets";
2586
- import { Effect as Effect5, PolicyStatement as PolicyStatement5 } from "aws-cdk-lib/aws-iam";
2587
2599
  import { Runtime as Runtime10 } from "aws-cdk-lib/aws-lambda";
2588
2600
  import { NodejsFunction as NodejsFunction10 } from "aws-cdk-lib/aws-lambda-nodejs";
2589
2601
  import { Construct as Construct17 } from "constructs";
2590
- var HANDLER_NAME9 = "provision-default-workspace.handler.js";
2602
+ var HANDLER_NAME9 = "cors-options-lambda.handler.js";
2591
2603
  function resolveHandlerEntry9(dirname) {
2592
2604
  const sameDir = path10.join(dirname, HANDLER_NAME9);
2593
2605
  if (fs10.existsSync(sameDir)) {
2594
2606
  return sameDir;
2595
2607
  }
2596
- return path10.join(dirname, "..", "..", "..", "..", "lib", HANDLER_NAME9);
2608
+ const fromLib = path10.join(dirname, "..", "..", "..", "lib", HANDLER_NAME9);
2609
+ return fromLib;
2597
2610
  }
2598
- var ProvisionDefaultWorkspaceLambda = class extends Construct17 {
2599
- constructor(scope, props) {
2600
- super(scope, "provision-default-workspace-lambda");
2611
+ var CorsOptionsLambda = class extends Construct17 {
2612
+ constructor(scope, id = "cors-options-lambda") {
2613
+ super(scope, id);
2601
2614
  this.lambda = new NodejsFunction10(this, "handler", {
2602
2615
  entry: resolveHandlerEntry9(__dirname),
2603
2616
  runtime: Runtime10.NODEJS_LATEST,
2604
- memorySize: 1024,
2605
- environment: {
2606
- DYNAMO_TABLE_NAME: props.dataStoreTable.tableName
2607
- }
2608
- });
2609
- props.dataStoreTable.grant(
2610
- this.lambda,
2611
- "dynamodb:PutItem",
2612
- "dynamodb:UpdateItem"
2613
- );
2614
- this.lambda.addToRolePolicy(
2615
- new PolicyStatement5({
2616
- effect: Effect5.ALLOW,
2617
- actions: ["dynamodb:Query"],
2618
- resources: [`${props.dataStoreTable.tableArn}/index/*`]
2619
- })
2620
- );
2621
- this.rule = new Rule4(this, "rule", {
2622
- eventBus: props.controlEventBus,
2623
- eventPattern: {
2624
- source: [USER_ONBOARDING_EVENT_SOURCE],
2625
- detailType: [PROVISION_DEFAULT_WORKSPACE_DETAIL_TYPE]
2626
- },
2627
- targets: [
2628
- new LambdaFunction4(this.lambda, {
2629
- retryAttempts: 2,
2630
- maxEventAge: Duration9.hours(2)
2631
- })
2632
- ]
2617
+ memorySize: 128
2633
2618
  });
2634
2619
  }
2635
2620
  };
2636
2621
 
2637
- // src/workflows/control-plane/user-onboarding/user-onboarding-workflow.ts
2622
+ // src/data/lambda/rest-api-lambda.ts
2623
+ import fs11 from "fs";
2624
+ import path11 from "path";
2625
+ import { Runtime as Runtime11 } from "aws-cdk-lib/aws-lambda";
2626
+ import { NodejsFunction as NodejsFunction11 } from "aws-cdk-lib/aws-lambda-nodejs";
2638
2627
  import { Construct as Construct18 } from "constructs";
2639
- var UserOnboardingWorkflow = class extends Construct18 {
2628
+ var HANDLER_NAME10 = "rest-api-lambda.handler.js";
2629
+ function resolveHandlerEntry10(dirname) {
2630
+ const sameDir = path11.join(dirname, HANDLER_NAME10);
2631
+ if (fs11.existsSync(sameDir)) {
2632
+ return sameDir;
2633
+ }
2634
+ const fromLib = path11.join(dirname, "..", "..", "..", "lib", HANDLER_NAME10);
2635
+ return fromLib;
2636
+ }
2637
+ var RestApiLambda = class extends Construct18 {
2640
2638
  constructor(scope, props) {
2641
- super(scope, "user-onboarding-workflow");
2642
- this.provisionDefaultWorkspace = new ProvisionDefaultWorkspaceLambda(this, {
2643
- dataStoreTable: props.dataStoreTable,
2644
- controlEventBus: props.controlEventBus
2639
+ super(scope, "rest-api-lambda");
2640
+ this.lambda = new NodejsFunction11(this, "handler", {
2641
+ entry: resolveHandlerEntry10(__dirname),
2642
+ runtime: Runtime11.NODEJS_LATEST,
2643
+ memorySize: 1024,
2644
+ environment: {
2645
+ DYNAMO_TABLE_NAME: props.dynamoTableName,
2646
+ BRANCH_TAG_VALUE: props.branchTagValue,
2647
+ HTTP_API_TAG_VALUE: props.httpApiTagValue,
2648
+ OPENHI_PG_CLUSTER_ARN: props.postgresClusterArn,
2649
+ OPENHI_PG_SECRET_ARN: props.postgresSecretArn,
2650
+ OPENHI_PG_DATABASE: props.postgresDatabase,
2651
+ OPENHI_PG_SCHEMA: props.postgresSchema,
2652
+ ...props.extraEnvironment
2653
+ },
2654
+ bundling: {
2655
+ minify: true,
2656
+ sourceMap: false
2657
+ }
2645
2658
  });
2646
2659
  }
2647
2660
  };
2648
2661
 
2649
- // src/services/open-hi-auth-service.ts
2650
- var _OpenHiAuthService = class _OpenHiAuthService extends OpenHiService {
2651
- constructor(ohEnv, props = {}) {
2652
- super(ohEnv, _OpenHiAuthService.SERVICE_TYPE, props);
2653
- /**
2654
- * Cross-stack reference to the data store table. Cached so repeated
2655
- * lookups share a single CDK construct id ("dynamo-db-data-store") in
2656
- * this stack — a second `Table.fromTableName` call under the same scope
2657
- * would collide.
2658
- */
2659
- this._dataStoreTable = null;
2660
- this._controlEventBus = null;
2661
- this.props = props;
2662
- this.userPoolKmsKey = this.createUserPoolKmsKey();
2663
- this.preTokenGenerationLambda = this.createPreTokenGenerationLambda();
2664
- this.postAuthenticationLambda = this.createPostAuthenticationLambda();
2665
- this.postConfirmationLambda = this.createPostConfirmationLambda();
2666
- this.userOnboardingWorkflow = this.createUserOnboardingWorkflow();
2667
- this.userPool = this.createUserPool();
2668
- this.grantPreTokenGenerationPermissions();
2669
- this.grantPostAuthenticationPermissions();
2670
- this.grantPostConfirmationPermissions();
2671
- this.userPoolClient = this.createUserPoolClient();
2672
- this.userPoolDomain = this.createUserPoolDomain();
2673
- }
2662
+ // src/services/open-hi-rest-api-service.ts
2663
+ var REST_API_BASE_URL_SSM_NAME = "REST_API_BASE_URL";
2664
+ var REST_API_DOMAIN_NAME_SSM_NAME = "REST_API_DOMAIN_NAME";
2665
+ var DEV_CORS_ALLOW_ORIGINS = [
2666
+ "http://localhost:3000",
2667
+ "https://localhost:3000",
2668
+ "http://localhost:5173",
2669
+ "https://localhost:5173",
2670
+ "http://127.0.0.1:3000",
2671
+ "https://127.0.0.1:3000",
2672
+ "http://127.0.0.1:5173",
2673
+ "https://127.0.0.1:5173"
2674
+ ];
2675
+ var _OpenHiRestApiService = class _OpenHiRestApiService extends OpenHiService {
2674
2676
  /**
2675
- * Returns an IUserPool by looking up the Auth stack's User Pool ID from SSM.
2677
+ * Compose the REST API's full per-deploy domain. Thin wrapper over
2678
+ * {@link OpenHiService.composeServiceDomain} that pins `domainPrefix`
2679
+ * to {@link API_DOMAIN_PREFIX}.
2680
+ *
2681
+ * Use from sibling stacks that need to predict the API's hostname
2682
+ * before the REST API stack is synthesised.
2676
2683
  */
2677
- static userPoolFromConstruct(scope) {
2678
- const userPoolId = DiscoverableStringParameter.valueForLookupName(scope, {
2679
- ssmParamName: CognitoUserPool.SSM_PARAM_NAME,
2680
- serviceType: _OpenHiAuthService.SERVICE_TYPE
2684
+ static composeFullDomain(opts) {
2685
+ return OpenHiService.composeServiceDomain({
2686
+ ...opts,
2687
+ domainPrefix: _OpenHiRestApiService.API_DOMAIN_PREFIX
2681
2688
  });
2682
- return UserPool2.fromUserPoolId(scope, "user-pool", userPoolId);
2683
- }
2684
- /**
2685
- * Returns an IUserPoolClient by looking up the Auth stack's User Pool Client ID from SSM.
2686
- */
2687
- static userPoolClientFromConstruct(scope) {
2688
- const userPoolClientId = DiscoverableStringParameter.valueForLookupName(
2689
- scope,
2690
- {
2691
- ssmParamName: CognitoUserPoolClient.SSM_PARAM_NAME,
2692
- serviceType: _OpenHiAuthService.SERVICE_TYPE
2693
- }
2694
- );
2695
- return UserPoolClient2.fromUserPoolClientId(
2696
- scope,
2697
- "user-pool-client",
2698
- userPoolClientId
2699
- );
2700
2689
  }
2701
2690
  /**
2702
- * Returns an IUserPoolDomain by looking up the Auth stack's User Pool Domain from SSM.
2691
+ * Returns an IHttpApi by looking up the REST API stack's HTTP API ID from SSM.
2703
2692
  */
2704
- static userPoolDomainFromConstruct(scope) {
2705
- const domainName = DiscoverableStringParameter.valueForLookupName(scope, {
2706
- ssmParamName: CognitoUserPoolDomain.SSM_PARAM_NAME,
2707
- serviceType: _OpenHiAuthService.SERVICE_TYPE
2693
+ static rootHttpApiFromConstruct(scope) {
2694
+ const httpApiId = DiscoverableStringParameter.valueForLookupName(scope, {
2695
+ ssmParamName: RootHttpApi.SSM_PARAM_NAME,
2696
+ serviceType: _OpenHiRestApiService.SERVICE_TYPE
2708
2697
  });
2709
- return UserPoolDomain2.fromDomainName(scope, "user-pool-domain", domainName);
2698
+ return HttpApi2.fromHttpApiAttributes(scope, "http-api", { httpApiId });
2710
2699
  }
2711
2700
  /**
2712
- * Returns the full Cognito Hosted UI base URL (e.g.
2713
- * `https://auth-abc.auth.us-east-2.amazoncognito.com`) by looking up
2714
- * the Auth stack's User Pool Domain from SSM and composing it with the
2715
- * calling stack's region.
2716
- *
2717
- * Equivalent to `UserPoolDomain.baseUrl()` on the concrete construct,
2718
- * but works across stacks where the looked-up `IUserPoolDomain` is an
2719
- * `Import` and does not carry the `baseUrl()` method. Assumes the
2720
- * domain was created as a Cognito-managed prefix domain (the only
2721
- * variant `OpenHiAuthService.createUserPoolDomain` produces).
2701
+ * Returns the REST API base URL (e.g. https://api.example.com) by looking it up from SSM.
2702
+ * Use in other stacks for E2E, scripts, or config.
2722
2703
  */
2723
- static userPoolDomainBaseUrlFromConstruct(scope) {
2724
- const domainName = DiscoverableStringParameter.valueForLookupName(scope, {
2725
- ssmParamName: CognitoUserPoolDomain.SSM_PARAM_NAME,
2726
- serviceType: _OpenHiAuthService.SERVICE_TYPE
2704
+ static restApiBaseUrlFromConstruct(scope) {
2705
+ return DiscoverableStringParameter.valueForLookupName(scope, {
2706
+ ssmParamName: REST_API_BASE_URL_SSM_NAME,
2707
+ serviceType: _OpenHiRestApiService.SERVICE_TYPE
2727
2708
  });
2728
- const region = Stack7.of(scope).region;
2729
- return `https://${domainName}.auth.${region}.amazoncognito.com`;
2730
2709
  }
2731
2710
  /**
2732
- * Returns an IKey (KMS) by looking up the Auth stack's User Pool KMS Key ARN from SSM.
2711
+ * Returns the REST API's custom domain name (bare hostname, no scheme — e.g.
2712
+ * `api.example.com`) by looking it up from SSM. Use as the host for a
2713
+ * CloudFront `HttpOrigin` so the website's distribution can proxy `/api/*`
2714
+ * to this stack's API Gateway without per-branch DNS knowledge.
2733
2715
  */
2734
- static userPoolKmsKeyFromConstruct(scope) {
2735
- const keyArn = DiscoverableStringParameter.valueForLookupName(scope, {
2736
- ssmParamName: CognitoUserPoolKmsKey.SSM_PARAM_NAME,
2737
- serviceType: _OpenHiAuthService.SERVICE_TYPE
2716
+ static restApiDomainNameFromConstruct(scope) {
2717
+ return DiscoverableStringParameter.valueForLookupName(scope, {
2718
+ ssmParamName: REST_API_DOMAIN_NAME_SSM_NAME,
2719
+ serviceType: _OpenHiRestApiService.SERVICE_TYPE
2738
2720
  });
2739
- return Key2.fromKeyArn(scope, "kms-key", keyArn);
2740
2721
  }
2741
2722
  get serviceType() {
2742
- return _OpenHiAuthService.SERVICE_TYPE;
2723
+ return _OpenHiRestApiService.SERVICE_TYPE;
2724
+ }
2725
+ constructor(ohEnv, props = {}) {
2726
+ super(ohEnv, _OpenHiRestApiService.SERVICE_TYPE, props);
2727
+ this.props = props;
2728
+ this.validateConfig(props);
2729
+ const hostedZone = this.createHostedZone();
2730
+ const certificate = this.createCertificate();
2731
+ this.apiDomainName = this.createApiDomainNameString(hostedZone);
2732
+ this.createRestApiBaseUrlParameter(this.apiDomainName);
2733
+ this.createRestApiDomainNameParameter(this.apiDomainName);
2734
+ const domainName = this.createDomainName(hostedZone, certificate);
2735
+ this.rootHttpApi = this.createRootHttpApi(domainName);
2736
+ this.createRestApiLambdaAndRoutes(hostedZone, domainName);
2743
2737
  }
2744
2738
  /**
2745
- * Creates the KMS key for the Cognito User Pool and exports its ARN to SSM.
2746
- * Look up via {@link OpenHiAuthService.userPoolKmsKeyFromConstruct}.
2747
- * Override to customize.
2739
+ * Validates that config required for the REST API stack is present.
2748
2740
  */
2749
- createUserPoolKmsKey() {
2750
- const key = new CognitoUserPoolKmsKey(this);
2751
- new DiscoverableStringParameter(this, "kms-key-param", {
2752
- ssmParamName: CognitoUserPoolKmsKey.SSM_PARAM_NAME,
2753
- stringValue: key.keyArn,
2754
- description: "KMS key ARN for Cognito User Pool (e.g. custom sender); cross-stack reference"
2755
- });
2756
- return key;
2741
+ validateConfig(props) {
2742
+ const { config } = props;
2743
+ if (!config) {
2744
+ throw new Error("Config is required");
2745
+ }
2746
+ if (!config.hostedZoneId) {
2747
+ throw new Error("Hosted zone ID is required");
2748
+ }
2749
+ if (!config.zoneName) {
2750
+ throw new Error("Zone name is required");
2751
+ }
2757
2752
  }
2758
2753
  /**
2759
- * Creates the Pre Token Generation Lambda (Cognito trigger). On every
2760
- * sign-in and token refresh the Lambda resolves the User by Cognito `sub`
2761
- * (GSI2) and injects `ohi_tid`, `ohi_wid`, `ohi_uid`, `ohi_uname` into
2762
- * both the ID token and the access token (ADR 2026-03-17-01).
2754
+ * Creates the hosted zone reference (imported from config).
2755
+ * Override to customize.
2763
2756
  */
2764
- createPreTokenGenerationLambda() {
2765
- const construct = new PreTokenGenerationLambda(this, {
2766
- dynamoTableName: this.dataStoreTable().tableName
2757
+ createHostedZone() {
2758
+ const { config } = this.props;
2759
+ return HostedZone3.fromHostedZoneAttributes(this, "root-zone", {
2760
+ hostedZoneId: config.hostedZoneId,
2761
+ zoneName: config.zoneName
2767
2762
  });
2768
- return construct.lambda;
2769
2763
  }
2770
2764
  /**
2771
- * Creates the Post Authentication Lambda (Cognito trigger). Calls
2772
- * AdminUserGlobalSignOut on every sign-in to enforce single-device-per-user
2773
- * sessions per ADR 2026-03-17-01.
2765
+ * Creates the wildcard certificate (imported from Global stack via SSM).
2766
+ * Override to customize.
2774
2767
  */
2775
- createPostAuthenticationLambda() {
2776
- const construct = new PostAuthenticationLambda(this);
2777
- return construct.lambda;
2768
+ createCertificate() {
2769
+ return OpenHiGlobalService.rootWildcardCertificateFromConstruct(this);
2778
2770
  }
2779
2771
  /**
2780
- * Creates the Post Confirmation Lambda (Cognito trigger). On sign-up
2781
- * confirmation, publishes a control-plane workflow event; provisioning lives
2782
- * behind EventBridge.
2772
+ * Returns the API domain name string (e.g. api.example.com or api-\{prefix\}.example.com).
2773
+ * Delegates to {@link OpenHiRestApiService.composeFullDomain} so the
2774
+ * release-vs-feature composition stays in one place; picks up
2775
+ * `this.defaultReleaseBranch` (not a hard-coded `"main"`).
2776
+ * Override to customize.
2783
2777
  */
2784
- createPostConfirmationLambda() {
2785
- const construct = new PostConfirmationLambda(this, {
2786
- controlEventBusName: this.controlEventBus().eventBusName
2787
- });
2788
- return construct.lambda;
2789
- }
2790
- createUserOnboardingWorkflow() {
2791
- return new UserOnboardingWorkflow(this, {
2792
- controlEventBus: this.controlEventBus(),
2793
- dataStoreTable: this.dataStoreTable()
2778
+ createApiDomainNameString(hostedZone) {
2779
+ return _OpenHiRestApiService.composeFullDomain({
2780
+ branchName: this.branchName,
2781
+ defaultReleaseBranch: this.defaultReleaseBranch,
2782
+ childZonePrefix: this.childZonePrefix,
2783
+ zoneName: hostedZone.zoneName
2794
2784
  });
2795
2785
  }
2796
- dataStoreTable() {
2797
- if (this._dataStoreTable === null) {
2798
- this._dataStoreTable = OpenHiDataService.dynamoDbDataStoreFromConstruct(this);
2799
- }
2800
- return this._dataStoreTable;
2801
- }
2802
- controlEventBus() {
2803
- if (this._controlEventBus === null) {
2804
- this._controlEventBus = OpenHiGlobalService.controlEventBusFromConstruct(this);
2805
- }
2806
- return this._controlEventBus;
2807
- }
2808
2786
  /**
2809
- * Creates the Cognito User Pool and exports its ID to SSM.
2810
- * Look up via {@link OpenHiAuthService.userPoolFromConstruct}.
2787
+ * Creates the SSM parameter for the REST API base URL.
2788
+ * Look up via {@link OpenHiRestApiService.restApiBaseUrlFromConstruct}.
2811
2789
  * Override to customize.
2812
2790
  */
2813
- createUserPool() {
2814
- const userPool = new CognitoUserPool(this, {
2815
- ...this.props.userPoolProps,
2816
- customSenderKmsKey: this.userPoolKmsKey
2817
- });
2818
- userPool.addTrigger(
2819
- UserPoolOperation.PRE_TOKEN_GENERATION_CONFIG,
2820
- this.preTokenGenerationLambda,
2821
- LambdaVersion.V2_0
2822
- );
2823
- userPool.addTrigger(
2824
- UserPoolOperation.POST_AUTHENTICATION,
2825
- this.postAuthenticationLambda
2826
- );
2827
- userPool.addTrigger(
2828
- UserPoolOperation.POST_CONFIRMATION,
2829
- this.postConfirmationLambda
2830
- );
2831
- new DiscoverableStringParameter(this, "user-pool-param", {
2832
- ssmParamName: CognitoUserPool.SSM_PARAM_NAME,
2833
- stringValue: userPool.userPoolId,
2834
- description: "Cognito User Pool ID for this Auth stack; cross-stack reference"
2791
+ createRestApiBaseUrlParameter(apiDomainName) {
2792
+ const restApiBaseUrl = `https://${apiDomainName}`;
2793
+ new DiscoverableStringParameter(this, "rest-api-base-url-param", {
2794
+ ssmParamName: REST_API_BASE_URL_SSM_NAME,
2795
+ stringValue: restApiBaseUrl,
2796
+ description: "REST API base URL for this deployment (E2E, scripts)"
2835
2797
  });
2836
- return userPool;
2837
- }
2838
- /**
2839
- * Grants the Pre Token Generation Lambda read-only access on the data
2840
- * store table and its GSIs. The Lambda only needs:
2841
- * - `Query` on GSI2 to resolve a User by Cognito `sub`
2842
- * - `GetItem` on the base table for direct User reads
2843
- *
2844
- * No write or scan access: a User missing `currentTenant`/`currentWorkspace`
2845
- * falls into the absent-claims path; repair belongs in a separate backfill.
2846
- */
2847
- grantPreTokenGenerationPermissions() {
2848
- const dataStoreTable = this.dataStoreTable();
2849
- const dynamoActions = ["dynamodb:GetItem", "dynamodb:Query"];
2850
- dataStoreTable.grant(this.preTokenGenerationLambda, ...dynamoActions);
2851
- this.preTokenGenerationLambda.addToRolePolicy(
2852
- new PolicyStatement6({
2853
- effect: Effect6.ALLOW,
2854
- actions: [...dynamoActions],
2855
- resources: [`${dataStoreTable.tableArn}/index/*`]
2856
- })
2857
- );
2858
- }
2859
- /**
2860
- * Grants the Post Authentication Lambda permission to call
2861
- * `cognito-idp:AdminUserGlobalSignOut`.
2862
- *
2863
- * Scoped via `Stack.of(this).formatArn` rather than `userPool.userPoolArn`
2864
- * because the User Pool registers this Lambda as a Post Authentication
2865
- * trigger, creating the cycle:
2866
- * userPool → lambda (trigger ARN) → role policy → userPool ARN.
2867
- * Using `formatArn` avoids referencing the User Pool resource directly
2868
- * while still scoping to user pools in this account+region. The Lambda
2869
- * is invoked only by Cognito with a Cognito-provided `event.userPoolId`,
2870
- * so the runtime target is constrained by the trigger contract.
2871
- */
2872
- grantPostAuthenticationPermissions() {
2873
- this.postAuthenticationLambda.addToRolePolicy(
2874
- new PolicyStatement6({
2875
- actions: ["cognito-idp:AdminUserGlobalSignOut"],
2876
- resources: [
2877
- Stack7.of(this).formatArn({
2878
- service: "cognito-idp",
2879
- resource: "userpool",
2880
- resourceName: "*"
2881
- })
2882
- ]
2883
- })
2884
- );
2885
2798
  }
2886
2799
  /**
2887
- * Grants the Post Confirmation Lambda publish-only access to the
2888
- * control-plane event bus. Workflow Lambdas own DynamoDB writes.
2800
+ * Creates the SSM parameter exposing the REST API's custom domain (bare
2801
+ * hostname, no scheme). Consumed by the website service as the CloudFront
2802
+ * `/api/*` origin host.
2803
+ * Look up via {@link OpenHiRestApiService.restApiDomainNameFromConstruct}.
2804
+ * Override to customize.
2889
2805
  */
2890
- grantPostConfirmationPermissions() {
2891
- this.controlEventBus().grantPutEventsTo(this.postConfirmationLambda);
2806
+ createRestApiDomainNameParameter(apiDomainName) {
2807
+ new DiscoverableStringParameter(this, "rest-api-domain-name-param", {
2808
+ ssmParamName: REST_API_DOMAIN_NAME_SSM_NAME,
2809
+ stringValue: apiDomainName,
2810
+ description: "REST API custom domain name (bare hostname) for cross-stack CloudFront origin lookup"
2811
+ });
2892
2812
  }
2893
2813
  /**
2894
- * Creates the User Pool Client and exports its ID to SSM (AUTH service type).
2895
- * Look up via {@link OpenHiAuthService.userPoolClientFromConstruct}.
2814
+ * Creates the API Gateway custom domain name resource.
2896
2815
  * Override to customize.
2897
2816
  */
2898
- createUserPoolClient() {
2899
- const client = new CognitoUserPoolClient(this, {
2900
- userPool: this.userPool
2901
- });
2902
- new DiscoverableStringParameter(this, "user-pool-client-param", {
2903
- ssmParamName: CognitoUserPoolClient.SSM_PARAM_NAME,
2904
- stringValue: client.userPoolClientId,
2905
- description: "Cognito User Pool Client ID for this Auth stack; cross-stack reference"
2817
+ createDomainName(_hostedZone, certificate) {
2818
+ const apiDomainName = this.createApiDomainNameString(_hostedZone);
2819
+ return new DomainName(this, "domain", {
2820
+ domainName: apiDomainName,
2821
+ certificate
2906
2822
  });
2907
- return client;
2908
2823
  }
2909
2824
  /**
2910
- * Creates the User Pool Domain (Cognito hosted UI) and exports domain name to SSM.
2911
- * Look up via {@link OpenHiAuthService.userPoolDomainFromConstruct}.
2912
- * Override to customize.
2825
+ * Creates the Lambda integration, HTTP routes, and API DNS record.
2826
+ * Override to customize. Uses {@link rootHttpApi} set by the constructor.
2913
2827
  */
2914
- createUserPoolDomain() {
2915
- const domain = new CognitoUserPoolDomain(this, {
2916
- userPool: this.userPool,
2917
- cognitoDomain: {
2918
- domainPrefix: `auth-${this.branchHash}`
2919
- }
2920
- });
2921
- new DiscoverableStringParameter(this, "user-pool-domain-param", {
2922
- ssmParamName: CognitoUserPoolDomain.SSM_PARAM_NAME,
2923
- stringValue: domain.domainName,
2924
- description: "Cognito User Pool Domain (hosted UI) for this Auth stack; cross-stack reference"
2925
- });
2926
- return domain;
2927
- }
2928
- };
2929
- _OpenHiAuthService.SERVICE_TYPE = "auth";
2930
- var OpenHiAuthService = _OpenHiAuthService;
2931
-
2932
- // src/services/open-hi-rest-api-service.ts
2933
- var import_config5 = __toESM(require_lib2());
2934
- import {
2935
- CorsHttpMethod,
2936
- DomainName,
2937
- HttpApi as HttpApi2,
2938
- HttpMethod,
2939
- HttpNoneAuthorizer,
2940
- HttpRoute,
2941
- HttpRouteKey
2942
- } from "aws-cdk-lib/aws-apigatewayv2";
2943
- import { HttpUserPoolAuthorizer } from "aws-cdk-lib/aws-apigatewayv2-authorizers";
2944
- import { HttpLambdaIntegration } from "aws-cdk-lib/aws-apigatewayv2-integrations";
2945
- import { Effect as Effect7, PolicyStatement as PolicyStatement7 } from "aws-cdk-lib/aws-iam";
2946
- import {
2947
- ARecord as ARecord3,
2948
- HostedZone as HostedZone3,
2949
- RecordTarget as RecordTarget3
2950
- } from "aws-cdk-lib/aws-route53";
2951
- import { ApiGatewayv2DomainProperties } from "aws-cdk-lib/aws-route53-targets";
2952
- import { Duration as Duration10 } from "aws-cdk-lib/core";
2953
- import { Construct as Construct21 } from "constructs";
2954
-
2955
- // src/data/lambda/cors-options-lambda.ts
2956
- import fs11 from "fs";
2957
- import path11 from "path";
2958
- import { Runtime as Runtime11 } from "aws-cdk-lib/aws-lambda";
2959
- import { NodejsFunction as NodejsFunction11 } from "aws-cdk-lib/aws-lambda-nodejs";
2960
- import { Construct as Construct19 } from "constructs";
2961
- var HANDLER_NAME10 = "cors-options-lambda.handler.js";
2962
- function resolveHandlerEntry10(dirname) {
2963
- const sameDir = path11.join(dirname, HANDLER_NAME10);
2964
- if (fs11.existsSync(sameDir)) {
2965
- return sameDir;
2966
- }
2967
- const fromLib = path11.join(dirname, "..", "..", "..", "lib", HANDLER_NAME10);
2968
- return fromLib;
2969
- }
2970
- var CorsOptionsLambda = class extends Construct19 {
2971
- constructor(scope, id = "cors-options-lambda") {
2972
- super(scope, id);
2973
- this.lambda = new NodejsFunction11(this, "handler", {
2974
- entry: resolveHandlerEntry10(__dirname),
2975
- runtime: Runtime11.NODEJS_LATEST,
2976
- memorySize: 128
2977
- });
2978
- }
2979
- };
2980
-
2981
- // src/data/lambda/rest-api-lambda.ts
2982
- import fs12 from "fs";
2983
- import path12 from "path";
2984
- import { Runtime as Runtime12 } from "aws-cdk-lib/aws-lambda";
2985
- import { NodejsFunction as NodejsFunction12 } from "aws-cdk-lib/aws-lambda-nodejs";
2986
- import { Construct as Construct20 } from "constructs";
2987
- var HANDLER_NAME11 = "rest-api-lambda.handler.js";
2988
- function resolveHandlerEntry11(dirname) {
2989
- const sameDir = path12.join(dirname, HANDLER_NAME11);
2990
- if (fs12.existsSync(sameDir)) {
2991
- return sameDir;
2992
- }
2993
- const fromLib = path12.join(dirname, "..", "..", "..", "lib", HANDLER_NAME11);
2994
- return fromLib;
2995
- }
2996
- var RestApiLambda = class extends Construct20 {
2997
- constructor(scope, props) {
2998
- super(scope, "rest-api-lambda");
2999
- this.lambda = new NodejsFunction12(this, "handler", {
3000
- entry: resolveHandlerEntry11(__dirname),
3001
- runtime: Runtime12.NODEJS_LATEST,
3002
- memorySize: 1024,
3003
- environment: {
3004
- DYNAMO_TABLE_NAME: props.dynamoTableName,
3005
- BRANCH_TAG_VALUE: props.branchTagValue,
3006
- HTTP_API_TAG_VALUE: props.httpApiTagValue,
3007
- OPENHI_PG_CLUSTER_ARN: props.postgresClusterArn,
3008
- OPENHI_PG_SECRET_ARN: props.postgresSecretArn,
3009
- OPENHI_PG_DATABASE: props.postgresDatabase,
3010
- OPENHI_PG_SCHEMA: props.postgresSchema,
3011
- ...props.extraEnvironment
3012
- },
3013
- bundling: {
3014
- minify: true,
3015
- sourceMap: false
3016
- }
3017
- });
3018
- }
3019
- };
3020
-
3021
- // src/services/open-hi-rest-api-service.ts
3022
- var REST_API_BASE_URL_SSM_NAME = "REST_API_BASE_URL";
3023
- var REST_API_DOMAIN_NAME_SSM_NAME = "REST_API_DOMAIN_NAME";
3024
- var DEV_CORS_ALLOW_ORIGINS = [
3025
- "http://localhost:3000",
3026
- "https://localhost:3000",
3027
- "http://localhost:5173",
3028
- "https://localhost:5173",
3029
- "http://127.0.0.1:3000",
3030
- "https://127.0.0.1:3000",
3031
- "http://127.0.0.1:5173",
3032
- "https://127.0.0.1:5173"
3033
- ];
3034
- var _OpenHiRestApiService = class _OpenHiRestApiService extends OpenHiService {
3035
- /**
3036
- * Compose the REST API's full per-deploy domain. Thin wrapper over
3037
- * {@link OpenHiService.composeServiceDomain} that pins `domainPrefix`
3038
- * to {@link API_DOMAIN_PREFIX}.
3039
- *
3040
- * Use from sibling stacks that need to predict the API's hostname
3041
- * before the REST API stack is synthesised.
3042
- */
3043
- static composeFullDomain(opts) {
3044
- return OpenHiService.composeServiceDomain({
3045
- ...opts,
3046
- domainPrefix: _OpenHiRestApiService.API_DOMAIN_PREFIX
3047
- });
3048
- }
3049
- /**
3050
- * Returns an IHttpApi by looking up the REST API stack's HTTP API ID from SSM.
3051
- */
3052
- static rootHttpApiFromConstruct(scope) {
3053
- const httpApiId = DiscoverableStringParameter.valueForLookupName(scope, {
3054
- ssmParamName: RootHttpApi.SSM_PARAM_NAME,
3055
- serviceType: _OpenHiRestApiService.SERVICE_TYPE
3056
- });
3057
- return HttpApi2.fromHttpApiAttributes(scope, "http-api", { httpApiId });
3058
- }
3059
- /**
3060
- * Returns the REST API base URL (e.g. https://api.example.com) by looking it up from SSM.
3061
- * Use in other stacks for E2E, scripts, or config.
3062
- */
3063
- static restApiBaseUrlFromConstruct(scope) {
3064
- return DiscoverableStringParameter.valueForLookupName(scope, {
3065
- ssmParamName: REST_API_BASE_URL_SSM_NAME,
3066
- serviceType: _OpenHiRestApiService.SERVICE_TYPE
3067
- });
3068
- }
3069
- /**
3070
- * Returns the REST API's custom domain name (bare hostname, no scheme — e.g.
3071
- * `api.example.com`) by looking it up from SSM. Use as the host for a
3072
- * CloudFront `HttpOrigin` so the website's distribution can proxy `/api/*`
3073
- * to this stack's API Gateway without per-branch DNS knowledge.
3074
- */
3075
- static restApiDomainNameFromConstruct(scope) {
3076
- return DiscoverableStringParameter.valueForLookupName(scope, {
3077
- ssmParamName: REST_API_DOMAIN_NAME_SSM_NAME,
3078
- serviceType: _OpenHiRestApiService.SERVICE_TYPE
3079
- });
3080
- }
3081
- get serviceType() {
3082
- return _OpenHiRestApiService.SERVICE_TYPE;
3083
- }
3084
- constructor(ohEnv, props = {}) {
3085
- super(ohEnv, _OpenHiRestApiService.SERVICE_TYPE, props);
3086
- this.props = props;
3087
- this.validateConfig(props);
3088
- const hostedZone = this.createHostedZone();
3089
- const certificate = this.createCertificate();
3090
- this.apiDomainName = this.createApiDomainNameString(hostedZone);
3091
- this.createRestApiBaseUrlParameter(this.apiDomainName);
3092
- this.createRestApiDomainNameParameter(this.apiDomainName);
3093
- const domainName = this.createDomainName(hostedZone, certificate);
3094
- this.rootHttpApi = this.createRootHttpApi(domainName);
3095
- this.createRestApiLambdaAndRoutes(hostedZone, domainName);
3096
- }
3097
- /**
3098
- * Validates that config required for the REST API stack is present.
3099
- */
3100
- validateConfig(props) {
3101
- const { config } = props;
3102
- if (!config) {
3103
- throw new Error("Config is required");
3104
- }
3105
- if (!config.hostedZoneId) {
3106
- throw new Error("Hosted zone ID is required");
3107
- }
3108
- if (!config.zoneName) {
3109
- throw new Error("Zone name is required");
3110
- }
3111
- }
3112
- /**
3113
- * Creates the hosted zone reference (imported from config).
3114
- * Override to customize.
3115
- */
3116
- createHostedZone() {
3117
- const { config } = this.props;
3118
- return HostedZone3.fromHostedZoneAttributes(this, "root-zone", {
3119
- hostedZoneId: config.hostedZoneId,
3120
- zoneName: config.zoneName
3121
- });
3122
- }
3123
- /**
3124
- * Creates the wildcard certificate (imported from Global stack via SSM).
3125
- * Override to customize.
3126
- */
3127
- createCertificate() {
3128
- return OpenHiGlobalService.rootWildcardCertificateFromConstruct(this);
3129
- }
3130
- /**
3131
- * Returns the API domain name string (e.g. api.example.com or api-\{prefix\}.example.com).
3132
- * Delegates to {@link OpenHiRestApiService.composeFullDomain} so the
3133
- * release-vs-feature composition stays in one place; picks up
3134
- * `this.defaultReleaseBranch` (not a hard-coded `"main"`).
3135
- * Override to customize.
3136
- */
3137
- createApiDomainNameString(hostedZone) {
3138
- return _OpenHiRestApiService.composeFullDomain({
3139
- branchName: this.branchName,
3140
- defaultReleaseBranch: this.defaultReleaseBranch,
3141
- childZonePrefix: this.childZonePrefix,
3142
- zoneName: hostedZone.zoneName
3143
- });
3144
- }
3145
- /**
3146
- * Creates the SSM parameter for the REST API base URL.
3147
- * Look up via {@link OpenHiRestApiService.restApiBaseUrlFromConstruct}.
3148
- * Override to customize.
3149
- */
3150
- createRestApiBaseUrlParameter(apiDomainName) {
3151
- const restApiBaseUrl = `https://${apiDomainName}`;
3152
- new DiscoverableStringParameter(this, "rest-api-base-url-param", {
3153
- ssmParamName: REST_API_BASE_URL_SSM_NAME,
3154
- stringValue: restApiBaseUrl,
3155
- description: "REST API base URL for this deployment (E2E, scripts)"
3156
- });
3157
- }
3158
- /**
3159
- * Creates the SSM parameter exposing the REST API's custom domain (bare
3160
- * hostname, no scheme). Consumed by the website service as the CloudFront
3161
- * `/api/*` origin host.
3162
- * Look up via {@link OpenHiRestApiService.restApiDomainNameFromConstruct}.
3163
- * Override to customize.
3164
- */
3165
- createRestApiDomainNameParameter(apiDomainName) {
3166
- new DiscoverableStringParameter(this, "rest-api-domain-name-param", {
3167
- ssmParamName: REST_API_DOMAIN_NAME_SSM_NAME,
3168
- stringValue: apiDomainName,
3169
- description: "REST API custom domain name (bare hostname) for cross-stack CloudFront origin lookup"
3170
- });
3171
- }
3172
- /**
3173
- * Creates the API Gateway custom domain name resource.
3174
- * Override to customize.
3175
- */
3176
- createDomainName(_hostedZone, certificate) {
3177
- const apiDomainName = this.createApiDomainNameString(_hostedZone);
3178
- return new DomainName(this, "domain", {
3179
- domainName: apiDomainName,
3180
- certificate
3181
- });
3182
- }
3183
- /**
3184
- * Creates the Lambda integration, HTTP routes, and API DNS record.
3185
- * Override to customize. Uses {@link rootHttpApi} set by the constructor.
3186
- */
3187
- createRestApiLambdaAndRoutes(hostedZone, domainName) {
3188
- const dataStoreTable = OpenHiDataService.dynamoDbDataStoreFromConstruct(this);
3189
- const postgresClusterArn = DataStorePostgresReplica.clusterArnFromConstruct(this);
3190
- const postgresSecretArn = DataStorePostgresReplica.secretArnFromConstruct(this);
3191
- const postgresDatabase = DataStorePostgresReplica.databaseNameFromConstruct(this);
3192
- const postgresSchema = getPostgresReplicaSchemaName(this.branchHash);
3193
- const extraEnvironment = this.resolveRuntimeConfigEnvVars();
3194
- const { lambda } = new RestApiLambda(this, {
3195
- dynamoTableName: dataStoreTable.tableName,
3196
- branchTagValue: this.branchName,
3197
- httpApiTagValue: RootHttpApi.SSM_PARAM_NAME,
3198
- postgresClusterArn,
3199
- postgresSecretArn,
3200
- postgresDatabase,
3201
- postgresSchema,
3202
- extraEnvironment
2828
+ createRestApiLambdaAndRoutes(hostedZone, domainName) {
2829
+ const dataStoreTable = OpenHiDataService.dynamoDbDataStoreFromConstruct(this);
2830
+ const postgresClusterArn = DataStorePostgresReplica.clusterArnFromConstruct(this);
2831
+ const postgresSecretArn = DataStorePostgresReplica.secretArnFromConstruct(this);
2832
+ const postgresDatabase = DataStorePostgresReplica.databaseNameFromConstruct(this);
2833
+ const postgresSchema = getPostgresReplicaSchemaName(this.branchHash);
2834
+ const extraEnvironment = this.resolveRuntimeConfigEnvVars();
2835
+ const { lambda } = new RestApiLambda(this, {
2836
+ dynamoTableName: dataStoreTable.tableName,
2837
+ branchTagValue: this.branchName,
2838
+ httpApiTagValue: RootHttpApi.SSM_PARAM_NAME,
2839
+ postgresClusterArn,
2840
+ postgresSecretArn,
2841
+ postgresDatabase,
2842
+ postgresSchema,
2843
+ extraEnvironment
3203
2844
  });
3204
2845
  lambda.addToRolePolicy(
3205
- new PolicyStatement7({
3206
- effect: Effect7.ALLOW,
2846
+ new PolicyStatement5({
2847
+ effect: Effect5.ALLOW,
3207
2848
  actions: [
3208
2849
  "rds-data:ExecuteStatement",
3209
2850
  "rds-data:BatchExecuteStatement"
@@ -3212,8 +2853,8 @@ var _OpenHiRestApiService = class _OpenHiRestApiService extends OpenHiService {
3212
2853
  })
3213
2854
  );
3214
2855
  lambda.addToRolePolicy(
3215
- new PolicyStatement7({
3216
- effect: Effect7.ALLOW,
2856
+ new PolicyStatement5({
2857
+ effect: Effect5.ALLOW,
3217
2858
  actions: ["secretsmanager:GetSecretValue"],
3218
2859
  resources: [postgresSecretArn]
3219
2860
  })
@@ -3231,15 +2872,15 @@ var _OpenHiRestApiService = class _OpenHiRestApiService extends OpenHiService {
3231
2872
  ];
3232
2873
  dataStoreTable.grant(lambda, ...dynamoActions);
3233
2874
  lambda.addToRolePolicy(
3234
- new PolicyStatement7({
3235
- effect: Effect7.ALLOW,
2875
+ new PolicyStatement5({
2876
+ effect: Effect5.ALLOW,
3236
2877
  actions: [...dynamoActions],
3237
2878
  resources: [`${dataStoreTable.tableArn}/index/*`]
3238
2879
  })
3239
2880
  );
3240
2881
  lambda.addToRolePolicy(
3241
- new PolicyStatement7({
3242
- effect: Effect7.ALLOW,
2882
+ new PolicyStatement5({
2883
+ effect: Effect5.ALLOW,
3243
2884
  actions: [
3244
2885
  "ssm:GetParameter",
3245
2886
  "ssm:GetParameters",
@@ -3320,11 +2961,18 @@ var _OpenHiRestApiService = class _OpenHiRestApiService extends OpenHiService {
3320
2961
  const { corsPreflight: cors, ...restRootHttpApiProps } = this.props.rootHttpApiProps ?? {};
3321
2962
  const isNonProd = this.ohEnv.ohStage.stageType !== import_config5.OPEN_HI_STAGE.PROD;
3322
2963
  const callerOrigins = cors?.allowOrigins ?? [];
3323
- const mergedOrigins = isNonProd ? Array.from(/* @__PURE__ */ new Set([...callerOrigins, ...DEV_CORS_ALLOW_ORIGINS])) : callerOrigins;
3324
- const corsPreflight = cors !== void 0 || isNonProd ? this.buildCorsPreflightOptions(mergedOrigins, cors) : void 0;
2964
+ const autoOrigins = this.resolveAutoInjectedCorsOrigins();
2965
+ const mergedOrigins = Array.from(
2966
+ /* @__PURE__ */ new Set([
2967
+ ...callerOrigins,
2968
+ ...autoOrigins,
2969
+ ...isNonProd ? DEV_CORS_ALLOW_ORIGINS : []
2970
+ ])
2971
+ );
2972
+ const corsPreflight = this.buildCorsPreflightOptions(mergedOrigins, cors);
3325
2973
  const rootHttpApi = new RootHttpApi(this, {
3326
2974
  ...restRootHttpApiProps,
3327
- ...corsPreflight !== void 0 && { corsPreflight },
2975
+ corsPreflight,
3328
2976
  defaultDomainMapping: {
3329
2977
  domainName,
3330
2978
  mappingKey: void 0
@@ -3338,6 +2986,33 @@ var _OpenHiRestApiService = class _OpenHiRestApiService extends OpenHiService {
3338
2986
  });
3339
2987
  return rootHttpApi;
3340
2988
  }
2989
+ /**
2990
+ * Returns the admin-console and marketing-website origins this REST API
2991
+ * stack should accept by default, composed from the same branch context
2992
+ * the website service will see at synth time. Both hostnames are
2993
+ * `https://`-only — they always resolve to real DNS records.
2994
+ *
2995
+ * Auto-injected on every stage (no `isNonProd` gate) so the admin SPA can
2996
+ * call the API cross-origin without the caller having to predict the
2997
+ * per-deploy hostname. Override to customize the auto-injected set.
2998
+ */
2999
+ resolveAutoInjectedCorsOrigins() {
3000
+ const zoneName = this.props.config.zoneName;
3001
+ const adminHost = OpenHiWebsiteService.composeFullDomain({
3002
+ domainPrefix: ADMIN_DOMAIN_PREFIX,
3003
+ branchName: this.branchName,
3004
+ defaultReleaseBranch: this.defaultReleaseBranch,
3005
+ childZonePrefix: this.childZonePrefix,
3006
+ zoneName
3007
+ });
3008
+ const websiteHost = OpenHiWebsiteService.composeFullDomain({
3009
+ branchName: this.branchName,
3010
+ defaultReleaseBranch: this.defaultReleaseBranch,
3011
+ childZonePrefix: this.childZonePrefix,
3012
+ zoneName
3013
+ });
3014
+ return [`https://${adminHost}`, `https://${websiteHost}`];
3015
+ }
3341
3016
  /**
3342
3017
  * Builds the full `CorsPreflightOptions` from a merged origins array,
3343
3018
  * filling defaults for `allowMethods`/`allowHeaders`/`allowCredentials`/
@@ -3357,7 +3032,7 @@ var _OpenHiRestApiService = class _OpenHiRestApiService extends OpenHiService {
3357
3032
  ],
3358
3033
  allowHeaders: cors?.allowHeaders ?? ["Content-Type", "Authorization"],
3359
3034
  allowCredentials: cors?.allowCredentials ?? true,
3360
- maxAge: cors?.maxAge ?? Duration10.days(1),
3035
+ maxAge: cors?.maxAge ?? Duration9.days(1),
3361
3036
  ...cors?.exposeHeaders !== void 0 && {
3362
3037
  exposeHeaders: cors.exposeHeaders
3363
3038
  }
@@ -3375,7 +3050,7 @@ var _OpenHiRestApiService = class _OpenHiRestApiService extends OpenHiService {
3375
3050
  * client-side from `window.location.origin`.
3376
3051
  */
3377
3052
  resolveRuntimeConfigEnvVars() {
3378
- const cognitoScope = new Construct21(this, "runtime-config");
3053
+ const cognitoScope = new Construct19(this, "runtime-config");
3379
3054
  const userPool = OpenHiAuthService.userPoolFromConstruct(cognitoScope);
3380
3055
  const userPoolClient = OpenHiAuthService.userPoolClientFromConstruct(cognitoScope);
3381
3056
  const cognitoDomainUrl = OpenHiAuthService.userPoolDomainBaseUrlFromConstruct(cognitoScope);
@@ -3386,342 +3061,761 @@ var _OpenHiRestApiService = class _OpenHiRestApiService extends OpenHiService {
3386
3061
  OPENHI_RUNTIME_CONFIG_API_BASE_URL: `https://${this.apiDomainName}`
3387
3062
  };
3388
3063
  }
3389
- };
3390
- _OpenHiRestApiService.SERVICE_TYPE = "rest-api";
3391
- /**
3392
- * Sub-domain prefix used by the REST API. Release-branch hostname is
3393
- * `api.<zone>`; per-PR preview hostname is `api-<childZonePrefix>.<zone>`.
3394
- */
3395
- _OpenHiRestApiService.API_DOMAIN_PREFIX = "api";
3396
- var OpenHiRestApiService = _OpenHiRestApiService;
3397
-
3398
- // src/services/open-hi-graphql-service.ts
3399
- import {
3400
- AuthorizationType,
3401
- UserPoolDefaultAction
3402
- } from "aws-cdk-lib/aws-appsync";
3403
- var _OpenHiGraphqlService = class _OpenHiGraphqlService extends OpenHiService {
3064
+ };
3065
+ _OpenHiRestApiService.SERVICE_TYPE = "rest-api";
3066
+ /**
3067
+ * Sub-domain prefix used by the REST API. Release-branch hostname is
3068
+ * `api.<zone>`; per-PR preview hostname is `api-<childZonePrefix>.<zone>`.
3069
+ */
3070
+ _OpenHiRestApiService.API_DOMAIN_PREFIX = "api";
3071
+ var OpenHiRestApiService = _OpenHiRestApiService;
3072
+
3073
+ // src/services/open-hi-website-service.ts
3074
+ var SSM_PARAM_NAME_FULL_DOMAIN = "WEBSITE_FULL_DOMAIN";
3075
+ var ADMIN_DOMAIN_PREFIX = "admin";
3076
+ var _OpenHiWebsiteService = class _OpenHiWebsiteService extends OpenHiService {
3077
+ /**
3078
+ * Compose the website's full per-deploy domain. Thin wrapper over
3079
+ * {@link OpenHiService.composeServiceDomain} that fills in
3080
+ * {@link DEFAULT_DOMAIN_PREFIX} when `domainPrefix` is omitted.
3081
+ *
3082
+ * Use from sibling stacks that need to predict the website's hostname
3083
+ * before the website stack is synthesised — e.g. the REST API stack
3084
+ * computing its CORS `allowOrigins` for the admin-console.
3085
+ */
3086
+ static composeFullDomain(opts) {
3087
+ return OpenHiService.composeServiceDomain({
3088
+ ...opts,
3089
+ domainPrefix: opts.domainPrefix ?? _OpenHiWebsiteService.DEFAULT_DOMAIN_PREFIX
3090
+ });
3091
+ }
3092
+ /**
3093
+ * Looks up the static-hosting bucket ARN published by the release-branch
3094
+ * deploy of this service.
3095
+ */
3096
+ static bucketArnFromConstruct(scope) {
3097
+ return DiscoverableStringParameter.valueForLookupName(scope, {
3098
+ ssmParamName: StaticHosting.SSM_PARAM_NAME_BUCKET_ARN,
3099
+ serviceType: _OpenHiWebsiteService.SERVICE_TYPE
3100
+ });
3101
+ }
3102
+ /**
3103
+ * Looks up the CloudFront distribution ARN published by the release-branch
3104
+ * deploy of this service.
3105
+ */
3106
+ static distributionArnFromConstruct(scope) {
3107
+ return DiscoverableStringParameter.valueForLookupName(scope, {
3108
+ ssmParamName: StaticHosting.SSM_PARAM_NAME_DISTRIBUTION_ARN,
3109
+ serviceType: _OpenHiWebsiteService.SERVICE_TYPE
3110
+ });
3111
+ }
3112
+ /**
3113
+ * Looks up the CloudFront distribution domain
3114
+ * (e.g. dXXXXX.cloudfront.net) published by the release-branch deploy.
3115
+ */
3116
+ static distributionDomainFromConstruct(scope) {
3117
+ return DiscoverableStringParameter.valueForLookupName(scope, {
3118
+ ssmParamName: StaticHosting.SSM_PARAM_NAME_DISTRIBUTION_DOMAIN,
3119
+ serviceType: _OpenHiWebsiteService.SERVICE_TYPE
3120
+ });
3121
+ }
3122
+ /**
3123
+ * Looks up the CloudFront distribution ID published by the release-branch
3124
+ * deploy of this service.
3125
+ */
3126
+ static distributionIdFromConstruct(scope) {
3127
+ return DiscoverableStringParameter.valueForLookupName(scope, {
3128
+ ssmParamName: StaticHosting.SSM_PARAM_NAME_DISTRIBUTION_ID,
3129
+ serviceType: _OpenHiWebsiteService.SERVICE_TYPE
3130
+ });
3131
+ }
3132
+ /**
3133
+ * Looks up the website's full domain (e.g. www.example.com) published by
3134
+ * the release-branch deploy of this service.
3135
+ */
3136
+ static fullDomainFromConstruct(scope) {
3137
+ return DiscoverableStringParameter.valueForLookupName(scope, {
3138
+ ssmParamName: SSM_PARAM_NAME_FULL_DOMAIN,
3139
+ serviceType: _OpenHiWebsiteService.SERVICE_TYPE
3140
+ });
3141
+ }
3142
+ get serviceType() {
3143
+ return _OpenHiWebsiteService.SERVICE_TYPE;
3144
+ }
3145
+ constructor(ohEnv, props) {
3146
+ super(ohEnv, _OpenHiWebsiteService.SERVICE_TYPE, props);
3147
+ this.props = props;
3148
+ this.validateConfig(props);
3149
+ const isReleaseBranch = this.branchName === this.defaultReleaseBranch;
3150
+ const hostedZone = this.createHostedZone();
3151
+ this.fullDomain = this.computeFullDomain(hostedZone);
3152
+ const shouldCreateHostingInfra = props.createHostingInfrastructure ?? isReleaseBranch;
3153
+ if (shouldCreateHostingInfra) {
3154
+ const certificate = this.createCertificate();
3155
+ this.staticHosting = this.createStaticHosting({
3156
+ certificate,
3157
+ hostedZone
3158
+ });
3159
+ this.createFullDomainParameter();
3160
+ } else if (!isReleaseBranch) {
3161
+ this.perBranchHostname = this.createPerBranchHostname(hostedZone);
3162
+ }
3163
+ if (props.createStaticContent !== false) {
3164
+ const bucket = this.resolveStaticHostingBucket();
3165
+ this.staticContent = this.createStaticContent(bucket);
3166
+ }
3167
+ }
3168
+ /**
3169
+ * Validates that config required for the website stack is present.
3170
+ */
3171
+ validateConfig(props) {
3172
+ const { config } = props;
3173
+ if (!config) {
3174
+ throw new Error("Config is required");
3175
+ }
3176
+ if (!config.zoneName) {
3177
+ throw new Error("Zone name is required");
3178
+ }
3179
+ if (!config.hostedZoneId) {
3180
+ throw new Error("Hosted zone ID is required to import the website zone");
3181
+ }
3182
+ }
3183
+ /**
3184
+ * Imports the website's hosted zone from config attributes (no SSM lookup).
3185
+ * The website attaches DNS records here on the release-branch deploy and
3186
+ * the same zone is imported on feature-branch deploys for any sub-domain
3187
+ * routing.
3188
+ * Override to customize.
3189
+ */
3190
+ createHostedZone() {
3191
+ return OpenHiGlobalService.rootHostedZoneFromConstruct(this, {
3192
+ zoneName: this.config.zoneName,
3193
+ hostedZoneId: this.config.hostedZoneId
3194
+ });
3195
+ }
3196
+ /**
3197
+ * Returns the wildcard certificate looked up from the Global service.
3198
+ * Override to customize.
3199
+ */
3200
+ createCertificate() {
3201
+ return OpenHiGlobalService.rootWildcardCertificateFromConstruct(this);
3202
+ }
3203
+ /**
3204
+ * Computes the full website domain from `domainPrefix`,
3205
+ * `childZonePrefix`, and the child zone name. Release-branch deploys
3206
+ * serve at `\<domainPrefix\>.\<zone\>` (e.g. `admin.dev.openhi.org`);
3207
+ * every other deploy serves a per-PR preview at
3208
+ * `\<domainPrefix\>-\<childZonePrefix\>.\<zone\>`
3209
+ * (e.g. `admin-feat-1093-patient-migration.dev.openhi.org`).
3210
+ *
3211
+ * Delegates to {@link OpenHiWebsiteService.composeFullDomain} so the
3212
+ * release-vs-feature composition stays in one place.
3213
+ */
3214
+ computeFullDomain(hostedZone) {
3215
+ return _OpenHiWebsiteService.composeFullDomain({
3216
+ domainPrefix: this.props.domainPrefix,
3217
+ branchName: this.branchName,
3218
+ defaultReleaseBranch: this.defaultReleaseBranch,
3219
+ childZonePrefix: this.childZonePrefix,
3220
+ zoneName: hostedZone.zoneName
3221
+ });
3222
+ }
3223
+ /**
3224
+ * Returns the sub-domain label (left of the zone) for the current
3225
+ * deploy. Used for the per-branch S3 key prefix passed to
3226
+ * {@link StaticContent} so the upload prefix always matches the
3227
+ * served hostname.
3228
+ *
3229
+ * Non-release deploys compose the per-PR slug as
3230
+ * `\<domainPrefix\>-\<childZonePrefix\>`, mirroring the REST API's
3231
+ * `api-\<childZonePrefix\>` convention. When `domainPrefix` is `admin`
3232
+ * (the only consumer today), the resulting sub-domain starts with
3233
+ * {@link PER_BRANCH_PREVIEW_PREFIX}, so the per-PR S3 key prefix
3234
+ * matches what `StaticHosting`'s lifecycle rule expires.
3235
+ */
3236
+ computeSubDomain() {
3237
+ const isReleaseBranch = this.branchName === this.defaultReleaseBranch;
3238
+ const domainPrefix = this.props.domainPrefix ?? _OpenHiWebsiteService.DEFAULT_DOMAIN_PREFIX;
3239
+ if (isReleaseBranch) {
3240
+ return domainPrefix;
3241
+ }
3242
+ return `${domainPrefix}-${this.childZonePrefix}`;
3243
+ }
3244
+ /**
3245
+ * Creates the StaticHosting infrastructure (bucket + distribution +
3246
+ * Lambda@Edge + 4 SSM params + DNS). The release-branch distribution
3247
+ * adds `*.\<zone\>` as a wildcard alt-name on top of the canonical
3248
+ * hostname so per-PR previews resolve via the same distribution.
3249
+ *
3250
+ * The bucket carries an S3 lifecycle rule that expires per-PR
3251
+ * preview content (keys under {@link PER_BRANCH_PREVIEW_PREFIX})
3252
+ * on non-production stages. PROD never gets the rule — see
3253
+ * `enablePreviewLifecycle`.
3254
+ */
3255
+ createStaticHosting(deps) {
3256
+ const restApi = this.props.restApi === true ? this.resolveRestApi() : void 0;
3257
+ const wildcardSan = `*.${deps.hostedZone.zoneName}`;
3258
+ return new StaticHosting(this, "static-hosting", {
3259
+ serviceType: _OpenHiWebsiteService.SERVICE_TYPE,
3260
+ certificate: deps.certificate,
3261
+ hostedZone: deps.hostedZone,
3262
+ domainNames: [this.fullDomain, wildcardSan],
3263
+ description: `OpenHI website (${this.fullDomain})`,
3264
+ prefixPattern: PER_BRANCH_PREVIEW_PREFIX,
3265
+ enablePreviewLifecycle: this.ohEnv.ohStage.stageType !== import_config6.OPEN_HI_STAGE.PROD,
3266
+ ...restApi !== void 0 && { restApi }
3267
+ });
3268
+ }
3269
+ /**
3270
+ * Resolves the REST API custom-domain hostname from the rest-api stack's
3271
+ * `REST_API_DOMAIN_NAME` SSM parameter. Wrapped in a private method so
3272
+ * it can be overridden / stubbed in subclasses and tests.
3273
+ */
3274
+ resolveRestApi() {
3275
+ return {
3276
+ domainName: OpenHiRestApiService.restApiDomainNameFromConstruct(this)
3277
+ };
3278
+ }
3279
+ /**
3280
+ * Creates the SSM parameter that publishes the website's full domain.
3281
+ * Look up via {@link OpenHiWebsiteService.fullDomainFromConstruct}.
3282
+ */
3283
+ createFullDomainParameter() {
3284
+ new DiscoverableStringParameter(this, "full-domain-param", {
3285
+ ssmParamName: SSM_PARAM_NAME_FULL_DOMAIN,
3286
+ serviceType: _OpenHiWebsiteService.SERVICE_TYPE,
3287
+ stringValue: this.fullDomain,
3288
+ description: "Full website domain (e.g. www.example.com)"
3289
+ });
3290
+ }
3291
+ /**
3292
+ * Creates the StaticContent uploader. Receives the resolved static-hosting
3293
+ * bucket from the constructor — on the release-branch deploy this is the
3294
+ * just-created {@link staticHosting} bucket (no SSM round-trip within a
3295
+ * single stack); on every other deploy it is imported from the bucket ARN
3296
+ * the release-branch deploy publishes to SSM, addressed against
3297
+ * {@link OpenHiService.releaseBranchHash}. See
3298
+ * {@link resolveStaticHostingBucket}.
3299
+ *
3300
+ * The S3 key prefix is `\<sub-domain\>.\<zone\>/\<contentDest\>` so the
3301
+ * upload location matches the Host-header-derived folder the Lambda@Edge
3302
+ * viewer-request handler prepends. Passing the zone name (rather than
3303
+ * `this.fullDomain`) for the `fullDomain` prop keeps the prefix flat —
3304
+ * `admin-feat-foo.dev.openhi.org/`, not
3305
+ * `admin-feat-foo.admin.dev.openhi.org/`.
3306
+ */
3307
+ createStaticContent(bucket) {
3308
+ const { contentSourceDirectory, contentDestinationDirectory } = this.props;
3309
+ return new StaticContent(this, "static-content", {
3310
+ bucket,
3311
+ contentSourceDirectory,
3312
+ contentDestinationDirectory,
3313
+ subDomain: this.computeSubDomain(),
3314
+ fullDomain: this.config.zoneName
3315
+ });
3316
+ }
3317
+ /**
3318
+ * Creates the per-PR `PerBranchHostname` alias record on non-release
3319
+ * branch deploys. The record points
3320
+ * `\<domainPrefix\>-\<childZonePrefix\>.\<zone\>` at the release-branch
3321
+ * CloudFront distribution (resolved from SSM against
3322
+ * {@link OpenHiService.releaseBranchHash}).
3323
+ */
3324
+ createPerBranchHostname(hostedZone) {
3325
+ return new PerBranchHostname(this, "per-branch-hostname", {
3326
+ hostname: this.fullDomain,
3327
+ hostedZone,
3328
+ serviceType: _OpenHiWebsiteService.SERVICE_TYPE
3329
+ });
3330
+ }
3331
+ /**
3332
+ * Returns an {@link IBucket} pointing at the static-hosting bucket the
3333
+ * uploaders write to. On the release-branch deploy this is the bucket
3334
+ * just provisioned by {@link staticHosting}; on every other deploy it's
3335
+ * imported from the bucket ARN the release-branch deploy publishes to
3336
+ * SSM, addressed against {@link OpenHiService.releaseBranchHash}.
3337
+ */
3338
+ resolveStaticHostingBucket() {
3339
+ if (this.staticHosting) {
3340
+ return this.staticHosting.bucket;
3341
+ }
3342
+ const bucketArn = DiscoverableStringParameter.valueForLookupName(this, {
3343
+ ssmParamName: StaticHosting.SSM_PARAM_NAME_BUCKET_ARN,
3344
+ serviceType: _OpenHiWebsiteService.SERVICE_TYPE,
3345
+ branchHash: this.releaseBranchHash
3346
+ });
3347
+ return Bucket3.fromBucketArn(this, "shared-bucket", bucketArn);
3348
+ }
3349
+ };
3350
+ _OpenHiWebsiteService.SERVICE_TYPE = "website";
3351
+ /**
3352
+ * Default `domainPrefix` for this service when none is supplied.
3353
+ * Release-branch hostname is `www.<zone>`; per-PR preview hostname is
3354
+ * `www-<childZonePrefix>.<zone>`.
3355
+ */
3356
+ _OpenHiWebsiteService.DEFAULT_DOMAIN_PREFIX = "www";
3357
+ var OpenHiWebsiteService = _OpenHiWebsiteService;
3358
+
3359
+ // src/workflows/control-plane/user-onboarding/provision-default-workspace-lambda.ts
3360
+ import fs12 from "fs";
3361
+ import path12 from "path";
3362
+ import { Duration as Duration10 } from "aws-cdk-lib";
3363
+ import { Rule as Rule4 } from "aws-cdk-lib/aws-events";
3364
+ import { LambdaFunction as LambdaFunction4 } from "aws-cdk-lib/aws-events-targets";
3365
+ import { Effect as Effect6, PolicyStatement as PolicyStatement6 } from "aws-cdk-lib/aws-iam";
3366
+ import { Runtime as Runtime12 } from "aws-cdk-lib/aws-lambda";
3367
+ import { NodejsFunction as NodejsFunction12 } from "aws-cdk-lib/aws-lambda-nodejs";
3368
+ import { Construct as Construct20 } from "constructs";
3369
+ var HANDLER_NAME11 = "provision-default-workspace.handler.js";
3370
+ function resolveHandlerEntry11(dirname) {
3371
+ const sameDir = path12.join(dirname, HANDLER_NAME11);
3372
+ if (fs12.existsSync(sameDir)) {
3373
+ return sameDir;
3374
+ }
3375
+ return path12.join(dirname, "..", "..", "..", "..", "lib", HANDLER_NAME11);
3376
+ }
3377
+ var ProvisionDefaultWorkspaceLambda = class extends Construct20 {
3378
+ constructor(scope, props) {
3379
+ super(scope, "provision-default-workspace-lambda");
3380
+ this.lambda = new NodejsFunction12(this, "handler", {
3381
+ entry: resolveHandlerEntry11(__dirname),
3382
+ runtime: Runtime12.NODEJS_LATEST,
3383
+ memorySize: 1024,
3384
+ environment: {
3385
+ DYNAMO_TABLE_NAME: props.dataStoreTable.tableName
3386
+ }
3387
+ });
3388
+ props.dataStoreTable.grant(
3389
+ this.lambda,
3390
+ "dynamodb:PutItem",
3391
+ "dynamodb:UpdateItem"
3392
+ );
3393
+ this.lambda.addToRolePolicy(
3394
+ new PolicyStatement6({
3395
+ effect: Effect6.ALLOW,
3396
+ actions: ["dynamodb:Query"],
3397
+ resources: [`${props.dataStoreTable.tableArn}/index/*`]
3398
+ })
3399
+ );
3400
+ this.rule = new Rule4(this, "rule", {
3401
+ eventBus: props.controlEventBus,
3402
+ eventPattern: {
3403
+ source: [USER_ONBOARDING_EVENT_SOURCE],
3404
+ detailType: [PROVISION_DEFAULT_WORKSPACE_DETAIL_TYPE]
3405
+ },
3406
+ targets: [
3407
+ new LambdaFunction4(this.lambda, {
3408
+ retryAttempts: 2,
3409
+ maxEventAge: Duration10.hours(2)
3410
+ })
3411
+ ]
3412
+ });
3413
+ }
3414
+ };
3415
+
3416
+ // src/workflows/control-plane/user-onboarding/user-onboarding-workflow.ts
3417
+ import { Construct as Construct21 } from "constructs";
3418
+ var UserOnboardingWorkflow = class extends Construct21 {
3419
+ constructor(scope, props) {
3420
+ super(scope, "user-onboarding-workflow");
3421
+ this.provisionDefaultWorkspace = new ProvisionDefaultWorkspaceLambda(this, {
3422
+ dataStoreTable: props.dataStoreTable,
3423
+ controlEventBus: props.controlEventBus
3424
+ });
3425
+ }
3426
+ };
3427
+
3428
+ // src/services/open-hi-auth-service.ts
3429
+ var LOCALHOST_OAUTH_CALLBACK_URLS = [
3430
+ "http://localhost:3000/oauth/callback",
3431
+ "https://localhost:3000/oauth/callback"
3432
+ ];
3433
+ var LOCALHOST_OAUTH_LOGOUT_URLS = [
3434
+ "http://localhost:3000/oauth/logout",
3435
+ "https://localhost:3000/oauth/logout"
3436
+ ];
3437
+ var _OpenHiAuthService = class _OpenHiAuthService extends OpenHiService {
3438
+ constructor(ohEnv, props = {}) {
3439
+ super(ohEnv, _OpenHiAuthService.SERVICE_TYPE, props);
3440
+ /**
3441
+ * Cross-stack reference to the data store table. Cached so repeated
3442
+ * lookups share a single CDK construct id ("dynamo-db-data-store") in
3443
+ * this stack — a second `Table.fromTableName` call under the same scope
3444
+ * would collide.
3445
+ */
3446
+ this._dataStoreTable = null;
3447
+ this._controlEventBus = null;
3448
+ this.props = props;
3449
+ this.userPoolKmsKey = this.createUserPoolKmsKey();
3450
+ this.preTokenGenerationLambda = this.createPreTokenGenerationLambda();
3451
+ this.postAuthenticationLambda = this.createPostAuthenticationLambda();
3452
+ this.postConfirmationLambda = this.createPostConfirmationLambda();
3453
+ this.userOnboardingWorkflow = this.createUserOnboardingWorkflow();
3454
+ this.userPool = this.createUserPool();
3455
+ this.grantPreTokenGenerationPermissions();
3456
+ this.grantPostAuthenticationPermissions();
3457
+ this.grantPostConfirmationPermissions();
3458
+ this.userPoolClient = this.createUserPoolClient();
3459
+ this.userPoolDomain = this.createUserPoolDomain();
3460
+ }
3404
3461
  /**
3405
- * Returns the GraphQL API by looking up the GraphQL stack's API ID from SSM.
3406
- * Use from other stacks to obtain an IGraphqlApi reference.
3462
+ * Returns an IUserPool by looking up the Auth stack's User Pool ID from SSM.
3407
3463
  */
3408
- static graphqlApiFromConstruct(scope) {
3409
- return RootGraphqlApi.fromConstruct(scope);
3410
- }
3411
- get serviceType() {
3412
- return _OpenHiGraphqlService.SERVICE_TYPE;
3413
- }
3414
- constructor(ohEnv, props = {}) {
3415
- super(ohEnv, _OpenHiGraphqlService.SERVICE_TYPE, props);
3416
- this.props = props;
3417
- this.rootGraphqlApi = this.createRootGraphqlApi();
3464
+ static userPoolFromConstruct(scope) {
3465
+ const userPoolId = DiscoverableStringParameter.valueForLookupName(scope, {
3466
+ ssmParamName: CognitoUserPool.SSM_PARAM_NAME,
3467
+ serviceType: _OpenHiAuthService.SERVICE_TYPE
3468
+ });
3469
+ return UserPool2.fromUserPoolId(scope, "user-pool", userPoolId);
3418
3470
  }
3419
- /** Creates the root GraphQL API with Cognito user pool. */
3420
- createRootGraphqlApi() {
3421
- const userPool = OpenHiAuthService.userPoolFromConstruct(this);
3422
- return new RootGraphqlApi(this, {
3423
- authorizationConfig: {
3424
- defaultAuthorization: {
3425
- authorizationType: AuthorizationType.USER_POOL,
3426
- userPoolConfig: {
3427
- userPool,
3428
- defaultAction: UserPoolDefaultAction.ALLOW
3429
- }
3430
- }
3471
+ /**
3472
+ * Returns an IUserPoolClient by looking up the Auth stack's User Pool Client ID from SSM.
3473
+ */
3474
+ static userPoolClientFromConstruct(scope) {
3475
+ const userPoolClientId = DiscoverableStringParameter.valueForLookupName(
3476
+ scope,
3477
+ {
3478
+ ssmParamName: CognitoUserPoolClient.SSM_PARAM_NAME,
3479
+ serviceType: _OpenHiAuthService.SERVICE_TYPE
3431
3480
  }
3481
+ );
3482
+ return UserPoolClient2.fromUserPoolClientId(
3483
+ scope,
3484
+ "user-pool-client",
3485
+ userPoolClientId
3486
+ );
3487
+ }
3488
+ /**
3489
+ * Returns an IUserPoolDomain by looking up the Auth stack's User Pool Domain from SSM.
3490
+ */
3491
+ static userPoolDomainFromConstruct(scope) {
3492
+ const domainName = DiscoverableStringParameter.valueForLookupName(scope, {
3493
+ ssmParamName: CognitoUserPoolDomain.SSM_PARAM_NAME,
3494
+ serviceType: _OpenHiAuthService.SERVICE_TYPE
3432
3495
  });
3496
+ return UserPoolDomain2.fromDomainName(scope, "user-pool-domain", domainName);
3433
3497
  }
3434
- };
3435
- _OpenHiGraphqlService.SERVICE_TYPE = "graphql-api";
3436
- var OpenHiGraphqlService = _OpenHiGraphqlService;
3437
-
3438
- // src/services/open-hi-website-service.ts
3439
- var import_config6 = __toESM(require_lib2());
3440
- import { Bucket as Bucket3 } from "aws-cdk-lib/aws-s3";
3441
- var SSM_PARAM_NAME_FULL_DOMAIN = "WEBSITE_FULL_DOMAIN";
3442
- var ADMIN_DOMAIN_PREFIX = "admin";
3443
- var _OpenHiWebsiteService = class _OpenHiWebsiteService extends OpenHiService {
3444
3498
  /**
3445
- * Compose the website's full per-deploy domain. Thin wrapper over
3446
- * {@link OpenHiService.composeServiceDomain} that fills in
3447
- * {@link DEFAULT_DOMAIN_PREFIX} when `domainPrefix` is omitted.
3499
+ * Returns the full Cognito Hosted UI base URL (e.g.
3500
+ * `https://auth-abc.auth.us-east-2.amazoncognito.com`) by looking up
3501
+ * the Auth stack's User Pool Domain from SSM and composing it with the
3502
+ * calling stack's region.
3448
3503
  *
3449
- * Use from sibling stacks that need to predict the website's hostname
3450
- * before the website stack is synthesised e.g. the REST API stack
3451
- * computing its CORS `allowOrigins` for the admin-console.
3504
+ * Equivalent to `UserPoolDomain.baseUrl()` on the concrete construct,
3505
+ * but works across stacks where the looked-up `IUserPoolDomain` is an
3506
+ * `Import` and does not carry the `baseUrl()` method. Assumes the
3507
+ * domain was created as a Cognito-managed prefix domain (the only
3508
+ * variant `OpenHiAuthService.createUserPoolDomain` produces).
3452
3509
  */
3453
- static composeFullDomain(opts) {
3454
- return OpenHiService.composeServiceDomain({
3455
- ...opts,
3456
- domainPrefix: opts.domainPrefix ?? _OpenHiWebsiteService.DEFAULT_DOMAIN_PREFIX
3510
+ static userPoolDomainBaseUrlFromConstruct(scope) {
3511
+ const domainName = DiscoverableStringParameter.valueForLookupName(scope, {
3512
+ ssmParamName: CognitoUserPoolDomain.SSM_PARAM_NAME,
3513
+ serviceType: _OpenHiAuthService.SERVICE_TYPE
3457
3514
  });
3515
+ const region = Stack7.of(scope).region;
3516
+ return `https://${domainName}.auth.${region}.amazoncognito.com`;
3458
3517
  }
3459
3518
  /**
3460
- * Looks up the static-hosting bucket ARN published by the release-branch
3461
- * deploy of this service.
3519
+ * Returns an IKey (KMS) by looking up the Auth stack's User Pool KMS Key ARN from SSM.
3462
3520
  */
3463
- static bucketArnFromConstruct(scope) {
3464
- return DiscoverableStringParameter.valueForLookupName(scope, {
3465
- ssmParamName: StaticHosting.SSM_PARAM_NAME_BUCKET_ARN,
3466
- serviceType: _OpenHiWebsiteService.SERVICE_TYPE
3521
+ static userPoolKmsKeyFromConstruct(scope) {
3522
+ const keyArn = DiscoverableStringParameter.valueForLookupName(scope, {
3523
+ ssmParamName: CognitoUserPoolKmsKey.SSM_PARAM_NAME,
3524
+ serviceType: _OpenHiAuthService.SERVICE_TYPE
3467
3525
  });
3526
+ return Key2.fromKeyArn(scope, "kms-key", keyArn);
3527
+ }
3528
+ get serviceType() {
3529
+ return _OpenHiAuthService.SERVICE_TYPE;
3468
3530
  }
3469
3531
  /**
3470
- * Looks up the CloudFront distribution ARN published by the release-branch
3471
- * deploy of this service.
3532
+ * Creates the KMS key for the Cognito User Pool and exports its ARN to SSM.
3533
+ * Look up via {@link OpenHiAuthService.userPoolKmsKeyFromConstruct}.
3534
+ * Override to customize.
3472
3535
  */
3473
- static distributionArnFromConstruct(scope) {
3474
- return DiscoverableStringParameter.valueForLookupName(scope, {
3475
- ssmParamName: StaticHosting.SSM_PARAM_NAME_DISTRIBUTION_ARN,
3476
- serviceType: _OpenHiWebsiteService.SERVICE_TYPE
3536
+ createUserPoolKmsKey() {
3537
+ const key = new CognitoUserPoolKmsKey(this);
3538
+ new DiscoverableStringParameter(this, "kms-key-param", {
3539
+ ssmParamName: CognitoUserPoolKmsKey.SSM_PARAM_NAME,
3540
+ stringValue: key.keyArn,
3541
+ description: "KMS key ARN for Cognito User Pool (e.g. custom sender); cross-stack reference"
3477
3542
  });
3543
+ return key;
3478
3544
  }
3479
3545
  /**
3480
- * Looks up the CloudFront distribution domain
3481
- * (e.g. dXXXXX.cloudfront.net) published by the release-branch deploy.
3546
+ * Creates the Pre Token Generation Lambda (Cognito trigger). On every
3547
+ * sign-in and token refresh the Lambda resolves the User by Cognito `sub`
3548
+ * (GSI2) and injects `ohi_tid`, `ohi_wid`, `ohi_uid`, `ohi_uname` into
3549
+ * both the ID token and the access token (ADR 2026-03-17-01).
3482
3550
  */
3483
- static distributionDomainFromConstruct(scope) {
3484
- return DiscoverableStringParameter.valueForLookupName(scope, {
3485
- ssmParamName: StaticHosting.SSM_PARAM_NAME_DISTRIBUTION_DOMAIN,
3486
- serviceType: _OpenHiWebsiteService.SERVICE_TYPE
3551
+ createPreTokenGenerationLambda() {
3552
+ const construct = new PreTokenGenerationLambda(this, {
3553
+ dynamoTableName: this.dataStoreTable().tableName
3487
3554
  });
3555
+ return construct.lambda;
3488
3556
  }
3489
3557
  /**
3490
- * Looks up the CloudFront distribution ID published by the release-branch
3491
- * deploy of this service.
3558
+ * Creates the Post Authentication Lambda (Cognito trigger). Calls
3559
+ * AdminUserGlobalSignOut on every sign-in to enforce single-device-per-user
3560
+ * sessions per ADR 2026-03-17-01.
3492
3561
  */
3493
- static distributionIdFromConstruct(scope) {
3494
- return DiscoverableStringParameter.valueForLookupName(scope, {
3495
- ssmParamName: StaticHosting.SSM_PARAM_NAME_DISTRIBUTION_ID,
3496
- serviceType: _OpenHiWebsiteService.SERVICE_TYPE
3497
- });
3562
+ createPostAuthenticationLambda() {
3563
+ const construct = new PostAuthenticationLambda(this);
3564
+ return construct.lambda;
3498
3565
  }
3499
3566
  /**
3500
- * Looks up the website's full domain (e.g. www.example.com) published by
3501
- * the release-branch deploy of this service.
3567
+ * Creates the Post Confirmation Lambda (Cognito trigger). On sign-up
3568
+ * confirmation, publishes a control-plane workflow event; provisioning lives
3569
+ * behind EventBridge.
3502
3570
  */
3503
- static fullDomainFromConstruct(scope) {
3504
- return DiscoverableStringParameter.valueForLookupName(scope, {
3505
- ssmParamName: SSM_PARAM_NAME_FULL_DOMAIN,
3506
- serviceType: _OpenHiWebsiteService.SERVICE_TYPE
3571
+ createPostConfirmationLambda() {
3572
+ const construct = new PostConfirmationLambda(this, {
3573
+ controlEventBusName: this.controlEventBus().eventBusName
3507
3574
  });
3575
+ return construct.lambda;
3508
3576
  }
3509
- get serviceType() {
3510
- return _OpenHiWebsiteService.SERVICE_TYPE;
3577
+ createUserOnboardingWorkflow() {
3578
+ return new UserOnboardingWorkflow(this, {
3579
+ controlEventBus: this.controlEventBus(),
3580
+ dataStoreTable: this.dataStoreTable()
3581
+ });
3511
3582
  }
3512
- constructor(ohEnv, props) {
3513
- super(ohEnv, _OpenHiWebsiteService.SERVICE_TYPE, props);
3514
- this.props = props;
3515
- this.validateConfig(props);
3516
- const isReleaseBranch = this.branchName === this.defaultReleaseBranch;
3517
- const hostedZone = this.createHostedZone();
3518
- this.fullDomain = this.computeFullDomain(hostedZone);
3519
- const shouldCreateHostingInfra = props.createHostingInfrastructure ?? isReleaseBranch;
3520
- if (shouldCreateHostingInfra) {
3521
- const certificate = this.createCertificate();
3522
- this.staticHosting = this.createStaticHosting({
3523
- certificate,
3524
- hostedZone
3525
- });
3526
- this.createFullDomainParameter();
3527
- } else if (!isReleaseBranch) {
3528
- this.perBranchHostname = this.createPerBranchHostname(hostedZone);
3529
- }
3530
- if (props.createStaticContent !== false) {
3531
- const bucket = this.resolveStaticHostingBucket();
3532
- this.staticContent = this.createStaticContent(bucket);
3583
+ dataStoreTable() {
3584
+ if (this._dataStoreTable === null) {
3585
+ this._dataStoreTable = OpenHiDataService.dynamoDbDataStoreFromConstruct(this);
3533
3586
  }
3587
+ return this._dataStoreTable;
3534
3588
  }
3535
- /**
3536
- * Validates that config required for the website stack is present.
3537
- */
3538
- validateConfig(props) {
3539
- const { config } = props;
3540
- if (!config) {
3541
- throw new Error("Config is required");
3542
- }
3543
- if (!config.zoneName) {
3544
- throw new Error("Zone name is required");
3545
- }
3546
- if (!config.hostedZoneId) {
3547
- throw new Error("Hosted zone ID is required to import the website zone");
3589
+ controlEventBus() {
3590
+ if (this._controlEventBus === null) {
3591
+ this._controlEventBus = OpenHiGlobalService.controlEventBusFromConstruct(this);
3548
3592
  }
3593
+ return this._controlEventBus;
3549
3594
  }
3550
3595
  /**
3551
- * Imports the website's hosted zone from config attributes (no SSM lookup).
3552
- * The website attaches DNS records here on the release-branch deploy and
3553
- * the same zone is imported on feature-branch deploys for any sub-domain
3554
- * routing.
3596
+ * Creates the Cognito User Pool and exports its ID to SSM.
3597
+ * Look up via {@link OpenHiAuthService.userPoolFromConstruct}.
3555
3598
  * Override to customize.
3556
3599
  */
3557
- createHostedZone() {
3558
- return OpenHiGlobalService.rootHostedZoneFromConstruct(this, {
3559
- zoneName: this.config.zoneName,
3560
- hostedZoneId: this.config.hostedZoneId
3600
+ createUserPool() {
3601
+ const userPool = new CognitoUserPool(this, {
3602
+ ...this.props.userPoolProps,
3603
+ customSenderKmsKey: this.userPoolKmsKey
3604
+ });
3605
+ userPool.addTrigger(
3606
+ UserPoolOperation.PRE_TOKEN_GENERATION_CONFIG,
3607
+ this.preTokenGenerationLambda,
3608
+ LambdaVersion.V2_0
3609
+ );
3610
+ userPool.addTrigger(
3611
+ UserPoolOperation.POST_AUTHENTICATION,
3612
+ this.postAuthenticationLambda
3613
+ );
3614
+ userPool.addTrigger(
3615
+ UserPoolOperation.POST_CONFIRMATION,
3616
+ this.postConfirmationLambda
3617
+ );
3618
+ new DiscoverableStringParameter(this, "user-pool-param", {
3619
+ ssmParamName: CognitoUserPool.SSM_PARAM_NAME,
3620
+ stringValue: userPool.userPoolId,
3621
+ description: "Cognito User Pool ID for this Auth stack; cross-stack reference"
3561
3622
  });
3623
+ return userPool;
3562
3624
  }
3563
3625
  /**
3564
- * Returns the wildcard certificate looked up from the Global service.
3565
- * Override to customize.
3626
+ * Grants the Pre Token Generation Lambda read-only access on the data
3627
+ * store table and its GSIs. The Lambda only needs:
3628
+ * - `Query` on GSI2 to resolve a User by Cognito `sub`
3629
+ * - `GetItem` on the base table for direct User reads
3630
+ *
3631
+ * No write or scan access: a User missing `currentTenant`/`currentWorkspace`
3632
+ * falls into the absent-claims path; repair belongs in a separate backfill.
3566
3633
  */
3567
- createCertificate() {
3568
- return OpenHiGlobalService.rootWildcardCertificateFromConstruct(this);
3634
+ grantPreTokenGenerationPermissions() {
3635
+ const dataStoreTable = this.dataStoreTable();
3636
+ const dynamoActions = ["dynamodb:GetItem", "dynamodb:Query"];
3637
+ dataStoreTable.grant(this.preTokenGenerationLambda, ...dynamoActions);
3638
+ this.preTokenGenerationLambda.addToRolePolicy(
3639
+ new PolicyStatement7({
3640
+ effect: Effect7.ALLOW,
3641
+ actions: [...dynamoActions],
3642
+ resources: [`${dataStoreTable.tableArn}/index/*`]
3643
+ })
3644
+ );
3569
3645
  }
3570
3646
  /**
3571
- * Computes the full website domain from `domainPrefix`,
3572
- * `childZonePrefix`, and the child zone name. Release-branch deploys
3573
- * serve at `\<domainPrefix\>.\<zone\>` (e.g. `admin.dev.openhi.org`);
3574
- * every other deploy serves a per-PR preview at
3575
- * `\<domainPrefix\>-\<childZonePrefix\>.\<zone\>`
3576
- * (e.g. `admin-feat-1093-patient-migration.dev.openhi.org`).
3647
+ * Grants the Post Authentication Lambda permission to call
3648
+ * `cognito-idp:AdminUserGlobalSignOut`.
3577
3649
  *
3578
- * Delegates to {@link OpenHiWebsiteService.composeFullDomain} so the
3579
- * release-vs-feature composition stays in one place.
3650
+ * Scoped via `Stack.of(this).formatArn` rather than `userPool.userPoolArn`
3651
+ * because the User Pool registers this Lambda as a Post Authentication
3652
+ * trigger, creating the cycle:
3653
+ * userPool → lambda (trigger ARN) → role policy → userPool ARN.
3654
+ * Using `formatArn` avoids referencing the User Pool resource directly
3655
+ * while still scoping to user pools in this account+region. The Lambda
3656
+ * is invoked only by Cognito with a Cognito-provided `event.userPoolId`,
3657
+ * so the runtime target is constrained by the trigger contract.
3580
3658
  */
3581
- computeFullDomain(hostedZone) {
3582
- return _OpenHiWebsiteService.composeFullDomain({
3583
- domainPrefix: this.props.domainPrefix,
3584
- branchName: this.branchName,
3585
- defaultReleaseBranch: this.defaultReleaseBranch,
3586
- childZonePrefix: this.childZonePrefix,
3587
- zoneName: hostedZone.zoneName
3588
- });
3659
+ grantPostAuthenticationPermissions() {
3660
+ this.postAuthenticationLambda.addToRolePolicy(
3661
+ new PolicyStatement7({
3662
+ actions: ["cognito-idp:AdminUserGlobalSignOut"],
3663
+ resources: [
3664
+ Stack7.of(this).formatArn({
3665
+ service: "cognito-idp",
3666
+ resource: "userpool",
3667
+ resourceName: "*"
3668
+ })
3669
+ ]
3670
+ })
3671
+ );
3589
3672
  }
3590
3673
  /**
3591
- * Returns the sub-domain label (left of the zone) for the current
3592
- * deploy. Used for the per-branch S3 key prefix passed to
3593
- * {@link StaticContent} so the upload prefix always matches the
3594
- * served hostname.
3595
- *
3596
- * Non-release deploys compose the per-PR slug as
3597
- * `\<domainPrefix\>-\<childZonePrefix\>`, mirroring the REST API's
3598
- * `api-\<childZonePrefix\>` convention. When `domainPrefix` is `admin`
3599
- * (the only consumer today), the resulting sub-domain starts with
3600
- * {@link PER_BRANCH_PREVIEW_PREFIX}, so the per-PR S3 key prefix
3601
- * matches what `StaticHosting`'s lifecycle rule expires.
3674
+ * Grants the Post Confirmation Lambda publish-only access to the
3675
+ * control-plane event bus. Workflow Lambdas own DynamoDB writes.
3602
3676
  */
3603
- computeSubDomain() {
3604
- const isReleaseBranch = this.branchName === this.defaultReleaseBranch;
3605
- const domainPrefix = this.props.domainPrefix ?? _OpenHiWebsiteService.DEFAULT_DOMAIN_PREFIX;
3606
- if (isReleaseBranch) {
3607
- return domainPrefix;
3608
- }
3609
- return `${domainPrefix}-${this.childZonePrefix}`;
3677
+ grantPostConfirmationPermissions() {
3678
+ this.controlEventBus().grantPutEventsTo(this.postConfirmationLambda);
3610
3679
  }
3611
3680
  /**
3612
- * Creates the StaticHosting infrastructure (bucket + distribution +
3613
- * Lambda@Edge + 4 SSM params + DNS). The release-branch distribution
3614
- * adds `*.\<zone\>` as a wildcard alt-name on top of the canonical
3615
- * hostname so per-PR previews resolve via the same distribution.
3616
- *
3617
- * The bucket carries an S3 lifecycle rule that expires per-PR
3618
- * preview content (keys under {@link PER_BRANCH_PREVIEW_PREFIX})
3619
- * on non-production stages. PROD never gets the rule — see
3620
- * `enablePreviewLifecycle`.
3681
+ * Creates the User Pool Client and exports its ID to SSM (AUTH service type).
3682
+ * OAuth flows are enabled with auto-injected callback/logout URLs derived
3683
+ * from this stack's branch context (see {@link resolveOAuthRedirectUrls}).
3684
+ * Look up via {@link OpenHiAuthService.userPoolClientFromConstruct}.
3685
+ * Override to customize.
3621
3686
  */
3622
- createStaticHosting(deps) {
3623
- const restApi = this.props.restApi === true ? this.resolveRestApi() : void 0;
3624
- const wildcardSan = `*.${deps.hostedZone.zoneName}`;
3625
- return new StaticHosting(this, "static-hosting", {
3626
- serviceType: _OpenHiWebsiteService.SERVICE_TYPE,
3627
- certificate: deps.certificate,
3628
- hostedZone: deps.hostedZone,
3629
- domainNames: [this.fullDomain, wildcardSan],
3630
- description: `OpenHI website (${this.fullDomain})`,
3631
- prefixPattern: PER_BRANCH_PREVIEW_PREFIX,
3632
- enablePreviewLifecycle: this.ohEnv.ohStage.stageType !== import_config6.OPEN_HI_STAGE.PROD,
3633
- ...restApi !== void 0 && { restApi }
3687
+ createUserPoolClient() {
3688
+ const { callbackUrls, logoutUrls } = this.resolveOAuthRedirectUrls();
3689
+ const client = new CognitoUserPoolClient(this, {
3690
+ userPool: this.userPool,
3691
+ oAuth: {
3692
+ flows: {
3693
+ authorizationCodeGrant: true,
3694
+ implicitCodeGrant: true
3695
+ },
3696
+ callbackUrls,
3697
+ logoutUrls
3698
+ }
3699
+ });
3700
+ new DiscoverableStringParameter(this, "user-pool-client-param", {
3701
+ ssmParamName: CognitoUserPoolClient.SSM_PARAM_NAME,
3702
+ stringValue: client.userPoolClientId,
3703
+ description: "Cognito User Pool Client ID for this Auth stack; cross-stack reference"
3634
3704
  });
3705
+ return client;
3635
3706
  }
3636
3707
  /**
3637
- * Resolves the REST API custom-domain hostname from the rest-api stack's
3638
- * `REST_API_DOMAIN_NAME` SSM parameter. Wrapped in a private method so
3639
- * it can be overridden / stubbed in subclasses and tests.
3708
+ * Returns the OAuth `callbackUrls` and `logoutUrls` the Cognito User Pool
3709
+ * Client should accept. Composed from the same branch context the website
3710
+ * service will see at synth time:
3711
+ *
3712
+ * - `https://admin{,-<childZonePrefix>}.<zone>/oauth/{callback,logout}`
3713
+ * - `https://www{,-<childZonePrefix>}.<zone>/oauth/{callback,logout}`
3714
+ *
3715
+ * Both deployed-host pairs are auto-injected on every stage. On non-prod
3716
+ * stages the localhost dev URLs from {@link LOCALHOST_OAUTH_CALLBACK_URLS}
3717
+ * / {@link LOCALHOST_OAUTH_LOGOUT_URLS} join the merge; on prod they are
3718
+ * deliberately excluded.
3719
+ *
3720
+ * If `zoneName` is absent (no-DNS test configurations), the deployed-host
3721
+ * pairs are skipped — only the localhost set survives, and only on
3722
+ * non-prod. Override to customize.
3640
3723
  */
3641
- resolveRestApi() {
3724
+ resolveOAuthRedirectUrls() {
3725
+ const isNonProd = this.ohEnv.ohStage.stageType !== import_config7.OPEN_HI_STAGE.PROD;
3726
+ const zoneName = this.props.config?.zoneName;
3727
+ const deployedOrigins = [];
3728
+ if (zoneName !== void 0) {
3729
+ const adminHost = OpenHiWebsiteService.composeFullDomain({
3730
+ domainPrefix: ADMIN_DOMAIN_PREFIX,
3731
+ branchName: this.branchName,
3732
+ defaultReleaseBranch: this.defaultReleaseBranch,
3733
+ childZonePrefix: this.childZonePrefix,
3734
+ zoneName
3735
+ });
3736
+ const websiteHost = OpenHiWebsiteService.composeFullDomain({
3737
+ branchName: this.branchName,
3738
+ defaultReleaseBranch: this.defaultReleaseBranch,
3739
+ childZonePrefix: this.childZonePrefix,
3740
+ zoneName
3741
+ });
3742
+ deployedOrigins.push(`https://${adminHost}`, `https://${websiteHost}`);
3743
+ }
3744
+ const localhostCallbacks = isNonProd ? LOCALHOST_OAUTH_CALLBACK_URLS : [];
3745
+ const localhostLogouts = isNonProd ? LOCALHOST_OAUTH_LOGOUT_URLS : [];
3642
3746
  return {
3643
- domainName: OpenHiRestApiService.restApiDomainNameFromConstruct(this)
3747
+ callbackUrls: [
3748
+ ...deployedOrigins.map((o) => `${o}/oauth/callback`),
3749
+ ...localhostCallbacks
3750
+ ],
3751
+ logoutUrls: [
3752
+ ...deployedOrigins.map((o) => `${o}/oauth/logout`),
3753
+ ...localhostLogouts
3754
+ ]
3644
3755
  };
3645
3756
  }
3646
3757
  /**
3647
- * Creates the SSM parameter that publishes the website's full domain.
3648
- * Look up via {@link OpenHiWebsiteService.fullDomainFromConstruct}.
3758
+ * Creates the User Pool Domain (Cognito hosted UI) and exports domain name to SSM.
3759
+ * Look up via {@link OpenHiAuthService.userPoolDomainFromConstruct}.
3760
+ * Override to customize.
3649
3761
  */
3650
- createFullDomainParameter() {
3651
- new DiscoverableStringParameter(this, "full-domain-param", {
3652
- ssmParamName: SSM_PARAM_NAME_FULL_DOMAIN,
3653
- serviceType: _OpenHiWebsiteService.SERVICE_TYPE,
3654
- stringValue: this.fullDomain,
3655
- description: "Full website domain (e.g. www.example.com)"
3762
+ createUserPoolDomain() {
3763
+ const domain = new CognitoUserPoolDomain(this, {
3764
+ userPool: this.userPool,
3765
+ cognitoDomain: {
3766
+ domainPrefix: `auth-${this.branchHash}`
3767
+ }
3656
3768
  });
3657
- }
3658
- /**
3659
- * Creates the StaticContent uploader. Receives the resolved static-hosting
3660
- * bucket from the constructor on the release-branch deploy this is the
3661
- * just-created {@link staticHosting} bucket (no SSM round-trip within a
3662
- * single stack); on every other deploy it is imported from the bucket ARN
3663
- * the release-branch deploy publishes to SSM, addressed against
3664
- * {@link OpenHiService.releaseBranchHash}. See
3665
- * {@link resolveStaticHostingBucket}.
3666
- *
3667
- * The S3 key prefix is `\<sub-domain\>.\<zone\>/\<contentDest\>` so the
3668
- * upload location matches the Host-header-derived folder the Lambda@Edge
3669
- * viewer-request handler prepends. Passing the zone name (rather than
3670
- * `this.fullDomain`) for the `fullDomain` prop keeps the prefix flat —
3671
- * `admin-feat-foo.dev.openhi.org/`, not
3672
- * `admin-feat-foo.admin.dev.openhi.org/`.
3673
- */
3674
- createStaticContent(bucket) {
3675
- const { contentSourceDirectory, contentDestinationDirectory } = this.props;
3676
- return new StaticContent(this, "static-content", {
3677
- bucket,
3678
- contentSourceDirectory,
3679
- contentDestinationDirectory,
3680
- subDomain: this.computeSubDomain(),
3681
- fullDomain: this.config.zoneName
3769
+ new DiscoverableStringParameter(this, "user-pool-domain-param", {
3770
+ ssmParamName: CognitoUserPoolDomain.SSM_PARAM_NAME,
3771
+ stringValue: domain.domainName,
3772
+ description: "Cognito User Pool Domain (hosted UI) for this Auth stack; cross-stack reference"
3682
3773
  });
3774
+ return domain;
3683
3775
  }
3776
+ };
3777
+ _OpenHiAuthService.SERVICE_TYPE = "auth";
3778
+ var OpenHiAuthService = _OpenHiAuthService;
3779
+
3780
+ // src/services/open-hi-graphql-service.ts
3781
+ import {
3782
+ AuthorizationType,
3783
+ UserPoolDefaultAction
3784
+ } from "aws-cdk-lib/aws-appsync";
3785
+ var _OpenHiGraphqlService = class _OpenHiGraphqlService extends OpenHiService {
3684
3786
  /**
3685
- * Creates the per-PR `PerBranchHostname` alias record on non-release
3686
- * branch deploys. The record points
3687
- * `\<domainPrefix\>-\<childZonePrefix\>.\<zone\>` at the release-branch
3688
- * CloudFront distribution (resolved from SSM against
3689
- * {@link OpenHiService.releaseBranchHash}).
3787
+ * Returns the GraphQL API by looking up the GraphQL stack's API ID from SSM.
3788
+ * Use from other stacks to obtain an IGraphqlApi reference.
3690
3789
  */
3691
- createPerBranchHostname(hostedZone) {
3692
- return new PerBranchHostname(this, "per-branch-hostname", {
3693
- hostname: this.fullDomain,
3694
- hostedZone,
3695
- serviceType: _OpenHiWebsiteService.SERVICE_TYPE
3696
- });
3790
+ static graphqlApiFromConstruct(scope) {
3791
+ return RootGraphqlApi.fromConstruct(scope);
3697
3792
  }
3698
- /**
3699
- * Returns an {@link IBucket} pointing at the static-hosting bucket the
3700
- * uploaders write to. On the release-branch deploy this is the bucket
3701
- * just provisioned by {@link staticHosting}; on every other deploy it's
3702
- * imported from the bucket ARN the release-branch deploy publishes to
3703
- * SSM, addressed against {@link OpenHiService.releaseBranchHash}.
3704
- */
3705
- resolveStaticHostingBucket() {
3706
- if (this.staticHosting) {
3707
- return this.staticHosting.bucket;
3708
- }
3709
- const bucketArn = DiscoverableStringParameter.valueForLookupName(this, {
3710
- ssmParamName: StaticHosting.SSM_PARAM_NAME_BUCKET_ARN,
3711
- serviceType: _OpenHiWebsiteService.SERVICE_TYPE,
3712
- branchHash: this.releaseBranchHash
3793
+ get serviceType() {
3794
+ return _OpenHiGraphqlService.SERVICE_TYPE;
3795
+ }
3796
+ constructor(ohEnv, props = {}) {
3797
+ super(ohEnv, _OpenHiGraphqlService.SERVICE_TYPE, props);
3798
+ this.props = props;
3799
+ this.rootGraphqlApi = this.createRootGraphqlApi();
3800
+ }
3801
+ /** Creates the root GraphQL API with Cognito user pool. */
3802
+ createRootGraphqlApi() {
3803
+ const userPool = OpenHiAuthService.userPoolFromConstruct(this);
3804
+ return new RootGraphqlApi(this, {
3805
+ authorizationConfig: {
3806
+ defaultAuthorization: {
3807
+ authorizationType: AuthorizationType.USER_POOL,
3808
+ userPoolConfig: {
3809
+ userPool,
3810
+ defaultAction: UserPoolDefaultAction.ALLOW
3811
+ }
3812
+ }
3813
+ }
3713
3814
  });
3714
- return Bucket3.fromBucketArn(this, "shared-bucket", bucketArn);
3715
3815
  }
3716
3816
  };
3717
- _OpenHiWebsiteService.SERVICE_TYPE = "website";
3718
- /**
3719
- * Default `domainPrefix` for this service when none is supplied.
3720
- * Release-branch hostname is `www.<zone>`; per-PR preview hostname is
3721
- * `www-<childZonePrefix>.<zone>`.
3722
- */
3723
- _OpenHiWebsiteService.DEFAULT_DOMAIN_PREFIX = "www";
3724
- var OpenHiWebsiteService = _OpenHiWebsiteService;
3817
+ _OpenHiGraphqlService.SERVICE_TYPE = "graphql-api";
3818
+ var OpenHiGraphqlService = _OpenHiGraphqlService;
3725
3819
 
3726
3820
  // src/workflows/control-plane/owning-delete-cascade/owning-delete-cascade-lambdas.ts
3727
3821
  import fs13 from "fs";
@@ -4270,6 +4364,8 @@ export {
4270
4364
  DataStorePostgresReplica,
4271
4365
  DiscoverableStringParameter,
4272
4366
  DynamoDbDataStore,
4367
+ LOCALHOST_OAUTH_CALLBACK_URLS,
4368
+ LOCALHOST_OAUTH_LOGOUT_URLS,
4273
4369
  OPENHI_REPO_TAG_KEY_ENV_VAR,
4274
4370
  OPENHI_RESOURCE_URN_SYSTEM,
4275
4371
  OPENHI_TAG_KEY_PREFIX_ENV_VAR,