@maschinenlesbar.org/bundeshaushalt-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/budget.d.ts +4 -0
  6. package/dist/src/cli/commands/budget.d.ts.map +1 -0
  7. package/dist/src/cli/commands/budget.js +86 -0
  8. package/dist/src/cli/commands/budget.js.map +1 -0
  9. package/dist/src/cli/index.d.ts +3 -0
  10. package/dist/src/cli/index.d.ts.map +1 -0
  11. package/dist/src/cli/index.js +6 -0
  12. package/dist/src/cli/index.js.map +1 -0
  13. package/dist/src/cli/io.d.ts +13 -0
  14. package/dist/src/cli/io.d.ts.map +1 -0
  15. package/dist/src/cli/io.js +7 -0
  16. package/dist/src/cli/io.js.map +1 -0
  17. package/dist/src/cli/program.d.ts +7 -0
  18. package/dist/src/cli/program.d.ts.map +1 -0
  19. package/dist/src/cli/program.js +54 -0
  20. package/dist/src/cli/program.js.map +1 -0
  21. package/dist/src/cli/run.d.ts +3 -0
  22. package/dist/src/cli/run.d.ts.map +1 -0
  23. package/dist/src/cli/run.js +48 -0
  24. package/dist/src/cli/run.js.map +1 -0
  25. package/dist/src/cli/shared.d.ts +51 -0
  26. package/dist/src/cli/shared.d.ts.map +1 -0
  27. package/dist/src/cli/shared.js +81 -0
  28. package/dist/src/cli/shared.js.map +1 -0
  29. package/dist/src/client/client.d.ts +9 -0
  30. package/dist/src/client/client.d.ts.map +1 -0
  31. package/dist/src/client/client.js +32 -0
  32. package/dist/src/client/client.js.map +1 -0
  33. package/dist/src/client/engine.d.ts +56 -0
  34. package/dist/src/client/engine.d.ts.map +1 -0
  35. package/dist/src/client/engine.js +159 -0
  36. package/dist/src/client/engine.js.map +1 -0
  37. package/dist/src/client/enums.d.ts +17 -0
  38. package/dist/src/client/enums.d.ts.map +1 -0
  39. package/dist/src/client/enums.js +16 -0
  40. package/dist/src/client/enums.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 +89 -0
  48. package/dist/src/client/http.js.map +1 -0
  49. package/dist/src/client/index.d.ts +11 -0
  50. package/dist/src/client/index.d.ts.map +1 -0
  51. package/dist/src/client/index.js +9 -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 +66 -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 { HaushaltError } from "../client/errors.js";
