@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.
@@ -6,12 +6,16 @@ export interface AwsDeploymentPlanOptions {
6
6
  hostname?: string;
7
7
  workspaceId?: string;
8
8
  vpcId?: string;
9
- rdsInstanceId?: string;
10
9
  ecrRepository?: string;
11
10
  image?: string;
12
11
  evidenceBucket?: string;
13
- hostedTokenSecretName?: string;
12
+ hostedSqliteDbPath?: string;
13
+ runtimePackageVersion?: string;
14
+ /** @deprecated Postgres is target-state only until the async adapter is implemented. */
15
+ rdsInstanceId?: string;
16
+ /** @deprecated Postgres is target-state only until the async adapter is implemented. */
14
17
  databaseSecretName?: string;
18
+ hostedTokenSecretName?: string;
15
19
  appEnvSecretName?: string;
16
20
  publicProbeSecretName?: string;
17
21
  privateProbeSecretName?: string;
@@ -19,7 +23,7 @@ export interface AwsDeploymentPlanOptions {
19
23
  }
20
24
  export interface AwsDeploymentPlan {
21
25
  kind: "open-uptime.aws-deployment-plan";
22
- version: 1;
26
+ version: 2;
23
27
  generatedAt: string;
24
28
  status: "blocked";
25
29
  canApply: false;
@@ -32,10 +36,13 @@ export interface AwsDeploymentPlan {
32
36
  mode: "hosted";
33
37
  resources: {
34
38
  ecrRepository: string;
39
+ imageBuilder: string;
35
40
  ecsCluster: string;
36
41
  services: AwsServicePlan[];
37
42
  vpcId: string;
38
- rdsInstanceId: string;
43
+ efsFileSystem: string;
44
+ efsAccessPoint: string;
45
+ hostedSqliteDbPath: string;
39
46
  evidenceBucket: string;
40
47
  loadBalancer: string;
41
48
  targetGroups: string[];
@@ -47,9 +54,18 @@ export interface AwsDeploymentPlan {
47
54
  image: {
48
55
  repository: string;
49
56
  uri: string;
57
+ dockerfile: string;
50
58
  buildCommand: string;
51
59
  pushCommands: string[];
52
60
  };
61
+ infra: {
62
+ path: string;
63
+ fmtCommand: string;
64
+ initCommand: string;
65
+ validateCommand: string;
66
+ planCommand: string;
67
+ applyAllowed: false;
68
+ };
53
69
  runbook: {
54
70
  preflight: string[];
55
71
  provision: string[];
@@ -70,6 +86,7 @@ export interface AwsServicePlan {
70
86
  name: string;
71
87
  role: "web" | "scheduler" | "public-probe" | "reporter" | "migration";
72
88
  desiredCount: number;
89
+ targetDesiredCount: number;
73
90
  taskRole: string;
74
91
  executionRole: string;
75
92
  logGroup: string;
@@ -1 +1 @@
1
- {"version":3,"file":"cloud-plan.d.ts","sourceRoot":"","sources":["../src/cloud-plan.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,wBAAwB;IACvC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,mBAAmB,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,iCAAiC,CAAC;IACxC,OAAO,EAAE,CAAC,CAAC;IACX,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,SAAS,CAAC;IAClB,QAAQ,EAAE,KAAK,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,QAAQ,CAAC;IACf,SAAS,EAAE;QACT,aAAa,EAAE,MAAM,CAAC;QACtB,UAAU,EAAE,MAAM,CAAC;QACnB,QAAQ,EAAE,cAAc,EAAE,CAAC;QAC3B,KAAK,EAAE,MAAM,CAAC;QACd,aAAa,EAAE,MAAM,CAAC;QACtB,cAAc,EAAE,MAAM,CAAC;QACvB,YAAY,EAAE,MAAM,CAAC;QACrB,YAAY,EAAE,MAAM,EAAE,CAAC;QACvB,cAAc,EAAE,MAAM,EAAE,CAAC;QACzB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QAChC,SAAS,EAAE,MAAM,EAAE,CAAC;QACpB,MAAM,EAAE,MAAM,EAAE,CAAC;KAClB,CAAC;IACF,KAAK,EAAE;QACL,UAAU,EAAE,MAAM,CAAC;QACnB,GAAG,EAAE,MAAM,CAAC;QACZ,YAAY,EAAE,MAAM,CAAC;QACrB,YAAY,EAAE,MAAM,EAAE,CAAC;KACxB,CAAC;IACF,OAAO,EAAE;QACP,SAAS,EAAE,MAAM,EAAE,CAAC;QACpB,SAAS,EAAE,MAAM,EAAE,CAAC;QACpB,MAAM,EAAE,MAAM,EAAE,CAAC;QACjB,QAAQ,EAAE,MAAM,EAAE,CAAC;QACnB,OAAO,EAAE,MAAM,EAAE,CAAC;KACnB,CAAC;IACF,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,gBAAgB,EAAE,MAAM,EAAE,CAAC;IAC3B,MAAM,EAAE;QACN,eAAe,EAAE,KAAK,CAAC;QACvB,gBAAgB,EAAE,KAAK,CAAC;QACxB,wBAAwB,EAAE,KAAK,CAAC;QAChC,KAAK,EAAE,MAAM,EAAE,CAAC;KACjB,CAAC;CACH;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,KAAK,GAAG,WAAW,GAAG,cAAc,GAAG,UAAU,GAAG,WAAW,CAAC;IACtE,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACpC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACjC;AAED,MAAM,WAAW,yBAAyB;IACxC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,kCAAkC,CAAC;IACzC,OAAO,EAAE,CAAC,CAAC;IACX,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,SAAS,CAAC;IAClB,QAAQ,EAAE,KAAK,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,eAAe,CAAC;IACtB,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC5B,KAAK,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC9D,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,MAAM,EAAE;QACN,gBAAgB,EAAE,KAAK,CAAC;QACxB,WAAW,EAAE,KAAK,CAAC;QACnB,KAAK,EAAE,MAAM,EAAE,CAAC;KACjB,CAAC;CACH;AAWD,wBAAgB,sBAAsB,CAAC,OAAO,GAAE,wBAA6B,GAAG,iBAAiB,CA4JhG;AAED,wBAAgB,uBAAuB,CAAC,OAAO,GAAE,yBAA8B,GAAG,kBAAkB,CA2DnG;AAED,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,kBAAkB,GAAG,MAAM,CASnE"}
1
+ {"version":3,"file":"cloud-plan.d.ts","sourceRoot":"","sources":["../src/cloud-plan.ts"],"names":[],"mappings":"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"}
@@ -1,13 +1,13 @@
1
1
  // @bun
2
2
  // src/cloud-plan.ts
3
- var DEFAULT_ACCOUNT = "hasna-xyz-infra";
3
+ var DEFAULT_ACCOUNT = "aws-profile";
4
4
  var DEFAULT_REGION = "us-east-1";
5
5
  var DEFAULT_STAGE = "prod";
6
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";
7
+ var DEFAULT_HOSTNAME = "uptime.example.com";
8
+ var DEFAULT_WORKSPACE_ID = "workspace-id";
9
+ var DEFAULT_VPC_ID = "vpc-xxxxxxxx";
10
+ var DEFAULT_HOSTED_SQLITE_DB = "/data/uptime/uptime.db";
11
11
  function buildAwsDeploymentPlan(options = {}) {
12
12
  const region = clean(options.region, DEFAULT_REGION);
13
13
  const stage = clean(options.stage, DEFAULT_STAGE);
@@ -15,36 +15,39 @@ function buildAwsDeploymentPlan(options = {}) {
15
15
  const accountName = clean(options.accountName, DEFAULT_ACCOUNT);
16
16
  const hostname = clean(options.hostname, DEFAULT_HOSTNAME);
17
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>`);
18
+ const ecrRepository = clean(options.ecrRepository, prefix);
19
+ const imageRepositoryUri = `<account-id>.dkr.ecr.${region}.amazonaws.com/${ecrRepository}`;
20
+ const image = clean(options.image, `${imageRepositoryUri}@sha256:<image-digest>`);
20
21
  const evidenceBucket = clean(options.evidenceBucket, `hasna-${stage}-${prefix}-evidence`);
22
+ const hostedSqliteDbPath = clean(options.hostedSqliteDbPath, DEFAULT_HOSTED_SQLITE_DB);
23
+ const runtimePackageVersion = clean(options.runtimePackageVersion, "0.1.7");
21
24
  const cluster = `${prefix}-${stage}`;
22
25
  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`)
26
+ appEnv: clean(options.appEnvSecretName, `open-uptime/${stage}/app/env`),
27
+ hostedToken: clean(options.hostedTokenSecretName, `open-uptime/${stage}/hosted-token`),
28
+ publicProbe: clean(options.publicProbeSecretName, `open-uptime/${stage}/probe/public`),
29
+ privateProbe: clean(options.privateProbeSecretName, `open-uptime/${stage}/probe/private`),
30
+ reporting: clean(options.reportingSecretName, `open-uptime/${stage}/reporting`)
29
31
  };
30
32
  const services = [
31
- servicePlan(prefix, stage, "web", 2, image, workspaceId, secrets, {
33
+ servicePlan(prefix, stage, "web", 1, image, workspaceId, secrets, {
32
34
  HASNA_UPTIME_MODE: "hosted",
35
+ HASNA_UPTIME_HOSTED_SQLITE_DB: hostedSqliteDbPath,
33
36
  HASNA_UPTIME_WORKSPACE_ID: workspaceId,
34
37
  HASNA_UPTIME_HOSTNAME: hostname
35
38
  }),
36
- servicePlan(prefix, stage, "scheduler", 1, image, workspaceId, secrets, {
39
+ servicePlan(prefix, stage, "scheduler", 0, image, workspaceId, secrets, {
37
40
  HASNA_UPTIME_MODE: "hosted",
38
41
  HASNA_UPTIME_WORKSPACE_ID: workspaceId,
39
42
  HASNA_UPTIME_COMPONENT: "scheduler"
40
43
  }),
41
- servicePlan(prefix, stage, "public-probe", 1, image, workspaceId, secrets, {
44
+ servicePlan(prefix, stage, "public-probe", 0, image, workspaceId, secrets, {
42
45
  HASNA_UPTIME_MODE: "hosted",
43
46
  HASNA_UPTIME_WORKSPACE_ID: workspaceId,
44
47
  HASNA_UPTIME_COMPONENT: "public-probe",
45
48
  HASNA_UPTIME_PROBE_LOCATION: region
46
49
  }),
47
- servicePlan(prefix, stage, "reporter", 1, image, workspaceId, secrets, {
50
+ servicePlan(prefix, stage, "reporter", 0, image, workspaceId, secrets, {
48
51
  HASNA_UPTIME_MODE: "hosted",
49
52
  HASNA_UPTIME_WORKSPACE_ID: workspaceId,
50
53
  HASNA_UPTIME_COMPONENT: "reporter"
@@ -57,7 +60,7 @@ function buildAwsDeploymentPlan(options = {}) {
57
60
  ];
58
61
  return {
59
62
  kind: "open-uptime.aws-deployment-plan",
60
- version: 1,
63
+ version: 2,
61
64
  generatedAt: new Date().toISOString(),
62
65
  status: "blocked",
63
66
  canApply: false,
@@ -70,10 +73,13 @@ function buildAwsDeploymentPlan(options = {}) {
70
73
  mode: "hosted",
71
74
  resources: {
72
75
  ecrRepository,
76
+ imageBuilder: `${prefix}-${stage}-image-builder`,
73
77
  ecsCluster: cluster,
74
78
  services,
75
79
  vpcId: clean(options.vpcId, DEFAULT_VPC_ID),
76
- rdsInstanceId: clean(options.rdsInstanceId, DEFAULT_RDS),
80
+ efsFileSystem: `${prefix}-${stage}-data`,
81
+ efsAccessPoint: `${prefix}-${stage}-uptime`,
82
+ hostedSqliteDbPath,
77
83
  evidenceBucket,
78
84
  loadBalancer: `${prefix}-${stage}-alb`,
79
85
  targetGroups: [`${prefix}-${stage}-web-tg`],
@@ -82,42 +88,54 @@ function buildAwsDeploymentPlan(options = {}) {
82
88
  `${prefix}-${stage}-web-sg`,
83
89
  `${prefix}-${stage}-scheduler-sg`,
84
90
  `${prefix}-${stage}-public-probe-sg`,
85
- `${prefix}-${stage}-rds-client-sg`
91
+ `${prefix}-${stage}-reporter-sg`,
92
+ `${prefix}-${stage}-migration-sg`,
93
+ `${prefix}-${stage}-efs-sg`
86
94
  ],
87
95
  secrets,
88
96
  logGroups: services.map((service) => service.logGroup),
89
97
  alarms: [
90
98
  `${prefix}-${stage}-web-5xx`,
91
- `${prefix}-${stage}-scheduler-stalled`,
92
- `${prefix}-${stage}-probe-stale`,
93
- `${prefix}-${stage}-report-delivery-failures`
99
+ `${prefix}-${stage}-web-unhealthy`
94
100
  ]
95
101
  },
96
102
  image: {
97
103
  repository: ecrRepository,
98
104
  uri: image,
99
- buildCommand: "BLOCKED: add a reviewed Dockerfile/container build target before running docker build",
105
+ dockerfile: "Dockerfile.package",
106
+ buildCommand: `BLOCKED: after infra approval, AWS CodeBuild builds Dockerfile.package from @hasna/uptime@${runtimePackageVersion} into ${imageRepositoryUri}`,
100
107
  pushCommands: [
101
- "BLOCKED: push only from approved CI/CD after the ECR repository and image digest policy exist",
108
+ `BLOCKED: start ${prefix}-${stage}-image-builder only through the approved deploy pipeline after @hasna/uptime@${runtimePackageVersion} is published`,
102
109
  "BLOCKED: deploy services by immutable image digest, not by mutable tags"
103
110
  ]
104
111
  },
112
+ infra: {
113
+ path: "infra/aws",
114
+ fmtCommand: "terraform -chdir=infra/aws fmt -check",
115
+ initCommand: "terraform -chdir=infra/aws init -backend=false",
116
+ validateCommand: "terraform -chdir=infra/aws validate",
117
+ planCommand: "terraform -chdir=infra/aws plan -out open-uptime.tfplan",
118
+ applyAllowed: false
119
+ },
105
120
  runbook: {
106
121
  preflight: [
107
122
  `aws sts get-caller-identity --profile ${accountName}`,
108
- `aws rds describe-db-instances --db-instance-identifier ${clean(options.rdsInstanceId, DEFAULT_RDS)} --region ${region}`,
109
123
  `aws ec2 describe-vpcs --vpc-ids ${clean(options.vpcId, DEFAULT_VPC_ID)} --region ${region}`,
124
+ `aws efs describe-file-systems --region ${region}`,
110
125
  "Confirm the infra repository and Terraform/CloudFormation owner before live mutation."
111
126
  ],
112
127
  provision: [
113
128
  `Infra PR must declare or update ECR repository ${ecrRepository}.`,
129
+ `Infra PR must declare CodeBuild image builder ${prefix}-${stage}-image-builder for @hasna/uptime@${runtimePackageVersion}.`,
114
130
  `Infra PR must declare hardened S3 evidence bucket ${evidenceBucket} with KMS, versioning, lifecycle, and public access block.`,
131
+ `Infra PR must declare encrypted EFS ${prefix}-${stage}-data with access point, mount targets, and AWS Backup plan.`,
115
132
  `Infra PR must declare ECS/Fargate cluster ${cluster}, ALB, target groups, security groups, IAM roles, CloudWatch log groups, and Secrets Manager refs.`,
116
133
  "Only apply the infra plan from the approved infrastructure repository after review evidence is attached."
117
134
  ],
118
135
  deploy: [
119
136
  "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.",
137
+ `Start the AWS image builder for @hasna/uptime@${runtimePackageVersion} and record the pushed image digest.`,
138
+ "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.",
121
139
  `Register task definitions for ${services.map((service) => service.name).join(", ")} using valueFrom secrets.`,
122
140
  `Update ECS services in cluster ${cluster} one component at a time through the approved deploy pipeline.`,
123
141
  `Create Route53/edge record for ${hostname} only after ALB health checks pass and auth denial smokes succeed.`
@@ -126,7 +144,7 @@ function buildAwsDeploymentPlan(options = {}) {
126
144
  "Keep previous task definition ARNs before each service update.",
127
145
  "Rollback through the approved deploy pipeline to the previously recorded task definition ARNs.",
128
146
  "Disable scheduler/reporter services before data rollback.",
129
- "Restore RDS snapshot only after explicit operator approval and audit record."
147
+ "Restore EFS backup recovery point only after explicit operator approval and audit record."
130
148
  ],
131
149
  spark01: [
132
150
  "Create a private probe identity with a caller-managed public key.",
@@ -135,19 +153,19 @@ function buildAwsDeploymentPlan(options = {}) {
135
153
  ]
136
154
  },
137
155
  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.",
156
+ "The infrastructure owner repository was not found in this workspace.",
157
+ "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.",
141
158
  "Hosted production auth/RBAC must replace broad static hosted-token operation before exposure.",
142
159
  "Public probe execution still needs DNS, redirect, and rebinding SSRF enforcement plus cloud check-job leases.",
143
160
  "Spark01 hosted probe enrollment, claim, submit, heartbeat, revocation, and rotation are not cloud-backed yet."
144
161
  ],
145
162
  requiredEvidence: [
146
163
  "Infrastructure PR/synth/plan from the approved infra repository.",
147
- "Container build smoke and immutable image digest.",
164
+ "CodeBuild image-builder run, container smoke, and immutable image digest.",
148
165
  "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.",
166
+ "ALB/TLS/DNS/auth denial smokes and web alarm checks.",
167
+ "Single-writer ECS evidence: one web task maximum and no scheduler/public-probe/reporter EFS mounts.",
168
+ "EFS encryption, access point, mount-target, AWS Backup, and restore-drill evidence.",
151
169
  "S3 bucket KMS, versioning, lifecycle, and public-access-block evidence.",
152
170
  "Spark01 private-probe registration, key-file mode, heartbeat, and revocation evidence."
153
171
  ],
@@ -157,7 +175,9 @@ function buildAwsDeploymentPlan(options = {}) {
157
175
  hostedLocalSqliteAllowed: false,
158
176
  notes: [
159
177
  "This plan generator does not call AWS.",
160
- "Hosted runtime must use Postgres; SQLite remains local/dev fallback only.",
178
+ "Blocked plan output intentionally avoids copy-pastable AWS mutation commands.",
179
+ "Hosted runtime uses explicit EFS-backed SQLite at HASNA_UPTIME_HOSTED_SQLITE_DB until the async Postgres adapter exists.",
180
+ "Do not set HASNA_UPTIME_DATABASE_URL for hosted tasks until the Postgres adapter is implemented.",
161
181
  "Secrets are represented as secret names/refs and must be injected with valueFrom.",
162
182
  "Actual deploy belongs in the deploy_release_operate_final goal node after infra review."
163
183
  ]
@@ -218,7 +238,7 @@ function buildSpark01CloudConfig(options = {}) {
218
238
  privateKeyInline: false,
219
239
  tokenInline: false,
220
240
  notes: [
221
- "This config is cloud-primary: Spark01 submits to hosted API state instead of local SQLite.",
241
+ "This config is hosted-targeted preflight: Spark01 must not start until cloud probe routes are backed by hosted state.",
222
242
  "The private key file path is referenced, not embedded.",
223
243
  "Hosted token or probe auth material must come from the machine secret store, not this generated config."
224
244
  ]
@@ -239,7 +259,8 @@ function servicePlan(prefix, stage, role, desiredCount, image, workspaceId, secr
239
259
  return {
240
260
  name,
241
261
  role,
242
- desiredCount,
262
+ desiredCount: 0,
263
+ targetDesiredCount: desiredCount,
243
264
  taskRole: `${name}-task-role`,
244
265
  executionRole: `${prefix}-${stage}-execution-role`,
245
266
  logGroup: `/ecs/${name}`,
@@ -248,7 +269,7 @@ function servicePlan(prefix, stage, role, desiredCount, image, workspaceId, secr
248
269
  HASNA_UPTIME_IMAGE: image,
249
270
  ...environment
250
271
  },
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 }
272
+ 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 }
252
273
  };
253
274
  }
254
275
  function clean(value, fallback) {
package/dist/index.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 {
@@ -3791,14 +3821,14 @@ class ApiError extends Error {
3791
3821
  }
3792
3822
 
3793
3823
  // src/cloud-plan.ts
3794
- var DEFAULT_ACCOUNT = "hasna-xyz-infra";
3824
+ var DEFAULT_ACCOUNT = "aws-profile";
3795
3825
  var DEFAULT_REGION = "us-east-1";
3796
3826
  var DEFAULT_STAGE = "prod";
3797
3827
  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";
3828
+ var DEFAULT_HOSTNAME = "uptime.example.com";
3829
+ var DEFAULT_WORKSPACE_ID = "workspace-id";
3830
+ var DEFAULT_VPC_ID = "vpc-xxxxxxxx";
3831
+ var DEFAULT_HOSTED_SQLITE_DB = "/data/uptime/uptime.db";
3802
3832
  function buildAwsDeploymentPlan(options = {}) {
3803
3833
  const region = clean(options.region, DEFAULT_REGION);
3804
3834
  const stage = clean(options.stage, DEFAULT_STAGE);
@@ -3806,36 +3836,39 @@ function buildAwsDeploymentPlan(options = {}) {
3806
3836
  const accountName = clean(options.accountName, DEFAULT_ACCOUNT);
3807
3837
  const hostname = clean(options.hostname, DEFAULT_HOSTNAME);
3808
3838
  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>`);
3839
+ const ecrRepository = clean(options.ecrRepository, prefix);
3840
+ const imageRepositoryUri = `<account-id>.dkr.ecr.${region}.amazonaws.com/${ecrRepository}`;
3841
+ const image = clean(options.image, `${imageRepositoryUri}@sha256:<image-digest>`);
3811
3842
  const evidenceBucket = clean(options.evidenceBucket, `hasna-${stage}-${prefix}-evidence`);
3843
+ const hostedSqliteDbPath = clean(options.hostedSqliteDbPath, DEFAULT_HOSTED_SQLITE_DB);
3844
+ const runtimePackageVersion = clean(options.runtimePackageVersion, "0.1.7");
3812
3845
  const cluster = `${prefix}-${stage}`;
3813
3846
  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`)
3847
+ appEnv: clean(options.appEnvSecretName, `open-uptime/${stage}/app/env`),
3848
+ hostedToken: clean(options.hostedTokenSecretName, `open-uptime/${stage}/hosted-token`),
3849
+ publicProbe: clean(options.publicProbeSecretName, `open-uptime/${stage}/probe/public`),
3850
+ privateProbe: clean(options.privateProbeSecretName, `open-uptime/${stage}/probe/private`),
3851
+ reporting: clean(options.reportingSecretName, `open-uptime/${stage}/reporting`)
3820
3852
  };
3821
3853
  const services = [
3822
- servicePlan(prefix, stage, "web", 2, image, workspaceId, secrets, {
3854
+ servicePlan(prefix, stage, "web", 1, image, workspaceId, secrets, {
3823
3855
  HASNA_UPTIME_MODE: "hosted",
3856
+ HASNA_UPTIME_HOSTED_SQLITE_DB: hostedSqliteDbPath,
3824
3857
  HASNA_UPTIME_WORKSPACE_ID: workspaceId,
3825
3858
  HASNA_UPTIME_HOSTNAME: hostname
3826
3859
  }),
3827
- servicePlan(prefix, stage, "scheduler", 1, image, workspaceId, secrets, {
3860
+ servicePlan(prefix, stage, "scheduler", 0, image, workspaceId, secrets, {
3828
3861
  HASNA_UPTIME_MODE: "hosted",
3829
3862
  HASNA_UPTIME_WORKSPACE_ID: workspaceId,
3830
3863
  HASNA_UPTIME_COMPONENT: "scheduler"
3831
3864
  }),
3832
- servicePlan(prefix, stage, "public-probe", 1, image, workspaceId, secrets, {
3865
+ servicePlan(prefix, stage, "public-probe", 0, image, workspaceId, secrets, {
3833
3866
  HASNA_UPTIME_MODE: "hosted",
3834
3867
  HASNA_UPTIME_WORKSPACE_ID: workspaceId,
3835
3868
  HASNA_UPTIME_COMPONENT: "public-probe",
3836
3869
  HASNA_UPTIME_PROBE_LOCATION: region
3837
3870
  }),
3838
- servicePlan(prefix, stage, "reporter", 1, image, workspaceId, secrets, {
3871
+ servicePlan(prefix, stage, "reporter", 0, image, workspaceId, secrets, {
3839
3872
  HASNA_UPTIME_MODE: "hosted",
3840
3873
  HASNA_UPTIME_WORKSPACE_ID: workspaceId,
3841
3874
  HASNA_UPTIME_COMPONENT: "reporter"
@@ -3848,7 +3881,7 @@ function buildAwsDeploymentPlan(options = {}) {
3848
3881
  ];
3849
3882
  return {
3850
3883
  kind: "open-uptime.aws-deployment-plan",
3851
- version: 1,
3884
+ version: 2,
3852
3885
  generatedAt: new Date().toISOString(),
3853
3886
  status: "blocked",
3854
3887
  canApply: false,
@@ -3861,10 +3894,13 @@ function buildAwsDeploymentPlan(options = {}) {
3861
3894
  mode: "hosted",
3862
3895
  resources: {
3863
3896
  ecrRepository,
3897
+ imageBuilder: `${prefix}-${stage}-image-builder`,
3864
3898
  ecsCluster: cluster,
3865
3899
  services,
3866
3900
  vpcId: clean(options.vpcId, DEFAULT_VPC_ID),
3867
- rdsInstanceId: clean(options.rdsInstanceId, DEFAULT_RDS),
3901
+ efsFileSystem: `${prefix}-${stage}-data`,
3902
+ efsAccessPoint: `${prefix}-${stage}-uptime`,
3903
+ hostedSqliteDbPath,
3868
3904
  evidenceBucket,
3869
3905
  loadBalancer: `${prefix}-${stage}-alb`,
3870
3906
  targetGroups: [`${prefix}-${stage}-web-tg`],
@@ -3873,42 +3909,54 @@ function buildAwsDeploymentPlan(options = {}) {
3873
3909
  `${prefix}-${stage}-web-sg`,
3874
3910
  `${prefix}-${stage}-scheduler-sg`,
3875
3911
  `${prefix}-${stage}-public-probe-sg`,
3876
- `${prefix}-${stage}-rds-client-sg`
3912
+ `${prefix}-${stage}-reporter-sg`,
3913
+ `${prefix}-${stage}-migration-sg`,
3914
+ `${prefix}-${stage}-efs-sg`
3877
3915
  ],
3878
3916
  secrets,
3879
3917
  logGroups: services.map((service) => service.logGroup),
3880
3918
  alarms: [
3881
3919
  `${prefix}-${stage}-web-5xx`,
3882
- `${prefix}-${stage}-scheduler-stalled`,
3883
- `${prefix}-${stage}-probe-stale`,
3884
- `${prefix}-${stage}-report-delivery-failures`
3920
+ `${prefix}-${stage}-web-unhealthy`
3885
3921
  ]
3886
3922
  },
3887
3923
  image: {
3888
3924
  repository: ecrRepository,
3889
3925
  uri: image,
3890
- buildCommand: "BLOCKED: add a reviewed Dockerfile/container build target before running docker build",
3926
+ dockerfile: "Dockerfile.package",
3927
+ buildCommand: `BLOCKED: after infra approval, AWS CodeBuild builds Dockerfile.package from @hasna/uptime@${runtimePackageVersion} into ${imageRepositoryUri}`,
3891
3928
  pushCommands: [
3892
- "BLOCKED: push only from approved CI/CD after the ECR repository and image digest policy exist",
3929
+ `BLOCKED: start ${prefix}-${stage}-image-builder only through the approved deploy pipeline after @hasna/uptime@${runtimePackageVersion} is published`,
3893
3930
  "BLOCKED: deploy services by immutable image digest, not by mutable tags"
3894
3931
  ]
3895
3932
  },
3933
+ infra: {
3934
+ path: "infra/aws",
3935
+ fmtCommand: "terraform -chdir=infra/aws fmt -check",
3936
+ initCommand: "terraform -chdir=infra/aws init -backend=false",
3937
+ validateCommand: "terraform -chdir=infra/aws validate",
3938
+ planCommand: "terraform -chdir=infra/aws plan -out open-uptime.tfplan",
3939
+ applyAllowed: false
3940
+ },
3896
3941
  runbook: {
3897
3942
  preflight: [
3898
3943
  `aws sts get-caller-identity --profile ${accountName}`,
3899
- `aws rds describe-db-instances --db-instance-identifier ${clean(options.rdsInstanceId, DEFAULT_RDS)} --region ${region}`,
3900
3944
  `aws ec2 describe-vpcs --vpc-ids ${clean(options.vpcId, DEFAULT_VPC_ID)} --region ${region}`,
3945
+ `aws efs describe-file-systems --region ${region}`,
3901
3946
  "Confirm the infra repository and Terraform/CloudFormation owner before live mutation."
3902
3947
  ],
3903
3948
  provision: [
3904
3949
  `Infra PR must declare or update ECR repository ${ecrRepository}.`,
3950
+ `Infra PR must declare CodeBuild image builder ${prefix}-${stage}-image-builder for @hasna/uptime@${runtimePackageVersion}.`,
3905
3951
  `Infra PR must declare hardened S3 evidence bucket ${evidenceBucket} with KMS, versioning, lifecycle, and public access block.`,
3952
+ `Infra PR must declare encrypted EFS ${prefix}-${stage}-data with access point, mount targets, and AWS Backup plan.`,
3906
3953
  `Infra PR must declare ECS/Fargate cluster ${cluster}, ALB, target groups, security groups, IAM roles, CloudWatch log groups, and Secrets Manager refs.`,
3907
3954
  "Only apply the infra plan from the approved infrastructure repository after review evidence is attached."
3908
3955
  ],
3909
3956
  deploy: [
3910
3957
  "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.",
3958
+ `Start the AWS image builder for @hasna/uptime@${runtimePackageVersion} and record the pushed image digest.`,
3959
+ "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.",
3912
3960
  `Register task definitions for ${services.map((service) => service.name).join(", ")} using valueFrom secrets.`,
3913
3961
  `Update ECS services in cluster ${cluster} one component at a time through the approved deploy pipeline.`,
3914
3962
  `Create Route53/edge record for ${hostname} only after ALB health checks pass and auth denial smokes succeed.`
@@ -3917,7 +3965,7 @@ function buildAwsDeploymentPlan(options = {}) {
3917
3965
  "Keep previous task definition ARNs before each service update.",
3918
3966
  "Rollback through the approved deploy pipeline to the previously recorded task definition ARNs.",
3919
3967
  "Disable scheduler/reporter services before data rollback.",
3920
- "Restore RDS snapshot only after explicit operator approval and audit record."
3968
+ "Restore EFS backup recovery point only after explicit operator approval and audit record."
3921
3969
  ],
3922
3970
  spark01: [
3923
3971
  "Create a private probe identity with a caller-managed public key.",
@@ -3926,19 +3974,19 @@ function buildAwsDeploymentPlan(options = {}) {
3926
3974
  ]
3927
3975
  },
3928
3976
  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.",
3977
+ "The infrastructure owner repository was not found in this workspace.",
3978
+ "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.",
3932
3979
  "Hosted production auth/RBAC must replace broad static hosted-token operation before exposure.",
3933
3980
  "Public probe execution still needs DNS, redirect, and rebinding SSRF enforcement plus cloud check-job leases.",
3934
3981
  "Spark01 hosted probe enrollment, claim, submit, heartbeat, revocation, and rotation are not cloud-backed yet."
3935
3982
  ],
3936
3983
  requiredEvidence: [
3937
3984
  "Infrastructure PR/synth/plan from the approved infra repository.",
3938
- "Container build smoke and immutable image digest.",
3985
+ "CodeBuild image-builder run, container smoke, and immutable image digest.",
3939
3986
  "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.",
3987
+ "ALB/TLS/DNS/auth denial smokes and web alarm checks.",
3988
+ "Single-writer ECS evidence: one web task maximum and no scheduler/public-probe/reporter EFS mounts.",
3989
+ "EFS encryption, access point, mount-target, AWS Backup, and restore-drill evidence.",
3942
3990
  "S3 bucket KMS, versioning, lifecycle, and public-access-block evidence.",
3943
3991
  "Spark01 private-probe registration, key-file mode, heartbeat, and revocation evidence."
3944
3992
  ],
@@ -3948,7 +3996,9 @@ function buildAwsDeploymentPlan(options = {}) {
3948
3996
  hostedLocalSqliteAllowed: false,
3949
3997
  notes: [
3950
3998
  "This plan generator does not call AWS.",
3951
- "Hosted runtime must use Postgres; SQLite remains local/dev fallback only.",
3999
+ "Blocked plan output intentionally avoids copy-pastable AWS mutation commands.",
4000
+ "Hosted runtime uses explicit EFS-backed SQLite at HASNA_UPTIME_HOSTED_SQLITE_DB until the async Postgres adapter exists.",
4001
+ "Do not set HASNA_UPTIME_DATABASE_URL for hosted tasks until the Postgres adapter is implemented.",
3952
4002
  "Secrets are represented as secret names/refs and must be injected with valueFrom.",
3953
4003
  "Actual deploy belongs in the deploy_release_operate_final goal node after infra review."
3954
4004
  ]
@@ -4009,7 +4059,7 @@ function buildSpark01CloudConfig(options = {}) {
4009
4059
  privateKeyInline: false,
4010
4060
  tokenInline: false,
4011
4061
  notes: [
4012
- "This config is cloud-primary: Spark01 submits to hosted API state instead of local SQLite.",
4062
+ "This config is hosted-targeted preflight: Spark01 must not start until cloud probe routes are backed by hosted state.",
4013
4063
  "The private key file path is referenced, not embedded.",
4014
4064
  "Hosted token or probe auth material must come from the machine secret store, not this generated config."
4015
4065
  ]
@@ -4030,7 +4080,8 @@ function servicePlan(prefix, stage, role, desiredCount, image, workspaceId, secr
4030
4080
  return {
4031
4081
  name,
4032
4082
  role,
4033
- desiredCount,
4083
+ desiredCount: 0,
4084
+ targetDesiredCount: desiredCount,
4034
4085
  taskRole: `${name}-task-role`,
4035
4086
  executionRole: `${prefix}-${stage}-execution-role`,
4036
4087
  logGroup: `/ecs/${name}`,
@@ -4039,7 +4090,7 @@ function servicePlan(prefix, stage, role, desiredCount, image, workspaceId, secr
4039
4090
  HASNA_UPTIME_IMAGE: image,
4040
4091
  ...environment
4041
4092
  },
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 }
4093
+ 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 }
4043
4094
  };
4044
4095
  }
4045
4096
  function clean(value, fallback) {