@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/cli/index.js
CHANGED
|
@@ -5728,12 +5728,18 @@ var MAX_PROBE_RESULT_FUTURE_MS = 5 * 60000;
|
|
|
5728
5728
|
class UptimeService {
|
|
5729
5729
|
store;
|
|
5730
5730
|
checkRunner;
|
|
5731
|
+
hostedResolveHost;
|
|
5732
|
+
hostedHttpRequest;
|
|
5733
|
+
hostedMaxRedirects;
|
|
5731
5734
|
leaseOwner = `svc_${randomUUID3().replace(/-/g, "").slice(0, 18)}`;
|
|
5732
5735
|
inFlightChecks = new Set;
|
|
5733
5736
|
inFlightReportSchedules = new Set;
|
|
5734
5737
|
constructor(options = {}) {
|
|
5735
5738
|
this.store = options.store ?? new UptimeStore({ mode: "local", ...options });
|
|
5736
5739
|
this.checkRunner = options.checkRunner ?? runMonitorCheck;
|
|
5740
|
+
this.hostedResolveHost = options.hostedResolveHost;
|
|
5741
|
+
this.hostedHttpRequest = options.hostedHttpRequest;
|
|
5742
|
+
this.hostedMaxRedirects = options.hostedMaxRedirects;
|
|
5737
5743
|
}
|
|
5738
5744
|
close() {
|
|
5739
5745
|
this.store.close();
|
|
@@ -5940,39 +5946,7 @@ class UptimeService {
|
|
|
5940
5946
|
const monitor = this.store.getMonitor(idOrName);
|
|
5941
5947
|
if (!monitor)
|
|
5942
5948
|
throw new Error(`Monitor not found: ${idOrName}`);
|
|
5943
|
-
|
|
5944
|
-
throw new Error(`Monitor is disabled: ${monitor.name}`);
|
|
5945
|
-
if (this.inFlightChecks.has(monitor.id))
|
|
5946
|
-
throw new Error(`Monitor check already in progress: ${monitor.name}`);
|
|
5947
|
-
const leaseTtlMs = Math.max(60000, (monitor.retryCount + 1) * monitor.timeoutMs + 1e4);
|
|
5948
|
-
if (!this.store.acquireCheckLease(monitor.id, this.leaseOwner, leaseTtlMs)) {
|
|
5949
|
-
throw new MonitorCheckBusyError(`Monitor check already in progress: ${monitor.name}`);
|
|
5950
|
-
}
|
|
5951
|
-
this.inFlightChecks.add(monitor.id);
|
|
5952
|
-
try {
|
|
5953
|
-
let attemptCount = 0;
|
|
5954
|
-
let last = null;
|
|
5955
|
-
const maxAttempts = Math.max(1, monitor.retryCount + 1);
|
|
5956
|
-
while (attemptCount < maxAttempts) {
|
|
5957
|
-
attemptCount += 1;
|
|
5958
|
-
last = await this.checkRunner(monitor);
|
|
5959
|
-
if (last.status === "up")
|
|
5960
|
-
break;
|
|
5961
|
-
}
|
|
5962
|
-
return this.store.recordCheckResult({
|
|
5963
|
-
monitorId: monitor.id,
|
|
5964
|
-
status: last.status,
|
|
5965
|
-
latencyMs: last.latencyMs,
|
|
5966
|
-
statusCode: last.statusCode ?? null,
|
|
5967
|
-
error: last.error ?? null,
|
|
5968
|
-
evidence: last.evidence ?? null,
|
|
5969
|
-
attemptCount,
|
|
5970
|
-
expectedMonitorRevision: monitor.revision
|
|
5971
|
-
});
|
|
5972
|
-
} finally {
|
|
5973
|
-
this.inFlightChecks.delete(monitor.id);
|
|
5974
|
-
this.store.releaseCheckLease(monitor.id, this.leaseOwner);
|
|
5975
|
-
}
|
|
5949
|
+
return this.recordMonitorCheck(monitor, { hostedTargetPolicy: false });
|
|
5976
5950
|
}
|
|
5977
5951
|
async checkAll() {
|
|
5978
5952
|
if (this.store.mode === "hosted")
|
|
@@ -5984,6 +5958,49 @@ class UptimeService {
|
|
|
5984
5958
|
}
|
|
5985
5959
|
return results;
|
|
5986
5960
|
}
|
|
5961
|
+
async checkHostedPublicMonitor(idOrName, options = {}) {
|
|
5962
|
+
this.assertHostedPublicChecksEnabled();
|
|
5963
|
+
const workspaceId = this.requireHostedWorkerWorkspaceId(options.workspaceId);
|
|
5964
|
+
const monitor = this.store.getMonitor(idOrName, { workspaceId });
|
|
5965
|
+
if (!monitor)
|
|
5966
|
+
throw new Error(`Monitor not found: ${idOrName}`);
|
|
5967
|
+
this.assertHostedPublicMonitor(monitor);
|
|
5968
|
+
const result = await this.recordMonitorCheck(monitor, { hostedTargetPolicy: true });
|
|
5969
|
+
this.auditStore().recordAuditEvent({
|
|
5970
|
+
workspaceId,
|
|
5971
|
+
action: "hosted_public_check.run",
|
|
5972
|
+
resourceType: "monitor",
|
|
5973
|
+
resourceId: monitor.id,
|
|
5974
|
+
message: `Ran hosted public check for ${monitor.name}`,
|
|
5975
|
+
metadata: {
|
|
5976
|
+
checkResultId: result.id,
|
|
5977
|
+
status: result.status,
|
|
5978
|
+
monitorKind: monitor.kind,
|
|
5979
|
+
operatorPath: "hosted_public_check"
|
|
5980
|
+
},
|
|
5981
|
+
actor: "hosted-public-check-worker"
|
|
5982
|
+
});
|
|
5983
|
+
return result;
|
|
5984
|
+
}
|
|
5985
|
+
async runDueHostedPublicChecks(now = new Date, options = {}) {
|
|
5986
|
+
this.assertHostedPublicChecksEnabled();
|
|
5987
|
+
const workspaceId = this.requireHostedWorkerWorkspaceId(options.workspaceId);
|
|
5988
|
+
const due = this.store.listMonitors({ workspaceId }).filter((monitor) => this.isHostedPublicMonitor(monitor) && this.isDue(monitor, now));
|
|
5989
|
+
const results = [];
|
|
5990
|
+
for (const monitor of due) {
|
|
5991
|
+
const current = this.store.getMonitor(monitor.id, { workspaceId });
|
|
5992
|
+
if (!current || !this.isHostedPublicMonitor(current) || !this.isDue(current, now))
|
|
5993
|
+
continue;
|
|
5994
|
+
try {
|
|
5995
|
+
results.push(await this.checkHostedPublicMonitor(current.id, { workspaceId }));
|
|
5996
|
+
} catch (error) {
|
|
5997
|
+
if (error instanceof MonitorCheckBusyError || error instanceof StaleCheckResultError)
|
|
5998
|
+
continue;
|
|
5999
|
+
throw error;
|
|
6000
|
+
}
|
|
6001
|
+
}
|
|
6002
|
+
return results;
|
|
6003
|
+
}
|
|
5987
6004
|
startScheduler(options = {}) {
|
|
5988
6005
|
if (this.store.mode === "hosted")
|
|
5989
6006
|
throw new Error("hosted scheduler requires check_jobs and probes");
|
|
@@ -6029,6 +6046,67 @@ class UptimeService {
|
|
|
6029
6046
|
const last = new Date(monitor.lastCheckedAt).getTime();
|
|
6030
6047
|
return now.getTime() - last >= monitor.intervalSeconds * 1000;
|
|
6031
6048
|
}
|
|
6049
|
+
async recordMonitorCheck(monitor, options) {
|
|
6050
|
+
if (!monitor.enabled)
|
|
6051
|
+
throw new Error(`Monitor is disabled: ${monitor.name}`);
|
|
6052
|
+
if (this.inFlightChecks.has(monitor.id))
|
|
6053
|
+
throw new Error(`Monitor check already in progress: ${monitor.name}`);
|
|
6054
|
+
const leaseTtlMs = Math.max(60000, (monitor.retryCount + 1) * monitor.timeoutMs + 1e4);
|
|
6055
|
+
if (!this.store.acquireCheckLease(monitor.id, this.leaseOwner, leaseTtlMs)) {
|
|
6056
|
+
throw new MonitorCheckBusyError(`Monitor check already in progress: ${monitor.name}`);
|
|
6057
|
+
}
|
|
6058
|
+
this.inFlightChecks.add(monitor.id);
|
|
6059
|
+
try {
|
|
6060
|
+
let attemptCount = 0;
|
|
6061
|
+
let last = null;
|
|
6062
|
+
const maxAttempts = Math.max(1, monitor.retryCount + 1);
|
|
6063
|
+
while (attemptCount < maxAttempts) {
|
|
6064
|
+
attemptCount += 1;
|
|
6065
|
+
last = options.hostedTargetPolicy ? await this.runHostedPublicCheckAttempt(monitor) : await this.checkRunner(monitor);
|
|
6066
|
+
if (last.status === "up")
|
|
6067
|
+
break;
|
|
6068
|
+
}
|
|
6069
|
+
return this.store.recordCheckResult({
|
|
6070
|
+
monitorId: monitor.id,
|
|
6071
|
+
status: last.status,
|
|
6072
|
+
latencyMs: last.latencyMs,
|
|
6073
|
+
statusCode: last.statusCode ?? null,
|
|
6074
|
+
error: last.error ?? null,
|
|
6075
|
+
evidence: last.evidence ?? null,
|
|
6076
|
+
attemptCount,
|
|
6077
|
+
expectedMonitorRevision: monitor.revision
|
|
6078
|
+
});
|
|
6079
|
+
} finally {
|
|
6080
|
+
this.inFlightChecks.delete(monitor.id);
|
|
6081
|
+
this.store.releaseCheckLease(monitor.id, this.leaseOwner);
|
|
6082
|
+
}
|
|
6083
|
+
}
|
|
6084
|
+
runHostedPublicCheckAttempt(monitor) {
|
|
6085
|
+
return runMonitorCheck(monitor, {
|
|
6086
|
+
hostedTargetPolicy: true,
|
|
6087
|
+
resolveHost: this.hostedResolveHost,
|
|
6088
|
+
hostedHttpRequest: this.hostedHttpRequest,
|
|
6089
|
+
maxRedirects: this.hostedMaxRedirects
|
|
6090
|
+
});
|
|
6091
|
+
}
|
|
6092
|
+
assertHostedPublicChecksEnabled() {
|
|
6093
|
+
if (this.store.mode !== "hosted")
|
|
6094
|
+
throw new Error("hosted public checks require hosted mode");
|
|
6095
|
+
}
|
|
6096
|
+
requireHostedWorkerWorkspaceId(workspaceId) {
|
|
6097
|
+
const value = workspaceId?.trim() || process.env.HASNA_UPTIME_WORKSPACE_ID?.trim();
|
|
6098
|
+
if (!value)
|
|
6099
|
+
throw new Error("hosted public checks require a workspace id");
|
|
6100
|
+
return value;
|
|
6101
|
+
}
|
|
6102
|
+
assertHostedPublicMonitor(monitor) {
|
|
6103
|
+
if (!this.isHostedPublicMonitor(monitor)) {
|
|
6104
|
+
throw new Error("hosted public checks support only HTTP and TCP monitors");
|
|
6105
|
+
}
|
|
6106
|
+
}
|
|
6107
|
+
isHostedPublicMonitor(monitor) {
|
|
6108
|
+
return monitor.kind === "http" || monitor.kind === "tcp";
|
|
6109
|
+
}
|
|
6032
6110
|
probeStore() {
|
|
6033
6111
|
if (this.store.mode === "hosted") {
|
|
6034
6112
|
throw new Error("hosted probe APIs require cloud check_jobs, workspace stores, and audit logging");
|
|
@@ -7139,6 +7217,7 @@ var DEFAULT_WORKSPACE_ID = "workspace-id";
|
|
|
7139
7217
|
var DEFAULT_VPC_ID = "vpc-xxxxxxxx";
|
|
7140
7218
|
var DEFAULT_HOSTED_SQLITE_DB = "/data/uptime/uptime.db";
|
|
7141
7219
|
var DEFAULT_PROTECTED_ACCESS_MODE = "cloudfront_default_domain";
|
|
7220
|
+
var DEFAULT_CLOUDFRONT_ORIGIN_PROTOCOL_POLICY = "http-only";
|
|
7142
7221
|
function buildAwsDeploymentPlan(options = {}) {
|
|
7143
7222
|
const region = clean(options.region, DEFAULT_REGION);
|
|
7144
7223
|
const stage = clean(options.stage, DEFAULT_STAGE);
|
|
@@ -7151,8 +7230,11 @@ function buildAwsDeploymentPlan(options = {}) {
|
|
|
7151
7230
|
const image = clean(options.image, `${imageRepositoryUri}@sha256:<image-digest>`);
|
|
7152
7231
|
const evidenceBucket = clean(options.evidenceBucket, `hasna-${stage}-${prefix}-evidence`);
|
|
7153
7232
|
const hostedSqliteDbPath = clean(options.hostedSqliteDbPath, DEFAULT_HOSTED_SQLITE_DB);
|
|
7154
|
-
const runtimePackageVersion = clean(options.runtimePackageVersion, "0.1.
|
|
7233
|
+
const runtimePackageVersion = clean(options.runtimePackageVersion, "0.1.25");
|
|
7234
|
+
const runtimePackageIntegrity = options.runtimePackageIntegrity?.trim() || undefined;
|
|
7155
7235
|
const protectedAccessMode = options.protectedAccessMode ?? DEFAULT_PROTECTED_ACCESS_MODE;
|
|
7236
|
+
const cloudfrontOriginProtocolPolicy = options.cloudfrontOriginProtocolPolicy ?? DEFAULT_CLOUDFRONT_ORIGIN_PROTOCOL_POLICY;
|
|
7237
|
+
const cloudfrontOriginDomainName = clean(options.cloudfrontOriginDomainName, "<alb-dns-name>");
|
|
7156
7238
|
const protectedAccessUrl = protectedAccessMode === "cloudfront_default_domain" ? "https://<cloudfront-domain>" : `https://${hostname}`;
|
|
7157
7239
|
const cluster = `${prefix}-${stage}`;
|
|
7158
7240
|
const secrets = {
|
|
@@ -7219,6 +7301,13 @@ function buildAwsDeploymentPlan(options = {}) {
|
|
|
7219
7301
|
protectedAccessMode,
|
|
7220
7302
|
edgeDistribution: protectedAccessMode === "cloudfront_default_domain" ? `${prefix}-${stage}-edge` : undefined,
|
|
7221
7303
|
protectedAccessUrl,
|
|
7304
|
+
cloudfrontOrigin: protectedAccessMode === "cloudfront_default_domain" ? {
|
|
7305
|
+
protocolPolicy: cloudfrontOriginProtocolPolicy,
|
|
7306
|
+
domainName: cloudfrontOriginProtocolPolicy === "https-only" ? cloudfrontOriginDomainName : "<alb-dns-name>",
|
|
7307
|
+
requiresMatchingCertificate: cloudfrontOriginProtocolPolicy === "https-only",
|
|
7308
|
+
liveTrafficApproved: false,
|
|
7309
|
+
risk: cloudfrontOriginProtocolPolicy === "http-only" ? "Temporary HTTP-origin bridge: do not use for token-bearing live traffic without explicit risk acceptance, 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
|
+
} : undefined,
|
|
7222
7311
|
originVerification: protectedAccessMode === "cloudfront_default_domain" ? {
|
|
7223
7312
|
mode: "cloudfront_origin_header",
|
|
7224
7313
|
requiredBeforeScaleUp: true,
|
|
@@ -7251,6 +7340,7 @@ function buildAwsDeploymentPlan(options = {}) {
|
|
|
7251
7340
|
repository: ecrRepository,
|
|
7252
7341
|
uri: image,
|
|
7253
7342
|
dockerfile: "Dockerfile.package",
|
|
7343
|
+
expectedIntegrity: runtimePackageIntegrity,
|
|
7254
7344
|
buildCommand: `BLOCKED: after infra approval, AWS CodeBuild builds Dockerfile.package from @hasna/uptime@${runtimePackageVersion} into ${imageRepositoryUri}`,
|
|
7255
7345
|
pushCommands: [
|
|
7256
7346
|
`BLOCKED: start ${prefix}-${stage}-image-builder only through the approved deploy pipeline after @hasna/uptime@${runtimePackageVersion} is published`,
|
|
@@ -7277,16 +7367,16 @@ function buildAwsDeploymentPlan(options = {}) {
|
|
|
7277
7367
|
`Infra PR must declare CodeBuild image builder ${prefix}-${stage}-image-builder for @hasna/uptime@${runtimePackageVersion}.`,
|
|
7278
7368
|
`Infra PR must declare hardened S3 evidence bucket ${evidenceBucket} with KMS, versioning, lifecycle, and public access block.`,
|
|
7279
7369
|
`Infra PR must declare encrypted EFS ${prefix}-${stage}-data with access point, mount targets, and AWS Backup plan.`,
|
|
7280
|
-
protectedAccessMode === "cloudfront_default_domain" ? "Infra PR must declare CloudFront default-domain HTTPS edge, ALB
|
|
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 HTTP-origin 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.`,
|
|
7281
7371
|
"Only apply the infra plan from the approved infrastructure repository after review evidence is attached."
|
|
7282
7372
|
],
|
|
7283
7373
|
deploy: [
|
|
7284
7374
|
"Build and publish the image only after the Dockerfile/container target is reviewed.",
|
|
7285
|
-
`Start the AWS image builder for @hasna/uptime@${runtimePackageVersion}
|
|
7375
|
+
runtimePackageIntegrity ? `Start the AWS image builder for @hasna/uptime@${runtimePackageVersion}; it must verify npm dist.integrity ${runtimePackageIntegrity} before extracting the package, then record the pushed image digest.` : `Start the AWS image builder for @hasna/uptime@${runtimePackageVersion}; set runtime_package_integrity from npm dist.integrity before live use, then record the pushed image digest.`,
|
|
7286
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.",
|
|
7287
7377
|
`Register task definitions for ${services.map((service) => service.name).join(", ")} using valueFrom secrets.`,
|
|
7288
7378
|
`Update ECS services in cluster ${cluster} one component at a time through the approved deploy pipeline.`,
|
|
7289
|
-
protectedAccessMode === "cloudfront_default_domain" ? "Use the CloudFront default HTTPS domain with origin verification header binding for first protected access;
|
|
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 record explicit HTTP-origin risk acceptance." : `Create Route53/edge record for ${hostname} only after ALB health checks pass and auth denial smokes succeed.`
|
|
7290
7380
|
],
|
|
7291
7381
|
rollback: [
|
|
7292
7382
|
"Keep previous task definition ARNs before each service update.",
|
|
@@ -7303,6 +7393,8 @@ function buildAwsDeploymentPlan(options = {}) {
|
|
|
7303
7393
|
blockers: [
|
|
7304
7394
|
"The infrastructure owner repository was not found in this workspace.",
|
|
7305
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 needs https-only origin mode or explicit risk acceptance."] : [],
|
|
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."] : [],
|
|
7306
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.",
|
|
7307
7399
|
"Hosted production auth/RBAC must replace broad static hosted-token operation before exposure.",
|
|
7308
7400
|
"Public probe execution still needs cloud check-job leases wired to runHostedHttpCheck and live policy-decision log evidence.",
|
|
@@ -7311,8 +7403,9 @@ function buildAwsDeploymentPlan(options = {}) {
|
|
|
7311
7403
|
requiredEvidence: [
|
|
7312
7404
|
"Infrastructure PR/synth/plan from the approved infra repository.",
|
|
7313
7405
|
"CodeBuild image-builder run, container smoke, and immutable image digest.",
|
|
7406
|
+
"Published package dist.integrity pinned in the private infra root or an explicit not-live exception.",
|
|
7314
7407
|
"ECS task definitions using secrets.valueFrom only.",
|
|
7315
|
-
"CloudFront-default-domain origin-header config
|
|
7408
|
+
"CloudFront-default-domain origin-header config, origin transport decision, direct-origin denial evidence, auth-denial smokes, and web alarm checks.",
|
|
7316
7409
|
"Single-writer ECS evidence: one web task maximum and no scheduler/public-probe/reporter EFS mounts.",
|
|
7317
7410
|
"EFS encryption, access point, mount-target, AWS Backup, and restore-drill evidence.",
|
|
7318
7411
|
"S3 bucket KMS, versioning, lifecycle, and public-access-block evidence.",
|
|
@@ -7325,11 +7418,13 @@ function buildAwsDeploymentPlan(options = {}) {
|
|
|
7325
7418
|
notes: [
|
|
7326
7419
|
"This plan generator does not call AWS.",
|
|
7327
7420
|
"Blocked plan output intentionally avoids copy-pastable AWS mutation commands.",
|
|
7328
|
-
"Default protected access uses CloudFront's HTTPS default domain so first deploy is not blocked on custom DNS or ACM.",
|
|
7421
|
+
"Default protected access uses CloudFront's HTTPS default domain so first zero-count deploy is not blocked on custom DNS or ACM.",
|
|
7329
7422
|
"CloudFront default-domain mode still requires origin verification header binding before live scale-up; the header value is sensitive state/config material, not public documentation.",
|
|
7423
|
+
"CloudFront HTTPS-origin mode requires a dedicated origin DNS hostname and matching ACM certificate; do not assume the ALB DNS name can satisfy TLS verification.",
|
|
7330
7424
|
"Hosted runtime uses explicit EFS-backed SQLite at HASNA_UPTIME_HOSTED_SQLITE_DB until the async Postgres adapter exists.",
|
|
7331
7425
|
"Do not set HASNA_UPTIME_DATABASE_URL for hosted tasks until the Postgres adapter is implemented.",
|
|
7332
7426
|
"Secrets are represented as secret names/refs and must be injected with valueFrom.",
|
|
7427
|
+
"Set runtime_package_integrity in the approved infra root after publish so the AWS image builder verifies the npm tarball before ECR build.",
|
|
7333
7428
|
"Actual deploy belongs in the deploy_release_operate_final goal node after infra review."
|
|
7334
7429
|
]
|
|
7335
7430
|
}
|
|
@@ -7396,7 +7491,10 @@ function buildPrivateProbeCloudConfig(options = {}) {
|
|
|
7396
7491
|
}
|
|
7397
7492
|
};
|
|
7398
7493
|
}
|
|
7399
|
-
function renderPrivateProbeEnv(config) {
|
|
7494
|
+
function renderPrivateProbeEnv(config, options = {}) {
|
|
7495
|
+
if (!options.allowBlocked && (!config.canStart || config.blockers.length > 0)) {
|
|
7496
|
+
throw new Error("private probe env output is blocked until hosted probe routes and cloud jobs are implemented");
|
|
7497
|
+
}
|
|
7400
7498
|
const required = ["HASNA_UPTIME_PRIVATE_PROBE_ID"];
|
|
7401
7499
|
const missing = required.filter((key) => !config.env[key]);
|
|
7402
7500
|
if (missing.length > 0) {
|
|
@@ -7439,6 +7537,13 @@ program2.name("uptime").description("Local-first uptime and downtime monitoring"
|
|
|
7439
7537
|
function service() {
|
|
7440
7538
|
return new UptimeService({ mode: "local" });
|
|
7441
7539
|
}
|
|
7540
|
+
function hostedService(opts) {
|
|
7541
|
+
return new UptimeService({
|
|
7542
|
+
mode: "hosted",
|
|
7543
|
+
hostedSqliteDbPath: opts.hostedSqliteDb,
|
|
7544
|
+
allowHostedLocalStore: opts.allowHostedLocalStore
|
|
7545
|
+
});
|
|
7546
|
+
}
|
|
7442
7547
|
function wantsJson(opts) {
|
|
7443
7548
|
return Boolean(opts?.json || program2.opts().json);
|
|
7444
7549
|
}
|
|
@@ -7741,7 +7846,7 @@ program2.command("audit").description("List local audit events").option("--resou
|
|
|
7741
7846
|
}
|
|
7742
7847
|
});
|
|
7743
7848
|
var cloud = program2.command("cloud").description("Generate dry-run cloud deployment and private-probe configuration artifacts");
|
|
7744
|
-
cloud.command("plan").description("Generate a dry-run AWS deployment plan").option("--account <name>", "AWS account/profile label", "aws-profile").option("--region <region>", "AWS region", "us-east-1").option("--stage <stage>", "deployment stage", "prod").option("--hostname <hostname>", "hosted Open Uptime hostname", "uptime.example.com").option("--workspace-id <id>", "workspace id", "workspace-id").option("--vpc-id <id>", "target VPC id").option("--hosted-sqlite-db <path>", "hosted SQLite path on the EFS mount").option("--rds-instance-id <id>", "deprecated; ignored until the hosted Postgres adapter exists").option("--database-secret-name <name>", "deprecated; ignored until the hosted Postgres adapter exists").option("--ecr-repository <name>", "ECR repository name").option("--image <uri>", "container image URI").option("--runtime-package-version <version>", "published @hasna/uptime version for the AWS image builder").addOption(new Option("--protected-access-mode <mode>", "protected web access mode").choices(["cloudfront_default_domain", "alb_https_cert"]).default("cloudfront_default_domain")).option("--evidence-bucket <name>", "S3 evidence bucket name").option("-j, --json", "print JSON").action((opts) => {
|
|
7849
|
+
cloud.command("plan").description("Generate a dry-run AWS deployment plan").option("--account <name>", "AWS account/profile label", "aws-profile").option("--region <region>", "AWS region", "us-east-1").option("--stage <stage>", "deployment stage", "prod").option("--hostname <hostname>", "hosted Open Uptime hostname", "uptime.example.com").option("--workspace-id <id>", "workspace id", "workspace-id").option("--vpc-id <id>", "target VPC id").option("--hosted-sqlite-db <path>", "hosted SQLite path on the EFS mount").option("--rds-instance-id <id>", "deprecated; ignored until the hosted Postgres adapter exists").option("--database-secret-name <name>", "deprecated; ignored until the hosted Postgres adapter exists").option("--ecr-repository <name>", "ECR repository name").option("--image <uri>", "container image URI").option("--runtime-package-version <version>", "published @hasna/uptime version for the AWS image builder").option("--runtime-package-integrity <integrity>", "expected npm dist.integrity for the runtime package").addOption(new Option("--protected-access-mode <mode>", "protected web access mode").choices(["cloudfront_default_domain", "alb_https_cert"]).default("cloudfront_default_domain")).addOption(new Option("--cloudfront-origin-protocol-policy <policy>", "CloudFront-to-ALB origin protocol policy").choices(["http-only", "https-only"]).default("http-only")).option("--cloudfront-origin-domain-name <hostname>", "origin hostname for CloudFront https-only mode; must resolve to the ALB and match certificate_arn").option("--evidence-bucket <name>", "S3 evidence bucket name").option("-j, --json", "print JSON").action((opts) => {
|
|
7745
7850
|
try {
|
|
7746
7851
|
const plan = buildAwsDeploymentPlan({
|
|
7747
7852
|
accountName: opts.account,
|
|
@@ -7756,7 +7861,10 @@ cloud.command("plan").description("Generate a dry-run AWS deployment plan").opti
|
|
|
7756
7861
|
ecrRepository: opts.ecrRepository,
|
|
7757
7862
|
image: opts.image,
|
|
7758
7863
|
runtimePackageVersion: opts.runtimePackageVersion,
|
|
7864
|
+
runtimePackageIntegrity: opts.runtimePackageIntegrity,
|
|
7759
7865
|
protectedAccessMode: opts.protectedAccessMode,
|
|
7866
|
+
cloudfrontOriginProtocolPolicy: opts.cloudfrontOriginProtocolPolicy,
|
|
7867
|
+
cloudfrontOriginDomainName: opts.cloudfrontOriginDomainName,
|
|
7760
7868
|
evidenceBucket: opts.evidenceBucket
|
|
7761
7869
|
});
|
|
7762
7870
|
print(plan, renderCloudPlan(plan), opts);
|
|
@@ -7764,7 +7872,7 @@ cloud.command("plan").description("Generate a dry-run AWS deployment plan").opti
|
|
|
7764
7872
|
fail(error);
|
|
7765
7873
|
}
|
|
7766
7874
|
});
|
|
7767
|
-
cloud.command("private-probe-config").description("Generate hosted-targeted private probe preflight configuration").option("--api-url <url>", "hosted Open Uptime API URL", "https://uptime.example.com/api/v1").option("--workspace-id <id>", "workspace id", "workspace-id").option("--probe-id <id>", "cloud registered private probe id").option("--private-key-file <path>", "private probe key file", "~/.hasna/uptime/probes/private-probe-01.key.pem").option("--machine-id <id>", "machine id", "private-probe-01").option("--log-level <level>", "probe log level", "info").option("--env", "print shell env file instead of summary text").option("-j, --json", "print JSON").action((opts) => {
|
|
7875
|
+
cloud.command("private-probe-config").description("Generate hosted-targeted private probe preflight configuration").option("--api-url <url>", "hosted Open Uptime API URL", "https://uptime.example.com/api/v1").option("--workspace-id <id>", "workspace id", "workspace-id").option("--probe-id <id>", "cloud registered private probe id").option("--private-key-file <path>", "private probe key file", "~/.hasna/uptime/probes/private-probe-01.key.pem").option("--machine-id <id>", "machine id", "private-probe-01").option("--log-level <level>", "probe log level", "info").option("--env", "print shell env file instead of summary text").option("--allow-blocked-env", "print the blocked preflight env anyway for review artifacts; do not start the probe").option("-j, --json", "print JSON").action((opts) => {
|
|
7768
7876
|
try {
|
|
7769
7877
|
const config = buildPrivateProbeCloudConfig({
|
|
7770
7878
|
apiUrl: opts.apiUrl,
|
|
@@ -7775,7 +7883,7 @@ cloud.command("private-probe-config").description("Generate hosted-targeted priv
|
|
|
7775
7883
|
logLevel: opts.logLevel
|
|
7776
7884
|
});
|
|
7777
7885
|
if (opts.env && !wantsJson(opts)) {
|
|
7778
|
-
console.log(renderPrivateProbeEnv(config));
|
|
7886
|
+
console.log(renderPrivateProbeEnv(config, { allowBlocked: opts.allowBlockedEnv }));
|
|
7779
7887
|
return;
|
|
7780
7888
|
}
|
|
7781
7889
|
print(config, renderPrivateProbeConfig(config), opts);
|
|
@@ -7783,6 +7891,22 @@ cloud.command("private-probe-config").description("Generate hosted-targeted priv
|
|
|
7783
7891
|
fail(error);
|
|
7784
7892
|
}
|
|
7785
7893
|
});
|
|
7894
|
+
var cloudPublicChecks = cloud.command("public-checks").description("Run hosted public HTTP/TCP checks against the configured hosted store");
|
|
7895
|
+
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
|
+
try {
|
|
7897
|
+
const svc = hostedService({
|
|
7898
|
+
hostedSqliteDb: opts.hostedSqliteDb,
|
|
7899
|
+
allowHostedLocalStore: opts.allowHostedLocalStore
|
|
7900
|
+
});
|
|
7901
|
+
const workspaceId = opts.workspaceId || process.env.HASNA_UPTIME_WORKSPACE_ID;
|
|
7902
|
+
const results = await svc.runDueHostedPublicChecks(new Date(opts.now), { workspaceId });
|
|
7903
|
+
svc.close();
|
|
7904
|
+
const data = { ok: true, workspaceId, checked: results.length, results };
|
|
7905
|
+
print(data, results.length ? renderCheckResults(results) : "No due hosted public checks", opts);
|
|
7906
|
+
} catch (error) {
|
|
7907
|
+
fail(error);
|
|
7908
|
+
}
|
|
7909
|
+
});
|
|
7786
7910
|
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) => {
|
|
7787
7911
|
try {
|
|
7788
7912
|
const svc = service();
|
|
@@ -8144,6 +8268,7 @@ function renderCloudPlan(plan) {
|
|
|
8144
8268
|
`host: ${plan.hostname}`,
|
|
8145
8269
|
`cluster: ${plan.resources.ecsCluster}`,
|
|
8146
8270
|
`image: ${plan.image.uri}`,
|
|
8271
|
+
...plan.image.expectedIntegrity ? [`package integrity: ${plan.image.expectedIntegrity}`] : [],
|
|
8147
8272
|
`image builder: ${plan.resources.imageBuilder}`,
|
|
8148
8273
|
`dockerfile: ${plan.image.dockerfile}`,
|
|
8149
8274
|
`infra: ${plan.infra.path}`,
|
|
@@ -8151,6 +8276,7 @@ function renderCloudPlan(plan) {
|
|
|
8151
8276
|
`efs: ${plan.resources.efsFileSystem}`,
|
|
8152
8277
|
`hosted sqlite: ${plan.resources.hostedSqliteDbPath}`,
|
|
8153
8278
|
`protected access: ${plan.resources.protectedAccessMode} ${plan.resources.protectedAccessUrl}`,
|
|
8279
|
+
...plan.resources.cloudfrontOrigin ? [`cloudfront origin: ${plan.resources.cloudfrontOrigin.protocolPolicy} ${plan.resources.cloudfrontOrigin.domainName}`] : [],
|
|
8154
8280
|
`services: ${plan.resources.services.map((service2) => `${service2.name}:${service2.desiredCount}/${service2.targetDesiredCount}`).join(", ")}`,
|
|
8155
8281
|
`evidence bucket: ${plan.resources.evidenceBucket}`,
|
|
8156
8282
|
`blockers: ${plan.blockers.length}`,
|
package/dist/cloud-plan.d.ts
CHANGED
|
@@ -11,7 +11,10 @@ export interface AwsDeploymentPlanOptions {
|
|
|
11
11
|
evidenceBucket?: string;
|
|
12
12
|
hostedSqliteDbPath?: string;
|
|
13
13
|
runtimePackageVersion?: string;
|
|
14
|
+
runtimePackageIntegrity?: string;
|
|
14
15
|
protectedAccessMode?: "cloudfront_default_domain" | "alb_https_cert";
|
|
16
|
+
cloudfrontOriginProtocolPolicy?: "http-only" | "https-only";
|
|
17
|
+
cloudfrontOriginDomainName?: string;
|
|
15
18
|
/** @deprecated Postgres is target-state only until the async adapter is implemented. */
|
|
16
19
|
rdsInstanceId?: string;
|
|
17
20
|
/** @deprecated Postgres is target-state only until the async adapter is implemented. */
|
|
@@ -49,6 +52,13 @@ export interface AwsDeploymentPlan {
|
|
|
49
52
|
protectedAccessMode: "cloudfront_default_domain" | "alb_https_cert";
|
|
50
53
|
edgeDistribution?: string;
|
|
51
54
|
protectedAccessUrl: string;
|
|
55
|
+
cloudfrontOrigin?: {
|
|
56
|
+
protocolPolicy: "http-only" | "https-only";
|
|
57
|
+
domainName: string;
|
|
58
|
+
requiresMatchingCertificate: boolean;
|
|
59
|
+
liveTrafficApproved: boolean;
|
|
60
|
+
risk?: string;
|
|
61
|
+
};
|
|
52
62
|
originVerification: {
|
|
53
63
|
mode: "cloudfront_origin_header" | "alb_tls";
|
|
54
64
|
requiredBeforeScaleUp: boolean;
|
|
@@ -66,6 +76,7 @@ export interface AwsDeploymentPlan {
|
|
|
66
76
|
repository: string;
|
|
67
77
|
uri: string;
|
|
68
78
|
dockerfile: string;
|
|
79
|
+
expectedIntegrity?: string;
|
|
69
80
|
buildCommand: string;
|
|
70
81
|
pushCommands: string[];
|
|
71
82
|
};
|
|
@@ -137,5 +148,7 @@ export interface PrivateProbeCloudConfig {
|
|
|
137
148
|
}
|
|
138
149
|
export declare function buildAwsDeploymentPlan(options?: AwsDeploymentPlanOptions): AwsDeploymentPlan;
|
|
139
150
|
export declare function buildPrivateProbeCloudConfig(options?: PrivateProbeCloudConfigOptions): PrivateProbeCloudConfig;
|
|
140
|
-
export declare function renderPrivateProbeEnv(config: PrivateProbeCloudConfig
|
|
151
|
+
export declare function renderPrivateProbeEnv(config: PrivateProbeCloudConfig, options?: {
|
|
152
|
+
allowBlocked?: boolean;
|
|
153
|
+
}): string;
|
|
141
154
|
//# sourceMappingURL=cloud-plan.d.ts.map
|
package/dist/cloud-plan.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cloud-plan.d.ts","sourceRoot":"","sources":["../src/cloud-plan.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"cloud-plan.d.ts","sourceRoot":"","sources":["../src/cloud-plan.ts"],"names":[],"mappings":"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"}
|
package/dist/cloud-plan.js
CHANGED
|
@@ -9,6 +9,7 @@ var DEFAULT_WORKSPACE_ID = "workspace-id";
|
|
|
9
9
|
var DEFAULT_VPC_ID = "vpc-xxxxxxxx";
|
|
10
10
|
var DEFAULT_HOSTED_SQLITE_DB = "/data/uptime/uptime.db";
|
|
11
11
|
var DEFAULT_PROTECTED_ACCESS_MODE = "cloudfront_default_domain";
|
|
12
|
+
var DEFAULT_CLOUDFRONT_ORIGIN_PROTOCOL_POLICY = "http-only";
|
|
12
13
|
function buildAwsDeploymentPlan(options = {}) {
|
|
13
14
|
const region = clean(options.region, DEFAULT_REGION);
|
|
14
15
|
const stage = clean(options.stage, DEFAULT_STAGE);
|
|
@@ -21,8 +22,11 @@ function buildAwsDeploymentPlan(options = {}) {
|
|
|
21
22
|
const image = clean(options.image, `${imageRepositoryUri}@sha256:<image-digest>`);
|
|
22
23
|
const evidenceBucket = clean(options.evidenceBucket, `hasna-${stage}-${prefix}-evidence`);
|
|
23
24
|
const hostedSqliteDbPath = clean(options.hostedSqliteDbPath, DEFAULT_HOSTED_SQLITE_DB);
|
|
24
|
-
const runtimePackageVersion = clean(options.runtimePackageVersion, "0.1.
|
|
25
|
+
const runtimePackageVersion = clean(options.runtimePackageVersion, "0.1.25");
|
|
26
|
+
const runtimePackageIntegrity = options.runtimePackageIntegrity?.trim() || undefined;
|
|
25
27
|
const protectedAccessMode = options.protectedAccessMode ?? DEFAULT_PROTECTED_ACCESS_MODE;
|
|
28
|
+
const cloudfrontOriginProtocolPolicy = options.cloudfrontOriginProtocolPolicy ?? DEFAULT_CLOUDFRONT_ORIGIN_PROTOCOL_POLICY;
|
|
29
|
+
const cloudfrontOriginDomainName = clean(options.cloudfrontOriginDomainName, "<alb-dns-name>");
|
|
26
30
|
const protectedAccessUrl = protectedAccessMode === "cloudfront_default_domain" ? "https://<cloudfront-domain>" : `https://${hostname}`;
|
|
27
31
|
const cluster = `${prefix}-${stage}`;
|
|
28
32
|
const secrets = {
|
|
@@ -89,6 +93,13 @@ function buildAwsDeploymentPlan(options = {}) {
|
|
|
89
93
|
protectedAccessMode,
|
|
90
94
|
edgeDistribution: protectedAccessMode === "cloudfront_default_domain" ? `${prefix}-${stage}-edge` : undefined,
|
|
91
95
|
protectedAccessUrl,
|
|
96
|
+
cloudfrontOrigin: protectedAccessMode === "cloudfront_default_domain" ? {
|
|
97
|
+
protocolPolicy: cloudfrontOriginProtocolPolicy,
|
|
98
|
+
domainName: cloudfrontOriginProtocolPolicy === "https-only" ? cloudfrontOriginDomainName : "<alb-dns-name>",
|
|
99
|
+
requiresMatchingCertificate: cloudfrontOriginProtocolPolicy === "https-only",
|
|
100
|
+
liveTrafficApproved: false,
|
|
101
|
+
risk: cloudfrontOriginProtocolPolicy === "http-only" ? "Temporary HTTP-origin bridge: do not use for token-bearing live traffic without explicit risk acceptance, 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
|
+
} : undefined,
|
|
92
103
|
originVerification: protectedAccessMode === "cloudfront_default_domain" ? {
|
|
93
104
|
mode: "cloudfront_origin_header",
|
|
94
105
|
requiredBeforeScaleUp: true,
|
|
@@ -121,6 +132,7 @@ function buildAwsDeploymentPlan(options = {}) {
|
|
|
121
132
|
repository: ecrRepository,
|
|
122
133
|
uri: image,
|
|
123
134
|
dockerfile: "Dockerfile.package",
|
|
135
|
+
expectedIntegrity: runtimePackageIntegrity,
|
|
124
136
|
buildCommand: `BLOCKED: after infra approval, AWS CodeBuild builds Dockerfile.package from @hasna/uptime@${runtimePackageVersion} into ${imageRepositoryUri}`,
|
|
125
137
|
pushCommands: [
|
|
126
138
|
`BLOCKED: start ${prefix}-${stage}-image-builder only through the approved deploy pipeline after @hasna/uptime@${runtimePackageVersion} is published`,
|
|
@@ -147,16 +159,16 @@ function buildAwsDeploymentPlan(options = {}) {
|
|
|
147
159
|
`Infra PR must declare CodeBuild image builder ${prefix}-${stage}-image-builder for @hasna/uptime@${runtimePackageVersion}.`,
|
|
148
160
|
`Infra PR must declare hardened S3 evidence bucket ${evidenceBucket} with KMS, versioning, lifecycle, and public access block.`,
|
|
149
161
|
`Infra PR must declare encrypted EFS ${prefix}-${stage}-data with access point, mount targets, and AWS Backup plan.`,
|
|
150
|
-
protectedAccessMode === "cloudfront_default_domain" ? "Infra PR must declare CloudFront default-domain HTTPS edge, ALB
|
|
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 HTTP-origin 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.`,
|
|
151
163
|
"Only apply the infra plan from the approved infrastructure repository after review evidence is attached."
|
|
152
164
|
],
|
|
153
165
|
deploy: [
|
|
154
166
|
"Build and publish the image only after the Dockerfile/container target is reviewed.",
|
|
155
|
-
`Start the AWS image builder for @hasna/uptime@${runtimePackageVersion}
|
|
167
|
+
runtimePackageIntegrity ? `Start the AWS image builder for @hasna/uptime@${runtimePackageVersion}; it must verify npm dist.integrity ${runtimePackageIntegrity} before extracting the package, then record the pushed image digest.` : `Start the AWS image builder for @hasna/uptime@${runtimePackageVersion}; set runtime_package_integrity from npm dist.integrity before live use, then record the pushed image digest.`,
|
|
156
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.",
|
|
157
169
|
`Register task definitions for ${services.map((service) => service.name).join(", ")} using valueFrom secrets.`,
|
|
158
170
|
`Update ECS services in cluster ${cluster} one component at a time through the approved deploy pipeline.`,
|
|
159
|
-
protectedAccessMode === "cloudfront_default_domain" ? "Use the CloudFront default HTTPS domain with origin verification header binding for first protected access;
|
|
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 record explicit HTTP-origin risk acceptance." : `Create Route53/edge record for ${hostname} only after ALB health checks pass and auth denial smokes succeed.`
|
|
160
172
|
],
|
|
161
173
|
rollback: [
|
|
162
174
|
"Keep previous task definition ARNs before each service update.",
|
|
@@ -173,6 +185,8 @@ function buildAwsDeploymentPlan(options = {}) {
|
|
|
173
185
|
blockers: [
|
|
174
186
|
"The infrastructure owner repository was not found in this workspace.",
|
|
175
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 needs https-only origin mode or explicit risk acceptance."] : [],
|
|
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."] : [],
|
|
176
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.",
|
|
177
191
|
"Hosted production auth/RBAC must replace broad static hosted-token operation before exposure.",
|
|
178
192
|
"Public probe execution still needs cloud check-job leases wired to runHostedHttpCheck and live policy-decision log evidence.",
|
|
@@ -181,8 +195,9 @@ function buildAwsDeploymentPlan(options = {}) {
|
|
|
181
195
|
requiredEvidence: [
|
|
182
196
|
"Infrastructure PR/synth/plan from the approved infra repository.",
|
|
183
197
|
"CodeBuild image-builder run, container smoke, and immutable image digest.",
|
|
198
|
+
"Published package dist.integrity pinned in the private infra root or an explicit not-live exception.",
|
|
184
199
|
"ECS task definitions using secrets.valueFrom only.",
|
|
185
|
-
"CloudFront-default-domain origin-header config
|
|
200
|
+
"CloudFront-default-domain origin-header config, origin transport decision, direct-origin denial evidence, auth-denial smokes, and web alarm checks.",
|
|
186
201
|
"Single-writer ECS evidence: one web task maximum and no scheduler/public-probe/reporter EFS mounts.",
|
|
187
202
|
"EFS encryption, access point, mount-target, AWS Backup, and restore-drill evidence.",
|
|
188
203
|
"S3 bucket KMS, versioning, lifecycle, and public-access-block evidence.",
|
|
@@ -195,11 +210,13 @@ function buildAwsDeploymentPlan(options = {}) {
|
|
|
195
210
|
notes: [
|
|
196
211
|
"This plan generator does not call AWS.",
|
|
197
212
|
"Blocked plan output intentionally avoids copy-pastable AWS mutation commands.",
|
|
198
|
-
"Default protected access uses CloudFront's HTTPS default domain so first deploy is not blocked on custom DNS or ACM.",
|
|
213
|
+
"Default protected access uses CloudFront's HTTPS default domain so first zero-count deploy is not blocked on custom DNS or ACM.",
|
|
199
214
|
"CloudFront default-domain mode still requires origin verification header binding before live scale-up; the header value is sensitive state/config material, not public documentation.",
|
|
215
|
+
"CloudFront HTTPS-origin mode requires a dedicated origin DNS hostname and matching ACM certificate; do not assume the ALB DNS name can satisfy TLS verification.",
|
|
200
216
|
"Hosted runtime uses explicit EFS-backed SQLite at HASNA_UPTIME_HOSTED_SQLITE_DB until the async Postgres adapter exists.",
|
|
201
217
|
"Do not set HASNA_UPTIME_DATABASE_URL for hosted tasks until the Postgres adapter is implemented.",
|
|
202
218
|
"Secrets are represented as secret names/refs and must be injected with valueFrom.",
|
|
219
|
+
"Set runtime_package_integrity in the approved infra root after publish so the AWS image builder verifies the npm tarball before ECR build.",
|
|
203
220
|
"Actual deploy belongs in the deploy_release_operate_final goal node after infra review."
|
|
204
221
|
]
|
|
205
222
|
}
|
|
@@ -266,7 +283,10 @@ function buildPrivateProbeCloudConfig(options = {}) {
|
|
|
266
283
|
}
|
|
267
284
|
};
|
|
268
285
|
}
|
|
269
|
-
function renderPrivateProbeEnv(config) {
|
|
286
|
+
function renderPrivateProbeEnv(config, options = {}) {
|
|
287
|
+
if (!options.allowBlocked && (!config.canStart || config.blockers.length > 0)) {
|
|
288
|
+
throw new Error("private probe env output is blocked until hosted probe routes and cloud jobs are implemented");
|
|
289
|
+
}
|
|
270
290
|
const required = ["HASNA_UPTIME_PRIVATE_PROBE_ID"];
|
|
271
291
|
const missing = required.filter((key) => !config.env[key]);
|
|
272
292
|
if (missing.length > 0) {
|