# Implementation Plan: Blossom Tools for Didactyl

## Objective

Add first-class Blossom tooling so the agent can upload, download, inspect, and delete blobs on Blossom-compatible servers. As a prerequisite, consolidate all HTTP client code into a single public API in `nostr_core_lib` so that both projects share one curl implementation.

---

## Problem: HTTP Client Duplication

There are currently **5 separate curl implementations** across the two projects:

| # | Project | File | Function | Methods | Visibility |
|---|---|---|---|---|---|
| 1 | nostr_core_lib | `cashu_mint.c:88` | `cashu_http_json_request()` | GET, POST | `static` |
| 2 | nostr_core_lib | `nip005.c:103` | `nip05_http_get()` | GET | `static` |
| 3 | nostr_core_lib | `nip011.c:344` | inline curl block | GET | `static` |
| 4 | Didactyl | `llm.c:88` | `perform_http_request()` | GET, POST | `static` |
| 5 | Didactyl | `tool_local.c:214` | inline in `execute_local_http_fetch()` | GET, POST, any | `static` |

Each has its own write callback, response buffer struct, CA bundle detection, SSL config, and error handling. Adding Blossom without consolidation would create a 6th copy.

### Decision

Consolidate all HTTP client code into a **single public API** in `nostr_core_lib`. Migrate all consumers in both projects to use it. Then build Blossom client on top of the same shared HTTP layer.

---

## Phase 1: Unified HTTP Client in nostr_core_lib

### New files

- `nostr_core_lib/nostr_core/nostr_http.h` — public API
- `nostr_core_lib/nostr_core/nostr_http.c` — single curl implementation

### Proposed API

```c
#ifndef NOSTR_HTTP_H
#define NOSTR_HTTP_H

#include <stddef.h>

// HTTP response container
typedef struct {
    char* body;           // Response body (malloc'd, caller frees)
    size_t body_len;      // Body length in bytes
    long status_code;     // HTTP status code
    char* content_type;   // Content-Type header value (malloc'd, caller frees)
    char* headers_raw;    // All response headers (malloc'd, caller frees, optional)
} nostr_http_response_t;

// HTTP request options
typedef struct {
    const char* method;           // "GET", "POST", "PUT", "DELETE", "HEAD" (default: "GET")
    const char* url;              // Required
    const char** headers;         // NULL-terminated array of "Key: Value" strings (optional)
    const unsigned char* body;    // Request body bytes (optional)
    size_t body_len;              // Body length (0 if no body)
    int timeout_seconds;          // Request timeout (default: 30)
    size_t max_response_bytes;    // Cap response body size (0 = unlimited)
    int follow_redirects;         // 1 = follow, 0 = don't (default: 1)
    int max_redirects;            // Max redirect hops (default: 3)
    const char* user_agent;       // User-Agent header (default: "nostr-core/VERSION")
    int capture_headers;          // 1 = capture response headers in headers_raw
} nostr_http_request_t;

// Set global CA bundle path for all HTTP requests
void nostr_http_set_ca_bundle(const char* ca_bundle_path);

// Auto-detect CA bundle from common system paths
const char* nostr_http_detect_ca_bundle(void);

// Perform an HTTP request
// Returns NOSTR_SUCCESS on successful HTTP round-trip (even 4xx/5xx).
// Returns NOSTR_ERROR_NETWORK_FAILED on connection/DNS/timeout failure.
// Caller must call nostr_http_response_free() on success.
int nostr_http_request(const nostr_http_request_t* req, nostr_http_response_t* resp);

// Free response resources
void nostr_http_response_free(nostr_http_response_t* resp);

// Convenience: simple GET returning body string
int nostr_http_get(const char* url, int timeout_seconds, char** body_out, long* status_out);

// Convenience: JSON POST returning body string
int nostr_http_post_json(const char* url, const char* json_body, int timeout_seconds,
                         char** body_out, long* status_out);

#endif
```

### Key design decisions

1. **Binary-safe body** — `body` is `unsigned char*` with explicit `body_len`, supporting both JSON text and raw file uploads
2. **Method-agnostic** — supports GET, POST, PUT, DELETE, HEAD, PATCH via string
3. **Response size cap** — `max_response_bytes` prevents OOM on large downloads
4. **Header capture** — optional `capture_headers` for HEAD requests (Blossom needs this)
5. **CA bundle** — single global setter replaces 5 separate detection functions
6. **Convenience wrappers** — `nostr_http_get()` and `nostr_http_post_json()` cover the common JSON API pattern used by cashu_mint, nip005, nip011

