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,648 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Projects API — Multi-project CRUD for The Lobby.
|
|
3
|
+
* Endpoints: list, get, import, delete, access management.
|
|
4
|
+
* All queries filtered by per-project access control (v7.0).
|
|
5
|
+
*/
|
|
6
|
+
import { access as fsAccess, readFile, realpath } from 'node:fs/promises';
|
|
7
|
+
import { join, resolve } from 'node:path';
|
|
8
|
+
import { addRoute } from '../router.js';
|
|
9
|
+
import { parseJsonBody } from '../lib/body-parser.js';
|
|
10
|
+
import { parseFrontmatter } from '../lib/frontmatter.js';
|
|
11
|
+
import { addProject, getProject, removeProject, findByDirectory, getProjectsForUser, grantAccess, revokeAccess, getProjectAccess, checkProjectAccess, linkProjects, unlinkProjects, getLinkedGroup, } from '../lib/project-registry.js';
|
|
12
|
+
import { getDeployPlan } from '../lib/deploy-coordinator.js';
|
|
13
|
+
import { getAggregateCosts } from '../lib/cost-tracker.js';
|
|
14
|
+
import { addLesson, getLessons, getLessonCount } from '../lib/agent-memory.js';
|
|
15
|
+
import { audit } from '../lib/audit-log.js';
|
|
16
|
+
import { validateSession, parseSessionCookie, getClientIp, isRemoteMode } from '../lib/tower-auth.js';
|
|
17
|
+
import { isValidRole, hasRole } from '../lib/user-manager.js';
|
|
18
|
+
import { sendJson } from '../lib/http-helpers.js';
|
|
19
|
+
/** Extract session from request. Returns null if not authenticated (local mode returns synthetic admin). */
|
|
20
|
+
function getSession(req) {
|
|
21
|
+
if (!isRemoteMode()) {
|
|
22
|
+
return { username: 'local', role: 'admin' };
|
|
23
|
+
}
|
|
24
|
+
const token = parseSessionCookie(req.headers.cookie);
|
|
25
|
+
const ip = getClientIp(req);
|
|
26
|
+
if (!token)
|
|
27
|
+
return null;
|
|
28
|
+
return validateSession(token, ip);
|
|
29
|
+
}
|
|
30
|
+
// GET /api/projects — list projects visible to the current user
|
|
31
|
+
addRoute('GET', '/api/projects', async (req, res) => {
|
|
32
|
+
const session = getSession(req);
|
|
33
|
+
if (!session) {
|
|
34
|
+
sendJson(res, 401, { success: false, error: 'Authentication required' });
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const projects = await getProjectsForUser(session.username, session.role);
|
|
38
|
+
// Annotate each project with the user's effective role for UI rendering
|
|
39
|
+
const annotated = projects.map((p) => {
|
|
40
|
+
let userRole = 'viewer';
|
|
41
|
+
if (session.role === 'admin' || p.owner === session.username) {
|
|
42
|
+
userRole = 'owner';
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
const entry = p.access.find((a) => a.username === session.username);
|
|
46
|
+
if (entry)
|
|
47
|
+
userRole = entry.role;
|
|
48
|
+
}
|
|
49
|
+
return { ...p, userRole };
|
|
50
|
+
});
|
|
51
|
+
sendJson(res, 200, { success: true, data: annotated });
|
|
52
|
+
});
|
|
53
|
+
// GET /api/projects/get — get single project by id (filtered by access)
|
|
54
|
+
addRoute('GET', '/api/projects/get', async (req, res) => {
|
|
55
|
+
const session = getSession(req);
|
|
56
|
+
if (!session) {
|
|
57
|
+
sendJson(res, 401, { success: false, error: 'Authentication required' });
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const url = new URL(req.url ?? '/', `http://${req.headers.host ?? 'localhost'}`);
|
|
61
|
+
const id = url.searchParams.get('id');
|
|
62
|
+
if (!id) {
|
|
63
|
+
sendJson(res, 400, { success: false, error: 'id query parameter is required' });
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
// Access check — returns null if project doesn't exist OR user has no access
|
|
67
|
+
const effectiveRole = await checkProjectAccess(id, session.username, session.role);
|
|
68
|
+
if (!effectiveRole) {
|
|
69
|
+
sendJson(res, 404, { success: false, error: 'Project not found' });
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const project = await getProject(id);
|
|
73
|
+
if (!project) {
|
|
74
|
+
sendJson(res, 404, { success: false, error: 'Project not found' });
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
sendJson(res, 200, { success: true, data: { ...project, userRole: effectiveRole } });
|
|
78
|
+
});
|
|
79
|
+
/** Scan a project directory for metadata. Reuses patterns from wizard/api/deploy.ts. */
|
|
80
|
+
async function scanProjectMetadata(dir) {
|
|
81
|
+
let name = 'Unknown';
|
|
82
|
+
try {
|
|
83
|
+
const claudeMd = await readFile(join(dir, 'CLAUDE.md'), 'utf-8');
|
|
84
|
+
const nameMatch = claudeMd.match(/\*\*Name:\*\*\s*(.+)/);
|
|
85
|
+
if (nameMatch) {
|
|
86
|
+
const extracted = nameMatch[1].trim();
|
|
87
|
+
if (!extracted.startsWith('['))
|
|
88
|
+
name = extracted;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
catch { /* use default */ }
|
|
92
|
+
let deployTarget = '';
|
|
93
|
+
let deployUrl = '';
|
|
94
|
+
let hostname = '';
|
|
95
|
+
let sshHost = '';
|
|
96
|
+
try {
|
|
97
|
+
const envContent = await readFile(join(dir, '.env'), 'utf-8');
|
|
98
|
+
const deployMatch = envContent.match(/DEPLOY_TARGET=(.+)/);
|
|
99
|
+
if (deployMatch) {
|
|
100
|
+
deployTarget = deployMatch[1].trim().replace(/^["']|["']$/g, '').split('#')[0].trim();
|
|
101
|
+
}
|
|
102
|
+
const hostnameMatch = envContent.match(/HOSTNAME=(.+)/);
|
|
103
|
+
if (hostnameMatch) {
|
|
104
|
+
hostname = hostnameMatch[1].trim().replace(/^["']|["']$/g, '').split('#')[0].trim();
|
|
105
|
+
if (hostname)
|
|
106
|
+
deployUrl = `https://${hostname}`;
|
|
107
|
+
}
|
|
108
|
+
if (deployTarget === 'vps') {
|
|
109
|
+
const sshMatch = envContent.match(/SSH_HOST=(.+)/);
|
|
110
|
+
if (sshMatch) {
|
|
111
|
+
sshHost = sshMatch[1].trim().replace(/^["']|["']$/g, '').split('#')[0].trim();
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
catch { /* no .env */ }
|
|
116
|
+
let framework = '';
|
|
117
|
+
let database = 'none';
|
|
118
|
+
try {
|
|
119
|
+
const prd = await readFile(join(dir, 'docs', 'PRD.md'), 'utf-8');
|
|
120
|
+
const { frontmatter } = parseFrontmatter(prd);
|
|
121
|
+
if (frontmatter.framework)
|
|
122
|
+
framework = frontmatter.framework;
|
|
123
|
+
if (frontmatter.database)
|
|
124
|
+
database = frontmatter.database;
|
|
125
|
+
if (frontmatter.deploy && !deployTarget)
|
|
126
|
+
deployTarget = frontmatter.deploy;
|
|
127
|
+
if (frontmatter.hostname && !hostname) {
|
|
128
|
+
hostname = frontmatter.hostname;
|
|
129
|
+
if (!deployUrl && hostname)
|
|
130
|
+
deployUrl = `https://${hostname}`;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
catch { /* no PRD or no frontmatter */ }
|
|
134
|
+
if (!framework) {
|
|
135
|
+
try {
|
|
136
|
+
const pkg = await readFile(join(dir, 'package.json'), 'utf-8');
|
|
137
|
+
const pkgData = JSON.parse(pkg);
|
|
138
|
+
const deps = pkgData.dependencies ?? {};
|
|
139
|
+
if (deps['next'])
|
|
140
|
+
framework = 'next.js';
|
|
141
|
+
else if (deps['express'])
|
|
142
|
+
framework = 'express';
|
|
143
|
+
}
|
|
144
|
+
catch { /* not a Node project */ }
|
|
145
|
+
if (!framework) {
|
|
146
|
+
try {
|
|
147
|
+
const reqs = await readFile(join(dir, 'requirements.txt'), 'utf-8');
|
|
148
|
+
if (reqs.toLowerCase().includes('django'))
|
|
149
|
+
framework = 'django';
|
|
150
|
+
else
|
|
151
|
+
framework = 'python';
|
|
152
|
+
}
|
|
153
|
+
catch { /* not Python */ }
|
|
154
|
+
}
|
|
155
|
+
if (!framework) {
|
|
156
|
+
try {
|
|
157
|
+
await fsAccess(join(dir, 'Gemfile'));
|
|
158
|
+
framework = 'rails';
|
|
159
|
+
}
|
|
160
|
+
catch { /* not Ruby */ }
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
let lastBuildPhase = 0;
|
|
164
|
+
try {
|
|
165
|
+
const buildState = await readFile(join(dir, 'logs', 'build-state.md'), 'utf-8');
|
|
166
|
+
const phaseMatch = buildState.match(/\*\*Current Phase:\*\*\s*(\d+)/);
|
|
167
|
+
if (phaseMatch) {
|
|
168
|
+
lastBuildPhase = parseInt(phaseMatch[1], 10);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
catch { /* no build state */ }
|
|
172
|
+
let healthCheckUrl = '';
|
|
173
|
+
if (deployUrl) {
|
|
174
|
+
healthCheckUrl = `${deployUrl}/api/health`;
|
|
175
|
+
}
|
|
176
|
+
return {
|
|
177
|
+
name,
|
|
178
|
+
directory: dir,
|
|
179
|
+
deployTarget: deployTarget || 'unknown',
|
|
180
|
+
deployUrl,
|
|
181
|
+
sshHost,
|
|
182
|
+
framework: framework || 'unknown',
|
|
183
|
+
database,
|
|
184
|
+
createdAt: new Date().toISOString(),
|
|
185
|
+
lastBuildPhase,
|
|
186
|
+
lastDeployAt: '',
|
|
187
|
+
healthCheckUrl,
|
|
188
|
+
monthlyCost: 0,
|
|
189
|
+
owner: '',
|
|
190
|
+
access: [],
|
|
191
|
+
linkedProjects: [],
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
// POST /api/projects/import — import an existing project (sets owner to current user)
|
|
195
|
+
addRoute('POST', '/api/projects/import', async (req, res) => {
|
|
196
|
+
const session = getSession(req);
|
|
197
|
+
if (!session) {
|
|
198
|
+
sendJson(res, 401, { success: false, error: 'Authentication required' });
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
const body = await parseJsonBody(req);
|
|
202
|
+
if (typeof body !== 'object' || body === null) {
|
|
203
|
+
sendJson(res, 400, { success: false, error: 'Request body must be a JSON object' });
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
const { directory } = body;
|
|
207
|
+
if (typeof directory !== 'string' || directory.trim().length === 0) {
|
|
208
|
+
sendJson(res, 400, { success: false, error: 'directory must be a non-empty string' });
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
if (directory.includes('..')) {
|
|
212
|
+
sendJson(res, 400, { success: false, error: 'directory must not contain ".." segments' });
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
let dir = resolve(directory);
|
|
216
|
+
if (!dir.startsWith('/')) {
|
|
217
|
+
sendJson(res, 400, { success: false, error: 'directory must be an absolute path' });
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
try {
|
|
221
|
+
await fsAccess(dir);
|
|
222
|
+
}
|
|
223
|
+
catch {
|
|
224
|
+
sendJson(res, 400, { success: false, error: 'Directory does not exist' });
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
// CROSS-R4-005 + IG-R4: Resolve symlinks and use the real path for all subsequent operations.
|
|
228
|
+
// This handles macOS /tmp → /private/tmp while ensuring we operate on the true filesystem location.
|
|
229
|
+
try {
|
|
230
|
+
dir = await realpath(dir);
|
|
231
|
+
}
|
|
232
|
+
catch {
|
|
233
|
+
sendJson(res, 400, { success: false, error: 'Could not resolve directory path' });
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
try {
|
|
237
|
+
await fsAccess(join(dir, 'CLAUDE.md'));
|
|
238
|
+
}
|
|
239
|
+
catch {
|
|
240
|
+
sendJson(res, 400, { success: false, error: 'Not a VoidForge project — no CLAUDE.md found' });
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
const existing = await findByDirectory(dir);
|
|
244
|
+
if (existing) {
|
|
245
|
+
sendJson(res, 409, { success: false, error: 'Project already registered', data: existing });
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
try {
|
|
249
|
+
const input = await scanProjectMetadata(dir);
|
|
250
|
+
input.owner = session.username; // Set owner to importing user
|
|
251
|
+
const project = await addProject(input);
|
|
252
|
+
const ip = getClientIp(req);
|
|
253
|
+
await audit('project_create', ip, session.username, { action: 'import', directory: dir, name: project.name });
|
|
254
|
+
sendJson(res, 201, { success: true, data: { ...project, userRole: 'owner' } });
|
|
255
|
+
}
|
|
256
|
+
catch (err) {
|
|
257
|
+
const message = err instanceof Error ? err.message : 'Import failed';
|
|
258
|
+
if (message.includes('already registered')) {
|
|
259
|
+
sendJson(res, 409, { success: false, error: 'Project already registered' });
|
|
260
|
+
}
|
|
261
|
+
else {
|
|
262
|
+
sendJson(res, 500, { success: false, error: 'Failed to import project' });
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
// POST /api/projects/delete — remove a project (owner or admin only)
|
|
267
|
+
addRoute('POST', '/api/projects/delete', async (req, res) => {
|
|
268
|
+
const session = getSession(req);
|
|
269
|
+
if (!session) {
|
|
270
|
+
sendJson(res, 401, { success: false, error: 'Authentication required' });
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
const body = await parseJsonBody(req);
|
|
274
|
+
if (typeof body !== 'object' || body === null) {
|
|
275
|
+
sendJson(res, 400, { success: false, error: 'Request body must be a JSON object' });
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
const { id } = body;
|
|
279
|
+
if (typeof id !== 'string' || id.trim().length === 0) {
|
|
280
|
+
sendJson(res, 400, { success: false, error: 'id must be a non-empty string' });
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
// Check access — only owner or admin can delete
|
|
284
|
+
const effectiveRole = await checkProjectAccess(id, session.username, session.role);
|
|
285
|
+
if (effectiveRole !== 'admin') {
|
|
286
|
+
sendJson(res, 404, { success: false, error: 'Project not found' });
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
const removed = await removeProject(id);
|
|
290
|
+
if (!removed) {
|
|
291
|
+
sendJson(res, 404, { success: false, error: 'Project not found' });
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
const ip = getClientIp(req);
|
|
295
|
+
await audit('project_delete', ip, session.username, { projectId: id });
|
|
296
|
+
sendJson(res, 200, { success: true });
|
|
297
|
+
});
|
|
298
|
+
// ── Access management endpoints ─────────────────────
|
|
299
|
+
// GET /api/projects/access — get access list for a project (owner or admin)
|
|
300
|
+
addRoute('GET', '/api/projects/access', async (req, res) => {
|
|
301
|
+
const session = getSession(req);
|
|
302
|
+
if (!session) {
|
|
303
|
+
sendJson(res, 401, { success: false, error: 'Authentication required' });
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
const url = new URL(req.url ?? '/', `http://${req.headers.host ?? 'localhost'}`);
|
|
307
|
+
const id = url.searchParams.get('id');
|
|
308
|
+
if (!id) {
|
|
309
|
+
sendJson(res, 400, { success: false, error: 'id query parameter is required' });
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
// Only owner or admin can view access list
|
|
313
|
+
const effectiveRole = await checkProjectAccess(id, session.username, session.role);
|
|
314
|
+
if (effectiveRole !== 'admin') {
|
|
315
|
+
sendJson(res, 404, { success: false, error: 'Project not found' });
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
const accessInfo = await getProjectAccess(id);
|
|
319
|
+
if (!accessInfo) {
|
|
320
|
+
sendJson(res, 404, { success: false, error: 'Project not found' });
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
sendJson(res, 200, { success: true, data: accessInfo });
|
|
324
|
+
});
|
|
325
|
+
// POST /api/projects/access/grant — grant access (owner or admin only)
|
|
326
|
+
addRoute('POST', '/api/projects/access/grant', async (req, res) => {
|
|
327
|
+
const session = getSession(req);
|
|
328
|
+
if (!session) {
|
|
329
|
+
sendJson(res, 401, { success: false, error: 'Authentication required' });
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
const body = await parseJsonBody(req);
|
|
333
|
+
if (typeof body !== 'object' || body === null) {
|
|
334
|
+
sendJson(res, 400, { success: false, error: 'Request body must be a JSON object' });
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
const { projectId, username, role } = body;
|
|
338
|
+
if (typeof projectId !== 'string' || projectId.trim().length === 0) {
|
|
339
|
+
sendJson(res, 400, { success: false, error: 'projectId is required' });
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
if (typeof username !== 'string' || username.trim().length === 0) {
|
|
343
|
+
sendJson(res, 400, { success: false, error: 'username is required' });
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
if (typeof role !== 'string' || !isValidRole(role) || role === 'admin') {
|
|
347
|
+
sendJson(res, 400, { success: false, error: 'role must be one of: deployer, viewer' });
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
// Only owner or admin can grant access
|
|
351
|
+
const effectiveRole = await checkProjectAccess(projectId, session.username, session.role);
|
|
352
|
+
if (effectiveRole !== 'admin') {
|
|
353
|
+
sendJson(res, 404, { success: false, error: 'Project not found' });
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
const ip = getClientIp(req);
|
|
357
|
+
try {
|
|
358
|
+
await grantAccess(projectId, username.trim(), role);
|
|
359
|
+
await audit('access_grant', ip, session.username, {
|
|
360
|
+
projectId,
|
|
361
|
+
target: username.trim(),
|
|
362
|
+
grantedRole: role,
|
|
363
|
+
});
|
|
364
|
+
sendJson(res, 200, { success: true });
|
|
365
|
+
}
|
|
366
|
+
catch (err) {
|
|
367
|
+
const message = err instanceof Error ? err.message : 'Failed to grant access';
|
|
368
|
+
if (message === 'Project not found') {
|
|
369
|
+
sendJson(res, 404, { success: false, error: 'Project not found' });
|
|
370
|
+
}
|
|
371
|
+
else {
|
|
372
|
+
sendJson(res, 400, { success: false, error: 'Failed to grant access' });
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
});
|
|
376
|
+
// POST /api/projects/access/revoke — revoke access (owner or admin only)
|
|
377
|
+
addRoute('POST', '/api/projects/access/revoke', async (req, res) => {
|
|
378
|
+
const session = getSession(req);
|
|
379
|
+
if (!session) {
|
|
380
|
+
sendJson(res, 401, { success: false, error: 'Authentication required' });
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
const body = await parseJsonBody(req);
|
|
384
|
+
if (typeof body !== 'object' || body === null) {
|
|
385
|
+
sendJson(res, 400, { success: false, error: 'Request body must be a JSON object' });
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
const { projectId, username } = body;
|
|
389
|
+
if (typeof projectId !== 'string' || projectId.trim().length === 0) {
|
|
390
|
+
sendJson(res, 400, { success: false, error: 'projectId is required' });
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
if (typeof username !== 'string' || username.trim().length === 0) {
|
|
394
|
+
sendJson(res, 400, { success: false, error: 'username is required' });
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
// Only owner or admin can revoke access
|
|
398
|
+
const effectiveRole = await checkProjectAccess(projectId, session.username, session.role);
|
|
399
|
+
if (effectiveRole !== 'admin') {
|
|
400
|
+
sendJson(res, 404, { success: false, error: 'Project not found' });
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
const ip = getClientIp(req);
|
|
404
|
+
try {
|
|
405
|
+
await revokeAccess(projectId, username.trim());
|
|
406
|
+
await audit('access_revoke', ip, session.username, {
|
|
407
|
+
projectId,
|
|
408
|
+
target: username.trim(),
|
|
409
|
+
});
|
|
410
|
+
sendJson(res, 200, { success: true });
|
|
411
|
+
}
|
|
412
|
+
catch (err) {
|
|
413
|
+
const message = err instanceof Error ? err.message : 'Failed to revoke access';
|
|
414
|
+
if (message === 'Project not found' || message === 'User has no access to revoke') {
|
|
415
|
+
sendJson(res, 400, { success: false, error: message });
|
|
416
|
+
}
|
|
417
|
+
else {
|
|
418
|
+
sendJson(res, 400, { success: false, error: 'Failed to revoke access' });
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
});
|
|
422
|
+
// ── Linked services endpoints ───────────────────────
|
|
423
|
+
// POST /api/projects/link — link two projects (owner/admin of both required)
|
|
424
|
+
addRoute('POST', '/api/projects/link', async (req, res) => {
|
|
425
|
+
const session = getSession(req);
|
|
426
|
+
if (!session) {
|
|
427
|
+
sendJson(res, 401, { success: false, error: 'Authentication required' });
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
const body = await parseJsonBody(req);
|
|
431
|
+
if (typeof body !== 'object' || body === null) {
|
|
432
|
+
sendJson(res, 400, { success: false, error: 'Request body must be a JSON object' });
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
const { projectIdA, projectIdB } = body;
|
|
436
|
+
if (typeof projectIdA !== 'string' || projectIdA.trim().length === 0) {
|
|
437
|
+
sendJson(res, 400, { success: false, error: 'projectIdA is required' });
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
if (typeof projectIdB !== 'string' || projectIdB.trim().length === 0) {
|
|
441
|
+
sendJson(res, 400, { success: false, error: 'projectIdB is required' });
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
// Must have admin access to BOTH projects
|
|
445
|
+
const roleA = await checkProjectAccess(projectIdA, session.username, session.role);
|
|
446
|
+
const roleB = await checkProjectAccess(projectIdB, session.username, session.role);
|
|
447
|
+
if (roleA !== 'admin' || roleB !== 'admin') {
|
|
448
|
+
sendJson(res, 404, { success: false, error: 'Project not found' });
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
const ip = getClientIp(req);
|
|
452
|
+
try {
|
|
453
|
+
await linkProjects(projectIdA, projectIdB);
|
|
454
|
+
await audit('access_grant', ip, session.username, {
|
|
455
|
+
action: 'link',
|
|
456
|
+
projectIdA,
|
|
457
|
+
projectIdB,
|
|
458
|
+
});
|
|
459
|
+
sendJson(res, 200, { success: true });
|
|
460
|
+
}
|
|
461
|
+
catch (err) {
|
|
462
|
+
const message = err instanceof Error ? err.message : 'Failed to link';
|
|
463
|
+
if (message === 'Cannot link a project to itself' || message === 'Project not found') {
|
|
464
|
+
sendJson(res, 400, { success: false, error: message });
|
|
465
|
+
}
|
|
466
|
+
else {
|
|
467
|
+
sendJson(res, 400, { success: false, error: 'Failed to link projects' });
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
});
|
|
471
|
+
// POST /api/projects/unlink — unlink two projects (owner/admin of either)
|
|
472
|
+
addRoute('POST', '/api/projects/unlink', async (req, res) => {
|
|
473
|
+
const session = getSession(req);
|
|
474
|
+
if (!session) {
|
|
475
|
+
sendJson(res, 401, { success: false, error: 'Authentication required' });
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
const body = await parseJsonBody(req);
|
|
479
|
+
if (typeof body !== 'object' || body === null) {
|
|
480
|
+
sendJson(res, 400, { success: false, error: 'Request body must be a JSON object' });
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
const { projectIdA, projectIdB } = body;
|
|
484
|
+
if (typeof projectIdA !== 'string' || projectIdA.trim().length === 0) {
|
|
485
|
+
sendJson(res, 400, { success: false, error: 'projectIdA is required' });
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
if (typeof projectIdB !== 'string' || projectIdB.trim().length === 0) {
|
|
489
|
+
sendJson(res, 400, { success: false, error: 'projectIdB is required' });
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
// Must have admin access to EITHER project
|
|
493
|
+
const roleA = await checkProjectAccess(projectIdA, session.username, session.role);
|
|
494
|
+
const roleB = await checkProjectAccess(projectIdB, session.username, session.role);
|
|
495
|
+
if (roleA !== 'admin' && roleB !== 'admin') {
|
|
496
|
+
sendJson(res, 404, { success: false, error: 'Project not found' });
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
const ip = getClientIp(req);
|
|
500
|
+
try {
|
|
501
|
+
await unlinkProjects(projectIdA, projectIdB);
|
|
502
|
+
await audit('access_revoke', ip, session.username, {
|
|
503
|
+
action: 'unlink',
|
|
504
|
+
projectIdA,
|
|
505
|
+
projectIdB,
|
|
506
|
+
});
|
|
507
|
+
sendJson(res, 200, { success: true });
|
|
508
|
+
}
|
|
509
|
+
catch (err) {
|
|
510
|
+
const message = err instanceof Error ? err.message : 'Failed to unlink';
|
|
511
|
+
if (message === 'Project not found') {
|
|
512
|
+
sendJson(res, 400, { success: false, error: message });
|
|
513
|
+
}
|
|
514
|
+
else {
|
|
515
|
+
sendJson(res, 400, { success: false, error: 'Failed to unlink projects' });
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
});
|
|
519
|
+
// GET /api/projects/linked — get linked projects for a project
|
|
520
|
+
addRoute('GET', '/api/projects/linked', async (req, res) => {
|
|
521
|
+
const session = getSession(req);
|
|
522
|
+
if (!session) {
|
|
523
|
+
sendJson(res, 401, { success: false, error: 'Authentication required' });
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
const url = new URL(req.url ?? '/', `http://${req.headers.host ?? 'localhost'}`);
|
|
527
|
+
const id = url.searchParams.get('id');
|
|
528
|
+
if (!id) {
|
|
529
|
+
sendJson(res, 400, { success: false, error: 'id query parameter is required' });
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
const effectiveRole = await checkProjectAccess(id, session.username, session.role);
|
|
533
|
+
if (!effectiveRole) {
|
|
534
|
+
sendJson(res, 404, { success: false, error: 'Project not found' });
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
const group = await getLinkedGroup(id);
|
|
538
|
+
// Filter linked projects by user access — don't leak data about projects the user can't see
|
|
539
|
+
const accessChecks = await Promise.all(group.filter((p) => p.id !== id).map(async (p) => ({
|
|
540
|
+
project: p,
|
|
541
|
+
hasAccess: !!(await checkProjectAccess(p.id, session.username, session.role)),
|
|
542
|
+
})));
|
|
543
|
+
const linked = accessChecks.filter((c) => c.hasAccess).map((c) => ({
|
|
544
|
+
id: c.project.id,
|
|
545
|
+
name: c.project.name,
|
|
546
|
+
deployTarget: c.project.deployTarget,
|
|
547
|
+
healthStatus: c.project.healthStatus,
|
|
548
|
+
lastDeployAt: c.project.lastDeployAt,
|
|
549
|
+
}));
|
|
550
|
+
sendJson(res, 200, { success: true, data: linked });
|
|
551
|
+
});
|
|
552
|
+
// POST /api/projects/deploy-check — check which linked projects need redeployment
|
|
553
|
+
addRoute('POST', '/api/projects/deploy-check', async (req, res) => {
|
|
554
|
+
const session = getSession(req);
|
|
555
|
+
if (!session) {
|
|
556
|
+
sendJson(res, 401, { success: false, error: 'Authentication required' });
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
const body = await parseJsonBody(req);
|
|
560
|
+
if (typeof body !== 'object' || body === null) {
|
|
561
|
+
sendJson(res, 400, { success: false, error: 'Request body must be a JSON object' });
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
const { projectId } = body;
|
|
565
|
+
if (typeof projectId !== 'string' || projectId.trim().length === 0) {
|
|
566
|
+
sendJson(res, 400, { success: false, error: 'projectId is required' });
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
const effectiveRole = await checkProjectAccess(projectId, session.username, session.role);
|
|
570
|
+
if (!effectiveRole) {
|
|
571
|
+
sendJson(res, 404, { success: false, error: 'Project not found' });
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
const ip = getClientIp(req);
|
|
575
|
+
const plan = await getDeployPlan(projectId, session.username, ip);
|
|
576
|
+
if (!plan) {
|
|
577
|
+
sendJson(res, 404, { success: false, error: 'Project not found' });
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
sendJson(res, 200, { success: true, data: plan });
|
|
581
|
+
});
|
|
582
|
+
// ── Cost + Lessons endpoints ────────────────────────
|
|
583
|
+
// GET /api/projects/costs — aggregate costs across accessible projects
|
|
584
|
+
addRoute('GET', '/api/projects/costs', async (req, res) => {
|
|
585
|
+
const session = getSession(req);
|
|
586
|
+
if (!session) {
|
|
587
|
+
sendJson(res, 401, { success: false, error: 'Authentication required' });
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
const costs = await getAggregateCosts(session.username, session.role);
|
|
591
|
+
sendJson(res, 200, { success: true, data: costs });
|
|
592
|
+
});
|
|
593
|
+
// GET /api/projects/lessons — get lessons (optionally filtered by framework)
|
|
594
|
+
addRoute('GET', '/api/projects/lessons', async (req, res) => {
|
|
595
|
+
const session = getSession(req);
|
|
596
|
+
if (!session) {
|
|
597
|
+
sendJson(res, 401, { success: false, error: 'Authentication required' });
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
const url = new URL(req.url ?? '/', `http://${req.headers.host ?? 'localhost'}`);
|
|
601
|
+
const framework = url.searchParams.get('framework') || undefined;
|
|
602
|
+
const category = url.searchParams.get('category') || undefined;
|
|
603
|
+
const lessons = await getLessons({ framework, category });
|
|
604
|
+
const count = await getLessonCount();
|
|
605
|
+
sendJson(res, 200, { success: true, data: { lessons, total: count } });
|
|
606
|
+
});
|
|
607
|
+
// POST /api/projects/lessons — add a lesson (deployer+)
|
|
608
|
+
addRoute('POST', '/api/projects/lessons', async (req, res) => {
|
|
609
|
+
const session = getSession(req);
|
|
610
|
+
if (!session) {
|
|
611
|
+
sendJson(res, 401, { success: false, error: 'Authentication required' });
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
// Deployer minimum to write lessons
|
|
615
|
+
if (!hasRole(session, 'deployer')) {
|
|
616
|
+
sendJson(res, 404, { success: false, error: 'Not found' });
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
const body = await parseJsonBody(req);
|
|
620
|
+
if (typeof body !== 'object' || body === null) {
|
|
621
|
+
sendJson(res, 400, { success: false, error: 'Request body must be a JSON object' });
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
const { framework, category, lesson, action, project, agent } = body;
|
|
625
|
+
if (typeof framework !== 'string' || typeof category !== 'string' ||
|
|
626
|
+
typeof lesson !== 'string' || typeof action !== 'string' ||
|
|
627
|
+
typeof project !== 'string' || typeof agent !== 'string') {
|
|
628
|
+
sendJson(res, 400, { success: false, error: 'framework, category, lesson, action, project, and agent are required strings' });
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
// Cap field lengths
|
|
632
|
+
if (lesson.length > 1000 || action.length > 500) {
|
|
633
|
+
sendJson(res, 400, { success: false, error: 'lesson max 1000 chars, action max 500 chars' });
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
const input = {
|
|
637
|
+
framework: framework.slice(0, 50),
|
|
638
|
+
category: category.slice(0, 50),
|
|
639
|
+
lesson: lesson.slice(0, 1000),
|
|
640
|
+
action: action.slice(0, 500),
|
|
641
|
+
project: project.slice(0, 100),
|
|
642
|
+
agent: agent.slice(0, 50),
|
|
643
|
+
};
|
|
644
|
+
const created = await addLesson(input);
|
|
645
|
+
const ip = getClientIp(req);
|
|
646
|
+
await audit('project_create', ip, session.username, { action: 'add_lesson', framework: input.framework, category: input.category });
|
|
647
|
+
sendJson(res, 201, { success: true, data: created });
|
|
648
|
+
});
|