Why
First end-to-end wire-path. SWARM_SPEC §4.1 requires every node to expose its self-description at a well-known URL, unauthenticated and idempotent. Discovery starts here — once this lands, an external curl can verify our node is reachable and the multihash invariant holds.
This PR also exists to PROVE that Phase 1a (nodes table) + Phase 1b (identity bootstrap) + Phase 2 (signature service) + Phase 3a (wire types) compose without modification. If they don't, that's the bug we want to find now.
Background — read this BEFORE starting
mcp__vector-memory__prime_context with task_description "implement /.well-known/mycelium-node endpoint for swarm discovery"
- Read
docs/SWARM_SPEC.md §3.3 (NodeAdvertisement fields) and §4.1 (endpoint contract) and §4.5 (common HTTP behavior)
- Look at how the existing dashboard/HTTP server is wired up (likely
src/services/http-*.ts or similar) — match the existing route-registration style
- Phase 1b's
node_identity_get MCP tool already knows how to read the local node's node_id + pubkey — REUSE its internal helper, do not re-read the keyfile twice in the codebase
- Phase 2's
signWireRecord (or whatever the signature.ts entrypoint is named) does the JCS+Ed25519 signing
What this issue delivers
A new module src/swarm/endpoints/node-advertisement.ts plus its registration in the existing HTTP entrypoint, that handles GET /.well-known/mycelium-node:
- Loads
node_id, pubkey from the row in nodes where is_self = true (single row, enforced by the partial unique index from migration 070)
- Reads
endpoint_url from env MYCELIUM_PUBLIC_URL (required; if missing, return 503 with body {"error":"MYCELIUM_PUBLIC_URL not configured"})
- Reads optional
display_name from env MYCELIUM_DISPLAY_NAME (≤ 64 chars per §3.3)
- Constructs a
NodeAdvertisement with spec_version = WIRE_SPEC_VERSION from Phase 3a
- Sets
signed_at = new Date().toISOString() with millisecond precision (UTC, Z suffix per the spec table)
- Signs via Phase 2's signer
- Responds 200 with
Content-Type: application/json; charset=utf-8 and Cache-Control: no-store
Acceptance criteria
- New file
src/swarm/endpoints/node-advertisement.ts
- Route
GET /.well-known/mycelium-node is reachable via the existing HTTP server (no new server, no new port)
- Integration test that:
- Boots the server (or uses the existing test harness)
- Hits the endpoint
- Asserts: status 200, correct content-type, correct cache-control, body parses as JSON, all required NodeAdvertisement fields present,
multihash(pubkey) === node_id (use the same multihash helper Phase 1b introduced)
- Asserts: signature verifies via Phase 2's verifier against the returned
pubkey
- README or CLAUDE.md note about the new env vars
MYCELIUM_PUBLIC_URL and MYCELIUM_DISPLAY_NAME
Hard constraints
- DO NOT add authentication. Per §4.1, this endpoint is unauthenticated.
- DO NOT introduce a new web framework. Use whatever already serves the dashboard.
- DO NOT generate keys here. If
nodes.is_self is missing, return 503 with a clear error — do NOT silently create a key.
- DO NOT change the response shape. The body MUST be exactly a single
NodeAdvertisement object, no envelope.
Out of scope
/swarm/peers, /swarm/lessons, /swarm/hubs → later Phase 3 / Phase 4 issues
- Validator integration → Phase 3b is the validator; this endpoint produces records, doesn't consume them
- TLS termination — assumed to be handled at the reverse proxy / dashboard layer; this endpoint serves plain HTTP and trusts the proxy for
https://
Why
First end-to-end wire-path. SWARM_SPEC §4.1 requires every node to expose its self-description at a well-known URL, unauthenticated and idempotent. Discovery starts here — once this lands, an external curl can verify our node is reachable and the multihash invariant holds.
This PR also exists to PROVE that Phase 1a (nodes table) + Phase 1b (identity bootstrap) + Phase 2 (signature service) + Phase 3a (wire types) compose without modification. If they don't, that's the bug we want to find now.
Background — read this BEFORE starting
mcp__vector-memory__prime_contextwith task_description "implement /.well-known/mycelium-node endpoint for swarm discovery"docs/SWARM_SPEC.md§3.3 (NodeAdvertisement fields) and §4.1 (endpoint contract) and §4.5 (common HTTP behavior)src/services/http-*.tsor similar) — match the existing route-registration stylenode_identity_getMCP tool already knows how to read the local node'snode_id+pubkey— REUSE its internal helper, do not re-read the keyfile twice in the codebasesignWireRecord(or whatever the signature.ts entrypoint is named) does the JCS+Ed25519 signingWhat this issue delivers
A new module
src/swarm/endpoints/node-advertisement.tsplus its registration in the existing HTTP entrypoint, that handlesGET /.well-known/mycelium-node:node_id,pubkeyfrom the row innodeswhereis_self = true(single row, enforced by the partial unique index from migration 070)endpoint_urlfrom envMYCELIUM_PUBLIC_URL(required; if missing, return 503 with body{"error":"MYCELIUM_PUBLIC_URL not configured"})display_namefrom envMYCELIUM_DISPLAY_NAME(≤ 64 chars per §3.3)NodeAdvertisementwithspec_version = WIRE_SPEC_VERSIONfrom Phase 3asigned_at = new Date().toISOString()with millisecond precision (UTC,Zsuffix per the spec table)Content-Type: application/json; charset=utf-8andCache-Control: no-storeAcceptance criteria
src/swarm/endpoints/node-advertisement.tsGET /.well-known/mycelium-nodeis reachable via the existing HTTP server (no new server, no new port)multihash(pubkey) === node_id(use the same multihash helper Phase 1b introduced)pubkeyMYCELIUM_PUBLIC_URLandMYCELIUM_DISPLAY_NAMEHard constraints
nodes.is_selfis missing, return 503 with a clear error — do NOT silently create a key.NodeAdvertisementobject, no envelope.Out of scope
/swarm/peers,/swarm/lessons,/swarm/hubs→ later Phase 3 / Phase 4 issueshttps://