@hasna/uptime 0.1.17 → 0.1.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.
package/CHANGELOG.md CHANGED
@@ -6,6 +6,31 @@ project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.1.19] - 2026-06-28
10
+
11
+ ### Added
12
+
13
+ - Added optional CloudFront origin verification header binding to the AWS
14
+ Terraform module. When enabled, CloudFront sends a private origin header and
15
+ the ALB listener returns `403` for direct origin requests that do not present
16
+ the matching value.
17
+
18
+ ### Changed
19
+
20
+ - Updated AWS runbooks, deployment metadata, and cloud source-of-truth docs to
21
+ distinguish CloudFront prefix-list narrowing from distribution-bound origin
22
+ access.
23
+
24
+ ## [0.1.18] - 2026-06-28
25
+
26
+ ### Changed
27
+
28
+ - Added explicit ECS container health checks to the AWS Terraform module task
29
+ definitions. The web task checks `/health`, while disabled non-web roles use
30
+ a hosted-environment sanity check until their worker entrypoints are enabled.
31
+ - Updated cloud planning and AWS deployment docs to keep the zero-count
32
+ deployment status clear while the private root is repinned and verified.
33
+
9
34
  ## [0.1.17] - 2026-06-28
10
35
 
11
36
  ### Fixed
package/dist/cli/index.js CHANGED
@@ -6943,7 +6943,7 @@ function buildAwsDeploymentPlan(options = {}) {
6943
6943
  const image = clean(options.image, `${imageRepositoryUri}@sha256:<image-digest>`);
6944
6944
  const evidenceBucket = clean(options.evidenceBucket, `hasna-${stage}-${prefix}-evidence`);
6945
6945
  const hostedSqliteDbPath = clean(options.hostedSqliteDbPath, DEFAULT_HOSTED_SQLITE_DB);
6946
- const runtimePackageVersion = clean(options.runtimePackageVersion, "0.1.17");
6946
+ const runtimePackageVersion = clean(options.runtimePackageVersion, "0.1.19");
6947
6947
  const protectedAccessMode = options.protectedAccessMode ?? DEFAULT_PROTECTED_ACCESS_MODE;
6948
6948
  const protectedAccessUrl = protectedAccessMode === "cloudfront_default_domain" ? "https://<cloudfront-domain>" : `https://${hostname}`;
6949
6949
  const cluster = `${prefix}-${stage}`;
@@ -6986,7 +6986,7 @@ function buildAwsDeploymentPlan(options = {}) {
6986
6986
  ];
6987
6987
  return {
6988
6988
  kind: "open-uptime.aws-deployment-plan",
6989
- version: 3,
6989
+ version: 4,
6990
6990
  generatedAt: new Date().toISOString(),
6991
6991
  status: "blocked",
6992
6992
  canApply: false,
@@ -7011,6 +7011,17 @@ function buildAwsDeploymentPlan(options = {}) {
7011
7011
  protectedAccessMode,
7012
7012
  edgeDistribution: protectedAccessMode === "cloudfront_default_domain" ? `${prefix}-${stage}-edge` : undefined,
7013
7013
  protectedAccessUrl,
7014
+ originVerification: protectedAccessMode === "cloudfront_default_domain" ? {
7015
+ mode: "cloudfront_origin_header",
7016
+ requiredBeforeScaleUp: true,
7017
+ headerName: "X-Open-Uptime-Origin-Verify",
7018
+ valueStoredInTerraformState: true,
7019
+ stateAccessWarning: "The origin verification header value is sensitive but is stored in encrypted Terraform state and CloudFront/ALB configuration; restrict state, plan, CloudFront distribution-read, and ELB rule-read access."
7020
+ } : {
7021
+ mode: "alb_tls",
7022
+ requiredBeforeScaleUp: false,
7023
+ valueStoredInTerraformState: false
7024
+ },
7014
7025
  targetGroups: [`${prefix}-${stage}-web-tg`],
7015
7026
  securityGroups: [
7016
7027
  `${prefix}-${stage}-alb-sg`,
@@ -7058,7 +7069,7 @@ function buildAwsDeploymentPlan(options = {}) {
7058
7069
  `Infra PR must declare CodeBuild image builder ${prefix}-${stage}-image-builder for @hasna/uptime@${runtimePackageVersion}.`,
7059
7070
  `Infra PR must declare hardened S3 evidence bucket ${evidenceBucket} with KMS, versioning, lifecycle, and public access block.`,
7060
7071
  `Infra PR must declare encrypted EFS ${prefix}-${stage}-data with access point, mount targets, and AWS Backup plan.`,
7061
- protectedAccessMode === "cloudfront_default_domain" ? "Infra PR must declare CloudFront default-domain HTTPS edge, ALB HTTP listener restricted to CloudFront origin-facing ranges, ECS/Fargate cluster, target groups, security groups, IAM roles, CloudWatch log groups, and Secrets Manager refs." : `Infra PR must declare ECS/Fargate cluster ${cluster}, ALB HTTPS listener, target groups, security groups, IAM roles, CloudWatch log groups, and Secrets Manager refs.`,
7072
+ protectedAccessMode === "cloudfront_default_domain" ? "Infra PR must declare CloudFront default-domain HTTPS edge, ALB HTTP listener restricted to CloudFront origin-facing ranges, CloudFront-only origin verification header binding, ECS/Fargate cluster, target groups, security groups, IAM roles, CloudWatch log groups, and Secrets Manager refs." : `Infra PR must declare ECS/Fargate cluster ${cluster}, ALB HTTPS listener, target groups, security groups, IAM roles, CloudWatch log groups, and Secrets Manager refs.`,
7062
7073
  "Only apply the infra plan from the approved infrastructure repository after review evidence is attached."
7063
7074
  ],
7064
7075
  deploy: [
@@ -7067,7 +7078,7 @@ function buildAwsDeploymentPlan(options = {}) {
7067
7078
  "For the EFS SQLite bridge, do not run migration, scheduler, public-probe, or reporter tasks; keep them at desired count 0 until Postgres and cloud leases exist.",
7068
7079
  `Register task definitions for ${services.map((service) => service.name).join(", ")} using valueFrom secrets.`,
7069
7080
  `Update ECS services in cluster ${cluster} one component at a time through the approved deploy pipeline.`,
7070
- protectedAccessMode === "cloudfront_default_domain" ? "Use the CloudFront default HTTPS domain for first protected access; add custom DNS/certificate only after edge ownership is approved." : `Create Route53/edge record for ${hostname} only after ALB health checks pass and auth denial smokes succeed.`
7081
+ protectedAccessMode === "cloudfront_default_domain" ? "Use the CloudFront default HTTPS domain with origin verification header binding for first protected access; add custom DNS/certificate only after edge ownership is approved." : `Create Route53/edge record for ${hostname} only after ALB health checks pass and auth denial smokes succeed.`
7071
7082
  ],
7072
7083
  rollback: [
7073
7084
  "Keep previous task definition ARNs before each service update.",
@@ -7083,6 +7094,7 @@ function buildAwsDeploymentPlan(options = {}) {
7083
7094
  },
7084
7095
  blockers: [
7085
7096
  "The infrastructure owner repository was not found in this workspace.",
7097
+ protectedAccessMode === "cloudfront_default_domain" ? "CloudFront origin verification header binding must be enabled and direct-origin denial must be proven before web desired count is raised above 0." : "ALB HTTPS ingress policy and auth-denial smokes must be proven before web desired count is raised above 0.",
7086
7098
  "The EFS SQLite bridge is single-writer only: web target desired count is 1 and scheduler/public-probe/reporter targets remain 0 until Postgres and cloud leases exist.",
7087
7099
  "Hosted production auth/RBAC must replace broad static hosted-token operation before exposure.",
7088
7100
  "Public probe execution still needs cloud check-job leases wired to runHostedHttpCheck and live policy-decision log evidence.",
@@ -7092,7 +7104,7 @@ function buildAwsDeploymentPlan(options = {}) {
7092
7104
  "Infrastructure PR/synth/plan from the approved infra repository.",
7093
7105
  "CodeBuild image-builder run, container smoke, and immutable image digest.",
7094
7106
  "ECS task definitions using secrets.valueFrom only.",
7095
- "CloudFront-default-domain or ALB TLS auth-denial smokes, direct-origin denial evidence, and web alarm checks.",
7107
+ "CloudFront-default-domain origin-header config or ALB TLS auth-denial smokes, direct-origin denial evidence, and web alarm checks.",
7096
7108
  "Single-writer ECS evidence: one web task maximum and no scheduler/public-probe/reporter EFS mounts.",
7097
7109
  "EFS encryption, access point, mount-target, AWS Backup, and restore-drill evidence.",
7098
7110
  "S3 bucket KMS, versioning, lifecycle, and public-access-block evidence.",
@@ -7106,6 +7118,7 @@ function buildAwsDeploymentPlan(options = {}) {
7106
7118
  "This plan generator does not call AWS.",
7107
7119
  "Blocked plan output intentionally avoids copy-pastable AWS mutation commands.",
7108
7120
  "Default protected access uses CloudFront's HTTPS default domain so first deploy is not blocked on custom DNS or ACM.",
7121
+ "CloudFront default-domain mode still requires origin verification header binding before live scale-up; the header value is sensitive state/config material, not public documentation.",
7109
7122
  "Hosted runtime uses explicit EFS-backed SQLite at HASNA_UPTIME_HOSTED_SQLITE_DB until the async Postgres adapter exists.",
7110
7123
  "Do not set HASNA_UPTIME_DATABASE_URL for hosted tasks until the Postgres adapter is implemented.",
7111
7124
  "Secrets are represented as secret names/refs and must be injected with valueFrom.",
@@ -7194,7 +7207,7 @@ function servicePlan(prefix, stage, role, desiredCount, image, workspaceId, secr
7194
7207
  taskRole: `${name}-task-role`,
7195
7208
  executionRole: `${prefix}-${stage}-execution-role`,
7196
7209
  logGroup: `/ecs/${name}`,
7197
- healthCommand: role === "web" ? "GET /health" : undefined,
7210
+ healthCommand: role === "web" ? "GET /health" : "hosted environment sanity check",
7198
7211
  environment: {
7199
7212
  HASNA_UPTIME_IMAGE: image,
7200
7213
  ...environment
@@ -24,7 +24,7 @@ export interface AwsDeploymentPlanOptions {
24
24
  }
25
25
  export interface AwsDeploymentPlan {
26
26
  kind: "open-uptime.aws-deployment-plan";
27
- version: 3;
27
+ version: 4;
28
28
  generatedAt: string;
29
29
  status: "blocked";
30
30
  canApply: false;
@@ -49,6 +49,13 @@ export interface AwsDeploymentPlan {
49
49
  protectedAccessMode: "cloudfront_default_domain" | "alb_https_cert";
50
50
  edgeDistribution?: string;
51
51
  protectedAccessUrl: string;
52
+ originVerification: {
53
+ mode: "cloudfront_origin_header" | "alb_tls";
54
+ requiredBeforeScaleUp: boolean;
55
+ headerName?: string;
56
+ valueStoredInTerraformState: boolean;
57
+ stateAccessWarning?: string;
58
+ };
52
59
  targetGroups: string[];
53
60
  securityGroups: string[];
54
61
  secrets: Record<string, string>;
@@ -1 +1 @@
1
- {"version":3,"file":"cloud-plan.d.ts","sourceRoot":"","sources":["../src/cloud-plan.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"cloud-plan.d.ts","sourceRoot":"","sources":["../src/cloud-plan.ts"],"names":[],"mappings":"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"}
@@ -21,7 +21,7 @@ function buildAwsDeploymentPlan(options = {}) {
21
21
  const image = clean(options.image, `${imageRepositoryUri}@sha256:<image-digest>`);
22
22
  const evidenceBucket = clean(options.evidenceBucket, `hasna-${stage}-${prefix}-evidence`);
23
23
  const hostedSqliteDbPath = clean(options.hostedSqliteDbPath, DEFAULT_HOSTED_SQLITE_DB);
24
- const runtimePackageVersion = clean(options.runtimePackageVersion, "0.1.17");
24
+ const runtimePackageVersion = clean(options.runtimePackageVersion, "0.1.19");
25
25
  const protectedAccessMode = options.protectedAccessMode ?? DEFAULT_PROTECTED_ACCESS_MODE;
26
26
  const protectedAccessUrl = protectedAccessMode === "cloudfront_default_domain" ? "https://<cloudfront-domain>" : `https://${hostname}`;
27
27
  const cluster = `${prefix}-${stage}`;
@@ -64,7 +64,7 @@ function buildAwsDeploymentPlan(options = {}) {
64
64
  ];
65
65
  return {
66
66
  kind: "open-uptime.aws-deployment-plan",
67
- version: 3,
67
+ version: 4,
68
68
  generatedAt: new Date().toISOString(),
69
69
  status: "blocked",
70
70
  canApply: false,
@@ -89,6 +89,17 @@ function buildAwsDeploymentPlan(options = {}) {
89
89
  protectedAccessMode,
90
90
  edgeDistribution: protectedAccessMode === "cloudfront_default_domain" ? `${prefix}-${stage}-edge` : undefined,
91
91
  protectedAccessUrl,
92
+ originVerification: protectedAccessMode === "cloudfront_default_domain" ? {
93
+ mode: "cloudfront_origin_header",
94
+ requiredBeforeScaleUp: true,
95
+ headerName: "X-Open-Uptime-Origin-Verify",
96
+ valueStoredInTerraformState: true,
97
+ stateAccessWarning: "The origin verification header value is sensitive but is stored in encrypted Terraform state and CloudFront/ALB configuration; restrict state, plan, CloudFront distribution-read, and ELB rule-read access."
98
+ } : {
99
+ mode: "alb_tls",
100
+ requiredBeforeScaleUp: false,
101
+ valueStoredInTerraformState: false
102
+ },
92
103
  targetGroups: [`${prefix}-${stage}-web-tg`],
93
104
  securityGroups: [
94
105
  `${prefix}-${stage}-alb-sg`,
@@ -136,7 +147,7 @@ function buildAwsDeploymentPlan(options = {}) {
136
147
  `Infra PR must declare CodeBuild image builder ${prefix}-${stage}-image-builder for @hasna/uptime@${runtimePackageVersion}.`,
137
148
  `Infra PR must declare hardened S3 evidence bucket ${evidenceBucket} with KMS, versioning, lifecycle, and public access block.`,
138
149
  `Infra PR must declare encrypted EFS ${prefix}-${stage}-data with access point, mount targets, and AWS Backup plan.`,
139
- protectedAccessMode === "cloudfront_default_domain" ? "Infra PR must declare CloudFront default-domain HTTPS edge, ALB HTTP listener restricted to CloudFront origin-facing ranges, ECS/Fargate cluster, target groups, security groups, IAM roles, CloudWatch log groups, and Secrets Manager refs." : `Infra PR must declare ECS/Fargate cluster ${cluster}, ALB HTTPS listener, target groups, security groups, IAM roles, CloudWatch log groups, and Secrets Manager refs.`,
150
+ protectedAccessMode === "cloudfront_default_domain" ? "Infra PR must declare CloudFront default-domain HTTPS edge, ALB HTTP listener restricted to CloudFront origin-facing ranges, CloudFront-only origin verification header binding, ECS/Fargate cluster, target groups, security groups, IAM roles, CloudWatch log groups, and Secrets Manager refs." : `Infra PR must declare ECS/Fargate cluster ${cluster}, ALB HTTPS listener, target groups, security groups, IAM roles, CloudWatch log groups, and Secrets Manager refs.`,
140
151
  "Only apply the infra plan from the approved infrastructure repository after review evidence is attached."
141
152
  ],
142
153
  deploy: [
@@ -145,7 +156,7 @@ function buildAwsDeploymentPlan(options = {}) {
145
156
  "For the EFS SQLite bridge, do not run migration, scheduler, public-probe, or reporter tasks; keep them at desired count 0 until Postgres and cloud leases exist.",
146
157
  `Register task definitions for ${services.map((service) => service.name).join(", ")} using valueFrom secrets.`,
147
158
  `Update ECS services in cluster ${cluster} one component at a time through the approved deploy pipeline.`,
148
- protectedAccessMode === "cloudfront_default_domain" ? "Use the CloudFront default HTTPS domain for first protected access; add custom DNS/certificate only after edge ownership is approved." : `Create Route53/edge record for ${hostname} only after ALB health checks pass and auth denial smokes succeed.`
159
+ protectedAccessMode === "cloudfront_default_domain" ? "Use the CloudFront default HTTPS domain with origin verification header binding for first protected access; add custom DNS/certificate only after edge ownership is approved." : `Create Route53/edge record for ${hostname} only after ALB health checks pass and auth denial smokes succeed.`
149
160
  ],
150
161
  rollback: [
151
162
  "Keep previous task definition ARNs before each service update.",
@@ -161,6 +172,7 @@ function buildAwsDeploymentPlan(options = {}) {
161
172
  },
162
173
  blockers: [
163
174
  "The infrastructure owner repository was not found in this workspace.",
175
+ protectedAccessMode === "cloudfront_default_domain" ? "CloudFront origin verification header binding must be enabled and direct-origin denial must be proven before web desired count is raised above 0." : "ALB HTTPS ingress policy and auth-denial smokes must be proven before web desired count is raised above 0.",
164
176
  "The EFS SQLite bridge is single-writer only: web target desired count is 1 and scheduler/public-probe/reporter targets remain 0 until Postgres and cloud leases exist.",
165
177
  "Hosted production auth/RBAC must replace broad static hosted-token operation before exposure.",
166
178
  "Public probe execution still needs cloud check-job leases wired to runHostedHttpCheck and live policy-decision log evidence.",
@@ -170,7 +182,7 @@ function buildAwsDeploymentPlan(options = {}) {
170
182
  "Infrastructure PR/synth/plan from the approved infra repository.",
171
183
  "CodeBuild image-builder run, container smoke, and immutable image digest.",
172
184
  "ECS task definitions using secrets.valueFrom only.",
173
- "CloudFront-default-domain or ALB TLS auth-denial smokes, direct-origin denial evidence, and web alarm checks.",
185
+ "CloudFront-default-domain origin-header config or ALB TLS auth-denial smokes, direct-origin denial evidence, and web alarm checks.",
174
186
  "Single-writer ECS evidence: one web task maximum and no scheduler/public-probe/reporter EFS mounts.",
175
187
  "EFS encryption, access point, mount-target, AWS Backup, and restore-drill evidence.",
176
188
  "S3 bucket KMS, versioning, lifecycle, and public-access-block evidence.",
@@ -184,6 +196,7 @@ function buildAwsDeploymentPlan(options = {}) {
184
196
  "This plan generator does not call AWS.",
185
197
  "Blocked plan output intentionally avoids copy-pastable AWS mutation commands.",
186
198
  "Default protected access uses CloudFront's HTTPS default domain so first deploy is not blocked on custom DNS or ACM.",
199
+ "CloudFront default-domain mode still requires origin verification header binding before live scale-up; the header value is sensitive state/config material, not public documentation.",
187
200
  "Hosted runtime uses explicit EFS-backed SQLite at HASNA_UPTIME_HOSTED_SQLITE_DB until the async Postgres adapter exists.",
188
201
  "Do not set HASNA_UPTIME_DATABASE_URL for hosted tasks until the Postgres adapter is implemented.",
189
202
  "Secrets are represented as secret names/refs and must be injected with valueFrom.",
@@ -272,7 +285,7 @@ function servicePlan(prefix, stage, role, desiredCount, image, workspaceId, secr
272
285
  taskRole: `${name}-task-role`,
273
286
  executionRole: `${prefix}-${stage}-execution-role`,
274
287
  logGroup: `/ecs/${name}`,
275
- healthCommand: role === "web" ? "GET /health" : undefined,
288
+ healthCommand: role === "web" ? "GET /health" : "hosted environment sanity check",
276
289
  environment: {
277
290
  HASNA_UPTIME_IMAGE: image,
278
291
  ...environment
package/dist/index.js CHANGED
@@ -4349,7 +4349,7 @@ function buildAwsDeploymentPlan(options = {}) {
4349
4349
  const image = clean(options.image, `${imageRepositoryUri}@sha256:<image-digest>`);
4350
4350
  const evidenceBucket = clean(options.evidenceBucket, `hasna-${stage}-${prefix}-evidence`);
4351
4351
  const hostedSqliteDbPath = clean(options.hostedSqliteDbPath, DEFAULT_HOSTED_SQLITE_DB);
4352
- const runtimePackageVersion = clean(options.runtimePackageVersion, "0.1.17");
4352
+ const runtimePackageVersion = clean(options.runtimePackageVersion, "0.1.19");
4353
4353
  const protectedAccessMode = options.protectedAccessMode ?? DEFAULT_PROTECTED_ACCESS_MODE;
4354
4354
  const protectedAccessUrl = protectedAccessMode === "cloudfront_default_domain" ? "https://<cloudfront-domain>" : `https://${hostname}`;
4355
4355
  const cluster = `${prefix}-${stage}`;
@@ -4392,7 +4392,7 @@ function buildAwsDeploymentPlan(options = {}) {
4392
4392
  ];
4393
4393
  return {
4394
4394
  kind: "open-uptime.aws-deployment-plan",
4395
- version: 3,
4395
+ version: 4,
4396
4396
  generatedAt: new Date().toISOString(),
4397
4397
  status: "blocked",
4398
4398
  canApply: false,
@@ -4417,6 +4417,17 @@ function buildAwsDeploymentPlan(options = {}) {
4417
4417
  protectedAccessMode,
4418
4418
  edgeDistribution: protectedAccessMode === "cloudfront_default_domain" ? `${prefix}-${stage}-edge` : undefined,
4419
4419
  protectedAccessUrl,
4420
+ originVerification: protectedAccessMode === "cloudfront_default_domain" ? {
4421
+ mode: "cloudfront_origin_header",
4422
+ requiredBeforeScaleUp: true,
4423
+ headerName: "X-Open-Uptime-Origin-Verify",
4424
+ valueStoredInTerraformState: true,
4425
+ stateAccessWarning: "The origin verification header value is sensitive but is stored in encrypted Terraform state and CloudFront/ALB configuration; restrict state, plan, CloudFront distribution-read, and ELB rule-read access."
4426
+ } : {
4427
+ mode: "alb_tls",
4428
+ requiredBeforeScaleUp: false,
4429
+ valueStoredInTerraformState: false
4430
+ },
4420
4431
  targetGroups: [`${prefix}-${stage}-web-tg`],
4421
4432
  securityGroups: [
4422
4433
  `${prefix}-${stage}-alb-sg`,
@@ -4464,7 +4475,7 @@ function buildAwsDeploymentPlan(options = {}) {
4464
4475
  `Infra PR must declare CodeBuild image builder ${prefix}-${stage}-image-builder for @hasna/uptime@${runtimePackageVersion}.`,
4465
4476
  `Infra PR must declare hardened S3 evidence bucket ${evidenceBucket} with KMS, versioning, lifecycle, and public access block.`,
4466
4477
  `Infra PR must declare encrypted EFS ${prefix}-${stage}-data with access point, mount targets, and AWS Backup plan.`,
4467
- protectedAccessMode === "cloudfront_default_domain" ? "Infra PR must declare CloudFront default-domain HTTPS edge, ALB HTTP listener restricted to CloudFront origin-facing ranges, ECS/Fargate cluster, target groups, security groups, IAM roles, CloudWatch log groups, and Secrets Manager refs." : `Infra PR must declare ECS/Fargate cluster ${cluster}, ALB HTTPS listener, target groups, security groups, IAM roles, CloudWatch log groups, and Secrets Manager refs.`,
4478
+ protectedAccessMode === "cloudfront_default_domain" ? "Infra PR must declare CloudFront default-domain HTTPS edge, ALB HTTP listener restricted to CloudFront origin-facing ranges, CloudFront-only origin verification header binding, ECS/Fargate cluster, target groups, security groups, IAM roles, CloudWatch log groups, and Secrets Manager refs." : `Infra PR must declare ECS/Fargate cluster ${cluster}, ALB HTTPS listener, target groups, security groups, IAM roles, CloudWatch log groups, and Secrets Manager refs.`,
4468
4479
  "Only apply the infra plan from the approved infrastructure repository after review evidence is attached."
4469
4480
  ],
4470
4481
  deploy: [
@@ -4473,7 +4484,7 @@ function buildAwsDeploymentPlan(options = {}) {
4473
4484
  "For the EFS SQLite bridge, do not run migration, scheduler, public-probe, or reporter tasks; keep them at desired count 0 until Postgres and cloud leases exist.",
4474
4485
  `Register task definitions for ${services.map((service) => service.name).join(", ")} using valueFrom secrets.`,
4475
4486
  `Update ECS services in cluster ${cluster} one component at a time through the approved deploy pipeline.`,
4476
- protectedAccessMode === "cloudfront_default_domain" ? "Use the CloudFront default HTTPS domain for first protected access; add custom DNS/certificate only after edge ownership is approved." : `Create Route53/edge record for ${hostname} only after ALB health checks pass and auth denial smokes succeed.`
4487
+ protectedAccessMode === "cloudfront_default_domain" ? "Use the CloudFront default HTTPS domain with origin verification header binding for first protected access; add custom DNS/certificate only after edge ownership is approved." : `Create Route53/edge record for ${hostname} only after ALB health checks pass and auth denial smokes succeed.`
4477
4488
  ],
4478
4489
  rollback: [
4479
4490
  "Keep previous task definition ARNs before each service update.",
@@ -4489,6 +4500,7 @@ function buildAwsDeploymentPlan(options = {}) {
4489
4500
  },
4490
4501
  blockers: [
4491
4502
  "The infrastructure owner repository was not found in this workspace.",
4503
+ protectedAccessMode === "cloudfront_default_domain" ? "CloudFront origin verification header binding must be enabled and direct-origin denial must be proven before web desired count is raised above 0." : "ALB HTTPS ingress policy and auth-denial smokes must be proven before web desired count is raised above 0.",
4492
4504
  "The EFS SQLite bridge is single-writer only: web target desired count is 1 and scheduler/public-probe/reporter targets remain 0 until Postgres and cloud leases exist.",
4493
4505
  "Hosted production auth/RBAC must replace broad static hosted-token operation before exposure.",
4494
4506
  "Public probe execution still needs cloud check-job leases wired to runHostedHttpCheck and live policy-decision log evidence.",
@@ -4498,7 +4510,7 @@ function buildAwsDeploymentPlan(options = {}) {
4498
4510
  "Infrastructure PR/synth/plan from the approved infra repository.",
4499
4511
  "CodeBuild image-builder run, container smoke, and immutable image digest.",
4500
4512
  "ECS task definitions using secrets.valueFrom only.",
4501
- "CloudFront-default-domain or ALB TLS auth-denial smokes, direct-origin denial evidence, and web alarm checks.",
4513
+ "CloudFront-default-domain origin-header config or ALB TLS auth-denial smokes, direct-origin denial evidence, and web alarm checks.",
4502
4514
  "Single-writer ECS evidence: one web task maximum and no scheduler/public-probe/reporter EFS mounts.",
4503
4515
  "EFS encryption, access point, mount-target, AWS Backup, and restore-drill evidence.",
4504
4516
  "S3 bucket KMS, versioning, lifecycle, and public-access-block evidence.",
@@ -4512,6 +4524,7 @@ function buildAwsDeploymentPlan(options = {}) {
4512
4524
  "This plan generator does not call AWS.",
4513
4525
  "Blocked plan output intentionally avoids copy-pastable AWS mutation commands.",
4514
4526
  "Default protected access uses CloudFront's HTTPS default domain so first deploy is not blocked on custom DNS or ACM.",
4527
+ "CloudFront default-domain mode still requires origin verification header binding before live scale-up; the header value is sensitive state/config material, not public documentation.",
4515
4528
  "Hosted runtime uses explicit EFS-backed SQLite at HASNA_UPTIME_HOSTED_SQLITE_DB until the async Postgres adapter exists.",
4516
4529
  "Do not set HASNA_UPTIME_DATABASE_URL for hosted tasks until the Postgres adapter is implemented.",
4517
4530
  "Secrets are represented as secret names/refs and must be injected with valueFrom.",
@@ -4600,7 +4613,7 @@ function servicePlan(prefix, stage, role, desiredCount, image, workspaceId, secr
4600
4613
  taskRole: `${name}-task-role`,
4601
4614
  executionRole: `${prefix}-${stage}-execution-role`,
4602
4615
  logGroup: `/ecs/${name}`,
4603
- healthCommand: role === "web" ? "GET /health" : undefined,
4616
+ healthCommand: role === "web" ? "GET /health" : "hosted environment sanity check",
4604
4617
  environment: {
4605
4618
  HASNA_UPTIME_IMAGE: image,
4606
4619
  ...environment
@@ -176,8 +176,8 @@ Before setting `desired_counts.web = 1`, verify:
176
176
  - the image is an immutable digest, not a mutable tag or placeholder;
177
177
  - required secrets have `AWSCURRENT` versions;
178
178
  - `HASNA_UPTIME_ALLOWED_ORIGINS` matches the public HTTPS edge origin;
179
- - CloudFront origin access is distribution-bound, not just narrowed to
180
- CloudFront origin-facing ranges;
179
+ - CloudFront origin access is distribution-bound with the CloudFront-only origin
180
+ verification header, not just narrowed to CloudFront origin-facing ranges;
181
181
  - web egress to ECR, Secrets Manager or SSM, CloudWatch Logs, S3, EFS, and any
182
182
  required endpoints has been proven from a real ECS task. Terraform endpoint
183
183
  ids, route tables, and security-group rules are creation evidence only; the
@@ -386,9 +386,11 @@ routes are backed by cloud check jobs and cloud audit rows.
386
386
  - Do not expose the ALB directly in CloudFront mode; ALB ingress must be limited
387
387
  to CloudFront origin-facing ranges.
388
388
  - Do not treat CloudFront prefix-list ingress as distribution-bound origin
389
- protection. Before enabling the web task, add CloudFront VPC origin/private
390
- ALB routing or require a CloudFront-only origin header whose secret value is
391
- not stored in Terraform state.
389
+ protection. In `cloudfront_default_domain` mode, enable the module's
390
+ CloudFront-only origin verification header and keep its generated value out of
391
+ the public repo and shared logs. Terraform redacts the sensitive input in CLI
392
+ output, but the value is still stored in encrypted Terraform state, saved plan
393
+ files, and AWS CloudFront/ALB configuration; restrict access accordingly.
392
394
  - Do not treat local SQLite, local project DBs, or private-probe local state as cloud
393
395
  authority after cutover.
394
396
  - Do configure owner/project/environment/service/cost-center tags and AWS
@@ -42,9 +42,14 @@ HTTPS origin so hosted mutation CSRF checks still work through the private HTTP
42
42
  origin hop.
43
43
 
44
44
  CloudFront prefix-list ingress is only a network narrowing control; it is not
45
- bound to one distribution. Add CloudFront VPC origin/private ALB routing or an
46
- ALB origin-header rule with the secret value managed outside Terraform state
47
- before enabling the web task.
45
+ bound to one distribution. Before enabling the web task, set
46
+ `enable_cloudfront_origin_verify_header = true` and provide a high-entropy
47
+ `cloudfront_origin_verify_header_value` from a private operator workflow. The
48
+ module then configures CloudFront to send that header, makes the ALB default
49
+ action return `403`, and forwards only requests with the matching header.
50
+ Terraform marks the value sensitive, but it still lives in encrypted Terraform
51
+ state and in CloudFront/ALB configuration; restrict state, saved plan,
52
+ CloudFront distribution-read, and ELB listener-rule-read access accordingly.
48
53
 
49
54
  All module resources carry owner, project, environment, service, account, app
50
55
  type, and cost-center tags. Set `monthly_budget_limit_usd` plus
@@ -71,6 +76,12 @@ Keep this disabled when private endpoints are the approved egress path. Runtime
71
76
  scale-up still requires ECS task evidence for image pull, secret injection, log
72
77
  delivery, S3 access, and EFS mount behavior.
73
78
 
79
+ Every ECS task definition includes an explicit container health check. The web
80
+ task checks `GET /health` through Bun's built-in `fetch`; disabled non-web roles
81
+ currently run a hosted-environment sanity check so scheduler, public-probe,
82
+ reporter, and migration tasks do not start from empty ECS health semantics when
83
+ their long-running workers are later enabled.
84
+
74
85
  Interface endpoint private DNS is VPC-wide. In shared VPCs, either keep endpoint
75
86
  creation in the approved networking root, or pass
76
87
  `additional_vpc_endpoint_source_security_group_ids` for every workload that must
package/infra/aws/main.tf CHANGED
@@ -26,6 +26,7 @@ locals {
26
26
  efs_enabled_services = toset(["web"])
27
27
  use_alb_https = var.protected_access_mode == "alb_https_cert"
28
28
  use_cloudfront = var.protected_access_mode == "cloudfront_default_domain"
29
+ use_origin_verify = local.use_cloudfront && var.enable_cloudfront_origin_verify_header
29
30
  services = {
30
31
  web = {
31
32
  desired_count = lookup(var.desired_counts, "web", 0)
@@ -79,6 +80,43 @@ locals {
79
80
  : ["arn:${data.aws_partition.current.partition}:ssm:${var.region}:${data.aws_caller_identity.current.account_id}:parameter/${local.prefix}/no-ssm-refs-configured"]
80
81
  )
81
82
  service_log_group_arns = [for group in aws_cloudwatch_log_group.service : "${group.arn}:*"]
83
+ service_health_checks = {
84
+ web = {
85
+ command = ["CMD-SHELL", "bun -e \"const r = await fetch('http://127.0.0.1:${local.container_port}/health'); process.exit(r.ok ? 0 : 1)\""]
86
+ interval = 30
87
+ timeout = 5
88
+ retries = 3
89
+ startPeriod = 30
90
+ }
91
+ scheduler = {
92
+ command = ["CMD-SHELL", "bun -e \"process.exit(process.env.HASNA_UPTIME_MODE === 'hosted' && process.env.HASNA_UPTIME_COMPONENT === 'scheduler' ? 0 : 1)\""]
93
+ interval = 30
94
+ timeout = 5
95
+ retries = 3
96
+ startPeriod = 30
97
+ }
98
+ "public-probe" = {
99
+ command = ["CMD-SHELL", "bun -e \"process.exit(process.env.HASNA_UPTIME_MODE === 'hosted' && process.env.HASNA_UPTIME_COMPONENT === 'public-probe' ? 0 : 1)\""]
100
+ interval = 30
101
+ timeout = 5
102
+ retries = 3
103
+ startPeriod = 30
104
+ }
105
+ reporter = {
106
+ command = ["CMD-SHELL", "bun -e \"process.exit(process.env.HASNA_UPTIME_MODE === 'hosted' && process.env.HASNA_UPTIME_COMPONENT === 'reporter' ? 0 : 1)\""]
107
+ interval = 30
108
+ timeout = 5
109
+ retries = 3
110
+ startPeriod = 30
111
+ }
112
+ migration = {
113
+ command = ["CMD-SHELL", "bun -e \"process.exit(process.env.HASNA_UPTIME_MODE === 'hosted' && process.env.HASNA_UPTIME_COMPONENT === 'migration' ? 0 : 1)\""]
114
+ interval = 30
115
+ timeout = 5
116
+ retries = 3
117
+ startPeriod = 30
118
+ }
119
+ }
82
120
  }
83
121
 
84
122
  data "aws_vpc" "target" {
@@ -863,10 +901,45 @@ resource "aws_lb_listener" "http_cloudfront" {
863
901
  protocol = "HTTP"
864
902
  tags = local.tags
865
903
 
866
- default_action {
904
+ dynamic "default_action" {
905
+ for_each = local.use_origin_verify ? [] : [1]
906
+ content {
907
+ type = "forward"
908
+ target_group_arn = aws_lb_target_group.web.arn
909
+ }
910
+ }
911
+
912
+ dynamic "default_action" {
913
+ for_each = local.use_origin_verify ? [1] : []
914
+ content {
915
+ type = "fixed-response"
916
+
917
+ fixed_response {
918
+ content_type = "text/plain"
919
+ message_body = "forbidden"
920
+ status_code = "403"
921
+ }
922
+ }
923
+ }
924
+ }
925
+
926
+ resource "aws_lb_listener_rule" "http_cloudfront_origin_verify" {
927
+ count = local.use_origin_verify ? 1 : 0
928
+ listener_arn = aws_lb_listener.http_cloudfront[0].arn
929
+ priority = var.cloudfront_origin_verify_listener_rule_priority
930
+ tags = local.tags
931
+
932
+ action {
867
933
  type = "forward"
868
934
  target_group_arn = aws_lb_target_group.web.arn
869
935
  }
936
+
937
+ condition {
938
+ http_header {
939
+ http_header_name = var.cloudfront_origin_verify_header_name
940
+ values = [var.cloudfront_origin_verify_header_value]
941
+ }
942
+ }
870
943
  }
871
944
 
872
945
  resource "aws_cloudfront_distribution" "open_uptime" {
@@ -881,6 +954,14 @@ resource "aws_cloudfront_distribution" "open_uptime" {
881
954
  domain_name = aws_lb.open_uptime.dns_name
882
955
  origin_id = "${local.prefix}-alb"
883
956
 
957
+ dynamic "custom_header" {
958
+ for_each = local.use_origin_verify ? [1] : []
959
+ content {
960
+ name = var.cloudfront_origin_verify_header_name
961
+ value = var.cloudfront_origin_verify_header_value
962
+ }
963
+ }
964
+
884
965
  custom_origin_config {
885
966
  http_port = 80
886
967
  https_port = 443
@@ -919,7 +1000,7 @@ resource "aws_cloudfront_distribution" "open_uptime" {
919
1000
  cloudfront_default_certificate = true
920
1001
  }
921
1002
 
922
- depends_on = [aws_lb_listener.http_cloudfront]
1003
+ depends_on = [aws_lb_listener.http_cloudfront, aws_lb_listener_rule.http_cloudfront_origin_verify]
923
1004
  }
924
1005
 
925
1006
  resource "aws_route53_record" "open_uptime" {
@@ -1090,6 +1171,7 @@ resource "aws_ecs_task_definition" "service" {
1090
1171
  readOnly = false
1091
1172
  }
1092
1173
  ] : []
1174
+ healthCheck = local.service_health_checks[each.key]
1093
1175
  secrets = [
1094
1176
  for name, value_from in each.value.secrets : {
1095
1177
  name = name
@@ -1135,7 +1217,7 @@ resource "aws_ecs_service" "web" {
1135
1217
  container_port = local.container_port
1136
1218
  }
1137
1219
 
1138
- depends_on = [aws_lb_listener.https, aws_lb_listener.http_cloudfront, aws_efs_mount_target.data]
1220
+ depends_on = [aws_lb_listener.https, aws_lb_listener.http_cloudfront, aws_lb_listener_rule.http_cloudfront_origin_verify, aws_efs_mount_target.data]
1139
1221
  }
1140
1222
 
1141
1223
  resource "aws_ecs_service" "worker" {
@@ -22,6 +22,14 @@ output "protected_access_url" {
22
22
  value = var.protected_access_mode == "cloudfront_default_domain" ? "https://${aws_cloudfront_distribution.open_uptime[0].domain_name}" : "https://${var.hostname}"
23
23
  }
24
24
 
25
+ output "cloudfront_origin_verify_header_enabled" {
26
+ value = local.use_origin_verify
27
+ }
28
+
29
+ output "cloudfront_origin_verify_header_name" {
30
+ value = local.use_origin_verify ? var.cloudfront_origin_verify_header_name : null
31
+ }
32
+
25
33
  output "evidence_bucket" {
26
34
  value = aws_s3_bucket.evidence.bucket
27
35
  }
@@ -11,12 +11,15 @@ workspace_id = "workspace-id"
11
11
  vpc_id = "vpc-xxxxxxxx"
12
12
  ecr_repository_name = "open-uptime"
13
13
  protected_access_mode = "cloudfront_default_domain"
14
+ enable_cloudfront_origin_verify_header = false
15
+ cloudfront_origin_verify_header_name = "X-Open-Uptime-Origin-Verify"
16
+ cloudfront_origin_verify_header_value = null
14
17
  public_subnet_ids = ["subnet-replace-public-a", "subnet-replace-public-b"]
15
18
  alb_ingress_cidr_blocks = []
16
19
  private_subnet_ids = ["subnet-replace-private-a", "subnet-replace-private-b"]
17
20
  private_route_table_ids = ["rtb-replace-private"]
18
21
  container_image = "123456789012.dkr.ecr.us-east-1.amazonaws.com/open-uptime@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
19
- runtime_package_version = "0.1.17"
22
+ runtime_package_version = "0.1.19"
20
23
  certificate_arn = null
21
24
  hosted_zone_id = null
22
25
  app_env_secret_arn = "arn:aws:secretsmanager:us-east-1:123456789012:secret:open-uptime/prod/app/env"
@@ -87,6 +87,91 @@ variable "protected_access_mode" {
87
87
  }
88
88
  }
89
89
 
90
+ variable "enable_cloudfront_origin_verify_header" {
91
+ description = "When true in cloudfront_default_domain mode, CloudFront sends a private origin header and the ALB listener rejects requests missing the matching value."
92
+ type = bool
93
+ default = false
94
+
95
+ validation {
96
+ condition = !var.enable_cloudfront_origin_verify_header || var.protected_access_mode == "cloudfront_default_domain"
97
+ error_message = "enable_cloudfront_origin_verify_header can only be true when protected_access_mode is cloudfront_default_domain."
98
+ }
99
+ }
100
+
101
+ variable "cloudfront_origin_verify_header_name" {
102
+ description = "CloudFront-only origin verification header name used when enable_cloudfront_origin_verify_header is true."
103
+ type = string
104
+ default = "X-Open-Uptime-Origin-Verify"
105
+
106
+ validation {
107
+ condition = (
108
+ can(regex("^[A-Za-z0-9-]+$", var.cloudfront_origin_verify_header_name))
109
+ && !startswith(lower(var.cloudfront_origin_verify_header_name), "x-amz-")
110
+ && !startswith(lower(var.cloudfront_origin_verify_header_name), "x-edge-")
111
+ && !contains([
112
+ "authorization",
113
+ "cache-control",
114
+ "connection",
115
+ "content-length",
116
+ "content-type",
117
+ "cookie",
118
+ "host",
119
+ "if-match",
120
+ "if-modified-since",
121
+ "if-none-match",
122
+ "if-range",
123
+ "if-unmodified-since",
124
+ "max-forwards",
125
+ "origin",
126
+ "pragma",
127
+ "proxy-authenticate",
128
+ "proxy-authorization",
129
+ "proxy-connection",
130
+ "range",
131
+ "request-range",
132
+ "te",
133
+ "trailer",
134
+ "transfer-encoding",
135
+ "upgrade",
136
+ "via",
137
+ "x-real-ip",
138
+ "x-uptime-hosted-token",
139
+ ], lower(var.cloudfront_origin_verify_header_name))
140
+ )
141
+ error_message = "cloudfront_origin_verify_header_name must be a safe CloudFront custom origin header name and must not use reserved, app-forwarded, or viewer-controlled header names."
142
+ }
143
+ }
144
+
145
+ variable "cloudfront_origin_verify_header_value" {
146
+ description = "Sensitive CloudFront-only origin verification header value. Required when enable_cloudfront_origin_verify_header is true."
147
+ type = string
148
+ default = null
149
+ nullable = true
150
+ sensitive = true
151
+
152
+ validation {
153
+ condition = (
154
+ !(var.enable_cloudfront_origin_verify_header && var.protected_access_mode == "cloudfront_default_domain")
155
+ || (
156
+ var.cloudfront_origin_verify_header_value != null
157
+ && can(regex("^[A-Za-z0-9_-]{32,256}$", var.cloudfront_origin_verify_header_value))
158
+ )
159
+ )
160
+ error_message = "cloudfront_origin_verify_header_value is required when origin verification is enabled and must be 32-256 URL-safe characters."
161
+ }
162
+ }
163
+
164
+ variable "cloudfront_origin_verify_listener_rule_priority" {
165
+ description = "ALB listener rule priority for the CloudFront origin verification header rule."
166
+ type = number
167
+ default = 100
168
+
169
+ validation {
170
+ condition = var.cloudfront_origin_verify_listener_rule_priority >= 1 && var.cloudfront_origin_verify_listener_rule_priority <= 50000
171
+ error_message = "cloudfront_origin_verify_listener_rule_priority must be between 1 and 50000."
172
+ }
173
+ }
174
+
90
175
  variable "public_subnet_ids" {
91
176
  description = "Public subnets for the ALB."
92
177
  type = list(string)
@@ -116,7 +201,7 @@ variable "container_image" {
116
201
  variable "runtime_package_version" {
117
202
  description = "Published @hasna/uptime package version that CodeBuild should build into the ECR image."
118
203
  type = string
119
- default = "0.1.17"
204
+ default = "0.1.19"
120
205
 
121
206
  validation {
122
207
  condition = can(regex("^[0-9]+\\.[0-9]+\\.[0-9]+(-[0-9A-Za-z.-]+)?$", var.runtime_package_version))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/uptime",
3
- "version": "0.1.17",
3
+ "version": "0.1.19",
4
4
  "description": "Local-first uptime and downtime monitoring service with CLI, MCP, SDK, SQLite persistence, and a dashboard.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",