@hasna/uptime 0.1.4 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -6,6 +6,27 @@ project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.1.5] - 2026-06-28
10
+
11
+ ### Added
12
+
13
+ - Dry-run AWS deployment plan generator for the `hasna-xyz-infra` target,
14
+ covering ECS/Fargate services, ECR image commands, ALB/RDS/S3/Secrets/Logs
15
+ resources, rollback steps, and safety assertions.
16
+ - Spark01 cloud-primary private probe config generator with JSON and env-file
17
+ rendering.
18
+ - CLI commands `uptime cloud plan` and `uptime cloud spark01-config`.
19
+ - SDK export `@hasna/uptime/cloud-plan`.
20
+ - Machine-readable `blocked`/`canApply:false` and `blocked`/`canStart:false`
21
+ gates plus blocker/evidence lists for AWS and Spark01 planning artifacts.
22
+
23
+ ### Security
24
+
25
+ - Cloud planning artifacts contain secret names/refs and file paths only; they
26
+ do not inline AWS credentials, hosted tokens, or private probe key material.
27
+ - Cloud plan generation is dry-run only and does not call AWS.
28
+ - Dry-run AWS output avoids copy-pastable live AWS mutation commands.
29
+
9
30
  ## [0.1.4] - 2026-06-28
10
31
 
11
32
  ### Added
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,11 @@ 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
+
42
49
  Private/local probes can submit signed results from another machine:
43
50
 
44
51
  ```bash
package/dist/cli/index.js CHANGED
@@ -6387,6 +6387,268 @@ 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 image = clean(options.image, `<account-id>.dkr.ecr.${region}.amazonaws.com/${ecrRepository}:<git-sha>`);
6408
+ const evidenceBucket = clean(options.evidenceBucket, `hasna-${stage}-${prefix}-evidence`);
6409
+ const cluster = `${prefix}-${stage}`;
6410
+ 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`)
6417
+ };
6418
+ const services = [
6419
+ servicePlan(prefix, stage, "web", 2, image, workspaceId, secrets, {
6420
+ HASNA_UPTIME_MODE: "hosted",
6421
+ HASNA_UPTIME_WORKSPACE_ID: workspaceId,
6422
+ HASNA_UPTIME_HOSTNAME: hostname
6423
+ }),
6424
+ servicePlan(prefix, stage, "scheduler", 1, image, workspaceId, secrets, {
6425
+ HASNA_UPTIME_MODE: "hosted",
6426
+ HASNA_UPTIME_WORKSPACE_ID: workspaceId,
6427
+ HASNA_UPTIME_COMPONENT: "scheduler"
6428
+ }),
6429
+ servicePlan(prefix, stage, "public-probe", 1, image, workspaceId, secrets, {
6430
+ HASNA_UPTIME_MODE: "hosted",
6431
+ HASNA_UPTIME_WORKSPACE_ID: workspaceId,
6432
+ HASNA_UPTIME_COMPONENT: "public-probe",
6433
+ HASNA_UPTIME_PROBE_LOCATION: region
6434
+ }),
6435
+ servicePlan(prefix, stage, "reporter", 1, image, workspaceId, secrets, {
6436
+ HASNA_UPTIME_MODE: "hosted",
6437
+ HASNA_UPTIME_WORKSPACE_ID: workspaceId,
6438
+ HASNA_UPTIME_COMPONENT: "reporter"
6439
+ }),
6440
+ servicePlan(prefix, stage, "migration", 0, image, workspaceId, secrets, {
6441
+ HASNA_UPTIME_MODE: "hosted",
6442
+ HASNA_UPTIME_WORKSPACE_ID: workspaceId,
6443
+ HASNA_UPTIME_COMPONENT: "migration"
6444
+ })
6445
+ ];
6446
+ return {
6447
+ kind: "open-uptime.aws-deployment-plan",
6448
+ version: 1,
6449
+ generatedAt: new Date().toISOString(),
6450
+ status: "blocked",
6451
+ canApply: false,
6452
+ accountName,
6453
+ region,
6454
+ stage,
6455
+ servicePrefix: prefix,
6456
+ hostname,
6457
+ workspaceId,
6458
+ mode: "hosted",
6459
+ resources: {
6460
+ ecrRepository,
6461
+ ecsCluster: cluster,
6462
+ services,
6463
+ vpcId: clean(options.vpcId, DEFAULT_VPC_ID),
6464
+ rdsInstanceId: clean(options.rdsInstanceId, DEFAULT_RDS),
6465
+ evidenceBucket,
6466
+ loadBalancer: `${prefix}-${stage}-alb`,
6467
+ targetGroups: [`${prefix}-${stage}-web-tg`],
6468
+ securityGroups: [
6469
+ `${prefix}-${stage}-alb-sg`,
6470
+ `${prefix}-${stage}-web-sg`,
6471
+ `${prefix}-${stage}-scheduler-sg`,
6472
+ `${prefix}-${stage}-public-probe-sg`,
6473
+ `${prefix}-${stage}-rds-client-sg`
6474
+ ],
6475
+ secrets,
6476
+ logGroups: services.map((service) => service.logGroup),
6477
+ alarms: [
6478
+ `${prefix}-${stage}-web-5xx`,
6479
+ `${prefix}-${stage}-scheduler-stalled`,
6480
+ `${prefix}-${stage}-probe-stale`,
6481
+ `${prefix}-${stage}-report-delivery-failures`
6482
+ ]
6483
+ },
6484
+ image: {
6485
+ repository: ecrRepository,
6486
+ uri: image,
6487
+ buildCommand: "BLOCKED: add a reviewed Dockerfile/container build target before running docker build",
6488
+ pushCommands: [
6489
+ "BLOCKED: push only from approved CI/CD after the ECR repository and image digest policy exist",
6490
+ "BLOCKED: deploy services by immutable image digest, not by mutable tags"
6491
+ ]
6492
+ },
6493
+ runbook: {
6494
+ preflight: [
6495
+ `aws sts get-caller-identity --profile ${accountName}`,
6496
+ `aws rds describe-db-instances --db-instance-identifier ${clean(options.rdsInstanceId, DEFAULT_RDS)} --region ${region}`,
6497
+ `aws ec2 describe-vpcs --vpc-ids ${clean(options.vpcId, DEFAULT_VPC_ID)} --region ${region}`,
6498
+ "Confirm the infra repository and Terraform/CloudFormation owner before live mutation."
6499
+ ],
6500
+ provision: [
6501
+ `Infra PR must declare or update ECR repository ${ecrRepository}.`,
6502
+ `Infra PR must declare hardened S3 evidence bucket ${evidenceBucket} with KMS, versioning, lifecycle, and public access block.`,
6503
+ `Infra PR must declare ECS/Fargate cluster ${cluster}, ALB, target groups, security groups, IAM roles, CloudWatch log groups, and Secrets Manager refs.`,
6504
+ "Only apply the infra plan from the approved infrastructure repository after review evidence is attached."
6505
+ ],
6506
+ deploy: [
6507
+ "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.",
6509
+ `Register task definitions for ${services.map((service) => service.name).join(", ")} using valueFrom secrets.`,
6510
+ `Update ECS services in cluster ${cluster} one component at a time through the approved deploy pipeline.`,
6511
+ `Create Route53/edge record for ${hostname} only after ALB health checks pass and auth denial smokes succeed.`
6512
+ ],
6513
+ rollback: [
6514
+ "Keep previous task definition ARNs before each service update.",
6515
+ "Rollback through the approved deploy pipeline to the previously recorded task definition ARNs.",
6516
+ "Disable scheduler/reporter services before data rollback.",
6517
+ "Restore RDS snapshot only after explicit operator approval and audit record."
6518
+ ],
6519
+ spark01: [
6520
+ "Create a private probe identity with a caller-managed public key.",
6521
+ "Install @hasna/uptime on Spark01 and write the generated env file with mode 0600.",
6522
+ "Run the private probe against the hosted /api/v1 probe endpoint once it exists."
6523
+ ]
6524
+ },
6525
+ 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.",
6529
+ "Hosted production auth/RBAC must replace broad static hosted-token operation before exposure.",
6530
+ "Public probe execution still needs DNS, redirect, and rebinding SSRF enforcement plus cloud check-job leases.",
6531
+ "Spark01 hosted probe enrollment, claim, submit, heartbeat, revocation, and rotation are not cloud-backed yet."
6532
+ ],
6533
+ requiredEvidence: [
6534
+ "Infrastructure PR/synth/plan from the approved infra repository.",
6535
+ "Container build smoke and immutable image digest.",
6536
+ "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.",
6539
+ "S3 bucket KMS, versioning, lifecycle, and public-access-block evidence.",
6540
+ "Spark01 private-probe registration, key-file mode, heartbeat, and revocation evidence."
6541
+ ],
6542
+ safety: {
6543
+ liveAwsMutation: false,
6544
+ plaintextSecrets: false,
6545
+ hostedLocalSqliteAllowed: false,
6546
+ notes: [
6547
+ "This plan generator does not call AWS.",
6548
+ "Hosted runtime must use Postgres; SQLite remains local/dev fallback only.",
6549
+ "Secrets are represented as secret names/refs and must be injected with valueFrom.",
6550
+ "Actual deploy belongs in the deploy_release_operate_final goal node after infra review."
6551
+ ]
6552
+ }
6553
+ };
6554
+ }
6555
+ function buildSpark01CloudConfig(options = {}) {
6556
+ const apiUrl = clean(options.apiUrl, `https://${DEFAULT_HOSTNAME}/api/v1`);
6557
+ const workspaceId = clean(options.workspaceId, DEFAULT_WORKSPACE_ID);
6558
+ const machineId = clean(options.machineId, "spark01");
6559
+ const privateKeyFile = clean(options.probePrivateKeyFile, "~/.hasna/uptime/probes/spark01.key.pem");
6560
+ const probeId = options.probeId?.trim();
6561
+ const blockers = [
6562
+ ...probeId ? [] : ["Cloud-registered private probe id is required before writing a sourceable env file."],
6563
+ "Hosted probe claim and submit routes still fail closed until cloud check_jobs and workspace stores are implemented.",
6564
+ "Spark01 enrollment, heartbeat, revocation, rotation, and bounded offline lease handling are not implemented yet."
6565
+ ];
6566
+ const env3 = {
6567
+ HASNA_UPTIME_MODE: "hosted",
6568
+ HASNA_UPTIME_API_URL: apiUrl,
6569
+ HASNA_UPTIME_WORKSPACE_ID: workspaceId,
6570
+ HASNA_UPTIME_MACHINE_ID: machineId,
6571
+ HASNA_UPTIME_PRIVATE_PROBE_KEY_FILE: privateKeyFile,
6572
+ HASNA_UPTIME_PROBE_CLASS: "private",
6573
+ HASNA_UPTIME_LOG_LEVEL: clean(options.logLevel, "info")
6574
+ };
6575
+ if (probeId)
6576
+ env3.HASNA_UPTIME_PRIVATE_PROBE_ID = probeId;
6577
+ return {
6578
+ kind: "open-uptime.spark01-cloud-config",
6579
+ version: 1,
6580
+ generatedAt: new Date().toISOString(),
6581
+ status: "blocked",
6582
+ canStart: false,
6583
+ machineId,
6584
+ mode: "private-probe",
6585
+ env: env3,
6586
+ files: [
6587
+ {
6588
+ path: privateKeyFile,
6589
+ mode: "0600",
6590
+ purpose: "Ed25519 private key generated on Spark01; never paste into cloud config."
6591
+ },
6592
+ {
6593
+ path: "~/.hasna/uptime/cloud.env",
6594
+ mode: "0600",
6595
+ purpose: "Non-secret cloud/probe runtime environment; token values stay in the machine secret store."
6596
+ }
6597
+ ],
6598
+ commands: [
6599
+ "bun install -g @hasna/uptime@latest",
6600
+ "Generate the Spark01 private key locally and register only its public key with the hosted control plane once registration exists.",
6601
+ "Write ~/.hasna/uptime/cloud.env from this plan, then source it for the private probe service.",
6602
+ "Start the private probe worker only after hosted /api/v1 probe claim/submit routes are backed by cloud jobs."
6603
+ ],
6604
+ blockers,
6605
+ safety: {
6606
+ privateKeyInline: false,
6607
+ tokenInline: false,
6608
+ notes: [
6609
+ "This config is cloud-primary: Spark01 submits to hosted API state instead of local SQLite.",
6610
+ "The private key file path is referenced, not embedded.",
6611
+ "Hosted token or probe auth material must come from the machine secret store, not this generated config."
6612
+ ]
6613
+ }
6614
+ };
6615
+ }
6616
+ function renderSpark01Env(config) {
6617
+ const required = ["HASNA_UPTIME_PRIVATE_PROBE_ID"];
6618
+ const missing = required.filter((key) => !config.env[key]);
6619
+ if (missing.length > 0) {
6620
+ throw new Error(`Spark01 env output requires ${missing.join(", ")}`);
6621
+ }
6622
+ return Object.entries(config.env).map(([key, value]) => `${key}=${shellEscape(value)}`).join(`
6623
+ `);
6624
+ }
6625
+ function servicePlan(prefix, stage, role, desiredCount, image, workspaceId, secrets, environment) {
6626
+ const name = `${prefix}-${stage}-${role}`;
6627
+ return {
6628
+ name,
6629
+ role,
6630
+ desiredCount,
6631
+ taskRole: `${name}-task-role`,
6632
+ executionRole: `${prefix}-${stage}-execution-role`,
6633
+ logGroup: `/ecs/${name}`,
6634
+ healthCommand: role === "web" ? "GET /health" : undefined,
6635
+ environment: {
6636
+ HASNA_UPTIME_IMAGE: image,
6637
+ ...environment
6638
+ },
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 }
6640
+ };
6641
+ }
6642
+ function clean(value, fallback) {
6643
+ const normalized = value?.trim();
6644
+ return normalized || fallback;
6645
+ }
6646
+ function shellEscape(value) {
6647
+ if (/^[A-Za-z0-9_./:@~-]+$/.test(value))
6648
+ return value;
6649
+ return `'${value.replace(/'/g, "'\\''")}'`;
6650
+ }
6651
+
6390
6652
  // src/cli/index.ts
