@maschinenlesbar.org/pegel-online-cli 0.0.1

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 (65) hide show
  1. package/CONTRIBUTING.md +25 -0
  2. package/LICENSE +661 -0
  3. package/LICENSING.md +47 -0
  4. package/README.md +203 -0
  5. package/dist/src/cli/commands/stations.d.ts +4 -0
  6. package/dist/src/cli/commands/stations.d.ts.map +1 -0
  7. package/dist/src/cli/commands/stations.js +45 -0
  8. package/dist/src/cli/commands/stations.js.map +1 -0
  9. package/dist/src/cli/commands/timeseries.d.ts +4 -0
  10. package/dist/src/cli/commands/timeseries.d.ts.map +1 -0
  11. package/dist/src/cli/commands/timeseries.js +38 -0
  12. package/dist/src/cli/commands/timeseries.js.map +1 -0
  13. package/dist/src/cli/index.d.ts +3 -0
  14. package/dist/src/cli/index.d.ts.map +1 -0
  15. package/dist/src/cli/index.js +6 -0
  16. package/dist/src/cli/index.js.map +1 -0
  17. package/dist/src/cli/io.d.ts +13 -0
  18. package/dist/src/cli/io.d.ts.map +1 -0
  19. package/dist/src/cli/io.js +7 -0
  20. package/dist/src/cli/io.js.map +1 -0
  21. package/dist/src/cli/program.d.ts +7 -0
  22. package/dist/src/cli/program.d.ts.map +1 -0
  23. package/dist/src/cli/program.js +52 -0
  24. package/dist/src/cli/program.js.map +1 -0
  25. package/dist/src/cli/run.d.ts +3 -0
  26. package/dist/src/cli/run.d.ts.map +1 -0
  27. package/dist/src/cli/run.js +62 -0
  28. package/dist/src/cli/run.js.map +1 -0
  29. package/dist/src/cli/shared.d.ts +44 -0
  30. package/dist/src/cli/shared.d.ts.map +1 -0
  31. package/dist/src/cli/shared.js +81 -0
  32. package/dist/src/cli/shared.js.map +1 -0
  33. package/dist/src/client/client.d.ts +28 -0
  34. package/dist/src/client/client.d.ts.map +1 -0
  35. package/dist/src/client/client.js +79 -0
  36. package/dist/src/client/client.js.map +1 -0
  37. package/dist/src/client/engine.d.ts +54 -0
  38. package/dist/src/client/engine.d.ts.map +1 -0
  39. package/dist/src/client/engine.js +120 -0
  40. package/dist/src/client/engine.js.map +1 -0
  41. package/dist/src/client/errors.d.ts +33 -0
  42. package/dist/src/client/errors.d.ts.map +1 -0
  43. package/dist/src/client/errors.js +40 -0
  44. package/dist/src/client/errors.js.map +1 -0
  45. package/dist/src/client/http.d.ts +26 -0
  46. package/dist/src/client/http.d.ts.map +1 -0
  47. package/dist/src/client/http.js +82 -0
  48. package/dist/src/client/http.js.map +1 -0
  49. package/dist/src/client/index.d.ts +10 -0
  50. package/dist/src/client/index.d.ts.map +1 -0
  51. package/dist/src/client/index.js +8 -0
  52. package/dist/src/client/index.js.map +1 -0
  53. package/dist/src/client/query.d.ts +9 -0
  54. package/dist/src/client/query.d.ts.map +1 -0
  55. package/dist/src/client/query.js +33 -0
  56. package/dist/src/client/query.js.map +1 -0
  57. package/dist/src/client/types.d.ts +71 -0
  58. package/dist/src/client/types.d.ts.map +1 -0
  59. package/dist/src/client/types.js +4 -0
  60. package/dist/src/client/types.js.map +1 -0
  61. package/dist/src/index.d.ts +2 -0
  62. package/dist/src/index.d.ts.map +1 -0
  63. package/dist/src/index.js +3 -0
  64. package/dist/src/index.js.map +1 -0
  65. package/package.json +66 -0
