add officer-invoiceshelf sidecar

owns the invoiceshelf contract: instance url, sanctum token and the company
header that scopes every request. the platform side is the usual thin
auth+forward proxy at /api/invoiceshelf and holds no credentials.

built against the live 2.4.2 instance rather than the 3.0.0-alpha.1 checkout
in _references — the route allow-list came from artisan route:list on the
running container. they differ: 2.4.2 has estimates/{id}/convert-to-invoice
but no invoices/{id}/convert-to-estimate.

three upstream quirks absorbed here:
- accept: application/json is mandatory, or an unauthenticated request 302s
  to an html login instead of returning 401
- origin/referer must never be sent, or statefulapi() switches to session+csrf
  and every request 419s. the proxy forwards neither.
- a wrong company header does not error, it silently returns another company's
  data. the pinned company is explicit and logged.

document pdfs are repaired: 2.4.2 prefixes them with a literal serialised http
response (201 bytes) inside a body already typed application/pdf. we slice to
the %PDF- magic. the report routes don't have the bug.

resources are an allow-list. backups, disks, modules, update/*, installation/*,
mail config, settings writes and ownership transfer stay unreachable, and the
per-resource action list keeps `send` — which really emails the customer —
from being reachable by accident.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-30 20:34:55 +00:00
co-authored by Claude Opus 5
parent f98ebe509f
commit b6dc73915d
10 changed files with 699 additions and 1 deletions
+3
View File
@@ -24,6 +24,7 @@ import { vaultRouter } from './api/vault/router';
import { slskdRouter } from './api/slskd/router';
import { headscaleRouter } from './api/headscale/router';
import { transmissionRouter } from './api/transmission/router';
import { invoiceshelfRouter } from './api/invoiceshelf/router';
import { vpnRouter } from './api/vpn/router';
import { systemMonitorRouter } from './api/system-monitor/system-monitor';
import { activityRouter } from './api/activity/router';
@@ -32,6 +33,7 @@ import './api/vault/sidecar-server'; // side-effect: capture the officer-vault r
import './api/slskd/sidecar-server'; // side-effect: capture the officer-slskd reverse-proxy port
import './api/headscale/sidecar-server'; // side-effect: capture the officer-headscale server port
import './api/transmission/sidecar-server'; // side-effect: capture the officer-transmission server port
import './api/invoiceshelf/sidecar-server'; // side-effect: capture the officer-invoiceshelf server port
import { devServerRouter, devServerProxyRouter } from './api/dev-server/router';
import { dockRouter } from './api/dock/dock';
import { integrationsRouter, googleCallbackHandler } from './api/integrations/integrations';
@@ -116,6 +118,7 @@ protectedRouter.route('/music', musicRouter);
protectedRouter.route('/slskd', slskdRouter);
protectedRouter.route('/headscale', headscaleRouter);
protectedRouter.route('/transmission', transmissionRouter);
protectedRouter.route('/invoiceshelf', invoiceshelfRouter);
protectedRouter.route('/vpn', vpnRouter);
protectedRouter.route('/system-monitor', systemMonitorRouter);
protectedRouter.route('/activity', activityRouter);