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,287 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vercel provisioner — creates a real Vercel project via API + generates vercel.json.
|
|
3
|
+
* v3.8.0: Links GitHub repo, sets env vars, polls deploy (ADR-015).
|
|
4
|
+
*/
|
|
5
|
+
import { writeFile, readFile } from 'node:fs/promises';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
import { httpsPost, httpsGet, httpsDelete, safeJsonParse, slugify } from './http-client.js';
|
|
8
|
+
import { recordResourcePending, recordResourceCreated } from '../provision-manifest.js';
|
|
9
|
+
import { appendEnvSection } from '../env-writer.js';
|
|
10
|
+
const DEPLOY_POLL_INTERVAL_MS = 5000;
|
|
11
|
+
const DEPLOY_POLL_TIMEOUT_MS = 300_000; // 5 minutes
|
|
12
|
+
export const vercelProvisioner = {
|
|
13
|
+
async validate(ctx) {
|
|
14
|
+
const errors = [];
|
|
15
|
+
if (!ctx.projectDir)
|
|
16
|
+
errors.push('Project directory is required');
|
|
17
|
+
if (!ctx.credentials['vercel-token'])
|
|
18
|
+
errors.push('Vercel API token is required');
|
|
19
|
+
return errors;
|
|
20
|
+
},
|
|
21
|
+
async provision(ctx, emit) {
|
|
22
|
+
const files = [];
|
|
23
|
+
const resources = [];
|
|
24
|
+
const outputs = {};
|
|
25
|
+
const token = ctx.credentials['vercel-token'];
|
|
26
|
+
const slug = slugify(ctx.projectName);
|
|
27
|
+
const framework = ctx.framework || 'next.js';
|
|
28
|
+
const headers = {
|
|
29
|
+
'Authorization': `Bearer ${token}`,
|
|
30
|
+
'Content-Type': 'application/json',
|
|
31
|
+
};
|
|
32
|
+
// Step 1: Create Vercel project
|
|
33
|
+
emit({ step: 'vercel-project', status: 'started', message: 'Creating Vercel project' });
|
|
34
|
+
let projectId = '';
|
|
35
|
+
try {
|
|
36
|
+
await recordResourcePending(ctx.runId, 'vercel-project', slug, 'global');
|
|
37
|
+
// Only pass framework if it's a Vercel-recognized value — otherwise omit and let Vercel auto-detect
|
|
38
|
+
const vercelFrameworks = {
|
|
39
|
+
'next.js': 'nextjs',
|
|
40
|
+
'nuxt': 'nuxtjs',
|
|
41
|
+
'svelte': 'sveltekit',
|
|
42
|
+
'remix': 'remix',
|
|
43
|
+
'gatsby': 'gatsby',
|
|
44
|
+
'astro': 'astro',
|
|
45
|
+
'vite': 'vite',
|
|
46
|
+
};
|
|
47
|
+
const projectBody = { name: slug };
|
|
48
|
+
const vercelFw = vercelFrameworks[framework];
|
|
49
|
+
if (vercelFw)
|
|
50
|
+
projectBody.framework = vercelFw;
|
|
51
|
+
const body = JSON.stringify(projectBody);
|
|
52
|
+
const res = await httpsPost('api.vercel.com', '/v10/projects', headers, body);
|
|
53
|
+
if (res.status === 200 || res.status === 201) {
|
|
54
|
+
const data = safeJsonParse(res.body);
|
|
55
|
+
projectId = data?.id ?? '';
|
|
56
|
+
if (!projectId)
|
|
57
|
+
throw new Error('Vercel returned no project ID');
|
|
58
|
+
resources.push({ type: 'vercel-project', id: projectId, region: 'global' });
|
|
59
|
+
await recordResourceCreated(ctx.runId, 'vercel-project', projectId, 'global');
|
|
60
|
+
outputs['VERCEL_PROJECT_ID'] = projectId;
|
|
61
|
+
outputs['VERCEL_PROJECT_NAME'] = data?.name ?? slug;
|
|
62
|
+
emit({ step: 'vercel-project', status: 'done', message: `Project "${data?.name}" created on Vercel` });
|
|
63
|
+
}
|
|
64
|
+
else if (res.status === 409) {
|
|
65
|
+
// Project already exists — fetch its ID for subsequent steps
|
|
66
|
+
try {
|
|
67
|
+
const existingRes = await httpsGet('api.vercel.com', `/v10/projects/${slug}`, headers);
|
|
68
|
+
if (existingRes.status === 200) {
|
|
69
|
+
const existingData = safeJsonParse(existingRes.body);
|
|
70
|
+
projectId = existingData?.id ?? '';
|
|
71
|
+
if (projectId) {
|
|
72
|
+
resources.push({ type: 'vercel-project', id: projectId, region: 'global' });
|
|
73
|
+
await recordResourceCreated(ctx.runId, 'vercel-project', projectId, 'global');
|
|
74
|
+
outputs['VERCEL_PROJECT_ID'] = projectId;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
catch { /* fetch failed, proceed without ID */ }
|
|
79
|
+
outputs['VERCEL_PROJECT_NAME'] = slug;
|
|
80
|
+
emit({ step: 'vercel-project', status: 'done', message: `Project "${slug}" already exists on Vercel — will use existing`, detail: projectId ? `ID: ${projectId}` : 'Could not fetch project ID' });
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
const errBody = safeJsonParse(res.body);
|
|
84
|
+
throw new Error(errBody?.error?.message || `Vercel API returned ${res.status}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
catch (err) {
|
|
88
|
+
emit({ step: 'vercel-project', status: 'error', message: 'Failed to create Vercel project', detail: err.message });
|
|
89
|
+
return { success: false, resources, outputs, files, error: err.message };
|
|
90
|
+
}
|
|
91
|
+
// Step 2: Add custom domain if hostname provided
|
|
92
|
+
if (ctx.hostname && projectId) {
|
|
93
|
+
emit({ step: 'vercel-domain', status: 'started', message: `Adding domain ${ctx.hostname} to Vercel project` });
|
|
94
|
+
try {
|
|
95
|
+
const domainBody = JSON.stringify({ name: ctx.hostname });
|
|
96
|
+
const domainRes = await httpsPost('api.vercel.com', `/v10/projects/${projectId}/domains`, headers, domainBody);
|
|
97
|
+
if (domainRes.status === 200 || domainRes.status === 201) {
|
|
98
|
+
outputs['VERCEL_DOMAIN'] = ctx.hostname;
|
|
99
|
+
emit({ step: 'vercel-domain', status: 'done', message: `Domain "${ctx.hostname}" added to Vercel project` });
|
|
100
|
+
}
|
|
101
|
+
else if (domainRes.status === 409) {
|
|
102
|
+
emit({ step: 'vercel-domain', status: 'done', message: `Domain "${ctx.hostname}" already configured on Vercel` });
|
|
103
|
+
outputs['VERCEL_DOMAIN'] = ctx.hostname;
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
const errBody = safeJsonParse(domainRes.body);
|
|
107
|
+
throw new Error(errBody?.error?.message || `Vercel domains API returned ${domainRes.status}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
catch (err) {
|
|
111
|
+
emit({ step: 'vercel-domain', status: 'error', message: 'Failed to add domain to Vercel', detail: err.message });
|
|
112
|
+
// Non-fatal — DNS wiring will still work, user can add domain manually
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
else if (ctx.hostname && !projectId) {
|
|
116
|
+
emit({ step: 'vercel-domain', status: 'skipped', message: 'Cannot add domain — no project ID (existing project)' });
|
|
117
|
+
}
|
|
118
|
+
// Step 3: Generate vercel.json
|
|
119
|
+
emit({ step: 'vercel-config', status: 'started', message: 'Generating vercel.json' });
|
|
120
|
+
try {
|
|
121
|
+
const config = {
|
|
122
|
+
$schema: 'https://openapi.vercel.sh/vercel.json',
|
|
123
|
+
};
|
|
124
|
+
if (framework === 'express') {
|
|
125
|
+
config.builds = [{ src: 'dist/index.js', use: '@vercel/node' }];
|
|
126
|
+
config.routes = [{ src: '/(.*)', dest: 'dist/index.js' }];
|
|
127
|
+
}
|
|
128
|
+
await writeFile(join(ctx.projectDir, 'vercel.json'), JSON.stringify(config, null, 2) + '\n', 'utf-8');
|
|
129
|
+
files.push('vercel.json');
|
|
130
|
+
emit({ step: 'vercel-config', status: 'done', message: 'Generated vercel.json' });
|
|
131
|
+
}
|
|
132
|
+
catch (err) {
|
|
133
|
+
emit({ step: 'vercel-config', status: 'error', message: 'Failed to write vercel.json', detail: err.message });
|
|
134
|
+
// Non-fatal — project was still created
|
|
135
|
+
}
|
|
136
|
+
// Step 4: Link GitHub repo (ADR-015 — auto-deploy on push)
|
|
137
|
+
const ghOwner = ctx.credentials['_github-owner'];
|
|
138
|
+
const ghRepo = ctx.credentials['_github-repo-name'];
|
|
139
|
+
if (projectId && ghOwner && ghRepo) {
|
|
140
|
+
emit({ step: 'vercel-link', status: 'started', message: `Linking GitHub repo ${ghOwner}/${ghRepo} to Vercel` });
|
|
141
|
+
try {
|
|
142
|
+
const linkBody = JSON.stringify({
|
|
143
|
+
type: 'github',
|
|
144
|
+
repo: `${ghOwner}/${ghRepo}`,
|
|
145
|
+
sourceless: false,
|
|
146
|
+
productionBranch: 'main',
|
|
147
|
+
});
|
|
148
|
+
const linkRes = await httpsPost('api.vercel.com', `/v10/projects/${projectId}/link`, headers, linkBody);
|
|
149
|
+
if (linkRes.status === 200 || linkRes.status === 201) {
|
|
150
|
+
emit({ step: 'vercel-link', status: 'done', message: `GitHub repo linked — auto-deploy enabled on push to main` });
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
const errBody = safeJsonParse(linkRes.body);
|
|
154
|
+
emit({ step: 'vercel-link', status: 'error', message: 'Failed to link GitHub repo', detail: errBody?.error?.message || `API returned ${linkRes.status}` });
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
catch (err) {
|
|
158
|
+
emit({ step: 'vercel-link', status: 'error', message: 'Failed to link GitHub repo', detail: err.message });
|
|
159
|
+
// Non-fatal — user can link manually
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
// Step 5: Set environment variables
|
|
163
|
+
if (projectId) {
|
|
164
|
+
emit({ step: 'vercel-envvars', status: 'started', message: 'Setting environment variables' });
|
|
165
|
+
try {
|
|
166
|
+
// Collect env vars from .env file (skip comments, empty lines, and VoidForge metadata)
|
|
167
|
+
let envContent = '';
|
|
168
|
+
try {
|
|
169
|
+
envContent = await readFile(join(ctx.projectDir, '.env'), 'utf-8');
|
|
170
|
+
}
|
|
171
|
+
catch { /* no .env */ }
|
|
172
|
+
const envVars = envContent
|
|
173
|
+
.split('\n')
|
|
174
|
+
.filter(line => line.includes('=') && !line.startsWith('#'))
|
|
175
|
+
.map(line => {
|
|
176
|
+
const idx = line.indexOf('=');
|
|
177
|
+
return { key: line.slice(0, idx).trim(), value: line.slice(idx + 1).trim().replace(/^["']|["']$/g, '') };
|
|
178
|
+
})
|
|
179
|
+
.filter(v => v.key && !v.key.startsWith('VERCEL_')); // Don't set Vercel metadata as env vars
|
|
180
|
+
if (envVars.length > 0) {
|
|
181
|
+
const envBody = JSON.stringify(envVars.map(v => ({
|
|
182
|
+
key: v.key,
|
|
183
|
+
value: v.value,
|
|
184
|
+
type: 'encrypted',
|
|
185
|
+
target: ['production', 'preview', 'development'],
|
|
186
|
+
})));
|
|
187
|
+
const envRes = await httpsPost('api.vercel.com', `/v10/projects/${projectId}/env`, headers, envBody);
|
|
188
|
+
if (envRes.status === 200 || envRes.status === 201) {
|
|
189
|
+
emit({ step: 'vercel-envvars', status: 'done', message: `Set ${envVars.length} environment variables` });
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
emit({ step: 'vercel-envvars', status: 'error', message: 'Failed to set env vars', detail: `API returned ${envRes.status}` });
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
emit({ step: 'vercel-envvars', status: 'done', message: 'No environment variables to set' });
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
catch (err) {
|
|
200
|
+
emit({ step: 'vercel-envvars', status: 'error', message: 'Failed to set env vars', detail: err.message });
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
// Step 6: Poll for deployment (triggered by GitHub push, ADR-015)
|
|
204
|
+
if (projectId && ghOwner && ghRepo) {
|
|
205
|
+
emit({ step: 'vercel-deploy', status: 'started', message: 'Waiting for deployment (triggered by git push)...' });
|
|
206
|
+
try {
|
|
207
|
+
const start = Date.now();
|
|
208
|
+
let deployUrl = '';
|
|
209
|
+
while (Date.now() - start < DEPLOY_POLL_TIMEOUT_MS) {
|
|
210
|
+
await new Promise(r => setTimeout(r, DEPLOY_POLL_INTERVAL_MS));
|
|
211
|
+
if (ctx.abortSignal?.aborted)
|
|
212
|
+
break;
|
|
213
|
+
const depRes = await httpsGet('api.vercel.com', `/v6/deployments?projectId=${projectId}&limit=1`, headers);
|
|
214
|
+
if (depRes.status !== 200)
|
|
215
|
+
continue;
|
|
216
|
+
const depData = safeJsonParse(depRes.body);
|
|
217
|
+
const latest = depData?.deployments?.[0];
|
|
218
|
+
if (!latest)
|
|
219
|
+
continue;
|
|
220
|
+
if (latest.readyState === 'READY' || latest.state === 'READY') {
|
|
221
|
+
deployUrl = latest.url ? `https://${latest.url}` : '';
|
|
222
|
+
break;
|
|
223
|
+
}
|
|
224
|
+
if (latest.readyState === 'ERROR' || latest.state === 'ERROR') {
|
|
225
|
+
emit({ step: 'vercel-deploy', status: 'error', message: 'Deployment failed on Vercel', detail: 'Check the Vercel dashboard for build logs' });
|
|
226
|
+
break;
|
|
227
|
+
}
|
|
228
|
+
const elapsed = Math.round((Date.now() - start) / 1000);
|
|
229
|
+
if (elapsed % 15 === 0) {
|
|
230
|
+
emit({ step: 'vercel-deploy', status: 'started', message: `Deployment status: ${latest.readyState || latest.state || 'building'}... (${elapsed}s)` });
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
if (deployUrl) {
|
|
234
|
+
outputs['DEPLOY_URL'] = deployUrl;
|
|
235
|
+
emit({ step: 'vercel-deploy', status: 'done', message: `Live at ${deployUrl}` });
|
|
236
|
+
}
|
|
237
|
+
else if (!ctx.abortSignal?.aborted) {
|
|
238
|
+
emit({ step: 'vercel-deploy', status: 'error', message: 'Deployment polling timed out — check Vercel dashboard', detail: 'The deployment may still be building' });
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
catch (err) {
|
|
242
|
+
emit({ step: 'vercel-deploy', status: 'error', message: 'Failed to poll deployment', detail: err.message });
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
else if (!ghOwner || !ghRepo) {
|
|
246
|
+
emit({ step: 'vercel-deploy', status: 'skipped', message: 'No GitHub repo linked — deploy manually with: npx vercel deploy' });
|
|
247
|
+
}
|
|
248
|
+
// Step 7: Write .env
|
|
249
|
+
emit({ step: 'vercel-env', status: 'started', message: 'Writing Vercel config to .env' });
|
|
250
|
+
try {
|
|
251
|
+
const envLines = [
|
|
252
|
+
`# VoidForge Vercel — generated ${new Date().toISOString()}`,
|
|
253
|
+
`VERCEL_PROJECT_NAME=${outputs['VERCEL_PROJECT_NAME'] || slug}`,
|
|
254
|
+
];
|
|
255
|
+
if (outputs['VERCEL_PROJECT_ID']) {
|
|
256
|
+
envLines.push(`VERCEL_PROJECT_ID=${outputs['VERCEL_PROJECT_ID']}`);
|
|
257
|
+
}
|
|
258
|
+
if (outputs['DEPLOY_URL']) {
|
|
259
|
+
envLines.push(`DEPLOY_URL=${outputs['DEPLOY_URL']}`);
|
|
260
|
+
}
|
|
261
|
+
envLines.push('# Auto-deploys on push to main');
|
|
262
|
+
await appendEnvSection(ctx.projectDir, envLines);
|
|
263
|
+
emit({ step: 'vercel-env', status: 'done', message: 'Vercel config written to .env' });
|
|
264
|
+
}
|
|
265
|
+
catch (err) {
|
|
266
|
+
emit({ step: 'vercel-env', status: 'error', message: 'Failed to write .env', detail: err.message });
|
|
267
|
+
}
|
|
268
|
+
return { success: true, resources, outputs, files };
|
|
269
|
+
},
|
|
270
|
+
async cleanup(resources, credentials) {
|
|
271
|
+
const token = credentials['vercel-token'];
|
|
272
|
+
if (!token)
|
|
273
|
+
return;
|
|
274
|
+
for (const resource of resources) {
|
|
275
|
+
if (resource.type === 'vercel-project') {
|
|
276
|
+
try {
|
|
277
|
+
await httpsDelete('api.vercel.com', `/v9/projects/${resource.id}`, {
|
|
278
|
+
'Authorization': `Bearer ${token}`,
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
catch (err) {
|
|
282
|
+
console.error(`Failed to delete Vercel project ${resource.id}:`, err.message);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
},
|
|
287
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PTY Manager — spawns and manages real pseudo-terminal processes.
|
|
3
|
+
* Uses node-pty (same lib as VS Code, Gitpod, GitHub Codespaces).
|
|
4
|
+
* Each PTY is a real shell with full capabilities.
|
|
5
|
+
*
|
|
6
|
+
* Haku moves between worlds seamlessly.
|
|
7
|
+
*/
|
|
8
|
+
export interface PtySession {
|
|
9
|
+
id: string;
|
|
10
|
+
projectName: string;
|
|
11
|
+
projectDir: string;
|
|
12
|
+
label: string;
|
|
13
|
+
username: string;
|
|
14
|
+
createdAt: number;
|
|
15
|
+
lastActivityAt: number;
|
|
16
|
+
cols: number;
|
|
17
|
+
rows: number;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Spawn a new PTY session.
|
|
21
|
+
* @param projectDir — directory to cd into
|
|
22
|
+
* @param projectName — human-readable project name
|
|
23
|
+
* @param label — tab label (e.g., "Claude Code", "Shell", "SSH: prod")
|
|
24
|
+
* @param initialCommand — optional command to auto-run after shell starts
|
|
25
|
+
* @param cols — terminal columns (default 120)
|
|
26
|
+
* @param rows — terminal rows (default 30)
|
|
27
|
+
*/
|
|
28
|
+
export declare function createSession(projectDir: string, projectName: string, label: string, initialCommand?: string, cols?: number, rows?: number, username?: string): Promise<PtySession>;
|
|
29
|
+
/** Write input (keystrokes) to a PTY session. */
|
|
30
|
+
export declare function writeToSession(sessionId: string, data: string): void;
|
|
31
|
+
/** Subscribe to PTY output. Returns an unsubscribe function. */
|
|
32
|
+
export declare function onSessionData(sessionId: string, listener: (data: string) => void): () => void;
|
|
33
|
+
/** Resize a PTY session. */
|
|
34
|
+
export declare function resizeSession(sessionId: string, cols: number, rows: number): void;
|
|
35
|
+
/** Kill a PTY session. */
|
|
36
|
+
export declare function killSession(sessionId: string): void;
|
|
37
|
+
/** List all active sessions. */
|
|
38
|
+
export declare function listSessions(): PtySession[];
|
|
39
|
+
/** Kill all sessions (graceful shutdown). */
|
|
40
|
+
export declare function killAllSessions(): void;
|
|
41
|
+
/** Get session count. */
|
|
42
|
+
export declare function sessionCount(): number;
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PTY Manager — spawns and manages real pseudo-terminal processes.
|
|
3
|
+
* Uses node-pty (same lib as VS Code, Gitpod, GitHub Codespaces).
|
|
4
|
+
* Each PTY is a real shell with full capabilities.
|
|
5
|
+
*
|
|
6
|
+
* Haku moves between worlds seamlessly.
|
|
7
|
+
*/
|
|
8
|
+
import { randomUUID } from 'node:crypto';
|
|
9
|
+
import { isRemoteMode } from './tower-auth.js';
|
|
10
|
+
import { audit } from './audit-log.js';
|
|
11
|
+
// node-pty is a native module — dynamic import to handle missing installs gracefully
|
|
12
|
+
let pty = null;
|
|
13
|
+
async function loadPty() {
|
|
14
|
+
if (pty)
|
|
15
|
+
return pty;
|
|
16
|
+
try {
|
|
17
|
+
pty = await import('node-pty');
|
|
18
|
+
return pty;
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
throw new Error('node-pty is not installed. Run: npm install node-pty');
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
const sessions = new Map();
|
|
25
|
+
const MAX_SESSIONS_LOCAL = 5;
|
|
26
|
+
const MAX_SESSIONS_REMOTE = 20; // 5 per project, 20 total across all projects
|
|
27
|
+
const IDLE_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes
|
|
28
|
+
// SEC-004/QA-003: Whitelist of allowed initial commands — prevent arbitrary command injection
|
|
29
|
+
const ALLOWED_INITIAL_COMMANDS = ['claude', 'claude --dangerously-skip-permissions', 'bash', 'zsh', 'sh', 'npm run dev', 'npm start', 'npm test'];
|
|
30
|
+
// SEC-013: Safe environment keys — no credential leakage into PTY sessions
|
|
31
|
+
// ANTHROPIC_API_KEY included only in local mode (user's own key).
|
|
32
|
+
// In remote mode, operator's API key must NOT leak to deployer-role users.
|
|
33
|
+
// Includes TMPDIR/SSH_AUTH_SOCK for tool compatibility.
|
|
34
|
+
const BASE_SAFE_ENV_KEYS = ['PATH', 'HOME', 'SHELL', 'USER', 'LANG', 'LC_ALL', 'LC_CTYPE', 'TERM_PROGRAM', 'EDITOR', 'VISUAL', 'XDG_CONFIG_HOME', 'XDG_DATA_HOME', 'NVM_DIR', 'NVM_BIN', 'NVM_INC', 'TMPDIR', 'TEMP', 'SSH_AUTH_SOCK', 'COLORTERM'];
|
|
35
|
+
// FLOW-R2-007: Only pass ANTHROPIC_API_KEY in local mode
|
|
36
|
+
function getSafeEnvKeys() {
|
|
37
|
+
if (isRemoteMode())
|
|
38
|
+
return BASE_SAFE_ENV_KEYS;
|
|
39
|
+
return [...BASE_SAFE_ENV_KEYS, 'ANTHROPIC_API_KEY'];
|
|
40
|
+
}
|
|
41
|
+
function resetIdleTimer(session) {
|
|
42
|
+
if (session.idleTimer)
|
|
43
|
+
clearTimeout(session.idleTimer);
|
|
44
|
+
session.idleTimer = setTimeout(() => {
|
|
45
|
+
console.log(` PTY session ${session.id} idle for 30 min — killing`);
|
|
46
|
+
killSession(session.id);
|
|
47
|
+
}, IDLE_TIMEOUT_MS);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Spawn a new PTY session.
|
|
51
|
+
* @param projectDir — directory to cd into
|
|
52
|
+
* @param projectName — human-readable project name
|
|
53
|
+
* @param label — tab label (e.g., "Claude Code", "Shell", "SSH: prod")
|
|
54
|
+
* @param initialCommand — optional command to auto-run after shell starts
|
|
55
|
+
* @param cols — terminal columns (default 120)
|
|
56
|
+
* @param rows — terminal rows (default 30)
|
|
57
|
+
*/
|
|
58
|
+
export async function createSession(projectDir, projectName, label, initialCommand, cols = 120, rows = 30, username = '') {
|
|
59
|
+
const maxSessions = isRemoteMode() ? MAX_SESSIONS_REMOTE : MAX_SESSIONS_LOCAL;
|
|
60
|
+
if (sessions.size >= maxSessions) {
|
|
61
|
+
// QA-007/UX-018: Prefer killing sessions with no connected listeners (disconnected tabs)
|
|
62
|
+
const disconnected = [...sessions.values()].filter(s => s.onData.size === 0);
|
|
63
|
+
if (disconnected.length > 0) {
|
|
64
|
+
killSession(disconnected.sort((a, b) => a.lastActivityAt - b.lastActivityAt)[0].id);
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
// All sessions have active listeners — reject instead of killing active work
|
|
68
|
+
throw new Error(`Maximum ${maxSessions} concurrent terminal sessions. Close a tab first.`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
const nodePty = await loadPty();
|
|
72
|
+
const shell = process.env['SHELL'] || '/bin/zsh';
|
|
73
|
+
const id = randomUUID();
|
|
74
|
+
// SEC-013: Build clean environment — no credential leakage into PTY
|
|
75
|
+
const safeEnv = {};
|
|
76
|
+
for (const key of getSafeEnvKeys()) {
|
|
77
|
+
if (process.env[key])
|
|
78
|
+
safeEnv[key] = process.env[key];
|
|
79
|
+
}
|
|
80
|
+
safeEnv['TERM'] = 'xterm-256color';
|
|
81
|
+
safeEnv['VOIDFORGE_SESSION'] = id;
|
|
82
|
+
// QA-R2-010 + QA-R3-002: Clamp cols/rows BEFORE spawnOptions construction
|
|
83
|
+
cols = Math.max(1, Math.min(500, Math.floor(cols)));
|
|
84
|
+
rows = Math.max(1, Math.min(200, Math.floor(rows)));
|
|
85
|
+
// Remote mode: spawn as forge-user for sandboxing (Layer 4)
|
|
86
|
+
const spawnOptions = {
|
|
87
|
+
name: 'xterm-256color',
|
|
88
|
+
cols,
|
|
89
|
+
rows,
|
|
90
|
+
cwd: projectDir,
|
|
91
|
+
env: safeEnv,
|
|
92
|
+
};
|
|
93
|
+
if (isRemoteMode()) {
|
|
94
|
+
// In remote mode, PTY spawns as forge-user (non-root sandboxing)
|
|
95
|
+
// The uid/gid would be set here in production: spawnOptions.uid = forgeUserUid;
|
|
96
|
+
// For scaffold/spec purposes, we document the intent
|
|
97
|
+
safeEnv['VOIDFORGE_REMOTE'] = '1';
|
|
98
|
+
}
|
|
99
|
+
const ptyProcess = nodePty.spawn(shell, [], spawnOptions);
|
|
100
|
+
const session = {
|
|
101
|
+
id,
|
|
102
|
+
projectName,
|
|
103
|
+
projectDir,
|
|
104
|
+
label,
|
|
105
|
+
username,
|
|
106
|
+
createdAt: Date.now(),
|
|
107
|
+
lastActivityAt: Date.now(),
|
|
108
|
+
cols,
|
|
109
|
+
rows,
|
|
110
|
+
process: ptyProcess,
|
|
111
|
+
onData: new Set(),
|
|
112
|
+
idleTimer: null,
|
|
113
|
+
};
|
|
114
|
+
// Forward PTY output to all listeners
|
|
115
|
+
ptyProcess.onData((data) => {
|
|
116
|
+
session.lastActivityAt = Date.now();
|
|
117
|
+
resetIdleTimer(session);
|
|
118
|
+
for (const listener of session.onData) {
|
|
119
|
+
try {
|
|
120
|
+
listener(data);
|
|
121
|
+
}
|
|
122
|
+
catch { /* listener error, ignore */ }
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
// Handle PTY exit
|
|
126
|
+
ptyProcess.onExit(({ exitCode }) => {
|
|
127
|
+
console.log(` PTY session ${id} exited (code ${exitCode})`);
|
|
128
|
+
if (session.idleTimer)
|
|
129
|
+
clearTimeout(session.idleTimer);
|
|
130
|
+
sessions.delete(id);
|
|
131
|
+
// Audit: log terminal end in remote mode
|
|
132
|
+
if (isRemoteMode()) {
|
|
133
|
+
audit('terminal_end', '', session.username, { sessionId: id, exitCode }).catch(() => { });
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
sessions.set(id, session);
|
|
137
|
+
resetIdleTimer(session);
|
|
138
|
+
// Audit: log terminal start in remote mode
|
|
139
|
+
if (isRemoteMode()) {
|
|
140
|
+
audit('terminal_start', '', username, { sessionId: id, project: projectName, label }).catch(() => { });
|
|
141
|
+
}
|
|
142
|
+
// SEC-004/QA-003: Validate initial command against whitelist
|
|
143
|
+
if (initialCommand && !ALLOWED_INITIAL_COMMANDS.includes(initialCommand)) {
|
|
144
|
+
initialCommand = undefined;
|
|
145
|
+
}
|
|
146
|
+
// Auto-run initial command after a short delay (let shell init complete)
|
|
147
|
+
if (initialCommand) {
|
|
148
|
+
setTimeout(() => {
|
|
149
|
+
if (sessions.has(id)) {
|
|
150
|
+
ptyProcess.write(initialCommand + '\r');
|
|
151
|
+
}
|
|
152
|
+
}, 500);
|
|
153
|
+
}
|
|
154
|
+
return {
|
|
155
|
+
id: session.id,
|
|
156
|
+
projectName: session.projectName,
|
|
157
|
+
projectDir: session.projectDir,
|
|
158
|
+
label: session.label,
|
|
159
|
+
username: session.username,
|
|
160
|
+
createdAt: session.createdAt,
|
|
161
|
+
lastActivityAt: session.lastActivityAt,
|
|
162
|
+
cols: session.cols,
|
|
163
|
+
rows: session.rows,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
/** Write input (keystrokes) to a PTY session. */
|
|
167
|
+
export function writeToSession(sessionId, data) {
|
|
168
|
+
const session = sessions.get(sessionId);
|
|
169
|
+
if (!session)
|
|
170
|
+
throw new Error(`Session not found: ${sessionId}`);
|
|
171
|
+
session.lastActivityAt = Date.now();
|
|
172
|
+
resetIdleTimer(session);
|
|
173
|
+
session.process.write(data);
|
|
174
|
+
}
|
|
175
|
+
/** Subscribe to PTY output. Returns an unsubscribe function. */
|
|
176
|
+
export function onSessionData(sessionId, listener) {
|
|
177
|
+
const session = sessions.get(sessionId);
|
|
178
|
+
if (!session)
|
|
179
|
+
throw new Error(`Session not found: ${sessionId}`);
|
|
180
|
+
session.onData.add(listener);
|
|
181
|
+
return () => { session.onData.delete(listener); };
|
|
182
|
+
}
|
|
183
|
+
/** Resize a PTY session. */
|
|
184
|
+
export function resizeSession(sessionId, cols, rows) {
|
|
185
|
+
const session = sessions.get(sessionId);
|
|
186
|
+
if (!session)
|
|
187
|
+
return;
|
|
188
|
+
// QA-016/SEC-007: Clamp resize values to sane bounds
|
|
189
|
+
cols = Math.max(1, Math.min(500, Math.floor(cols)));
|
|
190
|
+
rows = Math.max(1, Math.min(200, Math.floor(rows)));
|
|
191
|
+
session.cols = cols;
|
|
192
|
+
session.rows = rows;
|
|
193
|
+
session.process.resize(cols, rows);
|
|
194
|
+
}
|
|
195
|
+
/** Kill a PTY session. */
|
|
196
|
+
export function killSession(sessionId) {
|
|
197
|
+
const session = sessions.get(sessionId);
|
|
198
|
+
if (!session)
|
|
199
|
+
return;
|
|
200
|
+
if (session.idleTimer)
|
|
201
|
+
clearTimeout(session.idleTimer);
|
|
202
|
+
try {
|
|
203
|
+
session.process.kill();
|
|
204
|
+
}
|
|
205
|
+
catch { /* already dead */ }
|
|
206
|
+
sessions.delete(sessionId);
|
|
207
|
+
}
|
|
208
|
+
/** List all active sessions. */
|
|
209
|
+
export function listSessions() {
|
|
210
|
+
return [...sessions.values()].map((s) => ({
|
|
211
|
+
id: s.id,
|
|
212
|
+
projectName: s.projectName,
|
|
213
|
+
projectDir: s.projectDir,
|
|
214
|
+
label: s.label,
|
|
215
|
+
username: s.username,
|
|
216
|
+
createdAt: s.createdAt,
|
|
217
|
+
lastActivityAt: s.lastActivityAt,
|
|
218
|
+
cols: s.cols,
|
|
219
|
+
rows: s.rows,
|
|
220
|
+
}));
|
|
221
|
+
}
|
|
222
|
+
/** Kill all sessions (graceful shutdown). */
|
|
223
|
+
export function killAllSessions() {
|
|
224
|
+
for (const id of sessions.keys()) {
|
|
225
|
+
killSession(id);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
/** Get session count. */
|
|
229
|
+
export function sessionCount() {
|
|
230
|
+
return sessions.size;
|
|
231
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rate limiter core — re-exports from the outbound-rate-limiter pattern for wizard runtime use.
|
|
3
|
+
* ARCH-R2-012: Production code should not import from docs/patterns/ directly.
|
|
4
|
+
*/
|
|
5
|
+
export { OutboundRateLimiter, getLimiter } from './patterns/outbound-rate-limiter.js';
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rate limiter core — re-exports from the outbound-rate-limiter pattern for wizard runtime use.
|
|
3
|
+
* ARCH-R2-012: Production code should not import from docs/patterns/ directly.
|
|
4
|
+
*/
|
|
5
|
+
export { OutboundRateLimiter, getLimiter } from './patterns/outbound-rate-limiter.js';
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reconciliation Engine — Two-pass financial reconciliation (§9.17).
|
|
3
|
+
*
|
|
4
|
+
* Compares VoidForge's recorded spend/revenue against platform-reported values.
|
|
5
|
+
* Runs daily: preliminary at midnight UTC, authoritative at 06:00 UTC.
|
|
6
|
+
*
|
|
7
|
+
* PRD Reference: §9.4 (reconciliation), §9.9 (ReconciliationReport), §9.17 (two-pass)
|
|
8
|
+
*/
|
|
9
|
+
type Cents = number & {
|
|
10
|
+
readonly __brand: 'Cents';
|
|
11
|
+
};
|
|
12
|
+
type Ratio = number & {
|
|
13
|
+
readonly __brand: 'Ratio';
|
|
14
|
+
};
|
|
15
|
+
type AdPlatform = 'meta' | 'google' | 'tiktok' | 'linkedin' | 'twitter' | 'reddit';
|
|
16
|
+
type RevenueSource = 'stripe' | 'paddle';
|
|
17
|
+
interface ReconciliationReport {
|
|
18
|
+
id: string;
|
|
19
|
+
date: string;
|
|
20
|
+
type: 'preliminary' | 'final';
|
|
21
|
+
projectId: string;
|
|
22
|
+
spend: Array<{
|
|
23
|
+
platform: AdPlatform;
|
|
24
|
+
voidforgeRecorded: Cents;
|
|
25
|
+
platformReported: Cents;
|
|
26
|
+
discrepancy: Cents;
|
|
27
|
+
status: 'matched' | 'discrepancy' | 'unavailable';
|
|
28
|
+
}>;
|
|
29
|
+
revenue: Array<{
|
|
30
|
+
source: RevenueSource;
|
|
31
|
+
recorded: Cents;
|
|
32
|
+
reported: Cents;
|
|
33
|
+
discrepancy: Cents;
|
|
34
|
+
status: 'matched' | 'discrepancy' | 'unavailable';
|
|
35
|
+
}>;
|
|
36
|
+
netPosition: Cents;
|
|
37
|
+
blendedRoas: Ratio;
|
|
38
|
+
alerts: string[];
|
|
39
|
+
}
|
|
40
|
+
export declare function runReconciliation(projectId: string, date: string, type: 'preliminary' | 'final', platformSpendReports: Map<string, Cents>, // Platform-reported spend per platform
|
|
41
|
+
revenueSourceReports: Map<string, Cents>): Promise<ReconciliationReport>;
|
|
42
|
+
export declare function enforceCurrency(currency: string, platform: string): void;
|
|
43
|
+
export type { ReconciliationReport };
|