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,352 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tower Auth — Authentication engine for remote mode.
|
|
3
|
+
*
|
|
4
|
+
* 5-layer security: this module handles Layer 2 (Authentication).
|
|
5
|
+
* Two-password architecture: login password ≠ vault password.
|
|
6
|
+
*
|
|
7
|
+
* TOTP: RFC 6238, 30-second rotation, replay protection.
|
|
8
|
+
* ARCH-R2-003: Split into tower-auth + tower-session + tower-rate-limit.
|
|
9
|
+
*/
|
|
10
|
+
import { randomBytes, createHmac, pbkdf2 as pbkdf2Cb, timingSafeEqual } from 'node:crypto';
|
|
11
|
+
import { readFile, rename, mkdir, open, copyFile } from 'node:fs/promises';
|
|
12
|
+
import { join } from 'node:path';
|
|
13
|
+
import { homedir } from 'node:os';
|
|
14
|
+
// Re-export session + rate-limit modules for backward compatibility
|
|
15
|
+
export { createSession, validateSession, logout, invalidateUserSessions, updateSessionRole, cleanupExpiredSessions, getSessionCookieName, parseSessionCookie, buildSessionCookie, clearSessionCookie, isAuthExempt, } from './tower-session.js';
|
|
16
|
+
export { checkRateLimit, recordFailure, clearFailures, cleanupStaleEntries, } from './tower-rate-limit.js';
|
|
17
|
+
// Import for internal use
|
|
18
|
+
import { createSession, invalidateUserSessions, updateSessionRole, cleanupExpiredSessions } from './tower-session.js';
|
|
19
|
+
import { checkRateLimit, recordFailure, clearFailures, cleanupStaleEntries } from './tower-rate-limit.js';
|
|
20
|
+
const VOIDFORGE_DIR = join(homedir(), '.voidforge');
|
|
21
|
+
const AUTH_PATH = join(VOIDFORGE_DIR, 'auth.json');
|
|
22
|
+
const MAX_USERNAME_LENGTH = 64;
|
|
23
|
+
const MAX_PASSWORD_LENGTH = 256;
|
|
24
|
+
// ── Configuration ──────────────────────────────────
|
|
25
|
+
const TOTP_STEP = 30;
|
|
26
|
+
const TOTP_DIGITS = 6;
|
|
27
|
+
const PBKDF2_ITERATIONS = 210_000;
|
|
28
|
+
const PBKDF2_KEYLEN = 64;
|
|
29
|
+
const PBKDF2_DIGEST = 'sha512';
|
|
30
|
+
const SALT_LENGTH = 32;
|
|
31
|
+
const CLEANUP_INTERVAL_MS = 15 * 60 * 1000;
|
|
32
|
+
// ── In-memory state ────────────────────────────────
|
|
33
|
+
let remoteMode = false;
|
|
34
|
+
let lanMode = false;
|
|
35
|
+
let cleanupTimer = null;
|
|
36
|
+
// ── Write serialization ────────────────────────────
|
|
37
|
+
let writeQueue = Promise.resolve();
|
|
38
|
+
function serialized(fn) {
|
|
39
|
+
const result = writeQueue.then(fn, () => fn());
|
|
40
|
+
writeQueue = result.then(() => { }, () => { });
|
|
41
|
+
return result;
|
|
42
|
+
}
|
|
43
|
+
// ── Remote + LAN mode ──────────────────────────────
|
|
44
|
+
export function setRemoteMode(enabled) {
|
|
45
|
+
remoteMode = enabled;
|
|
46
|
+
if (enabled && !cleanupTimer) {
|
|
47
|
+
cleanupTimer = setInterval(() => {
|
|
48
|
+
cleanupExpiredSessions();
|
|
49
|
+
cleanupStaleEntries();
|
|
50
|
+
}, CLEANUP_INTERVAL_MS);
|
|
51
|
+
cleanupTimer.unref();
|
|
52
|
+
}
|
|
53
|
+
else if (!enabled && cleanupTimer) {
|
|
54
|
+
clearInterval(cleanupTimer);
|
|
55
|
+
cleanupTimer = null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
export function isRemoteMode() {
|
|
59
|
+
return remoteMode;
|
|
60
|
+
}
|
|
61
|
+
export function setLanMode(enabled) {
|
|
62
|
+
lanMode = enabled;
|
|
63
|
+
}
|
|
64
|
+
export function isLanMode() {
|
|
65
|
+
return lanMode;
|
|
66
|
+
}
|
|
67
|
+
/** Get client IP — delegates to rate-limit module but passes remoteMode state. */
|
|
68
|
+
export function getClientIp(req) {
|
|
69
|
+
// Inline to avoid circular: trusts X-Forwarded-For only in remote mode.
|
|
70
|
+
// Use leftmost entry (parts[0]) — the real client IP before any proxies.
|
|
71
|
+
// Previously used rightmost (parts[parts.length - 1]) which returned 127.0.0.1
|
|
72
|
+
// behind Caddy, making rate limiting and session IP binding ineffective.
|
|
73
|
+
if (remoteMode) {
|
|
74
|
+
const forwarded = req.headers['x-forwarded-for'];
|
|
75
|
+
if (typeof forwarded === 'string') {
|
|
76
|
+
const parts = forwarded.split(',');
|
|
77
|
+
return parts[0].trim();
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return req.socket.remoteAddress ?? 'unknown';
|
|
81
|
+
}
|
|
82
|
+
// ── Password hashing (PBKDF2, NIST-strength) ───────
|
|
83
|
+
async function hashPassword(password) {
|
|
84
|
+
const capped = password.slice(0, MAX_PASSWORD_LENGTH);
|
|
85
|
+
const salt = randomBytes(SALT_LENGTH);
|
|
86
|
+
const key = await new Promise((resolve, reject) => {
|
|
87
|
+
pbkdf2Cb(capped, salt, PBKDF2_ITERATIONS, PBKDF2_KEYLEN, PBKDF2_DIGEST, (err, k) => {
|
|
88
|
+
if (err)
|
|
89
|
+
reject(err);
|
|
90
|
+
else
|
|
91
|
+
resolve(k);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
return `pbkdf2:${PBKDF2_ITERATIONS}:${salt.toString('hex')}:${key.toString('hex')}`;
|
|
95
|
+
}
|
|
96
|
+
async function verifyPassword(password, stored) {
|
|
97
|
+
const capped = password.slice(0, MAX_PASSWORD_LENGTH);
|
|
98
|
+
const parts = stored.split(':');
|
|
99
|
+
if (parts[0] !== 'pbkdf2' || parts.length !== 4)
|
|
100
|
+
return false;
|
|
101
|
+
const iterations = parseInt(parts[1], 10);
|
|
102
|
+
const salt = Buffer.from(parts[2], 'hex');
|
|
103
|
+
const expectedKey = Buffer.from(parts[3], 'hex');
|
|
104
|
+
const actualKey = await new Promise((resolve, reject) => {
|
|
105
|
+
pbkdf2Cb(capped, salt, iterations, expectedKey.length, PBKDF2_DIGEST, (err, k) => {
|
|
106
|
+
if (err)
|
|
107
|
+
reject(err);
|
|
108
|
+
else
|
|
109
|
+
resolve(k);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
return timingSafeEqual(actualKey, expectedKey);
|
|
113
|
+
}
|
|
114
|
+
// ── TOTP (RFC 6238) ────────────────────────────────
|
|
115
|
+
function generateTotpSecret() {
|
|
116
|
+
const bytes = randomBytes(20);
|
|
117
|
+
return base32Encode(bytes);
|
|
118
|
+
}
|
|
119
|
+
function base32Encode(buffer) {
|
|
120
|
+
const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
|
|
121
|
+
let bits = '';
|
|
122
|
+
for (const byte of buffer) {
|
|
123
|
+
bits += byte.toString(2).padStart(8, '0');
|
|
124
|
+
}
|
|
125
|
+
let result = '';
|
|
126
|
+
for (let i = 0; i < bits.length; i += 5) {
|
|
127
|
+
const chunk = bits.slice(i, i + 5).padEnd(5, '0');
|
|
128
|
+
result += alphabet[parseInt(chunk, 2)];
|
|
129
|
+
}
|
|
130
|
+
return result;
|
|
131
|
+
}
|
|
132
|
+
function base32Decode(encoded) {
|
|
133
|
+
const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
|
|
134
|
+
let bits = '';
|
|
135
|
+
for (const char of encoded.toUpperCase()) {
|
|
136
|
+
const idx = alphabet.indexOf(char);
|
|
137
|
+
if (idx === -1)
|
|
138
|
+
continue;
|
|
139
|
+
bits += idx.toString(2).padStart(5, '0');
|
|
140
|
+
}
|
|
141
|
+
const bytes = [];
|
|
142
|
+
for (let i = 0; i + 8 <= bits.length; i += 8) {
|
|
143
|
+
bytes.push(parseInt(bits.slice(i, i + 8), 2));
|
|
144
|
+
}
|
|
145
|
+
return Buffer.from(bytes);
|
|
146
|
+
}
|
|
147
|
+
function generateTotp(secret, timeStep) {
|
|
148
|
+
const time = timeStep ?? Math.floor(Date.now() / 1000 / TOTP_STEP);
|
|
149
|
+
const timeBuffer = Buffer.alloc(8);
|
|
150
|
+
timeBuffer.writeUInt32BE(0, 0);
|
|
151
|
+
timeBuffer.writeUInt32BE(time, 4);
|
|
152
|
+
const key = base32Decode(secret);
|
|
153
|
+
const hmac = createHmac('sha1', key).update(timeBuffer).digest();
|
|
154
|
+
const offset = hmac[hmac.length - 1] & 0x0f;
|
|
155
|
+
const code = ((hmac[offset] & 0x7f) << 24 |
|
|
156
|
+
(hmac[offset + 1] & 0xff) << 16 |
|
|
157
|
+
(hmac[offset + 2] & 0xff) << 8 |
|
|
158
|
+
(hmac[offset + 3] & 0xff)) % (10 ** TOTP_DIGITS);
|
|
159
|
+
return code.toString().padStart(TOTP_DIGITS, '0');
|
|
160
|
+
}
|
|
161
|
+
function verifyTotp(secret, code, lastUsedStep) {
|
|
162
|
+
// VOIDFORGE_TEST: accept 000000 as valid TOTP for E2E test bypass
|
|
163
|
+
if (process.env['VOIDFORGE_TEST'] === '1' && code === '000000') {
|
|
164
|
+
return Math.floor(Date.now() / 1000 / TOTP_STEP);
|
|
165
|
+
}
|
|
166
|
+
const currentStep = Math.floor(Date.now() / 1000 / TOTP_STEP);
|
|
167
|
+
for (let offset = -1; offset <= 1; offset++) {
|
|
168
|
+
const step = currentStep + offset;
|
|
169
|
+
if (step <= lastUsedStep)
|
|
170
|
+
continue;
|
|
171
|
+
const expected = generateTotp(secret, step);
|
|
172
|
+
if (expected.length === code.length) {
|
|
173
|
+
const a = Buffer.from(expected);
|
|
174
|
+
const b = Buffer.from(code);
|
|
175
|
+
if (timingSafeEqual(a, b))
|
|
176
|
+
return step;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return -1;
|
|
180
|
+
}
|
|
181
|
+
// ── Auth store I/O (serialized + atomic writes) ────
|
|
182
|
+
async function readAuthStore() {
|
|
183
|
+
try {
|
|
184
|
+
const raw = await readFile(AUTH_PATH, 'utf-8');
|
|
185
|
+
const parsed = JSON.parse(raw);
|
|
186
|
+
if (typeof parsed !== 'object' || !Array.isArray(parsed.users)) {
|
|
187
|
+
throw new Error('Invalid auth store format');
|
|
188
|
+
}
|
|
189
|
+
for (const user of parsed.users) {
|
|
190
|
+
if (!user.role)
|
|
191
|
+
user.role = 'admin';
|
|
192
|
+
}
|
|
193
|
+
return parsed;
|
|
194
|
+
}
|
|
195
|
+
catch (err) {
|
|
196
|
+
if (err instanceof Error && 'code' in err && err.code === 'ENOENT') {
|
|
197
|
+
return { users: [], remoteMode: false };
|
|
198
|
+
}
|
|
199
|
+
throw new Error(`Auth store corrupted: ${err instanceof Error ? err.message : 'Unknown error'}`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
async function writeAuthStore(store) {
|
|
203
|
+
await mkdir(VOIDFORGE_DIR, { recursive: true });
|
|
204
|
+
// v17.0: Backup before write — prevents lockout on corruption.
|
|
205
|
+
// Matches project-registry.ts pattern.
|
|
206
|
+
try {
|
|
207
|
+
await copyFile(AUTH_PATH, AUTH_PATH + '.bak');
|
|
208
|
+
}
|
|
209
|
+
catch { /* No existing file to backup — first write */ }
|
|
210
|
+
const data = JSON.stringify(store, null, 2);
|
|
211
|
+
const tmpPath = AUTH_PATH + '.tmp';
|
|
212
|
+
const fh = await open(tmpPath, 'w', 0o600);
|
|
213
|
+
try {
|
|
214
|
+
await fh.writeFile(data);
|
|
215
|
+
await fh.sync();
|
|
216
|
+
}
|
|
217
|
+
finally {
|
|
218
|
+
await fh.close();
|
|
219
|
+
}
|
|
220
|
+
await rename(tmpPath, AUTH_PATH);
|
|
221
|
+
}
|
|
222
|
+
// ── Validation ─────────────────────────────────────
|
|
223
|
+
const USERNAME_PATTERN = /^[a-zA-Z0-9._-]+$/;
|
|
224
|
+
export function isValidUsername(username) {
|
|
225
|
+
return username.length >= 3 && username.length <= MAX_USERNAME_LENGTH && USERNAME_PATTERN.test(username);
|
|
226
|
+
}
|
|
227
|
+
// ── Public API ─────────────────────────────────────
|
|
228
|
+
export async function hasUsers() {
|
|
229
|
+
const store = await readAuthStore();
|
|
230
|
+
return store.users.length > 0;
|
|
231
|
+
}
|
|
232
|
+
export function createUser(username, password, role) {
|
|
233
|
+
return serialized(async () => {
|
|
234
|
+
const store = await readAuthStore();
|
|
235
|
+
const safeUsername = username.slice(0, MAX_USERNAME_LENGTH);
|
|
236
|
+
for (const user of store.users) {
|
|
237
|
+
const a = Buffer.from(user.username.padEnd(MAX_USERNAME_LENGTH));
|
|
238
|
+
const b = Buffer.from(safeUsername.padEnd(MAX_USERNAME_LENGTH));
|
|
239
|
+
if (a.length === b.length && timingSafeEqual(a, b)) {
|
|
240
|
+
throw new Error('Username already taken');
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
if (store.users.length > 0 && role === undefined) {
|
|
244
|
+
throw new Error('Invitation required for additional users');
|
|
245
|
+
}
|
|
246
|
+
const assignedRole = store.users.length === 0 ? 'admin' : role;
|
|
247
|
+
const passwordHash = await hashPassword(password);
|
|
248
|
+
const totpSecret = generateTotpSecret();
|
|
249
|
+
store.users.push({
|
|
250
|
+
username: safeUsername,
|
|
251
|
+
passwordHash,
|
|
252
|
+
totpSecret,
|
|
253
|
+
lastTotpStep: 0,
|
|
254
|
+
role: assignedRole,
|
|
255
|
+
createdAt: new Date().toISOString(),
|
|
256
|
+
});
|
|
257
|
+
store.remoteMode = true;
|
|
258
|
+
await writeAuthStore(store);
|
|
259
|
+
const totpUri = `otpauth://totp/VoidForge:${encodeURIComponent(safeUsername)}?secret=${totpSecret}&issuer=VoidForge&digits=${TOTP_DIGITS}&period=${TOTP_STEP}`;
|
|
260
|
+
return { totpSecret, totpUri };
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
export function removeUser(targetUsername) {
|
|
264
|
+
return serialized(async () => {
|
|
265
|
+
const store = await readAuthStore();
|
|
266
|
+
const idx = store.users.findIndex((u) => u.username === targetUsername);
|
|
267
|
+
if (idx === -1)
|
|
268
|
+
throw new Error('User not found');
|
|
269
|
+
const user = store.users[idx];
|
|
270
|
+
if (user.role === 'admin') {
|
|
271
|
+
const adminCount = store.users.filter((u) => u.role === 'admin').length;
|
|
272
|
+
if (adminCount <= 1)
|
|
273
|
+
throw new Error('Cannot remove the last admin');
|
|
274
|
+
}
|
|
275
|
+
store.users.splice(idx, 1);
|
|
276
|
+
await writeAuthStore(store);
|
|
277
|
+
invalidateUserSessions(targetUsername);
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
export function updateUserRole(targetUsername, newRole) {
|
|
281
|
+
return serialized(async () => {
|
|
282
|
+
const store = await readAuthStore();
|
|
283
|
+
const user = store.users.find((u) => u.username === targetUsername);
|
|
284
|
+
if (!user)
|
|
285
|
+
throw new Error('User not found');
|
|
286
|
+
if (user.role === 'admin' && newRole !== 'admin') {
|
|
287
|
+
const adminCount = store.users.filter((u) => u.role === 'admin').length;
|
|
288
|
+
if (adminCount <= 1)
|
|
289
|
+
throw new Error('Cannot demote the last admin');
|
|
290
|
+
}
|
|
291
|
+
user.role = newRole;
|
|
292
|
+
await writeAuthStore(store);
|
|
293
|
+
updateSessionRole(targetUsername, newRole);
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
export async function listUsers() {
|
|
297
|
+
const store = await readAuthStore();
|
|
298
|
+
return store.users.map((u) => ({
|
|
299
|
+
username: u.username,
|
|
300
|
+
role: u.role,
|
|
301
|
+
createdAt: u.createdAt,
|
|
302
|
+
}));
|
|
303
|
+
}
|
|
304
|
+
export async function getUserRole(username) {
|
|
305
|
+
const store = await readAuthStore();
|
|
306
|
+
const user = store.users.find((u) => u.username === username);
|
|
307
|
+
return user?.role ?? null;
|
|
308
|
+
}
|
|
309
|
+
export async function login(username, password, totpCode, ip) {
|
|
310
|
+
const rateCheck = checkRateLimit(ip);
|
|
311
|
+
if (!rateCheck.allowed) {
|
|
312
|
+
return { error: 'Too many attempts. Try again later.', retryAfterMs: rateCheck.retryAfterMs };
|
|
313
|
+
}
|
|
314
|
+
const safeUsername = username.slice(0, MAX_USERNAME_LENGTH);
|
|
315
|
+
const safePassword = password.slice(0, MAX_PASSWORD_LENGTH);
|
|
316
|
+
const store = await readAuthStore();
|
|
317
|
+
let matchedUser = null;
|
|
318
|
+
for (const user of store.users) {
|
|
319
|
+
const usernameA = Buffer.from(user.username.padEnd(MAX_USERNAME_LENGTH));
|
|
320
|
+
const usernameB = Buffer.from(safeUsername.padEnd(MAX_USERNAME_LENGTH));
|
|
321
|
+
if (usernameA.length === usernameB.length && timingSafeEqual(usernameA, usernameB)) {
|
|
322
|
+
matchedUser = user;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
if (!matchedUser) {
|
|
326
|
+
await hashPassword(safePassword);
|
|
327
|
+
recordFailure(ip);
|
|
328
|
+
return { error: 'Invalid credentials' };
|
|
329
|
+
}
|
|
330
|
+
const passwordValid = await verifyPassword(safePassword, matchedUser.passwordHash);
|
|
331
|
+
if (!passwordValid) {
|
|
332
|
+
recordFailure(ip);
|
|
333
|
+
return { error: 'Invalid credentials' };
|
|
334
|
+
}
|
|
335
|
+
const usedStep = verifyTotp(matchedUser.totpSecret, totpCode, matchedUser.lastTotpStep);
|
|
336
|
+
if (usedStep === -1) {
|
|
337
|
+
recordFailure(ip);
|
|
338
|
+
return { error: 'Invalid credentials' };
|
|
339
|
+
}
|
|
340
|
+
await serialized(async () => {
|
|
341
|
+
const currentStore = await readAuthStore();
|
|
342
|
+
const user = currentStore.users.find((u) => u.username === matchedUser.username);
|
|
343
|
+
if (user) {
|
|
344
|
+
user.lastTotpStep = usedStep;
|
|
345
|
+
await writeAuthStore(currentStore);
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
clearFailures(ip);
|
|
349
|
+
const currentRole = matchedUser.role ?? 'viewer';
|
|
350
|
+
const token = createSession(matchedUser.username, currentRole, ip);
|
|
351
|
+
return { token };
|
|
352
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tower Rate Limit — Per-IP rate limiting for login and auth endpoints.
|
|
3
|
+
* Extracted from tower-auth.ts (ARCH-R2-003).
|
|
4
|
+
*
|
|
5
|
+
* 5 attempts per 60-second window. Lockout after 10 consecutive failures (30 min).
|
|
6
|
+
*/
|
|
7
|
+
export declare function checkRateLimit(ip: string): {
|
|
8
|
+
allowed: boolean;
|
|
9
|
+
retryAfterMs: number;
|
|
10
|
+
};
|
|
11
|
+
export declare function recordFailure(ip: string): void;
|
|
12
|
+
export declare function clearFailures(ip: string): void;
|
|
13
|
+
/** Evict stale rate-limit entries (called by periodic cleanup). */
|
|
14
|
+
export declare function cleanupStaleEntries(): void;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tower Rate Limit — Per-IP rate limiting for login and auth endpoints.
|
|
3
|
+
* Extracted from tower-auth.ts (ARCH-R2-003).
|
|
4
|
+
*
|
|
5
|
+
* 5 attempts per 60-second window. Lockout after 10 consecutive failures (30 min).
|
|
6
|
+
*/
|
|
7
|
+
const RATE_LIMIT_WINDOW_MS = 60 * 1000;
|
|
8
|
+
const RATE_LIMIT_MAX = 5;
|
|
9
|
+
const LOCKOUT_THRESHOLD = 10;
|
|
10
|
+
const LOCKOUT_DURATION_MS = 30 * 60 * 1000;
|
|
11
|
+
const rateLimits = new Map();
|
|
12
|
+
export function checkRateLimit(ip) {
|
|
13
|
+
// VOIDFORGE_TEST: skip rate limiting so E2E tests aren't throttled
|
|
14
|
+
if (process.env['VOIDFORGE_TEST'] === '1') {
|
|
15
|
+
return { allowed: true, retryAfterMs: 0 };
|
|
16
|
+
}
|
|
17
|
+
const now = Date.now();
|
|
18
|
+
const entry = rateLimits.get(ip);
|
|
19
|
+
if (!entry) {
|
|
20
|
+
rateLimits.set(ip, { attempts: 1, firstAttempt: now, consecutiveFailures: 0, lockedUntil: 0 });
|
|
21
|
+
return { allowed: true, retryAfterMs: 0 };
|
|
22
|
+
}
|
|
23
|
+
if (entry.lockedUntil > now) {
|
|
24
|
+
return { allowed: false, retryAfterMs: entry.lockedUntil - now };
|
|
25
|
+
}
|
|
26
|
+
if (now - entry.firstAttempt > RATE_LIMIT_WINDOW_MS) {
|
|
27
|
+
entry.attempts = 1;
|
|
28
|
+
entry.firstAttempt = now;
|
|
29
|
+
return { allowed: true, retryAfterMs: 0 };
|
|
30
|
+
}
|
|
31
|
+
entry.attempts++;
|
|
32
|
+
if (entry.attempts > RATE_LIMIT_MAX) {
|
|
33
|
+
return { allowed: false, retryAfterMs: RATE_LIMIT_WINDOW_MS - (now - entry.firstAttempt) };
|
|
34
|
+
}
|
|
35
|
+
return { allowed: true, retryAfterMs: 0 };
|
|
36
|
+
}
|
|
37
|
+
export function recordFailure(ip) {
|
|
38
|
+
const entry = rateLimits.get(ip);
|
|
39
|
+
if (!entry)
|
|
40
|
+
return;
|
|
41
|
+
entry.consecutiveFailures++;
|
|
42
|
+
if (entry.consecutiveFailures >= LOCKOUT_THRESHOLD) {
|
|
43
|
+
entry.lockedUntil = Date.now() + LOCKOUT_DURATION_MS;
|
|
44
|
+
entry.consecutiveFailures = 0;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
export function clearFailures(ip) {
|
|
48
|
+
const entry = rateLimits.get(ip);
|
|
49
|
+
if (entry)
|
|
50
|
+
entry.consecutiveFailures = 0;
|
|
51
|
+
}
|
|
52
|
+
/** Evict stale rate-limit entries (called by periodic cleanup). */
|
|
53
|
+
export function cleanupStaleEntries() {
|
|
54
|
+
const now = Date.now();
|
|
55
|
+
for (const [ip, entry] of rateLimits) {
|
|
56
|
+
if (now - entry.firstAttempt > RATE_LIMIT_WINDOW_MS && entry.lockedUntil < now) {
|
|
57
|
+
rateLimits.delete(ip);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
// getClientIp removed — single source of truth is tower-auth.ts (v17.0 No Stubs cleanup)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tower Session — In-memory session management for remote mode.
|
|
3
|
+
* Extracted from tower-auth.ts (ARCH-R2-003).
|
|
4
|
+
*
|
|
5
|
+
* Sessions are never persisted to disk. Server restart = all sessions invalidated.
|
|
6
|
+
*/
|
|
7
|
+
export type UserRole = 'admin' | 'deployer' | 'viewer';
|
|
8
|
+
export interface SessionInfo {
|
|
9
|
+
username: string;
|
|
10
|
+
role: UserRole;
|
|
11
|
+
}
|
|
12
|
+
/** Create a new session. Invalidates existing sessions for the same user. */
|
|
13
|
+
export declare function createSession(username: string, role: UserRole, ip: string): string;
|
|
14
|
+
/** Validate a session token. Returns session info on success, null on failure. */
|
|
15
|
+
export declare function validateSession(token: string, ip: string): SessionInfo | null;
|
|
16
|
+
/** Invalidate a session (logout). */
|
|
17
|
+
export declare function logout(token: string): void;
|
|
18
|
+
/** Invalidate all sessions for a specific user. */
|
|
19
|
+
export declare function invalidateUserSessions(username: string): void;
|
|
20
|
+
/** Update role on active sessions for a user. */
|
|
21
|
+
export declare function updateSessionRole(username: string, newRole: UserRole): void;
|
|
22
|
+
/** Evict expired sessions (called by periodic cleanup). */
|
|
23
|
+
export declare function cleanupExpiredSessions(): void;
|
|
24
|
+
export declare function getSessionCookieName(): string;
|
|
25
|
+
export declare function parseSessionCookie(cookieHeader: string | undefined): string | null;
|
|
26
|
+
export declare function buildSessionCookie(token: string, secure: boolean): string;
|
|
27
|
+
export declare function clearSessionCookie(): string;
|
|
28
|
+
export declare function isAuthExempt(pathname: string): boolean;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tower Session — In-memory session management for remote mode.
|
|
3
|
+
* Extracted from tower-auth.ts (ARCH-R2-003).
|
|
4
|
+
*
|
|
5
|
+
* Sessions are never persisted to disk. Server restart = all sessions invalidated.
|
|
6
|
+
*/
|
|
7
|
+
import { randomBytes } from 'node:crypto';
|
|
8
|
+
const SESSION_TTL_MS = 8 * 60 * 60 * 1000; // 8 hours
|
|
9
|
+
const sessions = new Map();
|
|
10
|
+
/** Create a new session. Invalidates existing sessions for the same user. */
|
|
11
|
+
export function createSession(username, role, ip) {
|
|
12
|
+
// Invalidate existing sessions for this user (single active session)
|
|
13
|
+
const toDelete = [];
|
|
14
|
+
for (const [token, session] of sessions) {
|
|
15
|
+
if (session.username === username)
|
|
16
|
+
toDelete.push(token);
|
|
17
|
+
}
|
|
18
|
+
for (const token of toDelete)
|
|
19
|
+
sessions.delete(token);
|
|
20
|
+
const token = randomBytes(32).toString('hex');
|
|
21
|
+
sessions.set(token, {
|
|
22
|
+
token,
|
|
23
|
+
username,
|
|
24
|
+
role,
|
|
25
|
+
ip,
|
|
26
|
+
createdAt: Date.now(),
|
|
27
|
+
expiresAt: Date.now() + SESSION_TTL_MS,
|
|
28
|
+
ipBinding: true,
|
|
29
|
+
});
|
|
30
|
+
return token;
|
|
31
|
+
}
|
|
32
|
+
/** Validate a session token. Returns session info on success, null on failure. */
|
|
33
|
+
export function validateSession(token, ip) {
|
|
34
|
+
const session = sessions.get(token);
|
|
35
|
+
if (!session)
|
|
36
|
+
return null;
|
|
37
|
+
if (Date.now() > session.expiresAt) {
|
|
38
|
+
sessions.delete(token);
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
if (session.ipBinding && session.ip !== ip) {
|
|
42
|
+
sessions.delete(token);
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
return { username: session.username, role: session.role };
|
|
46
|
+
}
|
|
47
|
+
/** Invalidate a session (logout). */
|
|
48
|
+
export function logout(token) {
|
|
49
|
+
sessions.delete(token);
|
|
50
|
+
}
|
|
51
|
+
/** Invalidate all sessions for a specific user. */
|
|
52
|
+
export function invalidateUserSessions(username) {
|
|
53
|
+
const toDelete = [];
|
|
54
|
+
for (const [token, session] of sessions) {
|
|
55
|
+
if (session.username === username)
|
|
56
|
+
toDelete.push(token);
|
|
57
|
+
}
|
|
58
|
+
for (const token of toDelete)
|
|
59
|
+
sessions.delete(token);
|
|
60
|
+
}
|
|
61
|
+
/** Update role on active sessions for a user. */
|
|
62
|
+
export function updateSessionRole(username, newRole) {
|
|
63
|
+
for (const [, session] of sessions) {
|
|
64
|
+
if (session.username === username)
|
|
65
|
+
session.role = newRole;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/** Evict expired sessions (called by periodic cleanup). */
|
|
69
|
+
export function cleanupExpiredSessions() {
|
|
70
|
+
const now = Date.now();
|
|
71
|
+
for (const [token, session] of sessions) {
|
|
72
|
+
if (now > session.expiresAt)
|
|
73
|
+
sessions.delete(token);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
// ── Cookie helpers ────────────────────────────────────
|
|
77
|
+
const COOKIE_NAME = 'voidforge_session';
|
|
78
|
+
export function getSessionCookieName() {
|
|
79
|
+
return COOKIE_NAME;
|
|
80
|
+
}
|
|
81
|
+
export function parseSessionCookie(cookieHeader) {
|
|
82
|
+
if (!cookieHeader)
|
|
83
|
+
return null;
|
|
84
|
+
const match = cookieHeader.match(new RegExp(`(?:^|;\\s*)${COOKIE_NAME}=([^;]+)`));
|
|
85
|
+
return match ? match[1] : null;
|
|
86
|
+
}
|
|
87
|
+
export function buildSessionCookie(token, secure) {
|
|
88
|
+
const maxAge = SESSION_TTL_MS / 1000;
|
|
89
|
+
const flags = [
|
|
90
|
+
`${COOKIE_NAME}=${token}`,
|
|
91
|
+
`Max-Age=${maxAge}`,
|
|
92
|
+
'Path=/',
|
|
93
|
+
'HttpOnly',
|
|
94
|
+
'SameSite=Strict',
|
|
95
|
+
];
|
|
96
|
+
if (secure)
|
|
97
|
+
flags.push('Secure');
|
|
98
|
+
return flags.join('; ');
|
|
99
|
+
}
|
|
100
|
+
export function clearSessionCookie() {
|
|
101
|
+
return `${COOKIE_NAME}=; Max-Age=0; Path=/; HttpOnly; SameSite=Strict`;
|
|
102
|
+
}
|
|
103
|
+
// ── Auth exemptions ───────────────────────────────────
|
|
104
|
+
const AUTH_EXEMPT_PATHS = [
|
|
105
|
+
'/api/auth/login',
|
|
106
|
+
'/api/auth/logout',
|
|
107
|
+
'/api/auth/setup',
|
|
108
|
+
'/api/auth/session',
|
|
109
|
+
'/api/users/complete-invite',
|
|
110
|
+
'/login.html',
|
|
111
|
+
'/login.js',
|
|
112
|
+
'/invite.html',
|
|
113
|
+
'/invite.js',
|
|
114
|
+
'/styles.css',
|
|
115
|
+
'/favicon.svg',
|
|
116
|
+
];
|
|
117
|
+
export function isAuthExempt(pathname) {
|
|
118
|
+
return AUTH_EXEMPT_PATHS.includes(pathname);
|
|
119
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Treasury Backup — Encrypted daily snapshots (§9.17, §9.19.13).
|
|
3
|
+
*
|
|
4
|
+
* Daily snapshot of ~/.voidforge/treasury/ + growth state.
|
|
5
|
+
* Encrypted with vault password (AES-256-GCM).
|
|
6
|
+
* Retain 30 days. Runs as a heartbeat daemon scheduled job.
|
|
7
|
+
*
|
|
8
|
+
* PRD Reference: §9.17 (Backup Strategy), §9.19.13 (scope extension)
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Create an encrypted daily backup of the treasury directory.
|
|
12
|
+
* The backup is a gzipped tar-like concatenation of files, encrypted with AES-256-GCM.
|
|
13
|
+
* The vault password is the encryption key (via scrypt).
|
|
14
|
+
*/
|
|
15
|
+
export declare function createDailyBackup(vaultPassword: string): Promise<{
|
|
16
|
+
path: string;
|
|
17
|
+
files: number;
|
|
18
|
+
}>;
|
|
19
|
+
/**
|
|
20
|
+
* Export all financial data (encrypted with vault password).
|
|
21
|
+
* Used by /treasury --export and uninstall safety.
|
|
22
|
+
*/
|
|
23
|
+
export declare function exportTreasuryData(vaultPassword: string, outputPath: string): Promise<void>;
|