vibe 1
This commit is contained in:
parent
2f899341f5
commit
6a5b9805bc
3
.clinerules
Normal file
3
.clinerules
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
always use Typescript where relevant
|
||||||
|
don't try to modify files in the build folder (eg /dist)
|
||||||
|
don't put css or JS in the index.html - we should follow appropriate CSP policies
|
34
.gitignore
vendored
Normal file
34
.gitignore
vendored
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
|
||||||
|
# Build output
|
||||||
|
/dist/
|
||||||
|
|
||||||
|
# Dependency directories
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# TypeScript cache
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# Environment files
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
|
||||||
|
# IDE files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea/
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
|
||||||
|
# OS files
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
4
.husky/pre-commit
Executable file
4
.husky/pre-commit
Executable file
@ -0,0 +1,4 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
. "$(dirname "$0")/_/husky.sh"
|
||||||
|
|
||||||
|
cd client && npm run validate
|
26
client/.gitignore
vendored
Normal file
26
client/.gitignore
vendored
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
# Dependencies
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# Build output
|
||||||
|
dist/
|
||||||
|
|
||||||
|
# TypeScript cache
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# Environment files
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
|
||||||
|
# IDE files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea/
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
|
||||||
|
# OS files
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
73
client/eslint.config.js
Normal file
73
client/eslint.config.js
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
import tseslint from '@typescript-eslint/eslint-plugin';
|
||||||
|
import tsparser from '@typescript-eslint/parser';
|
||||||
|
import importPlugin from 'eslint-plugin-import';
|
||||||
|
import js from '@eslint/js';
|
||||||
|
|
||||||
|
export default [
|
||||||
|
js.configs.recommended,
|
||||||
|
{
|
||||||
|
ignores: [
|
||||||
|
'dist/**',
|
||||||
|
'build/**',
|
||||||
|
'node_modules/**',
|
||||||
|
'*.min.js',
|
||||||
|
'.eslintcache',
|
||||||
|
'*.tsbuildinfo',
|
||||||
|
'.vscode/**',
|
||||||
|
'.idea/**'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ['**/*.ts'],
|
||||||
|
languageOptions: {
|
||||||
|
parser: tsparser,
|
||||||
|
parserOptions: {
|
||||||
|
ecmaVersion: 2020,
|
||||||
|
sourceType: 'module',
|
||||||
|
project: './tsconfig.json'
|
||||||
|
},
|
||||||
|
ecmaVersion: 2020,
|
||||||
|
sourceType: 'module',
|
||||||
|
globals: {
|
||||||
|
document: 'readonly',
|
||||||
|
navigator: 'readonly',
|
||||||
|
window: 'readonly',
|
||||||
|
console: 'readonly',
|
||||||
|
alert: 'readonly'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
'@typescript-eslint': tseslint,
|
||||||
|
'import': importPlugin
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
// TypeScript specific rules
|
||||||
|
'@typescript-eslint/explicit-module-boundary-types': 'warn',
|
||||||
|
'@typescript-eslint/no-explicit-any': 'warn',
|
||||||
|
'@typescript-eslint/no-unused-vars': ['error', { 'argsIgnorePattern': '^_' }],
|
||||||
|
'@typescript-eslint/consistent-type-definitions': ['error', 'interface'],
|
||||||
|
'@typescript-eslint/consistent-type-imports': 'error',
|
||||||
|
|
||||||
|
// Import rules
|
||||||
|
'import/no-unresolved': 'off', // Turn off because we're using TypeScript paths
|
||||||
|
'import/named': 'error',
|
||||||
|
'import/default': 'error',
|
||||||
|
'import/namespace': 'error',
|
||||||
|
'import/order': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
'groups': ['builtin', 'external', 'internal', 'parent', 'sibling', 'index'],
|
||||||
|
'newlines-between': 'always',
|
||||||
|
'alphabetize': { 'order': 'asc', 'caseInsensitive': true }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
// General ESLint rules
|
||||||
|
'no-console': 'warn',
|
||||||
|
'eqeqeq': ['error', 'always'],
|
||||||
|
'no-var': 'error',
|
||||||
|
'prefer-const': 'error',
|
||||||
|
'curly': 'error',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
BIN
client/http.png
Normal file
BIN
client/http.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 35 KiB |
105
client/index-dist.html
Normal file
105
client/index-dist.html
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<!-- Implement strict Content Security Policy -->
|
||||||
|
<title>HTTP to Kind 21120 Converter</title>
|
||||||
|
<!-- Include the Nostr libraries -->
|
||||||
|
<script src='https://www.unpkg.com/nostr-login@latest/dist/unpkg.js'></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/nostr-tools@latest"></script>
|
||||||
|
<!-- Load our external TypeScript file (compiled to JS) -->
|
||||||
|
<script src="./src/client.js" type="module"></script>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
textarea {
|
||||||
|
width: 100%;
|
||||||
|
height: 200px;
|
||||||
|
font-family: monospace;
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
background-color: #4CAF50;
|
||||||
|
color: white;
|
||||||
|
padding: 10px 15px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
button:hover {
|
||||||
|
background-color: #45a049;
|
||||||
|
}
|
||||||
|
#output {
|
||||||
|
margin-top: 20px;
|
||||||
|
padding: 15px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
}
|
||||||
|
pre {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow-x: auto;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
.info-box {
|
||||||
|
background-color: #e7f3fe;
|
||||||
|
border-left: 6px solid #2196F3;
|
||||||
|
padding: 10px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
h2 {
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>HTTP Request to Kind 21120 Converter</h1>
|
||||||
|
|
||||||
|
<div class="info-box">
|
||||||
|
<p>This tool converts HTTP requests into a single Nostr event of kind 21120. The HTTP request is encrypted using NIP-44 in the content field with appropriate tags following the specification.</p>
|
||||||
|
<p>The server's public key (<strong>npub1r6knexka25dn9w9jnf5kf8xh6gfq7n3p38zfl7nn7cjjjsp4umcqnk0aun</strong>) is pre-populated for your convenience.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2>Server Information:</h2>
|
||||||
|
<div style="margin-bottom: 15px;">
|
||||||
|
<div style="margin-bottom: 10px;">
|
||||||
|
<label for="serverPubkey">Server Pubkey:</label><br>
|
||||||
|
<input type="text" id="serverPubkey" value="npub1r6knexka25dn9w9jnf5kf8xh6gfq7n3p38zfl7nn7cjjjsp4umcqnk0aun" style="width: 100%; padding: 8px;">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="relay">Response Relay (optional):</label><br>
|
||||||
|
<input type="text" id="relay" value="wss://relay.damus.io" style="width: 100%; padding: 8px;">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2>Enter HTTP Request:</h2>
|
||||||
|
<textarea id="httpRequest" placeholder="GET /index.html HTTP/1.1
|
||||||
|
Host: example.com
|
||||||
|
User-Agent: Browser/1.0
|
||||||
|
|
||||||
|
"></textarea>
|
||||||
|
|
||||||
|
<button id="convertButton">Convert to Event</button>
|
||||||
|
|
||||||
|
<div id="output" hidden>
|
||||||
|
<h2>Converted Event:</h2>
|
||||||
|
<pre id="eventOutput"></pre>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
47
client/index.html
Normal file
47
client/index.html
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<!-- Implement strict Content Security Policy -->
|
||||||
|
<title>HTTP to Kind 21120 Converter</title>
|
||||||
|
<!-- Load our CSS file -->
|
||||||
|
<link rel="stylesheet" href="./styles.css">
|
||||||
|
<!-- Include the Nostr extensions - these will be accessed via window.nostr -->
|
||||||
|
<script defer src="./bundle.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>HTTP Request to Kind 21120 Converter</h1>
|
||||||
|
|
||||||
|
<div class="info-box">
|
||||||
|
<p>This tool converts HTTP requests into a single Nostr event of kind 21120. The HTTP request is encrypted using NIP-44 in the content field with appropriate tags following the specification.</p>
|
||||||
|
<p>The server's public key (<strong>npub1r6knexka25dn9w9jnf5kf8xh6gfq7n3p38zfl7nn7cjjjsp4umcqnk0aun</strong>) is pre-populated for your convenience.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2>Server Information:</h2>
|
||||||
|
<div style="margin-bottom: 15px;">
|
||||||
|
<div style="margin-bottom: 10px;">
|
||||||
|
<label for="serverPubkey">Server Pubkey:</label><br>
|
||||||
|
<input type="text" id="serverPubkey" value="npub1r6knexka25dn9w9jnf5kf8xh6gfq7n3p38zfl7nn7cjjjsp4umcqnk0aun" style="width: 100%; padding: 8px;">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="relay">Response Relay (optional):</label><br>
|
||||||
|
<input type="text" id="relay" value="wss://relay.damus.io" style="width: 100%; padding: 8px;">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2>Enter HTTP Request:</h2>
|
||||||
|
<textarea id="httpRequest" placeholder="GET /index.html HTTP/1.1
|
||||||
|
Host: example.com
|
||||||
|
User-Agent: Browser/1.0
|
||||||
|
|
||||||
|
"></textarea>
|
||||||
|
|
||||||
|
<button id="convertButton">Convert to Event</button>
|
||||||
|
|
||||||
|
<div id="output" hidden>
|
||||||
|
<h2>Converted Event:</h2>
|
||||||
|
<pre id="eventOutput"></pre>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
14198
client/package-lock.json
generated
Normal file
14198
client/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
47
client/package.json
Normal file
47
client/package.json
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
{
|
||||||
|
"name": "http-to-nostr-client",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "A client tool to convert HTTP requests to Nostr Kind 21120 events",
|
||||||
|
"main": "dist/index.html",
|
||||||
|
"scripts": {
|
||||||
|
"build": "webpack --mode production",
|
||||||
|
"copy-assets": "mkdir -p dist && cp index.html dist/ && cp http.png dist/ 2>/dev/null || true",
|
||||||
|
"dev": "webpack serve --open",
|
||||||
|
"start": "npm run build && npm run copy-assets && npx serve dist",
|
||||||
|
"clean": "rm -rf dist",
|
||||||
|
"lint": "eslint . --ext .ts",
|
||||||
|
"lint:fix": "eslint . --ext .ts --fix",
|
||||||
|
"validate": "npm run lint && tsc --noEmit",
|
||||||
|
"prepare": "husky"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"nostr",
|
||||||
|
"http",
|
||||||
|
"converter"
|
||||||
|
],
|
||||||
|
"author": "",
|
||||||
|
"license": "MIT",
|
||||||
|
"devDependencies": {
|
||||||
|
"@typescript-eslint/eslint-plugin": "^8.29.0",
|
||||||
|
"@typescript-eslint/parser": "^8.29.0",
|
||||||
|
"buffer": "^6.0.3",
|
||||||
|
"copy-webpack-plugin": "^13.0.0",
|
||||||
|
"crypto-browserify": "^3.12.1",
|
||||||
|
"css-loader": "^7.1.2",
|
||||||
|
"eslint": "^9.24.0",
|
||||||
|
"eslint-plugin-import": "^2.31.0",
|
||||||
|
"husky": "^9.1.7",
|
||||||
|
"process": "^0.11.10",
|
||||||
|
"serve": "^14.0.0",
|
||||||
|
"stream-browserify": "^3.0.0",
|
||||||
|
"style-loader": "^4.0.0",
|
||||||
|
"ts-loader": "^9.5.2",
|
||||||
|
"typescript": "^5.0.0",
|
||||||
|
"webpack": "^5.98.0",
|
||||||
|
"webpack-cli": "^6.0.1",
|
||||||
|
"webpack-dev-server": "^5.2.1"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"nostr-tools": "^2.12.0"
|
||||||
|
}
|
||||||
|
}
|
16
client/src/client.ts
Normal file
16
client/src/client.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
// client.ts - External TypeScript file for HTTP to Nostr converter
|
||||||
|
// This follows strict CSP policies by avoiding inline scripts
|
||||||
|
|
||||||
|
// Import the converter function
|
||||||
|
import { displayConvertedEvent } from './converter';
|
||||||
|
|
||||||
|
// Initialize the event handlers when the DOM is loaded
|
||||||
|
document.addEventListener('DOMContentLoaded', function(): void {
|
||||||
|
// Set up the convert button click handler
|
||||||
|
const convertButton = document.getElementById('convertButton');
|
||||||
|
if (convertButton) {
|
||||||
|
convertButton.addEventListener('click', displayConvertedEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('HTTP to Nostr converter initialized');
|
||||||
|
});
|
17
client/src/config.ts
Normal file
17
client/src/config.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
/**
|
||||||
|
* Client-side configuration for HTTP to Nostr converter
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Default server information
|
||||||
|
export const defaultServerConfig = {
|
||||||
|
// Server's npub (hard-coded to match the server's config)
|
||||||
|
serverNpub: "npub1r6knexka25dn9w9jnf5kf8xh6gfq7n3p38zfl7nn7cjjjsp4umcqnk0aun",
|
||||||
|
// Default relay for responses
|
||||||
|
defaultRelay: "wss://relay.damus.io"
|
||||||
|
};
|
||||||
|
|
||||||
|
// Application settings
|
||||||
|
export const appSettings = {
|
||||||
|
// Expiration time for events in seconds (1 hour)
|
||||||
|
expirationTime: 3600
|
||||||
|
};
|
209
client/src/converter.ts
Normal file
209
client/src/converter.ts
Normal file
@ -0,0 +1,209 @@
|
|||||||
|
// Type definition for the window.nostrTools object
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
nostrTools: any;
|
||||||
|
nostr: any;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
import { defaultServerConfig, appSettings } from './config';
|
||||||
|
import * as nostrTools from 'nostr-tools';
|
||||||
|
|
||||||
|
// Generate a keypair for standalone mode (when no extension is available)
|
||||||
|
let standaloneSecretKey: Uint8Array | null = null;
|
||||||
|
let standalonePublicKey: string | null = null;
|
||||||
|
|
||||||
|
// Initialize the keypair
|
||||||
|
function initStandaloneKeypair(): { publicKey: string, secretKey: Uint8Array } {
|
||||||
|
if (!standaloneSecretKey) {
|
||||||
|
standaloneSecretKey = nostrTools.generateSecretKey();
|
||||||
|
standalonePublicKey = nostrTools.getPublicKey(standaloneSecretKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
publicKey: standalonePublicKey!,
|
||||||
|
secretKey: standaloneSecretKey
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses an HTTP request string and converts it directly to a kind 21120 Nostr event
|
||||||
|
* Following the specification in README.md
|
||||||
|
*
|
||||||
|
* @param httpRequest The raw HTTP request string
|
||||||
|
* @param pubkey The user's public key
|
||||||
|
* @param serverPubkey The public key of the remote HTTP server
|
||||||
|
* @param decryptkey The encryption key (required per specification)
|
||||||
|
* @param relay Optional relay for the response
|
||||||
|
* @returns Stringified Nostr event or null if conversion fails
|
||||||
|
*/
|
||||||
|
export function convertToEvent(
|
||||||
|
httpRequest: string,
|
||||||
|
pubkey: string,
|
||||||
|
serverPubkey: string,
|
||||||
|
decryptkey: string,
|
||||||
|
relay?: string
|
||||||
|
): string | null {
|
||||||
|
if (!httpRequest) {
|
||||||
|
alert('Please enter an HTTP request message.');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a single kind 21120 event with encrypted HTTP request as content
|
||||||
|
// Following the specification in README.md using actual NIP-44 encryption
|
||||||
|
|
||||||
|
// Use the server's pubkey for encryption (not the decryptkey)
|
||||||
|
let encryptedContent = httpRequest;
|
||||||
|
|
||||||
|
// Use actual NIP-44 encryption if available
|
||||||
|
if (window.nostr && window.nostr.nip44) {
|
||||||
|
try {
|
||||||
|
// The second argument MUST be the server's public key in hex format
|
||||||
|
let serverPubkeyHex = serverPubkey;
|
||||||
|
|
||||||
|
// Convert npub to hex if needed using nostr-tools
|
||||||
|
if (serverPubkey.startsWith('npub')) {
|
||||||
|
try {
|
||||||
|
const decoded = nostrTools.nip19.decode(serverPubkey);
|
||||||
|
if (decoded.type === 'npub' && decoded.data) {
|
||||||
|
serverPubkeyHex = decoded.data;
|
||||||
|
console.log("Converted npub to hex format:", serverPubkeyHex);
|
||||||
|
}
|
||||||
|
} catch (decodeError) {
|
||||||
|
console.error("Failed to decode npub:", decodeError);
|
||||||
|
throw new Error("Failed to decode npub. Please use a valid npub format.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate that we have a hex string of the right length
|
||||||
|
if (!/^[0-9a-f]{64}$/i.test(serverPubkeyHex)) {
|
||||||
|
throw new Error("Invalid server pubkey format. Must be a 64-character hex string.");
|
||||||
|
}
|
||||||
|
|
||||||
|
encryptedContent = window.nostr.nip44.encrypt(httpRequest, serverPubkeyHex);
|
||||||
|
console.log("Successfully encrypted content with NIP-44");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error encrypting with NIP-44:", error);
|
||||||
|
throw error; // Re-throw to prevent creating an event with unencrypted content
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.warn("NIP-44 encryption not available. Content will not be encrypted properly.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const event = {
|
||||||
|
kind: 21120,
|
||||||
|
pubkey: pubkey,
|
||||||
|
content: encryptedContent, // Encrypted HTTP request using NIP-44
|
||||||
|
tags: [
|
||||||
|
// Required tags per README specification
|
||||||
|
["p", serverPubkey], // P tag to indicate this is a REQUEST
|
||||||
|
["key", decryptkey], // Key for decryption
|
||||||
|
["expiration", Math.floor(Date.now() / 1000) + appSettings.expirationTime]
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add optional relay tag if provided
|
||||||
|
if (relay) {
|
||||||
|
event.tags.push(["r", relay]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return JSON.stringify(event, null, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// No need to define nostrTools if it's not being used
|
||||||
|
|
||||||
|
export async function displayConvertedEvent(): Promise<void> {
|
||||||
|
const httpRequestBox = document.getElementById('httpRequest') as HTMLTextAreaElement;
|
||||||
|
const eventOutputPre = document.getElementById('eventOutput') as HTMLElement;
|
||||||
|
const outputDiv = document.getElementById('output') as HTMLElement;
|
||||||
|
const serverPubkeyInput = document.getElementById('serverPubkey') as HTMLInputElement;
|
||||||
|
const relayInput = document.getElementById('relay') as HTMLInputElement;
|
||||||
|
|
||||||
|
if (httpRequestBox && eventOutputPre && outputDiv) {
|
||||||
|
// Get server pubkey and relay values from inputs
|
||||||
|
const serverPubkey = serverPubkeyInput && serverPubkeyInput.value ?
|
||||||
|
serverPubkeyInput.value : defaultServerConfig.serverNpub;
|
||||||
|
const relay = relayInput && relayInput.value ?
|
||||||
|
relayInput.value : defaultServerConfig.defaultRelay;
|
||||||
|
// Get or create a keypair
|
||||||
|
let pubkey: string;
|
||||||
|
let secretKey: Uint8Array | null = null;
|
||||||
|
|
||||||
|
// Try to get the pubkey from the NIP-07 extension
|
||||||
|
if (window.nostr) {
|
||||||
|
try {
|
||||||
|
pubkey = await window.nostr.getPublicKey();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to get pubkey from extension:", error);
|
||||||
|
// Fall back to standalone keypair
|
||||||
|
const keypair = initStandaloneKeypair();
|
||||||
|
pubkey = keypair.publicKey;
|
||||||
|
secretKey = keypair.secretKey;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// No extension, use standalone keypair
|
||||||
|
const keypair = initStandaloneKeypair();
|
||||||
|
pubkey = keypair.publicKey;
|
||||||
|
secretKey = keypair.secretKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert directly to a single kind 21120 event
|
||||||
|
const convertedEvent = convertToEvent(
|
||||||
|
httpRequestBox.value,
|
||||||
|
pubkey,
|
||||||
|
serverPubkey,
|
||||||
|
"$decryptkey",
|
||||||
|
relay
|
||||||
|
);
|
||||||
|
|
||||||
|
if (convertedEvent) {
|
||||||
|
// Parse the event to create a proper Nostr event object for signing
|
||||||
|
const parsedEvent = JSON.parse(convertedEvent);
|
||||||
|
const nostrEvent = {
|
||||||
|
kind: 21120,
|
||||||
|
tags: parsedEvent.tags,
|
||||||
|
content: parsedEvent.content,
|
||||||
|
created_at: Math.floor(Date.now() / 1000),
|
||||||
|
pubkey: parsedEvent.pubkey
|
||||||
|
};
|
||||||
|
|
||||||
|
let signedEvent;
|
||||||
|
|
||||||
|
if (window.nostr) {
|
||||||
|
try {
|
||||||
|
// Try to sign with the NIP-07 extension
|
||||||
|
signedEvent = await window.nostr.signEvent(nostrEvent);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error signing event with extension:", error);
|
||||||
|
// Fall back to signing with nostr-tools
|
||||||
|
if (secretKey) {
|
||||||
|
signedEvent = nostrTools.finalizeEvent(nostrEvent, secretKey);
|
||||||
|
} else {
|
||||||
|
eventOutputPre.textContent = "Error: Could not sign event. No extension or private key available.";
|
||||||
|
outputDiv.hidden = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (secretKey) {
|
||||||
|
// Sign with nostr-tools
|
||||||
|
signedEvent = nostrTools.finalizeEvent(nostrEvent, secretKey);
|
||||||
|
} else {
|
||||||
|
eventOutputPre.textContent = "Error: Could not sign event. No extension or private key available.";
|
||||||
|
outputDiv.hidden = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
eventOutputPre.textContent = JSON.stringify(signedEvent, null, 2);
|
||||||
|
|
||||||
|
outputDiv.hidden = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize event listeners when the DOM is fully loaded
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
// Only set up the click event handler without any automatic encryption
|
||||||
|
const convertButton = document.getElementById('convertButton');
|
||||||
|
if (convertButton) {
|
||||||
|
convertButton.addEventListener('click', displayConvertedEvent);
|
||||||
|
}
|
||||||
|
});
|
63
client/src/styles.css
Normal file
63
client/src/styles.css
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
width: 100%;
|
||||||
|
height: 200px;
|
||||||
|
font-family: monospace;
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
background-color: #4CAF50;
|
||||||
|
color: white;
|
||||||
|
padding: 10px 15px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
background-color: #45a049;
|
||||||
|
}
|
||||||
|
|
||||||
|
#output {
|
||||||
|
margin-top: 20px;
|
||||||
|
padding: 15px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow-x: auto;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-box {
|
||||||
|
background-color: #e7f3fe;
|
||||||
|
border-left: 6px solid #2196F3;
|
||||||
|
padding: 10px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
color: #555;
|
||||||
|
}
|
16
client/tsconfig.json
Normal file
16
client/tsconfig.json
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"module": "ES2020",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"outDir": "./dist/src",
|
||||||
|
"sourceMap": true,
|
||||||
|
"lib": ["DOM", "ES2020", "DOM.Iterable", "ScriptHost"],
|
||||||
|
"allowSyntheticDefaultImports": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"]
|
||||||
|
}
|
58
client/webpack.config.js
Normal file
58
client/webpack.config.js
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
const path = require('path');
|
||||||
|
const CopyPlugin = require('copy-webpack-plugin');
|
||||||
|
const webpack = require('webpack');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
mode: 'development',
|
||||||
|
entry: './src/client.ts',
|
||||||
|
// Ensure webpack creates browser-compatible output
|
||||||
|
target: 'web',
|
||||||
|
module: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
test: /\.tsx?$/,
|
||||||
|
use: 'ts-loader',
|
||||||
|
exclude: /node_modules/,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.css$/,
|
||||||
|
use: ['style-loader', 'css-loader'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
new webpack.ProvidePlugin({
|
||||||
|
Buffer: ['buffer', 'Buffer'],
|
||||||
|
process: 'process/browser',
|
||||||
|
}),
|
||||||
|
new CopyPlugin({
|
||||||
|
patterns: [
|
||||||
|
{ from: 'src/styles.css', to: 'styles.css' },
|
||||||
|
{ from: 'http.png', to: 'http.png' },
|
||||||
|
{ from: 'index.html', to: 'index.html' },
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
resolve: {
|
||||||
|
extensions: ['.tsx', '.ts', '.js'],
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
filename: 'bundle.js',
|
||||||
|
path: path.resolve(__dirname, 'dist'),
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
extensions: ['.tsx', '.ts', '.js'],
|
||||||
|
fallback: {
|
||||||
|
"crypto": false,
|
||||||
|
"buffer": false,
|
||||||
|
"stream": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
devServer: {
|
||||||
|
static: {
|
||||||
|
directory: path.join(__dirname, 'dist'),
|
||||||
|
},
|
||||||
|
compress: true,
|
||||||
|
port: 3000,
|
||||||
|
},
|
||||||
|
};
|
102
index.html
Normal file
102
index.html
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>HTTP to Kind 21120 Converter</title>
|
||||||
|
<script src='https://www.unpkg.com/nostr-login@latest/dist/unpkg.js'></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/nostr-tools@latest"></script>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
textarea {
|
||||||
|
width: 100%;
|
||||||
|
height: 200px;
|
||||||
|
font-family: monospace;
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
background-color: #4CAF50;
|
||||||
|
color: white;
|
||||||
|
padding: 10px 15px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
button:hover {
|
||||||
|
background-color: #45a049;
|
||||||
|
}
|
||||||
|
#output {
|
||||||
|
margin-top: 20px;
|
||||||
|
padding: 15px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
}
|
||||||
|
pre {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow-x: auto;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
.info-box {
|
||||||
|
background-color: #e7f3fe;
|
||||||
|
border-left: 6px solid #2196F3;
|
||||||
|
padding: 10px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
h2 {
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>HTTP Request to Kind 21120 Converter</h1>
|
||||||
|
|
||||||
|
<div class="info-box">
|
||||||
|
<p>This tool converts HTTP requests into a single Nostr event of kind 21120. The HTTP request is placed directly in the content field with appropriate tags following the specification.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2>Server Information:</h2>
|
||||||
|
<div style="margin-bottom: 15px;">
|
||||||
|
<div style="margin-bottom: 10px;">
|
||||||
|
<label for="serverPubkey">Server Pubkey:</label><br>
|
||||||
|
<input type="text" id="serverPubkey" placeholder="Enter server pubkey" style="width: 100%; padding: 8px;">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="relay">Response Relay (optional):</label><br>
|
||||||
|
<input type="text" id="relay" placeholder="wss://relay.example.com" style="width: 100%; padding: 8px;">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2>Enter HTTP Request:</h2>
|
||||||
|
<textarea id="httpRequest" placeholder="GET /index.html HTTP/1.1
|
||||||
|
Host: example.com
|
||||||
|
User-Agent: Browser/1.0
|
||||||
|
|
||||||
|
"></textarea>
|
||||||
|
|
||||||
|
<button id="convertButton">Convert to Event</button>
|
||||||
|
|
||||||
|
<div id="output" hidden>
|
||||||
|
<h2>Converted Event:</h2>
|
||||||
|
<pre id="eventOutput"></pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="./src/converter.js" type="module"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
26
package.json
Normal file
26
package.json
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"name": "http-to-nostr-project",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "HTTP to Nostr kind 21120 converter and server",
|
||||||
|
"scripts": {
|
||||||
|
"install:all": "npm run install:client && npm run install:server",
|
||||||
|
"install:client": "cd client && npm install",
|
||||||
|
"install:server": "cd server && npm install",
|
||||||
|
"build:all": "npm run build:client && npm run build:server",
|
||||||
|
"build:client": "cd client && npm run build",
|
||||||
|
"build:server": "cd server && npm run build",
|
||||||
|
"start:client": "cd client && npm run start",
|
||||||
|
"start:server": "cd server && npm run start",
|
||||||
|
"dev:server": "cd server && npm run dev",
|
||||||
|
"clean:all": "npm run clean:client && npm run clean:server",
|
||||||
|
"clean:client": "cd client && npm run clean",
|
||||||
|
"clean:server": "cd server && npm run clean"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"nostr",
|
||||||
|
"http",
|
||||||
|
"converter"
|
||||||
|
],
|
||||||
|
"author": "",
|
||||||
|
"license": "MIT"
|
||||||
|
}
|
31
server/.gitignore
vendored
Normal file
31
server/.gitignore
vendored
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
# Dependencies
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# Build output
|
||||||
|
dist/
|
||||||
|
|
||||||
|
# TypeScript cache
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# Environment files
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
|
||||||
|
# IDE files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea/
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
|
||||||
|
# OS files
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
36
server/package.json
Normal file
36
server/package.json
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
"name": "http-to-nostr-server",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "A server for processing HTTP request events from Nostr Kind 21120",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc",
|
||||||
|
"start": "node dist/index.js",
|
||||||
|
"dev": "ts-node src/index.ts",
|
||||||
|
"watch": "nodemon --exec ts-node src/index.ts",
|
||||||
|
"clean": "rm -rf dist"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"nostr",
|
||||||
|
"http",
|
||||||
|
"server"
|
||||||
|
],
|
||||||
|
"author": "",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@nostr-dev-kit/ndk": "^2.0.0",
|
||||||
|
"express": "^4.18.2",
|
||||||
|
"node-fetch": "^3.3.2",
|
||||||
|
"websocket-polyfill": "^0.0.3",
|
||||||
|
"ws": "^8.14.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/express": "^4.17.18",
|
||||||
|
"@types/node": "^20.8.4",
|
||||||
|
"@types/ws": "^8.5.6",
|
||||||
|
"nodemon": "^3.0.1",
|
||||||
|
"ts-node": "^10.9.1",
|
||||||
|
"typescript": "^5.2.2"
|
||||||
|
},
|
||||||
|
"type": "module"
|
||||||
|
}
|
21
server/src/config.ts
Normal file
21
server/src/config.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
// Hard-coded Nostr keys for the server
|
||||||
|
export const serverKeys = {
|
||||||
|
// These are test keys - DO NOT USE IN PRODUCTION
|
||||||
|
privateKeyHex: "d5d49f5d14c8e59fafb5858dd27712674b8f8c53f1d72dae4ee69bd201068c68",
|
||||||
|
publicKeyHex: "f3c75b9243135a548bc64545dcfd7e8725e20faeacb5c18ac581d7a010036f40",
|
||||||
|
npub: "npub1r6knexka25dn9w9jnf5kf8xh6gfq7n3p38zfl7nn7cjjjsp4umcqnk0aun",
|
||||||
|
nsec: "nsec1hsdfy8ch8y0zw8g0np0mzk3chg94y2t5sek7hh9xpv565h5ucqpqgf4t93"
|
||||||
|
};
|
||||||
|
|
||||||
|
// Relay configuration
|
||||||
|
export const relayUrls = [
|
||||||
|
'wss://relay.damus.io',
|
||||||
|
'wss://relay.nostr.info',
|
||||||
|
'wss://nostr.fmt.wiz.biz'
|
||||||
|
];
|
||||||
|
|
||||||
|
// Server configuration
|
||||||
|
export const serverConfig = {
|
||||||
|
port: process.env.PORT || 3000,
|
||||||
|
expirationTime: 3600 // 1 hour in seconds
|
||||||
|
};
|
39
server/src/generate-keys.ts
Normal file
39
server/src/generate-keys.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import * as crypto from 'crypto';
|
||||||
|
import * as nostrTools from 'nostr-tools';
|
||||||
|
|
||||||
|
// Generate a new random private key
|
||||||
|
function generatePrivateKey(): string {
|
||||||
|
return nostrTools.generatePrivateKey();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Derive public key from private key
|
||||||
|
function getPublicKey(privateKey: string): string {
|
||||||
|
return nostrTools.getPublicKey(privateKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert hex public key to npub format
|
||||||
|
function convertToNpub(publicKey: string): string {
|
||||||
|
return nostrTools.nip19.npubEncode(publicKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert hex private key to nsec format
|
||||||
|
function convertToNsec(privateKey: string): string {
|
||||||
|
return nostrTools.nip19.nsecEncode(privateKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate and display keys
|
||||||
|
function main() {
|
||||||
|
const privateKey = generatePrivateKey();
|
||||||
|
const publicKey = getPublicKey(privateKey);
|
||||||
|
const npub = convertToNpub(publicKey);
|
||||||
|
const nsec = convertToNsec(privateKey);
|
||||||
|
|
||||||
|
console.log('Generated Nostr Key Pair:');
|
||||||
|
console.log('------------------------');
|
||||||
|
console.log('Private Key (hex):', privateKey);
|
||||||
|
console.log('Public Key (hex):', publicKey);
|
||||||
|
console.log('NPUB:', npub);
|
||||||
|
console.log('NSEC:', nsec);
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
272
server/src/index.ts
Normal file
272
server/src/index.ts
Normal file
@ -0,0 +1,272 @@
|
|||||||
|
import express from 'express';
|
||||||
|
import { WebSocket, WebSocketServer } from 'ws';
|
||||||
|
import { NDKEvent, NDK, NDKPrivateKeySigner, NDKFilter, NDKRelay } from '@nostr-dev-kit/ndk';
|
||||||
|
import { createServer } from 'http';
|
||||||
|
import fetch from 'node-fetch';
|
||||||
|
import { serverKeys, relayUrls, serverConfig } from './config';
|
||||||
|
import * as crypto from 'crypto';
|
||||||
|
// Configuration
|
||||||
|
const config = {
|
||||||
|
port: serverConfig.port,
|
||||||
|
relayUrls: relayUrls,
|
||||||
|
privateKey: serverKeys.privateKeyHex,
|
||||||
|
pubkey: serverKeys.publicKeyHex,
|
||||||
|
npub: serverKeys.npub
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log(`Server running with npub: ${config.npub}`);
|
||||||
|
|
||||||
|
// Create NDK instance with the private key signer
|
||||||
|
const signer = new NDKPrivateKeySigner(config.privateKey);
|
||||||
|
const ndk = new NDK({
|
||||||
|
explicitRelayUrls: config.relayUrls,
|
||||||
|
signer
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initialize NDK
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
await ndk.connect();
|
||||||
|
console.log('NDK Connected to relays');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to connect to relays:', error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
// Set up Express server
|
||||||
|
const app = express();
|
||||||
|
const server = createServer(app);
|
||||||
|
|
||||||
|
app.use(express.json());
|
||||||
|
|
||||||
|
// Basic route to display server info
|
||||||
|
app.get('/', (req, res) => {
|
||||||
|
res.send(`
|
||||||
|
<h1>HTTP Request Server</h1>
|
||||||
|
<p>This server listens for Nostr kind 21120 events and processes HTTP requests.</p>
|
||||||
|
<p>Server pubkey: ${config.pubkey}</p>
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Start the HTTP server
|
||||||
|
server.listen(config.port, () => {
|
||||||
|
console.log(`Server running on http://localhost:${config.port}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Subscribe to kind 21120 events
|
||||||
|
async function subscribeToEvents() {
|
||||||
|
try {
|
||||||
|
// Create filter for kind 21120 events addressed to us
|
||||||
|
const filter: NDKFilter = {
|
||||||
|
kinds: [21120],
|
||||||
|
'#p': [config.pubkey]
|
||||||
|
};
|
||||||
|
|
||||||
|
// Subscribe to events
|
||||||
|
ndk.subscribe(filter, {
|
||||||
|
closeOnEose: false,
|
||||||
|
// Handle each received event
|
||||||
|
callback: (event: NDKEvent) => {
|
||||||
|
processEvent(event);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Subscribed to events with filter:', filter);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error subscribing to events:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start subscriptions
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
await subscribeToEvents();
|
||||||
|
console.log('Event subscriptions started');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to start subscriptions:', error);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
// Process incoming HTTP request events
|
||||||
|
async function processEvent(event: NDKEvent) {
|
||||||
|
console.log('Received event:', event.id);
|
||||||
|
|
||||||
|
// Verify this is a request (has p tag pointing to us)
|
||||||
|
const pTag = event.getMatchingTags('p').find(tag => tag[1] === config.pubkey);
|
||||||
|
if (!pTag) {
|
||||||
|
console.log('Not a request for this server, ignoring');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Extract the encrypted HTTP request from content
|
||||||
|
const encryptedContent = event.content;
|
||||||
|
|
||||||
|
// Use NDK's built-in NIP-44 decryption with our private key
|
||||||
|
let httpRequest: string;
|
||||||
|
try {
|
||||||
|
// NDK provides NIP-44 decryption through the event object or signer
|
||||||
|
httpRequest = await ndk.signer?.decrypt(encryptedContent) || '';
|
||||||
|
console.log('Successfully decrypted content with NIP-44');
|
||||||
|
} catch (decryptError) {
|
||||||
|
console.error('Failed to decrypt content with NIP-44:', decryptError);
|
||||||
|
// Fallback in case of decryption failure
|
||||||
|
httpRequest = encryptedContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Decrypted HTTP request:', httpRequest);
|
||||||
|
|
||||||
|
// Parse HTTP request
|
||||||
|
const parsedRequest = parseHttpRequest(httpRequest);
|
||||||
|
if (!parsedRequest) {
|
||||||
|
throw new Error('Failed to parse HTTP request');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute the HTTP request
|
||||||
|
const response = await executeHttpRequest(parsedRequest);
|
||||||
|
|
||||||
|
// Send response event
|
||||||
|
await sendResponseEvent(event.id, response);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error processing event:', error);
|
||||||
|
|
||||||
|
// Send error response
|
||||||
|
const errorResponse = `HTTP/1.1 500 Internal Server Error
|
||||||
|
Content-Type: text/plain
|
||||||
|
Content-Length: ${error.toString().length}
|
||||||
|
|
||||||
|
${error.toString()}`;
|
||||||
|
|
||||||
|
await sendResponseEvent(event.id, errorResponse);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse an HTTP request string into components
|
||||||
|
function parseHttpRequest(requestStr: string): any {
|
||||||
|
try {
|
||||||
|
const lines = requestStr.split('\n');
|
||||||
|
const firstLine = lines[0].trim().split(' ');
|
||||||
|
|
||||||
|
if (firstLine.length < 3) {
|
||||||
|
throw new Error('Invalid request line');
|
||||||
|
}
|
||||||
|
|
||||||
|
const method = firstLine[0];
|
||||||
|
const path = firstLine[1];
|
||||||
|
const protocol = firstLine[2];
|
||||||
|
|
||||||
|
// Parse headers
|
||||||
|
const headers: Record<string, string> = {};
|
||||||
|
let i = 1;
|
||||||
|
while (i < lines.length && lines[i].trim() !== '') {
|
||||||
|
const headerLine = lines[i].trim();
|
||||||
|
const separatorIndex = headerLine.indexOf(':');
|
||||||
|
|
||||||
|
if (separatorIndex > 0) {
|
||||||
|
const key = headerLine.substring(0, separatorIndex).trim();
|
||||||
|
const value = headerLine.substring(separatorIndex + 1).trim();
|
||||||
|
headers[key] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse body (if any)
|
||||||
|
let body = '';
|
||||||
|
if (i < lines.length - 1) {
|
||||||
|
body = lines.slice(i + 1).join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
return { method, path, protocol, headers, body };
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error parsing HTTP request:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute an HTTP request
|
||||||
|
async function executeHttpRequest(request: any): Promise<string> {
|
||||||
|
try {
|
||||||
|
// Build URL from path
|
||||||
|
const url = new URL(request.path, 'http://localhost');
|
||||||
|
|
||||||
|
// Prepare fetch options
|
||||||
|
const options: any = {
|
||||||
|
method: request.method,
|
||||||
|
headers: request.headers
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add body if present
|
||||||
|
if (request.body && request.method !== 'GET' && request.method !== 'HEAD') {
|
||||||
|
options.body = request.body;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Executing HTTP request to ${url}`);
|
||||||
|
|
||||||
|
// Execute the request
|
||||||
|
const response = await fetch(url.toString(), options);
|
||||||
|
|
||||||
|
// Build response string
|
||||||
|
let responseStr = `HTTP/1.1 ${response.status} ${response.statusText}\n`;
|
||||||
|
|
||||||
|
// Add headers
|
||||||
|
response.headers.forEach((value, key) => {
|
||||||
|
responseStr += `${key}: ${value}\n`;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add empty line between headers and body
|
||||||
|
responseStr += '\n';
|
||||||
|
|
||||||
|
// Add body
|
||||||
|
const body = await response.text();
|
||||||
|
responseStr += body;
|
||||||
|
|
||||||
|
return responseStr;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error executing HTTP request:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send response event using NDK
|
||||||
|
async function sendResponseEvent(requestId: string, responseContent: string) {
|
||||||
|
try {
|
||||||
|
// For a valid Nostr event, we need to manually set all required fields
|
||||||
|
const rawEvent = {
|
||||||
|
kind: 21120,
|
||||||
|
content: responseContent,
|
||||||
|
created_at: Math.floor(Date.now() / 1000),
|
||||||
|
pubkey: config.pubkey,
|
||||||
|
tags: [
|
||||||
|
['e', requestId], // E tag points to request event
|
||||||
|
['expiration', (Math.floor(Date.now() / 1000) + 3600).toString()] // 1 hour from now
|
||||||
|
],
|
||||||
|
id: '', // Will be generated during signing
|
||||||
|
sig: '' // Will be generated during signing
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create a new NDK event from the raw event
|
||||||
|
const event = new NDKEvent(ndk, rawEvent);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Sign the event with our private key
|
||||||
|
await event.sign();
|
||||||
|
console.log('Response event signed successfully');
|
||||||
|
|
||||||
|
// Publish to relays
|
||||||
|
await event.publish();
|
||||||
|
console.log(`Response sent for request ${requestId}`);
|
||||||
|
} catch (signError) {
|
||||||
|
console.error('Error signing/publishing event:', signError);
|
||||||
|
console.log('Event data:', rawEvent);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error sending response event:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The server is already started when NDK connects to relays earlier
|
||||||
|
console.log('HTTP to Nostr server is ready and listening for events');
|
15
server/tsconfig.json
Normal file
15
server/tsconfig.json
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"module": "ES2020",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"outDir": "./dist",
|
||||||
|
"strict": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"]
|
||||||
|
}
|
134
src/converter.ts
Normal file
134
src/converter.ts
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
// Type definition for the window.nostrTools object
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
nostrTools: any;
|
||||||
|
nostr: any;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses an HTTP request string and converts it directly to a kind 21120 Nostr event
|
||||||
|
* Following the specification in README.md
|
||||||
|
*
|
||||||
|
* @param httpRequest The raw HTTP request string
|
||||||
|
* @param pubkey The user's public key
|
||||||
|
* @param serverPubkey The public key of the remote HTTP server
|
||||||
|
* @param decryptkey The encryption key (required per specification)
|
||||||
|
* @param relay Optional relay for the response
|
||||||
|
* @returns Stringified Nostr event or null if conversion fails
|
||||||
|
*/
|
||||||
|
export function convertToEvent(
|
||||||
|
httpRequest: string,
|
||||||
|
pubkey: string,
|
||||||
|
serverPubkey: string,
|
||||||
|
decryptkey: string,
|
||||||
|
relay?: string
|
||||||
|
): string | null {
|
||||||
|
if (!httpRequest) {
|
||||||
|
alert('Please enter an HTTP request message.');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a single kind 21120 event with the HTTP request as content
|
||||||
|
// Following the specification in README.md
|
||||||
|
const event = {
|
||||||
|
kind: 21120,
|
||||||
|
pubkey: pubkey,
|
||||||
|
content: httpRequest, // Content directly contains the HTTP request
|
||||||
|
tags: [
|
||||||
|
// Required tags per README specification
|
||||||
|
["p", serverPubkey], // P tag to indicate this is a REQUEST
|
||||||
|
["key", `nip44Encrypt(${decryptkey})`],
|
||||||
|
["expiration", Math.floor(Date.now() / 1000) + 3600] // 1 hour from now
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add optional relay tag if provided
|
||||||
|
if (relay) {
|
||||||
|
event.tags.push(["r", relay]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return JSON.stringify(event, null, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get nostrTools from window object
|
||||||
|
const nostrTools = window.nostrTools || {};
|
||||||
|
|
||||||
|
export async function displayConvertedEvent() {
|
||||||
|
const httpRequestBox = document.getElementById('httpRequest') as HTMLTextAreaElement;
|
||||||
|
const eventOutputPre = document.getElementById('eventOutput') as HTMLElement;
|
||||||
|
const outputDiv = document.getElementById('output') as HTMLElement;
|
||||||
|
const serverPubkeyInput = document.getElementById('serverPubkey') as HTMLInputElement;
|
||||||
|
const relayInput = document.getElementById('relay') as HTMLInputElement;
|
||||||
|
|
||||||
|
if (httpRequestBox && eventOutputPre && outputDiv) {
|
||||||
|
// Get server pubkey and relay values from inputs
|
||||||
|
const serverPubkey = serverPubkeyInput && serverPubkeyInput.value ?
|
||||||
|
serverPubkeyInput.value : "<server-pubkey>";
|
||||||
|
const relay = relayInput && relayInput.value ?
|
||||||
|
relayInput.value : undefined;
|
||||||
|
|
||||||
|
// Convert directly to a single kind 21120 event
|
||||||
|
const convertedEvent = convertToEvent(
|
||||||
|
httpRequestBox.value,
|
||||||
|
"<pubkey>",
|
||||||
|
serverPubkey,
|
||||||
|
"$decryptkey",
|
||||||
|
relay
|
||||||
|
);
|
||||||
|
|
||||||
|
if (convertedEvent) {
|
||||||
|
// Parse the event to create a proper Nostr event object for signing
|
||||||
|
const parsedEvent = JSON.parse(convertedEvent);
|
||||||
|
const nostrEvent = {
|
||||||
|
kind: 21120,
|
||||||
|
tags: parsedEvent.tags,
|
||||||
|
content: parsedEvent.content,
|
||||||
|
created_at: Math.floor(Date.now() / 1000),
|
||||||
|
pubkey: parsedEvent.pubkey
|
||||||
|
};
|
||||||
|
|
||||||
|
if (window.nostr) {
|
||||||
|
try {
|
||||||
|
const signedEvent = await window.nostr.signEvent(nostrEvent);
|
||||||
|
eventOutputPre.textContent = JSON.stringify(signedEvent, null, 2);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error signing event:", error);
|
||||||
|
eventOutputPre.textContent = "Error signing event: " + error;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// If no NIP-07 extension, just show the unsigned event
|
||||||
|
eventOutputPre.textContent = JSON.stringify(nostrEvent, null, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
outputDiv.hidden = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize event listeners when the DOM is fully loaded
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
const convertButton = document.getElementById('convertButton');
|
||||||
|
if (convertButton) {
|
||||||
|
convertButton.addEventListener('click', displayConvertedEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Example usage for console logging
|
||||||
|
const exampleHttpRequest = 'GET /index.html HTTP/1.1\nHost: example.com\nUser-Agent: Browser/1.0\n\n';
|
||||||
|
const examplePubkey = "<pubkey>";
|
||||||
|
const exampleServerPubkey = "<server-pubkey>"; // This should be the remote server's pubkey
|
||||||
|
const exampleDecryptkey = "$decryptkey";
|
||||||
|
const exampleRelay = "wss://relay.example.com";
|
||||||
|
|
||||||
|
const convertedEvent = convertToEvent(
|
||||||
|
exampleHttpRequest,
|
||||||
|
examplePubkey,
|
||||||
|
exampleServerPubkey,
|
||||||
|
exampleDecryptkey,
|
||||||
|
exampleRelay
|
||||||
|
);
|
||||||
|
|
||||||
|
if (convertedEvent) {
|
||||||
|
console.log("Converted Event:", convertedEvent);
|
||||||
|
}
|
||||||
|
});
|
14
tsconfig.json
Normal file
14
tsconfig.json
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES6",
|
||||||
|
"module": "ES6",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"outDir": "./dist/src",
|
||||||
|
"sourceMap": true,
|
||||||
|
"lib": ["DOM", "ES6", "DOM.Iterable", "ScriptHost"]
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"]
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user