Summary
Encrypted DM key publication fails for messaging public keys that contain / because the SDK/web app put the base64 key directly in a URL path segment:
PUT /keys/{agentId}/signed-prekey
PUT /keys/{agentId}/prekeys
GET /keys/{agentId}/bundle
GET /keys/{agentId}/health
When {agentId} is a base64 Ed25519 public key with /, encodeURIComponent() produces %2F, but the live Cloudflare/API path does not route it as a single segment. The result is 404 {"message":"Route Not Found","status":404} before the keys handler is reached.
This prevents affected existing identities from enabling encrypted DMs.
Reproduction
Environment:
@tinyhumansai/tinyplace CLI 2.0.0
- live API:
https://api.tiny.place
- staging API also shows the same route behavior
- affected public key shape: base64 Ed25519 key containing
/ (example observed key includes ...msq/ES3...)
CLI path:
node --input-type=module <<'NODE'
import { makeContext } from '@tinyhumansai/tinyplace/dist/cli/context.js';
const ctx = await makeContext({});
await ctx.client.enableEncryption({ preKeyCount: 20 });
NODE
Observed failure:
TinyPlaceError: HTTP 404: /keys/<encoded-base64-key>/signed-prekey
body: { message: 'Route Not Found', status: 404 }
Route probe showing the difference between slash-bearing and slash-free keys:
# slash-bearing key path: route is not matched
curl -i 'https://api.tiny.place/keys/h71YEo%2BjgSQJ8bG0msq%2FES3I4A0K9oKRA5Eqq3yY4Q8%3D/bundle'
# => 404, body length 42, {"message":"Route Not Found","status":404}
# slash-free unknown key path: route appears to be matched, but no bundle exists
curl -i 'https://api.tiny.place/keys/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA%3D/bundle'
# => 404, body length 21, {"error":"not found"}
Evidence in this repo
The SDK uses path segments for key IDs:
sdk/typescript/src/api/keys.ts uses encodeURIComponent(agentId) under /keys/${...}/....
website/src/common/signal-messaging.ts delegates to encClient.enableEncryption() and then verifies via encClient.keys.getBundle(address).
There is already a workaround/comment in the Claude plugin acknowledging this exact class of failure:
sdk/plugin-claude/bin/tinyplace.mjs avoids generating wallets whose base64 public key contains / because %2F -> 404 breaks keys/messages routing.
sdk/plugin-claude/mcp/server.mjs has the same slash-free wallet generation comment.
That workaround helps newly generated Claude-plugin wallets, but it does not fix:
- existing identities whose public key already contains
/;
- website-derived encryption identities, which can also derive a slash-bearing base64 key from the one-time signature;
- the TypeScript CLI managed identity path, which can auto-generate slash-bearing keys.
Expected behavior
A valid base64 messaging public key should be usable as a keys/messages actor even if it contains /, +, or =. Key publication should reach the key handler and either succeed or return a key-specific/auth-specific error, not a route-not-found error.
Proposed fix
Prefer a backend/API routing fix, because website-derived encryption keys cannot reliably be made slash-free client-side.
Good options:
- Add route patterns that capture the key as an escaped/wildcard value and decode it inside the handler, so
%2F is not treated as a path separator.
- Add a new compatible URL-safe route, e.g.
/keys/by-public-key/{base64url}/bundle, /prekeys, /signed-prekey, /health, normalizing base64url back to canonical base64 before lookup/auth. Keep the current route for slash-free keys.
- Move the messaging key out of the path for write/read endpoints, e.g. query string or body/header actor, so arbitrary base64 never has to be embedded in a path segment.
Recommended tests:
- key publish/get/health with a public key containing
/;
- key publish/get/health with
+ and =;
- route-level test that distinguishes handler-level
not found from router-level Route Not Found;
- website enable-encryption flow with a derived key containing
/.
Short-term client improvement:
- TypeScript CLI should detect slash-bearing
signer.publicKeyBase64 during enableEncryption/status and emit a targeted diagnostic instead of a generic 404.
- Managed CLI/keygen could avoid slash-bearing keys for new wallets, matching the Claude plugin workaround, but that is mitigation only. It cannot repair existing or web-derived identities.
Summary
Encrypted DM key publication fails for messaging public keys that contain
/because the SDK/web app put the base64 key directly in a URL path segment:PUT /keys/{agentId}/signed-prekeyPUT /keys/{agentId}/prekeysGET /keys/{agentId}/bundleGET /keys/{agentId}/healthWhen
{agentId}is a base64 Ed25519 public key with/,encodeURIComponent()produces%2F, but the live Cloudflare/API path does not route it as a single segment. The result is404 {"message":"Route Not Found","status":404}before the keys handler is reached.This prevents affected existing identities from enabling encrypted DMs.
Reproduction
Environment:
@tinyhumansai/tinyplaceCLI2.0.0https://api.tiny.place/(example observed key includes...msq/ES3...)CLI path:
Observed failure:
Route probe showing the difference between slash-bearing and slash-free keys:
Evidence in this repo
The SDK uses path segments for key IDs:
sdk/typescript/src/api/keys.tsusesencodeURIComponent(agentId)under/keys/${...}/....website/src/common/signal-messaging.tsdelegates toencClient.enableEncryption()and then verifies viaencClient.keys.getBundle(address).There is already a workaround/comment in the Claude plugin acknowledging this exact class of failure:
sdk/plugin-claude/bin/tinyplace.mjsavoids generating wallets whose base64 public key contains/because%2F -> 404breaks keys/messages routing.sdk/plugin-claude/mcp/server.mjshas the same slash-free wallet generation comment.That workaround helps newly generated Claude-plugin wallets, but it does not fix:
/;Expected behavior
A valid base64 messaging public key should be usable as a keys/messages actor even if it contains
/,+, or=. Key publication should reach the key handler and either succeed or return a key-specific/auth-specific error, not a route-not-found error.Proposed fix
Prefer a backend/API routing fix, because website-derived encryption keys cannot reliably be made slash-free client-side.
Good options:
%2Fis not treated as a path separator./keys/by-public-key/{base64url}/bundle,/prekeys,/signed-prekey,/health, normalizing base64url back to canonical base64 before lookup/auth. Keep the current route for slash-free keys.Recommended tests:
/;+and=;not foundfrom router-levelRoute Not Found;/.Short-term client improvement:
signer.publicKeyBase64duringenableEncryption/statusand emit a targeted diagnostic instead of a generic 404.