diff --git a/lib/index.d.ts b/lib/index.d.ts new file mode 100644 index 0000000..8ece07c --- /dev/null +++ b/lib/index.d.ts @@ -0,0 +1,166 @@ +// Type definitions for @hapi/inert +// Project: https://github.com/hapijs/inert/ +// Definitions by: Steve Ognibene +// Alexander James Phillips +// Silas Rech + +import { Plugin, Request, ResponseObject } from '@hapi/hapi'; + +declare namespace inert { + type RequestHandler = (request: Request) => T; + + interface ReplyFileHandlerOptions { + /** + * confine - serve file relative to this directory and returns 403 Forbidden if the path resolves outside the confine directory. + * Defaults to true which uses the relativeTo route option as the confine. Set to false to disable this security feature. + */ + confine?: boolean | string | undefined; + /** + * filename - an optional filename to specify if sending a 'Content-Disposition' header, defaults to the basename of path + */ + filename?: string | undefined; + /** + * mode - specifies whether to include the 'Content-Disposition' header with the response. Available values: + * * false - header is not included. This is the default value. + * * 'attachment' + * *'inline' + */ + mode?: false | 'attachment' | 'inline' | undefined; + /** + * lookupCompressed - if true, looks for for a pre-compressed version of the file with the same filename with an extension, depending on the accepted encoding. Defaults to false. + */ + lookupCompressed?: boolean | undefined; + /** + * lookupMap - an object which maps content encoding to expected file name extension. Defaults to `{ gzip: '.gz' }. + */ + lookupMap?: { [index: string]: string } | undefined; + /** + * etagMethod - specifies the method used to calculate the ETag header response. Available values: + * * 'hash' - SHA1 sum of the file contents, suitable for distributed deployments. Default value. + * * 'simple' - Hex encoded size and modification date, suitable when files are stored on a single server. + * * false - Disable ETag computation. + */ + etagMethod?: 'hash' | 'simple' | false | undefined; + /** + * start - offset in file to reading from, defaults to 0. + */ + start?: number | undefined; + /** + * end - offset in file to stop reading from. If not set, will read to end of file. + */ + end?: number | undefined; + } + + interface FileHandlerRouteObject extends ReplyFileHandlerOptions { + /** + * path - a path string or function as described above (required). + */ + path: string | RequestHandler; + } + + interface DirectoryHandlerRouteObject { + /** + * path - (required) the directory root path (relative paths are resolved based on the route files configuration). Value can be: + * * a single path string used as the prefix for any resources requested by appending the request path parameter to the provided string. + * * an array of path strings. Each path will be attempted in order until a match is found (by following the same process as the single path string). + * * a function with the signature function(request) which returns the path string or an array of path strings. + * If the function returns an error, the error is passed back to the client in the response. + */ + path: string | string[] | RequestHandler; + /** + * index - optional boolean|string|string[], determines if an index file will be served if found in the folder when requesting a directory. + * The given string or strings specify the name(s) of the index file to look for. If true, looks for 'index.html'. + * Any falsy value disables index file lookup. Defaults to true. + */ + index?: boolean | string | string[] | undefined; + /** + * listing - optional boolean, determines if directory listing is generated when a directory is requested without an index document. Defaults to false. + */ + listing?: boolean | undefined; + /** + * showHidden - optional boolean, determines if hidden files will be shown and served. Defaults to false. + */ + showHidden?: boolean | undefined; + /** + * redirectToSlash - optional boolean, determines if requests for a directory without a trailing slash are redirected to the same path with the missing slash. + * Useful for ensuring relative links inside the response are resolved correctly. + * Disabled when the server config router.stripTrailingSlash is true.Defaults to false. + */ + redirectToSlash?: boolean | undefined; + /** + * lookupCompressed - optional boolean, instructs the file processor to look for the same filename with the '.gz' suffix for a pre-compressed + * version of the file to serve if the request supports content encoding. Defaults to false. + */ + lookupCompressed?: boolean | undefined; + /** + * etagMethod - specifies the method used to calculate the ETag header response. Available values: + * * 'hash' - SHA1 sum of the file contents, suitable for distributed deployments. Default value. + * * 'simple' - Hex encoded size and modification date, suitable when files are stored on a single server. + * * false - Disable ETag computation. + */ + etagMethod?: 'hash' | 'simple' | false | undefined; + /** + * defaultExtension - optional string, appended to file requests if the requested file is not found. Defaults to no extension. + */ + defaultExtension?: string | undefined; + } + + /** + * inert accepts the following registration options + * @see {@link https://github.com/hapijs/inert#registration-options} + */ + interface OptionalRegistrationOptions { + /** + * sets the maximum number of file etag hash values stored in the etags cache. Defaults to 10000. + */ + etagsCacheMaxSize?: number | undefined; + } +} + +declare const inert: { plugin: Plugin }; + +export = inert; + +declare module '@hapi/hapi' { + interface HandlerDecorations { + /** + * The file handler + * + * Generates a static file endpoint for serving a single file. file can be set to: + * * a relative or absolute file path string (relative paths are resolved based on the route files configuration). + * * a function with the signature function(request) which returns the relative or absolute file path. + * * an object with one or more of the following options @see IFileHandler + * @see {@link https://github.com/hapijs/inert#the-file-handler} + */ + file?: string | inert.RequestHandler | inert.FileHandlerRouteObject | undefined; + /** + * The directory handler + * + * Generates a directory endpoint for serving static content from a directory. + * Routes using the directory handler must include a path parameter at the end of the path string + * (e.g. /path/to/somewhere/{param} where the parameter name does not matter). + * The path parameter can use any of the parameter options (e.g. {param} for one level files only, + * {param?} for one level files or the directory root, {param*} for any level, or {param*3} for a specific level). + * If additional path parameters are present, they are ignored for the purpose of selecting the file system resource. + * The directory handler is an object with the following options: + * @see {@link https://github.com/hapijs/inert#the-directory-handler} + */ + directory?: inert.DirectoryHandlerRouteObject | undefined; + files?: + | { + /** + * Set the relative path + */ + relativeTo: string; + } + | undefined; + } + + interface ResponseToolkit { + /** + * Transmits a file from the file system. The 'Content-Type' header defaults to the matching mime type based on filename extension. + * @see {@link https://github.com/hapijs/inert#replyfilepath-options} + */ + file(path: string, options?: inert.ReplyFileHandlerOptions): ResponseObject; + } +} diff --git a/package.json b/package.json index de36283..5b5954a 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "version": "7.0.1", "repository": "https://github.com/hapijs/inert.git", "main": "lib/index.js", + "types": "lib/index.d.ts", "files": [ "lib" ], @@ -31,11 +32,14 @@ "@hapi/code": "^9.0.3", "@hapi/eslint-plugin": "*", "@hapi/file": "^3.0.0", - "@hapi/hapi": "^21.2.1", - "@hapi/lab": "^25.1.2" + "@hapi/hapi": "^21.3.0", + "@hapi/lab": "^25.1.2", + "@types/node": "^14.18.37", + "joi": "^17.8.3", + "typescript": "^4.9.5" }, "scripts": { - "test": "lab -f -a @hapi/code -t 100 -L", + "test": "lab -f -a @hapi/code -t 100 -L -Y", "test-cov-html": "lab -f -a @hapi/code -r html -o coverage.html" }, "license": "BSD-3-Clause" diff --git a/test/index.ts b/test/index.ts new file mode 100644 index 0000000..1d23d96 --- /dev/null +++ b/test/index.ts @@ -0,0 +1,107 @@ +import * as path from 'path'; + +import { Server, Lifecycle } from '@hapi/hapi'; +import * as inert from '../'; +import * as Lab from '@hapi/lab'; + +const { expect: check } = Lab.types; + +const server = new Server({ + port: 3000, + routes: { + files: { + relativeTo: path.join(__dirname, 'public'), + }, + }, +}); + +const provision = async () => { + await server.register(inert); + await server.register({ plugin: inert.plugin, options: { etagsCacheMaxSize: 100 } }); + + server.route({ + method: 'GET', + path: '/{param*}', + handler: { + directory: { + path: '.', + redirectToSlash: true, + index: true, + }, + }, + }); + + server.route({ + method: 'GET', + path: '/{param*}', + handler(request, h) { + check.type(h.file); + return h.file('awesome.png', { + confine: './images', + }); + }, + }); + + // https://github.com/hapijs/inert#serving-a-single-file + server.route({ + method: 'GET', + path: '/{path*}', + handler: { + file: 'page.html', + }, + }); + + // https://github.com/hapijs/inert#customized-file-response + server.route({ + method: 'GET', + path: '/file', + handler(request, h) { + let path = 'plain.txt'; + if (request.headers['x-magic'] === 'sekret') { + path = 'awesome.png'; + } + + return h.file(path).vary('x-magic'); + }, + }); + + const handler: Lifecycle.Method = (request, h) => { + const response = request.response; + if (response instanceof Error && response.output.statusCode === 404) { + return h.file('404.html').code(404); + } + + return h.continue; + }; + + server.ext('onPostHandler', handler); + + const file: inert.FileHandlerRouteObject = { + path: '', + confine: true, + }; + + const directory: inert.DirectoryHandlerRouteObject = { + path: '', + listing: true, + }; + + server.route({ + path: '', + method: 'GET', + handler: { + file, + directory: { + path() { + if (Math.random() > 0.5) { + return ''; + } else if (Math.random() > 0) { + return ['']; + } + return new Error(''); + }, + }, + }, + options: { files: { relativeTo: __dirname } }, + }); +};