@hasna/uptime 0.1.5 → 0.1.7

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,13 @@
1
+ .git
2
+ .gitignore
3
+ .project.json
4
+ node_modules
5
+ coverage
6
+ *.tgz
7
+ *.log
8
+ .env
9
+ .env.*
10
+ .hasna
11
+ infra/**/.terraform
12
+ infra/**/*.tfstate
13
+ infra/**/*.tfstate.*
package/CHANGELOG.md CHANGED
@@ -6,15 +6,53 @@ project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.1.7] - 2026-06-28
10
+
11
+ ### Added
12
+
13
+ - Explicit hosted EFS-backed SQLite runtime path with
14
+ `HASNA_UPTIME_HOSTED_SQLITE_DB` and `hosted-efs-sqlite` health metadata.
15
+ - AWS Terraform EFS file system, access point, ECS volume mount, and AWS Backup
16
+ plan for the hosted SQLite data store.
17
+ - `Dockerfile.package` plus AWS CodeBuild image-builder Terraform resources to
18
+ build the published npm package into ECR without relying on local Docker.
19
+
20
+ ### Changed
21
+
22
+ - Hosted AWS deployment artifacts no longer inject `HASNA_UPTIME_DATABASE_URL`;
23
+ the async Postgres adapter remains future work.
24
+ - The EFS-backed SQLite bridge is single-writer only: one web task maximum and
25
+ scheduler/public-probe/reporter services remain disabled until Postgres and
26
+ cloud leases exist.
27
+
28
+ ## [0.1.6] - 2026-06-28
29
+
30
+ ### Added
31
+
32
+ - Bun-based hosted runtime `Dockerfile` and `.dockerignore`.
33
+ - Reviewable Terraform/OpenTofu AWS starter plan under `infra/aws` for ECR,
34
+ S3 evidence storage, ECS/Fargate services, ALB/TLS/DNS, task roles,
35
+ CloudWatch logs, security groups, and secret refs.
36
+ - Cloud plan SDK/CLI fields that point to `Dockerfile` and `infra/aws` with
37
+ format/init/validate/plan commands while keeping apply disabled.
38
+
39
+ ### Security
40
+
41
+ - AWS infra templates use secret ARNs/valueFrom references and example
42
+ placeholders only; no plaintext service tokens, database URLs, or private keys
43
+ are stored in the repo.
44
+ - Terraform desired counts default to zero until hosted cloud-store/auth/probe
45
+ blockers are closed.
46
+
9
47
  ## [0.1.5] - 2026-06-28
10
48
 
11
49
  ### Added
12
50
 
13
- - Dry-run AWS deployment plan generator for the `hasna-xyz-infra` target,
51
+ - Dry-run AWS deployment plan generator for a reviewed AWS target,
14
52
  covering ECS/Fargate services, ECR image commands, ALB/RDS/S3/Secrets/Logs
15
53
  resources, rollback steps, and safety assertions.
16
- - Spark01 cloud-primary private probe config generator with JSON and env-file
17
- rendering.
54
+ - Spark01 hosted-targeted private probe preflight config generator with JSON and
55
+ env-file rendering.
18
56
  - CLI commands `uptime cloud plan` and `uptime cloud spark01-config`.
19
57
  - SDK export `@hasna/uptime/cloud-plan`.
20
58
  - Machine-readable `blocked`/`canApply:false` and `blocked`/`canStart:false`
package/Dockerfile ADDED
@@ -0,0 +1,31 @@
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 --gid 10001 uptime \
19
+ && adduser --system --uid 10001 --ingroup uptime uptime
20
+
21
+ COPY --from=build /app/package.json ./package.json
22
+ COPY --from=build /app/node_modules ./node_modules
23
+ COPY --from=build /app/dist ./dist
24
+
25
+ USER uptime
26
+ EXPOSE 3899
27
+
28
+ HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
29
+ CMD bun -e "const r = await fetch('http://127.0.0.1:3899/health'); process.exit(r.ok ? 0 : 1)"
30
+
31
+ CMD ["bun", "dist/cli/index.js", "serve", "--mode", "hosted", "--host", "0.0.0.0", "--port", "3899"]
@@ -0,0 +1,22 @@
1
+ # syntax=docker/dockerfile:1
2
+
3
+ FROM oven/bun:1.3.13-slim AS runtime
4
+ ENV NODE_ENV=production \
5
+ HASNA_UPTIME_MODE=hosted
6
+ WORKDIR /app
7
+
8
+ RUN addgroup --system --gid 10001 uptime \
9
+ && adduser --system --uid 10001 --ingroup uptime uptime
10
+
11
+ COPY package.json ./package.json
12
+ COPY dist ./dist
13
+
14
+ RUN bun install --production
15
+
16
+ USER uptime
17
+ EXPOSE 3899
18
+
19
+ HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
20
+ CMD bun -e "const r = await fetch('http://127.0.0.1:3899/health'); process.exit(r.ok ? 0 : 1)"
21
+
22
+ CMD ["bun", "dist/cli/index.js", "serve", "--mode", "hosted", "--host", "0.0.0.0", "--port", "3899"]
package/README.md CHANGED
@@ -46,6 +46,16 @@ 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`. Hosted AWS
52
+ runtime state currently uses explicit EFS-backed SQLite via
53
+ `HASNA_UPTIME_HOSTED_SQLITE_DB=/data/uptime/uptime.db` for one protected web
54
+ task maximum; do not set `HASNA_UPTIME_DATABASE_URL` until the async Postgres
55
+ adapter is implemented.
56
+ `Dockerfile.package` is used by the Terraform CodeBuild image builder to build
57
+ the published npm package into ECR from inside AWS.
58
+
49
59
  Private/local probes can submit signed results from another machine:
50
60
 
51
61
  ```bash
