bysquare 2.10.0 → 2.12.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/README.md CHANGED
@@ -1,24 +1,18 @@
1
1
  # bysquare
2
2
 
3
- Simple JavaScript library to encode and decode "PAY by square" string.
3
+ "PAY by square" is a national standard for QR code payments that was adopted by
4
+ the Slovak Banking Association in 2013. It is incorporated into a variety of
5
+ invoices, reminders and other payment regulations.
4
6
 
5
- **What is `PAY by square`?**
7
+ ## What it is
6
8
 
7
- It's a national standard for QR code payments that was adopted by the Slovak
8
- Banking Association in 2013. It is incorporated into a variety of invoices,
9
- reminders and other payment regulations.
9
+ - Simple JavaScript library to encode and decode "PAY by square" string.
10
+ - Aim to support simpe programming interface to encode and decode data for QR.
10
11
 
11
- **Can I generate an image?**
12
+ ## What it is not
12
13
 
13
- This library doesn't have a specific opinion and how the QR code string is
14
- transformed into images depends on how you implement it. See
15
- [examples](./docs/examples/).
16
-
17
- ## Features
18
-
19
- - Encode data to qr string
20
- - Decode data to json
21
- - Detect bysquare from qr string
14
+ - Generating QR code images.
15
+ - Parsing QR code images.
22
16
 
23
17
  ## Installation
24
18
 
@@ -63,59 +57,58 @@ import {
63
57
 
64
58
  ## Usage
65
59
 
66
- ### Encode
60
+ ### Basic Usage
61
+
62
+ Simple helper functions to wrap encoding for most common use cases.
63
+
64
+ - `simplePayment` - Encode simple payment data.
65
+ - `directDebit` - Encode direct debit data.
66
+ - `standingOrder` - Encode standing order data.
67
+
68
+ ```typescript
69
+ import { simplePayment } from "bysquare";
70
+
71
+ const qrstring = simplePayment({
72
+ amount: 100,
73
+ variableSymbol: "123456",
74
+ currencyCode: CurrencyCode.EUR,
75
+ iban: "SK9611000000002918599669",
76
+ });
77
+ ```
78
+
79
+ ### Adavanced usage
80
+
81
+ For more complex data use `encode` and `decode` functions:
67
82
 
68
83
  ```ts
69
84
  import {
70
85
  CurrencyCode,
71
86
  DataModel,
87
+ decode,
72
88
  encode,
73
89
  PaymentOptions,
74
90
  } from "bysquare";
75
91
 
76
- // string ready to be encoded to QR
77
- const qrString = encode({
92
+ const data = {
78
93
  invoiceId: "random-id",
79
94
  payments: [
80
95
  {
81
96
  type: PaymentOptions.PaymentOrder,
82
- amount: 100.0,
83
- bankAccounts: [
84
- {
85
- iban: "SK9611000000002918599669",
86
- },
87
- ],
88
97
  currencyCode: CurrencyCode.EUR,
98
+ amount: 100.0,
89
99
  variableSymbol: "123",
100
+ paymentNote: "hello world",
101
+ bankAccounts: [{ iban: "SK9611000000002918599669" }],
102
+ // ...more fields
90
103
  },
91
104
  ],
92
- });
93
- ```
105
+ } satisfies DataModel;
94
106
 
95
- ### Decode
107
+ // Encode data to a QR string
108
+ const qrstring = encode(data);
96
109
 
97
- ```ts
98
- import { decode } from "bysquare";
99
-
100
- const model = decode(
101
- "0405QH8090IFU27IV0J6HGGLIOTIBVHNQQJQ6LAVGNBT363HR13JC6CB54HSI0KH9FCRASHNQBSKAQD2LJ4AU400UVKDNDPFRKLOBEVVVU0QJ000",
102
- );
103
-
104
- // {
105
- // invoiceId: "random-id",
106
- // payments: [
107
- // {
108
- // type: 1,
109
- // amount: 100.0,
110
- // bankAccounts: [
111
- // { iban: "SK9611000000002918599669" },
112
- // ],
113
- // currencyCode: "EUR",
114
- // variableSymbol: "123",
115
- // }
116
- // ]
117
- // }
118
- //
110
+ // Decode QR string back to the original data model
111
+ const model = decode(qrstring);
119
112
  ```
120
113
 
121
114
  ## CLI
