README.md
# Treasury Operations Demo
This workspace contains a fictional multi-service treasury operations portal for local benchmarking only.
Services:
- `portal/` - TypeScript/JavaScript admin portal built on Express
- `payments-api/` - Go API for transfers and settlement jobs
- `docs-worker/` - Python Flask service for documents and background work
- `risk-api/` - Java HTTP API for risk operations
Each service uses local persistence and exposes a few thin HTTP routes with middleware, audit logging, and basic RBAC checks.
docs-worker/app.py
from __future__ import annotations
import json
import os
import queue
import sqlite3
import threading
from datetime import datetime
from pathlib import Path
from flask import Flask, Response, g, jsonify, request, send_file
BASE_DIR = Path(__file__).resolve().parent
DATA_DIR = Path(os.environ.get("DOCS_WORKER_DATA_DIR", str(BASE_DIR / "data"))).resolve()
STORAGE_DIR = DATA_DIR / "storage"
DB_PATH = DATA_DIR / "docs.db"
DATA_DIR.mkdir(parents=True, exist_ok=True)
STORAGE_DIR.mkdir(parents=True, exist_ok=True)
app = Flask(__name__)
job_queue: "queue.Queue[dict]" = queue.Queue()
def get_db() -> sqlite3.Connection:
if "db" not in g:
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
g.db = conn
return g.db
def init_db() -> None:
db = sqlite3.connect(DB_PATH)
db.executescript(
"""
create table if not exists organizations (
id integer primary key autoincrement,
name text not null,
org_code text not null unique,
region text not null,
tier text not null
);
create table if not exists teams (
id integer primary key autoincrement,
org_id integer not null,
name text not null,
cost_center text not null,
status text not null default 'active'
);
create table if not exists users (
id integer primary key autoincrement,
org_id integer not null,
team_id integer,
email text not null,
display_name text not null,
role text not null,
status text not null default 'active'
);
create table if not exists accounts (
id integer primary key autoincrement,
org_id integer not null,
account_no text not null,
currency text not null,
balance numeric not null default 0
);
create table if not exists beneficiaries (
id integer primary key autoincrement,
org_id integer not null,
name text not null,
bank_name text not null,
routing text not null
);
create table if not exists transfers (
id integer primary key autoincrement,
org_id integer not null,
account_id integer not null,
beneficiary_id integer not null,
amount numeric not null,
currency text not null,
status text not null,
memo text not null,
requested_by text not null
);
create table if not exists documents (
id integer primary key autoincrement,
org_id integer not null,
filename text not null,
original_name text not null,
mime_type text not null,
kind text not null,
created_at text not null default current_timestamp
);
create table if not exists risk_reviews (
id integer primary key autoincrement,
org_id integer not null,
subject text not null,
score numeric not null,
note text not null,
created_at text not null default current_timestamp
);
create table if not exists audit_records (
id integer primary key autoincrement,
org_id integer,
actor text not null,
action text not null,
resource text not null,
details text not null,
created_at text not null default current_timestamp
);
"""
)
if db.execute("select count(*) from organizations").fetchone()[0] == 0:
db.execute(
"insert into organizations (name, org_code, region, tier) values (?, ?, ?, ?)",
("Acme Treasury", "acme", "us-east", "enterprise"),
)
db.execute(
"insert into organizations (name, org_code, region, tier) values (?, ?, ?, ?)",
("Northwind Holdings", "northwind", "eu-west", "growth"),
)
db.commit()
db.close()
def current_identity() -> dict:
return {
"org_id": int(request.headers.get("X-Org-Id", "1")),
"user_id": request.headers.get("X-User-Id", "docs-user"),
"role": request.headers.get("X-Role", "viewer"),
}
@app.before_request
def load_identity() -> None:
g.identity = current_identity()
g.started_at = datetime.utcnow().isoformat()
@app.after_request
def audit_request(response: Response) -> Response:
identity = getattr(g, "identity", None)
if identity:
db = get_db()
db.execute(
"insert into audit_records (org_id, actor, action, resource, details) values (?, ?, ?, ?, ?)",
(
identity["org_id"],
identity["user_id"],
f"{request.method} {request.path}",
request.full_path,
json.dumps({"status": response.status_code, "args": request.args.to_dict(), "json": request.get_json(silent=True)}),
),
)
db.commit()
return response
@app.teardown_appcontext
def close_db(exc: Exception | None) -> None:
db = g.pop("db", None)
if db is not None:
db.close()
def require_roles(*allowed: str):
def decorator(fn):
def wrapped(*args, **kwargs):
if g.identity["role"] not in allowed:
return jsonify({"error": "forbidden"}), 403
return fn(*args, **kwargs)
wrapped.__name__ = fn.__name__
return wrapped
return decorator
def render_report(org_name: str, title: str, notes: str) -> str:
return f"""
<html>
<body>
<h1>{title}</h1>
<h2>{org_name}</h2>
<div>{notes}</div>
</body>
</html>
"""
def resolve_storage_path(filename: str, *, must_exist: bool = False) -> Path:
if not isinstance(filename, str) or not filename:
raise ValueError("invalid filename")
if filename in {".", ".."}:
raise ValueError("invalid filename")
if "\x00" in filename:
raise ValueError("invalid filename")
candidate_name = Path(filename).name
if candidate_name != filename or Path(filename).is_absolute():
raise ValueError("invalid filename")
base_dir = STORAGE_DIR.resolve()
target = base_dir / filename
if must_exist:
resolved = target.resolve(strict=True)
if resolved.parent != base_dir:
raise ValueError("invalid filename")
return resolved
if target.exists():
resolved = target.resolve(strict=True)
if resolved.parent != base_dir:
raise ValueError("invalid filename")
return target
def enqueue_job(payload: dict) -> None:
job_queue.put(payload)
def job_worker() -> None:
while True:
payload = job_queue.get()
if payload is None:
break
db = sqlite3.connect(DB_PATH)
db.execute(
"insert into audit_records (org_id, actor, action, resource, details) values (?, ?, ?, ?, ?)",
(
payload.get("org_id"),
"system",
"job.dispatch",
payload.get("kind", "document"),
json.dumps(payload),
),
)
if payload.get("kind") == "document":
filename = payload["filename"]
target = STORAGE_DIR / filename
target.write_text(payload.get("content", ""), encoding="utf-8")
db.execute(
"insert into documents (org_id, filename, original_name, mime_type, kind) values (?, ?, ?, ?, ?)",
(
payload.get("org_id"),
filename,
payload.get("original_name", filename),
payload.get("mime_type", "text/plain"),
payload.get("kind", "document"),
),
)
db.commit()
db.close()
job_queue.task_done()
init_db()
threading.Thread(target=job_worker, daemon=True).start()
@app.route("/health", methods=["GET"])
def health() -> tuple:
db = get_db()
counts = {
"documents": db.execute("select count(*) from documents").fetchone()[0],
"audit_records": db.execute("select count(*) from audit_records").fetchone()[0],
"jobs_pending": job_queue.qsize(),
}
return jsonify({"ok": True, "counts": counts})
@app.route("/documents", methods=["GET"])
@require_roles("admin", "manager", "viewer")
def search_documents() -> tuple:
clauses = ["org_id = ?"]
params: list[object] = [g.identity["org_id"]]
kind = request.args.get("kind", "").strip()
name = request.args.get("name", "").strip()
if kind:
clauses.append("kind = ?")
params.append(kind)
if name:
clauses.append("original_name like ?")
params.append(f"%{name}%")
rows = get_db().execute(
f"select id, filename, original_name, mime_type, kind, created_at from documents where {' and '.join(clauses)} order by id desc",
params,
).fetchall()
return jsonify({"data": [dict(row) for row in rows]})
@app.route("/docs/render", methods=["POST"])
@require_roles("admin", "manager")
def render_document() -> tuple:
payload = request.get_json(force=True)
org_name = payload["orgName"]
title = payload.get("title", "Treasury Notice")
notes = payload.get("notes", "")
filename = payload.get("filename", f"report-{int(datetime.utcnow().timestamp())}.html")
html = render_report(org_name, title, notes)
try:
target = resolve_storage_path(filename)
except ValueError:
return jsonify({"error": "invalid filename"}), 400
target.write_text(html, encoding="utf-8")
db = get_db()
db.execute(
"insert into documents (org_id, filename, original_name, mime_type, kind) values (?, ?, ?, ?, ?)",
(g.identity["org_id"], filename, filename, "text/html", "report"),
)
db.commit()
return jsonify({"filename": filename, "path": str(target)}), 201
@app.route("/docs/upload", methods=["POST"])
@require_roles("admin", "manager")
def upload_document() -> tuple:
upload = request.files.get("file")
if upload is None:
return jsonify({"error": "missing file"}), 400
filename = f"{int(datetime.utcnow().timestamp())}-{upload.filename}"
try:
target = resolve_storage_path(filename)
except ValueError:
return jsonify({"error": "invalid filename"}), 400
upload.save(target)
db = get_db()
db.execute(
"insert into documents (org_id, filename, original_name, mime_type, kind) values (?, ?, ?, ?, ?)",
(g.identity["org_id"], filename, upload.filename, upload.mimetype or "application/octet-stream", "upload"),
)
db.commit()
return jsonify({"filename": filename}), 201
@app.route("/docs/download/<path:filename>", methods=["GET"])
@require_roles("admin", "manager", "viewer")
def download_document(filename: str):
try:
target = resolve_storage_path(filename, must_exist=True)
except FileNotFoundError:
return jsonify({"error": "not found"}), 404
except ValueError:
return jsonify({"error": "invalid filename"}), 400
return send_file(target, as_attachment=True)
@app.route("/jobs/dispatch", methods=["POST"])
@require_roles("admin", "manager")
def dispatch_job() -> tuple:
payload = request.get_json(force=True)
payload["org_id"] = g.identity["org_id"]
enqueue_job(payload)
return jsonify({"queued": True, "kind": payload.get("kind", "document")}), 202
@app.route("/reports/summary", methods=["GET"])
@require_roles("admin", "manager")
def summary_report() -> tuple:
db = get_db()
org_id = int(request.args.get("orgId", g.identity["org_id"]))
title = request.args.get("title", "Document Summary")
org = db.execute("select name, region, tier from organizations where id = ?", (org_id,)).fetchone()
if org is None:
return jsonify({"error": "missing org"}), 404
notes = request.args.get("notes", "No notes supplied")
html = render_report(org["name"], title, notes)
return Response(html, mimetype="text/html")
if __name__ == "__main__":
app.run(host="127.0.0.1", port=int(os.environ.get("PORT", "4300")), debug=False)
docs-worker/requirements.txt
Flask==3.1.3
docs-worker/test_app.py
import os
import shutil
import tempfile
import importlib
import runpy
import unittest
from pathlib import Path
from urllib.parse import quote
from unittest.mock import patch
TEST_DATA_DIR = tempfile.mkdtemp(prefix="docs-worker-test-")
os.environ["DOCS_WORKER_DATA_DIR"] = TEST_DATA_DIR
docs_app = importlib.import_module("app")
class DownloadDocumentTests(unittest.TestCase):
@classmethod
def setUpClass(cls):
docs_app.app.config["TESTING"] = True
cls.client = docs_app.app.test_client()
cls.storage_dir = Path(TEST_DATA_DIR) / "storage"
@classmethod
def tearDownClass(cls):
shutil.rmtree(TEST_DATA_DIR, ignore_errors=True)
def test_download_rejects_traversal_and_symlink_escape(self):
outside = Path(TEST_DATA_DIR) / "outside.txt"
outside.write_text("secret", encoding="utf-8")
self.storage_dir.mkdir(parents=True, exist_ok=True)
(self.storage_dir / "escape.txt").symlink_to(outside)
traversal = self.client.get(
f"/docs/download/{quote('../outside.txt', safe='')}",
headers={"X-Role": "viewer", "X-Org-Id": "1"},
)
self.assertEqual(traversal.status_code, 400)
self.assertEqual(traversal.get_json(), {"error": "invalid filename"})
symlink = self.client.get(
"/docs/download/escape.txt",
headers={"X-Role": "viewer", "X-Org-Id": "1"},
)
self.assertEqual(symlink.status_code, 400)
self.assertEqual(symlink.get_json(), {"error": "invalid filename"})
def test_download_returns_existing_file(self):
self.storage_dir.mkdir(parents=True, exist_ok=True)
target = self.storage_dir / "statement.txt"
target.write_text("document-body", encoding="utf-8")
response = self.client.get(
"/docs/download/statement.txt",
headers={"X-Role": "viewer", "X-Org-Id": "1"},
)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data, b"document-body")
def test_main_binds_to_loopback(self):
with patch("flask.Flask.run") as run_mock:
runpy.run_module("app", run_name="__main__")
self.assertTrue(run_mock.called)
_, kwargs = run_mock.call_args
self.assertEqual(kwargs["host"], "127.0.0.1")
self.assertEqual(kwargs["debug"], False)
if __name__ == "__main__":
unittest.main()
payments-api/go.mod
module payments-api
go 1.22
require modernc.org/sqlite v1.36.0
require (
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
golang.org/x/exp v0.0.0-20230315142452-642cacee5cc0 // indirect
golang.org/x/sys v0.30.0 // indirect
modernc.org/libc v1.61.13 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.8.2 // indirect
)
payments-api/go.sum
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo=
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
golang.org/x/exp v0.0.0-20230315142452-642cacee5cc0 h1:pVgRXcIictcr+lBQIFeiwuwtDIs4eL21OuM9nyAADmo=
golang.org/x/exp v0.0.0-20230315142452-642cacee5cc0/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8=
golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg=
golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI=
modernc.org/cc/v4 v4.24.4 h1:TFkx1s6dCkQpd6dKurBNmpo+G8Zl4Sq/ztJ+2+DEsh0=
modernc.org/cc/v4 v4.24.4/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
modernc.org/ccgo/v4 v4.23.16 h1:Z2N+kk38b7SfySC1ZkpGLN2vthNJP1+ZzGZIlH7uBxo=
modernc.org/ccgo/v4 v4.23.16/go.mod h1:nNma8goMTY7aQZQNTyN9AIoJfxav4nvTnvKThAeMDdo=
modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
modernc.org/gc/v2 v2.6.3 h1:aJVhcqAte49LF+mGveZ5KPlsp4tdGdAOT4sipJXADjw=
modernc.org/gc/v2 v2.6.3/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/libc v1.61.13 h1:3LRd6ZO1ezsFiX1y+bHd1ipyEHIJKvuprv0sLTBwLW8=
modernc.org/libc v1.61.13/go.mod h1:8F/uJWL/3nNil0Lgt1Dpz+GgkApWh04N3el3hxJcA6E=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.8.2 h1:cL9L4bcoAObu4NkxOlKWBWtNHIsnnACGF/TbqQ6sbcI=
modernc.org/memory v1.8.2/go.mod h1:ZbjSvMO5NQ1A2i3bWeDiVMxIorXwdClKE/0SZ+BMotU=
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
modernc.org/sqlite v1.36.0 h1:EQXNRn4nIS+gfsKeUTymHIz1waxuv5BzU7558dHSfH8=
modernc.org/sqlite v1.36.0/go.mod h1:7MPwH7Z6bREicF9ZVUR78P1IKuxfZ8mRIDHD0iD+8TU=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
payments-api/main.go
package main
import (
"context"
"database/sql"
"encoding/json"
"fmt"
"log"
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
"sync"
_ "modernc.org/sqlite"
)
type identity struct {
OrgID int64
UserID string
Role string
}
type ctxKey string
const identityKey ctxKey = "identity"
type job struct {
TransferID int64
OrgID int64
}
type responseRecorder struct {
http.ResponseWriter
status int
}
func (r *responseRecorder) WriteHeader(code int) {
r.status = code
r.ResponseWriter.WriteHeader(code)
}
var (
db *sql.DB
jobs = make(chan job, 64)
startOnce sync.Once
dataDir = filepath.Join("data")
dbPath = filepath.Join(dataDir, "payments.db")
)
func main() {
mustInit()
startWorker()
http.HandleFunc("/healthz", withMiddleware(healthHandler))
http.HandleFunc("/accounts/search", withMiddleware(requireRoles("admin", "manager", "viewer")(accountsSearchHandler)))
http.HandleFunc("/beneficiaries", withMiddleware(requireRoles("admin", "manager", "viewer")(beneficiariesHandler)))
http.HandleFunc("/transfers", withMiddleware(requireRoles("admin", "manager")(transfersHandler)))
http.HandleFunc("/jobs/settlements", withMiddleware(requireRoles("admin", "manager")(settlementHandler)))
http.HandleFunc("/audit", withMiddleware(requireRoles("admin")(auditHandler)))
port := getenv("PORT", "4200")
log.Printf("payments-api listening on %s", port)
log.Fatal(http.ListenAndServe(":"+port, nil))
}
func mustInit() {
if err := os.MkdirAll(dataDir, 0o755); err != nil {
log.Fatal(err)
}
var err error
db, err = sql.Open("sqlite", "file:"+dbPath+"?_pragma=busy_timeout(5000)")
if err != nil {
log.Fatal(err)
}
db.SetMaxOpenConns(1)
for _, stmt := range []string{
`create table if not exists organizations (
id integer primary key autoincrement,
name text not null,
org_code text not null unique,
region text not null,
tier text not null
);`,
`create table if not exists teams (
id integer primary key autoincrement,
org_id integer not null,
name text not null,
cost_center text not null,
status text not null
);`,
`create table if not exists users (
id integer primary key autoincrement,
org_id integer not null,
team_id integer,
email text not null,
display_name text not null,
role text not null
);`,
`create table if not exists accounts (
id integer primary key autoincrement,
org_id integer not null,
account_no text not null,
currency text not null,
balance numeric not null default 0
);`,
`create table if not exists beneficiaries (
id integer primary key autoincrement,
org_id integer not null,
name text not null,
bank_name text not null,
routing text not null
);`,
`create table if not exists transfers (
id integer primary key autoincrement,
org_id integer not null,
account_id integer not null,
beneficiary_id integer not null,
amount numeric not null,
currency text not null,
status text not null,
memo text not null,
requested_by text not null,
created_at text not null default current_timestamp
);`,
`create table if not exists audit_records (
id integer primary key autoincrement,
org_id integer,
actor text not null,
action text not null,
resource text not null,
details text not null,
created_at text not null default current_timestamp
);`,
} {
if _, err := db.Exec(stmt); err != nil {
log.Fatal(err)
}
}
seedData()
}
func seedData() {
var count int
_ = db.QueryRow("select count(*) from organizations").Scan(&count)
if count > 0 {
return
}
orgs := []struct {
name, code, region, tier string
}{
{"Acme Treasury", "acme", "us-east", "enterprise"},
{"Northwind Holdings", "northwind", "eu-west", "growth"},
}
for _, org := range orgs {
res, _ := db.Exec("insert into organizations (name, org_code, region, tier) values (?, ?, ?, ?)", org.name, org.code, org.region, org.tier)
orgID, _ := res.LastInsertId()
_, _ = db.Exec("insert into teams (org_id, name, cost_center, status) values (?, ?, ?, ?)", orgID, "Operations", "100-ops", "active")
_, _ = db.Exec("insert into teams (org_id, name, cost_center, status) values (?, ?, ?, ?)", orgID, "Payments", "200-pay", "active")
_, _ = db.Exec("insert into accounts (org_id, account_no, currency, balance) values (?, ?, ?, ?)", orgID, "ACCT-"+strconv.FormatInt(orgID, 10), "USD", 1000000)
_, _ = db.Exec("insert into beneficiaries (org_id, name, bank_name, routing) values (?, ?, ?, ?)", orgID, "Primary Vendor", "Metro Trust", "110000")
}
}
func startWorker() {
startOnce.Do(func() {
go func() {
for j := range jobs {
_, _ = db.Exec("update transfers set status = ? where id = ? and org_id = ?", "settled", j.TransferID, j.OrgID)
audit(j.OrgID, "system", "settlement", fmt.Sprintf("transfer:%d", j.TransferID), map[string]any{"status": "settled"})
}
}()
})
}
func withMiddleware(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
rec := &responseRecorder{ResponseWriter: w, status: http.StatusOK}
id := identityFromRequest(r)
r = r.WithContext(context.WithValue(r.Context(), identityKey, id))
next(rec, r)
audit(id.OrgID, id.UserID, r.Method+" "+r.URL.Path, r.URL.String(), map[string]any{
"status": rec.status,
"query": r.URL.Query(),
})
}
}
func identityFromRequest(r *http.Request) identity {
orgID, _ := strconv.ParseInt(r.Header.Get("X-Org-Id"), 10, 64)
if orgID == 0 {
orgID = 1
}
role := r.Header.Get("X-Role")
if role == "" {
role = "viewer"
}
userID := r.Header.Get("X-User-Id")
if userID == "" {
userID = "payments-user"
}
return identity{OrgID: orgID, UserID: userID, Role: role}
}
func requireRoles(allowed ...string) func(http.HandlerFunc) http.HandlerFunc {
return func(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
id := mustIdentity(r)
for _, role := range allowed {
if id.Role == role {
next(w, r)
return
}
}
http.Error(w, "forbidden", http.StatusForbidden)
}
}
}
func mustIdentity(r *http.Request) identity {
if v := r.Context().Value(identityKey); v != nil {
if id, ok := v.(identity); ok {
return id
}
}
return identityFromRequest(r)
}
func healthHandler(w http.ResponseWriter, r *http.Request) {
var orgCount, transferCount int
_ = db.QueryRow("select count(*) from organizations").Scan(&orgCount)
_ = db.QueryRow("select count(*) from transfers").Scan(&transferCount)
_ = json.NewEncoder(w).Encode(map[string]any{
"ok": true,
"organizations": orgCount,
"transfers": transferCount,
})
}
func accountsSearchHandler(w http.ResponseWriter, r *http.Request) {
id := mustIdentity(r)
clauses := []string{"org_id = ?"}
args := []any{id.OrgID}
if currency := strings.TrimSpace(r.URL.Query().Get("currency")); currency != "" {
clauses = append(clauses, "currency = ?")
args = append(args, currency)
}
if accountNo := strings.TrimSpace(r.URL.Query().Get("accountNo")); accountNo != "" {
clauses = append(clauses, "account_no like ?")
args = append(args, "%"+accountNo+"%")
}
query := "select id, account_no, currency, balance from accounts where " + strings.Join(clauses, " and ") + " order by id desc"
rows, err := db.Query(query, args...)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer rows.Close()
var data []map[string]any
for rows.Next() {
var accountID int64
var accountNo, currency string
var balance float64
_ = rows.Scan(&accountID, &accountNo, ¤cy, &balance)
data = append(data, map[string]any{
"id": accountID,
"accountNo": accountNo,
"currency": currency,
"balance": balance,
})
}
_ = json.NewEncoder(w).Encode(map[string]any{"data": data})
}
func beneficiariesHandler(w http.ResponseWriter, r *http.Request) {
id := mustIdentity(r)
switch r.Method {
case http.MethodPost:
var body struct {
Name string `json:"name"`
BankName string `json:"bankName"`
Routing string `json:"routing"`
}
_ = json.NewDecoder(r.Body).Decode(&body)
res, err := db.Exec("insert into beneficiaries (org_id, name, bank_name, routing) values (?, ?, ?, ?)", id.OrgID, body.Name, body.BankName, body.Routing)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
insertedID, _ := res.LastInsertId()
_ = json.NewEncoder(w).Encode(map[string]any{"id": insertedID, "name": body.Name})
default:
nameQuery := strings.TrimSpace(r.URL.Query().Get("name"))
query := "select id, name, bank_name, routing from beneficiaries where org_id = ?"
args := []any{id.OrgID}
if nameQuery != "" {
query += " and name like ?"
args = append(args, "%"+nameQuery+"%")
}
rows, err := db.Query(query+" order by id desc", args...)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer rows.Close()
var data []map[string]any
for rows.Next() {
var beneficiaryID int64
var name, bankName, routing string
_ = rows.Scan(&beneficiaryID, &name, &bankName, &routing)
data = append(data, map[string]any{
"id": beneficiaryID,
"name": name,
"bankName": bankName,
"routing": routing,
})
}
_ = json.NewEncoder(w).Encode(map[string]any{"data": data})
}
}
func transfersHandler(w http.ResponseWriter, r *http.Request) {
id := mustIdentity(r)
switch r.Method {
case http.MethodPost:
var body struct {
AccountID int64 `json:"accountId"`
BeneficiaryID int64 `json:"beneficiaryId"`
Amount float64 `json:"amount"`
Currency string `json:"currency"`
Memo string `json:"memo"`
}
_ = json.NewDecoder(r.Body).Decode(&body)
res, err := db.Exec(
"insert into transfers (org_id, account_id, beneficiary_id, amount, currency, status, memo, requested_by) values (?, ?, ?, ?, ?, ?, ?, ?)",
id.OrgID, body.AccountID, body.BeneficiaryID, body.Amount, body.Currency, "queued", body.Memo, id.UserID,
)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
transferID, _ := res.LastInsertId()
jobs <- job{TransferID: transferID, OrgID: id.OrgID}
_ = json.NewEncoder(w).Encode(map[string]any{"id": transferID, "status": "queued"})
default:
rows, err := db.Query("select id, account_id, beneficiary_id, amount, currency, status, memo from transfers where org_id = ? order by id desc limit 50", id.OrgID)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer rows.Close()
var data []map[string]any
for rows.Next() {
var transferID, accountID, beneficiaryID int64
var amount float64
var currency, status, memo string
_ = rows.Scan(&transferID, &accountID, &beneficiaryID, &amount, ¤cy, &status, &memo)
data = append(data, map[string]any{
"id": transferID,
"accountId": accountID,
"beneficiaryId": beneficiaryID,
"amount": amount,
"currency": currency,
"status": status,
"memo": memo,
})
}
_ = json.NewEncoder(w).Encode(map[string]any{"data": data})
}
}
func settlementHandler(w http.ResponseWriter, r *http.Request) {
id := mustIdentity(r)
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
var body struct {
TransferID int64 `json:"transferId"`
}
_ = json.NewDecoder(r.Body).Decode(&body)
jobs <- job{TransferID: body.TransferID, OrgID: id.OrgID}
_ = json.NewEncoder(w).Encode(map[string]any{"queued": true, "transferId": body.TransferID})
}
func auditHandler(w http.ResponseWriter, r *http.Request) {
id := mustIdentity(r)
rows, err := db.Query("select id, actor, action, resource, details, created_at from audit_records where org_id = ? order by id desc limit 100", id.OrgID)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer rows.Close()
var data []map[string]any
for rows.Next() {
var auditID int64
var actor, action, resource, details, createdAt string
_ = rows.Scan(&auditID, &actor, &action, &resource, &details, &createdAt)
data = append(data, map[string]any{
"id": auditID, "actor": actor, "action": action, "resource": resource, "details": details, "createdAt": createdAt,
})
}
_ = json.NewEncoder(w).Encode(map[string]any{"data": data})
}
func audit(orgID int64, actor, action, resource string, details map[string]any) {
_, _ = db.Exec(
"insert into audit_records (org_id, actor, action, resource, details) values (?, ?, ?, ?, ?)",
orgID, actor, action, resource, mustJSON(details),
)
}
func mustJSON(v any) string {
b, _ := json.Marshal(v)
return string(b)
}
func getenv(key, fallback string) string {
if value := os.Getenv(key); value != "" {
return value
}
return fallback
}
portal/package-lock.json
{
"name": "treasury-portal",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "treasury-portal",
"version": "1.0.0",
"dependencies": {
"better-sqlite3": "^11.10.0",
"express": "^4.21.2",
"helmet": "8.3.0",
"multer": "^1.4.5-lts.1"
},
"devDependencies": {
"@types/express": "^5.0.3",
"@types/multer": "^1.4.12",
"@types/node": "^22.15.3",
"ts-node": "^10.9.2",
"typescript": "^5.8.3"
}
},
"node_modules/@cspotcode/source-map-support": {
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
"integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/trace-mapping": "0.3.9"
},
"engines": {
"node": ">=12"
}
},
"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.9",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
"integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/resolve-uri": "^3.0.3",
"@jridgewell/sourcemap-codec": "^1.4.10"
}
},
"node_modules/@tsconfig/node10": {
"version": "1.0.13",
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.13.tgz",
"integrity": "sha512-gcLdvR9HO1ZJBypsOGqaP6TFEzb6vIta0KSTLt9NAQ6pXQO3cRgSVyCN6pzYqI9DlJgY71XKO0dpDhCf08b3pg==",
"dev": true,
"license": "MIT"
},
"node_modules/@tsconfig/node12": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz",
"integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==",
"dev": true,
"license": "MIT"
},
"node_modules/@tsconfig/node14": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz",
"integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==",
"dev": true,
"license": "MIT"
},
"node_modules/@tsconfig/node16": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz",
"integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/body-parser": {
"version": "1.19.6",
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz",
"integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/connect": "*",
"@types/node": "*"
}
},
"node_modules/@types/connect": {
"version": "3.4.38",
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
"integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/express": {
"version": "5.0.6",
"resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz",
"integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/body-parser": "*",
"@types/express-serve-static-core": "^5.0.0",
"@types/serve-static": "^2"
}
},
"node_modules/@types/express-serve-static-core": {
"version": "5.1.3",
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.3.tgz",
"integrity": "sha512-dPfW8NFiOF4wOHc7+N/QSxlY9cfSsenewGbAz8C8U/MULPd/YZ27LvJUIlzaXie7e6Ove9YunJGgC9tbHD2cKw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*",
"@types/qs": "*",
"@types/range-parser": "*",
"@types/send": "*"
}
},
"node_modules/@types/http-errors": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz",
"integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/multer": {
"version": "1.4.13",
"resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.13.tgz",
"integrity": "sha512-bhhdtPw7JqCiEfC9Jimx5LqX9BDIPJEh2q/fQ4bqbBPtyEZYr3cvF22NwG0DmPZNYA0CAf2CnqDB4KIGGpJcaw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/express": "*"
}
},
"node_modules/@types/node": {
"version": "22.20.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.2.tgz",
"integrity": "sha512-xlvWf4Vs9n1PEVYwP1n4vvG07M6y8WgvJ2t0vbrWTmijsIHp1cS+uJ2kMIRdY3nHZK0nCYKrPeD171+SzF4/zw==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
}
},
"node_modules/@types/qs": {
"version": "6.15.1",
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.1.tgz",
"integrity": "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/range-parser": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz",
"integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/send": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz",
"integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/serve-static": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz",
"integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/http-errors": "*",
"@types/node": "*"
}
},
"node_modules/accepts": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
"integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
"license": "MIT",
"dependencies": {
"mime-types": "~2.1.34",
"negotiator": "0.6.3"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/acorn": {
"version": "8.18.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz",
"integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==",
"dev": true,
"license": "MIT",
"bin": {
"acorn": "bin/acorn"
},
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/acorn-walk": {
"version": "8.3.5",
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz",
"integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==",
"dev": true,
"license": "MIT",
"dependencies": {
"acorn": "^8.11.0"
},
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/append-field": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz",
"integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==",
"license": "MIT"
},
"node_modules/arg": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
"integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
"dev": true,
"license": "MIT"
},
"node_modules/array-flatten": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
"license": "MIT"
},
"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/better-sqlite3": {
"version": "11.10.0",
"resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.10.0.tgz",
"integrity": "sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"bindings": "^1.5.0",
"prebuild-install": "^7.1.1"
}
},
"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/bl/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/body-parser": {
"version": "1.20.8",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.8.tgz",
"integrity": "sha512-JNcyFQ64OiijEkPzUBTCe+hyPXUD/3LEldGQ6iF5LR1w00mx9o7xtDWHXBY2iItjdCFGoilOLNQbH943ut7pHA==",
"license": "MIT",
"dependencies": {
"bytes": "~3.1.2",
"content-type": "~1.0.5",
"debug": "2.6.9",
"depd": "2.0.0",
"destroy": "~1.2.0",
"http-errors": "~2.0.1",
"iconv-lite": "~0.4.24",
"on-finished": "~2.4.1",
"qs": "~6.16.0",
"raw-body": "~2.5.3",
"type-is": "~1.6.18",
"unpipe": "~1.0.0"
},
"engines": {
"node": ">= 0.8",
"npm": "1.2.8000 || >= 1.4.16"
}
},
"node_modules/body-parser/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/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-from": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
"license": "MIT"
},
"node_modules/busboy": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
"integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==",
"dependencies": {
"streamsearch": "^1.1.0"
},
"engines": {
"node": ">=10.16.0"
}
},
"node_modules/bytes": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
"integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"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/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/concat-stream": {
"version": "1.6.2",
"resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz",
"integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==",
"engines": [
"node >= 0.8"
],
"license": "MIT",
"dependencies": {
"buffer-from": "^1.0.0",
"inherits": "^2.0.3",
"readable-stream": "^2.2.2",
"typedarray": "^0.0.6"
}
},
"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/content-type": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
"integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/cookie": {
"version": "0.7.2",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/cookie-signature": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
"integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==",
"license": "MIT"
},
"node_modules/core-util-is": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
"license": "MIT"
},
"node_modules/create-require": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
"dev": true,
"license": "MIT"
},
"node_modules/debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"license": "MIT",
"dependencies": {
"ms": "2.0.0"
}
},
"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/destroy": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
"integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
"license": "MIT",
"engines": {
"node": ">= 0.8",
"npm": "1.2.8000 || >= 1.4.16"
}
},
"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/diff": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz",
"integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.3.1"
}
},
"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/ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
"license": "MIT"
},
"node_modules/encodeurl": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"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/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/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/etag": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
"integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"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/express": {
"version": "4.22.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz",
"integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==",
"license": "MIT",
"dependencies": {
"accepts": "~1.3.8",
"array-flatten": "1.1.1",
"body-parser": "~1.20.5",
"content-disposition": "~0.5.4",
"content-type": "~1.0.4",
"cookie": "~0.7.1",
"cookie-signature": "~1.0.6",
"debug": "2.6.9",
"depd": "2.0.0",
"encodeurl": "~2.0.0",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"finalhandler": "~1.3.1",
"fresh": "~0.5.2",
"http-errors": "~2.0.0",
"merge-descriptors": "1.0.3",
"methods": "~1.1.2",
"on-finished": "~2.4.1",
"parseurl": "~1.3.3",
"path-to-regexp": "~0.1.12",
"proxy-addr": "~2.0.7",
"qs": "~6.15.1",
"range-parser": "~1.2.1",
"safe-buffer": "5.2.1",
"send": "~0.19.0",
"serve-static": "~1.16.2",
"setprototypeof": "1.2.0",
"statuses": "~2.0.1",
"type-is": "~1.6.18",
"utils-merge": "1.0.1",
"vary": "~1.1.2"
},
"engines": {
"node": ">= 0.10.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"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/finalhandler": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz",
"integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==",
"license": "MIT",
"dependencies": {
"debug": "2.6.9",
"encodeurl": "~2.0.0",
"escape-html": "~1.0.3",
"on-finished": "~2.4.1",
"parseurl": "~1.3.3",
"statuses": "~2.0.2",
"unpipe": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/forwarded": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
"integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/fresh": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
"integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"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/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/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/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/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/helmet": {
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/helmet/-/helmet-8.3.0.tgz",
"integrity": "sha512-Qgpiaws3Sm30Av8Eah6sjMCZZwjlBu+E68rhpCWBshY1lb09HtLwj5GviX0OyQIn+ulUS0iX0AxN5n3tLZzz1w==",
"license": "MIT",
"engines": {
"node": ">=18.0.0"
},
"funding": {
"url": "https://github.com/sponsors/EvanHahn"
}
},
"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/iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
"license": "MIT",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3"
},
"engines": {
"node": ">=0.10.0"
}
},
"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": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
"integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
"license": "MIT",
"engines": {
"node": ">= 0.10"
}
},
"node_modules/isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
"license": "MIT"
},
"node_modules/make-error": {
"version": "1.3.6",
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
"dev": true,
"license": "ISC"
},
"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/media-typer": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
"integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/merge-descriptors": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
"integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/methods": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
"integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
"license": "MIT",
"bin": {
"mime": "cli.js"
},
"engines": {
"node": ">=4"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"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/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/mkdirp": {
"version": "0.5.6",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
"integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
"license": "MIT",
"dependencies": {
"minimist": "^1.2.6"
},
"bin": {
"mkdirp": "bin/cmd.js"
}
},
"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.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"license": "MIT"
},
"node_modules/multer": {
"version": "1.4.5-lts.2",
"resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.2.tgz",
"integrity": "sha512-VzGiVigcG9zUAoCNU+xShztrlr1auZOlurXynNvO9GiWD1/mTBbUljOKY+qMeazBqXgRnjzeEgJI/wyjJUHg9A==",
"deprecated": "Multer 1.x is impacted by a number of vulnerabilities, which have been patched in 2.x. You should upgrade to the latest 2.x version.",
"license": "MIT",
"dependencies": {
"append-field": "^1.0.0",
"busboy": "^1.0.0",
"concat-stream": "^1.5.2",
"mkdirp": "^0.5.4",
"object-assign": "^4.1.1",
"type-is": "^1.6.4",
"xtend": "^4.0.0"
},
"engines": {
"node": ">= 6.0.0"
}
},
"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/negotiator": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
"integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"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/object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"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-finished": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
"license": "MIT",
"dependencies": {
"ee-first": "1.1.1"
},
"engines": {
"node": ">= 0.8"
}
},
"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/parseurl": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/path-to-regexp": {
"version": "0.1.13",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz",
"integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==",
"license": "MIT"
},
"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-nextick-args": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
"license": "MIT"
},
"node_modules/proxy-addr": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
"integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
"license": "MIT",
"dependencies": {
"forwarded": "0.2.0",
"ipaddr.js": "1.9.1"
},
"engines": {
"node": ">= 0.10"
}
},
"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.15.3",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz",
"integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==",
"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/range-parser": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/raw-body": {
"version": "2.5.3",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz",
"integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==",
"license": "MIT",
"dependencies": {
"bytes": "~3.1.2",
"http-errors": "~2.0.1",
"iconv-lite": "~0.4.24",
"unpipe": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"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/readable-stream": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
"license": "MIT",
"dependencies": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
"isarray": "~1.0.0",
"process-nextick-args": "~2.0.0",
"safe-buffer": "~5.1.1",
"string_decoder": "~1.1.1",
"util-deprecate": "~1.0.1"
}
},
"node_modules/readable-stream/node_modules/safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"license": "MIT"
},
"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/safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"license": "MIT"
},
"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/send": {
"version": "0.19.2",
"resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz",
"integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==",
"license": "MIT",
"dependencies": {
"debug": "2.6.9",
"depd": "2.0.0",
"destroy": "1.2.0",
"encodeurl": "~2.0.0",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"fresh": "~0.5.2",
"http-errors": "~2.0.1",
"mime": "1.6.0",
"ms": "2.1.3",
"on-finished": "~2.4.1",
"range-parser": "~1.2.1",
"statuses": "~2.0.2"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/send/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/serve-static": {
"version": "1.16.3",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz",
"integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==",
"license": "MIT",
"dependencies": {
"encodeurl": "~2.0.0",
"escape-html": "~1.0.3",
"parseurl": "~1.3.3",
"send": "~0.19.1"
},
"engines": {
"node": ">= 0.8.0"
}
},
"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/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/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/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/streamsearch": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
"integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==",
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"license": "MIT",
"dependencies": {
"safe-buffer": "~5.1.0"
}
},
"node_modules/string_decoder/node_modules/safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"license": "MIT"
},
"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/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/tar-stream/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/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/ts-node": {
"version": "10.9.2",
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
"integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@cspotcode/source-map-support": "^0.8.0",
"@tsconfig/node10": "^1.0.7",
"@tsconfig/node12": "^1.0.7",
"@tsconfig/node14": "^1.0.0",
"@tsconfig/node16": "^1.0.2",
"acorn": "^8.4.1",
"acorn-walk": "^8.1.1",
"arg": "^4.1.0",
"create-require": "^1.1.0",
"diff": "^4.0.1",
"make-error": "^1.1.1",
"v8-compile-cache-lib": "^3.0.1",
"yn": "3.1.1"
},
"bin": {
"ts-node": "dist/bin.js",
"ts-node-cwd": "dist/bin-cwd.js",
"ts-node-esm": "dist/bin-esm.js",
"ts-node-script": "dist/bin-script.js",
"ts-node-transpile-only": "dist/bin-transpile.js",
"ts-script": "dist/bin-script-deprecated.js"
},
"peerDependencies": {
"@swc/core": ">=1.2.50",
"@swc/wasm": ">=1.2.50",
"@types/node": "*",
"typescript": ">=2.7"
},
"peerDependenciesMeta": {
"@swc/core": {
"optional": true
},
"@swc/wasm": {
"optional": true
}
}
},
"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/type-is": {
"version": "1.6.18",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
"integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
"license": "MIT",
"dependencies": {
"media-typer": "0.3.0",
"mime-types": "~2.1.24"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/typedarray": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
"integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==",
"license": "MIT"
},
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/undici-types": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"dev": true,
"license": "MIT"
},
"node_modules/unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
"integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"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/utils-merge": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
"integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
"license": "MIT",
"engines": {
"node": ">= 0.4.0"
}
},
"node_modules/v8-compile-cache-lib": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
"integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==",
"dev": true,
"license": "MIT"
},
"node_modules/vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
"integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"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/xtend": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
"license": "MIT",
"engines": {
"node": ">=0.4"
}
},
"node_modules/yn": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
"integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
}
}
}
}
portal/package.json
{
"name": "treasury-portal",
"private": true,
"version": "1.0.0",
"type": "commonjs",
"scripts": {
"dev": "ts-node src/server.ts",
"build": "tsc -p tsconfig.json",
"start": "node dist/server.js"
},
"dependencies": {
"better-sqlite3": "^11.10.0",
"helmet": "8.3.0",
"express": "^4.21.2",
"multer": "^1.4.5-lts.1"
},
"devDependencies": {
"@types/express": "^5.0.3",
"@types/multer": "^1.4.12",
"@types/node": "^22.15.3",
"ts-node": "^10.9.2",
"typescript": "^5.8.3"
}
}
portal/src/server.ts
import express, { NextFunction, Request, Response } from "express";
import fs from "fs";
import path from "path";
import multer from "multer";
import helmet from "helmet";
import { db, recordAudit, storagePath } from "./store.js";
type Identity = {
orgId: number;
userId: string;
role: "admin" | "manager" | "viewer";
};
declare global {
namespace Express {
interface Request {
identity?: Identity;
}
}
}
const app = express();
const upload = multer({ dest: path.resolve(__dirname, "..", "tmp") });
fs.mkdirSync(path.resolve(__dirname, "..", "tmp"), { recursive: true });
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(helmet());
app.use((req, res, next) => {
const orgId = Number(req.header("x-org-id") || "1");
const userId = req.header("x-user-id") || "portal-user";
const role = (req.header("x-role") || "viewer") as Identity["role"];
req.identity = { orgId, userId, role };
next();
});
app.use((req, res, next) => {
const finish = () => {
if (!req.identity) {
return;
}
recordAudit({
orgId: req.identity.orgId,
actor: req.identity.userId,
action: `${req.method} ${req.path}`,
resource: req.originalUrl,
details: {
statusCode: res.statusCode,
query: req.query,
body: req.body
}
});
};
res.on("finish", finish);
next();
});
function requireRole(allowed: Identity["role"][]) {
return (req: Request, res: Response, next: NextFunction) => {
if (!req.identity || !allowed.includes(req.identity.role)) {
res.status(403).json({ error: "forbidden" });
return;
}
next();
};
}
function buildOrgFilters(query: Request["query"]) {
const clauses: string[] = [];
const params: unknown[] = [];
if (typeof query.name === "string" && query.name.trim()) {
clauses.push("name like ?");
params.push(`%${query.name.trim()}%`);
}
if (typeof query.region === "string" && query.region.trim()) {
clauses.push("region = ?");
params.push(query.region.trim());
}
if (typeof query.tier === "string" && query.tier.trim()) {
clauses.push("tier = ?");
params.push(query.tier.trim());
}
const where = clauses.length ? ` where ${clauses.join(" and ")}` : "";
return { where, params };
}
app.get("/health", (_req, res) => {
const counts = {
organizations: db.prepare("select count(*) as count from organizations").get().count,
transfers: db.prepare("select count(*) as count from transfers").get().count,
auditRecords: db.prepare("select count(*) as count from audit_records").get().count
};
res.json({ ok: true, counts });
});
app.get("/orgs", requireRole(["admin", "manager", "viewer"]), (req, res) => {
const { where, params } = buildOrgFilters(req.query);
const sqlParts = ["select * from organizations"];
if (where) {
sqlParts.push(where);
}
sqlParts.push("order by id desc");
const rows = db.prepare(sqlParts.join(" ")).all(...params);
res.json({ data: rows });
});
app.post("/orgs", requireRole(["admin"]), (req, res) => {
const { name, orgCode, region, tier } = req.body;
const stmt = db.prepare(
"insert into organizations (name, org_code, region, tier) values (?, ?, ?, ?)"
);
const result = stmt.run(name, orgCode, region, tier);
res.status(201).json({ id: result.lastInsertRowid, name, orgCode, region, tier });
});
app.get("/teams", requireRole(["admin", "manager"]), (req, res) => {
const filters: string[] = [];
const params: unknown[] = [];
if (typeof req.query.orgId === "string" && req.query.orgId.trim()) {
filters.push("org_id = ?");
params.push(Number(req.query.orgId));
}
if (typeof req.query.query === "string" && req.query.query.trim()) {
filters.push("(name like ? or cost_center like ?)");
params.push(`%${req.query.query.trim()}%`, `%${req.query.query.trim()}%`);
}
const where = filters.length ? ` where ${filters.join(" and ")}` : "";
const sqlParts = ["select * from teams"];
if (where) {
sqlParts.push(where);
}
sqlParts.push("order by created_at desc");
const rows = db.prepare(sqlParts.join(" ")).all(...params);
res.json({ data: rows });
});
app.post("/users", requireRole(["admin", "manager"]), (req, res) => {
const { orgId, teamId, email, displayName, role, status } = req.body;
const result = db
.prepare(
"insert into users (org_id, team_id, email, display_name, role, status) values (?, ?, ?, ?, ?, ?)"
)
.run(orgId, teamId ?? null, email, displayName, role, status || "active");
res.status(201).json({ id: result.lastInsertRowid, orgId, teamId, email, displayName, role });
});
app.post("/beneficiaries", requireRole(["admin", "manager"]), (req, res) => {
const { orgId, name, bankName, routing } = req.body;
const result = db
.prepare("insert into beneficiaries (org_id, name, bank_name, routing) values (?, ?, ?, ?)")
.run(orgId, name, bankName, routing);
res.status(201).json({ id: result.lastInsertRowid });
});
app.post("/transfers", requireRole(["admin", "manager"]), (req, res) => {
const { orgId, accountId, beneficiaryId, amount, currency, memo } = req.body;
const result = db
.prepare(
"insert into transfers (org_id, account_id, beneficiary_id, amount, currency, status, memo, requested_by) values (?, ?, ?, ?, ?, ?, ?, ?)"
)
.run(orgId, accountId, beneficiaryId, amount, currency, "queued", memo || "", req.identity?.userId || "unknown");
res.status(201).json({ id: result.lastInsertRowid, status: "queued" });
});
app.post("/documents/upload", requireRole(["admin", "manager"]), upload.single("file"), (req, res) => {
const file = req.file;
if (!file) {
res.status(400).json({ error: "missing file" });
return;
}
const targetName = `${Date.now()}-${file.originalname}`;
let targetPath: string;
try {
targetPath = storagePath(targetName);
} catch (_error) {
res.status(400).json({ error: "invalid file name" });
return;
}
fs.copyFileSync(file.path, targetPath);
const result = db
.prepare(
"insert into documents (org_id, filename, original_name, mime_type) values (?, ?, ?, ?)"
)
.run(req.identity?.orgId || 1, targetName, file.originalname, file.mimetype);
res.status(201).json({ id: result.lastInsertRowid, filename: targetName });
});
app.get("/documents/:name", requireRole(["admin", "manager", "viewer"]), (req, res) => {
let filePath: string;
try {
filePath = storagePath(req.params.name);
} catch (_error) {
res.status(400).json({ error: "invalid file name" });
return;
}
if (!fs.existsSync(filePath)) {
res.status(404).json({ error: "not found" });
return;
}
res.setHeader("content-type", "application/octet-stream");
res.send(fs.readFileSync(filePath));
});
app.get("/reports/org-summary", requireRole(["admin", "manager"]), (req, res) => {
const orgId = Number(req.query.orgId || req.identity?.orgId || 1);
const org = db.prepare("select * from organizations where id = ?").get(orgId);
const transfers = db
.prepare("select * from transfers where org_id = ? order by created_at desc limit 25")
.all(orgId) as Array<{
currency: string;
amount: number;
status: string;
memo: string;
}>;
const title = typeof req.query.title === "string" ? req.query.title : "Treasury Summary";
const html = `
<html>
<body>
<h1>${title}</h1>
<h2>${org?.name ?? "Unknown org"}</h2>
<p>Region: ${org?.region ?? "n/a"} Tier: ${org?.tier ?? "n/a"}</p>
<ul>
${transfers
.map(
(transfer) =>
`<li>${transfer.currency} ${transfer.amount} - ${transfer.status} - ${transfer.memo}</li>`
)
.join("")}
</ul>
</body>
</html>
`;
res.type("html").send(html);
});
app.get("/audit", requireRole(["admin"]), (req, res) => {
const orgId = typeof req.query.orgId === "string" ? Number(req.query.orgId) : req.identity?.orgId;
const rows = db
.prepare("select * from audit_records where org_id = ? order by created_at desc limit 100")
.all(orgId);
res.json({ data: rows });
});
app.use((_req, res) => {
res.status(404).json({ error: "not found" });
});
const port = Number(process.env.PORT || 4100);
if (require.main === module) {
app.listen(port, () => {
console.log(`portal listening on ${port}`);
});
}
export { app };
portal/src/store.js
const fs = require("fs");
const path = require("path");
const Database = require("better-sqlite3");
const baseDir = path.resolve(__dirname, "..", "data");
const storageDir = path.join(baseDir, "documents");
const dbPath = path.join(baseDir, "portal.db");
fs.mkdirSync(storageDir, { recursive: true });
const db = new Database(dbPath);
db.pragma("journal_mode = WAL");
db.exec(`
create table if not exists organizations (
id integer primary key autoincrement,
name text not null,
org_code text not null unique,
region text not null,
tier text not null,
created_at text not null default (datetime('now'))
);
create table if not exists teams (
id integer primary key autoincrement,
org_id integer not null,
name text not null,
cost_center text not null,
status text not null default 'active',
created_at text not null default (datetime('now'))
);
create table if not exists users (
id integer primary key autoincrement,
org_id integer not null,
team_id integer,
email text not null,
display_name text not null,
role text not null,
status text not null default 'active',
created_at text not null default (datetime('now'))
);
create table if not exists accounts (
id integer primary key autoincrement,
org_id integer not null,
account_no text not null,
currency text not null,
balance numeric not null default 0,
created_at text not null default (datetime('now'))
);
create table if not exists beneficiaries (
id integer primary key autoincrement,
org_id integer not null,
name text not null,
bank_name text not null,
routing text not null,
created_at text not null default (datetime('now'))
);
create table if not exists transfers (
id integer primary key autoincrement,
org_id integer not null,
account_id integer not null,
beneficiary_id integer not null,
amount numeric not null,
currency text not null,
status text not null,
memo text not null,
requested_by text not null,
created_at text not null default (datetime('now'))
);
create table if not exists documents (
id integer primary key autoincrement,
org_id integer not null,
filename text not null,
original_name text not null,
mime_type text not null,
created_at text not null default (datetime('now'))
);
create table if not exists audit_records (
id integer primary key autoincrement,
org_id integer,
actor text not null,
action text not null,
resource text not null,
details text not null,
created_at text not null default (datetime('now'))
);
`);
function seed() {
const count = db.prepare("select count(*) as count from organizations").get().count;
if (count > 0) {
return;
}
const insertOrg = db.prepare("insert into organizations (name, org_code, region, tier) values (?, ?, ?, ?)");
const insertTeam = db.prepare("insert into teams (org_id, name, cost_center, status) values (?, ?, ?, ?)");
const insertUser = db.prepare("insert into users (org_id, team_id, email, display_name, role, status) values (?, ?, ?, ?, ?, ?)");
const insertAccount = db.prepare("insert into accounts (org_id, account_no, currency, balance) values (?, ?, ?, ?)");
const insertBeneficiary = db.prepare("insert into beneficiaries (org_id, name, bank_name, routing) values (?, ?, ?, ?)");
const acme = insertOrg.run("Acme Treasury", "acme", "us-east", "enterprise").lastInsertRowid;
const north = insertOrg.run("Northwind Holdings", "northwind", "eu-west", "growth").lastInsertRowid;
const ops = insertTeam.run(acme, "Operations", "100-ops", "active").lastInsertRowid;
const rev = insertTeam.run(acme, "Revenue", "200-rev", "active").lastInsertRowid;
const comp = insertTeam.run(north, "Compliance", "300-comp", "active").lastInsertRowid;
insertUser.run(acme, ops, "treasury.lead@acme.test", "Dana Ledger", "admin", "active");
insertUser.run(acme, rev, "payments.ops@acme.test", "Mika Flow", "manager", "active");
insertUser.run(north, comp, "risk.officer@northwind.test", "Riley Guard", "viewer", "active");
insertAccount.run(acme, "ACCT-10001", "USD", 1250000);
insertAccount.run(acme, "ACCT-10002", "EUR", 860000);
insertAccount.run(north, "ACCT-20001", "USD", 410000);
insertBeneficiary.run(acme, "Vendor One", "Harbor Bank", "110000");
insertBeneficiary.run(acme, "Payroll Service", "Metro Trust", "220000");
insertBeneficiary.run(north, "Regional Supplier", "Union Bank", "330000");
}
seed();
function recordAudit({ orgId = null, actor, action, resource, details }) {
db.prepare(
"insert into audit_records (org_id, actor, action, resource, details) values (?, ?, ?, ?, ?)"
).run(orgId, actor, action, resource, JSON.stringify(details));
}
function storagePath(name) {
return path.join(storageDir, name);
}
module.exports = {
db,
storageDir,
storagePath,
recordAudit
};
portal/tsconfig.json
{
"compilerOptions": {
"target": "ES2020",
"module": "CommonJS",
"moduleResolution": "Node",
"outDir": "dist",
"rootDir": "src",
"strict": true,
"esModuleInterop": true,
"allowJs": true,
"skipLibCheck": true,
"resolveJsonModule": true
},
"include": ["src/**/*"]
}
risk-api/pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.treasury</groupId>
<artifactId>risk-api</artifactId>
<version>1.0.0</version>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>2.3.232</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.18.3</version>
</dependency>
</dependencies>
</project>
risk-api/src/main/java/com/treasury/risk/RiskApi.java
package com.treasury.risk;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.sun.net.httpserver.Headers;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpServer;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.URI;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
public class RiskApi {
private static final ObjectMapper MAPPER = new ObjectMapper();
private static final Path DATA_DIR = Path.of("data");
private static final Path DB_PATH = DATA_DIR.resolve("risk.mv.db");
private static Connection db;
record Identity(long orgId, String userId, String role) {}
@FunctionalInterface
interface RouteHandler {
void handle(HttpExchange exchange) throws Exception;
}
public static void main(String[] args) throws Exception {
Files.createDirectories(DATA_DIR);
initDb();
int port = Integer.parseInt(System.getenv().getOrDefault("PORT", "4400"));
HttpServer server = HttpServer.create(new InetSocketAddress(port), 0);
server.createContext("/health", wrap(RiskApi::health));
server.createContext("/risk/exposures", wrap(requireRoles(RiskApi::exposures, "admin", "manager", "viewer")));
server.createContext("/risk/reviews", wrap(requireRoles(RiskApi::reviews, "admin", "manager")));
server.createContext("/risk/report", wrap(requireRoles(RiskApi::report, "admin", "manager")));
server.createContext("/audit", wrap(requireRoles(RiskApi::audit, "admin")));
server.start();
System.out.println("risk-api listening on " + port);
}
private static void initDb() throws Exception {
db = DriverManager.getConnection("jdbc:h2:file:" + DATA_DIR.resolve("risk").toAbsolutePath());
try (Statement st = db.createStatement()) {
st.execute("""
create table if not exists organizations (
id bigint auto_increment primary key,
name varchar not null,
org_code varchar not null unique,
region varchar not null,
tier varchar not null
)
""");
st.execute("""
create table if not exists teams (
id bigint auto_increment primary key,
org_id bigint not null,
name varchar not null,
cost_center varchar not null,
status varchar not null
)
""");
st.execute("""
create table if not exists users (
id bigint auto_increment primary key,
org_id bigint not null,
team_id bigint,
email varchar not null,
display_name varchar not null,
role varchar not null
)
""");
st.execute("""
create table if not exists accounts (
id bigint auto_increment primary key,
org_id bigint not null,
account_no varchar not null,
currency varchar not null,
balance decimal not null
)
""");
st.execute("""
create table if not exists beneficiaries (
id bigint auto_increment primary key,
org_id bigint not null,
name varchar not null,
bank_name varchar not null,
routing varchar not null
)
""");
st.execute("""
create table if not exists transfers (
id bigint auto_increment primary key,
org_id bigint not null,
account_id bigint not null,
beneficiary_id bigint not null,
amount decimal not null,
currency varchar not null,
status varchar not null,
memo varchar not null,
requested_by varchar not null
)
""");
st.execute("""
create table if not exists risk_reviews (
id bigint auto_increment primary key,
org_id bigint not null,
subject varchar not null,
score decimal not null,
note varchar not null,
created_at timestamp default current_timestamp
)
""");
st.execute("""
create table if not exists audit_records (
id bigint auto_increment primary key,
org_id bigint,
actor varchar not null,
action varchar not null,
resource varchar not null,
details varchar not null,
created_at timestamp default current_timestamp
)
""");
}
try (Statement st = db.createStatement(); ResultSet rs = st.executeQuery("select count(*) from organizations")) {
rs.next();
if (rs.getInt(1) == 0) {
seed();
}
}
}
private static void seed() throws Exception {
try (PreparedStatement org = db.prepareStatement("insert into organizations (name, org_code, region, tier) values (?, ?, ?, ?)")) {
org.setString(1, "Acme Treasury");
org.setString(2, "acme");
org.setString(3, "us-east");
org.setString(4, "enterprise");
org.executeUpdate();
org.setString(1, "Northwind Holdings");
org.setString(2, "northwind");
org.setString(3, "eu-west");
org.setString(4, "growth");
org.executeUpdate();
}
}
private static HttpHandler wrap(RouteHandler handler) {
return exchange -> {
try {
Identity identity = identity(exchange);
exchange.setAttribute("identity", identity);
handler.handle(exchange);
audit(identity.orgId(), identity.userId(), exchange.getRequestMethod() + " " + exchange.getRequestURI().getPath(), exchange.getRequestURI().toString(), Map.of("status", "completed"));
} catch (Exception e) {
sendJson(exchange, 500, Map.of("error", e.getMessage()));
}
};
}
private static RouteHandler requireRoles(RouteHandler handler, String... roles) {
return exchange -> {
Identity identity = (Identity) exchange.getAttribute("identity");
if (identity == null) {
identity = identity(exchange);
exchange.setAttribute("identity", identity);
}
for (String role : roles) {
if (role.equals(identity.role())) {
handler.handle(exchange);
return;
}
}
sendJson(exchange, 403, Map.of("error", "forbidden"));
};
}
private static Identity identity(HttpExchange exchange) {
Headers headers = exchange.getRequestHeaders();
long orgId = parseLong(headers.getFirst("X-Org-Id"), 1L);
String userId = headers.getFirst("X-User-Id");
if (userId == null || userId.isBlank()) {
userId = "risk-user";
}
String role = headers.getFirst("X-Role");
if (role == null || role.isBlank()) {
role = "viewer";
}
return new Identity(orgId, userId, role);
}
private static void health(HttpExchange exchange) throws Exception {
try (Statement st = db.createStatement(); ResultSet rs = st.executeQuery("select count(*) from risk_reviews")) {
rs.next();
sendJson(exchange, 200, Map.of("ok", true, "riskReviews", rs.getInt(1)));
}
}
private static void exposures(HttpExchange exchange) throws Exception {
Identity identity = (Identity) exchange.getAttribute("identity");
Map<String, String> query = query(exchange.getRequestURI());
List<String> clauses = new ArrayList<>();
List<Object> args = new ArrayList<>();
clauses.add("org_id = ?");
args.add(identity.orgId());
if (query.containsKey("status") && !query.get("status").isBlank()) {
clauses.add("status = ?");
args.add(query.get("status"));
}
if (query.containsKey("currency") && !query.get("currency").isBlank()) {
clauses.add("currency = ?");
args.add(query.get("currency"));
}
if (query.containsKey("memo") && !query.get("memo").isBlank()) {
clauses.add("memo like ?");
args.add("%" + query.get("memo") + "%");
}
String sql = "select id, account_id, beneficiary_id, amount, currency, status, memo from transfers where " + String.join(" and ", clauses) + " order by id desc";
List<Map<String, Object>> rows = query(sql, args);
sendJson(exchange, 200, Map.of("data", rows));
}
private static void reviews(HttpExchange exchange) throws Exception {
Identity identity = (Identity) exchange.getAttribute("identity");
if ("POST".equalsIgnoreCase(exchange.getRequestMethod())) {
Map<String, Object> body = readJson(exchange.getRequestBody());
try (PreparedStatement ps = db.prepareStatement("insert into risk_reviews (org_id, subject, score, note) values (?, ?, ?, ?)")) {
ps.setLong(1, identity.orgId());
ps.setString(2, String.valueOf(body.get("subject")));
ps.setBigDecimal(3, new java.math.BigDecimal(String.valueOf(body.get("score"))));
ps.setString(4, String.valueOf(body.get("note")));
ps.executeUpdate();
}
sendJson(exchange, 201, Map.of("created", true));
return;
}
List<Map<String, Object>> rows = query(
"select id, subject, score, note, created_at from risk_reviews where org_id = ? order by id desc",
List.of(identity.orgId())
);
sendJson(exchange, 200, Map.of("data", rows));
}
private static void report(HttpExchange exchange) throws Exception {
Identity identity = (Identity) exchange.getAttribute("identity");
Map<String, String> query = query(exchange.getRequestURI());
String title = query.getOrDefault("title", "Risk Exposure Report");
String subject = query.getOrDefault("subject", "Treasury Portfolio");
String note = query.getOrDefault("note", "No note supplied");
List<Map<String, Object>> rows = query(
"select id, amount, currency, status from transfers where org_id = ? order by id desc limit 10",
List.of(identity.orgId())
);
StringBuilder html = new StringBuilder();
html.append("<html><body><h1>").append(title).append("</h1>");
html.append("<h2>").append(subject).append("</h2>");
html.append("<p>").append(note).append("</p>");
html.append("<ul>");
for (Map<String, Object> row : rows) {
html.append("<li>").append(row.get("currency")).append(" ").append(row.get("amount")).append(" ").append(row.get("status")).append("</li>");
}
html.append("</ul></body></html>");
sendText(exchange, 200, html.toString(), "text/html");
}
private static void audit(HttpExchange exchange) throws Exception {
Identity identity = (Identity) exchange.getAttribute("identity");
List<Map<String, Object>> rows = query(
"select id, actor, action, resource, details, created_at from audit_records where org_id = ? order by id desc limit 100",
List.of(identity.orgId())
);
sendJson(exchange, 200, Map.of("data", rows));
}
private static void audit(long orgId, String actor, String action, String resource, Map<String, Object> details) throws Exception {
try (PreparedStatement ps = db.prepareStatement("insert into audit_records (org_id, actor, action, resource, details) values (?, ?, ?, ?, ?)")) {
ps.setLong(1, orgId);
ps.setString(2, actor);
ps.setString(3, action);
ps.setString(4, resource);
ps.setString(5, MAPPER.writeValueAsString(details));
ps.executeUpdate();
}
}
private static List<Map<String, Object>> query(String sql, List<Object> args) throws Exception {
try (PreparedStatement ps = db.prepareStatement(sql)) {
for (int i = 0; i < args.size(); i++) {
ps.setObject(i + 1, args.get(i));
}
try (ResultSet rs = ps.executeQuery()) {
List<Map<String, Object>> rows = new ArrayList<>();
while (rs.next()) {
Map<String, Object> row = new LinkedHashMap<>();
int cols = rs.getMetaData().getColumnCount();
for (int i = 1; i <= cols; i++) {
row.put(rs.getMetaData().getColumnLabel(i), rs.getObject(i));
}
rows.add(row);
}
return rows;
}
}
}
private static Map<String, String> query(URI uri) {
Map<String, String> out = new HashMap<>();
String raw = uri.getRawQuery();
if (raw == null || raw.isBlank()) {
return out;
}
for (String part : raw.split("&")) {
int idx = part.indexOf('=');
String key = idx >= 0 ? part.substring(0, idx) : part;
String value = idx >= 0 ? part.substring(idx + 1) : "";
out.put(urlDecode(key), urlDecode(value));
}
return out;
}
private static Map<String, Object> readJson(InputStream inputStream) throws IOException {
return MAPPER.readValue(inputStream, new TypeReference<>() {});
}
private static void sendJson(HttpExchange exchange, int status, Map<String, Object> payload) throws IOException {
byte[] data = MAPPER.writeValueAsBytes(payload);
exchange.getResponseHeaders().set("Content-Type", "application/json");
exchange.sendResponseHeaders(status, data.length);
try (OutputStream os = exchange.getResponseBody()) {
os.write(data);
}
}
private static void sendText(HttpExchange exchange, int status, String text, String contentType) throws IOException {
byte[] data = text.getBytes(StandardCharsets.UTF_8);
exchange.getResponseHeaders().set("Content-Type", contentType);
exchange.sendResponseHeaders(status, data.length);
try (OutputStream os = exchange.getResponseBody()) {
os.write(data);
}
}
private static long parseLong(String value, long fallback) {
try {
return value == null ? fallback : Long.parseLong(value);
} catch (NumberFormatException e) {
return fallback;
}
}
private static String urlDecode(String value) {
return URLDecoder.decode(value, StandardCharsets.UTF_8);
}
}