Version
v0.22.0
Date
2026-07-12
Highlights
- CDC Change Feed (Phases A–H). Outbox capture, pull API, dispatcher,
extension integration, retention, and Studio surface — plus skills/MCP
coverage. The change-data-capture pipeline lands end to end (#244). - Realtime studio co-editing, hardened. A read-gated, filterable realtime
plane: subscribe is permission-scoped and fail-closed, broadcasts are
signal-only (no row data on the wire), and the item editor shows a live
co-editing warning (#249). - Security hardening pass. Settings writes admin-gated with secret reads
redacted, external-JWT DoS guards + denial/issuer auditing, scheduled-release
publishes now audited, and an out-of-scope findings backlog wired into the
Definition of Done (#251). - v1 readiness groundwork. Golden-path E2E gate + dependency-audit in CI,
"Upgrading to 1.0" runbook, SECURITY.md, and the versioning policy — staging
the remaining work before the v1.0.0 tag (#243, #239).
Security
-
Realtime subscribe is read-gated and filterable. A studio session could
previously subscribe to any collection name and receive change signals for
collections it had noreadgrant on (metadata leak: which collections
change, when, and which item ids). The studio realtime ticket now embeds the
collections the principal canread(computed by PermissionService at ticket
issuance — admin bypass gets*), and the hub rejects any othersubscribe
withSUBSCRIBE_FORBIDDEN, fail-closed on a missing/empty allowlist. The
subscribemessage also accepts an optional Directus-stylefilter,
evaluated server-side per subscription over the event envelope
(collection/action/itemId— the wire is signal-only, so row data is
never filterable or leakable). -
Scheduled release publishes are now audited. The scheduler sweep
published due releases without writing any audit row — only manual publishes
were recorded. The sweep now writes the samerelease_published/
release_partially_published/release_publish_failedvocabulary
(shared helpers, counts-only metadata) withtrigger: 'scheduled'. -
Realtime studio broadcasts are signal-only. An item mutation used to
fan out the fullrow.datato every studio session subscribed to the
collection, without re-checking that session's read grant or field mask —
a client could read row content (including masked fields) straight off the
WebSocket. The broadcast now carries only the change signal
(collection/action/itemId,payload: null); the Studio client
re-fetches through the permission-enforced/itemsAPI, so field masking and
row RBAC apply by construction and no row content crosses the wire. -
External JWT auth: DoS guards + denial/issuer auditing. The verifier now
rejects an oversized bearer (> 8192chars) before parsing it and caps the
role-claim list it resolves (≤ 50), bounding attacker-controlled parse/query
work. Denied external authentications now write anexternal_auth_denied
audit row (classification code only — never the token, claims, or reason), and
issuer create/update/delete writeexternal_issuer_*audit rows. -
FK dependent-records now enforce the caller's RBAC. The
POST /api/v1/items/:collection/:id/resolve-dependentsand
GET …/dependentsendpoints previously ran the batchset_null/reassign
/deleteand the preflight report without the caller's permission context —
any authenticated tenant member could clear, reassign, or delete records in a
collection they had noupdate/deletegrant on, and read dependent ids they
could not otherwise see. The resolve path now gates each action against
update/deleteon the dependent collection (403FORBIDDEN), scopes batch
writes to the caller's row-level grant, delegates deletes through a
permission-carryingItemService, and the preflight requiresreadon the
target and only samples rows the caller may read. A source-independent
tripwire (dependents-service-rbac.test.ts) locks the gate, and Definition of
Done §2c gains a rule for request-path services that delegate toItemService
or write content tables directly. No schema or setup change. -
/api/v1/settingswrites are now admin-only.POST /api/v1/settingsand
DELETE /api/v1/settings/:keywere open to any authenticated site member,
letting a non-admin overwrite arbitrary settings keys (including
upload_policyandmedia.signedTransform) and bypass the admin gates on
dedicated config endpoints. Both now requirerequireSiteAdmin; reads stay
open because non-admin editors legitimately read keys likelocales. -
Settings reads redact secret-bearing fields.
GET /api/v1/settingsand
/:keypreviously returned raw values including secrets such as
media.signedTransform.secret. Secret-named fields (secret/token/password/
apiKey/…) are now redacted ([redacted]) on read for every caller; code that
needs the real value reads it directly from the DB, not this HTTP endpoint.
Performance
- Trusted external-JWT issuers are cached.
getTrustedIssuersqueried the
DB on every bearer-token request; it now reads throughruntime.cache
(auth:issuers:<siteId>, TTL 60s) and issuer create/update/delete drop the
key, so config changes apply within the TTL bound.
Added
- Change Feed (CDC Extension Integration). First-party transactional
outbox + relay over content mutations:lumibase_cdc_change_events/
_subscriptions/_deliveries(migration0007_cdc_change_feed, RLS
site-isolated), cursor-paginatedGET /api/v1/cdc/events, HMAC-signed
webhook dispatcher with retry/dead handling, sandboxed extension
subscribers (defineCdcSubscriber, manifest capability
cdc:subscribe:<collection>), retention + replay, Studio → Settings →
Change Feed panel, five governed AI skills and MCP tools.
Upgrade note: two new capability strings exist —cdc:subscribe
(read the feed / ack) andcdc:manage(AI-skill subscription
management). Admin roles satisfy them implicitly (adminAccess
wildcard); grant them explicitly only for narrow integration tokens.
deleteCdcSubscriptionis control-plane → HITL below autopilot.
Feed is off-by-default per site (cdc_feed.enabledor first active
subscription turns it on). No backfill: three new empty tables. - Registry-numbering tripwire (
pnpm registry:check). A CI check
(scripts/check-registry-numbering.mjs, wired into the CIchecksjob) fails
the build when the Setup Impact Registry#column contains a duplicate —
mechanizing the Definition of Done §2 uniqueness rule per §6 ("cơ giới hóa"),
replacing the manualgrep. - Out-of-scope findings backlog + Definition of Done §7. A single place
(.kiro/steering/out-of-scope-backlog.md) to log vulnerabilities, bugs, and
follow-up tasks discovered while working a PR but outside its scope, so they
are not lost after merge. DoD §7 makes logging them a required review step.
Fixed
build-release-manifest.mjsis now idempotent. Regenerating
apps/docs/public/releases.jsonon a plaindocs:buildno longer dirties the
working tree: editorial fields (migrationWarning,minimumSafeUpgradeVersion)
andreleaseDateare preserved from the committed manifest unless explicitly
overridden (env var, or a matching CHANGELOG heading for the date). This also
fixes a latent bug where a deploy build could clobber a hand-set editorial
value back to its default.
Changed
- Setup Impact Registry
#column deduplicated. Parallel branches had kept
picking "the next number" independently, leaving many collisions (#16/#20/#21
through #38). Colliding rows were renumbered to fresh ids (45–68), keeping the
occurrence that other rows cite by number so cross-references stay valid.
Migrations
0007_cdc_change_feed— Change Feed outbox/capture tables (additive,
idempotent). No destructive changes.