Class: AccessService
Defined in: server/access/access-service.ts:45
The consumer-facing authorization service. Compiles a per-request
PermissionSnapshot, evaluates checks through the shared deny-by-default
kernel, derives a minimized CapabilitySnapshot for the client, pushes
indexable list filters into a Prisma where, and writes leveled decision
audit off the hot path.
Extends
Constructors
Constructor
new AccessService(deps: AccessFeatureDeps): AccessService;Defined in: server/access/base-access-service.ts:36
Parameters
| Parameter | Type |
|---|---|
deps | AccessFeatureDeps |
Returns
AccessService
Inherited from
Properties
deps
protected readonly deps: AccessFeatureDeps;Defined in: server/access/base-access-service.ts:34
Inherited from
Accessors
actorId
Get Signature
get protected actorId(): string | null;Defined in: server/access/base-access-service.ts:40
Returns
string | null
Inherited from
Methods
authorizedWhere()
authorizedWhere(
resourceType: string,
subject: Subject,
action: `${string}.${string}`,
options?: {
context?: AccessContext;
scope?: GrantScope;
}
): Promise<AuthorizedWhereResult>;Defined in: server/access/access-service.ts:180
Compile a SQL where for "list resources I can edit". SELECTORS ONLY —
the result is honest: complete when SQL fully decides, partial (with a
reason and postFilterRequired) when a non-indexable condition means the
caller must post-filter with filterAuthorized, and none when no grant
applies. Uses the defineResourceAuthz registry to know which selector
fields are indexable.
Parameters
| Parameter | Type |
|---|---|
resourceType | string |
subject | Subject |
action | `${string}.${string}` |
options? | { context?: AccessContext; scope?: GrantScope; } |
options.context? | AccessContext |
options.scope? | GrantScope |
Returns
Promise<AuthorizedWhereResult>
can()
can(
subject: Subject,
action: `${string}.${string}`,
resource?: object,
context?: AccessContext,
options?: {
consistency?: "eventual" | "strong";
resourceType?: string;
scope?: GrantScope;
}
): Promise<Decision>;Defined in: server/access/access-service.ts:51
Resolve can(subject, action, resource?, context?). Deny-by-default,
fully explainable. Every call compiles afresh; consistency: "strong"
does not change transaction isolation or lock authority.
Parameters
| Parameter | Type | Description |
|---|---|---|
subject | Subject | - |
action | `${string}.${string}` | - |
resource? | object | - |
context? | AccessContext | - |
options? | { consistency?: "eventual" | "strong"; resourceType?: string; scope?: GrantScope; } | - |
options.consistency? | "eventual" | "strong" | - |
options.resourceType? | string | Resource type, enforced against any typed grant selector (D). |
options.scope? | GrantScope | - |
Returns
Promise<Decision>
canAll()
canAll(
subject: Subject,
checks: readonly {
action: `${string}.${string}`;
context?: AccessContext;
resource?: object;
resourceType?: string;
}[],
options?: {
scope?: GrantScope;
}
): Promise<Decision[]>;Defined in: server/access/access-service.ts:106
Batch variant. H2: each check's scope is resolved from ITS OWN context,
identically to can() — not just checks[0]'s — so a mixed-scope batch
can never let one check's org grants authorize another check in a
different scope. Snapshots are still compiled once per distinct scope
CHAIN (cached by the joined chain keys), so the common same-scope batch
pays for exactly one compile, same as before.
Resolved sequentially, not via Promise.all: compileSnapshot runs
inside deps.repos.transaction, which uses
TransactionManager's AsyncLocalStorage to nest into any ambient
transaction. Firing them concurrently would issue overlapping queries
against the same transaction/connection (this is exactly how it breaks
inside a test's wrapping transaction, and would do the same inside a
consumer's own outer transaction).
Parameters
| Parameter | Type |
|---|---|
subject | Subject |
checks | readonly { action: `${string}.${string}`; context?: AccessContext; resource?: object; resourceType?: string; }[] |
options? | { scope?: GrantScope; } |
options.scope? | GrantScope |
Returns
Promise<Decision[]>
coarsePermissionsForActor()
protected coarsePermissionsForActor(
actor: {
id: string;
type: SubjectType;
},
scope: GrantScope,
now?: Date
): Promise<CoarseActorAuthority>;Defined in: server/access/base-access-service.ts:117
H3: derive an actor's UNCONDITIONALLY-held authority for a target scope —
the delegable set for both GrantService.create's CreateGrantGuard and
RoleService.createVersion's CreateVersionGuard. Only grants the actor
holds with NO selector and NO condition contribute, mirroring
hasCoarsePermission: a narrowly-held permission must not be re-delegable
(as a grant) or re-addable (to a role) unconstrained. Effective-date
validity is honored. Shared here (not duplicated per service) so the two
escalation guards can never drift on what "coarsely held" means.
Derived over scopeChain(scope), so a platform admin can delegate inside
an organization without mode: "system" (which would skip every
escalation guard). The chain is derived here, never accepted as a
parameter — see scopeChain.
scopeKeys reports PROVENANCE: which scopes actually backed the actor.
That is what GrantService's self-org cap reads instead of a
caller-asserted actorOrganizationId, making it server-derived and
unspoofable.
Parameters
| Parameter | Type |
|---|---|
actor | { id: string; type: SubjectType; } |
actor.id | string |
actor.type | SubjectType |
scope | GrantScope |
now | Date |
Returns
Promise<CoarseActorAuthority>
Inherited from
BaseAccessService.coarsePermissionsForActor
compileSnapshot()
compileSnapshot(
subject: Subject,
scopes: GrantScope[],
_options?: {
consistency?: "eventual" | "strong";
}
): Promise<PermissionSnapshot>;Defined in: server/access/access-service.ts:342
Load the subject's (and their groups') active grants → roles → flat
permission sets into a PermissionSnapshot. In v1 this ALWAYS compiles
fresh from the DB — the versioned snapshot cache and per-subject
grant-version counter are deferred to v2 (see docs/rbac/design.md). Because
every compile reads the current transaction-visible state, revocation needs no cache invalidation and
consistency: "strong" is an accepted NO-OP. The signature (and the
consistency option on can/etc.) is preserved so a v2 cache can be
reintroduced behind it without touching call sites.
Parameters
| Parameter | Type |
|---|---|
subject | Subject |
scopes | GrantScope[] |
_options? | { consistency?: "eventual" | "strong"; } |
_options.consistency? | "eventual" | "strong" |
Returns
Promise<PermissionSnapshot>
filterAuthorized()
filterAuthorized<T extends object>(
subject: Subject,
resources: readonly T[],
action: `${string}.${string}`,
options?: {
context?: AccessContext;
resourceType?: string;
scope?: GrantScope;
}
): Promise<T[]>;Defined in: server/access/access-service.ts:147
Filter an in-memory list to the resources the subject is authorized for.
Type Parameters
| Type Parameter |
|---|
T extends object |
Parameters
| Parameter | Type | Description |
|---|---|---|
subject | Subject | - |
resources | readonly T[] | - |
action | `${string}.${string}` | - |
options? | { context?: AccessContext; resourceType?: string; scope?: GrantScope; } | - |
options.context? | AccessContext | - |
options.resourceType? | string | Resource type, enforced against any typed grant selector (D). |
options.scope? | GrantScope | - |
Returns
Promise<T[]>
getCapabilitySnapshot()
getCapabilitySnapshot(
subject: Subject,
actions: readonly `${string}.${string}`[],
options?: {
context?: AccessContext;
scope?: GrantScope;
}
): Promise<CapabilitySnapshot>;Defined in: server/access/access-service.ts:286
Compile a snapshot to the minimized, client-safe CapabilitySnapshot. Only
coarse global capabilities (unconstrained grants) are exposed — never the
raw grant/condition graph. actions narrows the map to the consumer's
known permission set.
Parameters
| Parameter | Type |
|---|---|
subject | Subject |
actions | readonly `${string}.${string}`[] |
options? | { context?: AccessContext; scope?: GrantScope; } |
options.context? | AccessContext |
options.scope? | GrantScope |
Returns
Promise<CapabilitySnapshot>
grantsForSubject()
grantsForSubject(subject: Subject, scope: GrantScope): Promise<CompiledGrant[]>;Defined in: server/access/access-service.ts:315
"What roles/grants does subject X hold IN THIS SCOPE?"
Deliberately EXACT — the only read on this service that does NOT walk the scope chain. The chain answers "may this subject act here?"; this answers "does this subject hold something here specifically?", and provisioners depend on the difference. A consumer that grants an org-scoped admin role only when it is absent would, under a unioned read, see the subject's PLATFORM admin grant, conclude the role is present, and silently never create the org-scoped grant. Do not "fix" the inconsistency.
Parameters
| Parameter | Type |
|---|---|
subject | Subject |
scope | GrantScope |
Returns
Promise<CompiledGrant[]>
loadActiveGrants()
protected loadActiveGrants(
repos: AccessRepositories,
subject: Subject,
scopes: GrantScope[]
): Promise<{
conditionLogic: JsonValue;
createdAt: Date;
createdBy: string | null;
deletedAt: Date | null;
deletedBy: string | null;
effectiveEnd: Date | null;
effectiveStart: Date | null;
grantFingerprint: string;
id: string;
organizationId: string | null;
roleId: string;
scopeKey: string;
scopeType: RbacScopeType;
selector: JsonValue;
subjectId: string;
subjectType: RbacSubjectType;
updatedAt: Date;
updatedBy: string | null;
}[]>;Defined in: server/access/base-access-service.ts:70
Security-critical lookup: load a subject's active grants across a set of scopes, expanding org-group membership for non-group subjects. The single source of truth for "which grants apply to this subject right now" — every caller (snapshot compilation, coarse-permission derivation) funnels here so the scopeKey derivation and group-expansion rule can never drift between call sites.
Queried one scope at a time rather than with a single scopeKey IN (…):
group expansion is a PER-SCOPE rule, not a property of the subject. It runs
only for an organization-scoped lookup AND a non-group subject — a
group subject's grants are read directly (it has no "groups" of its own to
expand), and platform scope has no org-group notion, so a group's
platform-scoped grant must NOT reach its members through an org-scoped
chain. Flattening the chain into one query with a unioned groupIds would
silently widen exactly that.
Must be called inside a repos.transaction(...) so the membership reads
and the grant reads share one transaction.
Parameters
| Parameter | Type |
|---|---|
repos | AccessRepositories |
subject | Subject |
scopes | GrantScope[] |
Returns
Promise<{
conditionLogic: JsonValue;
createdAt: Date;
createdBy: string | null;
deletedAt: Date | null;
deletedBy: string | null;
effectiveEnd: Date | null;
effectiveStart: Date | null;
grantFingerprint: string;
id: string;
organizationId: string | null;
roleId: string;
scopeKey: string;
scopeType: RbacScopeType;
selector: JsonValue;
subjectId: string;
subjectType: RbacSubjectType;
updatedAt: Date;
updatedBy: string | null;
}[]>
Inherited from
BaseAccessService.loadActiveGrants
subjectsForRole()
subjectsForRole(roleId: string): Promise<{
scopeKey: string;
subjectId: string;
subjectType: SubjectType;
}[]>;Defined in: server/access/access-service.ts:324
"Which subjects hold role R?"
Parameters
| Parameter | Type |
|---|---|
roleId | string |
Returns
Promise<{
scopeKey: string;
subjectId: string;
subjectType: SubjectType;
}[]>
transaction()
protected transaction<T>(fn: (deps: AccessFeatureDeps) => Promise<T>): Promise<T>;Defined in: server/access/base-access-service.ts:44
Type Parameters
| Type Parameter |
|---|
T |
Parameters
| Parameter | Type |
|---|---|
fn | (deps: AccessFeatureDeps) => Promise<T> |
Returns
Promise<T>