@getpeppr/cli 0.7.0 → 0.7.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.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,33 @@ All notable changes to `@getpeppr/cli` are documented here.
4
4
 
5
5
  The CLI bundles `@getpeppr/sdk` into its published artifact (tsup `noExternal`), so `validate`, `convert` and `init` run the SDK's validators, country rules and UBL builder locally — an SDK release only reaches CLI users once the CLI is rebundled. Rebundle-only releases are listed with the SDK version they ship.
6
6
 
7
+ ## [0.7.1] — 2026-08-18
8
+
9
+ Rebundle SDK 4.1.1. This release matters for `getpeppr validate`, which runs the
10
+ schematron validator locally from the bundled SDK.
11
+
12
+ **Four rule identifiers were wrong, and 0.7.0 still reports them.** They named
13
+ real rules from the Peppol rulebook — just not the ones being checked — which is
14
+ worse than a typo: you could look one up, read a genuine rule, and find it had
15
+ nothing to do with your document.
16
+
17
+ | `validate` reported | It now reports | What the check actually verifies |
18
+ | --- | --- | --- |
19
+ | `BR-S-01` | `BR-S-05` | standard-rated line must carry a VAT rate above zero |
20
+ | `BR-S-05` | `BR-Z-05` | zero-rated line must carry a rate of exactly zero |
21
+ | `BR-S-06` | `BR-E-05` | VAT-exempt line must carry a rate of exactly zero |
22
+ | `BR-S-08` | `BR-AE-05` | reverse-charge line must carry a rate of exactly zero |
23
+
24
+ The checks themselves were always right — a zero-rated line carrying a non-zero
25
+ rate was always caught. Only the label was wrong. **If you branch on any of these
26
+ identifiers in a pipeline, they change with this version.**
27
+
28
+ Fixed in SDK 4.1.0 on 2026-08-18; CLI users only receive it now, because the CLI
29
+ bundles the SDK rather than depending on it at runtime.
30
+
31
+ Also carries SDK 4.1.1, which corrects the JSDoc on `importFile()` — raw import
32
+ does not preserve your document byte for byte, and the SDK said it did.
33
+
7
34
  ## [0.7.0] — 2026-08-16
8
35
 
9
36
  Rebundle SDK 4.0.0 (GPR-1061). `getpeppr send --watch` could not confirm a
package/README.md CHANGED
@@ -175,6 +175,23 @@ Removes `~/.config/getpeppr/credentials.json`. No-op if the file doesn't exist.
175
175
  | `1` | Failure (invalid invoice, participant not found) |
176
176
  | `2` | Input error (file not found, invalid JSON, bad arguments) |
177
177
 
178
+ ### `send --watch` is two outcomes, not one
179
+
180
+ Since 0.7.0, a watch that fails because the API answered something the CLI
181
+ cannot honestly read exits **1**. Before 0.7.0 it printed a warning and exited
182
+ `0`, which meant an automation could read a broken API as a confirmed delivery.
183
+
184
+ A watch that runs out of time still exits **0**, and that is a different fact:
185
+ the document was accepted, and the watch stopped without having *seen* a
186
+ terminal status. It does not follow that the document is still in flight. The
187
+ deadline is checked between polls, so a status can turn terminal during the
188
+ final wait and never be read — the status printed on a timeout may already be
189
+ stale. Poll again or use a webhook rather than treating a timeout as a failure.
190
+
191
+ ⚠️ The 60 seconds bound when the **last poll starts**, not when the command
192
+ ends. The deadline is checked before each request, so a slow or retried call
193
+ carries the run past it. If a pipeline needs a hard ceiling, impose your own.
194
+
178
195
  ## CI/CD integration
179
196
 
