@hasna/uptime 0.1.5 → 0.1.6

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/.dockerignore ADDED
@@ -0,0 +1,14 @@
1
+ .git
2
+ .gitignore
3
+ .project.json
4
+ node_modules
5
+ dist
6
+ coverage
7
+ *.tgz
8
+ *.log
9
+ .env
10
+ .env.*
11
+ .hasna
12
+ infra/**/.terraform
13
+ infra/**/*.tfstate
14
+ infra/**/*.tfstate.*
package/CHANGELOG.md CHANGED
@@ -6,6 +6,25 @@ project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.1.6] - 2026-06-28
10
+
11
+ ### Added
12
+
13
+ - Bun-based hosted runtime `Dockerfile` and `.dockerignore`.
14
+ - Reviewable Terraform/OpenTofu AWS starter plan under `infra/aws` for ECR,
15
+ S3 evidence storage, ECS/Fargate services, ALB/TLS/DNS, task roles,
16
+ CloudWatch logs, security groups, and secret refs.
17
+ - Cloud plan SDK/CLI fields that point to `Dockerfile` and `infra/aws` with
18
+ format/init/validate/plan commands while keeping apply disabled.
19
+
20
+ ### Security
21
+
22
+ - AWS infra templates use secret ARNs/valueFrom references and example
23
+ placeholders only; no plaintext service tokens, database URLs, or private keys
24
+ are stored in the repo.
25
+ - Terraform desired counts default to zero until hosted cloud-store/auth/probe
26
+ blockers are closed.
27
+
9
28
  ## [0.1.5] - 2026-06-28
10
29
 
11
30
  ### Added
@@ -13,8 +32,8 @@ project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
13
32
  - Dry-run AWS deployment plan generator for the `hasna-xyz-infra` target,
14
33
  covering ECS/Fargate services, ECR image commands, ALB/RDS/S3/Secrets/Logs
15
34
  resources, rollback steps, and safety assertions.
16
- - Spark01 cloud-primary private probe config generator with JSON and env-file
17
- rendering.
35
+ - Spark01 hosted-targeted private probe preflight config generator with JSON and
36
+ env-file rendering.
18
37
  - CLI commands `uptime cloud plan` and `uptime cloud spark01-config`.
19
38
  - SDK export `@hasna/uptime/cloud-plan`.
20
39
  - Machine-readable `blocked`/`canApply:false` and `blocked`/`canStart:false`
package/Dockerfile ADDED
@@ -0,0 +1,30 @@
1
+ # syntax=docker/dockerfile:1
2
+
3
+ FROM oven/bun:1.3.13-slim AS build
4
+ WORKDIR /app
5
+
6
+ COPY package.json bun.lock tsconfig.json tsconfig.build.json ./
7
+ COPY src ./src
8
+
9
+ RUN bun install --frozen-lockfile
10
+ RUN bun run build
11
+ RUN bun install --production --frozen-lockfile
12
+
13
+ FROM oven/bun:1.3.13-slim AS runtime
14
+ ENV NODE_ENV=production \
15
+ HASNA_UPTIME_MODE=hosted
16
+ WORKDIR /app
17
+
18
+ RUN addgroup --system uptime && adduser --system --ingroup uptime uptime
19
+
20
+ COPY --from=build /app/package.json ./package.json
21
+ COPY --from=build /app/node_modules ./node_modules
22
+ COPY --from=build /app/dist ./dist
23
+
24
+ USER uptime
25
+ EXPOSE 3899
26
+
27
+ HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
28
+ CMD bun -e "const r = await fetch('http://127.0.0.1:3899/health'); process.exit(r.ok ? 0 : 1)"
29
+
30
+ CMD ["bun", "dist/cli/index.js", "serve", "--mode", "hosted", "--host", "0.0.0.0", "--port", "3899"]
package/README.md CHANGED
@@ -46,6 +46,10 @@ only. They do not call AWS, write secrets, or produce an approved deploy script;
46
46
  current output is intentionally blocked until the infra and cloud-store evidence
47
47
  in `docs/aws-deployment-runbook.md` is satisfied.
48
48
 
49
+ Deployment review artifacts live in `Dockerfile` and `infra/aws`. The Terraform
50
+ desired counts default to zero, and `uptime cloud plan --json` exposes the
51
+ format/init/validate/plan commands with `applyAllowed: false`.
52
+
49
53
  Private/local probes can submit signed results from another machine:
50
54
 
