Projects
+Waiting for dashboard
+Compliance
+Waiting for policy checks
+Exports
+Waiting for package manifests
+Enterprise Events
+Control Plane Payload
+No payload built.+
diff --git a/enterprise-interoperability-control-plane/README.md b/enterprise-interoperability-control-plane/README.md new file mode 100644 index 00000000..a0f7a02a --- /dev/null +++ b/enterprise-interoperability-control-plane/README.md @@ -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. diff --git a/enterprise-interoperability-control-plane/control-plane.js b/enterprise-interoperability-control-plane/control-plane.js new file mode 100644 index 00000000..a3ec3c62 --- /dev/null +++ b/enterprise-interoperability-control-plane/control-plane.js @@ -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, +}; diff --git a/enterprise-interoperability-control-plane/demo-data.js b/enterprise-interoperability-control-plane/demo-data.js new file mode 100644 index 00000000..60cd83ce --- /dev/null +++ b/enterprise-interoperability-control-plane/demo-data.js @@ -0,0 +1,219 @@ +window.SCIBASE_CONTROL_PLANE_REPORT = { + "institution": { + "id": "northstar-research-office", + "name": "Northstar Research Office" + }, + "dashboard": { + "projectCount": 2, + "privateProjectCount": 1, + "reproducibilityCoverage": 50, + "aiReviewsGenerated": 10, + "storageGb": 60, + "projectsByDepartment": { + "Materials Science": 1, + "Neuroscience": 1 + } + }, + "compliance": [ + { + "projectId": "alzheimers-cell-atlas", + "compliant": true, + "missing": [], + "tags": [ + "GRANT-TRACKED", + "OPEN-SCIENCE" + ] + }, + { + "projectId": "battery-materials-protocols", + "compliant": false, + "missing": [ + "data-availability", + "reproducibility" + ], + "tags": [ + "DOCTORAL-WORK" + ] + } + ], + "apiCatalog": [ + { + "id": "dspace-sync", + "system": "DSpace", + "auth": "oauth-client", + "endpoints": [ + "GET /projects", + "POST /exports" + ], + "eventTypes": [ + "project.published", + "project.compliance_evaluated" + ], + "status": "ready" + }, + { + "id": "canvas-notify", + "system": "Canvas LMS", + "auth": "signed-webhook", + "endpoints": [ + "POST /webhooks/research-output" + ], + "eventTypes": [ + "project.created", + "review.completed" + ], + "status": "ready" + } + ], + "exportPackages": [ + { + "projectId": "alzheimers-cell-atlas", + "title": "Alzheimer's Cell Atlas", + "version": "v0.1.0", + "targets": [ + { + "id": "zenodo", + "system": "Zenodo", + "format": "zenodo-bundle", + "ready": true, + "requiredMetadata": [ + "doi", + "orcid", + "funding" + ], + "missingMetadata": [] + }, + { + "id": "journal-jats", + "system": "Journal Submission", + "format": "jats", + "ready": true, + "requiredMetadata": [ + "doi", + "orcid" + ], + "missingMetadata": [] + }, + { + "id": "datacite", + "system": "DataCite", + "format": "datacite", + "ready": true, + "requiredMetadata": [ + "doi", + "funding" + ], + "missingMetadata": [] + } + ], + "files": [ + "manuscript/main.md", + "data/manifest.tsv", + "results/figures.zip" + ], + "metadata": { + "doi": "10.5555/scibase.demo.001", + "orcid": "0000-0002-1825-0097", + "funding": "NSF-OPEN-2026" + }, + "packageDigest": "5d0ceef1260564ab6b81149c9b8337d3e88dac8c0c35caf07d756000cf165980", + "readyTargets": [ + "zenodo", + "journal-jats", + "datacite" + ] + }, + { + "projectId": "battery-materials-protocols", + "title": "Battery Materials Protocols", + "version": "v0.1.0", + "targets": [ + { + "id": "zenodo", + "system": "Zenodo", + "format": "zenodo-bundle", + "ready": false, + "requiredMetadata": [ + "doi", + "orcid", + "funding" + ], + "missingMetadata": [ + "orcid" + ] + }, + { + "id": "journal-jats", + "system": "Journal Submission", + "format": "jats", + "ready": false, + "requiredMetadata": [ + "doi", + "orcid" + ], + "missingMetadata": [ + "orcid" + ] + }, + { + "id": "datacite", + "system": "DataCite", + "format": "datacite", + "ready": true, + "requiredMetadata": [ + "doi", + "funding" + ], + "missingMetadata": [] + } + ], + "files": [ + "protocols/synthesis.md", + "metadata.json" + ], + "metadata": { + "doi": "10.5555/scibase.demo.002", + "funding": "HORIZON-EU-DEMO" + }, + "packageDigest": "4eeccf38ccf96e8a21e678ab171bea8d75c1b37c9a12227c31848c88b3c1e3a0", + "readyTargets": [ + "datacite" + ] + } + ], + "webhookEvents": [ + { + "id": "70a11209d3fcca17", + "institutionId": "northstar-research-office", + "eventType": "project.compliance_evaluated", + "projectId": "alzheimers-cell-atlas", + "payload": { + "compliant": true, + "reproducibilityScore": 92 + }, + "createdAt": "2026-05-15T00:00:00.000Z", + "signature": "56bd4362a9e820677987dc101ed3b74049c88d65ed2b7d7e2e872d173299ab63", + "headers": { + "x-scibase-event": "project.compliance_evaluated", + "x-scibase-signature": "sha256=56bd4362a9e820677987dc101ed3b74049c88d65ed2b7d7e2e872d173299ab63" + } + }, + { + "id": "75e94ec51b5ed0d4", + "institutionId": "northstar-research-office", + "eventType": "project.compliance_evaluated", + "projectId": "battery-materials-protocols", + "payload": { + "compliant": false, + "reproducibilityScore": 76 + }, + "createdAt": "2026-05-15T00:00:00.000Z", + "signature": "f886adb602022a81e59c3b1c3e6ef5f2f612cd79464e8ff7a0612ca445040b3c", + "headers": { + "x-scibase-event": "project.compliance_evaluated", + "x-scibase-signature": "sha256=f886adb602022a81e59c3b1c3e6ef5f2f612cd79464e8ff7a0612ca445040b3c" + } + } + ], + "digest": "421b4cf84232a7f37080fae351e511b81ba6f10bdf33f4ca56549c8041f2a24a" +}; diff --git a/enterprise-interoperability-control-plane/demo.html b/enterprise-interoperability-control-plane/demo.html new file mode 100644 index 00000000..217f838f --- /dev/null +++ b/enterprise-interoperability-control-plane/demo.html @@ -0,0 +1,246 @@ + + +
+ + +Waiting for dashboard
+Waiting for policy checks
+Waiting for package manifests
+No payload built.+