Audit of staging + production PocketBase rules turned up two open holes:
- users.createRule = "" allowed any anonymous client to POST to
/api/collections/users/records and create a real account. The
dashboard exposes no signup UX — operators authenticate via the
superuser credentials in PbAuthPrompt — so create should be
admin-only.
- baseline.updateRule = "" (production only) allowed any anonymous
client to PATCH baseline rows, including silently flipping status
cells and overwriting the updated_by/updated_at audit fields. The
dashboard's baseline edit flow already gates writes behind
PbAuthPrompt, so locking updateRule to admin-only keeps the existing
operator workflow intact while removing the open hole.
Both changes were applied via the PocketBase admin API to staging first
(verified dashboard still renders and live status SSE still flows),
then to production (same verification, plus 403 confirmations on the
anonymous signup + anonymous baseline PATCH requests that previously
returned 200).
All other collection rules already had the right shape: status,
status_history, probe_runs, baseline retain listRule/viewRule = ""
because the dashboard reads them unauthenticated via the PocketBase JS
SDK; create/update/delete rules stayed null because the harness writes
with the superuser JWT.
GHCR auto-links container packages to their source repository when the
image includes org.opencontainers.image.source. Without this label the
package's repository field stays null on the API, which triggers the
daily drift audit alert even though Actions access is configured.
R2-A.11: two robustness improvements to 1777165230_create_probe_runs.js:
- CREATE INDEX statements now use IF NOT EXISTS so a partial-apply
doesn't trip the next migration run on the index DDL step. The
collection-presence gate above already covers saveCollection
idempotency; this covers the per-index path in case PB ever runs
index DDL separately from the schema commit.
- Document why the up-migration's findCollectionByNameOrId catch must
remain broad: PB JSVM does not expose typed error discrimination
(no ErrCollectionNotFound), so we cannot narrow without a runtime
feature change. The down-migration's catch is already narrowed
because it operates on a resolved-or-skip path.
CR-A1.6: four related tightenings to the probe_runs collection migration.
- Down-migration's catch now wraps only findCollectionByNameOrId, not
deleteCollection. A real delete failure (FK constraint, permission,
etc.) must propagate so the migration framework can roll back.
Swallowing both calls would have left a half-deleted collection live
in PB while looking like a clean down-migration.
- triggered field is now required:true. The writer always sets it
explicitly (running rows pass true|false), so the schema should match
the contract and fail loud at insert time on a forgetful caller.
- duration_ms field gains options.min:0 to reject negative durations
from clock skew at the storage layer.
- summary maxSize tightened from 2MB to 64KB. The shape is
{total, passed, failed, services?} — well under 64KB. The 2MB
ceiling was an exfiltration sink given the public listRule below;
budget now matches realistic max.
Per-invocation probe run history. One row per probe tick, distinct from
status/status_history (per-result state machine) — captures run-level
metadata (duration, triggered-vs-scheduled, pass/fail counts) for the
dashboard's "last N runs" widget consumed by B3 (status route) and B7
(probe-invoker hook).
PB migration creates the `probe_runs` collection with public-read /
superuser-write rules, mirroring `status` / `status_history`. Two
indexes: composite (probe_id, started_at DESC) for last-N lookups and
standalone (started_at DESC) for retention sweeps. Up/down migrations
are idempotent (no-op on re-apply, best-effort drop on rollback).
run-history.ts exports PROBE_RUNS_COLLECTION constant + ProbeRunWriter
with start/finish/recent. duration_ms is computed off the persisted
started_at (not a caller-supplied param) so the contract holds even if
clocks drift between start and finish. Filter values are JSON.stringify'd
to defend against probe ids containing PB filter metacharacters.
Tests use the same fakePb pattern as status-writer.test.ts; coverage
includes start state shape, finish state transitions (completed/failed),
duration computation off persisted row, recent() filter+sort+limit, and
empty-result handling.
PocketBase backend for showcase-ops: Dockerfile + entrypoint for the
Railway-hosted instance, JSVM main.pb.js hook for CORS + request
shaping, migration sequence creating status / status_history /
alert_state collections, CORS config, and the recreate_collections
v1/v2 + drop_history_fail_count schema drift corrections.