@jaypie/constructs 1.2.17 → 1.2.19

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.
@@ -9,7 +9,7 @@ var route53Targets = require('aws-cdk-lib/aws-route53-targets');
9
9
  var secretsmanager = require('aws-cdk-lib/aws-secretsmanager');
10
10
  var datadogCdkConstructsV2 = require('datadog-cdk-constructs-v2');
11
11
  var errors = require('@jaypie/errors');
12
- var awsIam = require('aws-cdk-lib/aws-iam');
12
+ var iam = require('aws-cdk-lib/aws-iam');
13
13
  var acm = require('aws-cdk-lib/aws-certificatemanager');
14
14
  var lambda = require('aws-cdk-lib/aws-lambda');
15
15
  var logDestinations = require('aws-cdk-lib/aws-logs-destinations');
@@ -28,6 +28,8 @@ var path = require('path');
28
28
  var awsCloudtrail = require('aws-cdk-lib/aws-cloudtrail');
29
29
  var awsSso = require('aws-cdk-lib/aws-sso');
30
30
  var awsSam = require('aws-cdk-lib/aws-sam');
31
+ var apigatewayv2 = require('aws-cdk-lib/aws-apigatewayv2');
32
+ var apigatewayv2Integrations = require('aws-cdk-lib/aws-apigatewayv2-integrations');
31
33
 
