@hasna/uptime 0.1.25 → 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/CHANGELOG.md CHANGED
@@ -6,6 +6,24 @@ project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.1.26] - 2026-06-29
10
+
11
+ ### Added
12
+
13
+ - Added explicit hosted worker preflight and fail-closed run entrypoints for
14
+ scheduler, public-probe, reporter, and migration roles.
15
+ - Added a bounded `uptime cloud public-checks worker` loop around the existing
16
+ hosted public-check smoke primitive for EFS SQLite bridge testing.
17
+
18
+ ### Changed
19
+
20
+ - Updated AWS non-web task definitions to use explicit fail-closed worker
21
+ commands and environment-aware preflight health checks instead of the
22
+ placeholder `cloud plan` command.
23
+ - Hardened Terraform scale-up validation so `web > 0` in CloudFront mode
24
+ requires origin verification and either HTTPS-origin mode or explicit
25
+ `allow_cloudfront_http_origin_live_traffic` risk acceptance.
26
+
9
27
  ## [0.1.25] - 2026-06-29
10
28
 
11
29
  ### Added
package/README.md CHANGED
@@ -32,6 +32,8 @@ uptime report-schedules run-due
32
32
  uptime report-schedules runs
33
33
  uptime audit
34
34
  uptime cloud plan --json
35
+ uptime cloud workers preflight --role public-probe --json
36
+ uptime cloud public-checks worker --workspace-id ws_internal --max-iterations 1 --hosted-sqlite-db /data/uptime/uptime.db
35
37
  uptime cloud private-probe-config --probe-id prb_private_01 --machine-id private-probe-01 --json
36
38
  uptime cloud private-probe-config --probe-id prb_private_01 --machine-id private-probe-01 --env --allow-blocked-env
37
39
  uptime incidents
@@ -63,6 +65,12 @@ acceptance. Hosted AWS runtime state currently uses explicit EFS-backed SQLite v
63
65
  `HASNA_UPTIME_HOSTED_SQLITE_DB=/data/uptime/uptime.db` for one protected web
64
66
  task maximum; do not set `HASNA_UPTIME_DATABASE_URL` until the async Postgres
65
67
  adapter is implemented.
68
+ `uptime cloud workers preflight --role <role>` reports why hosted scheduler,
69
+ public-probe, reporter, and migration roles remain blocked. Their `run`
70
+ entrypoints fail closed until Postgres, check jobs, channel refs, and migration
71
+ plans exist. `uptime cloud public-checks worker` is only a bounded EFS SQLite
72
+ bridge loop around hosted HTTP/TCP smoke checks; it is not the final cloud
73
+ `check_jobs`/lease/fencing protocol.
66
74
  `Dockerfile.package` is used by the Terraform CodeBuild image builder to build
67
75
  the published npm package into ECR from inside AWS.
68
76
 
@@ -224,7 +232,7 @@ check jobs, workspace stores, and audit logging are implemented. Local job reads
224
232
  redact fencing tokens; the claim response is the only API response that returns
225
233
  the active fencing token.
226
234
 
227
- Hosted `/api/v1/report-schedules*`, `/api/v1/report-runs`, and
235
+ Hosted `POST /api/v1/report`, `/api/v1/report-schedules*`, `/api/v1/report-runs`, and
228
236
  `/api/v1/audit-events` also fail closed until cloud channel refs, workspace
229
237
  stores, and cloud audit logging are implemented.
230
238
 
