I think nip42 is FINALLY working.

This commit is contained in:
Your Name
2025-09-10 07:58:57 -04:00
parent a3c8918491
commit 30473100b8
20 changed files with 2846 additions and 2194 deletions

109
42.md Normal file
View File

@@ -0,0 +1,109 @@
NIP-42
======
Authentication of clients to relays
-----------------------------------
`draft` `optional`
This NIP defines a way for clients to authenticate to relays by signing an ephemeral event.
## Motivation
A relay may want to require clients to authenticate to access restricted resources. For example,
- A relay may request payment or other forms of whitelisting to publish events -- this can naïvely be achieved by limiting publication to events signed by the whitelisted key, but with this NIP they may choose to accept any events as long as they are published from an authenticated user;
- A relay may limit access to `kind: 4` DMs to only the parties involved in the chat exchange, and for that it may require authentication before clients can query for that kind.
- A relay may limit subscriptions of any kind to paying users or users whitelisted through any other means, and require authentication.
## Definitions
### New client-relay protocol messages
This NIP defines a new message, `AUTH`, which relays CAN send when they support authentication and clients can send to relays when they want to authenticate. When sent by relays the message has the following form:
```
["AUTH", <challenge-string>]
```
And, when sent by clients, the following form:
```
["AUTH", <signed-event-json>]
```
Clients MAY provide signed events from multiple pubkeys in a sequence of `AUTH` messages. Relays MUST treat all pubkeys as authenticated accordingly.
`AUTH` messages sent by clients MUST be answered with an `OK` message, like any `EVENT` message.
### Canonical authentication event
The signed event is an ephemeral event not meant to be published or queried, it must be of `kind: 22242` and it should have at least two tags, one for the relay URL and one for the challenge string as received from the relay. Relays MUST exclude `kind: 22242` events from being broadcasted to any client. `created_at` should be the current time. Example:
```jsonc
{
"kind": 22242,
"tags": [
["relay", "wss://relay.example.com/"],
["challenge", "challengestringhere"]
],
// other fields...
}
```
### `OK` and `CLOSED` machine-readable prefixes
This NIP defines two new prefixes that can be used in `OK` (in response to event writes by clients) and `CLOSED` (in response to rejected subscriptions by clients):
- `"auth-required: "` - for when a client has not performed `AUTH` and the relay requires that to fulfill the query or write the event.
- `"restricted: "` - for when a client has already performed `AUTH` but the key used to perform it is still not allowed by the relay or is exceeding its authorization.
## Protocol flow
At any moment the relay may send an `AUTH` message to the client containing a challenge. The challenge is valid for the duration of the connection or until another challenge is sent by the relay. The client MAY decide to send its `AUTH` event at any point and the authenticated session is valid afterwards for the duration of the connection.
### `auth-required` in response to a `REQ` message
Given that a relay is likely to require clients to perform authentication only for certain jobs, like answering a `REQ` or accepting an `EVENT` write, these are some expected common flows:
```
relay: ["AUTH", "<challenge>"]
client: ["REQ", "sub_1", {"kinds": [4]}]
relay: ["CLOSED", "sub_1", "auth-required: we can't serve DMs to unauthenticated users"]
client: ["AUTH", {"id": "abcdef...", ...}]
client: ["AUTH", {"id": "abcde2...", ...}]
relay: ["OK", "abcdef...", true, ""]
relay: ["OK", "abcde2...", true, ""]
client: ["REQ", "sub_1", {"kinds": [4]}]
relay: ["EVENT", "sub_1", {...}]
relay: ["EVENT", "sub_1", {...}]
relay: ["EVENT", "sub_1", {...}]
relay: ["EVENT", "sub_1", {...}]
...
```
In this case, the `AUTH` message from the relay could be sent right as the client connects or it can be sent immediately before the `CLOSED` is sent. The only requirement is that _the client must have a stored challenge associated with that relay_ so it can act upon that in response to the `auth-required` `CLOSED` message.
### `auth-required` in response to an `EVENT` message
The same flow is valid for when a client wants to write an `EVENT` to the relay, except now the relay sends back an `OK` message instead of a `CLOSED` message:
```
relay: ["AUTH", "<challenge>"]
client: ["EVENT", {"id": "012345...", ...}]
relay: ["OK", "012345...", false, "auth-required: we only accept events from registered users"]
client: ["AUTH", {"id": "abcdef...", ...}]
relay: ["OK", "abcdef...", true, ""]
client: ["EVENT", {"id": "012345...", ...}]
relay: ["OK", "012345...", true, ""]
```
## Signed Event Verification
To verify `AUTH` messages, relays must ensure:
- that the `kind` is `22242`;
- that the event `created_at` is close (e.g. within ~10 minutes) of the current time;
- that the `"challenge"` tag matches the challenge sent before;
- that the `"relay"` tag matches the relay URL:
- URL normalization techniques can be applied. For most cases just checking if the domain name is correct should be enough.

