@hasna/uptime 0.1.8 → 0.1.10
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 +25 -3
- package/README.md +4 -4
- package/SECURITY.md +4 -2
- package/dist/api.js +104 -47
- package/dist/checks.d.ts +2 -1
- package/dist/checks.d.ts.map +1 -1
- package/dist/checks.js +2 -1
- package/dist/cli/index.js +125 -68
- package/dist/cloud-plan.d.ts +6 -6
- package/dist/cloud-plan.d.ts.map +1 -1
- package/dist/cloud-plan.js +17 -17
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +121 -64
- package/dist/mcp/index.js +92 -37
- package/dist/service.d.ts +33 -9
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +92 -37
- package/dist/store.d.ts +13 -3
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +78 -25
- package/dist/types.d.ts +3 -0
- package/dist/types.d.ts.map +1 -1
- package/docs/aws-deployment-runbook.md +229 -14
- package/infra/aws/README.md +12 -1
- package/infra/aws/main.tf +44 -5
- package/infra/aws/terraform.tfvars.example +9 -1
- package/infra/aws/variables.tf +48 -1
- package/package.json +1 -1
package/dist/cloud-plan.js
CHANGED
|
@@ -21,7 +21,7 @@ function buildAwsDeploymentPlan(options = {}) {
|
|
|
21
21
|
const image = clean(options.image, `${imageRepositoryUri}@sha256:<image-digest>`);
|
|
22
22
|
const evidenceBucket = clean(options.evidenceBucket, `hasna-${stage}-${prefix}-evidence`);
|
|
23
23
|
const hostedSqliteDbPath = clean(options.hostedSqliteDbPath, DEFAULT_HOSTED_SQLITE_DB);
|
|
24
|
-
const runtimePackageVersion = clean(options.runtimePackageVersion, "0.1.
|
|
24
|
+
const runtimePackageVersion = clean(options.runtimePackageVersion, "0.1.10");
|
|
25
25
|
const protectedAccessMode = options.protectedAccessMode ?? DEFAULT_PROTECTED_ACCESS_MODE;
|
|
26
26
|
const protectedAccessUrl = protectedAccessMode === "cloudfront_default_domain" ? "https://<cloudfront-domain>" : `https://${hostname}`;
|
|
27
27
|
const cluster = `${prefix}-${stage}`;
|
|
@@ -153,9 +153,9 @@ function buildAwsDeploymentPlan(options = {}) {
|
|
|
153
153
|
"Disable scheduler/reporter services before data rollback.",
|
|
154
154
|
"Restore EFS backup recovery point only after explicit operator approval and audit record."
|
|
155
155
|
],
|
|
156
|
-
|
|
156
|
+
privateProbe: [
|
|
157
157
|
"Create a private probe identity with a caller-managed public key.",
|
|
158
|
-
"Install @hasna/uptime on
|
|
158
|
+
"Install @hasna/uptime on the private probe operator machine and write the generated env file with mode 0600.",
|
|
159
159
|
"Run the private probe against the hosted /api/v1 probe endpoint once it exists."
|
|
160
160
|
]
|
|
161
161
|
},
|
|
@@ -164,7 +164,7 @@ function buildAwsDeploymentPlan(options = {}) {
|
|
|
164
164
|
"The EFS SQLite bridge is single-writer only: web target desired count is 1 and scheduler/public-probe/reporter targets remain 0 until Postgres and cloud leases exist.",
|
|
165
165
|
"Hosted production auth/RBAC must replace broad static hosted-token operation before exposure.",
|
|
166
166
|
"Public probe execution still needs DNS, redirect, and rebinding SSRF enforcement plus cloud check-job leases.",
|
|
167
|
-
"
|
|
167
|
+
"Private probe enrollment, claim, submit, heartbeat, revocation, and rotation are not cloud-backed yet."
|
|
168
168
|
],
|
|
169
169
|
requiredEvidence: [
|
|
170
170
|
"Infrastructure PR/synth/plan from the approved infra repository.",
|
|
@@ -174,7 +174,7 @@ function buildAwsDeploymentPlan(options = {}) {
|
|
|
174
174
|
"Single-writer ECS evidence: one web task maximum and no scheduler/public-probe/reporter EFS mounts.",
|
|
175
175
|
"EFS encryption, access point, mount-target, AWS Backup, and restore-drill evidence.",
|
|
176
176
|
"S3 bucket KMS, versioning, lifecycle, and public-access-block evidence.",
|
|
177
|
-
"
|
|
177
|
+
"Private-probe registration, key-file mode, heartbeat, and revocation evidence."
|
|
178
178
|
],
|
|
179
179
|
safety: {
|
|
180
180
|
liveAwsMutation: false,
|
|
@@ -192,16 +192,16 @@ function buildAwsDeploymentPlan(options = {}) {
|
|
|
192
192
|
}
|
|
193
193
|
};
|
|
194
194
|
}
|
|
195
|
-
function
|
|
195
|
+
function buildPrivateProbeCloudConfig(options = {}) {
|
|
196
196
|
const apiUrl = clean(options.apiUrl, `https://${DEFAULT_HOSTNAME}/api/v1`);
|
|
197
197
|
const workspaceId = clean(options.workspaceId, DEFAULT_WORKSPACE_ID);
|
|
198
|
-
const machineId = clean(options.machineId, "
|
|
199
|
-
const privateKeyFile = clean(options.probePrivateKeyFile, "~/.hasna/uptime/probes/
|
|
198
|
+
const machineId = clean(options.machineId, "private-probe-01");
|
|
199
|
+
const privateKeyFile = clean(options.probePrivateKeyFile, "~/.hasna/uptime/probes/private-probe-01.key.pem");
|
|
200
200
|
const probeId = options.probeId?.trim();
|
|
201
201
|
const blockers = [
|
|
202
202
|
...probeId ? [] : ["Cloud-registered private probe id is required before writing a sourceable env file."],
|
|
203
203
|
"Hosted probe claim and submit routes still fail closed until cloud check_jobs and workspace stores are implemented.",
|
|
204
|
-
"
|
|
204
|
+
"Private probe enrollment, heartbeat, revocation, rotation, and bounded offline lease handling are not implemented yet."
|
|
205
205
|
];
|
|
206
206
|
const env = {
|
|
207
207
|
HASNA_UPTIME_MODE: "hosted",
|
|
@@ -215,7 +215,7 @@ function buildSpark01CloudConfig(options = {}) {
|
|
|
215
215
|
if (probeId)
|
|
216
216
|
env.HASNA_UPTIME_PRIVATE_PROBE_ID = probeId;
|
|
217
217
|
return {
|
|
218
|
-
kind: "open-uptime.
|
|
218
|
+
kind: "open-uptime.private-probe-cloud-config",
|
|
219
219
|
version: 1,
|
|
220
220
|
generatedAt: new Date().toISOString(),
|
|
221
221
|
status: "blocked",
|
|
@@ -227,7 +227,7 @@ function buildSpark01CloudConfig(options = {}) {
|
|
|
227
227
|
{
|
|
228
228
|
path: privateKeyFile,
|
|
229
229
|
mode: "0600",
|
|
230
|
-
purpose: "Ed25519 private key generated on
|
|
230
|
+
purpose: "Ed25519 private key generated on the private probe machine; never paste into cloud config."
|
|
231
231
|
},
|
|
232
232
|
{
|
|
233
233
|
path: "~/.hasna/uptime/cloud.env",
|
|
@@ -237,7 +237,7 @@ function buildSpark01CloudConfig(options = {}) {
|
|
|
237
237
|
],
|
|
238
238
|
commands: [
|
|
239
239
|
"bun install -g @hasna/uptime@latest",
|
|
240
|
-
"Generate the
|
|
240
|
+
"Generate the private probe key locally and register only its public key with the hosted control plane once registration exists.",
|
|
241
241
|
"Write ~/.hasna/uptime/cloud.env from this plan, then source it for the private probe service.",
|
|
242
242
|
"Start the private probe worker only after hosted /api/v1 probe claim/submit routes are backed by cloud jobs."
|
|
243
243
|
],
|
|
@@ -246,18 +246,18 @@ function buildSpark01CloudConfig(options = {}) {
|
|
|
246
246
|
privateKeyInline: false,
|
|
247
247
|
tokenInline: false,
|
|
248
248
|
notes: [
|
|
249
|
-
"This config is hosted-targeted preflight:
|
|
249
|
+
"This config is hosted-targeted preflight: the private probe must not start until cloud probe routes are backed by hosted state.",
|
|
250
250
|
"The private key file path is referenced, not embedded.",
|
|
251
251
|
"Hosted token or probe auth material must come from the machine secret store, not this generated config."
|
|
252
252
|
]
|
|
253
253
|
}
|
|
254
254
|
};
|
|
255
255
|
}
|
|
256
|
-
function
|
|
256
|
+
function renderPrivateProbeEnv(config) {
|
|
257
257
|
const required = ["HASNA_UPTIME_PRIVATE_PROBE_ID"];
|
|
258
258
|
const missing = required.filter((key) => !config.env[key]);
|
|
259
259
|
if (missing.length > 0) {
|
|
260
|
-
throw new Error(`
|
|
260
|
+
throw new Error(`private probe env output requires ${missing.join(", ")}`);
|
|
261
261
|
}
|
|
262
262
|
return Object.entries(config.env).map(([key, value]) => `${key}=${shellEscape(value)}`).join(`
|
|
263
263
|
`);
|
|
@@ -290,7 +290,7 @@ function shellEscape(value) {
|
|
|
290
290
|
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
291
291
|
}
|
|
292
292
|
export {
|
|
293
|
-
|
|
294
|
-
|
|
293
|
+
renderPrivateProbeEnv,
|
|
294
|
+
buildPrivateProbeCloudConfig,
|
|
295
295
|
buildAwsDeploymentPlan
|
|
296
296
|
};
|
package/dist/index.d.ts
CHANGED
|
@@ -5,13 +5,13 @@ export { createApiHandler, serveUptime } from "./api.js";
|
|
|
5
5
|
export { applyImport, previewImport, rollbackImport } from "./imports.js";
|
|
6
6
|
export { buildUptimeReport, sendUptimeReport } from "./report.js";
|
|
7
7
|
export { generateProbeKeyPair, probePublicKeyFingerprint, probeResultSigningPayload, signProbeResult, verifyProbeResultSignature } from "./probes.js";
|
|
8
|
-
export { buildAwsDeploymentPlan,
|
|
8
|
+
export { buildAwsDeploymentPlan, buildPrivateProbeCloudConfig, renderPrivateProbeEnv } from "./cloud-plan.js";
|
|
9
9
|
export { uptimeHome, uptimeDbPath, uptimeHostedFallbackDbPath, ensureUptimeHome } from "./paths.js";
|
|
10
10
|
export type { UptimeBackup, UptimeBackupCheck, UptimeRuntimeMode, UptimeStoreOptions, MonitorProvenance, SaveImportBatchInput, StoredImportBatch, UpsertMonitorProvenanceInput, } from "./store.js";
|
|
11
11
|
export type { BrowserPageRunner, BrowserPageRunnerResult, FetchLike, } from "./checks.js";
|
|
12
12
|
export type { ImportAction, ImportApplyItem, ImportApplyResult, ImportCandidate, ImportPreview, ImportPreviewItem, ImportRequest, ImportRollbackItem, ImportRollbackResult, ImportSource, } from "./imports.js";
|
|
13
13
|
export type { BrowserFailedRequest, BrowserPageEvidence, AuditEvent, CheckAttemptResult, CheckEvidence, CheckResult, CheckStatus, CreateMonitorKind, CreateMonitorInput, CreateReportScheduleInput, ImportedMonitorInput, ImportedUpdateMonitorInput, EvidenceArtifact, Incident, IncidentStatus, ListAuditEventsOptions, ListReportRunsOptions, ListResultsOptions, Monitor, MonitorKind, MonitorStatus, MonitorSummary, ProbeCheckJob, ProbeCheckJobStatus, ProbeIdentity, ProbeResultSubmission, ProbeSubmissionReceipt, RecordAuditEventInput, ReportDeliveryChannel, ReportDeliveryRecord, ReportEmailChannelConfig, ReportLogsChannelConfig, ReportRun, ReportRunStatus, ReportSchedule, ReportScheduleChannels, ReportScheduleStatus, ReportSmsChannelConfig, SchedulerHandle, UpdateMonitorInput, UpdateReportScheduleInput, UptimeSummary, } from "./types.js";
|
|
14
14
|
export type { ProbeKeyPair, ProbeSigningInput } from "./probes.js";
|
|
15
|
-
export type { AwsDeploymentPlan, AwsDeploymentPlanOptions, AwsServicePlan,
|
|
15
|
+
export type { AwsDeploymentPlan, AwsDeploymentPlanOptions, AwsServicePlan, PrivateProbeCloudConfig, PrivateProbeCloudConfigOptions, } from "./cloud-plan.js";
|
|
16
16
|
export type { BuildUptimeReportOptions, SendUptimeReportOptions, UptimeEmailReportTarget, UptimeLogsReportTarget, UptimeReport, UptimeReportDelivery, UptimeSmsReportTarget, } from "./report.js";
|
|
17
17
|
//# sourceMappingURL=index.d.ts.map
|
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,EAAE,mBAAmB,EAAE,eAAe,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC9F,OAAO,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AACzD,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAC1E,OAAO,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAClE,OAAO,EAAE,oBAAoB,EAAE,yBAAyB,EAAE,yBAAyB,EAAE,eAAe,EAAE,0BAA0B,EAAE,MAAM,aAAa,CAAC;AACtJ,OAAO,EAAE,sBAAsB,EAAE,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AACjE,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACzC,OAAO,EAAE,mBAAmB,EAAE,eAAe,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC9F,OAAO,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AACzD,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAC1E,OAAO,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAClE,OAAO,EAAE,oBAAoB,EAAE,yBAAyB,EAAE,yBAAyB,EAAE,eAAe,EAAE,0BAA0B,EAAE,MAAM,aAAa,CAAC;AACtJ,OAAO,EAAE,sBAAsB,EAAE,4BAA4B,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AAC9G,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,0BAA0B,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AACpG,YAAY,EACV,YAAY,EACZ,iBAAiB,EACjB,iBAAiB,EACjB,kBAAkB,EAClB,iBAAiB,EACjB,oBAAoB,EACpB,iBAAiB,EACjB,4BAA4B,GAC7B,MAAM,YAAY,CAAC;AACpB,YAAY,EACV,iBAAiB,EACjB,uBAAuB,EACvB,SAAS,GACV,MAAM,aAAa,CAAC;AACrB,YAAY,EACV,YAAY,EACZ,eAAe,EACf,iBAAiB,EACjB,eAAe,EACf,aAAa,EACb,iBAAiB,EACjB,aAAa,EACb,kBAAkB,EAClB,oBAAoB,EACpB,YAAY,GACb,MAAM,cAAc,CAAC;AACtB,YAAY,EACV,oBAAoB,EACpB,mBAAmB,EACnB,UAAU,EACV,kBAAkB,EAClB,aAAa,EACb,WAAW,EACX,WAAW,EACX,iBAAiB,EACjB,kBAAkB,EAClB,yBAAyB,EACzB,oBAAoB,EACpB,0BAA0B,EAC1B,gBAAgB,EAChB,QAAQ,EACR,cAAc,EACd,sBAAsB,EACtB,qBAAqB,EACrB,kBAAkB,EAClB,OAAO,EACP,WAAW,EACX,aAAa,EACb,cAAc,EACd,aAAa,EACb,mBAAmB,EACnB,aAAa,EACb,qBAAqB,EACrB,sBAAsB,EACtB,qBAAqB,EACrB,qBAAqB,EACrB,oBAAoB,EACpB,wBAAwB,EACxB,uBAAuB,EACvB,SAAS,EACT,eAAe,EACf,cAAc,EACd,sBAAsB,EACtB,oBAAoB,EACpB,sBAAsB,EACtB,eAAe,EACf,kBAAkB,EAClB,yBAAyB,EACzB,aAAa,GACd,MAAM,YAAY,CAAC;AACpB,YAAY,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AACnE,YAAY,EACV,iBAAiB,EACjB,wBAAwB,EACxB,cAAc,EACd,uBAAuB,EACvB,8BAA8B,GAC/B,MAAM,iBAAiB,CAAC;AACzB,YAAY,EACV,wBAAwB,EACxB,uBAAuB,EACvB,uBAAuB,EACvB,sBAAsB,EACtB,YAAY,EACZ,oBAAoB,EACpB,qBAAqB,GACtB,MAAM,aAAa,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -844,7 +844,7 @@ var REQUIRED_TABLES = [
|
|
|
844
844
|
];
|
|
845
845
|
var PROBE_TABLES = new Set(["probe_identities", "probe_check_jobs", "probe_submissions"]);
|
|
846
846
|
var REPORT_AUDIT_TABLES = new Set(["report_schedules", "report_runs", "audit_events"]);
|
|
847
|
-
var CURRENT_SCHEMA_VERSION = "
|
|
847
|
+
var CURRENT_SCHEMA_VERSION = "4";
|
|
848
848
|
|
|
849
849
|
class StaleCheckResultError extends Error {
|
|
850
850
|
constructor(message) {
|
|
@@ -905,7 +905,8 @@ class UptimeStore {
|
|
|
905
905
|
this.db.run(`
|
|
906
906
|
CREATE TABLE IF NOT EXISTS monitors (
|
|
907
907
|
id TEXT PRIMARY KEY,
|
|
908
|
-
|
|
908
|
+
workspace_id TEXT NOT NULL DEFAULT 'local',
|
|
909
|
+
name TEXT NOT NULL,
|
|
909
910
|
kind TEXT NOT NULL CHECK (kind IN ('http', 'tcp', 'browser_page')),
|
|
910
911
|
url TEXT,
|
|
911
912
|
host TEXT,
|
|
@@ -920,9 +921,11 @@ class UptimeStore {
|
|
|
920
921
|
last_checked_at TEXT,
|
|
921
922
|
revision INTEGER NOT NULL DEFAULT 1,
|
|
922
923
|
created_at TEXT NOT NULL,
|
|
923
|
-
updated_at TEXT NOT NULL
|
|
924
|
+
updated_at TEXT NOT NULL,
|
|
925
|
+
UNIQUE (workspace_id, name)
|
|
924
926
|
)
|
|
925
927
|
`);
|
|
928
|
+
this.ensureColumn("monitors", "workspace_id", "TEXT NOT NULL DEFAULT 'local'");
|
|
926
929
|
this.ensureColumn("monitors", "revision", "INTEGER NOT NULL DEFAULT 1");
|
|
927
930
|
this.ensureMonitorKindAllowsBrowserPage();
|
|
928
931
|
this.db.run(`
|
|
@@ -1072,6 +1075,7 @@ class UptimeStore {
|
|
|
1072
1075
|
`);
|
|
1073
1076
|
this.db.query("INSERT OR REPLACE INTO schema_migrations (key, value, updated_at) VALUES ('schema_version', ?, ?)").run(CURRENT_SCHEMA_VERSION, new Date().toISOString());
|
|
1074
1077
|
this.db.run("CREATE INDEX IF NOT EXISTS idx_results_monitor_time ON check_results(monitor_id, checked_at DESC)");
|
|
1078
|
+
this.db.run("CREATE INDEX IF NOT EXISTS idx_monitors_workspace_enabled_name ON monitors(workspace_id, enabled, name)");
|
|
1075
1079
|
this.db.run("CREATE INDEX IF NOT EXISTS idx_incidents_monitor_status ON incidents(monitor_id, status)");
|
|
1076
1080
|
this.db.run("CREATE INDEX IF NOT EXISTS idx_check_leases_until ON check_leases(leased_until)");
|
|
1077
1081
|
this.db.run("CREATE INDEX IF NOT EXISTS idx_monitor_provenance_monitor ON monitor_provenance(monitor_id)");
|
|
@@ -1133,8 +1137,10 @@ class UptimeStore {
|
|
|
1133
1137
|
if (this.mode === "hosted")
|
|
1134
1138
|
assertHostedTargetAllowed(normalized);
|
|
1135
1139
|
const now = new Date().toISOString();
|
|
1140
|
+
const workspaceId = normalizeWorkspaceId(options.workspaceId ?? input.workspaceId ?? "local");
|
|
1136
1141
|
const monitor = {
|
|
1137
1142
|
id: newId("mon"),
|
|
1143
|
+
workspaceId,
|
|
1138
1144
|
name: normalized.name,
|
|
1139
1145
|
kind: normalized.kind,
|
|
1140
1146
|
url: normalized.url ?? null,
|
|
@@ -1153,22 +1159,33 @@ class UptimeStore {
|
|
|
1153
1159
|
updatedAt: now
|
|
1154
1160
|
};
|
|
1155
1161
|
this.db.query(`INSERT INTO monitors (
|
|
1156
|
-
id, name, kind, url, host, port, method, expected_status,
|
|
1162
|
+
id, workspace_id, name, kind, url, host, port, method, expected_status,
|
|
1157
1163
|
interval_seconds, timeout_ms, retry_count, enabled, status,
|
|
1158
1164
|
last_checked_at, revision, created_at, updated_at
|
|
1159
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(monitor.id, monitor.name, monitor.kind, monitor.url, monitor.host, monitor.port, monitor.method, monitor.expectedStatus, monitor.intervalSeconds, monitor.timeoutMs, monitor.retryCount, monitor.enabled ? 1 : 0, monitor.status, monitor.lastCheckedAt, monitor.revision, monitor.createdAt, monitor.updatedAt);
|
|
1165
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(monitor.id, monitor.workspaceId, monitor.name, monitor.kind, monitor.url, monitor.host, monitor.port, monitor.method, monitor.expectedStatus, monitor.intervalSeconds, monitor.timeoutMs, monitor.retryCount, monitor.enabled ? 1 : 0, monitor.status, monitor.lastCheckedAt, monitor.revision, monitor.createdAt, monitor.updatedAt);
|
|
1160
1166
|
return monitor;
|
|
1161
1167
|
}
|
|
1162
1168
|
listMonitors(options = {}) {
|
|
1163
|
-
const
|
|
1169
|
+
const workspaceId = options.workspaceId ? normalizeWorkspaceId(options.workspaceId) : undefined;
|
|
1170
|
+
const clauses = [];
|
|
1171
|
+
const args = [];
|
|
1172
|
+
if (workspaceId) {
|
|
1173
|
+
clauses.push("workspace_id = ?");
|
|
1174
|
+
args.push(workspaceId);
|
|
1175
|
+
}
|
|
1176
|
+
if (!options.includeDisabled)
|
|
1177
|
+
clauses.push("enabled = 1");
|
|
1178
|
+
const where = clauses.length ? `WHERE ${clauses.join(" AND ")}` : "";
|
|
1179
|
+
const rows = this.db.query(`SELECT * FROM monitors ${where} ORDER BY name ASC`).all(...args);
|
|
1164
1180
|
return rows.map(monitorFromRow);
|
|
1165
1181
|
}
|
|
1166
|
-
getMonitor(idOrName) {
|
|
1167
|
-
const
|
|
1182
|
+
getMonitor(idOrName, options = {}) {
|
|
1183
|
+
const workspaceId = options.workspaceId ? normalizeWorkspaceId(options.workspaceId) : undefined;
|
|
1184
|
+
const row = this.db.query(`SELECT * FROM monitors WHERE (id = ? OR name = ?)${workspaceId ? " AND workspace_id = ?" : ""}`).get(...workspaceId ? [idOrName, idOrName, workspaceId] : [idOrName, idOrName]);
|
|
1168
1185
|
return row ? monitorFromRow(row) : null;
|
|
1169
1186
|
}
|
|
1170
1187
|
updateMonitor(idOrName, input, options = {}) {
|
|
1171
|
-
const current = this.getMonitor(idOrName);
|
|
1188
|
+
const current = this.getMonitor(idOrName, { workspaceId: options.workspaceId });
|
|
1172
1189
|
if (!current)
|
|
1173
1190
|
throw new Error(`Monitor not found: ${idOrName}`);
|
|
1174
1191
|
if (this.mode === "hosted") {
|
|
@@ -1192,10 +1209,10 @@ class UptimeStore {
|
|
|
1192
1209
|
if (definitionChanged(current, next)) {
|
|
1193
1210
|
this.closeOpenIncident(current.id, updatedAt);
|
|
1194
1211
|
}
|
|
1195
|
-
return this.getMonitor(current.id);
|
|
1212
|
+
return this.getMonitor(current.id, { workspaceId: options.workspaceId });
|
|
1196
1213
|
}
|
|
1197
|
-
deleteMonitor(idOrName) {
|
|
1198
|
-
const current = this.getMonitor(idOrName);
|
|
1214
|
+
deleteMonitor(idOrName, options = {}) {
|
|
1215
|
+
const current = this.getMonitor(idOrName, { workspaceId: options.workspaceId });
|
|
1199
1216
|
if (!current)
|
|
1200
1217
|
return false;
|
|
1201
1218
|
this.db.query("DELETE FROM monitors WHERE id = ?").run(current.id);
|
|
@@ -1572,7 +1589,20 @@ class UptimeStore {
|
|
|
1572
1589
|
}
|
|
1573
1590
|
listResults(options = {}) {
|
|
1574
1591
|
const limit = clampLimit(options.limit ?? 50);
|
|
1575
|
-
const
|
|
1592
|
+
const workspaceId = options.workspaceId ? normalizeWorkspaceId(options.workspaceId) : undefined;
|
|
1593
|
+
const clauses = [];
|
|
1594
|
+
const args = [];
|
|
1595
|
+
if (options.monitorId) {
|
|
1596
|
+
clauses.push("check_results.monitor_id = ?");
|
|
1597
|
+
args.push(options.monitorId);
|
|
1598
|
+
}
|
|
1599
|
+
if (workspaceId) {
|
|
1600
|
+
clauses.push("monitors.workspace_id = ?");
|
|
1601
|
+
args.push(workspaceId);
|
|
1602
|
+
}
|
|
1603
|
+
const where = clauses.length ? `WHERE ${clauses.join(" AND ")}` : "";
|
|
1604
|
+
args.push(limit);
|
|
1605
|
+
const rows = this.db.query(`SELECT check_results.* FROM check_results JOIN monitors ON monitors.id = check_results.monitor_id ${where} ORDER BY checked_at DESC LIMIT ?`).all(...args);
|
|
1576
1606
|
return rows.map(checkResultFromRow);
|
|
1577
1607
|
}
|
|
1578
1608
|
getProvenance(source, sourceId) {
|
|
@@ -1615,24 +1645,28 @@ class UptimeStore {
|
|
|
1615
1645
|
const clauses = [];
|
|
1616
1646
|
const args = [];
|
|
1617
1647
|
if (options.status) {
|
|
1618
|
-
clauses.push("status = ?");
|
|
1648
|
+
clauses.push("incidents.status = ?");
|
|
1619
1649
|
args.push(options.status);
|
|
1620
1650
|
}
|
|
1621
1651
|
if (options.monitorId) {
|
|
1622
|
-
clauses.push("monitor_id = ?");
|
|
1652
|
+
clauses.push("incidents.monitor_id = ?");
|
|
1623
1653
|
args.push(options.monitorId);
|
|
1624
1654
|
}
|
|
1655
|
+
if (options.workspaceId) {
|
|
1656
|
+
clauses.push("monitors.workspace_id = ?");
|
|
1657
|
+
args.push(normalizeWorkspaceId(options.workspaceId));
|
|
1658
|
+
}
|
|
1625
1659
|
const where = clauses.length ? `WHERE ${clauses.join(" AND ")}` : "";
|
|
1626
1660
|
args.push(clampLimit(options.limit ?? 50));
|
|
1627
|
-
const rows = this.db.query(`SELECT
|
|
1661
|
+
const rows = this.db.query(`SELECT incidents.* FROM incidents JOIN monitors ON monitors.id = incidents.monitor_id ${where} ORDER BY opened_at DESC LIMIT ?`).all(...args);
|
|
1628
1662
|
return rows.map(incidentFromRow);
|
|
1629
1663
|
}
|
|
1630
1664
|
getOpenIncident(monitorId) {
|
|
1631
1665
|
const row = this.db.query("SELECT * FROM incidents WHERE monitor_id = ? AND status = 'open' ORDER BY opened_at DESC LIMIT 1").get(monitorId);
|
|
1632
1666
|
return row ? incidentFromRow(row) : null;
|
|
1633
1667
|
}
|
|
1634
|
-
summary() {
|
|
1635
|
-
const monitors = this.listMonitors({ includeDisabled: true });
|
|
1668
|
+
summary(options = {}) {
|
|
1669
|
+
const monitors = this.listMonitors({ includeDisabled: true, workspaceId: options.workspaceId });
|
|
1636
1670
|
const summaries = monitors.map((monitor) => this.monitorSummary(monitor));
|
|
1637
1671
|
return {
|
|
1638
1672
|
generatedAt: new Date().toISOString(),
|
|
@@ -1644,11 +1678,15 @@ class UptimeStore {
|
|
|
1644
1678
|
down: monitors.filter((m) => m.status === "down").length,
|
|
1645
1679
|
paused: monitors.filter((m) => !m.enabled || m.status === "paused").length,
|
|
1646
1680
|
unknown: monitors.filter((m) => m.status === "unknown").length,
|
|
1647
|
-
openIncidents: this.countOpenIncidents()
|
|
1681
|
+
openIncidents: this.countOpenIncidents(options.workspaceId)
|
|
1648
1682
|
}
|
|
1649
1683
|
};
|
|
1650
1684
|
}
|
|
1651
|
-
countOpenIncidents() {
|
|
1685
|
+
countOpenIncidents(workspaceId) {
|
|
1686
|
+
if (workspaceId) {
|
|
1687
|
+
const row2 = this.db.query("SELECT COUNT(*) AS count FROM incidents JOIN monitors ON monitors.id = incidents.monitor_id WHERE incidents.status = 'open' AND monitors.workspace_id = ?").get(normalizeWorkspaceId(workspaceId));
|
|
1688
|
+
return Number(row2?.count ?? 0);
|
|
1689
|
+
}
|
|
1652
1690
|
const row = this.db.query("SELECT COUNT(*) AS count FROM incidents WHERE status = 'open'").get();
|
|
1653
1691
|
return Number(row?.count ?? 0);
|
|
1654
1692
|
}
|
|
@@ -1712,7 +1750,9 @@ class UptimeStore {
|
|
|
1712
1750
|
}
|
|
1713
1751
|
ensureMonitorKindAllowsBrowserPage() {
|
|
1714
1752
|
const row = this.db.query("SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'monitors'").get();
|
|
1715
|
-
|
|
1753
|
+
const needsBrowserPage = !row?.sql?.includes("browser_page");
|
|
1754
|
+
const needsWorkspaceUnique = Boolean(row?.sql?.includes("name TEXT NOT NULL UNIQUE"));
|
|
1755
|
+
if (!row?.sql || !needsBrowserPage && !needsWorkspaceUnique)
|
|
1716
1756
|
return;
|
|
1717
1757
|
this.db.run("PRAGMA foreign_keys = OFF");
|
|
1718
1758
|
this.db.run("PRAGMA legacy_alter_table = ON");
|
|
@@ -1722,7 +1762,8 @@ class UptimeStore {
|
|
|
1722
1762
|
this.db.run(`
|
|
1723
1763
|
CREATE TABLE monitors (
|
|
1724
1764
|
id TEXT PRIMARY KEY,
|
|
1725
|
-
|
|
1765
|
+
workspace_id TEXT NOT NULL DEFAULT 'local',
|
|
1766
|
+
name TEXT NOT NULL,
|
|
1726
1767
|
kind TEXT NOT NULL CHECK (kind IN ('http', 'tcp', 'browser_page')),
|
|
1727
1768
|
url TEXT,
|
|
1728
1769
|
host TEXT,
|
|
@@ -1737,17 +1778,18 @@ class UptimeStore {
|
|
|
1737
1778
|
last_checked_at TEXT,
|
|
1738
1779
|
revision INTEGER NOT NULL DEFAULT 1,
|
|
1739
1780
|
created_at TEXT NOT NULL,
|
|
1740
|
-
updated_at TEXT NOT NULL
|
|
1781
|
+
updated_at TEXT NOT NULL,
|
|
1782
|
+
UNIQUE (workspace_id, name)
|
|
1741
1783
|
)
|
|
1742
1784
|
`);
|
|
1743
1785
|
this.db.run(`
|
|
1744
1786
|
INSERT INTO monitors (
|
|
1745
|
-
id, name, kind, url, host, port, method, expected_status,
|
|
1787
|
+
id, workspace_id, name, kind, url, host, port, method, expected_status,
|
|
1746
1788
|
interval_seconds, timeout_ms, retry_count, enabled, status,
|
|
1747
1789
|
last_checked_at, revision, created_at, updated_at
|
|
1748
1790
|
)
|
|
1749
1791
|
SELECT
|
|
1750
|
-
id, name, kind, url, host, port, method, expected_status,
|
|
1792
|
+
id, workspace_id, name, kind, url, host, port, method, expected_status,
|
|
1751
1793
|
interval_seconds, timeout_ms, retry_count, enabled, status,
|
|
1752
1794
|
last_checked_at, revision, created_at, updated_at
|
|
1753
1795
|
FROM monitors_old_kind
|
|
@@ -1947,6 +1989,16 @@ function rejectControlCharacters2(value, label) {
|
|
|
1947
1989
|
throw new Error(`${label} must not contain control characters`);
|
|
1948
1990
|
}
|
|
1949
1991
|
}
|
|
1992
|
+
function normalizeWorkspaceId(value) {
|
|
1993
|
+
const normalized = value.trim();
|
|
1994
|
+
if (!normalized)
|
|
1995
|
+
throw new Error("Workspace id is required");
|
|
1996
|
+
rejectControlCharacters2(normalized, "Workspace id");
|
|
1997
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9_.:-]{0,127}$/.test(normalized)) {
|
|
1998
|
+
throw new Error("Workspace id contains unsupported characters");
|
|
1999
|
+
}
|
|
2000
|
+
return normalized;
|
|
2001
|
+
}
|
|
1950
2002
|
function normalizeScheduleSlot(value) {
|
|
1951
2003
|
const slot = value.trim();
|
|
1952
2004
|
if (!slot)
|
|
@@ -2133,6 +2185,7 @@ function assertIsoTimestamp(value, label) {
|
|
|
2133
2185
|
function monitorFromRow(row) {
|
|
2134
2186
|
return {
|
|
2135
2187
|
id: row.id,
|
|
2188
|
+
workspaceId: row.workspace_id ?? "local",
|
|
2136
2189
|
name: row.name,
|
|
2137
2190
|
kind: row.kind,
|
|
2138
2191
|
url: row.url,
|
|
@@ -2614,20 +2667,20 @@ class UptimeService {
|
|
|
2614
2667
|
close() {
|
|
2615
2668
|
this.store.close();
|
|
2616
2669
|
}
|
|
2617
|
-
createMonitor(input) {
|
|
2618
|
-
return this.store.createMonitor(input);
|
|
2670
|
+
createMonitor(input, options = {}) {
|
|
2671
|
+
return this.store.createMonitor(input, options);
|
|
2619
2672
|
}
|
|
2620
|
-
updateMonitor(idOrName, input) {
|
|
2621
|
-
return this.store.updateMonitor(idOrName, input);
|
|
2673
|
+
updateMonitor(idOrName, input, options = {}) {
|
|
2674
|
+
return this.store.updateMonitor(idOrName, input, options);
|
|
2622
2675
|
}
|
|
2623
|
-
deleteMonitor(idOrName) {
|
|
2624
|
-
return this.store.deleteMonitor(idOrName);
|
|
2676
|
+
deleteMonitor(idOrName, options = {}) {
|
|
2677
|
+
return this.store.deleteMonitor(idOrName, options);
|
|
2625
2678
|
}
|
|
2626
2679
|
listMonitors(options = {}) {
|
|
2627
2680
|
return this.store.listMonitors(options);
|
|
2628
2681
|
}
|
|
2629
|
-
getMonitor(idOrName) {
|
|
2630
|
-
return this.store.getMonitor(idOrName);
|
|
2682
|
+
getMonitor(idOrName, options = {}) {
|
|
2683
|
+
return this.store.getMonitor(idOrName, options);
|
|
2631
2684
|
}
|
|
2632
2685
|
listResults(options = {}) {
|
|
2633
2686
|
return this.store.listResults(options);
|
|
@@ -2635,8 +2688,8 @@ class UptimeService {
|
|
|
2635
2688
|
listIncidents(options = {}) {
|
|
2636
2689
|
return this.store.listIncidents(options);
|
|
2637
2690
|
}
|
|
2638
|
-
summary() {
|
|
2639
|
-
return this.store.summary();
|
|
2691
|
+
summary(options = {}) {
|
|
2692
|
+
return this.store.summary(options);
|
|
2640
2693
|
}
|
|
2641
2694
|
createProbe(input) {
|
|
2642
2695
|
const store = this.probeStore();
|
|
@@ -2692,7 +2745,8 @@ class UptimeService {
|
|
|
2692
2745
|
return this.store.verifyBackup(backupPath);
|
|
2693
2746
|
}
|
|
2694
2747
|
buildReport(options = {}) {
|
|
2695
|
-
|
|
2748
|
+
const { workspaceId, ...reportOptions } = options;
|
|
2749
|
+
return buildUptimeReport(this.summary({ workspaceId }), reportOptions);
|
|
2696
2750
|
}
|
|
2697
2751
|
async sendReport(options = {}) {
|
|
2698
2752
|
if (this.store.mode === "hosted" && (options.email || options.sms || options.logs)) {
|
|
@@ -3016,6 +3070,7 @@ class UptimeService {
|
|
|
3016
3070
|
throw new Error("Probe job fencing token is invalid");
|
|
3017
3071
|
if (!job.leaseExpiresAt || job.leaseExpiresAt <= new Date().toISOString())
|
|
3018
3072
|
throw new Error("Probe job lease expired");
|
|
3073
|
+
const evidence = input.evidence ? normalizeBrowserEvidence(monitor.url ?? monitor.host ?? "https://example.invalid", input.evidence) : null;
|
|
3019
3074
|
const result = this.store.recordCheckResult({
|
|
3020
3075
|
monitorId: monitor.id,
|
|
3021
3076
|
checkedAt: input.checkedAt,
|
|
@@ -3023,7 +3078,7 @@ class UptimeService {
|
|
|
3023
3078
|
latencyMs: input.latencyMs,
|
|
3024
3079
|
statusCode: input.statusCode ?? null,
|
|
3025
3080
|
error: input.error ?? null,
|
|
3026
|
-
evidence
|
|
3081
|
+
evidence,
|
|
3027
3082
|
attemptCount: input.attemptCount ?? 1,
|
|
3028
3083
|
expectedMonitorRevision: input.monitorRevision
|
|
3029
3084
|
});
|
|
@@ -3568,11 +3623,11 @@ async function handleHostedRequest(service, request, url, options) {
|
|
|
3568
3623
|
}
|
|
3569
3624
|
const apiPath = `/api${url.pathname.slice("/api/v1".length)}`;
|
|
3570
3625
|
const scope = hostedScopeFor(request.method, apiPath);
|
|
3571
|
-
requireHostedActor(request, url, options, scope);
|
|
3626
|
+
const actor = requireHostedActor(request, url, options, scope);
|
|
3572
3627
|
if (["POST", "PATCH", "DELETE"].includes(request.method)) {
|
|
3573
3628
|
validateHostedMutationOrigin(request, url, options);
|
|
3574
3629
|
}
|
|
3575
|
-
return handleApiRoute(service, request, url, apiPath, options, true);
|
|
3630
|
+
return handleApiRoute(service, request, url, apiPath, options, true, actor);
|
|
3576
3631
|
}
|
|
3577
3632
|
function validateHostedMutationOrigin(request, url, options) {
|
|
3578
3633
|
const rawOrigin = request.headers.get("origin");
|
|
@@ -3587,12 +3642,12 @@ function validateHostedMutationOrigin(request, url, options) {
|
|
|
3587
3642
|
throw new ApiError("cross-origin mutation rejected", 403);
|
|
3588
3643
|
}
|
|
3589
3644
|
}
|
|
3590
|
-
async function handleApiRoute(service, request, url, apiPath, options, hosted) {
|
|
3645
|
+
async function handleApiRoute(service, request, url, apiPath, options, hosted, actor) {
|
|
3591
3646
|
if (request.method === "GET" && apiPath === "/api/summary") {
|
|
3592
|
-
return json(service.summary());
|
|
3647
|
+
return json(service.summary({ workspaceId: actor?.workspaceId }));
|
|
3593
3648
|
}
|
|
3594
3649
|
if (request.method === "GET" && apiPath === "/api/report") {
|
|
3595
|
-
return json(service.buildReport());
|
|
3650
|
+
return json(service.buildReport({ workspaceId: actor?.workspaceId }));
|
|
3596
3651
|
}
|
|
3597
3652
|
if (request.method === "POST" && apiPath === "/api/report") {
|
|
3598
3653
|
if (hosted)
|
|
@@ -3649,22 +3704,24 @@ async function handleApiRoute(service, request, url, apiPath, options, hosted) {
|
|
|
3649
3704
|
}));
|
|
3650
3705
|
}
|
|
3651
3706
|
if (request.method === "GET" && apiPath === "/api/monitors") {
|
|
3652
|
-
return json(service.listMonitors({ includeDisabled: url.searchParams.get("includeDisabled") === "true" }));
|
|
3707
|
+
return json(service.listMonitors({ includeDisabled: url.searchParams.get("includeDisabled") === "true", workspaceId: actor?.workspaceId }));
|
|
3653
3708
|
}
|
|
3654
3709
|
if (request.method === "POST" && apiPath === "/api/monitors") {
|
|
3655
|
-
return json(service.createMonitor(await jsonBody(request)), 201);
|
|
3710
|
+
return json(service.createMonitor(await jsonBody(request), { workspaceId: actor?.workspaceId }), 201);
|
|
3656
3711
|
}
|
|
3657
3712
|
if (request.method === "GET" && apiPath === "/api/incidents") {
|
|
3658
3713
|
const status = url.searchParams.get("status");
|
|
3659
3714
|
return json(service.listIncidents({
|
|
3660
3715
|
status: status === "open" || status === "closed" ? status : undefined,
|
|
3661
3716
|
monitorId: url.searchParams.get("monitorId") ?? undefined,
|
|
3717
|
+
workspaceId: actor?.workspaceId,
|
|
3662
3718
|
limit: numericParam(url, "limit", 50)
|
|
3663
3719
|
}));
|
|
3664
3720
|
}
|
|
3665
3721
|
if (request.method === "GET" && apiPath === "/api/results") {
|
|
3666
3722
|
return json(service.listResults({
|
|
3667
3723
|
monitorId: url.searchParams.get("monitorId") ?? undefined,
|
|
3724
|
+
workspaceId: actor?.workspaceId,
|
|
3668
3725
|
limit: numericParam(url, "limit", 50)
|
|
3669
3726
|
}));
|
|
3670
3727
|
}
|
|
@@ -3723,14 +3780,14 @@ async function handleApiRoute(service, request, url, apiPath, options, hosted) {
|
|
|
3723
3780
|
if (monitorMatch) {
|
|
3724
3781
|
const id = decodeURIComponent(monitorMatch[1]);
|
|
3725
3782
|
if (request.method === "GET" && !monitorMatch[2]) {
|
|
3726
|
-
const monitor = service.getMonitor(id);
|
|
3783
|
+
const monitor = service.getMonitor(id, { workspaceId: actor?.workspaceId });
|
|
3727
3784
|
return monitor ? json(monitor) : json({ error: "not found" }, 404);
|
|
3728
3785
|
}
|
|
3729
3786
|
if (request.method === "PATCH" && !monitorMatch[2]) {
|
|
3730
|
-
return json(service.updateMonitor(id, await jsonBody(request)));
|
|
3787
|
+
return json(service.updateMonitor(id, await jsonBody(request), { workspaceId: actor?.workspaceId }));
|
|
3731
3788
|
}
|
|
3732
3789
|
if (request.method === "DELETE" && !monitorMatch[2]) {
|
|
3733
|
-
return json({ deleted: service.deleteMonitor(id) });
|
|
3790
|
+
return json({ deleted: service.deleteMonitor(id, { workspaceId: actor?.workspaceId }) });
|
|
3734
3791
|
}
|
|
3735
3792
|
if (request.method === "POST" && monitorMatch[2] === "check") {
|
|
3736
3793
|
if (hosted)
|
|
@@ -3881,7 +3938,7 @@ function buildAwsDeploymentPlan(options = {}) {
|
|
|
3881
3938
|
const image = clean(options.image, `${imageRepositoryUri}@sha256:<image-digest>`);
|
|
3882
3939
|
const evidenceBucket = clean(options.evidenceBucket, `hasna-${stage}-${prefix}-evidence`);
|
|
3883
3940
|
const hostedSqliteDbPath = clean(options.hostedSqliteDbPath, DEFAULT_HOSTED_SQLITE_DB);
|
|
3884
|
-
const runtimePackageVersion = clean(options.runtimePackageVersion, "0.1.
|
|
3941
|
+
const runtimePackageVersion = clean(options.runtimePackageVersion, "0.1.10");
|
|
3885
3942
|
const protectedAccessMode = options.protectedAccessMode ?? DEFAULT_PROTECTED_ACCESS_MODE;
|
|
3886
3943
|
const protectedAccessUrl = protectedAccessMode === "cloudfront_default_domain" ? "https://<cloudfront-domain>" : `https://${hostname}`;
|
|
3887
3944
|
const cluster = `${prefix}-${stage}`;
|
|
@@ -4013,9 +4070,9 @@ function buildAwsDeploymentPlan(options = {}) {
|
|
|
4013
4070
|
"Disable scheduler/reporter services before data rollback.",
|
|
4014
4071
|
"Restore EFS backup recovery point only after explicit operator approval and audit record."
|
|
4015
4072
|
],
|
|
4016
|
-
|
|
4073
|
+
privateProbe: [
|
|
4017
4074
|
"Create a private probe identity with a caller-managed public key.",
|
|
4018
|
-
"Install @hasna/uptime on
|
|
4075
|
+
"Install @hasna/uptime on the private probe operator machine and write the generated env file with mode 0600.",
|
|
4019
4076
|
"Run the private probe against the hosted /api/v1 probe endpoint once it exists."
|
|
4020
4077
|
]
|
|
4021
4078
|
},
|
|
@@ -4024,7 +4081,7 @@ function buildAwsDeploymentPlan(options = {}) {
|
|
|
4024
4081
|
"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.",
|
|
4025
4082
|
"Hosted production auth/RBAC must replace broad static hosted-token operation before exposure.",
|
|
4026
4083
|
"Public probe execution still needs DNS, redirect, and rebinding SSRF enforcement plus cloud check-job leases.",
|
|
4027
|
-
"
|
|
4084
|
+
"Private probe enrollment, claim, submit, heartbeat, revocation, and rotation are not cloud-backed yet."
|
|
4028
4085
|
],
|
|
4029
4086
|
requiredEvidence: [
|
|
4030
4087
|
"Infrastructure PR/synth/plan from the approved infra repository.",
|
|
@@ -4034,7 +4091,7 @@ function buildAwsDeploymentPlan(options = {}) {
|
|
|
4034
4091
|
"Single-writer ECS evidence: one web task maximum and no scheduler/public-probe/reporter EFS mounts.",
|
|
4035
4092
|
"EFS encryption, access point, mount-target, AWS Backup, and restore-drill evidence.",
|
|
4036
4093
|
"S3 bucket KMS, versioning, lifecycle, and public-access-block evidence.",
|
|
4037
|
-
"
|
|
4094
|
+
"Private-probe registration, key-file mode, heartbeat, and revocation evidence."
|
|
4038
4095
|
],
|
|
4039
4096
|
safety: {
|
|
4040
4097
|
liveAwsMutation: false,
|
|
@@ -4052,16 +4109,16 @@ function buildAwsDeploymentPlan(options = {}) {
|
|
|
4052
4109
|
}
|
|
4053
4110
|
};
|
|
4054
4111
|
}
|
|
4055
|
-
function
|
|
4112
|
+
function buildPrivateProbeCloudConfig(options = {}) {
|
|
4056
4113
|
const apiUrl = clean(options.apiUrl, `https://${DEFAULT_HOSTNAME}/api/v1`);
|
|
4057
4114
|
const workspaceId = clean(options.workspaceId, DEFAULT_WORKSPACE_ID);
|
|
4058
|
-
const machineId = clean(options.machineId, "
|
|
4059
|
-
const privateKeyFile = clean(options.probePrivateKeyFile, "~/.hasna/uptime/probes/
|
|
4115
|
+
const machineId = clean(options.machineId, "private-probe-01");
|
|
4116
|
+
const privateKeyFile = clean(options.probePrivateKeyFile, "~/.hasna/uptime/probes/private-probe-01.key.pem");
|
|
4060
4117
|
const probeId = options.probeId?.trim();
|
|
4061
4118
|
const blockers = [
|
|
4062
4119
|
...probeId ? [] : ["Cloud-registered private probe id is required before writing a sourceable env file."],
|
|
4063
4120
|
"Hosted probe claim and submit routes still fail closed until cloud check_jobs and workspace stores are implemented.",
|
|
4064
|
-
"
|
|
4121
|
+
"Private probe enrollment, heartbeat, revocation, rotation, and bounded offline lease handling are not implemented yet."
|
|
4065
4122
|
];
|
|
4066
4123
|
const env2 = {
|
|
4067
4124
|
HASNA_UPTIME_MODE: "hosted",
|
|
@@ -4075,7 +4132,7 @@ function buildSpark01CloudConfig(options = {}) {
|
|
|
4075
4132
|
if (probeId)
|
|
4076
4133
|
env2.HASNA_UPTIME_PRIVATE_PROBE_ID = probeId;
|
|
4077
4134
|
return {
|
|
4078
|
-
kind: "open-uptime.
|
|
4135
|
+
kind: "open-uptime.private-probe-cloud-config",
|
|
4079
4136
|
version: 1,
|
|
4080
4137
|
generatedAt: new Date().toISOString(),
|
|
4081
4138
|
status: "blocked",
|
|
@@ -4087,7 +4144,7 @@ function buildSpark01CloudConfig(options = {}) {
|
|
|
4087
4144
|
{
|
|
4088
4145
|
path: privateKeyFile,
|
|
4089
4146
|
mode: "0600",
|
|
4090
|
-
purpose: "Ed25519 private key generated on
|
|
4147
|
+
purpose: "Ed25519 private key generated on the private probe machine; never paste into cloud config."
|
|
4091
4148
|
},
|
|
4092
4149
|
{
|
|
4093
4150
|
path: "~/.hasna/uptime/cloud.env",
|
|
@@ -4097,7 +4154,7 @@ function buildSpark01CloudConfig(options = {}) {
|
|
|
4097
4154
|
],
|
|
4098
4155
|
commands: [
|
|
4099
4156
|
"bun install -g @hasna/uptime@latest",
|
|
4100
|
-
"Generate the
|
|
4157
|
+
"Generate the private probe key locally and register only its public key with the hosted control plane once registration exists.",
|
|
4101
4158
|
"Write ~/.hasna/uptime/cloud.env from this plan, then source it for the private probe service.",
|
|
4102
4159
|
"Start the private probe worker only after hosted /api/v1 probe claim/submit routes are backed by cloud jobs."
|
|
4103
4160
|
],
|
|
@@ -4106,18 +4163,18 @@ function buildSpark01CloudConfig(options = {}) {
|
|
|
4106
4163
|
privateKeyInline: false,
|
|
4107
4164
|
tokenInline: false,
|
|
4108
4165
|
notes: [
|
|
4109
|
-
"This config is hosted-targeted preflight:
|
|
4166
|
+
"This config is hosted-targeted preflight: the private probe must not start until cloud probe routes are backed by hosted state.",
|
|
4110
4167
|
"The private key file path is referenced, not embedded.",
|
|
4111
4168
|
"Hosted token or probe auth material must come from the machine secret store, not this generated config."
|
|
4112
4169
|
]
|
|
4113
4170
|
}
|
|
4114
4171
|
};
|
|
4115
4172
|
}
|
|
4116
|
-
function
|
|
4173
|
+
function renderPrivateProbeEnv(config) {
|
|
4117
4174
|
const required = ["HASNA_UPTIME_PRIVATE_PROBE_ID"];
|
|
4118
4175
|
const missing = required.filter((key) => !config.env[key]);
|
|
4119
4176
|
if (missing.length > 0) {
|
|
4120
|
-
throw new Error(`
|
|
4177
|
+
throw new Error(`private probe env output requires ${missing.join(", ")}`);
|
|
4121
4178
|
}
|
|
4122
4179
|
return Object.entries(config.env).map(([key, value]) => `${key}=${shellEscape(value)}`).join(`
|
|
4123
4180
|
`);
|
|
@@ -4162,7 +4219,7 @@ export {
|
|
|
4162
4219
|
runHttpCheck,
|
|
4163
4220
|
runBrowserPageCheck,
|
|
4164
4221
|
rollbackImport,
|
|
4165
|
-
|
|
4222
|
+
renderPrivateProbeEnv,
|
|
4166
4223
|
probeResultSigningPayload,
|
|
4167
4224
|
probePublicKeyFingerprint,
|
|
4168
4225
|
previewImport,
|
|
@@ -4171,7 +4228,7 @@ export {
|
|
|
4171
4228
|
createUptimeClient,
|
|
4172
4229
|
createApiHandler,
|
|
4173
4230
|
buildUptimeReport,
|
|
4174
|
-
|
|
4231
|
+
buildPrivateProbeCloudConfig,
|
|
4175
4232
|
buildAwsDeploymentPlan,
|
|
4176
4233
|
applyImport,
|
|
4177
4234
|
UptimeStore,
|