content-grade 1.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/CONTRIBUTING.md +198 -0
- package/LICENSE +21 -0
- package/README.md +348 -0
- package/bin/content-grade.js +1033 -0
- package/bin/telemetry.js +230 -0
- package/dist/assets/index-BUN69TiT.js +78 -0
- package/dist/index.html +22 -0
- package/dist-server/server/app.js +36 -0
- package/dist-server/server/claude.js +22 -0
- package/dist-server/server/db.js +79 -0
- package/dist-server/server/index.js +62 -0
- package/dist-server/server/routes/demos.js +1701 -0
- package/dist-server/server/routes/stripe.js +136 -0
- package/dist-server/server/services/claude.js +55 -0
- package/dist-server/server/services/stripe.js +109 -0
- package/package.json +84 -0
package/dist/index.html
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>ContentGrade Suite</title>
|
|
7
|
+
<style>
|
|
8
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
9
|
+
body {
|
|
10
|
+
background: #0a0a0f;
|
|
11
|
+
color: #e0e0e0;
|
|
12
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
13
|
+
min-height: 100vh;
|
|
14
|
+
}
|
|
15
|
+
#root { min-height: 100vh; }
|
|
16
|
+
</style>
|
|
17
|
+
<script type="module" crossorigin src="/assets/index-BUN69TiT.js"></script>
|
|
18
|
+
</head>
|
|
19
|
+
<body>
|
|
20
|
+
<div id="root"></div>
|
|
21
|
+
</body>
|
|
22
|
+
</html>
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* App factory — creates and configures the Fastify instance without binding
|
|
3
|
+
* to a port. Used by both server/index.ts (production) and tests.
|
|
4
|
+
*/
|
|
5
|
+
import Fastify from 'fastify';
|
|
6
|
+
import cors from '@fastify/cors';
|
|
7
|
+
import { registerDemoRoutes } from './routes/demos.js';
|
|
8
|
+
import { registerStripeRoutes } from './routes/stripe.js';
|
|
9
|
+
export async function createApp(opts = {}) {
|
|
10
|
+
const app = Fastify({
|
|
11
|
+
logger: opts.logger ?? { level: 'warn' },
|
|
12
|
+
// Trust x-forwarded-for from reverse proxy for accurate IP-based rate limiting
|
|
13
|
+
trustProxy: true,
|
|
14
|
+
});
|
|
15
|
+
// Raw body for Stripe webhook signature verification
|
|
16
|
+
app.addContentTypeParser('application/json', { parseAs: 'buffer' }, (req, body, done) => {
|
|
17
|
+
req.rawBody = body;
|
|
18
|
+
try {
|
|
19
|
+
done(null, JSON.parse(body.toString()));
|
|
20
|
+
}
|
|
21
|
+
catch (err) {
|
|
22
|
+
done(err, undefined);
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
await app.register(cors, {
|
|
26
|
+
origin: true,
|
|
27
|
+
credentials: true,
|
|
28
|
+
});
|
|
29
|
+
registerDemoRoutes(app);
|
|
30
|
+
registerStripeRoutes(app);
|
|
31
|
+
app.get('/api/health', async () => ({
|
|
32
|
+
status: 'alive',
|
|
33
|
+
uptime: process.uptime(),
|
|
34
|
+
}));
|
|
35
|
+
return app;
|
|
36
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { execFile } from 'child_process';
|
|
2
|
+
import { promisify } from 'util';
|
|
3
|
+
const execFileAsync = promisify(execFile);
|
|
4
|
+
const MODEL_MAP = {
|
|
5
|
+
haiku: 'claude-haiku-4-5-20251001',
|
|
6
|
+
sonnet: 'claude-sonnet-4-6',
|
|
7
|
+
opus: 'claude-opus-4-6',
|
|
8
|
+
};
|
|
9
|
+
export async function askClaude(prompt, opts) {
|
|
10
|
+
const claudePath = process.env.CLAUDE_PATH || 'claude';
|
|
11
|
+
const model = MODEL_MAP[opts?.model ?? 'haiku'] ?? MODEL_MAP.haiku;
|
|
12
|
+
const args = ['-p', '--no-session-persistence', '--model', model];
|
|
13
|
+
if (opts?.systemPrompt) {
|
|
14
|
+
args.push('--system-prompt', opts.systemPrompt);
|
|
15
|
+
}
|
|
16
|
+
args.push(prompt);
|
|
17
|
+
const { stdout } = await execFileAsync(claudePath, args, {
|
|
18
|
+
timeout: 120000,
|
|
19
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
20
|
+
});
|
|
21
|
+
return stdout.trim();
|
|
22
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import Database from 'better-sqlite3';
|
|
2
|
+
import { mkdirSync } from 'fs';
|
|
3
|
+
import { resolve, dirname } from 'path';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
const DB_PATH = process.env.CONTENTGRADE_DB_PATH ?? resolve(__dirname, '../data/contentgrade.db');
|
|
7
|
+
let _db = null;
|
|
8
|
+
export function getDb() {
|
|
9
|
+
if (!_db) {
|
|
10
|
+
if (DB_PATH !== ':memory:') {
|
|
11
|
+
mkdirSync(resolve(__dirname, '../data'), { recursive: true });
|
|
12
|
+
}
|
|
13
|
+
_db = new Database(DB_PATH);
|
|
14
|
+
_db.pragma('journal_mode = WAL');
|
|
15
|
+
_db.pragma('foreign_keys = ON');
|
|
16
|
+
migrate(_db);
|
|
17
|
+
}
|
|
18
|
+
return _db;
|
|
19
|
+
}
|
|
20
|
+
/** Reset the DB singleton — used in tests to get a fresh in-memory DB. */
|
|
21
|
+
export function _resetDbForTests() {
|
|
22
|
+
if (_db) {
|
|
23
|
+
try {
|
|
24
|
+
_db.close();
|
|
25
|
+
}
|
|
26
|
+
catch { }
|
|
27
|
+
_db = null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function migrate(db) {
|
|
31
|
+
db.exec(`
|
|
32
|
+
CREATE TABLE IF NOT EXISTS usage_tracking (
|
|
33
|
+
id INTEGER PRIMARY KEY,
|
|
34
|
+
ip_hash TEXT NOT NULL,
|
|
35
|
+
endpoint TEXT NOT NULL,
|
|
36
|
+
date TEXT NOT NULL,
|
|
37
|
+
count INTEGER NOT NULL DEFAULT 0,
|
|
38
|
+
UNIQUE(ip_hash, endpoint, date)
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
CREATE TABLE IF NOT EXISTS email_captures (
|
|
42
|
+
id INTEGER PRIMARY KEY,
|
|
43
|
+
email TEXT NOT NULL,
|
|
44
|
+
tool TEXT NOT NULL,
|
|
45
|
+
score TEXT NOT NULL DEFAULT '',
|
|
46
|
+
ip_hash TEXT,
|
|
47
|
+
source TEXT,
|
|
48
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
CREATE TABLE IF NOT EXISTS stripe_customers (
|
|
52
|
+
id INTEGER PRIMARY KEY,
|
|
53
|
+
email TEXT NOT NULL UNIQUE,
|
|
54
|
+
stripe_customer_id TEXT NOT NULL,
|
|
55
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
CREATE TABLE IF NOT EXISTS stripe_subscriptions (
|
|
59
|
+
id INTEGER PRIMARY KEY,
|
|
60
|
+
email TEXT NOT NULL,
|
|
61
|
+
stripe_customer_id TEXT NOT NULL,
|
|
62
|
+
stripe_subscription_id TEXT NOT NULL UNIQUE,
|
|
63
|
+
plan_tier TEXT NOT NULL,
|
|
64
|
+
status TEXT NOT NULL,
|
|
65
|
+
current_period_end INTEGER NOT NULL DEFAULT 0,
|
|
66
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
67
|
+
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
CREATE TABLE IF NOT EXISTS stripe_one_time_purchases (
|
|
71
|
+
id INTEGER PRIMARY KEY,
|
|
72
|
+
email TEXT NOT NULL,
|
|
73
|
+
stripe_customer_id TEXT NOT NULL,
|
|
74
|
+
stripe_payment_intent_id TEXT NOT NULL UNIQUE,
|
|
75
|
+
product_key TEXT NOT NULL,
|
|
76
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
77
|
+
);
|
|
78
|
+
`);
|
|
79
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import Fastify from 'fastify';
|
|
2
|
+
import cors from '@fastify/cors';
|
|
3
|
+
import staticPlugin from '@fastify/static';
|
|
4
|
+
import { resolve, dirname } from 'path';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
import { existsSync } from 'fs';
|
|
7
|
+
import { getDb } from './db.js';
|
|
8
|
+
import { registerDemoRoutes } from './routes/demos.js';
|
|
9
|
+
import { registerStripeRoutes } from './routes/stripe.js';
|
|
10
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
const PORT = parseInt(process.env.PORT || '4000', 10);
|
|
12
|
+
const isProd = process.env.NODE_ENV === 'production';
|
|
13
|
+
// Init DB on startup
|
|
14
|
+
getDb();
|
|
15
|
+
const app = Fastify({ logger: { level: 'warn' } });
|
|
16
|
+
// Raw body for Stripe webhook signature verification
|
|
17
|
+
app.addContentTypeParser('application/json', { parseAs: 'buffer' }, (req, body, done) => {
|
|
18
|
+
req.rawBody = body;
|
|
19
|
+
try {
|
|
20
|
+
done(null, JSON.parse(body.toString()));
|
|
21
|
+
}
|
|
22
|
+
catch (err) {
|
|
23
|
+
done(err, undefined);
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
await app.register(cors, {
|
|
27
|
+
origin: true,
|
|
28
|
+
credentials: true,
|
|
29
|
+
});
|
|
30
|
+
// Register API routes
|
|
31
|
+
registerDemoRoutes(app);
|
|
32
|
+
registerStripeRoutes(app);
|
|
33
|
+
// Health check
|
|
34
|
+
app.get('/api/health', async () => ({
|
|
35
|
+
status: 'alive',
|
|
36
|
+
uptime: process.uptime(),
|
|
37
|
+
}));
|
|
38
|
+
// In production, serve the Vite build
|
|
39
|
+
if (isProd) {
|
|
40
|
+
const distPath = resolve(__dirname, '../dist');
|
|
41
|
+
if (existsSync(distPath)) {
|
|
42
|
+
await app.register(staticPlugin, {
|
|
43
|
+
root: distPath,
|
|
44
|
+
prefix: '/',
|
|
45
|
+
});
|
|
46
|
+
// SPA fallback — serve index.html for all non-API routes
|
|
47
|
+
app.setNotFoundHandler(async (req, reply) => {
|
|
48
|
+
if (!req.url.startsWith('/api')) {
|
|
49
|
+
return reply.sendFile('index.html');
|
|
50
|
+
}
|
|
51
|
+
reply.status(404).send({ error: 'Not found' });
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
await app.listen({ port: PORT, host: '0.0.0.0' });
|
|
57
|
+
console.log(`ContentGrade server running on http://0.0.0.0:${PORT}`);
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
console.error('Failed to start server:', err);
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|