cc.me / HTTP

HTTP.

OAuth callback trampolines and sealed webhook inboxes. cc.me redirects OAuth callbacks to local targets and keeps webhook deliveries encrypted until you claim and acknowledge them.

Use cases

OAuth callbacks

Use / with an absolute at target. Every callback query parameter except at is appended to the target.

https://cc.me/?at=http%3A%2F%2Fexample.local%2Fauth%2Fcallback

Short aliases

Create a path-only callback URL when a provider wants a stable redirect URI. Reusing the same target returns the same alias.

POST /c

{
  "at": "http://example.local/auth/callback"
}
{
  "url": "https://cc.me/c/1cuEdvnPA31f"
}

HTTP inbox

/i/<base64url-ed25519-public-key> is a tiny encrypted inbox. POST stores a request encrypted for that key; POST /i/<key-a>.<key-b> fans out one encrypted copy per recipient.

POST /i/xG72MCGkluShRByISwEzZVgjIUti3_7phnEch9PxJRI

Protocol adapters

Adapters live below the inbox URL and answer provider handshakes before storing the full request encrypted: /webmention, /websub, /slack, /pingback, /meta, /cloudevents, and /discord/<app-public-key>.

POST /i/.../webmention
content-type: application/x-www-form-urlencoded
source=https%3A%2F%2Fexample.com%2Fpost&target=https%3A%2F%2Fexample.net%2Fpage
GET /i/.../websub?hub.mode=subscribe&hub.topic=...&hub.challenge=abc
POST /i/.../slack { "type": "url_verification", "challenge": "3eZbr..." }
POST /i/.../pingback <methodCall><methodName>pingback.ping</methodName>...</methodCall>
GET /i/.../meta?v=secret&hub.mode=subscribe&hub.verify_token=secret&hub.challenge=abc
POST /i/.../cloudevents
ce-specversion: 1.0
ce-id: evt_1
ce-source: https://example.com/source
ce-type: com.example.event
content-type: application/json { "ok": true }
POST /i/.../discord/0123456789abcdef...
x-signature-ed25519: ...
x-signature-timestamp: 1781337600
content-type: application/json { "type": 1 }

Meta returns the plain hub.challenge. CloudEvents accepts binary HTTP mode and structured JSON. Discord is verified before capture; type: 1 returns {"type":1}, and other valid interactions are stored with {"type":5}.

Provider recipes

GitHub, GitLab, Stripe, and Shopify can use the plain inbox URL. After decrypting, verify the raw bodyBytes with the provider headers before acting.

Delivery control

Claim reserves ready deliveries until you ack or release them. If a client disappears after claiming, the service eventually makes the delivery ready again.

Owner authentication

GET, claim, ack, and release must prove ownership of the inbox private key. Each request needs an x-cc-me-timestamp header (Unix seconds) and an x-cc-me-signature header containing a base64url Ed25519 signature of:

cc-me-v1
{METHOD}
{PATH}
{TIMESTAMP}
{SHA256(BODY) base64url}

The JS client adds these headers automatically.

Responses

GET /i/xG72MCGkluShRByISwEzZVgjIUti3_7phnEch9PxJRI?l=10&p
x-cc-me-timestamp: 1781337600
x-cc-me-signature: base64url-ed25519-signature
{
  "count": 1,
  "items": [
    {
      "id": "m_01k8v7xq7gx1a8p0g7nq0f",
      "sealed": "base64url-encrypted-delivery"
    }
  ],
  "cursor": "opaque-cursor"
}

Claim returns the same shape without cursor. Decrypted items have this shape:

{
  "id": "m_01k8v7xq7gx1a8p0g7nq0f",
  "received_at_unix_ms": 1781337600000,
  "method": "POST",
  "path": "/i/...",
  "query": "source=github&delivery=42",
  "headers": [
    {
      "name": "content-type",
      "value_b64u": "YXBwbGljYXRpb24vanNvbg"
    }
  ],
  "body_b64u": "e30"
}

Error handling

API errors use the response status plus a compact JSON body. Treat 5xx as retryable with backoff; treat 4xx as a caller, auth, capacity, or provider handshake problem to fix before retrying. Common cases are 401 for bad owner signatures, 409 for full inboxes, and 413 for captures over 64 KiB.

{
  "error": "human-readable reason"
}

Forwarders should acknowledge only after local handling succeeds. If local handling fails after a claim, call release or let claim recovery make the delivery ready again.

Clients

Client libraries build trampoline URLs and read inbox deliveries. The JavaScript, Python, Go, Rust, and Ruby ports share one wire protocol, and each ships a cc-me forwarder that claims deliveries and replays them to a local endpoint. Keys are stored at ~/.cc-me.key by default (--key or CC_ME_KEY to override).

JavaScript

Forward an inbox to a local endpoint with the cc-me command:

npx cc-me http://example.local:8080/webhook
pnpx cc-me http://example.local:8080/webhook
bunx cc-me http://example.local:8080/webhook
deno run -A npm:cc-me http://example.local:8080/webhook

