Skip to content

Commit 5a1c7d9

Browse files
Merge pull request #254 from blurbeast/main
Add strict Cross-Origin Resource Sharing (CORS)
2 parents 7493bc4 + b1e8535 commit 5a1c7d9

29 files changed

Lines changed: 1780 additions & 1515 deletions

fix-db.js

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
const Database = require('better-sqlite3');
2+
const path = require('path');
3+
const fs = require('fs');
4+
5+
const dbPath = './data/substream.db';
6+
const dir = path.dirname(dbPath);
7+
if (!fs.existsSync(dir)) {
8+
fs.mkdirSync(dir, { recursive: true });
9+
}
10+
11+
const db = new Database(dbPath);
12+
13+
console.log('Creating manual core tables...');
14+
db.exec(`
15+
CREATE TABLE IF NOT EXISTS creators (
16+
id TEXT PRIMARY KEY,
17+
created_at TEXT NOT NULL
18+
);
19+
CREATE TABLE IF NOT EXISTS content (
20+
id TEXT PRIMARY KEY,
21+
creator_address TEXT NOT NULL,
22+
title TEXT NOT NULL,
23+
description TEXT,
24+
thumbnail TEXT,
25+
type TEXT NOT NULL,
26+
tags TEXT,
27+
created_at TEXT NOT NULL
28+
);
29+
CREATE TABLE IF NOT EXISTS tenants (
30+
id TEXT PRIMARY KEY,
31+
name TEXT NOT NULL,
32+
created_at TEXT NOT NULL
33+
);
34+
CREATE TABLE IF NOT EXISTS subscriptions (
35+
creator_id TEXT NOT NULL,
36+
wallet_address TEXT NOT NULL,
37+
active INTEGER NOT NULL DEFAULT 1,
38+
subscribed_at TEXT NOT NULL,
39+
unsubscribed_at TEXT,
40+
PRIMARY KEY (creator_id, wallet_address)
41+
);
42+
`);
43+
console.log('Manual core tables created.');
44+
db.close();

index.js

Lines changed: 48 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ const { setupApolloServer } = require('./src/graphql');
140140

141141
// Tier middleware — attaches req.user.tier to every request
142142
const { attachTier } = require('./middleware/tierAuth');
143+
const { MerchantCorsMiddleware } = require('./src/middleware/merchantCorsMiddleware');
143144

