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.
@@ -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
+ }