Or inspect an inbox locally before acknowledging deliveries:

npx cc-me inspect
pnpx cc-me inspect
bunx cc-me inspect
deno run -A npm:cc-me inspect

The forwarder persists and reuses ~/.cc-me.key by default. Use --key to choose a different key file:

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

The ESM package works in browsers, Node.js, Bun, and Deno. In browsers, privateKey() creates an in-memory key, or you can pass a stored key string. In server runtimes, privateKey(path) creates and reuses a user-private key file.

import { homedir } from "node:os";
import { CcMeClient, createAlias, privateKey } from "cc-me";

const alias = await createAlias("http://example.local/auth/callback");
console.log(alias.url);

const key = await privateKey(`${homedir()}/.cc-me.key`);
const cc = new CcMeClient({ privateKey: key });

console.log(await cc.inboxUrl());

const { requests } = await cc.claim({
  limit: 10,
  poll: true,
});

const claimed = requests.map((request) => request.id);
const handled = [];
try {
  for (const request of requests) {
    console.log(request.method, request.path, request.text());
    handled.push(request.id);
  }
  await cc.ack(handled);
} catch (error) {
  const pending = claimed.filter((id) => !handled.includes(id));
  if (pending.length) await cc.release(pending).catch(() => {});
  throw error;
}

The inspect subcommand is JavaScript-only.

Python

Install from PyPI (Python 3.10+, PyNaCl), then forward:

pip install cc-me
cc-me http://example.local:8080/webhook
from pathlib import Path
from cc_me import CcMeClient, create_alias, private_key

alias = create_alias("http://example.local/auth/callback")
print(alias.url)

cc = CcMeClient(private_key=private_key(Path.home() / ".cc-me.key"))
batch = cc.claim(limit=10, poll=True)
claimed = [request.id for request in batch.requests]
handled = []
try:
    for request in batch.requests:
        print(request.method, request.path, request.text())
        handled.append(request.id)
    cc.ack(handled)
except Exception:
    pending = [id for id in claimed if id not in handled]
    if pending:
        try:
            cc.release(pending)
        except Exception:
            pass
    raise

Go

Install the forwarder from the cc.me vanity path; the server answers the go-import handshake, so the installed binary is named cc.me:

go install cc.me@latest
cc.me http://example.local:8080/webhook

The library lives at cc.me/ccme:

import (
    "fmt"
    "log"

    ccme "cc.me/ccme"
)

key, err := ccme.PrivateKey("/path/to/.cc-me.key")
if err != nil { log.Fatal(err) }
cc, err := ccme.NewClient("", key)
if err != nil { log.Fatal(err) }

batch, err := cc.Claim(ccme.ClaimOptions{Limit: 10, Poll: true})
if err != nil { log.Fatal(err) }
claimed := make([]string, 0, len(batch.Requests))
var handled []string
handle := func(d *ccme.Delivery) error {
    fmt.Println(d.Method, d.Path, d.Text())
    return nil
}

for _, d := range batch.Requests {
    claimed = append(claimed, d.ID)
    if err := handle(d); err != nil {
        _, _ = cc.Release(claimed[len(handled):])
        log.Fatal(err)
    }
    handled = append(handled, d.ID)
}
if _, err := cc.Ack(handled); err != nil { log.Fatal(err) }

Rust

Install the cc-me binary, or add the cc-me crate with cargo add cc-me:

cargo install cc-me
cc-me http://example.local:8080/webhook
use cc_me::{CcMeClient, ListOptions, private_key};
use std::path::Path;

let key = private_key(Some(Path::new("/path/to/.cc-me.key")))?;
let cc = CcMeClient::new(key, None)?;

let batch = cc.claim(&ListOptions { limit: Some(10), poll: true, ..Default::default() })?;
let claimed: Vec<_> = batch.requests.iter().map(|d| d.id.clone()).collect();
let mut handled = Vec::new();
let handle = |d: &cc_me::Delivery| -> cc_me::Result<()> {
    println!("{} {} {}", d.method, d.path, d.text());
    Ok(())
};

for d in &batch.requests {
    if let Err(err) = handle(d) {
        let _ = cc.release(&claimed[handled.len()..]);
        return Err(err);
    }
    handled.push(d.id.clone());
}
cc.ack(&handled)?;

Ruby

Install the gem (Ruby 3.0+, RbNaCl with libsodium), then forward:

gem install cc-me
cc-me http://example.local:8080/webhook
require "cc_me"

alias_url = CcMe.create_alias("http://example.local/auth/callback")
puts alias_url.url

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

puts cc.inbox_url

batch = cc.claim(limit: 10, poll: true)
claimed = batch.requests.map(&:id)
handled = []
begin
  batch.requests.each do |request|
    puts [request.method, request.path, request.text].join(" ")
    handled << request.id
  end
  cc.ack(handled)
rescue
  pending = claimed - handled
  begin
    cc.release(pending) unless pending.empty?
  rescue
  end
  raise
end