๐Ÿ“ฆ EqualifyEverything / equalify-iris

๐Ÿ“„ index.ts ยท 151 lines
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
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151import express from "express";
import { mkdirSync } from "node:fs";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
import {
  anonymousToken,
  anonymousTokenWarning,
  applyTrustProxy,
  bedrockApiWarning,
  bundledAppWarning,
  clientIdWarning,
  loadConfig,
  perAgentKeyWarning,
  promptCacheTtlWarning,
} from "./config.ts";
import { Store } from "./store/db.ts";
import { makeAuthMiddleware } from "./auth/middleware.ts";
import { authRouter } from "./routes/auth.ts";
import { meRouter } from "./routes/me.ts";
import { sessionsRouter } from "./routes/sessions.ts";
import { statsRouter } from "./routes/stats.ts";
import { limitsRouter } from "./routes/limits.ts";
import { qualityRouter } from "./routes/quality.ts";
import { visionModelWarning } from "./providers/imageLimits.ts";
import { authRateLimit, generalRateLimit } from "./util/requestLimits.ts";

const cfg = loadConfig();

// The credential check config can do. An OAuth App id here would authenticate users
// and then fail every issue filing, with nothing at boot to say so (see
// clientIdWarning; the unambiguous `Ovโ€ฆ` case is a startup error in validateConfig).
const cidWarning = clientIdWarning(cfg.github.client_id);
if (cidWarning) console.warn(`WARNING: ${cidWarning}`);

// The other one: the bundled app is installed on one repo, so pointing upstream_repo
// elsewhere without registering your own app files nothing for anyone.
const appWarning = bundledAppWarning(cfg.github.client_id, cfg.github.upstream_repo);
if (appWarning) console.warn(`WARNING: ${appWarning}`);

// And the one that is not a mistake: anonymous access is ON, which is a deployment-wide
// policy whose every consequence is invisible from outside (see anonymousTokenWarning).
const anonWarning = anonymousTokenWarning(anonymousToken(cfg));
if (anonWarning) console.warn(`WARNING: ${anonWarning}`);

// A cache TTL nobody can spell is worth saying here, because boot is the only place it
// is observable at all โ€” the two TTLs differ in price, not in reported tokens.
const ttlWarning = promptCacheTtlWarning(cfg.providers);
if (ttlWarning) console.warn(`WARNING: ${ttlWarning}`);

// And a Bedrock `api` nobody can spell, for the same reason: the fallback works, so the
// only symptom is that the deployment is on the path it was trying to leave.
const apiWarning = bedrockApiWarning(cfg.providers);
if (apiWarning) console.warn(`WARNING: ${apiWarning}`);

// And an override that names no agent, which is the same failure on the one key that
// decides which model runs: the entry is ignored and the call takes the provider's own
// model. What ran is answerable afterwards โ€” `by_agent.<agent>.models` in diagnostics names
// the model each agent actually used โ€” but the key that was ignored is nameable only from
// here, because nothing downstream of resolution ever sees it.
const agentKeyWarning = perAgentKeyWarning(cfg.providers.per_agent, cfg.storage.agents_dir);
if (agentKeyWarning) console.warn(`WARNING: ${agentKeyWarning}`);

// What that switch made reachable: a vision model this build has no image limits for.
// Everything still runs, on the conservative defaults โ€” but the limits it publishes are
// then a guess, and nothing downstream of here can say so (providers/imageLimits.ts).
const visionWarning = visionModelWarning(cfg);
if (visionWarning) console.warn(`WARNING: ${visionWarning}`);

// Ensure the on-disk layout exists.
mkdirSync(join(cfg.storage.data_dir, "sessions"), { recursive: true });
mkdirSync(join(cfg.storage.data_dir, "tmp"), { recursive: true });

