diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..28661dd --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,33 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: j3lte + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**Runtime info:** +- [ ] Deno +- [ ] Node + - OS: [e.g. iOS] + - Version [e.g. 22] + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..bbcbbe7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: '' +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/workflows/npm.yml b/.github/workflows/npm.yml new file mode 100644 index 0000000..e8e4783 --- /dev/null +++ b/.github/workflows/npm.yml @@ -0,0 +1,36 @@ +name: Build NPM package (On Demand) +on: + workflow_dispatch: + inputs: + version: + description: "Release version (x.y.z):" + required: true + +jobs: + release: + name: Build NPM package (On Demand) + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Setup Deno + uses: denoland/setup-deno@v1 + + - name: Setup Node + uses: actions/setup-node@v3 + with: + node-version: 18.x + registry-url: 'https://registry.npmjs.org' + + - name: Run Deno dnt + run: deno task npm ${{ github.event.inputs.version }} + + - name: Check Version + run: cat ./npm/package.json | jq .version + + # Disabled for now + # - name: Publish to NPM + # run: cd ./npm && yarn publish --verbose --access public --new-version ${{ github.event.inputs.version }} + # env: + # NODE_AUTH_TOKEN: ${{ secrets.NPMJS_ACCESS_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..a936468 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,35 @@ +name: Build NPM package (On Release) + +on: + release: + types: + - published + +jobs: + release: + name: Build NPM package (On Demand) + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Setup Deno + uses: denoland/setup-deno@v1 + + - name: Setup Node + uses: actions/setup-node@v3 + with: + node-version: 18.x + registry-url: 'https://registry.npmjs.org' + + - name: Run Deno dnt + run: deno task npm ${{ github.ref_name }} + + - name: Check Version + run: cat ./npm/package.json | jq .version + + # Disabled for now + # - name: Publish to NPM + # run: cd ./npm && yarn publish --verbose --access public --new-version ${{ github.ref_name }} + # env: + # NODE_AUTH_TOKEN: ${{ secrets.NPMJS_ACCESS_TOKEN }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..087eb6a --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,40 @@ +name: Deno CI (test) + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + test: + name: Tests + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Setup Deno + uses: denoland/setup-deno@v1 + + - name: Check format + run: deno fmt --check + + - name: Check linting + run: deno lint + + - name: Tests + run: deno task test + + - name: Coverage + run: deno task coverage + + # Disabled for now + # - name: Upload coverage reports to Codecov + # uses: codecov/codecov-action@v3 + # with: + # token: ${{ secrets.CODECOV_TOKEN }} + # file: ./.coverage/coverage.lcov + # flags: unittests + # name: codecov-umbrella + # fail_ci_if_error: true diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..eb139ab --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +.env* +*.json +!deno.json +.DS_Store +node_modules +npm/ +_local_testing.ts +.coverage/ +coverage.lcov diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a6fa39f --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright © CaffCode 2023. All Rights Reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..1c37c5e --- /dev/null +++ b/README.md @@ -0,0 +1,15 @@ +# Pastedeno + +Pastebin Client for Deno/Node + +> Work in progress! + +> This is a fork of pastebin-ts, which is updated to work with Deno and Node + +## License + +[MIT](LICENSE) + +--- + +[!["Buy Me A Coffee"](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://www.buymeacoffee.com/j3lte) diff --git a/deno.json b/deno.json new file mode 100644 index 0000000..0d7979b --- /dev/null +++ b/deno.json @@ -0,0 +1,46 @@ +{ + "tasks": { + "format": "deno fmt ./src/", + "lint": "deno lint ./src/", + "test": "deno test --coverage=.coverage", + "coverage": "deno coverage .coverage --lcov --exclude=/test/ --exclude=/scripts/ > ./.coverage/coverage.lcov", + "test:watch": "deno test --watch", + "clean": "rm -r ./coverage", + "npm": "deno run -A ./scripts/build-npm.ts" + }, + "fmt": { + "options": { + "indentWidth": 2, + "lineWidth": 100, + "singleQuote": false, + "useTabs": false, + "proseWrap": "preserve" + }, + "files": { + "exclude": [ + ".coverage/", + "npm/", + ".github/", + "README.md" + ] + } + }, + "lint": { + "rules": { + "include": [ + "ban-untagged-todo", + "explicit-function-return-type" + ] + }, + "exclude": [ + "npm/" + ] + }, + "test": { + "exclude": [ + "npm/", + "src/node/", + ".coverage/" + ] + } +} diff --git a/deno.lock b/deno.lock new file mode 100644 index 0000000..6b55682 --- /dev/null +++ b/deno.lock @@ -0,0 +1,90 @@ +{ + "version": "3", + "redirects": { + "https://crux.land/router@0.0.5": "https://crux.land/api/get/2KNRVU.ts" + }, + "remote": { + "https://deno.land/std@0.140.0/_util/assert.ts": "e94f2eb37cebd7f199952e242c77654e43333c1ac4c5c700e929ea3aa5489f74", + "https://deno.land/std@0.140.0/_util/os.ts": "3b4c6e27febd119d36a416d7a97bd3b0251b77c88942c8f16ee5953ea13e2e49", + "https://deno.land/std@0.140.0/bytes/bytes_list.ts": "67eb118e0b7891d2f389dad4add35856f4ad5faab46318ff99653456c23b025d", + "https://deno.land/std@0.140.0/bytes/equals.ts": "fc16dff2090cced02497f16483de123dfa91e591029f985029193dfaa9d894c9", + "https://deno.land/std@0.140.0/bytes/mod.ts": "763f97d33051cc3f28af1a688dfe2830841192a9fea0cbaa55f927b49d49d0bf", + "https://deno.land/std@0.140.0/fmt/colors.ts": "30455035d6d728394781c10755351742dd731e3db6771b1843f9b9e490104d37", + "https://deno.land/std@0.140.0/fs/_util.ts": "0fb24eb4bfebc2c194fb1afdb42b9c3dda12e368f43e8f2321f84fc77d42cb0f", + "https://deno.land/std@0.140.0/fs/ensure_dir.ts": "9dc109c27df4098b9fc12d949612ae5c9c7169507660dcf9ad90631833209d9d", + "https://deno.land/std@0.140.0/io/buffer.ts": "bd0c4bf53db4b4be916ca5963e454bddfd3fcd45039041ea161dbf826817822b", + "https://deno.land/std@0.140.0/path/_constants.ts": "df1db3ffa6dd6d1252cc9617e5d72165cd2483df90e93833e13580687b6083c3", + "https://deno.land/std@0.140.0/path/_interface.ts": "ee3b431a336b80cf445441109d089b70d87d5e248f4f90ff906820889ecf8d09", + "https://deno.land/std@0.140.0/path/_util.ts": "c1e9686d0164e29f7d880b2158971d805b6e0efc3110d0b3e24e4b8af2190d2b", + "https://deno.land/std@0.140.0/path/common.ts": "bee563630abd2d97f99d83c96c2fa0cca7cee103e8cb4e7699ec4d5db7bd2633", + "https://deno.land/std@0.140.0/path/glob.ts": "cb5255638de1048973c3e69e420c77dc04f75755524cb3b2e160fe9277d939ee", + "https://deno.land/std@0.140.0/path/mod.ts": "d3e68d0abb393fb0bf94a6d07c46ec31dc755b544b13144dee931d8d5f06a52d", + "https://deno.land/std@0.140.0/path/posix.ts": "293cdaec3ecccec0a9cc2b534302dfe308adb6f10861fa183275d6695faace44", + "https://deno.land/std@0.140.0/path/separator.ts": "fe1816cb765a8068afb3e8f13ad272351c85cbc739af56dacfc7d93d710fe0f9", + "https://deno.land/std@0.140.0/path/win32.ts": "31811536855e19ba37a999cd8d1b62078235548d67902ece4aa6b814596dd757", + "https://deno.land/std@0.140.0/streams/conversion.ts": "712585bfa0172a97fb68dd46e784ae8ad59d11b88079d6a4ab098ff42e697d21", + "https://deno.land/std@0.181.0/_util/asserts.ts": "178dfc49a464aee693a7e285567b3d0b555dc805ff490505a8aae34f9cfb1462", + "https://deno.land/std@0.181.0/_util/os.ts": "d932f56d41e4f6a6093d56044e29ce637f8dcc43c5a90af43504a889cf1775e3", + "https://deno.land/std@0.181.0/fmt/colors.ts": "d67e3cd9f472535241a8e410d33423980bec45047e343577554d3356e1f0ef4e", + "https://deno.land/std@0.181.0/fs/_util.ts": "65381f341af1ff7f40198cee15c20f59951ac26e51ddc651c5293e24f9ce6f32", + "https://deno.land/std@0.181.0/fs/empty_dir.ts": "c3d2da4c7352fab1cf144a1ecfef58090769e8af633678e0f3fabaef98594688", + "https://deno.land/std@0.181.0/fs/ensure_dir.ts": "dc64c4c75c64721d4e3fb681f1382f803ff3d2868f08563ff923fdd20d071c40", + "https://deno.land/std@0.181.0/fs/expand_glob.ts": "e4f56259a0a70fe23f05215b00de3ac5e6ba46646ab2a06ebbe9b010f81c972a", + "https://deno.land/std@0.181.0/fs/walk.ts": "ea95ffa6500c1eda6b365be488c056edc7c883a1db41ef46ec3bf057b1c0fe32", + "https://deno.land/std@0.181.0/path/_constants.ts": "e49961f6f4f48039c0dfed3c3f93e963ca3d92791c9d478ac5b43183413136e0", + "https://deno.land/std@0.181.0/path/_interface.ts": "6471159dfbbc357e03882c2266d21ef9afdb1e4aa771b0545e90db58a0ba314b", + "https://deno.land/std@0.181.0/path/_util.ts": "d7abb1e0dea065f427b89156e28cdeb32b045870acdf865833ba808a73b576d0", + "https://deno.land/std@0.181.0/path/common.ts": "ee7505ab01fd22de3963b64e46cff31f40de34f9f8de1fff6a1bd2fe79380000", + "https://deno.land/std@0.181.0/path/glob.ts": "d479e0a695621c94d3fd7fe7abd4f9499caf32a8de13f25073451c6ef420a4e1", + "https://deno.land/std@0.181.0/path/mod.ts": "bf718f19a4fdd545aee1b06409ca0805bd1b68ecf876605ce632e932fe54510c", + "https://deno.land/std@0.181.0/path/posix.ts": "8b7c67ac338714b30c816079303d0285dd24af6b284f7ad63da5b27372a2c94d", + "https://deno.land/std@0.181.0/path/separator.ts": "0fb679739d0d1d7bf45b68dacfb4ec7563597a902edbaf3c59b50d5bcadd93b1", + "https://deno.land/std@0.181.0/path/win32.ts": "d186344e5583bcbf8b18af416d13d82b35a317116e6460a5a3953508c3de5bba", + "https://deno.land/x/code_block_writer@12.0.0/mod.ts": "2c3448060e47c9d08604c8f40dee34343f553f33edcdfebbf648442be33205e5", + "https://deno.land/x/code_block_writer@12.0.0/utils/string_utils.ts": "60cb4ec8bd335bf241ef785ccec51e809d576ff8e8d29da43d2273b69ce2a6ff", + "https://deno.land/x/deno_cache@0.6.2/auth_tokens.ts": "5d1d56474c54a9d152e44d43ea17c2e6a398dd1e9682c69811a313567c01ee1e", + "https://deno.land/x/deno_cache@0.6.2/cache.ts": "58b53c128b742757efcad10af9a3871f23b4e200674cb5b0ddf61164fb9b2fe7", + "https://deno.land/x/deno_cache@0.6.2/deno_dir.ts": "1ea355b8ba11c630d076b222b197cfc937dd81e5a4a260938997da99e8ff93a0", + "https://deno.land/x/deno_cache@0.6.2/deps.ts": "12cca94516cf2d3ed42fccd4b721ecd8060679253f077d83057511045b0081aa", + "https://deno.land/x/deno_cache@0.6.2/dirs.ts": "009c6f54e0b610914d6ce9f72f6f6ccfffd2d47a79a19061e0a9eb4253836069", + "https://deno.land/x/deno_cache@0.6.2/disk_cache.ts": "66a1e604a8d564b6dd0500326cac33d08b561d331036bf7272def80f2f7952aa", + "https://deno.land/x/deno_cache@0.6.2/file_fetcher.ts": "4f3e4a2c78a5ca1e4812099e5083f815a8525ab20d389b560b3517f6b1161dd6", + "https://deno.land/x/deno_cache@0.6.2/http_cache.ts": "407135eaf2802809ed373c230d57da7ef8dff923c4abf205410b9b99886491fd", + "https://deno.land/x/deno_cache@0.6.2/lib/deno_cache_dir.generated.js": "59f8defac32e8ebf2a30f7bc77e9d88f0e60098463fb1b75e00b9791a4bbd733", + "https://deno.land/x/deno_cache@0.6.2/lib/snippets/deno_cache_dir-a2aecaa9536c9402/fs.js": "cbe3a976ed63c72c7cb34ef845c27013033a3b11f9d8d3e2c4aa5dda2c0c7af6", + "https://deno.land/x/deno_cache@0.6.2/mod.ts": "b4004287e1c6123d7f07fe9b5b3e94ce6d990c4102949a89c527c68b19627867", + "https://deno.land/x/deno_cache@0.6.2/util.ts": "f3f5a0cfc60051f09162942fb0ee87a0e27b11a12aec4c22076e3006be4cc1e2", + "https://deno.land/x/dir@1.5.1/data_local_dir/mod.ts": "91eb1c4bfadfbeda30171007bac6d85aadacd43224a5ed721bbe56bc64e9eb66", + "https://deno.land/x/dnt@0.39.0/lib/compiler.ts": "7f4447531581896348b8a379ab94730856b42ae50d99043f2468328360293cb1", + "https://deno.land/x/dnt@0.39.0/lib/compiler_transforms.ts": "f21aba052f5dcf0b0595c734450842855c7f572e96165d3d34f8fed2fc1f7ba1", + "https://deno.land/x/dnt@0.39.0/lib/mod.deps.ts": "8d6123c8e1162037e58aa8126686a03d1e2cffb250a8757bf715f80242097597", + "https://deno.land/x/dnt@0.39.0/lib/npm_ignore.ts": "57fbb7e7b935417d225eec586c6aa240288905eb095847d3f6a88e290209df4e", + "https://deno.land/x/dnt@0.39.0/lib/package_json.ts": "607b0a4f44acad071a4c8533b312a27d6671eac8e6a23625c8350ce29eadb2ba", + "https://deno.land/x/dnt@0.39.0/lib/pkg/dnt_wasm.generated.js": "4f9c59b3ca6c875adabb10df256e273fff1129fca3a1557eb8936bddd7da7b18", + "https://deno.land/x/dnt@0.39.0/lib/pkg/snippets/dnt-wasm-a15ef721fa5290c5/helpers.js": "aba69a019a6da6f084898a6c7b903b8b583bc0dbd82bfb338449cf0b5bce58fd", + "https://deno.land/x/dnt@0.39.0/lib/shims.ts": "60fd285ad433c6944544595e7b885eab3eab09253252891380654f4cd3addaaa", + "https://deno.land/x/dnt@0.39.0/lib/test_runner/get_test_runner_code.ts": "4dc7a73a13b027341c0688df2b29a4ef102f287c126f134c33f69f0339b46968", + "https://deno.land/x/dnt@0.39.0/lib/test_runner/test_runner.ts": "4d0da0500ec427d5f390d9a8d42fb882fbeccc92c92d66b6f2e758606dbd40e6", + "https://deno.land/x/dnt@0.39.0/lib/transform.deps.ts": "2e159661e1c5c650de9a573babe0e319349fe493105157307ec2ad2f6a52c94e", + "https://deno.land/x/dnt@0.39.0/lib/types.ts": "b8e228b2fac44c2ae902fbb73b1689f6ab889915bd66486c8a85c0c24255f5fb", + "https://deno.land/x/dnt@0.39.0/lib/utils.ts": "224f15f33e7226a2fd991e438d0291d7ed8c7889807efa2e1ecb67d2d1db6720", + "https://deno.land/x/dnt@0.39.0/mod.ts": "9df36a862161d9eb376472b699f6cb08ba0ad1704e0826fbe13be766bd3c01da", + "https://deno.land/x/dnt@0.39.0/transform.ts": "f68743a14cf9bf53bfc9c81073871d69d447a7f9e3453e0447ca2fb78926bb1d", + "https://deno.land/x/ts_morph@20.0.0/bootstrap/mod.ts": "b53aad517f106c4079971fcd4a81ab79fadc40b50061a3ab2b741a09119d51e9", + "https://deno.land/x/ts_morph@20.0.0/bootstrap/ts_morph_bootstrap.js": "6645ac03c5e6687dfa8c78109dc5df0250b811ecb3aea2d97c504c35e8401c06", + "https://deno.land/x/ts_morph@20.0.0/common/DenoRuntime.ts": "6a7180f0c6e90dcf23ccffc86aa8271c20b1c4f34c570588d08a45880b7e172d", + "https://deno.land/x/ts_morph@20.0.0/common/mod.ts": "01985d2ee7da8d1caee318a9d07664774fbee4e31602bc2bb6bb62c3489555ed", + "https://deno.land/x/ts_morph@20.0.0/common/ts_morph_common.js": "2325f94f61dc5f3f98a1dab366dc93048d11b1433d718b10cfc6ee5a1cfebe8f", + "https://deno.land/x/ts_morph@20.0.0/common/typescript.js": "b9edf0a451685d13e0467a7ed4351d112b74bd1e256b915a2b941054e31c1736", + "https://deno.land/x/wasmbuild@0.15.1/cache.ts": "9d01b5cb24e7f2a942bbd8d14b093751fa690a6cde8e21709ddc97667e6669ed", + "https://deno.land/x/wasmbuild@0.15.1/loader.ts": "8c2fc10e21678e42f84c5135d8ab6ab7dc92424c3f05d2354896a29ccfd02a63", + "https://deno.land/x/xml@2.1.3/mod.ts": "4a314a7a28d1ec92f899ce4c6991f0356c77550a75955ec3f4a36733f08548e8", + "https://deno.land/x/xml@2.1.3/parse.ts": "614b8648345ae93c641368836947484d321c7ac9312ae12ec750434353cd7385", + "https://deno.land/x/xml@2.1.3/stringify.ts": "930d35431f153b29d36549cff08fcfbe978e52ccb56af1e3baa2e0760f418b04", + "https://deno.land/x/xml@2.1.3/utils/parser.ts": "263e06191bf7ec983eb542743f377f29f8a715590d67d1ffe4c848dd13389452", + "https://deno.land/x/xml@2.1.3/utils/stream.ts": "056e2f368d47932d77e431bbc4a8292359171cc9ce881ea31ce0aae30d763e68", + "https://deno.land/x/xml@2.1.3/utils/streamable.ts": "1603a5f10c859b95d4e9502365a0fba0b19d5d068356e20d5a6813cd37fee780", + "https://deno.land/x/xml@2.1.3/utils/stringifier.ts": "c701b506835237c0c6c0a08fd94e0a012b644def3f4c819c64788daf2e649ea3", + "https://deno.land/x/xml@2.1.3/utils/types.ts": "ecaf7785e54a6f1da6f8e56da2bce9853407ceb7d5b3b70f0a60a0890151fe4c" + } +} diff --git a/dev_deps.ts b/dev_deps.ts new file mode 100644 index 0000000..4764fa3 --- /dev/null +++ b/dev_deps.ts @@ -0,0 +1,16 @@ +export { + assertEquals, + assertMatch, + assertNotEquals, + assertThrows, +} from "https://deno.land/std@0.207.0/testing/asserts.ts"; +import * as mf from "https://deno.land/x/mock_fetch@0.3.0/mod.ts"; +export { mf }; +export { + afterAll, + afterEach, + beforeAll, + beforeEach, + describe, + it, +} from "https://deno.land/std@0.207.0/testing/bdd.ts"; diff --git a/mocks/fetch.ts b/mocks/fetch.ts new file mode 100644 index 0000000..93efbc0 --- /dev/null +++ b/mocks/fetch.ts @@ -0,0 +1 @@ +// WIP https://github.com/aegrumet/umbilical/blob/32331bdac3131e5cef140c5798204e48fd5b988e/mocks/fetch.ts diff --git a/mod.ts b/mod.ts new file mode 100644 index 0000000..a1ca67a --- /dev/null +++ b/mod.ts @@ -0,0 +1 @@ +export * from "./src/mod.ts"; diff --git a/scripts/build-npm.ts b/scripts/build-npm.ts new file mode 100644 index 0000000..3c7556f --- /dev/null +++ b/scripts/build-npm.ts @@ -0,0 +1,71 @@ +import { build, emptyDir } from "https://deno.land/x/dnt@0.39.0/mod.ts"; + +const cleanupTypes = async (dir: string) => { + for await (const dirEntry of Deno.readDir(dir)) { + const entryPath = `${dir}/${dirEntry.name}`; + if (dirEntry.isDirectory) { + await cleanupTypes(entryPath); + } else { + const file = await Deno.readTextFile(entryPath); + const newFile = file.replaceAll('.js"', '"'); + await Deno.writeTextFile(entryPath, newFile); + } + } +}; + +await emptyDir("./npm"); + +await build({ + entryPoints: ["./src/node/index.ts"], + outDir: "./npm", + mappings: {}, + declaration: "separate", + skipSourceOutput: true, + scriptModule: false, + shims: { + deno: true, + }, + test: false, + typeCheck: false, + compilerOptions: { + importHelpers: false, + target: "ES2021", + }, + package: { + // package.json properties + name: "pastedeno", + version: Deno.args[0] || "1.0.0", + description: "Pastebin client for Deno/Node", + license: "MIT", + publishConfiig: { + access: "public", + }, + keywords: [ + "pastebin", + "pastebin.com", + "pastebin-api", + "pastebin-client", + "pastebin-node", + "pastebin-deno", + "pastebin-js", + "pastebin-ts", + ], + author: { + name: "J.W. Lagendijk", + email: "jwlagendijk@gmail.com", + }, + repository: { + type: "git", + url: "git+https://github.com/j3lte/pastedeno.git", + }, + bugs: { + url: "https://github.com/j3lte/pastedeno/issues", + }, + }, + async postBuild(): Promise { + // steps to run after building and before running the tests + await Deno.copyFile("./LICENSE", "npm/LICENSE"); + await Deno.copyFile("./README.md", "npm/README.md"); + await cleanupTypes("./npm/types"); + }, +}); diff --git a/src/deno/Pastebin.ts b/src/deno/Pastebin.ts new file mode 100644 index 0000000..0e8af19 --- /dev/null +++ b/src/deno/Pastebin.ts @@ -0,0 +1,35 @@ +import { AbstractPastebin } from "../lib/Pastebin.ts"; +import { ICreatePasteFileOptions, ICreatePasteTextOptions } from "../lib/interfaces.ts"; + +export class Pastebin extends AbstractPastebin { + async createPasteFromFile( + options: ICreatePasteFileOptions = { file: "" }, + ): Promise { + if (options.file === "") { + return Promise.reject(new Error("File needed!")); + } + + let data: string; + + try { + // Check if options.file is a string or a Uint8Array + if (typeof options.file === "string") { + data = await Deno.readTextFile(options.file); + } else { + data = new TextDecoder().decode(options.file); + } + } catch (error) { + return Promise.reject(new Error(`Error reading file! ${error}`)); + } + + if (data.length === 0) { + return Promise.reject(new Error("Empty file!")); + } + + const pasteOpts = options as ICreatePasteTextOptions; + delete pasteOpts.file; + pasteOpts.text = data; + + return this.createPaste(pasteOpts); + } +} diff --git a/src/lib/Pastebin.ts b/src/lib/Pastebin.ts new file mode 100644 index 0000000..a9beef0 --- /dev/null +++ b/src/lib/Pastebin.ts @@ -0,0 +1,425 @@ +import { parse } from "https://deno.land/x/xml@2.1.3/mod.ts"; + +import { + ExpirationTime, + FormatType, + ICreatePasteTextOptions, + IPasteAPIOptions, + IPastebinOptions, + Paste, + PrivacyLevel, + User, +} from "./interfaces.ts"; + +const isNull = (value?: unknown): value is null => value === null; +const isUndefined = (value?: unknown): value is undefined => typeof value === "undefined"; +const ENDPOINTS = { + POST: "https://pastebin.com/api/api_post.php", + LOGIN: "https://pastebin.com/api/api_login.php", + APIRAW: "https://pastebin.com/api/api_raw.php", + RAW: "https://pastebin.com/raw.php?i=", +}; + +// @deno-fmt-ignore +const formatTypeArr: FormatType[] = [ + "4cs", "6502acme", "6502kickass", "6502tasm", "abap", "actionscript", "actionscript3", "ada", "aimms", "algol68", "apache", "applescript", "apt_sources", "arduino", "arm", "asm", "asp", "asymptote", "autoconf", "autohotkey", "autoit", "avisynth", "awk", "bascomavr", "bash", "basic4gl", "dos", "bibtex", "b3d", "blitzbasic", "bmx", "bnf", "boo", "bf", "c", "csharp", "c_winapi", "cpp", "cpp-winapi", "cpp-qt", "c_loadrunner", "caddcl", "cadlisp", "ceylon", "cfdg", "c_mac", "chaiscript", "chapel", "cil", "clojure", "klonec", "klonecpp", "cmake", "cobol", "coffeescript", "cfm", "css", "cuesheet", "d", "dart", "dcl", "dcpu16", "dcs", "delphi", "oxygene", "diff", "div", "dot", "e", "ezt", "ecmascript", "eiffel", "email", "epc", "erlang", "euphoria", "fsharp", "falcon", "filemaker", "fo", "f1", "fortran", "freebasic", "freeswitch", "gambas", "gml", "gdb", "gdscript", "genero", "genie", "gettext", "go", "godot-glsl", "groovy", "gwbasic", "haskell", "haxe", "hicest", "hq9plus", "html4strict", "html5", "icon", "idl", "ini", "inno", "intercal", "io", "ispfpanel", "j", "java", "java5", "javascript", "jcl", "jquery", "json", "julia", "kixtart", "kotlin", "ksp", "latex", "ldif", "lb", "lsl2", "lisp", "llvm", "locobasic", "logtalk", "lolcode", "lotusformulas", "lotusscript", "lscript", "lua", "m68k", "magiksf", "make", "mapbasic", "markdown", "matlab", "mercury", "metapost", "mirc", "mmix", "mk-61", "modula2", "modula3", "68000devpac", "mpasm", "mxml", "mysql", "nagios", "netrexx", "newlisp", "nginx", "nim", "nsis", "oberon2", "objeck", "objc", "ocaml", "ocaml-brief", "octave", "pf", "glsl", "oorexx", "oobas", "oracle8", "oracle11", "oz", "parasail", "parigp", "pascal", "pawn", "pcre", "per", "perl", "perl6", "phix", "php", "php-brief", "pic16", "pike", "pixelbender", "pli", "plsql", "postgresql", "postscript", "povray", "powerbuilder", "powershell", "proftpd", "progress", "prolog", "properties", "providex", "puppet", "purebasic", "pycon", "python", "pys60", "q", "qbasic", "qml", "rsplus", "racket", "rails", "rbs", "rebol", "reg", "rexx", "robots", "roff", "rpmspec", "ruby", "gnuplot", "rust", "sas", "scala", "scheme", "scilab", "scl", "sdlbasic", "smalltalk", "smarty", "spark", "sparql", "sqf", "sql", "sshconfig", "standardml", "stonescript", "sclang", "swift", "systemverilog", "tsql", "tcl", "teraterm", "texgraph", "thinbasic", "typescript", "typoscript", "unicon", "uscript", "upc", "urbi", "vala", "vbnet", "vbscript", "vedit", "verilog", "vhdl", "vim", "vb", "visualfoxpro", "visualprolog", "whitespace", "whois", "winbatch", "xbasic", "xml", "xojo", "xorg_conf", "xpp", "yaml", "yara", "z80", "zxbasic" ]; +const expirationTimeArr: string[] = Object.keys(ExpirationTime); + +export abstract class AbstractPastebin { + #config: IPastebinOptions; + + // We're able to overwrite fetch because it is an abstract class + fetch = globalThis.fetch; + + constructor(config?: IPastebinOptions | string | null) { + if (isUndefined(config) || isNull(config)) { + this.#config = {}; + return; + } + + let conf: IPastebinOptions | string = config; + if (typeof conf === "string") { + conf = { api_dev_key: conf }; + } + + this.#config = Object.assign({ + api_dev_key: null, + api_user_key: null, + api_user_name: null, + api_user_password: null, + }, conf); + } + + /** + * @param id ID of the paste + * @param isPrivate is the paste private? Needs authentication + * @returns + */ + async getPaste( + id: string, + isPrivate = false, + ): Promise { + let ID = id; + if (ID.startsWith("https://pastebin.com/")) { + ID = ID.replace("https://pastebin.com/", ""); + } + if (isPrivate) { + const params = this.#createParams("show_paste"); + params.api_paste_key = ID; + try { + await this.#createAPIuserKey(); + params.api_user_key = this.config.api_user_key as string; + + return this.#postRequest(ENDPOINTS.APIRAW, params); + } catch (error) { + return Promise.reject(error); + } + } + + return this.#getRequest(ENDPOINTS.RAW + ID); + } + + async createPaste(options: ICreatePasteTextOptions): Promise { + if (!this.hasDevKey) { + return Promise.reject(new Error("Dev key needed!")); + } + if (typeof options === "undefined") { + return Promise.reject(new Error("Create paste needs options!")); + } + + const { text, title, format, expiration } = options; + + let { privacy } = options; + + if (isUndefined(privacy) || typeof privacy !== "number") { + privacy = PrivacyLevel.PUBLIC_ANONYMOUS; + } else if (privacy > 3 || privacy < 0) { + return Promise.reject(new Error("Privacy level can only be 0 - 3")); + } + + const params = this.#createParams("paste"); + + params.api_paste_code = text; + params.api_paste_private = privacy; + + if (typeof text !== "string") { + return Promise.reject( + new Error("text can only be of type string!"), + ); + } + + if (!text || text.length === 0) { + return Promise.reject(new Error("Paste cannot have empty content")); + } + + if (typeof title === "string") { + params.api_paste_name = title; + } + + if (typeof format === "string") { + if (formatTypeArr.includes(format as FormatType)) { + params.api_paste_format = format; + } else { + return Promise.reject( + new Error(`Paste format ${options.format} is unknown!`), + ); + } + } + + if ( + privacy === PrivacyLevel.PRIVATE || + privacy === PrivacyLevel.PUBLIC_USER + ) { + try { + await this.#createAPIuserKey(); + } catch (error) { + return Promise.reject(error); + } + params.api_user_key = this.config.api_user_key as string; + } + + if (typeof expiration === "string") { + if (expirationTimeArr.includes(expiration)) { + params.api_paste_expire_date = expiration; + } else { + return Promise.reject( + new Error(`Expiration format '${expiration}' is unknown!`), + ); + } + } + + params.api_paste_private = privacy === PrivacyLevel.PUBLIC_USER + ? PrivacyLevel.PUBLIC_ANONYMOUS + : privacy; + + return this.#postRequest(ENDPOINTS.POST, params); + } + + async deletePaste(pasteID: string): Promise { + if (!this.hasDevKey) { + return Promise.reject(new Error("Dev key needed!")); + } + + let ID = pasteID; + if (ID.startsWith("https://pastebin.com/")) { + ID = ID.replace("https://pastebin.com/", ""); + } + + const params = this.#createParams("delete"); + params.api_paste_key = ID; + + try { + await this.#createAPIuserKey(); + } catch (error) { + return Promise.reject(error); + } + params.api_user_key = this.config.api_user_key as string; + + return this.#postRequest(ENDPOINTS.POST, params); + } + + async listUserPastes(limit = 50): Promise { + if (limit < 1 || limit > 1000) { + return Promise.reject( + new Error( + "listUserPastes only accepts a limit between 1 and 1000", + ), + ); + } + if (!this.hasDevKey) { + return Promise.reject(new Error("Dev key needed!")); + } + + const params = this.#createParams("list"); + params.api_results_limit = limit; + + try { + await this.#createAPIuserKey(); + } catch (error) { + return Promise.reject(error); + } + params.api_user_key = this.config.api_user_key as string; + + return this.#postAndParse(params, this.#parsePastes); + } + + async getUserInfo(): Promise { + if (!this.hasDevKey) { + return Promise.reject(new Error("Dev key needed!")); + } + + const params = this.#createParams("userdetails"); + + try { + await this.#createAPIuserKey(); + } catch (error) { + return Promise.reject(error); + } + params.api_user_key = this.config.api_user_key as string; + + return this.#postAndParse(params, this.#parseUser); + } + + #postAndParse(params: IPasteAPIOptions, parseFunc: (data: string) => T): Promise { + return this.#postRequest(ENDPOINTS.POST, params) + .then((data) => { + return parseFunc.call(this, data); + }); + } + + #createParams(option: string): IPasteAPIOptions { + const opts: IPasteAPIOptions = { + api_option: option, + }; + if (this.config.api_dev_key) { + opts.api_dev_key = this.config.api_dev_key; + } + return opts; + } + + #validateConfig(...validateKeys: Array): string | false { + const missing = validateKeys.filter( + (key) => + isUndefined(this.#config[key]) || + this.#config[key] === null || + this.#config[key] === "", + ); + + if (missing.length > 0) { + return `The following keys are missing: ${missing.join(",")}`; + } + + return false; + } + + #createAPIuserKey(): Promise { + const inValid = this.#validateConfig( + "api_dev_key", + "api_user_name", + "api_user_password", + ); + if (typeof inValid === "string") { + return Promise.reject(new Error(inValid)); + } + if ( + !isUndefined(this.config.api_user_key) && + !isNull(this.config.api_user_key) && + this.config.api_user_key !== "" + ) { + // We already have a key. Returning + return Promise.resolve(); + } + const { api_dev_key, api_user_name, api_user_password } = this.config as { + api_dev_key: string; + api_user_name: string; + api_user_password: string; + }; + + return this.#postRequest(ENDPOINTS.LOGIN, { + api_dev_key, + api_user_name, + api_user_password, + }).then((data) => { + if (data.length !== 32) { + return Promise.reject( + new Error(`Error in creating user key: ${data}`), + ); + } + this.config.api_user_key = data; + + return Promise.resolve(); + }); + } + + // Parse + + #parseUser(xml: string): User { + const data = parse(xml) as { user?: User }; + if (isUndefined(data) || isNull(data) || isUndefined(data.user)) { + throw new Error("No data returned to _parseUser!"); + } + return data.user; + } + + #parsePastes(xml: string): Paste[] { + const { root: data } = parse(`${xml}`) as unknown as { root: { paste: Paste[] } }; + if (isUndefined(data) || isNull(data) || isUndefined(data.paste)) { + throw new Error("No data returned to _parsePastes!"); + } + return data.paste; + } + + // Request + + #getRequestOptions( + method: "GET" | "POST", + params: Record | IPasteAPIOptions = {}, + ): RequestInit { + const abortController = AbortController ? new AbortController() : undefined; + const timeout = 4000; + + const _timeoutId = setTimeout(() => { + if (abortController) { + abortController.abort(); + } + }, timeout); + + const init: RequestInit = { + headers: new Headers({ + "User-Agent": "Pastebin-ts", + "Cache-Control": "no-cache", + }), + method, + cache: "no-cache", + signal: abortController ? abortController.signal : undefined, + }; + + if (method === "POST") { + // form + const formData = new FormData(); + for (const [key, value] of Object.entries(params)) { + formData.append(key, value); + } + init.body = formData; + } + + return init; + } + + async #handleResponse( + res: Response, + resolve: (value: string | PromiseLike) => void, + reject: (reason?: unknown) => void, + ): Promise { + if (!res || res === null) { + reject(new Error("No response!")); + return; + } + if (res.status === 404) { + reject(new Error("Not found!")); + return; + } + if (res.status === 403) { + reject(new Error("Forbidden! Is this paste private?")); + return; + } + if (!res.ok) { + reject(new Error(`Response not ok: ${res.status} : ${res.statusText}`)); + return; + } + let buffer: ArrayBuffer; + try { + buffer = await res.arrayBuffer(); + } catch (error) { + return reject(error); + } + // parse Buffer as text + const text = new TextDecoder().decode(buffer); + if (text.includes("Bad API request")) { + reject(new Error("Bad API request")); + return; + } + if (text.includes("Post limit")) { + reject(new Error("Post limit reached")); + return; + } + resolve(text); + } + + #abstractRequest( + method: "GET" | "POST", + path: string, + params?: Record | IPasteAPIOptions, + ): Promise { + return new Promise((resolve, reject) => { + const options = this.#getRequestOptions(method, params); + + if (!path) { + reject(new Error("No path provided!")); + return; + } + + this.fetch(path, options).then((res) => { + this.#handleResponse(res, resolve, reject); + }).catch((err) => { + reject(err); + }); + }); + } + + #getRequest(path: string): Promise { + return this.#abstractRequest("GET", path); + } + + #postRequest(path: string, params: Record | IPasteAPIOptions): Promise { + return this.#abstractRequest("POST", path, params); + } + + get config(): IPastebinOptions { + return this.#config; + } + + get hasDevKey(): boolean { + return this.#validateConfig("api_dev_key") === false; + } +} diff --git a/src/lib/interfaces.ts b/src/lib/interfaces.ts new file mode 100644 index 0000000..e1d3db8 --- /dev/null +++ b/src/lib/interfaces.ts @@ -0,0 +1,349 @@ +export interface IPastebinOptions { + api_dev_key?: null | string; + api_user_key?: null | string; + api_user_name?: null | string; + api_user_password?: null | string; +} + +export enum PrivacyLevel { + PUBLIC_ANONYMOUS = 0, + UNLISTED = 1, + PRIVATE = 2, + PUBLIC_USER = 3, +} + +export enum ExpirationTime { + NEVER = "N", + TEN_MINUTES = "10M", + ONE_HOUR = "1H", + ONE_DAY = "1D", + ONE_WEEK = "1W", + TWO_WEEKS = "2W", + ONE_MONTH = "1M", + SIX_MONTHS = "6M", + ONE_YEAR = "1Y", +} + +export type FormatType = + | "4cs" + | "6502acme" + | "6502kickass" + | "6502tasm" + | "abap" + | "actionscript" + | "actionscript3" + | "ada" + | "aimms" + | "algol68" + | "apache" + | "applescript" + | "apt_sources" + | "arduino" + | "arm" + | "asm" + | "asp" + | "asymptote" + | "autoconf" + | "autohotkey" + | "autoit" + | "avisynth" + | "awk" + | "bascomavr" + | "bash" + | "basic4gl" + | "dos" + | "bibtex" + | "b3d" + | "blitzbasic" + | "bmx" + | "bnf" + | "boo" + | "bf" + | "c" + | "csharp" + | "c_winapi" + | "cpp" + | "cpp-winapi" + | "cpp-qt" + | "c_loadrunner" + | "caddcl" + | "cadlisp" + | "ceylon" + | "cfdg" + | "c_mac" + | "chaiscript" + | "chapel" + | "cil" + | "clojure" + | "klonec" + | "klonecpp" + | "cmake" + | "cobol" + | "coffeescript" + | "cfm" + | "css" + | "cuesheet" + | "d" + | "dart" + | "dcl" + | "dcpu16" + | "dcs" + | "delphi" + | "oxygene" + | "diff" + | "div" + | "dot" + | "e" + | "ezt" + | "ecmascript" + | "eiffel" + | "email" + | "epc" + | "erlang" + | "euphoria" + | "fsharp" + | "falcon" + | "filemaker" + | "fo" + | "f1" + | "fortran" + | "freebasic" + | "freeswitch" + | "gambas" + | "gml" + | "gdb" + | "gdscript" + | "genero" + | "genie" + | "gettext" + | "go" + | "godot-glsl" + | "groovy" + | "gwbasic" + | "haskell" + | "haxe" + | "hicest" + | "hq9plus" + | "html4strict" + | "html5" + | "icon" + | "idl" + | "ini" + | "inno" + | "intercal" + | "io" + | "ispfpanel" + | "j" + | "java" + | "java5" + | "javascript" + | "jcl" + | "jquery" + | "json" + | "julia" + | "kixtart" + | "kotlin" + | "ksp" + | "latex" + | "ldif" + | "lb" + | "lsl2" + | "lisp" + | "llvm" + | "locobasic" + | "logtalk" + | "lolcode" + | "lotusformulas" + | "lotusscript" + | "lscript" + | "lua" + | "m68k" + | "magiksf" + | "make" + | "mapbasic" + | "markdown" + | "matlab" + | "mercury" + | "metapost" + | "mirc" + | "mmix" + | "mk-61" + | "modula2" + | "modula3" + | "68000devpac" + | "mpasm" + | "mxml" + | "mysql" + | "nagios" + | "netrexx" + | "newlisp" + | "nginx" + | "nim" + | "nsis" + | "oberon2" + | "objeck" + | "objc" + | "ocaml" + | "ocaml-brief" + | "octave" + | "pf" + | "glsl" + | "oorexx" + | "oobas" + | "oracle8" + | "oracle11" + | "oz" + | "parasail" + | "parigp" + | "pascal" + | "pawn" + | "pcre" + | "per" + | "perl" + | "perl6" + | "phix" + | "php" + | "php-brief" + | "pic16" + | "pike" + | "pixelbender" + | "pli" + | "plsql" + | "postgresql" + | "postscript" + | "povray" + | "powerbuilder" + | "powershell" + | "proftpd" + | "progress" + | "prolog" + | "properties" + | "providex" + | "puppet" + | "purebasic" + | "pycon" + | "python" + | "pys60" + | "q" + | "qbasic" + | "qml" + | "rsplus" + | "racket" + | "rails" + | "rbs" + | "rebol" + | "reg" + | "rexx" + | "robots" + | "roff" + | "rpmspec" + | "ruby" + | "gnuplot" + | "rust" + | "sas" + | "scala" + | "scheme" + | "scilab" + | "scl" + | "sdlbasic" + | "smalltalk" + | "smarty" + | "spark" + | "sparql" + | "sqf" + | "sql" + | "sshconfig" + | "standardml" + | "stonescript" + | "sclang" + | "swift" + | "systemverilog" + | "tsql" + | "tcl" + | "teraterm" + | "texgraph" + | "thinbasic" + | "typescript" + | "typoscript" + | "unicon" + | "uscript" + | "upc" + | "urbi" + | "vala" + | "vbnet" + | "vbscript" + | "vedit" + | "verilog" + | "vhdl" + | "vim" + | "vb" + | "visualfoxpro" + | "visualprolog" + | "whitespace" + | "whois" + | "winbatch" + | "xbasic" + | "xml" + | "xojo" + | "xorg_conf" + | "xpp" + | "yaml" + | "yara" + | "z80" + | "zxbasic" + | null; + +export interface ICreatePasteBaseOptions { + text?: string; + file?: string | T; + title?: string; + format?: FormatType; + privacy?: PrivacyLevel; + expiration?: ExpirationTime | null; +} + +export interface ICreatePasteTextOptions extends ICreatePasteBaseOptions { + text: string; +} + +export interface ICreatePasteFileOptions extends ICreatePasteBaseOptions { + file: string | T; +} + +export interface IPasteAPIOptions { + api_option: string; + api_dev_key?: string; + api_user_key?: string; + api_paste_code?: string; + api_paste_format?: FormatType; + api_paste_expire_date?: ExpirationTime; + api_paste_private?: number; + api_results_limit?: number; + api_paste_name?: string; + api_paste_key?: string; +} + +export interface Paste { + paste_key: string; + paste_date: number; + paste_title: string; + paste_size: number; + paste_expire_date: number; + paste_private: number; + paste_format_long: string; + paste_format_short: string; + paste_url: string; + paste_hits: number; +} + +export interface User { + user_name: string; + user_format_short: string; + user_expiration: string; + user_avatar_url: string; + user_private: number; + user_website: string | null; + user_email: string | null; + user_location: string | null; + user_account_type: number; +} diff --git a/src/mod.ts b/src/mod.ts new file mode 100644 index 0000000..662224b --- /dev/null +++ b/src/mod.ts @@ -0,0 +1,2 @@ +export * from "./deno/Pastebin.ts"; +export * from "./lib/interfaces.ts"; diff --git a/src/node/Pastebin.ts b/src/node/Pastebin.ts new file mode 100644 index 0000000..8c7d081 --- /dev/null +++ b/src/node/Pastebin.ts @@ -0,0 +1,37 @@ +import { AbstractPastebin } from "../lib/Pastebin.ts"; +import { ICreatePasteFileOptions, ICreatePasteTextOptions } from "../lib/interfaces.ts"; + +import fs from "node:fs/promises"; +import { Buffer } from "node:buffer"; + +export class Pastebin extends AbstractPastebin { + async createPasteFromFile( + options: ICreatePasteFileOptions = { file: "" }, + ): Promise { + if (options.file === "") { + return Promise.reject(new Error("File needed!")); + } + + let data: string; + + try { + if (Buffer.isBuffer(options.file)) { + data = options.file.toString("utf-8"); + } else { + data = await fs.readFile(options.file, "utf-8"); + } + } catch (error) { + return Promise.reject(new Error(`Error reading file! ${error}`)); + } + + if (data.length === 0) { + return Promise.reject(new Error("Empty file!")); + } + + const pasteOpts = options as ICreatePasteTextOptions; + delete pasteOpts.file; + pasteOpts.text = data; + + return this.createPaste(pasteOpts); + } +} diff --git a/src/node/index.ts b/src/node/index.ts new file mode 100644 index 0000000..ecaaaa4 --- /dev/null +++ b/src/node/index.ts @@ -0,0 +1,2 @@ +export * from "./Pastebin.ts"; +export * from "../lib/interfaces.ts";