@deployfoundation/foundation-deploy 0.2.15 → 0.2.16

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.
@@ -19,7 +19,7 @@ import {
19
19
  scheduledJobBindings,
20
20
  slackCommandPrefixes,
21
21
  webhookJobBindings
22
- } from "./chunk-7qn00ny1.js";
22
+ } from "./chunk-9rasrppa.js";
23
23
 
24
24
  // src/stacks/agent-stack.ts
25
25
  import * as cdk from "aws-cdk-lib";
@@ -240,6 +240,60 @@ class FoundationAgent extends cdk.Stack {
240
240
  resources: [secretArnPattern(knockOauthClient), secretArnPattern(knockCredential)]
241
241
  }));
242
242
  }
243
+ pagesPolicy(id, role, instance, options) {
244
+ if (!provisionsIntegration(instance, "pages"))
245
+ return;
246
+ const stack = cdk.Stack.of(this);
247
+ const table = stack.formatArn({
248
+ service: "dynamodb",
249
+ resource: "table",
250
+ resourceName: "*-PagesTable*"
251
+ });
252
+ const statements = [
253
+ new iam.PolicyStatement({
254
+ sid: "DenyDirectPagesAccess",
255
+ effect: iam.Effect.DENY,
256
+ actions: [
257
+ "s3:*",
258
+ "dynamodb:*",
259
+ "secretsmanager:GetSecretValue",
260
+ "secretsmanager:BatchGetSecretValue",
261
+ "cloudfront-keyvaluestore:*"
262
+ ],
263
+ resources: [
264
+ `arn:${stack.partition}:s3:::*pagesbucket*`,
265
+ `arn:${stack.partition}:s3:::*pagesbucket*/*`,
266
+ table,
267
+ `${table}/index/*`,
268
+ stack.formatArn({
269
+ service: "secretsmanager",
270
+ resource: "secret",
271
+ resourceName: `${this.names.secretPagesCookieKey}-??????`,
272
+ arnFormat: cdk.ArnFormat.COLON_RESOURCE_NAME
273
+ }),
274
+ `arn:${stack.partition}:cloudfront::${stack.account}:key-value-store/*`
275
+ ]
276
+ })
277
+ ];
278
+ if (options.invoke)
279
+ statements.push(new iam.PolicyStatement({
280
+ sid: "PagesApiInvoke",
281
+ actions: ["lambda:InvokeFunction"],
282
+ resources: [
283
+ stack.formatArn({
284
+ service: "lambda",
285
+ resource: "function",
286
+ resourceName: this.names.pagesApiFunctionName,
287
+ arnFormat: cdk.ArnFormat.COLON_RESOURCE_NAME
288
+ })
289
+ ]
290
+ }));
291
+ new iam.ManagedPolicy(this, id, {
292
+ description: `${instance.displayName} hosted pages: only through the Pages API`,
293
+ roles: [role],
294
+ statements
295
+ });
296
+ }
243
297
  denyDirectUpworkApprovalAccess(role, upworkApprovalTable) {
244
298
  if (upworkApprovalTable === undefined)
245
299
  return;
@@ -290,6 +344,7 @@ class FoundationAgent extends cdk.Stack {
290
344
  this.denyDirectKnockSecretAccess(role, props);
291
345
  this.denyDirectUpworkApprovalAccess(role, props.upworkApprovalTable);
292
346
  this.denyDirectEtradeAccess(role, props);
347
+ this.pagesPolicy("AgentReadOnlyPagesPolicy", role, props.instance, { invoke: false });
293
348
  role.addToPolicy(new iam.PolicyStatement({
294
349
  sid: "DenyAgentCoreBrowserMetadata",
295
350
  effect: iam.Effect.DENY,
@@ -350,6 +405,7 @@ class FoundationAgent extends cdk.Stack {
350
405
  this.denyDirectKnockSecretAccess(role, props);
351
406
  this.denyDirectUpworkApprovalAccess(role, props.upworkApprovalTable);
352
407
  this.denyDirectEtradeAccess(role, props);
408
+ this.pagesPolicy("AgentExecutionPagesPolicy", role, props.instance, { invoke: true });
353
409
  const releaseRepositoryArn = releaseImageRepositoryArn(this);
354
410
  role.addToPolicy(new iam.PolicyStatement({
355
411
  sid: "EcrPull",
@@ -2679,18 +2735,500 @@ function senderAddressFor(sender) {
2679
2735
  return address;
2680
2736
  }
2681
2737
 
2682
- // src/stacks/pipeline-stack.ts
2738
+ // src/stacks/pages-stack.ts
2683
2739
  import * as cdk7 from "aws-cdk-lib";
2740
+ import * as acm2 from "aws-cdk-lib/aws-certificatemanager";
2741
+ import * as cloudfront from "aws-cdk-lib/aws-cloudfront";
2742
+ import * as origins from "aws-cdk-lib/aws-cloudfront-origins";
2743
+ import * as dynamodb3 from "aws-cdk-lib/aws-dynamodb";
2744
+ import * as iam8 from "aws-cdk-lib/aws-iam";
2745
+ import * as lambda3 from "aws-cdk-lib/aws-lambda";
2746
+ import * as route532 from "aws-cdk-lib/aws-route53";
2747
+ import * as route53Targets2 from "aws-cdk-lib/aws-route53-targets";
2748
+ import * as s32 from "aws-cdk-lib/aws-s3";
2749
+ import * as secretsmanager4 from "aws-cdk-lib/aws-secretsmanager";
2750
+
2751
+ // src/stacks/pages-edge-function.ts
2752
+ var PAGES_EDGE_KEY_NAME = "cookie-key-v1";
2753
+ var PAGES_HOST_HEADER = "x-foundation-pages-host";
2754
+ var PAGES_VIEWER_REQUEST_SOURCE = `import cf from 'cloudfront';
2755
+ import crypto from 'crypto';
2756
+
2757
+ var kvs = cf.kvs('__KVS_ID__');
2758
+ var COOKIE = 'fpa';
2759
+ var KEY_NAME = '${PAGES_EDGE_KEY_NAME}';
2760
+ var HOST_HEADER = '${PAGES_HOST_HEADER}';
2761
+ var PAGE_RE = /^\\/p\\/([a-z0-9]{24})(\\/.*)?$/;
2762
+ var VERSION_RE = /^\\/v([1-9][0-9]{0,5})(\\/.*)?$/;
2763
+ var EXPORT_RE = /^\\/exports\\/v([1-9][0-9]{0,5})\\/([^\\/]+)$/;
2764
+ var UNSAFE_RE = /(^|\\/)\\.\\.?(\\/|$)|\\/\\/|%2e|%2f|%5c|\\\\/i;
2765
+ var USER_RE = /^[UW][A-Z0-9]{2,31}$/;
2766
+ var EXP_RE = /^[0-9]{1,12}$/;
2767
+
2768
+ function reply(status, description, body) {
2769
+ return {
2770
+ statusCode: status,
2771
+ statusDescription: description,
2772
+ headers: {
2773
+ 'content-type': { value: 'text/plain; charset=utf-8' },
2774
+ 'cache-control': { value: 'no-store' }
2775
+ },
2776
+ body: { encoding: 'text', data: body }
2777
+ };
2778
+ }
2779
+
2780
+ function redirect(location) {
2781
+ return {
2782
+ statusCode: 302,
2783
+ statusDescription: 'Found',
2784
+ headers: { location: { value: location }, 'cache-control': { value: 'no-store' } }
2785
+ };
2786
+ }
2787
+
2788
+ function equal(a, b) {
2789
+ if (a.length !== b.length) return false;
2790
+ var diff = 0;
2791
+ for (var i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
2792
+ return diff === 0;
2793
+ }
2794
+
2795
+ function validCookie(value, pageId, key, now) {
2796
+ if (typeof value !== 'string' || value.length > 256) return false;
2797
+ var parts = value.split('.');
2798
+ if (parts.length !== 5 || parts[0] !== 'v1' || parts[1] !== pageId) return false;
2799
+ if (!USER_RE.test(parts[2]) || !EXP_RE.test(parts[3]) || Number(parts[3]) <= now) return false;
2800
+ var mac = crypto.createHmac('sha256', key).update(parts.slice(0, 4).join('.')).digest('hex');
2801
+ return equal(mac, parts[4]);
2802
+ }
2803
+
2804
+ function cookieValues(request) {
2805
+ var cookie = request.cookies ? request.cookies[COOKIE] : undefined;
2806
+ if (!cookie) return [];
2807
+ var values = [cookie.value];
2808
+ if (cookie.multiValue) {
2809
+ for (var i = 0; i < cookie.multiValue.length; i++) values.push(cookie.multiValue[i].value);
2810
+ }
2811
+ return values;
2812
+ }
2813
+
2814
+ function objectPath(pageId, rest) {
2815
+ var prefix = '/pages/' + pageId;
2816
+ if (rest === '/') return prefix + '/latest.html';
2817
+ var version = VERSION_RE.exec(rest);
2818
+ if (version) {
2819
+ var sub = version[2];
2820
+ if (sub.charAt(sub.length - 1) === '/') {
2821
+ sub = sub + 'index.html';
2822
+ } else if (sub.slice(sub.lastIndexOf('/') + 1).indexOf('.') === -1) {
2823
+ sub = '/index.html';
2824
+ }
2825
+ return prefix + '/v' + version[1] + '/build' + sub;
2826
+ }
2827
+ var download = EXPORT_RE.exec(rest);
2828
+ if (download) return prefix + '/v' + download[1] + '/exports/' + download[2];
2829
+ return null;
2830
+ }
2831
+
2832
+ async function handler(event) {
2833
+ var request = event.request;
2834
+ var uri = request.uri;
2835
+ if (uri.indexOf('/_auth/') === 0) {
2836
+ request.headers[HOST_HEADER] = { value: request.headers.host ? request.headers.host.value : '' };
2837
+ return request;
2838
+ }
2839
+ var page = PAGE_RE.exec(uri);
2840
+ if (!page || UNSAFE_RE.test(uri)) return reply(404, 'Not Found', 'Not found');
2841
+ var pageId = page[1];
2842
+ var rest = page[2] || '';
2843
+ if (rest === '') return redirect(uri + '/');
2844
+ var version = VERSION_RE.exec(rest);
2845
+ if (version && version[2] === undefined) return redirect(uri + '/');
2846
+ var target = objectPath(pageId, rest);
2847
+ if (target === null) return reply(404, 'Not Found', 'Not found');
2848
+
2849
+ var key;
2850
+ try {
2851
+ key = await kvs.get(KEY_NAME);
2852
+ } catch (err) {
2853
+ return reply(503, 'Service Unavailable', 'Pages sign-in is starting up. Try again in a minute.');
2854
+ }
2855
+ var now = Math.floor(Date.now() / 1000);
2856
+ var values = cookieValues(request);
2857
+ for (var i = 0; i < values.length; i++) {
2858
+ if (validCookie(values[i], pageId, key, now)) {
2859
+ request.uri = target;
2860
+ return request;
2861
+ }
2862
+ }
2863
+ return redirect('/_auth/start?page=' + pageId + '&next=' + encodeURIComponent(uri));
2864
+ }
2865
+ `;
2866
+ function pagesViewerRequestCode(kvsId) {
2867
+ return PAGES_VIEWER_REQUEST_SOURCE.replace("__KVS_ID__", kvsId);
2868
+ }
2869
+
2870
+ // src/stacks/pages-stack.ts
2871
+ function pagesDomainPlan(instance, delegated) {
2872
+ const { domain, hostedZoneId } = instance.pages;
2873
+ if (domain === undefined)
2874
+ return { ownZone: false };
2875
+ const ownZone = hostedZoneId === undefined;
2876
+ return ownZone && !delegated ? { pending: domain, ownZone } : { attached: domain, ownZone };
2877
+ }
2878
+ var PAGES_CONTENT_SECURITY_POLICY = [
2879
+ "default-src 'self'",
2880
+ "script-src 'self' 'unsafe-inline' 'unsafe-eval' blob:",
2881
+ "style-src 'self' 'unsafe-inline' https://fonts.googleapis.com",
2882
+ "font-src 'self' data: https://fonts.gstatic.com",
2883
+ "img-src 'self' data: blob:",
2884
+ "media-src 'self' data: blob:",
2885
+ "connect-src 'self' data: blob:",
2886
+ "worker-src 'self' blob:",
2887
+ "frame-src 'self'",
2888
+ "object-src 'none'",
2889
+ "base-uri 'self'",
2890
+ "form-action 'self'",
2891
+ "frame-ancestors 'self'"
2892
+ ].join("; ");
2893
+
2894
+ class FoundationPages extends cdk7.Stack {
2895
+ bucket;
2896
+ table;
2897
+ apiFunction;
2898
+ distribution;
2899
+ constructor(scope, id, props) {
2900
+ super(scope, id, props);
2901
+ const { instance } = props;
2902
+ if (!instance.integrations.pages)
2903
+ throw new Error(`${id}: FoundationPages needs integrations.pages: true`);
2904
+ const names = instanceNames(instance);
2905
+ const display = instance.displayName;
2906
+ const plan = pagesDomainPlan(instance, props.domainDelegated === true);
2907
+ const domain = plan.attached;
2908
+ this.bucket = new s32.Bucket(this, "PagesBucket", {
2909
+ blockPublicAccess: s32.BlockPublicAccess.BLOCK_ALL,
2910
+ encryption: s32.BucketEncryption.S3_MANAGED,
2911
+ enforceSSL: true,
2912
+ versioned: true,
2913
+ objectOwnership: s32.ObjectOwnership.BUCKET_OWNER_ENFORCED,
2914
+ removalPolicy: cdk7.RemovalPolicy.RETAIN,
2915
+ lifecycleRules: [
2916
+ { id: "StagedUploads", prefix: "uploads/", expiration: cdk7.Duration.days(1) },
2917
+ { id: "NoncurrentObjects", noncurrentVersionExpiration: cdk7.Duration.days(30) },
2918
+ { id: "IncompleteMultipart", abortIncompleteMultipartUploadAfter: cdk7.Duration.days(1) }
2919
+ ]
2920
+ });
2921
+ this.table = new dynamodb3.Table(this, "PagesTable", {
2922
+ partitionKey: { name: "pk", type: dynamodb3.AttributeType.STRING },
2923
+ sortKey: { name: "sk", type: dynamodb3.AttributeType.STRING },
2924
+ billingMode: dynamodb3.BillingMode.PAY_PER_REQUEST,
2925
+ encryption: dynamodb3.TableEncryption.CUSTOMER_MANAGED,
2926
+ encryptionKey: props.dataKey,
2927
+ pointInTimeRecoverySpecification: { pointInTimeRecoveryEnabled: true },
2928
+ timeToLiveAttribute: "ttl",
2929
+ removalPolicy: cdk7.RemovalPolicy.RETAIN
2930
+ });
2931
+ this.table.addGlobalSecondaryIndex({
2932
+ indexName: "ChannelIndex",
2933
+ partitionKey: { name: "gsi1pk", type: dynamodb3.AttributeType.STRING },
2934
+ sortKey: { name: "gsi1sk", type: dynamodb3.AttributeType.STRING },
2935
+ projectionType: dynamodb3.ProjectionType.ALL
2936
+ });
2937
+ const cookieKey = new secretsmanager4.Secret(this, "CookieKey", {
2938
+ secretName: names.secretPagesCookieKey,
2939
+ description: `${display} hosted-pages cookie signing key ({ key }); read by the sign-in Lambda and copied to the edge key store`,
2940
+ removalPolicy: cdk7.RemovalPolicy.DESTROY,
2941
+ generateSecretString: {
2942
+ secretStringTemplate: "{}",
2943
+ generateStringKey: "key",
2944
+ passwordLength: 64,
2945
+ excludePunctuation: true
2946
+ }
2947
+ });
2948
+ const edgeKeys = new cloudfront.KeyValueStore(this, "EdgeKeys", {
2949
+ comment: `${display} pages cookie key for the viewer function`
2950
+ });
2951
+ const authRole = new iam8.Role(this, "PagesAuthRole", {
2952
+ assumedBy: new iam8.ServicePrincipal("lambda.amazonaws.com"),
2953
+ description: `${display} pages sign-in Lambda (Slack OIDC + membership check)`,
2954
+ managedPolicies: [
2955
+ iam8.ManagedPolicy.fromAwsManagedPolicyName("service-role/AWSLambdaBasicExecutionRole")
2956
+ ]
2957
+ });
2958
+ authRole.addToPolicy(new iam8.PolicyStatement({
2959
+ sid: "PagesAuthSecrets",
2960
+ actions: ["secretsmanager:GetSecretValue"],
2961
+ resources: [secretArnPattern3(props.secrets.slackApp), cookieKey.secretArn]
2962
+ }));
2963
+ authRole.addToPolicy(new iam8.PolicyStatement({
2964
+ sid: "PagesAuthPageLookup",
2965
+ actions: ["dynamodb:GetItem"],
2966
+ resources: [this.table.tableArn]
2967
+ }));
2968
+ authRole.addToPolicy(new iam8.PolicyStatement({
2969
+ sid: "PagesTableKey",
2970
+ actions: ["kms:Decrypt", "kms:DescribeKey"],
2971
+ resources: [props.dataKey.keyArn]
2972
+ }));
2973
+ const authFunction = new lambda3.Function(this, "PagesAuthFunction", {
2974
+ runtime: lambda3.Runtime.NODEJS_22_X,
2975
+ handler: "index.handler",
2976
+ code: lambdaCode(this, "pages-auth"),
2977
+ role: authRole,
2978
+ timeout: cdk7.Duration.seconds(15),
2979
+ memorySize: 256,
2980
+ environment: {
2981
+ FOUNDATION_SLACK_APP_SECRET_ID: props.secrets.slackApp.secretName,
2982
+ FOUNDATION_PAGES_COOKIE_SECRET_ID: cookieKey.secretName,
2983
+ FOUNDATION_SLACK_TEAM_ID: instance.slack.teamId,
2984
+ FOUNDATION_PAGES_TABLE_NAME: this.table.tableName,
2985
+ FOUNDATION_DISPLAY_NAME: display,
2986
+ ...domain === undefined ? {} : { FOUNDATION_PAGES_DOMAIN: domain }
2987
+ }
2988
+ });
2989
+ const authUrl = authFunction.addFunctionUrl({ authType: lambda3.FunctionUrlAuthType.AWS_IAM });
2990
+ const seedRole = new iam8.Role(this, "EdgeKeySeedRole", {
2991
+ assumedBy: new iam8.ServicePrincipal("lambda.amazonaws.com"),
2992
+ description: `${display} copies the pages cookie key into the edge key store`,
2993
+ managedPolicies: [
2994
+ iam8.ManagedPolicy.fromAwsManagedPolicyName("service-role/AWSLambdaBasicExecutionRole")
2995
+ ]
2996
+ });
2997
+ seedRole.addToPolicy(new iam8.PolicyStatement({
2998
+ sid: "ReadCookieKey",
2999
+ actions: ["secretsmanager:GetSecretValue"],
3000
+ resources: [cookieKey.secretArn]
3001
+ }));
3002
+ seedRole.addToPolicy(new iam8.PolicyStatement({
3003
+ sid: "WriteEdgeKey",
3004
+ actions: [
3005
+ "cloudfront-keyvaluestore:DescribeKeyValueStore",
3006
+ "cloudfront-keyvaluestore:PutKey"
3007
+ ],
3008
+ resources: [edgeKeys.keyValueStoreArn]
3009
+ }));
3010
+ const seedFunction = new lambda3.Function(this, "EdgeKeySeedFunction", {
3011
+ runtime: lambda3.Runtime.NODEJS_22_X,
3012
+ handler: "index.edgeKeyHandler",
3013
+ code: lambdaCode(this, "pages-auth"),
3014
+ role: seedRole,
3015
+ timeout: cdk7.Duration.seconds(60),
3016
+ memorySize: 256
3017
+ });
3018
+ new cdk7.CustomResource(this, "EdgeKeySeed", {
3019
+ serviceToken: seedFunction.functionArn,
3020
+ resourceType: "Custom::FoundationPagesEdgeKey",
3021
+ properties: {
3022
+ SecretId: cookieKey.secretArn,
3023
+ KeyValueStoreArn: edgeKeys.keyValueStoreArn,
3024
+ Generation: "1"
3025
+ }
3026
+ });
3027
+ const viewerRequest = new cloudfront.Function(this, "ViewerRequest", {
3028
+ runtime: cloudfront.FunctionRuntime.JS_2_0,
3029
+ keyValueStore: edgeKeys,
3030
+ comment: `${display} pages: cookie check and path mapping`,
3031
+ code: cloudfront.FunctionCode.fromInline(pagesViewerRequestCode(edgeKeys.keyValueStoreId))
3032
+ });
3033
+ const headers = new cloudfront.ResponseHeadersPolicy(this, "PagesHeaders", {
3034
+ comment: `${display} pages security headers`,
3035
+ securityHeadersBehavior: {
3036
+ contentSecurityPolicy: {
3037
+ contentSecurityPolicy: PAGES_CONTENT_SECURITY_POLICY,
3038
+ override: true
3039
+ },
3040
+ strictTransportSecurity: {
3041
+ accessControlMaxAge: cdk7.Duration.days(365),
3042
+ includeSubdomains: false,
3043
+ override: true
3044
+ },
3045
+ contentTypeOptions: { override: true },
3046
+ frameOptions: { frameOption: cloudfront.HeadersFrameOption.SAMEORIGIN, override: true },
3047
+ referrerPolicy: {
3048
+ referrerPolicy: cloudfront.HeadersReferrerPolicy.STRICT_ORIGIN_WHEN_CROSS_ORIGIN,
3049
+ override: true
3050
+ }
3051
+ },
3052
+ customHeadersBehavior: {
3053
+ customHeaders: [{ header: "X-Robots-Tag", value: "noindex, nofollow", override: true }]
3054
+ }
3055
+ });
3056
+ const zoneDomain = instance.pages.domain;
3057
+ const zone = zoneDomain === undefined ? undefined : instance.pages.hostedZoneId !== undefined ? route532.HostedZone.fromHostedZoneId(this, "PagesParentZone", instance.pages.hostedZoneId) : new route532.PublicHostedZone(this, "PagesZone", {
3058
+ zoneName: zoneDomain,
3059
+ comment: `${display} hosted pages (delegated from the company's DNS)`
3060
+ });
3061
+ let certificate;
3062
+ if (domain !== undefined && zone !== undefined) {
3063
+ const certificateRole = new iam8.Role(this, "PagesCertificateRole", {
3064
+ assumedBy: new iam8.ServicePrincipal("lambda.amazonaws.com"),
3065
+ description: `${display} issues the pages domain certificate in us-east-1`,
3066
+ managedPolicies: [
3067
+ iam8.ManagedPolicy.fromAwsManagedPolicyName("service-role/AWSLambdaBasicExecutionRole")
3068
+ ]
3069
+ });
3070
+ certificateRole.addToPolicy(new iam8.PolicyStatement({
3071
+ sid: "RequestCertificate",
3072
+ actions: ["acm:RequestCertificate"],
3073
+ resources: ["*"]
3074
+ }));
3075
+ certificateRole.addToPolicy(new iam8.PolicyStatement({
3076
+ sid: "ManageCertificate",
3077
+ actions: ["acm:DescribeCertificate", "acm:DeleteCertificate"],
3078
+ resources: [`arn:${this.partition}:acm:us-east-1:${this.account}:certificate/*`]
3079
+ }));
3080
+ certificateRole.addToPolicy(new iam8.PolicyStatement({
3081
+ sid: "ValidationRecord",
3082
+ actions: ["route53:ChangeResourceRecordSets"],
3083
+ resources: [`arn:${this.partition}:route53:::hostedzone/${zone.hostedZoneId}`]
3084
+ }));
3085
+ const certificateFunction = new lambda3.Function(this, "PagesCertificateFunction", {
3086
+ runtime: lambda3.Runtime.NODEJS_22_X,
3087
+ handler: "index.certificateHandler",
3088
+ code: lambdaCode(this, "pages-auth"),
3089
+ role: certificateRole,
3090
+ timeout: cdk7.Duration.minutes(15),
3091
+ memorySize: 256
3092
+ });
3093
+ const issued = new cdk7.CustomResource(this, "PagesCertificate", {
3094
+ serviceToken: certificateFunction.functionArn,
3095
+ resourceType: "Custom::FoundationPagesCertificate",
3096
+ properties: { DomainName: domain, HostedZoneId: zone.hostedZoneId }
3097
+ });
3098
+ certificate = acm2.Certificate.fromCertificateArn(this, "PagesCertificateArn", issued.getAttString("CertificateArn"));
3099
+ }
3100
+ const functionAssociations = [
3101
+ { function: viewerRequest, eventType: cloudfront.FunctionEventType.VIEWER_REQUEST }
3102
+ ];
3103
+ this.distribution = new cloudfront.Distribution(this, "Distribution", {
3104
+ comment: `${display} pages`,
3105
+ ...domain === undefined || certificate === undefined ? {} : { domainNames: [domain], certificate },
3106
+ defaultRootObject: "index.html",
3107
+ minimumProtocolVersion: cloudfront.SecurityPolicyProtocol.TLS_V1_2_2021,
3108
+ httpVersion: cloudfront.HttpVersion.HTTP2_AND_3,
3109
+ priceClass: cloudfront.PriceClass.PRICE_CLASS_100,
3110
+ defaultBehavior: {
3111
+ origin: origins.S3BucketOrigin.withOriginAccessControl(this.bucket, {
3112
+ originAccessLevels: [cloudfront.AccessLevel.READ, cloudfront.AccessLevel.LIST]
3113
+ }),
3114
+ viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
3115
+ allowedMethods: cloudfront.AllowedMethods.ALLOW_GET_HEAD,
3116
+ cachePolicy: cloudfront.CachePolicy.CACHING_OPTIMIZED,
3117
+ responseHeadersPolicy: headers,
3118
+ functionAssociations
3119
+ },
3120
+ additionalBehaviors: {
3121
+ "/_auth/*": {
3122
+ origin: origins.FunctionUrlOrigin.withOriginAccessControl(authUrl),
3123
+ viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
3124
+ allowedMethods: cloudfront.AllowedMethods.ALLOW_GET_HEAD,
3125
+ cachePolicy: cloudfront.CachePolicy.CACHING_DISABLED,
3126
+ originRequestPolicy: cloudfront.OriginRequestPolicy.ALL_VIEWER_EXCEPT_HOST_HEADER,
3127
+ responseHeadersPolicy: headers,
3128
+ functionAssociations
3129
+ }
3130
+ }
3131
+ });
3132
+ cdk7.Annotations.of(this.distribution).acknowledgeWarning("@aws-cdk/aws-cloudfront-origins:listBucketSecurityRisk", "The viewer function forwards only mapped object keys and 404s `/`; defaultRootObject is set too. LIST only turns a missing key into 404 instead of 403.");
3133
+ if (domain !== undefined && zone !== undefined) {
3134
+ const target = route532.RecordTarget.fromAlias(new route53Targets2.CloudFrontTarget(this.distribution));
3135
+ new route532.ARecord(this, "PagesAliasA", { zone, recordName: `${domain}.`, target });
3136
+ new route532.AaaaRecord(this, "PagesAliasAaaa", { zone, recordName: `${domain}.`, target });
3137
+ }
3138
+ const defaultUrl = `https://${this.distribution.distributionDomainName}`;
3139
+ const baseUrl = domain === undefined ? defaultUrl : `https://${domain}`;
3140
+ const apiRole = new iam8.Role(this, "PagesApiRole", {
3141
+ assumedBy: new iam8.ServicePrincipal("lambda.amazonaws.com"),
3142
+ description: `${display} Pages API proxy (the only writer of the pages bucket and table)`,
3143
+ managedPolicies: [
3144
+ iam8.ManagedPolicy.fromAwsManagedPolicyName("service-role/AWSLambdaBasicExecutionRole")
3145
+ ]
3146
+ });
3147
+ apiRole.addToPolicy(new iam8.PolicyStatement({
3148
+ sid: "PagesApiSigningSecret",
3149
+ actions: ["secretsmanager:GetSecretValue"],
3150
+ resources: [secretArnPattern3(props.secrets.signing)]
3151
+ }));
3152
+ apiRole.addToPolicy(new iam8.PolicyStatement({
3153
+ sid: "PagesTableData",
3154
+ actions: [
3155
+ "dynamodb:GetItem",
3156
+ "dynamodb:PutItem",
3157
+ "dynamodb:UpdateItem",
3158
+ "dynamodb:Query",
3159
+ "dynamodb:TransactWriteItems",
3160
+ "dynamodb:ConditionCheckItem"
3161
+ ],
3162
+ resources: [this.table.tableArn]
3163
+ }));
3164
+ apiRole.addToPolicy(new iam8.PolicyStatement({
3165
+ sid: "PagesTableKey",
3166
+ actions: ["kms:Decrypt", "kms:Encrypt", "kms:GenerateDataKey", "kms:DescribeKey"],
3167
+ resources: [props.dataKey.keyArn]
3168
+ }));
3169
+ apiRole.addToPolicy(new iam8.PolicyStatement({
3170
+ sid: "PagesObjects",
3171
+ actions: ["s3:GetObject", "s3:PutObject"],
3172
+ resources: [this.bucket.arnForObjects("pages/*"), this.bucket.arnForObjects("uploads/*")]
3173
+ }));
3174
+ this.apiFunction = new lambda3.Function(this, "PagesApiFunction", {
3175
+ functionName: names.pagesApiFunctionName,
3176
+ runtime: lambda3.Runtime.NODEJS_22_X,
3177
+ handler: "index.handler",
3178
+ code: lambdaCode(this, "pages-api"),
3179
+ role: apiRole,
3180
+ timeout: cdk7.Duration.seconds(300),
3181
+ memorySize: 1024,
3182
+ environment: {
3183
+ FOUNDATION_PAGES_TABLE_NAME: this.table.tableName,
3184
+ FOUNDATION_PAGES_BUCKET_NAME: this.bucket.bucketName,
3185
+ FOUNDATION_PAGES_BASE_URL: baseUrl,
3186
+ FOUNDATION_SLACK_TEAM_ID: instance.slack.teamId,
3187
+ FOUNDATION_SLACK_SIGNING_SECRET_ID: props.secrets.signing.secretName
3188
+ }
3189
+ });
3190
+ new cdk7.CfnOutput(this, "PagesApiFunctionArn", { value: this.apiFunction.functionArn });
3191
+ new cdk7.CfnOutput(this, "PagesBaseUrl", { value: baseUrl });
3192
+ new cdk7.CfnOutput(this, "PagesDistributionDomainName", {
3193
+ value: this.distribution.distributionDomainName
3194
+ });
3195
+ new cdk7.CfnOutput(this, "PagesAuthRedirectUrl", {
3196
+ description: "Slack OAuth redirect URL for Sign in with Slack on the pages domain",
3197
+ value: `${baseUrl}/_auth/callback`
3198
+ });
3199
+ new cdk7.CfnOutput(this, "PagesDefaultAuthRedirectUrl", {
3200
+ value: `${defaultUrl}/_auth/callback`
3201
+ });
3202
+ new cdk7.CfnOutput(this, "PagesCustomDomainAttached", {
3203
+ value: domain === undefined ? "false" : "true"
3204
+ });
3205
+ if (domain !== undefined)
3206
+ new cdk7.CfnOutput(this, "PagesCustomDomain", { value: domain });
3207
+ if (zone instanceof route532.PublicHostedZone) {
3208
+ new cdk7.CfnOutput(this, "PagesHostedZoneId", { value: zone.hostedZoneId });
3209
+ new cdk7.CfnOutput(this, "PagesNameServers", {
3210
+ description: `NS records to add for ${zoneDomain} at the company's DNS provider`,
3211
+ value: cdk7.Fn.join(",", zone.hostedZoneNameServers ?? [])
3212
+ });
3213
+ }
3214
+ }
3215
+ }
3216
+ function secretArnPattern3(secret) {
3217
+ return secret.secretFullArn ?? `${secret.secretArn}-??????`;
3218
+ }
3219
+
3220
+ // src/stacks/pipeline-stack.ts
3221
+ import * as cdk8 from "aws-cdk-lib";
2684
3222
  import * as codebuild from "aws-cdk-lib/aws-codebuild";
2685
3223
  import * as codepipeline from "aws-cdk-lib/aws-codepipeline";
2686
3224
  import * as actions from "aws-cdk-lib/aws-codepipeline-actions";
2687
- import * as iam8 from "aws-cdk-lib/aws-iam";
3225
+ import * as iam9 from "aws-cdk-lib/aws-iam";
2688
3226
  import * as sns3 from "aws-cdk-lib/aws-sns";
2689
3227
  import * as subscriptions from "aws-cdk-lib/aws-sns-subscriptions";
2690
3228
  var DEFAULT_BUILDSPEC_PATH = "buildspec.yml";
2691
- var BUILD_TIMEOUT = cdk7.Duration.minutes(60);
3229
+ var BUILD_TIMEOUT = cdk8.Duration.minutes(60);
2692
3230
 
2693
- class FoundationPipeline extends cdk7.Stack {
3231
+ class FoundationPipeline extends cdk8.Stack {
2694
3232
  pipeline;
2695
3233
  buildRole;
2696
3234
  constructor(scope, id, props) {
@@ -2701,11 +3239,11 @@ class FoundationPipeline extends cdk7.Stack {
2701
3239
  if (deploy.connectionArn === undefined)
2702
3240
  throw new Error(`instance ${props.instance.name}: deploy.connectionArn is required for a pipeline`);
2703
3241
  const [owner = "", repo = ""] = props.instance.github.repo.split("/");
2704
- this.buildRole = new iam8.Role(this, "BuildRole", {
3242
+ this.buildRole = new iam9.Role(this, "BuildRole", {
2705
3243
  description: `CodePipeline deployer for ${props.instance.name}`,
2706
- assumedBy: new iam8.ServicePrincipal("codebuild.amazonaws.com"),
3244
+ assumedBy: new iam9.ServicePrincipal("codebuild.amazonaws.com"),
2707
3245
  inlinePolicies: {
2708
- FoundationDeploy: new iam8.PolicyDocument({
3246
+ FoundationDeploy: new iam9.PolicyDocument({
2709
3247
  statements: deployStatements(this, names, { bucket: props.bucket, dataKey: props.dataKey }, props.instance)
2710
3248
  })
2711
3249
  }
@@ -2799,11 +3337,11 @@ class FoundationPipeline extends cdk7.Stack {
2799
3337
  this.pipeline.onStateChange("OnFailure", {
2800
3338
  description: `${props.instance.name} deploy failed`,
2801
3339
  eventPattern: { detail: { state: ["FAILED"] } },
2802
- target: new cdk7.aws_events_targets.SnsTopic(failureTopic)
3340
+ target: new cdk8.aws_events_targets.SnsTopic(failureTopic)
2803
3341
  });
2804
- new cdk7.CfnOutput(this, "PipelineName", { value: this.pipeline.pipelineName });
2805
- new cdk7.CfnOutput(this, "BuildRoleArn", { value: this.buildRole.roleArn });
3342
+ new cdk8.CfnOutput(this, "PipelineName", { value: this.pipeline.pipelineName });
3343
+ new cdk8.CfnOutput(this, "BuildRoleArn", { value: this.buildRole.roleArn });
2806
3344
  }
2807
3345
  }
2808
3346
 
2809
- export { FoundationAgent, FoundationApi, deployStatements, FoundationCi, FoundationData, FoundationNetwork, NewsletterStack, DEFAULT_BUILDSPEC_PATH, BUILD_TIMEOUT, FoundationPipeline };
3347
+ export { FoundationAgent, FoundationApi, deployStatements, FoundationCi, FoundationData, FoundationNetwork, NewsletterStack, FoundationPages, DEFAULT_BUILDSPEC_PATH, BUILD_TIMEOUT, FoundationPipeline };
package/dist/src/index.js CHANGED
@@ -6,10 +6,11 @@ import {
6
6
  FoundationCi,
7
7
  FoundationData,
8
8
  FoundationNetwork,
9
+ FoundationPages,
9
10
  FoundationPipeline,
10
11
  NewsletterStack,
11
12
  deployStatements
12
- } from "../chunk-83781ggk.js";
13
+ } from "../chunk-rq4dc5ns.js";
13
14
  import {
14
15
  BUN_VERSION,
15
16
  LAMBDA_ENTRY_POINTS,
@@ -42,7 +43,7 @@ import {
42
43
  skillsKey,
43
44
  skipBundle,
44
45
  verifyManifest
45
- } from "../chunk-7qn00ny1.js";
46
+ } from "../chunk-9rasrppa.js";
46
47
  export {
47
48
  BUILD_TIMEOUT,
48
49
  BUN_VERSION,
@@ -52,6 +53,7 @@ export {
52
53
  FoundationCi,
53
54
  FoundationData,
54
55
  FoundationNetwork,
56
+ FoundationPages,
55
57
  FoundationPipeline,
56
58
  LAMBDA_ENTRY_POINTS,
57
59
  LIVE_ENDPOINT_NAME,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deployfoundation/foundation-deploy",
3
- "version": "0.2.15",
3
+ "version": "0.2.16",
4
4
  "description": "Deploys one Foundation instance from a published, signed release: the CDK app and the deploy tool.",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,
package/src/artifacts.ts CHANGED
@@ -66,6 +66,8 @@ export const LAMBDA_ENTRY_POINTS = {
66
66
  "crm-proxy": { entry: "packages/capability-crm/src/proxy/handler.ts" },
67
67
  "upwork-proxy": { entry: "packages/capability-upwork/src/proxy/handler.ts" },
68
68
  "etrade-proxy": { entry: "packages/capability-etrade/src/proxy/handler.ts" },
69
+ "pages-api": { entry: "packages/capability-pages/src/proxy/handler.ts" },
70
+ "pages-auth": { entry: "packages/capability-pages/src/auth/lambda.ts" },
69
71
  "browser-proxy": { entry: "packages/gateway/src/browser-proxy.ts", playwright: true },
70
72
  "newsletter-public": { entry: "packages/newsletter/src/lambda-public.ts" },
71
73
  "newsletter-campaign": { entry: "packages/newsletter/src/lambda-campaign.ts" },
package/src/deploy/cli.ts CHANGED
@@ -149,6 +149,9 @@ export async function main(argv: readonly string[] = process.argv.slice(2)): Pro
149
149
  ...(process.env.INTERACTIVE_REQUEST_URL === undefined
150
150
  ? {}
151
151
  : { INTERACTIVE_REQUEST_URL: process.env.INTERACTIVE_REQUEST_URL }),
152
+ ...(process.env.PAGES_AUTH_REDIRECT_URL === undefined
153
+ ? {}
154
+ : { PAGES_AUTH_REDIRECT_URL: process.env.PAGES_AUTH_REDIRECT_URL }),
152
155
  }),
153
156
  ),
154
157
  );