@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.cjs CHANGED
@@ -23,12 +23,18 @@ __export(index_exports, {
23
23
  Beliq: () => Beliq,
24
24
  BeliqApiError: () => BeliqApiError,
25
25
  DEFAULT_BASE_URL: () => DEFAULT_BASE_URL,
26
+ DEFAULT_MAX_RETRIES: () => DEFAULT_MAX_RETRIES,
27
+ DEFAULT_TIMEOUT_MS: () => DEFAULT_TIMEOUT_MS,
26
28
  LIVE_CONVERT_SOURCE_FORMATS: () => LIVE_CONVERT_SOURCE_FORMATS,
27
29
  LIVE_CONVERT_TARGET_FORMATS: () => LIVE_CONVERT_TARGET_FORMATS,
30
+ LIVE_GENERATE_PRESETS: () => LIVE_GENERATE_PRESETS,
28
31
  LIVE_GENERATE_STANDARDS: () => LIVE_GENERATE_STANDARDS,
29
32
  LIVE_PARSE_FORMATS: () => LIVE_PARSE_FORMATS,
30
33
  LIVE_PROFILES: () => LIVE_PROFILES,
31
- LIVE_VALIDATE_FORMATS: () => LIVE_VALIDATE_FORMATS
34
+ LIVE_PROFILES_BY_STANDARD: () => LIVE_PROFILES_BY_STANDARD,
35
+ LIVE_VALIDATE_FORMATS: () => LIVE_VALIDATE_FORMATS,
36
+ isProfileAllowedForStandard: () => isProfileAllowedForStandard,
37
+ profilesForStandard: () => profilesForStandard
32
38
  });
33
39
  module.exports = __toCommonJS(index_exports);
34
40
 
