@safeapi/safeapi
Version:
SafeAPI: Secure, deterministic, and tamper-resistant API policy engine for Node and browser.
118 lines (117 loc) • 4.46 kB
JavaScript
import { createSafeApiAuditPayload } from "./audit/SafeApiAuditPayload";
import { deepFreeze } from "./shared";
import { evaluateGuards } from "./SafeApiGuardEngine";
import { computePolicyHash, normalizePolicy, } from "./SafeApiPolicyLifecycle";
function createDefaultContext() {
return {
clientId: "unknown",
environment: "dev",
metadata: {},
};
}
/** @internal */
export class SafeApiEngine {
policyResolver;
guard;
audit;
hooks;
constructor(options) {
this.policyResolver = options.policyResolver;
this.guard = options.guardEvaluator;
this.audit = options.audit;
this.hooks = options.hooks;
}
async process(request, context) {
await this.invokePreValidate(request);
const effectiveContext = { ...createDefaultContext(), ...context };
const resolvedPolicy = await this.policyResolver.resolve(request, effectiveContext);
const policy = normalizePolicy(resolvedPolicy);
const policyHash = computePolicyHash(policy);
await this.invokePreExecute(request, policy);
// Phase 13.2: Guard evaluation
const guardMatrix = policy.guards ?? [];
const guardEval = evaluateGuards(guardMatrix, effectiveContext, { trace: true });
if (!guardEval.allowed) {
const response = Object.freeze({
requestId: request.requestId,
status: "blocked",
payload: request.payload,
reason: `Blocked by guard: ${guardEval.matchedGuard}`,
policyId: policy.policyId,
});
await this.invokePostExecute(response, effectiveContext);
if (this.audit) {
const auditPayload = createSafeApiAuditPayload({
ruleId: policy.policyId ?? policy.ruleSetId,
phase: "safeapi",
priority: 0,
eventType: "evaluation",
timestampMs: 1,
meta: {
safeApi: {
policyHash,
guardTrace: guardEval.guardTrace,
result: "deny",
},
},
});
await this.audit.record(request, effectiveContext, response, auditPayload);
}
return response;
}
// If allowed, continue with existing pipeline
const guardResult = await this.guard.evaluate(request, effectiveContext, policy);
const response = this.buildResponse(request, policy, guardResult);
await this.invokePostExecute(response, effectiveContext);
if (this.audit) {
const auditPayload = createSafeApiAuditPayload({
ruleId: policy.policyId ?? policy.ruleSetId,
phase: "safeapi",
priority: 0,
eventType: "evaluation",
timestampMs: 1,
meta: {
safeApi: {
policyHash,
guardTrace: guardEval.guardTrace,
result: "allow",
},
},
});
await this.audit.record(request, effectiveContext, response, auditPayload);
}
return response;
}
buildResponse(request, policy, guardResult) {
const status = guardResult.allowed
? guardResult.modifiedPayload !== undefined
? "modified"
: "allowed"
: "blocked";
return Object.freeze({
requestId: request.requestId,
status,
payload: guardResult.modifiedPayload ?? request.payload,
reason: guardResult.reason,
policyId: policy.policyId,
});
}
async invokePreValidate(request) {
const handler = this.hooks?.preValidate;
if (!handler)
return;
await handler(deepFreeze(request));
}
async invokePreExecute(request, policy) {
const handler = this.hooks?.preExecute;
if (!handler)
return;
await handler(deepFreeze(request), deepFreeze(policy));
}
async invokePostExecute(response, context) {
const handler = this.hooks?.postExecute;
if (!handler)
return;
await handler(deepFreeze(response), deepFreeze(context));
}
}