4.2 KiB
sharing/grants/grants.ts
(no submodules)
Purpose
GrantManager is the deterministic in-memory grant store. It holds Grant records keyed by GrantId and answers permission queries via check(grantId, userId, action). The check is exhaustive: existence, non-revocation, non-expiry, grantee identity, and permission inclusion are all verified, in that order, with the first failure short-circuiting the result. Every check increments share.grant.check_total; denials additionally increment share.grant.denied_total with a reason attribute so dashboards can split denials by cause.
Features
- Synchronous
check— sub-millisecond by design (no IO, no crypto). - Injectable
Clockso expiry tests are deterministic. EventBusLikeintegration:grant:createdandgrant:revokedevents carry full context but never key material.- Hard cap on active grants (
GRANT_MANAGER_MAX_ENTRIES = 10_000_000) per the spec scalability table. - Filtered
list(query)supportinggranteeId,partitionId, andactivefilters. revokeis idempotent — already-revoked / missing grants are no-ops.
Usage: Basic Example
import { GrantManager } from "@suaveplan/drive-sharing/sharing/grants";
import { partitionId, userId } from "@suaveplan/drive-sharing/types/common";
const grants = new GrantManager();
const grant = grants.create({
grantorId: userId("alice"),
granteeId: userId("bob"),
partitionId: partitionId("p"),
permissions: ["read"],
});
grants.check(grant.id, userId("bob"), "read"); // { allowed: true }
Usage: Advanced Example
import { GrantManager } from "@suaveplan/drive-sharing/sharing/grants";
import { partitionId, userId } from "@suaveplan/drive-sharing/types/common";
let now = 0;
const events = new Map<string, unknown[]>();
const grants = new GrantManager({
clock: () => now,
events: {
on: () => () => {},
emit: (k, p) => {
const arr = events.get(k) ?? [];
arr.push(p);
events.set(k, arr);
},
},
});
const g = grants.create({
grantorId: userId("alice"),
granteeId: userId("bob"),
partitionId: partitionId("p"),
permissions: ["read"],
expiresAt: 1000,
});
now = 999;
console.log(grants.check(g.id, userId("bob"), "read")); // allowed
now = 1001;
console.log(grants.check(g.id, userId("bob"), "read")); // denied: expired
grants.revoke(g.id);
console.log(grants.list({ active: true })); // []
API Reference
class GrantManager
constructor(config?: GrantManagerConfig)create(options: CreateGrantOptions): Grantcheck(id, userId, action): GrantCheckResultrevoke(id): voidlist(query?): Grant[]get(id): Grant | undefined
interface CreateGrantOptions
{ grantorId, granteeId, partitionId, permissions, expiresAt? }.
interface Grant
{ id, grantorId, granteeId, partitionId, permissions, createdAt, expiresAt?, revoked }.
interface GrantCheckResult
{ allowed: boolean; reason?: string } — reason is one of grant_not_found, revoked, expired, grantee_mismatch, permission_missing.
interface GrantQuery
{ granteeId?, partitionId?, active? }.
interface GrantManagerEvents
grant:created— full grantgrant:revoked—{ id, partitionId, userId }
Implementation Notes
- The denial-reason strings are stable contract — dashboards may key on them. Renaming a reason is a major-version bump.
revokeis intentionally idempotent so a duplicate "revoke this grant" message from a retry-loop does not trigger a secondgrant:revokedevent.list({ active: true })reads the clock once per grant; for very large grant stores callers should preferlist({ partitionId, active: true })so the filter narrows before the active-check fires.- The Zod schema (
CreateGrantOptionsSchema) enforces non-emptypermissions. An attacker who callscreatewithpermissions: []is rejected at the boundary, never silently inserted. checkruns the action verb throughpermissions.includes(action as SharePermission)— the cast is safe because everyPolicyActionis also a validSharePermissionin the canonical list.