From 7e1ea05da1b7ef105855aea0062d443d425f0ca1 Mon Sep 17 00:00:00 2001 From: Andrew Zolotukhin Date: Sat, 6 Jun 2026 02:19:23 +0000 Subject: [PATCH 1/2] Prepare repository for open source launch --- .github/ISSUE_TEMPLATE/bug_report.yml | 65 +++++ .github/ISSUE_TEMPLATE/config.yml | 8 + .github/ISSUE_TEMPLATE/feature_request.yml | 45 ++++ .github/ISSUE_TEMPLATE/framework_question.yml | 39 +++ .github/PULL_REQUEST_TEMPLATE.md | 22 ++ CODE_OF_CONDUCT.md | 34 +++ CONTRIBUTING.md | 114 +++++++++ LICENSE | 21 ++ README.md | 242 ++++++++---------- SECURITY.md | 35 +++ apps/web/app/layout.tsx | 35 ++- apps/web/app/manifest.ts | 3 +- apps/web/components/landing-page.test.tsx | 18 +- apps/web/components/landing-page.tsx | 65 ++--- apps/web/public/og-image.png | Bin 0 -> 97072 bytes docs/assets/xpenser-landing-preview.png | Bin 0 -> 156906 bytes docs/cleverbrush-reference.md | 12 + package-lock.json | 1 + package.json | 20 ++ 19 files changed, 602 insertions(+), 177 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yml create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/feature_request.yml create mode 100644 .github/ISSUE_TEMPLATE/framework_question.yml create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 CODE_OF_CONDUCT.md create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE create mode 100644 SECURITY.md create mode 100644 apps/web/public/og-image.png create mode 100644 docs/assets/xpenser-landing-preview.png diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..7c010bf --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,65 @@ +name: Bug report +description: Report a reproducible xpenser problem. +title: "[Bug]: " +labels: + - bug +body: + - type: markdown + attributes: + value: | + Thanks for reporting a bug. Please do not include secrets, API keys, or personal finance data. + - type: textarea + id: summary + attributes: + label: Summary + description: What happened? + validations: + required: true + - type: textarea + id: steps + attributes: + label: Reproduction steps + description: List the smallest steps that reproduce the issue. + placeholder: | + 1. Start the app with ... + 2. Open ... + 3. Click ... + validations: + required: true + - type: textarea + id: expected + attributes: + label: Expected behavior + validations: + required: true + - type: textarea + id: actual + attributes: + label: Actual behavior + validations: + required: true + - type: dropdown + id: area + attributes: + label: Area + options: + - Web app + - API + - MCP server + - Telegram bot + - Self-hosting / Docker + - Cleverbrush reference docs + - Other + validations: + required: true + - type: textarea + id: environment + attributes: + label: Environment + description: Browser, OS, Node/npm versions, Docker version, deployment mode, or other relevant details. + - type: textarea + id: logs + attributes: + label: Logs or screenshots + description: Paste relevant logs or attach screenshots. Redact secrets and personal data. + render: shell diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..d3f7f3b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: false +contact_links: + - name: Questions and ideas + url: https://github.com/cleverbrush/xpenser/discussions + about: Use GitHub Discussions for open-ended questions, ideas, and Cleverbrush learning threads. + - name: Security reports + url: https://github.com/cleverbrush/xpenser/security/advisories/new + about: Please report vulnerabilities privately through GitHub Security Advisories. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000..8a2b73c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,45 @@ +name: Feature request +description: Suggest a product, self-hosting, integration, or documentation improvement. +title: "[Feature]: " +labels: + - enhancement +body: + - type: textarea + id: problem + attributes: + label: Problem + description: What user need or workflow should this solve? + validations: + required: true + - type: textarea + id: proposal + attributes: + label: Proposed solution + description: Describe the change you would like to see. + validations: + required: true + - type: dropdown + id: area + attributes: + label: Area + options: + - Finance workflow + - Self-hosting + - External API + - MCP server + - Telegram bot + - Cleverbrush reference example + - Documentation + - Other + validations: + required: true + - type: textarea + id: alternatives + attributes: + label: Alternatives considered + description: Mention any workaround, existing tool, or alternate design. + - type: textarea + id: contribution + attributes: + label: Contribution interest + description: If you want to work on this, note your intended approach or questions. diff --git a/.github/ISSUE_TEMPLATE/framework_question.yml b/.github/ISSUE_TEMPLATE/framework_question.yml new file mode 100644 index 0000000..dd34254 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/framework_question.yml @@ -0,0 +1,39 @@ +name: Cleverbrush framework question +description: Ask a question about how xpenser uses Cleverbrush Framework. +title: "[Cleverbrush]: " +labels: + - question +body: + - type: markdown + attributes: + value: | + For broad discussion, examples, and learning threads, GitHub Discussions is usually the best place. Use this issue form when the question points to a concrete documentation gap or unclear repository behavior. + - type: textarea + id: question + attributes: + label: Question + description: What are you trying to understand? + validations: + required: true + - type: textarea + id: context + attributes: + label: Context + description: Link the file, concept, endpoint, schema, or workflow you are reading. + - type: dropdown + id: area + attributes: + label: Framework area + options: + - Schema / contracts + - Server handlers + - OpenAPI + - Client middleware + - React forms + - Auth / authorization + - DI / ORM + - Observability + - MCP + - Other + validations: + required: true diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..0deb0ed --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,22 @@ +## Summary + + + +## Validation + + + +- [ ] `npm run lint` +- [ ] `npm run typecheck` +- [ ] `npm test` + +## Screenshots / Preview + + + +## Checklist + +- [ ] I kept the change focused. +- [ ] I updated docs or tests where needed. +- [ ] I checked for secrets, local env files, and generated build output. +- [ ] API changes keep contracts, endpoint metadata, and handlers aligned. diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..2c43c5b --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,34 @@ +# Code of Conduct + +## Our Pledge + +We want xpenser to be a practical, respectful project for people improving the +app, learning Cleverbrush Framework, or sharing operational experience from +self-hosting it. + +Participants are expected to communicate with care, assume good intent when +reasonable, and focus criticism on the work rather than the person. + +## Expected Behavior + +- Be respectful and direct. +- Keep discussions relevant to the project. +- Give actionable feedback when reviewing code or documentation. +- Credit others for ideas and contributions. +- Respect maintainers' time by keeping issues and pull requests focused. + +## Unacceptable Behavior + +- Harassment, threats, or personal attacks. +- Discriminatory language or behavior. +- Publishing private information without permission. +- Spam, trolling, or repeated off-topic comments. +- Deliberate disruption of project spaces. + +## Enforcement + +Maintainers may edit, hide, or remove comments; close issues or discussions; +block participants; or take other reasonable action to protect the project. + +If you need to report a conduct concern, contact the maintainers privately +through GitHub. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..e4abde7 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,114 @@ +# Contributing to xpenser + +Thanks for helping improve xpenser. This repository is both a personal finance +app and a reference implementation for Cleverbrush Framework, so changes should +keep the product useful and the framework patterns easy to learn. + +## Where To Start + +- Use GitHub Discussions for questions, design ideas, and Cleverbrush learning + threads. +- Use issues for actionable bugs, feature requests, and documentation gaps. +- Keep pull requests small enough to review. Prefer focused product, docs, + test, or framework-reference improvements over broad rewrites. + +Good first contribution areas: + +- README and self-hosting improvements. +- API, MCP, and typed-client examples. +- UI polish on existing workflows. +- Tests around Cleverbrush contract, form, and handler behavior. +- Small product improvements to transaction, dashboard, vendor, or category + workflows. + +## Local Setup + +Requirements: + +- Node.js 22 +- npm 11 +- Docker with Docker Compose v2 + +Install dependencies: + +```sh +npm install +``` + +Create local environment settings: + +```sh +cp .env.example .env +``` + +Build shared workspaces before starting dev servers: + +```sh +npm run build -w @xpenser/contracts +npm run build -w @xpenser/client +npm run build -w @xpenser/ui +``` + +Start PostgreSQL: + +```sh +docker compose up -d postgres +``` + +Start the app: + +```sh +npm run dev +``` + +Local URLs: + +- Web app: http://localhost:3000 +- API: http://localhost:4000 +- OpenAPI JSON: http://localhost:4000/openapi.json + +## Development Workflow + +Before opening a pull request, run the relevant checks: + +```sh +npm run lint +npm run typecheck +npm test +``` + +Use focused tests for the behavior you changed. Add or update Playwright tests +when changing user-facing workflows that should be validated end to end. + +The e2e suite needs a running app or deployed preview: + +```sh +PLAYWRIGHT_BASE_URL=http://localhost:3000 npm run test:e2e +``` + +## Cleverbrush Patterns To Preserve + +- Define public API shape in `packages/contracts`. +- Keep contract tree, API endpoint metadata tree, and handler tree aligned. +- Reuse named schema constants when the same shape appears in more than one + endpoint. +- Keep credential-bearing integrations behind server-side modules. +- Put tracing middleware before other API middleware. +- Prefer `ActionResult` helpers for expected API responses. + +See [Cleverbrush Reference Notes](./docs/cleverbrush-reference.md) for the +current framework map and tests that guard these patterns. + +## Pull Request Guidelines + +- Describe the user-facing behavior or documentation outcome clearly. +- Include validation commands and any skipped checks with reasons. +- Add screenshots for visible UI changes. +- Keep generated artifacts, build output, local env files, and secrets out of + the commit. +- Do not change unrelated formatting or refactor outside the request scope. + +## Security + +Do not report vulnerabilities in public issues. Use the process in +[SECURITY.md](./SECURITY.md). diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d49ddae --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Cleverbrush + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index c308a31..d189807 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,58 @@ # xpenser -Personal income and expense tracking app. - -xpenser also serves as a demonstrator for projects based on CleverBrush -Framework. See -[Cleverbrush Reference Notes](./docs/cleverbrush-reference.md) for the -framework integration patterns, security baseline, and tests that keep the app -usable as an example. - -## Local Development - -This setup runs the API and web app on your machine, with PostgreSQL running in -Docker. +Open-source personal income and expense tracking for people who want a +self-hosted finance app they can inspect, extend, and connect to their own +workflows. + +![xpenser landing page preview](./docs/assets/xpenser-landing-preview.png) + +xpenser is also a real-world reference app for +[Cleverbrush Framework](https://docs.cleverbrush.com). It shows how a +schema-first TypeScript stack can drive API contracts, validation, OpenAPI, +typed clients, React forms, auth-aware endpoints, observability, Telegram +workflows, and MCP access from one cohesive application. + +## Why xpenser + +- Track income, expenses, refunds, and returns with categories, notes, dates, + vendors, and currencies. +- Review daily, weekly, monthly, quarterly, and yearly summaries with category + split and trend context. +- Use multiple transaction currencies with automatic conversion to your default + currency through [Frankfurter](https://www.frankfurter.app/). +- Capture transaction scans, enrich vendor data, and keep setup workflows usable + on mobile and desktop. +- Receive optional weekly and monthly email summaries with OpenAI-generated + spending and income insights. +- Connect external tools through API keys, a typed Node client, a read-only MCP + server, and a Telegram bot. + +## Built With Cleverbrush + +xpenser is intentionally small enough to inspect while still exercising +production-shaped framework patterns: + +- `packages/contracts` defines the public contract with Cleverbrush schemas. +- `apps/api` exposes the contract through Cleverbrush server handlers, + auth metadata, OpenAPI, DI, logging, tracing, and MCP. +- `packages/client` wraps the generated Cleverbrush client with retry, timeout, + dedupe, batching, cache tags, and OpenTelemetry propagation. +- `packages/ui` binds Cleverbrush schema fields to reusable React form + controls. + +Start with [Cleverbrush Reference Notes](./docs/cleverbrush-reference.md) if +you are here to learn the framework patterns behind the app. For upstream +framework docs, use: + +- [Cleverbrush Framework docs](https://docs.cleverbrush.com) +- [Cleverbrush Schema docs](https://schema.cleverbrush.com) + +## Quick Start ### Prerequisites - Node.js 22 -- npm +- npm 11 - Docker with Docker Compose v2 (`docker compose`) ### 1. Install dependencies @@ -31,20 +67,12 @@ npm install cp .env.example .env ``` -The defaults in `.env.example` are already set up for local development with -PostgreSQL exposed on `localhost:5432`: - -```env -DB_HOST=localhost -DB_PORT=5432 -DB_NAME=xpenser -DB_USER=xpenser -DB_PASSWORD=xpenser_secret -``` +The defaults in `.env.example` are safe for local development and point the app +at PostgreSQL on `localhost:5432`. -### 3. Build the shared workspaces +### 3. Build shared workspaces -The apps import the local packages from their built `dist` outputs, so build the +The apps import local packages from their built `dist` outputs, so build the shared packages once before starting dev servers: ```sh @@ -55,13 +83,11 @@ npm run build -w @xpenser/ui ### 4. Start PostgreSQL -Start only the Postgres service from `docker-compose.yml`: - ```sh docker compose up -d postgres ``` -Check that it is running: +Useful checks: ```sh docker compose ps postgres @@ -83,12 +109,12 @@ Local URLs: - API health check: http://localhost:4000/health - OpenAPI JSON: http://localhost:4000/openapi.json -### Authentication +## Authentication -Email/password sign-in is built in and works without any external auth provider. -Accounts created this way must confirm their email before signing in. +Email/password sign-in works without any external auth provider. Accounts +created this way must confirm their email before signing in. -Google sign-in has two supported modes: +Google sign-in supports two modes: - Direct Google OAuth for self-hosted deployments. - Cleverbrush Passport for the hosted Cleverbrush deployment. @@ -110,7 +136,7 @@ Use `GOOGLE_SIGN_IN_MODE=direct` to require direct Google OAuth, `GOOGLE_SIGN_IN_MODE=disabled` to hide Google sign-in even when credentials are present. -#### Direct Google OAuth for self-hosting +### Direct Google OAuth Create an OAuth 2.0 client in Google Cloud Console: @@ -136,16 +162,11 @@ AUTH_GOOGLE_ID=your-google-oauth-client-id AUTH_GOOGLE_SECRET=your-google-oauth-client-secret ``` -The web app validates the Google profile through Auth.js, then calls the private -xpenser API with `WEB_API_SERVICE_SECRET`. The API resolves or creates a local -`google` user, stores the Google subject in `external_identities`, and returns -the same xpenser API JWT used by email/password sessions. - Google accounts must have a verified email address. If a local email/password -account already exists with the same email, Google sign-in is rejected instead of -silently linking the accounts. +account already exists with the same email, Google sign-in is rejected instead +of silently linking the accounts. -#### Passport for Cleverbrush deployment +### Cleverbrush Passport Passport is a private Cleverbrush auth broker. Self-hosted deployments should use direct Google OAuth unless they run their own compatible Passport service. @@ -160,68 +181,20 @@ PASSPORT_ENVIRONMENT=production PASSPORT_PUBLIC_KEY= ``` -`PASSPORT_PUBLIC_KEY` is optional. When it is empty, the API fetches -`/.well-known/public-key` and caches it in memory. If set, use -the base64-encoded PEM public key. - -Register the production Passport environment with: - -```sh -curl -X PUT "$PASSPORT_BASE_URL/api/projects/xpenser/environments/production" \ - -H "Authorization: ServiceKey $PASSPORT_SERVICE_KEY" \ - -H "Content-Type: application/json" \ - -d '{ - "frontend_origin": "https://xpenser.cleverbrush.com", - "callback_path": "/auth/callback", - "backend_auth_url": "https://xpenser.cleverbrush.com/external-api/auth/passport", - "status": "active" - }' -``` - -To see distributed traces during local development, run the Compose observability -services or the full Docker stack so `OTEL_EXPORTER_OTLP_ENDPOINT` points at a -live collector. The web app reports as `xpenser-web`; the API reports as -`xpenser-api`. - -## Database Commands - -Run migrations manually if needed: - -```sh -npm run db:run -w @xpenser/api -``` - -Stop Postgres without deleting data: - -```sh -docker compose stop postgres -``` - -Stop Compose services and remove containers/networks: - -```sh -docker compose down -``` - -Reset the local database by removing the Postgres volume: - -```sh -docker compose down -v -docker compose up -d postgres -``` +`PASSPORT_PUBLIC_KEY` is optional. When empty, the API fetches +`/.well-known/public-key` and caches it in memory. If set, +use the base64-encoded PEM public key. ## Full Docker Run -For a production-like local run, build and start the Compose stack: +For a production-like local run, build and start the full Compose stack: ```sh docker compose up --build ``` -This starts the containerized web app, API, PostgreSQL, Swagger UI, and the -observability services defined in `docker-compose.yml`. Requests that start in -the web app and call the API should appear in SigNoz as one distributed trace -with spans from both services. +This starts the containerized web app, API, PostgreSQL, Swagger UI, and +observability services defined in `docker-compose.yml`. Full Docker URLs: @@ -230,7 +203,11 @@ Full Docker URLs: - Swagger UI: http://localhost:8090 - SigNoz: http://localhost:8080 -## External API Access +For public deployments, put your reverse proxy in front of the web app and set +`APP_URL` to the public origin. The API service stays private on the Docker +network and the Next app exposes it under `/external-api`. + +## External API Create an API key from Settings -> Preferences -> API keys. The API key can be used as a bearer token with curl or with the typed Node client: @@ -264,15 +241,15 @@ await client.transactions.create({ ``` Omit `effect` or set it to `normal` for regular transactions. Use -`effect: 'reversal'` for refunds in expense categories or payments/chargebacks -in income categories; the entered amount stays positive and reports subtract it -from that category. +`effect: 'reversal'` for refunds in expense categories or payments and +chargebacks in income categories; the entered amount stays positive and reports +subtract it from that category. `X-API-Key: $XPENSER_API_KEY` is also accepted. ## MCP Server -xpenser also exposes a read-only MCP Streamable HTTP endpoint for AI agents at +xpenser exposes a read-only MCP Streamable HTTP endpoint for AI agents at `/external-api/mcp`. Use the same API key from Settings -> Preferences -> API keys as a bearer token: @@ -294,48 +271,45 @@ The MCP server exposes read-only tools for the current user, categories, transactions, dashboard summaries, and statistics. Transaction write operations are not exposed through MCP. -In Docker Compose, the API service stays private on the Docker network and the -Next app exposes it under `/external-api`. Put your host reverse proxy in front -of the web app: - -```nginx -server { - server_name xpenser.example.com; +## Development Commands - location / { - proxy_pass http://127.0.0.1:3000; - proxy_set_header Host $host; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - } -} +```sh +npm run lint +npm run typecheck +npm test +npm run test:e2e ``` -Set `APP_URL` to the public web origin. The API's OpenAPI server URL defaults to -`${APP_URL}/external-api` in Compose and can be overridden with -`PUBLIC_API_BASE_URL`. +Database helpers: + +```sh +npm run db:run -w @xpenser/api +docker compose stop postgres +docker compose down +docker compose down -v +``` -## Ephemeral PR Environments +The e2e suite requires `PLAYWRIGHT_BASE_URL` when run outside the GitHub PR +environment. -See [PR_ENVIRONMENTS.md](./PR_ENVIRONMENTS.md) for the nginx proxy script, -environment deploy script, and GitHub Actions setup. +## Contributing -## Troubleshooting +Contributions are welcome. Good first areas include documentation, self-hosting +guides, framework reference notes, UI polish, API examples, and small focused +product improvements. -If port `5432` is already in use, change `POSTGRES_PORT` in `.env` and update -`DB_PORT` to match. +- Read [CONTRIBUTING.md](./CONTRIBUTING.md) before opening a PR. +- Use issues for actionable bugs and feature requests. +- Use GitHub Discussions for questions, ideas, and Cleverbrush learning threads. +- Keep Cleverbrush contract, endpoint metadata, and handler trees aligned when + changing the API. -If port `3000` or `4000` is already in use, stop the conflicting process or -change the relevant app port before starting the dev servers. +## Project Status -If login/register fails after changing secrets or resetting data, stop the dev -server, clear browser cookies for `localhost`, and start the app again. +xpenser is early, practical, and evolving. The goal is to remain useful as a +personal finance app while staying clear enough for developers to learn how a +Cleverbrush full-stack project fits together. -If the Google sign-in button is hidden, either set `AUTH_GOOGLE_ID` and -`AUTH_GOOGLE_SECRET` for direct Google OAuth, set complete Passport variables -with `GOOGLE_SIGN_IN_MODE=passport`, or set `GOOGLE_SIGN_IN_MODE=direct` to fail -fast when Google credentials are missing. +## License -If Google returns a redirect URI mismatch, add the exact -`${APP_URL}/api/auth/callback/google` URL to the Google OAuth client. The scheme, -host, port, and path must match the public URL users open in the browser. +xpenser is released under the [MIT License](./LICENSE). diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..7ab805a --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,35 @@ +# Security Policy + +## Supported Versions + +xpenser is pre-1.0. Security fixes target the `main` branch unless a release +branch is explicitly announced. + +## Reporting A Vulnerability + +Please do not open a public issue for a vulnerability. + +Use GitHub private vulnerability reporting for this repository. If private +reporting is not visible, contact a maintainer privately through their GitHub +profile and include only the minimum information needed to establish a private +channel. + +Helpful details include: + +- Affected component or endpoint. +- Impact and who can trigger it. +- Reproduction steps or proof of concept. +- Whether credentials, API keys, Telegram links, or personal finance data may be + exposed. + +Maintainers will acknowledge valid reports as soon as practical and coordinate +fixes before public disclosure. + +## Security Baseline + +- Production startup rejects documented placeholder secrets. +- Passwords use scrypt with per-password salts. +- API keys, Telegram link tokens, and email confirmation tokens are stored as + hashes. +- MCP access requires an API-key principal and exposes read-only tools. +- Database spans redact SQL text at the instrumentation boundary. diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx index ad024b0..bc5b7e7 100644 --- a/apps/web/app/layout.tsx +++ b/apps/web/app/layout.tsx @@ -4,10 +4,39 @@ import './globals.css'; import { XpenserWebFormProvider } from '@/components/forms/schema-fields'; import { ThemeProvider } from '@/components/theme-provider'; +const publicUrl = process.env.APP_URL ?? 'https://xpenser.cleverbrush.com'; +const description = + 'Open-source personal finance tracking for self-hosted workflows, built with Cleverbrush Framework.'; + export const metadata: Metadata = { - title: 'xpenser', - description: - 'Personal finance tracking and Cleverbrush Framework demonstrator' + metadataBase: new URL(publicUrl), + applicationName: 'xpenser', + title: { + default: 'xpenser', + template: '%s | xpenser' + }, + description, + openGraph: { + type: 'website', + url: '/', + siteName: 'xpenser', + title: 'xpenser', + description, + images: [ + { + url: '/og-image.png', + width: 1200, + height: 630, + alt: 'xpenser personal finance app preview' + } + ] + }, + twitter: { + card: 'summary_large_image', + title: 'xpenser', + description, + images: ['/og-image.png'] + } }; export default function RootLayout({ diff --git a/apps/web/app/manifest.ts b/apps/web/app/manifest.ts index 928843b..376edfc 100644 --- a/apps/web/app/manifest.ts +++ b/apps/web/app/manifest.ts @@ -4,7 +4,8 @@ export default function manifest(): MetadataRoute.Manifest { return { name: 'xpenser', short_name: 'xpenser', - description: 'Personal income and expense tracking app.', + description: + 'Open-source personal finance tracking for self-hosted workflows.', start_url: '/dashboard', scope: '/', display: 'standalone', diff --git a/apps/web/components/landing-page.test.tsx b/apps/web/components/landing-page.test.tsx index ff5e109..9811dcf 100644 --- a/apps/web/components/landing-page.test.tsx +++ b/apps/web/components/landing-page.test.tsx @@ -15,18 +15,16 @@ describe('LandingPage', () => { screen.getByRole('heading', { level: 1, name: 'xpenser' }) ).toBeTruthy(); expect( - screen.getByText(/Cleverbrush Framework: typed contracts/i) + screen.getByText(/Track income, expenses, refunds/i) ).toBeTruthy(); expect( - screen.getByText(/initially a demonstrator of possibilities/i) + screen.getByText(/Learn Cleverbrush from a working app/i) ).toBeTruthy(); expect(screen.getAllByText(/Telegram bot/i).length).toBeGreaterThan(0); expect(screen.getAllByText(/MCP server/i).length).toBeGreaterThan(0); - expect( - screen.getAllByText(/Mobile-friendly interface/i).length - ).toBeGreaterThan(0); - expect(screen.getAllByText(/open-source/i).length).toBeGreaterThan(0); expect(screen.getAllByText(/self-hosted/i).length).toBeGreaterThan(0); + expect(screen.getAllByText(/open-source/i).length).toBeGreaterThan(0); + expect(screen.getAllByText(/MIT licensed/i).length).toBeGreaterThan(0); expect( screen.getAllByText(/multiple currencies/i).length ).toBeGreaterThan(0); @@ -58,7 +56,9 @@ describe('LandingPage', () => { ) ).toBe(true); - const docsLinks = screen.getAllByRole('link', { name: /docs/i }); + const docsLinks = screen.getAllByRole('link', { + name: /cleverbrush docs/i + }); expect( docsLinks.every( link => @@ -66,7 +66,9 @@ describe('LandingPage', () => { ) ).toBe(true); - const schemaLinks = screen.getAllByRole('link', { name: /schema/i }); + const schemaLinks = screen.getAllByRole('link', { + name: /schema docs/i + }); expect( schemaLinks.every( link => diff --git a/apps/web/components/landing-page.tsx b/apps/web/components/landing-page.tsx index 8c8c6ff..08cacb3 100644 --- a/apps/web/components/landing-page.tsx +++ b/apps/web/components/landing-page.tsx @@ -122,12 +122,12 @@ const capabilityRows = [ ] as const; const heroProofs = [ - 'Fast financial entry', - 'Mobile-friendly interface', - 'MCP server access', + 'Self-hosted finance app', + 'Multi-currency tracking', + 'Read-only MCP access', 'Telegram bot integration', - 'Open-source and self-hosted ready', - 'Frankfurter currency conversion' + 'Cleverbrush reference code', + 'MIT licensed' ] as const; const resourceLinks = [ @@ -139,12 +139,12 @@ const resourceLinks = [ { href: 'https://docs.cleverbrush.com', icon: BookOpenIcon, - label: 'Docs' + label: 'Cleverbrush Docs' }, { href: 'https://schema.cleverbrush.com', icon: BracesIcon, - label: 'Schema' + label: 'Schema Docs' } ] as const; @@ -389,19 +389,21 @@ export function LandingPage() {
-
+
- Cleverbrush Framework demonstrator + Open-source personal finance

xpenser

- Open-source, self-hosted ready personal income and - expense tracking that initially demonstrates what - can be built with Cleverbrush Framework: typed - contracts, schema-driven forms, observable services, - and connected app workflows. + Track income, expenses, refunds, currencies, + vendors, and reports in a self-hosted app that keeps + the code open for inspection. Under the product + surface, xpenser shows how Cleverbrush Framework + connects typed contracts, schema-driven forms, + observable services, Telegram, API, and MCP + workflows.