File size: 3,354 Bytes
cd8bd0a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
/**
 * Next.js 16 Proxy File Contract β€” Lockdown Test
 *
 * Next.js 16 deprecated `middleware.ts` in favour of `proxy.ts` (commit
 * 3fb72b973 renamed our copy to match the new convention). The framework
 * only invokes this file when ALL of the following hold:
 *
 *   1. The file lives at `src/proxy.ts` (since `src/app` is the app dir).
 *   2. It exports a function named exactly `proxy` (or default).
 *   3. The function delegates to `runAuthzPipeline` with `enforce: true`.
 *   4. The `config.matcher` covers every prefix routes are mounted under,
 *      so unauthenticated requests cannot slip past the centralized
 *      authorization tiers (PUBLIC / CLIENT_API / MANAGEMENT).
 *
 * Without ANY of these guarantees the pipeline silently becomes dead code
 * and every `/api/*` route falls back to per-route self-enforcement, which
 * is the failure mode that led to the v3.8.4 hardening pass. Lock the
 * contract down here so a future rename / refactor cannot regress it
 * unnoticed.
 *
 * @see docs/security/ROUTE_GUARD_TIERS.md
 * @see https://nextjs.org/docs/app/api-reference/file-conventions/proxy
 */

import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";

test("Next.js 16 proxy file exists at src/proxy.ts (not src/middleware.ts)", () => {
  assert.ok(fs.existsSync("src/proxy.ts"), "src/proxy.ts must exist (Next.js 16 file convention)");
  assert.ok(
    !fs.existsSync("src/middleware.ts"),
    "src/middleware.ts must NOT exist β€” Next.js 16 deprecated middleware.ts, the active file is src/proxy.ts"
  );
});

test("proxy.ts exports a function named 'proxy' (Next.js 16 requires this exact name)", () => {
  const content = fs.readFileSync("src/proxy.ts", "utf8");
  assert.match(
    content,
    /export\s+async\s+function\s+proxy\s*\(/,
    "must export `async function proxy(...)` β€” Next.js 16 only invokes this exact name"
  );
});

test("proxy.ts delegates to runAuthzPipeline with enforce: true", () => {
  const content = fs.readFileSync("src/proxy.ts", "utf8");
  assert.match(
    content,
    /runAuthzPipeline\([^)]*\{\s*enforce:\s*true\s*\}\s*\)/,
    "must call runAuthzPipeline with { enforce: true } β€” otherwise the pipeline runs in observe-only mode and never blocks"
  );
});

test("proxy.ts config.matcher covers every /api/* route plus dashboard and v1 aliases", () => {
  const content = fs.readFileSync("src/proxy.ts", "utf8");
  // Required prefixes β€” drop one and the corresponding routes go unguarded.
  const requiredMatchers = [
    '"/api/:path*"',
    '"/dashboard/:path*"',
    '"/v1/:path*"',
    '"/chat/:path*"',
    '"/responses/:path*"',
    '"/codex/:path*"',
    '"/models"',
  ];
  for (const matcher of requiredMatchers) {
    assert.ok(
      content.includes(matcher),
      `proxy.ts config.matcher must include ${matcher} β€” otherwise routes under that prefix bypass the authz pipeline`
    );
  }
});

test("proxy.ts does not declare runtime: 'edge' (Next.js 16 proxy is Node-only)", () => {
  const content = fs.readFileSync("src/proxy.ts", "utf8");
  assert.ok(
    !/runtime:\s*['"]edge['"]/.test(content),
    "proxy.ts MUST NOT set runtime: 'edge' β€” Next.js 16 only supports nodejs in proxy.ts. The pipeline depends on Node-only modules (jose, better-sqlite3) and would crash at request time on the edge runtime."
  );
});