@@ -0,0 +1,2 @@
1
+ export declare function encode(input: Uint8Array, addPadding?: boolean): string;
2
+ export declare function decode(input: string, isLoose?: boolean): Uint8Array;
@@ -0,0 +1,53 @@
1
+ const base32Hex = {
2
+ chars: "0123456789ABCDEFGHIJKLMNOPQRSTUV",
3
+ bits: 5,
4
+ mask: 0b11111, // Mask to extract 5 bits
5
+ };
6
+ export function encode(input, addPadding = true) {
7
+ const output = Array();
8
+ let buffer = 0;
9
+ let bitsLeft = 0;
10
+ for (let i = 0; i < input.length; i++) {
11
+ buffer = (buffer << 8) | input[i];
12
+ bitsLeft += 8;
13
+ while (bitsLeft >= base32Hex.bits) {
14
+ bitsLeft -= base32Hex.bits;
15
+ const index = (buffer >> bitsLeft) & base32Hex.mask;
16
+ output.push(base32Hex.chars[index]);
17
+ }
18
+ }
19
+ if (bitsLeft > 0) {
20
+ const maskedValue = (buffer << (base32Hex.bits - bitsLeft)) & base32Hex.mask;
21
+ output.push(base32Hex.chars[maskedValue]);
22
+ }
23
+ let base32hex = output.join("");
24
+ if (addPadding) {
25
+ const paddedLength = Math.ceil(base32hex.length / 8) * 8;
26
+ base32hex = base32hex.padEnd(paddedLength, "=");
27
+ }
28
+ return base32hex;
29
+ }
30
+ export function decode(input, isLoose = false) {
31
+ if (isLoose) {
32
+ input = input.toUpperCase();
33
+ const paddingNeeded = (8 - (input.length % 8)) % 8;
34
+ input += "=".repeat(paddingNeeded);
35
+ }
36
+ input = input.replace(/=+$/, "");
37
+ const output = Array();
38
+ let buffer = 0;
39
+ let bitsLeft = 0;
40
+ for (let i = 0; i < input.length; i++) {
41
+ const index = base32Hex.chars.indexOf(input[i]);
42
+ if (index === -1) {
43
+ throw new Error("Invalid base32hex string");
44
+ }
45
+ buffer = (buffer << base32Hex.bits) | index;
46
+ bitsLeft += base32Hex.bits;
47
+ if (bitsLeft >= 8) {
48
+ bitsLeft -= 8;
49
+ output.push((buffer >> bitsLeft) & 0xFF);
50
+ }
51
+ }
52
+ return Uint8Array.from(output);
53
+ }
@@ -0,0 +1 @@
1
+ export declare function crc32(data: string): number;
package/dist/crc32.js ADDED
@@ -0,0 +1,63 @@
1
+ // Computed CRC32 lookup table
2
+ // const CRC32_TABLE = new Uint32Array(256);
3
+ // for (let i = 0; i < CRC32_TABLE.length; i++) {
4
+ // let crc = i;
5
+ // for (let j = 0; j < 8; j++) {
6
+ // crc = (crc >>> 1) ^ (0xEDB88320 * (crc & 1));
7
+ // }
8
+ // CRC32_TABLE[i] = crc;
9
+ // }
10
+ // dprint-ignore
11
+ const CRC32_TABLE = [
12
+ 0x00000000, 0x77073096, 0xee0e612c, 0x990951ba, 0x076dc419, 0x706af48f,
13
+ 0xe963a535, 0x9e6495a3, 0x0edb8832, 0x79dcb8a4, 0xe0d5e91e, 0x97d2d988,
14
+ 0x09b64c2b, 0x7eb17cbd, 0xe7b82d07, 0x90bf1d91, 0x1db71064, 0x6ab020f2,
15
+ 0xf3b97148, 0x84be41de, 0x1adad47d, 0x6ddde4eb, 0xf4d4b551, 0x83d385c7,
16
+ 0x136c9856, 0x646ba8c0, 0xfd62f97a, 0x8a65c9ec, 0x14015c4f, 0x63066cd9,
17
+ 0xfa0f3d63, 0x8d080df5, 0x3b6e20c8, 0x4c69105e, 0xd56041e4, 0xa2677172,
18
+ 0x3c03e4d1, 0x4b04d447, 0xd20d85fd, 0xa50ab56b, 0x35b5a8fa, 0x42b2986c,
19
+ 0xdbbbc9d6, 0xacbcf940, 0x32d86ce3, 0x45df5c75, 0xdcd60dcf, 0xabd13d59,
20
+ 0x26d930ac, 0x51de003a, 0xc8d75180, 0xbfd06116, 0x21b4f4b5, 0x56b3c423,
21
+ 0xcfba9599, 0xb8bda50f, 0x2802b89e, 0x5f058808, 0xc60cd9b2, 0xb10be924,
22
+ 0x2f6f7c87, 0x58684c11, 0xc1611dab, 0xb6662d3d, 0x76dc4190, 0x01db7106,
23
+ 0x98d220bc, 0xefd5102a, 0x71b18589, 0x06b6b51f, 0x9fbfe4a5, 0xe8b8d433,
24
+ 0x7807c9a2, 0x0f00f934, 0x9609a88e, 0xe10e9818, 0x7f6a0dbb, 0x086d3d2d,
25
+ 0x91646c97, 0xe6635c01, 0x6b6b51f4, 0x1c6c6162, 0x856530d8, 0xf262004e,
26
+ 0x6c0695ed, 0x1b01a57b, 0x8208f4c1, 0xf50fc457, 0x65b0d9c6, 0x12b7e950,
27
+ 0x8bbeb8ea, 0xfcb9887c, 0x62dd1ddf, 0x15da2d49, 0x8cd37cf3, 0xfbd44c65,
28
+ 0x4db26158, 0x3ab551ce, 0xa3bc0074, 0xd4bb30e2, 0x4adfa541, 0x3dd895d7,
29
+ 0xa4d1c46d, 0xd3d6f4fb, 0x4369e96a, 0x346ed9fc, 0xad678846, 0xda60b8d0,
30
+ 0x44042d73, 0x33031de5, 0xaa0a4c5f, 0xdd0d7cc9, 0x5005713c, 0x270241aa,
31
+ 0xbe0b1010, 0xc90c2086, 0x5768b525, 0x206f85b3, 0xb966d409, 0xce61e49f,
32
+ 0x5edef90e, 0x29d9c998, 0xb0d09822, 0xc7d7a8b4, 0x59b33d17, 0x2eb40d81,
33
+ 0xb7bd5c3b, 0xc0ba6cad, 0xedb88320, 0x9abfb3b6, 0x03b6e20c, 0x74b1d29a,
34
+ 0xead54739, 0x9dd277af, 0x04db2615, 0x73dc1683, 0xe3630b12, 0x94643b84,
35
+ 0x0d6d6a3e, 0x7a6a5aa8, 0xe40ecf0b, 0x9309ff9d, 0x0a00ae27, 0x7d079eb1,
36
+ 0xf00f9344, 0x8708a3d2, 0x1e01f268, 0x6906c2fe, 0xf762575d, 0x806567cb,
37
+ 0x196c3671, 0x6e6b06e7, 0xfed41b76, 0x89d32be0, 0x10da7a5a, 0x67dd4acc,
38
+ 0xf9b9df6f, 0x8ebeeff9, 0x17b7be43, 0x60b08ed5, 0xd6d6a3e8, 0xa1d1937e,
39
+ 0x38d8c2c4, 0x4fdff252, 0xd1bb67f1, 0xa6bc5767, 0x3fb506dd, 0x48b2364b,
40
+ 0xd80d2bda, 0xaf0a1b4c, 0x36034af6, 0x41047a60, 0xdf60efc3, 0xa867df55,
41
+ 0x316e8eef, 0x4669be79, 0xcb61b38c, 0xbc66831a, 0x256fd2a0, 0x5268e236,
42
+ 0xcc0c7795, 0xbb0b4703, 0x220216b9, 0x5505262f, 0xc5ba3bbe, 0xb2bd0b28,
43
+ 0x2bb45a92, 0x5cb36a04, 0xc2d7ffa7, 0xb5d0cf31, 0x2cd99e8b, 0x5bdeae1d,
44
+ 0x9b64c2b0, 0xec63f226, 0x756aa39c, 0x026d930a, 0x9c0906a9, 0xeb0e363f,
45
+ 0x72076785, 0x05005713, 0x95bf4a82, 0xe2b87a14, 0x7bb12bae, 0x0cb61b38,
46
+ 0x92d28e9b, 0xe5d5be0d, 0x7cdcefb7, 0x0bdbdf21, 0x86d3d2d4, 0xf1d4e242,
47
+ 0x68ddb3f8, 0x1fda836e, 0x81be16cd, 0xf6b9265b, 0x6fb077e1, 0x18b74777,
48
+ 0x88085ae6, 0xff0f6a70, 0x66063bca, 0x11010b5c, 0x8f659eff, 0xf862ae69,
49
+ 0x616bffd3, 0x166ccf45, 0xa00ae278, 0xd70dd2ee, 0x4e048354, 0x3903b3c2,
50
+ 0xa7672661, 0xd06016f7, 0x4969474d, 0x3e6e77db, 0xaed16a4a, 0xd9d65adc,
51
+ 0x40df0b66, 0x37d83bf0, 0xa9bcae53, 0xdebb9ec5, 0x47b2cf7f, 0x30b5ffe9,
52
+ 0xbdbdf21c, 0xcabac28a, 0x53b39330, 0x24b4a3a6, 0xbad03605, 0xcdd70693,
53
+ 0x54de5729, 0x23d967bf, 0xb3667a2e, 0xc4614ab8, 0x5d681b02, 0x2a6f2b94,
54
+ 0xb40bbe37, 0xc30c8ea1, 0x5a05df1b, 0x2d02ef8d
55
+ ];
56
+ export function crc32(data) {
57
+ let crc = 0 ^ (-1);
58
+ const encoded = new TextEncoder().encode(data);
59
+ for (let i = 0; i < encoded.length; i++) {
60
+ crc = (crc >>> 8) ^ CRC32_TABLE[(crc ^ encoded[i]) & 0xFF];
61
+ }
62
+ return (crc ^ (-1)) >>> 0;
63
+ }
package/dist/decode.d.ts CHANGED
@@ -1,15 +1,31 @@
1
1
  import { DataModel } from "./index.js";
