@f-o-t/ofx 2.2.0 → 2.3.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/README.md CHANGED
@@ -464,36 +464,95 @@ type StreamEvent =
464
464
  | { type: "complete"; transactionCount: number };
465
465
  ```
466
466
 
467
- ## Schemas
467
+ ## Validation & Schemas
468
468
 
469
- All Zod schemas are exported for custom validation:
469
+ All parsing and generation functions use Zod schemas for runtime validation, ensuring type safety and data integrity.
470
+
471
+ ### Input Validation for Generation
472
+
473
+ When generating OFX files, you can validate your inputs using the exported schemas:
470
474
 
471
475
  ```typescript
472
- import { schemas } from "@fot/ofx";
476
+ import {
477
+ generateBankStatement,
478
+ generateBankStatementOptionsSchema,
479
+ type GenerateBankStatementOptions,
480
+ } from "@fot/ofx";
481
+
482
+ // Validate options before generating
483
+ const options: GenerateBankStatementOptions = {
484
+ bankId: "123456",
485
+ accountId: "987654321",
486
+ accountType: "CHECKING",
487
+ currency: "USD",
488
+ startDate: new Date("2025-01-01"),
489
+ endDate: new Date("2025-01-31"),
490
+ transactions: [],
491
+ };
492
+
493
+ // Runtime validation
494
+ const validatedOptions = generateBankStatementOptionsSchema.parse(options);
495
+ const statement = generateBankStatement(validatedOptions);
496
+ ```
497
+
498
+ ### Available Schemas
473
499
 
474
- const customTransactionSchema = schemas.transaction.extend({
500
+ All Zod schemas are exported for custom validation and extension:
501
+
502
+ ```typescript
503
+ import {
504
+ transactionSchema,
505
+ bankAccountSchema,
506
+ ofxDocumentSchema,
507
+ } from "@fot/ofx";
508
+
509
+ // Extend schemas for custom validation
510
+ const customTransactionSchema = transactionSchema.extend({
475
511
  customField: z.string(),
476
512
  });
