beliq-sevdesk 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -162,7 +162,7 @@ variable.
162
162
  | `SEVDESK_MAX_RETRIES` | | `4` | Retries on a sevDesk 429 / 5xx / network error. |
163
163
  | `SEVDESK_NOTIFY_WEBHOOK` | `--notify-webhook` | (none) | POST a JSON poll report here. Empty = no notifications. |
164
164
  | `SEVDESK_NOTIFY_ON` | | `failure` | `failure` (only on a failed invoice) or `always` (every poll). |
165
- | `SEVDESK_BASE_URL` | | `https://api.sevdesk.de/api/v1` | Override for a mock or a future version. |
165
+ | `SEVDESK_BASE_URL` | | `https://my.sevdesk.de/api/v1` | Override for a mock or a future version. |
166
166
  | `BELIQ_BASE_URL` | | `https://api.beliq.eu` | Override for a self-hosted beliq. |
167
167
  | `BELIQ_AUTH` | | `header` | How the beliq key is sent: `header` (X-API-Key) or `bearer`. |
168
168
 
package/dist/config.js CHANGED
@@ -1,8 +1,11 @@
1
1
  import { DEFAULT_BASE_URL, LIVE_CONVERT_TARGET_FORMATS, LIVE_PROFILES, } from '@beliq/sdk';
2
2
  import { flagBool, flagStr } from './args.js';
3
3
  import { ConfigError } from './errors.js';
4
- /** sevDesk default REST base. Endpoints hang off /Invoice, /Invoice/{id}/getXml. */
5
- export const DEFAULT_SEVDESK_BASE_URL = 'https://api.sevdesk.de/api/v1';
4
+ /**
5
+ * sevDesk default REST base. Endpoints hang off /Invoice, /Invoice/{id}/getXml.
6
+ * `api.sevdesk.de` serves the API documentation site and 404s every REST path.
7
+ */
8
+ export const DEFAULT_SEVDESK_BASE_URL = 'https://my.sevdesk.de/api/v1';
6
9
  /** sevDesk invoice status codes (Settings dropdown values), keyed by friendly name. */
7
10
  const STATUS_CODES = { draft: '100', open: '200', paid: '1000' };
8
11
  /** Read a non-empty trimmed value; flag wins over env. */
package/dist/sevdesk.js CHANGED
@@ -1,6 +1,8 @@
1
1
  import { SevDeskApiError } from './errors.js';
2
2
  /** A hard ceiling on pages walked per poll, so a misconfigured window can't loop forever. */
3
3
  const MAX_PAGES = 100;
4
+ /** Per-attempt deadline so a stalled sevDesk connection aborts instead of hanging the poll. */
5
+ const REQUEST_TIMEOUT_MS = 30_000;
4
6
  const encoder = new TextEncoder();
5
7
  const decoder = new TextDecoder('utf-8');
6
8
  function looksLikeXml(s) {
@@ -64,6 +66,7 @@ export class SevDeskClient {
64
66
  #sleep;
65
67
  #maxRetries;
66
68
  #baseRetryDelayMs;
69
+ #requestTimeoutMs;
67
70
  constructor(options) {
68
71
  if (!options.token)
69
72
  throw new Error('sevdesk: token is required');
@@ -77,6 +80,7 @@ export class SevDeskClient {
77
80
  this.#sleep = options.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
78
81
  this.#maxRetries = options.maxRetries ?? 4;
79
82
  this.#baseRetryDelayMs = options.baseRetryDelayMs ?? 500;
83
+ this.#requestTimeoutMs = options.requestTimeoutMs ?? REQUEST_TIMEOUT_MS;
80
84
  }
81
85
  async listInvoices(params) {
82
86
  const limit = params.pageSize && params.pageSize > 0 ? params.pageSize : 100;
@@ -122,25 +126,37 @@ export class SevDeskClient {
122
126
  }
123
127
  let lastError;
124
128
  for (let attempt = 0; attempt <= this.#maxRetries; attempt++) {
129
+ const controller = new AbortController();
130
+ const timer = setTimeout(() => controller.abort(), this.#requestTimeoutMs);
125
131
  let res;
132
+ let text = '';
126
133
  try {
127
134
  res = await this.#fetch(url.toString(), {
128
135
  method,
129
136
  headers: { Authorization: this.#token, Accept: 'application/json' },
137
+ signal: controller.signal,
130
138
  });
139
+ // Read the body under the same deadline so a stalled stream also aborts.
140
+ if (res.ok)
141
+ text = await res.text();
131
142
  }
132
143
  catch (err) {
133
- lastError = err;
144
+ lastError = controller.signal.aborted
145
+ ? new Error(`timed out after ${this.#requestTimeoutMs}ms`)
146
+ : err;
134
147
  if (attempt < this.#maxRetries) {
135
148
  await this.#backoff(attempt);
136
149
  continue;
137
150
  }
138
151
  throw new SevDeskApiError(`sevDesk ${method} ${path} network error: ${lastError.message}`, 0);
139
152
  }
153
+ finally {
154
+ clearTimeout(timer);
155
+ }
140
156
  if (res.ok) {
141
157
  return {
142
158
  status: res.status,
143
- text: await res.text(),
159
+ text,
144
160
  contentType: res.headers.get('content-type') ?? '',
145
161
  };
146
162
  }
@@ -148,6 +164,12 @@ export class SevDeskClient {
148
164
  await this.#backoff(attempt);
149
165
  continue;
150
166
  }
167
+ // An HTML 404 means the base URL is not the REST API. api.sevdesk.de serves the
168
+ // documentation site and answers every REST path this way.
169
+ if (res.status === 404 && (res.headers.get('content-type') ?? '').includes('text/html')) {
170
+ throw new SevDeskApiError(`sevDesk ${method} ${path} returned an HTML 404: ${this.#baseUrl} is not the REST API. ` +
171
+ 'The REST base is https://my.sevdesk.de/api/v1; check SEVDESK_BASE_URL.', res.status);
172
+ }
151
173
  throw new SevDeskApiError(`sevDesk ${method} ${path} failed with status ${res.status}`, res.status);
152
174
  }
153
175
  // Unreachable: the loop either returns or throws. Satisfies noImplicitReturns.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "beliq-sevdesk",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "Self-hostable worker that polls sevDesk for e-invoices, converts each to the formats a counterparty needs, and validates them against beliq's authority-pinned, drift-checked rules. The sevDesk token never leaves your environment.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -57,7 +57,7 @@
57
57
  "prepublishOnly": "npm run build"
58
58
  },
59
59
  "dependencies": {
60
- "@beliq/sdk": "^0.1.1"
60
+ "@beliq/sdk": "^0.3.0"
61
61
  },
62
62
  "devDependencies": {
63
63
  "@types/node": "^20.14.0",
@@ -65,6 +65,6 @@
65
65
  "@typescript-eslint/parser": "^7.18.0",
66
66
  "eslint": "^8.57.0",
67
67
  "typescript": "^5.5.4",
68
- "vitest": "^2.1.8"
68
+ "vitest": "^4.0.0"
69
69
  }
70
70
  }