@hasna/uptime 0.1.7 → 0.1.9

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.
@@ -8,6 +8,7 @@ var DEFAULT_HOSTNAME = "uptime.example.com";
8
8
  var DEFAULT_WORKSPACE_ID = "workspace-id";
9
9
  var DEFAULT_VPC_ID = "vpc-xxxxxxxx";
10
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);
@@ -20,7 +21,9 @@ function buildAwsDeploymentPlan(options = {}) {
20
21
  const image = clean(options.image, `${imageRepositoryUri}@sha256:<image-digest>`);
21
22
  const evidenceBucket = clean(options.evidenceBucket, `hasna-${stage}-${prefix}-evidence`);
22
23
  const hostedSqliteDbPath = clean(options.hostedSqliteDbPath, DEFAULT_HOSTED_SQLITE_DB);
23
- const runtimePackageVersion = clean(options.runtimePackageVersion, "0.1.7");
24
+ const runtimePackageVersion = clean(options.runtimePackageVersion, "0.1.9");
25
+ const protectedAccessMode = options.protectedAccessMode ?? DEFAULT_PROTECTED_ACCESS_MODE;
26
+ const protectedAccessUrl = protectedAccessMode === "cloudfront_default_domain" ? "https://<cloudfront-domain>" : `https://${hostname}`;
24
27
  const cluster = `${prefix}-${stage}`;
25
28
  const secrets = {
26
29
  appEnv: clean(options.appEnvSecretName, `open-uptime/${stage}/app/env`),
@@ -34,7 +37,8 @@ function buildAwsDeploymentPlan(options = {}) {
34
37
  HASNA_UPTIME_MODE: "hosted",
35
38
  HASNA_UPTIME_HOSTED_SQLITE_DB: hostedSqliteDbPath,
36
39
  HASNA_UPTIME_WORKSPACE_ID: workspaceId,
37
- HASNA_UPTIME_HOSTNAME: hostname
40
+ HASNA_UPTIME_HOSTNAME: hostname,
41
+ HASNA_UPTIME_ALLOWED_ORIGINS: protectedAccessUrl
38
42
  }),
39
43
  servicePlan(prefix, stage, "scheduler", 0, image, workspaceId, secrets, {
40
44
  HASNA_UPTIME_MODE: "hosted",
@@ -60,7 +64,7 @@ function buildAwsDeploymentPlan(options = {}) {
60
64
  ];
61
65
  return {
62
66
  kind: "open-uptime.aws-deployment-plan",
63
- version: 2,
67
+ version: 3,
64
68
  generatedAt: new Date().toISOString(),
65
69
  status: "blocked",
66
70
  canApply: false,
@@ -82,6 +86,9 @@ function buildAwsDeploymentPlan(options = {}) {
82
86
  hostedSqliteDbPath,
83
87
  evidenceBucket,
84
88
  loadBalancer: `${prefix}-${stage}-alb`,
89
+ protectedAccessMode,
90
+ edgeDistribution: protectedAccessMode === "cloudfront_default_domain" ? `${prefix}-${stage}-edge` : undefined,
91
+ protectedAccessUrl,
85
92
  targetGroups: [`${prefix}-${stage}-web-tg`],
86
93
  securityGroups: [
87
94
  `${prefix}-${stage}-alb-sg`,
@@ -129,7 +136,7 @@ function buildAwsDeploymentPlan(options = {}) {
129
136
  `Infra PR must declare CodeBuild image builder ${prefix}-${stage}-image-builder for @hasna/uptime@${runtimePackageVersion}.`,
130
137
  `Infra PR must declare hardened S3 evidence bucket ${evidenceBucket} with KMS, versioning, lifecycle, and public access block.`,
131
138
  `Infra PR must declare encrypted EFS ${prefix}-${stage}-data with access point, mount targets, and AWS Backup plan.`,
132
- `Infra PR must declare ECS/Fargate cluster ${cluster}, ALB, target groups, security groups, IAM roles, CloudWatch log groups, and Secrets Manager refs.`,
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.`,
133
140
  "Only apply the infra plan from the approved infrastructure repository after review evidence is attached."
134
141
  ],
135
142
  deploy: [
@@ -138,7 +145,7 @@ function buildAwsDeploymentPlan(options = {}) {
138
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.",
139
146
  `Register task definitions for ${services.map((service) => service.name).join(", ")} using valueFrom secrets.`,
140
147
  `Update ECS services in cluster ${cluster} one component at a time through the approved deploy pipeline.`,
141
- `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.`
142
149
  ],
143
150
  rollback: [
144
151
  "Keep previous task definition ARNs before each service update.",
@@ -146,9 +153,9 @@ function buildAwsDeploymentPlan(options = {}) {
146
153
  "Disable scheduler/reporter services before data rollback.",
147
154
  "Restore EFS backup recovery point only after explicit operator approval and audit record."
148
155
  ],
149
- spark01: [
156
+ privateProbe: [
150
157
  "Create a private probe identity with a caller-managed public key.",
151
- "Install @hasna/uptime on Spark01 and write the generated env file with mode 0600.",
158
+ "Install @hasna/uptime on the private probe operator machine and write the generated env file with mode 0600.",
152
159
  "Run the private probe against the hosted /api/v1 probe endpoint once it exists."
153
160
  ]
154
161
  },
@@ -157,17 +164,17 @@ function buildAwsDeploymentPlan(options = {}) {
157
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.",
158
165
  "Hosted production auth/RBAC must replace broad static hosted-token operation before exposure.",
159
166
  "Public probe execution still needs DNS, redirect, and rebinding SSRF enforcement plus cloud check-job leases.",
160
- "Spark01 hosted probe enrollment, claim, submit, heartbeat, revocation, and rotation are not cloud-backed yet."
167
+ "Private probe enrollment, claim, submit, heartbeat, revocation, and rotation are not cloud-backed yet."
161
168
  ],
162
169
  requiredEvidence: [
163
170
  "Infrastructure PR/synth/plan from the approved infra repository.",
164
171
  "CodeBuild image-builder run, container smoke, and immutable image digest.",
165
172
  "ECS task definitions using secrets.valueFrom only.",
166
- "ALB/TLS/DNS/auth denial smokes and web alarm checks.",
173
+ "CloudFront-default-domain or ALB TLS auth-denial smokes, direct-origin denial evidence, and web alarm checks.",
167
174
  "Single-writer ECS evidence: one web task maximum and no scheduler/public-probe/reporter EFS mounts.",
168
175
  "EFS encryption, access point, mount-target, AWS Backup, and restore-drill evidence.",
169
176
  "S3 bucket KMS, versioning, lifecycle, and public-access-block evidence.",
170
- "Spark01 private-probe registration, key-file mode, heartbeat, and revocation evidence."
177
+ "Private-probe registration, key-file mode, heartbeat, and revocation evidence."
171
178
  ],
172
179
  safety: {
173
180
  liveAwsMutation: false,
@@ -176,6 +183,7 @@ function buildAwsDeploymentPlan(options = {}) {
176
183
  notes: [
177
184
  "This plan generator does not call AWS.",
178
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.",
179
187
  "Hosted runtime uses explicit EFS-backed SQLite at HASNA_UPTIME_HOSTED_SQLITE_DB until the async Postgres adapter exists.",
180
188
  "Do not set HASNA_UPTIME_DATABASE_URL for hosted tasks until the Postgres adapter is implemented.",
181
189
  "Secrets are represented as secret names/refs and must be injected with valueFrom.",
@@ -184,16 +192,16 @@ function buildAwsDeploymentPlan(options = {}) {
184
192
  }
185
193
  };
186
194
  }
187
- function buildSpark01CloudConfig(options = {}) {
195
+ function buildPrivateProbeCloudConfig(options = {}) {
188
196
  const apiUrl = clean(options.apiUrl, `https://${DEFAULT_HOSTNAME}/api/v1`);
189
197
  const workspaceId = clean(options.workspaceId, DEFAULT_WORKSPACE_ID);
190
- const machineId = clean(options.machineId, "spark01");
191
- const privateKeyFile = clean(options.probePrivateKeyFile, "~/.hasna/uptime/probes/spark01.key.pem");
198
+ const machineId = clean(options.machineId, "private-probe-01");
199
+ const privateKeyFile = clean(options.probePrivateKeyFile, "~/.hasna/uptime/probes/private-probe-01.key.pem");
192
200
  const probeId = options.probeId?.trim();
193
201
  const blockers = [
194
202
  ...probeId ? [] : ["Cloud-registered private probe id is required before writing a sourceable env file."],
195
203
  "Hosted probe claim and submit routes still fail closed until cloud check_jobs and workspace stores are implemented.",
196
- "Spark01 enrollment, heartbeat, revocation, rotation, and bounded offline lease handling are not implemented yet."
204
+ "Private probe enrollment, heartbeat, revocation, rotation, and bounded offline lease handling are not implemented yet."
197
205
  ];
198
206
  const env = {
199
207
  HASNA_UPTIME_MODE: "hosted",
@@ -207,7 +215,7 @@ function buildSpark01CloudConfig(options = {}) {
207
215
  if (probeId)
208
216
  env.HASNA_UPTIME_PRIVATE_PROBE_ID = probeId;
209
217
  return {
210
- kind: "open-uptime.spark01-cloud-config",
218
+ kind: "open-uptime.private-probe-cloud-config",
211
219
  version: 1,
212
220
  generatedAt: new Date().toISOString(),
213
221
  status: "blocked",
@@ -219,7 +227,7 @@ function buildSpark01CloudConfig(options = {}) {
219
227
  {
220
228
  path: privateKeyFile,
221
229
  mode: "0600",
222
- purpose: "Ed25519 private key generated on Spark01; never paste into cloud config."
230
+ purpose: "Ed25519 private key generated on the private probe machine; never paste into cloud config."
223
231
  },
224
232
  {
225
233
  path: "~/.hasna/uptime/cloud.env",
@@ -229,7 +237,7 @@ function buildSpark01CloudConfig(options = {}) {
229
237
  ],
230
238
  commands: [
231
239
  "bun install -g @hasna/uptime@latest",
232
- "Generate the Spark01 private key locally and register only its public key with the hosted control plane once registration exists.",
240
+ "Generate the private probe key locally and register only its public key with the hosted control plane once registration exists.",
233
241
  "Write ~/.hasna/uptime/cloud.env from this plan, then source it for the private probe service.",
234
242
  "Start the private probe worker only after hosted /api/v1 probe claim/submit routes are backed by cloud jobs."
235
243
  ],
@@ -238,18 +246,18 @@ function buildSpark01CloudConfig(options = {}) {
238
246
  privateKeyInline: false,
239
247
  tokenInline: false,
240
248
  notes: [
241
- "This config is hosted-targeted preflight: Spark01 must not start until cloud probe routes are backed by hosted state.",
249
+ "This config is hosted-targeted preflight: the private probe must not start until cloud probe routes are backed by hosted state.",
242
250
  "The private key file path is referenced, not embedded.",
243
251
  "Hosted token or probe auth material must come from the machine secret store, not this generated config."
244
252
  ]
245
253
  }
246
254
  };
247
255
  }
248
- function renderSpark01Env(config) {
256
+ function renderPrivateProbeEnv(config) {
249
257
  const required = ["HASNA_UPTIME_PRIVATE_PROBE_ID"];
250
258
  const missing = required.filter((key) => !config.env[key]);
251
259
  if (missing.length > 0) {
252
- throw new Error(`Spark01 env output requires ${missing.join(", ")}`);
260
+ throw new Error(`private probe env output requires ${missing.join(", ")}`);
253
261
  }
254
262
  return Object.entries(config.env).map(([key, value]) => `${key}=${shellEscape(value)}`).join(`
255
263
  `);
@@ -282,7 +290,7 @@ function shellEscape(value) {
282
290
  return `'${value.replace(/'/g, "'\\''")}'`;
283
291
  }
284
292
  export {
285
- renderSpark01Env,
286
- buildSpark01CloudConfig,
293
+ renderPrivateProbeEnv,
294
+ buildPrivateProbeCloudConfig,
287
295
  buildAwsDeploymentPlan
288
296
  };
package/dist/index.d.ts CHANGED
@@ -5,13 +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
+ export { buildAwsDeploymentPlan, buildPrivateProbeCloudConfig, renderPrivateProbeEnv } from "./cloud-plan.js";
9
9
  export { uptimeHome, uptimeDbPath, uptimeHostedFallbackDbPath, ensureUptimeHome } from "./paths.js";
10
10
  export type { UptimeBackup, UptimeBackupCheck, UptimeRuntimeMode, UptimeStoreOptions, MonitorProvenance, SaveImportBatchInput, StoredImportBatch, UpsertMonitorProvenanceInput, } from "./store.js";
11
11
  export type { BrowserPageRunner, BrowserPageRunnerResult, FetchLike, } from "./checks.js";
12
12
  export type { ImportAction, ImportApplyItem, ImportApplyResult, ImportCandidate, ImportPreview, ImportPreviewItem, ImportRequest, ImportRollbackItem, ImportRollbackResult, ImportSource, } from "./imports.js";
13
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";
14
14
  export type { ProbeKeyPair, ProbeSigningInput } from "./probes.js";
15
- export type { AwsDeploymentPlan, AwsDeploymentPlanOptions, AwsServicePlan, Spark01CloudConfig, Spark01CloudConfigOptions, } from "./cloud-plan.js";
15
+ export type { AwsDeploymentPlan, AwsDeploymentPlanOptions, AwsServicePlan, PrivateProbeCloudConfig, PrivateProbeCloudConfigOptions, } from "./cloud-plan.js";
16
16
  export type { BuildUptimeReportOptions, SendUptimeReportOptions, UptimeEmailReportTarget, UptimeLogsReportTarget, UptimeReport, UptimeReportDelivery, UptimeSmsReportTarget, } from "./report.js";
17
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,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"}
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,4BAA4B,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AAC9G,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,uBAAuB,EACvB,8BAA8B,GAC/B,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
@@ -3510,6 +3510,7 @@ function serveUptime(options = {}) {
3510
3510
  apiToken: options.apiToken,
3511
3511
  hostedToken: options.hostedToken,
3512
3512
  hostedTokens: options.hostedTokens,
3513
+ hostedAllowedOrigins: options.hostedAllowedOrigins,
3513
3514
  allowUnsafeRemoteMutations: options.allowUnsafeRemoteMutations,
3514
3515
  trustedLoopback: isLoopbackHost(options.host ?? "127.0.0.1"),
3515
3516
  mode
@@ -3569,13 +3570,23 @@ async function handleHostedRequest(service, request, url, options) {
3569
3570
  const scope = hostedScopeFor(request.method, apiPath);
3570
3571
  requireHostedActor(request, url, options, scope);
3571
3572
  if (["POST", "PATCH", "DELETE"].includes(request.method)) {
3572
- const origin = request.headers.get("origin");
3573
- if (origin && origin !== `${url.protocol}//${url.host}`) {
3574
- throw new ApiError("cross-origin mutation rejected", 403);
3575
- }
3573
+ validateHostedMutationOrigin(request, url, options);
3576
3574
  }
3577
3575
  return handleApiRoute(service, request, url, apiPath, options, true);
3578
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
+ }
3579
3590
  async function handleApiRoute(service, request, url, apiPath, options, hosted) {
3580
3591
  if (request.method === "GET" && apiPath === "/api/summary") {
3581
3592
  return json(service.summary());
@@ -3794,6 +3805,34 @@ function resolveHostedTokens(options) {
3794
3805
  workspaceId: process.env.HASNA_UPTIME_WORKSPACE_ID ?? "default"
3795
3806
  }];
3796
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
+ }
3797
3836
  function safeTokenEqual(candidate, expected) {
3798
3837
  if (!candidate)
3799
3838
  return false;
@@ -3829,6 +3868,7 @@ var DEFAULT_HOSTNAME = "uptime.example.com";
3829
3868
  var DEFAULT_WORKSPACE_ID = "workspace-id";
3830
3869
  var DEFAULT_VPC_ID = "vpc-xxxxxxxx";
3831
3870
  var DEFAULT_HOSTED_SQLITE_DB = "/data/uptime/uptime.db";
3871
+ var DEFAULT_PROTECTED_ACCESS_MODE = "cloudfront_default_domain";
3832
3872
  function buildAwsDeploymentPlan(options = {}) {
3833
3873
  const region = clean(options.region, DEFAULT_REGION);
3834
3874
  const stage = clean(options.stage, DEFAULT_STAGE);
@@ -3841,7 +3881,9 @@ function buildAwsDeploymentPlan(options = {}) {
3841
3881
  const image = clean(options.image, `${imageRepositoryUri}@sha256:<image-digest>`);
3842
3882
  const evidenceBucket = clean(options.evidenceBucket, `hasna-${stage}-${prefix}-evidence`);
3843
3883
  const hostedSqliteDbPath = clean(options.hostedSqliteDbPath, DEFAULT_HOSTED_SQLITE_DB);
3844
- const runtimePackageVersion = clean(options.runtimePackageVersion, "0.1.7");
3884
+ const runtimePackageVersion = clean(options.runtimePackageVersion, "0.1.9");
3885
+ const protectedAccessMode = options.protectedAccessMode ?? DEFAULT_PROTECTED_ACCESS_MODE;
3886
+ const protectedAccessUrl = protectedAccessMode === "cloudfront_default_domain" ? "https://<cloudfront-domain>" : `https://${hostname}`;
3845
3887
  const cluster = `${prefix}-${stage}`;
3846
3888
  const secrets = {
3847
3889
  appEnv: clean(options.appEnvSecretName, `open-uptime/${stage}/app/env`),
@@ -3855,7 +3897,8 @@ function buildAwsDeploymentPlan(options = {}) {
3855
3897
  HASNA_UPTIME_MODE: "hosted",
3856
3898
  HASNA_UPTIME_HOSTED_SQLITE_DB: hostedSqliteDbPath,
3857
3899
  HASNA_UPTIME_WORKSPACE_ID: workspaceId,
3858
- HASNA_UPTIME_HOSTNAME: hostname
3900
+ HASNA_UPTIME_HOSTNAME: hostname,
3901
+ HASNA_UPTIME_ALLOWED_ORIGINS: protectedAccessUrl
3859
3902
  }),
3860
3903
  servicePlan(prefix, stage, "scheduler", 0, image, workspaceId, secrets, {
3861
3904
  HASNA_UPTIME_MODE: "hosted",
@@ -3881,7 +3924,7 @@ function buildAwsDeploymentPlan(options = {}) {
3881
3924
  ];
3882
3925
  return {
3883
3926
  kind: "open-uptime.aws-deployment-plan",
3884
- version: 2,
3927
+ version: 3,
3885
3928
  generatedAt: new Date().toISOString(),
3886
3929
  status: "blocked",
3887
3930
  canApply: false,
@@ -3903,6 +3946,9 @@ function buildAwsDeploymentPlan(options = {}) {
3903
3946
  hostedSqliteDbPath,
3904
3947
  evidenceBucket,
3905
3948
  loadBalancer: `${prefix}-${stage}-alb`,
3949
+ protectedAccessMode,
3950
+ edgeDistribution: protectedAccessMode === "cloudfront_default_domain" ? `${prefix}-${stage}-edge` : undefined,
3951
+ protectedAccessUrl,
3906
3952
  targetGroups: [`${prefix}-${stage}-web-tg`],
3907
3953
  securityGroups: [
3908
3954
  `${prefix}-${stage}-alb-sg`,
@@ -3950,7 +3996,7 @@ function buildAwsDeploymentPlan(options = {}) {
3950
3996
  `Infra PR must declare CodeBuild image builder ${prefix}-${stage}-image-builder for @hasna/uptime@${runtimePackageVersion}.`,
3951
3997
  `Infra PR must declare hardened S3 evidence bucket ${evidenceBucket} with KMS, versioning, lifecycle, and public access block.`,
3952
3998
  `Infra PR must declare encrypted EFS ${prefix}-${stage}-data with access point, mount targets, and AWS Backup plan.`,
3953
- `Infra PR must declare ECS/Fargate cluster ${cluster}, ALB, target groups, security groups, IAM roles, CloudWatch log groups, and Secrets Manager refs.`,
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.`,
3954
4000
  "Only apply the infra plan from the approved infrastructure repository after review evidence is attached."
3955
4001
  ],
3956
4002
  deploy: [
@@ -3959,7 +4005,7 @@ function buildAwsDeploymentPlan(options = {}) {
3959
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.",
3960
4006
  `Register task definitions for ${services.map((service) => service.name).join(", ")} using valueFrom secrets.`,
3961
4007
  `Update ECS services in cluster ${cluster} one component at a time through the approved deploy pipeline.`,
3962
- `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.`
3963
4009
  ],
3964
4010
  rollback: [
3965
4011
  "Keep previous task definition ARNs before each service update.",
@@ -3967,9 +4013,9 @@ function buildAwsDeploymentPlan(options = {}) {
3967
4013
  "Disable scheduler/reporter services before data rollback.",
3968
4014
  "Restore EFS backup recovery point only after explicit operator approval and audit record."
3969
4015
  ],
3970
- spark01: [
4016
+ privateProbe: [
3971
4017
  "Create a private probe identity with a caller-managed public key.",
3972
- "Install @hasna/uptime on Spark01 and write the generated env file with mode 0600.",
4018
+ "Install @hasna/uptime on the private probe operator machine and write the generated env file with mode 0600.",
3973
4019
  "Run the private probe against the hosted /api/v1 probe endpoint once it exists."
3974
4020
  ]
3975
4021
  },
@@ -3978,17 +4024,17 @@ function buildAwsDeploymentPlan(options = {}) {
3978
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.",
3979
4025
  "Hosted production auth/RBAC must replace broad static hosted-token operation before exposure.",
3980
4026
  "Public probe execution still needs DNS, redirect, and rebinding SSRF enforcement plus cloud check-job leases.",
3981
- "Spark01 hosted probe enrollment, claim, submit, heartbeat, revocation, and rotation are not cloud-backed yet."
4027
+ "Private probe enrollment, claim, submit, heartbeat, revocation, and rotation are not cloud-backed yet."
3982
4028
  ],
3983
4029
  requiredEvidence: [
3984
4030
  "Infrastructure PR/synth/plan from the approved infra repository.",
3985
4031
  "CodeBuild image-builder run, container smoke, and immutable image digest.",
3986
4032
  "ECS task definitions using secrets.valueFrom only.",
3987
- "ALB/TLS/DNS/auth denial smokes and web alarm checks.",
4033
+ "CloudFront-default-domain or ALB TLS auth-denial smokes, direct-origin denial evidence, and web alarm checks.",
3988
4034
  "Single-writer ECS evidence: one web task maximum and no scheduler/public-probe/reporter EFS mounts.",
3989
4035
  "EFS encryption, access point, mount-target, AWS Backup, and restore-drill evidence.",
3990
4036
  "S3 bucket KMS, versioning, lifecycle, and public-access-block evidence.",
3991
- "Spark01 private-probe registration, key-file mode, heartbeat, and revocation evidence."
4037
+ "Private-probe registration, key-file mode, heartbeat, and revocation evidence."
3992
4038
  ],
3993
4039
  safety: {
3994
4040
  liveAwsMutation: false,
@@ -3997,6 +4043,7 @@ function buildAwsDeploymentPlan(options = {}) {
3997
4043
  notes: [
3998
4044
  "This plan generator does not call AWS.",
3999
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.",
4000
4047
  "Hosted runtime uses explicit EFS-backed SQLite at HASNA_UPTIME_HOSTED_SQLITE_DB until the async Postgres adapter exists.",
4001
4048
  "Do not set HASNA_UPTIME_DATABASE_URL for hosted tasks until the Postgres adapter is implemented.",
4002
4049
  "Secrets are represented as secret names/refs and must be injected with valueFrom.",
@@ -4005,16 +4052,16 @@ function buildAwsDeploymentPlan(options = {}) {
4005
4052
  }
4006
4053
  };
4007
4054
  }
4008
- function buildSpark01CloudConfig(options = {}) {
4055
+ function buildPrivateProbeCloudConfig(options = {}) {
4009
4056
  const apiUrl = clean(options.apiUrl, `https://${DEFAULT_HOSTNAME}/api/v1`);
4010
4057
  const workspaceId = clean(options.workspaceId, DEFAULT_WORKSPACE_ID);
4011
- const machineId = clean(options.machineId, "spark01");
4012
- const privateKeyFile = clean(options.probePrivateKeyFile, "~/.hasna/uptime/probes/spark01.key.pem");
4058
+ const machineId = clean(options.machineId, "private-probe-01");
4059
+ const privateKeyFile = clean(options.probePrivateKeyFile, "~/.hasna/uptime/probes/private-probe-01.key.pem");
4013
4060
  const probeId = options.probeId?.trim();
4014
4061
  const blockers = [
4015
4062
  ...probeId ? [] : ["Cloud-registered private probe id is required before writing a sourceable env file."],
4016
4063
  "Hosted probe claim and submit routes still fail closed until cloud check_jobs and workspace stores are implemented.",
4017
- "Spark01 enrollment, heartbeat, revocation, rotation, and bounded offline lease handling are not implemented yet."
4064
+ "Private probe enrollment, heartbeat, revocation, rotation, and bounded offline lease handling are not implemented yet."
4018
4065
  ];
4019
4066
  const env2 = {
4020
4067
  HASNA_UPTIME_MODE: "hosted",
@@ -4028,7 +4075,7 @@ function buildSpark01CloudConfig(options = {}) {
4028
4075
  if (probeId)
4029
4076
  env2.HASNA_UPTIME_PRIVATE_PROBE_ID = probeId;
4030
4077
  return {
4031
- kind: "open-uptime.spark01-cloud-config",
4078
+ kind: "open-uptime.private-probe-cloud-config",
4032
4079
  version: 1,
4033
4080
  generatedAt: new Date().toISOString(),
4034
4081
  status: "blocked",
@@ -4040,7 +4087,7 @@ function buildSpark01CloudConfig(options = {}) {
4040
4087
  {
4041
4088
  path: privateKeyFile,
4042
4089
  mode: "0600",
4043
- purpose: "Ed25519 private key generated on Spark01; never paste into cloud config."
4090
+ purpose: "Ed25519 private key generated on the private probe machine; never paste into cloud config."
4044
4091
  },
4045
4092
  {
4046
4093
  path: "~/.hasna/uptime/cloud.env",
@@ -4050,7 +4097,7 @@ function buildSpark01CloudConfig(options = {}) {
4050
4097
  ],
4051
4098
  commands: [
4052
4099
  "bun install -g @hasna/uptime@latest",
4053
- "Generate the Spark01 private key locally and register only its public key with the hosted control plane once registration exists.",
4100
+ "Generate the private probe key locally and register only its public key with the hosted control plane once registration exists.",
4054
4101
  "Write ~/.hasna/uptime/cloud.env from this plan, then source it for the private probe service.",
4055
4102
  "Start the private probe worker only after hosted /api/v1 probe claim/submit routes are backed by cloud jobs."
4056
4103
  ],
@@ -4059,18 +4106,18 @@ function buildSpark01CloudConfig(options = {}) {
4059
4106
  privateKeyInline: false,
4060
4107
  tokenInline: false,
4061
4108
  notes: [
4062
- "This config is hosted-targeted preflight: Spark01 must not start until cloud probe routes are backed by hosted state.",
4109
+ "This config is hosted-targeted preflight: the private probe must not start until cloud probe routes are backed by hosted state.",
4063
4110
  "The private key file path is referenced, not embedded.",
4064
4111
  "Hosted token or probe auth material must come from the machine secret store, not this generated config."
4065
4112
  ]
4066
4113
  }
4067
4114
  };
4068
4115
  }
4069
- function renderSpark01Env(config) {
4116
+ function renderPrivateProbeEnv(config) {
4070
4117
  const required = ["HASNA_UPTIME_PRIVATE_PROBE_ID"];
4071
4118
  const missing = required.filter((key) => !config.env[key]);
4072
4119
  if (missing.length > 0) {
4073
- throw new Error(`Spark01 env output requires ${missing.join(", ")}`);
4120
+ throw new Error(`private probe env output requires ${missing.join(", ")}`);
4074
4121
  }
4075
4122
  return Object.entries(config.env).map(([key, value]) => `${key}=${shellEscape(value)}`).join(`
4076
4123
  `);
@@ -4115,7 +4162,7 @@ export {
4115
4162
  runHttpCheck,
4116
4163
  runBrowserPageCheck,
4117
4164
  rollbackImport,
4118
- renderSpark01Env,
4165
+ renderPrivateProbeEnv,
4119
4166
  probeResultSigningPayload,
4120
4167
  probePublicKeyFingerprint,
4121
4168
  previewImport,
@@ -4124,7 +4171,7 @@ export {
4124
4171
  createUptimeClient,
4125
4172
  createApiHandler,
4126
4173
  buildUptimeReport,
4127
- buildSpark01CloudConfig,
4174
+ buildPrivateProbeCloudConfig,
4128
4175
  buildAwsDeploymentPlan,
4129
4176
  applyImport,
4130
4177
  UptimeStore,
@@ -8,7 +8,7 @@ call AWS or mutate infrastructure.
8
8
 
9
9
  ```bash
10
10
  uptime cloud plan --json > open-uptime-aws-plan.json
11
- uptime cloud spark01-config --probe-id prb_spark01 --env > spark01-uptime.env
11
+ uptime cloud private-probe-config --probe-id prb_private_01 --machine-id private-probe-01 --env > private-probe-01-uptime.env
12
12
  ```
13
13
 
14
14
  Public package defaults are placeholders:
@@ -19,12 +19,13 @@ Public package defaults are placeholders:
19
19
  - hosted data path: EFS-mounted SQLite at `/data/uptime/uptime.db`
20
20
  - hostname: `uptime.example.com`
21
21
  - workspace id: `workspace-id`
22
+ - protected access mode: `cloudfront_default_domain`
22
23
 
23
24
  Override these with CLI flags or private deployment evidence for the real
24
25
  account, hostname, workspace id, VPC id, secret refs, and repository names.
25
26
 
26
27
  The generated AWS plan currently returns `status: "blocked"` and
27
- `canApply: false`. The generated Spark01 config returns `status: "blocked"` and
28
+ `canApply: false`. The generated private-probe config returns `status: "blocked"` and
28
29
  `canStart: false`. Treat both as review/preflight artifacts until the blockers
29
30
  and required evidence in the JSON output are resolved.
30
31
 
@@ -32,7 +33,7 @@ The app repo includes a hosted runtime `Dockerfile` and Terraform/OpenTofu
32
33
  starter files in `infra/aws`. The plan output points to these files and keeps
33
34
  `applyAllowed: false`.
34
35
 
35
- `uptime cloud spark01-config --env` requires a real `--probe-id`; it will not
36
+ `uptime cloud private-probe-config --env` requires a real `--probe-id`; it will not
36
37
  write a sourceable env file with a placeholder probe identity.
37
38
 
38
39
  ## Preflight
@@ -47,7 +48,9 @@ write a sourceable env file with a placeholder probe identity.
47
48
 
48
49
  3. Confirm the target VPC, private subnets, KMS key, and EFS/Backup plan inputs
49
50
  still match the plan.
50
- 4. Confirm Route53/edge ownership for the chosen hostname.
51
+ 4. Confirm the protected access mode. The first deploy can use the CloudFront
52
+ default HTTPS domain without custom DNS or ACM. Custom hostname deploys still
53
+ require Route53/edge ownership and an ACM certificate.
51
54
  5. Confirm the deployment role uses short-lived credentials or OIDC, not copied
52
55
  access keys.
53
56
 
@@ -59,7 +62,9 @@ The plan expects:
59
62
  - ECS/Fargate cluster with separate services for web, scheduler, public probe,
60
63
  reporter, and one-off migrations. In the current EFS SQLite bridge, only web
61
64
  may be enabled and it must run at desired count `0` or `1`.
62
- - ALB, TLS certificate, target group, and security groups.
65
+ - CloudFront default-domain HTTPS edge plus ALB HTTP origin restricted to
66
+ CloudFront origin-facing ranges, or an ALB HTTPS listener with ACM certificate
67
+ when custom DNS is approved.
63
68
  - Encrypted EFS file system, access point, mount targets, and AWS Backup plan
64
69
  for `HASNA_UPTIME_HOSTED_SQLITE_DB=/data/uptime/uptime.db`.
65
70
  - S3 bucket for redacted browser evidence and generated report artifacts.
@@ -82,12 +87,14 @@ terraform -chdir=infra/aws validate
82
87
  terraform -chdir=infra/aws plan -out open-uptime.tfplan
83
88
  ```
84
89
 
85
- ## Spark01
90
+ Use Terraform/OpenTofu 1.9 or newer for this starter.
86
91
 
87
- Spark01 should be a private probe/operator machine, not the hosted source of
88
- truth. The generated env file points Spark01 at hosted `/api/v1` state and
89
- references a local private-key file path. It does not include private key or
90
- token contents.
92
+ ## Private Probe Operator
93
+
94
+ The operator machine should be a private probe/operator machine, not the hosted
95
+ source of truth. The generated env file points the machine at hosted `/api/v1`
96
+ state and references a local private-key file path. It does not include private
97
+ key or token contents.
91
98
 
92
99
  The private probe service should not be enabled until hosted probe claim/submit
93
100
  routes are backed by cloud check jobs and cloud audit rows.
@@ -98,6 +105,9 @@ routes are backed by cloud check jobs and cloud audit rows.
98
105
  - Do deploy hosted mode with `HASNA_UPTIME_HOSTED_SQLITE_DB` pointing at the EFS
99
106
  mount path `/data/uptime/uptime.db`. Do not set `HASNA_UPTIME_DATABASE_URL`
100
107
  until the async Postgres adapter exists.
108
+ - Do set `HASNA_UPTIME_ALLOWED_ORIGINS` on the hosted web task to the public
109
+ HTTPS edge origin, such as the CloudFront default domain or approved custom
110
+ hostname.
101
111
  - Do not inline AWS keys, hosted tokens, Mailery keys, Open Logs tokens, database
102
112
  URLs, or probe private keys in task definitions. Use ECS `secrets.valueFrom`
103
113
  refs such as `HASNA_UPTIME_HOSTED_TOKEN`.
@@ -105,8 +115,16 @@ routes are backed by cloud check jobs and cloud audit rows.
105
115
  - Do not enable scheduler, public-probe, reporter, or migration workers against
106
116
  the EFS SQLite bridge; those services need Postgres/cloud leases first.
107
117
  - Do not expose dashboard/API routes without hosted auth and workspace checks.
108
- - Do not treat local SQLite, local project DBs, or Spark01 local state as cloud
118
+ - Do not expose the ALB directly in CloudFront mode; ALB ingress must be limited
119
+ to CloudFront origin-facing ranges.
120
+ - Do not treat CloudFront prefix-list ingress as distribution-bound origin
121
+ protection. Before enabling the web task, add CloudFront VPC origin/private
122
+ ALB routing or require a CloudFront-only origin header whose secret value is
123
+ not stored in Terraform state.
124
+ - Do not treat local SQLite, local project DBs, or private-probe local state as cloud
109
125
  authority after cutover.
126
+ - Do configure owner/project/environment/service/cost-center tags and AWS
127
+ Budgets alert recipients in the approved infra root before live scale-out.
110
128
 
111
129
  ## Rollback
112
130
 
@@ -15,6 +15,8 @@ terraform -chdir=infra/aws validate
15
15
  terraform -chdir=infra/aws plan -out open-uptime.tfplan
16
16
  ```
17
17
 
18
+ Terraform 1.9 or newer is required by the variable validation in this starter.
19
+
18
20
  Required inputs are declared in `variables.tf` and illustrated in
19
21
  `terraform.tfvars.example`. Secrets are passed as Secrets Manager/SSM ARNs only;
20
22
  never place plaintext tokens, database URLs, private keys, or channel
@@ -31,12 +33,31 @@ The included CodeBuild project builds `@hasna/uptime` from npm with
31
33
  `Dockerfile.package` and pushes the resulting image to ECR. This avoids
32
34
  depending on a local Docker daemon for image publication.
33
35
 
36
+ The default protected access mode is `cloudfront_default_domain`: CloudFront
37
+ serves HTTPS on its default domain while the ALB origin accepts HTTP only from
38
+ AWS's CloudFront origin-facing managed prefix list. Use `alb_https_cert` only
39
+ after custom DNS and an ACM certificate are approved.
40
+ The web task receives `HASNA_UPTIME_ALLOWED_ORIGINS` for the selected public
41
+ HTTPS origin so hosted mutation CSRF checks still work through the private HTTP
42
+ origin hop.
43
+
44
+ CloudFront prefix-list ingress is only a network narrowing control; it is not
45
+ bound to one distribution. Add CloudFront VPC origin/private ALB routing or an
46
+ ALB origin-header rule with the secret value managed outside Terraform state
47
+ before enabling the web task.
48
+
49
+ All module resources carry owner, project, environment, service, account, app
50
+ type, and cost-center tags. Set `monthly_budget_limit_usd` plus
51
+ `budget_alert_email_addresses` in the approved infra root to create AWS Budgets
52
+ forecasted and actual spend alerts. Leaving the email list empty skips budget
53
+ creation and is not sufficient for live scale-out approval.
54
+
34
55
  ## Current Blockers
35
56
 
36
57
  - Hosted production auth/RBAC still needs scoped, revocable credentials.
37
58
  - Public probe runtime still needs execution-time DNS/redirect/rebinding SSRF
38
59
  enforcement.
39
- - Spark01 hosted private-probe enrollment/heartbeat/revocation is still
60
+ - Hosted private-probe enrollment/heartbeat/revocation is still
40
61
  fail-closed.
41
62
 
42
63
  Keep `desired_count` at `0`, or at `1` for the protected web bridge only after