cc-me

Ruby client for cc.me. The library builds trampoline and inbox URLs and decrypts deliveries; the CLI forwards inbox deliveries to a local endpoint. Mirrors the canonical JavaScript client and follows the wire protocol in ../PROTOCOL.md.

Requires Ruby 3.0+ and RbNaCl (which needs libsodium installed).

gem install cc-me

Forward an inbox to a local endpoint:

cc-me http://example.local:8080/webhook

The CLI prints the inbox URL to register with the provider. It uses ~/.cc-me.key by default, creating it if needed and reusing it later. The key is an Ed25519 seed; the URL shows the derived Ed25519 public key. Use --key to choose a specific path:

cc-me --key ~/hooks.key http://example.local:8080/webhook

You can also set CC_ME_KEY, CC_ME_URL, and CC_ME_LIMIT.

require "cc_me"

alias_url = CcMe.create_alias("http://example.local/auth/callback")
puts "OAuth callback URL: #{alias_url.url}"

key = CcMe.private_key(File.join(Dir.home, ".cc-me.key"))
cc = CcMe::Client.new(private_key: key)

puts "Webhook URL: #{cc.inbox_url}"
puts "Webmention URL: #{cc.webmention_url}"
puts "WebSub URL: #{cc.websub_url}"
puts "Slack URL: #{cc.slack_url}"
puts "Pingback URL: #{cc.pingback_url}"
puts "Meta URL: #{cc.meta_url('shared-verify-token')}"
puts "CloudEvents URL: #{cc.cloudevents_url}"
puts "Discord URL: #{cc.discord_url('discord-app-public-key')}"

result = cc.claim(limit: 10, poll: true)

handled = []
result.requests.each do |request|
  puts [request.method, request.path, request.text].join(" ")
  handled << request.id
end
cc.ack(handled)

create_alias is idempotent: calling it again with the same target returns the same URL.

Protocol URL helpers return provider-ready receiver URLs. Webmention, WebSub, Slack Events API, Pingback, Meta-style webhooks, CloudEvents, and Discord Interactions deliveries arrive in the same inbox and are read with peek or claim.

meta_url(token) adds an optional verify token for Meta-style handshakes. cloudevents_url accepts binary, structured, and batched JSON CloudEvents. discord_url(app_public_key) verifies Discord signatures and answers interaction PINGs before storing non-PING interactions.

limit is optional. Omit it to use the service default:

result = cc.claim(poll: true)

peek returns a cursor for live inspectors and dashboards:

page = cc.peek(poll: true)
nxt = cc.peek(cursor: page.cursor, poll: true)

Call CcMe.private_key with no argument to create an in-memory key, or pass your own stored base64url seed string to CcMe::Client.new(private_key: ...). CcMe.private_key(path) creates and reuses a key file, keeping it private to the user (mode 0600) on Unix-like systems.

Each decrypted request exposes id, received_at_unix_ms, method, path, query, headers (each with name, value, value_bytes), body_bytes, and text / json helpers. The decrypted id is verified against the envelope id.

The inspect subcommand from the JS CLI is intentionally not ported.

Build & test

bundle install
bundle exec rake test