From f6001714b62e386112b96655ce14911aaf89abf5 Mon Sep 17 00:00:00 2001 From: Bobby Date: Sun, 17 May 2026 13:03:14 +1000 Subject: [PATCH] Add enterprise API change governance --- README.md | 4 + enterprise-api-change-governance/README.md | 38 ++ .../data/sample-change-plan.json | 165 +++++ .../docs/acceptance-notes.md | 41 ++ .../docs/demo.mp4 | Bin 0 -> 80745 bytes .../docs/demo.svg | 65 ++ .../docs/governance-report.json | 608 ++++++++++++++++++ .../docs/requirement-map.md | 29 + enterprise-api-change-governance/package.json | 16 + .../scripts/demo.js | 109 ++++ .../src/api-change-governance.js | 443 +++++++++++++ .../test/api-change-governance.test.js | 71 ++ 12 files changed, 1589 insertions(+) create mode 100644 enterprise-api-change-governance/README.md create mode 100644 enterprise-api-change-governance/data/sample-change-plan.json create mode 100644 enterprise-api-change-governance/docs/acceptance-notes.md create mode 100644 enterprise-api-change-governance/docs/demo.mp4 create mode 100644 enterprise-api-change-governance/docs/demo.svg create mode 100644 enterprise-api-change-governance/docs/governance-report.json create mode 100644 enterprise-api-change-governance/docs/requirement-map.md create mode 100644 enterprise-api-change-governance/package.json create mode 100644 enterprise-api-change-governance/scripts/demo.js create mode 100644 enterprise-api-change-governance/src/api-change-governance.js create mode 100644 enterprise-api-change-governance/test/api-change-governance.test.js diff --git a/README.md b/README.md index d338cf6..7c0d4fa 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,6 @@ # deepevents.ai deepevents.ai main codebase + +## SCIBASE contribution modules + +- [Enterprise API Change Governance](enterprise-api-change-governance/README.md): contract-change review for institutional REST APIs and webhook integrations. diff --git a/enterprise-api-change-governance/README.md b/enterprise-api-change-governance/README.md new file mode 100644 index 0000000..2f69376 --- /dev/null +++ b/enterprise-api-change-governance/README.md @@ -0,0 +1,38 @@ +# Enterprise API Change Governance + +This module adds a focused Enterprise Tooling slice for institutional REST API and webhook contract governance. It helps university, institute, and corporate R&D admins decide whether an integration-facing change is ready to ship, needs review, should be held, or must be blocked before it breaks downstream systems. + +## What It Covers + +- Versioned REST route and webhook event change review. +- Breaking-change detection for removed fields, type changes, new required fields, and short deprecation windows. +- Critical consumer readiness checks for notice acknowledgement, sandbox evidence, migration tickets, and version negotiation. +- Restricted research data gates that require DPA evidence before funder or repository integrations receive sensitive payloads. +- Admin dashboard metrics, prioritized remediation actions, migration plans, export evidence manifests, and signed webhook review events. + +## Why This Is Distinct + +Existing Enterprise Tooling submissions cover broad dashboards, export packages, compliance evidence packets, audit routing, webhook replay, identity drift, retention/legal hold, data residency, SLA, secret rotation, lab inventory, and compute/storage quotas. This slice focuses specifically on contract-change governance before APIs and webhook schemas are released to enterprise consumers. + +## References Reviewed + +- OpenAPI-style machine-readable contract diffs for REST API route review. +- CloudEvents-style event envelopes for routing, replay, and event metadata consistency. +- Backward-compatible API evolution practices such as versioned releases, deprecation windows, and consumer migration evidence. + +## Local Usage + +```bash +npm run check +npm test +npm run demo +``` + +The demo writes: + +- `docs/demo.svg` +- `docs/governance-report.json` + +This PR also includes a short synthetic walkthrough video at `docs/demo.mp4`. + +The committed sample data is synthetic and does not contain credentials, private research data, bank details, or personally identifying documents. diff --git a/enterprise-api-change-governance/data/sample-change-plan.json b/enterprise-api-change-governance/data/sample-change-plan.json new file mode 100644 index 0000000..6c2ac9e --- /dev/null +++ b/enterprise-api-change-governance/data/sample-change-plan.json @@ -0,0 +1,165 @@ +{ + "portfolioId": "enterprise-api-governance-demo", + "asOf": "2026-05-17T02:30:00.000Z", + "policy": { + "minimumBreakingDeprecationDays": 90, + "criticalConsumerNoticeDays": 60, + "minimumRollbackDays": 14, + "requireOpenApiDiff": true, + "requireWebhookSchemaVersion": true, + "requireSandboxEvidence": true, + "requireDpaForRestrictedExports": true + }, + "integrations": [ + { + "integrationId": "dspace-archive-prod", + "institution": "Northbridge University Library", + "systemType": "repository", + "criticality": "critical", + "contactGroup": "library-platforms", + "pinnedApiVersions": ["v1"], + "subscribedWebhookTypes": ["project.published.v1", "export.completed.v1"], + "lastSuccessfulSandboxRun": "2026-05-05", + "supportsVersionNegotiation": true, + "notificationStatus": "acknowledged", + "migrationTicket": "NBUL-4421", + "hasDataProcessingAgreement": true + }, + { + "integrationId": "canvas-outcomes-sync", + "institution": "Westlake Medical School", + "systemType": "lms", + "criticality": "standard", + "contactGroup": "learning-systems", + "pinnedApiVersions": ["v2"], + "subscribedWebhookTypes": ["review.completed.v1"], + "lastSuccessfulSandboxRun": "2026-05-12", + "supportsVersionNegotiation": false, + "notificationStatus": "acknowledged", + "migrationTicket": "WMS-1180", + "hasDataProcessingAgreement": true + }, + { + "integrationId": "funder-reporter-nightly", + "institution": "Horizon Bioinformatics Institute", + "systemType": "funder_portal", + "criticality": "critical", + "contactGroup": "grants-ops", + "pinnedApiVersions": ["v1"], + "subscribedWebhookTypes": ["compliance.flagged.v1", "export.completed.v1"], + "lastSuccessfulSandboxRun": null, + "supportsVersionNegotiation": false, + "notificationStatus": "missing", + "migrationTicket": null, + "hasDataProcessingAgreement": false + } + ], + "changes": [ + { + "changeId": "api-project-export-v3", + "title": "Introduce v3 project export manifest with versioned metadata blocks", + "ownerTeam": "enterprise-integrations", + "surface": "rest_api", + "changeKind": "additive_version", + "currentVersion": "v2", + "proposedVersion": "v3", + "effectiveDate": "2026-09-01", + "affectedRoutes": ["GET /api/projects/{projectId}/exports", "POST /api/projects/{projectId}/exports"], + "affectedWebhookTypes": [], + "breaking": false, + "restrictedResearchData": false, + "openApiDiffAttached": true, + "rollbackPlanDays": 30, + "parallelRunDays": 120, + "schemaDiff": { + "removedFields": [], + "renamedFields": [], + "typeChanges": [], + "newRequiredFields": [], + "newOptionalFields": ["metadata.versionHistory", "metadata.repositoryTargets"] + }, + "webhookEnvelope": { + "usesCloudEvents": true, + "schemaVersionField": "data.schemaVersion", + "idempotencyKeyField": "data.deliveryId", + "signatureVersion": "v2" + }, + "sandboxEvidence": { + "fixturePack": "exports-v3-2026-05", + "passingIntegrations": ["dspace-archive-prod", "canvas-outcomes-sync"], + "failingIntegrations": [] + } + }, + { + "changeId": "api-review-score-removal", + "title": "Remove legacy peerReview.score field from review completed payloads", + "ownerTeam": "peer-review-platform", + "surface": "webhook", + "changeKind": "breaking_removal", + "currentVersion": "review.completed.v1", + "proposedVersion": "review.completed.v2", + "effectiveDate": "2026-06-15", + "affectedRoutes": [], + "affectedWebhookTypes": ["review.completed.v1"], + "breaking": true, + "restrictedResearchData": false, + "openApiDiffAttached": false, + "rollbackPlanDays": 3, + "parallelRunDays": 29, + "schemaDiff": { + "removedFields": ["peerReview.score"], + "renamedFields": ["peerReview.rubric -> peerReview.rubricBreakdown"], + "typeChanges": ["peerReview.reviewerCount:number -> string"], + "newRequiredFields": ["peerReview.decisionCode"], + "newOptionalFields": [] + }, + "webhookEnvelope": { + "usesCloudEvents": false, + "schemaVersionField": null, + "idempotencyKeyField": null, + "signatureVersion": null + }, + "sandboxEvidence": { + "fixturePack": null, + "passingIntegrations": [], + "failingIntegrations": ["canvas-outcomes-sync"] + } + }, + { + "changeId": "api-compliance-flag-pii", + "title": "Add restricted compliance flag webhook for funder reporting", + "ownerTeam": "compliance-ops", + "surface": "webhook", + "changeKind": "new_event", + "currentVersion": null, + "proposedVersion": "compliance.flagged.v2", + "effectiveDate": "2026-07-20", + "affectedRoutes": ["GET /api/compliance/flags"], + "affectedWebhookTypes": ["compliance.flagged.v2"], + "breaking": false, + "restrictedResearchData": true, + "openApiDiffAttached": true, + "rollbackPlanDays": 21, + "parallelRunDays": 90, + "schemaDiff": { + "removedFields": [], + "renamedFields": [], + "typeChanges": [], + "newRequiredFields": ["flag.category", "flag.evidenceDigest"], + "newOptionalFields": ["flag.funderMandateId"] + }, + "webhookEnvelope": { + "usesCloudEvents": true, + "schemaVersionField": "data.schemaVersion", + "idempotencyKeyField": "data.deliveryId", + "signatureVersion": "v2" + }, + "sandboxEvidence": { + "fixturePack": "compliance-v2-2026-05", + "passingIntegrations": ["dspace-archive-prod"], + "failingIntegrations": ["funder-reporter-nightly"] + } + } + ], + "signingKeyId": "synthetic-key-2026-05" +} diff --git a/enterprise-api-change-governance/docs/acceptance-notes.md b/enterprise-api-change-governance/docs/acceptance-notes.md new file mode 100644 index 0000000..f932ad3 --- /dev/null +++ b/enterprise-api-change-governance/docs/acceptance-notes.md @@ -0,0 +1,41 @@ +# Acceptance Notes + +This contribution targets issue `#19` by adding a distinct Enterprise Tooling module for API and webhook contract-change governance. + +## Review Scope + +- Self-contained dependency-free Node module. +- Synthetic data only. +- No external credentials, accounts, payment details, bank information, private research data, or KYC material. +- No changes to existing application code paths. + +## Distinctness + +This does not duplicate existing SCIBASE Enterprise Tooling submissions for: + +- admin dashboard foundations +- export packages +- compliance evidence packets +- audit signal routing +- webhook replay ledgers +- identity provisioning drift +- retention/legal hold +- grant portfolio compliance +- data residency +- SLA/uptime monitoring +- secret rotation +- lab inventory readiness +- compute/storage quota governance + +It covers a narrower pre-release governance layer: whether enterprise-facing API routes and webhook contracts can safely change without breaking institutional consumers. + +## Validation Commands + +```bash +cd enterprise-api-change-governance +npm run check +npm test +npm run demo +``` + +Demo video: `enterprise-api-change-governance/docs/demo.mp4`. diff --git a/enterprise-api-change-governance/docs/demo.mp4 b/enterprise-api-change-governance/docs/demo.mp4 new file mode 100644 index 0000000000000000000000000000000000000000..4bfa356eef2d927eaaa7c8c6a8f6484da7f4b21a GIT binary patch literal 80745 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ΠPpD&onXT=Nnn>{vb9nC7MWNb6l_UwuY@5nk+G6 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 + + Enterprise API Change Governance demo + Dashboard preview for SCIBASE enterprise API and webhook compatibility review. + + + + + Enterprise API Change Governance + Versioned REST and webhook contract review for institutional integrations + + + Changes + 3 + + + + Blocked + 1 + + + + Critical Impacts + 1 + + + + Audit Digest + 793bd6d911b7dfb2499178e3d595bf52 + + + + + + Introduce v3 project export manifest with versioned metadata blocks + api-project-export-v3 - 1 affected integrations - no blockers + READY + risk 0 + + + + + Remove legacy peerReview.score field from review completed payloads + api-review-score-removal - 1 affected integrations - breaking-diff, removed-fields + BLOCKED + risk 100 + + + + + Add restricted compliance flag webhook for funder reporting + api-compliance-flag-pii - 1 affected integrations - new-required-fields, consumer-blockers + HOLD + risk 86 + + Outputs: admin actions, sandbox gates, migration notices, signed CloudEvents-style webhook evidence, export manifest. + diff --git a/enterprise-api-change-governance/docs/governance-report.json b/enterprise-api-change-governance/docs/governance-report.json new file mode 100644 index 0000000..4c0cb6d --- /dev/null +++ b/enterprise-api-change-governance/docs/governance-report.json @@ -0,0 +1,608 @@ +{ + "generatedAt": "2026-05-17T02:30:00.000Z", + "portfolioId": "enterprise-api-governance-demo", + "dashboard": { + "totalChanges": 3, + "byState": { + "ready": 1, + "watch": 0, + "hold": 1, + "blocked": 1 + }, + "blockedChanges": 1, + "holdOrBlockedChanges": 2, + "criticalIntegrationsImpacted": 1, + "consumerBlocks": 2, + "uniqueInstitutionsImpacted": 2, + "highestRiskScore": 100 + }, + "reviews": [ + { + "changeId": "api-project-export-v3", + "title": "Introduce v3 project export manifest with versioned metadata blocks", + "ownerTeam": "enterprise-integrations", + "surface": "rest_api", + "state": "ready", + "riskScore": 0, + "effectiveDate": "2026-09-01", + "breaking": false, + "compatibilityWindowDays": 120, + "affectedRoutes": [ + "GET /api/projects/{projectId}/exports", + "POST /api/projects/{projectId}/exports" + ], + "affectedWebhookTypes": [], + "affectedConsumers": [ + { + "integrationId": "canvas-outcomes-sync", + "institution": "Westlake Medical School", + "systemType": "lms", + "criticality": "standard", + "contactGroup": "learning-systems", + "readiness": "ready", + "blockers": [], + "warnings": [], + "migrationTicket": "WMS-1180", + "lastSuccessfulSandboxRun": "2026-05-12" + } + ], + "findings": [], + "adminActions": [ + "Approve api-project-export-v3 for enterprise rollout with monitored release notes." + ], + "migrationPlan": { + "changeId": "api-project-export-v3", + "noticeAudience": [ + "learning-systems" + ], + "sandboxFixturePack": "exports-v3-2026-05", + "rollbackPlanDays": 30, + "parallelRunDays": 120, + "requiredBeforeRelease": [ + "publish release note", + "monitor webhook deliveries" + ], + "suggestedOrder": [ + "publish contract diff and migration notes", + "run sandbox fixtures against affected integrations", + "collect critical-consumer acknowledgement", + "ship parallel version", + "monitor signed webhook evidence" + ] + }, + "evidenceDigest": "a35bc73066e732b78bb0f760fe27a9869df9d57b002b82f71a0e8b989440aa31" + }, + { + "changeId": "api-review-score-removal", + "title": "Remove legacy peerReview.score field from review completed payloads", + "ownerTeam": "peer-review-platform", + "surface": "webhook", + "state": "blocked", + "riskScore": 100, + "effectiveDate": "2026-06-15", + "breaking": true, + "compatibilityWindowDays": 29, + "affectedRoutes": [], + "affectedWebhookTypes": [ + "review.completed.v1" + ], + "affectedConsumers": [ + { + "integrationId": "canvas-outcomes-sync", + "institution": "Westlake Medical School", + "systemType": "lms", + "criticality": "standard", + "contactGroup": "learning-systems", + "readiness": "blocked", + "blockers": [ + "breaking change without version negotiation" + ], + "warnings": [], + "migrationTicket": "WMS-1180", + "lastSuccessfulSandboxRun": "2026-05-12" + } + ], + "findings": [ + { + "severity": "blocker", + "code": "breaking-diff", + "message": "Breaking changes must include a machine-readable diff for admins and integrators." + }, + { + "severity": "blocker", + "code": "removed-fields", + "message": "Removed fields detected: peerReview.score." + }, + { + "severity": "warning", + "code": "type-changes", + "message": "Type changes require fixture-backed migration notes: peerReview.reviewerCount:number -> string." + }, + { + "severity": "warning", + "code": "new-required-fields", + "message": "New required fields should ship behind a new version or default compatibility mode." + }, + { + "severity": "blocker", + "code": "short-deprecation-window", + "message": "Breaking change has 29 days of notice; policy requires 90." + }, + { + "severity": "blocker", + "code": "short-parallel-run", + "message": "Breaking changes need a parallel run window long enough for pinned institutional clients." + }, + { + "severity": "warning", + "code": "rollback-window", + "message": "Rollback plan covers 3 days; policy requires at least 14." + }, + { + "severity": "warning", + "code": "event-envelope", + "message": "Webhook changes should keep a standard event envelope for routing and replay." + }, + { + "severity": "blocker", + "code": "schema-version", + "message": "Webhook payloads need an explicit schema version field." + }, + { + "severity": "blocker", + "code": "idempotency-key", + "message": "Webhook payloads need a stable idempotency key for enterprise replay safety." + }, + { + "severity": "warning", + "code": "signature-version", + "message": "Webhook signing version should be explicit so consumers can rotate verifiers safely." + }, + { + "severity": "blocker", + "code": "consumer-blockers", + "message": "1 affected integrations are blocked." + } + ], + "adminActions": [ + "Block api-review-score-removal: Breaking changes must include a machine-readable diff for admins and integrators.", + "Block api-review-score-removal: Removed fields detected: peerReview.score.", + "Review api-review-score-removal: Type changes require fixture-backed migration notes: peerReview.reviewerCount:number -> string.", + "Review api-review-score-removal: New required fields should ship behind a new version or default compatibility mode.", + "Block api-review-score-removal: Breaking change has 29 days of notice; policy requires 90.", + "Block api-review-score-removal: Breaking changes need a parallel run window long enough for pinned institutional clients.", + "Review api-review-score-removal: Rollback plan covers 3 days; policy requires at least 14.", + "Review api-review-score-removal: Webhook changes should keep a standard event envelope for routing and replay.", + "Block api-review-score-removal: Webhook payloads need an explicit schema version field.", + "Block api-review-score-removal: Webhook payloads need a stable idempotency key for enterprise replay safety.", + "Review api-review-score-removal: Webhook signing version should be explicit so consumers can rotate verifiers safely.", + "Block api-review-score-removal: 1 affected integrations are blocked.", + "Open migration review for Westlake Medical School (canvas-outcomes-sync)." + ], + "migrationPlan": { + "changeId": "api-review-score-removal", + "noticeAudience": [ + "learning-systems" + ], + "sandboxFixturePack": null, + "rollbackPlanDays": 3, + "parallelRunDays": 29, + "requiredBeforeRelease": [ + "breaking-diff", + "removed-fields", + "short-deprecation-window", + "short-parallel-run", + "schema-version", + "idempotency-key", + "consumer-blockers" + ], + "suggestedOrder": [ + "publish contract diff and migration notes", + "run sandbox fixtures against affected integrations", + "collect critical-consumer acknowledgement", + "ship parallel version", + "monitor signed webhook evidence" + ] + }, + "evidenceDigest": "52a3c00315c9a5db1fd4cd30a7c3e3273c90b275aa410900dc0e9912e57d0134" + }, + { + "changeId": "api-compliance-flag-pii", + "title": "Add restricted compliance flag webhook for funder reporting", + "ownerTeam": "compliance-ops", + "surface": "webhook", + "state": "hold", + "riskScore": 86, + "effectiveDate": "2026-07-20", + "breaking": false, + "compatibilityWindowDays": 90, + "affectedRoutes": [ + "GET /api/compliance/flags" + ], + "affectedWebhookTypes": [ + "compliance.flagged.v2" + ], + "affectedConsumers": [ + { + "integrationId": "funder-reporter-nightly", + "institution": "Horizon Bioinformatics Institute", + "systemType": "funder_portal", + "criticality": "critical", + "contactGroup": "grants-ops", + "readiness": "blocked", + "blockers": [ + "restricted-data integration missing DPA" + ], + "warnings": [ + "sandbox evidence is stale or absent", + "notification not acknowledged", + "no migration ticket linked" + ], + "migrationTicket": null, + "lastSuccessfulSandboxRun": null + } + ], + "findings": [ + { + "severity": "warning", + "code": "new-required-fields", + "message": "New required fields should ship behind a new version or default compatibility mode." + }, + { + "severity": "blocker", + "code": "consumer-blockers", + "message": "1 affected integrations are blocked." + }, + { + "severity": "warning", + "code": "sandbox-evidence", + "message": "1 affected integrations need fresh sandbox evidence." + }, + { + "severity": "warning", + "code": "notice-ack", + "message": "1 affected integrations have not acknowledged migration notice." + }, + { + "severity": "blocker", + "code": "restricted-data-dpa", + "message": "Restricted research data cannot flow to integrations without DPA evidence." + } + ], + "adminActions": [ + "Review api-compliance-flag-pii: New required fields should ship behind a new version or default compatibility mode.", + "Block api-compliance-flag-pii: 1 affected integrations are blocked.", + "Review api-compliance-flag-pii: 1 affected integrations need fresh sandbox evidence.", + "Review api-compliance-flag-pii: 1 affected integrations have not acknowledged migration notice.", + "Block api-compliance-flag-pii: Restricted research data cannot flow to integrations without DPA evidence.", + "Open migration review for Horizon Bioinformatics Institute (funder-reporter-nightly)." + ], + "migrationPlan": { + "changeId": "api-compliance-flag-pii", + "noticeAudience": [ + "grants-ops" + ], + "sandboxFixturePack": "compliance-v2-2026-05", + "rollbackPlanDays": 21, + "parallelRunDays": 90, + "requiredBeforeRelease": [ + "consumer-blockers", + "restricted-data-dpa" + ], + "suggestedOrder": [ + "publish contract diff and migration notes", + "run sandbox fixtures against affected integrations", + "collect critical-consumer acknowledgement", + "ship parallel version", + "monitor signed webhook evidence" + ] + }, + "evidenceDigest": "b6d529654bab1df513308e97fd17297f4476afb80c08e768d54e28a72f94f908" + } + ], + "exportManifest": { + "manifestId": "api-change-governance:enterprise-api-governance-demo", + "generatedFor": [ + "institutional-admin-dashboard", + "api-consumer-notice-pack", + "webhook-delivery-review", + "compliance-export-evidence" + ], + "readyChangeIds": [ + "api-project-export-v3" + ], + "reviewChangeIds": [ + "api-review-score-removal", + "api-compliance-flag-pii" + ], + "evidenceDigests": { + "api-project-export-v3": "a35bc73066e732b78bb0f760fe27a9869df9d57b002b82f71a0e8b989440aa31", + "api-review-score-removal": "52a3c00315c9a5db1fd4cd30a7c3e3273c90b275aa410900dc0e9912e57d0134", + "api-compliance-flag-pii": "b6d529654bab1df513308e97fd17297f4476afb80c08e768d54e28a72f94f908" + } + }, + "webhookEvents": [ + { + "specversion": "1.0", + "type": "scibase.enterprise.api_change.ready", + "source": "/enterprise/api-change-governance", + "id": "58c5aa3ea0976ce9f7870fa7bea7d71f", + "subject": "api-project-export-v3", + "time": "2026-09-01", + "datacontenttype": "application/json", + "data": { + "changeId": "api-project-export-v3", + "state": "ready", + "riskScore": 0, + "affectedConsumers": 1, + "blockers": [], + "evidenceDigest": "a35bc73066e732b78bb0f760fe27a9869df9d57b002b82f71a0e8b989440aa31" + }, + "signatureKeyId": "synthetic-key-2026-05", + "signature": "9aa63e7edcd6aa9a141d40ad03748137f2f072e3734c427c0683468c8dd9b198" + }, + { + "specversion": "1.0", + "type": "scibase.enterprise.api_change.blocked", + "source": "/enterprise/api-change-governance", + "id": "3b5a0bc61fd4bc580f4eabd57824f9c2", + "subject": "api-review-score-removal", + "time": "2026-06-15", + "datacontenttype": "application/json", + "data": { + "changeId": "api-review-score-removal", + "state": "blocked", + "riskScore": 100, + "affectedConsumers": 1, + "blockers": [ + "breaking-diff", + "removed-fields", + "short-deprecation-window", + "short-parallel-run", + "schema-version", + "idempotency-key", + "consumer-blockers" + ], + "evidenceDigest": "52a3c00315c9a5db1fd4cd30a7c3e3273c90b275aa410900dc0e9912e57d0134" + }, + "signatureKeyId": "synthetic-key-2026-05", + "signature": "30f52b1549c748a69d37241d8e2ddc95a40932f0f2d1d48fb6e9c1f0afbf4268" + }, + { + "specversion": "1.0", + "type": "scibase.enterprise.api_change.hold", + "source": "/enterprise/api-change-governance", + "id": "48fe3ef563dd16d186da31cbd2cc1855", + "subject": "api-compliance-flag-pii", + "time": "2026-07-20", + "datacontenttype": "application/json", + "data": { + "changeId": "api-compliance-flag-pii", + "state": "hold", + "riskScore": 86, + "affectedConsumers": 1, + "blockers": [ + "consumer-blockers", + "restricted-data-dpa" + ], + "evidenceDigest": "b6d529654bab1df513308e97fd17297f4476afb80c08e768d54e28a72f94f908" + }, + "signatureKeyId": "synthetic-key-2026-05", + "signature": "b1fbb381c8766fc7e3ff91a0caf40e7c59ca6a0f8b4f900a0d5e2e90af96c334" + } + ], + "sanitizedInputEcho": { + "portfolioId": "enterprise-api-governance-demo", + "asOf": "2026-05-17T02:30:00.000Z", + "policy": { + "minimumBreakingDeprecationDays": 90, + "criticalConsumerNoticeDays": 60, + "minimumRollbackDays": 14, + "requireOpenApiDiff": true, + "requireWebhookSchemaVersion": true, + "requireSandboxEvidence": true, + "requireDpaForRestrictedExports": true + }, + "integrations": [ + { + "integrationId": "dspace-archive-prod", + "institution": "Northbridge University Library", + "systemType": "repository", + "criticality": "critical", + "contactGroup": "library-platforms", + "pinnedApiVersions": [ + "v1" + ], + "subscribedWebhookTypes": [ + "project.published.v1", + "export.completed.v1" + ], + "lastSuccessfulSandboxRun": "2026-05-05", + "supportsVersionNegotiation": true, + "notificationStatus": "acknowledged", + "migrationTicket": "NBUL-4421", + "hasDataProcessingAgreement": true + }, + { + "integrationId": "canvas-outcomes-sync", + "institution": "Westlake Medical School", + "systemType": "lms", + "criticality": "standard", + "contactGroup": "learning-systems", + "pinnedApiVersions": [ + "v2" + ], + "subscribedWebhookTypes": [ + "review.completed.v1" + ], + "lastSuccessfulSandboxRun": "2026-05-12", + "supportsVersionNegotiation": false, + "notificationStatus": "acknowledged", + "migrationTicket": "WMS-1180", + "hasDataProcessingAgreement": true + }, + { + "integrationId": "funder-reporter-nightly", + "institution": "Horizon Bioinformatics Institute", + "systemType": "funder_portal", + "criticality": "critical", + "contactGroup": "grants-ops", + "pinnedApiVersions": [ + "v1" + ], + "subscribedWebhookTypes": [ + "compliance.flagged.v1", + "export.completed.v1" + ], + "lastSuccessfulSandboxRun": null, + "supportsVersionNegotiation": false, + "notificationStatus": "missing", + "migrationTicket": null, + "hasDataProcessingAgreement": false + } + ], + "changes": [ + { + "changeId": "api-project-export-v3", + "title": "Introduce v3 project export manifest with versioned metadata blocks", + "ownerTeam": "enterprise-integrations", + "surface": "rest_api", + "changeKind": "additive_version", + "currentVersion": "v2", + "proposedVersion": "v3", + "effectiveDate": "2026-09-01", + "affectedRoutes": [ + "GET /api/projects/{projectId}/exports", + "POST /api/projects/{projectId}/exports" + ], + "affectedWebhookTypes": [], + "breaking": false, + "restrictedResearchData": false, + "openApiDiffAttached": true, + "rollbackPlanDays": 30, + "parallelRunDays": 120, + "schemaDiff": { + "removedFields": [], + "renamedFields": [], + "typeChanges": [], + "newRequiredFields": [], + "newOptionalFields": [ + "metadata.versionHistory", + "metadata.repositoryTargets" + ] + }, + "webhookEnvelope": { + "usesCloudEvents": true, + "schemaVersionField": "data.schemaVersion", + "idempotencyKeyField": "data.deliveryId", + "signatureVersion": "v2" + }, + "sandboxEvidence": { + "fixturePack": "exports-v3-2026-05", + "passingIntegrations": [ + "dspace-archive-prod", + "canvas-outcomes-sync" + ], + "failingIntegrations": [] + } + }, + { + "changeId": "api-review-score-removal", + "title": "Remove legacy peerReview.score field from review completed payloads", + "ownerTeam": "peer-review-platform", + "surface": "webhook", + "changeKind": "breaking_removal", + "currentVersion": "review.completed.v1", + "proposedVersion": "review.completed.v2", + "effectiveDate": "2026-06-15", + "affectedRoutes": [], + "affectedWebhookTypes": [ + "review.completed.v1" + ], + "breaking": true, + "restrictedResearchData": false, + "openApiDiffAttached": false, + "rollbackPlanDays": 3, + "parallelRunDays": 29, + "schemaDiff": { + "removedFields": [ + "peerReview.score" + ], + "renamedFields": [ + "peerReview.rubric -> peerReview.rubricBreakdown" + ], + "typeChanges": [ + "peerReview.reviewerCount:number -> string" + ], + "newRequiredFields": [ + "peerReview.decisionCode" + ], + "newOptionalFields": [] + }, + "webhookEnvelope": { + "usesCloudEvents": false, + "schemaVersionField": null, + "idempotencyKeyField": null, + "signatureVersion": null + }, + "sandboxEvidence": { + "fixturePack": null, + "passingIntegrations": [], + "failingIntegrations": [ + "canvas-outcomes-sync" + ] + } + }, + { + "changeId": "api-compliance-flag-pii", + "title": "Add restricted compliance flag webhook for funder reporting", + "ownerTeam": "compliance-ops", + "surface": "webhook", + "changeKind": "new_event", + "currentVersion": null, + "proposedVersion": "compliance.flagged.v2", + "effectiveDate": "2026-07-20", + "affectedRoutes": [ + "GET /api/compliance/flags" + ], + "affectedWebhookTypes": [ + "compliance.flagged.v2" + ], + "breaking": false, + "restrictedResearchData": true, + "openApiDiffAttached": true, + "rollbackPlanDays": 21, + "parallelRunDays": 90, + "schemaDiff": { + "removedFields": [], + "renamedFields": [], + "typeChanges": [], + "newRequiredFields": [ + "flag.category", + "flag.evidenceDigest" + ], + "newOptionalFields": [ + "flag.funderMandateId" + ] + }, + "webhookEnvelope": { + "usesCloudEvents": true, + "schemaVersionField": "data.schemaVersion", + "idempotencyKeyField": "data.deliveryId", + "signatureVersion": "v2" + }, + "sandboxEvidence": { + "fixturePack": "compliance-v2-2026-05", + "passingIntegrations": [ + "dspace-archive-prod" + ], + "failingIntegrations": [ + "funder-reporter-nightly" + ] + } + } + ], + "signingKeyId": "synthetic-key-2026-05" + }, + "auditDigest": "793bd6d911b7dfb2499178e3d595bf52b72796c83c3544a6a889df7b6848aeed" +} diff --git a/enterprise-api-change-governance/docs/requirement-map.md b/enterprise-api-change-governance/docs/requirement-map.md new file mode 100644 index 0000000..b66c188 --- /dev/null +++ b/enterprise-api-change-governance/docs/requirement-map.md @@ -0,0 +1,29 @@ +# Requirement Map + +## Admin Dashboards + +- `buildApiChangeGovernance()` emits `dashboard.totalChanges`, `dashboard.byState`, blocked-change counts, critical integration impacts, consumer blocks, and highest risk score. +- Each reviewed change includes a state, risk score, findings, and admin actions suitable for institutional admin queues. + +## API & Webhooks + +- REST API changes are gated by route lists, current/proposed versions, OpenAPI-style diff evidence, deprecation windows, rollback plans, and parallel run windows. +- Webhook changes are evaluated for event envelope consistency, schema version fields, idempotency keys, and signature-version evidence. +- `webhookEvents` emits signed CloudEvents-style governance events for downstream enterprise systems. + +## Export Pipelines + +- `exportManifest` groups ready vs. review-required changes and includes evidence digests for compliance export packets. +- Restricted research data changes are blocked when affected integrations do not have DPA evidence. + +## Enterprise Governance + +- Critical consumers require acknowledged notices before release. +- Sandbox fixture evidence is tracked for each affected institutional integration. +- Migration plans include notice audience, fixture packs, rollback windows, parallel run windows, and release sequencing. + +## Demo And Tests + +- `data/sample-change-plan.json` includes additive, breaking, and restricted-data rollout examples. +- `test/api-change-governance.test.js` covers ready, blocked, restricted-data hold, deterministic digest, signed events, and redaction behavior. +- `scripts/demo.js` prints a terminal summary and writes demo artifacts. diff --git a/enterprise-api-change-governance/package.json b/enterprise-api-change-governance/package.json new file mode 100644 index 0000000..0ab6c73 --- /dev/null +++ b/enterprise-api-change-governance/package.json @@ -0,0 +1,16 @@ +{ + "name": "enterprise-api-change-governance", + "version": "0.1.0", + "private": true, + "type": "module", + "description": "Enterprise API and webhook contract change governance for institutional SCIBASE integrations.", + "scripts": { + "check": "node --check src/api-change-governance.js && node --check scripts/demo.js && node --check test/api-change-governance.test.js", + "test": "node --test", + "demo": "node scripts/demo.js" + }, + "engines": { + "node": ">=20" + }, + "license": "MIT" +} diff --git a/enterprise-api-change-governance/scripts/demo.js b/enterprise-api-change-governance/scripts/demo.js new file mode 100644 index 0000000..73b6a67 --- /dev/null +++ b/enterprise-api-change-governance/scripts/demo.js @@ -0,0 +1,109 @@ +import { mkdir, readFile, writeFile } from "node:fs/promises"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { buildApiChangeGovernance } from "../src/api-change-governance.js"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const rootDir = join(__dirname, ".."); +const sample = JSON.parse(await readFile(join(rootDir, "data", "sample-change-plan.json"), "utf8")); +const report = buildApiChangeGovernance(sample, { generatedAt: sample.asOf }); + +await mkdir(join(rootDir, "docs"), { recursive: true }); +await writeFile(join(rootDir, "docs", "demo.svg"), renderSvg(report)); +await writeFile(join(rootDir, "docs", "governance-report.json"), `${JSON.stringify(report, null, 2)}\n`); + +console.log("Enterprise API Change Governance"); +console.log(`Portfolio: ${report.portfolioId}`); +console.log(`Changes: ${report.dashboard.totalChanges}`); +console.log(`State distribution: ${JSON.stringify(report.dashboard.byState)}`); +console.log(`Critical integrations impacted: ${report.dashboard.criticalIntegrationsImpacted}`); +console.log(`Highest risk score: ${report.dashboard.highestRiskScore}`); +console.log(`Audit digest: ${report.auditDigest}`); + +for (const review of report.reviews) { + console.log(""); + console.log(`${review.state.toUpperCase()} ${review.riskScore} - ${review.title}`); + console.log(`Affected consumers: ${review.affectedConsumers.length}`); + console.log(`Evidence digest: ${review.evidenceDigest}`); + for (const action of review.adminActions.slice(0, 3)) { + console.log(`- ${action}`); + } +} + +function renderSvg(report) { + const rows = report.reviews.map((review, index) => { + const y = 232 + index * 112; + const color = stateColor(review.state); + const findings = review.findings + .slice(0, 2) + .map((finding) => finding.code) + .join(", ") || "no blockers"; + return ` + + + + ${escapeXml(review.title)} + ${review.changeId} - ${review.affectedConsumers.length} affected integrations - ${escapeXml(findings)} + ${review.state.toUpperCase()} + risk ${review.riskScore} + `; + }).join(""); + + return ` + + Enterprise API Change Governance demo + Dashboard preview for SCIBASE enterprise API and webhook compatibility review. + + + + + Enterprise API Change Governance + Versioned REST and webhook contract review for institutional integrations + + + Changes + ${report.dashboard.totalChanges} + + + + Blocked + ${report.dashboard.blockedChanges} + + + + Critical Impacts + ${report.dashboard.criticalIntegrationsImpacted} + + + + Audit Digest + ${report.auditDigest.slice(0, 32)} + + ${rows} + Outputs: admin actions, sandbox gates, migration notices, signed CloudEvents-style webhook evidence, export manifest. + +`; +} + +function stateColor(state) { + if (state === "ready") return "#0e9f6e"; + if (state === "watch") return "#d97706"; + if (state === "hold") return "#c2410c"; + return "#c81e1e"; +} + +function escapeXml(value) { + return String(value) + .replaceAll("&", "&") + .replaceAll("<", "<") + .replaceAll(">", ">") + .replaceAll('"', """); +} diff --git a/enterprise-api-change-governance/src/api-change-governance.js b/enterprise-api-change-governance/src/api-change-governance.js new file mode 100644 index 0000000..85ef534 --- /dev/null +++ b/enterprise-api-change-governance/src/api-change-governance.js @@ -0,0 +1,443 @@ +import { createHash, createHmac } from "node:crypto"; + +const DEFAULT_POLICY = Object.freeze({ + minimumBreakingDeprecationDays: 90, + criticalConsumerNoticeDays: 60, + minimumRollbackDays: 14, + requireOpenApiDiff: true, + requireWebhookSchemaVersion: true, + requireSandboxEvidence: true, + requireDpaForRestrictedExports: true +}); + +const STATE_ORDER = ["ready", "watch", "hold", "blocked"]; + +const SECRET_KEYS = new Set([ + "apiKey", + "clientSecret", + "privateToken", + "signingKey", + "taxId" +]); + +export function buildApiChangeGovernance(input, options = {}) { + const generatedAt = options.generatedAt ?? new Date().toISOString(); + const policy = { ...DEFAULT_POLICY, ...(input?.policy ?? {}) }; + const asOf = new Date(input?.asOf ?? generatedAt); + const integrations = Array.isArray(input?.integrations) ? input.integrations : []; + const changes = Array.isArray(input?.changes) ? input.changes : []; + + const reviews = changes.map((change) => evaluateChange(change, integrations, policy, asOf)); + const dashboard = buildDashboard(reviews); + const exportManifest = buildExportManifest(input?.portfolioId, reviews); + const webhookEvents = reviews.map((review) => buildWebhookEvent(review, input?.signingKeyId)); + const sanitizedInputEcho = redactSensitiveFields(input); + const digestPayload = { + portfolioId: input?.portfolioId ?? null, + asOf: input?.asOf ?? null, + dashboard, + exportManifest, + reviews: reviews.map(toDigestableReview), + webhookEvents: webhookEvents.map(({ signature, ...event }) => event), + sanitizedInputEcho + }; + + return { + generatedAt, + portfolioId: input?.portfolioId ?? "unknown-portfolio", + dashboard, + reviews, + exportManifest, + webhookEvents, + sanitizedInputEcho, + auditDigest: sha256(stableStringify(digestPayload)) + }; +} + +export function evaluateChange(change, integrations, policy = DEFAULT_POLICY, asOf = new Date()) { + const affectedConsumers = buildAffectedConsumers(change, integrations, policy, asOf); + const findings = [ + ...evaluateContractEvidence(change, policy), + ...evaluateDeprecation(change, policy, asOf), + ...evaluateWebhookEnvelope(change, policy), + ...evaluateConsumerReadiness(change, affectedConsumers, policy), + ...evaluateRestrictedData(change, affectedConsumers, policy) + ]; + const state = stateFromFindings(findings); + const adminActions = buildAdminActions(change, findings, affectedConsumers); + + return { + changeId: change.changeId, + title: change.title, + ownerTeam: change.ownerTeam ?? "unassigned", + surface: change.surface ?? "unknown", + state, + riskScore: riskScore(findings, affectedConsumers), + effectiveDate: change.effectiveDate ?? null, + breaking: Boolean(change.breaking), + compatibilityWindowDays: numberOrZero(change.parallelRunDays), + affectedRoutes: Array.isArray(change.affectedRoutes) ? change.affectedRoutes : [], + affectedWebhookTypes: Array.isArray(change.affectedWebhookTypes) ? change.affectedWebhookTypes : [], + affectedConsumers, + findings, + adminActions, + migrationPlan: buildMigrationPlan(change, affectedConsumers, findings), + evidenceDigest: sha256(stableStringify({ + changeId: change.changeId, + state, + findings, + affectedConsumers: affectedConsumers.map(toDigestableConsumer) + })) + }; +} + +export function redactSensitiveFields(value) { + if (Array.isArray(value)) { + return value.map(redactSensitiveFields); + } + + if (!value || typeof value !== "object") { + return value; + } + + return Object.fromEntries( + Object.entries(value).map(([key, entry]) => [ + key, + SECRET_KEYS.has(key) ? "[redacted]" : redactSensitiveFields(entry) + ]) + ); +} + +export 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 buildAffectedConsumers(change, integrations, policy, asOf) { + return integrations + .filter((integration) => isIntegrationAffected(change, integration)) + .map((integration) => { + const sandboxAgeDays = ageInDays(integration.lastSuccessfulSandboxRun, asOf); + const lacksFreshSandbox = sandboxAgeDays == null || sandboxAgeDays > 30; + const needsCriticalNotice = integration.criticality === "critical" + && daysUntil(change.effectiveDate, asOf) < policy.criticalConsumerNoticeDays; + const notificationMissing = integration.notificationStatus !== "acknowledged"; + const hasVersionRisk = Boolean(change.breaking) + && !integration.supportsVersionNegotiation + && !integration.pinnedApiVersions?.includes(change.proposedVersion); + const hasDpaGap = Boolean(change.restrictedResearchData) && !integration.hasDataProcessingAgreement; + const blockers = [ + hasVersionRisk ? "breaking change without version negotiation" : null, + hasDpaGap ? "restricted-data integration missing DPA" : null, + needsCriticalNotice && notificationMissing ? "critical consumer lacks acknowledged notice" : null + ].filter(Boolean); + const warnings = [ + lacksFreshSandbox ? "sandbox evidence is stale or absent" : null, + notificationMissing ? "notification not acknowledged" : null, + !integration.migrationTicket ? "no migration ticket linked" : null + ].filter(Boolean); + + return { + integrationId: integration.integrationId, + institution: integration.institution, + systemType: integration.systemType, + criticality: integration.criticality ?? "standard", + contactGroup: integration.contactGroup ?? "unknown", + readiness: blockers.length > 0 ? "blocked" : warnings.length > 0 ? "needs-review" : "ready", + blockers, + warnings, + migrationTicket: integration.migrationTicket ?? null, + lastSuccessfulSandboxRun: integration.lastSuccessfulSandboxRun ?? null + }; + }); +} + +function isIntegrationAffected(change, integration) { + const versions = integration.pinnedApiVersions ?? []; + const webhooks = integration.subscribedWebhookTypes ?? []; + const routeAffected = change.currentVersion && versions.includes(change.currentVersion); + const proposedVersionAffected = change.proposedVersion && versions.includes(change.proposedVersion); + const webhookAffected = (change.affectedWebhookTypes ?? []).some((type) => webhooks.includes(type)); + const newWebhookForSameFamily = (change.affectedWebhookTypes ?? []).some((type) => { + const family = type.split(".").slice(0, -1).join("."); + return webhooks.some((subscribed) => subscribed.startsWith(`${family}.`)); + }); + + return Boolean(routeAffected || proposedVersionAffected || webhookAffected || newWebhookForSameFamily); +} + +function evaluateContractEvidence(change, policy) { + const findings = []; + if (policy.requireOpenApiDiff && change.surface !== "webhook" && !change.openApiDiffAttached) { + findings.push(finding("blocker", "contract-diff", "OpenAPI or route contract diff is required before enterprise review.")); + } + + if (change.breaking && !change.openApiDiffAttached) { + findings.push(finding("blocker", "breaking-diff", "Breaking changes must include a machine-readable diff for admins and integrators.")); + } + + const diff = change.schemaDiff ?? {}; + if ((diff.removedFields ?? []).length > 0) { + findings.push(finding("blocker", "removed-fields", `Removed fields detected: ${diff.removedFields.join(", ")}.`)); + } + if ((diff.typeChanges ?? []).length > 0) { + findings.push(finding("warning", "type-changes", `Type changes require fixture-backed migration notes: ${diff.typeChanges.join(", ")}.`)); + } + if ((diff.newRequiredFields ?? []).length > 0 && change.changeKind !== "additive_version") { + findings.push(finding("warning", "new-required-fields", "New required fields should ship behind a new version or default compatibility mode.")); + } + + return findings; +} + +function evaluateDeprecation(change, policy, asOf) { + const findings = []; + const deprecationDays = daysUntil(change.effectiveDate, asOf); + const parallelRunDays = numberOrZero(change.parallelRunDays); + const rollbackPlanDays = numberOrZero(change.rollbackPlanDays); + + if (change.breaking && deprecationDays < policy.minimumBreakingDeprecationDays) { + findings.push(finding("blocker", "short-deprecation-window", `Breaking change has ${deprecationDays} days of notice; policy requires ${policy.minimumBreakingDeprecationDays}.`)); + } + if (change.breaking && parallelRunDays < policy.minimumBreakingDeprecationDays) { + findings.push(finding("blocker", "short-parallel-run", "Breaking changes need a parallel run window long enough for pinned institutional clients.")); + } + if (rollbackPlanDays < policy.minimumRollbackDays) { + findings.push(finding("warning", "rollback-window", `Rollback plan covers ${rollbackPlanDays} days; policy requires at least ${policy.minimumRollbackDays}.`)); + } + + return findings; +} + +function evaluateWebhookEnvelope(change, policy) { + const findings = []; + const envelope = change.webhookEnvelope ?? {}; + const touchesWebhooks = (change.affectedWebhookTypes ?? []).length > 0 || change.surface === "webhook"; + + if (!touchesWebhooks) { + return findings; + } + + if (!envelope.usesCloudEvents) { + findings.push(finding("warning", "event-envelope", "Webhook changes should keep a standard event envelope for routing and replay.")); + } + if (policy.requireWebhookSchemaVersion && !envelope.schemaVersionField) { + findings.push(finding("blocker", "schema-version", "Webhook payloads need an explicit schema version field.")); + } + if (!envelope.idempotencyKeyField) { + findings.push(finding("blocker", "idempotency-key", "Webhook payloads need a stable idempotency key for enterprise replay safety.")); + } + if (!envelope.signatureVersion) { + findings.push(finding("warning", "signature-version", "Webhook signing version should be explicit so consumers can rotate verifiers safely.")); + } + + return findings; +} + +function evaluateConsumerReadiness(change, affectedConsumers, policy) { + const findings = []; + const blockedConsumers = affectedConsumers.filter((consumer) => consumer.readiness === "blocked"); + const staleSandboxConsumers = affectedConsumers.filter((consumer) => + consumer.warnings.includes("sandbox evidence is stale or absent")); + const missingNoticeConsumers = affectedConsumers.filter((consumer) => + consumer.warnings.includes("notification not acknowledged")); + + if (blockedConsumers.length > 0) { + findings.push(finding("blocker", "consumer-blockers", `${blockedConsumers.length} affected integrations are blocked.`)); + } + if (policy.requireSandboxEvidence && staleSandboxConsumers.length > 0) { + findings.push(finding("warning", "sandbox-evidence", `${staleSandboxConsumers.length} affected integrations need fresh sandbox evidence.`)); + } + if (missingNoticeConsumers.length > 0) { + findings.push(finding("warning", "notice-ack", `${missingNoticeConsumers.length} affected integrations have not acknowledged migration notice.`)); + } + + return findings; +} + +function evaluateRestrictedData(change, affectedConsumers, policy) { + if (!policy.requireDpaForRestrictedExports || !change.restrictedResearchData) { + return []; + } + + const missingDpa = affectedConsumers.filter((consumer) => + consumer.blockers.includes("restricted-data integration missing DPA")); + if (missingDpa.length === 0) { + return []; + } + + return [finding("blocker", "restricted-data-dpa", "Restricted research data cannot flow to integrations without DPA evidence.")]; +} + +function buildDashboard(reviews) { + const byState = Object.fromEntries(STATE_ORDER.map((state) => [state, 0])); + for (const review of reviews) { + byState[review.state] += 1; + } + + const affectedConsumers = reviews.flatMap((review) => review.affectedConsumers); + return { + totalChanges: reviews.length, + byState, + blockedChanges: reviews.filter((review) => review.state === "blocked").length, + holdOrBlockedChanges: reviews.filter((review) => ["hold", "blocked"].includes(review.state)).length, + criticalIntegrationsImpacted: affectedConsumers.filter((consumer) => consumer.criticality === "critical").length, + consumerBlocks: affectedConsumers.filter((consumer) => consumer.readiness === "blocked").length, + uniqueInstitutionsImpacted: new Set(affectedConsumers.map((consumer) => consumer.institution)).size, + highestRiskScore: reviews.reduce((highest, review) => Math.max(highest, review.riskScore), 0) + }; +} + +function buildExportManifest(portfolioId, reviews) { + return { + manifestId: `api-change-governance:${portfolioId ?? "unknown"}`, + generatedFor: [ + "institutional-admin-dashboard", + "api-consumer-notice-pack", + "webhook-delivery-review", + "compliance-export-evidence" + ], + readyChangeIds: reviews.filter((review) => review.state === "ready").map((review) => review.changeId), + reviewChangeIds: reviews.filter((review) => review.state !== "ready").map((review) => review.changeId), + evidenceDigests: Object.fromEntries(reviews.map((review) => [review.changeId, review.evidenceDigest])) + }; +} + +function buildWebhookEvent(review, signingKeyId) { + const event = { + specversion: "1.0", + type: `scibase.enterprise.api_change.${review.state}`, + source: "/enterprise/api-change-governance", + id: sha256(`${review.changeId}:${review.state}:${review.evidenceDigest}`).slice(0, 32), + subject: review.changeId, + time: review.effectiveDate, + datacontenttype: "application/json", + data: { + changeId: review.changeId, + state: review.state, + riskScore: review.riskScore, + affectedConsumers: review.affectedConsumers.length, + blockers: review.findings.filter((item) => item.severity === "blocker").map((item) => item.code), + evidenceDigest: review.evidenceDigest + } + }; + const signaturePayload = stableStringify(event); + return { + ...event, + signatureKeyId: signingKeyId ?? "synthetic-signing-key", + signature: createHmac("sha256", signingKeyId ?? "synthetic-signing-key") + .update(signaturePayload) + .digest("hex") + }; +} + +function buildAdminActions(change, findings, affectedConsumers) { + const actions = findings.map((item) => { + if (item.severity === "blocker") return `Block ${change.changeId}: ${item.message}`; + return `Review ${change.changeId}: ${item.message}`; + }); + + for (const consumer of affectedConsumers) { + if (consumer.readiness === "blocked") { + actions.push(`Open migration review for ${consumer.institution} (${consumer.integrationId}).`); + } else if (consumer.readiness === "needs-review") { + actions.push(`Request sandbox acknowledgement from ${consumer.contactGroup} for ${consumer.integrationId}.`); + } + } + + if (actions.length === 0) { + actions.push(`Approve ${change.changeId} for enterprise rollout with monitored release notes.`); + } + + return [...new Set(actions)]; +} + +function buildMigrationPlan(change, affectedConsumers, findings) { + const blockers = findings.filter((item) => item.severity === "blocker").map((item) => item.code); + return { + changeId: change.changeId, + noticeAudience: affectedConsumers.map((consumer) => consumer.contactGroup), + sandboxFixturePack: change.sandboxEvidence?.fixturePack ?? null, + rollbackPlanDays: numberOrZero(change.rollbackPlanDays), + parallelRunDays: numberOrZero(change.parallelRunDays), + requiredBeforeRelease: blockers.length > 0 ? blockers : ["publish release note", "monitor webhook deliveries"], + suggestedOrder: [ + "publish contract diff and migration notes", + "run sandbox fixtures against affected integrations", + "collect critical-consumer acknowledgement", + "ship parallel version", + "monitor signed webhook evidence" + ] + }; +} + +function stateFromFindings(findings) { + const blockers = findings.filter((item) => item.severity === "blocker").length; + const warnings = findings.filter((item) => item.severity === "warning").length; + if (blockers >= 3) return "blocked"; + if (blockers > 0) return "hold"; + if (warnings > 0) return "watch"; + return "ready"; +} + +function riskScore(findings, affectedConsumers) { + const blockerScore = findings.filter((item) => item.severity === "blocker").length * 22; + const warningScore = findings.filter((item) => item.severity === "warning").length * 8; + const criticalScore = affectedConsumers.filter((consumer) => consumer.criticality === "critical").length * 6; + const blockedConsumerScore = affectedConsumers.filter((consumer) => consumer.readiness === "blocked").length * 12; + return Math.min(100, blockerScore + warningScore + criticalScore + blockedConsumerScore); +} + +function finding(severity, code, message) { + return { severity, code, message }; +} + +function daysUntil(dateString, asOf) { + if (!dateString) return 0; + const date = new Date(`${dateString}T00:00:00.000Z`); + return Math.max(0, Math.ceil((date.getTime() - asOf.getTime()) / 86_400_000)); +} + +function ageInDays(dateString, asOf) { + if (!dateString) return null; + const date = new Date(`${dateString}T00:00:00.000Z`); + return Math.max(0, Math.floor((asOf.getTime() - date.getTime()) / 86_400_000)); +} + +function numberOrZero(value) { + return Number.isFinite(Number(value)) ? Number(value) : 0; +} + +function toDigestableReview(review) { + return { + changeId: review.changeId, + state: review.state, + riskScore: review.riskScore, + findings: review.findings, + affectedConsumers: review.affectedConsumers.map(toDigestableConsumer), + evidenceDigest: review.evidenceDigest + }; +} + +function toDigestableConsumer(consumer) { + return { + integrationId: consumer.integrationId, + readiness: consumer.readiness, + blockers: consumer.blockers, + warnings: consumer.warnings + }; +} + +function sha256(value) { + return createHash("sha256").update(value).digest("hex"); +} diff --git a/enterprise-api-change-governance/test/api-change-governance.test.js b/enterprise-api-change-governance/test/api-change-governance.test.js new file mode 100644 index 0000000..2c82a81 --- /dev/null +++ b/enterprise-api-change-governance/test/api-change-governance.test.js @@ -0,0 +1,71 @@ +import assert from "node:assert/strict"; +import { describe, it } from "node:test"; +import sample from "../data/sample-change-plan.json" with { type: "json" }; +import { + buildApiChangeGovernance, + evaluateChange, + redactSensitiveFields, + stableStringify +} from "../src/api-change-governance.js"; + +describe("enterprise API change governance", () => { + it("approves additive versioned API changes with fresh sandbox evidence", () => { + const additive = sample.changes[0]; + const review = evaluateChange(additive, sample.integrations, sample.policy, new Date(sample.asOf)); + + assert.equal(review.state, "ready"); + assert.equal(review.breaking, false); + assert.ok(review.riskScore < 20); + assert.equal(review.findings.length, 0); + assert.ok(review.adminActions.some((action) => action.includes("Approve"))); + }); + + it("blocks breaking webhook removals without versioning, idempotency, or adequate notice", () => { + const breaking = sample.changes[1]; + const review = evaluateChange(breaking, sample.integrations, sample.policy, new Date(sample.asOf)); + const codes = review.findings.map((finding) => finding.code); + + assert.equal(review.state, "blocked"); + assert.ok(codes.includes("breaking-diff")); + assert.ok(codes.includes("removed-fields")); + assert.ok(codes.includes("short-deprecation-window")); + assert.ok(codes.includes("schema-version")); + assert.ok(codes.includes("idempotency-key")); + }); + + it("detects restricted-data DPA and sandbox gaps before funder webhook rollout", () => { + const restricted = sample.changes[2]; + const review = evaluateChange(restricted, sample.integrations, sample.policy, new Date(sample.asOf)); + const funder = review.affectedConsumers.find((consumer) => consumer.integrationId === "funder-reporter-nightly"); + + assert.equal(review.state, "hold"); + assert.equal(funder.readiness, "blocked"); + assert.ok(funder.blockers.includes("restricted-data integration missing DPA")); + assert.ok(review.adminActions.some((action) => action.includes("Open migration review"))); + }); + + it("builds deterministic dashboard, manifest, webhook events, and digest", () => { + const first = buildApiChangeGovernance(sample, { generatedAt: sample.asOf }); + const second = buildApiChangeGovernance(sample, { generatedAt: "2035-01-01T00:00:00.000Z" }); + + assert.equal(first.auditDigest, second.auditDigest); + assert.equal(first.dashboard.totalChanges, 3); + assert.deepEqual(first.dashboard.byState, { ready: 1, watch: 0, hold: 1, blocked: 1 }); + assert.equal(first.exportManifest.readyChangeIds.includes("api-project-export-v3"), true); + assert.equal(first.webhookEvents.length, 3); + assert.match(first.webhookEvents[0].signature, /^[a-f0-9]{64}$/); + }); + + it("redacts private credentials before echoing input into review artifacts", () => { + const redacted = redactSensitiveFields({ + apiKey: "secret-api-key", + nested: { signingKey: "secret-hmac-key", safe: "visible" } + }); + const serialized = stableStringify(redacted); + + assert.equal(serialized.includes("secret-api-key"), false); + assert.equal(serialized.includes("secret-hmac-key"), false); + assert.equal(serialized.includes("[redacted]"), true); + assert.equal(serialized.includes("visible"), true); + }); +});