### Implementation notes

- Single `static size_t write_callback()` function
- Single `static size_t header_callback()` function (for header capture)
- CA bundle auto-detection consolidated from the 5 existing implementations
- SSL verification always on by default

---

## Phase 2: Migrate nostr_core_lib Internal Consumers

### cashu_mint.c

Replace `cashu_http_json_request()` (static, ~70 lines) with calls to `nostr_http_post_json()` / `nostr_http_get()`.

**Before:**
```c
static int cashu_http_json_request(const char* method, const char* url,
                                   const char* body, int timeout_seconds,
                                   char** response_out, long* status_out) {
    // 70 lines of curl boilerplate
}
```

**After:**
```c
static int cashu_http_json_request(const char* method, const char* url,
                                   const char* body, int timeout_seconds,
                                   char** response_out, long* status_out) {
    if (strcmp(method, "POST") == 0) {
        return nostr_http_post_json(url, body, timeout_seconds, response_out, status_out);
    }
    return nostr_http_get(url, timeout_seconds, response_out, status_out);
}
```

The function signature stays the same so all 20+ call sites in cashu_mint.c are unaffected.

### nip005.c

Replace `nip05_http_get()` (static, ~50 lines) with `nostr_http_get()`.

### nip011.c

Replace inline curl block (~40 lines) with `nostr_http_request()` using custom Accept header.

### Build changes

- Add `nostr_http.c` to the nostr_core_lib build
- Remove `#include <curl/curl.h>` from cashu_mint.c, nip005.c, nip011.c (only nostr_http.c includes it)
- Update `nostr_core.h` to include `nostr_http.h`

---

## Phase 3: Migrate Didactyl Consumers

### llm.c

Replace `perform_http_request()` (static, ~80 lines) with `nostr_http_request()`.

**Current signature:** `static char* perform_http_request(const char* url, const char* body, int is_post)`

**Migration:** Build a `nostr_http_request_t` with the Authorization Bearer header, call `nostr_http_request()`, extract body. The LLM-specific logic (WebSocket URL detection, debug logging, status code handling) stays in llm.c — only the curl plumbing moves out.

Also remove:
- `static size_t write_cb()` — replaced by nostr_http's callback
- `static const char* detect_ca_bundle_path()` — replaced by `nostr_http_detect_ca_bundle()`
- `typedef struct { char* data; size_t len; } response_buffer_t;` — replaced by `nostr_http_response_t`

### tool_local.c

Replace the inline curl block in `execute_local_http_fetch()` (~100 lines) with `nostr_http_request()`.

Also remove:
- `static size_t local_http_fetch_write_cb_local()` — replaced
- `static const char* detect_ca_bundle_path_for_tools_local()` — replaced
- `typedef struct { ... } local_http_fetch_buffer_t;` — replaced

### cashu_wallet.c

Remove `detect_ca_bundle_path_for_cashu_wallet()` — replaced by `nostr_http_detect_ca_bundle()` called once at init.

The `cashu_mint_set_ca_bundle()` call at line 423 becomes `nostr_http_set_ca_bundle()` called once in main.c startup.

### Build changes

- Remove `#include <curl/curl.h>` from llm.c and tool_local.c
- Didactyl only includes curl transitively through nostr_core_lib
- CA bundle detection happens once in main.c startup via `nostr_http_set_ca_bundle(nostr_http_detect_ca_bundle())`

### Verification

After migration, grep confirms zero direct curl usage in Didactyl:
```bash
grep -r "curl_easy_init\|CURL\s*\*\|curl_easy_setopt" src/ 
# Expected: no results
```

---

## Phase 4: Blossom Client in nostr_core_lib

### New files

- `nostr_core_lib/nostr_core/blossom_client.h` — public Blossom API
- `nostr_core_lib/nostr_core/blossom_client.c` — implementation using `nostr_http`

### Proposed API