@@ -0,0 +1,81 @@
1
+ // Shared helpers used across CLI command groups: option parsers, the global
2
+ // option resolver, and the JSON result renderer.
3
+ import { InvalidArgumentError } from "commander";
4
+ import { PegelError } from "../client/errors.js";
5
+ /** commander value-parser: a non-negative integer. */
6
+ export function parseIntArg(value) {
7
+ // Require a plain decimal integer. Reject blank/whitespace ("" and " " coerce
8
+ // to 0 via Number()), hex/scientific encodings (0x10, 1e3), and signs/decimals.
9
+ if (!/^[0-9]+$/.test(value)) {
10
+ throw new InvalidArgumentError("Expected a non-negative integer.");
11
+ }
12
+ const n = Number(value);
13
+ // Number() can still produce a non-exact integer for very large inputs (beyond
14
+ // 2^53); reject those rather than silently using a different value.
15
+ if (!Number.isSafeInteger(n)) {
16
+ throw new InvalidArgumentError("Expected a non-negative integer.");
17
+ }
18
+ return n;
19
+ }
20
+ /**
21
+ * Validate a required positional argument: reject an empty/blank value rather
22
+ * than forwarding it into the URL path (which would produce a malformed request
23
+ * like `/stations//W/...`). Returns the trimmed value.
24
+ */
25
+ export function requireArg(name, value) {
26
+ if (value === undefined || value.trim() === "") {
27
+ throw new PegelError(`Missing required <${name}> argument.`);
28
+ }
29
+ // Reject "." / ".." which encodeURIComponent leaves untouched and which would
30
+ // otherwise inject a relative path segment into the request URL.
31
+ if (value === "." || value === "..") {
32
+ throw new PegelError(`Invalid <${name}> argument: "${value}".`);
33
+ }
34
+ return value;
35
+ }
36
+ /**
37
+ * Normalise an optional `[timeseries]` positional: an empty/blank value behaves
38
+ * like omitting it and defaults to "W" (water level), matching the documented
39
+ * default. (`??` alone would forward an empty string into the path.)
40
+ */
41
+ export function timeseriesOr(value, fallback = "W") {
42
+ return value && value.trim() !== "" ? value : fallback;
43
+ }
44
+ /** Translate resolved global CLI options into client EngineOptions. */
45
+ export function toEngineOptions(global) {
46
+ const options = {};
47
+ if (global.baseUrl !== undefined)
48
+ options.baseUrl = global.baseUrl;
49
+ if (global.timeout !== undefined)
50
+ options.timeoutMs = global.timeout;
51
+ if (global.userAgent !== undefined)
52
+ options.userAgent = global.userAgent;
53
+ if (global.maxRetries !== undefined)
54
+ options.maxRetries = global.maxRetries;
55
+ if (global.maxResponseBytes !== undefined)
56
+ options.maxResponseBytes = global.maxResponseBytes;
57
+ return options;
58
+ }
59
+ /** Render a JSON value to stdout, pretty by default, compact with --compact. */
60
+ export function renderJson(deps, global, value) {
61
+ const text = global.compact ? JSON.stringify(value) : JSON.stringify(value, null, 2);
62
+ deps.io.out(text);
63
+ }
64
+ /**
65
+ * Wrap an async command action with consistent global-option resolution and
66
+ * client construction. The callback receives a context (client + resolved global
67
+ * options + this command's options) and the command's positional arguments.
68
+ *
69
+ * Commander invokes actions as (arg1, ..., argN, options, command); we slice off
70
+ * the trailing options object and command instance to recover the positionals.
71
+ */
72
+ export function action(deps, fn) {
73
+ return async (...args) => {
74
+ const command = args[args.length - 1];
75
+ const positionals = args.slice(0, Math.max(0, args.length - 2));
76
+ const global = command.optsWithGlobals();
77
+ const client = deps.createClient(toEngineOptions(global));
78
+ await fn({ client, global, opts: command.opts() }, positionals);
79
+ };
80
+ }
81
+ //# sourceMappingURL=shared.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shared.js","sourceRoot":"","sources":["../../../src/cli/shared.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,28 @@
1
+ import { RequestEngine, type EngineOptions } from "./engine.js";
2
+ import type { Station, Water, TimeseriesInfo, CurrentMeasurement, Measurement, StationListParams, IncludeParams, MeasurementsParams } from "./types.js";
3
+ /** Stations: list with filters, or fetch one by uuid/number/shortname/longname. */
4
+ declare class StationsResource {
5
+ private readonly e;
6
+ constructor(e: RequestEngine);
7
+ list(params?: StationListParams): Promise<Station[]>;
8
+ get(station: string, params?: IncludeParams): Promise<Station>;
9
+ }
10
+ /** Timeseries: metadata, the current measurement, a window of measurements, gauge marks. */
11
+ declare class TimeseriesResource {
12
+ private readonly e;
13
+ constructor(e: RequestEngine);
14
+ /** Timeseries metadata (e.g. "W" = water level, "Q" = flow). */
15
+ get(station: string, timeseries?: string, params?: IncludeParams): Promise<TimeseriesInfo>;
16
+ currentMeasurement(station: string, timeseries?: string): Promise<CurrentMeasurement>;
17
+ measurements(station: string, timeseries?: string, params?: MeasurementsParams): Promise<Measurement[]>;
18
+ }
19
+ export declare class PegelOnlineClient {
20
+ private readonly engine;
21
+ readonly stations: StationsResource;
22
+ readonly timeseries: TimeseriesResource;
23
+ constructor(options?: EngineOptions);
24
+ /** List all bodies of water (Gewässer) covered by the service. */
25
+ waters(): Promise<Water[]>;
26
+ }
27
+ export {};
28
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../../src/client/client.ts"],"names":[],"mappings":"AAQA,OAAO,EAAE,aAAa,EAAE,KAAK,aAAa,EAAE,MAAM,aAAa,CAAC;AAEhE,OAAO,KAAK,EACV,OAAO,EACP,KAAK,EACL,cAAc,EACd,kBAAkB,EAClB,WAAW,EACX,iBAAiB,EACjB,aAAa,EACb,kBAAkB,EACnB,MAAM,YAAY,CAAC;AAsBpB,mFAAmF;AACnF,cAAM,gBAAgB;IACR,OAAO,CAAC,QAAQ,CAAC,CAAC;gBAAD,CAAC,EAAE,aAAa;IAE7C,IAAI,CAAC,MAAM,GAAE,iBAAsB,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC;IAYxD,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,GAAE,aAAkB,GAAG,OAAO,CAAC,OAAO,CAAC;CAGnE;AAED,4FAA4F;AAC5F,cAAM,kBAAkB;IACV,OAAO,CAAC,QAAQ,CAAC,CAAC;gBAAD,CAAC,EAAE,aAAa;IAE7C,gEAAgE;IAChE,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,SAAM,EAAE,MAAM,GAAE,aAAkB,GAAG,OAAO,CAAC,cAAc,CAAC;IAO3F,kBAAkB,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,SAAM,GAAG,OAAO,CAAC,kBAAkB,CAAC;IAMlF,YAAY,CACV,OAAO,EAAE,MAAM,EACf,UAAU,SAAM,EAChB,MAAM,GAAE,kBAAuB,GAC9B,OAAO,CAAC,WAAW,EAAE,CAAC;CAM1B;AAED,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAgB;IAEvC,QAAQ,CAAC,QAAQ,EAAE,gBAAgB,CAAC;IACpC,QAAQ,CAAC,UAAU,EAAE,kBAAkB,CAAC;gBAE5B,OAAO,GAAE,aAAkB;IAMvC,kEAAkE;IAClE,MAAM,IAAI,OAAO,CAAC,KAAK,EAAE,CAAC;CAG3B"}
@@ -0,0 +1,79 @@
1
+ // PegelOnlineClient — a typed client over the open (no-auth) PEGELONLINE REST
2
+ // API v2 (https://www.pegelonline.wsv.de/webservices/rest-api/v2).
3
+ //
4
+ // client.stations.list({ waters: "RHEIN" })
5
+ // client.stations.get("BONN", { includeCurrentMeasurement: true })
6
+ // client.timeseries.currentMeasurement("BONN", "W")
7
+ // client.timeseries.measurements("BONN", "W", { start: "P3D" })
8
+ import { RequestEngine } from "./engine.js";
9
+ const API = "/webservices/rest-api/v2";
10
+ const enc = encodeURIComponent;
11
+ /** Drop undefined values so only the parameters the caller set are sent. */
12
+ function prune(params) {
13
+ const out = {};
14
+ for (const [k, v] of Object.entries(params)) {
15
+ if (v !== undefined)
16
+ out[k] = v;
17
+ }
18
+ return out;
19
+ }
20
+ function includeQuery(p) {
21
+ return prune({
22
+ includeTimeseries: p.includeTimeseries,
23
+ includeCurrentMeasurement: p.includeCurrentMeasurement,
24
+ includeCharacteristicValues: p.includeCharacteristicValues,
25
+ });
26
+ }
27
+ /** Stations: list with filters, or fetch one by uuid/number/shortname/longname. */
28
+ class StationsResource {
29
+ e;
30
+ constructor(e) {
31
+ this.e = e;
32
+ }
33
+ list(params = {}) {
34
+ const query = prune({
35
+ ids: params.ids && params.ids.length > 0 ? params.ids.join(",") : undefined,
36
+ waters: params.waters,
37
+ fuzzyId: params.fuzzyId,
38
+ includeTimeseries: params.includeTimeseries,
39
+ includeCurrentMeasurement: params.includeCurrentMeasurement,
40
+ includeCharacteristicValues: params.includeCharacteristicValues,
41
+ });
42
+ return this.e.getJson(`${API}/stations.json`, query);
43
+ }
44
+ get(station, params = {}) {
45
+ return this.e.getJson(`${API}/stations/${enc(station)}.json`, includeQuery(params));
46
+ }
47
+ }
48
+ /** Timeseries: metadata, the current measurement, a window of measurements, gauge marks. */
49
+ class TimeseriesResource {
50
+ e;
51
+ constructor(e) {
52
+ this.e = e;
53
+ }
54
+ /** Timeseries metadata (e.g. "W" = water level, "Q" = flow). */
55
+ get(station, timeseries = "W", params = {}) {
56
+ return this.e.getJson(`${API}/stations/${enc(station)}/${enc(timeseries)}.json`, includeQuery(params));
57
+ }
58
+ currentMeasurement(station, timeseries = "W") {
59
+ return this.e.getJson(`${API}/stations/${enc(station)}/${enc(timeseries)}/currentmeasurement.json`);
60
+ }
61
+ measurements(station, timeseries = "W", params = {}) {
62
+ return this.e.getJson(`${API}/stations/${enc(station)}/${enc(timeseries)}/measurements.json`, prune({ start: params.start, end: params.end }));
63
+ }
64
+ }
65
+ export class PegelOnlineClient {
66
+ engine;
67
+ stations;
68
+ timeseries;
69
+ constructor(options = {}) {
70
+ this.engine = new RequestEngine(options);
71
+ this.stations = new StationsResource(this.engine);
72
+ this.timeseries = new TimeseriesResource(this.engine);
73
+ }
74
+ /** List all bodies of water (Gewässer) covered by the service. */
75
+ waters() {
76
+ return this.engine.getJson(`${API}/waters.json`);
77
+ }
78
+ }
79
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../../../src/client/client.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,54 @@
1
+ import { type Transport } from "./http.js";
2
+ import { type QueryParams } from "./query.js";
3
+ export declare const DEFAULT_BASE_URL = "https://www.pegelonline.wsv.de";
4
+ export interface RawResponse {
5
+ data: Buffer;
6
+ contentType: string;
7
+ status: number;
8
+ }
9
+ export interface EngineOptions {
10
+ /** Base URL of the API. Defaults to https://www.pegelonline.wsv.de */
11
+ baseUrl?: string;
12
+ /** Swappable transport. Defaults to the built-in node http/https transport. */
13
+ transport?: Transport;
14
+ /** Value of the User-Agent header. */
15
+ userAgent?: string;
16
+ /** Per-request timeout in milliseconds (0 disables). */
17
+ timeoutMs?: number;
18
+ /** Number of automatic retries for transient (429/503) responses. */
19
+ maxRetries?: number;
20
+ /** Base backoff between retries in milliseconds (grows linearly). */
21
+ retryDelayMs?: number;
22
+ /** Number of HTTP redirects (301/302/303/307/308) to follow. Defaults to 5. */
23
+ maxRedirects?: number;
24
+ /**
25
+ * Hard cap on response body size in bytes (defends against memory exhaustion
26
+ * from a hostile/buggy endpoint). Defaults to 100 MiB; set to 0 for no limit.
27
+ */
28
+ maxResponseBytes?: number;
29
+ /** Injectable sleep, primarily for deterministic tests. */
30
+ sleep?: (ms: number) => Promise<void>;
31
+ }
32
+ export declare class RequestEngine {
33
+ private readonly baseUrl;
34
+ private readonly transport;
35
+ private readonly userAgent;
36
+ private readonly timeoutMs;
37
+ private readonly maxRetries;
38
+ private readonly retryDelayMs;
39
+ private readonly maxRedirects;
40
+ private readonly maxResponseBytes;
41
+ private readonly sleep;
42
+ constructor(options?: EngineOptions);
43
+ /** Build a fully-qualified URL from a path and optional query parameters. */
44
+ buildUrl(path: string, query?: QueryParams): string;
45
+ /** Perform a request with Accept negotiation and transient-error retries. */
46
+ request(method: string, path: string, options?: {
47
+ query?: QueryParams;
48
+ accept: string;
49
+ }): Promise<RawResponse>;
50
+ /** Perform a GET expecting JSON and parse it into `T`. */
51
+ getJson<T>(path: string, query?: QueryParams): Promise<T>;
52
+ private toApiError;
53
+ }
54
+ //# sourceMappingURL=engine.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../../../src/client/engine.ts"],"names":[],"mappings":"AAIA,OAAO,EAAqB,KAAK,SAAS,EAAE,MAAM,WAAW,CAAC;AAC9D,OAAO,EAAoB,KAAK,WAAW,EAAE,MAAM,YAAY,CAAC;AAGhE,eAAO,MAAM,gBAAgB,mCAAmC,CAAC;AAGjE,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,aAAa;IAC5B,sEAAsE;IACtE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,+EAA+E;IAC/E,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB,sCAAsC;IACtC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,wDAAwD;IACxD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,qEAAqE;IACrE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,qEAAqE;IACrE,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,+EAA+E;IAC/E,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;OAGG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,2DAA2D;IAC3D,KAAK,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACvC;AAOD,qBAAa,aAAa;IACxB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAY;IACtC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;IACtC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;IACtC,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAS;IAC1C,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAgC;gBAE1C,OAAO,GAAE,aAAkB;IAkBvC,6EAA6E;IAC7E,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,WAAW,GAAG,MAAM;IAMnD,6EAA6E;IACvE,OAAO,CACX,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EACZ,OAAO,GAAE;QAAE,KAAK,CAAC,EAAE,WAAW,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAmC,GAChF,OAAO,CAAC,WAAW,CAAC;IAsDvB,0DAA0D;IACpD,OAAO,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,CAAC,CAAC;IAU/D,OAAO,CAAC,UAAU;CAYnB"}
@@ -0,0 +1,120 @@
1
+ // The request engine: turns logical (method, path, query) calls into HTTP
2
+ // requests via a Transport, applies retry/backoff for transient statuses
3
+ // (429, 503), and decodes responses.
4
+ import { nodeHttpTransport } from "./http.js";
5
+ import { buildQueryString } from "./query.js";
6
+ import { PegelApiError, PegelError, PegelParseError } from "./errors.js";
7
+ export const DEFAULT_BASE_URL = "https://www.pegelonline.wsv.de";
8
+ const DEFAULT_USER_AGENT = "pegel-online-cli";
9
+ const DEFAULT_MAX_RESPONSE_BYTES = 100 * 1024 * 1024;
10
+ const realSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
11
+ export class RequestEngine {
12
+ baseUrl;
13
+ transport;
14
+ userAgent;
15
+ timeoutMs;
16
+ maxRetries;
17
+ retryDelayMs;
18
+ maxRedirects;
19
+ maxResponseBytes;
20
+ sleep;
21
+ constructor(options = {}) {
22
+ this.baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
23
+ this.transport = options.transport ?? nodeHttpTransport;
24
+ this.userAgent = options.userAgent ?? DEFAULT_USER_AGENT;
25
+ // Reject control characters (CR/LF in particular) up front with a typed error
26
+ // instead of letting Node throw a raw TypeError during header validation,
27
+ // which would surface as an "Unexpected error". Also closes header-injection.
28
+ if (/[\x00-\x1f\x7f]/.test(this.userAgent)) {
29
+ throw new PegelError("Invalid User-Agent: control characters are not allowed.");
30
+ }
31
+ this.timeoutMs = options.timeoutMs ?? 30_000;
32
+ this.maxRetries = options.maxRetries ?? 2;
33
+ this.retryDelayMs = options.retryDelayMs ?? 200;
34
+ this.maxRedirects = options.maxRedirects ?? 5;
35
+ this.maxResponseBytes = options.maxResponseBytes ?? DEFAULT_MAX_RESPONSE_BYTES;
36
+ this.sleep = options.sleep ?? realSleep;
37
+ }
38
+ /** Build a fully-qualified URL from a path and optional query parameters. */
39
+ buildUrl(path, query) {
40
+ const normalizedPath = path.startsWith("/") ? path : `/${path}`;
41
+ const qs = query ? buildQueryString(query) : "";
42
+ return `${this.baseUrl}${normalizedPath}${qs ? `?${qs}` : ""}`;
43
+ }
44
+ /** Perform a request with Accept negotiation and transient-error retries. */
45
+ async request(method, path, options = { accept: "application/json" }) {
46
+ let url = this.buildUrl(path, options.query);
47
+ const headers = {
48
+ Accept: options.accept,
49
+ "User-Agent": this.userAgent,
50
+ };
51
+ let attempt = 0;
52
+ let redirects = 0;
53
+ // attempts = initial try + maxRetries (redirects are counted separately)
54
+ for (;;) {
55
+ const response = await this.transport({
56
+ method,
57
+ url,
58
+ headers,
59
+ timeoutMs: this.timeoutMs,
60
+ ...(this.maxResponseBytes > 0 ? { maxResponseBytes: this.maxResponseBytes } : {}),
61
+ });
62
+ const status = response.status;
63
+ const retryable = status === 429 || status === 503;
64
+ if (retryable && attempt < this.maxRetries) {
65
+ attempt += 1;
66
+ await this.sleep(this.retryDelayMs * attempt);
67
+ continue;
68
+ }
69
+ // Follow redirects, resolving the Location relative to the current URL.
70
+ if (status >= 300 && status < 400 && redirects < this.maxRedirects) {
71
+ const location = response.headers["location"];
72
+ if (typeof location === "string" && location.length > 0) {
73
+ const next = new URL(location, url);
74
+ // Security: never carry credential-bearing headers across origins. The
75
+ // CLI sends none today, but this guards a future Authorization/Cookie
76
+ // header from leaking to an attacker-controlled redirect target.
77
+ if (next.origin !== new URL(url).origin) {
78
+ delete headers["Authorization"];
79
+ delete headers["Cookie"];
80
+ }
81
+ url = next.toString();
82
+ redirects += 1;
83
+ continue;
84
+ }
85
+ }
86
+ const contentType = String(response.headers["content-type"] ?? "");
87
+ if (status < 200 || status >= 300) {
88
+ throw this.toApiError(method, url, status, response.body);
89
+ }
90
+ return { data: response.body, contentType, status };
91
+ }
92
+ }
93
+ /** Perform a GET expecting JSON and parse it into `T`. */
94
+ async getJson(path, query) {
95
+ const res = await this.request("GET", path, { query, accept: "application/json" });
96
+ const text = res.data.toString("utf8");
97
+ try {
98
+ return JSON.parse(text);
99
+ }
100
+ catch (cause) {
101
+ throw new PegelParseError(`Failed to parse JSON response from ${path}`, { cause });
102
+ }
103
+ }
104
+ toApiError(method, url, status, body) {
105
+ const text = body.toString("utf8");
106
+ let detail;
107
+ try {
108
+ const parsed = JSON.parse(text);
109
+ if (parsed && typeof parsed.detail === "string")
110
+ detail = parsed.detail;
111
+ else if (parsed && typeof parsed.message === "string")
112
+ detail = parsed.message;
113
+ }
114
+ catch {
115
+ // Non-JSON error body; leave detail undefined.
116
+ }
117
+ return new PegelApiError({ status, url, method, body: text, detail });
118
+ }
119
+ }
120
+ //# sourceMappingURL=engine.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"engine.js","sourceRoot":"","sources":["../../../src/client/engine.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,33 @@
1
+ /** Base class for every error originating from this client. */
2
+ export declare class PegelError extends Error {
3
+ constructor(message: string, options?: {
4
+ cause?: unknown;
5
+ });
6
+ }
7
+ /**
8
+ * The API responded with a non-2xx status code. `detail` holds a human-readable
9
+ * message extracted from the response body when one is present.
10
+ */
11
+ export declare class PegelApiError extends PegelError {
12
+ readonly status: number;
13
+ readonly detail: string | undefined;
14
+ readonly url: string;
15
+ readonly method: string;
16
+ readonly body: string;
17
+ constructor(args: {
18
+ status: number;
19
+ url: string;
20
+ method: string;
21
+ body: string;
22
+ detail?: string;
23
+ });
24
+ /** True for statuses the API documents as transient and retry-able. */
25
+ get isRetryable(): boolean;
26
+ }
27
+ /** A transport-level failure (DNS, connection reset, timeout, ...). */
28
+ export declare class PegelNetworkError extends PegelError {
29
+ }
30
+ /** The response body could not be parsed as the expected JSON shape. */
31
+ export declare class PegelParseError extends PegelError {
32
+ }
33
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../../src/client/errors.ts"],"names":[],"mappings":"AAGA,+DAA+D;AAC/D,qBAAa,UAAW,SAAQ,KAAK;gBACvB,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE;CAI3D;AAED;;;GAGG;AACH,qBAAa,aAAc,SAAQ,UAAU;IAC3C,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;IACpC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;gBAEV,IAAI,EAAE;QAChB,MAAM,EAAE,MAAM,CAAC;QACf,GAAG,EAAE,MAAM,CAAC;QACZ,MAAM,EAAE,MAAM,CAAC;QACf,IAAI,EAAE,MAAM,CAAC;QACb,MAAM,CAAC,EAAE,MAAM,CAAC;KACjB;IAUD,uEAAuE;IACvE,IAAI,WAAW,IAAI,OAAO,CAEzB;CACF;AAED,uEAAuE;AACvE,qBAAa,iBAAkB,SAAQ,UAAU;CAAG;AAEpD,wEAAwE;AACxE,qBAAa,eAAgB,SAAQ,UAAU;CAAG"}
@@ -0,0 +1,40 @@
1
+ // Error types raised by the client. Kept free of any I/O so they are trivial to
2
+ // construct in tests and to `instanceof`-check by consumers.
3
+ /** Base class for every error originating from this client. */
4
+ export class PegelError extends Error {
5
+ constructor(message, options) {
6
+ super(message, options);
7
+ this.name = new.target.name;
8
+ }
9
+ }
10
+ /**
11
+ * The API responded with a non-2xx status code. `detail` holds a human-readable
12
+ * message extracted from the response body when one is present.
13
+ */
14
+ export class PegelApiError extends PegelError {
15
+ status;
16
+ detail;
17
+ url;
18
+ method;
19
+ body;
20
+ constructor(args) {
21
+ const detailPart = args.detail ? `: ${args.detail}` : "";
22
+ super(`HTTP ${args.status} for ${args.method} ${args.url}${detailPart}`);
23
+ this.status = args.status;
24
+ this.url = args.url;
25
+ this.method = args.method;
26
+ this.body = args.body;
27
+ this.detail = args.detail;
28
+ }
29
+ /** True for statuses the API documents as transient and retry-able. */
30
+ get isRetryable() {
31
+ return this.status === 429 || this.status === 503;
32
+ }
33
+ }
34
+ /** A transport-level failure (DNS, connection reset, timeout, ...). */
35
+ export class PegelNetworkError extends PegelError {
36
+ }
37
+ /** The response body could not be parsed as the expected JSON shape. */
38
+ export class PegelParseError extends PegelError {
39
+ }
40
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../../../src/client/errors.ts"],"names":[],"mappings":"AAAA,gFAAgF;AAChF,6DAA6D;AAE7D,+DAA+D;AAC/D,MAAM,OAAO,UAAW,SAAQ,KAAK;IACnC,YAAY,OAAe,EAAE,OAA6B;QACxD,KAAK,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;QACxB,IAAI,CAAC,IAAI,GAAG,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC;IAC9B,CAAC;CACF;AAED;;;GAGG;AACH,MAAM,OAAO,aAAc,SAAQ,UAAU;IAClC,MAAM,CAAS;IACf,MAAM,CAAqB;IAC3B,GAAG,CAAS;IACZ,MAAM,CAAS;IACf,IAAI,CAAS;IAEtB,YAAY,IAMX;QACC,MAAM,UAAU,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACzD,KAAK,CAAC,QAAQ,IAAI,CAAC,MAAM,QAAQ,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC,GAAG,GAAG,UAAU,EAAE,CAAC,CAAC;QACzE,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC;QAC1B,IAAI,CAAC,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC;QACpB,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC;QAC1B,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC;QACtB,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC;IAC5B,CAAC;IAED,uEAAuE;IACvE,IAAI,WAAW;QACb,OAAO,IAAI,CAAC,MAAM,KAAK,GAAG,IAAI,IAAI,CAAC,MAAM,KAAK,GAAG,CAAC;IACpD,CAAC;CACF;AAED,uEAAuE;AACvE,MAAM,OAAO,iBAAkB,SAAQ,UAAU;CAAG;AAEpD,wEAAwE;AACxE,MAAM,OAAO,eAAgB,SAAQ,UAAU;CAAG"}
@@ -0,0 +1,26 @@
1
+ import http from "node:http";
2
+ export interface HttpRequest {
3
+ method: string;
4
+ /** Fully-qualified absolute URL. */
5
+ url: string;
6
+ headers?: Record<string, string>;
7
+ /** Optional request body (already serialised). */
8
+ body?: string | Buffer;
9
+ /** Per-request timeout in milliseconds. */
10
+ timeoutMs?: number;
11
+ /** Hard cap on the response body size in bytes; the request aborts if exceeded. */
12
+ maxResponseBytes?: number;
13
+ }
14
+ export interface HttpResponse {
15
+ status: number;
16
+ headers: http.IncomingHttpHeaders;
17
+ body: Buffer;
18
+ }
19
+ export type Transport = (request: HttpRequest) => Promise<HttpResponse>;
20
+ /**
21
+ * Default transport. Resolves with the raw response (including non-2xx) — status
22
+ * interpretation is the client's job. Rejects only on transport-level failures
23
+ * (connection errors, timeouts, malformed URLs).
24
+ */
25
+ export declare const nodeHttpTransport: Transport;
26
+ //# sourceMappingURL=http.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../../../src/client/http.ts"],"names":[],"mappings":"AAQA,OAAO,IAAI,MAAM,WAAW,CAAC;AAI7B,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,oCAAoC;IACpC,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,kDAAkD;IAClD,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACvB,2CAA2C;IAC3C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,mFAAmF;IACnF,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,IAAI,CAAC,mBAAmB,CAAC;IAClC,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,MAAM,SAAS,GAAG,CAAC,OAAO,EAAE,WAAW,KAAK,OAAO,CAAC,YAAY,CAAC,CAAC;AAExE;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,EAAE,SAwE5B,CAAC"}
@@ -0,0 +1,82 @@
1
+ // HTTP transport built on Node's built-in `http`/`https` modules — no axios,
2
+ // no fetch polyfill, no third-party HTTP client.
3
+ //
4
+ // The transport is a plain function so it can be trivially swapped out in tests
5
+ // (inject a `mock.fn()` returning a canned HttpResponse) without touching the
6
+ // network. The default implementation below is exercised against a real local
7
+ // `http.createServer` in the test-suite.
8
+ import http from "node:http";
9
+ import https from "node:https";
10
+ import { PegelNetworkError } from "./errors.js";
11
+ /**
12
+ * Default transport. Resolves with the raw response (including non-2xx) — status
13
+ * interpretation is the client's job. Rejects only on transport-level failures
14
+ * (connection errors, timeouts, malformed URLs).
15
+ */
16
+ export const nodeHttpTransport = (request) => new Promise((resolve, reject) => {
17
+ let url;
18
+ try {
19
+ url = new URL(request.url);
20
+ }
21
+ catch {
22
+ reject(new PegelNetworkError(`Invalid URL: ${request.url}`));
23
+ return;
24
+ }
25
+ // Only http/https are supported. Reject anything else up front with a clear,
26
+ // typed error instead of letting Node throw an opaque ERR_INVALID_PROTOCOL
27
+ // (and so this never reaches the file:/ftp:/etc. drivers).
28
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
29
+ reject(new PegelNetworkError(`Unsupported protocol "${url.protocol}" in URL: ${request.url}`));
30
+ return;
31
+ }
32
+ const isHttps = url.protocol === "https:";
33
+ const driver = isHttps ? https : http;
34
+ const maxBytes = request.maxResponseBytes;
35
+ const req = driver.request(url, {
36
+ method: request.method,
37
+ headers: request.headers,
38
+ }, (res) => {
39
+ const chunks = [];
40
+ let received = 0;
41
+ let aborted = false;
42
+ res.on("data", (chunk) => {
43
+ if (aborted)
44
+ return;
45
+ received += chunk.length;
46
+ if (maxBytes !== undefined && received > maxBytes) {
47
+ aborted = true;
48
+ res.destroy();
49
+ reject(new PegelNetworkError(`Response exceeded maxResponseBytes (${maxBytes})`));
50
+ return;
51
+ }
52
+ chunks.push(chunk);
53
+ });
54
+ res.on("end", () => {
55
+ if (aborted)
56
+ return;
57
+ resolve({
58
+ status: res.statusCode ?? 0,
59
+ headers: res.headers,
60
+ body: Buffer.concat(chunks),
61
+ });
62
+ });
63
+ res.on("error", (err) => {
64
+ if (aborted)
65
+ return; // we already rejected with the size-cap error
66
+ reject(new PegelNetworkError(`Response stream error: ${err.message}`, { cause: err }));
67
+ });
68
+ });
69
+ if (request.timeoutMs && request.timeoutMs > 0) {
70
+ req.setTimeout(request.timeoutMs, () => {
71
+ req.destroy(new PegelNetworkError(`Request timed out after ${request.timeoutMs}ms`));
72
+ });
73
+ }
74
+ req.on("error", (err) => {
75
+ // A timeout destroy already passes an PegelNetworkError; don't double-wrap.
76
+ reject(err instanceof PegelNetworkError ? err : new PegelNetworkError(err.message, { cause: err }));
77
+ });
78
+ if (request.body !== undefined)
79
+ req.write(request.body);
80
+ req.end();
81
+ });
82
+ //# sourceMappingURL=http.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http.js","sourceRoot":"","sources":["../../../src/client/http.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ export { PegelOnlineClient } from "./client.js";
2
+ export { RequestEngine, DEFAULT_BASE_URL } from "./engine.js";
3
+ export type { EngineOptions, RawResponse } from "./engine.js";
4
+ export { nodeHttpTransport } from "./http.js";
5
+ export type { Transport, HttpRequest, HttpResponse } from "./http.js";
6
+ export { buildQueryString } from "./query.js";
7
+ export type { QueryParams, QueryValue } from "./query.js";
8
+ export { PegelError, PegelApiError, PegelNetworkError, PegelParseError } from "./errors.js";
9
+ export * from "./types.js";
10
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/client/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAC9D,YAAY,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC9D,OAAO,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAC;AAC9C,YAAY,EAAE,SAAS,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AACtE,OAAO,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAC9C,YAAY,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAC1D,OAAO,EAAE,UAAU,EAAE,aAAa,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAE5F,cAAc,YAAY,CAAC"}
@@ -0,0 +1,8 @@
1
+ // Public entry point for the API client library.
2
+ export { PegelOnlineClient } from "./client.js";
3
+ export { RequestEngine, DEFAULT_BASE_URL } from "./engine.js";
4
+ export { nodeHttpTransport } from "./http.js";
5
+ export { buildQueryString } from "./query.js";
6
+ export { PegelError, PegelApiError, PegelNetworkError, PegelParseError } from "./errors.js";
7
+ export * from "./types.js";
8
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/client/index.ts"],"names":[],"mappings":"AAAA,iDAAiD;AAEjD,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAE9D,OAAO,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAC;AAE9C,OAAO,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAE9C,OAAO,EAAE,UAAU,EAAE,aAAa,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAE5F,cAAc,YAAY,CAAC"}