services.sharing
Record-level sharing and editability checks.
services.sharing
- Stability:
stable - Canonical source:
packages/spec/src/contracts/sharing-service.ts
Methods
services.sharing.buildReadFilter(object: string, context: SharingExecutionContext): Promise<unknown | null>
services.sharing.canEdit(object: string, recordId: string, context: SharingExecutionContext): Promise<boolean>
services.sharing.canDelete(object: string, recordId: string, context: SharingExecutionContext): Promise<boolean>
services.sharing.canManageShares(object: string, recordId: string, context: SharingExecutionContext): Promise<boolean>
services.sharing.grant(input: GrantShareInput, context: SharingExecutionContext): Promise<RecordShare>
services.sharing.revoke(shareId: string, context: SharingExecutionContext, scope?: { object: string; recordId: string }): Promise<void>
services.sharing.listShares(object: string, recordId: string, context: SharingExecutionContext): Promise<RecordShare[]>Management authority (ADR-0111)
grant / revoke / listShares are management operations, enforced in the
service for every non-system caller: the caller must hold canManageShares on
the record — its owner, a holder of Modify All Data, a hierarchy
manager whose effective write DEPTH (unit / unit_and_below /
own_and_reports) covers the record's owner (via the enterprise
hierarchy-scope-resolver), or system context. A deployment without
@objectstack/plugin-security fails closed to owner-only; without the
enterprise resolver the DEPTH path is inert and authority stays owner +
Modify-All. Pass { isSystem: true } only from platform-internal machinery.
The verb boundary (ADR-0111 D3)
A share widens which rows a principal reaches, never which verbs they may
use. canEdit (the update gate) accepts an edit-level share; canDelete
(the delete gate) does not — delete is ownership (widened by write
DEPTH) or the modifyAllRecords super-user bypass only. Delete is not a share
level and never will be; a future per-record delete grant would be a capability
mask AND-ed with object CRUD, not a fourth access_level.
Returns
buildReadFilter:nullmeans unrestricted read; otherwise returns an engine filtercanEdit/canDelete/canManageShares: boolean decisions (they returnfalserather than throwing)grant/listShares: normalizedRecordSharerows
Typical Errors
FORBIDDEN(403) — a write denied by thecanEditgate. Thrown by the sharing engine middleware;canEdititself returnsfalserather than throwing.VALIDATION_FAILED(400) —grant/revokecalled without a required field (object,recordId,recipientId, orshareId), orgrantwith a non-userrecipientType(onlyuserrows are enforced by the gates; group/position recipients are delivered via sharing rules).PERMISSION_DENIED(403) — the caller does not holdcanManageShareson the record (ADR-0111 D1).NOT_FOUND(404) — the record is missing or not visible to the caller (indistinguishable by design), or arevokeshare id does not exist / does not belong to thescoperecord.CONFLICT(409) —revokeon a rule-materialised share (source != 'manual'); the next rule reconciliation would silently re-grant it. Deactivate or edit the sharing rule instead.SHARING_NOT_ENABLED(422) —granton an object the sharing gates never consult (public sharing model, noowner_idfield, a bypass object, orcontrolled_by_parent).
Example
const allowed = await services.sharing.canEdit('contract', ctx.input.id, {
userId: ctx.session?.userId,
// The hook exposes the caller's org as `organizationId` (the `session.tenantId`
// alias was removed in v11, #3290); it feeds the sharing context's `tenantId`.
tenantId: ctx.session?.organizationId,
positions: ctx.session?.positions,
});
if (!allowed) throw new Error('PERMISSION_DENIED');