From cd2ae84d57249d07f5e2c09a3c528d1a434f31f9 Mon Sep 17 00:00:00 2001 From: jmgasper Date: Tue, 1 Sep 2026 09:23:20 +1000 Subject: [PATCH 1/2] Remediate September 2026 security findings --- .nvmrc | 2 +- app-bootstrap.ts | 4 +- docker/Dockerfile | 43 +++--- migrator/package-lock.json | 12 +- migrator/package.json | 4 + package.json | 5 +- pnpm-lock.yaml | 123 +++++++++--------- pnpm-workspace.yaml | 14 +- src/common/helper.ts | 105 ++++++++++++--- .../ResourceRolePhaseDependencyService.ts | 2 +- src/services/ResourceService.ts | 2 +- test/postman/clearTestData.js | 3 +- test/postman/newman.js | 2 +- test/unit/outbound-url.test.ts | 43 ++++++ 14 files changed, 243 insertions(+), 121 deletions(-) create mode 100644 test/unit/outbound-url.test.ts diff --git a/.nvmrc b/.nvmrc index 3eb1386..0a2925f 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -26.5.0 +26.5.1 diff --git a/app-bootstrap.ts b/app-bootstrap.ts index cde8667..0fb6fc1 100644 --- a/app-bootstrap.ts +++ b/app-bootstrap.ts @@ -3,10 +3,10 @@ * * This module is loaded before controllers and services so their Joi schemas and * BigInt values retain the behavior exposed by the original JavaScript runtime. + * Node's native Promise remains in place because current HTTP dependencies use + * standard methods such as `Promise.withResolvers` that Bluebird does not expose. */ -global.Promise = require('bluebird') - const config = require('config') const Joi = require('joi') diff --git a/docker/Dockerfile b/docker/Dockerfile index 437f1ad..132244e 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,9 +1,8 @@ # syntax=docker/dockerfile:1 -ARG NODE_VERSION=26.5.0 -ARG ALPINE_VERSION=3.23 +ARG NODE_VERSION=26.5.1 -FROM node:${NODE_VERSION}-alpine${ALPINE_VERSION} AS build +FROM node:${NODE_VERSION}-alpine AS build ARG PNPM_VERSION=11.15.1 @@ -30,29 +29,31 @@ RUN mkdir -p src/scripts/seed \ && pnpm build \ && pnpm prune --prod -FROM node:${NODE_VERSION}-alpine${ALPINE_VERSION} AS production +FROM alpine:3.24 AS production + +ARG NODE_VERSION ENV NODE_ENV=production WORKDIR /resources-api RUN apk upgrade --no-cache \ - && apk add --no-cache bash openssl \ - && rm -rf /usr/local/lib/node_modules/npm \ - && rm -f /usr/local/bin/npm /usr/local/bin/npx - -COPY --from=build --chown=node:node /resources-api/dist ./dist -COPY --from=build --chown=node:node /resources-api/node_modules ./node_modules -COPY --from=build --chown=node:node /resources-api/package.json ./package.json -COPY --from=build --chown=node:node /resources-api/config ./config -COPY --from=build --chown=node:node /resources-api/prisma.config.ts ./prisma.config.ts -COPY --from=build --chown=node:node /resources-api/prisma ./prisma -COPY --from=build --chown=node:node /resources-api/docs ./docs -COPY --from=build --chown=node:node /resources-api/packages/resources-prisma-client ./packages/resources-prisma-client -COPY --from=build --chown=node:node /resources-api/src/generated ./src/generated -COPY --from=build --chown=node:node /resources-api/src/scripts/seed ./src/scripts/seed -COPY --from=build --chown=node:node --chmod=755 /resources-api/appStartUp.sh ./appStartUp.sh - -USER node + && apk add --no-cache bash "nodejs-current=${NODE_VERSION}-r0" \ + && addgroup -S -g 10001 app \ + && adduser -S -D -H -u 10001 -G app app + +COPY --from=build --chown=app:app /resources-api/dist ./dist +COPY --from=build --chown=app:app /resources-api/node_modules ./node_modules +COPY --from=build --chown=app:app /resources-api/package.json ./package.json +COPY --from=build --chown=app:app /resources-api/config ./config +COPY --from=build --chown=app:app /resources-api/prisma.config.ts ./prisma.config.ts +COPY --from=build --chown=app:app /resources-api/prisma ./prisma +COPY --from=build --chown=app:app /resources-api/docs ./docs +COPY --from=build --chown=app:app /resources-api/packages/resources-prisma-client ./packages/resources-prisma-client +COPY --from=build --chown=app:app /resources-api/src/generated ./src/generated +COPY --from=build --chown=app:app /resources-api/src/scripts/seed ./src/scripts/seed +COPY --from=build --chown=app:app --chmod=755 /resources-api/appStartUp.sh ./appStartUp.sh + +USER app EXPOSE 3000 CMD ["./appStartUp.sh"] diff --git a/migrator/package-lock.json b/migrator/package-lock.json index 0aba549..643c07b 100644 --- a/migrator/package-lock.json +++ b/migrator/package-lock.json @@ -1706,9 +1706,9 @@ } }, "node_modules/brace-expansion": { - "version": "1.1.16", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz", - "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", "dev": true, "license": "MIT", "dependencies": { @@ -3229,9 +3229,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "3.15.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.0.tgz", - "integrity": "sha512-ttBQIIQPDeLjpPOohtUdXuXUVoA2uIB6fEH9HyJ7234s5mBJ5wTx20njxplLZQgLaOfpmPQA7X2t5AX6tIPbog==", + "version": "3.15.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.1.tgz", + "integrity": "sha512-S99WuO3HlhO3XN41EtYUNl9zzXjoJx7QvmipxsJVxtCBT0YHEFy+iOJhjSvrmV12nYhWpZaM8lPHkJm0yUMbag==", "dev": true, "license": "MIT", "dependencies": { diff --git a/migrator/package.json b/migrator/package.json index 59b213d..d856af1 100644 --- a/migrator/package.json +++ b/migrator/package.json @@ -18,5 +18,9 @@ }, "devDependencies": { "jest": "^29.7.0" + }, + "overrides": { + "brace-expansion": "1.1.18", + "js-yaml": "3.15.1" } } diff --git a/package.json b/package.json index a00a048..95ac4a4 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "view-data": "node dist/src/scripts/view-data.js", "mock-api": "NODE_ENV=test node --require ts-node/register/transpile-only mock/mock-api.js", "mock-challenge-api": "NODE_ENV=test node --require ts-node/register/transpile-only mock/mock-challenge-api.js", - "test": "nyc --reporter=html --reporter=text mocha --require ts-node/register/transpile-only test/unit/logger.test.ts test/unit/prisma-adapter.test.ts test/unit/test.js --require test/common/prepare.js --timeout 60000 --exit", + "test": "nyc --reporter=html --reporter=text mocha --require ts-node/register/transpile-only test/unit/logger.test.ts test/unit/outbound-url.test.ts test/unit/prisma-adapter.test.ts test/unit/test.js --require test/common/prepare.js --timeout 60000 --exit", "test:logger": "mocha --require ts-node/register/transpile-only test/unit/logger.test.ts --timeout 10000", "test:adapter": "mocha --require ts-node/register/transpile-only test/unit/prisma-adapter.test.ts", "test:newman": "NODE_ENV=test node --require ts-node/register/transpile-only test/postman/newman.js", @@ -58,7 +58,6 @@ "@nestjs/platform-express": "^11.1.8", "@prisma/adapter-pg": "7.8.0", "@prisma/client": "7.8.0", - "bluebird": "^3.5.1", "body-parser": "1.20.6", "config": "^3.0.1", "cors": "^2.7.1", @@ -99,6 +98,6 @@ ] }, "volta": { - "node": "26.5.0" + "node": "26.5.1" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7d7587b..679be10 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -10,13 +10,20 @@ overrides: ajv@>=6.0.0 <6.14.0: 6.15.0 axios@<1.18.1: 1.18.1 body-parser@<1.20.6: 1.20.6 - brace-expansion@<1.1.16: 1.1.16 + brace-expansion@<1.1.18: 1.1.18 + brace-expansion@>=2.0.0 <2.1.4: 2.1.4 + brace-expansion@>=3.0.0 <3.0.6: 3.0.6 + brace-expansion@>=4.0.0 <5.0.9: 5.0.9 + deepmerge-ts@<8.0.0: 8.0.2 diff@>=6.0.0 <8.0.3: 8.0.3 - fast-uri@>=3.0.0 <3.1.4: 3.1.4 + fast-uri@>=3.0.0 <3.1.6: 3.1.6 flatted@<3.4.2: 3.4.2 handlebars@<4.7.9: 4.7.9 jose@>=3.0.0 <4.15.5: 4.15.5 - js-yaml@>=3.0.0 <3.15.0: 3.15.0 + hono@<4.12.34: 4.12.34 + ip-address@<=10.3.0: 10.3.1 + js-yaml@>=3.0.0 <3.15.1: 3.15.1 + js-yaml@>=4.0.0 <4.3.1: 4.3.1 lodash@<4.18.1: 4.18.1 node-forge@<1.4.0: 1.4.0 picomatch@<2.3.2: 2.3.2 @@ -28,6 +35,7 @@ overrides: minimatch@3.1.2: 3.1.4 path-to-regexp@0.1.12: 0.1.13 qs@<6.15.2: 6.15.2 + valibot@<=1.4.1: 1.4.2 importers: @@ -48,9 +56,6 @@ importers: '@prisma/client': specifier: 7.8.0 version: 7.8.0(prisma@7.8.0(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@5.9.3))(typescript@5.9.3) - bluebird: - specifier: ^3.5.1 - version: 3.7.2 body-parser: specifier: 1.20.6 version: 1.20.6(supports-color@5.5.0) @@ -404,7 +409,7 @@ packages: resolution: {integrity: sha512-bjD221KPLoJTWUwso1J6fGKiTXEUFedG/s0visavY4zakFPkeGURMRNly+FhBHs7T8Dz4qHaZIMX9ZoJHSJtKA==} engines: {node: '>=20'} peerDependencies: - hono: ^4 + hono: 4.12.34 '@humanfs/core@0.19.2': resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==} @@ -1244,9 +1249,6 @@ packages: bluebird@2.11.0: resolution: {integrity: sha512-UfFSr22dmHPQqPP9XWHRhq+gWnHCYguQGkXQlbyPtW5qTnhFWA8/iXg765tH0cAjy7l/zPJ1aBTO0g5XgA7kvQ==} - bluebird@3.7.2: - resolution: {integrity: sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==} - body-parser@1.20.6: resolution: {integrity: sha512-p5tAzS57i5MV9fZFDj9LeIiTZEufbSe2eDozP+ElheSUq1m74CRq1jI4mYNDdVs9vQztXFLuk/Gd6BWTdwRJ5g==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} @@ -1255,15 +1257,15 @@ packages: resolution: {integrity: sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==} engines: {node: '>=18'} - brace-expansion@1.1.16: - resolution: {integrity: sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==} + brace-expansion@1.1.18: + resolution: {integrity: sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==} - brace-expansion@2.1.2: - resolution: {integrity: sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==} + brace-expansion@2.1.4: + resolution: {integrity: sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==} - brace-expansion@5.0.7: - resolution: {integrity: sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==} - engines: {node: 18 || 20 || >=22} + brace-expansion@5.0.9: + resolution: {integrity: sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==} + engines: {node: 20 || >=22} braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} @@ -1589,9 +1591,9 @@ packages: deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} - deepmerge-ts@7.1.5: - resolution: {integrity: sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==} - engines: {node: '>=16.0.0'} + deepmerge-ts@8.0.2: + resolution: {integrity: sha512-uqbvqLUMrc6p0MO+WBRtTxY55hmyh94WRwI5a++PZe54X+bfVh59FSN7uWCBCW1CCVjzjnrwzfI8zidE2obMMw==} + engines: {node: '>=16.9.0'} deepmerge@4.3.1: resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} @@ -1760,6 +1762,7 @@ packages: eslint@9.39.5: resolution: {integrity: sha512-DgZS62aPLXKlnxILS/AYCoRvHaZeXceIzlXPkkGGzJWSow1aEk0lbTlxUSlyjC8jcaKxAdOnTDz+o1JFSBsyjw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. hasBin: true peerDependencies: jiti: '*' @@ -1841,8 +1844,8 @@ packages: fast-safe-stringify@2.1.1: resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} - fast-uri@3.1.4: - resolution: {integrity: sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw==} + fast-uri@3.1.6: + resolution: {integrity: sha512-7Ical1vFEMr0onbVzEDIreM22I4khW+fzyQPwvAFWBp1iwdshSZRsL4jjRvPG9JP1uiqMHRto+YU6R2/CzDz5Q==} fdir@6.5.0: resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} @@ -2110,8 +2113,8 @@ packages: resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} hasBin: true - hono@4.12.31: - resolution: {integrity: sha512-zJIHFrl6bq3RDd2YusFNCDlM8qUprxKswyi/OPzPyzKDdyBXDqWx8bZlZ7R+saTdSTatUmb3O7K4SspGPaEOQg==} + hono@4.12.34: + resolution: {integrity: sha512-GqXJqY/xJkJmuloTrnV1ZEXG3fqte+VjkUqoRNZXcrUidiUOP4fMSIHHY4tsqZBK++kVyWmt/AAfSUuy57/eSA==} engines: {node: '>=16.9.0'} html-escaper@2.0.2: @@ -2201,8 +2204,8 @@ packages: inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} - ip-address@10.2.0: - resolution: {integrity: sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==} + ip-address@10.3.1: + resolution: {integrity: sha512-1e9d3kb97NHJTIJDZW9rKqW2h6+dFa50Dy0fpPSMQp2ADje5gvKsXmdiK6dwY5t76TaTt5+P5N1Y/LoToIxP6g==} engines: {node: '>= 12'} ipaddr.js@1.9.1: @@ -2333,12 +2336,12 @@ packages: js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - js-yaml@3.15.0: - resolution: {integrity: sha512-ttBQIIQPDeLjpPOohtUdXuXUVoA2uIB6fEH9HyJ7234s5mBJ5wTx20njxplLZQgLaOfpmPQA7X2t5AX6tIPbog==} + js-yaml@3.15.1: + resolution: {integrity: sha512-S99WuO3HlhO3XN41EtYUNl9zzXjoJx7QvmipxsJVxtCBT0YHEFy+iOJhjSvrmV12nYhWpZaM8lPHkJm0yUMbag==} hasBin: true - js-yaml@4.3.0: - resolution: {integrity: sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==} + js-yaml@4.3.1: + resolution: {integrity: sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==} hasBin: true jsbn@0.1.1: @@ -3638,8 +3641,8 @@ packages: v8-compile-cache-lib@3.0.1: resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} - valibot@1.2.0: - resolution: {integrity: sha512-mm1rxUsmOxzrwnX5arGS+U4T25RdvpPjPN4yR0u9pUBov9+zGVtO84tif1eY4r6zWxVxu3KzIyknJy3rxfRZZg==} + valibot@1.4.2: + resolution: {integrity: sha512-gjdCvJ6d3RyHAneqxMYMW9QMCwYMb3jpOO0IyHZV1bnRHFBHrX3VkIILt5XYR0WhwHiH7Mty8ovuPZ/O3gamrg==} peerDependencies: typescript: '>=5' peerDependenciesMeta: @@ -4021,7 +4024,7 @@ snapshots: globals: 14.0.0 ignore: 5.3.2 import-fresh: 3.3.1 - js-yaml: 4.3.0 + js-yaml: 4.3.1 minimatch: 3.1.5 strip-json-comments: 3.1.1 transitivePeerDependencies: @@ -4054,9 +4057,9 @@ snapshots: dependencies: '@hapi/hoek': 11.0.7 - '@hono/node-server@2.0.11(hono@4.12.31)': + '@hono/node-server@2.0.11(hono@4.12.34)': dependencies: - hono: 4.12.31 + hono: 4.12.34 '@humanfs/core@0.19.2': dependencies: @@ -4228,7 +4231,7 @@ snapshots: camelcase: 5.3.1 find-up: 4.1.0 get-package-type: 0.1.0 - js-yaml: 3.15.0 + js-yaml: 3.15.1 resolve-from: 5.0.0 '@istanbuljs/schema@0.1.6': {} @@ -4417,7 +4420,7 @@ snapshots: '@prisma/config@7.8.0': dependencies: c12: 3.3.4 - deepmerge-ts: 7.1.5 + deepmerge-ts: 8.0.2 effect: 3.20.0 empathic: 2.0.0 transitivePeerDependencies: @@ -4432,19 +4435,19 @@ snapshots: '@electric-sql/pglite': 0.4.1 '@electric-sql/pglite-socket': 0.1.1(@electric-sql/pglite@0.4.1) '@electric-sql/pglite-tools': 0.3.1(@electric-sql/pglite@0.4.1) - '@hono/node-server': 2.0.11(hono@4.12.31) + '@hono/node-server': 2.0.11(hono@4.12.34) '@prisma/get-platform': 7.2.0 '@prisma/query-plan-executor': 7.2.0 '@prisma/streams-local': 0.1.2 foreground-child: 3.3.1 get-port-please: 3.2.0 - hono: 4.12.31 + hono: 4.12.34 http-status-codes: 2.3.0 pathe: 2.0.3 proper-lockfile: 4.1.2 remeda: 2.33.4 std-env: 3.10.0 - valibot: 1.2.0(typescript@5.9.3) + valibot: 1.4.2(typescript@5.9.3) zeptomatch: 2.1.0 transitivePeerDependencies: - typescript @@ -4903,14 +4906,14 @@ snapshots: ajv@8.18.0: dependencies: fast-deep-equal: 3.1.3 - fast-uri: 3.1.4 + fast-uri: 3.1.6 json-schema-traverse: 1.0.0 require-from-string: 2.0.2 ajv@8.20.0: dependencies: fast-deep-equal: 3.1.3 - fast-uri: 3.1.4 + fast-uri: 3.1.6 json-schema-traverse: 1.0.0 require-from-string: 2.0.2 @@ -5013,8 +5016,6 @@ snapshots: bluebird@2.11.0: {} - bluebird@3.7.2: {} - body-parser@1.20.6(supports-color@5.5.0): dependencies: bytes: 3.1.2 @@ -5046,16 +5047,16 @@ snapshots: transitivePeerDependencies: - supports-color - brace-expansion@1.1.16: + brace-expansion@1.1.18: dependencies: balanced-match: 1.0.2 concat-map: 0.0.1 - brace-expansion@2.1.2: + brace-expansion@2.1.4: dependencies: balanced-match: 1.0.2 - brace-expansion@5.0.7: + brace-expansion@5.0.9: dependencies: balanced-match: 4.0.4 @@ -5314,7 +5315,7 @@ snapshots: cosmiconfig@8.3.6(typescript@5.9.3): dependencies: import-fresh: 3.3.1 - js-yaml: 4.3.0 + js-yaml: 4.3.1 parse-json: 5.2.0 path-type: 4.0.0 optionalDependencies: @@ -5362,7 +5363,7 @@ snapshots: deep-is@0.1.4: {} - deepmerge-ts@7.1.5: {} + deepmerge-ts@8.0.2: {} deepmerge@4.3.1: {} @@ -5659,7 +5660,7 @@ snapshots: fast-safe-stringify@2.1.1: {} - fast-uri@3.1.4: {} + fast-uri@3.1.6: {} fdir@6.5.0(picomatch@4.0.5): optionalDependencies: @@ -5942,7 +5943,7 @@ snapshots: he@1.2.0: {} - hono@4.12.31: {} + hono@4.12.34: {} html-escaper@2.0.2: {} @@ -6028,7 +6029,7 @@ snapshots: inherits@2.0.4: {} - ip-address@10.2.0: {} + ip-address@10.3.1: {} ipaddr.js@1.9.1: {} @@ -6150,12 +6151,12 @@ snapshots: js-tokens@4.0.0: {} - js-yaml@3.15.0: + js-yaml@3.15.1: dependencies: argparse: 1.0.10 esprima: 4.0.1 - js-yaml@4.3.0: + js-yaml@4.3.1: dependencies: argparse: 2.0.1 @@ -6375,19 +6376,19 @@ snapshots: minimatch@10.2.5: dependencies: - brace-expansion: 5.0.7 + brace-expansion: 5.0.9 minimatch@3.1.4: dependencies: - brace-expansion: 1.1.16 + brace-expansion: 1.1.18 minimatch@3.1.5: dependencies: - brace-expansion: 1.1.16 + brace-expansion: 1.1.18 minimatch@9.0.9: dependencies: - brace-expansion: 2.1.2 + brace-expansion: 2.1.4 minimist@1.2.8: {} @@ -6411,7 +6412,7 @@ snapshots: glob: 10.5.0 he: 1.2.0 is-path-inside: 3.0.3 - js-yaml: 4.3.0 + js-yaml: 4.3.1 log-symbols: 4.1.0 minimatch: 9.0.9 ms: 2.1.3 @@ -7224,7 +7225,7 @@ snapshots: socks@2.8.9: dependencies: - ip-address: 10.2.0 + ip-address: 10.3.1 smart-buffer: 4.2.0 source-map-support@0.5.21: @@ -7548,7 +7549,7 @@ snapshots: v8-compile-cache-lib@3.0.1: {} - valibot@1.2.0(typescript@5.9.3): + valibot@1.4.2(typescript@5.9.3): optionalDependencies: typescript: 5.9.3 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index ad34206..69f24f7 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -4,13 +4,20 @@ overrides: 'ajv@>=6.0.0 <6.14.0': 6.15.0 'axios@<1.18.1': 1.18.1 'body-parser@<1.20.6': 1.20.6 - 'brace-expansion@<1.1.16': 1.1.16 + 'brace-expansion@<1.1.18': 1.1.18 + 'brace-expansion@>=2.0.0 <2.1.4': 2.1.4 + 'brace-expansion@>=3.0.0 <3.0.6': 3.0.6 + 'brace-expansion@>=4.0.0 <5.0.9': 5.0.9 + 'deepmerge-ts@<8.0.0': 8.0.2 'diff@>=6.0.0 <8.0.3': 8.0.3 - 'fast-uri@>=3.0.0 <3.1.4': 3.1.4 + 'fast-uri@>=3.0.0 <3.1.6': 3.1.6 'flatted@<3.4.2': 3.4.2 'handlebars@<4.7.9': 4.7.9 'jose@>=3.0.0 <4.15.5': 4.15.5 - 'js-yaml@>=3.0.0 <3.15.0': 3.15.0 + 'hono@<4.12.34': 4.12.34 + 'ip-address@<=10.3.0': 10.3.1 + 'js-yaml@>=3.0.0 <3.15.1': 3.15.1 + 'js-yaml@>=4.0.0 <4.3.1': 4.3.1 'lodash@<4.18.1': 4.18.1 'node-forge@<1.4.0': 1.4.0 'picomatch@<2.3.2': 2.3.2 @@ -22,6 +29,7 @@ overrides: 'minimatch@3.1.2': 3.1.4 'path-to-regexp@0.1.12': 0.1.13 'qs@<6.15.2': 6.15.2 + 'valibot@<=1.4.1': 1.4.2 # The published Bus API wrapper intentionally resolves Topcoder's core library # from its pinned GitHub archive rather than from the npm registry. diff --git a/src/common/helper.ts b/src/common/helper.ts index 9d9c138..4687292 100644 --- a/src/common/helper.ts +++ b/src/common/helper.ts @@ -25,6 +25,63 @@ const prisma = prismaClients.getClient() const prismaMember = prismaClients.getMemberClient() const prismaChallenge = prismaClients.getChallengeClient() +const OUTBOUND_APIS = Object.freeze({ + challengePhases: config.CHALLENGE_PHASES_API_URL, + challenges: config.CHALLENGE_API_URL, + groups: config.GROUPS_API_URL, + members: config.MEMBER_API_URL, + resources: `${config.API_BASE_URL}/${config.API_VERSION}/resources`, + submissions: config.SUBMISSIONS_API_URL, + terms: config.TERMS_API_URL +}) + +/** + * Build a URL for one explicitly allowlisted Topcoder API. + * + * Callers select a server-controlled API name and supply path components + * separately. Each component is encoded only after URL delimiters, control + * characters, traversal markers, and pre-encoded input have been rejected. + * This keeps untrusted resource ids and handles out of the target origin and + * prevents a request from escaping the configured API's base path. + * + * @param apiName Allowlisted API name from the internal `OUTBOUND_APIS` map. + * @param pathSegments Optional unencoded path components to append. + * @returns A canonical absolute HTTP(S) URL for the selected API. + * @throws {Error} When the API name is unknown, its configured base URL is + * unsafe, or a path component can change URL structure. + */ +function buildOutboundApiUrl (apiName, pathSegments: any[] = []) { + if (!Object.prototype.hasOwnProperty.call(OUTBOUND_APIS, apiName)) { + throw new Error(`Unknown outbound API: ${apiName}`) + } + if (!Array.isArray(pathSegments)) { + throw new Error('Outbound API path segments must be an array') + } + + const target = new URL(OUTBOUND_APIS[apiName]) + if (!['http:', 'https:'].includes(target.protocol) || target.username || target.password || target.search || target.hash) { + throw new Error(`Unsafe configured URL for outbound API: ${apiName}`) + } + + target.pathname = target.pathname.replace(/\/+$/, '') + for (const value of pathSegments) { + if (!['string', 'number', 'bigint'].includes(typeof value)) { + throw new Error('Outbound API path segments must be strings or numbers') + } + const segment = String(value) + const hasControlCharacter = Array.from(segment).some((character) => { + const codePoint = character.codePointAt(0) + return codePoint <= 31 || codePoint === 127 + }) + if (!segment || segment === '.' || segment === '..' || /[/\\?#%]/.test(segment) || hasControlCharacter) { + throw new Error('Unsafe outbound API path segment') + } + target.pathname += `/${encodeURIComponent(segment)}` + } + + return target.toString() +} + /** * Determine whether an error is one of the API's custom HTTP errors. * @@ -298,7 +355,7 @@ async function getChallengeById (challengeId, options: { includeDetails?: boolea const { includeDetails = false } = options if (includeDetails) { - const response = await getRequest(`${config.CHALLENGE_API_URL}/${challengeId}`) + const response = await getRequest('challenges', [challengeId]) return _.get(response, 'body', null) } @@ -389,7 +446,7 @@ async function getMemberDetailsByHandleFromV3Members (handle) { let memberId let email try { - const res = await getRequest(`${config.MEMBER_API_URL}/${handle}`) + const res = await getRequest('members', [handle]) if (_.get(res, 'body.userId')) { memberId = String(res.body.userId) } @@ -426,7 +483,7 @@ async function getMemberDetailsByIdFromMemberApi (userId) { let handle try { logger.warn(`getMemberDetailsByIdFromMemberApi ${handle} from v6`) - const res = await getRequest(`${config.MEMBER_API_URL}?userId=${userId}`) + const res = await getRequest('members', [], { userId }) if (_.get(res, 'body[0].userId')) { memberId = String(res.body[0].userId) } @@ -498,16 +555,22 @@ async function validateDuplicate (modelName, queryParams, errorMessage) { /** * Send an authenticated GET request with the service's M2M token. * - * @param url Target URL. + * The API host and base path come only from the internal allowlist, path + * components are structurally validated, and redirects are not followed. + * + * @param apiName Allowlisted API name. + * @param pathSegments Optional unencoded path components. * @param query Optional query parameters. * @returns The SuperAgent response. - * @throws M2M authentication, network, or HTTP response errors. + * @throws URL validation, M2M authentication, network, or HTTP response errors. */ -async function getRequest (url, query?: any) { +async function getRequest (apiName, pathSegments: any[] = [], query?: any) { + const url = buildOutboundApiUrl(apiName, pathSegments) const m2mToken = await m2m.getMachineToken(config.AUTH0_CLIENT_ID, config.AUTH0_CLIENT_SECRET) logger.debug(`GET ${url} with query ${JSON.stringify(query)}`) return request .get(url) + .redirects(0) .set('Authorization', `Bearer ${m2mToken}`) .set('Content-Type', 'application/json') .set('Accept', 'application/json') @@ -517,20 +580,24 @@ async function getRequest (url, query?: any) { /** * Send an authenticated POST request with the service's M2M token. * - * Errors are logged to the console and rethrown unchanged to preserve the - * existing API integration behavior. + * The API host and base path come only from the internal allowlist, path + * components are structurally validated, and redirects are not followed. + * Errors are logged and rethrown unchanged to preserve integration behavior. * - * @param url Target URL. + * @param apiName Allowlisted API name. + * @param pathSegments Optional unencoded path components. * @param data Optional request body. * @returns The SuperAgent response. - * @throws M2M authentication, network, or HTTP response errors. + * @throws URL validation, M2M authentication, network, or HTTP response errors. */ -async function postRequest (url, data?: any) { +async function postRequest (apiName, pathSegments: any[] = [], data?: any) { try { + const url = buildOutboundApiUrl(apiName, pathSegments) const m2mToken = await m2m.getMachineToken(config.AUTH0_CLIENT_ID, config.AUTH0_CLIENT_SECRET) const res = await request .post(url) + .redirects(0) .set('Authorization', `Bearer ${m2mToken}`) .set('Content-Type', 'application/json') .set('Accept', 'application/json') @@ -593,18 +660,18 @@ function setResHeaders (req, res, result) { /** * Fetch and concatenate all pages from a Topcoder API endpoint. * - * @param url Endpoint URL. + * @param apiName Allowlisted API name. * @param query Optional query parameters excluding `page` and `perPage`. * @returns All array records returned before an empty page or reported total. - * @throws M2M authentication, network, or HTTP response errors. + * @throws URL validation, M2M authentication, network, or HTTP response errors. */ -async function getAllPages (url, query?: any) { +async function getAllPages (apiName, query?: any) { const perPage = 100 let page = 1 let result = [] for (;;) { // get current page data - const res = await getRequest(url, _.assignIn({ page, perPage }, query || {})) + const res = await getRequest(apiName, [], _.assignIn({ page, perPage }, query || {})) if (!_.isArray(res.body) || res.body.length === 0) { break } @@ -629,8 +696,7 @@ async function getAllPages (url, query?: any) { * @throws M2M authentication, network, or Groups API errors. */ async function getUserGroupIds (userId) { - const url = config.GROUPS_API_URL + `/memberGroups/${userId}` - const response = await getRequest(url, { uuid: true }) + const response = await getRequest('groups', ['memberGroups', userId], { uuid: true }) return response.body } @@ -677,7 +743,7 @@ async function checkAgreedTerms (userId, terms) { const unAgreedTerms = [] const missingTerms = [] for (const term of terms) { - const res = await getRequest(`${config.TERMS_API_URL}/${term.id}`, { userId }) + const res = await getRequest('terms', [term.id], { userId }) if (!_.get(res, 'body.agreed', false)) { unAgreedTerms.push(_.get(res, 'body.title', term)) missingTerms.push({ @@ -714,7 +780,7 @@ async function advanceChallengePhase (challengeId, phase, operation, numAttempts try { console.log('Initiating advance phase:', challengeId, phase, operation) - const response = await postRequest(`${config.CHALLENGE_API_URL}/${challengeId}/advance-phase`, { + const response = await postRequest('challenges', [challengeId, 'advance-phase'], { phase, operation }) @@ -751,6 +817,7 @@ module.exports = { getById, update, validateDuplicate, + buildOutboundApiUrl, getRequest, postEvent, isCustomError, diff --git a/src/services/ResourceRolePhaseDependencyService.ts b/src/services/ResourceRolePhaseDependencyService.ts index dcbf8c6..7422446 100644 --- a/src/services/ResourceRolePhaseDependencyService.ts +++ b/src/services/ResourceRolePhaseDependencyService.ts @@ -57,7 +57,7 @@ getDependencies.schema = { */ async function validateDependency (data) { // validate phaseId - const phases = await helper.getAllPages(config.CHALLENGE_PHASES_API_URL) + const phases = await helper.getAllPages('challengePhases') if (!_.find(phases, (p) => p.id === data.phaseId)) { throw new errors.NotFoundError(`Not found phase id: ${data.phaseId}`) } diff --git a/src/services/ResourceService.ts b/src/services/ResourceService.ts index d8d0a32..6efafd3 100644 --- a/src/services/ResourceService.ts +++ b/src/services/ResourceService.ts @@ -460,7 +460,7 @@ async function init (currentUser, challengeId, resource, isCreated?: boolean) { } // check if the resource is reviewer role and has already made a submission in the challenge if (isCreated && (resource.roleId === config.REVIEWER_RESOURCE_ROLE_ID || resource.roleId === config.ITERATIVE_REVIEWER_RESOURCE_ROLE_ID)) { - const submissionsRes = await helper.getRequest(`${config.SUBMISSIONS_API_URL}`, { challengeId: challengeId, perPage: 100, memberId: memberId }) + const submissionsRes = await helper.getRequest('submissions', [], { challengeId: challengeId, perPage: 100, memberId: memberId }) const submissions = submissionsRes.body logger.debug(`Submissions made by member ${memberId} in challenge ${challengeId}: ${JSON.stringify(submissions)}`) if (submissions.meta.totalCount !== 0) { diff --git a/test/postman/clearTestData.js b/test/postman/clearTestData.js index 95424e0..9ec49cd 100644 --- a/test/postman/clearTestData.js +++ b/test/postman/clearTestData.js @@ -3,7 +3,6 @@ */ const logger = require('../../src/common/logger') const helper = require('../../src/common/helper') -const config = require('config') logger.info('Clear the Postman test data.') @@ -12,7 +11,7 @@ logger.info('Clear the Postman test data.') * @returns {Promise} */ const clearTestData = async () => { - await helper.postRequest(`${config.API_BASE_URL}/${config.API_VERSION}/resources/internal/jobs/clean`) + await helper.postRequest('resources', ['internal', 'jobs', 'clean']) } clearTestData().then(() => { diff --git a/test/postman/newman.js b/test/postman/newman.js index faf53e6..5db8835 100644 --- a/test/postman/newman.js +++ b/test/postman/newman.js @@ -523,7 +523,7 @@ const requests = [ */ async function clearTestData () { logger.info('Clear the Postman test data.') - await helper.postRequest(`${config.API_BASE_URL}/${config.API_VERSION}/resources/internal/jobs/clean`) + await helper.postRequest('resources', ['internal', 'jobs', 'clean']) logger.info('Finished clear the Postman test data.') } diff --git a/test/unit/outbound-url.test.ts b/test/unit/outbound-url.test.ts new file mode 100644 index 0000000..71be32b --- /dev/null +++ b/test/unit/outbound-url.test.ts @@ -0,0 +1,43 @@ +/** + * Security regression tests for outbound API URL construction. + */ + +const assert = require('node:assert/strict') +const config = require('config') +const helper = require('../../src/common/helper') + +describe('outbound API URL security', () => { + it('builds a canonical URL only from an allowlisted API and safe segments', () => { + const challengeId = '12345678-1234-1234-1234-123456789abc' + assert.equal( + helper.buildOutboundApiUrl('challenges', [challengeId]), + `${config.CHALLENGE_API_URL}/${challengeId}` + ) + }) + + it('encodes safe non-ASCII path data without changing the target origin', () => { + const target = new URL(helper.buildOutboundApiUrl('members', ['déveloper'])) + const configured = new URL(config.MEMBER_API_URL) + assert.equal(target.origin, configured.origin) + assert.equal(target.pathname, `${configured.pathname}/d%C3%A9veloper`) + }) + + it('rejects an arbitrary target instead of treating it as an API name', () => { + assert.throws( + () => helper.buildOutboundApiUrl('http://169.254.169.254/latest/meta-data'), + /Unknown outbound API/ + ) + }) + + it('rejects arbitrary GET and POST targets before authentication or I/O', async () => { + const metadataUrl = 'http://169.254.169.254/latest/meta-data' + await assert.rejects(helper.getRequest(metadataUrl), /Unknown outbound API/) + await assert.rejects(helper.postRequest(metadataUrl), /Unknown outbound API/) + }) + + for (const segment of ['..', '../admin', 'safe/../../admin', 'safe\\admin', 'safe?admin', 'safe#admin', 'safe%2fadmin', 'safe\u0000admin']) { + it(`rejects a path component that can alter URL structure: ${JSON.stringify(segment)}`, () => { + assert.throws(() => helper.buildOutboundApiUrl('challenges', [segment]), /Unsafe outbound API path segment/) + }) + } +}) From 48be080b5987e89cbdb0c2beb0ed29c529e96653 Mon Sep 17 00:00:00 2001 From: jmgasper Date: Tue, 1 Sep 2026 10:19:50 +1000 Subject: [PATCH 2/2] docs: remove committed JWT examples --- ReadMe.md | 2 ++ ...lenge-resource-api.postman_environment.json | 18 +++++++++--------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/ReadMe.md b/ReadMe.md index 07e97db..6e91039 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -190,6 +190,8 @@ pnpm run test:newman:clear The Postman entrypoint uses Newman 6 directly and retains the existing token coverage, per-folder execution order, cleanup, and `newman/reports.html` CircleCI artifact. The `html` reporter name selects that local aggregate report; the supported `cli`, `json`, and `junit` reporter names are passed through to Newman. +The exported manual Postman environment intentionally leaves bearer-token values blank. Populate those values only in a local, uncommitted environment before using the collection; never commit live or example JWTs. + CircleCI performs the Docker build and deployment using the existing `APPNAME`, `DEPLOY_ENV`, and parameter-store paths. The automated-test workflow uses Node 26.5.0, pnpm 11.15.1, and the same deployment environment names. See `Verification.md` for the regression checklist. diff --git a/docs/topcoder-challenge-resource-api.postman_environment.json b/docs/topcoder-challenge-resource-api.postman_environment.json index 3d87caf..5354f78 100644 --- a/docs/topcoder-challenge-resource-api.postman_environment.json +++ b/docs/topcoder-challenge-resource-api.postman_environment.json @@ -19,22 +19,22 @@ }, { "key": "user1_token", - "value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLWRldi5jb20iLCJoYW5kbGUiOiJkZW5pcyIsImV4cCI6MTY4MjgwMDE2OSwidXNlcklkIjoiMjUxMjgwIiwiaWF0IjoxNTQ5Nzk5NTY5LCJlbWFpbCI6ImVtYWlsQGRvbWFpbi5jb20ueiIsImp0aSI6IjljNDUxMWM1LWMxNjUtNGExYi04OTllLWI2NWFkMGUwMmI1NSJ9.BCF6xW3aQfHDDFbgGvvOKzvwEXVLWGf-TgF5JrtM9Tg", + "value": "", "enabled": true }, { "key": "user2_token", - "value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiY29waWxvdCJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoiaG9ob3NreSIsImV4cCI6MTY4MTc5MjM3MCwidXNlcklkIjoiMTYwOTY4MjMiLCJpYXQiOjE1NDk3OTE3NzAsImVtYWlsIjoiZW1haWxAZG9tYWluLmNvbS56IiwianRpIjoiZjFlNjEzYmUtZDViOS00MjMxLWJhYWUtZWU5ZjJkMjI3MjM0In0.YfzhT9rLFK6U2suPdiCSx_n7Ce6ZIt_oLZ0BcvfMqAk", + "value": "", "enabled": true }, { "key": "admin_token", - "value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiQ29ubmVjdCBTdXBwb3J0IiwiYWRtaW5pc3RyYXRvciIsInRlc3RSb2xlIiwiYWFhIiwidG9ueV90ZXN0XzEiLCJDb25uZWN0IE1hbmFnZXIiLCJDb25uZWN0IEFkbWluIiwiY29waWxvdCIsIkNvbm5lY3QgQ29waWxvdCBNYW5hZ2VyIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLWRldi5jb20iLCJoYW5kbGUiOiJUb255SiIsImV4cCI6MTY4MTA1MjIxMSwidXNlcklkIjoiODU0Nzg5OSIsImlhdCI6MTU0OTc5MTYxMSwiZW1haWwiOiJ0amVmdHMrZml4QHRvcGNvZGVyLmNvbSIsImp0aSI6ImY5NGQxZTI2LTNkMGUtNDZjYS04MTE1LTg3NTQ1NDRhMDhmMSJ9.ONh2RSE5NeJzyjBD68TSUdNFMs5v5BXtga4LchR2x3s", + "value": "", "enabled": true }, { "key": "m2m_all_token", - "value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwczovL3RvcGNvZGVyLWRldi5hdXRoMC5jb20vIiwic3ViIjoiZW5qdzE4MTBlRHozWFR3U08yUm4yWTljUVRyc3BuM0JAY2xpZW50cyIsImF1ZCI6Imh0dHBzOi8vbTJtLnRvcGNvZGVyLWRldi5jb20vIiwiaWF0IjoxNTUwOTA2Mzg4LCJleHAiOjE2ODA5OTI3ODgsImF6cCI6ImVuancxODEwZUR6M1hUd1NPMlJuMlk5Y1FUcnNwbjNCIiwic2NvcGUiOiJhbGw6cmVzb3VyY2VzIGFsbDpyZXNvdXJjZV9yb2xlcyIsImd0eSI6ImNsaWVudC1jcmVkZW50aWFscyJ9.UhuqMGrrNzj-VQz2-BogN28YZTE9uauIfdP7EOhrKB0", + "value": "", "enabled": true }, { @@ -54,17 +54,17 @@ }, { "key": "expire_token", - "value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiQ29ubmVjdCBTdXBwb3J0IiwiYWRtaW5pc3RyYXRvciIsInRlc3RSb2xlIiwiYWFhIiwidG9ueV90ZXN0XzEiLCJDb25uZWN0IE1hbmFnZXIiLCJDb25uZWN0IEFkbWluIiwiY29waWxvdCIsIkNvbm5lY3QgQ29waWxvdCBNYW5hZ2VyIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLWRldi5jb20iLCJoYW5kbGUiOiJUb255SiIsImV4cCI6MTU1MTA2MzIxMSwidXNlcklkIjoiODU0Nzg5OSIsImlhdCI6MTU1MTA1MzIxMSwiZW1haWwiOiJ0amVmdHMrZml4QHRvcGNvZGVyLmNvbSIsImp0aSI6ImY5NGQxZTI2LTNkMGUtNDZjYS04MTE1LTg3NTQ1NDRhMDhmMSJ9.97-pjuSGGqDAqK2FG2yi_3nmzB7ZMXQwtG0bi8_PlKk", + "value": "", "enabled": true }, { "key": "m2m_read_token", - "value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwczovL3RvcGNvZGVyLWRldi5hdXRoMC5jb20vIiwic3ViIjoiZW5qdzE4MTBlRHozWFR3U08yUm4yWTljUVRyc3BuM0JAY2xpZW50cyIsImF1ZCI6Imh0dHBzOi8vbTJtLnRvcGNvZGVyLWRldi5jb20vIiwiaWF0IjoxNTUwOTA2Mzg4LCJleHAiOjE2ODA5OTI3ODgsImF6cCI6ImVuancxODEwZUR6M1hUd1NPMlJuMlk5Y1FUcnNwbjNCIiwic2NvcGUiOiJyZWFkOnJlc291cmNlcyIsImd0eSI6ImNsaWVudC1jcmVkZW50aWFscyJ9.1EdekIx1jU2lDSIXlMFCPmXkzLWQ_ZvxPuutQWuTKu0", + "value": "", "enabled": true }, { "key": "m2m_modify_token", - "value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwczovL3RvcGNvZGVyLWRldi5hdXRoMC5jb20vIiwic3ViIjoiZW5qdzE4MTBlRHozWFR3U08yUm4yWTljUVRyc3BuM0JAY2xpZW50cyIsImF1ZCI6Imh0dHBzOi8vbTJtLnRvcGNvZGVyLWRldi5jb20vIiwiaWF0IjoxNTUwOTA2Mzg4LCJleHAiOjE2ODA5OTI3ODgsImF6cCI6ImVuancxODEwZUR6M1hUd1NPMlJuMlk5Y1FUcnNwbjNCIiwic2NvcGUiOiJjcmVhdGU6cmVzb3VyY2VzIGRlbGV0ZTpyZXNvdXJjZXMgdXBkYXRlOnJlc291cmNlcyIsImd0eSI6ImNsaWVudC1jcmVkZW50aWFscyJ9.f_3FLT3jMTnKwGIg8fcFP2lmShy5S9d5IvLNbXSj2Eo", + "value": "", "enabled": true }, { @@ -84,7 +84,7 @@ }, { "key": "user3_token", - "value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLWRldi5jb20iLCJoYW5kbGUiOiJsYXJzMjUyMCIsImV4cCI6MTY4MjgwMDE2OSwidXNlcklkIjoiMjg3MTMxIiwiaWF0IjoxNTQ5Nzk5NTY5LCJlbWFpbCI6ImVtYWlsQGRvbWFpbi5jb20ueiIsImp0aSI6IjljNDUxMWM1LWMxNjUtNGExYi04OTllLWI2NWFkMGUwMmI1NSJ9.QIXF4-qSkH7n-i0_4mzF3peFTZ5PjaSYHjm9EjhTfS4", + "value": "", "enabled": true }, { @@ -118,4 +118,4 @@ "_postman_variable_scope": "environment", "_postman_exported_at": "2020-03-20T09:04:47.996Z", "_postman_exported_using": "Postman/7.13.0" -} \ No newline at end of file +}