theokit 0.70.2 → 0.72.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{actions-virtual-module-2SM6YMTM.js → actions-virtual-module-6LND2WHL.js} +4 -4
- package/dist/{actions-virtual-module-5LRBP6YK.js → actions-virtual-module-WP47EKD7.js} +3 -3
- package/dist/adapters/agent-mount.js +2 -2
- package/dist/{agent-4FXJXZFD.js → agent-KUP3XA2S.js} +2 -2
- package/dist/{app-typed-client-LIIPHLO6.js → app-typed-client-6VIHSYJH.js} +3 -3
- package/dist/{app-typed-client-JFPV4YLS.js → app-typed-client-IMRSVARP.js} +4 -4
- package/dist/{aws-lambda-OCJM5YGB.js → aws-lambda-7AZNJGWU.js} +39 -8
- package/dist/aws-lambda-7AZNJGWU.js.map +1 -0
- package/dist/{build-ZC46IAFI.js → build-4J24BAEC.js} +7 -7
- package/dist/build-4J24BAEC.js.map +1 -0
- package/dist/{bun-KRUAWYRW.js → bun-NKVHOFSJ.js} +8 -105
- package/dist/bun-NKVHOFSJ.js.map +1 -0
- package/dist/{chunk-PGDBSK2L.js → chunk-7CHPOMP5.js} +2 -2
- package/dist/{chunk-FWEXQ2BR.js → chunk-BZ6DFSKG.js} +8 -8
- package/dist/{chunk-FFGP2LGQ.js → chunk-C5CAL3DX.js} +266 -1
- package/dist/chunk-C5CAL3DX.js.map +1 -0
- package/dist/{chunk-OSBGZYXZ.js → chunk-D2R5SJEL.js} +16 -2
- package/dist/chunk-D2R5SJEL.js.map +1 -0
- package/dist/{chunk-5XXXJ6DT.js → chunk-EZHDAVAF.js} +23 -7
- package/dist/chunk-EZHDAVAF.js.map +1 -0
- package/dist/{chunk-3DGIANAN.js → chunk-FJEKQ4FT.js} +1 -1
- package/dist/{chunk-3DGIANAN.js.map → chunk-FJEKQ4FT.js.map} +1 -1
- package/dist/{chunk-KI4DOTN4.js → chunk-G7U7FQVC.js} +3 -1
- package/dist/chunk-G7U7FQVC.js.map +1 -0
- package/dist/{chunk-XG6SQUEP.js → chunk-IB5VIXLR.js} +2 -2
- package/dist/{chunk-3NAKOWTX.js → chunk-IVGQMV3S.js} +2 -2
- package/dist/{chunk-2XYKRS7Y.js → chunk-JJXBRY2U.js} +76 -6
- package/dist/chunk-JJXBRY2U.js.map +1 -0
- package/dist/{chunk-DSPEN7ZJ.js → chunk-LJNMSKFL.js} +4 -4
- package/dist/{chunk-MS4A7N77.js → chunk-PEL3MJLM.js} +2 -2
- package/dist/{chunk-V6VARIR3.js → chunk-RDG5GMDW.js} +9 -9
- package/dist/{chunk-TNI4MARH.js → chunk-SFGGRC35.js} +23 -6
- package/dist/chunk-SFGGRC35.js.map +1 -0
- package/dist/{chunk-FFS7IRZF.js → chunk-SZWGXHY3.js} +2 -2
- package/dist/{chunk-SX7ZAHNE.js → chunk-WBB3MEEN.js} +16 -2
- package/dist/chunk-WBB3MEEN.js.map +1 -0
- package/dist/{chunk-TMQ52AZR.js → chunk-X2LFNDJQ.js} +2 -2
- package/dist/{chunk-NZDGV4YJ.js → chunk-YCF3N4JS.js} +2 -2
- package/dist/cli/index.js +9 -9
- package/dist/{cloudflare-GK4VD3EU.js → cloudflare-4M4XVNUH.js} +42 -11
- package/dist/cloudflare-4M4XVNUH.js.map +1 -0
- package/dist/{config-support-O4REYSK5.js → config-support-SB5NDQZB.js} +11 -5
- package/dist/{config-support-O4REYSK5.js.map → config-support-SB5NDQZB.js.map} +1 -1
- package/dist/{deno-deploy-WMNXRVGL.js → deno-deploy-IVACZ62D.js} +46 -12
- package/dist/deno-deploy-IVACZ62D.js.map +1 -0
- package/dist/{dev-CD4YU5FN.js → dev-JZVHEYN4.js} +9 -9
- package/dist/index.d.ts +12 -2
- package/dist/index.js +4 -4
- package/dist/{info-2H7I55YN.js → info-SUX7QVA3.js} +4 -4
- package/dist/{internal-api-TMLSNYMP.js → internal-api-3IZVNNGB.js} +3 -3
- package/dist/{internal-api-WAKX6AZW.js → internal-api-BNDRCEPZ.js} +4 -4
- package/dist/{load-config-VMKOV44Y.js → load-config-52H3EKAA.js} +4 -4
- package/dist/{mcp-HS26ZCU4.js → mcp-HCXHATU3.js} +3 -3
- package/dist/{netlify-VOL54SC7.js → netlify-3HEWV375.js} +28 -4
- package/dist/netlify-3HEWV375.js.map +1 -0
- package/dist/{node-OSVPYQ3D.js → node-QUGTW6XV.js} +2 -2
- package/dist/{observability-bootstrap-YQKMEYW7.js → observability-bootstrap-FP6LMMJE.js} +3 -3
- package/dist/{openapi-N4CXRUNT.js → openapi-Q53YOBIU.js} +4 -4
- package/dist/{preview-TYLABTAE.js → preview-FSUPVADO.js} +3 -3
- package/dist/{rate-limit-BdNDZ3vt.d.ts → rate-limit-fULgvzCz.d.ts} +11 -1
- package/dist/{rate-limit-store-BEJnhWdw.d.ts → rate-limit-store-n1wD4Yk4.d.ts} +48 -1
- package/dist/registry-SDBODRU6.js +23 -0
- package/dist/{routes-ECCDRHXZ.js → routes-FDF4SLOB.js} +4 -4
- package/dist/{schema-ND7GHPQK.js → schema-LHNWUR4V.js} +3 -3
- package/dist/{schemas-QCMSKQDK.js → schemas-2R2J64ZH.js} +2 -2
- package/dist/server/auth/index.d.ts +1 -1
- package/dist/server/index.d.ts +3 -3
- package/dist/server/index.js +15 -5
- package/dist/server/index.js.map +1 -1
- package/dist/server/rate-limit/index.d.ts +106 -14
- package/dist/server/rate-limit/index.js +15 -5
- package/dist/server/storage/index.js +1 -1
- package/dist/{server-boundary-RX2AFU5U.js → server-boundary-P52I6YMH.js} +4 -4
- package/dist/{server-boundary-NZPP33G6.js → server-boundary-WUKORSW2.js} +3 -3
- package/dist/{start-TYXS5I4M.js → start-LCA67RES.js} +14 -11
- package/dist/start-LCA67RES.js.map +1 -0
- package/dist/{static-7OJNB7D2.js → static-NQPLPAB6.js} +6 -2
- package/dist/{static-7OJNB7D2.js.map → static-NQPLPAB6.js.map} +1 -1
- package/dist/{theo-cloud-3ET4G5T6.js → theo-cloud-HMFNZ3FS.js} +3 -1
- package/dist/{theo-cloud-3ET4G5T6.js.map → theo-cloud-HMFNZ3FS.js.map} +1 -1
- package/dist/{vercel-LZTDSDJF.js → vercel-5PMYEEKF.js} +54 -6
- package/dist/vercel-5PMYEEKF.js.map +1 -0
- package/dist/vite-plugin/index.d.ts +2 -2
- package/dist/vite-plugin/index.js +4 -4
- package/dist/{vite-plugin-DDILUFYY.js → vite-plugin-WFA7NPYK.js} +8 -8
- package/package.json +1 -1
- package/dist/aws-lambda-OCJM5YGB.js.map +0 -1
- package/dist/build-ZC46IAFI.js.map +0 -1
- package/dist/bun-KRUAWYRW.js.map +0 -1
- package/dist/chunk-2XYKRS7Y.js.map +0 -1
- package/dist/chunk-5XXXJ6DT.js.map +0 -1
- package/dist/chunk-FFGP2LGQ.js.map +0 -1
- package/dist/chunk-KI4DOTN4.js.map +0 -1
- package/dist/chunk-OSBGZYXZ.js.map +0 -1
- package/dist/chunk-SX7ZAHNE.js.map +0 -1
- package/dist/chunk-TNI4MARH.js.map +0 -1
- package/dist/cloudflare-GK4VD3EU.js.map +0 -1
- package/dist/deno-deploy-WMNXRVGL.js.map +0 -1
- package/dist/netlify-VOL54SC7.js.map +0 -1
- package/dist/registry-DDBXDHJ2.js +0 -23
- package/dist/start-TYXS5I4M.js.map +0 -1
- package/dist/vercel-LZTDSDJF.js.map +0 -1
- /package/dist/{actions-virtual-module-2SM6YMTM.js.map → actions-virtual-module-6LND2WHL.js.map} +0 -0
- /package/dist/{actions-virtual-module-5LRBP6YK.js.map → actions-virtual-module-WP47EKD7.js.map} +0 -0
- /package/dist/{agent-4FXJXZFD.js.map → agent-KUP3XA2S.js.map} +0 -0
- /package/dist/{app-typed-client-LIIPHLO6.js.map → app-typed-client-6VIHSYJH.js.map} +0 -0
- /package/dist/{app-typed-client-JFPV4YLS.js.map → app-typed-client-IMRSVARP.js.map} +0 -0
- /package/dist/{chunk-PGDBSK2L.js.map → chunk-7CHPOMP5.js.map} +0 -0
- /package/dist/{chunk-FWEXQ2BR.js.map → chunk-BZ6DFSKG.js.map} +0 -0
- /package/dist/{chunk-XG6SQUEP.js.map → chunk-IB5VIXLR.js.map} +0 -0
- /package/dist/{chunk-3NAKOWTX.js.map → chunk-IVGQMV3S.js.map} +0 -0
- /package/dist/{chunk-DSPEN7ZJ.js.map → chunk-LJNMSKFL.js.map} +0 -0
- /package/dist/{chunk-MS4A7N77.js.map → chunk-PEL3MJLM.js.map} +0 -0
- /package/dist/{chunk-V6VARIR3.js.map → chunk-RDG5GMDW.js.map} +0 -0
- /package/dist/{chunk-FFS7IRZF.js.map → chunk-SZWGXHY3.js.map} +0 -0
- /package/dist/{chunk-TMQ52AZR.js.map → chunk-X2LFNDJQ.js.map} +0 -0
- /package/dist/{chunk-NZDGV4YJ.js.map → chunk-YCF3N4JS.js.map} +0 -0
- /package/dist/{dev-CD4YU5FN.js.map → dev-JZVHEYN4.js.map} +0 -0
- /package/dist/{info-2H7I55YN.js.map → info-SUX7QVA3.js.map} +0 -0
- /package/dist/{internal-api-TMLSNYMP.js.map → internal-api-3IZVNNGB.js.map} +0 -0
- /package/dist/{internal-api-WAKX6AZW.js.map → internal-api-BNDRCEPZ.js.map} +0 -0
- /package/dist/{load-config-VMKOV44Y.js.map → load-config-52H3EKAA.js.map} +0 -0
- /package/dist/{mcp-HS26ZCU4.js.map → mcp-HCXHATU3.js.map} +0 -0
- /package/dist/{node-OSVPYQ3D.js.map → node-QUGTW6XV.js.map} +0 -0
- /package/dist/{observability-bootstrap-YQKMEYW7.js.map → observability-bootstrap-FP6LMMJE.js.map} +0 -0
- /package/dist/{openapi-N4CXRUNT.js.map → openapi-Q53YOBIU.js.map} +0 -0
- /package/dist/{preview-TYLABTAE.js.map → preview-FSUPVADO.js.map} +0 -0
- /package/dist/{registry-DDBXDHJ2.js.map → registry-SDBODRU6.js.map} +0 -0
- /package/dist/{routes-ECCDRHXZ.js.map → routes-FDF4SLOB.js.map} +0 -0
- /package/dist/{schema-ND7GHPQK.js.map → schema-LHNWUR4V.js.map} +0 -0
- /package/dist/{schemas-QCMSKQDK.js.map → schemas-2R2J64ZH.js.map} +0 -0
- /package/dist/{server-boundary-RX2AFU5U.js.map → server-boundary-P52I6YMH.js.map} +0 -0
- /package/dist/{server-boundary-NZPP33G6.js.map → server-boundary-WUKORSW2.js.map} +0 -0
- /package/dist/{vite-plugin-DDILUFYY.js.map → vite-plugin-WFA7NPYK.js.map} +0 -0
|
@@ -5,18 +5,20 @@ import {
|
|
|
5
5
|
} from "./chunk-V5SSTT3C.js";
|
|
6
6
|
import {
|
|
7
7
|
deployedEntryPreamble
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-IB5VIXLR.js";
|
|
9
9
|
import {
|
|
10
10
|
agentsDirLiteral,
|
|
11
11
|
deployedAgentsFragment,
|
|
12
|
+
deployedRateLimitFragment,
|
|
12
13
|
deployedRuntimeConfigFragment,
|
|
13
14
|
deployedTraceFragment,
|
|
14
15
|
describeDeployedSecurityHeaders,
|
|
16
|
+
rateLimitCheckFragment,
|
|
15
17
|
serverDirLiteral
|
|
16
|
-
} from "./chunk-
|
|
18
|
+
} from "./chunk-C5CAL3DX.js";
|
|
17
19
|
import {
|
|
18
20
|
nodeAdapter
|
|
19
|
-
} from "./chunk-
|
|
21
|
+
} from "./chunk-G7U7FQVC.js";
|
|
20
22
|
import "./chunk-WSMLSIS5.js";
|
|
21
23
|
import {
|
|
22
24
|
assertServicesUnsupported,
|
|
@@ -26,107 +28,6 @@ import {
|
|
|
26
28
|
// src/adapters/bun.ts
|
|
27
29
|
import { mkdirSync, writeFileSync } from "fs";
|
|
28
30
|
import { resolve } from "path";
|
|
29
|
-
|
|
30
|
-
// src/adapters/deployed-rate-limit.ts
|
|
31
|
-
var UnserialisableRateLimitError = class extends Error {
|
|
32
|
-
constructor(target, reason, ways) {
|
|
33
|
-
super(
|
|
34
|
-
[
|
|
35
|
-
`Refusing to build for \`${target}\`: theo.config.ts declares a rate limit this target cannot carry.`,
|
|
36
|
-
``,
|
|
37
|
-
` ${reason}`,
|
|
38
|
-
``,
|
|
39
|
-
` Ways forward:`,
|
|
40
|
-
...ways.map((w) => ` \u2022 ${w}`),
|
|
41
|
-
``,
|
|
42
|
-
` This refuses rather than dropping the key, because a rate limit that silently does not`,
|
|
43
|
-
` apply looks exactly like one that does (usetheokit/theokit#461, #508).`
|
|
44
|
-
].join("\n")
|
|
45
|
-
);
|
|
46
|
-
this.target = target;
|
|
47
|
-
this.reason = reason;
|
|
48
|
-
}
|
|
49
|
-
target;
|
|
50
|
-
reason;
|
|
51
|
-
name = "UnserialisableRateLimitError";
|
|
52
|
-
};
|
|
53
|
-
function bakeableRateLimit(rateLimit, target) {
|
|
54
|
-
if (rateLimit === void 0) return void 0;
|
|
55
|
-
const cfg = rateLimit;
|
|
56
|
-
if (typeof cfg.keyBy === "function") {
|
|
57
|
-
throw new UnserialisableRateLimitError(
|
|
58
|
-
target,
|
|
59
|
-
"`security.rateLimit.keyBy` is a function, and a deployed entry has no literal for a closure.",
|
|
60
|
-
[
|
|
61
|
-
"use `keyBy: 'ip'` (the default), which this target resolves from the connection",
|
|
62
|
-
"build for `node` and run `theokit start`, which can call the function"
|
|
63
|
-
]
|
|
64
|
-
);
|
|
65
|
-
}
|
|
66
|
-
if (cfg.keyBy === "session" || cfg.keyBy === "user") {
|
|
67
|
-
throw new UnserialisableRateLimitError(
|
|
68
|
-
target,
|
|
69
|
-
`\`security.rateLimit.keyBy: '${cfg.keyBy}'\` needs a resolved session, and the limit runs before routing.`,
|
|
70
|
-
[
|
|
71
|
-
"use `keyBy: 'ip'`, which is resolvable at that point",
|
|
72
|
-
"build for `node` and run `theokit start`"
|
|
73
|
-
]
|
|
74
|
-
);
|
|
75
|
-
}
|
|
76
|
-
if (cfg.routes !== void 0) {
|
|
77
|
-
throw new UnserialisableRateLimitError(
|
|
78
|
-
target,
|
|
79
|
-
"`security.rateLimit.routes` needs the matched route, which is decided after the limit runs here.",
|
|
80
|
-
[
|
|
81
|
-
"declare a single global limit (`windowMs` + `max`)",
|
|
82
|
-
"build for `node` and run `theokit start`, which applies per-route limits"
|
|
83
|
-
]
|
|
84
|
-
);
|
|
85
|
-
}
|
|
86
|
-
const windowMs = cfg.windowMs;
|
|
87
|
-
const max = cfg.max;
|
|
88
|
-
if (typeof windowMs !== "number" || typeof max !== "number") return void 0;
|
|
89
|
-
return { windowMs, max };
|
|
90
|
-
}
|
|
91
|
-
function deployedRateLimitFragment(rateLimit, target, addressExpression) {
|
|
92
|
-
const baked = bakeableRateLimit(rateLimit, target);
|
|
93
|
-
if (baked === void 0) return [];
|
|
94
|
-
return [
|
|
95
|
-
`// #508 \u2014 the limit the app declared, carried as a literal because a deployed entry has no`,
|
|
96
|
-
`// theo.config.ts to read. The counter lives in this process, which outlives a request here.`,
|
|
97
|
-
`const RATE_LIMIT = createRateLimiterWeb({ windowMs: ${baked.windowMs}, max: ${baked.max} })`,
|
|
98
|
-
``,
|
|
99
|
-
`/**`,
|
|
100
|
-
` * The caller's address, from the connection rather than from a header.`,
|
|
101
|
-
` *`,
|
|
102
|
-
` * A header a client can set is a key a client can choose, which makes the bucket theirs to`,
|
|
103
|
-
` * split. An unresolved address falls back to one shared bucket \u2014 rare here, since that only`,
|
|
104
|
-
` * happens for a connection already gone, and the same fallback \`theokit start\` uses.`,
|
|
105
|
-
` */`,
|
|
106
|
-
`function callerAddress(request, server) {`,
|
|
107
|
-
` return ${addressExpression} ?? 'unknown'`,
|
|
108
|
-
`}`,
|
|
109
|
-
``,
|
|
110
|
-
`/** The 429 a limited caller gets, carrying the limiter's own headers. */`,
|
|
111
|
-
`function rateLimited(result) {`,
|
|
112
|
-
` return new Response(JSON.stringify({ error: { code: 'RATE_LIMITED', message: 'Too many requests' } }), {`,
|
|
113
|
-
` status: 429,`,
|
|
114
|
-
` headers: { 'Content-Type': 'application/json', ...result.headers },`,
|
|
115
|
-
` })`,
|
|
116
|
-
`}`
|
|
117
|
-
];
|
|
118
|
-
}
|
|
119
|
-
function rateLimitCheckFragment(rateLimit, indent) {
|
|
120
|
-
if (rateLimit === void 0) return [];
|
|
121
|
-
return [
|
|
122
|
-
`${indent}const limit = RATE_LIMIT(callerAddress(request, server))`,
|
|
123
|
-
`${indent}if (limit.limited) {`,
|
|
124
|
-
`${indent} return withCors(request, withSecurityHeaders(rateLimited(limit), SECURITY_HEADERS))`,
|
|
125
|
-
`${indent}}`
|
|
126
|
-
];
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
// src/adapters/bun.ts
|
|
130
31
|
var RUNTIME_GUARDS = [
|
|
131
32
|
`// EC-1: dev-mode guard`,
|
|
132
33
|
`if (process.env.NODE_ENV !== 'production') {`,
|
|
@@ -347,6 +248,8 @@ var bunAdapter = {
|
|
|
347
248
|
// already receives `server`, whose `requestIP` gives the peer address without a header.
|
|
348
249
|
"rateLimit"
|
|
349
250
|
],
|
|
251
|
+
// B-257 — a long-lived process: the in-process counter survives between requests.
|
|
252
|
+
enforcesRateLimit: "always",
|
|
350
253
|
build(config, cwd, ctx) {
|
|
351
254
|
return buildBun(config, cwd, {}, ctx);
|
|
352
255
|
}
|
|
@@ -356,4 +259,4 @@ export {
|
|
|
356
259
|
bunAdapter,
|
|
357
260
|
renderBunEntry
|
|
358
261
|
};
|
|
359
|
-
//# sourceMappingURL=bun-
|
|
262
|
+
//# sourceMappingURL=bun-NKVHOFSJ.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/adapters/bun.ts"],"sourcesContent":["/* eslint-disable security/detect-non-literal-fs-filename --\n * Bun deploy adapter. Writes to `cwd/.theokit/bun/`. Build-time.\n */\nimport { mkdirSync, writeFileSync } from 'node:fs'\nimport { resolve } from 'node:path'\n\nimport type { TheoConfig } from '../config/schema.js'\nimport type { SecurityHeadersConfig } from '../core/contracts/security-headers.js'\nimport { assertServicesUnsupported, readManifest } from '../services/index.js'\n\nimport { deployedAgentsFragment } from './deployed-agents.js'\nimport { type DeployedCorsOptions } from './deployed-cors.js'\nimport { type DeployedCsrfOptions } from './deployed-csrf.js'\nimport { planDeployedPlugins } from './deployed-plugins-module.js'\nimport { deployedEntryPreamble } from './deployed-preamble.js'\nimport {\n deployedRateLimitFragment,\n rateLimitCheckFragment,\n type DeployedRateLimitOptions,\n} from './deployed-rate-limit.js'\nimport {\n deployedRuntimeConfigFragment,\n agentsDirLiteral,\n serverDirLiteral,\n type DeployedRuntimeConfigOptions,\n type DeployedAgentsDirOptions,\n type DeployedServerDirOptions,\n} from './deployed-runtime-config.js'\nimport { deployedTraceFragment } from './deployed-trace.js'\nimport { nodeAdapter } from './node.js'\nimport { describeDeployedSecurityHeaders } from './security-headers.js'\nimport type { AdapterBuildContext, DeployAdapter } from './types.js'\n\nexport interface BunBuildDeps {\n runNodeBuild?: (config: TheoConfig, cwd: string, ctx?: AdapterBuildContext) => Promise<void>\n writeEntry?: (path: string, content: string) => void\n ensureDir?: (path: string) => void\n}\n\n/**\n * The module-scope refusals: this entry is production-only and Bun-only.\n *\n * Extracted from `renderBunEntry` when adding the configured-directory literal pushed it past the\n * per-function line budget. They are a cohesive block — three checks that all refuse to start —\n * and lifting them keeps the budget a signal about the renderer rather than about its preamble.\n */\nconst RUNTIME_GUARDS: readonly string[] = [\n `// EC-1: dev-mode guard`,\n `if (process.env.NODE_ENV !== 'production') {`,\n ` console.error('TheoBunAdapter is production-only. Use \\\\'theokit dev\\\\' (Node) for development.')`,\n ` process.exit(1)`,\n `}`,\n ``,\n `// Version check: Bun >= 1.1`,\n `if (typeof Bun === 'undefined') {`,\n ` console.error('TheoBunAdapter must run inside Bun. Got Node.js or unknown runtime.')`,\n ` process.exit(1)`,\n `}`,\n `{`,\n ` const [maj, min] = (Bun.version ?? '0.0').split('.').map(Number)`,\n ` if (maj < 1 || (maj === 1 && min < 1)) {`,\n ` console.error('TheoBunAdapter requires Bun >= 1.1; got ' + Bun.version)`,\n ` process.exit(1)`,\n ` }`,\n `}`,\n]\n\nexport function renderBunEntry(\n port: number,\n opts: {\n ssrStreaming?: boolean\n securityHeaders?: SecurityHeadersConfig\n } & DeployedAgentsDirOptions &\n DeployedCsrfOptions &\n DeployedRuntimeConfigOptions &\n DeployedServerDirOptions &\n DeployedCorsOptions &\n DeployedRateLimitOptions = {},\n): string {\n const streamingComment = opts.ssrStreaming\n ? `// T2.3 — ssrStreaming on; renderStreamingWeb may be consumed by app code`\n : `// (ssrStreaming off)`\n const runtimeConfig = deployedRuntimeConfigFragment(opts)\n const agentsFragment = deployedAgentsFragment(\n {\n kind: 'scan',\n projectRoot: 'cwd',\n loadModule: 'loadModule',\n serverDir: 'serverDir',\n agentsDirLiteral: agentsDirLiteral(opts),\n },\n {\n pathname: 'pathname',\n // B-185 — bound only when the runtime-config fragment declared the const, which is\n // exactly when a plugins module was emitted. Referencing it otherwise would emit an\n // identifier the entry never declares.\n pluginRunnerExpr:\n opts.runtimeConfigModule === undefined ? undefined : 'await THEO_PLUGIN_RUNNER',\n },\n )\n return [\n `// Generated by Theo — Bun Adapter`,\n `// Run: bun run .theokit/bun/server.mjs`,\n `// Requires Bun >= 1.1`,\n streamingComment,\n ``,\n ...RUNTIME_GUARDS,\n ``,\n `import { resolve, join } from 'node:path'`,\n `import { existsSync } from 'node:fs'`,\n `import { scanServerRoutes, matchRoute, executeRoute, createProductionLoader, extractTraceIdFromRequest, TRACE_HEADER, createCorsWebHandler${opts.rateLimit === undefined ? '' : ', createRateLimiterWeb'} } from 'theokit/server'`,\n `import { createWebShim } from 'theokit/adapters/web-shim'`,\n `import { buildSecurityHeaders, withSecurityHeaders } from 'theokit/adapters/security-headers'`,\n `// T3.2 — WS bridge for Bun runtime`,\n `import { createBunWsBridge } from 'theokit/adapters/ws-shim'`,\n `import { scanWebSocketRoutes } from 'theokit/server'`,\n ``,\n `const cwd = process.cwd()`,\n `const clientDir = resolve(cwd, '.theokit/client')`,\n `const serverDir = resolve(cwd, ${serverDirLiteral(opts)})`,\n `const port = process.env.PORT ? Number(process.env.PORT) : ${port}`,\n ``,\n `// #410 — the security baseline \\`theokit start\\` puts on every response,`,\n `// carried here as a literal because the deployed process has no`,\n `// theo.config.ts to read. Bun is the one Web target whose own handler serves`,\n `// the HTML document (static file + SPA fallback below), so the document gets`,\n `// these headers too -- with a nonce-less CSP, because that HTML was written`,\n `// at build time and carries no nonce on its script tags (EC-4).`,\n ...deployedEntryPreamble(runtimeConfig, agentsFragment, opts, 'bun'),\n ``,\n ...deployedRateLimitFragment(opts.rateLimit, 'bun', 'server.requestIP(request)?.address'),\n ``,\n `const routes = existsSync(serverDir) ? scanServerRoutes(serverDir) : []`,\n `const wsRoutes = existsSync(serverDir) ? scanWebSocketRoutes(serverDir) : []`,\n `const loadModule = createProductionLoader()`,\n ``,\n `// Bun WebSocket config — single handler dispatches to first ws route,`,\n `// or a default no-op when none declared. Per-route routing happens above`,\n `// (the upgrade decision is made in fetch handler before Bun.serve hands off).`,\n `const wsBridge = wsRoutes.length > 0`,\n ` ? createBunWsBridge({`,\n ` onOpen: () => {},`,\n ` onMessage: (ws, data) => { ws.send(data) },`,\n ` onClose: () => {},`,\n ` })`,\n ` : { open: () => {}, message: () => {}, close: () => {} }`,\n ``,\n `function notFoundResponse() {`,\n ` return new Response(JSON.stringify({ error: { code: 'NOT_FOUND', message: 'Route not found' } }), {`,\n ` status: 404,`,\n ` headers: { 'Content-Type': 'application/json' },`,\n ` })`,\n `}`,\n ``,\n `Bun.serve({`,\n ` port,`,\n ` websocket: wsBridge,`,\n ` async fetch(request, server) {`,\n ` // #409 — the preflight is answered BEFORE anything routes: an OPTIONS the router`,\n ` // handles is an OPTIONS the browser never gets a CORS answer to.`,\n ` const preflight = corsPreflight(request)`,\n ` if (preflight !== null) return withSecurityHeaders(preflight, SECURITY_HEADERS)`,\n ...rateLimitCheckFragment(opts.rateLimit, ' '),\n ` return withCors(request, withSecurityHeaders(await handleRequest(request), SECURITY_HEADERS))`,\n ` },`,\n `})`,\n ``,\n ...bunHandleRequestFragment(runtimeConfig.executeRouteSpread, agentsFragment.branch),\n ].join('\\n')\n}\n\n/**\n * The request handler, as generated source.\n *\n * Extracted for the reason `vercel.ts` extracts its own fragments: the emitter is one array\n * literal, so every line the entry gains counts against `max-lines-per-function`, and #410 added\n * the CSRF literal to an emitter that was already sitting exactly at the ceiling.\n */\nfunction bunHandleRequestFragment(runtimeSpread: string, agentBranch: readonly string[]): string[] {\n return [\n `async function handleRequest(request) {`,\n ` const url = new URL(request.url)`,\n ` const pathname = url.pathname`,\n ``,\n ` // 1) Static assets`,\n ` const staticPath = pathname === '/' ? '/index.html' : pathname`,\n ` const fullStatic = join(clientDir, staticPath)`,\n ` if (existsSync(fullStatic)) {`,\n ` const file = Bun.file(fullStatic)`,\n ` if (await file.exists()) return new Response(file)`,\n ` }`,\n ``,\n ` // 2) API routes through the full executeRoute pipeline via the shim`,\n ` if (pathname.startsWith('/api/') || __theoIsAgentCardPath(pathname)) {`,\n ...agentBranch,\n ``,\n ` const match = matchRoute(pathname, routes)`,\n ` if (!match) return notFoundResponse()`,\n ` const { req, res, toResponse } = createWebShim(request)`,\n ...deployedTraceFragment('request', ' '),\n ` const method = request.method.toUpperCase()`,\n ` // #382 — not awaited: toResponse() settles at the headers and carries`,\n ` // a live body, so Bun.serve streams while the handler writes.`,\n ` return toResponse(executeRoute({ route: match.route, method, params: match.params, req, res, loadModule, serverDir, requestId, ...CSRF_CONFIG, ${runtimeSpread} }))`,\n ` }`,\n ``,\n ` // 3) SPA fallback`,\n ` const indexPath = join(clientDir, 'index.html')`,\n ` if (existsSync(indexPath)) return new Response(Bun.file(indexPath))`,\n ``,\n ` return notFoundResponse()`,\n `}`,\n ``,\n `console.log('Theo (Bun) listening on http://localhost:' + port)`,\n ]\n}\n\nexport async function buildBun(\n config: TheoConfig,\n cwd: string,\n deps: BunBuildDeps = {},\n ctx?: AdapterBuildContext,\n): Promise<void> {\n // Wave 2 (T2.2) — reject polyglot services on this adapter.\n assertServicesUnsupported('bun', readManifest(cwd))\n\n const runNodeBuild = deps.runNodeBuild ?? nodeAdapter.build.bind(nodeAdapter)\n await runNodeBuild(config, cwd, ctx)\n\n const outputDir = resolve(cwd, '.theokit/bun')\n const ensureDir = deps.ensureDir ?? ((p: string) => mkdirSync(p, { recursive: true }))\n ensureDir(outputDir)\n\n const pluginsPlan = planDeployedPlugins(config.plugins, 'bun')\n const entry = renderBunEntry(config.port, {\n ssrStreaming: config.ssrStreaming,\n securityHeaders: config.security?.headers,\n csrf: config.security?.csrf,\n disallowed: config.security?.disallowed,\n cors: config.security?.cors,\n // #425 — a selector, not a transformer, so it rides as a literal like the values above.\n serialization: config.serialization,\n // #425 — the ONE concern that is not a literal. A closure cannot be baked, so a plugin\n // declared by module specifier is imported by the emitted module instead; a constructed one\n // is refused by name at build time rather than dropped in silence.\n runtimeConfigModule: pluginsPlan?.moduleSpecifier,\n // #95 — the configured directories, which `build.ts:210` already threads into the Vite\n // plugins. Without these two lines the literals above fall to their defaults and a project\n // with a custom dir deploys an entry that resolves a directory it does not have.\n serverDir: config.serverDir,\n agentsDir: config.agentsDir,\n })\n const write =\n deps.writeEntry ??\n ((p, c) => {\n writeFileSync(p, c)\n })\n if (pluginsPlan !== undefined) {\n // Beside the entry, so the emitted import is a sibling. Written through the same seam as\n // the entry so a test that captures one captures both (#425).\n write(resolve(outputDir, 'theo.plugins.mjs'), pluginsPlan.source)\n }\n write(resolve(outputDir, 'server.mjs'), entry)\n\n // eslint-disable-next-line no-console -- CLI build progress\n console.log('\\n ✓ Bun output → .theokit/bun/server.mjs')\n // eslint-disable-next-line no-console -- CLI build progress\n console.log(\n `${describeDeployedSecurityHeaders({\n target: 'bun',\n securityHeaders: config.security?.headers,\n // No deploy target other than the streamed Cloudflare worker renders HTML\n // at request time, so none of the rest can mint a nonce.\n mintsNonce: false,\n // Bun serves `.theokit/client` itself, so the document DOES pass through\n // the handler these headers are attached to.\n documentHeaders: 'handler',\n })}\\n`,\n )\n}\n\nexport const bunAdapter: DeployAdapter = {\n name: 'bun',\n streamsResponses: true,\n // #409 / #410 — the generated entry calls `executeRoute` with routes, loader\n // and serverDir only. CSRF, route policy, file middleware and Zod validation\n // still run because they live inside `executeRoute`; none of the remaining\n // configurable concerns reach it. Declared explicitly rather than omitted so\n // the gap is a statement in the source and not an absence.\n //\n // `securityHeaders` IS applied: the entry carries `security.headers` as a\n // literal and puts the built baseline on every response, the served document\n // included.\n servesAgents: true,\n appliesConfig: [\n 'securityHeaders',\n 'csrf',\n 'disallowed',\n 'cors',\n 'serialization',\n 'plugins',\n // #508 — bun is the first Web target to ENFORCE a declared limit rather than refuse the\n // build: `Bun.serve` is long-lived, so the in-process counter survives, and its handler\n // already receives `server`, whose `requestIP` gives the peer address without a header.\n 'rateLimit',\n ],\n // B-257 — a long-lived process: the in-process counter survives between requests.\n enforcesRateLimit: 'always',\n build(config, cwd, ctx) {\n return buildBun(config, cwd, {}, ctx)\n },\n}\n"],"mappings":"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","names":[]}
|
|
@@ -26,7 +26,7 @@ import {
|
|
|
26
26
|
parseAgentRequestBody,
|
|
27
27
|
readAgentPolicy,
|
|
28
28
|
subjectFromContext
|
|
29
|
-
} from "./chunk-
|
|
29
|
+
} from "./chunk-EZHDAVAF.js";
|
|
30
30
|
import {
|
|
31
31
|
getApprovalRegistry
|
|
32
32
|
} from "./chunk-BINRCILF.js";
|
|
@@ -901,4 +901,4 @@ export {
|
|
|
901
901
|
applyNonceToInlineScripts,
|
|
902
902
|
setupSsrDevMiddleware
|
|
903
903
|
};
|
|
904
|
-
//# sourceMappingURL=chunk-
|
|
904
|
+
//# sourceMappingURL=chunk-7CHPOMP5.js.map
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
isAgentCardPath,
|
|
14
14
|
matchAgentAuxRoute,
|
|
15
15
|
serveMatchedAuxRoute
|
|
16
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-IVGQMV3S.js";
|
|
17
17
|
import {
|
|
18
18
|
admitAgentRequest,
|
|
19
19
|
agentAccessDenied,
|
|
@@ -29,7 +29,7 @@ import {
|
|
|
29
29
|
securitySchema,
|
|
30
30
|
storageSchema,
|
|
31
31
|
uploadSchema
|
|
32
|
-
} from "./chunk-
|
|
32
|
+
} from "./chunk-WBB3MEEN.js";
|
|
33
33
|
import {
|
|
34
34
|
broadcastToDevtools
|
|
35
35
|
} from "./chunk-OIGJOYX6.js";
|
|
@@ -75,7 +75,7 @@ import {
|
|
|
75
75
|
} from "./chunk-VSLTKX5L.js";
|
|
76
76
|
import {
|
|
77
77
|
createRateLimiter
|
|
78
|
-
} from "./chunk-
|
|
78
|
+
} from "./chunk-SFGGRC35.js";
|
|
79
79
|
import {
|
|
80
80
|
createViteLoader
|
|
81
81
|
} from "./chunk-4ERYQBL6.js";
|
|
@@ -3024,19 +3024,19 @@ async function theoPluginAsync(rootOrOptions) {
|
|
|
3024
3024
|
}
|
|
3025
3025
|
const { assetsMapPlugin } = await import("./assets-map-CV2YZ3MY.js");
|
|
3026
3026
|
const assetsMap = assetsMapPlugin({ appDir: resolve9(projectRoot, options.appDir ?? "app") });
|
|
3027
|
-
const { appTypedClientPlugin } = await import("./app-typed-client-
|
|
3027
|
+
const { appTypedClientPlugin } = await import("./app-typed-client-6VIHSYJH.js");
|
|
3028
3028
|
const appClientPlugin = appTypedClientPlugin({
|
|
3029
3029
|
cwd: projectRoot,
|
|
3030
3030
|
serverDir: serverDirAbs,
|
|
3031
3031
|
distDir: resolve9(projectRoot, ".theokit")
|
|
3032
3032
|
});
|
|
3033
|
-
const { actionsVirtualModule } = await import("./actions-virtual-module-
|
|
3033
|
+
const { actionsVirtualModule } = await import("./actions-virtual-module-WP47EKD7.js");
|
|
3034
3034
|
const actionsPlugin = actionsVirtualModule({
|
|
3035
3035
|
serverDir: serverDirAbs,
|
|
3036
3036
|
distDir: resolve9(projectRoot, ".theokit")
|
|
3037
3037
|
});
|
|
3038
3038
|
const { agentsTypedClientPlugin } = await import("./agents-typed-client-PVUZU65B.js");
|
|
3039
|
-
const { generateManifest } = await import("./internal-api-
|
|
3039
|
+
const { generateManifest } = await import("./internal-api-3IZVNNGB.js");
|
|
3040
3040
|
const agentsClientPlugin = agentsTypedClientPlugin({
|
|
3041
3041
|
projectRoot,
|
|
3042
3042
|
agentsDir: agentsDirName,
|
|
@@ -3053,7 +3053,7 @@ async function theoPluginAsync(rootOrOptions) {
|
|
|
3053
3053
|
const controllerTransformPlugin = controllerSwcTransformPlugin({
|
|
3054
3054
|
serverDir: serverDirAbs
|
|
3055
3055
|
});
|
|
3056
|
-
const { serverOnlyImportBoundary } = await import("./server-boundary-
|
|
3056
|
+
const { serverOnlyImportBoundary } = await import("./server-boundary-WUKORSW2.js");
|
|
3057
3057
|
const serverBoundaryPlugin = serverOnlyImportBoundary({
|
|
3058
3058
|
projectRoot,
|
|
3059
3059
|
serverDir: serverDirAbs
|
|
@@ -3237,4 +3237,4 @@ export {
|
|
|
3237
3237
|
theoPluginAsync,
|
|
3238
3238
|
theoPlugin
|
|
3239
3239
|
};
|
|
3240
|
-
//# sourceMappingURL=chunk-
|
|
3240
|
+
//# sourceMappingURL=chunk-BZ6DFSKG.js.map
|
|
@@ -390,6 +390,269 @@ function describeDeployedSecurityHeaders(limits) {
|
|
|
390
390
|
return lines.join("\n");
|
|
391
391
|
}
|
|
392
392
|
|
|
393
|
+
// src/adapters/deployed-rate-limit.ts
|
|
394
|
+
var UnserialisableRateLimitError = class extends Error {
|
|
395
|
+
constructor(target, reason, ways) {
|
|
396
|
+
super(
|
|
397
|
+
[
|
|
398
|
+
`Refusing to build for \`${target}\`: theo.config.ts declares a rate limit this target cannot carry.`,
|
|
399
|
+
``,
|
|
400
|
+
` ${reason}`,
|
|
401
|
+
``,
|
|
402
|
+
` Ways forward:`,
|
|
403
|
+
...ways.map((w) => ` \u2022 ${w}`),
|
|
404
|
+
``,
|
|
405
|
+
` This refuses rather than dropping the key, because a rate limit that silently does not`,
|
|
406
|
+
` apply looks exactly like one that does (usetheokit/theokit#461, #508).`
|
|
407
|
+
].join("\n")
|
|
408
|
+
);
|
|
409
|
+
this.target = target;
|
|
410
|
+
this.reason = reason;
|
|
411
|
+
}
|
|
412
|
+
target;
|
|
413
|
+
reason;
|
|
414
|
+
name = "UnserialisableRateLimitError";
|
|
415
|
+
};
|
|
416
|
+
var IDENTIFIER = /^[A-Za-z_$][\w$]*$/;
|
|
417
|
+
var NOT_BINDABLE = /* @__PURE__ */ new Set([
|
|
418
|
+
"await",
|
|
419
|
+
"break",
|
|
420
|
+
"case",
|
|
421
|
+
"catch",
|
|
422
|
+
"class",
|
|
423
|
+
"const",
|
|
424
|
+
"continue",
|
|
425
|
+
"debugger",
|
|
426
|
+
"default",
|
|
427
|
+
"delete",
|
|
428
|
+
"do",
|
|
429
|
+
"else",
|
|
430
|
+
"enum",
|
|
431
|
+
"export",
|
|
432
|
+
"extends",
|
|
433
|
+
"false",
|
|
434
|
+
"finally",
|
|
435
|
+
"for",
|
|
436
|
+
"function",
|
|
437
|
+
"if",
|
|
438
|
+
"import",
|
|
439
|
+
"in",
|
|
440
|
+
"instanceof",
|
|
441
|
+
"new",
|
|
442
|
+
"null",
|
|
443
|
+
"return",
|
|
444
|
+
"super",
|
|
445
|
+
"switch",
|
|
446
|
+
"this",
|
|
447
|
+
"throw",
|
|
448
|
+
"true",
|
|
449
|
+
"try",
|
|
450
|
+
"typeof",
|
|
451
|
+
"var",
|
|
452
|
+
"void",
|
|
453
|
+
"while",
|
|
454
|
+
"with",
|
|
455
|
+
"yield"
|
|
456
|
+
]);
|
|
457
|
+
function assertBakeableStore(store, target) {
|
|
458
|
+
if (store === void 0) return;
|
|
459
|
+
if (!isStoreShaped(store)) {
|
|
460
|
+
throw new UnserialisableRateLimitError(
|
|
461
|
+
target,
|
|
462
|
+
"`security.rateLimit.store` must be an object naming a module and a factory.",
|
|
463
|
+
[
|
|
464
|
+
"declare `store: { module: '@upstash/redis', factory: 'Redis' }`",
|
|
465
|
+
"omit `store` and let the build refuse this target, which is honest about not limiting"
|
|
466
|
+
]
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
const { module: mod, factory } = store;
|
|
470
|
+
if (typeof mod !== "string" || mod.length === 0) {
|
|
471
|
+
throw new UnserialisableRateLimitError(
|
|
472
|
+
target,
|
|
473
|
+
"`security.rateLimit.store.module` must be the specifier the entry imports from.",
|
|
474
|
+
["declare `module: '@upstash/redis'`", "omit `store`"]
|
|
475
|
+
);
|
|
476
|
+
}
|
|
477
|
+
if (typeof factory !== "string" || !IDENTIFIER.test(factory) || NOT_BINDABLE.has(factory)) {
|
|
478
|
+
throw new UnserialisableRateLimitError(
|
|
479
|
+
target,
|
|
480
|
+
"`security.rateLimit.store.factory` must be a plain identifier \u2014 it is written into the generated entry as `import { <factory> }`, where a quoted string would not parse, so it is validated rather than escaped. Anything else would put arbitrary source in every deployment this config builds.",
|
|
481
|
+
[
|
|
482
|
+
"name the export directly: `factory: 'Redis'`",
|
|
483
|
+
"omit `store` and let the build refuse this target"
|
|
484
|
+
]
|
|
485
|
+
);
|
|
486
|
+
}
|
|
487
|
+
const { options } = store;
|
|
488
|
+
if (options !== void 0) {
|
|
489
|
+
if (typeof options !== "object" || options === null || Array.isArray(options)) {
|
|
490
|
+
throw new UnserialisableRateLimitError(
|
|
491
|
+
target,
|
|
492
|
+
"`security.rateLimit.store.options` must be an object of scalar values.",
|
|
493
|
+
[
|
|
494
|
+
"declare `options: { url: 'redis://\u2026', retries: 3 }`",
|
|
495
|
+
"omit `options` and configure the store inside the factory"
|
|
496
|
+
]
|
|
497
|
+
);
|
|
498
|
+
}
|
|
499
|
+
const offending = Object.entries(options).find(
|
|
500
|
+
([, v]) => typeof v !== "string" && typeof v !== "number" && typeof v !== "boolean"
|
|
501
|
+
);
|
|
502
|
+
if (offending !== void 0) {
|
|
503
|
+
throw new UnserialisableRateLimitError(
|
|
504
|
+
target,
|
|
505
|
+
`\`security.rateLimit.store.options.${offending[0]}\` must be a string, number or boolean.`,
|
|
506
|
+
[
|
|
507
|
+
"declare scalars only \u2014 strings, numbers and booleans",
|
|
508
|
+
"move anything else into the factory itself, which the generated entry calls at runtime"
|
|
509
|
+
]
|
|
510
|
+
);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
function isStoreShaped(store) {
|
|
515
|
+
return typeof store === "object" && store !== null && !Array.isArray(store);
|
|
516
|
+
}
|
|
517
|
+
function bakeableRateLimit(rateLimit, target) {
|
|
518
|
+
if (rateLimit === void 0) return void 0;
|
|
519
|
+
const cfg = rateLimit;
|
|
520
|
+
if (typeof cfg.keyBy === "function") {
|
|
521
|
+
throw new UnserialisableRateLimitError(
|
|
522
|
+
target,
|
|
523
|
+
"`security.rateLimit.keyBy` is a function, and a deployed entry has no literal for a closure.",
|
|
524
|
+
[
|
|
525
|
+
"use `keyBy: 'ip'` (the default), which this target resolves from the connection",
|
|
526
|
+
"build for `node` and run `theokit start`, which can call the function"
|
|
527
|
+
]
|
|
528
|
+
);
|
|
529
|
+
}
|
|
530
|
+
if (cfg.keyBy === "session" || cfg.keyBy === "user") {
|
|
531
|
+
throw new UnserialisableRateLimitError(
|
|
532
|
+
target,
|
|
533
|
+
`\`security.rateLimit.keyBy: '${cfg.keyBy}'\` needs a resolved session, and the limit runs before routing.`,
|
|
534
|
+
[
|
|
535
|
+
"use `keyBy: 'ip'`, which is resolvable at that point",
|
|
536
|
+
"build for `node` and run `theokit start`"
|
|
537
|
+
]
|
|
538
|
+
);
|
|
539
|
+
}
|
|
540
|
+
assertBakeableStore(cfg.store, target);
|
|
541
|
+
if (cfg.routes !== void 0) {
|
|
542
|
+
throw new UnserialisableRateLimitError(
|
|
543
|
+
target,
|
|
544
|
+
"`security.rateLimit.routes` needs the matched route, which is decided after the limit runs here.",
|
|
545
|
+
[
|
|
546
|
+
"declare a single global limit (`windowMs` + `max`)",
|
|
547
|
+
"build for `node` and run `theokit start`, which applies per-route limits"
|
|
548
|
+
]
|
|
549
|
+
);
|
|
550
|
+
}
|
|
551
|
+
const windowMs = cfg.windowMs;
|
|
552
|
+
const max = cfg.max;
|
|
553
|
+
if (typeof windowMs !== "number" || typeof max !== "number") return void 0;
|
|
554
|
+
const trustProxy = cfg.trustProxy;
|
|
555
|
+
return {
|
|
556
|
+
windowMs,
|
|
557
|
+
max,
|
|
558
|
+
trustProxy: typeof trustProxy === "boolean" || typeof trustProxy === "number" ? trustProxy : false,
|
|
559
|
+
// Validated above — `module` and `options` are escaped at emit time, `factory` was checked
|
|
560
|
+
// against IDENTIFIER because it cannot be.
|
|
561
|
+
store: cfg.store
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
function deployedRateLimitFragment(rateLimit, target, addressExpression, params = "request, server", importPrefix = "") {
|
|
565
|
+
const baked = bakeableRateLimit(rateLimit, target);
|
|
566
|
+
if (baked === void 0) return [];
|
|
567
|
+
return [
|
|
568
|
+
`// #508 \u2014 the limit the app declared, carried as a literal because a deployed entry has no`,
|
|
569
|
+
`// theo.config.ts to read.`,
|
|
570
|
+
`//`,
|
|
571
|
+
`// WHERE THIS COUNTER LIVES, and it is not the same answer per target. On a long-lived server`,
|
|
572
|
+
`// (\`bun\`) the process outlives a request and the count holds. On a per-invocation or`,
|
|
573
|
+
`// per-isolate runtime \u2014 Cloudflare, AWS Lambda, Netlify, Vercel, Deno Deploy \u2014 it does not:`,
|
|
574
|
+
`// the limit is PER INSTANCE, and a caller spread across instances gets that many budgets.`,
|
|
575
|
+
`// The address is resolved correctly either way; the counting is what B-257 is about, and`,
|
|
576
|
+
`// \`theokit build\` refuses a declared limit on those five until it is.`,
|
|
577
|
+
// B-262 — ONE builder, one signature, always async. The emitter used to branch here:
|
|
578
|
+
// `createRateLimiterWeb` returns a value and `createDurableRateLimiterWeb` returns a Promise, so
|
|
579
|
+
// the CALL SITE had to know which it got and emit `await` or not. A call site whose shape changes
|
|
580
|
+
// with the config is what produced B-257's missing `await`, and `buildRateLimiter` removes the
|
|
581
|
+
// decision rather than documenting it.
|
|
582
|
+
`import { buildRateLimiter } from '${importPrefix}theokit/server/rate-limit'`,
|
|
583
|
+
...baked.store === void 0 ? [
|
|
584
|
+
`const RATE_LIMIT = buildRateLimiter({ windowMs: ${baked.windowMs}, max: ${baked.max} }, undefined)`
|
|
585
|
+
] : [
|
|
586
|
+
// B-257 — a durable counter, named by the app. `module` and the options are JSON literals;
|
|
587
|
+
// `factory` was validated against IDENTIFIER at bake time because it lands here as a bare
|
|
588
|
+
// identifier and no escape can make that safe.
|
|
589
|
+
//
|
|
590
|
+
// The limiter's own import is emitted HERE rather than by each adapter. See `importPrefix`.
|
|
591
|
+
`import { ${baked.store.factory} } from ${JSON.stringify(baked.store.module)}`,
|
|
592
|
+
`const RATE_LIMIT_STORE = new ${baked.store.factory}(${JSON.stringify(baked.store.options ?? {})})`,
|
|
593
|
+
`const RATE_LIMIT = buildRateLimiter(`,
|
|
594
|
+
` { windowMs: ${baked.windowMs}, max: ${baked.max} },`,
|
|
595
|
+
` RATE_LIMIT_STORE,`,
|
|
596
|
+
`)`
|
|
597
|
+
],
|
|
598
|
+
``,
|
|
599
|
+
`// How many proxies the deployment declared in front of it. \`client-ip.ts\` reads a forwarded`,
|
|
600
|
+
`// header only when this says one wrote it: the header is whatever the client typed, so`,
|
|
601
|
+
`// trusting it unasked lets anyone rotate a forged value past the limiter with one \`curl -H\`.`,
|
|
602
|
+
`const TRUST_PROXY = ${JSON.stringify(baked.trustProxy)}`,
|
|
603
|
+
``,
|
|
604
|
+
`/**`,
|
|
605
|
+
` * The caller's address, from the connection rather than from a header.`,
|
|
606
|
+
` *`,
|
|
607
|
+
` * A header a client can set is a key a client can choose, which makes the bucket theirs to`,
|
|
608
|
+
` * split. An address this runtime cannot resolve returns \`undefined\`, and the caller answers`,
|
|
609
|
+
` * 503 rather than keying on a constant: one shared bucket is a budget the first caller each`,
|
|
610
|
+
` * window exhausts for everyone, which is worse than no limiting at all.`,
|
|
611
|
+
` */`,
|
|
612
|
+
`const addr = (v) => (typeof v === 'string' && v.trim() !== '' ? v.trim() : undefined)`,
|
|
613
|
+
`function callerAddress(${params}) {`,
|
|
614
|
+
` return addr(${addressExpression})`,
|
|
615
|
+
`}`,
|
|
616
|
+
``,
|
|
617
|
+
`/** The 503 a caller this runtime cannot name gets, instead of everyone's shared bucket. */`,
|
|
618
|
+
`function unnamedCaller() {`,
|
|
619
|
+
` return new Response(`,
|
|
620
|
+
` JSON.stringify({ error: { code: 'CALLER_UNRESOLVED', message: ${JSON.stringify(
|
|
621
|
+
`${target} could not resolve the caller's address, and a rate limit keyed on a constant is a denial of service`
|
|
622
|
+
)} } }),`,
|
|
623
|
+
` { status: 503, headers: { 'Content-Type': 'application/json' } },`,
|
|
624
|
+
` )`,
|
|
625
|
+
`}`,
|
|
626
|
+
``,
|
|
627
|
+
`/** The 429 a limited caller gets, carrying the limiter's own headers. */`,
|
|
628
|
+
`function rateLimited(result) {`,
|
|
629
|
+
` return new Response(JSON.stringify({ error: { code: 'RATE_LIMITED', message: 'Too many requests' } }), {`,
|
|
630
|
+
` status: 429,`,
|
|
631
|
+
` headers: { 'Content-Type': 'application/json', ...result.headers },`,
|
|
632
|
+
` })`,
|
|
633
|
+
`}`
|
|
634
|
+
];
|
|
635
|
+
}
|
|
636
|
+
function rateLimitCheckFragment(rateLimit, indent, args = "request, server", refuse = "return withCors(request, withSecurityHeaders(rateLimited(limit), SECURITY_HEADERS))", refuseUnnamed = "return withCors(request, withSecurityHeaders(unnamedCaller(), SECURITY_HEADERS))") {
|
|
637
|
+
if (rateLimit === void 0) return [];
|
|
638
|
+
return [
|
|
639
|
+
`${indent}const caller = callerAddress(${args})`,
|
|
640
|
+
`${indent}if (caller === undefined) {`,
|
|
641
|
+
`${indent} ${refuseUnnamed}`,
|
|
642
|
+
`${indent}}`,
|
|
643
|
+
// B-257 — the call side of the pair. `deployedRateLimitFragment` emits the DECLARATION; this
|
|
644
|
+
// emits the CALL, and the two branch on the same fact or the entry names a symbol it never
|
|
645
|
+
// declared. A durable limiter returns a promise; the sync facade does not.
|
|
646
|
+
// B-262 — `await` unconditionally. `buildRateLimiter` is always async, so this line no longer
|
|
647
|
+
// asks what the config declared. The conditional it replaces is where B-257's defect lived:
|
|
648
|
+
// a Promise read for `limited` is always `undefined`, always falsy, and every request passes.
|
|
649
|
+
`${indent}const limit = await RATE_LIMIT(caller)`,
|
|
650
|
+
`${indent}if (limit.limited) {`,
|
|
651
|
+
`${indent} ${refuse}`,
|
|
652
|
+
`${indent}}`
|
|
653
|
+
];
|
|
654
|
+
}
|
|
655
|
+
|
|
393
656
|
// src/adapters/deployed-runtime-config.ts
|
|
394
657
|
function serverDirLiteral(opts) {
|
|
395
658
|
return JSON.stringify(opts.serverDir ?? "server");
|
|
@@ -457,9 +720,11 @@ export {
|
|
|
457
720
|
deployedCsrfFragment,
|
|
458
721
|
securityHeadersDeclarations,
|
|
459
722
|
describeDeployedSecurityHeaders,
|
|
723
|
+
deployedRateLimitFragment,
|
|
724
|
+
rateLimitCheckFragment,
|
|
460
725
|
serverDirLiteral,
|
|
461
726
|
agentsDirLiteral,
|
|
462
727
|
deployedRuntimeConfigFragment,
|
|
463
728
|
deployedTraceFragment
|
|
464
729
|
};
|
|
465
|
-
//# sourceMappingURL=chunk-
|
|
730
|
+
//# sourceMappingURL=chunk-C5CAL3DX.js.map
|