v0.16.0
π Highlights
π A Harbor robot secret leak, closed at the source
Linking a project to a registry mints a Harbor robot account and saves its secret on registryProject.accountSecret β a column marked select: false. That marker was never enforced on the event pipeline: BaseSubscriber published event.entity verbatim, so the secret rode two sinks on every link β the realtime socket into the /resources rooms (joinable with REGISTRY_PROJECT_MANAGE, a lower bar than the REGISTRY_MANAGE the HTTP field read demands), and a persisted events row any EVENT_READ holder in any realm could list, because EventService.getMany applies no realm scoping.
BaseSubscriber now strips every select: false column before an entity reaches an event payload, derived from entity metadata so the select: false someone already wrote stays the one declaration:
// packages/server-db-kit/src/subscriber/base.ts
const hidden = metadata.columns.filter((column) => !column.isSelect);
// ...
for (const column of hidden) {
delete output[column.propertyPath.split('.')[0]];
}A key denylist (/(password|secret|hash|token|credential)/i) plus a fail-closed shape guard on POST /events close the one path the metadata strip cannot reach.
A second, smaller defect in the same system: a validator that threw on a client-controlled string β an IPv6 request IP, an oversized user agent, path or actor name β discarded the whole audit row, not just the field. Any IPv6 client behind the documented Nginx proxy erased every event it triggered; a forged X-Forwarded-For did it deliberately. Bounded fields now degrade instead:
// packages/telemetry-kit/src/domains/event/validator.ts
// Degrade the field, never the record β the same reason `requestIpAddress` carries `.catch(null)`.
function truncated(max: number) {
return zod.string().nullable()
.transform((value) => (value ? value.slice(0, max) : value));
}A related consistency fix landed the same day: publishing expiring: true with no expiresAt while EVENT_RETENTION_DAYS=0 used to save a row the retention sweep could never match; it now normalizes to expiring: false.
Warning
Rotate every Harbor robot account created or rotated by an earlier 0.x release. Bridge events carry a one-week TTL and self-clear, but a deployment with longer retention still holds them.
π Telemetry β Environment Variables
β‘ Every queryable field is now backed by a real index
All 15 rapiq entity schemas across server-core, server-storage and server-telemetry now declare their query surface as indexes β property-name sequences, each a leftmost prefix of a real primary key, unique constraint or entity index β and opt into rapiq's indexed enforcement. A filter has to anchor on an indexed column; a sort has to be a prefix of a declared sequence:
A CI drift gate (npm run test:schema-drift in every server app) now compares the migration chain against the entity metadata directly, so a migration that writes something the entities don't describe β or an entity change with no migration β fails instead of surfacing only as migration generate noise later. That gap is exactly what let two table renames leave seven analysis_entity-derived constraint names stranded (#1823); a new migration realigns them.
Getting there removed three query keys nothing could safely index: telemetry event filters on createdAt/updatedAt (they sit behind a read-side transformer that never applies to WHERE binds, so they silently matched the wrong rows) and the analysis list's description filter (an unindexable text column, and nothing in the UI ever sent it). It also fixed a real bug along the way β masterImage's derived sort allow-list had silently dropped the virtualPath sort the UI already requested, and masterImageGroup rejected every sort key it was sent.
Note
Adding a filterable or sortable key from here on requires its backing entity index and migration in the same change β schema-entity-parity.spec.ts fails otherwise.
π API Reference β Query Capability Discovery
π Detail pages and lists stop fetching everything twice
client-ui runs with ssr: true, but nothing fetched during the server render used to reach the browser: nine detail pages issued their request a second time on hydration, and every list's initial load resolved in a detached microtask after the HTML was already flushed β the server paid for the request and still shipped an empty list.
The handoff now rides @authup/client-web-kit's hydration store, already installed via @authup/client-web-nuxt and backed by nuxtApp.payload.data. A hydrating client adopts the server's snapshot and skips its own request entirely:
// apps/client-ui/pages/admin/events/[id].vue
const entity = await useEntityRecord<Event>(
`event:${route.params.id}`,
() => httpClient.event.getOne(route.params.id as string).then((r) => r.data),
'/admin/events',
);Note
List rows now render server-side for the first time, which puts <VCTimeago> on both sides of the hydration boundary. A relative-time string that crosses a bucket boundary between render and hydration is a harmless text mismatch β Vue repairs it β but it does show up as console.error noise on list pages in production builds.
π Frontend β Server Rendering and Hydration
π Bucket downloads now proxy through the UI's own origin
A file download is a top-level browser navigation, so it can never carry an Authorization header β server-storage falls back to the access_token cookie, which is host-only. Reaching a separately-hosted storage service therefore meant widening NUXT_PUBLIC_COOKIE_DOMAIN, and a wide cookie domain also reaches Authup's own hosted pages, which write a same-named cookie.
Downloads now route through a same-origin proxy on the UI server instead, so the existing host-only cookie is sent without widening anything:
// apps/client-ui/server/api/download/[type]/[id].get.ts
return proxyRequest(event, resolveDownloadUrl(target, storageUrl), { onResponse: forceNoStore });The target is resolved through a fixed allow-list (bucket / bucket-file, mapped straight to buckets/:id/stream and bucket-files/:id/stream) rather than an object literal, since a plain object's bracket lookup resolves keys like constructor/__proto__ through the prototype chain instead of missing. The response is forced Cache-Control: no-store, since the proxy URL carries no session marker and would otherwise let one account's browser cache serve another account's authorized stream.
π Frontend β Session Cookies
π A new node always connects to a registry now
Registering a node without an explicit registryId only auto-connected it when the deployment had exactly one registry β with two or more configured, POST /nodes silently left both registryId and registryProjectId null, and the create form has no registry field to work around it. The node could then neither push nor pull analysis images, with nothing in the UI to say why.
// apps/server-core/src/app/modules/registry/adapter.ts
// Oldest-first keeps the fallback stable: adding a registry later never
// re-points what new nodes connect to. An explicit registryId still wins.
const registry = await this.registryRepository.findOne({ where: {}, order: { createdAt: 'ASC', name: 'ASC' } });Because a node now always reaches the Registry tab already connected, switching registries collapses from a destructive Disconnect-then-Connect (which destroyed the registry project and its robot account first) into a single Switch action β the server already unlinks the stale project and provisions a fresh one in one update.
π Node Management β Registry Connection
π³ Analysis images build even when the base image drops root
The generated Dockerfile created the analysis code directory with RUN mkdir -p /opt/code/, which executes as the master image's own USER. Any base image that drops privileges (USER postgres, β¦) failed the build outright with a permission error β latent today, since no image in the catalogue currently sets USER, but one line away from breaking a build.
FROM <registry>/<virtualPath>
ADD paths.tar /opt/
CMD ["python", "/opt/code/entrypoint.py"]Extraction preserves mode and ownership without a RUN, so the build context now carries the code directory pre-packed at 0777 inside paths.tar instead. The same worker release also closes a path the tar extractor took on faith: a forged typeflag decoded as type: null and sailed past the old file-type check, so a symlink or an unregistered entry could reach the container. The check is now an allow-list (file only) instead of a deny-list, and a rejected entry no longer crashes the worker or leaks the in-flight upload connection β both were reachable today, not just through the new check.
-if (!entry.type || entry.type !== 'file') {
- return;
-}
+if (entry.type !== 'file') {
+ throw new Error(`Unsupported tar entry type ${entry.type} for ${entry.name}.`);
+}π§΅ Buckets can be traced back to the resource that created them
analysis_buckets gain refType / refId columns, mirroring the pattern telemetry's Event entity already uses. AnalysisStorageManager stamps both automatically on every bucket it provisions for an analysis, and either can be set directly when creating a bucket β refId alone is rejected, since a specific instance with no named kind is meaningless:
await bucketClient.create({ name: 'code', refType: 'analysis', refId: analysis.id });β οΈ Heads-Up Before Upgrading
| Change | What to do |
|---|---|
Telemetry event filters on createdAt/updatedAt are gone; sorts are limited to createdAt, now also the schema default |
Remove those filters from any saved event queries or dashboards; expect newest-first ordering unless you sort explicitly |
The analysis list no longer accepts filter[description] |
Drop it from any saved analysis queries |
EventAPI.update() / IEventAPI.update are removed from @privateaim/telemetry-kit |
Remove any code calling it β the route never existed, so nothing behavioral changes besides the compile error |
Event.scope is a closed EventScope union; POST /events now answers 400 outside it |
If you publish custom telemetry events, check scope against EventScope before this upgrade |
Every select: false column is stripped from entity-event payloads |
If anything reads a gated field (e.g. a rotated accountSecret) off a realtime event payload, switch it to an authenticated HTTP read |
| Harbor robot secrets may have leaked into the telemetry audit trail and realtime socket on earlier 0.x releases | Rotate every registry-project robot account created or rotated before this release |
Authup's account console default moved to <AUTHUP_URL>/console/account (an authup beta.64 change hub only now picked up via this release's beta.65 bump) |
If NUXT_PUBLIC_ACCOUNT_URL or a reverse-proxy rule assumes the old /account path, update it |
β¬οΈ Upgrading
npm install @privateaim/client-vue@^0.16.0 @privateaim/telemetry-kit@^0.16.0 @privateaim/storage-kit@^0.15.1
# bump whichever @privateaim/* packages your project depends onSelf-hosted deployments: apply server-core's and server-storage's new migrations β the constraint-name repair, the query indexes, and the buckets ref_type/ref_id columns β before starting the new image (node dist/cli/index.mjs migration run inside each service).
π Deployment β Configuration Β· API Reference
π Changelog
π Features
- back every queryable key with a real index and gate schema drift (#1873) (cdf83ce)
- client-vue,client-ui: hand server-rendered data to the client instead of fetching it twice (#1870) (d71b19c)
- squash unreleased migrations and add ref_type/ref_id to buckets (#1886) (bbad96a)
π©Ή Fixes
- connect every new node to a registry, and let node admins manage that connection (#1859) (0be5ce6)
- deps: bump authup to beta.65 and follow the FHS provisioning move (#1882) (0cb7565)
- deps: bump the minorandpatch group with 8 updates (#1862) (450bc71)
- server-core-worker,server-storage: create the code directory regardless of the base image user (#1875) (d46f8a3)
- server-core-worker: reject non-file tar entries, and settle every pack failure route (#1872) (fe8294f)
- telemetry-kit,server-db-kit,server-telemetry: harden the telemetry event system (#1866) (dd579e9)
- download proxy URL/allow-list bugs, telemetry retention consistency, tar-pack ancestor synthesis (#1888) (24b5c0a)
β οΈ Breaking Changes
- the telemetry event query surface narrowed β filters
createdAt/updatedAtare gone and sorts allow onlycreatedAt(now also the schema default); theanalysislist'sdescriptionfilter is gone. Multi-key sorts of individually advertised keys decode to the schema's sort default (or no ordering) unless a composite index backs them. - telemetry-kit, server-db-kit, server-telemetry:
EventAPI.update()andIEventAPI'supdateare removed (the route never existed and there is noEVENT_UPDATEpermission).Event['scope']is now the closedEventScopeunion, andPOST /eventsanswers 400 outside it.EntityEventMetadata.eventandSubscriberPublishPayload.typenarrow toDomainEventName. Entity-event payloads no longer carryselect: falsecolumns.
π¦ Dependencies
@privateaim/client-vuebumped from^0.15.0to^0.16.0@privateaim/telemetry-kitbumped from^0.15.0to^0.16.0@privateaim/client-vue-theme,@privateaim/core-kit,@privateaim/kit,@privateaim/storage-kitbumped from^0.15.0to^0.15.1