๐Ÿ“ฆ EqualifyEverything / equalify-iris

๐Ÿ“„ limits.ts ยท 78 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
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;
}