├── .eslintrc ├── .github ├── ISSUE_TEMPLATE │ ├── bug-report.md │ └── feature-request.md ├── pull-request-template.md └── workflows │ ├── api-docs.yaml │ ├── nodejs-ci-action.yml │ ├── publish-to-npm.yml │ ├── release-please.yml │ └── stale.yml ├── .gitignore ├── .gitmodules ├── .npmignore ├── .remarkrc ├── .vscode └── tasks.json ├── API_TRANSITION_GUIDE.md ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── MAINTAINERS.md ├── README.md ├── RELEASING.md ├── cucumber.js ├── examples ├── express-ex │ ├── README.md │ ├── index.js │ └── package.json ├── mqtt-ex │ ├── README.md │ ├── package.json │ ├── src │ │ └── index.ts │ └── tsconfig.json ├── payload │ ├── data-0.json │ ├── data-1.txt │ ├── v03 │ │ ├── structured-event-0.json │ │ ├── structured-event-1.json │ │ └── structured-event-2.json │ └── v1 │ │ ├── structured-event-0.json │ │ ├── structured-event-1.json │ │ └── structured-event-2.json ├── typescript-ex │ ├── README.md │ ├── package.json │ ├── src │ │ └── index.ts │ ├── tsconfig.json │ └── tslint.json └── websocket │ ├── README.md │ ├── client.js │ ├── index.html │ ├── package.json │ └── server.js ├── maintainer_guidelines.md ├── package-lock.json ├── package.json ├── pr_guidelines.md ├── src ├── constants.ts ├── event │ ├── cloudevent.ts │ ├── interfaces.ts │ ├── spec.ts │ └── validation.ts ├── index.ts ├── message │ ├── http │ │ ├── headers.ts │ │ └── index.ts │ ├── index.ts │ ├── kafka │ │ ├── headers.ts │ │ └── index.ts │ └── mqtt │ │ └── index.ts ├── parsers.ts ├── schema │ ├── cloudevent.json │ └── formats.js └── transport │ ├── emitter.ts │ ├── http │ └── index.ts │ └── protocols.ts ├── test ├── conformance │ └── steps.ts └── integration │ ├── batch_test.ts │ ├── ce.png │ ├── cloud_event_test.ts │ ├── constants_test.ts │ ├── emitter_factory_test.ts │ ├── emitter_singleton_test.ts │ ├── kafka_tests.ts │ ├── message_test.ts │ ├── mqtt_tests.ts │ ├── parser_test.ts │ ├── sdk_test.ts │ ├── spec_1_tests.ts │ └── utilities_test.ts ├── tsconfig.browser.json ├── tsconfig.json └── webpack.config.js /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "parserOptions": { 4 | "ecmaVersion": 2015, 5 | "sourceType": "module" 6 | }, 7 | "extends": [ 8 | "plugin:@typescript-eslint/recommended" 9 | ], 10 | "env": { 11 | "es6": true, 12 | "node": true, 13 | "mocha": true 14 | }, 15 | "plugins": [ 16 | "header" 17 | ], 18 | "ignorePatterns": ["**/schema/*"], 19 | "rules": { 20 | "no-var": "error", 21 | "standard/no-callback-literal": "off", 22 | "arrow-spacing": "error", 23 | "arrow-parens": ["error", "always"], 24 | "arrow-body-style": ["error", "as-needed"], 25 | "prefer-template": "error", 26 | "max-len": ["warn", { "code": 120 }], 27 | "no-console": ["error", { 28 | "allow": ["warn", "error"] 29 | }], 30 | "valid-jsdoc": "warn", 31 | "semi": ["error", "always"], 32 | "quotes": ["error", "double", { "allowTemplateLiterals": true }], 33 | "@typescript-eslint/no-explicit-any": "off", 34 | "header/header": [2, "block", ["", " Copyright 2021 The CloudEvents Authors"," SPDX-License-Identifier: Apache-2.0", ""], 2], 35 | "no-unused-vars": "off", 36 | "@typescript-eslint/no-unused-vars": ["error"] 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: Create a report to help us improve this module 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the Bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **Steps to Reproduce** 14 | 1. 15 | 2. 16 | 3. 17 | 18 | **Expected Behavior** 19 | A clear and concise description of what you expected to happen. 20 | 21 | 22 | 23 | **Additional context** 24 | Add any other context about the problem here. If applicable, add screenshots to help explain your problem. 25 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Request 3 | about: Suggest an idea for this module 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. 12 | 13 | **Describe the solution you would like to see** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /.github/pull-request-template.md: -------------------------------------------------------------------------------- 1 | 14 | ## Proposed Changes 15 | 16 | ## Description 17 | -------------------------------------------------------------------------------- /.github/workflows/api-docs.yaml: -------------------------------------------------------------------------------- 1 | name: API Docs 2 | 3 | on: release 4 | 5 | jobs: 6 | publish: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - 10 | name: Checkout 11 | uses: actions/checkout@v4 12 | - 13 | name: Generate API documentation 14 | run: npm install && npm run build:schema && npm run generate-docs 15 | - 16 | name: Deploy to GitHub Pages 17 | if: success() 18 | uses: crazy-max/ghaction-github-pages@v2 19 | with: 20 | target_branch: gh-pages 21 | build_dir: docs 22 | commit_message: | 23 | Deploy to GitHub Pages 24 | Signed-off-by: Lance Ball 25 | allow_empty_commit: false 26 | env: 27 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 28 | -------------------------------------------------------------------------------- /.github/workflows/nodejs-ci-action.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [ main ] 9 | pull_request: 10 | branches: [ main ] 11 | 12 | jobs: 13 | build: 14 | name: Build and test 15 | runs-on: ubuntu-latest 16 | strategy: 17 | matrix: 18 | node-version: [20.x, 22.x, 24.x] 19 | 20 | steps: 21 | - uses: actions/checkout@v4 22 | - name: Test on Node.js ${{ matrix.node-version }} 23 | uses: actions/setup-node@v4 24 | with: 25 | node-version: ${{ matrix.node-version }} 26 | - run: npm ci 27 | - run: npm run build --if-present 28 | - run: npm test 29 | 30 | coverage: 31 | name: Code coverage 32 | runs-on: ubuntu-latest 33 | steps: 34 | - uses: actions/checkout@v4 35 | with: 36 | submodules: true 37 | - name: Generate coverage report 38 | uses: actions/setup-node@v4 39 | with: 40 | node-version: 22.x 41 | - run: npm ci 42 | - run: npm run build --if-present 43 | - run: npm run coverage 44 | - name: Upload coverage report to storage 45 | uses: actions/upload-artifact@v4 46 | with: 47 | name: coverage 48 | path: coverage/lcov.info 49 | 50 | publish: 51 | name: Publish code coverage report 52 | needs: coverage 53 | runs-on: ubuntu-latest 54 | steps: 55 | - uses: actions/checkout@v4 56 | - name: Download coverage report from storage 57 | uses: actions/download-artifact@v4 58 | with: 59 | name: coverage 60 | - name: Upload coverage report to codacy 61 | uses: actions/setup-node@v4 62 | with: 63 | node-version: 22.x 64 | - run: | 65 | ( [[ "${CODACY_PROJECT_TOKEN}" != "" ]] && npm run coverage-publish ) || echo "Coverage report not published" 66 | env: 67 | CODACY_PROJECT_TOKEN: ${{secrets.CODACY_PROJECT_TOKEN}} 68 | -------------------------------------------------------------------------------- /.github/workflows/publish-to-npm.yml: -------------------------------------------------------------------------------- 1 | name: Publish to npmjs 2 | on: 3 | release: 4 | types: [created] 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | permissions: 9 | contents: read 10 | id-token: write 11 | steps: 12 | - uses: actions/checkout@v4 13 | - uses: actions/setup-node@v4 14 | with: 15 | node-version: '22.x' 16 | registry-url: 'https://registry.npmjs.org' 17 | - run: npm install -g npm 18 | - run: npm ci 19 | - run: npm publish --provenance --access public 20 | env: 21 | NODE_AUTH_TOKEN: ${{ secrets.CLOUDEVENTS_PUBLISH }} 22 | -------------------------------------------------------------------------------- /.github/workflows/release-please.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - main 5 | name: release-please 6 | jobs: 7 | release-please: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: GoogleCloudPlatform/release-please-action@v3 11 | id: release 12 | with: 13 | token: ${{ secrets.CLOUDEVENTS_RELEASES_TOKEN }} 14 | release-type: node 15 | package-name: cloudevents 16 | signoff: "Lucas Holmquist " 17 | changelog-types: '[{"type":"feat","section":"Features","hidden":false},{"type":"fix","section":"Bug Fixes","hidden":false},{"type":"docs","section":"Documentation","hidden":false},{"type":"chore","section":"Miscellaneous","hidden":false},{"type":"src","section":"Miscellaneous","hidden":false},{"type":"style","section":"Miscellaneous","hidden":false},{"type":"refactor","section":"Miscellaneous","hidden":false},{"type":"perf","section":"Performance","hidden":false},{"type":"test","section":"Tests","hidden":false}]' 18 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: Issue Triage 2 | 3 | on: 4 | schedule: 5 | - cron: "0 0 * * *" 6 | 7 | jobs: 8 | triage_issues: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/stale@v3 12 | with: 13 | repo-token: ${{ secrets.GITHUB_TOKEN }} 14 | stale-issue-message: 'This issue is stale because it has been open 30 days with no activity.' 15 | days-before-stale: 30 16 | days-before-close: -1 17 | stale-pr-message: 'This pull request is stale because it has been open 30 days with no activity.' 18 | stale-issue-label: 'status/no-issue-activity' 19 | stale-pr-label: 'status/no-pr-activity' 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Generated files 9 | *.d.ts 10 | index.js 11 | /lib 12 | /browser 13 | /bundles 14 | /dist 15 | /docs 16 | src/schema/v1.js 17 | 18 | # Runtime data 19 | pids 20 | *.pid 21 | *.seed 22 | *.pid.lock 23 | 24 | # Directory for instrumented libs generated by jscoverage/JSCover 25 | lib-cov 26 | 27 | # Coverage directory used by tools like istanbul 28 | coverage 29 | 30 | # nyc test coverage 31 | .nyc_output 32 | 33 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 34 | .grunt 35 | 36 | # Bower dependency directory (https://bower.io/) 37 | bower_components 38 | 39 | # node-waf configuration 40 | .lock-wscript 41 | 42 | # Compiled binary addons (https://nodejs.org/api/addons.html) 43 | build/Release 44 | build/ 45 | 46 | # Dependency directories 47 | node_modules/ 48 | jspm_packages/ 49 | 50 | # TypeScript v1 declaration files 51 | typings/ 52 | 53 | # Optional npm cache directory 54 | .npm 55 | 56 | # Optional eslint cache 57 | .eslintcache 58 | 59 | # Optional REPL history 60 | .node_repl_history 61 | 62 | # Output of 'npm pack' 63 | *.tgz 64 | 65 | # Yarn Integrity file 66 | .yarn-integrity 67 | 68 | # dotenv environment variables file 69 | .env 70 | 71 | # parcel-bundler cache (https://parceljs.org/) 72 | .cache 73 | 74 | # next.js build output 75 | .next 76 | 77 | # nuxt.js build output 78 | .nuxt 79 | 80 | # vuepress build output 81 | .vuepress/dist 82 | 83 | # Serverless directories 84 | .serverless 85 | 86 | # FuseBox cache 87 | .fusebox/ 88 | 89 | # Vim 90 | *.swp 91 | 92 | # Package lock 93 | package-lock.json 94 | 95 | # Jetbrains IDE directories 96 | .idea 97 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "conformance"] 2 | path = conformance 3 | url = git@github.com:cloudevents/conformance.git 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | examples/ 2 | .travis.yml 3 | 4 | -------------------------------------------------------------------------------- /.remarkrc: -------------------------------------------------------------------------------- 1 | { 2 | "remarkConfig": { 3 | "plugins": [ 4 | "remark-preset-lint-recommended", 5 | [ 6 | "remark-lint-list-item-indent", 7 | "space" 8 | ] 9 | ] 10 | } 11 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "type": "npm", 6 | "script": "build", 7 | "group": "build", 8 | "problemMatcher": [], 9 | "label": "npm: build", 10 | "detail": "tsc --project tsconfig.json && tsc --project tsconfig.browser.json && webpack" 11 | }, 12 | { 13 | "type": "npm", 14 | "script": "watch", 15 | "group": "build", 16 | "problemMatcher": [], 17 | "label": "npm: watch", 18 | "detail": "tsc --project tsconfig.json --watch" 19 | }, 20 | { 21 | "type": "npm", 22 | "script": "lint", 23 | "group": "build", 24 | "problemMatcher": [], 25 | "label": "npm: lint", 26 | "detail": "eslint 'src/**/*.{js,ts}' 'test/**/*.{js,ts}'" 27 | }, 28 | { 29 | "type": "npm", 30 | "script": "test", 31 | "group": "test", 32 | "problemMatcher": [], 33 | "label": "npm: test", 34 | "detail": "mocha --require ts-node/register ./test/integration/**/*.ts" 35 | }, 36 | ] 37 | } -------------------------------------------------------------------------------- /API_TRANSITION_GUIDE.md: -------------------------------------------------------------------------------- 1 | ## Deprecated API Transition Guide 2 | 3 | When APIs are deprecated, the following guide will show how to transition from removed APIs to the new ones 4 | 5 | 6 | ### Upgrading From 3.x to 4.0 7 | 8 | In the 3.2.0 release, a few APIs were set to be deprecated in the 4.0 release. With the release of 4.0.0, those APIs have been removed. 9 | 10 | #### Receiever 11 | 12 | The `Receiver` class has been removed. 13 | 14 | `Receiver.accept` should be transitioned to `HTTP.toEvent` 15 | 16 | Here is an example of what a `HTTP.toEvent` might look like using Express.js 17 | 18 | ```js 19 | const app = require("express")(); 20 | const { HTTP } = require("cloudevents"); 21 | 22 | app.post("/", (req, res) => { 23 | // body and headers come from an incoming HTTP request, e.g. express.js 24 | const receivedEvent = HTTP.toEvent({ headers: req.headers, body: req.body }); 25 | console.log(receivedEvent); 26 | }); 27 | ``` 28 | 29 | #### Emitter 30 | 31 | `Emit.send` should be transitioned to `HTTP.binary` for binary events and `HTTP.structured` for structured events 32 | 33 | `Emit.send` would use axios to emit the events. Since this now longer available, you are free to choose your own transport protocol. 34 | 35 | So for axios, it might look something like this: 36 | 37 | ```js 38 | const axios = require('axios').default; 39 | const { HTTP } = require("cloudevents"); 40 | 41 | 42 | const ce = new CloudEvent({ type, source, data }) 43 | const message = HTTP.binary(ce); // Or HTTP.structured(ce) 44 | 45 | axios({ 46 | method: 'post', 47 | url: '...', 48 | data: message.body, 49 | headers: message.headers, 50 | }); 51 | ``` 52 | 53 | You may also use the `emitterFor()` function as a convenience. 54 | 55 | ```js 56 | const axios = require('axios').default; 57 | const { emitterFor, Mode } = require("cloudevents"); 58 | 59 | function sendWithAxios(message) { 60 | // Do what you need with the message headers 61 | // and body in this function, then send the 62 | // event 63 | axios({ 64 | method: 'post', 65 | url: '...', 66 | data: message.body, 67 | headers: message.headers, 68 | }); 69 | } 70 | 71 | const emit = emitterFor(sendWithAxios, { mode: Mode.BINARY }); 72 | emit(new CloudEvent({ type, source, data })); 73 | ``` 74 | 75 | You may also use the `Emitter` singleton 76 | 77 | ```js 78 | const axios = require("axios").default; 79 | const { emitterFor, Mode, CloudEvent, Emitter } = require("cloudevents"); 80 | 81 | function sendWithAxios(message) { 82 | // Do what you need with the message headers 83 | // and body in this function, then send the 84 | // event 85 | axios({ 86 | method: "post", 87 | url: "...", 88 | data: message.body, 89 | headers: message.headers, 90 | }); 91 | } 92 | 93 | const emit = emitterFor(sendWithAxios, { mode: Mode.BINARY }); 94 | // Set the emit 95 | Emitter.on("cloudevent", emit); 96 | 97 | ... 98 | // In any part of the code will send the event 99 | new CloudEvent({ type, source, data }).emit(); 100 | 101 | // You can also have several listener to send the event to several endpoint 102 | ``` 103 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to CloudEvents' JavaScript SDK 2 | 3 | :+1::tada: First off, thanks for taking the time to contribute! :tada::+1: 4 | 5 | We welcome contributions from the community! Please take some time to become 6 | acquainted with the process before submitting a pull request. There are just 7 | a few things to keep in mind. 8 | 9 | ## Pull Requests 10 | 11 | Typically a pull request should relate to an existing issue. If you have 12 | found a bug, want to add an improvement, or suggest an API change, please 13 | create an issue before proceeding with a pull request. For very minor changes 14 | such as typos in the documentation this isn't really necessary. 15 | 16 | For step by step help with managing your pull request, have a look at our 17 | [PR Guidelines](pr_guidelines.md) document. 18 | 19 | ### Commit Messages 20 | 21 | Please follow the 22 | [Conventional Commits specification](https://www.conventionalcommits.org/en/v1.0.0/#summary). 23 | 24 | ### Sign your work 25 | 26 | Each PR must be signed. Be sure your `git` `user.name` and `user.email` are configured 27 | then use the `--signoff` flag for your commits. 28 | 29 | ```console 30 | git commit --signoff 31 | ``` 32 | 33 | ### Style Guide 34 | 35 | Code style for this module is maintained using [`eslint`](https://www.npmjs.com/package/eslint). 36 | When you run tests with `npm test` linting is performed first. If you want to 37 | check your code style for linting errors without running tests, you can just 38 | run `npm run lint`. If there are errors, you can usually fix them automatically 39 | by running `npm run fix`. 40 | 41 | Linting rules are declared in [.eslintrc](https://github.com/cloudevents/sdk-javascript/blob/main/.eslintrc). 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2018-2020 CloudEvents Authors 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /MAINTAINERS.md: -------------------------------------------------------------------------------- 1 | # Maintainers 2 | 3 | Current active maintainers of this SDK: 4 | 5 | - [Lance Ball](https://github.com/lance) 6 | - [Daniel Bevenius](https://github.com/danbev) 7 | - [Lucas Holmquist](https://github.com/lholmquist) 8 | - [Fabio Jose](https://github.com/fabiojose) 9 | - [Helio Frota](https://github.com/helio-frota) 10 | -------------------------------------------------------------------------------- /RELEASING.md: -------------------------------------------------------------------------------- 1 | # Module Release Guidelines 2 | 3 | ## `release-please` 4 | 5 | This project uses [`release-please-action`](https://github.com/google-github-actions/release-please-action) 6 | to manage CHANGELOG.md and automate our releases. It does so by parsing the git history, looking for 7 | [Conventional Commit](https://www.conventionalcommits.org/en/v1.0.0/) messages, and creating release PRs. 8 | 9 | For example: https://github.com/cloudevents/sdk-javascript/pull/475 10 | 11 | Each time a commit lands on `main`, the workflow updates the pull request to include the commit message 12 | in CHANGELOG.md, and bump the version in package.json. When you are ready to create a new release, simply 13 | land the pull request. This will result in a release commit, updating CHANGELOG.md and package.json, a version 14 | tag is created on that commit SHA, and a release is drafted in github.com. 15 | 16 | ### Publish to npm 17 | 18 | Once the new version has been created, we need to push it to npm. Assuming you have all the rights to do so, just run: 19 | 20 | ``` 21 | npm publish 22 | ``` 23 | -------------------------------------------------------------------------------- /cucumber.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 The CloudEvents Authors 3 | SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | // cucumber.js 7 | let common = [ 8 | "--require-module ts-node/register", // Load TypeScript module 9 | "--require test/conformance/steps.ts", // Load step definitions 10 | ].join(" "); 11 | 12 | module.exports = { 13 | default: common, 14 | }; 15 | -------------------------------------------------------------------------------- /examples/express-ex/README.md: -------------------------------------------------------------------------------- 1 | # Express Example 2 | 3 | ## How To Start 4 | 5 | ```bash 6 | npm start 7 | ``` 8 | 9 | ## Latest Supported Spec (v1.0) 10 | 11 | 12 | __A Structured One__ 13 | 14 | > Payload [example](../payload/v1/structured-event-0.json) 15 | 16 | ```bash 17 | curl -X POST \ 18 | -d'@../payload/v1/structured-event-0.json' \ 19 | -H'Content-Type:application/cloudevents+json' \ 20 | http://localhost:3000/ 21 | ``` 22 | 23 | __A Structured One with Extension__ 24 | 25 | > Payload [example](../payload/v1/structured-event-1.json) 26 | 27 | ```bash 28 | curl -X POST \ 29 | -d'@../payload/v1/structured-event-1.json' \ 30 | -H'Content-Type:application/cloudevents+json' \ 31 | http://localhost:3000/ 32 | ``` 33 | 34 | __A Structured One with Base64 Event Data__ 35 | 36 | > Payload [example](../payload/v1/structured-event-2.json) 37 | 38 | ```bash 39 | curl -X POST \ 40 | -d'@../payload/v1/structured-event-2.json' \ 41 | -H'Content-Type:application/cloudevents+json' \ 42 | http://localhost:3000/ 43 | ``` 44 | 45 | __A Binary One__ 46 | 47 | ```bash 48 | curl -X POST \ 49 | -d'@../payload/data-0.json' \ 50 | -H'Content-Type:application/json' \ 51 | -H'ce-specversion:1.0' \ 52 | -H'ce-type:com.github.pull.create' \ 53 | -H'ce-source:https://github.com/cloudevents/spec/pull/123' \ 54 | -H'ce-id:45c83279-c8a1-4db6-a703-b3768db93887' \ 55 | -H'ce-time:2019-11-06T11:17:00Z' \ 56 | http://localhost:3000/ 57 | ``` 58 | 59 | __A Binary One with Extension__ 60 | 61 | ```bash 62 | curl -X POST \ 63 | -d'@../payload/data-0.json' \ 64 | -H'Content-Type:application/json' \ 65 | -H'ce-specversion:1.0' \ 66 | -H'ce-type:com.github.pull.create' \ 67 | -H'ce-source:https://github.com/cloudevents/spec/pull/123' \ 68 | -H'ce-id:45c83279-c8a1-4db6-a703-b3768db93887' \ 69 | -H'ce-time:2019-11-06T11:17:00Z' \ 70 | -H'ce-myextension:extension value' \ 71 | http://localhost:3000/ 72 | ``` 73 | 74 | __A Binary One with Base 64 Encoding__ 75 | 76 | ```bash 77 | curl -X POST \ 78 | -d'@../payload/data-1.txt' \ 79 | -H'Content-Type:application/json' \ 80 | -H'ce-specversion:1.0' \ 81 | -H'ce-type:com.github.pull.create' \ 82 | -H'ce-source:https://github.com/cloudevents/spec/pull/123' \ 83 | -H'ce-id:45c83279-c8a1-4db6-a703-b3768db93887' \ 84 | -H'ce-time:2019-11-06T11:17:00Z' \ 85 | http://localhost:3000/ 86 | ``` 87 | 88 | 89 | ## Spec v0.3 90 | 91 | __A Structured One__ 92 | 93 | > Payload [example](../payload/v03/structured-event-0.json) 94 | 95 | ```bash 96 | curl -X POST \ 97 | -d'@../payload/v03/structured-event-0.json' \ 98 | -H'Content-Type:application/cloudevents+json' \ 99 | http://localhost:3000/ 100 | ``` 101 | 102 | __A Structured One with Extension__ 103 | 104 | > Payload [example](../payload/v03/structured-event-1.json) 105 | 106 | ```bash 107 | curl -X POST \ 108 | -d'@../payload/v03/structured-event-1.json' \ 109 | -H'Content-Type:application/cloudevents+json' \ 110 | http://localhost:3000/ 111 | ``` 112 | 113 | __A Structured One with Base64 Event Data__ 114 | 115 | > Payload [example](../payload/v03/structured-event-2.json) 116 | 117 | ```bash 118 | curl -X POST \ 119 | -d'@../payload/v03/structured-event-2.json' \ 120 | -H'Content-Type:application/cloudevents+json' \ 121 | http://localhost:3000/ 122 | ``` 123 | 124 | __A Binary One__ 125 | 126 | ```bash 127 | curl -X POST \ 128 | -d'@../payload/data-0.json' \ 129 | -H'Content-Type:application/json' \ 130 | -H'ce-specversion:0.3' \ 131 | -H'ce-type:com.github.pull.create' \ 132 | -H'ce-source:https://github.com/cloudevents/spec/pull/123' \ 133 | -H'ce-id:45c83279-c8a1-4db6-a703-b3768db93887' \ 134 | -H'ce-time:2019-06-21T17:31:00Z' \ 135 | http://localhost:3000/ 136 | ``` 137 | 138 | __A Binary One with Extension__ 139 | 140 | ```bash 141 | curl -X POST \ 142 | -d'@../payload/data-0.json' \ 143 | -H'Content-Type:application/json' \ 144 | -H'ce-specversion:0.3' \ 145 | -H'ce-type:com.github.pull.create' \ 146 | -H'ce-source:https://github.com/cloudevents/spec/pull/123' \ 147 | -H'ce-id:45c83279-c8a1-4db6-a703-b3768db93887' \ 148 | -H'ce-time:2019-06-21T17:31:00Z' \ 149 | -H'ce-myextension:extension value' \ 150 | http://localhost:3000/ 151 | ``` 152 | 153 | __A Binary One with Base 64 Encoding__ 154 | 155 | ```bash 156 | curl -X POST \ 157 | -d'@../payload/data-1.txt' \ 158 | -H'Content-Type:application/json' \ 159 | -H'ce-specversion:0.3' \ 160 | -H'ce-type:com.github.pull.create' \ 161 | -H'ce-source:https://github.com/cloudevents/spec/pull/123' \ 162 | -H'ce-id:45c83279-c8a1-4db6-a703-b3768db93887' \ 163 | -H'ce-time:2019-06-21T17:31:00Z' \ 164 | -H'ce-datacontentencoding:base64' \ 165 | http://localhost:3000/ 166 | ``` 167 | 168 | -------------------------------------------------------------------------------- /examples/express-ex/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | const express = require("express"); 4 | const { CloudEvent, HTTP } = require("cloudevents"); 5 | const app = express(); 6 | 7 | app.use((req, res, next) => { 8 | let data = ""; 9 | 10 | req.setEncoding("utf8"); 11 | req.on("data", function (chunk) { 12 | data += chunk; 13 | }); 14 | 15 | req.on("end", function () { 16 | req.body = data; 17 | next(); 18 | }); 19 | }); 20 | 21 | app.post("/", (req, res) => { 22 | console.log("HEADERS", req.headers); 23 | console.log("BODY", req.body); 24 | 25 | try { 26 | const event = HTTP.toEvent({ headers: req.headers, body: req.body }); 27 | // respond as an event 28 | const responseEventMessage = new CloudEvent({ 29 | source: '/', 30 | type: 'event:response', 31 | ...event, 32 | data: { 33 | hello: 'world' 34 | } 35 | }); 36 | 37 | // const message = HTTP.binary(responseEventMessage) 38 | const message = HTTP.structured(responseEventMessage) 39 | res.set(message.headers) 40 | res.send(message.body) 41 | 42 | } catch (err) { 43 | console.error(err); 44 | res.status(415).header("Content-Type", "application/json").send(JSON.stringify(err)); 45 | } 46 | }); 47 | 48 | app.listen(3000, () => { 49 | console.log("Example app listening on port 3000!"); 50 | }); 51 | -------------------------------------------------------------------------------- /examples/express-ex/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "express-ex", 3 | "version": "1.0.0", 4 | "description": "Examples to use CloudEvents with Express", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node index.js", 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "keywords": [ 11 | "cloudevents", 12 | "express" 13 | ], 14 | "author": "fabiojose@gmail.com", 15 | "license": "Apache-2.0", 16 | "dependencies": { 17 | "cloudevents": "^4.0.0", 18 | "express": "^4.17.1" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /examples/mqtt-ex/README.md: -------------------------------------------------------------------------------- 1 | # MQTT Example 2 | 3 | The MQTT message protocol are available since v5.3.0 4 | 5 | ## How To Start 6 | 7 | Install and compile: 8 | 9 | ```bash 10 | npm install 11 | npm run compile 12 | ``` 13 | 14 | Start a MQTT broker using Docker: 15 | 16 | ```bash 17 | docker run -it -d -p 1883:1883 eclipse-mosquitto:2.0 mosquitto -c /mosquitto-no-auth.conf 18 | ``` 19 | 20 | Then, start 21 | 22 | ```bash 23 | npm start 24 | ``` 25 | -------------------------------------------------------------------------------- /examples/mqtt-ex/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mqtt-ex", 3 | "version": "1.0.0", 4 | "description": "Simple mqtt example using CloudEvents types", 5 | "repository": "https://github.com/cloudevents/sdk-javascript.git", 6 | "main": "build/src/index.js", 7 | "types": "build/src/index.d.ts", 8 | "files": [ 9 | "build/src" 10 | ], 11 | "license": "Apache-2.0", 12 | "keywords": [], 13 | "scripts": { 14 | "start": "node build/index.js", 15 | "test": "echo \"Error: no test specified\" && exit 1", 16 | "check": "gts check", 17 | "clean": "gts clean", 18 | "compile": "tsc -p .", 19 | "watch": "tsc -p . --watch", 20 | "fix": "gts fix", 21 | "prepare": "npm run compile", 22 | "pretest": "npm run compile", 23 | "posttest": "npm run check" 24 | }, 25 | "devDependencies": { 26 | "@types/node": "^14.14.10", 27 | "@types/ws": "^8.5.4", 28 | "gts": "^3.0.3", 29 | "typescript": "~4.1.3" 30 | }, 31 | "dependencies": { 32 | "cloudevents": "^6.0.3", 33 | "mqtt": "^4.3.7" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /examples/mqtt-ex/src/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import { CloudEvent, MQTT } from "cloudevents"; 3 | import * as mqtt from "mqtt"; 4 | 5 | const client = mqtt.connect("mqtt://localhost:1883"); 6 | 7 | client.on("connect", function () { 8 | client.subscribe("presence", function (err) { 9 | if (err) return; 10 | const event = new CloudEvent({ 11 | source: "presence", 12 | type: "presence.event", 13 | datacontenttype: "application/json", 14 | data: { 15 | hello: "world", 16 | }, 17 | }); 18 | const { body, headers } = MQTT.binary(event); 19 | 20 | client.publish("presence", JSON.stringify(body), { 21 | properties: { 22 | userProperties: headers as mqtt.UserProperties, 23 | }, 24 | }); 25 | }); 26 | }); 27 | 28 | client.on("message", function (topic, message, packet) { 29 | const event = MQTT.toEvent({ 30 | body: JSON.parse(message.toString()), 31 | headers: packet.properties?.userProperties || {}, 32 | }); 33 | console.log(event); 34 | client.end(); 35 | }); 36 | -------------------------------------------------------------------------------- /examples/mqtt-ex/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./node_modules/gts/tsconfig-google.json", 3 | "compilerOptions": { 4 | "rootDir": "./src", 5 | "outDir": "./build/", 6 | "lib": [ 7 | "es6", 8 | "dom" 9 | ] 10 | }, 11 | "include": [ 12 | "src/**/*.ts", 13 | "test/**/*.ts" 14 | ], 15 | "allowJs": true 16 | } 17 | -------------------------------------------------------------------------------- /examples/payload/data-0.json: -------------------------------------------------------------------------------- 1 | { 2 | "much":"data-0" 3 | } 4 | -------------------------------------------------------------------------------- /examples/payload/data-1.txt: -------------------------------------------------------------------------------- 1 | eyJtdWNoIjoid293In0= 2 | -------------------------------------------------------------------------------- /examples/payload/v03/structured-event-0.json: -------------------------------------------------------------------------------- 1 | { 2 | "specversion":"0.3", 3 | "type":"com.github.pull.create", 4 | "source":"https://github.com/cloudevents/spec/pull/123", 5 | "id":"b25e2717-a470-45a0-8231-985a99aa9416", 6 | "time":"2019-07-04T17:31:00Z", 7 | "datacontenttype":"application/json", 8 | "data":{ 9 | "much":"wow" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /examples/payload/v03/structured-event-1.json: -------------------------------------------------------------------------------- 1 | { 2 | "specversion":"0.3", 3 | "type":"com.github.pull.create", 4 | "source":"https://github.com/cloudevents/spec/pull/123", 5 | "id":"70d3c768-63f8-40e7-aa9d-d197d530586b", 6 | "time":"2019-07-04T17:31:00Z", 7 | "datacontenttype":"application/json", 8 | "data":{ 9 | "much":"wow" 10 | }, 11 | "myextension" : { 12 | "some" : "thing" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /examples/payload/v03/structured-event-2.json: -------------------------------------------------------------------------------- 1 | { 2 | "specversion":"0.3", 3 | "type":"com.github.pull.create", 4 | "source":"https://github.com/cloudevents/spec/pull/123", 5 | "id":"70d3c768-63f8-40e7-aa9d-d197d530586b", 6 | "time":"2019-07-04T17:31:00Z", 7 | "datacontenttype":"application/json", 8 | "datacontentencoding":"base64", 9 | "data":"eyJtdWNoIjoid293In0=", 10 | "myextension" : { 11 | "some" : "thing" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /examples/payload/v1/structured-event-0.json: -------------------------------------------------------------------------------- 1 | { 2 | "specversion":"1.0", 3 | "type":"com.github.pull.create", 4 | "source":"https://github.com/cloudevents/spec/pull/123", 5 | "id":"b25e2717-a470-45a0-8231-985a99aa9416", 6 | "time":"2019-11-06T11:08:00Z", 7 | "datacontenttype":"application/json", 8 | "data":{ 9 | "much":"wow" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /examples/payload/v1/structured-event-1.json: -------------------------------------------------------------------------------- 1 | { 2 | "specversion":"1.0", 3 | "type":"com.github.pull.create", 4 | "source":"https://github.com/cloudevents/spec/pull/123", 5 | "id":"70d3c768-63f8-40e7-aa9d-d197d530586b", 6 | "time":"2019-11-06T11:08:00Z", 7 | "datacontenttype":"application/json", 8 | "data":{ 9 | "much":"wow" 10 | }, 11 | "myextension" : "something" 12 | } 13 | -------------------------------------------------------------------------------- /examples/payload/v1/structured-event-2.json: -------------------------------------------------------------------------------- 1 | { 2 | "specversion":"1.0", 3 | "type":"com.github.pull.create", 4 | "source":"https://github.com/cloudevents/spec/pull/123", 5 | "id":"70d3c768-63f8-40e7-aa9d-d197d530586b", 6 | "time":"2019-11-06T11:08:00Z", 7 | "datacontenttype":"application/json", 8 | "data_base64":"eyJtdWNoIjoid293In0=", 9 | "myextension" : "something" 10 | } 11 | -------------------------------------------------------------------------------- /examples/typescript-ex/README.md: -------------------------------------------------------------------------------- 1 | # Typescript Example 2 | 3 | The types are available since v1.0.0 4 | 5 | ## How To Start 6 | 7 | Install and transpile: 8 | 9 | ```bash 10 | npm install 11 | ``` 12 | 13 | Then, start 14 | 15 | ```bash 16 | npm start 17 | ``` 18 | -------------------------------------------------------------------------------- /examples/typescript-ex/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "typescript-ex", 3 | "version": "1.0.0", 4 | "description": "Simple typescript example using CloudEvents types", 5 | "repository": "https://github.com/cloudevents/sdk-javascript.git", 6 | "main": "build/src/index.js", 7 | "types": "build/src/index.d.ts", 8 | "files": [ 9 | "build/src" 10 | ], 11 | "license": "Apache-2.0", 12 | "keywords": [], 13 | "scripts": { 14 | "start": "node build/index.js", 15 | "test": "echo \"Error: no test specified\" && exit 1", 16 | "check": "gts check", 17 | "clean": "gts clean", 18 | "compile": "tsc -p .", 19 | "watch": "tsc -p . --watch", 20 | "fix": "gts fix", 21 | "prepare": "npm run compile", 22 | "pretest": "npm run compile", 23 | "posttest": "npm run check" 24 | }, 25 | "devDependencies": { 26 | "@types/node": "^14.14.10", 27 | "gts": "^3.0.3", 28 | "typescript": "~4.1.3" 29 | }, 30 | "dependencies": { 31 | "cloudevents": "~4.0.0" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /examples/typescript-ex/src/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | import { CloudEvent, HTTP } from "cloudevents"; 3 | 4 | export function doSomeStuff(): void { 5 | const myevent: CloudEvent = new CloudEvent({ 6 | source: "/source", 7 | type: "type", 8 | datacontenttype: "text/plain", 9 | dataschema: "https://d.schema.com/my.json", 10 | subject: "cha.json", 11 | data: "my-data", 12 | extension1: "some extension data", 13 | }); 14 | 15 | console.log("My structured event:", myevent); 16 | 17 | // ------ receiver structured 18 | // The header names should be standarized to use lowercase 19 | const headers = { 20 | "content-type": "application/cloudevents+json", 21 | }; 22 | 23 | // Typically used with an incoming HTTP request where myevent.format() is the actual 24 | // body of the HTTP 25 | console.log("Received structured event:", HTTP.toEvent({ headers, body: myevent })); 26 | 27 | // ------ receiver binary 28 | const data = { 29 | data: "dataString", 30 | }; 31 | const attributes = { 32 | "ce-type": "type", 33 | "ce-specversion": "1.0", 34 | "ce-source": "source", 35 | "ce-id": "id", 36 | "ce-time": "2019-06-16T11:42:00Z", 37 | "ce-dataschema": "http://schema.registry/v1", 38 | "Content-Type": "application/json", 39 | "ce-extension1": "extension1", 40 | }; 41 | 42 | console.log("My binary event:", HTTP.toEvent({ headers: attributes, body: data })); 43 | console.log("My binary event extensions:", HTTP.toEvent({ headers: attributes, body: data })); 44 | } 45 | 46 | doSomeStuff(); 47 | -------------------------------------------------------------------------------- /examples/typescript-ex/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./node_modules/gts/tsconfig-google.json", 3 | "compilerOptions": { 4 | "rootDir": "./src", 5 | "outDir": "./build/", 6 | "lib": [ 7 | "es6", 8 | "dom" 9 | ] 10 | }, 11 | "include": [ 12 | "src/**/*.ts", 13 | "test/**/*.ts" 14 | ], 15 | "allowJs": true 16 | } 17 | -------------------------------------------------------------------------------- /examples/typescript-ex/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "gts/tslint.json", 3 | "linterOptions": { 4 | "exclude": [ 5 | "**/*.json" 6 | ] 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /examples/websocket/README.md: -------------------------------------------------------------------------------- 1 | # WebSocket Example 2 | 3 | This example shows how simple it is to use CloudEvents over a websocket 4 | connection. The code here shows backend communication from two server 5 | side processes, and also between a browser and a server process. 6 | 7 | ## Running the Example 8 | 9 | This simple project consists of a server and a client. The server receives 10 | `CloudEvents` from the client over a local websocket connection. 11 | 12 | 13 | To get started, first install dependencies. 14 | 15 | ```sh 16 | npm install 17 | ``` 18 | 19 | ### Server 20 | The server opens a websocket and waits for incoming connections. It expects that any 21 | messages it receives will be a CloudEvent. When received, it reads the data field, 22 | expecting a zip code. It then fetches the current weather for that zip code and 23 | responds with a CloudEvent containing the body of the Weather API response as the 24 | event data. 25 | 26 | You will need to change one line in the `server.js` file and provide your Open 27 | Weather API key. You can also create a environment variable `OPEN_WEATHER_API_KEY` and store your key there. 28 | 29 | To start the server, run `node server.js`. 30 | 31 | ### Client 32 | Upon start, the client prompts a user for a zip code, then sends a CloudEvent over 33 | a websocket to the server with the provided zip code as the event data. The server 34 | fetches the current weather for that zip code and returns it as a CloudEvent. The 35 | client extracts the data and prints the current weather to the console. 36 | 37 | To start the client, run `node client.js` 38 | 39 | ### Browser 40 | Open the [`index.html`]('./index.html') file in your browser and provide a zip 41 | code in the provided form field. The browser will send the zip code in the data 42 | field of a CloudEvent over a websocket. When it receives a response from the server 43 | it prints the weather, or an error message, to the screen. 44 | 45 | To terminate the client or server, type CTL-C. 46 | -------------------------------------------------------------------------------- /examples/websocket/client.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | const readline = require("readline"); 3 | const WebSocket = require("ws"); 4 | const ws = new WebSocket("ws://localhost:8080"); 5 | 6 | const { CloudEvent } = require("cloudevents"); 7 | 8 | const rl = readline.createInterface({ 9 | input: process.stdin, 10 | output: process.stdout, 11 | }); 12 | 13 | rl.on("close", (_) => console.log("\n\nConnection closed! Press CTL-C to exit.")); 14 | 15 | ws.on("message", function incoming(message) { 16 | const event = new CloudEvent(JSON.parse(message)); 17 | if (event.type === "weather.error") { 18 | console.error(`Error: ${event.data}`); 19 | } else { 20 | print(event.data); 21 | } 22 | ask(); 23 | }); 24 | 25 | function ask() { 26 | rl.question("Would you like to see the current weather? Provide a zip code: ", function (zip) { 27 | console.log("Fetching weather data from server..."); 28 | const event = new CloudEvent({ 29 | type: "weather.query", 30 | source: "/weather.client", 31 | data: { zip }, 32 | }); 33 | ws.send(event.toString()); 34 | }); 35 | } 36 | 37 | function print(data) { 38 | console.log(` 39 | Current weather for ${data.name}: ${data.weather[0].main} 40 | ------------------------------------------ 41 | With ${data.weather[0].description}, the temperature is ${Math.round(data.main.temp)}F 42 | and the wind is blowing at ${Math.round(data.wind.speed)}mph. 43 | `); 44 | } 45 | 46 | ask(); 47 | -------------------------------------------------------------------------------- /examples/websocket/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | CloudEvent Example 5 | 6 | 53 | 54 | 55 |

Weather By Zip Code

56 |

Please provide a zip code 57 | 58 |

59 |

60 |

61 | 62 | 63 | -------------------------------------------------------------------------------- /examples/websocket/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "websocket-cloudevents", 3 | "version": "0.0.1", 4 | "description": "An example application that sends and receives CloudEvents over a websocket", 5 | "main": "server.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "node server.js" 9 | }, 10 | "keywords": [ 11 | "cloudevents", 12 | "example", 13 | "websocket" 14 | ], 15 | "author": "", 16 | "license": "ISC", 17 | "dependencies": { 18 | "cloudevents": "~4.0.0", 19 | "got": "^11.3.0", 20 | "ws": "^7.3.0" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /examples/websocket/server.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | const got = require("got"); 3 | 4 | const { CloudEvent } = require("cloudevents"); 5 | const WebSocket = require("ws"); 6 | const wss = new WebSocket.Server({ port: 8080 }); 7 | 8 | const api = "https://api.openweathermap.org/data/2.5/weather"; 9 | const key = process.env.OPEN_WEATHER_API_KEY || "REPLACE WITH API KEY"; 10 | 11 | console.log("WebSocket server started. Waiting for events."); 12 | 13 | wss.on("connection", function connection(ws) { 14 | console.log("Connection received"); 15 | ws.on("message", function incoming(message) { 16 | console.log(`Message received: ${message}`); 17 | const event = new CloudEvent(JSON.parse(message)); 18 | fetch(event.data.zip) 19 | .then((weather) => { 20 | const response = new CloudEvent({ 21 | datacontenttype: "application/json", 22 | type: "current.weather", 23 | source: "/weather.server", 24 | data: weather, 25 | }); 26 | ws.send(JSON.stringify(response)); 27 | }) 28 | .catch((err) => { 29 | console.error(err); 30 | ws.send( 31 | JSON.stringify( 32 | new CloudEvent({ 33 | type: "weather.error", 34 | source: "/weather.server", 35 | data: err.toString(), 36 | }), 37 | ), 38 | ); 39 | }); 40 | }); 41 | }); 42 | 43 | function fetch(zip) { 44 | const query = `${api}?zip=${zip}&appid=${key}&units=imperial`; 45 | return new Promise((resolve, reject) => { 46 | got(query) 47 | .then((response) => resolve(JSON.parse(response.body))) 48 | .catch((err) => reject(err.message)); 49 | }); 50 | } 51 | -------------------------------------------------------------------------------- /maintainer_guidelines.md: -------------------------------------------------------------------------------- 1 | # Maintainer's Guide 2 | 3 | ## Tips 4 | 5 | Here are a few tips for repository maintainers. 6 | 7 | * Stay on top of your pull requests. PRs that languish for too long can become difficult to merge. 8 | * Work from your own fork. As you are making contributions to the project, you should be working from your own fork just as outside contributors do. This keeps the branches in github to a minimum and reduces unnecessary CI runs. 9 | * Try to proactively label issues and pull requests with labels 10 | * Actively review pull requests as they are submitted 11 | * Triage issues once in a while in order to keep the repository alive. 12 | * If some issues are stale for too long because they are no longer valid/relevant or because the discussion reached no significant action items to perform, close them and invite the users to reopen if they need it. 13 | * If some PRs are no longer valid due to conflicts, but the PR is still needed, ask the contributor to rebase their PR. 14 | * If some issues and PRs are still relevant, use labels to help organize tasks. 15 | * If you find an issue that you want to create a pull request for, be sure to assign it to yourself so that other maintainers don't start working on it at the same time. 16 | 17 | ## Landing Pull Requests 18 | 19 | When landing pull requests, be sure to check the first line uses an appropriate commit message prefix (e.g. docs, feat, lib, etc). If there is more than one commit, try to squash into a single commit. Usually this can just be done with the GitHub UI when merging the PR. Use "Squash and merge". To help ensure that everyone in the community has an opportunity to review and comment on pull requests, it's often good to have some time after a pull request has been submitted, and before it has landed. Some guidelines here about approvals and timing. 20 | 21 | * No pull request may land without passing all automated checks 22 | * All pull requests require at least one approval from a maintainer before landing 23 | * A pull request author may approve their own PR, but will need an additional approval to land it 24 | * If a maintainer has submitted a pull request and it has not received approval from at least one other maintainer, it can be landed after 72 hours 25 | * If a pull request has both approvals and requested changes, it can't be landed until those requested changes are resolved 26 | 27 | ## Branch Management 28 | 29 | The `main` branch is the bleeding edge. New major versions of the module 30 | are cut from this branch and tagged. If you intend to submit a pull request 31 | you should use `main HEAD` as your starting point. 32 | 33 | Each major release will result in a new branch and tag. For example, the 34 | release of version 1.0.0 of the module will result in a `v1.0.0` tag on the 35 | release commit, and a new branch `v1.x.y` for subsequent minor and patch 36 | level releases of that major version. However, development will continue 37 | apace on `main` for the next major version - e.g. 2.0.0. Version branches 38 | are only created for each major version. Minor and patch level releases 39 | are simply tagged. 40 | 41 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cloudevents", 3 | "version": "9.0.0", 4 | "description": "CloudEvents SDK for JavaScript", 5 | "main": "dist/index.js", 6 | "scripts": { 7 | "watch": "tsc --project tsconfig.json --watch", 8 | "build:src": "tsc --project tsconfig.json", 9 | "build:browser": "tsc --project tsconfig.browser.json && webpack", 10 | "build:schema": "ajv compile -c ./src/schema/formats.js -s src/schema/cloudevent.json --strict-types false -o src/schema/v1.js", 11 | "build": "npm run build:schema && npm run build:src && npm run build:browser", 12 | "lint": "npm run lint:md && npm run lint:js", 13 | "lint:js": "eslint 'src/**/*.{js,ts}' 'test/**/*.{js,ts}' cucumber.js", 14 | "lint:md": "remark .", 15 | "lint:fix": "eslint 'src/**/*.{js,ts}' 'test/**/*.{js,ts}' --fix", 16 | "pretest": "npm run lint && npm run build && npm run conformance", 17 | "test": "mocha --require ts-node/register ./test/integration/**/*.ts", 18 | "test:one": "mocha --require ts-node/register", 19 | "conformance": "cucumber-js ./conformance/features/*-protocol-binding.feature -p default", 20 | "coverage": "nyc --reporter=lcov --reporter=text npm run test", 21 | "coverage-publish": "wget -qO - https://coverage.codacy.com/get.sh | bash -s report -l JavaScript -r coverage/lcov.info", 22 | "generate-docs": "typedoc --excludeNotDocumented --out docs src", 23 | "prepublishOnly": "npm run build" 24 | }, 25 | "files": [ 26 | "dist", 27 | "bundles" 28 | ], 29 | "repository": { 30 | "type": "git", 31 | "url": "git+https://github.com/cloudevents/sdk-javascript.git" 32 | }, 33 | "keywords": [ 34 | "events", 35 | "cloudevents", 36 | "sdk", 37 | "javascript", 38 | "cncf" 39 | ], 40 | "author": "cloudevents.io", 41 | "contributors": [ 42 | { 43 | "name": "Fábio José de Moraes", 44 | "email": "fabiojose@gmail.com", 45 | "url": "https://github.com/fabiojose" 46 | }, 47 | { 48 | "name": "Lance Ball", 49 | "email": "lball@redhat.com", 50 | "url": "https://github.com/lance" 51 | }, 52 | { 53 | "name": "Lucas Holmquist", 54 | "email": "lholmqui@redhat.com", 55 | "url": "https://github.com/lholmquist" 56 | }, 57 | { 58 | "name": "Grant Timmerman", 59 | "url": "https://github.com/grant" 60 | }, 61 | { 62 | "name": "Daniel Bevenius", 63 | "email": "daniel.bevenius@gmail.com", 64 | "url": "https://github.com/danbev" 65 | }, 66 | { 67 | "name": "Helio Frota", 68 | "url": "https://github.com/helio-frota" 69 | }, 70 | { 71 | "name": "Doug Davis", 72 | "email": "dug@us.ibm.com", 73 | "url": "https://github.com/duglin" 74 | }, 75 | { 76 | "name": "Remi Cattiau", 77 | "email": "rcattiau@gmail.com", 78 | "url": "https://github.com/loopingz" 79 | }, 80 | { 81 | "name": "Michele Angioni", 82 | "url": "https://github.com/micheleangioni" 83 | }, 84 | { 85 | "name": "Ali Ok", 86 | "email": "aliok@redhat.com", 87 | "url": "https://github.com/aliok" 88 | }, 89 | { 90 | "name": "Philip Hayes", 91 | "url": "https://github.com/deewhyweb" 92 | }, 93 | { 94 | "name": "Jingwen Peng", 95 | "url": "https://github.com/pengsrc" 96 | }, 97 | { 98 | "name": "Sidharth Vinod", 99 | "email": "sidharthv96@gmail.com", 100 | "url": "https://github.com/sidharthv96" 101 | }, 102 | { 103 | "name": "Matej Vasek", 104 | "url": "https://github.com/matejvasek" 105 | } 106 | ], 107 | "license": "Apache-2.0", 108 | "bugs": { 109 | "url": "https://github.com/cloudevents/sdk-javascript/issues" 110 | }, 111 | "homepage": "https://github.com/cloudevents/sdk-javascript#readme", 112 | "dependencies": { 113 | "ajv": "^8.11.0", 114 | "ajv-formats": "^2.1.1", 115 | "process": "^0.11.10", 116 | "json-bigint": "^1.0.0", 117 | "util": "^0.12.4", 118 | "uuid": "^8.3.2" 119 | }, 120 | "devDependencies": { 121 | "@cucumber/cucumber": "^8.0.0", 122 | "@types/chai": "^4.2.11", 123 | "@types/cucumber": "^6.0.1", 124 | "@types/got": "^9.6.11", 125 | "@types/json-bigint": "^1.0.1", 126 | "@types/mocha": "^7.0.2", 127 | "@types/node": "^14.14.10", 128 | "@types/superagent": "^4.1.10", 129 | "@types/uuid": "^8.3.4", 130 | "@typescript-eslint/eslint-plugin": "^4.29.0", 131 | "@typescript-eslint/parser": "^4.29.0", 132 | "ajv-cli": "^5.0.0", 133 | "axios": "^0.26.1", 134 | "chai": "~4.2.0", 135 | "eslint": "^7.32.0", 136 | "eslint-config-standard": "^16.0.3", 137 | "eslint-plugin-header": "^3.1.1", 138 | "eslint-plugin-import": "^2.23.4", 139 | "eslint-plugin-node": "^11.1.0", 140 | "eslint-plugin-promise": "^5.1.0", 141 | "got": "^11.8.5", 142 | "http-parser-js": "^0.5.2", 143 | "mocha": "^10.1.0", 144 | "nock": "~12.0.3", 145 | "nyc": "~15.0.0", 146 | "prettier": "^2.0.5", 147 | "remark-cli": "^10.0.0", 148 | "remark-lint": "^8.0.0", 149 | "remark-lint-list-item-indent": "^2.0.1", 150 | "remark-preset-lint-recommended": "^5.0.0", 151 | "superagent": "^7.1.1", 152 | "ts-node": "^10.8.1", 153 | "typedoc": "^0.22.11", 154 | "typescript": "^4.3.5", 155 | "webpack": "^5.76.0", 156 | "webpack-cli": "^4.10.0" 157 | }, 158 | "publishConfig": { 159 | "access": "public" 160 | }, 161 | "types": "./dist/index.d.ts", 162 | "engines": { 163 | "node": ">=20 <=24" 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /pr_guidelines.md: -------------------------------------------------------------------------------- 1 | # Pull Request Guidelines 2 | 3 | Here you will find step by step guidance for creating, submitting and updating 4 | a pull request in this repository. We hope it will help you have an easy time 5 | managing your work and a positive, satisfying experience when contributing 6 | your code. Thanks for getting involved! :rocket: 7 | 8 | - [Pull Request Guidelines](#pull-request-guidelines) 9 | - [Getting Started](#getting-started) 10 | - [Branches](#branches) 11 | - [Commit Messages](#commit-messages) 12 | - [Signing your commits](#signing-your-commits) 13 | - [Staying Current with `main`](#staying-current-with-main) 14 | - [Style Guide](#style-guide) 15 | - [Submitting and Updating Your Pull Request](#submitting-and-updating-your-pull-request) 16 | - [Congratulations!](#congratulations) 17 | 18 | ## Getting Started 19 | 20 | When creating a pull request, first fork this repository and clone it to your 21 | local development environment. Then add this repository as the upstream. 22 | 23 | ```console 24 | git clone https://github.com/mygithuborg/sdk-javascript.git 25 | cd sdk-javascript 26 | git remote add upstream https://github.com/cloudevents/sdk-javascript.git 27 | ``` 28 | 29 | ## Branches 30 | 31 | The first thing you'll need to do is create a branch for your work. 32 | If you are submitting a pull request that fixes or relates to an existing 33 | GitHub issue, you can use this in your branch name to keep things organized. 34 | For example, if you were to create a pull request to fix 35 | [this error with `httpAgent`](https://github.com/cloudevents/sdk-javascript/issues/48) 36 | you might create a branch named `48-fix-http-agent-error`. 37 | 38 | ```console 39 | git fetch upstream 40 | git reset --hard upstream/main 41 | git checkout FETCH_HEAD 42 | git checkout -b 48-fix-http-agent-error 43 | ``` 44 | 45 | ## Commit Messages 46 | 47 | Please follow the 48 | [Conventional Commits specification](https://www.conventionalcommits.org/en/v1.0.0/#summary). 49 | The first line of your commit should be prefixed with a type, be a single 50 | sentence with no period, and succinctly indicate what this commit changes. 51 | 52 | All commit message lines should be kept to fewer than 80 characters if possible. 53 | 54 | An example of a good commit message. 55 | 56 | ```log 57 | docs: remove 0.1, 0.2 spec support from README 58 | ``` 59 | 60 | If you are unsure what prefix to use for a commit, you can consult the 61 | [package.json](package.json) file. 62 | In the `standard-version.types` section, you can see all of the commit 63 | types that will be committed to the changelog based on the prefix in the first line of 64 | your commit message. For example, the commit message: 65 | 66 | ```log 67 | fix: removed a bug that was causing the rotation of the earth to change 68 | ``` 69 | 70 | will show up in the "Bug Fixes" section of the changelog for a given release. 71 | 72 | ### Signing your commits 73 | 74 | Each commit must be signed. Use the `--signoff` flag for your commits. 75 | 76 | ```console 77 | git commit --signoff 78 | ``` 79 | 80 | This will add a line to every git commit message: 81 | 82 | Signed-off-by: Joe Smith 83 | 84 | Use your real name (sorry, no pseudonyms or anonymous contributions.) 85 | 86 | The sign-off is a signature line at the end of your commit message. Your 87 | signature certifies that you wrote the patch or otherwise have the right to pass 88 | it on as open-source code. See [developercertificate.org](http://developercertificate.org/) 89 | for the full text of the certification. 90 | 91 | Be sure to have your `user.name` and `user.email` set in your git config. 92 | If your git config information is set properly then viewing the `git log` 93 | information for your commit will look something like this: 94 | 95 | ``` 96 | Author: Joe Smith 97 | Date: Thu Feb 2 11:41:15 2018 -0800 98 | 99 | Update README 100 | 101 | Signed-off-by: Joe Smith 102 | ``` 103 | 104 | Notice the `Author` and `Signed-off-by` lines match. If they don't your PR will 105 | be rejected by the automated DCO check. 106 | 107 | ## Staying Current with `main` 108 | 109 | As you are working on your branch, changes may happen on `main`. Before 110 | submitting your pull request, be sure that your branch has been updated 111 | with the latest commits. 112 | 113 | ```console 114 | git fetch upstream 115 | git rebase upstream/main 116 | ``` 117 | 118 | This may cause conflicts if the files you are changing on your branch are 119 | also changed on main. Error messages from `git` will indicate if conflicts 120 | exist and what files need attention. Resolve the conflicts in each file, then 121 | continue with the rebase with `git rebase --continue`. 122 | 123 | 124 | If you've already pushed some changes to your `origin` fork, you'll 125 | need to force push these changes. 126 | 127 | ```console 128 | git push -f origin 48-fix-http-agent-error 129 | ``` 130 | 131 | ## Style Guide 132 | 133 | Code style for this module is maintained using [`eslint`](https://www.npmjs.com/package/eslint). 134 | When you run tests with `npm test` linting is performed first. If you want to 135 | check your code style for linting errors without running tests, you can just 136 | run `npm run lint`. If there are errors, you can usually fix them automatically 137 | by running `npm run fix`. 138 | 139 | Linting rules are declared in [.eslintrc](https://github.com/cloudevents/sdk-javascript/blob/main/.eslintrc). 140 | 141 | ## Submitting and Updating Your Pull Request 142 | 143 | Before submitting a pull request, you should make sure that all of the tests 144 | successfully pass by running `npm test`. 145 | 146 | Once you have sent your pull request, `main` may continue to evolve 147 | before your pull request has landed. If there are any commits on `main` 148 | that conflict with your changes, you may need to update your branch with 149 | these changes before the pull request can land. Resolve conflicts the same 150 | way as before. 151 | 152 | ```console 153 | git fetch upstream 154 | git rebase upstream/main 155 | # fix any potential conflicts 156 | git push -f origin 48-fix-http-agent-error 157 | ``` 158 | 159 | This will cause the pull request to be updated with your changes, and 160 | CI will rerun. 161 | 162 | A maintainer may ask you to make changes to your pull request. Sometimes these 163 | changes are minor and shouldn't appear in the commit log. For example, you may 164 | have a typo in one of your code comments that should be fixed before merge. 165 | You can prevent this from adding noise to the commit log with an interactive 166 | rebase. See the [git documentation](https://git-scm.com/book/en/v2/Git-Tools-Rewriting-History) 167 | for details. 168 | 169 | ```console 170 | git commit -m "fixup: fix typo" 171 | git rebase -i upstream/main # follow git instructions 172 | ``` 173 | 174 | Once you have rebased your commits, you can force push to your fork as before. 175 | 176 | ## Congratulations! 177 | 178 | Congratulations! You've done it! We really appreciate the time and energy 179 | you've given to the project. Thank you. 180 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 The CloudEvents Authors 3 | SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | const CONSTANTS = Object.freeze({ 7 | CHARSET_DEFAULT: "utf-8", 8 | EXTENSIONS_PREFIX: "ce-", 9 | ENCODING_BASE64: "base64", 10 | DATA_ATTRIBUTE: "data", 11 | 12 | MIME_JSON: "application/json", 13 | MIME_OCTET_STREAM: "application/octet-stream", 14 | MIME_CE: "application/cloudevents", 15 | MIME_CE_JSON: "application/cloudevents+json", 16 | MIME_CE_BATCH: "application/cloudevents-batch+json", 17 | HEADER_CONTENT_TYPE: "content-type", 18 | DEFAULT_CONTENT_TYPE: "application/json; charset=utf-8", 19 | DEFAULT_CE_CONTENT_TYPE: "application/cloudevents+json; charset=utf-8", 20 | 21 | CE_HEADERS: { 22 | TYPE: "ce-type", 23 | SPEC_VERSION: "ce-specversion", 24 | SOURCE: "ce-source", 25 | ID: "ce-id", 26 | TIME: "ce-time", 27 | SUBJECT: "ce-subject", 28 | }, 29 | 30 | CE_ATTRIBUTES: { 31 | ID: "id", 32 | TYPE: "type", 33 | SOURCE: "source", 34 | SPEC_VERSION: "specversion", 35 | TIME: "time", 36 | CONTENT_TYPE: "datacontenttype", 37 | SUBJECT: "subject", 38 | DATA: "data", 39 | }, 40 | 41 | BINARY_HEADERS_03: { 42 | SCHEMA_URL: "ce-schemaurl", 43 | CONTENT_ENCODING: "ce-datacontentencoding", 44 | }, 45 | STRUCTURED_ATTRS_03: { 46 | SCHEMA_URL: "schemaurl", 47 | CONTENT_ENCODING: "datacontentencoding", 48 | }, 49 | 50 | BINARY_HEADERS_1: { 51 | DATA_SCHEMA: "ce-dataschema", 52 | }, 53 | STRUCTURED_ATTRS_1: { 54 | DATA_SCHEMA: "dataschema", 55 | DATA_BASE64: "data_base64", 56 | }, 57 | USE_BIG_INT_ENV: "CE_USE_BIG_INT" 58 | } as const); 59 | 60 | export default CONSTANTS; 61 | -------------------------------------------------------------------------------- /src/event/cloudevent.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 The CloudEvents Authors 3 | SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | import { ErrorObject } from "ajv"; 7 | import { v4 as uuidv4 } from "uuid"; 8 | import { Emitter } from ".."; 9 | 10 | import { CloudEventV1 } from "./interfaces"; 11 | import { validateCloudEvent } from "./spec"; 12 | import { ValidationError, isBinary, asBase64, isValidType, base64AsBinary } from "./validation"; 13 | 14 | /** 15 | * Constants representing the CloudEvent specification version 16 | */ 17 | export const V1 = "1.0"; 18 | export const V03 = "0.3"; 19 | 20 | /** 21 | * A CloudEvent describes event data in common formats to provide 22 | * interoperability across services, platforms and systems. 23 | * @see https://github.com/cloudevents/spec/blob/v1.0/spec.md 24 | */ 25 | export class CloudEvent implements CloudEventV1 { 26 | id: string; 27 | type: string; 28 | source: string; 29 | specversion: string; 30 | datacontenttype?: string; 31 | dataschema?: string; 32 | subject?: string; 33 | time?: string; 34 | data?: T; 35 | data_base64?: string; 36 | 37 | // Extensions should not exist as it's own object, but instead 38 | // exist as properties on the event as siblings of the others 39 | [key: string]: unknown; 40 | 41 | // V03 deprecated attributes 42 | schemaurl?: string; 43 | datacontentencoding?: string; 44 | 45 | /** 46 | * Creates a new CloudEvent object with the provided properties. If there is a chance that the event 47 | * properties will not conform to the CloudEvent specification, you may pass a boolean `false` as a 48 | * second parameter to bypass event validation. 49 | * 50 | * @param {object} event the event properties 51 | * @param {boolean?} strict whether to perform event validation when creating the object - default: true 52 | */ 53 | constructor(event: Partial>, strict = true) { 54 | // copy the incoming event so that we can delete properties as we go 55 | // everything left after we have deleted know properties becomes an extension 56 | const properties = { ...event }; 57 | 58 | this.id = (properties.id as string) || uuidv4(); 59 | delete properties.id; 60 | 61 | this.time = properties.time || new Date().toISOString(); 62 | delete properties.time; 63 | 64 | this.type = properties.type as string; 65 | delete (properties as any).type; 66 | 67 | this.source = properties.source as string; 68 | delete (properties as any).source; 69 | 70 | this.specversion = (properties.specversion) || V1; 71 | delete properties.specversion; 72 | 73 | this.datacontenttype = properties.datacontenttype; 74 | delete properties.datacontenttype; 75 | 76 | this.subject = properties.subject; 77 | delete properties.subject; 78 | 79 | this.datacontentencoding = properties.datacontentencoding as string; 80 | delete properties.datacontentencoding; 81 | 82 | this.dataschema = properties.dataschema as string; 83 | delete properties.dataschema; 84 | 85 | this.data_base64 = properties.data_base64 as string; 86 | 87 | if (this.data_base64) { 88 | this.data = base64AsBinary(this.data_base64) as unknown as T; 89 | } 90 | 91 | delete properties.data_base64; 92 | 93 | this.schemaurl = properties.schemaurl as string; 94 | delete properties.schemaurl; 95 | 96 | if (isBinary(properties.data)) { 97 | this.data_base64 = asBase64(properties.data as unknown as Buffer); 98 | } 99 | 100 | this.data = typeof properties.data !== "undefined" ? properties.data : this.data; 101 | delete properties.data; 102 | 103 | // sanity checking 104 | if (this.specversion === V1 && this.schemaurl) { 105 | throw new TypeError("cannot set schemaurl on version 1.0 event"); 106 | } else if (this.specversion === V03 && this.dataschema) { 107 | throw new TypeError("cannot set dataschema on version 0.3 event"); 108 | } 109 | 110 | // finally process any remaining properties - these are extensions 111 | for (const [key, value] of Object.entries(properties)) { 112 | // Extension names must only allow lowercase a-z and 0-9 in the name 113 | // names should not exceed 20 characters in length 114 | if (!key.match(/^[a-z0-9]+$/) && strict) { 115 | throw new ValidationError(`invalid extension name: ${key} 116 | CloudEvents attribute names MUST consist of lower-case letters ('a' to 'z') 117 | or digits ('0' to '9') from the ASCII character set. Attribute names SHOULD 118 | be descriptive and terse and SHOULD NOT exceed 20 characters in length.`); 119 | } 120 | 121 | // Value should be spec compliant 122 | // https://github.com/cloudevents/spec/blob/master/spec.md#type-system 123 | if (!isValidType(value) && strict) { 124 | throw new ValidationError(`invalid extension value: ${value} 125 | Extension values must conform to the CloudEvent type system. 126 | See: https://github.com/cloudevents/spec/blob/v1.0/spec.md#type-system`); 127 | } 128 | 129 | this[key] = value; 130 | } 131 | 132 | strict ? this.validate() : undefined; 133 | 134 | Object.freeze(this); 135 | } 136 | 137 | /** 138 | * Used by JSON.stringify(). The name is confusing, but this method is called by 139 | * JSON.stringify() when converting this object to JSON. 140 | * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify 141 | * @return {object} this event as a plain object 142 | */ 143 | toJSON(): Record { 144 | const event = { ...this }; 145 | event.time = new Date(this.time as string).toISOString(); 146 | 147 | if (event.data_base64 && event.data) { 148 | delete event.data; 149 | } 150 | 151 | return event; 152 | } 153 | 154 | toString(): string { 155 | return JSON.stringify(this); 156 | } 157 | 158 | /** 159 | * Validates this CloudEvent against the schema 160 | * @throws if the CloudEvent does not conform to the schema 161 | * @return {boolean} true if this event is valid 162 | */ 163 | public validate(): boolean { 164 | try { 165 | return validateCloudEvent(this); 166 | } catch (e) { 167 | if (e instanceof ValidationError) { 168 | throw e; 169 | } else { 170 | throw new ValidationError("invalid payload", [e] as ErrorObject[]); 171 | } 172 | } 173 | } 174 | 175 | /** 176 | * Emit this CloudEvent through the application 177 | * 178 | * @param {boolean} ensureDelivery fail the promise if one listener fail 179 | * @return {Promise} this 180 | */ 181 | public async emit(ensureDelivery = true): Promise { 182 | await Emitter.emitEvent(this, ensureDelivery); 183 | return this; 184 | } 185 | 186 | /** 187 | * Clone a CloudEvent with new/updated attributes 188 | * @param {object} options attributes to augment the CloudEvent without a `data` property 189 | * @param {boolean} strict whether or not to use strict validation when cloning (default: true) 190 | * @throws if the CloudEvent does not conform to the schema 191 | * @return {CloudEvent} returns a new CloudEvent 192 | */ 193 | public cloneWith(options: Partial, "data">>, strict?: boolean): CloudEvent; 194 | /** 195 | * Clone a CloudEvent with new/updated attributes and new data 196 | * @param {object} options attributes to augment the CloudEvent with a `data` property and type 197 | * @param {boolean} strict whether or not to use strict validation when cloning (default: true) 198 | * @throws if the CloudEvent does not conform to the schema 199 | * @return {CloudEvent} returns a new CloudEvent 200 | */ 201 | public cloneWith(options: Partial>, strict?: boolean): CloudEvent; 202 | /** 203 | * Clone a CloudEvent with new/updated attributes and possibly different data types 204 | * @param {object} options attributes to augment the CloudEvent 205 | * @param {boolean} strict whether or not to use strict validation when cloning (default: true) 206 | * @throws if the CloudEvent does not conform to the schema 207 | * @return {CloudEvent} returns a new CloudEvent 208 | */ 209 | public cloneWith(options: Partial>, strict = true): CloudEvent { 210 | return CloudEvent.cloneWith(this, options, strict); 211 | } 212 | 213 | /** 214 | * The native `console.log` value of the CloudEvent. 215 | * @return {string} The string representation of the CloudEvent. 216 | */ 217 | [Symbol.for("nodejs.util.inspect.custom")](): string { 218 | return this.toString(); 219 | } 220 | 221 | /** 222 | * Clone a CloudEvent with new or updated attributes. 223 | * @param {CloudEventV1} event an object that implements the {@linkcode CloudEventV1} interface 224 | * @param {Partial>} options an object with new or updated attributes 225 | * @param {boolean} strict `true` if the resulting event should be valid per the CloudEvent specification 226 | * @throws {ValidationError} if `strict` is `true` and the resulting event is invalid 227 | * @returns {CloudEvent} a CloudEvent cloned from `event` with `options` applied. 228 | */ 229 | public static cloneWith( 230 | event: CloudEventV1, 231 | options: Partial>, 232 | strict = true): CloudEvent { 233 | return new CloudEvent(Object.assign({}, event, options), strict); 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /src/event/interfaces.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 The CloudEvents Authors 3 | SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | /** 7 | * The object interface for CloudEvents 1.0. 8 | * @see https://github.com/cloudevents/spec/blob/v1.0/spec.md 9 | */ 10 | export interface CloudEventV1 extends CloudEventV1Attributes { 11 | // REQUIRED Attributes 12 | /** 13 | * [REQUIRED] Identifies the event. Producers MUST ensure that `source` + `id` 14 | * is unique for each distinct event. If a duplicate event is re-sent (e.g. due 15 | * to a network error) it MAY have the same `id`. Consumers MAY assume that 16 | * Events with identical `source` and `id` are duplicates. 17 | * @required Non-empty string. Unique within producer. 18 | * @example An event counter maintained by the producer 19 | * @example A UUID 20 | */ 21 | id: string; 22 | 23 | /** 24 | * [REQUIRED] The version of the CloudEvents specification which the event 25 | * uses. This enables the interpretation of the context. Compliant event 26 | * producers MUST use a value of `1.0` when referring to this version of the 27 | * specification. 28 | * @required MUST be a non-empty string. 29 | */ 30 | specversion: string; 31 | } 32 | 33 | export interface CloudEventV1Attributes extends CloudEventV1OptionalAttributes { 34 | /** 35 | * [REQUIRED] Identifies the context in which an event happened. Often this 36 | * will include information such as the type of the event source, the 37 | * organization publishing the event or the process that produced the event. The 38 | * exact syntax and semantics behind the data encoded in the URI is defined by 39 | * the event producer. 40 | * Producers MUST ensure that `source` + `id` is unique for each distinct event. 41 | * An application MAY assign a unique `source` to each distinct producer, which 42 | * makes it easy to produce unique IDs since no other producer will have the same 43 | * source. The application MAY use UUIDs, URNs, DNS authorities or an 44 | * application-specific scheme to create unique `source` identifiers. 45 | * A source MAY include more than one producer. In that case the producers MUST 46 | * collaborate to ensure that `source` + `id` is unique for each distinct event. 47 | * @required Non-empty URI-reference 48 | */ 49 | source: string; 50 | 51 | /** 52 | * [REQUIRED] This attribute contains a value describing the type of event 53 | * related to the originating occurrence. Often this attribute is used for 54 | * routing, observability, policy enforcement, etc. The format of this is 55 | * producer defined and might include information such as the version of the 56 | * `type` - see 57 | * [Versioning of Attributes in the Primer](primer.md#versioning-of-attributes) 58 | * for more information. 59 | * @required MUST be a non-empty string 60 | * @should SHOULD be prefixed with a reverse-DNS name. The prefixed domain dictates the 61 | * organization which defines the semantics of this event type. 62 | * @example com.github.pull.create 63 | * @example com.example.object.delete.v2 64 | */ 65 | type: string; 66 | } 67 | 68 | export interface CloudEventV1OptionalAttributes { 69 | /** 70 | * The following fields are optional. 71 | */ 72 | 73 | /** 74 | * [OPTIONAL] Content type of `data` value. This attribute enables `data` to 75 | * carry any type of content, whereby format and encoding might differ from that 76 | * of the chosen event format. For example, an event rendered using the 77 | * [JSON envelope](./json-format.md#3-envelope) format might carry an XML payload 78 | * in `data`, and the consumer is informed by this attribute being set to 79 | * "application/xml". The rules for how `data` content is rendered for different 80 | * `datacontenttype` values are defined in the event format specifications; for 81 | * example, the JSON event format defines the relationship in 82 | * [section 3.1](./json-format.md#31-handling-of-data). 83 | */ 84 | datacontenttype?: string; 85 | /** 86 | * [OPTIONAL] Identifies the schema that `data` adheres to. Incompatible 87 | * changes to the schema SHOULD be reflected by a different URI. See 88 | * [Versioning of Attributes in the Primer](primer.md#versioning-of-attributes) 89 | * for more information. 90 | * If present, MUST be a non-empty URI. 91 | */ 92 | dataschema?: string; 93 | /** 94 | * [OPTIONAL] This describes the subject of the event in the context of the 95 | * event producer (identified by `source`). In publish-subscribe scenarios, a 96 | * subscriber will typically subscribe to events emitted by a `source`, but the 97 | * `source` identifier alone might not be sufficient as a qualifier for any 98 | * specific event if the `source` context has internal sub-structure. 99 | * 100 | * Identifying the subject of the event in context metadata (opposed to only in 101 | * the `data` payload) is particularly helpful in generic subscription filtering 102 | * scenarios where middleware is unable to interpret the `data` content. In the 103 | * above example, the subscriber might only be interested in blobs with names 104 | * ending with '.jpg' or '.jpeg' and the `subject` attribute allows for 105 | * constructing a simple and efficient string-suffix filter for that subset of 106 | * events. 107 | * 108 | * If present, MUST be a non-empty string. 109 | * @example "https://example.com/storage/tenant/container" 110 | * @example "mynewfile.jpg" 111 | */ 112 | subject?: string; 113 | /** 114 | * [OPTIONAL] Timestamp of when the occurrence happened. If the time of the 115 | * occurrence cannot be determined then this attribute MAY be set to some other 116 | * time (such as the current time) by the CloudEvents producer, however all 117 | * producers for the same `source` MUST be consistent in this respect. In other 118 | * words, either they all use the actual time of the occurrence or they all use 119 | * the same algorithm to determine the value used. 120 | * @example "2020-08-08T14:48:09.769Z" 121 | */ 122 | time?: string; 123 | /** 124 | * [OPTIONAL] The event payload. This specification does not place any restriction 125 | * on the type of this information. It is encoded into a media format which is 126 | * specified by the datacontenttype attribute (e.g. application/json), and adheres 127 | * to the dataschema format when those respective attributes are present. 128 | */ 129 | data?: T; 130 | 131 | /** 132 | * [OPTIONAL] The event payload encoded as base64 data. This is used when the 133 | * data is in binary form. 134 | * @see https://github.com/cloudevents/spec/blob/v1.0/json-format.md#31-handling-of-data 135 | */ 136 | data_base64?: string; 137 | 138 | /** 139 | * [OPTIONAL] CloudEvents extension attributes. 140 | */ 141 | [key: string]: unknown; 142 | } 143 | -------------------------------------------------------------------------------- /src/event/spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 The CloudEvents Authors 3 | SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | import { ValidationError } from "./validation"; 7 | 8 | import { CloudEventV1 } from "./interfaces"; 9 | import { V1 } from "./cloudevent"; 10 | import validate from "../schema/v1"; 11 | 12 | 13 | export function validateCloudEvent(event: CloudEventV1): boolean { 14 | if (event.specversion === V1) { 15 | if (!validate(event)) { 16 | throw new ValidationError("invalid payload", (validate as any).errors); 17 | } 18 | } else { 19 | return false; 20 | } 21 | // attribute names must all be [a-z|0-9] 22 | const validation = /^[a-z0-9]+$/; 23 | for (const key in event) { 24 | if (validation.test(key) === false && key !== "data_base64") { 25 | throw new ValidationError(`invalid attribute name: "${key}"`); 26 | } 27 | } 28 | return true; 29 | } 30 | -------------------------------------------------------------------------------- /src/event/validation.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 The CloudEvents Authors 3 | SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | import { ErrorObject } from "ajv"; 7 | 8 | export type TypeArray = Int8Array | Uint8Array | Int16Array | Uint16Array | 9 | Int32Array | Uint32Array | Uint8ClampedArray | Float32Array | Float64Array; 10 | 11 | const globalThisPolyfill = (function() { 12 | try { 13 | return globalThis; 14 | } 15 | catch (e) { 16 | try { 17 | return self; 18 | } 19 | catch (e) { 20 | return global; 21 | } 22 | } 23 | }()); 24 | 25 | /** 26 | * An Error class that will be thrown when a CloudEvent 27 | * cannot be properly validated against a specification. 28 | */ 29 | export class ValidationError extends TypeError { 30 | errors?: string[] | ErrorObject[] | null; 31 | 32 | constructor(message: string, errors?: string[] | ErrorObject[] | null) { 33 | const messageString = 34 | errors instanceof Array 35 | ? // eslint-disable-next-line @typescript-eslint/ban-ts-comment 36 | // @ts-ignore 37 | errors?.reduce( 38 | (accum: string, err: Record) => 39 | accum.concat(` 40 | ${err instanceof Object ? JSON.stringify(err) : err}`), 41 | message, 42 | ) 43 | : message; 44 | super(messageString); 45 | this.errors = errors ? errors : []; 46 | } 47 | } 48 | 49 | export const isString = (v: unknown): boolean => typeof v === "string"; 50 | export const isObject = (v: unknown): boolean => typeof v === "object"; 51 | export const isDefined = (v: unknown): boolean => v !== null && typeof v !== "undefined"; 52 | 53 | export const isBoolean = (v: unknown): boolean => typeof v === "boolean"; 54 | export const isInteger = (v: unknown): boolean => Number.isInteger(v as number); 55 | export const isDate = (v: unknown): v is Date => v instanceof Date; 56 | export const isBinary = (v: unknown): boolean => ArrayBuffer.isView(v); 57 | 58 | export const isStringOrThrow = (v: unknown, t: Error): boolean => 59 | isString(v) 60 | ? true 61 | : (() => { 62 | throw t; 63 | })(); 64 | 65 | export const isDefinedOrThrow = (v: unknown, t: Error): boolean => 66 | isDefined(v) 67 | ? true 68 | : (() => { 69 | throw t; 70 | })(); 71 | 72 | export const isStringOrObjectOrThrow = (v: unknown, t: Error): boolean => 73 | isString(v) 74 | ? true 75 | : isObject(v) 76 | ? true 77 | : (() => { 78 | throw t; 79 | })(); 80 | 81 | export const equalsOrThrow = (v1: unknown, v2: unknown, t: Error): boolean => 82 | v1 === v2 83 | ? true 84 | : (() => { 85 | throw t; 86 | })(); 87 | 88 | export const isBase64 = (value: unknown): boolean => 89 | Buffer.from(value as string, "base64").toString("base64") === value; 90 | 91 | export const isBuffer = (value: unknown): boolean => value instanceof Buffer; 92 | 93 | export const asBuffer = (value: string | Buffer | TypeArray): Buffer => 94 | isBinary(value) 95 | ? Buffer.from((value as unknown) as string) 96 | : isBuffer(value) 97 | ? (value as Buffer) 98 | : (() => { 99 | throw new TypeError("is not buffer or a valid binary"); 100 | })(); 101 | 102 | export const base64AsBinary = (base64String: string): Uint8Array => { 103 | const toBinaryString = (base64Str: string): string => globalThisPolyfill.atob 104 | ? globalThisPolyfill.atob(base64Str) 105 | : Buffer.from(base64Str, "base64").toString("binary"); 106 | 107 | return Uint8Array.from(toBinaryString(base64String), (c) => c.charCodeAt(0)); 108 | }; 109 | 110 | export const asBase64 = 111 | (value: string | Buffer | TypeArray): string => asBuffer(value).toString("base64"); 112 | 113 | export const clone = (o: Record): Record => JSON.parse(JSON.stringify(o)); 114 | 115 | export const isJsonContentType = (contentType: string): "" | RegExpMatchArray | null => 116 | contentType && contentType.match(/(json)/i); 117 | 118 | export const asData = (data: unknown, contentType: string): string => { 119 | // pattern matching alike 120 | const maybeJson = 121 | isString(data) && !isBase64(data) && isJsonContentType(contentType) ? JSON.parse(data as string) : data; 122 | 123 | return isBinary(maybeJson) ? asBase64(maybeJson) : maybeJson; 124 | }; 125 | 126 | export const isValidType = (v: boolean | number | string | Date | TypeArray | unknown): boolean => 127 | isBoolean(v) || isInteger(v) || isString(v) || isDate(v) || isBinary(v) || isObject(v); 128 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 The CloudEvents Authors 3 | SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | import { CloudEvent, V1, V03 } from "./event/cloudevent"; 7 | import { ValidationError } from "./event/validation"; 8 | import { CloudEventV1, CloudEventV1Attributes } from "./event/interfaces"; 9 | 10 | import { Options, TransportFunction, EmitterFunction, emitterFor, Emitter } from "./transport/emitter"; 11 | import { httpTransport } from "./transport/http"; 12 | import { 13 | Headers, Mode, Binding, HTTP, Kafka, KafkaEvent, KafkaMessage, Message, MQTT, MQTTMessage, MQTTMessageFactory, 14 | Serializer, Deserializer } from "./message"; 15 | 16 | import CONSTANTS from "./constants"; 17 | 18 | export { 19 | // From event 20 | CloudEvent, 21 | V1, 22 | V03, 23 | ValidationError, 24 | Mode, 25 | HTTP, 26 | Kafka, 27 | MQTT, 28 | MQTTMessageFactory, 29 | emitterFor, 30 | httpTransport, 31 | Emitter, 32 | // From Constants 33 | CONSTANTS 34 | }; 35 | 36 | export type { 37 | CloudEventV1, 38 | CloudEventV1Attributes, 39 | // From message 40 | Headers, 41 | Binding, 42 | Message, 43 | Deserializer, 44 | Serializer, 45 | KafkaEvent, 46 | KafkaMessage, 47 | MQTTMessage, 48 | // From transport 49 | TransportFunction, 50 | EmitterFunction, 51 | Options 52 | }; 53 | -------------------------------------------------------------------------------- /src/message/http/headers.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 The CloudEvents Authors 3 | SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | import { PassThroughParser, DateParser, MappedParser } from "../../parsers"; 7 | import { CloudEventV1 } from "../.."; 8 | import { Headers } from "../"; 9 | import { V1 } from "../../event/cloudevent"; 10 | import CONSTANTS from "../../constants"; 11 | 12 | export const allowedContentTypes = [CONSTANTS.DEFAULT_CONTENT_TYPE, CONSTANTS.MIME_JSON, CONSTANTS.MIME_OCTET_STREAM]; 13 | export const requiredHeaders = [ 14 | CONSTANTS.CE_HEADERS.ID, 15 | CONSTANTS.CE_HEADERS.SOURCE, 16 | CONSTANTS.CE_HEADERS.TYPE, 17 | CONSTANTS.CE_HEADERS.SPEC_VERSION, 18 | ]; 19 | 20 | /** 21 | * Returns the HTTP headers that will be sent for this event when the HTTP transmission 22 | * mode is "binary". Events sent over HTTP in structured mode only have a single CE header 23 | * and that is "ce-id", corresponding to the event ID. 24 | * @param {CloudEvent} event a CloudEvent 25 | * @returns {Object} the headers that will be sent for the event 26 | */ 27 | export function headersFor(event: CloudEventV1): Headers { 28 | const headers: Headers = {}; 29 | let headerMap: Readonly<{ [key: string]: MappedParser }>; 30 | if (event.specversion === V1) { 31 | headerMap = v1headerMap; 32 | } else { 33 | headerMap = v03headerMap; 34 | } 35 | 36 | // iterate over the event properties - generate a header for each 37 | Object.getOwnPropertyNames(event).forEach((property) => { 38 | const value = event[property]; 39 | if (value !== undefined) { 40 | const map: MappedParser | undefined = headerMap[property] as MappedParser; 41 | if (map) { 42 | headers[map.name] = map.parser.parse(value as string) as string; 43 | } else if (property !== CONSTANTS.DATA_ATTRIBUTE && property !== `${CONSTANTS.DATA_ATTRIBUTE}_base64`) { 44 | headers[`${CONSTANTS.EXTENSIONS_PREFIX}${property}`] = value as string; 45 | } 46 | } 47 | }); 48 | // Treat time specially, since it's handled with getters and setters in CloudEvent 49 | if (event.time) { 50 | headers[CONSTANTS.CE_HEADERS.TIME] = new Date(event.time).toISOString(); 51 | } 52 | return headers; 53 | } 54 | 55 | /** 56 | * Sanitizes incoming headers by lowercasing them and potentially removing 57 | * encoding from the content-type header. 58 | * @param {Headers} headers HTTP headers as key/value pairs 59 | * @returns {Headers} the sanitized headers 60 | */ 61 | export function sanitize(headers: Headers): Headers { 62 | const sanitized: Headers = {}; 63 | 64 | Array.from(Object.keys(headers)) 65 | .filter((header) => Object.hasOwnProperty.call(headers, header)) 66 | .forEach((header) => (sanitized[header.toLowerCase()] = headers[header])); 67 | 68 | return sanitized; 69 | } 70 | 71 | function parser(name: string, parser = new PassThroughParser()): MappedParser { 72 | return { name: name, parser: parser }; 73 | } 74 | 75 | /** 76 | * A utility Map used to retrieve the header names for a CloudEvent 77 | * using the CloudEvent getter function. 78 | */ 79 | export const v1headerMap: Readonly<{ [key: string]: MappedParser }> = Object.freeze({ 80 | [CONSTANTS.CE_ATTRIBUTES.CONTENT_TYPE]: parser(CONSTANTS.HEADER_CONTENT_TYPE), 81 | [CONSTANTS.CE_ATTRIBUTES.SUBJECT]: parser(CONSTANTS.CE_HEADERS.SUBJECT), 82 | [CONSTANTS.CE_ATTRIBUTES.TYPE]: parser(CONSTANTS.CE_HEADERS.TYPE), 83 | [CONSTANTS.CE_ATTRIBUTES.SPEC_VERSION]: parser(CONSTANTS.CE_HEADERS.SPEC_VERSION), 84 | [CONSTANTS.CE_ATTRIBUTES.SOURCE]: parser(CONSTANTS.CE_HEADERS.SOURCE), 85 | [CONSTANTS.CE_ATTRIBUTES.ID]: parser(CONSTANTS.CE_HEADERS.ID), 86 | [CONSTANTS.CE_ATTRIBUTES.TIME]: parser(CONSTANTS.CE_HEADERS.TIME), 87 | [CONSTANTS.STRUCTURED_ATTRS_1.DATA_SCHEMA]: parser(CONSTANTS.BINARY_HEADERS_1.DATA_SCHEMA), 88 | }); 89 | 90 | export const v1binaryParsers: Record = Object.freeze({ 91 | [CONSTANTS.CE_HEADERS.TYPE]: parser(CONSTANTS.CE_ATTRIBUTES.TYPE), 92 | [CONSTANTS.CE_HEADERS.SPEC_VERSION]: parser(CONSTANTS.CE_ATTRIBUTES.SPEC_VERSION), 93 | [CONSTANTS.CE_HEADERS.SOURCE]: parser(CONSTANTS.CE_ATTRIBUTES.SOURCE), 94 | [CONSTANTS.CE_HEADERS.ID]: parser(CONSTANTS.CE_ATTRIBUTES.ID), 95 | [CONSTANTS.CE_HEADERS.TIME]: parser(CONSTANTS.CE_ATTRIBUTES.TIME, new DateParser()), 96 | [CONSTANTS.BINARY_HEADERS_1.DATA_SCHEMA]: parser(CONSTANTS.STRUCTURED_ATTRS_1.DATA_SCHEMA), 97 | [CONSTANTS.CE_HEADERS.SUBJECT]: parser(CONSTANTS.CE_ATTRIBUTES.SUBJECT), 98 | [CONSTANTS.CE_ATTRIBUTES.CONTENT_TYPE]: parser(CONSTANTS.CE_ATTRIBUTES.CONTENT_TYPE), 99 | [CONSTANTS.HEADER_CONTENT_TYPE]: parser(CONSTANTS.CE_ATTRIBUTES.CONTENT_TYPE), 100 | }); 101 | 102 | export const v1structuredParsers: Record = Object.freeze({ 103 | [CONSTANTS.CE_ATTRIBUTES.TYPE]: parser(CONSTANTS.CE_ATTRIBUTES.TYPE), 104 | [CONSTANTS.CE_ATTRIBUTES.SPEC_VERSION]: parser(CONSTANTS.CE_ATTRIBUTES.SPEC_VERSION), 105 | [CONSTANTS.CE_ATTRIBUTES.SOURCE]: parser(CONSTANTS.CE_ATTRIBUTES.SOURCE), 106 | [CONSTANTS.CE_ATTRIBUTES.ID]: parser(CONSTANTS.CE_ATTRIBUTES.ID), 107 | [CONSTANTS.CE_ATTRIBUTES.TIME]: parser(CONSTANTS.CE_ATTRIBUTES.TIME, new DateParser()), 108 | [CONSTANTS.STRUCTURED_ATTRS_1.DATA_SCHEMA]: parser(CONSTANTS.STRUCTURED_ATTRS_1.DATA_SCHEMA), 109 | [CONSTANTS.CE_ATTRIBUTES.CONTENT_TYPE]: parser(CONSTANTS.CE_ATTRIBUTES.CONTENT_TYPE), 110 | [CONSTANTS.CE_ATTRIBUTES.SUBJECT]: parser(CONSTANTS.CE_ATTRIBUTES.SUBJECT), 111 | [CONSTANTS.CE_ATTRIBUTES.DATA]: parser(CONSTANTS.CE_ATTRIBUTES.DATA), 112 | [CONSTANTS.STRUCTURED_ATTRS_1.DATA_BASE64]: parser(CONSTANTS.STRUCTURED_ATTRS_1.DATA_BASE64), 113 | }); 114 | 115 | /** 116 | * A utility Map used to retrieve the header names for a CloudEvent 117 | * using the CloudEvent getter function. 118 | */ 119 | export const v03headerMap: Readonly<{ [key: string]: MappedParser }> = Object.freeze({ 120 | [CONSTANTS.CE_ATTRIBUTES.CONTENT_TYPE]: parser(CONSTANTS.HEADER_CONTENT_TYPE), 121 | [CONSTANTS.CE_ATTRIBUTES.SUBJECT]: parser(CONSTANTS.CE_HEADERS.SUBJECT), 122 | [CONSTANTS.CE_ATTRIBUTES.TYPE]: parser(CONSTANTS.CE_HEADERS.TYPE), 123 | [CONSTANTS.CE_ATTRIBUTES.SPEC_VERSION]: parser(CONSTANTS.CE_HEADERS.SPEC_VERSION), 124 | [CONSTANTS.CE_ATTRIBUTES.SOURCE]: parser(CONSTANTS.CE_HEADERS.SOURCE), 125 | [CONSTANTS.CE_ATTRIBUTES.ID]: parser(CONSTANTS.CE_HEADERS.ID), 126 | [CONSTANTS.CE_ATTRIBUTES.TIME]: parser(CONSTANTS.CE_HEADERS.TIME), 127 | [CONSTANTS.STRUCTURED_ATTRS_03.CONTENT_ENCODING]: parser(CONSTANTS.BINARY_HEADERS_03.CONTENT_ENCODING), 128 | [CONSTANTS.STRUCTURED_ATTRS_03.SCHEMA_URL]: parser(CONSTANTS.BINARY_HEADERS_03.SCHEMA_URL), 129 | }); 130 | 131 | export const v03binaryParsers: Record = Object.freeze({ 132 | [CONSTANTS.CE_HEADERS.TYPE]: parser(CONSTANTS.CE_ATTRIBUTES.TYPE), 133 | [CONSTANTS.CE_HEADERS.SPEC_VERSION]: parser(CONSTANTS.CE_ATTRIBUTES.SPEC_VERSION), 134 | [CONSTANTS.CE_HEADERS.SOURCE]: parser(CONSTANTS.CE_ATTRIBUTES.SOURCE), 135 | [CONSTANTS.CE_HEADERS.ID]: parser(CONSTANTS.CE_ATTRIBUTES.ID), 136 | [CONSTANTS.CE_HEADERS.TIME]: parser(CONSTANTS.CE_ATTRIBUTES.TIME, new DateParser()), 137 | [CONSTANTS.BINARY_HEADERS_03.SCHEMA_URL]: parser(CONSTANTS.STRUCTURED_ATTRS_03.SCHEMA_URL), 138 | [CONSTANTS.CE_HEADERS.SUBJECT]: parser(CONSTANTS.CE_ATTRIBUTES.SUBJECT), 139 | [CONSTANTS.BINARY_HEADERS_03.CONTENT_ENCODING]: parser(CONSTANTS.STRUCTURED_ATTRS_03.CONTENT_ENCODING), 140 | [CONSTANTS.HEADER_CONTENT_TYPE]: parser(CONSTANTS.CE_ATTRIBUTES.CONTENT_TYPE), 141 | }); 142 | 143 | export const v03structuredParsers: Record = Object.freeze({ 144 | [CONSTANTS.CE_ATTRIBUTES.TYPE]: parser(CONSTANTS.CE_ATTRIBUTES.TYPE), 145 | [CONSTANTS.CE_ATTRIBUTES.SPEC_VERSION]: parser(CONSTANTS.CE_ATTRIBUTES.SPEC_VERSION), 146 | [CONSTANTS.CE_ATTRIBUTES.SOURCE]: parser(CONSTANTS.CE_ATTRIBUTES.SOURCE), 147 | [CONSTANTS.CE_ATTRIBUTES.ID]: parser(CONSTANTS.CE_ATTRIBUTES.ID), 148 | [CONSTANTS.CE_ATTRIBUTES.TIME]: parser(CONSTANTS.CE_ATTRIBUTES.TIME, new DateParser()), 149 | [CONSTANTS.STRUCTURED_ATTRS_03.SCHEMA_URL]: parser(CONSTANTS.STRUCTURED_ATTRS_03.SCHEMA_URL), 150 | [CONSTANTS.STRUCTURED_ATTRS_03.CONTENT_ENCODING]: parser(CONSTANTS.STRUCTURED_ATTRS_03.CONTENT_ENCODING), 151 | [CONSTANTS.CE_ATTRIBUTES.CONTENT_TYPE]: parser(CONSTANTS.CE_ATTRIBUTES.CONTENT_TYPE), 152 | [CONSTANTS.CE_ATTRIBUTES.SUBJECT]: parser(CONSTANTS.CE_ATTRIBUTES.SUBJECT), 153 | [CONSTANTS.CE_ATTRIBUTES.DATA]: parser(CONSTANTS.CE_ATTRIBUTES.DATA), 154 | }); 155 | -------------------------------------------------------------------------------- /src/message/http/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 The CloudEvents Authors 3 | SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | import { types } from "util"; 7 | 8 | import { CloudEvent, CloudEventV1, CONSTANTS, Mode, V1, V03 } from "../.."; 9 | import { Message, Headers, Binding } from ".."; 10 | 11 | import { 12 | headersFor, 13 | sanitize, 14 | v03binaryParsers, 15 | v03structuredParsers, 16 | v1binaryParsers, 17 | v1structuredParsers, 18 | } from "./headers"; 19 | import { isStringOrObjectOrThrow, ValidationError } from "../../event/validation"; 20 | import { JSONParser, MappedParser, Parser, parserByContentType } from "../../parsers"; 21 | 22 | /** 23 | * Serialize a CloudEvent for HTTP transport in binary mode 24 | * @implements {Serializer} 25 | * @see https://github.com/cloudevents/spec/blob/v1.0.1/http-protocol-binding.md#31-binary-content-mode 26 | * 27 | * @param {CloudEvent} event The event to serialize 28 | * @returns {Message} a Message object with headers and body 29 | */ 30 | function binary(event: CloudEventV1): Message { 31 | const contentType: Headers = { [CONSTANTS.HEADER_CONTENT_TYPE]: CONSTANTS.DEFAULT_CONTENT_TYPE }; 32 | const headers: Headers = { ...contentType, ...headersFor(event) }; 33 | let body = event.data; 34 | if (typeof event.data === "object" && !types.isTypedArray(event.data)) { 35 | // we'll stringify objects, but not binary data 36 | body = (JSON.stringify(event.data) as unknown) as T; 37 | } 38 | return { 39 | headers, 40 | body, 41 | }; 42 | } 43 | 44 | /** 45 | * Serialize a CloudEvent for HTTP transport in structured mode 46 | * @implements {Serializer} 47 | * @see https://github.com/cloudevents/spec/blob/v1.0.1/http-protocol-binding.md#32-structured-content-mode 48 | * 49 | * @param {CloudEvent} event the CloudEvent to be serialized 50 | * @returns {Message} a Message object with headers and body 51 | */ 52 | function structured(event: CloudEventV1): Message { 53 | if (event.data_base64) { 54 | // The event's data is binary - delete it 55 | event = (event as CloudEvent).cloneWith({ data: undefined }); 56 | } 57 | return { 58 | headers: { 59 | [CONSTANTS.HEADER_CONTENT_TYPE]: CONSTANTS.DEFAULT_CE_CONTENT_TYPE, 60 | }, 61 | body: event.toString(), 62 | }; 63 | } 64 | 65 | /** 66 | * Determine if a Message is a CloudEvent 67 | * @implements {Detector} 68 | * 69 | * @param {Message} message an incoming Message object 70 | * @returns {boolean} true if this Message is a CloudEvent 71 | */ 72 | function isEvent(message: Message): boolean { 73 | // TODO: this could probably be optimized 74 | try { 75 | deserialize(message); 76 | return true; 77 | } catch (err) { 78 | return false; 79 | } 80 | } 81 | 82 | /** 83 | * Converts a Message to a CloudEvent 84 | * @implements {Deserializer} 85 | * 86 | * @param {Message} message the incoming message 87 | * @return {CloudEvent} A new {CloudEvent} instance 88 | */ 89 | function deserialize(message: Message): CloudEvent | CloudEvent[] { 90 | const cleanHeaders: Headers = sanitize(message.headers); 91 | const mode: Mode = getMode(cleanHeaders); 92 | const version = getVersion(mode, cleanHeaders, message.body); 93 | switch (mode) { 94 | case Mode.BINARY: 95 | return parseBinary(message, version); 96 | case Mode.STRUCTURED: 97 | return parseStructured(message, version); 98 | case Mode.BATCH: 99 | return parseBatched(message); 100 | default: 101 | throw new ValidationError("Unknown Message mode"); 102 | } 103 | } 104 | 105 | /** 106 | * Determines the HTTP transport mode (binary or structured) based 107 | * on the incoming HTTP headers. 108 | * @param {Headers} headers the incoming HTTP headers 109 | * @returns {Mode} the transport mode 110 | */ 111 | function getMode(headers: Headers): Mode { 112 | const contentType = headers[CONSTANTS.HEADER_CONTENT_TYPE]; 113 | if (contentType) { 114 | if (contentType.startsWith(CONSTANTS.MIME_CE_BATCH)) { 115 | return Mode.BATCH; 116 | } else if (contentType.startsWith(CONSTANTS.MIME_CE)) { 117 | return Mode.STRUCTURED; 118 | } 119 | } 120 | 121 | if (headers[CONSTANTS.CE_HEADERS.ID]) { 122 | return Mode.BINARY; 123 | } 124 | throw new ValidationError("no cloud event detected"); 125 | } 126 | 127 | /** 128 | * Determines the version of an incoming CloudEvent based on the 129 | * HTTP headers or HTTP body, depending on transport mode. 130 | * @param {Mode} mode the HTTP transport mode 131 | * @param {Headers} headers the incoming HTTP headers 132 | * @param {Record} body the HTTP request body 133 | * @returns {Version} the CloudEvent specification version 134 | */ 135 | function getVersion(mode: Mode, headers: Headers, body: string | Record | unknown) { 136 | if (mode === Mode.BINARY) { 137 | // Check the headers for the version 138 | const versionHeader = headers[CONSTANTS.CE_HEADERS.SPEC_VERSION]; 139 | if (versionHeader) { 140 | return versionHeader; 141 | } 142 | } else { 143 | // structured mode - the version is in the body 144 | if (typeof body === "string") { 145 | return JSON.parse(body).specversion; 146 | } else { 147 | return (body as Record).specversion; 148 | } 149 | } 150 | return V1; 151 | } 152 | 153 | /** 154 | * Parses an incoming HTTP Message, converting it to a {CloudEvent} 155 | * instance if it conforms to the Cloud Event specification for this receiver. 156 | * 157 | * @param {Message} message the incoming HTTP Message 158 | * @param {string} version the spec version of the incoming event 159 | * @returns {CloudEvent} an instance of CloudEvent representing the incoming request 160 | * @throws {ValidationError} of the event does not conform to the spec 161 | */ 162 | function parseBinary(message: Message, version: string): CloudEvent { 163 | const headers = { ...message.headers }; 164 | let body = message.body; 165 | 166 | if (!headers) throw new ValidationError("headers is null or undefined"); 167 | 168 | // Clone and low case all headers names 169 | const sanitizedHeaders = sanitize(headers); 170 | 171 | const eventObj: { [key: string]: unknown | string | Record } = {}; 172 | const parserMap: Record = version === V03 ? v03binaryParsers : v1binaryParsers; 173 | 174 | for (const header in parserMap) { 175 | if (sanitizedHeaders[header]) { 176 | const mappedParser: MappedParser = parserMap[header]; 177 | eventObj[mappedParser.name] = mappedParser.parser.parse(sanitizedHeaders[header]); 178 | delete sanitizedHeaders[header]; 179 | delete headers[header]; 180 | } 181 | } 182 | 183 | // Every unprocessed header can be an extension 184 | for (const header in headers) { 185 | if (header.startsWith(CONSTANTS.EXTENSIONS_PREFIX)) { 186 | eventObj[header.substring(CONSTANTS.EXTENSIONS_PREFIX.length)] = headers[header]; 187 | } 188 | } 189 | 190 | const parser = parserByContentType[eventObj.datacontenttype as string]; 191 | if (parser && body) { 192 | body = parser.parse(body as string); 193 | } 194 | 195 | // At this point, if the datacontenttype is application/json and the datacontentencoding is base64 196 | // then the data has already been decoded as a string, then parsed as JSON. We don't need to have 197 | // the datacontentencoding property set - in fact, it's incorrect to do so. 198 | if (eventObj.datacontenttype === CONSTANTS.MIME_JSON && eventObj.datacontentencoding === CONSTANTS.ENCODING_BASE64) { 199 | delete eventObj.datacontentencoding; 200 | } 201 | 202 | return new CloudEvent({ ...eventObj, data: body } as CloudEventV1, false); 203 | } 204 | 205 | /** 206 | * Creates a new CloudEvent instance based on the provided payload and headers. 207 | * 208 | * @param {Message} message the incoming Message 209 | * @param {string} version the spec version of this message (v1 or v03) 210 | * @returns {CloudEvent} a new CloudEvent instance for the provided headers and payload 211 | * @throws {ValidationError} if the payload and header combination do not conform to the spec 212 | */ 213 | function parseStructured(message: Message, version: string): CloudEvent { 214 | const payload = message.body; 215 | const headers = message.headers; 216 | 217 | if (!payload) throw new ValidationError("payload is null or undefined"); 218 | if (!headers) throw new ValidationError("headers is null or undefined"); 219 | isStringOrObjectOrThrow(payload, new ValidationError("payload must be an object or a string")); 220 | 221 | // Clone and low case all headers names 222 | const sanitizedHeaders = sanitize(headers); 223 | 224 | const contentType = sanitizedHeaders[CONSTANTS.HEADER_CONTENT_TYPE]; 225 | const parser: Parser = contentType ? parserByContentType[contentType] : new JSONParser(); 226 | if (!parser) throw new ValidationError(`invalid content type ${sanitizedHeaders[CONSTANTS.HEADER_CONTENT_TYPE]}`); 227 | const incoming = { ...(parser.parse(payload as string) as Record) }; 228 | 229 | const eventObj: { [key: string]: unknown } = {}; 230 | const parserMap: Record = version === V03 ? v03structuredParsers : v1structuredParsers; 231 | 232 | for (const key in parserMap) { 233 | const property = incoming[key]; 234 | if (property) { 235 | const mappedParser: MappedParser = parserMap[key]; 236 | eventObj[mappedParser.name] = mappedParser.parser.parse(property as string); 237 | } 238 | delete incoming[key]; 239 | } 240 | 241 | // extensions are what we have left after processing all other properties 242 | for (const key in incoming) { 243 | eventObj[key] = incoming[key]; 244 | } 245 | 246 | // data_base64 is a property that only exists on V1 events. For V03 events, 247 | // there will be a .datacontentencoding property, and the .data property 248 | // itself will be encoded as base64 249 | if (eventObj.data_base64 || eventObj.datacontentencoding === CONSTANTS.ENCODING_BASE64) { 250 | const data = eventObj.data_base64 || eventObj.data; 251 | eventObj.data = new Uint32Array(Buffer.from(data as string, "base64")); 252 | delete eventObj.data_base64; 253 | delete eventObj.datacontentencoding; 254 | } 255 | return new CloudEvent(eventObj as CloudEventV1, false); 256 | } 257 | 258 | function parseBatched(message: Message): CloudEvent | CloudEvent[] { 259 | const ret: CloudEvent[] = []; 260 | const events = JSON.parse(message.body as string); 261 | events.forEach((element: CloudEvent) => { 262 | ret.push(new CloudEvent(element)); 263 | }); 264 | return ret; 265 | } 266 | 267 | /** 268 | * Bindings for HTTP transport support 269 | * @implements {@linkcode Binding} 270 | */ 271 | export const HTTP: Binding = { 272 | binary, 273 | structured, 274 | toEvent: deserialize, 275 | isEvent: isEvent, 276 | }; 277 | -------------------------------------------------------------------------------- /src/message/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 The CloudEvents Authors 3 | SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | import { IncomingHttpHeaders } from "http"; 7 | import { CloudEventV1 } from ".."; 8 | 9 | // reexport the protocol bindings 10 | export * from "./http"; 11 | export * from "./kafka"; 12 | export * from "./mqtt"; 13 | 14 | /** 15 | * Binding is an interface for transport protocols to implement, 16 | * which provides functions for sending CloudEvent Messages over 17 | * the wire. 18 | * @interface 19 | * 20 | * @property {@link Serializer} `binary` - converts a CloudEvent into a Message in binary mode 21 | * @property {@link Serializer} `structured` - converts a CloudEvent into a Message in structured mode 22 | * @property {@link Deserializer} `toEvent` - converts a Message into a CloudEvent 23 | * @property {@link Detector} `isEvent` - determines if a Message can be converted to a CloudEvent 24 | */ 25 | export interface Binding { 26 | binary: Serializer; 27 | structured: Serializer; 28 | toEvent: Deserializer; 29 | isEvent: Detector; 30 | } 31 | 32 | /** 33 | * Headers is an interface representing transport-agnostic headers as 34 | * key/value string pairs 35 | * @interface 36 | */ 37 | export interface Headers extends IncomingHttpHeaders { 38 | [key: string]: string | string[] | undefined; 39 | } 40 | 41 | /** 42 | * Message is an interface representing a CloudEvent as a 43 | * transport-agnostic message 44 | * @interface 45 | * @property {@linkcode Headers} `headers` - the headers for the event Message 46 | * @property {T | string | Buffer | unknown} `body` - the body of the event Message 47 | */ 48 | export interface Message { 49 | headers: Headers; 50 | body: T | string | Buffer | unknown; 51 | } 52 | 53 | /** 54 | * An enum representing the two transport modes, binary and structured 55 | * @interface 56 | */ 57 | export enum Mode { 58 | BINARY = "binary", 59 | STRUCTURED = "structured", 60 | BATCH = "batch", 61 | } 62 | 63 | /** 64 | * Serializer is an interface for functions that can convert a 65 | * CloudEvent into a Message. 66 | * @interface 67 | */ 68 | export interface Serializer { 69 | (event: CloudEventV1): M; 70 | } 71 | 72 | /** 73 | * Deserializer is a function interface that converts a 74 | * Message to a CloudEvent 75 | * @interface 76 | */ 77 | export interface Deserializer { 78 | (message: Message): CloudEventV1 | CloudEventV1[]; 79 | } 80 | 81 | /** 82 | * Detector is a function interface that detects whether 83 | * a message contains a valid CloudEvent 84 | * @interface 85 | */ 86 | export interface Detector { 87 | (message: Message): boolean; 88 | } 89 | -------------------------------------------------------------------------------- /src/message/kafka/headers.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 The CloudEvents Authors 3 | SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | import { CloudEventV1, CONSTANTS, Headers } from "../.."; 7 | 8 | type KafkaHeaders = Readonly<{ 9 | ID: string; 10 | TYPE: string; 11 | SOURCE: string; 12 | SPEC_VERSION: string; 13 | TIME: string; 14 | SUBJECT: string; 15 | DATACONTENTTYPE: string; 16 | DATASCHEMA: string; 17 | [key: string]: string; 18 | }> 19 | 20 | /** 21 | * The set of CloudEvent headers that may exist on a Kafka message 22 | */ 23 | export const KAFKA_CE_HEADERS: KafkaHeaders = Object.freeze({ 24 | /** corresponds to the CloudEvent#id */ 25 | ID: "ce_id", 26 | /** corresponds to the CloudEvent#type */ 27 | TYPE: "ce_type", 28 | /** corresponds to the CloudEvent#source */ 29 | SOURCE: "ce_source", 30 | /** corresponds to the CloudEvent#specversion */ 31 | SPEC_VERSION: "ce_specversion", 32 | /** corresponds to the CloudEvent#time */ 33 | TIME: "ce_time", 34 | /** corresponds to the CloudEvent#subject */ 35 | SUBJECT: "ce_subject", 36 | /** corresponds to the CloudEvent#datacontenttype */ 37 | DATACONTENTTYPE: "ce_datacontenttype", 38 | /** corresponds to the CloudEvent#dataschema */ 39 | DATASCHEMA: "ce_dataschema", 40 | } as const); 41 | 42 | export const HEADER_MAP: { [key: string]: string } = { 43 | [KAFKA_CE_HEADERS.ID]: "id", 44 | [KAFKA_CE_HEADERS.TYPE]: "type", 45 | [KAFKA_CE_HEADERS.SOURCE]: "source", 46 | [KAFKA_CE_HEADERS.SPEC_VERSION]: "specversion", 47 | [KAFKA_CE_HEADERS.TIME]: "time", 48 | [KAFKA_CE_HEADERS.SUBJECT]: "subject", 49 | [KAFKA_CE_HEADERS.DATACONTENTTYPE]: "datacontenttype", 50 | [KAFKA_CE_HEADERS.DATASCHEMA]: "dataschema" 51 | }; 52 | 53 | /** 54 | * A conveninece function to convert a CloudEvent into headers 55 | * @param {CloudEvent} event a CloudEvent object 56 | * @returns {Headers} the CloudEvent attribute as Kafka headers 57 | */ 58 | export function headersFor(event: CloudEventV1): Headers { 59 | const headers: Headers = {}; 60 | 61 | Object.getOwnPropertyNames(event).forEach((property) => { 62 | // Ignore the 'data' property 63 | // it becomes the Kafka message's 'value' field 64 | if (property != CONSTANTS.CE_ATTRIBUTES.DATA && property != CONSTANTS.STRUCTURED_ATTRS_1.DATA_BASE64) { 65 | // all CloudEvent property names get prefixed with 'ce_' 66 | // https://github.com/cloudevents/spec/blob/v1.0.1/kafka-protocol-binding.md#3231-property-names 67 | headers[`ce_${property}`] = event[property] as string; 68 | } 69 | }); 70 | 71 | return headers; 72 | } 73 | -------------------------------------------------------------------------------- /src/message/kafka/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 The CloudEvents Authors 3 | SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | import { CloudEvent, CloudEventV1, CONSTANTS, Mode, ValidationError } from "../.."; 7 | import { Message, Headers, Binding } from ".."; 8 | import { headersFor, HEADER_MAP, KAFKA_CE_HEADERS } from "./headers"; 9 | import { sanitize } from "../http/headers"; 10 | 11 | // Export the binding implementation and message interface 12 | export { 13 | Kafka 14 | }; 15 | 16 | export type { 17 | KafkaMessage, 18 | KafkaEvent 19 | }; 20 | 21 | /** 22 | * Bindings for Kafka transport 23 | * @implements {@linkcode Binding} 24 | */ 25 | const Kafka: Binding, KafkaMessage> = { 26 | binary: toBinaryKafkaMessage, 27 | structured: toStructuredKafkaMessage, 28 | toEvent: deserializeKafkaMessage, 29 | isEvent: isKafkaEvent, 30 | }; 31 | 32 | type Key = string | Buffer; 33 | 34 | /** 35 | * Extends the base Message type to include 36 | * Kafka-specific fields 37 | */ 38 | interface KafkaMessage extends Message { 39 | key: Key 40 | value: T 41 | timestamp?: string 42 | } 43 | 44 | /** 45 | * Extends the base CloudEventV1 interface to include a `partitionkey` field 46 | * which is explicitly mapped to KafkaMessage#key 47 | */ 48 | interface KafkaEvent extends CloudEventV1 { 49 | /** 50 | * Maps to KafkaMessage#key per 51 | * https://github.com/cloudevents/spec/blob/v1.0.1/kafka-protocol-binding.md#31-key-mapping 52 | */ 53 | partitionkey: Key 54 | } 55 | 56 | /** 57 | * Serialize a CloudEvent for Kafka in binary mode 58 | * @implements {Serializer} 59 | * @see https://github.com/cloudevents/spec/blob/v1.0.1/kafka-protocol-binding.md#32-binary-content-mode 60 | * 61 | * @param {KafkaEvent} event The event to serialize 62 | * @returns {KafkaMessage} a KafkaMessage instance 63 | */ 64 | function toBinaryKafkaMessage(event: CloudEventV1): KafkaMessage { 65 | // 3.2.1. Content Type 66 | // For the binary mode, the header content-type property MUST be mapped directly 67 | // to the CloudEvents datacontenttype attribute. 68 | const headers: Headers = { 69 | ...{ [CONSTANTS.HEADER_CONTENT_TYPE]: event.datacontenttype }, 70 | ...headersFor(event) 71 | }; 72 | return { 73 | headers, 74 | key: event.partitionkey as Key, 75 | value: event.data, 76 | body: event.data, 77 | timestamp: timestamp(event.time) 78 | }; 79 | } 80 | 81 | /** 82 | * Serialize a CloudEvent for Kafka in structured mode 83 | * @implements {Serializer} 84 | * @see https://github.com/cloudevents/spec/blob/v1.0.1/kafka-protocol-binding.md#33-structured-content-mode 85 | * 86 | * @param {CloudEvent} event the CloudEvent to be serialized 87 | * @returns {KafkaMessage} a KafkaMessage instance 88 | */ 89 | function toStructuredKafkaMessage(event: CloudEventV1): KafkaMessage { 90 | if ((event instanceof CloudEvent) && event.data_base64) { 91 | // The event's data is binary - delete it 92 | event = event.cloneWith({ data: undefined }); 93 | } 94 | const value = event.toString(); 95 | return { 96 | // All events may not have a partitionkey set, but if they do, 97 | // use it for the KafkaMessage#key per 98 | // https://github.com/cloudevents/spec/blob/v1.0.1/kafka-protocol-binding.md#31-key-mapping 99 | key: event.partitionkey as Key, 100 | value, 101 | headers: { 102 | [CONSTANTS.HEADER_CONTENT_TYPE]: CONSTANTS.DEFAULT_CE_CONTENT_TYPE, 103 | }, 104 | body: value, 105 | timestamp: timestamp(event.time) 106 | }; 107 | } 108 | 109 | /** 110 | * Converts a Message to a CloudEvent 111 | * @implements {Deserializer} 112 | * 113 | * @param {Message} message the incoming message 114 | * @return {KafkaEvent} A new {KafkaEvent} instance 115 | */ 116 | function deserializeKafkaMessage(message: Message): CloudEvent | CloudEvent[] { 117 | if (!isKafkaEvent(message)) { 118 | throw new ValidationError("No CloudEvent detected"); 119 | } 120 | const m = message as KafkaMessage; 121 | if (!m.value) { 122 | throw new ValidationError("Value is null or undefined"); 123 | } 124 | if (!m.headers) { 125 | throw new ValidationError("Headers are null or undefined"); 126 | } 127 | const cleanHeaders: Headers = sanitize(m.headers); 128 | const mode: Mode = getMode(cleanHeaders); 129 | switch (mode) { 130 | case Mode.BINARY: 131 | return parseBinary(m); 132 | case Mode.STRUCTURED: 133 | return parseStructured(m as unknown as KafkaMessage); 134 | case Mode.BATCH: 135 | return parseBatched(m as unknown as KafkaMessage); 136 | default: 137 | throw new ValidationError("Unknown Message mode"); 138 | } 139 | } 140 | 141 | /** 142 | * Determine if a Message is a CloudEvent via Kafka headers 143 | * @implements {Detector} 144 | * 145 | * @param {Message} message an incoming Message object 146 | * @returns {boolean} true if this Message is a CloudEvent 147 | */ 148 | function isKafkaEvent(message: Message): boolean { 149 | const headers = sanitize(message.headers); 150 | return !!headers[KAFKA_CE_HEADERS.ID] || // A binary mode event 151 | headers[CONSTANTS.HEADER_CONTENT_TYPE]?.startsWith(CONSTANTS.MIME_CE) as boolean || // A structured mode event 152 | headers[CONSTANTS.HEADER_CONTENT_TYPE]?.startsWith(CONSTANTS.MIME_CE_BATCH) as boolean; // A batch of events 153 | } 154 | 155 | /** 156 | * Determines what content mode a Kafka message is in given the provided headers 157 | * @param {Headers} headers the headers 158 | * @returns {Mode} the content mode of the KafkaMessage 159 | */ 160 | function getMode(headers: Headers): Mode { 161 | const contentType = headers[CONSTANTS.HEADER_CONTENT_TYPE]; 162 | if (contentType) { 163 | if (contentType.startsWith(CONSTANTS.MIME_CE_BATCH)) { 164 | return Mode.BATCH; 165 | } else if (contentType.startsWith(CONSTANTS.MIME_CE)) { 166 | return Mode.STRUCTURED; 167 | } 168 | } 169 | return Mode.BINARY; 170 | } 171 | 172 | /** 173 | * Parses a binary kafka CE message and returns a CloudEvent 174 | * @param {KafkaMessage} message the message 175 | * @returns {CloudEvent} a CloudEvent 176 | */ 177 | function parseBinary(message: KafkaMessage): CloudEvent { 178 | const eventObj: { [key: string ]: unknown } = {}; 179 | const headers = { ...message.headers }; 180 | 181 | eventObj.datacontenttype = headers[CONSTANTS.HEADER_CONTENT_TYPE]; 182 | 183 | for (const key in KAFKA_CE_HEADERS) { 184 | const h = KAFKA_CE_HEADERS[key]; 185 | if (!!headers[h]) { 186 | eventObj[HEADER_MAP[h]] = headers[h]; 187 | if (h === KAFKA_CE_HEADERS.TIME) { 188 | eventObj.time = new Date(eventObj.time as string).toISOString(); 189 | } 190 | delete headers[h]; 191 | } 192 | } 193 | 194 | // Any remaining headers are extension attributes 195 | // TODO: The spec is unlear on whether these should 196 | // be prefixed with 'ce_' as headers. We assume it is 197 | for (const key in headers) { 198 | if (key.startsWith("ce_")) { 199 | eventObj[key.replace("ce_", "")] = headers[key]; 200 | } 201 | } 202 | 203 | return new CloudEvent({ 204 | ...eventObj, 205 | data: extractBinaryData(message), 206 | partitionkey: message.key, 207 | }, false); 208 | } 209 | 210 | /** 211 | * Parses a structured kafka CE message and returns a CloudEvent 212 | * @param {KafkaMessage} message the message 213 | * @returns {CloudEvent} a KafkaEvent 214 | */ 215 | function parseStructured(message: KafkaMessage): CloudEvent { 216 | // Although the format of a structured encoded event could be something 217 | // other than JSON, e.g. XML, we currently only support JSON 218 | // encoded structured events. 219 | if (!message.headers[CONSTANTS.HEADER_CONTENT_TYPE]?.startsWith(CONSTANTS.MIME_CE_JSON)) { 220 | throw new ValidationError(`Unsupported event encoding ${message.headers[CONSTANTS.HEADER_CONTENT_TYPE]}`); 221 | } 222 | const eventObj = JSON.parse(message.value); 223 | eventObj.time = new Date(eventObj.time).toISOString(); 224 | return new CloudEvent({ 225 | ...eventObj, 226 | partitionkey: message.key, 227 | }, false); 228 | } 229 | 230 | /** 231 | * Parses a batch kafka CE message and returns a CloudEvent[] 232 | * @param {KafkaMessage} message the message 233 | * @returns {CloudEvent[]} an array of KafkaEvent 234 | */ 235 | function parseBatched(message: KafkaMessage): CloudEvent[] { 236 | // Although the format of batch encoded events could be something 237 | // other than JSON, e.g. XML, we currently only support JSON 238 | // encoded structured events. 239 | if (!message.headers[CONSTANTS.HEADER_CONTENT_TYPE]?.startsWith(CONSTANTS.MIME_CE_BATCH)) { 240 | throw new ValidationError(`Unsupported event encoding ${message.headers[CONSTANTS.HEADER_CONTENT_TYPE]}`); 241 | } 242 | const events = JSON.parse(message.value) as Record[]; 243 | return events.map((e) => new CloudEvent({ ...e, partitionkey: message.key }, false)); 244 | } 245 | 246 | /** 247 | * Gets the data from a binary kafka ce message as T 248 | * @param {KafkaMessage} message a KafkaMessage 249 | * @returns {string | undefined} the data in the message 250 | */ 251 | function extractBinaryData(message: KafkaMessage): T { 252 | let data = message.value as T; 253 | // If the event data is JSON, go ahead and parse it 254 | const datacontenttype = message.headers[CONSTANTS.HEADER_CONTENT_TYPE] as string; 255 | if (!!datacontenttype && datacontenttype.startsWith(CONSTANTS.MIME_JSON)) { 256 | if (typeof message.value === "string") { 257 | data = JSON.parse(message.value); 258 | } else if (typeof message.value === "object" && Buffer.isBuffer(message.value)) { 259 | data = JSON.parse(message.value.toString()); 260 | } 261 | } 262 | return data; 263 | } 264 | 265 | /** 266 | * Converts a possible date string into a correctly formatted 267 | * (for CloudEvents) ISO date string. 268 | * @param {string | undefined} t a possible date string 269 | * @returns {string | undefined} a properly formatted ISO date string or undefined 270 | */ 271 | function timestamp(t: string|undefined): string | undefined { 272 | return !!t ? `${Date.parse(t)}` : undefined; 273 | } 274 | -------------------------------------------------------------------------------- /src/message/mqtt/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 The CloudEvents Authors 3 | SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | import { Binding, Deserializer, CloudEvent, CloudEventV1, CONSTANTS, Message, ValidationError, Headers } from "../.."; 7 | import { base64AsBinary } from "../../event/validation"; 8 | 9 | export { 10 | MQTT, MQTTMessageFactory 11 | }; 12 | export type { MQTTMessage }; 13 | 14 | /** 15 | * Extends the base {@linkcode Message} interface to include MQTT attributes, some of which 16 | * are aliases of the {Message} attributes. 17 | */ 18 | interface MQTTMessage extends Message { 19 | /** 20 | * Identifies this message as a PUBLISH packet. MQTTMessages created with 21 | * the `binary` and `structured` Serializers will contain a "Content Type" 22 | * property in the PUBLISH record. 23 | * @see https://github.com/cloudevents/spec/blob/v1.0.1/mqtt-protocol-binding.md#3-mqtt-publish-message-mapping 24 | */ 25 | PUBLISH: Record | undefined 26 | /** 27 | * Alias of {Message#body} 28 | */ 29 | payload: T | undefined, 30 | /** 31 | * Alias of {Message#headers} 32 | */ 33 | "User Properties": Headers | undefined 34 | } 35 | 36 | /** 37 | * Binding for MQTT transport support 38 | * @implements @linkcode Binding 39 | */ 40 | const MQTT: Binding = { 41 | binary, 42 | structured, 43 | toEvent: toEvent as Deserializer, 44 | isEvent 45 | }; 46 | 47 | /** 48 | * Converts a CloudEvent into an MQTTMessage with the event's data as the message payload 49 | * @param {CloudEventV1} event a CloudEvent 50 | * @returns {MQTTMessage} the event serialized as an MQTTMessage with binary encoding 51 | * @implements {Serializer} 52 | */ 53 | function binary(event: CloudEventV1): MQTTMessage { 54 | const properties = { ...event }; 55 | 56 | let body = properties.data as T; 57 | 58 | if (!body && properties.data_base64) { 59 | body = base64AsBinary(properties.data_base64) as unknown as T; 60 | } 61 | 62 | delete properties.data; 63 | delete properties.data_base64; 64 | 65 | return MQTTMessageFactory(event.datacontenttype as string, properties, body); 66 | } 67 | 68 | /** 69 | * Converts a CloudEvent into an MQTTMessage with the event as the message payload 70 | * @param {CloudEventV1} event a CloudEvent 71 | * @returns {MQTTMessage} the event serialized as an MQTTMessage with structured encoding 72 | * @implements {Serializer} 73 | */ 74 | function structured(event: CloudEventV1): MQTTMessage { 75 | let body; 76 | if (event instanceof CloudEvent) { 77 | body = event.toJSON(); 78 | } else { 79 | body = event; 80 | } 81 | return MQTTMessageFactory(CONSTANTS.DEFAULT_CE_CONTENT_TYPE, {}, body) as MQTTMessage; 82 | } 83 | 84 | /** 85 | * A helper function to create an MQTTMessage object, with "User Properties" as an alias 86 | * for "headers" and "payload" an alias for body, and a "PUBLISH" record with a "Content Type" 87 | * property. 88 | * @param {string} contentType the "Content Type" attribute on PUBLISH 89 | * @param {Record} headers the headers and "User Properties" 90 | * @param {T} body the message body/payload 91 | * @returns {MQTTMessage} a message initialized with the provided attributes 92 | */ 93 | function MQTTMessageFactory(contentType: string, headers: Record, body: T): MQTTMessage { 94 | return { 95 | PUBLISH: { 96 | "Content Type": contentType 97 | }, 98 | body, 99 | get payload() { 100 | return this.body as T; 101 | }, 102 | headers: headers as Headers, 103 | get "User Properties"() { 104 | return this.headers as any; 105 | } 106 | }; 107 | } 108 | 109 | /** 110 | * Converts an MQTTMessage into a CloudEvent 111 | * @param {Message} message the message to deserialize 112 | * @param {boolean} strict determines if a ValidationError will be thrown on bad input - defaults to false 113 | * @returns {CloudEventV1} an event 114 | * @implements {Deserializer} 115 | */ 116 | function toEvent(message: Message, strict = false): CloudEventV1 | CloudEventV1[] { 117 | if (strict && !isEvent(message)) { 118 | throw new ValidationError("No CloudEvent detected"); 119 | } 120 | if (isStructuredMessage(message as MQTTMessage)) { 121 | const evt = (typeof message.body === "string") ? JSON.parse(message.body): message.body; 122 | return new CloudEvent({ 123 | ...evt as CloudEventV1 124 | }, false); 125 | } else { 126 | return new CloudEvent({ 127 | ...message.headers, 128 | data: message.body as T, 129 | }, false); 130 | } 131 | } 132 | 133 | /** 134 | * Determine if the message is a CloudEvent 135 | * @param {Message} message an MQTTMessage 136 | * @returns {boolean} true if the message contains an event 137 | */ 138 | function isEvent(message: Message): boolean { 139 | return isBinaryMessage(message) || isStructuredMessage(message as MQTTMessage); 140 | } 141 | 142 | function isBinaryMessage(message: Message): boolean { 143 | return (!!message.headers.id && !!message.headers.source 144 | && !! message.headers.type && !!message.headers.specversion); 145 | } 146 | 147 | function isStructuredMessage(message: MQTTMessage): boolean { 148 | if (!message) { return false; } 149 | return (message.PUBLISH && message?.PUBLISH["Content Type"]?.startsWith(CONSTANTS.MIME_CE_JSON)) || false; 150 | } 151 | -------------------------------------------------------------------------------- /src/parsers.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 The CloudEvents Authors 3 | SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | import JSONbig from "json-bigint"; 7 | import CONSTANTS from "./constants"; 8 | import { isString, isDefinedOrThrow, isStringOrObjectOrThrow, ValidationError } from "./event/validation"; 9 | 10 | const __JSON = JSON; 11 | export abstract class Parser { 12 | abstract parse(payload: Record | string | string[] | undefined): unknown; 13 | } 14 | 15 | export class JSONParser implements Parser { 16 | decorator?: Base64Parser; 17 | constructor(decorator?: Base64Parser) { 18 | this.decorator = decorator; 19 | } 20 | 21 | /** 22 | * Parses the payload with an optional decorator 23 | * @param {object|string} payload the JSON payload 24 | * @return {object} the parsed JSON payload. 25 | */ 26 | parse(payload: Record | string): string { 27 | if (typeof payload === "string") { 28 | // This is kind of a hack, but the payload data could be JSON in the form of a single 29 | // string, such as "some data". But without the quotes in the string, JSON.parse blows 30 | // up. We can check for this scenario and add quotes. Not sure if this is ideal. 31 | if (!/^[[|{|"]/.test(payload)) { 32 | payload = `"${payload}"`; 33 | } 34 | } 35 | if (this.decorator) { 36 | payload = this.decorator.parse(payload); 37 | } 38 | 39 | isDefinedOrThrow(payload, new ValidationError("null or undefined payload")); 40 | isStringOrObjectOrThrow(payload, new ValidationError("invalid payload type, allowed are: string or object")); 41 | 42 | if (process.env[CONSTANTS.USE_BIG_INT_ENV] === "true") { 43 | JSON = JSONbig(({ useNativeBigInt: true })) as JSON; 44 | } else { 45 | JSON = __JSON; 46 | } 47 | 48 | const parseJSON = (v: Record | string): string => (isString(v) ? JSON.parse(v as string) : v); 49 | return parseJSON(payload); 50 | } 51 | } 52 | 53 | export class PassThroughParser extends Parser { 54 | parse(payload: unknown): unknown { 55 | return payload; 56 | } 57 | } 58 | 59 | const jsonParser = new JSONParser(); 60 | export const parserByContentType: { [key: string]: Parser } = { 61 | [CONSTANTS.MIME_JSON]: jsonParser, 62 | [CONSTANTS.MIME_CE_JSON]: jsonParser, 63 | [CONSTANTS.DEFAULT_CONTENT_TYPE]: jsonParser, 64 | [CONSTANTS.DEFAULT_CE_CONTENT_TYPE]: jsonParser, 65 | [CONSTANTS.MIME_OCTET_STREAM]: new PassThroughParser(), 66 | }; 67 | 68 | export class Base64Parser implements Parser { 69 | decorator?: Parser; 70 | 71 | constructor(decorator?: Parser) { 72 | this.decorator = decorator; 73 | } 74 | 75 | parse(payload: Record | string): string { 76 | let payloadToParse = payload; 77 | if (this.decorator) { 78 | payloadToParse = this.decorator.parse(payload) as string; 79 | } 80 | 81 | return Buffer.from(payloadToParse as string, "base64").toString(); 82 | } 83 | } 84 | 85 | export interface MappedParser { 86 | name: string; 87 | parser: Parser; 88 | } 89 | 90 | export class DateParser extends Parser { 91 | parse(payload: string): string { 92 | let date = new Date(Date.parse(payload)); 93 | if (date.toString() === "Invalid Date") { 94 | date = new Date(); 95 | } 96 | return date.toISOString(); 97 | } 98 | } 99 | 100 | export const parserByEncoding: { [key: string]: { [key: string]: Parser } } = { 101 | base64: { 102 | [CONSTANTS.MIME_CE_JSON]: new JSONParser(new Base64Parser()), 103 | [CONSTANTS.MIME_OCTET_STREAM]: new PassThroughParser(), 104 | }, 105 | }; 106 | -------------------------------------------------------------------------------- /src/schema/cloudevent.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "description": "CloudEvents Specification JSON Schema", 4 | "type": "object", 5 | "properties": { 6 | "id": { 7 | "description": "Identifies the event.", 8 | "$ref": "#/definitions/iddef", 9 | "examples": [ 10 | "A234-1234-1234" 11 | ] 12 | }, 13 | "source": { 14 | "description": "Identifies the context in which an event happened.", 15 | "$ref": "#/definitions/sourcedef", 16 | "examples" : [ 17 | "https://github.com/cloudevents", 18 | "mailto:cncf-wg-serverless@lists.cncf.io", 19 | "urn:uuid:6e8bc430-9c3a-11d9-9669-0800200c9a66", 20 | "cloudevents/spec/pull/123", 21 | "/sensors/tn-1234567/alerts", 22 | "1-555-123-4567" 23 | ] 24 | }, 25 | "specversion": { 26 | "description": "The version of the CloudEvents specification which the event uses.", 27 | "$ref": "#/definitions/specversiondef", 28 | "examples": [ 29 | "1.0" 30 | ] 31 | }, 32 | "type": { 33 | "description": "Describes the type of event related to the originating occurrence.", 34 | "$ref": "#/definitions/typedef", 35 | "examples" : [ 36 | "com.github.pull_request.opened", 37 | "com.example.object.deleted.v2" 38 | ] 39 | }, 40 | "datacontenttype": { 41 | "description": "Content type of the data value. Must adhere to RFC 2046 format.", 42 | "$ref": "#/definitions/datacontenttypedef", 43 | "examples": [ 44 | "text/xml", 45 | "application/json", 46 | "image/png", 47 | "multipart/form-data" 48 | ] 49 | }, 50 | "dataschema": { 51 | "description": "Identifies the schema that data adheres to.", 52 | "$ref": "#/definitions/dataschemadef" 53 | }, 54 | "subject": { 55 | "description": "Describes the subject of the event in the context of the event producer (identified by source).", 56 | "$ref": "#/definitions/subjectdef", 57 | "examples": [ 58 | "mynewfile.jpg" 59 | ] 60 | }, 61 | "time": { 62 | "description": "Timestamp of when the occurrence happened. Must adhere to RFC 3339.", 63 | "$ref": "#/definitions/timedef", 64 | "examples": [ 65 | "2018-04-05T17:31:00Z" 66 | ] 67 | }, 68 | "data": { 69 | "description": "The event payload.", 70 | "$ref": "#/definitions/datadef", 71 | "examples": [ 72 | "" 73 | ] 74 | }, 75 | "data_base64": { 76 | "description": "Base64 encoded event payload. Must adhere to RFC4648.", 77 | "$ref": "#/definitions/data_base64def", 78 | "examples": [ 79 | "Zm9vYg==" 80 | ] 81 | } 82 | }, 83 | "required": ["id", "source", "specversion", "type"], 84 | "definitions": { 85 | "iddef": { 86 | "type": "string", 87 | "minLength": 1 88 | }, 89 | "sourcedef": { 90 | "type": "string", 91 | "format": "uri-reference", 92 | "minLength": 1 93 | }, 94 | "specversiondef": { 95 | "type": "string", 96 | "minLength": 1 97 | }, 98 | "typedef": { 99 | "type": "string", 100 | "minLength": 1 101 | }, 102 | "datacontenttypedef": { 103 | "type": ["string", "null"], 104 | "minLength": 1 105 | }, 106 | "dataschemadef": { 107 | "type": ["string", "null"], 108 | "format": "uri", 109 | "minLength": 1 110 | }, 111 | "subjectdef": { 112 | "type": ["string", "null"], 113 | "minLength": 1 114 | }, 115 | "timedef": { 116 | "type": ["string", "null"], 117 | "format": "date-time", 118 | "minLength": 1 119 | }, 120 | "datadef": { 121 | "type": ["object", "string", "number", "array", "boolean", "null"] 122 | }, 123 | "data_base64def": { 124 | "type": ["string", "null"], 125 | "contentEncoding": "base64" 126 | } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/schema/formats.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 The CloudEvents Authors 3 | SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | function formats(ajv) { 7 | require("ajv-formats")(ajv); 8 | } 9 | 10 | module.exports = formats; 11 | -------------------------------------------------------------------------------- /src/transport/emitter.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 The CloudEvents Authors 3 | SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | import { CloudEvent } from "../event/cloudevent"; 7 | import { HTTP, Message, Mode } from "../message"; 8 | import { EventEmitter } from "events"; 9 | 10 | /** 11 | * Options is an additional, optional dictionary of options that may 12 | * be passed to an EmitterFunction and TransportFunction 13 | * @interface 14 | */ 15 | export interface Options { 16 | [key: string]: string | Record | unknown; 17 | } 18 | 19 | /** 20 | * EmitterFunction is an invokable interface returned by {@linkcode emitterFor}. 21 | * Invoke an EmitterFunction with a CloudEvent and optional transport 22 | * options to send the event as a Message across supported transports. 23 | * @interface 24 | */ 25 | export interface EmitterFunction { 26 | (event: CloudEvent, options?: Options): Promise; 27 | } 28 | 29 | /** 30 | * TransportFunction is an invokable interface provided to the emitterFactory. 31 | * A TransportFunction's responsiblity is to send a JSON encoded event Message 32 | * across the wire. 33 | * @interface 34 | */ 35 | export interface TransportFunction { 36 | (message: Message, options?: Options): Promise; 37 | } 38 | 39 | const emitterDefaults: Options = { binding: HTTP, mode: Mode.BINARY }; 40 | /** 41 | * Creates and returns an {@linkcode EmitterFunction} using the supplied 42 | * {@linkcode TransportFunction}. The returned {@linkcode EmitterFunction} 43 | * will invoke the {@linkcode Binding}'s `binary` or `structured` function 44 | * to convert a {@linkcode CloudEvent} into a JSON 45 | * {@linkcode Message} based on the {@linkcode Mode} provided, and invoke the 46 | * TransportFunction with the Message and any supplied options. 47 | * 48 | * @param {TransportFunction} fn a TransportFunction that can accept an event Message 49 | * @param { {Binding, Mode} } options network binding and message serialization options 50 | * @param {Binding} options.binding a transport binding, e.g. HTTP 51 | * @param {Mode} options.mode the encoding mode (Mode.BINARY or Mode.STRUCTURED) 52 | * @returns {EmitterFunction} an EmitterFunction to send events with 53 | */ 54 | export function emitterFor(fn: TransportFunction, options = emitterDefaults): EmitterFunction { 55 | if (!fn) { 56 | throw new TypeError("A TransportFunction is required"); 57 | } 58 | const { binding, mode }: any = { ...emitterDefaults, ...options }; 59 | return function emit(event: CloudEvent, opts?: Options): Promise { 60 | opts = opts || {}; 61 | 62 | switch (mode) { 63 | case Mode.BINARY: 64 | return fn(binding.binary(event), opts); 65 | case Mode.STRUCTURED: 66 | return fn(binding.structured(event), opts); 67 | default: 68 | throw new TypeError(`Unexpected transport mode: ${mode}`); 69 | } 70 | }; 71 | } 72 | 73 | /** 74 | * A helper class to emit CloudEvents within an application 75 | */ 76 | export class Emitter { 77 | /** 78 | * Singleton store 79 | */ 80 | static instance: EventEmitter | undefined = undefined; 81 | 82 | /** 83 | * Return or create the Emitter singleton 84 | * 85 | * @return {Emitter} return Emitter singleton 86 | */ 87 | static getInstance(): EventEmitter { 88 | if (!Emitter.instance) { 89 | Emitter.instance = new EventEmitter(); 90 | } 91 | return Emitter.instance; 92 | } 93 | 94 | /** 95 | * Add a listener for eventing 96 | * 97 | * @param {string} event type to listen to 98 | * @param {Function} listener to call on event 99 | * @return {void} 100 | */ 101 | static on(event: "cloudevent" | "newListener" | "removeListener", listener: (...args: any[]) => void): void { 102 | Emitter.getInstance().on(event, listener); 103 | } 104 | 105 | /** 106 | * Emit an event inside this application 107 | * 108 | * @param {CloudEvent} event to emit 109 | * @param {boolean} ensureDelivery fail the promise if one listener fails 110 | * @return {void} 111 | */ 112 | static async emitEvent(event: CloudEvent, ensureDelivery = true): Promise { 113 | if (!ensureDelivery) { 114 | // Ensure delivery is disabled so we don't wait for Promise 115 | Emitter.getInstance().emit("cloudevent", event); 116 | } else { 117 | // Execute all listeners and wrap them in a Promise 118 | await Promise.all( 119 | Emitter.getInstance() 120 | .listeners("cloudevent") 121 | .map(async (l) => l(event)), 122 | ); 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/transport/http/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 The CloudEvents Authors 3 | SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | import { Socket } from "net"; 7 | import http, { OutgoingHttpHeaders } from "http"; 8 | import https, { RequestOptions } from "https"; 9 | 10 | import { Message, Options } from "../.."; 11 | import { TransportFunction } from "../emitter"; 12 | 13 | /** 14 | * httpTransport provides a simple HTTP Transport function, which can send a CloudEvent, 15 | * encoded as a Message to the endpoint. The returned function can be used with emitterFor() 16 | * to provide an event emitter, for example: 17 | * 18 | * const emitter = emitterFor(httpTransport("http://example.com")); 19 | * emitter.emit(myCloudEvent) 20 | * .then(resp => console.log(resp)); 21 | * 22 | * @param {string|URL} sink the destination endpoint for the event 23 | * @returns {TransportFunction} a function which can be used to send CloudEvents to _sink_ 24 | */ 25 | export function httpTransport(sink: string | URL): TransportFunction { 26 | const url = new URL(sink); 27 | let base: any; 28 | if (url.protocol === "https:") { 29 | base = https; 30 | } else if (url.protocol === "http:") { 31 | base = http; 32 | } else { 33 | throw new TypeError(`unsupported protocol ${url.protocol}`); 34 | } 35 | return function(message: Message, options?: Options): Promise { 36 | return new Promise((resolve, reject) => { 37 | options = { ...options }; 38 | 39 | // TODO: Callers should be able to set any Node.js RequestOptions 40 | const opts: RequestOptions = { 41 | method: "POST", 42 | headers: {...message.headers, ...options.headers as OutgoingHttpHeaders}, 43 | }; 44 | try { 45 | const response = { 46 | body: "", 47 | headers: {}, 48 | }; 49 | const req = base.request(url, opts, (res: Socket) => { 50 | res.setEncoding("utf-8"); 51 | response.headers = (res as any).headers; 52 | res.on("data", (chunk) => response.body += chunk); 53 | res.on("end", () => { resolve(response); }); 54 | }); 55 | req.on("error", reject); 56 | req.write(message.body); 57 | req.end(); 58 | } catch (err) { 59 | reject(err); 60 | } 61 | }); 62 | }; 63 | } 64 | -------------------------------------------------------------------------------- /src/transport/protocols.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 The CloudEvents Authors 3 | SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | /** 7 | * An enum representing the transport protocols for an event 8 | */ 9 | 10 | export const enum Protocol { 11 | HTTPBinary, 12 | HTTPStructured, 13 | HTTP, 14 | } 15 | -------------------------------------------------------------------------------- /test/conformance/steps.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 The CloudEvents Authors 3 | SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | /* eslint-disable @typescript-eslint/ban-ts-comment */ 7 | 8 | import { assert } from "chai"; 9 | import { Given, When, Then, World } from "@cucumber/cucumber"; 10 | import { Message, Headers, HTTP, KafkaMessage, Kafka } from "../../src"; 11 | 12 | // eslint-disable-next-line @typescript-eslint/no-var-requires 13 | const { HTTPParser } = require("http-parser-js"); 14 | 15 | const parser = new HTTPParser(HTTPParser.REQUEST); 16 | 17 | Given("Kafka Protocol Binding is supported", function (this: World) { 18 | return true; 19 | }); 20 | 21 | Given("a Kafka message with payload:", function (request: string) { 22 | // Create a KafkaMessage from the incoming HTTP request 23 | const value = Buffer.from(request); 24 | const message: KafkaMessage = { 25 | key: "", 26 | headers: {}, 27 | body: value, 28 | value, 29 | }; 30 | this.message = message; 31 | return true; 32 | }); 33 | 34 | Then("Kafka headers:", function (attributes: { rawTable: [] }) { 35 | this.message.headers = tableToObject(attributes.rawTable); 36 | }); 37 | 38 | When("parsed as Kafka message", function () { 39 | this.cloudevent = Kafka.toEvent(this.message); 40 | return true; 41 | }); 42 | 43 | Given("HTTP Protocol Binding is supported", function (this: World) { 44 | return true; 45 | }); 46 | 47 | Given("an HTTP request", function (request: string) { 48 | // Create a Message from the incoming HTTP request 49 | const message: Message = { 50 | headers: {}, 51 | body: "", 52 | }; 53 | parser.onHeadersComplete = function (record: Record) { 54 | message.headers = extractHeaders(record.headers); 55 | }; 56 | parser.onBody = function (body: Buffer, offset: number) { 57 | message.body = body.slice(offset).toString(); 58 | }; 59 | this.message = message; 60 | parser.execute(Buffer.from(request), 0, request.length); 61 | return true; 62 | }); 63 | 64 | When("parsed as HTTP request", function () { 65 | this.cloudevent = HTTP.toEvent(this.message); 66 | return true; 67 | }); 68 | 69 | Then("the attributes are:", function (attributes: { rawTable: [] }) { 70 | const expected = tableToObject(attributes.rawTable); 71 | assert.equal(this.cloudevent.id, expected.id); 72 | assert.equal(this.cloudevent.type, expected.type); 73 | assert.equal(this.cloudevent.source, expected.source); 74 | assert.equal(this.cloudevent.time, new Date(expected.time).toISOString()); 75 | assert.equal(this.cloudevent.specversion, expected.specversion); 76 | assert.equal(this.cloudevent.datacontenttype, expected.datacontenttype); 77 | return true; 78 | }); 79 | 80 | Then("the data is equal to the following JSON:", function (json: string) { 81 | assert.deepEqual(this.cloudevent.data, JSON.parse(json)); 82 | return true; 83 | }); 84 | 85 | function extractHeaders(arr: []): Headers { 86 | const obj: Headers = {}; 87 | // @ts-ignore 88 | return arr.reduce(({}, keyOrValue, index, arr) => { 89 | if (index % 2 === 0) { 90 | obj[keyOrValue] = arr[index + 1]; 91 | } 92 | return obj; 93 | }); 94 | } 95 | 96 | function tableToObject(table: []): Record { 97 | const obj: Record = {}; 98 | // @ts-ignore 99 | return table.reduce(({}, [key, value]: Array) => { 100 | obj[key] = value; 101 | return obj; 102 | }); 103 | } 104 | -------------------------------------------------------------------------------- /test/integration/batch_test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 The CloudEvents Authors 3 | SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | import { expect } from "chai"; 7 | import { CloudEvent, HTTP, Message } from "../../src"; 8 | import { Kafka, KafkaMessage } from "../../src/message"; 9 | 10 | const type = "org.cncf.cloudevents.example"; 11 | const source = "http://unit.test"; 12 | 13 | // Create a bunch of cloudevents that we can bunch together in a batch 14 | const fixture: any[] = []; 15 | for (let id = 0; id < 10; id++) { 16 | fixture.push( 17 | new CloudEvent({ 18 | id: `${id}`, 19 | source, 20 | type, 21 | }), 22 | ); 23 | } 24 | 25 | /** 26 | * A basic test to validate that we can handle simple batch mode 27 | */ 28 | describe("A batched CloudEvent message over HTTP", () => { 29 | it("Can be created with a typed Message", () => { 30 | const message: Message = { 31 | headers: { 32 | "content-type": "application/cloudevents-batch+json", 33 | }, 34 | body: JSON.stringify(fixture), 35 | }; 36 | const batch = HTTP.toEvent(message); 37 | expect(batch.length).to.equal(10); 38 | const ce = (batch as CloudEvent[])[0]; 39 | expect(typeof ce).to.equal("object"); 40 | expect(ce.constructor.name).to.equal("CloudEvent"); 41 | }); 42 | }); 43 | 44 | describe("A batched CloudEvent message over Kafka", () => { 45 | it("Can be created with a typed Message", () => { 46 | const value = JSON.stringify(fixture); 47 | const message: KafkaMessage = { 48 | key: "123", 49 | value, 50 | headers: { 51 | "content-type": "application/cloudevents-batch+json", 52 | }, 53 | body: value, 54 | }; 55 | const batch = Kafka.toEvent(message); 56 | expect(batch.length).to.equal(10); 57 | const ce = (batch as CloudEvent[])[0]; 58 | expect(typeof ce).to.equal("object"); 59 | expect(ce.constructor.name).to.equal("CloudEvent"); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /test/integration/ce.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudevents/sdk-javascript/3c8819e2bcf97fe7b352b2a004d511965a4cc414/test/integration/ce.png -------------------------------------------------------------------------------- /test/integration/cloud_event_test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 The CloudEvents Authors 3 | SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | import path from "path"; 7 | import fs from "fs"; 8 | 9 | import { expect } from "chai"; 10 | import { CloudEvent, CloudEventV1, ValidationError, V1 } from "../../src"; 11 | import { asBase64 } from "../../src/event/validation"; 12 | 13 | const type = "org.cncf.cloudevents.example"; 14 | const source = "http://unit.test"; 15 | const id = "b46cf653-d48a-4b90-8dfa-355c01061361"; 16 | 17 | const fixture = Object.freeze({ 18 | id, 19 | specversion: V1, 20 | source, 21 | type, 22 | data: `"some data"` 23 | }); 24 | 25 | const imageData = new Uint32Array(fs.readFileSync(path.join(process.cwd(), "test", "integration", "ce.png"))); 26 | const image_base64 = asBase64(imageData); 27 | 28 | // Do not replace this with the assignment of a class instance 29 | // as we just want to test if we can enumerate all explicitly defined fields! 30 | const cloudEventV1InterfaceFields: (keyof CloudEventV1)[] = Object.keys({ 31 | id: "", 32 | type: "", 33 | data: undefined, 34 | data_base64: "", 35 | source: "", 36 | time: "", 37 | datacontenttype: "", 38 | dataschema: "", 39 | specversion: "", 40 | subject: "" 41 | } as Required>); 42 | 43 | describe("A CloudEvent", () => { 44 | it("Can be constructed with a typed Message", () => { 45 | const ce = new CloudEvent(fixture); 46 | expect(ce.type).to.equal(type); 47 | expect(ce.source).to.equal(source); 48 | }); 49 | 50 | it("Can be constructed with loose validation", () => { 51 | const ce = new CloudEvent({}, false); 52 | expect(ce).to.be.instanceOf(CloudEvent); 53 | }); 54 | 55 | it("Loosely validated events can be cloned", () => { 56 | const ce = new CloudEvent({}, false); 57 | expect(ce.cloneWith({}, false)).to.be.instanceOf(CloudEvent); 58 | }); 59 | 60 | it("Loosely validated events throw when validated", () => { 61 | const ce = new CloudEvent({}, false); 62 | expect(ce.validate).to.throw(ValidationError, "invalid payload"); 63 | }); 64 | 65 | it("serializes as JSON with toString()", () => { 66 | const ce = new CloudEvent({ ...fixture, data: { lunch: "tacos" } }); 67 | expect(ce.toString()).to.deep.equal(JSON.stringify(ce)); 68 | expect(new CloudEvent(JSON.parse(ce.toString()))).to.deep.equal(ce); 69 | expect(new CloudEvent(JSON.parse(JSON.stringify(ce)))).to.deep.equal(ce); 70 | }); 71 | 72 | it("serializes as JSON with raw log", () => { 73 | const ce = new CloudEvent({ ...fixture, data: { lunch: "tacos" } }); 74 | const inspectSymbol = (Symbol.for("nodejs.util.inspect.custom") as unknown) as string; 75 | const ceToString = (ce[inspectSymbol] as CallableFunction).bind(ce); 76 | expect(ce.toString()).to.deep.equal(ceToString()); 77 | }); 78 | 79 | it("Throw a validation error for invalid extension names", () => { 80 | expect(() => { 81 | new CloudEvent({ "ext-1": "extension1", ...fixture }); 82 | }).throw("invalid extension name"); 83 | }); 84 | 85 | it("Not throw a validation error for invalid extension names, more than 20 chars", () => { 86 | expect(() => { 87 | new CloudEvent({ "123456789012345678901": "extension1", ...fixture }); 88 | }).not.throw("invalid extension name"); 89 | }); 90 | 91 | it("Throws a validation error for invalid uppercase extension names", () => { 92 | expect(() => { 93 | new CloudEvent({ ExtensionWithCaps: "extension value", ...fixture }); 94 | }).throw("invalid extension name"); 95 | }); 96 | 97 | it("CloudEventV1 interface fields should be enumerable", () => { 98 | const classInstanceKeys = Object.keys(new CloudEvent({ ...fixture })); 99 | 100 | for (const key of cloudEventV1InterfaceFields) { 101 | expect(classInstanceKeys).to.contain(key); 102 | } 103 | }); 104 | 105 | it("throws TypeError on trying to set any field value", () => { 106 | const ce = new CloudEvent({ 107 | ...fixture, 108 | mycustomfield: "initialValue" 109 | }); 110 | 111 | const keySet = new Set([...cloudEventV1InterfaceFields, ...Object.keys(ce)]); 112 | 113 | expect(keySet).not.to.be.empty; 114 | 115 | for (const cloudEventKey of keySet) { 116 | let threw = false; 117 | 118 | try { 119 | ce[cloudEventKey] = "newValue"; 120 | } catch (err) { 121 | threw = true; 122 | expect(err).to.be.instanceOf(TypeError); 123 | expect((err as TypeError).message).to.include("Cannot assign to read only property"); 124 | } 125 | 126 | if (!threw) { 127 | expect.fail(`Assigning a value to ${cloudEventKey} did not throw`); 128 | } 129 | } 130 | }); 131 | 132 | describe("toJSON()", () => { 133 | it("does not return data field if data_base64 field is set to comply with JSON format spec 3.1.1", () => { 134 | const binaryData = new Uint8Array([1,2,3]); 135 | 136 | const ce = new CloudEvent({ 137 | ...fixture, 138 | data: binaryData 139 | }); 140 | 141 | expect(ce.data).to.be.equal(binaryData); 142 | 143 | const json = ce.toJSON(); 144 | expect(json.data).to.not.exist; 145 | expect(json.data_base64).to.be.equal("AQID"); 146 | }); 147 | }); 148 | }); 149 | 150 | describe("A 1.0 CloudEvent", () => { 151 | it("has retreivable source and type attributes", () => { 152 | const ce = new CloudEvent(fixture); 153 | expect(ce.source).to.equal("http://unit.test"); 154 | expect(ce.type).to.equal("org.cncf.cloudevents.example"); 155 | }); 156 | 157 | it("defaults to specversion 1.0", () => { 158 | const ce = new CloudEvent({ source, type }); 159 | expect(ce.specversion).to.equal("1.0"); 160 | }); 161 | 162 | it("generates an ID if one is not provided in the constructor", () => { 163 | const ce = new CloudEvent({ source, type }); 164 | expect(ce.id).to.not.be.empty; 165 | }); 166 | 167 | it("can be constructed with an ID", () => { 168 | const ce = new CloudEvent({ id: "1234", specversion: V1, source, type }); 169 | expect(ce.id).to.equal("1234"); 170 | }); 171 | 172 | it("generates a timestamp by default", () => { 173 | const ce = new CloudEvent(fixture); 174 | expect(ce.time).to.not.be.empty; 175 | }); 176 | 177 | it("can be constructed with a timestamp", () => { 178 | const time = new Date().toISOString(); 179 | const ce = new CloudEvent({ time, ...fixture }); 180 | expect(ce.time).to.equal(time); 181 | }); 182 | 183 | it("can be constructed with a datacontenttype", () => { 184 | const ce = new CloudEvent({ datacontenttype: "application/json", ...fixture }); 185 | expect(ce.datacontenttype).to.equal("application/json"); 186 | }); 187 | 188 | it("can be constructed with a dataschema", () => { 189 | const ce = new CloudEvent({ dataschema: "http://my.schema", ...fixture }); 190 | expect(ce.dataschema).to.equal("http://my.schema"); 191 | }); 192 | 193 | it("can be constructed with a subject", () => { 194 | const ce = new CloudEvent({ subject: "science", ...fixture }); 195 | expect(ce.subject).to.equal("science"); 196 | }); 197 | 198 | // Handle deprecated attribute - should this really throw? 199 | it("throws a TypeError when constructed with a schemaurl", () => { 200 | expect(() => { 201 | new CloudEvent({ schemaurl: "http://throw.com", ...fixture }); 202 | }).to.throw(TypeError, "cannot set schemaurl on version 1.0 event"); 203 | }); 204 | 205 | it("can be constructed with data", () => { 206 | const ce = new CloudEvent({ 207 | ...fixture, 208 | data: { lunch: "tacos" }, 209 | }); 210 | expect(ce.data).to.deep.equal({ lunch: "tacos" }); 211 | }); 212 | 213 | it("can be constructed with data as an Array", () => { 214 | const ce = new CloudEvent({ 215 | ...fixture, 216 | data: [{ lunch: "tacos" }, { supper: "sushi" }], 217 | }); 218 | expect(ce.data).to.deep.equal([{ lunch: "tacos" }, { supper: "sushi" }]); 219 | }); 220 | 221 | it("can be constructed with data as a number", () => { 222 | const ce = new CloudEvent({ 223 | ...fixture, 224 | data: 100, 225 | }); 226 | expect(ce.data).to.equal(100); 227 | }); 228 | 229 | it("can be constructed with null data", () => { 230 | const ce = new CloudEvent({ 231 | ...fixture, 232 | data: null, 233 | }); 234 | expect(ce.data).to.equal(null); 235 | }); 236 | 237 | it("can be constructed with data as a boolean", () => { 238 | const ce = new CloudEvent({ 239 | ...fixture, 240 | data: true, 241 | }); 242 | expect(ce.data).to.be.true; 243 | }); 244 | 245 | it("can be constructed with binary data", () => { 246 | const ce = new CloudEvent({ 247 | ...fixture, 248 | data: imageData, 249 | }); 250 | expect(ce.data).to.equal(imageData); 251 | expect(ce.data_base64).to.equal(image_base64); 252 | }); 253 | 254 | it("can be constructed with extensions", () => { 255 | const extensions = { 256 | extensionkey: "extension-value", 257 | }; 258 | const ce = new CloudEvent({ 259 | ...extensions, 260 | ...fixture, 261 | }); 262 | expect(ce["extensionkey"]).to.equal(extensions["extensionkey"]); 263 | }); 264 | 265 | it("throws TypeError if the CloudEvent does not conform to the schema", () => { 266 | try { 267 | new CloudEvent({ 268 | ...fixture, 269 | source: (null as unknown) as string, 270 | }); 271 | } catch (err) { 272 | expect(err).to.be.instanceOf(TypeError); 273 | expect((err as TypeError).message).to.include("invalid payload"); 274 | } 275 | }); 276 | 277 | it("correctly formats a CloudEvent as JSON", () => { 278 | const ce = new CloudEvent({ ...fixture }); 279 | const json = ce.toString(); 280 | const obj = JSON.parse(json as string); 281 | expect(obj.type).to.equal(type); 282 | expect(obj.source).to.equal(source); 283 | expect(obj.specversion).to.equal(V1); 284 | }); 285 | 286 | it("throws if the provded source is empty string", () => { 287 | try { 288 | new CloudEvent({ 289 | id: "0815", 290 | specversion: "1.0", 291 | type: "my.event.type", 292 | source: "", 293 | }); 294 | } catch (err: any) { 295 | expect(err).to.be.instanceOf(ValidationError); 296 | const error = err.errors[0] as any; 297 | expect(err.message).to.include("invalid payload"); 298 | expect(error.instancePath).to.equal("/source"); 299 | expect(error.keyword).to.equal("minLength"); 300 | } 301 | }); 302 | }); 303 | -------------------------------------------------------------------------------- /test/integration/constants_test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 The CloudEvents Authors 3 | SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | import { expect } from "chai"; 7 | import { CONSTANTS } from "../../src"; 8 | 9 | describe("Constants exposed by top level exports", () => { 10 | it("Exports an ENCODING_BASE64 constant", () => { 11 | expect(CONSTANTS.ENCODING_BASE64).to.equal("base64"); 12 | }); 13 | it("Exports a DATA_ATTRIBUTE constant", () => { 14 | expect(CONSTANTS.DATA_ATTRIBUTE).to.equal("data"); 15 | }); 16 | it("Exports a MIME_JSON constant", () => { 17 | expect(CONSTANTS.MIME_JSON).to.equal("application/json"); 18 | }); 19 | it("Exports a MIME_OCTET_STREAM constant", () => { 20 | expect(CONSTANTS.MIME_OCTET_STREAM).to.equal("application/octet-stream"); 21 | }); 22 | it("Exports a MIME_CE constant", () => { 23 | expect(CONSTANTS.MIME_CE).to.equal("application/cloudevents"); 24 | }); 25 | it("Exports a MIME_CE_JSON constant", () => { 26 | expect(CONSTANTS.MIME_CE_JSON).to.equal("application/cloudevents+json"); 27 | }); 28 | it("Exports a HEADER_CONTENT_TYPE constant", () => { 29 | expect(CONSTANTS.HEADER_CONTENT_TYPE).to.equal("content-type"); 30 | }); 31 | it("Exports a DEFAULT_CONTENT_TYPE constant", () => { 32 | expect(CONSTANTS.DEFAULT_CONTENT_TYPE).to.equal(`${CONSTANTS.MIME_JSON}; charset=${CONSTANTS.CHARSET_DEFAULT}`); 33 | }); 34 | it("Exports a DEFAULT_CE_CONTENT_TYPE constant", () => { 35 | expect(CONSTANTS.DEFAULT_CE_CONTENT_TYPE).to.equal( 36 | `${CONSTANTS.MIME_CE_JSON}; charset=${CONSTANTS.CHARSET_DEFAULT}`, 37 | ); 38 | }); 39 | describe("V0.3 binary headers constants", () => { 40 | it("Provides a TYPE header", () => { 41 | expect(CONSTANTS.CE_HEADERS.TYPE).to.equal("ce-type"); 42 | }); 43 | it("Provides a SPEC_VERSION header", () => { 44 | expect(CONSTANTS.CE_HEADERS.SPEC_VERSION).to.equal("ce-specversion"); 45 | }); 46 | it("Provides a SOURCE header", () => { 47 | expect(CONSTANTS.CE_HEADERS.SOURCE).to.equal("ce-source"); 48 | }); 49 | it("Provides an ID header", () => { 50 | expect(CONSTANTS.CE_HEADERS.ID).to.equal("ce-id"); 51 | }); 52 | it("Provides a TIME header", () => { 53 | expect(CONSTANTS.CE_HEADERS.TIME).to.equal("ce-time"); 54 | }); 55 | it("Provides a SCHEMA_URL header", () => { 56 | expect(CONSTANTS.BINARY_HEADERS_03.SCHEMA_URL).to.equal("ce-schemaurl"); 57 | }); 58 | it("Provides a CONTENT_ENCODING header", () => { 59 | expect(CONSTANTS.BINARY_HEADERS_03.CONTENT_ENCODING).to.equal("ce-datacontentencoding"); 60 | }); 61 | it("Provides a SUBJECT header", () => { 62 | expect(CONSTANTS.CE_HEADERS.SUBJECT).to.equal("ce-subject"); 63 | }); 64 | it("Provides an EXTENSIONS_PREFIX constant", () => { 65 | expect(CONSTANTS.EXTENSIONS_PREFIX).to.equal("ce-"); 66 | }); 67 | }); 68 | describe("V0.3 structured attributes constants", () => { 69 | it("Provides a TYPE attribute", () => { 70 | expect(CONSTANTS.CE_ATTRIBUTES.TYPE).to.equal("type"); 71 | }); 72 | it("Provides a SPEC_VERSION attribute", () => { 73 | expect(CONSTANTS.CE_ATTRIBUTES.SPEC_VERSION).to.equal("specversion"); 74 | }); 75 | it("Provides a SOURCE attribute", () => { 76 | expect(CONSTANTS.CE_ATTRIBUTES.SOURCE).to.equal("source"); 77 | }); 78 | it("Provides an ID attribute", () => { 79 | expect(CONSTANTS.CE_ATTRIBUTES.ID).to.equal("id"); 80 | }); 81 | it("Provides a TIME attribute", () => { 82 | expect(CONSTANTS.CE_ATTRIBUTES.TIME).to.equal("time"); 83 | }); 84 | it("Provides a SCHEMA_URL attribute", () => { 85 | expect(CONSTANTS.STRUCTURED_ATTRS_03.SCHEMA_URL).to.equal("schemaurl"); 86 | }); 87 | it("Provides a CONTENT_ENCODING attribute", () => { 88 | expect(CONSTANTS.STRUCTURED_ATTRS_03.CONTENT_ENCODING).to.equal("datacontentencoding"); 89 | }); 90 | it("Provides a SUBJECT attribute", () => { 91 | expect(CONSTANTS.CE_ATTRIBUTES.SUBJECT).to.equal("subject"); 92 | }); 93 | it("Provides a DATA attribute", () => { 94 | expect(CONSTANTS.CE_ATTRIBUTES.DATA).to.equal("data"); 95 | }); 96 | }); 97 | describe("V01 binary headers constants", () => { 98 | it("Provides a TYPE header", () => { 99 | expect(CONSTANTS.CE_HEADERS.TYPE).to.equal("ce-type"); 100 | }); 101 | it("Provides a SPEC_VERSION header", () => { 102 | expect(CONSTANTS.CE_HEADERS.SPEC_VERSION).to.equal("ce-specversion"); 103 | }); 104 | it("Provides a SOURCE header", () => { 105 | expect(CONSTANTS.CE_HEADERS.SOURCE).to.equal("ce-source"); 106 | }); 107 | it("Provides an ID header", () => { 108 | expect(CONSTANTS.CE_HEADERS.ID).to.equal("ce-id"); 109 | }); 110 | it("Provides a TIME header", () => { 111 | expect(CONSTANTS.CE_HEADERS.TIME).to.equal("ce-time"); 112 | }); 113 | it("Provides a DATA_SCHEMA header", () => { 114 | expect(CONSTANTS.BINARY_HEADERS_1.DATA_SCHEMA).to.equal("ce-dataschema"); 115 | }); 116 | it("Provides a SUBJECT header", () => { 117 | expect(CONSTANTS.CE_HEADERS.SUBJECT).to.equal("ce-subject"); 118 | }); 119 | it("Provides an EXTENSIONS_PREFIX constant", () => { 120 | expect(CONSTANTS.EXTENSIONS_PREFIX).to.equal("ce-"); 121 | }); 122 | }); 123 | describe("V1 structured attributes constants", () => { 124 | it("Provides a TYPE attribute", () => { 125 | expect(CONSTANTS.CE_ATTRIBUTES.TYPE).to.equal("type"); 126 | }); 127 | it("Provides a SPEC_VERSION attribute", () => { 128 | expect(CONSTANTS.CE_ATTRIBUTES.SPEC_VERSION).to.equal("specversion"); 129 | }); 130 | it("Provides a SOURCE attribute", () => { 131 | expect(CONSTANTS.CE_ATTRIBUTES.SOURCE).to.equal("source"); 132 | }); 133 | it("Provides an ID attribute", () => { 134 | expect(CONSTANTS.CE_ATTRIBUTES.ID).to.equal("id"); 135 | }); 136 | it("Provides a TIME attribute", () => { 137 | expect(CONSTANTS.CE_ATTRIBUTES.TIME).to.equal("time"); 138 | }); 139 | it("Provides a DATA_SCHEMA attribute", () => { 140 | expect(CONSTANTS.STRUCTURED_ATTRS_1.DATA_SCHEMA).to.equal("dataschema"); 141 | }); 142 | it("Provides a CONTENT_TYPE attribute", () => { 143 | expect(CONSTANTS.CE_ATTRIBUTES.CONTENT_TYPE).to.equal("datacontenttype"); 144 | }); 145 | it("Provides a SUBJECT attribute", () => { 146 | expect(CONSTANTS.CE_ATTRIBUTES.SUBJECT).to.equal("subject"); 147 | }); 148 | it("Provides a DATA attribute", () => { 149 | expect(CONSTANTS.CE_ATTRIBUTES.DATA).to.equal("data"); 150 | }); 151 | it("Provides a DATA_BASE64 attribute", () => { 152 | expect(CONSTANTS.STRUCTURED_ATTRS_1.DATA_BASE64).to.equal("data_base64"); 153 | }); 154 | }); 155 | }); 156 | -------------------------------------------------------------------------------- /test/integration/emitter_factory_test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 The CloudEvents Authors 3 | SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | import "mocha"; 7 | import { expect } from "chai"; 8 | import nock from "nock"; 9 | import axios, { AxiosRequestHeaders } from "axios"; 10 | import request from "superagent"; 11 | import got from "got"; 12 | 13 | import CONSTANTS from "../../src/constants"; 14 | import { CloudEvent, HTTP, Message, Mode, Options, TransportFunction, emitterFor, httpTransport } 15 | from "../../src"; 16 | 17 | const DEFAULT_CE_CONTENT_TYPE = CONSTANTS.DEFAULT_CE_CONTENT_TYPE; 18 | const sink = "https://cloudevents.io/"; 19 | const type = "com.example.test"; 20 | const source = "urn:event:from:myapi/resource/123"; 21 | const ext1Name = "lunch"; 22 | const ext1Value = "tacos"; 23 | const ext2Name = "supper"; 24 | const ext2Value = "sushi"; 25 | const ext3Name = "snack"; 26 | const ext3Value = { value: "chips" }; 27 | 28 | const data = { 29 | lunchBreak: "noon", 30 | }; 31 | 32 | export const fixture = new CloudEvent({ 33 | source, 34 | type, 35 | data, 36 | [ext1Name]: ext1Value, 37 | [ext2Name]: ext2Value, 38 | [ext3Name]: ext3Value, 39 | }); 40 | 41 | function axiosEmitter(message: Message, options?: Options): Promise { 42 | return axios.post(sink, message.body, { headers: message.headers as AxiosRequestHeaders, ...options }); 43 | } 44 | 45 | function superagentEmitter(message: Message, options?: Options): Promise { 46 | const post = request.post(sink); 47 | // set any provided options 48 | if (options) { 49 | for (const key of Object.getOwnPropertyNames(options)) { 50 | if (options[key]) { 51 | post.set(key, options[key] as string); 52 | } 53 | } 54 | } 55 | // set headers 56 | for (const key of Object.getOwnPropertyNames(message.headers)) { 57 | post.set(key, message.headers[key] as string); 58 | } 59 | return post.send(message.body as string); 60 | } 61 | 62 | function gotEmitter(message: Message, options?: Options): Promise { 63 | return Promise.resolve( 64 | got.post(sink, { headers: message.headers, body: message.body as string, ...(options as Options) }), 65 | ); 66 | } 67 | 68 | describe("emitterFor() defaults", () => { 69 | it("Defaults to HTTP binding, binary mode", () => { 70 | function transport(message: Message): Promise { 71 | // A binary message will have the source attribute as a header 72 | expect(message.headers[CONSTANTS.CE_HEADERS.TYPE]).to.equal("emitter.test"); 73 | return Promise.resolve(); 74 | } 75 | const emitter = emitterFor(transport); 76 | emitter( 77 | new CloudEvent({ 78 | id: "1234", 79 | source: "/emitter/test", 80 | type: "emitter.test", 81 | }), 82 | ); 83 | }); 84 | 85 | it("Supports HTTP binding, structured mode", () => { 86 | function transport(message: Message): Promise { 87 | // A structured message will have the application/cloudevents+json header 88 | expect(message.headers["content-type"]).to.equal(CONSTANTS.DEFAULT_CE_CONTENT_TYPE); 89 | const body = JSON.parse(message.body as string); 90 | expect(body.id).to.equal("1234"); 91 | return Promise.resolve(); 92 | } 93 | const emitter = emitterFor(transport, { mode: Mode.STRUCTURED }); 94 | emitter( 95 | new CloudEvent({ 96 | id: "1234", 97 | source: "/emitter/test", 98 | type: "emitter.test", 99 | }), 100 | ); 101 | }); 102 | }); 103 | 104 | function setupMock(uri: string) { 105 | nock(uri) 106 | .post("/") 107 | .reply(function (uri: string, body: nock.Body) { 108 | // return the request body and the headers so they can be 109 | // examined in the test 110 | if (typeof body === "string") { 111 | body = JSON.parse(body); 112 | } 113 | const returnBody = { ...(body as Record), ...this.req.headers }; 114 | return [201, returnBody]; 115 | }); 116 | } 117 | 118 | describe("HTTP Transport Binding for emitterFactory", () => { 119 | beforeEach(() => { setupMock(sink); }); 120 | 121 | describe("HTTPS builtin", () => { 122 | testEmitterBinary(httpTransport(sink), "body"); 123 | }); 124 | 125 | describe("HTTP builtin", () => { 126 | setupMock("http://cloudevents.io"); 127 | testEmitterBinary(httpTransport("http://cloudevents.io"), "body"); 128 | setupMock("http://cloudevents.io"); 129 | testEmitterStructured(httpTransport("http://cloudevents.io"), "body"); 130 | }); 131 | 132 | describe("Axios", () => { 133 | testEmitterBinary(axiosEmitter, "data"); 134 | testEmitterStructured(axiosEmitter, "data"); 135 | }); 136 | describe("SuperAgent", () => { 137 | testEmitterBinary(superagentEmitter, "body"); 138 | testEmitterStructured(superagentEmitter, "body"); 139 | }); 140 | 141 | describe("Got", () => { 142 | testEmitterBinary(gotEmitter, "body"); 143 | testEmitterStructured(gotEmitter, "body"); 144 | }); 145 | }); 146 | 147 | function testEmitterBinary(fn: TransportFunction, bodyAttr: string) { 148 | it("Works as a binary event emitter", async () => { 149 | const emitter = emitterFor(fn); 150 | const response = (await emitter(fixture)) as Record>; 151 | let body = response[bodyAttr]; 152 | if (typeof body === "string") { 153 | body = JSON.parse(body); 154 | } 155 | assertBinary(body); 156 | }); 157 | } 158 | 159 | function testEmitterStructured(fn: TransportFunction, bodyAttr: string) { 160 | it("Works as a structured event emitter", async () => { 161 | const emitter = emitterFor(fn, { binding: HTTP, mode: Mode.STRUCTURED }); 162 | const response = (await emitter(fixture)) as Record>>; 163 | let body = response[bodyAttr]; 164 | if (typeof body === "string") { 165 | body = JSON.parse(body); 166 | } 167 | assertStructured(body); 168 | }); 169 | } 170 | 171 | /** 172 | * Verify the received binary answer compare to the original fixture message 173 | * 174 | * @param {Record>} response received to compare to fixture 175 | * @return {void} void 176 | */ 177 | export function assertBinary(response: Record): void { 178 | expect(response.lunchBreak).to.equal(data.lunchBreak); 179 | expect(response["ce-type"]).to.equal(type); 180 | expect(response["ce-source"]).to.equal(source); 181 | expect(response[`ce-${ext1Name}`]).to.deep.equal(ext1Value); 182 | expect(response[`ce-${ext2Name}`]).to.deep.equal(ext2Value); 183 | expect(response[`ce-${ext3Name}`]).to.deep.equal(ext3Value); 184 | } 185 | 186 | /** 187 | * Verify the received structured answer compare to the original fixture message 188 | * 189 | * @param {Record>} response received to compare to fixture 190 | * @return {void} void 191 | */ 192 | export function assertStructured(response: Record>): void { 193 | expect(response.data.lunchBreak).to.equal(data.lunchBreak); 194 | expect(response.type).to.equal(type); 195 | expect(response.source).to.equal(source); 196 | expect(response["content-type"]).to.equal(DEFAULT_CE_CONTENT_TYPE); 197 | expect(response[ext1Name]).to.deep.equal(ext1Value); 198 | expect(response[ext2Name]).to.deep.equal(ext2Value); 199 | expect(response[ext3Name]).to.deep.equal(ext3Value); 200 | } 201 | -------------------------------------------------------------------------------- /test/integration/emitter_singleton_test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 The CloudEvents Authors 3 | SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | import "mocha"; 7 | 8 | import { emitterFor, HTTP, Mode, Message, Emitter } from "../../src"; 9 | 10 | import { fixture, assertStructured } from "./emitter_factory_test"; 11 | 12 | import { rejects, doesNotReject } from "assert"; 13 | 14 | describe("Emitter Singleton", () => { 15 | it("emit a Node.js 'cloudevent' event as an EventEmitter", async () => { 16 | const msg: Message | unknown = await new Promise((resolve) => { 17 | const fn = async (message: Message) => { 18 | resolve(message); 19 | }; 20 | const emitter = emitterFor(fn, { binding: HTTP, mode: Mode.STRUCTURED }); 21 | Emitter.on("cloudevent", emitter); 22 | 23 | fixture.emit(false); 24 | }); 25 | let body: unknown = (msg as Message).body; 26 | if (typeof body === "string") { 27 | body = JSON.parse(body); 28 | } 29 | assertStructured({ ...(body as any), ...(msg as Message).headers }); 30 | }); 31 | 32 | it("emit a Node.js 'cloudevent' event as an EventEmitter with ensureDelivery", async () => { 33 | let msg: Message | unknown = undefined; 34 | const fn = async (message: Message) => { 35 | msg = message; 36 | }; 37 | const emitter = emitterFor(fn, { binding: HTTP, mode: Mode.STRUCTURED }); 38 | Emitter.on("cloudevent", emitter); 39 | await fixture.emit(true); 40 | let body: any = (msg as Message).body; 41 | if (typeof body === "string") { 42 | body = JSON.parse(body); 43 | } 44 | assertStructured({ ...(body as any), ...(msg as Message).headers }); 45 | }); 46 | 47 | it("emit a Node.js 'cloudevent' event as an EventEmitter with ensureDelivery Error", async () => { 48 | const emitter = async () => { 49 | throw new Error("Not sent"); 50 | }; 51 | Emitter.on("cloudevent", emitter); 52 | // Should fail with emitWithEnsureDelivery 53 | await rejects(() => fixture.emit(true)); 54 | // Should not fail with emitWithEnsureDelivery 55 | // Work locally but not on Github Actions 56 | if (!process.env.GITHUB_WORKFLOW) { 57 | await doesNotReject(() => fixture.emit(false)); 58 | } 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /test/integration/mqtt_tests.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 The CloudEvents Authors 3 | SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | import path from "path"; 7 | import fs from "fs"; 8 | 9 | import { expect } from "chai"; 10 | import { CloudEvent, CONSTANTS, V1, Headers } from "../../src"; 11 | import { asBase64 } from "../../src/event/validation"; 12 | import { Message, MQTT, MQTTMessage } from "../../src/message"; 13 | 14 | const type = "org.cncf.cloudevents.example"; 15 | const source = "urn:event:from:myapi/resource/123"; 16 | const time = new Date().toISOString(); 17 | const subject = "subject.ext"; 18 | const dataschema = "http://cloudevents.io/schema.json"; 19 | const datacontenttype = "application/json"; 20 | const id = "b46cf653-d48a-4b90-8dfa-355c01061361"; 21 | 22 | interface Idata { 23 | foo: string 24 | } 25 | const data: Idata = { 26 | foo: "bar", 27 | }; 28 | 29 | const ext1Name = "extension1"; 30 | const ext1Value = "foobar"; 31 | const ext2Name = "extension2"; 32 | const ext2Value = "acme"; 33 | 34 | // Binary data as base64 35 | const dataBinary = Uint8Array.from(JSON.stringify(data), (c) => c.codePointAt(0) as number); 36 | const data_base64 = asBase64(dataBinary); 37 | 38 | // Since the above is a special case (string as binary), let's test 39 | // with a real binary file one is likely to encounter in the wild 40 | const imageData = new Uint8Array(fs.readFileSync(path.join(process.cwd(), "test", "integration", "ce.png"))); 41 | const image_base64 = asBase64(imageData); 42 | 43 | const PUBLISH = {"Content Type": "application/json; charset=utf-8"}; 44 | 45 | const fixture = new CloudEvent({ 46 | specversion: V1, 47 | id, 48 | type, 49 | source, 50 | datacontenttype, 51 | subject, 52 | time, 53 | dataschema, 54 | data, 55 | [ext1Name]: ext1Value, 56 | [ext2Name]: ext2Value, 57 | }); 58 | 59 | describe("MQTT transport", () => { 60 | it("Handles events with no content-type and no datacontenttype", () => { 61 | const payload = "{Something[Not:valid}JSON"; 62 | const userProperties = fixture.toJSON() as Headers; 63 | const message: MQTTMessage = { 64 | PUBLISH: undefined, // no Content Type applied 65 | payload, 66 | "User Properties": userProperties, 67 | headers: userProperties, 68 | body: payload, 69 | }; 70 | const event = MQTT.toEvent(message) as CloudEvent; 71 | expect(event.data).to.equal(payload); 72 | expect(event.datacontentype).to.equal(undefined); 73 | }); 74 | 75 | it("Can detect invalid CloudEvent Messages", () => { 76 | // Create a message that is not an actual event 77 | const message: MQTTMessage = { 78 | payload: "Hello world!", 79 | PUBLISH: { 80 | "Content type": "text/plain", 81 | }, 82 | "User Properties": {}, 83 | headers: {}, 84 | body: undefined 85 | }; 86 | expect(MQTT.isEvent(message)).to.be.false; 87 | }); 88 | 89 | it("Can detect valid CloudEvent Messages", () => { 90 | // Now create a message that is an event 91 | const message = MQTT.binary( 92 | new CloudEvent({ 93 | source: "/message-test", 94 | type: "example", 95 | data, 96 | }), 97 | ); 98 | expect(MQTT.isEvent(message)).to.be.true; 99 | }); 100 | 101 | it("Handles CloudEvents with datacontenttype of text/plain", () => { 102 | const message: Message = MQTT.binary( 103 | new CloudEvent({ 104 | source: "/test", 105 | type: "example", 106 | datacontenttype: "text/plain", 107 | data: "Hello, friends!", 108 | }), 109 | ); 110 | const event = MQTT.toEvent(message) as CloudEvent; 111 | expect(event.data).to.equal(message.body); 112 | expect(event.validate()).to.be.true; 113 | }); 114 | 115 | it("Respects extension attribute casing (even if against spec)", () => { 116 | // Create a message that is an event 117 | const body = `{ "greeting": "hello" }`; 118 | const headers = { 119 | id: "1234", 120 | source: "test", 121 | type: "test.event", 122 | specversion: "1.0", 123 | LUNCH: "tacos", 124 | }; 125 | const message: MQTTMessage = { 126 | body, 127 | payload: body, 128 | PUBLISH, 129 | "User Properties": headers, 130 | headers 131 | }; 132 | expect(MQTT.isEvent(message)).to.be.true; 133 | const event = MQTT.toEvent(message) as CloudEvent; 134 | expect(event.LUNCH).to.equal("tacos"); 135 | expect(function () { 136 | event.validate(); 137 | }).to.throw("invalid attribute name: \"LUNCH\""); 138 | }); 139 | 140 | it("Can detect CloudEvent binary Messages with weird versions", () => { 141 | // Now create a message that is an event 142 | const body = `{ "greeting": "hello" }`; 143 | const headers = { 144 | id: "1234", 145 | source: "test", 146 | type: "test.event", 147 | specversion: "11.8", 148 | }; 149 | const message: MQTTMessage = { 150 | body, 151 | payload: body, 152 | PUBLISH, 153 | headers, 154 | "User Properties": headers, 155 | }; 156 | expect(MQTT.isEvent(message)).to.be.true; 157 | const event = MQTT.toEvent(message) as CloudEvent; 158 | expect(event.specversion).to.equal("11.8"); 159 | expect(event.validate()).to.be.false; 160 | }); 161 | 162 | it("Can detect CloudEvent structured Messages with weird versions", () => { 163 | // Now create a message that is an event 164 | const body = `{ "id": "123", "source": "test", "type": "test.event", "specversion": "11.8"}`; 165 | const message: MQTTMessage = { 166 | body, 167 | payload: body, 168 | headers: {}, 169 | PUBLISH: {"Content Type": CONSTANTS.MIME_CE_JSON}, 170 | "User Properties": {} 171 | }; 172 | expect(MQTT.isEvent(message)).to.be.true; 173 | expect(MQTT.toEvent(message)).not.to.throw; 174 | }); 175 | 176 | // Allow for external systems to send bad events - do what we can 177 | // to accept them 178 | it("Does not throw an exception when converting an invalid Message to a CloudEvent", () => { 179 | const body = `"hello world"`; 180 | const headers = { 181 | id: "1234", 182 | type: "example.bad.event", 183 | // no required source, thus an invalid event 184 | }; 185 | const message: MQTTMessage = { 186 | body, 187 | payload: body, 188 | PUBLISH, 189 | headers, 190 | "User Properties": headers, 191 | }; 192 | const event = MQTT.toEvent(message) as CloudEvent; 193 | expect(event).to.be.instanceOf(CloudEvent); 194 | // ensure that we actually now have an invalid event 195 | expect(event.validate).to.throw; 196 | }); 197 | 198 | it("Does not allow an invalid CloudEvent to be converted to a Message", () => { 199 | const badEvent = new CloudEvent( 200 | { 201 | source: "/example.source", 202 | type: "", // type is required, empty string will throw with strict validation 203 | }, 204 | false, // turn off strict validation 205 | ); 206 | expect(() => { 207 | MQTT.binary(badEvent); 208 | }).to.throw; 209 | expect(() => { 210 | MQTT.structured(badEvent); 211 | }).to.throw; 212 | }); 213 | 214 | it("Binary Messages can be created from a CloudEvent", () => { 215 | const message: Message = MQTT.binary(fixture); 216 | expect(message.body).to.equal(data); 217 | // validate all headers 218 | expect(message.headers.datacontenttype).to.equal(datacontenttype); 219 | expect(message.headers.specversion).to.equal(V1); 220 | expect(message.headers.id).to.equal(id); 221 | expect(message.headers.type).to.equal(type); 222 | expect(message.headers.source).to.equal(source); 223 | expect(message.headers.subject).to.equal(subject); 224 | expect(message.headers.time).to.equal(fixture.time); 225 | expect(message.headers.dataschema).to.equal(dataschema); 226 | expect(message.headers[ext1Name]).to.equal(ext1Value); 227 | expect(message.headers[ext2Name]).to.equal(ext2Value); 228 | }); 229 | 230 | it("Sets User Properties on binary messages", () => { 231 | const message: MQTTMessage = MQTT.binary(fixture) as MQTTMessage; 232 | expect(message.body).to.equal(data); 233 | // validate all headers 234 | expect(message["User Properties"]?.datacontenttype).to.equal(datacontenttype); 235 | expect(message["User Properties"]?.specversion).to.equal(V1); 236 | expect(message["User Properties"]?.id).to.equal(id); 237 | expect(message["User Properties"]?.type).to.equal(type); 238 | expect(message["User Properties"]?.source).to.equal(source); 239 | expect(message["User Properties"]?.subject).to.equal(subject); 240 | expect(message["User Properties"]?.time).to.equal(fixture.time); 241 | expect(message["User Properties"]?.dataschema).to.equal(dataschema); 242 | expect(message["User Properties"]?.[ext1Name]).to.equal(ext1Value); 243 | expect(message["User Properties"]?.[ext2Name]).to.equal(ext2Value); 244 | }); 245 | 246 | it("Structured Messages can be created from a CloudEvent", () => { 247 | const message = MQTT.structured(fixture) as MQTTMessage; 248 | expect(message.PUBLISH?.["Content Type"]).to.equal(CONSTANTS.DEFAULT_CE_CONTENT_TYPE); 249 | expect(message.body).to.deep.equal(message.payload); 250 | expect(message.payload).to.deep.equal(fixture.toJSON()); 251 | const body = message.body as Record; 252 | expect(body[CONSTANTS.CE_ATTRIBUTES.SPEC_VERSION]).to.equal(V1); 253 | expect(body[CONSTANTS.CE_ATTRIBUTES.ID]).to.equal(id); 254 | expect(body[CONSTANTS.CE_ATTRIBUTES.TYPE]).to.equal(type); 255 | expect(body[CONSTANTS.CE_ATTRIBUTES.SOURCE]).to.equal(source); 256 | expect(body[CONSTANTS.CE_ATTRIBUTES.SUBJECT]).to.equal(subject); 257 | expect(body[CONSTANTS.CE_ATTRIBUTES.TIME]).to.equal(fixture.time); 258 | expect(body[CONSTANTS.STRUCTURED_ATTRS_1.DATA_SCHEMA]).to.equal(dataschema); 259 | expect(body[ext1Name]).to.equal(ext1Value); 260 | expect(body[ext2Name]).to.equal(ext2Value); 261 | }); 262 | 263 | it("A CloudEvent can be converted from a binary Message", () => { 264 | const message = MQTT.binary(fixture); 265 | const event = MQTT.toEvent(message); 266 | expect(event).to.deep.equal(fixture); 267 | }); 268 | 269 | it("A CloudEvent can be converted from a structured Message", () => { 270 | const message = MQTT.structured(fixture); 271 | const event = MQTT.toEvent(message); 272 | expect(event).to.deep.equal(fixture); 273 | }); 274 | 275 | it("Converts binary data to base64 when serializing structured messages", () => { 276 | const event = fixture.cloneWith({ data: imageData, datacontenttype: "image/png" }); 277 | expect(event.data).to.equal(imageData); 278 | const message = MQTT.structured(event); 279 | expect((message.body as CloudEvent).data_base64).to.equal(image_base64); 280 | }); 281 | 282 | it("Converts base64 encoded data to binary when deserializing structured messages", () => { 283 | const message = MQTT.structured(fixture.cloneWith({ data: imageData, datacontenttype: "image/png" })); 284 | const eventDeserialized = MQTT.toEvent(message) as CloudEvent; 285 | expect(eventDeserialized.data).to.deep.equal(imageData); 286 | expect(eventDeserialized.data_base64).to.equal(image_base64); 287 | }); 288 | 289 | it("Converts base64 encoded data to binary when deserializing binary messages", () => { 290 | const message = MQTT.binary(fixture.cloneWith({ data: imageData, datacontenttype: "image/png" })); 291 | const eventDeserialized = MQTT.toEvent(message) as CloudEvent; 292 | expect(eventDeserialized.data).to.deep.equal(imageData); 293 | expect(eventDeserialized.data_base64).to.equal(image_base64); 294 | }); 295 | 296 | it("Keeps binary data binary when serializing binary messages", () => { 297 | const event = fixture.cloneWith({ data: dataBinary }); 298 | expect(event.data).to.equal(dataBinary); 299 | const message = MQTT.binary(event); 300 | expect(message.body).to.equal(dataBinary); 301 | }); 302 | 303 | it("Does not parse binary data from binary messages with content type application/json", () => { 304 | const message = MQTT.binary(fixture.cloneWith({ data: dataBinary })); 305 | const eventDeserialized = MQTT.toEvent(message) as CloudEvent; 306 | expect(eventDeserialized.data).to.deep.equal(dataBinary); 307 | expect(eventDeserialized.data_base64).to.equal(data_base64); 308 | }); 309 | }); 310 | -------------------------------------------------------------------------------- /test/integration/parser_test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 The CloudEvents Authors 3 | SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | import "mocha"; 7 | import { expect } from "chai"; 8 | 9 | import { JSONParser as Parser } from "../../src/parsers"; 10 | import { ValidationError } from "../../src/"; 11 | 12 | describe("JSON Event Format Parser", () => { 13 | it("Throw error when payload is an integer", () => { 14 | // setup 15 | const payload = 83; 16 | const parser = new Parser(); 17 | 18 | expect(parser.parse.bind(parser, (payload as unknown) as string)).to.throw( 19 | ValidationError, 20 | "invalid payload type, allowed are: string or object", 21 | ); 22 | }); 23 | 24 | it("Throw error when payload is null", () => { 25 | const payload = null; 26 | const parser = new Parser(); 27 | 28 | expect(parser.parse.bind(parser, (payload as unknown) as string)).to.throw( 29 | ValidationError, 30 | "null or undefined payload", 31 | ); 32 | }); 33 | 34 | it("Throw error when payload is undefined", () => { 35 | // setup 36 | const parser = new Parser(); 37 | 38 | // act and assert 39 | expect(parser.parse.bind(parser)).to.throw(ValidationError, "null or undefined payload"); 40 | }); 41 | 42 | it("Throw error when payload is a float", () => { 43 | // setup 44 | const payload = 8.3; 45 | const parser = new Parser(); 46 | 47 | expect(parser.parse.bind(parser, (payload as unknown) as string)).to.throw( 48 | ValidationError, 49 | "invalid payload type, allowed are: string or object", 50 | ); 51 | }); 52 | 53 | it("Throw error when payload is an invalid JSON", () => { 54 | // setup 55 | const payload = "{gg"; 56 | const parser = new Parser(); 57 | 58 | // TODO: Should the parser catch the SyntaxError and re-throw a ValidationError? 59 | expect(parser.parse.bind(parser, payload)).to.throw(SyntaxError); 60 | }); 61 | 62 | it("Accepts a string as valid JSON", () => { 63 | // setup 64 | const payload = "I am a string!"; 65 | const parser = new Parser(); 66 | 67 | expect(parser.parse(payload)).to.equal("I am a string!"); 68 | }); 69 | 70 | it("Must accept when the payload is a string well formed as JSON", () => { 71 | // setup 72 | const payload = "{\"much\" : \"wow\"}"; 73 | const parser = new Parser(); 74 | 75 | // act 76 | const actual = parser.parse(payload); 77 | 78 | // assert 79 | expect(actual).to.be.an("object"); 80 | }); 81 | }); 82 | -------------------------------------------------------------------------------- /test/integration/sdk_test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 The CloudEvents Authors 3 | SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | import "mocha"; 7 | import { expect } from "chai"; 8 | import { CloudEvent, CloudEventV1, V1, V03 } from "../../src"; 9 | 10 | const fixture: CloudEventV1 = { 11 | id: "123", 12 | type: "org.cloudevents.test", 13 | source: "http://cloudevents.io", 14 | specversion: V1, 15 | }; 16 | 17 | describe("The SDK Requirements", () => { 18 | it("should expose a CloudEvent type", () => { 19 | const event = new CloudEvent(fixture); 20 | expect(event instanceof CloudEvent).to.equal(true); 21 | }); 22 | 23 | describe("v0.3", () => { 24 | it("should create an (invalid) event using the right spec version", () => { 25 | expect( 26 | new CloudEvent({ 27 | ...fixture, 28 | specversion: V03, 29 | }, false).specversion, 30 | ).to.equal(V03); 31 | }); 32 | }); 33 | 34 | describe("v1.0", () => { 35 | it("should create an event using the right spec version", () => { 36 | expect(new CloudEvent(fixture).specversion).to.equal(V1); 37 | }); 38 | }); 39 | 40 | describe("Cloning events", () => { 41 | it("should clone simple objects that adhere to the CloudEventV1 interface", () => { 42 | const copy = CloudEvent.cloneWith(fixture, { id: "456" }, false); 43 | expect(copy.id).to.equal("456"); 44 | expect(copy.type).to.equal(fixture.type); 45 | expect(copy.source).to.equal(fixture.source); 46 | expect(copy.specversion).to.equal(fixture.specversion); 47 | }); 48 | 49 | it("should clone simple objects with data that adhere to the CloudEventV1 interface", () => { 50 | const copy = CloudEvent.cloneWith(fixture, { data: { lunch: "tacos" } }, false); 51 | expect(copy.data.lunch).to.equal("tacos"); 52 | }); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /test/integration/spec_1_tests.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 The CloudEvents Authors 3 | SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | import "mocha"; 7 | import { expect } from "chai"; 8 | import { CloudEvent, V1, ValidationError } from "../../src"; 9 | import { asBase64 } from "../../src/event/validation"; 10 | import Constants from "../../src/constants"; 11 | 12 | const id = "97699ec2-a8d9-47c1-bfa0-ff7aa526f838"; 13 | const type = "com.github.pull.create"; 14 | const source = "urn:event:from:myapi/resourse/123"; 15 | const time = new Date().toISOString(); 16 | const dataschema = "http://example.com/registry/myschema.json"; 17 | const data = { 18 | much: "wow", 19 | }; 20 | const subject = "subject-x0"; 21 | 22 | const cloudevent = new CloudEvent({ 23 | specversion: V1, 24 | id, 25 | source, 26 | type, 27 | subject, 28 | time, 29 | data, 30 | dataschema, 31 | datacontenttype: Constants.MIME_JSON, 32 | }); 33 | 34 | describe("CloudEvents Spec v1.0", () => { 35 | describe("REQUIRED Attributes", () => { 36 | it("Should have 'id'", () => { 37 | expect(cloudevent.id).to.equal(id); 38 | }); 39 | 40 | it("Should have 'source'", () => { 41 | expect(cloudevent.source).to.equal(source); 42 | }); 43 | 44 | it("Should have 'specversion'", () => { 45 | expect(cloudevent.specversion).to.equal("1.0"); 46 | }); 47 | 48 | it("Should have 'type'", () => { 49 | expect(cloudevent.type).to.equal(type); 50 | }); 51 | }); 52 | 53 | describe("OPTIONAL Attributes", () => { 54 | it("Should have 'datacontenttype'", () => { 55 | expect(cloudevent.datacontenttype).to.equal(Constants.MIME_JSON); 56 | }); 57 | 58 | it("Should have 'dataschema'", () => { 59 | expect(cloudevent.dataschema).to.equal(dataschema); 60 | }); 61 | 62 | it("Should have 'subject'", () => { 63 | expect(cloudevent.subject).to.equal(subject); 64 | }); 65 | 66 | it("Should have 'time'", () => { 67 | expect(cloudevent.time).to.equal(time); 68 | }); 69 | }); 70 | 71 | describe("Extensions Constraints", () => { 72 | it("should be ok when type is 'boolean'", () => { 73 | expect(cloudevent.cloneWith({ extboolean: true }).validate()).to.equal(true); 74 | }); 75 | 76 | it("should be ok when type is 'integer'", () => { 77 | expect(cloudevent.cloneWith({ extinteger: 2019 }).validate()).to.equal(true); 78 | }); 79 | 80 | it("should be ok when type is 'string'", () => { 81 | expect(cloudevent.cloneWith({ extstring: "an-string" }).validate()).to.equal(true); 82 | }); 83 | 84 | it("should be ok when type is 'Uint32Array' for 'Binary'", () => { 85 | const myBinary = new Uint32Array(2019); 86 | expect(cloudevent.cloneWith({ extbinary: myBinary }).validate()).to.equal(true); 87 | }); 88 | 89 | // URI 90 | it("should be ok when type is 'Date' for 'Timestamp'", () => { 91 | const myDate = new Date(); 92 | expect(cloudevent.cloneWith({ extdate: myDate }).validate()).to.equal(true); 93 | }); 94 | 95 | it("should be ok when the type is an object", () => { 96 | expect(cloudevent.cloneWith({ objectextension: { some: "object" } }).validate()).to.equal(true); 97 | }); 98 | 99 | it("should be ok when the type is an string converted from an object", () => { 100 | expect(cloudevent.cloneWith({ objectextension: JSON.stringify({ some: "object" }) }).validate()).to.equal(true); 101 | }); 102 | 103 | it("should only allow a-z|0-9 in the attribute names", () => { 104 | const testCases = [ 105 | "an extension", "an_extension", "an-extension", "an.extension", "an+extension" 106 | ]; 107 | testCases.forEach((testCase) => { 108 | const evt = cloudevent.cloneWith({ [testCase]: "a value"}, false); 109 | expect(() => evt.validate()).to.throw(ValidationError); 110 | }); 111 | }); 112 | }); 113 | 114 | describe("The Constraints check", () => { 115 | describe("'id'", () => { 116 | it("should throw an error when trying to remove", () => { 117 | expect(() => { 118 | delete (cloudevent as any).id; 119 | }).to.throw(TypeError); 120 | }); 121 | 122 | it("defaut ID create when an empty string", () => { 123 | const testEvent = cloudevent.cloneWith({ id: "" }); 124 | expect(testEvent.id.length).to.be.greaterThan(0); 125 | }); 126 | }); 127 | 128 | describe("'source'", () => { 129 | it("should throw an error when trying to remove", () => { 130 | expect(() => { 131 | delete (cloudevent as any).source; 132 | }).to.throw(TypeError); 133 | }); 134 | }); 135 | 136 | describe("'specversion'", () => { 137 | it("should throw an error when trying to remove", () => { 138 | expect(() => { 139 | delete (cloudevent as any).specversion; 140 | }).to.throw(TypeError); 141 | }); 142 | }); 143 | 144 | describe("'type'", () => { 145 | it("should throw an error when trying to remove", () => { 146 | expect(() => { 147 | delete (cloudevent as any).type; 148 | }).to.throw(TypeError); 149 | }); 150 | }); 151 | 152 | describe("'subject'", () => { 153 | it("should throw an error when is an empty string", () => { 154 | expect(() => { 155 | cloudevent.cloneWith({ subject: "" }); 156 | }).to.throw(ValidationError, "invalid payload"); 157 | }); 158 | }); 159 | 160 | describe("'time'", () => { 161 | it("must adhere to the format specified in RFC 3339", () => { 162 | const d = new Date(); 163 | const testEvent = cloudevent.cloneWith({ time: d.toString() }, false); 164 | // ensure that we always get back the same thing we passed in 165 | expect(testEvent.time).to.equal(d.toString()); 166 | // ensure that when stringified, the timestamp is in RFC3339 format 167 | expect(JSON.parse(JSON.stringify(testEvent)).time).to.equal(new Date(d.toString()).toISOString()); 168 | }); 169 | }); 170 | }); 171 | 172 | describe("Event data constraints", () => { 173 | it("Should have 'data'", () => { 174 | expect(cloudevent.data).to.deep.equal(data); 175 | }); 176 | 177 | it("should maintain the type of data when no datacontenttype is provided", () => { 178 | const ce = new CloudEvent({ 179 | source: "/cloudevents/test", 180 | type: "cloudevents.test", 181 | data: JSON.stringify(data), 182 | }); 183 | expect(typeof ce.data).to.equal("string"); 184 | }); 185 | 186 | const dataString = ")(*~^my data for ce#@#$%"; 187 | const testCases = [ 188 | { 189 | type: Int8Array, 190 | data: Int8Array.from(dataString, (c) => c.codePointAt(0) as number), 191 | expected: asBase64(Int8Array.from(dataString, (c) => c.codePointAt(0) as number)) 192 | }, 193 | { 194 | type: Uint8Array, 195 | data: Uint8Array.from(dataString, (c) => c.codePointAt(0) as number), 196 | expected: asBase64(Uint8Array.from(dataString, (c) => c.codePointAt(0) as number)) 197 | }, 198 | { 199 | type: Int16Array, 200 | data: Int16Array.from(dataString, (c) => c.codePointAt(0) as number), 201 | expected: asBase64(Int16Array.from(dataString, (c) => c.codePointAt(0) as number)) 202 | }, 203 | { 204 | type: Uint16Array, 205 | data: Uint16Array.from(dataString, (c) => c.codePointAt(0) as number), 206 | expected: asBase64(Uint16Array.from(dataString, (c) => c.codePointAt(0) as number)) 207 | }, 208 | { 209 | type: Int32Array, 210 | data: Int32Array.from(dataString, (c) => c.codePointAt(0) as number), 211 | expected: asBase64(Int32Array.from(dataString, (c) => c.codePointAt(0) as number)) 212 | }, 213 | { 214 | type: Uint32Array, 215 | data: Uint32Array.from(dataString, (c) => c.codePointAt(0) as number), 216 | expected: asBase64(Uint32Array.from(dataString, (c) => c.codePointAt(0) as number)) 217 | }, 218 | { 219 | type: Uint8ClampedArray, 220 | data: Uint8ClampedArray.from(dataString, (c) => c.codePointAt(0) as number), 221 | expected: asBase64(Uint8ClampedArray.from(dataString, (c) => c.codePointAt(0) as number)) 222 | }, 223 | { 224 | type: Float32Array, 225 | data: Float32Array.from(dataString, (c) => c.codePointAt(0) as number), 226 | expected: asBase64(Float32Array.from(dataString, (c) => c.codePointAt(0) as number)) 227 | }, 228 | { 229 | type: Float64Array, 230 | data: Float64Array.from(dataString, (c) => c.codePointAt(0) as number), 231 | expected: asBase64(Float64Array.from(dataString, (c) => c.codePointAt(0) as number)) 232 | }, 233 | ]; 234 | 235 | testCases.forEach((test) => { 236 | it(`should be ok when type is '${test.type.name}' for 'Binary'`, () => { 237 | const ce = cloudevent.cloneWith({ datacontenttype: "text/plain", data: test.data }); 238 | expect(ce.data_base64).to.equal(test.expected); 239 | }); 240 | }); 241 | }); 242 | }); 243 | -------------------------------------------------------------------------------- /test/integration/utilities_test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 The CloudEvents Authors 3 | SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | import "mocha"; 7 | import { expect } from "chai"; 8 | import { isStringOrThrow, equalsOrThrow, isBase64, asData } from "../../src/event/validation"; 9 | 10 | describe("Utilities", () => { 11 | describe("isStringOrThrow", () => { 12 | it("should throw when is not a string", () => { 13 | expect(isStringOrThrow.bind({}, 3.6, new Error("works!"))).to.throw("works!"); 14 | }); 15 | 16 | it("should return true when is a string", () => { 17 | expect(isStringOrThrow("cool", new Error("not throws!"))).to.equal(true); 18 | }); 19 | }); 20 | 21 | describe("equalsOrThrow", () => { 22 | it("should throw when they are not equals", () => { 23 | expect(equalsOrThrow.bind({}, "z", "a", new Error("works!"))).to.throw("works!"); 24 | }); 25 | 26 | it("should return true when they are equals", () => { 27 | expect(equalsOrThrow("z", "z", new Error())).to.equal(true); 28 | }); 29 | }); 30 | 31 | describe("isBase64", () => { 32 | it("should return false when is not base64 string", () => { 33 | const actual = isBase64("non base 64"); 34 | 35 | expect(actual).to.equal(false); 36 | }); 37 | 38 | it("should return true when is a base64 string", () => { 39 | const actual = isBase64("Y2xvdWRldmVudHMK"); 40 | 41 | expect(actual).to.equal(true); 42 | }); 43 | }); 44 | 45 | describe("asData", () => { 46 | it("should throw error when data is not a valid json", () => { 47 | const data = "not a json"; 48 | 49 | expect(asData.bind({}, data, "application/json")).to.throw(); 50 | }); 51 | 52 | it("should parse string content type as string", () => { 53 | const expected = "a string"; 54 | 55 | const actual = asData(expected, "text/plain"); 56 | 57 | expect(typeof actual).to.equal("string"); 58 | expect(actual).to.equal(expected); 59 | }); 60 | 61 | it("should parse 'application/json' as json object", () => { 62 | const expected = { 63 | much: "wow", 64 | myext: { 65 | ext: "x04", 66 | }, 67 | }; 68 | 69 | const actual = asData(JSON.stringify(expected), "application/json"); 70 | 71 | expect(typeof actual).to.equal("object"); 72 | expect(actual).to.deep.equal(expected); 73 | }); 74 | 75 | it("should parse 'application/cloudevents+json' as json object", () => { 76 | const expected = { 77 | much: "wow", 78 | myext: { 79 | ext: "x04", 80 | }, 81 | }; 82 | 83 | const actual = asData(JSON.stringify(expected), "application/cloudevents+json"); 84 | 85 | expect(typeof actual).to.equal("object"); 86 | expect(actual).to.deep.equal(expected); 87 | }); 88 | 89 | it("should parse 'text/json' as json object", () => { 90 | const expected = { 91 | much: "wow", 92 | myext: { 93 | ext: "x04", 94 | }, 95 | }; 96 | 97 | const actual = asData(JSON.stringify(expected), "text/json"); 98 | 99 | expect(typeof actual).to.equal("object"); 100 | expect(actual).to.deep.equal(expected); 101 | }); 102 | }); 103 | }); 104 | -------------------------------------------------------------------------------- /tsconfig.browser.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, /* Allow javascript files to be compiled. */ 4 | "checkJs": false, /* Report errors in .js files. */ 5 | "strict": true, /* Enable all strict type-checking options. */ 6 | "baseUrl": ".", 7 | "outDir": "./browser", 8 | "target": "es2016", 9 | "module": "commonjs", 10 | "moduleResolution": "node", 11 | "esModuleInterop": true, 12 | "lib": ["es2016", "dom", "es5"] 13 | }, 14 | "include": [ 15 | "src/**/*.ts" 16 | ] 17 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ 4 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ 5 | "allowJs": true, /* Allow javascript files to be compiled. */ 6 | "checkJs": false, /* Report errors in .js files. */ 7 | "strict": true, /* Enable all strict type-checking options. */ 8 | "noImplicitAny": true, 9 | "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 10 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 11 | "forceConsistentCasingInFileNames": true, /* Disallow inconsistently-cased references to the same file. */ 12 | "outDir": "./dist", 13 | "declaration": true, 14 | "experimentalDecorators": true, 15 | "isolatedModules": true, 16 | }, 17 | "compileOnSave": true, 18 | "include": [ 19 | "src/**/*.ts" 20 | ], 21 | "exclude": [ 22 | "node_modules" 23 | ], 24 | "typedocOptions": { 25 | "out": "docs" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const webpack = require("webpack"); 3 | 4 | module.exports = { 5 | entry: { 6 | "cloudevents": "./browser/index.js" 7 | }, 8 | resolve: { 9 | fallback: { 10 | util: require.resolve("util/"), 11 | http: false, 12 | https: false 13 | }, 14 | }, 15 | plugins: [ 16 | new webpack.ProvidePlugin({ 17 | process: 'process/browser' 18 | }) 19 | ], 20 | output: { 21 | path: path.resolve(__dirname, "bundles"), 22 | filename: "[name].js", 23 | libraryTarget: "umd", 24 | library: "cloudevents", 25 | umdNamedDefine: true 26 | }, 27 | devtool: "source-map", 28 | mode: "production" 29 | }; 30 | --------------------------------------------------------------------------------