From 53424c311e0e9481ad25a1e25c3d9e27131c836e Mon Sep 17 00:00:00 2001 From: Matt Kane Date: Tue, 27 Jul 2021 12:44:17 +0100 Subject: [PATCH 1/8] feat: add support for secrets 0.7.3-handle-secrets.0 --- .eslintrc.js | 21 +- package-lock.json | 394 +++++++++++++++++++++++++++++++++++++- package.json | 3 +- src/function/handler.d.ts | 4 +- src/lib/builder.d.ts | 3 +- src/lib/secrets.d.ts | 23 +++ src/lib/secrets.js | 34 ++++ src/lib/services.json | 17 ++ src/main.d.ts | 1 + src/main.js | 3 +- 10 files changed, 495 insertions(+), 8 deletions(-) create mode 100644 src/lib/secrets.d.ts create mode 100644 src/lib/secrets.js create mode 100644 src/lib/services.json diff --git a/.eslintrc.js b/.eslintrc.js index 44a088f0..53651f61 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -4,7 +4,15 @@ const { overrides } = require('@netlify/eslint-config-node') module.exports = { extends: '@netlify/eslint-config-node', - rules: {}, + // TODO: remove after https://github.com/netlify/eslint-config-node/pull/230 is merged and released + rules: { + 'node/no-unsupported-features/es-syntax': [ + 'error', + { + ignores: ['modules'], + }, + ], + }, overrides: [ ...overrides, { @@ -14,5 +22,16 @@ module.exports = { 'promise/prefer-await-to-callbacks': 'off', }, }, + // TODO: remove after https://github.com/netlify/eslint-config-node/pull/230 is merged and released + { + files: ['*.ts'], + extends: ['plugin:@typescript-eslint/recommended', 'plugin:import/typescript'], + }, ], + settings: { + // TODO: remove after https://github.com/netlify/eslint-config-node/pull/230 is merged and released + 'import/parsers': { + '@typescript-eslint/parser': ['.ts', '.tsx'], + }, + }, } diff --git a/package-lock.json b/package-lock.json index 5d81b450..f58bdfd2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@netlify/functions", - "version": "0.7.2", + "version": "0.7.3-handle-secrets.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@netlify/functions", - "version": "0.7.2", + "version": "0.7.3-handle-secrets.0", "license": "MIT", "dependencies": { "is-promise": "^4.0.0" @@ -15,6 +15,7 @@ "@commitlint/cli": "^13.0.0", "@commitlint/config-conventional": "^13.0.0", "@netlify/eslint-config-node": "^3.1.9", + "@typescript-eslint/eslint-plugin": "^4.28.5", "ava": "^2.4.0", "husky": "^4.3.8", "nyc": "^15.0.0" @@ -1182,6 +1183,12 @@ "@types/node": "*" } }, + "node_modules/@types/json-schema": { + "version": "7.0.8", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.8.tgz", + "integrity": "sha512-YSBPTLTVm2e2OoQIDYx8HaeWJ5tTToLH67kXR7zYNGupXMEHa2++G8k+DczX2cFVgalypqtyZIcU19AFcmOpmg==", + "dev": true + }, "node_modules/@types/mdast": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.7.tgz", @@ -1227,6 +1234,210 @@ "integrity": "sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==", "dev": true }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "4.28.5", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.28.5.tgz", + "integrity": "sha512-m31cPEnbuCqXtEZQJOXAHsHvtoDi9OVaeL5wZnO2KZTnkvELk+u6J6jHg+NzvWQxk+87Zjbc4lJS4NHmgImz6Q==", + "dev": true, + "dependencies": { + "@typescript-eslint/experimental-utils": "4.28.5", + "@typescript-eslint/scope-manager": "4.28.5", + "debug": "^4.3.1", + "functional-red-black-tree": "^1.0.1", + "regexpp": "^3.1.0", + "semver": "^7.3.5", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^4.0.0", + "eslint": "^5.0.0 || ^6.0.0 || ^7.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/experimental-utils": { + "version": "4.28.5", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.28.5.tgz", + "integrity": "sha512-bGPLCOJAa+j49hsynTaAtQIWg6uZd8VLiPcyDe4QPULsvQwLHGLSGKKcBN8/lBxIX14F74UEMK2zNDI8r0okwA==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.7", + "@typescript-eslint/scope-manager": "4.28.5", + "@typescript-eslint/types": "4.28.5", + "@typescript-eslint/typescript-estree": "4.28.5", + "eslint-scope": "^5.1.1", + "eslint-utils": "^3.0.0" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "*" + } + }, + "node_modules/@typescript-eslint/experimental-utils/node_modules/eslint-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", + "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^2.0.0" + }, + "engines": { + "node": "^10.0.0 || ^12.0.0 || >= 14.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=5" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "4.28.5", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.28.5.tgz", + "integrity": "sha512-NPCOGhTnkXGMqTznqgVbA5LqVsnw+i3+XA1UKLnAb+MG1Y1rP4ZSK9GX0kJBmAZTMIktf+dTwXToT6kFwyimbw==", + "dev": true, + "peer": true, + "dependencies": { + "@typescript-eslint/scope-manager": "4.28.5", + "@typescript-eslint/types": "4.28.5", + "@typescript-eslint/typescript-estree": "4.28.5", + "debug": "^4.3.1" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^5.0.0 || ^6.0.0 || ^7.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "4.28.5", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.28.5.tgz", + "integrity": "sha512-PHLq6n9nTMrLYcVcIZ7v0VY1X7dK309NM8ya9oL/yG8syFINIMHxyr2GzGoBYUdv3NUfCOqtuqps0ZmcgnZTfQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "4.28.5", + "@typescript-eslint/visitor-keys": "4.28.5" + }, + "engines": { + "node": "^8.10.0 || ^10.13.0 || >=11.10.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "4.28.5", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.28.5.tgz", + "integrity": "sha512-MruOu4ZaDOLOhw4f/6iudyks/obuvvZUAHBDSW80Trnc5+ovmViLT2ZMDXhUV66ozcl6z0LJfKs1Usldgi/WCA==", + "dev": true, + "engines": { + "node": "^8.10.0 || ^10.13.0 || >=11.10.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "4.28.5", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.28.5.tgz", + "integrity": "sha512-FzJUKsBX8poCCdve7iV7ShirP8V+ys2t1fvamVeD1rWpiAnIm550a+BX/fmTHrjEpQJ7ZAn+Z7ZZwJjytk9rZw==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "4.28.5", + "@typescript-eslint/visitor-keys": "4.28.5", + "debug": "^4.3.1", + "globby": "^11.0.3", + "is-glob": "^4.0.1", + "semver": "^7.3.5", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/globby": { + "version": "11.0.4", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.0.4.tgz", + "integrity": "sha512-9O4MVG9ioZJ08ffbcyVYyLOJLk5JQ688pJ4eMGLpdWLHq/Wr1D9BlriLQyL0E+jbkuePVZXYFj47QM/v093wHg==", + "dev": true, + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.1.1", + "ignore": "^5.1.4", + "merge2": "^1.3.0", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/ignore": { + "version": "5.1.8", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz", + "integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "4.28.5", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.28.5.tgz", + "integrity": "sha512-dva/7Rr+EkxNWdJWau26xU/0slnFlkh88v3TsyTgRS/IIYFi5iIfpCFM4ikw0vQTFUR9FYSSyqgK4w64gsgxhg==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "4.28.5", + "eslint-visitor-keys": "^2.0.0" + }, + "engines": { + "node": "^8.10.0 || ^10.13.0 || >=11.10.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, "node_modules/acorn": { "version": "7.4.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", @@ -9834,6 +10045,27 @@ "node": ">=4" } }, + "node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "node_modules/tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "dev": true, + "dependencies": { + "tslib": "^1.8.1" + }, + "engines": { + "node": ">= 6" + }, + "peerDependencies": { + "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -9867,6 +10099,20 @@ "is-typedarray": "^1.0.0" } }, + "node_modules/typescript": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.3.5.tgz", + "integrity": "sha512-DqQgihaQ9cUrskJo9kIyW/+g0Vxsk8cDtZ52a3NGh0YNTfpUSArXSohyUGnvbPazEPLu398C0UxmKSOrPumUzA==", + "dev": true, + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, "node_modules/uid2": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.3.tgz", @@ -11289,6 +11535,12 @@ "@types/node": "*" } }, + "@types/json-schema": { + "version": "7.0.8", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.8.tgz", + "integrity": "sha512-YSBPTLTVm2e2OoQIDYx8HaeWJ5tTToLH67kXR7zYNGupXMEHa2++G8k+DczX2cFVgalypqtyZIcU19AFcmOpmg==", + "dev": true + }, "@types/mdast": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.7.tgz", @@ -11334,6 +11586,122 @@ "integrity": "sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==", "dev": true }, + "@typescript-eslint/eslint-plugin": { + "version": "4.28.5", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.28.5.tgz", + "integrity": "sha512-m31cPEnbuCqXtEZQJOXAHsHvtoDi9OVaeL5wZnO2KZTnkvELk+u6J6jHg+NzvWQxk+87Zjbc4lJS4NHmgImz6Q==", + "dev": true, + "requires": { + "@typescript-eslint/experimental-utils": "4.28.5", + "@typescript-eslint/scope-manager": "4.28.5", + "debug": "^4.3.1", + "functional-red-black-tree": "^1.0.1", + "regexpp": "^3.1.0", + "semver": "^7.3.5", + "tsutils": "^3.21.0" + } + }, + "@typescript-eslint/experimental-utils": { + "version": "4.28.5", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.28.5.tgz", + "integrity": "sha512-bGPLCOJAa+j49hsynTaAtQIWg6uZd8VLiPcyDe4QPULsvQwLHGLSGKKcBN8/lBxIX14F74UEMK2zNDI8r0okwA==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.7", + "@typescript-eslint/scope-manager": "4.28.5", + "@typescript-eslint/types": "4.28.5", + "@typescript-eslint/typescript-estree": "4.28.5", + "eslint-scope": "^5.1.1", + "eslint-utils": "^3.0.0" + }, + "dependencies": { + "eslint-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", + "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^2.0.0" + } + } + } + }, + "@typescript-eslint/parser": { + "version": "4.28.5", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.28.5.tgz", + "integrity": "sha512-NPCOGhTnkXGMqTznqgVbA5LqVsnw+i3+XA1UKLnAb+MG1Y1rP4ZSK9GX0kJBmAZTMIktf+dTwXToT6kFwyimbw==", + "dev": true, + "peer": true, + "requires": { + "@typescript-eslint/scope-manager": "4.28.5", + "@typescript-eslint/types": "4.28.5", + "@typescript-eslint/typescript-estree": "4.28.5", + "debug": "^4.3.1" + } + }, + "@typescript-eslint/scope-manager": { + "version": "4.28.5", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.28.5.tgz", + "integrity": "sha512-PHLq6n9nTMrLYcVcIZ7v0VY1X7dK309NM8ya9oL/yG8syFINIMHxyr2GzGoBYUdv3NUfCOqtuqps0ZmcgnZTfQ==", + "dev": true, + "requires": { + "@typescript-eslint/types": "4.28.5", + "@typescript-eslint/visitor-keys": "4.28.5" + } + }, + "@typescript-eslint/types": { + "version": "4.28.5", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.28.5.tgz", + "integrity": "sha512-MruOu4ZaDOLOhw4f/6iudyks/obuvvZUAHBDSW80Trnc5+ovmViLT2ZMDXhUV66ozcl6z0LJfKs1Usldgi/WCA==", + "dev": true + }, + "@typescript-eslint/typescript-estree": { + "version": "4.28.5", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.28.5.tgz", + "integrity": "sha512-FzJUKsBX8poCCdve7iV7ShirP8V+ys2t1fvamVeD1rWpiAnIm550a+BX/fmTHrjEpQJ7ZAn+Z7ZZwJjytk9rZw==", + "dev": true, + "requires": { + "@typescript-eslint/types": "4.28.5", + "@typescript-eslint/visitor-keys": "4.28.5", + "debug": "^4.3.1", + "globby": "^11.0.3", + "is-glob": "^4.0.1", + "semver": "^7.3.5", + "tsutils": "^3.21.0" + }, + "dependencies": { + "globby": { + "version": "11.0.4", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.0.4.tgz", + "integrity": "sha512-9O4MVG9ioZJ08ffbcyVYyLOJLk5JQ688pJ4eMGLpdWLHq/Wr1D9BlriLQyL0E+jbkuePVZXYFj47QM/v093wHg==", + "dev": true, + "requires": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.1.1", + "ignore": "^5.1.4", + "merge2": "^1.3.0", + "slash": "^3.0.0" + } + }, + "ignore": { + "version": "5.1.8", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz", + "integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==", + "dev": true + } + } + }, + "@typescript-eslint/visitor-keys": { + "version": "4.28.5", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.28.5.tgz", + "integrity": "sha512-dva/7Rr+EkxNWdJWau26xU/0slnFlkh88v3TsyTgRS/IIYFi5iIfpCFM4ikw0vQTFUR9FYSSyqgK4w64gsgxhg==", + "dev": true, + "requires": { + "@typescript-eslint/types": "4.28.5", + "eslint-visitor-keys": "^2.0.0" + } + }, "acorn": { "version": "7.4.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", @@ -17838,6 +18206,21 @@ } } }, + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "dev": true, + "requires": { + "tslib": "^1.8.1" + } + }, "type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -17862,6 +18245,13 @@ "is-typedarray": "^1.0.0" } }, + "typescript": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.3.5.tgz", + "integrity": "sha512-DqQgihaQ9cUrskJo9kIyW/+g0Vxsk8cDtZ52a3NGh0YNTfpUSArXSohyUGnvbPazEPLu398C0UxmKSOrPumUzA==", + "dev": true, + "peer": true + }, "uid2": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.3.tgz", diff --git a/package.json b/package.json index 31ba0caf..1c7a8043 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "@netlify/functions", "main": "./src/main.js", "types": "./src/main.d.ts", - "version": "0.7.2", + "version": "0.7.3-handle-secrets.0", "description": "JavaScript utilities for Netlify Functions", "files": [ "src/**/*.js", @@ -58,6 +58,7 @@ "@commitlint/cli": "^13.0.0", "@commitlint/config-conventional": "^13.0.0", "@netlify/eslint-config-node": "^3.1.9", + "@typescript-eslint/eslint-plugin": "^4.28.5", "ava": "^2.4.0", "husky": "^4.3.8", "nyc": "^15.0.0" diff --git a/src/function/handler.d.ts b/src/function/handler.d.ts index 01485ffd..d5c6300d 100644 --- a/src/function/handler.d.ts +++ b/src/function/handler.d.ts @@ -6,6 +6,6 @@ export interface HandlerCallback { (error: any, response: Response): void } -export interface Handler { - (event: Event, context: Context, callback: HandlerCallback): void | Response | Promise +export interface Handler { + (event: Event, context: C, callback: HandlerCallback): void | Response | Promise } diff --git a/src/lib/builder.d.ts b/src/lib/builder.d.ts index 7493156c..f52bf505 100644 --- a/src/lib/builder.d.ts +++ b/src/lib/builder.d.ts @@ -1,7 +1,8 @@ +import { Context } from '../function/context' import { Handler } from '../function/handler' export interface Builder { - (handler: Handler): Handler + (handler: Handler): Handler } export declare const builder: Builder diff --git a/src/lib/secrets.d.ts b/src/lib/secrets.d.ts new file mode 100644 index 00000000..2c202276 --- /dev/null +++ b/src/lib/secrets.d.ts @@ -0,0 +1,23 @@ +import { Context } from '../function/context' +import { Handler } from '../function/handler' +import * as services from './services.json' + +export type Services = typeof services + +export type ServiceKey = keyof Services + +export type ServiceTokens = Services[T]['tokens'] + +export type NetlifySecrets = { + [K in ServiceKey]?: ServiceTokens +} + +export interface ContextWithSecrets extends Context { + secrets: NetlifySecrets +} + +export type HandlerWithSecrets = Handler + +export declare const getSecrets: () => NetlifySecrets + +export declare const withSecrets: (handler: HandlerWithSecrets) => Handler diff --git a/src/lib/secrets.js b/src/lib/secrets.js new file mode 100644 index 00000000..51397a35 --- /dev/null +++ b/src/lib/secrets.js @@ -0,0 +1,34 @@ +const process = require('process') + +// eslint-disable-next-line node/no-unpublished-require +const services = require('./services.json') + +const getSecrets = () => + Object.entries(services).reduce((secrets, [serviceName, service]) => { + const serviceSecrets = [] + // This is so if there are no secrets we don't add an empty object + Object.entries(service.tokens).forEach(([tokenName, token]) => { + if (token in process.env) { + serviceSecrets.push([tokenName, process.env[token]]) + } + }) + if (serviceSecrets.length !== 0) { + // No Object.fromEntries in node < 12 + return { + ...secrets, + [serviceName]: serviceSecrets.reduce((acc, [tokenName, token]) => ({ ...acc, [tokenName]: token }), {}), + } + } + return secrets + }, {}) + +// eslint-disable-next-line promise/prefer-await-to-callbacks +const withSecrets = (handler) => (event, context, callback) => { + const secrets = getSecrets() + return handler(event, { ...context, secrets }, callback) +} + +module.exports = { + getSecrets, + withSecrets, +} diff --git a/src/lib/services.json b/src/lib/services.json new file mode 100644 index 00000000..18e164e1 --- /dev/null +++ b/src/lib/services.json @@ -0,0 +1,17 @@ +{ + "github": { + "tokens": { + "token": "ONEGRAPH_GITHUB_TOKEN" + } + }, + "spotify": { + "tokens": { + "token": "ONEGRAPH_SPOTIFY_TOKEN" + } + }, + "salesforce": { + "tokens": { + "token": "ONEGRAPH_SALESFORCE_TOKEN" + } + } +} diff --git a/src/main.d.ts b/src/main.d.ts index 8d0a8a5a..9a1df06d 100644 --- a/src/main.d.ts +++ b/src/main.d.ts @@ -1,2 +1,3 @@ export * from './lib/builder' export * from './function' +export * from './lib/secrets' diff --git a/src/main.js b/src/main.js index 9c21cbe0..c576e3bf 100644 --- a/src/main.js +++ b/src/main.js @@ -1,3 +1,4 @@ const { builder } = require('./lib/builder') +const { withSecrets, getSecrets } = require('./lib/secrets') -module.exports = { builder } +module.exports = { builder, withSecrets, getSecrets } From 2392bb064ffe8cbc4aa1a6adec501c12c2689cd8 Mon Sep 17 00:00:00 2001 From: Matt Kane Date: Tue, 27 Jul 2021 14:46:55 +0100 Subject: [PATCH 2/8] fix: include json --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 1c7a8043..365cdebf 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "files": [ "src/**/*.js", "src/**/*.ts", + "src/**/*.json", "!src/**/*.test.js" ], "scripts": { From 249318916d97a7625b2a389b09d2bff98419dd39 Mon Sep 17 00:00:00 2001 From: Matt Kane Date: Tue, 27 Jul 2021 14:49:58 +0100 Subject: [PATCH 3/8] 0.7.3-handle-secrets.1 --- package-lock.json | 4 ++-- package.json | 2 +- src/lib/secrets.js | 1 - 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index f58bdfd2..e25d1af2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@netlify/functions", - "version": "0.7.3-handle-secrets.0", + "version": "0.7.3-handle-secrets.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@netlify/functions", - "version": "0.7.3-handle-secrets.0", + "version": "0.7.3-handle-secrets.1", "license": "MIT", "dependencies": { "is-promise": "^4.0.0" diff --git a/package.json b/package.json index 365cdebf..95557658 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "@netlify/functions", "main": "./src/main.js", "types": "./src/main.d.ts", - "version": "0.7.3-handle-secrets.0", + "version": "0.7.3-handle-secrets.1", "description": "JavaScript utilities for Netlify Functions", "files": [ "src/**/*.js", diff --git a/src/lib/secrets.js b/src/lib/secrets.js index 51397a35..71886b11 100644 --- a/src/lib/secrets.js +++ b/src/lib/secrets.js @@ -1,6 +1,5 @@ const process = require('process') -// eslint-disable-next-line node/no-unpublished-require const services = require('./services.json') const getSecrets = () => From 068277108711dc88ce78617681088994e63225d6 Mon Sep 17 00:00:00 2001 From: Daniel Woelfel Date: Wed, 4 Aug 2021 05:52:10 -0700 Subject: [PATCH 4/8] feat: fetch secrets from OneGraph as-needed (#107) * Make secrets lookup just-in-time and dynamic * fix: fix ci issues Fix eslint and prettier * feat: update TypeScript definitions and auth metadata Expands on the GraphQL query for the service auths * feat: add warning if Autlify has not been set up * feat: remove withContext and implement review feedback Co-authored-by: Sean Grove --- src/lib/consts.js | 2 + src/lib/secrets.d.ts | 39 ++++++----- src/lib/secrets.js | 147 ++++++++++++++++++++++++++++++++++++------ src/lib/services.json | 17 ----- 4 files changed, 153 insertions(+), 52 deletions(-) delete mode 100644 src/lib/services.json diff --git a/src/lib/consts.js b/src/lib/consts.js index ae2176cb..f71ab0b4 100644 --- a/src/lib/consts.js +++ b/src/lib/consts.js @@ -2,10 +2,12 @@ const BUILDER_FUNCTIONS_FLAG = true const HTTP_STATUS_METHOD_NOT_ALLOWED = 405 const HTTP_STATUS_OK = 200 const METADATA_VERSION = 1 +const ONEGRAPH_AUTHLIFY_APP_ID = '4d3de9a5-722f-4d27-9c96-2ac43c93c004' module.exports = { BUILDER_FUNCTIONS_FLAG, HTTP_STATUS_METHOD_NOT_ALLOWED, HTTP_STATUS_OK, METADATA_VERSION, + ONEGRAPH_AUTHLIFY_APP_ID, } diff --git a/src/lib/secrets.d.ts b/src/lib/secrets.d.ts index 2c202276..6821695f 100644 --- a/src/lib/secrets.d.ts +++ b/src/lib/secrets.d.ts @@ -1,23 +1,32 @@ -import { Context } from '../function/context' -import { Handler } from '../function/handler' -import * as services from './services.json' - -export type Services = typeof services - -export type ServiceKey = keyof Services - -export type ServiceTokens = Services[T]['tokens'] +export type ScopeInfo = { + category: string | null + scope: string + display: string + isDefault: boolean + isRequired: boolean + description: string | null + title: string | null +} -export type NetlifySecrets = { - [K in ServiceKey]?: ServiceTokens +export type Scope = { + scope: string + scopeInfo: ScopeInfo | null } -export interface ContextWithSecrets extends Context { - secrets: NetlifySecrets +export type Service = { + friendlyServiceName: string + service: string + isLoggedIn: boolean + bearerToken: string | null + grantedScopes: Array | null } -export type HandlerWithSecrets = Handler +export type NetlifySecrets = { + gitHub?: Service | null + spotify?: Service | null + salesforce?: Service | null + stripe?: Service | null +} export declare const getSecrets: () => NetlifySecrets -export declare const withSecrets: (handler: HandlerWithSecrets) => Handler diff --git a/src/lib/secrets.js b/src/lib/secrets.js index 71886b11..072f06dc 100644 --- a/src/lib/secrets.js +++ b/src/lib/secrets.js @@ -1,33 +1,140 @@ +const { Buffer } = require('buffer') +const https = require('https') const process = require('process') -const services = require('./services.json') +const { ONEGRAPH_AUTHLIFY_APP_ID } = require('./consts') -const getSecrets = () => - Object.entries(services).reduce((secrets, [serviceName, service]) => { - const serviceSecrets = [] - // This is so if there are no secrets we don't add an empty object - Object.entries(service.tokens).forEach(([tokenName, token]) => { - if (token in process.env) { - serviceSecrets.push([tokenName, process.env[token]]) +const camelize = function (text) { + const safe = text.replace(/[-_\s.]+(.)?/g, (_, sub) => (sub ? sub.toUpperCase() : '')) + return safe.slice(0, 1).toLowerCase() + safe.slice(1) +} + +// The services will be camelized versions of the OneGraph service enums +// unless overridden by the serviceNormalizeOverrides object +const serviceNormalizeOverrides = { + // Keys are the OneGraph service enums, values are the desired `secret.` names + GITHUB: 'gitHub', +} + +const oneGraphRequest = function (secretToken, requestBody) { + // eslint-disable-next-line node/no-unsupported-features/node-builtins + const requestBodyBuffer = Buffer.from(new TextEncoder().encode(requestBody)) + return new Promise((resolve, reject) => { + const port = 443 + + const options = { + host: 'serve.onegraph.com', + path: `/graphql?app_id=${ONEGRAPH_AUTHLIFY_APP_ID}`, + port, + method: 'POST', + headers: { + Authorization: `Bearer ${secretToken}`, + 'Content-Type': 'application/json', + Accept: 'application/json', + 'Content-Length': Buffer.byteLength(requestBodyBuffer), + }, + } + + const req = https.request(options, (res) => { + if (res.statusCode !== 200) { + return reject(new Error(res.statusCode)) } + + let body = [] + + res.on('data', (chunk) => { + body.push(chunk) + }) + + res.on('end', () => { + const data = Buffer.concat(body).toString() + try { + body = JSON.parse(data) + } catch (error) { + reject(error) + } + resolve(body) + }) }) - if (serviceSecrets.length !== 0) { - // No Object.fromEntries in node < 12 - return { - ...secrets, - [serviceName]: serviceSecrets.reduce((acc, [tokenName, token]) => ({ ...acc, [tokenName]: token }), {}), + + req.on('error', (error) => { + reject(error.message) + }) + + req.write(requestBodyBuffer) + + req.end() + }) +} + +const formatSecrets = (result) => { + const services = + result.data && result.data.me && result.data.me.serviceMetadata && result.data.me.serviceMetadata.loggedInServices + + if (services) { + const newSecrets = services.reduce((acc, service) => { + const normalized = serviceNormalizeOverrides[service.service] || camelize(service.friendlyServiceName) + // eslint-disable-next-line no-param-reassign + acc[normalized] = service + return acc + }, {}) + + return newSecrets + } + + return {} +} + +// Note: We may want to have configurable "sets" of secrets, +// e.g. "dev" and "prod" +const getSecrets = async () => { + const secretToken = process.env.ONEGRAPH_AUTHLIFY_TOKEN + + if (!secretToken) { + console.warn( + 'getSecrets is not set up. Visit Netlify Labs to enable it or trigger a new deploy if it has been enabled.', + ) + return {} + } + + // We select for more than we typically need here + // in order to allow for some metaprogramming for + // consumers downstream. Also, the data is typically + // static and shouldn't add any measurable overhead. + const doc = `query FindLoggedInServicesQuery { + me { + serviceMetadata { + loggedInServices { + friendlyServiceName + service + isLoggedIn + bearerToken + grantedScopes { + scope + scopeInfo { + category + scope + display + isDefault + isRequired + description + title + } + } + } } } - return secrets - }, {}) + }` + + const body = JSON.stringify({ query: doc }) + + const result = await oneGraphRequest(secretToken, body) + + const newSecrets = formatSecrets(result) -// eslint-disable-next-line promise/prefer-await-to-callbacks -const withSecrets = (handler) => (event, context, callback) => { - const secrets = getSecrets() - return handler(event, { ...context, secrets }, callback) + return newSecrets } module.exports = { getSecrets, - withSecrets, } diff --git a/src/lib/services.json b/src/lib/services.json deleted file mode 100644 index 18e164e1..00000000 --- a/src/lib/services.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "github": { - "tokens": { - "token": "ONEGRAPH_GITHUB_TOKEN" - } - }, - "spotify": { - "tokens": { - "token": "ONEGRAPH_SPOTIFY_TOKEN" - } - }, - "salesforce": { - "tokens": { - "token": "ONEGRAPH_SALESFORCE_TOKEN" - } - } -} From 17205dfeaa5e0373fceb257393a3d6d136550863 Mon Sep 17 00:00:00 2001 From: Tiffany Le-Nguyen Date: Thu, 2 Sep 2021 13:33:37 -0400 Subject: [PATCH 5/8] Update package.json --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 95557658..6a42fa13 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "@netlify/functions", "main": "./src/main.js", "types": "./src/main.d.ts", - "version": "0.7.3-handle-secrets.1", + "version": "0.7.3-handle-secrets.2", "description": "JavaScript utilities for Netlify Functions", "files": [ "src/**/*.js", From 7573d233e6ad2ff877c322473b6b60207f11142d Mon Sep 17 00:00:00 2001 From: Sean Grove Date: Thu, 9 Sep 2021 07:19:15 -0700 Subject: [PATCH 6/8] chore: Add provisional docs for auth usage (#109) * chore: Add provisional docs for auth usage Adds documentation to the README on how to use Netlify Integration Auth in functions. * Update README.md Co-authored-by: Rachael Stavchansky * Update README.md Co-authored-by: Rachael Stavchansky * Update README.md Co-authored-by: Rachael Stavchansky * Incorporate copy feedback * Update README.md Co-authored-by: Rachael Stavchansky Co-authored-by: Tiffany Le-Nguyen --- README.md | 102 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) diff --git a/README.md b/README.md index d97a97d4..ba255d9f 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,108 @@ The following types are exported: - `HandlerEvent` - `HandlerResponse` + +## Auth Management + +**Note: This feature is currently in beta under Netlify Labs** + +To make building on and interacting with third-party APIs as simple and powerful as possible, Netlify provides API secret provisioning and management, powered by [OneGraph](https://www.onegraph.com). It’s enabled on a per-site basis under [Netlify labs](https://app.netlify.com/user/labs) tab, where you can use the Netlify UI to select which services you want to make available for your functions or site builds, and which scopes you need access to. + +### Usage + +After you’ve enabled one or more services, you can access the relevant API tokens and secrets in your serverless functions with the `getSecrets` function exported from the `@netlify/functions` package. + +> `getSecrets` is fully typed, so you’ll have in-editor autocomplete to explore everything that’s available, and to be confident that you’re handling all of the edge cases + + ```js + import { getSecrets } from "@netlify/functions"; + + export const handler = async (event) => { + // Handle all fetching, refreshing, rotating, etc. of tokens + const secrets = await getSecrets(); + + // Check if the GitHub auth has been enabled for the site + if (!secrets.gitHub?.bearerToken) { + return { + statusCode: 412, + body: JSON.stringify({ + error: "You must enable the GitHub auth in your Netlify dashboard", + }), + headers: { + "Content-Type": "application/json", + }, + }; + } + + // If so, we can make calls to the GitHub API - REST or GraphQL! + const MyOctokit = Octokit.plugin(restEndpointMethods); + const octokit = new MyOctokit({ auth: secrets.gitHub.bearerToken }); + + // We'll list all open issues on the netlify/functions repository + const result = await octokit.rest.issues.list({ + owner: "netlify", + repo: "functions", + state: "open", + }); + + return { + statusCode: 200, + body: JSON.stringify(result), + headers: { + "Content-Type": "application/json", + }, + }; + }; + ``` + +### Checking additional metadata about auth token in your functions and site builds +Auth Management also tracks metadata for installed auth tokens. You can verify that an auth has been installed with the correct scopes before calling into an API (say, for example, to give a better error message in the developer logs). Here's an example: + + ```js + import { getSecrets } from "@netlify/functions"; + + export const handler = async (event) => { + // Handle all fetching, refreshing, rotating, etc. of tokens + const secrets = await getSecrets(); + + // We know that we need either "public_repo" or "repo" scopes granted + // in order to run this function properly + const sufficientScopes = ["public_repo", "repo"]; + + // Secrets have an optional `grantedScopes` field that has details + // on what the auth is allowed to do + const tokenHasScope = secrets.gitHub.grantedScopes?.some((grantedScope) => + sufficientScopes.includes(grantedScope.scope) + ); + + // Notice how we can leave a great error message that tells us exactly what + // we need to do to fix things. + if (!tokenHasScope) { + return { + statusCode: 412, + body: JSON.stringify({ + error: `You have enabled GitHub auth in your Netlify Auth dashboard, but it's missing a required scope. The auth must have one (or both) of the scopes: ${sufficientScopes.join(", ")}`, + }), + headers: { + "Content-Type": "application/json", + }, + }; + } + + // ... + } + ``` + +### Accessing integration auth tokens during development + +When running your site under `netlify dev`, the environmental variables that power the auth management will be synced, and you can transparently develop against the third party APIs - no additional configuration necessary! + +### Updating or removing auth tokens from your site + +At any time you can revisit the Auth Management tab for your site in [Netlify Labs](https://app.netlify.com/user/labs) (select your profile avatar, then Netlify Labs) to see the installed auth. From there, you can select new scopes for already-installed auth and then run through the browser-based auth flow again, and the new scopes will be available to all your existing, deployed functions and site builds _instantly_. + +You can also install new services or remove currently-installed services you’re no longer using. + ## Contributors Please see [CONTRIBUTING.md](./CONTRIBUTING.md) for instructions on how to set up and work on this repository. Thanks From 7525b7f613408f6ce4a0503ec2f2ec7a856bd27c Mon Sep 17 00:00:00 2001 From: Tiffany Le-Nguyen Date: Thu, 9 Sep 2021 11:04:29 -0400 Subject: [PATCH 7/8] Apply suggestions from code review --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index ba255d9f..94dbc494 100644 --- a/README.md +++ b/README.md @@ -75,7 +75,7 @@ The following types are exported: - `HandlerResponse` -## Auth Management +## Auth Token Manager **Note: This feature is currently in beta under Netlify Labs** @@ -129,7 +129,7 @@ After you’ve enabled one or more services, you can access the relevant API tok ``` ### Checking additional metadata about auth token in your functions and site builds -Auth Management also tracks metadata for installed auth tokens. You can verify that an auth has been installed with the correct scopes before calling into an API (say, for example, to give a better error message in the developer logs). Here's an example: +Auth Token Manager also tracks metadata for installed auth tokens. You can verify that an auth has been installed with the correct scopes before calling into an API (say, for example, to give a better error message in the developer logs). Here's an example: ```js import { getSecrets } from "@netlify/functions"; @@ -172,7 +172,7 @@ When running your site under `netlify dev`, the environmental variables that pow ### Updating or removing auth tokens from your site -At any time you can revisit the Auth Management tab for your site in [Netlify Labs](https://app.netlify.com/user/labs) (select your profile avatar, then Netlify Labs) to see the installed auth. From there, you can select new scopes for already-installed auth and then run through the browser-based auth flow again, and the new scopes will be available to all your existing, deployed functions and site builds _instantly_. +At any time you can revisit the Auth Token Manager tab for your site in [Netlify Labs](https://app.netlify.com/user/labs) (select your team, then select your profile avatar, then Netlify Labs) to see the installed auth. From there, you can select new scopes for already-installed auth and then run through the browser-based auth flow again, and the new scopes will be available to all your existing, deployed functions and site builds _instantly_. You can also install new services or remove currently-installed services you’re no longer using. From 16f9b9457fa8044dfaab44ef4276872c4424b888 Mon Sep 17 00:00:00 2001 From: Jason Lengstorf Date: Thu, 9 Sep 2021 10:44:20 -0700 Subject: [PATCH 8/8] fix: update the example with a tested demo function (#126) the original example was missing some imports and wouldn't run as-is. this example does --- README.md | 67 ++++++++++++++++++++++++++----------------------------- 1 file changed, 32 insertions(+), 35 deletions(-) diff --git a/README.md b/README.md index 94dbc494..42cb2361 100644 --- a/README.md +++ b/README.md @@ -88,44 +88,41 @@ After you’ve enabled one or more services, you can access the relevant API tok > `getSecrets` is fully typed, so you’ll have in-editor autocomplete to explore everything that’s available, and to be confident that you’re handling all of the edge cases ```js - import { getSecrets } from "@netlify/functions"; - - export const handler = async (event) => { - // Handle all fetching, refreshing, rotating, etc. of tokens - const secrets = await getSecrets(); - - // Check if the GitHub auth has been enabled for the site - if (!secrets.gitHub?.bearerToken) { - return { - statusCode: 412, - body: JSON.stringify({ - error: "You must enable the GitHub auth in your Netlify dashboard", - }), - headers: { - "Content-Type": "application/json", - }, - }; - } - - // If so, we can make calls to the GitHub API - REST or GraphQL! - const MyOctokit = Octokit.plugin(restEndpointMethods); - const octokit = new MyOctokit({ auth: secrets.gitHub.bearerToken }); - - // We'll list all open issues on the netlify/functions repository - const result = await octokit.rest.issues.list({ - owner: "netlify", - repo: "functions", - state: "open", - }); - + import { getSecrets } from '@netlify/functions'; + import { Octokit } from '@octokit/rest'; + + export async function handler(event) { + // check for a owner/org and repo name in the query params + const { owner = 'netlify', repo = 'functions' } = event.queryStringParameters; + + // load the secrets enabled via Netlify Auth Management + const secrets = await getSecrets(); + + // ensure that GitHub auth is enabled for this site + if (!secrets.gitHub?.bearerToken) { return { - statusCode: 200, - body: JSON.stringify(result), - headers: { - "Content-Type": "application/json", - }, + statusCode: 412, + body: JSON.stringify({ + error: + 'You must enable GitHub auth in your Netlify dashboard: https://app.netlify.com/user/labs', + }), }; + } + + // use Octokit with the GitHub secret + const octokit = new Octokit({ auth: secrets.gitHub.bearerToken }); + + // get a list of all issues assigned to the current user + const result = await octokit.repos.get({ owner, repo }); + + return { + statusCode: 200, + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(result.data), }; + } ``` ### Checking additional metadata about auth token in your functions and site builds