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.
- package/LICENSE +21 -0
- package/README.md +216 -0
- package/dist/__tests__/nostr-auth.middleware.test.d.ts +1 -0
- package/dist/__tests__/nostr-auth.middleware.test.js +104 -0
- package/dist/config/index.d.ts +7 -0
- package/dist/config/index.js +148 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config.d.ts +3 -0
- package/dist/config.js +68 -0
- package/dist/config.js.map +1 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.js +39 -0
- package/dist/index.js.map +1 -0
- package/dist/interfaces/nostr.interface.d.ts +29 -0
- package/dist/interfaces/nostr.interface.js +1 -0
- package/dist/middleware/nostr-auth.middleware.d.ts +14 -0
- package/dist/middleware/nostr-auth.middleware.js +102 -0
- package/dist/middleware/nostr-auth.middleware.js.map +1 -0
- package/dist/middleware/security.middleware.d.ts +5 -0
- package/dist/middleware/security.middleware.js +55 -0
- package/dist/middleware/security.middleware.js.map +1 -0
- package/dist/models/nostr-profile.d.ts +9 -0
- package/dist/models/nostr-profile.js +1 -0
- package/dist/scripts/generate-keypair.js +15 -0
- package/dist/scripts/tests/create-test-user.js +19 -0
- package/dist/scripts/tests/test-auth-live.js +156 -0
- package/dist/server.d.ts +1 -0
- package/dist/server.js +67 -0
- package/dist/server.js.map +1 -0
- package/dist/services/nostr.service.d.ts +11 -0
- package/dist/services/nostr.service.js +110 -0
- package/dist/services/nostr.service.js.map +1 -0
- package/dist/src/config/index.js +148 -0
- package/dist/src/config.js +60 -0
- package/dist/src/index.js +39 -0
- package/dist/src/middleware/nostr-auth.middleware.js +120 -0
- package/dist/src/middleware/security.middleware.js +55 -0
- package/dist/src/server.js +67 -0
- package/dist/src/services/nostr.service.js +287 -0
- package/dist/src/types/index.js +1 -0
- package/dist/src/utils/api-key.utils.js +72 -0
- package/dist/src/utils/crypto.utils.js +81 -0
- package/dist/src/utils/domain.utils.js +67 -0
- package/dist/src/utils/logger.js +25 -0
- package/dist/src/utils/types.js +1 -0
- package/dist/src/validators/event.validator.js +144 -0
- package/dist/types/index.d.ts +58 -0
- package/dist/types/index.js +1 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types.d.ts +57 -0
- package/dist/types.js +1 -0
- package/dist/utils/api-key.utils.d.ts +10 -0
- package/dist/utils/api-key.utils.js +65 -0
- package/dist/utils/api-key.utils.js.map +1 -0
- package/dist/utils/crypto.utils.d.ts +9 -0
- package/dist/utils/crypto.utils.js +80 -0
- package/dist/utils/crypto.utils.js.map +1 -0
- package/dist/utils/domain.utils.d.ts +31 -0
- package/dist/utils/domain.utils.js +67 -0
- package/dist/utils/domain.utils.js.map +1 -0
- package/dist/utils/jwt.utils.d.ts +4 -0
- package/dist/utils/jwt.utils.js +22 -0
- package/dist/utils/logger.d.ts +2 -0
- package/dist/utils/logger.js +25 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/types.d.ts +14 -0
- package/dist/utils/types.js +1 -0
- package/dist/utils/types.js.map +1 -0
- package/dist/validators/event.validator.d.ts +14 -0
- package/dist/validators/event.validator.js +137 -0
- package/dist/validators/event.validator.js.map +1 -0
- package/dist/validators/nostr-event.validator.d.ts +4 -0
- package/dist/validators/nostr-event.validator.js +14 -0
- 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
|
+
});
|