docs / @orangecheck/gate

@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.