@maschinenlesbar.org/pegel-online-cli 0.1.0 → 0.3.0

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.
Files changed (64) hide show
  1. package/README.md +29 -8
  2. package/dist/src/cli/commands/stations.d.ts +0 -1
  3. package/dist/src/cli/commands/stations.js +29 -5
  4. package/dist/src/cli/commands/timeseries.d.ts +0 -1
  5. package/dist/src/cli/commands/timeseries.js +3 -4
  6. package/dist/src/cli/index.d.ts +0 -1
  7. package/dist/src/cli/index.js +0 -1
  8. package/dist/src/cli/io.d.ts +2 -2
  9. package/dist/src/cli/io.js +7 -3
  10. package/dist/src/cli/program.d.ts +0 -1
  11. package/dist/src/cli/program.js +6 -7
  12. package/dist/src/cli/run.d.ts +12 -1
  13. package/dist/src/cli/run.js +35 -2
  14. package/dist/src/cli/shared.d.ts +21 -15
  15. package/dist/src/cli/shared.js +39 -48
  16. package/dist/src/client/client.d.ts +46 -1
  17. package/dist/src/client/client.js +152 -27
  18. package/dist/src/client/engine.d.ts +51 -13
  19. package/dist/src/client/engine.js +404 -81
  20. package/dist/src/client/errors.d.ts +38 -2
  21. package/dist/src/client/errors.js +74 -4
  22. package/dist/src/client/http.d.ts +26 -1
  23. package/dist/src/client/http.js +16 -2
  24. package/dist/src/client/index.d.ts +6 -4
  25. package/dist/src/client/index.js +4 -4
  26. package/dist/src/client/query.d.ts +0 -1
  27. package/dist/src/client/query.js +0 -1
  28. package/dist/src/client/types.d.ts +60 -10
  29. package/dist/src/client/types.js +0 -1
  30. package/dist/src/client/validate.d.ts +72 -0
  31. package/dist/src/client/validate.js +155 -0
  32. package/dist/src/index.d.ts +0 -1
  33. package/dist/src/index.js +0 -1
  34. package/package.json +4 -3
  35. package/dist/src/cli/commands/stations.d.ts.map +0 -1
  36. package/dist/src/cli/commands/stations.js.map +0 -1
  37. package/dist/src/cli/commands/timeseries.d.ts.map +0 -1
  38. package/dist/src/cli/commands/timeseries.js.map +0 -1
  39. package/dist/src/cli/index.d.ts.map +0 -1
  40. package/dist/src/cli/index.js.map +0 -1
  41. package/dist/src/cli/io.d.ts.map +0 -1
  42. package/dist/src/cli/io.js.map +0 -1
  43. package/dist/src/cli/program.d.ts.map +0 -1
  44. package/dist/src/cli/program.js.map +0 -1
  45. package/dist/src/cli/run.d.ts.map +0 -1
  46. package/dist/src/cli/run.js.map +0 -1
  47. package/dist/src/cli/shared.d.ts.map +0 -1
  48. package/dist/src/cli/shared.js.map +0 -1
  49. package/dist/src/client/client.d.ts.map +0 -1
  50. package/dist/src/client/client.js.map +0 -1
  51. package/dist/src/client/engine.d.ts.map +0 -1
  52. package/dist/src/client/engine.js.map +0 -1
  53. package/dist/src/client/errors.d.ts.map +0 -1
  54. package/dist/src/client/errors.js.map +0 -1
  55. package/dist/src/client/http.d.ts.map +0 -1
  56. package/dist/src/client/http.js.map +0 -1
  57. package/dist/src/client/index.d.ts.map +0 -1
  58. package/dist/src/client/index.js.map +0 -1
  59. package/dist/src/client/query.d.ts.map +0 -1
  60. package/dist/src/client/query.js.map +0 -1
  61. package/dist/src/client/types.d.ts.map +0 -1
  62. package/dist/src/client/types.js.map +0 -1
  63. package/dist/src/index.d.ts.map +0 -1
  64. package/dist/src/index.js.map +0 -1