6391
6653
  var program2 = new Command;
6392
6654
  program2.name("uptime").description("Local-first uptime and downtime monitoring").version(packageVersion()).option("-j, --json", "print JSON");
@@ -6694,6 +6956,45 @@ program2.command("audit").description("List local audit events").option("--resou
6694
6956
  fail(error);
6695
6957
  }
6696
6958
  });
6959
+ 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) => {
6961
+ try {
6962
+ const plan = buildAwsDeploymentPlan({
6963
+ accountName: opts.account,
6964
+ region: opts.region,
6965
+ stage: opts.stage,
6966
+ hostname: opts.hostname,
6967
+ workspaceId: opts.workspaceId,
6968
+ vpcId: opts.vpcId,
6969
+ rdsInstanceId: opts.rdsInstanceId,
6970
+ ecrRepository: opts.ecrRepository,
6971
+ image: opts.image,
6972
+ evidenceBucket: opts.evidenceBucket
6973
+ });
6974
+ print(plan, renderCloudPlan(plan), opts);
6975
+ } catch (error) {
6976
+ fail(error);
6977
+ }
6978
+ });
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) => {
6980
+ try {
6981
+ const config = buildSpark01CloudConfig({
6982
+ apiUrl: opts.apiUrl,
6983
+ workspaceId: opts.workspaceId,
6984
+ probeId: opts.probeId,
6985
+ probePrivateKeyFile: opts.privateKeyFile,
6986
+ machineId: opts.machineId,
6987
+ logLevel: opts.logLevel
6988
+ });
6989
+ if (opts.env && !wantsJson(opts)) {
6990
+ console.log(renderSpark01Env(config));
6991
+ return;
6992
+ }
6993
+ print(config, renderSpark01Config(config), opts);
6994
+ } catch (error) {
6995
+ fail(error);
6996
+ }
6997
+ });
6697
6998
  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
6999
  try {
6699
7000
  const svc = service();
@@ -7046,6 +7347,38 @@ function renderReportRuns(runs) {
7046
7347
  }).join(`
7047
7348
  `);
7048
7349
  }
