@hasna/uptime 0.1.4 → 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,46 @@ 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
+
28
+ ## [0.1.5] - 2026-06-28
29
+
30
+ ### Added
31
+
32
+ - Dry-run AWS deployment plan generator for the `hasna-xyz-infra` target,
33
+ covering ECS/Fargate services, ECR image commands, ALB/RDS/S3/Secrets/Logs
34
+ resources, rollback steps, and safety assertions.
35
+ - Spark01 hosted-targeted private probe preflight config generator with JSON and
36
+ env-file rendering.
37
+ - CLI commands `uptime cloud plan` and `uptime cloud spark01-config`.
38
+ - SDK export `@hasna/uptime/cloud-plan`.
39
+ - Machine-readable `blocked`/`canApply:false` and `blocked`/`canStart:false`
40
+ gates plus blocker/evidence lists for AWS and Spark01 planning artifacts.
41
+
42
+ ### Security
43
+
44
+ - Cloud planning artifacts contain secret names/refs and file paths only; they
45
+ do not inline AWS credentials, hosted tokens, or private probe key material.
46
+ - Cloud plan generation is dry-run only and does not call AWS.
47
+ - Dry-run AWS output avoids copy-pastable live AWS mutation commands.
48
+
9
49
  ## [0.1.4] - 2026-06-28
10
50
 
11
51
  ### Added
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
@@ -31,6 +31,8 @@ uptime report-schedules create ops --interval 3600 --email ops@example.com --fro
31
31
  uptime report-schedules run-due
32
32
  uptime report-schedules runs
33
33
  uptime audit
34
+ uptime cloud plan --json
35
+ uptime cloud spark01-config --probe-id prb_spark01 --env
34
36
  uptime incidents
35
37
  uptime serve --port 3899 --check
36
38
  ```
@@ -39,6 +41,15 @@ Scheduled reports persist endpoint and recipient configuration, but not send
39
41
  keys or API tokens. Configure `MAILERY_SEND_KEY`, `HASNA_MAILERY_SEND_KEY`,
40
42
  `HASNA_LOGS_API_TOKEN`, or the matching service env vars before scheduled runs.
41
43
 
44
+ The `uptime cloud ...` commands generate dry-run AWS/Spark01 planning artifacts
45
+ only. They do not call AWS, write secrets, or produce an approved deploy script;
46
+ current output is intentionally blocked until the infra and cloud-store evidence
47
+ in `docs/aws-deployment-runbook.md` is satisfied.
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
+
42
53
  Private/local probes can submit signed results from another machine:
43
54
 
44
55
  ```bash
package/dist/cli/index.js CHANGED
@@ -6387,6 +6387,277 @@ class ApiError extends Error {
6387
6387
  }
6388
6388
  }
6389
6389
 
