openvibers/openvibe.work AGENTS.md

AGENTS.md

main 6.2 KB · 103 lines · markdown

1# AGENTS.md — OpenVibe.Work
2
3## Project overview
4
5OpenVibe.Work (`https://openvibe.work`) is the network's job-listings service: it gathers listings from
6public job boards, keeps provenance and a short plain-text excerpt, makes them searchable without an
7account, and lets a signed-in person save searches, track applications and ask their OpenVibe.Actor
8agent for help preparing one. It does not republish a board's listing: every listing links back to the
9board, where the reader applies.
10
11- **Entry:** `server/index.js`; app `server/app.js`; port `4960`, service id `work`, env prefix `WORK`.
12- **API/pages:** `server/http/` — `api.js` (`/api/v1`), `pages.js`, `discovery.js`, `principal.js`,
13 `caller-limits.js`, `router.js`; rendering `server/render/` (`listing.js`, `markdown.js`,
14 `layout.js`, `html.js`, `short.js`); CSS in `public/css`.
15- **Ingest:** `server/ingest/` (`index.js`, `from-sources.js`) and `server/jobs/` (`sources.js`,
16 `store.js`, `text.js`, `hiring-line.js`, `job-types.js`, `places.js`, `companies.js`,
17 `rederive-hn-titles.js`); run it with `npm run ingest` or the in-process interval.
18- **Sources:** Arbeitnow, We Work Remotely, RemoteOK and Hacker News "Who is hiring" arrive through
19 OpenVibe.Sources (`server/ingest/from-sources.js`); Remotive is the one board Work still fetches
20 itself (its robots.txt disallows `/api/*`, its API terms grant the named, linked use). Every
21 board's terms are applied on every page (`/sources`, `GET /api/v1/sources`).
22- **Docs/config:** README "Data sources and their terms", `SECURITY.md`, `.env.example`.
23
24## Owns, does not own
25
26Owns its PostgreSQL tables and nothing else: `work_listings` (upserted on `(source, source_id)`, the
27board's id, original `url`, its fields, a short plain-text excerpt; expired listings kept but hidden),
28`work_saved_searches` (`subject = user:usr_…`, last looked), `work_source_fetches`,
29`work_applications` (`subject_id`, status/notes/dates/next step/last Actor task, one per person per
30listing), `work_application_events` (append-only history), `account_data_events` and
31`work_ingest_cursor`.
32
33Does not own: identity/accounts (Network — OAuth client `work`, PKCE S256, JWKS, canonical subject);
34event delivery (Events — Work creates its two account subscriptions at boot); the listings themselves
35(the boards; Work keeps an excerpt and a link back, and reads each board only on its terms).
36
37## Who calls it, what it calls
38
39Anyone can search and read the public API (`/api/v1/ping`, `/jobs`, `/jobs/:id`, `/sources`) and the
40Atom feed (`/jobs.atom`); a signed-in person uses Network SSO for saved searches, applications and the
41Actor assist. SDK `openvibe-sdk/work` is the public client (`jobs.search`, `jobs.get`, `sources`, and
42the person's `savedSearches`/`applications`). Work calls Network (SSO/JWKS), Events (subscriptions and
43the outbox), the board APIs (only configured base URLs, this service's User-Agent, a timeout and a size
44cap, storing only http(s) listing URLs) and OpenVibe.Actor (help prepare an application from the
45person's own agent). "Get alerts" hands a saved search to OpenVibe.Watch's feed form; Watch owns the
46watching.
47
48## Database and migrations
49
50PostgreSQL `ov_work` through `openvibe-sdk/db` (PgBouncer for the app, direct owner role for
51migrations): `0001` creates nothing, `0002` listings/saved searches/fetches, `0003` account data,
52`0004` the search index, `0005` applications, plus telemetry, ingest cursor, company slug index, config
53and the outbox rejection index. Applied at boot; never edit a migration that has run. The database is
54the only required readiness check; Network/JWKS/Valkey are optional and reported per check.
55
56## Auth and exposure
57
58Session tokens are httpOnly cookies; a FedCM assertion or an app/service token is never a session.
59Public pages/API are anonymous and cacheable; saved searches and applications are the person's own
60(`user:usr_…`), and an application is one per person per listing. `/internal/events` is signed and
61loopback-only; account export/deletion covers saved searches and applications, and a redelivery changes
62nothing. Request bodies are never logged; secrets live only in the env file.
63
64## Deploy
65
66`sudo ovhost deploy work`: checkout `/opt/openvibe.work`, unit `openvibe-work.service` on
67`127.0.0.1:4960`, env `/etc/openvibe/work.env`, database `ov_work`; nginx
68`deploy/nginx/openvibe.work.conf`. **Set `WORK_INGEST=on` in `/etc/openvibe/work.env` or no listing
69will ever arrive.** Rollback with `sudo ovhost rollback work --to <sha>`.
70
71## Commands and tests
72
73```bash
74npm ci
75npm run dev # http://127.0.0.1:4960
76npm test # every test/*.test.js, temp PGlite + mock Network
77npm test -- ingest login # only matching files
78npm run test:pg # the same on PostgreSQL
79npm run ingest # fetch the boards once (needs WORK_INGEST=on)
80```
81
82`test/run.js` uses `openvibe-shared/test-runner`; temp PGlite and an in-process Network mock, never the
83network. Coverage includes the board adapters and terms, the Hacker News line split/rederive, search
84and facets, saved searches, applications and their history, Actor assist, auth/JWKS/limits, discovery,
85account data and the pages.
86
87## Conventions and pitfalls
88
89- CommonJS, 4-space indent, single quotes, semicolons; problem+json errors; escaped HTML, and a listing's
90 excerpt is plain text (never Markdown/HTML from the board).
91- **Never republish a board's listing:** keep a short excerpt and link back to the original; name the
92 source and apply its terms on every page. A board that asks to be named or linked must stay named.
93- **The ingest only fetches configured base URLs**, never a URL chosen by a request, with this
94 service's User-Agent, a timeout and a size cap; only http(s) listing URLs are stored.
95- **Ingest is off unless `WORK_INGEST=on`** — a dev/test run must never call a board; the test suite
96 runs with it off.
97- **A saved search is keyed by the canonical subject**, and "new since you last looked" is computed
98 from that row, not from a client-supplied timestamp.
99- **One application per person per listing**, with an append-only event history; a status change is a
100 new event, not an overwritten field.
101- **Work does not watch:** "Get alerts" opens Watch with the Atom feed; only Watch runs the check and
102 the action.
103