stdin is the script pipe when run as `curl ... | bash`, so the -t 0
check silently skipped the domain question. Read from the controlling
terminal instead; no tty (CI/cron) still defaults to localhost.
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
install.sh now asks one question on fresh installs — the URL the app is
opened on. Accepts https://primora.example.com (TLS at the user's proxy),
primora.example.com, or 192.168.1.50:8085; the port becomes NGINX_PORT.
The answer rewrites every public URL var and COOKIE_DOMAIN for real DNS
names. PUBLIC_URL env keeps it non-interactive; legacy DOMAIN/NGINX_PORT
still work; reruns over an existing .env skip the question and display
the URL from the file.
Also trust both URL schemes on the serving host in trustedOrigins —
external TLS termination makes nginx report http while browsers arrive
over https.
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
The sbom job's `permissions: packages: read` replaces the default token
set, so `gh release upload` got 403 Resource not accessible. Grant
contents:write alongside packages:read.
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Self-hosted sign-up bounced back to the bootstrap screen for three
compounding reasons:
- better-auth's client resolves { data, error } on HTTP failure; the UI
ignored res.error and showed "Account created — signing you in", hiding
the real 403/429/500.
- trustedOrigins was a static localhost-only list. Browsing the instance
via LAN IP or custom port failed every auth POST once cookies existed.
Origins are now derived from the request's public host (forwarded by
nginx), so any origin serving the app works.
- hasMailTransport threw when core.settings was unreadable (fresh volume,
backend still migrating), so sign-up 500'd and the startup backfill
could crash-loop the service. Both now degrade to "no transport".
Verified live: LAN-style origin sign-up succeeds, missing core.settings
logs a warn instead of exiting, and the frontend shows real errors.
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
anchore/sbom-action does not create the output directory — the v0.8.1
run scanned images fine (lowercase owner works) but ENOENT'd writing
dist-release/sbom-*.cdx.json.
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
SMTP-less self-hosted instances still marked every account
emailVerified=false and attempted a verification mail that could
never arrive. Now: no transport → accounts are created verified,
the send is skipped, and existing unverified rows are backfilled
on startup. .env.example stops defaulting SMTP_HOST to the
dev-only mailpit service.
Verified live: fresh signup returns emailVerified=true, legacy
unverified users flip on restart, sign-in unaffected.
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
ghcr.io rejects mixed-case repo paths — syft failed to parse
ghcr.io/Dvorinka/primora-* so the v0.8.0 SBOM job errored.
Apply the same lowercase-owner normalization the image jobs use.
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
arm64 npm ci under QEMU SIGILLs on current node:20-alpine layers
(uncaught target signal 4) and wedges the build step — the v0.8.0
release stalled there twice. Build each platform natively and join
the arch tags with a manifests job.
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
A "Set up with an AI agent" dialog that mints a write-scoped API key and
renders a copyable prompt handing a coding agent everything needed to wire
a repository into a Primora project: telemetry ingest for any language,
db-connections for tracked databases, MCP/CLI agent access, and
verification steps. Opens at the end of new-project onboarding; also
reachable from the project dashboard and Projects page settings.
Authored By: TDvorak <[email protected]>
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
- handlers: pgx.ErrNoRows→404, InputError→400 with message preserved,
pgconn 23505→409 without constraint/SQLSTATE leak, unknown→500
- services: export NewInputError so error construction is testable
- frontend: vitest for errorMessage helper — server reason preferred,
status text and fallbacks verified
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
- api-client: getResponseBody returns Blob for non-JSON (fixes storage
preview/download TypeError); postgen.mjs reapplies the patch on regen
- Modal: error prop renders inside the dialog so request failures are
visible instead of hidden behind the overlay (6 pages wired)
- lib/api: extract server-provided error message from error.body
- Modal: unique id per instance (duplicate modal-title broke aria-label)
- nginx: index.html no-cache + immutable /assets (stale shell pointed at
deleted hashed assets after redeploy → white screen)
- vite: drop dead Google Fonts runtime caching (fonts are vendored)
- functions: audit function.created/updated/deleted/invoked
- AuditPage: expand action filter to all recorded actions
- MCP: whoami uses /context for API keys; projectId/orgId resolve via
/context with auth-aware precedence (stale `primora use` selection can
never authorize a differently-scoped key)
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
- Geist/Geist Mono via @fontsource inside the Vite bundle — the
Google Fonts <link> was blocked by the nginx CSP (style-src 'self'),
so the dashboard silently fell back and phoned home on self-hosted
instances. Fonts now serve from /assets under 'self' — CSP-clean,
airgapped, no console violation.
- mcp README: vault_exec described the old deny-list; rewritten for
the allow-list contract.
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
- POST /projects/:id/events accepts client-authored events restricted to
the custom.* namespace — system types can't be spoofed. They flow
through the same publishEvent fan-out: realtime, webhooks, and
event_pattern functions all fire.
- Presence: subscribe/unsubscribe bumps a per-project count and
broadcasts presence.update {online} to the realtime stream only —
deliberately not through publishEvent so webhooks and "*" functions
don't fire on connect noise. GET …/realtime/presence reads the count.
- @primora/client gains publish() and presence(); CLI gains events:send.
Verified live: presence 0->1 on subscribe, presence.update frame
delivered, custom event delivered to SSE + fired a custom.* function,
spoofed document.created rejected 400.
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Each invoke runs in an ephemeral locked-down container — no network,
read-only rootfs, 128MiB/0.5CPU/64-pid caps, non-root, cap-drop ALL.
Source travels as base64 in the container env and materializes in the
tmpfs, so no bind mount is needed (Docker Desktop / remote daemons work).
Timeout force-removes the named container.
exec driver stays the default; the trust-model docs now describe both.
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
- tauri-plugin-updater + process: pubkey embedded in tauri.conf.json,
createUpdaterArtifacts, endpoint = GitHub latest.json manifest.
- Connect screen checks before auto-connect (bounded 5s) — a saved
URL would otherwise navigate away before the check resolves.
"Update & restart" downloads, verifies minisign, installs, relaunches.
- Release workflow signs updater artifacts with
TAURI_SIGNING_PRIVATE_KEY secrets, ships the .sig files, and assembles
latest.json from them. No key → no manifest → no update, never a
silent unsigned install.
- anchore/sbom-action emits CycloneDX SBOMs per GHCR image plus the
source tree, attached to every release.
Note: the signing keypair was generated locally
(~/.tauri/primora-updater.key); the private half must be added to repo
secrets before the next release for updates to activate.
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
POST /buckets/:id/object-presigns mints SigV4 query-signed GET/PUT
URLs so clients move bytes directly against S3 — large transfers skip
the backend entirely.
- storage.Presigner optional interface; S3Store implements, local
driver returns a clear error (no external surface to sign).
- TTL 60s–1h (default 15m); download requires read role + existing
object row (no key probing); upload requires write role.
- S3_PUBLIC_ENDPOINT rewrites the client-facing host for internal
S3_ENDPOINTs — signatures bind the host, URLs are valid only where
minted.
- CLI: primora objects:presign <bucket> <key> [--upload] [--ttl].
Verified live against cloudserver: GET fetched the object with no
auth headers; PUT pushed 1 MB direct to S3; unsigned list → 403;
untracked-key download presign → 404.
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Migration 00016: function_id on scheduled_jobs/inbound_hooks,
event_pattern on functions, 'event' in the run-trigger enum.
- Jobs deliver to a function instead of HTTP when function_id is set;
the run's status_code mirrors the process exit code. Clearing the
field returns the job to URL delivery.
- Inbound hooks gain mode "function" — received body becomes stdin
payload, acked immediately, run recorded async.
- event_pattern (document.*, *, exact type) fires the function detached
on matching publishEvent — never blocks realtime/webhook fan-out.
- One executeFunction path serves manual/schedule/hook/event; run rows
record the trigger source.
- Backend image now ships bun 1.3.14 so Functions work under compose.
Dashboard: job target picker, hook function mode, event-pattern field.
Verified live: job run → success (status_code 0), hook POST → run,
document.created → event run with correct payload.
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Inside the frontend and nginx containers, localhost resolves to ::1
only while their nginx binds IPv4 — the wget healthcheck was refused
and both containers reported unhealthy despite serving fine.
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
core.functions + core.function_runs: project-scoped code executed by a
runtime binary on the backend host (bun or deno per function). Payload
arrives on stdin + PRIMORA_PAYLOAD env; stdout/stderr captured per run
(capped), with status/exit code/duration. Invoke returns 502 + the
recorded run on failure so errors stay observable.
Dashboard gains a Functions page: code editor, JSON payload invoke,
live result, run history. Runner config via FUNCTIONS_RUNTIME /
FUNCTIONS_TIMEOUT_SECONDS / FUNCTIONS_MAX_OUTPUT_BYTES.
Verified live: bun ran code, stdin payload delivered, error path returns
502 with captured stderr; timeout kills at deadline.
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
BACKEND_STORAGE_DRIVER=s3 swaps LocalStore for a stdlib-only SigV4
implementation — PutObject/GetObject/DeleteObject/CopyObject/ListObjectsV2
against any S3-compatible API (AWS, MinIO, Garage, R2). Storage becomes a
Store interface; storage_path records s3://bucket/key. Content-Length is
resolved by seeking or temp-file spooling — chunked uploads are rejected
by real S3 servers. Bucket auto-created on startup.
Verified live against zenko/cloudserver: bucket, put, get, copy, delete.
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
HA scheduler: the job scheduler and alert evaluator hold session-scoped
PostgreSQL advisory locks on pinned pool connections — replicas scan for
due work exactly once, failover verified live with two replicas. Manual
and hook-triggered runs stay available on any replica.
Email surface: mailer renders registered templates (invitation); every
send is logged to core.email_log with status and error text. New
GET /projects/:id/emails endpoint; Members page shows an Email log card.
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
core.inbound_hooks (migration 00013): POST /api/v1/hooks/<token> accepts
external events. event mode republishes as inbound.received through the
signed dispatcher + realtime; job mode triggers a scheduled-job run with
the received body as payload. Optional X-Primora-Signature HMAC verify.
Dashboard card on the Integrations page.
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
core.alert_rules (migration 00012) — heartbeat_silence and error_spike
rules evaluated by a 60s backend ticker. Transition-based firing:
alert.fired on breach, alert.resolved on recovery, both first-class
webhook event types riding the signed dispatcher + realtime stream.
Dashboard manages rules on the Integrations page.
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
createRealtimeClient opens the project SSE stream via fetch +
ReadableStream (EventSource cannot send X-API-Key), with capped
exponential-backoff reconnect and state callbacks. channel() filters by
event namespace — channel("documents").on("created", cb). Presence and
client-broadcast deliberately deferred.
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
GET /collections/:id/documents accepts filter (field.op.value terms; eq neq
gt gte lt lte like in is; dotted paths index into document data) and order
(field.asc|desc). Whitelisted field/operator parsing with parameterized
values; plain lists keep the sqlc path. CLI gains `documents list
--collection --filter --order`; OpenAPI + generated client updated.
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>