@festapp/banksync 0.1.4 → 0.1.5

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.
Files changed (2) hide show
  1. package/README.md +173 -103
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -1,145 +1,210 @@
1
- # BankSync
1
+ <h1 align="center">BankSync</h1>
2
2
 
3
- BankSync turns bank transaction notifications into durable, signed webhooks.
4
- It is a TypeScript package and a deployable Cloudflare Worker maintained by
5
- [Festapp](https://github.com/festappnet).
3
+ <p align="center"><strong>Bank notifications in. Verified, deduplicated webhooks out.</strong></p>
6
4
 
7
- The included Worker supports:
5
+ <p align="center">
6
+ <a href="https://www.npmjs.com/package/@festapp/banksync"><img alt="npm version" src="https://img.shields.io/npm/v/@festapp/banksync?label=npm"></a>
7
+ <a href="https://github.com/festappnet/banksync/actions/workflows/check.yml"><img alt="Check" src="https://github.com/festappnet/banksync/actions/workflows/check.yml/badge.svg"></a>
8
+ <a href="https://github.com/festappnet/banksync/actions/workflows/security.yml"><img alt="Security" src="https://github.com/festappnet/banksync/actions/workflows/security.yml/badge.svg"></a>
9
+ <a href="LICENSE"><img alt="MIT license" src="https://img.shields.io/badge/license-MIT-blue.svg"></a>
10
+ </p>
8
11
 
9
- - Fio Bank notification email and Fio API ingestion;
10
- - Air Bank notification email ingestion;
11
- - MIME parsing, sender allowlisting and per-account pairing addresses;
12
- - layered transaction deduplication in Cloudflare D1;
13
- - durable webhook delivery through Cloudflare Queues with retry and healing;
14
- - encrypted webhook secrets, audit records, retention and optional R2 backups.
12
+ BankSync is a TypeScript package and a deployable Cloudflare Worker for turning
13
+ Czech bank transaction notifications into durable, signed webhook events. It
14
+ hides email authentication, parsing, pairing, deduplication, retries, secret
15
+ handling, and delivery recovery behind one small consumer interface.
15
16
 
16
- ## Install as a package
17
+ ```text
18
+ receive bank activity → authenticate → normalize → deduplicate → deliver durably
19
+ ```
20
+
21
+ ## At a glance
22
+
23
+ | | |
24
+ |---|---|
25
+ | **Inputs** | Fio Bank email, Fio API, Air Bank email |
26
+ | **Output** | Signed `transaction.received` webhook, version `1` |
27
+ | **Runtime** | Cloudflare Workers, Email Routing, D1 and Queues; optional R2 |
28
+ | **Package** | Runtime-neutral helpers plus a separate Cloudflare Worker export |
29
+ | **Safety model** | Tenant ownership, authenticated email identity, durable idempotency and exact-host callback policy |
30
+
31
+ ```mermaid
32
+ flowchart LR
33
+ email[Bank notification email] --> worker[BankSync Worker]
34
+ fio[Fio API] --> worker
35
+ worker --> d1[(Cloudflare D1)]
36
+ worker --> queue[Cloudflare Queue]
37
+ queue --> consumer[Your webhook consumer]
38
+ worker -. encrypted backup .-> r2[(Cloudflare R2)]
39
+ ```
40
+
41
+ ## Choose your path
17
42
 
18
- `v0.1.0` is an unsafe preview and must not be used in production. Install only
19
- the latest version published to npm by the protected release workflow after its
20
- checksum and provenance are available:
43
+ | You want to… | Start here |
44
+ |---|---|
45
+ | Verify BankSync webhooks in an application | [Install the package](#install-the-package) and use `verifyWebhook` |
46
+ | Run your own BankSync ingestion Worker | [Deploy the Worker](#deploy-the-worker) |
47
+ | Understand the signed event format | [Webhook contract](#webhook-contract) |
48
+ | Review the security assumptions | [Security model](#security-model) |
49
+ | Contribute to BankSync | [Development](#development) |
50
+
51
+ ## Install the package
52
+
53
+ `v0.1.0` is an unsafe preview and must not be used in production. Install the
54
+ current npm release produced by the protected release workflow:
21
55
 
22
56
  ```bash
23
57
  pnpm add @festapp/banksync
24
58
  ```
25
59
 
26
- The root export contains runtime-neutral parsing and webhook helpers:
60
+ The root export is runtime-neutral. A typical consumer needs only the high-level
61
+ verifier:
27
62
 
28
63
  ```ts
29
64
  import {
30
- detectProvider,
31
- parseEmail,
32
65
  verifyWebhook,
33
66
  type WebhookEnvelope,
34
67
  } from "@festapp/banksync";
68
+
69
+ const bodyBytes = new Uint8Array(await request.arrayBuffer());
70
+
71
+ const envelope: WebhookEnvelope = await verifyWebhook({
72
+ secret,
73
+ timestamp: request.headers.get("x-banksync-timestamp") ?? "",
74
+ deliveryId: request.headers.get("x-banksync-delivery-id") ?? "",
75
+ signature: request.headers.get("x-banksync-signature") ?? "",
76
+ bodyBytes,
77
+ });
78
+
79
+ // Claim envelope.delivery_id atomically in durable storage before billing or
80
+ // any other side effect. Signature verification is not replay persistence.
35
81
  ```
36
82
 
37
- The Cloudflare Worker is a separate export so importing the core does not pull
38
- Cloudflare bindings into application code:
83
+ Other root exports cover provider detection and email parsing, payment-reference
84
+ resolution, Fio API mapping, currency normalization, ISO 11649 references, and
85
+ webhook signing. The Cloudflare implementation stays behind a separate export,
86
+ so application code does not pull in Worker bindings:
39
87
 
40
88
  ```ts
41
89
  export { default } from "@festapp/banksync/cloudflare";
42
90
  export type { Env } from "@festapp/banksync/cloudflare";
43
91
  ```
44
92
 
45
- ## Deploy to Cloudflare
46
-
47
- 1. Clone this repository and install dependencies with `pnpm install`.
48
- 2. Copy `wrangler.example.toml` to `wrangler.toml` and replace every placeholder.
49
- 3. Create the D1 database, queues and optional R2 bucket named in the config.
50
- 4. For a fresh database apply the baseline; for an existing production database
51
- whose history ends at `0009`, apply only `0010_security_hardening.sql` through
52
- a scoped reviewed migration operation.
53
- 5. Set `ADMIN_SECRET`, `WEBHOOK_KEK`, `ENCRYPTION_KEY_V1`, and the active
54
- `BACKUP_ENCRYPTION_KEY_Vn` with
93
+ ## What the Worker handles
94
+
95
+ - authenticates sender and recipient identity from Cloudflare-owned envelope
96
+ evidence and a configured trusted `Authentication-Results` authserv-id;
97
+ - parses supported Fio Bank and Air Bank notifications and can poll the Fio API;
98
+ - pairs each notification through a per-account receiver address;
99
+ - normalizes transaction data and deduplicates it in D1;
100
+ - creates durable delivery jobs and retries them through Cloudflare Queues;
101
+ - heals stalled delivery state without repeating a completed consumer outcome;
102
+ - encrypts stored credentials and optional R2 backup envelopes;
103
+ - records administrative audit events and applies bounded retention;
104
+ - exposes only coarse public health while protecting detailed operator routes.
105
+
106
+ ## Deploy the Worker
107
+
108
+ ### Prerequisites
109
+
110
+ - Node.js 20 or newer and pnpm;
111
+ - a Cloudflare account with Workers, D1, Queues and Email Routing;
112
+ - an R2 bucket only if encrypted backups are enabled;
113
+ - exact callback hostnames for every intended webhook consumer.
114
+
115
+ ### Setup
116
+
117
+ 1. Clone this repository and run `pnpm install`.
118
+ 2. Copy [`wrangler.example.toml`](wrangler.example.toml) to `wrangler.toml` and
119
+ replace every resource placeholder.
120
+ 3. Create the D1 database and queues named by the configuration.
121
+ 4. For a fresh database, apply `migrations/0001_schema.sql`. For a production
122
+ database whose recorded history ends at `0009`, apply only
123
+ `migrations/0010_security_hardening.sql` through a scoped, reviewed migration.
124
+ 5. Configure `ADMIN_SECRET`, `WEBHOOK_KEK`, `ENCRYPTION_KEY_V1`, and—when R2
125
+ backups are enabled—the selected `BACKUP_ENCRYPTION_KEY_Vn` with
55
126
  `pnpm wrangler secret put <NAME>`.
56
- 6. Configure `CALLBACK_HOST_ALLOWLIST` with exact consumer hostnames. Production
57
- refuses an empty allowlist, credentials, IP literals, special-use hosts,
58
- fragments, suffix matches, and redirects.
59
- 7. Inspect one sanitized accepted and rejected Cloudflare message to establish
60
- the Cloudflare-owned Authentication-Results authserv-id, then configure it as
61
- `EMAIL_AUTHSERV_ID`. Email ingest intentionally remains disabled while it is
62
- absent; MIME headers are never an identity fallback.
63
- 8. Run the release gates and deploy the exact attested artifact.
64
-
65
- All three key families must be independent random 32-byte base64 values.
66
- `BACKUP_ENCRYPTION_KEY_VERSION` selects the active backup key. Store backup
67
- keys outside R2 and test restore before rotation. Never commit `wrangler.toml`,
68
- `.dev.vars`, bank API tokens, backup keys, or consumer webhook secrets.
69
-
70
- ### Migration baseline
71
-
72
- Fresh databases are created from the single canonical schema baseline
73
- `migrations/0001_schema.sql` (schema version 10). The original incremental
74
- `0001`–`0009` history remains recorded in existing D1 databases but is not
75
- needed to create a new deployment. To stay compatible with those databases,
76
- production upgrade is `0010_security_hardening.sql`; all future migrations must
77
- start at `0011` or higher. Never renumber
78
- or replace the baseline filename.
79
-
80
- BankSync's D1 schema is intentionally independent of any consumer's billing
81
- database. Billing settlement belongs behind each consumer's signed webhook
82
- endpoint, so installing BankSync never creates or changes Mendelio/Supabase
83
- billing tables or routines.
84
-
85
- For local development, copy `.dev.vars.example` to `.dev.vars`, use placeholder
86
- Cloudflare resource identifiers in `wrangler.toml`, apply migrations locally,
87
- and run `pnpm dev`.
127
+ 6. Set `CALLBACK_HOST_ALLOWLIST` to exact consumer hostnames. Production rejects
128
+ an empty policy, credentials, IP literals, special-use hosts, suffix matches,
129
+ fragments and redirects.
130
+ 7. Inspect sanitized accepted and rejected Cloudflare messages, establish the
131
+ Cloudflare-owned authserv-id, and set `EMAIL_AUTHSERV_ID`. Email ingestion
132
+ deliberately remains disabled until this value is known; MIME headers are
133
+ never used as an identity fallback.
134
+ 8. Run `pnpm check`, review the dry-run artifact, and deploy the composition you
135
+ have configured.
136
+
137
+ All key families must use independent random 32-byte base64 values. Keep backup
138
+ keys outside R2 and prove restore before rotation. Never commit `wrangler.toml`,
139
+ `.dev.vars`, bank API tokens, backup keys or webhook secrets.
140
+
141
+ ### Migration rules
142
+
143
+ Fresh databases use the single canonical baseline `0001_schema.sql`, which
144
+ creates schema version 10. Existing databases retain their recorded `0001`–`0009`
145
+ history and advance through `0010_security_hardening.sql`. Future migrations
146
+ must start at `0011`; never renumber or replace the baseline.
147
+
148
+ BankSync D1 is intentionally independent of consumer billing databases.
149
+ BankSync emits authenticated transaction facts. Each consumer owns settlement,
150
+ invoicing and its own durable delivery claim.
151
+
152
+ For local development, use placeholder Cloudflare identifiers, copy
153
+ `.dev.vars.example` to `.dev.vars`, apply migrations locally, then run:
154
+
155
+ ```bash
156
+ pnpm dev
157
+ ```
88
158
 
89
159
  ## Webhook contract
90
160
 
91
- BankSync sends `transaction.received` version `1`. `delivery_id` is stable
92
- across retries and is the consumer idempotency key. Each request includes:
161
+ BankSync sends event `transaction.received` with `event_version: 1`.
162
+ `delivery_id` is stable across retries and is the consumer's idempotency key.
163
+ Each request includes:
93
164
 
94
165
  - `X-BankSync-Timestamp`
95
166
  - `X-BankSync-Delivery-Id`
96
167
  - `X-BankSync-Signature: sha256=<hex>`
97
168
 
98
- The HMAC input is the exact byte sequence
99
- `timestamp + "." + deliveryId + "." + bodyBytes`. Verify the raw body before
100
- JSON parsing. `verifyWebhook` validates raw-body HMAC, timestamp syntax and
101
- tolerance, delivery header/body equality, event, and event version, then returns
102
- a typed verified envelope. It throws `WebhookVerificationError` with a stable
103
- code. There is intentionally no HMAC-only public verifier.
169
+ The HMAC input is the exact byte sequence:
104
170
 
105
- ```ts
106
- import { verifyWebhook } from "@festapp/banksync";
107
-
108
- const bodyBytes = new Uint8Array(await request.arrayBuffer());
109
- const envelope = await verifyWebhook({
110
- secret,
111
- timestamp: request.headers.get("x-banksync-timestamp") ?? "",
112
- deliveryId: request.headers.get("x-banksync-delivery-id") ?? "",
113
- signature: request.headers.get("x-banksync-signature") ?? "",
114
- bodyBytes,
115
- });
116
- // Atomically claim envelope.delivery_id in durable consumer storage before any
117
- // billing or other side effect. The verifier cannot provide durable replay
118
- // protection on its own.
171
+ ```text
172
+ timestamp + "." + deliveryId + "." + bodyBytes
119
173
  ```
120
174
 
121
- ## Trust boundaries and operations
122
-
123
- - Cloudflare Email Routing is the trusted SMTP/envelope boundary. The Worker
124
- requires one configured trusted auth result plus aligned DKIM or DMARC and
125
- exact agreement between envelope and MIME identities.
126
- - `bank_accounts.owner_app_id` is the tenant authorization authority. Only an
127
- admin can create a cross-owner subscription.
128
- - Public HTTP exposes only `GET /health` with `{ "ok": true }` or a stable
129
- unavailable response. `/status` and `/health/deep` require admin auth.
130
- - Callback delivery revalidates the current exact-host policy immediately before
131
- every fetch and never follows redirects.
132
- - Idempotency is scoped by principal, method, and canonical path. Credential
133
- creation/rotation endpoints reject `Idempotency-Key` and never cache responses.
134
- - R2 backups are AES-256-GCM `.sql.enc` envelopes. Ephemeral idempotency and
135
- rate-limit tables are excluded. Decrypt to a new mode-0600 file without
136
- printing plaintext:
175
+ Verify the raw body before JSON parsing. `verifyWebhook` validates the raw-body
176
+ HMAC, timestamp syntax and tolerance, delivery header/body equality, event name
177
+ and event version. Failure throws `WebhookVerificationError` with a stable code.
178
+ There is intentionally no public HMAC-only verifier.
179
+
180
+ ## Security model
181
+
182
+ - **Email trust:** Cloudflare Email Routing is the SMTP/envelope seam. A message
183
+ needs a configured trusted auth result plus aligned DKIM or DMARC and exact
184
+ agreement between envelope and MIME identities.
185
+ - **Tenant authority:** `bank_accounts.owner_app_id` decides who may operate on
186
+ an account. Only an administrator may create a cross-owner subscription.
187
+ - **Callback egress:** every attempt revalidates the current exact-host policy
188
+ immediately before fetch and never follows redirects.
189
+ - **Replay handling:** BankSync keeps delivery IDs stable; consumers must claim
190
+ them atomically in their own durable storage before side effects.
191
+ - **Credential handling:** credential creation and rotation reject
192
+ `Idempotency-Key`; their plaintext responses never enter the idempotency cache.
193
+ - **Operator visibility:** public HTTP exposes only `GET /health`. `/status` and
194
+ `/health/deep` require administrator authentication.
195
+ - **Backups:** optional R2 backups are AES-256-GCM `.sql.enc` envelopes and omit
196
+ ephemeral idempotency and rate-limit tables.
197
+
198
+ Decrypt a backup into a new mode-0600 file without printing plaintext:
137
199
 
138
200
  ```bash
139
201
  BACKUP_DECRYPTION_KEY='<base64 key>' \
140
202
  node scripts/decrypt-backup.mjs backup.sql.enc restored.sql
141
203
  ```
142
204
 
205
+ For the full rollout and rollback rules, see
206
+ [`docs/security-hardening-rollout.md`](docs/security-hardening-rollout.md).
207
+
143
208
  ## Development
144
209
 
145
210
  ```bash
@@ -147,9 +212,14 @@ pnpm check
147
212
  pnpm pack --dry-run
148
213
  ```
149
214
 
150
- Provider-live tests are intentionally not part of the default suite and must
151
- never be run with production bank credentials.
215
+ `pnpm check` runs type checking, the complete test suite, dual ESM/CommonJS
216
+ builds, and package-export verification. Provider-live tests are intentionally
217
+ outside the default suite and must never use production bank credentials.
218
+
219
+ Stable releases are created from protected `v*` tags by GitHub Actions, publish
220
+ to npm through OIDC trusted publishing, and attach the exact same tarball,
221
+ checksum, provenance and SBOM to the GitHub Release.
152
222
 
153
223
  ## License
154
224
 
155
- MIT
225
+ [MIT](LICENSE) © Festapp
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@festapp/banksync",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "Bank transaction ingestion for Cloudflare Workers: Czech bank email and Fio API parsing, durable deduplication, and signed webhook delivery.",
5
5
  "type": "module",
6
6
  "packageManager": "pnpm@11.24.0",