51
55
  ```bash
package/dist/cli/index.js CHANGED
@@ -6404,7 +6404,8 @@ function buildAwsDeploymentPlan(options = {}) {
6404
6404
  const hostname = clean(options.hostname, DEFAULT_HOSTNAME);
6405
6405
  const workspaceId = clean(options.workspaceId, DEFAULT_WORKSPACE_ID);
6406
6406
  const ecrRepository = clean(options.ecrRepository, `hasna/opensource/${prefix}`);
6407
- const image = clean(options.image, `<account-id>.dkr.ecr.${region}.amazonaws.com/${ecrRepository}:<git-sha>`);
6407
+ const imageRepositoryUri = `<account-id>.dkr.ecr.${region}.amazonaws.com/${ecrRepository}`;
6408
+ const image = clean(options.image, `${imageRepositoryUri}@sha256:<image-digest>`);
6408
6409
  const evidenceBucket = clean(options.evidenceBucket, `hasna-${stage}-${prefix}-evidence`);
6409
6410
  const cluster = `${prefix}-${stage}`;
6410
6411
  const secrets = {
@@ -6470,26 +6471,34 @@ function buildAwsDeploymentPlan(options = {}) {
6470
6471
  `${prefix}-${stage}-web-sg`,
6471
6472
  `${prefix}-${stage}-scheduler-sg`,
6472
6473
  `${prefix}-${stage}-public-probe-sg`,
6473
- `${prefix}-${stage}-rds-client-sg`
6474
+ `${prefix}-${stage}-reporter-sg`,
6475
+ `${prefix}-${stage}-migration-sg`
6474
6476
  ],
6475
6477
  secrets,
6476
6478
  logGroups: services.map((service) => service.logGroup),
6477
6479
  alarms: [
6478
6480
  `${prefix}-${stage}-web-5xx`,
6479
- `${prefix}-${stage}-scheduler-stalled`,
6480
- `${prefix}-${stage}-probe-stale`,
6481
- `${prefix}-${stage}-report-delivery-failures`
6481
+ `${prefix}-${stage}-web-unhealthy`
6482
6482
  ]
6483
6483
  },
6484
6484
  image: {
6485
6485
  repository: ecrRepository,
6486
6486
  uri: image,
6487
- buildCommand: "BLOCKED: add a reviewed Dockerfile/container build target before running docker build",
6487
+ dockerfile: "Dockerfile",
6488
+ buildCommand: `docker build --pull -t ${imageRepositoryUri}:<git-sha> .`,
6488
6489
  pushCommands: [
6489
6490
  "BLOCKED: push only from approved CI/CD after the ECR repository and image digest policy exist",
6490
6491
  "BLOCKED: deploy services by immutable image digest, not by mutable tags"
6491
6492
  ]
6492
6493
  },
6494
+ infra: {
6495
+ path: "infra/aws",
6496
+ fmtCommand: "terraform -chdir=infra/aws fmt -check",
6497
+ initCommand: "terraform -chdir=infra/aws init -backend=false",
6498
+ validateCommand: "terraform -chdir=infra/aws validate",
6499
+ planCommand: "terraform -chdir=infra/aws plan -out open-uptime.tfplan",
6500
+ applyAllowed: false
6501
+ },
6493
6502
  runbook: {
6494
6503
  preflight: [
6495
6504
  `aws sts get-caller-identity --profile ${accountName}`,
@@ -6524,7 +6533,6 @@ function buildAwsDeploymentPlan(options = {}) {
6524
6533
  },
6525
6534
  blockers: [
6526
6535
  "The hasna-xyz-infra infrastructure owner repository was not found in this workspace.",
6527
- "The repo has no reviewed Dockerfile/container build target for image build and publish automation.",
6528
6536
  "Hosted Postgres storage adapter and migrations are not implemented.",
6529
6537
  "Hosted production auth/RBAC must replace broad static hosted-token operation before exposure.",
6530
6538
  "Public probe execution still needs DNS, redirect, and rebinding SSRF enforcement plus cloud check-job leases.",
@@ -6534,7 +6542,7 @@ function buildAwsDeploymentPlan(options = {}) {
6534
6542
  "Infrastructure PR/synth/plan from the approved infra repository.",
6535
6543
  "Container build smoke and immutable image digest.",
6536
6544
  "ECS task definitions using secrets.valueFrom only.",
6537
- "ALB/TLS/DNS/auth denial smokes.",
6545
+ "ALB/TLS/DNS/auth denial smokes and web alarm checks.",
6538
6546
  "RDS TLS, backups/PITR, scoped roles, and migration dry-run evidence.",
6539
6547
  "S3 bucket KMS, versioning, lifecycle, and public-access-block evidence.",
6540
6548
  "Spark01 private-probe registration, key-file mode, heartbeat, and revocation evidence."
@@ -6606,7 +6614,7 @@ function buildSpark01CloudConfig(options = {}) {
6606
6614
  privateKeyInline: false,
6607
6615
  tokenInline: false,
6608
6616
  notes: [
6609
- "This config is cloud-primary: Spark01 submits to hosted API state instead of local SQLite.",
6617
+ "This config is hosted-targeted preflight: Spark01 must not start until cloud probe routes are backed by hosted state.",
6610
6618
  "The private key file path is referenced, not embedded.",
6611
6619
  "Hosted token or probe auth material must come from the machine secret store, not this generated config."
6612
6620
  ]
@@ -6627,7 +6635,8 @@ function servicePlan(prefix, stage, role, desiredCount, image, workspaceId, secr
6627
6635
  return {
6628
6636
  name,
6629
6637
  role,
6630
- desiredCount,
6638
+ desiredCount: 0,
6639
+ targetDesiredCount: desiredCount,
6631
6640
  taskRole: `${name}-task-role`,
6632
6641
  executionRole: `${prefix}-${stage}-execution-role`,
6633
6642
  logGroup: `/ecs/${name}`,
@@ -6636,7 +6645,7 @@ function servicePlan(prefix, stage, role, desiredCount, image, workspaceId, secr
6636
6645
  HASNA_UPTIME_IMAGE: image,
6637
6646
  ...environment
6638
6647
  },
6639
- secrets: role === "public-probe" ? { DATABASE_URL: secrets.database, PROBE_CONFIG: secrets.publicProbe } : role === "reporter" ? { DATABASE_URL: secrets.database, REPORTING_CONFIG: secrets.reporting } : { DATABASE_URL: secrets.database, APP_ENV: secrets.appEnv }
6648
+ secrets: role === "web" ? { HASNA_UPTIME_DATABASE_URL: secrets.database, APP_ENV: secrets.appEnv, HASNA_UPTIME_HOSTED_TOKEN: secrets.hostedToken } : role === "public-probe" ? { PROBE_CONFIG: secrets.publicProbe } : role === "reporter" ? { HASNA_UPTIME_DATABASE_URL: secrets.database, REPORTING_CONFIG: secrets.reporting } : { HASNA_UPTIME_DATABASE_URL: secrets.database, APP_ENV: secrets.appEnv }
6640
6649
  };
6641
6650
  }
6642
6651
  function clean(value, fallback) {
@@ -6976,7 +6985,7 @@ cloud.command("plan").description("Generate a dry-run AWS deployment plan for ha
6976
6985
  fail(error);
6977
6986
  }
6978
6987
  });
6979
- cloud.command("spark01-config").description("Generate Spark01 cloud-primary private probe configuration").option("--api-url <url>", "hosted Open Uptime API URL", "https://uptime.hasna.xyz/api/v1").option("--workspace-id <id>", "workspace id", "wks_2tyysw05cwap").option("--probe-id <id>", "cloud registered private probe id").option("--private-key-file <path>", "Spark01 private probe key file", "~/.hasna/uptime/probes/spark01.key.pem").option("--machine-id <id>", "machine id", "spark01").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) => {
6988
+ cloud.command("spark01-config").description("Generate Spark01 hosted-targeted private probe preflight configuration").option("--api-url <url>", "hosted Open Uptime API URL", "https://uptime.hasna.xyz/api/v1").option("--workspace-id <id>", "workspace id", "wks_2tyysw05cwap").option("--probe-id <id>", "cloud registered private probe id").option("--private-key-file <path>", "Spark01 private probe key file", "~/.hasna/uptime/probes/spark01.key.pem").option("--machine-id <id>", "machine id", "spark01").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) => {
6980
6989
  try {
6981
6990
  const config = buildSpark01CloudConfig({
6982
6991
  apiUrl: opts.apiUrl,
@@ -7355,9 +7364,11 @@ function renderCloudPlan(plan) {
7355
7364
  `host: ${plan.hostname}`,
7356
7365
  `cluster: ${plan.resources.ecsCluster}`,
7357
7366
  `image: ${plan.image.uri}`,
7367
+ `dockerfile: ${plan.image.dockerfile}`,
7368
+ `infra: ${plan.infra.path}`,
7358
7369
  `vpc: ${plan.resources.vpcId}`,
7359
7370
  `rds: ${plan.resources.rdsInstanceId}`,
7360
- `services: ${plan.resources.services.map((service2) => `${service2.name}:${service2.desiredCount}`).join(", ")}`,
7371
+ `services: ${plan.resources.services.map((service2) => `${service2.name}:${service2.desiredCount}/${service2.targetDesiredCount}`).join(", ")}`,
7361
7372
  `evidence bucket: ${plan.resources.evidenceBucket}`,
7362
7373
  `blockers: ${plan.blockers.length}`,
7363
7374
  "live AWS mutation: false"
@@ -47,9 +47,18 @@ export interface AwsDeploymentPlan {
47
47
  image: {
48
48
  repository: string;
49
49
  uri: string;
50
+ dockerfile: string;
50
51
  buildCommand: string;
51
52
  pushCommands: string[];
52
53
  };
54
+ infra: {
55
+ path: string;
56
+ fmtCommand: string;
57
+ initCommand: string;
58
+ validateCommand: string;
59
+ planCommand: string;
60
+ applyAllowed: false;
61
+ };
53
62
  runbook: {
54
63
  preflight: string[];
55
64
  provision: string[];
@@ -70,6 +79,7 @@ export interface AwsServicePlan {
70
79
  name: string;
71
80
  role: "web" | "scheduler" | "public-probe" | "reporter" | "migration";
72
81
  desiredCount: number;
82
+ targetDesiredCount: number;
73
83
  taskRole: string;
74
84
  executionRole: string;
75
85
  logGroup: string;
@@ -1 +1 @@
1
- {"version":3,"file":"cloud-plan.d.ts","sourceRoot":"","sources":["../src/cloud-plan.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"cloud-plan.d.ts","sourceRoot":"","sources":["../src/cloud-plan.ts"],"names":[],"mappings":"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,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,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,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,YAAY,EAAE,MAAM,CAAC;QACrB,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,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,OAAO,EAAE,MAAM,EAAE,CAAC;KACnB,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,yBAAyB;IACxC,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,kBAAkB;IACjC,IAAI,EAAE,kCAAkC,CAAC;IACzC,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;AAWD,wBAAgB,sBAAsB,CAAC,OAAO,GAAE,wBAA6B,GAAG,iBAAiB,CAoKhG;AAED,wBAAgB,uBAAuB,CAAC,OAAO,GAAE,yBAA8B,GAAG,kBAAkB,CA2DnG;AAED,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,kBAAkB,GAAG,MAAM,CASnE"}
@@ -16,7 +16,8 @@ function buildAwsDeploymentPlan(options = {}) {
16
16
  const hostname = clean(options.hostname, DEFAULT_HOSTNAME);
17
17
  const workspaceId = clean(options.workspaceId, DEFAULT_WORKSPACE_ID);
18
18
  const ecrRepository = clean(options.ecrRepository, `hasna/opensource/${prefix}`);
19
- const image = clean(options.image, `<account-id>.dkr.ecr.${region}.amazonaws.com/${ecrRepository}:<git-sha>`);
19
+ const imageRepositoryUri = `<account-id>.dkr.ecr.${region}.amazonaws.com/${ecrRepository}`;
20
+ const image = clean(options.image, `${imageRepositoryUri}@sha256:<image-digest>`);
20
21
  const evidenceBucket = clean(options.evidenceBucket, `hasna-${stage}-${prefix}-evidence`);
21
22
  const cluster = `${prefix}-${stage}`;
22
23
  const secrets = {
@@ -82,26 +83,34 @@ function buildAwsDeploymentPlan(options = {}) {
82
83
  `${prefix}-${stage}-web-sg`,
83
84
  `${prefix}-${stage}-scheduler-sg`,
84
85
  `${prefix}-${stage}-public-probe-sg`,
85
- `${prefix}-${stage}-rds-client-sg`
86
+ `${prefix}-${stage}-reporter-sg`,
87
+ `${prefix}-${stage}-migration-sg`
86
88
  ],
87
89
  secrets,
88
90
  logGroups: services.map((service) => service.logGroup),
89
91
  alarms: [
90
92
  `${prefix}-${stage}-web-5xx`,
91
- `${prefix}-${stage}-scheduler-stalled`,
92
- `${prefix}-${stage}-probe-stale`,
93
- `${prefix}-${stage}-report-delivery-failures`
93
+ `${prefix}-${stage}-web-unhealthy`
94
94
  ]
95
95
  },
96
96
  image: {
97
97
  repository: ecrRepository,
98
98
  uri: image,
99
- buildCommand: "BLOCKED: add a reviewed Dockerfile/container build target before running docker build",
99
+ dockerfile: "Dockerfile",
100
+ buildCommand: `docker build --pull -t ${imageRepositoryUri}:<git-sha> .`,
100
101
  pushCommands: [
101
102
  "BLOCKED: push only from approved CI/CD after the ECR repository and image digest policy exist",
102
103
  "BLOCKED: deploy services by immutable image digest, not by mutable tags"
103
104
  ]
104
105
  },
106
+ infra: {
107
+ path: "infra/aws",
108
+ fmtCommand: "terraform -chdir=infra/aws fmt -check",
109
+ initCommand: "terraform -chdir=infra/aws init -backend=false",
110
+ validateCommand: "terraform -chdir=infra/aws validate",
111
+ planCommand: "terraform -chdir=infra/aws plan -out open-uptime.tfplan",
112
+ applyAllowed: false
113
+ },
105
114
  runbook: {
106
115
  preflight: [
107
116
  `aws sts get-caller-identity --profile ${accountName}`,
@@ -136,7 +145,6 @@ function buildAwsDeploymentPlan(options = {}) {
136
145
  },
137
146
  blockers: [
138
147
  "The hasna-xyz-infra infrastructure owner repository was not found in this workspace.",
139
- "The repo has no reviewed Dockerfile/container build target for image build and publish automation.",
140
148
  "Hosted Postgres storage adapter and migrations are not implemented.",
141
149
  "Hosted production auth/RBAC must replace broad static hosted-token operation before exposure.",
142
150
  "Public probe execution still needs DNS, redirect, and rebinding SSRF enforcement plus cloud check-job leases.",
@@ -146,7 +154,7 @@ function buildAwsDeploymentPlan(options = {}) {
146
154
  "Infrastructure PR/synth/plan from the approved infra repository.",
147
155
  "Container build smoke and immutable image digest.",
148
156
  "ECS task definitions using secrets.valueFrom only.",
149
- "ALB/TLS/DNS/auth denial smokes.",
157
+ "ALB/TLS/DNS/auth denial smokes and web alarm checks.",
150
158
  "RDS TLS, backups/PITR, scoped roles, and migration dry-run evidence.",
151
159
  "S3 bucket KMS, versioning, lifecycle, and public-access-block evidence.",
152
160
  "Spark01 private-probe registration, key-file mode, heartbeat, and revocation evidence."
@@ -218,7 +226,7 @@ function buildSpark01CloudConfig(options = {}) {
218
226
  privateKeyInline: false,
219
227
  tokenInline: false,
220
228
  notes: [
221
- "This config is cloud-primary: Spark01 submits to hosted API state instead of local SQLite.",
229
+ "This config is hosted-targeted preflight: Spark01 must not start until cloud probe routes are backed by hosted state.",
222
230
  "The private key file path is referenced, not embedded.",
223
231
  "Hosted token or probe auth material must come from the machine secret store, not this generated config."
224
232
  ]
@@ -239,7 +247,8 @@ function servicePlan(prefix, stage, role, desiredCount, image, workspaceId, secr
239
247
  return {
240
248
  name,
241
249
  role,
242
- desiredCount,
250
+ desiredCount: 0,
251
+ targetDesiredCount: desiredCount,
243
252
  taskRole: `${name}-task-role`,
244
253
  executionRole: `${prefix}-${stage}-execution-role`,
245
254
  logGroup: `/ecs/${name}`,
@@ -248,7 +257,7 @@ function servicePlan(prefix, stage, role, desiredCount, image, workspaceId, secr
248
257
  HASNA_UPTIME_IMAGE: image,
249
258
  ...environment
250
259
  },
251
- secrets: role === "public-probe" ? { DATABASE_URL: secrets.database, PROBE_CONFIG: secrets.publicProbe } : role === "reporter" ? { DATABASE_URL: secrets.database, REPORTING_CONFIG: secrets.reporting } : { DATABASE_URL: secrets.database, APP_ENV: secrets.appEnv }
260
+ secrets: role === "web" ? { HASNA_UPTIME_DATABASE_URL: secrets.database, APP_ENV: secrets.appEnv, HASNA_UPTIME_HOSTED_TOKEN: secrets.hostedToken } : role === "public-probe" ? { PROBE_CONFIG: secrets.publicProbe } : role === "reporter" ? { HASNA_UPTIME_DATABASE_URL: secrets.database, REPORTING_CONFIG: secrets.reporting } : { HASNA_UPTIME_DATABASE_URL: secrets.database, APP_ENV: secrets.appEnv }
252
261
  };
253
262
  }
254
263
  function clean(value, fallback) {
package/dist/index.js CHANGED
@@ -3807,7 +3807,8 @@ function buildAwsDeploymentPlan(options = {}) {
3807
3807
  const hostname = clean(options.hostname, DEFAULT_HOSTNAME);
3808
3808
  const workspaceId = clean(options.workspaceId, DEFAULT_WORKSPACE_ID);
3809
3809
  const ecrRepository = clean(options.ecrRepository, `hasna/opensource/${prefix}`);
3810
- const image = clean(options.image, `<account-id>.dkr.ecr.${region}.amazonaws.com/${ecrRepository}:<git-sha>`);
3810
+ const imageRepositoryUri = `<account-id>.dkr.ecr.${region}.amazonaws.com/${ecrRepository}`;
3811
+ const image = clean(options.image, `${imageRepositoryUri}@sha256:<image-digest>`);
3811
3812
  const evidenceBucket = clean(options.evidenceBucket, `hasna-${stage}-${prefix}-evidence`);
3812
3813
  const cluster = `${prefix}-${stage}`;
3813
3814
  const secrets = {
@@ -3873,26 +3874,34 @@ function buildAwsDeploymentPlan(options = {}) {
3873
3874
  `${prefix}-${stage}-web-sg`,
3874
3875
  `${prefix}-${stage}-scheduler-sg`,
3875
3876
  `${prefix}-${stage}-public-probe-sg`,
3876
- `${prefix}-${stage}-rds-client-sg`
3877
+ `${prefix}-${stage}-reporter-sg`,
3878
+ `${prefix}-${stage}-migration-sg`
3877
3879
  ],
3878
3880
  secrets,
3879
3881
  logGroups: services.map((service) => service.logGroup),
3880
3882
  alarms: [
3881
3883
  `${prefix}-${stage}-web-5xx`,
3882
- `${prefix}-${stage}-scheduler-stalled`,
3883
- `${prefix}-${stage}-probe-stale`,
3884
- `${prefix}-${stage}-report-delivery-failures`
3884
+ `${prefix}-${stage}-web-unhealthy`
3885
3885
  ]
3886
3886
  },
3887
3887
  image: {
3888
3888
  repository: ecrRepository,
3889
3889
  uri: image,
3890
- buildCommand: "BLOCKED: add a reviewed Dockerfile/container build target before running docker build",
3890
+ dockerfile: "Dockerfile",
3891
+ buildCommand: `docker build --pull -t ${imageRepositoryUri}:<git-sha> .`,
3891
3892
  pushCommands: [
3892
3893
  "BLOCKED: push only from approved CI/CD after the ECR repository and image digest policy exist",
3893
3894
  "BLOCKED: deploy services by immutable image digest, not by mutable tags"
3894
3895
  ]
3895
3896
  },
3897
+ infra: {
3898
+ path: "infra/aws",
3899
+ fmtCommand: "terraform -chdir=infra/aws fmt -check",
3900
+ initCommand: "terraform -chdir=infra/aws init -backend=false",
3901
+ validateCommand: "terraform -chdir=infra/aws validate",
3902
+ planCommand: "terraform -chdir=infra/aws plan -out open-uptime.tfplan",
3903
+ applyAllowed: false
3904
+ },
3896
3905
  runbook: {
3897
3906
  preflight: [
3898
3907
  `aws sts get-caller-identity --profile ${accountName}`,
@@ -3927,7 +3936,6 @@ function buildAwsDeploymentPlan(options = {}) {
3927
3936
  },
3928
3937
  blockers: [
3929
3938
  "The hasna-xyz-infra infrastructure owner repository was not found in this workspace.",
3930
- "The repo has no reviewed Dockerfile/container build target for image build and publish automation.",
3931
3939
  "Hosted Postgres storage adapter and migrations are not implemented.",
3932
3940
  "Hosted production auth/RBAC must replace broad static hosted-token operation before exposure.",
3933
3941
  "Public probe execution still needs DNS, redirect, and rebinding SSRF enforcement plus cloud check-job leases.",
@@ -3937,7 +3945,7 @@ function buildAwsDeploymentPlan(options = {}) {
3937
3945
  "Infrastructure PR/synth/plan from the approved infra repository.",
3938
3946
  "Container build smoke and immutable image digest.",
3939
3947
  "ECS task definitions using secrets.valueFrom only.",
3940
- "ALB/TLS/DNS/auth denial smokes.",
3948
+ "ALB/TLS/DNS/auth denial smokes and web alarm checks.",
3941
3949
  "RDS TLS, backups/PITR, scoped roles, and migration dry-run evidence.",
3942
3950
  "S3 bucket KMS, versioning, lifecycle, and public-access-block evidence.",
3943
3951
  "Spark01 private-probe registration, key-file mode, heartbeat, and revocation evidence."
@@ -4009,7 +4017,7 @@ function buildSpark01CloudConfig(options = {}) {
4009
4017
  privateKeyInline: false,
4010
4018
  tokenInline: false,
4011
4019
  notes: [
4012
- "This config is cloud-primary: Spark01 submits to hosted API state instead of local SQLite.",
4020
+ "This config is hosted-targeted preflight: Spark01 must not start until cloud probe routes are backed by hosted state.",
4013
4021
  "The private key file path is referenced, not embedded.",
4014
4022
  "Hosted token or probe auth material must come from the machine secret store, not this generated config."
4015
4023
  ]
@@ -4030,7 +4038,8 @@ function servicePlan(prefix, stage, role, desiredCount, image, workspaceId, secr
4030
4038
  return {
4031
4039
  name,
4032
4040
  role,
4033
- desiredCount,
4041
+ desiredCount: 0,
4042
+ targetDesiredCount: desiredCount,
4034
4043
  taskRole: `${name}-task-role`,
4035
4044
  executionRole: `${prefix}-${stage}-execution-role`,
4036
4045
  logGroup: `/ecs/${name}`,
@@ -4039,7 +4048,7 @@ function servicePlan(prefix, stage, role, desiredCount, image, workspaceId, secr
4039
4048
  HASNA_UPTIME_IMAGE: image,
4040
4049
  ...environment
4041
4050
  },
4042
- secrets: role === "public-probe" ? { DATABASE_URL: secrets.database, PROBE_CONFIG: secrets.publicProbe } : role === "reporter" ? { DATABASE_URL: secrets.database, REPORTING_CONFIG: secrets.reporting } : { DATABASE_URL: secrets.database, APP_ENV: secrets.appEnv }
4051
+ secrets: role === "web" ? { HASNA_UPTIME_DATABASE_URL: secrets.database, APP_ENV: secrets.appEnv, HASNA_UPTIME_HOSTED_TOKEN: secrets.hostedToken } : role === "public-probe" ? { PROBE_CONFIG: secrets.publicProbe } : role === "reporter" ? { HASNA_UPTIME_DATABASE_URL: secrets.database, REPORTING_CONFIG: secrets.reporting } : { HASNA_UPTIME_DATABASE_URL: secrets.database, APP_ENV: secrets.appEnv }
4043
4052
  };
4044
4053
  }
4045
4054
  function clean(value, fallback) {
@@ -27,6 +27,10 @@ The generated AWS plan currently returns `status: "blocked"` and
27
27
  `canStart: false`. Treat both as review/preflight artifacts until the blockers
28
28
  and required evidence in the JSON output are resolved.
29
29
 
30
+ The app repo includes a hosted runtime `Dockerfile` and Terraform/OpenTofu
31
+ starter files in `infra/aws`. The plan output points to these files and keeps
32
+ `applyAllowed: false`.
33
+
30
34
  `uptime cloud spark01-config --env` requires a real `--probe-id`; it will not
31
35
  write a sourceable env file with a placeholder probe identity.
32
36
 
@@ -57,13 +61,23 @@ The plan expects:
57
61
  - S3 bucket for redacted browser evidence and generated report artifacts.
58
62
  - Secrets Manager or SSM refs for database, app env, probe config, and
59
63
  reporting channel refs.
60
- - CloudWatch log groups and alarms for web 5xx, scheduler stalls, stale probes,
61
- and report delivery failures.
64
+ - CloudWatch log groups for every component plus initial web 5xx/unhealthy
65
+ alarms. Scheduler-stall, stale-probe, and report-delivery alarms remain
66
+ blocked until those workers emit cloud metrics.
62
67
 
63
68
  Provision these through the approved infrastructure repository and reviewed
64
69
  plan/apply flow. The local `uptime cloud plan` output intentionally avoids
65
70
  copy-pastable AWS mutation commands.
66
71
 
72
+ Plan the included Terraform/OpenTofu starter without a backend:
73
+
74
+ ```bash
75
+ terraform -chdir=infra/aws fmt -check
76
+ terraform -chdir=infra/aws init -backend=false
77
+ terraform -chdir=infra/aws validate
78
+ terraform -chdir=infra/aws plan -out open-uptime.tfplan
79
+ ```
80
+
67
81
  ## Spark01
68
82
 
69
83
  Spark01 should be a private probe/operator machine, not the hosted source of
@@ -77,8 +91,9 @@ routes are backed by cloud check jobs and cloud audit rows.
77
91
  ## Safety Rules
78
92
 
79
93
  - Do not deploy hosted mode with `HASNA_UPTIME_ALLOW_HOSTED_LOCAL_STORE=1`.
80
- - Do not inline AWS keys, hosted tokens, Mailery keys, Open Logs tokens, or
81
- probe private keys in task definitions.
94
+ - Do not inline AWS keys, hosted tokens, Mailery keys, Open Logs tokens, database
95
+ URLs, or probe private keys in task definitions. Use ECS `secrets.valueFrom`
96
+ refs such as `HASNA_UPTIME_DATABASE_URL` and `HASNA_UPTIME_HOSTED_TOKEN`.
82
97
  - Do not run public probe workers against private targets.
83
98
  - Do not expose dashboard/API routes without hosted auth and workspace checks.
84
99
  - Do not treat local SQLite, local project DBs, or Spark01 local state as cloud
@@ -0,0 +1,25 @@
1
+ # This file is maintained automatically by "terraform init".
2
+ # Manual edits may be lost in future updates.
3
+
4
+ provider "registry.terraform.io/hashicorp/aws" {
5
+ version = "5.100.0"
6
+ constraints = "~> 5.0"
7
+ hashes = [
8
+ "h1:wOhTPz6apLBuF7/FYZuCoXRK/MLgrNprZ3vXmq83g5k=",
9
+ "zh:054b8dd49f0549c9a7cc27d159e45327b7b65cf404da5e5a20da154b90b8a644",
10
+ "zh:0b97bf8d5e03d15d83cc40b0530a1f84b459354939ba6f135a0086c20ebbe6b2",
11
+ "zh:1589a2266af699cbd5d80737a0fe02e54ec9cf2ca54e7e00ac51c7359056f274",
12
+ "zh:6330766f1d85f01ae6ea90d1b214b8b74cc8c1badc4696b165b36ddd4cc15f7b",
13
+ "zh:7c8c2e30d8e55291b86fcb64bdf6c25489d538688545eb48fd74ad622e5d3862",
14
+ "zh:99b1003bd9bd32ee323544da897148f46a527f622dc3971af63ea3e251596342",
15
+ "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425",
16
+ "zh:9f8b909d3ec50ade83c8062290378b1ec553edef6a447c56dadc01a99f4eaa93",
17
+ "zh:aaef921ff9aabaf8b1869a86d692ebd24fbd4e12c21205034bb679b9caf883a2",
18
+ "zh:ac882313207aba00dd5a76dbd572a0ddc818bb9cbf5c9d61b28fe30efaec951e",
19
+ "zh:bb64e8aff37becab373a1a0cc1080990785304141af42ed6aa3dd4913b000421",
20
+ "zh:dfe495f6621df5540d9c92ad40b8067376350b005c637ea6efac5dc15028add4",
21
+ "zh:f0ddf0eaf052766cfe09dea8200a946519f653c384ab4336e2a4a64fdd6310e9",
22
+ "zh:f1b7e684f4c7ae1eed272b6de7d2049bb87a0275cb04dbb7cda6636f600699c9",
23
+ "zh:ff461571e3f233699bf690db319dfe46aec75e58726636a0d97dd9ac6e32fb70",
24
+ ]
25
+ }
@@ -0,0 +1,32 @@
1
+ # Open Uptime AWS Infra
2
+
3
+ This directory is a reviewable Terraform/OpenTofu starting point for deploying
4
+ Open Uptime in the `hasna-xyz-infra` AWS account. It is intentionally
5
+ plan-first. Do not apply it directly from this app repo unless the infrastructure
6
+ owner has approved this directory as the source of truth or has copied it into
7
+ the approved infra repository.
8
+
9
+ ## Expected Flow
10
+
11
+ ```bash
12
+ terraform -chdir=infra/aws fmt -check
13
+ terraform -chdir=infra/aws init -backend=false
14
+ terraform -chdir=infra/aws validate
15
+ terraform -chdir=infra/aws plan -out open-uptime.tfplan
16
+ ```
17
+
18
+ Required inputs are declared in `variables.tf` and illustrated in
19
+ `terraform.tfvars.example`. Secrets are passed as Secrets Manager/SSM ARNs only;
20
+ never place plaintext tokens, database URLs, private keys, or channel
21
+ credentials in `.tfvars` files.
22
+
23
+ ## Current Blockers
24
+
25
+ - Hosted Postgres adapter and migrations are not implemented in the app yet.
26
+ - Hosted production auth/RBAC still needs scoped, revocable credentials.
27
+ - Public probe runtime still needs execution-time DNS/redirect/rebinding SSRF
28
+ enforcement.
29
+ - Spark01 hosted private-probe enrollment/heartbeat/revocation is still
30
+ fail-closed.
31
+
32
+ Keep `desired_count` at `0` or plan-only until those blockers are closed.