Skip to content

Latest commit

 

History

History
187 lines (147 loc) · 8 KB

File metadata and controls

187 lines (147 loc) · 8 KB

AGENTS.md

Guidance for AI coding agents working in this repository. Human contributors should read it too — everything here is a real constraint, not a preference.

What this project is

paste-to-verify confirms that an Ethiopian CBE or Telebirr payment actually happened, starting from the SMS the customer received. It extracts the receipt link, fetches the provider's receipt page, normalizes it, and runs deterministic checks.

People use this to decide whether money arrived. A wrong verified: true is the worst possible bug in this codebase. Treat correctness as the priority over features, performance, or elegance.

Project structure

packages/core/     the published npm package (paste-to-verify)
  src/             library source — the only code that ships
  tests/           vitest suites + synthetic fixtures
  scripts/         live smoke checks, run manually, never in CI
apps/docs/         landing page + docs site (Next.js + Fumadocs)
apps/examples/     runnable demo servers — express/ and nextjs/

packages/* is for publishable npm libraries; apps/* is for deployables and consumers and is never published. A future PHP port belongs in a top-level php/ directory with its own composer.json — do NOT put it under packages/, which is claimed by the pnpm workspace.

Commands

Use pnpm. Never npm install, yarn, or bun — the lockfile is pnpm's.

pnpm install
pnpm build          # turbo: core, docs, examples
pnpm test           # vitest with coverage
pnpm typecheck
pnpm lint
pnpm format         # prettier --write
pnpm format:check   # what CI enforces

Scoped runs while iterating:

pnpm --filter paste-to-verify test
pnpm --filter @paste-to-verify/docs dev            # :3000
pnpm --filter @paste-to-verify/example-express dev # :3001

Before claiming work is done, run pnpm build && pnpm test && pnpm typecheck && pnpm lint && pnpm format:check — that is exactly what CI runs.

Invariants — do not break these

The receipt page is the source of truth. The SMS is not. The SMS supplies the receipt URL and fills fields the page omits, nothing more. When both sources have a field, the page value must win. Any change that lets SMS text override a page value is a security bug, because an SMS is trivially forged.

amount is the principal credited to the receiver, excluding fees. Fees, VAT, levies, totals and balances live under raw. The CBE SMS headline figure (e.g. 990.61) is the total debited, not the principal — that is why cbeProvider.parseSms deliberately never sets amount.

parseReceipt never throws an untyped error. Every failure path must surface as a PasteToVerifyError subclass with a stable code. Unexpected errors are wrapped in ReceiptParseError with the original as cause. Never let a raw TypeError escape.

verifyTransaction is pure, synchronous, and never throws. No network, no I/O, no clock reads except options.now. Failures are returned in failures, never raised.

Provider URLs match by hostname, never by substring. Use urlMatchesHost(url, host). url.includes('cbe.com.et') is a spoofing hole — https://cbe.com.et.evil.example/x contains it. There is a regression test for this; do not weaken it.

Timestamps are parsed by component and converted from EAT (UTC+3) to UTC. Never new Date(someString) — that silently assumes the host's timezone.

Never commit real receipt data. Committed fixtures and docs examples are synthetic and anonymized. Real receipts carry names, account numbers, and a working link. scripts/save-fixtures.mjs overwrites fixtures with live personal data — its output must never be committed.

Never reformat files under packages/core/tests/fixtures/. They are prettier-ignored on purpose: the parsers read text nodes and whitespace, so reformatting silently changes what the tests assert.

Writing code

  • TypeScript, strict. No any in shipped code; prefer precise types and narrow with type guards.
  • Every exported symbol gets a JSDoc block explaining what it does and any non-obvious contract. Match the density already in src/ — comments explain why, not what.
  • Prettier owns formatting (single quotes, semicolons, 100 columns, trailing commas). Do not hand-format; run pnpm format.
  • Keep packages/core dependency-light. It has exactly one runtime dependency (node-html-parser) and one optional peer (playwright). Adding a runtime dependency needs a real justification.
  • Node.js 18+ is the floor; the package ships ESM + CJS. Do not use APIs newer than Node 18 in src/.
  • Anything that touches the network or a browser must be injectable through RuntimeOptions (fetchImpl, render, timeoutMs) so it can be tested and run on edge runtimes.

Adding a provider

A provider is one object implementing ProviderPlugin: id, urlPattern, matchesUrl, parseSms, fetchAndParse, plus the optional buildReceiptUrl that powers parseReceiptId.

  • fetchAndParse is authoritative — return only what the page actually shows.
  • parseSms supplies fallbacks only, and must not guess at amount when the SMS headline is a total rather than a principal.
  • Throw ReceiptFetchError for network/non-2xx and ReceiptParseError when required fields are missing.
  • Register built-ins in src/providers/registry.ts. Consumers instead push onto the exported providers array — the docs must never tell a package user to recreate that file.

CBE-specific: the receipt token in the URL path (v2-…) is NOT the FT… transaction reference. buildReceiptUrl takes the token.

Testing

  • Vitest. Tests must never hit the network or launch a browser — inject stubs from tests/helpers.ts (stubFetch, recordingFetch, failingFetch, stubRenderer) via RuntimeOptions.
  • Coverage on packages/core/src stays above 90%. New branches need tests.
  • Every bug fix gets a regression test that fails before the fix.
  • Test the contract, not the implementation: assert on the normalized transaction and on failures, not on internal helper calls.
  • The scripts/*.mjs live checks are manual, need a gitignored .env, and must stay out of CI.

Docs site (apps/docs)

  • Content is MDX under content/docs/; meta.json controls sidebar order and section grouping. Frontmatter icon: accepts any Lucide icon name.
  • Callout, Cards, Steps, Tabs, TypeTable and code blocks are available in MDX without imports (see mdx-components.tsx).
  • Every factual claim must be verified against packages/core/src before it is written. The docs are the contract users read; drift is a bug.
  • Headings become anchor ids. When linking cross-page anchors, confirm the id exists rather than guessing the slug.

Examples (apps/examples)

  • Their dev/start scripts rebuild packages/core first, on purpose: they import the built dist, and a stale build silently breaks newer exports. Keep that step.
  • Demo mode injects fixture-backed fetchImpl/render for the sample receipts, so the demos work with no network. Keep them working offline.
  • Both servers accept { sms } | { url } | { provider, id } and must map typed error codes to HTTP status consistently with the docs.

Releasing

Semver, currently 0.x — minor versions may carry breaking changes.

  1. Update CHANGELOG.md.
  2. Bump version in packages/core/package.json.
  3. git tag vX.Y.Z && git push origin vX.Y.Z.

The Release workflow builds, tests, and publishes to npm via Trusted Publishing (OIDC) with provenance. Do not publish by hand unless that workflow is broken, and never commit an npm token.

Commits and pull requests

  • Write imperative subject lines that say what changed and why it matters. Explain reasoning in the body when the change is not self-evident.
  • Do not add AI attribution, co-author trailers, or tool advertisements to commits, PRs, or code comments.
  • Rebase onto the latest main before opening a PR; main is squash-merged and linear.
  • Do not commit or push unless the user explicitly asks.