2
+ export declare enum DecodeErrorMessage {
3
+ MissingIBAN = "IBAN is missing",
4
+ /**
5
+ * @description - find original LZMA error in extensions
6
+ */
7
+ LZMADecompressionFailed = "LZMA decompression failed",
8
+ /**
9
+ * @description - find found version in extensions
10
+ * @see {@link ./types#Version} for valid ranges
11
+ */
12
+ UnsupportedVersion = "Unsupported version"
13
+ }
14
+ export declare class DecodeError extends Error {
15
+ name: string;
16
+ extensions?: {
17
+ [name: string]: any;
18
+ };
19
+ constructor(message: DecodeErrorMessage, extensions?: {
20
+ [name: string]: any;
21
+ });
22
+ }
2
23
  /**
3
24
  * Generating by square Code
4
25
  *
5
26
  * @see 3.14.
6
27
  */
7
28
  export declare function deserialize(qr: string): DataModel;
8
- export declare class DecodeError extends Error {
9
- cause: Error;
10
- name: string;
11
- constructor(cause: Error, msg?: string);
12
- }
13
29
  /** @deprecated */
14
30
  export declare const parse: typeof decode;
15
31
  /**
@@ -21,7 +37,7 @@ export declare function decode(qr: string): DataModel;
21
37
  /**
22
38
  * Detect if qr string contains bysquare header.
23
39
  *
24
- * Bysquare header does not have too much information, therefore it is
25
- * not very reliable, there is room for improvement for the future.
40
+ * There is not magic header in the bysquare specification.
41
+ * Version is just 4 bites, so it is possible to have false positives.
26
42
  */
27
43
  export declare function detect(qr: string): boolean;
package/dist/decode.js CHANGED
@@ -1,6 +1,29 @@
1
1
  import { decompress } from "lzma1";
2
- import { base32hex } from "rfc4648";
2
+ import * as base32hex from "./base32hex.js";
3
3
  import { CurrencyCode, PaymentOptions, Version, } from "./index.js";