5
+ /**
6
+ * commander value-parser: a non-negative integer in plain decimal notation.
7
+ *
8
+ * Deliberately strict: only `/^\d+$/` is accepted. `Number()` would otherwise
9
+ * coerce empty strings (→0, which silently disables size/retry caps), hex/octal/
10
+ * binary (`0x10`→16), scientific (`1e9`), a leading `+`, and surrounding
11
+ * whitespace — none of which a user typing a "non-negative integer" intends.
12
+ */
13
+ export function parseIntArg(value) {
14
+ if (!/^\d+$/.test(value)) {
15
+ throw new InvalidArgumentError("Expected a non-negative integer.");
16
+ }
17
+ return Number(value);
18
+ }
19
+ /**
20
+ * commander value-parser: a non-negative integer with an inclusive upper bound.
21
+ * Used for options like `--max-retries` where an absurdly large value would turn
22
+ * a transient-error retry loop into an effective hang (DoS).
23
+ */
24
+ export function parseBoundedIntArg(max) {
25
+ return (value) => {
26
+ const n = parseIntArg(value);
27
+ if (n > max) {
28
+ throw new InvalidArgumentError(`Expected a non-negative integer <= ${max}.`);
29
+ }
30
+ return n;
31
+ };
32
+ }
33
+ /**
34
+ * Validate a positional argument against an allowed set (commander does not
35
+ * support .choices() on positional args). Throws a HaushaltError so run() prints a
36
+ * clear message and exits 1.
37
+ */
38
+ export function assertEnum(value, allowed, argName) {
39
+ if (!allowed.includes(value)) {
40
+ throw new HaushaltError(`Invalid ${argName} "${value}". Expected one of: ${allowed.join(", ")}.`);
41
+ }
42
+ return value;
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,9 @@
1
+ import { type EngineOptions } from "./engine.js";
2
+ import type { BudgetData, BudgetParams } from "./types.js";
3
+ export declare class BundeshaushaltClient {
4
+ private readonly engine;
5
+ constructor(options?: EngineOptions);
6
+ /** Budget data for a year + account, optionally scoped by quota/unit/id. */
7
+ budgetData(params: BudgetParams): Promise<BudgetData>;
8
+ }
9
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../../src/client/client.ts"],"names":[],"mappings":"AAMA,OAAO,EAAiB,KAAK,aAAa,EAAE,MAAM,aAAa,CAAC;AAEhE,OAAO,KAAK,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAQ3D,qBAAa,oBAAoB;IAC/B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAgB;gBAE3B,OAAO,GAAE,aAAkB;IAIvC,4EAA4E;IAC5E,UAAU,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,UAAU,CAAC;CAUtD"}
@@ -0,0 +1,32 @@
1
+ // BundeshaushaltClient — a typed client over the open (no-auth) budget-data
2
+ // endpoint of the German federal budget portal (https://bundeshaushalt.de).
3
+ //
4
+ // client.budgetData({ year: 2024, account: "expenses" })
5
+ // client.budgetData({ year: 2024, account: "expenses", id: "G-", unit: "group" })
6
+ import { RequestEngine } from "./engine.js";
7
+ // NOTE: This is an undocumented, internal endpoint of bundeshaushalt.de (note the
8
+ // "internalapi" path segment). It is not a published, stable public API and may
9
+ // change shape, rate-limit, or disappear without notice. It is the only route that
10
+ // serves this data today; isolate any change here if a public endpoint appears.
11
+ const PATH = "/internalapi/budgetData";
12
+ export class BundeshaushaltClient {
13
+ engine;
14
+ constructor(options = {}) {
15
+ this.engine = new RequestEngine(options);
16
+ }
17
+ /** Budget data for a year + account, optionally scoped by quota/unit/id. */
18
+ budgetData(params) {
19
+ const query = {
20
+ year: params.year,
21
+ account: params.account,
22
+ };
23
+ if (params.quota !== undefined)
24
+ query["quota"] = params.quota;
25
+ if (params.unit !== undefined)
26
+ query["unit"] = params.unit;
27
+ if (params.id !== undefined)
28
+ query["id"] = params.id;
29
+ return this.engine.getJson(PATH, query);
30
+ }
31
+ }
32
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../../../src/client/client.ts"],"names":[],"mappings":"AAAA,4EAA4E;AAC5E,4EAA4E;AAC5E,EAAE;AACF,2DAA2D;AAC3D,qFAAqF;AAErF,OAAO,EAAE,aAAa,EAAsB,MAAM,aAAa,CAAC;AAIhE,kFAAkF;AAClF,gFAAgF;AAChF,mFAAmF;AACnF,gFAAgF;AAChF,MAAM,IAAI,GAAG,yBAAyB,CAAC;AAEvC,MAAM,OAAO,oBAAoB;IACd,MAAM,CAAgB;IAEvC,YAAY,UAAyB,EAAE;QACrC,IAAI,CAAC,MAAM,GAAG,IAAI,aAAa,CAAC,OAAO,CAAC,CAAC;IAC3C,CAAC;IAED,4EAA4E;IAC5E,UAAU,CAAC,MAAoB;QAC7B,MAAM,KAAK,GAAgB;YACzB,IAAI,EAAE,MAAM,CAAC,IAAI;YACjB,OAAO,EAAE,MAAM,CAAC,OAAO;SACxB,CAAC;QACF,IAAI,MAAM,CAAC,KAAK,KAAK,SAAS;YAAE,KAAK,CAAC,OAAO,CAAC,GAAG,MAAM,CAAC,KAAK,CAAC;QAC9D,IAAI,MAAM,CAAC,IAAI,KAAK,SAAS;YAAE,KAAK,CAAC,MAAM,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC;QAC3D,IAAI,MAAM,CAAC,EAAE,KAAK,SAAS;YAAE,KAAK,CAAC,IAAI,CAAC,GAAG,MAAM,CAAC,EAAE,CAAC;QACrD,OAAO,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;IAC1C,CAAC;CACF"}
@@ -0,0 +1,56 @@
1
+ import { type Transport } from "./http.js";
2
+ import { type QueryParams } from "./query.js";
3
+ export declare const DEFAULT_BASE_URL = "https://bundeshaushalt.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://bundeshaushalt.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
+ /** Drop sensitive headers when a redirect points at a different origin. */
33
+ export declare function stripCrossOriginCredentials(headers: Record<string, string>, fromUrl: string, toUrl: string): Record<string, string>;
34
+ export declare class RequestEngine {
35
+ private readonly baseUrl;
36
+ private readonly transport;
37
+ private readonly userAgent;
38
+ private readonly timeoutMs;
39
+ private readonly maxRetries;
40
+ private readonly retryDelayMs;
41
+ private readonly maxRedirects;
42
+ private readonly maxResponseBytes;
43
+ private readonly sleep;
44
+ constructor(options?: EngineOptions);
45
+ /** Build a fully-qualified URL from a path and optional query parameters. */
46
+ buildUrl(path: string, query?: QueryParams): string;
47
+ /** Perform a request with Accept negotiation and transient-error retries. */
48
+ request(method: string, path: string, options?: {
49
+ query?: QueryParams;
50
+ accept: string;
51
+ }): Promise<RawResponse>;
52
+ /** Perform a GET expecting JSON and parse it into `T`. */
53
+ getJson<T>(path: string, query?: QueryParams): Promise<T>;
54
+ private toApiError;
55
+ }
56
+ //# 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,8BAA8B,CAAC;AAG5D,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,aAAa;IAC5B,iEAAiE;IACjE,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;AAWD,2EAA2E;AAC3E,wBAAgB,2BAA2B,CACzC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC/B,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,MAAM,GACZ,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAOxB;AA6BD,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;IAgBvC,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;IAuDvB,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,159 @@
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 { HaushaltApiError, HaushaltError, HaushaltNetworkError, HaushaltParseError } from "./errors.js";
7
+ export const DEFAULT_BASE_URL = "https://bundeshaushalt.de";
8
+ const DEFAULT_USER_AGENT = "bundeshaushalt-cli";
9
+ const DEFAULT_MAX_RESPONSE_BYTES = 100 * 1024 * 1024;
10
+ /**
11
+ * Headers that must never be replayed to a different origin on a redirect.
12
+ * Matched case-insensitively. This mirrors the credential-stripping behaviour of
13
+ * browsers / curl: a cross-origin `Location` must not leak auth material.
14
+ */
15
+ const SENSITIVE_HEADERS = new Set(["authorization", "x-api-key", "cookie"]);
16
+ /** Drop sensitive headers when a redirect points at a different origin. */
17
+ export function stripCrossOriginCredentials(headers, fromUrl, toUrl) {
18
+ if (new URL(fromUrl).origin === new URL(toUrl).origin)
19
+ return headers;
20
+ const safe = {};
21
+ for (const [name, value] of Object.entries(headers)) {
22
+ if (!SENSITIVE_HEADERS.has(name.toLowerCase()))
23
+ safe[name] = value;
24
+ }
25
+ return safe;
26
+ }
27
+ const realSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
28
+ /**
29
+ * Normalise a caller-supplied User-Agent into a safe header value.
30
+ *
31
+ * - An empty / whitespace-only value falls back to the default rather than
32
+ * sending an empty `User-Agent` (which would suppress the default entirely).
33
+ * - A value containing control characters (CR/LF/NUL etc.) is rejected with a
34
+ * typed error here, instead of letting node:http throw a raw TypeError deep in
35
+ * the request that surfaces as an ungraceful "Unexpected error".
36
+ */
37
+ function normalizeUserAgent(value) {
38
+ if (value === undefined)
39
+ return DEFAULT_USER_AGENT;
40
+ const trimmed = value.trim();
41
+ if (trimmed.length === 0)
42
+ return DEFAULT_USER_AGENT;
43
+ for (let i = 0; i < trimmed.length; i += 1) {
44
+ const code = trimmed.charCodeAt(i);
45
+ // Reject C0 controls (incl. CR/LF/NUL) and DEL before they reach node:http,
46
+ // which would otherwise throw an opaque TypeError ("Invalid character...").
47
+ if (code < 0x20 || code === 0x7f) {
48
+ throw new HaushaltError("Invalid User-Agent: control characters are not allowed.");
49
+ }
50
+ }
51
+ return trimmed;
52
+ }
53
+ export class RequestEngine {
54
+ baseUrl;
55
+ transport;
56
+ userAgent;
57
+ timeoutMs;
58
+ maxRetries;
59
+ retryDelayMs;
60
+ maxRedirects;
61
+ maxResponseBytes;
62
+ sleep;
63
+ constructor(options = {}) {
64
+ // An empty / whitespace-only baseUrl falls back to the default rather than
65
+ // collapsing (after trailing-slash stripping) to "" and building a relative
66
+ // URL that `new URL()` rejects with a confusing "Invalid URL".
67
+ const baseUrl = options.baseUrl?.trim() ? options.baseUrl : DEFAULT_BASE_URL;
68
+ this.baseUrl = baseUrl.replace(/\/+$/, "");
69
+ this.transport = options.transport ?? nodeHttpTransport;
70
+ this.userAgent = normalizeUserAgent(options.userAgent);
71
+ this.timeoutMs = options.timeoutMs ?? 30_000;
72
+ this.maxRetries = options.maxRetries ?? 2;
73
+ this.retryDelayMs = options.retryDelayMs ?? 200;
74
+ this.maxRedirects = options.maxRedirects ?? 5;
75
+ this.maxResponseBytes = options.maxResponseBytes ?? DEFAULT_MAX_RESPONSE_BYTES;
76
+ this.sleep = options.sleep ?? realSleep;
77
+ }
78
+ /** Build a fully-qualified URL from a path and optional query parameters. */
79
+ buildUrl(path, query) {
80
+ const normalizedPath = path.startsWith("/") ? path : `/${path}`;
81
+ const qs = query ? buildQueryString(query) : "";
82
+ return `${this.baseUrl}${normalizedPath}${qs ? `?${qs}` : ""}`;
83
+ }
84
+ /** Perform a request with Accept negotiation and transient-error retries. */
85
+ async request(method, path, options = { accept: "application/json" }) {
86
+ let url = this.buildUrl(path, options.query);
87
+ let headers = {
88
+ Accept: options.accept,
89
+ "User-Agent": this.userAgent,
90
+ };
91
+ let attempt = 0;
92
+ let redirects = 0;
93
+ // attempts = initial try + maxRetries (redirects are counted separately)
94
+ for (;;) {
95
+ const response = await this.transport({
96
+ method,
97
+ url,
98
+ headers,
99
+ timeoutMs: this.timeoutMs,
100
+ ...(this.maxResponseBytes > 0 ? { maxResponseBytes: this.maxResponseBytes } : {}),
101
+ });
102
+ const status = response.status;
103
+ const retryable = status === 429 || status === 503;
104
+ if (retryable && attempt < this.maxRetries) {
105
+ attempt += 1;
106
+ await this.sleep(this.retryDelayMs * attempt);
107
+ continue;
108
+ }
109
+ // Follow redirects, resolving the Location relative to the current URL.
110
+ if (status >= 300 && status < 400 && redirects < this.maxRedirects) {
111
+ const location = response.headers["location"];
112
+ if (typeof location === "string" && location.length > 0) {
113
+ const nextUrl = new URL(location, url);
114
+ // Refuse a downgrade from https to plaintext http on redirect.
115
+ if (new URL(url).protocol === "https:" && nextUrl.protocol === "http:") {
116
+ throw new HaushaltNetworkError(`Refusing to follow https->http redirect to ${nextUrl.toString()}`);
117
+ }
118
+ // Strip credential headers if the redirect crosses origins.
119
+ headers = stripCrossOriginCredentials(headers, url, nextUrl.toString());
120
+ url = nextUrl.toString();
121
+ redirects += 1;
122
+ continue;
123
+ }
124
+ }
125
+ const contentType = String(response.headers["content-type"] ?? "");
126
+ if (status < 200 || status >= 300) {
127
+ throw this.toApiError(method, url, status, response.body);
128
+ }
129
+ return { data: response.body, contentType, status };
130
+ }
131
+ }
132
+ /** Perform a GET expecting JSON and parse it into `T`. */
133
+ async getJson(path, query) {
134
+ const res = await this.request("GET", path, { query, accept: "application/json" });
135
+ const text = res.data.toString("utf8");
136
+ try {
137
+ return JSON.parse(text);
138
+ }
139
+ catch (cause) {
140
+ throw new HaushaltParseError(`Failed to parse JSON response from ${path}`, { cause });
141
+ }
142
+ }
143
+ toApiError(method, url, status, body) {
144
+ const text = body.toString("utf8");
145
+ let detail;
146
+ try {
147
+ const parsed = JSON.parse(text);
148
+ if (parsed && typeof parsed.detail === "string")
149
+ detail = parsed.detail;
150
+ else if (parsed && typeof parsed.message === "string")
151
+ detail = parsed.message;
152
+ }
153
+ catch {
154
+ // Non-JSON error body; leave detail undefined.
155
+ }
156
+ return new HaushaltApiError({ status, url, method, body: text, detail });
157
+ }
158
+ }
159
+ //# 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,17 @@
1
+ /** Which side of the budget to query. */
2
+ export declare const AccountValues: readonly ["expenses", "income"];
3
+ export type Account = (typeof AccountValues)[number];
4
+ /** Planned (`target`) vs. realised (`actual`) figures. */
5
+ export declare const QuotaValues: readonly ["target", "actual"];
6
+ export type Quota = (typeof QuotaValues)[number];
7
+ /**
8
+ * How budget elements are grouped:
9
+ * single — by individual budget item (Einzelplan/Titel)
10
+ * function — by functional area (Funktion)
11
+ * group — by economic group (Gruppe)
12
+ */
13
+ export declare const UnitValues: readonly ["single", "function", "group"];
14
+ export type Unit = (typeof UnitValues)[number];
15
+ /** Earliest year the API serves. */
16
+ export declare const MIN_YEAR = 2012;
17
+ //# sourceMappingURL=enums.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"enums.d.ts","sourceRoot":"","sources":["../../../src/client/enums.ts"],"names":[],"mappings":"AAGA,yCAAyC;AACzC,eAAO,MAAM,aAAa,iCAAkC,CAAC;AAC7D,MAAM,MAAM,OAAO,GAAG,CAAC,OAAO,aAAa,CAAC,CAAC,MAAM,CAAC,CAAC;AAErD,0DAA0D;AAC1D,eAAO,MAAM,WAAW,+BAAgC,CAAC;AACzD,MAAM,MAAM,KAAK,GAAG,CAAC,OAAO,WAAW,CAAC,CAAC,MAAM,CAAC,CAAC;AAEjD;;;;;GAKG;AACH,eAAO,MAAM,UAAU,0CAA2C,CAAC;AACnE,MAAM,MAAM,IAAI,GAAG,CAAC,OAAO,UAAU,CAAC,CAAC,MAAM,CAAC,CAAC;AAE/C,oCAAoC;AACpC,eAAO,MAAM,QAAQ,OAAO,CAAC"}
@@ -0,0 +1,16 @@
1
+ // Enum-like value sets. These const arrays double as runtime CLI choice
2
+ // validators and as TS union types.
3
+ /** Which side of the budget to query. */
4
+ export const AccountValues = ["expenses", "income"];
5
+ /** Planned (`target`) vs. realised (`actual`) figures. */
6
+ export const QuotaValues = ["target", "actual"];
7
+ /**
8
+ * How budget elements are grouped:
9
+ * single — by individual budget item (Einzelplan/Titel)
10
+ * function — by functional area (Funktion)
11
+ * group — by economic group (Gruppe)
12
+ */
13
+ export const UnitValues = ["single", "function", "group"];
14
+ /** Earliest year the API serves. */
15
+ export const MIN_YEAR = 2012;
16
+ //# sourceMappingURL=enums.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"enums.js","sourceRoot":"","sources":["../../../src/client/enums.ts"],"names":[],"mappings":"AAAA,wEAAwE;AACxE,oCAAoC;AAEpC,yCAAyC;AACzC,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,UAAU,EAAE,QAAQ,CAAU,CAAC;AAG7D,0DAA0D;AAC1D,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,QAAQ,EAAE,QAAQ,CAAU,CAAC;AAGzD;;;;;GAKG;AACH,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,QAAQ,EAAE,UAAU,EAAE,OAAO,CAAU,CAAC;AAGnE,oCAAoC;AACpC,MAAM,CAAC,MAAM,QAAQ,GAAG,IAAI,CAAC"}
@@ -0,0 +1,33 @@
1
+ /** Base class for every error originating from this client. */
2
+ export declare class HaushaltError 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 HaushaltApiError extends HaushaltError {
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 HaushaltNetworkError extends HaushaltError {
29
+ }
30
+ /** The response body could not be parsed as the expected JSON shape. */
31
+ export declare class HaushaltParseError extends HaushaltError {
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,aAAc,SAAQ,KAAK;gBAC1B,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE;CAI3D;AAED;;;GAGG;AACH,qBAAa,gBAAiB,SAAQ,aAAa;IACjD,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,oBAAqB,SAAQ,aAAa;CAAG;AAE1D,wEAAwE;AACxE,qBAAa,kBAAmB,SAAQ,aAAa;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 HaushaltError 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 HaushaltApiError extends HaushaltError {
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 HaushaltNetworkError extends HaushaltError {
36
+ }
37
+ /** The response body could not be parsed as the expected JSON shape. */
38
+ export class HaushaltParseError extends HaushaltError {
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,aAAc,SAAQ,KAAK;IACtC,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,gBAAiB,SAAQ,aAAa;IACxC,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,oBAAqB,SAAQ,aAAa;CAAG;AAE1D,wEAAwE;AACxE,MAAM,OAAO,kBAAmB,SAAQ,aAAa;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,SAiF5B,CAAC"}
@@ -0,0 +1,89 @@
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 { HaushaltNetworkError } 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 HaushaltNetworkError(`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 HaushaltNetworkError(`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 HaushaltNetworkError(`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 HaushaltNetworkError(`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 HaushaltNetworkError(`Request timed out after ${request.timeoutMs}ms`));
72
+ });
73
+ }
74
+ req.on("error", (err) => {
75
+ // A timeout destroy already passes an HaushaltNetworkError; don't double-wrap.
76
+ // Otherwise prepend the request method + URL so the failure is traceable
77
+ // (the raw Node message — e.g. "connect ECONNREFUSED 127.0.0.1:1" — has no
78
+ // indication of which request it belongs to).
79
+ reject(err instanceof HaushaltNetworkError
80
+ ? err
81
+ : new HaushaltNetworkError(`${request.method} ${request.url} failed: ${err.message}`, {
82
+ cause: err,
83
+ }));
84
+ });
85
+ if (request.body !== undefined)
86
+ req.write(request.body);
87
+ req.end();
88
+ });
89
+ //# 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,11 @@
1
+ export { BundeshaushaltClient } 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 { HaushaltError, HaushaltApiError, HaushaltNetworkError, HaushaltParseError, } from "./errors.js";
9
+ export * from "./enums.js";
10
+ export * from "./types.js";
11
+ //# sourceMappingURL=index.d.ts.map