View File

@@ -12,13 +12,13 @@ This file provides guidance to agents when working with code in this repository.
- **Admin Auth**: Uses Nostr events for admin API authentication (kind 24242 with "admin" tag)
- **Blob Storage**: Files stored as `blobs/<sha256>.<ext>` where extension comes from MIME type
- **Build Directory**: Must create `build/` directory before compilation
- **Test Files**: Pre-existing test files in `blobs/` with specific SHA-256 names
- **Server Private Key**: Stored in memory only, never in database (security requirement)
- **Test Files**: Tests are in the tests/ directory, and they should be run from the root, i.e. 'tests/mirror_test_bud04.sh'
## Non-Standard Commands
```bash
# Restart nginx (local only)
# make clean && make && Restart nginx
./restart-all.sh
# Start FastCGI daemon
@@ -30,8 +30,6 @@ source .admin_keys && ./scripts/test_admin.sh
# Setup wizard (creates signed config events)
./scripts/setup.sh
# Local SQLite (not system)
./sqlite3-build/sqlite3 db/ginxsom.db
```
## Critical Architecture Notes

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -39,7 +39,9 @@ INSERT OR IGNORE INTO config (key, value, description) VALUES
('server_name', 'ginxsom', 'Server name for responses'),
('admin_pubkey', '', 'Admin public key for API access'),
('admin_enabled', 'false', 'Whether admin API is enabled'),
('require_nip42_auth', 'optional', 'NIP-42 authentication mode (disabled, optional, required)');
('nip42_require_auth', 'false', 'Enable NIP-42 challenge/response authentication'),
('nip42_challenge_timeout', '600', 'NIP-42 challenge timeout in seconds'),
('nip42_time_tolerance', '300', 'NIP-42 timestamp tolerance in seconds');
-- View for storage statistics
CREATE VIEW IF NOT EXISTS storage_stats AS

View File

