@orangecheck/gate
Drop-in middleware. Wraps @orangecheck/sdk's check() and turns it into a next()-or-403 decision for any HTTP framework.
yarn add @orangecheck/gate
Four entry shapes
Pick the one that fits your stack.
1. Express / Connect / Next Pages API
import { ocGate } from '@orangecheck/gate';
app.post(
'/post',
ocGate({
minSats: 100_000,
minDays: 30,
address: { from: 'header' }, // reads X-OC-Address
}),
postHandler,
);
On block: sends 403 { error, subject, subjectKind, orangecheck? }.
2. Next Pages API wrapper
import { withOcGate } from '@orangecheck/gate';
async function handler(req, res) {
// req.orangecheck is the passing CheckResult
res.json({ sats: req.orangecheck.sats });
}
export default withOcGate(handler, {
minSats: 100_000,
minDays: 30,
address: { from: 'query', name: 'addr' },
});
3. Fetch-style — App Router, Hono, Workers, Bun
import { ocGateFetch } from '@orangecheck/gate';
export async function POST(req: Request) {
const decision = await ocGateFetch(req, {
minSats: 100_000,
address: { from: 'header' },
});
if (!decision.ok) {
return new Response(JSON.stringify({ error: decision.reason }), { status: 403 });
}
// ... proceed
}
4. Raw primitive — anything else
import { assertOc } from '@orangecheck/gate';
const decision = await assertOc(req, {
minSats: 100_000,
address: { from: 'header' },
});
if (!decision.ok) {
return { status: 403, body: { error: decision.reason } };
}
Subject sources
Pick exactly one of address, attestationId, or identity. Each is a SubjectSource — telling the gate where on the request to look.
// Header (default: X-OC-Address)
ocGate({ address: { from: 'header' } });
ocGate({ address: { from: 'header', name: 'x-my-addr' } });
// Cookie (default: oc_addr)
ocGate({ address: { from: 'cookie' } });
// Query string (default: ocAddr)
ocGate({ address: { from: 'query', name: 'addr' } });
// JSON body (dot-path)
ocGate({ address: { from: 'body', path: 'user.btcAddress' } });
// Custom extractor
ocGate({ address: { from: (req) => req.session?.btcAddress } });
Same shape for attestationId and identity — identity values are protocol:identifier strings like github:alice.
Options
interface GateOptions {
// Thresholds
minSats?: number; // default 0
minDays?: number; // default 0
// Subject source — pick one
address?: SubjectSource;
attestationId?: SubjectSource;
identity?: SubjectSource;
// In-process cache — matches /api/check's 60s cache by default
cacheTtlMs?: number; // default 60_000
cacheMax?: number; // default 1_000 entries
// Fail open when relays unreachable
failOpen?: boolean; // default false (closed)
// Override Nostr discovery relays
relays?: string[];
// Hooks
onDecision?: (req, decision) => void; // log every decision
onBlocked?: (req, res, decision) => void; // custom 403 response
}
Trust model
You trust the SubjectSource. If it's { from: 'header' }, you are trusting whatever header the client sends. That's fine for low-stakes gates (public forum, shared rate-limit bucket).
For high-stakes gates (airdrops, payments, unique-user voting), run the signed-challenge auth flow first. That cryptographically proves address control; then stash the proven address in a signed session cookie and have the gate read from the session:
app.post('/claim', ocGate({
minSats: 100_000,
address: { from: (req) => req.session.verifiedAddress },
}), handler);
Debug
Log every decision:
ocGate({
minSats: 100_000,
address: { from: 'header' },
onDecision: (req, decision) => {
logger.info({
path: req.url,
subject: decision.subject,
ok: decision.ok,
reason: decision.reason,
}, 'orangecheck gate');
},
});
License
MIT.