Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions enterprise-interoperability-control-plane/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Enterprise Interoperability Control Plane

Self-contained milestone for SCIBASE issue #19, "Enterprise Tooling".

The module models the enterprise operations layer an institution needs before connecting SCIBASE to internal systems:

- admin dashboard metrics for projects, departments, AI reviews, storage, and reproducibility coverage;
- compliance evidence reports for institutional policies;
- secure API/integration catalog for downstream systems;
- HMAC-signed webhook events for project and compliance updates;
- export package manifests for journal, repository, and funder targets.

It is dependency-free CommonJS and can be run directly with Node.

## Run

```bash
node enterprise-interoperability-control-plane/test.js
node enterprise-interoperability-control-plane/demo.js
```

Optional browser demo recording:

```bash
npm install playwright --no-save --no-package-lock
node enterprise-interoperability-control-plane/record-demo.js
```

## Requirement Mapping

| Issue #19 capability | Implementation |
| --- | --- |
| Admin dashboards | `buildAdminDashboard()` reports project counts, private projects, storage, AI reviews, reproducibility coverage, and department breakdowns. |
| Compliance tracking | `evaluateCompliance()` compares each project against institutional policy requirements. |
| Secure REST/API integrations | `buildApiCatalog()` exposes integration systems, auth modes, endpoints, event types, and readiness. |
| Webhook support | `createWebhookEvent()` creates deterministic webhook payloads with HMAC signatures and event headers. |
| Export pipelines | `buildExportPackage()` creates target-specific export manifests for repository, journal, and DataCite-style systems. |
| Metadata preservation | Export manifests carry DOI, ORCID, funding, files, semantic version, and package digest. |
| Enterprise control plane | `buildEnterpriseControlPlane()` combines dashboard, compliance, API catalog, exports, webhooks, and digest for review. |

## Files

- `control-plane.js` - core enterprise control-plane logic.
- `sample-institution.json` - institutional fixture.
- `demo.js` - CLI demo.
- `test.js` - dependency-free tests.
- `demo.html` - browser workflow demo.
- `record-demo.js` - Playwright recorder.
209 changes: 209 additions & 0 deletions enterprise-interoperability-control-plane/control-plane.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
"use strict";

const crypto = require("node:crypto");

