@festapp/banksync 0.2.1 → 0.2.3

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
@@ -279,3 +279,19 @@ Publishing this package does not deploy the shared Worker, mutate D1, enable ban
279
279
  polling, or authorize replay of another consumer's history.
280
280
 
281
281
  Canonical v2 transactions preserve bank-provided VS and payer reference without inferring an order identifier. Each consumer (Festapp, Mendelio, or another application) owns payment matching and business rules. The existing v1 RF-to-VS projection remains only as an explicit backwards-compatible adapter.
282
+
283
+ Successful schema 11 Fio imports immediately sweep the canonical webhook outbox into the delivery queue, including retryable dispatch failures from earlier imports. Periodic reconciliation remains recovery; consumers do not wait for its next tick in the normal path.
284
+
285
+
286
+ ### Fio token authorization failures
287
+
288
+ A newly created Fio token must also be authorized in Internetbanking; creating
289
+ or copying it alone does not activate it. Fio documents HTTP 500 as a nonexistent
290
+ or inactive token ([Fio API documentation](https://www.fio.cz/docs/cz/API_Bankovnictvi.pdf),
291
+ section 8). An observed inactive token returned that response after 30.4 seconds.
292
+ The client allows 55 seconds; an egress proxy should allow at least 45 seconds
293
+ upstream and mark forwarded responses with `x-fio-upstream-status`. Only a direct
294
+ or explicitly forwarded bank 500 becomes `fio_token_invalid_or_inactive`.
295
+ Transport timeouts and proxy failures remain transient, not token diagnoses.
296
+ Manual sync returns HTTP 422 with that stable error code, which is also stored
297
+ in `api_last_error` and exposed by the owner-scoped account status endpoint.
@@ -1,4 +1,4 @@
1
- "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }// src/normalize.ts
1
+ "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; } var _class;// src/normalize.ts
2
2
  var ISO_ALIASES = {
3
3
  "k\u010D": "CZK",
4
4
  "kc": "CZK",
@@ -568,6 +568,13 @@ var FioApiError = class extends Error {
568
568
  }
569
569
 
570
570
  };
571
+ var FioTokenInvalidOrInactive = (_class = class extends FioApiError {
572
+ __init() {this.code = "fio_token_invalid_or_inactive"}
573
+ constructor() {
574
+ super("fio_token_invalid_or_inactive", 500);_class.prototype.__init.call(this);;
575
+ this.name = "FioTokenInvalidOrInactive";
576
+ }
577
+ }, _class);
571
578
  var FioRateLimited = class extends FioApiError {
572
579
  constructor(status, retryAfterS) {
573
580
  super(`Fio API ${status}`, status);
@@ -591,10 +598,10 @@ async function fioRequest(op, token, directUrl, proxy, date, toDate) {
591
598
  method: "POST",
592
599
  headers: { "content-type": "application/json", "x-fio-proxy-secret": proxy.secret },
593
600
  body: JSON.stringify(date === void 0 ? { op, token } : { op, token, date, ...toDate ? { toDate } : {} }),
594
- signal: AbortSignal.timeout(2e4)
601
+ signal: AbortSignal.timeout(55e3)
595
602
  });
596
603
  }
597
- return fetch(directUrl, { signal: AbortSignal.timeout(2e4) });
604
+ return fetch(directUrl, { signal: AbortSignal.timeout(55e3) });
598
605
  }