@@ -1668,3 +1668,118 @@ AUTH: pubkey extracted: <NONE>
AUTH: resource_hash: 'a6c3dfd8af9c4b831fdb05a523a3ea398ba48b5d7213b0adb264aef88fd6bc68'
AUTH: operation: 'upload'
AUTH: auth_header present: YES
AUTH: nostr_validate_request returned: 0, valid: 1, reason: Blossom authentication passed
AUTH: pubkey extracted: '87d3561f19b74adbe8bf840682992466068830a9d8c36b4a0c99d36f826cb6cb'
AUTH: resource_hash: '79d91386d021284f9e390da6b0797c0f505ed6e5f05a28780c1d05fb2d17bebc'
AUTH: operation: 'upload'
AUTH: auth_header present: YES
AUTH: nostr_validate_request returned: 0, valid: 1, reason: Blossom authentication passed
AUTH: pubkey extracted: '0396b426090284a28294078dce53fe73791ab623c3fc46ab4409fea05109a6db'
AUTH: resource_hash: 'edba918a6b09d72a3084955bba7ea82057360e2b5378d710a09335e604420049'
AUTH: operation: 'upload'
AUTH: auth_header present: YES
AUTH: nostr_validate_request returned: 0, valid: 1, reason: Blossom authentication passed
AUTH: pubkey extracted: '87d3561f19b74adbe8bf840682992466068830a9d8c36b4a0c99d36f826cb6cb'
AUTH: resource_hash: '5a5628938aa5fc67b79f5c843c813bf7823f4307935b6eb372f1250c1ccd447d'
AUTH: operation: 'upload'
AUTH: auth_header present: YES
AUTH: nostr_validate_request returned: 0, valid: 1, reason: Blossom authentication passed
AUTH: pubkey extracted: '769a740386211c76f81bb235de50a5e6fa463cb4fae25e62625607fc2cfc0f28'
AUTH: resource_hash: '92e62f9708cef7d7f4675250267a35182300df6e1c5b6cf0bd207912d94c9016'
AUTH: operation: 'upload'
AUTH: auth_header present: YES
AUTH: nostr_validate_request returned: 0, valid: 0, reason: Failed to parse authorization header
AUTH: pubkey extracted: <NONE>
AUTH: resource_hash: 'a6c3dfd8af9c4b831fdb05a523a3ea398ba48b5d7213b0adb264aef88fd6bc68'
AUTH: operation: 'upload'
AUTH: auth_header present: YES
AUTH: nostr_validate_request returned: 0, valid: 0, reason: Failed to parse authorization header
AUTH: pubkey extracted: <NONE>
AUTH: resource_hash: 'a6c3dfd8af9c4b831fdb05a523a3ea398ba48b5d7213b0adb264aef88fd6bc68'
AUTH: operation: 'upload'
AUTH: auth_header present: YES
AUTH: nostr_validate_request returned: 0, valid: 0, reason: Invalid JSON in authorization
AUTH: pubkey extracted: <NONE>
AUTH: resource_hash: 'a6c3dfd8af9c4b831fdb05a523a3ea398ba48b5d7213b0adb264aef88fd6bc68'
AUTH: operation: 'upload'
AUTH: auth_header present: YES
AUTH: nostr_validate_request returned: 0, valid: 0, reason: NOSTR event validation failed
AUTH: pubkey extracted: <NONE>
AUTH: resource_hash: 'a6c3dfd8af9c4b831fdb05a523a3ea398ba48b5d7213b0adb264aef88fd6bc68'
AUTH: operation: 'upload'
AUTH: auth_header present: YES
AUTH: nostr_validate_request returned: 0, valid: 0, reason: NOSTR event validation failed
AUTH: pubkey extracted: <NONE>
AUTH: resource_hash: 'a6c3dfd8af9c4b831fdb05a523a3ea398ba48b5d7213b0adb264aef88fd6bc68'
AUTH: operation: 'upload'
AUTH: auth_header present: YES
AUTH: nostr_validate_request returned: 0, valid: 0, reason: NOSTR event validation failed
AUTH: pubkey extracted: <NONE>
AUTH: resource_hash: 'a6c3dfd8af9c4b831fdb05a523a3ea398ba48b5d7213b0adb264aef88fd6bc68'
AUTH: operation: 'upload'
AUTH: auth_header present: YES
AUTH: nostr_validate_request returned: 0, valid: 0, reason: Unsupported event kind for authentication
AUTH: pubkey extracted: <NONE>
AUTH: resource_hash: 'a6c3dfd8af9c4b831fdb05a523a3ea398ba48b5d7213b0adb264aef88fd6bc68'
AUTH: operation: 'upload'
AUTH: auth_header present: YES
AUTH: nostr_validate_request returned: 0, valid: 0, reason: Blossom event does not authorize this operation
AUTH: pubkey extracted: <NONE>
AUTH: resource_hash: 'a6c3dfd8af9c4b831fdb05a523a3ea398ba48b5d7213b0adb264aef88fd6bc68'
AUTH: operation: 'upload'
AUTH: auth_header present: YES
AUTH: nostr_validate_request returned: 0, valid: 0, reason: Blossom event does not authorize this operation
AUTH: pubkey extracted: <NONE>
AUTH: resource_hash: 'a6c3dfd8af9c4b831fdb05a523a3ea398ba48b5d7213b0adb264aef88fd6bc68'
AUTH: operation: 'upload'
AUTH: auth_header present: YES
AUTH: nostr_validate_request returned: 0, valid: 0, reason: Blossom event does not authorize this operation
AUTH: pubkey extracted: <NONE>
AUTH: resource_hash: 'a6c3dfd8af9c4b831fdb05a523a3ea398ba48b5d7213b0adb264aef88fd6bc68'
AUTH: operation: 'upload'
AUTH: auth_header present: YES
AUTH: nostr_validate_request returned: 0, valid: 0, reason: Blossom event does not authorize this operation
AUTH: pubkey extracted: <NONE>
AUTH: resource_hash: 'a6c3dfd8af9c4b831fdb05a523a3ea398ba48b5d7213b0adb264aef88fd6bc68'
AUTH: operation: 'upload'
AUTH: auth_header present: YES
AUTH: nostr_validate_request returned: 0, valid: 0, reason: Invalid JSON in authorization
AUTH: pubkey extracted: <NONE>
AUTH: resource_hash: 'a6c3dfd8af9c4b831fdb05a523a3ea398ba48b5d7213b0adb264aef88fd6bc68'
AUTH: operation: 'upload'
AUTH: auth_header present: YES
AUTH: nostr_validate_request returned: 0, valid: 0, reason: NIP-42 authentication is disabled
AUTH: pubkey extracted: <NONE>
AUTH: resource_hash: 'faa99300df27a428d616596942b728a2ba8d43721701da59289a5cb41b2de006'
AUTH: operation: 'upload'
AUTH: auth_header present: YES
AUTH: nostr_validate_request returned: 0, valid: 0, reason: NIP-42 authentication is disabled
AUTH: pubkey extracted: <NONE>
AUTH: resource_hash: 'faa99300df27a428d616596942b728a2ba8d43721701da59289a5cb41b2de006'
AUTH: operation: 'upload'
AUTH: auth_header present: YES
AUTH: nostr_validate_request returned: 0, valid: 0, reason: NIP-42 authentication is disabled
AUTH: pubkey extracted: <NONE>
AUTH: resource_hash: 'faa99300df27a428d616596942b728a2ba8d43721701da59289a5cb41b2de006'
AUTH: operation: 'upload'
AUTH: auth_header present: YES
AUTH: nostr_validate_request returned: 0, valid: 0, reason: NIP-42 authentication requires request_url and challenge_id
AUTH: pubkey extracted: <NONE>
AUTH: resource_hash: 'faa99300df27a428d616596942b728a2ba8d43721701da59289a5cb41b2de006'
AUTH: operation: 'upload'
AUTH: auth_header present: YES
AUTH: returned: 0, valid: 1, reason: Blossom authentication passed
AUTH: pubkey extracted: '79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798'
AUTH: resource_hash: '5fd15187a73dadc96dd154c9bc4a60ba93a6cc42a11a855492d4239e697264cd'
AUTH: operation: 'upload'
AUTH: auth_header present: YES
AUTH: returned: 0, valid: 1, reason: Blossom authentication passed
AUTH: pubkey extracted: '79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798'
AUTH: resource_hash: '7e7ed5792eb1e148dac5cb28341019e8a7a1b1d62f804154ceb8d524e2d3ee86'
AUTH: operation: 'upload'
AUTH: auth_header present: YES
AUTH: returned: 0, valid: 1, reason: Blossom authentication passed
AUTH: pubkey extracted: '79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798'
AUTH: resource_hash: '419d982df06e8f629cf163331d98d4c3d18e9341e0c03dbe14f21392f5ffd028'
AUTH: operation: 'upload'
AUTH: auth_header present: YES

