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
78import { Router } from "express";
import type { IrisConfig } from "../config.ts";
import { imageLimitsHint, resolveImageLimits } from "../providers/imageLimits.ts";
import { MAX_PDF_PAGES } from "../util/pdf.ts";
import { MAX_UPLOAD_FILES, publishedRateLimits, uploadCeilingBytes } from "../util/requestLimits.ts";
/**
* `GET /v1/limits` โ what this deployment accepts for an upload.
*
* Exists so the limits are stated in exactly one place and read from there. They are
* facts about the configured vision model (see providers/imageLimits.ts), and this
* project switches models; a hint hardcoded into the demo page or the API docs would
* keep confidently naming last month's numbers. The demo renders `image.hint`
* verbatim for that reason, and `POST /v1/sessions` rejects an upload by the same
* `max_image_bytes` served here โ so "what the page told me" and "what the API did"
* cannot disagree.
*
* ```json
* { "max_pages": 25,
* "image": { "max_bytes": 3932160, "max_long_edge_px": 1568, "max_dimension_px": 8000,
* "media_types": ["image/png", "..."], "extensions": [".png", "..."],
* "hint": "Each image must be under 3.7 MB and in one of PNG, ... format. โฆ" },
* "pdf": { "max_pages": 25 },
* "upload": { "max_files": 25, "max_request_bytes": 134217728 },
* "rate_limits": { "general_per_minute": 240, "upload_per_minute": 12,
* "max_upload_memory_mb": 256, "window_seconds": 60 } }
* ```
*
* Not handed the auth middleware, like `GET /v1/stats`, so it answers even where the
* operator set `server.api_token`: someone deciding whether their scan is small enough
* should not need the deployment's shared secret to find out, and the answer is the same
* for everyone. That is attachment and not position โ index.ts hands `auth` to `/v1/me`
* and `/v1/sessions` and to nothing else, so moving this mount would not change it.
* Deliberately NOT naming the model or provider that produced the numbers โ
* a public endpoint that announces the deployment's model id would be publishing
* infrastructure to answer a question about file sizes. The 400 from `POST /v1/sessions`
* is where a caller with an actual rejected upload gets the detail.
*
* Computed once at construction: config does not hot-reload in v1, so there is
* nothing here that can change between requests.
*/
export function limitsRouter(cfg: IrisConfig): Router {
const r = Router();
const limits = resolveImageLimits(cfg);
const body = {
// Total pages across all parts of one upload, whether they arrived as images or
// as the pages of a PDF.
max_pages: MAX_PDF_PAGES,
image: {
max_bytes: limits.max_image_bytes,
max_long_edge_px: limits.max_long_edge_px,
max_dimension_px: limits.max_dimension_px,
media_types: limits.media_types,
extensions: limits.extensions,
hint: imageLimitsHint(limits),
},
pdf: { max_pages: MAX_PDF_PAGES },
// What one request may carry, as opposed to what one image may be. Both are memory
// bounds rather than facts about documents (util/requestLimits.ts), and both are
// enforced in front of multer rather than inside the handler, so a client that
// batches uploads can see the shape of the request it should send.
upload: { max_files: MAX_UPLOAD_FILES, max_request_bytes: uploadCeilingBytes() },
// How often it may be asked, or null when this deployment does not limit request
// volume in the app. Same rationale as the upload limits: a budget a client can read
// is one it can pace itself against, instead of discovering it by being refused.
rate_limits: publishedRateLimits(cfg),
};
r.get("/", (_req, res) => {
// Cacheable for a long while by anything in front of this deployment: the answer
// changes only when an operator edits config and restarts.
res.set("Cache-Control", "public, max-age=3600");
res.json(body);
});
return r;
}