@hasna/uptime 0.1.1 → 0.1.3

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/dist/checks.d.ts CHANGED
@@ -1,10 +1,32 @@
1
- import type { CheckAttemptResult, Monitor } from "./types.js";
1
+ import type { BrowserFailedRequest, CheckAttemptResult, EvidenceArtifact, Monitor } from "./types.js";
2
2
  export type FetchLike = (input: string, init: RequestInit) => Promise<{
3
3
  status: number;
4
4
  }>;
5
+ export type BrowserPageRunner = (monitor: Monitor) => Promise<BrowserPageRunnerResult>;
6
+ export interface BrowserPageRunnerResult {
7
+ finalUrl?: string | null;
8
+ navigationStatus?: number | null;
9
+ consoleErrors?: string[];
10
+ pageErrors?: string[];
11
+ failedRequests?: BrowserFailedRequest[];
12
+ screenshot?: (Partial<EvidenceArtifact> & {
13
+ ref: string;
14
+ path?: string;
15
+ }) | null;
16
+ artifacts?: Array<Partial<EvidenceArtifact> & {
17
+ ref: string;
18
+ path?: string;
19
+ }>;
20
+ latencyMs?: number | null;
21
+ }
5
22
  export declare function runMonitorCheck(monitor: Monitor, options?: {
6
23
  fetch?: FetchLike;
24
+ browserPage?: BrowserPageRunner;
7
25
  }): Promise<CheckAttemptResult>;
8
26
  export declare function runHttpCheck(monitor: Monitor, fetchImpl?: FetchLike): Promise<CheckAttemptResult>;
9
27
  export declare function runTcpCheck(monitor: Monitor): Promise<CheckAttemptResult>;
28
+ export declare function runBrowserPageCheck(monitor: Monitor, options?: {
29
+ fetch?: FetchLike;
30
+ runner?: BrowserPageRunner;
31
+ }): Promise<CheckAttemptResult>;
10
32
  //# sourceMappingURL=checks.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"checks.d.ts","sourceRoot":"","sources":["../src/checks.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAE9D,MAAM,MAAM,SAAS,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,KAAK,OAAO,CAAC;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC;AAE1F,wBAAsB,eAAe,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,GAAE;IAAE,KAAK,CAAC,EAAE,SAAS,CAAA;CAAO,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAMxH;AAED,wBAAsB,YAAY,CAAC,OAAO,EAAE,OAAO,EAAE,SAAS,GAAE,SAAiB,GAAG,OAAO,CAAC,kBAAkB,CAAC,CA+B9G;AAED,wBAAsB,WAAW,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAsB/E"}
1
+ {"version":3,"file":"checks.d.ts","sourceRoot":"","sources":["../src/checks.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,oBAAoB,EAAuB,kBAAkB,EAAE,gBAAgB,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAE3H,MAAM,MAAM,SAAS,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,KAAK,OAAO,CAAC;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC;AAC1F,MAAM,MAAM,iBAAiB,GAAG,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,uBAAuB,CAAC,CAAC;AAEvF,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,gBAAgB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IACzB,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB,cAAc,CAAC,EAAE,oBAAoB,EAAE,CAAC;IACxC,UAAU,CAAC,EAAE,CAAC,OAAO,CAAC,gBAAgB,CAAC,GAAG;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,GAAG,IAAI,CAAC;IACjF,SAAS,CAAC,EAAE,KAAK,CAAC,OAAO,CAAC,gBAAgB,CAAC,GAAG;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC9E,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAED,wBAAsB,eAAe,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,GAAE;IAAE,KAAK,CAAC,EAAE,SAAS,CAAC;IAAC,WAAW,CAAC,EAAE,iBAAiB,CAAA;CAAO,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAQzJ;AAED,wBAAsB,YAAY,CAAC,OAAO,EAAE,OAAO,EAAE,SAAS,GAAE,SAAiB,GAAG,OAAO,CAAC,kBAAkB,CAAC,CA+B9G;AAED,wBAAsB,WAAW,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAsB/E;AAED,wBAAsB,mBAAmB,CACvC,OAAO,EAAE,OAAO,EAChB,OAAO,GAAE;IAAE,KAAK,CAAC,EAAE,SAAS,CAAC;IAAC,MAAM,CAAC,EAAE,iBAAiB,CAAA;CAAO,GAC9D,OAAO,CAAC,kBAAkB,CAAC,CAqD7B"}
package/dist/checks.js CHANGED
@@ -7,7 +7,11 @@ async function runMonitorCheck(monitor, options = {}) {
7
7
  }
8
8
  if (monitor.kind === "http")
9
9
  return runHttpCheck(monitor, options.fetch ?? fetch);
10
- return runTcpCheck(monitor);
10
+ if (monitor.kind === "browser_page")
11
+ return runBrowserPageCheck(monitor, { fetch: options.fetch, runner: options.browserPage });
12
+ if (monitor.kind === "tcp")
13
+ return runTcpCheck(monitor);
14
+ return { status: "down", latencyMs: null, error: `unsupported monitor kind: ${monitor.kind ?? "unknown"}` };
11
15
  }
