# `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 `Clock` so expiry tests are deterministic. - `EventBusLike` integration: `grant:created` and `grant:revoked` events 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)` supporting `granteeId`, `partitionId`, and `active` filters. - `revoke` is idempotent — already-revoked / missing grants are no-ops. ## Usage: Basic Example ```ts 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 ```ts import { GrantManager } from "@suaveplan/drive-sharing/sharing/grants"; import { partitionId, userId } from "@suaveplan/drive-sharing/types/common"; let now = 0; const events = new Map(); 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): Grant` - `check(id, userId, action): GrantCheckResult` - `revoke(id): void` - `list(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 grant - `grant: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. - `revoke` is intentionally idempotent so a duplicate "revoke this grant" message from a retry-loop does not trigger a second `grant:revoked` event. - `list({ active: true })` reads the clock once per grant; for very large grant stores callers should prefer `list({ partitionId, active: true })` so the filter narrows before the active-check fires. - The Zod schema (`CreateGrantOptionsSchema`) enforces non-empty `permissions`. An attacker who calls `create` with `permissions: []` is rejected at the boundary, never silently inserted. - `check` runs the action verb through `permissions.includes(action as SharePermission)` — the cast is safe because every `PolicyAction` is also a valid `SharePermission` in the canonical list.