@openhi/constructs 0.0.136 → 0.0.138

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