@@ -7,9 +7,29 @@ export declare class PegelError extends Error {
7
7
  /**
8
8
  * Replace the userinfo of a URL (`https://user:secret@host/...`) with `***`, so a
9
9
  * credential in a base URL never reaches an error message, a log or CI output.
10
- * A URL without userinfo, or one that does not parse, is returned unchanged.
10
+ * A value that does not parse as a URL (a port typo, an unencoded "#" in the
11
+ * password) or has no scheme (`user:pw@host`) is cut by text instead
12
+ * (`credentialsIn` + `redactCredentials`); one without userinfo is returned unchanged.
11
13
  */
12
14
  export declare function redactUrl(url: string): string;
15
+ /**
16
+ * The userinfo a URL-like value carries, exactly as written — `["alice:pa#ss"]` for
17
+ * `https://alice:pa#ss@host` — or `[]` when it carries none. It works on values that
18
+ * don't parse as a URL too, and on values with a prefix (`--base-url=https://u:p@h`):
19
+ * the userinfo is everything between `://` and the last `@` before the host. A value
20
+ * without a scheme counts when it reads `user:password@host`. Used to redact those
21
+ * exact strings from text that echoes the value (usage errors, help), whatever
22
+ * characters the password contains.
23
+ */
24
+ export declare function credentialsIn(value: string): string[];
25
+ /**
26
+ * `text` with every occurrence of each credential (as `credentialsIn` returns them)
27
+ * that is followed by `@` replaced by `***`. Matching the exact strings, not a
28
+ * pattern, covers passwords with spaces, quotes, `#`, `?` or `/` that no URL pattern
29
+ * can delimit. The percent-encoded form (`alice%3Apw%40`, as a URL typed where a
30
+ * station id belongs ends up in a request path) is replaced too.
31
+ */
32
+ export declare function redactCredentials(text: string, credentials: readonly string[]): string;
13
33
  /**
14
34
  * The API responded with a non-2xx status code. `detail` holds a human-readable
15
35
  * message extracted from the response body when one is present.
@@ -26,6 +46,12 @@ export declare class PegelApiError extends PegelError {
26
46
  * redacted. The message names it.
27
47
  */
28
48
  readonly location: string | undefined;
49
+ /**
50
+ * For a 429/503 that was not retried because its `Retry-After` asked for longer than
51
+ * the client waits (`MAX_RETRY_AFTER_MS`, 30 s): the wait the server asked for, in
52
+ * milliseconds. Retrying before then won't help.
53
+ */
54
+ readonly retryAfterMs: number | undefined;
29
55
  constructor(args: {
30
56
  status: number;
31
57
  url: string;
@@ -35,14 +61,24 @@ export declare class PegelApiError extends PegelError {
35
61
  location?: string;
36
62
  /** Redirects already followed when the limit stopped this one (> 0 only). */
37
63
  redirectsFollowed?: number;
64
+ /** A Retry-After longer than the client waits (not retried). */
65
+ retryAfterMs?: number;
66
+ /** Advice appended to the message (e.g. that a redirect dropped the credentials). */
67
+ hint?: string;
38
68
  });
39
69
  /** True for statuses the API documents as transient and retry-able. */
40
70
  get isRetryable(): boolean;
41
71
  }
72
+ /**
73
+ * An input the library rejects before sending any request: a client option or a
74
+ * method argument that breaks one of the rules in `validate.ts`. The message reads
75
+ * `Invalid <name>: <reason>`. The CLI reports it as a usage error (exit 2).
76
+ */
77
+ export declare class PegelValidationError extends PegelError {
78
+ }
42
79
  /** A transport-level failure (DNS, connection reset, timeout, ...). */
43
80
  export declare class PegelNetworkError extends PegelError {
44
81
  }
45
82
  /** The response body could not be parsed as the expected JSON shape. */
46
83
  export declare class PegelParseError extends PegelError {
47
84
  }
48
- //# sourceMappingURL=errors.d.ts.map
@@ -10,7 +10,9 @@ export class PegelError extends Error {
10
10
  /**
11
11
  * Replace the userinfo of a URL (`https://user:secret@host/...`) with `***`, so a
12
12
  * credential in a base URL never reaches an error message, a log or CI output.
13
- * A URL without userinfo, or one that does not parse, is returned unchanged.
13
+ * A value that does not parse as a URL (a port typo, an unencoded "#" in the
14
+ * password) or has no scheme (`user:pw@host`) is cut by text instead
15
+ * (`credentialsIn` + `redactCredentials`); one without userinfo is returned unchanged.
14
16
  */
15
17
  export function redactUrl(url) {
16
18
  let parsed;
@@ -18,14 +20,63 @@ export function redactUrl(url) {
18
20
  parsed = new URL(url);
19
21
  }
20
22
  catch {
21
- return url;
23
+ return redactCredentials(url, credentialsIn(url));
22
24
  }
25
+ // `user:pw@host` without a scheme parses as a URL with the scheme "user:": no userinfo.
23
26
  if (parsed.username === "" && parsed.password === "")
24
- return url;
27
+ return redactCredentials(url, credentialsIn(url));
25
28
  parsed.username = "***";
26
29
  parsed.password = "";
27
30
  return parsed.href;
28
31
  }
32
+ /**
33
+ * The userinfo a URL-like value carries, exactly as written — `["alice:pa#ss"]` for
34
+ * `https://alice:pa#ss@host` — or `[]` when it carries none. It works on values that
35
+ * don't parse as a URL too, and on values with a prefix (`--base-url=https://u:p@h`):
36
+ * the userinfo is everything between `://` and the last `@` before the host. A value
37
+ * without a scheme counts when it reads `user:password@host`. Used to redact those
38
+ * exact strings from text that echoes the value (usage errors, help), whatever
39
+ * characters the password contains.
40
+ */
41
+ export function credentialsIn(value) {
42
+ const schemeAt = value.indexOf("://");
43
+ const rest = schemeAt >= 0 ? value.slice(schemeAt + 3) : value;
44
+ // Without a scheme only the unmistakable `user:password@host` form counts.
45
+ if (schemeAt < 0 && !/^[^\s/@:]+:[^@]*@[^@\s/]/.test(rest))
46
+ return [];
47
+ // The URL itself starts at its scheme (`--base-url=https://…` has a prefix).
48
+ const scheme = schemeAt >= 0 ? /[a-z][a-z0-9+.-]*$/i.exec(value.slice(0, schemeAt)) : null;
49
+ let parses = false;
50
+ try {
51
+ new URL(schemeAt >= 0 ? value.slice(scheme?.index ?? schemeAt) : `http://${rest}`);
52
+ parses = true;
53
+ }
54
+ catch {
55
+ // Doesn't parse: the password may hold "/", "?", "#" or spaces.
56
+ }
57
+ // In a URL that parses, the userinfo ends at the last "@" of the authority (before
58
+ // the first "/", "?" or "#"); in one that doesn't, at the last "@" of the value.
59
+ const authority = parses ? rest.slice(0, rest.search(/[/?#]|$/)) : rest;
60
+ const end = authority.lastIndexOf("@");
61
+ return end > 0 ? [rest.slice(0, end)] : [];
62
+ }
63
+ /**
64
+ * `text` with every occurrence of each credential (as `credentialsIn` returns them)
65
+ * that is followed by `@` replaced by `***`. Matching the exact strings, not a
66
+ * pattern, covers passwords with spaces, quotes, `#`, `?` or `/` that no URL pattern
67
+ * can delimit. The percent-encoded form (`alice%3Apw%40`, as a URL typed where a
68
+ * station id belongs ends up in a request path) is replaced too.
69
+ */
70
+ export function redactCredentials(text, credentials) {
71
+ let out = text;
72
+ for (const secret of credentials) {
73
+ if (secret === "")
74
+ continue;
75
+ out = out.split(`${secret}@`).join("***@");
76
+ out = out.split(`${encodeURIComponent(secret)}%40`).join("***%40");
77
+ }
78
+ return out;
79
+ }
29
80
  /**
30
81
  * The API responded with a non-2xx status code. `detail` holds a human-readable
31
82
  * message extracted from the response body when one is present.
@@ -42,6 +93,12 @@ export class PegelApiError extends PegelError {
42
93
  * redacted. The message names it.
43
94
  */
44
95
  location;
96
+ /**
97
+ * For a 429/503 that was not retried because its `Retry-After` asked for longer than
98
+ * the client waits (`MAX_RETRY_AFTER_MS`, 30 s): the wait the server asked for, in
99
+ * milliseconds. Retrying before then won't help.
100
+ */
101
+ retryAfterMs;
45
102
  constructor(args) {
46
103
  const parts = [];
47
104
  if (args.detail)
@@ -54,6 +111,12 @@ export class PegelApiError extends PegelError {
54
111
  ? `redirect to ${args.location} not followed${limit}`
55
112
  : "redirect not followed (no Location header)");
56
113
  }
114
+ if (args.retryAfterMs !== undefined) {
115
+ parts.push(`the server asked to wait ${Math.ceil(args.retryAfterMs / 1000)} s (Retry-After), longer than the 30 s ` +
116
+ "the client waits, so it was not retried; try again after that");
117
+ }
118
+ if (args.hint)
119
+ parts.push(args.hint);
57
120
  const detailPart = parts.length > 0 ? `: ${parts.join("; ")}` : "";
58
121
  // The URL is shown without userinfo: a credential in --base-url must not leak.
59
122
  const url = redactUrl(args.url);
@@ -64,16 +127,23 @@ export class PegelApiError extends PegelError {
64
127
  this.body = args.body;
65
128
  this.detail = args.detail;
66
129
  this.location = args.location;
130
+ this.retryAfterMs = args.retryAfterMs;
67
131
  }
68
132
  /** True for statuses the API documents as transient and retry-able. */
69
133
  get isRetryable() {
70
134
  return this.status === 429 || this.status === 503;
71
135
  }
72
136
  }
137
+ /**
138
+ * An input the library rejects before sending any request: a client option or a
139
+ * method argument that breaks one of the rules in `validate.ts`. The message reads
140
+ * `Invalid <name>: <reason>`. The CLI reports it as a usage error (exit 2).
141
+ */
142
+ export class PegelValidationError extends PegelError {
143
+ }
73
144
  /** A transport-level failure (DNS, connection reset, timeout, ...). */
74
145
  export class PegelNetworkError extends PegelError {
75
146
  }
76
147
  /** The response body could not be parsed as the expected JSON shape. */
77
148
  export class PegelParseError extends PegelError {
78
149
  }
79
- //# sourceMappingURL=errors.js.map
@@ -6,17 +6,43 @@ export interface HttpRequest {
6
6
  headers?: Record<string, string>;
7
7
  /** Optional request body (already serialised). */
8
8
  body?: string | Buffer;
9
+ /**
10
+ * Always `"manual"`: a transport must not follow redirects itself (fetch does by
11
+ * default — pass `redirect: request.redirect`). The engine follows them, keeping the
12
+ * base URL's credentials on the same origin and dropping them, and credential headers,
13
+ * on another. A response whose `url` (the final URL a fetch transport may report) lies
14
+ * on another origin than the request is rejected as a PegelNetworkError.
15
+ */
16
+ redirect?: "manual";
9
17
  /** Timeout for the whole request, response body included, in milliseconds. */
10
18
  timeoutMs?: number;
11
19
  /** Hard cap on the response body size in bytes; the request aborts if exceeded. */
12
20
  maxResponseBytes?: number;
21
+ /**
22
+ * Aborted when the engine's overall deadline (`timeoutMs`) passes. A transport should
23
+ * stop the request then (`fetch(url, { signal })`); the engine rejects at the deadline
24
+ * either way, and enforces `maxResponseBytes` on the body it gets back, so neither
25
+ * limit depends on it.
26
+ */
27
+ signal?: AbortSignal;
13
28
  }
29
+ /**
30
+ * What a transport resolves with. The engine is lenient about the shapes a custom
31
+ * transport naturally returns: `headers` may be a plain record in any letter case, a
32
+ * WHATWG `Headers` object or a `Map`; `body` may be a Buffer, any `ArrayBuffer` view
33
+ * (fetch's `Uint8Array`, from any realm) or an `ArrayBuffer`. Anything else, or a
34
+ * `status` outside 100–599, is a PegelNetworkError.
35
+ */
14
36
  export interface HttpResponse {
15
37
  status: number;
16
38
  headers: http.IncomingHttpHeaders;
17
39
  body: Buffer;
40
+ /** The URL that answered, if the transport knows it (fetch's `Response.url`). */
41
+ url?: string;
18
42
  }
19
43
  export type Transport = (request: HttpRequest) => Promise<HttpResponse>;
44
+ /** The message for a body over the size cap, naming the option on both sides. */
45
+ export declare function sizeLimitMessage(maxBytes: number): string;
20
46
  /**
21
47
  * The longest delay Node's timers support (2^31 - 1 ms, about 24.8 days). A longer one
22
48
  * prints a TimeoutOverflowWarning and fires after 1 ms, so timeouts are capped here.
@@ -28,4 +54,3 @@ export declare const MAX_TIMEOUT_MS = 2147483647;
28
54
  * (connection errors, timeouts, malformed URLs).
29
55
  */
30
56
  export declare const nodeHttpTransport: Transport;
31
- //# sourceMappingURL=http.d.ts.map
@@ -8,6 +8,10 @@
8
8
  import http from "node:http";
9
9
  import https from "node:https";
10
10
  import { PegelNetworkError, redactUrl } from "./errors.js";
11
+ /** The message for a body over the size cap, naming the option on both sides. */
12
+ export function sizeLimitMessage(maxBytes) {
13
+ return `Response exceeded the size limit of ${maxBytes} bytes (maxResponseBytes; --max-response-bytes on the CLI)`;
14
+ }
11
15
  /**
12
16
  * The longest delay Node's timers support (2^31 - 1 ms, about 24.8 days). A longer one
13
17
  * prints a TimeoutOverflowWarning and fires after 1 ms, so timeouts are capped here.
@@ -71,7 +75,7 @@ export const nodeHttpTransport = (request) => new Promise((resolve, reject) => {
71
75
  if (maxBytes !== undefined && received > maxBytes) {
72
76
  aborted = true;
73
77
  res.destroy();
74
- settleReject(new PegelNetworkError(`Response exceeded maxResponseBytes (${maxBytes})`));
78
+ settleReject(new PegelNetworkError(sizeLimitMessage(maxBytes)));
75
79
  return;
76
80
  }
77
81
  chunks.push(chunk);
@@ -101,6 +105,17 @@ export const nodeHttpTransport = (request) => new Promise((resolve, reject) => {
101
105
  // Do not let the deadline timer alone keep the process alive.
102
106
  deadlineTimer.unref?.();
103
107
  }
108
+ if (request.signal !== undefined) {
109
+ const abort = () => {
110
+ const err = new PegelNetworkError(`Request timed out after ${request.timeoutMs ?? 0}ms`);
111
+ settleReject(err);
112
+ req.destroy(err);
113
+ };
114
+ if (request.signal.aborted)
115
+ abort();
116
+ else
117
+ request.signal.addEventListener("abort", abort, { once: true });
118
+ }
104
119
  req.on("error", (err) => {
105
120
  // A timeout destroy already passes an PegelNetworkError; don't double-wrap.
106
121
  settleReject(err instanceof PegelNetworkError ? err : new PegelNetworkError(err.message, { cause: err }));
@@ -109,4 +124,3 @@ export const nodeHttpTransport = (request) => new Promise((resolve, reject) => {
109
124
  req.write(request.body);
110
125
  req.end();
111
126
  });
112
- //# sourceMappingURL=http.js.map
@@ -1,10 +1,12 @@
1
- export { PegelOnlineClient } from "./client.js";
2
- export { RequestEngine, DEFAULT_BASE_URL, MAX_RETRIES, MAX_RETRY_AFTER_MS, parseRetryAfter, } from "./engine.js";
1
+ export { NO_VALUE_SENTINEL, PegelOnlineClient, stationListNotes } from "./client.js";
2
+ export type { StationListNote } from "./client.js";
3
+ export { RequestEngine, DEFAULT_BASE_URL, MAX_RETRIES, MAX_RETRY_AFTER_MS, isTransientNetworkError, parseRetryAfter, validateBaseUrl, } from "./engine.js";
3
4
  export type { EngineOptions, RawResponse } from "./engine.js";
4
5
  export { MAX_TIMEOUT_MS, nodeHttpTransport } from "./http.js";
5
6
  export type { Transport, HttpRequest, HttpResponse } from "./http.js";
6
7
  export { buildQueryString } from "./query.js";
7
8
  export type { QueryParams, QueryValue } from "./query.js";
8
- export { PegelError, PegelApiError, PegelNetworkError, PegelParseError, redactUrl } from "./errors.js";
9
+ export { PegelError, PegelApiError, PegelNetworkError, PegelParseError, PegelValidationError, redactUrl, credentialsIn, redactCredentials, } from "./errors.js";
10
+ export { assertValid, baseUrlProblem, baseUrlWhitespaceProblem, headerValueProblem, idListProblem, isBlank, knownKeysProblem, nonEmptyProblem, normalizeInput, optionalBooleanProblem, } from "./validate.js";
11
+ export type { Problem } from "./validate.js";
9
12
  export * from "./types.js";
10
- //# sourceMappingURL=index.d.ts.map
@@ -1,8 +1,8 @@
1
1
  // Public entry point for the API client library.
2
- export { PegelOnlineClient } from "./client.js";
3
- export { RequestEngine, DEFAULT_BASE_URL, MAX_RETRIES, MAX_RETRY_AFTER_MS, parseRetryAfter, } from "./engine.js";
2
+ export { NO_VALUE_SENTINEL, PegelOnlineClient, stationListNotes } from "./client.js";
3
+ export { RequestEngine, DEFAULT_BASE_URL, MAX_RETRIES, MAX_RETRY_AFTER_MS, isTransientNetworkError, parseRetryAfter, validateBaseUrl, } from "./engine.js";
4
4
  export { MAX_TIMEOUT_MS, nodeHttpTransport } from "./http.js";
5
5
  export { buildQueryString } from "./query.js";
6
- export { PegelError, PegelApiError, PegelNetworkError, PegelParseError, redactUrl } from "./errors.js";
6
+ export { PegelError, PegelApiError, PegelNetworkError, PegelParseError, PegelValidationError, redactUrl, credentialsIn, redactCredentials, } from "./errors.js";
7
+ export { assertValid, baseUrlProblem, baseUrlWhitespaceProblem, headerValueProblem, idListProblem, isBlank, knownKeysProblem, nonEmptyProblem, normalizeInput, optionalBooleanProblem, } from "./validate.js";
7
8
  export * from "./types.js";
8
- //# sourceMappingURL=index.js.map
@@ -6,4 +6,3 @@ export type QueryParams = Record<string, QueryValue>;
6
6
  * Returns an empty string when no parameters survive filtering.
7
7
  */
8
8
  export declare function buildQueryString(params: QueryParams): string;
9
- //# sourceMappingURL=query.d.ts.map
@@ -30,4 +30,3 @@ export function buildQueryString(params) {
30
30
  // URLSearchParams encodes spaces as "+"; "%20" is more broadly interoperable.
31
31
  return search.toString().replace(/\+/g, "%20");
32
32
  }
33
- //# sourceMappingURL=query.js.map
@@ -24,14 +24,32 @@ export interface Station {
24
24
  water?: Water;
25
25
  timeseries?: TimeseriesInfo[];
26
26
  }
27
+ /**
28
+ * The API's classification of a current water level (`stateMnwMhw`, `stateNswHsw`), as
29
+ * documented upstream:
30
+ * - `low` — at or below MNW (stateMnwMhw only);
31
+ * - `normal` — between MNW and MHW, or between 0 and HSW;
32
+ * - `high` — at or above MHW, or HSW;
33
+ * - `unknown` — the series has no MNW/MHW (or HSW) mark to compare with;
34
+ * - `commented` — **gauge malfunction or disruption** ("Fehlfunktion oder Störung"): the
35
+ * value may be wrong; the reason is in the series' `comment` (`TimeseriesInfo.comment`);
36
+ * - `out-dated` — the reading is older than 25 hours.
37
+ * Typed open (`string & {}`) so a value the API adds later still type-checks.
38
+ */
39
+ export type MeasurementState = "low" | "normal" | "high" | "unknown" | "commented" | "out-dated" | (string & {});
27
40
  /** A measurement value plus the API's state classifications. */
28
41
  export interface CurrentMeasurement {
29
42
  timestamp: string;
30
- value: number;
31
- /** Classification vs. the mean low/high water marks. */
32
- stateMnwMhw?: string;
33
- /** Classification vs. the lowest/highest navigable water marks. */
34
- stateNswHsw?: string;
43
+ /**
44
+ * The reading, in the unit of its timeseries (`TimeseriesInfo.unit` — not always `cm`
45
+ * for `W`). `null` when the gauge reported no value: the client maps the sentinel
46
+ * `99999` (`NO_VALUE_SENTINEL`) the API relays for that to `null`.
47
+ */
48
+ value: number | null;
49
+ /** Classification vs. the mean low/high water marks (water levels only). */
50
+ stateMnwMhw?: MeasurementState;
51
+ /** Classification vs. the lowest/highest navigable water marks (water levels only). */
52
+ stateNswHsw?: MeasurementState;
35
53
  }
36
54
  /** The datum a water-level series is measured from (Pegelnullpunkt). */
37
55
  export interface GaugeZero {
@@ -41,14 +59,37 @@ export interface GaugeZero {
41
59
  /** Date the datum applies from, e.g. "2019-11-01". */
42
60
  validFrom?: string;
43
61
  }
62
+ /**
63
+ * An operator's note on a timeseries, present while something is wrong with it — e.g.
64
+ * "Funktionsstörung, fehlerhafte Messwerte" (malfunction, faulty readings) at RINTELN,
65
+ * "Techn. Störung", "Behelfspegel - Messwerte können Fehler aufweisen" (temporary gauge,
66
+ * values may be wrong), "vorübergehend außer Betrieb". A current measurement whose state
67
+ * is `commented` points here.
68
+ */
69
+ export interface TimeseriesComment {
70
+ shortDescription: string;
71
+ longDescription?: string;
72
+ }
44
73
  /** Metadata for one timeseries of a station (e.g. "W" water level, "Q" flow). */
45
74
  export interface TimeseriesInfo {
46
75
  shortname: string;
47
76
  longname: string;
77
+ /**
78
+ * The unit of every value of this series, as the API publishes it. Read it; never
79
+ * assume it from the shortname: most `W` (water level) series are in `cm`, but canal
80
+ * and reservoir gauges publish `W` in `m+NN` (metres above sea level) or `m+PNP`
81
+ * (metres above the gauge zero) — 69 of 737 W series on 5 October 2026, e.g. MÜNSTER OW
82
+ * at 56.54 m+NN. `Q` is usually `m³/s`, temperatures `°C`.
83
+ */
48
84
  unit: string;
49
85
  equidistance?: number;
50
86
  /** Gauge zero of a water-level series (absent on e.g. flow series). */
51
87
  gaugeZero?: GaugeZero;
88
+ /**
89
+ * The operator's note while the series is disturbed (see {@link TimeseriesComment});
90
+ * absent otherwise. Read it whenever a reading's state is `commented`.
91
+ */
92
+ comment?: TimeseriesComment;
52
93
  currentMeasurement?: CurrentMeasurement;
53
94
  /**
54
95
  * Characteristic values (gauge marks), present only when requested; an empty
@@ -59,14 +100,24 @@ export interface TimeseriesInfo {
59
100
  /** One point of a measurements series. */
60
101
  export interface Measurement {
61
102
  timestamp: string;
62
- value: number;
103
+ /**
104
+ * The reading in the timeseries' unit, or `null` for a point without a value (the
105
+ * sentinel `99999`, mapped by the client). Skip `null` points before a minimum, maximum
106
+ * or trend.
107
+ */
108
+ value: number | null;
63
109
  }
64
110
  /** Parameters for the stations listing. */
65
111
  export interface StationListParams {
66
- /** Station identifiers (uuid/number/shortname/longname); sent comma-separated. */
112
+ /**
113
+ * Station identifiers (uuid/number/shortname/longname); sent comma-separated.
114
+ * At least one, none blank. A name may match more than one station (NEUSTADT: LEINE
115
+ * and OSTSEE) and an unknown one matches none, silently: `stationListNotes` reports both.
116
+ */
67
117
  ids?: string[];
68
- /** Water shortname filter. */
118
+ /** Water shortname filter; not blank. */
69
119
  waters?: string;
120
+ /** Fuzzy id match; not blank. */
70
121
  fuzzyId?: string;
71
122
  /** Embed each station's timeseries list. */
72
123
  includeTimeseries?: boolean;
@@ -90,9 +141,8 @@ export interface IncludeParams {
90
141
  includeCurrentMeasurement?: boolean;
91
142
  includeCharacteristicValues?: boolean;
92
143
  }
93
- /** Time window for a measurements request (ISO-8601 instants or periods, e.g. "P7D"). */
144
+ /** Time window for a measurements request (ISO-8601 instants or periods, e.g. "P7D"); neither bound may be blank. */
94
145
  export interface MeasurementsParams {
95
146
  start?: string;
96
147
  end?: string;
97
148
  }
98
- //# sourceMappingURL=types.d.ts.map
@@ -1,4 +1,3 @@
1
1
  // Domain types for the PEGELONLINE REST API v2 (pegelonline.wsv.de), the
2
2
  // Wasserstraßen- und Schifffahrtsverwaltung's water-level web service.
3
3
  export {};
4
- //# sourceMappingURL=types.js.map
@@ -0,0 +1,72 @@
1
+ /** A rule: the reason `value` is invalid (e.g. `"Expected a non-empty value."`), or `undefined` when it is valid. */
2
+ export type Problem<T = unknown> = (value: T) => string | undefined;
3
+ /**
4
+ * Throw a {@link PegelValidationError} with the message `Invalid <name>: <reason>`
5
+ * when `problem(value)` finds a reason; otherwise return `value` unchanged. Call it
6
+ * before any request, so a rejected input sends nothing. Async methods call it
7
+ * inside their body, so the rejection arrives as a rejected promise rather than a
8
+ * synchronous throw.
9
+ */
10
+ export declare function assertValid<T>(name: string, value: T, problem: Problem<T>): T;
11
+ /**
12
+ * The form in which an id, name or filter value is sent: surrounding whitespace removed
13
+ * and composed (NFC). The API matches station names, waters and ids exactly, so
14
+ * `"RHEIN "` (a trailing space from a copy) listed no station and `"BONN "` was a 404, and
15
+ * a decomposed umlaut ("KO" + U+0308 + "LN", as pasted from macOS file names or some
16
+ * PDFs) found nothing either; no name upstream begins or ends with whitespace. NFC, not
17
+ * NFKC: an id lookup must not rewrite compatibility characters, and case is left alone
18
+ * (the API ignores it for station ids, but not everywhere).
19
+ */
20
+ export declare function normalizeInput(value: string): string;
21
+ /** True for an empty or whitespace-only string. */
22
+ export declare function isBlank(value: string): boolean;
23
+ /**
24
+ * A filter or query value must be a non-blank string: the API treats an empty
25
+ * parameter (`?waters=`, `?start=`) as no filter, so a blank value would silently
26
+ * return the unfiltered set or the default window.
27
+ */
28
+ export declare const nonEmptyProblem: Problem<unknown>;
29
+ /**
30
+ * The `ids` filter of `stations.list`: at least one id, none of them blank. An
31
+ * empty list would be dropped and list every station; a blank entry would be sent
32
+ * as `ids=BONN,%20`.
33
+ */
34
+ export declare const idListProblem: Problem<unknown>;
35
+ /**
36
+ * Whitespace and control characters in a base URL. `new URL()` silently trims
37
+ * surrounding whitespace and strips an interior tab or newline, so the URL checks
38
+ * pass, but the engine concatenates request paths onto the raw string:
39
+ * `"https://h/ "` would request `/%20/webservices/...`, and a custom transport would
40
+ * get the raw padded value.
41
+ */
42
+ export declare const baseUrlWhitespaceProblem: Problem<unknown>;
43
+ /**
44
+ * The full base-URL rule set, in order: no whitespace or control characters
45
+ * (baseUrlWhitespaceProblem), an absolute URL, an `http:`/`https:` scheme, no
46
+ * query or fragment, and no `%` in the userinfo that doesn't start a valid escape
47
+ * (`%25` for a literal one). Request paths are appended to the base URL as a string, so a
48
+ * `?` or `#` in it would swallow every path: `http://h/?x=1` requests
49
+ * `/?x=1/webservices/...` and `http://h/#f` requests `/`. A path prefix is fine, and
50
+ * userinfo is allowed (Node sends it as Basic auth). The reasons name no URL, so a
51
+ * credential in it never reaches a message.
52
+ */
53
+ export declare const baseUrlProblem: Problem<unknown>;
54
+ /**
55
+ * A value that goes into an HTTP header (the User-Agent): non-blank, no C0 control
56
+ * or DEL (tab is allowed, as in HTTP), nothing above U+00FF. A blank value would be
57
+ * sent as an empty header instead of the default; Node's HTTP layer refuses the
58
+ * others with an opaque "Invalid character in header content" TypeError at request
59
+ * time, and an injected transport would send a CR/LF value as is (header
60
+ * injection). Checked by char code so the source stays free of control bytes.
61
+ */
62
+ export declare const headerValueProblem: Problem<unknown>;
63
+ /**
64
+ * A parameter object of a client method: a plain object whose own keys are all in
65
+ * `allowed` (an `undefined` value counts as unset and is ignored). A misspelled key
66
+ * (`water` for `waters`, `fuzzyID`), `__proto__` or `constructor` was dropped silently
67
+ * and the API answered with every station; TypeScript catches a typo, JavaScript and a
68
+ * JSON config do not. The reason names the key, a close match and the allowed keys.
69
+ */
70
+ export declare function knownKeysProblem(allowed: readonly string[]): Problem<unknown>;
71
+ /** An optional flag (`includeTimeseries` …): `true`, `false` or unset — not "yes", 1 or "false". */
72
+ export declare const optionalBooleanProblem: Problem<unknown>;