Repository navigation
feat(audit-trail): cross-collection timeline route [PRD-1257] - #1911
Merged
bexchauveto merged 7 commits intoOct 9, 2026
Merged
bexchauveto merged 7 commits into
bexchauveto merged 7 commits into
Conversation
2 new issues
|
|
Coverage Impact This PR will not change total coverage. Modified Files with Diff Coverage (6)
🛟 Help
|
bexchauveto
added this pull request to stack #1913
September 22, 2026 07:15
bexchauveto
force-pushed
the
feature/prd-1257-agent-nodejs-cross-collection-audit-trail-route-for-the
branch
from
September 22, 2026 07:17
829575d to
830de40
Compare
bexchauveto
force-pushed
the
feature/prd-1257-agent-nodejs-cross-collection-audit-trail-route-for-the
branch
from
September 22, 2026 07:55
830de40 to
70cbd1a
Compare
bexchauveto
force-pushed
the
feature/prd-1257-agent-nodejs-cross-collection-audit-trail-route-for-the
branch
from
September 22, 2026 07:56
70cbd1a to
e3913cb
Compare
bexchauveto
force-pushed
the
feature/prd-1257-agent-nodejs-cross-collection-audit-trail-route-for-the
branch
from
September 22, 2026 08:19
e3913cb to
7e558a9
Compare
bexchauveto
force-pushed
the
feature/prd-1257-agent-nodejs-cross-collection-audit-trail-route-for-the
branch
2 times, most recently
from
September 22, 2026 09:35
7e558a9 to
f0dd096
Compare
This was referenced Sep 22, 2026
Merged
bexchauveto
removed this pull request from stack #1913
September 22, 2026 12:05
bexchauveto
added this pull request to stack #1915
September 22, 2026 12:06
bexchauveto
force-pushed
the
feature/prd-1257-agent-nodejs-cross-collection-audit-trail-route-for-the
branch
2 times, most recently
from
September 23, 2026 09:14
14b7a63 to
c3e9b71
Compare
bexchauveto
force-pushed
the
feature/prd-1257-agent-nodejs-cross-collection-audit-trail-route-for-the
branch
from
September 23, 2026 09:36
c3e9b71 to
b4ae8f0
Compare
bexchauveto
force-pushed
the
feature/prd-1257-agent-nodejs-cross-collection-audit-trail-route-for-the
branch
from
September 23, 2026 09:37
b4ae8f0 to
13e290f
Compare
bexchauveto
force-pushed
the
feature/prd-1257-agent-nodejs-cross-collection-audit-trail-route-for-the
branch
from
September 23, 2026 09:47
13e290f to
4dc16ee
Compare
bexchauveto
force-pushed
the
feature/prd-1257-agent-nodejs-cross-collection-audit-trail-route-for-the
branch
3 times, most recently
from
September 30, 2026 15:23
04f1eab to
b8ec83e
Compare
Base automatically changed from
feature/prd-1256-agent-nodejs-operation-filter-on-the-audit-trail-route
to
main
September 30, 2026 15:34
bexchauveto
force-pushed
the
feature/prd-1257-agent-nodejs-cross-collection-audit-trail-route-for-the
branch
from
September 30, 2026 15:34
b8ec83e to
7eab810
Compare
bexchauveto
force-pushed
the
feature/prd-1257-agent-nodejs-cross-collection-audit-trail-route-for-the
branch
6 times, most recently
from
October 7, 2026 09:31
f1d3ed3 to
f1ae943
Compare
arnaud-moncel
self-requested a review
October 7, 2026 12:56
This was referenced Oct 7, 2026
bexchauveto
force-pushed
the
feature/prd-1257-agent-nodejs-cross-collection-audit-trail-route-for-the
branch
from
October 7, 2026 15:00
f1ae943 to
cc2e1de
Compare
Only a caller whose permission level is `admin` — exactly that level, not a privileged set — gets `previousValues` and `newValues` on the project-level timeline. Every other caller gets each row with both objects empty, so operation, author and timestamp still read. A record's own history is deliberately *not* gated: someone who can read the collection can already read the record, so its before/after values tell them nothing the record itself doesn't. The project-wide feed is a different exposure — every collection at once, with no record to know in advance — and that aggregate is what stays admin-only. Advertised as `restrictsProjectAuditValuesToAdmins` in the capabilities payload, so the front can tell an agent that enforces the rule from an older one that merely hides the values in its UI. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…vel page `GET /forest/_audit-trail` serves every collection at once, newest first, so the project-level page can show before/after values rather than activity logs alone. Each row already carries its `collection` and `recordId`, so the front can name and link the record it belongs to. Permissions are resolved per collection: only what the caller can read is queried, and a collection the caller sees only through a record-level scope is left out entirely — a scope can't be evaluated across a whole timeline without fetching every record it mentions, and the row alone would reveal that a record the caller cannot read exists and was touched. Paging is by cursor, not offset: a project-wide feed grows at the head, so an offset shifts rows across pages. `meta.cursor` carries an inclusive `before` bound plus the `excludeIds` already served at it, ties order by descending id, and a timestamp with more rows than fit on one page accumulates its exclusions rather than looping. The filter parsing the record route owns moves to `audit-trail/query-params` so both routes speak one contract. `listTimeline` is optional on `AuditStore`: a custom store written before this simply doesn't get the route mounted. Inherits the admin gate on detail values. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ast a tie From review: ids come back through `Number()` in the SQL store's `fromRow`, so an id past 2^53 rounds on the way out and the next page's `NOT IN` fails to exclude the row it names — the same page would then repeat forever. Staying on one timestamp must add at least the last row's id to the exclusions, so a cursor that would come back unchanged now reads as the end of the walk rather than another page. The upgrade path, if an audit table ever nears 9e15 rows, is to carry the id as a string through `AuditRecord`, the cursor and the SQL binding — a breaking change to a public type, not worth making for a ceiling nothing is near. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…oute Follows the rename below it: the comments on `readableCollections` say which kind of scope excludes a collection from the project-level feed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…e on From review: the routes preamble said all four are gated by `assertCanRead` on the target collection, which the cross-collection timeline has none of — it checks `canRead` per collection and queries the ones that pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…eline The timeline route has no record_id to narrow on, so each page sorted the whole table. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nce canUseAuditTrailTimeline [PRD-1257] Drops the admin gate on the cross-collection timeline. A caller who can read a collection already reads each record's history one at a time, so the feed serves the same values in aggregate. Who can open the project's Activity Logs is decided per team by Forest. restrictsProjectAuditValuesToAdmins goes with it. The front read it as "this agent mounts the timeline", which it now announces directly: canUseAuditTrailTimeline, true only when the store implements listTimeline. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
bexchauveto
force-pushed
the
feature/prd-1257-agent-nodejs-cross-collection-audit-trail-route-for-the
branch
from
October 9, 2026 07:06
4bf169f to
6c72f5a
Compare
bexchauveto
deleted the
feature/prd-1257-agent-nodejs-cross-collection-audit-trail-route-for-the
branch
October 9, 2026 12:33
forest-bot
added a commit
that referenced
this pull request
Oct 9, 2026
# @forestadmin/agent [1.108.0](https://github.com/ForestAdmin/agent-nodejs/compare/@forestadmin/agent@1.107.3...@forestadmin/agent@1.108.0) (2026-10-09) ### Features * **audit-trail:** cross-collection timeline route [PRD-1257] ([#1911](#1911)) ([811559f](811559f))
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