12
16
  async function runHttpCheck(monitor, fetchImpl = fetch) {
13
17
  if (!monitor.url)
@@ -65,8 +69,133 @@ async function runTcpCheck(monitor) {
65
69
  });
66
70
  });
67
71
  }
72
+ async function runBrowserPageCheck(monitor, options = {}) {
73
+ if (!monitor.url)
74
+ return { status: "down", latencyMs: null, error: "missing url" };
75
+ validateBrowserPageUrl(monitor.url);
76
+ if (!options.runner) {
77
+ const evidence = normalizeBrowserEvidence(monitor.url, {
78
+ finalUrl: monitor.url,
79
+ navigationStatus: null,
80
+ pageErrors: ["browser_page checks require a configured browser runner"]
81
+ });
82
+ return {
83
+ status: "down",
84
+ latencyMs: null,
85
+ statusCode: null,
86
+ error: "browser_page checks require a configured browser runner",
87
+ evidence
88
+ };
89
+ }
90
+ const started = performance.now();
91
+ try {
92
+ const raw = await options.runner(monitor);
93
+ const latencyMs = raw.latencyMs ?? Math.round((performance.now() - started) * 100) / 100;
94
+ const evidence = normalizeBrowserEvidence(monitor.url, raw);
95
+ const statusCode = raw.navigationStatus ?? evidence.navigationStatus;
96
+ const statusOk = statusCode == null ? false : monitor.expectedStatus == null ? statusCode >= 200 && statusCode < 400 : statusCode === monitor.expectedStatus;
97
+ const browserFailures = evidence.consoleErrors.length + evidence.pageErrors.length + evidence.failedRequests.length;
98
+ return {
99
+ status: statusOk && browserFailures === 0 ? "up" : "down",
100
+ latencyMs,
101
+ statusCode,
102
+ error: statusOk ? browserFailures === 0 ? null : `browser page captured ${browserFailures} error signal${browserFailures === 1 ? "" : "s"}` : `unexpected navigation status ${statusCode ?? "unknown"}`,
103
+ evidence
104
+ };
105
+ } catch (error) {
106
+ const safeError = redactText(error instanceof Error ? error.message : String(error));
107
+ const evidence = normalizeBrowserEvidence(monitor.url, {
108
+ finalUrl: monitor.url,
109
+ navigationStatus: null,
110
+ pageErrors: [safeError]
111
+ });
112
+ return {
113
+ status: "down",
114
+ latencyMs: Math.round((performance.now() - started) * 100) / 100,
115
+ statusCode: null,
116
+ error: safeError,
117
+ evidence
118
+ };
119
+ }
120
+ }
121
+ function normalizeBrowserEvidence(sourceUrl, raw) {
122
+ return {
123
+ kind: "browser_page",
124
+ finalUrl: raw.finalUrl ? redactUrl(raw.finalUrl) : redactUrl(sourceUrl),
125
+ navigationStatus: raw.navigationStatus ?? null,
126
+ consoleErrors: sanitizeStrings(raw.consoleErrors ?? []),
127
+ pageErrors: sanitizeStrings(raw.pageErrors ?? []),
128
+ failedRequests: (raw.failedRequests ?? []).slice(0, 50).map((request) => ({
129
+ url: redactUrl(request.url),
130
+ statusCode: request.statusCode ?? null,
131
+ error: request.error ? redactText(request.error) : null
132
+ })),
133
+ screenshot: raw.screenshot ? sanitizeArtifact(raw.screenshot) : null,
134
+ artifacts: (raw.artifacts ?? []).slice(0, 20).map(sanitizeArtifact),
135
+ redacted: true,
136
+ redactionStatus: "redacted",
137
+ retentionClass: "short"
138
+ };
139
+ }
140
+ function validateBrowserPageUrl(value) {
141
+ const parsed = new URL(value);
142
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
143
+ throw new Error("browser_page monitors require an http or https URL");
144
+ }
145
+ if (parsed.username || parsed.password) {
146
+ throw new Error("browser_page URLs must not contain userinfo");
147
+ }
148
+ }
149
+ function sanitizeStrings(values) {
150
+ return values.slice(0, 50).map(redactText).filter(Boolean);
151
+ }
152
+ function sanitizeArtifact(artifact) {
153
+ const ref = artifact.ref.trim();
154
+ if (artifact.path || ref.startsWith("/") || ref.toLowerCase().startsWith("file:")) {
155
+ throw new Error("browser evidence artifacts must use redacted artifact refs, not local paths");
156
+ }
157
+ if (!artifact.sha256 || !/^[a-f0-9]{64}$/i.test(artifact.sha256)) {
158
+ throw new Error("browser evidence artifacts require a sha256 checksum");
159
+ }
160
+ const bytes = artifact.bytes;
161
+ if (!Number.isInteger(bytes) || bytes == null || bytes < 0) {
162
+ throw new Error("browser evidence artifacts require a byte size");
163
+ }
164
+ return {
165
+ ref: redactText(ref),
166
+ sha256: artifact.sha256,
167
+ bytes,
168
+ contentType: redactText(artifact.contentType ?? "application/octet-stream") || "application/octet-stream",
169
+ retentionClass: "short"
170
+ };
171
+ }
172
+ function redactUrl(value) {
173
+ try {
174
+ const parsed = new URL(value);
175
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
176
+ return "[blocked-url]";
177
+ }
178
+ parsed.username = "";
179
+ parsed.password = "";
180
+ parsed.hash = "";
181
+ for (const key of parsed.searchParams.keys()) {
182
+ if (isSecretKey(key))
183
+ parsed.searchParams.set(key, "[redacted]");
184
+ }
185
+ return parsed.toString();
186
+ } catch {
187
+ return redactText(value);
188
+ }
189
+ }
190
+ function redactText(value) {
191
+ return value.replace(/\/(?:home|Users)\/[^\s"'<>]+/g, "[local-path]").replace(/Bearer\s+[A-Za-z0-9._~+/=-]+/gi, "Bearer [redacted]").replace(/((?:token|secret|password|passwd|api[_-]?key|access[_-]?token|auth|credential|session)[=:]\s*)[^\s&]+/gi, "$1[redacted]");
192
+ }
193
+ function isSecretKey(value) {
194
+ return /(token|secret|password|passwd|api[_-]?key|access[_-]?token|auth|credential|session)/i.test(value);
195
+ }
68
196
  export {
69
197
  runTcpCheck,
70
198
  runMonitorCheck,
71
- runHttpCheck
199
+ runHttpCheck,
200
+ runBrowserPageCheck
72
201
  };