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" - } - } -}