function stableStringify(value) {
if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
if (value && typeof value === "object") {
return `{${Object.keys(value)
.sort()
.map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`)
.join(",")}}`;
}
return JSON.stringify(value);
}

function digest(value) {
return crypto.createHash("sha256").update(stableStringify(value)).digest("hex");
}

function signPayload(payload, secret) {
return crypto.createHmac("sha256", secret).update(stableStringify(payload)).digest("hex");
}

function list(value) {
return Array.isArray(value) ? value.filter(Boolean) : [];
}

function normalizeInstitution(input) {
if (!input || typeof input !== "object") throw new Error("Institution payload is required");
return {
id: String(input.id || "institution"),
name: String(input.name || "Unnamed institution"),
projects: list(input.projects),
integrations: list(input.integrations),
exportTargets: list(input.exportTargets),
compliancePolicies: list(input.compliancePolicies),
};
}

function buildAdminDashboard(institution) {
const projects = institution.projects;
const privateProjects = projects.filter((project) => project.visibility === "private").length;
const reproducible = projects.filter((project) => Number(project.reproducibilityScore || 0) >= 80).length;
const aiReviews = projects.reduce((sum, project) => sum + Number(project.aiReviews || 0), 0);
const storageGb = projects.reduce((sum, project) => sum + Number(project.storageGb || 0), 0);
const departments = new Map();

for (const project of projects) {
const key = project.department || "Unassigned";
departments.set(key, (departments.get(key) || 0) + 1);
}

return {
projectCount: projects.length,
privateProjectCount: privateProjects,
reproducibilityCoverage: projects.length === 0 ? 0 : Math.round((reproducible / projects.length) * 100),
aiReviewsGenerated: aiReviews,
storageGb,
projectsByDepartment: Object.fromEntries([...departments.entries()].sort()),
};
}

function evaluateCompliance(institution) {
return institution.projects.map((project) => {
const missing = institution.compliancePolicies
.filter((policy) => !list(project.compliance).includes(policy.id))
.map((policy) => policy.id);
return {
projectId: project.id,
compliant: missing.length === 0,
missing,
tags: list(project.tags),
};
});
}

function buildApiCatalog(institution) {
return institution.integrations.map((integration) => ({
id: integration.id,
system: integration.system,
auth: integration.auth || "api-key",
endpoints: list(integration.endpoints),
eventTypes: list(integration.eventTypes),
status: integration.enabled === false ? "disabled" : "ready",
}));
}

function createWebhookEvent({ institution, eventType, projectId, payload, secret }) {
const event = {
id: digest({ eventType, projectId, payload }).slice(0, 16),
institutionId: institution.id,
eventType,
projectId,
payload,
createdAt: "2026-05-15T00:00:00.000Z",
};
return {
...event,
signature: signPayload(event, secret),
headers: {
"x-scibase-event": event.eventType,
"x-scibase-signature": `sha256=${signPayload(event, secret)}`,
},
};
}

function buildExportPackage(institution, projectId) {
const project = institution.projects.find((candidate) => candidate.id === projectId);
if (!project) throw new Error(`Unknown project: ${projectId}`);

const targets = institution.exportTargets.map((target) => ({
id: target.id,
system: target.system,
format: target.format,
ready: list(project.exportFormats).includes(target.format),
requiredMetadata: list(target.requiredMetadata),
missingMetadata: list(target.requiredMetadata).filter((field) => !project.metadata || !project.metadata[field]),
}));

const manifest = {
projectId: project.id,
title: project.title,
version: project.version || "v0.1.0",
targets,
files: list(project.files),
metadata: project.metadata || {},
};

return {
...manifest,
packageDigest: digest(manifest),
readyTargets: targets.filter((target) => target.ready && target.missingMetadata.length === 0).map((target) => target.id),
};
}

function buildEnterpriseControlPlane(rawInstitution, options = {}) {
const institution = normalizeInstitution(rawInstitution);
const dashboard = buildAdminDashboard(institution);
const compliance = evaluateCompliance(institution);
const apiCatalog = buildApiCatalog(institution);
const exportPackages = institution.projects.map((project) => buildExportPackage(institution, project.id));
const webhookEvents = institution.projects.map((project) =>
createWebhookEvent({
institution,
eventType: "project.compliance_evaluated",
projectId: project.id,
payload: {
compliant: compliance.find((entry) => entry.projectId === project.id).compliant,
reproducibilityScore: project.reproducibilityScore || 0,
},
secret: options.webhookSecret || "local-demo-secret",
}),
);

const report = {
institution: {
id: institution.id,
name: institution.name,
},
dashboard,
compliance,
apiCatalog,
exportPackages,
webhookEvents,
};

return {
...report,
digest: digest(report),
};
}

function validateControlPlane(report) {
const errors = [];
if (!report.dashboard) errors.push("admin dashboard is missing");
if (!Array.isArray(report.compliance)) errors.push("compliance report is missing");
if (!Array.isArray(report.apiCatalog)) errors.push("API catalog is missing");
if (!Array.isArray(report.exportPackages)) errors.push("export packages are missing");
if (!Array.isArray(report.webhookEvents)) errors.push("webhook events are missing");
if (!report.digest || report.digest.length !== 64) errors.push("digest is invalid");
if (report.digest && report.digest.length === 64) {
const { digest: reportDigest, ...signedReport } = report;
if (digest(signedReport) !== reportDigest) errors.push("digest does not match report payload");
}
for (const event of Array.isArray(report.webhookEvents) ? report.webhookEvents : []) {
if (!event.signature || event.signature.length !== 64) {
errors.push(`webhook signature is invalid for ${event.projectId || "unknown project"}`);
continue;
}
if (!event.headers || event.headers["x-scibase-signature"] !== `sha256=${event.signature}`) {
errors.push(`webhook signature header does not match for ${event.projectId || "unknown project"}`);
}
}
return { valid: errors.length === 0, errors };
}

module.exports = {
stableStringify,
digest,
signPayload,
normalizeInstitution,
buildAdminDashboard,
evaluateCompliance,
buildApiCatalog,
createWebhookEvent,
buildExportPackage,
buildEnterpriseControlPlane,
validateControlPlane,
};
Loading