Skip to content

Commit 5f5511f

Browse files
os-steveclaude
andauthored
docs(skills): data-hooks.md speaks ADR-0090 D3 vocabulary (#15177) (#15186)
The published catalog page stated the `session.roles` retirement three times and used the reserved word "role" as live vocabulary six more times. D3 makes "role" reserved-forbidden (capability = permission_set, distribution = position, hierarchy = business_unit); this page ships verbatim to third-party projects via `npx skills add`, so every live use teaches an AI reader the reserved word. Kept: the one retirement note (the `ctx.session` table row) and the one useful warning (never write `ctx.session?.roles?.includes`). Rewritten: the duplicate retirement note in the `HookContext` comment folds into "privilege is judged by the security service, never by a session claim"; the masking callout says "for a permission set or position"; the masking comment spells the rest in D3 vocabulary. The cross-object example no longer queries a field named `role` -- `sys_user.role` does exist as a legacy better-auth admin scalar, but ADR-0068 D2 stopped synthesizing it and its only writer was retired, so an example looking up "the admin" by it teaches authorization off a dead field; it now resolves a related user by id, which is what a hook actually does. Residue: 3 occurrences, all quoted history, which is what the follow-up baseline entry covers. Claude-Session: https://claude.ai/code/session_019RfFHiRCSs3JXLK4cwcfox Co-authored-by: Claude <noreply@anthropic.com>
1 parent 41b5a44 commit 5f5511f

1 file changed

Lines changed: 14 additions & 15 deletions

File tree

‎skills/objectstack-data/references/data-hooks.md‎

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -571,8 +571,8 @@ interface HookContext {
571571
organizationId?: string; // Active org — the single blessed name. Matches the
572572
// `organization_id` column + `current_user.organizationId` (RLS).
573573
// The former `tenantId` alias was removed in v16.
574-
// No `roles` here (retired in 17.0.0) — privilege is judged
575-
// by the security service, never by a role name in a hook.
574+
// Privilege is judged by the security service,
575+
// never by a session claim.
576576
accessToken?: string;
577577
isSystem?: boolean; // Elevated system context (engine self-writes).
578578
};
@@ -729,8 +729,8 @@ handler: async (ctx: HookContext) => {
729729
const users = ctx.api?.object('user');
730730

731731
// Query users — `where` is canonical (`filter` is a tolerated object alias)
732-
const admin = await users.findOne({
733-
where: { role: 'admin' }
732+
const owner = await users.findOne({
733+
where: { id: ctx.input.owner_id }
734734
});
735735

736736
// Create related record
@@ -819,11 +819,11 @@ const cascadeAccountUpdate = defineHook({
819819

820820
### 4. Data Masking on Read
821821

822-
> For **static** field masking (a field is always hidden/masked for a role),
823-
> prefer declarative **field-level metadata** (secret/masked fields) — it applies
824-
> on every read path automatically. Use an `afterFind` hook only for masking that
825-
> depends on runtime logic the field metadata can't express. A single `afterFind`
826-
> subscription covers both `find` and `findOne`.
822+
> For **static** field masking (a field is always hidden/masked for a permission
823+
> set or position), prefer declarative **field-level metadata** (secret/masked
824+
> fields) — it applies on every read path automatically. Use an `afterFind` hook
825+
> only for masking that depends on runtime logic the field metadata can't
826+
> express. A single `afterFind` subscription covers both `find` and `findOne`.
827827
828828
```typescript
829829
const maskSensitiveData = defineHook({
@@ -834,12 +834,11 @@ const maskSensitiveData = defineHook({
834834
// Exempt the engine's own elevated reads (`isSystem`) — internal writes
835835
// and self-reads must see the real values.
836836
//
837-
// ⚠️ Do NOT gate this on a role name. `ctx.session` carries no role list:
838-
// `session.roles` was declared for years, never produced by any engine
839-
// path, and retired in 17.0.0 — `ctx.session?.roles?.includes(…)`
840-
// was always `undefined`, so a mask written that way looked role-aware and
841-
// was not. A per-role exemption belongs in field-level permissions (the
842-
// callout above), which the read path applies for you.
837+
// ⚠️ Never gate this on a session claim: `ctx.session?.roles?.includes(…)`
838+
// is always `undefined` (see the ctx table above), so a mask written that
839+
// way never exempts anyone. A per-permission-set or per-position exemption
840+
// belongs in field-level permissions (the callout above), which the read
841+
// path applies for you.
843842
const isElevated = ctx.session?.isSystem === true;
844843

845844
if (!isElevated) {

0 commit comments

Comments
 (0)