@hasna/uptime 0.1.24 → 0.1.26

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/index.js CHANGED
@@ -4623,6 +4623,7 @@ var DEFAULT_WORKSPACE_ID = "workspace-id";
4623
4623
  var DEFAULT_VPC_ID = "vpc-xxxxxxxx";
4624
4624
  var DEFAULT_HOSTED_SQLITE_DB = "/data/uptime/uptime.db";
4625
4625
  var DEFAULT_PROTECTED_ACCESS_MODE = "cloudfront_default_domain";
4626
+ var DEFAULT_CLOUDFRONT_ORIGIN_PROTOCOL_POLICY = "http-only";
4626
4627
  function buildAwsDeploymentPlan(options = {}) {
4627
4628
  const region = clean(options.region, DEFAULT_REGION);
4628
4629
  const stage = clean(options.stage, DEFAULT_STAGE);
@@ -4635,8 +4636,11 @@ function buildAwsDeploymentPlan(options = {}) {
4635
4636
  const image = clean(options.image, `${imageRepositoryUri}@sha256:<image-digest>`);
4636
4637
  const evidenceBucket = clean(options.evidenceBucket, `hasna-${stage}-${prefix}-evidence`);
4637
4638
  const hostedSqliteDbPath = clean(options.hostedSqliteDbPath, DEFAULT_HOSTED_SQLITE_DB);
4638
- const runtimePackageVersion = clean(options.runtimePackageVersion, "0.1.24");
4639
+ const runtimePackageVersion = clean(options.runtimePackageVersion, "0.1.26");
4640
+ const runtimePackageIntegrity = options.runtimePackageIntegrity?.trim() || undefined;
4639
4641
  const protectedAccessMode = options.protectedAccessMode ?? DEFAULT_PROTECTED_ACCESS_MODE;
4642
+ const cloudfrontOriginProtocolPolicy = options.cloudfrontOriginProtocolPolicy ?? DEFAULT_CLOUDFRONT_ORIGIN_PROTOCOL_POLICY;
4643
+ const cloudfrontOriginDomainName = clean(options.cloudfrontOriginDomainName, "<alb-dns-name>");
4640
4644
  const protectedAccessUrl = protectedAccessMode === "cloudfront_default_domain" ? "https://<cloudfront-domain>" : `https://${hostname}`;
4641
4645
  const cluster = `${prefix}-${stage}`;
4642
4646
  const secrets = {
@@ -4703,6 +4707,13 @@ function buildAwsDeploymentPlan(options = {}) {
4703
4707
  protectedAccessMode,
4704
4708
  edgeDistribution: protectedAccessMode === "cloudfront_default_domain" ? `${prefix}-${stage}-edge` : undefined,
4705
4709
  protectedAccessUrl,
4710
+ cloudfrontOrigin: protectedAccessMode === "cloudfront_default_domain" ? {
4711
+ protocolPolicy: cloudfrontOriginProtocolPolicy,
4712
+ domainName: cloudfrontOriginProtocolPolicy === "https-only" ? cloudfrontOriginDomainName : "<alb-dns-name>",
4713
+ requiresMatchingCertificate: cloudfrontOriginProtocolPolicy === "https-only",
4714
+ liveTrafficApproved: false,
4715
+ risk: cloudfrontOriginProtocolPolicy === "http-only" ? "Temporary HTTP-origin bridge: web scale-up requires allow_cloudfront_http_origin_live_traffic=true for bounded smokes, or switch to https-only with cloudfront_origin_domain_name plus certificate_arn." : "CloudFront HTTPS-origin mode requires the origin hostname to resolve to the ALB and match certificate_arn."
4716
+ } : undefined,
4706
4717
  originVerification: protectedAccessMode === "cloudfront_default_domain" ? {
4707
4718
  mode: "cloudfront_origin_header",
4708
4719
  requiredBeforeScaleUp: true,
@@ -4735,6 +4746,7 @@ function buildAwsDeploymentPlan(options = {}) {
4735
4746
  repository: ecrRepository,
4736
4747
  uri: image,
4737
4748
  dockerfile: "Dockerfile.package",
4749
+ expectedIntegrity: runtimePackageIntegrity,
4738
4750
  buildCommand: `BLOCKED: after infra approval, AWS CodeBuild builds Dockerfile.package from @hasna/uptime@${runtimePackageVersion} into ${imageRepositoryUri}`,
4739
4751
  pushCommands: [
4740
4752
  `BLOCKED: start ${prefix}-${stage}-image-builder only through the approved deploy pipeline after @hasna/uptime@${runtimePackageVersion} is published`,
@@ -4761,16 +4773,16 @@ function buildAwsDeploymentPlan(options = {}) {
4761
4773
  `Infra PR must declare CodeBuild image builder ${prefix}-${stage}-image-builder for @hasna/uptime@${runtimePackageVersion}.`,
4762
4774
  `Infra PR must declare hardened S3 evidence bucket ${evidenceBucket} with KMS, versioning, lifecycle, and public access block.`,
4763
4775
  `Infra PR must declare encrypted EFS ${prefix}-${stage}-data with access point, mount targets, and AWS Backup plan.`,
4764
- 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.`,
4776
+ protectedAccessMode === "cloudfront_default_domain" ? "Infra PR must declare CloudFront default-domain HTTPS edge, ALB origin 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. Token-bearing live traffic must use cloudfront_origin_protocol_policy=https-only with a matching origin hostname/certificate, or carry explicit allow_cloudfront_http_origin_live_traffic=true bounded-smoke risk acceptance." : `Infra PR must declare ECS/Fargate cluster ${cluster}, ALB HTTPS listener, target groups, security groups, IAM roles, CloudWatch log groups, and Secrets Manager refs.`,
4765
4777
  "Only apply the infra plan from the approved infrastructure repository after review evidence is attached."
4766
4778
  ],
4767
4779
  deploy: [
4768
4780
  "Build and publish the image only after the Dockerfile/container target is reviewed.",
4769
- `Start the AWS image builder for @hasna/uptime@${runtimePackageVersion} and record the pushed image digest.`,
4781
+ runtimePackageIntegrity ? `Start the AWS image builder for @hasna/uptime@${runtimePackageVersion}; it must verify npm dist.integrity ${runtimePackageIntegrity} before extracting the package, then record the pushed image digest.` : `Start the AWS image builder for @hasna/uptime@${runtimePackageVersion}; set runtime_package_integrity from npm dist.integrity before live use, then record the pushed image digest.`,
4770
4782
  "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.",
4771
4783
  `Register task definitions for ${services.map((service) => service.name).join(", ")} using valueFrom secrets.`,
4772
4784
  `Update ECS services in cluster ${cluster} one component at a time through the approved deploy pipeline.`,
4773
- 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.`
4785
+ protectedAccessMode === "cloudfront_default_domain" ? "Use the CloudFront default HTTPS domain with origin verification header binding for first protected access; before token-bearing live traffic, switch the origin to https-only with a matching origin hostname/certificate or set allow_cloudfront_http_origin_live_traffic=true only for a bounded approved smoke." : `Create Route53/edge record for ${hostname} only after ALB health checks pass and auth denial smokes succeed.`
4774
4786
  ],
4775
4787
  rollback: [
4776
4788
  "Keep previous task definition ARNs before each service update.",
@@ -4787,6 +4799,8 @@ function buildAwsDeploymentPlan(options = {}) {
4787
4799
  blockers: [
4788
4800
  "The infrastructure owner repository was not found in this workspace.",
4789
4801
  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.",
4802
+ ...protectedAccessMode === "cloudfront_default_domain" && cloudfrontOriginProtocolPolicy === "http-only" ? ["CloudFront-to-ALB origin transport is still http-only; web scale-up now requires explicit allow_cloudfront_http_origin_live_traffic=true risk acceptance, and token-bearing live traffic should use https-only origin mode."] : [],
4803
+ ...protectedAccessMode === "cloudfront_default_domain" && cloudfrontOriginProtocolPolicy === "https-only" && cloudfrontOriginDomainName === "<alb-dns-name>" ? ["CloudFront https-only origin mode needs cloudfront_origin_domain_name that resolves to the ALB and matches certificate_arn."] : [],
4790
4804
  "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.",
4791
4805
  "Hosted production auth/RBAC must replace broad static hosted-token operation before exposure.",
4792
4806
  "Public probe execution still needs cloud check-job leases wired to runHostedHttpCheck and live policy-decision log evidence.",
@@ -4795,8 +4809,9 @@ function buildAwsDeploymentPlan(options = {}) {
4795
4809
  requiredEvidence: [
4796
4810
  "Infrastructure PR/synth/plan from the approved infra repository.",
4797
4811
  "CodeBuild image-builder run, container smoke, and immutable image digest.",
4812
+ "Published package dist.integrity pinned in the private infra root or an explicit not-live exception.",
4798
4813
  "ECS task definitions using secrets.valueFrom only.",
4799
- "CloudFront-default-domain origin-header config or ALB TLS auth-denial smokes, direct-origin denial evidence, and web alarm checks.",
4814
+ "CloudFront-default-domain origin-header config, origin transport decision, direct-origin denial evidence, auth-denial smokes, and web alarm checks.",
4800
4815
  "Single-writer ECS evidence: one web task maximum and no scheduler/public-probe/reporter EFS mounts.",
4801
4816
  "EFS encryption, access point, mount-target, AWS Backup, and restore-drill evidence.",
4802
4817
  "S3 bucket KMS, versioning, lifecycle, and public-access-block evidence.",
@@ -4809,11 +4824,13 @@ function buildAwsDeploymentPlan(options = {}) {
4809
4824
  notes: [
4810
4825
  "This plan generator does not call AWS.",
4811
4826
  "Blocked plan output intentionally avoids copy-pastable AWS mutation commands.",
4812
- "Default protected access uses CloudFront's HTTPS default domain so first deploy is not blocked on custom DNS or ACM.",
4827
+ "Default protected access uses CloudFront's HTTPS default domain so first zero-count deploy is not blocked on custom DNS or ACM.",
4813
4828
  "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.",
4829
+ "CloudFront HTTPS-origin mode requires a dedicated origin DNS hostname and matching ACM certificate; do not assume the ALB DNS name can satisfy TLS verification.",
4814
4830
  "Hosted runtime uses explicit EFS-backed SQLite at HASNA_UPTIME_HOSTED_SQLITE_DB until the async Postgres adapter exists.",
4815
4831
  "Do not set HASNA_UPTIME_DATABASE_URL for hosted tasks until the Postgres adapter is implemented.",
4816
4832
  "Secrets are represented as secret names/refs and must be injected with valueFrom.",
4833
+ "Set runtime_package_integrity in the approved infra root after publish so the AWS image builder verifies the npm tarball before ECR build.",
4817
4834
  "Actual deploy belongs in the deploy_release_operate_final goal node after infra review."
4818
4835
  ]
4819
4836
  }
@@ -4919,6 +4936,72 @@ function shellEscape(value) {
4919
4936
  return value;
4920
4937
  return `'${value.replace(/'/g, "'\\''")}'`;
4921
4938
  }
4939
+
4940
+ // src/workers.ts
4941
+ var DEFAULT_INTERVAL_MS = 30000;
4942
+ async function runHostedPublicChecksWorker(options) {
4943
+ const intervalMs = normalizePositiveInteger(options.intervalMs ?? DEFAULT_INTERVAL_MS, "intervalMs");
4944
+ const maxRuntimeMs = options.maxRuntimeMs === undefined ? undefined : normalizePositiveInteger(options.maxRuntimeMs, "maxRuntimeMs");
4945
+ const maxIterations = options.maxIterations === undefined ? undefined : normalizePositiveInteger(options.maxIterations, "maxIterations");
4946
+ const clock = options.now ?? (() => new Date);
4947
+ const sleep = options.sleep ?? abortableSleep;
4948
+ const startedAtDate = clock();
4949
+ const startedAt = startedAtDate.toISOString();
4950
+ const deadline = maxRuntimeMs === undefined ? undefined : startedAtDate.getTime() + maxRuntimeMs;
4951
+ let iterations = 0;
4952
+ let checked = 0;
4953
+ while (!options.signal?.aborted) {
4954
+ if (maxIterations !== undefined && iterations >= maxIterations)
4955
+ break;
4956
+ const now = clock();
4957
+ if (deadline !== undefined && now.getTime() >= deadline)
4958
+ break;
4959
+ const iteration = iterations + 1;
4960
+ const iterationStartedAt = now.toISOString();
4961
+ const results = await options.runner.runDueHostedPublicChecks(now, { workspaceId: options.workspaceId });
4962
+ const finishedAt = clock().toISOString();
4963
+ iterations = iteration;
4964
+ checked += results.length;
4965
+ options.onIteration?.({
4966
+ iteration,
4967
+ checked: results.length,
4968
+ startedAt: iterationStartedAt,
4969
+ finishedAt
4970
+ });
4971
+ if (maxIterations !== undefined && iterations >= maxIterations)
4972
+ break;
4973
+ if (deadline !== undefined && clock().getTime() >= deadline)
4974
+ break;
4975
+ await sleep(intervalMs, options.signal);
4976
+ }
4977
+ return {
4978
+ kind: "open-uptime.hosted-public-checks-worker",
4979
+ status: options.signal?.aborted ? "stopped" : "completed",
4980
+ workspaceId: options.workspaceId?.trim() || null,
4981
+ iterations,
4982
+ checked,
4983
+ startedAt,
4984
+ finishedAt: clock().toISOString()
4985
+ };
4986
+ }
4987
+ function normalizePositiveInteger(value, name) {
4988
+ if (!Number.isInteger(value) || value <= 0)
4989
+ throw new Error(`${name} must be a positive integer`);
4990
+ return value;
4991
+ }
4992
+ function abortableSleep(ms, signal) {
4993
+ if (signal?.aborted)
4994
+ return Promise.resolve();
4995
+ return new Promise((resolve) => {
4996
+ const timer = setTimeout(done, ms);
4997
+ function done() {
4998
+ signal?.removeEventListener("abort", done);
4999
+ clearTimeout(timer);
5000
+ resolve();
5001
+ }
5002
+ signal?.addEventListener("abort", done, { once: true });
5003
+ });
5004
+ }
4922
5005
  export {
4923
5006
  verifyProbeResultSignature,
4924
5007
  uptimeHostedFallbackDbPath,
@@ -4931,6 +5014,7 @@ export {
4931
5014
  runMonitorCheck,
4932
5015
  runHttpCheck,
4933
5016
  runHostedTcpCheck,
5017
+ runHostedPublicChecksWorker,
4934
5018
  runHostedHttpCheck,
4935
5019
  runBrowserPageCheck,
4936
5020
  rollbackImport,
@@ -0,0 +1,34 @@
1
+ import type { CheckResult } from "./types.js";
2
+ export interface HostedPublicCheckRunner {
3
+ runDueHostedPublicChecks(now?: Date, options?: {
4
+ workspaceId?: string;
5
+ }): Promise<CheckResult[]>;
6
+ }
7
+ export interface HostedPublicChecksWorkerOptions {
8
+ runner: HostedPublicCheckRunner;
9
+ workspaceId?: string;
10
+ intervalMs?: number;
11
+ maxRuntimeMs?: number;
12
+ maxIterations?: number;
13
+ signal?: AbortSignal;
14
+ now?: () => Date;
15
+ sleep?: (ms: number, signal?: AbortSignal) => Promise<void>;
16
+ onIteration?: (iteration: HostedPublicChecksWorkerIteration) => void;
17
+ }
18
+ export interface HostedPublicChecksWorkerIteration {
19
+ iteration: number;
20
+ checked: number;
21
+ startedAt: string;
22
+ finishedAt: string;
23
+ }
24
+ export interface HostedPublicChecksWorkerSummary {
25
+ kind: "open-uptime.hosted-public-checks-worker";
26
+ status: "completed" | "stopped";
27
+ workspaceId: string | null;
28
+ iterations: number;
29
+ checked: number;
30
+ startedAt: string;
31
+ finishedAt: string;
32
+ }
33
+ export declare function runHostedPublicChecksWorker(options: HostedPublicChecksWorkerOptions): Promise<HostedPublicChecksWorkerSummary>;
34
+ //# sourceMappingURL=workers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workers.d.ts","sourceRoot":"","sources":["../src/workers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C,MAAM,WAAW,uBAAuB;IACtC,wBAAwB,CAAC,GAAG,CAAC,EAAE,IAAI,EAAE,OAAO,CAAC,EAAE;QAAE,WAAW,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC,CAAC;CAClG;AAED,MAAM,WAAW,+BAA+B;IAC9C,MAAM,EAAE,uBAAuB,CAAC;IAChC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,GAAG,CAAC,EAAE,MAAM,IAAI,CAAC;IACjB,KAAK,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5D,WAAW,CAAC,EAAE,CAAC,SAAS,EAAE,iCAAiC,KAAK,IAAI,CAAC;CACtE;AAED,MAAM,WAAW,iCAAiC;IAChD,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,+BAA+B;IAC9C,IAAI,EAAE,yCAAyC,CAAC;IAChD,MAAM,EAAE,WAAW,GAAG,SAAS,CAAC;IAChC,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;CACpB;AAID,wBAAsB,2BAA2B,CAAC,OAAO,EAAE,+BAA+B,GAAG,OAAO,CAAC,+BAA+B,CAAC,CA4CpI"}
@@ -0,0 +1,69 @@
1
+ // @bun
2
+ // src/workers.ts
3
+ var DEFAULT_INTERVAL_MS = 30000;
4
+ async function runHostedPublicChecksWorker(options) {
5
+ const intervalMs = normalizePositiveInteger(options.intervalMs ?? DEFAULT_INTERVAL_MS, "intervalMs");
6
+ const maxRuntimeMs = options.maxRuntimeMs === undefined ? undefined : normalizePositiveInteger(options.maxRuntimeMs, "maxRuntimeMs");
7
+ const maxIterations = options.maxIterations === undefined ? undefined : normalizePositiveInteger(options.maxIterations, "maxIterations");
8
+ const clock = options.now ?? (() => new Date);
9
+ const sleep = options.sleep ?? abortableSleep;
10
+ const startedAtDate = clock();
11
+ const startedAt = startedAtDate.toISOString();
12
+ const deadline = maxRuntimeMs === undefined ? undefined : startedAtDate.getTime() + maxRuntimeMs;
13
+ let iterations = 0;
14
+ let checked = 0;
15
+ while (!options.signal?.aborted) {
16
+ if (maxIterations !== undefined && iterations >= maxIterations)
17
+ break;
18
+ const now = clock();
19
+ if (deadline !== undefined && now.getTime() >= deadline)
20
+ break;
21
+ const iteration = iterations + 1;
22
+ const iterationStartedAt = now.toISOString();
23
+ const results = await options.runner.runDueHostedPublicChecks(now, { workspaceId: options.workspaceId });
24
+ const finishedAt = clock().toISOString();
25
+ iterations = iteration;
26
+ checked += results.length;
27
+ options.onIteration?.({
28
+ iteration,
29
+ checked: results.length,
30
+ startedAt: iterationStartedAt,
31
+ finishedAt
32
+ });
33
+ if (maxIterations !== undefined && iterations >= maxIterations)
34
+ break;
35
+ if (deadline !== undefined && clock().getTime() >= deadline)
36
+ break;
37
+ await sleep(intervalMs, options.signal);
38
+ }
39
+ return {
40
+ kind: "open-uptime.hosted-public-checks-worker",
41
+ status: options.signal?.aborted ? "stopped" : "completed",
42
+ workspaceId: options.workspaceId?.trim() || null,
43
+ iterations,
44
+ checked,
45
+ startedAt,
46
+ finishedAt: clock().toISOString()
47
+ };
48
+ }
49
+ function normalizePositiveInteger(value, name) {
50
+ if (!Number.isInteger(value) || value <= 0)
51
+ throw new Error(`${name} must be a positive integer`);
52
+ return value;
53
+ }
54
+ function abortableSleep(ms, signal) {
55
+ if (signal?.aborted)
56
+ return Promise.resolve();
57
+ return new Promise((resolve) => {
58
+ const timer = setTimeout(done, ms);
59
+ function done() {
60
+ signal?.removeEventListener("abort", done);
61
+ clearTimeout(timer);
62
+ resolve();
63
+ }
64
+ signal?.addEventListener("abort", done, { once: true });
65
+ });
66
+ }
67
+ export {
68
+ runHostedPublicChecksWorker
69
+ };
@@ -60,9 +60,14 @@ start a private probe until the JSON output says `canStart: true`.
60
60
 
61
61
  4. Confirm the target VPC, private subnets, KMS key, and EFS/Backup plan inputs
62
62
  still match the plan.
63
- 5. Confirm the protected access mode. The first deploy can use the CloudFront
64
- default HTTPS domain without custom DNS or ACM. Custom hostname deploys still
65
- 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.
66
71
  6. Confirm the deployment role uses short-lived credentials or OIDC, not copied
67
72
  access keys.
68
73
  7. Create a private evidence directory outside the public repository. Store
@@ -77,9 +82,12 @@ The plan expects:
77
82
  - ECS/Fargate cluster with separate services for web, scheduler, public probe,
78
83
  reporter, and one-off migrations. In the current EFS SQLite bridge, only web
79
84
  may be enabled and it must run at desired count `0` or `1`.
80
- - CloudFront default-domain HTTPS edge plus ALB HTTP origin restricted to
81
- CloudFront origin-facing ranges, or an ALB HTTPS listener with ACM certificate
82
- 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.
83
91
  - Encrypted EFS file system, access point, mount targets, and AWS Backup plan
84
92
  for `HASNA_UPTIME_HOSTED_SQLITE_DB=/data/uptime/uptime.db`.
85
93
  - S3 bucket for redacted browser evidence and generated report artifacts.
@@ -151,6 +159,12 @@ aws codebuild start-build \
151
159
  --project-name "$IMAGE_BUILDER_PROJECT"
152
160
  ```
153
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
+
154
168
  Update the approved infra root so `container_image` is the immutable ECR digest,
155
169
  then re-plan with all services still at `0`.
156
170
 
@@ -179,8 +193,13 @@ Before setting `desired_counts.web = 1`, verify:
179
193
  - the image is an immutable digest, not a mutable tag or placeholder;
180
194
  - required secrets have `AWSCURRENT` versions;
181
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 by setting
199
+ `allow_cloudfront_http_origin_live_traffic = true`;
182
200
  - CloudFront origin access is distribution-bound with the CloudFront-only origin
183
- verification header, not just narrowed to CloudFront origin-facing ranges;
201
+ verification header by setting `enable_cloudfront_origin_verify_header = true`,
202
+ not just narrowed to CloudFront origin-facing ranges;
184
203
  - web egress to ECR, Secrets Manager or SSM, CloudWatch Logs, S3, EFS, and any
185
204
  required endpoints has been proven from a real ECS task. Terraform endpoint
186
205
  ids, route tables, and security-group rules are creation evidence only; the
@@ -405,9 +424,10 @@ routes are backed by cloud check jobs and cloud audit rows.
405
424
  hosted public-check runner, records target-policy decision evidence, and
406
425
  passes AWS smokes for denied DNS answers, redirect-to-denied targets, and
407
426
  address pinning. The SDK and `uptime cloud public-checks run-due` path now
408
- handle execution-time DNS and redirect enforcement for bounded smokes, but a
409
- sustained public-probe worker loop is not active until it is wired to cloud
410
- leases.
427
+ handle execution-time DNS and redirect enforcement for bounded smokes. The
428
+ `uptime cloud public-checks worker` command is an EFS SQLite bridge loop for
429
+ controlled smoke testing only; it is not the final cloud
430
+ `check_jobs`/lease/fencing protocol.
411
431
  - Do not enable scheduler, public-probe, reporter, or migration workers against
412
432
  the EFS SQLite bridge; those services need Postgres/cloud leases first.
413
433
  - Do not expose dashboard/API routes without hosted auth and workspace checks.
@@ -419,6 +439,10 @@ routes are backed by cloud check jobs and cloud audit rows.
419
439
  the public repo and shared logs. Terraform redacts the sensitive input in CLI
420
440
  output, but the value is still stored in encrypted Terraform state, saved plan
421
441
  files, and AWS CloudFront/ALB configuration; restrict access accordingly.
442
+ - Do not treat `cloudfront_origin_protocol_policy = "http-only"` as final for
443
+ token-bearing traffic. The module supports `https-only`, but that mode needs a
444
+ real origin DNS name and matching ACM certificate because CloudFront verifies
445
+ the custom-origin TLS certificate against the origin host.
422
446
  - Do not treat local SQLite, local project DBs, or private-probe local state as cloud
423
447
  authority after cutover.
424
448
  - 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,12 @@ 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. Terraform blocks
457
+ `desired_counts.web > 0` in CloudFront mode unless origin verification is
458
+ enabled and either HTTPS-origin mode is configured or
459
+ `allow_cloudfront_http_origin_live_traffic = true` records explicit bounded
460
+ smoke risk acceptance. Full production identity/RBAC is still not implemented.
443
461
  - Open Uptime is still SQLite-only for this bridge; only one protected web task
444
462
  may write EFS until Postgres and cloud leases exist.
445
463
  - Hosted API/dashboard auth, workspace RBAC, target policy, and Postgres leases
@@ -463,8 +481,12 @@ required before browser evidence or public probe scale-out.
463
481
  EFS SQLite bridge is explicitly temporary and not the target source of truth.
464
482
  - ECS task definitions use secret refs, not plaintext secret values.
465
483
  - ECS task definitions include explicit container health checks: web checks
466
- `/health`, while disabled non-web roles use a hosted-environment sanity check
467
- until their long-running worker commands are implemented.
484
+ `/health`, while disabled non-web roles run
485
+ `uptime cloud workers preflight --role <role> --healthcheck` and fail their
486
+ environment health check if hosted mode, component identity, or workspace env
487
+ is invalid. Their main commands call fail-closed
488
+ `uptime cloud workers run --role <role>` entrypoints until the real cloud data
489
+ paths are implemented.
468
490
  - Public probes cannot reach denied target classes; private monitors require
469
491
  private probes and approved inventory refs.
470
492
  - Backups, restore drill, rollback sequence, alarms, and cost estimate are
@@ -430,9 +430,10 @@ ECS/API/RDS/S3/probe lag/job backlog/delivery failures, and rollback commands.
430
430
  dashboard shell still fails closed; production-grade identity/RBAC is not
431
431
  implemented yet.
432
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
+ `uptime cloud public-checks run-due` operator path. A bounded
434
+ `uptime cloud public-checks worker` EFS SQLite bridge loop exists for
435
+ controlled smokes, but the cloud public-probe `check_jobs` lease/fencing path
436
+ and sustained ECS worker readiness are not wired yet.
436
437
  - `@hasna/cloud` hybrid mode still returns SQLite, so it is not cloud-primary.
437
438
  - The local cloud config currently points at a stale/non-resolving database host.
438
439
  - Todos has unresolved conflicts that must be reconciled before cloud cutover.
@@ -454,7 +455,10 @@ ECS/API/RDS/S3/probe lag/job backlog/delivery failures, and rollback commands.
454
455
  representative SQLite EFS backup/restore drill with integrity/count checks.
455
456
  It is not live: live scale-up is still blocked by edge/auth smokes, approved
456
457
  human/on-call SNS subscriptions and delivery smoke, and production auth
457
- hardening beyond scoped static operator tokens.
458
+ hardening beyond scoped static operator tokens. Terraform now prevents
459
+ accidental `web > 0` promotion in CloudFront mode unless origin verification
460
+ is enabled and either HTTPS-origin mode or explicit HTTP-origin risk
461
+ acceptance is configured.
458
462
  - Projects per-project cloud stores do not exist yet; current local
459
463
  `project.db` stores are not enough for cloud-backed canvases or JSON Render.
460
464
  - Browser/page monitoring lacks the artifact, redaction, retention, and storage
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "service": "open-uptime",
3
3
  "package": "@hasna/uptime",
4
- "intendedVersion": "0.1.24",
4
+ "intendedVersion": "0.1.26",
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`.
@@ -78,9 +90,12 @@ delivery, S3 access, and EFS mount behavior.
78
90
 
79
91
  Every ECS task definition includes an explicit container health check. The web
80
92
  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.
93
+ run `uptime cloud workers preflight --role <role> --healthcheck`, which verifies
94
+ hosted mode, component identity, and workspace env before reporting blocked
95
+ cloud prerequisites. Their main container commands call fail-closed
96
+ `uptime cloud workers run --role <role>` entrypoints so scheduler,
97
+ public-probe, reporter, and migration tasks no longer use `cloud plan` as a
98
+ placeholder.
84
99
 
85
100
  Interface endpoint private DNS is VPC-wide. In shared VPCs, either keep endpoint
86
101
  creation in the approved networking root, or pass
@@ -92,9 +107,18 @@ Store instead of Secrets Manager, add `ssm` to
92
107
  ## Current Blockers
93
108
 
94
109
  - Hosted production auth/RBAC still needs scoped, revocable credentials.
110
+ - The default `http-only` CloudFront origin bridge must be replaced with the
111
+ explicit HTTPS-origin mode or consciously accepted with documented risk before
112
+ token-bearing live traffic. The module blocks `desired_counts.web > 0` in
113
+ CloudFront mode unless origin verification is enabled, and it also requires
114
+ either `cloudfront_origin_protocol_policy = "https-only"` or explicit
115
+ `allow_cloudfront_http_origin_live_traffic = true` risk acceptance for bounded
116
+ smokes.
95
117
  - Public probe runtime has SDK-level hosted HTTP target-policy enforcement, but
96
- the public-probe worker and cloud check-job lease path are still disabled until
97
- they are wired to that runner and validated in AWS.
118
+ the public-probe cloud check-job lease path is still disabled until it is
119
+ wired to that runner and validated in AWS. The
120
+ `uptime cloud public-checks worker` command is an EFS SQLite bridge smoke loop,
121
+ not the final cloud worker protocol.
98
122
  - Hosted private-probe enrollment/heartbeat/revocation is still
99
123
  fail-closed.
100
124