@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,97 @@
|
|
|
1
|
+
import { decryptWebhookCredential, encryptWebhookCredential, isEncryptedWebhookCredential, } from './webhook-security.js';
|
|
2
|
+
export function assertWebhookMigrationCanApply(apply, serviceStopped = process.env['WEBHOOK_MIGRATION_SERVICE_STOPPED']) {
|
|
3
|
+
if (apply && serviceStopped !== '1') {
|
|
4
|
+
throw new Error('Refusing live migration: stop magnus-control-api and set WEBHOOK_MIGRATION_SERVICE_STOPPED=1');
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
function rejectUnsupportedEnvelope(value, field) {
|
|
8
|
+
if (value.startsWith('magnus-webhook:') && !isEncryptedWebhookCredential(value)) {
|
|
9
|
+
throw new Error(`Webhook ${field} has an unsupported encrypted envelope`);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
function validateHeaders(plaintext) {
|
|
13
|
+
const parsed = JSON.parse(plaintext);
|
|
14
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
15
|
+
throw new Error('Stored webhook headers must be a JSON object');
|
|
16
|
+
}
|
|
17
|
+
for (const [name, value] of Object.entries(parsed)) {
|
|
18
|
+
if (!name || typeof value !== 'string') {
|
|
19
|
+
throw new Error('Stored webhook headers must contain string names and values');
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function planMigration(rows) {
|
|
24
|
+
return rows.map((endpoint) => {
|
|
25
|
+
if (endpoint.secret !== null)
|
|
26
|
+
rejectUnsupportedEnvelope(endpoint.secret, 'secret');
|
|
27
|
+
rejectUnsupportedEnvelope(endpoint.headers, 'headers');
|
|
28
|
+
if (endpoint.url)
|
|
29
|
+
rejectUnsupportedEnvelope(endpoint.url, 'url');
|
|
30
|
+
const secretPlaintext = endpoint.secret === null
|
|
31
|
+
? null
|
|
32
|
+
: isEncryptedWebhookCredential(endpoint.secret)
|
|
33
|
+
? decryptWebhookCredential(endpoint.secret, endpoint.id, 'secret')
|
|
34
|
+
: endpoint.secret;
|
|
35
|
+
const headersPlaintext = isEncryptedWebhookCredential(endpoint.headers)
|
|
36
|
+
? decryptWebhookCredential(endpoint.headers, endpoint.id, 'headers')
|
|
37
|
+
: endpoint.headers;
|
|
38
|
+
validateHeaders(headersPlaintext);
|
|
39
|
+
return {
|
|
40
|
+
id: endpoint.id,
|
|
41
|
+
urlPlaintext: endpoint.url ? (isEncryptedWebhookCredential(endpoint.url) ? decryptWebhookCredential(endpoint.url, endpoint.id, 'url') : endpoint.url) : undefined,
|
|
42
|
+
migrateUrl: Boolean(endpoint.url && !isEncryptedWebhookCredential(endpoint.url)),
|
|
43
|
+
secretPlaintext,
|
|
44
|
+
headersPlaintext,
|
|
45
|
+
migrateSecret: endpoint.secret !== null && !isEncryptedWebhookCredential(endpoint.secret),
|
|
46
|
+
migrateHeaders: !isEncryptedWebhookCredential(endpoint.headers),
|
|
47
|
+
};
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
export function inspectWebhookCredentialMigration(rows) {
|
|
51
|
+
const plans = planMigration(rows);
|
|
52
|
+
return {
|
|
53
|
+
total: plans.length,
|
|
54
|
+
pending: plans.filter((plan) => plan.migrateSecret || plan.migrateHeaders || plan.migrateUrl).length,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
export async function applyWebhookCredentialMigration(store) {
|
|
58
|
+
return store.$transaction(async (transaction) => {
|
|
59
|
+
const rows = await transaction.webhookEndpoint.findMany({ orderBy: { id: 'asc' } });
|
|
60
|
+
const plans = planMigration(rows);
|
|
61
|
+
const pending = plans.filter((plan) => plan.migrateSecret || plan.migrateHeaders || plan.migrateUrl);
|
|
62
|
+
for (const plan of pending) {
|
|
63
|
+
const data = {};
|
|
64
|
+
if (plan.migrateSecret && plan.secretPlaintext !== null) {
|
|
65
|
+
data.secret = encryptWebhookCredential(plan.secretPlaintext, plan.id, 'secret');
|
|
66
|
+
}
|
|
67
|
+
if (plan.migrateUrl && plan.urlPlaintext)
|
|
68
|
+
data.url = encryptWebhookCredential(plan.urlPlaintext, plan.id, 'url');
|
|
69
|
+
if (plan.migrateHeaders) {
|
|
70
|
+
data.headers = encryptWebhookCredential(plan.headersPlaintext, plan.id, 'headers');
|
|
71
|
+
}
|
|
72
|
+
await transaction.webhookEndpoint.update({ where: { id: plan.id }, data });
|
|
73
|
+
}
|
|
74
|
+
const expected = new Map(plans.map((plan) => [plan.id, plan]));
|
|
75
|
+
const verified = await transaction.webhookEndpoint.findMany({ orderBy: { id: 'asc' } });
|
|
76
|
+
if (verified.length !== plans.length) {
|
|
77
|
+
throw new Error('Webhook migration verification row count changed');
|
|
78
|
+
}
|
|
79
|
+
for (const endpoint of verified) {
|
|
80
|
+
const plan = expected.get(endpoint.id);
|
|
81
|
+
if (!plan)
|
|
82
|
+
throw new Error('Webhook migration verification found an unexpected endpoint');
|
|
83
|
+
if (endpoint.secret !== null) {
|
|
84
|
+
const secret = decryptWebhookCredential(endpoint.secret, endpoint.id, 'secret');
|
|
85
|
+
if (secret !== plan.secretPlaintext)
|
|
86
|
+
throw new Error('Webhook secret migration verification failed');
|
|
87
|
+
}
|
|
88
|
+
if (plan.urlPlaintext && (!endpoint.url || decryptWebhookCredential(endpoint.url, endpoint.id, 'url') !== plan.urlPlaintext))
|
|
89
|
+
throw new Error('Webhook URL migration verification failed');
|
|
90
|
+
const headers = decryptWebhookCredential(endpoint.headers, endpoint.id, 'headers');
|
|
91
|
+
if (headers !== plan.headersPlaintext)
|
|
92
|
+
throw new Error('Webhook header migration verification failed');
|
|
93
|
+
}
|
|
94
|
+
return { total: plans.length, migrated: pending.length };
|
|
95
|
+
}, { maxWait: 5_000, timeout: 60_000 });
|
|
96
|
+
}
|
|
97
|
+
//# sourceMappingURL=webhook-credential-migration.js.map
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import { createCipheriv, createDecipheriv, createHash, randomBytes } from 'node:crypto';
|
|
2
|
+
import { promises as dns } from 'node:dns';
|
|
3
|
+
import { request as httpsRequest } from 'node:https';
|
|
4
|
+
import { BlockList, isIP } from 'node:net';
|
|
5
|
+
const ENVELOPE_PREFIX = 'magnus-webhook:v1';
|
|
6
|
+
const MAX_REDIRECTS = 0;
|
|
7
|
+
const MAX_RESPONSE_BYTES = 2_048;
|
|
8
|
+
const blockedAddresses = new BlockList();
|
|
9
|
+
for (const [network, prefix] of [
|
|
10
|
+
['0.0.0.0', 8], ['10.0.0.0', 8], ['100.64.0.0', 10], ['127.0.0.0', 8],
|
|
11
|
+
['169.254.0.0', 16], ['172.16.0.0', 12], ['192.0.0.0', 24], ['192.0.2.0', 24],
|
|
12
|
+
['192.168.0.0', 16], ['198.18.0.0', 15], ['198.51.100.0', 24],
|
|
13
|
+
['203.0.113.0', 24], ['224.0.0.0', 4], ['240.0.0.0', 4],
|
|
14
|
+
])
|
|
15
|
+
blockedAddresses.addSubnet(network, prefix, 'ipv4');
|
|
16
|
+
for (const [network, prefix] of [
|
|
17
|
+
['::', 128], ['::1', 128], ['64:ff9b::', 96], ['100::', 64],
|
|
18
|
+
['2001::', 23], ['2001:db8::', 32], ['2002::', 16], ['3fff::', 20],
|
|
19
|
+
['fc00::', 7], ['fe80::', 10], ['ff00::', 8],
|
|
20
|
+
])
|
|
21
|
+
blockedAddresses.addSubnet(network, prefix, 'ipv6');
|
|
22
|
+
function encryptionKey(raw = process.env['WEBHOOK_ENCRYPTION_KEY']) {
|
|
23
|
+
if (!raw || Buffer.byteLength(raw, 'utf8') < 32) {
|
|
24
|
+
throw new Error('WEBHOOK_ENCRYPTION_KEY must be a dedicated secret of at least 32 bytes');
|
|
25
|
+
}
|
|
26
|
+
return createHash('sha256')
|
|
27
|
+
.update('magnus:webhook-credential-store:v1\0')
|
|
28
|
+
.update(raw, 'utf8')
|
|
29
|
+
.digest();
|
|
30
|
+
}
|
|
31
|
+
function aad(endpointId, field) {
|
|
32
|
+
if (!endpointId)
|
|
33
|
+
throw new Error('Webhook endpoint ID is required for credential encryption');
|
|
34
|
+
return Buffer.from(`${ENVELOPE_PREFIX}\0${endpointId}\0${field}`, 'utf8');
|
|
35
|
+
}
|
|
36
|
+
export function isEncryptedWebhookCredential(value) {
|
|
37
|
+
return value.startsWith(`${ENVELOPE_PREFIX}:`);
|
|
38
|
+
}
|
|
39
|
+
export function encryptWebhookCredential(value, endpointId, field, keyMaterial) {
|
|
40
|
+
const iv = randomBytes(12);
|
|
41
|
+
const cipher = createCipheriv('aes-256-gcm', encryptionKey(keyMaterial), iv);
|
|
42
|
+
cipher.setAAD(aad(endpointId, field));
|
|
43
|
+
const ciphertext = Buffer.concat([cipher.update(value, 'utf8'), cipher.final()]);
|
|
44
|
+
const tag = cipher.getAuthTag();
|
|
45
|
+
return `${ENVELOPE_PREFIX}:${iv.toString('base64url')}:${tag.toString('base64url')}:${ciphertext.toString('base64url')}`;
|
|
46
|
+
}
|
|
47
|
+
export function decryptWebhookCredential(envelope, endpointId, field, keyMaterial) {
|
|
48
|
+
if (!isEncryptedWebhookCredential(envelope)) {
|
|
49
|
+
throw new Error(`Webhook ${field} is not encrypted; run the webhook credential migration`);
|
|
50
|
+
}
|
|
51
|
+
const parts = envelope.split(':');
|
|
52
|
+
if (parts.length !== 5 || parts[0] !== 'magnus-webhook' || parts[1] !== 'v1') {
|
|
53
|
+
throw new Error(`Webhook ${field} has a malformed encrypted envelope`);
|
|
54
|
+
}
|
|
55
|
+
const iv = Buffer.from(parts[2] ?? '', 'base64url');
|
|
56
|
+
const tag = Buffer.from(parts[3] ?? '', 'base64url');
|
|
57
|
+
const ciphertext = Buffer.from(parts[4] ?? '', 'base64url');
|
|
58
|
+
if (iv.length !== 12 || tag.length !== 16) {
|
|
59
|
+
throw new Error(`Webhook ${field} has a malformed encrypted envelope`);
|
|
60
|
+
}
|
|
61
|
+
const decipher = createDecipheriv('aes-256-gcm', encryptionKey(keyMaterial), iv);
|
|
62
|
+
decipher.setAAD(aad(endpointId, field));
|
|
63
|
+
decipher.setAuthTag(tag);
|
|
64
|
+
return Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString('utf8');
|
|
65
|
+
}
|
|
66
|
+
function canonicalOrigin(url) {
|
|
67
|
+
const hostname = url.hostname.toLowerCase().replace(/\.$/, '');
|
|
68
|
+
return `https://${hostname}${url.port ? `:${url.port}` : ''}`;
|
|
69
|
+
}
|
|
70
|
+
function allowedOrigins(raw = process.env['WEBHOOK_ALLOWED_ORIGINS'] ?? '') {
|
|
71
|
+
const values = raw.split(',').map((value) => value.trim()).filter(Boolean);
|
|
72
|
+
return new Set(values.map((value) => {
|
|
73
|
+
const url = new URL(value);
|
|
74
|
+
if (url.protocol !== 'https:' || url.username || url.password || url.search || url.hash
|
|
75
|
+
|| (url.pathname !== '/' && url.pathname !== '') || isIP(url.hostname.replace(/^\[|\]$/g, '')) !== 0) {
|
|
76
|
+
throw new Error('WEBHOOK_ALLOWED_ORIGINS must contain exact HTTPS DNS origins');
|
|
77
|
+
}
|
|
78
|
+
return canonicalOrigin(url);
|
|
79
|
+
}));
|
|
80
|
+
}
|
|
81
|
+
function isPublicAddress(address, family) {
|
|
82
|
+
if (family === 4)
|
|
83
|
+
return !blockedAddresses.check(address, 'ipv4');
|
|
84
|
+
if (family !== 6 || blockedAddresses.check(address, 'ipv6'))
|
|
85
|
+
return false;
|
|
86
|
+
const first = Number.parseInt(address.split(':', 1)[0] ?? '', 16);
|
|
87
|
+
return Number.isFinite(first) && first >= 0x2000 && first <= 0x3fff;
|
|
88
|
+
}
|
|
89
|
+
const defaultLookup = async (hostname) => dns.lookup(hostname, { all: true, verbatim: true });
|
|
90
|
+
/** Opt-in exact address pins for enrolled tailnet receivers. This never grants
|
|
91
|
+
* access to arbitrary private, loopback or metadata addresses. DNS must still
|
|
92
|
+
* match every configured pin and the transport uses that validated answer. */
|
|
93
|
+
function tailnetPins(origin) {
|
|
94
|
+
const raw = process.env['WEBHOOK_TAILNET_TARGETS_JSON'];
|
|
95
|
+
if (!raw)
|
|
96
|
+
return undefined;
|
|
97
|
+
const entries = JSON.parse(raw);
|
|
98
|
+
if (!entries || typeof entries !== 'object' || Array.isArray(entries) || Object.keys(entries).length > 16)
|
|
99
|
+
throw new Error('Invalid webhook tailnet target registry');
|
|
100
|
+
const tailnet = new BlockList();
|
|
101
|
+
tailnet.addSubnet('100.64.0.0', 10, 'ipv4');
|
|
102
|
+
for (const [key, pins] of Object.entries(entries)) {
|
|
103
|
+
if (allowedOrigins(key).size !== 1 || canonicalOrigin(new URL(key)) !== key || !Array.isArray(pins) || pins.length < 1 || pins.length > 8 ||
|
|
104
|
+
pins.some(ip => typeof ip !== 'string' || isIP(ip) !== 4 || !tailnet.check(ip, 'ipv4')))
|
|
105
|
+
throw new Error('Tailnet webhooks require exact HTTPS origins and tailnet IPv4 pins');
|
|
106
|
+
}
|
|
107
|
+
const pins = entries[origin];
|
|
108
|
+
return pins ? new Set(pins) : undefined;
|
|
109
|
+
}
|
|
110
|
+
export function createPinnedWebhookLookup(target) {
|
|
111
|
+
return (_hostname, options, callback) => {
|
|
112
|
+
if (options.all) {
|
|
113
|
+
callback(null, [{ address: target.address, family: target.family }]);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
callback(null, target.address, target.family);
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
async function resolveWebhookTarget(rawUrl, options = {}) {
|
|
120
|
+
let url;
|
|
121
|
+
try {
|
|
122
|
+
url = new URL(rawUrl);
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
throw new Error('Webhook target must be a valid URL');
|
|
126
|
+
}
|
|
127
|
+
const hostname = url.hostname.toLowerCase().replace(/^\[|\]$/g, '').replace(/\.$/, '');
|
|
128
|
+
if (url.protocol !== 'https:' || url.username || url.password || url.hash
|
|
129
|
+
|| !hostname || isIP(hostname) !== 0
|
|
130
|
+
|| hostname === 'localhost' || hostname.endsWith('.localhost')
|
|
131
|
+
|| hostname === 'metadata.google.internal' || hostname === 'instance-data') {
|
|
132
|
+
throw new Error('Webhook target must be an approved HTTPS DNS endpoint');
|
|
133
|
+
}
|
|
134
|
+
url.hostname = hostname;
|
|
135
|
+
const allowlist = allowedOrigins(options.allowedOriginsRaw);
|
|
136
|
+
if (!allowlist.has(canonicalOrigin(url))) {
|
|
137
|
+
throw new Error('Webhook target origin is not allowlisted');
|
|
138
|
+
}
|
|
139
|
+
const pins = tailnetPins(canonicalOrigin(url));
|
|
140
|
+
let addresses;
|
|
141
|
+
try {
|
|
142
|
+
addresses = await (options.lookup ?? defaultLookup)(hostname);
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
throw new Error('Webhook target DNS resolution failed');
|
|
146
|
+
}
|
|
147
|
+
if (addresses.length === 0
|
|
148
|
+
|| addresses.some(({ address, family }) => (family !== 4 && family !== 6) ||
|
|
149
|
+
(pins ? family !== 4 || !pins.has(address) : !isPublicAddress(address, family)))) {
|
|
150
|
+
throw new Error('Webhook target resolves to a non-public address');
|
|
151
|
+
}
|
|
152
|
+
const selected = addresses[0];
|
|
153
|
+
if (!selected || (selected.family !== 4 && selected.family !== 6)) {
|
|
154
|
+
throw new Error('Webhook target DNS resolution returned an unsupported address');
|
|
155
|
+
}
|
|
156
|
+
return { url, address: selected.address, family: selected.family };
|
|
157
|
+
}
|
|
158
|
+
export async function validateWebhookTarget(rawUrl, options = {}) {
|
|
159
|
+
return (await resolveWebhookTarget(rawUrl, options)).url;
|
|
160
|
+
}
|
|
161
|
+
const pinnedHttpsTransport = async (target, init) => new Promise((resolvePromise, reject) => {
|
|
162
|
+
let settled = false;
|
|
163
|
+
const chunks = [];
|
|
164
|
+
let bytes = 0;
|
|
165
|
+
let truncated = false;
|
|
166
|
+
const finish = (status) => {
|
|
167
|
+
if (settled)
|
|
168
|
+
return;
|
|
169
|
+
settled = true;
|
|
170
|
+
clearTimeout(deadline);
|
|
171
|
+
const captured = Buffer.concat(chunks).toString('utf8');
|
|
172
|
+
resolvePromise({
|
|
173
|
+
ok: status >= 200 && status < 300,
|
|
174
|
+
status,
|
|
175
|
+
responseBody: truncated ? `${captured}...` : captured,
|
|
176
|
+
});
|
|
177
|
+
};
|
|
178
|
+
const request = httpsRequest(target.url, {
|
|
179
|
+
method: 'POST',
|
|
180
|
+
headers: init.headers,
|
|
181
|
+
servername: target.url.hostname,
|
|
182
|
+
family: target.family,
|
|
183
|
+
lookup: createPinnedWebhookLookup(target),
|
|
184
|
+
agent: false,
|
|
185
|
+
}, (response) => {
|
|
186
|
+
const status = response.statusCode ?? 0;
|
|
187
|
+
if (status >= 300 && status < 400) {
|
|
188
|
+
response.destroy();
|
|
189
|
+
settled = true;
|
|
190
|
+
clearTimeout(deadline);
|
|
191
|
+
reject(new Error(`Webhook redirects are rejected (limit ${MAX_REDIRECTS})`));
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
response.on('data', (rawChunk) => {
|
|
195
|
+
if (settled)
|
|
196
|
+
return;
|
|
197
|
+
const chunk = Buffer.isBuffer(rawChunk) ? rawChunk : Buffer.from(rawChunk);
|
|
198
|
+
const remaining = MAX_RESPONSE_BYTES - bytes;
|
|
199
|
+
if (remaining > 0) {
|
|
200
|
+
chunks.push(chunk.subarray(0, remaining));
|
|
201
|
+
bytes += Math.min(chunk.length, remaining);
|
|
202
|
+
}
|
|
203
|
+
if (chunk.length > remaining) {
|
|
204
|
+
truncated = true;
|
|
205
|
+
response.destroy();
|
|
206
|
+
finish(status);
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
response.on('end', () => finish(status));
|
|
210
|
+
response.on('error', (error) => {
|
|
211
|
+
if (!settled) {
|
|
212
|
+
settled = true;
|
|
213
|
+
clearTimeout(deadline);
|
|
214
|
+
reject(error);
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
const deadline = setTimeout(() => request.destroy(new Error('Webhook request timed out')), init.timeoutMs);
|
|
219
|
+
request.on('error', (error) => {
|
|
220
|
+
if (settled)
|
|
221
|
+
return;
|
|
222
|
+
settled = true;
|
|
223
|
+
clearTimeout(deadline);
|
|
224
|
+
reject(error);
|
|
225
|
+
});
|
|
226
|
+
request.end(init.body);
|
|
227
|
+
});
|
|
228
|
+
export async function postWebhook(rawUrl, init, options = {}) {
|
|
229
|
+
const target = await resolveWebhookTarget(rawUrl, options);
|
|
230
|
+
const result = await (options.transport ?? pinnedHttpsTransport)(target, init);
|
|
231
|
+
if (result.status >= 300 && result.status < 400) {
|
|
232
|
+
throw new Error(`Webhook redirects are rejected (limit ${MAX_REDIRECTS})`);
|
|
233
|
+
}
|
|
234
|
+
const responseBytes = Buffer.from(result.responseBody, 'utf8');
|
|
235
|
+
if (responseBytes.length <= MAX_RESPONSE_BYTES)
|
|
236
|
+
return result;
|
|
237
|
+
return {
|
|
238
|
+
...result,
|
|
239
|
+
responseBody: `${responseBytes.subarray(0, MAX_RESPONSE_BYTES).toString('utf8')}...`,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
//# sourceMappingURL=webhook-security.js.map
|