```c
#ifndef NOSTR_BLOSSOM_CLIENT_H
#define NOSTR_BLOSSOM_CLIENT_H

#include "nostr_common.h"
#include "../cjson/cJSON.h"
#include <stddef.h>

// Blob descriptor returned by Blossom servers
typedef struct {
    char sha256[65];        // Hex-encoded SHA-256 hash
    char url[512];          // Canonical blob URL
    long size;              // Blob size in bytes
    char content_type[128]; // MIME type
    long created;           // Unix timestamp
} blossom_blob_descriptor_t;

// Set CA bundle for Blossom HTTP requests (delegates to nostr_http)
void blossom_set_ca_bundle(const char* ca_bundle_path);

// Create a kind 24242 Blossom authorization event
// Returns base64-encoded signed event string for Authorization header.
// Caller must free() the returned string.
char* blossom_create_auth_header(const unsigned char* private_key,
                                 const char* operation,       // "upload", "delete", "list"
                                 const char* sha256_hex,      // blob hash (NULL for list)
                                 int expiration_seconds);

// Upload file bytes to a Blossom server
// Returns NOSTR_SUCCESS and fills descriptor on success.
int blossom_upload(const char* server_url,
                   const unsigned char* data,
                   size_t data_len,
                   const char* content_type,
                   const unsigned char* private_key,  // for auth event (NULL = no auth)
                   const char* sha256_hex,            // pre-computed hash (NULL = compute)
                   int timeout_seconds,
                   blossom_blob_descriptor_t* descriptor_out);

// Upload a local file to a Blossom server
int blossom_upload_file(const char* server_url,
                        const char* file_path,
                        const char* content_type,
                        const unsigned char* private_key,
                        int timeout_seconds,
                        blossom_blob_descriptor_t* descriptor_out);

// Download a blob by SHA-256 hash
// Returns NOSTR_SUCCESS and fills body_out/body_len_out.
// Caller must free(*body_out).
int blossom_download(const char* server_url,
                     const char* sha256_hex,
                     int timeout_seconds,
                     size_t max_bytes,
                     unsigned char** body_out,
                     size_t* body_len_out,
                     char* content_type_out,       // buffer, at least 128 bytes
                     size_t content_type_out_size);

// Download a blob to a local file
int blossom_download_to_file(const char* server_url,
                             const char* sha256_hex,
                             const char* output_path,
                             int timeout_seconds,
                             size_t max_bytes,
                             blossom_blob_descriptor_t* descriptor_out);

// HEAD request — check blob existence and metadata
int blossom_head(const char* server_url,
                 const char* sha256_hex,
                 int timeout_seconds,
                 blossom_blob_descriptor_t* descriptor_out);

// Delete a blob by SHA-256 hash (requires auth)
int blossom_delete(const char* server_url,
                   const char* sha256_hex,
                   const unsigned char* private_key,
                   int timeout_seconds);

// List blobs for a pubkey
// Returns NOSTR_SUCCESS and fills descriptors array.
// Caller must free(*descriptors_out).
int blossom_list(const char* server_url,
                 const char* pubkey_hex,
                 int timeout_seconds,
                 blossom_blob_descriptor_t** descriptors_out,
                 int* count_out);

#endif
```

### Implementation details

- `blossom_create_auth_header()` uses `nostr_create_and_sign_event()` (kind 24242) + `base64_encode()`
- All HTTP calls go through `nostr_http_request()` — no direct curl usage
- `blossom_upload_file()` uses `nostr_sha256_file_stream()` to hash before upload
- `blossom_download_to_file()` verifies SHA-256 after download

### Build changes

- Add `blossom_client.c` to nostr_core_lib build
- Update `nostr_core.h` to include `blossom_client.h`
- Rebuild `libnostr_core_*.a`

---

## Phase 5: Blossom Tools in Didactyl

### New file

- `src/tools/tool_blossom.c` — thin tool wrappers calling `blossom_*()` from nostr_core_lib

### Tool set

| Tool | Description | Library Function |
|---|---|---|
| `blossom_upload` | Upload local file to Blossom server | `blossom_upload_file()` |
| `blossom_download` | Download blob to local file | `blossom_download_to_file()` |
| `blossom_head` | Check blob existence and metadata | `blossom_head()` |
| `blossom_delete` | Delete blob from server | `blossom_delete()` |
| `blossom_list` | List blobs by pubkey | `blossom_list()` |

### Tool schemas

(Unchanged from original plan — see Tool Contracts section below.)

### Integration points

1. **`tools_internal.h`** — add `execute_blossom_*()` prototypes
2. **`tools_dispatch.c`** — add `strcmp` branches
3. **`tools_schema.c`** — add OpenAI tool definitions
4. **`Makefile`** — add `$(SRC_DIR)/tools/tool_blossom.c` to SRCS
5. **`docs/TOOLS.md`** — add Blossom Storage Tools section