7350
+ function renderCloudPlan(plan) {
7351
+ return [
7352
+ `${plan.servicePrefix} ${plan.stage} AWS plan (${plan.accountName}/${plan.region})`,
7353
+ `status: ${plan.status}`,
7354
+ `can apply: ${plan.canApply}`,
7355
+ `host: ${plan.hostname}`,
7356
+ `cluster: ${plan.resources.ecsCluster}`,
7357
+ `image: ${plan.image.uri}`,
7358
+ `vpc: ${plan.resources.vpcId}`,
7359
+ `rds: ${plan.resources.rdsInstanceId}`,
7360
+ `services: ${plan.resources.services.map((service2) => `${service2.name}:${service2.desiredCount}`).join(", ")}`,
7361
+ `evidence bucket: ${plan.resources.evidenceBucket}`,
7362
+ `blockers: ${plan.blockers.length}`,
7363
+ "live AWS mutation: false"
7364
+ ].join(`
7365
+ `);
7366
+ }
7367
+ function renderSpark01Config(config) {
7368
+ return [
7369
+ `${config.machineId} ${config.mode} config`,
7370
+ `status: ${config.status}`,
7371
+ `can start: ${config.canStart}`,
7372
+ `api: ${config.env.HASNA_UPTIME_API_URL}`,
7373
+ `workspace: ${config.env.HASNA_UPTIME_WORKSPACE_ID}`,
7374
+ `probe: ${config.env.HASNA_UPTIME_PRIVATE_PROBE_ID ?? "<required>"}`,
7375
+ `key file: ${config.env.HASNA_UPTIME_PRIVATE_PROBE_KEY_FILE}`,
7376
+ `blockers: ${config.blockers.length}`,
7377
+ "private key inline: false",
7378
+ "token inline: false"
7379
+ ].join(`
7380
+ `);
7381
+ }
7049
7382
  function renderDeliveries(deliveries) {
7050
7383
  if (deliveries.length === 0)
7051
7384
  return "No report deliveries requested";
@@ -0,0 +1,113 @@
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
+ buildCommand: string;
51
+ pushCommands: string[];
52
+ };
53
+ runbook: {
54
+ preflight: string[];
55
+ provision: string[];
56
+ deploy: string[];
57
+ rollback: string[];
58
+ spark01: string[];
59
+ };
60
+ blockers: string[];
61
+ requiredEvidence: string[];
62
+ safety: {
63
+ liveAwsMutation: false;
64
+ plaintextSecrets: false;
65
+ hostedLocalSqliteAllowed: false;
66
+ notes: string[];
67
+ };
68
+ }
69
+ export interface AwsServicePlan {
70
+ name: string;
71
+ role: "web" | "scheduler" | "public-probe" | "reporter" | "migration";
72
+ desiredCount: number;
73
+ taskRole: string;
74
+ executionRole: string;
75
+ logGroup: string;
76
+ healthCommand?: string;
77
+ environment: Record<string, string>;
78
+ secrets: Record<string, string>;
79
+ }
80
+ export interface Spark01CloudConfigOptions {
81
+ apiUrl?: string;
82
+ workspaceId?: string;
83
+ probeId?: string;
84
+ probePrivateKeyFile?: string;
85
+ machineId?: string;
86
+ logLevel?: string;
87
+ }
88
+ export interface Spark01CloudConfig {
89
+ kind: "open-uptime.spark01-cloud-config";
90
+ version: 1;
91
+ generatedAt: string;
92
+ status: "blocked";
93
+ canStart: false;
94
+ machineId: string;
95
+ mode: "private-probe";
96
+ env: Record<string, string>;
97
+ files: Array<{
98
+ path: string;
99
+ mode: string;
100
+ purpose: string;
101
+ }>;
102
+ commands: string[];
103
+ blockers: string[];
104
+ safety: {
105
+ privateKeyInline: false;
106
+ tokenInline: false;
107
+ notes: string[];
108
+ };
109
+ }
110
+ export declare function buildAwsDeploymentPlan(options?: AwsDeploymentPlanOptions): AwsDeploymentPlan;
111
+ export declare function buildSpark01CloudConfig(options?: Spark01CloudConfigOptions): Spark01CloudConfig;
112
+ export declare function renderSpark01Env(config: Spark01CloudConfig): string;
113
+ //# 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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"}
@@ -0,0 +1,267 @@
1
+ // @bun
2
+ // src/cloud-plan.ts
3
+ var DEFAULT_ACCOUNT = "hasna-xyz-infra";
4
+ var DEFAULT_REGION = "us-east-1";
5
+ var DEFAULT_STAGE = "prod";
6
+ var DEFAULT_PREFIX = "open-uptime";
7
+ var DEFAULT_HOSTNAME = "uptime.hasna.xyz";
8
+ var DEFAULT_WORKSPACE_ID = "wks_2tyysw05cwap";
9
+ var DEFAULT_VPC_ID = "vpc-04c7f7abc1d3c3f56";
10
+ var DEFAULT_RDS = "hasna-xyz-infra-apps-prod-postgres";
11
+ function buildAwsDeploymentPlan(options = {}) {
12
+ const region = clean(options.region, DEFAULT_REGION);
13
+ const stage = clean(options.stage, DEFAULT_STAGE);
14
+ const prefix = clean(options.servicePrefix, DEFAULT_PREFIX);
15
+ const accountName = clean(options.accountName, DEFAULT_ACCOUNT);
16
+ const hostname = clean(options.hostname, DEFAULT_HOSTNAME);
17
+ const workspaceId = clean(options.workspaceId, DEFAULT_WORKSPACE_ID);
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>`);
20
+ const evidenceBucket = clean(options.evidenceBucket, `hasna-${stage}-${prefix}-evidence`);
21
+ const cluster = `${prefix}-${stage}`;
22
+ const secrets = {
23
+ database: clean(options.databaseSecretName, `hasna/xyz/opensource/uptime/${stage}/rds`),
24
+ appEnv: clean(options.appEnvSecretName, `hasna/xyz/opensource/uptime/${stage}/app/env`),
25
+ hostedToken: clean(options.hostedTokenSecretName, `hasna/xyz/opensource/uptime/${stage}/hosted-token`),
26
+ publicProbe: clean(options.publicProbeSecretName, `hasna/xyz/opensource/uptime/${stage}/probe/public`),
27
+ privateProbe: clean(options.privateProbeSecretName, `hasna/xyz/opensource/uptime/${stage}/probe/private`),
28
+ reporting: clean(options.reportingSecretName, `hasna/xyz/opensource/uptime/${stage}/reporting`)
29
+ };
30
+ const services = [
31
+ servicePlan(prefix, stage, "web", 2, image, workspaceId, secrets, {
32
+ HASNA_UPTIME_MODE: "hosted",
33
+ HASNA_UPTIME_WORKSPACE_ID: workspaceId,
34
+ HASNA_UPTIME_HOSTNAME: hostname
35
+ }),
36
+ servicePlan(prefix, stage, "scheduler", 1, image, workspaceId, secrets, {
37
+ HASNA_UPTIME_MODE: "hosted",
38
+ HASNA_UPTIME_WORKSPACE_ID: workspaceId,
39
+ HASNA_UPTIME_COMPONENT: "scheduler"
40
+ }),
41
+ servicePlan(prefix, stage, "public-probe", 1, image, workspaceId, secrets, {
42
+ HASNA_UPTIME_MODE: "hosted",
43
+ HASNA_UPTIME_WORKSPACE_ID: workspaceId,
44
+ HASNA_UPTIME_COMPONENT: "public-probe",
45
+ HASNA_UPTIME_PROBE_LOCATION: region
46
+ }),
47
+ servicePlan(prefix, stage, "reporter", 1, image, workspaceId, secrets, {
48
+ HASNA_UPTIME_MODE: "hosted",
49
+ HASNA_UPTIME_WORKSPACE_ID: workspaceId,
50
+ HASNA_UPTIME_COMPONENT: "reporter"
51
+ }),
52
+ servicePlan(prefix, stage, "migration", 0, image, workspaceId, secrets, {
53
+ HASNA_UPTIME_MODE: "hosted",
54
+ HASNA_UPTIME_WORKSPACE_ID: workspaceId,
55
+ HASNA_UPTIME_COMPONENT: "migration"
56
+ })
57
+ ];
58
+ return {
59
+ kind: "open-uptime.aws-deployment-plan",
60
+ version: 1,
61
+ generatedAt: new Date().toISOString(),
62
+ status: "blocked",
63
+ canApply: false,
64
+ accountName,
65
+ region,
66
+ stage,
67
+ servicePrefix: prefix,
68
+ hostname,
69
+ workspaceId,
70
+ mode: "hosted",
71
+ resources: {
72
+ ecrRepository,
73
+ ecsCluster: cluster,
74
+ services,
75
+ vpcId: clean(options.vpcId, DEFAULT_VPC_ID),
76
+ rdsInstanceId: clean(options.rdsInstanceId, DEFAULT_RDS),
77
+ evidenceBucket,
78
+ loadBalancer: `${prefix}-${stage}-alb`,
79
+ targetGroups: [`${prefix}-${stage}-web-tg`],
80
+ securityGroups: [
81
+ `${prefix}-${stage}-alb-sg`,
82
+ `${prefix}-${stage}-web-sg`,
83
+ `${prefix}-${stage}-scheduler-sg`,
84
+ `${prefix}-${stage}-public-probe-sg`,
85
+ `${prefix}-${stage}-rds-client-sg`
86
+ ],
87
+ secrets,
88
+ logGroups: services.map((service) => service.logGroup),
89
+ alarms: [
90
+ `${prefix}-${stage}-web-5xx`,
91
+ `${prefix}-${stage}-scheduler-stalled`,
92
+ `${prefix}-${stage}-probe-stale`,
93
+ `${prefix}-${stage}-report-delivery-failures`
94
+ ]
95
+ },
96
+ image: {
97
+ repository: ecrRepository,
98
+ uri: image,
99
+ buildCommand: "BLOCKED: add a reviewed Dockerfile/container build target before running docker build",
100
+ pushCommands: [
101
+ "BLOCKED: push only from approved CI/CD after the ECR repository and image digest policy exist",
102
+ "BLOCKED: deploy services by immutable image digest, not by mutable tags"
103
+ ]
104
+ },
105
+ runbook: {
106
+ preflight: [
107
+ `aws sts get-caller-identity --profile ${accountName}`,
108
+ `aws rds describe-db-instances --db-instance-identifier ${clean(options.rdsInstanceId, DEFAULT_RDS)} --region ${region}`,
109
+ `aws ec2 describe-vpcs --vpc-ids ${clean(options.vpcId, DEFAULT_VPC_ID)} --region ${region}`,
110
+ "Confirm the infra repository and Terraform/CloudFormation owner before live mutation."
111
+ ],
112
+ provision: [
113
+ `Infra PR must declare or update ECR repository ${ecrRepository}.`,
114
+ `Infra PR must declare hardened S3 evidence bucket ${evidenceBucket} with KMS, versioning, lifecycle, and public access block.`,
115
+ `Infra PR must declare ECS/Fargate cluster ${cluster}, ALB, target groups, security groups, IAM roles, CloudWatch log groups, and Secrets Manager refs.`,
116
+ "Only apply the infra plan from the approved infrastructure repository after review evidence is attached."
117
+ ],
118
+ deploy: [
119
+ "Build and publish the image only after the Dockerfile/container target is reviewed.",
120
+ "Run the migration task with the migrator role before web/scheduler/probe services.",
121
+ `Register task definitions for ${services.map((service) => service.name).join(", ")} using valueFrom secrets.`,
122
+ `Update ECS services in cluster ${cluster} one component at a time through the approved deploy pipeline.`,
123
+ `Create Route53/edge record for ${hostname} only after ALB health checks pass and auth denial smokes succeed.`
124
+ ],
125
+ rollback: [
126
+ "Keep previous task definition ARNs before each service update.",
127
+ "Rollback through the approved deploy pipeline to the previously recorded task definition ARNs.",
128
+ "Disable scheduler/reporter services before data rollback.",
129
+ "Restore RDS snapshot only after explicit operator approval and audit record."
130
+ ],
131
+ spark01: [
132
+ "Create a private probe identity with a caller-managed public key.",
133
+ "Install @hasna/uptime on Spark01 and write the generated env file with mode 0600.",
134
+ "Run the private probe against the hosted /api/v1 probe endpoint once it exists."
135
+ ]
136
+ },
137
+ blockers: [
138
+ "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
+ "Hosted Postgres storage adapter and migrations are not implemented.",
141
+ "Hosted production auth/RBAC must replace broad static hosted-token operation before exposure.",
142
+ "Public probe execution still needs DNS, redirect, and rebinding SSRF enforcement plus cloud check-job leases.",
143
+ "Spark01 hosted probe enrollment, claim, submit, heartbeat, revocation, and rotation are not cloud-backed yet."
144
+ ],
145
+ requiredEvidence: [
146
+ "Infrastructure PR/synth/plan from the approved infra repository.",
147
+ "Container build smoke and immutable image digest.",
148
+ "ECS task definitions using secrets.valueFrom only.",
149
+ "ALB/TLS/DNS/auth denial smokes.",
150
+ "RDS TLS, backups/PITR, scoped roles, and migration dry-run evidence.",
151
+ "S3 bucket KMS, versioning, lifecycle, and public-access-block evidence.",
152
+ "Spark01 private-probe registration, key-file mode, heartbeat, and revocation evidence."
153
+ ],
154
+ safety: {
155
+ liveAwsMutation: false,
156
+ plaintextSecrets: false,
157
+ hostedLocalSqliteAllowed: false,
158
+ notes: [
159
+ "This plan generator does not call AWS.",
160
+ "Hosted runtime must use Postgres; SQLite remains local/dev fallback only.",
161
+ "Secrets are represented as secret names/refs and must be injected with valueFrom.",
162
+ "Actual deploy belongs in the deploy_release_operate_final goal node after infra review."
163
+ ]
164
+ }
165
+ };
166
+ }
167
+ function buildSpark01CloudConfig(options = {}) {
168
+ const apiUrl = clean(options.apiUrl, `https://${DEFAULT_HOSTNAME}/api/v1`);
169
+ const workspaceId = clean(options.workspaceId, DEFAULT_WORKSPACE_ID);
170
+ const machineId = clean(options.machineId, "spark01");
171
+ const privateKeyFile = clean(options.probePrivateKeyFile, "~/.hasna/uptime/probes/spark01.key.pem");
172
+ const probeId = options.probeId?.trim();
173
+ const blockers = [
174
+ ...probeId ? [] : ["Cloud-registered private probe id is required before writing a sourceable env file."],
175
+ "Hosted probe claim and submit routes still fail closed until cloud check_jobs and workspace stores are implemented.",
176
+ "Spark01 enrollment, heartbeat, revocation, rotation, and bounded offline lease handling are not implemented yet."
177
+ ];
178
+ const env = {
179
+ HASNA_UPTIME_MODE: "hosted",
180
+ HASNA_UPTIME_API_URL: apiUrl,
181
+ HASNA_UPTIME_WORKSPACE_ID: workspaceId,
182
+ HASNA_UPTIME_MACHINE_ID: machineId,
183
+ HASNA_UPTIME_PRIVATE_PROBE_KEY_FILE: privateKeyFile,
184
+ HASNA_UPTIME_PROBE_CLASS: "private",
185
+ HASNA_UPTIME_LOG_LEVEL: clean(options.logLevel, "info")
186
+ };
187
+ if (probeId)
188
+ env.HASNA_UPTIME_PRIVATE_PROBE_ID = probeId;
189
+ return {
190
+ kind: "open-uptime.spark01-cloud-config",
191
+ version: 1,
192
+ generatedAt: new Date().toISOString(),
193
+ status: "blocked",
194
+ canStart: false,
195
+ machineId,
196
+ mode: "private-probe",
197
+ env,
198
+ files: [
199
+ {
200
+ path: privateKeyFile,
201
+ mode: "0600",
202
+ purpose: "Ed25519 private key generated on Spark01; never paste into cloud config."
203
+ },
204
+ {
205
+ path: "~/.hasna/uptime/cloud.env",
206
+ mode: "0600",
207
+ purpose: "Non-secret cloud/probe runtime environment; token values stay in the machine secret store."
208
+ }
209
+ ],
210
+ commands: [
211
+ "bun install -g @hasna/uptime@latest",
212
+ "Generate the Spark01 private key locally and register only its public key with the hosted control plane once registration exists.",
213
+ "Write ~/.hasna/uptime/cloud.env from this plan, then source it for the private probe service.",
214
+ "Start the private probe worker only after hosted /api/v1 probe claim/submit routes are backed by cloud jobs."
215
+ ],
216
+ blockers,
217
+ safety: {
218
+ privateKeyInline: false,
219
+ tokenInline: false,
220
+ notes: [
221
+ "This config is cloud-primary: Spark01 submits to hosted API state instead of local SQLite.",
222
+ "The private key file path is referenced, not embedded.",
223
+ "Hosted token or probe auth material must come from the machine secret store, not this generated config."
224
+ ]
225
+ }
226
+ };
227
+ }
228
+ function renderSpark01Env(config) {
229
+ const required = ["HASNA_UPTIME_PRIVATE_PROBE_ID"];
230
+ const missing = required.filter((key) => !config.env[key]);
231
+ if (missing.length > 0) {
232
+ throw new Error(`Spark01 env output requires ${missing.join(", ")}`);
233
+ }
234
+ return Object.entries(config.env).map(([key, value]) => `${key}=${shellEscape(value)}`).join(`
235
+ `);
236
+ }
237
+ function servicePlan(prefix, stage, role, desiredCount, image, workspaceId, secrets, environment) {
238
+ const name = `${prefix}-${stage}-${role}`;
239
+ return {
240
+ name,
241
+ role,
242
+ desiredCount,
243
+ taskRole: `${name}-task-role`,
244
+ executionRole: `${prefix}-${stage}-execution-role`,
245
+ logGroup: `/ecs/${name}`,
246
+ healthCommand: role === "web" ? "GET /health" : undefined,
247
+ environment: {
248
+ HASNA_UPTIME_IMAGE: image,
249
+ ...environment
250
+ },
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 }
252
+ };
253
+ }
254
+ function clean(value, fallback) {
255
+ const normalized = value?.trim();
256
+ return normalized || fallback;
257
+ }
258
+ function shellEscape(value) {
259
+ if (/^[A-Za-z0-9_./:@~-]+$/.test(value))
260
+ return value;
261
+ return `'${value.replace(/'/g, "'\\''")}'`;
262
+ }
263
+ export {
264
+ renderSpark01Env,
265
+ buildSpark01CloudConfig,
266
+ buildAwsDeploymentPlan
267
+ };
package/dist/index.d.ts CHANGED
@@ -5,11 +5,13 @@ export { createApiHandler, serveUptime } from "./api.js";
5
5
  export { applyImport, previewImport, rollbackImport } from "./imports.js";
