@openhi/constructs 0.0.137 → 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.js CHANGED
@@ -798,6 +798,8 @@ __export(src_exports, {
798
798
  DataStorePostgresReplica: () => DataStorePostgresReplica,
799
799
  DiscoverableStringParameter: () => DiscoverableStringParameter,
800
800
  DynamoDbDataStore: () => DynamoDbDataStore,
801
+ LOCALHOST_OAUTH_CALLBACK_URLS: () => LOCALHOST_OAUTH_CALLBACK_URLS,
802
+ LOCALHOST_OAUTH_LOGOUT_URLS: () => LOCALHOST_OAUTH_LOGOUT_URLS,
801
803
  OPENHI_REPO_TAG_KEY_ENV_VAR: () => OPENHI_REPO_TAG_KEY_ENV_VAR,
802
804
  OPENHI_RESOURCE_URN_SYSTEM: () => OPENHI_RESOURCE_URN_SYSTEM,
803
805
  OPENHI_TAG_KEY_PREFIX_ENV_VAR: () => OPENHI_TAG_KEY_PREFIX_ENV_VAR,
@@ -1473,23 +1475,10 @@ var import_aws_cognito2 = require("aws-cdk-lib/aws-cognito");
1473
1475
  var CognitoUserPoolClient = class extends import_aws_cognito2.UserPoolClient {
1474
1476
  constructor(scope, props) {
1475
1477
  super(scope, "user-pool-client", {
1476
- /**
1477
- * Defaults
1478
- */
1478
+ // Default: SPA client (no secret). OAuth flow + callback/logout URL
1479
+ // composition is the owning service's responsibility — pass via
1480
+ // `props.oAuth` (see `OpenHiAuthService.resolveOAuthRedirectUrls`).
1479
1481
  generateSecret: false,
1480
- oAuth: {
1481
- flows: {
1482
- authorizationCodeGrant: true,
1483
- implicitCodeGrant: true
1484
- },
1485
- callbackUrls: [
1486
- `http://localhost:3000/oauth/callback`,
1487
- `https://localhost:3000/oauth/callback`
1488
- ]
1489
- },
1490
- /**
1491
- * Overrideable props
1492
- */
1493
1482
  ...props
1494
1483
  });
1495
1484
  }
@@ -2816,10 +2805,11 @@ var StaticContent = class extends import_constructs10.Construct {
2816
2805
  };
2817
2806
 
2818
2807
  // src/services/open-hi-auth-service.ts
2808
+ var import_config7 = __toESM(require_lib());
2819
2809
  var import_aws_cognito4 = require("aws-cdk-lib/aws-cognito");
2820
- var import_aws_iam6 = require("aws-cdk-lib/aws-iam");
2810
+ var import_aws_iam7 = require("aws-cdk-lib/aws-iam");
2821
2811
  var import_aws_kms2 = require("aws-cdk-lib/aws-kms");
2822
- var import_core = require("aws-cdk-lib/core");
2812
+ var import_core2 = require("aws-cdk-lib/core");
2823
2813
 
2824
2814
  // src/services/open-hi-data-service.ts
2825
2815
  var import_config4 = __toESM(require_lib());
@@ -6792,620 +6782,247 @@ var _OpenHiDataService = class _OpenHiDataService extends OpenHiService {
6792
6782
  _OpenHiDataService.SERVICE_TYPE = "data";
6793
6783
  var OpenHiDataService = _OpenHiDataService;
6794
6784
 
6795
- // src/workflows/control-plane/user-onboarding/events.ts
6796
- var USER_ONBOARDING_EVENT_SOURCE = "openhi.control.user-onboarding";
6797
- var PROVISION_DEFAULT_WORKSPACE_DETAIL_TYPE = "ProvisionDefaultWorkspaceRequested";
6798
- var buildProvisionDefaultWorkspaceRequestedDetail = (event) => {
6799
- const attrs = event.request?.userAttributes ?? {};
6800
- const cognitoSub = attrs.sub?.trim();
6801
- if (!cognitoSub) {
6802
- return void 0;
6803
- }
6804
- const email = attrs.email?.trim();
6805
- const displayName = email || event.userName || cognitoSub;
6806
- return {
6807
- cognitoSub,
6808
- ...email ? { email } : {},
6809
- displayName,
6810
- trigger: {
6811
- source: "cognito.post-confirmation",
6812
- triggerSource: event.triggerSource,
6813
- userPoolId: event.userPoolId,
6814
- userName: event.userName,
6815
- clientId: event.callerContext?.clientId
6816
- }
6817
- };
6818
- };
6785
+ // src/services/open-hi-website-service.ts
6786
+ var import_config6 = __toESM(require_lib());
6787
+ var import_aws_s32 = require("aws-cdk-lib/aws-s3");
6819
6788
 
6820
- // src/workflows/control-plane/user-onboarding/provision-default-workspace-lambda.ts
6789
+ // src/services/open-hi-rest-api-service.ts
6790
+ var import_config5 = __toESM(require_lib());
6791
+ var import_aws_apigatewayv22 = require("aws-cdk-lib/aws-apigatewayv2");
6792
+ var import_aws_apigatewayv2_authorizers = require("aws-cdk-lib/aws-apigatewayv2-authorizers");
6793
+ var import_aws_apigatewayv2_integrations = require("aws-cdk-lib/aws-apigatewayv2-integrations");
6794
+ var import_aws_iam5 = require("aws-cdk-lib/aws-iam");
6795
+ var import_aws_route535 = require("aws-cdk-lib/aws-route53");
6796
+ var import_aws_route53_targets3 = require("aws-cdk-lib/aws-route53-targets");
6797
+ var import_core = require("aws-cdk-lib/core");
6798
+ var import_constructs19 = require("constructs");
6799
+
6800
+ // src/data/lambda/cors-options-lambda.ts
6821
6801
  var import_node_fs9 = __toESM(require("fs"));
6822
6802
  var import_node_path9 = __toESM(require("path"));
6823
- var import_aws_cdk_lib15 = require("aws-cdk-lib");
6824
- var import_aws_events8 = require("aws-cdk-lib/aws-events");
6825
- var import_aws_events_targets4 = require("aws-cdk-lib/aws-events-targets");
6826
- var import_aws_iam5 = require("aws-cdk-lib/aws-iam");
6827
6803
  var import_aws_lambda10 = require("aws-cdk-lib/aws-lambda");
6828
6804
  var import_aws_lambda_nodejs10 = require("aws-cdk-lib/aws-lambda-nodejs");
6829
6805
  var import_constructs17 = require("constructs");
6830
- var HANDLER_NAME9 = "provision-default-workspace.handler.js";
6806
+ var HANDLER_NAME9 = "cors-options-lambda.handler.js";
6831
6807
  function resolveHandlerEntry9(dirname) {
6832
6808
  const sameDir = import_node_path9.default.join(dirname, HANDLER_NAME9);
6833
6809
  if (import_node_fs9.default.existsSync(sameDir)) {
6834
6810
  return sameDir;
6835
6811
  }
6836
- return import_node_path9.default.join(dirname, "..", "..", "..", "..", "lib", HANDLER_NAME9);
6812
+ const fromLib = import_node_path9.default.join(dirname, "..", "..", "..", "lib", HANDLER_NAME9);
6813
+ return fromLib;
6837
6814
  }
6838
- var ProvisionDefaultWorkspaceLambda = class extends import_constructs17.Construct {
6839
- constructor(scope, props) {
6840
- super(scope, "provision-default-workspace-lambda");
6815
+ var CorsOptionsLambda = class extends import_constructs17.Construct {
6816
+ constructor(scope, id = "cors-options-lambda") {
6817
+ super(scope, id);
6841
6818
  this.lambda = new import_aws_lambda_nodejs10.NodejsFunction(this, "handler", {
6842
6819
  entry: resolveHandlerEntry9(__dirname),
6843
6820
  runtime: import_aws_lambda10.Runtime.NODEJS_LATEST,
6844
- memorySize: 1024,
6845
- environment: {
6846
- DYNAMO_TABLE_NAME: props.dataStoreTable.tableName
6847
- }
6848
- });
6849
- props.dataStoreTable.grant(
6850
- this.lambda,
6851
- "dynamodb:PutItem",
6852
- "dynamodb:UpdateItem"
6853
- );
6854
- this.lambda.addToRolePolicy(
6855
- new import_aws_iam5.PolicyStatement({
6856
- effect: import_aws_iam5.Effect.ALLOW,
6857
- actions: ["dynamodb:Query"],
6858
- resources: [`${props.dataStoreTable.tableArn}/index/*`]
6859
- })
6860
- );
6861
- this.rule = new import_aws_events8.Rule(this, "rule", {
6862
- eventBus: props.controlEventBus,
6863
- eventPattern: {
6864
- source: [USER_ONBOARDING_EVENT_SOURCE],
6865
- detailType: [PROVISION_DEFAULT_WORKSPACE_DETAIL_TYPE]
6866
- },
6867
- targets: [
6868
- new import_aws_events_targets4.LambdaFunction(this.lambda, {
6869
- retryAttempts: 2,
6870
- maxEventAge: import_aws_cdk_lib15.Duration.hours(2)
6871
- })
6872
- ]
6821
+ memorySize: 128
6873
6822
  });
6874
6823
  }
6875
6824
  };
6876
6825
 
6877
- // src/workflows/control-plane/user-onboarding/user-onboarding-workflow.ts
6826
+ // src/data/lambda/rest-api-lambda.ts
6827
+ var import_node_fs10 = __toESM(require("fs"));
6828
+ var import_node_path10 = __toESM(require("path"));
6829
+ var import_aws_lambda11 = require("aws-cdk-lib/aws-lambda");
6830
+ var import_aws_lambda_nodejs11 = require("aws-cdk-lib/aws-lambda-nodejs");
6878
6831
  var import_constructs18 = require("constructs");
6879
- var UserOnboardingWorkflow = class extends import_constructs18.Construct {
6832
+ var HANDLER_NAME10 = "rest-api-lambda.handler.js";
6833
+ function resolveHandlerEntry10(dirname) {
6834
+ const sameDir = import_node_path10.default.join(dirname, HANDLER_NAME10);
6835
+ if (import_node_fs10.default.existsSync(sameDir)) {
6836
+ return sameDir;
6837
+ }
6838
+ const fromLib = import_node_path10.default.join(dirname, "..", "..", "..", "lib", HANDLER_NAME10);
6839
+ return fromLib;
6840
+ }
6841
+ var RestApiLambda = class extends import_constructs18.Construct {
6880
6842
  constructor(scope, props) {
6881
- super(scope, "user-onboarding-workflow");
6882
- this.provisionDefaultWorkspace = new ProvisionDefaultWorkspaceLambda(this, {
6883
- dataStoreTable: props.dataStoreTable,
6884
- controlEventBus: props.controlEventBus
6843
+ super(scope, "rest-api-lambda");
6844
+ this.lambda = new import_aws_lambda_nodejs11.NodejsFunction(this, "handler", {
6845
+ entry: resolveHandlerEntry10(__dirname),
6846
+ runtime: import_aws_lambda11.Runtime.NODEJS_LATEST,
6847
+ memorySize: 1024,
6848
+ environment: {
6849
+ DYNAMO_TABLE_NAME: props.dynamoTableName,
6850
+ BRANCH_TAG_VALUE: props.branchTagValue,
6851
+ HTTP_API_TAG_VALUE: props.httpApiTagValue,
6852
+ OPENHI_PG_CLUSTER_ARN: props.postgresClusterArn,
6853
+ OPENHI_PG_SECRET_ARN: props.postgresSecretArn,
6854
+ OPENHI_PG_DATABASE: props.postgresDatabase,
6855
+ OPENHI_PG_SCHEMA: props.postgresSchema,
6856
+ ...props.extraEnvironment
6857
+ },
6858
+ bundling: {
6859
+ minify: true,
6860
+ sourceMap: false
6861
+ }
6885
6862
  });
6886
6863
  }
6887
6864
  };
6888
6865
 
6889
- // src/services/open-hi-auth-service.ts
6890
- var _OpenHiAuthService = class _OpenHiAuthService extends OpenHiService {
6891
- constructor(ohEnv, props = {}) {
6892
- super(ohEnv, _OpenHiAuthService.SERVICE_TYPE, props);
6893
- /**
6894
- * Cross-stack reference to the data store table. Cached so repeated
6895
- * lookups share a single CDK construct id ("dynamo-db-data-store") in
6896
- * this stack — a second `Table.fromTableName` call under the same scope
6897
- * would collide.
6898
- */
6899
- this._dataStoreTable = null;
6900
- this._controlEventBus = null;
6901
- this.props = props;
6902
- this.userPoolKmsKey = this.createUserPoolKmsKey();
6903
- this.preTokenGenerationLambda = this.createPreTokenGenerationLambda();
6904
- this.postAuthenticationLambda = this.createPostAuthenticationLambda();
6905
- this.postConfirmationLambda = this.createPostConfirmationLambda();
6906
- this.userOnboardingWorkflow = this.createUserOnboardingWorkflow();
6907
- this.userPool = this.createUserPool();
6908
- this.grantPreTokenGenerationPermissions();
6909
- this.grantPostAuthenticationPermissions();
6910
- this.grantPostConfirmationPermissions();
6911
- this.userPoolClient = this.createUserPoolClient();
6912
- this.userPoolDomain = this.createUserPoolDomain();
6913
- }
6866
+ // src/services/open-hi-rest-api-service.ts
6867
+ var REST_API_BASE_URL_SSM_NAME = "REST_API_BASE_URL";
6868
+ var REST_API_DOMAIN_NAME_SSM_NAME = "REST_API_DOMAIN_NAME";
6869
+ var DEV_CORS_ALLOW_ORIGINS = [
6870
+ "http://localhost:3000",
6871
+ "https://localhost:3000",
6872
+ "http://localhost:5173",
6873
+ "https://localhost:5173",
6874
+ "http://127.0.0.1:3000",
6875
+ "https://127.0.0.1:3000",
6876
+ "http://127.0.0.1:5173",
6877
+ "https://127.0.0.1:5173"
6878
+ ];
6879
+ var _OpenHiRestApiService = class _OpenHiRestApiService extends OpenHiService {
6914
6880
  /**
6915
- * Returns an IUserPool by looking up the Auth stack's User Pool ID from SSM.
6881
+ * Compose the REST API's full per-deploy domain. Thin wrapper over
6882
+ * {@link OpenHiService.composeServiceDomain} that pins `domainPrefix`
6883
+ * to {@link API_DOMAIN_PREFIX}.
6884
+ *
6885
+ * Use from sibling stacks that need to predict the API's hostname
6886
+ * before the REST API stack is synthesised.
6916
6887
  */
6917
- static userPoolFromConstruct(scope) {
6918
- const userPoolId = DiscoverableStringParameter.valueForLookupName(scope, {
6919
- ssmParamName: CognitoUserPool.SSM_PARAM_NAME,
6920
- serviceType: _OpenHiAuthService.SERVICE_TYPE
6888
+ static composeFullDomain(opts) {
6889
+ return OpenHiService.composeServiceDomain({
6890
+ ...opts,
6891
+ domainPrefix: _OpenHiRestApiService.API_DOMAIN_PREFIX
6921
6892
  });
6922
- return import_aws_cognito4.UserPool.fromUserPoolId(scope, "user-pool", userPoolId);
6923
- }
6924
- /**
6925
- * Returns an IUserPoolClient by looking up the Auth stack's User Pool Client ID from SSM.
6926
- */
6927
- static userPoolClientFromConstruct(scope) {
6928
- const userPoolClientId = DiscoverableStringParameter.valueForLookupName(
6929
- scope,
6930
- {
6931
- ssmParamName: CognitoUserPoolClient.SSM_PARAM_NAME,
6932
- serviceType: _OpenHiAuthService.SERVICE_TYPE
6933
- }
6934
- );
6935
- return import_aws_cognito4.UserPoolClient.fromUserPoolClientId(
6936
- scope,
6937
- "user-pool-client",
6938
- userPoolClientId
6939
- );
6940
6893
  }
6941
6894
  /**
6942
- * Returns an IUserPoolDomain by looking up the Auth stack's User Pool Domain from SSM.
6895
+ * Returns an IHttpApi by looking up the REST API stack's HTTP API ID from SSM.
6943
6896
  */
6944
- static userPoolDomainFromConstruct(scope) {
6945
- const domainName = DiscoverableStringParameter.valueForLookupName(scope, {
6946
- ssmParamName: CognitoUserPoolDomain.SSM_PARAM_NAME,
6947
- serviceType: _OpenHiAuthService.SERVICE_TYPE
6897
+ static rootHttpApiFromConstruct(scope) {
6898
+ const httpApiId = DiscoverableStringParameter.valueForLookupName(scope, {
6899
+ ssmParamName: RootHttpApi.SSM_PARAM_NAME,
6900
+ serviceType: _OpenHiRestApiService.SERVICE_TYPE
6948
6901
  });
6949
- return import_aws_cognito4.UserPoolDomain.fromDomainName(scope, "user-pool-domain", domainName);
6902
+ return import_aws_apigatewayv22.HttpApi.fromHttpApiAttributes(scope, "http-api", { httpApiId });
6950
6903
  }
6951
6904
  /**
6952
- * Returns the full Cognito Hosted UI base URL (e.g.
6953
- * `https://auth-abc.auth.us-east-2.amazoncognito.com`) by looking up
6954
- * the Auth stack's User Pool Domain from SSM and composing it with the
6955
- * calling stack's region.
6956
- *
6957
- * Equivalent to `UserPoolDomain.baseUrl()` on the concrete construct,
6958
- * but works across stacks where the looked-up `IUserPoolDomain` is an
6959
- * `Import` and does not carry the `baseUrl()` method. Assumes the
6960
- * domain was created as a Cognito-managed prefix domain (the only
6961
- * variant `OpenHiAuthService.createUserPoolDomain` produces).
6905
+ * Returns the REST API base URL (e.g. https://api.example.com) by looking it up from SSM.
6906
+ * Use in other stacks for E2E, scripts, or config.
6962
6907
  */
6963
- static userPoolDomainBaseUrlFromConstruct(scope) {
6964
- const domainName = DiscoverableStringParameter.valueForLookupName(scope, {
6965
- ssmParamName: CognitoUserPoolDomain.SSM_PARAM_NAME,
6966
- serviceType: _OpenHiAuthService.SERVICE_TYPE
6908
+ static restApiBaseUrlFromConstruct(scope) {
6909
+ return DiscoverableStringParameter.valueForLookupName(scope, {
6910
+ ssmParamName: REST_API_BASE_URL_SSM_NAME,
6911
+ serviceType: _OpenHiRestApiService.SERVICE_TYPE
6967
6912
  });
6968
- const region = import_core.Stack.of(scope).region;
6969
- return `https://${domainName}.auth.${region}.amazoncognito.com`;
6970
6913
  }
6971
6914
  /**
6972
- * Returns an IKey (KMS) by looking up the Auth stack's User Pool KMS Key ARN from SSM.
6915
+ * Returns the REST API's custom domain name (bare hostname, no scheme — e.g.
6916
+ * `api.example.com`) by looking it up from SSM. Use as the host for a
6917
+ * CloudFront `HttpOrigin` so the website's distribution can proxy `/api/*`
6918
+ * to this stack's API Gateway without per-branch DNS knowledge.
6973
6919
  */
6974
- static userPoolKmsKeyFromConstruct(scope) {
6975
- const keyArn = DiscoverableStringParameter.valueForLookupName(scope, {
6976
- ssmParamName: CognitoUserPoolKmsKey.SSM_PARAM_NAME,
6977
- serviceType: _OpenHiAuthService.SERVICE_TYPE
6920
+ static restApiDomainNameFromConstruct(scope) {
6921
+ return DiscoverableStringParameter.valueForLookupName(scope, {
6922
+ ssmParamName: REST_API_DOMAIN_NAME_SSM_NAME,
6923
+ serviceType: _OpenHiRestApiService.SERVICE_TYPE
6978
6924
  });
6979
- return import_aws_kms2.Key.fromKeyArn(scope, "kms-key", keyArn);
6980
6925
  }
6981
6926
  get serviceType() {
6982
- return _OpenHiAuthService.SERVICE_TYPE;
6927
+ return _OpenHiRestApiService.SERVICE_TYPE;
6983
6928
  }
6984
- /**
6985
- * Creates the KMS key for the Cognito User Pool and exports its ARN to SSM.
6986
- * Look up via {@link OpenHiAuthService.userPoolKmsKeyFromConstruct}.
6987
- * Override to customize.
6988
- */
6989
- createUserPoolKmsKey() {
6990
- const key = new CognitoUserPoolKmsKey(this);
6991
- new DiscoverableStringParameter(this, "kms-key-param", {
6992
- ssmParamName: CognitoUserPoolKmsKey.SSM_PARAM_NAME,
6993
- stringValue: key.keyArn,
6994
- description: "KMS key ARN for Cognito User Pool (e.g. custom sender); cross-stack reference"
6995
- });
6996
- return key;
6997
- }
6998
- /**
6999
- * Creates the Pre Token Generation Lambda (Cognito trigger). On every
7000
- * sign-in and token refresh the Lambda resolves the User by Cognito `sub`
7001
- * (GSI2) and injects `ohi_tid`, `ohi_wid`, `ohi_uid`, `ohi_uname` into
7002
- * both the ID token and the access token (ADR 2026-03-17-01).
7003
- */
7004
- createPreTokenGenerationLambda() {
7005
- const construct = new PreTokenGenerationLambda(this, {
7006
- dynamoTableName: this.dataStoreTable().tableName
7007
- });
7008
- return construct.lambda;
7009
- }
7010
- /**
7011
- * Creates the Post Authentication Lambda (Cognito trigger). Calls
7012
- * AdminUserGlobalSignOut on every sign-in to enforce single-device-per-user
7013
- * sessions per ADR 2026-03-17-01.
7014
- */
7015
- createPostAuthenticationLambda() {
7016
- const construct = new PostAuthenticationLambda(this);
7017
- return construct.lambda;
6929
+ constructor(ohEnv, props = {}) {
6930
+ super(ohEnv, _OpenHiRestApiService.SERVICE_TYPE, props);
6931
+ this.props = props;
6932
+ this.validateConfig(props);
6933
+ const hostedZone = this.createHostedZone();
6934
+ const certificate = this.createCertificate();
6935
+ this.apiDomainName = this.createApiDomainNameString(hostedZone);
6936
+ this.createRestApiBaseUrlParameter(this.apiDomainName);
6937
+ this.createRestApiDomainNameParameter(this.apiDomainName);
6938
+ const domainName = this.createDomainName(hostedZone, certificate);
6939
+ this.rootHttpApi = this.createRootHttpApi(domainName);
6940
+ this.createRestApiLambdaAndRoutes(hostedZone, domainName);
7018
6941
  }
7019
6942
  /**
7020
- * Creates the Post Confirmation Lambda (Cognito trigger). On sign-up
7021
- * confirmation, publishes a control-plane workflow event; provisioning lives
7022
- * behind EventBridge.
6943
+ * Validates that config required for the REST API stack is present.
7023
6944
  */
7024
- createPostConfirmationLambda() {
7025
- const construct = new PostConfirmationLambda(this, {
7026
- controlEventBusName: this.controlEventBus().eventBusName
7027
- });
7028
- return construct.lambda;
7029
- }
7030
- createUserOnboardingWorkflow() {
7031
- return new UserOnboardingWorkflow(this, {
7032
- controlEventBus: this.controlEventBus(),
7033
- dataStoreTable: this.dataStoreTable()
7034
- });
7035
- }
7036
- dataStoreTable() {
7037
- if (this._dataStoreTable === null) {
7038
- this._dataStoreTable = OpenHiDataService.dynamoDbDataStoreFromConstruct(this);
6945
+ validateConfig(props) {
6946
+ const { config } = props;
6947
+ if (!config) {
6948
+ throw new Error("Config is required");
7039
6949
  }
7040
- return this._dataStoreTable;
7041
- }
7042
- controlEventBus() {
7043
- if (this._controlEventBus === null) {
7044
- this._controlEventBus = OpenHiGlobalService.controlEventBusFromConstruct(this);
6950
+ if (!config.hostedZoneId) {
6951
+ throw new Error("Hosted zone ID is required");
6952
+ }
6953
+ if (!config.zoneName) {
6954
+ throw new Error("Zone name is required");
7045
6955
  }
7046
- return this._controlEventBus;
7047
6956
  }
7048
6957
  /**
7049
- * Creates the Cognito User Pool and exports its ID to SSM.
7050
- * Look up via {@link OpenHiAuthService.userPoolFromConstruct}.
6958
+ * Creates the hosted zone reference (imported from config).
7051
6959
  * Override to customize.
7052
6960
  */
7053
- createUserPool() {
7054
- const userPool = new CognitoUserPool(this, {
7055
- ...this.props.userPoolProps,
7056
- customSenderKmsKey: this.userPoolKmsKey
7057
- });
7058
- userPool.addTrigger(
7059
- import_aws_cognito4.UserPoolOperation.PRE_TOKEN_GENERATION_CONFIG,
7060
- this.preTokenGenerationLambda,
7061
- import_aws_cognito4.LambdaVersion.V2_0
7062
- );
7063
- userPool.addTrigger(
7064
- import_aws_cognito4.UserPoolOperation.POST_AUTHENTICATION,
7065
- this.postAuthenticationLambda
7066
- );
7067
- userPool.addTrigger(
7068
- import_aws_cognito4.UserPoolOperation.POST_CONFIRMATION,
7069
- this.postConfirmationLambda
7070
- );
7071
- new DiscoverableStringParameter(this, "user-pool-param", {
7072
- ssmParamName: CognitoUserPool.SSM_PARAM_NAME,
7073
- stringValue: userPool.userPoolId,
7074
- description: "Cognito User Pool ID for this Auth stack; cross-stack reference"
6961
+ createHostedZone() {
6962
+ const { config } = this.props;
6963
+ return import_aws_route535.HostedZone.fromHostedZoneAttributes(this, "root-zone", {
6964
+ hostedZoneId: config.hostedZoneId,
6965
+ zoneName: config.zoneName
7075
6966
  });
7076
- return userPool;
7077
6967
  }
7078
6968
  /**
7079
- * Grants the Pre Token Generation Lambda read-only access on the data
7080
- * store table and its GSIs. The Lambda only needs:
7081
- * - `Query` on GSI2 to resolve a User by Cognito `sub`
7082
- * - `GetItem` on the base table for direct User reads
7083
- *
7084
- * No write or scan access: a User missing `currentTenant`/`currentWorkspace`
7085
- * falls into the absent-claims path; repair belongs in a separate backfill.
6969
+ * Creates the wildcard certificate (imported from Global stack via SSM).
6970
+ * Override to customize.
7086
6971
  */
7087
- grantPreTokenGenerationPermissions() {
7088
- const dataStoreTable = this.dataStoreTable();
7089
- const dynamoActions = ["dynamodb:GetItem", "dynamodb:Query"];
7090
- dataStoreTable.grant(this.preTokenGenerationLambda, ...dynamoActions);
7091
- this.preTokenGenerationLambda.addToRolePolicy(
7092
- new import_aws_iam6.PolicyStatement({
7093
- effect: import_aws_iam6.Effect.ALLOW,
7094
- actions: [...dynamoActions],
7095
- resources: [`${dataStoreTable.tableArn}/index/*`]
7096
- })
7097
- );
6972
+ createCertificate() {
6973
+ return OpenHiGlobalService.rootWildcardCertificateFromConstruct(this);
7098
6974
  }
7099
6975
  /**
7100
- * Grants the Post Authentication Lambda permission to call
7101
- * `cognito-idp:AdminUserGlobalSignOut`.
7102
- *
7103
- * Scoped via `Stack.of(this).formatArn` rather than `userPool.userPoolArn`
7104
- * because the User Pool registers this Lambda as a Post Authentication
7105
- * trigger, creating the cycle:
7106
- * userPool → lambda (trigger ARN) → role policy → userPool ARN.
7107
- * Using `formatArn` avoids referencing the User Pool resource directly
7108
- * while still scoping to user pools in this account+region. The Lambda
7109
- * is invoked only by Cognito with a Cognito-provided `event.userPoolId`,
7110
- * so the runtime target is constrained by the trigger contract.
6976
+ * Returns the API domain name string (e.g. api.example.com or api-\{prefix\}.example.com).
6977
+ * Delegates to {@link OpenHiRestApiService.composeFullDomain} so the
6978
+ * release-vs-feature composition stays in one place; picks up
6979
+ * `this.defaultReleaseBranch` (not a hard-coded `"main"`).
6980
+ * Override to customize.
7111
6981
  */
7112
- grantPostAuthenticationPermissions() {
7113
- this.postAuthenticationLambda.addToRolePolicy(
7114
- new import_aws_iam6.PolicyStatement({
7115
- actions: ["cognito-idp:AdminUserGlobalSignOut"],
7116
- resources: [
7117
- import_core.Stack.of(this).formatArn({
7118
- service: "cognito-idp",
7119
- resource: "userpool",
7120
- resourceName: "*"
7121
- })
7122
- ]
7123
- })
7124
- );
6982
+ createApiDomainNameString(hostedZone) {
6983
+ return _OpenHiRestApiService.composeFullDomain({
6984
+ branchName: this.branchName,
6985
+ defaultReleaseBranch: this.defaultReleaseBranch,
6986
+ childZonePrefix: this.childZonePrefix,
6987
+ zoneName: hostedZone.zoneName
6988
+ });
7125
6989
  }
7126
6990
  /**
7127
- * Grants the Post Confirmation Lambda publish-only access to the
7128
- * control-plane event bus. Workflow Lambdas own DynamoDB writes.
6991
+ * Creates the SSM parameter for the REST API base URL.
6992
+ * Look up via {@link OpenHiRestApiService.restApiBaseUrlFromConstruct}.
6993
+ * Override to customize.
7129
6994
  */
7130
- grantPostConfirmationPermissions() {
7131
- this.controlEventBus().grantPutEventsTo(this.postConfirmationLambda);
6995
+ createRestApiBaseUrlParameter(apiDomainName) {
6996
+ const restApiBaseUrl = `https://${apiDomainName}`;
6997
+ new DiscoverableStringParameter(this, "rest-api-base-url-param", {
6998
+ ssmParamName: REST_API_BASE_URL_SSM_NAME,
6999
+ stringValue: restApiBaseUrl,
7000
+ description: "REST API base URL for this deployment (E2E, scripts)"
7001
+ });
7132
7002
  }
7133
7003
  /**
7134
- * Creates the User Pool Client and exports its ID to SSM (AUTH service type).
7135
- * Look up via {@link OpenHiAuthService.userPoolClientFromConstruct}.
7004
+ * Creates the SSM parameter exposing the REST API's custom domain (bare
7005
+ * hostname, no scheme). Consumed by the website service as the CloudFront
7006
+ * `/api/*` origin host.
7007
+ * Look up via {@link OpenHiRestApiService.restApiDomainNameFromConstruct}.
7136
7008
  * Override to customize.
7137
7009
  */
7138
- createUserPoolClient() {
7139
- const client = new CognitoUserPoolClient(this, {
7140
- userPool: this.userPool
7141
- });
7142
- new DiscoverableStringParameter(this, "user-pool-client-param", {
7143
- ssmParamName: CognitoUserPoolClient.SSM_PARAM_NAME,
7144
- stringValue: client.userPoolClientId,
7145
- description: "Cognito User Pool Client ID for this Auth stack; cross-stack reference"
7010
+ createRestApiDomainNameParameter(apiDomainName) {
7011
+ new DiscoverableStringParameter(this, "rest-api-domain-name-param", {
7012
+ ssmParamName: REST_API_DOMAIN_NAME_SSM_NAME,
7013
+ stringValue: apiDomainName,
7014
+ description: "REST API custom domain name (bare hostname) for cross-stack CloudFront origin lookup"
7146
7015
  });
7147
- return client;
7148
7016
  }
7149
7017
  /**
7150
- * Creates the User Pool Domain (Cognito hosted UI) and exports domain name to SSM.
7151
- * Look up via {@link OpenHiAuthService.userPoolDomainFromConstruct}.
7018
+ * Creates the API Gateway custom domain name resource.
7152
7019
  * Override to customize.
7153
7020
  */
7154
- createUserPoolDomain() {
7155
- const domain = new CognitoUserPoolDomain(this, {
7156
- userPool: this.userPool,
7157
- cognitoDomain: {
7158
- domainPrefix: `auth-${this.branchHash}`
7159
- }
7160
- });
7161
- new DiscoverableStringParameter(this, "user-pool-domain-param", {
7162
- ssmParamName: CognitoUserPoolDomain.SSM_PARAM_NAME,
7163
- stringValue: domain.domainName,
7164
- description: "Cognito User Pool Domain (hosted UI) for this Auth stack; cross-stack reference"
7165
- });
7166
- return domain;
7167
- }
7168
- };
7169
- _OpenHiAuthService.SERVICE_TYPE = "auth";
7170
- var OpenHiAuthService = _OpenHiAuthService;
7171
-
7172
- // src/services/open-hi-rest-api-service.ts
7173
- var import_config5 = __toESM(require_lib());
7174
- var import_aws_apigatewayv22 = require("aws-cdk-lib/aws-apigatewayv2");
7175
- var import_aws_apigatewayv2_authorizers = require("aws-cdk-lib/aws-apigatewayv2-authorizers");
7176
- var import_aws_apigatewayv2_integrations = require("aws-cdk-lib/aws-apigatewayv2-integrations");
7177
- var import_aws_iam7 = require("aws-cdk-lib/aws-iam");
7178
- var import_aws_route535 = require("aws-cdk-lib/aws-route53");
7179
- var import_aws_route53_targets3 = require("aws-cdk-lib/aws-route53-targets");
7180
- var import_core2 = require("aws-cdk-lib/core");
7181
- var import_constructs21 = require("constructs");
7182
-
7183
- // src/data/lambda/cors-options-lambda.ts
7184
- var import_node_fs10 = __toESM(require("fs"));
7185
- var import_node_path10 = __toESM(require("path"));
7186
- var import_aws_lambda11 = require("aws-cdk-lib/aws-lambda");
7187
- var import_aws_lambda_nodejs11 = require("aws-cdk-lib/aws-lambda-nodejs");
7188
- var import_constructs19 = require("constructs");
7189
- var HANDLER_NAME10 = "cors-options-lambda.handler.js";
7190
- function resolveHandlerEntry10(dirname) {
7191
- const sameDir = import_node_path10.default.join(dirname, HANDLER_NAME10);
7192
- if (import_node_fs10.default.existsSync(sameDir)) {
7193
- return sameDir;
7194
- }
7195
- const fromLib = import_node_path10.default.join(dirname, "..", "..", "..", "lib", HANDLER_NAME10);
7196
- return fromLib;
7197
- }
7198
- var CorsOptionsLambda = class extends import_constructs19.Construct {
7199
- constructor(scope, id = "cors-options-lambda") {
7200
- super(scope, id);
7201
- this.lambda = new import_aws_lambda_nodejs11.NodejsFunction(this, "handler", {
7202
- entry: resolveHandlerEntry10(__dirname),
7203
- runtime: import_aws_lambda11.Runtime.NODEJS_LATEST,
7204
- memorySize: 128
7205
- });
7206
- }
7207
- };
7208
-
7209
- // src/data/lambda/rest-api-lambda.ts
7210
- var import_node_fs11 = __toESM(require("fs"));
7211
- var import_node_path11 = __toESM(require("path"));
7212
- var import_aws_lambda12 = require("aws-cdk-lib/aws-lambda");
7213
- var import_aws_lambda_nodejs12 = require("aws-cdk-lib/aws-lambda-nodejs");
7214
- var import_constructs20 = require("constructs");
7215
- var HANDLER_NAME11 = "rest-api-lambda.handler.js";
7216
- function resolveHandlerEntry11(dirname) {
7217
- const sameDir = import_node_path11.default.join(dirname, HANDLER_NAME11);
7218
- if (import_node_fs11.default.existsSync(sameDir)) {
7219
- return sameDir;
7220
- }
7221
- const fromLib = import_node_path11.default.join(dirname, "..", "..", "..", "lib", HANDLER_NAME11);
7222
- return fromLib;
7223
- }
7224
- var RestApiLambda = class extends import_constructs20.Construct {
7225
- constructor(scope, props) {
7226
- super(scope, "rest-api-lambda");
7227
- this.lambda = new import_aws_lambda_nodejs12.NodejsFunction(this, "handler", {
7228
- entry: resolveHandlerEntry11(__dirname),
7229
- runtime: import_aws_lambda12.Runtime.NODEJS_LATEST,
7230
- memorySize: 1024,
7231
- environment: {
7232
- DYNAMO_TABLE_NAME: props.dynamoTableName,
7233
- BRANCH_TAG_VALUE: props.branchTagValue,
7234
- HTTP_API_TAG_VALUE: props.httpApiTagValue,
7235
- OPENHI_PG_CLUSTER_ARN: props.postgresClusterArn,
7236
- OPENHI_PG_SECRET_ARN: props.postgresSecretArn,
7237
- OPENHI_PG_DATABASE: props.postgresDatabase,
7238
- OPENHI_PG_SCHEMA: props.postgresSchema,
7239
- ...props.extraEnvironment
7240
- },
7241
- bundling: {
7242
- minify: true,
7243
- sourceMap: false
7244
- }
7245
- });
7246
- }
7247
- };
7248
-
7249
- // src/services/open-hi-rest-api-service.ts
7250
- var REST_API_BASE_URL_SSM_NAME = "REST_API_BASE_URL";
7251
- var REST_API_DOMAIN_NAME_SSM_NAME = "REST_API_DOMAIN_NAME";
7252
- var DEV_CORS_ALLOW_ORIGINS = [
7253
- "http://localhost:3000",
7254
- "https://localhost:3000",
7255
- "http://localhost:5173",
7256
- "https://localhost:5173",
7257
- "http://127.0.0.1:3000",
7258
- "https://127.0.0.1:3000",
7259
- "http://127.0.0.1:5173",
7260
- "https://127.0.0.1:5173"
7261
- ];
7262
- var _OpenHiRestApiService = class _OpenHiRestApiService extends OpenHiService {
7263
- /**
7264
- * Compose the REST API's full per-deploy domain. Thin wrapper over
7265
- * {@link OpenHiService.composeServiceDomain} that pins `domainPrefix`
7266
- * to {@link API_DOMAIN_PREFIX}.
7267
- *
7268
- * Use from sibling stacks that need to predict the API's hostname
7269
- * before the REST API stack is synthesised.
7270
- */
7271
- static composeFullDomain(opts) {
7272
- return OpenHiService.composeServiceDomain({
7273
- ...opts,
7274
- domainPrefix: _OpenHiRestApiService.API_DOMAIN_PREFIX
7275
- });
7276
- }
7277
- /**
7278
- * Returns an IHttpApi by looking up the REST API stack's HTTP API ID from SSM.
7279
- */
7280
- static rootHttpApiFromConstruct(scope) {
7281
- const httpApiId = DiscoverableStringParameter.valueForLookupName(scope, {
7282
- ssmParamName: RootHttpApi.SSM_PARAM_NAME,
7283
- serviceType: _OpenHiRestApiService.SERVICE_TYPE
7284
- });
7285
- return import_aws_apigatewayv22.HttpApi.fromHttpApiAttributes(scope, "http-api", { httpApiId });
7286
- }
7287
- /**
7288
- * Returns the REST API base URL (e.g. https://api.example.com) by looking it up from SSM.
7289
- * Use in other stacks for E2E, scripts, or config.
7290
- */
7291
- static restApiBaseUrlFromConstruct(scope) {
7292
- return DiscoverableStringParameter.valueForLookupName(scope, {
7293
- ssmParamName: REST_API_BASE_URL_SSM_NAME,
7294
- serviceType: _OpenHiRestApiService.SERVICE_TYPE
7295
- });
7296
- }
7297
- /**
7298
- * Returns the REST API's custom domain name (bare hostname, no scheme — e.g.
7299
- * `api.example.com`) by looking it up from SSM. Use as the host for a
7300
- * CloudFront `HttpOrigin` so the website's distribution can proxy `/api/*`
7301
- * to this stack's API Gateway without per-branch DNS knowledge.
7302
- */
7303
- static restApiDomainNameFromConstruct(scope) {
7304
- return DiscoverableStringParameter.valueForLookupName(scope, {
7305
- ssmParamName: REST_API_DOMAIN_NAME_SSM_NAME,
7306
- serviceType: _OpenHiRestApiService.SERVICE_TYPE
7307
- });
7308
- }
7309
- get serviceType() {
7310
- return _OpenHiRestApiService.SERVICE_TYPE;
7311
- }
7312
- constructor(ohEnv, props = {}) {
7313
- super(ohEnv, _OpenHiRestApiService.SERVICE_TYPE, props);
7314
- this.props = props;
7315
- this.validateConfig(props);
7316
- const hostedZone = this.createHostedZone();
7317
- const certificate = this.createCertificate();
7318
- this.apiDomainName = this.createApiDomainNameString(hostedZone);
7319
- this.createRestApiBaseUrlParameter(this.apiDomainName);
7320
- this.createRestApiDomainNameParameter(this.apiDomainName);
7321
- const domainName = this.createDomainName(hostedZone, certificate);
7322
- this.rootHttpApi = this.createRootHttpApi(domainName);
7323
- this.createRestApiLambdaAndRoutes(hostedZone, domainName);
7324
- }
7325
- /**
7326
- * Validates that config required for the REST API stack is present.
7327
- */
7328
- validateConfig(props) {
7329
- const { config } = props;
7330
- if (!config) {
7331
- throw new Error("Config is required");
7332
- }
7333
- if (!config.hostedZoneId) {
7334
- throw new Error("Hosted zone ID is required");
7335
- }
7336
- if (!config.zoneName) {
7337
- throw new Error("Zone name is required");
7338
- }
7339
- }
7340
- /**
7341
- * Creates the hosted zone reference (imported from config).
7342
- * Override to customize.
7343
- */
7344
- createHostedZone() {
7345
- const { config } = this.props;
7346
- return import_aws_route535.HostedZone.fromHostedZoneAttributes(this, "root-zone", {
7347
- hostedZoneId: config.hostedZoneId,
7348
- zoneName: config.zoneName
7349
- });
7350
- }
7351
- /**
7352
- * Creates the wildcard certificate (imported from Global stack via SSM).
7353
- * Override to customize.
7354
- */
7355
- createCertificate() {
7356
- return OpenHiGlobalService.rootWildcardCertificateFromConstruct(this);
7357
- }
7358
- /**
7359
- * Returns the API domain name string (e.g. api.example.com or api-\{prefix\}.example.com).
7360
- * Delegates to {@link OpenHiRestApiService.composeFullDomain} so the
7361
- * release-vs-feature composition stays in one place; picks up
7362
- * `this.defaultReleaseBranch` (not a hard-coded `"main"`).
7363
- * Override to customize.
7364
- */
7365
- createApiDomainNameString(hostedZone) {
7366
- return _OpenHiRestApiService.composeFullDomain({
7367
- branchName: this.branchName,
7368
- defaultReleaseBranch: this.defaultReleaseBranch,
7369
- childZonePrefix: this.childZonePrefix,
7370
- zoneName: hostedZone.zoneName
7371
- });
7372
- }
7373
- /**
7374
- * Creates the SSM parameter for the REST API base URL.
7375
- * Look up via {@link OpenHiRestApiService.restApiBaseUrlFromConstruct}.
7376
- * Override to customize.
7377
- */
7378
- createRestApiBaseUrlParameter(apiDomainName) {
7379
- const restApiBaseUrl = `https://${apiDomainName}`;
7380
- new DiscoverableStringParameter(this, "rest-api-base-url-param", {
7381
- ssmParamName: REST_API_BASE_URL_SSM_NAME,
7382
- stringValue: restApiBaseUrl,
7383
- description: "REST API base URL for this deployment (E2E, scripts)"
7384
- });
7385
- }
7386
- /**
7387
- * Creates the SSM parameter exposing the REST API's custom domain (bare
7388
- * hostname, no scheme). Consumed by the website service as the CloudFront
7389
- * `/api/*` origin host.
7390
- * Look up via {@link OpenHiRestApiService.restApiDomainNameFromConstruct}.
7391
- * Override to customize.
7392
- */
7393
- createRestApiDomainNameParameter(apiDomainName) {
7394
- new DiscoverableStringParameter(this, "rest-api-domain-name-param", {
7395
- ssmParamName: REST_API_DOMAIN_NAME_SSM_NAME,
7396
- stringValue: apiDomainName,
7397
- description: "REST API custom domain name (bare hostname) for cross-stack CloudFront origin lookup"
7398
- });
7399
- }
7400
- /**
7401
- * Creates the API Gateway custom domain name resource.
7402
- * Override to customize.
7403
- */
7404
- createDomainName(_hostedZone, certificate) {
7405
- const apiDomainName = this.createApiDomainNameString(_hostedZone);
7406
- return new import_aws_apigatewayv22.DomainName(this, "domain", {
7407
- domainName: apiDomainName,
7408
- certificate
7021
+ createDomainName(_hostedZone, certificate) {
7022
+ const apiDomainName = this.createApiDomainNameString(_hostedZone);
7023
+ return new import_aws_apigatewayv22.DomainName(this, "domain", {
7024
+ domainName: apiDomainName,
7025
+ certificate
7409
7026
  });
7410
7027
  }
7411
7028
  /**
@@ -7430,8 +7047,8 @@ var _OpenHiRestApiService = class _OpenHiRestApiService extends OpenHiService {
7430
7047
  extraEnvironment
7431
7048
  });
7432
7049
  lambda.addToRolePolicy(
7433
- new import_aws_iam7.PolicyStatement({
7434
- effect: import_aws_iam7.Effect.ALLOW,
7050
+ new import_aws_iam5.PolicyStatement({
7051
+ effect: import_aws_iam5.Effect.ALLOW,
7435
7052
  actions: [
7436
7053
  "rds-data:ExecuteStatement",
7437
7054
  "rds-data:BatchExecuteStatement"
@@ -7440,8 +7057,8 @@ var _OpenHiRestApiService = class _OpenHiRestApiService extends OpenHiService {
7440
7057
  })
7441
7058
  );
7442
7059
  lambda.addToRolePolicy(
7443
- new import_aws_iam7.PolicyStatement({
7444
- effect: import_aws_iam7.Effect.ALLOW,
7060
+ new import_aws_iam5.PolicyStatement({
7061
+ effect: import_aws_iam5.Effect.ALLOW,
7445
7062
  actions: ["secretsmanager:GetSecretValue"],
7446
7063
  resources: [postgresSecretArn]
7447
7064
  })
@@ -7459,15 +7076,15 @@ var _OpenHiRestApiService = class _OpenHiRestApiService extends OpenHiService {
7459
7076
  ];
7460
7077
  dataStoreTable.grant(lambda, ...dynamoActions);
7461
7078
  lambda.addToRolePolicy(
7462
- new import_aws_iam7.PolicyStatement({
7463
- effect: import_aws_iam7.Effect.ALLOW,
7079
+ new import_aws_iam5.PolicyStatement({
7080
+ effect: import_aws_iam5.Effect.ALLOW,
7464
7081
  actions: [...dynamoActions],
7465
7082
  resources: [`${dataStoreTable.tableArn}/index/*`]
7466
7083
  })
7467
7084
  );
7468
7085
  lambda.addToRolePolicy(
7469
- new import_aws_iam7.PolicyStatement({
7470
- effect: import_aws_iam7.Effect.ALLOW,
7086
+ new import_aws_iam5.PolicyStatement({
7087
+ effect: import_aws_iam5.Effect.ALLOW,
7471
7088
  actions: [
7472
7089
  "ssm:GetParameter",
7473
7090
  "ssm:GetParameters",
@@ -7548,11 +7165,18 @@ var _OpenHiRestApiService = class _OpenHiRestApiService extends OpenHiService {
7548
7165
  const { corsPreflight: cors, ...restRootHttpApiProps } = this.props.rootHttpApiProps ?? {};
7549
7166
  const isNonProd = this.ohEnv.ohStage.stageType !== import_config5.OPEN_HI_STAGE.PROD;
7550
7167
  const callerOrigins = cors?.allowOrigins ?? [];
7551
- const mergedOrigins = isNonProd ? Array.from(/* @__PURE__ */ new Set([...callerOrigins, ...DEV_CORS_ALLOW_ORIGINS])) : callerOrigins;
7552
- const corsPreflight = cors !== void 0 || isNonProd ? this.buildCorsPreflightOptions(mergedOrigins, cors) : void 0;
7168
+ const autoOrigins = this.resolveAutoInjectedCorsOrigins();
7169
+ const mergedOrigins = Array.from(
7170
+ /* @__PURE__ */ new Set([
7171
+ ...callerOrigins,
7172
+ ...autoOrigins,
7173
+ ...isNonProd ? DEV_CORS_ALLOW_ORIGINS : []
7174
+ ])
7175
+ );
7176
+ const corsPreflight = this.buildCorsPreflightOptions(mergedOrigins, cors);
7553
7177
  const rootHttpApi = new RootHttpApi(this, {
7554
7178
  ...restRootHttpApiProps,
7555
- ...corsPreflight !== void 0 && { corsPreflight },
7179
+ corsPreflight,
7556
7180
  defaultDomainMapping: {
7557
7181
  domainName,
7558
7182
  mappingKey: void 0
@@ -7566,6 +7190,33 @@ var _OpenHiRestApiService = class _OpenHiRestApiService extends OpenHiService {
7566
7190
  });
7567
7191
  return rootHttpApi;
7568
7192
  }
7193
+ /**
7194
+ * Returns the admin-console and marketing-website origins this REST API
7195
+ * stack should accept by default, composed from the same branch context
7196
+ * the website service will see at synth time. Both hostnames are
7197
+ * `https://`-only — they always resolve to real DNS records.
7198
+ *
7199
+ * Auto-injected on every stage (no `isNonProd` gate) so the admin SPA can
7200
+ * call the API cross-origin without the caller having to predict the
7201
+ * per-deploy hostname. Override to customize the auto-injected set.
7202
+ */
7203
+ resolveAutoInjectedCorsOrigins() {
7204
+ const zoneName = this.props.config.zoneName;
7205
+ const adminHost = OpenHiWebsiteService.composeFullDomain({
7206
+ domainPrefix: ADMIN_DOMAIN_PREFIX,
7207
+ branchName: this.branchName,
7208
+ defaultReleaseBranch: this.defaultReleaseBranch,
7209
+ childZonePrefix: this.childZonePrefix,
7210
+ zoneName
7211
+ });
7212
+ const websiteHost = OpenHiWebsiteService.composeFullDomain({
7213
+ branchName: this.branchName,
7214
+ defaultReleaseBranch: this.defaultReleaseBranch,
7215
+ childZonePrefix: this.childZonePrefix,
7216
+ zoneName
7217
+ });
7218
+ return [`https://${adminHost}`, `https://${websiteHost}`];
7219
+ }
7569
7220
  /**
7570
7221
  * Builds the full `CorsPreflightOptions` from a merged origins array,
7571
7222
  * filling defaults for `allowMethods`/`allowHeaders`/`allowCredentials`/
@@ -7585,7 +7236,7 @@ var _OpenHiRestApiService = class _OpenHiRestApiService extends OpenHiService {
7585
7236
  ],
7586
7237
  allowHeaders: cors?.allowHeaders ?? ["Content-Type", "Authorization"],
7587
7238
  allowCredentials: cors?.allowCredentials ?? true,
7588
- maxAge: cors?.maxAge ?? import_core2.Duration.days(1),
7239
+ maxAge: cors?.maxAge ?? import_core.Duration.days(1),
7589
7240
  ...cors?.exposeHeaders !== void 0 && {
7590
7241
  exposeHeaders: cors.exposeHeaders
7591
7242
  }
@@ -7603,7 +7254,7 @@ var _OpenHiRestApiService = class _OpenHiRestApiService extends OpenHiService {
7603
7254
  * client-side from `window.location.origin`.
7604
7255
  */
7605
7256
  resolveRuntimeConfigEnvVars() {
7606
- const cognitoScope = new import_constructs21.Construct(this, "runtime-config");
7257
+ const cognitoScope = new import_constructs19.Construct(this, "runtime-config");
7607
7258
  const userPool = OpenHiAuthService.userPoolFromConstruct(cognitoScope);
7608
7259
  const userPoolClient = OpenHiAuthService.userPoolClientFromConstruct(cognitoScope);
7609
7260
  const cognitoDomainUrl = OpenHiAuthService.userPoolDomainBaseUrlFromConstruct(cognitoScope);
@@ -7623,330 +7274,774 @@ _OpenHiRestApiService.SERVICE_TYPE = "rest-api";
7623
7274
  _OpenHiRestApiService.API_DOMAIN_PREFIX = "api";
7624
7275
  var OpenHiRestApiService = _OpenHiRestApiService;
7625
7276
 
7626
- // src/services/open-hi-graphql-service.ts
7627
- var import_aws_appsync2 = require("aws-cdk-lib/aws-appsync");
7628
- var _OpenHiGraphqlService = class _OpenHiGraphqlService extends OpenHiService {
7277
+ // src/services/open-hi-website-service.ts
7278
+ var SSM_PARAM_NAME_FULL_DOMAIN = "WEBSITE_FULL_DOMAIN";
7279
+ var ADMIN_DOMAIN_PREFIX = "admin";
7280
+ var _OpenHiWebsiteService = class _OpenHiWebsiteService extends OpenHiService {
7629
7281
  /**
7630
- * Returns the GraphQL API by looking up the GraphQL stack's API ID from SSM.
7631
- * Use from other stacks to obtain an IGraphqlApi reference.
7282
+ * Compose the website's full per-deploy domain. Thin wrapper over
7283
+ * {@link OpenHiService.composeServiceDomain} that fills in
7284
+ * {@link DEFAULT_DOMAIN_PREFIX} when `domainPrefix` is omitted.
7285
+ *
7286
+ * Use from sibling stacks that need to predict the website's hostname
7287
+ * before the website stack is synthesised — e.g. the REST API stack
7288
+ * computing its CORS `allowOrigins` for the admin-console.
7632
7289
  */
7633
- static graphqlApiFromConstruct(scope) {
7634
- return RootGraphqlApi.fromConstruct(scope);
7290
+ static composeFullDomain(opts) {
7291
+ return OpenHiService.composeServiceDomain({
7292
+ ...opts,
7293
+ domainPrefix: opts.domainPrefix ?? _OpenHiWebsiteService.DEFAULT_DOMAIN_PREFIX
7294
+ });
7635
7295
  }
7636
- get serviceType() {
7637
- return _OpenHiGraphqlService.SERVICE_TYPE;
7296
+ /**
7297
+ * Looks up the static-hosting bucket ARN published by the release-branch
7298
+ * deploy of this service.
7299
+ */
7300
+ static bucketArnFromConstruct(scope) {
7301
+ return DiscoverableStringParameter.valueForLookupName(scope, {
7302
+ ssmParamName: StaticHosting.SSM_PARAM_NAME_BUCKET_ARN,
7303
+ serviceType: _OpenHiWebsiteService.SERVICE_TYPE
7304
+ });
7305
+ }
7306
+ /**
7307
+ * Looks up the CloudFront distribution ARN published by the release-branch
7308
+ * deploy of this service.
7309
+ */
7310
+ static distributionArnFromConstruct(scope) {
7311
+ return DiscoverableStringParameter.valueForLookupName(scope, {
7312
+ ssmParamName: StaticHosting.SSM_PARAM_NAME_DISTRIBUTION_ARN,
7313
+ serviceType: _OpenHiWebsiteService.SERVICE_TYPE
7314
+ });
7315
+ }
7316
+ /**
7317
+ * Looks up the CloudFront distribution domain
7318
+ * (e.g. dXXXXX.cloudfront.net) published by the release-branch deploy.
7319
+ */
7320
+ static distributionDomainFromConstruct(scope) {
7321
+ return DiscoverableStringParameter.valueForLookupName(scope, {
7322
+ ssmParamName: StaticHosting.SSM_PARAM_NAME_DISTRIBUTION_DOMAIN,
7323
+ serviceType: _OpenHiWebsiteService.SERVICE_TYPE
7324
+ });
7325
+ }
7326
+ /**
7327
+ * Looks up the CloudFront distribution ID published by the release-branch
7328
+ * deploy of this service.
7329
+ */
7330
+ static distributionIdFromConstruct(scope) {
7331
+ return DiscoverableStringParameter.valueForLookupName(scope, {
7332
+ ssmParamName: StaticHosting.SSM_PARAM_NAME_DISTRIBUTION_ID,
7333
+ serviceType: _OpenHiWebsiteService.SERVICE_TYPE
7334
+ });
7335
+ }
7336
+ /**
7337
+ * Looks up the website's full domain (e.g. www.example.com) published by
7338
+ * the release-branch deploy of this service.
7339
+ */
7340
+ static fullDomainFromConstruct(scope) {
7341
+ return DiscoverableStringParameter.valueForLookupName(scope, {
7342
+ ssmParamName: SSM_PARAM_NAME_FULL_DOMAIN,
7343
+ serviceType: _OpenHiWebsiteService.SERVICE_TYPE
7344
+ });
7345
+ }
7346
+ get serviceType() {
7347
+ return _OpenHiWebsiteService.SERVICE_TYPE;
7348
+ }
7349
+ constructor(ohEnv, props) {
7350
+ super(ohEnv, _OpenHiWebsiteService.SERVICE_TYPE, props);
7351
+ this.props = props;
7352
+ this.validateConfig(props);
7353
+ const isReleaseBranch = this.branchName === this.defaultReleaseBranch;
7354
+ const hostedZone = this.createHostedZone();
7355
+ this.fullDomain = this.computeFullDomain(hostedZone);
7356
+ const shouldCreateHostingInfra = props.createHostingInfrastructure ?? isReleaseBranch;
7357
+ if (shouldCreateHostingInfra) {
7358
+ const certificate = this.createCertificate();
7359
+ this.staticHosting = this.createStaticHosting({
7360
+ certificate,
7361
+ hostedZone
7362
+ });
7363
+ this.createFullDomainParameter();
7364
+ } else if (!isReleaseBranch) {
7365
+ this.perBranchHostname = this.createPerBranchHostname(hostedZone);
7366
+ }
7367
+ if (props.createStaticContent !== false) {
7368
+ const bucket = this.resolveStaticHostingBucket();
7369
+ this.staticContent = this.createStaticContent(bucket);
7370
+ }
7371
+ }
7372
+ /**
7373
+ * Validates that config required for the website stack is present.
7374
+ */
7375
+ validateConfig(props) {
7376
+ const { config } = props;
7377
+ if (!config) {
7378
+ throw new Error("Config is required");
7379
+ }
7380
+ if (!config.zoneName) {
7381
+ throw new Error("Zone name is required");
7382
+ }
7383
+ if (!config.hostedZoneId) {
7384
+ throw new Error("Hosted zone ID is required to import the website zone");
7385
+ }
7386
+ }
7387
+ /**
7388
+ * Imports the website's hosted zone from config attributes (no SSM lookup).
7389
+ * The website attaches DNS records here on the release-branch deploy and
7390
+ * the same zone is imported on feature-branch deploys for any sub-domain
7391
+ * routing.
7392
+ * Override to customize.
7393
+ */
7394
+ createHostedZone() {
7395
+ return OpenHiGlobalService.rootHostedZoneFromConstruct(this, {
7396
+ zoneName: this.config.zoneName,
7397
+ hostedZoneId: this.config.hostedZoneId
7398
+ });
7399
+ }
7400
+ /**
7401
+ * Returns the wildcard certificate looked up from the Global service.
7402
+ * Override to customize.
7403
+ */
7404
+ createCertificate() {
7405
+ return OpenHiGlobalService.rootWildcardCertificateFromConstruct(this);
7406
+ }
7407
+ /**
7408
+ * Computes the full website domain from `domainPrefix`,
7409
+ * `childZonePrefix`, and the child zone name. Release-branch deploys
7410
+ * serve at `\<domainPrefix\>.\<zone\>` (e.g. `admin.dev.openhi.org`);
7411
+ * every other deploy serves a per-PR preview at
7412
+ * `\<domainPrefix\>-\<childZonePrefix\>.\<zone\>`
7413
+ * (e.g. `admin-feat-1093-patient-migration.dev.openhi.org`).
7414
+ *
7415
+ * Delegates to {@link OpenHiWebsiteService.composeFullDomain} so the
7416
+ * release-vs-feature composition stays in one place.
7417
+ */
7418
+ computeFullDomain(hostedZone) {
7419
+ return _OpenHiWebsiteService.composeFullDomain({
7420
+ domainPrefix: this.props.domainPrefix,
7421
+ branchName: this.branchName,
7422
+ defaultReleaseBranch: this.defaultReleaseBranch,
7423
+ childZonePrefix: this.childZonePrefix,
7424
+ zoneName: hostedZone.zoneName
7425
+ });
7426
+ }
7427
+ /**
7428
+ * Returns the sub-domain label (left of the zone) for the current
7429
+ * deploy. Used for the per-branch S3 key prefix passed to
7430
+ * {@link StaticContent} so the upload prefix always matches the
7431
+ * served hostname.
7432
+ *
7433
+ * Non-release deploys compose the per-PR slug as
7434
+ * `\<domainPrefix\>-\<childZonePrefix\>`, mirroring the REST API's
7435
+ * `api-\<childZonePrefix\>` convention. When `domainPrefix` is `admin`
7436
+ * (the only consumer today), the resulting sub-domain starts with
7437
+ * {@link PER_BRANCH_PREVIEW_PREFIX}, so the per-PR S3 key prefix
7438
+ * matches what `StaticHosting`'s lifecycle rule expires.
7439
+ */
7440
+ computeSubDomain() {
7441
+ const isReleaseBranch = this.branchName === this.defaultReleaseBranch;
7442
+ const domainPrefix = this.props.domainPrefix ?? _OpenHiWebsiteService.DEFAULT_DOMAIN_PREFIX;
7443
+ if (isReleaseBranch) {
7444
+ return domainPrefix;
7445
+ }
7446
+ return `${domainPrefix}-${this.childZonePrefix}`;
7447
+ }
7448
+ /**
7449
+ * Creates the StaticHosting infrastructure (bucket + distribution +
7450
+ * Lambda@Edge + 4 SSM params + DNS). The release-branch distribution
7451
+ * adds `*.\<zone\>` as a wildcard alt-name on top of the canonical
7452
+ * hostname so per-PR previews resolve via the same distribution.
7453
+ *
7454
+ * The bucket carries an S3 lifecycle rule that expires per-PR
7455
+ * preview content (keys under {@link PER_BRANCH_PREVIEW_PREFIX})
7456
+ * on non-production stages. PROD never gets the rule — see
7457
+ * `enablePreviewLifecycle`.
7458
+ */
7459
+ createStaticHosting(deps) {
7460
+ const restApi = this.props.restApi === true ? this.resolveRestApi() : void 0;
7461
+ const wildcardSan = `*.${deps.hostedZone.zoneName}`;
7462
+ return new StaticHosting(this, "static-hosting", {
7463
+ serviceType: _OpenHiWebsiteService.SERVICE_TYPE,
7464
+ certificate: deps.certificate,
7465
+ hostedZone: deps.hostedZone,
7466
+ domainNames: [this.fullDomain, wildcardSan],
7467
+ description: `OpenHI website (${this.fullDomain})`,
7468
+ prefixPattern: PER_BRANCH_PREVIEW_PREFIX,
7469
+ enablePreviewLifecycle: this.ohEnv.ohStage.stageType !== import_config6.OPEN_HI_STAGE.PROD,
7470
+ ...restApi !== void 0 && { restApi }
7471
+ });
7472
+ }
7473
+ /**
7474
+ * Resolves the REST API custom-domain hostname from the rest-api stack's
7475
+ * `REST_API_DOMAIN_NAME` SSM parameter. Wrapped in a private method so
7476
+ * it can be overridden / stubbed in subclasses and tests.
7477
+ */
7478
+ resolveRestApi() {
7479
+ return {
7480
+ domainName: OpenHiRestApiService.restApiDomainNameFromConstruct(this)
7481
+ };
7482
+ }
7483
+ /**
7484
+ * Creates the SSM parameter that publishes the website's full domain.
7485
+ * Look up via {@link OpenHiWebsiteService.fullDomainFromConstruct}.
7486
+ */
7487
+ createFullDomainParameter() {
7488
+ new DiscoverableStringParameter(this, "full-domain-param", {
7489
+ ssmParamName: SSM_PARAM_NAME_FULL_DOMAIN,
7490
+ serviceType: _OpenHiWebsiteService.SERVICE_TYPE,
7491
+ stringValue: this.fullDomain,
7492
+ description: "Full website domain (e.g. www.example.com)"
7493
+ });
7494
+ }
7495
+ /**
7496
+ * Creates the StaticContent uploader. Receives the resolved static-hosting
7497
+ * bucket from the constructor — on the release-branch deploy this is the
7498
+ * just-created {@link staticHosting} bucket (no SSM round-trip within a
7499
+ * single stack); on every other deploy it is imported from the bucket ARN
7500
+ * the release-branch deploy publishes to SSM, addressed against
7501
+ * {@link OpenHiService.releaseBranchHash}. See
7502
+ * {@link resolveStaticHostingBucket}.
7503
+ *
7504
+ * The S3 key prefix is `\<sub-domain\>.\<zone\>/\<contentDest\>` so the
7505
+ * upload location matches the Host-header-derived folder the Lambda@Edge
7506
+ * viewer-request handler prepends. Passing the zone name (rather than
7507
+ * `this.fullDomain`) for the `fullDomain` prop keeps the prefix flat —
7508
+ * `admin-feat-foo.dev.openhi.org/`, not
7509
+ * `admin-feat-foo.admin.dev.openhi.org/`.
7510
+ */
7511
+ createStaticContent(bucket) {
7512
+ const { contentSourceDirectory, contentDestinationDirectory } = this.props;
7513
+ return new StaticContent(this, "static-content", {
7514
+ bucket,
7515
+ contentSourceDirectory,
7516
+ contentDestinationDirectory,
7517
+ subDomain: this.computeSubDomain(),
7518
+ fullDomain: this.config.zoneName
7519
+ });
7520
+ }
7521
+ /**
7522
+ * Creates the per-PR `PerBranchHostname` alias record on non-release
7523
+ * branch deploys. The record points
7524
+ * `\<domainPrefix\>-\<childZonePrefix\>.\<zone\>` at the release-branch
7525
+ * CloudFront distribution (resolved from SSM against
7526
+ * {@link OpenHiService.releaseBranchHash}).
7527
+ */
7528
+ createPerBranchHostname(hostedZone) {
7529
+ return new PerBranchHostname(this, "per-branch-hostname", {
7530
+ hostname: this.fullDomain,
7531
+ hostedZone,
7532
+ serviceType: _OpenHiWebsiteService.SERVICE_TYPE
7533
+ });
7534
+ }
7535
+ /**
7536
+ * Returns an {@link IBucket} pointing at the static-hosting bucket the
7537
+ * uploaders write to. On the release-branch deploy this is the bucket
7538
+ * just provisioned by {@link staticHosting}; on every other deploy it's
7539
+ * imported from the bucket ARN the release-branch deploy publishes to
7540
+ * SSM, addressed against {@link OpenHiService.releaseBranchHash}.
7541
+ */
7542
+ resolveStaticHostingBucket() {
7543
+ if (this.staticHosting) {
7544
+ return this.staticHosting.bucket;
7545
+ }
7546
+ const bucketArn = DiscoverableStringParameter.valueForLookupName(this, {
7547
+ ssmParamName: StaticHosting.SSM_PARAM_NAME_BUCKET_ARN,
7548
+ serviceType: _OpenHiWebsiteService.SERVICE_TYPE,
7549
+ branchHash: this.releaseBranchHash
7550
+ });
7551
+ return import_aws_s32.Bucket.fromBucketArn(this, "shared-bucket", bucketArn);
7552
+ }
7553
+ };
7554
+ _OpenHiWebsiteService.SERVICE_TYPE = "website";
7555
+ /**
7556
+ * Default `domainPrefix` for this service when none is supplied.
7557
+ * Release-branch hostname is `www.<zone>`; per-PR preview hostname is
7558
+ * `www-<childZonePrefix>.<zone>`.
7559
+ */
7560
+ _OpenHiWebsiteService.DEFAULT_DOMAIN_PREFIX = "www";
7561
+ var OpenHiWebsiteService = _OpenHiWebsiteService;
7562
+
7563
+ // src/workflows/control-plane/user-onboarding/events.ts
7564
+ var USER_ONBOARDING_EVENT_SOURCE = "openhi.control.user-onboarding";
7565
+ var PROVISION_DEFAULT_WORKSPACE_DETAIL_TYPE = "ProvisionDefaultWorkspaceRequested";
7566
+ var buildProvisionDefaultWorkspaceRequestedDetail = (event) => {
7567
+ const attrs = event.request?.userAttributes ?? {};
7568
+ const cognitoSub = attrs.sub?.trim();
7569
+ if (!cognitoSub) {
7570
+ return void 0;
7571
+ }
7572
+ const email = attrs.email?.trim();
7573
+ const displayName = email || event.userName || cognitoSub;
7574
+ return {
7575
+ cognitoSub,
7576
+ ...email ? { email } : {},
7577
+ displayName,
7578
+ trigger: {
7579
+ source: "cognito.post-confirmation",
7580
+ triggerSource: event.triggerSource,
7581
+ userPoolId: event.userPoolId,
7582
+ userName: event.userName,
7583
+ clientId: event.callerContext?.clientId
7584
+ }
7585
+ };
7586
+ };
7587
+
7588
+ // src/workflows/control-plane/user-onboarding/provision-default-workspace-lambda.ts
7589
+ var import_node_fs11 = __toESM(require("fs"));
7590
+ var import_node_path11 = __toESM(require("path"));
7591
+ var import_aws_cdk_lib15 = require("aws-cdk-lib");
7592
+ var import_aws_events8 = require("aws-cdk-lib/aws-events");
7593
+ var import_aws_events_targets4 = require("aws-cdk-lib/aws-events-targets");
7594
+ var import_aws_iam6 = require("aws-cdk-lib/aws-iam");
7595
+ var import_aws_lambda12 = require("aws-cdk-lib/aws-lambda");
7596
+ var import_aws_lambda_nodejs12 = require("aws-cdk-lib/aws-lambda-nodejs");
7597
+ var import_constructs20 = require("constructs");
7598
+ var HANDLER_NAME11 = "provision-default-workspace.handler.js";
7599
+ function resolveHandlerEntry11(dirname) {
7600
+ const sameDir = import_node_path11.default.join(dirname, HANDLER_NAME11);
7601
+ if (import_node_fs11.default.existsSync(sameDir)) {
7602
+ return sameDir;
7603
+ }
7604
+ return import_node_path11.default.join(dirname, "..", "..", "..", "..", "lib", HANDLER_NAME11);
7605
+ }
7606
+ var ProvisionDefaultWorkspaceLambda = class extends import_constructs20.Construct {
7607
+ constructor(scope, props) {
7608
+ super(scope, "provision-default-workspace-lambda");
7609
+ this.lambda = new import_aws_lambda_nodejs12.NodejsFunction(this, "handler", {
7610
+ entry: resolveHandlerEntry11(__dirname),
7611
+ runtime: import_aws_lambda12.Runtime.NODEJS_LATEST,
7612
+ memorySize: 1024,
7613
+ environment: {
7614
+ DYNAMO_TABLE_NAME: props.dataStoreTable.tableName
7615
+ }
7616
+ });
7617
+ props.dataStoreTable.grant(
7618
+ this.lambda,
7619
+ "dynamodb:PutItem",
7620
+ "dynamodb:UpdateItem"
7621
+ );
7622
+ this.lambda.addToRolePolicy(
7623
+ new import_aws_iam6.PolicyStatement({
7624
+ effect: import_aws_iam6.Effect.ALLOW,
7625
+ actions: ["dynamodb:Query"],
7626
+ resources: [`${props.dataStoreTable.tableArn}/index/*`]
7627
+ })
7628
+ );
7629
+ this.rule = new import_aws_events8.Rule(this, "rule", {
7630
+ eventBus: props.controlEventBus,
7631
+ eventPattern: {
7632
+ source: [USER_ONBOARDING_EVENT_SOURCE],
7633
+ detailType: [PROVISION_DEFAULT_WORKSPACE_DETAIL_TYPE]
7634
+ },
7635
+ targets: [
7636
+ new import_aws_events_targets4.LambdaFunction(this.lambda, {
7637
+ retryAttempts: 2,
7638
+ maxEventAge: import_aws_cdk_lib15.Duration.hours(2)
7639
+ })
7640
+ ]
7641
+ });
7642
+ }
7643
+ };
7644
+
7645
+ // src/workflows/control-plane/user-onboarding/user-onboarding-workflow.ts
7646
+ var import_constructs21 = require("constructs");
7647
+ var UserOnboardingWorkflow = class extends import_constructs21.Construct {
7648
+ constructor(scope, props) {
7649
+ super(scope, "user-onboarding-workflow");
7650
+ this.provisionDefaultWorkspace = new ProvisionDefaultWorkspaceLambda(this, {
7651
+ dataStoreTable: props.dataStoreTable,
7652
+ controlEventBus: props.controlEventBus
7653
+ });
7638
7654
  }
7655
+ };
7656
+
7657
+ // src/services/open-hi-auth-service.ts
7658
+ var LOCALHOST_OAUTH_CALLBACK_URLS = [
7659
+ "http://localhost:3000/oauth/callback",
7660
+ "https://localhost:3000/oauth/callback"
7661
+ ];
7662
+ var LOCALHOST_OAUTH_LOGOUT_URLS = [
7663
+ "http://localhost:3000/oauth/logout",
7664
+ "https://localhost:3000/oauth/logout"
7665
+ ];
7666
+ var _OpenHiAuthService = class _OpenHiAuthService extends OpenHiService {
7639
7667
  constructor(ohEnv, props = {}) {
7640
- super(ohEnv, _OpenHiGraphqlService.SERVICE_TYPE, props);
7668
+ super(ohEnv, _OpenHiAuthService.SERVICE_TYPE, props);
7669
+ /**
7670
+ * Cross-stack reference to the data store table. Cached so repeated
7671
+ * lookups share a single CDK construct id ("dynamo-db-data-store") in
7672
+ * this stack — a second `Table.fromTableName` call under the same scope
7673
+ * would collide.
7674
+ */
7675
+ this._dataStoreTable = null;
7676
+ this._controlEventBus = null;
7641
7677
  this.props = props;
7642
- this.rootGraphqlApi = this.createRootGraphqlApi();
7678
+ this.userPoolKmsKey = this.createUserPoolKmsKey();
7679
+ this.preTokenGenerationLambda = this.createPreTokenGenerationLambda();
7680
+ this.postAuthenticationLambda = this.createPostAuthenticationLambda();
7681
+ this.postConfirmationLambda = this.createPostConfirmationLambda();
7682
+ this.userOnboardingWorkflow = this.createUserOnboardingWorkflow();
7683
+ this.userPool = this.createUserPool();
7684
+ this.grantPreTokenGenerationPermissions();
7685
+ this.grantPostAuthenticationPermissions();
7686
+ this.grantPostConfirmationPermissions();
7687
+ this.userPoolClient = this.createUserPoolClient();
7688
+ this.userPoolDomain = this.createUserPoolDomain();
7643
7689
  }
7644
- /** Creates the root GraphQL API with Cognito user pool. */
7645
- createRootGraphqlApi() {
7646
- const userPool = OpenHiAuthService.userPoolFromConstruct(this);
7647
- return new RootGraphqlApi(this, {
7648
- authorizationConfig: {
7649
- defaultAuthorization: {
7650
- authorizationType: import_aws_appsync2.AuthorizationType.USER_POOL,
7651
- userPoolConfig: {
7652
- userPool,
7653
- defaultAction: import_aws_appsync2.UserPoolDefaultAction.ALLOW
7654
- }
7655
- }
7690
+ /**
7691
+ * Returns an IUserPool by looking up the Auth stack's User Pool ID from SSM.
7692
+ */
7693
+ static userPoolFromConstruct(scope) {
7694
+ const userPoolId = DiscoverableStringParameter.valueForLookupName(scope, {
7695
+ ssmParamName: CognitoUserPool.SSM_PARAM_NAME,
7696
+ serviceType: _OpenHiAuthService.SERVICE_TYPE
7697
+ });
7698
+ return import_aws_cognito4.UserPool.fromUserPoolId(scope, "user-pool", userPoolId);
7699
+ }
7700
+ /**
7701
+ * Returns an IUserPoolClient by looking up the Auth stack's User Pool Client ID from SSM.
7702
+ */
7703
+ static userPoolClientFromConstruct(scope) {
7704
+ const userPoolClientId = DiscoverableStringParameter.valueForLookupName(
7705
+ scope,
7706
+ {
7707
+ ssmParamName: CognitoUserPoolClient.SSM_PARAM_NAME,
7708
+ serviceType: _OpenHiAuthService.SERVICE_TYPE
7656
7709
  }
7710
+ );
7711
+ return import_aws_cognito4.UserPoolClient.fromUserPoolClientId(
7712
+ scope,
7713
+ "user-pool-client",
7714
+ userPoolClientId
7715
+ );
7716
+ }
7717
+ /**
7718
+ * Returns an IUserPoolDomain by looking up the Auth stack's User Pool Domain from SSM.
7719
+ */
7720
+ static userPoolDomainFromConstruct(scope) {
7721
+ const domainName = DiscoverableStringParameter.valueForLookupName(scope, {
7722
+ ssmParamName: CognitoUserPoolDomain.SSM_PARAM_NAME,
7723
+ serviceType: _OpenHiAuthService.SERVICE_TYPE
7657
7724
  });
7725
+ return import_aws_cognito4.UserPoolDomain.fromDomainName(scope, "user-pool-domain", domainName);
7658
7726
  }
7659
- };
7660
- _OpenHiGraphqlService.SERVICE_TYPE = "graphql-api";
7661
- var OpenHiGraphqlService = _OpenHiGraphqlService;
7662
-
7663
- // src/services/open-hi-website-service.ts
7664
- var import_config6 = __toESM(require_lib());
7665
- var import_aws_s32 = require("aws-cdk-lib/aws-s3");
7666
- var SSM_PARAM_NAME_FULL_DOMAIN = "WEBSITE_FULL_DOMAIN";
7667
- var ADMIN_DOMAIN_PREFIX = "admin";
7668
- var _OpenHiWebsiteService = class _OpenHiWebsiteService extends OpenHiService {
7669
7727
  /**
7670
- * Compose the website's full per-deploy domain. Thin wrapper over
7671
- * {@link OpenHiService.composeServiceDomain} that fills in
7672
- * {@link DEFAULT_DOMAIN_PREFIX} when `domainPrefix` is omitted.
7728
+ * Returns the full Cognito Hosted UI base URL (e.g.
7729
+ * `https://auth-abc.auth.us-east-2.amazoncognito.com`) by looking up
7730
+ * the Auth stack's User Pool Domain from SSM and composing it with the
7731
+ * calling stack's region.
7673
7732
  *
7674
- * Use from sibling stacks that need to predict the website's hostname
7675
- * before the website stack is synthesised e.g. the REST API stack
7676
- * computing its CORS `allowOrigins` for the admin-console.
7733
+ * Equivalent to `UserPoolDomain.baseUrl()` on the concrete construct,
7734
+ * but works across stacks where the looked-up `IUserPoolDomain` is an
7735
+ * `Import` and does not carry the `baseUrl()` method. Assumes the
7736
+ * domain was created as a Cognito-managed prefix domain (the only
7737
+ * variant `OpenHiAuthService.createUserPoolDomain` produces).
7677
7738
  */
7678
- static composeFullDomain(opts) {
7679
- return OpenHiService.composeServiceDomain({
7680
- ...opts,
7681
- domainPrefix: opts.domainPrefix ?? _OpenHiWebsiteService.DEFAULT_DOMAIN_PREFIX
7739
+ static userPoolDomainBaseUrlFromConstruct(scope) {
7740
+ const domainName = DiscoverableStringParameter.valueForLookupName(scope, {
7741
+ ssmParamName: CognitoUserPoolDomain.SSM_PARAM_NAME,
7742
+ serviceType: _OpenHiAuthService.SERVICE_TYPE
7682
7743
  });
7744
+ const region = import_core2.Stack.of(scope).region;
7745
+ return `https://${domainName}.auth.${region}.amazoncognito.com`;
7683
7746
  }
7684
7747
  /**
7685
- * Looks up the static-hosting bucket ARN published by the release-branch
7686
- * deploy of this service.
7748
+ * Returns an IKey (KMS) by looking up the Auth stack's User Pool KMS Key ARN from SSM.
7687
7749
  */
7688
- static bucketArnFromConstruct(scope) {
7689
- return DiscoverableStringParameter.valueForLookupName(scope, {
7690
- ssmParamName: StaticHosting.SSM_PARAM_NAME_BUCKET_ARN,
7691
- serviceType: _OpenHiWebsiteService.SERVICE_TYPE
7750
+ static userPoolKmsKeyFromConstruct(scope) {
7751
+ const keyArn = DiscoverableStringParameter.valueForLookupName(scope, {
7752
+ ssmParamName: CognitoUserPoolKmsKey.SSM_PARAM_NAME,
7753
+ serviceType: _OpenHiAuthService.SERVICE_TYPE
7692
7754
  });
7755
+ return import_aws_kms2.Key.fromKeyArn(scope, "kms-key", keyArn);
7756
+ }
7757
+ get serviceType() {
7758
+ return _OpenHiAuthService.SERVICE_TYPE;
7693
7759
  }
7694
7760
  /**
7695
- * Looks up the CloudFront distribution ARN published by the release-branch
7696
- * deploy of this service.
7761
+ * Creates the KMS key for the Cognito User Pool and exports its ARN to SSM.
7762
+ * Look up via {@link OpenHiAuthService.userPoolKmsKeyFromConstruct}.
7763
+ * Override to customize.
7697
7764
  */
7698
- static distributionArnFromConstruct(scope) {
7699
- return DiscoverableStringParameter.valueForLookupName(scope, {
7700
- ssmParamName: StaticHosting.SSM_PARAM_NAME_DISTRIBUTION_ARN,
7701
- serviceType: _OpenHiWebsiteService.SERVICE_TYPE
7765
+ createUserPoolKmsKey() {
7766
+ const key = new CognitoUserPoolKmsKey(this);
7767
+ new DiscoverableStringParameter(this, "kms-key-param", {
7768
+ ssmParamName: CognitoUserPoolKmsKey.SSM_PARAM_NAME,
7769
+ stringValue: key.keyArn,
7770
+ description: "KMS key ARN for Cognito User Pool (e.g. custom sender); cross-stack reference"
7702
7771
  });
7772
+ return key;
7703
7773
  }
7704
7774
  /**
7705
- * Looks up the CloudFront distribution domain
7706
- * (e.g. dXXXXX.cloudfront.net) published by the release-branch deploy.
7775
+ * Creates the Pre Token Generation Lambda (Cognito trigger). On every
7776
+ * sign-in and token refresh the Lambda resolves the User by Cognito `sub`
7777
+ * (GSI2) and injects `ohi_tid`, `ohi_wid`, `ohi_uid`, `ohi_uname` into
7778
+ * both the ID token and the access token (ADR 2026-03-17-01).
7707
7779
  */
7708
- static distributionDomainFromConstruct(scope) {
7709
- return DiscoverableStringParameter.valueForLookupName(scope, {
7710
- ssmParamName: StaticHosting.SSM_PARAM_NAME_DISTRIBUTION_DOMAIN,
7711
- serviceType: _OpenHiWebsiteService.SERVICE_TYPE
7780
+ createPreTokenGenerationLambda() {
7781
+ const construct = new PreTokenGenerationLambda(this, {
7782
+ dynamoTableName: this.dataStoreTable().tableName
7712
7783
  });
7784
+ return construct.lambda;
7713
7785
  }
7714
7786
  /**
7715
- * Looks up the CloudFront distribution ID published by the release-branch
7716
- * deploy of this service.
7787
+ * Creates the Post Authentication Lambda (Cognito trigger). Calls
7788
+ * AdminUserGlobalSignOut on every sign-in to enforce single-device-per-user
7789
+ * sessions per ADR 2026-03-17-01.
7717
7790
  */
7718
- static distributionIdFromConstruct(scope) {
7719
- return DiscoverableStringParameter.valueForLookupName(scope, {
7720
- ssmParamName: StaticHosting.SSM_PARAM_NAME_DISTRIBUTION_ID,
7721
- serviceType: _OpenHiWebsiteService.SERVICE_TYPE
7722
- });
7791
+ createPostAuthenticationLambda() {
7792
+ const construct = new PostAuthenticationLambda(this);
7793
+ return construct.lambda;
7723
7794
  }
7724
7795
  /**
7725
- * Looks up the website's full domain (e.g. www.example.com) published by
7726
- * the release-branch deploy of this service.
7796
+ * Creates the Post Confirmation Lambda (Cognito trigger). On sign-up
7797
+ * confirmation, publishes a control-plane workflow event; provisioning lives
7798
+ * behind EventBridge.
7727
7799
  */
7728
- static fullDomainFromConstruct(scope) {
7729
- return DiscoverableStringParameter.valueForLookupName(scope, {
7730
- ssmParamName: SSM_PARAM_NAME_FULL_DOMAIN,
7731
- serviceType: _OpenHiWebsiteService.SERVICE_TYPE
7800
+ createPostConfirmationLambda() {
7801
+ const construct = new PostConfirmationLambda(this, {
7802
+ controlEventBusName: this.controlEventBus().eventBusName
7732
7803
  });
7804
+ return construct.lambda;
7733
7805
  }
7734
- get serviceType() {
7735
- return _OpenHiWebsiteService.SERVICE_TYPE;
7806
+ createUserOnboardingWorkflow() {
7807
+ return new UserOnboardingWorkflow(this, {
7808
+ controlEventBus: this.controlEventBus(),
7809
+ dataStoreTable: this.dataStoreTable()
7810
+ });
7736
7811
  }
7737
- constructor(ohEnv, props) {
7738
- super(ohEnv, _OpenHiWebsiteService.SERVICE_TYPE, props);
7739
- this.props = props;
7740
- this.validateConfig(props);
7741
- const isReleaseBranch = this.branchName === this.defaultReleaseBranch;
7742
- const hostedZone = this.createHostedZone();
7743
- this.fullDomain = this.computeFullDomain(hostedZone);
7744
- const shouldCreateHostingInfra = props.createHostingInfrastructure ?? isReleaseBranch;
7745
- if (shouldCreateHostingInfra) {
7746
- const certificate = this.createCertificate();
7747
- this.staticHosting = this.createStaticHosting({
7748
- certificate,
7749
- hostedZone
7750
- });
7751
- this.createFullDomainParameter();
7752
- } else if (!isReleaseBranch) {
7753
- this.perBranchHostname = this.createPerBranchHostname(hostedZone);
7754
- }
7755
- if (props.createStaticContent !== false) {
7756
- const bucket = this.resolveStaticHostingBucket();
7757
- this.staticContent = this.createStaticContent(bucket);
7812
+ dataStoreTable() {
7813
+ if (this._dataStoreTable === null) {
7814
+ this._dataStoreTable = OpenHiDataService.dynamoDbDataStoreFromConstruct(this);
7758
7815
  }
7816
+ return this._dataStoreTable;
7759
7817
  }
7760
- /**
7761
- * Validates that config required for the website stack is present.
7762
- */
7763
- validateConfig(props) {
7764
- const { config } = props;
7765
- if (!config) {
7766
- throw new Error("Config is required");
7767
- }
7768
- if (!config.zoneName) {
7769
- throw new Error("Zone name is required");
7770
- }
7771
- if (!config.hostedZoneId) {
7772
- throw new Error("Hosted zone ID is required to import the website zone");
7818
+ controlEventBus() {
7819
+ if (this._controlEventBus === null) {
7820
+ this._controlEventBus = OpenHiGlobalService.controlEventBusFromConstruct(this);
7773
7821
  }
7822
+ return this._controlEventBus;
7774
7823
  }
7775
7824
  /**
7776
- * Imports the website's hosted zone from config attributes (no SSM lookup).
7777
- * The website attaches DNS records here on the release-branch deploy and
7778
- * the same zone is imported on feature-branch deploys for any sub-domain
7779
- * routing.
7825
+ * Creates the Cognito User Pool and exports its ID to SSM.
7826
+ * Look up via {@link OpenHiAuthService.userPoolFromConstruct}.
7780
7827
  * Override to customize.
7781
7828
  */
7782
- createHostedZone() {
7783
- return OpenHiGlobalService.rootHostedZoneFromConstruct(this, {
7784
- zoneName: this.config.zoneName,
7785
- hostedZoneId: this.config.hostedZoneId
7829
+ createUserPool() {
7830
+ const userPool = new CognitoUserPool(this, {
7831
+ ...this.props.userPoolProps,
7832
+ customSenderKmsKey: this.userPoolKmsKey
7833
+ });
7834
+ userPool.addTrigger(
7835
+ import_aws_cognito4.UserPoolOperation.PRE_TOKEN_GENERATION_CONFIG,
7836
+ this.preTokenGenerationLambda,
7837
+ import_aws_cognito4.LambdaVersion.V2_0
7838
+ );
7839
+ userPool.addTrigger(
7840
+ import_aws_cognito4.UserPoolOperation.POST_AUTHENTICATION,
7841
+ this.postAuthenticationLambda
7842
+ );
7843
+ userPool.addTrigger(
7844
+ import_aws_cognito4.UserPoolOperation.POST_CONFIRMATION,
7845
+ this.postConfirmationLambda
7846
+ );
7847
+ new DiscoverableStringParameter(this, "user-pool-param", {
7848
+ ssmParamName: CognitoUserPool.SSM_PARAM_NAME,
7849
+ stringValue: userPool.userPoolId,
7850
+ description: "Cognito User Pool ID for this Auth stack; cross-stack reference"
7786
7851
  });
7852
+ return userPool;
7787
7853
  }
7788
7854
  /**
7789
- * Returns the wildcard certificate looked up from the Global service.
7790
- * Override to customize.
7855
+ * Grants the Pre Token Generation Lambda read-only access on the data
7856
+ * store table and its GSIs. The Lambda only needs:
7857
+ * - `Query` on GSI2 to resolve a User by Cognito `sub`
7858
+ * - `GetItem` on the base table for direct User reads
7859
+ *
7860
+ * No write or scan access: a User missing `currentTenant`/`currentWorkspace`
7861
+ * falls into the absent-claims path; repair belongs in a separate backfill.
7791
7862
  */
7792
- createCertificate() {
7793
- return OpenHiGlobalService.rootWildcardCertificateFromConstruct(this);
7863
+ grantPreTokenGenerationPermissions() {
7864
+ const dataStoreTable = this.dataStoreTable();
7865
+ const dynamoActions = ["dynamodb:GetItem", "dynamodb:Query"];
7866
+ dataStoreTable.grant(this.preTokenGenerationLambda, ...dynamoActions);
7867
+ this.preTokenGenerationLambda.addToRolePolicy(
7868
+ new import_aws_iam7.PolicyStatement({
7869
+ effect: import_aws_iam7.Effect.ALLOW,
7870
+ actions: [...dynamoActions],
7871
+ resources: [`${dataStoreTable.tableArn}/index/*`]
7872
+ })
7873
+ );
7794
7874
  }
7795
7875
  /**
7796
- * Computes the full website domain from `domainPrefix`,
7797
- * `childZonePrefix`, and the child zone name. Release-branch deploys
7798
- * serve at `\<domainPrefix\>.\<zone\>` (e.g. `admin.dev.openhi.org`);
7799
- * every other deploy serves a per-PR preview at
7800
- * `\<domainPrefix\>-\<childZonePrefix\>.\<zone\>`
7801
- * (e.g. `admin-feat-1093-patient-migration.dev.openhi.org`).
7876
+ * Grants the Post Authentication Lambda permission to call
7877
+ * `cognito-idp:AdminUserGlobalSignOut`.
7802
7878
  *
7803
- * Delegates to {@link OpenHiWebsiteService.composeFullDomain} so the
7804
- * release-vs-feature composition stays in one place.
7879
+ * Scoped via `Stack.of(this).formatArn` rather than `userPool.userPoolArn`
7880
+ * because the User Pool registers this Lambda as a Post Authentication
7881
+ * trigger, creating the cycle:
7882
+ * userPool → lambda (trigger ARN) → role policy → userPool ARN.
7883
+ * Using `formatArn` avoids referencing the User Pool resource directly
7884
+ * while still scoping to user pools in this account+region. The Lambda
7885
+ * is invoked only by Cognito with a Cognito-provided `event.userPoolId`,
7886
+ * so the runtime target is constrained by the trigger contract.
7805
7887
  */
7806
- computeFullDomain(hostedZone) {
7807
- return _OpenHiWebsiteService.composeFullDomain({
7808
- domainPrefix: this.props.domainPrefix,
7809
- branchName: this.branchName,
7810
- defaultReleaseBranch: this.defaultReleaseBranch,
7811
- childZonePrefix: this.childZonePrefix,
7812
- zoneName: hostedZone.zoneName
7813
- });
7888
+ grantPostAuthenticationPermissions() {
7889
+ this.postAuthenticationLambda.addToRolePolicy(
7890
+ new import_aws_iam7.PolicyStatement({
7891
+ actions: ["cognito-idp:AdminUserGlobalSignOut"],
7892
+ resources: [
7893
+ import_core2.Stack.of(this).formatArn({
7894
+ service: "cognito-idp",
7895
+ resource: "userpool",
7896
+ resourceName: "*"
7897
+ })
7898
+ ]
7899
+ })
7900
+ );
7814
7901
  }
7815
7902
  /**
7816
- * Returns the sub-domain label (left of the zone) for the current
7817
- * deploy. Used for the per-branch S3 key prefix passed to
7818
- * {@link StaticContent} so the upload prefix always matches the
7819
- * served hostname.
7820
- *
7821
- * Non-release deploys compose the per-PR slug as
7822
- * `\<domainPrefix\>-\<childZonePrefix\>`, mirroring the REST API's
7823
- * `api-\<childZonePrefix\>` convention. When `domainPrefix` is `admin`
7824
- * (the only consumer today), the resulting sub-domain starts with
7825
- * {@link PER_BRANCH_PREVIEW_PREFIX}, so the per-PR S3 key prefix
7826
- * matches what `StaticHosting`'s lifecycle rule expires.
7903
+ * Grants the Post Confirmation Lambda publish-only access to the
7904
+ * control-plane event bus. Workflow Lambdas own DynamoDB writes.
7827
7905
  */
7828
- computeSubDomain() {
7829
- const isReleaseBranch = this.branchName === this.defaultReleaseBranch;
7830
- const domainPrefix = this.props.domainPrefix ?? _OpenHiWebsiteService.DEFAULT_DOMAIN_PREFIX;
7831
- if (isReleaseBranch) {
7832
- return domainPrefix;
7833
- }
7834
- return `${domainPrefix}-${this.childZonePrefix}`;
7906
+ grantPostConfirmationPermissions() {
7907
+ this.controlEventBus().grantPutEventsTo(this.postConfirmationLambda);
7835
7908
  }
7836
7909
  /**
7837
- * Creates the StaticHosting infrastructure (bucket + distribution +
7838
- * Lambda@Edge + 4 SSM params + DNS). The release-branch distribution
7839
- * adds `*.\<zone\>` as a wildcard alt-name on top of the canonical
7840
- * hostname so per-PR previews resolve via the same distribution.
7841
- *
7842
- * The bucket carries an S3 lifecycle rule that expires per-PR
7843
- * preview content (keys under {@link PER_BRANCH_PREVIEW_PREFIX})
7844
- * on non-production stages. PROD never gets the rule — see
7845
- * `enablePreviewLifecycle`.
7910
+ * Creates the User Pool Client and exports its ID to SSM (AUTH service type).
7911
+ * OAuth flows are enabled with auto-injected callback/logout URLs derived
7912
+ * from this stack's branch context (see {@link resolveOAuthRedirectUrls}).
7913
+ * Look up via {@link OpenHiAuthService.userPoolClientFromConstruct}.
7914
+ * Override to customize.
7846
7915
  */
7847
- createStaticHosting(deps) {
7848
- const restApi = this.props.restApi === true ? this.resolveRestApi() : void 0;
7849
- const wildcardSan = `*.${deps.hostedZone.zoneName}`;
7850
- return new StaticHosting(this, "static-hosting", {
7851
- serviceType: _OpenHiWebsiteService.SERVICE_TYPE,
7852
- certificate: deps.certificate,
7853
- hostedZone: deps.hostedZone,
7854
- domainNames: [this.fullDomain, wildcardSan],
7855
- description: `OpenHI website (${this.fullDomain})`,
7856
- prefixPattern: PER_BRANCH_PREVIEW_PREFIX,
7857
- enablePreviewLifecycle: this.ohEnv.ohStage.stageType !== import_config6.OPEN_HI_STAGE.PROD,
7858
- ...restApi !== void 0 && { restApi }
7916
+ createUserPoolClient() {
7917
+ const { callbackUrls, logoutUrls } = this.resolveOAuthRedirectUrls();
7918
+ const client = new CognitoUserPoolClient(this, {
7919
+ userPool: this.userPool,
7920
+ oAuth: {
7921
+ flows: {
7922
+ authorizationCodeGrant: true,
7923
+ implicitCodeGrant: true
7924
+ },
7925
+ callbackUrls,
7926
+ logoutUrls
7927
+ }
7928
+ });
7929
+ new DiscoverableStringParameter(this, "user-pool-client-param", {
7930
+ ssmParamName: CognitoUserPoolClient.SSM_PARAM_NAME,
7931
+ stringValue: client.userPoolClientId,
7932
+ description: "Cognito User Pool Client ID for this Auth stack; cross-stack reference"
7859
7933
  });
7934
+ return client;
7860
7935
  }
7861
7936
  /**
7862
- * Resolves the REST API custom-domain hostname from the rest-api stack's
7863
- * `REST_API_DOMAIN_NAME` SSM parameter. Wrapped in a private method so
7864
- * it can be overridden / stubbed in subclasses and tests.
7937
+ * Returns the OAuth `callbackUrls` and `logoutUrls` the Cognito User Pool
7938
+ * Client should accept. Composed from the same branch context the website
7939
+ * service will see at synth time:
7940
+ *
7941
+ * - `https://admin{,-<childZonePrefix>}.<zone>/oauth/{callback,logout}`
7942
+ * - `https://www{,-<childZonePrefix>}.<zone>/oauth/{callback,logout}`
7943
+ *
7944
+ * Both deployed-host pairs are auto-injected on every stage. On non-prod
7945
+ * stages the localhost dev URLs from {@link LOCALHOST_OAUTH_CALLBACK_URLS}
7946
+ * / {@link LOCALHOST_OAUTH_LOGOUT_URLS} join the merge; on prod they are
7947
+ * deliberately excluded.
7948
+ *
7949
+ * If `zoneName` is absent (no-DNS test configurations), the deployed-host
7950
+ * pairs are skipped — only the localhost set survives, and only on
7951
+ * non-prod. Override to customize.
7865
7952
  */
7866
- resolveRestApi() {
7953
+ resolveOAuthRedirectUrls() {
7954
+ const isNonProd = this.ohEnv.ohStage.stageType !== import_config7.OPEN_HI_STAGE.PROD;
7955
+ const zoneName = this.props.config?.zoneName;
7956
+ const deployedOrigins = [];
7957
+ if (zoneName !== void 0) {
7958
+ const adminHost = OpenHiWebsiteService.composeFullDomain({
7959
+ domainPrefix: ADMIN_DOMAIN_PREFIX,
7960
+ branchName: this.branchName,
7961
+ defaultReleaseBranch: this.defaultReleaseBranch,
7962
+ childZonePrefix: this.childZonePrefix,
7963
+ zoneName
7964
+ });
7965
+ const websiteHost = OpenHiWebsiteService.composeFullDomain({
7966
+ branchName: this.branchName,
7967
+ defaultReleaseBranch: this.defaultReleaseBranch,
7968
+ childZonePrefix: this.childZonePrefix,
7969
+ zoneName
7970
+ });
7971
+ deployedOrigins.push(`https://${adminHost}`, `https://${websiteHost}`);
7972
+ }
7973
+ const localhostCallbacks = isNonProd ? LOCALHOST_OAUTH_CALLBACK_URLS : [];
7974
+ const localhostLogouts = isNonProd ? LOCALHOST_OAUTH_LOGOUT_URLS : [];
7867
7975
  return {
7868
- domainName: OpenHiRestApiService.restApiDomainNameFromConstruct(this)
7976
+ callbackUrls: [
7977
+ ...deployedOrigins.map((o) => `${o}/oauth/callback`),
7978
+ ...localhostCallbacks
7979
+ ],
7980
+ logoutUrls: [
7981
+ ...deployedOrigins.map((o) => `${o}/oauth/logout`),
7982
+ ...localhostLogouts
7983
+ ]
7869
7984
  };
7870
7985
  }
7871
7986
  /**
7872
- * Creates the SSM parameter that publishes the website's full domain.
7873
- * Look up via {@link OpenHiWebsiteService.fullDomainFromConstruct}.
7987
+ * Creates the User Pool Domain (Cognito hosted UI) and exports domain name to SSM.
7988
+ * Look up via {@link OpenHiAuthService.userPoolDomainFromConstruct}.
7989
+ * Override to customize.
7874
7990
  */
7875
- createFullDomainParameter() {
7876
- new DiscoverableStringParameter(this, "full-domain-param", {
7877
- ssmParamName: SSM_PARAM_NAME_FULL_DOMAIN,
7878
- serviceType: _OpenHiWebsiteService.SERVICE_TYPE,
7879
- stringValue: this.fullDomain,
7880
- description: "Full website domain (e.g. www.example.com)"
7991
+ createUserPoolDomain() {
7992
+ const domain = new CognitoUserPoolDomain(this, {
7993
+ userPool: this.userPool,
7994
+ cognitoDomain: {
7995
+ domainPrefix: `auth-${this.branchHash}`
7996
+ }
7881
7997
  });
7882
- }
7883
- /**
7884
- * Creates the StaticContent uploader. Receives the resolved static-hosting
7885
- * bucket from the constructor on the release-branch deploy this is the
7886
- * just-created {@link staticHosting} bucket (no SSM round-trip within a
7887
- * single stack); on every other deploy it is imported from the bucket ARN
7888
- * the release-branch deploy publishes to SSM, addressed against
7889
- * {@link OpenHiService.releaseBranchHash}. See
7890
- * {@link resolveStaticHostingBucket}.
7891
- *
7892
- * The S3 key prefix is `\<sub-domain\>.\<zone\>/\<contentDest\>` so the
7893
- * upload location matches the Host-header-derived folder the Lambda@Edge
7894
- * viewer-request handler prepends. Passing the zone name (rather than
7895
- * `this.fullDomain`) for the `fullDomain` prop keeps the prefix flat —
7896
- * `admin-feat-foo.dev.openhi.org/`, not
7897
- * `admin-feat-foo.admin.dev.openhi.org/`.
7898
- */
7899
- createStaticContent(bucket) {
7900
- const { contentSourceDirectory, contentDestinationDirectory } = this.props;
7901
- return new StaticContent(this, "static-content", {
7902
- bucket,
7903
- contentSourceDirectory,
7904
- contentDestinationDirectory,
7905
- subDomain: this.computeSubDomain(),
7906
- fullDomain: this.config.zoneName
7998
+ new DiscoverableStringParameter(this, "user-pool-domain-param", {
7999
+ ssmParamName: CognitoUserPoolDomain.SSM_PARAM_NAME,
8000
+ stringValue: domain.domainName,
8001
+ description: "Cognito User Pool Domain (hosted UI) for this Auth stack; cross-stack reference"
7907
8002
  });
8003
+ return domain;
7908
8004
  }
8005
+ };
8006
+ _OpenHiAuthService.SERVICE_TYPE = "auth";
8007
+ var OpenHiAuthService = _OpenHiAuthService;
8008
+
8009
+ // src/services/open-hi-graphql-service.ts
8010
+ var import_aws_appsync2 = require("aws-cdk-lib/aws-appsync");
8011
+ var _OpenHiGraphqlService = class _OpenHiGraphqlService extends OpenHiService {
7909
8012
  /**
7910
- * Creates the per-PR `PerBranchHostname` alias record on non-release
7911
- * branch deploys. The record points
7912
- * `\<domainPrefix\>-\<childZonePrefix\>.\<zone\>` at the release-branch
7913
- * CloudFront distribution (resolved from SSM against
7914
- * {@link OpenHiService.releaseBranchHash}).
8013
+ * Returns the GraphQL API by looking up the GraphQL stack's API ID from SSM.
8014
+ * Use from other stacks to obtain an IGraphqlApi reference.
7915
8015
  */
7916
- createPerBranchHostname(hostedZone) {
7917
- return new PerBranchHostname(this, "per-branch-hostname", {
7918
- hostname: this.fullDomain,
7919
- hostedZone,
7920
- serviceType: _OpenHiWebsiteService.SERVICE_TYPE
7921
- });
8016
+ static graphqlApiFromConstruct(scope) {
8017
+ return RootGraphqlApi.fromConstruct(scope);
7922
8018
  }
7923
- /**
7924
- * Returns an {@link IBucket} pointing at the static-hosting bucket the
7925
- * uploaders write to. On the release-branch deploy this is the bucket
7926
- * just provisioned by {@link staticHosting}; on every other deploy it's
7927
- * imported from the bucket ARN the release-branch deploy publishes to
7928
- * SSM, addressed against {@link OpenHiService.releaseBranchHash}.
7929
- */
7930
- resolveStaticHostingBucket() {
7931
- if (this.staticHosting) {
7932
- return this.staticHosting.bucket;
7933
- }
7934
- const bucketArn = DiscoverableStringParameter.valueForLookupName(this, {
7935
- ssmParamName: StaticHosting.SSM_PARAM_NAME_BUCKET_ARN,
7936
- serviceType: _OpenHiWebsiteService.SERVICE_TYPE,
7937
- branchHash: this.releaseBranchHash
8019
+ get serviceType() {
8020
+ return _OpenHiGraphqlService.SERVICE_TYPE;
8021
+ }
8022
+ constructor(ohEnv, props = {}) {
8023
+ super(ohEnv, _OpenHiGraphqlService.SERVICE_TYPE, props);
8024
+ this.props = props;
8025
+ this.rootGraphqlApi = this.createRootGraphqlApi();
8026
+ }
8027
+ /** Creates the root GraphQL API with Cognito user pool. */
8028
+ createRootGraphqlApi() {
8029
+ const userPool = OpenHiAuthService.userPoolFromConstruct(this);
8030
+ return new RootGraphqlApi(this, {
8031
+ authorizationConfig: {
8032
+ defaultAuthorization: {
8033
+ authorizationType: import_aws_appsync2.AuthorizationType.USER_POOL,
8034
+ userPoolConfig: {
8035
+ userPool,
8036
+ defaultAction: import_aws_appsync2.UserPoolDefaultAction.ALLOW
8037
+ }
8038
+ }
8039
+ }
7938
8040
  });
7939
- return import_aws_s32.Bucket.fromBucketArn(this, "shared-bucket", bucketArn);
7940
8041
  }
7941
8042
  };
7942
- _OpenHiWebsiteService.SERVICE_TYPE = "website";
7943
- /**
7944
- * Default `domainPrefix` for this service when none is supplied.
7945
- * Release-branch hostname is `www.<zone>`; per-PR preview hostname is
7946
- * `www-<childZonePrefix>.<zone>`.
7947
- */
7948
- _OpenHiWebsiteService.DEFAULT_DOMAIN_PREFIX = "www";
7949
- var OpenHiWebsiteService = _OpenHiWebsiteService;
8043
+ _OpenHiGraphqlService.SERVICE_TYPE = "graphql-api";
8044
+ var OpenHiGraphqlService = _OpenHiGraphqlService;
7950
8045
 
7951
8046
  // src/workflows/control-plane/owning-delete-cascade/events.ts
7952
8047
  var import_workflows5 = __toESM(require_lib2());
@@ -8482,6 +8577,8 @@ var RenameCascadeWorkflow = class extends import_constructs25.Construct {
8482
8577
  DataStorePostgresReplica,
8483
8578
  DiscoverableStringParameter,
8484
8579
  DynamoDbDataStore,
8580
+ LOCALHOST_OAUTH_CALLBACK_URLS,
8581
+ LOCALHOST_OAUTH_LOGOUT_URLS,
8485
8582
  OPENHI_REPO_TAG_KEY_ENV_VAR,
8486
8583
  OPENHI_RESOURCE_URN_SYSTEM,
8487
8584
  OPENHI_TAG_KEY_PREFIX_ENV_VAR,