/** * Whether an entity's plan (its `Config.Features`) allows a capability. Shared by * MainAPI and MainUI so the two give the same answer -- MainAPI * docs/planning/login-authz-payload.md. MainAPI enforces; MainUI only hides. * * Pure and dependency-free. The caller passes the feature list and the rule * table: the table is owned by MainAPI * (`src/core/authorization/catalog/capability-feature-requirements.ts`) and * generated into MainUI, so it is never copied into this package. * * The plan only limits. It never grants: the caller still checks SA, EA or the * user's grant. */ export const PlanFeatureModes = Object.freeze({ ACTIVE: "ACTIVE", READ_ONLY: "READ_ONLY", } as const); /** The entity's `Config.Features`: `{ Key, Mode }` entries; an absent key is a feature the entity lacks. */ export type PlanFeatureList = readonly { readonly Key?: unknown; readonly Mode?: unknown }[] | null | undefined; /** * The features one capability resource (the key without its action) needs. * - `Features`: all are required. * - `ReadOnlySafe`: the actions still allowed when a required feature is READ_ONLY. * - `ActionFeatures`: extra features one action needs. */ export interface CapabilityPlanRequirement { readonly Features: readonly string[]; readonly ReadOnlySafe: readonly string[]; readonly ActionFeatures?: Readonly>; } export type CapabilityPlanRequirements = Readonly>; function OwnValue(record: Readonly> | null | undefined, key: string): T | undefined { return record != null && Object.prototype.hasOwnProperty.call(record, key) ? record[key] : undefined; } /** * The feature-mode primitive: ACTIVE allows any use, READ_ONLY only a * read-only-safe one, an absent feature nothing. Direct feature guards (keyless * routes, document-dependent writes) call this; `IsAllowedByPlan` is built on it. */ export function FeatureModeAllows(features: PlanFeatureList, featureKey: string, readOnlySafe: boolean): boolean { if (!Array.isArray(features)) { return false; } const mode = features.find(feature => feature?.Key === featureKey)?.Mode; return mode === PlanFeatureModes.ACTIVE || (mode === PlanFeatureModes.READ_ONLY && readOnlySafe); } /** * Whether the plan allows a capability key (`area.resource.action`). A key whose * resource has no entry in `requirements` is denied: every tenant capability has * one, with `Features: []` when it needs none. Capabilities outside the table * (MainAPI's platform keys) must be exempted by the caller before calling this. */ export function IsAllowedByPlan( capabilityKey: string, features: PlanFeatureList, requirements: CapabilityPlanRequirements, ): boolean { if (typeof capabilityKey !== "string") { return false; } const split = capabilityKey.lastIndexOf("."); if (split <= 0 || split === capabilityKey.length - 1) { return false; } const requirement = OwnValue(requirements, capabilityKey.slice(0, split)); if (requirement == null) { return false; } const action = capabilityKey.slice(split + 1); const readOnlySafe = requirement.ReadOnlySafe.includes(action); const required = [...requirement.Features, ...(OwnValue(requirement.ActionFeatures, action) ?? [])]; return required.every(featureKey => FeatureModeAllows(features, featureKey, readOnlySafe)); }