From 46a2e2a7b0aab024e999f7f87cfb048925a9dd88 Mon Sep 17 00:00:00 2001 From: Nasyarobby Putra Date: Sun, 13 Sep 2026 22:58:47 +0700 Subject: [PATCH] feat: add gmail-hook monorepo with watcher, api, and web Ship the receipt pipeline (Livin/Grab/BRI), PocketBase spendings, PM2 ecosystem, and a proper .gitignore that excludes secrets, SQLite data, and media dumps. Co-authored-by: Cursor --- .env.example | 35 + .gitignore | 47 + AGENTS.md | 102 + apps/api/package.json | 17 + apps/api/src/config.js | 38 + apps/api/src/db.js | 82 + apps/api/src/google.js | 89 + apps/api/src/http.js | 14 + apps/api/src/index.js | 84 + apps/api/src/mime.js | 69 + apps/api/src/routes/auth.js | 53 + apps/api/src/routes/messages.js | 92 + apps/watcher/ecosystem.config.cjs | 28 + apps/watcher/package.json | 19 + apps/watcher/pnpm-lock.yaml | 1414 +++++ apps/watcher/pnpm-workspace.yaml | 2 + apps/watcher/scripts/list-messages.js | 140 + apps/watcher/scripts/process-messages.js | 243 + apps/watcher/src/config.js | 73 + apps/watcher/src/db.js | 102 + apps/watcher/src/google.js | 105 + .../src/handlers/fetch-and-log-message.js | 76 + apps/watcher/src/handlers/log-event.js | 12 + apps/watcher/src/handlers/match-rule.js | 58 + .../src/handlers/parse-matched-message.js | 76 + apps/watcher/src/handlers/save-spending.js | 126 + apps/watcher/src/http.js | 6 + apps/watcher/src/index.js | 92 + apps/watcher/src/lib/email.js | 66 + apps/watcher/src/lib/parse-helpers.js | 168 + apps/watcher/src/mime.js | 256 + apps/watcher/src/notify/ntfy.js | 102 + apps/watcher/src/parsers/bri.js | 68 + apps/watcher/src/parsers/grab.js | 247 + apps/watcher/src/parsers/livin.js | 94 + apps/watcher/src/pipeline.js | 12 + apps/watcher/src/pocketbase/client.js | 91 + apps/watcher/src/pocketbase/map.js | 52 + apps/watcher/src/pocketbase/schema.js | 121 + apps/watcher/src/process-fetched.js | 32 + apps/watcher/src/routes/pubsub.js | 169 + apps/watcher/src/rules/bri-transfer.js | 6 + apps/watcher/src/rules/grab-ereceipt.js | 6 + apps/watcher/src/rules/index.js | 5 + apps/watcher/src/rules/livin-qr-payment.js | 6 + apps/watcher/test/eml.test.js | 327 + apps/watcher/test/fixtures.js | 233 + apps/watcher/test/match.test.js | 171 + apps/watcher/test/ntfy.test.js | 23 + apps/watcher/test/parse-helpers.test.js | 17 + apps/watcher/test/parse.test.js | 276 + apps/web/index.html | 12 + apps/web/jsconfig.json | 7 + apps/web/package.json | 22 + apps/web/src/App.jsx | 56 + apps/web/src/api.js | 29 + apps/web/src/auth.jsx | 49 + apps/web/src/components/Layout.jsx | 34 + apps/web/src/index.css | 17 + apps/web/src/main.jsx | 16 + apps/web/src/pages/Inbox.jsx | 169 + apps/web/src/pages/Login.jsx | 35 + apps/web/src/pages/Message.jsx | 80 + apps/web/vite.config.js | 16 + package-lock.json | 5537 +++++++++++++++++ package.json | 26 + pnpm-lock.yaml | 206 + 67 files changed, 12153 insertions(+) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 AGENTS.md create mode 100644 apps/api/package.json create mode 100644 apps/api/src/config.js create mode 100644 apps/api/src/db.js create mode 100644 apps/api/src/google.js create mode 100644 apps/api/src/http.js create mode 100644 apps/api/src/index.js create mode 100644 apps/api/src/mime.js create mode 100644 apps/api/src/routes/auth.js create mode 100644 apps/api/src/routes/messages.js create mode 100644 apps/watcher/ecosystem.config.cjs create mode 100644 apps/watcher/package.json create mode 100644 apps/watcher/pnpm-lock.yaml create mode 100644 apps/watcher/pnpm-workspace.yaml create mode 100644 apps/watcher/scripts/list-messages.js create mode 100644 apps/watcher/scripts/process-messages.js create mode 100644 apps/watcher/src/config.js create mode 100644 apps/watcher/src/db.js create mode 100644 apps/watcher/src/google.js create mode 100644 apps/watcher/src/handlers/fetch-and-log-message.js create mode 100644 apps/watcher/src/handlers/log-event.js create mode 100644 apps/watcher/src/handlers/match-rule.js create mode 100644 apps/watcher/src/handlers/parse-matched-message.js create mode 100644 apps/watcher/src/handlers/save-spending.js create mode 100644 apps/watcher/src/http.js create mode 100644 apps/watcher/src/index.js create mode 100644 apps/watcher/src/lib/email.js create mode 100644 apps/watcher/src/lib/parse-helpers.js create mode 100644 apps/watcher/src/mime.js create mode 100644 apps/watcher/src/notify/ntfy.js create mode 100644 apps/watcher/src/parsers/bri.js create mode 100644 apps/watcher/src/parsers/grab.js create mode 100644 apps/watcher/src/parsers/livin.js create mode 100644 apps/watcher/src/pipeline.js create mode 100644 apps/watcher/src/pocketbase/client.js create mode 100644 apps/watcher/src/pocketbase/map.js create mode 100644 apps/watcher/src/pocketbase/schema.js create mode 100644 apps/watcher/src/process-fetched.js create mode 100644 apps/watcher/src/routes/pubsub.js create mode 100644 apps/watcher/src/rules/bri-transfer.js create mode 100644 apps/watcher/src/rules/grab-ereceipt.js create mode 100644 apps/watcher/src/rules/index.js create mode 100644 apps/watcher/src/rules/livin-qr-payment.js create mode 100644 apps/watcher/test/eml.test.js create mode 100644 apps/watcher/test/fixtures.js create mode 100644 apps/watcher/test/match.test.js create mode 100644 apps/watcher/test/ntfy.test.js create mode 100644 apps/watcher/test/parse-helpers.test.js create mode 100644 apps/watcher/test/parse.test.js create mode 100644 apps/web/index.html create mode 100644 apps/web/jsconfig.json create mode 100644 apps/web/package.json create mode 100644 apps/web/src/App.jsx create mode 100644 apps/web/src/api.js create mode 100644 apps/web/src/auth.jsx create mode 100644 apps/web/src/components/Layout.jsx create mode 100644 apps/web/src/index.css create mode 100644 apps/web/src/main.jsx create mode 100644 apps/web/src/pages/Inbox.jsx create mode 100644 apps/web/src/pages/Login.jsx create mode 100644 apps/web/src/pages/Message.jsx create mode 100644 apps/web/vite.config.js create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 pnpm-lock.yaml diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..e3c9986 --- /dev/null +++ b/.env.example @@ -0,0 +1,35 @@ +# Google OAuth Web client (gmail.readonly) +GOOGLE_CLIENT_ID= +GOOGLE_CLIENT_SECRET= + +# Must match an Authorized redirect URI in Google Cloud Console. +# Local (Vite proxies /callback to Fastify): +GOOGLE_REDIRECT_URI=http://localhost:5173/callback +# Production: +# GOOGLE_REDIRECT_URI=https://oauth.0dev.web.id/callback + +# 32+ random characters. Used to derive the session cookie key. +SESSION_SECRET=change-me-to-a-long-random-string + +PORT=3000 +NODE_ENV=development + +# Watcher (apps/watcher) — Gmail Pub/Sub push +WATCHER_PORT=3001 +# Full topic name, e.g. projects/my-gcp-project/topics/gmail-push +# A short name like gmail-push is expanded if GOOGLE_CLOUD_PROJECT is set. +GOOGLE_PUBSUB_TOPIC= +# GOOGLE_CLOUD_PROJECT= +# Shared secret. Push URL: https://host/pubsub/gmail?token=... +PUBSUB_VERIFICATION_TOKEN= +# Defaults to apps/api/data/app.db (same OAuth tokens as the web app) +# SQLITE_PATH= + +# PocketBase (watcher writes parsed receipts to collection `spendings`) +# Use a _superusers account (admin), not a users-collection login. +POCKETBASE_URL= +POCKETBASE_USER= +POCKETBASE_PASSWORD= + +# ntfy alerts for parse failures / non-duplicate skips (default: system topic) +# NTFY_URL=https://n.0dev.web.id/system diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..587a559 --- /dev/null +++ b/.gitignore @@ -0,0 +1,47 @@ +# Dependencies +node_modules/ + +# Build output +dist/ +build/ +*.tsbuildinfo + +# Env / secrets (keep example) +.env +.env.* +!.env.example + +# Local SQLite / runtime data +apps/api/data/ +*.db +*.db-wal +*.db-shm + +# Logs +*.log +npm-debug.log* +pnpm-debug.log* +yarn-debug.log* +yarn-error.log* + +# OS / editor +.DS_Store +Thumbs.db +.idea/ +.vscode/ +*.swp +*.swo +*~ + +# Test / coverage +coverage/ +.nyc_output/ + +# Accidental media dumps +*.mp4 +*.mov +*.webm +*.mkv + +# PM2 local dumps (process list lives on the host) +.pm2/ diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..12da9d4 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,102 @@ +# Agent notes — gmail-hook + +npm workspaces: `apps/api`, `apps/web`, `apps/watcher`. Goal is **e-receipt email → parsed spending → PocketBase**, not a generic Gmail client. + +## Layout + +| App | Role | +|---|---| +| `apps/api` | Google OAuth, session cookie, Gmail read API. Tokens in SQLite `apps/api/data/app.db`. | +| `apps/web` | React login / inbox UI (Vite). | +| `apps/watcher` | Gmail Pub/Sub push consumer. Matches receipts, parses JSON, writes `spendings`. | + +Shared MIME helpers live in each app (`mime.js`). Watcher reuses the same SQLite tokens as the API. + +## Watcher pipeline + +`POST /pubsub/gmail` → `history.list` (`messageAdded`) → per message: + +1. `logEvent` + `fetchAndLogMessage` — fetch outer mail; collect `emlMessages` if any +2. **Work items:** if `.eml` / `message/rfc822` attachments exist → process **each attachment only** (outer is a wrapper). If none → process the outer message as usual. +3. For each work item (sequential, in-process — no job queue): `matchRule` → `parseMatchedMessage` → `saveSpending` + +Handlers return `{ pass: {...} }` to merge into ctx or `{ break: true }` to stop. **Parse full `message.body`** (and `message.html` when needed), never the 500-char log `bodyPreview`. + +Attached `.eml` mails use nested From/Subject from Gmail’s expanded `message/rfc822` part. When nested HTML/text is not inlined (`body.data` missing, only `attachmentId`), the watcher fetches those attachments (`hydrateRfc822Message`) before parse. Synthetic `message_id` looks like `outerId#partId`; dedupe remains on `invoice_id`. + +## Match rules + +Resolve original sender: + +1. If body has `---------- Forwarded message ---------`, use its From / Subject / Date. +2. Else use Gmail envelope. + +| kind | From address | Subject | +|---|---|---| +| `livin.qr_payment.success` | `noreply.livin@bankmandiri.co.id` | `Pembayaran Berhasil!` | +| `grab.ereceipt.ride` | `no-reply@grab.com` | `Your Grab E-Receipt` | +| `grab.ereceipt.tip` | same From/Subject as ride; detected from tip body copy | tip invoice_id = `{Booking ID}:tip` | +| `grab.ereceipt.food` | same From/Subject; body has GrabFood / `Selamat menikmati makanan` | `Pesanan ID` as invoice_id | +| `bri.transfer.success` | `bankbri@bri.co.id` | `Pemindahan Dana Sesama Rekening BRI` | + +Production path is **direct provider mail**. Forwards still work via the Fwd header. Do **not** match on the forwarder envelope From. + +Rules: `apps/watcher/src/rules/`. Parsers: `apps/watcher/src/parsers/`. + +## Parsing notes + +- **Required for a spending row:** `amount` + `reference` (invoice id). Fail soft (log + break) if missing — never throw (Pub/Sub must ack). +- **Livin:** support clean forwarded lines and jammed HTML-stripped labels (`Tanggal13 Sep 2026`, `Tanggal15 Agu 2026`). Indonesian month names/abbreviations (`Agu`/`Agustus`, `Des`, …) are accepted. When merchant and city are glued on one line, keep the whole string as `merchant`; only split location when it is already a separate line ending in `- ID`. Do not maintain a city-name list. +- **Grab:** plain text often has date-only `Picked up on …`; pickup/dropoff times (`10:47AM`) live in HTML. Watcher keeps `message.html` and merges the first standalone AM/PM time into `occurredAt` (WIB). Tip receipts (`Your tip goes a long way…`, timestamp like `13 Sep 26 10:47 +0700`) become `grab.ereceipt.tip` with `invoice_id` `{bookingId}:tip` so they do not collide with the ride. +- **BRI / BRImo:** `Pemindahan Dana Sesama Rekening BRI` — amount from `Nominal` (not `Total`), `invoice_id` from `Nomor Referensi`, `description` from `Nama Tujuan`, datetime from combined `Tanggal` (`13 September 2026 , 19:00:00 WIB`). Other BRI subjects are out of scope until samples appear. +- Open-ended text (names, products, driver, compliments) is best-effort optional `details`. + +## PocketBase + +Env: `POCKETBASE_URL`, `POCKETBASE_USER`, `POCKETBASE_PASSWORD` (a `_superusers` account). Collection: `spendings`. + +- Unique `invoice_id` — duplicate insert → log skip, no throw. +- Leave `category` empty; user fills it in PocketBase admin. +- Store flat columns for querying; `details` for kind leftovers; `parsed` for the full watcher envelope JSON (audit snapshot). Keep `message_id` so the original Gmail message can be re-fetched if needed — do not store raw email body by default. +- Schema ensure runs on watcher boot (`ensureSpendingsSchema`). +- Never commit secrets. + +Parse failures and non-duplicate skips (incomplete record, save errors) POST to ntfy (`NTFY_URL`, default `https://n.0dev.web.id/system`) with `messageId` and message body. Duplicates do not notify. + +## Run + +```bash +npm install +npm run dev # api + web +# sign in once via UI so SQLite has tokens +npm run dev:watcher +npm test -w watcher # unit tests (no Gmail/Pub/Sub) +``` + +### Manual / backfill (forwarded old receipts) + +Gmail Date on a forward is “today”; `trx_date` still comes from the receipt body. Duplicates skip on unique `invoice_id`. Forward-as-attachment (multiple `*.eml`) is expanded the same way as the watcher. + +```bash +# List id / from / subject (from Gmail API) +npm run list-messages -- +npm run list-messages -- --query 'newer_than:14d has:attachment filename:eml' --max 100 + +# Dry-run recent Livin/Grab subjects (default query: newer_than:7d) +npm run process-messages -- --dry-run + +# Process a wrapper mail that has .eml attachments +npm run process-messages -- --ids OUTER_MESSAGE_ID + +# Process and save +npm run process-messages -- --query 'newer_than:2d subject:"Pembayaran Berhasil!"' +npm run process-messages -- --ids MESSAGE_ID_1,MESSAGE_ID_2 +npm run process-messages -- --user eleven16th@gmail.com --max 100 +``` + +## Adding a provider + +1. Rule module under `src/rules/` (kind, fromAddress, subject, source). +2. Parser under `src/parsers/`; register in `parse-matched-message.js`. +3. Fixture + tests under `test/`. +4. Map into spendings in `pocketbase/map.js` (`source`, `description`, `details`, `parsed`). diff --git a/apps/api/package.json b/apps/api/package.json new file mode 100644 index 0000000..d82e3ce --- /dev/null +++ b/apps/api/package.json @@ -0,0 +1,17 @@ +{ + "name": "api", + "private": true, + "type": "module", + "scripts": { + "dev": "node --watch src/index.js", + "start": "node src/index.js" + }, + "dependencies": { + "@fastify/secure-session": "^8.2.0", + "@fastify/static": "^8.2.0", + "better-sqlite3": "^12.2.0", + "dotenv": "^17.2.2", + "fastify": "^5.6.0", + "googleapis": "^159.0.0" + } +} diff --git a/apps/api/src/config.js b/apps/api/src/config.js new file mode 100644 index 0000000..b99cd01 --- /dev/null +++ b/apps/api/src/config.js @@ -0,0 +1,38 @@ +import { config } from "dotenv"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +export const rootDir = resolve(__dirname, "../../.."); +export const apiDir = resolve(__dirname, ".."); +export const webDistDir = resolve(__dirname, "../../web/dist"); + +config({ path: resolve(rootDir, ".env"), quiet: true }); + +export const PORT = Number(process.env.PORT || 3000); +export const NODE_ENV = process.env.NODE_ENV || "development"; +export const GOOGLE_CLIENT_ID = process.env.GOOGLE_CLIENT_ID || ""; +export const GOOGLE_CLIENT_SECRET = process.env.GOOGLE_CLIENT_SECRET || ""; +export const GOOGLE_REDIRECT_URI = + process.env.GOOGLE_REDIRECT_URI || "http://localhost:5173/callback"; +export const SESSION_SECRET = process.env.SESSION_SECRET || ""; + +export const GMAIL_SCOPE = "https://www.googleapis.com/auth/gmail.readonly"; +export const OAUTH_SCOPES = [ + GMAIL_SCOPE, + "openid", + "https://www.googleapis.com/auth/userinfo.email", + "https://www.googleapis.com/auth/userinfo.profile", +]; + +export function assertConfig() { + const missing = []; + if (!GOOGLE_CLIENT_ID) missing.push("GOOGLE_CLIENT_ID"); + if (!GOOGLE_CLIENT_SECRET) missing.push("GOOGLE_CLIENT_SECRET"); + if (!SESSION_SECRET || SESSION_SECRET.length < 16) { + missing.push("SESSION_SECRET (16+ characters)"); + } + if (missing.length) { + throw new Error(`Missing required env: ${missing.join(", ")}`); + } +} diff --git a/apps/api/src/db.js b/apps/api/src/db.js new file mode 100644 index 0000000..89cd74a --- /dev/null +++ b/apps/api/src/db.js @@ -0,0 +1,82 @@ +import { mkdirSync } from "node:fs"; +import { resolve } from "node:path"; +import Database from "better-sqlite3"; +import { apiDir } from "./config.js"; + +const dataDir = resolve(apiDir, "data"); +mkdirSync(dataDir, { recursive: true }); + +const db = new Database(resolve(dataDir, "app.db")); +db.pragma("journal_mode = WAL"); +db.pragma("foreign_keys = ON"); + +db.exec(` + CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + google_sub TEXT NOT NULL UNIQUE, + email TEXT NOT NULL, + name TEXT, + picture TEXT, + created_at TEXT NOT NULL DEFAULT (datetime('now')) + ); + + CREATE TABLE IF NOT EXISTS oauth_tokens ( + user_id INTEGER PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE, + access_token TEXT NOT NULL, + refresh_token TEXT, + expiry INTEGER NOT NULL + ); +`); + +const upsertUserStmt = db.prepare(` + INSERT INTO users (google_sub, email, name, picture) + VALUES (@googleSub, @email, @name, @picture) + ON CONFLICT(google_sub) DO UPDATE SET + email = excluded.email, + name = excluded.name, + picture = excluded.picture + RETURNING * +`); + +const saveTokensStmt = db.prepare(` + INSERT INTO oauth_tokens (user_id, access_token, refresh_token, expiry) + VALUES (@userId, @accessToken, @refreshToken, @expiry) + ON CONFLICT(user_id) DO UPDATE SET + access_token = excluded.access_token, + refresh_token = COALESCE(excluded.refresh_token, oauth_tokens.refresh_token), + expiry = excluded.expiry +`); + +const getUserByIdStmt = db.prepare(`SELECT * FROM users WHERE id = ?`); +const getTokensStmt = db.prepare(`SELECT * FROM oauth_tokens WHERE user_id = ?`); + +export function upsertUser({ googleSub, email, name, picture }) { + return upsertUserStmt.get({ googleSub, email, name, picture }); +} + +export function saveTokens(userId, { accessToken, refreshToken, expiry }) { + saveTokensStmt.run({ + userId, + accessToken, + refreshToken: refreshToken || null, + expiry, + }); +} + +export function getUserById(id) { + return getUserByIdStmt.get(id) || null; +} + +export function getTokens(userId) { + return getTokensStmt.get(userId) || null; +} + +export function publicUser(row) { + if (!row) return null; + return { + id: row.id, + email: row.email, + name: row.name, + picture: row.picture, + }; +} diff --git a/apps/api/src/google.js b/apps/api/src/google.js new file mode 100644 index 0000000..ef8acbc --- /dev/null +++ b/apps/api/src/google.js @@ -0,0 +1,89 @@ +import { google } from "googleapis"; +import { + GOOGLE_CLIENT_ID, + GOOGLE_CLIENT_SECRET, + GOOGLE_REDIRECT_URI, + OAUTH_SCOPES, +} from "./config.js"; +import { getTokens, saveTokens } from "./db.js"; +import { HttpError } from "./http.js"; + +export function createOAuthClient() { + return new google.auth.OAuth2( + GOOGLE_CLIENT_ID, + GOOGLE_CLIENT_SECRET, + GOOGLE_REDIRECT_URI, + ); +} + +export function authUrl(state) { + return createOAuthClient().generateAuthUrl({ + access_type: "offline", + prompt: "consent", + include_granted_scopes: true, + scope: OAUTH_SCOPES, + state, + }); +} + +export async function exchangeCode(code) { + const client = createOAuthClient(); + const { tokens } = await client.getToken(code); + client.setCredentials(tokens); + + const oauth2 = google.oauth2({ version: "v2", auth: client }); + const { data: profile } = await oauth2.userinfo.get(); + + if (!profile.id || !profile.email) { + throw new HttpError(400, "Google profile did not include email"); + } + + return { + profile: { + googleSub: profile.id, + email: profile.email, + name: profile.name || profile.email, + picture: profile.picture || null, + }, + tokens: { + accessToken: tokens.access_token, + refreshToken: tokens.refresh_token || null, + expiry: tokens.expiry_date || Date.now() + 3600 * 1000, + }, + }; +} + +function persistClientTokens(userId, client) { + client.on("tokens", (tokens) => { + if (!tokens.access_token) return; + saveTokens(userId, { + accessToken: tokens.access_token, + refreshToken: tokens.refresh_token || null, + expiry: tokens.expiry_date || Date.now() + 3600 * 1000, + }); + }); +} + +export async function gmailClientForUser(userId) { + const stored = getTokens(userId); + if (!stored?.access_token && !stored?.refresh_token) { + throw new HttpError(401, "Google account is not connected"); + } + + const client = createOAuthClient(); + client.setCredentials({ + access_token: stored.access_token, + refresh_token: stored.refresh_token, + expiry_date: stored.expiry, + }); + persistClientTokens(userId, client); + try { + await client.getAccessToken(); + } catch { + throw new HttpError( + 401, + "Gmail access expired or was revoked. Sign in again.", + ); + } + return google.gmail({ version: "v1", auth: client }); +} diff --git a/apps/api/src/http.js b/apps/api/src/http.js new file mode 100644 index 0000000..58e2adf --- /dev/null +++ b/apps/api/src/http.js @@ -0,0 +1,14 @@ +export class HttpError extends Error { + constructor(statusCode, message) { + super(message); + this.statusCode = statusCode; + } +} + +export function requireUser(request) { + const userId = request.session.get("userId"); + if (!userId) { + throw new HttpError(401, "Not authenticated"); + } + return userId; +} diff --git a/apps/api/src/index.js b/apps/api/src/index.js new file mode 100644 index 0000000..ae96f5a --- /dev/null +++ b/apps/api/src/index.js @@ -0,0 +1,84 @@ +import { createHash } from "node:crypto"; +import { existsSync } from "node:fs"; +import Fastify from "fastify"; +import fastifySecureSession from "@fastify/secure-session"; +import fastifyStatic from "@fastify/static"; +import { + NODE_ENV, + PORT, + SESSION_SECRET, + assertConfig, + webDistDir, +} from "./config.js"; +import { HttpError } from "./http.js"; +import { registerAuthRoutes } from "./routes/auth.js"; +import { registerMessageRoutes } from "./routes/messages.js"; + +assertConfig(); + +const app = Fastify({ + logger: true, + trustProxy: true, +}); + +await app.register(fastifySecureSession, { + key: createHash("sha256").update(SESSION_SECRET).digest(), + cookieName: "session", + expiry: 60 * 60 * 24 * 7, + cookie: { + path: "/", + httpOnly: true, + sameSite: "lax", + secure: NODE_ENV === "production", + }, +}); + +app.get("/api/health", async () => ({ ok: true })); + +await registerAuthRoutes(app); +await registerMessageRoutes(app); + +app.setErrorHandler((err, request, reply) => { + const status = err.statusCode || 500; + if (status >= 500) { + request.log.error(err); + } + const message = + err instanceof HttpError || status < 500 + ? err.message + : "Internal server error"; + reply.code(status).send({ error: message }); +}); + +const serveFrontend = existsSync(webDistDir); + +if (serveFrontend) { + await app.register(fastifyStatic, { + root: webDistDir, + wildcard: false, + }); + + app.setNotFoundHandler((request, reply) => { + if ( + request.method !== "GET" || + request.url.startsWith("/api") || + request.url.startsWith("/auth") || + request.url.startsWith("/callback") + ) { + reply.code(404).send({ error: "Not found" }); + return; + } + return reply.sendFile("index.html"); + }); +} + +try { + await app.listen({ port: PORT, host: "0.0.0.0" }); + app.log.info( + `API listening on http://localhost:${PORT}` + + (serveFrontend ? " (serving web dist)" : " (API only; use Vite in dev)"), + ); +} catch (err) { + app.log.error(err); + process.exit(1); +} diff --git a/apps/api/src/mime.js b/apps/api/src/mime.js new file mode 100644 index 0000000..6ab171c --- /dev/null +++ b/apps/api/src/mime.js @@ -0,0 +1,69 @@ +function decodeBase64Url(data) { + if (!data) return ""; + const pad = data.length % 4 === 0 ? "" : "=".repeat(4 - (data.length % 4)); + return Buffer.from( + data.replace(/-/g, "+").replace(/_/g, "/") + pad, + "base64", + ).toString("utf8"); +} + +function stripHtml(html) { + return html + .replace(//gi, "") + .replace(//gi, "") + .replace(//gi, "\n") + .replace(/<\/p>/gi, "\n\n") + .replace(/<\/div>/gi, "\n") + .replace(/<\/tr>/gi, "\n") + .replace(/<[^>]+>/g, "") + .replace(/ /g, " ") + .replace(/&/g, "&") + .replace(/</g, "<") + .replace(/>/g, ">") + .replace(/"/g, '"') + .replace(/'/g, "'") + .replace(/'/gi, "'") + .replace(/\n{3,}/g, "\n\n") + .trim(); +} + +function walkParts(payload, acc = { text: "", html: "" }) { + if (!payload) return acc; + const mime = payload.mimeType || ""; + const data = payload.body?.data; + if (data) { + if (mime === "text/plain") acc.text += decodeBase64Url(data); + else if (mime === "text/html") acc.html += decodeBase64Url(data); + } + for (const part of payload.parts || []) { + walkParts(part, acc); + } + return acc; +} + +export function extractBody(payload) { + const { text, html } = walkParts(payload); + if (text.trim()) return text.trim(); + if (html.trim()) return stripHtml(html); + return ""; +} + +export function header(payload, name) { + const match = payload?.headers?.find( + (h) => h.name?.toLowerCase() === name.toLowerCase(), + ); + return match?.value || ""; +} + +export function summarizeMessage(message) { + const payload = message.payload || {}; + return { + id: message.id, + threadId: message.threadId, + from: header(payload, "From"), + to: header(payload, "To"), + subject: header(payload, "Subject") || "(no subject)", + date: header(payload, "Date"), + snippet: message.snippet || "", + }; +} diff --git a/apps/api/src/routes/auth.js b/apps/api/src/routes/auth.js new file mode 100644 index 0000000..c027cab --- /dev/null +++ b/apps/api/src/routes/auth.js @@ -0,0 +1,53 @@ +import { randomBytes } from "node:crypto"; +import { getUserById, publicUser, saveTokens, upsertUser } from "../db.js"; +import { HttpError, requireUser } from "../http.js"; +import { authUrl, exchangeCode } from "../google.js"; + +export async function registerAuthRoutes(app) { + app.get("/auth/google", async (request, reply) => { + const state = randomBytes(16).toString("hex"); + request.session.set("oauthState", state); + return reply.redirect(authUrl(state)); + }); + + app.get("/callback", async (request, reply) => { + const { code, state, error } = request.query; + if (error) { + request.log.warn({ error }, "Google OAuth error"); + return reply.redirect("/login?error=denied"); + } + if (!code || typeof code !== "string") { + return reply.redirect("/login?error=missing_code"); + } + const expected = request.session.get("oauthState"); + if (!expected || state !== expected) { + return reply.redirect("/login?error=invalid_state"); + } + request.session.set("oauthState", undefined); + + try { + const { profile, tokens } = await exchangeCode(code); + const user = upsertUser(profile); + saveTokens(user.id, tokens); + request.session.set("userId", user.id); + return reply.redirect("/"); + } catch (err) { + request.log.error(err); + return reply.redirect("/login?error=exchange_failed"); + } + }); + + app.post("/auth/logout", async (request, reply) => { + request.session.delete(); + return reply.send({ ok: true }); + }); + + app.get("/api/me", async (request) => { + const userId = requireUser(request); + const user = getUserById(userId); + if (!user) { + throw new HttpError(401, "Not authenticated"); + } + return publicUser(user); + }); +} diff --git a/apps/api/src/routes/messages.js b/apps/api/src/routes/messages.js new file mode 100644 index 0000000..40bf790 --- /dev/null +++ b/apps/api/src/routes/messages.js @@ -0,0 +1,92 @@ +import { gmailClientForUser } from "../google.js"; +import { HttpError, requireUser } from "../http.js"; +import { extractBody, summarizeMessage } from "../mime.js"; + +function fromGoogleError(err) { + const status = Number(err.code || err.response?.status || err.statusCode); + if (status === 401 || status === 403) { + return new HttpError( + 401, + "Gmail access expired or was revoked. Sign in again.", + ); + } + if (status === 404) { + return new HttpError(404, "Message not found"); + } + return err; +} + +export async function registerMessageRoutes(app) { + app.get("/api/messages", async (request) => { + const userId = requireUser(request); + const gmail = await gmailClientForUser(userId); + const q = typeof request.query.q === "string" ? request.query.q : ""; + const pageToken = + typeof request.query.pageToken === "string" + ? request.query.pageToken + : undefined; + const maxResults = Math.min( + 50, + Math.max(1, Number(request.query.maxResults) || 20), + ); + + let list; + try { + list = await gmail.users.messages.list({ + userId: "me", + q: q || undefined, + maxResults, + pageToken, + }); + } catch (err) { + throw fromGoogleError(err); + } + + const refs = list.data.messages || []; + const messages = []; + const settled = await Promise.allSettled( + refs.map((ref) => + gmail.users.messages.get({ + userId: "me", + id: ref.id, + format: "metadata", + metadataHeaders: ["From", "To", "Subject", "Date"], + }), + ), + ); + + for (const result of settled) { + if (result.status === "fulfilled") { + messages.push(summarizeMessage(result.value.data)); + } + } + + return { + messages, + nextPageToken: list.data.nextPageToken || null, + resultSizeEstimate: list.data.resultSizeEstimate || 0, + }; + }); + + app.get("/api/messages/:id", async (request) => { + const userId = requireUser(request); + const gmail = await gmailClientForUser(userId); + const { id } = request.params; + + let data; + try { + ({ data } = await gmail.users.messages.get({ + userId: "me", + id, + format: "full", + })); + } catch (err) { + throw fromGoogleError(err); + } + + return { + ...summarizeMessage(data), + body: extractBody(data.payload), + }; + }); +} diff --git a/apps/watcher/ecosystem.config.cjs b/apps/watcher/ecosystem.config.cjs new file mode 100644 index 0000000..29f7ee9 --- /dev/null +++ b/apps/watcher/ecosystem.config.cjs @@ -0,0 +1,28 @@ +/** + * PM2 ecosystem for the Gmail receipt watcher. + * + * Loads secrets from the monorepo root `.env` via dotenv in src/config.js. + * Override port with WATCHER_PORT (default 3001; production on rpi4 uses 8083). + * + * pm2 start ecosystem.config.cjs + * pm2 save + */ +module.exports = { + apps: [ + { + name: "gmail-watcher", + cwd: __dirname, + script: "src/index.js", + interpreter: "node", + instances: 1, + exec_mode: "fork", + autorestart: true, + max_restarts: 10, + restart_delay: 2000, + env: { + NODE_ENV: "production", + WATCHER_PORT: "8083", + }, + }, + ], +}; diff --git a/apps/watcher/package.json b/apps/watcher/package.json new file mode 100644 index 0000000..7e76146 --- /dev/null +++ b/apps/watcher/package.json @@ -0,0 +1,19 @@ +{ + "name": "watcher", + "private": true, + "type": "module", + "scripts": { + "dev": "node --watch src/index.js", + "start": "node src/index.js", + "test": "node --test test/**/*.test.js", + "process-messages": "node scripts/process-messages.js", + "list-messages": "node scripts/list-messages.js" + }, + "dependencies": { + "better-sqlite3": "^12.2.0", + "dotenv": "^17.2.2", + "fastify": "^5.6.0", + "googleapis": "^159.0.0", + "node-html-parser": "^9.0.4" + } +} diff --git a/apps/watcher/pnpm-lock.yaml b/apps/watcher/pnpm-lock.yaml new file mode 100644 index 0000000..23f4e79 --- /dev/null +++ b/apps/watcher/pnpm-lock.yaml @@ -0,0 +1,1414 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + better-sqlite3: + specifier: ^12.2.0 + version: 12.11.1 + dotenv: + specifier: ^17.2.2 + version: 17.4.2 + fastify: + specifier: ^5.6.0 + version: 5.12.4 + googleapis: + specifier: ^159.0.0 + version: 159.0.0 + node-html-parser: + specifier: ^9.0.4 + version: 9.0.4 + +packages: + + '@fastify/ajv-compiler@4.0.6': + resolution: {integrity: sha512-NtuzM0SfaMJbGlnjr9LWQUN5LzgSrbB8tf/wRZNas+4E1O/Nmzl53e7ruT61HDZyRCJGC6FxIogmNZO1c5ETBA==} + + '@fastify/error@4.2.0': + resolution: {integrity: sha512-RSo3sVDXfHskiBZKBPRgnQTtIqpi/7zhJOEmAxCiBcM7d0uwdGdxLlsCaLzGs8v8NnxIRlfG0N51p5yFaOentQ==} + + '@fastify/fast-json-stringify-compiler@5.1.0': + resolution: {integrity: sha512-PxcYtKLbQ8Z+yApiqjK8FwxIwvEj38k2OiLc17u8dkJSlmfi2wHHPaSnaoqBPQqtvF8YVsDgDpP2snDCfFrpfw==} + + '@fastify/forwarded@3.0.2': + resolution: {integrity: sha512-NE8HgKLgYejV9lDpqkEFaDKMLYelJBVfHekhB0UKvX0ghagXRJqg68feg8er1NPXxG4N9i6vPxzt8E+3wHfcmA==} + + '@fastify/merge-json-schemas@0.2.1': + resolution: {integrity: sha512-OA3KGBCy6KtIvLf8DINC5880o5iBlDX4SxzLQS8HorJAbqluzLRn80UXU0bxZn7UOFhFgpRJDasfwn9nG4FG4A==} + + '@fastify/proxy-addr@5.1.0': + resolution: {integrity: sha512-INS+6gh91cLUjB+PVHfu1UqcB76Sqtpyp7bnL+FYojhjygvOPA9ctiD/JDKsyD9Xgu4hUhCSJBPig/w7duNajw==} + + '@isaacs/cliui@8.0.2': + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + + '@pinojs/redact@0.4.0': + resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==} + + '@pkgjs/parseargs@0.11.0': + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + + abstract-logging@2.0.1: + resolution: {integrity: sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==} + + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + + ajv-formats@3.0.1: + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + + ajv@8.20.0: + resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.3.0: + resolution: {integrity: sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ==} + engines: {node: '>=12'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} + engines: {node: '>=12'} + + atomic-sleep@1.0.0: + resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} + engines: {node: '>=8.0.0'} + + avvio@9.3.0: + resolution: {integrity: sha512-g2tQ7LE7oOSqDfwEm3M+ZCMTJc7KiZCdJ4UwyZJb5ckTKyYu50OYmvv0mCFXPuYXoM4zkSt8zM9XQ9KCvxA74A==} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + better-sqlite3@12.11.1: + resolution: {integrity: sha512-dq9AtApgg5PGFtBzPFSBl3HZQjHok5gaQCM6zh2Yk0aSmDCs1CbnVI8/HgASQkNKsWFpseIO9beg5xxpYhbIfA==} + engines: {node: 20.x || 22.x || 23.x || 24.x || 25.x || 26.x} + + bignumber.js@9.3.1: + resolution: {integrity: sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==} + + bindings@1.5.0: + resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} + + bl@4.1.0: + resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + + boolbase@1.0.0: + resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + + brace-expansion@2.1.4: + resolution: {integrity: sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==} + + buffer-equal-constant-time@1.0.1: + resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} + + buffer@5.7.1: + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + chownr@1.1.4: + resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + cookie@1.1.1: + resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} + engines: {node: '>=18'} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + css-select@5.2.2: + resolution: {integrity: sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==} + + css-what@6.2.2: + resolution: {integrity: sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==} + engines: {node: '>= 6'} + + data-uri-to-buffer@4.0.1: + resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} + engines: {node: '>= 12'} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decompress-response@6.0.0: + resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} + engines: {node: '>=10'} + + deep-extend@0.6.0: + resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} + engines: {node: '>=4.0.0'} + + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + dom-serializer@2.0.0: + resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} + + domelementtype@2.3.0: + resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} + + domhandler@5.0.3: + resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} + engines: {node: '>= 4'} + + domutils@3.2.2: + resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} + + dotenv@17.4.2: + resolution: {integrity: sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==} + engines: {node: '>=12'} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + + ecdsa-sig-formatter@1.0.11: + resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + + end-of-stream@1.4.5: + resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} + + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + + entities@8.1.0: + resolution: {integrity: sha512-kxL7msIffSuh9aaFAMD7rxAIuTRMAHMeBtgHW2yUdWw732ZNh4MehkF2gdjvtdmikkaIP9bFDDJOPlsvm7avrA==} + engines: {node: '>=20.19.0'} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.2: + resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==} + engines: {node: '>= 0.4'} + + expand-template@2.0.3: + resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} + engines: {node: '>=6'} + + extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + + fast-decode-uri-component@1.0.1: + resolution: {integrity: sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-json-stringify@7.0.1: + resolution: {integrity: sha512-eRSayARSbbwlBjpP4vnTTIRD5QPcIrmihPxDeN1DtKnHPg66UuJLx+8hlK1kaFdjvzyQ/dzALoi4vwAQ+T+iZA==} + + fast-querystring@1.1.2: + resolution: {integrity: sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==} + + fast-uri@3.1.7: + resolution: {integrity: sha512-dOvZVzjdZdz7phd9v6jCbwxrBW3fK6n8Rc0CtdmM4bumzMnxywBYhuph6J819RRw/ku+rLbelwfMunktuzVVHg==} + + fast-uri@4.1.4: + resolution: {integrity: sha512-dODXrIxlS9JSdgAnhIUKOosKV1oMtU2VtVw87QRaHzyl5jxO290Ii5tEZfCfzfWNHi3jKWwBSdQj0qIyshdZdQ==} + + fastify@5.12.4: + resolution: {integrity: sha512-zATD676fP6tBMsn+jQ7Q5/cO/Qo+149TXH0dUqUKel+1/hcZxwIbnflqtPa9EjlrvpY8eCZcL3M1UYrMc31Y7g==} + + fastq@1.20.3: + resolution: {integrity: sha512-XKv5nnLs6nLF71NgiKJLIZFLkPyIEuOselLG7ujZnGrRfQK8HpvY+WqKhAJUAdLomwVHErVS4LfxFlPq0/FTAw==} + + fetch-blob@3.2.0: + resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} + engines: {node: ^12.20 || >= 14.13} + + file-uri-to-path@1.0.0: + resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} + + find-my-way@9.9.0: + resolution: {integrity: sha512-sJsgZ1sQH2UDuowPuMKg8az7Qc8F0jnj+SKkFWU/+T0xcFlgV5skgXOGUqmQzOdmW6ALA7AhJINWx3qFBkbLHA==} + engines: {node: '>=20'} + + foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} + engines: {node: '>=14'} + + formdata-polyfill@4.0.10: + resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} + engines: {node: '>=12.20.0'} + + fs-constants@1.0.0: + resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + gaxios@7.1.3: + resolution: {integrity: sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ==} + engines: {node: '>=18'} + + gaxios@7.3.1: + resolution: {integrity: sha512-kB3rzJV7d9juLZh8/56QTXCwQfxyhdOMdyYk1HdQKFtF8TJTDTZQJtixWIwXdE9Jji91mC41DUNpjleo4L4eAQ==} + engines: {node: '>=18'} + + gcp-metadata@8.1.2: + resolution: {integrity: sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==} + engines: {node: '>=18'} + + gcp-metadata@8.1.4: + resolution: {integrity: sha512-iJ9KMsiu+xKtNRX0PmGLSaIU3bUBAyzWTyqKemKPzNPsmmsBCQYmlNg+brEbES7IHSXtdVwzBPzx1vz3FAaipw==} + engines: {node: '>=18'} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + github-from-package@0.0.0: + resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==} + + glob@10.5.0: + resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + hasBin: true + + google-auth-library@10.5.0: + resolution: {integrity: sha512-7ABviyMOlX5hIVD60YOfHw4/CxOfBhyduaYB+wbFWCWoni4N7SLcV46hrVRktuBbZjFC9ONyqamZITN7q3n32w==} + engines: {node: '>=18'} + + google-auth-library@10.9.1: + resolution: {integrity: sha512-i1ydyHrqcIxXkWh/uBmVkzCvIuq5yiK2ATndIe5XxKholrG/MTYP9xGYka4sQhrbIAgGjL2B6NOE7rFaiF3fXw==} + engines: {node: '>=18'} + + google-logging-utils@1.1.3: + resolution: {integrity: sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==} + engines: {node: '>=14'} + + googleapis-common@8.0.3: + resolution: {integrity: sha512-7g1yzQKx0mmNTjiK0H9dJ8eqKqDBveES9vLHeg5neb3BMQy/d1oQefIMhIpOVT8a+f+LOcixMEdRbFIW/cQUJw==} + engines: {node: '>=18'} + + googleapis@159.0.0: + resolution: {integrity: sha512-halby2+lQwHNxUDk70aQNXP1BlBwdwr7svTJZvDi7vKwrWbVMKhVrZ86h8p3zRcWbO4qAgLQ4ODAf8TgD3DhGQ==} + engines: {node: '>=18'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + gtoken@8.0.0: + resolution: {integrity: sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw==} + engines: {node: '>=18'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + hasown@2.0.4: + resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} + engines: {node: '>= 0.4'} + + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + + ipaddr.js@2.5.0: + resolution: {integrity: sha512-aq+t5NAc+cS6rZQQVWC2x98CPqGtKKTMDd4Gaodv0wShnItdKg/51djkGJ1hqH+Oy0ivDftCbSLCQob8zso01w==} + engines: {node: '>= 10'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + jackspeak@3.4.3: + resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + + json-bigint@1.0.0: + resolution: {integrity: sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==} + + json-schema-ref-resolver@3.0.0: + resolution: {integrity: sha512-hOrZIVL5jyYFjzk7+y7n5JDzGlU8rfWDuYyHwGa2WA8/pcmMHezp2xsVwxrebD/Q9t8Nc5DboieySDpCp4WG4A==} + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + jwa@2.0.1: + resolution: {integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==} + + jws@4.0.1: + resolution: {integrity: sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==} + + light-my-request@6.6.0: + resolution: {integrity: sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A==} + + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + mimic-response@3.1.0: + resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} + engines: {node: '>=10'} + + minimatch@9.0.9: + resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==} + engines: {node: '>=16 || 14 >=14.17'} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + minipass@7.1.3: + resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} + engines: {node: '>=16 || 14 >=14.17'} + + mkdirp-classic@0.5.3: + resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + napi-build-utils@2.0.0: + resolution: {integrity: sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==} + + node-abi@3.96.0: + resolution: {integrity: sha512-rebQ/lz7i0EkoLzUVSrKRzA69zMkwLp95kKMWoMDkkM00Suxz0D7zEQPwRml5fQum24mj7bPvmlgLAmu2JCiYg==} + engines: {node: '>=10'} + + node-domexception@1.0.0: + resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} + engines: {node: '>=10.5.0'} + deprecated: Use your platform's native DOMException instead + + node-fetch@3.3.2: + resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + node-html-parser@9.0.4: + resolution: {integrity: sha512-reUSrZkF1rO7hEYe5mJwtbVcQ6mB5QrViCZH6Wv+iz3er/PA9Q50gol15dMLQRDfdli4VzqPMPR24HNRFgVKFw==} + + nth-check@2.1.1: + resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + on-exit-leak-free@2.1.2: + resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} + engines: {node: '>=14.0.0'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} + + pino-abstract-transport@3.0.0: + resolution: {integrity: sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==} + + pino-std-serializers@7.1.0: + resolution: {integrity: sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==} + + pino@10.3.1: + resolution: {integrity: sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==} + hasBin: true + + prebuild-install@7.1.3: + resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==} + engines: {node: '>=10'} + deprecated: No longer maintained. Please contact the author of the relevant native addon; alternatives are available. + hasBin: true + + process-warning@4.0.1: + resolution: {integrity: sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q==} + + process-warning@5.1.0: + resolution: {integrity: sha512-jQSaVHsPgtyw60e1rQ/A+/ArPEj/S8pS/vFnyGa/gYFXrKk/6RuDkoqVDQ5NI5MmS01698ltlAk0NoDBNLujRw==} + + pump@3.0.4: + resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==} + + qs@6.16.0: + resolution: {integrity: sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA==} + engines: {node: '>=0.6'} + + quick-format-unescaped@4.0.4: + resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} + + rc@1.2.8: + resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} + hasBin: true + + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + + real-require@0.2.0: + resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} + engines: {node: '>= 12.13.0'} + + real-require@1.0.0: + resolution: {integrity: sha512-P4nbQYQfePJxRSmY+v/KINxVucm4NF3p3s7pJveMTtom52FR4YGltUQLB8idDXwDDWW+eYrWDFbuzUnjoWHF7g==} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + ret@0.5.0: + resolution: {integrity: sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==} + engines: {node: '>=10'} + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rfdc@1.4.1: + resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + + rimraf@5.0.10: + resolution: {integrity: sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==} + hasBin: true + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safe-regex2@5.1.1: + resolution: {integrity: sha512-mOSBvHGDZMuIEZMdOz/aCEYDCv0E7nfcNsIhUF+/P+xC7Hyf3FkvymqgPbg9D1EdSGu+uKbJgy09K/RKKc7kJA==} + hasBin: true + + safe-stable-stringify@2.5.0: + resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} + engines: {node: '>=10'} + + secure-json-parse@4.1.0: + resolution: {integrity: sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==} + + semver@7.8.5: + resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} + engines: {node: '>=10'} + hasBin: true + + set-cookie-parser@2.7.2: + resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + side-channel-list@1.0.1: + resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.1: + resolution: {integrity: sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==} + engines: {node: '>= 0.4'} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + simple-concat@1.0.1: + resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==} + + simple-get@4.0.1: + resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==} + + sonic-boom@4.2.1: + resolution: {integrity: sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==} + + split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.2.0: + resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} + engines: {node: '>=12'} + + strip-json-comments@2.0.1: + resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} + engines: {node: '>=0.10.0'} + + tar-fs@2.1.5: + resolution: {integrity: sha512-OboTd8mmMhZDNPV+UjQcK9yKAatXu2aJ+r1w4im1Otd4M4fl2hwvdoXUxIYHFTHWK/3y3FarBP70v3vwmGlOxw==} + + tar-stream@2.2.0: + resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} + engines: {node: '>=6'} + + thread-stream@4.2.0: + resolution: {integrity: sha512-e2zZ96wSChazBsbENf/Pcm/4swHt2cEKQ92rhUjkL9GCKiTDJIaTBenjE/m9DXi0QBmTMDkFDdOomUy20A1tDQ==} + engines: {node: '>=20'} + + toad-cache@3.7.4: + resolution: {integrity: sha512-m1TdR/rvT7kgGJZhspNtXdsdYk0fddFpJJFlG5s+UkPFo6lkLoZ3YLOaovPYjq1R75NP5JfeTlSHaOsE09peCg==} + engines: {node: '>=20'} + + tunnel-agent@0.6.0: + resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} + + url-template@2.0.8: + resolution: {integrity: sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==} + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + web-streams-polyfill@3.3.3: + resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} + engines: {node: '>= 8'} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + +snapshots: + + '@fastify/ajv-compiler@4.0.6': + dependencies: + ajv: 8.20.0 + ajv-formats: 3.0.1(ajv@8.20.0) + fast-uri: 4.1.4 + + '@fastify/error@4.2.0': {} + + '@fastify/fast-json-stringify-compiler@5.1.0': + dependencies: + fast-json-stringify: 7.0.1 + + '@fastify/forwarded@3.0.2': {} + + '@fastify/merge-json-schemas@0.2.1': + dependencies: + dequal: 2.0.3 + + '@fastify/proxy-addr@5.1.0': + dependencies: + '@fastify/forwarded': 3.0.2 + ipaddr.js: 2.5.0 + + '@isaacs/cliui@8.0.2': + dependencies: + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.2.0 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 + + '@pinojs/redact@0.4.0': {} + + '@pkgjs/parseargs@0.11.0': + optional: true + + abstract-logging@2.0.1: {} + + agent-base@7.1.4: {} + + ajv-formats@3.0.1(ajv@8.20.0): + optionalDependencies: + ajv: 8.20.0 + + ajv@8.20.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.7 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + ansi-regex@5.0.1: {} + + ansi-regex@6.3.0: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@6.2.3: {} + + atomic-sleep@1.0.0: {} + + avvio@9.3.0: + dependencies: + '@fastify/error': 4.2.0 + fastq: 1.20.3 + + balanced-match@1.0.2: {} + + base64-js@1.5.1: {} + + better-sqlite3@12.11.1: + dependencies: + bindings: 1.5.0 + prebuild-install: 7.1.3 + + bignumber.js@9.3.1: {} + + bindings@1.5.0: + dependencies: + file-uri-to-path: 1.0.0 + + bl@4.1.0: + dependencies: + buffer: 5.7.1 + inherits: 2.0.4 + readable-stream: 3.6.2 + + boolbase@1.0.0: {} + + brace-expansion@2.1.4: + dependencies: + balanced-match: 1.0.2 + + buffer-equal-constant-time@1.0.1: {} + + buffer@5.7.1: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + chownr@1.1.4: {} + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + cookie@1.1.1: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + css-select@5.2.2: + dependencies: + boolbase: 1.0.0 + css-what: 6.2.2 + domhandler: 5.0.3 + domutils: 3.2.2 + nth-check: 2.1.1 + + css-what@6.2.2: {} + + data-uri-to-buffer@4.0.1: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + decompress-response@6.0.0: + dependencies: + mimic-response: 3.1.0 + + deep-extend@0.6.0: {} + + dequal@2.0.3: {} + + detect-libc@2.1.2: {} + + dom-serializer@2.0.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + entities: 4.5.0 + + domelementtype@2.3.0: {} + + domhandler@5.0.3: + dependencies: + domelementtype: 2.3.0 + + domutils@3.2.2: + dependencies: + dom-serializer: 2.0.0 + domelementtype: 2.3.0 + domhandler: 5.0.3 + + dotenv@17.4.2: {} + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + eastasianwidth@0.2.0: {} + + ecdsa-sig-formatter@1.0.11: + dependencies: + safe-buffer: 5.2.1 + + emoji-regex@8.0.0: {} + + emoji-regex@9.2.2: {} + + end-of-stream@1.4.5: + dependencies: + once: 1.4.0 + + entities@4.5.0: {} + + entities@8.1.0: {} + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-object-atoms@1.1.2: + dependencies: + es-errors: 1.3.0 + + expand-template@2.0.3: {} + + extend@3.0.2: {} + + fast-decode-uri-component@1.0.1: {} + + fast-deep-equal@3.1.3: {} + + fast-json-stringify@7.0.1: + dependencies: + '@fastify/merge-json-schemas': 0.2.1 + ajv: 8.20.0 + ajv-formats: 3.0.1(ajv@8.20.0) + fast-uri: 4.1.4 + json-schema-ref-resolver: 3.0.0 + rfdc: 1.4.1 + + fast-querystring@1.1.2: + dependencies: + fast-decode-uri-component: 1.0.1 + + fast-uri@3.1.7: {} + + fast-uri@4.1.4: {} + + fastify@5.12.4: + dependencies: + '@fastify/ajv-compiler': 4.0.6 + '@fastify/error': 4.2.0 + '@fastify/fast-json-stringify-compiler': 5.1.0 + '@fastify/proxy-addr': 5.1.0 + abstract-logging: 2.0.1 + avvio: 9.3.0 + fast-json-stringify: 7.0.1 + find-my-way: 9.9.0 + light-my-request: 6.6.0 + pino: 10.3.1 + process-warning: 5.1.0 + rfdc: 1.4.1 + secure-json-parse: 4.1.0 + semver: 7.8.5 + toad-cache: 3.7.4 + + fastq@1.20.3: + dependencies: + reusify: 1.1.0 + + fetch-blob@3.2.0: + dependencies: + node-domexception: 1.0.0 + web-streams-polyfill: 3.3.3 + + file-uri-to-path@1.0.0: {} + + find-my-way@9.9.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-querystring: 1.1.2 + safe-regex2: 5.1.1 + + foreground-child@3.3.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + + formdata-polyfill@4.0.10: + dependencies: + fetch-blob: 3.2.0 + + fs-constants@1.0.0: {} + + function-bind@1.1.2: {} + + gaxios@7.1.3: + dependencies: + extend: 3.0.2 + https-proxy-agent: 7.0.6 + node-fetch: 3.3.2 + rimraf: 5.0.10 + transitivePeerDependencies: + - supports-color + + gaxios@7.3.1: + dependencies: + extend: 3.0.2 + https-proxy-agent: 7.0.6 + node-fetch: 3.3.2 + transitivePeerDependencies: + - supports-color + + gcp-metadata@8.1.2: + dependencies: + gaxios: 7.3.1 + google-logging-utils: 1.1.3 + json-bigint: 1.0.0 + transitivePeerDependencies: + - supports-color + + gcp-metadata@8.1.4: + dependencies: + gaxios: 7.1.3 + google-logging-utils: 1.1.3 + json-bigint: 1.0.0 + transitivePeerDependencies: + - supports-color + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.4 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.2 + + github-from-package@0.0.0: {} + + glob@10.5.0: + dependencies: + foreground-child: 3.3.1 + jackspeak: 3.4.3 + minimatch: 9.0.9 + minipass: 7.1.3 + package-json-from-dist: 1.0.1 + path-scurry: 1.11.1 + + google-auth-library@10.5.0: + dependencies: + base64-js: 1.5.1 + ecdsa-sig-formatter: 1.0.11 + gaxios: 7.1.3 + gcp-metadata: 8.1.4 + google-logging-utils: 1.1.3 + gtoken: 8.0.0 + jws: 4.0.1 + transitivePeerDependencies: + - supports-color + + google-auth-library@10.9.1: + dependencies: + base64-js: 1.5.1 + ecdsa-sig-formatter: 1.0.11 + gaxios: 7.3.1 + gcp-metadata: 8.1.2 + google-logging-utils: 1.1.3 + jws: 4.0.1 + transitivePeerDependencies: + - supports-color + + google-logging-utils@1.1.3: {} + + googleapis-common@8.0.3: + dependencies: + extend: 3.0.2 + gaxios: 7.1.3 + google-auth-library: 10.5.0 + google-logging-utils: 1.1.3 + qs: 6.16.0 + url-template: 2.0.8 + transitivePeerDependencies: + - supports-color + + googleapis@159.0.0: + dependencies: + google-auth-library: 10.9.1 + googleapis-common: 8.0.3 + transitivePeerDependencies: + - supports-color + + gopd@1.2.0: {} + + gtoken@8.0.0: + dependencies: + gaxios: 7.1.3 + jws: 4.0.1 + transitivePeerDependencies: + - supports-color + + has-symbols@1.1.0: {} + + hasown@2.0.4: + dependencies: + function-bind: 1.1.2 + + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + ieee754@1.2.1: {} + + inherits@2.0.4: {} + + ini@1.3.8: {} + + ipaddr.js@2.5.0: {} + + is-fullwidth-code-point@3.0.0: {} + + isexe@2.0.0: {} + + jackspeak@3.4.3: + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + + json-bigint@1.0.0: + dependencies: + bignumber.js: 9.3.1 + + json-schema-ref-resolver@3.0.0: + dependencies: + dequal: 2.0.3 + + json-schema-traverse@1.0.0: {} + + jwa@2.0.1: + dependencies: + buffer-equal-constant-time: 1.0.1 + ecdsa-sig-formatter: 1.0.11 + safe-buffer: 5.2.1 + + jws@4.0.1: + dependencies: + jwa: 2.0.1 + safe-buffer: 5.2.1 + + light-my-request@6.6.0: + dependencies: + cookie: 1.1.1 + process-warning: 4.0.1 + set-cookie-parser: 2.7.2 + + lru-cache@10.4.3: {} + + math-intrinsics@1.1.0: {} + + mimic-response@3.1.0: {} + + minimatch@9.0.9: + dependencies: + brace-expansion: 2.1.4 + + minimist@1.2.8: {} + + minipass@7.1.3: {} + + mkdirp-classic@0.5.3: {} + + ms@2.1.3: {} + + napi-build-utils@2.0.0: {} + + node-abi@3.96.0: + dependencies: + semver: 7.8.5 + + node-domexception@1.0.0: {} + + node-fetch@3.3.2: + dependencies: + data-uri-to-buffer: 4.0.1 + fetch-blob: 3.2.0 + formdata-polyfill: 4.0.10 + + node-html-parser@9.0.4: + dependencies: + css-select: 5.2.2 + entities: 8.1.0 + + nth-check@2.1.1: + dependencies: + boolbase: 1.0.0 + + object-inspect@1.13.4: {} + + on-exit-leak-free@2.1.2: {} + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + package-json-from-dist@1.0.1: {} + + path-key@3.1.1: {} + + path-scurry@1.11.1: + dependencies: + lru-cache: 10.4.3 + minipass: 7.1.3 + + pino-abstract-transport@3.0.0: + dependencies: + split2: 4.2.0 + + pino-std-serializers@7.1.0: {} + + pino@10.3.1: + dependencies: + '@pinojs/redact': 0.4.0 + atomic-sleep: 1.0.0 + on-exit-leak-free: 2.1.2 + pino-abstract-transport: 3.0.0 + pino-std-serializers: 7.1.0 + process-warning: 5.1.0 + quick-format-unescaped: 4.0.4 + real-require: 0.2.0 + safe-stable-stringify: 2.5.0 + sonic-boom: 4.2.1 + thread-stream: 4.2.0 + + prebuild-install@7.1.3: + dependencies: + detect-libc: 2.1.2 + expand-template: 2.0.3 + github-from-package: 0.0.0 + minimist: 1.2.8 + mkdirp-classic: 0.5.3 + napi-build-utils: 2.0.0 + node-abi: 3.96.0 + pump: 3.0.4 + rc: 1.2.8 + simple-get: 4.0.1 + tar-fs: 2.1.5 + tunnel-agent: 0.6.0 + + process-warning@4.0.1: {} + + process-warning@5.1.0: {} + + pump@3.0.4: + dependencies: + end-of-stream: 1.4.5 + once: 1.4.0 + + qs@6.16.0: + dependencies: + es-define-property: 1.0.1 + side-channel: 1.1.1 + + quick-format-unescaped@4.0.4: {} + + rc@1.2.8: + dependencies: + deep-extend: 0.6.0 + ini: 1.3.8 + minimist: 1.2.8 + strip-json-comments: 2.0.1 + + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + + real-require@0.2.0: {} + + real-require@1.0.0: {} + + require-from-string@2.0.2: {} + + ret@0.5.0: {} + + reusify@1.1.0: {} + + rfdc@1.4.1: {} + + rimraf@5.0.10: + dependencies: + glob: 10.5.0 + + safe-buffer@5.2.1: {} + + safe-regex2@5.1.1: + dependencies: + ret: 0.5.0 + + safe-stable-stringify@2.5.0: {} + + secure-json-parse@4.1.0: {} + + semver@7.8.5: {} + + set-cookie-parser@2.7.2: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + side-channel-list@1.0.1: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.1: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.1 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + + signal-exit@4.1.0: {} + + simple-concat@1.0.1: {} + + simple-get@4.0.1: + dependencies: + decompress-response: 6.0.0 + once: 1.4.0 + simple-concat: 1.0.1 + + sonic-boom@4.2.1: + dependencies: + atomic-sleep: 1.0.0 + + split2@4.2.0: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.2.0 + + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.2.0: + dependencies: + ansi-regex: 6.3.0 + + strip-json-comments@2.0.1: {} + + tar-fs@2.1.5: + dependencies: + chownr: 1.1.4 + mkdirp-classic: 0.5.3 + pump: 3.0.4 + tar-stream: 2.2.0 + + tar-stream@2.2.0: + dependencies: + bl: 4.1.0 + end-of-stream: 1.4.5 + fs-constants: 1.0.0 + inherits: 2.0.4 + readable-stream: 3.6.2 + + thread-stream@4.2.0: + dependencies: + real-require: 1.0.0 + + toad-cache@3.7.4: {} + + tunnel-agent@0.6.0: + dependencies: + safe-buffer: 5.2.1 + + url-template@2.0.8: {} + + util-deprecate@1.0.2: {} + + web-streams-polyfill@3.3.3: {} + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@8.1.0: + dependencies: + ansi-styles: 6.2.3 + string-width: 5.1.2 + strip-ansi: 7.2.0 + + wrappy@1.0.2: {} diff --git a/apps/watcher/pnpm-workspace.yaml b/apps/watcher/pnpm-workspace.yaml new file mode 100644 index 0000000..65783dc --- /dev/null +++ b/apps/watcher/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +allowBuilds: + better-sqlite3: true diff --git a/apps/watcher/scripts/list-messages.js b/apps/watcher/scripts/list-messages.js new file mode 100644 index 0000000..75368fb --- /dev/null +++ b/apps/watcher/scripts/list-messages.js @@ -0,0 +1,140 @@ +#!/usr/bin/env node +/** + * List Gmail messages: id, from, subject (metadata only). + * + * Usage: + * npm run list-messages -w watcher + * npm run list-messages -w watcher -- --query 'newer_than:14d has:attachment filename:eml' + * npm run list-messages -w watcher -- --max 100 --json + * + * Options: + * --query Gmail search (default: newer_than:7d) + * --user Mailbox (default: first user with tokens) + * --max Max messages (default 50) + * --json Print JSON lines instead of a table + */ + +import { GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET } from "../src/config.js"; +import { getUserByEmail, listUsersWithTokens } from "../src/db.js"; +import { gmailClientForUser } from "../src/google.js"; +import { header } from "../src/mime.js"; + +const DEFAULT_QUERY = "newer_than:7d"; + +function parseArgs(argv) { + const out = { + query: null, + user: null, + max: 50, + json: false, + help: false, + }; + for (let i = 0; i < argv.length; i++) { + const a = argv[i]; + if (a === "--help" || a === "-h") out.help = true; + else if (a === "--json") out.json = true; + else if (a === "--query") out.query = argv[++i]; + else if (a === "--user") out.user = argv[++i]; + else if (a === "--max") out.max = Math.max(1, Number(argv[++i]) || 50); + else if (a.startsWith("--query=")) out.query = a.slice("--query=".length); + else if (a.startsWith("--user=")) out.user = a.slice("--user=".length); + else if (a.startsWith("--max=")) { + out.max = Math.max(1, Number(a.slice("--max=".length)) || 50); + } + } + return out; +} + +async function listMessageIds(gmail, query, max) { + const ids = []; + let pageToken; + while (ids.length < max) { + const { data } = await gmail.users.messages.list({ + userId: "me", + q: query, + maxResults: Math.min(50, max - ids.length), + pageToken, + }); + for (const m of data.messages || []) { + if (m.id) ids.push(m.id); + if (ids.length >= max) break; + } + pageToken = data.nextPageToken; + if (!pageToken) break; + } + return ids; +} + +function metaFromPayload(payload) { + return { + from: header(payload, "From") || "", + subject: header(payload, "Subject") || "(no subject)", + date: header(payload, "Date") || "", + }; +} + +async function main() { + const args = parseArgs(process.argv.slice(2)); + if (args.help) { + console.log(`Usage: list-messages [--query q] [--user email] [--max n] [--json] +Default query: ${DEFAULT_QUERY}`); + process.exit(0); + } + + if (!GOOGLE_CLIENT_ID || !GOOGLE_CLIENT_SECRET) { + console.error("Missing GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET in .env"); + process.exit(1); + } + + const users = listUsersWithTokens(); + if (!users.length) { + console.error("No signed-in users with tokens. Sign in via the web UI first."); + process.exit(1); + } + + let user = users[0]; + if (args.user) { + const byEmail = getUserByEmail(args.user); + const byId = users.find((u) => String(u.id) === String(args.user)); + user = byEmail || byId || null; + if (!user) { + console.error(`User not found: ${args.user}`); + process.exit(1); + } + } + + const gmail = await gmailClientForUser(user.id); + const query = args.query || DEFAULT_QUERY; + const ids = await listMessageIds(gmail, query, args.max); + + if (!args.json) { + console.error(`# mailbox=${user.email} query=${JSON.stringify(query)} count=${ids.length}`); + } + + for (const id of ids) { + const { data } = await gmail.users.messages.get({ + userId: "me", + id, + format: "metadata", + metadataHeaders: ["From", "Subject", "Date"], + }); + const meta = metaFromPayload(data.payload || {}); + const row = { + id, + from: meta.from, + subject: meta.subject, + date: meta.date, + }; + + if (args.json) { + console.log(JSON.stringify(row)); + } else { + console.log(`${row.id}\t${row.from}\t${row.subject}`); + } + } +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/apps/watcher/scripts/process-messages.js b/apps/watcher/scripts/process-messages.js new file mode 100644 index 0000000..21e43cb --- /dev/null +++ b/apps/watcher/scripts/process-messages.js @@ -0,0 +1,243 @@ +#!/usr/bin/env node +/** + * Manually process Gmail messages through match → parse → PocketBase save. + * + * Use when forwarding old receipts: Gmail Date may be "today", but trx_date + * comes from the receipt body (Tanggal/Jam, Picked up on, …). Existing + * invoice_id rows are skipped (unique constraint). + * + * Also expands .eml / message/rfc822 attachments (forward-as-attachment). + * + * Usage: + * npm run process-messages -w watcher -- --query 'newer_than:2d subject:"Pembayaran Berhasil!"' + * npm run process-messages -w watcher -- --ids 1a099bd61efd234d,1a099b8b9d239e5e + * npm run process-messages -w watcher -- --dry-run --max 20 + * + * Options: + * --query Gmail search (default targets Livin/Grab subjects, newer_than:7d) + * --ids Explicit message IDs (skips list search) + * --user Mailbox to use (default: first user with tokens) + * --max Max messages to process (default 50) + * --dry-run Parse and log only; do not write PocketBase + */ + +import { + GOOGLE_CLIENT_ID, + GOOGLE_CLIENT_SECRET, + POCKETBASE_URL, +} from "../src/config.js"; +import { getUserByEmail, listUsersWithTokens } from "../src/db.js"; +import { gmailClientForUser } from "../src/google.js"; +import { fetchAndLogMessage } from "../src/handlers/fetch-and-log-message.js"; +import { matchRule } from "../src/handlers/match-rule.js"; +import { parseMatchedMessage } from "../src/handlers/parse-matched-message.js"; +import { saveSpending } from "../src/handlers/save-spending.js"; +import { runPipeline } from "../src/pipeline.js"; +import { processFetchedMessage } from "../src/process-fetched.js"; +import { pocketbaseConfigured } from "../src/pocketbase/client.js"; + +const DEFAULT_QUERY = + 'newer_than:7d (subject:"Pembayaran Berhasil!" OR subject:"Your Grab E-Receipt" OR subject:"Pemindahan Dana Sesama Rekening BRI")'; + +function parseArgs(argv) { + const out = { + query: null, + ids: [], + user: null, + max: 50, + dryRun: false, + help: false, + }; + for (let i = 0; i < argv.length; i++) { + const a = argv[i]; + if (a === "--help" || a === "-h") out.help = true; + else if (a === "--dry-run") out.dryRun = true; + else if (a === "--query") out.query = argv[++i]; + else if (a === "--ids") { + out.ids = String(argv[++i] || "") + .split(",") + .map((s) => s.trim()) + .filter(Boolean); + } else if (a === "--user") out.user = argv[++i]; + else if (a === "--max") out.max = Math.max(1, Number(argv[++i]) || 50); + else if (a.startsWith("--query=")) out.query = a.slice("--query=".length); + else if (a.startsWith("--ids=")) { + out.ids = a + .slice("--ids=".length) + .split(",") + .map((s) => s.trim()) + .filter(Boolean); + } else if (a.startsWith("--user=")) out.user = a.slice("--user=".length); + else if (a.startsWith("--max=")) { + out.max = Math.max(1, Number(a.slice("--max=".length)) || 50); + } + } + return out; +} + +function makeLog() { + const line = (level, obj, msg) => { + const payload = + typeof obj === "string" ? { msg: obj } : { ...(obj || {}), msg }; + const { msg: message, ...rest } = payload; + console.log( + JSON.stringify({ + level, + time: new Date().toISOString(), + ...rest, + msg: message || msg, + }), + ); + }; + return { + info: (obj, msg) => line("info", obj, msg), + warn: (obj, msg) => line("warn", obj, msg), + error: (obj, msg) => line("error", obj, msg), + }; +} + +async function listMessageIds(gmail, query, max) { + const ids = []; + let pageToken; + while (ids.length < max) { + const { data } = await gmail.users.messages.list({ + userId: "me", + q: query, + maxResults: Math.min(50, max - ids.length), + pageToken, + }); + for (const m of data.messages || []) { + if (m.id) ids.push(m.id); + if (ids.length >= max) break; + } + pageToken = data.nextPageToken; + if (!pageToken) break; + } + return ids; +} + +async function main() { + const args = parseArgs(process.argv.slice(2)); + if (args.help) { + console.log(`Usage: process-messages [--query q] [--ids id1,id2] [--user email] [--max n] [--dry-run] +Default query: ${DEFAULT_QUERY}`); + process.exit(0); + } + + if (!GOOGLE_CLIENT_ID || !GOOGLE_CLIENT_SECRET) { + console.error("Missing GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET in .env"); + process.exit(1); + } + + const users = listUsersWithTokens(); + if (!users.length) { + console.error("No signed-in users with tokens. Sign in via the web UI first."); + process.exit(1); + } + + let user = users[0]; + if (args.user) { + const byEmail = getUserByEmail(args.user); + const byId = users.find((u) => String(u.id) === String(args.user)); + user = byEmail || byId || null; + if (!user) { + console.error(`User not found: ${args.user}`); + process.exit(1); + } + } + + const log = makeLog(); + const gmail = await gmailClientForUser(user.id); + + let messageIds = args.ids; + if (!messageIds.length) { + const query = args.query || DEFAULT_QUERY; + log.info({ query, max: args.max, user: user.email }, "process-messages: listing"); + messageIds = await listMessageIds(gmail, query, args.max); + } else { + messageIds = messageIds.slice(0, args.max); + } + + log.info( + { + count: messageIds.length, + user: user.email, + dryRun: args.dryRun, + pocketbase: pocketbaseConfigured() ? POCKETBASE_URL : null, + }, + "process-messages: start", + ); + + const fetchHandlers = [fetchAndLogMessage]; + const processHandlers = args.dryRun + ? [matchRule, parseMatchedMessage] + : [matchRule, parseMatchedMessage, saveSpending]; + + const summary = { + processed: 0, + matched: 0, + saved: 0, + duplicate: 0, + skipped: 0, + errors: 0, + }; + + for (const messageId of messageIds) { + try { + const fetched = await runPipeline(fetchHandlers, { + notification: { emailAddress: user.email, historyId: null }, + userId: user.id, + messageId, + gmail, + log, + }); + + if (!fetched.message) { + summary.skipped++; + continue; + } + + const results = await processFetchedMessage(fetched, processHandlers); + for (const result of results) { + summary.processed++; + if (!result.rule) { + summary.skipped++; + continue; + } + summary.matched++; + + if (args.dryRun) { + log.info( + { + messageId: result.messageId, + kind: result.parsed?.kind, + invoice_id: result.parsed?.reference, + amount: result.parsed?.amount, + occurredAt: result.parsed?.occurredAt, + }, + "process-messages: dry-run parsed", + ); + continue; + } + + if (result.saved) summary.saved++; + else if (result.duplicate) summary.duplicate++; + else if (result.error) summary.errors++; + else summary.skipped++; + } + } catch (err) { + summary.errors++; + log.error( + { messageId, err: err.message || String(err) }, + "process-messages: failed", + ); + } + } + + log.info(summary, "process-messages: done"); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/apps/watcher/src/config.js b/apps/watcher/src/config.js new file mode 100644 index 0000000..6ccbf4e --- /dev/null +++ b/apps/watcher/src/config.js @@ -0,0 +1,73 @@ +import { config } from "dotenv"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +export const rootDir = resolve(__dirname, "../../.."); +export const watcherDir = resolve(__dirname, ".."); +export const defaultSqlitePath = resolve(__dirname, "../../api/data/app.db"); + +config({ path: resolve(rootDir, ".env"), quiet: true }); + +export const PORT = Number(process.env.WATCHER_PORT || 3001); +export const NODE_ENV = process.env.NODE_ENV || "development"; +export const GOOGLE_CLIENT_ID = process.env.GOOGLE_CLIENT_ID || ""; +export const GOOGLE_CLIENT_SECRET = process.env.GOOGLE_CLIENT_SECRET || ""; +export const GOOGLE_REDIRECT_URI = + process.env.GOOGLE_REDIRECT_URI || "http://localhost:5173/callback"; +export const GOOGLE_CLOUD_PROJECT = + process.env.GOOGLE_CLOUD_PROJECT || process.env.GCLOUD_PROJECT || ""; +export const PUBSUB_VERIFICATION_TOKEN = + process.env.PUBSUB_VERIFICATION_TOKEN || ""; +export const SQLITE_PATH = process.env.SQLITE_PATH || defaultSqlitePath; + +export const POCKETBASE_URL = (process.env.POCKETBASE_URL || "").replace( + /\/$/, + "", +); +export const POCKETBASE_USER = process.env.POCKETBASE_USER || ""; +export const POCKETBASE_PASSWORD = process.env.POCKETBASE_PASSWORD || ""; + +/** ntfy topic URL, e.g. https://n.0dev.web.id/system */ +export const NTFY_URL = (process.env.NTFY_URL || "https://n.0dev.web.id/system").replace( + /\/$/, + "", +); + +export const WATCH_RENEW_MS = 12 * 60 * 60 * 1000; + +const PUBSUB_TOPIC_RE = /^projects\/[^/]+\/topics\/[^/]+$/; + +export function resolvePubsubTopic( + topic, + { projectId = GOOGLE_CLOUD_PROJECT } = {}, +) { + const trimmed = (topic || "").trim(); + if (!trimmed) return ""; + if (PUBSUB_TOPIC_RE.test(trimmed)) return trimmed; + if (trimmed.includes("/")) return trimmed; + if (!projectId) return trimmed; + return `projects/${projectId}/topics/${trimmed}`; +} + +export const GOOGLE_PUBSUB_TOPIC = resolvePubsubTopic( + process.env.GOOGLE_PUBSUB_TOPIC || "", +); + +export function assertConfig() { + const missing = []; + if (!GOOGLE_CLIENT_ID) missing.push("GOOGLE_CLIENT_ID"); + if (!GOOGLE_CLIENT_SECRET) missing.push("GOOGLE_CLIENT_SECRET"); + if (!GOOGLE_PUBSUB_TOPIC) missing.push("GOOGLE_PUBSUB_TOPIC"); + if (NODE_ENV === "production" && !PUBSUB_VERIFICATION_TOKEN) { + missing.push("PUBSUB_VERIFICATION_TOKEN"); + } + if (missing.length) { + throw new Error(`Missing required env: ${missing.join(", ")}`); + } + if (!PUBSUB_TOPIC_RE.test(GOOGLE_PUBSUB_TOPIC)) { + throw new Error( + `GOOGLE_PUBSUB_TOPIC must be a full Pub/Sub name like projects/my-gcp-project/topics/gmail-push (got "${process.env.GOOGLE_PUBSUB_TOPIC || ""}")`, + ); + } +} diff --git a/apps/watcher/src/db.js b/apps/watcher/src/db.js new file mode 100644 index 0000000..7e06544 --- /dev/null +++ b/apps/watcher/src/db.js @@ -0,0 +1,102 @@ +import { mkdirSync } from "node:fs"; +import { dirname } from "node:path"; +import Database from "better-sqlite3"; +import { SQLITE_PATH } from "./config.js"; + +mkdirSync(dirname(SQLITE_PATH), { recursive: true }); + +const db = new Database(SQLITE_PATH); +db.pragma("journal_mode = WAL"); +db.pragma("foreign_keys = ON"); +db.pragma("busy_timeout = 5000"); + +db.exec(` + CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + google_sub TEXT NOT NULL UNIQUE, + email TEXT NOT NULL, + name TEXT, + picture TEXT, + created_at TEXT NOT NULL DEFAULT (datetime('now')) + ); + + CREATE TABLE IF NOT EXISTS oauth_tokens ( + user_id INTEGER PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE, + access_token TEXT NOT NULL, + refresh_token TEXT, + expiry INTEGER NOT NULL + ); + + CREATE TABLE IF NOT EXISTS watch_state ( + user_id INTEGER PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE, + history_id TEXT NOT NULL, + expiration INTEGER NOT NULL + ); +`); + +const getUserByIdStmt = db.prepare(`SELECT * FROM users WHERE id = ?`); +const getUserByEmailStmt = db.prepare( + `SELECT * FROM users WHERE LOWER(email) = LOWER(?)`, +); +const getTokensStmt = db.prepare(`SELECT * FROM oauth_tokens WHERE user_id = ?`); +const listUsersWithTokensStmt = db.prepare(` + SELECT u.* + FROM users u + INNER JOIN oauth_tokens t ON t.user_id = u.id +`); +const saveTokensStmt = db.prepare(` + INSERT INTO oauth_tokens (user_id, access_token, refresh_token, expiry) + VALUES (@userId, @accessToken, @refreshToken, @expiry) + ON CONFLICT(user_id) DO UPDATE SET + access_token = excluded.access_token, + refresh_token = COALESCE(excluded.refresh_token, oauth_tokens.refresh_token), + expiry = excluded.expiry +`); +const getWatchStateStmt = db.prepare( + `SELECT * FROM watch_state WHERE user_id = ?`, +); +const saveWatchStateStmt = db.prepare(` + INSERT INTO watch_state (user_id, history_id, expiration) + VALUES (@userId, @historyId, @expiration) + ON CONFLICT(user_id) DO UPDATE SET + history_id = excluded.history_id, + expiration = excluded.expiration +`); + +export function getUserById(id) { + return getUserByIdStmt.get(id) || null; +} + +export function getUserByEmail(email) { + if (!email) return null; + return getUserByEmailStmt.get(email) || null; +} + +export function getTokens(userId) { + return getTokensStmt.get(userId) || null; +} + +export function listUsersWithTokens() { + return listUsersWithTokensStmt.all(); +} + +export function saveTokens(userId, { accessToken, refreshToken, expiry }) { + saveTokensStmt.run({ + userId, + accessToken, + refreshToken: refreshToken || null, + expiry, + }); +} + +export function getWatchState(userId) { + return getWatchStateStmt.get(userId) || null; +} + +export function saveWatchState(userId, { historyId, expiration }) { + saveWatchStateStmt.run({ + userId, + historyId: String(historyId), + expiration: Number(expiration), + }); +} diff --git a/apps/watcher/src/google.js b/apps/watcher/src/google.js new file mode 100644 index 0000000..34770ab --- /dev/null +++ b/apps/watcher/src/google.js @@ -0,0 +1,105 @@ +import { google } from "googleapis"; +import { + GOOGLE_CLIENT_ID, + GOOGLE_CLIENT_SECRET, + GOOGLE_PUBSUB_TOPIC, + GOOGLE_REDIRECT_URI, +} from "./config.js"; +import { getTokens, getWatchState, saveTokens, saveWatchState } from "./db.js"; +import { HttpError } from "./http.js"; + +export function createOAuthClient() { + return new google.auth.OAuth2( + GOOGLE_CLIENT_ID, + GOOGLE_CLIENT_SECRET, + GOOGLE_REDIRECT_URI, + ); +} + +function persistClientTokens(userId, client) { + client.on("tokens", (tokens) => { + if (!tokens.access_token) return; + saveTokens(userId, { + accessToken: tokens.access_token, + refreshToken: tokens.refresh_token || null, + expiry: tokens.expiry_date || Date.now() + 3600 * 1000, + }); + }); +} + +export async function gmailClientForUser(userId) { + const stored = getTokens(userId); + if (!stored?.access_token && !stored?.refresh_token) { + throw new HttpError(401, "Google account is not connected"); + } + + const client = createOAuthClient(); + client.setCredentials({ + access_token: stored.access_token, + refresh_token: stored.refresh_token, + expiry_date: stored.expiry, + }); + persistClientTokens(userId, client); + try { + await client.getAccessToken(); + } catch { + throw new HttpError( + 401, + "Gmail access expired or was revoked. Sign in again.", + ); + } + return google.gmail({ version: "v1", auth: client }); +} + +export async function startWatch(userId, { resetCursor = false } = {}) { + const gmail = await gmailClientForUser(userId); + const { data } = await gmail.users.watch({ + userId: "me", + requestBody: { + topicName: GOOGLE_PUBSUB_TOPIC, + labelIds: ["INBOX"], + }, + }); + + const existing = getWatchState(userId); + const historyId = + resetCursor || !existing?.history_id + ? String(data.historyId) + : existing.history_id; + + saveWatchState(userId, { + historyId, + expiration: Number(data.expiration) || Date.now() + 7 * 24 * 3600 * 1000, + }); + + return { ...data, historyId }; +} + +export async function listHistory(gmail, startHistoryId) { + const messageIds = new Set(); + let pageToken; + let latestHistoryId = String(startHistoryId); + + do { + const { data } = await gmail.users.history.list({ + userId: "me", + startHistoryId: String(startHistoryId), + pageToken, + historyTypes: ["messageAdded"], + }); + if (data.historyId) latestHistoryId = String(data.historyId); + for (const record of data.history || []) { + for (const added of record.messagesAdded || []) { + const id = added.message?.id; + if (id) messageIds.add(id); + } + } + pageToken = data.nextPageToken; + } while (pageToken); + + return { messageIds: [...messageIds], historyId: latestHistoryId }; +} + +export function googleStatus(err) { + return Number(err.code || err.response?.status || err.statusCode || 0); +} diff --git a/apps/watcher/src/handlers/fetch-and-log-message.js b/apps/watcher/src/handlers/fetch-and-log-message.js new file mode 100644 index 0000000..f60fc3d --- /dev/null +++ b/apps/watcher/src/handlers/fetch-and-log-message.js @@ -0,0 +1,76 @@ +import { + extractBody, + extractHtml, + hydrateRfc822Message, + listRfc822Parts, + summarizeMessage, +} from "../mime.js"; +import { googleStatus } from "../google.js"; + +const BODY_PREVIEW = 500; + +export async function fetchAndLogMessage(ctx) { + const { gmail, messageId, log } = ctx; + try { + const { data } = await gmail.users.messages.get({ + userId: "me", + id: messageId, + format: "full", + }); + const message = { + ...summarizeMessage(data), + body: extractBody(data.payload), + html: extractHtml(data.payload), + }; + + const rfcParts = listRfc822Parts(data.payload); + const emlMessages = []; + for (let index = 0; index < rfcParts.length; index++) { + const part = rfcParts[index]; + const hydrated = await hydrateRfc822Message(gmail, message.id, part, index); + emlMessages.push(hydrated); + } + + const emptyEmls = emlMessages.filter((m) => !m.body && !m.html).length; + + log.info( + { + messageId, + from: message.from, + subject: message.subject, + date: message.date, + snippet: message.snippet, + bodyPreview: (message.body || "").slice(0, BODY_PREVIEW), + emlCount: emlMessages.length, + emptyEmlCount: emptyEmls || undefined, + }, + "gmail-watch: message", + ); + + if (emlMessages.length) { + log.info( + { + messageId, + emlCount: emlMessages.length, + filenames: emlMessages.map((m) => m.attachmentFilename), + }, + "gmail-watch: eml attachments", + ); + } + + return { + pass: { + message, + emlMessages, + gmailPayload: data.payload, + }, + }; + } catch (err) { + const status = googleStatus(err); + if (status === 404) { + log.warn({ messageId }, "gmail-watch: message not found"); + return { break: true }; + } + throw err; + } +} diff --git a/apps/watcher/src/handlers/log-event.js b/apps/watcher/src/handlers/log-event.js new file mode 100644 index 0000000..d47989f --- /dev/null +++ b/apps/watcher/src/handlers/log-event.js @@ -0,0 +1,12 @@ +export async function logEvent(ctx) { + ctx.log.info( + { + emailAddress: ctx.notification?.emailAddress, + historyId: ctx.notification?.historyId, + messageId: ctx.messageId, + userId: ctx.userId, + }, + "gmail-watch: event", + ); + return { pass: { loggedAt: Date.now() } }; +} diff --git a/apps/watcher/src/handlers/match-rule.js b/apps/watcher/src/handlers/match-rule.js new file mode 100644 index 0000000..399dfaa --- /dev/null +++ b/apps/watcher/src/handlers/match-rule.js @@ -0,0 +1,58 @@ +import { extractEmail, resolveOriginal, subjectMatches } from "../lib/email.js"; +import { rules } from "../rules/index.js"; + +/** + * Match a fetched message against known receipt rules using original From + Subject. + * Breaks the pipeline when nothing matches. + */ +export async function matchRule(ctx) { + const { message, log, messageId } = ctx; + if (!message) { + log.warn({ messageId }, "gmail-watch: match skipped (no message)"); + return { break: true }; + } + + const original = resolveOriginal(message); + const fromEmail = extractEmail(original.from); + + const rule = rules.find( + (r) => + fromEmail === r.fromAddress.toLowerCase() && + subjectMatches(original.subject, r.subject), + ); + + if (!rule) { + log.info( + { + messageId, + from: original.from, + subject: original.subject, + viaForward: original.viaForward, + }, + "gmail-watch: no rule match", + ); + return { break: true }; + } + + log.info( + { + messageId, + kind: rule.kind, + from: original.from, + subject: original.subject, + viaForward: original.viaForward, + }, + "gmail-watch: rule matched", + ); + + return { + pass: { + rule, + original: { + from: original.from, + subject: original.subject, + date: original.date, + }, + }, + }; +} diff --git a/apps/watcher/src/handlers/parse-matched-message.js b/apps/watcher/src/handlers/parse-matched-message.js new file mode 100644 index 0000000..404246d --- /dev/null +++ b/apps/watcher/src/handlers/parse-matched-message.js @@ -0,0 +1,76 @@ +import { parseBriTransfer } from "../parsers/bri.js"; +import { parseGrabReceipt } from "../parsers/grab.js"; +import { parseLivinReceipt } from "../parsers/livin.js"; +import { notifyReceiptIssue } from "../notify/ntfy.js"; + +const PARSERS = { + "livin.qr_payment.success": parseLivinReceipt, + "grab.ereceipt.ride": parseGrabReceipt, + "bri.transfer.success": parseBriTransfer, +}; + +/** + * Parse a matched message body into the shared envelope JSON. + * On failure, logs a warning, notifies ntfy, and breaks (Pub/Sub stays healthy). + */ +export async function parseMatchedMessage(ctx) { + const { message, rule, original, log, messageId, notification } = ctx; + const parse = PARSERS[rule?.kind]; + if (!parse) { + log.warn({ messageId, kind: rule?.kind }, "gmail-watch: no parser for kind"); + await notifyReceiptIssue( + { + reason: "no parser for kind", + messageId, + kind: rule?.kind, + message, + }, + log, + ); + return { break: true }; + } + + const fields = parse(message.body || "", { html: message.html || "" }); + if (!fields) { + log.warn( + { messageId, kind: rule.kind }, + "gmail-watch: parse failed", + ); + await notifyReceiptIssue( + { + reason: "parse failed", + messageId, + kind: rule.kind, + message, + }, + log, + ); + return { break: true }; + } + + // Grab tip shares From/Subject with ride; parser sets kind/reference. + const kind = fields.kind || rule.kind; + + const parsed = { + kind, + messageId, + mailbox: notification?.emailAddress || "", + matched: { + from: message.from || "", + subject: message.subject || "", + }, + original: { + from: original?.from || "", + subject: original?.subject || "", + date: original?.date || "", + }, + amount: fields.amount, + currency: fields.currency || "IDR", + reference: fields.reference, + occurredAt: fields.occurredAt, + details: fields.details || {}, + }; + + log.info({ messageId, kind: parsed.kind, parsed }, "gmail-watch: parsed"); + return { pass: { parsed } }; +} diff --git a/apps/watcher/src/handlers/save-spending.js b/apps/watcher/src/handlers/save-spending.js new file mode 100644 index 0000000..5a09dc3 --- /dev/null +++ b/apps/watcher/src/handlers/save-spending.js @@ -0,0 +1,126 @@ +import { pocketbaseConfigured, createRecord } from "../pocketbase/client.js"; +import { toSpendingRecord } from "../pocketbase/map.js"; +import { notifyReceiptIssue } from "../notify/ntfy.js"; + +/** + * Insert a parsed receipt into PocketBase spendings. + * Unique invoice_id conflicts are logged and skipped (no throw). + * Non-duplicate skips / errors notify ntfy. + */ +export async function saveSpending(ctx) { + const { parsed, rule, log, messageId, message } = ctx; + + if (!parsed) { + log.warn({ messageId }, "gmail-watch: save skipped (no parsed)"); + await notifyReceiptIssue( + { + reason: "save skipped (no parsed)", + messageId, + message, + }, + log, + ); + return { break: true }; + } + + if (!pocketbaseConfigured()) { + log.warn( + { messageId, kind: parsed.kind }, + "gmail-watch: pocketbase not configured; parsed only", + ); + await notifyReceiptIssue( + { + reason: "pocketbase not configured", + messageId, + kind: parsed.kind, + message, + extra: { parsed }, + }, + log, + ); + return { pass: { saved: false } }; + } + + const record = toSpendingRecord(parsed, rule); + if (!record.invoice_id || record.amount == null || !record.trx_date) { + log.warn( + { messageId, kind: parsed.kind, record }, + "gmail-watch: save skipped (incomplete record)", + ); + await notifyReceiptIssue( + { + reason: "save skipped (incomplete record)", + messageId, + kind: parsed.kind, + message, + record, + extra: { parsed }, + }, + log, + ); + return { break: true }; + } + + try { + const { ok, status, data } = await createRecord("spendings", record); + if (ok) { + log.info( + { + messageId, + kind: parsed.kind, + invoice_id: record.invoice_id, + recordId: data.id, + }, + "gmail-watch: spending saved", + ); + return { pass: { saved: true, spendingId: data.id } }; + } + + // Unique constraint / duplicate — no ntfy + const msg = String(data?.message || ""); + const isUnique = + status === 400 && + (/unique|invoice_id/i.test(msg) || + data?.data?.invoice_id || + JSON.stringify(data).includes("unique")); + + if (isUnique) { + log.info( + { messageId, invoice_id: record.invoice_id, status }, + "gmail-watch: spending duplicate skipped", + ); + return { pass: { saved: false, duplicate: true } }; + } + + log.error( + { messageId, status, data }, + "gmail-watch: spending save failed", + ); + await notifyReceiptIssue( + { + reason: "spending save failed", + messageId, + kind: parsed.kind, + message, + record, + extra: { status, data }, + }, + log, + ); + return { pass: { saved: false, error: true } }; + } catch (err) { + log.error({ err, messageId }, "gmail-watch: spending save error"); + await notifyReceiptIssue( + { + reason: "spending save error", + messageId, + kind: parsed.kind, + message, + record, + extra: { error: err.message || String(err) }, + }, + log, + ); + return { pass: { saved: false, error: true } }; + } +} diff --git a/apps/watcher/src/http.js b/apps/watcher/src/http.js new file mode 100644 index 0000000..e460601 --- /dev/null +++ b/apps/watcher/src/http.js @@ -0,0 +1,6 @@ +export class HttpError extends Error { + constructor(statusCode, message) { + super(message); + this.statusCode = statusCode; + } +} diff --git a/apps/watcher/src/index.js b/apps/watcher/src/index.js new file mode 100644 index 0000000..048d08f --- /dev/null +++ b/apps/watcher/src/index.js @@ -0,0 +1,92 @@ +import Fastify from "fastify"; +import { GOOGLE_PUBSUB_TOPIC, PORT, WATCH_RENEW_MS, assertConfig } from "./config.js"; +import { listUsersWithTokens } from "./db.js"; +import { startWatch } from "./google.js"; +import { fetchAndLogMessage } from "./handlers/fetch-and-log-message.js"; +import { logEvent } from "./handlers/log-event.js"; +import { matchRule } from "./handlers/match-rule.js"; +import { parseMatchedMessage } from "./handlers/parse-matched-message.js"; +import { saveSpending } from "./handlers/save-spending.js"; +import { HttpError } from "./http.js"; +import { pocketbaseConfigured } from "./pocketbase/client.js"; +import { ensureSpendingsSchema } from "./pocketbase/schema.js"; +import { registerPubsubRoutes } from "./routes/pubsub.js"; + +assertConfig(); + +const fetchHandlers = [logEvent, fetchAndLogMessage]; +const processHandlers = [matchRule, parseMatchedMessage, saveSpending]; + +const app = Fastify({ + logger: true, + trustProxy: true, +}); + +app.get("/health", async () => ({ ok: true })); + +await registerPubsubRoutes(app, { fetchHandlers, processHandlers }); + +app.setErrorHandler((err, request, reply) => { + const status = err.statusCode || 500; + if (status >= 500) { + request.log.error(err); + } + const message = + err instanceof HttpError || status < 500 + ? err.message + : "Internal server error"; + reply.code(status).send({ error: message }); +}); + +async function renewAllWatches() { + const users = listUsersWithTokens(); + if (!users.length) { + app.log.warn("gmail-watch: no signed-in users with tokens"); + return; + } + for (const user of users) { + try { + await startWatch(user.id); + app.log.info( + { userId: user.id, email: user.email }, + "gmail-watch: watch started", + ); + } catch (err) { + app.log.error({ err, userId: user.id, email: user.email }, "gmail-watch: watch failed"); + } + } +} + +let renewTimer; +app.addHook("onClose", async () => { + if (renewTimer) clearInterval(renewTimer); +}); + +try { + await app.listen({ port: PORT, host: "0.0.0.0" }); + app.log.info( + { topic: GOOGLE_PUBSUB_TOPIC }, + `Watcher listening on http://localhost:${PORT}`, + ); + + if (pocketbaseConfigured()) { + try { + await ensureSpendingsSchema(app.log); + } catch (err) { + app.log.error({ err }, "pocketbase: schema ensure failed"); + } + } else { + app.log.warn("pocketbase: not configured; spendings will not be saved"); + } + + await renewAllWatches(); + renewTimer = setInterval(() => { + renewAllWatches().catch((err) => { + app.log.error({ err }, "gmail-watch: renew cycle failed"); + }); + }, WATCH_RENEW_MS); + renewTimer.unref?.(); +} catch (err) { + app.log.error(err); + process.exit(1); +} diff --git a/apps/watcher/src/lib/email.js b/apps/watcher/src/lib/email.js new file mode 100644 index 0000000..76f8be1 --- /dev/null +++ b/apps/watcher/src/lib/email.js @@ -0,0 +1,66 @@ +/** + * Extract a bare email address from a From header value. + * e.g. `"Livin'" ` → `noreply.livin@bankmandiri.co.id` + */ +export function extractEmail(from) { + if (!from) return ""; + const angle = from.match(/<([^>]+)>/); + if (angle) return angle[1].trim().toLowerCase(); + const bare = from.match(/[\w.+-]+@[\w.-]+\.\w+/); + return bare ? bare[0].toLowerCase() : ""; +} + +/** + * Parse a Gmail "Forwarded message" header block from the body. + * Returns null if no forwarded header is present. + */ +export function parseForwardedHeader(body) { + if (!body) return null; + const marker = /-{2,}\s*Forwarded message\s*-{2,}/i; + const idx = body.search(marker); + if (idx < 0) return null; + + const after = body.slice(idx); + const headerMatch = after.match( + /From:\s*(.+)\r?\nDate:\s*(.+)\r?\nSubject:\s*(.+)\r?\nTo:\s*(.+)/i, + ); + if (!headerMatch) return null; + + return { + from: headerMatch[1].trim(), + date: headerMatch[2].trim(), + subject: headerMatch[3].trim(), + to: headerMatch[4].trim(), + }; +} + +/** + * Resolve the original sender/subject/date for matching. + * Prefer forwarded header when present; otherwise use the Gmail envelope. + */ +export function resolveOriginal(message) { + const forwarded = parseForwardedHeader(message?.body || ""); + if (forwarded) { + return { + from: forwarded.from, + subject: forwarded.subject, + date: forwarded.date, + viaForward: true, + }; + } + return { + from: message?.from || "", + subject: message?.subject || "", + date: message?.date || "", + viaForward: false, + }; +} + +export function subjectMatches(actual, expected) { + if (!actual || !expected) return false; + const a = actual.trim().toLowerCase(); + const e = expected.trim().toLowerCase(); + // Strip common Fwd:/Re: prefixes for comparison of the core subject + const strip = (s) => s.replace(/^(fwd|re):\s*/i, "").trim(); + return a === e || strip(a) === strip(e) || a.includes(e); +} diff --git a/apps/watcher/src/lib/parse-helpers.js b/apps/watcher/src/lib/parse-helpers.js new file mode 100644 index 0000000..9bc96ab --- /dev/null +++ b/apps/watcher/src/lib/parse-helpers.js @@ -0,0 +1,168 @@ +/** Parse Indonesian Rupiah strings like "Rp 22.000,00" or "Rp16.600" into integers. */ +export function parseIdrAmount(raw) { + if (raw == null) return null; + const s = String(raw).replace(/\s/g, ""); + const match = s.match(/Rp?\s*([\d.]+(?:,\d{2})?)/i) || s.match(/([\d.]+(?:,\d{2})?)/); + if (!match) return null; + // Indonesian: dots are thousands separators, comma is decimal + const whole = match[1].replace(/\./g, "").replace(/,\d+$/, ""); + const n = Number(whole); + return Number.isFinite(n) ? n : null; +} + +const MONTHS = { + // English + jan: 1, + january: 1, + feb: 2, + february: 2, + mar: 3, + march: 3, + apr: 4, + april: 4, + may: 5, + jun: 6, + june: 6, + jul: 7, + july: 7, + aug: 8, + august: 8, + sep: 9, + sept: 9, + september: 9, + oct: 10, + october: 10, + nov: 11, + november: 11, + dec: 12, + december: 12, + // Indonesian (Livin uses e.g. "15 Agu 2026") + januari: 1, + februari: 2, + maret: 3, + mei: 5, + juni: 6, + juli: 7, + agustus: 8, + agu: 8, + ags: 8, + oktober: 10, + okt: 10, + nop: 11, + desember: 12, + des: 12, +}; + +/** + * Parse dates like "13 Sep 2026", "13 September 2026", or Indonesian + * "15 Agu 2026" into YYYY-MM-DD. Also accepts 2-digit years ("13 Sep 26"). + */ +export function parseDayMonthYear(raw) { + if (!raw) return null; + const m = String(raw) + .trim() + .match(/(\d{1,2})\s+([A-Za-z]+)\s+(\d{2,4})\b/); + if (!m) return null; + const day = Number(m[1]); + const month = MONTHS[m[2].toLowerCase()]; + let year = Number(m[3]); + if (m[3].length === 2) year += year >= 70 ? 1900 : 2000; + if (!month || !day || !year) return null; + return `${year}-${String(month).padStart(2, "0")}-${String(day).padStart(2, "0")}`; +} + +/** + * Parse Grab tip timestamps like "13 Sep 26 10:47 +0700" → ISO with offset. + */ +export function parseGrabDateTime(raw) { + if (!raw) return null; + const m = String(raw) + .trim() + .match( + /(\d{1,2})\s+([A-Za-z]+)\s+(\d{2,4})\s+(\d{1,2}):(\d{2})(?::(\d{2}))?\s*([+-]\d{4})?/, + ); + if (!m) return null; + const dateYmd = parseDayMonthYear(`${m[1]} ${m[2]} ${m[3]}`); + if (!dateYmd) return null; + const hh = String(Number(m[4])).padStart(2, "0"); + const mm = m[5]; + const ss = m[6] || "00"; + const offset = m[7] + ? `${m[7].slice(0, 3)}:${m[7].slice(3)}` + : "+07:00"; + return `${dateYmd}T${hh}:${mm}:${ss}${offset}`; +} + +/** + * Parse time like "10:43:51 WIB" into HH:MM:SS. + */ +export function parseTimeWib(raw) { + if (!raw) return null; + const m = String(raw).trim().match(/(\d{1,2}):(\d{2})(?::(\d{2}))?/); + if (!m) return null; + const hh = String(Number(m[1])).padStart(2, "0"); + const mm = m[2]; + const ss = m[3] || "00"; + return `${hh}:${mm}:${ss}`; +} + +/** Combine YYYY-MM-DD + HH:MM:SS as WIB (+07:00) ISO string. */ +export function toWibIso(dateYmd, timeHms) { + if (!dateYmd) return null; + if (!timeHms) return dateYmd; + return `${dateYmd}T${timeHms}+07:00`; +} + +/** + * Parse "10:47AM" / "10:47 AM" into 24h HH:MM:SS. + */ +export function parseAmPmTime(raw) { + if (!raw) return null; + const m = String(raw) + .trim() + .match(/^(\d{1,2}):(\d{2})(?::(\d{2}))?\s*([AaPp][Mm])$/); + if (!m) return null; + let hour = Number(m[1]); + const minute = m[2]; + const second = m[3] || "00"; + const ap = m[4].toUpperCase(); + if (ap === "AM") { + if (hour === 12) hour = 0; + } else if (hour !== 12) { + hour += 12; + } + return `${String(hour).padStart(2, "0")}:${minute}:${second}`; +} + +/** + * Capture value after a label, allowing optional whitespace between label and value + * (including jammed HTML-stripped forms like Tanggal13 Sep 2026). + */ +export function captureAfterLabel(body, label, { until } = {}) { + const escaped = label.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const untilPart = until + ? `(?=${until.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")})` + : "(?=\\n|$)"; + const re = new RegExp(`${escaped}\\s*([^\\n]*?)${untilPart}`, "i"); + const m = body.match(re); + return m ? m[1].trim() : null; +} + +/** + * Capture a multi-line block after a label until the next known label or blank run. + */ +export function captureBlockAfterLabel(body, label, nextLabels = []) { + const escaped = label.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const next = + nextLabels.length > 0 + ? nextLabels.map((l) => l.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|") + : null; + const until = next ? `(?=${next}|$)` : "$"; + const re = new RegExp(`${escaped}\\s*([\\s\\S]*?)${until}`, "i"); + const m = body.match(re); + if (!m) return null; + return m[1] + .split(/\r?\n/) + .map((l) => l.trim()) + .filter(Boolean); +} diff --git a/apps/watcher/src/mime.js b/apps/watcher/src/mime.js new file mode 100644 index 0000000..b09b19d --- /dev/null +++ b/apps/watcher/src/mime.js @@ -0,0 +1,256 @@ +function decodeBase64Url(data) { + if (!data) return ""; + const pad = data.length % 4 === 0 ? "" : "=".repeat(4 - (data.length % 4)); + return Buffer.from( + data.replace(/-/g, "+").replace(/_/g, "/") + pad, + "base64", + ).toString("utf8"); +} + +function stripHtml(html) { + return html + .replace(//gi, "") + .replace(//gi, "") + .replace(//gi, "\n") + .replace(/<\/p>/gi, "\n\n") + .replace(/<\/div>/gi, "\n") + .replace(/<\/tr>/gi, "\n") + .replace(/<[^>]+>/g, "") + .replace(/ /g, " ") + .replace(/&/g, "&") + .replace(/</g, "<") + .replace(/>/g, ">") + .replace(/"/g, '"') + .replace(/'/g, "'") + .replace(/'/gi, "'") + .replace(/\n{3,}/g, "\n\n") + .trim(); +} + +function walkParts(payload, acc = { text: "", html: "" }) { + if (!payload) return acc; + const mime = payload.mimeType || ""; + const data = payload.body?.data; + if (data) { + if (mime === "text/plain") acc.text += decodeBase64Url(data); + else if (mime === "text/html") acc.html += decodeBase64Url(data); + } + for (const part of payload.parts || []) { + walkParts(part, acc); + } + return acc; +} + +/** Return both MIME text and html parts (decoded). */ +export function extractMimeParts(payload) { + const { text, html } = walkParts(payload); + return { + text: text.trim(), + html: html.trim(), + }; +} + +export function extractBody(payload) { + const { text, html } = extractMimeParts(payload); + if (text) return text; + if (html) return stripHtml(html); + return ""; +} + +export function extractHtml(payload) { + return extractMimeParts(payload).html; +} + +export function header(payload, name) { + const match = payload?.headers?.find( + (h) => h.name?.toLowerCase() === name.toLowerCase(), + ); + return match?.value || ""; +} + +export function summarizeMessage(message) { + const payload = message.payload || {}; + return { + id: message.id, + threadId: message.threadId, + from: header(payload, "From"), + to: header(payload, "To"), + subject: header(payload, "Subject") || "(no subject)", + date: header(payload, "Date"), + snippet: message.snippet || "", + }; +} + +function isRfc822Part(part) { + if (!part) return false; + const mime = (part.mimeType || "").toLowerCase(); + const filename = (part.filename || "").toLowerCase(); + return mime === "message/rfc822" || filename.endsWith(".eml"); +} + +/** + * Collect message/rfc822 (or *.eml) attachment parts. Does not recurse into + * nested .eml inside .eml — only direct attachment parts under the tree. + */ +export function listRfc822Parts(payload, acc = []) { + if (!payload) return acc; + for (const part of payload.parts || []) { + if (isRfc822Part(part)) { + acc.push(part); + continue; // do not recurse into nested eml + } + listRfc822Parts(part, acc); + } + return acc; +} + +function findNestedHeaderSource(part) { + if (!part) return null; + if (header(part, "From") || header(part, "Subject")) return part; + for (const child of part.parts || []) { + const found = findNestedHeaderSource(child); + if (found) return found; + } + return null; +} + +/** + * Build a synthetic message from a Gmail-expanded message/rfc822 part. + * When nested text/html is only available via attachmentId (common on large + * batches), call hydrateRfc822Message instead. + */ +export function messageFromRfc822Part(part, { parentMessageId, index = 0 } = {}) { + const headerSource = findNestedHeaderSource(part) || part; + const partId = part.partId || String(index); + const id = parentMessageId ? `${parentMessageId}#${partId}` : partId; + return { + id, + threadId: null, + from: header(headerSource, "From"), + to: header(headerSource, "To"), + subject: header(headerSource, "Subject") || "(no subject)", + date: header(headerSource, "Date"), + snippet: "", + body: extractBody(part), + html: extractHtml(part), + attachmentFilename: part.filename || "", + parentMessageId: parentMessageId || null, + _rfc822Part: part, + }; +} + +/** Collect text/plain + text/html nodes that need attachments.get. */ +export function listNestedBodyAttachmentRefs(part, acc = []) { + if (!part) return acc; + const mime = (part.mimeType || "").toLowerCase(); + const attachId = part.body?.attachmentId; + if (attachId && (mime === "text/plain" || mime === "text/html") && !part.body?.data) { + acc.push({ mime, attachmentId: attachId }); + } + for (const child of part.parts || []) { + listNestedBodyAttachmentRefs(child, acc); + } + return acc; +} + +/** + * Decode a simple multipart .eml (no nested multipart recursion beyond one level + * of multipart/*) into { text, html }. Used when only the top-level rfc822 blob + * is downloadable. + */ +export function extractMimePartsFromRawEml(raw) { + if (!raw) return { text: "", html: "" }; + let text = ""; + let html = ""; + + // Split on MIME boundaries (best-effort) + const boundaryMatch = raw.match(/boundary="?([^";\r\n]+)"?/i); + const chunks = boundaryMatch + ? raw.split(new RegExp(`\\r?\\n--${boundaryMatch[1].replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?:--)?\\r?\\n`)) + : [raw]; + + for (const chunk of chunks) { + const headerEnd = chunk.search(/\r?\n\r?\n/); + if (headerEnd < 0) continue; + const headers = chunk.slice(0, headerEnd); + let body = chunk.slice(headerEnd).replace(/^\r?\n\r?\n/, ""); + const ct = headers.match(/Content-Type:\s*([^\r\n;]+)/i)?.[1]?.trim().toLowerCase() || ""; + const cte = headers.match(/Content-Transfer-Encoding:\s*([^\r\n]+)/i)?.[1]?.trim().toLowerCase() || ""; + + if (cte === "base64") { + body = Buffer.from(body.replace(/\s+/g, ""), "base64").toString("utf8"); + } else if (cte === "quoted-printable") { + body = body + .replace(/=\r?\n/g, "") + .replace(/=([0-9A-Fa-f]{2})/g, (_, h) => String.fromCharCode(parseInt(h, 16))); + } + + if (ct === "text/plain") text += body; + else if (ct === "text/html") html += body; + } + + return { text: text.trim(), html: html.trim() }; +} + +/** + * Ensure an rfc822-derived message has body/html. Fetches nested Gmail + * attachments when Gmail did not inline the content. + */ +export async function hydrateRfc822Message(gmail, parentMessageId, part, index = 0) { + const message = messageFromRfc822Part(part, { parentMessageId, index }); + if (message.body || message.html) { + delete message._rfc822Part; + return message; + } + + const refs = listNestedBodyAttachmentRefs(part); + let text = ""; + let html = ""; + + for (const ref of refs) { + const { data } = await gmail.users.messages.attachments.get({ + userId: "me", + messageId: parentMessageId, + id: ref.attachmentId, + }); + const decoded = decodeBase64Url(data.data); + if (ref.mime === "text/plain") text += decoded; + else if (ref.mime === "text/html") html += decoded; + } + + // Fallback: download the whole .eml blob + if (!text && !html && part.body?.attachmentId) { + const { data } = await gmail.users.messages.attachments.get({ + userId: "me", + messageId: parentMessageId, + id: part.body.attachmentId, + }); + const raw = decodeBase64Url(data.data); + const parts = extractMimePartsFromRawEml(raw); + text = parts.text; + html = parts.html; + } + + message.html = html.trim(); + message.body = text.trim() || (html ? stripHtml(html) : ""); + delete message._rfc822Part; + return message; +} + +/** + * Expand a fetched Gmail payload into work items: + * - if .eml / message/rfc822 attachments exist → those only + * - else → the outer message + */ +export function expandWorkItems(outerMessage, payload) { + const parts = listRfc822Parts(payload); + if (!parts.length) return [outerMessage]; + return parts.map((part, index) => + messageFromRfc822Part(part, { + parentMessageId: outerMessage.id, + index, + }), + ); +} + + diff --git a/apps/watcher/src/notify/ntfy.js b/apps/watcher/src/notify/ntfy.js new file mode 100644 index 0000000..31b646e --- /dev/null +++ b/apps/watcher/src/notify/ntfy.js @@ -0,0 +1,102 @@ +import { NTFY_URL } from "../config.js"; + +/** + * POST a notification to ntfy. Never throws — failures are logged only. + */ +export async function notifyNtfy( + { + title, + message, + priority = "default", + tags = [], + }, + log = console, +) { + if (!NTFY_URL) return { skipped: true }; + + try { + const headers = { + Title: title.slice(0, 250), + Priority: String(priority), + "Content-Type": "text/plain; charset=utf-8", + }; + if (tags.length) headers.Tags = tags.join(","); + + const res = await fetch(NTFY_URL, { + method: "POST", + headers, + body: message, + }); + + if (!res.ok) { + const text = await res.text().catch(() => ""); + log.warn?.( + { status: res.status, text: text.slice(0, 200) }, + "ntfy: publish failed", + ); + return { ok: false, status: res.status }; + } + return { ok: true }; + } catch (err) { + log.warn?.({ err: err.message || String(err) }, "ntfy: publish error"); + return { ok: false, error: true }; + } +} + +/** Build a notification body for a failed/skipped receipt work item. */ +export function formatReceiptAlert({ + reason, + messageId, + kind, + message, + record, + extra, +}) { + const lines = [ + `reason: ${reason}`, + `messageId: ${messageId || ""}`, + `kind: ${kind || ""}`, + `from: ${message?.from || ""}`, + `subject: ${message?.subject || ""}`, + `date: ${message?.date || ""}`, + `attachment: ${message?.attachmentFilename || ""}`, + `parentMessageId: ${message?.parentMessageId || ""}`, + ]; + + if (record) { + lines.push(`record: ${JSON.stringify(record)}`); + } + if (extra) { + lines.push(`extra: ${JSON.stringify(extra)}`); + } + + const body = message?.body || ""; + lines.push("", "----- message body -----", body || "(empty)"); + + if (message?.html) { + lines.push( + "", + "----- message html (length) -----", + String(message.html.length), + ); + } + + // ntfy payloads should stay reasonably sized + const text = lines.join("\n"); + const max = 12000; + if (text.length <= max) return text; + return `${text.slice(0, max)}\n\n… truncated (${text.length} chars total)`; +} + +export async function notifyReceiptIssue(payload, log) { + const body = formatReceiptAlert(payload); + return notifyNtfy( + { + title: `gmail-hook: ${payload.reason}`, + message: body, + priority: "high", + tags: ["warning", "email"], + }, + log, + ); +} diff --git a/apps/watcher/src/parsers/bri.js b/apps/watcher/src/parsers/bri.js new file mode 100644 index 0000000..faa42f0 --- /dev/null +++ b/apps/watcher/src/parsers/bri.js @@ -0,0 +1,68 @@ +import { + captureAfterLabel, + captureBlockAfterLabel, + parseDayMonthYear, + parseIdrAmount, + parseTimeWib, + toWibIso, +} from "../lib/parse-helpers.js"; + +/** + * Parse BRImo / Bank BRI transfer success body (plain text). + * Required: Nominal + Nomor Referensi. + */ +export function parseBriTransfer(body) { + if (!body) return null; + + const payerMatch = body.match(/Halo[,\s*]*([^*\n,]+)/i); + const payerName = payerMatch ? payerMatch[1].replace(/\*/g, "").trim() : null; + + const tanggalRaw = captureAfterLabel(body, "Tanggal"); + const dateYmd = parseDayMonthYear(tanggalRaw); + const timeHms = parseTimeWib(tanggalRaw); + const occurredAt = toWibIso(dateYmd, timeHms); + + // Prefer Nominal over Total (Total = Nominal + Biaya Admin). + const nominalRaw = captureAfterLabel(body, "Nominal"); + const amount = parseIdrAmount(nominalRaw); + + const reference = + captureAfterLabel(body, "Nomor Referensi") || + body.match(/Nomor\s+Referensi\s*([^\s\n]+)/i)?.[1] || + null; + + if (amount == null || !reference) return null; + + const namaTujuan = captureAfterLabel(body, "Nama Tujuan"); + const jenisTransaksi = captureAfterLabel(body, "Jenis Transaksi"); + const bankTujuan = captureAfterLabel(body, "Bank Tujuan"); + const nomorTujuan = captureAfterLabel(body, "Nomor Tujuan"); + const catatanRaw = captureAfterLabel(body, "Catatan"); + const biayaAdmin = parseIdrAmount(captureAfterLabel(body, "Biaya Admin")); + + const fundingLines = captureBlockAfterLabel(body, "Sumber Dana", [ + "Jenis Transaksi", + ]); + const fundingSource = + fundingLines?.join(" ").replace(/\s+/g, " ").trim() || null; + + const details = {}; + if (payerName) details.payerName = payerName; + if (namaTujuan) details.namaTujuan = namaTujuan.trim(); + if (jenisTransaksi) details.jenisTransaksi = jenisTransaksi.trim(); + if (bankTujuan) details.bankTujuan = bankTujuan.trim(); + if (nomorTujuan) details.nomorTujuan = nomorTujuan.trim(); + if (catatanRaw != null && catatanRaw.trim() && catatanRaw.trim() !== "-") { + details.catatan = catatanRaw.trim(); + } + if (biayaAdmin != null) details.biayaAdmin = biayaAdmin; + if (fundingSource) details.fundingSource = fundingSource; + + return { + amount, + currency: "IDR", + reference: reference.trim(), + occurredAt, + details, + }; +} diff --git a/apps/watcher/src/parsers/grab.js b/apps/watcher/src/parsers/grab.js new file mode 100644 index 0000000..ea55191 --- /dev/null +++ b/apps/watcher/src/parsers/grab.js @@ -0,0 +1,247 @@ +import { parse } from "node-html-parser"; +import { + captureAfterLabel, + captureBlockAfterLabel, + parseAmPmTime, + parseDayMonthYear, + parseGrabDateTime, + parseIdrAmount, + toWibIso, +} from "../lib/parse-helpers.js"; + +function stripForwardedHeader(body) { + const m = body.match( + /-{2,}\s*Forwarded message\s*-{2,}[\s\S]*?\nTo:[^\n]*\n+/i, + ); + if (!m) return body; + return body.slice(m.index + m[0].length); +} + +/** Tip receipts share From/Subject with ride e-receipts but different body copy. */ +export function isGrabTipBody(body) { + if (!body) return false; + if (/your tip goes a long way/i.test(body)) return true; + if (/thanks!\s*your tip/i.test(body)) return true; + // Tip layout: has a Tip section, no ride pickup line + if ( + !/picked up on/i.test(body) && + !/hope you enjoyed your ride/i.test(body) && + !/selamat menikmati makanan/i.test(body) && + !/grabfood/i.test(body) && + /\btip\b/i.test(body) && + /total paid/i.test(body) + ) { + return true; + } + return false; +} + +/** GrabFood order receipts (ID/EN) — same From/Subject as rides. */ +export function isGrabFoodBody(body) { + if (!body) return false; + if (/selamat menikmati makanan/i.test(body)) return true; + if (/jenis kendaraan:\s*\r?\n\s*GrabFood/i.test(body)) return true; + if (/\bGrabFood\b/i.test(body) && /pesanan id/i.test(body)) return true; + return false; +} + +function extractProduct(content) { + const productMatch = content.match( + /^\s*(Bike\s+\w+|Car\s+\w+|Grab\w*|Food|Express|Mart)[^\n]*/im, + ); + return productMatch ? productMatch[1].trim() : null; +} + +function extractBookingId(body) { + return ( + captureAfterLabel(body, "Booking ID:") || + body.match(/Booking ID:\s*([A-Z0-9-]+)/i)?.[1] || + null + ); +} + +function extractTotalPaid(body) { + return ( + captureAfterLabel(body, "Total Paid") || + body.match(/Total Paid\s*\r?\n\s*(Rp[\d.,]+)/i)?.[1] + ); +} + +/** + * Grab ride HTML puts pickup/dropoff as standalone "10:47AM" divs next to + * locations. Plain text only has "Picked up on 13 September 2026" (date only). + * Returns { pickup, dropoff } as HH:MM:SS (24h) when found. + */ +export function extractGrabTripTimesFromHtml(html) { + if (!html) return { pickup: null, dropoff: null }; + const root = parse(html); + const times = []; + for (const el of root.querySelectorAll("div")) { + const t = (el.text || "").replace(/\u00a0/g, " ").trim(); + if (!/^\d{1,2}:\d{2}(?::\d{2})?\s*[AaPp][Mm]$/.test(t)) continue; + const hms = parseAmPmTime(t); + if (hms) times.push(hms); + } + return { + pickup: times[0] || null, + dropoff: times[1] || null, + }; +} + +function parseRide(body, content, html) { + const product = extractProduct(content); + const pickedUpRaw = captureAfterLabel(body, "Picked up on"); + const dateYmd = parseDayMonthYear(pickedUpRaw); + const { pickup, dropoff } = extractGrabTripTimesFromHtml(html); + const occurredAt = toWibIso(dateYmd, pickup) || dateYmd; + + const bookingId = extractBookingId(body); + const amount = parseIdrAmount(extractTotalPaid(body)); + + if (amount == null || !bookingId) return null; + + const details = { variant: "ride" }; + if (product) details.product = product; + if (dropoff) details.dropoffAt = toWibIso(dateYmd, dropoff); + + const driverMatch = body.match( + /Receipt issued by the driver\s+([^.\n]+)/i, + ); + if (driverMatch) details.driver = driverMatch[1].trim(); + + const ratingMatch = body.match( + /(\d(?:\.\d)?)\s*(?:\[image: stars\]|★|stars)/i, + ); + if (ratingMatch) details.driverRating = Number(ratingMatch[1]); + + const complimentsBlock = body.match( + /Compliments for driver\s*\r?\n([\s\S]*?)(?=\r?\nBreakdown|\r?\nPassenger|$)/i, + ); + if (complimentsBlock) { + const compliments = complimentsBlock[1] + .split(/\r?\n/) + .map((l) => l.trim()) + .filter((l) => l && !/^\[image/i.test(l)); + if (compliments.length) details.compliments = compliments; + } + + const passenger = captureAfterLabel(body, "Passenger"); + if (passenger) { + details.passenger = passenger.replace(/\s*Profile.*$/i, "").trim(); + } + + return { + variant: "ride", + kind: "grab.ereceipt.ride", + amount, + currency: "IDR", + reference: bookingId.trim(), + occurredAt, + details, + }; +} + +function parseTip(body, content) { + const product = extractProduct(content); + const bookingId = extractBookingId(body); + const amount = parseIdrAmount(extractTotalPaid(body)); + + if (amount == null || !bookingId) return null; + + // Tip timestamp sits on its own line, e.g. "13 Sep 26 10:47 +0700" + const tipTs = + body.match( + /(\d{1,2}\s+[A-Za-z]+\s+\d{2,4}\s+\d{1,2}:\d{2}(?::\d{2})?\s*[+-]\d{4})/, + )?.[1] || null; + const occurredAt = parseGrabDateTime(tipTs); + + const details = { variant: "tip" }; + if (product) details.product = product; + + return { + variant: "tip", + kind: "grab.ereceipt.tip", + amount, + currency: "IDR", + // Separate invoice from the ride spend for the same Booking ID + reference: `${bookingId.trim()}:tip`, + occurredAt, + details, + }; +} + +function parseFood(body, content) { + const orderId = + captureAfterLabel(body, "Pesanan ID") || + body.match(/Pesanan ID\s*\r?\n\s*([A-Z0-9-]+)/i)?.[1] || + captureAfterLabel(body, "Order ID") || + null; + + const totalRaw = + body.match(/TOTAL\s*\(INCL\.?\s*TAX\)\s*IDR\s*([\d.]+)/i)?.[0] || + body.match(/TOTAL\s*\r?\n\s*IDR\s*([\d.]+)/i)?.[0] || + body.match(/IDR\s*[\d.]+/)?.[0]; + const amount = parseIdrAmount(totalRaw); + + if (amount == null || !orderId) return null; + + const occurredAt = parseGrabDateTime( + body.match( + /(\d{1,2}\s+[A-Za-z]+\s+\d{2,4}\s+\d{1,2}:\d{2}(?::\d{2})?\s*[+-]\d{4})/, + )?.[1], + ); + + const details = { variant: "food", product: "Food" }; + + const vehicle = + captureAfterLabel(body, "Jenis Kendaraan:") || + body.match(/Jenis Kendaraan:\s*\r?\n\s*([^\n]+)/i)?.[1]; + if (vehicle) details.vehicleType = vehicle.trim(); + + const forName = + captureAfterLabel(body, "Diterbitkan untuk") || + body.match(/Diterbitkan untuk\s*\r?\n\s*([^\n]+)/i)?.[1]; + if (forName) details.passenger = forName.trim(); + + const merchantLines = captureBlockAfterLabel(body, "Pesanan Dari:", [ + "Profil:", + "Detail Tagihan", + "Detail Pembayaran", + ]); + if (merchantLines?.length) { + details.merchant = merchantLines[0]; + if (merchantLines.length > 1) { + details.location = merchantLines + .slice(1) + .join(" ") + .replace(/]+>/g, "") + .replace(/\s+/g, " ") + .trim(); + } + } + + const payment = + captureAfterLabel(body, "Detail Pembayaran:") || + body.match(/Detail Pembayaran:\s*\r?\n\s*([^\n]+)/i)?.[1]; + if (payment && !/Jumlah/i.test(payment)) { + details.paymentMethod = payment.trim(); + } + + return { + variant: "food", + kind: "grab.ereceipt.food", + amount, + currency: "IDR", + reference: orderId.trim(), + occurredAt, + details, + }; +} + +export function parseGrabReceipt(body, { html = "" } = {}) { + if (!body) return null; + const content = stripForwardedHeader(body); + if (isGrabTipBody(body)) return parseTip(body, content); + if (isGrabFoodBody(body)) return parseFood(body, content); + return parseRide(body, content, html); +} diff --git a/apps/watcher/src/parsers/livin.js b/apps/watcher/src/parsers/livin.js new file mode 100644 index 0000000..23c5953 --- /dev/null +++ b/apps/watcher/src/parsers/livin.js @@ -0,0 +1,94 @@ +import { + captureAfterLabel, + captureBlockAfterLabel, + parseDayMonthYear, + parseIdrAmount, + parseTimeWib, + toWibIso, +} from "../lib/parse-helpers.js"; + +/** + * Split Penerima lines into merchant + location when the body already + * separates them. If HTML-stripped glue merges them into one line + * (e.g. SNACK BAROKAHJAKARTA BARAT - ID), keep the whole string as merchant + * — do not guess Indonesian city names. + */ +export function splitMerchantLocation(lines) { + if (!lines || !lines.length) { + return { merchant: null, location: null }; + } + + if (lines.length >= 2) { + const last = lines[lines.length - 1]; + if (/-\s*ID\s*$/i.test(last)) { + return { + merchant: lines.slice(0, -1).join(" ").trim() || null, + location: last.trim(), + }; + } + } + + return { merchant: lines.join(" ").trim() || null, location: null }; +} + +export function parseLivinReceipt(body) { + if (!body) return null; + + const payerMatch = body.match(/Halo\s+([^,\n]+)/i); + const payerName = payerMatch ? payerMatch[1].trim() : null; + + const penerimaLines = captureBlockAfterLabel(body, "Penerima", [ + "Tanggal", + "Jam", + "Nominal", + ]); + const { merchant, location } = splitMerchantLocation(penerimaLines); + + const tanggalRaw = captureAfterLabel(body, "Tanggal"); + const jamRaw = captureAfterLabel(body, "Jam"); + const dateYmd = parseDayMonthYear(tanggalRaw); + const timeHms = parseTimeWib(jamRaw); + const occurredAt = toWibIso(dateYmd, timeHms); + + const nominalRaw = captureAfterLabel(body, "Nominal Transaksi"); + const amount = parseIdrAmount(nominalRaw); + + const reference = + captureAfterLabel(body, "No. Referensi") || + body.match(/No\.\s*Referensi\s*([^\s\n]+)/i)?.[1] || + null; + + if (amount == null || !reference) return null; + + const details = {}; + if (payerName) details.payerName = payerName; + if (merchant) details.merchant = merchant; + if (location) details.location = location; + + const qrisRef = + captureAfterLabel(body, "No. Ref. QRIS") || + body.match(/No\.\s*Ref\.\s*QRIS\s*([^\s\n]+)/i)?.[1]; + const merchantPan = captureAfterLabel(body, "Merchant PAN"); + const customerPan = captureAfterLabel(body, "Customer PAN"); + const acquirer = captureAfterLabel(body, "Pengakuisisi"); + const terminalId = captureAfterLabel(body, "Terminal ID"); + const fundingLines = captureBlockAfterLabel(body, "Sumber Dana", [ + "Simpan email", + ]); + const fundingSource = fundingLines?.join(" ").replace(/\s+/g, " ").trim() || null; + + if (qrisRef) details.qrisRef = qrisRef; + if (merchantPan) details.merchantPan = merchantPan; + if (customerPan) details.customerPan = customerPan; + if (acquirer) details.acquirer = acquirer; + if (terminalId) details.terminalId = terminalId.trim(); + if (fundingSource) details.fundingSource = fundingSource; + + return { + amount, + currency: "IDR", + reference: reference.trim(), + occurredAt, + details, + }; +} diff --git a/apps/watcher/src/pipeline.js b/apps/watcher/src/pipeline.js new file mode 100644 index 0000000..97c932b --- /dev/null +++ b/apps/watcher/src/pipeline.js @@ -0,0 +1,12 @@ +export async function runPipeline(handlers, ctx) { + let current = { ...ctx }; + for (const handler of handlers) { + const result = await handler(current); + if (!result) continue; + if (result.pass && typeof result.pass === "object" && !Array.isArray(result.pass)) { + current = { ...current, ...result.pass }; + } + if (result.break) break; + } + return current; +} diff --git a/apps/watcher/src/pocketbase/client.js b/apps/watcher/src/pocketbase/client.js new file mode 100644 index 0000000..38f3bd6 --- /dev/null +++ b/apps/watcher/src/pocketbase/client.js @@ -0,0 +1,91 @@ +import { + POCKETBASE_PASSWORD, + POCKETBASE_URL, + POCKETBASE_USER, +} from "../config.js"; + +let cachedToken = null; +let cachedTokenAt = 0; +const TOKEN_TTL_MS = 50 * 60 * 1000; // refresh before typical 1h expiry + +export function pocketbaseConfigured() { + return Boolean(POCKETBASE_URL && POCKETBASE_USER && POCKETBASE_PASSWORD); +} + +async function authSuperuser() { + const res = await fetch( + `${POCKETBASE_URL}/api/collections/_superusers/auth-with-password`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + identity: POCKETBASE_USER, + password: POCKETBASE_PASSWORD, + }), + }, + ); + const data = await res.json().catch(() => ({})); + if (!res.ok || !data.token) { + throw new Error( + `PocketBase auth failed (${res.status}): ${data.message || "unknown"}`, + ); + } + cachedToken = data.token; + cachedTokenAt = Date.now(); + return cachedToken; +} + +async function getToken() { + if (cachedToken && Date.now() - cachedTokenAt < TOKEN_TTL_MS) { + return cachedToken; + } + return authSuperuser(); +} + +export async function pbRequest(path, { method = "GET", body, retry = true } = {}) { + if (!pocketbaseConfigured()) { + throw new Error("PocketBase is not configured"); + } + const token = await getToken(); + const res = await fetch(`${POCKETBASE_URL}${path}`, { + method, + headers: { + Authorization: token, + ...(body ? { "Content-Type": "application/json" } : {}), + }, + body: body ? JSON.stringify(body) : undefined, + }); + + if (res.status === 401 && retry) { + cachedToken = null; + await authSuperuser(); + return pbRequest(path, { method, body, retry: false }); + } + + const text = await res.text(); + let data = {}; + try { + data = text ? JSON.parse(text) : {}; + } catch { + data = { raw: text }; + } + return { ok: res.ok, status: res.status, data }; +} + +export async function getCollection(name) { + return pbRequest(`/api/collections/${name}`); +} + +export async function updateCollection(name, patch) { + return pbRequest(`/api/collections/${name}`, { + method: "PATCH", + body: patch, + }); +} + +export async function createRecord(collection, record) { + return pbRequest(`/api/collections/${collection}/records`, { + method: "POST", + body: record, + }); +} diff --git a/apps/watcher/src/pocketbase/map.js b/apps/watcher/src/pocketbase/map.js new file mode 100644 index 0000000..6b10022 --- /dev/null +++ b/apps/watcher/src/pocketbase/map.js @@ -0,0 +1,52 @@ +/** + * Map parsed receipt envelope → PocketBase spendings record. + */ +export function toSpendingRecord(parsed, rule) { + const source = rule?.source || parsed.kind?.split(".")[0] || ""; + let description = ""; + if (source === "livin") { + description = parsed.details?.merchant || ""; + } else if (source === "bri") { + description = parsed.details?.namaTujuan || ""; + } else if (source === "grab") { + if (parsed.kind === "grab.ereceipt.food" || parsed.details?.variant === "food") { + const merchant = parsed.details?.merchant; + description = merchant ? `Grab Food ${merchant}` : "Grab Food"; + } else { + const product = parsed.details?.product; + const base = product ? `Grab ${product}` : "Grab"; + description = + parsed.kind === "grab.ereceipt.tip" || parsed.details?.variant === "tip" + ? `${base} tip` + : base; + } + } + + return { + invoice_id: parsed.reference, + amount: parsed.amount, + trx_date: toPocketBaseDate(parsed.occurredAt), + source, + description, + category: "", + message_id: parsed.messageId || "", + details: parsed.details || {}, + // Full watcher envelope snapshot for audit / debugging (not the raw email body). + parsed, + }; +} + +/** + * PocketBase date fields accept "YYYY-MM-DD HH:mm:ss.SSSZ" (UTC) or date-only. + */ +export function toPocketBaseDate(occurredAt) { + if (!occurredAt) return null; + // Date-only (Grab) + if (/^\d{4}-\d{2}-\d{2}$/.test(occurredAt)) { + return `${occurredAt} 00:00:00.000Z`; + } + const d = new Date(occurredAt); + if (Number.isNaN(d.getTime())) return null; + // PocketBase: "2026-09-13 07:48:31.000Z" + return d.toISOString().replace("T", " "); +} diff --git a/apps/watcher/src/pocketbase/schema.js b/apps/watcher/src/pocketbase/schema.js new file mode 100644 index 0000000..cd03a30 --- /dev/null +++ b/apps/watcher/src/pocketbase/schema.js @@ -0,0 +1,121 @@ +import { getCollection, pocketbaseConfigured, updateCollection } from "./client.js"; + +const SPENDINGS = "spendings"; + +const DESIRED_FIELDS = [ + { + name: "invoice_id", + type: "text", + required: true, + min: 0, + }, + { + name: "amount", + type: "number", + required: true, + }, + { + name: "trx_date", + type: "date", + required: true, + }, + { + name: "source", + type: "text", + required: true, + min: 0, + }, + { + name: "description", + type: "text", + required: false, + min: 0, + }, + { + name: "category", + type: "text", + required: false, + min: 0, + }, + { + name: "message_id", + type: "text", + required: false, + min: 0, + }, + { + name: "details", + type: "json", + required: false, + }, + { + name: "parsed", + type: "json", + required: false, + }, +]; + +const UNIQUE_INVOICE_INDEX = + "CREATE UNIQUE INDEX IF NOT EXISTS idx_spendings_invoice_id ON spendings (invoice_id)"; + +/** + * Ensure spendings has the fields and unique invoice_id index we need. + * Safe on an empty collection. Idempotent. + */ +export async function ensureSpendingsSchema(log = console) { + if (!pocketbaseConfigured()) { + log.warn?.("pocketbase: skipped schema ensure (not configured)"); + return { skipped: true }; + } + + const { ok, status, data } = await getCollection(SPENDINGS); + if (!ok) { + throw new Error( + `pocketbase: cannot load spendings collection (${status}): ${data.message || ""}`, + ); + } + + const existing = data.fields || []; + const byName = new Map(existing.map((f) => [f.name, f])); + + // Keep system/autodate fields; merge desired custom fields by name. + const systemNames = new Set(["id", "created", "updated"]); + const nextFields = existing.filter((f) => systemNames.has(f.name)); + + for (const desired of DESIRED_FIELDS) { + const prev = byName.get(desired.name); + if (prev) { + nextFields.push({ + ...prev, + type: desired.type, + required: desired.required, + ...(desired.min != null ? { min: desired.min } : {}), + }); + } else { + nextFields.push({ ...desired }); + } + } + + // Preserve any other unknown custom fields the user may have added. + for (const f of existing) { + if (systemNames.has(f.name)) continue; + if (DESIRED_FIELDS.some((d) => d.name === f.name)) continue; + nextFields.push(f); + } + + const indexes = Array.isArray(data.indexes) ? [...data.indexes] : []; + if (!indexes.some((idx) => /invoice_id/i.test(String(idx)))) { + indexes.push(UNIQUE_INVOICE_INDEX); + } + + const patch = { fields: nextFields, indexes }; + const result = await updateCollection(SPENDINGS, patch); + if (!result.ok) { + throw new Error( + `pocketbase: failed to update spendings schema (${result.status}): ${result.data.message || JSON.stringify(result.data)}`, + ); + } + + log.info?.({ collection: SPENDINGS }, "pocketbase: spendings schema ensured"); + return { ok: true }; +} diff --git a/apps/watcher/src/process-fetched.js b/apps/watcher/src/process-fetched.js new file mode 100644 index 0000000..ef14b29 --- /dev/null +++ b/apps/watcher/src/process-fetched.js @@ -0,0 +1,32 @@ +import { runPipeline } from "./pipeline.js"; + +/** + * After fetch: if .eml attachments exist, process each; otherwise process outer. + * `fetchResult` is the ctx after [logEvent, fetchAndLogMessage]. + */ +export async function processFetchedMessage(fetchResult, processHandlers) { + if (!fetchResult?.message) return []; + + const workItems = fetchResult.emlMessages?.length + ? fetchResult.emlMessages + : [fetchResult.message]; + + const results = []; + for (const item of workItems) { + const result = await runPipeline(processHandlers, { + ...fetchResult, + message: item, + messageId: item.id, + // Clear prior match/parse/save state from a previous item + rule: undefined, + original: undefined, + parsed: undefined, + saved: undefined, + duplicate: undefined, + error: undefined, + spendingId: undefined, + }); + results.push(result); + } + return results; +} diff --git a/apps/watcher/src/routes/pubsub.js b/apps/watcher/src/routes/pubsub.js new file mode 100644 index 0000000..41c7afb --- /dev/null +++ b/apps/watcher/src/routes/pubsub.js @@ -0,0 +1,169 @@ +import { timingSafeEqual } from "node:crypto"; +import { PUBSUB_VERIFICATION_TOKEN } from "../config.js"; +import { getUserByEmail, getWatchState, saveWatchState } from "../db.js"; +import { + gmailClientForUser, + googleStatus, + listHistory, + startWatch, +} from "../google.js"; +import { HttpError } from "../http.js"; +import { runPipeline } from "../pipeline.js"; +import { processFetchedMessage } from "../process-fetched.js"; + +const userLocks = new Map(); + +function withUserLock(userId, fn) { + const prev = userLocks.get(userId) || Promise.resolve(); + const next = prev.then(fn, fn); + userLocks.set( + userId, + next.then( + () => {}, + () => {}, + ), + ); + return next; +} + +function tokenFromRequest(request) { + const q = request.query?.token; + if (typeof q === "string" && q) return q; + const header = + request.headers["x-pubsub-verification-token"] || + request.headers["x-goog-pubsub-verification-token"]; + if (typeof header === "string" && header) return header; + const auth = request.headers.authorization; + if (typeof auth === "string" && auth.startsWith("Bearer ")) { + return auth.slice(7); + } + return ""; +} + +function tokensMatch(provided, expected) { + const a = Buffer.from(provided); + const b = Buffer.from(expected); + if (a.length !== b.length) return false; + return timingSafeEqual(a, b); +} + +function verifyPush(request) { + if (!PUBSUB_VERIFICATION_TOKEN) return; + if (!tokensMatch(tokenFromRequest(request), PUBSUB_VERIFICATION_TOKEN)) { + throw new HttpError(403, "Invalid Pub/Sub verification token"); + } +} + +function decodeNotification(body) { + const data = body?.message?.data; + if (typeof data !== "string" || !data) return null; + try { + const json = Buffer.from(data, "base64").toString("utf8"); + const parsed = JSON.parse(json); + if (!parsed?.emailAddress || parsed.historyId == null) return null; + return { + emailAddress: parsed.emailAddress, + historyId: String(parsed.historyId), + }; + } catch { + return null; + } +} + +async function processNotification(log, { fetchHandlers, processHandlers }, notification) { + const user = getUserByEmail(notification.emailAddress); + if (!user) { + log.warn( + { email: notification.emailAddress }, + "gmail-watch: unknown mailbox, acking", + ); + return; + } + + await withUserLock(user.id, () => + handleUserNotification(log, { fetchHandlers, processHandlers }, user, notification), + ); +} + +async function handleUserNotification( + log, + { fetchHandlers, processHandlers }, + user, + notification, +) { + const gmail = await gmailClientForUser(user.id); + + const state = getWatchState(user.id); + const startHistoryId = state?.history_id; + if (!startHistoryId) { + log.info( + { userId: user.id }, + "gmail-watch: no cursor yet, starting watch and skipping backlog", + ); + await startWatch(user.id, { resetCursor: true }); + return; + } + + let result; + try { + result = await listHistory(gmail, startHistoryId); + } catch (err) { + const status = googleStatus(err); + if (status === 404) { + log.warn( + { userId: user.id, startHistoryId }, + "gmail-watch: history expired, re-watching without backfill", + ); + await startWatch(user.id, { resetCursor: true }); + return; + } + throw err; + } + + for (const messageId of result.messageIds) { + const fetched = await runPipeline(fetchHandlers, { + notification, + userId: user.id, + messageId, + gmail, + log, + }); + await processFetchedMessage(fetched, processHandlers); + } + + saveWatchState(user.id, { + historyId: result.historyId || notification.historyId, + expiration: state.expiration || Date.now() + 7 * 24 * 3600 * 1000, + }); +} + +export async function registerPubsubRoutes(app, { fetchHandlers, processHandlers }) { + app.post("/pubsub/gmail", async (request, reply) => { + verifyPush(request); + + const notification = decodeNotification(request.body); + if (!notification) { + request.log.warn("gmail-watch: undecodable Pub/Sub payload, acking"); + return reply.code(204).send(); + } + + try { + await processNotification( + request.log, + { fetchHandlers, processHandlers }, + notification, + ); + } catch (err) { + const status = googleStatus(err) || err.statusCode || 500; + if (status === 401 || status === 403) { + throw new HttpError( + 500, + "Gmail access expired or was revoked. Sign in again.", + ); + } + throw err; + } + + return reply.code(204).send(); + }); +} diff --git a/apps/watcher/src/rules/bri-transfer.js b/apps/watcher/src/rules/bri-transfer.js new file mode 100644 index 0000000..c4429c9 --- /dev/null +++ b/apps/watcher/src/rules/bri-transfer.js @@ -0,0 +1,6 @@ +export const briTransfer = { + kind: "bri.transfer.success", + source: "bri", + fromAddress: "bankbri@bri.co.id", + subject: "Pemindahan Dana Sesama Rekening BRI", +}; diff --git a/apps/watcher/src/rules/grab-ereceipt.js b/apps/watcher/src/rules/grab-ereceipt.js new file mode 100644 index 0000000..b4abcef --- /dev/null +++ b/apps/watcher/src/rules/grab-ereceipt.js @@ -0,0 +1,6 @@ +export const grabEreceipt = { + kind: "grab.ereceipt.ride", + source: "grab", + fromAddress: "no-reply@grab.com", + subject: "Your Grab E-Receipt", +}; diff --git a/apps/watcher/src/rules/index.js b/apps/watcher/src/rules/index.js new file mode 100644 index 0000000..e4eb222 --- /dev/null +++ b/apps/watcher/src/rules/index.js @@ -0,0 +1,5 @@ +import { briTransfer } from "./bri-transfer.js"; +import { grabEreceipt } from "./grab-ereceipt.js"; +import { livinQrPayment } from "./livin-qr-payment.js"; + +export const rules = [livinQrPayment, grabEreceipt, briTransfer]; diff --git a/apps/watcher/src/rules/livin-qr-payment.js b/apps/watcher/src/rules/livin-qr-payment.js new file mode 100644 index 0000000..11d4e32 --- /dev/null +++ b/apps/watcher/src/rules/livin-qr-payment.js @@ -0,0 +1,6 @@ +export const livinQrPayment = { + kind: "livin.qr_payment.success", + source: "livin", + fromAddress: "noreply.livin@bankmandiri.co.id", + subject: "Pembayaran Berhasil!", +}; diff --git a/apps/watcher/test/eml.test.js b/apps/watcher/test/eml.test.js new file mode 100644 index 0000000..786671b --- /dev/null +++ b/apps/watcher/test/eml.test.js @@ -0,0 +1,327 @@ +import assert from "node:assert/strict"; +import { describe, it } from "node:test"; +import { + expandWorkItems, + listRfc822Parts, + messageFromRfc822Part, +} from "../src/mime.js"; +import { processFetchedMessage } from "../src/process-fetched.js"; +import { matchRule } from "../src/handlers/match-rule.js"; +import { parseMatchedMessage } from "../src/handlers/parse-matched-message.js"; + +const silentLog = { + info() {}, + warn() {}, + error() {}, +}; + +function textPart(text, partId = "0.0") { + return { + partId, + mimeType: "text/plain", + body: { + data: Buffer.from(text, "utf8") + .toString("base64") + .replace(/\+/g, "-") + .replace(/\//g, "_") + .replace(/=+$/, ""), + }, + }; +} + +function htmlPart(html, partId = "0.1") { + return { + partId, + mimeType: "text/html", + body: { + data: Buffer.from(html, "utf8") + .toString("base64") + .replace(/\+/g, "-") + .replace(/\//g, "_") + .replace(/=+$/, ""), + }, + }; +} + +function rfc822Part({ + partId, + filename, + from, + subject, + date, + bodyText, + bodyHtml, +}) { + return { + partId, + mimeType: "message/rfc822", + filename, + parts: [ + { + partId: `${partId}.0`, + mimeType: "multipart/alternative", + headers: [ + { name: "From", value: from }, + { name: "Subject", value: subject }, + { name: "Date", value: date }, + ], + parts: [ + textPart(bodyText, `${partId}.0.0`), + ...(bodyHtml ? [htmlPart(bodyHtml, `${partId}.0.1`)] : []), + ], + }, + ], + }; +} + +const grabRideBody = `Bike Hemat +Hope you enjoyed your ride! +Picked up on 13 September 2026 +Booking ID: A-TESTBOOKING1 +Total Paid +Rp16.600 +Receipt issued by the driver Mahendra. +Passenger +Bintang +`; + +const grabFoodBody = `Selamat menikmati makanan Anda! +TOTAL +IDR 49000 TANGGAL | WAKTU +11 Sep 26 18:24 +0700 +Detail Pesanan +Jenis Kendaraan: +GrabFood +Diterbitkan untuk +Bintang +Pesanan ID +00112712674-TESTFOOD1 +Pesanan Dari: +Doner Kebab - Kalibata City +Profil: +Personal +Detail Tagihan +Detail Pembayaran: +OVO Cash +TOTAL (INCL. TAX) IDR 49000 +`; + +describe("listRfc822Parts", () => { + it("finds message/rfc822 attachments and does not treat outer as eml", () => { + const payload = { + mimeType: "multipart/mixed", + parts: [ + { + mimeType: "multipart/alternative", + parts: [textPart("wrapper")], + }, + rfc822Part({ + partId: "1", + filename: "Your Grab E-Receipt.eml", + from: "Grab ", + subject: "Your Grab E-Receipt", + date: "Wed, 9 Sep 2026 06:09:32 +0000", + bodyText: grabRideBody, + }), + rfc822Part({ + partId: "2", + filename: "Your Grab E-Receipt.eml", + from: "Grab ", + subject: "Your Grab E-Receipt", + date: "Thu, 10 Sep 2026 11:28:24 +0000", + bodyText: grabFoodBody, + }), + ], + }; + const parts = listRfc822Parts(payload); + assert.equal(parts.length, 2); + assert.equal(parts[0].filename, "Your Grab E-Receipt.eml"); + }); +}); + +describe("messageFromRfc822Part", () => { + it("builds synthetic message with nested From/Subject/body", () => { + const part = rfc822Part({ + partId: "1", + filename: "Your Grab E-Receipt.eml", + from: "Grab ", + subject: "Your Grab E-Receipt", + date: "Wed, 9 Sep 2026 06:09:32 +0000", + bodyText: grabRideBody, + }); + const msg = messageFromRfc822Part(part, { + parentMessageId: "outer123", + index: 0, + }); + assert.equal(msg.id, "outer123#1"); + assert.equal(msg.from, "Grab "); + assert.equal(msg.subject, "Your Grab E-Receipt"); + assert.match(msg.body, /Booking ID: A-TESTBOOKING1/); + assert.equal(msg.parentMessageId, "outer123"); + assert.equal(msg.attachmentFilename, "Your Grab E-Receipt.eml"); + }); +}); + +describe("expandWorkItems", () => { + it("returns outer only when no eml", () => { + const outer = { + id: "outer", + from: "me@x.com", + subject: "hi", + body: "x", + html: "", + }; + const payload = { + mimeType: "multipart/alternative", + parts: [textPart("hi")], + }; + const items = expandWorkItems(outer, payload); + assert.equal(items.length, 1); + assert.equal(items[0], outer); + }); + + it("returns eml messages when attachments exist", () => { + const outer = { + id: "outer123", + from: "NSRB ", + subject: "Test attachment", + body: "Regards", + html: "", + }; + const payload = { + mimeType: "multipart/mixed", + parts: [ + { mimeType: "multipart/alternative", parts: [textPart("Regards")] }, + rfc822Part({ + partId: "1", + filename: "a.eml", + from: "Grab ", + subject: "Your Grab E-Receipt", + date: "Wed, 9 Sep 2026 06:09:32 +0000", + bodyText: grabRideBody, + }), + ], + }; + const items = expandWorkItems(outer, payload); + assert.equal(items.length, 1); + assert.equal(items[0].from, "Grab "); + assert.equal(items[0].id, "outer123#1"); + }); +}); + +describe("processFetchedMessage fan-out", () => { + it("processes each eml attachment and skips outer wrapper", async () => { + const fetched = { + notification: { emailAddress: "eleven16th@gmail.com" }, + log: silentLog, + message: { + id: "outer123", + from: "NSRB ", + subject: "Test attachment", + body: "Regards", + html: "", + }, + emlMessages: [ + { + id: "outer123#1", + from: "Grab ", + subject: "Your Grab E-Receipt", + date: "Wed, 9 Sep 2026 06:09:32 +0000", + body: grabRideBody, + html: "", + parentMessageId: "outer123", + }, + { + id: "outer123#2", + from: "Grab ", + subject: "Your Grab E-Receipt", + date: "Thu, 10 Sep 2026 11:28:24 +0000", + body: grabFoodBody, + html: "", + parentMessageId: "outer123", + }, + ], + }; + + const results = await processFetchedMessage(fetched, [ + matchRule, + parseMatchedMessage, + ]); + assert.equal(results.length, 2); + assert.equal(results[0].parsed?.kind, "grab.ereceipt.ride"); + assert.equal(results[0].parsed?.reference, "A-TESTBOOKING1"); + assert.equal(results[1].parsed?.kind, "grab.ereceipt.food"); + assert.equal(results[1].parsed?.reference, "00112712674-TESTFOOD1"); + }); + + it("processes outer message when no eml attachments", async () => { + const fetched = { + notification: { emailAddress: "eleven16th@gmail.com" }, + log: silentLog, + message: { + id: "direct1", + from: `"Livin'" `, + subject: "Pembayaran Berhasil!", + date: "Sun, 13 Sep 2026 14:48:36 +0700", + body: `Halo TEST, +Penerima +Shop +JAKARTA SELATAN - ID +Tanggal 13 Sep 2026 +Jam 10:00:00 WIB +Nominal Transaksi Rp 10.000,00 +No. Referensi 9999999999999999999`, + html: "", + }, + emlMessages: [], + }; + + const results = await processFetchedMessage(fetched, [ + matchRule, + parseMatchedMessage, + ]); + assert.equal(results.length, 1); + assert.equal(results[0].parsed?.kind, "livin.qr_payment.success"); + }); +}); + +describe("matchRule on synthetic eml", () => { + it("matches Grab from nested headers without Fwd", async () => { + const result = await matchRule({ + messageId: "outer#1", + log: silentLog, + message: { + id: "outer#1", + from: "Grab ", + subject: "Your Grab E-Receipt", + body: grabRideBody, + html: "", + }, + }); + assert.equal(result.pass.rule.kind, "grab.ereceipt.ride"); + assert.equal(result.pass.original.from, "Grab "); + }); +}); + +describe("extractMimePartsFromRawEml", () => { + it("pulls text/html from a simple multipart eml", async () => { + const { extractMimePartsFromRawEml } = await import("../src/mime.js"); + const raw = [ + 'Content-Type: multipart/alternative; boundary="bnd"', + "", + "--bnd", + "Content-Type: text/plain; charset=UTF-8", + "", + "Hello plain", + "--bnd", + "Content-Type: text/html; charset=UTF-8", + "", + "Hello html", + "--bnd--", + "", + ].join("\r\n"); + const parts = extractMimePartsFromRawEml(raw); + assert.match(parts.text, /Hello plain/); + assert.match(parts.html, /Hello html/); + }); +}); diff --git a/apps/watcher/test/fixtures.js b/apps/watcher/test/fixtures.js new file mode 100644 index 0000000..1a41517 --- /dev/null +++ b/apps/watcher/test/fixtures.js @@ -0,0 +1,233 @@ +export const livinDirectBody = `Livin' by Mandiri + +Pembayaran Berhasil +Halo NASYAROBBY NUGRAHA PUTRA, + +Berikut adalah detail transaksi Anda dengan QR: + +Penerima + +SNACK BAROKAHJAKARTA BARAT - ID + +Tanggal13 Sep 2026 + + +Jam14:48:31 WIB + + + Nominal TransaksiRp 14.000,00 + + +No. Referensi2609131121540314991 + + +No. Ref. QRIS609532729372 + + +Merchant PAN9360091436372513569 + + +Customer PAN9360000822021345851 + + +PengakuisisiGoPay + + +Terminal ID A01 + +Sumber Dana + + Kartu Kredit - Mandiri Platinum ****4585 + +Simpan email ini sebagai ref`; + +export const livinForwardedBody = `---------- Forwarded message --------- +From: Livin' +Date: Sun, Sep 13, 2026 at 10:44 AM +Subject: Pembayaran Berhasil! +To: + + +[image: livinbymandiri] +Pembayaran Berhasil + +Halo NASYAROBBY NUGRAHA PUTRA, + +Berikut adalah detail transaksi Anda dengan QR: + +Penerima +Eleven Chicken Cabang Gah + +JAKARTA SELATAN - ID + +Tanggal 13 Sep 2026 + + +Jam 10:43:51 WIB + + +Nominal Transaksi Rp 22.000,00 + + +No. Referensi 2609131121539559589`; + +export const grabBody = `Bike Hemat +[image: Grab] + +Hope you enjoyed your ride! +Picked up on 13 September 2026 +Booking ID: A-9RXV4K2GWCLOAV +Total Paid +Rp16.600 +Receipt issued by the driver Mahendra. +5.0 [image: stars] +Compliments for driver +Layanan Mantap +Breakdown +Fare +16.600 +Fare +16.600 +Total Paid +16.600 +Passenger +Bintang +Profile +`; + +export const grabForwardedBody = `---------- Forwarded message --------- +From: Grab +Date: Sun, Sep 13, 2026 at 10:58 AM +Subject: Your Grab E-Receipt +To: + + +${grabBody}`; + +export const grabTipForwardedBody = `---------- Forwarded message --------- +From: Grab +Date: Sun, Sep 13, 2026 at 11:51 AM +Subject: Your Grab E-Receipt +To: + + +Bike Hemat +[image: Grab] + +Thanks! Your tip goes a long way for your driver. +13 Sep 26 10:47 +0700 +Total Paid +Rp2.000 +Tip +From: Apartemen Kalibata City, Jl. Kalibata Raya No.1, Rawajati, Pancoran, +Kota Jakarta Selatan, DKI Jakarta, Indonesia, 12750 +Booking ID: A-9RXV4K2GWCLOAV +`; + +/** Minimal Grab ride HTML: pickup/dropoff times only exist here, not in plain text. */ +export const grabRideHtml = ` +
Picked up on 13 September 2026
+
Booking ID: A-9RXV4K2GWCLOAV
+ + + + + + + +
+
Gate II Apartemen Kalibata City
+
10:47AM
+
+
Badan Koordinasi Penanaman Modal
+
10:58AM
+
+`; + +export const grabFoodForwardedBody = `---------- Forwarded message --------- +From: +Date: Fri, Sep 11, 2026 at 7:01 PM +Subject: Your Grab E-Receipt +To: + + +[image: E-receipt] +Selamat menikmati makanan Anda! +TOTAL +IDR 49000 TANGGAL | WAKTU +11 Sep 26 18:24 +0700 +Detail Pesanan +Jenis Kendaraan: +GrabFood +Diterbitkan untuk +Bintang +Pesanan ID +00112712674-C8EUEZDGEEAZTX +Pesanan Dari: +Doner Kebab - Kalibata City +Kalibata City, Foodcourt Teras Kuliner, Lt. Dasar Units Fb. 23 (Doner +Kebab), Jl. Raya Kalibata No. 1, Rawajati, Pancoran, Jakarta Selatan +Profil: +Personal +Detail Tagihan +Detail Pembayaran: +OVO Cash +Detail Pembayaran: Jumlah: +1x Fish n Chips IDR 45000 + +Subtotal IDR 45000 +Restaurant packaging charge IDR 4000 +TOTAL (INCL. TAX) IDR 49000 +`; + +export const briTransferDirectBody = `Halo,*NASYAROBBY NUGRAHA PUTRA* +Berikut ini adalah informasi transaksi yang telah Anda lakukan di Aplikasi +BRImo. +Transaksi Berhasil +Tanggal 13 September 2026 , 19:00:00 WIB +Nomor Referensi 212189333864 +------------------------------ +Sumber Dana NASYAROBBY NUGRAHA PUTRA +0121 **** **** 500 +Jenis Transaksi Transfer Bank BRI +Bank Tujuan BANK BRI +Nomor Tujuan 1060 0100 2070 507 +Nama Tujuan NELLY ARISANTI +Catatan - +------------------------------ +Nominal Rp4.000.000 +Biaya Admin Rp0 +*Total* *Rp4.000.000* + + +Semoga informasi ini dapat bermanfaat bagi Anda.`; + +export const briTransferForwardedBody = `Nasyarobby N. Putra +Boston University MSSD Program + +---------- Forwarded message --------- +From: Bank BRI +Date: Sun, Sep 13, 2026, 7:00 PM +Subject: Pemindahan Dana Sesama Rekening BRI +To: + + +Halo,*NASYAROBBY NUGRAHA PUTRA* +Berikut ini adalah informasi transaksi yang telah Anda lakukan di Aplikasi +BRImo. +Transaksi Berhasil +Tanggal 13 September 2026 , 19:00:00 WIB +Nomor Referensi 212189333864 +------------------------------ +Sumber Dana NASYAROBBY NUGRAHA PUTRA +0121 **** **** 500 +Jenis Transaksi Transfer Bank BRI +Bank Tujuan BANK BRI +Nomor Tujuan 1060 0100 2070 507 +Nama Tujuan NELLY ARISANTI +Catatan - +------------------------------ +Nominal Rp4.000.000 +Biaya Admin Rp0 +*Total* *Rp4.000.000* +`; diff --git a/apps/watcher/test/match.test.js b/apps/watcher/test/match.test.js new file mode 100644 index 0000000..9041dd6 --- /dev/null +++ b/apps/watcher/test/match.test.js @@ -0,0 +1,171 @@ +import assert from "node:assert/strict"; +import { describe, it } from "node:test"; +import { + extractEmail, + parseForwardedHeader, + resolveOriginal, + subjectMatches, +} from "../src/lib/email.js"; +import { matchRule } from "../src/handlers/match-rule.js"; +import { + briTransferDirectBody, + briTransferForwardedBody, + grabBody, + grabForwardedBody, + livinDirectBody, + livinForwardedBody, +} from "./fixtures.js"; + +const silentLog = { + info() {}, + warn() {}, + error() {}, +}; + +describe("extractEmail", () => { + it("parses angle-bracket addresses", () => { + assert.equal( + extractEmail(`"Livin'" `), + "noreply.livin@bankmandiri.co.id", + ); + }); +}); + +describe("parseForwardedHeader", () => { + it("reads original From/Subject from a Fwd block", () => { + const h = parseForwardedHeader(livinForwardedBody); + assert.ok(h); + assert.match(h.from, /noreply\.livin@bankmandiri\.co\.id/); + assert.equal(h.subject, "Pembayaran Berhasil!"); + }); + + it("returns null when there is no Fwd block", () => { + assert.equal(parseForwardedHeader(livinDirectBody), null); + }); +}); + +describe("resolveOriginal", () => { + it("uses envelope for direct mail", () => { + const o = resolveOriginal({ + from: `"Livin'" `, + subject: "Pembayaran Berhasil!", + date: "Sun, 13 Sep 2026 14:48:36 +0700 (WIB)", + body: livinDirectBody, + }); + assert.equal(o.viaForward, false); + assert.equal(extractEmail(o.from), "noreply.livin@bankmandiri.co.id"); + }); + + it("uses Fwd header for forwarded mail", () => { + const o = resolveOriginal({ + from: "NSRB ", + subject: "Fwd: Your Grab E-Receipt", + body: grabForwardedBody, + }); + assert.equal(o.viaForward, true); + assert.equal(extractEmail(o.from), "no-reply@grab.com"); + assert.equal(o.subject, "Your Grab E-Receipt"); + }); +}); + +describe("subjectMatches", () => { + it("matches exact and Fwd-prefixed subjects", () => { + assert.equal(subjectMatches("Pembayaran Berhasil!", "Pembayaran Berhasil!"), true); + assert.equal(subjectMatches("Fwd: Pembayaran Berhasil!", "Pembayaran Berhasil!"), true); + assert.equal(subjectMatches("Hello", "Pembayaran Berhasil!"), false); + }); +}); + +describe("matchRule", () => { + it("matches direct Livin", async () => { + const result = await matchRule({ + messageId: "1a099bd61efd234d", + log: silentLog, + message: { + from: `"Livin'" `, + subject: "Pembayaran Berhasil!", + date: "Sun, 13 Sep 2026 14:48:36 +0700 (WIB)", + body: livinDirectBody, + }, + }); + assert.equal(result.pass.rule.kind, "livin.qr_payment.success"); + }); + + it("matches forwarded Livin via original header", async () => { + const result = await matchRule({ + messageId: "1a099b0c04c4397a", + log: silentLog, + message: { + from: "NSRB ", + subject: "Fwd: Pembayaran Berhasil!", + body: livinForwardedBody, + }, + }); + assert.equal(result.pass.rule.kind, "livin.qr_payment.success"); + }); + + it("matches direct Grab", async () => { + const result = await matchRule({ + messageId: "grab-direct", + log: silentLog, + message: { + from: "Grab ", + subject: "Your Grab E-Receipt", + body: grabBody, + }, + }); + assert.equal(result.pass.rule.kind, "grab.ereceipt.ride"); + }); + + it("matches forwarded Grab via original header", async () => { + const result = await matchRule({ + messageId: "1a099b8b9d239e5e", + log: silentLog, + message: { + from: "NSRB ", + subject: "Fwd: Your Grab E-Receipt", + body: grabForwardedBody, + }, + }); + assert.equal(result.pass.rule.kind, "grab.ereceipt.ride"); + }); + + it("matches direct BRI transfer", async () => { + const result = await matchRule({ + messageId: "bri-direct", + log: silentLog, + message: { + from: "Bank BRI ", + subject: "Pemindahan Dana Sesama Rekening BRI", + body: briTransferDirectBody, + }, + }); + assert.equal(result.pass.rule.kind, "bri.transfer.success"); + }); + + it("matches forwarded BRI transfer via original header", async () => { + const result = await matchRule({ + messageId: "1a09b6cd9a3bc6bd", + log: silentLog, + message: { + from: "Nasyarobby Putra ", + subject: "Fwd: Pemindahan Dana Sesama Rekening BRI", + body: briTransferForwardedBody, + }, + }); + assert.equal(result.pass.rule.kind, "bri.transfer.success"); + }); + + it("breaks on unknown from/subject", async () => { + const result = await matchRule({ + messageId: "x", + log: silentLog, + message: { + from: "Someone ", + subject: "Hello", + body: "hi", + }, + }); + assert.equal(result.break, true); + }); +}); diff --git a/apps/watcher/test/ntfy.test.js b/apps/watcher/test/ntfy.test.js new file mode 100644 index 0000000..3d451e9 --- /dev/null +++ b/apps/watcher/test/ntfy.test.js @@ -0,0 +1,23 @@ +import assert from "node:assert/strict"; +import { describe, it } from "node:test"; +import { formatReceiptAlert } from "../src/notify/ntfy.js"; + +describe("formatReceiptAlert", () => { + it("includes reason, messageId, and body", () => { + const text = formatReceiptAlert({ + reason: "parse failed", + messageId: "abc#1", + kind: "grab.ereceipt.ride", + message: { + from: "Grab ", + subject: "Your Grab E-Receipt", + body: "Total Paid\nRp1.000", + html: "x", + }, + }); + assert.match(text, /reason: parse failed/); + assert.match(text, /messageId: abc#1/); + assert.match(text, /Total Paid/); + assert.match(text, /html \(length\)/); + }); +}); diff --git a/apps/watcher/test/parse-helpers.test.js b/apps/watcher/test/parse-helpers.test.js new file mode 100644 index 0000000..0a72888 --- /dev/null +++ b/apps/watcher/test/parse-helpers.test.js @@ -0,0 +1,17 @@ +import assert from "node:assert/strict"; +import { describe, it } from "node:test"; +import { parseDayMonthYear } from "../src/lib/parse-helpers.js"; + +describe("parseDayMonthYear", () => { + it("parses English months", () => { + assert.equal(parseDayMonthYear("13 Sep 2026"), "2026-09-13"); + assert.equal(parseDayMonthYear("13 September 2026"), "2026-09-13"); + }); + + it("parses Indonesian months", () => { + assert.equal(parseDayMonthYear("15 Agu 2026"), "2026-08-15"); + assert.equal(parseDayMonthYear("15 Agustus 2026"), "2026-08-15"); + assert.equal(parseDayMonthYear("1 Des 26"), "2026-12-01"); + assert.equal(parseDayMonthYear("1 Mei 2026"), "2026-05-01"); + }); +}); diff --git a/apps/watcher/test/parse.test.js b/apps/watcher/test/parse.test.js new file mode 100644 index 0000000..dd72392 --- /dev/null +++ b/apps/watcher/test/parse.test.js @@ -0,0 +1,276 @@ +import assert from "node:assert/strict"; +import { describe, it } from "node:test"; +import { parseBriTransfer } from "../src/parsers/bri.js"; +import { parseGrabReceipt, extractGrabTripTimesFromHtml } from "../src/parsers/grab.js"; +import { parseLivinReceipt, splitMerchantLocation } from "../src/parsers/livin.js"; +import { toSpendingRecord, toPocketBaseDate } from "../src/pocketbase/map.js"; +import { briTransfer } from "../src/rules/bri-transfer.js"; +import { livinQrPayment } from "../src/rules/livin-qr-payment.js"; +import { grabEreceipt } from "../src/rules/grab-ereceipt.js"; +import { + briTransferDirectBody, + briTransferForwardedBody, + grabBody, + grabFoodForwardedBody, + grabForwardedBody, + grabRideHtml, + grabTipForwardedBody, + livinDirectBody, + livinForwardedBody, +} from "./fixtures.js"; + +describe("splitMerchantLocation", () => { + it("splits clean multi-line penerima", () => { + const r = splitMerchantLocation([ + "Eleven Chicken Cabang Gah", + "JAKARTA SELATAN - ID", + ]); + assert.equal(r.merchant, "Eleven Chicken Cabang Gah"); + assert.equal(r.location, "JAKARTA SELATAN - ID"); + }); + + it("keeps glued merchant+city as merchant only", () => { + const r = splitMerchantLocation(["SNACK BAROKAHJAKARTA BARAT - ID"]); + assert.equal(r.merchant, "SNACK BAROKAHJAKARTA BARAT - ID"); + assert.equal(r.location, null); + }); +}); + +describe("parseLivinReceipt", () => { + it("parses direct HTML-stripped body", () => { + const r = parseLivinReceipt(livinDirectBody); + assert.ok(r); + assert.equal(r.amount, 14000); + assert.equal(r.reference, "2609131121540314991"); + assert.equal(r.occurredAt, "2026-09-13T14:48:31+07:00"); + assert.equal(r.details.payerName, "NASYAROBBY NUGRAHA PUTRA"); + assert.equal(r.details.merchant, "SNACK BAROKAHJAKARTA BARAT - ID"); + assert.equal(r.details.location, undefined); + assert.equal(r.details.qrisRef, "609532729372"); + assert.equal(r.details.acquirer, "GoPay"); + }); + + it("parses forwarded clean body", () => { + const r = parseLivinReceipt(livinForwardedBody); + assert.ok(r); + assert.equal(r.amount, 22000); + assert.equal(r.reference, "2609131121539559589"); + assert.equal(r.occurredAt, "2026-09-13T10:43:51+07:00"); + assert.equal(r.details.merchant, "Eleven Chicken Cabang Gah"); + assert.equal(r.details.location, "JAKARTA SELATAN - ID"); + }); + + it("returns null without amount/reference", () => { + assert.equal(parseLivinReceipt("Halo someone\nPenerima\nFoo"), null); + }); + + it("parses Indonesian month abbreviation (Agu)", () => { + const body = `Halo NASYAROBBY NUGRAHA PUTRA, +Penerima +RM Bu Sum Ebony, KalibataJAKARTA SELATAN - ID +Tanggal15 Agu 2026 +Jam09:45:25 WIB +Nominal TransaksiRp 15.000,00 +No. Referensi2608151121545704363 +No. Ref. QRIS608550990105`; + const r = parseLivinReceipt(body); + assert.ok(r); + assert.equal(r.amount, 15000); + assert.equal(r.reference, "2608151121545704363"); + assert.equal(r.occurredAt, "2026-08-15T09:45:25+07:00"); + }); +}); + +describe("parseGrabReceipt", () => { + it("parses direct Grab body", () => { + const r = parseGrabReceipt(grabBody); + assert.ok(r); + assert.equal(r.kind, "grab.ereceipt.ride"); + assert.equal(r.amount, 16600); + assert.equal(r.reference, "A-9RXV4K2GWCLOAV"); + assert.equal(r.occurredAt, "2026-09-13"); + assert.equal(r.details.product, "Bike Hemat"); + assert.equal(r.details.driver, "Mahendra"); + assert.equal(r.details.passenger, "Bintang"); + assert.deepEqual(r.details.compliments, ["Layanan Mantap"]); + }); + + it("merges pickup time from HTML into occurredAt", () => { + const r = parseGrabReceipt(grabBody, { html: grabRideHtml }); + assert.ok(r); + assert.equal(r.occurredAt, "2026-09-13T10:47:00+07:00"); + assert.equal(r.details.dropoffAt, "2026-09-13T10:58:00+07:00"); + }); + + it("extracts standalone AM/PM trip times from HTML", () => { + const t = extractGrabTripTimesFromHtml(grabRideHtml); + assert.equal(t.pickup, "10:47:00"); + assert.equal(t.dropoff, "10:58:00"); + }); + + it("parses forwarded Grab body", () => { + const r = parseGrabReceipt(grabForwardedBody); + assert.ok(r); + assert.equal(r.amount, 16600); + assert.equal(r.reference, "A-9RXV4K2GWCLOAV"); + assert.equal(r.details.product, "Bike Hemat"); + assert.equal(r.kind, "grab.ereceipt.ride"); + }); + + it("parses Grab tip as a separate kind/invoice", () => { + const r = parseGrabReceipt(grabTipForwardedBody); + assert.ok(r); + assert.equal(r.kind, "grab.ereceipt.tip"); + assert.equal(r.variant, "tip"); + assert.equal(r.amount, 2000); + assert.equal(r.reference, "A-9RXV4K2GWCLOAV:tip"); + assert.equal(r.occurredAt, "2026-09-13T10:47:00+07:00"); + assert.equal(r.details.product, "Bike Hemat"); + assert.equal(r.details.variant, "tip"); + }); + + it("parses Grab Food receipt", () => { + const r = parseGrabReceipt(grabFoodForwardedBody); + assert.ok(r); + assert.equal(r.kind, "grab.ereceipt.food"); + assert.equal(r.variant, "food"); + assert.equal(r.amount, 49000); + assert.equal(r.reference, "00112712674-C8EUEZDGEEAZTX"); + assert.equal(r.occurredAt, "2026-09-11T18:24:00+07:00"); + assert.equal(r.details.merchant, "Doner Kebab - Kalibata City"); + assert.equal(r.details.vehicleType, "GrabFood"); + assert.equal(r.details.passenger, "Bintang"); + assert.equal(r.details.paymentMethod, "OVO Cash"); + }); +}); + +describe("parseBriTransfer", () => { + it("parses direct BRImo transfer body", () => { + const r = parseBriTransfer(briTransferDirectBody); + assert.ok(r); + assert.equal(r.amount, 4000000); + assert.equal(r.reference, "212189333864"); + assert.equal(r.occurredAt, "2026-09-13T19:00:00+07:00"); + assert.equal(r.details.payerName, "NASYAROBBY NUGRAHA PUTRA"); + assert.equal(r.details.namaTujuan, "NELLY ARISANTI"); + assert.equal(r.details.jenisTransaksi, "Transfer Bank BRI"); + assert.equal(r.details.bankTujuan, "BANK BRI"); + assert.equal(r.details.nomorTujuan, "1060 0100 2070 507"); + assert.equal(r.details.biayaAdmin, 0); + assert.equal( + r.details.fundingSource, + "NASYAROBBY NUGRAHA PUTRA 0121 **** **** 500", + ); + assert.equal(r.details.catatan, undefined); + }); + + it("parses forwarded BRImo transfer body", () => { + const r = parseBriTransfer(briTransferForwardedBody); + assert.ok(r); + assert.equal(r.amount, 4000000); + assert.equal(r.reference, "212189333864"); + assert.equal(r.details.namaTujuan, "NELLY ARISANTI"); + }); + + it("returns null without amount/reference", () => { + assert.equal(parseBriTransfer("Halo someone\nTransaksi Berhasil"), null); + }); +}); + +describe("toSpendingRecord", () => { + it("maps Livin envelope to spendings fields", () => { + const parsed = { + kind: "livin.qr_payment.success", + messageId: "1a099bd61efd234d", + amount: 14000, + reference: "2609131121540314991", + occurredAt: "2026-09-13T14:48:31+07:00", + details: { + merchant: "SNACK BAROKAHJAKARTA BARAT - ID", + qrisRef: "609532729372", + }, + }; + const rec = toSpendingRecord(parsed, livinQrPayment); + assert.equal(rec.invoice_id, "2609131121540314991"); + assert.equal(rec.amount, 14000); + assert.equal(rec.source, "livin"); + assert.equal(rec.description, "SNACK BAROKAHJAKARTA BARAT - ID"); + assert.equal(rec.category, ""); + assert.equal(rec.message_id, "1a099bd61efd234d"); + assert.equal(rec.trx_date, toPocketBaseDate(parsed.occurredAt)); + assert.deepEqual(rec.parsed, parsed); + }); + + it("maps Grab envelope to spendings fields", () => { + const parsed = { + kind: "grab.ereceipt.ride", + messageId: "1a099b8b9d239e5e", + amount: 16600, + reference: "A-9RXV4K2GWCLOAV", + occurredAt: "2026-09-13", + details: { product: "Bike Hemat", driver: "Mahendra" }, + }; + const rec = toSpendingRecord(parsed, grabEreceipt); + assert.equal(rec.invoice_id, "A-9RXV4K2GWCLOAV"); + assert.equal(rec.source, "grab"); + assert.equal(rec.description, "Grab Bike Hemat"); + assert.equal(rec.trx_date, "2026-09-13 00:00:00.000Z"); + assert.deepEqual(rec.parsed, parsed); + }); + + it("maps Grab tip envelope to spendings fields", () => { + const parsed = { + kind: "grab.ereceipt.tip", + messageId: "1a099e731cb511dc", + amount: 2000, + reference: "A-9RXV4K2GWCLOAV:tip", + occurredAt: "2026-09-13T10:47:00+07:00", + details: { product: "Bike Hemat", variant: "tip" }, + }; + const rec = toSpendingRecord(parsed, grabEreceipt); + assert.equal(rec.invoice_id, "A-9RXV4K2GWCLOAV:tip"); + assert.equal(rec.source, "grab"); + assert.equal(rec.description, "Grab Bike Hemat tip"); + assert.equal(rec.trx_date, toPocketBaseDate(parsed.occurredAt)); + }); + + it("maps Grab Food envelope to spendings fields", () => { + const parsed = { + kind: "grab.ereceipt.food", + messageId: "1a09a07cbd6f58cb", + amount: 49000, + reference: "00112712674-C8EUEZDGEEAZTX", + occurredAt: "2026-09-11T18:24:00+07:00", + details: { + variant: "food", + product: "Food", + merchant: "Doner Kebab - Kalibata City", + }, + }; + const rec = toSpendingRecord(parsed, grabEreceipt); + assert.equal(rec.invoice_id, "00112712674-C8EUEZDGEEAZTX"); + assert.equal(rec.source, "grab"); + assert.equal(rec.description, "Grab Food Doner Kebab - Kalibata City"); + }); + + it("maps BRI transfer envelope to spendings fields", () => { + const parsed = { + kind: "bri.transfer.success", + messageId: "1a09b6cd9a3bc6bd", + amount: 4000000, + reference: "212189333864", + occurredAt: "2026-09-13T19:00:00+07:00", + details: { + namaTujuan: "NELLY ARISANTI", + jenisTransaksi: "Transfer Bank BRI", + }, + }; + const rec = toSpendingRecord(parsed, briTransfer); + assert.equal(rec.invoice_id, "212189333864"); + assert.equal(rec.amount, 4000000); + assert.equal(rec.source, "bri"); + assert.equal(rec.description, "NELLY ARISANTI"); + assert.equal(rec.trx_date, toPocketBaseDate(parsed.occurredAt)); + assert.deepEqual(rec.parsed, parsed); + }); +}); diff --git a/apps/web/index.html b/apps/web/index.html new file mode 100644 index 0000000..b3f0fa9 --- /dev/null +++ b/apps/web/index.html @@ -0,0 +1,12 @@ + + + + + + Gmail Reader + + +
+ + + diff --git a/apps/web/jsconfig.json b/apps/web/jsconfig.json new file mode 100644 index 0000000..afb6c9a --- /dev/null +++ b/apps/web/jsconfig.json @@ -0,0 +1,7 @@ +{ + "compilerOptions": { + "jsx": "react-jsx", + "module": "ESNext", + "moduleResolution": "bundler" + } +} diff --git a/apps/web/package.json b/apps/web/package.json new file mode 100644 index 0000000..4ff699c --- /dev/null +++ b/apps/web/package.json @@ -0,0 +1,22 @@ +{ + "name": "web", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "react": "^19.1.1", + "react-dom": "^19.1.1", + "react-router-dom": "^7.8.2" + }, + "devDependencies": { + "@tailwindcss/vite": "^4.1.13", + "@vitejs/plugin-react": "^5.0.2", + "daisyui": "^5.1.13", + "tailwindcss": "^4.1.13", + "vite": "^7.1.5" + } +} diff --git a/apps/web/src/App.jsx b/apps/web/src/App.jsx new file mode 100644 index 0000000..8409f9f --- /dev/null +++ b/apps/web/src/App.jsx @@ -0,0 +1,56 @@ +import { Navigate, Outlet, Route, Routes } from "react-router-dom"; +import { useAuth } from "./auth.jsx"; +import Layout from "./components/Layout.jsx"; +import Inbox from "./pages/Inbox.jsx"; +import Login from "./pages/Login.jsx"; +import Message from "./pages/Message.jsx"; + +function Protected() { + const { user, loading } = useAuth(); + if (loading) { + return ( +
+ +
+ ); + } + if (!user) return ; + return ( + + + + ); +} + +function GuestOnly({ children }) { + const { user, loading } = useAuth(); + if (loading) { + return ( +
+ +
+ ); + } + if (user) return ; + return children; +} + +export default function App() { + return ( + + + + + } + /> + }> + } /> + } /> + + } /> + + ); +} diff --git a/apps/web/src/api.js b/apps/web/src/api.js new file mode 100644 index 0000000..571f83c --- /dev/null +++ b/apps/web/src/api.js @@ -0,0 +1,29 @@ +export class AuthError extends Error { + constructor() { + super("Not authenticated"); + this.name = "AuthError"; + } +} + +export async function api(path, options = {}) { + const headers = { ...(options.headers || {}) }; + if (options.body && !headers["Content-Type"]) { + headers["Content-Type"] = "application/json"; + } + + const res = await fetch(path, { + credentials: "include", + ...options, + headers, + }); + + if (res.status === 401) { + throw new AuthError(); + } + + const data = await res.json().catch(() => ({})); + if (!res.ok) { + throw new Error(data.error || res.statusText); + } + return data; +} diff --git a/apps/web/src/auth.jsx b/apps/web/src/auth.jsx new file mode 100644 index 0000000..1490fd4 --- /dev/null +++ b/apps/web/src/auth.jsx @@ -0,0 +1,49 @@ +import { createContext, useContext, useEffect, useState } from "react"; +import { api, AuthError } from "./api.js"; + +const AuthContext = createContext(null); + +export function AuthProvider({ children }) { + const [user, setUser] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + let cancelled = false; + api("/api/me") + .then((me) => { + if (!cancelled) setUser(me); + }) + .catch((err) => { + if (!cancelled && !(err instanceof AuthError)) { + console.error(err); + } + if (!cancelled) setUser(null); + }) + .finally(() => { + if (!cancelled) setLoading(false); + }); + return () => { + cancelled = true; + }; + }, []); + + async function logout() { + try { + await api("/auth/logout", { method: "POST" }); + } finally { + setUser(null); + } + } + + return ( + + {children} + + ); +} + +export function useAuth() { + const ctx = useContext(AuthContext); + if (!ctx) throw new Error("useAuth must be used within AuthProvider"); + return ctx; +} diff --git a/apps/web/src/components/Layout.jsx b/apps/web/src/components/Layout.jsx new file mode 100644 index 0000000..fa7cff4 --- /dev/null +++ b/apps/web/src/components/Layout.jsx @@ -0,0 +1,34 @@ +import { Link } from "react-router-dom"; +import { useAuth } from "../auth.jsx"; + +export default function Layout({ children }) { + const { user, logout } = useAuth(); + + return ( +
+
+
+ + Gmail Reader + +
+
+ + {user?.email} + + {user?.picture ? ( +
+
+ +
+
+ ) : null} + +
+
+
{children}
+
+ ); +} diff --git a/apps/web/src/index.css b/apps/web/src/index.css new file mode 100644 index 0000000..9a66a69 --- /dev/null +++ b/apps/web/src/index.css @@ -0,0 +1,17 @@ +@import "tailwindcss"; +@plugin "daisyui" { + themes: corporate --default, business --prefersdark; +} + +html, +body, +#root { + min-height: 100%; +} + +.message-body { + white-space: pre-wrap; + word-break: break-word; + font-family: ui-sans-serif, system-ui, sans-serif; + line-height: 1.55; +} diff --git a/apps/web/src/main.jsx b/apps/web/src/main.jsx new file mode 100644 index 0000000..2f52456 --- /dev/null +++ b/apps/web/src/main.jsx @@ -0,0 +1,16 @@ +import { StrictMode } from "react"; +import { createRoot } from "react-dom/client"; +import { BrowserRouter } from "react-router-dom"; +import App from "./App.jsx"; +import { AuthProvider } from "./auth.jsx"; +import "./index.css"; + +createRoot(document.getElementById("root")).render( + + + + + + + , +); diff --git a/apps/web/src/pages/Inbox.jsx b/apps/web/src/pages/Inbox.jsx new file mode 100644 index 0000000..9367145 --- /dev/null +++ b/apps/web/src/pages/Inbox.jsx @@ -0,0 +1,169 @@ +import { useEffect, useState } from "react"; +import { Link, useSearchParams } from "react-router-dom"; +import { api } from "../api.js"; + +function formatDate(value) { + if (!value) return ""; + const parsed = new Date(value); + if (Number.isNaN(parsed.getTime())) return value; + return parsed.toLocaleString(undefined, { + month: "short", + day: "numeric", + hour: "2-digit", + minute: "2-digit", + }); +} + +export default function Inbox() { + const [searchParams, setSearchParams] = useSearchParams(); + const q = searchParams.get("q") || ""; + const [draft, setDraft] = useState(q); + const [messages, setMessages] = useState([]); + const [nextPageToken, setNextPageToken] = useState(null); + const [loading, setLoading] = useState(true); + const [loadingMore, setLoadingMore] = useState(false); + const [error, setError] = useState(null); + + useEffect(() => { + setDraft(q); + }, [q]); + + useEffect(() => { + let cancelled = false; + setLoading(true); + setError(null); + const params = new URLSearchParams(); + if (q) params.set("q", q); + api(`/api/messages?${params}`) + .then((data) => { + if (cancelled) return; + setMessages(data.messages); + setNextPageToken(data.nextPageToken); + }) + .catch((err) => { + if (!cancelled) setError(err.message); + }) + .finally(() => { + if (!cancelled) setLoading(false); + }); + return () => { + cancelled = true; + }; + }, [q]); + + function onSearch(event) { + event.preventDefault(); + const next = draft.trim(); + if (next) setSearchParams({ q: next }); + else setSearchParams({}); + } + + async function loadMore() { + if (!nextPageToken) return; + setLoadingMore(true); + setError(null); + try { + const params = new URLSearchParams({ pageToken: nextPageToken }); + if (q) params.set("q", q); + const data = await api(`/api/messages?${params}`); + setMessages((prev) => [...prev, ...data.messages]); + setNextPageToken(data.nextPageToken); + } catch (err) { + setError(err.message); + } finally { + setLoadingMore(false); + } + } + + return ( +
+
+ setDraft(e.target.value)} + placeholder='Search Gmail, e.g. from:alice subject:invoice' + aria-label="Search mail" + /> + +
+ + {q ? ( +

+ Results for {q}{" "} + +

+ ) : ( +

Latest mail

+ )} + + {error ? ( +
+ {error} +
+ ) : null} + + {loading ? ( +
+ +
+ ) : messages.length === 0 ? ( +
+
+

No messages found.

+
+
+ ) : ( +
    + {messages.map((message) => ( +
  • + +
    + {message.from} + +
    + {message.subject} + + {message.snippet} + + +
  • + ))} +
+ )} + + {nextPageToken && !loading ? ( +
+ +
+ ) : null} +
+ ); +} diff --git a/apps/web/src/pages/Login.jsx b/apps/web/src/pages/Login.jsx new file mode 100644 index 0000000..56c28fa --- /dev/null +++ b/apps/web/src/pages/Login.jsx @@ -0,0 +1,35 @@ +import { useSearchParams } from "react-router-dom"; + +const ERRORS = { + denied: "Google access was denied.", + missing_code: "OAuth callback was missing a code.", + invalid_state: "OAuth state did not match. Try signing in again.", + exchange_failed: "Could not complete Google sign-in.", +}; + +export default function Login() { + const [params] = useSearchParams(); + const error = ERRORS[params.get("error")] || null; + + return ( +
+
+
+

Gmail Reader

+

+ Sign in with Google to list, search, and read mail. Access is + read-only. +

+ {error ? ( +
+ {error} +
+ ) : null} + + Sign in with Google + +
+
+
+ ); +} diff --git a/apps/web/src/pages/Message.jsx b/apps/web/src/pages/Message.jsx new file mode 100644 index 0000000..9e7d267 --- /dev/null +++ b/apps/web/src/pages/Message.jsx @@ -0,0 +1,80 @@ +import { useEffect, useState } from "react"; +import { Link, useParams } from "react-router-dom"; +import { api } from "../api.js"; + +function formatDate(value) { + if (!value) return ""; + const parsed = new Date(value); + if (Number.isNaN(parsed.getTime())) return value; + return parsed.toLocaleString(); +} + +export default function Message() { + const { id } = useParams(); + const [message, setMessage] = useState(null); + const [error, setError] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + let cancelled = false; + setLoading(true); + setError(null); + api(`/api/messages/${encodeURIComponent(id)}`) + .then((data) => { + if (!cancelled) setMessage(data); + }) + .catch((err) => { + if (!cancelled) setError(err.message); + }) + .finally(() => { + if (!cancelled) setLoading(false); + }); + return () => { + cancelled = true; + }; + }, [id]); + + return ( +
+ + ← Inbox + + + {loading ? ( +
+ +
+ ) : error ? ( +
+ {error} +
+ ) : message ? ( +
+
+

{message.subject}

+
+
+
From
+
{message.from}
+
+ {message.to ? ( +
+
To
+
{message.to}
+
+ ) : null} +
+
Date
+
{formatDate(message.date)}
+
+
+
+
+ {message.body || message.snippet || "(empty message)"} +
+
+
+ ) : null} +
+ ); +} diff --git a/apps/web/vite.config.js b/apps/web/vite.config.js new file mode 100644 index 0000000..b5cb8e8 --- /dev/null +++ b/apps/web/vite.config.js @@ -0,0 +1,16 @@ +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; +import tailwindcss from "@tailwindcss/vite"; + +export default defineConfig({ + plugins: [react(), tailwindcss()], + server: { + port: 5173, + strictPort: true, + proxy: { + "/api": "http://localhost:3000", + "/auth": "http://localhost:3000", + "/callback": "http://localhost:3000", + }, + }, +}); diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..feb32e9 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,5537 @@ +{ + "name": "gmail-summarizer", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "gmail-summarizer", + "workspaces": [ + "apps/*" + ], + "devDependencies": { + "concurrently": "^9.2.1" + }, + "engines": { + "node": ">=20" + } + }, + "apps/api": { + "dependencies": { + "@fastify/secure-session": "^8.2.0", + "@fastify/static": "^8.2.0", + "better-sqlite3": "^12.2.0", + "dotenv": "^17.2.2", + "fastify": "^5.6.0", + "googleapis": "^159.0.0" + } + }, + "apps/watcher": { + "dependencies": { + "better-sqlite3": "^12.2.0", + "dotenv": "^17.2.2", + "fastify": "^5.6.0", + "googleapis": "^159.0.0", + "node-html-parser": "^9.0.4" + } + }, + "apps/web": { + "dependencies": { + "react": "^19.1.1", + "react-dom": "^19.1.1", + "react-router-dom": "^7.8.2" + }, + "devDependencies": { + "@tailwindcss/vite": "^4.1.13", + "@vitejs/plugin-react": "^5.0.2", + "daisyui": "^5.1.13", + "tailwindcss": "^4.1.13", + "vite": "^7.1.5" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", + "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.8", + "@babel/types": "^7.29.8", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz", + "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", + "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", + "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz", + "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.8", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.8", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", + "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", + "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", + "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", + "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", + "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", + "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", + "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", + "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", + "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", + "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", + "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", + "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", + "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", + "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", + "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", + "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", + "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", + "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@fastify/accept-negotiator": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@fastify/accept-negotiator/-/accept-negotiator-2.1.0.tgz", + "integrity": "sha512-F3EVbzWt+xcnVaOHmWyIlpuFtbxOln7HDZQsh09MtMmMm/CipMayNt8hnIL8VQi54u2ZociDbf+iluGYkf7B1A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/@fastify/ajv-compiler": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@fastify/ajv-compiler/-/ajv-compiler-4.0.6.tgz", + "integrity": "sha512-NtuzM0SfaMJbGlnjr9LWQUN5LzgSrbB8tf/wRZNas+4E1O/Nmzl53e7ruT61HDZyRCJGC6FxIogmNZO1c5ETBA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "ajv": "^8.12.0", + "ajv-formats": "^3.0.1", + "fast-uri": "^4.0.0" + } + }, + "node_modules/@fastify/cookie": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/@fastify/cookie/-/cookie-11.1.2.tgz", + "integrity": "sha512-Dtrpk/YOGUsbRMvP/8ZqPpwnMRv0qSqodFdoQ2B589Obc7jw4s4Qla+cV72Bsm7WsZJnqlYFX/i7uSBq0xzg6g==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "cookie": "^2.0.0", + "fastify-plugin": "^6.0.0" + } + }, + "node_modules/@fastify/cookie/node_modules/fastify-plugin": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-6.0.0.tgz", + "integrity": "sha512-fZOty7z3O7vOliF6d8bHE3wiEh1KcNnKEQensSgTk9C1DvN6nRLS++XVd86v33Hw/8u9Un8A1zDrQ8ujcQDHEg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/@fastify/error": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@fastify/error/-/error-4.2.0.tgz", + "integrity": "sha512-RSo3sVDXfHskiBZKBPRgnQTtIqpi/7zhJOEmAxCiBcM7d0uwdGdxLlsCaLzGs8v8NnxIRlfG0N51p5yFaOentQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/@fastify/fast-json-stringify-compiler": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@fastify/fast-json-stringify-compiler/-/fast-json-stringify-compiler-5.1.0.tgz", + "integrity": "sha512-PxcYtKLbQ8Z+yApiqjK8FwxIwvEj38k2OiLc17u8dkJSlmfi2wHHPaSnaoqBPQqtvF8YVsDgDpP2snDCfFrpfw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "fast-json-stringify": "^7.0.0" + } + }, + "node_modules/@fastify/forwarded": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@fastify/forwarded/-/forwarded-3.0.2.tgz", + "integrity": "sha512-NE8HgKLgYejV9lDpqkEFaDKMLYelJBVfHekhB0UKvX0ghagXRJqg68feg8er1NPXxG4N9i6vPxzt8E+3wHfcmA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/@fastify/merge-json-schemas": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@fastify/merge-json-schemas/-/merge-json-schemas-0.2.1.tgz", + "integrity": "sha512-OA3KGBCy6KtIvLf8DINC5880o5iBlDX4SxzLQS8HorJAbqluzLRn80UXU0bxZn7UOFhFgpRJDasfwn9nG4FG4A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/@fastify/proxy-addr": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@fastify/proxy-addr/-/proxy-addr-5.1.0.tgz", + "integrity": "sha512-INS+6gh91cLUjB+PVHfu1UqcB76Sqtpyp7bnL+FYojhjygvOPA9ctiD/JDKsyD9Xgu4hUhCSJBPig/w7duNajw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/forwarded": "^3.0.0", + "ipaddr.js": "^2.1.0" + } + }, + "node_modules/@fastify/secure-session": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@fastify/secure-session/-/secure-session-8.3.0.tgz", + "integrity": "sha512-jyXbYWAp5MCaCA+7idsV+Op3kMN/oDtskZ0SE3QPl53Lo0GFF7p/lk1UEliGaT0OSX+RGSrd2gQlUu1ta1t7yQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/cookie": "^11.0.1", + "fastify-plugin": "^5.0.0", + "sodium-native": "^5.0.1" + }, + "bin": { + "secure-session": "genkey.js" + } + }, + "node_modules/@fastify/send": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@fastify/send/-/send-4.1.1.tgz", + "integrity": "sha512-BYo+EiaKwlxH+WetGk6hAs1d39iP0y1gqB8lGF/qwkJ9ZZ/cBY1vx5NvExb9Sc3yRMFjD5X4Eyh4e4+TzRkzdw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@lukeed/ms": "^2.0.2", + "escape-html": "~1.0.3", + "fast-decode-uri-component": "^1.0.1", + "http-errors": "^2.0.0", + "mime": "^3" + } + }, + "node_modules/@fastify/static": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@fastify/static/-/static-8.3.0.tgz", + "integrity": "sha512-yKxviR5PH1OKNnisIzZKmgZSus0r2OZb8qCSbqmw34aolT4g3UlzYfeBRym+HJ1J471CR8e2ldNub4PubD1coA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/accept-negotiator": "^2.0.0", + "@fastify/send": "^4.0.0", + "content-disposition": "^0.5.4", + "fastify-plugin": "^5.0.0", + "fastq": "^1.17.1", + "glob": "^11.0.0" + } + }, + "node_modules/@isaacs/cliui": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-9.0.0.tgz", + "integrity": "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@lukeed/ms": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@lukeed/ms/-/ms-2.0.2.tgz", + "integrity": "sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@napi-rs/lzma-linux-x64-gnu": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", + "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@pinojs/redact": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz", + "integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==", + "license": "MIT" + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.3", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz", + "integrity": "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.63.1.tgz", + "integrity": "sha512-UZ8sUxPTiHWYX9QNdJedb1kDZSpS1t/VPWBWGSgqHNi9w3Cu6IXvu2mzbhiTiPvtrqgTQJ+zqiAq2iPIPilpaQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.63.1.tgz", + "integrity": "sha512-cQ4nFQABN5cDvDpbvJ7bMStCpnaVxynZrRMfUJYgxcIk9Sh54FIO1vtfkg0B69REjER77ioZ/ov+eAApx/KmLQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.63.1.tgz", + "integrity": "sha512-FQNqd1lRy/0QhDk3xeRIkSBiCpXCiDnZO3YLVdcDKN1UBiKToNftCzcXYNLshmPDUMlu2TdeS8tGcsU6f3YF1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.63.1.tgz", + "integrity": "sha512-pvD16V939D3CloK0+qikpGaxiPrDUXTe7Y5cWOMkMSy7m1cawa8EGy/kXYi/G/cKAC4HDAbSnzCIk1WmsoOKXg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.63.1.tgz", + "integrity": "sha512-pcFGeL2345VwdTnJhA6zLbew+YgWB0qBG2+dMtXjCicf6+rm6kO6cOoh5VnTe0ZMrMRgRyuHmCJxZWrIdzYuOw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.63.1.tgz", + "integrity": "sha512-mRJlqSRulVzcKq/LKA6ICSIc3K/l4fzlVn/gePn2nXIHy8seRi5z/eeRE0d/XMBxcMldiXtQTSpRj0tkkC3g8Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.63.1.tgz", + "integrity": "sha512-YDUNvVM85TI3g/1OpnqKP1h4NeW/j64DfWMf+G3M809xNk1bJSnpFp4sh83NpmVE5DXnkh8ULor4LTVZKoYLHw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.63.1.tgz", + "integrity": "sha512-7Mcn71p9ZuQFAj+h+dhQXy/yeLePRS2yKRnmW1DijA9thKO5qap0GNOIQK4yQ6iP3SU0Mrb/yWo8h8vgRba8lw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.63.1.tgz", + "integrity": "sha512-4YiLQTX6U4CSl0L9cluep9A9W6UmTfqBDc2/CH6wlu54pl4E7Jn3cOD8oxzvBDEGk/JMKgJ47C8g+radF7mwvg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.63.1.tgz", + "integrity": "sha512-2ra8F7w8OquwZN9z2/fKFnli69wa8PLwaVzRMIPGb13ByMJwC28Fbp8YcVGoUhlYMTt7j5j9bNgpysrN2UM+vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.63.1.tgz", + "integrity": "sha512-Sy20ncyhjmBP0Ml+UvQbimjlk6VFgjW5uNP+qqwHB00mTE8Bl2C1TuHTlRwK2YoXeZbee5lP2XevBWVkAQAtSQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.63.1.tgz", + "integrity": "sha512-noITLp8oNjYliPnGWmLyelIHwULGqbHloQHGw1rtxbWhTuWooRpnZarZQJ1y9EUC4szuCusCc+HEpUtxpIwYvA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.63.1.tgz", + "integrity": "sha512-hlxxXd+F1mWiAcaFR7Sv9ZQT6m6UfI8+Vy/kFJzztq2pDMU/0wZ9sish0iszNZvsQDo8Gc0i5yuFEOz5dDf6fA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.63.1.tgz", + "integrity": "sha512-EF7OpqQTQ/BvGqLzUi4rEHuagCV9MugAUXSHemwPW5vxZ75RR+jxO/2j95Ph2dalMpFHSVECjRoioHZgA9zOYA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.63.1.tgz", + "integrity": "sha512-wQO3JesW9PRkwlabQ27y7sPfVOOTLRG73I4F2UYHG5PXun3J9U3y+b7ezVKSYbsvSKGQ1k1cq8Qlun4C9kLt3w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.63.1.tgz", + "integrity": "sha512-ouAGwhO6wHRXdnOVCOsB0tRFkA7nhNB2Nwax6oECXN0YiN8EYUTBAOudADOB1PI+yDL61TeNx/u7MVCzksNbkQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.63.1.tgz", + "integrity": "sha512-q2R38Sn+1J8RxhfJ+T54wSWmyKXWec+9jgDfqO2AtArEqHO5R2aeayp5H5OYLr5UYDVGsVaZPEFUooMhYCdz5A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.63.1.tgz", + "integrity": "sha512-gfI5T24WLLuFfSKw7Go/zDXjAAV0fny0swTaDv+WjK7vqcw4cRhFfdsyKL1n+ukI+ooBxn3bVQnyrn06WpI50w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.63.1.tgz", + "integrity": "sha512-4h6XqthmB4Hspji84wvgk+ElodTsGj+dbZqHJHHtKxj4mYq0ANSEEPX9ys3moJueqsRjwpaJYH7874Itwnj2ow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.63.1.tgz", + "integrity": "sha512-dlfCOa87o1VAYegLQ9EKilx2JCeRofiyPGhTCmqnuXZ6bMPiycO1rq1+sKoulAp7pGLIsTIw+1x5R+zgh5LhhA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.63.1.tgz", + "integrity": "sha512-cjkLbOlfcm3QGhMM1J5zaZjsw1GggbN6rw9UTSSRrPrR1KkcXnN7Uq9rPw34xImQ9VOY9GN+6u2Zj80B9ptkcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.63.1.tgz", + "integrity": "sha512-Li1KdUnWGE4N3e1F/B4RTB1ms+nG4WBgjByO46pkeBVX/2UBsY53xf5vK9WygVmnH3RwncIST7lkSdLSY6P9lg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.63.1.tgz", + "integrity": "sha512-t4ZYOSoLTgwhuFMrmTMLx/+i1DQVK7HYqMc6kY46EApwi8X0nIVphzdNoThU3xt6n+N5urG1/gxBdCaKDLavfg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.63.1.tgz", + "integrity": "sha512-RgroPfMmKlD1RzSDxvwgcPiy2HNQKoYV7OmwIXDsk73uKW5t6B/V8KIy27SMv/FNXFo/oSBtWc9J0X7t91ezZg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.63.1.tgz", + "integrity": "sha512-at8QVep6S3h5Y6gSbdGU06bRY5WJkf6WUduM9YtvYMbYhB1MOFfUgc6kehitQXzOtMSaT70q7f9ydPhpqu821w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@tailwindcss/node": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.3.tgz", + "integrity": "sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.24.1", + "jiti": "^2.7.0", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.3.3" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.3.tgz", + "integrity": "sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.3.3", + "@tailwindcss/oxide-darwin-arm64": "4.3.3", + "@tailwindcss/oxide-darwin-x64": "4.3.3", + "@tailwindcss/oxide-freebsd-x64": "4.3.3", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.3", + "@tailwindcss/oxide-linux-arm64-gnu": "4.3.3", + "@tailwindcss/oxide-linux-arm64-musl": "4.3.3", + "@tailwindcss/oxide-linux-x64-gnu": "4.3.3", + "@tailwindcss/oxide-linux-x64-musl": "4.3.3", + "@tailwindcss/oxide-wasm32-wasi": "4.3.3", + "@tailwindcss/oxide-win32-arm64-msvc": "4.3.3", + "@tailwindcss/oxide-win32-x64-msvc": "4.3.3" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.3.tgz", + "integrity": "sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.3.tgz", + "integrity": "sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.3.tgz", + "integrity": "sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.3.tgz", + "integrity": "sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.3.tgz", + "integrity": "sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.3.tgz", + "integrity": "sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.3.tgz", + "integrity": "sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.3.tgz", + "integrity": "sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.3.tgz", + "integrity": "sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.3.tgz", + "integrity": "sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.11.1", + "@emnapi/runtime": "^1.11.1", + "@emnapi/wasi-threads": "^1.2.2", + "@napi-rs/wasm-runtime": "^1.1.4", + "@tybys/wasm-util": "^0.10.2", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.3.tgz", + "integrity": "sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.3.tgz", + "integrity": "sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.3.3.tgz", + "integrity": "sha512-yYU8cogLeSh/ms2jh8Fj7jaba/EWa7Ja6GoUqYZaraEuCI5YS6ms6ObZgjjedm+jm6XZjdNRWBpPP6Z86oOxcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.3.3", + "@tailwindcss/oxide": "4.3.3", + "tailwindcss": "4.3.3" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7 || ^8" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitejs/plugin-react": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.2.0.tgz", + "integrity": "sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.29.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-rc.3", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.18.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/abstract-logging": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz", + "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==", + "license": "MIT" + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv/node_modules/fast-uri": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.7.tgz", + "integrity": "sha512-dOvZVzjdZdz7phd9v6jCbwxrBW3fK6n8Rc0CtdmM4bumzMnxywBYhuph6J819RRw/ku+rLbelwfMunktuzVVHg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/api": { + "resolved": "apps/api", + "link": true + }, + "node_modules/atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/avvio": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/avvio/-/avvio-9.3.0.tgz", + "integrity": "sha512-g2tQ7LE7oOSqDfwEm3M+ZCMTJc7KiZCdJ4UwyZJb5ckTKyYu50OYmvv0mCFXPuYXoM4zkSt8zM9XQ9KCvxA74A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/error": "^4.0.0", + "fastq": "^1.17.1" + } + }, + "node_modules/b4a": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.1.tgz", + "integrity": "sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw==", + "license": "Apache-2.0", + "peerDependencies": { + "react-native-b4a": "*" + }, + "peerDependenciesMeta": { + "react-native-b4a": { + "optional": true + } + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/bare-addon-resolve": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/bare-addon-resolve/-/bare-addon-resolve-1.10.1.tgz", + "integrity": "sha512-F/SD2du8keuYSb4xipnGz5j2E6yhNdHA8ZVxtHae6h2uOrpBIjjbhXvjzKZbr5XUOzqBzh/i8GVFycj2DlFQIA==", + "license": "Apache-2.0", + "dependencies": { + "bare-module-resolve": "^1.10.0", + "bare-semver": "^1.0.0" + }, + "peerDependencies": { + "bare-url": "*" + }, + "peerDependenciesMeta": { + "bare-url": { + "optional": true + } + } + }, + "node_modules/bare-ansi-escapes": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/bare-ansi-escapes/-/bare-ansi-escapes-2.2.3.tgz", + "integrity": "sha512-02ES4/E2RbrtZSnHJ9LntBhYkLA6lPpSEeP8iqS3MccBIVhVBlEmruF1I7HZqx5Q8aiTeYfQVeqmrU9YO2yYoQ==", + "license": "Apache-2.0", + "dependencies": { + "bare-stream": "^2.6.5" + }, + "peerDependencies": { + "bare-buffer": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + } + } + }, + "node_modules/bare-assert": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/bare-assert/-/bare-assert-1.2.0.tgz", + "integrity": "sha512-c6uvgvTJBspTDxtVnPgrBKmLgcpW3Fp72NVKDLg6oT4QjQbhGtvrkHMhGYMK1sh4vjBHOBmuUalyt9hSzV37fQ==", + "license": "Apache-2.0", + "dependencies": { + "bare-inspect": "^3.1.2" + } + }, + "node_modules/bare-events": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.9.2.tgz", + "integrity": "sha512-AIPKioV7/Y/8KfZ3AAhjPJxLLbY49S64Ym5DakZlUg75qQiTgUq9hEJoEwa4eUezPUlXRy/i5NpsKvo9jgKmoA==", + "license": "Apache-2.0", + "peerDependencies": { + "bare-abort-controller": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + } + } + }, + "node_modules/bare-inspect": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/bare-inspect/-/bare-inspect-3.1.10.tgz", + "integrity": "sha512-Qu01XZNTIT2/bS9k7c6A3OXuAzBnWwDCejGri76BUltdFqVe6Et/IxVOSSbcDpreSSWwqQ5pAu/bxFE6hPDWRQ==", + "license": "Apache-2.0", + "dependencies": { + "bare-ansi-escapes": "^2.1.0", + "bare-type": "^1.0.0" + }, + "engines": { + "bare": ">=1.18.0" + } + }, + "node_modules/bare-module-resolve": { + "version": "1.12.5", + "resolved": "https://registry.npmjs.org/bare-module-resolve/-/bare-module-resolve-1.12.5.tgz", + "integrity": "sha512-VOncxVvVk8SQVw9vhcBnoTJD/74aR5DgdRPCm0gQ7uB5MsWpBJnoCeJrwEHKiz09O83ndf3NTjsz3LEuFxAq5A==", + "license": "Apache-2.0", + "dependencies": { + "bare-semver": "^1.0.0" + }, + "peerDependencies": { + "bare-url": "*" + }, + "peerDependenciesMeta": { + "bare-url": { + "optional": true + } + } + }, + "node_modules/bare-semver": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/bare-semver/-/bare-semver-1.1.0.tgz", + "integrity": "sha512-1Hw5qJ7hXdVt3uPUqjeFTuxyvBUJauvz5A1I2jk8gzjZMHp04n//6nV9MDbG9CMw78JHY2lGV0w6s//LrASm2w==", + "license": "Apache-2.0" + }, + "node_modules/bare-stream": { + "version": "2.13.4", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.13.4.tgz", + "integrity": "sha512-PcrQ8lVLbiJscNm1Kez+Yp4Gy4AHGcN1lzwjvf5NybWen7VvEgUfyfnXYJ2zNqWnzOfCb1Abq6lH8ti0syQszA==", + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.8.1", + "streamx": "^2.25.0", + "teex": "^1.0.1" + }, + "peerDependencies": { + "bare-abort-controller": "*", + "bare-buffer": "*", + "bare-events": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + }, + "bare-buffer": { + "optional": true + }, + "bare-events": { + "optional": true + } + } + }, + "node_modules/bare-type": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/bare-type/-/bare-type-1.1.1.tgz", + "integrity": "sha512-5HwbjDbnYr+Lwu5I59IqwOFcRcVo4jxo408t42gKhKnikw0ojwyPXPlx58AQP4dR2VvGw57ppX/qxYCn60Wujw==", + "license": "Apache-2.0", + "engines": { + "bare": ">=1.2.0" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.21", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.21.tgz", + "integrity": "sha512-uh8vpY/1/YyFkunIDFH/12p7/7VdPKA1hejMVEbdkEaWnUz0Hesvx5EbiU6XxjyHZIOju+ZMbQJkRh+es3/spQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/better-sqlite3": { + "version": "12.11.1", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.11.1.tgz", + "integrity": "sha512-dq9AtApgg5PGFtBzPFSBl3HZQjHok5gaQCM6zh2Yk0aSmDCs1CbnVI8/HgASQkNKsWFpseIO9beg5xxpYhbIfA==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + }, + "engines": { + "node": "20.x || 22.x || 23.x || 24.x || 25.x || 26.x" + } + }, + "node_modules/bignumber.js": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "license": "ISC" + }, + "node_modules/brace-expansion": { + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/browserslist": { + "version": "4.28.9", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.9.tgz", + "integrity": "sha512-EWazOblFYUvlGZcfGhPUPmYh3nikUxBVb+y9MJun5f3hBi812X+8MSQTujLBtgK3cf51fJWbWfOjyeO954d+Eg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.11.20", + "caniuse-lite": "^1.0.30001810", + "electron-to-chromium": "^1.5.420", + "node-releases": "^2.0.54", + "update-browserslist-db": "^1.3.2" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001810", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001810.tgz", + "integrity": "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/concurrently": { + "version": "9.2.4", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.4.tgz", + "integrity": "sha512-TZ0CEhyzvFjgtAvHTusDMgj7wNdihCh7LLLrzdUOXIhdlnL2JBBGA9eJxR24rtqgmdjh3OA3hrN1rCHj6HM8qA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "4.1.2", + "rxjs": "7.8.2", + "shell-quote": "1.9.0", + "supports-color": "8.1.1", + "tree-kill": "1.2.2", + "yargs": "17.7.2" + }, + "bin": { + "conc": "dist/bin/concurrently.js", + "concurrently": "dist/bin/concurrently.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-2.0.1.tgz", + "integrity": "sha512-yuToqVvRrj6pfDXREyQAAv8SkAEk/8GS3jQRTiUMm66TVtBYmqQeoEjL2Lmq8Rpo6271vH76InTChTitEAm65w==", + "license": "MIT", + "engines": { + "node": ">=22" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-select": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/daisyui": { + "version": "5.7.28", + "resolved": "https://registry.npmjs.org/daisyui/-/daisyui-5.7.28.tgz", + "integrity": "sha512-BxQap2QOj7NQy5nEsjTHRmybmUZP3t+O5G6vfdidZuptBsh/QyKN9wbdrGDZk198fMI3UyYnA/rCWlggZIhGnw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/saadeghi/daisyui?sponsor=1" + } + }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/dom-serializer/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dotenv": { + "version": "17.4.2", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", + "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "license": "MIT" + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.423", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.423.tgz", + "integrity": "sha512-rRZfTSY8ptHYMQxa+uIycJMFKmY1T0GIApNMXJYGehguTZa56TEEl19pKPCoBqk5Gpf7QizZn/jt7xur+DYxag==", + "dev": true, + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.24.5", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.5.tgz", + "integrity": "sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/entities": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.1.0.tgz", + "integrity": "sha512-kxL7msIffSuh9aaFAMD7rxAIuTRMAHMeBtgHW2yUdWw732ZNh4MehkF2gdjvtdmikkaIP9bFDDJOPlsvm7avrA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/events-universal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", + "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.7.0" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "node_modules/fast-decode-uri-component": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz", + "integrity": "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==", + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "license": "MIT" + }, + "node_modules/fast-json-stringify": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fast-json-stringify/-/fast-json-stringify-7.0.1.tgz", + "integrity": "sha512-eRSayARSbbwlBjpP4vnTTIRD5QPcIrmihPxDeN1DtKnHPg66UuJLx+8hlK1kaFdjvzyQ/dzALoi4vwAQ+T+iZA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/merge-json-schemas": "^0.2.0", + "ajv": "^8.12.0", + "ajv-formats": "^3.0.1", + "fast-uri": "^4.0.0", + "json-schema-ref-resolver": "^3.0.0", + "rfdc": "^1.2.0" + } + }, + "node_modules/fast-querystring": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fast-querystring/-/fast-querystring-1.1.2.tgz", + "integrity": "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==", + "license": "MIT", + "dependencies": { + "fast-decode-uri-component": "^1.0.1" + } + }, + "node_modules/fast-uri": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-4.1.4.tgz", + "integrity": "sha512-dODXrIxlS9JSdgAnhIUKOosKV1oMtU2VtVw87QRaHzyl5jxO290Ii5tEZfCfzfWNHi3jKWwBSdQj0qIyshdZdQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fastify": { + "version": "5.12.3", + "resolved": "https://registry.npmjs.org/fastify/-/fastify-5.12.3.tgz", + "integrity": "sha512-reZ8wce5VNCcufIt9AVtzZa3L4u1j8esikn7OEgHWLVpRpL5R7Y2+Xzj70OUkv5zDfzUAxXZT6cu4Rt0zr3EKA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/ajv-compiler": "^4.0.5", + "@fastify/error": "^4.0.0", + "@fastify/fast-json-stringify-compiler": "^5.0.0", + "@fastify/proxy-addr": "^5.0.0", + "abstract-logging": "^2.0.1", + "avvio": "^9.0.0", + "fast-json-stringify": "^7.0.0", + "find-my-way": "^9.6.0", + "light-my-request": "^6.0.0", + "pino": "^9.14.0 || ^10.1.0", + "process-warning": "^5.1.0", + "rfdc": "^1.3.1", + "secure-json-parse": "^4.0.0", + "semver": "^7.6.0", + "toad-cache": "^3.7.0" + } + }, + "node_modules/fastify-plugin": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-5.1.0.tgz", + "integrity": "sha512-FAIDA8eovSt5qcDgcBvDuX/v0Cjz0ohGhENZ/wpc3y+oZCY2afZ9Baqql3g/lC+OHRnciQol4ww7tuthOb9idw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/fastify/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/fastq": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.3.tgz", + "integrity": "sha512-XKv5nnLs6nLF71NgiKJLIZFLkPyIEuOselLG7ujZnGrRfQK8HpvY+WqKhAJUAdLomwVHErVS4LfxFlPq0/FTAw==", + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" + }, + "node_modules/find-my-way": { + "version": "9.9.0", + "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-9.9.0.tgz", + "integrity": "sha512-sJsgZ1sQH2UDuowPuMKg8az7Qc8F0jnj+SKkFWU/+T0xcFlgV5skgXOGUqmQzOdmW6ALA7AhJINWx3qFBkbLHA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-querystring": "^1.0.0", + "safe-regex2": "^5.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gaxios": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.3.1.tgz", + "integrity": "sha512-kB3rzJV7d9juLZh8/56QTXCwQfxyhdOMdyYk1HdQKFtF8TJTDTZQJtixWIwXdE9Jji91mC41DUNpjleo4L4eAQ==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/gcp-metadata": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.2.tgz", + "integrity": "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==", + "license": "Apache-2.0", + "dependencies": { + "gaxios": "^7.0.0", + "google-logging-utils": "^1.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, + "node_modules/glob": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.1.0.tgz", + "integrity": "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "BlueOak-1.0.0", + "dependencies": { + "foreground-child": "^3.3.1", + "jackspeak": "^4.1.1", + "minimatch": "^10.1.1", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/google-auth-library": { + "version": "10.9.1", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.9.1.tgz", + "integrity": "sha512-i1ydyHrqcIxXkWh/uBmVkzCvIuq5yiK2ATndIe5XxKholrG/MTYP9xGYka4sQhrbIAgGjL2B6NOE7rFaiF3fXw==", + "license": "Apache-2.0", + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^7.1.4", + "gcp-metadata": "8.1.2", + "google-logging-utils": "1.1.3", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/google-logging-utils": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz", + "integrity": "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/googleapis": { + "version": "159.0.0", + "resolved": "https://registry.npmjs.org/googleapis/-/googleapis-159.0.0.tgz", + "integrity": "sha512-halby2+lQwHNxUDk70aQNXP1BlBwdwr7svTJZvDi7vKwrWbVMKhVrZ86h8p3zRcWbO4qAgLQ4ODAf8TgD3DhGQ==", + "license": "Apache-2.0", + "dependencies": { + "google-auth-library": "^10.2.0", + "googleapis-common": "^8.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/googleapis-common": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/googleapis-common/-/googleapis-common-8.0.3.tgz", + "integrity": "sha512-7g1yzQKx0mmNTjiK0H9dJ8eqKqDBveES9vLHeg5neb3BMQy/d1oQefIMhIpOVT8a+f+LOcixMEdRbFIW/cQUJw==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "gaxios": "7.1.3", + "google-auth-library": "10.5.0", + "google-logging-utils": "1.1.3", + "qs": "^6.7.0", + "url-template": "^2.0.8" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/googleapis-common/node_modules/gaxios": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.3.tgz", + "integrity": "sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.2", + "rimraf": "^5.0.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/googleapis-common/node_modules/google-auth-library": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.5.0.tgz", + "integrity": "sha512-7ABviyMOlX5hIVD60YOfHw4/CxOfBhyduaYB+wbFWCWoni4N7SLcV46hrVRktuBbZjFC9ONyqamZITN7q3n32w==", + "license": "Apache-2.0", + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^7.0.0", + "gcp-metadata": "^8.0.0", + "google-logging-utils": "^1.0.0", + "gtoken": "^8.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/gtoken": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-8.0.0.tgz", + "integrity": "sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw==", + "license": "MIT", + "dependencies": { + "gaxios": "^7.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.5.0.tgz", + "integrity": "sha512-aq+t5NAc+cS6rZQQVWC2x98CPqGtKKTMDd4Gaodv0wShnItdKg/51djkGJ1hqH+Oy0ivDftCbSLCQob8zso01w==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jackspeak": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.2.3.tgz", + "integrity": "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^9.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "license": "MIT", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, + "node_modules/json-schema-ref-resolver": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/json-schema-ref-resolver/-/json-schema-ref-resolver-3.0.0.tgz", + "integrity": "sha512-hOrZIVL5jyYFjzk7+y7n5JDzGlU8rfWDuYyHwGa2WA8/pcmMHezp2xsVwxrebD/Q9t8Nc5DboieySDpCp4WG4A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/light-my-request": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-6.6.0.tgz", + "integrity": "sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause", + "dependencies": { + "cookie": "^1.0.1", + "process-warning": "^4.0.0", + "set-cookie-parser": "^2.6.0" + } + }, + "node_modules/light-my-request/node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/light-my-request/node_modules/process-warning": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-4.0.1.tgz", + "integrity": "sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, + "node_modules/node-abi": { + "version": "3.96.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.96.0.tgz", + "integrity": "sha512-rebQ/lz7i0EkoLzUVSrKRzA69zMkwLp95kKMWoMDkkM00Suxz0D7zEQPwRml5fQum24mj7bPvmlgLAmu2JCiYg==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-abi/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/node-html-parser": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/node-html-parser/-/node-html-parser-9.0.4.tgz", + "integrity": "sha512-reUSrZkF1rO7hEYe5mJwtbVcQ6mB5QrViCZH6Wv+iz3er/PA9Q50gol15dMLQRDfdli4VzqPMPR24HNRFgVKFw==", + "license": "MIT", + "dependencies": { + "css-select": "^5.1.0", + "entities": "^8.0.0" + } + }, + "node_modules/node-releases": { + "version": "2.0.54", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.54.tgz", + "integrity": "sha512-YHs7BmmcsdAI5Ozuf8JZo6PT0mv2GIWC9vMfvUC3dp65M8hn7Ux8CPL+2oBI7juNuj9d0ndhTcznq2ODBps9cQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "license": "BlueOak-1.0.0" + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pino": { + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/pino/-/pino-10.3.1.tgz", + "integrity": "sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==", + "license": "MIT", + "dependencies": { + "@pinojs/redact": "^0.4.0", + "atomic-sleep": "^1.0.0", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^3.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^5.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^4.0.1", + "thread-stream": "^4.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-abstract-transport": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-3.0.0.tgz", + "integrity": "sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==", + "license": "MIT", + "dependencies": { + "split2": "^4.0.0" + } + }, + "node_modules/pino-std-serializers": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz", + "integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==", + "license": "MIT" + }, + "node_modules/postcss": { + "version": "8.5.28", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz", + "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.18", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/process-warning": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.1.0.tgz", + "integrity": "sha512-jQSaVHsPgtyw60e1rQ/A+/ArPEj/S8pS/vFnyGa/gYFXrKk/6RuDkoqVDQ5NI5MmS01698ltlAk0NoDBNLujRw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/qs": { + "version": "6.16.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.16.0.tgz", + "integrity": "sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA==", + "license": "BSD-3-Clause", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", + "license": "MIT" + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/react": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz", + "integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz", + "integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.8" + } + }, + "node_modules/react-refresh": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.3.tgz", + "integrity": "sha512-gyXgtdr5uACJ5b1Q4udzjVV+tb/rlHIMJKuJ0e89R4Kzgz47z/rgP0dIKxktqIEUhDHluGTPJJH/wRha7CyqsA==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.18.3.tgz", + "integrity": "sha512-ytVbyBBM7vMfRCam25r0WMhSVSom909A8p+8m0/f1w853dz/xfFu6etAT2SEbVoSnI+ZoPRDqIsQXVT89gp7kg==", + "license": "MIT", + "dependencies": { + "react-router": "7.18.3" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/react-router/node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/require-addon": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/require-addon/-/require-addon-1.2.0.tgz", + "integrity": "sha512-VNPDZlYgIYQwWp9jMTzljx+k0ZtatKlcvOhktZ/anNPI3dQ9NXk7cq2U4iJ1wd9IrytRnYhyEocFWbkdPb+MYA==", + "license": "Apache-2.0", + "dependencies": { + "bare-addon-resolve": "^1.3.0" + }, + "engines": { + "bare": ">=1.10.0" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ret": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.5.0.tgz", + "integrity": "sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "license": "MIT" + }, + "node_modules/rimraf": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", + "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==", + "license": "ISC", + "dependencies": { + "glob": "^10.3.7" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/rimraf/node_modules/ansi-regex": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.3.0.tgz", + "integrity": "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/rimraf/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/rimraf/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/rimraf/node_modules/brace-expansion": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", + "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/rimraf/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT" + }, + "node_modules/rimraf/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/rimraf/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, + "node_modules/rimraf/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/rimraf/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/rimraf/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/rollup": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.63.1.tgz", + "integrity": "sha512-3Df9jsstwhccuEfmAMi9l8XUh/GOkVObmFTU7CCVBysEbcOZLl84jCtaAZMcPiMz2EGKsATzQcU+Xr3n/wU6cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@napi-rs/lzma-linux-x64-gnu": "1.5.1", + "@rollup/rollup-android-arm-eabi": "4.63.1", + "@rollup/rollup-android-arm64": "4.63.1", + "@rollup/rollup-darwin-arm64": "4.63.1", + "@rollup/rollup-darwin-x64": "4.63.1", + "@rollup/rollup-freebsd-arm64": "4.63.1", + "@rollup/rollup-freebsd-x64": "4.63.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.63.1", + "@rollup/rollup-linux-arm-musleabihf": "4.63.1", + "@rollup/rollup-linux-arm64-gnu": "4.63.1", + "@rollup/rollup-linux-arm64-musl": "4.63.1", + "@rollup/rollup-linux-loong64-gnu": "4.63.1", + "@rollup/rollup-linux-loong64-musl": "4.63.1", + "@rollup/rollup-linux-ppc64-gnu": "4.63.1", + "@rollup/rollup-linux-ppc64-musl": "4.63.1", + "@rollup/rollup-linux-riscv64-gnu": "4.63.1", + "@rollup/rollup-linux-riscv64-musl": "4.63.1", + "@rollup/rollup-linux-s390x-gnu": "4.63.1", + "@rollup/rollup-linux-x64-gnu": "4.63.1", + "@rollup/rollup-linux-x64-musl": "4.63.1", + "@rollup/rollup-openbsd-x64": "4.63.1", + "@rollup/rollup-openharmony-arm64": "4.63.1", + "@rollup/rollup-win32-arm64-msvc": "4.63.1", + "@rollup/rollup-win32-ia32-msvc": "4.63.1", + "@rollup/rollup-win32-x64-gnu": "4.63.1", + "@rollup/rollup-win32-x64-msvc": "4.63.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-regex2": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-5.1.1.tgz", + "integrity": "sha512-mOSBvHGDZMuIEZMdOz/aCEYDCv0E7nfcNsIhUF+/P+xC7Hyf3FkvymqgPbg9D1EdSGu+uKbJgy09K/RKKc7kJA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "ret": "~0.5.0" + }, + "bin": { + "safe-regex2": "bin/safe-regex2.js" + } + }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/secure-json-parse": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-4.1.0.tgz", + "integrity": "sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/shell-quote": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.9.0.tgz", + "integrity": "sha512-Iov+JwFv/2HcTpcwNMKd8+IWNb8tboQJNQTkAY/LLVK7gGH9jy+LGkVqPxfekHl+yMmiqXszdGWXgkfml7hjqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/sodium-native": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/sodium-native/-/sodium-native-5.1.0.tgz", + "integrity": "sha512-3RxgyWyJlhTsABPnJVpCI5CoTDANZTqqFrEPqr+kjfnRaBihpVtMUE3yTF40ukdoB1APXeoBNKF3MzZAIHg39g==", + "license": "MIT", + "dependencies": { + "bare-assert": "^1.2.0", + "require-addon": "^1.1.0", + "which-runtime": "^1.2.1" + }, + "engines": { + "bare": ">=1.16.0" + } + }, + "node_modules/sonic-boom": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz", + "integrity": "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==", + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/streamx": { + "version": "2.28.1", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.28.1.tgz", + "integrity": "sha512-zEzXb0s5Cds7tqMH6rhZ05lcJydCWiQPEwiNngVqzsxCc962vLY4Uw+mW7od8kDH258k2Uz/JrOkdIAAhSh9VA==", + "license": "MIT", + "dependencies": { + "events-universal": "^1.0.0", + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/tailwindcss": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.3.tgz", + "integrity": "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tar-fs": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.5.tgz", + "integrity": "sha512-OboTd8mmMhZDNPV+UjQcK9yKAatXu2aJ+r1w4im1Otd4M4fl2hwvdoXUxIYHFTHWK/3y3FarBP70v3vwmGlOxw==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/teex": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", + "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", + "license": "MIT", + "dependencies": { + "streamx": "^2.12.5" + } + }, + "node_modules/text-decoder": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz", + "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==", + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.6.4" + } + }, + "node_modules/thread-stream": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-4.2.0.tgz", + "integrity": "sha512-e2zZ96wSChazBsbENf/Pcm/4swHt2cEKQ92rhUjkL9GCKiTDJIaTBenjE/m9DXi0QBmTMDkFDdOomUy20A1tDQ==", + "license": "MIT", + "dependencies": { + "real-require": "^1.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/thread-stream/node_modules/real-require": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-1.0.0.tgz", + "integrity": "sha512-P4nbQYQfePJxRSmY+v/KINxVucm4NF3p3s7pJveMTtom52FR4YGltUQLB8idDXwDDWW+eYrWDFbuzUnjoWHF7g==", + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/toad-cache": { + "version": "3.7.4", + "resolved": "https://registry.npmjs.org/toad-cache/-/toad-cache-3.7.4.tgz", + "integrity": "sha512-m1TdR/rvT7kgGJZhspNtXdsdYk0fddFpJJFlG5s+UkPFo6lkLoZ3YLOaovPYjq1R75NP5JfeTlSHaOsE09peCg==", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.2.tgz", + "integrity": "sha512-UQ+MSxlhRm1bzjhU+DcuXfjFO1FzNtqhK5+9Yvlp90ItDLk5vT932A0rFu619nf7RVS+Y/VeaUW1jaRDqZ8VJw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/url-template": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/url-template/-/url-template-2.0.8.tgz", + "integrity": "sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==", + "license": "BSD" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/vite": { + "version": "7.3.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.6.tgz", + "integrity": "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0 || ^0.28.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/watcher": { + "resolved": "apps/watcher", + "link": true + }, + "node_modules/web": { + "resolved": "apps/web", + "link": true + }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-runtime": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/which-runtime/-/which-runtime-1.4.0.tgz", + "integrity": "sha512-0ugbP4CJW4e2D20jvEcC4973dCgIaHI4Rw1PT+26U9zEve7FyYdWAIwUnoeOYvoCfn+wXHoHTKb1KhkYlb60Pw==", + "license": "Apache-2.0" + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..9a5515b --- /dev/null +++ b/package.json @@ -0,0 +1,26 @@ +{ + "name": "gmail-summarizer", + "private": true, + "workspaces": [ + "apps/*" + ], + "scripts": { + "dev": "concurrently -k -n api,web -c cyan,magenta \"npm run dev -w api\" \"npm run dev -w web\"", + "dev:api": "npm run dev -w api", + "dev:web": "npm run dev -w web", + "dev:watcher": "npm run dev -w watcher", + "test": "npm run test -w watcher", + "test:watcher": "npm run test -w watcher", + "process-messages": "npm run process-messages -w watcher --", + "list-messages": "npm run list-messages -w watcher --", + "build": "npm run build -w web", + "start": "npm run start -w api", + "start:watcher": "npm run start -w watcher" + }, + "engines": { + "node": ">=20" + }, + "devDependencies": { + "concurrently": "^9.2.1" + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..763dabb --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,206 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + devDependencies: + concurrently: + specifier: ^9.2.1 + version: 9.2.4 + +packages: + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + concurrently@9.2.4: + resolution: {integrity: sha512-TZ0CEhyzvFjgtAvHTusDMgj7wNdihCh7LLLrzdUOXIhdlnL2JBBGA9eJxR24rtqgmdjh3OA3hrN1rCHj6HM8qA==} + engines: {node: '>=18'} + hasBin: true + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + rxjs@7.8.2: + resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} + + shell-quote@1.9.0: + resolution: {integrity: sha512-Iov+JwFv/2HcTpcwNMKd8+IWNb8tboQJNQTkAY/LLVK7gGH9jy+LGkVqPxfekHl+yMmiqXszdGWXgkfml7hjqA==} + engines: {node: '>= 0.4'} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-color@8.1.1: + resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} + engines: {node: '>=10'} + + tree-kill@1.2.2: + resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} + hasBin: true + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + +snapshots: + + ansi-regex@5.0.1: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + concurrently@9.2.4: + dependencies: + chalk: 4.1.2 + rxjs: 7.8.2 + shell-quote: 1.9.0 + supports-color: 8.1.1 + tree-kill: 1.2.2 + yargs: 17.7.2 + + emoji-regex@8.0.0: {} + + escalade@3.2.0: {} + + get-caller-file@2.0.5: {} + + has-flag@4.0.0: {} + + is-fullwidth-code-point@3.0.0: {} + + require-directory@2.1.1: {} + + rxjs@7.8.2: + dependencies: + tslib: 2.8.1 + + shell-quote@1.9.0: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-color@8.1.1: + dependencies: + has-flag: 4.0.0 + + tree-kill@1.2.2: {} + + tslib@2.8.1: {} + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + y18n@5.0.8: {} + + yargs-parser@21.1.1: {} + + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1