@festapp/banksync 0.0.0-bootstrap.20260831 → 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.
- package/README.md +173 -103
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,145 +1,210 @@
|
|
|
1
|
-
|
|
1
|
+
<h1 align="center">BankSync</h1>
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
|
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
|
-
|
|
38
|
-
|
|
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
|
-
##
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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.
|
|
57
|
-
|
|
58
|
-
fragments
|
|
59
|
-
7. Inspect
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
8. Run the
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
keys outside R2 and
|
|
68
|
-
`.dev.vars`, bank API tokens, backup keys
|
|
69
|
-
|
|
70
|
-
### Migration
|
|
71
|
-
|
|
72
|
-
Fresh databases
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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`
|
|
92
|
-
across retries and is the consumer idempotency key.
|
|
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
|
-
```
|
|
106
|
-
|
|
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
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
151
|
-
|
|
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.
|
|
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",
|