@@ -68,6 +74,18 @@ function toBytes(input) {
68
74
  function decodeUtf8(bytes) {
69
75
  return new TextDecoder("utf-8").decode(bytes);
70
76
  }
77
+ function decodeBase64(input) {
78
+ const b64 = input.replace(/-/g, "+").replace(/_/g, "/");
79
+ if (typeof atob === "function") {
80
+ const bin = atob(b64);
81
+ const out = new Uint8Array(bin.length);
82
+ for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
83
+ return out;
84
+ }
85
+ const maybeBuffer = globalThis.Buffer;
86
+ if (maybeBuffer) return new Uint8Array(maybeBuffer.from(b64, "base64"));
87
+ throw new Error("beliq: no base64 decoder available in this runtime");
88
+ }
71
89
  var PDF_MAGIC = [37, 80, 68, 70, 45];
72
90
  function sniffContentType(bytes) {
73
91
  const isPdf = bytes.length >= PDF_MAGIC.length && PDF_MAGIC.every((b, i) => bytes[i] === b);
@@ -106,7 +124,8 @@ function buildRequest(params) {
106
124
  path: "/v1/generate",
107
125
  jsonBody: merged,
108
126
  contentType: "application/json",
109
- outputKind: "binary"
127
+ // JSON mode returns the seal envelope; binary mode returns the raw document body.
128
+ ...params.sealed ? { accept: "application/json", outputKind: "json" } : { outputKind: "binary" }
110
129
  };
111
130
  }
112
131
  case "validate": {
@@ -190,8 +209,30 @@ function errorFromResponse(status, bytes) {
190
209
 
191
210
  // src/constants.ts
192
211
  var DEFAULT_BASE_URL = "https://api.beliq.eu";
212
+ var DEFAULT_TIMEOUT_MS = 9e4;
213
+ var DEFAULT_MAX_RETRIES = 3;
193
214
  var LIVE_GENERATE_STANDARDS = ["xrechnung", "zugferd", "facturx", "peppol-bis"];
215
+ var LIVE_GENERATE_PRESETS = [
216
+ { id: "xrechnung", label: "XRechnung", standard: "xrechnung", output: "xml" },
217
+ { id: "factur-x", label: "Factur-X", standard: "facturx", output: "pdf", facturxProfile: "en16931" },
218
+ { id: "zugferd", label: "ZUGFeRD", standard: "zugferd", output: "pdf" },
219
+ { id: "peppol-bis", label: "Peppol BIS 3.0", standard: "peppol-bis", output: "xml" },
220
+ { id: "nlcius", label: "NLCIUS", standard: "peppol-bis", output: "xml", profile: "netherlands-nlcius" }
221
+ ];
194
222
  var LIVE_PROFILES = ["basicwl", "en16931", "extended", "extended-ctc-fr"];
223
+ var LIVE_PROFILES_BY_STANDARD = {
224
+ xrechnung: ["xrechnung"],
225
+ "peppol-bis": ["peppol", "romania-ro-cius", "netherlands-nlcius"],
226
+ zugferd: ["basicwl", "en16931", "extended"],
227
+ facturx: ["basicwl", "en16931", "extended", "extended-ctc-fr"]
228
+ };
229
+ function profilesForStandard(standard) {
230
+ return LIVE_PROFILES_BY_STANDARD[standard] ?? [];
231
+ }
232
+ function isProfileAllowedForStandard(standard, profile) {
233
+ const allowed = profilesForStandard(standard);
234
+ return allowed.length === 0 || allowed.includes(profile);
235
+ }
195
236
  var LIVE_VALIDATE_FORMATS = ["auto", "cii", "ubl"];
196
237
  var LIVE_PARSE_FORMATS = ["auto", "cii", "ubl"];
197
238
  var LIVE_CONVERT_SOURCE_FORMATS = [
@@ -213,6 +254,9 @@ var LIVE_CONVERT_TARGET_FORMATS = [
213
254
  ];
214
255
 
215
256
  // src/transport.ts
257
+ var RETRYABLE_STATUSES = /* @__PURE__ */ new Set([429, 502, 503]);
258
+ var MAX_RETRY_AFTER_MS = 3e4;
259
+ var BACKOFF_BASE_MS = 500;
216
260
  function authHeaders(config) {
217
261
  return config.auth === "bearer" ? { Authorization: `Bearer ${config.apiKey}` } : { "X-API-Key": config.apiKey };
218
262
  }
@@ -226,26 +270,86 @@ function buildUrl(baseUrl, path, query) {
226
270
  const qs = params.toString();
227
271
  return qs ? `${baseUrl}${path}?${qs}` : `${baseUrl}${path}`;
228
272
  }
273
+ function retryDelayMs(res, attempt) {
274
+ const header = res?.headers.get("retry-after");
275
+ if (header) {
276
+ const seconds = Number(header);
277
+ if (Number.isFinite(seconds) && seconds >= 0) {
278
+ return Math.min(seconds * 1e3, MAX_RETRY_AFTER_MS) + Math.random() * 250;
279
+ }
280
+ }
281
+ const backoff = BACKOFF_BASE_MS * 2 ** attempt;
282
+ return backoff + Math.random() * backoff;
283
+ }
284
+ var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
229
285
  async function send(config, req) {
230
286
  const headers = { ...authHeaders(config) };
231
287
  if (req.contentType) headers["Content-Type"] = req.contentType;
288
+ if (req.accept) headers["Accept"] = req.accept;
232
289
  const body = req.jsonBody !== void 0 ? JSON.stringify(req.jsonBody) : req.rawBody;
233
- const res = await config.fetchImpl(buildUrl(config.baseUrl, req.path, req.query), {
234
- method: req.method,
235
- headers,
236
- body
237
- });
238
- const bytes = new Uint8Array(await res.arrayBuffer());
239
- if (!res.ok) throw errorFromResponse(res.status, bytes);
240
- return { status: res.status, headers: res.headers, bytes };
290
+ const url = buildUrl(config.baseUrl, req.path, req.query);
291
+ let lastError;
292
+ for (let attempt = 0; attempt <= config.maxRetries; attempt++) {
293
+ const controller = new AbortController();
294
+ const timer = config.timeoutMs > 0 ? setTimeout(() => controller.abort(), config.timeoutMs) : void 0;
295
+ let res;
296
+ try {
297
+ res = await config.fetchImpl(url, {
298
+ method: req.method,
299
+ headers,
300
+ body,
301
+ signal: controller.signal
302
+ });
303
+ const bytes = new Uint8Array(await res.arrayBuffer());
304
+ if (res.ok) return { status: res.status, headers: res.headers, bytes };
305
+ lastError = errorFromResponse(res.status, bytes);
306
+ if (!RETRYABLE_STATUSES.has(res.status) || attempt === config.maxRetries) {
307
+ throw lastError;
308
+ }
309
+ } catch (err) {
310
+ if (err instanceof BeliqApiError && !RETRYABLE_STATUSES.has(err.status)) throw err;
311
+ if (controller.signal.aborted) {
312
+ throw new BeliqApiError(
313
+ `beliq: request to ${req.path} exceeded ${config.timeoutMs}ms`,
314
+ { status: 0, code: "TIMEOUT" }
315
+ );
316
+ }
317
+ lastError = err;
318
+ if (attempt === config.maxRetries) throw err;
319
+ } finally {
320
+ if (timer) clearTimeout(timer);
321
+ }
322
+ await sleep(retryDelayMs(res, attempt));
323
+ }
324
+ throw lastError;
241
325
  }
242
326
 
243
327
  // src/client.ts
328
+ var TEST_KEY_PREFIX = "blq_test_";
329
+ function livemodeHeader(headers) {
330
+ const raw = headers.get("x-beliq-livemode");
331
+ if (raw === "true") return true;
332
+ if (raw === "false") return false;
333
+ return void 0;
334
+ }
335
+ function parseRulesetArtifacts(headers) {
336
+ const raw = headers.get("x-ruleset-artifacts");
337
+ if (!raw) return void 0;
338
+ try {
339
+ const parsed = JSON.parse(raw);
340
+ return Array.isArray(parsed) ? parsed : void 0;
341
+ } catch {
342
+ return void 0;
343
+ }
344
+ }
244
345
  function generateMeta(headers) {
245
346
  return {
246
347
  schematronVersion: headers.get("x-schematron-version") ?? void 0,
247
348
  pdfKind: headers.get("x-pdf-kind") ?? void 0,
248
- outputEnvelope: headers.get("x-output-envelope") ?? void 0
349
+ outputEnvelope: headers.get("x-output-envelope") ?? void 0,
350
+ rulesetSha256: headers.get("x-ruleset-sha256") ?? void 0,
351
+ rulesetArtifacts: parseRulesetArtifacts(headers),
352
+ livemode: livemodeHeader(headers)
249
353
  };
250
354
  }
251
355
  function convertMeta(headers) {
@@ -265,22 +369,32 @@ function convertMeta(headers) {
265
369
  profileDetected: headers.get("x-profile-detected") ?? void 0,
266
370
  lostElementsCount: countRaw != null ? Number(countRaw) : void 0,
267
371
  lostElements,
268
- conversionTools: headers.get("x-conversion-tools") ?? void 0
372
+ conversionTools: headers.get("x-conversion-tools") ?? void 0,
373
+ livemode: livemodeHeader(headers)
269
374
  };
270
375
  }
271
376
  var Beliq = class {
272
377
  #config;
378
+ /**
379
+ * True for a live key, false for a blq_test_ sandbox key, derived from the key
380
+ * prefix (the same rule the server applies). Available before any request; the
381
+ * per-response x-beliq-livemode header is surfaced on generate/convert meta.
382
+ */
383
+ livemode;
273
384
  constructor(options) {
274
385
  if (!options?.apiKey) throw new Error("beliq: apiKey is required");
275
386
  const fetchImpl = options.fetch ?? globalThis.fetch;
276
387
  if (typeof fetchImpl !== "function") {
277
388
  throw new Error("beliq: no global fetch available; pass options.fetch");
278
389
  }
390
+ this.livemode = !options.apiKey.startsWith(TEST_KEY_PREFIX);
279
391
  this.#config = {
280
392
  apiKey: options.apiKey,
281
393
  baseUrl: (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, ""),
282
394
  auth: options.auth ?? "header",
283
- fetchImpl
395
+ fetchImpl,
396
+ timeoutMs: options.timeoutMs ?? DEFAULT_TIMEOUT_MS,
397
+ maxRetries: options.maxRetries ?? DEFAULT_MAX_RETRIES
284
398
  };
285
399
  }
286
400
  /** Account, plan, and quota context for the key. Does not consume quota. */
@@ -289,14 +403,37 @@ var Beliq = class {
289
403
  }
290
404
  /** Generate a compliant e-invoice from an EN 16931 object. */
291
405
  async generate(input) {
292
- const res = await send(this.#config, buildRequest({ operation: "generate", ...input }));
406
+ const res = await send(
407
+ this.#config,
408
+ buildRequest({ operation: "generate", ...input, sealed: input.seal })
409
+ );
410
+ const meta = generateMeta(res.headers);
411
+ if (input.seal) {
412
+ const envelope = parseEnvelope(res.bytes);
413
+ if (envelope?.success === false) throw errorFromResponse(res.status, res.bytes);
414
+ const data = envelope?.data;
415
+ if (!data || data.output === void 0) {
416
+ throw new BeliqApiError("beliq: seal response was not a JSON envelope", {
417
+ status: res.status
418
+ });
419
+ }
420
+ const bytes = decodeBase64(data.output);
421
+ const contentType2 = data.contentType ?? "application/octet-stream";
422
+ return {
423
+ contentType: contentType2,
424
+ bytes,
425
+ xml: contentType2.includes("xml") ? decodeUtf8(bytes) : void 0,
426
+ sha256: data.sha256,
427
+ validationResult: data.validationResult,
428
+ meta
429
+ };
430
+ }
293
431
  const contentType = res.headers.get("content-type") ?? "application/octet-stream";
294
- const isXml = contentType.includes("xml");
295
432
  return {
296
433
  contentType,
297
434
  bytes: res.bytes,
298
- xml: isXml ? decodeUtf8(res.bytes) : void 0,
299
- meta: generateMeta(res.headers)
435
+ xml: contentType.includes("xml") ? decodeUtf8(res.bytes) : void 0,
436
+ meta
300
437
  };
301
438
  }
302
439
  /** Validate a document against authority-pinned rules. */
@@ -358,10 +495,16 @@ var Beliq = class {
358
495
  Beliq,
359
496
  BeliqApiError,
360
497
  DEFAULT_BASE_URL,
498
+ DEFAULT_MAX_RETRIES,
499
+ DEFAULT_TIMEOUT_MS,
361
500
  LIVE_CONVERT_SOURCE_FORMATS,
362
501
  LIVE_CONVERT_TARGET_FORMATS,
502
+ LIVE_GENERATE_PRESETS,
363
503
  LIVE_GENERATE_STANDARDS,
364
504
  LIVE_PARSE_FORMATS,
365
505
  LIVE_PROFILES,
366
- LIVE_VALIDATE_FORMATS
506
+ LIVE_PROFILES_BY_STANDARD,
507
+ LIVE_VALIDATE_FORMATS,
508
+ isProfileAllowedForStandard,
509
+ profilesForStandard
367
510
  });