@hasna/uptime 0.1.23 → 0.1.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +24 -0
- package/Dockerfile.package +2 -2
- package/NOTICE +1 -1
- package/README.md +9 -3
- package/THIRD_PARTY_NOTICES.md +4 -1
- package/bun.lock +221 -0
- package/dist/api.js +111 -33
- package/dist/cli/index.js +169 -43
- package/dist/cloud-plan.d.ts +14 -1
- package/dist/cloud-plan.d.ts.map +1 -1
- package/dist/cloud-plan.js +27 -7
- package/dist/index.js +138 -40
- package/dist/mcp/index.js +111 -33
- package/dist/service.d.ts +19 -0
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +111 -33
- package/docs/aws-deployment-runbook.md +40 -14
- package/docs/aws-runtime-security.md +25 -10
- package/docs/cloud-source-of-truth.md +10 -7
- package/docs/deployment-metadata.example.json +4 -2
- package/infra/aws/README.md +26 -11
- package/infra/aws/main.tf +116 -34
- package/infra/aws/outputs.tf +8 -0
- package/infra/aws/terraform.tfvars.example +4 -1
- package/infra/aws/variables.tf +54 -5
- package/package.json +2 -1
package/dist/service.js
CHANGED
|
@@ -3150,12 +3150,18 @@ var MAX_PROBE_RESULT_FUTURE_MS = 5 * 60000;
|
|
|
3150
3150
|
class UptimeService {
|
|
3151
3151
|
store;
|
|
3152
3152
|
checkRunner;
|
|
3153
|
+
hostedResolveHost;
|
|
3154
|
+
hostedHttpRequest;
|
|
3155
|
+
hostedMaxRedirects;
|
|
3153
3156
|
leaseOwner = `svc_${randomUUID3().replace(/-/g, "").slice(0, 18)}`;
|
|
3154
3157
|
inFlightChecks = new Set;
|
|
3155
3158
|
inFlightReportSchedules = new Set;
|
|
3156
3159
|
constructor(options = {}) {
|
|
3157
3160
|
this.store = options.store ?? new UptimeStore({ mode: "local", ...options });
|
|
3158
3161
|
this.checkRunner = options.checkRunner ?? runMonitorCheck;
|
|
3162
|
+
this.hostedResolveHost = options.hostedResolveHost;
|
|
3163
|
+
this.hostedHttpRequest = options.hostedHttpRequest;
|
|
3164
|
+
this.hostedMaxRedirects = options.hostedMaxRedirects;
|
|
3159
3165
|
}
|
|
3160
3166
|
close() {
|
|
3161
3167
|
this.store.close();
|
|
@@ -3362,39 +3368,7 @@ class UptimeService {
|
|
|
3362
3368
|
const monitor = this.store.getMonitor(idOrName);
|
|
3363
3369
|
if (!monitor)
|
|
3364
3370
|
throw new Error(`Monitor not found: ${idOrName}`);
|
|
3365
|
-
|
|
3366
|
-
throw new Error(`Monitor is disabled: ${monitor.name}`);
|
|
3367
|
-
if (this.inFlightChecks.has(monitor.id))
|
|
3368
|
-
throw new Error(`Monitor check already in progress: ${monitor.name}`);
|
|
3369
|
-
const leaseTtlMs = Math.max(60000, (monitor.retryCount + 1) * monitor.timeoutMs + 1e4);
|
|
3370
|
-
if (!this.store.acquireCheckLease(monitor.id, this.leaseOwner, leaseTtlMs)) {
|
|
3371
|
-
throw new MonitorCheckBusyError(`Monitor check already in progress: ${monitor.name}`);
|
|
3372
|
-
}
|
|
3373
|
-
this.inFlightChecks.add(monitor.id);
|
|
3374
|
-
try {
|
|
3375
|
-
let attemptCount = 0;
|
|
3376
|
-
let last = null;
|
|
3377
|
-
const maxAttempts = Math.max(1, monitor.retryCount + 1);
|
|
3378
|
-
while (attemptCount < maxAttempts) {
|
|
3379
|
-
attemptCount += 1;
|
|
3380
|
-
last = await this.checkRunner(monitor);
|
|
3381
|
-
if (last.status === "up")
|
|
3382
|
-
break;
|
|
3383
|
-
}
|
|
3384
|
-
return this.store.recordCheckResult({
|
|
3385
|
-
monitorId: monitor.id,
|
|
3386
|
-
status: last.status,
|
|
3387
|
-
latencyMs: last.latencyMs,
|
|
3388
|
-
statusCode: last.statusCode ?? null,
|
|
3389
|
-
error: last.error ?? null,
|
|
3390
|
-
evidence: last.evidence ?? null,
|
|
3391
|
-
attemptCount,
|
|
3392
|
-
expectedMonitorRevision: monitor.revision
|
|
3393
|
-
});
|
|
3394
|
-
} finally {
|
|
3395
|
-
this.inFlightChecks.delete(monitor.id);
|
|
3396
|
-
this.store.releaseCheckLease(monitor.id, this.leaseOwner);
|
|
3397
|
-
}
|
|
3371
|
+
return this.recordMonitorCheck(monitor, { hostedTargetPolicy: false });
|
|
3398
3372
|
}
|
|
3399
3373
|
async checkAll() {
|
|
3400
3374
|
if (this.store.mode === "hosted")
|
|
@@ -3406,6 +3380,49 @@ class UptimeService {
|
|
|
3406
3380
|
}
|
|
3407
3381
|
return results;
|
|
3408
3382
|
}
|
|
3383
|
+
async checkHostedPublicMonitor(idOrName, options = {}) {
|
|
3384
|
+
this.assertHostedPublicChecksEnabled();
|
|
3385
|
+
const workspaceId = this.requireHostedWorkerWorkspaceId(options.workspaceId);
|
|
3386
|
+
const monitor = this.store.getMonitor(idOrName, { workspaceId });
|
|
3387
|
+
if (!monitor)
|
|
3388
|
+
throw new Error(`Monitor not found: ${idOrName}`);
|
|
3389
|
+
this.assertHostedPublicMonitor(monitor);
|
|
3390
|
+
const result = await this.recordMonitorCheck(monitor, { hostedTargetPolicy: true });
|
|
3391
|
+
this.auditStore().recordAuditEvent({
|
|
3392
|
+
workspaceId,
|
|
3393
|
+
action: "hosted_public_check.run",
|
|
3394
|
+
resourceType: "monitor",
|
|
3395
|
+
resourceId: monitor.id,
|
|
3396
|
+
message: `Ran hosted public check for ${monitor.name}`,
|
|
3397
|
+
metadata: {
|
|
3398
|
+
checkResultId: result.id,
|
|
3399
|
+
status: result.status,
|
|
3400
|
+
monitorKind: monitor.kind,
|
|
3401
|
+
operatorPath: "hosted_public_check"
|
|
3402
|
+
},
|
|
3403
|
+
actor: "hosted-public-check-worker"
|
|
3404
|
+
});
|
|
3405
|
+
return result;
|
|
3406
|
+
}
|
|
3407
|
+
async runDueHostedPublicChecks(now = new Date, options = {}) {
|
|
3408
|
+
this.assertHostedPublicChecksEnabled();
|
|
3409
|
+
const workspaceId = this.requireHostedWorkerWorkspaceId(options.workspaceId);
|
|
3410
|
+
const due = this.store.listMonitors({ workspaceId }).filter((monitor) => this.isHostedPublicMonitor(monitor) && this.isDue(monitor, now));
|
|
3411
|
+
const results = [];
|
|
3412
|
+
for (const monitor of due) {
|
|
3413
|
+
const current = this.store.getMonitor(monitor.id, { workspaceId });
|
|
3414
|
+
if (!current || !this.isHostedPublicMonitor(current) || !this.isDue(current, now))
|
|
3415
|
+
continue;
|
|
3416
|
+
try {
|
|
3417
|
+
results.push(await this.checkHostedPublicMonitor(current.id, { workspaceId }));
|
|
3418
|
+
} catch (error) {
|
|
3419
|
+
if (error instanceof MonitorCheckBusyError || error instanceof StaleCheckResultError)
|
|
3420
|
+
continue;
|
|
3421
|
+
throw error;
|
|
3422
|
+
}
|
|
3423
|
+
}
|
|
3424
|
+
return results;
|
|
3425
|
+
}
|
|
3409
3426
|
startScheduler(options = {}) {
|
|
3410
3427
|
if (this.store.mode === "hosted")
|
|
3411
3428
|
throw new Error("hosted scheduler requires check_jobs and probes");
|
|
@@ -3451,6 +3468,67 @@ class UptimeService {
|
|
|
3451
3468
|
const last = new Date(monitor.lastCheckedAt).getTime();
|
|
3452
3469
|
return now.getTime() - last >= monitor.intervalSeconds * 1000;
|
|
3453
3470
|
}
|
|
3471
|
+
async recordMonitorCheck(monitor, options) {
|
|
3472
|
+
if (!monitor.enabled)
|
|
3473
|
+
throw new Error(`Monitor is disabled: ${monitor.name}`);
|
|
3474
|
+
if (this.inFlightChecks.has(monitor.id))
|
|
3475
|
+
throw new Error(`Monitor check already in progress: ${monitor.name}`);
|
|
3476
|
+
const leaseTtlMs = Math.max(60000, (monitor.retryCount + 1) * monitor.timeoutMs + 1e4);
|
|
3477
|
+
if (!this.store.acquireCheckLease(monitor.id, this.leaseOwner, leaseTtlMs)) {
|
|
3478
|
+
throw new MonitorCheckBusyError(`Monitor check already in progress: ${monitor.name}`);
|
|
3479
|
+
}
|
|
3480
|
+
this.inFlightChecks.add(monitor.id);
|
|
3481
|
+
try {
|
|
3482
|
+
let attemptCount = 0;
|
|
3483
|
+
let last = null;
|
|
3484
|
+
const maxAttempts = Math.max(1, monitor.retryCount + 1);
|
|
3485
|
+
while (attemptCount < maxAttempts) {
|
|
3486
|
+
attemptCount += 1;
|
|
3487
|
+
last = options.hostedTargetPolicy ? await this.runHostedPublicCheckAttempt(monitor) : await this.checkRunner(monitor);
|
|
3488
|
+
if (last.status === "up")
|
|
3489
|
+
break;
|
|
3490
|
+
}
|
|
3491
|
+
return this.store.recordCheckResult({
|
|
3492
|
+
monitorId: monitor.id,
|
|
3493
|
+
status: last.status,
|
|
3494
|
+
latencyMs: last.latencyMs,
|
|
3495
|
+
statusCode: last.statusCode ?? null,
|
|
3496
|
+
error: last.error ?? null,
|
|
3497
|
+
evidence: last.evidence ?? null,
|
|
3498
|
+
attemptCount,
|
|
3499
|
+
expectedMonitorRevision: monitor.revision
|
|
3500
|
+
});
|
|
3501
|
+
} finally {
|
|
3502
|
+
this.inFlightChecks.delete(monitor.id);
|
|
3503
|
+
this.store.releaseCheckLease(monitor.id, this.leaseOwner);
|
|
3504
|
+
}
|
|
3505
|
+
}
|
|
3506
|
+
runHostedPublicCheckAttempt(monitor) {
|
|
3507
|
+
return runMonitorCheck(monitor, {
|
|
3508
|
+
hostedTargetPolicy: true,
|
|
3509
|
+
resolveHost: this.hostedResolveHost,
|
|
3510
|
+
hostedHttpRequest: this.hostedHttpRequest,
|
|
3511
|
+
maxRedirects: this.hostedMaxRedirects
|
|
3512
|
+
});
|
|
3513
|
+
}
|
|
3514
|
+
assertHostedPublicChecksEnabled() {
|
|
3515
|
+
if (this.store.mode !== "hosted")
|
|
3516
|
+
throw new Error("hosted public checks require hosted mode");
|
|
3517
|
+
}
|
|
3518
|
+
requireHostedWorkerWorkspaceId(workspaceId) {
|
|
3519
|
+
const value = workspaceId?.trim() || process.env.HASNA_UPTIME_WORKSPACE_ID?.trim();
|
|
3520
|
+
if (!value)
|
|
3521
|
+
throw new Error("hosted public checks require a workspace id");
|
|
3522
|
+
return value;
|
|
3523
|
+
}
|
|
3524
|
+
assertHostedPublicMonitor(monitor) {
|
|
3525
|
+
if (!this.isHostedPublicMonitor(monitor)) {
|
|
3526
|
+
throw new Error("hosted public checks support only HTTP and TCP monitors");
|
|
3527
|
+
}
|
|
3528
|
+
}
|
|
3529
|
+
isHostedPublicMonitor(monitor) {
|
|
3530
|
+
return monitor.kind === "http" || monitor.kind === "tcp";
|
|
3531
|
+
}
|
|
3454
3532
|
probeStore() {
|
|
3455
3533
|
if (this.store.mode === "hosted") {
|
|
3456
3534
|
throw new Error("hosted probe APIs require cloud check_jobs, workspace stores, and audit logging");
|
|
@@ -8,7 +8,8 @@ call AWS or mutate infrastructure.
|
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
10
|
uptime cloud plan --json > open-uptime-aws-plan.json
|
|
11
|
-
uptime cloud private-probe-config --probe-id prb_private_01 --machine-id private-probe-01 --
|
|
11
|
+
uptime cloud private-probe-config --probe-id prb_private_01 --machine-id private-probe-01 --json > private-probe-01-preflight.json
|
|
12
|
+
uptime cloud private-probe-config --probe-id prb_private_01 --machine-id private-probe-01 --env --allow-blocked-env > private-probe-01-review-only.env
|
|
12
13
|
```
|
|
13
14
|
|
|
14
15
|
Public package defaults are placeholders:
|
|
@@ -33,8 +34,10 @@ The app repo includes a hosted runtime `Dockerfile` and Terraform/OpenTofu
|
|
|
33
34
|
starter files in `infra/aws`. The plan output points to these files and keeps
|
|
34
35
|
`applyAllowed: false`.
|
|
35
36
|
|
|
36
|
-
`uptime cloud private-probe-config --env`
|
|
37
|
-
|
|
37
|
+
`uptime cloud private-probe-config --env` is blocked by default while hosted
|
|
38
|
+
probe routes remain fail-closed. It requires both a real `--probe-id` and the
|
|
39
|
+
explicit `--allow-blocked-env` review override; do not use that env output to
|
|
40
|
+
start a private probe until the JSON output says `canStart: true`.
|
|
38
41
|
|
|
39
42
|
## Preflight
|
|
40
43
|
|
|
@@ -57,9 +60,14 @@ write a sourceable env file with a placeholder probe identity.
|
|
|
57
60
|
|
|
58
61
|
4. Confirm the target VPC, private subnets, KMS key, and EFS/Backup plan inputs
|
|
59
62
|
still match the plan.
|
|
60
|
-
5. Confirm the protected access mode. The first deploy can use the
|
|
61
|
-
default HTTPS domain without custom DNS or ACM.
|
|
62
|
-
|
|
63
|
+
5. Confirm the protected access mode. The first zero-count deploy can use the
|
|
64
|
+
CloudFront default HTTPS domain without custom DNS or ACM. Before
|
|
65
|
+
token-bearing live traffic, either set
|
|
66
|
+
`cloudfront_origin_protocol_policy = "https-only"` with a dedicated
|
|
67
|
+
`cloudfront_origin_domain_name` that resolves to the ALB and a matching ACM
|
|
68
|
+
`certificate_arn`, or record an explicit risk acceptance for the temporary
|
|
69
|
+
HTTP-origin bridge. Custom hostname deploys still require Route53/edge
|
|
70
|
+
ownership and an ACM certificate.
|
|
63
71
|
6. Confirm the deployment role uses short-lived credentials or OIDC, not copied
|
|
64
72
|
access keys.
|
|
65
73
|
7. Create a private evidence directory outside the public repository. Store
|
|
@@ -74,9 +82,12 @@ The plan expects:
|
|
|
74
82
|
- ECS/Fargate cluster with separate services for web, scheduler, public probe,
|
|
75
83
|
reporter, and one-off migrations. In the current EFS SQLite bridge, only web
|
|
76
84
|
may be enabled and it must run at desired count `0` or `1`.
|
|
77
|
-
- CloudFront default-domain HTTPS edge plus ALB
|
|
78
|
-
CloudFront origin-facing ranges
|
|
79
|
-
|
|
85
|
+
- CloudFront default-domain HTTPS edge plus an ALB origin restricted to
|
|
86
|
+
CloudFront origin-facing ranges. The default zero-count bridge uses HTTP to
|
|
87
|
+
the origin; token-bearing live traffic should use the module's HTTPS-origin
|
|
88
|
+
mode with `cloudfront_origin_domain_name` plus `certificate_arn`, or a
|
|
89
|
+
documented risk acceptance. Direct ALB HTTPS mode also requires custom DNS and
|
|
90
|
+
an ACM certificate.
|
|
80
91
|
- Encrypted EFS file system, access point, mount targets, and AWS Backup plan
|
|
81
92
|
for `HASNA_UPTIME_HOSTED_SQLITE_DB=/data/uptime/uptime.db`.
|
|
82
93
|
- S3 bucket for redacted browser evidence and generated report artifacts.
|
|
@@ -148,6 +159,12 @@ aws codebuild start-build \
|
|
|
148
159
|
--project-name "$IMAGE_BUILDER_PROJECT"
|
|
149
160
|
```
|
|
150
161
|
|
|
162
|
+
The private infra root should set `runtime_package_integrity` to the published
|
|
163
|
+
npm `dist.integrity` value for the exact `runtime_package_version`. The image
|
|
164
|
+
builder verifies that value before extracting the package. If the value is not
|
|
165
|
+
set, record why the tarball is not integrity-pinned and keep the service
|
|
166
|
+
not-live.
|
|
167
|
+
|
|
151
168
|
Update the approved infra root so `container_image` is the immutable ECR digest,
|
|
152
169
|
then re-plan with all services still at `0`.
|
|
153
170
|
|
|
@@ -176,6 +193,9 @@ Before setting `desired_counts.web = 1`, verify:
|
|
|
176
193
|
- the image is an immutable digest, not a mutable tag or placeholder;
|
|
177
194
|
- required secrets have `AWSCURRENT` versions;
|
|
178
195
|
- `HASNA_UPTIME_ALLOWED_ORIGINS` matches the public HTTPS edge origin;
|
|
196
|
+
- CloudFront-to-origin transport is either `https-only` with an origin hostname
|
|
197
|
+
whose certificate matches that hostname, or the HTTP-origin bridge has a
|
|
198
|
+
named risk owner and approval recorded in private evidence;
|
|
179
199
|
- CloudFront origin access is distribution-bound with the CloudFront-only origin
|
|
180
200
|
verification header, not just narrowed to CloudFront origin-facing ranges;
|
|
181
201
|
- web egress to ECR, Secrets Manager or SSM, CloudWatch Logs, S3, EFS, and any
|
|
@@ -398,11 +418,13 @@ routes are backed by cloud check jobs and cloud audit rows.
|
|
|
398
418
|
URLs, or probe private keys in task definitions. Use ECS `secrets.valueFrom`
|
|
399
419
|
refs such as `HASNA_UPTIME_HOSTED_TOKEN`.
|
|
400
420
|
- Do not run public probe workers against private targets.
|
|
401
|
-
- Do not enable public probe workers until their cloud check-job path calls
|
|
402
|
-
|
|
403
|
-
smokes for denied DNS answers, redirect-to-denied targets, and
|
|
404
|
-
pinning. The SDK
|
|
405
|
-
|
|
421
|
+
- Do not enable public probe workers until their cloud check-job path calls the
|
|
422
|
+
hosted public-check runner, records target-policy decision evidence, and
|
|
423
|
+
passes AWS smokes for denied DNS answers, redirect-to-denied targets, and
|
|
424
|
+
address pinning. The SDK and `uptime cloud public-checks run-due` path now
|
|
425
|
+
handle execution-time DNS and redirect enforcement for bounded smokes, but a
|
|
426
|
+
sustained public-probe worker loop is not active until it is wired to cloud
|
|
427
|
+
leases.
|
|
406
428
|
- Do not enable scheduler, public-probe, reporter, or migration workers against
|
|
407
429
|
the EFS SQLite bridge; those services need Postgres/cloud leases first.
|
|
408
430
|
- Do not expose dashboard/API routes without hosted auth and workspace checks.
|
|
@@ -414,6 +436,10 @@ routes are backed by cloud check jobs and cloud audit rows.
|
|
|
414
436
|
the public repo and shared logs. Terraform redacts the sensitive input in CLI
|
|
415
437
|
output, but the value is still stored in encrypted Terraform state, saved plan
|
|
416
438
|
files, and AWS CloudFront/ALB configuration; restrict access accordingly.
|
|
439
|
+
- Do not treat `cloudfront_origin_protocol_policy = "http-only"` as final for
|
|
440
|
+
token-bearing traffic. The module supports `https-only`, but that mode needs a
|
|
441
|
+
real origin DNS name and matching ACM certificate because CloudFront verifies
|
|
442
|
+
the custom-origin TLS certificate against the origin host.
|
|
417
443
|
- Do not treat local SQLite, local project DBs, or private-probe local state as cloud
|
|
418
444
|
authority after cutover.
|
|
419
445
|
- Do configure owner/project/environment/service/cost-center tags, AWS Budgets
|
|
@@ -110,10 +110,12 @@ Target shape inside the approved VPC:
|
|
|
110
110
|
|
|
111
111
|
Security groups:
|
|
112
112
|
|
|
113
|
-
- `open-uptime-alb-sg`: in `cloudfront_default_domain` mode, inbound
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
113
|
+
- `open-uptime-alb-sg`: in `cloudfront_default_domain` mode, inbound origin
|
|
114
|
+
traffic is limited to AWS's CloudFront origin-facing managed prefix list on
|
|
115
|
+
`80` for the temporary HTTP bridge or `443` when
|
|
116
|
+
`cloudfront_origin_protocol_policy = "https-only"`; in `alb_https_cert` mode,
|
|
117
|
+
inbound `443` is limited to the approved edge/source CIDR policy. Outbound is
|
|
118
|
+
only to the web target group.
|
|
117
119
|
- `open-uptime-web-sg`: inbound only from ALB, outbound to RDS, S3 endpoint,
|
|
118
120
|
Secrets Manager, Logs, and internal service endpoints.
|
|
119
121
|
- `open-uptime-scheduler-sg`: no inbound, outbound to RDS, Logs, Secrets
|
|
@@ -136,10 +138,15 @@ infra PR.
|
|
|
136
138
|
|
|
137
139
|
Public web exposure requires defense in depth:
|
|
138
140
|
|
|
139
|
-
- first deployment may terminate viewer TLS at CloudFront's default
|
|
140
|
-
domain, restrict ALB
|
|
141
|
+
- first zero-count deployment may terminate viewer TLS at CloudFront's default
|
|
142
|
+
HTTPS domain, restrict ALB origin ingress to CloudFront origin-facing ranges,
|
|
141
143
|
and require the module's CloudFront-only origin verification header at the ALB
|
|
142
144
|
listener;
|
|
145
|
+
- token-bearing live traffic should use CloudFront HTTPS-origin mode by setting
|
|
146
|
+
`cloudfront_origin_protocol_policy = "https-only"`, a dedicated
|
|
147
|
+
`cloudfront_origin_domain_name` that resolves to the ALB, and a matching ACM
|
|
148
|
+
`certificate_arn`. CloudFront validates the custom-origin certificate against
|
|
149
|
+
the origin hostname, so the ALB DNS name is not enough for this mode;
|
|
143
150
|
- CloudFront prefix-list ingress is not distribution-bound by itself. In
|
|
144
151
|
`cloudfront_default_domain` mode, set
|
|
145
152
|
`enable_cloudfront_origin_verify_header = true` and provide a high-entropy
|
|
@@ -154,7 +161,7 @@ Public web exposure requires defense in depth:
|
|
|
154
161
|
identity layer;
|
|
155
162
|
- hosted web tasks must set `HASNA_UPTIME_ALLOWED_ORIGINS` to the public HTTPS
|
|
156
163
|
edge origin so browser mutation checks do not compare CloudFront HTTPS origins
|
|
157
|
-
against the
|
|
164
|
+
against the ALB origin hostname;
|
|
158
165
|
- Open Uptime still enforces app-level auth and workspace RBAC on every route
|
|
159
166
|
except `/health`;
|
|
160
167
|
- `/health` returns only service liveness/readiness and no monitor data;
|
|
@@ -379,7 +386,9 @@ Minimum implementation path:
|
|
|
379
386
|
|
|
380
387
|
1. review the repo-owned `Dockerfile` and package-image `Dockerfile.package`;
|
|
381
388
|
2. add the ECR repository and CodeBuild package image builder;
|
|
382
|
-
3. build the published npm package into ECR
|
|
389
|
+
3. build the published npm package into ECR, verify the expected npm
|
|
390
|
+
`dist.integrity` when `runtime_package_integrity` is set, install production
|
|
391
|
+
dependencies with the published `bun.lock`, and record the immutable digest;
|
|
383
392
|
4. run typecheck, tests, package checks, and container smoke locally/CI;
|
|
384
393
|
5. for the EFS bridge, keep the desired count at one web task maximum and zero
|
|
385
394
|
scheduler/public-probe/reporter/migration tasks;
|
|
@@ -425,6 +434,11 @@ PR must include a rough monthly estimate for:
|
|
|
425
434
|
Evidence retention and browser trace capture are the primary variable costs.
|
|
426
435
|
Default retention must be short until usage is measured.
|
|
427
436
|
|
|
437
|
+
ECS services must enable AWS-managed tags and `propagate_tags = "SERVICE"` so
|
|
438
|
+
service-launched tasks retain cost allocation tags. One-off smoke tasks run
|
|
439
|
+
outside ECS service propagation and must pass equivalent tags explicitly in the
|
|
440
|
+
operator command/evidence.
|
|
441
|
+
|
|
428
442
|
The AWS Terraform starter exposes optional AWS Budgets alerts through
|
|
429
443
|
`monthly_budget_limit_usd` and `budget_alert_email_addresses`; the approved
|
|
430
444
|
infra root must set real human/on-call notification targets and prove
|
|
@@ -438,8 +452,9 @@ required before browser evidence or public probe scale-out.
|
|
|
438
452
|
evidence bucket, encrypted logs, Backup, EFS, and service secret containers.
|
|
439
453
|
It is not live: services remain at desired count `0`, secrets have
|
|
440
454
|
`AWSCURRENT` values, scoped hosted-token descriptors can be used for operator
|
|
441
|
-
smokes, and
|
|
442
|
-
|
|
455
|
+
smokes, and the HTTPS-origin/custom-hostname path still needs an approved ACM
|
|
456
|
+
cert, DNS record, plan/apply, and edge smoke. Full production identity/RBAC is
|
|
457
|
+
still not implemented.
|
|
443
458
|
- Open Uptime is still SQLite-only for this bridge; only one protected web task
|
|
444
459
|
may write EFS until Postgres and cloud leases exist.
|
|
445
460
|
- Hosted API/dashboard auth, workspace RBAC, target policy, and Postgres leases
|
|
@@ -122,11 +122,12 @@ or delete workspace B data.
|
|
|
122
122
|
The target-state architecture uses one shared target policy at both
|
|
123
123
|
configuration time and execution time. The current hosted API implements
|
|
124
124
|
configuration-time checks for direct targets, and the SDK exposes
|
|
125
|
-
`runHostedHttpCheck`
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
125
|
+
`runHostedHttpCheck` plus a bounded hosted public-check service/CLI path for
|
|
126
|
+
workspace-scoped HTTP/TCP checks. Those paths perform runtime DNS resolution,
|
|
127
|
+
address pinning, redirect validation, DNS-rebinding protection, and
|
|
128
|
+
decision-record evidence. Long-running public probe execution stays disabled
|
|
129
|
+
until cloud check-job leases and the public-probe worker loop are wired to that
|
|
130
|
+
runner and validated in AWS.
|
|
130
131
|
|
|
131
132
|
Public probes must deny:
|
|
132
133
|
|
|
@@ -428,8 +429,10 @@ ECS/API/RDS/S3/probe lag/job backlog/delivery failures, and rollback commands.
|
|
|
428
429
|
- Hosted API reads are protected only by a broad bootstrap token, and the hosted
|
|
429
430
|
dashboard shell still fails closed; production-grade identity/RBAC is not
|
|
430
431
|
implemented yet.
|
|
431
|
-
- Outbound target policy for hosted HTTP
|
|
432
|
-
cloud public-
|
|
432
|
+
- Outbound target policy for hosted HTTP/TCP checks exists in the SDK and the
|
|
433
|
+
`uptime cloud public-checks run-due` operator path. The cloud public-probe
|
|
434
|
+
worker loop, durable check-job lease path, and sustained ECS liveness are not
|
|
435
|
+
wired yet.
|
|
433
436
|
- `@hasna/cloud` hybrid mode still returns SQLite, so it is not cloud-primary.
|
|
434
437
|
- The local cloud config currently points at a stale/non-resolving database host.
|
|
435
438
|
- Todos has unresolved conflicts that must be reconciled before cloud cutover.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"service": "open-uptime",
|
|
3
3
|
"package": "@hasna/uptime",
|
|
4
|
-
"intendedVersion": "0.1.
|
|
4
|
+
"intendedVersion": "0.1.25",
|
|
5
5
|
"accountProfile": "<aws-profile>",
|
|
6
6
|
"accountId": "<aws-account-id>",
|
|
7
7
|
"region": "us-east-1",
|
|
@@ -18,7 +18,9 @@
|
|
|
18
18
|
"mode": "cloudfront_default_domain",
|
|
19
19
|
"url": "https://<cloudfront-domain>",
|
|
20
20
|
"allowedOriginsEnv": "HASNA_UPTIME_ALLOWED_ORIGINS=https://<cloudfront-domain>",
|
|
21
|
-
"originPolicy": "ALB
|
|
21
|
+
"originPolicy": "ALB origin ingress restricted to CloudFront origin-facing ranges plus CloudFront-only origin verification header before scale-up; HTTPS-origin mode requires cloudfront_origin_domain_name plus matching certificate_arn",
|
|
22
|
+
"originProtocolPolicy": "http-only-or-https-only",
|
|
23
|
+
"originDomainName": "<alb-dns-name-or-approved-origin-hostname>",
|
|
22
24
|
"originVerifyHeaderRequiredBeforeScaleUp": true,
|
|
23
25
|
"originVerifyHeaderEnabled": "<true-after-private-root-apply>",
|
|
24
26
|
"originVerifyHeaderName": "<private-header-name>",
|
package/infra/aws/README.md
CHANGED
|
@@ -31,31 +31,43 @@ adapter and cloud leases are implemented. Do not set
|
|
|
31
31
|
|
|
32
32
|
The included CodeBuild project builds `@hasna/uptime` from npm with
|
|
33
33
|
`Dockerfile.package` and pushes the resulting image to ECR. This avoids
|
|
34
|
-
depending on a local Docker daemon for image publication.
|
|
34
|
+
depending on a local Docker daemon for image publication. Set
|
|
35
|
+
`runtime_package_integrity` in the private root after publish to make CodeBuild
|
|
36
|
+
verify the npm tarball `dist.integrity` before extracting it. The package image
|
|
37
|
+
also installs production dependencies from the published `bun.lock` with
|
|
38
|
+
`--frozen-lockfile`.
|
|
35
39
|
|
|
36
40
|
The default protected access mode is `cloudfront_default_domain`: CloudFront
|
|
37
|
-
serves HTTPS on its default domain while the ALB origin
|
|
38
|
-
|
|
39
|
-
|
|
41
|
+
serves HTTPS on its default domain while the ALB origin is limited to AWS's
|
|
42
|
+
CloudFront origin-facing managed prefix list. The default origin protocol is the
|
|
43
|
+
temporary `http-only` bridge. Before token-bearing live traffic, prefer setting
|
|
44
|
+
`cloudfront_origin_protocol_policy = "https-only"` with a dedicated
|
|
45
|
+
`cloudfront_origin_domain_name` that resolves to the ALB and a matching ACM
|
|
46
|
+
`certificate_arn`. Use `alb_https_cert` only when bypassing CloudFront after
|
|
47
|
+
custom DNS and an ACM certificate are approved.
|
|
40
48
|
The web task receives `HASNA_UPTIME_ALLOWED_ORIGINS` for the selected public
|
|
41
|
-
HTTPS origin so hosted mutation CSRF checks still work through the
|
|
42
|
-
origin
|
|
49
|
+
HTTPS origin so hosted mutation CSRF checks still work through the selected
|
|
50
|
+
edge/origin path.
|
|
43
51
|
|
|
44
52
|
CloudFront prefix-list ingress is only a network narrowing control; it is not
|
|
45
53
|
bound to one distribution. Before enabling the web task, set
|
|
46
54
|
`enable_cloudfront_origin_verify_header = true` and provide a high-entropy
|
|
47
55
|
`cloudfront_origin_verify_header_value` from a private operator workflow. The
|
|
48
56
|
module then configures CloudFront to send that header, makes the ALB default
|
|
49
|
-
action return `403`, and forwards only requests with the matching header
|
|
57
|
+
action return `403`, and forwards only requests with the matching header on the
|
|
58
|
+
selected HTTP or HTTPS origin listener.
|
|
50
59
|
Terraform marks the value sensitive, but it still lives in encrypted Terraform
|
|
51
60
|
state and in CloudFront/ALB configuration; restrict state, saved plan,
|
|
52
61
|
CloudFront distribution-read, and ELB listener-rule-read access accordingly.
|
|
53
62
|
|
|
54
63
|
All module resources carry owner, project, environment, service, account, app
|
|
55
|
-
type, and cost-center tags.
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
64
|
+
type, and cost-center tags. ECS services enable AWS-managed tags and propagate
|
|
65
|
+
service tags to launched tasks. Any one-off `run-task` smoke must pass the same
|
|
66
|
+
tag set explicitly because it is outside service propagation. Set
|
|
67
|
+
`monthly_budget_limit_usd` plus `budget_alert_email_addresses` in the approved
|
|
68
|
+
infra root to create AWS Budgets forecasted and actual spend alerts. Leaving the
|
|
69
|
+
email list empty skips budget creation and is not sufficient for live scale-out
|
|
70
|
+
approval.
|
|
59
71
|
|
|
60
72
|
Private AWS API egress can be pinned through opt-in VPC endpoints by setting
|
|
61
73
|
`enable_private_vpc_endpoints = true` and passing `private_route_table_ids`.
|
|
@@ -92,6 +104,9 @@ Store instead of Secrets Manager, add `ssm` to
|
|
|
92
104
|
## Current Blockers
|
|
93
105
|
|
|
94
106
|
- Hosted production auth/RBAC still needs scoped, revocable credentials.
|
|
107
|
+
- The default `http-only` CloudFront origin bridge must be replaced with the
|
|
108
|
+
explicit HTTPS-origin mode or consciously accepted with documented risk before
|
|
109
|
+
token-bearing live traffic.
|
|
95
110
|
- Public probe runtime has SDK-level hosted HTTP target-policy enforcement, but
|
|
96
111
|
the public-probe worker and cloud check-job lease path are still disabled until
|
|
97
112
|
they are wired to that runner and validated in AWS.
|