diff --git a/.github/workflows/bump-version.yml b/.github/workflows/bump-version.yml new file mode 100644 index 0000000..bdd47aa --- /dev/null +++ b/.github/workflows/bump-version.yml @@ -0,0 +1,29 @@ +name: Bump Version on Merge to Main + +on: + push: + branches: [main] + +jobs: + bump-version: + runs-on: ubuntu-latest + + if: "!contains(github.event.head_commit.message, 'ci: bump version')" # skip version bump commits + + steps: + - name: Checkout repo + uses: actions/checkout@v4 + with: + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: Bump version (patch) + run: | + git config --global user.email "ci-bot@tellihealth.com" + git config --global user.name "CI Bot" + npm version patch -m "ci: bump version to %s" + git push origin main --follow-tags diff --git a/.github/workflows/node.yml b/.github/workflows/node.yml index 0fcd3be..da609b6 100644 --- a/.github/workflows/node.yml +++ b/.github/workflows/node.yml @@ -15,13 +15,15 @@ jobs: strategy: matrix: - node-version: [20.x, 22.x] + node-version: [20.x] # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ + if: "!contains(github.event.head_commit.message, 'ci: bump version')" # skip version bump commits + steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} cache: "npm" diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml index e739a1d..d416e80 100644 --- a/.github/workflows/npm-publish.yml +++ b/.github/workflows/npm-publish.yml @@ -8,26 +8,18 @@ on: types: [created] jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 - with: - node-version: 20 - - run: npm ci - # - run: npm test - publish-npm: - needs: build runs-on: ubuntu-latest + strategy: + matrix: + node-version: [20.x] steps: - - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 with: - node-version: 20 - registry-url: https://registry.npmjs.org/ + node-version: ${{ matrix.node-version }} + registry-url: https://npm.pkg.github.com/ - run: npm ci - run: npm publish env: - NODE_AUTH_TOKEN: ${{secrets.npm_token}} + NODE_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}} diff --git a/README.md b/README.md index 5a64520..96f4418 100644 --- a/README.md +++ b/README.md @@ -25,14 +25,14 @@ To add this plugin to your package.json: **Using npm:** ```bash -npm install --save-dev serverless-openapi-documenter +npm install --save-dev @telllihealth/serverless-openapi-documenter ``` Next you need to add the plugin to the `plugins` section of your `serverless.yml` file. ```yml plugins: - - serverless-openapi-documenter + - "@telllihealth/serverless-openapi-documenter" ``` > Note: Add this plugin _after_ `serverless-offline` to prevent issues with `String.replaceAll` being overridden incorrectly. @@ -103,6 +103,8 @@ Options: | tags[].externalDocs.url | `custom.documentation.tags.externalDocumentation.url` | | tags[].externalDocs.description | `custom.documentation.tags.externalDocumentation.description` | | tags[].externalDocs.x- | `custom.documentation.tags.externalDocumentation.x-` if extended specifications provided | +| basePath | `custom.documentation.basePath`Specifies the base path to prepend to all Lambda HTTP function paths in the generated OpenAPI specification | +| paths | `custom.documentation.paths` OpenAPI paths that are not backed by Lambda functions | | path[path] | functions.functions.events.[http OR httpApi].path | | path[path].servers[].description | functions.functions.servers.description | | path[path].servers[].url | functions.functions.servers.url | @@ -545,6 +547,53 @@ functions: application/json: ${file(create_request.json)} ``` +#### Adding non-Lambda endpoints with `paths` + +You can add OpenAPI paths that are not backed by Lambda functions using the `paths` field under `custom.documentation`. This is useful for documenting endpoints handled outside of Serverless functions, such as static assets, third-party integrations, or legacy APIs. + +The `paths` field should follow the [OpenAPI Paths Object](https://spec.openapis.org/oas/v3.0.4#paths-object) structure. Any paths defined here will be merged into the generated OpenAPI document alongside Lambda-backed endpoints. + +**Example:** + +```yml +custom: + documentation: + title: My API + version: "1" + paths: + /static/health: + get: + summary: Health check for static endpoint + description: Returns status of the static service + responses: + "200": + description: OK + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: ok + /external/webhook: + post: + summary: Webhook endpoint + description: Receives webhook calls from external service + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + event: + type: string + responses: + "204": + description: No Content +``` + #### Functions To define the documentation for a given function event, you need to create a `documentation` attribute for your `http` or `httpApi` event in your `serverless.yml` file. diff --git a/package-lock.json b/package-lock.json index cfeb455..cb704e6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -484,20 +484,22 @@ } }, "node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } }, "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, + "license": "MIT", "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" @@ -1045,10 +1047,11 @@ } }, "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, + "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" }, @@ -1445,6 +1448,7 @@ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.12.0" } @@ -2655,6 +2659,7 @@ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "dev": true, + "license": "MIT", "dependencies": { "is-number": "^7.0.0" }, @@ -3369,20 +3374,20 @@ "dev": true }, "brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "requires": { "balanced-match": "^1.0.0" } }, "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, "requires": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" } }, "browser-stdout": { @@ -3771,9 +3776,9 @@ "integrity": "sha512-RLoqTXE8/vPmMuTI88DAzhMYC99I8BWv7zYP4A1puo5HIjEJ5EX48ighy4ZyKMG9EDXxBgW6e++cn7d1xuFghA==" }, "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, "requires": { "to-regex-range": "^5.0.1" diff --git a/package.json b/package.json index 4ec37dd..f4183d6 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "serverless-openapi-documenter", + "name": "@tellihealth/serverless-openapi-documenter", "version": "0.0.113", "description": "Generate OpenAPI v3 documentation and Postman Collections from your Serverless Config", "main": "index.js", @@ -38,10 +38,10 @@ }, "repository": { "type": "git", - "url": "https://github.com/JaredCE/serverless-openapi-documenter.git" + "url": "https://github.com/tellihealth/serverless-openapi-documenter.git" }, "bugs": { - "url": "https://github.com/JaredCE/serverless-openapi-documenter/issues" + "url": "https://github.com/tellihealth/serverless-openapi-documenter/issues" }, "license": "MIT", "dependencies": { @@ -62,4 +62,4 @@ "nock": "^14.0.2", "sinon": "^20.0.0" } -} +} \ No newline at end of file diff --git a/src/definitionGenerator.js b/src/definitionGenerator.js index 9bfb223..980fc51 100644 --- a/src/definitionGenerator.js +++ b/src/definitionGenerator.js @@ -127,9 +127,13 @@ class DefinitionGenerator { } } - await this.createPaths().catch((err) => { - throw err; - }); + await this.createPaths() + .then(() => { + this.mergeExistingPaths(); + }) + .catch((err) => { + throw err; + }); this.cleanupLinks(); @@ -255,6 +259,13 @@ class DefinitionGenerator { slashPath = `/${(event?.http?.path || event.httpApi?.path) ?? ""}`; } + const basePath = this.getBasePath(); + + if (basePath) { + // append the base path so server is just the pure domain + slashPath = `/${basePath}${slashPath}`; + } + if (paths[slashPath]) { Object.assign(paths[slashPath], path); } else { @@ -266,6 +277,38 @@ class DefinitionGenerator { Object.assign(this.openAPI, { paths }); } + mergeExistingPaths() { + const paths = this.serverless.service.custom.documentation.paths; + + if (paths) { + const basePath = this.getBasePath(); + + if (basePath) { + // check is paths's keys are not starting with `basePath` prefix and if set append it + for (const key of Object.keys(paths)) { + if (!key.startsWith(`/${basePath}`)) { + paths[`/${basePath}${key}`] = paths[key]; + delete paths[key]; + } + } + } + + const origPaths = this.openAPI.paths || {}; + + Object.assign(this.openAPI, { paths: { ...origPaths, ...paths } }); + } + } + + /** + * @description Retrieves the basePath value if set, allowing the server URL to be just the plain domain. The `basePath` will be prepended to each Lambda HTTP path. If `basePath` starts with a slash (/) returns the basePath without the leading slash. + * @returns {string} + */ + getBasePath() { + const basePath = + this.serverless.service.custom.documentation.basePath || ""; + return basePath.replace(/^\//, ""); + } + createServers(servers) { const serverDoc = servers; const newServers = []; diff --git a/src/owasp.js b/src/owasp.js index 5189e5c..a1eb611 100644 --- a/src/owasp.js +++ b/src/owasp.js @@ -59,6 +59,9 @@ class OWASP { description: "The X-Content-Type-Options response HTTP header is a marker used by the server to indicate that the [MIME types](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types) advertised in the [Content-Type](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type) headers should be followed and not be changed. The header allows you to avoid [MIME type sniffing](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types#mime_sniffing) by saying that the MIME types are deliberately configured. - [MDN Link](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Content-Type-Options)", }, + "X-DNS-Prefetch-Control": { + description: "Controls DNS prefetching.", + }, "X-Frame-Options": { description: "The X-Frame-Options [HTTP](https://developer.mozilla.org/en-US/docs/Web/HTTP) response header can be used to indicate whether or not a browser should be allowed to render a page in a [](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/frame), [