mirror of
https://github.com/hanzoai/chat.git
synced 2026-09-30 20:51:17 +00:00
Re-adds the deliberately-deleted (ee1d712254) guest stack so an anonymous
visitor lands on the chat composer and can chat ONE free model without login,
then hits the SSO wall after a small per-IP quota. Adapted to today's code
(@hanzochat/* package names, @hanzo/iam bridge, tenant-bearer billing) — NOT a
blind revert: hanzoCloudKey.ts stays deleted.
Server (fail-closed, server-enforced):
- services/guestConfig.js: env-gated config + ephemeral guest principal/user +
guest-scoped endpoints/models builders (default model zen5-flash).
- controllers/auth/GuestController.js + POST /v1/chat/auth/guest: short-lived
guest JWT ({guest:true}, per-token random id) signed with JWT_SECRET; the
route is per-IP mint-limited (guestTokenLimiter).
- middleware/requireGuestOrJwtAuth.js: accepts guest tokens ONLY where mounted;
the jwt strategy still rejects guests everywhere else (clean 401, no CastError).
- middleware/enforceGuestScope.js: pins endpoint+model, strips
agents/tools/files/spec/preset; 403 on any other endpoint/model.
- limiters/{guestLimiters,guestMessageLimiter}.js + utils/guestClientIp.js:
per-REAL-IP quota (CF-Connecting-IP, not the token) via in-memory limiterCache
(correct at replicas:1/Recreate; Redis NOT required). Exhaust -> 402 GUEST_LIMIT.
- endpoints/custom/initialize.ts: guest principal -> shared capped GUEST_API_KEY
(KMS chat-guest-key), skipping per-user hk-/bearer billing; fail-closed if
unset. Authenticated bearer path unchanged.
- agents/index.js: guest-capable completion chain; guest-safe /chat/active poll;
AND the guest SSE read-back — GET /chat/stream/:streamId registered with
requireGuestOrJwtAuth ABOVE the strict guard (else a guest 401s reading its own
stream -> empty reply). Per-job ownership check (job.metadata.userId===req.user.id)
is unchanged: foreign=403, missing=404, no cross-principal stream leak.
- Wire models/endpoints + guest-safe bootstrap (user/convos/favorites) to
requireGuestOrJwtAuth; emit allowGuestChat/guestMessageMax in /v1/config;
balance-gate bypass for guests (no org, bounded by limiter + capped key).
Tests: restored 6 guest specs (40/40 green) + 3 guest-billing cases in
initialize.spec. Client already guest-ready (zero client changes).
Docs: LLM.md guest section corrected to in-memory-at-replicas:1 + GUEST_API_KEY.
Do NOT deploy: red security review next, then cto native build + CR image pin.
42 lines
1.7 KiB
JavaScript
42 lines
1.7 KiB
JavaScript
const rateLimit = require('express-rate-limit');
|
|
const { limiterCache } = require('@hanzochat/api');
|
|
const { guestClientIp } = require('~/server/utils');
|
|
const { getGuestConfig } = require('~/server/services/guestConfig');
|
|
|
|
const GUEST_LIMIT_WINDOW_MS = 24 * 60 * 60 * 1000;
|
|
|
|
const handler = (req, res) => {
|
|
return res.status(402).json({
|
|
type: 'GUEST_LIMIT',
|
|
message: 'Guest message limit reached. Log in to continue.',
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Per-IP quota limiter for anonymous guest chat completions.
|
|
*
|
|
* The key is the REAL client IP (`guestClientIp` → Cloudflare `CF-Connecting-IP`),
|
|
* NOT the guest token, so clearing cookies, opening incognito, or minting a fresh
|
|
* guest token does not reset the count. It only counts requests made by an
|
|
* authenticated guest principal; logged-in users skip the counter entirely. On
|
|
* exhaustion it returns `402 { type: 'GUEST_LIMIT' }`, the signal the client maps
|
|
* to the login gate.
|
|
*
|
|
* The store comes from `limiterCache`, which returns `undefined` when `USE_REDIS`
|
|
* is off — so `express-rate-limit` uses its in-process MemoryStore. At
|
|
* `replicas: 1` with a `Recreate` rollout there is never a second live pod to
|
|
* round-robin, so the in-memory count is the authoritative per-IP quota; Redis is
|
|
* NOT required (it was killed platform-wide). The only reset is a pod restart — an
|
|
* infrequent operational event, not an attacker-triggerable action.
|
|
*/
|
|
const guestMessageLimiter = rateLimit({
|
|
windowMs: GUEST_LIMIT_WINDOW_MS,
|
|
max: () => getGuestConfig().messageMax,
|
|
handler,
|
|
keyGenerator: guestClientIp,
|
|
skip: (req) => req.user?.guest !== true,
|
|
store: limiterCache('guest_message_limiter'),
|
|
});
|
|
|
|
module.exports = { guestMessageLimiter };
|