thevoidforge 21.0.0
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/dist/scripts/vault-read.d.ts +11 -0
- package/dist/scripts/vault-read.js +89 -0
- package/dist/scripts/voidforge.d.ts +20 -0
- package/dist/scripts/voidforge.js +404 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/dist/wizard/api/auth.d.ts +5 -0
- package/dist/wizard/api/auth.js +133 -0
- package/dist/wizard/api/blueprint.d.ts +45 -0
- package/dist/wizard/api/blueprint.js +184 -0
- package/dist/wizard/api/cloud-providers.d.ts +16 -0
- package/dist/wizard/api/cloud-providers.js +363 -0
- package/dist/wizard/api/credentials.d.ts +1 -0
- package/dist/wizard/api/credentials.js +258 -0
- package/dist/wizard/api/danger-room.d.ts +18 -0
- package/dist/wizard/api/danger-room.js +401 -0
- package/dist/wizard/api/deploy.d.ts +4 -0
- package/dist/wizard/api/deploy.js +164 -0
- package/dist/wizard/api/prd.d.ts +1 -0
- package/dist/wizard/api/prd.js +363 -0
- package/dist/wizard/api/project.d.ts +1 -0
- package/dist/wizard/api/project.js +239 -0
- package/dist/wizard/api/projects.d.ts +6 -0
- package/dist/wizard/api/projects.js +648 -0
- package/dist/wizard/api/provision.d.ts +4 -0
- package/dist/wizard/api/provision.js +535 -0
- package/dist/wizard/api/terminal.d.ts +25 -0
- package/dist/wizard/api/terminal.js +241 -0
- package/dist/wizard/api/users.d.ts +6 -0
- package/dist/wizard/api/users.js +244 -0
- package/dist/wizard/api/war-room.d.ts +14 -0
- package/dist/wizard/api/war-room.js +45 -0
- package/dist/wizard/lib/ad-platform-core.d.ts +6 -0
- package/dist/wizard/lib/ad-platform-core.js +1 -0
- package/dist/wizard/lib/adapters/index.d.ts +52 -0
- package/dist/wizard/lib/adapters/index.js +38 -0
- package/dist/wizard/lib/adapters/sandbox-bank.d.ts +17 -0
- package/dist/wizard/lib/adapters/sandbox-bank.js +77 -0
- package/dist/wizard/lib/adapters/sandbox.d.ts +39 -0
- package/dist/wizard/lib/adapters/sandbox.js +174 -0
- package/dist/wizard/lib/adapters/stripe.d.ts +19 -0
- package/dist/wizard/lib/adapters/stripe.js +143 -0
- package/dist/wizard/lib/adapters/types.d.ts +9 -0
- package/dist/wizard/lib/adapters/types.js +10 -0
- package/dist/wizard/lib/agent-memory.d.ts +36 -0
- package/dist/wizard/lib/agent-memory.js +114 -0
- package/dist/wizard/lib/anomaly-detection.d.ts +59 -0
- package/dist/wizard/lib/anomaly-detection.js +122 -0
- package/dist/wizard/lib/anthropic.d.ts +21 -0
- package/dist/wizard/lib/anthropic.js +105 -0
- package/dist/wizard/lib/asset-scanner.d.ts +23 -0
- package/dist/wizard/lib/asset-scanner.js +107 -0
- package/dist/wizard/lib/audit-log.d.ts +23 -0
- package/dist/wizard/lib/audit-log.js +70 -0
- package/dist/wizard/lib/autonomy-controller.d.ts +76 -0
- package/dist/wizard/lib/autonomy-controller.js +183 -0
- package/dist/wizard/lib/body-parser.d.ts +2 -0
- package/dist/wizard/lib/body-parser.js +36 -0
- package/dist/wizard/lib/build-analytics.d.ts +39 -0
- package/dist/wizard/lib/build-analytics.js +91 -0
- package/dist/wizard/lib/build-step.d.ts +21 -0
- package/dist/wizard/lib/build-step.js +104 -0
- package/dist/wizard/lib/campaign-proposer.d.ts +39 -0
- package/dist/wizard/lib/campaign-proposer.js +180 -0
- package/dist/wizard/lib/campaign-state-machine.d.ts +63 -0
- package/dist/wizard/lib/campaign-state-machine.js +114 -0
- package/dist/wizard/lib/ci-generator.d.ts +14 -0
- package/dist/wizard/lib/ci-generator.js +187 -0
- package/dist/wizard/lib/claude-merge.d.ts +38 -0
- package/dist/wizard/lib/claude-merge.js +115 -0
- package/dist/wizard/lib/codegen/erd-gen.d.ts +16 -0
- package/dist/wizard/lib/codegen/erd-gen.js +98 -0
- package/dist/wizard/lib/codegen/integrations.d.ts +18 -0
- package/dist/wizard/lib/codegen/integrations.js +189 -0
- package/dist/wizard/lib/codegen/openapi-gen.d.ts +15 -0
- package/dist/wizard/lib/codegen/openapi-gen.js +79 -0
- package/dist/wizard/lib/codegen/prisma-types.d.ts +15 -0
- package/dist/wizard/lib/codegen/prisma-types.js +44 -0
- package/dist/wizard/lib/codegen/seed-gen.d.ts +16 -0
- package/dist/wizard/lib/codegen/seed-gen.js +128 -0
- package/dist/wizard/lib/compliance.d.ts +51 -0
- package/dist/wizard/lib/compliance.js +112 -0
- package/dist/wizard/lib/correlation-engine.d.ts +59 -0
- package/dist/wizard/lib/correlation-engine.js +151 -0
- package/dist/wizard/lib/cost-estimator.d.ts +22 -0
- package/dist/wizard/lib/cost-estimator.js +72 -0
- package/dist/wizard/lib/cost-tracker.d.ts +27 -0
- package/dist/wizard/lib/cost-tracker.js +37 -0
- package/dist/wizard/lib/daemon-aggregator.d.ts +71 -0
- package/dist/wizard/lib/daemon-aggregator.js +204 -0
- package/dist/wizard/lib/daemon-core.d.ts +6 -0
- package/dist/wizard/lib/daemon-core.js +5 -0
- package/dist/wizard/lib/dashboard-data.d.ts +132 -0
- package/dist/wizard/lib/dashboard-data.js +336 -0
- package/dist/wizard/lib/dashboard-ws.d.ts +25 -0
- package/dist/wizard/lib/dashboard-ws.js +91 -0
- package/dist/wizard/lib/deep-current.d.ts +77 -0
- package/dist/wizard/lib/deep-current.js +234 -0
- package/dist/wizard/lib/deploy-coordinator.d.ts +40 -0
- package/dist/wizard/lib/deploy-coordinator.js +86 -0
- package/dist/wizard/lib/deploy-log.d.ts +28 -0
- package/dist/wizard/lib/deploy-log.js +52 -0
- package/dist/wizard/lib/desktop-notify.d.ts +27 -0
- package/dist/wizard/lib/desktop-notify.js +98 -0
- package/dist/wizard/lib/dns/cloudflare-dns.d.ts +35 -0
- package/dist/wizard/lib/dns/cloudflare-dns.js +216 -0
- package/dist/wizard/lib/dns/cloudflare-registrar.d.ts +31 -0
- package/dist/wizard/lib/dns/cloudflare-registrar.js +148 -0
- package/dist/wizard/lib/dns/types.d.ts +22 -0
- package/dist/wizard/lib/dns/types.js +4 -0
- package/dist/wizard/lib/document-discovery.d.ts +33 -0
- package/dist/wizard/lib/document-discovery.js +145 -0
- package/dist/wizard/lib/env-validator.d.ts +14 -0
- package/dist/wizard/lib/env-validator.js +205 -0
- package/dist/wizard/lib/env-writer.d.ts +13 -0
- package/dist/wizard/lib/env-writer.js +26 -0
- package/dist/wizard/lib/exec.d.ts +30 -0
- package/dist/wizard/lib/exec.js +52 -0
- package/dist/wizard/lib/experiment.d.ts +70 -0
- package/dist/wizard/lib/experiment.js +169 -0
- package/dist/wizard/lib/extensions.d.ts +20 -0
- package/dist/wizard/lib/extensions.js +183 -0
- package/dist/wizard/lib/financial/adapter-factory.d.ts +47 -0
- package/dist/wizard/lib/financial/adapter-factory.js +225 -0
- package/dist/wizard/lib/financial/billing/base.d.ts +6 -0
- package/dist/wizard/lib/financial/billing/base.js +1 -0
- package/dist/wizard/lib/financial/billing/google-billing.d.ts +56 -0
- package/dist/wizard/lib/financial/billing/google-billing.js +298 -0
- package/dist/wizard/lib/financial/billing/meta-billing.d.ts +54 -0
- package/dist/wizard/lib/financial/billing/meta-billing.js +243 -0
- package/dist/wizard/lib/financial/billing/tiktok-billing.d.ts +54 -0
- package/dist/wizard/lib/financial/billing/tiktok-billing.js +260 -0
- package/dist/wizard/lib/financial/campaign/base.d.ts +13 -0
- package/dist/wizard/lib/financial/campaign/base.js +1 -0
- package/dist/wizard/lib/financial/campaign/google-campaign.d.ts +42 -0
- package/dist/wizard/lib/financial/campaign/google-campaign.js +388 -0
- package/dist/wizard/lib/financial/campaign/meta-campaign.d.ts +41 -0
- package/dist/wizard/lib/financial/campaign/meta-campaign.js +311 -0
- package/dist/wizard/lib/financial/campaign/sandbox-campaign.d.ts +45 -0
- package/dist/wizard/lib/financial/campaign/sandbox-campaign.js +261 -0
- package/dist/wizard/lib/financial/campaign/tiktok-campaign.d.ts +40 -0
- package/dist/wizard/lib/financial/campaign/tiktok-campaign.js +350 -0
- package/dist/wizard/lib/financial/funding-auto.d.ts +44 -0
- package/dist/wizard/lib/financial/funding-auto.js +52 -0
- package/dist/wizard/lib/financial/funding-policy.d.ts +60 -0
- package/dist/wizard/lib/financial/funding-policy.js +179 -0
- package/dist/wizard/lib/financial/platform-planner.d.ts +47 -0
- package/dist/wizard/lib/financial/platform-planner.js +134 -0
- package/dist/wizard/lib/financial/reconciliation-engine.d.ts +78 -0
- package/dist/wizard/lib/financial/reconciliation-engine.js +193 -0
- package/dist/wizard/lib/financial/registry.d.ts +22 -0
- package/dist/wizard/lib/financial/registry.js +26 -0
- package/dist/wizard/lib/financial/reporting.d.ts +96 -0
- package/dist/wizard/lib/financial/reporting.js +198 -0
- package/dist/wizard/lib/financial/stablecoin/base.d.ts +6 -0
- package/dist/wizard/lib/financial/stablecoin/base.js +1 -0
- package/dist/wizard/lib/financial/stablecoin/circle.d.ts +54 -0
- package/dist/wizard/lib/financial/stablecoin/circle.js +367 -0
- package/dist/wizard/lib/financial/stablecoin/mercury.d.ts +24 -0
- package/dist/wizard/lib/financial/stablecoin/mercury.js +171 -0
- package/dist/wizard/lib/financial/stablecoin/sandbox-stablecoin.d.ts +47 -0
- package/dist/wizard/lib/financial/stablecoin/sandbox-stablecoin.js +202 -0
- package/dist/wizard/lib/financial/treasury-planner.d.ts +52 -0
- package/dist/wizard/lib/financial/treasury-planner.js +128 -0
- package/dist/wizard/lib/financial-core.d.ts +6 -0
- package/dist/wizard/lib/financial-core.js +5 -0
- package/dist/wizard/lib/financial-vault.d.ts +34 -0
- package/dist/wizard/lib/financial-vault.js +199 -0
- package/dist/wizard/lib/frontmatter.d.ts +30 -0
- package/dist/wizard/lib/frontmatter.js +96 -0
- package/dist/wizard/lib/gap-analysis.d.ts +37 -0
- package/dist/wizard/lib/gap-analysis.js +218 -0
- package/dist/wizard/lib/github.d.ts +22 -0
- package/dist/wizard/lib/github.js +261 -0
- package/dist/wizard/lib/headless-deploy.d.ts +14 -0
- package/dist/wizard/lib/headless-deploy.js +452 -0
- package/dist/wizard/lib/health-monitor.d.ts +15 -0
- package/dist/wizard/lib/health-monitor.js +91 -0
- package/dist/wizard/lib/health-poller.d.ts +9 -0
- package/dist/wizard/lib/health-poller.js +123 -0
- package/dist/wizard/lib/heartbeat.d.ts +15 -0
- package/dist/wizard/lib/heartbeat.js +827 -0
- package/dist/wizard/lib/http-helpers.d.ts +9 -0
- package/dist/wizard/lib/http-helpers.js +24 -0
- package/dist/wizard/lib/image-gen.d.ts +56 -0
- package/dist/wizard/lib/image-gen.js +159 -0
- package/dist/wizard/lib/instance-sizing.d.ts +26 -0
- package/dist/wizard/lib/instance-sizing.js +51 -0
- package/dist/wizard/lib/kongo/analytics.d.ts +29 -0
- package/dist/wizard/lib/kongo/analytics.js +179 -0
- package/dist/wizard/lib/kongo/campaigns.d.ts +52 -0
- package/dist/wizard/lib/kongo/campaigns.js +91 -0
- package/dist/wizard/lib/kongo/client.d.ts +58 -0
- package/dist/wizard/lib/kongo/client.js +221 -0
- package/dist/wizard/lib/kongo/jobs.d.ts +57 -0
- package/dist/wizard/lib/kongo/jobs.js +122 -0
- package/dist/wizard/lib/kongo/pages.d.ts +60 -0
- package/dist/wizard/lib/kongo/pages.js +150 -0
- package/dist/wizard/lib/kongo/provisioner.d.ts +64 -0
- package/dist/wizard/lib/kongo/provisioner.js +116 -0
- package/dist/wizard/lib/kongo/seed.d.ts +49 -0
- package/dist/wizard/lib/kongo/seed.js +237 -0
- package/dist/wizard/lib/kongo/types.d.ts +323 -0
- package/dist/wizard/lib/kongo/types.js +11 -0
- package/dist/wizard/lib/kongo/variants.d.ts +57 -0
- package/dist/wizard/lib/kongo/variants.js +88 -0
- package/dist/wizard/lib/kongo/webhooks.d.ts +41 -0
- package/dist/wizard/lib/kongo/webhooks.js +112 -0
- package/dist/wizard/lib/marker.d.ts +28 -0
- package/dist/wizard/lib/marker.js +79 -0
- package/dist/wizard/lib/migrator.d.ts +35 -0
- package/dist/wizard/lib/migrator.js +190 -0
- package/dist/wizard/lib/natural-language-deploy.d.ts +30 -0
- package/dist/wizard/lib/natural-language-deploy.js +186 -0
- package/dist/wizard/lib/network.d.ts +22 -0
- package/dist/wizard/lib/network.js +72 -0
- package/dist/wizard/lib/oauth-core.d.ts +6 -0
- package/dist/wizard/lib/oauth-core.js +5 -0
- package/dist/wizard/lib/open-browser.d.ts +1 -0
- package/dist/wizard/lib/open-browser.js +26 -0
- package/dist/wizard/lib/patterns/ad-billing-adapter.d.ts +209 -0
- package/dist/wizard/lib/patterns/ad-billing-adapter.js +269 -0
- package/dist/wizard/lib/patterns/ad-platform-adapter.d.ts +200 -0
- package/dist/wizard/lib/patterns/ad-platform-adapter.js +212 -0
- package/dist/wizard/lib/patterns/daemon-process.d.ts +88 -0
- package/dist/wizard/lib/patterns/daemon-process.js +271 -0
- package/dist/wizard/lib/patterns/financial-transaction.d.ts +161 -0
- package/dist/wizard/lib/patterns/financial-transaction.js +132 -0
- package/dist/wizard/lib/patterns/funding-plan.d.ts +136 -0
- package/dist/wizard/lib/patterns/funding-plan.js +200 -0
- package/dist/wizard/lib/patterns/oauth-token-lifecycle.d.ts +94 -0
- package/dist/wizard/lib/patterns/oauth-token-lifecycle.js +139 -0
- package/dist/wizard/lib/patterns/outbound-rate-limiter.d.ts +67 -0
- package/dist/wizard/lib/patterns/outbound-rate-limiter.js +216 -0
- package/dist/wizard/lib/patterns/revenue-source-adapter.d.ts +96 -0
- package/dist/wizard/lib/patterns/revenue-source-adapter.js +182 -0
- package/dist/wizard/lib/patterns/stablecoin-adapter.d.ts +218 -0
- package/dist/wizard/lib/patterns/stablecoin-adapter.js +264 -0
- package/dist/wizard/lib/prd-validator.d.ts +39 -0
- package/dist/wizard/lib/prd-validator.js +137 -0
- package/dist/wizard/lib/project-init.d.ts +24 -0
- package/dist/wizard/lib/project-init.js +193 -0
- package/dist/wizard/lib/project-registry.d.ts +86 -0
- package/dist/wizard/lib/project-registry.js +359 -0
- package/dist/wizard/lib/provision-manifest.d.ts +44 -0
- package/dist/wizard/lib/provision-manifest.js +164 -0
- package/dist/wizard/lib/provisioner-registry.d.ts +15 -0
- package/dist/wizard/lib/provisioner-registry.js +34 -0
- package/dist/wizard/lib/provisioners/aws-vps.d.ts +6 -0
- package/dist/wizard/lib/provisioners/aws-vps.js +643 -0
- package/dist/wizard/lib/provisioners/cloudflare.d.ts +6 -0
- package/dist/wizard/lib/provisioners/cloudflare.js +300 -0
- package/dist/wizard/lib/provisioners/docker.d.ts +6 -0
- package/dist/wizard/lib/provisioners/docker.js +75 -0
- package/dist/wizard/lib/provisioners/http-client.d.ts +20 -0
- package/dist/wizard/lib/provisioners/http-client.js +79 -0
- package/dist/wizard/lib/provisioners/railway.d.ts +6 -0
- package/dist/wizard/lib/provisioners/railway.js +413 -0
- package/dist/wizard/lib/provisioners/scripts/caddyfile.d.ts +10 -0
- package/dist/wizard/lib/provisioners/scripts/caddyfile.js +54 -0
- package/dist/wizard/lib/provisioners/scripts/deploy-vps.d.ts +10 -0
- package/dist/wizard/lib/provisioners/scripts/deploy-vps.js +112 -0
- package/dist/wizard/lib/provisioners/scripts/docker-compose.d.ts +11 -0
- package/dist/wizard/lib/provisioners/scripts/docker-compose.js +91 -0
- package/dist/wizard/lib/provisioners/scripts/dockerfile.d.ts +5 -0
- package/dist/wizard/lib/provisioners/scripts/dockerfile.js +185 -0
- package/dist/wizard/lib/provisioners/scripts/ecosystem-config.d.ts +10 -0
- package/dist/wizard/lib/provisioners/scripts/ecosystem-config.js +36 -0
- package/dist/wizard/lib/provisioners/scripts/provision-vps.d.ts +14 -0
- package/dist/wizard/lib/provisioners/scripts/provision-vps.js +202 -0
- package/dist/wizard/lib/provisioners/scripts/rollback-vps.d.ts +10 -0
- package/dist/wizard/lib/provisioners/scripts/rollback-vps.js +67 -0
- package/dist/wizard/lib/provisioners/self-deploy.d.ts +41 -0
- package/dist/wizard/lib/provisioners/self-deploy.js +185 -0
- package/dist/wizard/lib/provisioners/static-s3.d.ts +6 -0
- package/dist/wizard/lib/provisioners/static-s3.js +235 -0
- package/dist/wizard/lib/provisioners/types.d.ts +40 -0
- package/dist/wizard/lib/provisioners/types.js +4 -0
- package/dist/wizard/lib/provisioners/vercel.d.ts +6 -0
- package/dist/wizard/lib/provisioners/vercel.js +287 -0
- package/dist/wizard/lib/pty-manager.d.ts +42 -0
- package/dist/wizard/lib/pty-manager.js +231 -0
- package/dist/wizard/lib/rate-limiter-core.d.ts +5 -0
- package/dist/wizard/lib/rate-limiter-core.js +5 -0
- package/dist/wizard/lib/reconciliation.d.ts +43 -0
- package/dist/wizard/lib/reconciliation.js +173 -0
- package/dist/wizard/lib/revenue-types.d.ts +5 -0
- package/dist/wizard/lib/revenue-types.js +1 -0
- package/dist/wizard/lib/route-optimizer.d.ts +28 -0
- package/dist/wizard/lib/route-optimizer.js +93 -0
- package/dist/wizard/lib/s3-deploy.d.ts +19 -0
- package/dist/wizard/lib/s3-deploy.js +156 -0
- package/dist/wizard/lib/safety-tiers.d.ts +76 -0
- package/dist/wizard/lib/safety-tiers.js +134 -0
- package/dist/wizard/lib/sentry-generator.d.ts +15 -0
- package/dist/wizard/lib/sentry-generator.js +116 -0
- package/dist/wizard/lib/server-config.d.ts +13 -0
- package/dist/wizard/lib/server-config.js +23 -0
- package/dist/wizard/lib/service-install.d.ts +18 -0
- package/dist/wizard/lib/service-install.js +182 -0
- package/dist/wizard/lib/site-scanner.d.ts +80 -0
- package/dist/wizard/lib/site-scanner.js +262 -0
- package/dist/wizard/lib/ssh-deploy.d.ts +25 -0
- package/dist/wizard/lib/ssh-deploy.js +225 -0
- package/dist/wizard/lib/templates.d.ts +24 -0
- package/dist/wizard/lib/templates.js +219 -0
- package/dist/wizard/lib/totp.d.ts +35 -0
- package/dist/wizard/lib/totp.js +276 -0
- package/dist/wizard/lib/tower-auth.d.ts +43 -0
- package/dist/wizard/lib/tower-auth.js +352 -0
- package/dist/wizard/lib/tower-rate-limit.d.ts +14 -0
- package/dist/wizard/lib/tower-rate-limit.js +61 -0
- package/dist/wizard/lib/tower-session.d.ts +28 -0
- package/dist/wizard/lib/tower-session.js +119 -0
- package/dist/wizard/lib/treasury-backup.d.ts +23 -0
- package/dist/wizard/lib/treasury-backup.js +126 -0
- package/dist/wizard/lib/treasury-heartbeat.d.ts +82 -0
- package/dist/wizard/lib/treasury-heartbeat.js +1104 -0
- package/dist/wizard/lib/updater.d.ts +29 -0
- package/dist/wizard/lib/updater.js +190 -0
- package/dist/wizard/lib/user-manager.d.ts +39 -0
- package/dist/wizard/lib/user-manager.js +182 -0
- package/dist/wizard/lib/vault.d.ts +26 -0
- package/dist/wizard/lib/vault.js +161 -0
- package/dist/wizard/router.d.ts +5 -0
- package/dist/wizard/router.js +15 -0
- package/dist/wizard/server.d.ts +18 -0
- package/dist/wizard/server.js +436 -0
- package/package.json +59 -0
|
@@ -0,0 +1,535 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provisioning API routes — SSE-streamed infrastructure provisioning.
|
|
3
|
+
*/
|
|
4
|
+
import { randomUUID } from 'node:crypto';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { realpath } from 'node:fs/promises';
|
|
7
|
+
import { addRoute } from '../router.js';
|
|
8
|
+
import { getSessionPassword } from './credentials.js';
|
|
9
|
+
import { vaultGet, vaultKeys } from '../lib/vault.js';
|
|
10
|
+
import { parseJsonBody } from '../lib/body-parser.js';
|
|
11
|
+
import { provisioners, provisionKeys, GITHUB_LINKED_TARGETS, GITHUB_OPTIONAL_TARGETS } from '../lib/provisioner-registry.js';
|
|
12
|
+
import { createManifest, updateManifestStatus, readManifest, deleteManifest, listIncompleteRuns, manifestToCreatedResources, } from '../lib/provision-manifest.js';
|
|
13
|
+
import { provisionDns, cleanupDnsRecords } from '../lib/dns/cloudflare-dns.js';
|
|
14
|
+
import { registerDomain } from '../lib/dns/cloudflare-registrar.js';
|
|
15
|
+
import { prepareGithub } from '../lib/github.js';
|
|
16
|
+
import { sshDeploy } from '../lib/ssh-deploy.js';
|
|
17
|
+
import { s3Deploy } from '../lib/s3-deploy.js';
|
|
18
|
+
import { runBuildStep, getBuildOutputDir } from '../lib/build-step.js';
|
|
19
|
+
import { generateEnvValidator } from '../lib/env-validator.js';
|
|
20
|
+
import { emitCostEstimate } from '../lib/cost-estimator.js';
|
|
21
|
+
import { logDeploy, listDeploys } from '../lib/deploy-log.js';
|
|
22
|
+
import { setupHealthMonitoring } from '../lib/health-monitor.js';
|
|
23
|
+
import { generateSentryInit } from '../lib/sentry-generator.js';
|
|
24
|
+
import { sendJson } from '../lib/http-helpers.js';
|
|
25
|
+
const provisionRuns = new Map();
|
|
26
|
+
/** Concurrency lock — only one provisioning run at a time (F-02). */
|
|
27
|
+
let activeProvisionRun = null;
|
|
28
|
+
/** Scope credentials to only the keys a provisioner needs. Internal _-prefixed keys pass through. */
|
|
29
|
+
function scopeCredentials(allCreds, target) {
|
|
30
|
+
const allowed = provisionKeys[target] || [];
|
|
31
|
+
const scoped = {};
|
|
32
|
+
for (const key of allowed) {
|
|
33
|
+
if (allCreds[key])
|
|
34
|
+
scoped[key] = allCreds[key];
|
|
35
|
+
}
|
|
36
|
+
// Internal keys (injected by pre-steps) always pass through
|
|
37
|
+
for (const [key, val] of Object.entries(allCreds)) {
|
|
38
|
+
if (key.startsWith('_'))
|
|
39
|
+
scoped[key] = val;
|
|
40
|
+
}
|
|
41
|
+
return scoped;
|
|
42
|
+
}
|
|
43
|
+
async function loadCredentials(password) {
|
|
44
|
+
const keys = await vaultKeys(password);
|
|
45
|
+
const creds = {};
|
|
46
|
+
for (const key of keys) {
|
|
47
|
+
const val = await vaultGet(password, key);
|
|
48
|
+
if (val)
|
|
49
|
+
creds[key] = val;
|
|
50
|
+
}
|
|
51
|
+
return creds;
|
|
52
|
+
}
|
|
53
|
+
// POST /api/provision/start — SSE stream provisioning events
|
|
54
|
+
addRoute('POST', '/api/provision/start', async (req, res) => {
|
|
55
|
+
const password = getSessionPassword();
|
|
56
|
+
if (!password) {
|
|
57
|
+
sendJson(res, 401, { error: 'Vault is locked.' });
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
// Parse and validate BEFORE acquiring the lock (IG-R2: prevent lock deadlock on validation failure)
|
|
61
|
+
const body = await parseJsonBody(req);
|
|
62
|
+
// Domain format validation (Fix 4: server-side validation)
|
|
63
|
+
if (body.hostname && !/^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?(\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)+$/i.test(body.hostname)) {
|
|
64
|
+
sendJson(res, 400, { error: 'Invalid hostname format. Expected something like: myapp.example.com' });
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
if (!body.projectDir || !body.projectName || !body.deployTarget) {
|
|
68
|
+
sendJson(res, 400, { error: 'projectDir, projectName, and deployTarget are required' });
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
// Validate projectDir (Kenobi: prevent directory traversal / file exfiltration)
|
|
72
|
+
if (!body.projectDir.startsWith('/') || body.projectDir.includes('..')) {
|
|
73
|
+
sendJson(res, 400, { error: 'projectDir must be an absolute path with no ".." segments' });
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
// IG-R4: Resolve symlinks and use real path for all operations
|
|
77
|
+
try {
|
|
78
|
+
body.projectDir = await realpath(body.projectDir);
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
sendJson(res, 400, { error: 'Could not resolve project directory path' });
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const provisioner = provisioners[body.deployTarget];
|
|
85
|
+
if (!provisioner) {
|
|
86
|
+
sendJson(res, 400, { error: `Unknown deploy target: ${body.deployTarget}` });
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
// Load and validate credentials BEFORE acquiring lock (IG-R3: all failable steps before lock)
|
|
90
|
+
let allCredentials;
|
|
91
|
+
try {
|
|
92
|
+
allCredentials = await loadCredentials(password);
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
sendJson(res, 500, { error: 'Failed to load credentials from vault' });
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
// Scope credentials to only what this provisioner needs (ADR-020)
|
|
99
|
+
const scopedCreds = scopeCredentials(allCredentials, body.deployTarget);
|
|
100
|
+
const runId = randomUUID();
|
|
101
|
+
const ctx = {
|
|
102
|
+
runId,
|
|
103
|
+
projectDir: body.projectDir,
|
|
104
|
+
projectName: body.projectName,
|
|
105
|
+
deployTarget: body.deployTarget,
|
|
106
|
+
framework: (body.framework || 'express').toLowerCase(),
|
|
107
|
+
database: body.database || 'none',
|
|
108
|
+
cache: body.cache || 'none',
|
|
109
|
+
instanceType: body.instanceType || 't3.micro',
|
|
110
|
+
hostname: body.hostname || '',
|
|
111
|
+
credentials: scopedCreds,
|
|
112
|
+
};
|
|
113
|
+
// Validate provisioner context BEFORE acquiring lock (IG-R3: prevents lock leak)
|
|
114
|
+
const errors = await provisioner.validate(ctx);
|
|
115
|
+
if (errors.length > 0) {
|
|
116
|
+
sendJson(res, 400, { error: errors.join('; ') });
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
// CROSS-R4-011: Lock acquired AFTER all validation passes — only SSE streaming can fail from here
|
|
120
|
+
if (activeProvisionRun) {
|
|
121
|
+
sendJson(res, 429, { error: 'A provisioning run is already in progress. Wait for it to complete.' });
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
activeProvisionRun = runId;
|
|
125
|
+
// Start SSE stream
|
|
126
|
+
res.writeHead(200, {
|
|
127
|
+
'Content-Type': 'text/event-stream',
|
|
128
|
+
'Cache-Control': 'no-cache',
|
|
129
|
+
'Connection': 'keep-alive',
|
|
130
|
+
});
|
|
131
|
+
let clientDisconnected = false;
|
|
132
|
+
function sseWrite(chunk) {
|
|
133
|
+
if (clientDisconnected || res.writableEnded)
|
|
134
|
+
return;
|
|
135
|
+
try {
|
|
136
|
+
res.write(chunk);
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
clientDisconnected = true;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
function sseEnd() {
|
|
143
|
+
if (clientDisconnected || res.writableEnded)
|
|
144
|
+
return;
|
|
145
|
+
try {
|
|
146
|
+
res.end();
|
|
147
|
+
}
|
|
148
|
+
catch { /* already closed */ }
|
|
149
|
+
}
|
|
150
|
+
const abortController = new AbortController();
|
|
151
|
+
ctx.abortSignal = abortController.signal;
|
|
152
|
+
req.on('close', () => {
|
|
153
|
+
clientDisconnected = true;
|
|
154
|
+
abortController.abort();
|
|
155
|
+
clearInterval(keepaliveTimer);
|
|
156
|
+
});
|
|
157
|
+
// SSE keepalive — prevents proxy/VPN/browser timeout on idle connections
|
|
158
|
+
const keepaliveTimer = setInterval(() => {
|
|
159
|
+
sseWrite(': keepalive\n\n');
|
|
160
|
+
}, 15000);
|
|
161
|
+
let eventId = 0;
|
|
162
|
+
const emit = (event) => {
|
|
163
|
+
eventId++;
|
|
164
|
+
sseWrite(`id: ${eventId}\ndata: ${JSON.stringify(event)}\n\n`);
|
|
165
|
+
};
|
|
166
|
+
const region = allCredentials['aws-region'] || 'us-east-1';
|
|
167
|
+
// Persist manifest to disk before starting (crash recovery)
|
|
168
|
+
await createManifest(runId, body.deployTarget, region, body.projectName);
|
|
169
|
+
/** Shared outputs that pre-steps inject and provisioners consume. */
|
|
170
|
+
const sharedOutputs = {};
|
|
171
|
+
try {
|
|
172
|
+
// ── GitHub pre-step (ADR-011) ──────────────────────────────────
|
|
173
|
+
// Runs before the provisioner so platforms can link to the repo.
|
|
174
|
+
// Uses allCredentials — GitHub token is not in provisioner-scoped creds (ADR-020).
|
|
175
|
+
const hasGithub = allCredentials['github-token'];
|
|
176
|
+
const needsGithub = GITHUB_LINKED_TARGETS.includes(body.deployTarget);
|
|
177
|
+
const wantsGithub = GITHUB_OPTIONAL_TARGETS.includes(body.deployTarget);
|
|
178
|
+
if (hasGithub && (needsGithub || wantsGithub)) {
|
|
179
|
+
const ghResult = await prepareGithub(runId, allCredentials['github-token'], allCredentials['github-owner'] || null, body.projectName, body.projectDir, emit, abortController.signal, ctx.framework, body.deployTarget);
|
|
180
|
+
if (ghResult.success) {
|
|
181
|
+
sharedOutputs['GITHUB_REPO_URL'] = ghResult.repoUrl;
|
|
182
|
+
sharedOutputs['GITHUB_OWNER'] = ghResult.owner;
|
|
183
|
+
sharedOutputs['GITHUB_REPO_NAME'] = ghResult.repoName;
|
|
184
|
+
}
|
|
185
|
+
else if (needsGithub) {
|
|
186
|
+
// For platforms that require GitHub, warn but continue (graceful degradation)
|
|
187
|
+
emit({ step: 'github-warning', status: 'error', message: `GitHub setup failed — ${body.deployTarget} project will be created without auto-deploy. Push manually later.`, detail: ghResult.error });
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
else if (!hasGithub && needsGithub) {
|
|
191
|
+
emit({ step: 'github-skip', status: 'skipped', message: `No GitHub token in vault. ${body.deployTarget} project will be created without auto-deploy. Add GitHub credentials for CI/CD.` });
|
|
192
|
+
}
|
|
193
|
+
// Pass GitHub outputs to provisioner via credentials (provisioners read from credentials map)
|
|
194
|
+
if (sharedOutputs['GITHUB_OWNER']) {
|
|
195
|
+
ctx.credentials['_github-owner'] = sharedOutputs['GITHUB_OWNER'];
|
|
196
|
+
ctx.credentials['_github-repo-name'] = sharedOutputs['GITHUB_REPO_NAME'];
|
|
197
|
+
}
|
|
198
|
+
// ── Cost estimation (ADR-022) ──────────────────────────────────
|
|
199
|
+
emitCostEstimate(body.deployTarget, ctx.instanceType, ctx.database, ctx.cache, emit);
|
|
200
|
+
// ── Provisioner ──────────────────────────────────────────────
|
|
201
|
+
const result = await provisioner.provision(ctx, emit);
|
|
202
|
+
// Merge shared outputs into result
|
|
203
|
+
for (const [k, v] of Object.entries(sharedOutputs)) {
|
|
204
|
+
result.outputs[k] = v;
|
|
205
|
+
}
|
|
206
|
+
// ── Pre-deploy build step (ADR-016) ────────────────────────────
|
|
207
|
+
// Runs AFTER provisioner but BEFORE deploy actions.
|
|
208
|
+
if (result.success && body.deployTarget !== 'docker') {
|
|
209
|
+
const buildResult = await runBuildStep(body.projectDir, ctx.framework, emit, abortController.signal);
|
|
210
|
+
if (!buildResult.success) {
|
|
211
|
+
emit({ step: 'build-fatal', status: 'error', message: 'Build failed — infrastructure was created, but code deploy will be skipped. Fix the build locally and deploy manually.', detail: buildResult.error });
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
// ── Deploy post-step (v3.8.0 Last Mile) ──────────────────────
|
|
215
|
+
if (result.success && body.deployTarget === 'vps') {
|
|
216
|
+
// AWS VPS: SSH in and execute deploy scripts
|
|
217
|
+
const sshHost = result.outputs['SSH_HOST'];
|
|
218
|
+
const sshUser = result.outputs['SSH_USER'] || 'ec2-user';
|
|
219
|
+
const sshKey = result.outputs['SSH_KEY_PATH'] || '.ssh/deploy-key.pem';
|
|
220
|
+
if (sshHost) {
|
|
221
|
+
const deployResult = await sshDeploy(body.projectDir, sshHost, sshUser, sshKey, ctx.hostname || undefined, ctx.framework, emit, abortController.signal);
|
|
222
|
+
if (deployResult.deployUrl) {
|
|
223
|
+
result.outputs['DEPLOY_URL'] = deployResult.deployUrl;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
else {
|
|
227
|
+
emit({ step: 'deploy-skip', status: 'skipped', message: 'No SSH host available — SSH deploy skipped' });
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
else if (result.success && body.deployTarget === 'static') {
|
|
231
|
+
// S3 Static: Upload build directory
|
|
232
|
+
const bucket = result.outputs['S3_BUCKET'];
|
|
233
|
+
const websiteUrl = result.outputs['S3_WEBSITE_URL'];
|
|
234
|
+
const awsKeyId = allCredentials['aws-access-key-id'];
|
|
235
|
+
const awsSecret = allCredentials['aws-secret-access-key'];
|
|
236
|
+
if (bucket && websiteUrl && awsKeyId && awsSecret) {
|
|
237
|
+
const s3Result = await s3Deploy(bucket, join(body.projectDir, getBuildOutputDir(ctx.framework)), allCredentials['aws-region'] || 'us-east-1', {
|
|
238
|
+
accessKeyId: awsKeyId,
|
|
239
|
+
secretAccessKey: awsSecret,
|
|
240
|
+
}, websiteUrl, emit);
|
|
241
|
+
if (s3Result.deployUrl) {
|
|
242
|
+
result.outputs['DEPLOY_URL'] = s3Result.deployUrl;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
else {
|
|
246
|
+
emit({ step: 'deploy-skip', status: 'skipped', message: 'No S3 bucket available — upload skipped' });
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
else if (result.success && (body.deployTarget === 'vercel' || body.deployTarget === 'cloudflare' || body.deployTarget === 'railway')) {
|
|
250
|
+
// Platform deploys are triggered by the git push — provisioner handles polling
|
|
251
|
+
const deployUrl = result.outputs['DEPLOY_URL'] || result.outputs['VERCEL_DOMAIN'] || result.outputs['CF_PROJECT_URL'] || result.outputs['RAILWAY_DOMAIN'];
|
|
252
|
+
if (deployUrl && !result.outputs['DEPLOY_URL']) {
|
|
253
|
+
result.outputs['DEPLOY_URL'] = deployUrl.startsWith('http') ? deployUrl : `https://${deployUrl}`;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
// Domain registration — pre-DNS step (non-fatal, irreversible)
|
|
257
|
+
// Registration creates the Cloudflare zone, which DNS needs to exist (ADR-010)
|
|
258
|
+
if (result.success && body.registerDomain && ctx.hostname && allCredentials['cloudflare-api-token'] && allCredentials['cloudflare-account-id']) {
|
|
259
|
+
const regResult = await registerDomain(allCredentials['cloudflare-api-token'], allCredentials['cloudflare-account-id'], ctx.hostname, emit);
|
|
260
|
+
if (regResult.success) {
|
|
261
|
+
result.outputs['REGISTRAR_DOMAIN'] = regResult.domain || ctx.hostname;
|
|
262
|
+
if (regResult.expiresAt)
|
|
263
|
+
result.outputs['REGISTRAR_EXPIRY'] = regResult.expiresAt;
|
|
264
|
+
// Note: domain registration is NOT tracked for cleanup — it's irreversible
|
|
265
|
+
}
|
|
266
|
+
// Registration failure is non-fatal — DNS may still work if zone already exists
|
|
267
|
+
}
|
|
268
|
+
else if (result.success && body.registerDomain && ctx.hostname && !allCredentials['cloudflare-account-id']) {
|
|
269
|
+
emit({ step: 'registrar-skip', status: 'skipped', message: 'Domain registration requested but no Cloudflare Account ID in vault. Add it in Cloud Providers.' });
|
|
270
|
+
}
|
|
271
|
+
else if (result.success && body.registerDomain && ctx.hostname && !allCredentials['cloudflare-api-token']) {
|
|
272
|
+
emit({ step: 'registrar-skip', status: 'skipped', message: 'Domain registration requested but no Cloudflare API token in vault. Add Cloudflare credentials to enable registration.' });
|
|
273
|
+
}
|
|
274
|
+
// DNS post-provision step (non-fatal)
|
|
275
|
+
if (result.success && ctx.hostname && allCredentials['cloudflare-api-token']) {
|
|
276
|
+
const dnsResult = await provisionDns(runId, allCredentials['cloudflare-api-token'], ctx.hostname, body.deployTarget, result.outputs, emit);
|
|
277
|
+
// Add DNS records to resource list for cleanup tracking
|
|
278
|
+
if (dnsResult.records.length > 0) {
|
|
279
|
+
for (const record of dnsResult.records) {
|
|
280
|
+
result.resources.push({
|
|
281
|
+
type: 'dns-record',
|
|
282
|
+
id: `${dnsResult.zoneId}:${record.id}`,
|
|
283
|
+
region: 'global',
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
result.outputs['DNS_HOSTNAME'] = ctx.hostname;
|
|
287
|
+
result.outputs['DNS_ZONE_ID'] = dnsResult.zoneId;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
else if (result.success && ctx.hostname && !allCredentials['cloudflare-api-token']) {
|
|
291
|
+
emit({ step: 'dns-skip', status: 'skipped', message: `Hostname "${ctx.hostname}" set but no Cloudflare token in vault. Add Cloudflare credentials to enable DNS wiring.` });
|
|
292
|
+
}
|
|
293
|
+
// ── Sentry integration (ADR-024) — before env-validator so DSN is in .env ──
|
|
294
|
+
if (result.success) {
|
|
295
|
+
await generateSentryInit(body.projectDir, ctx.framework, allCredentials['sentry-dsn'], emit);
|
|
296
|
+
}
|
|
297
|
+
// ── Environment validation script (ADR-018) ──────────────────
|
|
298
|
+
if (result.success) {
|
|
299
|
+
const envResult = await generateEnvValidator(body.projectDir, ctx.framework);
|
|
300
|
+
if (envResult.file) {
|
|
301
|
+
const hint = envResult.file.endsWith('.py')
|
|
302
|
+
? 'Add "python validate_env.py &&" before your start command'
|
|
303
|
+
: 'Add "node validate-env.js &&" before your start command in package.json';
|
|
304
|
+
emit({ step: 'env-validator', status: 'done', message: `Generated ${envResult.file} — ${hint}` });
|
|
305
|
+
}
|
|
306
|
+
else {
|
|
307
|
+
emit({ step: 'env-validator', status: 'skipped', message: 'No .env file found — env validation script skipped' });
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
// ── Health monitoring (ADR-023) ──────────────────────────────
|
|
311
|
+
if (result.success) {
|
|
312
|
+
const deployUrl = result.outputs['DEPLOY_URL'] || '';
|
|
313
|
+
await setupHealthMonitoring(body.deployTarget, body.projectDir, body.projectName, deployUrl, result.outputs, emit);
|
|
314
|
+
}
|
|
315
|
+
// ── Deploy logging (ADR-021) ─────────────────────────────────
|
|
316
|
+
if (result.success) {
|
|
317
|
+
try {
|
|
318
|
+
// Sanitize outputs before persisting — strip secrets (same keywords as SSE sanitizer)
|
|
319
|
+
const logOutputs = { ...result.outputs };
|
|
320
|
+
delete logOutputs['DB_PASSWORD'];
|
|
321
|
+
delete logOutputs['GITHUB_TOKEN'];
|
|
322
|
+
const SAFE_LOG_KEYS = new Set(['DEPLOY_URL', 'S3_WEBSITE_URL', 'CF_PROJECT_URL', 'GITHUB_REPO_URL', 'SSH_KEY_PATH']);
|
|
323
|
+
for (const key of Object.keys(logOutputs)) {
|
|
324
|
+
if (SAFE_LOG_KEYS.has(key))
|
|
325
|
+
continue;
|
|
326
|
+
const lk = key.toLowerCase();
|
|
327
|
+
if (lk.includes('password') || lk.includes('secret') || lk.includes('token')
|
|
328
|
+
|| lk.includes('credential') || lk.includes('_key') || lk.includes('_pass')
|
|
329
|
+
|| lk.includes('_pwd') || lk.includes('passphrase') || lk.includes('bearer')
|
|
330
|
+
|| lk.includes('oauth') || lk.includes('jwt') || lk.includes('signing')
|
|
331
|
+
|| lk.includes('private') || lk.includes('connection_uri') || lk.includes('database_url')
|
|
332
|
+
|| lk.includes('redis_url') || lk.includes('mongo_uri')
|
|
333
|
+
|| lk.includes('cert') || lk.includes('hmac') || lk.includes('auth_code')) {
|
|
334
|
+
delete logOutputs[key];
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
const logPath = await logDeploy({
|
|
338
|
+
runId,
|
|
339
|
+
timestamp: new Date().toISOString(),
|
|
340
|
+
target: body.deployTarget,
|
|
341
|
+
projectName: body.projectName,
|
|
342
|
+
framework: ctx.framework,
|
|
343
|
+
deployUrl: result.outputs['DEPLOY_URL'] || '',
|
|
344
|
+
hostname: ctx.hostname,
|
|
345
|
+
region,
|
|
346
|
+
resources: result.resources.map(r => ({ type: r.type, id: r.id })),
|
|
347
|
+
outputs: logOutputs,
|
|
348
|
+
});
|
|
349
|
+
emit({ step: 'deploy-log', status: 'done', message: `Deploy logged to ${logPath}` });
|
|
350
|
+
}
|
|
351
|
+
catch {
|
|
352
|
+
// Non-fatal — deploy succeeded even if logging fails
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
// Track for cleanup by run ID (in-memory for current session)
|
|
356
|
+
if (result.resources.length > 0) {
|
|
357
|
+
// Store only cleanup-relevant credentials, not the full vault (Kenobi: minimize credential exposure)
|
|
358
|
+
const cleanupCreds = {};
|
|
359
|
+
const cleanupKeys = {
|
|
360
|
+
vps: ['aws-access-key-id', 'aws-secret-access-key', 'aws-region'],
|
|
361
|
+
static: ['aws-access-key-id', 'aws-secret-access-key', 'aws-region'],
|
|
362
|
+
vercel: ['vercel-token'],
|
|
363
|
+
railway: ['railway-token'],
|
|
364
|
+
cloudflare: ['cloudflare-api-token'],
|
|
365
|
+
docker: [],
|
|
366
|
+
};
|
|
367
|
+
for (const key of (cleanupKeys[body.deployTarget] || [])) {
|
|
368
|
+
if (allCredentials[key])
|
|
369
|
+
cleanupCreds[key] = allCredentials[key];
|
|
370
|
+
}
|
|
371
|
+
// Always include Cloudflare token if DNS records were created
|
|
372
|
+
if (result.resources.some(r => r.type === 'dns-record') && allCredentials['cloudflare-api-token']) {
|
|
373
|
+
cleanupCreds['cloudflare-api-token'] = allCredentials['cloudflare-api-token'];
|
|
374
|
+
}
|
|
375
|
+
provisionRuns.set(runId, {
|
|
376
|
+
resources: result.resources,
|
|
377
|
+
credentials: cleanupCreds,
|
|
378
|
+
target: body.deployTarget,
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
// Update manifest on disk with final status
|
|
382
|
+
await updateManifestStatus(runId, result.success ? 'complete' : 'failed');
|
|
383
|
+
// Strip DB_PASSWORD from SSE payload — secret must not leak to the client (Kenobi F-03)
|
|
384
|
+
const safeOutputs = { ...result.outputs };
|
|
385
|
+
delete safeOutputs['DB_PASSWORD'];
|
|
386
|
+
delete safeOutputs['GITHUB_TOKEN'];
|
|
387
|
+
// IG-R4/R5: Secret stripping — broad keywords + allowlist for safe output keys
|
|
388
|
+
const SAFE_OUTPUT_KEYS = new Set(['DEPLOY_URL', 'S3_WEBSITE_URL', 'CF_PROJECT_URL', 'GITHUB_REPO_URL', 'SSH_KEY_PATH']);
|
|
389
|
+
for (const key of Object.keys(safeOutputs)) {
|
|
390
|
+
if (SAFE_OUTPUT_KEYS.has(key))
|
|
391
|
+
continue;
|
|
392
|
+
const lk = key.toLowerCase();
|
|
393
|
+
if (lk.includes('password') || lk.includes('secret') || lk.includes('token')
|
|
394
|
+
|| lk.includes('credential') || lk.includes('_key') || lk.includes('_pass')
|
|
395
|
+
|| lk.includes('_pwd') || lk.includes('passphrase') || lk.includes('bearer')
|
|
396
|
+
|| lk.includes('oauth') || lk.includes('jwt') || lk.includes('signing')
|
|
397
|
+
|| lk.includes('private') || lk.includes('connection_uri') || lk.includes('database_url')
|
|
398
|
+
|| lk.includes('redis_url') || lk.includes('mongo_uri')
|
|
399
|
+
|| lk.includes('cert') || lk.includes('hmac') || lk.includes('auth_code')) {
|
|
400
|
+
delete safeOutputs[key];
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
const safeResult = { ...result, outputs: safeOutputs };
|
|
404
|
+
sseWrite(`data: ${JSON.stringify({ step: 'complete', status: result.success ? 'done' : 'error', message: result.success ? 'Provisioning complete' : result.error || 'Provisioning failed', result: safeResult, runId })}\n\n`);
|
|
405
|
+
}
|
|
406
|
+
catch (err) {
|
|
407
|
+
const errMsg = err.message;
|
|
408
|
+
console.error('Provisioning fatal error:', errMsg);
|
|
409
|
+
await updateManifestStatus(runId, 'failed');
|
|
410
|
+
// Include sanitized error detail so the user can act on it (UX H-01)
|
|
411
|
+
const safeErrMsg = errMsg.replace(/[A-Za-z0-9+/=]{16,}/g, '***'); // Strip tokens (16+ chars, IG-R2)
|
|
412
|
+
sseWrite(`data: ${JSON.stringify({ step: 'fatal', status: 'error', message: 'Provisioning failed unexpectedly. Check that credentials are valid and try again.', detail: safeErrMsg })}\n\n`);
|
|
413
|
+
}
|
|
414
|
+
finally {
|
|
415
|
+
activeProvisionRun = null;
|
|
416
|
+
clearInterval(keepaliveTimer);
|
|
417
|
+
sseWrite('data: [DONE]\n\n');
|
|
418
|
+
sseEnd();
|
|
419
|
+
}
|
|
420
|
+
});
|
|
421
|
+
// POST /api/provision/cleanup — clean up resources from a provisioning run
|
|
422
|
+
addRoute('POST', '/api/provision/cleanup', async (req, res) => {
|
|
423
|
+
const password = getSessionPassword();
|
|
424
|
+
if (!password) {
|
|
425
|
+
sendJson(res, 401, { error: 'Vault is locked.' });
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
const body = await parseJsonBody(req);
|
|
429
|
+
// If no runId provided, clean up the most recent run
|
|
430
|
+
let runId = body.runId;
|
|
431
|
+
if (!runId) {
|
|
432
|
+
const keys = [...provisionRuns.keys()];
|
|
433
|
+
runId = keys[keys.length - 1];
|
|
434
|
+
}
|
|
435
|
+
// Try in-memory runs first, then fall back to disk manifests (crash recovery)
|
|
436
|
+
let target;
|
|
437
|
+
let resources;
|
|
438
|
+
let credentials;
|
|
439
|
+
if (runId && provisionRuns.has(runId)) {
|
|
440
|
+
const run = provisionRuns.get(runId);
|
|
441
|
+
target = run.target;
|
|
442
|
+
resources = run.resources;
|
|
443
|
+
credentials = run.credentials;
|
|
444
|
+
}
|
|
445
|
+
else if (runId) {
|
|
446
|
+
// Crash recovery: load from disk manifest + vault credentials
|
|
447
|
+
const manifest = await readManifest(runId);
|
|
448
|
+
if (!manifest || manifest.status === 'cleaned') {
|
|
449
|
+
sendJson(res, 200, { cleaned: true, message: 'No resources to clean up' });
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
target = manifest.target;
|
|
453
|
+
resources = manifestToCreatedResources(manifest);
|
|
454
|
+
credentials = await loadCredentials(password);
|
|
455
|
+
}
|
|
456
|
+
else {
|
|
457
|
+
sendJson(res, 200, { cleaned: true, message: 'No resources to clean up' });
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
if (resources.length === 0) {
|
|
461
|
+
await deleteManifest(runId);
|
|
462
|
+
sendJson(res, 200, { cleaned: true, message: 'No resources to clean up' });
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
const provisioner = provisioners[target];
|
|
466
|
+
if (!provisioner) {
|
|
467
|
+
sendJson(res, 400, { error: `Unknown target: ${target}` });
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
try {
|
|
471
|
+
// Clean up DNS records separately (they're not managed by the provisioner)
|
|
472
|
+
// Skip github-repo — repos are tracked for idempotency, not cleanup (ADR-012)
|
|
473
|
+
const dnsResources = resources.filter((r) => r.type === 'dns-record');
|
|
474
|
+
const infraResources = resources.filter((r) => r.type !== 'dns-record' && r.type !== 'github-repo');
|
|
475
|
+
if (dnsResources.length > 0 && credentials['cloudflare-api-token']) {
|
|
476
|
+
await cleanupDnsRecords(credentials['cloudflare-api-token'], dnsResources.map((r) => r.id));
|
|
477
|
+
}
|
|
478
|
+
// Clean up infrastructure resources via the provisioner
|
|
479
|
+
if (infraResources.length > 0) {
|
|
480
|
+
await provisioner.cleanup(infraResources, credentials);
|
|
481
|
+
}
|
|
482
|
+
const count = resources.length;
|
|
483
|
+
provisionRuns.delete(runId);
|
|
484
|
+
await updateManifestStatus(runId, 'cleaned');
|
|
485
|
+
await deleteManifest(runId);
|
|
486
|
+
const notes = [];
|
|
487
|
+
// Domain registration and GitHub repos are irreversible — always warn
|
|
488
|
+
notes.push('Note: If a domain was registered during this run, that purchase cannot be reversed. Manage it at dash.cloudflare.com.');
|
|
489
|
+
if (resources.some((r) => r.type === 'github-repo')) {
|
|
490
|
+
notes.push('Note: GitHub repository was not deleted (repos are preserved). Delete manually at github.com if needed.');
|
|
491
|
+
}
|
|
492
|
+
sendJson(res, 200, { cleaned: true, message: `Cleaned up ${count} resources`, notes });
|
|
493
|
+
}
|
|
494
|
+
catch (err) {
|
|
495
|
+
sendJson(res, 500, { error: `Cleanup failed: ${err.message}` });
|
|
496
|
+
}
|
|
497
|
+
});
|
|
498
|
+
// GET /api/deploys — list recent deploy history (ADR-021)
|
|
499
|
+
addRoute('GET', '/api/deploys', async (_req, res) => {
|
|
500
|
+
const password = getSessionPassword();
|
|
501
|
+
if (!password) {
|
|
502
|
+
sendJson(res, 401, { error: 'Vault is locked.' });
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
const deploys = await listDeploys();
|
|
506
|
+
sendJson(res, 200, {
|
|
507
|
+
deploys: deploys.map(d => ({
|
|
508
|
+
timestamp: d.timestamp,
|
|
509
|
+
target: d.target,
|
|
510
|
+
projectName: d.projectName,
|
|
511
|
+
deployUrl: d.deployUrl,
|
|
512
|
+
hostname: d.hostname,
|
|
513
|
+
resourceCount: d.resources.length,
|
|
514
|
+
})),
|
|
515
|
+
});
|
|
516
|
+
});
|
|
517
|
+
// GET /api/provision/incomplete — check for orphaned runs from crashes
|
|
518
|
+
addRoute('GET', '/api/provision/incomplete', async (_req, res) => {
|
|
519
|
+
const password = getSessionPassword();
|
|
520
|
+
if (!password) {
|
|
521
|
+
sendJson(res, 401, { error: 'Vault is locked.' });
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
const incomplete = await listIncompleteRuns();
|
|
525
|
+
sendJson(res, 200, {
|
|
526
|
+
runs: incomplete.map((m) => ({
|
|
527
|
+
runId: m.runId,
|
|
528
|
+
startedAt: m.startedAt,
|
|
529
|
+
target: m.target,
|
|
530
|
+
projectName: m.projectName,
|
|
531
|
+
resourceCount: m.resources.filter((r) => r.status === 'created').length,
|
|
532
|
+
resources: m.resources.filter((r) => r.status === 'created').map((r) => `${r.type}: ${r.id}`),
|
|
533
|
+
})),
|
|
534
|
+
});
|
|
535
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terminal API — WebSocket bridge between browser (xterm.js) and server (node-pty).
|
|
3
|
+
* Also REST endpoints for session management.
|
|
4
|
+
*
|
|
5
|
+
* WebSocket protocol:
|
|
6
|
+
* Client → Server: raw keystrokes (text frames)
|
|
7
|
+
* Server → Client: raw terminal output (text frames)
|
|
8
|
+
* Client → Server: JSON control messages: { type: "resize", cols, rows }
|
|
9
|
+
* Server → Client: JSON control messages: { type: "exit", code }
|
|
10
|
+
*
|
|
11
|
+
* Auth: vault password required in the WebSocket URL query string.
|
|
12
|
+
*/
|
|
13
|
+
import type { IncomingMessage } from 'node:http';
|
|
14
|
+
import type { Duplex } from 'node:stream';
|
|
15
|
+
/**
|
|
16
|
+
* Handle a WebSocket upgrade request for a terminal session.
|
|
17
|
+
* URL: /ws/terminal?session=<id>&token=<authToken>
|
|
18
|
+
*
|
|
19
|
+
* Auth flow: vault password → origin check → HMAC token → session existence.
|
|
20
|
+
* Then ws library handles the protocol handshake.
|
|
21
|
+
*/
|
|
22
|
+
export declare function handleTerminalUpgrade(req: IncomingMessage, socket: Duplex, head: Buffer, userSession?: {
|
|
23
|
+
username: string;
|
|
24
|
+
role: 'admin' | 'deployer' | 'viewer';
|
|
25
|
+
}): void;
|