477
513
  ```
478
514
 
479
- Available schemas:
480
-
481
- - `schemas.transaction`
482
- - `schemas.transactionType`
483
- - `schemas.transactionList`
484
- - `schemas.bankAccount`
485
- - `schemas.creditCardAccount`
486
- - `schemas.accountType`
487
- - `schemas.balance`
488
- - `schemas.status`
489
- - `schemas.financialInstitution`
490
- - `schemas.signOnResponse`
491
- - `schemas.bankStatementResponse`
492
- - `schemas.creditCardStatementResponse`
493
- - `schemas.ofxDocument`
494
- - `schemas.ofxHeader`
495
- - `schemas.ofxResponse`
496
- - `schemas.ofxDate`
515
+ **Parsing Schemas:**
516
+ - `ofxDocumentSchema` - Complete OFX document
517
+ - `ofxHeaderSchema` - OFX file header
518
+ - `ofxResponseSchema` - OFX response body
519
+ - `transactionSchema` - Individual transaction
520
+ - `transactionTypeSchema` - Transaction type enum
521
+ - `transactionListSchema` - List of transactions
522
+ - `bankAccountSchema` - Bank account information
523
+ - `creditCardAccountSchema` - Credit card account information
524
+ - `accountTypeSchema` - Account type enum
525
+ - `balanceSchema` - Balance information
526
+ - `statusSchema` - Status response
527
+ - `financialInstitutionSchema` - Financial institution info
528
+ - `signOnResponseSchema` - Sign-on response
529
+ - `ofxDateSchema` - OFX date with timezone
530
+
531
+ **Generation Schemas:**
532
+ - `generateHeaderOptionsSchema` - OFX header generation options
533
+ - `generateTransactionInputSchema` - Transaction input for generation
534
+ - `generateBankStatementOptionsSchema` - Bank statement generation options
535
+ - `generateCreditCardStatementOptionsSchema` - Credit card statement generation options
536
+
537
+ ## Security
538
+
539
+ This library includes several security features to protect against malicious OFX files:
540
+
541
+ ### Prototype Pollution Protection
542
+
543
+ The SGML parser is protected against prototype pollution attacks. Malicious OFX files attempting to inject `__proto__`, `constructor`, or `prototype` tags are safely ignored, preventing potential remote code execution.
544
+
545
+ ### Input Validation
546
+
547
+ All parsing functions validate input data against strict Zod schemas, rejecting malformed or invalid OFX data before processing. This prevents:
548
+ - Type confusion attacks
549
+ - Invalid date/number formats
550
+ - Missing required fields
551
+ - Unexpected data structures
552
+
553
+ ### Safe Entity Decoding
554
+
555
+ HTML entities in OFX text fields are decoded using a whitelist approach, preventing XSS-style attacks through crafted entity sequences
497
556
 
498
557
  ## Performance
499
558
 
package/dist/index.d.ts CHANGED
@@ -1616,74 +1616,160 @@ declare function getTransactions(document: OFXDocument): OFXTransaction[];
1616
1616
  declare function getAccountInfo(document: OFXDocument): (OFXBankAccount | OFXCreditCardAccount)[];
1617
1617
  declare function getBalance(document: OFXDocument): BalanceInfo[];
1618
1618
  declare function getSignOnInfo(document: OFXDocument): OFXSignOnResponse;
1619
- interface GenerateHeaderOptions {
1620
- version?: string;
1621
- encoding?: string;
1622
- charset?: string;
1623
- }
1619
+ import { z as z2 } from "zod";
1620
+ declare const generateHeaderOptionsSchema: z2.ZodOptional<z2.ZodObject<{
1621
+ version: z2.ZodOptional<z2.ZodString>;
1622
+ encoding: z2.ZodOptional<z2.ZodString>;
1623
+ charset: z2.ZodOptional<z2.ZodString>;
1624
+ }, z2.core.$strip>>;
1625
+ type GenerateHeaderOptions = z2.infer<typeof generateHeaderOptionsSchema>;
1624
1626
  declare function generateHeader(options?: GenerateHeaderOptions): string;
1625
- interface GenerateTransactionInput {
1626
- type: OFXTransactionType;
1627
- datePosted: Date;
1628
- amount: number;
1629
- fitId: string;
1630
- name?: string;
1631
- memo?: string;
1632
- checkNum?: string;
1633
- refNum?: string;
1634
- }
1635
- interface GenerateBankStatementOptions {
1636
- bankId: string;
1637
- accountId: string;
1638
- accountType: OFXAccountType;
1639
- currency: string;
1640
- startDate: Date;
1641
- endDate: Date;
1642
- transactions: GenerateTransactionInput[];
1643
- ledgerBalance?: {
1644
- amount: number;
1645
- asOfDate: Date;
1646
- };
1647
- availableBalance?: {
1648
- amount: number;
1649
- asOfDate: Date;
1650
- };
1651
- financialInstitution?: {
1652
- org?: string;
1653
- fid?: string;
1654
- };
1655
- language?: string;
1656
- }
1627
+ declare const generateTransactionInputSchema: z2.ZodObject<{
1628
+ type: z2.ZodEnum<{
1629
+ CREDIT: "CREDIT";
1630
+ DEBIT: "DEBIT";
1631
+ INT: "INT";
1632
+ DIV: "DIV";
1633
+ FEE: "FEE";
1634
+ SRVCHG: "SRVCHG";
1635
+ DEP: "DEP";
1636
+ ATM: "ATM";
1637
+ POS: "POS";
1638
+ XFER: "XFER";
1639
+ CHECK: "CHECK";
1640
+ PAYMENT: "PAYMENT";
1641
+ CASH: "CASH";
1642
+ DIRECTDEP: "DIRECTDEP";
1643
+ DIRECTDEBIT: "DIRECTDEBIT";
1644
+ REPEATPMT: "REPEATPMT";
1645
+ HOLD: "HOLD";
1646
+ OTHER: "OTHER";
1647
+ }>;
1648
+ datePosted: z2.ZodDate;
1649
+ amount: z2.ZodNumber;
1650
+ fitId: z2.ZodString;
1651
+ name: z2.ZodOptional<z2.ZodString>;
1652
+ memo: z2.ZodOptional<z2.ZodString>;
1653
+ checkNum: z2.ZodOptional<z2.ZodString>;
1654
+ refNum: z2.ZodOptional<z2.ZodString>;
1655
+ }, z2.core.$strip>;
1656
+ type GenerateTransactionInput = z2.infer<typeof generateTransactionInputSchema>;
1657
+ declare const generateBankStatementOptionsSchema: z2.ZodObject<{
1658
+ bankId: z2.ZodString;
1659
+ accountId: z2.ZodString;
1660
+ accountType: z2.ZodEnum<{
1661
+ CHECKING: "CHECKING";
1662
+ SAVINGS: "SAVINGS";
1663
+ MONEYMRKT: "MONEYMRKT";
1664
+ CREDITLINE: "CREDITLINE";
1665
+ CD: "CD";
1666
+ }>;
1667
+ currency: z2.ZodString;
1668
+ startDate: z2.ZodDate;
1669
+ endDate: z2.ZodDate;
1670
+ transactions: z2.ZodArray<z2.ZodObject<{
1671
+ type: z2.ZodEnum<{
1672
+ CREDIT: "CREDIT";
1673
+ DEBIT: "DEBIT";
1674
+ INT: "INT";
1675
+ DIV: "DIV";
1676
+ FEE: "FEE";
1677
+ SRVCHG: "SRVCHG";
1678
+ DEP: "DEP";
1679
+ ATM: "ATM";
1680
+ POS: "POS";
1681
+ XFER: "XFER";
1682
+ CHECK: "CHECK";
1683
+ PAYMENT: "PAYMENT";
1684
+ CASH: "CASH";
1685
+ DIRECTDEP: "DIRECTDEP";
1686
+ DIRECTDEBIT: "DIRECTDEBIT";
1687
+ REPEATPMT: "REPEATPMT";
1688
+ HOLD: "HOLD";
1689
+ OTHER: "OTHER";
1690
+ }>;
1691
+ datePosted: z2.ZodDate;
1692
+ amount: z2.ZodNumber;
1693
+ fitId: z2.ZodString;
1694
+ name: z2.ZodOptional<z2.ZodString>;
1695
+ memo: z2.ZodOptional<z2.ZodString>;
1696
+ checkNum: z2.ZodOptional<z2.ZodString>;
1697
+ refNum: z2.ZodOptional<z2.ZodString>;
1698
+ }, z2.core.$strip>>;
1699
+ ledgerBalance: z2.ZodOptional<z2.ZodObject<{
1700
+ amount: z2.ZodNumber;
1701
+ asOfDate: z2.ZodDate;
1702
+ }, z2.core.$strip>>;
1703
+ availableBalance: z2.ZodOptional<z2.ZodObject<{
1704
+ amount: z2.ZodNumber;
1705
+ asOfDate: z2.ZodDate;
1706
+ }, z2.core.$strip>>;
1707
+ financialInstitution: z2.ZodOptional<z2.ZodObject<{
1708
+ org: z2.ZodOptional<z2.ZodString>;
1709
+ fid: z2.ZodOptional<z2.ZodString>;
1710
+ }, z2.core.$strip>>;
1711
+ language: z2.ZodOptional<z2.ZodString>;
1712
+ }, z2.core.$strip>;
1713
+ type GenerateBankStatementOptions = z2.infer<typeof generateBankStatementOptionsSchema>;
1657
1714
  declare function generateBankStatement(options: GenerateBankStatementOptions): string;
1658
- interface GenerateCreditCardStatementOptions {
1659
- accountId: string;
1660
- currency: string;
1661
- startDate: Date;
1662
- endDate: Date;
1663
- transactions: GenerateTransactionInput[];
1664
- ledgerBalance?: {
1665
- amount: number;
1666
- asOfDate: Date;
1667
- };
1668
- availableBalance?: {
1669
- amount: number;
1670
- asOfDate: Date;
1671
- };
1672
- financialInstitution?: {
1673
- org?: string;
1674
- fid?: string;
1675
- };
1676
- language?: string;
1677
- }
1715
+ declare const generateCreditCardStatementOptionsSchema: z2.ZodObject<{
1716
+ accountId: z2.ZodString;
1717
+ currency: z2.ZodString;
1718
+ startDate: z2.ZodDate;
1719
+ endDate: z2.ZodDate;
1720
+ transactions: z2.ZodArray<z2.ZodObject<{
1721
+ type: z2.ZodEnum<{
1722
+ CREDIT: "CREDIT";
1723
+ DEBIT: "DEBIT";
1724
+ INT: "INT";
1725
+ DIV: "DIV";
1726
+ FEE: "FEE";
1727
+ SRVCHG: "SRVCHG";
1728
+ DEP: "DEP";
1729
+ ATM: "ATM";
1730
+ POS: "POS";
1731
+ XFER: "XFER";
1732
+ CHECK: "CHECK";
1733
+ PAYMENT: "PAYMENT";
1734
+ CASH: "CASH";
1735
+ DIRECTDEP: "DIRECTDEP";
1736
+ DIRECTDEBIT: "DIRECTDEBIT";
1737
+ REPEATPMT: "REPEATPMT";
1738
+ HOLD: "HOLD";
1739
+ OTHER: "OTHER";
1740
+ }>;
1741
+ datePosted: z2.ZodDate;
1742
+ amount: z2.ZodNumber;
1743
+ fitId: z2.ZodString;
1744
+ name: z2.ZodOptional<z2.ZodString>;
1745
+ memo: z2.ZodOptional<z2.ZodString>;
1746
+ checkNum: z2.ZodOptional<z2.ZodString>;
1747
+ refNum: z2.ZodOptional<z2.ZodString>;
1748
+ }, z2.core.$strip>>;
1749
+ ledgerBalance: z2.ZodOptional<z2.ZodObject<{
1750
+ amount: z2.ZodNumber;
1751
+ asOfDate: z2.ZodDate;
1752
+ }, z2.core.$strip>>;
1753
+ availableBalance: z2.ZodOptional<z2.ZodObject<{
1754
+ amount: z2.ZodNumber;
1755
+ asOfDate: z2.ZodDate;
1756
+ }, z2.core.$strip>>;
1757
+ financialInstitution: z2.ZodOptional<z2.ZodObject<{
1758
+ org: z2.ZodOptional<z2.ZodString>;
1759
+ fid: z2.ZodOptional<z2.ZodString>;
1760
+ }, z2.core.$strip>>;
1761
+ language: z2.ZodOptional<z2.ZodString>;
1762
+ }, z2.core.$strip>;
1763
+ type GenerateCreditCardStatementOptions = z2.infer<typeof generateCreditCardStatementOptionsSchema>;
1678
1764
  declare function generateCreditCardStatement(options: GenerateCreditCardStatementOptions): string;
1679
- import { z as z2 } from "zod";
1765
+ import { z as z3 } from "zod";
1680
1766
  declare function getEncodingFromCharset(charset?: string): string;
1681
1767
  type ParseResult<T> = {
1682
1768
  success: true;
1683
1769
  data: T;
1684
1770
  } | {
1685
1771
  success: false;
1686
- error: z2.ZodError;
1772
+ error: z3.ZodError;
1687
1773
  };
1688
1774
  declare function parse(content: string): ParseResult<OFXDocument>;
1689
1775
  declare function parseOrThrow(content: string): OFXDocument;
@@ -1797,4 +1883,4 @@ declare function formatOfxDate(date: Date, timezone?: {
1797
1883
  offset: number;
1798
1884
  name: string;
1799
1885
  }): string;
1800
- export { parseStreamToArray, parseStream, parseOrThrow, parseBufferOrThrow, parseBuffer, parseBatchStreamToArray, parseBatchStream, parse, getTransactions, getSignOnInfo, getEncodingFromCharset, getBalance, getAccountInfo, generateHeader, generateCreditCardStatement, generateBankStatement, formatOfxDate, StreamOptions, StreamEvent, ParseResult, OFXTransactionType, OFXTransactionList, OFXTransaction, OFXStatus, OFXSignOnResponse, OFXSignOnMessageSetResponse, OFXResponse, OFXHeader, OFXFinancialInstitution, OFXDocument, OFXDate, OFXCreditCardStatementTransactionResponse, OFXCreditCardStatementResponse, OFXCreditCardMessageSetResponse, OFXCreditCardAccount, OFXBankStatementTransactionResponse, OFXBankStatementResponse, OFXBankMessageSetResponse, OFXBankAccount, OFXBalance, OFXAccountType, GenerateTransactionInput, GenerateHeaderOptions, GenerateCreditCardStatementOptions, GenerateBankStatementOptions, BatchStreamEvent, BatchParsedFile, BatchFileInput, BalanceInfo };
1886
+ export { parseStreamToArray, parseStream, parseOrThrow, parseBufferOrThrow, parseBuffer, parseBatchStreamToArray, parseBatchStream, parse, getTransactions, getSignOnInfo, getEncodingFromCharset, getBalance, getAccountInfo, generateTransactionInputSchema, generateHeaderOptionsSchema, generateHeader, generateCreditCardStatementOptionsSchema, generateCreditCardStatement, generateBankStatementOptionsSchema, generateBankStatement, formatOfxDate, StreamOptions, StreamEvent, ParseResult, OFXTransactionType, OFXTransactionList, OFXTransaction, OFXStatus, OFXSignOnResponse, OFXSignOnMessageSetResponse, OFXResponse, OFXHeader, OFXFinancialInstitution, OFXDocument, OFXDate, OFXCreditCardStatementTransactionResponse, OFXCreditCardStatementResponse, OFXCreditCardMessageSetResponse, OFXCreditCardAccount, OFXBankStatementTransactionResponse, OFXBankStatementResponse, OFXBankMessageSetResponse, OFXBankAccount, OFXBalance, OFXAccountType, GenerateTransactionInput, GenerateHeaderOptions, GenerateCreditCardStatementOptions, GenerateBankStatementOptions, BatchStreamEvent, BatchParsedFile, BatchFileInput, BalanceInfo };
package/dist/index.js CHANGED
@@ -1,5 +1,18 @@
1
1
  // src/utils.ts
2
2
  var toArray = (value) => Array.isArray(value) ? value : [value];
3
+ var ENTITY_MAP = {
4
+ "&amp;": "&",
5
+ "&apos;": "'",
6
+ "&gt;": ">",
7
+ "&lt;": "<",
8
+ "&quot;": '"'
9
+ };
10
+ var ENTITY_REGEX = /&(?:amp|lt|gt|quot|apos);/g;
11
+ function decodeEntities(text) {
12
+ if (!text.includes("&"))
13
+ return text;
14
+ return text.replace(ENTITY_REGEX, (match) => ENTITY_MAP[match] ?? match);
15
+ }
3
16
  var pad = (n, width = 2) => n.toString().padStart(width, "0");
4
17
  function escapeOfxText(text) {
5
18
  if (!text.includes("&") && !text.includes("<") && !text.includes(">")) {
@@ -95,182 +108,19 @@ function getSignOnInfo(document) {
95
108
  return document.OFX.SIGNONMSGSRSV1.SONRS;
96
109
  }
97
110
  // src/generator.ts
98
- function generateHeader(options) {
99
- const version = options?.version ?? "100";
100
- const encoding = options?.encoding ?? "USASCII";
101
- const charset = options?.charset ?? "1252";
102
- return [
103
- "OFXHEADER:100",
104
- "DATA:OFXSGML",
105
- `VERSION:${version}`,
106
- "SECURITY:NONE",
107
- `ENCODING:${encoding}`,
108
- `CHARSET:${charset}`,
109
- "COMPRESSION:NONE",
110
- "OLDFILEUID:NONE",
111
- "NEWFILEUID:NONE",
112
- ""
113
- ].join(`
114
- `);
115
- }
116
- function generateTransaction(trn) {
117
- const lines = [
118
- "<STMTTRN>",
119
- `<TRNTYPE>${trn.type}`,
120
- `<DTPOSTED>${formatOfxDate(trn.datePosted)}`,
121
- `<TRNAMT>${formatAmount(trn.amount)}`,
122
- `<FITID>${escapeOfxText(trn.fitId)}`
123
- ];
124
- if (trn.name) {
125
- lines.push(`<NAME>${escapeOfxText(trn.name)}`);
126
- }
127
- if (trn.memo) {
128
- lines.push(`<MEMO>${escapeOfxText(trn.memo)}`);
129
- }
130
- if (trn.checkNum) {
131
- lines.push(`<CHECKNUM>${escapeOfxText(trn.checkNum)}`);
132
- }
133
- if (trn.refNum) {
134
- lines.push(`<REFNUM>${escapeOfxText(trn.refNum)}`);
135
- }
136
- lines.push("</STMTTRN>");
137
- return lines.join(`
138
- `);
139
- }
140
- function generateBankStatement(options) {
141
- const parts = [generateHeader()];
142
- const serverDate = formatOfxDate(new Date);
143
- const language = options.language ?? "POR";
144
- parts.push(`<OFX>
145
- <SIGNONMSGSRSV1>
146
- <SONRS>
147
- <STATUS>
148
- <CODE>0
149
- <SEVERITY>INFO
150
- </STATUS>
151
- <DTSERVER>${serverDate}
152
- <LANGUAGE>${language}`);
153
- if (options.financialInstitution) {
154
- parts.push("<FI>");
155
- if (options.financialInstitution.org) {
156
- parts.push(`<ORG>${escapeOfxText(options.financialInstitution.org)}`);
157
- }
158
- if (options.financialInstitution.fid) {
159
- parts.push(`<FID>${escapeOfxText(options.financialInstitution.fid)}`);
160
- }
161
- parts.push("</FI>");
162
- }
163
- parts.push(`</SONRS>
164
- </SIGNONMSGSRSV1>
165
- <BANKMSGSRSV1>
166
- <STMTTRNRS>
167
- <TRNUID>0
168
- <STATUS>
169
- <CODE>0
170
- <SEVERITY>INFO
171
- </STATUS>
172
- <STMTRS>
173
- <CURDEF>${options.currency}
174
- <BANKACCTFROM>
175
- <BANKID>${escapeOfxText(options.bankId)}
176
- <ACCTID>${escapeOfxText(options.accountId)}
177
- <ACCTTYPE>${options.accountType}
178
- </BANKACCTFROM>
179
- <BANKTRANLIST>
180
- <DTSTART>${formatOfxDate(options.startDate)}
181
- <DTEND>${formatOfxDate(options.endDate)}`);
182
- for (const trn of options.transactions) {
183
- parts.push(generateTransaction(trn));
184
- }
185
- parts.push("</BANKTRANLIST>");
186
- if (options.ledgerBalance) {
187
- parts.push(`<LEDGERBAL>
188
- <BALAMT>${formatAmount(options.ledgerBalance.amount)}
189
- <DTASOF>${formatOfxDate(options.ledgerBalance.asOfDate)}
190
- </LEDGERBAL>`);
191
- }
192
- if (options.availableBalance) {
193
- parts.push(`<AVAILBAL>
194
- <BALAMT>${formatAmount(options.availableBalance.amount)}
195
- <DTASOF>${formatOfxDate(options.availableBalance.asOfDate)}
196
- </AVAILBAL>`);
197
- }
198
- parts.push(`</STMTRS>
199
- </STMTTRNRS>
200
- </BANKMSGSRSV1>
201
- </OFX>`);
202
- return parts.join(`
203
- `);
204
- }
205
- function generateCreditCardStatement(options) {
206
- const parts = [generateHeader()];
207
- const serverDate = formatOfxDate(new Date);
208
- const language = options.language ?? "POR";
209
- parts.push(`<OFX>
210
- <SIGNONMSGSRSV1>
211
- <SONRS>
212
- <STATUS>
213
- <CODE>0
214
- <SEVERITY>INFO
215
- </STATUS>
216
- <DTSERVER>${serverDate}
217
- <LANGUAGE>${language}`);
218
- if (options.financialInstitution) {
219
- parts.push("<FI>");
220
- if (options.financialInstitution.org) {
221
- parts.push(`<ORG>${escapeOfxText(options.financialInstitution.org)}`);
222
- }
223
- if (options.financialInstitution.fid) {
224
- parts.push(`<FID>${escapeOfxText(options.financialInstitution.fid)}`);
225
- }
226
- parts.push("</FI>");
227
- }
228
- parts.push(`</SONRS>
229
- </SIGNONMSGSRSV1>
230
- <CREDITCARDMSGSRSV1>
231
- <CCSTMTTRNRS>
232
- <TRNUID>0
233
- <STATUS>
234
- <CODE>0
235
- <SEVERITY>INFO
236
- </STATUS>
237
- <CCSTMTRS>
238
- <CURDEF>${options.currency}
239
- <CCACCTFROM>
240
- <ACCTID>${escapeOfxText(options.accountId)}
241
- </CCACCTFROM>
242
- <BANKTRANLIST>
243
- <DTSTART>${formatOfxDate(options.startDate)}
244
- <DTEND>${formatOfxDate(options.endDate)}`);
245
- for (const trn of options.transactions) {
246
- parts.push(generateTransaction(trn));
247
- }
248
- parts.push("</BANKTRANLIST>");
249
- if (options.ledgerBalance) {
250
- parts.push(`<LEDGERBAL>
251
- <BALAMT>${formatAmount(options.ledgerBalance.amount)}
252
- <DTASOF>${formatOfxDate(options.ledgerBalance.asOfDate)}
253
- </LEDGERBAL>`);
254
- }
255
- if (options.availableBalance) {
256
- parts.push(`<AVAILBAL>
257
- <BALAMT>${formatAmount(options.availableBalance.amount)}
258
- <DTASOF>${formatOfxDate(options.availableBalance.asOfDate)}
259
- </AVAILBAL>`);
260
- }
261
- parts.push(`</CCSTMTRS>
262
- </CCSTMTTRNRS>
263
- </CREDITCARDMSGSRSV1>
264
- </OFX>`);
265
- return parts.join(`
266
- `);
267
- }
268
- // src/parser.ts
269
111
  import { z as z2 } from "zod";
270
112
 
271
113
  // src/schemas.ts
272
114
  import { z } from "zod";
273
115
  var toFloat = (val) => Number.parseFloat(val);
116
+ var dateComponentsSchema = z.object({
117
+ year: z.number(),
118
+ month: z.number(),
119
+ day: z.number(),
120
+ hour: z.number(),
121
+ minute: z.number(),
122
+ second: z.number()
123
+ });
274
124
  var DATE_REGEX = /^(\d{4})(\d{2})(\d{2})(\d{2})?(\d{2})?(\d{2})?/;
275
125
  var TIMEZONE_REGEX = /\[([+-]?\d+):(\w+)\]/;
276
126
  function parseDateComponents(val) {
@@ -463,15 +313,234 @@ var ofxDocumentSchema = z.object({
463
313
  OFX: ofxResponseSchema
464
314
  });
465
315
 
316
+ // src/generator.ts
317
+ var generateHeaderOptionsSchema = z2.object({
318
+ version: z2.string().optional(),
319
+ encoding: z2.string().optional(),
320
+ charset: z2.string().optional()
321
+ }).optional();
322
+ function generateHeader(options) {
323
+ const version = options?.version ?? "100";
324
+ const encoding = options?.encoding ?? "USASCII";
325
+ const charset = options?.charset ?? "1252";
326
+ return [
327
+ "OFXHEADER:100",
328
+ "DATA:OFXSGML",
329
+ `VERSION:${version}`,
330
+ "SECURITY:NONE",
331
+ `ENCODING:${encoding}`,
332
+ `CHARSET:${charset}`,
333
+ "COMPRESSION:NONE",
334
+ "OLDFILEUID:NONE",
335
+ "NEWFILEUID:NONE",
336
+ ""
337
+ ].join(`
338
+ `);
339
+ }
340
+ var generateTransactionInputSchema = z2.object({
341
+ type: transactionTypeSchema,
342
+ datePosted: z2.date(),
343
+ amount: z2.number(),
344
+ fitId: z2.string().min(1),
345
+ name: z2.string().optional(),
346
+ memo: z2.string().optional(),
347
+ checkNum: z2.string().optional(),
348
+ refNum: z2.string().optional()
349
+ });
350
+ function generateTransaction(trn) {
351
+ const lines = [
352
+ "<STMTTRN>",
353
+ `<TRNTYPE>${trn.type}`,
354
+ `<DTPOSTED>${formatOfxDate(trn.datePosted)}`,
355
+ `<TRNAMT>${formatAmount(trn.amount)}`,
356
+ `<FITID>${escapeOfxText(trn.fitId)}`
357
+ ];
358
+ if (trn.name) {
359
+ lines.push(`<NAME>${escapeOfxText(trn.name)}`);
360
+ }
361
+ if (trn.memo) {
362
+ lines.push(`<MEMO>${escapeOfxText(trn.memo)}`);
363
+ }
364
+ if (trn.checkNum) {
365
+ lines.push(`<CHECKNUM>${escapeOfxText(trn.checkNum)}`);
366
+ }
367
+ if (trn.refNum) {
368
+ lines.push(`<REFNUM>${escapeOfxText(trn.refNum)}`);
369
+ }
370
+ lines.push("</STMTTRN>");
371
+ return lines.join(`
372
+ `);
373
+ }
374
+ var balanceSchema2 = z2.object({
375
+ amount: z2.number(),
376
+ asOfDate: z2.date()
377
+ });
378
+ var financialInstitutionSchema2 = z2.object({
379
+ org: z2.string().optional(),
380
+ fid: z2.string().optional()
381
+ });
382
+ var generateBankStatementOptionsSchema = z2.object({
383
+ bankId: z2.string().min(1),
384
+ accountId: z2.string().min(1),
385
+ accountType: accountTypeSchema,
386
+ currency: z2.string().min(1),
387
+ startDate: z2.date(),
388
+ endDate: z2.date(),
389
+ transactions: z2.array(generateTransactionInputSchema),
390
+ ledgerBalance: balanceSchema2.optional(),
391
+ availableBalance: balanceSchema2.optional(),
392
+ financialInstitution: financialInstitutionSchema2.optional(),
393
+ language: z2.string().optional()
394
+ });
395
+ function generateBankStatement(options) {
396
+ const parts = [generateHeader()];
397
+ const serverDate = formatOfxDate(new Date);
398
+ const language = options.language ?? "POR";
399
+ parts.push(`<OFX>
400
+ <SIGNONMSGSRSV1>
401
+ <SONRS>
402
+ <STATUS>
403
+ <CODE>0
404
+ <SEVERITY>INFO
405
+ </STATUS>
406
+ <DTSERVER>${serverDate}
407
+ <LANGUAGE>${language}`);
408
+ if (options.financialInstitution) {
409
+ parts.push("<FI>");
410
+ if (options.financialInstitution.org) {
411
+ parts.push(`<ORG>${escapeOfxText(options.financialInstitution.org)}`);
412
+ }
413
+ if (options.financialInstitution.fid) {
414
+ parts.push(`<FID>${escapeOfxText(options.financialInstitution.fid)}`);
415
+ }
416
+ parts.push("</FI>");
417
+ }
418
+ parts.push(`</SONRS>
419
+ </SIGNONMSGSRSV1>
420
+ <BANKMSGSRSV1>
421
+ <STMTTRNRS>
422
+ <TRNUID>0
423
+ <STATUS>
424
+ <CODE>0
425
+ <SEVERITY>INFO
426
+ </STATUS>
427
+ <STMTRS>
428
+ <CURDEF>${options.currency}
429
+ <BANKACCTFROM>
430
+ <BANKID>${escapeOfxText(options.bankId)}
431
+ <ACCTID>${escapeOfxText(options.accountId)}
432
+ <ACCTTYPE>${options.accountType}
433
+ </BANKACCTFROM>
434
+ <BANKTRANLIST>
435
+ <DTSTART>${formatOfxDate(options.startDate)}
436
+ <DTEND>${formatOfxDate(options.endDate)}`);
437
+ for (const trn of options.transactions) {
438
+ parts.push(generateTransaction(trn));
439
+ }
440
+ parts.push("</BANKTRANLIST>");
441
+ if (options.ledgerBalance) {
442
+ parts.push(`<LEDGERBAL>
443
+ <BALAMT>${formatAmount(options.ledgerBalance.amount)}
444
+ <DTASOF>${formatOfxDate(options.ledgerBalance.asOfDate)}
445
+ </LEDGERBAL>`);
446
+ }
447
+ if (options.availableBalance) {
448
+ parts.push(`<AVAILBAL>
449
+ <BALAMT>${formatAmount(options.availableBalance.amount)}
450
+ <DTASOF>${formatOfxDate(options.availableBalance.asOfDate)}
451
+ </AVAILBAL>`);
452
+ }
453
+ parts.push(`</STMTRS>
454
+ </STMTTRNRS>
455
+ </BANKMSGSRSV1>
456
+ </OFX>`);
457
+ return parts.join(`
458
+ `);
459
+ }
460
+ var generateCreditCardStatementOptionsSchema = z2.object({
461
+ accountId: z2.string().min(1),
462
+ currency: z2.string().min(1),
463
+ startDate: z2.date(),
464
+ endDate: z2.date(),
465
+ transactions: z2.array(generateTransactionInputSchema),
466
+ ledgerBalance: balanceSchema2.optional(),
467
+ availableBalance: balanceSchema2.optional(),
468
+ financialInstitution: financialInstitutionSchema2.optional(),
469
+ language: z2.string().optional()
470
+ });
471
+ function generateCreditCardStatement(options) {
472
+ const parts = [generateHeader()];
473
+ const serverDate = formatOfxDate(new Date);
474
+ const language = options.language ?? "POR";
475
+ parts.push(`<OFX>
476
+ <SIGNONMSGSRSV1>
477
+ <SONRS>
478
+ <STATUS>
479
+ <CODE>0
480
+ <SEVERITY>INFO
481
+ </STATUS>
482
+ <DTSERVER>${serverDate}
483
+ <LANGUAGE>${language}`);
484
+ if (options.financialInstitution) {
485
+ parts.push("<FI>");
486
+ if (options.financialInstitution.org) {
487
+ parts.push(`<ORG>${escapeOfxText(options.financialInstitution.org)}`);
488
+ }
489
+ if (options.financialInstitution.fid) {
490
+ parts.push(`<FID>${escapeOfxText(options.financialInstitution.fid)}`);
491
+ }
492
+ parts.push("</FI>");
493
+ }
494
+ parts.push(`</SONRS>
495
+ </SIGNONMSGSRSV1>
496
+ <CREDITCARDMSGSRSV1>
497
+ <CCSTMTTRNRS>
498
+ <TRNUID>0
499
+ <STATUS>
500
+ <CODE>0
501
+ <SEVERITY>INFO
502
+ </STATUS>
503
+ <CCSTMTRS>
504
+ <CURDEF>${options.currency}
505
+ <CCACCTFROM>
506
+ <ACCTID>${escapeOfxText(options.accountId)}
507
+ </CCACCTFROM>
508
+ <BANKTRANLIST>
509
+ <DTSTART>${formatOfxDate(options.startDate)}
510
+ <DTEND>${formatOfxDate(options.endDate)}`);
511
+ for (const trn of options.transactions) {
512
+ parts.push(generateTransaction(trn));
513
+ }
514
+ parts.push("</BANKTRANLIST>");
515
+ if (options.ledgerBalance) {
516
+ parts.push(`<LEDGERBAL>
517
+ <BALAMT>${formatAmount(options.ledgerBalance.amount)}
518
+ <DTASOF>${formatOfxDate(options.ledgerBalance.asOfDate)}
519
+ </LEDGERBAL>`);
520
+ }
521
+ if (options.availableBalance) {
522
+ parts.push(`<AVAILBAL>
523
+ <BALAMT>${formatAmount(options.availableBalance.amount)}
524
+ <DTASOF>${formatOfxDate(options.availableBalance.asOfDate)}
525
+ </AVAILBAL>`);
526
+ }
527
+ parts.push(`</CCSTMTRS>
528
+ </CCSTMTTRNRS>
529
+ </CREDITCARDMSGSRSV1>
530
+ </OFX>`);
531
+ return parts.join(`
532
+ `);
533
+ }
466
534
  // src/parser.ts
535
+ import { z as z3 } from "zod";
467
536
  var CHARSET_MAP = {
468
537
  "1252": "windows-1252",
469
538
  "WINDOWS-1252": "windows-1252",
470
539
  CP1252: "windows-1252",
471
- "8859-1": "iso-8859-1",
472
- "ISO-8859-1": "iso-8859-1",
473
- LATIN1: "iso-8859-1",
474
- "LATIN-1": "iso-8859-1",
540
+ "8859-1": "windows-1252",
541
+ "ISO-8859-1": "windows-1252",
542
+ LATIN1: "windows-1252",
543
+ "LATIN-1": "windows-1252",
475
544
  "UTF-8": "utf-8",
476
545
  UTF8: "utf-8",
477
546
  NONE: "utf-8",
@@ -483,18 +552,10 @@ function getEncodingFromCharset(charset) {
483
552
  const normalized = charset.toUpperCase().trim();
484
553
  return CHARSET_MAP[normalized] ?? "windows-1252";
485
554
  }
486
- var ENTITY_MAP = {
487
- "&amp;": "&",
488
- "&apos;": "'",
489
- "&gt;": ">",
490
- "&lt;": "<",
491
- "&quot;": '"'
492
- };
493
- var ENTITY_REGEX = /&(?:amp|lt|gt|quot|apos);/g;
494
- function decodeEntities(text) {
495
- return text.replace(ENTITY_REGEX, (match) => ENTITY_MAP[match] ?? match);
496
- }
497
555
  function addToContent(content, key, value) {
556
+ if (key === "__proto__" || key === "constructor" || key === "prototype") {
557
+ return;
558
+ }
498
559
  const existing = content[key];
499
560
  if (existing !== undefined) {
500
561
  if (Array.isArray(existing)) {
@@ -558,7 +619,7 @@ function generateFitId(txn, index) {
558
619
  let hash = 0;
559
620
  for (let i = 0;i < input.length; i++) {
560
621
  hash = (hash << 5) - hash + input.charCodeAt(i);
561
- hash = hash & hash;
622
+ hash = hash | 0;
562
623
  }
563
624
  return `AUTO${Math.abs(hash).toString(16).toUpperCase().padStart(8, "0")}`;
564
625
  }
@@ -642,7 +703,7 @@ function parse(content) {
642
703
  try {
643
704
  if (typeof content !== "string") {
644
705
  return {
645
- error: new z2.ZodError([
706
+ error: new z3.ZodError([
646
707
  {
647
708
  code: "invalid_type",
648
709
  expected: "string",
@@ -655,7 +716,7 @@ function parse(content) {
655
716
  }
656
717
  if (content.trim() === "") {
657
718
  return {
658
- error: new z2.ZodError([
719
+ error: new z3.ZodError([
659
720
  {
660
721
  code: "custom",
661
722
  message: "Content cannot be empty",
@@ -677,7 +738,7 @@ function parse(content) {
677
738
  success: true
678
739
  };
679
740
  } catch (err) {
680
- if (err instanceof z2.ZodError) {
741
+ if (err instanceof z3.ZodError) {
681
742
  return { error: err, success: false };
682
743
  }
683
744
  throw err;
@@ -733,7 +794,7 @@ function hasUtf8MultiByte(buffer) {
733
794
  }
734
795
  function parseHeaderFromBuffer(buffer) {
735
796
  const maxHeaderSize = Math.min(buffer.length, 1000);
736
- const headerSection = new TextDecoder("iso-8859-1").decode(buffer.slice(0, maxHeaderSize));
797
+ const headerSection = new TextDecoder("windows-1252").decode(buffer.slice(0, maxHeaderSize));
737
798
  const header = {};
738
799
  const singleLineMatch = headerSection.match(/^(OFXHEADER:\d+.*?)(?=<OFX|<\?xml)/is);
739
800
  if (singleLineMatch?.[1]) {
@@ -775,7 +836,7 @@ function parseBuffer(buffer) {
775
836
  try {
776
837
  if (!(buffer instanceof Uint8Array)) {
777
838
  return {
778
- error: new z2.ZodError([
839
+ error: new z3.ZodError([
779
840
  {
780
841
  code: "invalid_type",
781
842
  expected: "object",
@@ -788,7 +849,7 @@ function parseBuffer(buffer) {
788
849
  }
789
850
  if (buffer.length === 0) {
790
851
  return {
791
- error: new z2.ZodError([
852
+ error: new z3.ZodError([
792
853
  {
793
854
  code: "custom",
794
855
  message: "Buffer cannot be empty",
@@ -803,7 +864,7 @@ function parseBuffer(buffer) {
803
864
  const content = decoder.decode(buffer);
804
865
  return parse(content);
805
866
  } catch (err) {
806
- if (err instanceof z2.ZodError) {
867
+ if (err instanceof z3.ZodError) {
807
868
  return { error: err, success: false };
808
869
  }
809
870
  throw err;
@@ -817,19 +878,6 @@ function parseBufferOrThrow(buffer) {
817
878
  return result.data;
818
879
  }
819
880
  // src/stream.ts
820
- var ENTITY_MAP2 = {
821
- "&amp;": "&",
822
- "&apos;": "'",
823
- "&gt;": ">",
824
- "&lt;": "<",
825
- "&quot;": '"'
826
- };
827
- var ENTITY_REGEX2 = /&(?:amp|lt|gt|quot|apos);/g;
828
- function decodeEntities2(text) {
829
- if (!text.includes("&"))
830
- return text;
831
- return text.replace(ENTITY_REGEX2, (match) => ENTITY_MAP2[match] ?? match);
832
- }
833
881
  function parseHeaderFromBuffer2(buffer) {
834
882
  const lines = buffer.split(/\r?\n/);
835
883
  const header = {};
@@ -966,7 +1014,7 @@ async function* parseStream(input, options) {
966
1014
  state.objectStack.length = Math.max(pathIndex + 1, 1);
967
1015
  }
968
1016
  } else if (textContent) {
969
- const decoded = decodeEntities2(textContent);
1017
+ const decoded = decodeEntities(textContent);
970
1018
  const existing = currentObj[tagName];
971
1019
  if (existing !== undefined) {
972
1020
  if (Array.isArray(existing)) {
@@ -1015,7 +1063,7 @@ async function* parseStream(input, options) {
1015
1063
  combined.set(chunk, offset);
1016
1064
  offset += chunk.length;
1017
1065
  }
1018
- const headerSection = new TextDecoder("iso-8859-1").decode(combined.slice(0, Math.min(combined.length, 1000)));
1066
+ const headerSection = new TextDecoder("windows-1252").decode(combined.slice(0, Math.min(combined.length, 1000)));
1019
1067
  if (headerSection.includes("<OFX") || headerSection.includes("<?xml")) {
1020
1068
  const charsetMatch = headerSection.match(/CHARSET:(\S+)/i);
1021
1069
  if (charsetMatch && !detectedEncoding) {
@@ -1088,7 +1136,7 @@ async function* parseBatchStream(files, options) {
1088
1136
  yield { type: "file_start", fileIndex: i, filename: file.filename };
1089
1137
  try {
1090
1138
  let fileTransactionCount = 0;
1091
- const headerSection = new TextDecoder("iso-8859-1").decode(file.buffer.slice(0, Math.min(file.buffer.length, 1000)));
1139
+ const headerSection = new TextDecoder("windows-1252").decode(file.buffer.slice(0, Math.min(file.buffer.length, 1000)));
1092
1140
  const charsetMatch = headerSection.match(/CHARSET:(\S+)/i);
1093
1141
  const encoding = charsetMatch ? getEncodingFromCharset(charsetMatch[1]) : options?.encoding ?? "utf-8";
1094
1142
  const decoder = new TextDecoder(encoding);
@@ -1196,8 +1244,12 @@ export {
1196
1244
  getEncodingFromCharset,
1197
1245
  getBalance,
1198
1246
  getAccountInfo,
1247
+ generateTransactionInputSchema,
1248
+ generateHeaderOptionsSchema,
1199
1249
  generateHeader,
1250
+ generateCreditCardStatementOptionsSchema,
1200
1251
  generateCreditCardStatement,
1252
+ generateBankStatementOptionsSchema,
1201
1253
  generateBankStatement,
1202
1254
  formatOfxDate
1203
1255
  };
package/package.json CHANGED
@@ -3,14 +3,14 @@
3
3
  "url": "https://github.com/F-O-T/montte-nx/issues"
4
4
  },
5
5
  "dependencies": {
6
- "zod": "4.1.13"
6
+ "zod": "4.2.1"
7
7
  },
8
8
  "description": "Typesafe ofx handling",
9
9
  "devDependencies": {
10
- "@biomejs/biome": "2.3.8",
11
- "@types/bun": "1.3.3",
10
+ "@biomejs/biome": "2.3.10",
11
+ "@types/bun": "1.3.5",
12
12
  "bumpp": "10.3.2",
13
- "bunup": "0.16.10",
13
+ "bunup": "0.16.11",
14
14
  "typescript": "5.9.3"
15
15
  },
16
16
  "exports": {
@@ -59,5 +59,5 @@
59
59
  },
60
60
  "type": "module",
61
61
  "types": "./dist/index.d.ts",
62
- "version": "2.2.0"
62
+ "version": "2.3.1"
63
63
  }