openvibers/openvibe.coupons AGENTS.md

AGENTS.md

main 9.5 KB · 160 lines · markdown

1# AGENTS.md — OpenVibe.Coupons
2
3## Project overview
4
5Coupon codes with merchant matching, restrictions, expiry and real-people validity reports, plus the
6public API the OpenVibe browser helper uses. Distinct from Deals even where they cross-link. Node.js/
7Express, 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
151. **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.
172. **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".
193. **Expired codes leave active results predictably** — at the instant their known expiry passes, in
20 the API, pages, feeds, sitemaps and Search.
214. **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
26The 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
40Plus `extension_installs` (browser-helper tokens, hashes only), the Sources importer state/holds, the
41SDK outbox and the publishing index sequencer.
42
43Not 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
77npm ci
78npm run dev # NODE_ENV=development node server/index.js
79npm start # node server/index.js, http://127.0.0.1:4850
80npm test # test/run.js, PGlite, mock Network/Sources/Events
81npm run test:pg # same on PostgreSQL (COUPONS_TEST_STORE=pg)
82npm test -- lifecycle # filter by file name; --strict makes a skip fail
83ov 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,
90add 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
92and 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
107Every page is server-rendered and **useful without JavaScript**; there is no SPA, bundler or
108transpiler. `openvibe-shared` supplies the Frame, legal/SEO and `/shared/*` from the pinned
109node_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
116lockfile 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/
124Events mocks. The README's acceptance table maps each rule to its test: `lifecycle.test.js` (unknown
125stays unknown, status fields refused from everyone, AI held for review, expiry at the instant, the
126sweep 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