6390
+ // src/cloud-plan.ts
6391
+ var DEFAULT_ACCOUNT = "hasna-xyz-infra";
6392
+ var DEFAULT_REGION = "us-east-1";
6393
+ var DEFAULT_STAGE = "prod";
6394
+ 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";
6399
+ function buildAwsDeploymentPlan(options = {}) {
6400
+ const region = clean(options.region, DEFAULT_REGION);
6401
+ const stage = clean(options.stage, DEFAULT_STAGE);
6402
+ const prefix = clean(options.servicePrefix, DEFAULT_PREFIX);
6403
+ const accountName = clean(options.accountName, DEFAULT_ACCOUNT);
6404
+ const hostname = clean(options.hostname, DEFAULT_HOSTNAME);
6405
+ const workspaceId = clean(options.workspaceId, DEFAULT_WORKSPACE_ID);
6406
+ const ecrRepository = clean(options.ecrRepository, `hasna/opensource/${prefix}`);
6407
+ const imageRepositoryUri = `<account-id>.dkr.ecr.${region}.amazonaws.com/${ecrRepository}`;
6408
+ const image = clean(options.image, `${imageRepositoryUri}@sha256:<image-digest>`);
6409
+ const evidenceBucket = clean(options.evidenceBucket, `hasna-${stage}-${prefix}-evidence`);
6410
+ const cluster = `${prefix}-${stage}`;
6411
+ const secrets = {
6412
+ database: clean(options.databaseSecretName, `hasna/xyz/opensource/uptime/${stage}/rds`),
6413
+ appEnv: clean(options.appEnvSecretName, `hasna/xyz/opensource/uptime/${stage}/app/env`),
6414
+ hostedToken: clean(options.hostedTokenSecretName, `hasna/xyz/opensource/uptime/${stage}/hosted-token`),
6415
+ publicProbe: clean(options.publicProbeSecretName, `hasna/xyz/opensource/uptime/${stage}/probe/public`),
6416
+ privateProbe: clean(options.privateProbeSecretName, `hasna/xyz/opensource/uptime/${stage}/probe/private`),
6417
+ reporting: clean(options.reportingSecretName, `hasna/xyz/opensource/uptime/${stage}/reporting`)
6418
+ };
6419
+ const services = [
6420
+ servicePlan(prefix, stage, "web", 2, image, workspaceId, secrets, {
6421
+ HASNA_UPTIME_MODE: "hosted",
6422
+ HASNA_UPTIME_WORKSPACE_ID: workspaceId,
6423
+ HASNA_UPTIME_HOSTNAME: hostname
6424
+ }),
6425
+ servicePlan(prefix, stage, "scheduler", 1, image, workspaceId, secrets, {
6426
+ HASNA_UPTIME_MODE: "hosted",
6427
+ HASNA_UPTIME_WORKSPACE_ID: workspaceId,
6428
+ HASNA_UPTIME_COMPONENT: "scheduler"
6429
+ }),
6430
+ servicePlan(prefix, stage, "public-probe", 1, image, workspaceId, secrets, {
6431
+ HASNA_UPTIME_MODE: "hosted",
6432
+ HASNA_UPTIME_WORKSPACE_ID: workspaceId,
6433
+ HASNA_UPTIME_COMPONENT: "public-probe",
6434
+ HASNA_UPTIME_PROBE_LOCATION: region
6435
+ }),
6436
+ servicePlan(prefix, stage, "reporter", 1, image, workspaceId, secrets, {
6437
+ HASNA_UPTIME_MODE: "hosted",
6438
+ HASNA_UPTIME_WORKSPACE_ID: workspaceId,
6439
+ HASNA_UPTIME_COMPONENT: "reporter"
6440
+ }),
6441
+ servicePlan(prefix, stage, "migration", 0, image, workspaceId, secrets, {
6442
+ HASNA_UPTIME_MODE: "hosted",
6443
+ HASNA_UPTIME_WORKSPACE_ID: workspaceId,
6444
+ HASNA_UPTIME_COMPONENT: "migration"
6445
+ })
6446
+ ];
6447
+ return {
6448
+ kind: "open-uptime.aws-deployment-plan",
6449
+ version: 1,
6450
+ generatedAt: new Date().toISOString(),
6451
+ status: "blocked",
6452
+ canApply: false,
6453
+ accountName,
6454
+ region,
6455
+ stage,
6456
+ servicePrefix: prefix,
6457
+ hostname,
6458
+ workspaceId,
6459
+ mode: "hosted",
6460
+ resources: {
6461
+ ecrRepository,
6462
+ ecsCluster: cluster,
6463
+ services,
6464
+ vpcId: clean(options.vpcId, DEFAULT_VPC_ID),
6465
+ rdsInstanceId: clean(options.rdsInstanceId, DEFAULT_RDS),
6466
+ evidenceBucket,
6467
+ loadBalancer: `${prefix}-${stage}-alb`,
6468
+ targetGroups: [`${prefix}-${stage}-web-tg`],
6469
+ securityGroups: [
6470
+ `${prefix}-${stage}-alb-sg`,
6471
+ `${prefix}-${stage}-web-sg`,
6472
+ `${prefix}-${stage}-scheduler-sg`,
6473
+ `${prefix}-${stage}-public-probe-sg`,
6474
+ `${prefix}-${stage}-reporter-sg`,
6475
+ `${prefix}-${stage}-migration-sg`
6476
+ ],
6477
+ secrets,
6478
+ logGroups: services.map((service) => service.logGroup),
6479
+ alarms: [
6480
+ `${prefix}-${stage}-web-5xx`,
6481
+ `${prefix}-${stage}-web-unhealthy`
6482
+ ]
6483
+ },
6484
+ image: {
6485
+ repository: ecrRepository,
6486
+ uri: image,
6487
+ dockerfile: "Dockerfile",
6488
+ buildCommand: `docker build --pull -t ${imageRepositoryUri}:<git-sha> .`,
6489
+ pushCommands: [
6490
+ "BLOCKED: push only from approved CI/CD after the ECR repository and image digest policy exist",
6491
+ "BLOCKED: deploy services by immutable image digest, not by mutable tags"
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
+ },
6502
+ runbook: {
6503
+ preflight: [
6504
+ `aws sts get-caller-identity --profile ${accountName}`,
6505
+ `aws rds describe-db-instances --db-instance-identifier ${clean(options.rdsInstanceId, DEFAULT_RDS)} --region ${region}`,
6506
+ `aws ec2 describe-vpcs --vpc-ids ${clean(options.vpcId, DEFAULT_VPC_ID)} --region ${region}`,
6507
+ "Confirm the infra repository and Terraform/CloudFormation owner before live mutation."
6508
+ ],
6509
+ provision: [
6510
+ `Infra PR must declare or update ECR repository ${ecrRepository}.`,
6511
+ `Infra PR must declare hardened S3 evidence bucket ${evidenceBucket} with KMS, versioning, lifecycle, and public access block.`,
6512
+ `Infra PR must declare ECS/Fargate cluster ${cluster}, ALB, target groups, security groups, IAM roles, CloudWatch log groups, and Secrets Manager refs.`,
6513
+ "Only apply the infra plan from the approved infrastructure repository after review evidence is attached."
6514
+ ],
6515
+ deploy: [
6516
+ "Build and publish the image only after the Dockerfile/container target is reviewed.",
6517
+ "Run the migration task with the migrator role before web/scheduler/probe services.",
6518
+ `Register task definitions for ${services.map((service) => service.name).join(", ")} using valueFrom secrets.`,
6519
+ `Update ECS services in cluster ${cluster} one component at a time through the approved deploy pipeline.`,
6520
+ `Create Route53/edge record for ${hostname} only after ALB health checks pass and auth denial smokes succeed.`
6521
+ ],
6522
+ rollback: [
6523
+ "Keep previous task definition ARNs before each service update.",
6524
+ "Rollback through the approved deploy pipeline to the previously recorded task definition ARNs.",
6525
+ "Disable scheduler/reporter services before data rollback.",
6526
+ "Restore RDS snapshot only after explicit operator approval and audit record."
6527
+ ],
6528
+ spark01: [
6529
+ "Create a private probe identity with a caller-managed public key.",
6530
+ "Install @hasna/uptime on Spark01 and write the generated env file with mode 0600.",
6531
+ "Run the private probe against the hosted /api/v1 probe endpoint once it exists."
6532
+ ]
6533
+ },
6534
+ blockers: [
6535
+ "The hasna-xyz-infra infrastructure owner repository was not found in this workspace.",
6536
+ "Hosted Postgres storage adapter and migrations are not implemented.",
6537
+ "Hosted production auth/RBAC must replace broad static hosted-token operation before exposure.",
6538
+ "Public probe execution still needs DNS, redirect, and rebinding SSRF enforcement plus cloud check-job leases.",
6539
+ "Spark01 hosted probe enrollment, claim, submit, heartbeat, revocation, and rotation are not cloud-backed yet."
6540
+ ],
6541
+ requiredEvidence: [
6542
+ "Infrastructure PR/synth/plan from the approved infra repository.",
6543
+ "Container build smoke and immutable image digest.",
6544
+ "ECS task definitions using secrets.valueFrom only.",
6545
+ "ALB/TLS/DNS/auth denial smokes and web alarm checks.",
6546
+ "RDS TLS, backups/PITR, scoped roles, and migration dry-run evidence.",
6547
+ "S3 bucket KMS, versioning, lifecycle, and public-access-block evidence.",
6548
+ "Spark01 private-probe registration, key-file mode, heartbeat, and revocation evidence."
6549
+ ],
6550
+ safety: {
6551
+ liveAwsMutation: false,
6552
+ plaintextSecrets: false,
6553
+ hostedLocalSqliteAllowed: false,
6554
+ notes: [
6555
+ "This plan generator does not call AWS.",
6556
+ "Hosted runtime must use Postgres; SQLite remains local/dev fallback only.",
6557
+ "Secrets are represented as secret names/refs and must be injected with valueFrom.",
6558
+ "Actual deploy belongs in the deploy_release_operate_final goal node after infra review."
6559
+ ]
6560
+ }
6561
+ };
6562
+ }
6563
+ function buildSpark01CloudConfig(options = {}) {
6564
+ const apiUrl = clean(options.apiUrl, `https://${DEFAULT_HOSTNAME}/api/v1`);
6565
+ const workspaceId = clean(options.workspaceId, DEFAULT_WORKSPACE_ID);
6566
+ const machineId = clean(options.machineId, "spark01");
6567
+ const privateKeyFile = clean(options.probePrivateKeyFile, "~/.hasna/uptime/probes/spark01.key.pem");
6568
+ const probeId = options.probeId?.trim();
6569
+ const blockers = [
6570
+ ...probeId ? [] : ["Cloud-registered private probe id is required before writing a sourceable env file."],
6571
+ "Hosted probe claim and submit routes still fail closed until cloud check_jobs and workspace stores are implemented.",
6572
+ "Spark01 enrollment, heartbeat, revocation, rotation, and bounded offline lease handling are not implemented yet."
6573
+ ];
6574
+ const env3 = {
6575
+ HASNA_UPTIME_MODE: "hosted",
6576
+ HASNA_UPTIME_API_URL: apiUrl,
6577
+ HASNA_UPTIME_WORKSPACE_ID: workspaceId,
6578
+ HASNA_UPTIME_MACHINE_ID: machineId,
6579
+ HASNA_UPTIME_PRIVATE_PROBE_KEY_FILE: privateKeyFile,
6580
+ HASNA_UPTIME_PROBE_CLASS: "private",
6581
+ HASNA_UPTIME_LOG_LEVEL: clean(options.logLevel, "info")
6582
+ };
6583
+ if (probeId)
6584
+ env3.HASNA_UPTIME_PRIVATE_PROBE_ID = probeId;
6585
+ return {
6586
+ kind: "open-uptime.spark01-cloud-config",
6587
+ version: 1,
6588
+ generatedAt: new Date().toISOString(),
6589
+ status: "blocked",
6590
+ canStart: false,
6591
+ machineId,
6592
+ mode: "private-probe",
6593
+ env: env3,
6594
+ files: [
6595
+ {
6596
+ path: privateKeyFile,
6597
+ mode: "0600",
6598
+ purpose: "Ed25519 private key generated on Spark01; never paste into cloud config."
6599
+ },
6600
+ {
6601
+ path: "~/.hasna/uptime/cloud.env",
6602
+ mode: "0600",
6603
+ purpose: "Non-secret cloud/probe runtime environment; token values stay in the machine secret store."
6604
+ }
6605
+ ],
6606
+ commands: [
6607
+ "bun install -g @hasna/uptime@latest",
6608
+ "Generate the Spark01 private key locally and register only its public key with the hosted control plane once registration exists.",
6609
+ "Write ~/.hasna/uptime/cloud.env from this plan, then source it for the private probe service.",
6610
+ "Start the private probe worker only after hosted /api/v1 probe claim/submit routes are backed by cloud jobs."
6611
+ ],
6612
+ blockers,
6613
+ safety: {
6614
+ privateKeyInline: false,
6615
+ tokenInline: false,
6616
+ notes: [
6617
+ "This config is hosted-targeted preflight: Spark01 must not start until cloud probe routes are backed by hosted state.",
6618
+ "The private key file path is referenced, not embedded.",
6619
+ "Hosted token or probe auth material must come from the machine secret store, not this generated config."
6620
+ ]
6621
+ }
6622
+ };
6623
+ }
6624
+ function renderSpark01Env(config) {
6625
+ const required = ["HASNA_UPTIME_PRIVATE_PROBE_ID"];
6626
+ const missing = required.filter((key) => !config.env[key]);
6627
+ if (missing.length > 0) {
6628
+ throw new Error(`Spark01 env output requires ${missing.join(", ")}`);
6629
+ }
6630
+ return Object.entries(config.env).map(([key, value]) => `${key}=${shellEscape(value)}`).join(`
6631
+ `);
6632
+ }
6633
+ function servicePlan(prefix, stage, role, desiredCount, image, workspaceId, secrets, environment) {
6634
+ const name = `${prefix}-${stage}-${role}`;
6635
+ return {
6636
+ name,
6637
+ role,
6638
+ desiredCount: 0,
6639
+ targetDesiredCount: desiredCount,
6640
+ taskRole: `${name}-task-role`,
6641
+ executionRole: `${prefix}-${stage}-execution-role`,
6642
+ logGroup: `/ecs/${name}`,
6643
+ healthCommand: role === "web" ? "GET /health" : undefined,
6644
+ environment: {
6645
+ HASNA_UPTIME_IMAGE: image,
6646
+ ...environment
6647
+ },
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 }
6649
+ };
6650
+ }
6651
+ function clean(value, fallback) {
6652
+ const normalized = value?.trim();
6653
+ return normalized || fallback;
6654
+ }
6655
+ function shellEscape(value) {
6656
+ if (/^[A-Za-z0-9_./:@~-]+$/.test(value))
6657
+ return value;
6658
+ return `'${value.replace(/'/g, "'\\''")}'`;
6659
+ }
6660
+
6390
6661
  // src/cli/index.ts
