openvibers/openvibe.coupons AGENTS.md
AGENTS.md
| 1 | # AGENTS.md — OpenVibe.Coupons |
| 2 | |
| 3 | ## Project overview |
| 4 | |
| 5 | Coupon codes with merchant matching, restrictions, expiry and real-people validity reports, plus the |
| 6 | public API the OpenVibe browser helper uses. Distinct from Deals even where they cross-link. Node.js/ |
| 7 | Express, PostgreSQL (`ov_coupons`), server-rendered, CommonJS, **no build step**. |
| 8 | |
| 9 | - Service id `coupons`, port `4850`, origin `https://openvibe.coupons`, unit `openvibe-coupons.service`, |
| 10 | release root `/opt/openvibe.coupons`. |
| 11 | - It holds **no codes** today: nothing is seeded and no coupon source is enabled. |
| 12 | |
| 13 | ## The four rules |
| 14 | |
| 15 | 1. **No code is "working" because a model guessed it.** Status comes only from recent reports by |
| 16 | people; a submission, a source, staff or AI output cannot set it. |
| 17 | 2. **Unknown stays unknown.** An unstated expiry is "Expiry unknown" (`null` in JSON); no recent reports |
| 18 | means no confidence number; "no restrictions stated" is never "no restrictions". |
| 19 | 3. **Expired codes leave active results predictably** — at the instant their known expiry passes, in |
| 20 | the API, pages, feeds, sitemaps and Search. |
| 21 | 4. **A merchant page (or any page) cannot obtain anyone's data.** Lookups return the same bytes to |
| 22 | every caller; the API ignores cookies; the extension holds no OpenVibe session. |
| 23 | |
| 24 | ## What it owns |
| 25 | |
| 26 | The nine charter tables in [migrations/](migrations/): |
| 27 | |
| 28 | | Table | What | |
| 29 | |---|---| |
| 30 | | `coupon_merchants` | `pending` (member proposal, invisible until staff approve), `active`, `disabled` | |
| 31 | | `coupon_merchant_domains` | normalized host, registrable domain (eTLD+1), subdomain rule, optional path prefix | |
| 32 | | `coupons` | code; status exactly `unknown\|reported_working\|reported_failed\|expired\|disabled`; confidence `NULL` when no recent reports; `expires_at` `NULL` with precision (`instant`/`date`) and basis (`evidence`/`submitter`/`source`/`staff`); review state `pending\|published`; origin `member\|staff\|source\|ai` | |
| 33 | | `coupon_sources` | evidence: member/staff submissions (URL, own-domain flag), Sources items (id, revision, retrieval time), AI runs | |
| 34 | | `coupon_restrictions` | min spend (minor units + ISO 4217), categories, new customers only, ISO 3166-1 regions, other | |
| 35 | | `coupon_validation_reports` | worked/failed; one per (reporter, code, UTC day); reporter is an HMAC of the subject | |
| 36 | | `coupon_status_history` | every status/confidence change with reason and actor | |
| 37 | | `coupon_application_hints` | how to apply a code (one code or all of a merchant's) | |
| 38 | | `coupon_watches` | private member watches | |
| 39 | |
| 40 | Plus `extension_installs` (browser-helper tokens, hashes only), the Sources importer state/holds, the |
| 41 | SDK outbox and the publishing index sequencer. |
| 42 | |
| 43 | Not owned: the merchant's real terms, the browser extension itself |
| 44 | ([OpenVibe.Extensions](https://github.com/OpenVibers/OpenVibe.Extensions)), identity, the Search index. |
| 45 | |
| 46 | ## Who calls it, and whom it calls |
| 47 | |
| 48 | - Callers: people in the browser; the **browser helper** with an extension token; OpenVibe.Sources' |
| 49 | importer; services with a Network service token audience `openvibe.coupons` and the route's |
| 50 | capability; staff moderation. |
| 51 | - Depends on: **Network** (SSO client `coupons`, JWKS, account export/deletion); **Sources** |
| 52 | (`sources.item.read`, category `coupons`; the seeded `staff-coupon-codes` source is disabled until a |
| 53 | person enables it); **Events** (SDK outbox); **Search** (consumes `coupons.index_document.*`); |
| 54 | `openvibe-publishing` (seo gate, ssr, index hooks); PostgreSQL and Valkey; `openvibe-contracts`/ |
| 55 | `openvibe-sdk`/`openvibe-shared` pins. |
| 56 | |
| 57 | ## Repository map |
| 58 | |
| 59 | - **Entry:** [server/index.js](server/index.js) — app, worker (expiry sweep, importer), outbox; |
| 60 | graceful stop. |
| 61 | - **App:** [server/app.js](server/app.js) — helmet/CSP, `/auth`, `/shared`, `/api/v1` (API), |
| 62 | `/api`, pages, discovery, health/ready. |
| 63 | - **Config/db:** [server/config.js](server/config.js), [server/db.js](server/db.js). |
| 64 | - **Domain:** [server/domain/](server/domain/) — `publication.js`, `merchants.js`, `coupons.js`, |
| 65 | `reports.js`, `installs.js`, `watches.js`, `facts.js`, `moderation.js`. |
| 66 | - **Clients:** [server/clients/sources.js](server/clients/sources.js). |
| 67 | - **HTTP/render:** [server/http/api.js](server/http/api.js), `public.js`, `discovery.js`, |
| 68 | `actor-limits.js`; [server/render/](server/render/) — pages, home, offer, store, short, layout. |
| 69 | - **Auth:** [server/auth/viewer.js](server/auth/viewer.js); [server/sharing.js](server/sharing.js). |
| 70 | - **Worker/importer:** [server/worker.js](server/worker.js), `server/events/consumer.js`. |
| 71 | - **Deploy:** [deploy/systemd/openvibe-coupons.service](deploy/systemd/openvibe-coupons.service), |
| 72 | [deploy/nginx/openvibe.coupons.conf](deploy/nginx/openvibe.coupons.conf). |
| 73 | |
| 74 | ## Commands |
| 75 | |
| 76 | ```bash |
| 77 | npm ci |
| 78 | npm run dev # NODE_ENV=development node server/index.js |
| 79 | npm start # node server/index.js, http://127.0.0.1:4850 |
| 80 | npm test # test/run.js, PGlite, mock Network/Sources/Events |
| 81 | npm run test:pg # same on PostgreSQL (COUPONS_TEST_STORE=pg) |
| 82 | npm test -- lifecycle # filter by file name; --strict makes a skip fail |
| 83 | ov test # the harness's sharded run of what the change reaches |
| 84 | ``` |
| 85 | |
| 86 | ## Database and migrations |
| 87 | |
| 88 | `ov_coupons` on the host's data role (ADR-035); PGlite in development/tests. Async through |
| 89 | `openvibe-sdk/db`; migrations `migrations/NNNN_*.sql` applied at boot; never edit an applied migration, |
| 90 | add a new number. The status/confidence history is append-only in spirit: a change writes |
| 91 | `coupon_status_history` and the outbox in the same transaction, and the expiry sweep records the event |
| 92 | and tombstone once. |
| 93 | |
| 94 | ## Auth and privacy |
| 95 | |
| 96 | - People sign in through Network SSO for submissions, watches and reports. The **public API takes an |
| 97 | `Authorization` header only**; it ignores cookies and returns the same bytes to every caller, so a |
| 98 | merchant page cannot read a visitor's data. |
| 99 | - The browser helper authenticates with an **extension token**: stored as a hash, scoped, revocable, |
| 100 | and refused immediately on revocation; no OpenVibe session lives in the extension. |
| 101 | - Reports are never anonymous, deduplicated per (reporter, code, UTC day), only the latest per person |
| 102 | counts, and rate-limited per hour/day; the reporter is an HMAC, not a subject id. |
| 103 | - Service callers need the route's capability (`coupons.*`); staff actions are audited. |
| 104 | |
| 105 | ## Frontend loading model |
| 106 | |
| 107 | Every page is server-rendered and **useful without JavaScript**; there is no SPA, bundler or |
| 108 | transpiler. `openvibe-shared` supplies the Frame, legal/SEO and `/shared/*` from the pinned |
| 109 | node_modules; `openvibe-publishing` supplies the SSR/index helpers. Discovery (robots, sitemap, feeds, |
| 110 | `llms.txt`, IndexNow) is generated from the same data. CSP and cache policy are in |
| 111 | [server/app.js](server/app.js) and `openvibe-shared/cache-policy`. |
| 112 | |
| 113 | ## Deploy |
| 114 | |
| 115 | `sudo ovhost deploy coupons` (git-checkout: fetch, fast-forward `/opt/openvibe.coupons`, install on a |
| 116 | lockfile change, restart, wait for `/api/ready`). Unit `openvibe-coupons.service` on |
| 117 | `127.0.0.1:4850`, env `/etc/openvibe/coupons.env`; the release migrates `ov_coupons` at boot; vhost |
| 118 | [deploy/nginx/openvibe.coupons.conf](deploy/nginx/openvibe.coupons.conf). Rollback |
| 119 | `sudo ovhost rollback coupons --to <sha>`. No secret in a command line, file or log. |
| 120 | |
| 121 | ## Tests |
| 122 | |
| 123 | `npm test` runs each `test/*.test.js` in its own process with PGlite and in-process Network/Sources/ |
| 124 | Events mocks. The README's acceptance table maps each rule to its test: `lifecycle.test.js` (unknown |
| 125 | stays unknown, status fields refused from everyone, AI held for review, expiry at the instant, the |
| 126 | sweep once), `reports.test.js` (dedupe, latest-per-person, rate limits, no cookies/anonymous), |
| 127 | `confidence.test.js` (the formula), `extension-privacy.test.js` (hashing, revocation, scopes) and more. |
| 128 | `ov test` runs the same. |
| 129 | |
| 130 | ## Conventions |
| 131 | |
| 132 | - CommonJS, 4-space indent, single quotes, semicolons; domain in `server/domain/`, HTTP in |
| 133 | `server/http/`, no SQL in routes. |
| 134 | - Ids/errors/capabilities from `openvibe-contracts`; errors are problem+json with stable codes. |
| 135 | - Dates are UTC; an expiry carries its precision and basis, and an unknown one stays `null`. |
| 136 | - Escape all rendered text; no personal data in URLs, JSON-LD, feeds or the API. |
| 137 | |
| 138 | ## Pitfalls |
| 139 | |
| 140 | - **A model, a source or staff cannot mark a code working.** Only recent people's reports move the |
| 141 | status; an AI-origin code is `pending` review. Do not add an admin "set working" path. |
| 142 | - **Unknown is not zero/false.** No expiry → `null`, no reports → `confidence: null`, no stated |
| 143 | restriction → "none stated". Do not substitute defaults. |
| 144 | - **Expiry is instantaneous.** API/page/feed/sitemap/Search must agree at `t-1ms` and `t`; an expired |
| 145 | page is noindex. |
| 146 | - **The public API is cookie-blind.** Do not add session-based personalisation or per-caller bytes. |
| 147 | - **Reports are per person per code per day**, latest wins; a second report does not add a second |
| 148 | count, and no anonymous report is accepted. |
| 149 | - **Tracking/affiliate links are not introduced here**; a source's link policy is explicit, and the |
| 150 | code's evidence URL must be on the merchant's own domain where the rule says so. |
| 151 | - **The extension token is a secret**: hash at rest, never returned after creation, revocation immediate. |
| 152 | - **No Offer/validity JSON-LD** — the README deliberately omits it; do not add price/validity markup. |
| 153 | |
| 154 | ## Documentation |
| 155 | |
| 156 | - [README.md](README.md) — the rules, tables, statuses/confidence, routes, API, extension credentials, |
| 157 | events, capabilities, acceptance, security and deploy. |
| 158 | - [docs/capabilities-proposal/](docs/capabilities-proposal/), [docs/service-manifest-proposal.json](docs/service-manifest-proposal.json). |
| 159 | - OpenVibe.Contracts holds the `coupons.*` shapes; Sources owns the item contract. |
| 160 |