@accounter/scraper-app
Version:
Scraper app with Fastify server and React UI
3,731 lines • 127 kB
JavaScript
// src/server/index.ts
import { existsSync } from "fs";
import path2 from "path";
import { fileURLToPath, pathToFileURL } from "url";
import Fastify from "fastify";
import staticPlugin from "@fastify/static";
// src/server/history.ts
import { readFile, writeFile } from "fs/promises";
var MAX_RECORDS = 100;
var writeQueues = /* @__PURE__ */ new Map();
function enqueue(filePath, work) {
const prev = writeQueues.get(filePath) ?? Promise.resolve();
const next = prev.then(work, work);
writeQueues.set(filePath, next);
void next.finally(() => {
if (writeQueues.get(filePath) === next) writeQueues.delete(filePath);
});
return next;
}
async function doAppend(record, filePath) {
let records = [];
try {
const raw = await readFile(filePath, "utf8");
const parsed = JSON.parse(raw);
records = Array.isArray(parsed) ? parsed : [];
} catch {
}
records.push(record);
if (records.length > MAX_RECORDS) {
records = records.slice(-MAX_RECORDS);
}
await writeFile(filePath, JSON.stringify(records, null, 2), "utf8");
}
function appendRun(record, filePath) {
return enqueue(filePath, () => doAppend(record, filePath));
}
async function readHistory(filePath) {
try {
const raw = await readFile(filePath, "utf8");
const parsed = JSON.parse(raw);
const records = Array.isArray(parsed) ? parsed : [];
return records.slice(-MAX_RECORDS).reverse();
} catch {
return [];
}
}
// src/server/vault-store.ts
import { constants } from "fs";
import { access, copyFile, mkdir, rename, unlink } from "fs/promises";
import path from "path";
// src/server/vault.ts
import { createCipheriv, createDecipheriv, randomBytes, scrypt } from "crypto";
import { readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
import { z, ZodError } from "zod";
function getVaultPath() {
return process.env["VAULT_PATH"] ?? ".vault";
}
var ALGORITHM = "aes-256-gcm";
var KEY_LEN = 32;
var SALT_LEN = 32;
var IV_LEN = 12;
var TAG_LEN = 16;
var SCRYPT_N = 65536;
var SCRYPT_R = 8;
var SCRYPT_P = 1;
var SCRYPT_MAXMEM = 128 * SCRYPT_N * SCRYPT_R * 2;
var HEADER_LEN = 6;
var PoalimAccountSchema = z.object({
id: z.string(),
nickname: z.string().optional(),
userCode: z.string(),
password: z.string(),
options: z.object({
isBusinessAccount: z.boolean().optional(),
acceptedAccountNumbers: z.array(z.string()).optional(),
acceptedBranchNumbers: z.array(z.string()).optional(),
ignoredAccountNumbers: z.array(z.string()).optional(),
ignoredBranchNumbers: z.array(z.string()).optional()
}).optional()
});
var DiscountAccountSchema = z.object({
id: z.string(),
ID: z.string(),
password: z.string(),
code: z.string().optional(),
nickname: z.string().optional()
});
var IsracardAmexAccountSchema = z.object({
id: z.string(),
nickname: z.string().optional(),
ownerId: z.string(),
password: z.string(),
last6Digits: z.string(),
options: z.object({
acceptedCardNumbers: z.array(z.string()).optional(),
ignoredCardNumbers: z.array(z.string()).optional(),
cardNumberMapping: z.record(z.string(), z.string()).optional()
}).optional()
});
var CalAccountSchema = z.object({
id: z.string(),
nickname: z.string().optional(),
username: z.string(),
password: z.string(),
last4Digits: z.string(),
options: z.object({
acceptedCardNumbers: z.array(z.string()).optional(),
ignoredCardNumbers: z.array(z.string()).optional()
}).optional()
});
var MaxAccountSchema = z.object({
id: z.string(),
nickname: z.string().optional(),
username: z.string(),
password: z.string(),
options: z.object({
acceptedCardNumbers: z.array(z.string()).optional(),
ignoredCardNumbers: z.array(z.string()).optional()
}).optional()
});
var OtsarHahayalAccountSchema = z.object({
id: z.string(),
nickname: z.string().optional(),
userCode: z.string(),
password: z.string()
});
var SettingsSchema = z.object({
showBrowser: z.boolean().default(false),
fetchBankOfIsraelRates: z.boolean().default(true),
concurrentScraping: z.boolean().default(false),
defaultDateRangeMonths: z.number().int().positive().default(3),
historyFilePath: z.string().default("./history.json"),
saveHistory: z.boolean().default(true),
serverUrl: z.string().optional(),
apiKey: z.string().optional(),
vaultPath: z.string().trim().min(1).optional()
});
var AccountRecordSchema = z.object({
id: z.string(),
sourceId: z.string(),
sourceType: z.enum([
"poalim",
"discount",
"isracard",
"amex",
"cal",
"max",
"otsar-hahayal",
"otsar-hahayal-credit-card"
]),
accountNumber: z.string(),
branchNumber: z.string().optional(),
status: z.enum(["accepted", "ignored", "pending"]).default("pending")
});
var VaultSchema = z.object({
poalimAccounts: z.array(PoalimAccountSchema).default([]),
discountAccounts: z.array(DiscountAccountSchema).default([]),
isracardAccounts: z.array(IsracardAmexAccountSchema).default([]),
amexAccounts: z.array(IsracardAmexAccountSchema).default([]),
calAccounts: z.array(CalAccountSchema).default([]),
maxAccounts: z.array(MaxAccountSchema).default([]),
otsarHahayalAccounts: z.array(OtsarHahayalAccountSchema).default([]),
accountRecords: z.array(AccountRecordSchema).default([]),
settings: SettingsSchema.default({
showBrowser: false,
fetchBankOfIsraelRates: true,
concurrentScraping: false,
defaultDateRangeMonths: 3,
historyFilePath: "./history.json",
saveHistory: true
})
});
function deriveKey(password, salt, options) {
return new Promise((resolve, reject) => {
scrypt(password, salt, KEY_LEN, options, (err, key) => {
if (err) reject(err);
else resolve(key);
});
});
}
async function encryptVault(vault, password) {
const salt = randomBytes(SALT_LEN);
const iv = randomBytes(IV_LEN);
const key = await deriveKey(password, salt, {
N: SCRYPT_N,
r: SCRYPT_R,
p: SCRYPT_P,
maxmem: SCRYPT_MAXMEM
});
const cipher = createCipheriv(ALGORITHM, key, iv);
const encrypted = Buffer.concat([cipher.update(JSON.stringify(vault), "utf8"), cipher.final()]);
const authTag = cipher.getAuthTag();
const header = Buffer.alloc(HEADER_LEN);
header.writeUInt32BE(SCRYPT_N, 0);
header.writeUInt8(SCRYPT_R, 4);
header.writeUInt8(SCRYPT_P, 5);
return Buffer.concat([header, salt, iv, authTag, encrypted]).toString("base64");
}
async function decryptVault(blob, password) {
try {
const buf = Buffer.from(blob, "base64");
const N = buf.readUInt32BE(0);
const r = buf.readUInt8(4);
const p = buf.readUInt8(5);
const salt = buf.subarray(HEADER_LEN, HEADER_LEN + SALT_LEN);
const iv = buf.subarray(HEADER_LEN + SALT_LEN, HEADER_LEN + SALT_LEN + IV_LEN);
const authTag = buf.subarray(
HEADER_LEN + SALT_LEN + IV_LEN,
HEADER_LEN + SALT_LEN + IV_LEN + TAG_LEN
);
const body = buf.subarray(HEADER_LEN + SALT_LEN + IV_LEN + TAG_LEN);
const key = await deriveKey(password, salt, { N, r, p, maxmem: 128 * N * r * 2 });
const decipher = createDecipheriv(ALGORITHM, key, iv);
decipher.setAuthTag(authTag);
const plaintext = Buffer.concat([decipher.update(body), decipher.final()]).toString("utf8");
return VaultSchema.parse(JSON.parse(plaintext));
} catch (err) {
if (err instanceof ZodError) {
console.error("[vault] schema validation failed after decryption:", err.issues);
}
return null;
}
}
function defaultVault() {
return VaultSchema.parse({ settings: {} });
}
async function loadVaultFile(filePath, password) {
const blob = await readFile2(filePath, "utf8");
return decryptVault(blob.trim(), password);
}
async function saveVaultFile(filePath, vault, password) {
const data = await encryptVault(vault, password);
await writeFile2(filePath, data, "utf8");
}
// src/server/vault-store.ts
var _vault = null;
var _password = null;
var _vaultPath = null;
function getCurrentVaultPath() {
return _vaultPath ?? getVaultPath();
}
async function hasVaultFile() {
return access(getCurrentVaultPath()).then(() => true).catch(() => false);
}
async function unlockVault(password) {
if (!await hasVaultFile()) return "not-found";
const vault = await loadVaultFile(getVaultPath(), password);
if (vault === null) return "wrong-password";
_vault = vault;
_password = password;
_vaultPath = getVaultPath();
return "ok";
}
function getVault() {
if (_vault === null) throw new Error("Vault is locked");
return _vault;
}
async function updateVault(fn) {
if (_vault === null || _password === null) throw new Error("Vault is locked");
_vault = fn(_vault);
await saveVaultFile(getCurrentVaultPath(), _vault, _password);
}
function isLocked() {
return _vault === null;
}
function lockVault() {
_vault = null;
_password = null;
_vaultPath = null;
}
async function moveVaultFile(newPath) {
if (_vault === null || _password === null) throw new Error("Vault is locked");
const oldPath = path.resolve(getCurrentVaultPath());
const normalizedNewPath = path.resolve(newPath);
if (oldPath === normalizedNewPath) return;
await mkdir(path.dirname(normalizedNewPath), { recursive: true });
const destExists = await access(normalizedNewPath).then(() => true).catch(() => false);
if (destExists) throw new Error("Destination already exists");
try {
await rename(oldPath, normalizedNewPath);
} catch (err) {
if (err.code !== "EXDEV") throw err;
await copyFile(oldPath, normalizedNewPath, constants.COPYFILE_EXCL);
await unlink(oldPath);
}
_vaultPath = normalizedNewPath;
process.env["VAULT_PATH"] = normalizedNewPath;
}
// src/server/history-routes.ts
async function registerHistoryRoutes(app) {
app.get("/api/history", async (_req, reply) => {
if (isLocked()) return reply.status(401).send({ error: "vault-locked" });
const { historyFilePath } = getVault().settings;
return readHistory(historyFilePath);
});
}
// src/server/vault-routes.ts
import { readFile as readFile3, rename as rename2, writeFile as writeFile3 } from "fs/promises";
// src/server/accounts-routes.ts
function guardLocked(reply) {
if (isLocked()) return reply.status(401).send({ error: "vault-locked" });
return null;
}
var SOURCE_TYPE_TO_VAULT_KEY = {
poalim: "poalimAccounts",
discount: "discountAccounts",
isracard: "isracardAccounts",
amex: "amexAccounts",
cal: "calAccounts",
max: "maxAccounts",
"otsar-hahayal": "otsarHahayalAccounts",
"otsar-hahayal-credit-card": "otsarHahayalAccounts"
};
async function registerAccountsRoutes(app) {
app.get("/api/vault/accounts", async (_req, reply) => {
const blocked = guardLocked(reply);
if (blocked) return blocked;
const vault = getVault();
const { accountRecords } = vault;
return accountRecords.map((r) => {
const vaultKey = SOURCE_TYPE_TO_VAULT_KEY[r.sourceType];
const accounts = vaultKey ? vault[vaultKey] : void 0;
return {
...r,
nickname: accounts?.find((a) => a.id === r.sourceId)?.nickname
};
});
});
app.put(
"/api/vault/accounts/:id",
async (req, reply) => {
const blocked = guardLocked(reply);
if (blocked) return blocked;
const { id } = req.params;
const { status } = req.body;
if (status !== "accepted" && status !== "ignored") {
return reply.status(400).send({ error: "status must be accepted or ignored" });
}
const vault = getVault();
if (!vault.accountRecords.some((a) => a.id === id)) {
return reply.status(404).send({ error: "not-found" });
}
await updateVault((v) => {
const idx = v.accountRecords.findIndex((a) => a.id === id);
if (idx === -1) return v;
const accounts = [...v.accountRecords];
accounts[idx] = { ...accounts[idx], status };
return { ...v, accountRecords: accounts };
});
return getVault().accountRecords;
}
);
app.delete("/api/vault/accounts/:id", async (req, reply) => {
const blocked = guardLocked(reply);
if (blocked) return blocked;
const { id } = req.params;
if (!getVault().accountRecords.some((a) => a.id === id)) {
return reply.status(404).send({ error: "not-found" });
}
await updateVault((v) => ({
...v,
accountRecords: v.accountRecords.filter((a) => a.id !== id)
}));
return getVault().accountRecords;
});
}
// src/server/settings-routes.ts
function guardLocked2(reply) {
if (isLocked()) return reply.status(401).send({ error: "vault-locked" });
return null;
}
async function registerSettingsRoutes(app) {
app.get("/api/vault/settings", async (_req, reply) => {
const blocked = guardLocked2(reply);
if (blocked) return blocked;
return getVault().settings;
});
app.put("/api/vault/settings", async (req, reply) => {
const blocked = guardLocked2(reply);
if (blocked) return blocked;
const parsed = SettingsSchema.partial().safeParse(req.body);
if (!parsed.success) return reply.status(400).send({ error: parsed.error.issues });
const newPath = parsed.data.vaultPath;
if (newPath !== void 0 && newPath !== getCurrentVaultPath()) {
try {
await moveVaultFile(newPath);
} catch {
return reply.status(500).send({ error: "move-failed" });
}
}
await updateVault((v) => ({ ...v, settings: { ...v.settings, ...parsed.data } }));
return getVault().settings;
});
}
// src/server/sources-routes.ts
import { randomUUID } from "crypto";
import { z as z2 } from "zod";
var SourceConfigSchema = z2.discriminatedUnion("type", [
PoalimAccountSchema.extend({ type: z2.literal("poalim") }),
DiscountAccountSchema.extend({ type: z2.literal("discount") }),
IsracardAmexAccountSchema.extend({ type: z2.literal("isracard") }),
IsracardAmexAccountSchema.extend({ type: z2.literal("amex") }),
CalAccountSchema.extend({ type: z2.literal("cal") }),
MaxAccountSchema.extend({ type: z2.literal("max") }),
OtsarHahayalAccountSchema.extend({ type: z2.literal("otsar-hahayal") })
]);
function collectSources(vault) {
return [
...vault.poalimAccounts.map((a) => ({ ...a, type: "poalim" })),
...vault.discountAccounts.map((a) => ({ ...a, type: "discount" })),
...vault.isracardAccounts.map((a) => ({ ...a, type: "isracard" })),
...vault.amexAccounts.map((a) => ({ ...a, type: "amex" })),
...vault.calAccounts.map((a) => ({ ...a, type: "cal" })),
...vault.maxAccounts.map((a) => ({ ...a, type: "max" })),
...vault.otsarHahayalAccounts.map((a) => ({ ...a, type: "otsar-hahayal" }))
];
}
function appendSource(vault, source) {
const { type, ...rest } = source;
const v = { ...vault };
switch (type) {
case "poalim":
v.poalimAccounts = [...vault.poalimAccounts, rest];
break;
case "discount":
v.discountAccounts = [
...vault.discountAccounts,
rest
];
break;
case "isracard":
v.isracardAccounts = [
...vault.isracardAccounts,
rest
];
break;
case "amex":
v.amexAccounts = [...vault.amexAccounts, rest];
break;
case "cal":
v.calAccounts = [...vault.calAccounts, rest];
break;
case "max":
v.maxAccounts = [...vault.maxAccounts, rest];
break;
case "otsar-hahayal":
v.otsarHahayalAccounts = [
...vault.otsarHahayalAccounts,
rest
];
break;
}
return v;
}
var typeToKey = {
poalim: "poalimAccounts",
discount: "discountAccounts",
isracard: "isracardAccounts",
amex: "amexAccounts",
cal: "calAccounts",
max: "maxAccounts",
"otsar-hahayal": "otsarHahayalAccounts"
};
var typeToPatchSchema = {
poalim: PoalimAccountSchema.omit({ id: true }).partial(),
discount: DiscountAccountSchema.omit({ id: true }).partial(),
isracard: IsracardAmexAccountSchema.omit({ id: true }).partial(),
amex: IsracardAmexAccountSchema.omit({ id: true }).partial(),
cal: CalAccountSchema.omit({ id: true }).partial(),
max: MaxAccountSchema.omit({ id: true }).partial(),
"otsar-hahayal": OtsarHahayalAccountSchema.omit({ id: true }).partial()
};
function findSourceKey(vault, id) {
for (const [type, key] of Object.entries(typeToKey)) {
const arr = vault[key];
const idx = arr.findIndex((a) => a.id === id);
if (idx !== -1) return { key, idx, type };
}
return null;
}
function patchSource(vault, found, patch) {
const { key, idx } = found;
const arr = vault[key];
const newArr = [...arr];
newArr[idx] = { ...arr[idx], ...patch };
return { ...vault, [key]: newArr };
}
function removeSource(vault, id) {
const found = findSourceKey(vault, id);
if (!found) return null;
const { key } = found;
const arr = vault[key];
return { ...vault, [key]: arr.filter((a) => a.id !== id) };
}
function guardLocked3(reply) {
if (isLocked()) {
return reply.status(401).send({ error: "vault-locked" });
}
return null;
}
async function registerSourcesRoutes(app) {
app.get("/api/vault/sources", async (_req, reply) => {
const blocked = guardLocked3(reply);
if (blocked) return blocked;
return collectSources(getVault());
});
app.post("/api/vault/sources", async (req, reply) => {
const blocked = guardLocked3(reply);
if (blocked) return blocked;
const parsed = SourceConfigSchema.safeParse({ ...req.body, id: randomUUID() });
if (!parsed.success) return reply.status(400).send({ error: parsed.error.issues });
await updateVault((v) => appendSource(v, parsed.data));
return collectSources(getVault());
});
app.put(
"/api/vault/sources/:id",
async (req, reply) => {
const blocked = guardLocked3(reply);
if (blocked) return blocked;
const found = findSourceKey(getVault(), req.params.id);
if (!found) return reply.status(404).send({ error: "not-found" });
const parsed = typeToPatchSchema[found.type].safeParse(req.body);
if (!parsed.success) return reply.status(400).send({ error: parsed.error.issues });
await updateVault((v) => patchSource(v, found, parsed.data));
return collectSources(getVault());
}
);
app.delete("/api/vault/sources/:id", async (req, reply) => {
const blocked = guardLocked3(reply);
if (blocked) return blocked;
const updated = removeSource(getVault(), req.params.id);
if (!updated) return reply.status(404).send({ error: "not-found" });
await updateVault(() => updated);
return collectSources(getVault());
});
}
// src/server/vault-routes.ts
async function registerVaultRoutes(app) {
app.get("/api/vault/status", async () => ({
locked: isLocked(),
hasFile: await hasVaultFile()
}));
app.post(
"/api/vault/unlock",
{
schema: {
body: {
type: "object",
required: ["password"],
properties: { password: { type: "string" } }
}
}
},
async (req, reply) => {
const result = await unlockVault(req.body.password);
if (result === "ok") return { ok: true };
const status = result === "not-found" ? 404 : 401;
return reply.status(status).send({ error: result });
}
);
app.post(
"/api/vault/create",
{
schema: {
body: {
type: "object",
required: ["password", "serverUrl", "apiKey"],
properties: {
password: { type: "string" },
serverUrl: { type: "string" },
apiKey: { type: "string" }
}
}
}
},
async (req, reply) => {
if (await hasVaultFile()) return reply.status(409).send({ error: "vault-already-exists" });
const { password, serverUrl, apiKey } = req.body;
lockVault();
const vault = defaultVault();
vault.settings.serverUrl = serverUrl;
vault.settings.apiKey = apiKey;
await saveVaultFile(getVaultPath(), vault, password);
const result = await unlockVault(password);
if (result !== "ok") return reply.status(500).send({ error: result });
return reply.status(201).send({ ok: true });
}
);
app.get("/api/vault/env-path", async () => ({
path: getVaultPath()
}));
app.get("/api/vault/download", async (_req, reply) => {
try {
const fileBuffer = await readFile3(getVaultPath());
reply.header("Content-Type", "application/octet-stream");
reply.header("Content-Disposition", 'attachment; filename=".vault"');
return reply.send(fileBuffer);
} catch (err) {
if (err.code === "ENOENT") {
return reply.status(404).send({ error: "no-vault-file" });
}
throw err;
}
});
app.addContentTypeParser(
"application/octet-stream",
{ parseAs: "buffer" },
(_req, body, done) => done(null, body)
);
app.post(
"/api/vault/upload",
{ config: { rawBody: false } },
async (req, reply) => {
const force = req.query.force === "true";
const vaultPath = getVaultPath();
try {
const exists = await hasVaultFile();
if (exists && !force) {
return reply.status(409).send({ error: "vault-already-exists" });
}
await writeFile3(vaultPath + ".tmp", req.body);
await rename2(vaultPath + ".tmp", vaultPath);
lockVault();
return reply.status(200).send({ ok: true });
} catch (err) {
req.log.error(err, "Vault upload failed");
return reply.status(500).send({ error: "write-failed" });
}
}
);
app.get("/api/vault/path", async (_req, reply) => {
if (isLocked()) return reply.status(401).send({ error: "vault-locked" });
return { path: getVaultPath() };
});
app.get("/api/vault/test-connection", async (_req, reply) => {
if (isLocked()) return reply.status(401).send({ error: "vault-locked" });
const { serverUrl, apiKey } = getVault().settings;
if (!serverUrl || !apiKey) {
return reply.status(400).send({ error: "Server URL and API key are not configured" });
}
const t0 = Date.now();
try {
const res = await fetch(serverUrl, {
method: "POST",
headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` },
body: JSON.stringify({ query: "{ __typename }" }),
signal: AbortSignal.timeout(1e4)
});
const latencyMs = Date.now() - t0;
if (!res.ok) {
const text = await res.text().catch(() => res.statusText);
return { ok: false, error: `HTTP ${res.status}: ${text}`, latencyMs };
}
return { ok: true, latencyMs };
} catch (err) {
const latencyMs = Date.now() - t0;
return { ok: false, error: err instanceof Error ? err.message : String(err), latencyMs };
}
});
await registerSourcesRoutes(app);
await registerSettingsRoutes(app);
await registerAccountsRoutes(app);
}
// src/server/websocket.ts
import websocketPlugin from "@fastify/websocket";
// src/shared/ws-protocol.ts
import { z as z3 } from "zod";
// src/shared/source-types.ts
var SOURCE_TYPES = [
"poalim",
"discount",
"isracard",
"amex",
"cal",
"max",
"otsar-hahayal",
"otsar-hahayal-credit-card"
];
// src/shared/ws-protocol.ts
var CancelScrapeSchema = z3.object({
type: z3.literal("cancel-scrape")
});
var PingSchema = z3.object({
type: z3.literal("ping")
});
var RunStartSchema = z3.object({
type: z3.literal("run-start"),
sourceIds: z3.array(z3.string()).optional(),
dateFrom: z3.string().optional(),
dateTo: z3.string().optional()
});
var OtpSubmitSchema = z3.object({
type: z3.literal("otp-submit"),
sourceId: z3.string(),
otp: z3.string()
});
var ClientMessageSchema = z3.discriminatedUnion("type", [
CancelScrapeSchema,
PingSchema,
RunStartSchema,
OtpSubmitSchema
]);
var ConnectedSchema = z3.object({
type: z3.literal("connected")
});
var PongSchema = z3.object({
type: z3.literal("pong")
});
var WsErrorSchema = z3.object({
type: z3.literal("error"),
message: z3.string()
});
var TaskPendingSchema = z3.object({
type: z3.literal("task-pending"),
sourceId: z3.string()
});
var TaskRunningSchema = z3.object({
type: z3.literal("task-running"),
sourceId: z3.string()
});
var InsertedTransactionSummarySchema = z3.object({
id: z3.string(),
date: z3.string().nullable().optional(),
description: z3.string().nullable().optional(),
amount: z3.string().nullable().optional(),
account: z3.string().nullable().optional()
});
var ChangedFieldSchema = z3.object({
field: z3.string(),
oldValue: z3.string().nullable().optional(),
newValue: z3.string().nullable().optional()
});
var ChangedTransactionSchema = z3.object({
id: z3.string(),
changedFields: z3.array(ChangedFieldSchema)
});
var TaskDoneSchema = z3.object({
type: z3.literal("task-done"),
sourceId: z3.string(),
inserted: z3.number(),
skipped: z3.number(),
insertedIds: z3.array(z3.string()),
insertedTransactions: z3.array(InsertedTransactionSummarySchema).optional(),
changedTransactions: z3.array(ChangedTransactionSchema).optional()
});
var TaskErrorSchema = z3.object({
type: z3.literal("task-error"),
sourceId: z3.string(),
message: z3.string(),
stack: z3.string().optional()
});
var TaskBlockedSchema = z3.object({
type: z3.literal("task-blocked"),
sourceId: z3.string(),
sourceType: z3.enum(SOURCE_TYPES),
unknownAccounts: z3.array(z3.string())
});
var OtpRequiredSchema = z3.object({
type: z3.literal("otp-required"),
sourceId: z3.string(),
prompt: z3.string().optional()
});
var RunCompleteSchema = z3.object({
type: z3.literal("run-complete"),
totalInserted: z3.number(),
totalSkipped: z3.number(),
errors: z3.number()
});
var RunErrorSchema = z3.object({
type: z3.literal("run-error"),
message: z3.string()
});
var TaskMonthFetchingSchema = z3.object({
type: z3.literal("task-month-fetching"),
sourceId: z3.string(),
month: z3.string()
});
var TaskMonthFetchedSchema = z3.object({
type: z3.literal("task-month-fetched"),
sourceId: z3.string(),
month: z3.string(),
transactionCount: z3.number()
});
var TaskMonthErrorSchema = z3.object({
type: z3.literal("task-month-error"),
sourceId: z3.string(),
month: z3.string(),
error: z3.string()
});
var TaskMonthUploadingSchema = z3.object({
type: z3.literal("task-month-uploading"),
sourceId: z3.string(),
month: z3.string(),
transactionCount: z3.number()
});
var TaskMonthUploadedSchema = z3.object({
type: z3.literal("task-month-uploaded"),
sourceId: z3.string(),
month: z3.string(),
inserted: z3.number(),
skipped: z3.number()
});
var TaskAccountsFoundSchema = z3.object({
type: z3.literal("task-accounts-found"),
sourceId: z3.string(),
accounts: z3.array(
z3.object({
accountNumber: z3.string(),
branchNumber: z3.number(),
bankNumber: z3.number()
})
)
});
var TaskAccountVaultCheckedSchema = z3.object({
type: z3.literal("task-account-vault-checked"),
sourceId: z3.string(),
accountId: z3.string(),
status: z3.enum(["accepted", "ignored", "blocked", "unknown"])
});
var TaskAccountTxnsFetchingSchema = z3.object({
type: z3.literal("task-account-txns-fetching"),
sourceId: z3.string(),
accountId: z3.string(),
txnType: z3.enum(["ils", "foreign", "swift"])
});
var TaskAccountTxnsUploadingSchema = z3.object({
type: z3.literal("task-account-txns-uploading"),
sourceId: z3.string(),
accountId: z3.string(),
txnType: z3.enum(["ils", "foreign", "swift"]),
count: z3.number()
});
var TaskAccountTxnsDoneSchema = z3.object({
type: z3.literal("task-account-txns-done"),
sourceId: z3.string(),
accountId: z3.string(),
txnType: z3.enum(["ils", "foreign", "swift"]),
inserted: z3.number(),
skipped: z3.number()
});
var ServerMessageSchema = z3.discriminatedUnion("type", [
ConnectedSchema,
PongSchema,
WsErrorSchema,
TaskPendingSchema,
TaskRunningSchema,
TaskDoneSchema,
TaskErrorSchema,
TaskBlockedSchema,
OtpRequiredSchema,
RunCompleteSchema,
RunErrorSchema,
TaskMonthFetchingSchema,
TaskMonthFetchedSchema,
TaskMonthErrorSchema,
TaskMonthUploadingSchema,
TaskMonthUploadedSchema,
TaskAccountsFoundSchema,
TaskAccountVaultCheckedSchema,
TaskAccountTxnsFetchingSchema,
TaskAccountTxnsUploadingSchema,
TaskAccountTxnsDoneSchema
]);
// src/server/account-discovery.ts
import { randomUUID as randomUUID2 } from "crypto";
// src/server/check-accounts.ts
function extractAccountIdentifiers(type, payload) {
switch (type) {
case "poalim": {
const p = payload;
if (!("accountNumber" in p) || !("branchNumber" in p)) {
return [];
}
return [`${p.branchNumber}-${p.accountNumber}`];
}
case "discount": {
const p = payload;
return [...new Set(p.map((entry) => entry.accountNumber))];
}
case "isracard":
case "amex": {
const p = payload;
return p.CardsTransactionsListBean.cardNumberList.map((c) => c.match(/\d{4}/)?.[0]).filter((c) => c !== void 0);
}
case "cal": {
const p = payload;
return [...new Set(p.map((entry) => entry.card))];
}
case "max": {
const p = payload;
return [...new Set(p.map((account) => account.accountNumber))];
}
case "otsar-hahayal": {
const p = payload;
return [...new Set(p.map((a) => `${a.branch}-${a.account}`))];
}
case "otsar-hahayal-credit-card": {
const p = payload;
return [...new Set(p.map(({ card }) => card.maskedPan))];
}
}
}
function checkAccounts(type, payload, known) {
const identifiers = extractAccountIdentifiers(type, payload);
const accepted = [];
const ignored = [];
const unknown = [];
for (const id of identifiers) {
const record = known.find((a) => a.sourceType === type && a.accountNumber === id);
if (!record) {
unknown.push(id);
} else if (record.status === "ignored") {
ignored.push(id);
} else if (record.status === "accepted") {
accepted.push(id);
} else {
unknown.push(id);
}
}
return { accepted, ignored, unknown };
}
// src/server/account-discovery.ts
async function registerDiscoveredAccounts(sourceType, sourceId, payloads) {
const existing = getVault().accountRecords;
const newRecords = [];
for (const payload of payloads) {
for (const accountNumber of extractAccountIdentifiers(sourceType, payload)) {
const alreadyKnown = existing.some(
(r) => r.sourceType === sourceType && r.accountNumber === accountNumber
);
if (!alreadyKnown && !newRecords.some(
(r) => r.sourceType === sourceType && r.sourceId === sourceId && r.accountNumber === accountNumber
)) {
newRecords.push({
id: randomUUID2(),
sourceId,
sourceType,
accountNumber,
status: "pending"
});
}
}
}
if (newRecords.length > 0) {
await updateVault((v) => ({
...v,
accountRecords: [...v.accountRecords, ...newRecords]
}));
}
}
// src/server/filter-payload.ts
function effectiveSet(accepted, ignored, all) {
const base = accepted?.length ? accepted : all;
const ignoredSet = new Set(ignored ?? []);
return new Set(base.filter((c) => !ignoredSet.has(c)));
}
function filterIsracardAmex(payload, accepted, ignored) {
const bean = payload.CardsTransactionsListBean;
const allCards = bean.cardNumberList.map((c) => c.match(/\d{4}/)?.[0]);
const allowed = effectiveSet(accepted, ignored, allCards.filter(Boolean));
const filteredBean = {};
for (const [key, val] of Object.entries(bean)) {
if (/^Index\d+$/.test(key)) {
const card = allCards[Number(key.slice(5))];
if (!card || !allowed.has(card)) {
continue;
}
}
filteredBean[key] = val;
}
return {
...payload,
CardsTransactionsListBean: filteredBean
};
}
function filterPayload(type, payload, creds) {
switch (type) {
case "isracard":
case "amex": {
const p = payload;
const opts = creds.options;
return filterIsracardAmex(
p,
opts?.acceptedCardNumbers,
opts?.ignoredCardNumbers
);
}
case "cal": {
const p = payload;
const opts = creds.options;
const allCards = [...new Set(p.map((e) => e.card))];
const allowed = effectiveSet(opts?.acceptedCardNumbers, opts?.ignoredCardNumbers, allCards);
return p.filter((e) => allowed.has(e.card));
}
case "max": {
const p = payload;
const opts = creds.options;
const allAccounts = [...new Set(p.map((e) => e.accountNumber))];
const allowed = effectiveSet(
opts?.acceptedCardNumbers,
opts?.ignoredCardNumbers,
allAccounts
);
return p.filter((e) => allowed.has(e.accountNumber));
}
case "poalim": {
const opts = creds.options;
if ("retrievalTransactionData" in payload) {
const p = payload;
const { accountNumber, branchNumber } = p.retrievalTransactionData;
const allAccounts = [String(accountNumber)];
const allBranches = [String(branchNumber)];
const allowedAccounts = effectiveSet(
opts?.acceptedAccountNumbers,
opts?.ignoredAccountNumbers,
allAccounts
);
const allowedBranches = effectiveSet(
opts?.acceptedBranchNumbers,
opts?.ignoredBranchNumbers,
allBranches
);
if (!allowedAccounts.has(String(accountNumber)) || !allowedBranches.has(String(branchNumber))) {
return { ...p, transactions: [] };
}
return p;
}
if ("balancesAndLimitsDataList" in payload) {
return payload;
}
if ("swiftsList" in payload) {
return payload;
}
return payload;
}
case "discount": {
const p = payload;
return p;
}
}
}
// src/server/graphql/client.ts
import { GraphQLClient } from "graphql-request";
// src/server/utils.ts
function convertNumberDateToString(numberDate) {
if (numberDate < 1e7 || numberDate > 99999999) {
throw new Error("Invalid number date");
}
const stringDate = numberDate.toString();
const year = stringDate.substring(0, 4);
const month = stringDate.substring(4, 6);
const day = stringDate.substring(6, 8);
return `${year}-${month}-${day}`;
}
function convertPoalimCurrencyCodeToSymbol(code) {
switch (code) {
case 19:
return "USD";
case 27:
return "GBP";
case 36:
return "AUD";
case 51:
return "SEK";
case 100:
return "EUR";
case 140:
return "CAD";
case 248:
return "JPY";
default:
throw new Error(`New Poalim account currency - ${code}`);
}
}
function getPoalimSwiftCodeExtensions(swiftTransferDetailsList, index) {
if (index >= swiftTransferDetailsList.length) {
return "";
}
if (swiftTransferDetailsList[index].swiftTransferAttributeCode !== null) {
return "";
}
const value = swiftTransferDetailsList[index].swiftTransferAttributeValue;
return `
${value}${getPoalimSwiftCodeExtensions(swiftTransferDetailsList, index + 1)}`;
}
function findPoalimSwiftElement(transaction, attribute, defaultEmptyString = false) {
const emptyValue = defaultEmptyString ? " " : null;
const index = transaction.swiftTransferDetailsList.findIndex(
(element) => element.swiftTransferAttributeCode === attribute
);
if (index === -1) {
return emptyValue;
}
let value = transaction.swiftTransferDetailsList[index].swiftTransferAttributeValue;
if (value === null || value === void 0) {
return emptyValue;
}
value += getPoalimSwiftCodeExtensions(transaction.swiftTransferDetailsList, index + 1);
return value;
}
// src/server/graphql/mutations.ts
var UPLOAD_POALIM_ILS = (
/* GraphQL */
`
mutation UploadPoalimIlsTransactions($transactions: [PoalimIlsTransactionInput!]!) {
uploadPoalimIlsTransactions(transactions: $transactions) {
inserted
skipped
insertedIds
insertedTransactions {
id
date
description
amount
account
}
changedTransactions {
id
changedFields {
field
oldValue
newValue
}
}
}
}
`
);
var UPLOAD_POALIM_FOREIGN = (
/* GraphQL */
`
mutation UploadPoalimForeignTransactions($transactions: [PoalimForeignTransactionInput!]!) {
uploadPoalimForeignTransactions(transactions: $transactions) {
inserted
skipped
insertedIds
insertedTransactions {
id
date
description
amount
account
}
changedTransactions {
id
changedFields {
field
oldValue
newValue
}
}
}
}
`
);
var UPLOAD_POALIM_SWIFT = (
/* GraphQL */
`
mutation UploadPoalimSwiftTransactions($swifts: [PoalimSwiftTransactionInput!]!) {
uploadPoalimSwiftTransactions(swifts: $swifts) {
inserted
skipped
insertedIds
insertedTransactions {
id
date
description
amount
account
}
changedTransactions {
id
changedFields {
field
oldValue
newValue
}
}
}
}
`
);
var UPLOAD_ISRACARD = (
/* GraphQL */
`
mutation UploadIsracardTransactions($transactions: [IsracardTransactionInput!]!) {
uploadIsracardTransactions(transactions: $transactions) {
inserted
skipped
insertedIds
insertedTransactions {
id
date
description
amount
account
}
changedTransactions {
id
changedFields {
field
oldValue
newValue
}
}
}
}
`
);
var UPLOAD_AMEX = (
/* GraphQL */
`
mutation UploadAmexTransactions($transactions: [AmexTransactionInput!]!) {
uploadAmexTransactions(transactions: $transactions) {
inserted
skipped
insertedIds
insertedTransactions {
id
date
description
amount
account
}
changedTransactions {
id
changedFields {
field
oldValue
newValue
}
}
}
}
`
);
var UPLOAD_CAL = (
/* GraphQL */
`
mutation UploadCalTransactions($transactions: [CalTransactionInput!]!) {
uploadCalTransactions(transactions: $transactions) {
inserted
skipped
insertedIds
insertedTransactions {
id
date
description
amount
account
}
changedTransactions {
id
changedFields {
field
oldValue
newValue
}
}
}
}
`
);
var UPLOAD_DISCOUNT = (
/* GraphQL */
`
mutation UploadDiscountTransactions($transactions: [DiscountTransactionInput!]!) {
uploadDiscountTransactions(transactions: $transactions) {
inserted
skipped
insertedIds
insertedTransactions {
id
date
description
amount
account
}
changedTransactions {
id
changedFields {
field
oldValue
newValue
}
}
}
}
`
);
var UPLOAD_MAX = (
/* GraphQL */
`
mutation UploadMaxTransactions($transactions: [MaxTransactionInput!]!) {
uploadMaxTransactions(transactions: $transactions) {
inserted
skipped
insertedIds
insertedTransactions {
id
date
description
amount
account
}
changedTransactions {
id
changedFields {
field
oldValue
newValue
}
}
}
}
`
);
var UPLOAD_CURRENCY_RATES = (
/* GraphQL */
`
mutation UploadCurrencyRates($rates: [CurrencyRateInput!]!) {
uploadCurrencyRates(rates: $rates) {
inserted
skipped
insertedIds
insertedTransactions {
id
date
description
amount
account
}
changedTransactions {
id
changedFields {
field
oldValue
newValue
}
}
}
}
`
);
var UPLOAD_OTSAR_ILS = (
/* GraphQL */
`
mutation UploadOtsarHahayalIlsTransactions($transactions: [OtsarHahayalIlsTransactionInput!]!) {
uploadOtsarHahayalIlsTransactions(transactions: $transactions) {
inserted
skipped
insertedIds
insertedTransactions {
id
date
description
amount
account
}
changedTransactions {
id
changedFields {
field
oldValue
newValue
}
}
}
}
`
);
var UPLOAD_OTSAR_FOREIGN = (
/* GraphQL */
`
mutation UploadOtsarHahayalForeignTransactions(
$transactions: [OtsarHahayalForeignTransactionInput!]!
) {
uploadOtsarHahayalForeignTransactions(transactions: $transactions) {
inserted
skipped
insertedIds
insertedTransactions {
id
date
description
amount
account
}
changedTransactions {
id
changedFields {
field
oldValue
newValue
}
}
}
}
`
);
var UPLOAD_OTSAR_CREDITCARD = (
/* GraphQL */
`
mutation UploadOtsarHahayalCreditCardTransactions(
$transactions: [OtsarHahayalCreditCardTransactionInput!]!
) {
uploadOtsarHahayalCreditCardTransactions(transactions: $transactions) {
inserted
skipped
insertedIds
insertedTransactions {
id
date
description
amount
account
}
changedTransactions {
id
changedFields {
field
oldValue
newValue
}
}
}
}
`
);
function poalimIlsVars(payload) {
const { accountNumber, branchNumber, bankNumber } = payload.retrievalTransactionData;
const transactions = payload.transactions.map((t) => {
const beneficiaryDetails = t.beneficiaryDetailsData ? {
beneficiaryDetailsData: null,
// InputMaybe<Scalars['String']['input']>; OPTIONAL
beneficiaryDetailsDataMessageDetail: t.beneficiaryDetailsData?.messageDetail,
// InputMaybe<Scalars['String']['input']>; OPTIONAL
beneficiaryDetailsDataMessageHeadline: t.beneficiaryDetailsData?.messageHeadline,
// InputMaybe<Scalars['String']['input']>; OPTIONAL
beneficiaryDetailsDataPartyHeadline: t.beneficiaryDetailsData?.partyHeadline,
// InputMaybe<Scalars['String']['input']>; OPTIONAL
beneficiaryDetailsDataPartyName: t.beneficiaryDetailsData?.partyName,
// InputMaybe<Scalars['String']['input']>; OPTIONAL
beneficiaryDetailsDataRecordNumber: t.beneficiaryDetailsData?.recordNumber,
// InputMaybe<Scalars['Int']['input']>; OPTIONAL
beneficiaryDetailsDataTableNumber: t.beneficiaryDetailsData?.tableNumber
// InputMaybe<Scalars['Int']['input']>; OPTIONAL
} : {};
return {
accountNumber,
activityDescription: t.activityDescription,
// Scalars['String']['input'];
activityDescriptionIncludeValueDate: t.activityDescriptionIncludeValueDate,
// InputMaybe<Scalars['String']['input']>; OPTIONAL
activityTypeCode: t.activityTypeCode,
// Scalars['Int']['input'];
bankNumber,
...beneficiaryDetails,
branchNumber,
comment: t.comment,
commentExistenceSwitch: t.commentExistenceSwitch !== 0,
contraAccountNumber: t.contraAccountNumber,
contraAccountTypeCode: t.contraAccountTypeCode,
contraBankNumber: t.contraBankNumber,
contraBranchNumber: t.contraBranchNumber,
currentBalance: t.currentBalance,
dataGroupCode: t.dataGroupCode,
details: t.details,
differentDateIndication: t.differentDateIndication,
englishActionDesc: t.englishActionDesc,
eventActivityTypeCode: t.eventActivityTypeCode,
eventAmount: t.eventAmount,
eventDate: convertNumberDateToString(t.eventDate),
eventId: String(t.eventId),
executingBranchNumber: t.executingBranchNumber,
expandedEventDate: t.expandedEventDate,
fieldDescDisplaySwitch: t.fieldDescDisplaySwitch === 1,
formattedEventDate: t.formattedEventDate,
formattedOriginalEventCreateDate: t.formattedOriginalEventCreateDate,
formattedValueDate: t.formattedValueDate,
internalLinkCode: t.internalLinkCode,
marketingOfferContext: t.marketingOfferContext !== 0,
offerActivityContext: t.offerActivityContext,
originalEventCreateDate: t.originalEventCreateDate,
pfmDetails: t.pfmDetails,
recordNumber: t.recordNumber,
referenceCatenatedNumber: t.referenceCatenatedNumber,
referenceNumber: String(t.referenceNumber),
rejectedDataEventPertainingIndication: t.rejectedDataEventPertainingIndication,
serialNumber: t.serialNumber,
tableNumber: t.tableNumber,
textCode: t.textCode,
transactionType: t.transactionType,
valueDate: convertNumberDateToString(t.valueDate)
};
});
return { transactions };
}
function poalimForeignVars(payload, account) {
const transactions = payload.balancesAndLimitsDataList.flatMap((entry) => {
const currency = convertPoalimCurrencyCodeToSymbol(entry.currencyCode);
const bankNumber = "bankNumber" in entry ? entry.bankNumber : account.bankNumber;
const branchNumber = "branchNumber" in entry ? entry.branchNumber : account.branchNumber;
const accountNumber = "accountNumber" in entry ? entry.accountNumber : account.accountNumber;
return entry.transactions.map((t) => {
const metadataAttributes = {};
if (t.metadata) {
if (t.metadata.attributes) {
metadataAttributes.metadataAttributesOriginalEventKey = JSON.stringify(
t.metadata.attributes.originalEventKey
);
metadataAttributes.metadataAttributesContraBranchNumber = JSON.stringify(
t.metadata.attributes.contraBranchNumber
);
metadataAttributes.metadataAttributesContraAccountNumber = JSON.stringify(
t.metadata.attributes.contraAccountNumber
);
metadataAttributes.metadataAttributesContraBankNumber = JSON.stringify(
t.metadata.attributes.contraBankNumber
);
metadataAttributes.metadataAttributesContraAccountFieldNameLable = JSON.stringify(
t.metadata.attributes.contraAccountFieldNameLable
);
metadataAttributes.metadataAttributesDataGroupCode = JSON.stringify(
t.metadata.attributes.dataGroupCode
);
metadataAttributes.metadataAttributesCurrencyRate = JSON.stringify(
t.metadata.attributes.currencyRate
);
metadataAttributes.metadataAttributesContraCurrencyCode = JSON.stringify(
t.metadata.attributes.contraCurrencyCode
);
metadataAttributes.metadataAttributesRateFixingCode = JSON.stringify(
t.metadata.attributes.rateFixingCode
);
} else {
metadataAttributes.metadataAttributesContraAccountFieldNameLable = null;
metadataAttributes.metadataAttributesContraAccountNumber = null;
metadataAttributes.metadataAttributesContraBankNumber = null;
metadataAttributes.metadataAttributesContraBranchNumber = null;
metadataAttributes.metadataAttributesContraCurrencyCode = null;
metadataAttributes.metadataAttributesCurrencyRate = null;
metadataAttributes.metadataAttributesDataGroupCode = null;
metadataAttributes.metadataAttributesOriginalEventKey = null;
metadataAttributes.metadataAttributesRateFixingCode = null;
}
}
return {
accountName: "accountName" in t ? t.accountName : null,
accountNumber,
activityDescription: t.activityDescription,
activityTypeCode: t.activityTypeCode,
bankNumber,
branchNumber,
commentExistenceSwitch: "commentExistenceSwitch" in t ? t.commentExistenceSwitch !== 0 : false,
comments: "comments" in t ? t.comments : null,
contraAccountFieldNameLable: "contraAccountFieldNameLable" in t ? t.contraAccountFieldNameLable : null,
contraAccountNumber: "contraAccountNumber" in t ? t.contraAccountNumber : 0,
contraBankNumber: "contraBankNumber" in t ? t.contraBankNumber : 0,
contraBranchNumber: "contraBranchNumber" in t ? t.contraBranchNumber : 0,
contraCurrencyCode: t.contraCurrencyCode,
currency,
currencyLongDescription: t.currencyLongDescription,
currencyRate: t.currencyRate,
currencySwiftCode: t.currencySwiftCode,
currentBalance: t.currentBalance,
dataGroupCode: "dataGroupCode" in t ? t.dataGroupCode !== 0 : false,
eventActivityTypeCode: t.eventActivityTypeCode,
eventAmount: t.eventAmount,
eventDetails: t.eventDetails,
eventNumber: t.eventNumber,
executingDate: convertNumberDateToString(t.executingDate),
formattedExecutingDate: t.formattedExecutingDate,
formattedValueDate: t.formattedValueDate,
...metadataAttributes,
originalEventKey: "originalEventKey" in t ? t.originalEventKey !== 0 : false,
originalSystemId: t.originalSystemId,
rateFixingCode: t.rateFixingCode,
rateFixingDescription: t.rateFixingDescription,
rateFixingShortDescription: t.rateFixingShortDescription,
referenceCatenatedNumber: t.referenceCatenatedNumber,
referenceNumber: String(t.referenceNumber),
transactionType: t.transactionType,
urlAddress: t.urlAddress,
validityDate: convertNumberDateToString(t.validityDate),
valueDate: convertNumberDateToString(t.valueDate)
};
});
});
return { transactions };
}
function poalimSwiftVars(payload, bankAccount) {
const swifts = payload.swiftsList.map((s) => ({
accountNumber: bankAccount.accountNumber,
branchNumber: bankAccount.branchNumber,
bankNumber: bankAccount.bankNumber,
startDate: s.startDate.toString(),
formattedStartDate: s.formattedStartDate,
swiftStatusCode: s.swiftStatusCode,
swiftStatusDesc: s.swiftStatusDesc,
amount: s.amount,
currencyCodeCatenatedKey: s.currencyCodeCatenatedKey,
currencyLongDescription: s.currencyLongDescription,
chargePartyName: s.chargePartyName,
referenceNumber: s.referenceNumber,
transferCatenatedId: s.transferCatenatedId,
dataOriginCode: s.dataOriginCode.toString(),
swiftIsnSerialNumber: s.details.swiftBankDetails.swiftIsnSerialNumber,
swiftBankCode: s.details.swiftBankDetails.swiftBankCode,
orderCustomerName: s.details.swiftBankDetails.orderCustomerName,
beneficiaryEnglishStreetName: s.details.swiftBankDetails.beneficiaryEnglishStreetName1,
beneficiaryEnglishCityName: s.details.swiftBankDetails.beneficiaryEnglishCityName1,
beneficiaryEnglishCountryName: s.details.swiftBankDetails.beneficiaryEnglishCountryName,
swiftSendersReference20: findPoalimSwiftElement(s.details, ":20:"),
swiftBankOperationCode23B: findPoalimSwiftElement(s.details, ":23B:"),
swiftInstructionCode23E: findPoalimSwiftElement(s.details, ":23E:"),
swiftValueDateCurrencyAmount32A: findPoalimSwiftElement(s.details, ":32A:"),
swiftCurrencyInstructedAmount33B: findPoalimSwiftElement(s.details, ":33B:", true),
swiftExchangeRate36: findPoalimSwiftElement(s.details, ":36:", true),
swiftOrderingCustomer50K: findPoalimSwiftElement(s.details, ":50K:") ?? findPoalimSwiftElement(s.details, ":50F:", true),
swiftOrderingInstitution52A: findPoalimSwiftElement(s.details, ":52A:", true),
swiftOrderingInstitution52D: findPoalimSwiftElement(s.details, ":52D:", true),
swiftSendersCorrespondent53A: findPoalimSwiftElement(s.details, ":53B:") ?? findPoalimSwiftElement(s.details, ":53A:", true),
swiftReceiversCorrespondent54A: findPoalimSwiftElement(s.details, ":54A:", true),
swiftAccountWithInstitution57: findPoalimSwiftElement(s.details, ":57A:") ?? findPoalimSwiftElement(s.details, ":57D:", true),
swiftBeneficiaryCustomer59: findPoalimSwiftElement(s.details, ":59:") ?? findPoalimSwiftElement(s.details, ":59F:", true),
swiftRemittanceInformation70: findPoalimSwiftElement(s.details, ":70:", true),
swiftDetailsOfCharges71A: findPoalimSwiftElement(s.details, ":71A:", true),
swiftSendersCharges71F: findPoalimSwiftElement(s.details, ":71F:", true),
swiftSendersToReceiverInformation72: findPoalimSwiftElement(s.details, ":72:", true),
swiftRegulatoryReporting77B: findPoalimSwiftElement(s.details, ":77B:", true)
}));
return { swifts };
}
function transformIsracardAmexTransaction(t, card) {
const inputTransaction = {
card,
specificDate: t.specificDate,
cardIndex: Number.parseInt(t.cardIndex, 10),
dealsInbound: t.dealsInbound,
supplierId: t.supplierId ? Number(t.supplierId) : null,
supplierName: t.supplierName,
dealSumType: t.dealSumType,
paymentSumSign: t.paymentSumSign,
purchaseDate: t.purchaseDate,
fullPurchaseDate: t.fullPurchaseDate,
moreInfo: t.moreInfo,
horaatKeva: t.horaatKeva,
voucherNumber: t.voucherNumber ? Number(t.voucherNumber) : null,
voucherNumberRatz: t.voucherNumberRatz ? Number(t.voucherNumberRatz) : null,
solek: t.solek,
purchaseDateOutbound: t.purchaseDateOutbound,
fullPurchaseDateOutbound: t.fullPurchaseDateOutbound,
currencyId: t.currencyId,
currentPaymentCurrency: t.currentPaymentCurrency,
city: t.city,
supplierNameOutbound: t.supplierNameOutbound,
fullSupplierNameOutbound: t.fullSupplierNameOutbound,
paymentDate: t.paymentDate,
fullPaymentDate: t.fullPaymentDate,
isShowDealsOutbound: t.isShowDealsOutbound,
adendum: t.adendum,
voucherNumberRatzOutbound: t.voucherNumberRatzOutbound ? Number(t.voucherNumberRatzOutbound) : null,
isShowLinkForSupplierDetails: t.isShowLinkForSupplierDetails,
dealSum: t.dealSum,
paymentSum: t.paymentSum,
fullSupplierNameHeb: t.fullSupplierNameHeb,
dealSumOutbound: t.dealSumOutbound,
paymentSumOutbound: t.paymentSumOutbound,
isHoraatKeva: t.isHoraatKeva,
stage: t.stage,
returnCode: t.returnCode,
message: t.message,
returnMessage: t.returnMessage,
displayProperties: t.displayProperties,
tablePageNum: t.tablePageNum === "0" ? false : true,
isError: t.isError,
isCaptcha: t.isCaptcha,
isButton: t.isButton,
siteName: t.siteName,
kodMatbeaMekori: t.kodMatbeaMekori ?? null,
esbServicesCall: t.EsbServicesCall ?? null
};
const optionalTransactionKeys = [
"clientIpAddress",
"bcKey",
"chargingDate",
"requestNumber",
"accountErrorCode",
"monthlyRefundCardIndex",
"id",
"EsbServicesCall"
// renamed to esbServicesCall above, to coerce to camelCase
];
for (const key of optionalTransactionKeys) {
if (inputTransaction[key] !== void 0) {
delete inputTransaction[key];
}
}
return inputTransaction;
}
function flattenIsracardAmexPayloads(payloads) {
return payloads.flatMap((p) => {
const cardNumbers = p.CardsTransactionsListBean.cardNumberList.map((c) => c.match(/\d{4}/)?.[0]);
return Object.keys(p.CardsTransactionsListBean).filter((k) => /^Index\d+$/.test(k)).flatMap((k) => {
const card = cardNumbers[Number(k.slice(5))];
if (!card) {
throw new Error(`Missing card number for ${k} in Isracard payload`);
}
const idx = p.CardsTransactionsListBean[k];
return idx.CurrentCardTransactions.flatMap((cardGroup) => {
const israelTxns = (cardGroup.txnIsrael ?? []).map(
(t) => transformIsracardAmexTransaction(t, card)
);
const abroadTxns = (cardGroup.txnAbroad ?? []).map(
(t) => transformIsracardAmexTransaction(t, card)
);
return [...israelTxns, ...abroadTxns];
});
});
});
}
function isracardVars(payloads) {
return { transactions: flattenIsracardAmexPayloads(payloads) };
}
function amexVars(payloads) {
return { transactions: flattenIsracardAmexPayloads(payloads) };
}
function calVars(payload) {
const transactions = payload.flatMap(
(entry) => entry.transactions.map((t) => ({
...t,
card: entry.card,
// Coerce numeric string fields
trnAmt: t.trnAmt == null ? null : String(t.trnAmt),
amtBeforeConvAndIndex: t.amtBeforeConvAndIndex == null ? null : String(t.amtBeforeConvAndIndex),
cashAccountTrnAmt: t.cashAccountTrnAmt == null ? null : String(t.cashAccountTrnAmt)
}))
);
return { transactions };
}
function discountVars(payload) {
const transactions = payload.flatMap(
(entry) => entry.transactions.map((t) => {
const raw = t;
return {
accountNumber: entry.accountNumber,
operationDate: raw["OperationDate"] ?? null,
valueDate: raw["ValueDate"] ?? null,
operationCode: raw["OperationCode"] ?? null,
operationDescription: raw["OperationDescription"] ?? null,
operationDescription2: raw["OperationDescription2"] ?? null,
operationDescription3: raw["OperationDescription3"] ?? null,
operationBranch: raw["OperationBranch"] ?? null,
operationBank: raw["OperationBank"] ?? null,
channel: raw["Channel"] ?? null,
channelName: raw["ChannelName"] ?? null,
checkNumber: raw["CheckNumber"] ?? null,
instituteCode: raw["InstituteCode"] ?? null,
operationAmount: raw["OperationAmount"] == null ? null : String(raw["OperationAmount"]),
balanceAfterOperation: raw["BalanceAfterOperation"] == null ? null : String(raw["BalanceAfterOperation"]),
operationNumber: raw["OperationNumber"] ?? null,
branchTreasuryNumber: raw["BranchTreasuryNumber"] ?? null,
urn: raw["Urn"] ?? null,
operationDetailsServiceName: raw["OperationDetailsServiceName"] ?? null,
commissionChannelCode: raw["CommissionChannelCode"] ?? null,
commissionChannelName: raw["CommissionChannelName"] ?? null,
commissionTypeName: raw["CommissionTypeName"] ?? null,
businessDayDate: raw["BusinessDayDate"] ?? null,
eventName: raw["EventName"] ?? null,
categoryCode: raw["CategoryCode"] ?? null,
categoryDescCode: raw["CategoryDescCode"] ?? null,
categoryDescription: raw["CategoryDescription"] ?? null,
operationDescriptionToDisplay: raw["OperationDescriptionToDisplay"] ?? null,
operationOrder: raw["OperationOrder"] ?? null,
isLastSeen: raw["IsLastSeen"] ?? null
};
})
);
return { transactions };
}
function maxVars(payload) {
const transactions = payload.flatMap(
(entry) => entry.txns.map((t) => ({
...t,
// Coerce numeric string fields
actualPaymentAmount: t.actualPaymentAmount == null ? null : String(t.actualPaymentAmount),
dealDataAmount: t.dealDataAmount == null ? null : String(t.dealDataAmount),
dealDataAmountIls: t.dealDataAmountIls == null ? null : String(t.dealDataAmountIls)
}))
);
return { transactions };
}
function otsarIlsVars(ilsData) {
const transactions = ilsData.flatMap(
({ account, accountType, transactions: txns }) => txns.map((t) => ({
accountNumber: Number(account.account),
accountType,
branchNumber: Number(account.branch),
actionCode: t.ActionCode,
bfbSource: t.bfbSource,
closingBalance: t.closingBalance,
correspondentAccount: t.CorrespondentAccount,
correspondentAccountType: t.CorrespondentAccountType,
correspondentBank: t.CorrespondentBank,
correspondentBranch: t.CorrespondentBranch,
creditAmount: t.creditAmount,
customerName: t.CustomerName,
dateOfBusinessDay: t.dateOfBusinessDay,
dateOfRegistration: t.dateOfRegistration,
debitAmount: t.debitAmount,
depositorId: t.DepositorId,
description: t.description,
drillDownUrl: t.drillDownUrl,
drillDownData: t.drillDownData == null ? void 0 : JSON.stringify(t.drillDownData),
firstTransactionOfDay: t.firstTransactionOfDay,
lastTransactionOfDay: t.lastTransactionOfDay,
name: t.Name,
openingBalance: t.openingBalance,
operationSource: t.OprationSource,
originReference: t.originReference ?? void 0,
reference: t.reference,
salaryInd: t.SalaryInd,
transactionReason: t.TransactionReason,
transactionSource: t.transactionSource
}))
);
return { transactions };
}
function otsarForeignVars(foreignData) {
const transactions = foreignData.flatMap(
({ metadata, transactions: txns }) => txns.map((t) => ({
account: metadata.account,
branch: metadata.branch,
accountType: metadata.accountType,
currency: metadata.currency,
openingBalance: metadata.openingBalance,
balance: t.balance ?? void 0,
valueDate: t.valueDate,
credit: t.credit,
debit: t.debit,
description: t.description,
sp: t.sp ?? void 0,
reference: t.reference,
date: t.date,
subTransactions: JSON.stringify(t.subTransactions)
}))
);
return { transactions };
}
function otsarCreditCardVars(creditCardData) {
const transactions = creditCardData.flatMap(
({ card, dealGroup, transactions: txns }) => txns.map((t) => ({
resourceId: card.resourceId,
maskedPan: card.maskedPan,
cardType: card.cardType,
dealGroup,
date: t.date,
chargeDate: t.chargeDate,
name: t.name,
dealAmount: t.dealAmount,
chargeAmount: t.chargeAmount,
notes: t.notes,
walletType: t.walletType,
chargeCurrency: t.chargeCurrency,
dealCurrency: t.dealCurrency,
counter: t.counter ?? 0
}))
);
return { transactions };
}
function currencyRatesVars(payload) {
const byDate = /* @__PURE__ */ new Map();
for (const entry of payload) {
const row = byDate.get(entry.date) ?? {};
row[entry.currency.toLowerCase()] = entry.rate;
byDate.set(entry.date, row);
}
const rates = Array.from(byDate.entries()).map(([exchangeDate, cols]) => ({
exchangeDate,
usd: cols["usd"],
eur: cols["eur"],
gbp: cols["gbp"],
cad: cols["cad"],
jpy: cols["jpy"],
aud: cols["aud"],
sek: cols["sek"]
}));
return { rates };
}
// src/server/graphql/client.ts
function extractResult(data, key) {
const result = data[key];
if (!result) throw new Error(`GraphQL response missing field: ${key}`);
return result;
}
function createUploadClient(serverUrl, apiKey) {
const gql = new GraphQLClient(serverUrl, {
headers: { "X-API-Key": apiKey }
});
async function request(doc, vars, key) {
const data = await gql.request(doc, vars);
return extractResult(data, key);
}
return {
async uploadPoalimIls(payload) {
return request(UPLOAD_POALIM_ILS, poalimIlsVars(payload), "uploadPoalimIlsTransactions");
},
async uploadPoalimForeign(payload, bankAccount) {
return request(
UPLOAD_POALIM_FOREIGN,
poalimForeignVars(payload, bankAccount),
"uploadPoalimForeignTransactions"
);
},
async uploadPoalimSwift(payload, bankAccount) {
return request(
UPLOAD_POALIM_SWIFT,
poalimSwiftVars(payload, bankAccount),
"uploadPoalimSwiftTransactions"
);
},
async uploadIsracard(payloads) {
return request(UPLOAD_ISRACARD, isracardVars(payloads), "uploadIsracardTransactions");
},
async uploadAmex(payloads) {
return request(UPLOAD_AMEX, amexVars(payloads), "uploadAmexTransactions");
},
async uploadCal(payload) {
return request(UPLOAD_CAL, calVars(payload), "uploadCalTransactions");
},
async uploadDiscount(payload) {
return request(UPLOAD_DISCOUNT, discountVars(payload), "uploadDiscountTransactions");
},
async uploadMax(payload) {
return request(UPLOAD_MAX, maxVars(payload), "uploadMaxTransactions");
},
async uploadCurrencyRates(payload) {
return request(UPLOAD_CURRENCY_RATES, currencyRatesVars(payload), "uploadCurrencyRates");
},
async uploadOtsarIls(ilsData) {
return request(UPLOAD_OTSAR_ILS, otsarIlsVars(ilsData), "uploadOtsarHahayalIlsTransactions");
},
async uploadOtsarForeign(foreignData) {
return request(
UPLOAD_OTSAR_FOREIGN,
otsarForeignVars(foreignData),
"uploadOtsarHahayalForeignTransactions"
);
},
async uploadOtsarCreditCard(creditCardData) {
return request(
UPLOAD_OTSAR_CREDITCARD,
otsarCreditCardVars(creditCardData),
"uploadOtsarHahayalCreditCardTransactions"
);
}
};
}
// src/server/otp-manager.ts
var OtpTimeoutError = class extends Error {
constructor(sourceId) {
super(`OTP timeout for source "${sourceId}"`);
this.sourceId = sourceId;
this.name = "OtpTimeoutError";
}
sourceId;
};
var OtpCancelledError = class extends Error {
constructor(sourceId) {
super(`OTP cancelled for source "${sourceId}"`);
this.sourceId = sourceId;
this.name = "OtpCancelledError";
}
sourceId;
};
var OtpManager = class {
pending = /* @__PURE__ */ new Map();
waitForOtp(sourceId, emit, timeoutMs = 12e4) {
this.cancelOtp(sourceId);
return new Promise((resolve, reject) => {
emit({ type: "otp-required", sourceId });
const timer = setTimeout(() => {
this.pending.delete(sourceId);
reject(new OtpTimeoutError(sourceId));
}, timeoutMs);
this.pending.set(sourceId, { resolve, reject, timer });
});
}
submitOtp(sourceId, otp) {
const entry = this.pending.get(sourceId);
if (!entry) return;
clearTimeout(entry.timer);
this.pending.delete(sourceId);
entry.resolve(otp);
}
cancelOtp(sourceId) {
const entry = this.pending.get(sourceId);
if (!entry) return;
clearTimeout(entry.timer);
this.pending.delete(sourceId);
entry.reject(new OtpCancelledError(sourceId));
}
hasPendingOtp(sourceId) {
return this.pending.has(sourceId);
}
};
// src/server/scrape-runner.ts
import { randomUUID as randomUUID3 } from "crypto";
var ERR_RUN_IN_PROGRESS = "Run already in progress";
var BlockedError = class extends Error {
blockedAccounts;
constructor(blockedAccounts) {
super("blocked");
this.name = "BlockedError";
this.blockedAccounts = blockedAccounts;
}
};
var _running = false;
async function startRun(tasks, concurrent, emit) {
if (_running) throw new Error(ERR_RUN_IN_PROGRESS);
_running = true;
const id = randomUUID3();
const startedAt = /* @__PURE__ */ new Date();
let errorCount = 0;
try {
for (const task of tasks) {
emit({ type: "task-pending", sourceId: task.sourceId });
}
const runTask = async (task) => {
emit({ type: "task-running", sourceId: task.sourceId });
try {
const result = await task.run();
emit({
type: "task-done",
sourceId: task.sourceId,
inserted: result.inserted,
skipped: result.skipped,
insertedIds: result.insertedIds,
...result.insertedTransactions != null && {
insertedTransactions: result.insertedTransactions
},
...result.changedTransactions != null && {
changedTransactions: result.changedTransactions
}
});
return { task, result, status: "done" };
} catch (e) {
if (e instanceof BlockedError) {
return {
task,
result: {
inserted: 0,
skipped: 0,
insertedIds: [],
insertedTransactions: [],
changedTransactions: []
},
status: "blocked",
blockedAccounts: e.blockedAccounts
};
}
errorCount++;
const message = e instanceof Error ? e.message : String(e);
const stack = e instanceof Error ? e.stack : void 0;
emit({ type: "task-error", sourceId: task.sourceId, message, ...stack && { stack } });
return {
task,
result: {
inserted: 0,
skipped: 0,
insertedIds: [],
insertedTransactions: [],
changedTransactions: []
},
status: "error",
error: message
};
}
};
let outcomes;
if (concurrent) {
outcomes = await Promise.all(tasks.map(runTask));
} else {
outcomes = [];
for (const task of tasks) {
outcomes.push(await runTask(task));
}
}
const totalInserted = outcomes.reduce((sum, o) => sum + o.result.inserted, 0);
const totalSkipped = outcomes.reduce((sum, o) => sum + o.result.skipped, 0);
const sources = outcomes.map((o) => ({
sourceId: o.task.sourceId,
nickname: o.task.nickname,
sourceType: o.task.type,
status: o.status,
inserted: o.result.inserted,
skipped: o.result.skipped,
...o.error != null && { error: o.error },
...o.blockedAccounts != null && { blockedAccounts: o.blockedAccounts }
}));
emit({ type: "run-complete", totalInserted, totalSkipped, errors: errorCount });
return {
id,
startedAt,
completedAt: /* @__PURE__ */ new Date(),
totalInserted,
totalSkipped,
errorCount,
sources
};
} finally {
_running = false;
}
}
// src/server/scrapers/amex.ts
import { addMonths as addMonths2, format as format2, startOfMonth as startOfMonth2 } from "date-fns";
import { init as init2 } from "@accounter/modern-poalim-scraper";
// src/server/payload-schemas/isracard.schema.ts
import { z as z4 } from "zod";
var TxnIsraelSchema = z4.object({
cardIndex: z4.string(),
supplierName: z4.string().nullable(),
dealSum: z4.string().nullable(),
fullPurchaseDate: z4.string().nullable(),
purchaseDate: z4.string().nullable(),
voucherNumber: z4.string().nullable(),
voucherNumberRatz: z4.string()
}).loose();
var TxnAbroadSchema = z4.object({
cardIndex: z4.string(),
fullSupplierNameOutbound: z4.string(),
dealSumOutbound: z4.string().nullable(),
fullPurchaseDateOutbound: z4.string().nullable(),
paymentSumOutbound: z4.string(),
voucherNumberRatzOutbound: z4.string()
}).loose();
var CurrentCardTransactionsSchema = z4.object({
txnIsrael: z4.array(TxnIsraelSchema).nullable().optional(),
txnAbroad: z4.array(TxnAbroadSchema).nullable().optional()
}).loose();
var IndexSchema = z4.object({
"@AllCards": z4.string(),
CurrentCardTransactions: z4.array(CurrentCardTransactionsSchema)
}).loose();
var HeaderSchema = z4.object({
Status: z4.string(),
Message: z4.string().nullable()
}).loose();
var CardsTransactionsListBeanSchema = z4.object({ cardNumberList: z4.array(z4.string()), Index0: IndexSchema }).catchall(z4.union([IndexSchema, z4.unknown()]));
var IsracardPayloadSchema = z4.object({
Header: HeaderSchema,
CardsTransactionsListBean: CardsTransactionsListBeanSchema
}).loose();
// src/server/payload-schemas/cal.schema.ts
import { z as z5 } from "zod";
var CalTransactionSchema = z5.object({
trnIntId: z5.string(),
merchantName: z5.string(),
trnPurchaseDate: z5.string(),
trnAmt: z5.number(),
trnCurrencySymbol: z5.string(),
trnType: z5.string(),
debCrdDate: z5.string(),
amtBeforeConvAndIndex: z5.number(),
debCrdCurrencySymbol: z5.string()
}).loose();
var CalMonthResultSchema = z5.object({
card: z5.string(),
month: z5.string(),
transactions: z5.array(CalTransactionSchema)
});
var CalPayloadSchema = z5.array(CalMonthResultSchema);
// src/server/payload-schemas/currency-rates.schema.ts
import { z as z6 } from "zod";
var CurrencyRateEntrySchema = z6.object({
date: z6.string(),
currency: z6.enum(["USD", "EUR", "GBP", "CAD", "JPY", "AUD", "SEK"]),
rate: z6.number()
});
var CurrencyRatesPayloadSchema = z6.array(CurrencyRateEntrySchema);
// src/server/payload-schemas/discount.schema.ts
import { z as z7 } from "zod";
var DiscountTransactionSchema = z7.object({
OperationDate: z7.string(),
ValueDate: z7.string(),
OperationCode: z7.string(),
OperationDescription: z7.string(),
OperationAmount: z7.number(),
BalanceAfterOperation: z7.number(),
OperationNumber: z7.number()
}).loose();
var DiscountMonthResultSchema = z7.object({
accountNumber: z7.string(),
month: z7.string(),
balance: z7.number(),
transactions: z7.array(DiscountTransactionSchema)
});
var DiscountPayloadSchema = z7.array(DiscountMonthResultSchema);
// src/server/payload-schemas/max.schema.ts
import { z as z8 } from "zod";
var MaxTransactionSchema = z8.object({
cardIndex: z8.number(),
categoryId: z8.number(),
merchantName: z8.string(),
originalAmount: z8.number(),
originalCurrency: z8.string(),
purchaseDate: z8.string(),
uid: z8.string(),
planName: z8.string(),
planTypeId: z8.number()
}).loose();
var MaxAccountSchema2 = z8.object({
accountNumber: z8.string(),
txns: z8.array(MaxTransactionSchema)
});
var MaxPayloadSchema = z8.array(MaxAccountSchema2);
// src/server/payload-schemas/poalim-foreign.schema.ts
import { z as z9 } from "zod";
var ForeignTransactionSchema = z9.object({
activityDescription: z9.string(),
activityTypeCode: z9.number(),
eventAmount: z9.number(),
currencySwiftCode: z9.string(),
currencyRate: z9.number(),
currentBalance: z9.number(),
referenceNumber: z9.number(),
transactionType: z9.string()
}).loose();
var BalancesAndLimitsItemSchema = z9.object({
currencySwiftCode: z9.string(),
currencyCode: z9.number(),
transactions: z9.array(ForeignTransactionSchema)
}).loose();
var PoalimForeignPayloadSchema = z9.object({
balancesAndLimitsDataList: z9.array(BalancesAndLimitsItemSchema)
}).loose();
// src/server/payload-schemas/poalim-ils.schema.ts
import { z as z10 } from "zod";
var IlsTransactionSchema = z10.object({
activityDescription: z10.string(),
activityTypeCode: z10.number(),
eventAmount: z10.number(),
eventDate: z10.number(),
serialNumber: z10.number(),
transactionType: z10.enum(["REGULAR", "TODAY", "FUTURE"]),
currentBalance: z10.number(),
referenceNumber: z10.number()
}).loose();
var RetrievalTransactionDataSchema = z10.object({
accountNumber: z10.number(),
branchNumber: z10.number(),
bankNumber: z10.number()
}).loose();
var PoalimIlsPayloadSchema = z10.object({
transactions: z10.array(IlsTransactionSchema),
retrievalTransactionData: RetrievalTransactionDataSchema
}).loose();
// src/server/payload-schemas/poalim-swift.schema.ts
import { z as z11 } from "zod";
var SwiftItemSchema = z11.object({
startDate: z11.number(),
swiftStatusCode: z11.string(),
amount: z11.number(),
currencyCodeCatenatedKey: z11.string(),
chargePartyName: z11.string(),
referenceNumber: z11.string(),
transferCatenatedId: z11.string()
}).loose();
var PoalimSwiftPayloadSchema = z11.object({
swiftsList: z11.array(SwiftItemSchema)
}).loose();
// src/server/validate-payload.ts
var PayloadValidationError = class extends Error {
constructor(payloadType, zodError) {
super(`[${payloadType}] payload validation failed: ${zodError.message}`);
this.payloadType = payloadType;
this.zodError = zodError;
this.name = "PayloadValidationError";
this.cause = zodError;
}
payloadType;
zodError;
};
var schemas = {
"poalim-ils": PoalimIlsPayloadSchema,
"poalim-foreign": PoalimForeignPayloadSchema,
"poalim-swift": PoalimSwiftPayloadSchema,
isracard: IsracardPayloadSchema,
amex: IsracardPayloadSchema,
cal: CalPayloadSchema,
discount: DiscountPayloadSchema,
max: MaxPayloadSchema,
"currency-rates": CurrencyRatesPayloadSchema
};
function validatePayload(type, raw) {
const schema = schemas[type];
const result = schema.safeParse(raw);
if (!result.success) {
throw new PayloadValidationError(type, result.error);
}
return result.data;
}
// src/server/scrapers/isracard.ts
import { addMonths, format, startOfMonth } from "date-fns";
import { init } from "@accounter/modern-poalim-scraper";
function buildMonthList(dateFrom, dateTo) {
const months = [];
let current = startOfMonth(dateFrom);
const end = startOfMonth(addMonths(dateTo, 1));
while (current <= end) {
months.push(current);
current = addMonths(current, 1);
}
return months;
}
function countIsracardTransactions(data) {
const bean = data.CardsTransactionsListBean;
if (!bean) return 0;
return Object.values(bean).reduce((sum, entry) => {
if (entry && typeof entry === "object" && !Array.isArray(entry) && "@AllCards" in entry && "CurrentCardTransactions" in entry) {
let txnCount = 0;
for (const subEntry of Array.isArray(entry.CurrentCardTransactions) ? entry.CurrentCardTransactions : []) {
if (!subEntry || typeof subEntry !== "object" || Array.isArray(subEntry)) {
continue;
}
if ("txnIsrael" in subEntry) {
txnCount += subEntry.txnIsrael?.length ?? 0;
} else if ("txnAbroad" in subEntry) {
txnCount += subEntry.txnAbroad?.length ?? 0;
}
}
return sum + txnCount;
}
return sum;
}, 0);
}
async function scrapeIsracard(creds, dateFrom, dateTo, emit, headless = true) {
const { isracard: isracardFn, close } = await init({ headless });
try {
const scraper = await isracardFn(
{ ID: creds.ownerId, password: creds.password, card6Digits: creds.last6Digits },
{ validateSchema: true }
);
const months = buildMonthList(dateFrom, dateTo);
const results = [];
for (const month of months) {
await new Promise((resolve) => setTimeout(resolve, 2e3 + Math.random() * 3e3));
const monthStr = format(month, "yyyy-MM");
emit({ type: "task-month-fetching", sourceId: creds.id, month: monthStr });
try {
const { data, isValid } = await scraper.getMonthTransactions(month);
if (!data) continue;
if (!isValid) {
throw new Error(`Invalid Isracard data for ${monthStr}`);
}
if (data.Header?.Status !== "1") {
throw new Error(
`Isracard login/password issue (Header.Status=${data.Header?.Status}) for ${monthStr}`
);
}
const validated = validatePayload("isracard", data);
const txnCount = countIsracardTransactions(validated);
emit({
type: "task-month-fetched",
sourceId: creds.id,
month: monthStr,
transactionCount: txnCount
});
results.push({ month: monthStr, data: validated });
} catch (err) {
emit({
type: "task-month-error",
sourceId: creds.id,
month: monthStr,
error: err instanceof Error ? err.message : String(err)
});
}
}
if (results.length === 0) throw new Error("All months failed to scrape");
return results;
} finally {
await close();
}
}
// src/server/scrapers/amex.ts
function buildMonthList2(dateFrom, dateTo) {
const months = [];
let current = startOfMonth2(dateFrom);
const end = startOfMonth2(dateTo);
while (current <= end) {
months.push(current);
current = addMonths2(current, 1);
}
return months;
}
async function scrapeAmex(creds, dateFrom, dateTo, emit, headless = true) {
const { amex: amexFn, close } = await init2({ headless });
try {
const scraper = await amexFn(
{ ID: creds.ownerId, password: creds.password, card6Digits: creds.last6Digits },
{ validateSchema: true }
);
const months = buildMonthList2(dateFrom, dateTo);
const results = [];
for (const month of months) {
await new Promise((resolve) => setTimeout(resolve, 2e3 + Math.random() * 3e3));
const monthStr = format2(month, "yyyy-MM");
emit({ type: "task-month-fetching", sourceId: creds.id, month: monthStr });
try {
const { data, isValid } = await scraper.getMonthTransactions(month);
if (!data) continue;
if (!isValid) {
throw new Error(`Invalid Amex data for ${monthStr}`);
}
if (data.Header?.Status !== "1") {
throw new Error(
`Amex login/password issue (Header.Status=${data.Header?.Status}) for ${monthStr}`
);
}
const validated = validatePayload("amex", data);
const txnCount = countIsracardTransactions(validated);
emit({
type: "task-month-fetched",
sourceId: creds.id,
month: monthStr,
transactionCount: txnCount
});
results.push({ month: monthStr, data: validated });
} catch (err) {
emit({
type: "task-month-error",
sourceId: creds.id,
month: monthStr,
error: err instanceof Error ? err.message : String(err)
});
}
}
if (results.length === 0) throw new Error("All months failed to scrape");
return results;
} finally {
await close();
}
}
// src/server/scrapers/cal.ts
import { addMonths as addMonths3, format as format3, startOfMonth as startOfMonth3 } from "date-fns";
import { init as init3 } from "@accounter/modern-poalim-scraper";
function buildMonthList3(dateFrom, dateTo) {
const months = [];
let current = startOfMonth3(dateFrom);
const end = startOfMonth3(dateTo);
while (current <= end) {
months.push(current);
current = addMonths3(current, 1);
}
return months;
}
async function scrapeCal(creds, dateFrom, dateTo, emit, headless = true) {
const { cal: calFn, close } = await init3({ headless });
try {
const scraper = await calFn({
username: creds.username,
password: creds.password,
last4Digits: creds.last4Digits
});
const months = buildMonthList3(dateFrom, dateTo);
const results = [];
for (const month of months) {
await new Promise((resolve) => setTimeout(resolve, 2e3 + Math.random() * 3e3));
const monthStr = format3(month, "yyyy-MM");
emit({ type: "task-month-fetching", sourceId: creds.id, month: monthStr });
try {
const transactions = await scraper.getMonthTransactions(creds.last4Digits, month);
emit({
type: "task-month-fetched",
sourceId: creds.id,
month: monthStr,
transactionCount: transactions.length
});
results.push({ card: creds.last4Digits, month: monthStr, transactions });
} catch (err) {
emit({
type: "task-month-error",
sourceId: creds.id,
month: monthStr,
error: err instanceof Error ? err.message : String(err)
});
}
}
return validatePayload("cal", results);
} finally {
await close();
}
}
// src/server/scrapers/currency-rates.ts
import { format as format4 } from "date-fns";
import { XMLParser } from "fast-xml-parser";
var BOI_URL = "https://edge.boi.gov.il/FusionEdgeServer/sdmx/v2/data/dataflow/BOI.STATISTICS/EXR/1.0/RER_USD_ILS,RER_EUR_ILS,RER_GBP_ILS,RER_CAD_ILS,RER_JPY_ILS,RER_AUD_ILS,RER_SEK_ILS";
var CURRENCIES = ["USD", "EUR", "GBP", "CAD", "JPY", "AUD", "SEK"];
async function scrapeCurrencyRates(emit, dateFrom, dateTo) {
const res = await fetch(BOI_URL);
if (!res.ok) throw new Error(`BOI fetch failed: ${res.status} ${res.statusText}`);
const xml = await res.text();
const parser = new XMLParser({
ignoreAttributes: false,
isArray: (name) => name === "Series" || name === "Obs",
removeNSPrefix: true
});
const parsed = parser.parse(xml);
const series = parsed?.StructureSpecificData?.DataSet?.Series ?? [];
if (series.length === 0) {
return validatePayload("currency-rates", []);
}
const today = format4(/* @__PURE__ */ new Date(), "yyyy-MM-dd");
const rateMap = /* @__PURE__ */ new Map();
for (const currencyData of series) {
const cd = currencyData;
const currency = cd["@_BASE_CURRENCY"];
if (!CURRENCIES.includes(currency)) continue;
const obs = Array.isArray(cd["Obs"]) ? cd["Obs"] : [];
for (const entry of obs) {
const e = entry;
const date = e["@_TIME_PERIOD"];
const value = Number(e["@_OBS_VALUE"]);
if (!date || Number.isNaN(value) || date >= today || date < format4(dateFrom, "yyyy-MM-dd") || date > format4(dateTo, "yyyy-MM-dd")) {
continue;
}
const rate = currency === "JPY" ? value * 0.01 : value;
if (!rateMap.has(date)) rateMap.set(date, /* @__PURE__ */ new Map());
rateMap.get(date).set(currency, rate);
}
}
const entries = [];
for (const [date, currencyRates] of rateMap) {
for (const [currency, rate] of currencyRates) {
entries.push({ date, currency, rate });
}
}
const validated = validatePayload("currency-rates", entries);
emit({
type: "task-month-fetched",
sourceId: "currency-rates",
month: "rates",
transactionCount: validated.length
});
return validated;
}
// src/server/scrapers/discount.ts
import { addMonths as addMonths4, format as format5, startOfMonth as startOfMonth4 } from "date-fns";
import { init as init4 } from "@accounter/modern-poalim-scraper";
function buildMonthList4(dateFrom, dateTo) {
const months = [];
let current = startOfMonth4(dateFrom);
const end = startOfMonth4(dateTo);
while (current <= end) {
months.push(current);
current = addMonths4(current, 1);
}
return months;
}
async function scrapeDiscount(creds, dateFrom, dateTo, emit, headless = true) {
const { discount: discountFn, close } = await init4({ headless });
try {
const scraper = await discountFn({
ID: creds.ID,
password: creds.password,
...creds.code ? { code: creds.code } : {}
});
const months = buildMonthList4(dateFrom, dateTo);
const results = [];
for (const month of months) {
await new Promise((resolve) => setTimeout(resolve, 2e3 + Math.random() * 3e3));
const monthStr = format5(month, "yyyy-MM");
emit({ type: "task-month-fetching", sourceId: creds.id, month: monthStr });
try {
const { accountNumber, balance, transactions } = await scraper.getMonthTransactions(month);
emit({
type: "task-month-fetched",
sourceId: creds.id,
month: monthStr,
transactionCount: transactions.length
});
results.push({ accountNumber, month: monthStr, balance, transactions });
} catch (err) {
emit({
type: "task-month-error",
sourceId: creds.id,
month: monthStr,
error: err instanceof Error ? err.message : String(err)
});
}
}
return validatePayload("discount", results);
} finally {
await close();
}
}
// src/server/scrapers/max.ts
import { init as init5 } from "@accounter/modern-poalim-scraper";
async function scrapeMax(creds, dateFrom, _dateTo, emit, headless = true) {
const { max: maxFn, close } = await init5({ headless });
try {
const scraper = await maxFn(
{ username: creds.username, password: creds.password },
{ startDate: dateFrom }
);
const accounts = await scraper.getTransactions();
const validated = validatePayload("max", accounts);
for (const account of validated) {
emit({
type: "task-month-fetched",
sourceId: creds.id,
month: account.accountNumber,
transactionCount: account.txns.length
});
}
return validated;
} finally {
await close();
}
}
// src/server/scrapers/otsar-hahayal.ts
import { init as init6 } from "@accounter/modern-poalim-scraper";
var DEAL_GROUPS = [
"localDeals",
"euroDeals",
"dollarDeals",
"shekelMatahDeals",
"localCurrentDebitDeals",
"euroCurrentDebitDeals",
"dollarCurrentDebitDeals",
"shekelMatahCurrentDebitDeals"
];
function monthsBetween(from, to) {
const months = [];
const cursor = new Date(from.getFullYear(), from.getMonth(), 1);
const end = new Date(to.getFullYear(), to.getMonth(), 1);
while (cursor <= end) {
const yyyy = cursor.getFullYear();
const mm = String(cursor.getMonth() + 1).padStart(2, "0");
months.push(`${yyyy}-${mm}-01`);
cursor.setMonth(cursor.getMonth() + 1);
}
return months;
}
async function scrapeOtsarHahayal(creds, dateFrom, dateTo, headless, emit) {
const { otsarHahayal, close } = await init6({ headless });
const scraper = await otsarHahayal(
{ userCode: creds.userCode, password: creds.password },
{
fromDate: dateFrom.toISOString(),
toDate: dateTo.toISOString(),
headless
}
);
try {
const { data: accounts } = await scraper.getAccounts();
if (!accounts || accounts.length === 0) {
return { ilsData: [], foreignData: [], creditCardData: [] };
}
emit({
type: "task-accounts-found",
sourceId: creds.id,
accounts: accounts.map((a) => ({
accountNumber: a.account,
branchNumber: Number(a.branch),
bankNumber: Number(a.bank)
}))
});
const ilsData = [];
for (const account of accounts) {
const accountId = `${account.branch}-${account.account}`;
emit({
type: "task-account-txns-fetching",
sourceId: creds.id,
accountId,
txnType: "ils"
});
const { data: ilsResponse } = await scraper.ilsTransactions({
accountNumber: Number(account.account),
branch: account.branch
});
if (ilsResponse?.transactions?.length) {
ilsData.push({
account,
accountType: ilsResponse.accountType,
transactions: ilsResponse.transactions.filter(
(t) => t.bfbSource !== "23" && t.bfbSource !== 23
)
});
}
}
const foreignData = await scraper.foreignTransactions();
for (const foreignAccount of foreignData) {
const accountId = `${foreignAccount.metadata.branch}-${foreignAccount.metadata.account}-${foreignAccount.metadata.subAccount}`;
emit({
type: "task-account-txns-fetching",
sourceId: creds.id,
accountId,
txnType: "foreign"
});
}
const creditCardData = [];
const { data: creditCardsResponse } = await scraper.getCreditCards();
if (creditCardsResponse?.cards?.length) {
const billingMonths = monthsBetween(dateFrom, dateTo);
for (const card of creditCardsResponse.cards) {
for (const month of billingMonths) {
const { data: billingPeriod } = await scraper.getCreditCardTransactions(
{ resourceId: card.resourceId, cardType: card.cardType, debitDay: card.debitDay },
month
);
if (!billingPeriod) continue;
for (const group of DEAL_GROUPS) {
const dealGroup = billingPeriod[group];
if (dealGroup?.deals?.length) {
creditCardData.push({
card,
dealGroup: group,
transactions: dealGroup.deals
});
}
}
}
}
}
return { ilsData, foreignData, creditCardData };
} finally {
await scraper.close();
await close();
}
}
// src/server/scrapers/poalim.ts
import {
init as init7
} from "@accounter/modern-poalim-scraper";
var OTP_TIMEOUT_MS = 5 * 60 * 1e3;
function describeValidationError(errors) {
if (errors == null) return "unknown validation error";
if (typeof errors === "string") return errors;
if (errors instanceof Error) {
if ("issues" in errors && Array.isArray(errors.issues)) {
return describeValidationError(errors.issues);
}
return errors.message;
}
if (Array.isArray(errors)) {
return errors.map((err) => {
if (err && typeof err === "object" && "message" in err) {
const pathStr = "path" in err && Array.isArray(err.path) && err.path.length > 0 ? ` at ${err.path.join(".")}` : "";
return `${err.message}${pathStr}`;
}
return String(err);
}).join(", ");
}
try {
return JSON.stringify(errors);
} catch {
return String(errors);
}
}
async function scrapePoalim(creds, dateFrom, dateTo, headless, otpManager, emit) {
const otpCallback = () => otpManager.waitForOtp(creds.id, emit, OTP_TIMEOUT_MS);
const { hapoalim: hapoalimFn, close } = await init7({ headless });
const months = Math.max(
1,
(dateTo.getFullYear() - dateFrom.getFullYear()) * 12 + (dateTo.getMonth() - dateFrom.getMonth()) + 1
);
const options = {
isBusiness: creds.options?.isBusinessAccount ?? false,
otpCallback,
duration: months,
validateSchema: true
};
try {
const scraper = await hapoalimFn(
{ userCode: creds.userCode, password: creds.password },
options
);
if (scraper === "Unknown Error") {
throw new Error("Hapoalim login failed: Unknown Error");
}
const {
data: accounts,
isValid: accountsValid,
errors: accountsErrors
} = await scraper.getAccountsData();
if (accountsValid === false) {
throw new Error(
`Poalim accounts data failed schema validation: ${describeValidationError(accountsErrors)}`
);
}
if (!accounts || accounts.length === 0) {
return [];
}
emit({
type: "task-accounts-found",
sourceId: creds.id,
accounts: accounts.map((a) => ({
accountNumber: String(a.accountNumber),
branchNumber: a.branchNumber,
bankNumber: a.bankNumber
}))
});
const opts = creds.options;
const filteredAccounts = [];
for (const account of accounts) {
const { accountNumber, branchNumber } = account;
const allAccounts = [String(accountNumber)];
const allBranches = [String(branchNumber)];
const allowedAccounts = effectiveSet(
opts?.acceptedAccountNumbers,
opts?.ignoredAccountNumbers,
allAccounts
);
const allowedBranches = effectiveSet(
opts?.acceptedBranchNumbers,
opts?.ignoredBranchNumbers,
allBranches
);
if (!allowedAccounts.has(String(accountNumber)) || !allowedBranches.has(String(branchNumber))) {
continue;
}
filteredAccounts.push(account);
}
await registerDiscoveredAccounts("poalim", creds.id, filteredAccounts);
const currentAccountRecords = getVault().accountRecords;
const allUnknown = [];
for (const account of filteredAccounts) {
const check = checkAccounts("poalim", account, currentAccountRecords);
allUnknown.push(...check.unknown);
emit({
type: "task-account-vault-checked",
sourceId: creds.id,
accountId: `${account.branchNumber}-${account.accountNumber}`,
status: "accepted"
});
}
if (allUnknown.length > 0) {
emit({
type: "task-blocked",
sourceId: creds.id,
sourceType: "poalim",
unknownAccounts: [...new Set(allUnknown)]
});
throw new BlockedError([...new Set(allUnknown)]);
}
const accountsData = [];
const isBusiness = creds.options?.isBusinessAccount ?? false;
for (const account of filteredAccounts) {
let ils = null;
let foreign = null;
let swift = null;
const accountId = `${account.branchNumber}-${account.accountNumber}`;
const accountRef = {
bankNumber: account.bankNumber,
branchNumber: account.branchNumber,
accountNumber: account.accountNumber
};
emit({ type: "task-account-txns-fetching", sourceId: creds.id, accountId, txnType: "ils" });
const {
data: ilsData,
isValid: ilsValid,
errors: ilsErrors
} = await scraper.getILSTransactions(accountRef);
if (ilsValid === false) {
throw new Error(
`Poalim ILS transactions failed schema validation for account ${accountId}: ${describeValidationError(ilsErrors)}`
);
}
if (ilsData) {
ils = validatePayload("poalim-ils", ilsData);
}
emit({
type: "task-account-txns-fetching",
sourceId: creds.id,
accountId,
txnType: "foreign"
});
const {
data: foreignData,
isValid: foreignValid,
errors: foreignErrors
} = await scraper.getForeignTransactions(accountRef, isBusiness);
if (foreignValid === false) {
throw new Error(
`Poalim foreign transactions failed schema validation for account ${accountId}: ${describeValidationError(foreignErrors)}`
);
}
if (foreignData) {
foreign = validatePayload("poalim-foreign", foreignData);
}
emit({ type: "task-account-txns-fetching", sourceId: creds.id, accountId, txnType: "swift" });
const {
data: swiftData,
isValid: swiftValid,
errors: swiftErrors
} = await scraper.getForeignSwiftTransactions(accountRef);
if (swiftValid === false) {
throw new Error(
`Poalim SWIFT transactions failed schema validation for account ${accountId}: ${describeValidationError(swiftErrors)}`
);
}
if (swiftData) {
const validated = validatePayload("poalim-swift", swiftData);
const decoratedSwiftsList = [];
for (const transaction of validated.swiftsList) {
const extendedTransaction = await scraper.getForeignSwiftTransaction(
accountRef,
transaction.transferCatenatedId,
transaction.dataOriginCode
);
if (!extendedTransaction.isValid || !extendedTransaction.data) {
throw new Error(
`Failed to fetch details for SWIFT transaction ${transaction.transferCatenatedId}`
);
}
decoratedSwiftsList.push({ ...transaction, details: extendedTransaction.data });
}
swift = { ...validated, swiftsList: decoratedSwiftsList };
}
accountsData.push({ ils, foreign, swift, bankAccount: accountRef });
}
return accountsData;
} catch (error) {
console.error("Error in scrapePoalim:", error instanceof Error ? error.stack : error);
throw error;
} finally {
await close();
}
}
// src/server/websocket.ts
function send(socket, msg) {
socket.send(JSON.stringify(msg));
}
function collectSourceRefs(vault) {
return [
...vault.poalimAccounts.map((a) => ({ id: a.id, type: "poalim" })),
...vault.discountAccounts.map((a) => ({ id: a.id, type: "discount" })),
...vault.isracardAccounts.map((a) => ({ id: a.id, type: "isracard" })),
...vault.amexAccounts.map((a) => ({ id: a.id, type: "amex" })),
...vault.calAccounts.map((a) => ({ id: a.id, type: "cal" })),
...vault.maxAccounts.map((a) => ({ id: a.id, type: "max" })),
...vault.otsarHahayalAccounts.map((a) => ({ id: a.id, type: "otsar-hahayal" }))
];
}
function buildTask(src, vault, emit, otpManager, uploadClient, dateFrom, dateTo) {
return {
sourceId: src.id,
nickname: src.id,
type: src.type,
run: async () => {
const headless = !vault.settings.showBrowser;
if (src.type === "otsar-hahayal") {
try {
const creds2 = vault.otsarHahayalAccounts.find((a) => a.id === src.id);
if (!creds2) throw new Error(`Otsar HaHayal account ${src.id} not found in vault`);
const { ilsData, foreignData, creditCardData } = await scrapeOtsarHahayal(
creds2,
dateFrom,
dateTo,
headless,
emit
);
const foreignAccountEntries = foreignData.map((d) => ({
account: `${String(d.metadata.account)}-${d.metadata.subAccount}`,
branch: String(d.metadata.branch)
}));
const accountPayload = [
...ilsData.map((d) => ({ account: d.account.account, branch: d.account.branch })),
...foreignAccountEntries
];
await registerDiscoveredAccounts("otsar-hahayal", creds2.id, [accountPayload]);
const check2 = checkAccounts("otsar-hahayal", accountPayload, getVault().accountRecords);
if (check2.unknown.length > 0) {
emit({
type: "task-blocked",
sourceId: src.id,
sourceType: "otsar-hahayal",
unknownAccounts: check2.unknown
});
throw new BlockedError(check2.unknown);
}
const ignoredAccountIds = new Set(check2.ignored);
const acceptedIlsData = ilsData.filter(
(d) => !ignoredAccountIds.has(`${d.account.branch}-${d.account.account}`)
);
const acceptedForeignData = foreignData.filter(
(d) => !ignoredAccountIds.has(
`${d.metadata.branch}-${d.metadata.account}-${d.metadata.subAccount}`
)
);
const creditCardPayload = creditCardData.map((d) => ({ card: d.card }));
let acceptedCreditCardData = creditCardData;
if (creditCardPayload.length > 0) {
await registerDiscoveredAccounts("otsar-hahayal-credit-card", creds2.id, [
creditCardPayload
]);
const ccCheck = checkAccounts(
"otsar-hahayal-credit-card",
creditCardPayload,
getVault().accountRecords
);
if (ccCheck.unknown.length > 0) {
emit({
type: "task-blocked",
sourceId: src.id,
sourceType: "otsar-hahayal",
unknownAccounts: ccCheck.unknown
});
throw new BlockedError(ccCheck.unknown);
}
const ignoredCards = new Set(ccCheck.ignored);
acceptedCreditCardData = creditCardData.filter(
(d) => !ignoredCards.has(d.card.maskedPan)
);
}
if (!uploadClient)
return {
inserted: 0,
skipped: 0,
insertedIds: [],
insertedTransactions: [],
changedTransactions: []
};
let totalInserted2 = 0;
let totalSkipped2 = 0;
const allIds2 = [];
const insertedTransactions2 = [];
const changedTransactions2 = [];
for (const d of acceptedIlsData) {
const accountId = `${d.account.branch}-${d.account.account}`;
emit({
type: "task-account-txns-uploading",
sourceId: src.id,
accountId,
txnType: "ils",
count: d.transactions.length
});
const r = await uploadClient.uploadOtsarIls([d]);
emit({
type: "task-account-txns-done",
sourceId: src.id,
accountId,
txnType: "ils",
inserted: r.inserted,
skipped: r.skipped
});
totalInserted2 += r.inserted;
totalSkipped2 += r.skipped;
allIds2.push(...r.insertedIds);
insertedTransactions2.push(...r.insertedTransactions ?? []);
changedTransactions2.push(...r.changedTransactions ?? []);
}
for (const d of acceptedForeignData) {
const accountId = `${d.metadata.branch}-${d.metadata.account}-${d.metadata.subAccount}`;
emit({
type: "task-account-txns-uploading",
sourceId: src.id,
accountId,
txnType: "foreign",
count: d.transactions.length
});
const rf = await uploadClient.uploadOtsarForeign([d]);
emit({
type: "task-account-txns-done",
sourceId: src.id,
accountId,
txnType: "foreign",
inserted: rf.inserted,
skipped: rf.skipped
});
totalInserted2 += rf.inserted;
totalSkipped2 += rf.skipped;
allIds2.push(...rf.insertedIds);
insertedTransactions2.push(...rf.insertedTransactions ?? []);
changedTransactions2.push(...rf.changedTransactions ?? []);
}
if (acceptedCreditCardData.length > 0) {
const ccCount = acceptedCreditCardData.reduce((s, d) => s + d.transactions.length, 0);
emit({
type: "task-month-uploading",
sourceId: src.id,
month: "credit-cards",
transactionCount: ccCount
});
const rc = await uploadClient.uploadOtsarCreditCard(acceptedCreditCardData);
emit({
type: "task-month-uploaded",
sourceId: src.id,
month: "credit-cards",
inserted: rc.inserted,
skipped: rc.skipped
});
totalInserted2 += rc.inserted;
totalSkipped2 += rc.skipped;
allIds2.push(...rc.insertedIds);
insertedTransactions2.push(...rc.insertedTransactions ?? []);
changedTransactions2.push(...rc.changedTransactions ?? []);
}
return {
inserted: totalInserted2,
skipped: totalSkipped2,
insertedIds: allIds2,
insertedTransactions: insertedTransactions2,
changedTransactions: changedTransactions2
};
} catch (err) {
console.error({ err }, "[task] Otsar HaHayal scrape error");
throw err;
}
}
if (src.type === "poalim") {
try {
const creds2 = vault.poalimAccounts.find((a) => a.id === src.id);
if (!creds2) throw new Error(`Poalim account ${src.id} not found in vault`);
const accountsData = await scrapePoalim(
creds2,
dateFrom,
dateTo,
headless,
otpManager,
emit
);
if (!uploadClient)
return {
inserted: 0,
skipped: 0,
insertedIds: [],
insertedTransactions: [],
changedTransactions: []
};
let totalInserted2 = 0;
let totalSkipped2 = 0;
const allIds2 = [];
const insertedTransactions2 = [];
const changedTransactions2 = [];
for (const accountData of accountsData) {
const accountId = `${accountData.bankAccount.branchNumber}-${accountData.bankAccount.accountNumber}`;
const ilsPayload = accountData.ils;
if (ilsPayload) {
const pendingIlsTxns = ilsPayload.transactions?.filter(
(t) => t.transactionType === "TODAY" || t.transactionType === "FUTURE"
) ?? [];
for (const t of pendingIlsTxns) {
console.info(
`[poalim] Skipping ${t.transactionType} ILS transaction: ${t.activityDescription}, ILS ${t.eventAmount}, account ${ilsPayload.retrievalTransactionData.accountNumber}`
);
}
const filteredIlsPayload = {
...ilsPayload,
transactions: ilsPayload.transactions?.filter(
(t) => t.transactionType !== "TODAY" && t.transactionType !== "FUTURE"
) ?? []
};
const ilsCount = filteredIlsPayload.transactions.length;
emit({
type: "task-account-txns-uploading",
sourceId: src.id,
accountId,
txnType: "ils",
count: ilsCount
});
const r = await uploadClient.uploadPoalimIls(filteredIlsPayload);
emit({
type: "task-account-txns-done",
sourceId: src.id,
accountId,
txnType: "ils",
inserted: r.inserted,
skipped: r.skipped + pendingIlsTxns.length
});
totalInserted2 += r.inserted;
totalSkipped2 += r.skipped + pendingIlsTxns.length;
allIds2.push(...r.insertedIds);
insertedTransactions2.push(...r.insertedTransactions ?? []);
changedTransactions2.push(...r.changedTransactions ?? []);
}
const foreignPayload = accountData.foreign;
if (foreignPayload) {
let foreignPendingCount = 0;
const filteredForeignPayload = {
...foreignPayload,
balancesAndLimitsDataList: foreignPayload.balancesAndLimitsDataList.map((entry) => {
const pending = entry.transactions.filter(
(t) => t.transactionType === "TODAY" || t.transactionType === "FUTURE"
);
for (const t of pending) {
console.info(
`[poalim] Skipping ${t.transactionType} foreign transaction: ${t.activityDescription}, account ${accountData.bankAccount.accountNumber}`
);
}
foreignPendingCount += pending.length;
return {
...entry,
transactions: entry.transactions.filter(
(t) => t.transactionType !== "TODAY" && t.transactionType !== "FUTURE"
)
};
})
};
const foreignCount = filteredForeignPayload.balancesAndLimitsDataList.reduce(
(sum, e) => sum + e.transactions.length,
0
);
emit({
type: "task-account-txns-uploading",
sourceId: src.id,
accountId,
txnType: "foreign",
count: foreignCount
});
const rf = await uploadClient.uploadPoalimForeign(
filteredForeignPayload,
accountData.bankAccount
);
emit({
type: "task-account-txns-done",
sourceId: src.id,
accountId,
txnType: "foreign",
inserted: rf.inserted,
skipped: rf.skipped + foreignPendingCount
});
totalInserted2 += rf.inserted;
totalSkipped2 += rf.skipped + foreignPendingCount;
allIds2.push(...rf.insertedIds);
insertedTransactions2.push(...rf.insertedTransactions ?? []);
changedTransactions2.push(...rf.changedTransactions ?? []);
}
const swiftPayload = accountData.swift;
if (swiftPayload) {
const swiftCount = swiftPayload.swiftsList?.length ?? 0;
emit({
type: "task-account-txns-uploading",
sourceId: src.id,
accountId,
txnType: "swift",
count: swiftCount
});
const rs = await uploadClient.uploadPoalimSwift(
swiftPayload,
accountData.bankAccount
);
emit({
type: "task-account-txns-done",
sourceId: src.id,
accountId,
txnType: "swift",
inserted: rs.inserted,
skipped: rs.skipped
});
totalInserted2 += rs.inserted;
totalSkipped2 += rs.skipped;
allIds2.push(...rs.insertedIds);
insertedTransactions2.push(...rs.insertedTransactions ?? []);
changedTransactions2.push(...rs.changedTransactions ?? []);
}
}
return {
inserted: totalInserted2,
skipped: totalSkipped2,
insertedIds: allIds2,
insertedTransactions: insertedTransactions2,
changedTransactions: changedTransactions2
};
} catch (err) {
console.error({ err }, "[task] Poalim scrape error");
throw err;
}
}
let isracardPayloads = null;
let payloads;
switch (src.type) {
case "isracard": {
const creds2 = vault.isracardAccounts.find((a) => a.id === src.id);
if (!creds2) throw new Error(`Isracard account ${src.id} not found in vault`);
isracardPayloads = await scrapeIsracard(creds2, dateFrom, dateTo, emit, headless);
payloads = isracardPayloads.map((p) => p.data);
break;
}
case "amex": {
const creds2 = vault.amexAccounts.find((a) => a.id === src.id);
if (!creds2) throw new Error(`Amex account ${src.id} not found in vault`);
isracardPayloads = await scrapeAmex(creds2, dateFrom, dateTo, emit, headless);
payloads = isracardPayloads.map((p) => p.data);
break;
}
case "cal": {
const creds2 = vault.calAccounts.find((a) => a.id === src.id);
if (!creds2) throw new Error(`Cal account ${src.id} not found in vault`);
payloads = [await scrapeCal(creds2, dateFrom, dateTo, emit, headless)];
break;
}
case "discount": {
const creds2 = vault.discountAccounts.find((a) => a.id === src.id);
if (!creds2) throw new Error(`Discount account ${src.id} not found in vault`);
payloads = [await scrapeDiscount(creds2, dateFrom, dateTo, emit, headless)];
break;
}
case "max": {
const creds2 = vault.maxAccounts.find((a) => a.id === src.id);
if (!creds2) throw new Error(`Max account ${src.id} not found in vault`);
payloads = [await scrapeMax(creds2, dateFrom, dateTo, emit, headless)];
break;
}
default:
throw new Error(`Unhandled source type: ${src.type}`);
}
const representativePayload = payloads[0];
if (!representativePayload)
return {
inserted: 0,
skipped: 0,
insertedIds: [],
insertedTransactions: [],
changedTransactions: []
};
let creds;
switch (src.type) {
case "isracard":
creds = vault.isracardAccounts.find((a) => a.id === src.id);
break;
case "amex":
creds = vault.amexAccounts.find((a) => a.id === src.id);
break;
case "cal":
creds = vault.calAccounts.find((a) => a.id === src.id);
break;
case "max":
creds = vault.maxAccounts.find((a) => a.id === src.id);
break;
case "discount":
creds = vault.discountAccounts.find((a) => a.id === src.id);
break;
}
if (creds) {
payloads = payloads.map((p) => filterPayload(src.type, p, creds));
isracardPayloads &&= isracardPayloads.map((p, i) => ({
...p,
data: payloads[i]
}));
}
await registerDiscoveredAccounts(src.type, src.id, payloads);
const check = checkAccounts(src.type, payloads[0], getVault().accountRecords);
if (check.unknown.length > 0) {
emit({
type: "task-blocked",
sourceId: src.id,
sourceType: src.type,
unknownAccounts: check.unknown
});
throw new BlockedError(check.unknown);
}
if (!uploadClient)
return {
inserted: 0,
skipped: 0,
insertedIds: [],
insertedTransactions: [],
changedTransactions: []
};
let totalInserted = 0;
let totalSkipped = 0;
const allIds = [];
const changedTransactions = [];
const insertedTransactions = [];
if ((src.type === "isracard" || src.type === "amex") && isracardPayloads) {
for (const { month, data: monthData } of isracardPayloads) {
const txnCount = countIsracardTransactions(monthData);
emit({
type: "task-month-uploading",
sourceId: src.id,
month,
transactionCount: txnCount
});
let result;
if (src.type === "isracard") {
result = await uploadClient.uploadIsracard([monthData]);
} else {
result = await uploadClient.uploadAmex([monthData]);
}
emit({
type: "task-month-uploaded",
sourceId: src.id,
month,
inserted: result.inserted,
skipped: result.skipped
});
totalInserted += result.inserted;
totalSkipped += result.skipped;
allIds.push(...result.insertedIds);
changedTransactions.push(...result.changedTransactions ?? []);
insertedTransactions.push(...result.insertedTransactions ?? []);
}
} else {
for (const payload of payloads) {
let result;
switch (src.type) {
case "cal": {
for (const entry of payload) {
emit({
type: "task-month-uploading",
sourceId: src.id,
month: entry.month,
transactionCount: entry.transactions.length
});
result = await uploadClient.uploadCal([entry]);
emit({
type: "task-month-uploaded",
sourceId: src.id,
month: entry.month,
inserted: result.inserted,
skipped: result.skipped
});
totalInserted += result.inserted;
totalSkipped += result.skipped;
allIds.push(...result.insertedIds);
changedTransactions.push(...result.changedTransactions ?? []);
insertedTransactions.push(...result.insertedTransactions ?? []);
}
result = {
inserted: 0,
skipped: 0,
insertedIds: [],
changedTransactions: [],
insertedTransactions: []
};
break;
}
case "discount": {
for (const entry of payload) {
emit({
type: "task-month-uploading",
sourceId: src.id,
month: entry.month,
transactionCount: entry.transactions.length
});
result = await uploadClient.uploadDiscount([entry]);
emit({
type: "task-month-uploaded",
sourceId: src.id,
month: entry.month,
inserted: result.inserted,
skipped: result.skipped
});
totalInserted += result.inserted;
totalSkipped += result.skipped;
allIds.push(...result.insertedIds);
changedTransactions.push(...result.changedTransactions ?? []);
insertedTransactions.push(...result.insertedTransactions ?? []);
}
result = {
inserted: 0,
skipped: 0,
insertedIds: [],
changedTransactions: [],
insertedTransactions: []
};
break;
}
case "max": {
for (const account of payload) {
emit({
type: "task-month-uploading",
sourceId: src.id,
month: account.accountNumber,
transactionCount: account.txns.length
});
result = await uploadClient.uploadMax([account]);
emit({
type: "task-month-uploaded",
sourceId: src.id,
month: account.accountNumber,
inserted: result.inserted,
skipped: result.skipped
});
totalInserted += result.inserted;
totalSkipped += result.skipped;
allIds.push(...result.insertedIds);
changedTransactions.push(...result.changedTransactions ?? []);
insertedTransactions.push(...result.insertedTransactions ?? []);
}
result = {
inserted: 0,
skipped: 0,
insertedIds: [],
changedTransactions: [],
insertedTransactions: []
};
break;
}
default:
result = {
inserted: 0,
skipped: 0,
insertedIds: [],
changedTransactions: [],
insertedTransactions: []
};
}
totalInserted += result.inserted;
totalSkipped += result.skipped;
allIds.push(...result.insertedIds);
}
}
return {
inserted: totalInserted,
skipped: totalSkipped,
insertedIds: allIds,
changedTransactions,
insertedTransactions
};
}
};
}
async function registerWebSocketRoute(app) {
await app.register(websocketPlugin);
app.get(
"/ws",
{
websocket: true,
preValidation: async (_req, reply) => {
if (isLocked()) {
await reply.status(401).send({ error: "vault-locked" });
}
}
},
(socket, _req) => {
socket.on("error", (err) => app.log.error({ err }, "[ws] socket error"));
send(socket, { type: "connected" });
const emit = (msg) => send(socket, msg);
let activeOtpManager = null;
socket.on("message", (raw) => {
let parsed;
try {
parsed = JSON.parse(raw.toString());
} catch {
app.log.warn("[ws] non-JSON message received");
return;
}
const result = ClientMessageSchema.safeParse(parsed);
if (!result.success) {
app.log.warn({ received: parsed }, "[ws] unknown message type");
send(socket, { type: "error", message: "Unknown message type" });
return;
}
const msg = result.data;
switch (msg.type) {
case "ping":
send(socket, { type: "pong" });
break;
case "run-start": {
let vault;
try {
vault = getVault();
} catch (e) {
send(socket, { type: "error", message: String(e) });
break;
}
const allRefs = collectSourceRefs(vault);
const sourceIds = msg.sourceIds;
const sources = sourceIds ? allRefs.filter((s) => sourceIds.includes(s.id)) : allRefs;
if (sources.length === 0 && !vault.settings.fetchBankOfIsraelRates) {
send(socket, { type: "run-error", message: "No matching sources found" });
break;
}
if (activeOtpManager) {
send(socket, { type: "run-error", message: ERR_RUN_IN_PROGRESS });
break;
}
const runDateTo = msg.dateTo ? new Date(msg.dateTo) : /* @__PURE__ */ new Date();
const months = vault.settings.defaultDateRangeMonths ?? 3;
const runDateFrom = msg.dateFrom ? new Date(msg.dateFrom) : new Date(
runDateTo.getFullYear(),
runDateTo.getMonth() - months,
runDateTo.getDate()
);
activeOtpManager = new OtpManager();
const uploadClient = vault.settings.serverUrl && vault.settings.apiKey ? createUploadClient(vault.settings.serverUrl, vault.settings.apiKey) : null;
const tasks = sources.map(
(src) => buildTask(src, vault, emit, activeOtpManager, uploadClient, runDateFrom, runDateTo)
);
if (vault.settings.fetchBankOfIsraelRates) {
tasks.push({
sourceId: "currency-rates",
nickname: "Currency Rates (Bank of Israel)",
type: "currency-rates",
run: async () => {
const payload = await scrapeCurrencyRates(emit, runDateFrom, runDateTo);
if (!uploadClient)
return {
inserted: 0,
skipped: 0,
insertedIds: [],
changedTransactions: [],
insertedTransactions: []
};
emit({
type: "task-month-uploading",
sourceId: "currency-rates",
month: "rates",
transactionCount: payload.length
});
const result2 = await uploadClient.uploadCurrencyRates(payload);
emit({
type: "task-month-uploaded",
sourceId: "currency-rates",
month: "rates",
inserted: result2.inserted,
skipped: result2.skipped
});
return result2;
}
});
}
const { saveHistory, historyFilePath } = vault.settings;
void startRun(tasks, vault.settings.concurrentScraping, emit).then(async (runRecord) => {
if (saveHistory) {
try {
await appendRun(
{
...runRecord,
startedAt: runRecord.startedAt.toISOString(),
completedAt: runRecord.completedAt.toISOString()
},
historyFilePath
);
} catch (err) {
app.log.error(err, "[ws] failed to append run history");
}
}
}).catch((err) => {
const message = err instanceof Error ? err.message : String(err);
if (message === ERR_RUN_IN_PROGRESS) {
send(socket, { type: "run-error", message });
} else {
app.log.error(err, "[ws] startRun error");
send(socket, { type: "error", message });
}
}).finally(() => {
activeOtpManager = null;
});
break;
}
case "cancel-scrape":
break;
case "otp-submit":
activeOtpManager?.submitOtp(msg.sourceId, msg.otp);
app.log.info({ sourceId: msg.sourceId }, "[ws] otp-submit received");
break;
}
});
socket.on("close", () => {
app.log.info("[ws] client disconnected");
});
}
);
}
// src/server/index.ts
async function buildServer() {
const app = Fastify({ logger: true });
app.get("/healthz", async () => ({ ok: true }));
await registerVaultRoutes(app);
await registerHistoryRoutes(app);
await registerWebSocketRoute(app);
const uiRoot = path2.join(path2.dirname(fileURLToPath(import.meta.url)), "../ui");
if (existsSync(uiRoot)) {
await app.register(staticPlugin, { root: uiRoot, prefix: "/", wildcard: true });
app.setNotFoundHandler((_req, reply) => reply.sendFile("index.html"));
}
return app;
}
if (import.meta.url === pathToFileURL(process.argv[1]).href) {
const port = process.env["PORT"] ? Number(process.env["PORT"]) : 4001;
const app = await buildServer();
try {
await app.listen({ port, host: "0.0.0.0" });
} catch (err) {
app.log.error(err);
process.exit(1);
}
}
export {
buildServer
};
//# sourceMappingURL=index.js.map