6391
6662
  var program2 = new Command;
6392
6663
  program2.name("uptime").description("Local-first uptime and downtime monitoring").version(packageVersion()).option("-j, --json", "print JSON");
@@ -6694,6 +6965,45 @@ program2.command("audit").description("List local audit events").option("--resou
6694
6965
  fail(error);
6695
6966
  }
6696
6967
  });
6968
+ var cloud = program2.command("cloud").description("Generate dry-run cloud deployment and Spark01 configuration artifacts");
6969
+ 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) => {
6970
+ try {
6971
+ const plan = buildAwsDeploymentPlan({
6972
+ accountName: opts.account,
6973
+ region: opts.region,
6974
+ stage: opts.stage,
6975
+ hostname: opts.hostname,
6976
+ workspaceId: opts.workspaceId,
6977
+ vpcId: opts.vpcId,
6978
+ rdsInstanceId: opts.rdsInstanceId,
6979
+ ecrRepository: opts.ecrRepository,
6980
+ image: opts.image,
6981
+ evidenceBucket: opts.evidenceBucket
6982
+ });
6983
+ print(plan, renderCloudPlan(plan), opts);
6984
+ } catch (error) {
6985
+ fail(error);
6986
+ }
6987
+ });
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) => {
6989
+ try {
6990
+ const config = buildSpark01CloudConfig({
6991
+ apiUrl: opts.apiUrl,
6992
+ workspaceId: opts.workspaceId,
6993
+ probeId: opts.probeId,
6994
+ probePrivateKeyFile: opts.privateKeyFile,
6995
+ machineId: opts.machineId,
6996
+ logLevel: opts.logLevel
6997
+ });
6998
+ if (opts.env && !wantsJson(opts)) {
6999
+ console.log(renderSpark01Env(config));
7000
+ return;
7001
+ }
7002
+ print(config, renderSpark01Config(config), opts);
7003
+ } catch (error) {
7004
+ fail(error);
7005
+ }
7006
+ });
6697
7007
  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) => {
6698
7008
  try {
6699
7009
  const svc = service();
@@ -7046,6 +7356,40 @@ function renderReportRuns(runs) {
7046
7356
  }).join(`
7047
7357
  `);
7048
7358
  }
