docs: enterprise refactor plan with ralph specs
This commit is contained in:
137
dist/core/AnomalyDetector.js
vendored
Normal file
137
dist/core/AnomalyDetector.js
vendored
Normal file
@@ -0,0 +1,137 @@
|
||||
"use strict";
|
||||
/**
|
||||
* AnomalyDetector — heuristic rules to detect anomalies from observations.
|
||||
* Each rule is independent and testable in isolation.
|
||||
*/
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.AnomalyDetector = void 0;
|
||||
let anomalyCounter = 0;
|
||||
function makeId() {
|
||||
anomalyCounter += 1;
|
||||
return `anom_${Date.now()}_${anomalyCounter.toString().padStart(4, '0')}`;
|
||||
}
|
||||
class AnomalyDetector {
|
||||
detect(observation, actionTrace) {
|
||||
const anomalies = [];
|
||||
const httpAnomaly = this.checkHttpErrors(observation, actionTrace);
|
||||
if (httpAnomaly)
|
||||
anomalies.push(httpAnomaly);
|
||||
const jsAnomaly = this.checkJsExceptions(observation, actionTrace);
|
||||
if (jsAnomaly)
|
||||
anomalies.push(jsAnomaly);
|
||||
const consoleAnomaly = this.checkConsoleErrors(observation, actionTrace);
|
||||
if (consoleAnomaly)
|
||||
anomalies.push(consoleAnomaly);
|
||||
return anomalies;
|
||||
}
|
||||
/** Rule: HTTP 4xx or 5xx responses */
|
||||
checkHttpErrors(observation, actionTrace) {
|
||||
const errorResponses = observation.httpResponses.filter((r) => r.status >= 400);
|
||||
if (errorResponses.length === 0)
|
||||
return null;
|
||||
const hasServerError = errorResponses.some((r) => r.status >= 500);
|
||||
const severity = hasServerError ? 'high' : 'medium';
|
||||
const statusCodes = errorResponses.map((r) => r.status).join(', ');
|
||||
return this.buildAnomaly({
|
||||
type: 'http_error',
|
||||
severity,
|
||||
observationId: observation.id,
|
||||
actionTrace,
|
||||
description: `HTTP error responses detected: ${statusCodes}`,
|
||||
evidence: {
|
||||
httpLog: errorResponses,
|
||||
rawErrors: errorResponses.map((r) => `${r.method} ${r.url} → ${r.status} (${r.durationMs}ms)`),
|
||||
},
|
||||
});
|
||||
}
|
||||
/** Rule: uncaught JS exceptions */
|
||||
checkJsExceptions(observation, actionTrace) {
|
||||
if (observation.jsExceptions.length === 0)
|
||||
return null;
|
||||
return this.buildAnomaly({
|
||||
type: 'js_exception',
|
||||
severity: 'high',
|
||||
observationId: observation.id,
|
||||
actionTrace,
|
||||
description: `Uncaught JS exception: ${observation.jsExceptions[0]}`,
|
||||
evidence: {
|
||||
rawErrors: observation.jsExceptions,
|
||||
},
|
||||
});
|
||||
}
|
||||
/** Rule: console.error messages */
|
||||
checkConsoleErrors(observation, actionTrace) {
|
||||
if (observation.consoleErrors.length === 0)
|
||||
return null;
|
||||
return this.buildAnomaly({
|
||||
type: 'console_error',
|
||||
severity: 'low',
|
||||
observationId: observation.id,
|
||||
actionTrace,
|
||||
description: `Console error detected: ${observation.consoleErrors[0]}`,
|
||||
evidence: {
|
||||
rawErrors: observation.consoleErrors,
|
||||
},
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Rule: server accepted clearly invalid/empty fuzz input (got 2xx).
|
||||
* fuzzedValue is the value that was submitted; responseStatus is the HTTP response.
|
||||
*/
|
||||
checkValidationBypass(observation, actionTrace, fuzzedValue) {
|
||||
const has2xx = observation.httpResponses.some((r) => r.status >= 200 && r.status < 300);
|
||||
if (!has2xx)
|
||||
return null;
|
||||
return this.buildAnomaly({
|
||||
type: 'validation_bypass',
|
||||
severity: 'high',
|
||||
observationId: observation.id,
|
||||
actionTrace,
|
||||
description: `Server accepted invalid input without error (value: ${JSON.stringify(fuzzedValue).substring(0, 50)})`,
|
||||
evidence: { httpLog: observation.httpResponses, rawErrors: [`Fuzzed with: ${fuzzedValue}`] },
|
||||
});
|
||||
}
|
||||
/** Rule: server returned 500 on a fuzzed input */
|
||||
checkServerErrorOnFuzz(observation, actionTrace) {
|
||||
const has5xx = observation.httpResponses.some((r) => r.status >= 500);
|
||||
if (!has5xx)
|
||||
return null;
|
||||
return this.buildAnomaly({
|
||||
type: 'server_error_on_fuzz',
|
||||
severity: 'high',
|
||||
observationId: observation.id,
|
||||
actionTrace,
|
||||
description: 'Server returned 5xx on fuzzed input',
|
||||
evidence: {
|
||||
httpLog: observation.httpResponses.filter((r) => r.status >= 500),
|
||||
rawErrors: observation.jsExceptions,
|
||||
},
|
||||
});
|
||||
}
|
||||
/** Rule: fuzzed script tag appears in response body (XSS reflection) */
|
||||
checkXssReflection(observation, actionTrace, domSnapshot) {
|
||||
if (!domSnapshot.includes('<script>alert(1)</script>'))
|
||||
return null;
|
||||
return this.buildAnomaly({
|
||||
type: 'xss_reflection',
|
||||
severity: 'critical',
|
||||
observationId: observation.id,
|
||||
actionTrace,
|
||||
description: 'XSS reflection detected: fuzzed script tag appeared in DOM',
|
||||
evidence: { rawErrors: ['XSS payload reflected in DOM'] },
|
||||
});
|
||||
}
|
||||
buildAnomaly(params) {
|
||||
return {
|
||||
id: makeId(),
|
||||
type: params.type,
|
||||
severity: params.severity,
|
||||
observationId: params.observationId,
|
||||
actionTrace: params.actionTrace,
|
||||
description: params.description,
|
||||
evidence: params.evidence,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
}
|
||||
}
|
||||
exports.AnomalyDetector = AnomalyDetector;
|
||||
53
dist/core/ExplorationConfig.js
vendored
Normal file
53
dist/core/ExplorationConfig.js
vendored
Normal file
@@ -0,0 +1,53 @@
|
||||
"use strict";
|
||||
/**
|
||||
* ExplorationConfig — defines scope, auth, fuzzing, multi-browser, a11y,
|
||||
* performance, visual regression, and network chaos settings for a session.
|
||||
*/
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.DEFAULT_EXPLORATION_CONFIG = exports.NETWORK_PROFILES = void 0;
|
||||
exports.NETWORK_PROFILES = {
|
||||
'fast-3g': { downloadKbps: 1500, uploadKbps: 750, latencyMs: 40, offline: false },
|
||||
'slow-3g': { downloadKbps: 400, uploadKbps: 150, latencyMs: 400, offline: false },
|
||||
'2g': { downloadKbps: 50, uploadKbps: 30, latencyMs: 800, offline: false },
|
||||
'offline': { downloadKbps: 0, uploadKbps: 0, latencyMs: 0, offline: true },
|
||||
'none': null,
|
||||
};
|
||||
exports.DEFAULT_EXPLORATION_CONFIG = {
|
||||
allowedDomains: [],
|
||||
maxStates: 50,
|
||||
maxDepth: 5,
|
||||
actionDelayMs: 500,
|
||||
sessionTimeoutMs: 300000,
|
||||
excludedPaths: [],
|
||||
excludedSelectors: [],
|
||||
auth: null,
|
||||
fuzzingEnabled: true,
|
||||
fuzzingIntensity: 'medium',
|
||||
browsers: ['chromium'],
|
||||
mobileDevice: 'none',
|
||||
viewport: null,
|
||||
accessibility: {
|
||||
enabled: true,
|
||||
minImpact: 'serious',
|
||||
wcagLevel: 'AA',
|
||||
},
|
||||
performance: {
|
||||
enabled: true,
|
||||
lcpThresholdMs: 4000,
|
||||
clsThreshold: 0.25,
|
||||
inpThresholdMs: 500,
|
||||
ttfbThresholdMs: 1800,
|
||||
},
|
||||
visualRegression: {
|
||||
enabled: false,
|
||||
threshold: 0.001,
|
||||
screenshotFullPage: false,
|
||||
ignoreSelectors: [],
|
||||
},
|
||||
networkChaos: {
|
||||
enabled: false,
|
||||
profile: 'none',
|
||||
blockedEndpoints: [],
|
||||
slowEndpoints: [],
|
||||
},
|
||||
};
|
||||
197
dist/core/ExplorationEngine.js
vendored
Normal file
197
dist/core/ExplorationEngine.js
vendored
Normal file
@@ -0,0 +1,197 @@
|
||||
"use strict";
|
||||
/**
|
||||
* ExplorationEngine — the core loop of ABE.
|
||||
* Selects states, executes actions, records observations, and detects anomalies.
|
||||
* Depends only on core interfaces — never imports concrete plugins.
|
||||
*/
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.ExplorationEngine = void 0;
|
||||
const AnomalyDetector_1 = require("./AnomalyDetector");
|
||||
const Logger_1 = require("./Logger");
|
||||
class ExplorationEngine {
|
||||
constructor(config) {
|
||||
/** Accumulated action trace for the current session */
|
||||
this.actionTrace = [];
|
||||
/** Set to true to abort the running loop */
|
||||
this.aborted = false;
|
||||
this.graph = config.graph;
|
||||
this.agent = config.agent;
|
||||
this.detector = config.detector ?? new AnomalyDetector_1.AnomalyDetector();
|
||||
this.collectors = config.collectors ?? [];
|
||||
this.exporters = config.exporters ?? [];
|
||||
this.reproducer = config.reproducer;
|
||||
this.logger = config.logger ?? new Logger_1.NullLogger();
|
||||
this.seed = config.seed;
|
||||
this.url = config.url;
|
||||
this.maxSteps = config.maxSteps ?? 100;
|
||||
this.outputDir = config.outputDir ?? './reports';
|
||||
this.events = config.events ?? {};
|
||||
this.sessionId = config.sessionId ?? `${Date.now()}_${config.seed}`;
|
||||
this.explorationConfig = config.explorationConfig ?? {};
|
||||
this.fuzzingPlugin = config.fuzzingPlugin;
|
||||
this.stateHooks = config.stateHooks ?? [];
|
||||
}
|
||||
/** Signals the engine to stop after the current step completes. */
|
||||
stop() {
|
||||
this.aborted = true;
|
||||
}
|
||||
async run() {
|
||||
const anomalies = [];
|
||||
let stepsExecuted = 0;
|
||||
let depth = 0;
|
||||
const sessionTimeoutMs = this.explorationConfig.sessionTimeoutMs ?? 0;
|
||||
const maxDepth = this.explorationConfig.maxDepth ?? Infinity;
|
||||
const sessionStart = Date.now();
|
||||
this.logger.log({
|
||||
event: 'session_start',
|
||||
timestamp: sessionStart,
|
||||
seed: this.seed,
|
||||
target: this.url,
|
||||
});
|
||||
this.events.onSessionStarted?.(this.sessionId, this.url);
|
||||
const isTimedOut = () => sessionTimeoutMs > 0 && Date.now() - sessionStart >= sessionTimeoutMs;
|
||||
try {
|
||||
await this.agent.launch(this.url);
|
||||
// Capture initial state
|
||||
const initialState = await this.agent.captureState();
|
||||
this.graph.addState(initialState);
|
||||
this.logger.log({
|
||||
event: 'state_discovered',
|
||||
timestamp: Date.now(),
|
||||
stateId: initialState.id,
|
||||
url: initialState.url,
|
||||
title: initialState.title,
|
||||
});
|
||||
this.events.onStateDiscovered?.(this.sessionId, initialState.id, initialState.url, initialState.title);
|
||||
while (stepsExecuted < this.maxSteps && !this.aborted && !isTimedOut() && depth <= maxDepth) {
|
||||
const currentState = this.graph.getNextToExplore();
|
||||
if (!currentState)
|
||||
break;
|
||||
// Mark state as being explored
|
||||
this.graph.incrementVisit(currentState.id);
|
||||
// Discover available actions in this state
|
||||
const actions = await this.agent.discoverActions(currentState);
|
||||
if (actions.length === 0)
|
||||
continue;
|
||||
// Select action deterministically using seed + step count
|
||||
const actionIndex = (this.seed + stepsExecuted) % actions.length;
|
||||
const action = actions[actionIndex];
|
||||
this.logger.log({
|
||||
event: 'action_executed',
|
||||
timestamp: Date.now(),
|
||||
actionId: action.id,
|
||||
type: action.type,
|
||||
selector: action.selector,
|
||||
value: action.value,
|
||||
url: action.url,
|
||||
});
|
||||
this.events.onActionExecuted?.(this.sessionId, action.type, action.selector, Date.now());
|
||||
// Execute action and capture observation
|
||||
const observation = await this.agent.executeAction(action);
|
||||
this.actionTrace.push(action);
|
||||
// Record new state if discovered
|
||||
if (!this.graph.hasState(observation.newStateId)) {
|
||||
const newState = await this.agent.captureState();
|
||||
this.graph.addState(newState);
|
||||
depth += 1;
|
||||
this.logger.log({
|
||||
event: 'state_discovered',
|
||||
timestamp: Date.now(),
|
||||
stateId: newState.id,
|
||||
url: newState.url,
|
||||
title: newState.title,
|
||||
});
|
||||
this.events.onStateDiscovered?.(this.sessionId, newState.id, newState.url, newState.title);
|
||||
// Run per-state hooks (visual regression, accessibility, performance)
|
||||
for (const hook of this.stateHooks) {
|
||||
const hookAnomalies = await hook(newState, this.agent, this.sessionId, [...this.actionTrace]).catch(() => []);
|
||||
for (const anomaly of hookAnomalies) {
|
||||
anomalies.push(anomaly);
|
||||
this.logger.log({
|
||||
event: 'anomaly_detected',
|
||||
timestamp: Date.now(),
|
||||
anomalyId: anomaly.id,
|
||||
type: anomaly.type,
|
||||
severity: anomaly.severity,
|
||||
});
|
||||
this.events.onAnomalyDetected?.(this.sessionId, anomaly);
|
||||
for (const exporter of this.exporters) {
|
||||
await exporter.export(anomaly, `${this.outputDir}/${anomaly.id}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
this.graph.recordTransition(currentState.id, action, observation.newStateId);
|
||||
this.logger.log({
|
||||
event: 'exploration_step',
|
||||
timestamp: Date.now(),
|
||||
stateId: currentState.id,
|
||||
actionId: action.id,
|
||||
});
|
||||
// Detect anomalies
|
||||
const detected = this.detector.detect(observation, [...this.actionTrace]);
|
||||
for (const anomaly of detected) {
|
||||
for (const collector of this.collectors) {
|
||||
const evidence = await collector.collect(anomaly, this.agent);
|
||||
Object.assign(anomaly.evidence, evidence);
|
||||
}
|
||||
anomalies.push(anomaly);
|
||||
this.logger.log({
|
||||
event: 'anomaly_detected',
|
||||
timestamp: Date.now(),
|
||||
anomalyId: anomaly.id,
|
||||
type: anomaly.type,
|
||||
severity: anomaly.severity,
|
||||
});
|
||||
this.events.onAnomalyDetected?.(this.sessionId, anomaly);
|
||||
for (const exporter of this.exporters) {
|
||||
const reportDir = `${this.outputDir}/${anomaly.id}`;
|
||||
await exporter.export(anomaly, reportDir);
|
||||
}
|
||||
}
|
||||
stepsExecuted += 1;
|
||||
// Run fuzzing if enabled and plugin provided
|
||||
if (this.fuzzingPlugin &&
|
||||
this.explorationConfig.fuzzingEnabled !== false &&
|
||||
currentState.domSnapshot) {
|
||||
const fuzzActions = this.fuzzingPlugin.generateFuzzActions(currentState.domSnapshot, currentState);
|
||||
for (const fuzzAction of fuzzActions) {
|
||||
if (this.aborted || isTimedOut())
|
||||
break;
|
||||
const fuzzObs = await this.agent.executeAction(fuzzAction);
|
||||
this.actionTrace.push(fuzzAction);
|
||||
const fuzzAnomalies = this.detector.detect(fuzzObs, [...this.actionTrace]);
|
||||
for (const anomaly of fuzzAnomalies) {
|
||||
for (const collector of this.collectors) {
|
||||
const evidence = await collector.collect(anomaly, this.agent);
|
||||
Object.assign(anomaly.evidence, evidence);
|
||||
}
|
||||
anomalies.push(anomaly);
|
||||
this.events.onAnomalyDetected?.(this.sessionId, anomaly);
|
||||
for (const exporter of this.exporters) {
|
||||
await exporter.export(anomaly, `${this.outputDir}/${anomaly.id}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
this.events.onSessionError?.(this.sessionId, msg);
|
||||
await this.agent.close().catch(() => undefined);
|
||||
throw err;
|
||||
}
|
||||
await this.agent.close();
|
||||
const statesVisited = this.graph.getAllStates().filter((s) => s.visitCount > 0).length;
|
||||
this.logger.log({
|
||||
event: 'session_end',
|
||||
timestamp: Date.now(),
|
||||
statesVisited,
|
||||
anomaliesFound: anomalies.length,
|
||||
});
|
||||
this.events.onSessionCompleted?.(this.sessionId, statesVisited, anomalies.length);
|
||||
return { statesVisited, anomaliesFound: anomalies.length, anomalies };
|
||||
}
|
||||
}
|
||||
exports.ExplorationEngine = ExplorationEngine;
|
||||
66
dist/core/Logger.js
vendored
Normal file
66
dist/core/Logger.js
vendored
Normal file
@@ -0,0 +1,66 @@
|
||||
"use strict";
|
||||
/**
|
||||
* Logger — writes structured JSON log events to a .jsonl file.
|
||||
* One JSON object per line.
|
||||
*/
|
||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
var desc = Object.getOwnPropertyDescriptor(m, k);
|
||||
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
||||
desc = { enumerable: true, get: function() { return m[k]; } };
|
||||
}
|
||||
Object.defineProperty(o, k2, desc);
|
||||
}) : (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
o[k2] = m[k];
|
||||
}));
|
||||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||
}) : function(o, v) {
|
||||
o["default"] = v;
|
||||
});
|
||||
var __importStar = (this && this.__importStar) || (function () {
|
||||
var ownKeys = function(o) {
|
||||
ownKeys = Object.getOwnPropertyNames || function (o) {
|
||||
var ar = [];
|
||||
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
||||
return ar;
|
||||
};
|
||||
return ownKeys(o);
|
||||
};
|
||||
return function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
})();
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.NullLogger = exports.FileLogger = void 0;
|
||||
const fs = __importStar(require("fs"));
|
||||
const path = __importStar(require("path"));
|
||||
class FileLogger {
|
||||
constructor(logDir, sessionId) {
|
||||
const logPath = path.join(logDir, `session_${sessionId}.jsonl`);
|
||||
fs.mkdirSync(logDir, { recursive: true });
|
||||
this.stream = fs.createWriteStream(logPath, { flags: 'a' });
|
||||
}
|
||||
log(event) {
|
||||
this.stream.write(JSON.stringify(event) + '\n');
|
||||
}
|
||||
close() {
|
||||
this.stream.end();
|
||||
}
|
||||
}
|
||||
exports.FileLogger = FileLogger;
|
||||
/** No-op logger for testing */
|
||||
class NullLogger {
|
||||
constructor() {
|
||||
this.events = [];
|
||||
}
|
||||
log(event) {
|
||||
this.events.push(event);
|
||||
}
|
||||
}
|
||||
exports.NullLogger = NullLogger;
|
||||
83
dist/core/StateGraph.js
vendored
Normal file
83
dist/core/StateGraph.js
vendored
Normal file
@@ -0,0 +1,83 @@
|
||||
"use strict";
|
||||
/**
|
||||
* StateGraph — manages known states and transitions between them.
|
||||
* Uses BFS ordering by default for exploration scheduling.
|
||||
*/
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.StateGraph = void 0;
|
||||
class StateGraph {
|
||||
constructor() {
|
||||
this.states = new Map();
|
||||
this.transitions = [];
|
||||
/** Insertion order for BFS */
|
||||
this.insertionOrder = [];
|
||||
}
|
||||
addState(state) {
|
||||
if (!this.states.has(state.id)) {
|
||||
this.states.set(state.id, state);
|
||||
this.insertionOrder.push(state.id);
|
||||
}
|
||||
else {
|
||||
// Update visit count on revisit
|
||||
const existing = this.states.get(state.id);
|
||||
this.states.set(state.id, { ...existing, visitCount: existing.visitCount + 1 });
|
||||
}
|
||||
}
|
||||
hasState(stateId) {
|
||||
return this.states.has(stateId);
|
||||
}
|
||||
getState(stateId) {
|
||||
return this.states.get(stateId);
|
||||
}
|
||||
incrementVisit(stateId) {
|
||||
const state = this.states.get(stateId);
|
||||
if (state) {
|
||||
this.states.set(stateId, { ...state, visitCount: state.visitCount + 1 });
|
||||
}
|
||||
}
|
||||
recordTransition(fromId, action, toId) {
|
||||
this.transitions.push({
|
||||
fromId,
|
||||
action,
|
||||
toId,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
}
|
||||
/** Returns all states that have never been visited (visitCount === 0) */
|
||||
getUnvisited() {
|
||||
return this.insertionOrder
|
||||
.map((id) => this.states.get(id))
|
||||
.filter((s) => s.visitCount === 0);
|
||||
}
|
||||
/** BFS heuristic: returns the oldest unvisited state, or null if none */
|
||||
getNextToExplore() {
|
||||
const unvisited = this.getUnvisited();
|
||||
return unvisited.length > 0 ? unvisited[0] : null;
|
||||
}
|
||||
getAllStates() {
|
||||
return this.insertionOrder.map((id) => this.states.get(id));
|
||||
}
|
||||
getTransitions() {
|
||||
return [...this.transitions];
|
||||
}
|
||||
toJSON() {
|
||||
return {
|
||||
stateCount: this.states.size,
|
||||
transitionCount: this.transitions.length,
|
||||
states: this.getAllStates().map((s) => ({
|
||||
id: s.id,
|
||||
url: s.url,
|
||||
title: s.title,
|
||||
visitCount: s.visitCount,
|
||||
})),
|
||||
transitions: this.transitions.map((t) => ({
|
||||
fromId: t.fromId,
|
||||
toId: t.toId,
|
||||
actionId: t.action.id,
|
||||
actionType: t.action.type,
|
||||
timestamp: t.timestamp,
|
||||
})),
|
||||
};
|
||||
}
|
||||
}
|
||||
exports.StateGraph = StateGraph;
|
||||
6
dist/core/interfaces.js
vendored
Normal file
6
dist/core/interfaces.js
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
"use strict";
|
||||
/**
|
||||
* ABE Core Interfaces
|
||||
* Core data types only. Must NOT import from src/plugins/.
|
||||
*/
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
Reference in New Issue
Block a user