180
197
  ```bash
package/dist/index.js CHANGED
@@ -1161,7 +1161,7 @@ function statusFamily(status) {
1161
1161
  var TERMINAL_FAILURE_STATUSES = STATUS_PRECEDENCE.filter((e) => e.family === "terminal-failure").map((e) => e.status);
1162
1162
 
1163
1163
  // ../sdk/dist/version.js
1164
- var SDK_VERSION = "4.0.0";
1164
+ var SDK_VERSION = "4.1.1";
1165
1165
 
1166
1166
  // ../sdk/dist/core/client.js
1167
1167
  function findHeaderCaseInsensitive(headers, name) {
@@ -1753,7 +1753,11 @@ var GetpepprAdapter = class {
1753
1753
  const body = {
1754
1754
  file: arrayBufferToBase64(options.file),
1755
1755
  filename: options.filename,
1756
- mimeType: options.mimeType ?? detectMimeType(options.filename)
1756
+ mimeType: options.mimeType ?? detectMimeType(options.filename),
1757
+ // Declared, never derived from the document: routing decides delivery, and
1758
+ // parsing a caller's XML for a destination would put a parse error on the
1759
+ // "whose invoice goes where" path.
1760
+ to: options.to
1757
1761
  };
1758
1762
  const result = await this.request("POST", "/invoices/import", body);
1759
1763
  return parseSendResult(result);
@@ -1863,6 +1867,10 @@ function parseSendResult(body) {
1863
1867
  const detail = parseStatusDetail(result.detail);
1864
1868
  if (detail)
1865
1869
  sendResult.detail = detail;
1870
+ const rulebook = result.rulebook;
1871
+ if (typeof rulebook === "object" && rulebook !== null && typeof rulebook.peppol === "string" && typeof rulebook.verifiedAt === "string") {
1872
+ sendResult.rulebook = rulebook;
1873
+ }
1866
1874
  const submissionId = optionalWireId(result.submissionId);
1867
1875
  if (submissionId)
1868
1876
  sendResult.submissionId = submissionId;
@@ -2354,9 +2362,30 @@ ${validation.errors.map((e) => ` - ${e.field}: ${e.message}${e.suggestion ? ` (
2354
2362
  return this.adapter.validateDocumentServer(input);
2355
2363
  }
2356
2364
  /**
2357
- * Import an invoice from a file (XML, PDF, JSON).
2358
- * The file is base64-encoded and sent to the gateway, which forwards it
2359
- * as multipart/form-data to the provider's import endpoint.
2365
+ * Send a UBL Invoice or CreditNote you built yourself.
2366
+ *
2367
+ * getpeppr does not regenerate, normalise, or repair the document we
2368
+ * forward the bytes you supplied, unchanged, to the network. Only UBL Invoice
2369
+ * and CreditNote are accepted — a PDF, a CII document, or an XML that is
2370
+ * neither is refused. The file is base64-encoded into a JSON body; there is
2371
+ * no multipart upload.
2372
+ *
2373
+ * ⚠️ Byte-for-byte equality is NOT guaranteed, because the network
2374
+ * re-serialises the document in transit. Measured 2026-08-18 on a test
2375
+ * document: namespace declarations come back reordered, numeric character
2376
+ * references are resolved (`A` → `A`), whitespace inside tags is dropped,
2377
+ * and no element was added, removed or altered. That is one document and four
2378
+ * kinds of difference — indicative, not a warranty of what is preserved.
2379
+ * **If you seal your documents, hash a canonical form (C14N) rather than the
2380
+ * raw bytes.**
2381
+ *
2382
+ * `to` is required and is never read from the document. Routing decides
2383
+ * delivery, the document travels as payload, and getpeppr will not guess a
2384
+ * destination by parsing your XML.
2385
+ *
2386
+ * Before transmission the document is validated against the complete official
2387
+ * OpenPeppol rulebooks. A document violating a `fatal` rule is refused and is
2388
+ * NOT sent; the error names the rule.
2360
2389
  *
2361
2390
  * @example
2362
2391
  * ```ts
@@ -2364,10 +2393,29 @@ ${validation.errors.map((e) => ` - ${e.field}: ${e.message}${e.suggestion ? ` (
2364
2393
  * const result = await peppol.invoices.importFile({
2365
2394
  * file: xmlBytes,
2366
2395
  * filename: "invoice.xml",
2396
+ * to: { peppolId: "0208:0685660237" },
2367
2397
  * });
2368
2398
  * console.log(result.id, result.status);
2369
2399
  * ```
2370
- * @throws {PeppolApiError} 501 if the gateway provider does not support file import
2400
+ *
2401
+ * @throws {PeppolApiError} 400 — `invalid_base64`, or a missing `file` /
2402
+ * `filename`. `missing_recipient` when `to.peppolId` is absent.
2403
+ * @throws {PeppolApiError} 422 — the document was refused and NOT sent. Two
2404
+ * families, and they do NOT retry the same way:
2405
+ *
2406
+ * - **The document was rejected** (`validation_failed`, `not_ubl_document`,
2407
+ * `document_too_complex`, `undecodable_document`, `unsupported_encoding`).
2408
+ * Terminal: the same bytes fail identically forever. Fix the document —
2409
+ * retrying is pure waste, and `validation_failed` names the rule.
2410
+ * - **The account may not send right now** (`peppol_identity_incomplete`,
2411
+ * `peppol_identity_not_verified`, `platform_billing_not_active`,
2412
+ * `production_access_expired`). ⛔ NOT terminal: these describe account
2413
+ * state, and account state changes — a verification completes, a contract
2414
+ * is activated. The identical document will go through once it does.
2415
+ *
2416
+ * Treating the second family as terminal costs a customer a real invoice;
2417
+ * treating the first as retryable costs an infinite loop. See the API
2418
+ * reference for the full list.
2371
2419
  */
2372
2420
  async importFile(options) {
2373
2421
  return this.adapter.importInvoice(options);
@@ -3174,7 +3222,7 @@ var brCo16 = (input) => {
3174
3222
  }
3175
3223
  return [];
3176
3224
  };
3177
- var brS01 = (input) => {
3225
+ var brS05 = (input) => {
3178
3226
  const violations = [];
3179
3227
  if (!input.lines)
3180
3228
  return violations;
@@ -3182,43 +3230,43 @@ var brS01 = (input) => {
3182
3230
  const line = input.lines[i];
3183
3231
  const category = line.vatCategory ?? "S";
3184
3232
  if (category === "S" && (line.vatRate === void 0 || line.vatRate <= 0)) {
3185
- violations.push(violation("BR-S-01", "error", `Line ${i}: standard rate (S) requires vatRate > 0, got ${line.vatRate ?? "undefined"}.`, `lines[${i}].vatRate`));
3233
+ violations.push(violation("BR-S-05", "error", `Line ${i}: standard rate (S) requires vatRate > 0, got ${line.vatRate ?? "undefined"}.`, `lines[${i}].vatRate`));
3186
3234
  }
3187
3235
  }
3188
3236
  return violations;
3189
3237
  };
3190
- var brS05 = (input) => {
3238
+ var brZ05 = (input) => {
3191
3239
  const violations = [];
3192
3240
  if (!input.lines)
3193
3241
  return violations;
3194
3242
  for (let i = 0; i < input.lines.length; i++) {
3195
3243
  const line = input.lines[i];
3196
3244
  if (line.vatCategory === "Z" && line.vatRate !== 0) {
3197
- violations.push(violation("BR-S-05", "error", `Line ${i}: zero-rated (Z) requires vatRate = 0, got ${line.vatRate}.`, `lines[${i}].vatRate`));
3245
+ violations.push(violation("BR-Z-05", "error", `Line ${i}: zero-rated (Z) requires vatRate = 0, got ${line.vatRate}.`, `lines[${i}].vatRate`));
3198
3246
  }
3199
3247
  }
3200
3248
  return violations;
3201
3249
  };
3202
- var brS06 = (input) => {
3250
+ var brE05 = (input) => {
3203
3251
  const violations = [];
3204
3252
  if (!input.lines)
3205
3253
  return violations;
3206
3254
  for (let i = 0; i < input.lines.length; i++) {
3207
3255
  const line = input.lines[i];
3208
3256
  if (line.vatCategory === "E" && line.vatRate !== 0) {
3209
- violations.push(violation("BR-S-06", "error", `Line ${i}: exempt (E) requires vatRate = 0, got ${line.vatRate}.`, `lines[${i}].vatRate`));
3257
+ violations.push(violation("BR-E-05", "error", `Line ${i}: exempt (E) requires vatRate = 0, got ${line.vatRate}.`, `lines[${i}].vatRate`));
3210
3258
  }
3211
3259
  }
3212
3260
  return violations;
3213
3261
  };
3214
- var brS08 = (input) => {
3262
+ var brAe05 = (input) => {
3215
3263
  const violations = [];
3216
3264
  if (!input.lines)
3217
3265
  return violations;
3218
3266
  for (let i = 0; i < input.lines.length; i++) {
3219
3267
  const line = input.lines[i];
3220
3268
  if (line.vatCategory === "AE" && line.vatRate !== 0) {
3221
- violations.push(violation("BR-S-08", "error", `Line ${i}: reverse charge (AE) requires vatRate = 0, got ${line.vatRate}.`, `lines[${i}].vatRate`));
3269
+ violations.push(violation("BR-AE-05", "error", `Line ${i}: reverse charge (AE) requires vatRate = 0, got ${line.vatRate}.`, `lines[${i}].vatRate`));
3222
3270
  }
3223
3271
  }
3224
3272
  return violations;
@@ -3281,11 +3329,11 @@ var ALL_RULES = [
3281
3329
  brCo13,
3282
3330
  brCo15,
3283
3331
  brCo16,
3284
- // Tax categories (BR-S)
3285
- brS01,
3332
+ // Tax categories (one family per category)
3286
3333
  brS05,
3287
- brS06,
3288
- brS08,
3334
+ brZ05,
3335
+ brE05,
3336
+ brAe05,
3289
3337
  // Peppol-specific
3290
3338
  peppolR004,
3291
3339
  vatCategoryCodes,
@@ -3306,10 +3354,31 @@ function validateSchematron(input) {
3306
3354
  }
3307
3355
  return {
3308
3356
  valid: errors.length === 0,
3357
+ coverage: { rulesChecked: SDK_SCHEMATRON_RULE_IDS.length, ofNetworkFatalRules: "partial" },
3309
3358
  errors,
3310
3359
  warnings
3311
3360
  };
3312
3361
  }
3362
+ var SDK_SCHEMATRON_RULE_IDS = [
3363
+ "BR-02",
3364
+ "BR-03",
3365
+ "BR-06",
3366
+ "BR-07",
3367
+ "BR-08",
3368
+ "BR-09",
3369
+ "BR-10",
3370
+ "BR-CL-17",
3371
+ "BR-CO-10",
3372
+ "BR-CO-13",
3373
+ "BR-CO-15",
3374
+ "BR-CO-16",
3375
+ "BR-S-05",
3376
+ "BR-Z-05",
3377
+ "BR-E-05",
3378
+ "BR-AE-05",
3379
+ "PEPPOL-EN16931-R004",
3380
+ "PEPPOL-EN16931-R080"
3381
+ ];
3313
3382
 
3314
3383
  // src/commands/validate.ts
3315
3384
  function runValidation(input) {