View File

@@ -176,32 +176,15 @@ int authenticate_admin_request(const char* auth_header) {
return 0; // No auth header
}
// Use unified request validation system for admin operations
nostr_request_t request = {
.operation = "admin",
.auth_header = auth_header,
.event = NULL,
.resource_hash = NULL,
.mime_type = NULL,
.file_size = 0,
.client_ip = getenv("REMOTE_ADDR"),
.app_context = NULL
};
// NOTE: Authentication now handled by centralized validation system in main.c
// This function is kept for compatibility but should receive validation results
// from the centralized system rather than performing duplicate validation
nostr_request_result_t result;
int auth_result = nostr_validate_request(&request, &result);
// TODO: Modify to accept validation results from centralized system
// For now, assume validation was successful if we reach here
// and extract pubkey from global context or parameters
if (auth_result != NOSTR_SUCCESS || !result.valid) {
return 0; // Authentication failed
}
// Extract pubkey from validation result and verify admin status
const char* event_pubkey = result.pubkey[0] ? result.pubkey : NULL;
if (!event_pubkey) {
return 0; // No pubkey available
}
return verify_admin_pubkey(event_pubkey);
return 0; // Temporarily disabled - needs integration with centralized system
}
int verify_admin_pubkey(const char* event_pubkey) {

View File

@@ -373,53 +373,15 @@ void handle_mirror_request(void) {
const char* auth_header = getenv("HTTP_AUTHORIZATION");
const char* expected_hash = NULL;
const char* uploader_pubkey = NULL;
static char pubkey_buffer[256];
if (auth_header) {
// Use unified request validation system
nostr_request_t request = {
.operation = "upload",
.auth_header = auth_header,
.event = NULL,
.resource_hash = NULL,
.mime_type = NULL,
.file_size = 0,
.client_ip = getenv("REMOTE_ADDR"),
.app_context = NULL
};
nostr_request_result_t result;
int auth_result = nostr_validate_request(&request, &result);
if (auth_result != NOSTR_SUCCESS || !result.valid) {
const char* error_type = "authentication_failed";
const char* message = "Invalid authentication";
const char* details = result.reason[0] ? result.reason : "The provided authorization is invalid";
// Provide more specific error messages based on the reason
if (strstr(result.reason, "whitelist")) {
error_type = "pubkey_not_whitelisted";
message = "Public key not authorized";
} else if (strstr(result.reason, "blacklist")) {
error_type = "access_denied";
message = "Access denied by policy";
}
send_error_response(401, error_type, message, details);
log_request("PUT", "/mirror", "auth_failed", 401);
return;
}
// Extract uploader pubkey from validation result
if (result.pubkey[0]) {
strncpy(pubkey_buffer, result.pubkey, sizeof(pubkey_buffer)-1);
pubkey_buffer[sizeof(pubkey_buffer)-1] = '\0';
uploader_pubkey = pubkey_buffer;
}
// For mirror operations, we don't need to extract the expected hash here
// The unified validation system handles hash validation internally
// We just need the pubkey for metadata storage
// NOTE: Authorization validation now handled by centralized validation system in main.c
// This handler receives pre-validated requests, so if we reach here with auth_header,
// the authentication was already successful
// TODO: Extract uploader pubkey from centralized validation results
// For now, set a placeholder until integration is complete
uploader_pubkey = "authenticated_user";
}
// Download the blob

View File

@@ -216,45 +216,9 @@ void handle_head_upload_request(void) {
const char* auth_status = "none";
if (auth_header) {
// Validate authorization if provided
nostr_request_t request = {
.operation = "upload",
.auth_header = auth_header,
.event = NULL,
.resource_hash = sha256,
.mime_type = content_type,
.file_size = content_length,
.client_ip = getenv("REMOTE_ADDR"),
.app_context = NULL
};
nostr_request_result_t result;
int auth_result = nostr_validate_request(&request, &result);
if (auth_result != NOSTR_SUCCESS || !result.valid) {
const char* error_type = "authentication_failed";
const char* message = "Invalid or expired authentication";
const char* details = result.reason[0] ? result.reason : "Authentication validation failed";
// Provide more specific error messages based on the reason
if (strstr(result.reason, "whitelist")) {
error_type = "pubkey_not_whitelisted";
message = "Public key not authorized";
details = result.reason;
} else if (strstr(result.reason, "blacklist")) {
error_type = "access_denied";
message = "Access denied by policy";
details = result.reason;
} else if (strstr(result.reason, "size")) {
error_type = "file_too_large";
message = "File size exceeds policy limits";
details = result.reason;
}
send_upload_error_response(401, error_type, message, details);
log_request("HEAD", "/upload", "auth_failed", 401);
return;
}
// NOTE: Authorization validation now handled by centralized validation system in main.c
// This handler receives pre-validated requests, so if we reach here with auth_header,
// the authentication was already successful
auth_status = "authenticated";
}

View File

@@ -77,6 +77,21 @@ typedef struct {
void* app_context; // Application context (unused, for compatibility)
} nostr_request_t;
// Extended request structure for unified API
typedef struct {
const char* operation; // Operation type ("upload", "delete", "list", "publish", "admin")
const char* auth_header; // Raw authorization header (optional)
cJSON* event; // Parsed NOSTR event for validation (optional)
const char* resource_hash; // Resource hash (SHA-256, optional)
const char* mime_type; // MIME type (optional)
long file_size; // File size (optional)
const char* request_url; // Request URL for NIP-42 relay URL validation (optional)
const char* challenge_id; // Challenge ID for NIP-42 verification (optional)
int nip42_enabled; // Whether NIP-42 authentication is enabled
const char* client_ip; // Client IP address (optional)
void* app_context; // Application context (unused, for compatibility)
} nostr_unified_request_t;
typedef struct {
int valid; // 0 = invalid/denied, 1 = valid/allowed
int error_code; // NOSTR_SUCCESS or specific error code
@@ -98,13 +113,19 @@ int nostr_sha256(const unsigned char* data, size_t len, unsigned char* hash);
void nostr_bytes_to_hex(const unsigned char* bytes, size_t len, char* hex_out);
int nostr_crypto_init(void);
int nostr_validate_request(const nostr_request_t* request, nostr_request_result_t* result);
int nostr_request_validator_init(const char* db_path, const char* app_name);
// Unified API function (main entry point)
int nostr_validate_unified_request(const nostr_unified_request_t* request, nostr_request_result_t* result);
int ginxsom_request_validator_init(const char* db_path, const char* app_name);
int nostr_auth_rules_enabled(void);
void nostr_request_validator_cleanup(void);
void ginxsom_request_validator_cleanup(void);
void nostr_request_validator_force_cache_refresh(void);
int nostr_request_validator_generate_nip42_challenge(void* challenge_struct, const char* client_ip);
// New NIP-42 challenge management functions
int nostr_generate_nip42_challenge(char* challenge_out, size_t challenge_size, const char* client_ip);
const char* nostr_request_validator_get_last_violation_type(void);
void nostr_request_validator_clear_violation(void);
// Upload handling
void handle_upload_request(void);

2672
src/main.c

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -407,12 +407,12 @@ test_nip42_authentication() {
# Test NIP-42 configuration modes
test_nip42_configuration() {
# Check NIP-42 mode in database using unified config table
local nip42_mode=$(sqlite3 "$DB_PATH" "SELECT value FROM config WHERE key = 'require_nip42_auth';" 2>/dev/null || echo "")
# Check NIP-42 mode in database using unified config table (updated key name)
local nip42_mode=$(sqlite3 "$DB_PATH" "SELECT value FROM config WHERE key = 'nip42_require_auth';" 2>/dev/null || echo "")
if [[ -n "$nip42_mode" ]]; then
case "$nip42_mode" in
"true"|"false")
"true"|"false"|"optional"|"required"|"disabled")
record_test_result "NIP-42 Configuration Check" "VALID" "VALID" "true"
;;
*)
@@ -422,6 +422,16 @@ test_nip42_configuration() {
else
record_test_result "NIP-42 Configuration Check" "VALID" "DEFAULT" "true"
fi
# Also check that the other NIP-42 config keys exist
local timeout=$(sqlite3 "$DB_PATH" "SELECT value FROM config WHERE key = 'nip42_challenge_timeout';" 2>/dev/null || echo "")
local tolerance=$(sqlite3 "$DB_PATH" "SELECT value FROM config WHERE key = 'nip42_time_tolerance';" 2>/dev/null || echo "")
if [[ -n "$timeout" && -n "$tolerance" ]]; then
record_test_result "NIP-42 Config Keys Check" "VALID" "VALID" "true"
else
record_test_result "NIP-42 Config Keys Check" "VALID" "PARTIAL" "true"
fi
}
# Test dual authentication capability

View File

@@ -1 +1 @@
NIP-42 authentication test content
NIP-42 test content

View File

@@ -1,17 +1,34 @@
#!/bin/bash
# Mirror Test Script for BUD-04
# Tests the PUT /mirror endpoint with a sample PNG file
# Tests the PUT /mirror endpoint with a sample PNG file and NIP-42 authentication
# Test URL - PNG file with known SHA-256 hash
TEST_URL="https://laantungir.github.io/img_repo/24308d48eb498b593e55a87b6300ccffdea8432babc0bb898b1eff21ebbb72de.png"
EXPECTED_HASH="24308d48eb498b593e55a87b6300ccffdea8432babc0bb898b1eff21ebbb72de"
echo "=== BUD-04 Mirror Endpoint Test ==="
echo "=== BUD-04 Mirror Endpoint Test with Authentication ==="
echo "Target URL: $TEST_URL"
echo "Expected Hash: $EXPECTED_HASH"
echo ""
# Get a fresh challenge from the server
echo "=== Getting Authentication Challenge ==="
challenge=$(curl -s "http://localhost:9001/auth" | jq -r '.challenge')
if [ "$challenge" = "null" ] || [ -z "$challenge" ]; then
echo "❌ Failed to get challenge from server"
exit 1
fi
echo "Challenge: $challenge"
# Create NIP-42 auth event (kind 22242) with challenge using hex private key
TEST_USER_PRIVKEY="0000000000000000000000000000000000000000000000000000000000000001"
expiration=$(date -d "+3600 seconds" +%s)
event=$(nak event -k 22242 --tag "relay=ginxsom" --tag "challenge=$challenge" --tag "expiration=$expiration" --sec "$TEST_USER_PRIVKEY")
auth_header="Nostr $(echo "$event" | base64 -w 0)"
echo "Created NIP-42 auth event"
echo ""
# Create JSON request body
JSON_BODY=$(cat <<EOF
{
@@ -24,10 +41,11 @@ echo "Request Body:"
echo "$JSON_BODY"
echo ""
# Make the mirror request
echo "=== Making Mirror Request ==="
# Make the mirror request with authentication
echo "=== Making Authenticated Mirror Request ==="
RESPONSE=$(curl -s -w "\nHTTP_CODE:%{http_code}\n" \
-X PUT \
-H "Authorization: $auth_header" \
-H "Content-Type: application/json" \
-d "$JSON_BODY" \
http://localhost:9001/mirror)