Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 | 10x 6x 6x 1x 1x 5x 5x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x | /**
* The partner-facing sub-router (F-21, RRM partner surface).
*
* Mounted at `/api/partner`. Everything here is a partner looking at their own
* data, so the session guard is applied once, at this boundary, rather than
* repeated in each handler — one place to read, one place to get wrong.
*
* The guard is registered against the two data subtrees BY NAME rather than as
* a blanket `use("*", ...)`. `/api/partner/auth/*` (request-otp, verify-otp,
* logout, session) is public by contract and is mounted here too, so a
* wildcard would shadow it and demand a session from the very endpoints that
* exist to create one. Commit 9672d79f fixed exactly that bug on the
* social-studio router; this is that lesson applied ahead of time.
*
* Both the bare path and the `/*` form are registered for each subtree. The
* `/*` form alone does match the bare path on the Hono in this repo today —
* measured, not assumed — so the second registration is redundant right now.
* It stays because the failure mode if that ever changes is not a 404, it is
* `GET /me` served without a session. `index.test.ts` asserts the bare paths
* are guarded, so the redundancy is a second line of defence rather than the
* only one.
*/
import { Hono, type MiddlewareHandler } from "hono";
import type { RrmProspect } from "../../db/schema/rrm";
import { requirePartnerSession } from "../../lib/rrm/partner-session";
import { contextMiddleware } from "../../middleware";
import auth from "./auth.routes";
import me, { type PartnerEnv } from "./me.routes";
import payouts from "./payouts.routes";
import referrals from "./referrals.routes";
import share from "./share.routes";
type PartnerRouterEnv = {
Bindings: CloudflareBindings;
Variables: PartnerEnv["Variables"] & {
/** What `requirePartnerSession` itself publishes. */
prospectId?: string;
prospect?: RrmProspect;
};
};
/**
* The guard, plus one line of adaptation.
*
* `requirePartnerSession` publishes the partner as `prospectId`; the handlers
* in this directory read `partnerId`. The names differ because the
* generic one is ambiguous in a codebase where admin routes also carry a
* prospect in context, and a handler that scopes money by a variable someone
* else might set is a handler one careless `c.set` away from a leak.
*
* Bridged here rather than renamed in either place: this is the file whose job
* is wiring, and doing it here means the handlers keep a name that can only
* have come from an authenticated partner session.
*/
const guard: MiddlewareHandler<PartnerRouterEnv> = (c, next) =>
requirePartnerSession(c, async () => {
// Either name. The middleware publishes `prospectId` today; the build
// contract for it named `partnerId`. Reading both means a rename
// on that side is a no-op here rather than a 404 on every data route.
const prospectId = c.get("partnerId") ?? c.get("prospectId");
// The guard answers 401 rather than calling `next` when it cannot
// resolve a partner, so this is unreachable with an empty id — the
// check is here so that if that ever stops being true, the request
// fails closed instead of querying the ledger with `undefined`.
// Assigning `c.res` rather than returning: this runs as the guard's
// `next`, whose return value Hono discards, and an unfinalized context
// surfaces as a 500 instead of an answer.
if (!prospectId) {
c.res = c.json({ error: "unauthenticated" }, 401);
return;
}
c.set("partnerId", prospectId);
await next();
});
const partner = new Hono<PartnerRouterEnv>();
// Every route below reads `c.get("dal")`, which is set in exactly one place:
// middleware/context.middleware.ts. Each sub-router mounts it itself — see
// routes/pro/index.ts:39 and routes/admin/index.ts:45 — because index.ts does
// not apply it globally. Without this line every partner request, including
// login, throws on `c.get("dal")` being undefined.
partner.use("*", contextMiddleware);
partner.use("/me", guard);
partner.use("/me/*", guard);
partner.use("/share", guard);
partner.use("/share/*", guard);
partner.use("/referrals", guard);
partner.use("/referrals/*", guard);
partner.use("/payouts", guard);
partner.use("/payouts/*", guard);
// `/auth` is mounted here but deliberately NOT in the guard list above: these
// are the endpoints that issue the session, so requiring one would be a lock
// with its key inside. Every directory barrel in this repo mounts its own
// route files (see routes/admin/rrm/index.ts), so mounting `/api/partner` gets
// the whole partner surface and login cannot be left switched off by accident.
partner.route("/auth", auth);
partner.route("/me", me);
partner.route("/referrals", referrals);
partner.route("/share", share);
partner.route("/payouts", payouts);
export default partner;
|