From 7add396fca21f0e722fa7f0a6ee922f08f3e4703 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 4 Sep 2026 05:10:10 +0000 Subject: [PATCH] docs(skills): data-hooks.md speaks ADR-0090 D3 vocabulary (#15177) 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. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_019RfFHiRCSs3JXLK4cwcfox --- .../objectstack-data/references/data-hooks.md | 29 +++++++++---------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/skills/objectstack-data/references/data-hooks.md b/skills/objectstack-data/references/data-hooks.md index 5974b02269..b2f24f1ebe 100644 --- a/skills/objectstack-data/references/data-hooks.md +++ b/skills/objectstack-data/references/data-hooks.md @@ -571,8 +571,8 @@ interface HookContext { organizationId?: string; // Active org — the single blessed name. Matches the // `organization_id` column + `current_user.organizationId` (RLS). // The former `tenantId` alias was removed in v16. - // No `roles` here (retired in 17.0.0) — privilege is judged - // by the security service, never by a role name in a hook. + // Privilege is judged by the security service, + // never by a session claim. accessToken?: string; isSystem?: boolean; // Elevated system context (engine self-writes). }; @@ -729,8 +729,8 @@ handler: async (ctx: HookContext) => { const users = ctx.api?.object('user'); // Query users — `where` is canonical (`filter` is a tolerated object alias) - const admin = await users.findOne({ - where: { role: 'admin' } + const owner = await users.findOne({ + where: { id: ctx.input.owner_id } }); // Create related record @@ -819,11 +819,11 @@ const cascadeAccountUpdate = defineHook({ ### 4. Data Masking on Read -> For **static** field masking (a field is always hidden/masked for a role), -> prefer declarative **field-level metadata** (secret/masked fields) — it applies -> on every read path automatically. Use an `afterFind` hook only for masking that -> depends on runtime logic the field metadata can't express. A single `afterFind` -> subscription covers both `find` and `findOne`. +> For **static** field masking (a field is always hidden/masked for a permission +> set or position), prefer declarative **field-level metadata** (secret/masked +> fields) — it applies on every read path automatically. Use an `afterFind` hook +> only for masking that depends on runtime logic the field metadata can't +> express. A single `afterFind` subscription covers both `find` and `findOne`. ```typescript const maskSensitiveData = defineHook({ @@ -834,12 +834,11 @@ const maskSensitiveData = defineHook({ // Exempt the engine's own elevated reads (`isSystem`) — internal writes // and self-reads must see the real values. // - // ⚠️ Do NOT gate this on a role name. `ctx.session` carries no role list: - // `session.roles` was declared for years, never produced by any engine - // path, and retired in 17.0.0 — `ctx.session?.roles?.includes(…)` - // was always `undefined`, so a mask written that way looked role-aware and - // was not. A per-role exemption belongs in field-level permissions (the - // callout above), which the read path applies for you. + // ⚠️ Never gate this on a session claim: `ctx.session?.roles?.includes(…)` + // is always `undefined` (see the ctx table above), so a mask written that + // way never exempts anyone. A per-permission-set or per-position exemption + // belongs in field-level permissions (the callout above), which the read + // path applies for you. const isElevated = ctx.session?.isSystem === true; if (!isElevated) {