6
6
  export { buildUptimeReport, sendUptimeReport } from "./report.js";
7
7
  export { generateProbeKeyPair, probePublicKeyFingerprint, probeResultSigningPayload, signProbeResult, verifyProbeResultSignature } from "./probes.js";
8
+ export { buildAwsDeploymentPlan, buildSpark01CloudConfig, renderSpark01Env } from "./cloud-plan.js";
8
9
  export { uptimeHome, uptimeDbPath, uptimeHostedFallbackDbPath, ensureUptimeHome } from "./paths.js";
9
10
  export type { UptimeBackup, UptimeBackupCheck, UptimeRuntimeMode, UptimeStoreOptions, MonitorProvenance, SaveImportBatchInput, StoredImportBatch, UpsertMonitorProvenanceInput, } from "./store.js";
10
11
  export type { BrowserPageRunner, BrowserPageRunnerResult, FetchLike, } from "./checks.js";
11
12
  export type { ImportAction, ImportApplyItem, ImportApplyResult, ImportCandidate, ImportPreview, ImportPreviewItem, ImportRequest, ImportRollbackItem, ImportRollbackResult, ImportSource, } from "./imports.js";
12
13
  export type { BrowserFailedRequest, BrowserPageEvidence, AuditEvent, CheckAttemptResult, CheckEvidence, CheckResult, CheckStatus, CreateMonitorKind, CreateMonitorInput, CreateReportScheduleInput, ImportedMonitorInput, ImportedUpdateMonitorInput, EvidenceArtifact, Incident, IncidentStatus, ListAuditEventsOptions, ListReportRunsOptions, ListResultsOptions, Monitor, MonitorKind, MonitorStatus, MonitorSummary, ProbeCheckJob, ProbeCheckJobStatus, ProbeIdentity, ProbeResultSubmission, ProbeSubmissionReceipt, RecordAuditEventInput, ReportDeliveryChannel, ReportDeliveryRecord, ReportEmailChannelConfig, ReportLogsChannelConfig, ReportRun, ReportRunStatus, ReportSchedule, ReportScheduleChannels, ReportScheduleStatus, ReportSmsChannelConfig, SchedulerHandle, UpdateMonitorInput, UpdateReportScheduleInput, UptimeSummary, } from "./types.js";
