Host-remote refresh token implementation (#18044)

This commit is contained in:
nitin
2026-02-25 00:07:29 +05:30
committed by GitHub
parent b56f85f36a
commit 887371054a
22 changed files with 1234 additions and 59 deletions
@@ -0,0 +1,529 @@
import { transform } from 'esbuild';
import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { pathToFileURL } from 'node:url';
import {
afterAll,
beforeAll,
beforeEach,
describe,
expect,
it,
vi,
} from 'vitest';
vi.mock('twenty-shared/application', () => ({
DEFAULT_APP_ACCESS_TOKEN_NAME: 'TWENTY_APP_ACCESS_TOKEN',
DEFAULT_API_KEY_NAME: 'TWENTY_API_KEY',
DEFAULT_API_URL_NAME: 'TWENTY_API_URL',
GENERATED_DIR: 'generated',
}));
import { ClientService } from '@/cli/utilities/client/client-service';
type TwentyClassType = new (options?: {
url?: string;
metadataUrl?: string;
fetch?: typeof globalThis.fetch;
}) => {
query: (request: Record<string, unknown>) => Promise<unknown>;
uploadFile: (
fileBuffer: Buffer,
filename: string,
contentType: string,
fieldMetadataUniversalIdentifier: string,
) => Promise<{
id: string;
path: string;
size: number;
createdAt: string;
url: string;
}>;
};
const stubGeneratedIndexSource = `
export type QueryGenqlSelection = Record<string, unknown>
export type MutationGenqlSelection = Record<string, unknown>
export type GraphqlOperation = Record<string, unknown>
export type ClientOptions = {
url?: string
metadataUrl?: string
headers?: HeadersInit | (() => HeadersInit | Promise<HeadersInit>)
fetcher?: (operation: GraphqlOperation | GraphqlOperation[]) => Promise<unknown>
fetch?: typeof globalThis.fetch
batch?: unknown
}
export type Client = {
query: (request: QueryGenqlSelection & { __name?: string }) => Promise<unknown>
mutation: (
request: MutationGenqlSelection & { __name?: string },
) => Promise<unknown>
}
export class GenqlError extends Error {
constructor(
public readonly errors: unknown,
public readonly data: unknown,
) {
super('GenqlError')
}
}
export const createClient = (options: ClientOptions): Client => {
return {
query: (request) => {
return options.fetcher?.({
query: 'query',
variables: request,
})
},
mutation: (request) => {
return options.fetcher?.({
query: 'mutation',
variables: request,
})
},
}
}
`;
const createJsonResponse = ({
body,
status = 200,
statusText = 'OK',
}: {
body: unknown;
status?: number;
statusText?: string;
}) =>
new Response(JSON.stringify(body), {
status,
statusText,
headers: { 'Content-Type': 'application/json' },
});
const getAuthorizationHeaderValue = (requestInit: RequestInit | undefined) => {
return new Headers(requestInit?.headers).get('Authorization');
};
describe('ClientService generated Twenty auth behavior', () => {
let temporaryGeneratedClientDirectory: string;
let TwentyClass: TwentyClassType;
beforeAll(async () => {
temporaryGeneratedClientDirectory = await mkdtemp(
join(tmpdir(), 'twenty-generated-client-'),
);
const temporaryGeneratedIndexTsPath = join(
temporaryGeneratedClientDirectory,
'index.ts',
);
await writeFile(temporaryGeneratedIndexTsPath, stubGeneratedIndexSource);
const clientService = new ClientService();
await (
clientService as unknown as {
injectTwentyClient: (output: string) => Promise<void>;
}
).injectTwentyClient(temporaryGeneratedClientDirectory);
const generatedIndexContent = await readFile(
temporaryGeneratedIndexTsPath,
'utf-8',
);
const transpiledModule = await transform(generatedIndexContent, {
loader: 'ts',
format: 'esm',
target: 'es2022',
});
const temporaryGeneratedIndexMjsPath = join(
temporaryGeneratedClientDirectory,
'index.mjs',
);
await writeFile(temporaryGeneratedIndexMjsPath, transpiledModule.code);
const generatedModule = await import(
`${pathToFileURL(temporaryGeneratedIndexMjsPath).href}?t=${Date.now()}`
);
TwentyClass = generatedModule.default as TwentyClassType;
});
beforeEach(() => {
delete process.env.TWENTY_APP_ACCESS_TOKEN;
delete process.env.TWENTY_API_KEY;
delete (globalThis as Record<string, unknown>)
.frontComponentHostCommunicationApi;
});
afterAll(async () => {
if (temporaryGeneratedClientDirectory) {
await rm(temporaryGeneratedClientDirectory, {
recursive: true,
force: true,
});
}
});
it('uses TWENTY_APP_ACCESS_TOKEN before TWENTY_API_KEY', async () => {
process.env.TWENTY_APP_ACCESS_TOKEN = 'application-token';
process.env.TWENTY_API_KEY = 'api-key-token';
const capturedAuthorizationHeaders: string[] = [];
const fetchMock = vi.fn(
async (_url: string | URL | Request, requestInit?: RequestInit) => {
const authorizationHeaderValue =
getAuthorizationHeaderValue(requestInit);
if (authorizationHeaderValue) {
capturedAuthorizationHeaders.push(authorizationHeaderValue);
}
return createJsonResponse({
body: { data: { record: { id: 'record-id' } } },
});
},
);
const twentyClient = new TwentyClass({
url: 'https://example.com/graphql',
fetch: fetchMock as unknown as typeof globalThis.fetch,
});
await twentyClient.query({ record: { id: true } });
expect(capturedAuthorizationHeaders).toEqual(['Bearer application-token']);
});
it('falls back to TWENTY_API_KEY when TWENTY_APP_ACCESS_TOKEN is absent', async () => {
process.env.TWENTY_API_KEY = 'legacy-api-key-token';
const capturedAuthorizationHeaders: string[] = [];
const fetchMock = vi.fn(
async (_url: string | URL | Request, requestInit?: RequestInit) => {
const authorizationHeaderValue =
getAuthorizationHeaderValue(requestInit);
if (authorizationHeaderValue) {
capturedAuthorizationHeaders.push(authorizationHeaderValue);
}
return createJsonResponse({
body: { data: { record: { id: 'record-id' } } },
});
},
);
const twentyClient = new TwentyClass({
url: 'https://example.com/graphql',
fetch: fetchMock as unknown as typeof globalThis.fetch,
});
await twentyClient.query({ record: { id: true } });
expect(capturedAuthorizationHeaders).toEqual([
'Bearer legacy-api-key-token',
]);
});
it('refreshes and retries once after auth error when refresh callback is available', async () => {
process.env.TWENTY_APP_ACCESS_TOKEN = 'stale-token';
const requestAccessTokenRefresh = vi
.fn<() => Promise<string>>()
.mockResolvedValue('fresh-token');
(globalThis as Record<string, unknown>).frontComponentHostCommunicationApi =
{
requestAccessTokenRefresh,
};
const fetchMock = vi.fn(
async (_url: string | URL | Request, requestInit?: RequestInit) => {
const authorizationHeaderValue =
getAuthorizationHeaderValue(requestInit);
if (authorizationHeaderValue === 'Bearer stale-token') {
return createJsonResponse({
body: {
errors: [
{
extensions: { code: 'UNAUTHENTICATED' },
message: 'Unauthorized',
},
],
},
});
}
return createJsonResponse({
body: { data: { record: { id: 'record-id' } } },
});
},
);
const twentyClient = new TwentyClass({
url: 'https://example.com/graphql',
fetch: fetchMock as unknown as typeof globalThis.fetch,
});
const queryResult = await twentyClient.query({ record: { id: true } });
expect(queryResult).toEqual({ data: { record: { id: 'record-id' } } });
expect(requestAccessTokenRefresh).toHaveBeenCalledTimes(1);
expect(fetchMock).toHaveBeenCalledTimes(2);
expect(process.env.TWENTY_APP_ACCESS_TOKEN).toBe('fresh-token');
expect(process.env.TWENTY_API_KEY).toBe('fresh-token');
});
it('deduplicates concurrent token refresh requests', async () => {
process.env.TWENTY_APP_ACCESS_TOKEN = 'stale-token';
const requestAccessTokenRefresh = vi
.fn<() => Promise<string>>()
.mockImplementation(async () => {
await Promise.resolve();
return 'fresh-token';
});
(globalThis as Record<string, unknown>).frontComponentHostCommunicationApi =
{
requestAccessTokenRefresh,
};
const fetchMock = vi.fn(
async (_url: string | URL | Request, requestInit?: RequestInit) => {
const authorizationHeaderValue =
getAuthorizationHeaderValue(requestInit);
if (authorizationHeaderValue === 'Bearer stale-token') {
return createJsonResponse({
body: {
errors: [
{
extensions: { code: 'UNAUTHENTICATED' },
message: 'Unauthorized',
},
],
},
});
}
return createJsonResponse({
body: { data: { record: { id: 'record-id' } } },
});
},
);
const twentyClient = new TwentyClass({
url: 'https://example.com/graphql',
fetch: fetchMock as unknown as typeof globalThis.fetch,
});
await Promise.all([
twentyClient.query({ record: { id: true } }),
twentyClient.query({ record: { id: true } }),
]);
expect(requestAccessTokenRefresh).toHaveBeenCalledTimes(1);
expect(fetchMock).toHaveBeenCalledTimes(4);
});
it('retries uploadFile once after 401 when refresh callback is available', async () => {
process.env.TWENTY_APP_ACCESS_TOKEN = 'stale-token';
const requestAccessTokenRefresh = vi
.fn<() => Promise<string>>()
.mockResolvedValue('fresh-token');
(globalThis as Record<string, unknown>).frontComponentHostCommunicationApi =
{
requestAccessTokenRefresh,
};
const fetchMock = vi.fn(
async (_url: string | URL | Request, requestInit?: RequestInit) => {
const authorizationHeaderValue =
getAuthorizationHeaderValue(requestInit);
if (authorizationHeaderValue === 'Bearer stale-token') {
return createJsonResponse({
body: { message: 'Unauthorized' },
status: 401,
statusText: 'Unauthorized',
});
}
return createJsonResponse({
body: {
data: {
uploadFilesFieldFileByUniversalIdentifier: {
id: 'uploaded-file-id',
path: 'test/path.txt',
size: 10,
createdAt: '2026-02-24T00:00:00.000Z',
url: 'https://example.com/test/path.txt',
},
},
},
});
},
);
const twentyClient = new TwentyClass({
url: 'https://example.com/graphql',
metadataUrl: 'https://example.com/metadata',
fetch: fetchMock as unknown as typeof globalThis.fetch,
});
const uploadResult = await twentyClient.uploadFile(
Buffer.from('content'),
'test.txt',
'text/plain',
'field-uuid',
);
expect(uploadResult.id).toBe('uploaded-file-id');
expect(requestAccessTokenRefresh).toHaveBeenCalledTimes(1);
expect(fetchMock).toHaveBeenCalledTimes(2);
});
it('bubbles auth error when refresh callback throws', async () => {
process.env.TWENTY_APP_ACCESS_TOKEN = 'stale-token';
const requestAccessTokenRefresh = vi
.fn<() => Promise<string>>()
.mockRejectedValue(new Error('refresh failed'));
(globalThis as Record<string, unknown>).frontComponentHostCommunicationApi =
{
requestAccessTokenRefresh,
};
const consoleErrorSpy = vi
.spyOn(console, 'error')
.mockImplementation(() => undefined);
const fetchMock = vi.fn(async () => {
return createJsonResponse({
body: { message: 'Unauthorized' },
status: 401,
statusText: 'Unauthorized',
});
});
const twentyClient = new TwentyClass({
url: 'https://example.com/graphql',
fetch: fetchMock as unknown as typeof globalThis.fetch,
});
await expect(twentyClient.query({ record: { id: true } })).rejects.toThrow(
'Unauthorized',
);
expect(requestAccessTokenRefresh).toHaveBeenCalledTimes(1);
expect(fetchMock).toHaveBeenCalledTimes(1);
expect(consoleErrorSpy).toHaveBeenCalledWith(
'Twenty client: token refresh failed',
expect.any(Error),
);
consoleErrorSpy.mockRestore();
});
it('bubbles auth error when refresh callback returns an empty token', async () => {
process.env.TWENTY_APP_ACCESS_TOKEN = 'stale-token';
const requestAccessTokenRefresh = vi
.fn<() => Promise<string>>()
.mockResolvedValue('');
(globalThis as Record<string, unknown>).frontComponentHostCommunicationApi =
{
requestAccessTokenRefresh,
};
const fetchMock = vi.fn(async () => {
return createJsonResponse({
body: { message: 'Unauthorized' },
status: 401,
statusText: 'Unauthorized',
});
});
const twentyClient = new TwentyClass({
url: 'https://example.com/graphql',
fetch: fetchMock as unknown as typeof globalThis.fetch,
});
await expect(twentyClient.query({ record: { id: true } })).rejects.toThrow(
'Unauthorized',
);
expect(requestAccessTokenRefresh).toHaveBeenCalledTimes(1);
expect(fetchMock).toHaveBeenCalledTimes(1);
});
it('bubbles auth error without retry when refresh callback is unavailable', async () => {
process.env.TWENTY_APP_ACCESS_TOKEN = 'stale-token';
const fetchMock = vi.fn(async () => {
return createJsonResponse({
body: { message: 'Unauthorized' },
status: 401,
statusText: 'Unauthorized',
});
});
const twentyClient = new TwentyClass({
url: 'https://example.com/graphql',
fetch: fetchMock as unknown as typeof globalThis.fetch,
});
await expect(twentyClient.query({ record: { id: true } })).rejects.toThrow(
'Unauthorized',
);
expect(fetchMock).toHaveBeenCalledTimes(1);
});
it('calls global fetch with globalThis binding when no custom fetch is provided', async () => {
process.env.TWENTY_APP_ACCESS_TOKEN = 'application-token';
const originalFetch = globalThis.fetch;
const capturedFetchThisValues: unknown[] = [];
const fetchMock = vi.fn(function (
this: unknown,
_url: string | URL | Request,
_requestInit?: RequestInit,
) {
capturedFetchThisValues.push(this);
return Promise.resolve(
createJsonResponse({
body: { data: { record: { id: 'record-id' } } },
}),
);
}) as unknown as typeof globalThis.fetch;
globalThis.fetch = fetchMock;
try {
const twentyClient = new TwentyClass({
url: 'https://example.com/graphql',
});
await twentyClient.query({ record: { id: true } });
expect(fetchMock).toHaveBeenCalledTimes(1);
expect(capturedFetchThisValues).toEqual([globalThis]);
} finally {
globalThis.fetch = originalFetch;
}
});
});
@@ -3,6 +3,7 @@ import { generate } from '@genql/cli';
import * as fs from 'fs-extra';
import { join } from 'path';
import {
DEFAULT_APP_ACCESS_TOKEN_NAME,
DEFAULT_API_KEY_NAME,
DEFAULT_API_URL_NAME,
GENERATED_DIR,
@@ -65,12 +66,108 @@ export class ClientService {
// Custom Twenty client (auto-injected)
// ----------------------------------------------------
const defaultOptions: ClientOptions = {
const APP_ACCESS_TOKEN_ENV_KEY = '${DEFAULT_APP_ACCESS_TOKEN_NAME}';
const API_KEY_ENV_KEY = '${DEFAULT_API_KEY_NAME}';
type TwentyClientOptions = ClientOptions & {
metadataUrl?: string;
}
type ProcessEnvironment = Record<string, string | undefined>
type GraphqlError = {
message?: string;
extensions?: { code?: string };
}
type GraphqlResponsePayload = {
data?: Record<string, unknown>
errors?: GraphqlError[];
}
type GraphqlResponse = {
status: number;
statusText: string;
payload: GraphqlResponsePayload | null;
rawBody: string;
}
const getProcessEnvironment = (): ProcessEnvironment => {
const processObject = (globalThis as { process?: { env?: ProcessEnvironment } })
.process;
return processObject?.env ?? {};
}
const getTokenFromAuthorizationHeader = (
authorizationHeader: string | undefined,
): string | null => {
if (typeof authorizationHeader !== 'string') {
return null;
}
const trimmedAuthorizationHeader = authorizationHeader.trim();
if (trimmedAuthorizationHeader.length === 0) {
return null;
}
if (trimmedAuthorizationHeader === 'Bearer') {
return null;
}
if (trimmedAuthorizationHeader.startsWith('Bearer ')) {
return trimmedAuthorizationHeader.slice('Bearer '.length).trim();
}
return trimmedAuthorizationHeader;
}
const getTokenFromHeaders = (headers: HeadersInit | undefined): string | null => {
if (!headers) {
return null;
}
if (headers instanceof Headers) {
return getTokenFromAuthorizationHeader(headers.get('Authorization') ?? undefined);
}
if (Array.isArray(headers)) {
const matchedAuthorizationHeader = headers.find(
([headerName]) => headerName.toLowerCase() === 'authorization',
);
return getTokenFromAuthorizationHeader(matchedAuthorizationHeader?.[1]);
}
const headersRecord = headers as Record<string, string | undefined>;
return getTokenFromAuthorizationHeader(
headersRecord.Authorization ?? headersRecord.authorization,
);
}
const hasAuthenticationErrorInGraphqlPayload = (
payload: GraphqlResponsePayload | null,
): boolean => {
if (!payload?.errors) {
return false;
}
return payload.errors.some((error) => {
return (
error.extensions?.code === 'UNAUTHENTICATED' ||
// Fallback for payloads that don't provide structured error codes.
error.message?.toLowerCase() === 'unauthorized'
);
});
}
const defaultOptions: TwentyClientOptions = {
url: \`\${process.env.${DEFAULT_API_URL_NAME}}/graphql\`,
metadataUrl: \`\${process.env.${DEFAULT_API_URL_NAME}}/metadata\`,
headers: {
'Content-Type': 'application/json',
Authorization: \`Bearer \${process.env.${DEFAULT_API_KEY_NAME}}\`,
},
}
@@ -78,21 +175,54 @@ export default class Twenty {
private client: Client;
private url: string;
private metadataUrl: string;
private authorizationToken: string;
private requestOptions: RequestInit;
private headers: HeadersInit | (() => HeadersInit | Promise<HeadersInit>);
private fetchImplementation: typeof globalThis.fetch | null;
private authorizationToken: string | null;
private refreshAccessTokenPromise: Promise<string | null> | null = null;
constructor(options?: ClientOptions) {
const merged: ClientOptions = {
constructor(options?: TwentyClientOptions) {
const merged: TwentyClientOptions = {
...defaultOptions,
...options,
headers: {
...defaultOptions.headers,
...(options?.headers ?? {}),
},
};
this.client = createClient(merged);
this.url = merged.url;
this.metadataUrl = merged.metadataUrl;
this.authorizationToken = merged.headers.Authorization;
}
const {
url,
metadataUrl,
headers,
fetch: customFetchImplementation,
fetcher: _fetcher,
batch: _batch,
...requestOptions
} = merged;
this.url = url ?? '';
this.metadataUrl = metadataUrl ?? this.url.replace(/\\/graphql$/, '/metadata');
this.requestOptions = requestOptions;
this.headers = headers ?? {};
this.fetchImplementation = customFetchImplementation ?? globalThis.fetch ?? null;
const processEnvironment = getProcessEnvironment();
const tokenFromHeaders = getTokenFromHeaders(
typeof headers === 'function' ? undefined : headers,
);
// Priority: explicit header > TWENTY_APP_ACCESS_TOKEN > TWENTY_API_KEY (legacy fallback).
this.authorizationToken =
tokenFromHeaders ??
processEnvironment[APP_ACCESS_TOKEN_ENV_KEY] ??
processEnvironment[API_KEY_ENV_KEY] ??
null;
this.client = createClient({
...merged,
headers: undefined,
fetcher: async (operation) =>
this.executeGraphqlRequestWithOptionalRefresh({
operation,
}),
});
}
query<R extends QueryGenqlSelection>(request: R & { __name?: string }) {
@@ -129,21 +259,208 @@ export default class Twenty {
form.append('map', JSON.stringify({ '0': ['variables.file'] }));
form.append('0', new Blob([fileBuffer], { type: contentType }), filename);
const response = await fetch(this.metadataUrl, {
method: 'POST',
headers: {
Authorization: this.authorizationToken,
const result = await this.executeGraphqlRequestWithOptionalRefresh({
operation: form,
url: this.metadataUrl,
headers: {},
requestInit: {
method: 'POST',
},
body: form,
});
const result = await response.json();
if (result.errors) {
throw new GenqlError(result.errors, result.data);
}
return result.data.uploadFilesFieldFileByUniversalIdentifier;
const data = result.data as Record<string, unknown>;
return data.uploadFilesFieldFileByUniversalIdentifier as {
id: string;
path: string;
size: number;
createdAt: string;
url: string;
}
}
private async executeGraphqlRequestWithOptionalRefresh({
operation,
url = this.url,
headers,
requestInit,
}: {
operation: GraphqlOperation | GraphqlOperation[] | FormData;
url?: string;
headers?: HeadersInit;
requestInit?: RequestInit;
}) {
const firstResponse = await this.executeGraphqlRequest({
operation,
url,
headers,
requestInit,
token: this.authorizationToken,
});
if (this.shouldRefreshToken(firstResponse)) {
const refreshedAccessToken = await this.requestRefreshedAccessToken();
if (refreshedAccessToken) {
const retryResponse = await this.executeGraphqlRequest({
operation,
url,
headers,
requestInit,
token: refreshedAccessToken,
});
return this.assertResponseIsSuccessful(retryResponse);
}
}
return this.assertResponseIsSuccessful(firstResponse);
}
private async executeGraphqlRequest({
operation,
url,
headers,
requestInit,
token,
}: {
operation: GraphqlOperation | GraphqlOperation[] | FormData;
url: string;
headers?: HeadersInit;
requestInit?: RequestInit;
token: string | null;
}): Promise<GraphqlResponse> {
if (!this.fetchImplementation) {
throw new Error(
'Global \`fetch\` function is not available, pass a fetch implementation to the Twenty client',
);
}
const resolvedHeaders = await this.resolveHeaders();
const requestHeaders = new Headers(resolvedHeaders);
if (headers) {
new Headers(headers).forEach((value, key) => requestHeaders.set(key, value));
}
if (operation instanceof FormData) {
requestHeaders.delete('Content-Type');
} else {
requestHeaders.set('Content-Type', 'application/json');
}
if (token) {
requestHeaders.set('Authorization', \`Bearer \${token}\`);
} else {
requestHeaders.delete('Authorization');
}
const response = await this.fetchImplementation.call(globalThis, url, {
...this.requestOptions,
...requestInit,
method: requestInit?.method ?? 'POST',
headers: requestHeaders,
body: operation instanceof FormData ? operation : JSON.stringify(operation),
});
const rawBody = await response.text();
let payload: GraphqlResponsePayload | null = null;
if (rawBody.trim().length > 0) {
try {
payload = JSON.parse(rawBody) as GraphqlResponsePayload;
} catch {
payload = null;
}
}
return {
status: response.status,
statusText: response.statusText,
payload,
rawBody,
}
}
private async resolveHeaders(): Promise<HeadersInit> {
if (typeof this.headers === 'function') {
return (await this.headers()) ?? {};
}
return this.headers ?? {};
}
private shouldRefreshToken(response: GraphqlResponse): boolean {
if (response.status === 401) {
return true;
}
return hasAuthenticationErrorInGraphqlPayload(response.payload);
}
private assertResponseIsSuccessful(response: GraphqlResponse) {
if (response.status < 200 || response.status >= 300) {
throw new Error(\`\${response.statusText}: \${response.rawBody}\`);
}
if (response.payload === null) {
throw new Error('Invalid JSON response');
}
return response.payload;
}
private async requestRefreshedAccessToken(): Promise<string | null> {
const refreshAccessTokenFunction = (
globalThis as {
frontComponentHostCommunicationApi?: {
requestAccessTokenRefresh?: () => Promise<string>
}
}
).frontComponentHostCommunicationApi?.requestAccessTokenRefresh;
if (typeof refreshAccessTokenFunction !== 'function') {
return null;
}
if (!this.refreshAccessTokenPromise) {
this.refreshAccessTokenPromise = refreshAccessTokenFunction()
.then((refreshedAccessToken) => {
if (
typeof refreshedAccessToken !== 'string' ||
refreshedAccessToken.length === 0
) {
return null;
}
this.setAuthorizationToken(refreshedAccessToken);
return refreshedAccessToken;
})
.catch((error) => {
console.error('Twenty client: token refresh failed', error);
return null;
})
.finally(() => {
this.refreshAccessTokenPromise = null;
});
}
return this.refreshAccessTokenPromise;
}
private setAuthorizationToken(token: string) {
this.authorizationToken = token;
const processEnvironment = getProcessEnvironment();
processEnvironment[APP_ACCESS_TOKEN_ENV_KEY] = token;
processEnvironment[API_KEY_ENV_KEY] = token;
}
}