fixes PRD-1257. Stacked on #1910 → #1909.
GET /forest/_audit-trailserves every collection at once, newest first, so the project-level activity page can show before/after values rather than activity logs alone. Rows already carrycollectionandrecordId, so the front can render "…on the Customer joe@ex.ample" and link it.{ "data": [ /* rows */ ], "meta": { "cursor": { "before": "2026-01-05T00:00:00.000Z", "excludeIds": [12, 11] } } }Values: no admin gate (changed on 8 October)
This PR first carried an admin gate on the timeline's values (
restrictsProjectAuditValuesToAdmins, moved here from the withdrawn #1908). Commit4bf169f7adrops it: a caller who can read a collection gets its rows withpreviousValues/newValues, whatever their permission level.Why that holds: someone who can read the collection can already read each record's history one at a time, so the feed serves the same values in aggregate (the "aggregation boundary, not a data barrier" framing of PRD-1259). Who can open the project's Activity Logs page at all is decided per team by Forest (
featuresDeactivated/TeamDeactivatableFeature.activityLogs, enforced by the front and private-api). This route does not see that team setting; it only returns what the caller can read anyway.The capability changes with it:
canUseAuditTrailTimeline, true only when the store implementslistTimeline. It replacesrestrictsProjectAuditValuesToAdmins, which the front was using as a proxy for "this agent mounts the route". Nothing has shipped with the old flag, so there is no compatibility to keep.Permissions
This is the first audit route with no single record to authorize against:
Cost of that second rule: a caller scoped on every collection gets an empty timeline. Say the word if you'd rather have the rows with values blanked instead.
Cursor contract (Ruby implements against this)
Offset paging is wrong here: a project-wide feed grows at the head, so
skipshifts rows across pages.page[size]still applies (default 20, cap 100).beforetimestampexcludeIdsInclusive on purpose: rows sharing the boundary timestamp must not fall between two pages, so they come back and
excludeIdsdrops the ones already sent. Ties order by descendingid. A timestamp holding more rows than fit on one page accumulates its exclusions across pages.meta.cursorisnullon the last page, and also when the walk cannot move past the current timestamp — see thenextCursorcomment for the ceiling that guards (ids past 2^53, whichfromRowrounds) and its upgrade path.Filters
The record route's, minus
fields(a column name means nothing across collections):userIds,operation,startDate/endDate/timezone,search— parsed by the same code, now insrc/audit-trail/query-params.tsso the two routes cannot drift.Store
listTimelineis optional onAuditStore: adding a required method would break anyone with their own store. A store that doesn't implement it doesn't get the route mounted.Tests
16 route cases (permission sweep, scope exclusion, empty-without-store-call, filter forwarding, five cursor cases incl. the no-progress guard, a non-admin served the values, conditional mounting), 7 sql-store cases, capability-payload cases including
canUseAuditTrailTimelinewith and withoutlistTimeline.1675 passed, lint clean.🤖 Generated with Claude Code
Note
Add cross-collection audit-trail timeline route with permission gate
GET /_audit-trailroute (audit-trail-timeline.ts) that returns audit rows across collections the caller can read without a record-level scope, with cursor pagination that handles timestamp ties by excluding already-served idslistTimelineas an optionalAuditStoremethod with a SQL implementation in sql-store.ts, plus a new timestamp/id index migration (002) for existing audit tablescanUseAuditTrailTimelinecapability flag are only exposed when the store implementslistTimeline; stores without it keep working and skip mountingMacroscope summarized 6c72f5a.