thevoidforge 21.0.11 → 21.0.13

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.
Files changed (108) hide show
  1. package/dist/.claude/commands/ai.md +69 -0
  2. package/dist/.claude/commands/architect.md +121 -0
  3. package/dist/.claude/commands/assemble.md +201 -0
  4. package/dist/.claude/commands/assess.md +75 -0
  5. package/dist/.claude/commands/blueprint.md +135 -0
  6. package/dist/.claude/commands/build.md +116 -0
  7. package/dist/.claude/commands/campaign.md +201 -0
  8. package/dist/.claude/commands/cultivation.md +166 -0
  9. package/dist/.claude/commands/current.md +128 -0
  10. package/dist/.claude/commands/dangerroom.md +74 -0
  11. package/dist/.claude/commands/debrief.md +178 -0
  12. package/dist/.claude/commands/deploy.md +99 -0
  13. package/dist/.claude/commands/devops.md +143 -0
  14. package/dist/.claude/commands/gauntlet.md +140 -0
  15. package/dist/.claude/commands/git.md +104 -0
  16. package/dist/.claude/commands/grow.md +146 -0
  17. package/dist/.claude/commands/imagine.md +126 -0
  18. package/dist/.claude/commands/portfolio.md +50 -0
  19. package/dist/.claude/commands/prd.md +113 -0
  20. package/dist/.claude/commands/qa.md +107 -0
  21. package/dist/.claude/commands/review.md +151 -0
  22. package/dist/.claude/commands/security.md +100 -0
  23. package/dist/.claude/commands/test.md +96 -0
  24. package/dist/.claude/commands/thumper.md +116 -0
  25. package/dist/.claude/commands/treasury.md +100 -0
  26. package/dist/.claude/commands/ux.md +118 -0
  27. package/dist/.claude/commands/vault.md +189 -0
  28. package/dist/.claude/commands/void.md +108 -0
  29. package/dist/CHANGELOG.md +1918 -0
  30. package/dist/CLAUDE.md +250 -0
  31. package/dist/HOLOCRON.md +856 -0
  32. package/dist/VERSION.md +123 -0
  33. package/dist/docs/NAMING_REGISTRY.md +478 -0
  34. package/dist/docs/methods/AI_INTELLIGENCE.md +276 -0
  35. package/dist/docs/methods/ASSEMBLER.md +142 -0
  36. package/dist/docs/methods/BACKEND_ENGINEER.md +165 -0
  37. package/dist/docs/methods/BUILD_JOURNAL.md +185 -0
  38. package/dist/docs/methods/BUILD_PROTOCOL.md +426 -0
  39. package/dist/docs/methods/CAMPAIGN.md +568 -0
  40. package/dist/docs/methods/CONTEXT_MANAGEMENT.md +189 -0
  41. package/dist/docs/methods/DEEP_CURRENT.md +184 -0
  42. package/dist/docs/methods/DEVOPS_ENGINEER.md +295 -0
  43. package/dist/docs/methods/FIELD_MEDIC.md +261 -0
  44. package/dist/docs/methods/FORGE_ARTIST.md +108 -0
  45. package/dist/docs/methods/FORGE_KEEPER.md +268 -0
  46. package/dist/docs/methods/GAUNTLET.md +344 -0
  47. package/dist/docs/methods/GROWTH_STRATEGIST.md +466 -0
  48. package/dist/docs/methods/HEARTBEAT.md +168 -0
  49. package/dist/docs/methods/MCP_INTEGRATION.md +139 -0
  50. package/dist/docs/methods/MUSTER.md +148 -0
  51. package/dist/docs/methods/PRD_GENERATOR.md +186 -0
  52. package/dist/docs/methods/PRODUCT_DESIGN_FRONTEND.md +250 -0
  53. package/dist/docs/methods/QA_ENGINEER.md +337 -0
  54. package/dist/docs/methods/RELEASE_MANAGER.md +145 -0
  55. package/dist/docs/methods/SECURITY_AUDITOR.md +320 -0
  56. package/dist/docs/methods/SUB_AGENTS.md +335 -0
  57. package/dist/docs/methods/SYSTEMS_ARCHITECT.md +171 -0
  58. package/dist/docs/methods/TESTING.md +359 -0
  59. package/dist/docs/methods/THUMPER.md +175 -0
  60. package/dist/docs/methods/TIME_VAULT.md +120 -0
  61. package/dist/docs/methods/TREASURY.md +184 -0
  62. package/dist/docs/methods/TROUBLESHOOTING.md +265 -0
  63. package/dist/docs/patterns/README.md +52 -0
  64. package/dist/docs/patterns/ad-billing-adapter.ts +537 -0
  65. package/dist/docs/patterns/ad-platform-adapter.ts +421 -0
  66. package/dist/docs/patterns/ai-classifier.ts +195 -0
  67. package/dist/docs/patterns/ai-eval.ts +272 -0
  68. package/dist/docs/patterns/ai-orchestrator.ts +341 -0
  69. package/dist/docs/patterns/ai-router.ts +194 -0
  70. package/dist/docs/patterns/ai-tool-schema.ts +237 -0
  71. package/dist/docs/patterns/api-route.ts +241 -0
  72. package/dist/docs/patterns/backtest-engine.ts +499 -0
  73. package/dist/docs/patterns/browser-review.ts +292 -0
  74. package/dist/docs/patterns/combobox.tsx +300 -0
  75. package/dist/docs/patterns/component.tsx +262 -0
  76. package/dist/docs/patterns/daemon-process.ts +338 -0
  77. package/dist/docs/patterns/data-pipeline.ts +297 -0
  78. package/dist/docs/patterns/database-migration.ts +466 -0
  79. package/dist/docs/patterns/e2e-test.ts +629 -0
  80. package/dist/docs/patterns/error-handling.ts +312 -0
  81. package/dist/docs/patterns/execution-safety.ts +601 -0
  82. package/dist/docs/patterns/financial-transaction.ts +342 -0
  83. package/dist/docs/patterns/funding-plan.ts +462 -0
  84. package/dist/docs/patterns/game-entity.ts +137 -0
  85. package/dist/docs/patterns/game-loop.ts +113 -0
  86. package/dist/docs/patterns/game-state.ts +143 -0
  87. package/dist/docs/patterns/job-queue.ts +225 -0
  88. package/dist/docs/patterns/kongo-integration.ts +164 -0
  89. package/dist/docs/patterns/middleware.ts +363 -0
  90. package/dist/docs/patterns/mobile-screen.tsx +139 -0
  91. package/dist/docs/patterns/mobile-service.ts +167 -0
  92. package/dist/docs/patterns/multi-tenant.ts +382 -0
  93. package/dist/docs/patterns/oauth-token-lifecycle.ts +223 -0
  94. package/dist/docs/patterns/outbound-rate-limiter.ts +260 -0
  95. package/dist/docs/patterns/prompt-template.ts +195 -0
  96. package/dist/docs/patterns/revenue-source-adapter.ts +311 -0
  97. package/dist/docs/patterns/service.ts +224 -0
  98. package/dist/docs/patterns/sse-endpoint.ts +118 -0
  99. package/dist/docs/patterns/stablecoin-adapter.ts +511 -0
  100. package/dist/docs/patterns/third-party-script.ts +68 -0
  101. package/dist/scripts/thumper/gom-jabbar.sh +241 -0
  102. package/dist/scripts/thumper/relay.sh +610 -0
  103. package/dist/scripts/thumper/scan.sh +359 -0
  104. package/dist/scripts/thumper/thumper.sh +190 -0
  105. package/dist/scripts/thumper/water-rings.sh +76 -0
  106. package/dist/wizard/ui/index.html +1 -1
  107. package/package.json +1 -1
  108. package/dist/tsconfig.tsbuildinfo +0 -1
