@ts-cloud/core 0.16.17 → 0.16.18

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.
@@ -23,8 +23,15 @@
23
23
  * }
24
24
  * ```
25
25
  *
26
+ * {@link validateOriginGroups} checks a whole distribution config the same way
27
+ * CloudFront does, so a distribution assembled by hand fails at template
28
+ * generation instead of half way through a CloudFormation deploy.
29
+ *
30
+ * Verified against a live distribution (S3 origins in two regions, failover on
31
+ * a missing object and on a primary that denies every read) for
32
+ * stacksjs/stacks#1159.
33
+ *
26
34
  * @see https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/high_availability_origin_failover.html
27
- * @experimental Built against the AWS documentation; not yet exercised against a live distribution.
28
35
  */
29
36
  /** Every status code CloudFront accepts as a failover criterion. */
30
37
  export declare const CLOUDFRONT_FAILOVER_STATUS_CODES: readonly number[];
@@ -38,6 +45,27 @@ export declare const DEFAULT_FAILOVER_STATUS_CODES: readonly number[];
38
45
  export declare const ORIGIN_GROUP_ALLOWED_METHODS: readonly string[];
39
46
  /** Suffix appended to the primary origin id to name its origin group. */
40
47
  export declare const ORIGIN_GROUP_ID_SUFFIX = "-failover-group";
48
+ /**
49
+ * How CloudFront picks the origin it asks first.
50
+ *
51
+ * - `default`: always the primary (the first member), falling back to the
52
+ * secondary on a failover status code.
53
+ * - `media-quality-based`: CloudFront scores both origins and asks the better
54
+ * one first. Only valid when both members are AWS Elemental MediaPackage v2
55
+ * endpoints.
56
+ */
57
+ export type OriginGroupSelectionCriteria = 'default' | 'media-quality-based';
58
+ export declare const ORIGIN_GROUP_SELECTION_CRITERIA: readonly OriginGroupSelectionCriteria[];
59
+ /** `ConnectionAttempts` CloudFront accepts on an origin (its default is 3). */
60
+ export declare const ORIGIN_CONNECTION_ATTEMPTS: {
61
+ readonly min: 1;
62
+ readonly max: 3;
63
+ };
64
+ /** `ConnectionTimeout`, in seconds, CloudFront accepts on an origin (its default is 10). */
65
+ export declare const ORIGIN_CONNECTION_TIMEOUT: {
66
+ readonly min: 1;
67
+ readonly max: 10;
68
+ };
41
69
  export interface OriginGroupOptions {
42
70
  /** Id of the origin CloudFront asks first. */
43
71
  primaryOriginId: string;
@@ -47,6 +75,11 @@ export interface OriginGroupOptions {
47
75
  statusCodes?: readonly number[];
48
76
  /** Origin group id, which cache behaviors use as their `TargetOriginId`. @default `<primaryOriginId>-failover-group` */
49
77
  groupId?: string;
78
+ /**
79
+ * How CloudFront picks the origin it asks first. Omitted from the template
80
+ * unless set, which CloudFront treats as `default`.
81
+ */
82
+ selectionCriteria?: OriginGroupSelectionCriteria;
50
83
  }
51
84
  export interface CloudFrontOriginGroup {
52
85
  Id: string;
@@ -62,6 +95,14 @@ export interface CloudFrontOriginGroup {
62
95
  OriginId: string;
63
96
  }>;
64
97
  };
98
+ SelectionCriteria?: OriginGroupSelectionCriteria;
99
+ }
100
+ /** Connection tuning for one origin. Unset keys keep CloudFront's defaults (3 attempts, 10 seconds). */
101
+ export interface OriginConnectionOptions {
102
+ /** Times CloudFront tries to connect to the origin, 1-3. */
103
+ connectionAttempts?: number;
104
+ /** Seconds CloudFront waits to establish a connection, 1-10. */
105
+ connectionTimeout?: number;
65
106
  }
66
107
  export interface CloudFrontOriginGroups {
67
108
  Quantity: number;
@@ -76,6 +117,22 @@ export interface CloudFrontOriginGroups {
76
117
  export declare function resolveFailoverStatusCodes(statusCodes?: readonly number[]): number[];
77
118
  /** Build one origin group (primary first, secondary second). */
78
119
  export declare function buildOriginGroup(options: OriginGroupOptions): CloudFrontOriginGroup;
120
+ /**
121
+ * Validate an origin's connection tuning and return it in template form
122
+ * (`ConnectionAttempts` / `ConnectionTimeout`), holding only the keys that were
123
+ * set so an untuned origin's template is unchanged.
124
+ *
125
+ * Lowering these on the primary is how failover gets faster: by default
126
+ * CloudFront spends up to 30 seconds (3 attempts of 10 seconds) on an
127
+ * unreachable primary before it asks the secondary.
128
+ *
129
+ * @param where Names the origin in the error, e.g. `infrastructure.cdn.main`.
130
+ * @throws when a value is not a whole number in CloudFront's range.
131
+ */
132
+ export declare function resolveOriginConnection(options: OriginConnectionOptions | undefined, where?: string): {
133
+ ConnectionAttempts?: number;
134
+ ConnectionTimeout?: number;
135
+ };
79
136
  /** Build a distribution's `OriginGroups` block holding a single failover group. */
80
137
  export declare function buildOriginGroups(options: OriginGroupOptions): CloudFrontOriginGroups;
81
138
  /**
@@ -95,3 +152,26 @@ export declare function assertOriginGroupMethods(allowedMethods: readonly string
95
152
  * S3 *website* endpoints only speak HTTP and are custom origins.
96
153
  */
97
154
  export declare function isS3RestEndpoint(domainName: string): boolean;
155
+ /**
156
+ * Check every origin group in a distribution config against the rules
157
+ * CloudFront enforces, and throw a message that names the broken piece.
158
+ *
159
+ * Accepts the CloudFormation `DistributionConfig` (arrays) and the CloudFront
160
+ * API's (`{ Quantity, Items }`), and skips comparisons on values that are
161
+ * CloudFormation intrinsics. Checked:
162
+ *
163
+ * - each group has exactly two members, both existing origins, and distinct;
164
+ * - group ids are unique and do not shadow an origin id;
165
+ * - failover status codes are ones CloudFront accepts, and `Quantity` matches;
166
+ * - `SelectionCriteria` is `default` or `media-quality-based`, the latter only
167
+ * between MediaPackage v2 origins;
168
+ * - every cache behavior (default and path) that targets a group allows only
169
+ * GET, HEAD and OPTIONS;
170
+ * - every cache behavior targets an origin or group that exists;
171
+ * - every origin's `ConnectionAttempts` (1-3) and `ConnectionTimeout` (1-10).
172
+ *
173
+ * A distribution with no origin groups passes the group checks trivially.
174
+ *
175
+ * @param where Names the distribution in errors, e.g. `infrastructure.cdn.main`.
176
+ */
177
+ export declare function validateOriginGroups(distributionConfig: Record<string, any>, where?: string): void;
@@ -1,5 +1,6 @@
1
1
  import type { CloudFrontDistribution, CloudFrontOriginAccessControl, IAMRole, LambdaFunction } from '@ts-cloud/aws-types';
2
2
  import type { EnvironmentType } from '../types';
3
+ import type { OriginConnectionOptions, OriginGroupSelectionCriteria } from './cdn-failover';
3
4
  export interface DistributionOptions {
4
5
  slug: string;
5
6
  environment: EnvironmentType;
@@ -14,15 +15,14 @@ export interface DistributionOptions {
14
15
  /**
15
16
  * Secondary origin CloudFront fails over to when the primary errors.
16
17
  * Adds an origin group and points the default cache behavior at it.
17
- * @experimental See {@link CDN.addOriginFailover}.
18
+ * See {@link CDN.addOriginFailover}.
18
19
  */
19
20
  failoverOrigin?: FailoverOriginConfig;
20
21
  }
21
22
  /**
22
23
  * Secondary origin for CloudFront origin failover.
23
- * @experimental Built against the AWS documentation; not yet exercised against a live distribution.
24
24
  */
25
- export interface FailoverOriginConfig {
25
+ export interface FailoverOriginConfig extends OriginConnectionOptions {
26
26
  /** Domain of the secondary origin, e.g. a replica bucket in another region. */
27
27
  domainName: string;
28
28
  /** Origin kind. @default 's3' for an S3 REST endpoint, otherwise 'custom' */
@@ -35,8 +35,17 @@ export interface FailoverOriginConfig {
35
35
  * 416, 429, 500, 502, 503, 504. @default [500, 502, 503, 504]
36
36
  */
37
37
  statusCodes?: number[];
38
+ /** How CloudFront picks the origin it asks first. @default CloudFront's 'default' (primary first) */
39
+ selectionCriteria?: OriginGroupSelectionCriteria;
40
+ /**
41
+ * Connection tuning for the *primary* origin. Lower values make failover
42
+ * faster: CloudFront otherwise spends up to 30 seconds (3 attempts of 10
43
+ * seconds) on an unreachable primary. `connectionAttempts` and
44
+ * `connectionTimeout` on this object tune the secondary.
45
+ */
46
+ primary?: OriginConnectionOptions;
38
47
  }
39
- export interface OriginConfig {
48
+ export interface OriginConfig extends OriginConnectionOptions {
40
49
  type?: 's3' | 'alb' | 'custom';
41
50
  id?: string;
42
51
  originId?: string;
@@ -79,9 +88,8 @@ export declare class CDN {
79
88
  *
80
89
  * CloudFront only fails over GET, HEAD and OPTIONS, and rejects an origin
81
90
  * group behind a cache behavior that allows writes, so this throws if the
82
- * default cache behavior allows any other method.
83
- *
84
- * @experimental Built against the AWS documentation; not yet exercised against a live distribution.
91
+ * default cache behavior allows any other method. The finished config is
92
+ * checked with {@link validateOriginGroups}.
85
93
  */
86
94
  static addOriginFailover(distribution: CloudFrontDistribution, failover: FailoverOriginConfig): CloudFrontDistribution;
87
95
  /**
@@ -6,6 +6,7 @@ export * from './storage';
6
6
  export * from './registry';
7
7
  export * from './cdn';
8
8
  export * from './cdn-failover';
9
+ export * from './storage-failover';
9
10
  export * from './dns';
10
11
  export * from './security';
11
12
  export * from './compute';
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Cross-region failover for a website bucket (stacksjs/stacks#1159).
3
+ *
4
+ * The CloudFront half is an origin group (see `cdn-failover.ts`): the primary
5
+ * bucket first, a replica bucket in another region second. This module holds
6
+ * the pure pieces the bucket half needs:
7
+ *
8
+ * - {@link resolveStorageFailover} turns `infrastructure.storage.<name>.failover`
9
+ * into a concrete plan (replica name and region, status codes, connection
10
+ * tuning) and rejects configs CloudFront or S3 would refuse later.
11
+ * - {@link buildReplicationRole} and {@link buildReplicationConfiguration}
12
+ * emit the IAM role and `ReplicationConfiguration` that copy every write to
13
+ * the replica.
14
+ * - {@link STORAGE_FAILOVER_METADATA_KEY} names the template `Metadata` entry
15
+ * that tells a deployer which replicas to create before the stack and grant
16
+ * CloudFront access to after it. The replica cannot live in the stack: a
17
+ * CloudFormation stack only creates buckets in its own region.
18
+ */
19
+ import type { StorageFailoverConfig } from '../types';
20
+ /**
21
+ * Failover codes for a bucket origin when none are given. An S3 origin behind
22
+ * origin access control answers a missing object with 403 (the distribution
23
+ * may not list the bucket), so 403 and 404 cover an object the primary lost;
24
+ * the 5xx codes cover the bucket or its region failing.
25
+ */
26
+ export declare const DEFAULT_STORAGE_FAILOVER_STATUS_CODES: readonly number[];
27
+ /** Template `Metadata` key listing the failover replicas a stack depends on. */
28
+ export declare const STORAGE_FAILOVER_METADATA_KEY = "TsCloud::StorageFailover";
29
+ /** Sid of the replica bucket-policy statement that lets the distribution read it. */
30
+ export declare const FAILOVER_REPLICA_POLICY_SID = "AllowCloudFrontFailoverRead";
31
+ /** One replica, as recorded in the template metadata and consumed by the deployer. */
32
+ export interface StorageFailoverReplica {
33
+ /** `infrastructure.storage` key. */
34
+ name: string;
35
+ primaryBucket: string;
36
+ primaryRegion: string;
37
+ replicaBucket: string;
38
+ replicaRegion: string;
39
+ /** Whether S3 replication copies writes to the replica (and so whether it must be versioned). */
40
+ replicate: boolean;
41
+ /** Stack output holding the distribution ARN the replica policy is scoped to. */
42
+ distributionArnOutput: string;
43
+ }
44
+ export interface ResolvedStorageFailover {
45
+ replicaBucket: string;
46
+ replicaRegion: string;
47
+ replicate: boolean;
48
+ statusCodes: number[];
49
+ primaryConnection: {
50
+ ConnectionAttempts?: number;
51
+ ConnectionTimeout?: number;
52
+ };
53
+ }
54
+ /**
55
+ * Resolve and validate a bucket's failover config.
56
+ *
57
+ * @param where Names the bucket in errors, e.g. `infrastructure.storage.public`.
58
+ * @throws when the region is missing, malformed or the primary's own region,
59
+ * when the replica name is not a valid bucket name (or collides with the
60
+ * primary), or when a status code or connection setting is out of range.
61
+ */
62
+ export declare function resolveStorageFailover(options: {
63
+ failover: StorageFailoverConfig;
64
+ primaryBucket: string;
65
+ primaryRegion: string;
66
+ where?: string;
67
+ }): ResolvedStorageFailover;
68
+ /** S3 REST endpoint of a bucket, the domain a CloudFront S3 origin uses. */
69
+ export declare function s3RegionalDomain(bucket: string, region: string): string;
70
+ /**
71
+ * IAM role S3 assumes to replicate `primaryBucket` into `replicaBucket`. The
72
+ * permissions are the minimum the S3 docs list for live replication,
73
+ * including delete markers.
74
+ */
75
+ export declare function buildReplicationRole(options: {
76
+ primaryBucket: string;
77
+ replicaBucket: string;
78
+ roleName?: string;
79
+ }): any;
80
+ /**
81
+ * `ReplicationConfiguration` for the primary bucket: one rule copying every
82
+ * object, and its deletes, to the replica.
83
+ *
84
+ * @param roleArn The replication role's ARN, usually `{ 'Fn::GetAtt': [roleLogicalId, 'Arn'] }`.
85
+ */
86
+ export declare function buildReplicationConfiguration(options: {
87
+ replicaBucket: string;
88
+ roleArn: unknown;
89
+ }): any;
90
+ /**
91
+ * The replica bucket-policy statement that lets one distribution, through
92
+ * origin access control, read the replica.
93
+ */
94
+ export declare function buildFailoverReplicaPolicyStatement(options: {
95
+ replicaBucket: string;
96
+ distributionArn: string;
97
+ }): any;
98
+ /** Read the failover replicas a generated template depends on (empty when none). */
99
+ export declare function storageFailoverReplicasFromTemplate(template: unknown): StorageFailoverReplica[];
@@ -18,6 +18,10 @@ export declare class TemplateBuilder {
18
18
  * Add an output to the template
19
19
  */
20
20
  addOutput(name: string, output: NonNullable<CloudFormationTemplate['Outputs']>[string]): this;
21
+ /**
22
+ * Set a template-level `Metadata` entry
23
+ */
24
+ addMetadata(key: string, value: unknown): this;
21
25
  /**
22
26
  * Check if a resource already exists in the template
23
27
  */
package/dist/types.d.ts CHANGED
@@ -2194,6 +2194,18 @@ export interface StorageItemConfig {
2194
2194
  * Defaults to directory, which matches most blog/static-export outputs.
2195
2195
  */
2196
2196
  pathRewriteStyle?: 'directory' | 'flat';
2197
+ /**
2198
+ * CloudFront origin failover to a replica of this bucket in another region.
2199
+ * Only applies to a website bucket served through CloudFront. Off unless set.
2200
+ *
2201
+ * @example
2202
+ * ```ts
2203
+ * storage: {
2204
+ * public: { website: true, failover: { region: 'us-west-2' } },
2205
+ * }
2206
+ * ```
2207
+ */
2208
+ failover?: StorageFailoverConfig;
2197
2209
  /**
2198
2210
  * Whether this bucket serves a single-page application (SPA).
2199
2211
  * When true: 403/404 errors return index.html with status 200 (for client-side routing).
@@ -4211,6 +4223,53 @@ export interface DatabaseItemConfig {
4211
4223
  }>;
4212
4224
  }>;
4213
4225
  }
4226
+ /** Object form of {@link CdnItemConfig.failoverOrigin}. */
4227
+ export interface CdnFailoverOriginConfig {
4228
+ /** Domain of the secondary origin. */
4229
+ domain: string;
4230
+ /** Path CloudFront prepends to requests sent to the secondary. */
4231
+ originPath?: string;
4232
+ /** Times CloudFront tries to connect to the secondary, 1-3. */
4233
+ connectionAttempts?: number;
4234
+ /** Seconds CloudFront waits to connect to the secondary, 1-10. */
4235
+ connectionTimeout?: number;
4236
+ }
4237
+ /**
4238
+ * CloudFront origin failover for a website bucket: a replica bucket in a
4239
+ * second region that CloudFront serves from when the primary bucket fails.
4240
+ *
4241
+ * ts-cloud creates the replica (versioned, private, encrypted) in
4242
+ * {@link region} before the stack deploys, replicates every write to it with
4243
+ * S3 replication, copies objects that predate replication across, and lets
4244
+ * the distribution read it through the same origin access control as the
4245
+ * primary. The replica lives outside the CloudFormation stack, because a stack
4246
+ * can only create buckets in its own region, so removing the stack leaves it
4247
+ * behind.
4248
+ */
4249
+ export interface StorageFailoverConfig {
4250
+ /** Region of the replica bucket. Must differ from the stack's region. */
4251
+ region: string;
4252
+ /** Replica bucket name. @default `<primary bucket>-<region>` */
4253
+ bucket?: string;
4254
+ /**
4255
+ * Primary status codes that send a request to the replica. CloudFront
4256
+ * accepts 400, 403, 404, 416, 429, 500, 502, 503 and 504.
4257
+ * @default [403, 404, 500, 502, 503, 504] - an S3 origin behind origin access
4258
+ * control answers a missing object with 403, so 403 and 404 cover an object
4259
+ * the primary lost, and the 5xx codes cover the bucket or region failing.
4260
+ */
4261
+ statusCodes?: number[];
4262
+ /** Times CloudFront tries to connect to the primary bucket, 1-3. @default CloudFront's 3 */
4263
+ connectionAttempts?: number;
4264
+ /** Seconds CloudFront waits to connect to the primary bucket, 1-10. @default CloudFront's 10 */
4265
+ connectionTimeout?: number;
4266
+ /**
4267
+ * Replicate the primary bucket to the replica with S3 replication (and turn
4268
+ * on the versioning that requires). Set false to keep the replica in sync
4269
+ * yourself. @default true
4270
+ */
4271
+ replicate?: boolean;
4272
+ }
4214
4273
  export interface CdnItemConfig {
4215
4274
  origin?: string;
4216
4275
  customDomain?: string | {
@@ -4230,6 +4289,15 @@ export interface CdnItemConfig {
4230
4289
  originShield?: boolean;
4231
4290
  /** AWS region used by Origin Shield. Defaults to the deployment region. */
4232
4291
  originShieldRegion?: string;
4292
+ /**
4293
+ * Times CloudFront tries to connect to {@link origin}, 1-3. CloudFront's
4294
+ * default is 3. Lower it, with {@link connectionTimeout}, to fail over
4295
+ * faster: an unreachable primary otherwise costs up to 30 seconds before
4296
+ * CloudFront asks {@link failoverOrigin}.
4297
+ */
4298
+ connectionAttempts?: number;
4299
+ /** Seconds CloudFront waits to connect to {@link origin}, 1-10. CloudFront's default is 10. */
4300
+ connectionTimeout?: number;
4233
4301
  /**
4234
4302
  * Secondary origin CloudFront fails over to when {@link origin} errors,
4235
4303
  * e.g. a replica bucket in another region. Adds a CloudFront origin group
@@ -4237,16 +4305,17 @@ export interface CdnItemConfig {
4237
4305
  * CloudFront only fails over GET, HEAD and OPTIONS requests. An S3 REST
4238
4306
  * endpoint becomes an S3 origin; any other host an HTTPS-only custom origin.
4239
4307
  *
4240
- * @experimental Generated from the AWS documentation; not yet verified
4241
- * against a live distribution.
4308
+ * A string is the secondary's domain. The object form also tunes the
4309
+ * secondary's connection settings and sets its origin path.
4242
4310
  */
4243
- failoverOrigin?: string;
4311
+ failoverOrigin?: string | CdnFailoverOriginConfig;
4244
4312
  /**
4245
4313
  * Status codes from {@link origin} that trigger failover to
4246
4314
  * {@link failoverOrigin}. CloudFront accepts 400, 403, 404, 416, 429, 500,
4247
- * 502, 503 and 504; anything else throws at template generation.
4315
+ * 502, 503 and 504; anything else throws at template generation. Add 403
4316
+ * and 404 to fail over on an object the primary does not have (an S3 origin
4317
+ * behind origin access control answers a missing object with 403).
4248
4318
  * @default [500, 502, 503, 504]
4249
- * @experimental
4250
4319
  */
4251
4320
  failoverStatusCodes?: number[];
4252
4321
  /**
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ts-cloud/core",
3
3
  "type": "module",
4
- "version": "0.16.17",
4
+ "version": "0.16.18",
5
5
  "description": "Core CloudFormation generation library for ts-cloud",
6
6
  "author": "Chris Breuer <chris@stacksjs.com>",
7
7
  "license": "MIT",
@@ -31,7 +31,7 @@
31
31
  "typecheck": "tsc --noEmit"
32
32
  },
33
33
  "dependencies": {
34
- "@ts-cloud/aws-types": "0.16.17"
34
+ "@ts-cloud/aws-types": "0.16.18"
35
35
  },
36
36
  "devDependencies": {
37
37
  "typescript": "^7.0.2"