@farm.js/core 0.1.0-beta.97 → 0.1.0-beta.99

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.
Files changed (269) hide show
  1. package/dist/after.cjs.map +1 -0
  2. package/dist/agent-runtime.cjs +7 -3
  3. package/dist/agent-runtime.cjs.map +1 -0
  4. package/dist/agent-runtime.d.mts +2 -2
  5. package/dist/agent-runtime.d.ts +2 -2
  6. package/dist/agent-runtime.mjs +2 -1
  7. package/dist/agent-runtime.mjs.map +1 -1
  8. package/dist/api/route-manager.cjs.map +1 -0
  9. package/dist/api/route-manager.d.mts +2 -2
  10. package/dist/api/route-manager.d.ts +2 -2
  11. package/dist/api/runtime.cjs.map +1 -0
  12. package/dist/api.cjs +26 -1
  13. package/dist/api.cjs.map +1 -0
  14. package/dist/api.d.mts +3 -3
  15. package/dist/api.d.ts +3 -3
  16. package/dist/api.mjs +2 -2
  17. package/dist/{app-CIOx3LvY.d.ts → app-CGjrKNoN.d.ts} +4 -4
  18. package/dist/{app-DBd6437H.d.mts → app-DOcLs_iq.d.mts} +4 -4
  19. package/dist/app-markdown.cjs +21 -0
  20. package/dist/app-markdown.cjs.map +1 -0
  21. package/dist/app-markdown.d.mts +19 -4
  22. package/dist/app-markdown.d.ts +19 -4
  23. package/dist/app-markdown.mjs +7 -1
  24. package/dist/cache.cjs.map +1 -0
  25. package/dist/{chunk-VMCHLH3Y.mjs → chunk-26BCDVJS.mjs} +19 -1
  26. package/dist/chunk-26BCDVJS.mjs.map +1 -0
  27. package/dist/{chunk-FP5Q67FO.mjs → chunk-2HYYZCCI.mjs} +308 -19
  28. package/dist/chunk-2HYYZCCI.mjs.map +1 -0
  29. package/dist/{chunk-WKDAA6V2.mjs → chunk-3BTAMIQE.mjs} +100 -41
  30. package/dist/chunk-3BTAMIQE.mjs.map +1 -0
  31. package/dist/{chunk-WD2EBM3I.mjs → chunk-3CQJV3NC.mjs} +2 -2
  32. package/dist/{chunk-I3OOUWO5.mjs → chunk-3GDRV7SG.mjs} +2 -2
  33. package/dist/{chunk-YIBV77KZ.mjs → chunk-4WLAVHCY.mjs} +2 -2
  34. package/dist/{chunk-KDP5ZSWP.mjs → chunk-7PS5NKOM.mjs} +26 -11
  35. package/dist/chunk-7PS5NKOM.mjs.map +1 -0
  36. package/dist/{chunk-X4SFHKFH.mjs → chunk-BU65A3LZ.mjs} +10 -6
  37. package/dist/chunk-BU65A3LZ.mjs.map +1 -0
  38. package/dist/{chunk-WMJGA32P.mjs → chunk-BXVOHG6B.mjs} +27 -2
  39. package/dist/{chunk-WMJGA32P.mjs.map → chunk-BXVOHG6B.mjs.map} +1 -1
  40. package/dist/{chunk-BZAIS65C.mjs → chunk-DIHNOVZZ.mjs} +40 -13
  41. package/dist/chunk-DIHNOVZZ.mjs.map +1 -0
  42. package/dist/{chunk-ISSFLGJ5.mjs → chunk-FDWECN3W.mjs} +8 -4
  43. package/dist/chunk-FDWECN3W.mjs.map +1 -0
  44. package/dist/chunk-FDWXXTWR.mjs +114 -0
  45. package/dist/chunk-FDWXXTWR.mjs.map +1 -0
  46. package/dist/{chunk-FSIXEIOB.mjs → chunk-L7FSTPGS.mjs} +2 -2
  47. package/dist/{chunk-J3MPQ4MQ.mjs → chunk-LG4XB6RN.mjs} +2 -2
  48. package/dist/{chunk-J3MPQ4MQ.mjs.map → chunk-LG4XB6RN.mjs.map} +1 -1
  49. package/dist/{chunk-AAC66ILR.mjs → chunk-LXXG3K2Y.mjs} +61 -2
  50. package/dist/chunk-LXXG3K2Y.mjs.map +1 -0
  51. package/dist/{chunk-GJ7W6LNK.mjs → chunk-N5RK4LK6.mjs} +3 -3
  52. package/dist/{chunk-D7F7YZ7L.mjs → chunk-NCVFPT67.mjs} +100 -40
  53. package/dist/{chunk-D7F7YZ7L.mjs.map → chunk-NCVFPT67.mjs.map} +1 -1
  54. package/dist/{chunk-UMUWR5XA.mjs → chunk-PURU7FXR.mjs} +2 -2
  55. package/dist/{chunk-2SKCXYLB.mjs → chunk-QQCCBMTH.mjs} +60 -6
  56. package/dist/chunk-QQCCBMTH.mjs.map +1 -0
  57. package/dist/chunk-RKUPOYQV.mjs +28 -0
  58. package/dist/chunk-RKUPOYQV.mjs.map +1 -0
  59. package/dist/{chunk-52BR7LTX.mjs → chunk-STSPSJUI.mjs} +3 -3
  60. package/dist/{chunk-EPDIF432.mjs → chunk-UOGX6UNO.mjs} +37 -17
  61. package/dist/chunk-UOGX6UNO.mjs.map +1 -0
  62. package/dist/{chunk-IZBX2TEU.mjs → chunk-VQG3QWWK.mjs} +2 -2
  63. package/dist/{chunk-35NPLKQJ.mjs → chunk-XWM2C267.mjs} +21 -105
  64. package/dist/chunk-XWM2C267.mjs.map +1 -0
  65. package/dist/{chunk-7ZVN3YZI.mjs → chunk-Z5ARESQG.mjs} +43 -1
  66. package/dist/chunk-Z5ARESQG.mjs.map +1 -0
  67. package/dist/client/lifecycle.cjs.map +1 -0
  68. package/dist/client/lifecycle.mjs +2 -2
  69. package/dist/{client-plugins-B6-07YmP.d.mts → client-plugins-BqtHoJ9C.d.mts} +91 -3
  70. package/dist/{client-plugins-By6MVUX-.d.ts → client-plugins-DYprkCKN.d.ts} +91 -3
  71. package/dist/client-plugins.cjs +1 -1
  72. package/dist/client-plugins.cjs.map +1 -0
  73. package/dist/client-plugins.d.mts +2 -2
  74. package/dist/client-plugins.d.ts +2 -2
  75. package/dist/client-plugins.mjs +2 -2
  76. package/dist/client.cjs +61 -16
  77. package/dist/client.cjs.map +1 -0
  78. package/dist/client.d.mts +5 -5
  79. package/dist/client.d.ts +5 -5
  80. package/dist/client.mjs +7 -7
  81. package/dist/config.cjs.map +1 -0
  82. package/dist/config.d.mts +2 -2
  83. package/dist/config.d.ts +2 -2
  84. package/dist/cron.cjs +48 -12
  85. package/dist/cron.cjs.map +1 -0
  86. package/dist/cron.mjs +2 -1
  87. package/dist/deferred.cjs.map +1 -0
  88. package/dist/deployment.cjs.map +1 -0
  89. package/dist/{devtools-XWZX6CCL.mjs → devtools-24N3U5IG.mjs} +9 -7
  90. package/dist/{devtools-XWZX6CCL.mjs.map → devtools-24N3U5IG.mjs.map} +1 -1
  91. package/dist/docs.cjs +7 -3
  92. package/dist/docs.cjs.map +1 -0
  93. package/dist/docs.mjs +9 -7
  94. package/dist/env-types.cjs.map +1 -0
  95. package/dist/env.cjs.map +1 -0
  96. package/dist/environment/vite.cjs.map +1 -0
  97. package/dist/environment.cjs.map +1 -0
  98. package/dist/font.cjs.map +1 -0
  99. package/dist/headers.cjs.map +1 -0
  100. package/dist/i18n/client.cjs.map +1 -0
  101. package/dist/i18n/index.cjs.map +1 -0
  102. package/dist/i18n/server.cjs.map +1 -0
  103. package/dist/image/server.cjs +99 -40
  104. package/dist/image/server.cjs.map +1 -0
  105. package/dist/image/server.d.mts +6 -0
  106. package/dist/image/server.d.ts +6 -0
  107. package/dist/image/server.mjs +1 -1
  108. package/dist/image/sharp.cjs +1 -0
  109. package/dist/image/sharp.cjs.map +1 -0
  110. package/dist/image/sharp.d.mts +1 -1
  111. package/dist/image/sharp.d.ts +1 -1
  112. package/dist/image/sharp.mjs +1 -1
  113. package/dist/image/sharp.mjs.map +1 -1
  114. package/dist/image.cjs.map +1 -0
  115. package/dist/index.cjs +943 -231
  116. package/dist/index.cjs.map +1 -0
  117. package/dist/index.d.mts +11 -11
  118. package/dist/index.d.ts +11 -11
  119. package/dist/index.mjs +38 -24
  120. package/dist/index.mjs.map +1 -1
  121. package/dist/instrumentation.cjs.map +1 -0
  122. package/dist/{integration-orm-LLW6WZNK.mjs → integration-orm-G5BPBT5X.mjs} +3 -3
  123. package/dist/integrations.cjs +168 -5
  124. package/dist/integrations.cjs.map +1 -0
  125. package/dist/integrations.d.mts +2 -2
  126. package/dist/integrations.d.ts +2 -2
  127. package/dist/integrations.mjs +10 -3
  128. package/dist/internal/build-runtime.cjs +745 -139
  129. package/dist/internal/build-runtime.cjs.map +1 -0
  130. package/dist/internal/build-runtime.d.mts +4 -4
  131. package/dist/internal/build-runtime.d.ts +4 -4
  132. package/dist/internal/build-runtime.mjs +16 -14
  133. package/dist/internal/client-runtime.cjs +7 -2
  134. package/dist/internal/client-runtime.cjs.map +1 -0
  135. package/dist/internal/client-runtime.mjs +8 -3
  136. package/dist/internal/client-runtime.mjs.map +1 -1
  137. package/dist/internal/config-runtime.cjs +59 -42
  138. package/dist/internal/config-runtime.cjs.map +1 -0
  139. package/dist/internal/config-runtime.d.mts +3 -3
  140. package/dist/internal/config-runtime.d.ts +3 -3
  141. package/dist/internal/config-runtime.mjs +8 -6
  142. package/dist/internal/isolated-boundary.cjs.map +1 -0
  143. package/dist/internal/metadata-image-runtime.cjs.map +1 -0
  144. package/dist/internal/product-telemetry-runtime.cjs +52 -5
  145. package/dist/internal/product-telemetry-runtime.cjs.map +1 -0
  146. package/dist/internal/product-telemetry-runtime.d.mts +12 -1
  147. package/dist/internal/product-telemetry-runtime.d.ts +12 -1
  148. package/dist/internal/product-telemetry-runtime.mjs +50 -5
  149. package/dist/internal/product-telemetry-runtime.mjs.map +1 -1
  150. package/dist/internal/production-node-env.cjs.map +1 -0
  151. package/dist/internal/production-runtime.cjs +56 -10
  152. package/dist/internal/production-runtime.cjs.map +1 -0
  153. package/dist/internal/production-runtime.d.mts +8 -8
  154. package/dist/internal/production-runtime.d.ts +8 -8
  155. package/dist/internal/production-runtime.mjs +2 -2
  156. package/dist/markdown.cjs.map +1 -0
  157. package/dist/{metadata-route-8_V3UonT.d.ts → metadata-route-D2be_0kC.d.ts} +17 -2
  158. package/dist/{metadata-route-CWP_HjGU.d.mts → metadata-route-Dw5CrDIZ.d.mts} +17 -2
  159. package/dist/middleware.cjs.map +1 -0
  160. package/dist/middleware.d.mts +4 -4
  161. package/dist/middleware.d.ts +4 -4
  162. package/dist/navigation.cjs.map +1 -0
  163. package/dist/{nitro-EGRNOB5P.mjs → nitro-YJEP3FWM.mjs} +4 -3
  164. package/dist/{nitro-EGRNOB5P.mjs.map → nitro-YJEP3FWM.mjs.map} +1 -1
  165. package/dist/observability.cjs.map +1 -0
  166. package/dist/plugin.cjs +599 -104
  167. package/dist/plugin.cjs.map +1 -0
  168. package/dist/plugin.d.mts +2 -2
  169. package/dist/plugin.d.ts +2 -2
  170. package/dist/plugin.mjs +13 -11
  171. package/dist/{production-runtime-CdmzsDax.d.ts → production-runtime-CTqpD1C8.d.ts} +2 -2
  172. package/dist/{production-runtime-BGRH1dKG.d.mts → production-runtime-HNqNZd4D.d.mts} +2 -2
  173. package/dist/query/client.cjs.map +1 -0
  174. package/dist/query/index.cjs.map +1 -0
  175. package/dist/query/parsers.cjs.map +1 -0
  176. package/dist/query/server.cjs.map +1 -0
  177. package/dist/{render-plan-DLxJYAA5.d.mts → render-plan-COscpW-i.d.mts} +1 -1
  178. package/dist/{render-plan-0GeHiGVZ.d.ts → render-plan-Z4kSxaT_.d.ts} +1 -1
  179. package/dist/renderer/react/client.cjs.map +1 -0
  180. package/dist/renderer/react/server.cjs.map +1 -0
  181. package/dist/renderer/react/vite.cjs.map +1 -0
  182. package/dist/renderer-client.cjs +26 -1
  183. package/dist/renderer-client.cjs.map +1 -0
  184. package/dist/renderer-client.d.mts +4 -4
  185. package/dist/renderer-client.d.ts +4 -4
  186. package/dist/renderer-client.mjs +2 -2
  187. package/dist/renderer.cjs.map +1 -0
  188. package/dist/{request-BstWbZ6O.d.ts → request-BEDb2jE9.d.ts} +1 -1
  189. package/dist/{request-BCNXSXLh.d.mts → request-CbIAqJY9.d.mts} +1 -1
  190. package/dist/request.cjs.map +1 -0
  191. package/dist/request.d.mts +3 -3
  192. package/dist/request.d.ts +3 -3
  193. package/dist/router.cjs.map +1 -0
  194. package/dist/routes.cjs.map +1 -0
  195. package/dist/routes.d.mts +2 -2
  196. package/dist/routes.d.ts +2 -2
  197. package/dist/server-action-security.cjs +117 -104
  198. package/dist/server-action-security.cjs.map +1 -0
  199. package/dist/server-action-security.mjs +2 -1
  200. package/dist/server-fn-client.cjs.map +1 -0
  201. package/dist/server-fn.cjs.map +1 -0
  202. package/dist/server-fn.mjs +3 -2
  203. package/dist/server-plugins.cjs.map +1 -0
  204. package/dist/server-plugins.d.mts +3 -3
  205. package/dist/server-plugins.d.ts +3 -3
  206. package/dist/server-query-client.cjs +26 -1
  207. package/dist/server-query-client.cjs.map +1 -0
  208. package/dist/server-query-client.mjs +3 -3
  209. package/dist/server-query.cjs.map +1 -0
  210. package/dist/server-query.mjs +4 -3
  211. package/dist/server.cjs +686 -170
  212. package/dist/server.cjs.map +1 -0
  213. package/dist/server.d.mts +8 -8
  214. package/dist/server.d.ts +8 -8
  215. package/dist/server.mjs +16 -14
  216. package/dist/server.mjs.map +1 -1
  217. package/dist/{spa-router-COgu4txb.d.ts → spa-router-CjeQites.d.ts} +1 -1
  218. package/dist/{spa-router-BeyAVSc6.d.mts → spa-router-FJbNou1a.d.mts} +1 -1
  219. package/dist/storage.cjs +7 -3
  220. package/dist/storage.cjs.map +1 -0
  221. package/dist/storage.mjs +1 -1
  222. package/dist/testing.cjs.map +1 -0
  223. package/dist/testing.d.mts +2 -2
  224. package/dist/testing.d.ts +2 -2
  225. package/dist/testing.mjs +2 -1
  226. package/dist/testing.mjs.map +1 -1
  227. package/dist/theme/client.cjs.map +1 -0
  228. package/dist/theme/index.cjs.map +1 -0
  229. package/dist/theme/runtime.cjs.map +1 -0
  230. package/dist/theme/server.cjs.map +1 -0
  231. package/dist/{utils-DL2VwDQl.d.mts → utils-BiMJv9Ys.d.mts} +1 -1
  232. package/dist/{utils-DkU2c_1B.d.ts → utils-BpnKRFv9.d.ts} +1 -1
  233. package/dist/version.cjs +1 -1
  234. package/dist/version.cjs.map +1 -0
  235. package/dist/version.mjs +1 -1
  236. package/dist/vite.cjs +599 -104
  237. package/dist/vite.cjs.map +1 -0
  238. package/dist/vite.d.mts +2 -2
  239. package/dist/vite.d.ts +2 -2
  240. package/dist/vite.mjs +13 -11
  241. package/dist/{workflows-ChyK_oTu.d.mts → workflows-BantWytQ.d.mts} +7 -0
  242. package/dist/{workflows-ChyK_oTu.d.ts → workflows-BantWytQ.d.ts} +7 -0
  243. package/dist/workflows.cjs +73 -5
  244. package/dist/workflows.cjs.map +1 -0
  245. package/dist/workflows.d.mts +1 -1
  246. package/dist/workflows.d.ts +1 -1
  247. package/dist/workflows.mjs +2 -1
  248. package/package.json +17 -17
  249. package/dist/chunk-2SKCXYLB.mjs.map +0 -1
  250. package/dist/chunk-35NPLKQJ.mjs.map +0 -1
  251. package/dist/chunk-7ZVN3YZI.mjs.map +0 -1
  252. package/dist/chunk-AAC66ILR.mjs.map +0 -1
  253. package/dist/chunk-BZAIS65C.mjs.map +0 -1
  254. package/dist/chunk-EPDIF432.mjs.map +0 -1
  255. package/dist/chunk-FP5Q67FO.mjs.map +0 -1
  256. package/dist/chunk-ISSFLGJ5.mjs.map +0 -1
  257. package/dist/chunk-KDP5ZSWP.mjs.map +0 -1
  258. package/dist/chunk-VMCHLH3Y.mjs.map +0 -1
  259. package/dist/chunk-WKDAA6V2.mjs.map +0 -1
  260. package/dist/chunk-X4SFHKFH.mjs.map +0 -1
  261. /package/dist/{chunk-WD2EBM3I.mjs.map → chunk-3CQJV3NC.mjs.map} +0 -0
  262. /package/dist/{chunk-I3OOUWO5.mjs.map → chunk-3GDRV7SG.mjs.map} +0 -0
  263. /package/dist/{chunk-YIBV77KZ.mjs.map → chunk-4WLAVHCY.mjs.map} +0 -0
  264. /package/dist/{chunk-FSIXEIOB.mjs.map → chunk-L7FSTPGS.mjs.map} +0 -0
  265. /package/dist/{chunk-GJ7W6LNK.mjs.map → chunk-N5RK4LK6.mjs.map} +0 -0
  266. /package/dist/{chunk-UMUWR5XA.mjs.map → chunk-PURU7FXR.mjs.map} +0 -0
  267. /package/dist/{chunk-52BR7LTX.mjs.map → chunk-STSPSJUI.mjs.map} +0 -0
  268. /package/dist/{chunk-IZBX2TEU.mjs.map → chunk-VQG3QWWK.mjs.map} +0 -0
  269. /package/dist/{integration-orm-LLW6WZNK.mjs.map → integration-orm-G5BPBT5X.mjs.map} +0 -0
