15c72ade1c84f2de7ad17e0b4f98912a25818c83
4 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
13dec6b4b8 |
Rebuild the shell, add Calendar and Learn, and govern reads
Seven parallel agents and an adversarial verification pass. The three things worth knowing before reading the diff: RBAC WAS ALREADY BUILT. docs/build-plan.md marks F2 and F3 outstanding and is stale — packages/core/src/permissions.ts and lib/mutation.ts shipped long ago. So this does not rebuild them; it closes the gaps an audit found. The big one is that reads were entirely ungoverned: every GET was "any authenticated member", so a junior demand rep and a research contractor could both pull per-block supplier cost and break-even prices from /api/capacity/margin, and every contract's negotiated terms. For a company whose margin is the business, that was the hole that mattered. Adds book:read / economics:read / team:read, a readGuard middleware, and a `viewer` role below member. THE BUTTON AND THE 403 DISAGREED — the exact thing F3 said must never happen. Contracts.tsx never called can() at all, so its save button was always enabled against a server requiring contract:sign; Capacity.tsx gated commitment creation on deal:write/demand while the server wanted commitment:write/supply. POST /api/activities was the one write bypassing executeMutation: no capability check, and any member could mutate accounts.lastActivityAt as a side effect. It is now a proper mutation() behind activity:write. The shell becomes three panes — a collapsible shadcn sidebar with an account switcher on the Piggy accent, a header with real search, and Piggy docked to the right, page-aware and persistent across navigation. The phone keeps its bottom tab bar, which is the thing this product already beat trycompai/crm on, and gains the sidebar as a sheet. Calendar is a projection over thirteen dated sources rather than a new table, because a table would duplicate dates that already live on contracts, deals and commitments and would drift — and one ledger answering the question is the whole argument. It surfaces export_authorizations and compliance_artifacts, which had indexed expires_at columns, schema comments saying they must be alerted on, and no read endpoint or UI anywhere. Learn carries two tracks. Concepts are members-only; the platform track can be opened with a share code by someone with no account. The code mints a scoped learn-only token and never a Principal — every route here resolves a principal and then checks capabilities, so a principal-minting code would be one missing check away from leaking the book. "Only platform-track rows may be code-visible" is a database CHECK constraint as well as a write-path rule, and a test asserts a valid learn token still gets 401 on /api/dashboard, /api/accounts and /api/contracts — the same invariant scripts/deploy.sh refuses to ship without. CD becomes tag-to-ship. CI publishes an image to the Gitea registry on a release-* tag and cloud-2 pulls it, so no credential on the shared runner can execute anything on production — by construction rather than by policy. Both halves of deploy.sh's original rule survive: nothing on the runner reaches the host, and a human still decides when it ships. deploy.sh gains a rollback and a public-origin check, and PIG_IMAGE now reaches compose through `sudo env`, without which sudo's env_reset silently resolved every release to pig:local. Tests 141 -> 261. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
6cf80747cc |
Keep PIG out of search results until it is meant to be found
CI / verify (push) Successful in 3m11s
The app is pre-launch and shared by link with a handful of people at Prime
Intellect. It should not be accumulating a search footprint yet.
Three layers, because each covers a gap the others leave:
- robots.txt asks well-behaved crawlers not to fetch at all.
- The <meta name="robots"> tag covers the HTML document for anything that
fetched anyway.
- X-Robots-Tag covers everything that is NOT the HTML document — og.png,
the manifest, the built assets — which the meta tag cannot reach.
noarchive and nosnippet are there so a cache or an excerpt cannot outlive
the page once this is reversed.
Deliberately NOT stripped: the og:/twitter: tags. Link unfurlers are not
crawlers — they fetch on behalf of the person pasting the link, and a
rendered card is exactly what we want when this is shared.
The real gate remains authentication: / returns the sign-in screen and every
/api/ route returns 401. This only stops the app being indexed.
To go public: delete robots.txt, drop the meta tag, drop the header.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
93818a2d2c |
Document two proxy traps found by deploying rather than assuming
The deployment came up healthy, served a valid certificate, and returned 200 — and was completely unreachable. Two distinct causes, both invisible from inside the host: 1. Every other site on this proxy binds to a private VNIC address. Caddy groups site blocks into servers BY listen address, so a block without `bind` landed in a separate server on :443. The specific listener wins for traffic arriving on that address, which is all public traffic after NAT, so requests hit the server that had never heard of these hostnames and fell through to an empty 200. Testing from the host with --resolve 127.0.0.1 worked perfectly, which is exactly why this was worth chasing from a third machine instead of trusting a local check. 2. The CSP blocked the inline pre-paint theme script, so dark-mode users would have seen a white flash on every load. Fixed with the script's hash rather than 'unsafe-inline', which would have defeated the policy, and rather than an external file, which would have reintroduced the flash. Editing that script changes its hash and silently breaks it, so that is written down. Verified from an independent host: health returns JSON, the app serves, an unauthenticated API call is refused, the short alias redirects, security headers are present, and the existing sites on the proxy are unaffected. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
c747eb2aa7 |
Add deployment: Dockerfile, compose, proxy config, and docs
One container plus a Postgres behind any TLS-terminating proxy. Nothing is specific to a particular host. The app and API are served from a SINGLE origin. This is not tidiness: browser auth sessions live in per-origin storage, so splitting them across two hostnames makes sign-in loop in a way that presents as a server fault. The short alias redirects rather than serving a second origin. Two safety properties verified by running the image, not by reading the code: - With NODE_ENV=production and no SUPABASE_URL, the process refuses to start and says why. Serving the whole CRM unauthenticated is a worse outcome than failing to deploy, so the failure is deliberate and loud. - In production the development auth bypass does not apply: an unauthenticated request to /api/dashboard returns 401 rather than adopting the first user in the table. The Dockerfile typechecks all six packages as a build gate, so a deploy that does not compile fails at build time rather than in front of a user. Runtime runs unprivileged as `node`, and Postgres is not published to the host. Docs cover the ontology and why it is shaped this way, agent connection for Claude Code / Codex / prime-agent / Buzz, and the provenance rules governing seed data about real people — including how to have your record removed. Verified: image builds, container reports healthy, serves the SPA, enforces auth, and the production guard exits non-zero. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |