@openhi/constructs 0.0.137 → 0.0.139

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