@netlify/functions
Advanced tools
Comparing version 0.7.3-handle-secrets.1 to 0.7.3-handle-secrets.2
@@ -5,3 +5,3 @@ { | ||
"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", | ||
@@ -8,0 +8,0 @@ "files": [ |
@@ -5,2 +5,3 @@ const BUILDER_FUNCTIONS_FLAG = true | ||
const METADATA_VERSION = 1 | ||
const ONEGRAPH_AUTHLIFY_APP_ID = '4d3de9a5-722f-4d27-9c96-2ac43c93c004' | ||
@@ -12,2 +13,3 @@ module.exports = { | ||
METADATA_VERSION, | ||
ONEGRAPH_AUTHLIFY_APP_ID, | ||
} |
@@ -1,23 +0,32 @@ | ||
import { Context } from '../function/context' | ||
import { Handler } from '../function/handler' | ||
import * as services from './services.json' | ||
export type ScopeInfo = { | ||
category: string | null | ||
scope: string | ||
display: string | ||
isDefault: boolean | ||
isRequired: boolean | ||
description: string | null | ||
title: string | null | ||
} | ||
export type Services = typeof services | ||
export type Scope = { | ||
scope: string | ||
scopeInfo: ScopeInfo | null | ||
} | ||
export type ServiceKey = keyof Services | ||
export type Service = { | ||
friendlyServiceName: string | ||
service: string | ||
isLoggedIn: boolean | ||
bearerToken: string | null | ||
grantedScopes: Array<Scope> | null | ||
} | ||
export type ServiceTokens<T extends ServiceKey> = Services[T]['tokens'] | ||
export type NetlifySecrets = { | ||
[K in ServiceKey]?: ServiceTokens<K> | ||
gitHub?: Service | null | ||
spotify?: Service | null | ||
salesforce?: Service | null | ||
stripe?: Service | null | ||
} | ||
export interface ContextWithSecrets extends Context { | ||
secrets: NetlifySecrets | ||
} | ||
export type HandlerWithSecrets = Handler<ContextWithSecrets> | ||
export declare const getSecrets: () => NetlifySecrets | ||
export declare const withSecrets: <C extends Context>(handler: HandlerWithSecrets) => Handler<C> |
@@ -0,28 +1,136 @@ | ||
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.<service>` 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 | ||
}, {}) | ||
}` | ||
// eslint-disable-next-line promise/prefer-await-to-callbacks | ||
const withSecrets = (handler) => (event, context, callback) => { | ||
const secrets = getSecrets() | ||
return handler(event, { ...context, secrets }, callback) | ||
const body = JSON.stringify({ query: doc }) | ||
const result = await oneGraphRequest(secretToken, body) | ||
const newSecrets = formatSecrets(result) | ||
return newSecrets | ||
} | ||
@@ -32,3 +140,2 @@ | ||
getSecrets, | ||
withSecrets, | ||
} |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a package.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 2 instances in 1 package
14148
276
2
15
2