@hasna/uptime 0.1.25 → 0.1.26
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 +18 -0
- package/README.md +9 -1
- package/dist/cli/index.js +223 -5
- package/dist/cloud-plan.js +5 -5
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +72 -5
- package/dist/workers.d.ts +34 -0
- package/dist/workers.d.ts.map +1 -0
- package/dist/workers.js +69 -0
- package/docs/aws-deployment-runbook.md +8 -5
- package/docs/aws-runtime-security.md +11 -4
- package/docs/cloud-source-of-truth.md +8 -4
- package/docs/deployment-metadata.example.json +1 -1
- package/infra/aws/README.md +15 -6
- package/infra/aws/main.tf +8 -8
- package/infra/aws/terraform.tfvars.example +2 -1
- package/infra/aws/variables.tf +26 -1
- package/package.json +6 -2
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,24 @@ project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.1.26] - 2026-06-29
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- Added explicit hosted worker preflight and fail-closed run entrypoints for
|
|
14
|
+
scheduler, public-probe, reporter, and migration roles.
|
|
15
|
+
- Added a bounded `uptime cloud public-checks worker` loop around the existing
|
|
16
|
+
hosted public-check smoke primitive for EFS SQLite bridge testing.
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- Updated AWS non-web task definitions to use explicit fail-closed worker
|
|
21
|
+
commands and environment-aware preflight health checks instead of the
|
|
22
|
+
placeholder `cloud plan` command.
|
|
23
|
+
- Hardened Terraform scale-up validation so `web > 0` in CloudFront mode
|
|
24
|
+
requires origin verification and either HTTPS-origin mode or explicit
|
|
25
|
+
`allow_cloudfront_http_origin_live_traffic` risk acceptance.
|
|
26
|
+
|
|
9
27
|
## [0.1.25] - 2026-06-29
|
|
10
28
|
|
|
11
29
|
### Added
|
package/README.md
CHANGED
|
@@ -32,6 +32,8 @@ uptime report-schedules run-due
|
|
|
32
32
|
uptime report-schedules runs
|
|
33
33
|
uptime audit
|
|
34
34
|
uptime cloud plan --json
|
|
35
|
+
uptime cloud workers preflight --role public-probe --json
|
|
36
|
+
uptime cloud public-checks worker --workspace-id ws_internal --max-iterations 1 --hosted-sqlite-db /data/uptime/uptime.db
|
|
35
37
|
uptime cloud private-probe-config --probe-id prb_private_01 --machine-id private-probe-01 --json
|
|
36
38
|
uptime cloud private-probe-config --probe-id prb_private_01 --machine-id private-probe-01 --env --allow-blocked-env
|
|
37
39
|
uptime incidents
|
|
@@ -63,6 +65,12 @@ acceptance. Hosted AWS runtime state currently uses explicit EFS-backed SQLite v
|
|
|
63
65
|
`HASNA_UPTIME_HOSTED_SQLITE_DB=/data/uptime/uptime.db` for one protected web
|
|
64
66
|
task maximum; do not set `HASNA_UPTIME_DATABASE_URL` until the async Postgres
|
|
65
67
|
adapter is implemented.
|
|
68
|
+
`uptime cloud workers preflight --role <role>` reports why hosted scheduler,
|
|
69
|
+
public-probe, reporter, and migration roles remain blocked. Their `run`
|
|
70
|
+
entrypoints fail closed until Postgres, check jobs, channel refs, and migration
|
|
71
|
+
plans exist. `uptime cloud public-checks worker` is only a bounded EFS SQLite
|
|
72
|
+
bridge loop around hosted HTTP/TCP smoke checks; it is not the final cloud
|
|
73
|
+
`check_jobs`/lease/fencing protocol.
|
|
66
74
|
`Dockerfile.package` is used by the Terraform CodeBuild image builder to build
|
|
67
75
|
the published npm package into ECR from inside AWS.
|
|
68
76
|
|
|
@@ -224,7 +232,7 @@ check jobs, workspace stores, and audit logging are implemented. Local job reads
|
|
|
224
232
|
redact fencing tokens; the claim response is the only API response that returns
|
|
225
233
|
the active fencing token.
|
|
226
234
|
|
|
227
|
-
Hosted `/api/v1/report-schedules*`, `/api/v1/report-runs`, and
|
|
235
|
+
Hosted `POST /api/v1/report`, `/api/v1/report-schedules*`, `/api/v1/report-runs`, and
|
|
228
236
|
`/api/v1/audit-events` also fail closed until cloud channel refs, workspace
|
|
229
237
|
stores, and cloud audit logging are implemented.
|
|
230
238
|
|
package/dist/cli/index.js
CHANGED
|
@@ -7230,7 +7230,7 @@ function buildAwsDeploymentPlan(options = {}) {
|
|
|
7230
7230
|
const image = clean(options.image, `${imageRepositoryUri}@sha256:<image-digest>`);
|
|
7231
7231
|
const evidenceBucket = clean(options.evidenceBucket, `hasna-${stage}-${prefix}-evidence`);
|
|
7232
7232
|
const hostedSqliteDbPath = clean(options.hostedSqliteDbPath, DEFAULT_HOSTED_SQLITE_DB);
|
|
7233
|
-
const runtimePackageVersion = clean(options.runtimePackageVersion, "0.1.
|
|
7233
|
+
const runtimePackageVersion = clean(options.runtimePackageVersion, "0.1.26");
|
|
7234
7234
|
const runtimePackageIntegrity = options.runtimePackageIntegrity?.trim() || undefined;
|
|
7235
7235
|
const protectedAccessMode = options.protectedAccessMode ?? DEFAULT_PROTECTED_ACCESS_MODE;
|
|
7236
7236
|
const cloudfrontOriginProtocolPolicy = options.cloudfrontOriginProtocolPolicy ?? DEFAULT_CLOUDFRONT_ORIGIN_PROTOCOL_POLICY;
|
|
@@ -7306,7 +7306,7 @@ function buildAwsDeploymentPlan(options = {}) {
|
|
|
7306
7306
|
domainName: cloudfrontOriginProtocolPolicy === "https-only" ? cloudfrontOriginDomainName : "<alb-dns-name>",
|
|
7307
7307
|
requiresMatchingCertificate: cloudfrontOriginProtocolPolicy === "https-only",
|
|
7308
7308
|
liveTrafficApproved: false,
|
|
7309
|
-
risk: cloudfrontOriginProtocolPolicy === "http-only" ? "Temporary HTTP-origin bridge:
|
|
7309
|
+
risk: cloudfrontOriginProtocolPolicy === "http-only" ? "Temporary HTTP-origin bridge: web scale-up requires allow_cloudfront_http_origin_live_traffic=true for bounded smokes, or switch to https-only with cloudfront_origin_domain_name plus certificate_arn." : "CloudFront HTTPS-origin mode requires the origin hostname to resolve to the ALB and match certificate_arn."
|
|
7310
7310
|
} : undefined,
|
|
7311
7311
|
originVerification: protectedAccessMode === "cloudfront_default_domain" ? {
|
|
7312
7312
|
mode: "cloudfront_origin_header",
|
|
@@ -7367,7 +7367,7 @@ function buildAwsDeploymentPlan(options = {}) {
|
|
|
7367
7367
|
`Infra PR must declare CodeBuild image builder ${prefix}-${stage}-image-builder for @hasna/uptime@${runtimePackageVersion}.`,
|
|
7368
7368
|
`Infra PR must declare hardened S3 evidence bucket ${evidenceBucket} with KMS, versioning, lifecycle, and public access block.`,
|
|
7369
7369
|
`Infra PR must declare encrypted EFS ${prefix}-${stage}-data with access point, mount targets, and AWS Backup plan.`,
|
|
7370
|
-
protectedAccessMode === "cloudfront_default_domain" ? "Infra PR must declare CloudFront default-domain HTTPS edge, ALB origin listener restricted to CloudFront origin-facing ranges, CloudFront-only origin verification header binding, ECS/Fargate cluster, target groups, security groups, IAM roles, CloudWatch log groups, and Secrets Manager refs. Token-bearing live traffic must use cloudfront_origin_protocol_policy=https-only with a matching origin hostname/certificate, or carry explicit
|
|
7370
|
+
protectedAccessMode === "cloudfront_default_domain" ? "Infra PR must declare CloudFront default-domain HTTPS edge, ALB origin listener restricted to CloudFront origin-facing ranges, CloudFront-only origin verification header binding, ECS/Fargate cluster, target groups, security groups, IAM roles, CloudWatch log groups, and Secrets Manager refs. Token-bearing live traffic must use cloudfront_origin_protocol_policy=https-only with a matching origin hostname/certificate, or carry explicit allow_cloudfront_http_origin_live_traffic=true bounded-smoke risk acceptance." : `Infra PR must declare ECS/Fargate cluster ${cluster}, ALB HTTPS listener, target groups, security groups, IAM roles, CloudWatch log groups, and Secrets Manager refs.`,
|
|
7371
7371
|
"Only apply the infra plan from the approved infrastructure repository after review evidence is attached."
|
|
7372
7372
|
],
|
|
7373
7373
|
deploy: [
|
|
@@ -7376,7 +7376,7 @@ function buildAwsDeploymentPlan(options = {}) {
|
|
|
7376
7376
|
"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.",
|
|
7377
7377
|
`Register task definitions for ${services.map((service) => service.name).join(", ")} using valueFrom secrets.`,
|
|
7378
7378
|
`Update ECS services in cluster ${cluster} one component at a time through the approved deploy pipeline.`,
|
|
7379
|
-
protectedAccessMode === "cloudfront_default_domain" ? "Use the CloudFront default HTTPS domain with origin verification header binding for first protected access; before token-bearing live traffic, switch the origin to https-only with a matching origin hostname/certificate or
|
|
7379
|
+
protectedAccessMode === "cloudfront_default_domain" ? "Use the CloudFront default HTTPS domain with origin verification header binding for first protected access; before token-bearing live traffic, switch the origin to https-only with a matching origin hostname/certificate or set allow_cloudfront_http_origin_live_traffic=true only for a bounded approved smoke." : `Create Route53/edge record for ${hostname} only after ALB health checks pass and auth denial smokes succeed.`
|
|
7380
7380
|
],
|
|
7381
7381
|
rollback: [
|
|
7382
7382
|
"Keep previous task definition ARNs before each service update.",
|
|
@@ -7393,7 +7393,7 @@ function buildAwsDeploymentPlan(options = {}) {
|
|
|
7393
7393
|
blockers: [
|
|
7394
7394
|
"The infrastructure owner repository was not found in this workspace.",
|
|
7395
7395
|
protectedAccessMode === "cloudfront_default_domain" ? "CloudFront origin verification header binding must be enabled and direct-origin denial must be proven before web desired count is raised above 0." : "ALB HTTPS ingress policy and auth-denial smokes must be proven before web desired count is raised above 0.",
|
|
7396
|
-
...protectedAccessMode === "cloudfront_default_domain" && cloudfrontOriginProtocolPolicy === "http-only" ? ["CloudFront-to-ALB origin transport is still http-only; token-bearing live traffic
|
|
7396
|
+
...protectedAccessMode === "cloudfront_default_domain" && cloudfrontOriginProtocolPolicy === "http-only" ? ["CloudFront-to-ALB origin transport is still http-only; web scale-up now requires explicit allow_cloudfront_http_origin_live_traffic=true risk acceptance, and token-bearing live traffic should use https-only origin mode."] : [],
|
|
7397
7397
|
...protectedAccessMode === "cloudfront_default_domain" && cloudfrontOriginProtocolPolicy === "https-only" && cloudfrontOriginDomainName === "<alb-dns-name>" ? ["CloudFront https-only origin mode needs cloudfront_origin_domain_name that resolves to the ALB and matches certificate_arn."] : [],
|
|
7398
7398
|
"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.",
|
|
7399
7399
|
"Hosted production auth/RBAC must replace broad static hosted-token operation before exposure.",
|
|
@@ -7531,6 +7531,72 @@ function shellEscape(value) {
|
|
|
7531
7531
|
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
7532
7532
|
}
|
|
7533
7533
|
|
|
7534
|
+
// src/workers.ts
|
|
7535
|
+
var DEFAULT_INTERVAL_MS = 30000;
|
|
7536
|
+
async function runHostedPublicChecksWorker(options) {
|
|
7537
|
+
const intervalMs = normalizePositiveInteger(options.intervalMs ?? DEFAULT_INTERVAL_MS, "intervalMs");
|
|
7538
|
+
const maxRuntimeMs = options.maxRuntimeMs === undefined ? undefined : normalizePositiveInteger(options.maxRuntimeMs, "maxRuntimeMs");
|
|
7539
|
+
const maxIterations = options.maxIterations === undefined ? undefined : normalizePositiveInteger(options.maxIterations, "maxIterations");
|
|
7540
|
+
const clock = options.now ?? (() => new Date);
|
|
7541
|
+
const sleep = options.sleep ?? abortableSleep;
|
|
7542
|
+
const startedAtDate = clock();
|
|
7543
|
+
const startedAt = startedAtDate.toISOString();
|
|
7544
|
+
const deadline = maxRuntimeMs === undefined ? undefined : startedAtDate.getTime() + maxRuntimeMs;
|
|
7545
|
+
let iterations = 0;
|
|
7546
|
+
let checked = 0;
|
|
7547
|
+
while (!options.signal?.aborted) {
|
|
7548
|
+
if (maxIterations !== undefined && iterations >= maxIterations)
|
|
7549
|
+
break;
|
|
7550
|
+
const now = clock();
|
|
7551
|
+
if (deadline !== undefined && now.getTime() >= deadline)
|
|
7552
|
+
break;
|
|
7553
|
+
const iteration = iterations + 1;
|
|
7554
|
+
const iterationStartedAt = now.toISOString();
|
|
7555
|
+
const results = await options.runner.runDueHostedPublicChecks(now, { workspaceId: options.workspaceId });
|
|
7556
|
+
const finishedAt = clock().toISOString();
|
|
7557
|
+
iterations = iteration;
|
|
7558
|
+
checked += results.length;
|
|
7559
|
+
options.onIteration?.({
|
|
7560
|
+
iteration,
|
|
7561
|
+
checked: results.length,
|
|
7562
|
+
startedAt: iterationStartedAt,
|
|
7563
|
+
finishedAt
|
|
7564
|
+
});
|
|
7565
|
+
if (maxIterations !== undefined && iterations >= maxIterations)
|
|
7566
|
+
break;
|
|
7567
|
+
if (deadline !== undefined && clock().getTime() >= deadline)
|
|
7568
|
+
break;
|
|
7569
|
+
await sleep(intervalMs, options.signal);
|
|
7570
|
+
}
|
|
7571
|
+
return {
|
|
7572
|
+
kind: "open-uptime.hosted-public-checks-worker",
|
|
7573
|
+
status: options.signal?.aborted ? "stopped" : "completed",
|
|
7574
|
+
workspaceId: options.workspaceId?.trim() || null,
|
|
7575
|
+
iterations,
|
|
7576
|
+
checked,
|
|
7577
|
+
startedAt,
|
|
7578
|
+
finishedAt: clock().toISOString()
|
|
7579
|
+
};
|
|
7580
|
+
}
|
|
7581
|
+
function normalizePositiveInteger(value, name) {
|
|
7582
|
+
if (!Number.isInteger(value) || value <= 0)
|
|
7583
|
+
throw new Error(`${name} must be a positive integer`);
|
|
7584
|
+
return value;
|
|
7585
|
+
}
|
|
7586
|
+
function abortableSleep(ms, signal) {
|
|
7587
|
+
if (signal?.aborted)
|
|
7588
|
+
return Promise.resolve();
|
|
7589
|
+
return new Promise((resolve) => {
|
|
7590
|
+
const timer = setTimeout(done, ms);
|
|
7591
|
+
function done() {
|
|
7592
|
+
signal?.removeEventListener("abort", done);
|
|
7593
|
+
clearTimeout(timer);
|
|
7594
|
+
resolve();
|
|
7595
|
+
}
|
|
7596
|
+
signal?.addEventListener("abort", done, { once: true });
|
|
7597
|
+
});
|
|
7598
|
+
}
|
|
7599
|
+
|
|
7534
7600
|
// src/cli/index.ts
|
|
7535
7601
|
var program2 = new Command;
|
|
7536
7602
|
program2.name("uptime").description("Local-first uptime and downtime monitoring").version(packageVersion()).option("-j, --json", "print JSON");
|
|
@@ -7891,6 +7957,32 @@ cloud.command("private-probe-config").description("Generate hosted-targeted priv
|
|
|
7891
7957
|
fail(error);
|
|
7892
7958
|
}
|
|
7893
7959
|
});
|
|
7960
|
+
var cloudWorkers = cloud.command("workers").description("Inspect and run hosted worker entrypoints");
|
|
7961
|
+
cloudWorkers.command("preflight").description("Check one hosted worker entrypoint without starting work").requiredOption("--role <role>", "scheduler, public-probe, reporter, or migration").option("--healthcheck", "exit non-zero when hosted mode, component, or workspace env is invalid").option("-j, --json", "print JSON").action((opts) => {
|
|
7962
|
+
try {
|
|
7963
|
+
const preflight = buildHostedWorkerPreflight(parseWorkerRole(opts.role));
|
|
7964
|
+
print(preflight, renderHostedWorkerPreflight(preflight), opts);
|
|
7965
|
+
if (opts.healthcheck && !hostedWorkerEnvironmentOk(preflight))
|
|
7966
|
+
process.exit(1);
|
|
7967
|
+
} catch (error) {
|
|
7968
|
+
fail(error);
|
|
7969
|
+
}
|
|
7970
|
+
});
|
|
7971
|
+
cloudWorkers.command("run").description("Run one hosted worker entrypoint; fails closed until cloud prerequisites exist").requiredOption("--role <role>", "scheduler, public-probe, reporter, or migration").option("-j, --json", "print JSON").action((opts) => {
|
|
7972
|
+
try {
|
|
7973
|
+
const preflight = buildHostedWorkerPreflight(parseWorkerRole(opts.role));
|
|
7974
|
+
const error = `hosted ${preflight.role} worker runtime is blocked until cloud prerequisites exist`;
|
|
7975
|
+
if (wantsJson(opts)) {
|
|
7976
|
+
console.log(JSON.stringify({ ok: false, error, preflight }, null, 2));
|
|
7977
|
+
} else {
|
|
7978
|
+
console.error(source_default.red(sanitizeTerminal(error)));
|
|
7979
|
+
console.error(renderHostedWorkerPreflight(preflight));
|
|
7980
|
+
}
|
|
7981
|
+
process.exit(1);
|
|
7982
|
+
} catch (error) {
|
|
7983
|
+
fail(error);
|
|
7984
|
+
}
|
|
7985
|
+
});
|
|
7894
7986
|
var cloudPublicChecks = cloud.command("public-checks").description("Run hosted public HTTP/TCP checks against the configured hosted store");
|
|
7895
7987
|
cloudPublicChecks.command("run-due").description("Run due hosted public HTTP/TCP checks for one workspace").option("--workspace-id <id>", "workspace id; defaults to HASNA_UPTIME_WORKSPACE_ID").option("--now <iso>", "due timestamp", new Date().toISOString()).option("--hosted-sqlite-db <path>", "hosted SQLite path on cloud-mounted storage").option("--allow-hosted-local-store", "allow hosted mode to use local SQLite as an explicit fallback").option("-j, --json", "print JSON").action(async (opts) => {
|
|
7896
7988
|
try {
|
|
@@ -7907,6 +7999,37 @@ cloudPublicChecks.command("run-due").description("Run due hosted public HTTP/TCP
|
|
|
7907
7999
|
fail(error);
|
|
7908
8000
|
}
|
|
7909
8001
|
});
|
|
8002
|
+
cloudPublicChecks.command("worker").description("Run a bounded EFS SQLite bridge loop around hosted public checks").option("--workspace-id <id>", "workspace id; defaults to HASNA_UPTIME_WORKSPACE_ID").option("--interval-ms <ms>", "sleep interval between iterations", parseInteger, 30000).option("--max-runtime-ms <ms>", "stop after this many milliseconds", parseInteger).option("--max-iterations <n>", "stop after this many iterations", parseInteger).option("--hosted-sqlite-db <path>", "hosted SQLite path on cloud-mounted storage").option("--allow-hosted-local-store", "allow hosted mode to use local SQLite as an explicit fallback").option("-j, --json", "print JSON").action(async (opts) => {
|
|
8003
|
+
const abortController = new AbortController;
|
|
8004
|
+
const onSignal = () => abortController.abort();
|
|
8005
|
+
process.once("SIGINT", onSignal);
|
|
8006
|
+
process.once("SIGTERM", onSignal);
|
|
8007
|
+
try {
|
|
8008
|
+
const svc = hostedService({
|
|
8009
|
+
hostedSqliteDb: opts.hostedSqliteDb,
|
|
8010
|
+
allowHostedLocalStore: opts.allowHostedLocalStore
|
|
8011
|
+
});
|
|
8012
|
+
const workspaceId = opts.workspaceId || process.env.HASNA_UPTIME_WORKSPACE_ID;
|
|
8013
|
+
const summary = await runHostedPublicChecksWorker({
|
|
8014
|
+
runner: svc,
|
|
8015
|
+
workspaceId,
|
|
8016
|
+
intervalMs: opts.intervalMs,
|
|
8017
|
+
maxRuntimeMs: opts.maxRuntimeMs,
|
|
8018
|
+
maxIterations: opts.maxIterations,
|
|
8019
|
+
signal: abortController.signal,
|
|
8020
|
+
onIteration: wantsJson(opts) ? undefined : (iteration) => {
|
|
8021
|
+
console.log(`iteration ${iteration.iteration}: checked ${iteration.checked}`);
|
|
8022
|
+
}
|
|
8023
|
+
});
|
|
8024
|
+
svc.close();
|
|
8025
|
+
print(summary, renderHostedPublicChecksWorkerSummary(summary), opts);
|
|
8026
|
+
} catch (error) {
|
|
8027
|
+
fail(error);
|
|
8028
|
+
} finally {
|
|
8029
|
+
process.removeListener("SIGINT", onSignal);
|
|
8030
|
+
process.removeListener("SIGTERM", onSignal);
|
|
8031
|
+
}
|
|
8032
|
+
});
|
|
7910
8033
|
program2.command("results").description("List recent check results").option("--monitor <id>", "filter by monitor id").option("--limit <n>", "max rows", parseInteger, 20).option("-j, --json", "print JSON").action((opts) => {
|
|
7911
8034
|
try {
|
|
7912
8035
|
const svc = service();
|
|
@@ -8299,6 +8422,101 @@ function renderPrivateProbeConfig(config) {
|
|
|
8299
8422
|
].join(`
|
|
8300
8423
|
`);
|
|
8301
8424
|
}
|
|
8425
|
+
function renderHostedPublicChecksWorkerSummary(summary) {
|
|
8426
|
+
return [
|
|
8427
|
+
"hosted public checks worker",
|
|
8428
|
+
`status: ${summary.status}`,
|
|
8429
|
+
`workspace: ${summary.workspaceId ?? "<unset>"}`,
|
|
8430
|
+
`iterations: ${summary.iterations}`,
|
|
8431
|
+
`checked: ${summary.checked}`,
|
|
8432
|
+
`started: ${summary.startedAt}`,
|
|
8433
|
+
`finished: ${summary.finishedAt}`
|
|
8434
|
+
].join(`
|
|
8435
|
+
`);
|
|
8436
|
+
}
|
|
8437
|
+
function parseWorkerRole(value) {
|
|
8438
|
+
if (value === "scheduler" || value === "public-probe" || value === "reporter" || value === "migration")
|
|
8439
|
+
return value;
|
|
8440
|
+
throw new Error(`Unknown hosted worker role: ${value}`);
|
|
8441
|
+
}
|
|
8442
|
+
function buildHostedWorkerPreflight(role) {
|
|
8443
|
+
const mode = process.env.HASNA_UPTIME_MODE?.trim() || "";
|
|
8444
|
+
const component = process.env.HASNA_UPTIME_COMPONENT?.trim() || "";
|
|
8445
|
+
const workspaceId = process.env.HASNA_UPTIME_WORKSPACE_ID?.trim() || "";
|
|
8446
|
+
const checks = [
|
|
8447
|
+
{ name: "hosted-mode", ok: mode === "hosted", detail: mode || "<unset>" },
|
|
8448
|
+
{ name: "component", ok: !component || component === role, detail: component || "<unset>" },
|
|
8449
|
+
{ name: "workspace", ok: Boolean(workspaceId), detail: workspaceId || "<unset>" },
|
|
8450
|
+
{ name: "postgres-adapter", ok: false, detail: "not implemented" },
|
|
8451
|
+
{ name: "cloud-worker-leases", ok: false, detail: "not implemented" }
|
|
8452
|
+
];
|
|
8453
|
+
if (role === "reporter") {
|
|
8454
|
+
checks.push({ name: "cloud-channel-refs", ok: false, detail: "not implemented" });
|
|
8455
|
+
}
|
|
8456
|
+
if (role === "public-probe") {
|
|
8457
|
+
checks.push({ name: "public-probe-job-claims", ok: false, detail: "not implemented" });
|
|
8458
|
+
}
|
|
8459
|
+
if (role === "migration") {
|
|
8460
|
+
checks.push({ name: "cloud-migration-plan", ok: false, detail: "not implemented" });
|
|
8461
|
+
}
|
|
8462
|
+
const blockers = checks.filter((check) => !check.ok).map((check) => `${check.name}: ${check.detail}`);
|
|
8463
|
+
return {
|
|
8464
|
+
kind: "open-uptime.hosted-worker-preflight",
|
|
8465
|
+
role,
|
|
8466
|
+
status: "blocked",
|
|
8467
|
+
canStart: false,
|
|
8468
|
+
mode: mode || "<unset>",
|
|
8469
|
+
component: component || "<unset>",
|
|
8470
|
+
workspaceId: workspaceId || null,
|
|
8471
|
+
blockers,
|
|
8472
|
+
checks,
|
|
8473
|
+
nextActions: hostedWorkerNextActions(role)
|
|
8474
|
+
};
|
|
8475
|
+
}
|
|
8476
|
+
function hostedWorkerNextActions(role) {
|
|
8477
|
+
const shared = [
|
|
8478
|
+
"Keep the ECS service desired count at 0 until this preflight reports canStart=true.",
|
|
8479
|
+
"Move authoritative hosted state from the EFS SQLite bridge to the cloud store with transactional leases."
|
|
8480
|
+
];
|
|
8481
|
+
if (role === "scheduler") {
|
|
8482
|
+
return [
|
|
8483
|
+
...shared,
|
|
8484
|
+
"Implement deterministic check_jobs creation with a scheduler lease and duplicate-slot protection."
|
|
8485
|
+
];
|
|
8486
|
+
}
|
|
8487
|
+
if (role === "public-probe") {
|
|
8488
|
+
return [
|
|
8489
|
+
...shared,
|
|
8490
|
+
"Implement cloud job claiming, fencing tokens, target-policy decision logs, and result ingestion for public HTTP/TCP checks."
|
|
8491
|
+
];
|
|
8492
|
+
}
|
|
8493
|
+
if (role === "reporter") {
|
|
8494
|
+
return [
|
|
8495
|
+
...shared,
|
|
8496
|
+
"Implement workspace-authorized report channel refs, idempotent delivery keys, retry/backoff, and delivery alarms."
|
|
8497
|
+
];
|
|
8498
|
+
}
|
|
8499
|
+
return [
|
|
8500
|
+
...shared,
|
|
8501
|
+
"Implement reviewed cloud schema migrations with dry-run counts, backup evidence, and rollback instructions."
|
|
8502
|
+
];
|
|
8503
|
+
}
|
|
8504
|
+
function renderHostedWorkerPreflight(preflight) {
|
|
8505
|
+
return [
|
|
8506
|
+
`${preflight.role} hosted worker preflight`,
|
|
8507
|
+
`status: ${preflight.status}`,
|
|
8508
|
+
`can start: ${preflight.canStart}`,
|
|
8509
|
+
`mode: ${sanitizeField(preflight.mode)}`,
|
|
8510
|
+
`component: ${sanitizeField(preflight.component)}`,
|
|
8511
|
+
`workspace: ${sanitizeField(preflight.workspaceId ?? "<unset>")}`,
|
|
8512
|
+
`blockers: ${preflight.blockers.length}`,
|
|
8513
|
+
...preflight.blockers.map((blocker) => `- ${sanitizeField(blocker)}`)
|
|
8514
|
+
].join(`
|
|
8515
|
+
`);
|
|
8516
|
+
}
|
|
8517
|
+
function hostedWorkerEnvironmentOk(preflight) {
|
|
8518
|
+
return preflight.checks.filter((check) => check.name === "hosted-mode" || check.name === "component" || check.name === "workspace").every((check) => check.ok);
|
|
8519
|
+
}
|
|
8302
8520
|
function renderDeliveries(deliveries) {
|
|
8303
8521
|
if (deliveries.length === 0)
|
|
8304
8522
|
return "No report deliveries requested";
|
package/dist/cloud-plan.js
CHANGED
|
@@ -22,7 +22,7 @@ function buildAwsDeploymentPlan(options = {}) {
|
|
|
22
22
|
const image = clean(options.image, `${imageRepositoryUri}@sha256:<image-digest>`);
|
|
23
23
|
const evidenceBucket = clean(options.evidenceBucket, `hasna-${stage}-${prefix}-evidence`);
|
|
24
24
|
const hostedSqliteDbPath = clean(options.hostedSqliteDbPath, DEFAULT_HOSTED_SQLITE_DB);
|
|
25
|
-
const runtimePackageVersion = clean(options.runtimePackageVersion, "0.1.
|
|
25
|
+
const runtimePackageVersion = clean(options.runtimePackageVersion, "0.1.26");
|
|
26
26
|
const runtimePackageIntegrity = options.runtimePackageIntegrity?.trim() || undefined;
|
|
27
27
|
const protectedAccessMode = options.protectedAccessMode ?? DEFAULT_PROTECTED_ACCESS_MODE;
|
|
28
28
|
const cloudfrontOriginProtocolPolicy = options.cloudfrontOriginProtocolPolicy ?? DEFAULT_CLOUDFRONT_ORIGIN_PROTOCOL_POLICY;
|
|
@@ -98,7 +98,7 @@ function buildAwsDeploymentPlan(options = {}) {
|
|
|
98
98
|
domainName: cloudfrontOriginProtocolPolicy === "https-only" ? cloudfrontOriginDomainName : "<alb-dns-name>",
|
|
99
99
|
requiresMatchingCertificate: cloudfrontOriginProtocolPolicy === "https-only",
|
|
100
100
|
liveTrafficApproved: false,
|
|
101
|
-
risk: cloudfrontOriginProtocolPolicy === "http-only" ? "Temporary HTTP-origin bridge:
|
|
101
|
+
risk: cloudfrontOriginProtocolPolicy === "http-only" ? "Temporary HTTP-origin bridge: web scale-up requires allow_cloudfront_http_origin_live_traffic=true for bounded smokes, or switch to https-only with cloudfront_origin_domain_name plus certificate_arn." : "CloudFront HTTPS-origin mode requires the origin hostname to resolve to the ALB and match certificate_arn."
|
|
102
102
|
} : undefined,
|
|
103
103
|
originVerification: protectedAccessMode === "cloudfront_default_domain" ? {
|
|
104
104
|
mode: "cloudfront_origin_header",
|
|
@@ -159,7 +159,7 @@ function buildAwsDeploymentPlan(options = {}) {
|
|
|
159
159
|
`Infra PR must declare CodeBuild image builder ${prefix}-${stage}-image-builder for @hasna/uptime@${runtimePackageVersion}.`,
|
|
160
160
|
`Infra PR must declare hardened S3 evidence bucket ${evidenceBucket} with KMS, versioning, lifecycle, and public access block.`,
|
|
161
161
|
`Infra PR must declare encrypted EFS ${prefix}-${stage}-data with access point, mount targets, and AWS Backup plan.`,
|
|
162
|
-
protectedAccessMode === "cloudfront_default_domain" ? "Infra PR must declare CloudFront default-domain HTTPS edge, ALB origin listener restricted to CloudFront origin-facing ranges, CloudFront-only origin verification header binding, ECS/Fargate cluster, target groups, security groups, IAM roles, CloudWatch log groups, and Secrets Manager refs. Token-bearing live traffic must use cloudfront_origin_protocol_policy=https-only with a matching origin hostname/certificate, or carry explicit
|
|
162
|
+
protectedAccessMode === "cloudfront_default_domain" ? "Infra PR must declare CloudFront default-domain HTTPS edge, ALB origin listener restricted to CloudFront origin-facing ranges, CloudFront-only origin verification header binding, ECS/Fargate cluster, target groups, security groups, IAM roles, CloudWatch log groups, and Secrets Manager refs. Token-bearing live traffic must use cloudfront_origin_protocol_policy=https-only with a matching origin hostname/certificate, or carry explicit allow_cloudfront_http_origin_live_traffic=true bounded-smoke risk acceptance." : `Infra PR must declare ECS/Fargate cluster ${cluster}, ALB HTTPS listener, target groups, security groups, IAM roles, CloudWatch log groups, and Secrets Manager refs.`,
|
|
163
163
|
"Only apply the infra plan from the approved infrastructure repository after review evidence is attached."
|
|
164
164
|
],
|
|
165
165
|
deploy: [
|
|
@@ -168,7 +168,7 @@ function buildAwsDeploymentPlan(options = {}) {
|
|
|
168
168
|
"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.",
|
|
169
169
|
`Register task definitions for ${services.map((service) => service.name).join(", ")} using valueFrom secrets.`,
|
|
170
170
|
`Update ECS services in cluster ${cluster} one component at a time through the approved deploy pipeline.`,
|
|
171
|
-
protectedAccessMode === "cloudfront_default_domain" ? "Use the CloudFront default HTTPS domain with origin verification header binding for first protected access; before token-bearing live traffic, switch the origin to https-only with a matching origin hostname/certificate or
|
|
171
|
+
protectedAccessMode === "cloudfront_default_domain" ? "Use the CloudFront default HTTPS domain with origin verification header binding for first protected access; before token-bearing live traffic, switch the origin to https-only with a matching origin hostname/certificate or set allow_cloudfront_http_origin_live_traffic=true only for a bounded approved smoke." : `Create Route53/edge record for ${hostname} only after ALB health checks pass and auth denial smokes succeed.`
|
|
172
172
|
],
|
|
173
173
|
rollback: [
|
|
174
174
|
"Keep previous task definition ARNs before each service update.",
|
|
@@ -185,7 +185,7 @@ function buildAwsDeploymentPlan(options = {}) {
|
|
|
185
185
|
blockers: [
|
|
186
186
|
"The infrastructure owner repository was not found in this workspace.",
|
|
187
187
|
protectedAccessMode === "cloudfront_default_domain" ? "CloudFront origin verification header binding must be enabled and direct-origin denial must be proven before web desired count is raised above 0." : "ALB HTTPS ingress policy and auth-denial smokes must be proven before web desired count is raised above 0.",
|
|
188
|
-
...protectedAccessMode === "cloudfront_default_domain" && cloudfrontOriginProtocolPolicy === "http-only" ? ["CloudFront-to-ALB origin transport is still http-only; token-bearing live traffic
|
|
188
|
+
...protectedAccessMode === "cloudfront_default_domain" && cloudfrontOriginProtocolPolicy === "http-only" ? ["CloudFront-to-ALB origin transport is still http-only; web scale-up now requires explicit allow_cloudfront_http_origin_live_traffic=true risk acceptance, and token-bearing live traffic should use https-only origin mode."] : [],
|
|
189
189
|
...protectedAccessMode === "cloudfront_default_domain" && cloudfrontOriginProtocolPolicy === "https-only" && cloudfrontOriginDomainName === "<alb-dns-name>" ? ["CloudFront https-only origin mode needs cloudfront_origin_domain_name that resolves to the ALB and matches certificate_arn."] : [],
|
|
190
190
|
"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.",
|
|
191
191
|
"Hosted production auth/RBAC must replace broad static hosted-token operation before exposure.",
|
package/dist/index.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ 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
8
|
export { buildAwsDeploymentPlan, buildPrivateProbeCloudConfig, renderPrivateProbeEnv } from "./cloud-plan.js";
|
|
9
|
+
export { runHostedPublicChecksWorker } from "./workers.js";
|
|
9
10
|
export { uptimeHome, uptimeDbPath, uptimeHostedFallbackDbPath, ensureUptimeHome } from "./paths.js";
|
|
10
11
|
export type { UptimeBackup, UptimeBackupCheck, UptimeRuntimeMode, UptimeStoreOptions, MonitorProvenance, SaveImportBatchInput, StoredImportBatch, UpsertMonitorProvenanceInput, } from "./store.js";
|
|
11
12
|
export type { BrowserPageRunner, BrowserPageRunnerResult, FetchLike, HostedDnsResolver, HostedHttpCheckOptions, HostedHttpRequestContext, HostedHttpRequestLike, HostedHttpResponse, HostedTcpCheckOptions, MonitorCheckOptions, } from "./checks.js";
|
|
@@ -13,5 +14,6 @@ export type { ImportAction, ImportApplyItem, ImportApplyResult, ImportCandidate,
|
|
|
13
14
|
export type { BrowserFailedRequest, BrowserPageEvidence, AuditEvent, CheckAttemptResult, CheckEvidence, CheckResult, CheckStatus, CreateMonitorKind, CreateMonitorInput, CreateReportScheduleInput, ImportedMonitorInput, ImportedUpdateMonitorInput, EvidenceArtifact, HttpTargetPolicyDecision, HttpTargetPolicyEvidence, 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
15
|
export type { ProbeKeyPair, ProbeSigningInput } from "./probes.js";
|
|
15
16
|
export type { AwsDeploymentPlan, AwsDeploymentPlanOptions, AwsServicePlan, PrivateProbeCloudConfig, PrivateProbeCloudConfigOptions, } from "./cloud-plan.js";
|
|
17
|
+
export type { HostedPublicCheckRunner, HostedPublicChecksWorkerIteration, HostedPublicChecksWorkerOptions, HostedPublicChecksWorkerSummary, } from "./workers.js";
|
|
16
18
|
export type { BuildUptimeReportOptions, SendUptimeReportOptions, UptimeEmailReportTarget, UptimeLogsReportTarget, UptimeReport, UptimeReportDelivery, UptimeSmsReportTarget, } from "./report.js";
|
|
17
19
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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,EACL,qBAAqB,EACrB,0BAA0B,EAC1B,iCAAiC,EACjC,mBAAmB,EACnB,kBAAkB,EAClB,iBAAiB,EACjB,eAAe,EACf,YAAY,EACZ,WAAW,GACZ,MAAM,aAAa,CAAC;AACrB,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,EACT,iBAAiB,EACjB,sBAAsB,EACtB,wBAAwB,EACxB,qBAAqB,EACrB,kBAAkB,EAClB,qBAAqB,EACrB,mBAAmB,GACpB,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,wBAAwB,EACxB,wBAAwB,EACxB,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"}
|
|
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,EACL,qBAAqB,EACrB,0BAA0B,EAC1B,iCAAiC,EACjC,mBAAmB,EACnB,kBAAkB,EAClB,iBAAiB,EACjB,eAAe,EACf,YAAY,EACZ,WAAW,GACZ,MAAM,aAAa,CAAC;AACrB,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,2BAA2B,EAAE,MAAM,cAAc,CAAC;AAC3D,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,EACT,iBAAiB,EACjB,sBAAsB,EACtB,wBAAwB,EACxB,qBAAqB,EACrB,kBAAkB,EAClB,qBAAqB,EACrB,mBAAmB,GACpB,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,wBAAwB,EACxB,wBAAwB,EACxB,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,uBAAuB,EACvB,iCAAiC,EACjC,+BAA+B,EAC/B,+BAA+B,GAChC,MAAM,cAAc,CAAC;AACtB,YAAY,EACV,wBAAwB,EACxB,uBAAuB,EACvB,uBAAuB,EACvB,sBAAsB,EACtB,YAAY,EACZ,oBAAoB,EACpB,qBAAqB,GACtB,MAAM,aAAa,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -4636,7 +4636,7 @@ function buildAwsDeploymentPlan(options = {}) {
|
|
|
4636
4636
|
const image = clean(options.image, `${imageRepositoryUri}@sha256:<image-digest>`);
|
|
4637
4637
|
const evidenceBucket = clean(options.evidenceBucket, `hasna-${stage}-${prefix}-evidence`);
|
|
4638
4638
|
const hostedSqliteDbPath = clean(options.hostedSqliteDbPath, DEFAULT_HOSTED_SQLITE_DB);
|
|
4639
|
-
const runtimePackageVersion = clean(options.runtimePackageVersion, "0.1.
|
|
4639
|
+
const runtimePackageVersion = clean(options.runtimePackageVersion, "0.1.26");
|
|
4640
4640
|
const runtimePackageIntegrity = options.runtimePackageIntegrity?.trim() || undefined;
|
|
4641
4641
|
const protectedAccessMode = options.protectedAccessMode ?? DEFAULT_PROTECTED_ACCESS_MODE;
|
|
4642
4642
|
const cloudfrontOriginProtocolPolicy = options.cloudfrontOriginProtocolPolicy ?? DEFAULT_CLOUDFRONT_ORIGIN_PROTOCOL_POLICY;
|
|
@@ -4712,7 +4712,7 @@ function buildAwsDeploymentPlan(options = {}) {
|
|
|
4712
4712
|
domainName: cloudfrontOriginProtocolPolicy === "https-only" ? cloudfrontOriginDomainName : "<alb-dns-name>",
|
|
4713
4713
|
requiresMatchingCertificate: cloudfrontOriginProtocolPolicy === "https-only",
|
|
4714
4714
|
liveTrafficApproved: false,
|
|
4715
|
-
risk: cloudfrontOriginProtocolPolicy === "http-only" ? "Temporary HTTP-origin bridge:
|
|
4715
|
+
risk: cloudfrontOriginProtocolPolicy === "http-only" ? "Temporary HTTP-origin bridge: web scale-up requires allow_cloudfront_http_origin_live_traffic=true for bounded smokes, or switch to https-only with cloudfront_origin_domain_name plus certificate_arn." : "CloudFront HTTPS-origin mode requires the origin hostname to resolve to the ALB and match certificate_arn."
|
|
4716
4716
|
} : undefined,
|
|
4717
4717
|
originVerification: protectedAccessMode === "cloudfront_default_domain" ? {
|
|
4718
4718
|
mode: "cloudfront_origin_header",
|
|
@@ -4773,7 +4773,7 @@ function buildAwsDeploymentPlan(options = {}) {
|
|
|
4773
4773
|
`Infra PR must declare CodeBuild image builder ${prefix}-${stage}-image-builder for @hasna/uptime@${runtimePackageVersion}.`,
|
|
4774
4774
|
`Infra PR must declare hardened S3 evidence bucket ${evidenceBucket} with KMS, versioning, lifecycle, and public access block.`,
|
|
4775
4775
|
`Infra PR must declare encrypted EFS ${prefix}-${stage}-data with access point, mount targets, and AWS Backup plan.`,
|
|
4776
|
-
protectedAccessMode === "cloudfront_default_domain" ? "Infra PR must declare CloudFront default-domain HTTPS edge, ALB origin listener restricted to CloudFront origin-facing ranges, CloudFront-only origin verification header binding, ECS/Fargate cluster, target groups, security groups, IAM roles, CloudWatch log groups, and Secrets Manager refs. Token-bearing live traffic must use cloudfront_origin_protocol_policy=https-only with a matching origin hostname/certificate, or carry explicit
|
|
4776
|
+
protectedAccessMode === "cloudfront_default_domain" ? "Infra PR must declare CloudFront default-domain HTTPS edge, ALB origin listener restricted to CloudFront origin-facing ranges, CloudFront-only origin verification header binding, ECS/Fargate cluster, target groups, security groups, IAM roles, CloudWatch log groups, and Secrets Manager refs. Token-bearing live traffic must use cloudfront_origin_protocol_policy=https-only with a matching origin hostname/certificate, or carry explicit allow_cloudfront_http_origin_live_traffic=true bounded-smoke risk acceptance." : `Infra PR must declare ECS/Fargate cluster ${cluster}, ALB HTTPS listener, target groups, security groups, IAM roles, CloudWatch log groups, and Secrets Manager refs.`,
|
|
4777
4777
|
"Only apply the infra plan from the approved infrastructure repository after review evidence is attached."
|
|
4778
4778
|
],
|
|
4779
4779
|
deploy: [
|
|
@@ -4782,7 +4782,7 @@ function buildAwsDeploymentPlan(options = {}) {
|
|
|
4782
4782
|
"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.",
|
|
4783
4783
|
`Register task definitions for ${services.map((service) => service.name).join(", ")} using valueFrom secrets.`,
|
|
4784
4784
|
`Update ECS services in cluster ${cluster} one component at a time through the approved deploy pipeline.`,
|
|
4785
|
-
protectedAccessMode === "cloudfront_default_domain" ? "Use the CloudFront default HTTPS domain with origin verification header binding for first protected access; before token-bearing live traffic, switch the origin to https-only with a matching origin hostname/certificate or
|
|
4785
|
+
protectedAccessMode === "cloudfront_default_domain" ? "Use the CloudFront default HTTPS domain with origin verification header binding for first protected access; before token-bearing live traffic, switch the origin to https-only with a matching origin hostname/certificate or set allow_cloudfront_http_origin_live_traffic=true only for a bounded approved smoke." : `Create Route53/edge record for ${hostname} only after ALB health checks pass and auth denial smokes succeed.`
|
|
4786
4786
|
],
|
|
4787
4787
|
rollback: [
|
|
4788
4788
|
"Keep previous task definition ARNs before each service update.",
|
|
@@ -4799,7 +4799,7 @@ function buildAwsDeploymentPlan(options = {}) {
|
|
|
4799
4799
|
blockers: [
|
|
4800
4800
|
"The infrastructure owner repository was not found in this workspace.",
|
|
4801
4801
|
protectedAccessMode === "cloudfront_default_domain" ? "CloudFront origin verification header binding must be enabled and direct-origin denial must be proven before web desired count is raised above 0." : "ALB HTTPS ingress policy and auth-denial smokes must be proven before web desired count is raised above 0.",
|
|
4802
|
-
...protectedAccessMode === "cloudfront_default_domain" && cloudfrontOriginProtocolPolicy === "http-only" ? ["CloudFront-to-ALB origin transport is still http-only; token-bearing live traffic
|
|
4802
|
+
...protectedAccessMode === "cloudfront_default_domain" && cloudfrontOriginProtocolPolicy === "http-only" ? ["CloudFront-to-ALB origin transport is still http-only; web scale-up now requires explicit allow_cloudfront_http_origin_live_traffic=true risk acceptance, and token-bearing live traffic should use https-only origin mode."] : [],
|
|
4803
4803
|
...protectedAccessMode === "cloudfront_default_domain" && cloudfrontOriginProtocolPolicy === "https-only" && cloudfrontOriginDomainName === "<alb-dns-name>" ? ["CloudFront https-only origin mode needs cloudfront_origin_domain_name that resolves to the ALB and matches certificate_arn."] : [],
|
|
4804
4804
|
"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.",
|
|
4805
4805
|
"Hosted production auth/RBAC must replace broad static hosted-token operation before exposure.",
|
|
@@ -4936,6 +4936,72 @@ function shellEscape(value) {
|
|
|
4936
4936
|
return value;
|
|
4937
4937
|
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
4938
4938
|
}
|
|
4939
|
+
|
|
4940
|
+
// src/workers.ts
|
|
4941
|
+
var DEFAULT_INTERVAL_MS = 30000;
|
|
4942
|
+
async function runHostedPublicChecksWorker(options) {
|
|
4943
|
+
const intervalMs = normalizePositiveInteger(options.intervalMs ?? DEFAULT_INTERVAL_MS, "intervalMs");
|
|
4944
|
+
const maxRuntimeMs = options.maxRuntimeMs === undefined ? undefined : normalizePositiveInteger(options.maxRuntimeMs, "maxRuntimeMs");
|
|
4945
|
+
const maxIterations = options.maxIterations === undefined ? undefined : normalizePositiveInteger(options.maxIterations, "maxIterations");
|
|
4946
|
+
const clock = options.now ?? (() => new Date);
|
|
4947
|
+
const sleep = options.sleep ?? abortableSleep;
|
|
4948
|
+
const startedAtDate = clock();
|
|
4949
|
+
const startedAt = startedAtDate.toISOString();
|
|
4950
|
+
const deadline = maxRuntimeMs === undefined ? undefined : startedAtDate.getTime() + maxRuntimeMs;
|
|
4951
|
+
let iterations = 0;
|
|
4952
|
+
let checked = 0;
|
|
4953
|
+
while (!options.signal?.aborted) {
|
|
4954
|
+
if (maxIterations !== undefined && iterations >= maxIterations)
|
|
4955
|
+
break;
|
|
4956
|
+
const now = clock();
|
|
4957
|
+
if (deadline !== undefined && now.getTime() >= deadline)
|
|
4958
|
+
break;
|
|
4959
|
+
const iteration = iterations + 1;
|
|
4960
|
+
const iterationStartedAt = now.toISOString();
|
|
4961
|
+
const results = await options.runner.runDueHostedPublicChecks(now, { workspaceId: options.workspaceId });
|
|
4962
|
+
const finishedAt = clock().toISOString();
|
|
4963
|
+
iterations = iteration;
|
|
4964
|
+
checked += results.length;
|
|
4965
|
+
options.onIteration?.({
|
|
4966
|
+
iteration,
|
|
4967
|
+
checked: results.length,
|
|
4968
|
+
startedAt: iterationStartedAt,
|
|
4969
|
+
finishedAt
|
|
4970
|
+
});
|
|
4971
|
+
if (maxIterations !== undefined && iterations >= maxIterations)
|
|
4972
|
+
break;
|
|
4973
|
+
if (deadline !== undefined && clock().getTime() >= deadline)
|
|
4974
|
+
break;
|
|
4975
|
+
await sleep(intervalMs, options.signal);
|
|
4976
|
+
}
|
|
4977
|
+
return {
|
|
4978
|
+
kind: "open-uptime.hosted-public-checks-worker",
|
|
4979
|
+
status: options.signal?.aborted ? "stopped" : "completed",
|
|
4980
|
+
workspaceId: options.workspaceId?.trim() || null,
|
|
4981
|
+
iterations,
|
|
4982
|
+
checked,
|
|
4983
|
+
startedAt,
|
|
4984
|
+
finishedAt: clock().toISOString()
|
|
4985
|
+
};
|
|
4986
|
+
}
|
|
4987
|
+
function normalizePositiveInteger(value, name) {
|
|
4988
|
+
if (!Number.isInteger(value) || value <= 0)
|
|
4989
|
+
throw new Error(`${name} must be a positive integer`);
|
|
4990
|
+
return value;
|
|
4991
|
+
}
|
|
4992
|
+
function abortableSleep(ms, signal) {
|
|
4993
|
+
if (signal?.aborted)
|
|
4994
|
+
return Promise.resolve();
|
|
4995
|
+
return new Promise((resolve) => {
|
|
4996
|
+
const timer = setTimeout(done, ms);
|
|
4997
|
+
function done() {
|
|
4998
|
+
signal?.removeEventListener("abort", done);
|
|
4999
|
+
clearTimeout(timer);
|
|
5000
|
+
resolve();
|
|
5001
|
+
}
|
|
5002
|
+
signal?.addEventListener("abort", done, { once: true });
|
|
5003
|
+
});
|
|
5004
|
+
}
|
|
4939
5005
|
export {
|
|
4940
5006
|
verifyProbeResultSignature,
|
|
4941
5007
|
uptimeHostedFallbackDbPath,
|
|
@@ -4948,6 +5014,7 @@ export {
|
|
|
4948
5014
|
runMonitorCheck,
|
|
4949
5015
|
runHttpCheck,
|
|
4950
5016
|
runHostedTcpCheck,
|
|
5017
|
+
runHostedPublicChecksWorker,
|
|
4951
5018
|
runHostedHttpCheck,
|
|
4952
5019
|
runBrowserPageCheck,
|
|
4953
5020
|
rollbackImport,
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { CheckResult } from "./types.js";
|
|
2
|
+
export interface HostedPublicCheckRunner {
|
|
3
|
+
runDueHostedPublicChecks(now?: Date, options?: {
|
|
4
|
+
workspaceId?: string;
|
|
5
|
+
}): Promise<CheckResult[]>;
|
|
6
|
+
}
|
|
7
|
+
export interface HostedPublicChecksWorkerOptions {
|
|
8
|
+
runner: HostedPublicCheckRunner;
|
|
9
|
+
workspaceId?: string;
|
|
10
|
+
intervalMs?: number;
|
|
11
|
+
maxRuntimeMs?: number;
|
|
12
|
+
maxIterations?: number;
|
|
13
|
+
signal?: AbortSignal;
|
|
14
|
+
now?: () => Date;
|
|
15
|
+
sleep?: (ms: number, signal?: AbortSignal) => Promise<void>;
|
|
16
|
+
onIteration?: (iteration: HostedPublicChecksWorkerIteration) => void;
|
|
17
|
+
}
|
|
18
|
+
export interface HostedPublicChecksWorkerIteration {
|
|
19
|
+
iteration: number;
|
|
20
|
+
checked: number;
|
|
21
|
+
startedAt: string;
|
|
22
|
+
finishedAt: string;
|
|
23
|
+
}
|
|
24
|
+
export interface HostedPublicChecksWorkerSummary {
|
|
25
|
+
kind: "open-uptime.hosted-public-checks-worker";
|
|
26
|
+
status: "completed" | "stopped";
|
|
27
|
+
workspaceId: string | null;
|
|
28
|
+
iterations: number;
|
|
29
|
+
checked: number;
|
|
30
|
+
startedAt: string;
|
|
31
|
+
finishedAt: string;
|
|
32
|
+
}
|
|
33
|
+
export declare function runHostedPublicChecksWorker(options: HostedPublicChecksWorkerOptions): Promise<HostedPublicChecksWorkerSummary>;
|
|
34
|
+
//# sourceMappingURL=workers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workers.d.ts","sourceRoot":"","sources":["../src/workers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C,MAAM,WAAW,uBAAuB;IACtC,wBAAwB,CAAC,GAAG,CAAC,EAAE,IAAI,EAAE,OAAO,CAAC,EAAE;QAAE,WAAW,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC,CAAC;CAClG;AAED,MAAM,WAAW,+BAA+B;IAC9C,MAAM,EAAE,uBAAuB,CAAC;IAChC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,GAAG,CAAC,EAAE,MAAM,IAAI,CAAC;IACjB,KAAK,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5D,WAAW,CAAC,EAAE,CAAC,SAAS,EAAE,iCAAiC,KAAK,IAAI,CAAC;CACtE;AAED,MAAM,WAAW,iCAAiC;IAChD,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,+BAA+B;IAC9C,IAAI,EAAE,yCAAyC,CAAC;IAChD,MAAM,EAAE,WAAW,GAAG,SAAS,CAAC;IAChC,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;CACpB;AAID,wBAAsB,2BAA2B,CAAC,OAAO,EAAE,+BAA+B,GAAG,OAAO,CAAC,+BAA+B,CAAC,CA4CpI"}
|
package/dist/workers.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// src/workers.ts
|
|
3
|
+
var DEFAULT_INTERVAL_MS = 30000;
|
|
4
|
+
async function runHostedPublicChecksWorker(options) {
|
|
5
|
+
const intervalMs = normalizePositiveInteger(options.intervalMs ?? DEFAULT_INTERVAL_MS, "intervalMs");
|
|
6
|
+
const maxRuntimeMs = options.maxRuntimeMs === undefined ? undefined : normalizePositiveInteger(options.maxRuntimeMs, "maxRuntimeMs");
|
|
7
|
+
const maxIterations = options.maxIterations === undefined ? undefined : normalizePositiveInteger(options.maxIterations, "maxIterations");
|
|
8
|
+
const clock = options.now ?? (() => new Date);
|
|
9
|
+
const sleep = options.sleep ?? abortableSleep;
|
|
10
|
+
const startedAtDate = clock();
|
|
11
|
+
const startedAt = startedAtDate.toISOString();
|
|
12
|
+
const deadline = maxRuntimeMs === undefined ? undefined : startedAtDate.getTime() + maxRuntimeMs;
|
|
13
|
+
let iterations = 0;
|
|
14
|
+
let checked = 0;
|
|
15
|
+
while (!options.signal?.aborted) {
|
|
16
|
+
if (maxIterations !== undefined && iterations >= maxIterations)
|
|
17
|
+
break;
|
|
18
|
+
const now = clock();
|
|
19
|
+
if (deadline !== undefined && now.getTime() >= deadline)
|
|
20
|
+
break;
|
|
21
|
+
const iteration = iterations + 1;
|
|
22
|
+
const iterationStartedAt = now.toISOString();
|
|
23
|
+
const results = await options.runner.runDueHostedPublicChecks(now, { workspaceId: options.workspaceId });
|
|
24
|
+
const finishedAt = clock().toISOString();
|
|
25
|
+
iterations = iteration;
|
|
26
|
+
checked += results.length;
|
|
27
|
+
options.onIteration?.({
|
|
28
|
+
iteration,
|
|
29
|
+
checked: results.length,
|
|
30
|
+
startedAt: iterationStartedAt,
|
|
31
|
+
finishedAt
|
|
32
|
+
});
|
|
33
|
+
if (maxIterations !== undefined && iterations >= maxIterations)
|
|
34
|
+
break;
|
|
35
|
+
if (deadline !== undefined && clock().getTime() >= deadline)
|
|
36
|
+
break;
|
|
37
|
+
await sleep(intervalMs, options.signal);
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
kind: "open-uptime.hosted-public-checks-worker",
|
|
41
|
+
status: options.signal?.aborted ? "stopped" : "completed",
|
|
42
|
+
workspaceId: options.workspaceId?.trim() || null,
|
|
43
|
+
iterations,
|
|
44
|
+
checked,
|
|
45
|
+
startedAt,
|
|
46
|
+
finishedAt: clock().toISOString()
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function normalizePositiveInteger(value, name) {
|
|
50
|
+
if (!Number.isInteger(value) || value <= 0)
|
|
51
|
+
throw new Error(`${name} must be a positive integer`);
|
|
52
|
+
return value;
|
|
53
|
+
}
|
|
54
|
+
function abortableSleep(ms, signal) {
|
|
55
|
+
if (signal?.aborted)
|
|
56
|
+
return Promise.resolve();
|
|
57
|
+
return new Promise((resolve) => {
|
|
58
|
+
const timer = setTimeout(done, ms);
|
|
59
|
+
function done() {
|
|
60
|
+
signal?.removeEventListener("abort", done);
|
|
61
|
+
clearTimeout(timer);
|
|
62
|
+
resolve();
|
|
63
|
+
}
|
|
64
|
+
signal?.addEventListener("abort", done, { once: true });
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
export {
|
|
68
|
+
runHostedPublicChecksWorker
|
|
69
|
+
};
|
|
@@ -195,9 +195,11 @@ Before setting `desired_counts.web = 1`, verify:
|
|
|
195
195
|
- `HASNA_UPTIME_ALLOWED_ORIGINS` matches the public HTTPS edge origin;
|
|
196
196
|
- CloudFront-to-origin transport is either `https-only` with an origin hostname
|
|
197
197
|
whose certificate matches that hostname, or the HTTP-origin bridge has a
|
|
198
|
-
named risk owner and approval recorded in private evidence
|
|
198
|
+
named risk owner and approval recorded in private evidence by setting
|
|
199
|
+
`allow_cloudfront_http_origin_live_traffic = true`;
|
|
199
200
|
- CloudFront origin access is distribution-bound with the CloudFront-only origin
|
|
200
|
-
verification header
|
|
201
|
+
verification header by setting `enable_cloudfront_origin_verify_header = true`,
|
|
202
|
+
not just narrowed to CloudFront origin-facing ranges;
|
|
201
203
|
- web egress to ECR, Secrets Manager or SSM, CloudWatch Logs, S3, EFS, and any
|
|
202
204
|
required endpoints has been proven from a real ECS task. Terraform endpoint
|
|
203
205
|
ids, route tables, and security-group rules are creation evidence only; the
|
|
@@ -422,9 +424,10 @@ routes are backed by cloud check jobs and cloud audit rows.
|
|
|
422
424
|
hosted public-check runner, records target-policy decision evidence, and
|
|
423
425
|
passes AWS smokes for denied DNS answers, redirect-to-denied targets, and
|
|
424
426
|
address pinning. The SDK and `uptime cloud public-checks run-due` path now
|
|
425
|
-
handle execution-time DNS and redirect enforcement for bounded smokes
|
|
426
|
-
|
|
427
|
-
|
|
427
|
+
handle execution-time DNS and redirect enforcement for bounded smokes. The
|
|
428
|
+
`uptime cloud public-checks worker` command is an EFS SQLite bridge loop for
|
|
429
|
+
controlled smoke testing only; it is not the final cloud
|
|
430
|
+
`check_jobs`/lease/fencing protocol.
|
|
428
431
|
- Do not enable scheduler, public-probe, reporter, or migration workers against
|
|
429
432
|
the EFS SQLite bridge; those services need Postgres/cloud leases first.
|
|
430
433
|
- Do not expose dashboard/API routes without hosted auth and workspace checks.
|
|
@@ -453,8 +453,11 @@ required before browser evidence or public probe scale-out.
|
|
|
453
453
|
It is not live: services remain at desired count `0`, secrets have
|
|
454
454
|
`AWSCURRENT` values, scoped hosted-token descriptors can be used for operator
|
|
455
455
|
smokes, and the HTTPS-origin/custom-hostname path still needs an approved ACM
|
|
456
|
-
cert, DNS record, plan/apply, and edge smoke.
|
|
457
|
-
|
|
456
|
+
cert, DNS record, plan/apply, and edge smoke. Terraform blocks
|
|
457
|
+
`desired_counts.web > 0` in CloudFront mode unless origin verification is
|
|
458
|
+
enabled and either HTTPS-origin mode is configured or
|
|
459
|
+
`allow_cloudfront_http_origin_live_traffic = true` records explicit bounded
|
|
460
|
+
smoke risk acceptance. Full production identity/RBAC is still not implemented.
|
|
458
461
|
- Open Uptime is still SQLite-only for this bridge; only one protected web task
|
|
459
462
|
may write EFS until Postgres and cloud leases exist.
|
|
460
463
|
- Hosted API/dashboard auth, workspace RBAC, target policy, and Postgres leases
|
|
@@ -478,8 +481,12 @@ required before browser evidence or public probe scale-out.
|
|
|
478
481
|
EFS SQLite bridge is explicitly temporary and not the target source of truth.
|
|
479
482
|
- ECS task definitions use secret refs, not plaintext secret values.
|
|
480
483
|
- ECS task definitions include explicit container health checks: web checks
|
|
481
|
-
`/health`, while disabled non-web roles
|
|
482
|
-
|
|
484
|
+
`/health`, while disabled non-web roles run
|
|
485
|
+
`uptime cloud workers preflight --role <role> --healthcheck` and fail their
|
|
486
|
+
environment health check if hosted mode, component identity, or workspace env
|
|
487
|
+
is invalid. Their main commands call fail-closed
|
|
488
|
+
`uptime cloud workers run --role <role>` entrypoints until the real cloud data
|
|
489
|
+
paths are implemented.
|
|
483
490
|
- Public probes cannot reach denied target classes; private monitors require
|
|
484
491
|
private probes and approved inventory refs.
|
|
485
492
|
- Backups, restore drill, rollback sequence, alarms, and cost estimate are
|
|
@@ -430,9 +430,10 @@ ECS/API/RDS/S3/probe lag/job backlog/delivery failures, and rollback commands.
|
|
|
430
430
|
dashboard shell still fails closed; production-grade identity/RBAC is not
|
|
431
431
|
implemented yet.
|
|
432
432
|
- Outbound target policy for hosted HTTP/TCP checks exists in the SDK and the
|
|
433
|
-
`uptime cloud public-checks run-due` operator path.
|
|
434
|
-
|
|
435
|
-
|
|
433
|
+
`uptime cloud public-checks run-due` operator path. A bounded
|
|
434
|
+
`uptime cloud public-checks worker` EFS SQLite bridge loop exists for
|
|
435
|
+
controlled smokes, but the cloud public-probe `check_jobs` lease/fencing path
|
|
436
|
+
and sustained ECS worker readiness are not wired yet.
|
|
436
437
|
- `@hasna/cloud` hybrid mode still returns SQLite, so it is not cloud-primary.
|
|
437
438
|
- The local cloud config currently points at a stale/non-resolving database host.
|
|
438
439
|
- Todos has unresolved conflicts that must be reconciled before cloud cutover.
|
|
@@ -454,7 +455,10 @@ ECS/API/RDS/S3/probe lag/job backlog/delivery failures, and rollback commands.
|
|
|
454
455
|
representative SQLite EFS backup/restore drill with integrity/count checks.
|
|
455
456
|
It is not live: live scale-up is still blocked by edge/auth smokes, approved
|
|
456
457
|
human/on-call SNS subscriptions and delivery smoke, and production auth
|
|
457
|
-
hardening beyond scoped static operator tokens.
|
|
458
|
+
hardening beyond scoped static operator tokens. Terraform now prevents
|
|
459
|
+
accidental `web > 0` promotion in CloudFront mode unless origin verification
|
|
460
|
+
is enabled and either HTTPS-origin mode or explicit HTTP-origin risk
|
|
461
|
+
acceptance is configured.
|
|
458
462
|
- Projects per-project cloud stores do not exist yet; current local
|
|
459
463
|
`project.db` stores are not enough for cloud-backed canvases or JSON Render.
|
|
460
464
|
- Browser/page monitoring lacks the artifact, redaction, retention, and storage
|
package/infra/aws/README.md
CHANGED
|
@@ -90,9 +90,12 @@ delivery, S3 access, and EFS mount behavior.
|
|
|
90
90
|
|
|
91
91
|
Every ECS task definition includes an explicit container health check. The web
|
|
92
92
|
task checks `GET /health` through Bun's built-in `fetch`; disabled non-web roles
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
93
|
+
run `uptime cloud workers preflight --role <role> --healthcheck`, which verifies
|
|
94
|
+
hosted mode, component identity, and workspace env before reporting blocked
|
|
95
|
+
cloud prerequisites. Their main container commands call fail-closed
|
|
96
|
+
`uptime cloud workers run --role <role>` entrypoints so scheduler,
|
|
97
|
+
public-probe, reporter, and migration tasks no longer use `cloud plan` as a
|
|
98
|
+
placeholder.
|
|
96
99
|
|
|
97
100
|
Interface endpoint private DNS is VPC-wide. In shared VPCs, either keep endpoint
|
|
98
101
|
creation in the approved networking root, or pass
|
|
@@ -106,10 +109,16 @@ Store instead of Secrets Manager, add `ssm` to
|
|
|
106
109
|
- Hosted production auth/RBAC still needs scoped, revocable credentials.
|
|
107
110
|
- The default `http-only` CloudFront origin bridge must be replaced with the
|
|
108
111
|
explicit HTTPS-origin mode or consciously accepted with documented risk before
|
|
109
|
-
token-bearing live traffic.
|
|
112
|
+
token-bearing live traffic. The module blocks `desired_counts.web > 0` in
|
|
113
|
+
CloudFront mode unless origin verification is enabled, and it also requires
|
|
114
|
+
either `cloudfront_origin_protocol_policy = "https-only"` or explicit
|
|
115
|
+
`allow_cloudfront_http_origin_live_traffic = true` risk acceptance for bounded
|
|
116
|
+
smokes.
|
|
110
117
|
- Public probe runtime has SDK-level hosted HTTP target-policy enforcement, but
|
|
111
|
-
the public-probe
|
|
112
|
-
|
|
118
|
+
the public-probe cloud check-job lease path is still disabled until it is
|
|
119
|
+
wired to that runner and validated in AWS. The
|
|
120
|
+
`uptime cloud public-checks worker` command is an EFS SQLite bridge smoke loop,
|
|
121
|
+
not the final cloud worker protocol.
|
|
113
122
|
- Hosted private-probe enrollment/heartbeat/revocation is still
|
|
114
123
|
fail-closed.
|
|
115
124
|
|
package/infra/aws/main.tf
CHANGED
|
@@ -40,22 +40,22 @@ locals {
|
|
|
40
40
|
}
|
|
41
41
|
scheduler = {
|
|
42
42
|
desired_count = lookup(var.desired_counts, "scheduler", 0)
|
|
43
|
-
command = ["bun", "dist/cli/index.js", "cloud", "
|
|
43
|
+
command = ["bun", "dist/cli/index.js", "cloud", "workers", "run", "--role", "scheduler"]
|
|
44
44
|
secrets = { APP_ENV = var.app_env_secret_arn }
|
|
45
45
|
}
|
|
46
46
|
"public-probe" = {
|
|
47
47
|
desired_count = lookup(var.desired_counts, "public-probe", 0)
|
|
48
|
-
command = ["bun", "dist/cli/index.js", "cloud", "
|
|
48
|
+
command = ["bun", "dist/cli/index.js", "cloud", "workers", "run", "--role", "public-probe"]
|
|
49
49
|
secrets = { PROBE_CONFIG = var.public_probe_secret_arn }
|
|
50
50
|
}
|
|
51
51
|
reporter = {
|
|
52
52
|
desired_count = lookup(var.desired_counts, "reporter", 0)
|
|
53
|
-
command = ["bun", "dist/cli/index.js", "cloud", "
|
|
53
|
+
command = ["bun", "dist/cli/index.js", "cloud", "workers", "run", "--role", "reporter"]
|
|
54
54
|
secrets = { REPORTING_CONFIG = var.reporting_secret_arn }
|
|
55
55
|
}
|
|
56
56
|
migration = {
|
|
57
57
|
desired_count = lookup(var.desired_counts, "migration", 0)
|
|
58
|
-
command = ["bun", "dist/cli/index.js", "cloud", "
|
|
58
|
+
command = ["bun", "dist/cli/index.js", "cloud", "workers", "run", "--role", "migration"]
|
|
59
59
|
secrets = { APP_ENV = var.app_env_secret_arn }
|
|
60
60
|
}
|
|
61
61
|
}
|
|
@@ -94,28 +94,28 @@ locals {
|
|
|
94
94
|
startPeriod = 30
|
|
95
95
|
}
|
|
96
96
|
scheduler = {
|
|
97
|
-
command = ["CMD-SHELL", "bun
|
|
97
|
+
command = ["CMD-SHELL", "bun dist/cli/index.js cloud workers preflight --role scheduler --healthcheck --json >/tmp/open-uptime-worker-preflight.json"]
|
|
98
98
|
interval = 30
|
|
99
99
|
timeout = 5
|
|
100
100
|
retries = 3
|
|
101
101
|
startPeriod = 30
|
|
102
102
|
}
|
|
103
103
|
"public-probe" = {
|
|
104
|
-
command = ["CMD-SHELL", "bun
|
|
104
|
+
command = ["CMD-SHELL", "bun dist/cli/index.js cloud workers preflight --role public-probe --healthcheck --json >/tmp/open-uptime-worker-preflight.json"]
|
|
105
105
|
interval = 30
|
|
106
106
|
timeout = 5
|
|
107
107
|
retries = 3
|
|
108
108
|
startPeriod = 30
|
|
109
109
|
}
|
|
110
110
|
reporter = {
|
|
111
|
-
command = ["CMD-SHELL", "bun
|
|
111
|
+
command = ["CMD-SHELL", "bun dist/cli/index.js cloud workers preflight --role reporter --healthcheck --json >/tmp/open-uptime-worker-preflight.json"]
|
|
112
112
|
interval = 30
|
|
113
113
|
timeout = 5
|
|
114
114
|
retries = 3
|
|
115
115
|
startPeriod = 30
|
|
116
116
|
}
|
|
117
117
|
migration = {
|
|
118
|
-
command = ["CMD-SHELL", "bun
|
|
118
|
+
command = ["CMD-SHELL", "bun dist/cli/index.js cloud workers preflight --role migration --healthcheck --json >/tmp/open-uptime-worker-preflight.json"]
|
|
119
119
|
interval = 30
|
|
120
120
|
timeout = 5
|
|
121
121
|
retries = 3
|
|
@@ -12,6 +12,7 @@ vpc_id = "vpc-xxxxxxxx"
|
|
|
12
12
|
ecr_repository_name = "open-uptime"
|
|
13
13
|
protected_access_mode = "cloudfront_default_domain"
|
|
14
14
|
cloudfront_origin_protocol_policy = "http-only"
|
|
15
|
+
allow_cloudfront_http_origin_live_traffic = false
|
|
15
16
|
cloudfront_origin_domain_name = null
|
|
16
17
|
enable_cloudfront_origin_verify_header = false
|
|
17
18
|
cloudfront_origin_verify_header_name = "X-Open-Uptime-Origin-Verify"
|
|
@@ -21,7 +22,7 @@ alb_ingress_cidr_blocks = []
|
|
|
21
22
|
private_subnet_ids = ["subnet-replace-private-a", "subnet-replace-private-b"]
|
|
22
23
|
private_route_table_ids = ["rtb-replace-private"]
|
|
23
24
|
container_image = "123456789012.dkr.ecr.us-east-1.amazonaws.com/open-uptime@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
|
24
|
-
runtime_package_version = "0.1.
|
|
25
|
+
runtime_package_version = "0.1.26"
|
|
25
26
|
runtime_package_integrity = null
|
|
26
27
|
certificate_arn = null
|
|
27
28
|
hosted_zone_id = null
|
package/infra/aws/variables.tf
CHANGED
|
@@ -98,6 +98,12 @@ variable "cloudfront_origin_protocol_policy" {
|
|
|
98
98
|
}
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
+
variable "allow_cloudfront_http_origin_live_traffic" {
|
|
102
|
+
description = "Explicit risk acceptance for setting web desired count above 0 while CloudFront-to-ALB origin transport is http-only. Keep false unless a named operator accepts the temporary HTTP-origin bridge risk for a bounded smoke."
|
|
103
|
+
type = bool
|
|
104
|
+
default = false
|
|
105
|
+
}
|
|
106
|
+
|
|
101
107
|
variable "cloudfront_origin_domain_name" {
|
|
102
108
|
description = "DNS hostname CloudFront uses for the ALB custom origin when cloudfront_origin_protocol_policy is https-only. The hostname must resolve to the ALB and match certificate_arn. Leave null for the default HTTP-origin bridge."
|
|
103
109
|
type = string
|
|
@@ -235,7 +241,7 @@ variable "container_image" {
|
|
|
235
241
|
variable "runtime_package_version" {
|
|
236
242
|
description = "Published @hasna/uptime package version that CodeBuild should build into the ECR image."
|
|
237
243
|
type = string
|
|
238
|
-
default = "0.1.
|
|
244
|
+
default = "0.1.26"
|
|
239
245
|
|
|
240
246
|
validation {
|
|
241
247
|
condition = can(regex("^[0-9]+\\.[0-9]+\\.[0-9]+(-[0-9A-Za-z.-]+)?$", var.runtime_package_version))
|
|
@@ -347,6 +353,25 @@ variable "desired_counts" {
|
|
|
347
353
|
])
|
|
348
354
|
error_message = "EFS SQLite bridge requires web desired count 0 or 1 and scheduler/public-probe/reporter/migration desired counts 0."
|
|
349
355
|
}
|
|
356
|
+
|
|
357
|
+
validation {
|
|
358
|
+
condition = (
|
|
359
|
+
lookup(var.desired_counts, "web", 0) == 0
|
|
360
|
+
|| var.protected_access_mode != "cloudfront_default_domain"
|
|
361
|
+
|| var.enable_cloudfront_origin_verify_header
|
|
362
|
+
)
|
|
363
|
+
error_message = "web desired count above 0 in cloudfront_default_domain mode requires enable_cloudfront_origin_verify_header=true."
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
validation {
|
|
367
|
+
condition = (
|
|
368
|
+
lookup(var.desired_counts, "web", 0) == 0
|
|
369
|
+
|| var.protected_access_mode != "cloudfront_default_domain"
|
|
370
|
+
|| var.cloudfront_origin_protocol_policy == "https-only"
|
|
371
|
+
|| var.allow_cloudfront_http_origin_live_traffic
|
|
372
|
+
)
|
|
373
|
+
error_message = "web desired count above 0 requires CloudFront HTTPS-origin mode, or explicit allow_cloudfront_http_origin_live_traffic=true risk acceptance for a bounded smoke."
|
|
374
|
+
}
|
|
350
375
|
}
|
|
351
376
|
|
|
352
377
|
variable "alarm_actions" {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hasna/uptime",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.26",
|
|
4
4
|
"description": "Local-first uptime and downtime monitoring service with CLI, MCP, SDK, SQLite persistence, and a dashboard.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -69,10 +69,14 @@
|
|
|
69
69
|
"./cloud-plan": {
|
|
70
70
|
"types": "./dist/cloud-plan.d.ts",
|
|
71
71
|
"import": "./dist/cloud-plan.js"
|
|
72
|
+
},
|
|
73
|
+
"./workers": {
|
|
74
|
+
"types": "./dist/workers.d.ts",
|
|
75
|
+
"import": "./dist/workers.js"
|
|
72
76
|
}
|
|
73
77
|
},
|
|
74
78
|
"scripts": {
|
|
75
|
-
"build": "rm -rf dist && bun build src/cli/index.ts --outdir dist/cli --target bun --external @modelcontextprotocol/sdk && bun build src/mcp/index.ts --outdir dist/mcp --target bun --external @modelcontextprotocol/sdk && bun build src/index.ts src/api.ts src/service.ts src/store.ts src/checks.ts src/imports.ts src/report.ts src/probes.ts src/cloud-plan.ts src/types.ts src/paths.ts src/dashboard.ts src/version.ts --root src --outdir dist --target bun && tsc -p tsconfig.build.json --emitDeclarationOnly --outDir dist && chmod +x dist/cli/index.js dist/mcp/index.js",
|
|
79
|
+
"build": "rm -rf dist && bun build src/cli/index.ts --outdir dist/cli --target bun --external @modelcontextprotocol/sdk && bun build src/mcp/index.ts --outdir dist/mcp --target bun --external @modelcontextprotocol/sdk && bun build src/index.ts src/api.ts src/service.ts src/store.ts src/checks.ts src/imports.ts src/report.ts src/probes.ts src/cloud-plan.ts src/workers.ts src/types.ts src/paths.ts src/dashboard.ts src/version.ts --root src --outdir dist --target bun && tsc -p tsconfig.build.json --emitDeclarationOnly --outDir dist && chmod +x dist/cli/index.js dist/mcp/index.js",
|
|
76
80
|
"typecheck": "tsc --noEmit",
|
|
77
81
|
"test": "bun test ./tests",
|
|
78
82
|
"dev:cli": "bun run src/cli/index.ts",
|