32
34
  function _interopNamespaceDefault(e) {
33
35
  var n = Object.create(null);
@@ -52,6 +54,7 @@ var apiGateway__namespace = /*#__PURE__*/_interopNamespaceDefault(apiGateway);
52
54
  var route53__namespace = /*#__PURE__*/_interopNamespaceDefault(route53);
53
55
  var route53Targets__namespace = /*#__PURE__*/_interopNamespaceDefault(route53Targets);
54
56
  var secretsmanager__namespace = /*#__PURE__*/_interopNamespaceDefault(secretsmanager);
57
+ var iam__namespace = /*#__PURE__*/_interopNamespaceDefault(iam);
55
58
  var acm__namespace = /*#__PURE__*/_interopNamespaceDefault(acm);
56
59
  var lambda__namespace = /*#__PURE__*/_interopNamespaceDefault(lambda);
57
60
  var logDestinations__namespace = /*#__PURE__*/_interopNamespaceDefault(logDestinations);
@@ -63,6 +66,8 @@ var cloudfront__namespace = /*#__PURE__*/_interopNamespaceDefault(cloudfront);
63
66
  var origins__namespace = /*#__PURE__*/_interopNamespaceDefault(origins);
64
67
  var dynamodb__namespace = /*#__PURE__*/_interopNamespaceDefault(dynamodb);
65
68
  var path__namespace = /*#__PURE__*/_interopNamespaceDefault(path);
69
+ var apigatewayv2__namespace = /*#__PURE__*/_interopNamespaceDefault(apigatewayv2);
70
+ var apigatewayv2Integrations__namespace = /*#__PURE__*/_interopNamespaceDefault(apigatewayv2Integrations);
66
71
 
67
72
  const CDK$2 = {
68
73
  ACCOUNT: {
@@ -434,22 +439,22 @@ function extendDatadogRole(scope, options) {
434
439
  }
435
440
  const { id = "DatadogCustomPolicy", project, service = CDK$2.SERVICE.DATADOG, } = options || {};
436
441
  // Lookup the Datadog role
437
- const datadogRole = awsIam.Role.fromRoleArn(scope, "DatadogRole", datadogRoleArn);
442
+ const datadogRole = iam.Role.fromRoleArn(scope, "DatadogRole", datadogRoleArn);
438
443
  // Build policy statements
439
444
  const statements = [
440
445
  // Allow view budget
441
- new awsIam.PolicyStatement({
446
+ new iam.PolicyStatement({
442
447
  actions: ["budgets:ViewBudget"],
443
448
  resources: ["*"],
444
449
  }),
445
450
  // Allow describe log groups
446
- new awsIam.PolicyStatement({
451
+ new iam.PolicyStatement({
447
452
  actions: ["logs:DescribeLogGroups"],
448
453
  resources: ["*"],
449
454
  }),
450
455
  ];
451
456
  // Create the custom policy
452
- const datadogCustomPolicy = new awsIam.Policy(scope, id, {
457
+ const datadogCustomPolicy = new iam.Policy(scope, id, {
453
458
  roles: [datadogRole],
454
459
  statements,
455
460
  });
@@ -2225,22 +2230,22 @@ class JaypieDatadogBucket extends constructs.Construct {
2225
2230
  }
2226
2231
  const { project, service = CDK$2.SERVICE.DATADOG } = options || {};
2227
2232
  // Lookup the Datadog role
2228
- const datadogRole = awsIam.Role.fromRoleArn(this, "DatadogRole", datadogRoleArn);
2233
+ const datadogRole = iam.Role.fromRoleArn(this, "DatadogRole", datadogRoleArn);
2229
2234
  // Build policy statements for bucket access
2230
2235
  const statements = [
2231
2236
  // Allow list bucket
2232
- new awsIam.PolicyStatement({
2237
+ new iam.PolicyStatement({
2233
2238
  actions: ["s3:ListBucket"],
2234
2239
  resources: [this.bucket.bucketArn],
2235
2240
  }),
2236
2241
  // Allow read and write to the bucket
2237
- new awsIam.PolicyStatement({
2242
+ new iam.PolicyStatement({
2238
2243
  actions: ["s3:GetObject", "s3:PutObject"],
2239
2244
  resources: [`${this.bucket.bucketArn}/*`],
2240
2245
  }),
2241
2246
  ];
2242
2247
  // Create the custom policy
2243
- const datadogBucketPolicy = new awsIam.Policy(this, "DatadogBucketPolicy", {
2248
+ const datadogBucketPolicy = new iam.Policy(this, "DatadogBucketPolicy", {
2244
2249
  roles: [datadogRole],
2245
2250
  statements,
2246
2251
  });
@@ -3009,8 +3014,8 @@ class JaypieGitHubDeployRole extends constructs.Construct {
3009
3014
  repoRestriction = `repo:${organization}/*:*`;
3010
3015
  }
3011
3016
  // Create the IAM role
3012
- this._role = new awsIam.Role(this, "GitHubActionsRole", {
3013
- assumedBy: new awsIam.FederatedPrincipal(oidcProviderArn, {
3017
+ this._role = new iam.Role(this, "GitHubActionsRole", {
3018
+ assumedBy: new iam.FederatedPrincipal(oidcProviderArn, {
3014
3019
  StringLike: {
3015
3020
  "token.actions.githubusercontent.com:sub": repoRestriction,
3016
3021
  },
@@ -3020,12 +3025,12 @@ class JaypieGitHubDeployRole extends constructs.Construct {
3020
3025
  });
3021
3026
  cdk.Tags.of(this._role).add(CDK$2.TAG.ROLE, CDK$2.ROLE.DEPLOY);
3022
3027
  // Allow the role to access the GitHub OIDC provider
3023
- this._role.addToPolicy(new awsIam.PolicyStatement({
3028
+ this._role.addToPolicy(new iam.PolicyStatement({
3024
3029
  actions: ["sts:AssumeRoleWithWebIdentity"],
3025
3030
  resources: [`arn:aws:iam::${accountId}:oidc-provider/*`],
3026
3031
  }));
3027
3032
  // Allow the role to deploy CDK apps
3028
- this._role.addToPolicy(new awsIam.PolicyStatement({
3033
+ this._role.addToPolicy(new iam.PolicyStatement({
3029
3034
  actions: [
3030
3035
  "cloudformation:CreateStack",
3031
3036
  "cloudformation:DeleteStack",
@@ -3042,12 +3047,12 @@ class JaypieGitHubDeployRole extends constructs.Construct {
3042
3047
  "s3:GetObject",
3043
3048
  "s3:ListBucket",
3044
3049
  ],
3045
- effect: awsIam.Effect.ALLOW,
3050
+ effect: iam.Effect.ALLOW,
3046
3051
  resources: ["*"],
3047
3052
  }));
3048
- this._role.addToPolicy(new awsIam.PolicyStatement({
3053
+ this._role.addToPolicy(new iam.PolicyStatement({
3049
3054
  actions: ["iam:PassRole", "sts:AssumeRole"],
3050
- effect: awsIam.Effect.ALLOW,
3055
+ effect: iam.Effect.ALLOW,
3051
3056
  resources: [
3052
3057
  "arn:aws:iam::*:role/cdk-hnb659fds-deploy-role-*",
3053
3058
  "arn:aws:iam::*:role/cdk-hnb659fds-file-publishing-*",
@@ -3142,7 +3147,7 @@ class JaypieHostedZone extends constructs.Construct {
3142
3147
  cdk__namespace.Tags.of(this.logGroup).add(CDK$2.TAG.PROJECT, project);
3143
3148
  }
3144
3149
  // Grant Route 53 permissions to write to the log group
3145
- this.logGroup.grantWrite(new awsIam.ServicePrincipal(SERVICE.ROUTE53));
3150
+ this.logGroup.grantWrite(new iam.ServicePrincipal(SERVICE.ROUTE53));
3146
3151
  // Add destination based on configuration
3147
3152
  if (destination !== false) {
3148
3153
  const lambdaDestination = destination === true
@@ -3217,13 +3222,19 @@ class JaypieMongoDbSecret extends JaypieEnvSecret {
3217
3222
  class JaypieNextJs extends constructs.Construct {
3218
3223
  constructor(scope, id, props) {
3219
3224
  super(scope, id);
3220
- const domainName = typeof props?.domainName === "string"
3221
- ? props.domainName
3222
- : envHostname(props?.domainName);
3225
+ // Determine if we should use a custom domain
3226
+ const useDomain = props?.domainProps !== false;
3227
+ // Resolve domain name only if using a custom domain
3228
+ const domainName = useDomain
3229
+ ? typeof props?.domainName === "string"
3230
+ ? props.domainName
3231
+ : envHostname(props?.domainName)
3232
+ : undefined;
3223
3233
  this.domainName = domainName;
3224
- const domainNameSanitized = domainName
3225
- .replace(/\./g, "-")
3226
- .replace(/[^a-zA-Z0-9]/g, "_");
3234
+ // Use domain name or construct ID for cache policy naming
3235
+ const cachePolicyIdentifier = domainName
3236
+ ? domainName.replace(/\./g, "-").replace(/[^a-zA-Z0-9]/g, "_")
3237
+ : id.replace(/[^a-zA-Z0-9]/g, "_");
3227
3238
  // Resolve environment from array or object syntax
3228
3239
  const environment = resolveEnvironment(props?.environment);
3229
3240
  const envSecrets = props?.envSecrets || {};
@@ -3260,27 +3271,32 @@ class JaypieNextJs extends constructs.Construct {
3260
3271
  }, {});
3261
3272
  const nextjs = new cdkNextjsStandalone.Nextjs(this, "NextJsApp", {
3262
3273
  nextjsPath,
3263
- domainProps: {
3264
- domainName,
3265
- hostedZone: resolveHostedZone(this, {
3266
- zone: props?.hostedZone,
3267
- }),
3268
- },
3274
+ // Only configure custom domain if useDomain is true
3275
+ ...(useDomain &&
3276
+ domainName && {
3277
+ domainProps: {
3278
+ domainName,
3279
+ hostedZone: resolveHostedZone(this, {
3280
+ zone: props?.hostedZone,
3281
+ }),
3282
+ },
3283
+ }),
3269
3284
  environment: {
3270
3285
  ...jaypieLambdaEnv(),
3271
3286
  ...environment,
3272
3287
  ...secretsEnvironment,
3273
3288
  ...jaypieSecretsEnvironment,
3274
3289
  ...nextPublicEnv,
3275
- NEXT_PUBLIC_SITE_URL: `https://${domainName}`,
3290
+ // NEXT_PUBLIC_SITE_URL will be set after construct creation for CloudFront URL
3291
+ ...(domainName && { NEXT_PUBLIC_SITE_URL: `https://${domainName}` }),
3276
3292
  },
3277
3293
  overrides: {
3278
3294
  nextjsDistribution: {
3279
3295
  imageCachePolicyProps: {
3280
- cachePolicyName: `NextJsImageCachePolicy-${domainNameSanitized}`,
3296
+ cachePolicyName: `NextJsImageCachePolicy-${cachePolicyIdentifier}`,
3281
3297
  },
3282
3298
  serverCachePolicyProps: {
3283
- cachePolicyName: `NextJsServerCachePolicy-${domainNameSanitized}`,
3299
+ cachePolicyName: `NextJsServerCachePolicy-${cachePolicyIdentifier}`,
3284
3300
  },
3285
3301
  },
3286
3302
  nextjsImage: {
@@ -3295,6 +3311,10 @@ class JaypieNextJs extends constructs.Construct {
3295
3311
  },
3296
3312
  },
3297
3313
  });
3314
+ // Set NEXT_PUBLIC_SITE_URL to CloudFront URL when no custom domain
3315
+ if (!domainName) {
3316
+ nextjs.serverFunction.lambdaFunction.addEnvironment("NEXT_PUBLIC_SITE_URL", `https://${nextjs.distribution.distributionDomain}`);
3317
+ }
3298
3318
  addDatadogLayers(nextjs.imageOptimizationFunction);
3299
3319
  addDatadogLayers(nextjs.serverFunction.lambdaFunction);
3300
3320
  // Grant secret read permissions
@@ -3425,21 +3445,21 @@ class JaypieOrganizationTrail extends constructs.Construct {
3425
3445
  ],
3426
3446
  });
3427
3447
  // Add CloudTrail bucket policies
3428
- this.bucket.addToResourcePolicy(new awsIam.PolicyStatement({
3448
+ this.bucket.addToResourcePolicy(new iam.PolicyStatement({
3429
3449
  actions: ["s3:GetBucketAcl"],
3430
- effect: awsIam.Effect.ALLOW,
3431
- principals: [new awsIam.ServicePrincipal("cloudtrail.amazonaws.com")],
3450
+ effect: iam.Effect.ALLOW,
3451
+ principals: [new iam.ServicePrincipal("cloudtrail.amazonaws.com")],
3432
3452
  resources: [this.bucket.bucketArn],
3433
3453
  }));
3434
- this.bucket.addToResourcePolicy(new awsIam.PolicyStatement({
3454
+ this.bucket.addToResourcePolicy(new iam.PolicyStatement({
3435
3455
  actions: ["s3:PutObject"],
3436
3456
  conditions: {
3437
3457
  StringEquals: {
3438
3458
  "s3:x-amz-acl": "bucket-owner-full-control",
3439
3459
  },
3440
3460
  },
3441
- effect: awsIam.Effect.ALLOW,
3442
- principals: [new awsIam.ServicePrincipal("cloudtrail.amazonaws.com")],
3461
+ effect: iam.Effect.ALLOW,
3462
+ principals: [new iam.ServicePrincipal("cloudtrail.amazonaws.com")],
3443
3463
  resources: [`${this.bucket.bucketArn}/*`],
3444
3464
  }));
3445
3465
  // Add tags to bucket
@@ -3532,9 +3552,9 @@ class JaypieSsoPermissions extends constructs.Construct {
3532
3552
  ],
3533
3553
  },
3534
3554
  managedPolicies: [
3535
- awsIam.ManagedPolicy.fromAwsManagedPolicyName("AdministratorAccess")
3555
+ iam.ManagedPolicy.fromAwsManagedPolicyName("AdministratorAccess")
3536
3556
  .managedPolicyArn,
3537
- awsIam.ManagedPolicy.fromAwsManagedPolicyName("AWSManagementConsoleBasicUserAccess").managedPolicyArn,
3557
+ iam.ManagedPolicy.fromAwsManagedPolicyName("AWSManagementConsoleBasicUserAccess").managedPolicyArn,
3538
3558
  ],
3539
3559
  sessionDuration: cdk.Duration.hours(1).toIsoString(),
3540
3560
  tags: [
@@ -3613,10 +3633,10 @@ class JaypieSsoPermissions extends constructs.Construct {
3613
3633
  ],
3614
3634
  },
3615
3635
  managedPolicies: [
3616
- awsIam.ManagedPolicy.fromAwsManagedPolicyName("AmazonQDeveloperAccess")
3636
+ iam.ManagedPolicy.fromAwsManagedPolicyName("AmazonQDeveloperAccess")
3617
3637
  .managedPolicyArn,
3618
- awsIam.ManagedPolicy.fromAwsManagedPolicyName("AWSManagementConsoleBasicUserAccess").managedPolicyArn,
3619
- awsIam.ManagedPolicy.fromAwsManagedPolicyName("ReadOnlyAccess")
3638
+ iam.ManagedPolicy.fromAwsManagedPolicyName("AWSManagementConsoleBasicUserAccess").managedPolicyArn,
3639
+ iam.ManagedPolicy.fromAwsManagedPolicyName("ReadOnlyAccess")
3620
3640
  .managedPolicyArn,
3621
3641
  ],
3622
3642
  sessionDuration: cdk.Duration.hours(12).toIsoString(),
@@ -3671,12 +3691,12 @@ class JaypieSsoPermissions extends constructs.Construct {
3671
3691
  ],
3672
3692
  },
3673
3693
  managedPolicies: [
3674
- awsIam.ManagedPolicy.fromAwsManagedPolicyName("AmazonQDeveloperAccess")
3694
+ iam.ManagedPolicy.fromAwsManagedPolicyName("AmazonQDeveloperAccess")
3675
3695
  .managedPolicyArn,
3676
- awsIam.ManagedPolicy.fromAwsManagedPolicyName("AWSManagementConsoleBasicUserAccess").managedPolicyArn,
3677
- awsIam.ManagedPolicy.fromAwsManagedPolicyName("ReadOnlyAccess")
3696
+ iam.ManagedPolicy.fromAwsManagedPolicyName("AWSManagementConsoleBasicUserAccess").managedPolicyArn,
3697
+ iam.ManagedPolicy.fromAwsManagedPolicyName("ReadOnlyAccess")
3678
3698
  .managedPolicyArn,
3679
- awsIam.ManagedPolicy.fromAwsManagedPolicyName("job-function/SystemAdministrator").managedPolicyArn,
3699
+ iam.ManagedPolicy.fromAwsManagedPolicyName("job-function/SystemAdministrator").managedPolicyArn,
3680
3700
  ],
3681
3701
  sessionDuration: cdk.Duration.hours(4).toIsoString(),
3682
3702
  tags: [
@@ -3889,8 +3909,8 @@ class JaypieWebDeploymentBucket extends constructs.Construct {
3889
3909
  repo = `repo:${process.env.CDK_ENV_REPO}:*`;
3890
3910
  }
3891
3911
  if (repo) {
3892
- const bucketDeployRole = new awsIam.Role(this, "DestinationBucketDeployRole", {
3893
- assumedBy: new awsIam.FederatedPrincipal(cdk.Fn.importValue(CDK$2.IMPORT.OIDC_PROVIDER), {
3912
+ const bucketDeployRole = new iam.Role(this, "DestinationBucketDeployRole", {
3913
+ assumedBy: new iam.FederatedPrincipal(cdk.Fn.importValue(CDK$2.IMPORT.OIDC_PROVIDER), {
3894
3914
  StringLike: {
3895
3915
  "token.actions.githubusercontent.com:sub": repo,
3896
3916
  },
@@ -3899,8 +3919,8 @@ class JaypieWebDeploymentBucket extends constructs.Construct {
3899
3919
  });
3900
3920
  cdk.Tags.of(bucketDeployRole).add(CDK$2.TAG.ROLE, CDK$2.ROLE.DEPLOY);
3901
3921
  // Allow the role to write to the bucket
3902
- bucketDeployRole.addToPolicy(new awsIam.PolicyStatement({
3903
- effect: awsIam.Effect.ALLOW,
3922
+ bucketDeployRole.addToPolicy(new iam.PolicyStatement({
3923
+ effect: iam.Effect.ALLOW,
3904
3924
  actions: [
3905
3925
  "s3:DeleteObject",
3906
3926
  "s3:GetObject",
@@ -3909,16 +3929,16 @@ class JaypieWebDeploymentBucket extends constructs.Construct {
3909
3929
  ],
3910
3930
  resources: [`${this.bucket.bucketArn}/*`],
3911
3931
  }));
3912
- bucketDeployRole.addToPolicy(new awsIam.PolicyStatement({
3913
- effect: awsIam.Effect.ALLOW,
3932
+ bucketDeployRole.addToPolicy(new iam.PolicyStatement({
3933
+ effect: iam.Effect.ALLOW,
3914
3934
  actions: ["s3:ListBucket"],
3915
3935
  resources: [this.bucket.bucketArn],
3916
3936
  }));
3917
3937
  // Allow the role to describe the current stack
3918
3938
  const stack = cdk.Stack.of(this);
3919
- bucketDeployRole.addToPolicy(new awsIam.PolicyStatement({
3939
+ bucketDeployRole.addToPolicy(new iam.PolicyStatement({
3920
3940
  actions: ["cloudformation:DescribeStacks"],
3921
- effect: awsIam.Effect.ALLOW,
3941
+ effect: iam.Effect.ALLOW,
3922
3942
  resources: [
3923
3943
  `arn:aws:cloudformation:${stack.region}:${stack.account}:stack/${stack.stackName}/*`,
3924
3944
  ],
@@ -4135,6 +4155,377 @@ class JaypieTraceSigningKeySecret extends JaypieEnvSecret {
4135
4155
  }
4136
4156
  }
4137
4157
 
4158
+ //
4159
+ //
4160
+ // Main
4161
+ //
4162
+ class JaypieWebSocket extends constructs.Construct {
4163
+ constructor(scope, id, props = {}) {
4164
+ super(scope, id);
4165
+ const { certificate = true, connect, default: defaultHandler, disconnect, handler, host: propsHost, logRetention = logs__namespace.RetentionDays.THREE_MONTHS, name, roleTag = CDK$2.ROLE.API, routes = {}, stageName = "production", zone: propsZone, } = props;
4166
+ // Validate: either handler OR individual handlers, not both
4167
+ const hasIndividualHandlers = connect || disconnect || defaultHandler;
4168
+ if (handler && hasIndividualHandlers) {
4169
+ throw new Error("Cannot specify both 'handler' and individual route handlers (connect/disconnect/default)");
4170
+ }
4171
+ // Determine zone from props or environment
4172
+ let zone = propsZone;
4173
+ if (!zone && process.env.CDK_ENV_HOSTED_ZONE) {
4174
+ zone = process.env.CDK_ENV_HOSTED_ZONE;
4175
+ }
4176
+ // Determine host from props or environment
4177
+ let host;
4178
+ if (typeof propsHost === "string") {
4179
+ host = propsHost;
4180
+ }
4181
+ else if (typeof propsHost === "object") {
4182
+ // Resolve host from HostConfig using envHostname()
4183
+ host = envHostname(propsHost);
4184
+ }
4185
+ else if (process.env.CDK_ENV_WS_HOST_NAME) {
4186
+ host = process.env.CDK_ENV_WS_HOST_NAME;
4187
+ }
4188
+ else if (process.env.CDK_ENV_WS_SUBDOMAIN &&
4189
+ process.env.CDK_ENV_HOSTED_ZONE) {
4190
+ host = mergeDomain(process.env.CDK_ENV_WS_SUBDOMAIN, process.env.CDK_ENV_HOSTED_ZONE);
4191
+ }
4192
+ const apiName = name || constructEnvName("WebSocket");
4193
+ // Create WebSocket API
4194
+ this._api = new apigatewayv2__namespace.WebSocketApi(this, "Api", {
4195
+ apiName,
4196
+ });
4197
+ cdk.Tags.of(this._api).add(CDK$2.TAG.ROLE, roleTag);
4198
+ // Add routes with Lambda integrations
4199
+ const connectHandler = handler || connect;
4200
+ const disconnectHandler = handler || disconnect;
4201
+ const defaultRouteHandler = handler || defaultHandler;
4202
+ if (connectHandler) {
4203
+ this._api.addRoute("$connect", {
4204
+ integration: new apigatewayv2Integrations__namespace.WebSocketLambdaIntegration("ConnectIntegration", connectHandler),
4205
+ });
4206
+ }
4207
+ if (disconnectHandler) {
4208
+ this._api.addRoute("$disconnect", {
4209
+ integration: new apigatewayv2Integrations__namespace.WebSocketLambdaIntegration("DisconnectIntegration", disconnectHandler),
4210
+ });
4211
+ }
4212
+ if (defaultRouteHandler) {
4213
+ this._api.addRoute("$default", {
4214
+ integration: new apigatewayv2Integrations__namespace.WebSocketLambdaIntegration("DefaultIntegration", defaultRouteHandler),
4215
+ });
4216
+ }
4217
+ // Add custom routes
4218
+ for (const [routeKey, routeHandler] of Object.entries(routes)) {
4219
+ this._api.addRoute(routeKey, {
4220
+ integration: new apigatewayv2Integrations__namespace.WebSocketLambdaIntegration(`${routeKey}Integration`, routeHandler),
4221
+ });
4222
+ }
4223
+ // Create log group for access logs
4224
+ // Note: logGroup is created for future use when API Gateway v2 WebSocket
4225
+ // access logging is fully supported in CDK
4226
+ new logs__namespace.LogGroup(this, "AccessLogs", {
4227
+ removalPolicy: cdk.RemovalPolicy.DESTROY,
4228
+ retention: logRetention,
4229
+ });
4230
+ // Create stage
4231
+ this._stage = new apigatewayv2__namespace.WebSocketStage(this, "Stage", {
4232
+ autoDeploy: true,
4233
+ stageName,
4234
+ webSocketApi: this._api,
4235
+ });
4236
+ cdk.Tags.of(this._stage).add(CDK$2.TAG.ROLE, roleTag);
4237
+ // Set up custom domain if host and zone are provided
4238
+ let hostedZone;
4239
+ let certificateToUse;
4240
+ if (host && zone) {
4241
+ hostedZone = resolveHostedZone(this, { zone });
4242
+ // Use resolveCertificate to create certificate at stack level (enables reuse)
4243
+ certificateToUse = resolveCertificate(this, {
4244
+ certificate,
4245
+ domainName: host,
4246
+ roleTag: CDK$2.ROLE.HOSTING,
4247
+ zone: hostedZone,
4248
+ });
4249
+ this._certificate = certificateToUse;
4250
+ this._host = host;
4251
+ if (certificateToUse) {
4252
+ // Create custom domain
4253
+ this._domainName = new apigatewayv2__namespace.DomainName(this, "DomainName", {
4254
+ certificate: certificateToUse,
4255
+ domainName: host,
4256
+ });
4257
+ cdk.Tags.of(this._domainName).add(CDK$2.TAG.ROLE, roleTag);
4258
+ // Map domain to stage
4259
+ new apigatewayv2__namespace.ApiMapping(this, "ApiMapping", {
4260
+ api: this._api,
4261
+ domainName: this._domainName,
4262
+ stage: this._stage,
4263
+ });
4264
+ // Create DNS record
4265
+ new route53__namespace.ARecord(this, "AliasRecord", {
4266
+ recordName: host,
4267
+ target: route53__namespace.RecordTarget.fromAlias(new route53Targets__namespace.ApiGatewayv2DomainProperties(this._domainName.regionalDomainName, this._domainName.regionalHostedZoneId)),
4268
+ zone: hostedZone,
4269
+ });
4270
+ // Also create AAAA record for IPv6
4271
+ new route53__namespace.AaaaRecord(this, "AaaaAliasRecord", {
4272
+ recordName: host,
4273
+ target: route53__namespace.RecordTarget.fromAlias(new route53Targets__namespace.ApiGatewayv2DomainProperties(this._domainName.regionalDomainName, this._domainName.regionalHostedZoneId)),
4274
+ zone: hostedZone,
4275
+ });
4276
+ }
4277
+ }
4278
+ // Grant all handlers permission to manage connections
4279
+ const allHandlers = new Set();
4280
+ if (connectHandler)
4281
+ allHandlers.add(connectHandler);
4282
+ if (disconnectHandler)
4283
+ allHandlers.add(disconnectHandler);
4284
+ if (defaultRouteHandler)
4285
+ allHandlers.add(defaultRouteHandler);
4286
+ Object.values(routes).forEach((h) => allHandlers.add(h));
4287
+ for (const lambdaHandler of allHandlers) {
4288
+ this.grantManageConnections(lambdaHandler);
4289
+ }
4290
+ }
4291
+ //
4292
+ //
4293
+ // Public accessors
4294
+ //
4295
+ get api() {
4296
+ return this._api;
4297
+ }
4298
+ get apiId() {
4299
+ return this._api.apiId;
4300
+ }
4301
+ get certificate() {
4302
+ return this._certificate;
4303
+ }
4304
+ get domainName() {
4305
+ return this._domainName?.name;
4306
+ }
4307
+ /**
4308
+ * The WebSocket endpoint URL.
4309
+ * Uses custom domain if configured, otherwise returns the default stage URL.
4310
+ */
4311
+ get endpoint() {
4312
+ if (this._host) {
4313
+ return `wss://${this._host}`;
4314
+ }
4315
+ return this._stage.url;
4316
+ }
4317
+ get host() {
4318
+ return this._host;
4319
+ }
4320
+ get stage() {
4321
+ return this._stage;
4322
+ }
4323
+ /**
4324
+ * The callback URL for API Gateway Management API.
4325
+ * Use this URL to send messages to connected clients.
4326
+ */
4327
+ get callbackUrl() {
4328
+ if (this._host) {
4329
+ return `https://${this._host}`;
4330
+ }
4331
+ // Extract callback URL from stage URL
4332
+ // Stage URL: wss://abc123.execute-api.us-east-1.amazonaws.com/production
4333
+ // Callback URL: https://abc123.execute-api.us-east-1.amazonaws.com/production
4334
+ return this._stage.url.replace("wss://", "https://");
4335
+ }
4336
+ //
4337
+ //
4338
+ // Public methods
4339
+ //
4340
+ /**
4341
+ * Grant a Lambda function permission to manage WebSocket connections
4342
+ * (post to connections, delete connections).
4343
+ */
4344
+ grantManageConnections(grantee) {
4345
+ return iam__namespace.Grant.addToPrincipal({
4346
+ actions: ["execute-api:ManageConnections"],
4347
+ grantee: grantee.grantPrincipal,
4348
+ resourceArns: [
4349
+ cdk.Stack.of(this).formatArn({
4350
+ arnFormat: cdk.ArnFormat.SLASH_RESOURCE_SLASH_RESOURCE_NAME,
4351
+ resource: this._api.apiId,
4352
+ resourceName: `${this._stage.stageName}/*`,
4353
+ service: "execute-api",
4354
+ }),
4355
+ ],
4356
+ });
4357
+ }
4358
+ }
4359
+
4360
+ /**
4361
+ * JaypieWebSocketLambda - A Lambda function optimized for WebSocket handlers.
4362
+ *
4363
+ * Provides sensible defaults for WebSocket event handling:
4364
+ * - 30 second timeout (same as API handlers)
4365
+ * - API role tag
4366
+ *
4367
+ * @example
4368
+ * ```typescript
4369
+ * const handler = new JaypieWebSocketLambda(this, "ChatHandler", {
4370
+ * code: "dist/handlers",
4371
+ * handler: "chat.handler",
4372
+ * secrets: ["MONGODB_URI"],
4373
+ * });
4374
+ *
4375
+ * new JaypieWebSocket(this, "Chat", {
4376
+ * host: "ws.example.com",
4377
+ * handler,
4378
+ * });
4379
+ * ```
4380
+ */
4381
+ class JaypieWebSocketLambda extends JaypieLambda {
4382
+ constructor(scope, id, props) {
4383
+ super(scope, id, {
4384
+ roleTag: CDK$2.ROLE.API,
4385
+ timeout: cdk.Duration.seconds(CDK$2.DURATION.EXPRESS_API),
4386
+ ...props,
4387
+ });
4388
+ }
4389
+ }
4390
+
4391
+ //
4392
+ //
4393
+ // Main
4394
+ //
4395
+ /**
4396
+ * JaypieWebSocketTable - DynamoDB table for storing WebSocket connection IDs.
4397
+ *
4398
+ * Provides a simple table structure for tracking active WebSocket connections:
4399
+ * - Partition key: connectionId (String)
4400
+ * - TTL attribute: expiresAt (for automatic cleanup)
4401
+ * - Optional GSI: userId-index (for looking up connections by user)
4402
+ *
4403
+ * @example
4404
+ * ```typescript
4405
+ * const connectionTable = new JaypieWebSocketTable(this, "Connections");
4406
+ *
4407
+ * const ws = new JaypieWebSocket(this, "Chat", {
4408
+ * host: "ws.example.com",
4409
+ * handler: chatHandler,
4410
+ * });
4411
+ *
4412
+ * // Grant Lambda access to the table
4413
+ * connectionTable.grantReadWriteData(chatHandler);
4414
+ *
4415
+ * // Pass table name to Lambda
4416
+ * chatHandler.addEnvironment("CONNECTION_TABLE", connectionTable.tableName);
4417
+ * ```
4418
+ *
4419
+ * @example
4420
+ * // With user index for looking up all connections for a user
4421
+ * const connectionTable = new JaypieWebSocketTable(this, "Connections", {
4422
+ * userIndex: true,
4423
+ * ttl: Duration.hours(12),
4424
+ * });
4425
+ */
4426
+ class JaypieWebSocketTable extends constructs.Construct {
4427
+ constructor(scope, id, props = {}) {
4428
+ super(scope, id);
4429
+ const { roleTag = CDK$2.ROLE.STORAGE, tableName, ttl = cdk.Duration.hours(24), userIndex = false, } = props;
4430
+ this._ttlDuration = ttl;
4431
+ // Build global secondary indexes
4432
+ const globalSecondaryIndexes = [];
4433
+ if (userIndex) {
4434
+ globalSecondaryIndexes.push({
4435
+ indexName: "userId-index",
4436
+ partitionKey: { name: "userId", type: dynamodb__namespace.AttributeType.STRING },
4437
+ sortKey: { name: "connectedAt", type: dynamodb__namespace.AttributeType.STRING },
4438
+ });
4439
+ }
4440
+ // Create the table
4441
+ this._table = new dynamodb__namespace.TableV2(this, "Table", {
4442
+ billing: dynamodb__namespace.Billing.onDemand(),
4443
+ globalSecondaryIndexes: globalSecondaryIndexes.length > 0 ? globalSecondaryIndexes : undefined,
4444
+ partitionKey: {
4445
+ name: "connectionId",
4446
+ type: dynamodb__namespace.AttributeType.STRING,
4447
+ },
4448
+ removalPolicy: cdk.RemovalPolicy.DESTROY,
4449
+ tableName: tableName || constructEnvName("WebSocketConnections"),
4450
+ timeToLiveAttribute: "expiresAt",
4451
+ });
4452
+ cdk.Tags.of(this._table).add(CDK$2.TAG.ROLE, roleTag);
4453
+ }
4454
+ //
4455
+ //
4456
+ // Public accessors
4457
+ //
4458
+ /**
4459
+ * The underlying DynamoDB TableV2 construct.
4460
+ */
4461
+ get table() {
4462
+ return this._table;
4463
+ }
4464
+ /**
4465
+ * The name of the DynamoDB table.
4466
+ */
4467
+ get tableName() {
4468
+ return this._table.tableName;
4469
+ }
4470
+ /**
4471
+ * The ARN of the DynamoDB table.
4472
+ */
4473
+ get tableArn() {
4474
+ return this._table.tableArn;
4475
+ }
4476
+ /**
4477
+ * TTL duration for connections in seconds.
4478
+ * Use this to calculate expiresAt when storing connections.
4479
+ */
4480
+ get ttlSeconds() {
4481
+ return this._ttlDuration.toSeconds();
4482
+ }
4483
+ //
4484
+ //
4485
+ // Grant methods
4486
+ //
4487
+ /**
4488
+ * Grant read permissions to the table.
4489
+ */
4490
+ grantReadData(grantee) {
4491
+ return this._table.grantReadData(grantee);
4492
+ }
4493
+ /**
4494
+ * Grant write permissions to the table.
4495
+ */
4496
+ grantWriteData(grantee) {
4497
+ return this._table.grantWriteData(grantee);
4498
+ }
4499
+ /**
4500
+ * Grant read and write permissions to the table.
4501
+ */
4502
+ grantReadWriteData(grantee) {
4503
+ return this._table.grantReadWriteData(grantee);
4504
+ }
4505
+ //
4506
+ //
4507
+ // Convenience methods
4508
+ //
4509
+ /**
4510
+ * Add the table name to a Lambda function's environment variables.
4511
+ * Also grants read/write access to the table.
4512
+ */
4513
+ connectLambda(lambdaFunction, options = {}) {
4514
+ const { envKey = "CONNECTION_TABLE", readOnly = false } = options;
4515
+ // Add environment variable
4516
+ if ("addEnvironment" in lambdaFunction) {
4517
+ lambdaFunction.addEnvironment(envKey, this.tableName);
4518
+ }
4519
+ // Grant permissions
4520
+ if (readOnly) {
4521
+ this.grantReadData(lambdaFunction.grantPrincipal);
4522
+ }
4523
+ else {
4524
+ this.grantReadWriteData(lambdaFunction.grantPrincipal);
4525
+ }
4526
+ }
4527
+ }
4528
+
4138
4529
  exports.CDK = CDK$2;
4139
4530
  exports.JaypieAccountLoggingBucket = JaypieAccountLoggingBucket;
4140
4531
  exports.JaypieApiGateway = JaypieApiGateway;
@@ -4165,6 +4556,9 @@ exports.JaypieStack = JaypieStack;
4165
4556
  exports.JaypieStaticWebBucket = JaypieStaticWebBucket;
4166
4557
  exports.JaypieTraceSigningKeySecret = JaypieTraceSigningKeySecret;
4167
4558
  exports.JaypieWebDeploymentBucket = JaypieWebDeploymentBucket;
4559
+ exports.JaypieWebSocket = JaypieWebSocket;
4560
+ exports.JaypieWebSocketLambda = JaypieWebSocketLambda;
4561
+ exports.JaypieWebSocketTable = JaypieWebSocketTable;
4168
4562
  exports.addDatadogLayers = addDatadogLayers;
4169
4563
  exports.clearAllCertificateCaches = clearAllCertificateCaches;
4170
4564
  exports.clearAllSecretsCaches = clearAllSecretsCaches;