144145
/**
145146
* Create the Express application with injectable services for testing.
@@ -173,7 +174,8 @@ async function createApp(dependencies = {}) {
173174
const tokenService = dependencies.tokenService || new CdnTokenService(config);
174175

175176
// ── Global middleware ──────────────────────────────────────────────────────
176-
app.use(cors());
177+
const merchantCors = new MerchantCorsMiddleware(database);
178+
app.use(cors(merchantCors.corsOptionsDelegate()));
177179
app.use(express.json({ limit: '10mb' }));
178180
app.use(express.urlencoded({ extended: true }));
179181

@@ -248,6 +250,9 @@ async function createApp(dependencies = {}) {
248250
amlScannerWorker.start().catch(error => {
249251
console.error('Failed to start AML scanner worker:', error);
250252
});
253+
}
254+
255+
251256

252257
// Start federation worker if ActivityPub is enabled
253258
if (config.activityPub?.enabled !== false) {
@@ -344,14 +349,12 @@ async function createApp(dependencies = {}) {
344349
});
345350

346351
// Start federation worker if ActivityPub is enabled
347-
if (config.activityPub?.enabled !== false) {
348-
federationWorker.start().catch(error => {
349-
console.error('Failed to start federation worker:', error);
350-
});
351-
}
352+
if (config.activityPub?.enabled !== false) {
353+
federationWorker.start().catch(error => {
354+
console.error('Failed to start federation worker:', error);
355+
});
356+
}
352357

353-
app.use(cors());
354-
app.use(express.json());
355358

356359

357360
// Subscription events webhook
@@ -678,51 +681,48 @@ async function createApp(dependencies = {}) {
678681
requireCreatorAuth(creatorAuthService),
679682
(req, res) => {
680683
const format = String(req.query.format || '').toLowerCase();
681-
// Get creator stats (including cached subscriber count)
682-
app.get('/api/creator/:id/stats', (req, res) => {
683-
try {
684-
const creatorId = req.params.id;
685-
const subscriberCount = database.getCreatorSubscriberCount(creatorId);
686684

687-
return res.status(200).json({ success: true, data: { creatorId, subscriberCount } });
688-
} catch (error) {
689-
return res.status(500).json({ success: false, error: error.message || 'Failed to fetch stats' });
690-
}
691-
});
692-
693-
app.get('/api/creator/audit-log/export', requireCreatorAuth(creatorAuthService), (req, res) => {
694-
const format = String(req.query.format || '').toLowerCase();
695-
696-
if (!['csv', 'pdf'].includes(format)) {
697-
return res.status(400).json({ success: false, error: 'format must be one of: csv, pdf' });
698-
}
685+
if (!['csv', 'pdf'].includes(format)) {
686+
return res.status(400).json({ success: false, error: 'format must be one of: csv, pdf' });
687+
}
699688

700-
const logs = auditLogService.listByCreatorId(req.creator.id);
701-
const exportTimestamp = new Date().toISOString();
702-
703-
if (format === 'csv') {
704-
const csv = buildAuditLogCsv(logs);
705-
res.setHeader('Content-Type', 'text/csv; charset=utf-8');
706-
res.setHeader(
707-
'Content-Disposition',
708-
`attachment; filename="creator-audit-log-${req.creator.id}.csv"`,
709-
);
710-
return res.status(200).send(csv);
711-
}
689+
const logs = auditLogService.listByCreatorId(req.creator.id);
690+
const exportTimestamp = new Date().toISOString();
712691

713-
const pdf = buildAuditLogPdf({
714-
creatorId: req.creator.id,
715-
exportedAt: exportTimestamp,
716-
logs,
717-
});
718-
res.setHeader('Content-Type', 'application/pdf');
692+
if (format === 'csv') {
693+
const csv = buildAuditLogCsv(logs);
694+
res.setHeader('Content-Type', 'text/csv; charset=utf-8');
719695
res.setHeader(
720696
'Content-Disposition',
721-
`attachment; filename="creator-audit-log-${req.creator.id}.pdf"`,
697+
`attachment; filename="creator-audit-log-${req.creator.id}.csv"`,
722698
);
723-
return res.status(200).send(pdf);
724-
},
699+
return res.status(200).send(csv);
700+
}
701+
702+
const pdf = buildAuditLogPdf({
703+
creatorId: req.creator.id,
704+
exportedAt: exportTimestamp,
705+
logs,
706+
});
707+
res.setHeader('Content-Type', 'application/pdf');
708+
res.setHeader(
709+
'Content-Disposition',
710+
`attachment; filename="creator-audit-log-${req.creator.id}.pdf"`,
725711
);
712+
return res.status(200).send(pdf);
713+
}
714+
);
715+
716+
app.get('/api/creator/:id/stats', (req, res) => {
717+
try {
718+
const creatorId = req.params.id;
719+
const subscriberCount = database.getCreatorSubscriberCount(creatorId);
720+
return res.status(200).json({ success: true, data: { creatorId, subscriberCount } });
721+
} catch (error) {
722+
return res.status(500).json({ success: false, error: error.message || 'Failed to fetch stats' });
723+
}
724+
});
725+
726726

727727
// ── Error handlers ─────────────────────────────────────────────────────────
728728
app.use(createErrorMonitoringMiddleware(endpointMonitoringService));
@@ -896,10 +896,10 @@ async function createApp(dependencies = {}) {
896896
});
897897
});
898898

899-
return app;
900-
}
899+
return app;
901900
}
902901

902+
903903
// ── Private helpers ────────────────────────────────────────────────────────
904904

905905
function extractToken(req) {

knexfile.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,8 @@ module.exports = {
3434
createRetryIntervalMillis: 200,
3535
afterCreate: (conn, done) => {
3636
// Enable WAL mode for better concurrent read performance
37-
conn.run('PRAGMA journal_mode = WAL;');
38-
conn.run('PRAGMA busy_timeout = 5000;');
37+
conn.exec('PRAGMA journal_mode = WAL;');
38+
conn.exec('PRAGMA busy_timeout = 5000;');
3939
done(null, conn);
4040
},
4141
},

migrations/20240428000001_create_merchants_table.js renamed to migrations/knex/001_create_merchants_table.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
*/
55
exports.up = function(knex) {
66
return knex.schema.createTable('merchants', (table) => {
7-
table.uuid('id').primary().defaultTo(knex.raw('gen_random_uuid()'));
7+
table.string('id').primary().defaultTo(knex.raw('(lower(hex(randomblob(16))))'));
88
table.string('name').notNullable();
99
table.string('base_currency').notNullable().defaultTo('USD');
1010
table.timestamp('created_at').defaultTo(knex.fn.now());

migrations/20240428000002_create_merchant_balances_table.js renamed to migrations/knex/002_create_merchant_balances_table.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44
*/
55
exports.up = function(knex) {
66
return knex.schema.createTable('merchant_balances', (table) => {
7-
table.uuid('id').primary().defaultTo(knex.raw('gen_random_uuid()'));
8-
table.uuid('merchant_id').notNullable().references('id').inTable('merchants').onDelete('CASCADE');
7+
table.string('id').primary().defaultTo(knex.raw('(lower(hex(randomblob(16))))'));
8+
table.string('merchant_id').notNullable().references('id').inTable('merchants').onDelete('CASCADE');
99
table.string('asset_code').notNullable(); // e.g., 'XLM', 'USDC', 'EURC'
1010
table.string('asset_issuer').nullable(); // Stellar asset issuer
1111
table.decimal('balance', 20, 8).notNullable().defaultTo(0);

migrations/knex/006_add_activitypub_tables.js

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ exports.up = function(knex) {
22
return knex.schema
33
// ActivityPub actors table
44
.createTable('activitypub_actors', function(table) {
5-
table.string('creator_address').primary().references('address').inTable('creators').onDelete('CASCADE');
5+
table.string('creator_address').primary().references('id').inTable('creators').onDelete('CASCADE');
66
table.text('public_key').notNullable();
77
table.text('private_key').notNullable();
88
table.string('actor_id').notNullable().unique();
@@ -18,7 +18,7 @@ exports.up = function(knex) {
1818
// ActivityPub followers table
1919
.createTable('activitypub_followers', function(table) {
2020
table.increments('id').primary();
21-
table.string('creator_address').notNullable().references('address').inTable('creators').onDelete('CASCADE');
21+
table.string('creator_address').notNullable().references('id').inTable('creators').onDelete('CASCADE');
2222
table.string('follower_actor').notNullable();
2323
table.string('follower_inbox');
2424
table.string('follower_shared_inbox');
@@ -35,7 +35,7 @@ exports.up = function(knex) {
3535
// ActivityPub activities table (sent activities)
3636
.createTable('activitypub_activities', function(table) {
3737
table.increments('id').primary();
38-
table.string('creator_address').notNullable().references('address').inTable('creators').onDelete('CASCADE');
38+
table.string('creator_address').notNullable().references('id').inTable('creators').onDelete('CASCADE');
3939
table.string('activity_id').notNullable().unique();
4040
table.string('activity_type').notNullable();
4141
table.string('object_type').notNullable();
@@ -53,7 +53,7 @@ exports.up = function(knex) {
5353
// ActivityPub engagements table (received activities)
5454
.createTable('activitypub_engagements', function(table) {
5555
table.increments('id').primary();
56-
table.string('creator_address').notNullable().references('address').inTable('creators').onDelete('CASCADE');
56+
table.string('creator_address').notNullable().references('id').inTable('creators').onDelete('CASCADE');
5757
table.string('activity_type').notNullable();
5858
table.string('activity_actor').notNullable();
5959
table.string('activity_id').notNullable();
@@ -69,7 +69,7 @@ exports.up = function(knex) {
6969
// Federation queue table (for background processing)
7070
.createTable('federation_queue', function(table) {
7171
table.increments('id').primary();
72-
table.string('creator_address').notNullable().references('address').inTable('creators').onDelete('CASCADE');
72+
table.string('creator_address').notNullable().references('id').inTable('creators').onDelete('CASCADE');
7373
table.string('content_id').notNullable().references('id').inTable('content').onDelete('CASCADE');
7474
table.string('activity_type').notNullable().defaultTo('Announce');
7575
table.json('activity_data').notNullable();

migrations/knex/007_add_leaderboard_tables.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ exports.up = function(knex) {
22
return knex.schema
33
// Streaming payments table for tracking fan payments
44
.createTable('streaming_payments', function(table) {
5-
table.string('id').primary().defaultTo(knex.raw('lower(hex(randomblob(16)))'));
5+
table.string('id').primary().defaultTo(knex.raw('(lower(hex(randomblob(16))))'));
66
table.string('creator_address').notNullable().index();
77
table.string('fan_address').notNullable().index();
88
table.decimal('amount', 20, 8).notNullable();
@@ -19,7 +19,7 @@ exports.up = function(knex) {
1919

2020
// Content likes table for engagement tracking
2121
.createTable('content_likes', function(table) {
22-
table.string('id').primary().defaultTo(knex.raw('lower(hex(randomblob(16)))'));
22+
table.string('id').primary().defaultTo(knex.raw('(lower(hex(randomblob(16))))'));
2323
table.string('content_id').notNullable().index();
2424
table.string('creator_address').notNullable().index();
2525
table.string('fan_address').notNullable().index();
@@ -36,7 +36,7 @@ exports.up = function(knex) {
3636

3737
// Leaderboard snapshots for historical data
3838
.createTable('leaderboard_snapshots', function(table) {
39-
table.string('id').primary().defaultTo(knex.raw('lower(hex(randomblob(16)))'));
39+
table.string('id').primary().defaultTo(knex.raw('(lower(hex(randomblob(16))))'));
4040
table.string('creator_address').notNullable().index();
4141
table.string('fan_address').notNullable().index();
4242
table.string('season').notNullable().index();
@@ -54,7 +54,7 @@ exports.up = function(knex) {
5454

5555
// Fan engagement summary for quick lookups
5656
.createTable('fan_engagement_summary', function(table) {
57-
table.string('id').primary().defaultTo(knex.raw('lower(hex(randomblob(16)))'));
57+
table.string('id').primary().defaultTo(knex.raw('(lower(hex(randomblob(16))))'));
5858
table.string('creator_address').notNullable().index();
5959
table.string('fan_address').notNullable().index();
6060
table.string('season').notNullable().index();

migrations/knex/008_add_social_token_tables.js renamed to migrations/knex/008a_add_social_token_tables.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ exports.up = function(knex) {
2020

2121
// Social token sessions table for balance re-verification
2222
.createTable('social_token_sessions', function(table) {
23-
table.string('session_id').primary().defaultTo(knex.raw('lower(hex(randomblob(16)))'));
23+
table.string('session_id').primary().defaultTo(knex.raw('(lower(hex(randomblob(16))))'));
2424
table.string('user_address').notNullable().index();
2525
table.string('content_id').notNullable().references('id').inTable('content').onDelete('CASCADE');
2626
table.string('asset_code').notNullable().index();
@@ -34,12 +34,12 @@ exports.up = function(knex) {
3434
// Indexes for performance
3535
table.index(['user_address', 'still_valid']);
3636
table.index(['content_id', 'still_valid']);
37-
table.index(['last_verified']);
3837
})
38+
3939

4040
// Social token access logs for analytics
4141
.createTable('social_token_access_logs', function(table) {
42-
table.string('id').primary().defaultTo(knex.raw('lower(hex(randomblob(16)))'));
42+
table.string('id').primary().defaultTo(knex.raw('(lower(hex(randomblob(16))))'));
4343
table.string('user_address').notNullable().index();
4444
table.string('content_id').notNullable().references('id').inTable('content').onDelete('CASCADE');
4545
table.boolean('has_access').notNullable().index();

migrations/knex/008_add_tenant_feature_flags.js renamed to migrations/knex/008b_add_tenant_feature_flags.js

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,11 @@
99
exports.up = async function(knex) {
1010
// Create tenant_configurations table
1111
await knex.schema.createTable('tenant_configurations', function(table) {
12-
table.uuid('id').primary().defaultTo(knex.raw('(gen_random_uuid())'));
13-
table.uuid('tenant_id').notNullable();
12+
table.string('id').primary().defaultTo(knex.raw('(lower(hex(randomblob(16))))'));
13+
table.string('tenant_id').notNullable();
1414
table.string('flag_name', 100).notNullable();
1515
table.boolean('flag_value').defaultTo(false);
16-
table.jsonb('metadata').defaultTo('{}');
16+
table.json('metadata').defaultTo('{}');
1717
table.timestamp('created_at').defaultTo(knex.fn.now());
1818
table.timestamp('updated_at').defaultTo(knex.fn.now());
1919

@@ -28,14 +28,14 @@ exports.up = async function(knex) {
2828

2929
// Create feature_flag_audit_log table
3030
await knex.schema.createTable('feature_flag_audit_log', function(table) {
31-
table.uuid('id').primary().defaultTo(knex.raw('(gen_random_uuid())'));
32-
table.uuid('tenant_id').notNullable();
31+
table.string('id').primary().defaultTo(knex.raw('(lower(hex(randomblob(16))))'));
32+
table.string('tenant_id').notNullable();
3333
table.string('flag_name', 100).notNullable();
3434
table.boolean('old_value');
3535
table.boolean('new_value').notNullable();
3636
table.string('changed_by', 255).notNullable(); // User or system that made the change
3737
table.string('change_reason', 500);
38-
table.jsonb('metadata').defaultTo('{}');
38+
table.json('metadata').defaultTo('{}');
3939
table.timestamp('created_at').defaultTo(knex.fn.now());
4040

4141
// Indexes for audit queries
@@ -57,23 +57,23 @@ exports.up = async function(knex) {
5757
'{"auto_created": true}' as metadata
5858
FROM tenants t
5959
CROSS JOIN (
60-
VALUES
61-
('enable_crypto_checkout', false),
62-
('enable_b2b_invoicing', false),
63-
('require_kyc_for_subs', false),
64-
('enable_advanced_analytics', false),
65-
('enable_api_webhooks', false),
66-
('enable_custom_branding', false),
67-
('enable_priority_support', false),
68-
('enable_bulk_operations', false)
69-
) AS f(flag_name, default_value)
60+
SELECT 'enable_crypto_checkout' AS flag_name, 0 AS default_value
61+
UNION ALL SELECT 'enable_b2b_invoicing', 0
62+
UNION ALL SELECT 'require_kyc_for_subs', 0
63+
UNION ALL SELECT 'enable_advanced_analytics', 0
64+
UNION ALL SELECT 'enable_api_webhooks', 0
65+
UNION ALL SELECT 'enable_custom_branding', 0
66+
UNION ALL SELECT 'enable_priority_support', 0
67+
UNION ALL SELECT 'enable_bulk_operations', 0
68+
) AS f
7069
WHERE NOT EXISTS (
7170
SELECT 1 FROM tenant_configurations tc
7271
WHERE tc.tenant_id = t.id AND tc.flag_name = f.flag_name
7372
)
7473
`);
7574
};
7675

76+
7777
exports.down = async function(knex) {
7878
await knex.schema.dropTableIfExists('feature_flag_audit_log');
7979
await knex.schema.dropTableIfExists('tenant_configurations');

0 commit comments

Comments
 (0)