package/dist/cli/index.js CHANGED
@@ -7230,7 +7230,7 @@ function buildAwsDeploymentPlan(options = {}) {
7230
7230
  const image = clean(options.image, `${imageRepositoryUri}@sha256:<image-digest>`);
7231
7231
  const evidenceBucket = clean(options.evidenceBucket, `hasna-${stage}-${prefix}-evidence`);
7232
7232
  const hostedSqliteDbPath = clean(options.hostedSqliteDbPath, DEFAULT_HOSTED_SQLITE_DB);
7233
- const runtimePackageVersion = clean(options.runtimePackageVersion, "0.1.25");
7233
+ const runtimePackageVersion = clean(options.runtimePackageVersion, "0.1.26");
7234
7234
  const runtimePackageIntegrity = options.runtimePackageIntegrity?.trim() || undefined;
7235
7235
  const protectedAccessMode = options.protectedAccessMode ?? DEFAULT_PROTECTED_ACCESS_MODE;
7236
7236
  const cloudfrontOriginProtocolPolicy = options.cloudfrontOriginProtocolPolicy ?? DEFAULT_CLOUDFRONT_ORIGIN_PROTOCOL_POLICY;
@@ -7306,7 +7306,7 @@ function buildAwsDeploymentPlan(options = {}) {
7306
7306
  domainName: cloudfrontOriginProtocolPolicy === "https-only" ? cloudfrontOriginDomainName : "<alb-dns-name>",
7307
7307
  requiresMatchingCertificate: cloudfrontOriginProtocolPolicy === "https-only",
7308
7308
  liveTrafficApproved: false,
7309
- risk: cloudfrontOriginProtocolPolicy === "http-only" ? "Temporary HTTP-origin bridge: do not use for token-bearing live traffic without explicit risk acceptance, 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."
7309
+ 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."
7310
7310
  } : undefined,
7311
7311
  originVerification: protectedAccessMode === "cloudfront_default_domain" ? {
7312
7312
  mode: "cloudfront_origin_header",
@@ -7367,7 +7367,7 @@ function buildAwsDeploymentPlan(options = {}) {
7367
7367
  `Infra PR must declare CodeBuild image builder ${prefix}-${stage}-image-builder for @hasna/uptime@${runtimePackageVersion}.`,
7368
7368
  `Infra PR must declare hardened S3 evidence bucket ${evidenceBucket} with KMS, versioning, lifecycle, and public access block.`,
7369
7369
  `Infra PR must declare encrypted EFS ${prefix}-${stage}-data with access point, mount targets, and AWS Backup plan.`,
7370
- 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 HTTP-origin 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.`,
7370
+ 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.`,
7371
7371
  "Only apply the infra plan from the approved infrastructure repository after review evidence is attached."
7372
7372
  ],
7373
7373
  deploy: [
@@ -7376,7 +7376,7 @@ function buildAwsDeploymentPlan(options = {}) {
7376
7376
  "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.",
7377
7377
  `Register task definitions for ${services.map((service) => service.name).join(", ")} using valueFrom secrets.`,
7378
7378
  `Update ECS services in cluster ${cluster} one component at a time through the approved deploy pipeline.`,
7379
- 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 record explicit HTTP-origin risk acceptance." : `Create Route53/edge record for ${hostname} only after ALB health checks pass and auth denial smokes succeed.`
7379
+ 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.`
7380
7380
  ],
7381
7381
  rollback: [
7382
7382
  "Keep previous task definition ARNs before each service update.",
@@ -7393,7 +7393,7 @@ function buildAwsDeploymentPlan(options = {}) {
7393
7393
  blockers: [
7394
7394
  "The infrastructure owner repository was not found in this workspace.",
7395
7395
  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.",
7396
- ...protectedAccessMode === "cloudfront_default_domain" && cloudfrontOriginProtocolPolicy === "http-only" ? ["CloudFront-to-ALB origin transport is still http-only; token-bearing live traffic needs https-only origin mode or explicit risk acceptance."] : [],
7396
+ ...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."] : [],
7397
7397
  ...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."] : [],
7398
7398
  "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.",
7399
7399
  "Hosted production auth/RBAC must replace broad static hosted-token operation before exposure.",
@@ -7531,6 +7531,72 @@ function shellEscape(value) {
7531
7531
  return `'${value.replace(/'/g, "'\\''")}'`;
7532
7532
  }
7533
7533
 
7534
+ // src/workers.ts
7535
+ var DEFAULT_INTERVAL_MS = 30000;
7536
+ async function runHostedPublicChecksWorker(options) {
7537
+ const intervalMs = normalizePositiveInteger(options.intervalMs ?? DEFAULT_INTERVAL_MS, "intervalMs");
7538
+ const maxRuntimeMs = options.maxRuntimeMs === undefined ? undefined : normalizePositiveInteger(options.maxRuntimeMs, "maxRuntimeMs");
7539
+ const maxIterations = options.maxIterations === undefined ? undefined : normalizePositiveInteger(options.maxIterations, "maxIterations");
7540
+ const clock = options.now ?? (() => new Date);
7541
+ const sleep = options.sleep ?? abortableSleep;
7542
+ const startedAtDate = clock();
7543
+ const startedAt = startedAtDate.toISOString();
7544
+ const deadline = maxRuntimeMs === undefined ? undefined : startedAtDate.getTime() + maxRuntimeMs;
7545
+ let iterations = 0;
7546
+ let checked = 0;
7547
+ while (!options.signal?.aborted) {
7548
+ if (maxIterations !== undefined && iterations >= maxIterations)
7549
+ break;
7550
+ const now = clock();
7551
+ if (deadline !== undefined && now.getTime() >= deadline)
7552
+ break;
7553
+ const iteration = iterations + 1;
7554
+ const iterationStartedAt = now.toISOString();
7555
+ const results = await options.runner.runDueHostedPublicChecks(now, { workspaceId: options.workspaceId });
7556
+ const finishedAt = clock().toISOString();
7557
+ iterations = iteration;
7558
+ checked += results.length;
7559
+ options.onIteration?.({
7560
+ iteration,
7561
+ checked: results.length,
7562
+ startedAt: iterationStartedAt,
7563
+ finishedAt
7564
+ });
7565
+ if (maxIterations !== undefined && iterations >= maxIterations)
7566
+ break;
7567
+ if (deadline !== undefined && clock().getTime() >= deadline)
7568
+ break;
7569
+ await sleep(intervalMs, options.signal);
7570
+ }
7571
+ return {
7572
+ kind: "open-uptime.hosted-public-checks-worker",
7573
+ status: options.signal?.aborted ? "stopped" : "completed",
7574
+ workspaceId: options.workspaceId?.trim() || null,
7575
+ iterations,
7576
+ checked,
7577
+ startedAt,
7578
+ finishedAt: clock().toISOString()
7579
+ };
7580
+ }
7581
+ function normalizePositiveInteger(value, name) {
7582
+ if (!Number.isInteger(value) || value <= 0)
7583
+ throw new Error(`${name} must be a positive integer`);
7584
+ return value;
7585
+ }
7586
+ function abortableSleep(ms, signal) {
7587
+ if (signal?.aborted)
7588
+ return Promise.resolve();
7589
+ return new Promise((resolve) => {
7590
+ const timer = setTimeout(done, ms);
7591
+ function done() {
7592
+ signal?.removeEventListener("abort", done);
7593
+ clearTimeout(timer);
7594
+ resolve();
7595
+ }
7596
+ signal?.addEventListener("abort", done, { once: true });
7597
+ });
7598
+ }
7599
+
7534
7600
  // src/cli/index.ts
7535
7601
  var program2 = new Command;
7536
7602
  program2.name("uptime").description("Local-first uptime and downtime monitoring").version(packageVersion()).option("-j, --json", "print JSON");
@@ -7891,6 +7957,32 @@ cloud.command("private-probe-config").description("Generate hosted-targeted priv
7891
7957
  fail(error);
7892
7958
  }
7893
7959
  });
7960
+ var cloudWorkers = cloud.command("workers").description("Inspect and run hosted worker entrypoints");
7961
+ cloudWorkers.command("preflight").description("Check one hosted worker entrypoint without starting work").requiredOption("--role <role>", "scheduler, public-probe, reporter, or migration").option("--healthcheck", "exit non-zero when hosted mode, component, or workspace env is invalid").option("-j, --json", "print JSON").action((opts) => {
7962
+ try {
7963
+ const preflight = buildHostedWorkerPreflight(parseWorkerRole(opts.role));
7964
+ print(preflight, renderHostedWorkerPreflight(preflight), opts);
7965
+ if (opts.healthcheck && !hostedWorkerEnvironmentOk(preflight))
7966
+ process.exit(1);
7967
+ } catch (error) {
7968
+ fail(error);
7969
+ }
7970
+ });
7971
+ cloudWorkers.command("run").description("Run one hosted worker entrypoint; fails closed until cloud prerequisites exist").requiredOption("--role <role>", "scheduler, public-probe, reporter, or migration").option("-j, --json", "print JSON").action((opts) => {
7972
+ try {
7973
+ const preflight = buildHostedWorkerPreflight(parseWorkerRole(opts.role));
7974
+ const error = `hosted ${preflight.role} worker runtime is blocked until cloud prerequisites exist`;
7975
+ if (wantsJson(opts)) {
7976
+ console.log(JSON.stringify({ ok: false, error, preflight }, null, 2));
7977
+ } else {
7978
+ console.error(source_default.red(sanitizeTerminal(error)));
7979
+ console.error(renderHostedWorkerPreflight(preflight));
7980
+ }
7981
+ process.exit(1);
7982
+ } catch (error) {
7983
+ fail(error);
7984
+ }
7985
+ });
7894
7986
  var cloudPublicChecks = cloud.command("public-checks").description("Run hosted public HTTP/TCP checks against the configured hosted store");
