@hasna/uptime 0.1.23 → 0.1.25

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/dist/service.js CHANGED
@@ -3150,12 +3150,18 @@ var MAX_PROBE_RESULT_FUTURE_MS = 5 * 60000;
3150
3150
  class UptimeService {
3151
3151
  store;
3152
3152
  checkRunner;
3153
+ hostedResolveHost;
3154
+ hostedHttpRequest;
3155
+ hostedMaxRedirects;
3153
3156
  leaseOwner = `svc_${randomUUID3().replace(/-/g, "").slice(0, 18)}`;
3154
3157
  inFlightChecks = new Set;
3155
3158
  inFlightReportSchedules = new Set;
3156
3159
  constructor(options = {}) {
3157
3160
  this.store = options.store ?? new UptimeStore({ mode: "local", ...options });
3158
3161
  this.checkRunner = options.checkRunner ?? runMonitorCheck;
3162
+ this.hostedResolveHost = options.hostedResolveHost;
3163
+ this.hostedHttpRequest = options.hostedHttpRequest;
3164
+ this.hostedMaxRedirects = options.hostedMaxRedirects;
3159
3165
  }
3160
3166
  close() {
3161
3167
  this.store.close();
@@ -3362,39 +3368,7 @@ class UptimeService {
3362
3368
  const monitor = this.store.getMonitor(idOrName);
3363
3369
  if (!monitor)
3364
3370
  throw new Error(`Monitor not found: ${idOrName}`);
3365
- if (!monitor.enabled)
3366
- throw new Error(`Monitor is disabled: ${monitor.name}`);
3367
- if (this.inFlightChecks.has(monitor.id))
3368
- throw new Error(`Monitor check already in progress: ${monitor.name}`);
3369
- const leaseTtlMs = Math.max(60000, (monitor.retryCount + 1) * monitor.timeoutMs + 1e4);
3370
- if (!this.store.acquireCheckLease(monitor.id, this.leaseOwner, leaseTtlMs)) {
3371
- throw new MonitorCheckBusyError(`Monitor check already in progress: ${monitor.name}`);
3372
- }
3373
- this.inFlightChecks.add(monitor.id);
3374
- try {
3375
- let attemptCount = 0;
3376
- let last = null;
3377
- const maxAttempts = Math.max(1, monitor.retryCount + 1);
3378
- while (attemptCount < maxAttempts) {
3379
- attemptCount += 1;
3380
- last = await this.checkRunner(monitor);
3381
- if (last.status === "up")
3382
- break;
3383
- }
3384
- return this.store.recordCheckResult({
3385
- monitorId: monitor.id,
3386
- status: last.status,
3387
- latencyMs: last.latencyMs,
3388
- statusCode: last.statusCode ?? null,
3389
- error: last.error ?? null,
3390
- evidence: last.evidence ?? null,
3391
- attemptCount,
3392
- expectedMonitorRevision: monitor.revision
3393
- });
3394
- } finally {
3395
- this.inFlightChecks.delete(monitor.id);
3396
- this.store.releaseCheckLease(monitor.id, this.leaseOwner);
3397
- }
3371
+ return this.recordMonitorCheck(monitor, { hostedTargetPolicy: false });
3398
3372
  }
3399
3373
  async checkAll() {
3400
3374
  if (this.store.mode === "hosted")
@@ -3406,6 +3380,49 @@ class UptimeService {
3406
3380
  }
3407
3381
  return results;
3408
3382
  }
3383
+ async checkHostedPublicMonitor(idOrName, options = {}) {
3384
+ this.assertHostedPublicChecksEnabled();
3385
+ const workspaceId = this.requireHostedWorkerWorkspaceId(options.workspaceId);
3386
+ const monitor = this.store.getMonitor(idOrName, { workspaceId });
3387
+ if (!monitor)
3388
+ throw new Error(`Monitor not found: ${idOrName}`);
3389
+ this.assertHostedPublicMonitor(monitor);
3390
+ const result = await this.recordMonitorCheck(monitor, { hostedTargetPolicy: true });
3391
+ this.auditStore().recordAuditEvent({
3392
+ workspaceId,
3393
+ action: "hosted_public_check.run",
3394
+ resourceType: "monitor",
3395
+ resourceId: monitor.id,
3396
+ message: `Ran hosted public check for ${monitor.name}`,
3397
+ metadata: {
3398
+ checkResultId: result.id,
3399
+ status: result.status,
3400
+ monitorKind: monitor.kind,
3401
+ operatorPath: "hosted_public_check"
3402
+ },
3403
+ actor: "hosted-public-check-worker"
3404
+ });
3405
+ return result;
3406
+ }
3407
+ async runDueHostedPublicChecks(now = new Date, options = {}) {
3408
+ this.assertHostedPublicChecksEnabled();
3409
+ const workspaceId = this.requireHostedWorkerWorkspaceId(options.workspaceId);
3410
+ const due = this.store.listMonitors({ workspaceId }).filter((monitor) => this.isHostedPublicMonitor(monitor) && this.isDue(monitor, now));
3411
+ const results = [];
3412
+ for (const monitor of due) {
3413
+ const current = this.store.getMonitor(monitor.id, { workspaceId });
3414
+ if (!current || !this.isHostedPublicMonitor(current) || !this.isDue(current, now))
3415
+ continue;
3416
+ try {
3417
+ results.push(await this.checkHostedPublicMonitor(current.id, { workspaceId }));
3418
+ } catch (error) {
3419
+ if (error instanceof MonitorCheckBusyError || error instanceof StaleCheckResultError)
3420
+ continue;
3421
+ throw error;
3422
+ }
3423
+ }
3424
+ return results;
3425
+ }
3409
3426
  startScheduler(options = {}) {
3410
3427
  if (this.store.mode === "hosted")
3411
3428
  throw new Error("hosted scheduler requires check_jobs and probes");
@@ -3451,6 +3468,67 @@ class UptimeService {
3451
3468
  const last = new Date(monitor.lastCheckedAt).getTime();
3452
3469
  return now.getTime() - last >= monitor.intervalSeconds * 1000;
3453
3470
  }
3471
+ async recordMonitorCheck(monitor, options) {
3472
+ if (!monitor.enabled)
3473
+ throw new Error(`Monitor is disabled: ${monitor.name}`);
3474
+ if (this.inFlightChecks.has(monitor.id))
3475
+ throw new Error(`Monitor check already in progress: ${monitor.name}`);
3476
+ const leaseTtlMs = Math.max(60000, (monitor.retryCount + 1) * monitor.timeoutMs + 1e4);
3477
+ if (!this.store.acquireCheckLease(monitor.id, this.leaseOwner, leaseTtlMs)) {
3478
+ throw new MonitorCheckBusyError(`Monitor check already in progress: ${monitor.name}`);
3479
+ }
3480
+ this.inFlightChecks.add(monitor.id);
3481
+ try {
3482
+ let attemptCount = 0;
3483
+ let last = null;
3484
+ const maxAttempts = Math.max(1, monitor.retryCount + 1);
3485
+ while (attemptCount < maxAttempts) {
3486
+ attemptCount += 1;
3487
+ last = options.hostedTargetPolicy ? await this.runHostedPublicCheckAttempt(monitor) : await this.checkRunner(monitor);
3488
+ if (last.status === "up")
3489
+ break;
3490
+ }
3491
+ return this.store.recordCheckResult({
3492
+ monitorId: monitor.id,
3493
+ status: last.status,
3494
+ latencyMs: last.latencyMs,
3495
+ statusCode: last.statusCode ?? null,
3496
+ error: last.error ?? null,
3497
+ evidence: last.evidence ?? null,
3498
+ attemptCount,
3499
+ expectedMonitorRevision: monitor.revision
3500
+ });
3501
+ } finally {
3502
+ this.inFlightChecks.delete(monitor.id);
3503
+ this.store.releaseCheckLease(monitor.id, this.leaseOwner);
3504
+ }
3505
+ }
3506
+ runHostedPublicCheckAttempt(monitor) {
3507
+ return runMonitorCheck(monitor, {
3508
+ hostedTargetPolicy: true,
3509
+ resolveHost: this.hostedResolveHost,
3510
+ hostedHttpRequest: this.hostedHttpRequest,
3511
+ maxRedirects: this.hostedMaxRedirects
3512
+ });
3513
+ }
3514
+ assertHostedPublicChecksEnabled() {
3515
+ if (this.store.mode !== "hosted")
3516
+ throw new Error("hosted public checks require hosted mode");
3517
+ }
3518
+ requireHostedWorkerWorkspaceId(workspaceId) {
3519
+ const value = workspaceId?.trim() || process.env.HASNA_UPTIME_WORKSPACE_ID?.trim();
3520
+ if (!value)
3521
+ throw new Error("hosted public checks require a workspace id");
3522
+ return value;
3523
+ }
3524
+ assertHostedPublicMonitor(monitor) {
3525
+ if (!this.isHostedPublicMonitor(monitor)) {
3526
+ throw new Error("hosted public checks support only HTTP and TCP monitors");
3527
+ }
3528
+ }
3529
+ isHostedPublicMonitor(monitor) {
3530
+ return monitor.kind === "http" || monitor.kind === "tcp";
3531
+ }
3454
3532
  probeStore() {
3455
3533
  if (this.store.mode === "hosted") {
3456
3534
  throw new Error("hosted probe APIs require cloud check_jobs, workspace stores, and audit logging");
@@ -8,7 +8,8 @@ call AWS or mutate infrastructure.
8
8
 
9
9
  ```bash
10
10
  uptime cloud plan --json > open-uptime-aws-plan.json
11
- uptime cloud private-probe-config --probe-id prb_private_01 --machine-id private-probe-01 --env > private-probe-01-uptime.env
11
+ uptime cloud private-probe-config --probe-id prb_private_01 --machine-id private-probe-01 --json > private-probe-01-preflight.json
12
+ uptime cloud private-probe-config --probe-id prb_private_01 --machine-id private-probe-01 --env --allow-blocked-env > private-probe-01-review-only.env
12
13
  ```
13
14
 
14
15
  Public package defaults are placeholders:
@@ -33,8 +34,10 @@ The app repo includes a hosted runtime `Dockerfile` and Terraform/OpenTofu
33
34
  starter files in `infra/aws`. The plan output points to these files and keeps
34
35
  `applyAllowed: false`.
35
36
 
36
- `uptime cloud private-probe-config --env` requires a real `--probe-id`; it will not
37
- write a sourceable env file with a placeholder probe identity.
37
+ `uptime cloud private-probe-config --env` is blocked by default while hosted
38
+ probe routes remain fail-closed. It requires both a real `--probe-id` and the
39
+ explicit `--allow-blocked-env` review override; do not use that env output to
40
+ start a private probe until the JSON output says `canStart: true`.
38
41
 
39
42
  ## Preflight
40
43
 
@@ -57,9 +60,14 @@ write a sourceable env file with a placeholder probe identity.
57
60
 
58
61
  4. Confirm the target VPC, private subnets, KMS key, and EFS/Backup plan inputs
59
62
  still match the plan.
60
- 5. Confirm the protected access mode. The first deploy can use the CloudFront
61
- default HTTPS domain without custom DNS or ACM. Custom hostname deploys still
62
- require Route53/edge ownership and an ACM certificate.
63
+ 5. Confirm the protected access mode. The first zero-count deploy can use the
64
+ CloudFront default HTTPS domain without custom DNS or ACM. Before
65
+ token-bearing live traffic, either set
66
+ `cloudfront_origin_protocol_policy = "https-only"` with a dedicated
67
+ `cloudfront_origin_domain_name` that resolves to the ALB and a matching ACM
68
+ `certificate_arn`, or record an explicit risk acceptance for the temporary
69
+ HTTP-origin bridge. Custom hostname deploys still require Route53/edge
70
+ ownership and an ACM certificate.
63
71
  6. Confirm the deployment role uses short-lived credentials or OIDC, not copied
64
72
  access keys.
65
73
  7. Create a private evidence directory outside the public repository. Store
@@ -74,9 +82,12 @@ The plan expects:
74
82
  - ECS/Fargate cluster with separate services for web, scheduler, public probe,
75
83
  reporter, and one-off migrations. In the current EFS SQLite bridge, only web
76
84
  may be enabled and it must run at desired count `0` or `1`.
77
- - CloudFront default-domain HTTPS edge plus ALB HTTP origin restricted to
78
- CloudFront origin-facing ranges, or an ALB HTTPS listener with ACM certificate
79
- when custom DNS is approved.
85
+ - CloudFront default-domain HTTPS edge plus an ALB origin restricted to
86
+ CloudFront origin-facing ranges. The default zero-count bridge uses HTTP to
87
+ the origin; token-bearing live traffic should use the module's HTTPS-origin
88
+ mode with `cloudfront_origin_domain_name` plus `certificate_arn`, or a
89
+ documented risk acceptance. Direct ALB HTTPS mode also requires custom DNS and
90
+ an ACM certificate.
80
91
  - Encrypted EFS file system, access point, mount targets, and AWS Backup plan
81
92
  for `HASNA_UPTIME_HOSTED_SQLITE_DB=/data/uptime/uptime.db`.
82
93
  - S3 bucket for redacted browser evidence and generated report artifacts.
@@ -148,6 +159,12 @@ aws codebuild start-build \
148
159
  --project-name "$IMAGE_BUILDER_PROJECT"
149
160
  ```
150
161
 
162
+ The private infra root should set `runtime_package_integrity` to the published
163
+ npm `dist.integrity` value for the exact `runtime_package_version`. The image
164
+ builder verifies that value before extracting the package. If the value is not
165
+ set, record why the tarball is not integrity-pinned and keep the service
166
+ not-live.
167
+
151
168
  Update the approved infra root so `container_image` is the immutable ECR digest,
152
169
  then re-plan with all services still at `0`.
153
170
 
@@ -176,6 +193,9 @@ Before setting `desired_counts.web = 1`, verify:
176
193
  - the image is an immutable digest, not a mutable tag or placeholder;
177
194
  - required secrets have `AWSCURRENT` versions;
178
195
  - `HASNA_UPTIME_ALLOWED_ORIGINS` matches the public HTTPS edge origin;
196
+ - CloudFront-to-origin transport is either `https-only` with an origin hostname
197
+ whose certificate matches that hostname, or the HTTP-origin bridge has a
198
+ named risk owner and approval recorded in private evidence;
179
199
  - CloudFront origin access is distribution-bound with the CloudFront-only origin
180
200
  verification header, not just narrowed to CloudFront origin-facing ranges;
181
201
  - web egress to ECR, Secrets Manager or SSM, CloudWatch Logs, S3, EFS, and any
@@ -398,11 +418,13 @@ routes are backed by cloud check jobs and cloud audit rows.
398
418
  URLs, or probe private keys in task definitions. Use ECS `secrets.valueFrom`
399
419
  refs such as `HASNA_UPTIME_HOSTED_TOKEN`.
400
420
  - Do not run public probe workers against private targets.
401
- - Do not enable public probe workers until their cloud check-job path calls
402
- `runHostedHttpCheck`, records target-policy decision evidence, and passes AWS
403
- smokes for denied DNS answers, redirect-to-denied targets, and address
404
- pinning. The SDK runner now handles execution-time DNS and redirect
405
- enforcement, but it is not active until the worker is wired to it.
421
+ - Do not enable public probe workers until their cloud check-job path calls the
422
+ hosted public-check runner, records target-policy decision evidence, and
423
+ passes AWS smokes for denied DNS answers, redirect-to-denied targets, and
424
+ address pinning. The SDK and `uptime cloud public-checks run-due` path now
425
+ handle execution-time DNS and redirect enforcement for bounded smokes, but a
426
+ sustained public-probe worker loop is not active until it is wired to cloud
427
+ leases.
406
428
  - Do not enable scheduler, public-probe, reporter, or migration workers against
407
429
  the EFS SQLite bridge; those services need Postgres/cloud leases first.
408
430
  - Do not expose dashboard/API routes without hosted auth and workspace checks.
@@ -414,6 +436,10 @@ routes are backed by cloud check jobs and cloud audit rows.
414
436
  the public repo and shared logs. Terraform redacts the sensitive input in CLI
415
437
  output, but the value is still stored in encrypted Terraform state, saved plan
416
438
  files, and AWS CloudFront/ALB configuration; restrict access accordingly.
439
+ - Do not treat `cloudfront_origin_protocol_policy = "http-only"` as final for
440
+ token-bearing traffic. The module supports `https-only`, but that mode needs a
441
+ real origin DNS name and matching ACM certificate because CloudFront verifies
442
+ the custom-origin TLS certificate against the origin host.
417
443
  - Do not treat local SQLite, local project DBs, or private-probe local state as cloud
418
444
  authority after cutover.
419
445
  - Do configure owner/project/environment/service/cost-center tags, AWS Budgets
@@ -110,10 +110,12 @@ Target shape inside the approved VPC:
110
110
 
111
111
  Security groups:
112
112
 
113
- - `open-uptime-alb-sg`: in `cloudfront_default_domain` mode, inbound `80` only
114
- from AWS's CloudFront origin-facing managed prefix list; in `alb_https_cert`
115
- mode, inbound `443` only from the approved edge/source CIDR policy. Outbound
116
- is only to the web target group.
113
+ - `open-uptime-alb-sg`: in `cloudfront_default_domain` mode, inbound origin
114
+ traffic is limited to AWS's CloudFront origin-facing managed prefix list on
115
+ `80` for the temporary HTTP bridge or `443` when
116
+ `cloudfront_origin_protocol_policy = "https-only"`; in `alb_https_cert` mode,
117
+ inbound `443` is limited to the approved edge/source CIDR policy. Outbound is
118
+ only to the web target group.
117
119
  - `open-uptime-web-sg`: inbound only from ALB, outbound to RDS, S3 endpoint,
118
120
  Secrets Manager, Logs, and internal service endpoints.
119
121
  - `open-uptime-scheduler-sg`: no inbound, outbound to RDS, Logs, Secrets
@@ -136,10 +138,15 @@ infra PR.
136
138
 
137
139
  Public web exposure requires defense in depth:
138
140
 
139
- - first deployment may terminate viewer TLS at CloudFront's default HTTPS
140
- domain, restrict ALB HTTP origin ingress to CloudFront origin-facing ranges,
141
+ - first zero-count deployment may terminate viewer TLS at CloudFront's default
142
+ HTTPS domain, restrict ALB origin ingress to CloudFront origin-facing ranges,
141
143
  and require the module's CloudFront-only origin verification header at the ALB
142
144
  listener;
145
+ - token-bearing live traffic should use CloudFront HTTPS-origin mode by setting
146
+ `cloudfront_origin_protocol_policy = "https-only"`, a dedicated
147
+ `cloudfront_origin_domain_name` that resolves to the ALB, and a matching ACM
148
+ `certificate_arn`. CloudFront validates the custom-origin certificate against
149
+ the origin hostname, so the ALB DNS name is not enough for this mode;
143
150
  - CloudFront prefix-list ingress is not distribution-bound by itself. In
144
151
  `cloudfront_default_domain` mode, set
145
152
  `enable_cloudfront_origin_verify_header = true` and provide a high-entropy
@@ -154,7 +161,7 @@ Public web exposure requires defense in depth:
154
161
  identity layer;
155
162
  - hosted web tasks must set `HASNA_UPTIME_ALLOWED_ORIGINS` to the public HTTPS
156
163
  edge origin so browser mutation checks do not compare CloudFront HTTPS origins
157
- against the private HTTP ALB origin hop;
164
+ against the ALB origin hostname;
158
165
  - Open Uptime still enforces app-level auth and workspace RBAC on every route
159
166
  except `/health`;
160
167
  - `/health` returns only service liveness/readiness and no monitor data;
@@ -379,7 +386,9 @@ Minimum implementation path:
379
386
 
380
387
  1. review the repo-owned `Dockerfile` and package-image `Dockerfile.package`;
381
388
  2. add the ECR repository and CodeBuild package image builder;
382
- 3. build the published npm package into ECR and record the immutable digest;
389
+ 3. build the published npm package into ECR, verify the expected npm
390
+ `dist.integrity` when `runtime_package_integrity` is set, install production
391
+ dependencies with the published `bun.lock`, and record the immutable digest;
383
392
  4. run typecheck, tests, package checks, and container smoke locally/CI;
384
393
  5. for the EFS bridge, keep the desired count at one web task maximum and zero
385
394
  scheduler/public-probe/reporter/migration tasks;
@@ -425,6 +434,11 @@ PR must include a rough monthly estimate for:
425
434
  Evidence retention and browser trace capture are the primary variable costs.
426
435
  Default retention must be short until usage is measured.
427
436
 
437
+ ECS services must enable AWS-managed tags and `propagate_tags = "SERVICE"` so
438
+ service-launched tasks retain cost allocation tags. One-off smoke tasks run
439
+ outside ECS service propagation and must pass equivalent tags explicitly in the
440
+ operator command/evidence.
441
+
428
442
  The AWS Terraform starter exposes optional AWS Budgets alerts through
429
443
  `monthly_budget_limit_usd` and `budget_alert_email_addresses`; the approved
430
444
  infra root must set real human/on-call notification targets and prove
@@ -438,8 +452,9 @@ required before browser evidence or public probe scale-out.
438
452
  evidence bucket, encrypted logs, Backup, EFS, and service secret containers.
439
453
  It is not live: services remain at desired count `0`, secrets have
440
454
  `AWSCURRENT` values, scoped hosted-token descriptors can be used for operator
441
- smokes, and no ACM cert or Route53 record exists for a later custom-hostname
442
- path. Full production identity/RBAC is still not implemented.
455
+ smokes, and the HTTPS-origin/custom-hostname path still needs an approved ACM
456
+ cert, DNS record, plan/apply, and edge smoke. Full production identity/RBAC is
457
+ still not implemented.
443
458
  - Open Uptime is still SQLite-only for this bridge; only one protected web task
444
459
  may write EFS until Postgres and cloud leases exist.
445
460
  - Hosted API/dashboard auth, workspace RBAC, target policy, and Postgres leases
@@ -122,11 +122,12 @@ or delete workspace B data.
122
122
  The target-state architecture uses one shared target policy at both
123
123
  configuration time and execution time. The current hosted API implements
124
124
  configuration-time checks for direct targets, and the SDK exposes
125
- `runHostedHttpCheck` for hosted public HTTP probes. That runner performs runtime
126
- DNS resolution, address pinning, redirect validation, DNS-rebinding protection,
127
- and decision-record evidence. Public probe execution stays disabled until cloud
128
- check-job leases and the public-probe worker are wired to that runner and
129
- validated in AWS.
125
+ `runHostedHttpCheck` plus a bounded hosted public-check service/CLI path for
126
+ workspace-scoped HTTP/TCP checks. Those paths perform runtime DNS resolution,
127
+ address pinning, redirect validation, DNS-rebinding protection, and
128
+ decision-record evidence. Long-running public probe execution stays disabled
129
+ until cloud check-job leases and the public-probe worker loop are wired to that
130
+ runner and validated in AWS.
130
131
 
131
132
  Public probes must deny:
132
133
 
@@ -428,8 +429,10 @@ ECS/API/RDS/S3/probe lag/job backlog/delivery failures, and rollback commands.
428
429
  - Hosted API reads are protected only by a broad bootstrap token, and the hosted
429
430
  dashboard shell still fails closed; production-grade identity/RBAC is not
430
431
  implemented yet.
431
- - Outbound target policy for hosted HTTP probes exists in the SDK, but the
432
- cloud public-probe worker and lease path are not wired to it yet.
432
+ - Outbound target policy for hosted HTTP/TCP checks exists in the SDK and the
433
+ `uptime cloud public-checks run-due` operator path. The cloud public-probe
434
+ worker loop, durable check-job lease path, and sustained ECS liveness are not
435
+ wired yet.
433
436
  - `@hasna/cloud` hybrid mode still returns SQLite, so it is not cloud-primary.
434
437
  - The local cloud config currently points at a stale/non-resolving database host.
435
438
  - Todos has unresolved conflicts that must be reconciled before cloud cutover.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "service": "open-uptime",
3
3
  "package": "@hasna/uptime",
4
- "intendedVersion": "0.1.23",
4
+ "intendedVersion": "0.1.25",
5
5
  "accountProfile": "<aws-profile>",
6
6
  "accountId": "<aws-account-id>",
7
7
  "region": "us-east-1",
@@ -18,7 +18,9 @@
18
18
  "mode": "cloudfront_default_domain",
19
19
  "url": "https://<cloudfront-domain>",
20
20
  "allowedOriginsEnv": "HASNA_UPTIME_ALLOWED_ORIGINS=https://<cloudfront-domain>",
21
- "originPolicy": "ALB HTTP ingress restricted to CloudFront origin-facing ranges plus CloudFront-only origin verification header before scale-up",
21
+ "originPolicy": "ALB origin ingress restricted to CloudFront origin-facing ranges plus CloudFront-only origin verification header before scale-up; HTTPS-origin mode requires cloudfront_origin_domain_name plus matching certificate_arn",
22
+ "originProtocolPolicy": "http-only-or-https-only",
23
+ "originDomainName": "<alb-dns-name-or-approved-origin-hostname>",
22
24
  "originVerifyHeaderRequiredBeforeScaleUp": true,
23
25
  "originVerifyHeaderEnabled": "<true-after-private-root-apply>",
24
26
  "originVerifyHeaderName": "<private-header-name>",
@@ -31,31 +31,43 @@ adapter and cloud leases are implemented. Do not set
31
31
 
32
32
  The included CodeBuild project builds `@hasna/uptime` from npm with
33
33
  `Dockerfile.package` and pushes the resulting image to ECR. This avoids
34
- depending on a local Docker daemon for image publication.
34
+ depending on a local Docker daemon for image publication. Set
35
+ `runtime_package_integrity` in the private root after publish to make CodeBuild
36
+ verify the npm tarball `dist.integrity` before extracting it. The package image
37
+ also installs production dependencies from the published `bun.lock` with
38
+ `--frozen-lockfile`.
35
39
 
36
40
  The default protected access mode is `cloudfront_default_domain`: CloudFront
37
- serves HTTPS on its default domain while the ALB origin accepts HTTP only from
38
- AWS's CloudFront origin-facing managed prefix list. Use `alb_https_cert` only
39
- after custom DNS and an ACM certificate are approved.
41
+ serves HTTPS on its default domain while the ALB origin is limited to AWS's
42
+ CloudFront origin-facing managed prefix list. The default origin protocol is the
43
+ temporary `http-only` bridge. Before token-bearing live traffic, prefer setting
44
+ `cloudfront_origin_protocol_policy = "https-only"` with a dedicated
45
+ `cloudfront_origin_domain_name` that resolves to the ALB and a matching ACM
46
+ `certificate_arn`. Use `alb_https_cert` only when bypassing CloudFront after
47
+ custom DNS and an ACM certificate are approved.
40
48
  The web task receives `HASNA_UPTIME_ALLOWED_ORIGINS` for the selected public
41
- HTTPS origin so hosted mutation CSRF checks still work through the private HTTP
42
- origin hop.
49
+ HTTPS origin so hosted mutation CSRF checks still work through the selected
50
+ edge/origin path.
43
51
 
44
52
  CloudFront prefix-list ingress is only a network narrowing control; it is not
45
53
  bound to one distribution. Before enabling the web task, set
46
54
  `enable_cloudfront_origin_verify_header = true` and provide a high-entropy
47
55
  `cloudfront_origin_verify_header_value` from a private operator workflow. The
48
56
  module then configures CloudFront to send that header, makes the ALB default
49
- action return `403`, and forwards only requests with the matching header.
57
+ action return `403`, and forwards only requests with the matching header on the
58
+ selected HTTP or HTTPS origin listener.
50
59
  Terraform marks the value sensitive, but it still lives in encrypted Terraform
51
60
  state and in CloudFront/ALB configuration; restrict state, saved plan,
52
61
  CloudFront distribution-read, and ELB listener-rule-read access accordingly.
53
62
 
54
63
  All module resources carry owner, project, environment, service, account, app
55
- type, and cost-center tags. Set `monthly_budget_limit_usd` plus
56
- `budget_alert_email_addresses` in the approved infra root to create AWS Budgets
57
- forecasted and actual spend alerts. Leaving the email list empty skips budget
58
- creation and is not sufficient for live scale-out approval.
64
+ type, and cost-center tags. ECS services enable AWS-managed tags and propagate
65
+ service tags to launched tasks. Any one-off `run-task` smoke must pass the same
66
+ tag set explicitly because it is outside service propagation. Set
67
+ `monthly_budget_limit_usd` plus `budget_alert_email_addresses` in the approved
68
+ infra root to create AWS Budgets forecasted and actual spend alerts. Leaving the
69
+ email list empty skips budget creation and is not sufficient for live scale-out
70
+ approval.
59
71
 
60
72
  Private AWS API egress can be pinned through opt-in VPC endpoints by setting
61
73
  `enable_private_vpc_endpoints = true` and passing `private_route_table_ids`.
@@ -92,6 +104,9 @@ Store instead of Secrets Manager, add `ssm` to
92
104
  ## Current Blockers
93
105
 
94
106
  - Hosted production auth/RBAC still needs scoped, revocable credentials.
107
+ - The default `http-only` CloudFront origin bridge must be replaced with the
108
+ explicit HTTPS-origin mode or consciously accepted with documented risk before
109
+ token-bearing live traffic.
95
110
  - Public probe runtime has SDK-level hosted HTTP target-policy enforcement, but
96
111
  the public-probe worker and cloud check-job lease path are still disabled until
97
112
  they are wired to that runner and validated in AWS.