@beliq/sdk 0.1.1 → 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.
package/dist/index.js CHANGED
@@ -34,6 +34,18 @@ function toBytes(input) {
34
34
  function decodeUtf8(bytes) {
35
35
  return new TextDecoder("utf-8").decode(bytes);
36
36
  }
37
+ function decodeBase64(input) {
38
+ const b64 = input.replace(/-/g, "+").replace(/_/g, "/");
39
+ if (typeof atob === "function") {
40
+ const bin = atob(b64);
41
+ const out = new Uint8Array(bin.length);
42
+ for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
43
+ return out;
44
+ }
45
+ const maybeBuffer = globalThis.Buffer;
46
+ if (maybeBuffer) return new Uint8Array(maybeBuffer.from(b64, "base64"));
47
+ throw new Error("beliq: no base64 decoder available in this runtime");
48
+ }
37
49
  var PDF_MAGIC = [37, 80, 68, 70, 45];
38
50
  function sniffContentType(bytes) {
39
51
  const isPdf = bytes.length >= PDF_MAGIC.length && PDF_MAGIC.every((b, i) => bytes[i] === b);
@@ -72,7 +84,8 @@ function buildRequest(params) {
72
84
  path: "/v1/generate",
73
85
  jsonBody: merged,
74
86
  contentType: "application/json",
75
- outputKind: "binary"
87
+ // JSON mode returns the seal envelope; binary mode returns the raw document body.
88
+ ...params.sealed ? { accept: "application/json", outputKind: "json" } : { outputKind: "binary" }
76
89
  };
77
90
  }
78
91
  case "validate": {
@@ -156,8 +169,30 @@ function errorFromResponse(status, bytes) {
156
169
 
157
170
  // src/constants.ts
158
171
  var DEFAULT_BASE_URL = "https://api.beliq.eu";
172
+ var DEFAULT_TIMEOUT_MS = 9e4;
173
+ var DEFAULT_MAX_RETRIES = 3;
159
174
  var LIVE_GENERATE_STANDARDS = ["xrechnung", "zugferd", "facturx", "peppol-bis"];
175
+ var LIVE_GENERATE_PRESETS = [
176
+ { id: "xrechnung", label: "XRechnung", standard: "xrechnung", output: "xml" },
177
+ { id: "factur-x", label: "Factur-X", standard: "facturx", output: "pdf", facturxProfile: "en16931" },
178
+ { id: "zugferd", label: "ZUGFeRD", standard: "zugferd", output: "pdf" },
179
+ { id: "peppol-bis", label: "Peppol BIS 3.0", standard: "peppol-bis", output: "xml" },
180
+ { id: "nlcius", label: "NLCIUS", standard: "peppol-bis", output: "xml", profile: "netherlands-nlcius" }
181
+ ];
160
182
  var LIVE_PROFILES = ["basicwl", "en16931", "extended", "extended-ctc-fr"];
183
+ var LIVE_PROFILES_BY_STANDARD = {
184
+ xrechnung: ["xrechnung"],
185
+ "peppol-bis": ["peppol", "romania-ro-cius", "netherlands-nlcius"],
186
+ zugferd: ["basicwl", "en16931", "extended"],
187
+ facturx: ["basicwl", "en16931", "extended", "extended-ctc-fr"]
188
+ };
189
+ function profilesForStandard(standard) {
190
+ return LIVE_PROFILES_BY_STANDARD[standard] ?? [];
191
+ }
192
+ function isProfileAllowedForStandard(standard, profile) {
193
+ const allowed = profilesForStandard(standard);
194
+ return allowed.length === 0 || allowed.includes(profile);
195
+ }
161
196
  var LIVE_VALIDATE_FORMATS = ["auto", "cii", "ubl"];
162
197
  var LIVE_PARSE_FORMATS = ["auto", "cii", "ubl"];
163
198
  var LIVE_CONVERT_SOURCE_FORMATS = [
@@ -179,6 +214,9 @@ var LIVE_CONVERT_TARGET_FORMATS = [
179
214
  ];
180
215
 
181
216
  // src/transport.ts
217
+ var RETRYABLE_STATUSES = /* @__PURE__ */ new Set([429, 502, 503]);
218
+ var MAX_RETRY_AFTER_MS = 3e4;
219
+ var BACKOFF_BASE_MS = 500;
182
220
  function authHeaders(config) {
183
221
  return config.auth === "bearer" ? { Authorization: `Bearer ${config.apiKey}` } : { "X-API-Key": config.apiKey };
184
222
  }
@@ -192,26 +230,86 @@ function buildUrl(baseUrl, path, query) {
192
230
  const qs = params.toString();
193
231
  return qs ? `${baseUrl}${path}?${qs}` : `${baseUrl}${path}`;
194
232
  }
233
+ function retryDelayMs(res, attempt) {
234
+ const header = res?.headers.get("retry-after");
235
+ if (header) {
236
+ const seconds = Number(header);
237
+ if (Number.isFinite(seconds) && seconds >= 0) {
238
+ return Math.min(seconds * 1e3, MAX_RETRY_AFTER_MS) + Math.random() * 250;
239
+ }
240
+ }
241
+ const backoff = BACKOFF_BASE_MS * 2 ** attempt;
242
+ return backoff + Math.random() * backoff;
243
+ }
244
+ var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
195
245
  async function send(config, req) {
196
246
  const headers = { ...authHeaders(config) };
197
247
  if (req.contentType) headers["Content-Type"] = req.contentType;
248
+ if (req.accept) headers["Accept"] = req.accept;
198
249
  const body = req.jsonBody !== void 0 ? JSON.stringify(req.jsonBody) : req.rawBody;
199
- const res = await config.fetchImpl(buildUrl(config.baseUrl, req.path, req.query), {
200
- method: req.method,
201
- headers,
202
- body
203
- });
204
- const bytes = new Uint8Array(await res.arrayBuffer());
205
- if (!res.ok) throw errorFromResponse(res.status, bytes);
206
- return { status: res.status, headers: res.headers, bytes };
250
+ const url = buildUrl(config.baseUrl, req.path, req.query);
251
+ let lastError;
252
+ for (let attempt = 0; attempt <= config.maxRetries; attempt++) {
253
+ const controller = new AbortController();
254
+ const timer = config.timeoutMs > 0 ? setTimeout(() => controller.abort(), config.timeoutMs) : void 0;
255
+ let res;
256
+ try {
257
+ res = await config.fetchImpl(url, {
258
+ method: req.method,
259
+ headers,
260
+ body,
261
+ signal: controller.signal
262
+ });
263
+ const bytes = new Uint8Array(await res.arrayBuffer());
264
+ if (res.ok) return { status: res.status, headers: res.headers, bytes };
265
+ lastError = errorFromResponse(res.status, bytes);
266
+ if (!RETRYABLE_STATUSES.has(res.status) || attempt === config.maxRetries) {
267
+ throw lastError;
268
+ }
269
+ } catch (err) {
270
+ if (err instanceof BeliqApiError && !RETRYABLE_STATUSES.has(err.status)) throw err;
271
+ if (controller.signal.aborted) {
272
+ throw new BeliqApiError(
273
+ `beliq: request to ${req.path} exceeded ${config.timeoutMs}ms`,
274
+ { status: 0, code: "TIMEOUT" }
275
+ );
276
+ }
277
+ lastError = err;
278
+ if (attempt === config.maxRetries) throw err;
279
+ } finally {
280
+ if (timer) clearTimeout(timer);
281
+ }
282
+ await sleep(retryDelayMs(res, attempt));
283
+ }
284
+ throw lastError;
207
285
  }
208
286
 
209
287
  // src/client.ts
288
+ var TEST_KEY_PREFIX = "blq_test_";
289
+ function livemodeHeader(headers) {
290
+ const raw = headers.get("x-beliq-livemode");
291
+ if (raw === "true") return true;
292
+ if (raw === "false") return false;
293
+ return void 0;
294
+ }
295
+ function parseRulesetArtifacts(headers) {
296
+ const raw = headers.get("x-ruleset-artifacts");
297
+ if (!raw) return void 0;
298
+ try {
299
+ const parsed = JSON.parse(raw);
300
+ return Array.isArray(parsed) ? parsed : void 0;
301
+ } catch {
302
+ return void 0;
303
+ }
304
+ }
210
305
  function generateMeta(headers) {
211
306
  return {
212
307
  schematronVersion: headers.get("x-schematron-version") ?? void 0,
213
308
  pdfKind: headers.get("x-pdf-kind") ?? void 0,
214
- outputEnvelope: headers.get("x-output-envelope") ?? void 0
309
+ outputEnvelope: headers.get("x-output-envelope") ?? void 0,
310
+ rulesetSha256: headers.get("x-ruleset-sha256") ?? void 0,
311
+ rulesetArtifacts: parseRulesetArtifacts(headers),
312
+ livemode: livemodeHeader(headers)
215
313
  };
216
314
  }
217
315
  function convertMeta(headers) {
@@ -231,22 +329,32 @@ function convertMeta(headers) {
231
329
  profileDetected: headers.get("x-profile-detected") ?? void 0,
232
330
  lostElementsCount: countRaw != null ? Number(countRaw) : void 0,
233
331
  lostElements,
234
- conversionTools: headers.get("x-conversion-tools") ?? void 0
332
+ conversionTools: headers.get("x-conversion-tools") ?? void 0,
333
+ livemode: livemodeHeader(headers)
235
334
  };
236
335
  }
237
336
  var Beliq = class {
238
337
  #config;
338
+ /**
339
+ * True for a live key, false for a blq_test_ sandbox key, derived from the key
340
+ * prefix (the same rule the server applies). Available before any request; the
341
+ * per-response x-beliq-livemode header is surfaced on generate/convert meta.
342
+ */
343
+ livemode;
239
344
  constructor(options) {
240
345
  if (!options?.apiKey) throw new Error("beliq: apiKey is required");
241
346
  const fetchImpl = options.fetch ?? globalThis.fetch;
242
347
  if (typeof fetchImpl !== "function") {
243
348
  throw new Error("beliq: no global fetch available; pass options.fetch");
244
349
  }
350
+ this.livemode = !options.apiKey.startsWith(TEST_KEY_PREFIX);
245
351
  this.#config = {
246
352
  apiKey: options.apiKey,
247
353
  baseUrl: (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, ""),
248
354
  auth: options.auth ?? "header",
249
- fetchImpl
355
+ fetchImpl,
356
+ timeoutMs: options.timeoutMs ?? DEFAULT_TIMEOUT_MS,
357
+ maxRetries: options.maxRetries ?? DEFAULT_MAX_RETRIES
250
358
  };
251
359
  }
252
360
  /** Account, plan, and quota context for the key. Does not consume quota. */
@@ -255,14 +363,37 @@ var Beliq = class {
255
363
  }
256
364
  /** Generate a compliant e-invoice from an EN 16931 object. */
257
365
  async generate(input) {
258
- const res = await send(this.#config, buildRequest({ operation: "generate", ...input }));
366
+ const res = await send(
367
+ this.#config,
368
+ buildRequest({ operation: "generate", ...input, sealed: input.seal })
369
+ );
370
+ const meta = generateMeta(res.headers);
371
+ if (input.seal) {
372
+ const envelope = parseEnvelope(res.bytes);
373
+ if (envelope?.success === false) throw errorFromResponse(res.status, res.bytes);
374
+ const data = envelope?.data;
375
+ if (!data || data.output === void 0) {
376
+ throw new BeliqApiError("beliq: seal response was not a JSON envelope", {
377
+ status: res.status
378
+ });
379
+ }
380
+ const bytes = decodeBase64(data.output);
381
+ const contentType2 = data.contentType ?? "application/octet-stream";
382
+ return {
383
+ contentType: contentType2,
384
+ bytes,
385
+ xml: contentType2.includes("xml") ? decodeUtf8(bytes) : void 0,
386
+ sha256: data.sha256,
387
+ validationResult: data.validationResult,
388
+ meta
389
+ };
390
+ }
259
391
  const contentType = res.headers.get("content-type") ?? "application/octet-stream";
260
- const isXml = contentType.includes("xml");
261
392
  return {
262
393
  contentType,
263
394
  bytes: res.bytes,
264
- xml: isXml ? decodeUtf8(res.bytes) : void 0,
265
- meta: generateMeta(res.headers)
395
+ xml: contentType.includes("xml") ? decodeUtf8(res.bytes) : void 0,
396
+ meta
266
397
  };
267
398
  }
268
399
  /** Validate a document against authority-pinned rules. */
@@ -323,10 +454,16 @@ export {
323
454
  Beliq,
324
455
  BeliqApiError,
325
456
  DEFAULT_BASE_URL,
457
+ DEFAULT_MAX_RETRIES,
458
+ DEFAULT_TIMEOUT_MS,
326
459
  LIVE_CONVERT_SOURCE_FORMATS,
327
460
  LIVE_CONVERT_TARGET_FORMATS,
461
+ LIVE_GENERATE_PRESETS,
328
462
  LIVE_GENERATE_STANDARDS,
329
463
  LIVE_PARSE_FORMATS,
330
464
  LIVE_PROFILES,
331
- LIVE_VALIDATE_FORMATS
465
+ LIVE_PROFILES_BY_STANDARD,
466
+ LIVE_VALIDATE_FORMATS,
467
+ isProfileAllowedForStandard,
468
+ profilesForStandard
332
469
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@beliq/sdk",
3
- "version": "0.1.1",
3
+ "version": "0.3.0",
4
4
  "description": "Official beliq SDK: generate, validate, parse, and convert EU-compliant e-invoices (XRechnung, ZUGFeRD, Factur-X, Peppol BIS) against authority-pinned, drift-checked rules.",
5
5
  "keywords": [
6
6
  "beliq",
@@ -72,19 +72,25 @@
72
72
  "sync:spec": "node scripts/sync-spec.mjs",
73
73
  "gen:types": "node scripts/gen-types.mjs",
74
74
  "openapi:check": "node scripts/gen-types.mjs --check",
75
+ "spec:drift": "node scripts/check-live-drift.mjs",
75
76
  "test": "vitest run --exclude test/integration.test.ts",
76
77
  "test:integration": "vitest run test/integration.test.ts",
77
- "prepublishOnly": "npm run build"
78
+ "prepublishOnly": "npm run build",
79
+ "check:profiles": "node --experimental-strip-types scripts/check-profile-drift.mjs"
78
80
  },
79
81
  "dependencies": {},
82
+ "overrides": {
83
+ "esbuild": "^0.28.1",
84
+ "js-yaml": "^4.3.1"
85
+ },
80
86
  "devDependencies": {
87
+ "@eslint/js": "^10.0.0",
81
88
  "@types/node": "^20.14.0",
82
- "@typescript-eslint/eslint-plugin": "^7.18.0",
83
- "@typescript-eslint/parser": "^7.18.0",
84
- "eslint": "^8.57.0",
89
+ "eslint": "^10.0.0",
85
90
  "openapi-typescript": "^7.4.0",
86
91
  "tsup": "^8.3.0",
87
92
  "typescript": "^5.5.4",
88
- "vitest": "^2.1.8"
93
+ "typescript-eslint": "^8.0.0",
94
+ "vitest": "^4.0.0"
89
95
  }
90
96
  }
@@ -1,22 +0,0 @@
1
- type PlainObject = Record<string, unknown>;
2
- declare function isPlainObject(value: unknown): value is PlainObject;
3
- /** Deep-merge `source` into `target` (source wins). Arrays and scalars overwrite. */
4
- declare function mergeDeep(target: PlainObject, source: PlainObject): PlainObject;
5
- /** Drop undefined/empty entries so optional query params are omitted, not sent blank. */
6
- declare function compactQuery(query: PlainObject): PlainObject;
7
- /** Document bytes a caller may pass to validate/parse/convert. */
8
- type DocumentInput = string | Uint8Array | ArrayBuffer | ArrayBufferView;
9
- declare function toBytes(input: DocumentInput): Uint8Array;
10
- declare function decodeUtf8(bytes: Uint8Array): string;
11
- /** Sniff `application/pdf` vs `application/xml` from the leading bytes. */
12
- declare function sniffContentType(bytes: Uint8Array): string;
13
-
14
- declare const DEFAULT_BASE_URL = "https://api.beliq.eu";
15
- declare const LIVE_GENERATE_STANDARDS: readonly ["xrechnung", "zugferd", "facturx", "peppol-bis"];
16
- declare const LIVE_PROFILES: readonly ["basicwl", "en16931", "extended", "extended-ctc-fr"];
17
- declare const LIVE_VALIDATE_FORMATS: readonly ["auto", "cii", "ubl"];
18
- declare const LIVE_PARSE_FORMATS: readonly ["auto", "cii", "ubl"];
19
- declare const LIVE_CONVERT_SOURCE_FORMATS: readonly ["auto", "cii", "ubl", "zugferd", "facturx", "xrechnung", "peppol-bis"];
20
- declare const LIVE_CONVERT_TARGET_FORMATS: readonly ["cii", "ubl", "zugferd", "facturx", "xrechnung", "peppol-bis"];
21
-
22
- export { DEFAULT_BASE_URL as D, LIVE_CONVERT_SOURCE_FORMATS as L, type PlainObject as P, type DocumentInput as a, LIVE_CONVERT_TARGET_FORMATS as b, LIVE_GENERATE_STANDARDS as c, LIVE_PARSE_FORMATS as d, LIVE_PROFILES as e, LIVE_VALIDATE_FORMATS as f, compactQuery as g, decodeUtf8 as h, isPlainObject as i, mergeDeep as m, sniffContentType as s, toBytes as t };
@@ -1,22 +0,0 @@
1
- type PlainObject = Record<string, unknown>;
2
- declare function isPlainObject(value: unknown): value is PlainObject;
3
- /** Deep-merge `source` into `target` (source wins). Arrays and scalars overwrite. */
4
- declare function mergeDeep(target: PlainObject, source: PlainObject): PlainObject;
5
- /** Drop undefined/empty entries so optional query params are omitted, not sent blank. */
6
- declare function compactQuery(query: PlainObject): PlainObject;
7
- /** Document bytes a caller may pass to validate/parse/convert. */
8
- type DocumentInput = string | Uint8Array | ArrayBuffer | ArrayBufferView;
9
- declare function toBytes(input: DocumentInput): Uint8Array;
10
- declare function decodeUtf8(bytes: Uint8Array): string;
11
- /** Sniff `application/pdf` vs `application/xml` from the leading bytes. */
12
- declare function sniffContentType(bytes: Uint8Array): string;
13
-
14
- declare const DEFAULT_BASE_URL = "https://api.beliq.eu";
15
- declare const LIVE_GENERATE_STANDARDS: readonly ["xrechnung", "zugferd", "facturx", "peppol-bis"];
16
- declare const LIVE_PROFILES: readonly ["basicwl", "en16931", "extended", "extended-ctc-fr"];
17
- declare const LIVE_VALIDATE_FORMATS: readonly ["auto", "cii", "ubl"];
18
- declare const LIVE_PARSE_FORMATS: readonly ["auto", "cii", "ubl"];
19
- declare const LIVE_CONVERT_SOURCE_FORMATS: readonly ["auto", "cii", "ubl", "zugferd", "facturx", "xrechnung", "peppol-bis"];
20
- declare const LIVE_CONVERT_TARGET_FORMATS: readonly ["cii", "ubl", "zugferd", "facturx", "xrechnung", "peppol-bis"];
21
-
22
- export { DEFAULT_BASE_URL as D, LIVE_CONVERT_SOURCE_FORMATS as L, type PlainObject as P, type DocumentInput as a, LIVE_CONVERT_TARGET_FORMATS as b, LIVE_GENERATE_STANDARDS as c, LIVE_PARSE_FORMATS as d, LIVE_PROFILES as e, LIVE_VALIDATE_FORMATS as f, compactQuery as g, decodeUtf8 as h, isPlainObject as i, mergeDeep as m, sniffContentType as s, toBytes as t };