const store = new Store(cfg.storage.database);
// Clear sessions orphaned by a previous shutdown (their in-process run is gone).
const stale = store.failStaleSessions();
if (stale > 0) console.log(`Marked ${stale} interrupted session(s) as failed on startup.`);
const app = express();
// Whose address `req.ip` is. Off unless a deployment says how many proxies are in front
// of it, because the rate limits below are only per-caller if this is right: unset behind
// Caddy every caller looks like the proxy, and set too permissively every caller can
// claim to be someone new (see normalizeTrustProxy).
//
// The third startup warning comes from here, for either way this key can be wrong: `true`
// is accepted by Express and defeats every per-address limit, because the address then
// comes from a header the client can write (coerced to one hop), and a value Express cannot
// compile would otherwise be a crash naming no config key (trusted as nothing instead).
const proxyWarning = applyTrustProxy(app, cfg.server.trust_proxy);
if (proxyWarning) console.warn(`WARNING: ${proxyWarning}`);
app.use(express.json({ limit: "2mb" }));

// Liveness probe (unauthenticated) โ€” confirms the service is up.
//
// Registered ABOVE the rate limiter on purpose, and it is the only /v1 route that is: a
// probe that answers 429 reports the deployment as down, which is the opposite of what it
// is for. It also polls from one address (a container healthcheck runs on the same host),
// so it is precisely the caller a per-address budget would spend itself on.
app.get("/v1/health", (_req, res) => res.json({ status: "ok", service: "equalify-iris" }));

// How much anyone may ask of this deployment (util/requestLimits.ts). Mounted here โ€”
// above every route below, below the probe above โ€” so a flood is refused before it
// reaches a handler, the store, or multer. The run queue bounds pipeline compute, which
// is a later and narrower question: nothing in it stops a polling loop from occupying
// the event loop with synchronous SQLite reads.
app.use("/v1", generalRateLimit(cfg));

// The public tally of pages converted (unauthenticated, aggregate-only). The
// browser app reads it to report how many pages Iris has made accessible, so it
// has to answer before anyone signs in โ€” and it is mounted here, above the auth
// middleware, for exactly that reason.
app.use("/v1/stats", statsRouter(store));

// What this deployment accepts for an upload (unauthenticated, no user data). Above
// the auth middleware for the same reason as the tally, plus one of its own: the
// browser app states the file limits on the upload step, where the visitor has not
// signed in yet โ€” and someone deciding whether a scan is small enough should not have
// to authenticate to find out.
app.use("/v1/limits", limitsRouter(cfg));

// The deployment-wide quality tally, read by the weekly
// quality-report workflow. Mounted above the GitHub auth middleware because it
// carries its own guard โ€” a shared secret, since the data belongs to no user and the
// caller is a CI job with no GitHub identity. Answers 404 until
// `server.quality_token` is set.
app.use("/v1/quality", qualityRouter(store, cfg.server));

// The browser app is the front door, served at the root (unauthenticated; it
// drives the /v1 API itself). no-store so a deploy never serves a stale page.
const demoFile = fileURLToPath(new URL("../public/demo.html", import.meta.url));
app.get("/", (_req, res) => {
  res.set("Cache-Control", "no-store");
  res.sendFile(demoFile);
});
// Keep the old /demo path working for any shared links.
app.get("/demo", (_req, res) => res.redirect(302, "/"));

// Auth endpoints are unauthenticated by definition, which is also why they get a
// tighter budget than the rest: there is no credential to count against yet, and every
// device-flow poll spends an outbound call to GitHub. Counted in ADDITION to the general
// limiter above โ€” the stricter of the two is simply the one that bites first.
app.use("/v1/auth", authRateLimit(cfg), authRouter(cfg));

// Everything else requires a GitHub bearer token.
const auth = makeAuthMiddleware(store, cfg);
app.use("/v1/me", auth, meRouter(cfg));
app.use("/v1/sessions", auth, sessionsRouter(cfg, store));

const port = cfg.server.port;
app.listen(port, () => {
  console.log(`Equalify Iris listening on http://localhost:${port} (base_url: ${cfg.server.base_url})`);
});