### Pattern

Follows the exact same pattern as Cashu:

```
tool_blossom.c (arg parsing + JSON result formatting)
  → blossom_client.h (nostr_core_lib - domain API)
    → nostr_http.h (nostr_core_lib - shared HTTP client)
```

Just like:

```
tool_cashu_wallet.c (arg parsing + JSON result formatting)
  → cashu_wallet.c (Didactyl - wallet state + Nostr persistence)
    → cashu_mint.h (nostr_core_lib - domain API)
      → nostr_http.h (nostr_core_lib - shared HTTP client)
```

---

## Tool Contracts

All tools return a JSON object with at minimum:

```json
{
  "success": true,
  "error": "...optional on failure..."
}
```

### `blossom_upload`

**Input:**
```json
{
  "type": "object",
  "properties": {
    "server": { "type": "string", "description": "Blossom server base URL; omit if default configured" },
    "file_path": { "type": "string", "description": "Relative local path inside working directory" },
    "content_type": { "type": "string", "description": "Optional MIME type override" }
  },
  "required": ["file_path"]
}
```

**Output:**
```json
{
  "success": true,
  "server": "https://blossom.example",
  "sha256": "<64-hex>",
  "size": 12345,
  "content_type": "image/png",
  "url": "https://blossom.example/<sha256>"
}
```

### `blossom_download`

**Input:**
```json
{
  "type": "object",
  "properties": {
    "server": { "type": "string" },
    "sha256": { "type": "string", "description": "Blob hash hex identifier" },
    "url": { "type": "string", "description": "Direct blob URL if server+sha256 not provided" },
    "output_path": { "type": "string", "description": "Relative path to write file" },
    "overwrite": { "type": "boolean", "default": false }
  },
  "required": ["output_path"]
}
```

**Output:**
```json
{
  "success": true,
  "output_path": "downloads/file.bin",
  "bytes_written": 12345,
  "sha256": "<computed-64-hex>",
  "verified": true,
  "content_type": "application/octet-stream"
}
```

### `blossom_head`

**Input:**
```json
{
  "type": "object",
  "properties": {
    "server": { "type": "string" },
    "sha256": { "type": "string" },
    "url": { "type": "string" }
  }
}
```

**Output:**
```json
{
  "success": true,
  "exists": true,
  "sha256": "<64-hex>",
  "size": 12345,
  "content_type": "image/jpeg"
}
```

### `blossom_delete`

**Input:**
```json
{
  "type": "object",
  "properties": {
    "server": { "type": "string" },
    "sha256": { "type": "string" }
  },
  "required": ["sha256"]
}
```

**Output:**
```json
{
  "success": true,
  "deleted": true,
  "server": "https://blossom.example",
  "sha256": "<64-hex>"
}
```

### `blossom_list`

**Input:**
```json
{
  "type": "object",
  "properties": {
    "server": { "type": "string" },
    "pubkey": { "type": "string", "description": "Hex pubkey; defaults to agent pubkey" }
  }
}
```

**Output:**
```json
{
  "success": true,
  "server": "https://blossom.example",
  "pubkey": "<64-hex>",
  "blobs": [
    { "sha256": "...", "size": 12345, "content_type": "image/png", "url": "...", "created": 1679000000 }
  ],
  "count": 1
}
```

---

## Validation and Safety Rules

1. `server` must be `https://` unless explicit config allows insecure local testing
2. `sha256` must match `^[0-9a-fA-F]{64}$`
3. `file_path` and `output_path` must be safe relative paths (reuse `tool_local.c` pattern)
4. Enforce maximum upload/download size from config
5. Refuse overwrite unless `overwrite=true`
6. Normalize all errors into `{"success": false, "error": "..."}`
7. Do not leak keys/secrets in returned payload

---

## Execution Order

### Step 1: `nostr_http` in nostr_core_lib
- Create `nostr_http.h` and `nostr_http.c`
- Write unit tests for GET, POST, PUT, DELETE, HEAD
- Verify CA bundle detection works across distros

### Step 2: Migrate nostr_core_lib consumers
- Refactor `cashu_mint.c` to use `nostr_http`
- Refactor `nip005.c` to use `nostr_http`
- Refactor `nip011.c` to use `nostr_http`
- Remove direct `#include <curl/curl.h>` from all three
- Run existing tests to verify no regressions

