-
Notifications
You must be signed in to change notification settings - Fork 157
Expand file tree
/
Copy pathend-user-session-auth.ts
More file actions
122 lines (105 loc) · 3.52 KB
/
Copy pathend-user-session-auth.ts
File metadata and controls
122 lines (105 loc) · 3.52 KB
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
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
import { HTTPException } from "hono/http-exception";
import { db } from "@llmgateway/db";
import type { Context, Next } from "hono";
/**
* LLM SDK: authentication for browser requests bearing an ephemeral
* end-user session token (`es_…`). Validates the token + expiry, loads the bound
* wallet, and stashes the resolved session on the context. Shared by the wallet
* endpoints and the session-refresh endpoint.
*/
export interface AuthenticatedSession {
sessionId: string;
walletId: string;
endCustomerId: string;
projectId: string;
organizationId: string;
/** `test` wallets top up against the Stripe sandbox. */
mode: "live" | "test";
markupPercent: number;
/** Top-up bonus multiplier (percent) funded from the developer org's credits. */
bonusPercent: number;
/** Origins allowed to call with this session (CORS), from the project. */
allowedOrigins: string[] | null;
}
declare module "hono" {
interface ContextVariableMap {
endUserSession?: AuthenticatedSession;
}
}
export async function endUserSessionAuth(c: Context, next: Next) {
const authHeader = c.req.header("Authorization");
const token = authHeader?.startsWith("Bearer ")
? authHeader.slice("Bearer ".length).trim()
: c.req.header("x-api-key")?.trim();
if (!token) {
throw new HTTPException(401, {
message:
"Missing session token. Pass it as 'Authorization: Bearer es_…'.",
});
}
const session = await db.query.endUserSession.findFirst({
where: {
token: { eq: token },
status: { eq: "active" },
},
with: { wallet: { with: { endCustomer: true, project: true } } },
});
if (!session || !session.wallet) {
throw new HTTPException(401, { message: "Invalid session token" });
}
if (session.expiresAt.getTime() < Date.now()) {
throw new HTTPException(401, {
message: "Session expired. Mint a fresh session token from your backend.",
});
}
if (session.wallet.status !== "active") {
throw new HTTPException(402, { message: "Wallet is frozen" });
}
// Reject sessions whose end customer was blocked/deleted or whose project was
// deactivated/deleted after the token was minted.
if (
session.wallet.endCustomer &&
session.wallet.endCustomer.status !== "active"
) {
throw new HTTPException(401, { message: "End customer is inactive" });
}
const projectStatus = session.wallet.project?.status;
if (projectStatus && projectStatus !== "active") {
throw new HTTPException(401, { message: "Project is inactive" });
}
// Defense-in-depth origin allowlist (see gateway chat handler).
const allowedOrigins = session.wallet.project?.allowedOrigins ?? null;
const origin = c.req.header("Origin");
if (
origin &&
allowedOrigins &&
allowedOrigins.length > 0 &&
!allowedOrigins.includes(origin)
) {
throw new HTTPException(403, {
message: "Origin not allowed for this project",
});
}
const markupPercent = Number(
session.wallet.markupPercentOverride ??
session.wallet.project?.endUserMarkupPercent ??
"0",
);
const bonusPercent = Number(
session.wallet.bonusPercentOverride ??
session.wallet.project?.endUserTopUpBonusPercent ??
"0",
);
c.set("endUserSession", {
sessionId: session.id,
walletId: session.wallet.id,
endCustomerId: session.wallet.endCustomerId,
projectId: session.wallet.projectId,
organizationId: session.wallet.organizationId,
mode: session.wallet.mode,
markupPercent: Number.isFinite(markupPercent) ? markupPercent : 0,
bonusPercent: Number.isFinite(bonusPercent) ? bonusPercent : 0,
allowedOrigins: session.wallet.project?.allowedOrigins ?? null,
});
await next();
}