ksef-client-ts 0.5.2 → 0.6.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/cli.js CHANGED
@@ -281,8 +281,8 @@ function isRetryableError(error, policy) {
281
281
  if (code && RETRYABLE_ERROR_CODES.has(code)) return true;
282
282
  return false;
283
283
  }
284
- function isRetryableStatus(status6, policy) {
285
- return policy.retryableStatusCodes.includes(status6);
284
+ function isRetryableStatus(status7, policy) {
285
+ return policy.retryableStatusCodes.includes(status7);
286
286
  }
287
287
  function sleep(ms) {
288
288
  return new Promise((resolve2) => setTimeout(resolve2, ms));
@@ -508,9 +508,9 @@ var init_rest_client = __esm({
508
508
  return response;
509
509
  }
510
510
  buildUrl(request) {
511
- const path9 = this.routeBuilder.build(request.path);
511
+ const path10 = this.routeBuilder.build(request.path);
512
512
  const base = this.options.baseUrl;
513
- const url2 = new URL(`${base}${path9}`);
513
+ const url2 = new URL(`${base}${path10}`);
514
514
  const query2 = request.getQuery();
515
515
  for (const [key, value] of query2) {
516
516
  url2.searchParams.append(key, value);
@@ -681,21 +681,21 @@ var init_rest_request = __esm({
681
681
  _query = [];
682
682
  _presigned = false;
683
683
  _skipAuthRetry = false;
684
- constructor(method, path9) {
684
+ constructor(method, path10) {
685
685
  this.method = method;
686
- this.path = path9;
686
+ this.path = path10;
687
687
  }
688
- static get(path9) {
689
- return new _RestRequest("GET", path9);
688
+ static get(path10) {
689
+ return new _RestRequest("GET", path10);
690
690
  }
691
- static post(path9) {
692
- return new _RestRequest("POST", path9);
691
+ static post(path10) {
692
+ return new _RestRequest("POST", path10);
693
693
  }
694
- static put(path9) {
695
- return new _RestRequest("PUT", path9);
694
+ static put(path10) {
695
+ return new _RestRequest("PUT", path10);
696
696
  }
697
- static delete(path9) {
698
- return new _RestRequest("DELETE", path9);
697
+ static delete(path10) {
698
+ return new _RestRequest("DELETE", path10);
699
699
  }
700
700
  body(data) {
701
701
  this._body = data;
@@ -1094,8 +1094,8 @@ var init_session_status = __esm({
1094
1094
  if (filter.dateModifiedFrom) req.query("dateModifiedFrom", filter.dateModifiedFrom);
1095
1095
  if (filter.dateModifiedTo) req.query("dateModifiedTo", filter.dateModifiedTo);
1096
1096
  if (filter.statuses) {
1097
- for (const status6 of filter.statuses) {
1098
- req.query("statuses", status6);
1097
+ for (const status7 of filter.statuses) {
1098
+ req.query("statuses", status7);
1099
1099
  }
1100
1100
  }
1101
1101
  }
@@ -1469,20 +1469,20 @@ var init_lighthouse = __esm({
1469
1469
  this.lighthouseUrl = options.lighthouseUrl;
1470
1470
  this.timeout = options.timeout;
1471
1471
  }
1472
- async fetchJson(path9) {
1472
+ async fetchJson(path10) {
1473
1473
  if (!this.lighthouseUrl) {
1474
1474
  throw new KSeFError(
1475
1475
  "Lighthouse API is not available for the DEMO environment. Use TEST or PROD instead."
1476
1476
  );
1477
1477
  }
1478
- const response = await fetch(`${this.lighthouseUrl}${path9}`, {
1478
+ const response = await fetch(`${this.lighthouseUrl}${path10}`, {
1479
1479
  headers: { Accept: "application/json" },
1480
1480
  signal: AbortSignal.timeout(this.timeout)
1481
1481
  });
1482
1482
  if (!response.ok) {
1483
1483
  const body = await response.text();
1484
1484
  throw new KSeFError(
1485
- `Lighthouse ${path9} failed: HTTP ${response.status} \u2014 ${body}`
1485
+ `Lighthouse ${path10} failed: HTTP ${response.status} \u2014 ${body}`
1486
1486
  );
1487
1487
  }
1488
1488
  return await response.json();
@@ -2087,12 +2087,450 @@ var init_auth_xml_builder = __esm({
2087
2087
  }
2088
2088
  });
2089
2089
 
2090
+ // src/offline/deadline.ts
2091
+ function getDefaultReason(mode) {
2092
+ switch (mode) {
2093
+ case "offline24":
2094
+ return "PLANNED";
2095
+ case "offline":
2096
+ return "SYSTEM_UNAVAILABLE";
2097
+ case "awaryjny":
2098
+ return "EMERGENCY";
2099
+ case "awaria_calkowita":
2100
+ return "TOTAL_FAILURE";
2101
+ }
2102
+ }
2103
+ function nextBusinessDay(from) {
2104
+ const d = new Date(from);
2105
+ d.setUTCDate(d.getUTCDate() + 1);
2106
+ while (d.getUTCDay() === 0 || d.getUTCDay() === 6) {
2107
+ d.setUTCDate(d.getUTCDate() + 1);
2108
+ }
2109
+ return d;
2110
+ }
2111
+ function addBusinessDays(from, days) {
2112
+ if (!Number.isInteger(days) || days < 0) {
2113
+ throw new Error(`days must be a non-negative integer, got ${days}`);
2114
+ }
2115
+ const d = new Date(from);
2116
+ let remaining = days;
2117
+ while (remaining > 0) {
2118
+ d.setUTCDate(d.getUTCDate() + 1);
2119
+ if (d.getUTCDay() !== 0 && d.getUTCDay() !== 6) {
2120
+ remaining--;
2121
+ }
2122
+ }
2123
+ return d;
2124
+ }
2125
+ function endOfDay(d) {
2126
+ const result = new Date(d);
2127
+ result.setUTCHours(23, 59, 59, 999);
2128
+ return result;
2129
+ }
2130
+ function getMaintenanceEndFallback(mw) {
2131
+ if (mw.endTime) {
2132
+ return new Date(mw.endTime);
2133
+ }
2134
+ const start = new Date(mw.startTime);
2135
+ start.setUTCDate(start.getUTCDate() + 7);
2136
+ return start;
2137
+ }
2138
+ function calculateOfflineDeadline(mode, invoiceDate, maintenanceWindow) {
2139
+ const date = typeof invoiceDate === "string" ? new Date(invoiceDate) : invoiceDate;
2140
+ switch (mode) {
2141
+ case "offline24": {
2142
+ return endOfDay(nextBusinessDay(date));
2143
+ }
2144
+ case "offline": {
2145
+ const base = maintenanceWindow ? getMaintenanceEndFallback(maintenanceWindow) : date;
2146
+ return endOfDay(nextBusinessDay(base));
2147
+ }
2148
+ case "awaryjny": {
2149
+ const base = maintenanceWindow ? getMaintenanceEndFallback(maintenanceWindow) : date;
2150
+ return endOfDay(addBusinessDays(base, 7));
2151
+ }
2152
+ case "awaria_calkowita": {
2153
+ return new Date(FAR_FUTURE);
2154
+ }
2155
+ }
2156
+ }
2157
+ function isExpired(submitBy) {
2158
+ const deadline = typeof submitBy === "string" ? new Date(submitBy) : submitBy;
2159
+ return Date.now() > deadline.getTime();
2160
+ }
2161
+ var FAR_FUTURE;
2162
+ var init_deadline = __esm({
2163
+ "src/offline/deadline.ts"() {
2164
+ "use strict";
2165
+ FAR_FUTURE = /* @__PURE__ */ new Date("9999-12-31T23:59:59Z");
2166
+ }
2167
+ });
2168
+
2169
+ // src/models/document-structures/types.ts
2170
+ var SystemCode, FORM_CODES, DEFAULT_FORM_CODE, INVOICE_TYPES_BY_SYSTEM_CODE, FORM_CODE_KEYS;
2171
+ var init_types = __esm({
2172
+ "src/models/document-structures/types.ts"() {
2173
+ "use strict";
2174
+ SystemCode = {
2175
+ FA_2: "FA (2)",
2176
+ FA_3: "FA (3)",
2177
+ PEF_3: "PEF (3)",
2178
+ PEF_KOR_3: "PEF_KOR (3)",
2179
+ FA_RR_1: "FA_RR (1)"
2180
+ };
2181
+ FORM_CODES = {
2182
+ FA_2: { systemCode: "FA (2)", schemaVersion: "1-0E", value: "FA" },
2183
+ FA_3: { systemCode: "FA (3)", schemaVersion: "1-0E", value: "FA" },
2184
+ PEF_3: { systemCode: "PEF (3)", schemaVersion: "2-1", value: "PEF" },
2185
+ PEF_KOR_3: { systemCode: "PEF_KOR (3)", schemaVersion: "2-1", value: "PEF" },
2186
+ FA_RR_1_LEGACY: { systemCode: "FA_RR (1)", schemaVersion: "1-0E", value: "RR" },
2187
+ FA_RR_1_TRANSITION: { systemCode: "FA_RR (1)", schemaVersion: "1-1E", value: "RR" },
2188
+ FA_RR_1: { systemCode: "FA_RR (1)", schemaVersion: "1-1E", value: "FA_RR" }
2189
+ };
2190
+ DEFAULT_FORM_CODE = FORM_CODES.FA_3;
2191
+ INVOICE_TYPES_BY_SYSTEM_CODE = {
2192
+ [SystemCode.FA_2]: ["Vat", "Zal", "Kor", "Roz", "Upr", "KorZal", "KorRoz"],
2193
+ [SystemCode.FA_3]: ["Vat", "Zal", "Kor", "Roz", "Upr", "KorZal", "KorRoz"],
2194
+ [SystemCode.PEF_3]: ["VatPef", "VatPefSp", "KorPef"],
2195
+ [SystemCode.PEF_KOR_3]: ["KorPef"],
2196
+ [SystemCode.FA_RR_1]: ["VatRr", "KorVatRr"]
2197
+ };
2198
+ FORM_CODE_KEYS = {
2199
+ FA2: FORM_CODES.FA_2,
2200
+ FA3: FORM_CODES.FA_3,
2201
+ PEF3: FORM_CODES.PEF_3,
2202
+ PEFKOR3: FORM_CODES.PEF_KOR_3,
2203
+ FARR1: FORM_CODES.FA_RR_1
2204
+ };
2205
+ }
2206
+ });
2207
+
2208
+ // src/models/document-structures/helpers.ts
2209
+ function validateFormCodeForSession(formCode, sessionType) {
2210
+ if (sessionType === "online") return true;
2211
+ return !BATCH_DISALLOWED_SYSTEM_CODES.has(formCode.systemCode);
2212
+ }
2213
+ var SYSTEM_CODE_TO_FORM_CODE, ALL_FORM_CODES, BATCH_DISALLOWED_SYSTEM_CODES;
2214
+ var init_helpers = __esm({
2215
+ "src/models/document-structures/helpers.ts"() {
2216
+ "use strict";
2217
+ init_types();
2218
+ SYSTEM_CODE_TO_FORM_CODE = {
2219
+ [SystemCode.FA_2]: FORM_CODES.FA_2,
2220
+ [SystemCode.FA_3]: FORM_CODES.FA_3,
2221
+ [SystemCode.PEF_3]: FORM_CODES.PEF_3,
2222
+ [SystemCode.PEF_KOR_3]: FORM_CODES.PEF_KOR_3,
2223
+ [SystemCode.FA_RR_1]: FORM_CODES.FA_RR_1
2224
+ };
2225
+ ALL_FORM_CODES = Object.values(FORM_CODES);
2226
+ BATCH_DISALLOWED_SYSTEM_CODES = /* @__PURE__ */ new Set([
2227
+ SystemCode.PEF_3,
2228
+ SystemCode.PEF_KOR_3
2229
+ ]);
2230
+ }
2231
+ });
2232
+
2233
+ // src/models/document-structures/index.ts
2234
+ var init_document_structures = __esm({
2235
+ "src/models/document-structures/index.ts"() {
2236
+ "use strict";
2237
+ init_types();
2238
+ init_helpers();
2239
+ }
2240
+ });
2241
+
2242
+ // src/workflows/offline-invoice-workflow.ts
2243
+ import crypto3 from "crypto";
2244
+ var OfflineInvoiceWorkflow;
2245
+ var init_offline_invoice_workflow = __esm({
2246
+ "src/workflows/offline-invoice-workflow.ts"() {
2247
+ "use strict";
2248
+ init_ksef_api_error();
2249
+ init_deadline();
2250
+ init_document_structures();
2251
+ OfflineInvoiceWorkflow = class {
2252
+ constructor(qrService) {
2253
+ this.qrService = qrService;
2254
+ }
2255
+ async generate(input, options) {
2256
+ if (!input.invoiceXml || input.invoiceXml.trim().length === 0) {
2257
+ throw new Error("invoiceXml must not be empty");
2258
+ }
2259
+ if (!input.invoiceNumber) {
2260
+ throw new Error("invoiceNumber is required");
2261
+ }
2262
+ if (!input.sellerNip) {
2263
+ throw new Error("sellerNip is required");
2264
+ }
2265
+ const mode = options?.mode ?? "offline24";
2266
+ const reason = getDefaultReason(mode);
2267
+ const invoiceHashBase64 = crypto3.createHash("sha256").update(input.invoiceXml).digest("base64");
2268
+ const kod1Url = this.qrService.buildInvoiceVerificationUrl(
2269
+ input.sellerNip,
2270
+ input.invoiceDate,
2271
+ invoiceHashBase64
2272
+ );
2273
+ let kod2Url;
2274
+ if (options?.certificate) {
2275
+ kod2Url = this.qrService.buildCertificateVerificationUrl(
2276
+ input.sellerIdentifier.type,
2277
+ input.sellerIdentifier.value,
2278
+ input.sellerNip,
2279
+ options.certificate.certificateSerial,
2280
+ invoiceHashBase64,
2281
+ options.certificate.privateKeyPem
2282
+ );
2283
+ }
2284
+ const submitBy = options?.customDeadline ? typeof options.customDeadline === "string" ? options.customDeadline : options.customDeadline.toISOString() : calculateOfflineDeadline(mode, input.invoiceDate, options?.maintenanceWindow).toISOString();
2285
+ const metadata = {
2286
+ id: crypto3.randomUUID(),
2287
+ mode,
2288
+ reason,
2289
+ status: "GENERATED",
2290
+ invoiceNumber: input.invoiceNumber,
2291
+ invoiceDate: input.invoiceDate,
2292
+ invoiceXml: input.invoiceXml,
2293
+ sellerNip: input.sellerNip,
2294
+ sellerIdentifier: input.sellerIdentifier,
2295
+ buyerIdentifier: input.buyerIdentifier,
2296
+ totalAmount: input.totalAmount,
2297
+ currency: input.currency,
2298
+ kod1Url,
2299
+ kod2Url,
2300
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
2301
+ submitBy,
2302
+ maintenanceWindowId: options?.maintenanceWindow?.id
2303
+ };
2304
+ if (options?.storage) {
2305
+ await options.storage.save(metadata);
2306
+ }
2307
+ return metadata;
2308
+ }
2309
+ async submit(client, options) {
2310
+ const { storage, checkExpiry = true } = options;
2311
+ let invoices2;
2312
+ if (options.invoiceIds) {
2313
+ invoices2 = [];
2314
+ const notFound = [];
2315
+ for (const id of options.invoiceIds) {
2316
+ const inv = await storage.get(id);
2317
+ if (inv) {
2318
+ invoices2.push(inv);
2319
+ } else {
2320
+ notFound.push(id);
2321
+ }
2322
+ }
2323
+ if (notFound.length > 0) {
2324
+ throw new Error(`Offline invoice(s) not found: ${notFound.join(", ")}`);
2325
+ }
2326
+ } else {
2327
+ invoices2 = await storage.list({ status: ["GENERATED", "QUEUED", "SUBMITTED"] });
2328
+ }
2329
+ const result = {
2330
+ total: invoices2.length,
2331
+ submitted: 0,
2332
+ accepted: 0,
2333
+ rejected: 0,
2334
+ failed: 0,
2335
+ expired: 0,
2336
+ results: []
2337
+ };
2338
+ if (invoices2.length === 0) return result;
2339
+ const pending = [];
2340
+ for (const inv of invoices2) {
2341
+ if (checkExpiry && isExpired(inv.submitBy)) {
2342
+ await storage.update(inv.id, { status: "EXPIRED" });
2343
+ result.expired++;
2344
+ result.results.push({
2345
+ invoiceId: inv.id,
2346
+ invoiceNumber: inv.invoiceNumber,
2347
+ status: "EXPIRED"
2348
+ });
2349
+ } else {
2350
+ pending.push(inv);
2351
+ }
2352
+ }
2353
+ if (pending.length === 0) return result;
2354
+ await client.crypto.init();
2355
+ const encData = client.crypto.getEncryptionData();
2356
+ const formCode = options.formCode ?? DEFAULT_FORM_CODE;
2357
+ const openResp = await client.onlineSession.openSession(
2358
+ { formCode, encryption: encData.encryptionInfo }
2359
+ );
2360
+ const sessionRef = openResp.referenceNumber;
2361
+ try {
2362
+ for (const inv of pending) {
2363
+ await storage.update(inv.id, { status: "QUEUED" });
2364
+ try {
2365
+ const data = new TextEncoder().encode(inv.invoiceXml);
2366
+ const plainMeta = client.crypto.getFileMetadata(data);
2367
+ const encrypted = client.crypto.encryptAES256(data, encData.cipherKey, encData.cipherIv);
2368
+ const encMeta = client.crypto.getFileMetadata(encrypted);
2369
+ await storage.update(inv.id, { status: "SUBMITTED", submittedAt: (/* @__PURE__ */ new Date()).toISOString() });
2370
+ const resp = await client.onlineSession.sendInvoice(sessionRef, {
2371
+ invoiceHash: plainMeta.hashSHA,
2372
+ invoiceSize: plainMeta.fileSize,
2373
+ encryptedInvoiceHash: encMeta.hashSHA,
2374
+ encryptedInvoiceSize: encMeta.fileSize,
2375
+ encryptedInvoiceContent: Buffer.from(encrypted).toString("base64"),
2376
+ offlineMode: true
2377
+ });
2378
+ await storage.update(inv.id, {
2379
+ status: "ACCEPTED",
2380
+ acceptedAt: (/* @__PURE__ */ new Date()).toISOString(),
2381
+ ksefReferenceNumber: resp.referenceNumber
2382
+ });
2383
+ result.submitted++;
2384
+ result.accepted++;
2385
+ result.results.push({
2386
+ invoiceId: inv.id,
2387
+ invoiceNumber: inv.invoiceNumber,
2388
+ status: "ACCEPTED",
2389
+ ksefReferenceNumber: resp.referenceNumber
2390
+ });
2391
+ } catch (err) {
2392
+ const message = err instanceof Error ? err.message : String(err);
2393
+ if (err instanceof KSeFApiError) {
2394
+ await storage.update(inv.id, {
2395
+ status: "REJECTED",
2396
+ error: { code: err.statusCode, message }
2397
+ });
2398
+ result.submitted++;
2399
+ result.rejected++;
2400
+ result.results.push({
2401
+ invoiceId: inv.id,
2402
+ invoiceNumber: inv.invoiceNumber,
2403
+ status: "REJECTED",
2404
+ error: { code: err.statusCode, message }
2405
+ });
2406
+ } else {
2407
+ await storage.update(inv.id, {
2408
+ status: "QUEUED",
2409
+ error: { code: 0, message }
2410
+ });
2411
+ result.failed++;
2412
+ result.results.push({
2413
+ invoiceId: inv.id,
2414
+ invoiceNumber: inv.invoiceNumber,
2415
+ status: "QUEUED",
2416
+ error: { code: 0, message }
2417
+ });
2418
+ }
2419
+ }
2420
+ }
2421
+ } finally {
2422
+ try {
2423
+ await client.onlineSession.closeSession(sessionRef);
2424
+ } catch {
2425
+ }
2426
+ }
2427
+ return result;
2428
+ }
2429
+ // TODO(perf): Each correction opens a separate KSeF session.
2430
+ // For batch corrections, consider a correctBatch() sharing one session (like submit()).
2431
+ async correct(client, options) {
2432
+ const { storage, rejectedInvoiceId, correctedInvoiceXml } = options;
2433
+ const original = await storage.get(rejectedInvoiceId);
2434
+ if (!original) {
2435
+ throw new Error(`Offline invoice not found: ${rejectedInvoiceId}`);
2436
+ }
2437
+ if (original.status !== "REJECTED") {
2438
+ throw new Error(
2439
+ original.status === "CORRECTED" ? `Invoice ${rejectedInvoiceId} has already been corrected (by ${original.correctedBy})` : `Only rejected invoices can be corrected (current status: ${original.status})`
2440
+ );
2441
+ }
2442
+ const originalHash = crypto3.createHash("sha256").update(original.invoiceXml).digest("base64");
2443
+ await client.crypto.init();
2444
+ const encData = client.crypto.getEncryptionData();
2445
+ const formCode = options.formCode ?? DEFAULT_FORM_CODE;
2446
+ const openResp = await client.onlineSession.openSession(
2447
+ { formCode, encryption: encData.encryptionInfo }
2448
+ );
2449
+ const sessionRef = openResp.referenceNumber;
2450
+ try {
2451
+ const data = new TextEncoder().encode(correctedInvoiceXml);
2452
+ const plainMeta = client.crypto.getFileMetadata(data);
2453
+ const encrypted = client.crypto.encryptAES256(data, encData.cipherKey, encData.cipherIv);
2454
+ const encMeta = client.crypto.getFileMetadata(encrypted);
2455
+ const correctionId = crypto3.randomUUID();
2456
+ const submittedAt = (/* @__PURE__ */ new Date()).toISOString();
2457
+ const correctedHashBase64 = crypto3.createHash("sha256").update(correctedInvoiceXml).digest("base64");
2458
+ const kod1Url = this.qrService.buildInvoiceVerificationUrl(
2459
+ original.sellerNip,
2460
+ original.invoiceDate,
2461
+ correctedHashBase64
2462
+ );
2463
+ let kod2Url;
2464
+ if (options.certificate) {
2465
+ kod2Url = this.qrService.buildCertificateVerificationUrl(
2466
+ original.sellerIdentifier.type,
2467
+ original.sellerIdentifier.value,
2468
+ original.sellerNip,
2469
+ options.certificate.certificateSerial,
2470
+ correctedHashBase64,
2471
+ options.certificate.privateKeyPem
2472
+ );
2473
+ }
2474
+ const correctionMetadata = {
2475
+ id: correctionId,
2476
+ mode: original.mode,
2477
+ reason: original.reason,
2478
+ status: "SUBMITTED",
2479
+ invoiceNumber: original.invoiceNumber,
2480
+ invoiceDate: original.invoiceDate,
2481
+ invoiceXml: correctedInvoiceXml,
2482
+ sellerNip: original.sellerNip,
2483
+ sellerIdentifier: original.sellerIdentifier,
2484
+ buyerIdentifier: original.buyerIdentifier,
2485
+ kod1Url,
2486
+ kod2Url,
2487
+ generatedAt: submittedAt,
2488
+ submitBy: original.submitBy,
2489
+ submittedAt,
2490
+ correctedInvoiceId: rejectedInvoiceId
2491
+ };
2492
+ await storage.save(correctionMetadata);
2493
+ const resp = await client.onlineSession.sendInvoice(sessionRef, {
2494
+ invoiceHash: plainMeta.hashSHA,
2495
+ invoiceSize: plainMeta.fileSize,
2496
+ encryptedInvoiceHash: encMeta.hashSHA,
2497
+ encryptedInvoiceSize: encMeta.fileSize,
2498
+ encryptedInvoiceContent: Buffer.from(encrypted).toString("base64"),
2499
+ offlineMode: true,
2500
+ hashOfCorrectedInvoice: originalHash
2501
+ });
2502
+ await storage.update(correctionId, {
2503
+ status: "ACCEPTED",
2504
+ acceptedAt: (/* @__PURE__ */ new Date()).toISOString(),
2505
+ ksefReferenceNumber: resp.referenceNumber
2506
+ });
2507
+ await storage.update(rejectedInvoiceId, {
2508
+ status: "CORRECTED",
2509
+ correctedBy: correctionId
2510
+ });
2511
+ return {
2512
+ invoiceId: correctionId,
2513
+ invoiceNumber: correctionMetadata.invoiceNumber,
2514
+ status: "ACCEPTED",
2515
+ ksefReferenceNumber: resp.referenceNumber
2516
+ };
2517
+ } finally {
2518
+ try {
2519
+ await client.onlineSession.closeSession(sessionRef);
2520
+ } catch {
2521
+ }
2522
+ }
2523
+ }
2524
+ };
2525
+ }
2526
+ });
2527
+
2090
2528
  // src/crypto/signature-service.ts
2091
2529
  var signature_service_exports = {};
2092
2530
  __export(signature_service_exports, {
2093
2531
  SignatureService: () => SignatureService
2094
2532
  });
2095
- import * as crypto3 from "crypto";
2533
+ import * as crypto4 from "crypto";
2096
2534
  import { ExclusiveCanonicalization } from "xml-crypto";
2097
2535
  import { DOMParser, XMLSerializer } from "@xmldom/xmldom";
2098
2536
  function extractDerFromPem(pem) {
@@ -2112,7 +2550,7 @@ function canonicalize(elem) {
2112
2550
  function computeRootDigest(doc) {
2113
2551
  const root = doc.documentElement;
2114
2552
  const canonical = canonicalize(root);
2115
- return crypto3.createHash("sha256").update(canonical, "utf-8").digest("base64");
2553
+ return crypto4.createHash("sha256").update(canonical, "utf-8").digest("base64");
2116
2554
  }
2117
2555
  function computeSignedPropertiesDigest(qualifyingPropertiesXml) {
2118
2556
  const parser = new DOMParser();
@@ -2122,7 +2560,7 @@ function computeSignedPropertiesDigest(qualifyingPropertiesXml) {
2122
2560
  throw new Error("SignedProperties element not found in QualifyingProperties");
2123
2561
  }
2124
2562
  const canonical = canonicalize(signedProps);
2125
- return crypto3.createHash("sha256").update(canonical, "utf-8").digest("base64");
2563
+ return crypto4.createHash("sha256").update(canonical, "utf-8").digest("base64");
2126
2564
  }
2127
2565
  function buildSignedInfo(signatureAlgorithm, rootDigest, signedPropertiesDigest) {
2128
2566
  return [
@@ -2153,12 +2591,12 @@ function computeSignatureValue(canonicalSignedInfo, privateKey, isEc) {
2153
2591
  const data = Buffer.from(canonicalSignedInfo, "utf-8");
2154
2592
  let signature;
2155
2593
  if (isEc) {
2156
- signature = crypto3.sign("sha256", data, {
2594
+ signature = crypto4.sign("sha256", data, {
2157
2595
  key: privateKey,
2158
2596
  dsaEncoding: "ieee-p1363"
2159
2597
  });
2160
2598
  } else {
2161
- signature = crypto3.sign("sha256", data, privateKey);
2599
+ signature = crypto4.sign("sha256", data, privateKey);
2162
2600
  }
2163
2601
  return signature.toString("base64");
2164
2602
  }
@@ -2258,11 +2696,11 @@ var init_signature_service = __esm({
2258
2696
  }
2259
2697
  const certDer = extractDerFromPem(certPem);
2260
2698
  const certBase64 = certDer.toString("base64");
2261
- const certDigest = crypto3.createHash("sha256").update(certDer).digest("base64");
2262
- const x5093 = new crypto3.X509Certificate(certPem);
2699
+ const certDigest = crypto4.createHash("sha256").update(certDer).digest("base64");
2700
+ const x5093 = new crypto4.X509Certificate(certPem);
2263
2701
  const issuerName = normalizeIssuerDn(x5093.issuer);
2264
2702
  const serialNumber = hexSerialToDecimal(x5093.serialNumber);
2265
- const privateKey = crypto3.createPrivateKey(
2703
+ const privateKey = crypto4.createPrivateKey(
2266
2704
  passphrase ? { key: privateKeyPem, format: "pem", passphrase } : privateKeyPem
2267
2705
  );
2268
2706
  const isEc = privateKey.asymmetricKeyType === "ec";
@@ -2427,6 +2865,7 @@ var init_client = __esm({
2427
2865
  init_cryptography_service();
2428
2866
  init_verification_link_service();
2429
2867
  init_auth_xml_builder();
2868
+ init_offline_invoice_workflow();
2430
2869
  KSeFClient = class {
2431
2870
  auth;
2432
2871
  activeSessions;
@@ -2445,6 +2884,7 @@ var init_client = __esm({
2445
2884
  qr;
2446
2885
  options;
2447
2886
  authManager;
2887
+ _offline;
2448
2888
  constructor(options) {
2449
2889
  this.options = resolveOptions(options);
2450
2890
  const authManager = options?.authManager ?? new DefaultAuthManager(async () => {
@@ -2473,6 +2913,12 @@ var init_client = __esm({
2473
2913
  this.testData = new TestDataService(restClient, this.options.environmentName);
2474
2914
  this.qr = new VerificationLinkService(this.options.baseQrUrl);
2475
2915
  }
2916
+ get offline() {
2917
+ if (!this._offline) {
2918
+ this._offline = new OfflineInvoiceWorkflow(this.qr);
2919
+ }
2920
+ return this._offline;
2921
+ }
2476
2922
  async loginWithToken(token, nip) {
2477
2923
  const challenge2 = await this.auth.getChallenge();
2478
2924
  await this.crypto.init();
@@ -2507,10 +2953,10 @@ var init_client = __esm({
2507
2953
  }
2508
2954
  async awaitAuthReady(referenceNumber, authToken) {
2509
2955
  for (let i = 0; i < 30; i++) {
2510
- const status6 = await this.auth.getAuthStatus(referenceNumber, authToken);
2511
- if (status6.status.code === 200) return;
2512
- if (status6.status.code !== 100) {
2513
- throw new Error(`Authentication failed with status ${status6.status.code}: ${status6.status.description}`);
2956
+ const status7 = await this.auth.getAuthStatus(referenceNumber, authToken);
2957
+ if (status7.status.code === 200) return;
2958
+ if (status7.status.code !== 100) {
2959
+ throw new Error(`Authentication failed with status ${status7.status.code}: ${status7.status.description}`);
2514
2960
  }
2515
2961
  await new Promise((r) => setTimeout(r, 1e3));
2516
2962
  }
@@ -2545,79 +2991,6 @@ var init_polling = __esm({
2545
2991
  }
2546
2992
  });
2547
2993
 
2548
- // src/models/document-structures/types.ts
2549
- var SystemCode, FORM_CODES, DEFAULT_FORM_CODE, INVOICE_TYPES_BY_SYSTEM_CODE, FORM_CODE_KEYS;
2550
- var init_types = __esm({
2551
- "src/models/document-structures/types.ts"() {
2552
- "use strict";
2553
- SystemCode = {
2554
- FA_2: "FA (2)",
2555
- FA_3: "FA (3)",
2556
- PEF_3: "PEF (3)",
2557
- PEF_KOR_3: "PEF_KOR (3)",
2558
- FA_RR_1: "FA_RR (1)"
2559
- };
2560
- FORM_CODES = {
2561
- FA_2: { systemCode: "FA (2)", schemaVersion: "1-0E", value: "FA" },
2562
- FA_3: { systemCode: "FA (3)", schemaVersion: "1-0E", value: "FA" },
2563
- PEF_3: { systemCode: "PEF (3)", schemaVersion: "2-1", value: "PEF" },
2564
- PEF_KOR_3: { systemCode: "PEF_KOR (3)", schemaVersion: "2-1", value: "PEF" },
2565
- FA_RR_1_LEGACY: { systemCode: "FA_RR (1)", schemaVersion: "1-0E", value: "RR" },
2566
- FA_RR_1_TRANSITION: { systemCode: "FA_RR (1)", schemaVersion: "1-1E", value: "RR" },
2567
- FA_RR_1: { systemCode: "FA_RR (1)", schemaVersion: "1-1E", value: "FA_RR" }
2568
- };
2569
- DEFAULT_FORM_CODE = FORM_CODES.FA_3;
2570
- INVOICE_TYPES_BY_SYSTEM_CODE = {
2571
- [SystemCode.FA_2]: ["Vat", "Zal", "Kor", "Roz", "Upr", "KorZal", "KorRoz"],
2572
- [SystemCode.FA_3]: ["Vat", "Zal", "Kor", "Roz", "Upr", "KorZal", "KorRoz"],
2573
- [SystemCode.PEF_3]: ["VatPef", "VatPefSp", "KorPef"],
2574
- [SystemCode.PEF_KOR_3]: ["KorPef"],
2575
- [SystemCode.FA_RR_1]: ["VatRr", "KorVatRr"]
2576
- };
2577
- FORM_CODE_KEYS = {
2578
- FA2: FORM_CODES.FA_2,
2579
- FA3: FORM_CODES.FA_3,
2580
- PEF3: FORM_CODES.PEF_3,
2581
- PEFKOR3: FORM_CODES.PEF_KOR_3,
2582
- FARR1: FORM_CODES.FA_RR_1
2583
- };
2584
- }
2585
- });
2586
-
2587
- // src/models/document-structures/helpers.ts
2588
- function validateFormCodeForSession(formCode, sessionType) {
2589
- if (sessionType === "online") return true;
2590
- return !BATCH_DISALLOWED_SYSTEM_CODES.has(formCode.systemCode);
2591
- }
2592
- var SYSTEM_CODE_TO_FORM_CODE, ALL_FORM_CODES, BATCH_DISALLOWED_SYSTEM_CODES;
2593
- var init_helpers = __esm({
2594
- "src/models/document-structures/helpers.ts"() {
2595
- "use strict";
2596
- init_types();
2597
- SYSTEM_CODE_TO_FORM_CODE = {
2598
- [SystemCode.FA_2]: FORM_CODES.FA_2,
2599
- [SystemCode.FA_3]: FORM_CODES.FA_3,
2600
- [SystemCode.PEF_3]: FORM_CODES.PEF_3,
2601
- [SystemCode.PEF_KOR_3]: FORM_CODES.PEF_KOR_3,
2602
- [SystemCode.FA_RR_1]: FORM_CODES.FA_RR_1
2603
- };
2604
- ALL_FORM_CODES = Object.values(FORM_CODES);
2605
- BATCH_DISALLOWED_SYSTEM_CODES = /* @__PURE__ */ new Set([
2606
- SystemCode.PEF_3,
2607
- SystemCode.PEF_KOR_3
2608
- ]);
2609
- }
2610
- });
2611
-
2612
- // src/models/document-structures/index.ts
2613
- var init_document_structures = __esm({
2614
- "src/models/document-structures/index.ts"() {
2615
- "use strict";
2616
- init_types();
2617
- init_helpers();
2618
- }
2619
- });
2620
-
2621
2994
  // src/xml/upo-parser.ts
2622
2995
  import { XMLParser } from "fast-xml-parser";
2623
2996
  function isRecord(value) {
@@ -2742,11 +3115,58 @@ var init_upo_parser = __esm({
2742
3115
  }
2743
3116
  });
2744
3117
 
3118
+ // src/xml/invoice-field-extractor.ts
3119
+ import { XMLParser as XMLParser2 } from "fast-xml-parser";
3120
+ function extractInvoiceFields(xml) {
3121
+ const parsed = invoiceParser.parse(xml);
3122
+ const root = parsed?.Faktura;
3123
+ if (!root || typeof root !== "object") {
3124
+ throw new Error(
3125
+ "Cannot extract invoice fields: missing <Faktura> root element. Ensure the XML is a valid KSeF invoice (FA_2, FA_3, or FA_RR)."
3126
+ );
3127
+ }
3128
+ if (root.Fa && typeof root.Fa === "object") {
3129
+ const invoiceDate = nonEmptyString(root.Fa.P_1);
3130
+ const invoiceNumber = nonEmptyString(root.Fa.P_2);
3131
+ if (invoiceDate && invoiceNumber) {
3132
+ return { invoiceNumber, invoiceDate };
3133
+ }
3134
+ }
3135
+ if (root.FakturaRR && typeof root.FakturaRR === "object") {
3136
+ const invoiceDate = nonEmptyString(root.FakturaRR.P_4B);
3137
+ const invoiceNumber = nonEmptyString(root.FakturaRR.P_4C);
3138
+ if (invoiceDate && invoiceNumber) {
3139
+ return { invoiceNumber, invoiceDate };
3140
+ }
3141
+ }
3142
+ throw new Error(
3143
+ "Cannot extract invoice number and date from XML. Expected <Fa><P_1>/<P_2> (FA format) or <FakturaRR><P_4B>/<P_4C> (RR format)."
3144
+ );
3145
+ }
3146
+ function nonEmptyString(value) {
3147
+ return typeof value === "string" && value.length > 0 ? value : void 0;
3148
+ }
3149
+ var invoiceParser;
3150
+ var init_invoice_field_extractor = __esm({
3151
+ "src/xml/invoice-field-extractor.ts"() {
3152
+ "use strict";
3153
+ invoiceParser = new XMLParser2({
3154
+ ignoreAttributes: false,
3155
+ attributeNamePrefix: "@_",
3156
+ parseTagValue: false,
3157
+ parseAttributeValue: false,
3158
+ removeNSPrefix: true,
3159
+ trimValues: false
3160
+ });
3161
+ }
3162
+ });
3163
+
2745
3164
  // src/xml/index.ts
2746
3165
  var init_xml = __esm({
2747
3166
  "src/xml/index.ts"() {
2748
3167
  "use strict";
2749
3168
  init_upo_parser();
3169
+ init_invoice_field_extractor();
2750
3170
  }
2751
3171
  });
2752
3172
 
@@ -4488,11 +4908,11 @@ async function validateSchema(xml, options, _parsed) {
4488
4908
  const prefix = rootElement ? `/${rootElement}/` : "/";
4489
4909
  const validationErrors = result.error.issues.map((issue) => {
4490
4910
  const zodPath = issue.path.join("/");
4491
- const path9 = zodPath ? `${prefix}${zodPath}` : rootElement ? `/${rootElement}` : void 0;
4911
+ const path10 = zodPath ? `${prefix}${zodPath}` : rootElement ? `/${rootElement}` : void 0;
4492
4912
  return {
4493
4913
  code: mapZodErrorCode(issue),
4494
4914
  message: issue.message,
4495
- path: path9
4915
+ path: path10
4496
4916
  };
4497
4917
  });
4498
4918
  return { valid: false, schemaType, errors: validationErrors };
@@ -4532,9 +4952,9 @@ function validateBusinessRules(xml, _parsed) {
4532
4952
  collectDateErrors(object, rootElement, errors);
4533
4953
  return { valid: errors.length === 0, schemaType, errors };
4534
4954
  }
4535
- function collectNipPeselErrors(obj, path9, errors) {
4955
+ function collectNipPeselErrors(obj, path10, errors) {
4536
4956
  for (const [key, value] of Object.entries(obj)) {
4537
- const currentPath = path9 ? `${path9}/${key}` : key;
4957
+ const currentPath = path10 ? `${path10}/${key}` : key;
4538
4958
  if (key === "NIP" && typeof value === "string") {
4539
4959
  if (!isValidNip(value)) {
4540
4960
  errors.push({
@@ -4614,7 +5034,7 @@ var init_invoice_validator = __esm({
4614
5034
  });
4615
5035
 
4616
5036
  // src/builders/batch-file.ts
4617
- import * as crypto4 from "crypto";
5037
+ import * as crypto5 from "crypto";
4618
5038
  function splitBuffer(data, maxPartSize) {
4619
5039
  if (data.length <= maxPartSize) {
4620
5040
  return [data];
@@ -4626,7 +5046,7 @@ function splitBuffer(data, maxPartSize) {
4626
5046
  return parts;
4627
5047
  }
4628
5048
  function sha256Base64(data) {
4629
- return crypto4.createHash("sha256").update(data).digest("base64");
5049
+ return crypto5.createHash("sha256").update(data).digest("base64");
4630
5050
  }
4631
5051
  var BATCH_MAX_PART_SIZE, BATCH_MAX_TOTAL_SIZE, BATCH_MAX_PARTS, BatchFileBuilder;
4632
5052
  var init_batch_file = __esm({
@@ -4937,10 +5357,10 @@ var init_batch_session_workflow = __esm({
4937
5357
  });
4938
5358
 
4939
5359
  // src/cli/index.ts
4940
- import { readFileSync as readFileSync8 } from "fs";
5360
+ import { readFileSync as readFileSync9 } from "fs";
4941
5361
  import { fileURLToPath } from "url";
4942
5362
  import { dirname as dirname2, resolve } from "path";
4943
- import { defineCommand as defineCommand17, runMain } from "citty";
5363
+ import { defineCommand as defineCommand18, runMain } from "citty";
4944
5364
 
4945
5365
  // src/cli/commands/config.ts
4946
5366
  import { defineCommand } from "citty";
@@ -5506,13 +5926,13 @@ var login = defineCommand2({
5506
5926
  if (token) {
5507
5927
  await client.loginWithToken(token, nip);
5508
5928
  } else if (args.p12) {
5509
- const fs13 = await import("fs");
5510
- const p12Buffer = fs13.readFileSync(args.p12);
5929
+ const fs15 = await import("fs");
5930
+ const p12Buffer = fs15.readFileSync(args.p12);
5511
5931
  await client.loginWithPkcs12(p12Buffer, args["p12-password"] ?? "", nip);
5512
5932
  } else if (args.cert && args.key) {
5513
- const fs13 = await import("fs");
5514
- const certPem = fs13.readFileSync(args.cert, "utf-8");
5515
- const keyPem = fs13.readFileSync(args.key, "utf-8");
5933
+ const fs15 = await import("fs");
5934
+ const certPem = fs15.readFileSync(args.cert, "utf-8");
5935
+ const keyPem = fs15.readFileSync(args.key, "utf-8");
5516
5936
  await client.loginWithCertificate(certPem, keyPem, nip, args["key-password"]);
5517
5937
  } else {
5518
5938
  throw new Error("Provide --token, --p12, or both --cert and --key for authentication.");
@@ -7616,12 +8036,12 @@ import fs10 from "fs";
7616
8036
  import path7 from "path";
7617
8037
 
7618
8038
  // src/crypto/certificate-service.ts
7619
- import * as crypto5 from "crypto";
8039
+ import * as crypto6 from "crypto";
7620
8040
  import * as x5092 from "@peculiar/x509";
7621
8041
  var CertificateService = class {
7622
8042
  static getSha256Fingerprint(certPem) {
7623
8043
  const der = extractDerFromPem2(certPem);
7624
- return crypto5.createHash("sha256").update(der).digest("hex").toUpperCase();
8044
+ return crypto6.createHash("sha256").update(der).digest("hex").toUpperCase();
7625
8045
  }
7626
8046
  static async generatePersonalCertificate(givenName, surname, serialNumber, commonName, method = "RSA") {
7627
8047
  const nameParts = [];
@@ -7644,7 +8064,7 @@ var CertificateService = class {
7644
8064
  }
7645
8065
  };
7646
8066
  async function generateSelfSigned(subject2, method) {
7647
- x5092.cryptoProvider.set(crypto5.webcrypto);
8067
+ x5092.cryptoProvider.set(crypto6.webcrypto);
7648
8068
  let algorithm;
7649
8069
  let signingAlgorithm;
7650
8070
  if (method === "ECDSA") {
@@ -7659,7 +8079,7 @@ async function generateSelfSigned(subject2, method) {
7659
8079
  };
7660
8080
  signingAlgorithm = algorithm;
7661
8081
  }
7662
- const keys = await crypto5.webcrypto.subtle.generateKey(
8082
+ const keys = await crypto6.webcrypto.subtle.generateKey(
7663
8083
  algorithm,
7664
8084
  true,
7665
8085
  ["sign", "verify"]
@@ -7676,9 +8096,9 @@ async function generateSelfSigned(subject2, method) {
7676
8096
  serialNumber: Date.now().toString(16)
7677
8097
  });
7678
8098
  const certPem = cert.toString("pem");
7679
- const pkcs8 = await crypto5.webcrypto.subtle.exportKey("pkcs8", keys.privateKey);
8099
+ const pkcs8 = await crypto6.webcrypto.subtle.exportKey("pkcs8", keys.privateKey);
7680
8100
  const privateKeyPem = pemEncode2(new Uint8Array(pkcs8), "PRIVATE KEY");
7681
- const fingerprint = crypto5.createHash("sha256").update(Buffer.from(cert.rawData)).digest("hex").toUpperCase();
8101
+ const fingerprint = crypto6.createHash("sha256").update(Buffer.from(cert.rawData)).digest("hex").toUpperCase();
7682
8102
  return { certificatePem: certPem, privateKeyPem, fingerprint };
7683
8103
  }
7684
8104
  function extractDerFromPem2(pem) {
@@ -8136,6 +8556,7 @@ var invoice2 = defineCommand9({
8136
8556
  format: { type: "string", description: "Output format: png or svg (default: png)" },
8137
8557
  size: { type: "string", description: "QR code size in pixels (default: 300)" },
8138
8558
  label: { type: "string", description: "Label text (SVG only)" },
8559
+ offline: { type: "boolean", description: 'Use "OFFLINE" as label (SVG, overrides --label)' },
8139
8560
  o: { type: "string", description: "Output file path" },
8140
8561
  env: { type: "string", description: "Environment (test/demo/prod)" },
8141
8562
  json: { type: "boolean", description: "Output as JSON" },
@@ -8155,7 +8576,8 @@ var invoice2 = defineCommand9({
8155
8576
  return;
8156
8577
  }
8157
8578
  if (format === "svg") {
8158
- const svg = args.label ? await QrCodeService.generateQrCodeSvgWithLabel(url2, args.label, { width: size }) : await QrCodeService.generateQrCodeSvg(url2, { width: size });
8579
+ const effectiveLabel = args.offline ? "OFFLINE" : args.label;
8580
+ const svg = effectiveLabel ? await QrCodeService.generateQrCodeSvgWithLabel(url2, effectiveLabel, { width: size }) : await QrCodeService.generateQrCodeSvg(url2, { width: size });
8159
8581
  if (args.o) {
8160
8582
  fs11.writeFileSync(args.o, svg);
8161
8583
  outputSuccess(`QR code saved to ${args.o}
@@ -9022,11 +9444,11 @@ var doctorCommand = defineCommand14({
9022
9444
  const controller = new AbortController();
9023
9445
  const timeout = setTimeout(() => controller.abort(), 5e3);
9024
9446
  try {
9025
- const status6 = await client.lighthouse.getStatus();
9447
+ const status7 = await client.lighthouse.getStatus();
9026
9448
  checks.push({
9027
9449
  name: "connectivity",
9028
9450
  status: "pass",
9029
- message: `API reachable (${status6.status})`
9451
+ message: `API reachable (${status7.status})`
9030
9452
  });
9031
9453
  } finally {
9032
9454
  clearTimeout(timeout);
@@ -9470,11 +9892,474 @@ var setupCommand = defineCommand16({
9470
9892
  }
9471
9893
  });
9472
9894
 
9895
+ // src/cli/commands/offline.ts
9896
+ import * as fs14 from "fs";
9897
+ import { defineCommand as defineCommand17 } from "citty";
9898
+ import { consola as consola15 } from "consola";
9899
+
9900
+ // src/offline/file-storage.ts
9901
+ import * as fs13 from "fs/promises";
9902
+ import * as path9 from "path";
9903
+ import * as os6 from "os";
9904
+
9905
+ // src/offline/storage.ts
9906
+ function matchesFilter(invoice3, filter) {
9907
+ if (filter.status !== void 0) {
9908
+ const statuses = Array.isArray(filter.status) ? filter.status : [filter.status];
9909
+ if (!statuses.includes(invoice3.status)) return false;
9910
+ }
9911
+ if (filter.mode !== void 0 && invoice3.mode !== filter.mode) return false;
9912
+ if (filter.sellerNip !== void 0 && invoice3.sellerNip !== filter.sellerNip) return false;
9913
+ if (filter.expiringBefore !== void 0) {
9914
+ const cutoff = typeof filter.expiringBefore === "string" ? new Date(filter.expiringBefore).getTime() : filter.expiringBefore.getTime();
9915
+ if (new Date(invoice3.submitBy).getTime() >= cutoff) return false;
9916
+ }
9917
+ return true;
9918
+ }
9919
+
9920
+ // src/offline/file-storage.ts
9921
+ function resolveDir(dir) {
9922
+ if (dir === "~" || dir.startsWith("~/")) {
9923
+ return path9.join(os6.homedir(), dir.slice(1));
9924
+ }
9925
+ return dir;
9926
+ }
9927
+ var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
9928
+ function validateId(id) {
9929
+ if (!UUID_RE.test(id)) {
9930
+ throw new Error(`Invalid invoice ID: ${id}`);
9931
+ }
9932
+ }
9933
+ var FileOfflineInvoiceStorage = class {
9934
+ dir;
9935
+ constructor(directory) {
9936
+ this.dir = resolveDir(directory ?? "~/.ksef/offline");
9937
+ }
9938
+ async ensureDir() {
9939
+ await fs13.mkdir(this.dir, { recursive: true });
9940
+ }
9941
+ filePath(id) {
9942
+ validateId(id);
9943
+ return path9.join(this.dir, `${id}.json`);
9944
+ }
9945
+ async save(invoice3) {
9946
+ await this.ensureDir();
9947
+ const file = this.filePath(invoice3.id);
9948
+ const tmp = `${file}.tmp`;
9949
+ await fs13.writeFile(tmp, JSON.stringify(invoice3, null, 2));
9950
+ await fs13.rename(tmp, file);
9951
+ }
9952
+ async get(id) {
9953
+ const file = this.filePath(id);
9954
+ try {
9955
+ return JSON.parse(await fs13.readFile(file, "utf-8"));
9956
+ } catch (err) {
9957
+ if (err instanceof Error && "code" in err && err.code === "ENOENT") {
9958
+ return null;
9959
+ }
9960
+ console.warn(`Warning: Failed to read offline invoice ${id}: ${err instanceof Error ? err.message : String(err)}`);
9961
+ return null;
9962
+ }
9963
+ }
9964
+ async list(filter) {
9965
+ let files;
9966
+ try {
9967
+ files = (await fs13.readdir(this.dir)).filter((f) => f.endsWith(".json"));
9968
+ } catch {
9969
+ return [];
9970
+ }
9971
+ const results = [];
9972
+ for (const file of files) {
9973
+ try {
9974
+ const data = JSON.parse(
9975
+ await fs13.readFile(path9.join(this.dir, file), "utf-8")
9976
+ );
9977
+ if (!filter || matchesFilter(data, filter)) {
9978
+ results.push(data);
9979
+ }
9980
+ } catch (err) {
9981
+ console.warn(`Warning: Skipping corrupt offline invoice file ${file}: ${err instanceof Error ? err.message : String(err)}`);
9982
+ }
9983
+ }
9984
+ return results;
9985
+ }
9986
+ /**
9987
+ * Update invoice metadata (read-modify-write).
9988
+ *
9989
+ * Note: No file locking — concurrent updates to the same ID may cause
9990
+ * lost writes. Acceptable for CLI (single process). Library consumers
9991
+ * running parallel operations should use external locking.
9992
+ */
9993
+ async update(id, updates) {
9994
+ const existing = await this.get(id);
9995
+ if (!existing) throw new Error(`Offline invoice not found: ${id}`);
9996
+ await this.save({ ...existing, ...updates });
9997
+ }
9998
+ async delete(id) {
9999
+ const file = this.filePath(id);
10000
+ try {
10001
+ await fs13.unlink(file);
10002
+ } catch (e) {
10003
+ if (e instanceof Error && "code" in e && e.code !== "ENOENT") throw e;
10004
+ }
10005
+ }
10006
+ };
10007
+
10008
+ // src/cli/commands/offline.ts
10009
+ init_invoice_field_extractor();
10010
+ function getGlobalOpts14(args) {
10011
+ return {
10012
+ env: args.env,
10013
+ json: args.json,
10014
+ verbose: args.verbose,
10015
+ timeout: args.timeout,
10016
+ nip: args.nip
10017
+ };
10018
+ }
10019
+ function getStorage(args) {
10020
+ return new FileOfflineInvoiceStorage(args["store-dir"]);
10021
+ }
10022
+ var globalArgs = {
10023
+ env: { type: "string", description: "Environment (test/demo/prod)" },
10024
+ json: { type: "boolean", description: "Output as JSON" },
10025
+ verbose: { type: "boolean", description: "Verbose output" },
10026
+ timeout: { type: "string", description: "Request timeout (ms)" },
10027
+ nip: { type: "string", description: "NIP number" },
10028
+ "store-dir": { type: "string", description: "Offline invoice store directory" }
10029
+ };
10030
+ var generate3 = defineCommand17({
10031
+ meta: { name: "generate", description: "Generate offline invoice metadata with QR codes" },
10032
+ args: {
10033
+ ...globalArgs,
10034
+ xml: { type: "positional", description: "Invoice XML file path", required: true },
10035
+ mode: { type: "string", description: "Offline mode (offline24/offline/awaryjny)" },
10036
+ key: { type: "string", description: "Private key PEM file for KOD II signing" },
10037
+ "cert-serial": { type: "string", description: "Certificate serial number (hex)" },
10038
+ "context-type": { type: "string", description: "Seller context type (default: Nip)" },
10039
+ "context-id": { type: "string", description: "Seller context value" },
10040
+ "qr-format": { type: "string", description: "QR format: png or svg (default: png)" },
10041
+ "qr-out": { type: "string", description: "Directory to save QR images" },
10042
+ "no-store": { type: "boolean", description: "Do not save to local store" }
10043
+ },
10044
+ run({ args }) {
10045
+ return withErrorHandler(async () => {
10046
+ const globalOpts = getGlobalOpts14(args);
10047
+ const config = loadConfig();
10048
+ const xmlPath = args.xml;
10049
+ if (!fs14.existsSync(xmlPath)) {
10050
+ throw new Error(`File not found: ${xmlPath}`);
10051
+ }
10052
+ const invoiceXml = fs14.readFileSync(xmlPath, "utf-8");
10053
+ const sellerNip = args.nip ?? config.nip;
10054
+ if (!sellerNip) {
10055
+ throw new Error("NIP is required. Use --nip or set via `ksef config set --nip`");
10056
+ }
10057
+ const contextType = args["context-type"] ?? "Nip";
10058
+ const contextId = args["context-id"] ?? sellerNip;
10059
+ const client = createClient(globalOpts);
10060
+ const workflow = client.offline;
10061
+ if (args.key && !args["cert-serial"]) {
10062
+ throw new Error("--cert-serial is required when using --key");
10063
+ }
10064
+ const certificate2 = args.key ? {
10065
+ privateKeyPem: fs14.readFileSync(args.key, "utf-8"),
10066
+ certificateSerial: args["cert-serial"]
10067
+ } : void 0;
10068
+ const validModes = ["offline24", "offline", "awaryjny", "awaria_calkowita"];
10069
+ const modeArg = args.mode ?? "offline24";
10070
+ if (!validModes.includes(modeArg)) {
10071
+ throw new Error(`Invalid --mode "${modeArg}". Must be one of: ${validModes.join(", ")}`);
10072
+ }
10073
+ const storage = args["no-store"] ? void 0 : getStorage(args);
10074
+ const { invoiceNumber, invoiceDate } = extractInvoiceFields(invoiceXml);
10075
+ const metadata = await workflow.generate(
10076
+ {
10077
+ invoiceNumber,
10078
+ invoiceDate,
10079
+ invoiceXml,
10080
+ sellerNip,
10081
+ sellerIdentifier: { type: contextType, value: contextId }
10082
+ },
10083
+ {
10084
+ mode: modeArg,
10085
+ certificate: certificate2,
10086
+ storage
10087
+ }
10088
+ );
10089
+ if (args["qr-out"]) {
10090
+ const outDir = args["qr-out"];
10091
+ fs14.mkdirSync(outDir, { recursive: true });
10092
+ const format = args["qr-format"] ?? "png";
10093
+ if (format !== "png" && format !== "svg") {
10094
+ throw new Error(`Invalid --qr-format "${format}". Must be one of: png, svg`);
10095
+ }
10096
+ const shortId = metadata.id.slice(0, 8);
10097
+ if (format === "svg") {
10098
+ const svg1 = await QrCodeService.generateQrCodeSvgWithLabel(metadata.kod1Url, "OFFLINE");
10099
+ fs14.writeFileSync(`${outDir}/${shortId}-kod1.svg`, svg1);
10100
+ if (metadata.kod2Url) {
10101
+ const svg2 = await QrCodeService.generateQrCodeSvgWithLabel(metadata.kod2Url, "CERTYFIKAT");
10102
+ fs14.writeFileSync(`${outDir}/${shortId}-kod2.svg`, svg2);
10103
+ }
10104
+ } else {
10105
+ const png1 = await QrCodeService.generateQrCode(metadata.kod1Url);
10106
+ fs14.writeFileSync(`${outDir}/${shortId}-kod1.png`, png1);
10107
+ if (metadata.kod2Url) {
10108
+ const png2 = await QrCodeService.generateQrCode(metadata.kod2Url);
10109
+ fs14.writeFileSync(`${outDir}/${shortId}-kod2.png`, png2);
10110
+ }
10111
+ }
10112
+ outputSuccess(`QR codes saved to ${outDir}/`);
10113
+ }
10114
+ if (!certificate2) {
10115
+ outputWarning("No certificate provided \u2014 KOD II QR code was not generated. Use --key and --cert-serial for offline compliance.");
10116
+ }
10117
+ if (args.json) {
10118
+ outputResult(metadata, { json: true });
10119
+ } else {
10120
+ outputKeyValue({
10121
+ ID: metadata.id,
10122
+ Mode: metadata.mode,
10123
+ Status: metadata.status,
10124
+ Deadline: metadata.submitBy,
10125
+ "KOD I": metadata.kod1Url,
10126
+ "KOD II": metadata.kod2Url ?? "(not generated)"
10127
+ }, { json: false });
10128
+ }
10129
+ });
10130
+ }
10131
+ });
10132
+ var list4 = defineCommand17({
10133
+ meta: { name: "list", description: "List stored offline invoices" },
10134
+ args: {
10135
+ ...globalArgs,
10136
+ status: { type: "string", description: "Filter by status" },
10137
+ mode: { type: "string", description: "Filter by mode" },
10138
+ expiring: { type: "boolean", description: "Show only invoices expiring within 24h" }
10139
+ },
10140
+ run({ args }) {
10141
+ return withErrorHandler(async () => {
10142
+ const storage = getStorage(args);
10143
+ const filter = {};
10144
+ if (args.status) filter.status = args.status;
10145
+ if (args.mode) filter.mode = args.mode;
10146
+ if (args.expiring) {
10147
+ const cutoff = new Date(Date.now() + 24 * 60 * 60 * 1e3);
10148
+ filter.expiringBefore = cutoff.toISOString();
10149
+ }
10150
+ const invoices2 = await storage.list(Object.keys(filter).length > 0 ? filter : void 0);
10151
+ if (invoices2.length === 0) {
10152
+ if (args.json) {
10153
+ outputResult([], { json: true });
10154
+ } else {
10155
+ outputSuccess("No offline invoices found.");
10156
+ }
10157
+ return;
10158
+ }
10159
+ outputTable(
10160
+ invoices2.map((i) => ({
10161
+ id: i.id.slice(0, 8),
10162
+ number: i.invoiceNumber,
10163
+ mode: i.mode,
10164
+ status: i.status,
10165
+ deadline: i.submitBy.slice(0, 19),
10166
+ nip: i.sellerNip
10167
+ })),
10168
+ [
10169
+ { key: "id", label: "ID" },
10170
+ { key: "number", label: "Number" },
10171
+ { key: "mode", label: "Mode" },
10172
+ { key: "status", label: "Status" },
10173
+ { key: "deadline", label: "Deadline" },
10174
+ { key: "nip", label: "NIP" }
10175
+ ],
10176
+ { json: args.json }
10177
+ );
10178
+ });
10179
+ }
10180
+ });
10181
+ var status6 = defineCommand17({
10182
+ meta: { name: "status", description: "Show offline invoice details" },
10183
+ args: {
10184
+ ...globalArgs,
10185
+ id: { type: "positional", description: "Invoice ID", required: true }
10186
+ },
10187
+ run({ args }) {
10188
+ return withErrorHandler(async () => {
10189
+ const storage = getStorage(args);
10190
+ const invoice3 = await storage.get(args.id);
10191
+ if (!invoice3) {
10192
+ throw new Error(`Offline invoice not found: ${args.id}`);
10193
+ }
10194
+ if (args.json) {
10195
+ outputResult(invoice3, { json: true });
10196
+ } else {
10197
+ outputKeyValue({
10198
+ ID: invoice3.id,
10199
+ Number: invoice3.invoiceNumber,
10200
+ Date: invoice3.invoiceDate,
10201
+ Mode: invoice3.mode,
10202
+ Reason: invoice3.reason,
10203
+ Status: invoice3.status,
10204
+ "Seller NIP": invoice3.sellerNip,
10205
+ Deadline: invoice3.submitBy,
10206
+ "Generated At": invoice3.generatedAt,
10207
+ "Submitted At": invoice3.submittedAt ?? "-",
10208
+ "KSeF Reference": invoice3.ksefReferenceNumber ?? "-",
10209
+ "KOD I URL": invoice3.kod1Url,
10210
+ "KOD II URL": invoice3.kod2Url ?? "-",
10211
+ Error: invoice3.error ? `${invoice3.error.code}: ${invoice3.error.message}` : "-"
10212
+ }, { json: false });
10213
+ }
10214
+ });
10215
+ }
10216
+ });
10217
+ var submit = defineCommand17({
10218
+ meta: { name: "submit", description: "Submit offline invoices to KSeF" },
10219
+ args: {
10220
+ ...globalArgs,
10221
+ all: { type: "boolean", description: "Submit all pending invoices" },
10222
+ "no-check-expiry": { type: "boolean", description: "Skip expiry check" },
10223
+ ids: { type: "positional", description: "Invoice IDs to submit" }
10224
+ },
10225
+ run({ args }) {
10226
+ return withErrorHandler(async () => {
10227
+ const globalOpts = getGlobalOpts14(args);
10228
+ const { client } = await requireSession(globalOpts);
10229
+ const storage = getStorage(args);
10230
+ const invoiceIds = args.ids ? args.ids.split(",").map((s) => s.trim()) : void 0;
10231
+ if (args.all && invoiceIds) {
10232
+ throw new Error("Cannot use --all together with specific invoice IDs. Use one or the other.");
10233
+ }
10234
+ if (!args.all && !invoiceIds) {
10235
+ throw new Error("Specify invoice IDs or use --all to submit all pending invoices.");
10236
+ }
10237
+ const result = await client.offline.submit(client, {
10238
+ storage,
10239
+ invoiceIds: args.all ? void 0 : invoiceIds,
10240
+ checkExpiry: !args["no-check-expiry"]
10241
+ });
10242
+ if (result.total === 0) {
10243
+ outputSuccess("No pending invoices to submit.");
10244
+ return;
10245
+ }
10246
+ if (args.json) {
10247
+ outputResult(result, { json: true });
10248
+ } else {
10249
+ outputSuccess(
10250
+ `Submitted: ${result.submitted}, Accepted: ${result.accepted}, Rejected: ${result.rejected}, Expired: ${result.expired}`
10251
+ );
10252
+ if (result.results.length > 0) {
10253
+ outputTable(
10254
+ result.results.map((r) => ({
10255
+ id: r.invoiceId.slice(0, 8),
10256
+ number: r.invoiceNumber,
10257
+ status: r.status,
10258
+ ref: r.ksefReferenceNumber ?? "-"
10259
+ })),
10260
+ [
10261
+ { key: "id", label: "ID" },
10262
+ { key: "number", label: "Number" },
10263
+ { key: "status", label: "Status" },
10264
+ { key: "ref", label: "KSeF Ref" }
10265
+ ],
10266
+ { json: false }
10267
+ );
10268
+ }
10269
+ }
10270
+ });
10271
+ }
10272
+ });
10273
+ var correct = defineCommand17({
10274
+ meta: { name: "correct", description: "Technical correction for rejected offline invoice" },
10275
+ args: {
10276
+ ...globalArgs,
10277
+ id: { type: "positional", description: "Rejected invoice ID", required: true },
10278
+ xml: { type: "positional", description: "Corrected XML file path", required: true }
10279
+ },
10280
+ run({ args }) {
10281
+ return withErrorHandler(async () => {
10282
+ const globalOpts = getGlobalOpts14(args);
10283
+ const { client } = await requireSession(globalOpts);
10284
+ const storage = getStorage(args);
10285
+ const xmlPath = args.xml;
10286
+ if (!fs14.existsSync(xmlPath)) {
10287
+ throw new Error(`File not found: ${xmlPath}`);
10288
+ }
10289
+ const correctedXml = fs14.readFileSync(xmlPath, "utf-8");
10290
+ const result = await client.offline.correct(client, {
10291
+ rejectedInvoiceId: args.id,
10292
+ correctedInvoiceXml: correctedXml,
10293
+ storage
10294
+ });
10295
+ if (args.json) {
10296
+ outputResult(result, { json: true });
10297
+ } else {
10298
+ outputSuccess(`Correction submitted. KSeF ref: ${result.ksefReferenceNumber ?? "pending"}`);
10299
+ }
10300
+ });
10301
+ }
10302
+ });
10303
+ var del = defineCommand17({
10304
+ meta: { name: "delete", description: "Delete offline invoice from local store" },
10305
+ args: {
10306
+ ...globalArgs,
10307
+ id: { type: "positional", description: "Invoice ID to delete" },
10308
+ expired: { type: "boolean", description: "Delete all expired invoices" },
10309
+ force: { type: "boolean", description: "Skip confirmation prompt" }
10310
+ },
10311
+ run({ args }) {
10312
+ return withErrorHandler(async () => {
10313
+ const storage = getStorage(args);
10314
+ if (args.expired) {
10315
+ const expired = await storage.list({ status: "EXPIRED" });
10316
+ if (expired.length === 0) {
10317
+ outputSuccess("No expired invoices to delete.");
10318
+ return;
10319
+ }
10320
+ if (!args.force) {
10321
+ if (!process.stdin.isTTY) {
10322
+ throw new Error(
10323
+ `${expired.length} expired invoice(s) would be deleted. Use --force to confirm in non-interactive mode.`
10324
+ );
10325
+ }
10326
+ const confirmed = await consola15.prompt(
10327
+ `Delete ${expired.length} expired invoice(s)?`,
10328
+ { type: "confirm", initial: false }
10329
+ );
10330
+ if (!confirmed) {
10331
+ consola15.info("Cancelled.");
10332
+ return;
10333
+ }
10334
+ }
10335
+ for (const inv of expired) {
10336
+ await storage.delete(inv.id);
10337
+ }
10338
+ outputSuccess(`Deleted ${expired.length} expired invoice(s).`);
10339
+ return;
10340
+ }
10341
+ if (!args.id) {
10342
+ throw new Error("Specify an invoice ID or use --expired to delete all expired invoices.");
10343
+ }
10344
+ const invoice3 = await storage.get(args.id);
10345
+ if (!invoice3) {
10346
+ throw new Error(`Offline invoice not found: ${args.id}`);
10347
+ }
10348
+ await storage.delete(args.id);
10349
+ outputSuccess(`Deleted offline invoice ${args.id}`);
10350
+ });
10351
+ }
10352
+ });
10353
+ var offlineCommand = defineCommand17({
10354
+ meta: { name: "offline", description: "Offline invoice management" },
10355
+ subCommands: { generate: generate3, list: list4, status: status6, submit, correct, delete: del }
10356
+ });
10357
+
9473
10358
  // src/cli/index.ts
9474
10359
  var __dirname = dirname2(fileURLToPath(import.meta.url));
9475
- var pkg = JSON.parse(readFileSync8(resolve(__dirname, "..", "package.json"), "utf-8"));
10360
+ var pkg = JSON.parse(readFileSync9(resolve(__dirname, "..", "package.json"), "utf-8"));
9476
10361
  var version = pkg.version;
9477
- var main = defineCommand17({
10362
+ var main = defineCommand18({
9478
10363
  meta: {
9479
10364
  name: "ksef",
9480
10365
  version,
@@ -9494,6 +10379,7 @@ var main = defineCommand17({
9494
10379
  limits: limitsCommand,
9495
10380
  peppol: peppolCommand,
9496
10381
  "test-data": testDataCommand,
10382
+ offline: offlineCommand,
9497
10383
  doctor: doctorCommand,
9498
10384
  completion: completionCommand
9499
10385
  }