@jaypie/constructs 1.2.18 → 1.2.20

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
@@ -3440,21 +3445,21 @@ class JaypieOrganizationTrail extends constructs.Construct {
3440
3445
  ],
3441
3446
  });
3442
3447
  // Add CloudTrail bucket policies
3443
- this.bucket.addToResourcePolicy(new awsIam.PolicyStatement({
3448
+ this.bucket.addToResourcePolicy(new iam.PolicyStatement({
3444
3449
  actions: ["s3:GetBucketAcl"],
3445
- effect: awsIam.Effect.ALLOW,
3446
- principals: [new awsIam.ServicePrincipal("cloudtrail.amazonaws.com")],
3450
+ effect: iam.Effect.ALLOW,
3451
+ principals: [new iam.ServicePrincipal("cloudtrail.amazonaws.com")],
3447
3452
  resources: [this.bucket.bucketArn],
3448
3453
  }));
3449
- this.bucket.addToResourcePolicy(new awsIam.PolicyStatement({
3454
+ this.bucket.addToResourcePolicy(new iam.PolicyStatement({
3450
3455
  actions: ["s3:PutObject"],
3451
3456
  conditions: {
3452
3457
  StringEquals: {
3453
3458
  "s3:x-amz-acl": "bucket-owner-full-control",
3454
3459
  },
3455
3460
  },
3456
- effect: awsIam.Effect.ALLOW,
3457
- principals: [new awsIam.ServicePrincipal("cloudtrail.amazonaws.com")],
3461
+ effect: iam.Effect.ALLOW,
3462
+ principals: [new iam.ServicePrincipal("cloudtrail.amazonaws.com")],
3458
3463
  resources: [`${this.bucket.bucketArn}/*`],
3459
3464
  }));
3460
3465
  // Add tags to bucket
@@ -3547,9 +3552,9 @@ class JaypieSsoPermissions extends constructs.Construct {
3547
3552
  ],
3548
3553
  },
3549
3554
  managedPolicies: [
3550
- awsIam.ManagedPolicy.fromAwsManagedPolicyName("AdministratorAccess")
3555
+ iam.ManagedPolicy.fromAwsManagedPolicyName("AdministratorAccess")
3551
3556
  .managedPolicyArn,
3552
- awsIam.ManagedPolicy.fromAwsManagedPolicyName("AWSManagementConsoleBasicUserAccess").managedPolicyArn,
3557
+ iam.ManagedPolicy.fromAwsManagedPolicyName("AWSManagementConsoleBasicUserAccess").managedPolicyArn,
3553
3558
  ],
3554
3559
  sessionDuration: cdk.Duration.hours(1).toIsoString(),
3555
3560
  tags: [
@@ -3628,10 +3633,10 @@ class JaypieSsoPermissions extends constructs.Construct {
3628
3633
  ],
3629
3634
  },
3630
3635
  managedPolicies: [
3631
- awsIam.ManagedPolicy.fromAwsManagedPolicyName("AmazonQDeveloperAccess")
3636
+ iam.ManagedPolicy.fromAwsManagedPolicyName("AmazonQDeveloperAccess")
3632
3637
  .managedPolicyArn,
3633
- awsIam.ManagedPolicy.fromAwsManagedPolicyName("AWSManagementConsoleBasicUserAccess").managedPolicyArn,
3634
- awsIam.ManagedPolicy.fromAwsManagedPolicyName("ReadOnlyAccess")
3638
+ iam.ManagedPolicy.fromAwsManagedPolicyName("AWSManagementConsoleBasicUserAccess").managedPolicyArn,
3639
+ iam.ManagedPolicy.fromAwsManagedPolicyName("ReadOnlyAccess")
3635
3640
  .managedPolicyArn,
3636
3641
  ],
3637
3642
  sessionDuration: cdk.Duration.hours(12).toIsoString(),
@@ -3686,12 +3691,12 @@ class JaypieSsoPermissions extends constructs.Construct {
3686
3691
  ],
3687
3692
  },
3688
3693
  managedPolicies: [
3689
- awsIam.ManagedPolicy.fromAwsManagedPolicyName("AmazonQDeveloperAccess")
3694
+ iam.ManagedPolicy.fromAwsManagedPolicyName("AmazonQDeveloperAccess")
3690
3695
  .managedPolicyArn,
3691
- awsIam.ManagedPolicy.fromAwsManagedPolicyName("AWSManagementConsoleBasicUserAccess").managedPolicyArn,
3692
- awsIam.ManagedPolicy.fromAwsManagedPolicyName("ReadOnlyAccess")
3696
+ iam.ManagedPolicy.fromAwsManagedPolicyName("AWSManagementConsoleBasicUserAccess").managedPolicyArn,
3697
+ iam.ManagedPolicy.fromAwsManagedPolicyName("ReadOnlyAccess")
3693
3698
  .managedPolicyArn,
3694
- awsIam.ManagedPolicy.fromAwsManagedPolicyName("job-function/SystemAdministrator").managedPolicyArn,
3699
+ iam.ManagedPolicy.fromAwsManagedPolicyName("job-function/SystemAdministrator").managedPolicyArn,
3695
3700
  ],
3696
3701
  sessionDuration: cdk.Duration.hours(4).toIsoString(),
3697
3702
  tags: [
@@ -3904,8 +3909,8 @@ class JaypieWebDeploymentBucket extends constructs.Construct {
3904
3909
  repo = `repo:${process.env.CDK_ENV_REPO}:*`;
3905
3910
  }
3906
3911
  if (repo) {
3907
- const bucketDeployRole = new awsIam.Role(this, "DestinationBucketDeployRole", {
3908
- 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), {
3909
3914
  StringLike: {
3910
3915
  "token.actions.githubusercontent.com:sub": repo,
3911
3916
  },
@@ -3914,8 +3919,8 @@ class JaypieWebDeploymentBucket extends constructs.Construct {
3914
3919
  });
3915
3920
  cdk.Tags.of(bucketDeployRole).add(CDK$2.TAG.ROLE, CDK$2.ROLE.DEPLOY);
3916
3921
  // Allow the role to write to the bucket
3917
- bucketDeployRole.addToPolicy(new awsIam.PolicyStatement({
3918
- effect: awsIam.Effect.ALLOW,
3922
+ bucketDeployRole.addToPolicy(new iam.PolicyStatement({
3923
+ effect: iam.Effect.ALLOW,
3919
3924
  actions: [
3920
3925
  "s3:DeleteObject",
3921
3926
  "s3:GetObject",
@@ -3924,16 +3929,16 @@ class JaypieWebDeploymentBucket extends constructs.Construct {
3924
3929
  ],
3925
3930
  resources: [`${this.bucket.bucketArn}/*`],
3926
3931
  }));
3927
- bucketDeployRole.addToPolicy(new awsIam.PolicyStatement({
3928
- effect: awsIam.Effect.ALLOW,
3932
+ bucketDeployRole.addToPolicy(new iam.PolicyStatement({
3933
+ effect: iam.Effect.ALLOW,
3929
3934
  actions: ["s3:ListBucket"],
3930
3935
  resources: [this.bucket.bucketArn],
3931
3936
  }));
3932
3937
  // Allow the role to describe the current stack
3933
3938
  const stack = cdk.Stack.of(this);
3934
- bucketDeployRole.addToPolicy(new awsIam.PolicyStatement({
3939
+ bucketDeployRole.addToPolicy(new iam.PolicyStatement({
3935
3940
  actions: ["cloudformation:DescribeStacks"],
3936
- effect: awsIam.Effect.ALLOW,
3941
+ effect: iam.Effect.ALLOW,
3937
3942
  resources: [
3938
3943
  `arn:aws:cloudformation:${stack.region}:${stack.account}:stack/${stack.stackName}/*`,
3939
3944
  ],
@@ -4150,6 +4155,377 @@ class JaypieTraceSigningKeySecret extends JaypieEnvSecret {
4150
4155
  }
4151
4156
  }
4152
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}/POST/@connections/*`,
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
+
4153
4529
  exports.CDK = CDK$2;
4154
4530
  exports.JaypieAccountLoggingBucket = JaypieAccountLoggingBucket;
4155
4531
  exports.JaypieApiGateway = JaypieApiGateway;
@@ -4180,6 +4556,9 @@ exports.JaypieStack = JaypieStack;
4180
4556
  exports.JaypieStaticWebBucket = JaypieStaticWebBucket;
4181
4557
  exports.JaypieTraceSigningKeySecret = JaypieTraceSigningKeySecret;
4182
4558
  exports.JaypieWebDeploymentBucket = JaypieWebDeploymentBucket;
4559
+ exports.JaypieWebSocket = JaypieWebSocket;
4560
+ exports.JaypieWebSocketLambda = JaypieWebSocketLambda;
4561
+ exports.JaypieWebSocketTable = JaypieWebSocketTable;
4183
4562
  exports.addDatadogLayers = addDatadogLayers;
4184
4563
  exports.clearAllCertificateCaches = clearAllCertificateCaches;
4185
4564
  exports.clearAllSecretsCaches = clearAllSecretsCaches;