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>
93 lines
2.6 KiB
JavaScript
93 lines
2.6 KiB
JavaScript
module.exports = {
|
|
apps: [
|
|
{
|
|
name: 'officer',
|
|
script: 'bun',
|
|
args: 'start',
|
|
watch: false,
|
|
},
|
|
// The Anthropic credential proxy. Despite the old name (`officer-claude`) this process does NOT
|
|
// run agents — it holds the proxy secret and forwards to api.anthropic.com. The process that runs
|
|
// agents is `officer-agent` below.
|
|
{
|
|
name: 'officer-anthropic-proxy',
|
|
script: 'bun',
|
|
args: 'run src/servers/sidecar/claude/index.ts',
|
|
watch: false,
|
|
},
|
|
// The process that actually runs `claude`. It used to be spawned on demand by the main server,
|
|
// which made every agent session a grandchild of `officer` and killed it on every restart. As a PM2
|
|
// peer it survives them. It resolves the owner from the database and the proxy secret from the
|
|
// proxy's state file, so it needs nothing from `officer` in order to start.
|
|
{
|
|
name: 'officer-agent',
|
|
script: 'bun',
|
|
args: 'run src/servers/sidecar/claude/user-instance.ts',
|
|
watch: false,
|
|
},
|
|
{
|
|
name: 'officer-opencode',
|
|
script: 'bun',
|
|
args: 'run src/servers/sidecar/opencode/index.ts',
|
|
watch: false,
|
|
},
|
|
{
|
|
name: 'officer-email',
|
|
script: 'bun',
|
|
args: 'run src/servers/sidecar/email/index.ts',
|
|
watch: false,
|
|
},
|
|
// The only sidecar run by `node` rather than `bun`, and the only one that is not TypeScript: node-pty
|
|
// is a native addon. It also does not use sidecar/connect.ts, and carries its own copy of the
|
|
// reconnect loop.
|
|
{
|
|
name: 'officer-pty',
|
|
script: 'node',
|
|
args: 'src/servers/sidecar/pty/index.mjs',
|
|
watch: false,
|
|
},
|
|
{
|
|
name: 'officer-vnc',
|
|
script: 'bun',
|
|
args: 'run src/servers/sidecar/vnc/index.ts',
|
|
watch: false,
|
|
},
|
|
{
|
|
name: 'officer-music',
|
|
script: 'bun',
|
|
args: 'run src/servers/sidecar/music/index.ts',
|
|
watch: false,
|
|
},
|
|
{
|
|
name: 'officer-vault',
|
|
script: 'bun',
|
|
args: 'run src/servers/sidecar/vault/index.ts',
|
|
watch: false,
|
|
},
|
|
{
|
|
name: 'officer-slskd',
|
|
script: 'bun',
|
|
args: 'run src/servers/sidecar/slskd/index.ts',
|
|
watch: false,
|
|
},
|
|
{
|
|
name: 'officer-headscale',
|
|
script: 'bun',
|
|
args: 'run src/servers/sidecar/headscale/index.ts',
|
|
watch: false,
|
|
},
|
|
{
|
|
name: 'officer-transmission',
|
|
script: 'bun',
|
|
args: 'run src/servers/sidecar/transmission/index.ts',
|
|
watch: false,
|
|
},
|
|
{
|
|
name: 'officer-invoiceshelf',
|
|
script: 'bun',
|
|
args: 'run src/servers/sidecar/invoiceshelf/index.ts',
|
|
watch: false,
|
|
},
|
|
],
|
|
};
|