4
+ export var DecodeErrorMessage;
5
+ (function (DecodeErrorMessage) {
6
+ DecodeErrorMessage["MissingIBAN"] = "IBAN is missing";
7
+ /**
8
+ * @description - find original LZMA error in extensions
9
+ */
10
+ DecodeErrorMessage["LZMADecompressionFailed"] = "LZMA decompression failed";
11
+ /**
12
+ * @description - find found version in extensions
13
+ * @see {@link ./types#Version} for valid ranges
14
+ */
15
+ DecodeErrorMessage["UnsupportedVersion"] = "Unsupported version";
16
+ })(DecodeErrorMessage || (DecodeErrorMessage = {}));
17
+ export class DecodeError extends Error {
18
+ name = "DecodeError";
19
+ extensions;
20
+ constructor(message, extensions) {
21
+ super(message);
22
+ if (extensions) {
23
+ this.extensions = extensions;
24
+ }
25
+ }
26
+ }
4
27
  function cleanUndefined(obj) {
5
28
  Object.keys(obj).forEach((key) => {
6
29
  if (typeof obj[key] === "undefined") {
@@ -8,6 +31,12 @@ function cleanUndefined(obj) {
8
31
  }
9
32
  });
10
33
  }
34
+ function decodeNumber(value) {
35
+ return value?.length ? Number(value) : undefined;
36
+ }
37
+ function decodeString(value) {
38
+ return value?.length ? value : undefined;
39
+ }
11
40
  /**
12
41
  * Generating by square Code
13
42
  *
@@ -26,82 +55,62 @@ export function deserialize(qr) {
26
55
  const ammount = data.shift();
27
56
  const currency = data.shift();
28
57
  const dueDate = data.shift();
29
- const variables = data.shift();
30
- const constants = data.shift();
31
- const specifics = data.shift();
58
+ const variableSymbol = data.shift();
59
+ const constantSymbol = data.shift();
60
+ const specificSymbol = data.shift();
32
61
  const originatorRefInfo = data.shift();
33
62
  const paymentNote = data.shift();
34
63
  let payment = {
35
64
  bankAccounts: [],
36
65
  type: Number(paymentOptions),
37
66
  currencyCode: currency ?? CurrencyCode.EUR,
38
- amount: ammount?.length
39
- ? Number(ammount)
40
- : undefined,
41
- paymentDueDate: dueDate?.length
42
- ? dueDate
43
- : undefined,
44
- variableSymbol: variables?.length
45
- ? variables
46
- : undefined,
47
- constantSymbol: constants?.length
48
- ? constants
49
- : undefined,
50
- specificSymbol: specifics?.length
51
- ? specifics
52
- : undefined,
53
- originatorsReferenceInformation: originatorRefInfo?.length
54
- ? originatorRefInfo
55
- : undefined,
56
- paymentNote: paymentNote?.length
57
- ? paymentNote
58
- : undefined,
67
+ amount: Number(ammount),
68
+ paymentDueDate: dueDate || undefined,
69
+ variableSymbol: variableSymbol || undefined,
70
+ constantSymbol: constantSymbol || undefined,
71
+ specificSymbol: specificSymbol || undefined,
72
+ originatorsReferenceInformation: originatorRefInfo || undefined,
73
+ paymentNote: paymentNote || undefined,
59
74
  };
60
- const accountslen = Number(data.shift());
61
- for (let j = 0; j < accountslen; j++) {
75
+ const numberOfAccounts = Number(data.shift());
76
+ for (let j = 0; j < numberOfAccounts; j++) {
62
77
  const iban = data.shift();
63
78
  if (iban === undefined || iban.length === 0) {
64
- throw new Error("Missing IBAN");
79
+ throw new DecodeError(DecodeErrorMessage.MissingIBAN);
65
80
  }
66
81
  const bic = data.shift();
67
82
  const account = {
68
83
  iban: iban,
69
- bic: bic?.length
70
- ? bic
71
- : undefined,
84
+ bic: bic || undefined,
72
85
  };
73
86
  cleanUndefined(account);
74
87
  payment.bankAccounts.push(account);
75
88
  }
76
- data.shift(); // StandingOrderExt
77
- data.shift(); // DirectDebitExt
78
- // narrowing payment type
79
- switch (payment.type) {
80
- case PaymentOptions.PaymentOrder:
81
- break;
82
- case PaymentOptions.StandingOrder:
83
- payment = {
84
- ...payment,
85
- day: Number(data.shift()),
86
- month: Number(data.shift()),
87
- periodicity: data.shift(),
88
- lastDate: data.shift(),
89
- };
90
- break;
91
- case PaymentOptions.DirectDebit:
92
- payment = {
93
- ...payment,
94
- directDebitScheme: Number(data.shift()),
95
- directDebitType: Number(data.shift()),
96
- mandateId: data.shift(),
97
- creditorId: data.shift(),
98
- contractId: data.shift(),
99
- maxAmount: Number(data.shift()),
100
- validTillDate: data.shift(),
101
- };
102
- break;
103
- default:
104
- break;
89
+ const standingOrderExt = data.shift();
90
+ if (standingOrderExt === "1" && payment.type === PaymentOptions.StandingOrder) {
91
+ payment = {
92
+ ...payment,
93
+ day: decodeNumber(data.shift()),
94
+ month: decodeNumber(data.shift()),
95
+ periodicity: decodeString(data.shift()),
96
+ lastDate: decodeString(data.shift()),
97
+ };
98
+ }
99
+ const directDebitExt = data.shift();
100
+ if (directDebitExt === "1" && payment.type === PaymentOptions.DirectDebit) {
101
+ payment = {
102
+ ...payment,
103
+ directDebitScheme: decodeNumber(data.shift()),
104
+ directDebitType: decodeNumber(data.shift()),
105
+ variableSymbol: decodeString(data.shift()),
106
+ specificSymbol: decodeString(data.shift()),
107
+ originatorsReferenceInformation: decodeString(data.shift()),
108
+ mandateId: decodeString(data.shift()),
109
+ creditorId: decodeString(data.shift()),
110
+ contractId: decodeString(data.shift()),
111
+ maxAmount: decodeNumber(data.shift()),
112
+ validTillDate: decodeString(data.shift()),
113
+ };
105
114
  }
106
115
  cleanUndefined(payment);
107
116
  output.payments.push(payment);
@@ -112,15 +121,9 @@ export function deserialize(qr) {
112
121
  const addressLine2 = data.shift();
113
122
  if (Boolean(name) || Boolean(addressLine1) || Boolean(addressLine2)) {
114
123
  const beneficiary = {
115
- name: name?.length
116
- ? name
117
- : undefined,
118
- street: addressLine1?.length
119
- ? addressLine1
120
- : undefined,
121
- city: addressLine2?.length
122
- ? addressLine2
123
- : undefined,
124
+ name: name || undefined,
125
+ street: addressLine1 || undefined,
126
+ city: addressLine2 || undefined,
124
127
  };
125
128
  cleanUndefined(beneficiary);
126
129
  output.payments[i].beneficiary = beneficiary;
@@ -148,14 +151,6 @@ function bysquareHeaderDecoder(header) {
148
151
  reserved,
149
152
  };
150
153
  }
151
- export class DecodeError extends Error {
152
- cause;
153
- name = "DecodeError";
154
- constructor(cause, msg) {
155
- super(msg);
156
- this.cause = cause;
157
- }
158
- }
159
154
  /** @deprecated */
160
155
  export const parse = decode;
161
156
  /**
@@ -164,17 +159,13 @@ export const parse = decode;
164
159
  * @see 3.16.
165
160
  */
166
161
  export function decode(qr) {
167
- let bytes;
168
- try {
169
- bytes = base32hex.parse(qr, { loose: true });
170
- }
171
- catch (error) {
172
- throw new DecodeError(error, "Unable to decode QR string base32hex encoding");
173
- }
162
+ const bytes = base32hex.decode(qr);
174
163
  const bysquareHeader = bytes.slice(0, 2);
175
164
  const decodedBysquareHeader = bysquareHeaderDecoder(bysquareHeader);
176
165
  if ((decodedBysquareHeader.version > Version["1.1.0"])) {
177
- throw new Error(`Unsupported Bysquare version '${decodedBysquareHeader.version}' in header detected. Only '0' and '1' values are supported`);
166
+ throw new DecodeError(DecodeErrorMessage.UnsupportedVersion, {
167
+ version: decodedBysquareHeader.version,
168
+ });
178
169
  }
179
170
  /**
180
171
  * The process of decompressing data requires the addition of an LZMA header
@@ -182,11 +173,13 @@ export function decode(qr) {
182
173
  * algorithm to properly interpret and extract the original uncompressed
183
174
  * data. Bysquare only store properties
184
175
  *
185
- * <----------------------- 13-bytes ----------------------->
176
+ * @see https://docs.fileformat.com/compression/lzma/
186
177
  *
187
- * +------------+----+----+----+----+--+--+--+--+--+--+--+--+
188
- * | Properties | Dictionary Size | Uncompressed Size |
189
- * +------------+----+----+----+----+--+--+--+--+--+--+--+--+
178
+ * +---------------+---------------------------+-------------------+
179
+ * | 1B | 4B | 8B |
180
+ * +---------------+---------------------------+-------------------+
181
+ * | Properties | Dictionary Size | Uncompressed Size |
182
+ * +---------------+---------------------------+-------------------+
190
183
  */
191
184
  const defaultProperties = [0x5D]; // lc=3, lp=0, pb=2
192
185
  const defaultDictionarySize = [0x00, 0x02, 0x00, 0x00]; // 2^17
@@ -207,7 +200,7 @@ export function decode(qr) {
207
200
  decompressed = decompress(body);
208
201
  }
209
202
  catch (error) {
210
- throw new DecodeError(error, "LZMA decompression failed");
203
+ throw new DecodeError(DecodeErrorMessage.LZMADecompressionFailed, { error });
211
204
  }
212
205
  if (typeof decompressed === "string") {
213
206
  return deserialize(decompressed);
@@ -220,21 +213,21 @@ export function decode(qr) {
220
213
  /**
221
214
  * Detect if qr string contains bysquare header.
222
215
  *
223
- * Bysquare header does not have too much information, therefore it is
224
- * not very reliable, there is room for improvement for the future.
216
+ * There is not magic header in the bysquare specification.
217
+ * Version is just 4 bites, so it is possible to have false positives.
225
218
  */
226
219
  export function detect(qr) {
227
- let parsed;
220
+ let decoded;
228
221
  try {
229
- parsed = base32hex.parse(qr, { loose: true });
222
+ decoded = base32hex.decode(qr, true);
230
223
  }
231
- catch {
224
+ catch (error) {
232
225
  return false;
233
226
  }
234
- if (parsed.byteLength < 2) {
227
+ if (decoded.byteLength < 2) {
235
228
  return false;
236
229
  }
237
- const bysquareHeader = parsed.subarray(0, 2);
230
+ const bysquareHeader = decoded.subarray(0, 2);
238
231
  const header = bysquareHeaderDecoder(bysquareHeader);
239
232
  const isValid = [
240
233
  header.bysquareType,
package/dist/encode.d.ts CHANGED
@@ -1,4 +1,38 @@
1
1
  import { DataModel } from "./types.js";
2
+ export declare enum EncodeErrorMessage {
3
+ /**
4
+ * @description - find invalid value in extensions
5
+ */
6
+ BySquareType = "Invalid BySquareType value in header, valid range <0,15>",
7
+ /**
8
+ * @description - find invalid value in extensions
9
+ * @see {@link ./types#Version} for valid ranges
10
+ */
11
+ Version = "Invalid Version value in header",
12
+ /**
13
+ * @description - find invalid value in extensions
14
+ */
15
+ DocumentType = "Invalid DocumentType value in header, valid range <0,15>",
16
+ /**
17
+ * @description - find invalid value in extensions
18
+ */
19
+ Reserved = "Invalid Reserved value in header, valid range <0,15>",
20
+ /**
21
+ * @description - find actual size of header in extensions
22
+ * @see MAX_COMPRESSED_SIZE
23
+ */
24
+ HeaderDataSize = "Allowed header data size exceeded"
25
+ }
26
+ export declare class EncodeError extends Error {
27
+ name: string;
28
+ extensions?: {
29
+ [name: string]: any;
30
+ };
31
+ constructor(message: EncodeErrorMessage, extensions?: {
32
+ [name: string]: any;
33
+ });
34
+ }
35
+ export declare const MAX_COMPRESSED_SIZE = 131072;
2
36
  /**
3
37
  * Returns a 2 byte buffer that represents the header of the bysquare
4
38
  * specification
@@ -40,6 +74,7 @@ export declare function addChecksum(serialized: string): Uint8Array;
40
74
  * @see Table 15.
41
75
  */
42
76
  export declare function serialize(data: DataModel): string;
77
+ export declare function removeDiacritics(model: DataModel): void;
43
78
  type Options = {
44
79
  /**
45
80
  * Many banking apps do not support diacritics, which results in errors when
package/dist/encode.js CHANGED
@@ -1,10 +1,45 @@
1
- import crc32 from "crc-32";
2
1
  import { compress } from "lzma1";
3
- import { base32hex } from "rfc4648";
2
+ import * as base32hex from "./base32hex.js";
3
+ import { crc32 } from "./crc32.js";
4
4
  import { deburr } from "./deburr.js";
5
5
  import { PaymentOptions, Version, } from "./types.js";
6
6
  import { validateDataModel } from "./validations.js";
7
- const MAX_COMPRESSED_SIZE = 131_072; // 2^17
7
+ export var EncodeErrorMessage;
8
+ (function (EncodeErrorMessage) {
9
+ /**
10
+ * @description - find invalid value in extensions
11
+ */
12
+ EncodeErrorMessage["BySquareType"] = "Invalid BySquareType value in header, valid range <0,15>";
13
+ /**
14
+ * @description - find invalid value in extensions
15
+ * @see {@link ./types#Version} for valid ranges
16
+ */
17
+ EncodeErrorMessage["Version"] = "Invalid Version value in header";
18
+ /**
19
+ * @description - find invalid value in extensions
20
+ */
21
+ EncodeErrorMessage["DocumentType"] = "Invalid DocumentType value in header, valid range <0,15>";
22
+ /**
23
+ * @description - find invalid value in extensions
24
+ */
25
+ EncodeErrorMessage["Reserved"] = "Invalid Reserved value in header, valid range <0,15>";
26
+ /**
27
+ * @description - find actual size of header in extensions
28
+ * @see MAX_COMPRESSED_SIZE
29
+ */
30
+ EncodeErrorMessage["HeaderDataSize"] = "Allowed header data size exceeded";
31
+ })(EncodeErrorMessage || (EncodeErrorMessage = {}));
32
+ export class EncodeError extends Error {
33
+ name = "EncodeError";
34
+ extensions;
35
+ constructor(message, extensions) {
36
+ super(message);
37
+ if (extensions) {
38
+ this.extensions = extensions;
39
+ }
40
+ }
41
+ }
42
+ export const MAX_COMPRESSED_SIZE = 131_072; // 2^17
8
43
  /**
9
44
  * Returns a 2 byte buffer that represents the header of the bysquare
10
45
  * specification
@@ -27,16 +62,16 @@ header = [
27
62
  0x00, 0x00
28
63
  ]) {
29
64
  if (header[0] < 0 || header[0] > 15) {
30
- throw new Error(`Invalid BySquareType value '${header[0]}' in header, valid range <0,15>`);
65
+ throw new EncodeError(EncodeErrorMessage.BySquareType, { invalidValue: header[0] });
31
66
  }
32
67
  if (header[1] < 0 || header[1] > 15) {
33
- throw new Error(`Invalid Version value '${header[1]}' in header, valid range <0,15>`);
68
+ throw new EncodeError(EncodeErrorMessage.Version, { invalidValue: header[1] });
34
69
  }
35
70
  if (header[2] < 0 || header[2] > 15) {
36
- throw new Error(`Invalid DocumentType value '${header[2]}' in header, valid range <0,15>`);
71
+ throw new EncodeError(EncodeErrorMessage.DocumentType, { invalidValue: header[2] });
37
72
  }
38
73
  if (header[3] < 0 || header[3] > 15) {
39
- throw new Error(`Invalid Reserved value '${header[3]}' in header, valid range <0,15>`);
74
+ throw new EncodeError(EncodeErrorMessage.Reserved, { invalidValue: header[3] });
40
75
  }
41
76
  const [bySquareType, version, documentType, reserved,] = header;
42
77
  // Combine 4-nibbles to 2-bytes
@@ -52,7 +87,10 @@ header = [
52
87
  */
53
88
  export function headerDataLength(length) {
54
89
  if (length >= MAX_COMPRESSED_SIZE) {
55
- throw new Error(`Data size ${length} exceeds limit of ${MAX_COMPRESSED_SIZE} bytes`);
90
+ throw new EncodeError(EncodeErrorMessage.HeaderDataSize, {
91
+ actualSize: length,
92
+ allowedSize: MAX_COMPRESSED_SIZE,
93
+ });
56
94
  }
57
95
  const header = new ArrayBuffer(2);
58
96
  new DataView(header).setUint16(0, length, true);
@@ -65,7 +103,7 @@ export function headerDataLength(length) {
65
103
  */
66
104
  export function addChecksum(serialized) {
67
105
  const checksum = new ArrayBuffer(4);
68
- new DataView(checksum).setUint32(0, crc32.str(serialized), true);
106
+ new DataView(checksum).setUint32(0, crc32(serialized), true);
69
107
  const byteArray = new TextEncoder().encode(serialized);
70
108
  return Uint8Array.from([
71
109
  ...new Uint8Array(checksum),
@@ -131,7 +169,7 @@ export function serialize(data) {
131
169
  }
132
170
  return serialized.join("\t");
133
171
  }
134
- function removeDiacritics(model) {
172
+ export function removeDiacritics(model) {
135
173
  for (const payment of model.payments) {
136
174
  if (payment.paymentNote) {
137
175
  payment.paymentNote = deburr(payment.paymentNote);
@@ -161,19 +199,29 @@ export function encode(model, options) {
161
199
  validateDataModel(model);
162
200
  }
163
201
  const payload = serialize(model);
164
- const withChecksum = addChecksum(payload);
165
- const compressed = Uint8Array.from(compress(withChecksum));
166
- const _lzmaHeader = Uint8Array.from(compressed.subarray(0, 13));
167
- const lzmaBody = Uint8Array.from(compressed.subarray(13));
202
+ const payloadChecked = addChecksum(payload);
203
+ const payloadCompressed = Uint8Array.from(compress(payloadChecked));
204
+ /**
205
+ * The LZMA files has a 13-byte header that is followed by the LZMA
206
+ * compressed data.
207
+ *
208
+ * @see https://docs.fileformat.com/compression/lzma/
209
+ *
210
+ * +---------------+---------------------------+-------------------+
211
+ * | 1B | 4B | 8B |
212
+ * +---------------+---------------------------+-------------------+
213
+ * | Properties | Dictionary Size | Uncompressed Size |
214
+ * +---------------+---------------------------+-------------------+
215
+ */
216
+ const _lzmaHeader = Uint8Array.from(payloadCompressed.subarray(0, 13));
217
+ const lzmaBody = Uint8Array.from(payloadCompressed.subarray(13));
168
218
  const output = Uint8Array.from([
169
219
  // NOTE: Newer version 1.1.0 is not supported by all apps (e.g., TatraBanka).
170
220
  // We recommend using version "1.0.0" for better compatibility.
171
221
  // ...headerBysquare([0x00, Version["1.1.0"], 0x00, 0x00]),
172
222
  ...headerBysquare([0x00, Version["1.0.0"], 0x00, 0x00]),
173
- ...headerDataLength(withChecksum.byteLength),
223
+ ...headerDataLength(payloadChecked.byteLength),
174
224
  ...lzmaBody,
175
225
  ]);
176
- return base32hex.stringify(output, {
177
- pad: false,
178
- });
226
+ return base32hex.encode(output, false);
179
227
  }
@@ -0,0 +1,16 @@
1
+ import { type BankAccount, SimplePayment, type StandingOrder } from "./types.js";
2
+ type PaymentInput = Pick<BankAccount, "iban"> & Pick<SimplePayment, "amount" | "currencyCode" | "variableSymbol">;
3
+ /**
4
+ * Vytvorí QR pre jednorázovú platbu
5
+ */
6
+ export declare function simplePayment(input: PaymentInput): string;
7
+ /**
8
+ * Vytvorí QR pre inkaso
9
+ */
10
+ export declare function directDebit(input: PaymentInput): string;
11
+ type StandingInput = PaymentInput & Pick<StandingOrder, "day" | "periodicity">;
12
+ /**
13
+ * Vytvorí QR pre trvalý príkaz
14
+ */
15
+ export declare function standingOrder(input: StandingInput): string;
16
+ export {};
@@ -0,0 +1,52 @@
1
+ import { encode } from "./encode.js";
2
+ import { CurrencyCode, PaymentOptions, } from "./types.js";
3
+ /**
4
+ * Vytvorí QR pre jednorázovú platbu
5
+ */
6
+ export function simplePayment(input) {
7
+ return encode({
8
+ payments: [
9
+ {
10
+ type: PaymentOptions.PaymentOrder,
11
+ amount: input.amount,
12
+ variableSymbol: input.variableSymbol,
13
+ currencyCode: CurrencyCode.EUR,
14
+ bankAccounts: [{ iban: input.iban }],
15
+ },
16
+ ],
17
+ });
18
+ }
19
+ /**
20
+ * Vytvorí QR pre inkaso
21
+ */
22
+ export function directDebit(input) {
23
+ return encode({
24
+ payments: [
25
+ {
26
+ type: PaymentOptions.DirectDebit,
27
+ amount: input.amount,
28
+ variableSymbol: input.variableSymbol,
29
+ currencyCode: CurrencyCode.EUR,
30
+ bankAccounts: [{ iban: input.iban }],
31
+ },
32
+ ],
33
+ });
34
+ }
35
+ /**
36
+ * Vytvorí QR pre trvalý príkaz
37
+ */
38
+ export function standingOrder(input) {
39
+ return encode({
40
+ payments: [
41
+ {
42
+ type: PaymentOptions.StandingOrder,
43
+ day: input.day,
44
+ periodicity: input.periodicity,
45
+ amount: input.amount,
46
+ variableSymbol: input.variableSymbol,
47
+ currencyCode: CurrencyCode.EUR,
48
+ bankAccounts: [{ iban: input.iban }],
49
+ },
50
+ ],
51
+ });
52
+ }
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export { decode, detect, parse } from "./decode.js";
2
2
  export { encode, generate } from "./encode.js";
3
3
  export { validateDataModel, ValidationErrorMessage } from "./validations.js";
4
+ export * from "./helpers.js";
4
5
  export * from "./types.js";
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  export { decode, detect, parse } from "./decode.js";
2
2
  export { encode, generate } from "./encode.js";
3
3
  export { validateDataModel, ValidationErrorMessage } from "./validations.js";
4
+ export * from "./helpers.js";
4
5
  export * from "./types.js";
@@ -0,0 +1,43 @@
1
+ import { CurrencyCode, PaymentOptions, Periodicity } from "./types.js";
2
+ export declare const payloadWithPaymentOrder: {
3
+ invoiceId: string;
4
+ payments: {
5
+ type: PaymentOptions.PaymentOrder;
6
+ amount: number;
7
+ bankAccounts: {
8
+ iban: string;
9
+ }[];
10
+ currencyCode: CurrencyCode;
11
+ variableSymbol: string;
12
+ }[];
13
+ };
14
+ export declare const serializedPaymentOrder: string;
15
+ export declare const payloadWithStandingOrder: {
16
+ invoiceId: string;
17
+ payments: {
18
+ type: PaymentOptions.StandingOrder;
19
+ amount: number;
20
+ bankAccounts: {
21
+ iban: string;
22
+ }[];
23
+ periodicity: Periodicity.Monthly;
24
+ currencyCode: CurrencyCode;
25
+ variableSymbol: string;
26
+ lastDate: string;
27
+ day: number;
28
+ }[];
29
+ };
30
+ export declare const serializedStandingOrder: string;
31
+ export declare const payloadWithDirectDebit: {
32
+ invoiceId: string;
33
+ payments: {
34
+ type: PaymentOptions.DirectDebit;
35
+ amount: number;
36
+ bankAccounts: {
37
+ iban: string;
38
+ }[];
39
+ currencyCode: CurrencyCode;
40
+ variableSymbol: string;
41
+ }[];
42
+ };
43
+ export declare const serializedDirectDebit: string;
@@ -0,0 +1,123 @@
1
+ import { CurrencyCode, PaymentOptions, Periodicity, } from "./types.js";
2
+ export const payloadWithPaymentOrder = {
3
+ invoiceId: "random-id",
4
+ payments: [
5
+ {
6
+ type: PaymentOptions.PaymentOrder,
7
+ amount: 100.0,
8
+ bankAccounts: [
9
+ { iban: "SK9611000000002918599669" },
10
+ ],
11
+ currencyCode: CurrencyCode.EUR,
12
+ variableSymbol: "123",
13
+ },
14
+ ],
15
+ };
16
+ export const serializedPaymentOrder = /** dprint-ignore */ [
17
+ "random-id",
18
+ "\t", "1",
19
+ "\t", "1",
20
+ "\t", "100",
21
+ "\t", "EUR",
22
+ "\t",
23
+ "\t", "123",
24
+ "\t",
25
+ "\t",
26
+ "\t",
27
+ "\t",
28
+ "\t", "1",
29
+ "\t", "SK9611000000002918599669",
30
+ "\t",
31
+ "\t", "0",
32
+ "\t", "0",
33
+ "\t",
34
+ "\t",
35
+ "\t",
36
+ ].join("");
37
+ export const payloadWithStandingOrder = {
38
+ invoiceId: "random-id",
39
+ payments: [
40
+ {
41
+ type: PaymentOptions.StandingOrder,
42
+ amount: 100.0,
43
+ bankAccounts: [
44
+ { iban: "SK9611000000002918599669" },
45
+ ],
46
+ periodicity: Periodicity.Monthly,
47
+ currencyCode: CurrencyCode.EUR,
48
+ variableSymbol: "123",
49
+ lastDate: "20241011",
50
+ day: 1,
51
+ },
52
+ ],
53
+ };
54
+ export const serializedStandingOrder = /** dprint-ignore */ [
55
+ "random-id",
56
+ "\t", "1",
57
+ "\t", "2",
58
+ "\t", "100",
59
+ "\t", "EUR",
60
+ "\t",
61
+ "\t", "123",
62
+ "\t",
63
+ "\t",
64
+ "\t",
65
+ "\t",
66
+ "\t", "1",
67
+ "\t", "SK9611000000002918599669",
68
+ "\t",
69
+ "\t", "1",
70
+ "\t", "1",
71
+ "\t",
72
+ "\t", "m",
73
+ "\t", "20241011",
74
+ "\t", "0",
75
+ "\t",
76
+ "\t",
77
+ "\t",
78
+ ].join("");
79
+ export const payloadWithDirectDebit = {
80
+ invoiceId: "random-id",
81
+ payments: [
82
+ {
83
+ type: PaymentOptions.DirectDebit,
84
+ amount: 100.0,
85
+ bankAccounts: [
86
+ { iban: "SK9611000000002918599669" },
87
+ ],
88
+ currencyCode: CurrencyCode.EUR,
89
+ variableSymbol: "123",
90
+ },
91
+ ],
92
+ };
93
+ export const serializedDirectDebit = /** dprint-ignore */ [
94
+ "random-id",
95
+ "\t", "1",
96
+ "\t", "4",
97
+ "\t", "100",
98
+ "\t", "EUR",
99
+ "\t",
100
+ "\t", "123",
101
+ "\t",
102
+ "\t",
103
+ "\t",
104
+ "\t",
105
+ "\t", "1",
106
+ "\t", "SK9611000000002918599669",
107
+ "\t",
108
+ "\t", "0",
109
+ "\t", "1",
110
+ "\t",
111
+ "\t",
112
+ "\t", "123",
113
+ "\t",
114
+ "\t",
115
+ "\t",
116
+ "\t",
117
+ "\t",
118
+ "\t",
119
+ "\t",
120
+ "\t",
121
+ "\t",
122
+ "\t",
123
+ ].join("");
@@ -0,0 +1 @@
1
+ {"root":["../src/base32hex.test.ts","../src/base32hex.ts","../src/cli.ts","../src/crc32.test.ts","../src/crc32.ts","../src/deburr.test.ts","../src/deburr.ts","../src/decode.test.ts","../src/decode.ts","../src/encode.test.ts","../src/encode.ts","../src/helper.test.ts","../src/helpers.ts","../src/index.ts","../src/test_assets.ts","../src/types.ts","../src/validations.test.ts","../src/validations.ts"],"version":"5.6.2"}
package/dist/types.d.ts CHANGED
@@ -248,7 +248,7 @@ export type StandingOrder = SimplePayment & {
248
248
  /**
249
249
  * Opakovanie (periodicita) trvalého príkazu.
250
250
  */
251
- periodicity?: Periodicity;
251
+ periodicity: Periodicity;
252
252
  /**
253
253
  * Dátum poslednej platby v trvalom príkaze.
254
254
  *
@@ -17,7 +17,7 @@ export class ValidationError extends Error {
17
17
  * @param path - navigates to the specific field in DataModel, where error occurred
18
18
  */
19
19
  constructor(message, path) {
20
- super(String(message));
20
+ super(message);
21
21
  this.path = path;
22
22
  }
23
23
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "bysquare",
3
3
  "description": "It's a national standard for payment QR codes adopted by Slovak Banking Association (SBA)",
4
- "version": "2.10.0",
4
+ "version": "2.12.0",
5
5
  "license": "Apache-2.0",
6
6
  "funding": "https://github.com/sponsors/xseman",
7
7
  "homepage": "https://github.com/xseman/bysquare#readme",
@@ -24,21 +24,19 @@
24
24
  "typecheck": "tsc --noEmit",
25
25
  "version": "git checkout develop && npm test",
26
26
  "postversion": "echo 'Now run npm run build && npm publish'",
27
- "test": "TS_NODE_TRANSPILE_ONLY=true node --test --loader=ts-node/esm --no-warnings src/*.test.ts",
27
+ "test": "TS_NODE_TRANSPILE_ONLY=true node --test --experimental-test-coverage --loader=ts-node/esm --no-warnings src/*.test.ts",
28
28
  "test:watch": "TS_NODE_TRANSPILE_ONLY=true node --test --watch --loader=ts-node/esm --no-warnings src/*.test.ts"
29
29
  },
30
30
  "dependencies": {
31
- "crc-32": "~1.2.0",
32
- "lzma1": "0.0.2",
33
- "rfc4648": "~1.5.0",
31
+ "lzma1": "0.0.3",
34
32
  "validator": "^13.12.0"
35
33
  },
36
34
  "devDependencies": {
37
- "@types/node": ">=18.18.2",
35
+ "@types/node": "^22.7.0",
38
36
  "@types/validator": "^13.12.0",
39
37
  "dprint": "~0.47.0",
40
38
  "ts-node": "~10.9.0",
41
- "typescript": "~5.5.0"
39
+ "typescript": "~5.6.0"
42
40
  },
43
41
  "type": "module",
44
42
  "bin": "./dist/cli.js",
@@ -54,7 +52,7 @@
54
52
  "!dist/*.test.*"
55
53
  ],
56
54
  "engines": {
57
- "node": ">=18.18.2",
55
+ "node": ">=16",
58
56
  "npm": ">=7"
59
57
  }
60
58
  }