Thanks to visit codestin.com
Credit goes to github.com

Skip to content
This repository was archived by the owner on Jun 9, 2025. It is now read-only.

feat: fetch secrets from OneGraph as-needed #107

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/lib/consts.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
39 changes: 24 additions & 15 deletions src/lib/secrets.d.ts
Original file line number Diff line number Diff line change
@@ -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<T extends ServiceKey> = 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<K>
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<Scope> | null
}

export type HandlerWithSecrets = Handler<ContextWithSecrets>
export type NetlifySecrets = {
gitHub?: Service | null
spotify?: Service | null
salesforce?: Service | null
stripe?: Service | null
}

export declare const getSecrets: () => NetlifySecrets

export declare const withSecrets: <C extends Context>(handler: HandlerWithSecrets) => Handler<C>
147 changes: 127 additions & 20 deletions src/lib/secrets.js
Original file line number Diff line number Diff line change
@@ -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.<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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think optional chaining is what I miss most when not transpiling 😁


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,
}
17 changes: 0 additions & 17 deletions src/lib/services.json

This file was deleted.