@hasna/uptime 0.1.23 → 0.1.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -5728,12 +5728,18 @@ var MAX_PROBE_RESULT_FUTURE_MS = 5 * 60000;
5728
5728
  class UptimeService {
5729
5729
  store;
5730
5730
  checkRunner;
5731
+ hostedResolveHost;
5732
+ hostedHttpRequest;
5733
+ hostedMaxRedirects;
5731
5734
  leaseOwner = `svc_${randomUUID3().replace(/-/g, "").slice(0, 18)}`;
5732
5735
  inFlightChecks = new Set;
5733
5736
  inFlightReportSchedules = new Set;
5734
5737
  constructor(options = {}) {
5735
5738
  this.store = options.store ?? new UptimeStore({ mode: "local", ...options });
5736
5739
  this.checkRunner = options.checkRunner ?? runMonitorCheck;
5740
+ this.hostedResolveHost = options.hostedResolveHost;
5741
+ this.hostedHttpRequest = options.hostedHttpRequest;
5742
+ this.hostedMaxRedirects = options.hostedMaxRedirects;
5737
5743
  }
5738
5744
  close() {
5739
5745
  this.store.close();
@@ -5940,39 +5946,7 @@ class UptimeService {
5940
5946
  const monitor = this.store.getMonitor(idOrName);
5941
5947
  if (!monitor)
5942
5948
  throw new Error(`Monitor not found: ${idOrName}`);
5943
- if (!monitor.enabled)
5944
- throw new Error(`Monitor is disabled: ${monitor.name}`);
5945
- if (this.inFlightChecks.has(monitor.id))
5946
- throw new Error(`Monitor check already in progress: ${monitor.name}`);
5947
- const leaseTtlMs = Math.max(60000, (monitor.retryCount + 1) * monitor.timeoutMs + 1e4);
5948
- if (!this.store.acquireCheckLease(monitor.id, this.leaseOwner, leaseTtlMs)) {
5949
- throw new MonitorCheckBusyError(`Monitor check already in progress: ${monitor.name}`);
5950
- }
5951
- this.inFlightChecks.add(monitor.id);
5952
- try {
5953
- let attemptCount = 0;
5954
- let last = null;
5955
- const maxAttempts = Math.max(1, monitor.retryCount + 1);
5956
- while (attemptCount < maxAttempts) {
5957
- attemptCount += 1;
5958
- last = await this.checkRunner(monitor);
5959
- if (last.status === "up")
5960
- break;
5961
- }
5962
- return this.store.recordCheckResult({
5963
- monitorId: monitor.id,
5964
- status: last.status,
5965
- latencyMs: last.latencyMs,
5966
- statusCode: last.statusCode ?? null,
5967
- error: last.error ?? null,
5968
- evidence: last.evidence ?? null,
5969
- attemptCount,
5970
- expectedMonitorRevision: monitor.revision
5971
- });
5972
- } finally {
5973
- this.inFlightChecks.delete(monitor.id);
5974
- this.store.releaseCheckLease(monitor.id, this.leaseOwner);
5975
- }
5949
+ return this.recordMonitorCheck(monitor, { hostedTargetPolicy: false });
5976
5950
  }
5977
5951
  async checkAll() {
5978
5952
  if (this.store.mode === "hosted")
@@ -5984,6 +5958,49 @@ class UptimeService {
5984
5958
  }
5985
5959
  return results;
5986
5960
  }
5961
+ async checkHostedPublicMonitor(idOrName, options = {}) {
5962
+ this.assertHostedPublicChecksEnabled();
5963
+ const workspaceId = this.requireHostedWorkerWorkspaceId(options.workspaceId);
5964
+ const monitor = this.store.getMonitor(idOrName, { workspaceId });
5965
+ if (!monitor)
5966
+ throw new Error(`Monitor not found: ${idOrName}`);
5967
+ this.assertHostedPublicMonitor(monitor);
5968
+ const result = await this.recordMonitorCheck(monitor, { hostedTargetPolicy: true });
5969
+ this.auditStore().recordAuditEvent({
5970
+ workspaceId,
5971
+ action: "hosted_public_check.run",
5972
+ resourceType: "monitor",
5973
+ resourceId: monitor.id,
5974
+ message: `Ran hosted public check for ${monitor.name}`,
5975
+ metadata: {
5976
+ checkResultId: result.id,
5977
+ status: result.status,
5978
+ monitorKind: monitor.kind,
5979
+ operatorPath: "hosted_public_check"
5980
+ },
5981
+ actor: "hosted-public-check-worker"
5982
+ });
5983
+ return result;
5984
+ }
5985
+ async runDueHostedPublicChecks(now = new Date, options = {}) {
5986
+ this.assertHostedPublicChecksEnabled();
5987
+ const workspaceId = this.requireHostedWorkerWorkspaceId(options.workspaceId);
5988
+ const due = this.store.listMonitors({ workspaceId }).filter((monitor) => this.isHostedPublicMonitor(monitor) && this.isDue(monitor, now));
5989
+ const results = [];
5990
+ for (const monitor of due) {
5991
+ const current = this.store.getMonitor(monitor.id, { workspaceId });
5992
+ if (!current || !this.isHostedPublicMonitor(current) || !this.isDue(current, now))
5993
+ continue;
5994
+ try {
5995
+ results.push(await this.checkHostedPublicMonitor(current.id, { workspaceId }));
5996
+ } catch (error) {
5997
+ if (error instanceof MonitorCheckBusyError || error instanceof StaleCheckResultError)
5998
+ continue;
5999
+ throw error;
6000
+ }
6001
+ }
6002
+ return results;
6003
+ }
5987
6004
  startScheduler(options = {}) {
5988
6005
  if (this.store.mode === "hosted")
5989
6006
  throw new Error("hosted scheduler requires check_jobs and probes");
@@ -6029,6 +6046,67 @@ class UptimeService {
6029
6046
  const last = new Date(monitor.lastCheckedAt).getTime();
6030
6047
  return now.getTime() - last >= monitor.intervalSeconds * 1000;
6031
6048
  }
6049
+ async recordMonitorCheck(monitor, options) {
6050
+ if (!monitor.enabled)
6051
+ throw new Error(`Monitor is disabled: ${monitor.name}`);
6052
+ if (this.inFlightChecks.has(monitor.id))
6053
+ throw new Error(`Monitor check already in progress: ${monitor.name}`);
6054
+ const leaseTtlMs = Math.max(60000, (monitor.retryCount + 1) * monitor.timeoutMs + 1e4);
6055
+ if (!this.store.acquireCheckLease(monitor.id, this.leaseOwner, leaseTtlMs)) {
6056
+ throw new MonitorCheckBusyError(`Monitor check already in progress: ${monitor.name}`);
6057
+ }
6058
+ this.inFlightChecks.add(monitor.id);
6059
+ try {
6060
+ let attemptCount = 0;
6061
+ let last = null;
6062
+ const maxAttempts = Math.max(1, monitor.retryCount + 1);
6063
+ while (attemptCount < maxAttempts) {
6064
+ attemptCount += 1;
6065
+ last = options.hostedTargetPolicy ? await this.runHostedPublicCheckAttempt(monitor) : await this.checkRunner(monitor);
6066
+ if (last.status === "up")
6067
+ break;
6068
+ }
6069
+ return this.store.recordCheckResult({
6070
+ monitorId: monitor.id,
6071
+ status: last.status,
6072
+ latencyMs: last.latencyMs,
6073
+ statusCode: last.statusCode ?? null,
6074
+ error: last.error ?? null,
6075
+ evidence: last.evidence ?? null,
6076
+ attemptCount,
6077
+ expectedMonitorRevision: monitor.revision
6078
+ });
6079
+ } finally {
6080
+ this.inFlightChecks.delete(monitor.id);
6081
+ this.store.releaseCheckLease(monitor.id, this.leaseOwner);
6082
+ }
6083
+ }
6084
+ runHostedPublicCheckAttempt(monitor) {
6085
+ return runMonitorCheck(monitor, {
6086
+ hostedTargetPolicy: true,
6087
+ resolveHost: this.hostedResolveHost,
6088
+ hostedHttpRequest: this.hostedHttpRequest,
6089
+ maxRedirects: this.hostedMaxRedirects
6090
+ });
6091
+ }
6092
+ assertHostedPublicChecksEnabled() {
6093
+ if (this.store.mode !== "hosted")
6094
+ throw new Error("hosted public checks require hosted mode");
6095
+ }
6096
+ requireHostedWorkerWorkspaceId(workspaceId) {
6097
+ const value = workspaceId?.trim() || process.env.HASNA_UPTIME_WORKSPACE_ID?.trim();
6098
+ if (!value)
6099
+ throw new Error("hosted public checks require a workspace id");
6100
+ return value;
6101
+ }
6102
+ assertHostedPublicMonitor(monitor) {
6103
+ if (!this.isHostedPublicMonitor(monitor)) {
6104
+ throw new Error("hosted public checks support only HTTP and TCP monitors");
6105
+ }
6106
+ }
6107
+ isHostedPublicMonitor(monitor) {
6108
+ return monitor.kind === "http" || monitor.kind === "tcp";
6109
+ }
6032
6110
  probeStore() {
6033
6111
  if (this.store.mode === "hosted") {
6034
6112
  throw new Error("hosted probe APIs require cloud check_jobs, workspace stores, and audit logging");
@@ -7139,6 +7217,7 @@ var DEFAULT_WORKSPACE_ID = "workspace-id";
7139
7217
  var DEFAULT_VPC_ID = "vpc-xxxxxxxx";
7140
7218
  var DEFAULT_HOSTED_SQLITE_DB = "/data/uptime/uptime.db";
7141
7219
  var DEFAULT_PROTECTED_ACCESS_MODE = "cloudfront_default_domain";
7220
+ var DEFAULT_CLOUDFRONT_ORIGIN_PROTOCOL_POLICY = "http-only";
7142
7221
  function buildAwsDeploymentPlan(options = {}) {
7143
7222
  const region = clean(options.region, DEFAULT_REGION);
7144
7223
  const stage = clean(options.stage, DEFAULT_STAGE);
@@ -7151,8 +7230,11 @@ function buildAwsDeploymentPlan(options = {}) {
7151
7230
  const image = clean(options.image, `${imageRepositoryUri}@sha256:<image-digest>`);
7152
7231
  const evidenceBucket = clean(options.evidenceBucket, `hasna-${stage}-${prefix}-evidence`);
7153
7232
  const hostedSqliteDbPath = clean(options.hostedSqliteDbPath, DEFAULT_HOSTED_SQLITE_DB);
7154
- const runtimePackageVersion = clean(options.runtimePackageVersion, "0.1.23");
7233
+ const runtimePackageVersion = clean(options.runtimePackageVersion, "0.1.25");
7234
+ const runtimePackageIntegrity = options.runtimePackageIntegrity?.trim() || undefined;
7155
7235
  const protectedAccessMode = options.protectedAccessMode ?? DEFAULT_PROTECTED_ACCESS_MODE;
7236
+ const cloudfrontOriginProtocolPolicy = options.cloudfrontOriginProtocolPolicy ?? DEFAULT_CLOUDFRONT_ORIGIN_PROTOCOL_POLICY;
7237
+ const cloudfrontOriginDomainName = clean(options.cloudfrontOriginDomainName, "<alb-dns-name>");
7156
7238
  const protectedAccessUrl = protectedAccessMode === "cloudfront_default_domain" ? "https://<cloudfront-domain>" : `https://${hostname}`;
7157
7239
  const cluster = `${prefix}-${stage}`;
7158
7240
  const secrets = {
@@ -7219,6 +7301,13 @@ function buildAwsDeploymentPlan(options = {}) {
7219
7301
  protectedAccessMode,
7220
7302
  edgeDistribution: protectedAccessMode === "cloudfront_default_domain" ? `${prefix}-${stage}-edge` : undefined,
7221
7303
  protectedAccessUrl,
7304
+ cloudfrontOrigin: protectedAccessMode === "cloudfront_default_domain" ? {
7305
+ protocolPolicy: cloudfrontOriginProtocolPolicy,
7306
+ domainName: cloudfrontOriginProtocolPolicy === "https-only" ? cloudfrontOriginDomainName : "<alb-dns-name>",
7307
+ requiresMatchingCertificate: cloudfrontOriginProtocolPolicy === "https-only",
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."
7310
+ } : undefined,
7222
7311
  originVerification: protectedAccessMode === "cloudfront_default_domain" ? {
7223
7312
  mode: "cloudfront_origin_header",
7224
7313
  requiredBeforeScaleUp: true,
@@ -7251,6 +7340,7 @@ function buildAwsDeploymentPlan(options = {}) {
7251
7340
  repository: ecrRepository,
7252
7341
  uri: image,
7253
7342
  dockerfile: "Dockerfile.package",
7343
+ expectedIntegrity: runtimePackageIntegrity,
7254
7344
  buildCommand: `BLOCKED: after infra approval, AWS CodeBuild builds Dockerfile.package from @hasna/uptime@${runtimePackageVersion} into ${imageRepositoryUri}`,
7255
7345
  pushCommands: [
7256
7346
  `BLOCKED: start ${prefix}-${stage}-image-builder only through the approved deploy pipeline after @hasna/uptime@${runtimePackageVersion} is published`,
@@ -7277,16 +7367,16 @@ function buildAwsDeploymentPlan(options = {}) {
7277
7367
  `Infra PR must declare CodeBuild image builder ${prefix}-${stage}-image-builder for @hasna/uptime@${runtimePackageVersion}.`,
7278
7368
  `Infra PR must declare hardened S3 evidence bucket ${evidenceBucket} with KMS, versioning, lifecycle, and public access block.`,
7279
7369
  `Infra PR must declare encrypted EFS ${prefix}-${stage}-data with access point, mount targets, and AWS Backup plan.`,
7280
- 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.`,
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.`,
7281
7371
  "Only apply the infra plan from the approved infrastructure repository after review evidence is attached."
7282
7372
  ],
7283
7373
  deploy: [
7284
7374
  "Build and publish the image only after the Dockerfile/container target is reviewed.",
7285
- `Start the AWS image builder for @hasna/uptime@${runtimePackageVersion} and record the pushed image digest.`,
7375
+ 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.`,
7286
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.",
7287
7377
  `Register task definitions for ${services.map((service) => service.name).join(", ")} using valueFrom secrets.`,
7288
7378
  `Update ECS services in cluster ${cluster} one component at a time through the approved deploy pipeline.`,
7289
- 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.`
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.`
7290
7380
  ],
7291
7381
  rollback: [
7292
7382
  "Keep previous task definition ARNs before each service update.",
@@ -7303,6 +7393,8 @@ function buildAwsDeploymentPlan(options = {}) {
7303
7393
  blockers: [
7304
7394
  "The infrastructure owner repository was not found in this workspace.",
7305
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."] : [],
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."] : [],
7306
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.",
7307
7399
  "Hosted production auth/RBAC must replace broad static hosted-token operation before exposure.",
7308
7400
  "Public probe execution still needs cloud check-job leases wired to runHostedHttpCheck and live policy-decision log evidence.",
@@ -7311,8 +7403,9 @@ function buildAwsDeploymentPlan(options = {}) {
7311
7403
  requiredEvidence: [
7312
7404
  "Infrastructure PR/synth/plan from the approved infra repository.",
7313
7405
  "CodeBuild image-builder run, container smoke, and immutable image digest.",
7406
+ "Published package dist.integrity pinned in the private infra root or an explicit not-live exception.",
7314
7407
  "ECS task definitions using secrets.valueFrom only.",
7315
- "CloudFront-default-domain origin-header config or ALB TLS auth-denial smokes, direct-origin denial evidence, and web alarm checks.",
7408
+ "CloudFront-default-domain origin-header config, origin transport decision, direct-origin denial evidence, auth-denial smokes, and web alarm checks.",
7316
7409
  "Single-writer ECS evidence: one web task maximum and no scheduler/public-probe/reporter EFS mounts.",
7317
7410
  "EFS encryption, access point, mount-target, AWS Backup, and restore-drill evidence.",
7318
7411
  "S3 bucket KMS, versioning, lifecycle, and public-access-block evidence.",
@@ -7325,11 +7418,13 @@ function buildAwsDeploymentPlan(options = {}) {
7325
7418
  notes: [
7326
7419
  "This plan generator does not call AWS.",
7327
7420
  "Blocked plan output intentionally avoids copy-pastable AWS mutation commands.",
7328
- "Default protected access uses CloudFront's HTTPS default domain so first deploy is not blocked on custom DNS or ACM.",
7421
+ "Default protected access uses CloudFront's HTTPS default domain so first zero-count deploy is not blocked on custom DNS or ACM.",
7329
7422
  "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.",
7423
+ "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.",
7330
7424
  "Hosted runtime uses explicit EFS-backed SQLite at HASNA_UPTIME_HOSTED_SQLITE_DB until the async Postgres adapter exists.",
7331
7425
  "Do not set HASNA_UPTIME_DATABASE_URL for hosted tasks until the Postgres adapter is implemented.",
7332
7426
  "Secrets are represented as secret names/refs and must be injected with valueFrom.",
7427
+ "Set runtime_package_integrity in the approved infra root after publish so the AWS image builder verifies the npm tarball before ECR build.",
7333
7428
  "Actual deploy belongs in the deploy_release_operate_final goal node after infra review."
7334
7429
  ]
7335
7430
  }
@@ -7396,7 +7491,10 @@ function buildPrivateProbeCloudConfig(options = {}) {
7396
7491
  }
7397
7492
  };
7398
7493
  }
7399
- function renderPrivateProbeEnv(config) {
7494
+ function renderPrivateProbeEnv(config, options = {}) {
7495
+ if (!options.allowBlocked && (!config.canStart || config.blockers.length > 0)) {
7496
+ throw new Error("private probe env output is blocked until hosted probe routes and cloud jobs are implemented");
7497
+ }
7400
7498
  const required = ["HASNA_UPTIME_PRIVATE_PROBE_ID"];
7401
7499
  const missing = required.filter((key) => !config.env[key]);
7402
7500
  if (missing.length > 0) {
@@ -7439,6 +7537,13 @@ program2.name("uptime").description("Local-first uptime and downtime monitoring"
7439
7537
  function service() {
7440
7538
  return new UptimeService({ mode: "local" });
7441
7539
  }
7540
+ function hostedService(opts) {
7541
+ return new UptimeService({
7542
+ mode: "hosted",
7543
+ hostedSqliteDbPath: opts.hostedSqliteDb,
7544
+ allowHostedLocalStore: opts.allowHostedLocalStore
7545
+ });
7546
+ }
7442
7547
  function wantsJson(opts) {
7443
7548
  return Boolean(opts?.json || program2.opts().json);
7444
7549
  }
@@ -7741,7 +7846,7 @@ program2.command("audit").description("List local audit events").option("--resou
7741
7846
  }
7742
7847
  });
7743
7848
  var cloud = program2.command("cloud").description("Generate dry-run cloud deployment and private-probe configuration artifacts");
7744
- cloud.command("plan").description("Generate a dry-run AWS deployment plan").option("--account <name>", "AWS account/profile label", "aws-profile").option("--region <region>", "AWS region", "us-east-1").option("--stage <stage>", "deployment stage", "prod").option("--hostname <hostname>", "hosted Open Uptime hostname", "uptime.example.com").option("--workspace-id <id>", "workspace id", "workspace-id").option("--vpc-id <id>", "target VPC id").option("--hosted-sqlite-db <path>", "hosted SQLite path on the EFS mount").option("--rds-instance-id <id>", "deprecated; ignored until the hosted Postgres adapter exists").option("--database-secret-name <name>", "deprecated; ignored until the hosted Postgres adapter exists").option("--ecr-repository <name>", "ECR repository name").option("--image <uri>", "container image URI").option("--runtime-package-version <version>", "published @hasna/uptime version for the AWS image builder").addOption(new Option("--protected-access-mode <mode>", "protected web access mode").choices(["cloudfront_default_domain", "alb_https_cert"]).default("cloudfront_default_domain")).option("--evidence-bucket <name>", "S3 evidence bucket name").option("-j, --json", "print JSON").action((opts) => {
7849
+ cloud.command("plan").description("Generate a dry-run AWS deployment plan").option("--account <name>", "AWS account/profile label", "aws-profile").option("--region <region>", "AWS region", "us-east-1").option("--stage <stage>", "deployment stage", "prod").option("--hostname <hostname>", "hosted Open Uptime hostname", "uptime.example.com").option("--workspace-id <id>", "workspace id", "workspace-id").option("--vpc-id <id>", "target VPC id").option("--hosted-sqlite-db <path>", "hosted SQLite path on the EFS mount").option("--rds-instance-id <id>", "deprecated; ignored until the hosted Postgres adapter exists").option("--database-secret-name <name>", "deprecated; ignored until the hosted Postgres adapter exists").option("--ecr-repository <name>", "ECR repository name").option("--image <uri>", "container image URI").option("--runtime-package-version <version>", "published @hasna/uptime version for the AWS image builder").option("--runtime-package-integrity <integrity>", "expected npm dist.integrity for the runtime package").addOption(new Option("--protected-access-mode <mode>", "protected web access mode").choices(["cloudfront_default_domain", "alb_https_cert"]).default("cloudfront_default_domain")).addOption(new Option("--cloudfront-origin-protocol-policy <policy>", "CloudFront-to-ALB origin protocol policy").choices(["http-only", "https-only"]).default("http-only")).option("--cloudfront-origin-domain-name <hostname>", "origin hostname for CloudFront https-only mode; must resolve to the ALB and match certificate_arn").option("--evidence-bucket <name>", "S3 evidence bucket name").option("-j, --json", "print JSON").action((opts) => {
7745
7850
  try {
7746
7851
  const plan = buildAwsDeploymentPlan({
7747
7852
  accountName: opts.account,
@@ -7756,7 +7861,10 @@ cloud.command("plan").description("Generate a dry-run AWS deployment plan").opti
7756
7861
  ecrRepository: opts.ecrRepository,
7757
7862
  image: opts.image,
7758
7863
  runtimePackageVersion: opts.runtimePackageVersion,
7864
+ runtimePackageIntegrity: opts.runtimePackageIntegrity,
7759
7865
  protectedAccessMode: opts.protectedAccessMode,
7866
+ cloudfrontOriginProtocolPolicy: opts.cloudfrontOriginProtocolPolicy,
7867
+ cloudfrontOriginDomainName: opts.cloudfrontOriginDomainName,
7760
7868
  evidenceBucket: opts.evidenceBucket
7761
7869
  });
7762
7870
  print(plan, renderCloudPlan(plan), opts);
@@ -7764,7 +7872,7 @@ cloud.command("plan").description("Generate a dry-run AWS deployment plan").opti
7764
7872
  fail(error);
7765
7873
  }
7766
7874
  });
7767
- cloud.command("private-probe-config").description("Generate hosted-targeted private probe preflight configuration").option("--api-url <url>", "hosted Open Uptime API URL", "https://uptime.example.com/api/v1").option("--workspace-id <id>", "workspace id", "workspace-id").option("--probe-id <id>", "cloud registered private probe id").option("--private-key-file <path>", "private probe key file", "~/.hasna/uptime/probes/private-probe-01.key.pem").option("--machine-id <id>", "machine id", "private-probe-01").option("--log-level <level>", "probe log level", "info").option("--env", "print shell env file instead of summary text").option("-j, --json", "print JSON").action((opts) => {
7875
+ cloud.command("private-probe-config").description("Generate hosted-targeted private probe preflight configuration").option("--api-url <url>", "hosted Open Uptime API URL", "https://uptime.example.com/api/v1").option("--workspace-id <id>", "workspace id", "workspace-id").option("--probe-id <id>", "cloud registered private probe id").option("--private-key-file <path>", "private probe key file", "~/.hasna/uptime/probes/private-probe-01.key.pem").option("--machine-id <id>", "machine id", "private-probe-01").option("--log-level <level>", "probe log level", "info").option("--env", "print shell env file instead of summary text").option("--allow-blocked-env", "print the blocked preflight env anyway for review artifacts; do not start the probe").option("-j, --json", "print JSON").action((opts) => {
7768
7876
  try {
7769
7877
  const config = buildPrivateProbeCloudConfig({
7770
7878
  apiUrl: opts.apiUrl,
@@ -7775,7 +7883,7 @@ cloud.command("private-probe-config").description("Generate hosted-targeted priv
7775
7883
  logLevel: opts.logLevel
7776
7884
  });
7777
7885
  if (opts.env && !wantsJson(opts)) {
7778
- console.log(renderPrivateProbeEnv(config));
7886
+ console.log(renderPrivateProbeEnv(config, { allowBlocked: opts.allowBlockedEnv }));
7779
7887
  return;
7780
7888
  }
7781
7889
  print(config, renderPrivateProbeConfig(config), opts);
@@ -7783,6 +7891,22 @@ cloud.command("private-probe-config").description("Generate hosted-targeted priv
7783
7891
  fail(error);
7784
7892
  }
7785
7893
  });
7894
+ var cloudPublicChecks = cloud.command("public-checks").description("Run hosted public HTTP/TCP checks against the configured hosted store");
7895
+ 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
+ try {
7897
+ const svc = hostedService({
7898
+ hostedSqliteDb: opts.hostedSqliteDb,
7899
+ allowHostedLocalStore: opts.allowHostedLocalStore
7900
+ });
7901
+ const workspaceId = opts.workspaceId || process.env.HASNA_UPTIME_WORKSPACE_ID;
7902
+ const results = await svc.runDueHostedPublicChecks(new Date(opts.now), { workspaceId });
7903
+ svc.close();
7904
+ const data = { ok: true, workspaceId, checked: results.length, results };
7905
+ print(data, results.length ? renderCheckResults(results) : "No due hosted public checks", opts);
7906
+ } catch (error) {
7907
+ fail(error);
7908
+ }
7909
+ });
7786
7910
  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) => {
7787
7911
  try {
7788
7912
  const svc = service();
@@ -8144,6 +8268,7 @@ function renderCloudPlan(plan) {
8144
8268
  `host: ${plan.hostname}`,
8145
8269
  `cluster: ${plan.resources.ecsCluster}`,
8146
8270
  `image: ${plan.image.uri}`,
8271
+ ...plan.image.expectedIntegrity ? [`package integrity: ${plan.image.expectedIntegrity}`] : [],
8147
8272
  `image builder: ${plan.resources.imageBuilder}`,
8148
8273
  `dockerfile: ${plan.image.dockerfile}`,
8149
8274
  `infra: ${plan.infra.path}`,
@@ -8151,6 +8276,7 @@ function renderCloudPlan(plan) {
8151
8276
  `efs: ${plan.resources.efsFileSystem}`,
8152
8277
  `hosted sqlite: ${plan.resources.hostedSqliteDbPath}`,
8153
8278
  `protected access: ${plan.resources.protectedAccessMode} ${plan.resources.protectedAccessUrl}`,
8279
+ ...plan.resources.cloudfrontOrigin ? [`cloudfront origin: ${plan.resources.cloudfrontOrigin.protocolPolicy} ${plan.resources.cloudfrontOrigin.domainName}`] : [],
8154
8280
  `services: ${plan.resources.services.map((service2) => `${service2.name}:${service2.desiredCount}/${service2.targetDesiredCount}`).join(", ")}`,
8155
8281
  `evidence bucket: ${plan.resources.evidenceBucket}`,
8156
8282
  `blockers: ${plan.blockers.length}`,
@@ -11,7 +11,10 @@ export interface AwsDeploymentPlanOptions {
11
11
  evidenceBucket?: string;
12
12
  hostedSqliteDbPath?: string;
13
13
  runtimePackageVersion?: string;
14
+ runtimePackageIntegrity?: string;
14
15
  protectedAccessMode?: "cloudfront_default_domain" | "alb_https_cert";
16
+ cloudfrontOriginProtocolPolicy?: "http-only" | "https-only";
17
+ cloudfrontOriginDomainName?: string;
15
18
  /** @deprecated Postgres is target-state only until the async adapter is implemented. */
16
19
  rdsInstanceId?: string;
17
20
  /** @deprecated Postgres is target-state only until the async adapter is implemented. */
@@ -49,6 +52,13 @@ export interface AwsDeploymentPlan {
49
52
  protectedAccessMode: "cloudfront_default_domain" | "alb_https_cert";
50
53
  edgeDistribution?: string;
51
54
  protectedAccessUrl: string;
55
+ cloudfrontOrigin?: {
56
+ protocolPolicy: "http-only" | "https-only";
57
+ domainName: string;
58
+ requiresMatchingCertificate: boolean;
59
+ liveTrafficApproved: boolean;
60
+ risk?: string;
61
+ };
52
62
  originVerification: {
53
63
  mode: "cloudfront_origin_header" | "alb_tls";
54
64
  requiredBeforeScaleUp: boolean;
@@ -66,6 +76,7 @@ export interface AwsDeploymentPlan {
66
76
  repository: string;
67
77
  uri: string;
68
78
  dockerfile: string;
79
+ expectedIntegrity?: string;
69
80
  buildCommand: string;
70
81
  pushCommands: string[];
71
82
  };
@@ -137,5 +148,7 @@ export interface PrivateProbeCloudConfig {
137
148
  }
138
149
  export declare function buildAwsDeploymentPlan(options?: AwsDeploymentPlanOptions): AwsDeploymentPlan;
139
150
  export declare function buildPrivateProbeCloudConfig(options?: PrivateProbeCloudConfigOptions): PrivateProbeCloudConfig;
140
- export declare function renderPrivateProbeEnv(config: PrivateProbeCloudConfig): string;
151
+ export declare function renderPrivateProbeEnv(config: PrivateProbeCloudConfig, options?: {
152
+ allowBlocked?: boolean;
153
+ }): string;
141
154
  //# sourceMappingURL=cloud-plan.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"cloud-plan.d.ts","sourceRoot":"","sources":["../src/cloud-plan.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"cloud-plan.d.ts","sourceRoot":"","sources":["../src/cloud-plan.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,wBAAwB;IACvC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC,mBAAmB,CAAC,EAAE,2BAA2B,GAAG,gBAAgB,CAAC;IACrE,8BAA8B,CAAC,EAAE,WAAW,GAAG,YAAY,CAAC;IAC5D,0BAA0B,CAAC,EAAE,MAAM,CAAC;IACpC,wFAAwF;IACxF,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,wFAAwF;IACxF,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,mBAAmB,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,iCAAiC,CAAC;IACxC,OAAO,EAAE,CAAC,CAAC;IACX,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,SAAS,CAAC;IAClB,QAAQ,EAAE,KAAK,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,QAAQ,CAAC;IACf,SAAS,EAAE;QACT,aAAa,EAAE,MAAM,CAAC;QACtB,YAAY,EAAE,MAAM,CAAC;QACrB,UAAU,EAAE,MAAM,CAAC;QACnB,QAAQ,EAAE,cAAc,EAAE,CAAC;QAC3B,KAAK,EAAE,MAAM,CAAC;QACd,aAAa,EAAE,MAAM,CAAC;QACtB,cAAc,EAAE,MAAM,CAAC;QACvB,kBAAkB,EAAE,MAAM,CAAC;QAC3B,cAAc,EAAE,MAAM,CAAC;QACvB,YAAY,EAAE,MAAM,CAAC;QACrB,mBAAmB,EAAE,2BAA2B,GAAG,gBAAgB,CAAC;QACpE,gBAAgB,CAAC,EAAE,MAAM,CAAC;QAC1B,kBAAkB,EAAE,MAAM,CAAC;QAC3B,gBAAgB,CAAC,EAAE;YACjB,cAAc,EAAE,WAAW,GAAG,YAAY,CAAC;YAC3C,UAAU,EAAE,MAAM,CAAC;YACnB,2BAA2B,EAAE,OAAO,CAAC;YACrC,mBAAmB,EAAE,OAAO,CAAC;YAC7B,IAAI,CAAC,EAAE,MAAM,CAAC;SACf,CAAC;QACF,kBAAkB,EAAE;YAClB,IAAI,EAAE,0BAA0B,GAAG,SAAS,CAAC;YAC7C,qBAAqB,EAAE,OAAO,CAAC;YAC/B,UAAU,CAAC,EAAE,MAAM,CAAC;YACpB,2BAA2B,EAAE,OAAO,CAAC;YACrC,kBAAkB,CAAC,EAAE,MAAM,CAAC;SAC7B,CAAC;QACF,YAAY,EAAE,MAAM,EAAE,CAAC;QACvB,cAAc,EAAE,MAAM,EAAE,CAAC;QACzB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QAChC,SAAS,EAAE,MAAM,EAAE,CAAC;QACpB,MAAM,EAAE,MAAM,EAAE,CAAC;KAClB,CAAC;IACF,KAAK,EAAE;QACL,UAAU,EAAE,MAAM,CAAC;QACnB,GAAG,EAAE,MAAM,CAAC;QACZ,UAAU,EAAE,MAAM,CAAC;QACnB,iBAAiB,CAAC,EAAE,MAAM,CAAC;QAC3B,YAAY,EAAE,MAAM,CAAC;QACrB,YAAY,EAAE,MAAM,EAAE,CAAC;KACxB,CAAC;IACF,KAAK,EAAE;QACL,IAAI,EAAE,MAAM,CAAC;QACb,UAAU,EAAE,MAAM,CAAC;QACnB,WAAW,EAAE,MAAM,CAAC;QACpB,eAAe,EAAE,MAAM,CAAC;QACxB,WAAW,EAAE,MAAM,CAAC;QACpB,YAAY,EAAE,KAAK,CAAC;KACrB,CAAC;IACF,OAAO,EAAE;QACP,SAAS,EAAE,MAAM,EAAE,CAAC;QACpB,SAAS,EAAE,MAAM,EAAE,CAAC;QACpB,MAAM,EAAE,MAAM,EAAE,CAAC;QACjB,QAAQ,EAAE,MAAM,EAAE,CAAC;QACnB,YAAY,EAAE,MAAM,EAAE,CAAC;KACxB,CAAC;IACF,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,gBAAgB,EAAE,MAAM,EAAE,CAAC;IAC3B,MAAM,EAAE;QACN,eAAe,EAAE,KAAK,CAAC;QACvB,gBAAgB,EAAE,KAAK,CAAC;QACxB,wBAAwB,EAAE,KAAK,CAAC;QAChC,KAAK,EAAE,MAAM,EAAE,CAAC;KACjB,CAAC;CACH;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,KAAK,GAAG,WAAW,GAAG,cAAc,GAAG,UAAU,GAAG,WAAW,CAAC;IACtE,YAAY,EAAE,MAAM,CAAC;IACrB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACpC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACjC;AAED,MAAM,WAAW,8BAA8B;IAC7C,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,uBAAuB;IACtC,IAAI,EAAE,wCAAwC,CAAC;IAC/C,OAAO,EAAE,CAAC,CAAC;IACX,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,SAAS,CAAC;IAClB,QAAQ,EAAE,KAAK,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,eAAe,CAAC;IACtB,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC5B,KAAK,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC9D,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,MAAM,EAAE;QACN,gBAAgB,EAAE,KAAK,CAAC;QACxB,WAAW,EAAE,KAAK,CAAC;QACnB,KAAK,EAAE,MAAM,EAAE,CAAC;KACjB,CAAC;CACH;AAaD,wBAAgB,sBAAsB,CAAC,OAAO,GAAE,wBAA6B,GAAG,iBAAiB,CAsOhG;AAED,wBAAgB,4BAA4B,CAAC,OAAO,GAAE,8BAAmC,GAAG,uBAAuB,CA2DlH;AAED,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,uBAAuB,EAAE,OAAO,GAAE;IAAE,YAAY,CAAC,EAAE,OAAO,CAAA;CAAO,GAAG,MAAM,CAYvH"}
@@ -9,6 +9,7 @@ var DEFAULT_WORKSPACE_ID = "workspace-id";
9
9
  var DEFAULT_VPC_ID = "vpc-xxxxxxxx";
10
10
  var DEFAULT_HOSTED_SQLITE_DB = "/data/uptime/uptime.db";
11
11
  var DEFAULT_PROTECTED_ACCESS_MODE = "cloudfront_default_domain";
12
+ var DEFAULT_CLOUDFRONT_ORIGIN_PROTOCOL_POLICY = "http-only";
12
13
  function buildAwsDeploymentPlan(options = {}) {
13
14
  const region = clean(options.region, DEFAULT_REGION);
14
15
  const stage = clean(options.stage, DEFAULT_STAGE);
@@ -21,8 +22,11 @@ function buildAwsDeploymentPlan(options = {}) {
21
22
  const image = clean(options.image, `${imageRepositoryUri}@sha256:<image-digest>`);
22
23
  const evidenceBucket = clean(options.evidenceBucket, `hasna-${stage}-${prefix}-evidence`);
23
24
  const hostedSqliteDbPath = clean(options.hostedSqliteDbPath, DEFAULT_HOSTED_SQLITE_DB);
24
- const runtimePackageVersion = clean(options.runtimePackageVersion, "0.1.23");
25
+ const runtimePackageVersion = clean(options.runtimePackageVersion, "0.1.25");
26
+ const runtimePackageIntegrity = options.runtimePackageIntegrity?.trim() || undefined;
25
27
  const protectedAccessMode = options.protectedAccessMode ?? DEFAULT_PROTECTED_ACCESS_MODE;
28
+ const cloudfrontOriginProtocolPolicy = options.cloudfrontOriginProtocolPolicy ?? DEFAULT_CLOUDFRONT_ORIGIN_PROTOCOL_POLICY;
29
+ const cloudfrontOriginDomainName = clean(options.cloudfrontOriginDomainName, "<alb-dns-name>");
26
30
  const protectedAccessUrl = protectedAccessMode === "cloudfront_default_domain" ? "https://<cloudfront-domain>" : `https://${hostname}`;
27
31
  const cluster = `${prefix}-${stage}`;
28
32
  const secrets = {
@@ -89,6 +93,13 @@ function buildAwsDeploymentPlan(options = {}) {
89
93
  protectedAccessMode,
90
94
  edgeDistribution: protectedAccessMode === "cloudfront_default_domain" ? `${prefix}-${stage}-edge` : undefined,
91
95
  protectedAccessUrl,
96
+ cloudfrontOrigin: protectedAccessMode === "cloudfront_default_domain" ? {
97
+ protocolPolicy: cloudfrontOriginProtocolPolicy,
98
+ domainName: cloudfrontOriginProtocolPolicy === "https-only" ? cloudfrontOriginDomainName : "<alb-dns-name>",
99
+ requiresMatchingCertificate: cloudfrontOriginProtocolPolicy === "https-only",
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."
102
+ } : undefined,
92
103
  originVerification: protectedAccessMode === "cloudfront_default_domain" ? {
93
104
  mode: "cloudfront_origin_header",
94
105
  requiredBeforeScaleUp: true,
@@ -121,6 +132,7 @@ function buildAwsDeploymentPlan(options = {}) {
121
132
  repository: ecrRepository,
122
133
  uri: image,
123
134
  dockerfile: "Dockerfile.package",
135
+ expectedIntegrity: runtimePackageIntegrity,
124
136
  buildCommand: `BLOCKED: after infra approval, AWS CodeBuild builds Dockerfile.package from @hasna/uptime@${runtimePackageVersion} into ${imageRepositoryUri}`,
125
137
  pushCommands: [
126
138
  `BLOCKED: start ${prefix}-${stage}-image-builder only through the approved deploy pipeline after @hasna/uptime@${runtimePackageVersion} is published`,
@@ -147,16 +159,16 @@ function buildAwsDeploymentPlan(options = {}) {
147
159
  `Infra PR must declare CodeBuild image builder ${prefix}-${stage}-image-builder for @hasna/uptime@${runtimePackageVersion}.`,
148
160
  `Infra PR must declare hardened S3 evidence bucket ${evidenceBucket} with KMS, versioning, lifecycle, and public access block.`,
149
161
  `Infra PR must declare encrypted EFS ${prefix}-${stage}-data with access point, mount targets, and AWS Backup plan.`,
150
- protectedAccessMode === "cloudfront_default_domain" ? "Infra PR must declare CloudFront default-domain HTTPS edge, ALB HTTP listener restricted to CloudFront origin-facing ranges, CloudFront-only origin verification header binding, ECS/Fargate cluster, target groups, security groups, IAM roles, CloudWatch log groups, and Secrets Manager refs." : `Infra PR must declare ECS/Fargate cluster ${cluster}, ALB HTTPS listener, target groups, security groups, IAM roles, CloudWatch log groups, and Secrets Manager refs.`,
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.`,
151
163
  "Only apply the infra plan from the approved infrastructure repository after review evidence is attached."
152
164
  ],
153
165
  deploy: [
154
166
  "Build and publish the image only after the Dockerfile/container target is reviewed.",
155
- `Start the AWS image builder for @hasna/uptime@${runtimePackageVersion} and record the pushed image digest.`,
167
+ 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.`,
156
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.",
157
169
  `Register task definitions for ${services.map((service) => service.name).join(", ")} using valueFrom secrets.`,
158
170
  `Update ECS services in cluster ${cluster} one component at a time through the approved deploy pipeline.`,
159
- protectedAccessMode === "cloudfront_default_domain" ? "Use the CloudFront default HTTPS domain with origin verification header binding for first protected access; add custom DNS/certificate only after edge ownership is approved." : `Create Route53/edge record for ${hostname} only after ALB health checks pass and auth denial smokes succeed.`
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.`
160
172
  ],
161
173
  rollback: [
162
174
  "Keep previous task definition ARNs before each service update.",
@@ -173,6 +185,8 @@ function buildAwsDeploymentPlan(options = {}) {
173
185
  blockers: [
174
186
  "The infrastructure owner repository was not found in this workspace.",
175
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."] : [],
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."] : [],
176
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.",
177
191
  "Hosted production auth/RBAC must replace broad static hosted-token operation before exposure.",
178
192
  "Public probe execution still needs cloud check-job leases wired to runHostedHttpCheck and live policy-decision log evidence.",
@@ -181,8 +195,9 @@ function buildAwsDeploymentPlan(options = {}) {
181
195
  requiredEvidence: [
182
196
  "Infrastructure PR/synth/plan from the approved infra repository.",
183
197
  "CodeBuild image-builder run, container smoke, and immutable image digest.",
198
+ "Published package dist.integrity pinned in the private infra root or an explicit not-live exception.",
184
199
  "ECS task definitions using secrets.valueFrom only.",
185
- "CloudFront-default-domain origin-header config or ALB TLS auth-denial smokes, direct-origin denial evidence, and web alarm checks.",
200
+ "CloudFront-default-domain origin-header config, origin transport decision, direct-origin denial evidence, auth-denial smokes, and web alarm checks.",
186
201
  "Single-writer ECS evidence: one web task maximum and no scheduler/public-probe/reporter EFS mounts.",
187
202
  "EFS encryption, access point, mount-target, AWS Backup, and restore-drill evidence.",
188
203
  "S3 bucket KMS, versioning, lifecycle, and public-access-block evidence.",
@@ -195,11 +210,13 @@ function buildAwsDeploymentPlan(options = {}) {
195
210
  notes: [
196
211
  "This plan generator does not call AWS.",
197
212
  "Blocked plan output intentionally avoids copy-pastable AWS mutation commands.",
198
- "Default protected access uses CloudFront's HTTPS default domain so first deploy is not blocked on custom DNS or ACM.",
213
+ "Default protected access uses CloudFront's HTTPS default domain so first zero-count deploy is not blocked on custom DNS or ACM.",
199
214
  "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.",
215
+ "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.",
200
216
  "Hosted runtime uses explicit EFS-backed SQLite at HASNA_UPTIME_HOSTED_SQLITE_DB until the async Postgres adapter exists.",
201
217
  "Do not set HASNA_UPTIME_DATABASE_URL for hosted tasks until the Postgres adapter is implemented.",
202
218
  "Secrets are represented as secret names/refs and must be injected with valueFrom.",
219
+ "Set runtime_package_integrity in the approved infra root after publish so the AWS image builder verifies the npm tarball before ECR build.",
203
220
  "Actual deploy belongs in the deploy_release_operate_final goal node after infra review."
204
221
  ]
205
222
  }
@@ -266,7 +283,10 @@ function buildPrivateProbeCloudConfig(options = {}) {
266
283
  }
267
284
  };
268
285
  }
269
- function renderPrivateProbeEnv(config) {
286
+ function renderPrivateProbeEnv(config, options = {}) {
287
+ if (!options.allowBlocked && (!config.canStart || config.blockers.length > 0)) {
288
+ throw new Error("private probe env output is blocked until hosted probe routes and cloud jobs are implemented");
289
+ }
270
290
  const required = ["HASNA_UPTIME_PRIVATE_PROBE_ID"];
271
291
  const missing = required.filter((key) => !config.env[key]);
272
292
  if (missing.length > 0) {