599
606
  function retryAfterSeconds(headers) {
600
607
  const raw = headers.get("Retry-After");
@@ -602,7 +609,10 @@ function retryAfterSeconds(headers) {
602
609
  const n = Number.parseInt(raw, 10);
603
610
  return Number.isFinite(n) && n >= 0 ? n : null;
604
611
  }
605
- async function ensureFioResponse(res) {
612
+ async function ensureFioResponse(res, proxy) {
613
+ if (res.status === 500 && (!proxy || res.headers.get("x-fio-upstream-status") === "500")) {
614
+ throw new FioTokenInvalidOrInactive();
615
+ }
606
616
  if (res.status === 429 || res.status === 409) {
607
617
  throw new FioRateLimited(res.status, retryAfterSeconds(res.headers));
608
618
  }
@@ -615,7 +625,7 @@ async function ensureFioResponse(res) {
615
625
  }
616
626
  async function fetchNewTransactions(token, proxy) {
617
627
  const res = await fioRequest("transactions", token, `${endpoint("last", token)}/transactions.json`, proxy);
618
- await ensureFioResponse(res);
628
+ await ensureFioResponse(res, proxy);
619
629
  const json = await res.json();
620
630
  const transactions = _nullishCoalesce(_optionalChain([json, 'access', _4 => _4.accountStatement, 'optionalAccess', _5 => _5.transactionList, 'optionalAccess', _6 => _6.transaction]), () => ( []));
621
631
  return Array.isArray(transactions) ? transactions : [transactions];
@@ -625,7 +635,7 @@ async function setFioPointer(token, yyyyMmDd, proxy) {
625
635
  throw new Error("invalid_fio_pointer_date");
626
636
  }
627
637
  const res = await fioRequest("set-last-date", token, `${endpoint("set-last-date", token)}/${yyyyMmDd}/`, proxy, yyyyMmDd);
628
- await ensureFioResponse(res);
638
+ await ensureFioResponse(res, proxy);
629
639
  }
630
640
  function column(raw, idx) {
631
641
  const value = _optionalChain([raw, 'access', _7 => _7[`column${idx}`], 'optionalAccess', _8 => _8.value]);
@@ -702,7 +712,7 @@ async function fetchFioStatement(token, from, to, proxy) {
702
712
  for (const date of [from, to]) if (!/^\d{4}-\d{2}-\d{2}$/.test(date) || !Number.isFinite(Date.parse(date))) throw new Error("invalid_statement_window");
703
713
  if (from > to || Date.parse(to) - Date.parse(from) > 90 * 864e5) throw new Error("unbounded_statement_window");
704
714
  const response = await fioRequest("periods", token, `${endpoint("periods", token)}/${from}/${to}/transactions.json`, proxy, from, to);
705
- await ensureFioResponse(response);
715
+ await ensureFioResponse(response, proxy);
706
716
  const document = await response.json();
707
717
  const statement = document.accountStatement;
708
718
  if (!_optionalChain([statement, 'optionalAccess', _9 => _9.info])) throw new Error("missing_statement_info");
@@ -732,4 +742,5 @@ async function fetchFioStatement(token, from, to, proxy) {
732
742
 
733
743
 
734
744
 
735
- exports.normalizeCurrency = normalizeCurrency; exports.toCents = toCents; exports.decimalToCents = decimalToCents; exports.BANK_CODES = BANK_CODES; exports.detectProvider = detectProvider; exports.parseEmail = parseEmail; exports.encodeRf = encodeRf; exports.decodeRf = decodeRf; exports.resolveVariableSymbol = resolveVariableSymbol; exports.extractReferenceCandidates = extractReferenceCandidates; exports.buildWebhookEnvelope = buildWebhookEnvelope; exports.signWebhook = signWebhook; exports.WebhookVerificationError = WebhookVerificationError; exports.verifyWebhook = verifyWebhook; exports.FioApiError = FioApiError; exports.FioRateLimited = FioRateLimited; exports.FioTransientFailure = FioTransientFailure; exports.fetchNewTransactions = fetchNewTransactions; exports.setFioPointer = setFioPointer; exports.mapFioTransaction = mapFioTransaction; exports.fetchFioStatement = fetchFioStatement;
745
+
746
+ exports.normalizeCurrency = normalizeCurrency; exports.toCents = toCents; exports.decimalToCents = decimalToCents; exports.BANK_CODES = BANK_CODES; exports.detectProvider = detectProvider; exports.parseEmail = parseEmail; exports.encodeRf = encodeRf; exports.decodeRf = decodeRf; exports.resolveVariableSymbol = resolveVariableSymbol; exports.extractReferenceCandidates = extractReferenceCandidates; exports.buildWebhookEnvelope = buildWebhookEnvelope; exports.signWebhook = signWebhook; exports.WebhookVerificationError = WebhookVerificationError; exports.verifyWebhook = verifyWebhook; exports.FioApiError = FioApiError; exports.FioTokenInvalidOrInactive = FioTokenInvalidOrInactive; exports.FioRateLimited = FioRateLimited; exports.FioTransientFailure = FioTransientFailure; exports.fetchNewTransactions = fetchNewTransactions; exports.setFioPointer = setFioPointer; exports.mapFioTransaction = mapFioTransaction; exports.fetchFioStatement = fetchFioStatement;
@@ -568,6 +568,13 @@ var FioApiError = class extends Error {
568
568
  }
569
569
  status;
570
570
  };
571
+ var FioTokenInvalidOrInactive = class extends FioApiError {
572
+ code = "fio_token_invalid_or_inactive";
573
+ constructor() {
574
+ super("fio_token_invalid_or_inactive", 500);
575
+ this.name = "FioTokenInvalidOrInactive";
576
+ }
577
+ };
571
578
  var FioRateLimited = class extends FioApiError {
572
579
  constructor(status, retryAfterS) {
573
580
  super(`Fio API ${status}`, status);
@@ -591,10 +598,10 @@ async function fioRequest(op, token, directUrl, proxy, date, toDate) {
591
598
  method: "POST",
592
599
  headers: { "content-type": "application/json", "x-fio-proxy-secret": proxy.secret },
593
600
  body: JSON.stringify(date === void 0 ? { op, token } : { op, token, date, ...toDate ? { toDate } : {} }),
594
- signal: AbortSignal.timeout(2e4)
601
+ signal: AbortSignal.timeout(55e3)
595
602
  });
596
603
  }
597
- return fetch(directUrl, { signal: AbortSignal.timeout(2e4) });
604
+ return fetch(directUrl, { signal: AbortSignal.timeout(55e3) });
598
605
  }
599
606
  function retryAfterSeconds(headers) {
600
607
  const raw = headers.get("Retry-After");
@@ -602,7 +609,10 @@ function retryAfterSeconds(headers) {
602
609
  const n = Number.parseInt(raw, 10);
603
610
  return Number.isFinite(n) && n >= 0 ? n : null;
604
611
  }
605
- async function ensureFioResponse(res) {
612
+ async function ensureFioResponse(res, proxy) {
613
+ if (res.status === 500 && (!proxy || res.headers.get("x-fio-upstream-status") === "500")) {
614
+ throw new FioTokenInvalidOrInactive();
615
+ }
606
616
  if (res.status === 429 || res.status === 409) {
607
617
  throw new FioRateLimited(res.status, retryAfterSeconds(res.headers));
608
618
  }
@@ -615,7 +625,7 @@ async function ensureFioResponse(res) {
615
625
  }
616
626
  async function fetchNewTransactions(token, proxy) {
617
627
  const res = await fioRequest("transactions", token, `${endpoint("last", token)}/transactions.json`, proxy);
618
- await ensureFioResponse(res);
628
+ await ensureFioResponse(res, proxy);
619
629
  const json = await res.json();
620
630
  const transactions = json.accountStatement?.transactionList?.transaction ?? [];
621
631
  return Array.isArray(transactions) ? transactions : [transactions];
@@ -625,7 +635,7 @@ async function setFioPointer(token, yyyyMmDd, proxy) {
625
635
  throw new Error("invalid_fio_pointer_date");
626
636
  }
627
637
  const res = await fioRequest("set-last-date", token, `${endpoint("set-last-date", token)}/${yyyyMmDd}/`, proxy, yyyyMmDd);
628
- await ensureFioResponse(res);
638
+ await ensureFioResponse(res, proxy);
629
639
  }
630
640
  function column(raw, idx) {
631
641
  const value = raw[`column${idx}`]?.value;
@@ -702,7 +712,7 @@ async function fetchFioStatement(token, from, to, proxy) {
702
712
  for (const date of [from, to]) if (!/^\d{4}-\d{2}-\d{2}$/.test(date) || !Number.isFinite(Date.parse(date))) throw new Error("invalid_statement_window");
703
713
  if (from > to || Date.parse(to) - Date.parse(from) > 90 * 864e5) throw new Error("unbounded_statement_window");
704
714
  const response = await fioRequest("periods", token, `${endpoint("periods", token)}/${from}/${to}/transactions.json`, proxy, from, to);
705
- await ensureFioResponse(response);
715
+ await ensureFioResponse(response, proxy);
706
716
  const document = await response.json();
707
717
  const statement = document.accountStatement;
708
718
  if (!statement?.info) throw new Error("missing_statement_info");
@@ -726,6 +736,7 @@ export {
726
736
  WebhookVerificationError,
727
737
  verifyWebhook,
728
738
  FioApiError,
739
+ FioTokenInvalidOrInactive,
729
740
  FioRateLimited,
730
741
  FioTransientFailure,
731
742
  fetchNewTransactions,
@@ -10,7 +10,8 @@
10
10
 
11
11
 
12
12
 
13
- var _chunkN7BVOSFNcjs = require('./chunk-N7BVOSFN.cjs');
13
+
14
+ var _chunk6TJITWUFcjs = require('./chunk-6TJITWUF.cjs');
14
15
 
15
16
  // src/mime.ts
16
17
  var _postalmime = require('postal-mime'); var _postalmime2 = _interopRequireDefault(_postalmime);
@@ -67,13 +68,13 @@ function classifyEmail(extracted, identity, account, allowlist) {
67
68
  if (account.ingest_mode === "api") {
68
69
  return { kind: "reject", reason: "email_ingest_disabled", bankAccountId: account.id, received: false };
69
70
  }
70
- const provider = _chunkN7BVOSFNcjs.detectProvider.call(void 0, extracted.text, account.account_number);
71
+ const provider = _chunk6TJITWUFcjs.detectProvider.call(void 0, extracted.text, account.account_number);
71
72
  if (provider === "unknown") {
72
73
  return { kind: "reject", reason: `unknown_provider: ${account.account_number}`, bankAccountId: account.id, received: true };
73
74
  }
74
75
  let parsed;
75
76
  try {
76
- parsed = _chunkN7BVOSFNcjs.parseEmail.call(void 0, extracted.text, provider);
77
+ parsed = _chunk6TJITWUFcjs.parseEmail.call(void 0, extracted.text, provider);
77
78
  } catch (err) {
78
79
  return { kind: "reject", reason: `${err}`.replace(/^Error:\s*/, ""), bankAccountId: account.id, received: true };
79
80
  }
@@ -610,7 +611,7 @@ async function insertTransaction(db, args) {
610
611
  const v2 = _optionalChain([schema, 'optionalAccess', _16 => _16.value]) === "11";
611
612
  const p = {
612
613
  ...args.payload,
613
- vs: v2 ? _nullishCoalesce(_nullishCoalesce(args.payload.raw_vs, () => ( args.payload.vs)), () => ( null)) : _chunkN7BVOSFNcjs.resolveVariableSymbol.call(void 0, args.payload)
614
+ vs: v2 ? _nullishCoalesce(_nullishCoalesce(args.payload.raw_vs, () => ( args.payload.vs)), () => ( null)) : _chunk6TJITWUFcjs.resolveVariableSymbol.call(void 0, args.payload)
614
615
  };
615
616
  if (p.transaction_id != null && p.source === "fio_api") {
616
617
  const hit = await db.prepare(SQL_FIND_DUPLICATE_FIO).bind(args.bank_account_id, p.transaction_id).first();
@@ -1227,7 +1228,7 @@ async function ensureDeliveryJobs(db, transactionId, cutover) {
1227
1228
  for (const row of candidates.results) {
1228
1229
  const { pairing_code, consumer_app_id, event_version, created_at: _createdAt, ...transaction } = row;
1229
1230
  const deliveryId = _ulid.ulid.call(void 0, );
1230
- const envelope = _chunkN7BVOSFNcjs.buildWebhookEnvelope.call(void 0, {
1231
+ const envelope = _chunk6TJITWUFcjs.buildWebhookEnvelope.call(void 0, {
1231
1232
  delivery_id: deliveryId,
1232
1233
  pairing_code: row.pairing_code,
1233
1234
  transaction,
@@ -1549,7 +1550,7 @@ async function httpResult(response, deliveryId, usedPrevSecret, primaryStatus) {
1549
1550
  return "receipt" in parsed ? { kind: "http", httpStatus: response.status, usedPrevSecret, primaryStatus, receipt: parsed.receipt } : { kind: "http", httpStatus: response.status, usedPrevSecret, primaryStatus, receiptError: parsed.error };
1550
1551
  }
1551
1552
  async function postSigned(url2, envelope, secret) {
1552
- const signed = await _chunkN7BVOSFNcjs.signWebhook.call(void 0, { envelope, secret });
1553
+ const signed = await _chunk6TJITWUFcjs.signWebhook.call(void 0, { envelope, secret });
1553
1554
  const controller = new AbortController();
1554
1555
  const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
1555
1556
  try {
@@ -2707,7 +2708,7 @@ async function recoverBankAccount(db, account, env, proxy) {
2707
2708
  let statement;
2708
2709
  if (batch.cipher !== null && batch.key_version !== null) statement = JSON.parse(await decryptSecret(batch.cipher, batch.key_version, env));
2709
2710
  else {
2710
- statement = await _chunkN7BVOSFNcjs.fetchFioStatement.call(void 0, credential, batch.from_date, batch.to_date, proxy);
2711
+ statement = await _chunk6TJITWUFcjs.fetchFioStatement.call(void 0, credential, batch.from_date, batch.to_date, proxy);
2711
2712
  await renew();
2712
2713
  const encrypted = await encryptSecret(JSON.stringify(statement), env);
2713
2714
  await db.prepare(`UPDATE bank_recovery_batches SET cipher=?,key_version=?,state='spooled' WHERE id=?`).bind(encrypted.cipher, encrypted.keyVersion, batch.id).run();
@@ -2719,7 +2720,7 @@ async function recoverBankAccount(db, account, env, proxy) {
2719
2720
  let inserted = 0, skipped = 0;
2720
2721
  for (const raw of statement.transactions) {
2721
2722
  await renew();
2722
- const mapped = _chunkN7BVOSFNcjs.mapFioTransaction.call(void 0, raw);
2723
+ const mapped = _chunk6TJITWUFcjs.mapFioTransaction.call(void 0, raw);
2723
2724
  if (!mapped || mapped.identity_kind !== "movement") throw new Error("fio_unverified_movement");
2724
2725
  if (mapped.currency !== String(_nullishCoalesce(_optionalChain([statement, 'access', _63 => _63.info, 'optionalAccess', _64 => _64.currency]), () => ( ""))).toUpperCase()) throw new Error("fio_statement_currency_mismatch");
2725
2726
  const result = await insertTransaction(db, { bank_account_id: account.id, payload: mapped });
@@ -2962,13 +2963,13 @@ async function runBankApiSync(env, account) {
2962
2963
  const schema = await env.DB.prepare("SELECT value FROM schema_meta WHERE key='version'").first();
2963
2964
  if (_optionalChain([schema, 'optionalAccess', _75 => _75.value]) === "11") {
2964
2965
  const result = await recoverBankAccount(env.DB, account, env, fioProxyConfig(env));
2965
- await ensureDeliveryJobs(env.DB);
2966
- return { bank_account_id: account.id, provider: account.account_type, inserted: result.inserted, skipped_duplicate: result.skipped, skipped_outgoing: 0, parse_errors: 0, queued_webhooks: 0, backfill: isBackfill, deferred: false };
2966
+ const delivery = await createWebhookDeliveryCoordinator(env).sweep();
2967
+ return { bank_account_id: account.id, provider: account.account_type, inserted: result.inserted, skipped_duplicate: result.skipped, skipped_outgoing: 0, parse_errors: 0, queued_webhooks: delivery.queued, backfill: isBackfill, deferred: false };
2967
2968
  }
2968
2969
  const token = await decryptSecret(account.api_token_cipher, account.api_token_key_ver, env);
2969
2970
  if (isBackfill && account.api_last_success_at === null) {
2970
2971
  await markBankAccountApiFetchStarted(env.DB, account.id);
2971
- await _chunkN7BVOSFNcjs.setFioPointer.call(void 0, token, yyyyMmDdDaysAgo(90), fioProxyConfig(env));
2972
+ await _chunk6TJITWUFcjs.setFioPointer.call(void 0, token, yyyyMmDdDaysAgo(90), fioProxyConfig(env));
2972
2973
  await markBankAccountApiPointerSet(env.DB, account.id);
2973
2974
  await writeEvent(env.DB, {
2974
2975
  event_type: "api_pointer_set",
@@ -2988,7 +2989,7 @@ async function runBankApiSync(env, account) {
2988
2989
  };
2989
2990
  }
2990
2991
  await markBankAccountApiFetchStarted(env.DB, account.id);
2991
- const raw = await _chunkN7BVOSFNcjs.fetchNewTransactions.call(void 0, token, fioProxyConfig(env));
2992
+ const raw = await _chunk6TJITWUFcjs.fetchNewTransactions.call(void 0, token, fioProxyConfig(env));
2992
2993
  let inserted = 0;
2993
2994
  let skippedDuplicate = 0;
2994
2995
  let skippedOutgoing = 0;
@@ -2997,7 +2998,7 @@ async function runBankApiSync(env, account) {
2997
2998
  for (const item of raw) {
2998
2999
  let mapped;
2999
3000
  try {
3000
- mapped = _chunkN7BVOSFNcjs.mapFioTransaction.call(void 0, item);
3001
+ mapped = _chunk6TJITWUFcjs.mapFioTransaction.call(void 0, item);
3001
3002
  } catch (err) {
3002
3003
  parseErrors++;
3003
3004
  await insertParseLog(env.DB, {
@@ -3052,8 +3053,8 @@ async function runBankApiSync(env, account) {
3052
3053
  deferred: false
3053
3054
  };
3054
3055
  } catch (err) {
3055
- await markBankAccountApiFetchFailure(env.DB, account.id, String(err));
3056
- if (err instanceof _chunkN7BVOSFNcjs.FioRateLimited) {
3056
+ await markBankAccountApiFetchFailure(env.DB, account.id, err instanceof _chunk6TJITWUFcjs.FioTokenInvalidOrInactive ? err.code : String(err));
3057
+ if (err instanceof _chunk6TJITWUFcjs.FioRateLimited) {
3057
3058
  await writeEvent(env.DB, {
3058
3059
  event_type: "api_rate_limited",
3059
3060
  bank_account_id: account.id,
@@ -3078,11 +3079,11 @@ async function runDueBankApiSyncs(env) {
3078
3079
  deferred: result.deferred
3079
3080
  });
3080
3081
  } catch (err) {
3081
- if (err instanceof _chunkN7BVOSFNcjs.FioRateLimited) {
3082
+ if (err instanceof _chunk6TJITWUFcjs.FioRateLimited) {
3082
3083
  log("api_rate_limited", { bank_account_id: account.id, account_type: account.account_type, retry_after_s: err.retryAfterS });
3083
3084
  continue;
3084
3085
  }
3085
- if (err instanceof _chunkN7BVOSFNcjs.FioTransientFailure) {
3086
+ if (err instanceof _chunk6TJITWUFcjs.FioTransientFailure) {
3086
3087
  logError("api_transient_failure", err, { bank_account_id: account.id, account_type: account.account_type });
3087
3088
  continue;
3088
3089
  }
@@ -3408,6 +3409,10 @@ async function dispatch(url2, req, env, authCtx, bodyText) {
3408
3409
  return jsonResponse(accounts);
3409
3410
  }
3410
3411
  const baMatch = url2.pathname.match(/^\/bank-accounts\/(\d+)$/);
3412
+ if (baMatch && req.method === "GET") {
3413
+ const account = await requireOwnedAccount(env, Number(baMatch[1]), authCtx);
3414
+ return account instanceof Response ? account : jsonResponse(account);
3415
+ }
3411
3416
  if (baMatch && req.method === "PUT") {
3412
3417
  const id = Number(baMatch[1]);
3413
3418
  const existing = await requireOwnedAccount(env, id, authCtx);
@@ -3546,7 +3551,10 @@ async function dispatch(url2, req, env, authCtx, bodyText) {
3546
3551
  api_backfill_done: _nullishCoalesce(_optionalChain([updated, 'optionalAccess', _99 => _99.api_backfill_done]), () => ( false))
3547
3552
  });
3548
3553
  } catch (err) {
3549
- if (err instanceof _chunkN7BVOSFNcjs.FioRateLimited || err instanceof _chunkN7BVOSFNcjs.FioTransientFailure) {
3554
+ if (err instanceof _chunk6TJITWUFcjs.FioTokenInvalidOrInactive) {
3555
+ return jsonResponse({ error: err.code, bank_http_status: err.status }, 422);
3556
+ }
3557
+ if (err instanceof _chunk6TJITWUFcjs.FioRateLimited || err instanceof _chunk6TJITWUFcjs.FioTransientFailure) {
3550
3558
  logError("fio_api_manual_sync_failed", err, { bank_account_id: id });
3551
3559
  return jsonResponse({ error: "fio_api_transient_failure" }, 503);
3552
3560
  }
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  FioRateLimited,
3
+ FioTokenInvalidOrInactive,
3
4
  FioTransientFailure,
4
5
  buildWebhookEnvelope,
5
6
  detectProvider,
@@ -10,7 +11,7 @@ import {
10
11
  resolveVariableSymbol,
11
12
  setFioPointer,
12
13
  signWebhook
13
- } from "./chunk-3WM2GGFL.js";
14
+ } from "./chunk-PIBCQAMQ.js";
14
15
 
15
16
  // src/mime.ts
16
17
  import PostalMime from "postal-mime";
@@ -2962,8 +2963,8 @@ async function runBankApiSync(env, account) {
2962
2963
  const schema = await env.DB.prepare("SELECT value FROM schema_meta WHERE key='version'").first();
2963
2964
  if (schema?.value === "11") {
2964
2965
  const result = await recoverBankAccount(env.DB, account, env, fioProxyConfig(env));
2965
- await ensureDeliveryJobs(env.DB);
2966
- return { bank_account_id: account.id, provider: account.account_type, inserted: result.inserted, skipped_duplicate: result.skipped, skipped_outgoing: 0, parse_errors: 0, queued_webhooks: 0, backfill: isBackfill, deferred: false };
2966
+ const delivery = await createWebhookDeliveryCoordinator(env).sweep();
2967
+ return { bank_account_id: account.id, provider: account.account_type, inserted: result.inserted, skipped_duplicate: result.skipped, skipped_outgoing: 0, parse_errors: 0, queued_webhooks: delivery.queued, backfill: isBackfill, deferred: false };
2967
2968
  }
2968
2969
  const token = await decryptSecret(account.api_token_cipher, account.api_token_key_ver, env);
2969
2970
  if (isBackfill && account.api_last_success_at === null) {
@@ -3052,7 +3053,7 @@ async function runBankApiSync(env, account) {
3052
3053
  deferred: false
3053
3054
  };
3054
3055
  } catch (err) {
3055
- await markBankAccountApiFetchFailure(env.DB, account.id, String(err));
3056
+ await markBankAccountApiFetchFailure(env.DB, account.id, err instanceof FioTokenInvalidOrInactive ? err.code : String(err));
3056
3057
  if (err instanceof FioRateLimited) {
3057
3058
  await writeEvent(env.DB, {
3058
3059
  event_type: "api_rate_limited",
@@ -3408,6 +3409,10 @@ async function dispatch(url2, req, env, authCtx, bodyText) {
3408
3409
  return jsonResponse(accounts);
3409
3410
  }
3410
3411
  const baMatch = url2.pathname.match(/^\/bank-accounts\/(\d+)$/);
3412
+ if (baMatch && req.method === "GET") {
3413
+ const account = await requireOwnedAccount(env, Number(baMatch[1]), authCtx);
3414
+ return account instanceof Response ? account : jsonResponse(account);
3415
+ }
3411
3416
  if (baMatch && req.method === "PUT") {
3412
3417
  const id = Number(baMatch[1]);
3413
3418
  const existing = await requireOwnedAccount(env, id, authCtx);
@@ -3546,6 +3551,9 @@ async function dispatch(url2, req, env, authCtx, bodyText) {
3546
3551
  api_backfill_done: updated?.api_backfill_done ?? false
3547
3552
  });
3548
3553
  } catch (err) {
3554
+ if (err instanceof FioTokenInvalidOrInactive) {
3555
+ return jsonResponse({ error: err.code, bank_http_status: err.status }, 422);
3556
+ }
3549
3557
  if (err instanceof FioRateLimited || err instanceof FioTransientFailure) {
3550
3558
  logError("fio_api_manual_sync_failed", err, { bank_account_id: id });
3551
3559
  return jsonResponse({ error: "fio_api_transient_failure" }, 503);
package/dist/index.cjs CHANGED
@@ -19,8 +19,8 @@
19
19
 
20
20
 
21
21
 
22
- var _chunkN7BVOSFNcjs = require('./chunk-N7BVOSFN.cjs');
23
22
 
23
+ var _chunk6TJITWUFcjs = require('./chunk-6TJITWUF.cjs');
24
24
 
25
25
 
26
26
 
@@ -41,4 +41,6 @@ var _chunkN7BVOSFNcjs = require('./chunk-N7BVOSFN.cjs');
41
41
 
42
42
 
43
43
 
44
- exports.BANK_CODES = _chunkN7BVOSFNcjs.BANK_CODES; exports.FioApiError = _chunkN7BVOSFNcjs.FioApiError; exports.FioRateLimited = _chunkN7BVOSFNcjs.FioRateLimited; exports.FioTransientFailure = _chunkN7BVOSFNcjs.FioTransientFailure; exports.WebhookVerificationError = _chunkN7BVOSFNcjs.WebhookVerificationError; exports.buildWebhookEnvelope = _chunkN7BVOSFNcjs.buildWebhookEnvelope; exports.decimalToCents = _chunkN7BVOSFNcjs.decimalToCents; exports.decodeRf = _chunkN7BVOSFNcjs.decodeRf; exports.detectProvider = _chunkN7BVOSFNcjs.detectProvider; exports.encodeRf = _chunkN7BVOSFNcjs.encodeRf; exports.extractReferenceCandidates = _chunkN7BVOSFNcjs.extractReferenceCandidates; exports.fetchNewTransactions = _chunkN7BVOSFNcjs.fetchNewTransactions; exports.mapFioTransaction = _chunkN7BVOSFNcjs.mapFioTransaction; exports.normalizeCurrency = _chunkN7BVOSFNcjs.normalizeCurrency; exports.parseEmail = _chunkN7BVOSFNcjs.parseEmail; exports.resolveVariableSymbol = _chunkN7BVOSFNcjs.resolveVariableSymbol; exports.setFioPointer = _chunkN7BVOSFNcjs.setFioPointer; exports.signWebhook = _chunkN7BVOSFNcjs.signWebhook; exports.toCents = _chunkN7BVOSFNcjs.toCents; exports.verifyWebhook = _chunkN7BVOSFNcjs.verifyWebhook;
44
+
45
+
46
+ exports.BANK_CODES = _chunk6TJITWUFcjs.BANK_CODES; exports.FioApiError = _chunk6TJITWUFcjs.FioApiError; exports.FioRateLimited = _chunk6TJITWUFcjs.FioRateLimited; exports.FioTokenInvalidOrInactive = _chunk6TJITWUFcjs.FioTokenInvalidOrInactive; exports.FioTransientFailure = _chunk6TJITWUFcjs.FioTransientFailure; exports.WebhookVerificationError = _chunk6TJITWUFcjs.WebhookVerificationError; exports.buildWebhookEnvelope = _chunk6TJITWUFcjs.buildWebhookEnvelope; exports.decimalToCents = _chunk6TJITWUFcjs.decimalToCents; exports.decodeRf = _chunk6TJITWUFcjs.decodeRf; exports.detectProvider = _chunk6TJITWUFcjs.detectProvider; exports.encodeRf = _chunk6TJITWUFcjs.encodeRf; exports.extractReferenceCandidates = _chunk6TJITWUFcjs.extractReferenceCandidates; exports.fetchNewTransactions = _chunk6TJITWUFcjs.fetchNewTransactions; exports.mapFioTransaction = _chunk6TJITWUFcjs.mapFioTransaction; exports.normalizeCurrency = _chunk6TJITWUFcjs.normalizeCurrency; exports.parseEmail = _chunk6TJITWUFcjs.parseEmail; exports.resolveVariableSymbol = _chunk6TJITWUFcjs.resolveVariableSymbol; exports.setFioPointer = _chunk6TJITWUFcjs.setFioPointer; exports.signWebhook = _chunk6TJITWUFcjs.signWebhook; exports.toCents = _chunk6TJITWUFcjs.toCents; exports.verifyWebhook = _chunk6TJITWUFcjs.verifyWebhook;
package/dist/index.d.cts CHANGED
@@ -88,6 +88,13 @@ declare class FioApiError extends Error {
88
88
  readonly status: number;
89
89
  constructor(message: string, status: number);
90
90
  }
91
+ /** Fio documents HTTP 500 as a nonexistent/inactive token, including one
92
+ * created but not yet authorized in Internetbanking. Never infer this from
93
+ * a proxy-generated 500 or a transport timeout. */
94
+ declare class FioTokenInvalidOrInactive extends FioApiError {
95
+ readonly code = "fio_token_invalid_or_inactive";
96
+ constructor();
97
+ }
91
98
  declare class FioRateLimited extends FioApiError {
92
99
  readonly retryAfterS: number | null;
93
100
  constructor(status: number, retryAfterS: number | null);
@@ -111,4 +118,4 @@ declare function fetchNewTransactions(token: string, proxy?: FioProxyConfig): Pr
111
118
  declare function setFioPointer(token: string, yyyyMmDd: string, proxy?: FioProxyConfig): Promise<void>;
112
119
  declare function mapFioTransaction(raw: FioTransaction): Omit<Transaction, 'id' | 'bank_account_id'> | null;
113
120
 
114
- export { BANK_CODES, type EmailProvider, FioApiError, type FioColumn, type FioProxyConfig, FioRateLimited, type FioTransaction, FioTransientFailure, type ParsedEmailTransaction, type ReferenceSource, type SignedWebhook, Transaction, type VerifyWebhookArgs, WebhookEnvelope, type WebhookVerificationCode, WebhookVerificationError, buildWebhookEnvelope, decimalToCents, decodeRf, detectProvider, encodeRf, extractReferenceCandidates, fetchNewTransactions, mapFioTransaction, normalizeCurrency, parseEmail, resolveVariableSymbol, setFioPointer, signWebhook, toCents, verifyWebhook };
121
+ export { BANK_CODES, type EmailProvider, FioApiError, type FioColumn, type FioProxyConfig, FioRateLimited, FioTokenInvalidOrInactive, type FioTransaction, FioTransientFailure, type ParsedEmailTransaction, type ReferenceSource, type SignedWebhook, Transaction, type VerifyWebhookArgs, WebhookEnvelope, type WebhookVerificationCode, WebhookVerificationError, buildWebhookEnvelope, decimalToCents, decodeRf, detectProvider, encodeRf, extractReferenceCandidates, fetchNewTransactions, mapFioTransaction, normalizeCurrency, parseEmail, resolveVariableSymbol, setFioPointer, signWebhook, toCents, verifyWebhook };
package/dist/index.d.ts CHANGED
@@ -88,6 +88,13 @@ declare class FioApiError extends Error {
88
88
  readonly status: number;
89
89
  constructor(message: string, status: number);
90
90
  }
91
+ /** Fio documents HTTP 500 as a nonexistent/inactive token, including one
92
+ * created but not yet authorized in Internetbanking. Never infer this from
93
+ * a proxy-generated 500 or a transport timeout. */
94
+ declare class FioTokenInvalidOrInactive extends FioApiError {
95
+ readonly code = "fio_token_invalid_or_inactive";
96
+ constructor();
97
+ }
91
98
  declare class FioRateLimited extends FioApiError {
92
99
  readonly retryAfterS: number | null;
93
100
  constructor(status: number, retryAfterS: number | null);
@@ -111,4 +118,4 @@ declare function fetchNewTransactions(token: string, proxy?: FioProxyConfig): Pr
111
118
  declare function setFioPointer(token: string, yyyyMmDd: string, proxy?: FioProxyConfig): Promise<void>;
112
119
  declare function mapFioTransaction(raw: FioTransaction): Omit<Transaction, 'id' | 'bank_account_id'> | null;
113
120
 
114
- export { BANK_CODES, type EmailProvider, FioApiError, type FioColumn, type FioProxyConfig, FioRateLimited, type FioTransaction, FioTransientFailure, type ParsedEmailTransaction, type ReferenceSource, type SignedWebhook, Transaction, type VerifyWebhookArgs, WebhookEnvelope, type WebhookVerificationCode, WebhookVerificationError, buildWebhookEnvelope, decimalToCents, decodeRf, detectProvider, encodeRf, extractReferenceCandidates, fetchNewTransactions, mapFioTransaction, normalizeCurrency, parseEmail, resolveVariableSymbol, setFioPointer, signWebhook, toCents, verifyWebhook };
121
+ export { BANK_CODES, type EmailProvider, FioApiError, type FioColumn, type FioProxyConfig, FioRateLimited, FioTokenInvalidOrInactive, type FioTransaction, FioTransientFailure, type ParsedEmailTransaction, type ReferenceSource, type SignedWebhook, Transaction, type VerifyWebhookArgs, WebhookEnvelope, type WebhookVerificationCode, WebhookVerificationError, buildWebhookEnvelope, decimalToCents, decodeRf, detectProvider, encodeRf, extractReferenceCandidates, fetchNewTransactions, mapFioTransaction, normalizeCurrency, parseEmail, resolveVariableSymbol, setFioPointer, signWebhook, toCents, verifyWebhook };
package/dist/index.js CHANGED
@@ -2,6 +2,7 @@ import {
2
2
  BANK_CODES,
3
3
  FioApiError,
4
4
  FioRateLimited,
5
+ FioTokenInvalidOrInactive,
5
6
  FioTransientFailure,
6
7
  WebhookVerificationError,
7
8
  buildWebhookEnvelope,
@@ -19,11 +20,12 @@ import {
19
20
  signWebhook,
20
21
  toCents,
21
22
  verifyWebhook
22
- } from "./chunk-3WM2GGFL.js";
23
+ } from "./chunk-PIBCQAMQ.js";
23
24
  export {
24
25
  BANK_CODES,
25
26
  FioApiError,
26
27
  FioRateLimited,
28
+ FioTokenInvalidOrInactive,
27
29
  FioTransientFailure,
28
30
  WebhookVerificationError,
29
31
  buildWebhookEnvelope,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@festapp/banksync",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
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",