@@ -0,0 +1,262 @@
1
+ /**
2
+ * Pattern: React Component with All States
3
+ *
4
+ * Key principles:
5
+ * - Every component handles: loading, empty, error, and success states
6
+ * - Keyboard accessible — interactive elements focusable, no mouse-only actions
7
+ * - Semantic HTML — use the right element, not div-with-onClick
8
+ * - Loading states prevent layout shift (skeleton matches content shape)
9
+ * - Error states are actionable — tell the user what to do
10
+ * - Empty states guide — never just show blank space
11
+ *
12
+ * Agents: Legolas (code), Samwise (a11y), Bilbo (copy), Gimli (performance)
13
+ *
14
+ * Framework adaptations:
15
+ * Next.js/React: This file (JSX, hooks, 'use client')
16
+ * Vue: Same 4-state pattern with v-if/v-else, <template> blocks
17
+ * Svelte: Same pattern with {#if}/{:else} blocks
18
+ * Django templates: Conditional blocks for each state, HTMX for loading
19
+ * Rails: Turbo Frames for loading states, partials for each state
20
+ *
21
+ * === Django Templates + HTMX Deep Dive ===
22
+ *
23
+ * {# templates/projects/list.html — same 4-state pattern, no JS framework #}
24
+ * {% if loading %}
25
+ * <div role="status" aria-label="Loading projects">
26
+ * {% for _ in "123" %}<div class="skeleton h-16 rounded-lg bg-gray-100 animate-pulse"></div>{% endfor %}
27
+ * </div>
28
+ * {% elif error %}
29
+ * <div role="alert" class="border border-red-200 bg-red-50 p-4">
30
+ * <p>Couldn't load projects: {{ error }}</p>
31
+ * <button hx-get="{% url 'project-list' %}" hx-target="#project-list">Try again</button>
32
+ * </div>
33
+ * {% elif not projects %}
34
+ * <div class="border-dashed p-8 text-center">No projects yet.</div>
35
+ * {% else %}
36
+ * {% for project in projects %}{% include "projects/_card.html" %}{% endfor %}
37
+ * {% endif %}
38
+ *
39
+ * {# HTMX: loading states via hx-indicator, partial swaps via hx-target #}
40
+ * {# Keyboard: ensure HTMX-swapped content gets focus management via hx-on::after-swap #}
41
+ *
42
+ * === FastAPI + Jinja2 + HTMX Deep Dive ===
43
+ *
44
+ * Same template pattern as Django. FastAPI serves templates via:
45
+ * from fastapi.templating import Jinja2Templates
46
+ * templates = Jinja2Templates(directory="templates")
47
+ *
48
+ * For SPA frontends: FastAPI serves JSON API, React/Vue/Svelte handles the 4 states.
49
+ * For HTMX: same pattern as Django templates above, endpoint returns HTML fragments.
50
+ *
51
+ * The framework changes, the principle doesn't:
52
+ * EVERY data-driven component handles loading, empty, error, and success.
53
+ *
54
+ * === useEffect + Store Interaction Rules ===
55
+ *
56
+ * ANTI-PATTERN: useEffect that depends on a store value it indirectly modifies
57
+ * useEffect(() => {
58
+ * loadData(id); // calls store.set() which changes currentData
59
+ * }, [currentData]); // currentData changes → effect fires → infinite loop
60
+ *
61
+ * CORRECT: depend on the primitive trigger, not the store result
62
+ * useEffect(() => {
63
+ * if (id) loadData(id);
64
+ * }, [id]); // URL param triggers load, not the store value
65
+ *
66
+ * CORRECT: ref guard for load-once effects
67
+ * const loaded = useRef(false);
68
+ * useEffect(() => {
69
+ * if (loaded.current) return;
70
+ * loaded.current = true;
71
+ * loadData(id);
72
+ * }, []);
73
+ *
74
+ * RULE: For every useEffect with store values in its dependency array,
75
+ * verify the effect body does NOT trigger a store update that changes
76
+ * those same dependencies. If it does → infinite render loop.
77
+ *
78
+ * RULE: Never call panelRef.current?.focus() in a useEffect without a
79
+ * ref guard. Arrow function callbacks in parent components change identity
80
+ * on every render, causing the effect to re-run and steal focus.
81
+ *
82
+ * === Collapsible / Accordion Pattern ===
83
+ *
84
+ * When building any expand/collapse UI (accordions, dropdowns, FAQ sections,
85
+ * expandable cards), this ARIA checklist is required:
86
+ *
87
+ * <button
88
+ * type="button" // Not "submit" — prevents form submission
89
+ * aria-expanded={isOpen} // Announces open/closed state
90
+ * aria-controls={contentId} // Links trigger to content panel
91
+ * className="focus-visible:ring-2" // Keyboard focus indicator
92
+ * onClick={() => setIsOpen(!isOpen)}
93
+ * >
94
+ * Section Title
95
+ * </button>
96
+ * {isOpen && (
97
+ * <div id={contentId}> // Must match aria-controls value
98
+ * Panel content
99
+ * </div>
100
+ * )}
101
+ *
102
+ * Use useId() (React 18+) or a stable ID generator for contentId.
103
+ * Keyboard: Enter/Space toggles. This is automatic with <button>.
104
+ *
105
+ * Common mistake: adding aria-expanded but forgetting aria-controls + id.
106
+ * The three are a unit — aria-expanded alone tells screen readers the
107
+ * state but not WHAT content it controls.
108
+ * (Field report #43: custom accordions had aria-expanded but no
109
+ * aria-controls or content id — a11y violation caught in review.)
110
+ */
111
+
112
+ // ── Portal Pattern (iframe-safe overlays) ────────────
113
+ // Any dropdown, popover, modal, or tooltip that must render ABOVE an iframe
114
+ // MUST use createPortal to document.body.
115
+ //
116
+ // Why: Iframes with allow-same-origin create impenetrable stacking contexts.
117
+ // z-index alone will NOT work — the iframe's stacking context always wins.
118
+ //
119
+ // import { createPortal } from 'react-dom';
120
+ //
121
+ // function Dropdown({ children, isOpen }: { children: React.ReactNode; isOpen: boolean }) {
122
+ // if (!isOpen) return null;
123
+ // return createPortal(
124
+ // <div style={{ position: 'fixed', zIndex: 9999 }}>{children}</div>,
125
+ // document.body
126
+ // );
127
+ // }
128
+ //
129
+ // Django/HTMX equivalent: render the overlay in a top-level <div> outside the iframe container.
130
+ // (Field report #79: iframe stacking context defeated z-index on map overlay)
131
+
132
+ 'use client'
133
+
134
+ import { useState } from 'react'
135
+
136
+ // --- Types co-located with component ---
137
+ interface Project {
138
+ id: string
139
+ name: string
140
+ description: string | null
141
+ createdAt: string
142
+ }
143
+
144
+ interface ProjectListProps {
145
+ projects: Project[]
146
+ isLoading: boolean
147
+ error: string | null
148
+ onDelete: (id: string) => void
149
+ onRetry: () => void
150
+ }
151
+
152
+ // --- Main component ---
153
+ export function ProjectList({
154
+ projects,
155
+ isLoading,
156
+ error,
157
+ onDelete,
158
+ onRetry,
159
+ }: ProjectListProps) {
160
+ // Loading state — skeleton matches content shape (Gimli — no layout shift)
161
+ if (isLoading) {
162
+ return (
163
+ <div role="status" aria-label="Loading projects">
164
+ <ul className="space-y-3">
165
+ {Array.from({ length: 3 }).map((_, i) => (
166
+ <li key={i} className="h-16 rounded-lg bg-gray-100 animate-pulse" />
167
+ ))}
168
+ </ul>
169
+ <span className="sr-only">Loading projects...</span>
170
+ </div>
171
+ )
172
+ }
173
+
174
+ // Error state — actionable, not just "something went wrong" (Bilbo — clear copy)
175
+ if (error) {
176
+ return (
177
+ <div role="alert" className="rounded-lg border border-red-200 bg-red-50 p-4">
178
+ <p className="font-medium text-red-800">Couldn't load your projects</p>
179
+ <p className="mt-1 text-sm text-red-600">{error}</p>
180
+ <button
181
+ onClick={onRetry}
182
+ className="mt-3 text-sm font-medium text-red-700 underline hover:text-red-800"
183
+ >
184
+ Try again
185
+ </button>
186
+ </div>
187
+ )
188
+ }
189
+
190
+ // Empty state — guide the user, don't show blank (Bilbo — helpful copy)
191
+ if (projects.length === 0) {
192
+ return (
193
+ <div className="rounded-lg border border-dashed border-gray-300 p-8 text-center">
194
+ <p className="font-medium text-gray-900">No projects yet</p>
195
+ <p className="mt-1 text-sm text-gray-500">
196
+ Create your first project to get started.
197
+ </p>
198
+ </div>
199
+ )
200
+ }
201
+
202
+ // Success state
203
+ return (
204
+ <ul className="space-y-3" role="list">
205
+ {projects.map((project) => (
206
+ <ProjectCard key={project.id} project={project} onDelete={onDelete} />
207
+ ))}
208
+ </ul>
209
+ )
210
+ }
211
+
212
+ // --- Sub-component ---
213
+ function ProjectCard({
214
+ project,
215
+ onDelete,
216
+ }: {
217
+ project: Project
218
+ onDelete: (id: string) => void
219
+ }) {
220
+ const [confirmDelete, setConfirmDelete] = useState(false)
221
+
222
+ return (
223
+ <li className="flex items-center justify-between rounded-lg border p-4">
224
+ <div>
225
+ <h3 className="font-medium text-gray-900">{project.name}</h3>
226
+ {project.description && (
227
+ <p className="mt-0.5 text-sm text-gray-500">{project.description}</p>
228
+ )}
229
+ </div>
230
+
231
+ {/* Dangerous action requires confirmation (Radagast — safety) */}
232
+ {confirmDelete ? (
233
+ <div className="flex gap-2" role="group" aria-label="Confirm deletion">
234
+ <button
235
+ onClick={() => {
236
+ onDelete(project.id)
237
+ setConfirmDelete(false)
238
+ }}
239
+ className="rounded bg-red-600 px-3 py-1.5 text-sm font-medium text-white hover:bg-red-700"
240
+ aria-label={`Confirm delete ${project.name}`}
241
+ >
242
+ Delete
243
+ </button>
244
+ <button
245
+ onClick={() => setConfirmDelete(false)}
246
+ className="rounded border px-3 py-1.5 text-sm font-medium text-gray-700 hover:bg-gray-50"
247
+ >
248
+ Cancel
249
+ </button>
250
+ </div>
251
+ ) : (
252
+ <button
253
+ onClick={() => setConfirmDelete(true)}
254
+ className="rounded border px-3 py-1.5 text-sm font-medium text-gray-700 hover:bg-gray-50"
255
+ aria-label={`Delete ${project.name}`}
256
+ >
257
+ Delete
258
+ </button>
259
+ )}
260
+ </li>
261
+ )
262
+ }
@@ -0,0 +1,338 @@
1
+ /**
2
+ * Pattern: Daemon Process (Heartbeat)
3
+ *
4
+ * Key principles:
5
+ * - PID file with flock to prevent concurrent instances
6
+ * - Unix domain socket for IPC (ADR-1: single writer)
7
+ * - Session token auth (rotated every 24 hours)
8
+ * - Graceful shutdown on SIGTERM (10s deadline)
9
+ * - Sleep/wake recovery with tiered catch-up
10
+ * - Core dumps disabled for processes holding vault keys
11
+ * - Log rotation: daily or at 10MB, retain 7 days
12
+ * - macOS: F_FULLFSYNC for financial files
13
+ *
14
+ * Agents: Dockson (treasury), Heartbeat daemon
15
+ *
16
+ * PRD Reference: §9.7, §9.18, §9.19.2, §9.20.11
17
+ */
18
+
19
+ import { createServer } from 'node:net';
20
+ import { randomBytes } from 'node:crypto';
21
+ import { writeFile, readFile, unlink, mkdir, open, rename } from 'node:fs/promises';
22
+ import { existsSync, createWriteStream } from 'node:fs';
23
+ import { join } from 'node:path';
24
+ import { homedir, platform } from 'node:os';
25
+
26
+ const VOIDFORGE_DIR = join(homedir(), '.voidforge');
27
+ const RUN_DIR = join(VOIDFORGE_DIR, 'run');
28
+ const PID_FILE = join(RUN_DIR, 'heartbeat.pid');
29
+ const SOCKET_PATH = join(RUN_DIR, 'heartbeat.sock');
30
+ const TOKEN_FILE = join(VOIDFORGE_DIR, 'heartbeat.token');
31
+ const STATE_FILE = join(VOIDFORGE_DIR, 'heartbeat.json');
32
+ const LOG_FILE = join(VOIDFORGE_DIR, 'heartbeat.log');
33
+
34
+ type DaemonState = 'starting' | 'healthy' | 'degraded' | 'recovering' | 'recovery_failed' | 'shutting_down' | 'stopped';
35
+
36
+ interface HeartbeatState {
37
+ pid: number;
38
+ state: DaemonState;
39
+ startedAt: string;
40
+ lastHeartbeat: string;
41
+ lastEventId: number;
42
+ cultivationState: string;
43
+ activePlatforms: string[];
44
+ activeCampaigns: number;
45
+ todaySpend: number; // Cents
46
+ dailyBudget: number; // Cents
47
+ alerts: string[];
48
+ tokenHealth: Record<string, { status: string; expiresAt: string }>;
49
+ lastAgentMessage?: { agent: string; text: string; timestamp: string };
50
+ // Treasury state (v19.0 — present when stablecoin funding is configured)
51
+ stablecoinBalanceCents?: number;
52
+ bankBalanceCents?: number;
53
+ runwayDays?: number;
54
+ fundingFrozen?: boolean;
55
+ pendingTransferCount?: number;
56
+ }
57
+
58
+ // ── PID Management ────────────────────────────────────
59
+
60
+ async function writePidFile(): Promise<void> {
61
+ await mkdir(RUN_DIR, { recursive: true, mode: 0o700 });
62
+ const fh = await open(PID_FILE, 'w', 0o600);
63
+ try {
64
+ await fh.writeFile(String(process.pid));
65
+ await fh.sync();
66
+ } finally {
67
+ await fh.close();
68
+ }
69
+ }
70
+
71
+ async function checkStalePid(): Promise<boolean> {
72
+ if (!existsSync(PID_FILE)) return false;
73
+ try {
74
+ const pid = parseInt(await readFile(PID_FILE, 'utf-8'));
75
+ process.kill(pid, 0); // Check if process exists (throws if not)
76
+ return true; // Another daemon is alive
77
+ } catch {
78
+ await unlink(PID_FILE).catch(() => {});
79
+ return false; // Stale PID — cleaned up
80
+ }
81
+ }
82
+
83
+ async function removePidFile(): Promise<void> {
84
+ await unlink(PID_FILE).catch(() => {});
85
+ }
86
+
87
+ // ── Session Token ─────────────────────────────────────
88
+ // Rotated every 24 hours (§9.19.15)
89
+
90
+ async function generateSessionToken(): Promise<string> {
91
+ const token = randomBytes(32).toString('hex');
92
+ await mkdir(RUN_DIR, { recursive: true, mode: 0o700 });
93
+ const tmpPath = TOKEN_FILE + '.tmp.' + process.pid;
94
+ const fh = await open(tmpPath, 'w', 0o600);
95
+ try {
96
+ await fh.writeFile(token);
97
+ await fh.sync();
98
+ } finally {
99
+ await fh.close();
100
+ }
101
+ await rename(tmpPath, TOKEN_FILE);
102
+ return token;
103
+ }
104
+
105
+ function validateToken(provided: string, expected: string): boolean {
106
+ if (provided.length !== expected.length) return false;
107
+ const a = Buffer.from(provided);
108
+ const b = Buffer.from(expected);
109
+ const { timingSafeEqual } = require('node:crypto');
110
+ return timingSafeEqual(a, b);
111
+ }
112
+
113
+ // ── Socket Server ─────────────────────────────────────
114
+ // JSON-over-HTTP on Unix domain socket (§9.20.11)
115
+
116
+ function createSocketServer(
117
+ sessionToken: string,
118
+ handleRequest: (method: string, path: string, body: unknown, auth: { hasToken: boolean; vaultPassword: string; totpCode: string }) => Promise<{ status: number; body: unknown }>
119
+ ): ReturnType<typeof createServer> {
120
+ const MAX_REQUEST_SIZE = 1024 * 1024; // 1MB — R2-NIGHTWING-003: enforce during streaming
121
+
122
+ const server = createServer(async (conn) => {
123
+ let data = '';
124
+ let rejected = false;
125
+ conn.on('data', (chunk) => {
126
+ data += chunk.toString();
127
+ if (data.length > MAX_REQUEST_SIZE && !rejected) {
128
+ rejected = true;
129
+ conn.write('HTTP/1.1 413 Payload Too Large\r\nContent-Type: application/json\r\n\r\n{"ok":false,"error":"Request too large"}');
130
+ conn.end();
131
+ }
132
+ });
133
+ conn.on('end', async () => {
134
+ try {
135
+ // Parse HTTP-like request from the socket
136
+ const lines = data.split('\r\n');
137
+ const [method, path] = (lines[0] || 'GET /').split(' ');
138
+
139
+ // Extract auth headers — pass VALUES, not just presence (SEC-001 fix)
140
+ const authHeader = lines.find(l => l.toLowerCase().startsWith('authorization:'));
141
+ const token = authHeader ? authHeader.split(' ').pop() || '' : '';
142
+ const hasToken = validateToken(token, sessionToken);
143
+
144
+ const vaultHeader = lines.find(l => l.toLowerCase().startsWith('x-vault-password:'));
145
+ const vaultPassword = vaultHeader ? vaultHeader.substring(vaultHeader.indexOf(':') + 1).trim() : '';
146
+
147
+ const totpHeader = lines.find(l => l.toLowerCase().startsWith('x-totp-code:'));
148
+ const totpCode = totpHeader ? totpHeader.substring(totpHeader.indexOf(':') + 1).trim() : '';
149
+
150
+ // Parse body (after blank line)
151
+ const bodyStart = data.indexOf('\r\n\r\n');
152
+
153
+ if (rejected) return; // Already rejected during streaming
154
+
155
+ const body = bodyStart >= 0 ? JSON.parse(data.substring(bodyStart + 4) || '{}') : {};
156
+
157
+ const result = await handleRequest(method, path, body, {
158
+ hasToken,
159
+ vaultPassword, // Actual value for verification by handler
160
+ totpCode, // Actual value for verification by handler
161
+ });
162
+
163
+ conn.write(`HTTP/1.1 ${result.status} OK\r\nContent-Type: application/json\r\n\r\n${JSON.stringify(result.body)}`);
164
+ } catch (err) {
165
+ conn.write(`HTTP/1.1 500 Error\r\nContent-Type: application/json\r\n\r\n${JSON.stringify({ ok: false, error: 'Internal error' })}`);
166
+ }
167
+ conn.end();
168
+ });
169
+ });
170
+
171
+ return server;
172
+ }
173
+
174
+ async function startSocketServer(server: ReturnType<typeof createServer>): Promise<void> {
175
+ // Clean up stale socket
176
+ if (existsSync(SOCKET_PATH)) {
177
+ try {
178
+ const { connect } = require('node:net');
179
+ const testConn = connect(SOCKET_PATH);
180
+ await new Promise<void>((resolve, reject) => {
181
+ testConn.on('connect', () => { testConn.destroy(); reject(new Error('Another daemon is running')); });
182
+ testConn.on('error', () => { resolve(); }); // ECONNREFUSED = stale socket
183
+ });
184
+ await unlink(SOCKET_PATH);
185
+ } catch (err) {
186
+ if ((err as Error).message === 'Another daemon is running') throw err;
187
+ }
188
+ }
189
+
190
+ await new Promise<void>((resolve, reject) => {
191
+ server.listen(SOCKET_PATH, () => {
192
+ // Set socket permissions (§9.18)
193
+ require('node:fs').chmodSync(SOCKET_PATH, 0o600);
194
+ resolve();
195
+ });
196
+ server.on('error', reject);
197
+ });
198
+ }
199
+
200
+ // ── State File ────────────────────────────────────────
201
+
202
+ async function writeState(state: HeartbeatState): Promise<void> {
203
+ const tmpPath = STATE_FILE + '.tmp.' + process.pid;
204
+ const fh = await open(tmpPath, 'w');
205
+ try {
206
+ await fh.writeFile(JSON.stringify(state, null, 2));
207
+ if (platform() === 'darwin') {
208
+ await fh.datasync(); // Best-effort on macOS; see §9.18 F_FULLFSYNC caveat
209
+ } else {
210
+ await fh.sync();
211
+ }
212
+ } finally {
213
+ await fh.close();
214
+ }
215
+ await rename(tmpPath, STATE_FILE);
216
+ }
217
+
218
+ // ── Signal Handling ───────────────────────────────────
219
+
220
+ function setupSignalHandlers(
221
+ cleanup: () => Promise<void>,
222
+ server: ReturnType<typeof createServer>
223
+ ): void {
224
+ let shuttingDown = false;
225
+
226
+ async function shutdown(signal: string): Promise<void> {
227
+ if (shuttingDown) return;
228
+ shuttingDown = true;
229
+
230
+ // 10-second deadline for in-flight requests
231
+ const deadline = setTimeout(() => {
232
+ process.exit(1);
233
+ }, 10000);
234
+
235
+ try {
236
+ server.close();
237
+ await cleanup();
238
+ await removePidFile();
239
+ await unlink(SOCKET_PATH).catch(() => {});
240
+ clearTimeout(deadline);
241
+ process.exit(0);
242
+ } catch {
243
+ clearTimeout(deadline);
244
+ process.exit(1);
245
+ }
246
+ }
247
+
248
+ process.on('SIGTERM', () => shutdown('SIGTERM'));
249
+ process.on('SIGINT', () => shutdown('SIGINT'));
250
+
251
+ // Disable core dumps — vault key in memory (§9.18)
252
+ try {
253
+ // @ts-expect-error — setrlimit not in Node.js types
254
+ if (process.setrlimit) process.setrlimit('core', { soft: 0, hard: 0 });
255
+ } catch { /* not available on all platforms */ }
256
+ }
257
+
258
+ // ── Job Scheduler ─────────────────────────────────────
259
+
260
+ interface ScheduledJob {
261
+ name: string;
262
+ intervalMs: number;
263
+ handler: () => Promise<void>;
264
+ lastRun: number;
265
+ }
266
+
267
+ class JobScheduler {
268
+ private jobs: ScheduledJob[] = [];
269
+ private timer: ReturnType<typeof setInterval> | null = null;
270
+ private lastTick: number = Date.now();
271
+
272
+ add(name: string, intervalMs: number, handler: () => Promise<void>): void {
273
+ this.jobs.push({ name, intervalMs, handler, lastRun: 0 });
274
+ }
275
+
276
+ start(): void {
277
+ this.lastTick = Date.now();
278
+ this.timer = setInterval(() => this.tick(), 1000);
279
+ }
280
+
281
+ stop(): void {
282
+ if (this.timer) clearInterval(this.timer);
283
+ }
284
+
285
+ private async tick(): Promise<void> {
286
+ const now = Date.now();
287
+ const delta = now - this.lastTick;
288
+ this.lastTick = now;
289
+
290
+ // Sleep/wake detection (§9.18): if delta > 2x expected (2s), catch-up mode
291
+ if (delta > 2000) {
292
+ // Stagger overdue jobs over 5 minutes, prioritize token refresh
293
+ const overdue = this.jobs.filter(j => now - j.lastRun > j.intervalMs);
294
+ const sorted = overdue.sort((a, b) => {
295
+ if (a.name.includes('token')) return -1;
296
+ if (b.name.includes('token')) return 1;
297
+ return 0;
298
+ });
299
+ for (const job of sorted) {
300
+ try { await job.handler(); } catch { /* logged by handler */ }
301
+ job.lastRun = now;
302
+ }
303
+ return;
304
+ }
305
+
306
+ // Normal tick: run due jobs
307
+ for (const job of this.jobs) {
308
+ if (now - job.lastRun >= job.intervalMs) {
309
+ try { await job.handler(); } catch { /* logged by handler */ }
310
+ job.lastRun = now;
311
+ }
312
+ }
313
+ }
314
+ }
315
+
316
+ // ── Log Rotation ──────────────────────────────────────
317
+
318
+ function createLogger(logPath: string): { log: (msg: string) => void; close: () => void } {
319
+ let stream = createWriteStream(logPath, { flags: 'a' });
320
+ return {
321
+ log(msg: string) {
322
+ stream.write(JSON.stringify({ ts: new Date().toISOString(), msg }) + '\n');
323
+ },
324
+ close() { stream.end(); }
325
+ };
326
+ }
327
+
328
+ export {
329
+ writePidFile, checkStalePid, removePidFile,
330
+ generateSessionToken, validateToken,
331
+ createSocketServer, startSocketServer,
332
+ writeState,
333
+ setupSignalHandlers,
334
+ JobScheduler,
335
+ createLogger,
336
+ PID_FILE, SOCKET_PATH, TOKEN_FILE, STATE_FILE, LOG_FILE,
337
+ };
338
+ export type { DaemonState, HeartbeatState, ScheduledJob };