13
14
  export type { ProbeKeyPair, ProbeSigningInput } from "./probes.js";
15
+ export type { AwsDeploymentPlan, AwsDeploymentPlanOptions, AwsServicePlan, Spark01CloudConfig, Spark01CloudConfigOptions, } from "./cloud-plan.js";
14
16
  export type { BuildUptimeReportOptions, SendUptimeReportOptions, UptimeEmailReportTarget, UptimeLogsReportTarget, UptimeReport, UptimeReportDelivery, UptimeSmsReportTarget, } from "./report.js";
15
17
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AACjE,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACzC,OAAO,EAAE,mBAAmB,EAAE,eAAe,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC9F,OAAO,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AACzD,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAC1E,OAAO,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAClE,OAAO,EAAE,oBAAoB,EAAE,yBAAyB,EAAE,yBAAyB,EAAE,eAAe,EAAE,0BAA0B,EAAE,MAAM,aAAa,CAAC;AACtJ,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,0BAA0B,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AACpG,YAAY,EACV,YAAY,EACZ,iBAAiB,EACjB,iBAAiB,EACjB,kBAAkB,EAClB,iBAAiB,EACjB,oBAAoB,EACpB,iBAAiB,EACjB,4BAA4B,GAC7B,MAAM,YAAY,CAAC;AACpB,YAAY,EACV,iBAAiB,EACjB,uBAAuB,EACvB,SAAS,GACV,MAAM,aAAa,CAAC;AACrB,YAAY,EACV,YAAY,EACZ,eAAe,EACf,iBAAiB,EACjB,eAAe,EACf,aAAa,EACb,iBAAiB,EACjB,aAAa,EACb,kBAAkB,EAClB,oBAAoB,EACpB,YAAY,GACb,MAAM,cAAc,CAAC;AACtB,YAAY,EACV,oBAAoB,EACpB,mBAAmB,EACnB,UAAU,EACV,kBAAkB,EAClB,aAAa,EACb,WAAW,EACX,WAAW,EACX,iBAAiB,EACjB,kBAAkB,EAClB,yBAAyB,EACzB,oBAAoB,EACpB,0BAA0B,EAC1B,gBAAgB,EAChB,QAAQ,EACR,cAAc,EACd,sBAAsB,EACtB,qBAAqB,EACrB,kBAAkB,EAClB,OAAO,EACP,WAAW,EACX,aAAa,EACb,cAAc,EACd,aAAa,EACb,mBAAmB,EACnB,aAAa,EACb,qBAAqB,EACrB,sBAAsB,EACtB,qBAAqB,EACrB,qBAAqB,EACrB,oBAAoB,EACpB,wBAAwB,EACxB,uBAAuB,EACvB,SAAS,EACT,eAAe,EACf,cAAc,EACd,sBAAsB,EACtB,oBAAoB,EACpB,sBAAsB,EACtB,eAAe,EACf,kBAAkB,EAClB,yBAAyB,EACzB,aAAa,GACd,MAAM,YAAY,CAAC;AACpB,YAAY,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AACnE,YAAY,EACV,wBAAwB,EACxB,uBAAuB,EACvB,uBAAuB,EACvB,sBAAsB,EACtB,YAAY,EACZ,oBAAoB,EACpB,qBAAqB,GACtB,MAAM,aAAa,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AACjE,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACzC,OAAO,EAAE,mBAAmB,EAAE,eAAe,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC9F,OAAO,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AACzD,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAC1E,OAAO,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAClE,OAAO,EAAE,oBAAoB,EAAE,yBAAyB,EAAE,yBAAyB,EAAE,eAAe,EAAE,0BAA0B,EAAE,MAAM,aAAa,CAAC;AACtJ,OAAO,EAAE,sBAAsB,EAAE,uBAAuB,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AACpG,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,0BAA0B,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AACpG,YAAY,EACV,YAAY,EACZ,iBAAiB,EACjB,iBAAiB,EACjB,kBAAkB,EAClB,iBAAiB,EACjB,oBAAoB,EACpB,iBAAiB,EACjB,4BAA4B,GAC7B,MAAM,YAAY,CAAC;AACpB,YAAY,EACV,iBAAiB,EACjB,uBAAuB,EACvB,SAAS,GACV,MAAM,aAAa,CAAC;AACrB,YAAY,EACV,YAAY,EACZ,eAAe,EACf,iBAAiB,EACjB,eAAe,EACf,aAAa,EACb,iBAAiB,EACjB,aAAa,EACb,kBAAkB,EAClB,oBAAoB,EACpB,YAAY,GACb,MAAM,cAAc,CAAC;AACtB,YAAY,EACV,oBAAoB,EACpB,mBAAmB,EACnB,UAAU,EACV,kBAAkB,EAClB,aAAa,EACb,WAAW,EACX,WAAW,EACX,iBAAiB,EACjB,kBAAkB,EAClB,yBAAyB,EACzB,oBAAoB,EACpB,0BAA0B,EAC1B,gBAAgB,EAChB,QAAQ,EACR,cAAc,EACd,sBAAsB,EACtB,qBAAqB,EACrB,kBAAkB,EAClB,OAAO,EACP,WAAW,EACX,aAAa,EACb,cAAc,EACd,aAAa,EACb,mBAAmB,EACnB,aAAa,EACb,qBAAqB,EACrB,sBAAsB,EACtB,qBAAqB,EACrB,qBAAqB,EACrB,oBAAoB,EACpB,wBAAwB,EACxB,uBAAuB,EACvB,SAAS,EACT,eAAe,EACf,cAAc,EACd,sBAAsB,EACtB,oBAAoB,EACpB,sBAAsB,EACtB,eAAe,EACf,kBAAkB,EAClB,yBAAyB,EACzB,aAAa,GACd,MAAM,YAAY,CAAC;AACpB,YAAY,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AACnE,YAAY,EACV,iBAAiB,EACjB,wBAAwB,EACxB,cAAc,EACd,kBAAkB,EAClB,yBAAyB,GAC1B,MAAM,iBAAiB,CAAC;AACzB,YAAY,EACV,wBAAwB,EACxB,uBAAuB,EACvB,uBAAuB,EACvB,sBAAsB,EACtB,YAAY,EACZ,oBAAoB,EACpB,qBAAqB,GACtB,MAAM,aAAa,CAAC"}
package/dist/index.js CHANGED
@@ -3789,6 +3789,268 @@ class ApiError extends Error {
3789
3789
  this.status = status;
3790
3790
  }