@@ -98,6 +98,7 @@ var FarmImageRequestError = _FarmImageRequestError;
98
98
  function createFarmImageHandler(config, options) {
99
99
  const fetcher = options.fetch ?? globalThis.fetch;
100
100
  const cache = new FarmImageMemoryCache(options.cacheEntries ?? 100);
101
+ const inflight = /* @__PURE__ */ new Map();
101
102
  const allowedWidths = /* @__PURE__ */ new Set([...config.deviceSizes, ...config.imageSizes]);
102
103
  const allowedQualities = new Set(config.qualities);
103
104
  return /* @__PURE__ */ __name(async function handleFarmImage(request) {
@@ -126,50 +127,55 @@ function createFarmImageHandler(config, options) {
126
127
  "quality"
127
128
  );
128
129
  const accept = request.headers.get("accept") ?? "";
130
+ const negotiatedFormat = selectOutputFormat(accept, config.formats) ?? "";
129
131
  const cacheKey = `${sourceUrl.href}
130
132
  ${width}
131
133
  ${quality}
132
- ${accept}`;
134
+ ${negotiatedFormat}`;
133
135
  let optimized = cache.get(cacheKey);
134
136
  if (!optimized) {
135
- const fetchedSource = await fetchImageSource(
136
- sourceUrl,
137
- requestUrl.origin,
138
- config,
139
- fetcher,
140
- options.fetchRemote,
141
- options.validateRemoteUrl,
142
- request.signal
143
- );
144
- const source = await readResponseWithLimit(
145
- fetchedSource.response,
146
- config.maximumResponseBody
147
- );
148
- const sourceType = detectImageContentType(source);
149
- validateSourceType(sourceType, config);
150
- throwIfAborted(request.signal);
151
- const result = await options.transform({
152
- source,
153
- sourceUrl: fetchedSource.url,
154
- sourceType,
155
- width,
156
- quality,
157
- accept,
158
- formats: config.formats,
159
- signal: request.signal
137
+ optimized = await runCoalesced(inflight, cacheKey, request.signal, async (signal) => {
138
+ const fetchedSource = await fetchImageSource(
139
+ sourceUrl,
140
+ requestUrl.origin,
141
+ config,
142
+ fetcher,
143
+ options.fetchRemote,
144
+ options.validateRemoteUrl,
145
+ signal
146
+ );
147
+ const source = await readResponseWithLimit(
148
+ fetchedSource.response,
149
+ config.maximumResponseBody
150
+ );
151
+ const sourceType = detectImageContentType(source);
152
+ validateSourceType(sourceType, config);
153
+ throwIfAborted(signal);
154
+ const result = await options.transform({
155
+ source,
156
+ sourceUrl: fetchedSource.url,
157
+ sourceType,
158
+ width,
159
+ quality,
160
+ accept,
161
+ formats: config.formats,
162
+ signal,
163
+ maximumResponseBody: config.maximumResponseBody
164
+ });
165
+ throwIfAborted(signal);
166
+ validateTransformedResult(result, config);
167
+ const entry = {
168
+ ...result,
169
+ etag: createImageEtag(result.body),
170
+ cacheControl: `public, max-age=${config.minimumCacheTTL}, stale-while-revalidate=${Math.max(
171
+ config.minimumCacheTTL,
172
+ 60
173
+ )}`,
174
+ expiresAt: Date.now() + config.minimumCacheTTL * 1e3
175
+ };
176
+ cache.set(cacheKey, entry);
177
+ return entry;
160
178
  });
161
- throwIfAborted(request.signal);
162
- validateTransformedResult(result, config);
163
- optimized = {
164
- ...result,
165
- etag: createImageEtag(result.body),
166
- cacheControl: `public, max-age=${config.minimumCacheTTL}, stale-while-revalidate=${Math.max(
167
- config.minimumCacheTTL,
168
- 60
169
- )}`,
170
- expiresAt: Date.now() + config.minimumCacheTTL * 1e3
171
- };
172
- cache.set(cacheKey, optimized);
173
179
  }
174
180
  return createOptimizedImageResponse(request, optimized, config);
175
181
  } catch (error) {
@@ -184,11 +190,13 @@ ${accept}`;
184
190
  }, "handleFarmImage");
185
191
  }
186
192
  __name(createFarmImageHandler, "createFarmImageHandler");
