openvibers/openvibe.work AGENTS.md
AGENTS.md
| 1 | # AGENTS.md — OpenVibe.Work |
| 2 | |
| 3 | ## Project overview |
| 4 | |
| 5 | OpenVibe.Work (`https://openvibe.work`) is the network's job-listings service: it gathers listings from |
| 6 | public job boards, keeps provenance and a short plain-text excerpt, makes them searchable without an |
| 7 | account, and lets a signed-in person save searches, track applications and ask their OpenVibe.Actor |
| 8 | agent for help preparing one. It does not republish a board's listing: every listing links back to the |
| 9 | board, 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 | |
| 26 | Owns its PostgreSQL tables and nothing else: `work_listings` (upserted on `(source, source_id)`, the |
| 27 | board'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 |
| 30 | listing), `work_application_events` (append-only history), `account_data_events` and |
| 31 | `work_ingest_cursor`. |
| 32 | |
| 33 | Does not own: identity/accounts (Network — OAuth client `work`, PKCE S256, JWKS, canonical subject); |
| 34 | event 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 | |
| 39 | Anyone can search and read the public API (`/api/v1/ping`, `/jobs`, `/jobs/:id`, `/sources`) and the |
| 40 | Atom feed (`/jobs.atom`); a signed-in person uses Network SSO for saved searches, applications and the |
| 41 | Actor assist. SDK `openvibe-sdk/work` is the public client (`jobs.search`, `jobs.get`, `sources`, and |
| 42 | the person's `savedSearches`/`applications`). Work calls Network (SSO/JWKS), Events (subscriptions and |
| 43 | the outbox), the board APIs (only configured base URLs, this service's User-Agent, a timeout and a size |
| 44 | cap, storing only http(s) listing URLs) and OpenVibe.Actor (help prepare an application from the |
| 45 | person's own agent). "Get alerts" hands a saved search to OpenVibe.Watch's feed form; Watch owns the |
| 46 | watching. |
| 47 | |
| 48 | ## Database and migrations |
| 49 | |
| 50 | PostgreSQL `ov_work` through `openvibe-sdk/db` (PgBouncer for the app, direct owner role for |
| 51 | migrations): `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 |
| 53 | and the outbox rejection index. Applied at boot; never edit a migration that has run. The database is |
| 54 | the only required readiness check; Network/JWKS/Valkey are optional and reported per check. |
| 55 | |
| 56 | ## Auth and exposure |
| 57 | |
| 58 | Session tokens are httpOnly cookies; a FedCM assertion or an app/service token is never a session. |
| 59 | Public 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 |
| 61 | loopback-only; account export/deletion covers saved searches and applications, and a redelivery changes |
| 62 | nothing. 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 |
| 69 | will ever arrive.** Rollback with `sudo ovhost rollback work --to <sha>`. |
| 70 | |
| 71 | ## Commands and tests |
| 72 | |
| 73 | ```bash |
| 74 | npm ci |
| 75 | npm run dev # http://127.0.0.1:4960 |
| 76 | npm test # every test/*.test.js, temp PGlite + mock Network |
| 77 | npm test -- ingest login # only matching files |
| 78 | npm run test:pg # the same on PostgreSQL |
| 79 | npm 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 |
| 83 | network. Coverage includes the board adapters and terms, the Hacker News line split/rederive, search |
| 84 | and facets, saved searches, applications and their history, Actor assist, auth/JWKS/limits, discovery, |
| 85 | account 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 |