ksef-client-ts 0.5.2 → 0.6.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/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;
@@ -988,6 +988,32 @@ var init_online_session = __esm({
988
988
  }
989
989
  });
990
990
 
991
+ // src/utils/concurrency.ts
992
+ async function runWithConcurrency(tasks, parallelism) {
993
+ if (tasks.length === 0) return;
994
+ const limit = Math.max(1, Math.min(parallelism, tasks.length));
995
+ const controller = new AbortController();
996
+ let index = 0;
997
+ const workers = Array.from({ length: limit }, async () => {
998
+ while (index < tasks.length && !controller.signal.aborted) {
999
+ const current = index;
1000
+ index += 1;
1001
+ try {
1002
+ await tasks[current](controller.signal);
1003
+ } catch (err) {
1004
+ controller.abort();
1005
+ throw err;
1006
+ }
1007
+ }
1008
+ });
1009
+ await Promise.all(workers);
1010
+ }
1011
+ var init_concurrency = __esm({
1012
+ "src/utils/concurrency.ts"() {
1013
+ "use strict";
1014
+ }
1015
+ });
1016
+
991
1017
  // src/services/batch-session.ts
992
1018
  var BatchSessionService;
993
1019
  var init_batch_session = __esm({
@@ -996,6 +1022,7 @@ var init_batch_session = __esm({
996
1022
  init_ksef_feature();
997
1023
  init_rest_request();
998
1024
  init_routes();
1025
+ init_concurrency();
999
1026
  BatchSessionService = class {
1000
1027
  restClient;
1001
1028
  constructor(restClient) {
@@ -1009,12 +1036,10 @@ var init_batch_session = __esm({
1009
1036
  const response = await this.restClient.execute(req);
1010
1037
  return response.body;
1011
1038
  }
1012
- async sendParts(openResponse, parts) {
1013
- const uploadRequests = openResponse.partUploadRequests;
1014
- const tasks = parts.map(async (part) => {
1015
- const uploadReq = uploadRequests.find(
1016
- (r) => r.ordinalNumber === part.ordinalNumber
1017
- );
1039
+ async sendParts(openResponse, parts, parallelism) {
1040
+ const uploadMap = new Map(openResponse.partUploadRequests.map((r) => [r.ordinalNumber, r]));
1041
+ const tasks = parts.map((part) => async (signal) => {
1042
+ const uploadReq = uploadMap.get(part.ordinalNumber);
1018
1043
  if (!uploadReq) {
1019
1044
  throw new Error(`No upload request found for part ${part.ordinalNumber}`);
1020
1045
  }
@@ -1022,25 +1047,38 @@ var init_batch_session = __esm({
1022
1047
  for (const [k, v] of Object.entries(uploadReq.headers)) {
1023
1048
  if (v != null) headers[k] = v;
1024
1049
  }
1025
- await fetch(uploadReq.url, {
1050
+ const resp = await fetch(uploadReq.url, {
1026
1051
  method: uploadReq.method,
1027
1052
  headers,
1028
- body: part.data
1053
+ body: part.data,
1054
+ signal
1029
1055
  });
1056
+ if (!resp.ok) {
1057
+ const body = await resp.text().catch(() => "");
1058
+ throw new Error(
1059
+ `Upload failed for part ${part.ordinalNumber}: HTTP ${resp.status}${body ? ` \u2014 ${body}` : ""}`
1060
+ );
1061
+ }
1030
1062
  });
1031
- await Promise.all(tasks);
1063
+ if (parallelism !== void 0) {
1064
+ await runWithConcurrency(tasks, parallelism);
1065
+ } else {
1066
+ const ac = new AbortController();
1067
+ await Promise.all(tasks.map((t) => t(ac.signal).catch((err) => {
1068
+ ac.abort();
1069
+ throw err;
1070
+ })));
1071
+ }
1032
1072
  }
1033
1073
  /**
1034
- * Upload parts sequentially (not in parallel) because each part uses a
1035
- * streaming body (`duplex: 'half'`). Parallel streaming uploads can cause
1036
- * backpressure issues and exceed memory limits for large payloads.
1074
+ * Upload parts using streaming bodies (`duplex: 'half'`).
1075
+ * By default uploads sequentially to avoid backpressure issues.
1076
+ * Pass `parallelism` to enable bounded concurrent uploads.
1037
1077
  */
1038
- async sendPartsWithStream(openResponse, parts) {
1039
- const uploadRequests = openResponse.partUploadRequests;
1040
- for (const part of parts) {
1041
- const uploadReq = uploadRequests.find(
1042
- (r) => r.ordinalNumber === part.ordinalNumber
1043
- );
1078
+ async sendPartsWithStream(openResponse, parts, parallelism) {
1079
+ const uploadMap = new Map(openResponse.partUploadRequests.map((r) => [r.ordinalNumber, r]));
1080
+ const uploadPart = async (part, signal) => {
1081
+ const uploadReq = uploadMap.get(part.ordinalNumber);
1044
1082
  if (!uploadReq) {
1045
1083
  throw new Error(`No upload request found for part ${part.ordinalNumber}`);
1046
1084
  }
@@ -1052,11 +1090,23 @@ var init_batch_session = __esm({
1052
1090
  method: uploadReq.method,
1053
1091
  headers,
1054
1092
  body: part.dataStream,
1093
+ signal,
1055
1094
  // @ts-expect-error -- Node 18+ undici supports duplex for streaming body
1056
1095
  duplex: "half"
1057
1096
  });
1058
1097
  if (!resp.ok) {
1059
- throw new Error(`Upload failed for part ${part.ordinalNumber}: HTTP ${resp.status}`);
1098
+ const body = await resp.text().catch(() => "");
1099
+ throw new Error(
1100
+ `Upload failed for part ${part.ordinalNumber}: HTTP ${resp.status}${body ? ` \u2014 ${body}` : ""}`
1101
+ );
1102
+ }
1103
+ };
1104
+ if (parallelism !== void 0) {
1105
+ await runWithConcurrency(parts.map((p) => (signal) => uploadPart(p, signal)), parallelism);
1106
+ } else {
1107
+ const { signal } = new AbortController();
1108
+ for (const part of parts) {
1109
+ await uploadPart(part, signal);
1060
1110
  }
1061
1111
  }
1062
1112
  }
@@ -1094,8 +1144,8 @@ var init_session_status = __esm({
1094
1144
  if (filter.dateModifiedFrom) req.query("dateModifiedFrom", filter.dateModifiedFrom);
1095
1145
  if (filter.dateModifiedTo) req.query("dateModifiedTo", filter.dateModifiedTo);
1096
1146
  if (filter.statuses) {
1097
- for (const status6 of filter.statuses) {
1098
- req.query("statuses", status6);
1147
+ for (const status7 of filter.statuses) {
1148
+ req.query("statuses", status7);
1099
1149
  }
1100
1150
  }
1101
1151
  }
@@ -1169,7 +1219,10 @@ var init_invoice_download = __esm({
1169
1219
  async getInvoice(ksefNumber) {
1170
1220
  const req = RestRequest.get(Routes.Invoices.byKsefNumber(ksefNumber));
1171
1221
  const response = await this.restClient.executeRaw(req);
1172
- return new TextDecoder().decode(response.body);
1222
+ return {
1223
+ xml: new TextDecoder().decode(response.body),
1224
+ hash: response.headers.get("x-ms-meta-hash") ?? void 0
1225
+ };
1173
1226
  }
1174
1227
  async queryInvoiceMetadata(filters, pageOffset, pageSize, sortOrder) {
1175
1228
  const req = RestRequest.post(Routes.Invoices.queryMetadata).body(filters);
@@ -1469,20 +1522,20 @@ var init_lighthouse = __esm({
1469
1522
  this.lighthouseUrl = options.lighthouseUrl;
1470
1523
  this.timeout = options.timeout;
1471
1524
  }
1472
- async fetchJson(path9) {
1525
+ async fetchJson(path10) {
1473
1526
  if (!this.lighthouseUrl) {
1474
1527
  throw new KSeFError(
1475
1528
  "Lighthouse API is not available for the DEMO environment. Use TEST or PROD instead."
1476
1529
  );
1477
1530
  }
1478
- const response = await fetch(`${this.lighthouseUrl}${path9}`, {
1531
+ const response = await fetch(`${this.lighthouseUrl}${path10}`, {
1479
1532
  headers: { Accept: "application/json" },
1480
1533
  signal: AbortSignal.timeout(this.timeout)
1481
1534
  });
1482
1535
  if (!response.ok) {
1483
1536
  const body = await response.text();
1484
1537
  throw new KSeFError(
1485
- `Lighthouse ${path9} failed: HTTP ${response.status} \u2014 ${body}`
1538
+ `Lighthouse ${path10} failed: HTTP ${response.status} \u2014 ${body}`
1486
1539
  );
1487
1540
  }
1488
1541
  return await response.json();
@@ -2087,12 +2140,517 @@ var init_auth_xml_builder = __esm({
2087
2140
  }
2088
2141
  });
2089
2142
 
2143
+ // src/offline/holidays.ts
2144
+ function toDateKey(d) {
2145
+ return d.toISOString().slice(0, 10);
2146
+ }
2147
+ function dateKey(year, month, day) {
2148
+ return toDateKey(new Date(Date.UTC(year, month - 1, day)));
2149
+ }
2150
+ function addDays(d, n) {
2151
+ const result = new Date(d);
2152
+ result.setUTCDate(result.getUTCDate() + n);
2153
+ return result;
2154
+ }
2155
+ function computeEasterSunday(year) {
2156
+ const a = year % 19;
2157
+ const b = Math.floor(year / 100);
2158
+ const c = year % 100;
2159
+ const d = Math.floor(b / 4);
2160
+ const e = b % 4;
2161
+ const f = Math.floor((b + 8) / 25);
2162
+ const g = Math.floor((b - f + 1) / 3);
2163
+ const h = (19 * a + b - d - g + 15) % 30;
2164
+ const i = Math.floor(c / 4);
2165
+ const k = c % 4;
2166
+ const l = (32 + 2 * e + 2 * i - h - k) % 7;
2167
+ const m = Math.floor((a + 11 * h + 22 * l) / 451);
2168
+ const month = Math.floor((h + l - 7 * m + 114) / 31);
2169
+ const day = (h + l - 7 * m + 114) % 31 + 1;
2170
+ return new Date(Date.UTC(year, month - 1, day));
2171
+ }
2172
+ function getPolishHolidays(year) {
2173
+ const cached = holidayCache.get(year);
2174
+ if (cached) return cached;
2175
+ const easter = computeEasterSunday(year);
2176
+ const holidays = /* @__PURE__ */ new Set([
2177
+ // Fixed
2178
+ dateKey(year, 1, 1),
2179
+ dateKey(year, 1, 6),
2180
+ dateKey(year, 5, 1),
2181
+ dateKey(year, 5, 3),
2182
+ dateKey(year, 8, 15),
2183
+ dateKey(year, 11, 1),
2184
+ dateKey(year, 11, 11),
2185
+ dateKey(year, 12, 25),
2186
+ dateKey(year, 12, 26),
2187
+ // Wigilia — added by Dz.U. 2024 poz. 1965, effective from 2025
2188
+ ...year >= 2025 ? [dateKey(year, 12, 24)] : [],
2189
+ // Moveable
2190
+ toDateKey(easter),
2191
+ toDateKey(addDays(easter, 1)),
2192
+ toDateKey(addDays(easter, 49)),
2193
+ toDateKey(addDays(easter, 60))
2194
+ ]);
2195
+ holidayCache.set(year, holidays);
2196
+ return holidays;
2197
+ }
2198
+ function isPolishHoliday(date) {
2199
+ return getPolishHolidays(date.getUTCFullYear()).has(toDateKey(date));
2200
+ }
2201
+ var holidayCache;
2202
+ var init_holidays = __esm({
2203
+ "src/offline/holidays.ts"() {
2204
+ "use strict";
2205
+ holidayCache = /* @__PURE__ */ new Map();
2206
+ }
2207
+ });
2208
+
2209
+ // src/offline/deadline.ts
2210
+ function getDefaultReason(mode) {
2211
+ switch (mode) {
2212
+ case "offline24":
2213
+ return "PLANNED";
2214
+ case "offline":
2215
+ return "SYSTEM_UNAVAILABLE";
2216
+ case "awaryjny":
2217
+ return "EMERGENCY";
2218
+ case "awaria_calkowita":
2219
+ return "TOTAL_FAILURE";
2220
+ }
2221
+ }
2222
+ function nextBusinessDay(from) {
2223
+ const d = new Date(from);
2224
+ d.setUTCDate(d.getUTCDate() + 1);
2225
+ while (d.getUTCDay() === 0 || d.getUTCDay() === 6 || isPolishHoliday(d)) {
2226
+ d.setUTCDate(d.getUTCDate() + 1);
2227
+ }
2228
+ return d;
2229
+ }
2230
+ function addBusinessDays(from, days) {
2231
+ if (!Number.isInteger(days) || days < 0) {
2232
+ throw new Error(`days must be a non-negative integer, got ${days}`);
2233
+ }
2234
+ const d = new Date(from);
2235
+ let remaining = days;
2236
+ while (remaining > 0) {
2237
+ d.setUTCDate(d.getUTCDate() + 1);
2238
+ if (d.getUTCDay() !== 0 && d.getUTCDay() !== 6 && !isPolishHoliday(d)) {
2239
+ remaining--;
2240
+ }
2241
+ }
2242
+ return d;
2243
+ }
2244
+ function endOfDay(d) {
2245
+ const result = new Date(d);
2246
+ result.setUTCHours(23, 59, 59, 999);
2247
+ return result;
2248
+ }
2249
+ function getMaintenanceEndFallback(mw) {
2250
+ if (mw.endTime) {
2251
+ return new Date(mw.endTime);
2252
+ }
2253
+ const start = new Date(mw.startTime);
2254
+ start.setUTCDate(start.getUTCDate() + 7);
2255
+ return start;
2256
+ }
2257
+ function calculateOfflineDeadline(mode, invoiceDate, maintenanceWindow) {
2258
+ const date = typeof invoiceDate === "string" ? new Date(invoiceDate) : invoiceDate;
2259
+ switch (mode) {
2260
+ case "offline24": {
2261
+ return endOfDay(nextBusinessDay(date));
2262
+ }
2263
+ case "offline": {
2264
+ const base = maintenanceWindow ? getMaintenanceEndFallback(maintenanceWindow) : date;
2265
+ return endOfDay(nextBusinessDay(base));
2266
+ }
2267
+ case "awaryjny": {
2268
+ const base = maintenanceWindow ? getMaintenanceEndFallback(maintenanceWindow) : date;
2269
+ return endOfDay(addBusinessDays(base, 7));
2270
+ }
2271
+ case "awaria_calkowita": {
2272
+ return new Date(FAR_FUTURE);
2273
+ }
2274
+ }
2275
+ }
2276
+ function isExpired(submitBy) {
2277
+ const deadline = typeof submitBy === "string" ? new Date(submitBy) : submitBy;
2278
+ return Date.now() > deadline.getTime();
2279
+ }
2280
+ var FAR_FUTURE;
2281
+ var init_deadline = __esm({
2282
+ "src/offline/deadline.ts"() {
2283
+ "use strict";
2284
+ init_holidays();
2285
+ FAR_FUTURE = /* @__PURE__ */ new Date("9999-12-31T23:59:59Z");
2286
+ }
2287
+ });
2288
+
2289
+ // src/models/document-structures/types.ts
2290
+ var SystemCode, FORM_CODES, DEFAULT_FORM_CODE, INVOICE_TYPES_BY_SYSTEM_CODE, FORM_CODE_KEYS;
2291
+ var init_types = __esm({
2292
+ "src/models/document-structures/types.ts"() {
2293
+ "use strict";
2294
+ SystemCode = {
2295
+ FA_2: "FA (2)",
2296
+ FA_3: "FA (3)",
2297
+ PEF_3: "PEF (3)",
2298
+ PEF_KOR_3: "PEF_KOR (3)",
2299
+ FA_RR_1: "FA_RR (1)"
2300
+ };
2301
+ FORM_CODES = {
2302
+ FA_2: { systemCode: "FA (2)", schemaVersion: "1-0E", value: "FA" },
2303
+ FA_3: { systemCode: "FA (3)", schemaVersion: "1-0E", value: "FA" },
2304
+ PEF_3: { systemCode: "PEF (3)", schemaVersion: "2-1", value: "PEF" },
2305
+ PEF_KOR_3: { systemCode: "PEF_KOR (3)", schemaVersion: "2-1", value: "PEF" },
2306
+ FA_RR_1_LEGACY: { systemCode: "FA_RR (1)", schemaVersion: "1-0E", value: "RR" },
2307
+ FA_RR_1_TRANSITION: { systemCode: "FA_RR (1)", schemaVersion: "1-1E", value: "RR" },
2308
+ FA_RR_1: { systemCode: "FA_RR (1)", schemaVersion: "1-1E", value: "FA_RR" }
2309
+ };
2310
+ DEFAULT_FORM_CODE = FORM_CODES.FA_3;
2311
+ INVOICE_TYPES_BY_SYSTEM_CODE = {
2312
+ [SystemCode.FA_2]: ["Vat", "Zal", "Kor", "Roz", "Upr", "KorZal", "KorRoz"],
2313
+ [SystemCode.FA_3]: ["Vat", "Zal", "Kor", "Roz", "Upr", "KorZal", "KorRoz"],
2314
+ [SystemCode.PEF_3]: ["VatPef", "VatPefSp", "KorPef"],
2315
+ [SystemCode.PEF_KOR_3]: ["KorPef"],
2316
+ [SystemCode.FA_RR_1]: ["VatRr", "KorVatRr"]
2317
+ };
2318
+ FORM_CODE_KEYS = {
2319
+ FA2: FORM_CODES.FA_2,
2320
+ FA3: FORM_CODES.FA_3,
2321
+ PEF3: FORM_CODES.PEF_3,
2322
+ PEFKOR3: FORM_CODES.PEF_KOR_3,
2323
+ FARR1: FORM_CODES.FA_RR_1
2324
+ };
2325
+ }
2326
+ });
2327
+
2328
+ // src/models/document-structures/helpers.ts
2329
+ function validateFormCodeForSession(formCode, sessionType) {
2330
+ if (sessionType === "online") return true;
2331
+ return !BATCH_DISALLOWED_SYSTEM_CODES.has(formCode.systemCode);
2332
+ }
2333
+ var SYSTEM_CODE_TO_FORM_CODE, ALL_FORM_CODES, BATCH_DISALLOWED_SYSTEM_CODES;
2334
+ var init_helpers = __esm({
2335
+ "src/models/document-structures/helpers.ts"() {
2336
+ "use strict";
2337
+ init_types();
2338
+ SYSTEM_CODE_TO_FORM_CODE = {
2339
+ [SystemCode.FA_2]: FORM_CODES.FA_2,
2340
+ [SystemCode.FA_3]: FORM_CODES.FA_3,
2341
+ [SystemCode.PEF_3]: FORM_CODES.PEF_3,
2342
+ [SystemCode.PEF_KOR_3]: FORM_CODES.PEF_KOR_3,
2343
+ [SystemCode.FA_RR_1]: FORM_CODES.FA_RR_1
2344
+ };
2345
+ ALL_FORM_CODES = Object.values(FORM_CODES);
2346
+ BATCH_DISALLOWED_SYSTEM_CODES = /* @__PURE__ */ new Set([
2347
+ SystemCode.PEF_3,
2348
+ SystemCode.PEF_KOR_3
2349
+ ]);
2350
+ }
2351
+ });
2352
+
2353
+ // src/models/document-structures/index.ts
2354
+ var init_document_structures = __esm({
2355
+ "src/models/document-structures/index.ts"() {
2356
+ "use strict";
2357
+ init_types();
2358
+ init_helpers();
2359
+ }
2360
+ });
2361
+
2362
+ // src/workflows/offline-invoice-workflow.ts
2363
+ import crypto3 from "crypto";
2364
+ var OfflineInvoiceWorkflow;
2365
+ var init_offline_invoice_workflow = __esm({
2366
+ "src/workflows/offline-invoice-workflow.ts"() {
2367
+ "use strict";
2368
+ init_ksef_api_error();
2369
+ init_deadline();
2370
+ init_document_structures();
2371
+ OfflineInvoiceWorkflow = class {
2372
+ constructor(qrService) {
2373
+ this.qrService = qrService;
2374
+ }
2375
+ async generate(input, options) {
2376
+ if (!input.invoiceXml || input.invoiceXml.trim().length === 0) {
2377
+ throw new Error("invoiceXml must not be empty");
2378
+ }
2379
+ if (!input.invoiceNumber) {
2380
+ throw new Error("invoiceNumber is required");
2381
+ }
2382
+ if (!input.sellerNip) {
2383
+ throw new Error("sellerNip is required");
2384
+ }
2385
+ const mode = options?.mode ?? "offline24";
2386
+ const reason = getDefaultReason(mode);
2387
+ const invoiceHashBase64 = crypto3.createHash("sha256").update(input.invoiceXml).digest("base64");
2388
+ const kod1Url = this.qrService.buildInvoiceVerificationUrl(
2389
+ input.sellerNip,
2390
+ input.invoiceDate,
2391
+ invoiceHashBase64
2392
+ );
2393
+ let kod2Url;
2394
+ if (options?.certificate) {
2395
+ kod2Url = this.qrService.buildCertificateVerificationUrl(
2396
+ input.sellerIdentifier.type,
2397
+ input.sellerIdentifier.value,
2398
+ input.sellerNip,
2399
+ options.certificate.certificateSerial,
2400
+ invoiceHashBase64,
2401
+ options.certificate.privateKeyPem
2402
+ );
2403
+ }
2404
+ const submitBy = options?.customDeadline ? typeof options.customDeadline === "string" ? options.customDeadline : options.customDeadline.toISOString() : calculateOfflineDeadline(mode, input.invoiceDate, options?.maintenanceWindow).toISOString();
2405
+ const metadata = {
2406
+ id: crypto3.randomUUID(),
2407
+ mode,
2408
+ reason,
2409
+ status: "GENERATED",
2410
+ invoiceNumber: input.invoiceNumber,
2411
+ invoiceDate: input.invoiceDate,
2412
+ invoiceXml: input.invoiceXml,
2413
+ sellerNip: input.sellerNip,
2414
+ sellerIdentifier: input.sellerIdentifier,
2415
+ buyerIdentifier: input.buyerIdentifier,
2416
+ totalAmount: input.totalAmount,
2417
+ currency: input.currency,
2418
+ kod1Url,
2419
+ kod2Url,
2420
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
2421
+ submitBy,
2422
+ maintenanceWindowId: options?.maintenanceWindow?.id
2423
+ };
2424
+ if (options?.storage) {
2425
+ await options.storage.save(metadata);
2426
+ }
2427
+ return metadata;
2428
+ }
2429
+ async submit(client, options) {
2430
+ const { storage, checkExpiry = true } = options;
2431
+ let invoices2;
2432
+ if (options.invoiceIds) {
2433
+ invoices2 = [];
2434
+ const notFound = [];
2435
+ for (const id of options.invoiceIds) {
2436
+ const inv = await storage.get(id);
2437
+ if (inv) {
2438
+ invoices2.push(inv);
2439
+ } else {
2440
+ notFound.push(id);
2441
+ }
2442
+ }
2443
+ if (notFound.length > 0) {
2444
+ throw new Error(`Offline invoice(s) not found: ${notFound.join(", ")}`);
2445
+ }
2446
+ } else {
2447
+ invoices2 = await storage.list({ status: ["GENERATED", "QUEUED", "SUBMITTED"] });
2448
+ }
2449
+ const result = {
2450
+ total: invoices2.length,
2451
+ submitted: 0,
2452
+ accepted: 0,
2453
+ rejected: 0,
2454
+ failed: 0,
2455
+ expired: 0,
2456
+ results: []
2457
+ };
2458
+ if (invoices2.length === 0) return result;
2459
+ const pending = [];
2460
+ for (const inv of invoices2) {
2461
+ if (checkExpiry && isExpired(inv.submitBy)) {
2462
+ await storage.update(inv.id, { status: "EXPIRED" });
2463
+ result.expired++;
2464
+ result.results.push({
2465
+ invoiceId: inv.id,
2466
+ invoiceNumber: inv.invoiceNumber,
2467
+ status: "EXPIRED"
2468
+ });
2469
+ } else {
2470
+ pending.push(inv);
2471
+ }
2472
+ }
2473
+ if (pending.length === 0) return result;
2474
+ await client.crypto.init();
2475
+ const encData = client.crypto.getEncryptionData();
2476
+ const formCode = options.formCode ?? DEFAULT_FORM_CODE;
2477
+ const openResp = await client.onlineSession.openSession(
2478
+ { formCode, encryption: encData.encryptionInfo }
2479
+ );
2480
+ const sessionRef = openResp.referenceNumber;
2481
+ try {
2482
+ for (const inv of pending) {
2483
+ await storage.update(inv.id, { status: "QUEUED" });
2484
+ try {
2485
+ const data = new TextEncoder().encode(inv.invoiceXml);
2486
+ const plainMeta = client.crypto.getFileMetadata(data);
2487
+ const encrypted = client.crypto.encryptAES256(data, encData.cipherKey, encData.cipherIv);
2488
+ const encMeta = client.crypto.getFileMetadata(encrypted);
2489
+ await storage.update(inv.id, { status: "SUBMITTED", submittedAt: (/* @__PURE__ */ new Date()).toISOString() });
2490
+ const resp = await client.onlineSession.sendInvoice(sessionRef, {
2491
+ invoiceHash: plainMeta.hashSHA,
2492
+ invoiceSize: plainMeta.fileSize,
2493
+ encryptedInvoiceHash: encMeta.hashSHA,
2494
+ encryptedInvoiceSize: encMeta.fileSize,
2495
+ encryptedInvoiceContent: Buffer.from(encrypted).toString("base64"),
2496
+ offlineMode: true
2497
+ });
2498
+ await storage.update(inv.id, {
2499
+ status: "ACCEPTED",
2500
+ acceptedAt: (/* @__PURE__ */ new Date()).toISOString(),
2501
+ ksefReferenceNumber: resp.referenceNumber
2502
+ });
2503
+ result.submitted++;
2504
+ result.accepted++;
2505
+ result.results.push({
2506
+ invoiceId: inv.id,
2507
+ invoiceNumber: inv.invoiceNumber,
2508
+ status: "ACCEPTED",
2509
+ ksefReferenceNumber: resp.referenceNumber
2510
+ });
2511
+ } catch (err) {
2512
+ const message = err instanceof Error ? err.message : String(err);
2513
+ if (err instanceof KSeFApiError) {
2514
+ await storage.update(inv.id, {
2515
+ status: "REJECTED",
2516
+ error: { code: err.statusCode, message }
2517
+ });
2518
+ result.submitted++;
2519
+ result.rejected++;
2520
+ result.results.push({
2521
+ invoiceId: inv.id,
2522
+ invoiceNumber: inv.invoiceNumber,
2523
+ status: "REJECTED",
2524
+ error: { code: err.statusCode, message }
2525
+ });
2526
+ } else {
2527
+ await storage.update(inv.id, {
2528
+ status: "QUEUED",
2529
+ error: { code: 0, message }
2530
+ });
2531
+ result.failed++;
2532
+ result.results.push({
2533
+ invoiceId: inv.id,
2534
+ invoiceNumber: inv.invoiceNumber,
2535
+ status: "QUEUED",
2536
+ error: { code: 0, message }
2537
+ });
2538
+ }
2539
+ }
2540
+ }
2541
+ } finally {
2542
+ try {
2543
+ await client.onlineSession.closeSession(sessionRef);
2544
+ } catch {
2545
+ }
2546
+ }
2547
+ return result;
2548
+ }
2549
+ // TODO(perf): Each correction opens a separate KSeF session.
2550
+ // For batch corrections, consider a correctBatch() sharing one session (like submit()).
2551
+ async correct(client, options) {
2552
+ const { storage, rejectedInvoiceId, correctedInvoiceXml } = options;
2553
+ const original = await storage.get(rejectedInvoiceId);
2554
+ if (!original) {
2555
+ throw new Error(`Offline invoice not found: ${rejectedInvoiceId}`);
2556
+ }
2557
+ if (original.status !== "REJECTED") {
2558
+ throw new Error(
2559
+ original.status === "CORRECTED" ? `Invoice ${rejectedInvoiceId} has already been corrected (by ${original.correctedBy})` : `Only rejected invoices can be corrected (current status: ${original.status})`
2560
+ );
2561
+ }
2562
+ const originalHash = crypto3.createHash("sha256").update(original.invoiceXml).digest("base64");
2563
+ await client.crypto.init();
2564
+ const encData = client.crypto.getEncryptionData();
2565
+ const formCode = options.formCode ?? DEFAULT_FORM_CODE;
2566
+ const openResp = await client.onlineSession.openSession(
2567
+ { formCode, encryption: encData.encryptionInfo }
2568
+ );
2569
+ const sessionRef = openResp.referenceNumber;
2570
+ try {
2571
+ const data = new TextEncoder().encode(correctedInvoiceXml);
2572
+ const plainMeta = client.crypto.getFileMetadata(data);
2573
+ const encrypted = client.crypto.encryptAES256(data, encData.cipherKey, encData.cipherIv);
2574
+ const encMeta = client.crypto.getFileMetadata(encrypted);
2575
+ const correctionId = crypto3.randomUUID();
2576
+ const submittedAt = (/* @__PURE__ */ new Date()).toISOString();
2577
+ const correctedHashBase64 = crypto3.createHash("sha256").update(correctedInvoiceXml).digest("base64");
2578
+ const kod1Url = this.qrService.buildInvoiceVerificationUrl(
2579
+ original.sellerNip,
2580
+ original.invoiceDate,
2581
+ correctedHashBase64
2582
+ );
2583
+ let kod2Url;
2584
+ if (options.certificate) {
2585
+ kod2Url = this.qrService.buildCertificateVerificationUrl(
2586
+ original.sellerIdentifier.type,
2587
+ original.sellerIdentifier.value,
2588
+ original.sellerNip,
2589
+ options.certificate.certificateSerial,
2590
+ correctedHashBase64,
2591
+ options.certificate.privateKeyPem
2592
+ );
2593
+ }
2594
+ const correctionMetadata = {
2595
+ id: correctionId,
2596
+ mode: original.mode,
2597
+ reason: original.reason,
2598
+ status: "SUBMITTED",
2599
+ invoiceNumber: original.invoiceNumber,
2600
+ invoiceDate: original.invoiceDate,
2601
+ invoiceXml: correctedInvoiceXml,
2602
+ sellerNip: original.sellerNip,
2603
+ sellerIdentifier: original.sellerIdentifier,
2604
+ buyerIdentifier: original.buyerIdentifier,
2605
+ kod1Url,
2606
+ kod2Url,
2607
+ generatedAt: submittedAt,
2608
+ submitBy: original.submitBy,
2609
+ submittedAt,
2610
+ correctedInvoiceId: rejectedInvoiceId
2611
+ };
2612
+ await storage.save(correctionMetadata);
2613
+ const resp = await client.onlineSession.sendInvoice(sessionRef, {
2614
+ invoiceHash: plainMeta.hashSHA,
2615
+ invoiceSize: plainMeta.fileSize,
2616
+ encryptedInvoiceHash: encMeta.hashSHA,
2617
+ encryptedInvoiceSize: encMeta.fileSize,
2618
+ encryptedInvoiceContent: Buffer.from(encrypted).toString("base64"),
2619
+ offlineMode: true,
2620
+ hashOfCorrectedInvoice: originalHash
2621
+ });
2622
+ await storage.update(correctionId, {
2623
+ status: "ACCEPTED",
2624
+ acceptedAt: (/* @__PURE__ */ new Date()).toISOString(),
2625
+ ksefReferenceNumber: resp.referenceNumber
2626
+ });
2627
+ await storage.update(rejectedInvoiceId, {
2628
+ status: "CORRECTED",
2629
+ correctedBy: correctionId
2630
+ });
2631
+ return {
2632
+ invoiceId: correctionId,
2633
+ invoiceNumber: correctionMetadata.invoiceNumber,
2634
+ status: "ACCEPTED",
2635
+ ksefReferenceNumber: resp.referenceNumber
2636
+ };
2637
+ } finally {
2638
+ try {
2639
+ await client.onlineSession.closeSession(sessionRef);
2640
+ } catch {
2641
+ }
2642
+ }
2643
+ }
2644
+ };
2645
+ }
2646
+ });
2647
+
2090
2648
  // src/crypto/signature-service.ts
2091
2649
  var signature_service_exports = {};
2092
2650
  __export(signature_service_exports, {
2093
2651
  SignatureService: () => SignatureService
2094
2652
  });
2095
- import * as crypto3 from "crypto";
2653
+ import * as crypto4 from "crypto";
2096
2654
  import { ExclusiveCanonicalization } from "xml-crypto";
2097
2655
  import { DOMParser, XMLSerializer } from "@xmldom/xmldom";
2098
2656
  function extractDerFromPem(pem) {
@@ -2112,7 +2670,7 @@ function canonicalize(elem) {
2112
2670
  function computeRootDigest(doc) {
2113
2671
  const root = doc.documentElement;
2114
2672
  const canonical = canonicalize(root);
2115
- return crypto3.createHash("sha256").update(canonical, "utf-8").digest("base64");
2673
+ return crypto4.createHash("sha256").update(canonical, "utf-8").digest("base64");
2116
2674
  }
2117
2675
  function computeSignedPropertiesDigest(qualifyingPropertiesXml) {
2118
2676
  const parser = new DOMParser();
@@ -2122,7 +2680,7 @@ function computeSignedPropertiesDigest(qualifyingPropertiesXml) {
2122
2680
  throw new Error("SignedProperties element not found in QualifyingProperties");
2123
2681
  }
2124
2682
  const canonical = canonicalize(signedProps);
2125
- return crypto3.createHash("sha256").update(canonical, "utf-8").digest("base64");
2683
+ return crypto4.createHash("sha256").update(canonical, "utf-8").digest("base64");
2126
2684
  }
2127
2685
  function buildSignedInfo(signatureAlgorithm, rootDigest, signedPropertiesDigest) {
2128
2686
  return [
@@ -2153,12 +2711,12 @@ function computeSignatureValue(canonicalSignedInfo, privateKey, isEc) {
2153
2711
  const data = Buffer.from(canonicalSignedInfo, "utf-8");
2154
2712
  let signature;
2155
2713
  if (isEc) {
2156
- signature = crypto3.sign("sha256", data, {
2714
+ signature = crypto4.sign("sha256", data, {
2157
2715
  key: privateKey,
2158
2716
  dsaEncoding: "ieee-p1363"
2159
2717
  });
2160
2718
  } else {
2161
- signature = crypto3.sign("sha256", data, privateKey);
2719
+ signature = crypto4.sign("sha256", data, privateKey);
2162
2720
  }
2163
2721
  return signature.toString("base64");
2164
2722
  }
@@ -2258,11 +2816,11 @@ var init_signature_service = __esm({
2258
2816
  }
2259
2817
  const certDer = extractDerFromPem(certPem);
2260
2818
  const certBase64 = certDer.toString("base64");
2261
- const certDigest = crypto3.createHash("sha256").update(certDer).digest("base64");
2262
- const x5093 = new crypto3.X509Certificate(certPem);
2819
+ const certDigest = crypto4.createHash("sha256").update(certDer).digest("base64");
2820
+ const x5093 = new crypto4.X509Certificate(certPem);
2263
2821
  const issuerName = normalizeIssuerDn(x5093.issuer);
2264
2822
  const serialNumber = hexSerialToDecimal(x5093.serialNumber);
2265
- const privateKey = crypto3.createPrivateKey(
2823
+ const privateKey = crypto4.createPrivateKey(
2266
2824
  passphrase ? { key: privateKeyPem, format: "pem", passphrase } : privateKeyPem
2267
2825
  );
2268
2826
  const isEc = privateKey.asymmetricKeyType === "ec";
@@ -2427,6 +2985,7 @@ var init_client = __esm({
2427
2985
  init_cryptography_service();
2428
2986
  init_verification_link_service();
2429
2987
  init_auth_xml_builder();
2988
+ init_offline_invoice_workflow();
2430
2989
  KSeFClient = class {
2431
2990
  auth;
2432
2991
  activeSessions;
@@ -2445,6 +3004,8 @@ var init_client = __esm({
2445
3004
  qr;
2446
3005
  options;
2447
3006
  authManager;
3007
+ _baseRestClientConfig;
3008
+ _offline;
2448
3009
  constructor(options) {
2449
3010
  this.options = resolveOptions(options);
2450
3011
  const authManager = options?.authManager ?? new DefaultAuthManager(async () => {
@@ -2455,6 +3016,8 @@ var init_client = __esm({
2455
3016
  });
2456
3017
  this.authManager = authManager;
2457
3018
  const restClientConfig = buildRestClientConfig(options, authManager);
3019
+ const { authManager: _am, ...baseConfig } = restClientConfig;
3020
+ this._baseRestClientConfig = baseConfig;
2458
3021
  const restClient = new RestClient(this.options, restClientConfig);
2459
3022
  const fetcher = new CertificateFetcher(restClient);
2460
3023
  this.crypto = new CryptographyService(fetcher);
@@ -2473,6 +3036,16 @@ var init_client = __esm({
2473
3036
  this.testData = new TestDataService(restClient, this.options.environmentName);
2474
3037
  this.qr = new VerificationLinkService(this.options.baseQrUrl);
2475
3038
  }
3039
+ get offline() {
3040
+ if (!this._offline) {
3041
+ this._offline = new OfflineInvoiceWorkflow(this.qr);
3042
+ }
3043
+ return this._offline;
3044
+ }
3045
+ /** @internal Create a RestClient with a different AuthManager, preserving transport/retry/rateLimit config. */
3046
+ createScopedRestClient(authManager) {
3047
+ return new RestClient(this.options, { ...this._baseRestClientConfig, authManager });
3048
+ }
2476
3049
  async loginWithToken(token, nip) {
2477
3050
  const challenge2 = await this.auth.getChallenge();
2478
3051
  await this.crypto.init();
@@ -2507,10 +3080,10 @@ var init_client = __esm({
2507
3080
  }
2508
3081
  async awaitAuthReady(referenceNumber, authToken) {
2509
3082
  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}`);
3083
+ const status7 = await this.auth.getAuthStatus(referenceNumber, authToken);
3084
+ if (status7.status.code === 200) return;
3085
+ if (status7.status.code !== 100) {
3086
+ throw new Error(`Authentication failed with status ${status7.status.code}: ${status7.status.description}`);
2514
3087
  }
2515
3088
  await new Promise((r) => setTimeout(r, 1e3));
2516
3089
  }
@@ -2545,79 +3118,6 @@ var init_polling = __esm({
2545
3118
  }
2546
3119
  });
2547
3120
 
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
3121
  // src/xml/upo-parser.ts
2622
3122
  import { XMLParser } from "fast-xml-parser";
2623
3123
  function isRecord(value) {
@@ -2742,11 +3242,58 @@ var init_upo_parser = __esm({
2742
3242
  }
2743
3243
  });
2744
3244
 
3245
+ // src/xml/invoice-field-extractor.ts
3246
+ import { XMLParser as XMLParser2 } from "fast-xml-parser";
3247
+ function extractInvoiceFields(xml) {
3248
+ const parsed = invoiceParser.parse(xml);
3249
+ const root = parsed?.Faktura;
3250
+ if (!root || typeof root !== "object") {
3251
+ throw new Error(
3252
+ "Cannot extract invoice fields: missing <Faktura> root element. Ensure the XML is a valid KSeF invoice (FA_2, FA_3, or FA_RR)."
3253
+ );
3254
+ }
3255
+ if (root.Fa && typeof root.Fa === "object") {
3256
+ const invoiceDate = nonEmptyString(root.Fa.P_1);
3257
+ const invoiceNumber = nonEmptyString(root.Fa.P_2);
3258
+ if (invoiceDate && invoiceNumber) {
3259
+ return { invoiceNumber, invoiceDate };
3260
+ }
3261
+ }
3262
+ if (root.FakturaRR && typeof root.FakturaRR === "object") {
3263
+ const invoiceDate = nonEmptyString(root.FakturaRR.P_4B);
3264
+ const invoiceNumber = nonEmptyString(root.FakturaRR.P_4C);
3265
+ if (invoiceDate && invoiceNumber) {
3266
+ return { invoiceNumber, invoiceDate };
3267
+ }
3268
+ }
3269
+ throw new Error(
3270
+ "Cannot extract invoice number and date from XML. Expected <Fa><P_1>/<P_2> (FA format) or <FakturaRR><P_4B>/<P_4C> (RR format)."
3271
+ );
3272
+ }
3273
+ function nonEmptyString(value) {
3274
+ return typeof value === "string" && value.length > 0 ? value : void 0;
3275
+ }
3276
+ var invoiceParser;
3277
+ var init_invoice_field_extractor = __esm({
3278
+ "src/xml/invoice-field-extractor.ts"() {
3279
+ "use strict";
3280
+ invoiceParser = new XMLParser2({
3281
+ ignoreAttributes: false,
3282
+ attributeNamePrefix: "@_",
3283
+ parseTagValue: false,
3284
+ parseAttributeValue: false,
3285
+ removeNSPrefix: true,
3286
+ trimValues: false
3287
+ });
3288
+ }
3289
+ });
3290
+
2745
3291
  // src/xml/index.ts
2746
3292
  var init_xml = __esm({
2747
3293
  "src/xml/index.ts"() {
2748
3294
  "use strict";
2749
3295
  init_upo_parser();
3296
+ init_invoice_field_extractor();
2750
3297
  }
2751
3298
  });
2752
3299
 
@@ -4488,11 +5035,11 @@ async function validateSchema(xml, options, _parsed) {
4488
5035
  const prefix = rootElement ? `/${rootElement}/` : "/";
4489
5036
  const validationErrors = result.error.issues.map((issue) => {
4490
5037
  const zodPath = issue.path.join("/");
4491
- const path9 = zodPath ? `${prefix}${zodPath}` : rootElement ? `/${rootElement}` : void 0;
5038
+ const path10 = zodPath ? `${prefix}${zodPath}` : rootElement ? `/${rootElement}` : void 0;
4492
5039
  return {
4493
5040
  code: mapZodErrorCode(issue),
4494
5041
  message: issue.message,
4495
- path: path9
5042
+ path: path10
4496
5043
  };
4497
5044
  });
4498
5045
  return { valid: false, schemaType, errors: validationErrors };
@@ -4532,9 +5079,9 @@ function validateBusinessRules(xml, _parsed) {
4532
5079
  collectDateErrors(object, rootElement, errors);
4533
5080
  return { valid: errors.length === 0, schemaType, errors };
4534
5081
  }
4535
- function collectNipPeselErrors(obj, path9, errors) {
5082
+ function collectNipPeselErrors(obj, path10, errors) {
4536
5083
  for (const [key, value] of Object.entries(obj)) {
4537
- const currentPath = path9 ? `${path9}/${key}` : key;
5084
+ const currentPath = path10 ? `${path10}/${key}` : key;
4538
5085
  if (key === "NIP" && typeof value === "string") {
4539
5086
  if (!isValidNip(value)) {
4540
5087
  errors.push({
@@ -4614,7 +5161,7 @@ var init_invoice_validator = __esm({
4614
5161
  });
4615
5162
 
4616
5163
  // src/builders/batch-file.ts
4617
- import * as crypto4 from "crypto";
5164
+ import * as crypto6 from "crypto";
4618
5165
  function splitBuffer(data, maxPartSize) {
4619
5166
  if (data.length <= maxPartSize) {
4620
5167
  return [data];
@@ -4625,8 +5172,8 @@ function splitBuffer(data, maxPartSize) {
4625
5172
  }
4626
5173
  return parts;
4627
5174
  }
4628
- function sha256Base64(data) {
4629
- return crypto4.createHash("sha256").update(data).digest("base64");
5175
+ function sha256Base642(data) {
5176
+ return crypto6.createHash("sha256").update(data).digest("base64");
4630
5177
  }
4631
5178
  var BATCH_MAX_PART_SIZE, BATCH_MAX_TOTAL_SIZE, BATCH_MAX_PARTS, BatchFileBuilder;
4632
5179
  var init_batch_file = __esm({
@@ -4663,7 +5210,7 @@ var init_batch_file = __esm({
4663
5210
  `Data requires ${rawParts.length} parts, exceeding maximum of ${BATCH_MAX_PARTS}`
4664
5211
  );
4665
5212
  }
4666
- const zipHash = sha256Base64(zipBytes);
5213
+ const zipHash = sha256Base642(zipBytes);
4667
5214
  const encryptedParts = [];
4668
5215
  const fileParts = rawParts.map((raw, i) => {
4669
5216
  const encrypted = encryptFn(raw);
@@ -4671,7 +5218,7 @@ var init_batch_file = __esm({
4671
5218
  return {
4672
5219
  ordinalNumber: i + 1,
4673
5220
  fileSize: encrypted.length,
4674
- fileHash: sha256Base64(encrypted)
5221
+ fileHash: sha256Base642(encrypted)
4675
5222
  };
4676
5223
  });
4677
5224
  return {
@@ -4754,7 +5301,7 @@ var init_batch_file = __esm({
4754
5301
  encryptedChunks.push(value);
4755
5302
  }
4756
5303
  const encryptedData = new Uint8Array(Buffer.concat(encryptedChunks));
4757
- const encryptedMeta = sha256Base64(encryptedData);
5304
+ const encryptedMeta = sha256Base642(encryptedData);
4758
5305
  fileParts.push({
4759
5306
  ordinalNumber: partIndex + 1,
4760
5307
  fileSize: encryptedData.byteLength,
@@ -4798,6 +5345,9 @@ __export(batch_session_workflow_exports, {
4798
5345
  uploadBatchStreamParsed: () => uploadBatchStreamParsed
4799
5346
  });
4800
5347
  async function uploadBatch(client, zipData, options) {
5348
+ if (options?.parallelism !== void 0 && (!Number.isInteger(options.parallelism) || options.parallelism < 1)) {
5349
+ throw new Error("parallelism must be a positive integer");
5350
+ }
4801
5351
  await client.crypto.init();
4802
5352
  if (options?.validate) {
4803
5353
  const { unzip: unzip2 } = await Promise.resolve().then(() => (init_zip(), zip_exports));
@@ -4840,7 +5390,7 @@ async function uploadBatch(client, zipData, options) {
4840
5390
  },
4841
5391
  ordinalNumber: i + 1
4842
5392
  }));
4843
- await client.batchSession.sendParts(openResp, sendingParts);
5393
+ await client.batchSession.sendParts(openResp, sendingParts, options?.parallelism);
4844
5394
  await client.batchSession.closeSession(openResp.referenceNumber);
4845
5395
  const result = await pollUntil(
4846
5396
  () => client.sessionStatus.getSessionStatus(openResp.referenceNumber),
@@ -4861,6 +5411,9 @@ async function uploadBatch(client, zipData, options) {
4861
5411
  };
4862
5412
  }
4863
5413
  async function uploadBatchStream(client, zipStreamFactory, zipSize, options) {
5414
+ if (options?.parallelism !== void 0 && (!Number.isInteger(options.parallelism) || options.parallelism < 1)) {
5415
+ throw new Error("parallelism must be a positive integer");
5416
+ }
4864
5417
  await client.crypto.init();
4865
5418
  const encData = client.crypto.getEncryptionData();
4866
5419
  const formCode = options?.formCode ?? DEFAULT_FORM_CODE;
@@ -4882,7 +5435,7 @@ async function uploadBatchStream(client, zipStreamFactory, zipSize, options) {
4882
5435
  },
4883
5436
  options?.upoVersion
4884
5437
  );
4885
- await client.batchSession.sendPartsWithStream(openResp, streamParts);
5438
+ await client.batchSession.sendPartsWithStream(openResp, streamParts, options?.parallelism);
4886
5439
  await client.batchSession.closeSession(openResp.referenceNumber);
4887
5440
  const result = await pollUntil(
4888
5441
  () => client.sessionStatus.getSessionStatus(openResp.referenceNumber),
@@ -4937,10 +5490,10 @@ var init_batch_session_workflow = __esm({
4937
5490
  });
4938
5491
 
4939
5492
  // src/cli/index.ts
4940
- import { readFileSync as readFileSync8 } from "fs";
5493
+ import { readFileSync as readFileSync9 } from "fs";
4941
5494
  import { fileURLToPath } from "url";
4942
5495
  import { dirname as dirname2, resolve } from "path";
4943
- import { defineCommand as defineCommand17, runMain } from "citty";
5496
+ import { defineCommand as defineCommand18, runMain } from "citty";
4944
5497
 
4945
5498
  // src/cli/commands/config.ts
4946
5499
  import { defineCommand } from "citty";
@@ -5506,13 +6059,13 @@ var login = defineCommand2({
5506
6059
  if (token) {
5507
6060
  await client.loginWithToken(token, nip);
5508
6061
  } else if (args.p12) {
5509
- const fs13 = await import("fs");
5510
- const p12Buffer = fs13.readFileSync(args.p12);
6062
+ const fs15 = await import("fs");
6063
+ const p12Buffer = fs15.readFileSync(args.p12);
5511
6064
  await client.loginWithPkcs12(p12Buffer, args["p12-password"] ?? "", nip);
5512
6065
  } 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");
6066
+ const fs15 = await import("fs");
6067
+ const certPem = fs15.readFileSync(args.cert, "utf-8");
6068
+ const keyPem = fs15.readFileSync(args.key, "utf-8");
5516
6069
  await client.loginWithCertificate(certPem, keyPem, nip, args["key-password"]);
5517
6070
  } else {
5518
6071
  throw new Error("Provide --token, --p12, or both --cert and --key for authentication.");
@@ -6226,6 +6779,17 @@ var FileHwmStore = class {
6226
6779
  // src/workflows/invoice-export-workflow.ts
6227
6780
  init_zip();
6228
6781
  init_polling();
6782
+
6783
+ // src/utils/hash.ts
6784
+ import crypto5 from "crypto";
6785
+ function sha256Base64(data) {
6786
+ return crypto5.createHash("sha256").update(data).digest("base64");
6787
+ }
6788
+ function verifyHash(data, expectedHash) {
6789
+ return sha256Base64(data) === expectedHash;
6790
+ }
6791
+
6792
+ // src/workflows/invoice-export-workflow.ts
6229
6793
  async function doExport(client, filters, options) {
6230
6794
  await client.crypto.init();
6231
6795
  const encData = client.crypto.getEncryptionData();
@@ -6254,6 +6818,7 @@ async function doExport(client, filters, options) {
6254
6818
  url: p.url,
6255
6819
  method: p.method,
6256
6820
  partSize: p.partSize,
6821
+ partHash: p.partHash,
6257
6822
  encryptedPartSize: p.encryptedPartSize,
6258
6823
  encryptedPartHash: p.encryptedPartHash,
6259
6824
  expirationDate: p.expirationDate
@@ -6315,6 +6880,9 @@ async function incrementalExportAndDownload(client, options) {
6315
6880
  throw new Error(`Download failed for part ${part.ordinalNumber}: HTTP ${resp.status}`);
6316
6881
  }
6317
6882
  const encryptedData = new Uint8Array(await resp.arrayBuffer());
6883
+ if (options.verifyHash !== false && !verifyHash(encryptedData, part.encryptedPartHash)) {
6884
+ throw new Error(`Hash mismatch for export part ${part.ordinalNumber}`);
6885
+ }
6318
6886
  const decrypted = client.crypto.decryptAES256(encryptedData, encData.cipherKey, encData.cipherIv);
6319
6887
  decryptedParts.push(decrypted);
6320
6888
  }
@@ -6523,6 +7091,7 @@ var send = defineCommand5({
6523
7091
  stream: { type: "boolean", description: "Use stream-based batch upload (for ZIP files, reduces memory usage)" },
6524
7092
  formCode: { type: "string", description: "Document type: FA2, FA3, PEF3, PEFKOR3, FARR1 (default: FA3)" },
6525
7093
  validate: { type: "boolean", description: "Validate XML before sending" },
7094
+ parallelism: { type: "string", description: "Number of concurrent part uploads (batch mode)" },
6526
7095
  env: { type: "string", description: "Environment (test/demo/prod)" },
6527
7096
  json: { type: "boolean", description: "Output as JSON" },
6528
7097
  verbose: { type: "boolean", description: "Show HTTP request/response details" },
@@ -6536,6 +7105,10 @@ var send = defineCommand5({
6536
7105
  const config = loadConfig();
6537
7106
  const nip = args.nip ?? config.nip;
6538
7107
  const filePath = args.path;
7108
+ const parallelism = args.parallelism ? Number(args.parallelism) : void 0;
7109
+ if (parallelism !== void 0 && (!Number.isInteger(parallelism) || parallelism < 1)) {
7110
+ throw new Error("--parallelism must be a positive integer");
7111
+ }
6539
7112
  const formCodeKey = args.formCode;
6540
7113
  let formCode = DEFAULT_FORM_CODE;
6541
7114
  if (formCodeKey) {
@@ -6568,7 +7141,8 @@ var send = defineCommand5({
6568
7141
  if (!args.json) consola9.start(`Sending batch via stream (${(zipSize / 1e6).toFixed(1)} MB)...`);
6569
7142
  const result = await uploadBatchStream2(client, zipStreamFactory, zipSize, {
6570
7143
  formCode,
6571
- pollOptions: { intervalMs: 3e3 }
7144
+ pollOptions: { intervalMs: 3e3 },
7145
+ parallelism
6572
7146
  });
6573
7147
  if (args.json) {
6574
7148
  outputResult(result, { json: true });
@@ -6632,7 +7206,7 @@ var send = defineCommand5({
6632
7206
  { formCode, batchFile: batchFileInfo, encryption: encryptionData.encryptionInfo }
6633
7207
  );
6634
7208
  saveOnlineSessionRef(openResult.referenceNumber);
6635
- await client.batchSession.sendParts(openResult, parts);
7209
+ await client.batchSession.sendParts(openResult, parts, parallelism);
6636
7210
  await client.batchSession.closeSession(openResult.referenceNumber);
6637
7211
  clearOnlineSessionRef();
6638
7212
  if (args.json) {
@@ -6697,9 +7271,9 @@ var get = defineCommand5({
6697
7271
  return withErrorHandler(async () => {
6698
7272
  const globalOpts = getGlobalOpts4(args);
6699
7273
  const { client } = await requireSession(globalOpts);
6700
- const xml = await client.invoices.getInvoice(args.ksefNumber);
7274
+ const { xml, hash } = await client.invoices.getInvoice(args.ksefNumber);
6701
7275
  if (args.json) {
6702
- outputResult({ ksefNumber: args.ksefNumber, xml }, { json: true });
7276
+ outputResult({ ksefNumber: args.ksefNumber, xml, hash }, { json: true });
6703
7277
  return;
6704
7278
  }
6705
7279
  if (args.o) {
@@ -7616,12 +8190,12 @@ import fs10 from "fs";
7616
8190
  import path7 from "path";
7617
8191
 
7618
8192
  // src/crypto/certificate-service.ts
7619
- import * as crypto5 from "crypto";
8193
+ import * as crypto7 from "crypto";
7620
8194
  import * as x5092 from "@peculiar/x509";
7621
8195
  var CertificateService = class {
7622
8196
  static getSha256Fingerprint(certPem) {
7623
8197
  const der = extractDerFromPem2(certPem);
7624
- return crypto5.createHash("sha256").update(der).digest("hex").toUpperCase();
8198
+ return crypto7.createHash("sha256").update(der).digest("hex").toUpperCase();
7625
8199
  }
7626
8200
  static async generatePersonalCertificate(givenName, surname, serialNumber, commonName, method = "RSA") {
7627
8201
  const nameParts = [];
@@ -7644,7 +8218,7 @@ var CertificateService = class {
7644
8218
  }
7645
8219
  };
7646
8220
  async function generateSelfSigned(subject2, method) {
7647
- x5092.cryptoProvider.set(crypto5.webcrypto);
8221
+ x5092.cryptoProvider.set(crypto7.webcrypto);
7648
8222
  let algorithm;
7649
8223
  let signingAlgorithm;
7650
8224
  if (method === "ECDSA") {
@@ -7659,7 +8233,7 @@ async function generateSelfSigned(subject2, method) {
7659
8233
  };
7660
8234
  signingAlgorithm = algorithm;
7661
8235
  }
7662
- const keys = await crypto5.webcrypto.subtle.generateKey(
8236
+ const keys = await crypto7.webcrypto.subtle.generateKey(
7663
8237
  algorithm,
7664
8238
  true,
7665
8239
  ["sign", "verify"]
@@ -7676,9 +8250,9 @@ async function generateSelfSigned(subject2, method) {
7676
8250
  serialNumber: Date.now().toString(16)
7677
8251
  });
7678
8252
  const certPem = cert.toString("pem");
7679
- const pkcs8 = await crypto5.webcrypto.subtle.exportKey("pkcs8", keys.privateKey);
8253
+ const pkcs8 = await crypto7.webcrypto.subtle.exportKey("pkcs8", keys.privateKey);
7680
8254
  const privateKeyPem = pemEncode2(new Uint8Array(pkcs8), "PRIVATE KEY");
7681
- const fingerprint = crypto5.createHash("sha256").update(Buffer.from(cert.rawData)).digest("hex").toUpperCase();
8255
+ const fingerprint = crypto7.createHash("sha256").update(Buffer.from(cert.rawData)).digest("hex").toUpperCase();
7682
8256
  return { certificatePem: certPem, privateKeyPem, fingerprint };
7683
8257
  }
7684
8258
  function extractDerFromPem2(pem) {
@@ -8136,6 +8710,7 @@ var invoice2 = defineCommand9({
8136
8710
  format: { type: "string", description: "Output format: png or svg (default: png)" },
8137
8711
  size: { type: "string", description: "QR code size in pixels (default: 300)" },
8138
8712
  label: { type: "string", description: "Label text (SVG only)" },
8713
+ offline: { type: "boolean", description: 'Use "OFFLINE" as label (SVG, overrides --label)' },
8139
8714
  o: { type: "string", description: "Output file path" },
8140
8715
  env: { type: "string", description: "Environment (test/demo/prod)" },
8141
8716
  json: { type: "boolean", description: "Output as JSON" },
@@ -8155,7 +8730,8 @@ var invoice2 = defineCommand9({
8155
8730
  return;
8156
8731
  }
8157
8732
  if (format === "svg") {
8158
- const svg = args.label ? await QrCodeService.generateQrCodeSvgWithLabel(url2, args.label, { width: size }) : await QrCodeService.generateQrCodeSvg(url2, { width: size });
8733
+ const effectiveLabel = args.offline ? "OFFLINE" : args.label;
8734
+ const svg = effectiveLabel ? await QrCodeService.generateQrCodeSvgWithLabel(url2, effectiveLabel, { width: size }) : await QrCodeService.generateQrCodeSvg(url2, { width: size });
8159
8735
  if (args.o) {
8160
8736
  fs11.writeFileSync(args.o, svg);
8161
8737
  outputSuccess(`QR code saved to ${args.o}
@@ -9022,11 +9598,11 @@ var doctorCommand = defineCommand14({
9022
9598
  const controller = new AbortController();
9023
9599
  const timeout = setTimeout(() => controller.abort(), 5e3);
9024
9600
  try {
9025
- const status6 = await client.lighthouse.getStatus();
9601
+ const status7 = await client.lighthouse.getStatus();
9026
9602
  checks.push({
9027
9603
  name: "connectivity",
9028
9604
  status: "pass",
9029
- message: `API reachable (${status6.status})`
9605
+ message: `API reachable (${status7.status})`
9030
9606
  });
9031
9607
  } finally {
9032
9608
  clearTimeout(timeout);
@@ -9470,11 +10046,474 @@ var setupCommand = defineCommand16({
9470
10046
  }
9471
10047
  });
9472
10048
 
10049
+ // src/cli/commands/offline.ts
10050
+ import * as fs14 from "fs";
10051
+ import { defineCommand as defineCommand17 } from "citty";
10052
+ import { consola as consola15 } from "consola";
10053
+
10054
+ // src/offline/file-storage.ts
10055
+ import * as fs13 from "fs/promises";
10056
+ import * as path9 from "path";
10057
+ import * as os6 from "os";
10058
+
10059
+ // src/offline/storage.ts
10060
+ function matchesFilter(invoice3, filter) {
10061
+ if (filter.status !== void 0) {
10062
+ const statuses = Array.isArray(filter.status) ? filter.status : [filter.status];
10063
+ if (!statuses.includes(invoice3.status)) return false;
10064
+ }
10065
+ if (filter.mode !== void 0 && invoice3.mode !== filter.mode) return false;
10066
+ if (filter.sellerNip !== void 0 && invoice3.sellerNip !== filter.sellerNip) return false;
10067
+ if (filter.expiringBefore !== void 0) {
10068
+ const cutoff = typeof filter.expiringBefore === "string" ? new Date(filter.expiringBefore).getTime() : filter.expiringBefore.getTime();
10069
+ if (new Date(invoice3.submitBy).getTime() >= cutoff) return false;
10070
+ }
10071
+ return true;
10072
+ }
10073
+
10074
+ // src/offline/file-storage.ts
10075
+ function resolveDir(dir) {
10076
+ if (dir === "~" || dir.startsWith("~/")) {
10077
+ return path9.join(os6.homedir(), dir.slice(1));
10078
+ }
10079
+ return dir;
10080
+ }
10081
+ var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
10082
+ function validateId(id) {
10083
+ if (!UUID_RE.test(id)) {
10084
+ throw new Error(`Invalid invoice ID: ${id}`);
10085
+ }
10086
+ }
10087
+ var FileOfflineInvoiceStorage = class {
10088
+ dir;
10089
+ constructor(directory) {
10090
+ this.dir = resolveDir(directory ?? "~/.ksef/offline");
10091
+ }
10092
+ async ensureDir() {
10093
+ await fs13.mkdir(this.dir, { recursive: true });
10094
+ }
10095
+ filePath(id) {
10096
+ validateId(id);
10097
+ return path9.join(this.dir, `${id}.json`);
10098
+ }
10099
+ async save(invoice3) {
10100
+ await this.ensureDir();
10101
+ const file = this.filePath(invoice3.id);
10102
+ const tmp = `${file}.tmp`;
10103
+ await fs13.writeFile(tmp, JSON.stringify(invoice3, null, 2));
10104
+ await fs13.rename(tmp, file);
10105
+ }
10106
+ async get(id) {
10107
+ const file = this.filePath(id);
10108
+ try {
10109
+ return JSON.parse(await fs13.readFile(file, "utf-8"));
10110
+ } catch (err) {
10111
+ if (err instanceof Error && "code" in err && err.code === "ENOENT") {
10112
+ return null;
10113
+ }
10114
+ console.warn(`Warning: Failed to read offline invoice ${id}: ${err instanceof Error ? err.message : String(err)}`);
10115
+ return null;
10116
+ }
10117
+ }
10118
+ async list(filter) {
10119
+ let files;
10120
+ try {
10121
+ files = (await fs13.readdir(this.dir)).filter((f) => f.endsWith(".json"));
10122
+ } catch {
10123
+ return [];
10124
+ }
10125
+ const results = [];
10126
+ for (const file of files) {
10127
+ try {
10128
+ const data = JSON.parse(
10129
+ await fs13.readFile(path9.join(this.dir, file), "utf-8")
10130
+ );
10131
+ if (!filter || matchesFilter(data, filter)) {
10132
+ results.push(data);
10133
+ }
10134
+ } catch (err) {
10135
+ console.warn(`Warning: Skipping corrupt offline invoice file ${file}: ${err instanceof Error ? err.message : String(err)}`);
10136
+ }
10137
+ }
10138
+ return results;
10139
+ }
10140
+ /**
10141
+ * Update invoice metadata (read-modify-write).
10142
+ *
10143
+ * Note: No file locking — concurrent updates to the same ID may cause
10144
+ * lost writes. Acceptable for CLI (single process). Library consumers
10145
+ * running parallel operations should use external locking.
10146
+ */
10147
+ async update(id, updates) {
10148
+ const existing = await this.get(id);
10149
+ if (!existing) throw new Error(`Offline invoice not found: ${id}`);
10150
+ await this.save({ ...existing, ...updates });
10151
+ }
10152
+ async delete(id) {
10153
+ const file = this.filePath(id);
10154
+ try {
10155
+ await fs13.unlink(file);
10156
+ } catch (e) {
10157
+ if (e instanceof Error && "code" in e && e.code !== "ENOENT") throw e;
10158
+ }
10159
+ }
10160
+ };
10161
+
10162
+ // src/cli/commands/offline.ts
10163
+ init_invoice_field_extractor();
10164
+ function getGlobalOpts14(args) {
10165
+ return {
10166
+ env: args.env,
10167
+ json: args.json,
10168
+ verbose: args.verbose,
10169
+ timeout: args.timeout,
10170
+ nip: args.nip
10171
+ };
10172
+ }
10173
+ function getStorage(args) {
10174
+ return new FileOfflineInvoiceStorage(args["store-dir"]);
10175
+ }
10176
+ var globalArgs = {
10177
+ env: { type: "string", description: "Environment (test/demo/prod)" },
10178
+ json: { type: "boolean", description: "Output as JSON" },
10179
+ verbose: { type: "boolean", description: "Verbose output" },
10180
+ timeout: { type: "string", description: "Request timeout (ms)" },
10181
+ nip: { type: "string", description: "NIP number" },
10182
+ "store-dir": { type: "string", description: "Offline invoice store directory" }
10183
+ };
10184
+ var generate3 = defineCommand17({
10185
+ meta: { name: "generate", description: "Generate offline invoice metadata with QR codes" },
10186
+ args: {
10187
+ ...globalArgs,
10188
+ xml: { type: "positional", description: "Invoice XML file path", required: true },
10189
+ mode: { type: "string", description: "Offline mode (offline24/offline/awaryjny)" },
10190
+ key: { type: "string", description: "Private key PEM file for KOD II signing" },
10191
+ "cert-serial": { type: "string", description: "Certificate serial number (hex)" },
10192
+ "context-type": { type: "string", description: "Seller context type (default: Nip)" },
10193
+ "context-id": { type: "string", description: "Seller context value" },
10194
+ "qr-format": { type: "string", description: "QR format: png or svg (default: png)" },
10195
+ "qr-out": { type: "string", description: "Directory to save QR images" },
10196
+ "no-store": { type: "boolean", description: "Do not save to local store" }
10197
+ },
10198
+ run({ args }) {
10199
+ return withErrorHandler(async () => {
10200
+ const globalOpts = getGlobalOpts14(args);
10201
+ const config = loadConfig();
10202
+ const xmlPath = args.xml;
10203
+ if (!fs14.existsSync(xmlPath)) {
10204
+ throw new Error(`File not found: ${xmlPath}`);
10205
+ }
10206
+ const invoiceXml = fs14.readFileSync(xmlPath, "utf-8");
10207
+ const sellerNip = args.nip ?? config.nip;
10208
+ if (!sellerNip) {
10209
+ throw new Error("NIP is required. Use --nip or set via `ksef config set --nip`");
10210
+ }
10211
+ const contextType = args["context-type"] ?? "Nip";
10212
+ const contextId = args["context-id"] ?? sellerNip;
10213
+ const client = createClient(globalOpts);
10214
+ const workflow = client.offline;
10215
+ if (args.key && !args["cert-serial"]) {
10216
+ throw new Error("--cert-serial is required when using --key");
10217
+ }
10218
+ const certificate2 = args.key ? {
10219
+ privateKeyPem: fs14.readFileSync(args.key, "utf-8"),
10220
+ certificateSerial: args["cert-serial"]
10221
+ } : void 0;
10222
+ const validModes = ["offline24", "offline", "awaryjny", "awaria_calkowita"];
10223
+ const modeArg = args.mode ?? "offline24";
10224
+ if (!validModes.includes(modeArg)) {
10225
+ throw new Error(`Invalid --mode "${modeArg}". Must be one of: ${validModes.join(", ")}`);
10226
+ }
10227
+ const storage = args["no-store"] ? void 0 : getStorage(args);
10228
+ const { invoiceNumber, invoiceDate } = extractInvoiceFields(invoiceXml);
10229
+ const metadata = await workflow.generate(
10230
+ {
10231
+ invoiceNumber,
10232
+ invoiceDate,
10233
+ invoiceXml,
10234
+ sellerNip,
10235
+ sellerIdentifier: { type: contextType, value: contextId }
10236
+ },
10237
+ {
10238
+ mode: modeArg,
10239
+ certificate: certificate2,
10240
+ storage
10241
+ }
10242
+ );
10243
+ if (args["qr-out"]) {
10244
+ const outDir = args["qr-out"];
10245
+ fs14.mkdirSync(outDir, { recursive: true });
10246
+ const format = args["qr-format"] ?? "png";
10247
+ if (format !== "png" && format !== "svg") {
10248
+ throw new Error(`Invalid --qr-format "${format}". Must be one of: png, svg`);
10249
+ }
10250
+ const shortId = metadata.id.slice(0, 8);
10251
+ if (format === "svg") {
10252
+ const svg1 = await QrCodeService.generateQrCodeSvgWithLabel(metadata.kod1Url, "OFFLINE");
10253
+ fs14.writeFileSync(`${outDir}/${shortId}-kod1.svg`, svg1);
10254
+ if (metadata.kod2Url) {
10255
+ const svg2 = await QrCodeService.generateQrCodeSvgWithLabel(metadata.kod2Url, "CERTYFIKAT");
10256
+ fs14.writeFileSync(`${outDir}/${shortId}-kod2.svg`, svg2);
10257
+ }
10258
+ } else {
10259
+ const png1 = await QrCodeService.generateQrCode(metadata.kod1Url);
10260
+ fs14.writeFileSync(`${outDir}/${shortId}-kod1.png`, png1);
10261
+ if (metadata.kod2Url) {
10262
+ const png2 = await QrCodeService.generateQrCode(metadata.kod2Url);
10263
+ fs14.writeFileSync(`${outDir}/${shortId}-kod2.png`, png2);
10264
+ }
10265
+ }
10266
+ outputSuccess(`QR codes saved to ${outDir}/`);
10267
+ }
10268
+ if (!certificate2) {
10269
+ outputWarning("No certificate provided \u2014 KOD II QR code was not generated. Use --key and --cert-serial for offline compliance.");
10270
+ }
10271
+ if (args.json) {
10272
+ outputResult(metadata, { json: true });
10273
+ } else {
10274
+ outputKeyValue({
10275
+ ID: metadata.id,
10276
+ Mode: metadata.mode,
10277
+ Status: metadata.status,
10278
+ Deadline: metadata.submitBy,
10279
+ "KOD I": metadata.kod1Url,
10280
+ "KOD II": metadata.kod2Url ?? "(not generated)"
10281
+ }, { json: false });
10282
+ }
10283
+ });
10284
+ }
10285
+ });
10286
+ var list4 = defineCommand17({
10287
+ meta: { name: "list", description: "List stored offline invoices" },
10288
+ args: {
10289
+ ...globalArgs,
10290
+ status: { type: "string", description: "Filter by status" },
10291
+ mode: { type: "string", description: "Filter by mode" },
10292
+ expiring: { type: "boolean", description: "Show only invoices expiring within 24h" }
10293
+ },
10294
+ run({ args }) {
10295
+ return withErrorHandler(async () => {
10296
+ const storage = getStorage(args);
10297
+ const filter = {};
10298
+ if (args.status) filter.status = args.status;
10299
+ if (args.mode) filter.mode = args.mode;
10300
+ if (args.expiring) {
10301
+ const cutoff = new Date(Date.now() + 24 * 60 * 60 * 1e3);
10302
+ filter.expiringBefore = cutoff.toISOString();
10303
+ }
10304
+ const invoices2 = await storage.list(Object.keys(filter).length > 0 ? filter : void 0);
10305
+ if (invoices2.length === 0) {
10306
+ if (args.json) {
10307
+ outputResult([], { json: true });
10308
+ } else {
10309
+ outputSuccess("No offline invoices found.");
10310
+ }
10311
+ return;
10312
+ }
10313
+ outputTable(
10314
+ invoices2.map((i) => ({
10315
+ id: i.id.slice(0, 8),
10316
+ number: i.invoiceNumber,
10317
+ mode: i.mode,
10318
+ status: i.status,
10319
+ deadline: i.submitBy.slice(0, 19),
10320
+ nip: i.sellerNip
10321
+ })),
10322
+ [
10323
+ { key: "id", label: "ID" },
10324
+ { key: "number", label: "Number" },
10325
+ { key: "mode", label: "Mode" },
10326
+ { key: "status", label: "Status" },
10327
+ { key: "deadline", label: "Deadline" },
10328
+ { key: "nip", label: "NIP" }
10329
+ ],
10330
+ { json: args.json }
10331
+ );
10332
+ });
10333
+ }
10334
+ });
10335
+ var status6 = defineCommand17({
10336
+ meta: { name: "status", description: "Show offline invoice details" },
10337
+ args: {
10338
+ ...globalArgs,
10339
+ id: { type: "positional", description: "Invoice ID", required: true }
10340
+ },
10341
+ run({ args }) {
10342
+ return withErrorHandler(async () => {
10343
+ const storage = getStorage(args);
10344
+ const invoice3 = await storage.get(args.id);
10345
+ if (!invoice3) {
10346
+ throw new Error(`Offline invoice not found: ${args.id}`);
10347
+ }
10348
+ if (args.json) {
10349
+ outputResult(invoice3, { json: true });
10350
+ } else {
10351
+ outputKeyValue({
10352
+ ID: invoice3.id,
10353
+ Number: invoice3.invoiceNumber,
10354
+ Date: invoice3.invoiceDate,
10355
+ Mode: invoice3.mode,
10356
+ Reason: invoice3.reason,
10357
+ Status: invoice3.status,
10358
+ "Seller NIP": invoice3.sellerNip,
10359
+ Deadline: invoice3.submitBy,
10360
+ "Generated At": invoice3.generatedAt,
10361
+ "Submitted At": invoice3.submittedAt ?? "-",
10362
+ "KSeF Reference": invoice3.ksefReferenceNumber ?? "-",
10363
+ "KOD I URL": invoice3.kod1Url,
10364
+ "KOD II URL": invoice3.kod2Url ?? "-",
10365
+ Error: invoice3.error ? `${invoice3.error.code}: ${invoice3.error.message}` : "-"
10366
+ }, { json: false });
10367
+ }
10368
+ });
10369
+ }
10370
+ });
10371
+ var submit = defineCommand17({
10372
+ meta: { name: "submit", description: "Submit offline invoices to KSeF" },
10373
+ args: {
10374
+ ...globalArgs,
10375
+ all: { type: "boolean", description: "Submit all pending invoices" },
10376
+ "no-check-expiry": { type: "boolean", description: "Skip expiry check" },
10377
+ ids: { type: "positional", description: "Invoice IDs to submit" }
10378
+ },
10379
+ run({ args }) {
10380
+ return withErrorHandler(async () => {
10381
+ const globalOpts = getGlobalOpts14(args);
10382
+ const { client } = await requireSession(globalOpts);
10383
+ const storage = getStorage(args);
10384
+ const invoiceIds = args.ids ? args.ids.split(",").map((s) => s.trim()) : void 0;
10385
+ if (args.all && invoiceIds) {
10386
+ throw new Error("Cannot use --all together with specific invoice IDs. Use one or the other.");
10387
+ }
10388
+ if (!args.all && !invoiceIds) {
10389
+ throw new Error("Specify invoice IDs or use --all to submit all pending invoices.");
10390
+ }
10391
+ const result = await client.offline.submit(client, {
10392
+ storage,
10393
+ invoiceIds: args.all ? void 0 : invoiceIds,
10394
+ checkExpiry: !args["no-check-expiry"]
10395
+ });
10396
+ if (result.total === 0) {
10397
+ outputSuccess("No pending invoices to submit.");
10398
+ return;
10399
+ }
10400
+ if (args.json) {
10401
+ outputResult(result, { json: true });
10402
+ } else {
10403
+ outputSuccess(
10404
+ `Submitted: ${result.submitted}, Accepted: ${result.accepted}, Rejected: ${result.rejected}, Expired: ${result.expired}`
10405
+ );
10406
+ if (result.results.length > 0) {
10407
+ outputTable(
10408
+ result.results.map((r) => ({
10409
+ id: r.invoiceId.slice(0, 8),
10410
+ number: r.invoiceNumber,
10411
+ status: r.status,
10412
+ ref: r.ksefReferenceNumber ?? "-"
10413
+ })),
10414
+ [
10415
+ { key: "id", label: "ID" },
10416
+ { key: "number", label: "Number" },
10417
+ { key: "status", label: "Status" },
10418
+ { key: "ref", label: "KSeF Ref" }
10419
+ ],
10420
+ { json: false }
10421
+ );
10422
+ }
10423
+ }
10424
+ });
10425
+ }
10426
+ });
10427
+ var correct = defineCommand17({
10428
+ meta: { name: "correct", description: "Technical correction for rejected offline invoice" },
10429
+ args: {
10430
+ ...globalArgs,
10431
+ id: { type: "positional", description: "Rejected invoice ID", required: true },
10432
+ xml: { type: "positional", description: "Corrected XML file path", required: true }
10433
+ },
10434
+ run({ args }) {
10435
+ return withErrorHandler(async () => {
10436
+ const globalOpts = getGlobalOpts14(args);
10437
+ const { client } = await requireSession(globalOpts);
10438
+ const storage = getStorage(args);
10439
+ const xmlPath = args.xml;
10440
+ if (!fs14.existsSync(xmlPath)) {
10441
+ throw new Error(`File not found: ${xmlPath}`);
10442
+ }
10443
+ const correctedXml = fs14.readFileSync(xmlPath, "utf-8");
10444
+ const result = await client.offline.correct(client, {
10445
+ rejectedInvoiceId: args.id,
10446
+ correctedInvoiceXml: correctedXml,
10447
+ storage
10448
+ });
10449
+ if (args.json) {
10450
+ outputResult(result, { json: true });
10451
+ } else {
10452
+ outputSuccess(`Correction submitted. KSeF ref: ${result.ksefReferenceNumber ?? "pending"}`);
10453
+ }
10454
+ });
10455
+ }
10456
+ });
10457
+ var del = defineCommand17({
10458
+ meta: { name: "delete", description: "Delete offline invoice from local store" },
10459
+ args: {
10460
+ ...globalArgs,
10461
+ id: { type: "positional", description: "Invoice ID to delete" },
10462
+ expired: { type: "boolean", description: "Delete all expired invoices" },
10463
+ force: { type: "boolean", description: "Skip confirmation prompt" }
10464
+ },
10465
+ run({ args }) {
10466
+ return withErrorHandler(async () => {
10467
+ const storage = getStorage(args);
10468
+ if (args.expired) {
10469
+ const expired = await storage.list({ status: "EXPIRED" });
10470
+ if (expired.length === 0) {
10471
+ outputSuccess("No expired invoices to delete.");
10472
+ return;
10473
+ }
10474
+ if (!args.force) {
10475
+ if (!process.stdin.isTTY) {
10476
+ throw new Error(
10477
+ `${expired.length} expired invoice(s) would be deleted. Use --force to confirm in non-interactive mode.`
10478
+ );
10479
+ }
10480
+ const confirmed = await consola15.prompt(
10481
+ `Delete ${expired.length} expired invoice(s)?`,
10482
+ { type: "confirm", initial: false }
10483
+ );
10484
+ if (!confirmed) {
10485
+ consola15.info("Cancelled.");
10486
+ return;
10487
+ }
10488
+ }
10489
+ for (const inv of expired) {
10490
+ await storage.delete(inv.id);
10491
+ }
10492
+ outputSuccess(`Deleted ${expired.length} expired invoice(s).`);
10493
+ return;
10494
+ }
10495
+ if (!args.id) {
10496
+ throw new Error("Specify an invoice ID or use --expired to delete all expired invoices.");
10497
+ }
10498
+ const invoice3 = await storage.get(args.id);
10499
+ if (!invoice3) {
10500
+ throw new Error(`Offline invoice not found: ${args.id}`);
10501
+ }
10502
+ await storage.delete(args.id);
10503
+ outputSuccess(`Deleted offline invoice ${args.id}`);
10504
+ });
10505
+ }
10506
+ });
10507
+ var offlineCommand = defineCommand17({
10508
+ meta: { name: "offline", description: "Offline invoice management" },
10509
+ subCommands: { generate: generate3, list: list4, status: status6, submit, correct, delete: del }
10510
+ });
10511
+
9473
10512
  // src/cli/index.ts
9474
10513
  var __dirname = dirname2(fileURLToPath(import.meta.url));
9475
- var pkg = JSON.parse(readFileSync8(resolve(__dirname, "..", "package.json"), "utf-8"));
10514
+ var pkg = JSON.parse(readFileSync9(resolve(__dirname, "..", "package.json"), "utf-8"));
9476
10515
  var version = pkg.version;
9477
- var main = defineCommand17({
10516
+ var main = defineCommand18({
9478
10517
  meta: {
9479
10518
  name: "ksef",
9480
10519
  version,
@@ -9494,6 +10533,7 @@ var main = defineCommand17({
9494
10533
  limits: limitsCommand,
9495
10534
  peppol: peppolCommand,
9496
10535
  "test-data": testDataCommand,
10536
+ offline: offlineCommand,
9497
10537
  doctor: doctorCommand,
9498
10538
  completion: completionCommand
9499
10539
  }