@hasna/uptime 0.1.6 → 0.1.8

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.
@@ -1,13 +1,14 @@
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
+ var DEFAULT_PROTECTED_ACCESS_MODE = "cloudfront_default_domain";
11
12
  function buildAwsDeploymentPlan(options = {}) {
12
13
  const region = clean(options.region, DEFAULT_REGION);
13
14
  const stage = clean(options.stage, DEFAULT_STAGE);
@@ -15,37 +16,42 @@ function buildAwsDeploymentPlan(options = {}) {
15
16
  const accountName = clean(options.accountName, DEFAULT_ACCOUNT);
16
17
  const hostname = clean(options.hostname, DEFAULT_HOSTNAME);
17
18
  const workspaceId = clean(options.workspaceId, DEFAULT_WORKSPACE_ID);
18
- const ecrRepository = clean(options.ecrRepository, `hasna/opensource/${prefix}`);
19
+ const ecrRepository = clean(options.ecrRepository, prefix);
19
20
  const imageRepositoryUri = `<account-id>.dkr.ecr.${region}.amazonaws.com/${ecrRepository}`;
20
21
  const image = clean(options.image, `${imageRepositoryUri}@sha256:<image-digest>`);
21
22
  const evidenceBucket = clean(options.evidenceBucket, `hasna-${stage}-${prefix}-evidence`);
23
+ const hostedSqliteDbPath = clean(options.hostedSqliteDbPath, DEFAULT_HOSTED_SQLITE_DB);
24
+ const runtimePackageVersion = clean(options.runtimePackageVersion, "0.1.8");
25
+ const protectedAccessMode = options.protectedAccessMode ?? DEFAULT_PROTECTED_ACCESS_MODE;
26
+ const protectedAccessUrl = protectedAccessMode === "cloudfront_default_domain" ? "https://<cloudfront-domain>" : `https://${hostname}`;
22
27
  const cluster = `${prefix}-${stage}`;
23
28
  const secrets = {
24
- database: clean(options.databaseSecretName, `hasna/xyz/opensource/uptime/${stage}/rds`),
25
- appEnv: clean(options.appEnvSecretName, `hasna/xyz/opensource/uptime/${stage}/app/env`),
26
- hostedToken: clean(options.hostedTokenSecretName, `hasna/xyz/opensource/uptime/${stage}/hosted-token`),
27
- publicProbe: clean(options.publicProbeSecretName, `hasna/xyz/opensource/uptime/${stage}/probe/public`),
28
- privateProbe: clean(options.privateProbeSecretName, `hasna/xyz/opensource/uptime/${stage}/probe/private`),
29
- reporting: clean(options.reportingSecretName, `hasna/xyz/opensource/uptime/${stage}/reporting`)
29
+ appEnv: clean(options.appEnvSecretName, `open-uptime/${stage}/app/env`),
30
+ hostedToken: clean(options.hostedTokenSecretName, `open-uptime/${stage}/hosted-token`),
31
+ publicProbe: clean(options.publicProbeSecretName, `open-uptime/${stage}/probe/public`),
32
+ privateProbe: clean(options.privateProbeSecretName, `open-uptime/${stage}/probe/private`),
33
+ reporting: clean(options.reportingSecretName, `open-uptime/${stage}/reporting`)
30
34
  };
31
35
  const services = [
32
- servicePlan(prefix, stage, "web", 2, image, workspaceId, secrets, {
36
+ servicePlan(prefix, stage, "web", 1, image, workspaceId, secrets, {
33
37
  HASNA_UPTIME_MODE: "hosted",
38
+ HASNA_UPTIME_HOSTED_SQLITE_DB: hostedSqliteDbPath,
34
39
  HASNA_UPTIME_WORKSPACE_ID: workspaceId,
35
- HASNA_UPTIME_HOSTNAME: hostname
40
+ HASNA_UPTIME_HOSTNAME: hostname,
41
+ HASNA_UPTIME_ALLOWED_ORIGINS: protectedAccessUrl
36
42
  }),
37
- servicePlan(prefix, stage, "scheduler", 1, image, workspaceId, secrets, {
43
+ servicePlan(prefix, stage, "scheduler", 0, image, workspaceId, secrets, {
38
44
  HASNA_UPTIME_MODE: "hosted",
39
45
  HASNA_UPTIME_WORKSPACE_ID: workspaceId,
40
46
  HASNA_UPTIME_COMPONENT: "scheduler"
41
47
  }),
42
- servicePlan(prefix, stage, "public-probe", 1, image, workspaceId, secrets, {
48
+ servicePlan(prefix, stage, "public-probe", 0, image, workspaceId, secrets, {
43
49
  HASNA_UPTIME_MODE: "hosted",
44
50
  HASNA_UPTIME_WORKSPACE_ID: workspaceId,
45
51
  HASNA_UPTIME_COMPONENT: "public-probe",
46
52
  HASNA_UPTIME_PROBE_LOCATION: region
47
53
  }),
48
- servicePlan(prefix, stage, "reporter", 1, image, workspaceId, secrets, {
54
+ servicePlan(prefix, stage, "reporter", 0, image, workspaceId, secrets, {
49
55
  HASNA_UPTIME_MODE: "hosted",
50
56
  HASNA_UPTIME_WORKSPACE_ID: workspaceId,
51
57
  HASNA_UPTIME_COMPONENT: "reporter"
@@ -58,7 +64,7 @@ function buildAwsDeploymentPlan(options = {}) {
58
64
  ];
59
65
  return {
60
66
  kind: "open-uptime.aws-deployment-plan",
61
- version: 1,
67
+ version: 3,
62
68
  generatedAt: new Date().toISOString(),
63
69
  status: "blocked",
64
70
  canApply: false,
@@ -71,12 +77,18 @@ function buildAwsDeploymentPlan(options = {}) {
71
77
  mode: "hosted",
72
78
  resources: {
73
79
  ecrRepository,
80
+ imageBuilder: `${prefix}-${stage}-image-builder`,
74
81
  ecsCluster: cluster,
75
82
  services,
76
83
  vpcId: clean(options.vpcId, DEFAULT_VPC_ID),
77
- rdsInstanceId: clean(options.rdsInstanceId, DEFAULT_RDS),
84
+ efsFileSystem: `${prefix}-${stage}-data`,
85
+ efsAccessPoint: `${prefix}-${stage}-uptime`,
86
+ hostedSqliteDbPath,
78
87
  evidenceBucket,
79
88
  loadBalancer: `${prefix}-${stage}-alb`,
89
+ protectedAccessMode,
90
+ edgeDistribution: protectedAccessMode === "cloudfront_default_domain" ? `${prefix}-${stage}-edge` : undefined,
91
+ protectedAccessUrl,
80
92
  targetGroups: [`${prefix}-${stage}-web-tg`],
81
93
  securityGroups: [
82
94
  `${prefix}-${stage}-alb-sg`,
@@ -84,7 +96,8 @@ function buildAwsDeploymentPlan(options = {}) {
84
96
  `${prefix}-${stage}-scheduler-sg`,
85
97
  `${prefix}-${stage}-public-probe-sg`,
86
98
  `${prefix}-${stage}-reporter-sg`,
87
- `${prefix}-${stage}-migration-sg`
99
+ `${prefix}-${stage}-migration-sg`,
100
+ `${prefix}-${stage}-efs-sg`
88
101
  ],
89
102
  secrets,
90
103
  logGroups: services.map((service) => service.logGroup),
@@ -96,10 +109,10 @@ function buildAwsDeploymentPlan(options = {}) {
96
109
  image: {
97
110
  repository: ecrRepository,
98
111
  uri: image,
99
- dockerfile: "Dockerfile",
100
- buildCommand: `docker build --pull -t ${imageRepositoryUri}:<git-sha> .`,
112
+ dockerfile: "Dockerfile.package",
113
+ buildCommand: `BLOCKED: after infra approval, AWS CodeBuild builds Dockerfile.package from @hasna/uptime@${runtimePackageVersion} into ${imageRepositoryUri}`,
101
114
  pushCommands: [
102
- "BLOCKED: push only from approved CI/CD after the ECR repository and image digest policy exist",
115
+ `BLOCKED: start ${prefix}-${stage}-image-builder only through the approved deploy pipeline after @hasna/uptime@${runtimePackageVersion} is published`,
103
116
  "BLOCKED: deploy services by immutable image digest, not by mutable tags"
104
117
  ]
105
118
  },
@@ -114,28 +127,31 @@ function buildAwsDeploymentPlan(options = {}) {
114
127
  runbook: {
115
128
  preflight: [
116
129
  `aws sts get-caller-identity --profile ${accountName}`,
117
- `aws rds describe-db-instances --db-instance-identifier ${clean(options.rdsInstanceId, DEFAULT_RDS)} --region ${region}`,
118
130
  `aws ec2 describe-vpcs --vpc-ids ${clean(options.vpcId, DEFAULT_VPC_ID)} --region ${region}`,
131
+ `aws efs describe-file-systems --region ${region}`,
119
132
  "Confirm the infra repository and Terraform/CloudFormation owner before live mutation."
120
133
  ],
121
134
  provision: [
122
135
  `Infra PR must declare or update ECR repository ${ecrRepository}.`,
136
+ `Infra PR must declare CodeBuild image builder ${prefix}-${stage}-image-builder for @hasna/uptime@${runtimePackageVersion}.`,
123
137
  `Infra PR must declare hardened S3 evidence bucket ${evidenceBucket} with KMS, versioning, lifecycle, and public access block.`,
124
- `Infra PR must declare ECS/Fargate cluster ${cluster}, ALB, target groups, security groups, IAM roles, CloudWatch log groups, and Secrets Manager refs.`,
138
+ `Infra PR must declare encrypted EFS ${prefix}-${stage}-data with access point, mount targets, and AWS Backup plan.`,
139
+ protectedAccessMode === "cloudfront_default_domain" ? "Infra PR must declare CloudFront default-domain HTTPS edge, ALB HTTP listener restricted to CloudFront origin-facing ranges, ECS/Fargate cluster, target groups, security groups, IAM roles, CloudWatch log groups, and Secrets Manager refs." : `Infra PR must declare ECS/Fargate cluster ${cluster}, ALB HTTPS listener, target groups, security groups, IAM roles, CloudWatch log groups, and Secrets Manager refs.`,
125
140
  "Only apply the infra plan from the approved infrastructure repository after review evidence is attached."
126
141
  ],
127
142
  deploy: [
128
143
  "Build and publish the image only after the Dockerfile/container target is reviewed.",
129
- "Run the migration task with the migrator role before web/scheduler/probe services.",
144
+ `Start the AWS image builder for @hasna/uptime@${runtimePackageVersion} and record the pushed image digest.`,
145
+ "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.",
130
146
  `Register task definitions for ${services.map((service) => service.name).join(", ")} using valueFrom secrets.`,
131
147
  `Update ECS services in cluster ${cluster} one component at a time through the approved deploy pipeline.`,
132
- `Create Route53/edge record for ${hostname} only after ALB health checks pass and auth denial smokes succeed.`
148
+ protectedAccessMode === "cloudfront_default_domain" ? "Use the CloudFront default HTTPS domain for first protected access; add custom DNS/certificate only after edge ownership is approved." : `Create Route53/edge record for ${hostname} only after ALB health checks pass and auth denial smokes succeed.`
133
149
  ],
134
150
  rollback: [
135
151
  "Keep previous task definition ARNs before each service update.",
136
152
  "Rollback through the approved deploy pipeline to the previously recorded task definition ARNs.",
137
153
  "Disable scheduler/reporter services before data rollback.",
138
- "Restore RDS snapshot only after explicit operator approval and audit record."
154
+ "Restore EFS backup recovery point only after explicit operator approval and audit record."
139
155
  ],
140
156
  spark01: [
141
157
  "Create a private probe identity with a caller-managed public key.",
@@ -144,18 +160,19 @@ function buildAwsDeploymentPlan(options = {}) {
144
160
  ]
145
161
  },
146
162
  blockers: [
147
- "The hasna-xyz-infra infrastructure owner repository was not found in this workspace.",
148
- "Hosted Postgres storage adapter and migrations are not implemented.",
163
+ "The infrastructure owner repository was not found in this workspace.",
164
+ "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.",
149
165
  "Hosted production auth/RBAC must replace broad static hosted-token operation before exposure.",
150
166
  "Public probe execution still needs DNS, redirect, and rebinding SSRF enforcement plus cloud check-job leases.",
151
167
  "Spark01 hosted probe enrollment, claim, submit, heartbeat, revocation, and rotation are not cloud-backed yet."
152
168
  ],
153
169
  requiredEvidence: [
154
170
  "Infrastructure PR/synth/plan from the approved infra repository.",
155
- "Container build smoke and immutable image digest.",
171
+ "CodeBuild image-builder run, container smoke, and immutable image digest.",
156
172
  "ECS task definitions using secrets.valueFrom only.",
157
- "ALB/TLS/DNS/auth denial smokes and web alarm checks.",
158
- "RDS TLS, backups/PITR, scoped roles, and migration dry-run evidence.",
173
+ "CloudFront-default-domain or ALB TLS auth-denial smokes, direct-origin denial evidence, and web alarm checks.",
174
+ "Single-writer ECS evidence: one web task maximum and no scheduler/public-probe/reporter EFS mounts.",
175
+ "EFS encryption, access point, mount-target, AWS Backup, and restore-drill evidence.",
159
176
  "S3 bucket KMS, versioning, lifecycle, and public-access-block evidence.",
160
177
  "Spark01 private-probe registration, key-file mode, heartbeat, and revocation evidence."
161
178
  ],
@@ -165,7 +182,10 @@ function buildAwsDeploymentPlan(options = {}) {
165
182
  hostedLocalSqliteAllowed: false,
166
183
  notes: [
167
184
  "This plan generator does not call AWS.",
168
- "Hosted runtime must use Postgres; SQLite remains local/dev fallback only.",
185
+ "Blocked plan output intentionally avoids copy-pastable AWS mutation commands.",
186
+ "Default protected access uses CloudFront's HTTPS default domain so first deploy is not blocked on custom DNS or ACM.",
187
+ "Hosted runtime uses explicit EFS-backed SQLite at HASNA_UPTIME_HOSTED_SQLITE_DB until the async Postgres adapter exists.",
188
+ "Do not set HASNA_UPTIME_DATABASE_URL for hosted tasks until the Postgres adapter is implemented.",
169
189
  "Secrets are represented as secret names/refs and must be injected with valueFrom.",
170
190
  "Actual deploy belongs in the deploy_release_operate_final goal node after infra review."
171
191
  ]
@@ -257,7 +277,7 @@ function servicePlan(prefix, stage, role, desiredCount, image, workspaceId, secr
257
277
  HASNA_UPTIME_IMAGE: image,
258
278
  ...environment
259
279
  },
260
- secrets: role === "web" ? { HASNA_UPTIME_DATABASE_URL: secrets.database, APP_ENV: secrets.appEnv, HASNA_UPTIME_HOSTED_TOKEN: secrets.hostedToken } : role === "public-probe" ? { PROBE_CONFIG: secrets.publicProbe } : role === "reporter" ? { HASNA_UPTIME_DATABASE_URL: secrets.database, REPORTING_CONFIG: secrets.reporting } : { HASNA_UPTIME_DATABASE_URL: secrets.database, APP_ENV: secrets.appEnv }
280
+ 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 }
261
281
  };
262
282
  }
263
283
  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 {
@@ -3480,6 +3510,7 @@ function serveUptime(options = {}) {
3480
3510
  apiToken: options.apiToken,
3481
3511
  hostedToken: options.hostedToken,
3482
3512
  hostedTokens: options.hostedTokens,
3513
+ hostedAllowedOrigins: options.hostedAllowedOrigins,
3483
3514
  allowUnsafeRemoteMutations: options.allowUnsafeRemoteMutations,
3484
3515
  trustedLoopback: isLoopbackHost(options.host ?? "127.0.0.1"),
3485
3516
  mode
@@ -3539,13 +3570,23 @@ async function handleHostedRequest(service, request, url, options) {
3539
3570
  const scope = hostedScopeFor(request.method, apiPath);
3540
3571
  requireHostedActor(request, url, options, scope);
3541
3572
  if (["POST", "PATCH", "DELETE"].includes(request.method)) {
3542
- const origin = request.headers.get("origin");
3543
- if (origin && origin !== `${url.protocol}//${url.host}`) {
3544
- throw new ApiError("cross-origin mutation rejected", 403);
3545
- }
3573
+ validateHostedMutationOrigin(request, url, options);
3546
3574
  }
3547
3575
  return handleApiRoute(service, request, url, apiPath, options, true);
3548
3576
  }
3577
+ function validateHostedMutationOrigin(request, url, options) {
3578
+ const rawOrigin = request.headers.get("origin");
3579
+ const origin = normalizeOrigin(rawOrigin);
3580
+ if (rawOrigin && !origin) {
3581
+ throw new ApiError("cross-origin mutation rejected", 403);
3582
+ }
3583
+ if (!origin)
3584
+ return;
3585
+ const allowedOrigins = new Set([`${url.protocol}//${url.host}`, ...resolveHostedAllowedOrigins(options)]);
3586
+ if (!allowedOrigins.has(origin)) {
3587
+ throw new ApiError("cross-origin mutation rejected", 403);
3588
+ }
3589
+ }
3549
3590
  async function handleApiRoute(service, request, url, apiPath, options, hosted) {
3550
3591
  if (request.method === "GET" && apiPath === "/api/summary") {
3551
3592
  return json(service.summary());
@@ -3764,6 +3805,34 @@ function resolveHostedTokens(options) {
3764
3805
  workspaceId: process.env.HASNA_UPTIME_WORKSPACE_ID ?? "default"
3765
3806
  }];
3766
3807
  }
3808
+ function resolveHostedAllowedOrigins(options) {
3809
+ const configured = options.hostedAllowedOrigins ?? splitCsv(process.env.HASNA_UPTIME_ALLOWED_ORIGINS);
3810
+ return configured.map((origin) => normalizeAllowedOrigin(origin)).filter((origin) => Boolean(origin));
3811
+ }
3812
+ function splitCsv(value) {
3813
+ if (!value)
3814
+ return [];
3815
+ return value.split(",").map((entry) => entry.trim()).filter(Boolean);
3816
+ }
3817
+ function normalizeAllowedOrigin(value) {
3818
+ const origin = normalizeOrigin(value);
3819
+ if (!origin) {
3820
+ throw new ApiError(`invalid hosted allowed origin: ${value}`, 500);
3821
+ }
3822
+ return origin;
3823
+ }
3824
+ function normalizeOrigin(value) {
3825
+ if (!value?.trim())
3826
+ return;
3827
+ try {
3828
+ const parsed = new URL(value.trim());
3829
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:")
3830
+ return;
3831
+ return `${parsed.protocol}//${parsed.host}`;
3832
+ } catch {
3833
+ return;
3834
+ }
3835
+ }
3767
3836
  function safeTokenEqual(candidate, expected) {
3768
3837
  if (!candidate)
3769
3838
  return false;
@@ -3791,14 +3860,15 @@ class ApiError extends Error {
3791
3860
  }
3792
3861
 
3793
3862
  // src/cloud-plan.ts
3794
- var DEFAULT_ACCOUNT = "hasna-xyz-infra";
3863
+ var DEFAULT_ACCOUNT = "aws-profile";
3795
3864
  var DEFAULT_REGION = "us-east-1";
3796
3865
  var DEFAULT_STAGE = "prod";
3797
3866
  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";
3867
+ var DEFAULT_HOSTNAME = "uptime.example.com";
3868
+ var DEFAULT_WORKSPACE_ID = "workspace-id";
3869
+ var DEFAULT_VPC_ID = "vpc-xxxxxxxx";
3870
+ var DEFAULT_HOSTED_SQLITE_DB = "/data/uptime/uptime.db";
3871
+ var DEFAULT_PROTECTED_ACCESS_MODE = "cloudfront_default_domain";
3802
3872
  function buildAwsDeploymentPlan(options = {}) {
3803
3873
  const region = clean(options.region, DEFAULT_REGION);
3804
3874
  const stage = clean(options.stage, DEFAULT_STAGE);
@@ -3806,37 +3876,42 @@ function buildAwsDeploymentPlan(options = {}) {
3806
3876
  const accountName = clean(options.accountName, DEFAULT_ACCOUNT);
3807
3877
  const hostname = clean(options.hostname, DEFAULT_HOSTNAME);
3808
3878
  const workspaceId = clean(options.workspaceId, DEFAULT_WORKSPACE_ID);
3809
- const ecrRepository = clean(options.ecrRepository, `hasna/opensource/${prefix}`);
3879
+ const ecrRepository = clean(options.ecrRepository, prefix);
3810
3880
  const imageRepositoryUri = `<account-id>.dkr.ecr.${region}.amazonaws.com/${ecrRepository}`;
3811
3881
  const image = clean(options.image, `${imageRepositoryUri}@sha256:<image-digest>`);
3812
3882
  const evidenceBucket = clean(options.evidenceBucket, `hasna-${stage}-${prefix}-evidence`);
3883
+ const hostedSqliteDbPath = clean(options.hostedSqliteDbPath, DEFAULT_HOSTED_SQLITE_DB);
3884
+ const runtimePackageVersion = clean(options.runtimePackageVersion, "0.1.8");
3885
+ const protectedAccessMode = options.protectedAccessMode ?? DEFAULT_PROTECTED_ACCESS_MODE;
3886
+ const protectedAccessUrl = protectedAccessMode === "cloudfront_default_domain" ? "https://<cloudfront-domain>" : `https://${hostname}`;
3813
3887
  const cluster = `${prefix}-${stage}`;
3814
3888
  const secrets = {
3815
- database: clean(options.databaseSecretName, `hasna/xyz/opensource/uptime/${stage}/rds`),
3816
- appEnv: clean(options.appEnvSecretName, `hasna/xyz/opensource/uptime/${stage}/app/env`),
3817
- hostedToken: clean(options.hostedTokenSecretName, `hasna/xyz/opensource/uptime/${stage}/hosted-token`),
3818
- publicProbe: clean(options.publicProbeSecretName, `hasna/xyz/opensource/uptime/${stage}/probe/public`),
3819
- privateProbe: clean(options.privateProbeSecretName, `hasna/xyz/opensource/uptime/${stage}/probe/private`),
3820
- reporting: clean(options.reportingSecretName, `hasna/xyz/opensource/uptime/${stage}/reporting`)
3889
+ appEnv: clean(options.appEnvSecretName, `open-uptime/${stage}/app/env`),
3890
+ hostedToken: clean(options.hostedTokenSecretName, `open-uptime/${stage}/hosted-token`),
3891
+ publicProbe: clean(options.publicProbeSecretName, `open-uptime/${stage}/probe/public`),
3892
+ privateProbe: clean(options.privateProbeSecretName, `open-uptime/${stage}/probe/private`),
3893
+ reporting: clean(options.reportingSecretName, `open-uptime/${stage}/reporting`)
3821
3894
  };
3822
3895
  const services = [
3823
- servicePlan(prefix, stage, "web", 2, image, workspaceId, secrets, {
3896
+ servicePlan(prefix, stage, "web", 1, image, workspaceId, secrets, {
3824
3897
  HASNA_UPTIME_MODE: "hosted",
3898
+ HASNA_UPTIME_HOSTED_SQLITE_DB: hostedSqliteDbPath,
3825
3899
  HASNA_UPTIME_WORKSPACE_ID: workspaceId,
3826
- HASNA_UPTIME_HOSTNAME: hostname
3900
+ HASNA_UPTIME_HOSTNAME: hostname,
3901
+ HASNA_UPTIME_ALLOWED_ORIGINS: protectedAccessUrl
3827
3902
  }),
3828
- servicePlan(prefix, stage, "scheduler", 1, image, workspaceId, secrets, {
3903
+ servicePlan(prefix, stage, "scheduler", 0, image, workspaceId, secrets, {
3829
3904
  HASNA_UPTIME_MODE: "hosted",
3830
3905
  HASNA_UPTIME_WORKSPACE_ID: workspaceId,
3831
3906
  HASNA_UPTIME_COMPONENT: "scheduler"
3832
3907
  }),
3833
- servicePlan(prefix, stage, "public-probe", 1, image, workspaceId, secrets, {
3908
+ servicePlan(prefix, stage, "public-probe", 0, image, workspaceId, secrets, {
3834
3909
  HASNA_UPTIME_MODE: "hosted",
3835
3910
  HASNA_UPTIME_WORKSPACE_ID: workspaceId,
3836
3911
  HASNA_UPTIME_COMPONENT: "public-probe",
3837
3912
  HASNA_UPTIME_PROBE_LOCATION: region
3838
3913
  }),
3839
- servicePlan(prefix, stage, "reporter", 1, image, workspaceId, secrets, {
3914
+ servicePlan(prefix, stage, "reporter", 0, image, workspaceId, secrets, {
3840
3915
  HASNA_UPTIME_MODE: "hosted",
3841
3916
  HASNA_UPTIME_WORKSPACE_ID: workspaceId,
3842
3917
  HASNA_UPTIME_COMPONENT: "reporter"
@@ -3849,7 +3924,7 @@ function buildAwsDeploymentPlan(options = {}) {
3849
3924
  ];
3850
3925
  return {
3851
3926
  kind: "open-uptime.aws-deployment-plan",
3852
- version: 1,
3927
+ version: 3,
3853
3928
  generatedAt: new Date().toISOString(),
3854
3929
  status: "blocked",
3855
3930
  canApply: false,
@@ -3862,12 +3937,18 @@ function buildAwsDeploymentPlan(options = {}) {
3862
3937
  mode: "hosted",
3863
3938
  resources: {
3864
3939
  ecrRepository,
3940
+ imageBuilder: `${prefix}-${stage}-image-builder`,
3865
3941
  ecsCluster: cluster,
3866
3942
  services,
3867
3943
  vpcId: clean(options.vpcId, DEFAULT_VPC_ID),
3868
- rdsInstanceId: clean(options.rdsInstanceId, DEFAULT_RDS),
3944
+ efsFileSystem: `${prefix}-${stage}-data`,
3945
+ efsAccessPoint: `${prefix}-${stage}-uptime`,
3946
+ hostedSqliteDbPath,
3869
3947
  evidenceBucket,
3870
3948
  loadBalancer: `${prefix}-${stage}-alb`,
3949
+ protectedAccessMode,
3950
+ edgeDistribution: protectedAccessMode === "cloudfront_default_domain" ? `${prefix}-${stage}-edge` : undefined,
3951
+ protectedAccessUrl,
3871
3952
  targetGroups: [`${prefix}-${stage}-web-tg`],
3872
3953
  securityGroups: [
3873
3954
  `${prefix}-${stage}-alb-sg`,
@@ -3875,7 +3956,8 @@ function buildAwsDeploymentPlan(options = {}) {
3875
3956
  `${prefix}-${stage}-scheduler-sg`,
3876
3957
  `${prefix}-${stage}-public-probe-sg`,
3877
3958
  `${prefix}-${stage}-reporter-sg`,
3878
- `${prefix}-${stage}-migration-sg`
3959
+ `${prefix}-${stage}-migration-sg`,
3960
+ `${prefix}-${stage}-efs-sg`
3879
3961
  ],
3880
3962
  secrets,
3881
3963
  logGroups: services.map((service) => service.logGroup),
@@ -3887,10 +3969,10 @@ function buildAwsDeploymentPlan(options = {}) {
3887
3969
  image: {
3888
3970
  repository: ecrRepository,
3889
3971
  uri: image,
3890
- dockerfile: "Dockerfile",
3891
- buildCommand: `docker build --pull -t ${imageRepositoryUri}:<git-sha> .`,
3972
+ dockerfile: "Dockerfile.package",
3973
+ buildCommand: `BLOCKED: after infra approval, AWS CodeBuild builds Dockerfile.package from @hasna/uptime@${runtimePackageVersion} into ${imageRepositoryUri}`,
3892
3974
  pushCommands: [
3893
- "BLOCKED: push only from approved CI/CD after the ECR repository and image digest policy exist",
3975
+ `BLOCKED: start ${prefix}-${stage}-image-builder only through the approved deploy pipeline after @hasna/uptime@${runtimePackageVersion} is published`,
3894
3976
  "BLOCKED: deploy services by immutable image digest, not by mutable tags"
3895
3977
  ]
3896
3978
  },
@@ -3905,28 +3987,31 @@ function buildAwsDeploymentPlan(options = {}) {
3905
3987
  runbook: {
3906
3988
  preflight: [
3907
3989
  `aws sts get-caller-identity --profile ${accountName}`,
3908
- `aws rds describe-db-instances --db-instance-identifier ${clean(options.rdsInstanceId, DEFAULT_RDS)} --region ${region}`,
3909
3990
  `aws ec2 describe-vpcs --vpc-ids ${clean(options.vpcId, DEFAULT_VPC_ID)} --region ${region}`,
3991
+ `aws efs describe-file-systems --region ${region}`,
3910
3992
  "Confirm the infra repository and Terraform/CloudFormation owner before live mutation."
3911
3993
  ],
3912
3994
  provision: [
3913
3995
  `Infra PR must declare or update ECR repository ${ecrRepository}.`,
3996
+ `Infra PR must declare CodeBuild image builder ${prefix}-${stage}-image-builder for @hasna/uptime@${runtimePackageVersion}.`,
3914
3997
  `Infra PR must declare hardened S3 evidence bucket ${evidenceBucket} with KMS, versioning, lifecycle, and public access block.`,
3915
- `Infra PR must declare ECS/Fargate cluster ${cluster}, ALB, target groups, security groups, IAM roles, CloudWatch log groups, and Secrets Manager refs.`,
3998
+ `Infra PR must declare encrypted EFS ${prefix}-${stage}-data with access point, mount targets, and AWS Backup plan.`,
3999
+ protectedAccessMode === "cloudfront_default_domain" ? "Infra PR must declare CloudFront default-domain HTTPS edge, ALB HTTP listener restricted to CloudFront origin-facing ranges, ECS/Fargate cluster, target groups, security groups, IAM roles, CloudWatch log groups, and Secrets Manager refs." : `Infra PR must declare ECS/Fargate cluster ${cluster}, ALB HTTPS listener, target groups, security groups, IAM roles, CloudWatch log groups, and Secrets Manager refs.`,
3916
4000
  "Only apply the infra plan from the approved infrastructure repository after review evidence is attached."
3917
4001
  ],
3918
4002
  deploy: [
3919
4003
  "Build and publish the image only after the Dockerfile/container target is reviewed.",
3920
- "Run the migration task with the migrator role before web/scheduler/probe services.",
4004
+ `Start the AWS image builder for @hasna/uptime@${runtimePackageVersion} and record the pushed image digest.`,
4005
+ "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.",
3921
4006
  `Register task definitions for ${services.map((service) => service.name).join(", ")} using valueFrom secrets.`,
3922
4007
  `Update ECS services in cluster ${cluster} one component at a time through the approved deploy pipeline.`,
3923
- `Create Route53/edge record for ${hostname} only after ALB health checks pass and auth denial smokes succeed.`
4008
+ protectedAccessMode === "cloudfront_default_domain" ? "Use the CloudFront default HTTPS domain for first protected access; add custom DNS/certificate only after edge ownership is approved." : `Create Route53/edge record for ${hostname} only after ALB health checks pass and auth denial smokes succeed.`
3924
4009
  ],
3925
4010
  rollback: [
3926
4011
  "Keep previous task definition ARNs before each service update.",
3927
4012
  "Rollback through the approved deploy pipeline to the previously recorded task definition ARNs.",
3928
4013
  "Disable scheduler/reporter services before data rollback.",
3929
- "Restore RDS snapshot only after explicit operator approval and audit record."
4014
+ "Restore EFS backup recovery point only after explicit operator approval and audit record."
3930
4015
  ],
3931
4016
  spark01: [
3932
4017
  "Create a private probe identity with a caller-managed public key.",
@@ -3935,18 +4020,19 @@ function buildAwsDeploymentPlan(options = {}) {
3935
4020
  ]
3936
4021
  },
3937
4022
  blockers: [
3938
- "The hasna-xyz-infra infrastructure owner repository was not found in this workspace.",
3939
- "Hosted Postgres storage adapter and migrations are not implemented.",
4023
+ "The infrastructure owner repository was not found in this workspace.",
4024
+ "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.",
3940
4025
  "Hosted production auth/RBAC must replace broad static hosted-token operation before exposure.",
3941
4026
  "Public probe execution still needs DNS, redirect, and rebinding SSRF enforcement plus cloud check-job leases.",
3942
4027
  "Spark01 hosted probe enrollment, claim, submit, heartbeat, revocation, and rotation are not cloud-backed yet."
3943
4028
  ],
3944
4029
  requiredEvidence: [
3945
4030
  "Infrastructure PR/synth/plan from the approved infra repository.",
3946
- "Container build smoke and immutable image digest.",
4031
+ "CodeBuild image-builder run, container smoke, and immutable image digest.",
3947
4032
  "ECS task definitions using secrets.valueFrom only.",
3948
- "ALB/TLS/DNS/auth denial smokes and web alarm checks.",
3949
- "RDS TLS, backups/PITR, scoped roles, and migration dry-run evidence.",
4033
+ "CloudFront-default-domain or ALB TLS auth-denial smokes, direct-origin denial evidence, and web alarm checks.",
4034
+ "Single-writer ECS evidence: one web task maximum and no scheduler/public-probe/reporter EFS mounts.",
4035
+ "EFS encryption, access point, mount-target, AWS Backup, and restore-drill evidence.",
3950
4036
  "S3 bucket KMS, versioning, lifecycle, and public-access-block evidence.",
3951
4037
  "Spark01 private-probe registration, key-file mode, heartbeat, and revocation evidence."
3952
4038
  ],
@@ -3956,7 +4042,10 @@ function buildAwsDeploymentPlan(options = {}) {
3956
4042
  hostedLocalSqliteAllowed: false,
3957
4043
  notes: [
3958
4044
  "This plan generator does not call AWS.",
3959
- "Hosted runtime must use Postgres; SQLite remains local/dev fallback only.",
4045
+ "Blocked plan output intentionally avoids copy-pastable AWS mutation commands.",
4046
+ "Default protected access uses CloudFront's HTTPS default domain so first deploy is not blocked on custom DNS or ACM.",
4047
+ "Hosted runtime uses explicit EFS-backed SQLite at HASNA_UPTIME_HOSTED_SQLITE_DB until the async Postgres adapter exists.",
4048
+ "Do not set HASNA_UPTIME_DATABASE_URL for hosted tasks until the Postgres adapter is implemented.",
3960
4049
  "Secrets are represented as secret names/refs and must be injected with valueFrom.",
3961
4050
  "Actual deploy belongs in the deploy_release_operate_final goal node after infra review."
3962
4051
  ]
@@ -4048,7 +4137,7 @@ function servicePlan(prefix, stage, role, desiredCount, image, workspaceId, secr
4048
4137
  HASNA_UPTIME_IMAGE: image,
4049
4138
  ...environment
4050
4139
  },
4051
- secrets: role === "web" ? { HASNA_UPTIME_DATABASE_URL: secrets.database, APP_ENV: secrets.appEnv, HASNA_UPTIME_HOSTED_TOKEN: secrets.hostedToken } : role === "public-probe" ? { PROBE_CONFIG: secrets.publicProbe } : role === "reporter" ? { HASNA_UPTIME_DATABASE_URL: secrets.database, REPORTING_CONFIG: secrets.reporting } : { HASNA_UPTIME_DATABASE_URL: secrets.database, APP_ENV: secrets.appEnv }
4140
+ 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 }
4052
4141
  };
4053
4142
  }
4054
4143
  function clean(value, fallback) {