package/dist/api.js CHANGED
@@ -820,10 +820,12 @@ function ensureUptimeHome() {
820
820
  }
821
821
 
822
822
  // src/store.ts
823
- import { copyFileSync, existsSync, mkdirSync as mkdirSync2, statSync } from "fs";
823
+ import { copyFileSync, existsSync, mkdirSync as mkdirSync2, statfsSync, statSync } from "fs";
824
824
  import { dirname, join as join2 } from "path";
825
825
  import { randomUUID as randomUUID2 } from "crypto";
826
826
  import { Database } from "bun:sqlite";
827
+ var DEFAULT_HOSTED_SQLITE_DB_PATH = "/data/uptime/uptime.db";
828
+ var NFS_SUPER_MAGIC = 26985;
827
829
  var SECRET_URL_PARAM_PATTERN = /(token|secret|password|passwd|api[_-]?key|access[_-]?token|auth|credential|session)/i;
828
830
  var REQUIRED_TABLES = [
829
831
  "schema_migrations",
@@ -860,18 +862,39 @@ class UptimeStore {
860
862
  this.mode = resolveRuntimeMode(options.mode ?? "local");
861
863
  const cloudDatabaseUrl = options.cloudDatabaseUrl ?? process.env.HASNA_UPTIME_DATABASE_URL;
862
864
  if (this.mode === "hosted" && cloudDatabaseUrl) {
863
- throw new Error("hosted cloud database adapter is not implemented yet");
865
+ throw new Error("hosted Postgres adapter is not implemented yet; use HASNA_UPTIME_HOSTED_SQLITE_DB on cloud-mounted storage for the current hosted deployment path");
864
866
  }
865
- if (this.mode === "hosted" && !allowHostedLocalStore(options.allowHostedLocalStore)) {
866
- throw new Error("hosted mode requires a cloud data layer; set HASNA_UPTIME_ALLOW_HOSTED_LOCAL_STORE=1 only for explicit local fallback testing");
867
+ const hostedSqliteDbPath = options.hostedSqliteDbPath ?? process.env.HASNA_UPTIME_HOSTED_SQLITE_DB;
868
+ if (this.mode === "hosted" && hostedSqliteDbPath) {
869
+ if (hostedSqliteDbPath === ":memory:" || !hostedSqliteDbPath.startsWith("/")) {
870
+ throw new Error("HASNA_UPTIME_HOSTED_SQLITE_DB must be an absolute path on mounted cloud storage");
871
+ }
872
+ const approvedHostedPath = hostedSqliteDbPath === DEFAULT_HOSTED_SQLITE_DB_PATH;
873
+ if (!approvedHostedPath && !allowHostedLocalStore(options.allowHostedLocalStore)) {
874
+ throw new Error(`HASNA_UPTIME_HOSTED_SQLITE_DB must be ${DEFAULT_HOSTED_SQLITE_DB_PATH}; set HASNA_UPTIME_ALLOW_HOSTED_LOCAL_STORE=1 only for explicit local fallback testing`);
875
+ }
876
+ const verifiedCloudMount = approvedHostedPath && isNfsMount(dirname(hostedSqliteDbPath));
877
+ if (approvedHostedPath && !verifiedCloudMount && !allowHostedLocalStore(options.allowHostedLocalStore)) {
878
+ throw new Error(`${DEFAULT_HOSTED_SQLITE_DB_PATH} must be on a mounted EFS/NFS filesystem; refusing to create hosted task-local SQLite`);
879
+ }
880
+ this.dataMode = verifiedCloudMount ? "hosted-efs-sqlite" : "hosted-local-sqlite";
881
+ this.dbPath = hostedSqliteDbPath;
882
+ } else if (this.mode === "hosted") {
883
+ if (!allowHostedLocalStore(options.allowHostedLocalStore)) {
884
+ throw new Error("hosted mode requires HASNA_UPTIME_HOSTED_SQLITE_DB on mounted cloud storage; set HASNA_UPTIME_ALLOW_HOSTED_LOCAL_STORE=1 only for explicit local fallback testing");
885
+ }
886
+ this.dataMode = "hosted-local-sqlite";
887
+ this.dbPath = options.dbPath ?? uptimeHostedFallbackDbPath();
888
+ } else {
889
+ this.dataMode = "local-sqlite";
890
+ this.dbPath = options.dbPath ?? uptimeDbPath();
867
891
  }
868
- this.dataMode = this.mode === "hosted" ? "hosted-local-sqlite" : "local-sqlite";
869
- this.dbPath = options.dbPath ?? (this.mode === "hosted" ? uptimeHostedFallbackDbPath() : uptimeDbPath());
870
- if (this.dbPath !== ":memory:") {
892
+ if (this.dbPath !== ":memory:" && this.dataMode !== "hosted-efs-sqlite") {
871
893
  mkdirSync2(dirname(this.dbPath), { recursive: true });
872
894
  }
873
895
  this.db = new Database(this.dbPath, { create: true });
874
- this.db.run("PRAGMA journal_mode = WAL");
896
+ this.db.run(this.dataMode === "hosted-efs-sqlite" ? "PRAGMA journal_mode = DELETE" : "PRAGMA journal_mode = WAL");
897
+ this.db.run("PRAGMA busy_timeout = 5000");
875
898
  this.db.run("PRAGMA foreign_keys = ON");
876
899
  this.migrate();
877
900
  }
@@ -1751,6 +1774,13 @@ function resolveRuntimeMode(mode) {
1751
1774
  function allowHostedLocalStore(value) {
1752
1775
  return value === true || process.env.HASNA_UPTIME_ALLOW_HOSTED_LOCAL_STORE === "1";
1753
1776
  }
1777
+ function isNfsMount(path) {
1778
+ try {
1779
+ return statfsSync(path).type === NFS_SUPER_MAGIC;
1780
+ } catch {
1781
+ return false;
1782
+ }
1783
+ }
1754
1784
  function verifyBackupFile(backupPath) {
1755
1785
  const db = new Database(backupPath, { readonly: true });
1756
1786
  try {
package/dist/cli/index.js CHANGED
@@ -3381,7 +3381,7 @@ function stableJson(value) {
3381
3381
  }
3382
3382
 
3383
3383
  // src/store.ts
3384
- import { copyFileSync, existsSync, mkdirSync as mkdirSync2, statSync } from "fs";
3384
+ import { copyFileSync, existsSync, mkdirSync as mkdirSync2, statfsSync, statSync } from "fs";
3385
3385
  import { dirname, join as join2 } from "path";
3386
3386
  import { randomUUID as randomUUID2 } from "crypto";
3387
3387
  import { Database } from "bun:sqlite";
@@ -3406,6 +3406,8 @@ function ensureUptimeHome() {
3406
3406
  }
3407
3407
 
3408
3408
  // src/store.ts
3409
+ var DEFAULT_HOSTED_SQLITE_DB_PATH = "/data/uptime/uptime.db";
3410
+ var NFS_SUPER_MAGIC = 26985;
3409
3411
  var SECRET_URL_PARAM_PATTERN = /(token|secret|password|passwd|api[_-]?key|access[_-]?token|auth|credential|session)/i;
3410
3412
  var REQUIRED_TABLES = [
3411
3413
  "schema_migrations",
@@ -3442,18 +3444,39 @@ class UptimeStore {
3442
3444
  this.mode = resolveRuntimeMode(options.mode ?? "local");
3443
3445
  const cloudDatabaseUrl = options.cloudDatabaseUrl ?? process.env.HASNA_UPTIME_DATABASE_URL;
3444
3446
  if (this.mode === "hosted" && cloudDatabaseUrl) {
3445
- throw new Error("hosted cloud database adapter is not implemented yet");
3447
+ throw new Error("hosted Postgres adapter is not implemented yet; use HASNA_UPTIME_HOSTED_SQLITE_DB on cloud-mounted storage for the current hosted deployment path");
3446
3448
  }
3447
- if (this.mode === "hosted" && !allowHostedLocalStore(options.allowHostedLocalStore)) {
3448
- throw new Error("hosted mode requires a cloud data layer; set HASNA_UPTIME_ALLOW_HOSTED_LOCAL_STORE=1 only for explicit local fallback testing");
3449
+ const hostedSqliteDbPath = options.hostedSqliteDbPath ?? process.env.HASNA_UPTIME_HOSTED_SQLITE_DB;
3450
+ if (this.mode === "hosted" && hostedSqliteDbPath) {
3451
+ if (hostedSqliteDbPath === ":memory:" || !hostedSqliteDbPath.startsWith("/")) {
3452
+ throw new Error("HASNA_UPTIME_HOSTED_SQLITE_DB must be an absolute path on mounted cloud storage");
3453
+ }
3454
+ const approvedHostedPath = hostedSqliteDbPath === DEFAULT_HOSTED_SQLITE_DB_PATH;
3455
+ if (!approvedHostedPath && !allowHostedLocalStore(options.allowHostedLocalStore)) {
3456
+ throw new Error(`HASNA_UPTIME_HOSTED_SQLITE_DB must be ${DEFAULT_HOSTED_SQLITE_DB_PATH}; set HASNA_UPTIME_ALLOW_HOSTED_LOCAL_STORE=1 only for explicit local fallback testing`);
3457
+ }
3458
+ const verifiedCloudMount = approvedHostedPath && isNfsMount(dirname(hostedSqliteDbPath));
3459
+ if (approvedHostedPath && !verifiedCloudMount && !allowHostedLocalStore(options.allowHostedLocalStore)) {
3460
+ throw new Error(`${DEFAULT_HOSTED_SQLITE_DB_PATH} must be on a mounted EFS/NFS filesystem; refusing to create hosted task-local SQLite`);
3461
+ }
3462
+ this.dataMode = verifiedCloudMount ? "hosted-efs-sqlite" : "hosted-local-sqlite";
3463
+ this.dbPath = hostedSqliteDbPath;
3464
+ } else if (this.mode === "hosted") {
3465
+ if (!allowHostedLocalStore(options.allowHostedLocalStore)) {
3466
+ throw new Error("hosted mode requires HASNA_UPTIME_HOSTED_SQLITE_DB on mounted cloud storage; set HASNA_UPTIME_ALLOW_HOSTED_LOCAL_STORE=1 only for explicit local fallback testing");
3467
+ }
3468
+ this.dataMode = "hosted-local-sqlite";
3469
+ this.dbPath = options.dbPath ?? uptimeHostedFallbackDbPath();
3470
+ } else {
3471
+ this.dataMode = "local-sqlite";
3472
+ this.dbPath = options.dbPath ?? uptimeDbPath();
3449
3473
  }
3450
- this.dataMode = this.mode === "hosted" ? "hosted-local-sqlite" : "local-sqlite";
3451
- this.dbPath = options.dbPath ?? (this.mode === "hosted" ? uptimeHostedFallbackDbPath() : uptimeDbPath());
3452
- if (this.dbPath !== ":memory:") {
3474
+ if (this.dbPath !== ":memory:" && this.dataMode !== "hosted-efs-sqlite") {
3453
3475
  mkdirSync2(dirname(this.dbPath), { recursive: true });
3454
3476
  }
3455
3477
  this.db = new Database(this.dbPath, { create: true });
3456
- this.db.run("PRAGMA journal_mode = WAL");
3478
+ this.db.run(this.dataMode === "hosted-efs-sqlite" ? "PRAGMA journal_mode = DELETE" : "PRAGMA journal_mode = WAL");
3479
+ this.db.run("PRAGMA busy_timeout = 5000");
3457
3480
  this.db.run("PRAGMA foreign_keys = ON");
3458
3481
  this.migrate();
3459
3482
  }
@@ -4333,6 +4356,13 @@ function resolveRuntimeMode(mode) {
4333
4356
  function allowHostedLocalStore(value) {
4334
4357
  return value === true || process.env.HASNA_UPTIME_ALLOW_HOSTED_LOCAL_STORE === "1";
4335
4358
  }
4359
+ function isNfsMount(path) {
4360
+ try {
4361
+ return statfsSync(path).type === NFS_SUPER_MAGIC;
4362
+ } catch {
4363
+ return false;
4364
+ }
4365
+ }
4336
4366
  function verifyBackupFile(backupPath) {
4337
4367
  const db = new Database(backupPath, { readonly: true });
4338
4368
  try {
@@ -6388,14 +6418,14 @@ class ApiError extends Error {
6388
6418
  }
6389
6419
 
6390
6420
  // src/cloud-plan.ts
6391
- var DEFAULT_ACCOUNT = "hasna-xyz-infra";
6421
+ var DEFAULT_ACCOUNT = "aws-profile";
6392
6422
  var DEFAULT_REGION = "us-east-1";
6393
6423
  var DEFAULT_STAGE = "prod";
6394
6424
  var DEFAULT_PREFIX = "open-uptime";
6395
- var DEFAULT_HOSTNAME = "uptime.hasna.xyz";
6396
- var DEFAULT_WORKSPACE_ID = "wks_2tyysw05cwap";
6397
- var DEFAULT_VPC_ID = "vpc-04c7f7abc1d3c3f56";
6398
- var DEFAULT_RDS = "hasna-xyz-infra-apps-prod-postgres";
6425
+ var DEFAULT_HOSTNAME = "uptime.example.com";
6426
+ var DEFAULT_WORKSPACE_ID = "workspace-id";
6427
+ var DEFAULT_VPC_ID = "vpc-xxxxxxxx";
6428
+ var DEFAULT_HOSTED_SQLITE_DB = "/data/uptime/uptime.db";
6399
6429
  function buildAwsDeploymentPlan(options = {}) {
6400
6430
  const region = clean(options.region, DEFAULT_REGION);
6401
6431
  const stage = clean(options.stage, DEFAULT_STAGE);
@@ -6403,36 +6433,39 @@ function buildAwsDeploymentPlan(options = {}) {
6403
6433
  const accountName = clean(options.accountName, DEFAULT_ACCOUNT);
6404
6434
  const hostname = clean(options.hostname, DEFAULT_HOSTNAME);
6405
6435
  const workspaceId = clean(options.workspaceId, DEFAULT_WORKSPACE_ID);
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>`);
6436
+ const ecrRepository = clean(options.ecrRepository, prefix);
6437
+ const imageRepositoryUri = `<account-id>.dkr.ecr.${region}.amazonaws.com/${ecrRepository}`;
6438
+ const image = clean(options.image, `${imageRepositoryUri}@sha256:<image-digest>`);
6408
6439
  const evidenceBucket = clean(options.evidenceBucket, `hasna-${stage}-${prefix}-evidence`);
6440
+ const hostedSqliteDbPath = clean(options.hostedSqliteDbPath, DEFAULT_HOSTED_SQLITE_DB);
6441
+ const runtimePackageVersion = clean(options.runtimePackageVersion, "0.1.7");
6409
6442
  const cluster = `${prefix}-${stage}`;
6410
6443
  const secrets = {
6411
- database: clean(options.databaseSecretName, `hasna/xyz/opensource/uptime/${stage}/rds`),
6412
- appEnv: clean(options.appEnvSecretName, `hasna/xyz/opensource/uptime/${stage}/app/env`),
6413
- hostedToken: clean(options.hostedTokenSecretName, `hasna/xyz/opensource/uptime/${stage}/hosted-token`),
6414
- publicProbe: clean(options.publicProbeSecretName, `hasna/xyz/opensource/uptime/${stage}/probe/public`),
6415
- privateProbe: clean(options.privateProbeSecretName, `hasna/xyz/opensource/uptime/${stage}/probe/private`),
6416
- reporting: clean(options.reportingSecretName, `hasna/xyz/opensource/uptime/${stage}/reporting`)
6444
+ appEnv: clean(options.appEnvSecretName, `open-uptime/${stage}/app/env`),
6445
+ hostedToken: clean(options.hostedTokenSecretName, `open-uptime/${stage}/hosted-token`),
6446
+ publicProbe: clean(options.publicProbeSecretName, `open-uptime/${stage}/probe/public`),
6447
+ privateProbe: clean(options.privateProbeSecretName, `open-uptime/${stage}/probe/private`),
6448
+ reporting: clean(options.reportingSecretName, `open-uptime/${stage}/reporting`)
6417
6449
  };
6418
6450
  const services = [
6419
- servicePlan(prefix, stage, "web", 2, image, workspaceId, secrets, {
6451
+ servicePlan(prefix, stage, "web", 1, image, workspaceId, secrets, {
6420
6452
  HASNA_UPTIME_MODE: "hosted",
6453
+ HASNA_UPTIME_HOSTED_SQLITE_DB: hostedSqliteDbPath,
6421
6454
  HASNA_UPTIME_WORKSPACE_ID: workspaceId,
6422
6455
  HASNA_UPTIME_HOSTNAME: hostname
6423
6456
  }),
6424
- servicePlan(prefix, stage, "scheduler", 1, image, workspaceId, secrets, {
6457
+ servicePlan(prefix, stage, "scheduler", 0, image, workspaceId, secrets, {
6425
6458
  HASNA_UPTIME_MODE: "hosted",
6426
6459
  HASNA_UPTIME_WORKSPACE_ID: workspaceId,
6427
6460
  HASNA_UPTIME_COMPONENT: "scheduler"
6428
6461
  }),
6429
- servicePlan(prefix, stage, "public-probe", 1, image, workspaceId, secrets, {
6462
+ servicePlan(prefix, stage, "public-probe", 0, image, workspaceId, secrets, {
6430
6463
  HASNA_UPTIME_MODE: "hosted",
6431
6464
  HASNA_UPTIME_WORKSPACE_ID: workspaceId,
6432
6465
  HASNA_UPTIME_COMPONENT: "public-probe",
6433
6466
  HASNA_UPTIME_PROBE_LOCATION: region
6434
6467
  }),
6435
- servicePlan(prefix, stage, "reporter", 1, image, workspaceId, secrets, {
6468
+ servicePlan(prefix, stage, "reporter", 0, image, workspaceId, secrets, {
6436
6469
  HASNA_UPTIME_MODE: "hosted",
6437
6470
  HASNA_UPTIME_WORKSPACE_ID: workspaceId,
6438
6471
  HASNA_UPTIME_COMPONENT: "reporter"
@@ -6445,7 +6478,7 @@ function buildAwsDeploymentPlan(options = {}) {
6445
6478
  ];
6446
6479
  return {
6447
6480
  kind: "open-uptime.aws-deployment-plan",
6448
- version: 1,
6481
+ version: 2,
6449
6482
  generatedAt: new Date().toISOString(),
6450
6483
  status: "blocked",
6451
6484
  canApply: false,
@@ -6458,10 +6491,13 @@ function buildAwsDeploymentPlan(options = {}) {
6458
6491
  mode: "hosted",
6459
6492
  resources: {
6460
6493
  ecrRepository,
6494
+ imageBuilder: `${prefix}-${stage}-image-builder`,
6461
6495
  ecsCluster: cluster,
6462
6496
  services,
6463
6497
  vpcId: clean(options.vpcId, DEFAULT_VPC_ID),
6464
- rdsInstanceId: clean(options.rdsInstanceId, DEFAULT_RDS),
6498
+ efsFileSystem: `${prefix}-${stage}-data`,
6499
+ efsAccessPoint: `${prefix}-${stage}-uptime`,
6500
+ hostedSqliteDbPath,
6465
6501
  evidenceBucket,
6466
6502
  loadBalancer: `${prefix}-${stage}-alb`,
6467
6503
  targetGroups: [`${prefix}-${stage}-web-tg`],
@@ -6470,42 +6506,54 @@ function buildAwsDeploymentPlan(options = {}) {
6470
6506
  `${prefix}-${stage}-web-sg`,
6471
6507
  `${prefix}-${stage}-scheduler-sg`,
6472
6508
  `${prefix}-${stage}-public-probe-sg`,
6473
- `${prefix}-${stage}-rds-client-sg`
6509
+ `${prefix}-${stage}-reporter-sg`,
6510
+ `${prefix}-${stage}-migration-sg`,
6511
+ `${prefix}-${stage}-efs-sg`
6474
6512
  ],
6475
6513
  secrets,
6476
6514
  logGroups: services.map((service) => service.logGroup),
6477
6515
  alarms: [
6478
6516
  `${prefix}-${stage}-web-5xx`,
6479
- `${prefix}-${stage}-scheduler-stalled`,
6480
- `${prefix}-${stage}-probe-stale`,
6481
- `${prefix}-${stage}-report-delivery-failures`
6517
+ `${prefix}-${stage}-web-unhealthy`
6482
6518
  ]
6483
6519
  },
6484
6520
  image: {
6485
6521
  repository: ecrRepository,
6486
6522
  uri: image,
6487
- buildCommand: "BLOCKED: add a reviewed Dockerfile/container build target before running docker build",
6523
+ dockerfile: "Dockerfile.package",
6524
+ buildCommand: `BLOCKED: after infra approval, AWS CodeBuild builds Dockerfile.package from @hasna/uptime@${runtimePackageVersion} into ${imageRepositoryUri}`,
6488
6525
  pushCommands: [
6489
- "BLOCKED: push only from approved CI/CD after the ECR repository and image digest policy exist",
6526
+ `BLOCKED: start ${prefix}-${stage}-image-builder only through the approved deploy pipeline after @hasna/uptime@${runtimePackageVersion} is published`,
6490
6527
  "BLOCKED: deploy services by immutable image digest, not by mutable tags"
6491
6528
  ]
6492
6529
  },
6530
+ infra: {
6531
+ path: "infra/aws",
6532
+ fmtCommand: "terraform -chdir=infra/aws fmt -check",
6533
+ initCommand: "terraform -chdir=infra/aws init -backend=false",
6534
+ validateCommand: "terraform -chdir=infra/aws validate",
6535
+ planCommand: "terraform -chdir=infra/aws plan -out open-uptime.tfplan",
6536
+ applyAllowed: false
6537
+ },
6493
6538
  runbook: {
6494
6539
  preflight: [
6495
6540
  `aws sts get-caller-identity --profile ${accountName}`,
6496
- `aws rds describe-db-instances --db-instance-identifier ${clean(options.rdsInstanceId, DEFAULT_RDS)} --region ${region}`,
6497
6541
  `aws ec2 describe-vpcs --vpc-ids ${clean(options.vpcId, DEFAULT_VPC_ID)} --region ${region}`,
6542
+ `aws efs describe-file-systems --region ${region}`,
6498
6543
  "Confirm the infra repository and Terraform/CloudFormation owner before live mutation."
6499
6544
  ],
6500
6545
  provision: [
6501
6546
  `Infra PR must declare or update ECR repository ${ecrRepository}.`,
6547
+ `Infra PR must declare CodeBuild image builder ${prefix}-${stage}-image-builder for @hasna/uptime@${runtimePackageVersion}.`,
6502
6548
  `Infra PR must declare hardened S3 evidence bucket ${evidenceBucket} with KMS, versioning, lifecycle, and public access block.`,
6549
+ `Infra PR must declare encrypted EFS ${prefix}-${stage}-data with access point, mount targets, and AWS Backup plan.`,
6503
6550
  `Infra PR must declare ECS/Fargate cluster ${cluster}, ALB, target groups, security groups, IAM roles, CloudWatch log groups, and Secrets Manager refs.`,
6504
6551
  "Only apply the infra plan from the approved infrastructure repository after review evidence is attached."
6505
6552
  ],
6506
6553
  deploy: [
6507
6554
  "Build and publish the image only after the Dockerfile/container target is reviewed.",
6508
- "Run the migration task with the migrator role before web/scheduler/probe services.",
6555
+ `Start the AWS image builder for @hasna/uptime@${runtimePackageVersion} and record the pushed image digest.`,
6556
+ "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.",
6509
6557
  `Register task definitions for ${services.map((service) => service.name).join(", ")} using valueFrom secrets.`,
6510
6558
  `Update ECS services in cluster ${cluster} one component at a time through the approved deploy pipeline.`,
6511
6559
  `Create Route53/edge record for ${hostname} only after ALB health checks pass and auth denial smokes succeed.`
@@ -6514,7 +6562,7 @@ function buildAwsDeploymentPlan(options = {}) {
6514
6562
  "Keep previous task definition ARNs before each service update.",
6515
6563
  "Rollback through the approved deploy pipeline to the previously recorded task definition ARNs.",
6516
6564
  "Disable scheduler/reporter services before data rollback.",
6517
- "Restore RDS snapshot only after explicit operator approval and audit record."
6565
+ "Restore EFS backup recovery point only after explicit operator approval and audit record."
6518
6566
  ],
6519
6567
  spark01: [
6520
6568
  "Create a private probe identity with a caller-managed public key.",
@@ -6523,19 +6571,19 @@ function buildAwsDeploymentPlan(options = {}) {
6523
6571
  ]
6524
6572
  },
6525
6573
  blockers: [
6526
- "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
- "Hosted Postgres storage adapter and migrations are not implemented.",
6574
+ "The infrastructure owner repository was not found in this workspace.",
6575
+ "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.",
6529
6576
  "Hosted production auth/RBAC must replace broad static hosted-token operation before exposure.",
6530
6577
  "Public probe execution still needs DNS, redirect, and rebinding SSRF enforcement plus cloud check-job leases.",
6531
6578
  "Spark01 hosted probe enrollment, claim, submit, heartbeat, revocation, and rotation are not cloud-backed yet."
6532
6579
  ],
6533
6580
  requiredEvidence: [
6534
6581
  "Infrastructure PR/synth/plan from the approved infra repository.",
6535
- "Container build smoke and immutable image digest.",
6582
+ "CodeBuild image-builder run, container smoke, and immutable image digest.",
6536
6583
  "ECS task definitions using secrets.valueFrom only.",
6537
- "ALB/TLS/DNS/auth denial smokes.",
6538
- "RDS TLS, backups/PITR, scoped roles, and migration dry-run evidence.",
6584
+ "ALB/TLS/DNS/auth denial smokes and web alarm checks.",
6585
+ "Single-writer ECS evidence: one web task maximum and no scheduler/public-probe/reporter EFS mounts.",
6586
+ "EFS encryption, access point, mount-target, AWS Backup, and restore-drill evidence.",
6539
6587
  "S3 bucket KMS, versioning, lifecycle, and public-access-block evidence.",
6540
6588
  "Spark01 private-probe registration, key-file mode, heartbeat, and revocation evidence."
6541
6589
  ],
@@ -6545,7 +6593,9 @@ function buildAwsDeploymentPlan(options = {}) {
6545
6593
  hostedLocalSqliteAllowed: false,
6546
6594
  notes: [
6547
6595
  "This plan generator does not call AWS.",
6548
- "Hosted runtime must use Postgres; SQLite remains local/dev fallback only.",
6596
+ "Blocked plan output intentionally avoids copy-pastable AWS mutation commands.",
6597
+ "Hosted runtime uses explicit EFS-backed SQLite at HASNA_UPTIME_HOSTED_SQLITE_DB until the async Postgres adapter exists.",
6598
+ "Do not set HASNA_UPTIME_DATABASE_URL for hosted tasks until the Postgres adapter is implemented.",
6549
6599
  "Secrets are represented as secret names/refs and must be injected with valueFrom.",
6550
6600
  "Actual deploy belongs in the deploy_release_operate_final goal node after infra review."
6551
6601
  ]
@@ -6606,7 +6656,7 @@ function buildSpark01CloudConfig(options = {}) {
6606
6656
  privateKeyInline: false,
6607
6657
  tokenInline: false,
6608
6658
  notes: [
6609
- "This config is cloud-primary: Spark01 submits to hosted API state instead of local SQLite.",
6659
+ "This config is hosted-targeted preflight: Spark01 must not start until cloud probe routes are backed by hosted state.",
6610
6660
  "The private key file path is referenced, not embedded.",
6611
6661
  "Hosted token or probe auth material must come from the machine secret store, not this generated config."
6612
6662
  ]
@@ -6627,7 +6677,8 @@ function servicePlan(prefix, stage, role, desiredCount, image, workspaceId, secr
6627
6677
  return {
6628
6678
  name,
6629
6679
  role,
6630
- desiredCount,
6680
+ desiredCount: 0,
6681
+ targetDesiredCount: desiredCount,
6631
6682
  taskRole: `${name}-task-role`,
6632
6683
  executionRole: `${prefix}-${stage}-execution-role`,
6633
6684
  logGroup: `/ecs/${name}`,
@@ -6636,7 +6687,7 @@ function servicePlan(prefix, stage, role, desiredCount, image, workspaceId, secr
6636
6687
  HASNA_UPTIME_IMAGE: image,
6637
6688
  ...environment
6638
6689
  },
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 }
6690
+ secrets: role === "web" ? { APP_ENV: secrets.appEnv, HASNA_UPTIME_HOSTED_TOKEN: secrets.hostedToken } : role === "public-probe" ? { PROBE_CONFIG: secrets.publicProbe } : role === "reporter" ? { REPORTING_CONFIG: secrets.reporting } : { APP_ENV: secrets.appEnv }
6640
6691
  };
6641
6692
  }
6642
6693
  function clean(value, fallback) {
@@ -6957,7 +7008,7 @@ program2.command("audit").description("List local audit events").option("--resou
6957
7008
  }
6958
7009
  });
6959
7010
  var cloud = program2.command("cloud").description("Generate dry-run cloud deployment and Spark01 configuration artifacts");
6960
- cloud.command("plan").description("Generate a dry-run AWS deployment plan for hasna-xyz-infra").option("--account <name>", "AWS account/profile label", "hasna-xyz-infra").option("--region <region>", "AWS region", "us-east-1").option("--stage <stage>", "deployment stage", "prod").option("--hostname <hostname>", "hosted Open Uptime hostname", "uptime.hasna.xyz").option("--workspace-id <id>", "workspace id", "wks_2tyysw05cwap").option("--vpc-id <id>", "target VPC id").option("--rds-instance-id <id>", "existing RDS instance id").option("--ecr-repository <name>", "ECR repository name").option("--image <uri>", "container image URI").option("--evidence-bucket <name>", "S3 evidence bucket name").option("-j, --json", "print JSON").action((opts) => {
7011
+ 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("--evidence-bucket <name>", "S3 evidence bucket name").option("-j, --json", "print JSON").action((opts) => {
6961
7012
  try {
6962
7013
  const plan = buildAwsDeploymentPlan({
6963
7014
  accountName: opts.account,
@@ -6966,9 +7017,12 @@ cloud.command("plan").description("Generate a dry-run AWS deployment plan for ha
6966
7017
  hostname: opts.hostname,
6967
7018
  workspaceId: opts.workspaceId,
6968
7019
  vpcId: opts.vpcId,
7020
+ hostedSqliteDbPath: opts.hostedSqliteDb,
6969
7021
  rdsInstanceId: opts.rdsInstanceId,
7022
+ databaseSecretName: opts.databaseSecretName,
6970
7023
  ecrRepository: opts.ecrRepository,
6971
7024
  image: opts.image,
7025
+ runtimePackageVersion: opts.runtimePackageVersion,
6972
7026
  evidenceBucket: opts.evidenceBucket
6973
7027
  });
6974
7028
  print(plan, renderCloudPlan(plan), opts);
@@ -6976,7 +7030,7 @@ cloud.command("plan").description("Generate a dry-run AWS deployment plan for ha
6976
7030
  fail(error);
6977
7031
  }
6978
7032
  });
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) => {
7033
+ cloud.command("spark01-config").description("Generate Spark01 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>", "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
7034
  try {
6981
7035
  const config = buildSpark01CloudConfig({
6982
7036
  apiUrl: opts.apiUrl,
@@ -7176,7 +7230,7 @@ program2.command("restore <backup-path>").description("Restore a verified local
7176
7230
  fail(error);
7177
7231
  }
7178
7232
  });
7179
- program2.command("serve").description("Serve the local API and dashboard").option("--host <host>", "host to bind", "127.0.0.1").option("--port <port>", "port", parseInteger, 3899).option("--check", "run the scheduler while serving").addOption(new Option("--mode <mode>", "runtime mode").choices(["local", "hosted"]).default("local")).option("--api-token <token>", "token required for non-loopback mutation hosts").option("--hosted-token <token>", "scoped hosted-mode token").option("--allow-hosted-local-store", "allow hosted mode to use local SQLite as an explicit fallback").option("--allow-unsafe-remote-mutations", "allow state-changing requests from non-loopback hosts without a token").option("-j, --json", "print JSON").action((opts) => {
7233
+ program2.command("serve").description("Serve the local API and dashboard").option("--host <host>", "host to bind", "127.0.0.1").option("--port <port>", "port", parseInteger, 3899).option("--check", "run the scheduler while serving").addOption(new Option("--mode <mode>", "runtime mode").choices(["local", "hosted"]).default("local")).option("--api-token <token>", "token required for non-loopback mutation hosts").option("--hosted-token <token>", "scoped hosted-mode token").option("--hosted-sqlite-db <path>", "absolute SQLite database path on hosted cloud-mounted storage").option("--allow-hosted-local-store", "allow hosted mode to use local SQLite as an explicit fallback").option("--allow-unsafe-remote-mutations", "allow state-changing requests from non-loopback hosts without a token").option("-j, --json", "print JSON").action((opts) => {
7180
7234
  try {
7181
7235
  const { server } = serveUptime({
7182
7236
  host: opts.host,
@@ -7185,6 +7239,7 @@ program2.command("serve").description("Serve the local API and dashboard").optio
7185
7239
  mode: opts.mode,
7186
7240
  apiToken: opts.apiToken,
7187
7241
  hostedToken: opts.hostedToken,
7242
+ hostedSqliteDbPath: opts.hostedSqliteDb,
7188
7243
  allowHostedLocalStore: opts.allowHostedLocalStore,
7189
7244
  allowUnsafeRemoteMutations: opts.allowUnsafeRemoteMutations
7190
7245
  });
@@ -7355,9 +7410,13 @@ function renderCloudPlan(plan) {
7355
7410
  `host: ${plan.hostname}`,
7356
7411
  `cluster: ${plan.resources.ecsCluster}`,
7357
7412
  `image: ${plan.image.uri}`,
7413
+ `image builder: ${plan.resources.imageBuilder}`,
7414
+ `dockerfile: ${plan.image.dockerfile}`,
7415
+ `infra: ${plan.infra.path}`,
7358
7416
  `vpc: ${plan.resources.vpcId}`,
7359
- `rds: ${plan.resources.rdsInstanceId}`,
7360
- `services: ${plan.resources.services.map((service2) => `${service2.name}:${service2.desiredCount}`).join(", ")}`,
7417
+ `efs: ${plan.resources.efsFileSystem}`,
7418
+ `hosted sqlite: ${plan.resources.hostedSqliteDbPath}`,
7419
+ `services: ${plan.resources.services.map((service2) => `${service2.name}:${service2.desiredCount}/${service2.targetDesiredCount}`).join(", ")}`,
7361
7420
  `evidence bucket: ${plan.resources.evidenceBucket}`,
7362
7421
  `blockers: ${plan.blockers.length}`,
7363
7422
  "live AWS mutation: false"