5.3 KiB
partitions/partition-manager/partition-manager.ts
(no submodules)
Purpose
PartitionManager orchestrates the lifecycle of partition records. It accepts a PartitionStore for persistence and a KeyRing for per-user key material, validates every mutation against the active policy via validateAction, and emits partition:* and member:* events on a caller-supplied EventBusLike. The manager is the single throat through which all partition state changes pass; downstream modules (grants, tokens, transfer) treat the partition as immutable input.
Features
- Five mutating operations (
create,delete,addMember,removeMember,update) plus three read-only operations (get,list,getKeyRing). - Zod-validated inputs at every public entrypoint via
CreatePartitionOptionsSchema/AddMemberOptionsSchema/UpdatePartitionOptionsSchema. - Injectable
Clockfor deterministic timestamping and an injectable id generator for deterministic IDs in tests. EventBusLike<PartitionManagerEvents>carriespartition:created,partition:deleted,partition:updated,member:added,member:removed. No key material in event payloads.- OTel spans on every mutating operation; counter
share.partition.member_totalper partition; countershare.partition.create_total. - Graceful-degradation read-only toggle (
setReadOnly) — mutations throwSHARE_POLICY_VIOLATIONwhile the flag is true.
Usage: Basic Example
import { PartitionManager } from "@suaveplan/drive-sharing/partitions/partition-manager";
import { InMemoryPartitionStore } from "@suaveplan/drive-sharing/partitions/partition-store";
import { KeyRing } from "@suaveplan/drive-sharing/encryption/key-ring";
import { userId } from "@suaveplan/drive-sharing/types/common";
const manager = new PartitionManager({
store: new InMemoryPartitionStore(),
keyRing: new KeyRing(),
});
const partition = await manager.create({
name: "research",
type: "shared",
ownerId: userId("alice"),
});
Usage: Advanced Example
import { PartitionManager } from "@suaveplan/drive-sharing/partitions/partition-manager";
import { InMemoryPartitionStore } from "@suaveplan/drive-sharing/partitions/partition-store";
import { KeyRing } from "@suaveplan/drive-sharing/encryption/key-ring";
import { partitionId, userId } from "@suaveplan/drive-sharing/types/common";
let now = 0;
let counter = 0;
const events = new Map<string, unknown[]>();
const bus = {
on: () => () => {},
emit: (k: string, p: unknown) => {
const arr = events.get(k) ?? [];
arr.push(p);
events.set(k, arr);
},
};
const manager = new PartitionManager({
store: new InMemoryPartitionStore(),
keyRing: new KeyRing(),
clock: () => (now += 1000),
idGenerator: () => `id-${(counter += 1)}`,
events: bus,
});
const p = await manager.create({
name: "vault",
type: "encrypted",
ownerId: userId("alice"),
policy: { maxMembers: 3 },
});
await manager.addMember(p.id, userId("bob"), { permissions: ["read"] });
manager.setReadOnly(true);
// further mutations throw SHARE_POLICY_VIOLATION
API Reference
class PartitionManager
constructor(config: PartitionManagerConfig)create(options): Promise<Partition>get(id): Promise<Partition>— throwsSHARE_PARTITION_NOT_FOUND.list(): Promise<Partition[]>delete(id): Promise<void>addMember(id, userId, options?): Promise<Partition>— throwsSHARE_POLICY_VIOLATIONwhen policy denies.removeMember(id, userId): Promise<Partition>— idempotent.update(id, options): Promise<Partition>setReadOnly(value): void/isReadOnly(): booleangetKeyRing(): KeyRing
interface CreatePartitionOptions
{ name, type, ownerId, policy? }.
interface AddMemberOptions
{ permissions?, wrappedKey? }. Default permissions are ["read"].
interface UpdatePartitionOptions
{ name?, policy? }.
interface PartitionManagerEvents
partition:created / partition:deleted / partition:updated / member:added / member:removed. No key material.
interface PartitionManagerConfig
{ store, keyRing, policy?, clock?, events?, idGenerator? }.
Implementation Notes
- The default policy is computed once in the constructor (
createPolicy(config.policy ?? {})). Per-partition policies are layered on top viamergePoliciesincreateandupdate. addMembervalidates the partition's policy against the actionshare(the canonical "I am modifying membership" verb); when the policy denies, the call throwsSHARE_POLICY_VIOLATION. Importantly, the check is performed against the partition's CURRENT membership — adding the Nth member whenmaxMembersis N triggers the denial branch.removeMemberis intentionally idempotent. Calling it twice with the same userId returns the same partition the second time and does NOT emitmember:removed. This matches the spec scenario "RemoveMember restores count".setReadOnly(true)is the spec's documented graceful-degradation hook; the partition store's health check is the trigger but the toggle is exposed publicly so consumers can wire any policy they like.- Event emission is fire-and-forget — the
EventBusLike.emitcontract is synchronous and does not surface listener errors. Asynchronous side effects belong to the listener, not the manager.