7359
+ function renderCloudPlan(plan) {
7360
+ return [
7361
+ `${plan.servicePrefix} ${plan.stage} AWS plan (${plan.accountName}/${plan.region})`,
7362
+ `status: ${plan.status}`,
7363
+ `can apply: ${plan.canApply}`,
7364
+ `host: ${plan.hostname}`,
7365
+ `cluster: ${plan.resources.ecsCluster}`,
7366
+ `image: ${plan.image.uri}`,
7367
+ `dockerfile: ${plan.image.dockerfile}`,
7368
+ `infra: ${plan.infra.path}`,
7369
+ `vpc: ${plan.resources.vpcId}`,
7370
+ `rds: ${plan.resources.rdsInstanceId}`,
7371
+ `services: ${plan.resources.services.map((service2) => `${service2.name}:${service2.desiredCount}/${service2.targetDesiredCount}`).join(", ")}`,
7372
+ `evidence bucket: ${plan.resources.evidenceBucket}`,
7373
+ `blockers: ${plan.blockers.length}`,
7374
+ "live AWS mutation: false"
7375
+ ].join(`
7376
+ `);
7377
+ }
7378
+ function renderSpark01Config(config) {
7379
+ return [
7380
+ `${config.machineId} ${config.mode} config`,
7381
+ `status: ${config.status}`,
7382
+ `can start: ${config.canStart}`,
7383
+ `api: ${config.env.HASNA_UPTIME_API_URL}`,
7384
+ `workspace: ${config.env.HASNA_UPTIME_WORKSPACE_ID}`,
7385
+ `probe: ${config.env.HASNA_UPTIME_PRIVATE_PROBE_ID ?? "<required>"}`,
7386
+ `key file: ${config.env.HASNA_UPTIME_PRIVATE_PROBE_KEY_FILE}`,
7387
+ `blockers: ${config.blockers.length}`,
7388
+ "private key inline: false",
7389
+ "token inline: false"
7390
+ ].join(`
7391
+ `);
7392
+ }
7049
7393
  function renderDeliveries(deliveries) {
7050
7394
  if (deliveries.length === 0)
7051
7395
  return "No report deliveries requested";
@@ -0,0 +1,123 @@
1
+ export interface AwsDeploymentPlanOptions {
2
+ accountName?: string;
3
+ region?: string;
4
+ stage?: string;
5
+ servicePrefix?: string;
6
+ hostname?: string;
7
+ workspaceId?: string;
8
+ vpcId?: string;
9
+ rdsInstanceId?: string;
10
+ ecrRepository?: string;
11
+ image?: string;
12
+ evidenceBucket?: string;
13
+ hostedTokenSecretName?: string;
14
+ databaseSecretName?: string;
15
+ appEnvSecretName?: string;
16
+ publicProbeSecretName?: string;
17
+ privateProbeSecretName?: string;
18
+ reportingSecretName?: string;
19
+ }
20
+ export interface AwsDeploymentPlan {
21
+ kind: "open-uptime.aws-deployment-plan";
22
+ version: 1;
23
+ generatedAt: string;
24
+ status: "blocked";
25
+ canApply: false;
26
+ accountName: string;
27
+ region: string;
28
+ stage: string;
29
+ servicePrefix: string;
30
+ hostname: string;
31
+ workspaceId: string;
32
+ mode: "hosted";
33
+ resources: {
34
+ ecrRepository: string;
35
+ ecsCluster: string;
36
+ services: AwsServicePlan[];
37
+ vpcId: string;
38
+ rdsInstanceId: string;
39
+ evidenceBucket: string;
40
+ loadBalancer: string;
41
+ targetGroups: string[];
42
+ securityGroups: string[];
43
+ secrets: Record<string, string>;
44
+ logGroups: string[];
45
+ alarms: string[];
46
+ };
47
+ image: {
48
+ repository: string;
49
+ uri: string;
50
+ dockerfile: string;
51
+ buildCommand: string;
52
+ pushCommands: string[];
53
+ };
54
+ infra: {
55
+ path: string;
56
+ fmtCommand: string;
57
+ initCommand: string;
58
+ validateCommand: string;
59
+ planCommand: string;
60
+ applyAllowed: false;
61
+ };
62
+ runbook: {
63
+ preflight: string[];
64
+ provision: string[];
65
+ deploy: string[];
66
+ rollback: string[];
67
+ spark01: string[];
68
+ };
69
+ blockers: string[];
70
+ requiredEvidence: string[];
71
+ safety: {
72
+ liveAwsMutation: false;
73
+ plaintextSecrets: false;
74
+ hostedLocalSqliteAllowed: false;
75
+ notes: string[];
76
+ };
77
+ }
78
+ export interface AwsServicePlan {
79
+ name: string;
80
+ role: "web" | "scheduler" | "public-probe" | "reporter" | "migration";
81
+ desiredCount: number;
82
+ targetDesiredCount: number;
83
+ taskRole: string;
84
+ executionRole: string;
85
+ logGroup: string;
86
+ healthCommand?: string;
87
+ environment: Record<string, string>;
88
+ secrets: Record<string, string>;
89
+ }
90
+ export interface Spark01CloudConfigOptions {
91
+ apiUrl?: string;
92
+ workspaceId?: string;
93
+ probeId?: string;
94
+ probePrivateKeyFile?: string;
95
+ machineId?: string;
96
+ logLevel?: string;
97
+ }
98
+ export interface Spark01CloudConfig {
99
+ kind: "open-uptime.spark01-cloud-config";
100
+ version: 1;
101
+ generatedAt: string;
102
+ status: "blocked";
103
+ canStart: false;
104
+ machineId: string;
105
+ mode: "private-probe";
106
+ env: Record<string, string>;
107
+ files: Array<{
108
+ path: string;
109
+ mode: string;
110
+ purpose: string;
111
+ }>;
112
+ commands: string[];
113
+ blockers: string[];
114
+ safety: {
115
+ privateKeyInline: false;
116
+ tokenInline: false;
117
+ notes: string[];
118
+ };
119
+ }
120
+ export declare function buildAwsDeploymentPlan(options?: AwsDeploymentPlanOptions): AwsDeploymentPlan;
121
+ export declare function buildSpark01CloudConfig(options?: Spark01CloudConfigOptions): Spark01CloudConfig;
122
+ export declare function renderSpark01Env(config: Spark01CloudConfig): string;
123
+ //# sourceMappingURL=cloud-plan.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cloud-plan.d.ts","sourceRoot":"","sources":["../src/cloud-plan.ts"],"names":[],"mappings":"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"}