### Step 3: Migrate Didactyl consumers
- Refactor `llm.c` to use `nostr_http`
- Refactor `tool_local.c` to use `nostr_http`
- Update `cashu_wallet.c` CA bundle init
- Remove all direct curl includes from Didactyl src/
- Verify: `grep -r "curl_easy_init" src/` returns zero results
- Run existing tests

### Step 4: `blossom_client` in nostr_core_lib
- Create `blossom_client.h` and `blossom_client.c`
- Implement auth event builder, upload, download, head, delete, list
- Write unit tests

### Step 5: Blossom tools in Didactyl
- Create `tool_blossom.c`
- Add schemas, dispatch, prototypes
- Update docs
- Run full test suite

---

## Architecture Diagram

```mermaid
graph TD
    subgraph "Didactyl Tools Layer"
        TB[tool_blossom.c]
        TCW[tool_cashu_wallet.c]
        TL[tool_local.c]
        LLM[llm.c]
    end

    subgraph "Didactyl Domain Layer"
        CW[cashu_wallet.c]
    end

    subgraph "nostr_core_lib - Domain Clients"
        BC[blossom_client.c]
        CM[cashu_mint.c]
        N05[nip005.c]
        N11[nip011.c]
    end

    subgraph "nostr_core_lib - Shared Infrastructure"
        NH[nostr_http.c<br/>Single curl implementation]
        NIP1[nip001.c<br/>Event signing]
        UTIL[utils.c<br/>SHA-256 + base64 + hex]
    end

    TB --> BC
    TCW --> CW
    CW --> CM
    TL --> NH
    LLM --> NH

    BC --> NH
    BC --> NIP1
    BC --> UTIL
    CM --> NH
    N05 --> NH
    N11 --> NH
```

---

## Testing Plan

### Unit tests
1. `nostr_http` — GET/POST/PUT/DELETE/HEAD, timeouts, max_response_bytes, CA bundle
2. `blossom_client` — auth event creation, upload/download/head/delete/list
3. Argument parsing and validation for all Blossom tools

### Integration tests
- Mock Blossom server for upload/download/head/delete/list
- Verify SHA-256 integrity on download
- Test auth event expiration
- Test error responses (404, 401, 403, 500)

### Regression tests
- All existing Cashu wallet tests pass after cashu_mint migration
- All existing NIP-05 and NIP-11 tests pass
- LLM requests work correctly after llm.c migration
- `local_http_fetch` tool works correctly after tool_local.c migration

---

## Risks and Mitigations

1. **nostr_core_lib API change breaks Didactyl build**
   - Mitigation: version-pin nostr_core_lib; test both projects together before release

2. **Subtle curl behavior differences after migration**
   - Mitigation: keep convenience wrappers thin; run existing test suites at each step

3. **Binary body support gaps**
   - Mitigation: `nostr_http_request_t.body` is `unsigned char*` with explicit length from day one

4. **CA bundle detection regression on specific distros**
   - Mitigation: consolidate all 5 existing detection paths into one comprehensive function

5. **Blossom server protocol variance**
   - Mitigation: defensive response parsing; test against multiple server implementations

---

## Files Changed Summary

### nostr_core_lib (new)
- `nostr_core/nostr_http.h` — shared HTTP client API
- `nostr_core/nostr_http.c` — shared HTTP client implementation
- `nostr_core/blossom_client.h` — Blossom client API
- `nostr_core/blossom_client.c` — Blossom client implementation

### nostr_core_lib (modified)
- `nostr_core/cashu_mint.c` — replace static curl with `nostr_http`
- `nostr_core/nip005.c` — replace static curl with `nostr_http`
- `nostr_core/nip011.c` — replace static curl with `nostr_http`
- `nostr_core/nostr_core.h` — add includes for new headers
- `build.sh` — add new source files

### Didactyl (new)
- `src/tools/tool_blossom.c` — Blossom tool implementations

### Didactyl (modified)
- `src/llm.c` — replace static curl with `nostr_http`
- `src/tools/tool_local.c` — replace static curl with `nostr_http`
- `src/cashu_wallet.c` — update CA bundle init
- `src/main.c` — add `nostr_http_set_ca_bundle()` call at startup
- `src/tools/tools_internal.h` — add Blossom prototypes
- `src/tools/tools_dispatch.c` — add Blossom dispatch
- `src/tools/tools_schema.c` — add Blossom schemas
- `Makefile` — add `tool_blossom.c` to SRCS
- `docs/TOOLS.md` — add Blossom section
