beehaven 0.8.16
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/LICENSE +21 -0
- package/README.md +201 -0
- package/dist/__tests__/_cli-switch.js +105 -0
- package/dist/__tests__/agent-wrap-probe.js +136 -0
- package/dist/agent-attribution.js +192 -0
- package/dist/agent-identity-daemon.js +135 -0
- package/dist/agent-onboard.js +156 -0
- package/dist/agent-scope.js +79 -0
- package/dist/agent-sessions.js +370 -0
- package/dist/agent-state.js +482 -0
- package/dist/app-manifest.js +221 -0
- package/dist/app-signer.js +54 -0
- package/dist/app-signing.js +127 -0
- package/dist/audio-play.js +98 -0
- package/dist/avatar-protocol.js +25 -0
- package/dist/avatar-route.js +79 -0
- package/dist/bee/cloud-proxy.js +130 -0
- package/dist/blocked-actions.js +120 -0
- package/dist/build-channel.js +101 -0
- package/dist/buildings/daemon-workspace.js +475 -0
- package/dist/claude-bridge.js +310 -0
- package/dist/claude-credential-store.js +633 -0
- package/dist/clearly-sync.js +518 -0
- package/dist/clearly.js +616 -0
- package/dist/cli-state.js +334 -0
- package/dist/cli.js +6973 -0
- package/dist/commands/agent-skills.js +302 -0
- package/dist/commands/agent.js +1310 -0
- package/dist/commands/app.js +1123 -0
- package/dist/commands/auth.js +171 -0
- package/dist/commands/building-shell.js +392 -0
- package/dist/commands/canvas.js +932 -0
- package/dist/commands/claude.js +218 -0
- package/dist/commands/compliance.js +387 -0
- package/dist/commands/compose.js +507 -0
- package/dist/commands/composition.js +95 -0
- package/dist/commands/connect-session.js +409 -0
- package/dist/commands/conversations.js +160 -0
- package/dist/commands/costs.js +443 -0
- package/dist/commands/d1.js +253 -0
- package/dist/commands/daemon-cmd.js +162 -0
- package/dist/commands/deploy.js +164 -0
- package/dist/commands/design.js +617 -0
- package/dist/commands/dev-generate.js +125 -0
- package/dist/commands/dev.js +109 -0
- package/dist/commands/devices.js +135 -0
- package/dist/commands/explain-router.js +24 -0
- package/dist/commands/explain.js +882 -0
- package/dist/commands/explore.js +264 -0
- package/dist/commands/help.js +2418 -0
- package/dist/commands/init.js +408 -0
- package/dist/commands/install.js +129 -0
- package/dist/commands/library.js +229 -0
- package/dist/commands/marketplace.js +138 -0
- package/dist/commands/mcp-install.js +476 -0
- package/dist/commands/mention.js +257 -0
- package/dist/commands/omni.js +529 -0
- package/dist/commands/secrets.js +323 -0
- package/dist/commands/self-update.js +206 -0
- package/dist/commands/sheet.js +264 -0
- package/dist/commands/shell-output.js +309 -0
- package/dist/commands/shell.js +1048 -0
- package/dist/commands/shop.js +368 -0
- package/dist/commands/skills.js +311 -0
- package/dist/commands/slides.js +418 -0
- package/dist/commands/swarm.js +341 -0
- package/dist/commands/ticket.js +1301 -0
- package/dist/commands/tool.js +312 -0
- package/dist/commands/trash.js +106 -0
- package/dist/commands/treehouse.js +120 -0
- package/dist/commands/user.js +240 -0
- package/dist/commands/voice-tts.js +319 -0
- package/dist/commands/voice.js +166 -0
- package/dist/commands/watch-git.js +401 -0
- package/dist/commands/webhook.js +159 -0
- package/dist/commands/workspace.js +3354 -0
- package/dist/composition-repos.js +91 -0
- package/dist/daemon-avatar.js +130 -0
- package/dist/daemon-claude-code-auto-install.js +52 -0
- package/dist/daemon-client.js +363 -0
- package/dist/daemon-codex.js +349 -0
- package/dist/daemon-entry.js +101 -0
- package/dist/daemon-fetch-allowlist.js +194 -0
- package/dist/daemon-gemini.js +224 -0
- package/dist/daemon-mcp-clearly.js +353 -0
- package/dist/daemon-mcp-sandbox.js +127 -0
- package/dist/daemon-omni-company.js +338 -0
- package/dist/daemon-omni-hooks.js +375 -0
- package/dist/daemon-omni-identity.js +229 -0
- package/dist/daemon-omni-pipeline.js +251 -0
- package/dist/daemon-omni-policies.js +346 -0
- package/dist/daemon-omni-principal.js +580 -0
- package/dist/daemon-omni-tool-gate.js +116 -0
- package/dist/daemon-omni.js +1442 -0
- package/dist/daemon-path-transform.js +120 -0
- package/dist/daemon-write-sandbox.js +191 -0
- package/dist/daemon.js +4471 -0
- package/dist/dashboard.js +846 -0
- package/dist/dep-installer.js +286 -0
- package/dist/dev-mode.js +67 -0
- package/dist/doc-indexer.js +169 -0
- package/dist/e2e-crypto.js +94 -0
- package/dist/feature-flags.js +154 -0
- package/dist/file-browser.js +88 -0
- package/dist/first-party-hives.js +211 -0
- package/dist/folder-sync.js +226 -0
- package/dist/health.js +56 -0
- package/dist/ipc-auth.js +299 -0
- package/dist/ipc-client.js +495 -0
- package/dist/ipc-server.js +684 -0
- package/dist/keychain.js +224 -0
- package/dist/lib/admin-client.js +73 -0
- package/dist/lib/mobile-build-parser.js +156 -0
- package/dist/local-app-manager.js +660 -0
- package/dist/log-redaction.js +89 -0
- package/dist/logger.js +108 -0
- package/dist/manifest.js +143 -0
- package/dist/marketplace.js +793 -0
- package/dist/min-version-copy.js +51 -0
- package/dist/min-version.js +214 -0
- package/dist/public-surface.js +23 -0
- package/dist/relay-presets.js +89 -0
- package/dist/relay.js +2723 -0
- package/dist/repl.js +619 -0
- package/dist/setup-relay.js +363 -0
- package/dist/setup-service.js +536 -0
- package/dist/shared/bee-memory/index.js +6 -0
- package/dist/shared/bee-memory/types.js +682 -0
- package/dist/shared/claude-code/daemon-types.js +112 -0
- package/dist/shared/claude-code/index.js +2 -0
- package/dist/shared/claude-code/types.js +1 -0
- package/dist/shared/status/schema.js +133 -0
- package/dist/shell-session.js +198 -0
- package/dist/speak-lock.js +97 -0
- package/dist/spinner.js +57 -0
- package/dist/telemetry.js +166 -0
- package/dist/terminal/block-parser.js +207 -0
- package/dist/terminal/shell-integration.js +108 -0
- package/dist/transcript-tailer.js +215 -0
- package/dist/tts-qwen3.js +547 -0
- package/dist/types.js +4 -0
- package/dist/utm.js +38 -0
- package/dist/verb-surface.js +331 -0
- package/dist/version-utils.js +85 -0
- package/dist/watcher.js +207 -0
- package/package.json +88 -0
- package/python/qwen3_tts.py +230 -0
- package/scripts/stamp-build-channel.mjs +115 -0
- package/scripts/sync-installed.mjs +159 -0
- package/scripts/sync-skills.mjs +112 -0
- package/skills/README.md +24 -0
- package/skills/agent-doodle-space/SKILL.md +118 -0
- package/skills/agent-home/SKILL.md +98 -0
- package/skills/beehaven-cli/SKILL.md +144 -0
- package/skills/claude-shopper/README.md +90 -0
- package/skills/claude-shopper/SKILL.md +151 -0
- package/skills/clearly-agent/SKILL.md +183 -0
- package/skills/clearly-canvas/SKILL.md +222 -0
- package/skills/clearly-canvas/authoring.md +160 -0
- package/skills/clearly-canvas/communications.md +32 -0
- package/skills/clearly-canvas/composition-patterns.md +84 -0
- package/skills/clearly-canvas/example.mjs +78 -0
- package/skills/clearly-canvas/project-plans.md +156 -0
- package/skills/clearly-canvas/review.mjs +68 -0
- package/skills/clearly-workspace/SKILL.md +79 -0
- package/skills/clearly-workspace/survey.mjs +101 -0
- package/skills/codebase-map/SKILL.md +142 -0
- package/skills/design-craft/SKILL.md +300 -0
- package/skills/index.json +79 -0
- package/skills/layout-systems/SKILL.md +184 -0
- package/skills/pair-on-canvas/SKILL.md +92 -0
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `beehaven claude` — LOCAL, single-user Claude Code via the Claude Agent SDK,
|
|
3
|
+
* authenticated by the user's OWN Claude subscription (local OAuth).
|
|
4
|
+
*
|
|
5
|
+
* Restored from the pre-"destroy-the-treehouse" daemon integration but wired
|
|
6
|
+
* LOCAL-ONLY: the SDK `query()` runs in THIS process, reading the user's OAuth
|
|
7
|
+
* from `~/.claude/.credentials.json` (written by `claude setup-token`). The
|
|
8
|
+
* cloud→daemon orchestration path — disabled for Anthropic credential-use-policy
|
|
9
|
+
* compliance (see relay.ts) — stays OFF; nothing here syncs a token to the cloud
|
|
10
|
+
* or lets a remote drive Claude. It's your machine, your subscription, your turns.
|
|
11
|
+
*
|
|
12
|
+
* Subcommands:
|
|
13
|
+
* login run `claude setup-token` (browser OAuth) to authorize the SDK
|
|
14
|
+
* status show which credential is active (plan / api-key), masked
|
|
15
|
+
* logout remove the beehaven-stored credential copy
|
|
16
|
+
* run "<task>" run one Claude turn in cwd (streamed) — also the default:
|
|
17
|
+
* "<task>" `beehaven claude "refactor auth.ts"`
|
|
18
|
+
*
|
|
19
|
+
* Flags (run): --composition <id> --cwd <dir> --model <m> --continue --resume <sid>
|
|
20
|
+
*/
|
|
21
|
+
import { startPlanOauth, readClaudeCredential, deleteClaudeCredential, findClaudeBinary, } from '../claude-credential-store.js';
|
|
22
|
+
import { promptOmni, subscribe, endSession } from '../daemon-omni.js';
|
|
23
|
+
import { withDevMode } from '../dev-mode.js';
|
|
24
|
+
import { userInfo } from 'node:os';
|
|
25
|
+
// Tiny inline ANSI (spinner.ts only exports the spinner factory).
|
|
26
|
+
const c = {
|
|
27
|
+
cyan: (s) => `\x1b[36m${s}\x1b[0m`,
|
|
28
|
+
green: (s) => `\x1b[32m${s}\x1b[0m`,
|
|
29
|
+
red: (s) => `\x1b[31m${s}\x1b[0m`,
|
|
30
|
+
dim: (s) => `\x1b[2m${s}\x1b[0m`,
|
|
31
|
+
yellow: (s) => `\x1b[33m${s}\x1b[0m`,
|
|
32
|
+
};
|
|
33
|
+
export async function cmdClaude(positionals, values) {
|
|
34
|
+
const sub = positionals[1];
|
|
35
|
+
switch (sub) {
|
|
36
|
+
case 'login':
|
|
37
|
+
return claudeLogin();
|
|
38
|
+
case 'status':
|
|
39
|
+
return claudeStatus();
|
|
40
|
+
case 'logout':
|
|
41
|
+
return claudeLogout();
|
|
42
|
+
case undefined:
|
|
43
|
+
case 'help':
|
|
44
|
+
case '--help':
|
|
45
|
+
case '-h':
|
|
46
|
+
return printHelp();
|
|
47
|
+
case 'run':
|
|
48
|
+
return claudeRun(positionals.slice(2).join(' '), values);
|
|
49
|
+
default:
|
|
50
|
+
// `beehaven claude "<task>"` — everything after `claude` is the task.
|
|
51
|
+
return claudeRun(positionals.slice(1).join(' '), values);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function printHelp() {
|
|
55
|
+
console.log(`
|
|
56
|
+
${c.cyan('beehaven claude')} — local Claude Code via the Claude Agent SDK (your subscription)
|
|
57
|
+
|
|
58
|
+
${c.dim('login')} authorize the SDK with your Claude login (runs ${c.dim('claude setup-token')})
|
|
59
|
+
${c.dim('status')} show the active credential (plan / api-key), masked
|
|
60
|
+
${c.dim('logout')} remove the stored credential copy
|
|
61
|
+
${c.dim('run "<task>"')} run one Claude turn in the current directory (streamed)
|
|
62
|
+
${c.dim('"<task>"')} shorthand for run
|
|
63
|
+
|
|
64
|
+
${c.dim('Flags:')} --composition <id> --cwd <dir> --model <m> --continue --resume <sid>
|
|
65
|
+
|
|
66
|
+
First time: ${c.cyan('beehaven claude login')} → ${c.cyan('beehaven claude "list the files here"')}
|
|
67
|
+
`);
|
|
68
|
+
}
|
|
69
|
+
async function claudeLogin() {
|
|
70
|
+
const bin = await findClaudeBinary();
|
|
71
|
+
if (!bin) {
|
|
72
|
+
console.error(c.red('✗') +
|
|
73
|
+
' Claude Code binary not found. Install it from https://claude.com/download (or reinstall the Mac app), then retry.');
|
|
74
|
+
process.exitCode = 1;
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
console.log(c.dim(`Running ${bin} setup-token — a browser window will open for the Claude OAuth flow…`));
|
|
78
|
+
await new Promise((resolve) => {
|
|
79
|
+
let settled = false;
|
|
80
|
+
const finish = () => {
|
|
81
|
+
if (!settled) {
|
|
82
|
+
settled = true;
|
|
83
|
+
resolve();
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
startPlanOauth({
|
|
87
|
+
onOutput: (chunk) => process.stdout.write(chunk),
|
|
88
|
+
onExit: (code) => {
|
|
89
|
+
if (code === 0) {
|
|
90
|
+
console.log('\n' +
|
|
91
|
+
c.green('✓') +
|
|
92
|
+
' Authorized. The Claude Agent SDK will use your subscription (~/.claude/.credentials.json).');
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
console.error('\n' + c.red('✗') + ` claude setup-token exited with code ${code}.`);
|
|
96
|
+
process.exitCode = 1;
|
|
97
|
+
}
|
|
98
|
+
finish();
|
|
99
|
+
},
|
|
100
|
+
}).then((r) => {
|
|
101
|
+
if (!('error' in r))
|
|
102
|
+
return; // spawned OK — onExit will fire finish()
|
|
103
|
+
console.error(c.red('✗') + ' ' + r.error);
|
|
104
|
+
process.exitCode = 1;
|
|
105
|
+
finish();
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
async function claudeStatus() {
|
|
110
|
+
const [cred, bin] = await Promise.all([readClaudeCredential(), findClaudeBinary()]);
|
|
111
|
+
console.log(` Claude binary: ${bin ? c.green(bin) : c.red('not found')}`);
|
|
112
|
+
if (!cred) {
|
|
113
|
+
console.log(` Credential: ${c.yellow('none')} — run ${c.cyan('beehaven claude login')}`);
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
console.log(` Credential: ${c.green(cred.credType)} (${cred.maskedKey})`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
async function claudeLogout() {
|
|
120
|
+
await deleteClaudeCredential();
|
|
121
|
+
console.log(c.green('✓') +
|
|
122
|
+
' Removed the beehaven-stored credential copy. ' +
|
|
123
|
+
c.dim('(This does NOT sign you out of Claude Code itself — run `claude logout` for that.)'));
|
|
124
|
+
}
|
|
125
|
+
async function claudeRun(taskRaw, values) {
|
|
126
|
+
const task = (taskRaw || '').trim();
|
|
127
|
+
if (!task) {
|
|
128
|
+
console.error(c.red('✗') + ' Provide a task, e.g. ' + c.cyan('beehaven claude "refactor auth.ts"'));
|
|
129
|
+
process.exitCode = 1;
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
// The SDK reads ~/.claude/.credentials.json natively; this is just a friendly pre-check.
|
|
133
|
+
const cred = await readClaudeCredential();
|
|
134
|
+
if (!cred && !(await findClaudeBinary())) {
|
|
135
|
+
console.error(c.red('✗') + ' Not authorized. Run ' + c.cyan('beehaven claude login') + ' first.');
|
|
136
|
+
process.exitCode = 1;
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
const sessionId = `cli-${process.pid}-${Date.now()}`;
|
|
140
|
+
const userId = userInfo().username || 'local';
|
|
141
|
+
let sawText = false;
|
|
142
|
+
let resolveDone = () => { };
|
|
143
|
+
const finished = new Promise((res) => {
|
|
144
|
+
resolveDone = res;
|
|
145
|
+
});
|
|
146
|
+
const cleanup = (code) => {
|
|
147
|
+
try {
|
|
148
|
+
unsub();
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
/* idempotent */
|
|
152
|
+
}
|
|
153
|
+
try {
|
|
154
|
+
endSession(sessionId);
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
/* best-effort */
|
|
158
|
+
}
|
|
159
|
+
process.exitCode = code;
|
|
160
|
+
resolveDone();
|
|
161
|
+
};
|
|
162
|
+
const unsub = subscribe((ev) => {
|
|
163
|
+
if (ev.sessionId !== sessionId)
|
|
164
|
+
return;
|
|
165
|
+
switch (ev.type) {
|
|
166
|
+
case 'agent:text-delta':
|
|
167
|
+
process.stdout.write(ev.text);
|
|
168
|
+
sawText = true;
|
|
169
|
+
break;
|
|
170
|
+
case 'agent:tool-call':
|
|
171
|
+
process.stdout.write('\n' + c.dim(` ⚙ ${ev.tool}(${JSON.stringify(ev.input ?? {}).slice(0, 80)})`) + '\n');
|
|
172
|
+
break;
|
|
173
|
+
case 'agent:tool-result':
|
|
174
|
+
process.stdout.write(c.dim(` ${ev.ok ? '✓' : '✗'} ${ev.tool}${ev.preview ? ' — ' + ev.preview.slice(0, 80) : ''}`) + '\n');
|
|
175
|
+
break;
|
|
176
|
+
case 'agent:compacting':
|
|
177
|
+
process.stdout.write(c.dim(` … compacting context (${ev.kind})`) + '\n');
|
|
178
|
+
break;
|
|
179
|
+
case 'agent:done':
|
|
180
|
+
if (!sawText && ev.result?.text)
|
|
181
|
+
process.stdout.write(ev.result.text);
|
|
182
|
+
process.stdout.write('\n' + c.green('✓') + c.dim(` done (${ev.result?.turns ?? '?'} turns, ${ev.result?.durationMs ?? '?'}ms)`) + '\n');
|
|
183
|
+
cleanup(0);
|
|
184
|
+
break;
|
|
185
|
+
case 'agent:error':
|
|
186
|
+
process.stderr.write('\n' + c.red('✗') + ' ' + ev.error + '\n');
|
|
187
|
+
cleanup(1);
|
|
188
|
+
break;
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
try {
|
|
192
|
+
await promptOmni({
|
|
193
|
+
prompt: task,
|
|
194
|
+
sessionId,
|
|
195
|
+
userId,
|
|
196
|
+
// Dev mode — the SAME instruction the maid chat's Claude gets
|
|
197
|
+
// (apps/electron/electron/treehouse-agents.ts). Two processes, two builds,
|
|
198
|
+
// two Claude Agent SDK integrations; without this the two behaved
|
|
199
|
+
// differently, and only the one nobody drives from a terminal was asked to
|
|
200
|
+
// report what it trips over. promptOmni has always taken appendSystemPrompt
|
|
201
|
+
// — this caller simply never passed one.
|
|
202
|
+
appendSystemPrompt: withDevMode(),
|
|
203
|
+
model: typeof values.model === 'string' ? values.model : undefined,
|
|
204
|
+
cwd: typeof values.cwd === 'string' ? values.cwd : undefined,
|
|
205
|
+
compositionId: typeof values.composition === 'string' ? values.composition : undefined,
|
|
206
|
+
continueSession: !!values.continue,
|
|
207
|
+
resumeSessionId: typeof values.resume === 'string' ? values.resume : undefined,
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
catch (err) {
|
|
211
|
+
process.stderr.write(c.red('✗') + ' ' + (err?.message ?? String(err)) + '\n');
|
|
212
|
+
cleanup(1);
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
await finished;
|
|
216
|
+
// The SDK subprocess may keep handles open; exit deterministically once the turn is done.
|
|
217
|
+
setImmediate(() => process.exit(process.exitCode ?? 0));
|
|
218
|
+
}
|
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* beehaven compliance — Security & compliance report
|
|
3
|
+
*
|
|
4
|
+
* Scans the project for compliance posture:
|
|
5
|
+
* - Auth configuration (Passport provider, MFA, session management)
|
|
6
|
+
* - Data classification (tables with classification tags)
|
|
7
|
+
* - Audit logging (AuditChain configured)
|
|
8
|
+
* - Encryption (at-rest encryption, KeyRing)
|
|
9
|
+
* - API key management (ApiKeyManager usage)
|
|
10
|
+
* - Lock/access control (per-building locks)
|
|
11
|
+
* - Rate limiting (Middleware, Counter)
|
|
12
|
+
* - Visitor session management (credit limits, action allowlists)
|
|
13
|
+
* - CORS configuration
|
|
14
|
+
* - Environment variable security (secrets vs plaintext)
|
|
15
|
+
*
|
|
16
|
+
* Usage:
|
|
17
|
+
* beehaven compliance # Full report
|
|
18
|
+
* beehaven compliance --format json # JSON output
|
|
19
|
+
* beehaven compliance --check # Exit 1 if critical issues found
|
|
20
|
+
*/
|
|
21
|
+
import { readFileSync, existsSync, readdirSync, statSync } from 'fs';
|
|
22
|
+
import { join, relative } from 'path';
|
|
23
|
+
import { color } from '../spinner.js';
|
|
24
|
+
export async function compliance(args) {
|
|
25
|
+
const projectDir = args.path ?? process.cwd();
|
|
26
|
+
const findings = [];
|
|
27
|
+
console.log(color.cyan('\n🔒 Beehaven Compliance Report\n'));
|
|
28
|
+
// Scan source files
|
|
29
|
+
const sourceFiles = findSourceFiles(projectDir);
|
|
30
|
+
// ── Auth Configuration ───────────────────────────────────
|
|
31
|
+
checkAuth(sourceFiles, findings);
|
|
32
|
+
// ── Data Classification ──────────────────────────────────
|
|
33
|
+
checkClassification(sourceFiles, findings);
|
|
34
|
+
// ── Audit Logging ────────────────────────────────────────
|
|
35
|
+
checkAuditLog(sourceFiles, findings);
|
|
36
|
+
// ── Encryption ───────────────────────────────────────────
|
|
37
|
+
checkEncryption(sourceFiles, findings);
|
|
38
|
+
// ── API Key Management ───────────────────────────────────
|
|
39
|
+
checkApiKeys(sourceFiles, findings);
|
|
40
|
+
// ── Access Control (Locks) ───────────────────────────────
|
|
41
|
+
checkLocks(sourceFiles, findings);
|
|
42
|
+
// ── Rate Limiting ────────────────────────────────────────
|
|
43
|
+
checkRateLimiting(sourceFiles, findings);
|
|
44
|
+
// ── Visitor Sessions ─────────────────────────────────────
|
|
45
|
+
checkVisitorSessions(sourceFiles, findings);
|
|
46
|
+
// ── Environment Variables ────────────────────────────────
|
|
47
|
+
checkEnvSecurity(projectDir, findings);
|
|
48
|
+
// ── CORS ─────────────────────────────────────────────────
|
|
49
|
+
checkCors(sourceFiles, findings);
|
|
50
|
+
// ── MFA ──────────────────────────────────────────────────
|
|
51
|
+
checkMFA(sourceFiles, findings);
|
|
52
|
+
// Build report
|
|
53
|
+
const report = buildReport(findings);
|
|
54
|
+
if (args.format === 'json') {
|
|
55
|
+
console.log(JSON.stringify(report, null, 2));
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
printReport(report);
|
|
59
|
+
}
|
|
60
|
+
if (args.check && report.findings.some((f) => f.severity === 'critical')) {
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
// ── Checks ──────────────────────────────────────────────────────────
|
|
65
|
+
function checkAuth(files, findings) {
|
|
66
|
+
const hasPassport = files.some((f) => f.content.includes('Passport.'));
|
|
67
|
+
const hasAuthVerify = files.some((f) => f.content.includes('auth:') && f.content.includes('verify'));
|
|
68
|
+
if (!hasPassport && !hasAuthVerify) {
|
|
69
|
+
findings.push({
|
|
70
|
+
category: 'Auth',
|
|
71
|
+
severity: 'critical',
|
|
72
|
+
message: 'No auth provider configured (Passport or verify function)',
|
|
73
|
+
fix: 'Add auth: { verify: Passport.firebase({ projectId }) } to defineHive()',
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
findings.push({ category: 'Auth', severity: 'pass', message: 'Auth provider configured' });
|
|
78
|
+
}
|
|
79
|
+
const hasTicket = files.some((f) => f.content.includes('/auth/ticket'));
|
|
80
|
+
if (hasTicket) {
|
|
81
|
+
findings.push({
|
|
82
|
+
category: 'Auth',
|
|
83
|
+
severity: 'pass',
|
|
84
|
+
message: 'Ticket exchange enabled for WebSocket auth',
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function checkClassification(files, findings) {
|
|
89
|
+
const hasClassification = files.some((f) => f.content.includes('ClassificationRegistry') || f.content.includes('classification:'));
|
|
90
|
+
if (hasClassification) {
|
|
91
|
+
findings.push({
|
|
92
|
+
category: 'Data',
|
|
93
|
+
severity: 'pass',
|
|
94
|
+
message: 'Data classification configured',
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
findings.push({
|
|
99
|
+
category: 'Data',
|
|
100
|
+
severity: 'warning',
|
|
101
|
+
message: 'No data classification — consider tagging tables with sensitivity levels',
|
|
102
|
+
fix: 'Use ClassificationRegistry to tag tables as public/internal/confidential/restricted',
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
function checkAuditLog(files, findings) {
|
|
107
|
+
const hasAudit = files.some((f) => f.content.includes('AuditChain') ||
|
|
108
|
+
f.content.includes('AuditSink') ||
|
|
109
|
+
f.content.includes('auditChain'));
|
|
110
|
+
if (hasAudit) {
|
|
111
|
+
findings.push({ category: 'Audit', severity: 'pass', message: 'Audit logging configured' });
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
findings.push({
|
|
115
|
+
category: 'Audit',
|
|
116
|
+
severity: 'warning',
|
|
117
|
+
message: 'No audit logging — required for SOC2 compliance',
|
|
118
|
+
fix: 'Add AuditChain with D1 backend for tamper-proof audit trail',
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
function checkEncryption(files, findings) {
|
|
123
|
+
const hasEncryption = files.some((f) => f.content.includes('Encryption') || f.content.includes('KeyRing'));
|
|
124
|
+
if (hasEncryption) {
|
|
125
|
+
findings.push({
|
|
126
|
+
category: 'Encryption',
|
|
127
|
+
severity: 'pass',
|
|
128
|
+
message: 'At-rest encryption configured',
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
findings.push({
|
|
133
|
+
category: 'Encryption',
|
|
134
|
+
severity: 'info',
|
|
135
|
+
message: 'No field-level encryption — consider for PII columns',
|
|
136
|
+
fix: 'Use Encryption class for encrypting sensitive columns',
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
function checkApiKeys(files, findings) {
|
|
141
|
+
const hasApiKeys = files.some((f) => f.content.includes('ApiKeyManager'));
|
|
142
|
+
const hasPlaintextKeys = files.some((f) => /apiKey\s*[:=]\s*['"][^'"]{20,}['"]/.test(f.content));
|
|
143
|
+
if (hasPlaintextKeys) {
|
|
144
|
+
findings.push({
|
|
145
|
+
category: 'API Keys',
|
|
146
|
+
severity: 'critical',
|
|
147
|
+
message: 'Hardcoded API key detected in source code',
|
|
148
|
+
fix: 'Move API keys to environment variables or secrets',
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
if (hasApiKeys) {
|
|
152
|
+
findings.push({
|
|
153
|
+
category: 'API Keys',
|
|
154
|
+
severity: 'pass',
|
|
155
|
+
message: 'ApiKeyManager configured (SHA-256 hashed storage)',
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
function checkLocks(files, findings) {
|
|
160
|
+
const hasLock = files.some((f) => f.content.includes('Lock.') || f.content.includes('lock =') || f.content.includes('lock:'));
|
|
161
|
+
if (hasLock) {
|
|
162
|
+
findings.push({
|
|
163
|
+
category: 'Access',
|
|
164
|
+
severity: 'pass',
|
|
165
|
+
message: 'Lock-based access control configured',
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
findings.push({
|
|
170
|
+
category: 'Access',
|
|
171
|
+
severity: 'warning',
|
|
172
|
+
message: 'No explicit lock on buildings — all authenticated users can access',
|
|
173
|
+
fix: 'Add Lock.member(), Lock.owner(), or Lock.roles() to restrict access',
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
function checkRateLimiting(files, findings) {
|
|
178
|
+
const hasRateLimit = files.some((f) => f.content.includes('rateLimit') ||
|
|
179
|
+
f.content.includes('Counter') ||
|
|
180
|
+
f.content.includes('RATE_LIMITER'));
|
|
181
|
+
if (hasRateLimit) {
|
|
182
|
+
findings.push({
|
|
183
|
+
category: 'Rate Limit',
|
|
184
|
+
severity: 'pass',
|
|
185
|
+
message: 'Rate limiting configured',
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
findings.push({
|
|
190
|
+
category: 'Rate Limit',
|
|
191
|
+
severity: 'warning',
|
|
192
|
+
message: 'No rate limiting — vulnerable to abuse',
|
|
193
|
+
fix: 'Add rateLimit to tiers or use CF Rate Limiting binding',
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
function checkVisitorSessions(files, findings) {
|
|
198
|
+
const hasVisitors = files.some((f) => f.content.includes('VisitorSessionManager'));
|
|
199
|
+
if (hasVisitors) {
|
|
200
|
+
findings.push({
|
|
201
|
+
category: 'Visitors',
|
|
202
|
+
severity: 'pass',
|
|
203
|
+
message: 'Visitor session management with credit limits',
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
function checkEnvSecurity(projectDir, findings) {
|
|
208
|
+
// Check for .env in git
|
|
209
|
+
const gitignorePath = join(projectDir, '.gitignore');
|
|
210
|
+
if (existsSync(gitignorePath)) {
|
|
211
|
+
const gitignore = readFileSync(gitignorePath, 'utf-8');
|
|
212
|
+
if (!gitignore.includes('.env')) {
|
|
213
|
+
findings.push({
|
|
214
|
+
category: 'Env',
|
|
215
|
+
severity: 'critical',
|
|
216
|
+
message: '.env not in .gitignore — secrets may be committed',
|
|
217
|
+
fix: 'Add .env to .gitignore',
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
else {
|
|
221
|
+
findings.push({ category: 'Env', severity: 'pass', message: '.env excluded from git' });
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
// Check for .env file with secrets
|
|
225
|
+
const envPath = join(projectDir, '.env');
|
|
226
|
+
if (existsSync(envPath)) {
|
|
227
|
+
const env = readFileSync(envPath, 'utf-8');
|
|
228
|
+
const secretPatterns = [/sk_live_/i, /whsec_/i, /-----BEGIN/i, /password\s*=/i];
|
|
229
|
+
for (const pattern of secretPatterns) {
|
|
230
|
+
if (pattern.test(env)) {
|
|
231
|
+
findings.push({
|
|
232
|
+
category: 'Env',
|
|
233
|
+
severity: 'warning',
|
|
234
|
+
message: 'Production secrets detected in .env file',
|
|
235
|
+
fix: 'Use wrangler secret or CF dashboard for production secrets',
|
|
236
|
+
});
|
|
237
|
+
break;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
// Check wrangler.toml for plaintext secrets
|
|
242
|
+
const wranglerPath = join(projectDir, 'wrangler.toml');
|
|
243
|
+
if (existsSync(wranglerPath)) {
|
|
244
|
+
const wrangler = readFileSync(wranglerPath, 'utf-8');
|
|
245
|
+
if (/secret|key|password|token/i.test(wrangler) && /=\s*"[^"]{10,}"/.test(wrangler)) {
|
|
246
|
+
findings.push({
|
|
247
|
+
category: 'Env',
|
|
248
|
+
severity: 'warning',
|
|
249
|
+
message: 'Possible secrets in wrangler.toml — use wrangler secret instead',
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
function checkCors(files, findings) {
|
|
255
|
+
const hasWildcardCors = files.some((f) => f.content.includes("'*'") && f.content.includes('Access-Control'));
|
|
256
|
+
if (hasWildcardCors) {
|
|
257
|
+
findings.push({
|
|
258
|
+
category: 'CORS',
|
|
259
|
+
severity: 'warning',
|
|
260
|
+
message: 'Wildcard CORS origin detected — consider restricting to specific domains',
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
function checkMFA(files, findings) {
|
|
265
|
+
const hasMFA = files.some((f) => f.content.includes('withMFA') || f.content.includes('MFA'));
|
|
266
|
+
if (hasMFA) {
|
|
267
|
+
findings.push({ category: 'MFA', severity: 'pass', message: 'MFA (TOTP) configured' });
|
|
268
|
+
}
|
|
269
|
+
else {
|
|
270
|
+
findings.push({
|
|
271
|
+
category: 'MFA',
|
|
272
|
+
severity: 'info',
|
|
273
|
+
message: 'No MFA configured — consider for admin/sensitive operations',
|
|
274
|
+
fix: 'Use withMFA() to wrap Passport for admin routes',
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
// ── Report Builder ──────────────────────────────────────────────────
|
|
279
|
+
function buildReport(findings) {
|
|
280
|
+
const categories = [...new Set(findings.map((f) => f.category))];
|
|
281
|
+
const summary = {};
|
|
282
|
+
let totalScore = 0;
|
|
283
|
+
let totalMax = 0;
|
|
284
|
+
for (const cat of categories) {
|
|
285
|
+
const catFindings = findings.filter((f) => f.category === cat);
|
|
286
|
+
const max = catFindings.length * 10;
|
|
287
|
+
let score = 0;
|
|
288
|
+
for (const f of catFindings) {
|
|
289
|
+
switch (f.severity) {
|
|
290
|
+
case 'pass':
|
|
291
|
+
score += 10;
|
|
292
|
+
break;
|
|
293
|
+
case 'info':
|
|
294
|
+
score += 7;
|
|
295
|
+
break;
|
|
296
|
+
case 'warning':
|
|
297
|
+
score += 3;
|
|
298
|
+
break;
|
|
299
|
+
case 'critical':
|
|
300
|
+
score += 0;
|
|
301
|
+
break;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
summary[cat] = { score, max, findings: catFindings.length };
|
|
305
|
+
totalScore += score;
|
|
306
|
+
totalMax += max;
|
|
307
|
+
}
|
|
308
|
+
const pct = totalMax > 0 ? Math.round((totalScore / totalMax) * 100) : 0;
|
|
309
|
+
let grade;
|
|
310
|
+
if (pct >= 90)
|
|
311
|
+
grade = 'A';
|
|
312
|
+
else if (pct >= 80)
|
|
313
|
+
grade = 'B';
|
|
314
|
+
else if (pct >= 70)
|
|
315
|
+
grade = 'C';
|
|
316
|
+
else if (pct >= 60)
|
|
317
|
+
grade = 'D';
|
|
318
|
+
else
|
|
319
|
+
grade = 'F';
|
|
320
|
+
return { score: totalScore, maxScore: totalMax, grade, findings, summary };
|
|
321
|
+
}
|
|
322
|
+
function printReport(report) {
|
|
323
|
+
const severityColors = {
|
|
324
|
+
critical: color.red,
|
|
325
|
+
warning: color.yellow,
|
|
326
|
+
info: color.cyan,
|
|
327
|
+
pass: color.green,
|
|
328
|
+
};
|
|
329
|
+
const severityIcons = {
|
|
330
|
+
critical: 'FAIL',
|
|
331
|
+
warning: 'WARN',
|
|
332
|
+
info: 'INFO',
|
|
333
|
+
pass: 'PASS',
|
|
334
|
+
};
|
|
335
|
+
// Group by category
|
|
336
|
+
const categories = [...new Set(report.findings.map((f) => f.category))];
|
|
337
|
+
for (const cat of categories) {
|
|
338
|
+
const catFindings = report.findings.filter((f) => f.category === cat);
|
|
339
|
+
const catSummary = report.summary[cat];
|
|
340
|
+
const pct = catSummary.max > 0 ? Math.round((catSummary.score / catSummary.max) * 100) : 0;
|
|
341
|
+
console.log(`${color.bold(cat)} ${color.dim(`(${pct}%)`)}`);
|
|
342
|
+
for (const f of catFindings) {
|
|
343
|
+
const icon = severityIcons[f.severity];
|
|
344
|
+
const colorFn = severityColors[f.severity];
|
|
345
|
+
console.log(` ${colorFn(icon)} ${f.message}`);
|
|
346
|
+
if (f.fix)
|
|
347
|
+
console.log(` ${color.dim(`Fix: ${f.fix}`)}`);
|
|
348
|
+
}
|
|
349
|
+
console.log('');
|
|
350
|
+
}
|
|
351
|
+
// Overall score
|
|
352
|
+
const pct = report.maxScore > 0 ? Math.round((report.score / report.maxScore) * 100) : 0;
|
|
353
|
+
const gradeColor = report.grade === 'A' ? color.green : report.grade === 'B' ? color.yellow : color.red;
|
|
354
|
+
console.log(`${color.bold('Overall:')} ${gradeColor(`Grade ${report.grade}`)} (${pct}%)`);
|
|
355
|
+
const criticals = report.findings.filter((f) => f.severity === 'critical').length;
|
|
356
|
+
const warnings = report.findings.filter((f) => f.severity === 'warning').length;
|
|
357
|
+
if (criticals > 0)
|
|
358
|
+
console.log(color.red(` ${criticals} critical issue(s) require immediate attention`));
|
|
359
|
+
if (warnings > 0)
|
|
360
|
+
console.log(color.yellow(` ${warnings} warning(s) should be addressed`));
|
|
361
|
+
console.log('');
|
|
362
|
+
}
|
|
363
|
+
function findSourceFiles(dir) {
|
|
364
|
+
const files = [];
|
|
365
|
+
const extensions = ['.ts', '.tsx', '.js', '.jsx'];
|
|
366
|
+
const ignoreDirs = ['node_modules', 'dist', '.git', '_archive'];
|
|
367
|
+
function walk(d) {
|
|
368
|
+
try {
|
|
369
|
+
for (const entry of readdirSync(d)) {
|
|
370
|
+
const full = join(d, entry);
|
|
371
|
+
try {
|
|
372
|
+
const stat = statSync(full);
|
|
373
|
+
if (stat.isDirectory() && !ignoreDirs.includes(entry)) {
|
|
374
|
+
walk(full);
|
|
375
|
+
}
|
|
376
|
+
else if (stat.isFile() && extensions.some((ext) => entry.endsWith(ext))) {
|
|
377
|
+
files.push({ path: relative(dir, full), content: readFileSync(full, 'utf-8') });
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
catch { }
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
catch { }
|
|
384
|
+
}
|
|
385
|
+
walk(dir);
|
|
386
|
+
return files;
|
|
387
|
+
}
|