UNPKG

@beignet/core

Version:

Core framework primitives for Beignet

172 lines 5.56 kB
/** * @beignet/core/entitlements * * Provider-neutral product access primitives for Beignet applications. */ /** * Error thrown by `requireEntitlement(...)` when product access is denied. */ export class EntitlementRequiredError extends Error { code; status = 403; details; entitlement; subject; constructor(input, decision = denyEntitlement()) { super(decision.reason ?? `Entitlement "${input.entitlement}" is required for this action.`); this.name = "EntitlementRequiredError"; this.code = decision.code ?? "ENTITLEMENT_REQUIRED"; this.details = decision.details; this.entitlement = input.entitlement; this.subject = input.subject; } } /** * Create an explicit allow decision. */ export function allowEntitlement() { return { allowed: true }; } /** * Create an explicit deny decision. */ export function denyEntitlement(reasonOrDecision) { if (typeof reasonOrDecision === "string") { return { allowed: false, reason: reasonOrDecision }; } return { allowed: false, ...reasonOrDecision, }; } /** * Create an entitlement port from an app-owned resolver. */ export function createEntitlements(options) { const observeDecision = createDecisionObserver(options.onDecision); async function evaluate(input, checkOptions) { const startedAt = entitlementNow(); try { const decision = normalizeEntitlementDecision(await options.inspect(input)); observeDecision({ source: checkOptions?.source ?? "inspect", ctx: checkOptions?.ctx, entitlement: input.entitlement, subject: input.subject, decision, durationMs: entitlementNow() - startedAt, }); return decision; } catch (error) { observeDecision({ source: checkOptions?.source ?? "inspect", ctx: checkOptions?.ctx, entitlement: input.entitlement, subject: input.subject, error, durationMs: entitlementNow() - startedAt, }); throw error; } } return { async can(input, checkOptions) { return (await evaluate(input, { ...checkOptions, source: "can" })) .allowed; }, inspect(input, checkOptions) { return evaluate(input, { ...checkOptions, source: checkOptions?.source ?? "inspect", }); }, }; } /** * Create a static entitlement port for tests, starters, and simple apps. */ export function createStaticEntitlements(options) { const subjectKey = options.subjectKey ?? defaultSubjectKey; return createEntitlements({ inspect(input) { const granted = options.grants[subjectKey(input.subject)] ?? []; if (granted.includes(input.entitlement)) return allowEntitlement(); return denyEntitlement({ reason: `Entitlement "${input.entitlement}" is not granted.`, code: "ENTITLEMENT_NOT_GRANTED", details: { entitlement: input.entitlement, subject: input.subject, }, ...options.denied, }); }, onDecision: options.onDecision, }); } /** * Require an entitlement or throw a framework-owned 403 error. */ export async function requireEntitlement(ctx, input, options) { const decision = await ctx.ports.entitlements.inspect(input, { ctx, source: "require", }); if (decision.allowed) return decision; throw options?.error ? options.error(decision, input) : new EntitlementRequiredError(input, decision); } function normalizeEntitlementDecision(result) { if (typeof result === "boolean") { return result ? allowEntitlement() : denyEntitlement(); } return result; } function entitlementNow() { return typeof performance !== "undefined" ? performance.now() : Date.now(); } function contextStringField(ctx, field) { if (!ctx || typeof ctx !== "object") return undefined; const value = ctx[field]; return typeof value === "string" ? value : undefined; } function enrichObservation(observation) { return { ...observation, requestId: contextStringField(observation.ctx, "requestId"), traceId: contextStringField(observation.ctx, "traceId"), spanId: contextStringField(observation.ctx, "spanId"), parentSpanId: contextStringField(observation.ctx, "parentSpanId"), traceparent: contextStringField(observation.ctx, "traceparent"), }; } function isPromiseLike(value) { return (value !== null && (typeof value === "object" || typeof value === "function") && typeof value.then === "function"); } function createDecisionObserver(observer) { return (observation) => { if (!observer) return; try { const result = observer(enrichObservation(observation)); if (isPromiseLike(result)) { Promise.resolve(result).catch(() => undefined); } } catch { // Observers are diagnostic only and must not affect entitlement checks. } }; } function defaultSubjectKey(subject) { return `${subject.type}:${subject.id}`; } //# sourceMappingURL=index.js.map