@beignet/core
Version:
Core framework primitives for Beignet
172 lines • 5.56 kB
JavaScript
/**
* @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