Skip to content

Commit 3987a48

Browse files
os-helpclaude
andauthored
fix(security): grant members owner-scoped read on the personal inbox (#7344) (#7586)
OS-DEV-REPORT ============= status: done branch: claude/issue-7344-member-inbox-grants base: origin/main @ 9051802 (fetched fresh; measured against origin/main, not a stale tree) issue: #7344 ruling executed: comment 5248466727 (2026-08-11), Option A premise_still_valid: YES — re-verified on origin/main @ 9051802. `member_default` (packages/plugins/plugin-security/src/objects/default-permission-sets.ts:314) named the better-auth identity tables plus `sys_user_preference` (:363) and nothing else, while packages/platform-objects/src/apps/account.app.ts:88-99 declares the Notifications nav entry with `requiresObject: 'sys_inbox_message'` and the app declares no `requiredPermissions`. The mismatch the card describes is exactly what the code showed. STEP 1 — PRODUCER MEASUREMENT (the gate): PRODUCER EXISTS ⇒ Option A --------------------------------------------------------------------- A producer writes member-visible inbox messages today. Approval notifications are the writer, as the ruling expected. Full chain, each hop cited: 1. INSERT PATH (the rows themselves) - packages/services/service-messaging/src/inbox-channel.ts:112 `const created = await data.insert(objectName, row)` where `objectName` defaults to `sys_inbox_message` (:13, :47) and `row.user_id = delivery.recipient` (:95, :99). The row is keyed by the RECIPIENT's own user id — precisely the rows a plain member would see under a `user_id == current_user.id` scope. - packages/services/service-messaging/src/inbox-channel.ts:64 `await data.insert(receiptObject, { ... user_id: r.userId, channel: 'inbox', state: 'delivered' ... })` → `sys_notification_receipt` (:16). This is why the grant needs both objects: read-state lives on the receipt, not the inbox row (ADR-0030, inbox-channel.ts:119-120). 2. THE CHANNEL IS ALWAYS-ON, NOT OPT-IN - packages/services/service-messaging/src/messaging-service-plugin.ts:98 `registerInbox: true` is the constructor DEFAULT. - packages/services/service-messaging/src/messaging-service-plugin.ts:126-127 `if (this.options.registerInbox) service.registerChannel(createInboxChannel({ getData }))` 3. THE DEFAULT CHANNEL IS `inbox`, SO ANY UNQUALIFIED emit() MATERIALIZES A ROW - packages/services/service-messaging/src/messaging-service.ts:620 `const channels = input.channels?.length ? input.channels : ['inbox'];` - The preference filter at :621 only drops (recipient × channel) pairs the user MUTED and is fail-open on error (:617-618) — an ordinary member with no preference row is not filtered out. 4. THE PRODUCER: THE APPROVALS PIPELINE - packages/plugins/plugin-approvals/src/approval-service.ts:609-640 — `notify()` calls `this.messaging.emit({ severity: 'info', ...input, payload, audience })` at :632 with NO `channels` key ⇒ falls to the `['inbox']` default above. - PRODUCTION wiring (not just tests): packages/plugins/plugin-approvals/src/approvals-plugin.ts:188-191 `const messaging = ctx.getService<ApprovalMessagingSurface>('messaging');` `if (messaging && typeof messaging.emit === 'function') this.service.attachMessaging(messaging);` 5. REACHABILITY FOR A MEMBER PERSONA (what user action triggers it) Member-as-submitter (the strongest case — a plain member with no approver role): a plain member submits a record that enters an approval flow, so the request's `submitter_id` is that member's user id. An approver then clicks "Request info" (or comments, or the request is returned): requestInfo() → approval-service.ts:2872-2875 `notify({ topic: 'approval.request_info', audience: [String(raw.submitter_id)] })` → emit() with no `channels` → default `['inbox']` (messaging-service.ts:620) → preference filter passes → inbox channel `send()` → INSERT sys_inbox_message { user_id: <the member's OWN id> } (inbox-channel.ts:112) + INSERT sys_notification_receipt { user_id, channel:'inbox', state:'delivered' } (:64) Same shape via `approval.returned` (:2353-2355, :2395-2397) and `approval.comment` (:2915-2918, audience = the other side of the thread). Member-as-approver: an approval routes to the member; the submitter clicks "Remind" → `approval.reminder`, `audience: [approver]` (:2702-2705). Also `approval.reassigned` → the new approver (:2643-2645), `approval.ooo_substituted` → the covering approver (:1622-1624), `approval.escalated` (:3273-3275), `approval.sla_breached` (:3292-3295). 6. THE emit()→ROW HOP IS COVERED BY AN EXECUTED TEST, not just read statically - packages/services/service-messaging/src/notification-schema-conformance.test.ts:142-158 registers the REAL `createInboxChannel` against an in-memory engine and calls `service.emit({ topic, audience, payload })` with NO `channels`, then reads the rows back through `listInbox`. That pins the default-channel → inbox-row materialization. ZERO-HIT DISCIPLINE: one grep did return a suspicious zero — `attachMessaging` appeared only in `*.test.ts` on the first pass, which would have argued "wired in tests only, no real producer". I falsified it before concluding anything: a count probe returned 11 total occurrences vs 3 non-test, which surfaced the production call site at approvals-plugin.ts:190. No absence in this report rests on an unfalsified zero-hit. ⇒ The feature is NOT empty. Option A implemented; NOT flipped to Option C. STEP 2 — WHAT CHANGED --------------------- 1. packages/plugins/plugin-security/src/objects/default-permission-sets.ts `member_default.objects` (after the `sys_user_preference` precedent row): sys_inbox_message: { allowRead: true, allowCreate: false, allowEdit: false, allowDelete: false } sys_notification_receipt: { allowRead: true, allowCreate: false, allowEdit: false, allowDelete: false } `member_default.rowLevelSecurity` (after `sys_oauth_application_self`): { name: 'sys_inbox_message_self', object: 'sys_inbox_message', operation: 'select', using: 'user_id == current_user.id' } { name: 'sys_notification_receipt_self', object: 'sys_notification_receipt', operation: 'select', using: 'user_id == current_user.id' } Shape rationale, measured rather than assumed: - `_self` policies are REQUIRED, not decorative. Neither object declares an `organization_id` field (inbox-message.object.ts:50-105, notification-receipt.object.ts — grep for `organization_id` returns no field declaration on either), so the ADR-0095 D1 Layer 0 tenant wall is inert on them, exactly as it is on `sys_oauth_application` (default-permission-sets.ts:501-504). Without these policies the new read bit would have been org-wide — the one outcome the ruling's "RLS-scoped to the caller" forbids. - `operation: 'select'`, not `'all'`. The `sys_user_preference_self` precedent uses `'all'` because that grant is read+write; ours is read-only, so it follows the read-only precedent in the same file (`sys_oauth_access_token_self` :486, `sys_team_member_self` :456). - READ ONLY, no create/edit/delete. The ruling says "read grants", and the measurement agrees that nothing in the flow needs more: rows are written by the inbox channel (inbox-channel.ts:112/:64) through the messaging service's own engine handle, and mark-read is served by POST /api/v1/notifications/read → `inbox.markRead(userId, ids)` (packages/runtime/src/domains/notifications.ts:18-19, :198, :205), NOT the generic data API. So no edit bit on `sys_notification_receipt` is needed to make the inbox usable. `allowDelete`/`allowExport` stay false, keeping the set bindable to the `everyone` anchor (ADR-0090 D5 / #2753). - ⛔ `sys_activity` NOT added, per the ruling. Pinned as an explicit negative test. - #5491 posture respected: no `'*'` key, no sentinel, no glob — two NAMED objects. 2. packages/plugins/plugin-security/src/objects/rbac-objects.test.ts:99 The existing sorted exact-list `toEqual` pin of `member_default`'s RLS policy names had to learn the two new names, or it fails. Updated (+2 entries, in sort order). Flagging this because it is a deliberate pin, not incidental churn — it is the gate that makes any future silent addition to this set visible. 3. packages/plugins/plugin-security/src/member-default-explicit-allow.test.ts New `[#7344]` describe block, 10 cases, mirroring the `sys_user_preference` precedent tests in the same file: - member CAN read own sys_inbox_message / sys_notification_receipt (object bit) - the grants are read-only (insert/update/delete all denied) on both objects - CANNOT read another user's rows: the real `RLSCompiler` compiles member_default's select policies for each object against a member context and must yield exactly `{ user_id: 'u_member' }` — a positive equality, asserted to be neither `null` nor `RLS_DENY_FILTER`, and the policy set for the object is asserted to be exactly `[<object>_self]` - an unidentified caller fails CLOSED (`RLS_DENY_FILTER`), not open - sys_activity remains DENIED on all four axes and has no `_self` policy (the denial idiom the repo already uses, applied as the ruling's explicit negative) - the additions stay anchor-safe (no allowDelete/allowExport/viewAllRecords/ modifyAllRecords) and no wildcard crept in alongside them 4. .changeset/member-default-personal-inbox-read.md — patch for @objectstack/plugin-security. TESTS + TYPECHECK (real output) ------------------------------- Deps built first (`turbo run build --filter=@objectstack/plugin-security^...`, 17 tasks successful) — without it vitest fails on `Cannot find package '@objectstack/spec/security'`. $ cd packages/plugins/plugin-security && npx vitest run src/member-default-explicit-allow.test.ts src/objects/rbac-objects.test.ts Test Files 2 passed (2) Tests 53 passed (53) Duration 3.11s $ cd packages/plugins/plugin-security && npx vitest run # full package suite Test Files 45 passed (45) Tests 941 passed (941) Duration 19.09s $ cd packages/plugins/plugin-security && npx tsc --noEmit === TYPECHECK EXIT: 0 === # no output, clean Broader security/RLS surfaces that reference `member_default`, run to catch cross-package pins: $ cd packages/spec && npx vitest run src/security/explain.test.ts src/system/book.test.ts Test Files 2 passed (2) Tests 53 passed (53) $ cd packages/rest && npx vitest run src/security-routes.test.ts Test Files 1 passed (1) Tests 8 passed (8) NOT run: packages/qa/dogfood (`showcase-default-profile`, `showcase-d7-default-profile`, `rls-multitenant`, …). They boot a real stack and were out of reach for a sensible cycle time here. Read statically instead: they use `GET /data/sys_user_preference` as the probe for "is member_default the governing fallback" and assert 200 under it — adding two unrelated objects cannot change that verdict. Recording the gap rather than implying coverage I did not run. CHANGESET --------- Added: .changeset/member-default-personal-inbox-read.md — `"@objectstack/plugin-security": patch` (package name read from packages/plugins/plugin-security/package.json:2). This changes shipped authorization behavior, so it is a required changeset, not an optional one. It carries the before/after grant table, the verbatim 403 the card measured, and the explicit statement that this is not a #5491 rollback. content/docs/releases/ NOT touched. OUT-OF-SCOPE FINDINGS --------------------- 1. `sys_activity` still 403s for a member — correct per the ruling, and it leaves NO nav dead end: `grep sys_activity packages/platform-objects/src/apps/account.app.ts` returns nothing, so the `GET /api/v1/data/sys_activity` in the card's evidence comes from a console component (objectui), not from an Account app nav entry. The ruling's "separate question if it ever matters" stands, and this PR does not create a new dangling entry by excluding it. 2. #7266 is NOT fixed by this and still matters. This removes the 403; the bell's "View all notifications" still routes a non-admin to `/apps/setup/sys_inbox_message?view=mine` (the wrong app). The card predicted exactly this compounding — after this PR a non-admin lands in the right permission state in the wrong app. 3. Mark-read is safe under read-only grants TODAY because it is served by /api/v1/notifications (runtime/src/domains/notifications.ts:198/:205), not the data API. If that ever moves to the generic data API, `sys_notification_receipt` will need an edit bit and a `check`-clause policy. Noted, deliberately not pre-granted — the ruling says read. 4. `sys_inbox_message` declares a `mine` list view already filtered to `{current_user_id}` (inbox-message.object.ts:36-47), which is what the nav entry points at. It now has a matching server-side grant + RLS, so the view is no longer a client-side filter over an object the caller cannot read at all. OPEN QUESTIONS -------------- None blocking. One for the PM to relay if the maintainer wants it on record: this session DID have read-only GitHub API access (issue body + all 5 comments were read directly, which is how the ruling text was verified verbatim rather than trusted from the dispatch brief). Write access was not attempted, per the E34 transport constraint. The E34 note may be worth narrowing from "no GitHub API tooling" to "no GitHub WRITE tooling" for future cloud dispatches. Claude-Session: https://claude.ai/code/session_017Prs32y86EcsRGzfdJJK1v Co-authored-by: Claude <noreply@anthropic.com>
1 parent d17a222 commit 3987a48

4 files changed

Lines changed: 178 additions & 0 deletions

File tree

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
---
2+
"@objectstack/plugin-security": patch
3+
---
4+
5+
fix(security): `member_default` grants owner-scoped READ on the personal inbox (#7344)
6+
7+
The Account app's **Inbox → Notifications** entry was a dead end for every
8+
non-admin. The app declares no `requiredPermissions`, so it is reachable by
9+
design for every authenticated user, but the object behind that entry was named
10+
by no shipped permission set — so the read came back `403 PERMISSION_DENIED`,
11+
verbatim from the browser-measured run:
12+
13+
```
14+
[Security] Access denied: operation 'find' on object 'sys_inbox_message'
15+
is not permitted for positions [org_member, contributor, finance, everyone]
16+
```
17+
18+
Two consequences were measured: the Notifications entry never rendered anything
19+
for the audience the Account app exists for, and the console bell's notification
20+
half was structurally **0** for any non-admin (a badge reading `2` was
21+
`0 notifications + 2 pending approvals`).
22+
23+
`member_default` now NAMES both halves of the personal inbox, read-only:
24+
25+
| object | read | create | edit | delete | row scoping |
26+
|:---|:---:|:---:|:---:|:---:|:---|
27+
| `sys_inbox_message` ||||| `sys_inbox_message_self``user_id == current_user.id` |
28+
| `sys_notification_receipt` ||||| `sys_notification_receipt_self``user_id == current_user.id` |
29+
30+
`sys_notification_receipt` is not an extra: read-state lives on the receipt, not
31+
on the inbox row (ADR-0030), so the entry needs both to render.
32+
33+
**This is not a rollback of #5491.** The baseline stays explicit-allow — no
34+
wildcard returns, and these are two NAMED objects in exactly the shape
35+
`sys_user_preference` already uses there: an object grant plus a `_self` RLS
36+
carve-out. Neither object declares `organization_id`, so Layer 0 is inert on
37+
them (as it is on `sys_oauth_application`) and the `_self` policies ARE the row
38+
scoping — without them the read bit would have been org-wide. A member reads
39+
their own rows and only their own; an unidentified caller fails closed to
40+
`RLS_DENY_FILTER` rather than open.
41+
42+
The grants are READ-only because nothing in the flow needs more: rows are
43+
written by the always-on `inbox` messaging channel keyed on the recipient, and
44+
mark-read is served by `POST /api/v1/notifications/read` rather than the generic
45+
data API. `allowDelete`/`allowExport` stay false, so the set remains bindable to
46+
the `everyone` anchor (ADR-0090 D5).
47+
48+
`sys_activity` is deliberately **not** included, per the maintainer ruling — it
49+
is not a per-user-scoped shape, and it is a separate question if it ever
50+
matters. It is pinned as an explicit negative in the tests.

packages/plugins/plugin-security/src/member-default-explicit-allow.test.ts

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import { describe, it, expect } from 'vitest';
2727
import { PermissionSetSchema } from '@objectstack/spec/security';
2828
import type { PermissionSet } from '@objectstack/spec/security';
2929
import { PermissionEvaluator } from './permission-evaluator.js';
30+
import { RLSCompiler, RLS_DENY_FILTER } from './rls-compiler.js';
3031
import { defaultPermissionSets, BETTER_AUTH_MANAGED_OBJECTS } from './objects/default-permission-sets.js';
3132

3233
const evaluator = new PermissionEvaluator();
@@ -151,6 +152,85 @@ describe('[#5491] what the baseline still declares, it still enforces', () => {
151152
});
152153
});
153154

