{"id":13527545,"url":"https://github.com/avoidwork/tenso","last_synced_at":"2025-05-16T08:07:04.837Z","repository":{"id":19298018,"uuid":"22535404","full_name":"avoidwork/tenso","owner":"avoidwork","description":"Tenso is an HTTP REST API framework","archived":false,"fork":false,"pushed_at":"2025-05-12T17:51:13.000Z","size":5489,"stargazers_count":172,"open_issues_count":3,"forks_count":6,"subscribers_count":6,"default_branch":"master","last_synced_at":"2025-05-16T00:04:08.618Z","etag":null,"topics":["api","api-gateway","cqrs","gateway","microservice","microservices","rest"],"latest_commit_sha":null,"homepage":"","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"bsd-3-clause","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/avoidwork.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null},"funding":{"github":["avoidwork"]}},"created_at":"2014-08-02T01:05:00.000Z","updated_at":"2025-05-12T17:51:16.000Z","dependencies_parsed_at":"2023-12-28T13:53:18.101Z","dependency_job_id":"8c22842e-19f8-446a-babc-5e4e8fc5ee47","html_url":"https://github.com/avoidwork/tenso","commit_stats":{"total_commits":1360,"total_committers":5,"mean_commits":272.0,"dds":0.09411764705882353,"last_synced_commit":"d60a883a071702ff9b07b0c202871cfc7dcd7882"},"previous_names":[],"tags_count":548,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/avoidwork%2Ftenso","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/avoidwork%2Ftenso/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/avoidwork%2Ftenso/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/avoidwork%2Ftenso/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/avoidwork","download_url":"https://codeload.github.com/avoidwork/tenso/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":254493385,"owners_count":22080127,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["api","api-gateway","cqrs","gateway","microservice","microservices","rest"],"created_at":"2024-08-01T06:01:50.767Z","updated_at":"2025-05-16T08:06:59.822Z","avatar_url":"https://github.com/avoidwork.png","language":"JavaScript","readme":"# Tenso\n\nTenso is an HTTP REST API framework, that will handle the serialization \u0026 creation of hypermedia links; all you have to do is give it `Arrays` or `Objects`.\n\n## Example\nCreating an API with Tenso can be this simple:\n\n```javascript\nimport {tenso} from \"tenso\";\n\nexport const app = tenso();\n\napp.get(\"/\", \"Hello, World!\");\napp.start();\n```\n\n### Creating Routes\nRoutes are loaded as a module, with each HTTP method as an export, affording a very customizable API server.\n\nYou can use `res` to `res.send(body[, status, headers])`, `res.redirect(url)`, or `res.error(status[, Error])`. \n\nThe following example will create GET routes that will return an `Array` at `/`, an `Error` at `/reports/tps`, \u0026 a version 4 UUID at `/uuid`.\n\nAs of 10.3.0 you can specify `always` as a method to run middleware before authorization middleware, which will skip `always` middleware registered after it (via instance methods).\n\nAs of 17.2.0 you can have routes exit the middleware pipeline immediately by setting them in the `exit` Array. This differs from `unprotect` as there is no request body handling.\n\n#### Example\n\n##### Routes\n\n```javascript\nimport {randomUUID as uuid} from \"crypto\";\n\nexport const initRoutes = {\n\t\"get\": {\n\t\t\"/\": [\"reports\", \"uuid\"],\n\t\t\"/reports\": [\"tps\"],\n\t\t\"/reports/tps\": (req, res) =\u003e res.error(785, Error(\"TPS Cover Sheet not attached\")),\n\t\t\"/uuid\": (req, res) =\u003e res.send(uuid(), 200, {\"cache-control\": \"no-cache\"})\n\t}\n};\n```\n\n##### Server\n\n```javascript\nimport {tenso} from \"tenso\";\nimport {initRoutes} from \"./routes\";\n\nexport const app = tenso({initRoutes});\n\napp.start();\n```\n\n#### Protected Routes\nProtected routes are routes that require authorization for access, and will redirect to authentication end points if needed.\n\n#### Unprotected Routes\nUnprotected routes are routes that do not require authorization for access, and will exit the authorization pipeline early to avoid rate limiting, csrf tokens, \u0026 other security measures. These routes are the DMZ of your API! _You_ **must** secure these end points with alternative methods if accepting input!\n\n#### Reserved Route\nThe `/assets/*` route is reserved for the HTML browsable interface assets; please do not try to reuse this for data.\n\n### Request Helpers\nTenso decorates `req` with \"helpers\" such as `req.allow`, `req.csrf`, `req.ip`, `req.parsed`, \u0026 `req.private`. `PATCH`, `PUT`, \u0026 `POST` payloads are available as `req.body`. Sessions are available as `req.session` when using `local` authentication.\n\nTenso decorates `res` with \"helpers\" such as `res.send()`, `res.status()`, \u0026 `res.json()`.\n\n## Extensibility\nTenso is extensible, and can be customized with custom parsers, renderers, \u0026 serializers.\n\n### Parsers\nCustom parsers can be registered with `server.parser('mimetype', fn);` or directly on `server.parsers`. The parameters for a parser are `(arg)`.\n\nTenso has parsers for:\n\n- `application/json`\n- `application/x-www-form-urlencoded`\n- `application/jsonl`\n- `application/json-lines`\n- `text/json-lines`\n\n### Renderers\nCustom renderers can be registered with `server.renderer('mimetype', fn);`. The parameters for a renderer are `(req, res, arg)`.\n\nTenso has renderers for:\n\n- `application/javascript`\n- `application/json`\n- `application/jsonl`\n- `application/json-lines`\n- `text/json-lines`\n- `application/yaml`\n- `application/xml`\n- `text/csv`\n- `text/html`\n\n### Serializers\nCustom serializers can be registered with `server.serializer('mimetype', fn);`. The parameters for a serializer are `(arg, err, status = 200, stack = false)`.\n\nTenso has two default serializers which can be overridden:\n\n- `plain` for plain text responses\n- `custom` for standard response shape\n\n```json\n{\n  \"data\": \"`null` or ?\",\n  \"error\": \"`null` or an `Error` stack trace / message\",\n  \"links\": [],\n  \"status\": 200\n}\n```\n\n## Responses\nResponses will have a standard shape, and will be utf-8 by default. The result will be in `data`. Hypermedia (pagination \u0026 links) will be in `links:[ {\"uri\": \"...\", \"rel\": \"...\"}, ...]`, \u0026 also in the `Link` HTTP header.\n\nPage size can be specified via the `page_size` parameter, e.g. `?page_size=25`.\n\nSort order can be specified via then `order-by` which accepts `[field ]asc|desc` \u0026 can be combined like an SQL 'ORDER BY', e.g. `?order_by=desc` or `?order_by=lastName%20asc\u0026order_by=firstName%20asc\u0026order_by=age%20desc`\n\n## REST / Hypermedia\nHypermedia is a prerequisite of REST, and is best described by the [Richardson Maturity Model](http://martinfowler.com/articles/richardsonMaturityModel.html). Tenso will automagically paginate Arrays of results, or parse Entity representations for keys that imply\nrelationships, and create the appropriate Objects in the `link` Array, as well as the `Link` HTTP header. Object keys that match this pattern: `/_(guid|uuid|id|uri|url)$/` will be considered\nhypermedia links.\n\nFor example, if the key `user_id` was found, it would be mapped to `/users/:id` with a link `rel` of `related`.\n\nTenso will bend the rules of REST when using authentication strategies provided by passport.js, or CSRF if is enabled, because they rely on a session. Session storage is in memory, or Redis. You have the option of a stateless or stateful API.\n\nHypermedia processing of the response body can be disabled as of `10.2.0`, by setting `req.hypermedia = false` and/or `req.hypermediaHeader` via middleware.\n\n## Configuration\nThis is the default configuration for Tenso, without authentication or SSL. This would be ideal for development, but not production! Enabling SSL is as easy as providing file paths for the two keys.\n\nEverything is optional! You can provide as much, or as little configuration as you like.\n\n```\n{\n\tauth: {\n\t\tdelay: 0,\n\t\tprotect: [],\n\t\tunprotect: [],\n\t\tbasic: {\n\t\t\tenabled: false,\n\t\t\tlist: []\n\t\t},\n\t\tbearer: {\n\t\t\tenabled: false,\n\t\t\ttokens: []\n\t\t},\n\t\tjwt: {\n\t\t\tenabled: false,\n\t\t\tauth: null,\n\t\t\taudience: EMPTY,\n\t\t\talgorithms: [\n\t\t\t\t\"HS256\",\n\t\t\t\t\"HS384\",\n\t\t\t\t\"HS512\"\n\t\t\t],\n\t\t\tignoreExpiration: false,\n\t\t\tissuer: \"\",\n\t\t\tscheme: \"bearer\",\n\t\t\tsecretOrKey: \"\"\n\t\t},\n\t\tmsg: {\n\t\t\tlogin: \"POST 'username' \u0026 'password' to authenticate\"\n\t\t},\n\t\toauth2: {\n\t\t\tenabled: false,\n\t\t\tauth: null,\n\t\t\tauth_url: \"\",\n\t\t\ttoken_url: \"\",\n\t\t\tclient_id: \"\",\n\t\t\tclient_secret: \"\"\n\t\t},\n\t\turi: {\n\t\t\tlogin: \"/auth/login\",\n\t\t\tlogout: \"/auth/logout\",\n\t\t\tredirect: \"/\",\n\t\t\troot: \"/auth\"\n\t\t},\n\t\tsaml: {\n\t\t\tenabled: false,\n\t\t\tauth: null\n\t\t}\n\t},\n\tautoindex: false,\n\tcacheSize: 1000,\n\tcacheTTL: 300000,\n\tcatchAll: true,\n\tcharset: \"utf-8\",\n\tcorsExpose: \"cache-control, content-language, content-type, expires, last-modified, pragma\",\n\tdefaultHeaders: {\n\t\t\"content-type\": \"application/json; charset=utf-8\",\n\t\t\"vary\": \"accept, accept-encoding, accept-language, origin\"\n\t},\n\tdigit: 3,\n\tetags: true,\n\texit: [],\n\thost: \"0.0.0.0\",\n\thypermedia: {\n\t\tenabled: true,\n\t\theader: true\n\t},\n\tindex: [],\n\tinitRoutes: {},\n\tjsonIndent: 0,\n\tlogging: {\n\t\tenabled: true,\n\t\tformat: \"%h %l %u %t \\\"%r\\\" %\u003es %b\",\n\t\tlevel: \"debug\",\n\t\tstack: true\n\t},\n\tmaxBytes: 0,\n\tmimeType: \"application/json\",\n\torigins: [\"*\"],\n\tpageSize: 5,\n\tport: 8000,\n\tprometheus: {\n\t\tenabled: false,\n\t\tmetrics: {\n\t\t\tincludeMethod: true,\n\t\t\tincludePath: true,\n\t\t\tincludeStatusCode: true,\n\t\t\tincludeUp: true,\n\t\t\tbuckets: [0.001, 0.01, 0.1, 1, 2, 3, 5, 7, 10, 15, 20, 25, 30, 35, 40, 50, 70, 100, 200],\n\t\t\tcustomLabels: {}\n\t\t}\n\t},\n\trate: {\n\t\tenabled: false,\n\t\tlimit: 450,\n\t\tmessage: \"Too many requests\",\n\t\toverride: null,\n\t\treset: 900,\n\t\tstatus: 429\n\t},\n\trenderHeaders: true,\n\ttime: true,\n\tsecurity: {\n\t\tkey: \"x-csrf-token\",\n\t\tsecret: \"\",\n\t\tcsrf: true,\n\t\tcsp: null,\n\t\txframe: \"\",\n\t\tp3p: \"\",\n\t\thsts: null,\n\t\txssProtection: true,\n\t\tnosniff: true\n\t},\n\tsession: {\n\t\tcookie: {\n\t\t\thttpOnly: true,\n\t\t\tpath: \"/\",\n\t\t\tsameSite: true,\n\t\t\tsecure: \"auto\"\n\t\t},\n\t\tname: \"tenso.sid\",\n\t\tproxy: true,\n\t\tredis: {\n\t\t\thost: \"127.0.0.1\",\n\t\t\tport: 6379\n\t\t},\n\t\trolling: true,\n\t\tresave: true,\n\t\tsaveUninitialized: true,\n\t\tsecret: \"tensoABC\",\n\t\tstore: \"memory\"\n\t},\n\tsilent: false,\n\tssl: {\n\t\tcert: null,\n\t\tkey: null,\n\t\tpfx: null\n\t},\n\twebroot: {\n\t\troot: \"process.cwd()/www\",\n\t\tstatic: \"/assets\",\n\t\ttemplate: \"template.html\"\n\t}\n}\n```\n\n## Authentication\nThe `protect` Array is the endpoints that will require authentication. The `redirect` String is the end point users will be redirected to upon successfully authenticating, the default is `/`.\n\nSessions are used for non `Basic` or `Bearer Token` authentication, and will have `/login`, `/logout`, \u0026 custom routes. Redis is supported for session storage.\n\nMultiple authentication strategies can be enabled at once.\n\nAuthentication attempts have a random delay to deal with \"timing attacks\"; always rate limit in production environment!\n\n### Basic Auth\n```\n{\n\t\"auth\": {\n\t\t\"basic\": {\n\t\t\t\"enabled\": true,\n\t\t\t\"list\": [\"username:password\", ...],\n\t\t},\n\t\t\"protect\": [\"/\"]\n\t}\n}\n```\n\n### JSON Web Token\nJSON Web Token (JWT) authentication is stateless and does not have an entry point. The `auth(token, callback)` function must verify `token.sub`, and must execute `callback(err, user)`.\n\nThis authentication strategy relies on out-of-band information for the `secret`, and other optional token attributes.\n\n```\n{\n\t\"auth\": {\n\t\t\"jwt\": {\n\t\t\t\"enabled\": true,\n\t\t\t\"auth\": function (token, cb) { ... }, /* Authentication handler, to 'find' or 'create' a User */\n\t\t\t\"algorithms\": [], /* Optional signing algorithms, defaults to [\"HS256\", \"HS384\", \"HS512\"] */\n\t\t\t\"audience\": \"\", /* Optional, used to verify `aud` */\n\t\t\t\"issuer: \"\", /* Optional, used to verify `iss` */\n\t\t\t\"ignoreExpiration\": false, /* Optional, set to `true` to ignore expired tokens */\n\t\t\t\"scheme\": \"Bearer\", /* Optional, set to specify the `Authorization` scheme */\n\t\t\t\"secretOrKey\": \"\"\n\t\t}\n\t\t\"protect\": [\"/private\"]\n\t}\n}\n```\n\n### OAuth2\nOAuth2 authentication will create `/auth`, `/auth/oauth2`, \u0026 `/auth/oauth2/callback` routes. `auth(accessToken, refreshToken, profile, callback)` must execute `callback(err, user)`.\n \n```\n{\n\t\"auth\": {\n\t\t\"oauth2\": {\n\t\t\t\"enabled\": true,\n\t\t\t\"auth\": function ( ... ) { ... }, /* Authentication handler, to 'find' or 'create' a User */\n\t\t\t\"auth_url\": \"\", /* Authorization URL */\n\t\t\t\"token_url\": \"\", /* Token URL */\n\t\t\t\"client_id\": \"\", /* Get this from authorization server */\n\t\t\t\"client_secret\": \"\" /* Get this from authorization server */\n\t\t},\n\t\t\"protect\": [\"/private\"]\n\t}\n}\n```\n\n### Oauth2 Bearer Token\n```\n{\n\t\"auth\": {\n\t\t\"bearer\": {\n\t\t\t\"enabled\": true,\n\t\t\t\"tokens\": [\"abc\", ...]\n\t\t},\n\t\t\"protect\": [\"/\"]\n\t}\n}\n```\n\n### SAML\nSAML authentication will create `/auth`, `/auth/saml`, \u0026 `/auth/saml/callback` routes. `auth(profile, callback)` must execute `callback(err, user)`.\n\nTenso uses [passport-saml](https://github.com/bergie/passport-saml), for configuration options please visit it's homepage.\n \n```\n{\n\t\"auth\": {\n\t\t\"saml\": {\n\t\t\t\"enabled\": true,\n\t\t\t...\n\t\t},\n\t\t\"protect\": [\"/private\"]\n\t}\n}\n```\n\n## Sessions\nSessions can use a memory (default) or redis store. Memory will limit your sessions to a single server instance, while redis will allow you to share sessions across a cluster of processes, or machines. To use redis, set the `store` property to \"redis\".\n\nIf the session `secret` is not provided, a version 4 `UUID` will be used.\n\n```\n{\n\t\"session\" : {\n\t\tcookie: {\n\t\t\thttpOnly: true,\n\t\t\tpath: \"/\",\n\t\t\tsameSite: true,\n\t\t\tsecure: false\n\t\t},\n\t\tname: \"tenso.sid\",\n\t\tproxy: true,\n\t\tredis: {\n\t\t\thost: \"127.0.0.1\",\n\t\t\tport: 6379\n\t\t},\n\t\trolling: true,\n\t\tresave: true,\n\t\tsaveUninitialized: true,\n\t\tsecret: \"tensoABC\",\n\t\tstore: \"memory\"\n\t}\n}\n```\n\n\n## Security\nTenso uses [lusca](https://github.com/krakenjs/lusca#api) for security as a middleware. Please see it's documentation for how to configure it; each method \u0026 argument is a key:value pair for `security`.\n\n```\n{\n\t\"security\": { ... }\n}\n```\n\n## Rate Limiting\nRate limiting is controlled by configuration, and is disabled by default. Rate limiting is based on `token`, `session`, or `ip`, depending upon authentication method.\n\nRate limiting can be overridden by providing an `override` function that takes `req` \u0026 `rate`, and must return (a modified) `rate`.\n\n```\n{\n\t\"rate\": {\n\t\t\"enabled\": true,\n\t\t\"limit\": 450, /* Maximum requests allowed before `reset` */\n\t\t\"reset\": 900, /* TTL in seconds */\n\t\t\"status\": 429, /* Optional HTTP status */\n\t\t\"message\": \"Too many requests\",  /* Optional error message */\n\t\t\"override\": function ( req, rate ) { ... } /* Override the default rate limiting */\n\t}\n}\n```\n\n## Limiting upload size\nA 'max byte' limit can be enforced on all routes that handle `PATCH`, `POST`, \u0026 `PUT` requests. The default limit is 20 KB (20480 B).\n\n```\n{\n\t\"maxBytes\": 5242880\n}\n```\n\n## Logging\nStandard log levels are supported, and are emitted to `stdout` \u0026 `stderr`. Stack traces can be enabled.\n\n```\n{\n\t\"logging\": {\n\t\t\"level\": \"warn\",\n\t\t\"enabled\": true,\n\t\t\"stack\": true\n\t}\n}\n```\n\n## HTML Renderer\nThe HTML template can be overridden with a custom HTML document.\n\nDark mode is supported! The `dark` class will be added to the `body` tag if the user's browser is in dark mode.\n\n```\nwebroot: {\n    root: \"full path\",\n    static: \"folder to serve static assets\",\n    template: \"html template\"\n}\n```\n\n## Serving files\nCustom file routes can be created like this:\n\n```\napp.files(\"/folder\", \"/full/path/to/parent\");\n```\n\n## EventSource streams\nCreate \u0026 cache an `EventSource` stream to send messages to a Client. See [tiny-eventsource](https://github.com/avoidwork/tiny-eventsource) for configuration options:\n\n```\nconst streams = new Map();\n\n...\n\n\"/stream\": (req, res) =\u003e {\n const id = req.user.userId;\n\n if (streams.has(id) === false) {\n   streams.set(id, req.server.eventsource({ms: 3e4), \"initialized\");\n }\n\n streams.get(id).init(req, res);\n}\n\n...\n\n// Send data to Clients\nstreams.get(id).send({...});\n```\n\n## Prometheus\n\nPrometheus metrics can be enabled by setting `{prometheus: {enabled: true}}`. The metrics will be available at `/metrics`.\n\n## Testing\n\nTenso has ~80% code coverage with its tests. Test coverage will be added in the future.\n\n```console\n-----------|---------|----------|---------|---------|-------------------------------------------------------------------------------------------------------------------------------\nFile       | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s\n-----------|---------|----------|---------|---------|-------------------------------------------------------------------------------------------------------------------------------\nAll files  |   78.15 |    54.93 |   68.75 |   78.58 |                                                                                                                               \n tenso.cjs |   78.15 |    54.93 |   68.75 |   78.58 | ...85,1094,1102,1104,1115-1118,1139,1149-1175,1196-1200,1243-1251,1297-1298,1325-1365,1370,1398-1406,1412-1413,1425,1455-1456 \n-----------|---------|----------|---------|---------|-------------------------------------------------------------------------------------------------------------------------------\n```\n\n## Benchmark\n\n1. Clone repository from [GitHub](https://github.com/avoidwork/tenso).\n1. Install dependencies with `npm` or `yarn`.\n1. Execute `benchmark` script with `npm` or `yarn`.\n\n## License\nCopyright (c) 2024 Jason Mulligan\n\nLicensed under the BSD-3-Clause license.\n","funding_links":["https://github.com/sponsors/avoidwork"],"categories":["JavaScript","rest"],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Favoidwork%2Ftenso","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Favoidwork%2Ftenso","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Favoidwork%2Ftenso/lists"}