3791
3791
  }
3792
+
3793
+ // src/cloud-plan.ts
3794
+ var DEFAULT_ACCOUNT = "hasna-xyz-infra";
3795
+ var DEFAULT_REGION = "us-east-1";
3796
+ var DEFAULT_STAGE = "prod";
3797
+ var DEFAULT_PREFIX = "open-uptime";
3798
+ var DEFAULT_HOSTNAME = "uptime.hasna.xyz";
3799
+ var DEFAULT_WORKSPACE_ID = "wks_2tyysw05cwap";
3800
+ var DEFAULT_VPC_ID = "vpc-04c7f7abc1d3c3f56";
3801
+ var DEFAULT_RDS = "hasna-xyz-infra-apps-prod-postgres";
3802
+ function buildAwsDeploymentPlan(options = {}) {
3803
+ const region = clean(options.region, DEFAULT_REGION);
3804
+ const stage = clean(options.stage, DEFAULT_STAGE);
3805
+ const prefix = clean(options.servicePrefix, DEFAULT_PREFIX);
3806
+ const accountName = clean(options.accountName, DEFAULT_ACCOUNT);
3807
+ const hostname = clean(options.hostname, DEFAULT_HOSTNAME);
3808
+ const workspaceId = clean(options.workspaceId, DEFAULT_WORKSPACE_ID);
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>`);
3811
+ const evidenceBucket = clean(options.evidenceBucket, `hasna-${stage}-${prefix}-evidence`);
3812
+ const cluster = `${prefix}-${stage}`;
3813
+ const secrets = {
3814
+ database: clean(options.databaseSecretName, `hasna/xyz/opensource/uptime/${stage}/rds`),
3815
+ appEnv: clean(options.appEnvSecretName, `hasna/xyz/opensource/uptime/${stage}/app/env`),
3816
+ hostedToken: clean(options.hostedTokenSecretName, `hasna/xyz/opensource/uptime/${stage}/hosted-token`),
3817
+ publicProbe: clean(options.publicProbeSecretName, `hasna/xyz/opensource/uptime/${stage}/probe/public`),
3818
+ privateProbe: clean(options.privateProbeSecretName, `hasna/xyz/opensource/uptime/${stage}/probe/private`),
3819
+ reporting: clean(options.reportingSecretName, `hasna/xyz/opensource/uptime/${stage}/reporting`)
3820
+ };
3821
+ const services = [
3822
+ servicePlan(prefix, stage, "web", 2, image, workspaceId, secrets, {
3823
+ HASNA_UPTIME_MODE: "hosted",
3824
+ HASNA_UPTIME_WORKSPACE_ID: workspaceId,
3825
+ HASNA_UPTIME_HOSTNAME: hostname
3826
+ }),
3827
+ servicePlan(prefix, stage, "scheduler", 1, image, workspaceId, secrets, {
3828
+ HASNA_UPTIME_MODE: "hosted",
3829
+ HASNA_UPTIME_WORKSPACE_ID: workspaceId,
3830
+ HASNA_UPTIME_COMPONENT: "scheduler"
3831
+ }),
3832
+ servicePlan(prefix, stage, "public-probe", 1, image, workspaceId, secrets, {
3833
+ HASNA_UPTIME_MODE: "hosted",
3834
+ HASNA_UPTIME_WORKSPACE_ID: workspaceId,
3835
+ HASNA_UPTIME_COMPONENT: "public-probe",
3836
+ HASNA_UPTIME_PROBE_LOCATION: region
3837
+ }),
3838
+ servicePlan(prefix, stage, "reporter", 1, image, workspaceId, secrets, {
3839
+ HASNA_UPTIME_MODE: "hosted",
3840
+ HASNA_UPTIME_WORKSPACE_ID: workspaceId,
3841
+ HASNA_UPTIME_COMPONENT: "reporter"
3842
+ }),
3843
+ servicePlan(prefix, stage, "migration", 0, image, workspaceId, secrets, {
3844
+ HASNA_UPTIME_MODE: "hosted",
3845
+ HASNA_UPTIME_WORKSPACE_ID: workspaceId,
3846
+ HASNA_UPTIME_COMPONENT: "migration"
3847
+ })
3848
+ ];
3849
+ return {
3850
+ kind: "open-uptime.aws-deployment-plan",
3851
+ version: 1,
3852
+ generatedAt: new Date().toISOString(),
3853
+ status: "blocked",
3854
+ canApply: false,
3855
+ accountName,
3856
+ region,
3857
+ stage,
3858
+ servicePrefix: prefix,
3859
+ hostname,
3860
+ workspaceId,
3861
+ mode: "hosted",
3862
+ resources: {
3863
+ ecrRepository,
3864
+ ecsCluster: cluster,
3865
+ services,
3866
+ vpcId: clean(options.vpcId, DEFAULT_VPC_ID),
3867
+ rdsInstanceId: clean(options.rdsInstanceId, DEFAULT_RDS),
3868
+ evidenceBucket,
3869
+ loadBalancer: `${prefix}-${stage}-alb`,
3870
+ targetGroups: [`${prefix}-${stage}-web-tg`],
3871
+ securityGroups: [
3872
+ `${prefix}-${stage}-alb-sg`,
3873
+ `${prefix}-${stage}-web-sg`,
3874
+ `${prefix}-${stage}-scheduler-sg`,
3875
+ `${prefix}-${stage}-public-probe-sg`,
3876
+ `${prefix}-${stage}-rds-client-sg`
3877
+ ],
3878
+ secrets,
3879
+ logGroups: services.map((service) => service.logGroup),
3880
+ alarms: [
3881
+ `${prefix}-${stage}-web-5xx`,
3882
+ `${prefix}-${stage}-scheduler-stalled`,
3883
+ `${prefix}-${stage}-probe-stale`,
3884
+ `${prefix}-${stage}-report-delivery-failures`
3885
+ ]
3886
+ },
3887
+ image: {
3888
+ repository: ecrRepository,
3889
+ uri: image,
3890
+ buildCommand: "BLOCKED: add a reviewed Dockerfile/container build target before running docker build",
3891
+ pushCommands: [
3892
+ "BLOCKED: push only from approved CI/CD after the ECR repository and image digest policy exist",
3893
+ "BLOCKED: deploy services by immutable image digest, not by mutable tags"
3894
+ ]
3895
+ },
3896
+ runbook: {
3897
+ preflight: [
3898
+ `aws sts get-caller-identity --profile ${accountName}`,
3899
+ `aws rds describe-db-instances --db-instance-identifier ${clean(options.rdsInstanceId, DEFAULT_RDS)} --region ${region}`,
3900
+ `aws ec2 describe-vpcs --vpc-ids ${clean(options.vpcId, DEFAULT_VPC_ID)} --region ${region}`,
3901
+ "Confirm the infra repository and Terraform/CloudFormation owner before live mutation."
3902
+ ],
3903
+ provision: [
3904
+ `Infra PR must declare or update ECR repository ${ecrRepository}.`,
3905
+ `Infra PR must declare hardened S3 evidence bucket ${evidenceBucket} with KMS, versioning, lifecycle, and public access block.`,
3906
+ `Infra PR must declare ECS/Fargate cluster ${cluster}, ALB, target groups, security groups, IAM roles, CloudWatch log groups, and Secrets Manager refs.`,
3907
+ "Only apply the infra plan from the approved infrastructure repository after review evidence is attached."
3908
+ ],
3909
+ deploy: [
3910
+ "Build and publish the image only after the Dockerfile/container target is reviewed.",
3911
+ "Run the migration task with the migrator role before web/scheduler/probe services.",
3912
+ `Register task definitions for ${services.map((service) => service.name).join(", ")} using valueFrom secrets.`,
3913
+ `Update ECS services in cluster ${cluster} one component at a time through the approved deploy pipeline.`,
3914
+ `Create Route53/edge record for ${hostname} only after ALB health checks pass and auth denial smokes succeed.`
3915
+ ],
3916
+ rollback: [
3917
+ "Keep previous task definition ARNs before each service update.",
3918
+ "Rollback through the approved deploy pipeline to the previously recorded task definition ARNs.",
3919
+ "Disable scheduler/reporter services before data rollback.",
3920
+ "Restore RDS snapshot only after explicit operator approval and audit record."
3921
+ ],
3922
+ spark01: [
3923
+ "Create a private probe identity with a caller-managed public key.",
3924
+ "Install @hasna/uptime on Spark01 and write the generated env file with mode 0600.",
3925
+ "Run the private probe against the hosted /api/v1 probe endpoint once it exists."
3926
+ ]
3927
+ },
3928
+ blockers: [
3929
+ "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
+ "Hosted Postgres storage adapter and migrations are not implemented.",
3932
+ "Hosted production auth/RBAC must replace broad static hosted-token operation before exposure.",
3933
+ "Public probe execution still needs DNS, redirect, and rebinding SSRF enforcement plus cloud check-job leases.",
3934
+ "Spark01 hosted probe enrollment, claim, submit, heartbeat, revocation, and rotation are not cloud-backed yet."
3935
+ ],
3936
+ requiredEvidence: [
3937
+ "Infrastructure PR/synth/plan from the approved infra repository.",
3938
+ "Container build smoke and immutable image digest.",
3939
+ "ECS task definitions using secrets.valueFrom only.",
3940
+ "ALB/TLS/DNS/auth denial smokes.",
3941
+ "RDS TLS, backups/PITR, scoped roles, and migration dry-run evidence.",
3942
+ "S3 bucket KMS, versioning, lifecycle, and public-access-block evidence.",
3943
+ "Spark01 private-probe registration, key-file mode, heartbeat, and revocation evidence."
3944
+ ],
3945
+ safety: {
3946
+ liveAwsMutation: false,
3947
+ plaintextSecrets: false,
3948
+ hostedLocalSqliteAllowed: false,
3949
+ notes: [
3950
+ "This plan generator does not call AWS.",
3951
+ "Hosted runtime must use Postgres; SQLite remains local/dev fallback only.",
3952
+ "Secrets are represented as secret names/refs and must be injected with valueFrom.",
3953
+ "Actual deploy belongs in the deploy_release_operate_final goal node after infra review."
3954
+ ]
3955
+ }
3956
+ };
3957
+ }
3958
+ function buildSpark01CloudConfig(options = {}) {
3959
+ const apiUrl = clean(options.apiUrl, `https://${DEFAULT_HOSTNAME}/api/v1`);
3960
+ const workspaceId = clean(options.workspaceId, DEFAULT_WORKSPACE_ID);
3961
+ const machineId = clean(options.machineId, "spark01");
3962
+ const privateKeyFile = clean(options.probePrivateKeyFile, "~/.hasna/uptime/probes/spark01.key.pem");
3963
+ const probeId = options.probeId?.trim();
3964
+ const blockers = [
3965
+ ...probeId ? [] : ["Cloud-registered private probe id is required before writing a sourceable env file."],
3966
+ "Hosted probe claim and submit routes still fail closed until cloud check_jobs and workspace stores are implemented.",
3967
+ "Spark01 enrollment, heartbeat, revocation, rotation, and bounded offline lease handling are not implemented yet."
3968
+ ];
3969
+ const env2 = {
3970
+ HASNA_UPTIME_MODE: "hosted",
3971
+ HASNA_UPTIME_API_URL: apiUrl,
3972
+ HASNA_UPTIME_WORKSPACE_ID: workspaceId,
3973
+ HASNA_UPTIME_MACHINE_ID: machineId,
3974
+ HASNA_UPTIME_PRIVATE_PROBE_KEY_FILE: privateKeyFile,
3975
+ HASNA_UPTIME_PROBE_CLASS: "private",
3976
+ HASNA_UPTIME_LOG_LEVEL: clean(options.logLevel, "info")
3977
+ };
3978
+ if (probeId)
3979
+ env2.HASNA_UPTIME_PRIVATE_PROBE_ID = probeId;
3980
+ return {
3981
+ kind: "open-uptime.spark01-cloud-config",
3982
+ version: 1,
3983
+ generatedAt: new Date().toISOString(),
3984
+ status: "blocked",
3985
+ canStart: false,
3986
+ machineId,
3987
+ mode: "private-probe",
3988
+ env: env2,
3989
+ files: [
3990
+ {
3991
+ path: privateKeyFile,
3992
+ mode: "0600",
3993
+ purpose: "Ed25519 private key generated on Spark01; never paste into cloud config."
3994
+ },
3995
+ {
3996
+ path: "~/.hasna/uptime/cloud.env",
3997
+ mode: "0600",
3998
+ purpose: "Non-secret cloud/probe runtime environment; token values stay in the machine secret store."
3999
+ }
4000
+ ],
4001
+ commands: [
4002
+ "bun install -g @hasna/uptime@latest",
4003
+ "Generate the Spark01 private key locally and register only its public key with the hosted control plane once registration exists.",
4004
+ "Write ~/.hasna/uptime/cloud.env from this plan, then source it for the private probe service.",
4005
+ "Start the private probe worker only after hosted /api/v1 probe claim/submit routes are backed by cloud jobs."
4006
+ ],
4007
+ blockers,
4008
+ safety: {
4009
+ privateKeyInline: false,
4010
+ tokenInline: false,
4011
+ notes: [
4012
+ "This config is cloud-primary: Spark01 submits to hosted API state instead of local SQLite.",
4013
+ "The private key file path is referenced, not embedded.",
4014
+ "Hosted token or probe auth material must come from the machine secret store, not this generated config."
4015
+ ]
4016
+ }
4017
+ };
4018
+ }
4019
+ function renderSpark01Env(config) {
4020
+ const required = ["HASNA_UPTIME_PRIVATE_PROBE_ID"];
4021
+ const missing = required.filter((key) => !config.env[key]);
4022
+ if (missing.length > 0) {
4023
+ throw new Error(`Spark01 env output requires ${missing.join(", ")}`);
4024
+ }
4025
+ return Object.entries(config.env).map(([key, value]) => `${key}=${shellEscape(value)}`).join(`
4026
+ `);
4027
+ }
4028
+ function servicePlan(prefix, stage, role, desiredCount, image, workspaceId, secrets, environment) {
4029
+ const name = `${prefix}-${stage}-${role}`;
4030
+ return {
4031
+ name,
4032
+ role,
4033
+ desiredCount,
4034
+ taskRole: `${name}-task-role`,
4035
+ executionRole: `${prefix}-${stage}-execution-role`,
4036
+ logGroup: `/ecs/${name}`,
4037
+ healthCommand: role === "web" ? "GET /health" : undefined,
4038
+ environment: {
4039
+ HASNA_UPTIME_IMAGE: image,
4040
+ ...environment
4041
+ },
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 }
4043
+ };
4044
+ }
4045
+ function clean(value, fallback) {
4046
+ const normalized = value?.trim();
4047
+ return normalized || fallback;
4048
+ }
4049
+ function shellEscape(value) {
4050
+ if (/^[A-Za-z0-9_./:@~-]+$/.test(value))
4051
+ return value;
4052
+ return `'${value.replace(/'/g, "'\\''")}'`;
4053
+ }
3792
4054
  export {
3793
4055
  verifyProbeResultSignature,
3794
4056
  uptimeHostedFallbackDbPath,
@@ -3802,6 +4064,7 @@ export {
3802
4064
  runHttpCheck,
3803
4065
  runBrowserPageCheck,
3804
4066
  rollbackImport,
4067
+ renderSpark01Env,
3805
4068
  probeResultSigningPayload,
3806
4069
  probePublicKeyFingerprint,
3807
4070
  previewImport,
@@ -3810,6 +4073,8 @@ export {
3810
4073
  createUptimeClient,
3811
4074
  createApiHandler,
3812
4075
  buildUptimeReport,
4076
+ buildSpark01CloudConfig,
4077
+ buildAwsDeploymentPlan,
3813
4078
  applyImport,
3814
4079
  UptimeStore,
3815
4080
  UptimeService
@@ -0,0 +1,92 @@
1
+ # AWS Deployment Runbook
2
+
3
+ This runbook is for the `hasna-xyz-infra` AWS account target. It is intentionally
4
+ dry-run first: the local generator produces a plan and command list, but it does
5
+ not call AWS or mutate infrastructure.
6
+
7
+ ## Generate The Plan
8
+
9
+ ```bash
10
+ uptime cloud plan --json > open-uptime-aws-plan.json
11
+ uptime cloud spark01-config --probe-id prb_spark01 --env > spark01-uptime.env
12
+ ```
13
+
14
+ Defaults come from the current design inventory:
15
+
16
+ - account/profile label: `hasna-xyz-infra`
17
+ - region: `us-east-1`
18
+ - VPC: `vpc-04c7f7abc1d3c3f56`
19
+ - RDS instance: `hasna-xyz-infra-apps-prod-postgres`
20
+ - hostname: `uptime.hasna.xyz`
21
+ - workspace id: `wks_2tyysw05cwap`
22
+
23
+ Override these with CLI flags if the infra owner chooses a different value.
24
+
25
+ The generated AWS plan currently returns `status: "blocked"` and
26
+ `canApply: false`. The generated Spark01 config returns `status: "blocked"` and
27
+ `canStart: false`. Treat both as review/preflight artifacts until the blockers
28
+ and required evidence in the JSON output are resolved.
29
+
30
+ `uptime cloud spark01-config --env` requires a real `--probe-id`; it will not
31
+ write a sourceable env file with a placeholder probe identity.
32
+
33
+ ## Preflight
34
+
35
+ 1. Locate the real `hasna-xyz-infra` infrastructure repository or create the
36
+ change in the approved owner repository.
37
+ 2. Confirm the AWS caller identity:
38
+
39
+ ```bash
40
+ aws sts get-caller-identity --profile hasna-xyz-infra
41
+ ```
42
+
43
+ 3. Confirm the target VPC and RDS instance still match the plan.
44
+ 4. Confirm Route53/edge ownership for the chosen hostname.
45
+ 5. Confirm the deployment role uses short-lived credentials or OIDC, not copied
46
+ access keys.
47
+
48
+ ## Required Resources
49
+
50
+ The plan expects:
51
+
52
+ - ECR repository for the Open Uptime image.
53
+ - ECS/Fargate cluster with separate services for web, scheduler, public probe,
54
+ reporter, and one-off migrations.
55
+ - ALB, TLS certificate, target group, and security groups.
56
+ - Existing private Postgres instance with dedicated Uptime roles or database.
57
+ - S3 bucket for redacted browser evidence and generated report artifacts.
58
+ - Secrets Manager or SSM refs for database, app env, probe config, and
59
+ reporting channel refs.
60
+ - CloudWatch log groups and alarms for web 5xx, scheduler stalls, stale probes,
61
+ and report delivery failures.
62
+
63
+ Provision these through the approved infrastructure repository and reviewed
64
+ plan/apply flow. The local `uptime cloud plan` output intentionally avoids
65
+ copy-pastable AWS mutation commands.
66
+
67
+ ## Spark01
68
+
69
+ Spark01 should be a private probe/operator machine, not the hosted source of
70
+ truth. The generated env file points Spark01 at hosted `/api/v1` state and
71
+ references a local private-key file path. It does not include private key or
72
+ token contents.
73
+
74
+ The private probe service should not be enabled until hosted probe claim/submit
75
+ routes are backed by cloud check jobs and cloud audit rows.
76
+
77
+ ## Safety Rules
78
+
79
+ - 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.
82
+ - Do not run public probe workers against private targets.
83
+ - Do not expose dashboard/API routes without hosted auth and workspace checks.
84
+ - Do not treat local SQLite, local project DBs, or Spark01 local state as cloud
85
+ authority after cutover.
86
+
87
+ ## Rollback
88
+
89
+ Before each service update, record the previous task definition ARN. Roll back
90
+ by disabling scheduler/reporter work first, then restoring the previous web or
91
+ worker task definition. RDS snapshot restore requires separate operator approval
92
+ and an audit event.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/uptime",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "Local-first uptime and downtime monitoring service with CLI, MCP, SDK, SQLite persistence, and a dashboard.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -23,6 +23,7 @@
23
23
  "files": [
24
24
  "dist",
25
25
  "README.md",
26
+ "docs/aws-deployment-runbook.md",
26
27
  "CHANGELOG.md",
27
28
  "LICENSE",
28
29
  "NOTICE",
@@ -55,10 +56,14 @@
55
56
  "./probes": {
56
57
  "types": "./dist/probes.d.ts",
57
58
  "import": "./dist/probes.js"
59
+ },
60
+ "./cloud-plan": {
61
+ "types": "./dist/cloud-plan.d.ts",
62
+ "import": "./dist/cloud-plan.js"
58
63
  }
59
64
  },
60
65
  "scripts": {
61
- "build": "rm -rf dist && bun build src/cli/index.ts --outdir dist/cli --target bun --external @modelcontextprotocol/sdk && bun build src/mcp/index.ts --outdir dist/mcp --target bun --external @modelcontextprotocol/sdk && bun build src/index.ts src/api.ts src/service.ts src/store.ts src/checks.ts src/imports.ts src/report.ts src/probes.ts src/types.ts src/paths.ts src/dashboard.ts src/version.ts --root src --outdir dist --target bun && tsc -p tsconfig.build.json --emitDeclarationOnly --outDir dist && chmod +x dist/cli/index.js dist/mcp/index.js",
66
+ "build": "rm -rf dist && bun build src/cli/index.ts --outdir dist/cli --target bun --external @modelcontextprotocol/sdk && bun build src/mcp/index.ts --outdir dist/mcp --target bun --external @modelcontextprotocol/sdk && bun build src/index.ts src/api.ts src/service.ts src/store.ts src/checks.ts src/imports.ts src/report.ts src/probes.ts src/cloud-plan.ts src/types.ts src/paths.ts src/dashboard.ts src/version.ts --root src --outdir dist --target bun && tsc -p tsconfig.build.json --emitDeclarationOnly --outDir dist && chmod +x dist/cli/index.js dist/mcp/index.js",
62
67
  "typecheck": "tsc --noEmit",
63
68
  "test": "bun test ./tests",
64
69
  "dev:cli": "bun run src/cli/index.ts",