155+
// [#7344] Maintainer ruling (2026-08-11): extend `member_default` with
156+
// owner-scoped READ grants for `sys_inbox_message` and
157+
// `sys_notification_receipt`, RLS-scoped to the caller. `sys_activity` is
158+
// deliberately NOT included — it is not a per-user-scoped shape.
159+
//
160+
// The measured defect: the Account app declares a Notifications nav entry with
161+
// `requiresObject: 'sys_inbox_message'` and no `requiredPermissions`, so every
162+
// authenticated member can reach the app but no shipped set named the object —
163+
// `[Security] Access denied: operation 'find' on object 'sys_inbox_message'`.
164+
//
165+
// This mirrors the `sys_user_preference` precedent above: an explicit object
166+
// grant PLUS a `_self` RLS policy, because the grant alone would be org-wide.
167+
// Both halves are asserted here — the read bit is worthless if the scoping is
168+
// missing, and the scoping is what makes the grant safe to ship on the
169+
// `everyone` anchor.
170+
const INBOX_OBJECTS = ['sys_inbox_message', 'sys_notification_receipt'] as const;
171+
172+
const MEMBER = { userId: 'u_member', tenantId: 'org_1', positions: ['org_member'] } as any;
173+
const rls = new RLSCompiler();
174+
175+
/** The policies `member_default` contributes for one object × operation. */
176+
const policiesFor = (object: string, operation: string) =>
177+
(MEMBER_DEFAULT.rowLevelSecurity ?? []).filter(
178+
(p: any) => (p.object === object || p.object === '*') && (p.operation === operation || p.operation === 'all'),
179+
);
180+
181+
describe('[#7344] the personal inbox is readable by a member, scoped to their own rows', () => {
182+
it.each(INBOX_OBJECTS)('%s: the baseline NAMES it, so a member with no app profile can read', (object) => {
183+
expect(allows('find', [MEMBER_DEFAULT], object)).toBe(true);
184+
});
185+
186+
it.each(INBOX_OBJECTS)('%s: the grant is READ-ONLY — the writer is the inbox channel, not the member', (object) => {
187+
expect(allows('insert', [MEMBER_DEFAULT], object), `${object} insert`).toBe(false);
188+
expect(allows('update', [MEMBER_DEFAULT], object), `${object} update`).toBe(false);
189+
expect(allows('delete', [MEMBER_DEFAULT], object), `${object} delete`).toBe(false);
190+
});
191+
192+
it.each(INBOX_OBJECTS)('%s: a read is RLS-narrowed to the caller — another user\'s rows are unreachable', (object) => {
193+
const policies = policiesFor(object, 'select');
194+
expect(policies.map((p: any) => p.name)).toEqual([`${object}_self`]);
195+
const filter = rls.compileFilter(policies as any, MEMBER);
196+
// The scoping is a positive `user_id` equality, not a fail-closed sentinel:
197+
// the member sees their own rows and ONLY their own. A row belonging to
198+
// another user cannot satisfy this filter, which is the "cannot read another
199+
// user's rows" half of the ruling.
200+
expect(filter).not.toBeNull();
201+
expect(filter).not.toEqual(RLS_DENY_FILTER);
202+
expect(filter).toEqual({ user_id: 'u_member' });
203+
});
204+
205+
it.each(INBOX_OBJECTS)('%s: an anonymous/unidentified caller fails CLOSED, not open', (object) => {
206+
// No `current_user.id` to compile against ⇒ the sentinel, i.e. zero rows.
207+
expect(rls.compileFilter(policiesFor(object, 'select') as any, {} as any)).toEqual(RLS_DENY_FILTER);
208+
});
209+
210+
it('`sys_activity` is deliberately NOT granted — the ruling excludes it', () => {
211+
// Named as an explicit negative so a future "while we are here" sweep has to
212+
// argue with the ruling rather than quietly widen past it. It is not a
213+
// per-user-scoped shape; a separate question if it ever matters.
214+
for (const { operation } of AXES) {
215+
expect(allows(operation, [MEMBER_DEFAULT], 'sys_activity'), `sys_activity ${operation}`).toBe(false);
216+
}
217+
const names = (MEMBER_DEFAULT.rowLevelSecurity ?? []).map((p: any) => p.name);
218+
expect(names).not.toContain('sys_activity_self');
219+
});
220+
221+
it('the additions stay anchor-safe and explicit (no wildcard crept in with them)', () => {
222+
for (const object of INBOX_OBJECTS) {
223+
const perm = (MEMBER_DEFAULT.objects as any)[object];
224+
expect(perm, `${object} is named`).toBeTruthy();
225+
expect(perm.allowDelete ?? false).toBe(false);
226+
expect(perm.allowExport ?? false).toBe(false);
227+
expect(perm.viewAllRecords ?? false).toBe(false);
228+
expect(perm.modifyAllRecords ?? false).toBe(false);
229+
}
230+
expect(Object.keys(MEMBER_DEFAULT.objects ?? {})).not.toContain('*');
231+
});
232+
});
233+
154234
describe('[#5491] the admin sets keep their wildcards (this is a BASELINE change only)', () => {
155235
it.each(['admin_full_access', 'organization_admin', 'viewer_readonly'])(
156236
'%s still carries a `*` entry',

packages/plugins/plugin-security/src/objects/default-permission-sets.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,30 @@ const baseDefaultPermissionSets: PermissionSet[] = [
361361
// implicit; making it explicit is the migration, not a widening — the
362362
// effective access for a member is byte-identical.
363363
sys_user_preference: { allowRead: true, allowCreate: true, allowEdit: true, allowDelete: false },
364+
// [#7344] The personal inbox, READ-ONLY. Same reasoning as the line above,
365+
// applied to the other pair of platform objects a platform app points every
366+
// authenticated member at: the Account app's `Inbox` group declares a
367+
// Notifications entry with `requiresObject: 'sys_inbox_message'`
368+
// (`packages/platform-objects/src/apps/account.app.ts`) and declares no
369+
// `requiredPermissions`, so the app is reachable by design while the object
370+
// behind the entry was named by no shipped set — every non-admin got
371+
// `403 PERMISSION_DENIED` and the bell's notification half was structurally
372+
// zero. `sys_notification_receipt` rides along because read-state lives
373+
// there (ADR-0030), not on the inbox row, so the entry needs both.
374+
//
375+
// Maintainer ruling (2026-08-11): READ grants only. Rows are produced by
376+
// the always-on `inbox` messaging channel keyed on the recipient
377+
// (`service-messaging/src/inbox-channel.ts`) under the service's own engine
378+
// access, and mark-read is served by `/api/v1/notifications` rather than the
379+
// generic data API — so no create/edit bit is needed by the flow, and
380+
// `allowDelete` stays false like everything else in this anchor-bound set.
381+
// `sys_activity` is deliberately NOT included: it is not a per-user-scoped
382+
// shape (no `user_id` to scope by), and it is a separate question if it ever
383+
// matters. Two NAMED additions in #5491's explicit-allow shape — the
384+
// `_self` policies below scope both to the caller — not a widening pattern
385+
// and not a step back toward a wildcard.
386+
sys_inbox_message: { allowRead: true, allowCreate: false, allowEdit: false, allowDelete: false },
387+
sys_notification_receipt: { allowRead: true, allowCreate: false, allowEdit: false, allowDelete: false },
364388
},
365389
rowLevelSecurity: [
366390
// [ADR-0095 D1] The wildcard `tenant_isolation` policy RETIRED here — the
@@ -508,6 +532,25 @@ const baseDefaultPermissionSets: PermissionSet[] = [
508532
operation: 'all',
509533
using: 'user_id == current_user.id',
510534
},
535+
// [#7344] The personal inbox (Account → Inbox → Notifications, and the
536+
// console bell). Neither object declares `organization_id`, so Layer 0 is
537+
// inert on them exactly as it is on `sys_oauth_application` above and these
538+
// `_self` policies ARE their row scoping — without them the read bit added
539+
// to `objects` would be org-wide, which is the one outcome the ruling's
540+
// "RLS-scoped to the caller" forbids. `select` (not `all`) because the
541+
// grants are read-only; the writer is the inbox channel, not the member.
542+
{
543+
name: 'sys_inbox_message_self',
544+
object: 'sys_inbox_message',
545+
operation: 'select',
546+
using: 'user_id == current_user.id',
547+
},
548+
{
549+
name: 'sys_notification_receipt_self',
550+
object: 'sys_notification_receipt',
551+
operation: 'select',
552+
using: 'user_id == current_user.id',
553+
},
511554
],
512555
}),
513556
PermissionSetSchema.parse({

packages/plugins/plugin-security/src/objects/rbac-objects.test.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,11 @@ describe('default permission sets', () => {
102102
'sys_account_self',
103103
'sys_api_key_self',
104104
'sys_device_code_self',
105+
// [#7344] The personal-inbox pair — not better-auth tables, but the same
106+
// `_self` shape for the same reason: no `organization_id`, so Layer 0 is
107+
// inert and these policies are the row scoping for their read grants.
108+
'sys_inbox_message_self',
109+
'sys_notification_receipt_self',
105110
'sys_oauth_access_token_self',
106111
'sys_oauth_application_self',
107112
'sys_oauth_consent_self',

0 commit comments

Comments
 (0)