// A RESOURCE-SCOPED guard: "owner of THIS team", not "owner globally". // // The `resource` extractor pulls the resource id out of the decoded input; the // framework then asks rbac's `resolveResourceRoles` (app.config.ts) which roles // the caller holds on that specific id, compiles them through the SAME role // map, and grants the scope only if the compiled set contains it. // // The order is worth knowing, because it decides how much your membership // lookup gets hit: a GLOBALLY held scope (or `admin:full`) satisfies the guard // without the resolver ever being called. Only the gap — "no global grant" — // falls through to the per-resource question. And that fall-through is // fail-CLOSED: a resolver that throws or rejects denies, it does not degrade // to the global answer. // // Without this, an app has to choose between a global grant (too broad) and // hand-rolled membership checks inside every handler (untestable, easy to // forget, invisible to `voltro check`). import { defineMutation } from '@voltro/protocol' import { Schema } from 'effect' export const renameTeam = defineMutation({ name: 'teams.rename', target: { table: 'teams', op: 'update' }, guards: [{ scope: 'teams:rename', resource: (input: { teamId: string }) => input.teamId, }], input: Schema.Struct({ teamId: Schema.NonEmptyString, name: Schema.NonEmptyString, }), output: Schema.Struct({ id: Schema.String, name: Schema.String, }), })