nostr-auth-middleware 0.2.7

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 (74) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +216 -0
  3. package/dist/__tests__/nostr-auth.middleware.test.d.ts +1 -0
  4. package/dist/__tests__/nostr-auth.middleware.test.js +104 -0
  5. package/dist/config/index.d.ts +7 -0
  6. package/dist/config/index.js +148 -0
  7. package/dist/config/index.js.map +1 -0
  8. package/dist/config.d.ts +3 -0
  9. package/dist/config.js +68 -0
  10. package/dist/config.js.map +1 -0
  11. package/dist/index.d.ts +33 -0
  12. package/dist/index.js +39 -0
  13. package/dist/index.js.map +1 -0
  14. package/dist/interfaces/nostr.interface.d.ts +29 -0
  15. package/dist/interfaces/nostr.interface.js +1 -0
  16. package/dist/middleware/nostr-auth.middleware.d.ts +14 -0
  17. package/dist/middleware/nostr-auth.middleware.js +102 -0
  18. package/dist/middleware/nostr-auth.middleware.js.map +1 -0
  19. package/dist/middleware/security.middleware.d.ts +5 -0
  20. package/dist/middleware/security.middleware.js +55 -0
  21. package/dist/middleware/security.middleware.js.map +1 -0
  22. package/dist/models/nostr-profile.d.ts +9 -0
  23. package/dist/models/nostr-profile.js +1 -0
  24. package/dist/scripts/generate-keypair.js +15 -0
  25. package/dist/scripts/tests/create-test-user.js +19 -0
  26. package/dist/scripts/tests/test-auth-live.js +156 -0
  27. package/dist/server.d.ts +1 -0
  28. package/dist/server.js +67 -0
  29. package/dist/server.js.map +1 -0
  30. package/dist/services/nostr.service.d.ts +11 -0
  31. package/dist/services/nostr.service.js +110 -0
  32. package/dist/services/nostr.service.js.map +1 -0
  33. package/dist/src/config/index.js +148 -0
  34. package/dist/src/config.js +60 -0
  35. package/dist/src/index.js +39 -0
  36. package/dist/src/middleware/nostr-auth.middleware.js +120 -0
  37. package/dist/src/middleware/security.middleware.js +55 -0
  38. package/dist/src/server.js +67 -0
  39. package/dist/src/services/nostr.service.js +287 -0
  40. package/dist/src/types/index.js +1 -0
  41. package/dist/src/utils/api-key.utils.js +72 -0
  42. package/dist/src/utils/crypto.utils.js +81 -0
  43. package/dist/src/utils/domain.utils.js +67 -0
  44. package/dist/src/utils/logger.js +25 -0
  45. package/dist/src/utils/types.js +1 -0
  46. package/dist/src/validators/event.validator.js +144 -0
  47. package/dist/types/index.d.ts +58 -0
  48. package/dist/types/index.js +1 -0
  49. package/dist/types/index.js.map +1 -0
  50. package/dist/types.d.ts +57 -0
  51. package/dist/types.js +1 -0
  52. package/dist/utils/api-key.utils.d.ts +10 -0
  53. package/dist/utils/api-key.utils.js +65 -0
  54. package/dist/utils/api-key.utils.js.map +1 -0
  55. package/dist/utils/crypto.utils.d.ts +9 -0
  56. package/dist/utils/crypto.utils.js +80 -0
  57. package/dist/utils/crypto.utils.js.map +1 -0
  58. package/dist/utils/domain.utils.d.ts +31 -0
  59. package/dist/utils/domain.utils.js +67 -0
  60. package/dist/utils/domain.utils.js.map +1 -0
  61. package/dist/utils/jwt.utils.d.ts +4 -0
  62. package/dist/utils/jwt.utils.js +22 -0
  63. package/dist/utils/logger.d.ts +2 -0
  64. package/dist/utils/logger.js +25 -0
  65. package/dist/utils/logger.js.map +1 -0
  66. package/dist/utils/types.d.ts +14 -0
  67. package/dist/utils/types.js +1 -0
  68. package/dist/utils/types.js.map +1 -0
  69. package/dist/validators/event.validator.d.ts +14 -0
  70. package/dist/validators/event.validator.js +137 -0
  71. package/dist/validators/event.validator.js.map +1 -0
  72. package/dist/validators/nostr-event.validator.d.ts +4 -0
  73. package/dist/validators/nostr-event.validator.js +14 -0
  74. package/package.json +120 -0
