@hivemagnus/magnus-server 0.0.0-stage → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +171 -0
- package/LICENSE +202 -0
- package/README.md +169 -2
- package/agent-version.json +1 -0
- package/dist/ai/HEARTBEAT.md +56 -0
- package/dist/ai/IDENTITY.md +54 -0
- package/dist/ai/MEMORY.md +40 -0
- package/dist/ai/TOOLS.md +538 -0
- package/dist/ai/USER.md +32 -0
- package/dist/cli/args.js +62 -0
- package/dist/cli/env-catalog.js +131 -0
- package/dist/cli/preflight.js +89 -0
- package/dist/cli.js +175 -0
- package/dist/config/cors.js +16 -0
- package/dist/config/env.js +392 -0
- package/dist/config/features.js +16 -0
- package/dist/config/index.js +3 -0
- package/dist/config/listener-security.js +38 -0
- package/dist/config/version.js +31 -0
- package/dist/db/client.js +44 -0
- package/dist/db/generated/prisma/browser.js +18 -0
- package/dist/db/generated/prisma/client.js +35 -0
- package/dist/db/generated/prisma/commonInputTypes.js +11 -0
- package/dist/db/generated/prisma/enums.js +42 -0
- package/dist/db/generated/prisma/internal/class.js +50 -0
- package/dist/db/generated/prisma/internal/prismaNamespace.js +1598 -0
- package/dist/db/generated/prisma/internal/prismaNamespaceBrowser.js +1569 -0
- package/dist/db/generated/prisma/models/A2ATask.js +2 -0
- package/dist/db/generated/prisma/models/AdmissionFence.js +2 -0
- package/dist/db/generated/prisma/models/Agent.js +2 -0
- package/dist/db/generated/prisma/models/AiConversation.js +2 -0
- package/dist/db/generated/prisma/models/AiDashboard.js +2 -0
- package/dist/db/generated/prisma/models/AiMemory.js +2 -0
- package/dist/db/generated/prisma/models/AiMessage.js +2 -0
- package/dist/db/generated/prisma/models/AiScheduledOp.js +2 -0
- package/dist/db/generated/prisma/models/AlertConditionState.js +2 -0
- package/dist/db/generated/prisma/models/AlertRule.js +2 -0
- package/dist/db/generated/prisma/models/AlertViolation.js +2 -0
- package/dist/db/generated/prisma/models/AnomalyEvent.js +2 -0
- package/dist/db/generated/prisma/models/ApplicationObservation.js +2 -0
- package/dist/db/generated/prisma/models/ArdentInstallation.js +2 -0
- package/dist/db/generated/prisma/models/ArdentProfile.js +2 -0
- package/dist/db/generated/prisma/models/ArdentScopeEnrollment.js +2 -0
- package/dist/db/generated/prisma/models/CommandResourceFence.js +2 -0
- package/dist/db/generated/prisma/models/CommandResourceLease.js +2 -0
- package/dist/db/generated/prisma/models/CommandResult.js +2 -0
- package/dist/db/generated/prisma/models/CronJob.js +2 -0
- package/dist/db/generated/prisma/models/CronJobExecution.js +2 -0
- package/dist/db/generated/prisma/models/DeployJob.js +2 -0
- package/dist/db/generated/prisma/models/DriftScan.js +2 -0
- package/dist/db/generated/prisma/models/EngineeringMetricSample.js +2 -0
- package/dist/db/generated/prisma/models/EngineeringRecoveryExercise.js +2 -0
- package/dist/db/generated/prisma/models/ExecutionRun.js +2 -0
- package/dist/db/generated/prisma/models/FilePatchTask.js +2 -0
- package/dist/db/generated/prisma/models/GpuSnapshot.js +2 -0
- package/dist/db/generated/prisma/models/HealthSnapshot.js +2 -0
- package/dist/db/generated/prisma/models/HermesCoreRollout.js +2 -0
- package/dist/db/generated/prisma/models/HermesCoreRolloutHost.js +2 -0
- package/dist/db/generated/prisma/models/HermesCredentialPool.js +2 -0
- package/dist/db/generated/prisma/models/HermesCredentialPoolAttachment.js +2 -0
- package/dist/db/generated/prisma/models/HermesCronJob.js +2 -0
- package/dist/db/generated/prisma/models/HermesHook.js +2 -0
- package/dist/db/generated/prisma/models/HermesInstallation.js +2 -0
- package/dist/db/generated/prisma/models/HermesMcpServer.js +2 -0
- package/dist/db/generated/prisma/models/HermesMcpServerAttachment.js +2 -0
- package/dist/db/generated/prisma/models/HermesMessagingBinding.js +2 -0
- package/dist/db/generated/prisma/models/HermesOperationJob.js +2 -0
- package/dist/db/generated/prisma/models/HermesPluginInstall.js +2 -0
- package/dist/db/generated/prisma/models/HermesProfile.js +2 -0
- package/dist/db/generated/prisma/models/HermesProviderConfig.js +2 -0
- package/dist/db/generated/prisma/models/HermesRegistryClient.js +2 -0
- package/dist/db/generated/prisma/models/HermesRegistryInventory.js +2 -0
- package/dist/db/generated/prisma/models/HermesRegistryReceipt.js +2 -0
- package/dist/db/generated/prisma/models/HermesRegistryRevision.js +2 -0
- package/dist/db/generated/prisma/models/HermesRegistryTarget.js +2 -0
- package/dist/db/generated/prisma/models/HermesRole.js +2 -0
- package/dist/db/generated/prisma/models/HermesRoleApplication.js +2 -0
- package/dist/db/generated/prisma/models/HermesSkillAssignment.js +2 -0
- package/dist/db/generated/prisma/models/HermesToolsetPolicy.js +2 -0
- package/dist/db/generated/prisma/models/IncidentLesson.js +2 -0
- package/dist/db/generated/prisma/models/LibraryCategory.js +2 -0
- package/dist/db/generated/prisma/models/LibraryFile.js +2 -0
- package/dist/db/generated/prisma/models/LogEntry.js +2 -0
- package/dist/db/generated/prisma/models/MagnusReleaseCanary.js +2 -0
- package/dist/db/generated/prisma/models/MaintenanceWindow.js +2 -0
- package/dist/db/generated/prisma/models/ManagedCronDefinition.js +2 -0
- package/dist/db/generated/prisma/models/ManagedCronOccurrence.js +2 -0
- package/dist/db/generated/prisma/models/Node.js +2 -0
- package/dist/db/generated/prisma/models/Notification.js +2 -0
- package/dist/db/generated/prisma/models/NotificationPreference.js +2 -0
- package/dist/db/generated/prisma/models/RemediationExecution.js +2 -0
- package/dist/db/generated/prisma/models/RemediationRule.js +2 -0
- package/dist/db/generated/prisma/models/RemoteCommand.js +2 -0
- package/dist/db/generated/prisma/models/ResourceDrainParticipant.js +2 -0
- package/dist/db/generated/prisma/models/ResourceEnrollment.js +2 -0
- package/dist/db/generated/prisma/models/ResourceParticipantWork.js +2 -0
- package/dist/db/generated/prisma/models/Role.js +2 -0
- package/dist/db/generated/prisma/models/RoleApplication.js +2 -0
- package/dist/db/generated/prisma/models/RolloutTarget.js +2 -0
- package/dist/db/generated/prisma/models/SecurityExportOutbox.js +2 -0
- package/dist/db/generated/prisma/models/SecurityFusionAction.js +2 -0
- package/dist/db/generated/prisma/models/SecurityFusionActionApproval.js +2 -0
- package/dist/db/generated/prisma/models/SecurityFusionCase.js +2 -0
- package/dist/db/generated/prisma/models/SecurityFusionCaseActivity.js +2 -0
- package/dist/db/generated/prisma/models/SecurityFusionCaseEntity.js +2 -0
- package/dist/db/generated/prisma/models/SecurityFusionCaseEvent.js +2 -0
- package/dist/db/generated/prisma/models/SecurityFusionCaseRelationship.js +2 -0
- package/dist/db/generated/prisma/models/SecurityFusionCollector.js +2 -0
- package/dist/db/generated/prisma/models/SecurityFusionCommandOutbox.js +2 -0
- package/dist/db/generated/prisma/models/SecurityFusionDeletionReceipt.js +2 -0
- package/dist/db/generated/prisma/models/SecurityFusionDelivery.js +2 -0
- package/dist/db/generated/prisma/models/SecurityFusionEntity.js +2 -0
- package/dist/db/generated/prisma/models/SecurityFusionEntityAlias.js +2 -0
- package/dist/db/generated/prisma/models/SecurityFusionEvent.js +2 -0
- package/dist/db/generated/prisma/models/SecurityFusionEventEntity.js +2 -0
- package/dist/db/generated/prisma/models/SecurityFusionEvidence.js +2 -0
- package/dist/db/generated/prisma/models/SecurityFusionInvestigation.js +2 -0
- package/dist/db/generated/prisma/models/SecurityFusionNonce.js +2 -0
- package/dist/db/generated/prisma/models/SecurityFusionReceiptChainHead.js +2 -0
- package/dist/db/generated/prisma/models/SecurityFusionRuleVersion.js +2 -0
- package/dist/db/generated/prisma/models/SecurityFusionSourceState.js +2 -0
- package/dist/db/generated/prisma/models/SecurityServiceState.js +2 -0
- package/dist/db/generated/prisma/models/SharedResourceFence.js +2 -0
- package/dist/db/generated/prisma/models/SharedResourceLease.js +2 -0
- package/dist/db/generated/prisma/models/SharedResourceLeaseResource.js +2 -0
- package/dist/db/generated/prisma/models/SkillInstallation.js +2 -0
- package/dist/db/generated/prisma/models/SkillRegistryEntry.js +2 -0
- package/dist/db/generated/prisma/models/SkillTag.js +2 -0
- package/dist/db/generated/prisma/models/SkillVersion.js +2 -0
- package/dist/db/generated/prisma/models/TeamApprovalDecision.js +2 -0
- package/dist/db/generated/prisma/models/User.js +2 -0
- package/dist/db/generated/prisma/models/WebhookDelivery.js +2 -0
- package/dist/db/generated/prisma/models/WebhookEndpoint.js +2 -0
- package/dist/db/generated/prisma/models.js +2 -0
- package/dist/db/index.js +3 -0
- package/dist/db/read-retry.js +35 -0
- package/dist/db/seed.js +348 -0
- package/dist/index.js +162 -0
- package/dist/middleware/auth.js +361 -0
- package/dist/middleware/body-limit.js +14 -0
- package/dist/middleware/correlation-id.js +13 -0
- package/dist/middleware/error-handler.js +28 -0
- package/dist/middleware/iam-token.js +21 -0
- package/dist/middleware/rate-limit.js +78 -0
- package/dist/middleware/request-logger.js +85 -0
- package/dist/middleware/security-headers.js +23 -0
- package/dist/middleware/tracing.js +120 -0
- package/dist/middleware/user-status.js +33 -0
- package/dist/openapi/spec.js +2656 -0
- package/dist/queue/redis.js +20 -0
- package/dist/queue/requeue.js +27 -0
- package/dist/queue/workers.js +342 -0
- package/dist/routes/ai-schedule.js +73 -0
- package/dist/routes/alerts.js +79 -0
- package/dist/routes/anomalies.js +101 -0
- package/dist/routes/ardent.js +82 -0
- package/dist/routes/assistant.js +417 -0
- package/dist/routes/audit.js +138 -0
- package/dist/routes/codex.js +65 -0
- package/dist/routes/commands.js +226 -0
- package/dist/routes/credentials.js +58 -0
- package/dist/routes/cron.js +292 -0
- package/dist/routes/dependencies.js +49 -0
- package/dist/routes/deploy.js +18 -0
- package/dist/routes/discovery.js +168 -0
- package/dist/routes/docs.js +24 -0
- package/dist/routes/drift.js +126 -0
- package/dist/routes/events-stream.js +87 -0
- package/dist/routes/files.js +214 -0
- package/dist/routes/fleet-checks.js +21 -0
- package/dist/routes/health.js +133 -0
- package/dist/routes/heartbeat.js +545 -0
- package/dist/routes/hermes-registry.js +278 -0
- package/dist/routes/hermes.js +61 -0
- package/dist/routes/library.js +402 -0
- package/dist/routes/logs.js +172 -0
- package/dist/routes/managed-cron.js +23 -0
- package/dist/routes/mcp.js +39 -0
- package/dist/routes/metrics.js +187 -0
- package/dist/routes/nodes.js +951 -0
- package/dist/routes/notifications.js +130 -0
- package/dist/routes/ops.js +258 -0
- package/dist/routes/orchestra.js +430 -0
- package/dist/routes/predictions.js +23 -0
- package/dist/routes/profiling.js +68 -0
- package/dist/routes/quick-command.js +72 -0
- package/dist/routes/remediation.js +111 -0
- package/dist/routes/resource-leases.js +297 -0
- package/dist/routes/roles.js +370 -0
- package/dist/routes/rollout.js +124 -0
- package/dist/routes/scan.js +116 -0
- package/dist/routes/security-backup.js +58 -0
- package/dist/routes/security-fusion.js +605 -0
- package/dist/routes/security-integration.js +71 -0
- package/dist/routes/self-update.js +129 -0
- package/dist/routes/shared-resource-leases.js +78 -0
- package/dist/routes/skills.js +485 -0
- package/dist/routes/stats.js +96 -0
- package/dist/routes/team-approvals.js +23 -0
- package/dist/routes/terminal-ticket.js +15 -0
- package/dist/routes/terminal.js +17 -0
- package/dist/routes/topology.js +37 -0
- package/dist/routes/users.js +219 -0
- package/dist/routes/webhooks.js +137 -0
- package/dist/schemas/command-reconciliation.js +14 -0
- package/dist/schemas/commands.js +51 -0
- package/dist/schemas/cron.js +33 -0
- package/dist/schemas/deploy.js +20 -0
- package/dist/schemas/files.js +14 -0
- package/dist/schemas/hermes.js +69 -0
- package/dist/schemas/index.js +11 -0
- package/dist/schemas/library.js +48 -0
- package/dist/schemas/logs.js +35 -0
- package/dist/schemas/managed-cron.js +127 -0
- package/dist/schemas/node-responses.js +87 -0
- package/dist/schemas/node-session-receipt.js +84 -0
- package/dist/schemas/nodes.js +36 -0
- package/dist/schemas/orchestra.js +118 -0
- package/dist/schemas/resource-participant-reconciliation.js +99 -0
- package/dist/schemas/roles.js +54 -0
- package/dist/schemas/shared-resource-lease.js +132 -0
- package/dist/schemas/skills.js +44 -0
- package/dist/schemas/users.js +28 -0
- package/dist/scripts/run-disposable-recovery-exercise.js +39 -0
- package/dist/sdk/index.js +2 -0
- package/dist/sdk/node-api-client.js +474 -0
- package/dist/security/approval-assertion.js +66 -0
- package/dist/security/command-completion-registry.js +35 -0
- package/dist/security/command-request.js +12 -0
- package/dist/security/command-result-signature.js +34 -0
- package/dist/security/managed-http.js +45 -0
- package/dist/security/native-completion-registry.js +44 -0
- package/dist/server.js +178 -0
- package/dist/services/admission-fence.js +71 -0
- package/dist/services/ai-assistant-service.js +2523 -0
- package/dist/services/ai-correlation-service.js +225 -0
- package/dist/services/ai-export-service.js +141 -0
- package/dist/services/ai-insights-service.js +219 -0
- package/dist/services/ai-schedule-service.js +145 -0
- package/dist/services/ai-scratchpad-service.js +222 -0
- package/dist/services/ai-tools.js +1343 -0
- package/dist/services/alert-service.js +313 -0
- package/dist/services/anomaly-detector.js +419 -0
- package/dist/services/api-local-work.js +79 -0
- package/dist/services/application-probes.js +309 -0
- package/dist/services/ardent-scope-enrollment.js +123 -0
- package/dist/services/ardent-service.js +86 -0
- package/dist/services/audit-service.js +308 -0
- package/dist/services/codex-service.js +209 -0
- package/dist/services/command-lifecycle.js +60 -0
- package/dist/services/command-resource-leases.js +305 -0
- package/dist/services/control-host-node.js +242 -0
- package/dist/services/credential-rotation-service.js +294 -0
- package/dist/services/dependency-service.js +145 -0
- package/dist/services/disposable-recovery-runner.js +595 -0
- package/dist/services/docker-group-health.js +121 -0
- package/dist/services/engineering-assurance.js +492 -0
- package/dist/services/engineering-evidence-ledger.js +94 -0
- package/dist/services/engineering-metric-evidence.js +26 -0
- package/dist/services/event-bus.js +227 -0
- package/dist/services/fleet-check-log.js +102 -0
- package/dist/services/forecast-service.js +154 -0
- package/dist/services/health-retention.js +175 -0
- package/dist/services/heartbeat-stream.js +63 -0
- package/dist/services/hermes-catalog-import.js +62 -0
- package/dist/services/hermes-cli-parser.js +671 -0
- package/dist/services/hermes-core-rollout.js +90 -0
- package/dist/services/hermes-dashboard-endpoint.js +13 -0
- package/dist/services/hermes-plugin-source.js +149 -0
- package/dist/services/hermes-registry-service.js +258 -0
- package/dist/services/hermes-service.js +566 -0
- package/dist/services/hivemem-client.js +85 -0
- package/dist/services/host-local-writer.js +82 -0
- package/dist/services/incident-evidence.js +110 -0
- package/dist/services/incident-lessons.js +186 -0
- package/dist/services/llm-providers/anthropic.js +143 -0
- package/dist/services/llm-providers/index.js +7 -0
- package/dist/services/llm-providers/llama-cpp.js +237 -0
- package/dist/services/llm-providers/ollama.js +292 -0
- package/dist/services/llm-providers/openai-compatible.js +200 -0
- package/dist/services/llm-providers/provider-error.js +36 -0
- package/dist/services/llm-providers/registry.js +45 -0
- package/dist/services/llm-providers/types.js +2 -0
- package/dist/services/log-emitter.js +19 -0
- package/dist/services/log-retention.js +66 -0
- package/dist/services/maintenance-service.js +29 -0
- package/dist/services/managed-cron.js +493 -0
- package/dist/services/managed-ssh-admission.js +68 -0
- package/dist/services/monitor-pulse.js +10 -0
- package/dist/services/node-issue-monitor.js +71 -0
- package/dist/services/node-service.js +35 -0
- package/dist/services/notification-retention.js +51 -0
- package/dist/services/notification-service.js +94 -0
- package/dist/services/operational-observations.js +112 -0
- package/dist/services/operations-config.js +67 -0
- package/dist/services/ops-service.js +491 -0
- package/dist/services/orchestra-extras-service.js +370 -0
- package/dist/services/orchestra-resources-service.js +530 -0
- package/dist/services/os-scheduler-writer.js +73 -0
- package/dist/services/password-service.js +22 -0
- package/dist/services/prediction-service.js +284 -0
- package/dist/services/recovery-exercises.js +682 -0
- package/dist/services/remediation-service.js +325 -0
- package/dist/services/resource-enrollment.js +433 -0
- package/dist/services/resource-node-lock.js +18 -0
- package/dist/services/resource-participant-work.js +302 -0
- package/dist/services/role-service.js +388 -0
- package/dist/services/rollout-service.js +440 -0
- package/dist/services/security-export.js +185 -0
- package/dist/services/security-fusion-actions.js +22 -0
- package/dist/services/security-fusion-approvals.js +83 -0
- package/dist/services/security-fusion-evidence-integrity.js +144 -0
- package/dist/services/security-fusion-ingest.js +101 -0
- package/dist/services/security-fusion-outbox.js +185 -0
- package/dist/services/security-fusion-receipt-chain.js +134 -0
- package/dist/services/security-fusion-service.js +751 -0
- package/dist/services/security-fusion-verification.js +50 -0
- package/dist/services/self-update-service.js +200 -0
- package/dist/services/shared-resource-leases.js +413 -0
- package/dist/services/skill-publication-bundle.js +90 -0
- package/dist/services/skill-publication-settlement.js +36 -0
- package/dist/services/skill-publication-state.js +40 -0
- package/dist/services/skill-registry-service.js +327 -0
- package/dist/services/skill-service.js +148 -0
- package/dist/services/ssh-exec-service.js +139 -0
- package/dist/services/ssh-host-verification.js +27 -0
- package/dist/services/tailscale-node-key-service.js +75 -0
- package/dist/services/team-approvals.js +118 -0
- package/dist/services/topology-service.js +138 -0
- package/dist/services/transfer-authority-snapshot.js +151 -0
- package/dist/services/webhook-credential-migration.js +97 -0
- package/dist/services/webhook-security.js +242 -0
- package/dist/services/webhook-service.js +494 -0
- package/dist/types/auth.js +3 -0
- package/dist/types/index.js +3 -0
- package/dist/types/library.js +3 -0
- package/dist/types/notifications.js +3 -0
- package/dist/types/roles.js +3 -0
- package/dist/types/shared-contracts.js +3 -0
- package/dist/utils/env.js +141 -0
- package/dist/utils/heartbeat-peer.js +19 -0
- package/dist/utils/idempotency.js +45 -0
- package/dist/utils/ip.js +25 -0
- package/dist/utils/json-fields.js +94 -0
- package/dist/utils/local-tls.js +39 -0
- package/dist/utils/magnus-home.js +15 -0
- package/dist/utils/node-url.js +59 -0
- package/dist/utils/redact.js +26 -0
- package/dist/utils/safe-equal.js +13 -0
- package/dist/utils/sentinel-nodes.js +16 -0
- package/dist/utils/validate.js +41 -0
- package/package.json +119 -4
- package/prisma/migrations/20261031000000_baseline/migration.sql +2935 -0
- package/prisma/migrations/migration_lock.toml +3 -0
- package/prisma/schema.prisma +2185 -0
- package/prisma.config.ts +14 -0
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Durable encrypted credential rotation.
|
|
3
|
+
*
|
|
4
|
+
* Rotation state is stored as AES-256-GCM encrypted envelopes on disk. Only
|
|
5
|
+
* short SHA-256 fingerprints are exposed to route handlers, audit logs, and UI
|
|
6
|
+
* callers. The active secret plus one unexpired previous secret are accepted so
|
|
7
|
+
* agents can roll during a grace window without fleet downtime.
|
|
8
|
+
*/
|
|
9
|
+
import { magnusHomePath } from '../utils/magnus-home.js';
|
|
10
|
+
import { createCipheriv, createDecipheriv, createHash, randomBytes, timingSafeEqual, } from 'node:crypto';
|
|
11
|
+
import { closeSync, existsSync, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync, renameSync, writeFileSync, } from 'node:fs';
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
export const DEFAULT_GRACE_MS = 60 * 60 * 1_000; // 1 hour
|
|
14
|
+
const STORE_VERSION = 1;
|
|
15
|
+
const ENVELOPE_PREFIX = 'enc:v1:';
|
|
16
|
+
function defaultStatePath() {
|
|
17
|
+
return magnusHomePath('state', 'credential-rotations.json');
|
|
18
|
+
}
|
|
19
|
+
export function getCredentialRotationStatePath() {
|
|
20
|
+
return process.env['CREDENTIAL_ROTATION_STATE_PATH'] ?? defaultStatePath();
|
|
21
|
+
}
|
|
22
|
+
function encryptionKey() {
|
|
23
|
+
const source = process.env['CREDENTIAL_ROTATION_KEY'] ?? '';
|
|
24
|
+
if (source.length < 32) {
|
|
25
|
+
throw new Error('CREDENTIAL_ROTATION_KEY must be a dedicated secret of at least 32 characters');
|
|
26
|
+
}
|
|
27
|
+
return createHash('sha256').update(source).digest();
|
|
28
|
+
}
|
|
29
|
+
export function secretFingerprint(value) {
|
|
30
|
+
return createHash('sha256').update(value).digest('hex').slice(0, 12);
|
|
31
|
+
}
|
|
32
|
+
function aadFor(name, id) {
|
|
33
|
+
return Buffer.from(`magnus:credential:${name}:${id}`, 'utf-8');
|
|
34
|
+
}
|
|
35
|
+
function encryptSecret(name, id, plaintext) {
|
|
36
|
+
const iv = randomBytes(12);
|
|
37
|
+
const cipher = createCipheriv('aes-256-gcm', encryptionKey(), iv);
|
|
38
|
+
cipher.setAAD(aadFor(name, id));
|
|
39
|
+
const ciphertext = Buffer.concat([cipher.update(plaintext, 'utf-8'), cipher.final()]);
|
|
40
|
+
const tag = cipher.getAuthTag();
|
|
41
|
+
return `${ENVELOPE_PREFIX}${iv.toString('base64url')}:${tag.toString('base64url')}:${ciphertext.toString('base64url')}`;
|
|
42
|
+
}
|
|
43
|
+
function decryptSecret(name, version) {
|
|
44
|
+
if (!version.encryptedSecret.startsWith(ENVELOPE_PREFIX)) {
|
|
45
|
+
throw new Error(`Credential ${name} is not encrypted`);
|
|
46
|
+
}
|
|
47
|
+
const payload = version.encryptedSecret.slice(ENVELOPE_PREFIX.length);
|
|
48
|
+
const [ivRaw, tagRaw, ciphertextRaw] = payload.split(':');
|
|
49
|
+
if (!ivRaw || !tagRaw || ciphertextRaw === undefined) {
|
|
50
|
+
throw new Error(`Credential ${name} has a malformed encrypted envelope`);
|
|
51
|
+
}
|
|
52
|
+
const decipher = createDecipheriv('aes-256-gcm', encryptionKey(), Buffer.from(ivRaw, 'base64url'));
|
|
53
|
+
decipher.setAAD(aadFor(name, version.id));
|
|
54
|
+
decipher.setAuthTag(Buffer.from(tagRaw, 'base64url'));
|
|
55
|
+
return decipher.update(Buffer.from(ciphertextRaw, 'base64url')).toString('utf-8') + decipher.final('utf-8');
|
|
56
|
+
}
|
|
57
|
+
function emptyStore() {
|
|
58
|
+
return { version: STORE_VERSION, credentials: {} };
|
|
59
|
+
}
|
|
60
|
+
function readStore() {
|
|
61
|
+
const statePath = getCredentialRotationStatePath();
|
|
62
|
+
if (!existsSync(statePath))
|
|
63
|
+
return emptyStore();
|
|
64
|
+
const metadata = lstatSync(statePath);
|
|
65
|
+
if (!metadata.isFile() || metadata.isSymbolicLink() || (metadata.mode & 0o077) !== 0) {
|
|
66
|
+
throw new Error('Credential rotation state must be a regular non-symlink file with mode 0600');
|
|
67
|
+
}
|
|
68
|
+
const parsed = JSON.parse(readFileSync(statePath, 'utf-8'));
|
|
69
|
+
if (parsed.version !== STORE_VERSION || !parsed.credentials || typeof parsed.credentials !== 'object') {
|
|
70
|
+
throw new Error('Credential rotation state has an unsupported schema');
|
|
71
|
+
}
|
|
72
|
+
return parsed;
|
|
73
|
+
}
|
|
74
|
+
function writeStore(store) {
|
|
75
|
+
const statePath = getCredentialRotationStatePath();
|
|
76
|
+
mkdirSync(path.dirname(statePath), { recursive: true, mode: 0o700 });
|
|
77
|
+
const tmpPath = `${statePath}.${process.pid}.${Date.now()}.tmp`;
|
|
78
|
+
writeFileSync(tmpPath, `${JSON.stringify(store, null, 2)}\n`, { encoding: 'utf-8', mode: 0o600 });
|
|
79
|
+
const fileDescriptor = openSync(tmpPath, 'r');
|
|
80
|
+
try {
|
|
81
|
+
fsyncSync(fileDescriptor);
|
|
82
|
+
}
|
|
83
|
+
finally {
|
|
84
|
+
closeSync(fileDescriptor);
|
|
85
|
+
}
|
|
86
|
+
renameSync(tmpPath, statePath);
|
|
87
|
+
const directoryDescriptor = openSync(path.dirname(statePath), 'r');
|
|
88
|
+
try {
|
|
89
|
+
fsyncSync(directoryDescriptor);
|
|
90
|
+
}
|
|
91
|
+
finally {
|
|
92
|
+
closeSync(directoryDescriptor);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
function nowIso() {
|
|
96
|
+
return new Date().toISOString();
|
|
97
|
+
}
|
|
98
|
+
function makeVersion(name, plaintext, createdAt = nowIso(), expiresAt) {
|
|
99
|
+
assertCredentialSecret(name, plaintext);
|
|
100
|
+
const id = randomBytes(16).toString('base64url');
|
|
101
|
+
return {
|
|
102
|
+
id,
|
|
103
|
+
encryptedSecret: encryptSecret(name, id, plaintext),
|
|
104
|
+
fingerprint: secretFingerprint(plaintext),
|
|
105
|
+
createdAt,
|
|
106
|
+
...(expiresAt ? { expiresAt } : {}),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
function assertCredentialSecret(name, value) {
|
|
110
|
+
if (value.length < 32) {
|
|
111
|
+
throw new Error(`${name} must contain at least 32 characters`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
function decryptValidatedSecret(name, version, allowLegacyRetirement = false) {
|
|
115
|
+
const value = decryptSecret(name, version);
|
|
116
|
+
if (version.legacyRetirementOnly) {
|
|
117
|
+
const createdAt = Date.parse(version.createdAt);
|
|
118
|
+
const expiresAt = version.expiresAt ? Date.parse(version.expiresAt) : Number.NaN;
|
|
119
|
+
if (!allowLegacyRetirement
|
|
120
|
+
|| value.length < 16
|
|
121
|
+
|| value.length >= 32
|
|
122
|
+
|| !Number.isFinite(createdAt)
|
|
123
|
+
|| !Number.isFinite(expiresAt)
|
|
124
|
+
|| expiresAt <= createdAt
|
|
125
|
+
|| expiresAt - createdAt > DEFAULT_GRACE_MS) {
|
|
126
|
+
throw new Error(`Credential ${name} has an invalid one-time legacy retirement envelope`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
assertCredentialSecret(name, value);
|
|
131
|
+
}
|
|
132
|
+
if (secretFingerprint(value) !== version.fingerprint) {
|
|
133
|
+
throw new Error(`Credential ${name} fingerprint does not match its encrypted value`);
|
|
134
|
+
}
|
|
135
|
+
return value;
|
|
136
|
+
}
|
|
137
|
+
function isExpired(version, atMs = Date.now()) {
|
|
138
|
+
return Boolean(version?.expiresAt && Date.parse(version.expiresAt) <= atMs);
|
|
139
|
+
}
|
|
140
|
+
function pruneExpired(store) {
|
|
141
|
+
let changed = false;
|
|
142
|
+
for (const record of Object.values(store.credentials)) {
|
|
143
|
+
if (record?.previous && isExpired(record.previous)) {
|
|
144
|
+
delete record.previous;
|
|
145
|
+
changed = true;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return changed;
|
|
149
|
+
}
|
|
150
|
+
function ensureRecord(name, currentValue) {
|
|
151
|
+
assertCredentialSecret(name, currentValue);
|
|
152
|
+
const store = readStore();
|
|
153
|
+
const changed = pruneExpired(store);
|
|
154
|
+
const existing = store.credentials[name];
|
|
155
|
+
if (existing) {
|
|
156
|
+
decryptValidatedSecret(name, existing.active);
|
|
157
|
+
if (existing.previous)
|
|
158
|
+
decryptValidatedSecret(name, existing.previous, true);
|
|
159
|
+
if (changed)
|
|
160
|
+
writeStore(store);
|
|
161
|
+
return { store, record: existing };
|
|
162
|
+
}
|
|
163
|
+
const active = makeVersion(name, currentValue);
|
|
164
|
+
const record = { active };
|
|
165
|
+
store.credentials[name] = record;
|
|
166
|
+
writeStore(store);
|
|
167
|
+
return { store, record };
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Return the active secret for callers that need to talk to agents.
|
|
171
|
+
*/
|
|
172
|
+
export function resolveCredentialSecret(name, currentValue) {
|
|
173
|
+
const { record } = ensureRecord(name, currentValue);
|
|
174
|
+
return decryptValidatedSecret(name, record.active);
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Start a durable rotation and return the generated plaintext only to the
|
|
178
|
+
* caller responsible for distributing it. Route responses should redact it.
|
|
179
|
+
*/
|
|
180
|
+
export function rotateCredential(name, currentValue, gracePeriodMs = DEFAULT_GRACE_MS) {
|
|
181
|
+
const { store, record } = ensureRecord(name, currentValue);
|
|
182
|
+
const currentSecret = decryptValidatedSecret(name, record.active);
|
|
183
|
+
const rotatedAt = nowIso();
|
|
184
|
+
const expiresAt = new Date(Date.now() + gracePeriodMs).toISOString();
|
|
185
|
+
const newSecret = randomBytes(32).toString('base64url');
|
|
186
|
+
const previous = makeVersion(name, currentSecret, record.active.createdAt, expiresAt);
|
|
187
|
+
const active = makeVersion(name, newSecret, rotatedAt);
|
|
188
|
+
store.credentials[name] = {
|
|
189
|
+
active,
|
|
190
|
+
previous,
|
|
191
|
+
rotatedAt,
|
|
192
|
+
gracePeriodMs,
|
|
193
|
+
};
|
|
194
|
+
writeStore(store);
|
|
195
|
+
return {
|
|
196
|
+
name,
|
|
197
|
+
newSecret,
|
|
198
|
+
newFingerprint: active.fingerprint,
|
|
199
|
+
previousFingerprint: previous.fingerprint,
|
|
200
|
+
gracePeriodMs,
|
|
201
|
+
expiresAt,
|
|
202
|
+
rotatedAt,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Verify a provided secret against the current value plus any unexpired
|
|
207
|
+
* previous secret. This remains synchronous for auth middleware.
|
|
208
|
+
*/
|
|
209
|
+
export function verifyWithRotation(name, currentValue, provided) {
|
|
210
|
+
const { store, record } = ensureRecord(name, currentValue);
|
|
211
|
+
let changed = false;
|
|
212
|
+
if (safeEqual(provided, decryptValidatedSecret(name, record.active)))
|
|
213
|
+
return true;
|
|
214
|
+
if (record.previous && isExpired(record.previous)) {
|
|
215
|
+
delete record.previous;
|
|
216
|
+
changed = true;
|
|
217
|
+
}
|
|
218
|
+
if (changed)
|
|
219
|
+
writeStore(store);
|
|
220
|
+
return Boolean(record.previous && safeEqual(provided, decryptValidatedSecret(name, record.previous, true)));
|
|
221
|
+
}
|
|
222
|
+
export function getCredentialStatus(name, currentValue) {
|
|
223
|
+
assertCredentialSecret(name, currentValue);
|
|
224
|
+
const store = readStore();
|
|
225
|
+
const record = store.credentials[name];
|
|
226
|
+
if (!record) {
|
|
227
|
+
return {
|
|
228
|
+
name,
|
|
229
|
+
activeFingerprint: secretFingerprint(currentValue),
|
|
230
|
+
previousFingerprint: null,
|
|
231
|
+
rotatedAt: null,
|
|
232
|
+
expiresAt: null,
|
|
233
|
+
gracePeriodMs: null,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
decryptValidatedSecret(name, record.active);
|
|
237
|
+
if (record.previous)
|
|
238
|
+
decryptValidatedSecret(name, record.previous, true);
|
|
239
|
+
const previous = record.previous && !isExpired(record.previous) ? record.previous : undefined;
|
|
240
|
+
return {
|
|
241
|
+
name,
|
|
242
|
+
activeFingerprint: record.active.fingerprint,
|
|
243
|
+
previousFingerprint: previous?.fingerprint ?? null,
|
|
244
|
+
rotatedAt: record.rotatedAt ?? null,
|
|
245
|
+
expiresAt: previous?.expiresAt ?? null,
|
|
246
|
+
gracePeriodMs: record.gracePeriodMs ?? null,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
export function retirePreviousCredential(name, currentValue) {
|
|
250
|
+
const { store, record } = ensureRecord(name, currentValue);
|
|
251
|
+
const retiredPreviousFingerprint = record.previous?.fingerprint ?? null;
|
|
252
|
+
if (record.previous) {
|
|
253
|
+
delete record.previous;
|
|
254
|
+
writeStore(store);
|
|
255
|
+
}
|
|
256
|
+
return {
|
|
257
|
+
name,
|
|
258
|
+
activeFingerprint: record.active.fingerprint,
|
|
259
|
+
previousFingerprint: null,
|
|
260
|
+
retiredPreviousFingerprint,
|
|
261
|
+
rotatedAt: record.rotatedAt ?? null,
|
|
262
|
+
expiresAt: null,
|
|
263
|
+
gracePeriodMs: record.gracePeriodMs ?? null,
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
/** List active credential rotations for admin visibility. */
|
|
267
|
+
export function listActiveRotations() {
|
|
268
|
+
const store = readStore();
|
|
269
|
+
return Object.entries(store.credentials).map(([name, record]) => {
|
|
270
|
+
const credentialName = name;
|
|
271
|
+
decryptValidatedSecret(credentialName, record.active);
|
|
272
|
+
if (record.previous)
|
|
273
|
+
decryptValidatedSecret(credentialName, record.previous, true);
|
|
274
|
+
const previous = record.previous && !isExpired(record.previous) ? record.previous : undefined;
|
|
275
|
+
return {
|
|
276
|
+
name,
|
|
277
|
+
activeFingerprint: record.active.fingerprint,
|
|
278
|
+
previousFingerprint: previous?.fingerprint ?? null,
|
|
279
|
+
rotatedAt: record.rotatedAt ?? null,
|
|
280
|
+
expiresAt: previous?.expiresAt ?? null,
|
|
281
|
+
gracePeriodMs: record.gracePeriodMs ?? null,
|
|
282
|
+
};
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
function safeEqual(a, b) {
|
|
286
|
+
const aBuf = Buffer.from(a);
|
|
287
|
+
const bBuf = Buffer.from(b);
|
|
288
|
+
if (aBuf.length !== bBuf.length) {
|
|
289
|
+
timingSafeEqual(aBuf, aBuf);
|
|
290
|
+
return false;
|
|
291
|
+
}
|
|
292
|
+
return timingSafeEqual(aBuf, bBuf);
|
|
293
|
+
}
|
|
294
|
+
//# sourceMappingURL=credential-rotation-service.js.map
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dependency-service.ts — Node dependency mapping.
|
|
3
|
+
*
|
|
4
|
+
* Discovers service dependencies between nodes by:
|
|
5
|
+
* 1. Querying each agent for listening ports + active connections
|
|
6
|
+
* 2. Matching connections to known node IPs/ports
|
|
7
|
+
* 3. Building a dependency graph (e.g., app → database → cache)
|
|
8
|
+
*/
|
|
9
|
+
import { SENTINEL_NODE_IDS } from '../utils/sentinel-nodes.js';
|
|
10
|
+
import { boundedJson } from './application-probes.js';
|
|
11
|
+
import { lookup } from 'node:dns/promises';
|
|
12
|
+
import { normalizeIpAddress } from '../utils/ip.js';
|
|
13
|
+
import { getDb } from '../db/index.js';
|
|
14
|
+
import { getNodeManagerSecret } from '../utils/env.js';
|
|
15
|
+
// ── Well-known port lookup ──
|
|
16
|
+
const KNOWN_SERVICES = {
|
|
17
|
+
22: 'ssh', 80: 'http', 443: 'https', 3000: 'webapp',
|
|
18
|
+
3306: 'mysql', 5432: 'postgres', 6379: 'redis', 6380: 'redis',
|
|
19
|
+
27017: 'mongodb', 9200: 'elasticsearch', 8080: 'http-alt',
|
|
20
|
+
5672: 'rabbitmq', 9092: 'kafka', 2181: 'zookeeper',
|
|
21
|
+
8443: 'https-alt', 4100: 'magnus-agent', 4000: 'magnus-api',
|
|
22
|
+
11434: 'ollama',
|
|
23
|
+
};
|
|
24
|
+
function detectService(port, process) {
|
|
25
|
+
if (process) {
|
|
26
|
+
const lower = process.toLowerCase();
|
|
27
|
+
if (lower.includes('postgres') || lower.includes('psql'))
|
|
28
|
+
return 'postgres';
|
|
29
|
+
if (lower.includes('mysql') || lower.includes('mariadb'))
|
|
30
|
+
return 'mysql';
|
|
31
|
+
if (lower.includes('redis'))
|
|
32
|
+
return 'redis';
|
|
33
|
+
if (lower.includes('mongo'))
|
|
34
|
+
return 'mongodb';
|
|
35
|
+
if (lower.includes('nginx'))
|
|
36
|
+
return 'nginx';
|
|
37
|
+
if (lower.includes('node'))
|
|
38
|
+
return 'node';
|
|
39
|
+
}
|
|
40
|
+
return KNOWN_SERVICES[port] ?? `port-${port}`;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Query an agent for its network connections via the agent API.
|
|
44
|
+
* Falls back to empty arrays on error.
|
|
45
|
+
*/
|
|
46
|
+
async function queryAgentConnections(nodeIp, nodePort) {
|
|
47
|
+
const observedAt = new Date().toISOString();
|
|
48
|
+
const unknown = (reason) => ({ services: [], connections: [], collection: { status: 'unknown', observedAt, reason } });
|
|
49
|
+
try {
|
|
50
|
+
const address = `https://${nodeIp}:${nodePort}`;
|
|
51
|
+
const resp = await fetch(`${address}/api/v1/network-info`, {
|
|
52
|
+
headers: { 'x-node-token': getNodeManagerSecret() },
|
|
53
|
+
signal: AbortSignal.timeout(5000),
|
|
54
|
+
});
|
|
55
|
+
if (!resp.ok)
|
|
56
|
+
return unknown(`Agent returned HTTP ${resp.status}`);
|
|
57
|
+
const data = (await boundedJson(resp));
|
|
58
|
+
if (!Array.isArray(data.listeningPorts) || !Array.isArray(data.connections))
|
|
59
|
+
return unknown('Invalid network observation');
|
|
60
|
+
return {
|
|
61
|
+
collection: { status: 'pass', observedAt, reason: null },
|
|
62
|
+
services: data.listeningPorts ?? [],
|
|
63
|
+
connections: data.connections ?? [],
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return unknown('Agent network observation unavailable');
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Build a dependency graph across all registered nodes.
|
|
72
|
+
*/
|
|
73
|
+
export async function buildDependencyGraph() {
|
|
74
|
+
const db = getDb();
|
|
75
|
+
const nodes = await db.node.findMany({
|
|
76
|
+
take: 1000,
|
|
77
|
+
where: { id: { notIn: SENTINEL_NODE_IDS } },
|
|
78
|
+
select: { id: true, name: true, hostname: true, ipAddress: true, port: true },
|
|
79
|
+
});
|
|
80
|
+
// Map IP → node ID for matching connections
|
|
81
|
+
const ipToNode = new Map();
|
|
82
|
+
for (const n of nodes) {
|
|
83
|
+
ipToNode.set(normalizeIpAddress(n.ipAddress), { id: n.id, name: n.name });
|
|
84
|
+
}
|
|
85
|
+
await Promise.allSettled(nodes.map(async (n) => {
|
|
86
|
+
let timer;
|
|
87
|
+
try {
|
|
88
|
+
const addresses = await Promise.race([Promise.allSettled([...new Set([n.ipAddress, n.hostname])].map(host => lookup(host, { all: true }))).then(results => results.flatMap(r => r.status === 'fulfilled' ? r.value : [])), new Promise((_, reject) => { timer = setTimeout(() => reject(new Error('DNS timeout')), 2000); })]);
|
|
89
|
+
for (const address of addresses)
|
|
90
|
+
ipToNode.set(normalizeIpAddress(address.address), { id: n.id, name: n.name });
|
|
91
|
+
}
|
|
92
|
+
finally {
|
|
93
|
+
if (timer)
|
|
94
|
+
clearTimeout(timer);
|
|
95
|
+
}
|
|
96
|
+
}));
|
|
97
|
+
// Query all agents in parallel
|
|
98
|
+
const results = await Promise.allSettled(nodes.map(async (n) => {
|
|
99
|
+
const info = await queryAgentConnections(n.ipAddress, n.port);
|
|
100
|
+
return { nodeId: n.id, ...info };
|
|
101
|
+
}));
|
|
102
|
+
const depNodes = [];
|
|
103
|
+
const edgeMap = new Map();
|
|
104
|
+
for (const r of results) {
|
|
105
|
+
if (r.status !== 'fulfilled')
|
|
106
|
+
continue;
|
|
107
|
+
const { nodeId, services, connections, collection } = r.value;
|
|
108
|
+
const node = nodes.find((n) => n.id === nodeId);
|
|
109
|
+
if (!node)
|
|
110
|
+
continue;
|
|
111
|
+
depNodes.push({ id: nodeId, name: node.name, ip: node.ipAddress, services, collection });
|
|
112
|
+
// Match connections to known nodes
|
|
113
|
+
for (const conn of connections) {
|
|
114
|
+
if (conn.state !== 'ESTABLISHED')
|
|
115
|
+
continue;
|
|
116
|
+
const target = ipToNode.get(normalizeIpAddress(conn.remoteIp));
|
|
117
|
+
if (!target || target.id === nodeId)
|
|
118
|
+
continue;
|
|
119
|
+
const edgeKey = `${nodeId}→${target.id}:${conn.remotePort}`;
|
|
120
|
+
const existing = edgeMap.get(edgeKey);
|
|
121
|
+
if (existing) {
|
|
122
|
+
existing.connectionCount++;
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
const destination = results.find(r => r.status === 'fulfilled' && r.value.nodeId === target.id);
|
|
126
|
+
const proc = destination?.status === 'fulfilled' ? destination.value.services.find(s => s.port === conn.remotePort) : undefined;
|
|
127
|
+
edgeMap.set(edgeKey, {
|
|
128
|
+
from: nodeId,
|
|
129
|
+
to: target.id,
|
|
130
|
+
fromPort: conn.localPort,
|
|
131
|
+
toPort: conn.remotePort,
|
|
132
|
+
service: detectService(conn.remotePort, proc?.process),
|
|
133
|
+
connectionCount: 1,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
nodes: depNodes,
|
|
140
|
+
edges: [...edgeMap.values()],
|
|
141
|
+
discoveredAt: new Date().toISOString(),
|
|
142
|
+
status: depNodes.some(n => n.collection.status !== 'pass') ? 'partial' : 'complete',
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
//# sourceMappingURL=dependency-service.js.map
|