193
+ var DEFAULT_IMAGE_TRANSFORM_BODY_LIMIT = 10 * 1024 * 1024;
187
194
  function createCloudflareImageTransformer(fetcher = globalThis.fetch) {
188
- return async ({ sourceUrl, width, quality, accept, formats, signal }) => {
195
+ return async ({ sourceUrl, width, quality, accept, formats, signal, maximumResponseBody }) => {
189
196
  const format = selectOutputFormat(accept, formats);
190
197
  const response = await fetcher(sourceUrl, {
191
198
  signal,
199
+ redirect: "manual",
192
200
  headers: { accept: "image/*" },
193
201
  cf: {
194
202
  image: {
@@ -199,6 +207,14 @@ function createCloudflareImageTransformer(fetcher = globalThis.fetch) {
199
207
  }
200
208
  }
201
209
  });
210
+ if (response.status >= 300 && response.status < 400) {
211
+ void cancelResponseBody(response);
212
+ throw new FarmImageRequestError(
213
+ "UNSUPPORTED_IMAGE",
214
+ 502,
215
+ "Source image redirected after validation"
216
+ );
217
+ }
202
218
  if (!response.ok) {
203
219
  throw new FarmImageRequestError(
204
220
  "UNSUPPORTED_IMAGE",
@@ -206,7 +222,10 @@ function createCloudflareImageTransformer(fetcher = globalThis.fetch) {
206
222
  "Cloudflare could not transform the source image"
207
223
  );
208
224
  }
209
- const body = new Uint8Array(await response.arrayBuffer());
225
+ const body = await readResponseWithLimit(
226
+ response,
227
+ maximumResponseBody ?? DEFAULT_IMAGE_TRANSFORM_BODY_LIMIT
228
+ );
210
229
  return {
211
230
  body,
212
231
  contentType: normalizeImageContentType(response.headers.get("content-type")) || detectImageContentType(body)
@@ -357,6 +376,46 @@ async function fetchImageSource(initialUrl, requestOrigin, config, fetcher, fetc
357
376
  }
358
377
  }
359
378
  __name(fetchImageSource, "fetchImageSource");
379
+ async function runCoalesced(inflight, key, requestSignal, run) {
380
+ let entry = inflight.get(key);
381
+ if (!entry) {
382
+ const controller = new AbortController();
383
+ const created = {
384
+ controller,
385
+ waiters: 0,
386
+ promise: void 0
387
+ };
388
+ created.promise = run(controller.signal).finally(() => {
389
+ if (inflight.get(key) === created) inflight.delete(key);
390
+ });
391
+ created.promise.catch(() => {
392
+ });
393
+ inflight.set(key, created);
394
+ entry = created;
395
+ }
396
+ const pending = entry;
397
+ pending.waiters += 1;
398
+ try {
399
+ return await raceRequestAbort(pending.promise, requestSignal);
400
+ } finally {
401
+ pending.waiters -= 1;
402
+ if (pending.waiters === 0 && inflight.get(key) === pending) {
403
+ inflight.delete(key);
404
+ pending.controller.abort();
405
+ }
406
+ }
407
+ }
408
+ __name(runCoalesced, "runCoalesced");
409
+ function raceRequestAbort(promise, signal) {
410
+ if (!signal) return promise;
411
+ if (signal.aborted) return Promise.reject(signal.reason ?? new Error("Aborted"));
412
+ return new Promise((resolve, reject) => {
413
+ const onAbort = /* @__PURE__ */ __name(() => reject(signal.reason ?? new Error("Aborted")), "onAbort");
414
+ signal.addEventListener("abort", onAbort, { once: true });
415
+ promise.then(resolve, reject).finally(() => signal.removeEventListener("abort", onAbort));
416
+ });
417
+ }
418
+ __name(raceRequestAbort, "raceRequestAbort");
360
419
  async function readResponseWithLimit(response, limit) {
361
420
  const contentLength = response.headers.get("content-length");
362
421
  if (contentLength && Number(contentLength) > limit) {
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/image-server.ts","../../src/server-http.ts"],"sourcesContent":["import type {\n FarmImageFormat,\n FarmImageLocalPattern,\n FarmImageRemotePattern,\n ResolvedFarmImageConfig,\n} from \"./image-config\";\nimport { matchesFarmIfNoneMatch } from \"./server-http\";\n\nexport interface FarmImageTransformInput {\n source: Uint8Array;\n sourceUrl: URL;\n sourceType: string;\n width: number;\n quality: number;\n accept: string;\n formats: readonly FarmImageFormat[];\n signal: AbortSignal;\n /**\n * Byte ceiling for any source the transformer fetches itself. Supplied by the\n * image handler; transformers that re-fetch the origin (Cloudflare) must\n * enforce it, since they bypass the handler's bounded read.\n */\n maximumResponseBody?: number;\n}\n\nexport interface FarmImageTransformResult {\n body: Uint8Array;\n contentType: string;\n}\n\nexport type FarmImageTransformer = (\n input: FarmImageTransformInput,\n) => Promise<FarmImageTransformResult>;\n\nexport interface CreateFarmImageHandlerOptions {\n fetch?: typeof globalThis.fetch;\n /** Node-only fetcher that validates the DNS result used for remote connections. @internal */\n fetchRemote?: typeof globalThis.fetch;\n transform: FarmImageTransformer;\n validateRemoteUrl?: (url: URL) => void | Promise<void>;\n onError?: (error: unknown, request: Request) => void;\n cacheEntries?: number;\n}\n\nexport type FarmImageHandler = (request: Request) => Promise<Response | null>;\n\ntype OptimizedImage = FarmImageTransformResult & {\n etag: string;\n cacheControl: string;\n expiresAt: number;\n};\n\ntype FarmImageRequestErrorCode =\n | \"BODY_TOO_LARGE\"\n | \"DISALLOWED_SOURCE\"\n | \"INVALID_METHOD\"\n | \"INVALID_PARAMETER\"\n | \"PRIVATE_SOURCE\"\n | \"TOO_MANY_REDIRECTS\"\n | \"UNSUPPORTED_IMAGE\";\n\nexport class FarmImageRequestError extends Error {\n readonly code: FarmImageRequestErrorCode;\n readonly status: number;\n\n constructor(code: FarmImageRequestErrorCode, status: number, message: string) {\n super(message);\n this.name = \"FarmImageRequestError\";\n this.code = code;\n this.status = status;\n }\n}\n\nexport function createFarmImageHandler(\n config: ResolvedFarmImageConfig,\n options: CreateFarmImageHandlerOptions,\n): FarmImageHandler {\n const fetcher = options.fetch ?? globalThis.fetch;\n const cache = new FarmImageMemoryCache(options.cacheEntries ?? 100);\n // Identical concurrent misses share one fetch + transform. Without this a\n // burst for an uncached image (a new page going live, a CDN cold start)\n // fetches the origin and runs the codec once per request.\n const inflight = new Map<string, InflightOptimization>();\n const allowedWidths = new Set([...config.deviceSizes, ...config.imageSizes]);\n const allowedQualities = new Set(config.qualities);\n\n return async function handleFarmImage(request): Promise<Response | null> {\n const requestUrl = new URL(request.url);\n if (requestUrl.pathname !== config.path) return null;\n\n try {\n if (config.provider === \"none\") {\n throw new FarmImageRequestError(\n \"DISALLOWED_SOURCE\",\n 404,\n \"The Farm image optimizer is disabled\",\n );\n }\n if (request.method !== \"GET\" && request.method !== \"HEAD\") {\n throw new FarmImageRequestError(\n \"INVALID_METHOD\",\n 405,\n \"The Farm image optimizer only accepts GET and HEAD\",\n );\n }\n\n const sourceUrl = await resolveImageSourceUrl(requestUrl, config, options.validateRemoteUrl);\n const width = parseAllowedInteger(requestUrl.searchParams.get(\"w\"), allowedWidths, \"width\");\n const quality = parseAllowedInteger(\n requestUrl.searchParams.get(\"q\"),\n allowedQualities,\n \"quality\",\n );\n const accept = request.headers.get(\"accept\") ?? \"\";\n // Key on the format the Accept header negotiates to, not the header text.\n // Both transformers derive their output from `selectOutputFormat(accept,\n // formats)` alone, so every header that negotiates to the same format\n // produces byte-identical output. Keying on the raw header let a caller\n // vary it freely (`image/webp,*/*;q=0.8`, reordered lists, extra params)\n // and force an uncached fetch and transform each time.\n const negotiatedFormat = selectOutputFormat(accept, config.formats) ?? \"\";\n const cacheKey = `${sourceUrl.href}\\n${width}\\n${quality}\\n${negotiatedFormat}`;\n let optimized = cache.get(cacheKey);\n\n if (!optimized) {\n optimized = await runCoalesced(inflight, cacheKey, request.signal, async (signal) => {\n const fetchedSource = await fetchImageSource(\n sourceUrl,\n requestUrl.origin,\n config,\n fetcher,\n options.fetchRemote,\n options.validateRemoteUrl,\n signal,\n );\n const source = await readResponseWithLimit(\n fetchedSource.response,\n config.maximumResponseBody,\n );\n const sourceType = detectImageContentType(source);\n validateSourceType(sourceType, config);\n throwIfAborted(signal);\n\n const result = await options.transform({\n source,\n sourceUrl: fetchedSource.url,\n sourceType,\n width,\n quality,\n accept,\n formats: config.formats,\n signal,\n maximumResponseBody: config.maximumResponseBody,\n });\n throwIfAborted(signal);\n validateTransformedResult(result, config);\n\n const entry = {\n ...result,\n etag: createImageEtag(result.body),\n cacheControl: `public, max-age=${config.minimumCacheTTL}, stale-while-revalidate=${Math.max(\n config.minimumCacheTTL,\n 60,\n )}`,\n expiresAt: Date.now() + config.minimumCacheTTL * 1_000,\n };\n cache.set(cacheKey, entry);\n return entry;\n });\n }\n\n return createOptimizedImageResponse(request, optimized, config);\n } catch (error) {\n if (!(error instanceof FarmImageRequestError) && !isAbortError(error)) {\n try {\n options.onError?.(error, request);\n } catch {\n // Error reporting must not replace the optimizer's sanitized response.\n }\n }\n return createFarmImageErrorResponse(error);\n }\n };\n}\n\n/** Mirrors the `images.maximumResponseBody` default (\"10mb\"). */\nconst DEFAULT_IMAGE_TRANSFORM_BODY_LIMIT = 10 * 1024 * 1024;\n\nexport function createCloudflareImageTransformer(\n fetcher: typeof globalThis.fetch = globalThis.fetch,\n): FarmImageTransformer {\n return async ({ sourceUrl, width, quality, accept, formats, signal, maximumResponseBody }) => {\n const format = selectOutputFormat(accept, formats);\n // Cloudflare resizing works by letting the edge fetch the origin, so this\n // request cannot reuse the bytes the handler already read. It still must not\n // be a weaker fetch than the validated one: `redirect: \"manual\"` keeps it\n // from silently following a hop the handler never validated, and the body is\n // read under the same ceiling as the handler's own read.\n const response = await fetcher(sourceUrl, {\n signal,\n redirect: \"manual\",\n headers: { accept: \"image/*\" },\n cf: {\n image: {\n fit: \"scale-down\",\n width,\n quality,\n ...(format ? { format: format === \"image/avif\" ? \"avif\" : \"webp\" } : {}),\n },\n },\n } as RequestInit);\n\n if (response.status >= 300 && response.status < 400) {\n void cancelResponseBody(response);\n throw new FarmImageRequestError(\n \"UNSUPPORTED_IMAGE\",\n 502,\n \"Source image redirected after validation\",\n );\n }\n\n if (!response.ok) {\n throw new FarmImageRequestError(\n \"UNSUPPORTED_IMAGE\",\n response.status === 404 ? 404 : 502,\n \"Cloudflare could not transform the source image\",\n );\n }\n\n const body = await readResponseWithLimit(\n response,\n maximumResponseBody ?? DEFAULT_IMAGE_TRANSFORM_BODY_LIMIT,\n );\n return {\n body,\n contentType:\n normalizeImageContentType(response.headers.get(\"content-type\")) ||\n detectImageContentType(body),\n };\n };\n}\n\nexport function selectOutputFormat(\n accept: string,\n formats: readonly FarmImageFormat[],\n): FarmImageFormat | undefined {\n const qualityByFormat = new Map<string, number>();\n\n for (const range of accept.split(\",\")) {\n const [rawType, ...parameters] = range.split(\";\");\n const type = rawType.trim().toLowerCase();\n if (!type) continue;\n\n let quality = 1;\n for (const parameter of parameters) {\n const [rawName, rawValue] = parameter.split(\"=\", 2);\n if (rawName.trim().toLowerCase() !== \"q\") continue;\n const parsed = Number(rawValue?.trim());\n quality = Number.isFinite(parsed) && parsed >= 0 && parsed <= 1 ? parsed : 0;\n break;\n }\n\n qualityByFormat.set(type, Math.max(qualityByFormat.get(type) ?? 0, quality));\n }\n\n let selected: FarmImageFormat | undefined;\n let selectedQuality = 0;\n for (const format of formats) {\n const quality = qualityByFormat.get(format) ?? 0;\n if (quality > selectedQuality) {\n selected = format;\n selectedQuality = quality;\n }\n }\n return selected;\n}\n\nexport function isPrivateImageAddress(address: string): boolean {\n const value = address.toLowerCase().replace(/^\\[|\\]$/g, \"\");\n if (value === \"::\" || value === \"::1\") return true;\n if (/^(?:fc|fd)[0-9a-f]{2}:/.test(value) || /^fe[89ab][0-9a-f]:/.test(value)) return true;\n if (value.startsWith(\"::ffff:\")) return isPrivateImageAddress(value.slice(7));\n\n const parts = value.split(\".\");\n if (parts.length !== 4 || parts.some((part) => !/^\\d{1,3}$/.test(part))) return false;\n const octets = parts.map(Number);\n if (octets.some((part) => part > 255)) return false;\n\n const [a, b] = octets;\n return (\n a === 0 ||\n a === 10 ||\n a === 127 ||\n (a === 100 && b >= 64 && b <= 127) ||\n (a === 169 && b === 254) ||\n (a === 172 && b >= 16 && b <= 31) ||\n (a === 192 && (b === 0 || b === 168)) ||\n (a === 198 && (b === 18 || b === 19)) ||\n a >= 224\n );\n}\n\nfunction parseAllowedInteger(\n raw: string | null,\n allowed: ReadonlySet<number>,\n name: string,\n): number {\n if (!raw || !/^\\d+$/.test(raw)) {\n throw new FarmImageRequestError(\n \"INVALID_PARAMETER\",\n 400,\n `Image ${name} must be an allowed integer`,\n );\n }\n const value = Number(raw);\n if (!allowed.has(value)) {\n throw new FarmImageRequestError(\"INVALID_PARAMETER\", 400, `Image ${name} is not configured`);\n }\n return value;\n}\n\nasync function resolveImageSourceUrl(\n requestUrl: URL,\n config: ResolvedFarmImageConfig,\n validateRemoteUrl: CreateFarmImageHandlerOptions[\"validateRemoteUrl\"],\n): Promise<URL> {\n const raw = requestUrl.searchParams.get(\"url\");\n if (!raw || raw.length > 4096 || raw.startsWith(\"//\")) {\n throw new FarmImageRequestError(\"INVALID_PARAMETER\", 400, \"Invalid image source URL\");\n }\n\n let sourceUrl: URL;\n try {\n sourceUrl = raw.startsWith(\"/\") ? new URL(raw, requestUrl.origin) : new URL(raw);\n } catch {\n throw new FarmImageRequestError(\"INVALID_PARAMETER\", 400, \"Invalid image source URL\");\n }\n\n await validateImageSourceUrl(sourceUrl, requestUrl.origin, config, validateRemoteUrl);\n return sourceUrl;\n}\n\nasync function validateImageSourceUrl(\n sourceUrl: URL,\n requestOrigin: string,\n config: ResolvedFarmImageConfig,\n validateRemoteUrl: CreateFarmImageHandlerOptions[\"validateRemoteUrl\"],\n): Promise<void> {\n if (sourceUrl.protocol !== \"http:\" && sourceUrl.protocol !== \"https:\") {\n throw new FarmImageRequestError(\"DISALLOWED_SOURCE\", 400, \"Unsupported image protocol\");\n }\n if (sourceUrl.username || sourceUrl.password || sourceUrl.hash) {\n throw new FarmImageRequestError(\"DISALLOWED_SOURCE\", 400, \"Unsafe image source URL\");\n }\n\n if (sourceUrl.origin === requestOrigin) {\n if (\n sourceUrl.pathname === config.path ||\n !matchesLocalPatterns(sourceUrl, config.localPatterns)\n ) {\n throw new FarmImageRequestError(\n \"DISALLOWED_SOURCE\",\n 400,\n \"Local image source is not allowed\",\n );\n }\n return;\n }\n\n if (!matchesRemoteSource(sourceUrl, config)) {\n throw new FarmImageRequestError(\"DISALLOWED_SOURCE\", 400, \"Remote image source is not allowed\");\n }\n if (!config.dangerouslyAllowLocalIP && isPrivateImageAddress(sourceUrl.hostname)) {\n throw new FarmImageRequestError(\"PRIVATE_SOURCE\", 400, \"Private image source is not allowed\");\n }\n if (!config.dangerouslyAllowLocalIP) {\n await validateRemoteUrl?.(sourceUrl);\n }\n}\n\nasync function fetchImageSource(\n initialUrl: URL,\n requestOrigin: string,\n config: ResolvedFarmImageConfig,\n fetcher: typeof globalThis.fetch,\n fetchRemote: typeof globalThis.fetch | undefined,\n validateRemoteUrl: CreateFarmImageHandlerOptions[\"validateRemoteUrl\"],\n signal: AbortSignal,\n): Promise<{ response: Response; url: URL }> {\n let currentUrl = initialUrl;\n\n for (let redirectCount = 0; ; redirectCount += 1) {\n throwIfAborted(signal);\n const sourceFetcher = currentUrl.origin === requestOrigin ? fetcher : (fetchRemote ?? fetcher);\n const response = await sourceFetcher(currentUrl, {\n method: \"GET\",\n redirect: \"manual\",\n signal,\n headers: {\n accept: \"image/avif,image/webp,image/*,*/*;q=0.8\",\n \"user-agent\": \"Farm.js Image Optimizer\",\n },\n });\n\n if (![301, 302, 303, 307, 308].includes(response.status)) {\n if (!response.ok) {\n await cancelResponseBody(response);\n throw new FarmImageRequestError(\n \"UNSUPPORTED_IMAGE\",\n response.status === 404 ? 404 : 502,\n \"Could not fetch source image\",\n );\n }\n return { response, url: currentUrl };\n }\n\n if (redirectCount >= config.maximumRedirects) {\n await cancelResponseBody(response);\n throw new FarmImageRequestError(\n \"TOO_MANY_REDIRECTS\",\n 400,\n \"Source image exceeded the redirect limit\",\n );\n }\n const location = response.headers.get(\"location\");\n if (!location) {\n await cancelResponseBody(response);\n throw new FarmImageRequestError(\"UNSUPPORTED_IMAGE\", 502, \"Invalid image redirect\");\n }\n await cancelResponseBody(response);\n currentUrl = new URL(location, currentUrl);\n await validateImageSourceUrl(currentUrl, requestOrigin, config, validateRemoteUrl);\n }\n}\n\ntype InflightOptimization = {\n promise: Promise<OptimizedImage>;\n controller: AbortController;\n waiters: number;\n};\n\n/**\n * Share one in-flight optimization between identical concurrent requests.\n *\n * The shared work runs under its own AbortController rather than any single\n * request's signal, so one caller going away cannot cancel the image everyone\n * else is waiting for. The controller is aborted only when the last waiter\n * leaves, so an abandoned burst still stops promptly.\n */\nasync function runCoalesced(\n inflight: Map<string, InflightOptimization>,\n key: string,\n requestSignal: AbortSignal,\n run: (signal: AbortSignal) => Promise<OptimizedImage>,\n): Promise<OptimizedImage> {\n let entry = inflight.get(key);\n if (!entry) {\n const controller = new AbortController();\n const created: InflightOptimization = {\n controller,\n waiters: 0,\n promise: undefined as unknown as Promise<OptimizedImage>,\n };\n created.promise = run(controller.signal).finally(() => {\n if (inflight.get(key) === created) inflight.delete(key);\n });\n // Every waiter can detach before the shared work settles: an already\n // aborted request returns early without ever attaching to this promise,\n // and the last waiter leaving aborts the controller. Keep one no-op\n // handler so that rejection is never reported as unhandled. Waiters still\n // observe it, because this does not replace the promise they await.\n created.promise.catch(() => {});\n inflight.set(key, created);\n entry = created;\n }\n\n const pending = entry;\n pending.waiters += 1;\n try {\n return await raceRequestAbort(pending.promise, requestSignal);\n } finally {\n pending.waiters -= 1;\n if (pending.waiters === 0 && inflight.get(key) === pending) {\n inflight.delete(key);\n pending.controller.abort();\n }\n }\n}\n\nfunction raceRequestAbort(\n promise: Promise<OptimizedImage>,\n signal: AbortSignal,\n): Promise<OptimizedImage> {\n if (!signal) return promise;\n if (signal.aborted) return Promise.reject(signal.reason ?? new Error(\"Aborted\"));\n\n return new Promise<OptimizedImage>((resolve, reject) => {\n const onAbort = () => reject(signal.reason ?? new Error(\"Aborted\"));\n signal.addEventListener(\"abort\", onAbort, { once: true });\n promise.then(resolve, reject).finally(() => signal.removeEventListener(\"abort\", onAbort));\n });\n}\n\nasync function readResponseWithLimit(response: Response, limit: number): Promise<Uint8Array> {\n const contentLength = response.headers.get(\"content-length\");\n if (contentLength && Number(contentLength) > limit) {\n // Cleanup (including an unread tee branch) must not delay the size rejection.\n void cancelResponseBody(response);\n throw new FarmImageRequestError(\"BODY_TOO_LARGE\", 413, \"Source image is too large\");\n }\n\n if (!response.body) return new Uint8Array();\n const reader = response.body.getReader();\n const chunks: Uint8Array[] = [];\n let byteLength = 0;\n\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n byteLength += value.byteLength;\n if (byteLength > limit) {\n const error = new FarmImageRequestError(\"BODY_TOO_LARGE\", 413, \"Source image is too large\");\n void reader.cancel(error).catch(() => {});\n throw error;\n }\n chunks.push(value);\n }\n } finally {\n reader.releaseLock();\n }\n\n const result = new Uint8Array(byteLength);\n let offset = 0;\n for (const chunk of chunks) {\n result.set(chunk, offset);\n offset += chunk.byteLength;\n }\n return result;\n}\n\nasync function cancelResponseBody(response: Response): Promise<void> {\n try {\n await response.body?.cancel();\n } catch {\n // Cleanup must not replace the request error or redirect result.\n }\n}\n\nfunction detectImageContentType(bytes: Uint8Array): string {\n if (\n bytes.length >= 8 &&\n bytes[0] === 0x89 &&\n bytes[1] === 0x50 &&\n bytes[2] === 0x4e &&\n bytes[3] === 0x47\n ) {\n return \"image/png\";\n }\n if (bytes.length >= 3 && bytes[0] === 0xff && bytes[1] === 0xd8 && bytes[2] === 0xff) {\n return \"image/jpeg\";\n }\n if (bytes.length >= 6) {\n const signature = new TextDecoder().decode(bytes.slice(0, 6));\n if (signature === \"GIF87a\" || signature === \"GIF89a\") return \"image/gif\";\n }\n if (bytes.length >= 12) {\n const riff = new TextDecoder().decode(bytes.slice(0, 4));\n const webp = new TextDecoder().decode(bytes.slice(8, 12));\n if (riff === \"RIFF\" && webp === \"WEBP\") return \"image/webp\";\n const box = new TextDecoder().decode(bytes.slice(4, 12));\n if (box.startsWith(\"ftypavif\") || box.startsWith(\"ftypavis\")) return \"image/avif\";\n }\n\n const prefix = new TextDecoder().decode(bytes.slice(0, 512)).trimStart().toLowerCase();\n if (prefix.startsWith(\"<svg\") || (prefix.startsWith(\"<?xml\") && prefix.includes(\"<svg\"))) {\n return \"image/svg+xml\";\n }\n return \"\";\n}\n\nfunction validateSourceType(type: string, config: ResolvedFarmImageConfig): void {\n if (!type || (type === \"image/svg+xml\" && !config.dangerouslyAllowSVG)) {\n throw new FarmImageRequestError(\"UNSUPPORTED_IMAGE\", 415, \"Unsupported source image\");\n }\n}\n\nfunction validateTransformedResult(\n result: FarmImageTransformResult,\n config: ResolvedFarmImageConfig,\n): void {\n if (!(result.body instanceof Uint8Array) || result.body.byteLength === 0) {\n throw new Error(\"The image transformer returned an empty response\");\n }\n if (result.body.byteLength > config.maximumResponseBody) {\n throw new FarmImageRequestError(\"BODY_TOO_LARGE\", 413, \"Optimized image is too large\");\n }\n const contentType = normalizeImageContentType(result.contentType);\n if (!contentType || (contentType === \"image/svg+xml\" && !config.dangerouslyAllowSVG)) {\n throw new Error(\"The image transformer returned an unsupported content type\");\n }\n result.contentType = contentType;\n}\n\nfunction normalizeImageContentType(value: string | null): string {\n const type = value?.split(\";\", 1)[0].trim().toLowerCase() ?? \"\";\n return type.startsWith(\"image/\") ? type : \"\";\n}\n\nfunction matchesRemoteSource(url: URL, config: ResolvedFarmImageConfig): boolean {\n if (config.domains.includes(url.hostname.toLowerCase())) return true;\n return config.remotePatterns.some((pattern) => matchesRemotePattern(url, pattern));\n}\n\nfunction matchesRemotePattern(url: URL, pattern: FarmImageRemotePattern): boolean {\n return (\n (!pattern.protocol || url.protocol === `${pattern.protocol}:`) &&\n matchesHostname(url.hostname, pattern.hostname) &&\n (pattern.port === undefined || url.port === pattern.port) &&\n matchesGlob(url.pathname, pattern.pathname ?? \"/**\") &&\n (pattern.search === undefined || url.search === pattern.search)\n );\n}\n\nfunction matchesLocalPatterns(url: URL, patterns: readonly FarmImageLocalPattern[]): boolean {\n return patterns.some(\n (pattern) =>\n matchesGlob(url.pathname, pattern.pathname) &&\n (pattern.search === undefined || url.search === pattern.search),\n );\n}\n\nfunction matchesHostname(hostname: string, pattern: string): boolean {\n const normalizedHostname = hostname.toLowerCase();\n const normalizedPattern = pattern.toLowerCase();\n if (normalizedPattern.startsWith(\"**.\")) {\n const suffix = normalizedPattern.slice(3);\n return normalizedHostname === suffix || normalizedHostname.endsWith(`.${suffix}`);\n }\n if (normalizedPattern.startsWith(\"*.\")) {\n const suffix = normalizedPattern.slice(2);\n const prefix = normalizedHostname.slice(0, -(suffix.length + 1));\n return normalizedHostname.endsWith(`.${suffix}`) && !!prefix && !prefix.includes(\".\");\n }\n return normalizedHostname === normalizedPattern;\n}\n\nfunction matchesGlob(value: string, pattern: string): boolean {\n const escaped = pattern.replace(/[.+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n const source = escaped.replace(/\\*\\*/g, \"\\0\").replace(/\\*/g, \"[^/]*\").replace(/\\0/g, \".*\");\n return new RegExp(`^${source}$`).test(value);\n}\n\nfunction createOptimizedImageResponse(\n request: Request,\n image: OptimizedImage,\n config: ResolvedFarmImageConfig,\n): Response {\n const headers = new Headers({\n \"cache-control\": image.cacheControl,\n \"content-type\": image.contentType,\n \"content-length\": String(image.body.byteLength),\n \"content-disposition\": \"inline\",\n etag: image.etag,\n vary: \"Accept\",\n \"x-content-type-options\": \"nosniff\",\n });\n if (image.contentType === \"image/svg+xml\" && config.dangerouslyAllowSVG) {\n headers.set(\"content-security-policy\", \"default-src 'none'; sandbox\");\n }\n if (matchesFarmIfNoneMatch(request.headers.get(\"if-none-match\"), image.etag)) {\n headers.delete(\"content-length\");\n return new Response(null, { status: 304, headers });\n }\n const body =\n request.method === \"HEAD\"\n ? null\n : image.body.buffer.slice(\n image.body.byteOffset,\n image.body.byteOffset + image.body.byteLength,\n );\n return new Response(body as ArrayBuffer | null, { status: 200, headers });\n}\n\nfunction createFarmImageErrorResponse(error: unknown): Response {\n const status =\n error instanceof FarmImageRequestError ? error.status : isAbortError(error) ? 499 : 500;\n const headers = new Headers({\n \"cache-control\": \"no-store\",\n \"content-type\": \"text/plain; charset=utf-8\",\n \"x-content-type-options\": \"nosniff\",\n });\n if (status === 405) headers.set(\"allow\", \"GET, HEAD\");\n\n const message =\n status === 400\n ? \"Invalid image request\"\n : status === 404\n ? \"Image not found\"\n : status === 405\n ? \"Method not allowed\"\n : status === 413\n ? \"Image is too large\"\n : status === 415\n ? \"Unsupported image\"\n : status === 499\n ? \"Image request cancelled\"\n : \"Image optimization failed\";\n return new Response(message, { status, headers });\n}\n\nfunction createImageEtag(bytes: Uint8Array): string {\n let hash = 0x811c9dc5;\n for (const byte of bytes) {\n hash ^= byte;\n hash = Math.imul(hash, 0x01000193);\n }\n return `W/\"farm-${bytes.byteLength.toString(16)}-${(hash >>> 0).toString(16)}\"`;\n}\n\nfunction throwIfAborted(signal: AbortSignal): void {\n if (signal.aborted) {\n throw signal.reason instanceof Error\n ? signal.reason\n : new DOMException(\"The image request was aborted\", \"AbortError\");\n }\n}\n\nfunction isAbortError(error: unknown): boolean {\n return error instanceof Error && error.name === \"AbortError\";\n}\n\nclass FarmImageMemoryCache {\n private readonly entries = new Map<string, OptimizedImage>();\n\n constructor(private readonly capacity: number) {}\n\n get(key: string): OptimizedImage | undefined {\n const value = this.entries.get(key);\n if (!value) return undefined;\n if (value.expiresAt <= Date.now()) {\n this.entries.delete(key);\n return undefined;\n }\n this.entries.delete(key);\n this.entries.set(key, value);\n return value;\n }\n\n set(key: string, value: OptimizedImage): void {\n if (this.capacity <= 0) return;\n this.entries.delete(key);\n this.entries.set(key, value);\n while (this.entries.size > this.capacity) {\n const oldestKey = this.entries.keys().next().value as string | undefined;\n if (oldestKey === undefined) break;\n this.entries.delete(oldestKey);\n }\n }\n}\n","import { assertBrowserStableRoutePath } from \"./routing/specificity\";\n\ninterface FarmNodeAbortRequest {\n aborted?: boolean;\n once(event: \"aborted\", listener: () => void): unknown;\n off(event: \"aborted\", listener: () => void): unknown;\n}\n\ninterface FarmNodeAbortResponse {\n writableEnded: boolean;\n once(event: \"close\" | \"finish\", listener: () => void): unknown;\n off(event: \"close\" | \"finish\", listener: () => void): unknown;\n}\n\n/** Share disconnect semantics between development and production Node requests. */\nexport function createFarmNodeRequestAbortSignal(\n req: FarmNodeAbortRequest,\n res: FarmNodeAbortResponse,\n): AbortSignal {\n const controller = new AbortController();\n let disposed = false;\n const dispose = () => {\n if (disposed) return;\n disposed = true;\n req.off(\"aborted\", abort);\n res.off(\"close\", abortOnEarlyClose);\n res.off(\"finish\", dispose);\n controller.signal.removeEventListener(\"abort\", dispose);\n };\n const abort = () => controller.abort();\n const abortOnEarlyClose = () => {\n if (!res.writableEnded) abort();\n dispose();\n };\n\n if (req.aborted) {\n controller.abort();\n return controller.signal;\n }\n\n req.once(\"aborted\", abort);\n res.once(\"close\", abortOnEarlyClose);\n res.once(\"finish\", dispose);\n controller.signal.addEventListener(\"abort\", dispose, { once: true });\n return controller.signal;\n}\n\nexport const DEFAULT_FARM_SERVER_BODY_SIZE_LIMIT = 10_000_000;\nexport const DEFAULT_FARM_SERVER_HEADERS_TIMEOUT = 60_000;\nexport const DEFAULT_FARM_SERVER_REQUEST_TIMEOUT = 300_000;\nexport const DEFAULT_FARM_SERVER_KEEP_ALIVE_TIMEOUT = 5_000;\nexport const DEFAULT_FARM_SERVER_GRACEFUL_SHUTDOWN_TIMEOUT = 30_000;\nconst MAX_FARM_SERVER_TIMEOUT = 2_147_483_647;\n\nexport type FarmServerDuration = number | `${number}${\"ms\" | \"s\" | \"m\" | \"h\"}`;\n\nexport interface FarmServerHealthConfig {\n /** Liveness endpoint. It remains healthy while a production process drains. */\n livenessPath?: string;\n /** Readiness endpoint. It returns 503 until startup completes and while draining. */\n readinessPath?: string;\n}\n\nexport interface ResolvedFarmServerHealthConfig {\n enabled: boolean;\n livenessPath: string;\n readinessPath: string;\n}\n\nexport interface FarmServerConfig {\n /** Maximum request body size for API routes, integrations, workflows, and uploads. */\n bodySizeLimit?: number | string;\n /** Trust proxy-provided client address and request authority headers. Enable only behind a trusted proxy. */\n trustProxy?: boolean;\n /** Maximum time for a Node client to send complete request headers. */\n headersTimeout?: FarmServerDuration;\n /** Maximum time for a Node client to send the complete request. */\n requestTimeout?: FarmServerDuration;\n /** How long an idle Node keep-alive connection remains open after a response. */\n keepAliveTimeout?: FarmServerDuration;\n /** Maximum time the Node adapter drains traffic before forcing shutdown. */\n gracefulShutdownTimeout?: FarmServerDuration;\n /** Production liveness and readiness endpoints. Set to false to disable them. */\n health?: false | FarmServerHealthConfig;\n}\n\nexport interface ResolvedFarmServerConfig {\n bodySizeLimit: number;\n trustProxy: boolean;\n headersTimeout: number;\n requestTimeout: number;\n keepAliveTimeout: number;\n gracefulShutdownTimeout: number;\n health: ResolvedFarmServerHealthConfig;\n}\n\nexport type FarmRequestBodyErrorCode = \"BODY_TOO_LARGE\" | \"INVALID_CONTENT_LENGTH\";\n\n/** Apply the weak entity-tag comparison required by If-None-Match. */\nexport function matchesFarmIfNoneMatch(\n value: string | readonly string[] | null | undefined,\n etag: string,\n): boolean {\n const expected = parseEntityTag(trimOptionalWhitespace(etag));\n if (!expected) return false;\n\n const values = Array.isArray(value) ? value : [value];\n const combined = values\n .filter((header): header is string => typeof header === \"string\")\n .join(\",\");\n const fieldValue = trimOptionalWhitespace(combined);\n if (!fieldValue) return false;\n if (fieldValue === \"*\") return true;\n\n let matched = false;\n let hasEntityTag = false;\n for (const candidate of splitEntityTags(fieldValue)) {\n const token = trimOptionalWhitespace(candidate);\n if (!token) continue;\n\n const parsed = parseEntityTag(token);\n if (!parsed) return false;\n hasEntityTag = true;\n if (parsed === expected) matched = true;\n }\n\n return hasEntityTag && matched;\n}\n\nfunction trimOptionalWhitespace(value: string): string {\n return value.replace(/^[\\t ]+|[\\t ]+$/g, \"\");\n}\n\nfunction parseEntityTag(value: string): string | null {\n const opaqueTag = value.startsWith(\"W/\") ? value.slice(2) : value;\n if (opaqueTag.length < 2 || opaqueTag[0] !== '\"' || opaqueTag.at(-1) !== '\"') return null;\n\n for (let index = 1; index < opaqueTag.length - 1; index++) {\n const code = opaqueTag.charCodeAt(index);\n if (code === 0x21 || (code >= 0x23 && code <= 0x7e) || code >= 0x80) continue;\n return null;\n }\n\n return opaqueTag;\n}\n\nfunction splitEntityTags(value: string): string[] {\n const tags: string[] = [];\n let start = 0;\n let quoted = false;\n\n for (let index = 0; index < value.length; index++) {\n const character = value[index];\n if (character === '\"') {\n quoted = !quoted;\n } else if (character === \",\" && !quoted) {\n tags.push(value.slice(start, index));\n start = index + 1;\n }\n }\n\n tags.push(value.slice(start));\n return tags;\n}\n\nexport class FarmRequestBodyError extends Error {\n readonly code: FarmRequestBodyErrorCode;\n readonly status: number;\n\n constructor(code: FarmRequestBodyErrorCode, status: number, message: string) {\n super(message);\n this.name = \"FarmRequestBodyError\";\n this.code = code;\n this.status = status;\n }\n}\n\nexport function resolveFarmServerConfig(\n config: FarmServerConfig | ResolvedFarmServerConfig | undefined,\n): ResolvedFarmServerConfig {\n const headersTimeout = parseFarmServerDuration(\n config?.headersTimeout ?? DEFAULT_FARM_SERVER_HEADERS_TIMEOUT,\n \"server.headersTimeout\",\n );\n const requestTimeout = parseFarmServerDuration(\n config?.requestTimeout ?? DEFAULT_FARM_SERVER_REQUEST_TIMEOUT,\n \"server.requestTimeout\",\n );\n if (headersTimeout > requestTimeout) {\n throw new TypeError(\"server.headersTimeout must not exceed server.requestTimeout\");\n }\n\n return Object.freeze({\n bodySizeLimit: parseBodySizeLimit(\n config?.bodySizeLimit ?? DEFAULT_FARM_SERVER_BODY_SIZE_LIMIT,\n \"server.bodySizeLimit\",\n ),\n trustProxy: config?.trustProxy === true,\n headersTimeout,\n requestTimeout,\n keepAliveTimeout: parseFarmServerDuration(\n config?.keepAliveTimeout ?? DEFAULT_FARM_SERVER_KEEP_ALIVE_TIMEOUT,\n \"server.keepAliveTimeout\",\n ),\n gracefulShutdownTimeout: parseFarmServerDuration(\n config?.gracefulShutdownTimeout ?? DEFAULT_FARM_SERVER_GRACEFUL_SHUTDOWN_TIMEOUT,\n \"server.gracefulShutdownTimeout\",\n ),\n health: resolveFarmServerHealthConfig(config?.health),\n });\n}\n\nexport function parseFarmServerDuration(\n value: FarmServerDuration,\n optionName = \"duration\",\n): number {\n if (typeof value === \"number\") {\n if (!Number.isSafeInteger(value) || value <= 0) {\n throw new TypeError(`${optionName} must be a positive safe integer`);\n }\n if (value > MAX_FARM_SERVER_TIMEOUT) {\n throw new TypeError(`${optionName} must not exceed ${MAX_FARM_SERVER_TIMEOUT} milliseconds`);\n }\n return value;\n }\n\n const match = value\n .trim()\n .toLowerCase()\n .match(/^(\\d+(?:\\.\\d+)?)\\s*(ms|s|m|h)$/);\n if (!match) {\n throw new TypeError(`${optionName} must be milliseconds or a duration such as \"30s\" or \"2m\"`);\n }\n\n const amount = Number(match[1]);\n const unit = match[2];\n const multiplier = unit === \"ms\" ? 1 : unit === \"s\" ? 1_000 : unit === \"m\" ? 60_000 : 3_600_000;\n const milliseconds = Math.floor(amount * multiplier);\n if (!Number.isSafeInteger(milliseconds) || milliseconds <= 0) {\n throw new TypeError(`${optionName} must resolve to a positive safe integer`);\n }\n if (milliseconds > MAX_FARM_SERVER_TIMEOUT) {\n throw new TypeError(`${optionName} must not exceed ${MAX_FARM_SERVER_TIMEOUT} milliseconds`);\n }\n return milliseconds;\n}\n\nfunction resolveFarmServerHealthConfig(\n config: false | FarmServerHealthConfig | ResolvedFarmServerHealthConfig | undefined,\n): ResolvedFarmServerHealthConfig {\n if (config === false || (config && \"enabled\" in config && config.enabled === false)) {\n return Object.freeze({\n enabled: false,\n livenessPath: \"/_farm/health/live\",\n readinessPath: \"/_farm/health/ready\",\n });\n }\n\n const livenessPath = normalizeHealthPath(\n config?.livenessPath ?? \"/_farm/health/live\",\n \"server.health.livenessPath\",\n );\n const readinessPath = normalizeHealthPath(\n config?.readinessPath ?? \"/_farm/health/ready\",\n \"server.health.readinessPath\",\n );\n if (livenessPath === readinessPath) {\n throw new TypeError(\"server.health livenessPath and readinessPath must be different\");\n }\n\n return Object.freeze({ enabled: true, livenessPath, readinessPath });\n}\n\nfunction normalizeHealthPath(value: string, optionName: string): string {\n const path = value.trim();\n if (!path.startsWith(\"/\") || path.includes(\"?\") || path.includes(\"#\") || path.includes(\"*\")) {\n throw new TypeError(`${optionName} must be an absolute pathname without a query or wildcard`);\n }\n const normalized = path.length > 1 ? path.replace(/\\/+$/, \"\") || \"/\" : path;\n assertBrowserStableRoutePath(normalized);\n return normalized;\n}\n\nexport function parseBodySizeLimit(value: number | string, optionName = \"bodySizeLimit\"): number {\n if (typeof value === \"number\") {\n if (!Number.isSafeInteger(value) || value <= 0) {\n throw new TypeError(`${optionName} must be a positive safe integer`);\n }\n return value;\n }\n\n const match = value\n .trim()\n .toLowerCase()\n .match(/^(\\d+(?:\\.\\d+)?)\\s*(b|kb|mb|gb|kib|mib|gib)?$/);\n if (!match) {\n throw new TypeError(`${optionName} must be bytes or a size string such as \"500kb\" or \"10mb\"`);\n }\n\n const amount = Number(match[1]);\n const unit = match[2] ?? \"b\";\n const multiplier: Record<string, number> = {\n b: 1,\n kb: 1_000,\n mb: 1_000_000,\n gb: 1_000_000_000,\n kib: 1_024,\n mib: 1_048_576,\n gib: 1_073_741_824,\n };\n const bytes = Math.floor(amount * multiplier[unit]);\n\n if (!Number.isSafeInteger(bytes) || bytes <= 0) {\n throw new TypeError(`${optionName} must resolve to a positive safe integer`);\n }\n\n return bytes;\n}\n\nexport async function bufferFarmRequestBody(request: Request, limit: number): Promise<Request> {\n if (request.method === \"GET\" || request.method === \"HEAD\" || request.body === null) {\n return request;\n }\n\n const bytes = await readFarmRequestBody(request, limit);\n const body = new Uint8Array(bytes.byteLength);\n body.set(bytes);\n return new Request(request, {\n // oxlint-disable-next-line unicorn/no-invalid-fetch-options -- GET and HEAD return above.\n body: body.buffer,\n });\n}\n\nexport async function readFarmRequestBody(request: Request, limit: number): Promise<Uint8Array> {\n try {\n validateContentLength(request.headers.get(\"content-length\"), limit);\n } catch (error) {\n // A cloned Request is a tee branch: its cancellation may wait for the\n // untouched branch. Rejection must not wait for producer-owned cleanup.\n void request.body?.cancel(error).catch(() => {});\n throw error;\n }\n throwIfAborted(request.signal);\n if (!request.body) return new Uint8Array();\n\n const reader = request.body.getReader();\n const chunks: Uint8Array[] = [];\n let total = 0;\n const cancelBodyRead = () => {\n void reader.cancel(request.signal.reason).catch(() => {});\n };\n request.signal.addEventListener(\"abort\", cancelBodyRead, { once: true });\n\n try {\n while (true) {\n throwIfAborted(request.signal);\n const { done, value } = await reader.read();\n // Cancellation resolves a pending read as EOF, not necessarily an error.\n throwIfAborted(request.signal);\n if (done) break;\n if (!value) continue;\n\n total += value.byteLength;\n if (total > limit) {\n const error = new FarmRequestBodyError(\"BODY_TOO_LARGE\", 413, \"Request body is too large\");\n void reader.cancel(error).catch(() => {});\n throw error;\n }\n chunks.push(value);\n }\n } catch (error) {\n if (request.signal.aborted) throwIfAborted(request.signal);\n throw error;\n } finally {\n request.signal.removeEventListener(\"abort\", cancelBodyRead);\n reader.releaseLock();\n }\n\n const body = new Uint8Array(total);\n let offset = 0;\n for (const chunk of chunks) {\n body.set(chunk, offset);\n offset += chunk.byteLength;\n }\n return body;\n}\n\nexport async function readNodeRequestBody(\n request: {\n headers: Record<string, string | string[] | undefined>;\n on(event: \"data\", listener: (chunk: unknown) => void): unknown;\n on(event: \"end\", listener: () => void): unknown;\n on(event: \"error\", listener: (error: Error) => void): unknown;\n removeListener?(event: string, listener: (...args: any[]) => void): unknown;\n resume?(): unknown;\n },\n limit: number,\n): Promise<Buffer> {\n const rawContentLength = request.headers[\"content-length\"];\n const contentLength = Array.isArray(rawContentLength) ? rawContentLength[0] : rawContentLength;\n try {\n validateContentLength(contentLength, limit);\n } catch (error) {\n request.resume?.();\n throw error;\n }\n\n return await new Promise<Buffer>((resolve, reject) => {\n const chunks: Buffer[] = [];\n let total = 0;\n let settled = false;\n\n const cleanup = () => {\n request.removeListener?.(\"data\", onData);\n request.removeListener?.(\"end\", onEnd);\n request.removeListener?.(\"error\", onError);\n };\n const rejectOnce = (error: Error) => {\n if (settled) return;\n settled = true;\n cleanup();\n request.resume?.();\n reject(error);\n };\n const onData = (chunk: unknown) => {\n const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk as any);\n total += bytes.byteLength;\n if (total > limit) {\n rejectOnce(new FarmRequestBodyError(\"BODY_TOO_LARGE\", 413, \"Request body is too large\"));\n return;\n }\n chunks.push(bytes);\n };\n const onEnd = () => {\n if (settled) return;\n settled = true;\n cleanup();\n resolve(Buffer.concat(chunks, total));\n };\n const onError = (error: Error) => rejectOnce(error);\n\n request.on(\"data\", onData);\n request.on(\"end\", onEnd);\n request.on(\"error\", onError);\n });\n}\n\nexport function createFarmRequestBodyErrorResponse(error: unknown): Response | null {\n if (!(error instanceof FarmRequestBodyError)) return null;\n\n return new Response(error.status === 413 ? \"Payload Too Large\" : \"Bad Request\", {\n status: error.status,\n headers: {\n \"cache-control\": \"no-store\",\n \"content-type\": \"text/plain; charset=utf-8\",\n \"x-content-type-options\": \"nosniff\",\n },\n });\n}\n\nfunction validateContentLength(value: string | null | undefined, limit: number): void {\n const contentLength = value?.trim();\n if (!contentLength) return;\n if (!/^\\d+$/.test(contentLength)) {\n throw new FarmRequestBodyError(\"INVALID_CONTENT_LENGTH\", 400, \"Invalid content-length header\");\n }\n if (Number(contentLength) > limit) {\n throw new FarmRequestBodyError(\"BODY_TOO_LARGE\", 413, \"Request body is too large\");\n }\n}\n\nfunction throwIfAborted(signal: AbortSignal): void {\n if (!signal.aborted) return;\n if (signal.reason !== undefined) throw signal.reason;\n throw new DOMException(\"The operation was aborted\", \"AbortError\");\n}\n"],"mappings":"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","names":[]}
@@ -9,6 +9,12 @@ interface FarmImageTransformInput {
9
9
  accept: string;
10
10
  formats: readonly FarmImageFormat[];
11
11
  signal: AbortSignal;
12
+ /**
13
+ * Byte ceiling for any source the transformer fetches itself. Supplied by the
14
+ * image handler; transformers that re-fetch the origin (Cloudflare) must
15
+ * enforce it, since they bypass the handler's bounded read.
16
+ */
17
+ maximumResponseBody?: number;
12
18
  }
13
19
  interface FarmImageTransformResult {
14
20
  body: Uint8Array;
@@ -9,6 +9,12 @@ interface FarmImageTransformInput {
9
9
  accept: string;
10
10
  formats: readonly FarmImageFormat[];
11
11
  signal: AbortSignal;
12
+ /**
13
+ * Byte ceiling for any source the transformer fetches itself. Supplied by the
14
+ * image handler; transformers that re-fetch the origin (Cloudflare) must
15
+ * enforce it, since they bypass the handler's bounded read.
16
+ */
17
+ maximumResponseBody?: number;
12
18
  }
13
19
  interface FarmImageTransformResult {
14
20
  body: Uint8Array;
@@ -4,7 +4,7 @@ import {
4
4
  createFarmImageHandler,
5
5
  isPrivateImageAddress,
6
6
  selectOutputFormat
7
- } from "../chunk-WKDAA6V2.mjs";
7
+ } from "../chunk-3BTAMIQE.mjs";
8
8
  import "../chunk-ZHQMHJAV.mjs";
9
9
  import "../chunk-DZU3EKQP.mjs";
10
10
  import "../chunk-CGILA3WO.mjs";
@@ -48,6 +48,7 @@ var _FarmImageRequestError = class _FarmImageRequestError extends Error {
48
48
  };
49
49
  __name(_FarmImageRequestError, "FarmImageRequestError");
50
50
  var FarmImageRequestError = _FarmImageRequestError;
51
+ var DEFAULT_IMAGE_TRANSFORM_BODY_LIMIT = 10 * 1024 * 1024;
51
52
  function selectOutputFormat(accept, formats) {
52
53
  const qualityByFormat = /* @__PURE__ */ new Map();
53
54
  for (const range of accept.split(",")) {
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/image-sharp.ts","../../src/image-server.ts"],"sourcesContent":["import type { ResolvedFarmImageConfig } from \"./image-config\";\nimport {\n FarmImageRequestError,\n isPrivateImageAddress,\n selectOutputFormat,\n type FarmImageTransformer,\n} from \"./image-server\";\n\ntype NodeImageDnsLookup = (\n hostname: string,\n options: { all: true; verbatim: true },\n callback: (\n error: NodeJS.ErrnoException | null,\n addresses: Array<{ address: string; family: number }>,\n ) => void,\n) => void;\n\nexport function createSharpImageTransformer(): FarmImageTransformer {\n return async ({ source, sourceType, width, quality, accept, formats, signal }) => {\n // Sharp is an optional native runtime. Load it only when an image request\n // actually needs a transform so disabled/unused image pipelines do not add\n // a startup dependency or native-module initialization cost.\n const { default: sharp } = await import(\"sharp\");\n throwIfAborted(signal);\n const outputFormat = selectOutputFormat(accept, formats);\n let pipeline = sharp(source, {\n animated: sourceType === \"image/gif\" || sourceType === \"image/webp\",\n failOn: \"warning\",\n limitInputPixels: 268_402_689,\n })\n .rotate()\n .resize({ width, fit: \"inside\", withoutEnlargement: true });\n\n // When the Accept header matches none of the configured formats, keep the\n // source's own format (svg rasterizes to png) instead of forcing JPEG —\n // that preserved neither transparency nor the truth of the content type.\n let encodedType: string;\n if (outputFormat === \"image/avif\") {\n pipeline = pipeline.avif({ quality });\n encodedType = \"image/avif\";\n } else if (outputFormat === \"image/webp\") {\n pipeline = pipeline.webp({ quality });\n encodedType = \"image/webp\";\n } else if (sourceType === \"image/png\" || sourceType === \"image/svg+xml\") {\n pipeline = pipeline.png();\n encodedType = \"image/png\";\n } else if (sourceType === \"image/gif\") {\n pipeline = pipeline.gif();\n encodedType = \"image/gif\";\n } else if (sourceType === \"image/webp\") {\n pipeline = pipeline.webp({ quality });\n encodedType = \"image/webp\";\n } else if (sourceType === \"image/avif\") {\n pipeline = pipeline.avif({ quality });\n encodedType = \"image/avif\";\n } else {\n pipeline = pipeline.jpeg({ quality });\n encodedType = \"image/jpeg\";\n }\n\n const body = await pipeline.toBuffer();\n throwIfAborted(signal);\n return {\n body,\n contentType: encodedType,\n };\n };\n}\n\nexport function createNodeImageUrlValidator(config: ResolvedFarmImageConfig) {\n return async function validateNodeImageUrl(url: URL): Promise<void> {\n if (config.dangerouslyAllowLocalIP) return;\n\n let addresses: Array<{ address: string; family: number }>;\n try {\n // DNS is only needed by remote image requests. Keeping it out of the\n // initial server module graph reduces normal page/API startup work.\n const { lookup } = await import(\"node:dns/promises\");\n addresses = await lookup(url.hostname, { all: true, verbatim: true });\n } catch {\n throw new FarmImageRequestError(\n \"PRIVATE_SOURCE\",\n 400,\n \"Could not safely resolve the image source\",\n );\n }\n if (addresses.length === 0 || addresses.some(({ address }) => isPrivateImageAddress(address))) {\n throw new FarmImageRequestError(\"PRIVATE_SOURCE\", 400, \"Private image source is not allowed\");\n }\n };\n}\n\n/**\n * Create a remote image fetcher whose socket lookup rejects private addresses.\n * Validation and connection share this lookup, closing the DNS-rebinding gap\n * left by resolving a hostname before a separate global fetch.\n */\nexport function createNodeImageFetcher(\n config: Pick<ResolvedFarmImageConfig, \"dangerouslyAllowLocalIP\">,\n lookup?: NodeImageDnsLookup,\n): typeof globalThis.fetch {\n if (config.dangerouslyAllowLocalIP) return globalThis.fetch.bind(globalThis);\n\n return (async (input: RequestInfo | URL, init: RequestInit = {}) => {\n const inputRequest = input instanceof Request ? input : undefined;\n const url = input instanceof URL ? input : new URL(inputRequest?.url ?? String(input));\n if (url.protocol !== \"http:\" && url.protocol !== \"https:\") {\n throw new FarmImageRequestError(\"DISALLOWED_SOURCE\", 400, \"Unsupported image protocol\");\n }\n\n const [{ request }, { Readable }, dns] = await Promise.all([\n url.protocol === \"https:\" ? import(\"node:https\") : import(\"node:http\"),\n import(\"node:stream\"),\n lookup ? Promise.resolve(null) : import(\"node:dns\"),\n ]);\n const resolveHostname = lookup ?? (dns!.lookup as unknown as NodeImageDnsLookup);\n const headers = new Headers(inputRequest?.headers);\n new Headers(init.headers).forEach((value, name) => headers.set(name, value));\n if (!headers.has(\"accept-encoding\")) headers.set(\"accept-encoding\", \"identity\");\n\n return new Promise<Response>((resolve, reject) => {\n const nodeRequest = request(\n url,\n {\n method: init.method ?? inputRequest?.method ?? \"GET\",\n headers: Object.fromEntries(headers.entries()),\n signal: init.signal ?? inputRequest?.signal,\n lookup(hostname, options, callback) {\n resolveHostname(hostname, { all: true, verbatim: true }, (error, addresses) => {\n if (error) {\n callback(error, \"\", 4);\n return;\n }\n if (\n addresses.length === 0 ||\n addresses.some(({ address }) => isPrivateImageAddress(address))\n ) {\n callback(\n new FarmImageRequestError(\n \"PRIVATE_SOURCE\",\n 400,\n \"Private image source is not allowed\",\n ),\n \"\",\n 4,\n );\n return;\n }\n\n if (options.all) {\n (\n callback as unknown as (\n error: null,\n addresses: Array<{ address: string; family: number }>,\n ) => void\n )(null, addresses);\n return;\n }\n const address = addresses[0]!;\n callback(null, address.address, address.family);\n });\n },\n },\n (nodeResponse) => {\n const status = nodeResponse.statusCode ?? 500;\n if (status < 200 || status > 599) {\n nodeResponse.resume();\n reject(\n new FarmImageRequestError(\n \"UNSUPPORTED_IMAGE\",\n 502,\n \"Image source returned an invalid HTTP status\",\n ),\n );\n return;\n }\n const responseHeaders = new Headers();\n for (let index = 0; index < nodeResponse.rawHeaders.length; index += 2) {\n responseHeaders.append(\n nodeResponse.rawHeaders[index]!,\n nodeResponse.rawHeaders[index + 1]!,\n );\n }\n const body =\n status === 204 || status === 205 || status === 304\n ? null\n : (Readable.toWeb(nodeResponse) as ReadableStream);\n resolve(\n new Response(body, {\n status,\n statusText: nodeResponse.statusMessage,\n headers: responseHeaders,\n }),\n );\n },\n );\n nodeRequest.once(\"error\", reject);\n nodeRequest.end();\n });\n }) as typeof globalThis.fetch;\n}\n\nfunction throwIfAborted(signal: AbortSignal): void {\n if (signal.aborted) {\n throw signal.reason instanceof Error\n ? signal.reason\n : new DOMException(\"The image request was aborted\", \"AbortError\");\n }\n}\n","import type {\n FarmImageFormat,\n FarmImageLocalPattern,\n FarmImageRemotePattern,\n ResolvedFarmImageConfig,\n} from \"./image-config\";\nimport { matchesFarmIfNoneMatch } from \"./server-http\";\n\nexport interface FarmImageTransformInput {\n source: Uint8Array;\n sourceUrl: URL;\n sourceType: string;\n width: number;\n quality: number;\n accept: string;\n formats: readonly FarmImageFormat[];\n signal: AbortSignal;\n /**\n * Byte ceiling for any source the transformer fetches itself. Supplied by the\n * image handler; transformers that re-fetch the origin (Cloudflare) must\n * enforce it, since they bypass the handler's bounded read.\n */\n maximumResponseBody?: number;\n}\n\nexport interface FarmImageTransformResult {\n body: Uint8Array;\n contentType: string;\n}\n\nexport type FarmImageTransformer = (\n input: FarmImageTransformInput,\n) => Promise<FarmImageTransformResult>;\n\nexport interface CreateFarmImageHandlerOptions {\n fetch?: typeof globalThis.fetch;\n /** Node-only fetcher that validates the DNS result used for remote connections. @internal */\n fetchRemote?: typeof globalThis.fetch;\n transform: FarmImageTransformer;\n validateRemoteUrl?: (url: URL) => void | Promise<void>;\n onError?: (error: unknown, request: Request) => void;\n cacheEntries?: number;\n}\n\nexport type FarmImageHandler = (request: Request) => Promise<Response | null>;\n\ntype OptimizedImage = FarmImageTransformResult & {\n etag: string;\n cacheControl: string;\n expiresAt: number;\n};\n\ntype FarmImageRequestErrorCode =\n | \"BODY_TOO_LARGE\"\n | \"DISALLOWED_SOURCE\"\n | \"INVALID_METHOD\"\n | \"INVALID_PARAMETER\"\n | \"PRIVATE_SOURCE\"\n | \"TOO_MANY_REDIRECTS\"\n | \"UNSUPPORTED_IMAGE\";\n\nexport class FarmImageRequestError extends Error {\n readonly code: FarmImageRequestErrorCode;\n readonly status: number;\n\n constructor(code: FarmImageRequestErrorCode, status: number, message: string) {\n super(message);\n this.name = \"FarmImageRequestError\";\n this.code = code;\n this.status = status;\n }\n}\n\nexport function createFarmImageHandler(\n config: ResolvedFarmImageConfig,\n options: CreateFarmImageHandlerOptions,\n): FarmImageHandler {\n const fetcher = options.fetch ?? globalThis.fetch;\n const cache = new FarmImageMemoryCache(options.cacheEntries ?? 100);\n // Identical concurrent misses share one fetch + transform. Without this a\n // burst for an uncached image (a new page going live, a CDN cold start)\n // fetches the origin and runs the codec once per request.\n const inflight = new Map<string, InflightOptimization>();\n const allowedWidths = new Set([...config.deviceSizes, ...config.imageSizes]);\n const allowedQualities = new Set(config.qualities);\n\n return async function handleFarmImage(request): Promise<Response | null> {\n const requestUrl = new URL(request.url);\n if (requestUrl.pathname !== config.path) return null;\n\n try {\n if (config.provider === \"none\") {\n throw new FarmImageRequestError(\n \"DISALLOWED_SOURCE\",\n 404,\n \"The Farm image optimizer is disabled\",\n );\n }\n if (request.method !== \"GET\" && request.method !== \"HEAD\") {\n throw new FarmImageRequestError(\n \"INVALID_METHOD\",\n 405,\n \"The Farm image optimizer only accepts GET and HEAD\",\n );\n }\n\n const sourceUrl = await resolveImageSourceUrl(requestUrl, config, options.validateRemoteUrl);\n const width = parseAllowedInteger(requestUrl.searchParams.get(\"w\"), allowedWidths, \"width\");\n const quality = parseAllowedInteger(\n requestUrl.searchParams.get(\"q\"),\n allowedQualities,\n \"quality\",\n );\n const accept = request.headers.get(\"accept\") ?? \"\";\n // Key on the format the Accept header negotiates to, not the header text.\n // Both transformers derive their output from `selectOutputFormat(accept,\n // formats)` alone, so every header that negotiates to the same format\n // produces byte-identical output. Keying on the raw header let a caller\n // vary it freely (`image/webp,*/*;q=0.8`, reordered lists, extra params)\n // and force an uncached fetch and transform each time.\n const negotiatedFormat = selectOutputFormat(accept, config.formats) ?? \"\";\n const cacheKey = `${sourceUrl.href}\\n${width}\\n${quality}\\n${negotiatedFormat}`;\n let optimized = cache.get(cacheKey);\n\n if (!optimized) {\n optimized = await runCoalesced(inflight, cacheKey, request.signal, async (signal) => {\n const fetchedSource = await fetchImageSource(\n sourceUrl,\n requestUrl.origin,\n config,\n fetcher,\n options.fetchRemote,\n options.validateRemoteUrl,\n signal,\n );\n const source = await readResponseWithLimit(\n fetchedSource.response,\n config.maximumResponseBody,\n );\n const sourceType = detectImageContentType(source);\n validateSourceType(sourceType, config);\n throwIfAborted(signal);\n\n const result = await options.transform({\n source,\n sourceUrl: fetchedSource.url,\n sourceType,\n width,\n quality,\n accept,\n formats: config.formats,\n signal,\n maximumResponseBody: config.maximumResponseBody,\n });\n throwIfAborted(signal);\n validateTransformedResult(result, config);\n\n const entry = {\n ...result,\n etag: createImageEtag(result.body),\n cacheControl: `public, max-age=${config.minimumCacheTTL}, stale-while-revalidate=${Math.max(\n config.minimumCacheTTL,\n 60,\n )}`,\n expiresAt: Date.now() + config.minimumCacheTTL * 1_000,\n };\n cache.set(cacheKey, entry);\n return entry;\n });\n }\n\n return createOptimizedImageResponse(request, optimized, config);\n } catch (error) {\n if (!(error instanceof FarmImageRequestError) && !isAbortError(error)) {\n try {\n options.onError?.(error, request);\n } catch {\n // Error reporting must not replace the optimizer's sanitized response.\n }\n }\n return createFarmImageErrorResponse(error);\n }\n };\n}\n\n/** Mirrors the `images.maximumResponseBody` default (\"10mb\"). */\nconst DEFAULT_IMAGE_TRANSFORM_BODY_LIMIT = 10 * 1024 * 1024;\n\nexport function createCloudflareImageTransformer(\n fetcher: typeof globalThis.fetch = globalThis.fetch,\n): FarmImageTransformer {\n return async ({ sourceUrl, width, quality, accept, formats, signal, maximumResponseBody }) => {\n const format = selectOutputFormat(accept, formats);\n // Cloudflare resizing works by letting the edge fetch the origin, so this\n // request cannot reuse the bytes the handler already read. It still must not\n // be a weaker fetch than the validated one: `redirect: \"manual\"` keeps it\n // from silently following a hop the handler never validated, and the body is\n // read under the same ceiling as the handler's own read.\n const response = await fetcher(sourceUrl, {\n signal,\n redirect: \"manual\",\n headers: { accept: \"image/*\" },\n cf: {\n image: {\n fit: \"scale-down\",\n width,\n quality,\n ...(format ? { format: format === \"image/avif\" ? \"avif\" : \"webp\" } : {}),\n },\n },\n } as RequestInit);\n\n if (response.status >= 300 && response.status < 400) {\n void cancelResponseBody(response);\n throw new FarmImageRequestError(\n \"UNSUPPORTED_IMAGE\",\n 502,\n \"Source image redirected after validation\",\n );\n }\n\n if (!response.ok) {\n throw new FarmImageRequestError(\n \"UNSUPPORTED_IMAGE\",\n response.status === 404 ? 404 : 502,\n \"Cloudflare could not transform the source image\",\n );\n }\n\n const body = await readResponseWithLimit(\n response,\n maximumResponseBody ?? DEFAULT_IMAGE_TRANSFORM_BODY_LIMIT,\n );\n return {\n body,\n contentType:\n normalizeImageContentType(response.headers.get(\"content-type\")) ||\n detectImageContentType(body),\n };\n };\n}\n\nexport function selectOutputFormat(\n accept: string,\n formats: readonly FarmImageFormat[],\n): FarmImageFormat | undefined {\n const qualityByFormat = new Map<string, number>();\n\n for (const range of accept.split(\",\")) {\n const [rawType, ...parameters] = range.split(\";\");\n const type = rawType.trim().toLowerCase();\n if (!type) continue;\n\n let quality = 1;\n for (const parameter of parameters) {\n const [rawName, rawValue] = parameter.split(\"=\", 2);\n if (rawName.trim().toLowerCase() !== \"q\") continue;\n const parsed = Number(rawValue?.trim());\n quality = Number.isFinite(parsed) && parsed >= 0 && parsed <= 1 ? parsed : 0;\n break;\n }\n\n qualityByFormat.set(type, Math.max(qualityByFormat.get(type) ?? 0, quality));\n }\n\n let selected: FarmImageFormat | undefined;\n let selectedQuality = 0;\n for (const format of formats) {\n const quality = qualityByFormat.get(format) ?? 0;\n if (quality > selectedQuality) {\n selected = format;\n selectedQuality = quality;\n }\n }\n return selected;\n}\n\nexport function isPrivateImageAddress(address: string): boolean {\n const value = address.toLowerCase().replace(/^\\[|\\]$/g, \"\");\n if (value === \"::\" || value === \"::1\") return true;\n if (/^(?:fc|fd)[0-9a-f]{2}:/.test(value) || /^fe[89ab][0-9a-f]:/.test(value)) return true;\n if (value.startsWith(\"::ffff:\")) return isPrivateImageAddress(value.slice(7));\n\n const parts = value.split(\".\");\n if (parts.length !== 4 || parts.some((part) => !/^\\d{1,3}$/.test(part))) return false;\n const octets = parts.map(Number);\n if (octets.some((part) => part > 255)) return false;\n\n const [a, b] = octets;\n return (\n a === 0 ||\n a === 10 ||\n a === 127 ||\n (a === 100 && b >= 64 && b <= 127) ||\n (a === 169 && b === 254) ||\n (a === 172 && b >= 16 && b <= 31) ||\n (a === 192 && (b === 0 || b === 168)) ||\n (a === 198 && (b === 18 || b === 19)) ||\n a >= 224\n );\n}\n\nfunction parseAllowedInteger(\n raw: string | null,\n allowed: ReadonlySet<number>,\n name: string,\n): number {\n if (!raw || !/^\\d+$/.test(raw)) {\n throw new FarmImageRequestError(\n \"INVALID_PARAMETER\",\n 400,\n `Image ${name} must be an allowed integer`,\n );\n }\n const value = Number(raw);\n if (!allowed.has(value)) {\n throw new FarmImageRequestError(\"INVALID_PARAMETER\", 400, `Image ${name} is not configured`);\n }\n return value;\n}\n\nasync function resolveImageSourceUrl(\n requestUrl: URL,\n config: ResolvedFarmImageConfig,\n validateRemoteUrl: CreateFarmImageHandlerOptions[\"validateRemoteUrl\"],\n): Promise<URL> {\n const raw = requestUrl.searchParams.get(\"url\");\n if (!raw || raw.length > 4096 || raw.startsWith(\"//\")) {\n throw new FarmImageRequestError(\"INVALID_PARAMETER\", 400, \"Invalid image source URL\");\n }\n\n let sourceUrl: URL;\n try {\n sourceUrl = raw.startsWith(\"/\") ? new URL(raw, requestUrl.origin) : new URL(raw);\n } catch {\n throw new FarmImageRequestError(\"INVALID_PARAMETER\", 400, \"Invalid image source URL\");\n }\n\n await validateImageSourceUrl(sourceUrl, requestUrl.origin, config, validateRemoteUrl);\n return sourceUrl;\n}\n\nasync function validateImageSourceUrl(\n sourceUrl: URL,\n requestOrigin: string,\n config: ResolvedFarmImageConfig,\n validateRemoteUrl: CreateFarmImageHandlerOptions[\"validateRemoteUrl\"],\n): Promise<void> {\n if (sourceUrl.protocol !== \"http:\" && sourceUrl.protocol !== \"https:\") {\n throw new FarmImageRequestError(\"DISALLOWED_SOURCE\", 400, \"Unsupported image protocol\");\n }\n if (sourceUrl.username || sourceUrl.password || sourceUrl.hash) {\n throw new FarmImageRequestError(\"DISALLOWED_SOURCE\", 400, \"Unsafe image source URL\");\n }\n\n if (sourceUrl.origin === requestOrigin) {\n if (\n sourceUrl.pathname === config.path ||\n !matchesLocalPatterns(sourceUrl, config.localPatterns)\n ) {\n throw new FarmImageRequestError(\n \"DISALLOWED_SOURCE\",\n 400,\n \"Local image source is not allowed\",\n );\n }\n return;\n }\n\n if (!matchesRemoteSource(sourceUrl, config)) {\n throw new FarmImageRequestError(\"DISALLOWED_SOURCE\", 400, \"Remote image source is not allowed\");\n }\n if (!config.dangerouslyAllowLocalIP && isPrivateImageAddress(sourceUrl.hostname)) {\n throw new FarmImageRequestError(\"PRIVATE_SOURCE\", 400, \"Private image source is not allowed\");\n }\n if (!config.dangerouslyAllowLocalIP) {\n await validateRemoteUrl?.(sourceUrl);\n }\n}\n\nasync function fetchImageSource(\n initialUrl: URL,\n requestOrigin: string,\n config: ResolvedFarmImageConfig,\n fetcher: typeof globalThis.fetch,\n fetchRemote: typeof globalThis.fetch | undefined,\n validateRemoteUrl: CreateFarmImageHandlerOptions[\"validateRemoteUrl\"],\n signal: AbortSignal,\n): Promise<{ response: Response; url: URL }> {\n let currentUrl = initialUrl;\n\n for (let redirectCount = 0; ; redirectCount += 1) {\n throwIfAborted(signal);\n const sourceFetcher = currentUrl.origin === requestOrigin ? fetcher : (fetchRemote ?? fetcher);\n const response = await sourceFetcher(currentUrl, {\n method: \"GET\",\n redirect: \"manual\",\n signal,\n headers: {\n accept: \"image/avif,image/webp,image/*,*/*;q=0.8\",\n \"user-agent\": \"Farm.js Image Optimizer\",\n },\n });\n\n if (![301, 302, 303, 307, 308].includes(response.status)) {\n if (!response.ok) {\n await cancelResponseBody(response);\n throw new FarmImageRequestError(\n \"UNSUPPORTED_IMAGE\",\n response.status === 404 ? 404 : 502,\n \"Could not fetch source image\",\n );\n }\n return { response, url: currentUrl };\n }\n\n if (redirectCount >= config.maximumRedirects) {\n await cancelResponseBody(response);\n throw new FarmImageRequestError(\n \"TOO_MANY_REDIRECTS\",\n 400,\n \"Source image exceeded the redirect limit\",\n );\n }\n const location = response.headers.get(\"location\");\n if (!location) {\n await cancelResponseBody(response);\n throw new FarmImageRequestError(\"UNSUPPORTED_IMAGE\", 502, \"Invalid image redirect\");\n }\n await cancelResponseBody(response);\n currentUrl = new URL(location, currentUrl);\n await validateImageSourceUrl(currentUrl, requestOrigin, config, validateRemoteUrl);\n }\n}\n\ntype InflightOptimization = {\n promise: Promise<OptimizedImage>;\n controller: AbortController;\n waiters: number;\n};\n\n/**\n * Share one in-flight optimization between identical concurrent requests.\n *\n * The shared work runs under its own AbortController rather than any single\n * request's signal, so one caller going away cannot cancel the image everyone\n * else is waiting for. The controller is aborted only when the last waiter\n * leaves, so an abandoned burst still stops promptly.\n */\nasync function runCoalesced(\n inflight: Map<string, InflightOptimization>,\n key: string,\n requestSignal: AbortSignal,\n run: (signal: AbortSignal) => Promise<OptimizedImage>,\n): Promise<OptimizedImage> {\n let entry = inflight.get(key);\n if (!entry) {\n const controller = new AbortController();\n const created: InflightOptimization = {\n controller,\n waiters: 0,\n promise: undefined as unknown as Promise<OptimizedImage>,\n };\n created.promise = run(controller.signal).finally(() => {\n if (inflight.get(key) === created) inflight.delete(key);\n });\n // Every waiter can detach before the shared work settles: an already\n // aborted request returns early without ever attaching to this promise,\n // and the last waiter leaving aborts the controller. Keep one no-op\n // handler so that rejection is never reported as unhandled. Waiters still\n // observe it, because this does not replace the promise they await.\n created.promise.catch(() => {});\n inflight.set(key, created);\n entry = created;\n }\n\n const pending = entry;\n pending.waiters += 1;\n try {\n return await raceRequestAbort(pending.promise, requestSignal);\n } finally {\n pending.waiters -= 1;\n if (pending.waiters === 0 && inflight.get(key) === pending) {\n inflight.delete(key);\n pending.controller.abort();\n }\n }\n}\n\nfunction raceRequestAbort(\n promise: Promise<OptimizedImage>,\n signal: AbortSignal,\n): Promise<OptimizedImage> {\n if (!signal) return promise;\n if (signal.aborted) return Promise.reject(signal.reason ?? new Error(\"Aborted\"));\n\n return new Promise<OptimizedImage>((resolve, reject) => {\n const onAbort = () => reject(signal.reason ?? new Error(\"Aborted\"));\n signal.addEventListener(\"abort\", onAbort, { once: true });\n promise.then(resolve, reject).finally(() => signal.removeEventListener(\"abort\", onAbort));\n });\n}\n\nasync function readResponseWithLimit(response: Response, limit: number): Promise<Uint8Array> {\n const contentLength = response.headers.get(\"content-length\");\n if (contentLength && Number(contentLength) > limit) {\n // Cleanup (including an unread tee branch) must not delay the size rejection.\n void cancelResponseBody(response);\n throw new FarmImageRequestError(\"BODY_TOO_LARGE\", 413, \"Source image is too large\");\n }\n\n if (!response.body) return new Uint8Array();\n const reader = response.body.getReader();\n const chunks: Uint8Array[] = [];\n let byteLength = 0;\n\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n byteLength += value.byteLength;\n if (byteLength > limit) {\n const error = new FarmImageRequestError(\"BODY_TOO_LARGE\", 413, \"Source image is too large\");\n void reader.cancel(error).catch(() => {});\n throw error;\n }\n chunks.push(value);\n }\n } finally {\n reader.releaseLock();\n }\n\n const result = new Uint8Array(byteLength);\n let offset = 0;\n for (const chunk of chunks) {\n result.set(chunk, offset);\n offset += chunk.byteLength;\n }\n return result;\n}\n\nasync function cancelResponseBody(response: Response): Promise<void> {\n try {\n await response.body?.cancel();\n } catch {\n // Cleanup must not replace the request error or redirect result.\n }\n}\n\nfunction detectImageContentType(bytes: Uint8Array): string {\n if (\n bytes.length >= 8 &&\n bytes[0] === 0x89 &&\n bytes[1] === 0x50 &&\n bytes[2] === 0x4e &&\n bytes[3] === 0x47\n ) {\n return \"image/png\";\n }\n if (bytes.length >= 3 && bytes[0] === 0xff && bytes[1] === 0xd8 && bytes[2] === 0xff) {\n return \"image/jpeg\";\n }\n if (bytes.length >= 6) {\n const signature = new TextDecoder().decode(bytes.slice(0, 6));\n if (signature === \"GIF87a\" || signature === \"GIF89a\") return \"image/gif\";\n }\n if (bytes.length >= 12) {\n const riff = new TextDecoder().decode(bytes.slice(0, 4));\n const webp = new TextDecoder().decode(bytes.slice(8, 12));\n if (riff === \"RIFF\" && webp === \"WEBP\") return \"image/webp\";\n const box = new TextDecoder().decode(bytes.slice(4, 12));\n if (box.startsWith(\"ftypavif\") || box.startsWith(\"ftypavis\")) return \"image/avif\";\n }\n\n const prefix = new TextDecoder().decode(bytes.slice(0, 512)).trimStart().toLowerCase();\n if (prefix.startsWith(\"<svg\") || (prefix.startsWith(\"<?xml\") && prefix.includes(\"<svg\"))) {\n return \"image/svg+xml\";\n }\n return \"\";\n}\n\nfunction validateSourceType(type: string, config: ResolvedFarmImageConfig): void {\n if (!type || (type === \"image/svg+xml\" && !config.dangerouslyAllowSVG)) {\n throw new FarmImageRequestError(\"UNSUPPORTED_IMAGE\", 415, \"Unsupported source image\");\n }\n}\n\nfunction validateTransformedResult(\n result: FarmImageTransformResult,\n config: ResolvedFarmImageConfig,\n): void {\n if (!(result.body instanceof Uint8Array) || result.body.byteLength === 0) {\n throw new Error(\"The image transformer returned an empty response\");\n }\n if (result.body.byteLength > config.maximumResponseBody) {\n throw new FarmImageRequestError(\"BODY_TOO_LARGE\", 413, \"Optimized image is too large\");\n }\n const contentType = normalizeImageContentType(result.contentType);\n if (!contentType || (contentType === \"image/svg+xml\" && !config.dangerouslyAllowSVG)) {\n throw new Error(\"The image transformer returned an unsupported content type\");\n }\n result.contentType = contentType;\n}\n\nfunction normalizeImageContentType(value: string | null): string {\n const type = value?.split(\";\", 1)[0].trim().toLowerCase() ?? \"\";\n return type.startsWith(\"image/\") ? type : \"\";\n}\n\nfunction matchesRemoteSource(url: URL, config: ResolvedFarmImageConfig): boolean {\n if (config.domains.includes(url.hostname.toLowerCase())) return true;\n return config.remotePatterns.some((pattern) => matchesRemotePattern(url, pattern));\n}\n\nfunction matchesRemotePattern(url: URL, pattern: FarmImageRemotePattern): boolean {\n return (\n (!pattern.protocol || url.protocol === `${pattern.protocol}:`) &&\n matchesHostname(url.hostname, pattern.hostname) &&\n (pattern.port === undefined || url.port === pattern.port) &&\n matchesGlob(url.pathname, pattern.pathname ?? \"/**\") &&\n (pattern.search === undefined || url.search === pattern.search)\n );\n}\n\nfunction matchesLocalPatterns(url: URL, patterns: readonly FarmImageLocalPattern[]): boolean {\n return patterns.some(\n (pattern) =>\n matchesGlob(url.pathname, pattern.pathname) &&\n (pattern.search === undefined || url.search === pattern.search),\n );\n}\n\nfunction matchesHostname(hostname: string, pattern: string): boolean {\n const normalizedHostname = hostname.toLowerCase();\n const normalizedPattern = pattern.toLowerCase();\n if (normalizedPattern.startsWith(\"**.\")) {\n const suffix = normalizedPattern.slice(3);\n return normalizedHostname === suffix || normalizedHostname.endsWith(`.${suffix}`);\n }\n if (normalizedPattern.startsWith(\"*.\")) {\n const suffix = normalizedPattern.slice(2);\n const prefix = normalizedHostname.slice(0, -(suffix.length + 1));\n return normalizedHostname.endsWith(`.${suffix}`) && !!prefix && !prefix.includes(\".\");\n }\n return normalizedHostname === normalizedPattern;\n}\n\nfunction matchesGlob(value: string, pattern: string): boolean {\n const escaped = pattern.replace(/[.+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n const source = escaped.replace(/\\*\\*/g, \"\\0\").replace(/\\*/g, \"[^/]*\").replace(/\\0/g, \".*\");\n return new RegExp(`^${source}$`).test(value);\n}\n\nfunction createOptimizedImageResponse(\n request: Request,\n image: OptimizedImage,\n config: ResolvedFarmImageConfig,\n): Response {\n const headers = new Headers({\n \"cache-control\": image.cacheControl,\n \"content-type\": image.contentType,\n \"content-length\": String(image.body.byteLength),\n \"content-disposition\": \"inline\",\n etag: image.etag,\n vary: \"Accept\",\n \"x-content-type-options\": \"nosniff\",\n });\n if (image.contentType === \"image/svg+xml\" && config.dangerouslyAllowSVG) {\n headers.set(\"content-security-policy\", \"default-src 'none'; sandbox\");\n }\n if (matchesFarmIfNoneMatch(request.headers.get(\"if-none-match\"), image.etag)) {\n headers.delete(\"content-length\");\n return new Response(null, { status: 304, headers });\n }\n const body =\n request.method === \"HEAD\"\n ? null\n : image.body.buffer.slice(\n image.body.byteOffset,\n image.body.byteOffset + image.body.byteLength,\n );\n return new Response(body as ArrayBuffer | null, { status: 200, headers });\n}\n\nfunction createFarmImageErrorResponse(error: unknown): Response {\n const status =\n error instanceof FarmImageRequestError ? error.status : isAbortError(error) ? 499 : 500;\n const headers = new Headers({\n \"cache-control\": \"no-store\",\n \"content-type\": \"text/plain; charset=utf-8\",\n \"x-content-type-options\": \"nosniff\",\n });\n if (status === 405) headers.set(\"allow\", \"GET, HEAD\");\n\n const message =\n status === 400\n ? \"Invalid image request\"\n : status === 404\n ? \"Image not found\"\n : status === 405\n ? \"Method not allowed\"\n : status === 413\n ? \"Image is too large\"\n : status === 415\n ? \"Unsupported image\"\n : status === 499\n ? \"Image request cancelled\"\n : \"Image optimization failed\";\n return new Response(message, { status, headers });\n}\n\nfunction createImageEtag(bytes: Uint8Array): string {\n let hash = 0x811c9dc5;\n for (const byte of bytes) {\n hash ^= byte;\n hash = Math.imul(hash, 0x01000193);\n }\n return `W/\"farm-${bytes.byteLength.toString(16)}-${(hash >>> 0).toString(16)}\"`;\n}\n\nfunction throwIfAborted(signal: AbortSignal): void {\n if (signal.aborted) {\n throw signal.reason instanceof Error\n ? signal.reason\n : new DOMException(\"The image request was aborted\", \"AbortError\");\n }\n}\n\nfunction isAbortError(error: unknown): boolean {\n return error instanceof Error && error.name === \"AbortError\";\n}\n\nclass FarmImageMemoryCache {\n private readonly entries = new Map<string, OptimizedImage>();\n\n constructor(private readonly capacity: number) {}\n\n get(key: string): OptimizedImage | undefined {\n const value = this.entries.get(key);\n if (!value) return undefined;\n if (value.expiresAt <= Date.now()) {\n this.entries.delete(key);\n return undefined;\n }\n this.entries.delete(key);\n this.entries.set(key, value);\n return value;\n }\n\n set(key: string, value: OptimizedImage): void {\n if (this.capacity <= 0) return;\n this.entries.delete(key);\n this.entries.set(key, value);\n while (this.entries.size > this.capacity) {\n const oldestKey = this.entries.keys().next().value as string | undefined;\n if (oldestKey === undefined) break;\n this.entries.delete(oldestKey);\n }\n }\n}\n"],"mappings":"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","names":["address"]}
@@ -15,6 +15,6 @@ declare function createNodeImageUrlValidator(config: ResolvedFarmImageConfig): (
15
15
  * Validation and connection share this lookup, closing the DNS-rebinding gap
16
16
  * left by resolving a hostname before a separate global fetch.
17
17
  */
18
- declare function createNodeImageFetcher(config: ResolvedFarmImageConfig, lookup?: NodeImageDnsLookup): typeof globalThis.fetch;
18
+ declare function createNodeImageFetcher(config: Pick<ResolvedFarmImageConfig, "dangerouslyAllowLocalIP">, lookup?: NodeImageDnsLookup): typeof globalThis.fetch;
19
19
 
20
20
  export { createNodeImageFetcher, createNodeImageUrlValidator, createSharpImageTransformer };
@@ -15,6 +15,6 @@ declare function createNodeImageUrlValidator(config: ResolvedFarmImageConfig): (
15
15
  * Validation and connection share this lookup, closing the DNS-rebinding gap
16
16
  * left by resolving a hostname before a separate global fetch.
17
17
  */
18
- declare function createNodeImageFetcher(config: ResolvedFarmImageConfig, lookup?: NodeImageDnsLookup): typeof globalThis.fetch;
18
+ declare function createNodeImageFetcher(config: Pick<ResolvedFarmImageConfig, "dangerouslyAllowLocalIP">, lookup?: NodeImageDnsLookup): typeof globalThis.fetch;
19
19
 
20
20
  export { createNodeImageFetcher, createNodeImageUrlValidator, createSharpImageTransformer };
@@ -2,7 +2,7 @@ import {
2
2
  FarmImageRequestError,
3
3
  isPrivateImageAddress,
4
4
  selectOutputFormat
5
- } from "../chunk-WKDAA6V2.mjs";
5
+ } from "../chunk-3BTAMIQE.mjs";
6
6
  import "../chunk-ZHQMHJAV.mjs";
7
7
  import "../chunk-DZU3EKQP.mjs";
8
8
  import {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/image-sharp.ts"],"sourcesContent":["import type { ResolvedFarmImageConfig } from \"./image-config\";\nimport {\n FarmImageRequestError,\n isPrivateImageAddress,\n selectOutputFormat,\n type FarmImageTransformer,\n} from \"./image-server\";\n\ntype NodeImageDnsLookup = (\n hostname: string,\n options: { all: true; verbatim: true },\n callback: (\n error: NodeJS.ErrnoException | null,\n addresses: Array<{ address: string; family: number }>,\n ) => void,\n) => void;\n\nexport function createSharpImageTransformer(): FarmImageTransformer {\n return async ({ source, sourceType, width, quality, accept, formats, signal }) => {\n // Sharp is an optional native runtime. Load it only when an image request\n // actually needs a transform so disabled/unused image pipelines do not add\n // a startup dependency or native-module initialization cost.\n const { default: sharp } = await import(\"sharp\");\n throwIfAborted(signal);\n const outputFormat = selectOutputFormat(accept, formats);\n let pipeline = sharp(source, {\n animated: sourceType === \"image/gif\" || sourceType === \"image/webp\",\n failOn: \"warning\",\n limitInputPixels: 268_402_689,\n })\n .rotate()\n .resize({ width, fit: \"inside\", withoutEnlargement: true });\n\n // When the Accept header matches none of the configured formats, keep the\n // source's own format (svg rasterizes to png) instead of forcing JPEG —\n // that preserved neither transparency nor the truth of the content type.\n let encodedType: string;\n if (outputFormat === \"image/avif\") {\n pipeline = pipeline.avif({ quality });\n encodedType = \"image/avif\";\n } else if (outputFormat === \"image/webp\") {\n pipeline = pipeline.webp({ quality });\n encodedType = \"image/webp\";\n } else if (sourceType === \"image/png\" || sourceType === \"image/svg+xml\") {\n pipeline = pipeline.png();\n encodedType = \"image/png\";\n } else if (sourceType === \"image/gif\") {\n pipeline = pipeline.gif();\n encodedType = \"image/gif\";\n } else if (sourceType === \"image/webp\") {\n pipeline = pipeline.webp({ quality });\n encodedType = \"image/webp\";\n } else if (sourceType === \"image/avif\") {\n pipeline = pipeline.avif({ quality });\n encodedType = \"image/avif\";\n } else {\n pipeline = pipeline.jpeg({ quality });\n encodedType = \"image/jpeg\";\n }\n\n const body = await pipeline.toBuffer();\n throwIfAborted(signal);\n return {\n body,\n contentType: encodedType,\n };\n };\n}\n\nexport function createNodeImageUrlValidator(config: ResolvedFarmImageConfig) {\n return async function validateNodeImageUrl(url: URL): Promise<void> {\n if (config.dangerouslyAllowLocalIP) return;\n\n let addresses: Array<{ address: string; family: number }>;\n try {\n // DNS is only needed by remote image requests. Keeping it out of the\n // initial server module graph reduces normal page/API startup work.\n const { lookup } = await import(\"node:dns/promises\");\n addresses = await lookup(url.hostname, { all: true, verbatim: true });\n } catch {\n throw new FarmImageRequestError(\n \"PRIVATE_SOURCE\",\n 400,\n \"Could not safely resolve the image source\",\n );\n }\n if (addresses.length === 0 || addresses.some(({ address }) => isPrivateImageAddress(address))) {\n throw new FarmImageRequestError(\"PRIVATE_SOURCE\", 400, \"Private image source is not allowed\");\n }\n };\n}\n\n/**\n * Create a remote image fetcher whose socket lookup rejects private addresses.\n * Validation and connection share this lookup, closing the DNS-rebinding gap\n * left by resolving a hostname before a separate global fetch.\n */\nexport function createNodeImageFetcher(\n config: ResolvedFarmImageConfig,\n lookup?: NodeImageDnsLookup,\n): typeof globalThis.fetch {\n if (config.dangerouslyAllowLocalIP) return globalThis.fetch.bind(globalThis);\n\n return (async (input: RequestInfo | URL, init: RequestInit = {}) => {\n const inputRequest = input instanceof Request ? input : undefined;\n const url = input instanceof URL ? input : new URL(inputRequest?.url ?? String(input));\n if (url.protocol !== \"http:\" && url.protocol !== \"https:\") {\n throw new FarmImageRequestError(\"DISALLOWED_SOURCE\", 400, \"Unsupported image protocol\");\n }\n\n const [{ request }, { Readable }, dns] = await Promise.all([\n url.protocol === \"https:\" ? import(\"node:https\") : import(\"node:http\"),\n import(\"node:stream\"),\n lookup ? Promise.resolve(null) : import(\"node:dns\"),\n ]);\n const resolveHostname = lookup ?? (dns!.lookup as unknown as NodeImageDnsLookup);\n const headers = new Headers(inputRequest?.headers);\n new Headers(init.headers).forEach((value, name) => headers.set(name, value));\n if (!headers.has(\"accept-encoding\")) headers.set(\"accept-encoding\", \"identity\");\n\n return new Promise<Response>((resolve, reject) => {\n const nodeRequest = request(\n url,\n {\n method: init.method ?? inputRequest?.method ?? \"GET\",\n headers: Object.fromEntries(headers.entries()),\n signal: init.signal ?? inputRequest?.signal,\n lookup(hostname, options, callback) {\n resolveHostname(hostname, { all: true, verbatim: true }, (error, addresses) => {\n if (error) {\n callback(error, \"\", 4);\n return;\n }\n if (\n addresses.length === 0 ||\n addresses.some(({ address }) => isPrivateImageAddress(address))\n ) {\n callback(\n new FarmImageRequestError(\n \"PRIVATE_SOURCE\",\n 400,\n \"Private image source is not allowed\",\n ),\n \"\",\n 4,\n );\n return;\n }\n\n if (options.all) {\n (\n callback as unknown as (\n error: null,\n addresses: Array<{ address: string; family: number }>,\n ) => void\n )(null, addresses);\n return;\n }\n const address = addresses[0]!;\n callback(null, address.address, address.family);\n });\n },\n },\n (nodeResponse) => {\n const status = nodeResponse.statusCode ?? 500;\n if (status < 200 || status > 599) {\n nodeResponse.resume();\n reject(\n new FarmImageRequestError(\n \"UNSUPPORTED_IMAGE\",\n 502,\n \"Image source returned an invalid HTTP status\",\n ),\n );\n return;\n }\n const responseHeaders = new Headers();\n for (let index = 0; index < nodeResponse.rawHeaders.length; index += 2) {\n responseHeaders.append(\n nodeResponse.rawHeaders[index]!,\n nodeResponse.rawHeaders[index + 1]!,\n );\n }\n const body =\n status === 204 || status === 205 || status === 304\n ? null\n : (Readable.toWeb(nodeResponse) as ReadableStream);\n resolve(\n new Response(body, {\n status,\n statusText: nodeResponse.statusMessage,\n headers: responseHeaders,\n }),\n );\n },\n );\n nodeRequest.once(\"error\", reject);\n nodeRequest.end();\n });\n }) as typeof globalThis.fetch;\n}\n\nfunction throwIfAborted(signal: AbortSignal): void {\n if (signal.aborted) {\n throw signal.reason instanceof Error\n ? signal.reason\n : new DOMException(\"The image request was aborted\", \"AbortError\");\n }\n}\n"],"mappings":"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","names":["address"]}
1
+ {"version":3,"sources":["../../src/image-sharp.ts"],"sourcesContent":["import type { ResolvedFarmImageConfig } from \"./image-config\";\nimport {\n FarmImageRequestError,\n isPrivateImageAddress,\n selectOutputFormat,\n type FarmImageTransformer,\n} from \"./image-server\";\n\ntype NodeImageDnsLookup = (\n hostname: string,\n options: { all: true; verbatim: true },\n callback: (\n error: NodeJS.ErrnoException | null,\n addresses: Array<{ address: string; family: number }>,\n ) => void,\n) => void;\n\nexport function createSharpImageTransformer(): FarmImageTransformer {\n return async ({ source, sourceType, width, quality, accept, formats, signal }) => {\n // Sharp is an optional native runtime. Load it only when an image request\n // actually needs a transform so disabled/unused image pipelines do not add\n // a startup dependency or native-module initialization cost.\n const { default: sharp } = await import(\"sharp\");\n throwIfAborted(signal);\n const outputFormat = selectOutputFormat(accept, formats);\n let pipeline = sharp(source, {\n animated: sourceType === \"image/gif\" || sourceType === \"image/webp\",\n failOn: \"warning\",\n limitInputPixels: 268_402_689,\n })\n .rotate()\n .resize({ width, fit: \"inside\", withoutEnlargement: true });\n\n // When the Accept header matches none of the configured formats, keep the\n // source's own format (svg rasterizes to png) instead of forcing JPEG —\n // that preserved neither transparency nor the truth of the content type.\n let encodedType: string;\n if (outputFormat === \"image/avif\") {\n pipeline = pipeline.avif({ quality });\n encodedType = \"image/avif\";\n } else if (outputFormat === \"image/webp\") {\n pipeline = pipeline.webp({ quality });\n encodedType = \"image/webp\";\n } else if (sourceType === \"image/png\" || sourceType === \"image/svg+xml\") {\n pipeline = pipeline.png();\n encodedType = \"image/png\";\n } else if (sourceType === \"image/gif\") {\n pipeline = pipeline.gif();\n encodedType = \"image/gif\";\n } else if (sourceType === \"image/webp\") {\n pipeline = pipeline.webp({ quality });\n encodedType = \"image/webp\";\n } else if (sourceType === \"image/avif\") {\n pipeline = pipeline.avif({ quality });\n encodedType = \"image/avif\";\n } else {\n pipeline = pipeline.jpeg({ quality });\n encodedType = \"image/jpeg\";\n }\n\n const body = await pipeline.toBuffer();\n throwIfAborted(signal);\n return {\n body,\n contentType: encodedType,\n };\n };\n}\n\nexport function createNodeImageUrlValidator(config: ResolvedFarmImageConfig) {\n return async function validateNodeImageUrl(url: URL): Promise<void> {\n if (config.dangerouslyAllowLocalIP) return;\n\n let addresses: Array<{ address: string; family: number }>;\n try {\n // DNS is only needed by remote image requests. Keeping it out of the\n // initial server module graph reduces normal page/API startup work.\n const { lookup } = await import(\"node:dns/promises\");\n addresses = await lookup(url.hostname, { all: true, verbatim: true });\n } catch {\n throw new FarmImageRequestError(\n \"PRIVATE_SOURCE\",\n 400,\n \"Could not safely resolve the image source\",\n );\n }\n if (addresses.length === 0 || addresses.some(({ address }) => isPrivateImageAddress(address))) {\n throw new FarmImageRequestError(\"PRIVATE_SOURCE\", 400, \"Private image source is not allowed\");\n }\n };\n}\n\n/**\n * Create a remote image fetcher whose socket lookup rejects private addresses.\n * Validation and connection share this lookup, closing the DNS-rebinding gap\n * left by resolving a hostname before a separate global fetch.\n */\nexport function createNodeImageFetcher(\n config: Pick<ResolvedFarmImageConfig, \"dangerouslyAllowLocalIP\">,\n lookup?: NodeImageDnsLookup,\n): typeof globalThis.fetch {\n if (config.dangerouslyAllowLocalIP) return globalThis.fetch.bind(globalThis);\n\n return (async (input: RequestInfo | URL, init: RequestInit = {}) => {\n const inputRequest = input instanceof Request ? input : undefined;\n const url = input instanceof URL ? input : new URL(inputRequest?.url ?? String(input));\n if (url.protocol !== \"http:\" && url.protocol !== \"https:\") {\n throw new FarmImageRequestError(\"DISALLOWED_SOURCE\", 400, \"Unsupported image protocol\");\n }\n\n const [{ request }, { Readable }, dns] = await Promise.all([\n url.protocol === \"https:\" ? import(\"node:https\") : import(\"node:http\"),\n import(\"node:stream\"),\n lookup ? Promise.resolve(null) : import(\"node:dns\"),\n ]);\n const resolveHostname = lookup ?? (dns!.lookup as unknown as NodeImageDnsLookup);\n const headers = new Headers(inputRequest?.headers);\n new Headers(init.headers).forEach((value, name) => headers.set(name, value));\n if (!headers.has(\"accept-encoding\")) headers.set(\"accept-encoding\", \"identity\");\n\n return new Promise<Response>((resolve, reject) => {\n const nodeRequest = request(\n url,\n {\n method: init.method ?? inputRequest?.method ?? \"GET\",\n headers: Object.fromEntries(headers.entries()),\n signal: init.signal ?? inputRequest?.signal,\n lookup(hostname, options, callback) {\n resolveHostname(hostname, { all: true, verbatim: true }, (error, addresses) => {\n if (error) {\n callback(error, \"\", 4);\n return;\n }\n if (\n addresses.length === 0 ||\n addresses.some(({ address }) => isPrivateImageAddress(address))\n ) {\n callback(\n new FarmImageRequestError(\n \"PRIVATE_SOURCE\",\n 400,\n \"Private image source is not allowed\",\n ),\n \"\",\n 4,\n );\n return;\n }\n\n if (options.all) {\n (\n callback as unknown as (\n error: null,\n addresses: Array<{ address: string; family: number }>,\n ) => void\n )(null, addresses);\n return;\n }\n const address = addresses[0]!;\n callback(null, address.address, address.family);\n });\n },\n },\n (nodeResponse) => {\n const status = nodeResponse.statusCode ?? 500;\n if (status < 200 || status > 599) {\n nodeResponse.resume();\n reject(\n new FarmImageRequestError(\n \"UNSUPPORTED_IMAGE\",\n 502,\n \"Image source returned an invalid HTTP status\",\n ),\n );\n return;\n }\n const responseHeaders = new Headers();\n for (let index = 0; index < nodeResponse.rawHeaders.length; index += 2) {\n responseHeaders.append(\n nodeResponse.rawHeaders[index]!,\n nodeResponse.rawHeaders[index + 1]!,\n );\n }\n const body =\n status === 204 || status === 205 || status === 304\n ? null\n : (Readable.toWeb(nodeResponse) as ReadableStream);\n resolve(\n new Response(body, {\n status,\n statusText: nodeResponse.statusMessage,\n headers: responseHeaders,\n }),\n );\n },\n );\n nodeRequest.once(\"error\", reject);\n nodeRequest.end();\n });\n }) as typeof globalThis.fetch;\n}\n\nfunction throwIfAborted(signal: AbortSignal): void {\n if (signal.aborted) {\n throw signal.reason instanceof Error\n ? signal.reason\n : new DOMException(\"The image request was aborted\", \"AbortError\");\n }\n}\n"],"mappings":"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","names":["address"]}