UNPKG

@accounter/scraper-app

Version:
3,731 lines • 127 kB
// 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