@@ -0,0 +1,148 @@
1
+ import { createLogger } from '../utils/logger.js';
2
+ import { generateKeyPair } from '../utils/crypto.utils.js';
3
+ import { createClient } from '@supabase/supabase-js';
4
+ import { writeFileSync, readFileSync } from 'fs';
5
+ import { resolve } from 'path';
6
+ import dotenv from 'dotenv';
7
+ const logger = createLogger('Config');
8
+ // Initialize config with default values
9
+ export const config = {
10
+ // Server config
11
+ port: parseInt(process.env.PORT || '3002'),
12
+ nodeEnv: process.env.NODE_ENV || 'development',
13
+ corsOrigins: process.env.CORS_ORIGINS?.split(',') || '*',
14
+ security: {
15
+ trustedProxies: process.env.TRUSTED_PROXIES?.split(',') || false,
16
+ allowedIPs: process.env.ALLOWED_IPS?.split(',') || [],
17
+ apiKeys: process.env.API_KEYS?.split(',') || [],
18
+ rateLimitWindowMs: parseInt(process.env.RATE_LIMIT_WINDOW_MS || '900000'),
19
+ rateLimitMaxRequests: parseInt(process.env.RATE_LIMIT_MAX_REQUESTS || '100'),
20
+ },
21
+ // Supabase config
22
+ supabaseUrl: process.env.SUPABASE_URL,
23
+ supabaseKey: process.env.SUPABASE_KEY,
24
+ // Nostr config
25
+ nostrRelays: process.env.NOSTR_RELAYS?.split(',') || [
26
+ 'wss://relay.maiqr.app',
27
+ 'wss://relay.damus.io',
28
+ 'wss://relay.nostr.band'
29
+ ],
30
+ privateKey: process.env.SERVER_PRIVATE_KEY,
31
+ keyManagementMode: process.env.KEY_MANAGEMENT_MODE || 'development',
32
+ // Auth config
33
+ jwtSecret: process.env.JWT_SECRET || 'maiqr_nostr_auth_secret_key_2024',
34
+ jwtExpiresIn: '1h',
35
+ testMode: process.env.TEST_MODE === 'true',
36
+ // Optional configs
37
+ eventTimeoutMs: 5000,
38
+ challengePrefix: 'nostr:auth:'
39
+ };
40
+ export async function loadConfig(envPath) {
41
+ // Load environment variables
42
+ if (envPath) {
43
+ dotenv.config({ path: envPath });
44
+ }
45
+ else {
46
+ dotenv.config();
47
+ }
48
+ const loadedConfig = {
49
+ // Server config
50
+ port: parseInt(process.env.PORT || '3002'),
51
+ nodeEnv: process.env.NODE_ENV || 'development',
52
+ corsOrigins: process.env.CORS_ORIGINS?.split(',') || '*',
53
+ security: {
54
+ trustedProxies: process.env.TRUSTED_PROXIES?.split(',') || false,
55
+ allowedIPs: process.env.ALLOWED_IPS?.split(',') || [],
56
+ apiKeys: process.env.API_KEYS?.split(',') || [],
57
+ rateLimitWindowMs: parseInt(process.env.RATE_LIMIT_WINDOW_MS || '900000'),
58
+ rateLimitMaxRequests: parseInt(process.env.RATE_LIMIT_MAX_REQUESTS || '100'),
59
+ },
60
+ // Supabase config
61
+ supabaseUrl: process.env.SUPABASE_URL,
62
+ supabaseKey: process.env.SUPABASE_KEY,
63
+ // Nostr config
64
+ nostrRelays: process.env.NOSTR_RELAYS?.split(',') || [
65
+ 'wss://relay.maiqr.app',
66
+ 'wss://relay.damus.io',
67
+ 'wss://relay.nostr.band'
68
+ ],
69
+ privateKey: process.env.SERVER_PRIVATE_KEY,
70
+ publicKey: process.env.SERVER_PUBLIC_KEY,
71
+ keyManagementMode: process.env.KEY_MANAGEMENT_MODE,
72
+ // Auth config
73
+ jwtSecret: process.env.JWT_SECRET,
74
+ jwtExpiresIn: process.env.JWT_EXPIRES_IN || '24h',
75
+ testMode: process.env.NODE_ENV !== 'production',
76
+ // Optional configs
77
+ eventTimeoutMs: parseInt(process.env.EVENT_TIMEOUT_MS || '5000'),
78
+ challengePrefix: process.env.CHALLENGE_PREFIX || 'nostr:auth:'
79
+ };
80
+ // Try to load keys from environment
81
+ if (process.env.SERVER_PRIVATE_KEY) {
82
+ const keyPair = await generateKeyPair();
83
+ loadedConfig.privateKey = process.env.SERVER_PRIVATE_KEY;
84
+ loadedConfig.publicKey = keyPair.publicKey;
85
+ logger.info('Loaded server keys from environment');
86
+ return loadedConfig;
87
+ }
88
+ // If in production, try to load from Supabase
89
+ if (!loadedConfig.testMode && loadedConfig.supabaseUrl && loadedConfig.supabaseKey) {
90
+ const supabase = createClient(loadedConfig.supabaseUrl, loadedConfig.supabaseKey);
91
+ try {
92
+ const { data, error } = await supabase
93
+ .from('server_keys')
94
+ .select('private_key, public_key')
95
+ .single();
96
+ if (error)
97
+ throw error;
98
+ if (data) {
99
+ loadedConfig.privateKey = data.private_key;
100
+ loadedConfig.publicKey = data.public_key;
101
+ logger.info('Loaded server keys from Supabase');
102
+ return loadedConfig;
103
+ }
104
+ }
105
+ catch (error) {
106
+ logger.warn('Failed to load keys from Supabase:', error);
107
+ }
108
+ }
109
+ // Generate new keys if none exist
110
+ logger.warn('No server keys found - generating new keypair');
111
+ const keyPair = await generateKeyPair();
112
+ loadedConfig.privateKey = Buffer.from(keyPair.privateKey).toString('hex');
113
+ loadedConfig.publicKey = keyPair.publicKey;
114
+ // Save to .env file in development
115
+ if (loadedConfig.testMode) {
116
+ try {
117
+ const envPath = resolve(process.cwd(), '.env');
118
+ const envContent = readFileSync(envPath, 'utf8');
119
+ const updatedContent = envContent
120
+ .replace(/^SERVER_PRIVATE_KEY=.*$/m, `SERVER_PRIVATE_KEY=${loadedConfig.privateKey}`)
121
+ .replace(/^SERVER_PUBLIC_KEY=.*$/m, `SERVER_PUBLIC_KEY=${loadedConfig.publicKey}`);
122
+ writeFileSync(envPath, updatedContent);
123
+ logger.info('Saved new server keys to .env file');
124
+ }
125
+ catch (error) {
126
+ logger.warn('Failed to save keys to .env file:', error);
127
+ }
128
+ }
129
+ // Save to Supabase in production
130
+ else if (loadedConfig.supabaseUrl && loadedConfig.supabaseKey) {
131
+ const supabase = createClient(loadedConfig.supabaseUrl, loadedConfig.supabaseKey);
132
+ try {
133
+ const { error } = await supabase
134
+ .from('server_keys')
135
+ .upsert({
136
+ private_key: loadedConfig.privateKey,
137
+ public_key: loadedConfig.publicKey
138
+ });
139
+ if (error)
140
+ throw error;
141
+ logger.info('Saved new server keys to Supabase');
142
+ }
143
+ catch (error) {
144
+ logger.warn('Failed to save keys to Supabase:', error);
145
+ }
146
+ }
147
+ return loadedConfig;
148
+ }
@@ -0,0 +1,60 @@
1
+ import dotenv from 'dotenv';
2
+ import { createLogger } from './utils/logger.js';
3
+ import { hexToBytes } from '@noble/hashes/utils';
4
+ import { getPublicKey } from './utils/crypto.utils.js';
5
+ dotenv.config();
6
+ const logger = createLogger('Config');
7
+ function getEnvWithWarning(key) {
8
+ const value = process.env[key];
9
+ if (!value) {
10
+ logger.warn(`Missing environment variable: ${key} - Some features may be limited`);
11
+ }
12
+ return value;
13
+ }
14
+ function validatePrivateKey(key) {
15
+ if (!key) {
16
+ throw new Error('SERVER_PRIVATE_KEY is required');
17
+ }
18
+ try {
19
+ // Validate key format
20
+ if (!/^[0-9a-f]{64}$/.test(key)) {
21
+ throw new Error('SERVER_PRIVATE_KEY must be a 64-character hex string');
22
+ }
23
+ // Test key derivation
24
+ const privateKeyBytes = hexToBytes(key);
25
+ const pubkey = getPublicKey(privateKeyBytes);
26
+ logger.info('Server keys validated. Public key:', pubkey);
27
+ return key;
28
+ }
29
+ catch (error) {
30
+ logger.error('Invalid SERVER_PRIVATE_KEY:', error);
31
+ throw error;
32
+ }
33
+ }
34
+ export const config = {
35
+ // Server config
36
+ port: parseInt(process.env.PORT || '3002', 10),
37
+ nodeEnv: process.env.NODE_ENV || 'development',
38
+ // CORS
39
+ corsOrigins: process.env.CORS_ORIGINS?.split(',') || '*',
40
+ // Nostr config
41
+ nostrRelays: process.env.NOSTR_RELAYS?.split(',') || [
42
+ 'wss://relay.damus.io',
43
+ 'wss://relay.nostr.band'
44
+ ],
45
+ // Supabase config - optional for testing
46
+ supabaseUrl: getEnvWithWarning('SUPABASE_URL'),
47
+ supabaseKey: getEnvWithWarning('SUPABASE_KEY'),
48
+ // JWT config - optional for testing
49
+ jwtSecret: getEnvWithWarning('JWT_SECRET'),
50
+ jwtExpiresIn: process.env.JWT_EXPIRES_IN || '24h',
51
+ // Key Management Mode
52
+ keyManagementMode: (process.env.KEY_MANAGEMENT_MODE === 'production' ? 'production' : 'development'),
53
+ // Server key pair - required and validated
54
+ privateKey: validatePrivateKey(process.env.SERVER_PRIVATE_KEY),
55
+ publicKey: process.env.SERVER_PUBLIC_KEY,
56
+ // Testing mode - disables Supabase and JWT requirements
57
+ testMode: process.env.TEST_MODE === 'true',
58
+ // Logging
59
+ logLevel: process.env.LOG_LEVEL || 'info'
60
+ };
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Main exports for the Nostr Auth Middleware package
3
+ * @module @maiqr/nostr-auth-enroll
4
+ */
5
+ // Core middleware
6
+ import { NostrAuthMiddleware } from './middleware/nostr-auth.middleware.js';
7
+ // Re-export middleware
8
+ export { NostrAuthMiddleware };
9
+ // Crypto utilities
10
+ export { generateChallenge, generateEventHash, getPublicKey, verifySignature } from './utils/crypto.utils.js';
11
+ // Services
12
+ export { NostrService } from './services/nostr.service.js';
13
+ // Validators
14
+ export { NostrEventValidator } from './validators/event.validator.js';
15
+ // Configuration
16
+ export { config } from './config.js';
17
+ /**
18
+ * Create and configure a new Nostr Auth Middleware instance
19
+ * @param config Configuration options for the middleware
20
+ * @returns Configured NostrAuthMiddleware instance
21
+ *
22
+ * @example
23
+ * ```typescript
24
+ * import { createNostrAuth } from '@maiqr/nostr-auth-enroll';
25
+ *
26
+ * const nostrAuth = createNostrAuth({
27
+ * supabaseUrl: process.env.SUPABASE_URL,
28
+ * supabaseKey: process.env.SUPABASE_KEY,
29
+ * privateKey: process.env.SERVER_PRIVATE_KEY
30
+ * });
31
+ *
32
+ * app.use('/auth/nostr', nostrAuth.router);
33
+ * ```
34
+ */
35
+ export const createNostrAuth = (config) => {
36
+ return new NostrAuthMiddleware(config);
37
+ };
38
+ // Default export for CommonJS compatibility
39
+ export default NostrAuthMiddleware;
@@ -0,0 +1,120 @@
1
+ import { Router } from 'express';
2
+ import { NostrService } from '../services/nostr.service.js';
3
+ import { createLogger } from '../utils/logger.js';
4
+ const logger = createLogger('NostrAuthMiddleware');
5
+ export class NostrAuthMiddleware {
6
+ constructor(config) {
7
+ this.config = config;
8
+ this.router = Router();
9
+ this.nostrService = new NostrService(config);
10
+ this.setupRoutes();
11
+ }
12
+ setupRoutes() {
13
+ this.router.post('/challenge', this.handleChallenge.bind(this));
14
+ this.router.post('/verify', this.handleVerify.bind(this));
15
+ this.router.post('/enroll', this.handleEnroll.bind(this));
16
+ this.router.post('/enroll/verify', this.handleVerifyEnrollment.bind(this));
17
+ this.router.get('/profile/:pubkey', this.handleProfileFetch.bind(this));
18
+ }
19
+ async handleChallenge(req, res, next) {
20
+ try {
21
+ const { pubkey } = req.body;
22
+ if (!pubkey) {
23
+ res.status(400).json({ error: 'Missing pubkey' });
24
+ return;
25
+ }
26
+ logger.info('Creating challenge for pubkey:', pubkey);
27
+ const challenge = await this.nostrService.createChallenge(pubkey);
28
+ logger.info('Challenge created:', challenge);
29
+ // Format response to match test expectations
30
+ res.json({
31
+ event: challenge.event,
32
+ challengeId: challenge.id
33
+ });
34
+ }
35
+ catch (error) {
36
+ logger.error('Failed to create challenge:', error);
37
+ res.status(500).json({ error: 'Failed to create challenge' });
38
+ }
39
+ }
40
+ async handleVerify(req, res, next) {
41
+ try {
42
+ const { challengeId, signedEvent } = req.body;
43
+ if (!challengeId || !signedEvent) {
44
+ res.status(400).json({ error: 'Missing challengeId or signedEvent' });
45
+ return;
46
+ }
47
+ logger.info('Verifying challenge:', challengeId);
48
+ const result = await this.nostrService.verifyChallenge(challengeId, signedEvent);
49
+ if (result.success) {
50
+ res.json({
51
+ success: true,
52
+ token: result.token,
53
+ profile: result.profile
54
+ });
55
+ }
56
+ else {
57
+ res.status(401).json({ error: 'Invalid signature' });
58
+ }
59
+ }
60
+ catch (error) {
61
+ logger.error('Failed to verify challenge:', error);
62
+ res.status(500).json({ error: 'Failed to verify challenge' });
63
+ }
64
+ }
65
+ async handleEnroll(req, res, next) {
66
+ try {
67
+ const { pubkey } = req.body;
68
+ if (!pubkey) {
69
+ res.status(400).json({ error: 'Missing pubkey' });
70
+ return;
71
+ }
72
+ logger.info('Starting enrollment for pubkey:', pubkey);
73
+ const enrollment = await this.nostrService.startEnrollment(pubkey);
74
+ logger.info('Enrollment started:', enrollment);
75
+ res.json({ enrollment });
76
+ }
77
+ catch (error) {
78
+ logger.error('Failed to start enrollment:', error);
79
+ res.status(500).json({ error: 'Failed to start enrollment' });
80
+ }
81
+ }
82
+ async handleVerifyEnrollment(req, res, next) {
83
+ try {
84
+ const { signedEvent } = req.body;
85
+ if (!signedEvent) {
86
+ res.status(400).json({ error: 'Missing signedEvent' });
87
+ return;
88
+ }
89
+ logger.info('Verifying enrollment:', signedEvent);
90
+ const result = await this.nostrService.verifyEnrollment(signedEvent);
91
+ if (!result.success) {
92
+ res.status(401).json({ error: result.message });
93
+ return;
94
+ }
95
+ res.json(result);
96
+ }
97
+ catch (error) {
98
+ logger.error('Failed to verify enrollment:', error);
99
+ res.status(500).json({ error: 'Failed to verify enrollment' });
100
+ }
101
+ }
102
+ async handleProfileFetch(req, res, next) {
103
+ try {
104
+ const { pubkey } = req.params;
105
+ if (!pubkey) {
106
+ res.status(400).json({ error: 'Public key is required' });
107
+ return;
108
+ }
109
+ const profile = await this.nostrService.fetchProfile(pubkey);
110
+ res.json(profile);
111
+ }
112
+ catch (error) {
113
+ logger.error('Profile fetch failed:', error);
114
+ res.status(500).json({ error: 'Failed to fetch profile' });
115
+ }
116
+ }
117
+ getRouter() {
118
+ return this.router;
119
+ }
120
+ }
@@ -0,0 +1,55 @@
1
+ import rateLimit from 'express-rate-limit';
2
+ import { createLogger } from '../utils/logger.js';
3
+ const logger = createLogger('SecurityMiddleware');
4
+ // API Key validation
5
+ export const validateApiKey = (req, res, next) => {
6
+ const apiKey = req.header('X-API-Key');
7
+ const validApiKeys = process.env.API_KEYS?.split(',') || [];
8
+ if (!apiKey || !validApiKeys.includes(apiKey)) {
9
+ logger.warn(`Invalid API key attempt from IP: ${req.ip}`);
10
+ return res.status(401).json({ error: 'Invalid API key' });
11
+ }
12
+ next();
13
+ };
14
+ // IP whitelist middleware
15
+ export const ipWhitelist = (req, res, next) => {
16
+ const allowedIPs = process.env.ALLOWED_IPS?.split(',').filter(Boolean) || [];
17
+ // If no IPs are specified, allow all
18
+ if (allowedIPs.length === 0) {
19
+ return next();
20
+ }
21
+ const clientIP = req.ip;
22
+ if (!clientIP || !allowedIPs.includes(clientIP)) {
23
+ logger.warn(`Blocked request from unauthorized IP: ${clientIP || 'unknown'}`);
24
+ return res.status(403).json({ error: 'Access denied' });
25
+ }
26
+ next();
27
+ };
28
+ // Rate limiting configuration
29
+ export const rateLimiter = rateLimit({
30
+ windowMs: parseInt(process.env.RATE_LIMIT_WINDOW_MS || '900000'), // Default 15 minutes
31
+ max: parseInt(process.env.RATE_LIMIT_MAX_REQUESTS || '100'), // Default 100 requests per window
32
+ message: { error: 'Too many requests, please try again later' },
33
+ standardHeaders: true,
34
+ legacyHeaders: false,
35
+ handler: (req, res) => {
36
+ logger.warn(`Rate limit exceeded for IP: ${req.ip}`);
37
+ res.status(429).json({ error: 'Too many requests, please try again later' });
38
+ },
39
+ });
40
+ // Security headers middleware
41
+ export const securityHeaders = (req, res, next) => {
42
+ // Strict Transport Security
43
+ res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
44
+ // Content Security Policy
45
+ res.setHeader('Content-Security-Policy', "default-src 'self'");
46
+ // XSS Protection
47
+ res.setHeader('X-XSS-Protection', '1; mode=block');
48
+ // No Sniff
49
+ res.setHeader('X-Content-Type-Options', 'nosniff');
50
+ // Referrer Policy
51
+ res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
52
+ // Frame Options
53
+ res.setHeader('X-Frame-Options', 'DENY');
54
+ next();
55
+ };
@@ -0,0 +1,67 @@
1
+ import express from 'express';
2
+ import cors from 'cors';
3
+ import helmet from 'helmet';
4
+ import { createLogger } from './utils/logger.js';
5
+ import { NostrAuthMiddleware } from './middleware/nostr-auth.middleware.js';
6
+ import { validateApiKey, ipWhitelist, rateLimiter, securityHeaders } from './middleware/security.middleware.js';
7
+ import { config } from './config/index.js';
8
+ const logger = createLogger('Server');
9
+ const app = express();
10
+ const PORT = process.env.PORT || 3002;
11
+ // Trust proxy if behind a reverse proxy
12
+ app.set('trust proxy', config.security?.trustedProxies || false);
13
+ // Security Middleware
14
+ app.use(helmet());
15
+ app.use(securityHeaders);
16
+ app.use(ipWhitelist);
17
+ app.use(rateLimiter);
18
+ // CORS configuration
19
+ app.use(cors({
20
+ origin: config.corsOrigins || '*',
21
+ methods: ['GET', 'POST', 'OPTIONS'],
22
+ allowedHeaders: ['Content-Type', 'Authorization', 'X-API-Key'],
23
+ credentials: true
24
+ }));
25
+ app.use(express.json());
26
+ // Request logging
27
+ app.use((req, res, next) => {
28
+ logger.info(`${req.method} ${req.url} from ${req.ip}`);
29
+ next();
30
+ });
31
+ // Health check endpoint (no API key required)
32
+ app.get('/health', (req, res) => {
33
+ res.json({ status: 'ok', timestamp: new Date().toISOString() });
34
+ });
35
+ // Initialize Nostr auth middleware
36
+ const nostrAuth = new NostrAuthMiddleware({
37
+ port: config.port,
38
+ nodeEnv: config.nodeEnv,
39
+ corsOrigins: config.corsOrigins,
40
+ nostrRelays: config.nostrRelays ?? [
41
+ 'wss://relay.maiqr.app',
42
+ 'wss://relay.damus.io',
43
+ 'wss://relay.nostr.band'
44
+ ],
45
+ eventTimeoutMs: 5000,
46
+ challengePrefix: 'nostr:auth:',
47
+ supabaseUrl: config.supabaseUrl,
48
+ supabaseKey: config.supabaseKey,
49
+ jwtSecret: config.jwtSecret,
50
+ testMode: config.testMode,
51
+ privateKey: config.privateKey,
52
+ publicKey: config.publicKey,
53
+ keyManagementMode: config.keyManagementMode
54
+ });
55
+ // Mount Nostr auth routes with API key validation
56
+ app.use('/auth/nostr', validateApiKey, nostrAuth.getRouter());
57
+ // Error handling
58
+ app.use((err, req, res, next) => {
59
+ logger.error('Unhandled error:', err);
60
+ res.status(500).json({ error: 'Internal server error' });
61
+ });
62
+ // Start server
63
+ app.listen(PORT, () => {
64
+ logger.info(`Nostr Auth Middleware running on port ${PORT}`);
65
+ logger.info(`Connected to Supabase at ${config.supabaseUrl}`);
66
+ logger.info(`Using Nostr relays: ${config.nostrRelays?.join(', ') ?? 'default relays'}`);
67
+ });