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

Commit 9019789

Browse files
authored
refactor(authlify): get authlify token from header (#326)
* feat: get netlify graph token from headers * chore: cleanup * chore: be a little more careful about backwards compatibility * feat: add getGraphToken and address some feedback * chore: implement remaining review feedback * fix: fix copy/paste error * chore: cleanup comments
1 parent c3b0fe5 commit 9019789

File tree

5 files changed

+222
-74
lines changed

5 files changed

+222
-74
lines changed

src/function/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,5 @@ export { Context as HandlerContext } from './context'
22
export { Event as HandlerEvent } from './event'
33
export { Handler, HandlerCallback } from './handler'
44
export { Response as HandlerResponse } from './response'
5-
export { getSecrets, withSecrets } from '../lib/secrets'
5+
export { getSecrets, withSecrets, getNetlifyGraphToken, GraphTokenResponse, HasHeaders } from '../lib/graph'
66
export { NetlifySecrets } from '../lib/secrets_helper'

src/lib/secrets.ts renamed to src/lib/graph.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@ import { Event as HandlerEvent } from '../function/event'
33
import { BaseHandler, HandlerCallback } from '../function/handler'
44
import { Response } from '../function/response'
55

6-
import { getSecrets, HandlerEventWithOneGraph, NetlifySecrets } from './secrets_helper'
6+
import { getSecrets, NetlifySecrets } from './secrets_helper'
77
// Fine-grained control during the preview, less necessary with a more proactive OneGraph solution
88
export { getSecrets } from './secrets_helper'
9+
export { getNetlifyGraphToken, GraphTokenResponse, HasHeaders } from './graph_token'
910

1011
export interface ContextWithSecrets extends Context {
1112
secrets: NetlifySecrets
@@ -17,12 +18,12 @@ export type HandlerWithSecrets = BaseHandler<Response, ContextWithSecrets>
1718
export const withSecrets =
1819
(handler: BaseHandler<Response, ContextWithSecrets>) =>
1920
async (
20-
event: HandlerEventWithOneGraph | HandlerEvent,
21+
event: HandlerEvent,
2122
context: HandlerContext,
2223
// eslint-disable-next-line promise/prefer-await-to-callbacks
2324
callback: HandlerCallback<Response>,
2425
) => {
25-
const secrets = await getSecrets(event as HandlerEventWithOneGraph)
26+
const secrets = await getSecrets(event)
2627

2728
return handler(event, { ...context, secrets }, callback)
2829
}

src/lib/graph_request.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { Buffer } from 'buffer'
2+
import { request } from 'https'
3+
import { env } from 'process'
4+
5+
const siteId = env.SITE_ID
6+
7+
const GRAPH_HOST = 'graph.netlify.com'
8+
9+
export const graphRequest = function (secretToken: string, requestBody: Uint8Array): Promise<string> {
10+
return new Promise((resolve, reject) => {
11+
const port = 443
12+
13+
const options = {
14+
host: GRAPH_HOST,
15+
path: `/graphql?app_id=${siteId}`,
16+
port,
17+
method: 'POST',
18+
headers: {
19+
Authorization: `Bearer ${secretToken}`,
20+
'Content-Type': 'application/json',
21+
Accept: 'application/json',
22+
'Content-Length': requestBody ? Buffer.byteLength(requestBody) : 0,
23+
},
24+
}
25+
26+
const req = request(options, (res) => {
27+
if (res.statusCode !== 200) {
28+
return reject(new Error(String(res.statusCode)))
29+
}
30+
31+
const body: Array<Uint8Array> = []
32+
33+
res.on('data', (chunk) => {
34+
body.push(chunk)
35+
})
36+
37+
res.on('end', () => {
38+
const data = Buffer.concat(body).toString()
39+
try {
40+
resolve(data)
41+
} catch (error) {
42+
reject(error)
43+
}
44+
})
45+
})
46+
47+
req.on('error', (error) => {
48+
reject(error)
49+
})
50+
51+
req.write(requestBody)
52+
53+
req.end()
54+
})
55+
}

src/lib/graph_token.ts

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import { env } from 'process'
2+
3+
export type GraphTokenResponseError = {
4+
type: 'missing-event-in-function' | 'provided-event-in-build'
5+
message: string
6+
}
7+
8+
export type GraphTokenResponse = {
9+
errors?: GraphTokenResponseError[]
10+
token?: string | null
11+
}
12+
13+
const TOKEN_HEADER = 'X-Nf-Graph-Token'
14+
15+
// Matches Web API Headers type (https://developer.mozilla.org/en-US/docs/Web/API/Headers)
16+
interface RequestHeaders {
17+
get(name: string): string | null
18+
}
19+
20+
// Matches http.IncomingHttpHeaders
21+
interface IncomingHttpHeaders {
22+
[key: string]: string | string[] | undefined
23+
}
24+
25+
export interface HasHeaders {
26+
headers: RequestHeaders | IncomingHttpHeaders
27+
}
28+
29+
const hasRequestStyleHeaders = function (headers: RequestHeaders | IncomingHttpHeaders): headers is RequestHeaders {
30+
return (headers as RequestHeaders).get !== undefined && typeof headers.get === 'function'
31+
}
32+
33+
const graphTokenFromIncomingHttpStyleHeaders = function (
34+
headers: RequestHeaders | IncomingHttpHeaders,
35+
): string | null | undefined {
36+
if (TOKEN_HEADER in headers) {
37+
const header = headers[TOKEN_HEADER]
38+
if (header == null || typeof header === 'string') {
39+
return header
40+
}
41+
return header[0]
42+
}
43+
}
44+
45+
// Backwards compatibility with older version of cli that doesn't inject header
46+
const authlifyTokenFallback = function (event: HasHeaders): GraphTokenResponse {
47+
const token = (event as { authlifyToken?: string | null })?.authlifyToken
48+
return { token }
49+
}
50+
51+
const graphTokenFromEvent = function (event: HasHeaders): GraphTokenResponse {
52+
const { headers } = event
53+
// Check if object first in case there is a header with key `get`
54+
const token = graphTokenFromIncomingHttpStyleHeaders(headers)
55+
if (token) {
56+
return { token }
57+
}
58+
59+
if (hasRequestStyleHeaders(headers)) {
60+
return { token: headers.get(TOKEN_HEADER) }
61+
}
62+
63+
return authlifyTokenFallback(event)
64+
}
65+
66+
const graphTokenFromEnv = function (): GraphTokenResponse {
67+
// _NETLIFY_GRAPH_TOKEN injected by next plugin
68+
// eslint-disable-next-line no-underscore-dangle
69+
const token = env._NETLIFY_GRAPH_TOKEN || env.NETLIFY_GRAPH_TOKEN
70+
return { token }
71+
}
72+
73+
const isEventRequired = function (): boolean {
74+
const localDev = env.NETLIFY_DEV === 'true'
75+
const localBuild = !localDev && env.NETLIFY_LOCAL === 'true'
76+
const remoteBuild = env.NETLIFY === 'true'
77+
// neither `localBuild` nor `remoteBuild` will be true in the on-demand builder case
78+
const inBuildPhase = localBuild || remoteBuild
79+
80+
const inGetStaticProps =
81+
// Set by the nextjs plugin
82+
// eslint-disable-next-line no-underscore-dangle
83+
typeof env._NETLIFY_GRAPH_TOKEN !== 'undefined'
84+
85+
return !inBuildPhase && !inGetStaticProps
86+
}
87+
88+
const incorrectArgumentsErrors = function (
89+
event: HasHeaders | null | undefined,
90+
): undefined | GraphTokenResponseError[] {
91+
const requiresEvent = isEventRequired()
92+
93+
if (requiresEvent && event == null) {
94+
const errorMessage =
95+
'You must provide an event or request to `getNetlifyGraphToken` when used in functions and on-demand builders.'
96+
return [{ type: 'missing-event-in-function', message: errorMessage }]
97+
}
98+
99+
if (!requiresEvent && event != null) {
100+
const errorMessage = 'You must not pass arguments to `getNetlifyGraphToken` when used in builds.'
101+
return [{ type: 'provided-event-in-build', message: errorMessage }]
102+
}
103+
}
104+
105+
const logErrors = function (errors: GraphTokenResponseError[]) {
106+
for (const error of errors) {
107+
// Log errors to help guide developer
108+
console.error(error.message)
109+
}
110+
}
111+
112+
export const getNetlifyGraphToken = function (
113+
event?: HasHeaders | null | undefined,
114+
// caller can prevent error log. Allows getSecrets to provide better errors
115+
supressLog?: boolean,
116+
): GraphTokenResponse {
117+
const errors = incorrectArgumentsErrors(event)
118+
119+
if (errors) {
120+
if (!supressLog) {
121+
logErrors(errors)
122+
}
123+
return { errors }
124+
}
125+
126+
return event ? graphTokenFromEvent(event) : graphTokenFromEnv()
127+
}

src/lib/secrets_helper.ts

Lines changed: 35 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
1-
import { Buffer } from 'buffer'
2-
import { request } from 'https'
3-
import { env } from 'process'
4-
5-
import { Event as HandlerEvent } from '../function/event'
1+
import { graphRequest } from './graph_request'
2+
import { getNetlifyGraphToken, GraphTokenResponseError, HasHeaders } from './graph_token'
63

74
const services = {
85
gitHub: null,
@@ -40,7 +37,7 @@ export type NetlifySecrets = {
4037
[K in ServiceKey]?: Service
4138
} & { [key: string]: Service }
4239

43-
type OneGraphSecretsResponse = {
40+
type GraphSecretsResponse = {
4441
data?: {
4542
me?: {
4643
serviceMetadata?: {
@@ -50,8 +47,6 @@ type OneGraphSecretsResponse = {
5047
}
5148
}
5249

53-
const siteId = env.SITE_ID
54-
5550
const camelize = function (text: string) {
5651
const safe = text.replace(/[-_\s.]+(.)?/g, (_, sub) => (sub ? sub.toUpperCase() : ''))
5752
return safe.slice(0, 1).toLowerCase() + safe.slice(1)
@@ -69,56 +64,7 @@ const serviceNormalizeOverrides: ServiceNormalizeOverrides = {
6964
GITHUB: 'gitHub',
7065
}
7166

72-
const oneGraphRequest = function (secretToken: string, requestBody: Uint8Array): Promise<OneGraphSecretsResponse> {
73-
return new Promise((resolve, reject) => {
74-
const port = 443
75-
76-
const options = {
77-
host: 'serve.onegraph.com',
78-
path: `/graphql?app_id=${siteId}`,
79-
port,
80-
method: 'POST',
81-
headers: {
82-
Authorization: `Bearer ${secretToken}`,
83-
'Content-Type': 'application/json',
84-
Accept: 'application/json',
85-
'Content-Length': requestBody ? Buffer.byteLength(requestBody) : 0,
86-
},
87-
}
88-
89-
const req = request(options, (res) => {
90-
if (res.statusCode !== 200) {
91-
return reject(new Error(String(res.statusCode)))
92-
}
93-
94-
const body: Array<Uint8Array> = []
95-
96-
res.on('data', (chunk) => {
97-
body.push(chunk)
98-
})
99-
100-
res.on('end', () => {
101-
const data = Buffer.concat(body).toString()
102-
try {
103-
const result: OneGraphSecretsResponse = JSON.parse(data)
104-
resolve(result)
105-
} catch (error) {
106-
reject(error)
107-
}
108-
})
109-
})
110-
111-
req.on('error', (error) => {
112-
reject(error)
113-
})
114-
115-
req.write(requestBody)
116-
117-
req.end()
118-
})
119-
}
120-
121-
const formatSecrets = (result: OneGraphSecretsResponse | undefined) => {
67+
const formatSecrets = (result: GraphSecretsResponse | undefined) => {
12268
const responseServices = result?.data?.me?.serviceMetadata?.loggedInServices
12369

12470
if (!responseServices) {
@@ -133,20 +79,38 @@ const formatSecrets = (result: OneGraphSecretsResponse | undefined) => {
13379
return newSecrets
13480
}
13581

136-
type OneGraphPayload = { authlifyToken: string | undefined }
137-
138-
export type HandlerEventWithOneGraph = HandlerEvent & OneGraphPayload
82+
const logErrors = function (errors: GraphTokenResponseError[]) {
83+
for (const error of errors) {
84+
let errorMessage
85+
switch (error.type) {
86+
case 'missing-event-in-function':
87+
errorMessage =
88+
'You must provide an event or request to `getSecrets` when used in functions and on-demand builders.'
89+
break
90+
case 'provided-event-in-build':
91+
errorMessage = 'You must not pass arguments to `getSecrets` when used in builds.'
92+
break
93+
default: {
94+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
95+
const exhaustiveCheck: never = error.type
96+
errorMessage = error.type
97+
break
98+
}
99+
}
100+
const message: string = errorMessage
101+
console.error(message)
102+
}
103+
}
139104

140105
// Note: We may want to have configurable "sets" of secrets,
141106
// e.g. "dev" and "prod"
142-
export const getSecrets = async (
143-
event?: HandlerEventWithOneGraph | HandlerEvent | undefined,
144-
): Promise<NetlifySecrets> => {
145-
// Allow us to get the token from event if present, else fallback to checking the env
146-
const eventToken = (event as HandlerEventWithOneGraph)?.authlifyToken
147-
const secretToken = eventToken || env.ONEGRAPH_AUTHLIFY_TOKEN
148-
149-
if (!secretToken) {
107+
export const getSecrets = async (event?: HasHeaders | null | undefined): Promise<NetlifySecrets> => {
108+
const graphTokenResponse = getNetlifyGraphToken(event, true)
109+
const graphToken = graphTokenResponse.token
110+
if (!graphToken) {
111+
if (graphTokenResponse.errors) {
112+
logErrors(graphTokenResponse.errors)
113+
}
150114
return {}
151115
}
152116

@@ -182,7 +146,8 @@ export const getSecrets = async (
182146
const body = JSON.stringify({ query: doc })
183147

184148
// eslint-disable-next-line node/no-unsupported-features/node-builtins
185-
const result = await oneGraphRequest(secretToken, new TextEncoder().encode(body))
149+
const resultBody = await graphRequest(graphToken, new TextEncoder().encode(body))
150+
const result: GraphSecretsResponse = JSON.parse(resultBody)
186151

187152
const newSecrets = formatSecrets(result)
188153

0 commit comments

Comments
 (0)