7895
7987
  cloudPublicChecks.command("run-due").description("Run due hosted public HTTP/TCP checks for one workspace").option("--workspace-id <id>", "workspace id; defaults to HASNA_UPTIME_WORKSPACE_ID").option("--now <iso>", "due timestamp", new Date().toISOString()).option("--hosted-sqlite-db <path>", "hosted SQLite path on cloud-mounted storage").option("--allow-hosted-local-store", "allow hosted mode to use local SQLite as an explicit fallback").option("-j, --json", "print JSON").action(async (opts) => {
7896
7988
  try {
@@ -7907,6 +7999,37 @@ cloudPublicChecks.command("run-due").description("Run due hosted public HTTP/TCP
7907
7999
  fail(error);
7908
8000
  }
7909
8001
  });
8002
+ cloudPublicChecks.command("worker").description("Run a bounded EFS SQLite bridge loop around hosted public checks").option("--workspace-id <id>", "workspace id; defaults to HASNA_UPTIME_WORKSPACE_ID").option("--interval-ms <ms>", "sleep interval between iterations", parseInteger, 30000).option("--max-runtime-ms <ms>", "stop after this many milliseconds", parseInteger).option("--max-iterations <n>", "stop after this many iterations", parseInteger).option("--hosted-sqlite-db <path>", "hosted SQLite path on cloud-mounted storage").option("--allow-hosted-local-store", "allow hosted mode to use local SQLite as an explicit fallback").option("-j, --json", "print JSON").action(async (opts) => {
8003
+ const abortController = new AbortController;
8004
+ const onSignal = () => abortController.abort();
8005
+ process.once("SIGINT", onSignal);
8006
+ process.once("SIGTERM", onSignal);
8007
+ try {
8008
+ const svc = hostedService({
8009
+ hostedSqliteDb: opts.hostedSqliteDb,
8010
+ allowHostedLocalStore: opts.allowHostedLocalStore
8011
+ });
8012
+ const workspaceId = opts.workspaceId || process.env.HASNA_UPTIME_WORKSPACE_ID;
8013
+ const summary = await runHostedPublicChecksWorker({
8014
+ runner: svc,
8015
+ workspaceId,
8016
+ intervalMs: opts.intervalMs,
8017
+ maxRuntimeMs: opts.maxRuntimeMs,
8018
+ maxIterations: opts.maxIterations,
8019
+ signal: abortController.signal,
8020
+ onIteration: wantsJson(opts) ? undefined : (iteration) => {
8021
+ console.log(`iteration ${iteration.iteration}: checked ${iteration.checked}`);
8022
+ }
8023
+ });
8024
+ svc.close();
8025
+ print(summary, renderHostedPublicChecksWorkerSummary(summary), opts);
8026
+ } catch (error) {
8027
+ fail(error);
8028
+ } finally {
8029
+ process.removeListener("SIGINT", onSignal);
8030
+ process.removeListener("SIGTERM", onSignal);
8031
+ }
8032
+ });
7910
8033
  program2.command("results").description("List recent check results").option("--monitor <id>", "filter by monitor id").option("--limit <n>", "max rows", parseInteger, 20).option("-j, --json", "print JSON").action((opts) => {
7911
8034
  try {
7912
8035
  const svc = service();
@@ -8299,6 +8422,101 @@ function renderPrivateProbeConfig(config) {
8299
8422
  ].join(`
8300
8423
  `);
8301
8424
  }
8425
+ function renderHostedPublicChecksWorkerSummary(summary) {
8426
+ return [
8427
+ "hosted public checks worker",
8428
+ `status: ${summary.status}`,
8429
+ `workspace: ${summary.workspaceId ?? "<unset>"}`,
8430
+ `iterations: ${summary.iterations}`,
8431
+ `checked: ${summary.checked}`,
8432
+ `started: ${summary.startedAt}`,
8433
+ `finished: ${summary.finishedAt}`
8434
+ ].join(`
8435
+ `);
8436
+ }
8437
+ function parseWorkerRole(value) {
8438
+ if (value === "scheduler" || value === "public-probe" || value === "reporter" || value === "migration")
8439
+ return value;
8440
+ throw new Error(`Unknown hosted worker role: ${value}`);
8441
+ }
8442
+ function buildHostedWorkerPreflight(role) {
8443
+ const mode = process.env.HASNA_UPTIME_MODE?.trim() || "";
8444
+ const component = process.env.HASNA_UPTIME_COMPONENT?.trim() || "";
8445
+ const workspaceId = process.env.HASNA_UPTIME_WORKSPACE_ID?.trim() || "";
8446
+ const checks = [
8447
+ { name: "hosted-mode", ok: mode === "hosted", detail: mode || "<unset>" },
8448
+ { name: "component", ok: !component || component === role, detail: component || "<unset>" },
8449
+ { name: "workspace", ok: Boolean(workspaceId), detail: workspaceId || "<unset>" },
8450
+ { name: "postgres-adapter", ok: false, detail: "not implemented" },
8451
+ { name: "cloud-worker-leases", ok: false, detail: "not implemented" }
8452
+ ];
8453
+ if (role === "reporter") {
8454
+ checks.push({ name: "cloud-channel-refs", ok: false, detail: "not implemented" });
8455
+ }
8456
+ if (role === "public-probe") {
8457
+ checks.push({ name: "public-probe-job-claims", ok: false, detail: "not implemented" });
8458
+ }
8459
+ if (role === "migration") {
8460
+ checks.push({ name: "cloud-migration-plan", ok: false, detail: "not implemented" });
8461
+ }
8462
+ const blockers = checks.filter((check) => !check.ok).map((check) => `${check.name}: ${check.detail}`);
8463
+ return {
8464
+ kind: "open-uptime.hosted-worker-preflight",
8465
+ role,
8466
+ status: "blocked",
8467
+ canStart: false,
8468
+ mode: mode || "<unset>",
8469
+ component: component || "<unset>",
8470
+ workspaceId: workspaceId || null,
8471
+ blockers,
8472
+ checks,
8473
+ nextActions: hostedWorkerNextActions(role)
8474
+ };
8475
+ }
8476
+ function hostedWorkerNextActions(role) {
8477
+ const shared = [
8478
+ "Keep the ECS service desired count at 0 until this preflight reports canStart=true.",
8479
+ "Move authoritative hosted state from the EFS SQLite bridge to the cloud store with transactional leases."
8480
+ ];
8481
+ if (role === "scheduler") {
8482
+ return [
8483
+ ...shared,
8484
+ "Implement deterministic check_jobs creation with a scheduler lease and duplicate-slot protection."
8485
+ ];
8486
+ }
8487
+ if (role === "public-probe") {
8488
+ return [
8489
+ ...shared,
8490
+ "Implement cloud job claiming, fencing tokens, target-policy decision logs, and result ingestion for public HTTP/TCP checks."
8491
+ ];
8492
+ }
8493
+ if (role === "reporter") {
8494
+ return [
8495
+ ...shared,
8496
+ "Implement workspace-authorized report channel refs, idempotent delivery keys, retry/backoff, and delivery alarms."
8497
+ ];
8498
+ }
8499
+ return [
8500
+ ...shared,
8501
+ "Implement reviewed cloud schema migrations with dry-run counts, backup evidence, and rollback instructions."
8502
+ ];
8503
+ }
8504
+ function renderHostedWorkerPreflight(preflight) {
8505
+ return [
8506
+ `${preflight.role} hosted worker preflight`,
8507
+ `status: ${preflight.status}`,
8508
+ `can start: ${preflight.canStart}`,
8509
+ `mode: ${sanitizeField(preflight.mode)}`,
8510
+ `component: ${sanitizeField(preflight.component)}`,
8511
+ `workspace: ${sanitizeField(preflight.workspaceId ?? "<unset>")}`,
8512
+ `blockers: ${preflight.blockers.length}`,
8513
+ ...preflight.blockers.map((blocker) => `- ${sanitizeField(blocker)}`)
8514
+ ].join(`
8515
+ `);
8516
+ }
8517
+ function hostedWorkerEnvironmentOk(preflight) {
8518
+ return preflight.checks.filter((check) => check.name === "hosted-mode" || check.name === "component" || check.name === "workspace").every((check) => check.ok);
8519
+ }
8302
8520
  function renderDeliveries(deliveries) {
8303
8521
  if (deliveries.length === 0)
8304
8522
  return "No report deliveries requested";
@@ -22,7 +22,7 @@ function buildAwsDeploymentPlan(options = {}) {
22
22
  const image = clean(options.image, `${imageRepositoryUri}@sha256:<image-digest>`);
23
23
  const evidenceBucket = clean(options.evidenceBucket, `hasna-${stage}-${prefix}-evidence`);
24
24
  const hostedSqliteDbPath = clean(options.hostedSqliteDbPath, DEFAULT_HOSTED_SQLITE_DB);
25
- const runtimePackageVersion = clean(options.runtimePackageVersion, "0.1.25");
25
+ const runtimePackageVersion = clean(options.runtimePackageVersion, "0.1.26");
26
26
  const runtimePackageIntegrity = options.runtimePackageIntegrity?.trim() || undefined;
27
27
  const protectedAccessMode = options.protectedAccessMode ?? DEFAULT_PROTECTED_ACCESS_MODE;
28
28
  const cloudfrontOriginProtocolPolicy = options.cloudfrontOriginProtocolPolicy ?? DEFAULT_CLOUDFRONT_ORIGIN_PROTOCOL_POLICY;
@@ -98,7 +98,7 @@ function buildAwsDeploymentPlan(options = {}) {
98
98
  domainName: cloudfrontOriginProtocolPolicy === "https-only" ? cloudfrontOriginDomainName : "<alb-dns-name>",
99
99
  requiresMatchingCertificate: cloudfrontOriginProtocolPolicy === "https-only",
100
100
  liveTrafficApproved: false,
101
- risk: cloudfrontOriginProtocolPolicy === "http-only" ? "Temporary HTTP-origin bridge: do not use for token-bearing live traffic without explicit risk acceptance, 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."
101
+ 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."
102
102
  } : undefined,
103
103
  originVerification: protectedAccessMode === "cloudfront_default_domain" ? {
104
104
  mode: "cloudfront_origin_header",
@@ -159,7 +159,7 @@ function buildAwsDeploymentPlan(options = {}) {
159
159
  `Infra PR must declare CodeBuild image builder ${prefix}-${stage}-image-builder for @hasna/uptime@${runtimePackageVersion}.`,
160
160
  `Infra PR must declare hardened S3 evidence bucket ${evidenceBucket} with KMS, versioning, lifecycle, and public access block.`,
161
161
  `Infra PR must declare encrypted EFS ${prefix}-${stage}-data with access point, mount targets, and AWS Backup plan.`,
162
- 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 HTTP-origin 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.`,
162
+ 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.`,
163
163
  "Only apply the infra plan from the approved infrastructure repository after review evidence is attached."
164
164
  ],
165
165
  deploy: [
@@ -168,7 +168,7 @@ function buildAwsDeploymentPlan(options = {}) {
168
168
  "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.",
169
169
  `Register task definitions for ${services.map((service) => service.name).join(", ")} using valueFrom secrets.`,
170
170
  `Update ECS services in cluster ${cluster} one component at a time through the approved deploy pipeline.`,
171
- 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 record explicit HTTP-origin risk acceptance." : `Create Route53/edge record for ${hostname} only after ALB health checks pass and auth denial smokes succeed.`
171
+ 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.`
172
172
  ],
173
173
  rollback: [
174
174
  "Keep previous task definition ARNs before each service update.",
@@ -185,7 +185,7 @@ function buildAwsDeploymentPlan(options = {}) {
185
185
  blockers: [
186
186
  "The infrastructure owner repository was not found in this workspace.",
187
187
  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.",
188
- ...protectedAccessMode === "cloudfront_default_domain" && cloudfrontOriginProtocolPolicy === "http-only" ? ["CloudFront-to-ALB origin transport is still http-only; token-bearing live traffic needs https-only origin mode or explicit risk acceptance."] : [],
188
+ ...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."] : [],
189
189
  ...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."] : [],
190
190
  "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.",
191
191
  "Hosted production auth/RBAC must replace broad static hosted-token operation before exposure.",
package/dist/index.d.ts CHANGED
@@ -6,6 +6,7 @@ export { applyImport, previewImport, rollbackImport } from "./imports.js";
6
6
  export { buildUptimeReport, sendUptimeReport } from "./report.js";
7
7
  export { generateProbeKeyPair, probePublicKeyFingerprint, probeResultSigningPayload, signProbeResult, verifyProbeResultSignature } from "./probes.js";
8
8
  export { buildAwsDeploymentPlan, buildPrivateProbeCloudConfig, renderPrivateProbeEnv } from "./cloud-plan.js";
9
+ export { runHostedPublicChecksWorker } from "./workers.js";
9
10
  export { uptimeHome, uptimeDbPath, uptimeHostedFallbackDbPath, ensureUptimeHome } from "./paths.js";
10
11
  export type { UptimeBackup, UptimeBackupCheck, UptimeRuntimeMode, UptimeStoreOptions, MonitorProvenance, SaveImportBatchInput, StoredImportBatch, UpsertMonitorProvenanceInput, } from "./store.js";
11
12
  export type { BrowserPageRunner, BrowserPageRunnerResult, FetchLike, HostedDnsResolver, HostedHttpCheckOptions, HostedHttpRequestContext, HostedHttpRequestLike, HostedHttpResponse, HostedTcpCheckOptions, MonitorCheckOptions, } from "./checks.js";
@@ -13,5 +14,6 @@ export type { ImportAction, ImportApplyItem, ImportApplyResult, ImportCandidate,
13
14
  export type { BrowserFailedRequest, BrowserPageEvidence, AuditEvent, CheckAttemptResult, CheckEvidence, CheckResult, CheckStatus, CreateMonitorKind, CreateMonitorInput, CreateReportScheduleInput, ImportedMonitorInput, ImportedUpdateMonitorInput, EvidenceArtifact, HttpTargetPolicyDecision, HttpTargetPolicyEvidence, Incident, IncidentStatus, ListAuditEventsOptions, ListReportRunsOptions, ListResultsOptions, Monitor, MonitorKind, MonitorStatus, MonitorSummary, ProbeCheckJob, ProbeCheckJobStatus, ProbeIdentity, ProbeResultSubmission, ProbeSubmissionReceipt, RecordAuditEventInput, ReportDeliveryChannel, ReportDeliveryRecord, ReportEmailChannelConfig, ReportLogsChannelConfig, ReportRun, ReportRunStatus, ReportSchedule, ReportScheduleChannels, ReportScheduleStatus, ReportSmsChannelConfig, SchedulerHandle, UpdateMonitorInput, UpdateReportScheduleInput, UptimeSummary, } from "./types.js";
14
15
  export type { ProbeKeyPair, ProbeSigningInput } from "./probes.js";
15
16
  export type { AwsDeploymentPlan, AwsDeploymentPlanOptions, AwsServicePlan, PrivateProbeCloudConfig, PrivateProbeCloudConfigOptions, } from "./cloud-plan.js";
17
+ export type { HostedPublicCheckRunner, HostedPublicChecksWorkerIteration, HostedPublicChecksWorkerOptions, HostedPublicChecksWorkerSummary, } from "./workers.js";
16
18
  export type { BuildUptimeReportOptions, SendUptimeReportOptions, UptimeEmailReportTarget, UptimeLogsReportTarget, UptimeReport, UptimeReportDelivery, UptimeSmsReportTarget, } from "./report.js";
17
19
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AACjE,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACzC,OAAO,EACL,qBAAqB,EACrB,0BAA0B,EAC1B,iCAAiC,EACjC,mBAAmB,EACnB,kBAAkB,EAClB,iBAAiB,EACjB,eAAe,EACf,YAAY,EACZ,WAAW,GACZ,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AACzD,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAC1E,OAAO,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAClE,OAAO,EAAE,oBAAoB,EAAE,yBAAyB,EAAE,yBAAyB,EAAE,eAAe,EAAE,0BAA0B,EAAE,MAAM,aAAa,CAAC;AACtJ,OAAO,EAAE,sBAAsB,EAAE,4BAA4B,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AAC9G,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,0BAA0B,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AACpG,YAAY,EACV,YAAY,EACZ,iBAAiB,EACjB,iBAAiB,EACjB,kBAAkB,EAClB,iBAAiB,EACjB,oBAAoB,EACpB,iBAAiB,EACjB,4BAA4B,GAC7B,MAAM,YAAY,CAAC;AACpB,YAAY,EACV,iBAAiB,EACjB,uBAAuB,EACvB,SAAS,EACT,iBAAiB,EACjB,sBAAsB,EACtB,wBAAwB,EACxB,qBAAqB,EACrB,kBAAkB,EAClB,qBAAqB,EACrB,mBAAmB,GACpB,MAAM,aAAa,CAAC;AACrB,YAAY,EACV,YAAY,EACZ,eAAe,EACf,iBAAiB,EACjB,eAAe,EACf,aAAa,EACb,iBAAiB,EACjB,aAAa,EACb,kBAAkB,EAClB,oBAAoB,EACpB,YAAY,GACb,MAAM,cAAc,CAAC;AACtB,YAAY,EACV,oBAAoB,EACpB,mBAAmB,EACnB,UAAU,EACV,kBAAkB,EAClB,aAAa,EACb,WAAW,EACX,WAAW,EACX,iBAAiB,EACjB,kBAAkB,EAClB,yBAAyB,EACzB,oBAAoB,EACpB,0BAA0B,EAC1B,gBAAgB,EAChB,wBAAwB,EACxB,wBAAwB,EACxB,QAAQ,EACR,cAAc,EACd,sBAAsB,EACtB,qBAAqB,EACrB,kBAAkB,EAClB,OAAO,EACP,WAAW,EACX,aAAa,EACb,cAAc,EACd,aAAa,EACb,mBAAmB,EACnB,aAAa,EACb,qBAAqB,EACrB,sBAAsB,EACtB,qBAAqB,EACrB,qBAAqB,EACrB,oBAAoB,EACpB,wBAAwB,EACxB,uBAAuB,EACvB,SAAS,EACT,eAAe,EACf,cAAc,EACd,sBAAsB,EACtB,oBAAoB,EACpB,sBAAsB,EACtB,eAAe,EACf,kBAAkB,EAClB,yBAAyB,EACzB,aAAa,GACd,MAAM,YAAY,CAAC;AACpB,YAAY,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AACnE,YAAY,EACV,iBAAiB,EACjB,wBAAwB,EACxB,cAAc,EACd,uBAAuB,EACvB,8BAA8B,GAC/B,MAAM,iBAAiB,CAAC;AACzB,YAAY,EACV,wBAAwB,EACxB,uBAAuB,EACvB,uBAAuB,EACvB,sBAAsB,EACtB,YAAY,EACZ,oBAAoB,EACpB,qBAAqB,GACtB,MAAM,aAAa,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AACjE,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACzC,OAAO,EACL,qBAAqB,EACrB,0BAA0B,EAC1B,iCAAiC,EACjC,mBAAmB,EACnB,kBAAkB,EAClB,iBAAiB,EACjB,eAAe,EACf,YAAY,EACZ,WAAW,GACZ,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AACzD,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAC1E,OAAO,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAClE,OAAO,EAAE,oBAAoB,EAAE,yBAAyB,EAAE,yBAAyB,EAAE,eAAe,EAAE,0BAA0B,EAAE,MAAM,aAAa,CAAC;AACtJ,OAAO,EAAE,sBAAsB,EAAE,4BAA4B,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AAC9G,OAAO,EAAE,2BAA2B,EAAE,MAAM,cAAc,CAAC;AAC3D,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,0BAA0B,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AACpG,YAAY,EACV,YAAY,EACZ,iBAAiB,EACjB,iBAAiB,EACjB,kBAAkB,EAClB,iBAAiB,EACjB,oBAAoB,EACpB,iBAAiB,EACjB,4BAA4B,GAC7B,MAAM,YAAY,CAAC;AACpB,YAAY,EACV,iBAAiB,EACjB,uBAAuB,EACvB,SAAS,EACT,iBAAiB,EACjB,sBAAsB,EACtB,wBAAwB,EACxB,qBAAqB,EACrB,kBAAkB,EAClB,qBAAqB,EACrB,mBAAmB,GACpB,MAAM,aAAa,CAAC;AACrB,YAAY,EACV,YAAY,EACZ,eAAe,EACf,iBAAiB,EACjB,eAAe,EACf,aAAa,EACb,iBAAiB,EACjB,aAAa,EACb,kBAAkB,EAClB,oBAAoB,EACpB,YAAY,GACb,MAAM,cAAc,CAAC;AACtB,YAAY,EACV,oBAAoB,EACpB,mBAAmB,EACnB,UAAU,EACV,kBAAkB,EAClB,aAAa,EACb,WAAW,EACX,WAAW,EACX,iBAAiB,EACjB,kBAAkB,EAClB,yBAAyB,EACzB,oBAAoB,EACpB,0BAA0B,EAC1B,gBAAgB,EAChB,wBAAwB,EACxB,wBAAwB,EACxB,QAAQ,EACR,cAAc,EACd,sBAAsB,EACtB,qBAAqB,EACrB,kBAAkB,EAClB,OAAO,EACP,WAAW,EACX,aAAa,EACb,cAAc,EACd,aAAa,EACb,mBAAmB,EACnB,aAAa,EACb,qBAAqB,EACrB,sBAAsB,EACtB,qBAAqB,EACrB,qBAAqB,EACrB,oBAAoB,EACpB,wBAAwB,EACxB,uBAAuB,EACvB,SAAS,EACT,eAAe,EACf,cAAc,EACd,sBAAsB,EACtB,oBAAoB,EACpB,sBAAsB,EACtB,eAAe,EACf,kBAAkB,EAClB,yBAAyB,EACzB,aAAa,GACd,MAAM,YAAY,CAAC;AACpB,YAAY,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AACnE,YAAY,EACV,iBAAiB,EACjB,wBAAwB,EACxB,cAAc,EACd,uBAAuB,EACvB,8BAA8B,GAC/B,MAAM,iBAAiB,CAAC;AACzB,YAAY,EACV,uBAAuB,EACvB,iCAAiC,EACjC,+BAA+B,EAC/B,+BAA+B,GAChC,MAAM,cAAc,CAAC;AACtB,YAAY,EACV,wBAAwB,EACxB,uBAAuB,EACvB,uBAAuB,EACvB,sBAAsB,EACtB,YAAY,EACZ,oBAAoB,EACpB,qBAAqB,GACtB,MAAM,aAAa,CAAC"}
package/dist/index.js CHANGED
@@ -4636,7 +4636,7 @@ function buildAwsDeploymentPlan(options = {}) {
4636
4636
  const image = clean(options.image, `${imageRepositoryUri}@sha256:<image-digest>`);
4637
4637
  const evidenceBucket = clean(options.evidenceBucket, `hasna-${stage}-${prefix}-evidence`);
4638
4638
  const hostedSqliteDbPath = clean(options.hostedSqliteDbPath, DEFAULT_HOSTED_SQLITE_DB);
4639
- const runtimePackageVersion = clean(options.runtimePackageVersion, "0.1.25");
4639
+ const runtimePackageVersion = clean(options.runtimePackageVersion, "0.1.26");
4640
4640
  const runtimePackageIntegrity = options.runtimePackageIntegrity?.trim() || undefined;
4641
4641
  const protectedAccessMode = options.protectedAccessMode ?? DEFAULT_PROTECTED_ACCESS_MODE;
4642
4642
  const cloudfrontOriginProtocolPolicy = options.cloudfrontOriginProtocolPolicy ?? DEFAULT_CLOUDFRONT_ORIGIN_PROTOCOL_POLICY;
@@ -4712,7 +4712,7 @@ function buildAwsDeploymentPlan(options = {}) {
4712
4712
  domainName: cloudfrontOriginProtocolPolicy === "https-only" ? cloudfrontOriginDomainName : "<alb-dns-name>",
4713
4713
  requiresMatchingCertificate: cloudfrontOriginProtocolPolicy === "https-only",
4714
4714
  liveTrafficApproved: false,
4715
- risk: cloudfrontOriginProtocolPolicy === "http-only" ? "Temporary HTTP-origin bridge: do not use for token-bearing live traffic without explicit risk acceptance, 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."
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
4716
  } : undefined,
4717
4717
  originVerification: protectedAccessMode === "cloudfront_default_domain" ? {
4718
4718
  mode: "cloudfront_origin_header",
@@ -4773,7 +4773,7 @@ function buildAwsDeploymentPlan(options = {}) {
4773
4773
  `Infra PR must declare CodeBuild image builder ${prefix}-${stage}-image-builder for @hasna/uptime@${runtimePackageVersion}.`,
4774
4774
  `Infra PR must declare hardened S3 evidence bucket ${evidenceBucket} with KMS, versioning, lifecycle, and public access block.`,
4775
4775
  `Infra PR must declare encrypted EFS ${prefix}-${stage}-data with access point, mount targets, and AWS Backup plan.`,
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 HTTP-origin 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.`,
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.`,
4777
4777
  "Only apply the infra plan from the approved infrastructure repository after review evidence is attached."
4778
4778
  ],
4779
4779
  deploy: [
@@ -4782,7 +4782,7 @@ function buildAwsDeploymentPlan(options = {}) {
4782
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.",
4783
4783
  `Register task definitions for ${services.map((service) => service.name).join(", ")} using valueFrom secrets.`,
4784
4784
  `Update ECS services in cluster ${cluster} one component at a time through the approved deploy pipeline.`,
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 record explicit HTTP-origin risk acceptance." : `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.`
4786
4786
  ],
4787
4787
  rollback: [
4788
4788
  "Keep previous task definition ARNs before each service update.",
@@ -4799,7 +4799,7 @@ function buildAwsDeploymentPlan(options = {}) {
4799
4799
  blockers: [
4800
4800
  "The infrastructure owner repository was not found in this workspace.",
4801
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; token-bearing live traffic needs https-only origin mode or explicit risk acceptance."] : [],
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
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."] : [],
4804
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.",
4805
4805
  "Hosted production auth/RBAC must replace broad static hosted-token operation before exposure.",
@@ -4936,6 +4936,72 @@ function shellEscape(value) {
4936
4936
  return value;
4937
4937
  return `'${value.replace(/'/g, "'\\''")}'`;
4938
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
+ }
4939
5005
  export {
4940
5006
  verifyProbeResultSignature,
4941
5007
  uptimeHostedFallbackDbPath,
@@ -4948,6 +5014,7 @@ export {
4948
5014
  runMonitorCheck,
4949
5015
  runHttpCheck,
4950
5016
  runHostedTcpCheck,
5017
+ runHostedPublicChecksWorker,
4951
5018
  runHostedHttpCheck,
4952
5019
  runBrowserPageCheck,
4953
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
+ };
@@ -195,9 +195,11 @@ Before setting `desired_counts.web = 1`, verify:
195
195
  - `HASNA_UPTIME_ALLOWED_ORIGINS` matches the public HTTPS edge origin;
196
196
  - CloudFront-to-origin transport is either `https-only` with an origin hostname
197
197
  whose certificate matches that hostname, or the HTTP-origin bridge has a
198
- named risk owner and approval recorded in private evidence;
198
+ named risk owner and approval recorded in private evidence by setting
199
+ `allow_cloudfront_http_origin_live_traffic = true`;
199
200
  - CloudFront origin access is distribution-bound with the CloudFront-only origin
200
- 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;
201
203
  - web egress to ECR, Secrets Manager or SSM, CloudWatch Logs, S3, EFS, and any
202
204
  required endpoints has been proven from a real ECS task. Terraform endpoint
203
205
  ids, route tables, and security-group rules are creation evidence only; the
@@ -422,9 +424,10 @@ routes are backed by cloud check jobs and cloud audit rows.
422
424
  hosted public-check runner, records target-policy decision evidence, and
423
425
  passes AWS smokes for denied DNS answers, redirect-to-denied targets, and
424
426
  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.
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.
428
431
  - Do not enable scheduler, public-probe, reporter, or migration workers against
429
432
  the EFS SQLite bridge; those services need Postgres/cloud leases first.
430
433
  - Do not expose dashboard/API routes without hosted auth and workspace checks.
@@ -453,8 +453,11 @@ required before browser evidence or public probe scale-out.
453
453
  It is not live: services remain at desired count `0`, secrets have
454
454
  `AWSCURRENT` values, scoped hosted-token descriptors can be used for operator
455
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.
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.
458
461
  - Open Uptime is still SQLite-only for this bridge; only one protected web task
459
462
  may write EFS until Postgres and cloud leases exist.
460
463
  - Hosted API/dashboard auth, workspace RBAC, target policy, and Postgres leases
@@ -478,8 +481,12 @@ required before browser evidence or public probe scale-out.
478
481
  EFS SQLite bridge is explicitly temporary and not the target source of truth.
479
482
  - ECS task definitions use secret refs, not plaintext secret values.
480
483
  - ECS task definitions include explicit container health checks: web checks
481
- `/health`, while disabled non-web roles use a hosted-environment sanity check
482
- 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.
483
490
  - Public probes cannot reach denied target classes; private monitors require
484
491
  private probes and approved inventory refs.
485
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.25",
4
+ "intendedVersion": "0.1.26",
5
5
  "accountProfile": "<aws-profile>",
6
6
  "accountId": "<aws-account-id>",
7
7
  "region": "us-east-1",
@@ -90,9 +90,12 @@ delivery, S3 access, and EFS mount behavior.
90
90
 
91
91
  Every ECS task definition includes an explicit container health check. The web
92
92
  task checks `GET /health` through Bun's built-in `fetch`; disabled non-web roles
93
- currently run a hosted-environment sanity check so scheduler, public-probe,
94
- reporter, and migration tasks do not start from empty ECS health semantics when
95
- 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.
96
99
 
97
100
  Interface endpoint private DNS is VPC-wide. In shared VPCs, either keep endpoint
98
101
  creation in the approved networking root, or pass
@@ -106,10 +109,16 @@ Store instead of Secrets Manager, add `ssm` to
106
109
  - Hosted production auth/RBAC still needs scoped, revocable credentials.
107
110
  - The default `http-only` CloudFront origin bridge must be replaced with the
108
111
  explicit HTTPS-origin mode or consciously accepted with documented risk before
109
- token-bearing live traffic.
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.
110
117
  - Public probe runtime has SDK-level hosted HTTP target-policy enforcement, but
111
- the public-probe worker and cloud check-job lease path are still disabled until
112
- 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.
113
122
  - Hosted private-probe enrollment/heartbeat/revocation is still
114
123
  fail-closed.
115
124
 
package/infra/aws/main.tf CHANGED
@@ -40,22 +40,22 @@ locals {
40
40
  }
41
41
  scheduler = {
42
42
  desired_count = lookup(var.desired_counts, "scheduler", 0)
43
- command = ["bun", "dist/cli/index.js", "cloud", "plan"]
43
+ command = ["bun", "dist/cli/index.js", "cloud", "workers", "run", "--role", "scheduler"]
44
44
  secrets = { APP_ENV = var.app_env_secret_arn }
45
45
  }
46
46
  "public-probe" = {
47
47
  desired_count = lookup(var.desired_counts, "public-probe", 0)
48
- command = ["bun", "dist/cli/index.js", "cloud", "plan"]
48
+ command = ["bun", "dist/cli/index.js", "cloud", "workers", "run", "--role", "public-probe"]
49
49
  secrets = { PROBE_CONFIG = var.public_probe_secret_arn }
50
50
  }
51
51
  reporter = {
52
52
  desired_count = lookup(var.desired_counts, "reporter", 0)
53
- command = ["bun", "dist/cli/index.js", "cloud", "plan"]
53
+ command = ["bun", "dist/cli/index.js", "cloud", "workers", "run", "--role", "reporter"]
54
54
  secrets = { REPORTING_CONFIG = var.reporting_secret_arn }
55
55
  }
56
56
  migration = {
57
57
  desired_count = lookup(var.desired_counts, "migration", 0)
58
- command = ["bun", "dist/cli/index.js", "cloud", "plan"]
58
+ command = ["bun", "dist/cli/index.js", "cloud", "workers", "run", "--role", "migration"]
59
59
  secrets = { APP_ENV = var.app_env_secret_arn }
60
60
  }
61
61
  }
@@ -94,28 +94,28 @@ locals {
94
94
  startPeriod = 30
95
95
  }
96
96
  scheduler = {
97
- command = ["CMD-SHELL", "bun -e \"process.exit(process.env.HASNA_UPTIME_MODE === 'hosted' && process.env.HASNA_UPTIME_COMPONENT === 'scheduler' ? 0 : 1)\""]
97
+ command = ["CMD-SHELL", "bun dist/cli/index.js cloud workers preflight --role scheduler --healthcheck --json >/tmp/open-uptime-worker-preflight.json"]
98
98
  interval = 30
99
99
  timeout = 5
100
100
  retries = 3
101
101
  startPeriod = 30
102
102
  }
103
103
  "public-probe" = {
104
- command = ["CMD-SHELL", "bun -e \"process.exit(process.env.HASNA_UPTIME_MODE === 'hosted' && process.env.HASNA_UPTIME_COMPONENT === 'public-probe' ? 0 : 1)\""]
104
+ command = ["CMD-SHELL", "bun dist/cli/index.js cloud workers preflight --role public-probe --healthcheck --json >/tmp/open-uptime-worker-preflight.json"]
105
105
  interval = 30
106
106
  timeout = 5
107
107
  retries = 3
108
108
  startPeriod = 30
109
109
  }
110
110
  reporter = {
111
- command = ["CMD-SHELL", "bun -e \"process.exit(process.env.HASNA_UPTIME_MODE === 'hosted' && process.env.HASNA_UPTIME_COMPONENT === 'reporter' ? 0 : 1)\""]
111
+ command = ["CMD-SHELL", "bun dist/cli/index.js cloud workers preflight --role reporter --healthcheck --json >/tmp/open-uptime-worker-preflight.json"]
112
112
  interval = 30
113
113
  timeout = 5
114
114
  retries = 3
115
115
  startPeriod = 30
116
116
  }
117
117
  migration = {
118
- command = ["CMD-SHELL", "bun -e \"process.exit(process.env.HASNA_UPTIME_MODE === 'hosted' && process.env.HASNA_UPTIME_COMPONENT === 'migration' ? 0 : 1)\""]
118
+ command = ["CMD-SHELL", "bun dist/cli/index.js cloud workers preflight --role migration --healthcheck --json >/tmp/open-uptime-worker-preflight.json"]
119
119
  interval = 30
120
120
  timeout = 5
121
121
  retries = 3
@@ -12,6 +12,7 @@ vpc_id = "vpc-xxxxxxxx"
12
12
  ecr_repository_name = "open-uptime"
13
13
  protected_access_mode = "cloudfront_default_domain"
14
14
  cloudfront_origin_protocol_policy = "http-only"
15
+ allow_cloudfront_http_origin_live_traffic = false
15
16
  cloudfront_origin_domain_name = null
16
17
  enable_cloudfront_origin_verify_header = false
17
18
  cloudfront_origin_verify_header_name = "X-Open-Uptime-Origin-Verify"
@@ -21,7 +22,7 @@ alb_ingress_cidr_blocks = []
21
22
  private_subnet_ids = ["subnet-replace-private-a", "subnet-replace-private-b"]
22
23
  private_route_table_ids = ["rtb-replace-private"]
23
24
  container_image = "123456789012.dkr.ecr.us-east-1.amazonaws.com/open-uptime@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
24
- runtime_package_version = "0.1.25"
25
+ runtime_package_version = "0.1.26"
25
26
  runtime_package_integrity = null
26
27
  certificate_arn = null
27
28
  hosted_zone_id = null
@@ -98,6 +98,12 @@ variable "cloudfront_origin_protocol_policy" {
98
98
  }
99
99
  }
100
100
 
101
+ variable "allow_cloudfront_http_origin_live_traffic" {
102
+ description = "Explicit risk acceptance for setting web desired count above 0 while CloudFront-to-ALB origin transport is http-only. Keep false unless a named operator accepts the temporary HTTP-origin bridge risk for a bounded smoke."
103
+ type = bool
104
+ default = false
105
+ }
106
+
101
107
  variable "cloudfront_origin_domain_name" {
102
108
  description = "DNS hostname CloudFront uses for the ALB custom origin when cloudfront_origin_protocol_policy is https-only. The hostname must resolve to the ALB and match certificate_arn. Leave null for the default HTTP-origin bridge."
103
109
  type = string
@@ -235,7 +241,7 @@ variable "container_image" {
235
241
  variable "runtime_package_version" {
236
242
  description = "Published @hasna/uptime package version that CodeBuild should build into the ECR image."
237
243
  type = string
238
- default = "0.1.25"
244
+ default = "0.1.26"
239
245
 
240
246
  validation {
241
247
  condition = can(regex("^[0-9]+\\.[0-9]+\\.[0-9]+(-[0-9A-Za-z.-]+)?$", var.runtime_package_version))
@@ -347,6 +353,25 @@ variable "desired_counts" {
347
353
  ])
348
354
  error_message = "EFS SQLite bridge requires web desired count 0 or 1 and scheduler/public-probe/reporter/migration desired counts 0."
349
355
  }
356
+
357
+ validation {
358
+ condition = (
359
+ lookup(var.desired_counts, "web", 0) == 0
360
+ || var.protected_access_mode != "cloudfront_default_domain"
361
+ || var.enable_cloudfront_origin_verify_header
362
+ )
363
+ error_message = "web desired count above 0 in cloudfront_default_domain mode requires enable_cloudfront_origin_verify_header=true."
364
+ }
365
+
366
+ validation {
367
+ condition = (
368
+ lookup(var.desired_counts, "web", 0) == 0
369
+ || var.protected_access_mode != "cloudfront_default_domain"
370
+ || var.cloudfront_origin_protocol_policy == "https-only"
371
+ || var.allow_cloudfront_http_origin_live_traffic
372
+ )
373
+ error_message = "web desired count above 0 requires CloudFront HTTPS-origin mode, or explicit allow_cloudfront_http_origin_live_traffic=true risk acceptance for a bounded smoke."
374
+ }
350
375
  }
351
376
 
352
377
  variable "alarm_actions" {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/uptime",
3
- "version": "0.1.25",
3
+ "version": "0.1.26",
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",
@@ -69,10 +69,14 @@
69
69
  "./cloud-plan": {
70
70
  "types": "./dist/cloud-plan.d.ts",
71
71
  "import": "./dist/cloud-plan.js"
72
+ },
73
+ "./workers": {
74
+ "types": "./dist/workers.d.ts",
75
+ "import": "./dist/workers.js"
72
76
  }
73
77
  },
74
78
  "scripts": {
75
- "build": "rm -rf dist && bun build src/cli/index.ts --outdir dist/cli --target bun --external @modelcontextprotocol/sdk && bun build src/mcp/index.ts --outdir dist/mcp --target bun --external @modelcontextprotocol/sdk && bun build src/index.ts src/api.ts src/service.ts src/store.ts src/checks.ts src/imports.ts src/report.ts src/probes.ts src/cloud-plan.ts src/types.ts src/paths.ts src/dashboard.ts src/version.ts --root src --outdir dist --target bun && tsc -p tsconfig.build.json --emitDeclarationOnly --outDir dist && chmod +x dist/cli/index.js dist/mcp/index.js",
79
+ "build": "rm -rf dist && bun build src/cli/index.ts --outdir dist/cli --target bun --external @modelcontextprotocol/sdk && bun build src/mcp/index.ts --outdir dist/mcp --target bun --external @modelcontextprotocol/sdk && bun build src/index.ts src/api.ts src/service.ts src/store.ts src/checks.ts src/imports.ts src/report.ts src/probes.ts src/cloud-plan.ts src/workers.ts src/types.ts src/paths.ts src/dashboard.ts src/version.ts --root src --outdir dist --target bun && tsc -p tsconfig.build.json --emitDeclarationOnly --outDir dist && chmod +x dist/cli/index.js dist/mcp/index.js",
76
80
  "typecheck": "tsc --noEmit",
77
81
  "test": "bun test ./tests",
78
82
  "dev:cli": "bun run src/cli/index.ts",